diff --git a/app/components/AgentToolFormModal.vue b/app/components/AgentToolFormModal.vue index dd7e88c..a1df3c2 100644 --- a/app/components/AgentToolFormModal.vue +++ b/app/components/AgentToolFormModal.vue @@ -33,6 +33,7 @@ const form = reactive({ config: props.tool?.config ?? "", inputSchema: "", enabled: props.tool ? props.tool.enabled === 1 : true, + needsApproval: props.tool ? props.tool.needsApproval === 1 : false, sortOrder: props.tool?.sortOrder ?? 0, }) @@ -90,6 +91,7 @@ async function save() { type: form.type, config: parsedConfig, enabled: form.enabled ? 1 : 0, + needsApproval: form.needsApproval ? 1 : 0, sortOrder: Number(form.sortOrder) || 0, } @@ -183,6 +185,12 @@ function onOverlayClick(e: MouseEvent) { 启用 +
+ +
diff --git a/app/composables/useLlmChat.ts b/app/composables/useLlmChat.ts index ce7a809..0e3e9bb 100644 --- a/app/composables/useLlmChat.ts +++ b/app/composables/useLlmChat.ts @@ -1,6 +1,6 @@ -import { processDataStream } from 'ai' +import { parseJsonEventStream, uiMessageChunkSchema } from 'ai' -export type MessagePartType = 'text' | 'reasoning' | 'tool-call' | 'tool-result' +export type MessagePartType = 'text' | 'reasoning' | 'tool-call' | 'tool-result' | 'tool-approval' export interface MessagePart { id: string @@ -10,7 +10,11 @@ export interface MessagePart { toolCallId?: string args?: unknown result?: unknown - state?: 'call' | 'result' + state?: 'call' | 'result' | 'approval-requested' | 'approval-responded' + approvalId?: string + approved?: boolean + approvalReason?: string + isAutomaticApproval?: boolean reasoningLoading?: boolean reasoningDuration?: number } @@ -73,6 +77,248 @@ export function useLlmChat(options: UseLlmChatOptions) { } } + function toUIMessageParts(msg: LlmChatMessage): any[] { + if (!msg.parts) return msg.content ? [{ type: 'text', text: msg.content }] : [] + const parts: any[] = [] + for (const p of msg.parts) { + if (p.type === 'text' && p.text) { + parts.push({ type: 'text', text: p.text }) + } else if (p.type === 'reasoning' && p.text) { + parts.push({ type: 'reasoning', text: p.text }) + } else if (p.type === 'tool-call') { + if (p.state === 'approval-requested') { + parts.push({ + type: `tool-${p.toolName}`, + toolCallId: p.toolCallId, + state: 'approval-requested', + input: p.args, + approval: { id: p.approvalId ?? '' }, + }) + } else if (p.state === 'approval-responded') { + parts.push({ + type: `tool-${p.toolName}`, + toolCallId: p.toolCallId, + state: 'approval-responded', + input: p.args, + approval: { + id: p.approvalId ?? '', + approved: p.approved ?? false, + reason: p.approvalReason, + }, + }) + } else if (p.state === 'result') { + parts.push({ + type: `tool-${p.toolName}`, + toolCallId: p.toolCallId, + state: 'output-available', + input: p.args, + output: p.result, + }) + } else { + parts.push({ + type: `tool-${p.toolName}`, + toolCallId: p.toolCallId, + state: 'input-available', + input: p.args, + }) + } + } else if (p.type === 'tool-approval') { + if (p.state === 'approval-responded') { + parts.push({ + type: `tool-${p.toolName}`, + toolCallId: p.toolCallId, + state: 'approval-responded', + input: p.args, + approval: { + id: p.approvalId ?? '', + approved: p.approved ?? false, + reason: p.approvalReason, + }, + }) + } + } + } + return parts + } + + function buildRequestBody(extraMessages?: LlmChatMessage[]) { + const allMessages = extraMessages ? [...messages.value, ...extraMessages] : messages.value + const uiMessages: any[] = [] + + if (systemPrompt?.()) { + uiMessages.push({ role: 'system', parts: [{ type: 'text', text: systemPrompt() }] }) + } + + for (const m of allMessages) { + if (!m.content && (!m.parts || m.parts.length === 0)) continue + uiMessages.push({ + role: m.role, + parts: toUIMessageParts(m), + }) + } + + return { + modelId: modelId(), + messages: uiMessages, + enableThinking: enableThinking?.() ?? false, + enableTools: enableTools?.() ?? false, + } + } + + async function processStream(res: Response, assistantIdx: number) { + if (!res.body) throw new Error('响应体为空') + + let reasoningStartTime: number | null = null + + const chunkStream = parseJsonEventStream({ + stream: res.body, + schema: uiMessageChunkSchema, + }) + + const reader = chunkStream.getReader() + for (;;) { + const { done, value: parsed } = await reader.read() + if (done) break + if (!parsed.success) continue + const chunk = parsed.value + + switch (chunk.type) { + case 'reasoning-start': { + const msg = messages.value[assistantIdx] + if (!msg) break + if (reasoningStartTime === null) reasoningStartTime = Date.now() + appendPart(msg, { id: generateId(), type: 'reasoning', text: '', reasoningLoading: true }) + break + } + case 'reasoning-delta': { + const msg = messages.value[assistantIdx] + if (!msg) break + const part = getOrCreateLastPart(msg, 'reasoning') + if (part) { + part.text = (part.text ?? '') + chunk.delta + } + break + } + case 'reasoning-end': { + const msg = messages.value[assistantIdx] + if (msg) updateLastReasoningDuration(msg) + break + } + case 'text-delta': { + const msg = messages.value[assistantIdx] + if (!msg) break + updateLastReasoningDuration(msg) + let part = getOrCreateLastPart(msg, 'text') + if (!part) { + part = { id: generateId(), type: 'text', text: '' } + appendPart(msg, part) + } + part.text = (part.text ?? '') + chunk.delta + msg.content += chunk.delta + break + } + case 'error': { + errorMessage.value = chunk.errorText || '流式响应出错' + break + } + case 'tool-input-available': { + const msg = messages.value[assistantIdx] + if (!msg) break + updateLastReasoningDuration(msg) + appendPart(msg, { + id: generateId(), + type: 'tool-call', + toolName: chunk.toolName, + toolCallId: chunk.toolCallId, + args: chunk.input, + state: 'call', + }) + break + } + case 'tool-output-available': { + const msg = messages.value[assistantIdx] + if (!msg || !msg.parts) break + const callPart = msg.parts.find(p => p.type === 'tool-call' && p.toolCallId === chunk.toolCallId) + if (callPart) { + callPart.result = chunk.output + callPart.state = 'result' + } + break + } + case 'tool-output-denied': { + const msg = messages.value[assistantIdx] + if (!msg || !msg.parts) break + const callPart = msg.parts.find(p => p.type === 'tool-call' && p.toolCallId === chunk.toolCallId) + if (callPart) { + callPart.state = 'result' + callPart.result = '工具执行被拒绝' + } + break + } + case 'tool-approval-request': { + const msg = messages.value[assistantIdx] + if (!msg) break + updateLastReasoningDuration(msg) + const isAutomatic = !!(chunk as any).isAutomatic + const existingPart = msg.parts?.find(p => p.type === 'tool-call' && p.toolCallId === chunk.toolCallId) + if (existingPart) { + existingPart.state = 'approval-requested' + existingPart.approvalId = chunk.approvalId + existingPart.isAutomaticApproval = isAutomatic + } else { + appendPart(msg, { + id: generateId(), + type: 'tool-call', + toolName: (chunk as any).toolName, + toolCallId: chunk.toolCallId, + args: (chunk as any).input, + state: 'approval-requested', + approvalId: chunk.approvalId, + isAutomaticApproval: isAutomatic, + }) + } + break + } + case 'tool-approval-response': { + const msg = messages.value[assistantIdx] + if (!msg || !msg.parts) break + const part = msg.parts.find(p => p.type === 'tool-call' && p.approvalId === chunk.approvalId) + if (part) { + part.state = 'approval-responded' + part.approved = chunk.approved + if (chunk.reason) part.approvalReason = chunk.reason + } + break + } + default: + break + } + } + + const msg = messages.value[assistantIdx] + if (msg) updateLastReasoningDuration(msg) + } + + function validateAssistantContent(assistantIdx: number) { + const finalMsg = messages.value[assistantIdx] + if (finalMsg && !errorMessage.value) { + const hasText = finalMsg.parts?.some(p => p.type === 'text' && p.text) + const hasToolCall = finalMsg.parts?.some(p => p.type === 'tool-call') + const hasPendingApproval = finalMsg.parts?.some(p => p.state === 'approval-requested' && !p.isAutomaticApproval) + if (hasPendingApproval) return + if (!hasText && !hasToolCall) { + errorMessage.value = '模型未返回任何内容(可能已达到工具调用次数上限或模型无响应)' + messages.value.splice(assistantIdx, 1) + } else if (!hasText && hasToolCall) { + finalMsg.parts?.push({ + id: generateId(), + type: 'text', + text: '(已达到工具调用次数上限,模型未能生成最终回答。以上是工具调用的尝试记录。)', + }) + } + } + } + async function sendMessage(text: string) { const trimmed = text.trim() const mid = modelId() @@ -95,23 +341,11 @@ export function useLlmChat(options: UseLlmChatOptions) { isLoading.value = true abortController = new AbortController() - let reasoningStartTime: number | null = null - try { const res = await fetch(apiEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - modelId: mid, - messages: [ - ...(systemPrompt?.() ? [{ role: 'system' as const, content: systemPrompt() }] : []), - ...messages.value - .filter(m => m.content) - .map(m => ({ role: m.role, content: m.content })), - ], - enableThinking: enableThinking?.() ?? false, - enableTools: enableTools?.() ?? false, - }), + body: JSON.stringify(buildRequestBody()), signal: abortController.signal, }) @@ -120,83 +354,8 @@ export function useLlmChat(options: UseLlmChatOptions) { throw new Error(errText || `请求失败 (${res.status})`) } - if (!res.body) { - throw new Error('响应体为空') - } - - await processDataStream({ - stream: res.body, - onReasoningPart: (text) => { - const msg = messages.value[assistantIdx] - if (!msg) return - let part = getOrCreateLastPart(msg, 'reasoning') - if (!part) { - if (reasoningStartTime === null) reasoningStartTime = Date.now() - part = { id: generateId(), type: 'reasoning', text: '', reasoningLoading: true } - appendPart(msg, part) - } - part.text = (part.text ?? '') + text - }, - onTextPart: (text) => { - const msg = messages.value[assistantIdx] - if (!msg) return - updateLastReasoningDuration(msg) - let part = getOrCreateLastPart(msg, 'text') - if (!part) { - part = { id: generateId(), type: 'text', text: '' } - appendPart(msg, part) - } - part.text = (part.text ?? '') + text - msg.content += text - }, - onErrorPart: (error) => { - errorMessage.value = error || '流式响应出错' - }, - onToolCallPart: (part) => { - const msg = messages.value[assistantIdx] - if (!msg) return - updateLastReasoningDuration(msg) - appendPart(msg, { - id: generateId(), - type: 'tool-call', - toolName: part.toolName, - toolCallId: part.toolCallId, - args: part.args, - state: 'call', - }) - }, - onToolResultPart: (part) => { - const msg = messages.value[assistantIdx] - if (!msg || !msg.parts) return - const callPart = msg.parts.find(p => p.type === 'tool-call' && p.toolCallId === part.toolCallId) - if (callPart) { - callPart.result = part.result - callPart.state = 'result' - } - }, - }) - - const msg = messages.value[assistantIdx] - if (msg) updateLastReasoningDuration(msg) - - // 检查是否有实际内容产出 - const finalMsg = messages.value[assistantIdx] - if (finalMsg && !errorMessage.value) { - const hasText = finalMsg.parts?.some(p => p.type === 'text' && p.text) - const hasToolCall = finalMsg.parts?.some(p => p.type === 'tool-call') - if (!hasText && !hasToolCall) { - // 完全无内容,移除空消息 - errorMessage.value = '模型未返回任何内容(可能已达到工具调用次数上限或模型无响应)' - messages.value.splice(assistantIdx, 1) - } else if (!hasText && hasToolCall) { - // 有工具调用但无最终文本回答(maxSteps 用完),保留工具记录,追加提示 - finalMsg.parts?.push({ - id: generateId(), - type: 'text', - text: '(已达到工具调用次数上限,模型未能生成最终回答。以上是工具调用的尝试记录。)', - }) - } - } + await processStream(res, assistantIdx) + validateAssistantContent(assistantIdx) } catch (err: any) { if (err.name === 'AbortError') { // user stopped @@ -213,6 +372,49 @@ export function useLlmChat(options: UseLlmChatOptions) { } } + async function respondToApproval(toolCallId: string, approved: boolean, reason?: string) { + if (isLoading.value) return + + const assistantMsg = messages.value.find(m => + m.parts?.some(p => p.toolCallId === toolCallId && p.state === 'approval-requested'), + ) + if (!assistantMsg) return + + const approvalPart = assistantMsg.parts?.find(p => p.toolCallId === toolCallId && p.state === 'approval-requested') + if (!approvalPart) return + + approvalPart.state = 'approval-responded' + approvalPart.approved = approved + approvalPart.approvalReason = reason + + isLoading.value = true + abortController = new AbortController() + + try { + const res = await fetch(apiEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(buildRequestBody()), + signal: abortController.signal, + }) + + if (!res.ok) { + const errText = await res.text() + throw new Error(errText || `请求失败 (${res.status})`) + } + + await processStream(res, messages.value.indexOf(assistantMsg)) + validateAssistantContent(messages.value.indexOf(assistantMsg)) + } catch (err: any) { + if (err.name !== 'AbortError') { + errorMessage.value = err.message || '请求失败' + } + } finally { + isLoading.value = false + abortController = null + } + } + function stopGeneration() { if (abortController) { abortController.abort() @@ -232,5 +434,6 @@ export function useLlmChat(options: UseLlmChatOptions) { sendMessage, stopGeneration, clearChat, + respondToApproval, } } diff --git a/app/pages/admin/agent-tools/index.vue b/app/pages/admin/agent-tools/index.vue index f44fe45..55f3b29 100644 --- a/app/pages/admin/agent-tools/index.vue +++ b/app/pages/admin/agent-tools/index.vue @@ -7,6 +7,7 @@ interface AgentToolRow { type: string config: string enabled: number + needsApproval: number sortOrder: number createdAt: string updatedAt: string @@ -68,6 +69,19 @@ async function handleToggle(tool: AgentToolRow) { } } +async function handleToggleApproval(tool: AgentToolRow) { + try { + await $fetch(`/api/agent-tools/${tool.id}`, { + method: "PUT", + body: { needsApproval: !tool.needsApproval }, + }) + $toast.success(tool.needsApproval ? "已关闭审批" : "已开启审批") + refresh() + } catch (e: any) { + $toast.error(e?.data?.message ?? "操作失败") + } +} + function openEdit(tool: AgentToolRow) { editingTool.value = tool showFormModal.value = true @@ -119,13 +133,14 @@ function onExecuteClose() { Slug 类型 状态 + 审批 排序 操作 - 暂无工具 + 暂无工具 @@ -141,6 +156,11 @@ function onExecuteClose() { {{ statusBadge(t.enabled).label }} + + + {{ t.needsApproval ? '需审批' : '直接执行' }} + + {{ t.sortOrder }}
@@ -148,6 +168,9 @@ function onExecuteClose() { +
@@ -357,6 +380,16 @@ function onExecuteClose() { color: #b06000; } +.badge-approval { + background: #fef0f0; + color: #e53e3e; +} + +.badge-no-approval { + background: #f0f0f0; + color: #666; +} + .sort-cell { color: var(--color-muted); font-size: 13px; diff --git a/app/pages/settings/llm-test/index.vue b/app/pages/settings/llm-test/index.vue index 7262c74..6541714 100644 --- a/app/pages/settings/llm-test/index.vue +++ b/app/pages/settings/llm-test/index.vue @@ -64,7 +64,7 @@ const selectedModel = computed(() => { return allModels.value.find(m => m.id === selectedModelId.value) ?? null }) -const { messages, isLoading, errorMessage, sendMessage, stopGeneration, clearChat } = useLlmChat({ +const { messages, isLoading, errorMessage, sendMessage, stopGeneration, clearChat, respondToApproval } = useLlmChat({ modelId: () => selectedModelId.value, systemPrompt: () => systemPrompt.value, enableThinking: () => enableThinking.value, @@ -297,13 +297,47 @@ function modelTypeClass(type: string) { {{ part.toolName }} - {{ part.state === 'call' ? '调用中...' : '完成' }} + + + +
参数: {{ JSON.stringify(part.args) }}
+ + +
+ + +
+ + +
+ 拒绝原因: + {{ part.approvalReason }} +
+
结果:
{{ typeof part.result === 'string' ? part.result.slice(0, 500) : JSON.stringify(part.result, null, 2)?.slice(0, 500) }}
@@ -1034,6 +1068,72 @@ function modelTypeClass(type: string) { color: var(--color-accent-teal); } +.tool-state.approval-requested { + background: rgba(232, 165, 90, 0.15); + color: #b06000; +} + +.tool-state.approval-responded { + background: rgba(93, 184, 166, 0.15); + color: var(--color-accent-teal); +} + +.tool-approval-actions { + display: flex; + gap: 8px; + margin-top: 8px; +} + +.btn-approve, +.btn-deny { + display: flex; + align-items: center; + gap: 4px; + padding: 6px 14px; + font-size: 12px; + font-weight: 500; + border: none; + border-radius: 6px; + cursor: pointer; + transition: all 0.15s ease; +} + +.btn-approve { + background: rgba(93, 184, 166, 0.15); + color: var(--color-accent-teal); +} + +.btn-approve:hover:not(:disabled) { + background: rgba(93, 184, 166, 0.25); +} + +.btn-deny { + background: rgba(198, 69, 69, 0.1); + color: var(--color-error); +} + +.btn-deny:hover:not(:disabled) { + background: rgba(198, 69, 69, 0.2); +} + +.btn-approve:disabled, +.btn-deny:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-approve :deep(svg), +.btn-deny :deep(svg) { + width: 14px; + height: 14px; +} + +.tool-approval-reason { + margin-top: 6px; + font-size: 11px; + color: var(--color-error); +} + .tool-args, .tool-result { display: flex; diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite index 1472317..a045b3a 100644 Binary files a/packages/drizzle-pkg/db.sqlite and b/packages/drizzle-pkg/db.sqlite differ diff --git a/packages/drizzle-pkg/lib/schema/agent-tool.ts b/packages/drizzle-pkg/lib/schema/agent-tool.ts index a615373..fa7a583 100644 --- a/packages/drizzle-pkg/lib/schema/agent-tool.ts +++ b/packages/drizzle-pkg/lib/schema/agent-tool.ts @@ -27,6 +27,7 @@ export const agentTools = sqliteTable( type: text("type", { length: 30 }).notNull(), config: text("config").notNull(), enabled: integer("enabled").default(1).notNull(), + needsApproval: integer("needs_approval").default(0).notNull(), sortOrder: integer("sort_order").default(0).notNull(), createdAt: integer("created_at", { mode: "timestamp_ms" }) .defaultNow() diff --git a/packages/drizzle-pkg/migrations/0015_high_red_shift.sql b/packages/drizzle-pkg/migrations/0015_high_red_shift.sql new file mode 100644 index 0000000..c3ec82b --- /dev/null +++ b/packages/drizzle-pkg/migrations/0015_high_red_shift.sql @@ -0,0 +1 @@ +ALTER TABLE `agent_tools` ADD `needs_approval` integer DEFAULT 0 NOT NULL; \ No newline at end of file diff --git a/packages/drizzle-pkg/migrations/meta/0015_snapshot.json b/packages/drizzle-pkg/migrations/meta/0015_snapshot.json new file mode 100644 index 0000000..45e6242 --- /dev/null +++ b/packages/drizzle-pkg/migrations/meta/0015_snapshot.json @@ -0,0 +1,1877 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "84cd1430-dea1-45b0-b4cf-6bd7c02331dd", + "prevId": "ead47458-3228-4d3f-8638-6b2898643de9", + "tables": { + "agent_tool_logs": { + "name": "agent_tool_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "tool_id": { + "name": "tool_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_slug": { + "name": "tool_slug", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "input": { + "name": "input", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text(20)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "agent_tool_logs_tool_id_idx": { + "name": "agent_tool_logs_tool_id_idx", + "columns": [ + "tool_id" + ], + "isUnique": false + }, + "agent_tool_logs_created_at_idx": { + "name": "agent_tool_logs_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_tools": { + "name": "agent_tools", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "needs_approval": { + "name": "needs_approval", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "agent_tools_slug_idx": { + "name": "agent_tools_slug_idx", + "columns": [ + "slug" + ], + "isUnique": true + }, + "agent_tools_enabled_idx": { + "name": "agent_tools_enabled_idx", + "columns": [ + "enabled" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauth_accounts": { + "name": "oauth_accounts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_user_id": { + "name": "provider_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "nickname": { + "name": "nickname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_configs": { + "name": "app_configs", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value_type": { + "name": "value_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_configs": { + "name": "user_configs", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value_type": { + "name": "value_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "user_configs_user_id_idx": { + "name": "user_configs_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_configs_user_id_users_id_fk": { + "name": "user_configs_user_id_users_id_fk", + "tableFrom": "user_configs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_configs_user_id_key_pk": { + "columns": [ + "user_id", + "key" + ], + "name": "user_configs_user_id_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "article_cards": { + "name": "article_cards", + "columns": { + "article_id": { + "name": "article_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "card_id": { + "name": "card_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_article_card_article": { + "name": "idx_article_card_article", + "columns": [ + "article_id" + ], + "isUnique": false + }, + "idx_article_card_card": { + "name": "idx_article_card_card", + "columns": [ + "card_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "article_cards_article_id_articles_id_fk": { + "name": "article_cards_article_id_articles_id_fk", + "tableFrom": "article_cards", + "tableTo": "articles", + "columnsFrom": [ + "article_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "article_cards_card_id_cards_id_fk": { + "name": "article_cards_card_id_cards_id_fk", + "tableFrom": "article_cards", + "tableTo": "cards", + "columnsFrom": [ + "card_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "article_card_pk": { + "columns": [ + "article_id", + "card_id" + ], + "name": "article_card_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "articles": { + "name": "articles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "title": { + "name": "title", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cover": { + "name": "cover", + "type": "text(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "idx_article_status": { + "name": "idx_article_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "idx_article_created": { + "name": "idx_article_created", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "card_images": { + "name": "card_images", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "card_id": { + "name": "card_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text(500)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_card_image_card": { + "name": "idx_card_image_card", + "columns": [ + "card_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "card_images_card_id_cards_id_fk": { + "name": "card_images_card_id_cards_id_fk", + "tableFrom": "card_images", + "tableTo": "cards", + "columnsFrom": [ + "card_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "card_tags": { + "name": "card_tags", + "columns": { + "card_id": { + "name": "card_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag_id": { + "name": "tag_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_card_tag_card": { + "name": "idx_card_tag_card", + "columns": [ + "card_id" + ], + "isUnique": false + }, + "idx_card_tag_tag": { + "name": "idx_card_tag_tag", + "columns": [ + "tag_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "card_tags_card_id_cards_id_fk": { + "name": "card_tags_card_id_cards_id_fk", + "tableFrom": "card_tags", + "tableTo": "cards", + "columnsFrom": [ + "card_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "card_tags_tag_id_tags_id_fk": { + "name": "card_tags_tag_id_tags_id_fk", + "tableFrom": "card_tags", + "tableTo": "tags", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "card_tag_pk": { + "columns": [ + "card_id", + "tag_id" + ], + "name": "card_tag_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cards": { + "name": "cards", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aspect_ratio": { + "name": "aspect_ratio", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category_id": { + "name": "category_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "idx_card_category": { + "name": "idx_card_category", + "columns": [ + "category_id" + ], + "isUnique": false + }, + "idx_card_type": { + "name": "idx_card_type", + "columns": [ + "type" + ], + "isUnique": false + }, + "idx_card_created": { + "name": "idx_card_created", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "cards_category_id_categories_id_fk": { + "name": "cards_category_id_categories_id_fk", + "tableFrom": "cards", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "categories": { + "name": "categories", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "idx_category_slug": { + "name": "idx_category_slug", + "columns": [ + "slug" + ], + "isUnique": true + }, + "idx_category_parent": { + "name": "idx_category_parent", + "columns": [ + "parent_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chat_messages": { + "name": "chat_messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "nickname": { + "name": "nickname", + "type": "text(20)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_chat_msg_created": { + "name": "idx_chat_msg_created", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "favorites": { + "name": "favorites", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "card_id": { + "name": "card_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "idx_favorite_user": { + "name": "idx_favorite_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_favorite_card": { + "name": "idx_favorite_card", + "columns": [ + "card_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "favorites_user_id_users_id_fk": { + "name": "favorites_user_id_users_id_fk", + "tableFrom": "favorites", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "favorites_card_id_cards_id_fk": { + "name": "favorites_card_id_cards_id_fk", + "tableFrom": "favorites", + "tableTo": "cards", + "columnsFrom": [ + "card_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "favorite_pk": { + "columns": [ + "user_id", + "card_id" + ], + "name": "favorite_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ideas": { + "name": "ideas", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "author": { + "name": "author", + "type": "text(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text(30)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text(30)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "idx_idea_created": { + "name": "idx_idea_created", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text(1000)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "idx_project_created": { + "name": "idx_project_created", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tags": { + "name": "tags", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_tag_slug": { + "name": "idx_tag_slug", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tools": { + "name": "tools", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_models": { + "name": "llm_models", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(200)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model_id": { + "name": "model_id", + "type": "text(200)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'text'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_tokens": { + "name": "max_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "idx_llm_model_provider": { + "name": "idx_llm_model_provider", + "columns": [ + "provider_id" + ], + "isUnique": false + }, + "idx_llm_model_type": { + "name": "idx_llm_model_type", + "columns": [ + "type" + ], + "isUnique": false + } + }, + "foreignKeys": { + "llm_models_provider_id_llm_providers_id_fk": { + "name": "llm_models_provider_id_llm_providers_id_fk", + "tableFrom": "llm_models", + "tableTo": "llm_providers", + "columnsFrom": [ + "provider_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_providers": { + "name": "llm_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parse_mode": { + "name": "parse_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'openai'" + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "idx_llm_provider_user_slug": { + "name": "idx_llm_provider_user_slug", + "columns": [ + "user_id", + "slug" + ], + "isUnique": true + }, + "idx_llm_provider_user": { + "name": "idx_llm_provider_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_llm_provider_status": { + "name": "idx_llm_provider_status", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": { + "llm_providers_user_id_users_id_fk": { + "name": "llm_providers_user_id_users_id_fk", + "tableFrom": "llm_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scheduled_tasks": { + "name": "scheduled_tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "function_name": { + "name": "function_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "function_payload": { + "name": "function_payload", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "http_method": { + "name": "http_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "http_url": { + "name": "http_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "http_headers": { + "name": "http_headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "http_body": { + "name": "http_body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "catch_up": { + "name": "catch_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "max_retries": { + "name": "max_retries", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "retry_delay_seconds": { + "name": "retry_delay_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 60 + }, + "timeout_seconds": { + "name": "timeout_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "task_execution_logs": { + "name": "task_execution_logs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "task_execution_logs_task_id_scheduled_tasks_id_fk": { + "name": "task_execution_logs_task_id_scheduled_tasks_id_fk", + "tableFrom": "task_execution_logs", + "tableTo": "scheduled_tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/drizzle-pkg/migrations/meta/_journal.json b/packages/drizzle-pkg/migrations/meta/_journal.json index e8b3efd..6965254 100644 --- a/packages/drizzle-pkg/migrations/meta/_journal.json +++ b/packages/drizzle-pkg/migrations/meta/_journal.json @@ -106,6 +106,13 @@ "when": 1785894874773, "tag": "0014_breezy_maestro", "breakpoints": true + }, + { + "idx": 15, + "version": "6", + "when": 1785942019319, + "tag": "0015_high_red_shift", + "breakpoints": true } ] } \ No newline at end of file diff --git a/server/api/agent-tools/[id].put.ts b/server/api/agent-tools/[id].put.ts index 0a72686..f8a5014 100644 --- a/server/api/agent-tools/[id].put.ts +++ b/server/api/agent-tools/[id].put.ts @@ -10,7 +10,7 @@ export default defineWrappedResponseHandler(async (event) => { } const body = await readBody(event); - const { name, slug, description, type, config, enabled, sortOrder } = body; + const { name, slug, description, type, config, enabled, needsApproval, sortOrder } = body; try { const result = await updateAgentTool(id, { @@ -20,6 +20,7 @@ export default defineWrappedResponseHandler(async (event) => { type, config, enabled, + needsApproval, sortOrder, }); if (!result) { diff --git a/server/api/agent-tools/index.post.ts b/server/api/agent-tools/index.post.ts index 6c99a57..b111271 100644 --- a/server/api/agent-tools/index.post.ts +++ b/server/api/agent-tools/index.post.ts @@ -5,7 +5,7 @@ export default defineWrappedResponseHandler(async (event) => { await requireAdmin(event); const body = await readBody(event); - const { name, slug, description, type, config, enabled, sortOrder } = body; + const { name, slug, description, type, config, enabled, needsApproval, sortOrder } = body; if (!name || !slug || !type) { return R.throwError(422, "name、slug、type 不能为空", null); @@ -19,6 +19,7 @@ export default defineWrappedResponseHandler(async (event) => { type, config: config ?? {}, enabled, + needsApproval, sortOrder, }); return R.success(result); diff --git a/server/api/llm/chat/index.post.ts b/server/api/llm/chat/index.post.ts index 1958b2f..13a4f65 100644 --- a/server/api/llm/chat/index.post.ts +++ b/server/api/llm/chat/index.post.ts @@ -1,7 +1,7 @@ import { requireUser } from "#server/utils/context"; import { getProviderById, getModelById } from "#server/service/llm"; import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; -import { streamText, type LanguageModelV1 } from "ai"; +import { type LanguageModel, streamText, stepCountIs, convertToModelMessages } from 'ai'; import { getEnabledToolsForLlm } from "#server/service/agent-tool"; import log4js from "logger"; @@ -15,7 +15,7 @@ function resolveModel( parseMode: string; }, modelId: string, -): LanguageModelV1 { +): LanguageModel { const baseUrl = provider.baseUrl?.replace(/\/+$/, "") || undefined; if (provider.parseMode === "anthropic") { @@ -31,7 +31,7 @@ function resolveModel( baseURL: baseUrl || "https://api.openai.com/v1", }); - return openaiCompatible(modelId) as LanguageModelV1; + return openaiCompatible(modelId) as LanguageModel; } export default defineEventHandler(async (event) => { @@ -43,7 +43,7 @@ export default defineEventHandler(async (event) => { const body = await readBody(event); const { modelId: llmModelId, messages, enableThinking, enableTools } = body as { modelId: number; - messages: { role: "user" | "assistant" | "system"; content: string }[]; + messages: any[]; enableThinking?: boolean; enableTools?: boolean; }; @@ -84,14 +84,16 @@ export default defineEventHandler(async (event) => { const languageModel = resolveModel(provider, model.modelId); - const tools = enableTools ? await getEnabledToolsForLlm() : undefined; + const { tools, approvalConfig } = enableTools ? await getEnabledToolsForLlm() : { tools: undefined, approvalConfig: {} }; + + const modelMessages = await convertToModelMessages(messages); const result = streamText({ model: languageModel, - messages, - maxTokens: model.maxTokens || undefined, + messages: modelMessages, + maxOutputTokens: model.maxTokens || undefined, ...(tools && Object.keys(tools).length > 0 - ? { tools, maxSteps: 8 } + ? { tools, stopWhen: stepCountIs(8), toolApproval: approvalConfig } : {}), ...(enableThinking ? { @@ -112,11 +114,11 @@ export default defineEventHandler(async (event) => { event.context.requestId ?? "-", finishReason, steps.length, - usage?.promptTokens ?? 0, - usage?.completionTokens ?? 0, + usage?.inputTokens ?? 0, + usage?.outputTokens ?? 0, ); }, }); - return result.toDataStreamResponse({ sendReasoning: true }); + return result.toUIMessageStreamResponse({ sendReasoning: true }); }); diff --git a/server/service/agent-tool/index.ts b/server/service/agent-tool/index.ts index f13686b..de510da 100644 --- a/server/service/agent-tool/index.ts +++ b/server/service/agent-tool/index.ts @@ -89,6 +89,7 @@ export interface CreateAgentToolInput { type: string; config: Record; enabled?: boolean; + needsApproval?: boolean; sortOrder?: number; } @@ -99,6 +100,7 @@ export interface UpdateAgentToolInput { type?: string; config?: Record; enabled?: boolean; + needsApproval?: boolean; sortOrder?: number; } @@ -175,6 +177,7 @@ export async function createAgentTool(input: CreateAgentToolInput): Promise { return result; } -export async function getEnabledToolsForLlm(): Promise>> { +export async function getEnabledToolsForLlm(): Promise<{ + tools: Record>; + approvalConfig: Record; +}> { const tools = await dbGlobal .select() .from(agentTools) @@ -319,6 +326,7 @@ export async function getEnabledToolsForLlm(): Promise = {}; + const approvalConfig: Record = {}; for (const agentTool of tools) { const executor = getExecutor(agentTool.type); if (!executor) continue; @@ -334,7 +342,7 @@ export async function getEnabledToolsForLlm(): Promise { const r = zodSchema.safeParse(v); return r.success @@ -354,8 +362,9 @@ export async function getEnabledToolsForLlm(): Promise