Browse Source

feat: 更新工具调用状态管理,增加准备和中断状态支持

feat/agent-independence
npmrun 2 months ago
parent
commit
e80feb46d6
  1. 29
      app/components/agent/AgentToolCallBlock.vue
  2. 34
      app/composables/useAgentChat.ts
  3. 2
      app/types/chat.ts
  4. 2
      e2e/global-setup.ts
  5. 9
      e2e/specs/agent-documents.spec.ts
  6. BIN
      packages/drizzle-pkg/db.sqlite
  7. 4
      server/service/agent-tool/index.ts

29
app/components/agent/AgentToolCallBlock.vue

@ -19,7 +19,9 @@ const collapsed = ref(false);
<Icon name="lucide:wrench" class="tool-icon" /> <Icon name="lucide:wrench" class="tool-icon" />
<span class="tool-name">{{ part.toolName }}</span> <span class="tool-name">{{ part.toolName }}</span>
<span :class="['tool-state', part.state]"> <span :class="['tool-state', part.state]">
<template v-if="part.state === 'call'">调用中…</template> <template v-if="part.state === 'preparing'">准备参数…</template>
<template v-else-if="part.state === 'call'">执行中…</template>
<template v-else-if="part.state === 'interrupted'">参数生成中断</template>
<template v-else-if="part.state === 'approval-requested'"> <template v-else-if="part.state === 'approval-requested'">
{{ part.isAutomaticApproval ? "自动批准中…" : "等待审批" }} {{ part.isAutomaticApproval ? "自动批准中…" : "等待审批" }}
</template> </template>
@ -35,6 +37,11 @@ const collapsed = ref(false);
</div> </div>
<div v-show="!collapsed" class="tool-call-body"> <div v-show="!collapsed" class="tool-call-body">
<div v-if="part.state === 'preparing'" class="tool-preparing">
<Icon name="lucide:loader-circle" class="spin" />
<span>正在生成调用参数…</span>
</div>
<div v-if="part.args" class="tool-args"> <div v-if="part.args" class="tool-args">
<span class="tool-label">参数</span> <span class="tool-label">参数</span>
<pre>{{ JSON.stringify(part.args, null, 2) }}</pre> <pre>{{ JSON.stringify(part.args, null, 2) }}</pre>
@ -119,7 +126,9 @@ const collapsed = ref(false);
color: var(--color-muted); color: var(--color-muted);
} }
.tool-state.preparing { color: var(--color-accent-amber); }
.tool-state.call { 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.result { color: var(--color-accent-teal); }
.tool-state.approval-requested { color: var(--color-accent-amber); } .tool-state.approval-requested { color: var(--color-accent-amber); }
.tool-state.approval-responded { color: var(--color-body); } .tool-state.approval-responded { color: var(--color-body); }
@ -224,4 +233,22 @@ const collapsed = ref(false);
font-size: 12px; font-size: 12px;
color: var(--color-body); 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); }
}
</style> </style>

34
app/composables/useAgentChat.ts

@ -286,10 +286,34 @@ export function useAgentChat(options: UseAgentChatOptions) {
errorMessage.value = chunk.errorText || "流式响应出错"; errorMessage.value = chunk.errorText || "流式响应出错";
break; break;
} }
case "tool-input-start": {
const msg = messages.value[assistantIdx];
if (!msg) break;
updateLastReasoningDuration(msg);
appendPart(msg, {
id: generateId(),
type: "tool-call",
toolName: chunk.toolName,
toolCallId: chunk.toolCallId,
args: undefined,
state: "preparing",
});
break;
}
case "tool-input-delta": {
break;
}
case "tool-input-available": { case "tool-input-available": {
const msg = messages.value[assistantIdx]; const msg = messages.value[assistantIdx];
if (!msg) break; if (!msg) break;
updateLastReasoningDuration(msg); 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, { appendPart(msg, {
id: generateId(), id: generateId(),
type: "tool-call", type: "tool-call",
@ -298,6 +322,7 @@ export function useAgentChat(options: UseAgentChatOptions) {
args: chunk.input, args: chunk.input,
state: "call", state: "call",
}); });
}
break; break;
} }
case "tool-output-available": { case "tool-output-available": {
@ -361,7 +386,14 @@ export function useAgentChat(options: UseAgentChatOptions) {
} }
const msg = messages.value[assistantIdx]; 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) { function validateAssistantContent(assistantIdx: number, isApprovalContinue = false) {

2
app/types/chat.ts

@ -8,7 +8,7 @@ export interface MessagePart {
toolCallId?: string toolCallId?: string
args?: unknown args?: unknown
result?: unknown result?: unknown
state?: 'call' | 'result' | 'approval-requested' | 'approval-responded' state?: 'preparing' | 'call' | 'result' | 'approval-requested' | 'approval-responded' | 'interrupted'
approvalId?: string approvalId?: string
approved?: boolean approved?: boolean
approvalReason?: string approvalReason?: string

2
e2e/global-setup.ts

@ -313,7 +313,7 @@ async function seedLlmData(dbPath: string) {
// 插入 default agent(seed 脚本只做 bootstrap admin,不含 agent seed) // 插入 default agent(seed 脚本只做 bootstrap admin,不含 agent seed)
await client.execute({ 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)`, VALUES ('default', '默认助手', '通用 AI 助手', 'You are a helpful AI assistant.', ?, 'llm', 0, 1, 6, 1, 0, 0, 1)`,
args: [Number(modelId)], args: [Number(modelId)],
}); });

9
e2e/specs/agent-documents.spec.ts

@ -189,8 +189,9 @@ test.describe("Agent 文档", () => {
await expect(docLink).toBeVisible({ timeout: 10000 }); await expect(docLink).toBeVisible({ timeout: 10000 });
}); });
test("带 agentSlug 创建的文档绑定到该 agent", async ({ page, request }) => { test("带 agentSlug 创建的文档绑定到该 agent", async ({ page }) => {
await loginViaUI(page); await loginViaUI(page);
const request = page.request;
const title = `E2E绑定Agent_${Date.now()}`; const title = `E2E绑定Agent_${Date.now()}`;
const res = await request.post("/api/agent-documents", { const res = await request.post("/api/agent-documents", {
@ -210,8 +211,9 @@ test.describe("Agent 文档", () => {
await request.delete(`/api/agent-documents/${docId}`); await request.delete(`/api/agent-documents/${docId}`);
}); });
test("不带 agentSlug 创建的文档为全局共享", async ({ page, request }) => { test("不带 agentSlug 创建的文档为全局共享", async ({ page }) => {
await loginViaUI(page); await loginViaUI(page);
const request = page.request;
const title = `E2E全局文档_${Date.now()}`; const title = `E2E全局文档_${Date.now()}`;
const res = await request.post("/api/agent-documents", { const res = await request.post("/api/agent-documents", {
@ -235,8 +237,9 @@ test.describe("Agent 文档", () => {
await request.delete(`/api/agent-documents/${docId}`); await request.delete(`/api/agent-documents/${docId}`);
}); });
test("PATCH 切换文档归属(全局 ↔ agent 专属)", async ({ page, request }) => { test("PATCH 切换文档归属(全局 ↔ agent 专属)", async ({ page }) => {
await loginViaUI(page); await loginViaUI(page);
const request = page.request;
// 先创建全局文档 // 先创建全局文档
const createRes = await request.post("/api/agent-documents", { const createRes = await request.post("/api/agent-documents", {

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.

4
server/service/agent-tool/index.ts

@ -510,7 +510,7 @@ export async function getAgentToolsForChat(params: {
}, },
}), }),
execute: async (input: unknown) => { 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) { if (!execResult.success) {
return `工具执行失败: ${execResult.error ?? "未知错误"}。请停止调用此工具,基于已有信息回答用户或告知用户此工具不可用。`; return `工具执行失败: ${execResult.error ?? "未知错误"}。请停止调用此工具,基于已有信息回答用户或告知用户此工具不可用。`;
} }
@ -602,7 +602,7 @@ export async function getAgentToolsForChatByAgentId(params: {
}, },
}), }),
execute: async (input: unknown) => { 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) { if (!execResult.success) {
return `工具执行失败: ${execResult.error ?? "未知错误"}。请停止调用此工具,基于已有信息回答用户或告知用户此工具不可用。`; return `工具执行失败: ${execResult.error ?? "未知错误"}。请停止调用此工具,基于已有信息回答用户或告知用户此工具不可用。`;
} }

Loading…
Cancel
Save