//! 工具事件域 — handleToolEvent //! //! 处理:AiToolCallStarted / AiToolCallCompleted / AiToolAutoApproved / AiApprovalRequired / AiApprovalResult。 //! 依赖方向:本模块 → aiShared / useAiPendingState(叶子)/ useAiStream,不导入任何同级子 composable。 import { emit } from '@tauri-apps/api/event' import { state } from '@/stores/ai' import { t } from '@/i18n/i18n-helpers' import { findToolCall, clearApprovalTimer, startApprovalTimer, startToolSlowTimer, clearToolSlowTimer, addPendingApproval, removePendingApprovalById, bumpToolEpoch } from './aiShared' import { removePendingDirAuth } from './useAiPendingState' import { clearStreamWatchdog } from './useAiStream' import type { AiChatEvent, AiToolCallInfo } from '@/api/types' /** tool 域:工具卡片状态流转/会话级信任/审批。返回 true=已处理。 */ export function handleToolEvent(event: AiChatEvent): boolean { switch (event.type) { case 'AiToolCallStarted': { const info: AiToolCallInfo = { id: event.id, name: event.name, args: event.args, status: 'running', // 后端事件不带耗时,前端记开始时刻,Completed 时差值即单卡耗时 startedAt: Date.now(), } // 同 id 重复 Started 不重复 push(provider id 不稳,防残留 running 空卡) if (!findToolCall(event.id)) { // #15(2026-10-05):末条非 assistant(如 system 占位/思考 pill/完成轨迹折叠重排)时 // 原直接丢弃工具卡 → running 卡消失但工具仍在执行。兜底向前找最近 assistant 落卡; // 全程无 assistant(极端:首轮工具前无任何回复)才真丢弃(与原行为一致)。 const lastMsg = state.messages[state.messages.length - 1] let target = (lastMsg && lastMsg.role === 'assistant') ? lastMsg : null if (!target) { for (let i = state.messages.length - 1; i >= 0; i--) { if (state.messages[i].role === 'assistant') { target = state.messages[i]; break } } } if (target) { target.toolCalls = target.toolCalls || [] target.toolCalls.push(info) // 工具态变更 → 版本号自增(MessageList 收起联动 O(1) 检测;完成/待审批/拒绝经 // aiShared 的 pending 出入口自增,此处仅覆盖「新卡 push」这一不经 pending 的路径) bumpToolEpoch() } } // 工具开始执行即挂慢执行计时器(超时仅 toast,不动 running 态) startToolSlowTimer(event.id, event.name) return true } case 'AiToolCallCompleted': { const tc = findToolCall(event.id) if (tc) { tc.status = 'completed' tc.result = event.result tc.completedAt = Date.now() // 清流式缓冲:结果由 result 渲染,流式行是执行中间态,留着会双显 tc.streamLines = undefined } // 移除待审批(含 per-conv 集合,后台会话集合里的同 id 挂起一并清) removePendingApprovalById(event.id) clearToolSlowTimer(event.id) // 工具结束(无论审批通过后执行还是被拒),清审批超时计时器 clearApprovalTimer(event.id) return true } case 'AiToolAutoApproved': { // 会话级信任:经事件总线桥接弹本地 toast(composable 无组件上下文) void emit('ai-tool-auto-approved-toast', { tool: event.tool, dir: event.dir }) return true } case 'AiCommandOutput': { // 工具执行实时流式(run_command shell 行级 / ask_uclaw 进度块级): // 按 id 路由到对应工具卡,streamLines 追加(上限 200 行防长任务刷爆响应式数组, // 超限丢头保尾——最新进度更有价值)。Completed 时清空(结果由 result 渲染,防双显)。 // 找不到工具卡(极早到/已收尾)静默忽略。 const tc = findToolCall(event.id) if (tc) { if (!tc.streamLines) tc.streamLines = [] tc.streamLines.push(event.line) if (tc.streamLines.length > 200) tc.streamLines.splice(0, tc.streamLines.length - 200) } return true } case 'AiApprovalRequired': { clearStreamWatchdog(event.conversation_id || undefined) // 等用户审批,不计整流超时 const info: AiToolCallInfo = { id: event.id, name: event.name, args: event.args, status: 'pending_approval', kind: event.kind ?? 'risk', // reason 必须写入 info,否则浮窗 ApprovalPopup 的 tc.reason 永远空(审批说明丢失) reason: event.reason, conversationId: event.conversation_id ?? state.activeConversationId ?? undefined, } // per-conv 归会话追加(同 id 幂等):跨会话事件不再互相覆盖;首次到达才做后续副作用 if (addPendingApproval(info)) { const tc = findToolCall(event.id) if (tc) { tc.status = 'pending_approval' tc.reason = event.reason // write_file 审批注入行级 diff(旧文件 vs 新内容),前端审批卡预览 if (event.diff) tc.diff = event.diff } // 进入审批等待 → 取消该工具慢执行计时器(审批耗时由用户主导,非执行慢),再启动审批超时计时器 clearToolSlowTimer(event.id) startApprovalTimer(event.id, event.name, 'risk') // 通知 ToolCardList 展开折叠组 + scroll(pending 卡落折叠分组不可见时) void emit('ai-pending-arrived', { toolCallId: event.id }) } return true } case 'AiApprovalResult': { if (!event.approved) { const tc = findToolCall(event.id) if (tc) { tc.status = 'rejected' tc.result = t('aiTool.rejectedHint') } // 移除待审批(含 per-conv 集合) removePendingApprovalById(event.id) clearToolSlowTimer(event.id) clearApprovalTimer(event.id) } else { const tc = findToolCall(event.id) // 后端对整批工具先批量发 Started 再走审批,审批等待不算执行耗时:通过时重置开始时刻 if (tc) tc.startedAt = Date.now() // 审批通过 → 工具重回执行态,重启慢执行计时器;对称清审批超时计时器防到点误拒 startToolSlowTimer(event.id, tc?.name || '') clearApprovalTimer(event.id) } // 路径授权挂起的工具收到 ApprovalResult → 按 id 移除该条(数组化后只清这一条) removePendingDirAuth(event.id) return true } default: return false } }