feat(ui): 优化流式输出工具调用渲染

- 添加 tool_start/tool_end WebSocket 事件支持
- 流式消息复用 ChatMessage 组件渲染工具调用卡片
- 修复 AI SDK v5 格式兼容问题(input/output 字段)
- 修复会话恢复时 tool-result 格式错误
- 放宽 ToolState schema 中 input 字段类型为 unknown
This commit is contained in:
2025-12-15 17:35:39 +08:00
parent 865e0906b9
commit 3fd8fd98b8
12 changed files with 384 additions and 58 deletions
+12 -5
View File
@@ -224,7 +224,8 @@ export class SessionManager {
} else if (role === 'assistant') {
// Assistant 消息:文本 + 工具调用
const content: unknown[] = [];
const completedTools: Array<{ toolCallId: string; toolName: string; output: unknown }> = [];
// input 使用 unknown 类型以兼容 AI SDK(可能是对象、字符串等)
const completedTools: Array<{ toolCallId: string; toolName: string; input: unknown; output: unknown }> = [];
for (const part of parts) {
if (part.type === 'text') {
@@ -232,11 +233,12 @@ export class SessionManager {
} else if (part.type === 'tool') {
// 只有非 pending 状态的工具调用才添加到 AI SDK 消息
if (part.state.status !== 'pending') {
// AI SDK v5 使用 input 字段(不是 args
content.push({
type: 'tool-call',
toolCallId: part.toolCallId,
toolName: part.toolName,
args: part.state.input,
input: part.state.input,
});
// 收集已完成的工具结果
@@ -244,12 +246,14 @@ export class SessionManager {
completedTools.push({
toolCallId: part.toolCallId,
toolName: part.toolName,
input: part.state.input,
output: part.state.output,
});
} else if (part.state.status === 'error') {
completedTools.push({
toolCallId: part.toolCallId,
toolName: part.toolName,
input: part.state.input,
output: part.state.error,
});
}
@@ -273,6 +277,7 @@ export class SessionManager {
}
// 添加 tool 消息(如果有已完成的工具)
// AI SDK v5 要求 tool-result 必须包含 input 和 output 字段
if (completedTools.length > 0) {
result.push({
role: 'tool',
@@ -280,7 +285,8 @@ export class SessionManager {
type: 'tool-result',
toolCallId: t.toolCallId,
toolName: t.toolName,
result: t.output,
input: t.input,
output: t.output,
})),
} as unknown as ModelMessage);
}
@@ -454,7 +460,8 @@ export class SessionManager {
for (const item of message.content) {
const itemType = (item as { type: string }).type;
if (itemType === 'tool-result') {
const toolResult = item as unknown as { toolCallId: string; toolName: string; result: unknown };
// AI SDK v5 使用 output 字段存储结果(不是 result)
const toolResult = item as unknown as { toolCallId: string; toolName: string; output: unknown };
const partId = toolCallPartIds.get(toolResult.toolCallId);
if (partId) {
// 更新工具状态为 completed
@@ -463,7 +470,7 @@ export class SessionManager {
const startTime = part?.type === 'tool' && part.state.status === 'running'
? part.state.time.start
: Date.now();
await PartStorage.setToolCompleted(currentAssistantMsgId, partId, toolResult.result, startTime);
await PartStorage.setToolCompleted(currentAssistantMsgId, partId, toolResult.output, startTime);
}
}
}