# 工作区多角度代码走查(2026-06-15) > 范围:工作区未提交改动 22 文件 547 行 + 近 6 提交前端改动。4 路并行代理按文件域切分走查(AiChat.vue / View 层 / stores 重构 / composables+ToolCard)。 > 方法:每路代理独立 `git diff` + 多维度走查(正确性/健壮性/边界/性能/DRY/安全/异步时序/i18n/Vue3+Pinia+TS 规范),主代理整合 + 去重 + 关联已有 todo。 > 性质:**dry — 仅走查 + 文档,不改代码**(本会话职责:走查/整理/建待办,见 memory `session-role-diagnose-only`)。 > 关联:[近期改动代码审查-2026-06-15.md](./近期改动代码审查-2026-06-15.md)(FR-S1/S7/S8+近5提交);[自研块级memo流式渲染审查-2026-06-15.md](./自研块级memo流式渲染审查-2026-06-15.md)(块级 memo,CR-04~07,本走查跳过不重复)。 --- ## 🔴 必须修复(4 独立根因) ### ① [src/stores/project.ts:273] selectType 字段名与后端 IPC 不匹配 → 多选审批静默失效(P0 功能) **来源**:stores 代理。 **改什么**:`invoke('approve_human_approval', {...})` 传 `selectType`(camelCase),后端 `src-tauri/src/commands/workflow.rs:211` 签名 `select_type: Option`(snake_case)。Tauri 2 `#[tauri::command]` **默认不做 camelCase→snake_case 转换**(全项目仅 `state.rs:30` 有 `rename_all`,与 IPC 命令无关)。 **影响**:后端收到 `select_type = None` → 归一化为 `Single`(workflow.rs:214-217)→ 多选 UI 勾多项提交被后端拒「单选审批只能提交一个决策」。**单选偶然兼容(缺省=Single)掩盖了问题**。F-260615-01 多选审批功能完全不可用。 **证据**:同 invoke 对象 `execution_id`/`node_id` 已是 snake_case,唯独 `selectType` 破坏约定;`grep selectType` 在 `src-tauri/` 零命中。 **怎么改**: ```diff - selectType: state.pendingApproval.select_type ?? 'single', // Tauri camelCase→snake_case 自动转 select_type + select_type: state.pendingApproval.select_type ?? 'single', ``` 删误导性注释。单行修复。**修后须手动验证一次多选审批全链路**。 --- ### ② [useAiSend/useAiStream/useAiEvents] 流式 timeout/error/stop 收尾不清队列 → 队列消息静默丢失(P0 功能) **来源**:composables 代理。 **改什么**:`sendMessage`(useAiSend.ts:36-42)在 `state.streaming === true` 时入队 `state.queue`。`drainQueue`(useAiSend.ts:25-29)**仅在 AiCompleted 事件中调用**(useAiEvents.ts:218)。若流式因 `onStreamTimeout`/`AiError`/`stopChat` 收尾(非走 AiCompleted),队列消息**永远不被发出**,用户输入"消失"无提示。 **影响**:用户生成中输入消息 → 流式超时/stopChat/后端崩溃 → 队列消息静默丢失,用户以为已发送但无回复。 **关联去重**:与 **B-260615-22**(前后端状态不同步,streaming 复位撞后端拦截)相关但不同角度——B-22 讲状态不同步致发送撞拦截,本项讲收尾路径不清队列致消息丢失。两者独立。 **怎么改**:`onStreamTimeout`(useAiStream.ts:21-49)/ `AiError` case(useAiEvents.ts:222-237)/ `stopChat` 三处收尾路径都应清空队列并提示(或回填输入框): ```diff // onStreamTimeout() 末尾,clearStreamWatchdog() 之后 + if (state.queue.length) { + state.queue = [] + content += '\n(待发送队列中的消息已取消,请重新发送)' + } ``` `approveToolCall`(useAiSend.ts:82-107)IPC 失败 catch `throw e` 路径同样漏清队列,归并本项一起修。 --- ### ③ [useAiStream.ts onStreamTimeout] 超时不回滚 running toolCall → 审批后卡片永久骨架屏(P0 功能,B-07 残留) **来源**:composables 代理。 **改什么**:`approveToolCall`(useAiSend.ts:82-98)乐观置 `tc.status = 'running'` 后,若后端既不回 `AiToolCallCompleted` 也不回 `AiApprovalResult`(IPC 成功但后端 hang),看门狗超时回调 `onStreamTimeout`(useAiStream.ts:21-49)**只复位 `state.streaming`/`generatingConvId`/`currentText` + push 错误气泡,不碰 `messages[].toolCalls[].status`**。卡片 status 仍是 running → 渲染骨架屏(ToolCard.vue:24),审批按钮在 `pending_approval` 分支(ToolCard.vue:31)才显示 → 用户看到永久骨架屏,无重审入口。 **关联去重**:**B-260615-07 已实施**(approveToolCall 乐观置 running 加 watchdog 兜底),但 B-07 只加了 watchdog 重启,**onStreamTimeout 回调本身没回滚 toolCall.status**——这是 B-07 的残留遗漏点。本项是 B-07 的补全。 **怎么改**:`onStreamTimeout` 中扫一遍把 running 的 toolCall 回滚: ```diff // onStreamTimeout() 中,clearStreamWatchdog() 之后、push 消息之前 + for (let i = state.messages.length - 1; i >= 0; i--) { + const tcs = state.messages[i].toolCalls + if (!tcs) continue + for (const tc of tcs) { + if (tc.status === 'running') { tc.status = 'rejected'; tc.result = '审批/执行超时未响应' } + } + } ``` --- ### ④ i18n 硬编码一组 → 英文 locale 中英混杂(P1 文案) **来源**:View 代理(ProjectDetail 多选按钮 🔴)+ composables 代理(onStreamTimeout 文案 🟡)。 **改什么**: - **`ProjectDetail.vue:201`**(🔴):多选审批按钮 `确认({{ multiDecisions.length }})` 硬编码,同文件其他文案全走 `$t('projectDetail.xxx')`,i18n 体系完整。英文 locale 下多选弹层中英混杂。 - **`useAiStream.ts:39-41`**(🟡):`onStreamTimeout` 两条错误文案硬编码(B-260615-03 改了文案但没接 i18n)。 - **`AiChat.vue:675`**(⚪):handleSend toast `发送失败:${msg}` 硬编码。 **关联去重**:`AiChat.vue:459` confirmClearChat 硬编码 = **B-260615-20 已记技术债**(「文案硬编码未接 i18n,同 confirmClearChat 先例」),不重复记,但本组修复时一并处理。 **怎么改**:补 i18n key(中英): - `projectDetail.approvalConfirm` / `approvalConfirmCount({count})` - `ai.streamInterruptedAfterTool` / `ai.streamInterrupted` - `aiChat.toastSendFail` 模板/composable 改用 `t('...')`。 --- ## 🟡 建议改进 ### ⑤ 四份 `.ai-md` 样式逐字重复 ~180 行 → 抽全局样式或 `` 组件(最大 DRY 债) **来源**:View 代理。**位置**:ProjectDetail.vue:549-607 / Ideas.vue:815-873 / Knowledge.vue:610-668 / TaskDetail.vue。 `:deep(p/ul/li/code/pre/blockquote/h1-h3/a/strong/hr/table/th/td)` 样式规则**逐字符相同**(仅外层选择器不同)。B-260615-24(TaskDetail)首创,B-260615-25 复制粘贴到三视图。composable 抽了渲染逻辑(useMarkdown),样式没跟着抽。 **怎么改**:方案 A(推荐,轻量)— `src/styles/ai-md.css` 全局规则 + `main.ts` import 一次,四视图删 scoped 重复块。方案 B(彻底)— 抽 `` 组件含 v-html+scoped 样式。先 A,第 5 处需求再升 B。 ### ⑥ useMarkdown 加 `useRendered(getText)` 辅助 → 消除 computed+onMounted 三处重复(与 ⑤ 同源) **来源**:View 代理。ProjectDetail:251 / Ideas:225 / Knowledge:249 三处 `renderedDesc = computed(() => { void mdReady.value; return renderMd(...) })` + `onMounted(loadMarkdown)` 模式逐字一致。 **怎么改**:useMarkdown 加 `useMarkdownRender(getText)` 返回 `{ html, loadMarkdown }`,调用点一行。做 ⑤ 方案 B 组件则此项自动消失。 ### ⑦ switchConversation `JSON.parse(arguments)` 无逐条容错 → 单条坏参数清空整对话 **来源**:composables 代理。useAiConversations.ts:81 `JSON.parse(tc.function?.arguments || '{}')` 在 map 内,外层 try-catch(:87)粒度过粗——单条 tool_call arguments 损坏 → `state.messages = []` 整对话空白。 **怎么改**:包成安全函数逐条容错(返回 {} 兜底)。 ### ⑧ args 消费方全用 `as any` → 类型逃逸 **来源**:composables 代理。ToolCard.vue:241,328 + useAiConversations.ts:71。`AiToolCallInfo.args` 类型 `unknown`(types.ts:205)但消费方 `as any` 绕过,拼写错无编译期检查。 **怎么改**:定义 `type ToolArgs = Record`,出口收窄 + 消费方类型守卫。 ### ⑨ approveToolCall 重复查找 → 复用 findToolCall **来源**:composables 代理。useAiSend.ts:82-86 `state.messages.flatMap(m => m.toolCalls || []).find(...)`,而 findToolCall(useAiEvents:75)已是反向遍历统一查找。同一语义两套查找,DRY 违反。 **怎么改**:`approveToolCall` 改用 `findToolCall(toolCallId)`(useAiSend 已 import useAiEvents,不新增耦合方向)。 ### ⑩ useAiEvents switch 缺 AiHeartbeat case **来源**:composables 代理。useAiEvents.ts:115 看门狗重置白名单 `['AiApprovalRequired','AiCompleted','AiError']`,AiHeartbeat 不在排除表 → 会 resetStreamWatchdog(行为正确),但 switch(:118)无 AiHeartbeat case 落到末尾。功能对但可读性差。 **怎么改**:补 `case 'AiHeartbeat': break`(显式 no-op)。 ### ⑪ approveHumanApproval 签名 decision/decisions 歧义 **来源**:stores 代理。project.ts:255 `(decision, comment?, decisions?)` 三参,多选传首项 decision 凑数 + 全量 decisions,依赖后端归一化。前端注释「decision 传首项(向后兼容)」冗余。 **怎么改**:改 options 对象签名 `{ decisions: string[], comment? }`,或不改则在 JSDoc 明确「decisions 优先,decision 占位」契约。 ### ⑫ ToolCard 杂项 **来源**:composables 代理。formatBytes(undefined) 误显「0 B」(:227,应区分 undefined 与 0);shouldKeepOpen 与 toolCategory 命名判定风格不一(:131 精确 vs :206 includes);v-for `:key="arg.key"` 无 index 兜底(:33,LLM 畸形 args 极低概率)。 --- ## ⚪ 可选优化 - **_toastTimer 卸载未 clearTimeout**(AiChat.vue:436,728)— onBeforeUnmount 清了 rafId 漏了 _toastTimer,同组件不对称。补一行。 - **markdown 外层容器 span/p 包 block**(ProjectDetail:119 span / Ideas:66 p)— 改 div 避免 inline 包 block,浏览器容错实际不崩。 - **内联 SVG 常量散落重复**(ToolCard.vue:127-128,213-218)— 抽 SVG 常量表。 - **appSettings.ts:8 注释「store 层」指代模糊**(settings.ts 已删)— 改指 `stores/ai.ts`。 - **stores/index.ts 空行不统一** — 格式。 - **router TaskDetail 路由 icon 语义**(:44-49)— 核对是否有 menu 遍历逻辑,详情页防误入菜单。 - **options 透传缺 shape 校验**(project.ts:271)— 后端兜底已校验,记录。 - **projectNameById computed 每实例重建**(ToolCard.vue:288-293)— 可选提升 store/inject 共享。 - **useConfirm answerConfirm resolve null 静默**(:43-47)— dev 警告。 - **useAiEvents.ts:25 `t` 的 `as any`** — 已知权衡(绕 vue-i18n TS2589),记录。 --- ## ✅ 亮点 1. **settings.ts mock 删除彻底零残留**(stores 代理):`git log -p` 确认是初始提交纯 mock(98 行硬编码 provider+连接),全 src 零 `useSettingsStore`/`AIProvider`/`GeneralSettings` import(grep 验证)。`api/settings`(HTTP 层)同名不同模块不构成残留。真实 provider CRUD 走 `api/ai.ts`+`stores/ai.ts`,偏好走 `appSettings.ts`,删除干净。 2. **useConfirm 抽取消除重复彻底**(composables 代理):Projects/ProjectDetail/Ideas/Settings/AiChat 五处全迁移,无残留 `window.confirm`、无旧 confirmState 定义。Settings reactive→ref 模板自动解包正确。 3. **useMarkdown 单例+DOMPurify+缓存+兜底四件套**(View 代理):模块级 `_marked/_purify/mdReady/_mdCache` 单例避免多视图重复加载 marked,renderMd 强制 sanitize 杜绝 XSS,未就绪 escapeFallback 降级。三视图复用零成本。 4. **`void mdReady.value` 显式响应式依赖**(View 代理):修了 B-25 漏响应式致首次纯文本后不重算的坑,三处一致 + 注释到位。Vue3 computed 追踪非同步读的正确用法。 5. **findToolCall 反向遍历 + 不建索引的决策有据**(composables 代理):注释详述 state.messages 多处整体替换致独立 Map 索引易失配,反向扫描零额外状态。onStreamTimeout 复制扫描逻辑而非 import 是为避循环依赖,判断准确。 6. **switchConversation 二次 token 比对**(composables 代理):第二个 await 后再 `if (mySwitchId !== _latestSwitchId) return`,防 A→B 切换期间 A 的 pending 覆写 B,补了首个异步点后的竞态漏。 7. **sendMessage IPC 失败回滚完善**(composables 代理):复位 streaming+清 watchdog+移除 user 消息和空气泡,让 handleSend 回填输入框重试。 8. **多选审批状态清空双保险**(View 代理):watch pendingApproval 在 showDialog 前清 multiDecisions,handleApprovalMulti 结束也清,immediate:true 覆盖首次挂载。 9. **demoDag 下线决策有据**(View 代理):R-PD-2 删除根因明确(script 节点不再注册,构造含 script 的 DagDef 会 build_dag 失败),删得干净无死代码。 10. **i18n 中英 key 完整对齐**(composables 代理):aiTool.ts 两份逐行比对,11 个 CRUD key 中英都有,无缺 key。 --- ## 📊 摘要 | # | 等级 | 文件:行 | 修改内容 | 对应 todo | |---|------|---------|----------|-----------| | ① | 🔴P0 | project.ts:273 | selectType→select_type,多选审批静默失效 | B-260615-34 | | ② | 🔴P0 | useAiSend/Stream/Events | timeout/error/stop 收尾清队列,防消息丢失 | B-260615-32 | | ③ | 🔴P0 | useAiStream onStreamTimeout | 回滚 running toolCall,防骨架屏卡死(B-07 残留) | B-260615-33 | | ④ | 🔴P1 | ProjectDetail:201 + useAiStream:39 + AiChat:675 | i18n 一组(多选按钮🔴+错误文案🟡+toast⚪) | CR-260615-08 | | ⑤ | 🟡P1 | ProjectDetail/Ideas/Knowledge/TaskDetail | .ai-md 180 行抽全局样式/组件 | CR-260615-09 | | ⑥ | 🟡P2 | ProjectDetail:251/Ideas:225/Knowledge:249 | useMarkdown useRendered 辅助(同 ⑤ 源) | CR-260615-10 | | ⑦ | 🟡P2 | useAiConversations.ts:81 | JSON.parse 逐条容错 | CR-260615-11 | | ⑧ | 🟡P2 | ToolCard:241,328 + useAiConversations:71 | args 去 as any 收窄类型 | CR-260615-11 | | ⑨ | 🟡P2 | useAiSend.ts:82-86 | approveToolCall 复用 findToolCall | CR-260615-11 | | ⑩ | 🟡P2 | useAiEvents.ts:118 | switch 补 AiHeartbeat case | CR-260615-11 | | ⑪ | 🟡P2 | project.ts:255 | approveHumanApproval 签名改 options 对象 | CR-260615-11 | | ⑫ | 🟡P2 | ToolCard.vue 多处 | formatBytes/命名判定/key 兜底 | CR-260615-11 | | ⚪ | P3 | AiChat:436 等 10 项 | 见 ⚪ 区 | CR-260615-12 | **总计**:🔴4(3 个 P0 功能 + 1 个 P1 文案)🟡8 ⚪10 **总体评价**:工作区改动主干质量高(settings.ts 删除彻底/useConfirm 收敛/useMarkdown 四件套/响应式修复正确/竞态防护到位)。**3 个 P0 功能 bug 集中在 AI 交互异步收尾路径**(队列丢失/骨架屏卡死)+ **1 个 IPC 字段名笔误**(selectType)—— 均非设计缺陷,是收尾路径不完整 + 命名笔误,修复成本低。最大技术债是 ⑤ .ai-md 样式 180 行重复(B-24/25 复制粘贴源头),建议抽全局样式根治。 **质量评级**:良(改 ①②③④ 达优;⑤⑥ 顺带清最大 DRY 债) --- ## 来源 - 4 路并行代理走查(AiChat.vue / View 层 / stores 重构 / composables+ToolCard),2026-06-15,本会话内 spawn。 - 关联已有 todo:B-260615-07(approveToolCall watchdog,③ 残留点)、B-260615-20(handleSend toast i18n 技术债,④ 同源)、B-260615-22(前后端状态不同步,② 相关)、B-260615-24/25(.ai-md 样式首创/复制,⑤ 源头)。