重构: CR-11 composables+ToolCard 健壮性 6 子项

⑦ useAiConversations JSON.parse 逐条 try/catch 容错(坏条降级不清空整对话);⑧ ToolCard 去 as any + argString 安全取字段;⑨ useAiSend approveToolCall 复用 findToolCall;⑩ useAiEvents 补 AiHeartbeat case 防穿透;⑪ approveHumanApproval 签名收敛(decisions:string[] + select_type 判单/多选)+ ProjectDetail 调用方同步(单选包数组/多选直传);⑫ ToolCard formatBytes/formatToolName/argString 兜底。批5 ww1mh6mry,vue-tsc 0
This commit is contained in:
2026-06-15 05:45:04 +08:00
parent 0e196ee86f
commit fddca9daf1
6 changed files with 68 additions and 31 deletions

View File

@@ -74,15 +74,27 @@ export async function switchConversation(id: string) {
content: m.content || '',
model: m.model,
timestamp: Date.now(),
toolCalls: m.tool_calls?.map((tc: any) => ({
id: tc.id,
name: tc.function?.name || '',
args: typeof tc.function?.arguments === 'string'
? JSON.parse(tc.function.arguments || '{}')
: tc.function?.arguments || {},
status: 'completed' as const,
result: toolResultMap.get(tc.id),
})),
toolCalls: m.tool_calls?.map((tc: any): AiToolCallInfo => {
// 逐条容错:单条坏 arguments 仅降级为空对象,不影响整条对话回填
let args: unknown = {}
const rawArgs = tc.function?.arguments
if (typeof rawArgs === 'string') {
try {
args = rawArgs ? JSON.parse(rawArgs) : {}
} catch {
args = {}
}
} else if (rawArgs && typeof rawArgs === 'object') {
args = rawArgs
}
return {
id: tc.id,
name: tc.function?.name || '',
args,
status: 'completed' as const,
result: toolResultMap.get(tc.id),
}
}),
}))
} catch {
state.messages = []

View File

@@ -127,6 +127,10 @@ export function handleEvent(event: AiChatEvent) {
break
}
case 'AiHeartbeat':
// 心跳事件:仅维持看门狗(已在上方 reset),无需额外处理;显式 case 防 switch 穿透
break
case 'AiToolCallStarted': {
const info: AiToolCallInfo = {
id: event.id,

View File

@@ -13,7 +13,7 @@ import { aiApi } from '../../api'
import { useAppSettingsStore } from '../../stores/appSettings'
import { state } from '../../stores/ai'
import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream'
import { startListener } from './useAiEvents'
import { startListener, findToolCall } from './useAiEvents'
import { nextMsgId } from './aiShared'
const appSettings = useAppSettingsStore()
@@ -81,9 +81,8 @@ async function sendMessage(text: string, skill?: string) {
/** 工具审批:乐观置 running,IPC 失败时改 completed+错误文案(不回滚避免卡按钮) */
async function approveToolCall(toolCallId: string, approved: boolean) {
// 乐观置运行中,禁用审批按钮防重复点击(后端事件回来后转 completed/rejected)
const tc = state.messages
.flatMap(m => m.toolCalls || [])
.find(t => t.id === toolCallId)
// 复用 findToolCall(反向扫描)避免重复实现查找逻辑
const tc = findToolCall(toolCallId)
if (tc) tc.status = 'running'
// 重启看门狗覆盖审批执行→续生成窗口(useAiEvents.ts:162 AiApprovalRequired 已 clear,
// 审批态无心跳兜底;approve 后后端要跑工具+续生成,期间任何后端异常不回则按钮永久 running。