- T1 usage 口径: anthropic usage 两层容错+cache 三口径归一(prompt=input+read+creation) - T2 压缩水位判据: compress_should_trigger 双模式+ratio 钳位,开关关逐字节等价 - T3 跨会话检索: history_search 工具(两跳式)+message_repo 关键词检索+prompt 指引 - T4 代语义闭环: status 终局补写+飞书停靠/换代推卡+交付引导+watcher 兜底补漏 - 段序缓存重排: build_runtime_prompt 恒静段上移/工作上下文压尾(前缀缓存失效面收窄) - 智谱缓存解析: anthropic 臂补 prompt_tokens_details.cached_tokens 嵌套回退 - 工时禁令: 提示词双语加「禁止预估工时,AI Coding 工时无意义」 - 拖拽文件落盘: 非图片附件存引用行,防整段文本灌入输入框 - 事实修正: 提示词「40+ 工具」→「80+」(基线 81);tool_failure_memory 测试串行锁除竞态 - UI 拆分: TopBar/ToolResultBody 壳层化(top-bar/tool-result-body 子件目录)
143 lines
6.3 KiB
TypeScript
143 lines
6.3 KiB
TypeScript
//! 工具事件域 — 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 { 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',
|
|
// reason 必须写入 info,否则 ToolCard/浮层的 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.expires_at_millis ?? null)
|
|
// 通知 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)
|
|
}
|
|
return true
|
|
}
|
|
|
|
default:
|
|
return false
|
|
}
|
|
}
|