Files
DevFlow/docs/05-代码审查/工作区多角度走查-2026-06-15.md
绝尘 f30df333b3 docs: 巡检简报+todo 回写(2026-06-15 第2轮)
巡检发现:
- 2196c77 workflow 整文件替换回退破坏(AiChat+Ideas 12项功能)
- B-260615-03 truncated 标志已落地
- AR-8-scroll scrollToBottom smooth 已补
- CR-260615-09 .ai-md 残余:4详情页各21处 scoped .ai-md
  (全局 ai-md.css 75行已建,旧副本待清理但非阻塞)
2026-06-15 17:23:57 +08:00

206 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 工作区多角度代码走查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)(块级 memoCR-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` caseuseAiEvents.ts:222-237/ `stopChat` 三处收尾路径都应清空队列并提示(或回填输入框):
```diff
// onStreamTimeout() 末尾clearStreamWatchdog() 之后
+ if (state.queue.length) {
+ state.queue = []
+ content += '\n(待发送队列中的消息已取消,请重新发送)'
+ }
```
`approveToolCall`useAiSend.ts:82-107IPC 失败 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-24TaskDetail首创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(...)`,而 findToolCalluseAiEvents: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 与 0shouldKeepOpen 与 toolCategory 命名判定风格不一(:131 精确 vs :206 includesv-for `:key="arg.key"` 无 index 兜底(:33LLM 畸形 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` 确认是初始提交纯 mock98 行硬编码 provider+连接),全 src 零 `useSettingsStore`/`AIProvider`/`GeneralSettings` importgrep 验证)。`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` 单例避免多视图重复加载 markedrenderMd 强制 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 前清 multiDecisionshandleApprovalMulti 结束也清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 |
**总计**🔴43 个 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+ToolCard2026-06-15本会话内 spawn。
- 关联已有 todoB-260615-07approveToolCall watchdog③ 残留点、B-260615-20handleSend toast i18n 技术债,④ 同源、B-260615-22前后端状态不同步② 相关、B-260615-24/25.ai-md 样式首创/复制,⑤ 源头)。