diff --git a/app/components/agent/AgentToolCallBlock.vue b/app/components/agent/AgentToolCallBlock.vue index cd4e54a..fc44848 100644 --- a/app/components/agent/AgentToolCallBlock.vue +++ b/app/components/agent/AgentToolCallBlock.vue @@ -19,7 +19,9 @@ const collapsed = ref(false); {{ part.toolName }} - + + + @@ -35,6 +37,11 @@ const collapsed = ref(false);
+
+ + 正在生成调用参数… +
+
参数
{{ JSON.stringify(part.args, null, 2) }}
@@ -119,7 +126,9 @@ const collapsed = ref(false); color: var(--color-muted); } +.tool-state.preparing { color: var(--color-accent-amber); } .tool-state.call { color: var(--color-accent-amber); } +.tool-state.interrupted { color: var(--color-accent-red, #e74c3c); } .tool-state.result { color: var(--color-accent-teal); } .tool-state.approval-requested { color: var(--color-accent-amber); } .tool-state.approval-responded { color: var(--color-body); } @@ -224,4 +233,22 @@ const collapsed = ref(false); font-size: 12px; color: var(--color-body); } + +.tool-preparing { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 0; + font-size: 12px; + color: var(--color-muted); +} + +.tool-preparing .spin { + font-size: 13px; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} diff --git a/app/composables/useAgentChat.ts b/app/composables/useAgentChat.ts index 165d524..4abd59d 100644 --- a/app/composables/useAgentChat.ts +++ b/app/composables/useAgentChat.ts @@ -286,7 +286,7 @@ export function useAgentChat(options: UseAgentChatOptions) { errorMessage.value = chunk.errorText || "流式响应出错"; break; } - case "tool-input-available": { + case "tool-input-start": { const msg = messages.value[assistantIdx]; if (!msg) break; updateLastReasoningDuration(msg); @@ -295,11 +295,36 @@ export function useAgentChat(options: UseAgentChatOptions) { type: "tool-call", toolName: chunk.toolName, toolCallId: chunk.toolCallId, - args: chunk.input, - state: "call", + args: undefined, + state: "preparing", }); break; } + case "tool-input-delta": { + break; + } + case "tool-input-available": { + const msg = messages.value[assistantIdx]; + if (!msg) break; + updateLastReasoningDuration(msg); + const existing = msg.parts?.find( + (p) => p.type === "tool-call" && p.toolCallId === chunk.toolCallId && p.state === "preparing" + ); + if (existing) { + existing.args = chunk.input; + existing.state = "call"; + } else { + 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; @@ -361,7 +386,14 @@ export function useAgentChat(options: UseAgentChatOptions) { } const msg = messages.value[assistantIdx]; - if (msg) updateLastReasoningDuration(msg); + if (msg) { + updateLastReasoningDuration(msg); + for (const p of msg.parts ?? []) { + if (p.type === "tool-call" && p.state === "preparing") { + p.state = "interrupted"; + } + } + } } function validateAssistantContent(assistantIdx: number, isApprovalContinue = false) { diff --git a/app/types/chat.ts b/app/types/chat.ts index 695c5c4..f0cb839 100644 --- a/app/types/chat.ts +++ b/app/types/chat.ts @@ -8,7 +8,7 @@ export interface MessagePart { toolCallId?: string args?: unknown result?: unknown - state?: 'call' | 'result' | 'approval-requested' | 'approval-responded' + state?: 'preparing' | 'call' | 'result' | 'approval-requested' | 'approval-responded' | 'interrupted' approvalId?: string approved?: boolean approvalReason?: string diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts index a248956..ed50d94 100644 --- a/e2e/global-setup.ts +++ b/e2e/global-setup.ts @@ -313,7 +313,7 @@ async function seedLlmData(dbPath: string) { // 插入 default agent(seed 脚本只做 bootstrap admin,不含 agent seed) await client.execute({ - sql: `INSERT INTO agents (slug, name, description, system_prompt, default_model_id, title_strategy, enable_thinking, enable_tools, max_step_count, is_default, is_callable, sort_order, enabled) + sql: `INSERT OR IGNORE INTO agents (slug, name, description, system_prompt, default_model_id, title_strategy, enable_thinking, enable_tools, max_step_count, is_default, is_callable, sort_order, enabled) VALUES ('default', '默认助手', '通用 AI 助手', 'You are a helpful AI assistant.', ?, 'llm', 0, 1, 6, 1, 0, 0, 1)`, args: [Number(modelId)], }); diff --git a/e2e/specs/agent-documents.spec.ts b/e2e/specs/agent-documents.spec.ts index 78639fe..f1ce6b0 100644 --- a/e2e/specs/agent-documents.spec.ts +++ b/e2e/specs/agent-documents.spec.ts @@ -189,8 +189,9 @@ test.describe("Agent 文档", () => { await expect(docLink).toBeVisible({ timeout: 10000 }); }); - test("带 agentSlug 创建的文档绑定到该 agent", async ({ page, request }) => { + test("带 agentSlug 创建的文档绑定到该 agent", async ({ page }) => { await loginViaUI(page); + const request = page.request; const title = `E2E绑定Agent_${Date.now()}`; const res = await request.post("/api/agent-documents", { @@ -210,8 +211,9 @@ test.describe("Agent 文档", () => { await request.delete(`/api/agent-documents/${docId}`); }); - test("不带 agentSlug 创建的文档为全局共享", async ({ page, request }) => { + test("不带 agentSlug 创建的文档为全局共享", async ({ page }) => { await loginViaUI(page); + const request = page.request; const title = `E2E全局文档_${Date.now()}`; const res = await request.post("/api/agent-documents", { @@ -235,8 +237,9 @@ test.describe("Agent 文档", () => { await request.delete(`/api/agent-documents/${docId}`); }); - test("PATCH 切换文档归属(全局 ↔ agent 专属)", async ({ page, request }) => { + test("PATCH 切换文档归属(全局 ↔ agent 专属)", async ({ page }) => { await loginViaUI(page); + const request = page.request; // 先创建全局文档 const createRes = await request.post("/api/agent-documents", { diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite index 6c5fa39..fa66cd8 100644 Binary files a/packages/drizzle-pkg/db.sqlite and b/packages/drizzle-pkg/db.sqlite differ diff --git a/server/service/agent-tool/index.ts b/server/service/agent-tool/index.ts index e5904d3..32f071e 100644 --- a/server/service/agent-tool/index.ts +++ b/server/service/agent-tool/index.ts @@ -509,15 +509,15 @@ export async function getAgentToolsForChat(params: { : { success: false as const, error: r.error }; }, }), - execute: async (input: unknown) => { - const execResult = await executeAgentTool(agentTool.id, input, userId); - if (!execResult.success) { - return `工具执行失败: ${execResult.error ?? "未知错误"}。请停止调用此工具,基于已有信息回答用户或告知用户此工具不可用。`; - } - const meta = execResult.metadata; - const sizeInfo = meta?.responseSize ? `${meta.responseSize} bytes` : `未知大小`; - const statusInfo = meta?.statusCode ? `HTTP ${meta.statusCode}` : ""; - const dataStr = typeof execResult.data === "string" ? execResult.data : JSON.stringify(execResult.data); + execute: async (input: unknown) => { + const execResult = await executeAgentTool(agentTool.id, input, userId, undefined, agentId); + if (!execResult.success) { + return `工具执行失败: ${execResult.error ?? "未知错误"}。请停止调用此工具,基于已有信息回答用户或告知用户此工具不可用。`; + } + const meta = execResult.metadata; + const sizeInfo = meta?.responseSize ? `${meta.responseSize} bytes` : `未知大小`; + const statusInfo = meta?.statusCode ? `HTTP ${meta.statusCode}` : ""; + const dataStr = typeof execResult.data === "string" ? execResult.data : JSON.stringify(execResult.data); return `[${agentTool.type} 结果 ${statusInfo} ${sizeInfo}]\n${dataStr}`; }, needsApproval: false, @@ -602,7 +602,7 @@ export async function getAgentToolsForChatByAgentId(params: { }, }), execute: async (input: unknown) => { - const execResult = await executeAgentTool(agentTool.id, input, userId); + const execResult = await executeAgentTool(agentTool.id, input, userId, undefined, agentId); if (!execResult.success) { return `工具执行失败: ${execResult.error ?? "未知错误"}。请停止调用此工具,基于已有信息回答用户或告知用户此工具不可用。`; }