# 工作区多角度代码走查（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<String>`（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 行 → 抽全局样式或 `<AiMarkdown>` 组件（最大 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（彻底）— 抽 `<AiMarkdown :text>` 组件含 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<string, unknown>`，出口收窄 + 消费方类型守卫。

### ⑨ 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 样式首创/复制，⑤ 源头）。
