You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
241 lines
9.7 KiB
241 lines
9.7 KiB
/**
|
|
* 部署后首次启动前执行:
|
|
* 1. 若库中尚无 admin,则用环境变量创建首个管理员。
|
|
* 2. 若库中尚无 agent,则种入 default + coder agent 及工具集(对齐 seed.ts)。
|
|
* 使用 SQLite(与 `migrate-sqlite.js` 相同)。
|
|
*
|
|
* 环境变量(与 seed.ts 一致):
|
|
* - DATABASE_URL:SQLite 路径,可为 `file:/path/to/db.sqlite` 或裸路径
|
|
* - BOOTSTRAP_ADMIN_USERNAME / BOOTSTRAP_ADMIN_PASSWORD:可选;未设置或校验失败则跳过
|
|
*/
|
|
import { createClient } from '@libsql/client'
|
|
import { hash } from 'bcryptjs'
|
|
import { mkdirSync } from 'node:fs'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
/** 与 `server/service/auth/index.ts` 一致 */
|
|
const USERNAME_REGEX = /^[a-zA-Z0-9_]{3,20}$/
|
|
const MIN_PASSWORD_LENGTH = 6
|
|
|
|
/**
|
|
* 与 `migrate-sqlite.js` 一致:相对路径相对 `process.cwd()`(例如 `.output/run.sh` 下与迁移写入同一文件)。
|
|
* Nitro 内 `resolveSqliteDatabaseUrl` 在无法锚定 drizzle-pkg 时也会回退到 cwd,避免「迁移/seed 成功、服务 CANTOPEN」。
|
|
*/
|
|
function resolveSqliteFilePath(dbUrl) {
|
|
const stripped = dbUrl.startsWith('file:') ? dbUrl.slice('file:'.length) : dbUrl
|
|
if (!stripped) {
|
|
throw new Error('DATABASE_URL 未设置,且未提供有效的 SQLite 文件路径')
|
|
}
|
|
if (path.isAbsolute(stripped)) {
|
|
return stripped
|
|
}
|
|
return path.resolve(process.cwd(), stripped)
|
|
}
|
|
|
|
function openClient() {
|
|
const dbUrl = process.env.DATABASE_URL || ''
|
|
const sqlitePath = resolveSqliteFilePath(dbUrl)
|
|
mkdirSync(path.dirname(sqlitePath), { recursive: true })
|
|
return createClient({ url: `file:${sqlitePath}` })
|
|
}
|
|
|
|
// ============ Agent / Tool seed 数据(对齐 packages/drizzle-pkg/seed.ts)============
|
|
const DEFAULT_SYSTEM_PROMPT =
|
|
'You are a helpful AI assistant. Answer questions concisely and accurately. When tools are available, use them proactively to provide better answers.'
|
|
|
|
const CODER_SYSTEM_PROMPT = `You are a professional coding assistant. You help users with programming questions, code review, debugging, and software design.
|
|
|
|
Guidelines:
|
|
- Provide clear, concise, and accurate answers
|
|
- When showing code, use proper markdown code blocks with language tags
|
|
- Proactively use available tools (fetch, search) to gather information when needed
|
|
- Explain your reasoning when making architectural or design decisions
|
|
- If you're unsure, say so rather than guessing
|
|
- Focus on practical, working solutions`
|
|
|
|
const TOOLS_SEED = [
|
|
{ id: 'tool_fetch_html', name: 'fetch_html', slug: 'fetch_html', description: 'Fetch a URL and return the HTML content', type: 'fetch', config: { parseMode: 'raw', timeout: 30000, allowedDomains: ['*'], blockedDomains: [], maxResponseSize: 102400, defaultMethod: 'GET', defaultHeaders: {} }, enabled: 1, needsApproval: 0, adminOnly: 0, sortOrder: 10 },
|
|
{ id: 'tool_fetch_md', name: 'fetch_md', slug: 'fetch_md', description: 'Fetch a URL and return the content as markdown', type: 'fetch', config: { parseMode: 'markdown', timeout: 30000, allowedDomains: ['*'], blockedDomains: [], maxResponseSize: 102400, defaultMethod: 'GET', defaultHeaders: {} }, enabled: 1, needsApproval: 0, adminOnly: 0, sortOrder: 11 },
|
|
{ id: 'tool_fetch_json', name: 'fetch_json', slug: 'fetch_json', description: 'Fetch a URL and return the JSON content', type: 'fetch', config: { parseMode: 'json', timeout: 30000, allowedDomains: ['*'], blockedDomains: [], maxResponseSize: 102400, defaultMethod: 'GET', defaultHeaders: {} }, enabled: 1, needsApproval: 0, adminOnly: 0, sortOrder: 12 },
|
|
{ id: 'tool_user_info', name: 'user-info', slug: 'user-info', description: 'Get current user information', type: 'user-info', config: {}, enabled: 1, needsApproval: 0, adminOnly: 0, sortOrder: 80 },
|
|
{ id: 'tool_search', name: 'search', slug: 'search', description: 'Search the web for information', type: 'search', config: { maxResults: 5 }, enabled: 1, needsApproval: 0, adminOnly: 0, sortOrder: 90 },
|
|
{ id: 'tool_document', name: 'document', slug: 'document', description: '创建、读取、更新、删除和列出文档,用于记忆管理和交付物产出', type: 'document', config: { approvalRequiredActions: ['delete'], maxContentLength: 10000, defaultTokenBudget: 2000, maxListSize: 20 }, enabled: 1, needsApproval: 1, adminOnly: 0, sortOrder: 150 },
|
|
{ id: 'tool_agent_invoke', name: 'agent-invoke', slug: 'agent-invoke', description: '调用其他 Agent 处理子任务,实现多 Agent 协作', type: 'agent-invoke', config: { maxRecursionDepth: 3, resultTruncateLength: 8000, timeoutMs: 60000, maxOutputTokens: 8192 }, enabled: 1, needsApproval: 0, adminOnly: 0, sortOrder: 160 },
|
|
]
|
|
|
|
const AGENTS_SEED = [
|
|
{
|
|
slug: 'default',
|
|
name: '默认助手',
|
|
description: '通用 AI 助手',
|
|
systemPrompt: DEFAULT_SYSTEM_PROMPT,
|
|
titleStrategy: 'llm',
|
|
enableThinking: 0,
|
|
enableTools: 1,
|
|
maxStepCount: 6,
|
|
isDefault: 1,
|
|
isCallable: 0,
|
|
sortOrder: 0,
|
|
enabled: 1,
|
|
toolIds: TOOLS_SEED.map((t) => t.id),
|
|
},
|
|
{
|
|
slug: 'coder',
|
|
name: '编程助手',
|
|
description: '专注于编程问题的 AI 助手',
|
|
systemPrompt: CODER_SYSTEM_PROMPT,
|
|
titleStrategy: 'first-line',
|
|
enableThinking: 1,
|
|
enableTools: 1,
|
|
maxStepCount: 12,
|
|
isDefault: 0,
|
|
isCallable: 1,
|
|
sortOrder: 10,
|
|
enabled: 1,
|
|
toolIds: ['tool_fetch_md', 'tool_search'],
|
|
},
|
|
]
|
|
|
|
async function seedAdmin(db) {
|
|
const rs = await db.execute({
|
|
sql: `SELECT id FROM users WHERE role = ? LIMIT 1`,
|
|
args: ['admin'],
|
|
})
|
|
if (rs.rows.length > 0) {
|
|
console.log('Bootstrap skipped: admin exists')
|
|
return
|
|
}
|
|
|
|
const username = process.env.BOOTSTRAP_ADMIN_USERNAME
|
|
const password = process.env.BOOTSTRAP_ADMIN_PASSWORD
|
|
|
|
if (!username || !password) {
|
|
console.warn(
|
|
'Bootstrap skipped: set BOOTSTRAP_ADMIN_USERNAME and BOOTSTRAP_ADMIN_PASSWORD',
|
|
)
|
|
return
|
|
}
|
|
|
|
if (!USERNAME_REGEX.test(username) || password.length < MIN_PASSWORD_LENGTH) {
|
|
console.warn(
|
|
'Bootstrap skipped: invalid BOOTSTRAP_ADMIN_USERNAME or BOOTSTRAP_ADMIN_PASSWORD',
|
|
)
|
|
return
|
|
}
|
|
|
|
const passwordHash = await hash(password, 10)
|
|
const maxRs = await db.execute(`SELECT COALESCE(MAX(id), 0) AS maxId FROM users`)
|
|
const { maxId } = maxRs.rows[0]
|
|
const userId = (maxId ?? 0) + 1
|
|
|
|
try {
|
|
await db.execute({
|
|
sql: `
|
|
INSERT INTO users (id, username, password, role, status)
|
|
VALUES (@id, @username, @password, @role, @status)
|
|
`,
|
|
args: {
|
|
id: userId,
|
|
username,
|
|
password: passwordHash,
|
|
role: 'admin',
|
|
status: 'active',
|
|
},
|
|
})
|
|
console.log('Bootstrap complete: admin user created')
|
|
} catch (err) {
|
|
console.warn(
|
|
'Bootstrap skipped: could not insert admin (unique conflict or DB error)',
|
|
err,
|
|
)
|
|
}
|
|
}
|
|
|
|
async function seedAgentsAndTools(db) {
|
|
// 幂等:已有 agent 则跳过
|
|
const agentRs = await db.execute(`SELECT COUNT(*) AS cnt FROM agents`)
|
|
const agentCount = agentRs.rows[0]?.cnt ?? 0
|
|
if (agentCount > 0) {
|
|
console.log('Seed skipped: agents already exist')
|
|
return
|
|
}
|
|
|
|
// 1. 种入工具
|
|
const toolRs = await db.execute(`SELECT COUNT(*) AS cnt FROM agent_tools`)
|
|
const toolCount = toolRs.rows[0]?.cnt ?? 0
|
|
if (toolCount === 0) {
|
|
for (const t of TOOLS_SEED) {
|
|
await db.execute({
|
|
sql: `INSERT INTO agent_tools (id, name, slug, description, type, config, enabled, needs_approval, admin_only, sort_order)
|
|
VALUES (@id, @name, @slug, @description, @type, @config, @enabled, @needsApproval, @adminOnly, @sortOrder)`,
|
|
args: {
|
|
id: t.id,
|
|
name: t.name,
|
|
slug: t.slug,
|
|
description: t.description,
|
|
type: t.type,
|
|
config: JSON.stringify(t.config),
|
|
enabled: t.enabled,
|
|
needsApproval: t.needsApproval,
|
|
adminOnly: t.adminOnly,
|
|
sortOrder: t.sortOrder,
|
|
},
|
|
})
|
|
}
|
|
console.log(`Seeded ${TOOLS_SEED.length} tools`)
|
|
} else {
|
|
console.log(`Seed skipped: ${toolCount} tools already exist`)
|
|
}
|
|
|
|
// 2. 种入 agent + 工具关联
|
|
for (const a of AGENTS_SEED) {
|
|
const result = await db.execute({
|
|
sql: `INSERT INTO agents (slug, name, description, system_prompt, title_strategy, enable_thinking, enable_tools, max_step_count, is_default, is_callable, sort_order, enabled)
|
|
VALUES (@slug, @name, @description, @systemPrompt, @titleStrategy, @enableThinking, @enableTools, @maxStepCount, @isDefault, @isCallable, @sortOrder, @enabled)`,
|
|
args: {
|
|
slug: a.slug,
|
|
name: a.name,
|
|
description: a.description,
|
|
systemPrompt: a.systemPrompt,
|
|
titleStrategy: a.titleStrategy,
|
|
enableThinking: a.enableThinking,
|
|
enableTools: a.enableTools,
|
|
maxStepCount: a.maxStepCount,
|
|
isDefault: a.isDefault,
|
|
isCallable: a.isCallable,
|
|
sortOrder: a.sortOrder,
|
|
enabled: a.enabled,
|
|
},
|
|
})
|
|
const agentId = result.lastInsertRowid
|
|
for (let i = 0; i < a.toolIds.length; i++) {
|
|
await db.execute({
|
|
sql: `INSERT INTO agent_tool_associations (agent_id, tool_id, sort_order) VALUES (@agentId, @toolId, @sortOrder)`,
|
|
args: { agentId, toolId: a.toolIds[i], sortOrder: i },
|
|
})
|
|
}
|
|
console.log(`Seeded agent "${a.slug}" with ${a.toolIds.length} tool associations`)
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const db = openClient()
|
|
try {
|
|
await seedAdmin(db)
|
|
await seedAgentsAndTools(db)
|
|
} finally {
|
|
// @libsql/client 无需手动 close
|
|
}
|
|
}
|
|
|
|
const isMain =
|
|
process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
|
|
if (isMain) {
|
|
// 成功时自然退出(退出码 0);仅未捕获错误时 process.exit(1)。
|
|
main().catch((e) => {
|
|
console.error(e)
|
|
process.exit(1)
|
|
})
|
|
}
|
|
|