Files
DevFlow/docs/05-代码审查/aichat-技术债审查-2026-06-21.md
绝尘 bd6a41fe6e 新增: 批次工作落地(推进链/评估闭环/事件总线/并发/加固) + 技术债清理 + 文档整理
后端:
- 工作流推进链(D-03):advance_task/状态机/闸门走 df-nodes Node trait,conditions 条件引擎扩展
- 想法评估闭环:启发式评分+对抗评估,df-ideas/scoring + df-storage/idea_eval_repo + idea 前端打通
- 全局事件数据总线:df-ai/context+context_helpers+augmentation 跨模块解耦
- AI planner/plan_hint/intent:aichat B 路线并行多轮基础
- patch_file 加固(TD-03/04):读改写整体锁防 lost update,expected_hash 合约闭环
- 压缩超时兜底(F-15 卡死根治)
- F-09 多会话并发:LlmConcurrency per-conv + streamingGuard 前端守护 + verify 脚本
- 知识注入 DRY/skills/audit 扩展

清理:
- aichat 技术债(误报 allow/死导入/过时注释 30 项)
- URGENT.md 删除(11 项加急全解决/迁 todo)
- 文档整理(todo/待决策/待审查/ARCHITECTURE/INDEX + 总线/技术债审查新文档)
2026-06-21 20:51:26 +08:00

105 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.
# AI Chat 各模块技术债审查报告
> 日期:2026-06-21 | 方法:workflow 串行 10 单元,每单元独立 grep 核验(不信文档/注释) | 基线:src-tauri cargo check 0 error(L2 拆分后)
> 产物:0 P0 / 7 P1 / 25 P2 / 40 P3 | 已登记 8 / 新债 64
> 关联:[[code-review-anti-contamination]] [[devflow-todo-docs-split]] | 审查 workflow wf_92b61c35-4d2
## 模块健康度
| 模块 | 设计点 | 健康 | 债数 |
|---|---|---|---|
| 1-agentic-loop | 8 | {'healthy': 8} | 7 |
| 2-stream-recv | 7 | {'healthy': 7} | 6 |
| DevFlow AI Chat — 3-compress | 8 | {'healthy': 6, 'smell': 2} | 8 |
| AI Chat · tool_registry.rs | 8 | {'healthy': 8} | 8 |
| src-tauri/src/commands/ai/au | 12 | {'healthy': 12} | 7 |
| 6-session-state | 7 | {'healthy': 6, 'smell': 1} | 7 |
| 7-ipc-approval | 8 | {'healthy': 6, 'smell': 1, 'risk': 1} | 6 |
| AI Chat【8-provider-pool】 | 7 | {'healthy': 7} | 6 |
| 9-fe-events-cards | 9 | {'healthy': 9} | 9 |
| 10-fe-conversation | 9 | {'healthy': 9} | 8 |
## P1(7 项 · 已修 2 / 归路线 5)
### [P1] watchdog 单计时器 + onStreamTimeout 全清式收尾,与 F-09 多会话并发冲突:整流超时(可能误报)会 clear 所有 generatingConvs + 清空 queue(含用户已排队未发的输入)+ 清 currentText,误伤其他正在生成的会话与静默丢弃排队用户输入
- **位置**:src/composables/ai/useAiStream.ts:30-32
- **根因**:_streamWatchdog 是模块级单计时器(useAiStream.ts:23)无 per-conv 隔离;onStreamTimeout 注释自陈「无法确定具体 conv,保守全清」。F-09 多会话并发下,A 会话超时会连带杀掉 B 会话的 generating 态;且 state.queue(useAiSend.ts:304 push 用户排队输入)被一刀切,用户排队消息静默丢失(注释称「防丢失」但 clear 本身即丢失)
- **修法**:watchdog 改 per-conv Map<convId, timer>,onStreamTimeout 携带 convId 仅清该 conv 态;queue clear 改为仅清当前 conv 关联项或保留队列(超时不应删用户未发输入)
- **归因**:并发/数据丢失 | 已登记:no
### [P1] patch_file 读-改-写在 FILE_LOCKS 之外,CAS 与 new_content 计算基于陈旧读,锁只序列化最终写不防 lost-update
- **位置**:src-tauri/src/commands/ai/tool_registry.rs:1247
- **根因**:L3 文件指纹校验(1176) + old_text CAS 比对(1209) + new_content 计算(1197-1240)全在「阶段一:无锁纯读」段;L1 tokio::Mutex 仅在「阶段二」保护 backup→tmp→rename 序列(1242-1269)。两个并发 patch_file 同文件:A/B 各自读旧内容→A 获锁写入释放→B 获锁用基于旧内容算的 new_content 覆盖 A 的改动(lost update)。FILE_LOCKS 注释明示「防同文件并发读写冲突」(270)但作用域界定错误,锁只防写写交错不防读写交错。
- **修法**:将读文件+校验+new_content 计算整体移入 FILE_LOCKS 锁临界区(同一 abs_path 的 lock entry),或在锁内重读 content 再做 CAS;改 per-path 独立 Mutex(每个 abs_path 一把锁,非全局 HashMap 占位)让读改写串行。注意 write_file(Medium,trust_hits 并行)不取 FILE_LOCKS,patch_file+write_file 同文件并发也未被保护(同根因)。
- **归因**:并发 | 已登记:no
### [P1] expected_hash schema 契约破裂:描述称「read_file 返回的 file_hash」,但 read_file 从不返回该字段;LLM 无从获取合法 hash,指纹防并发形同虚设
- **位置**:src-tauri/src/commands/ai/tool_registry.rs:1112
- **根因**:grep 全文件 file_hash 仅此一处(1112 schema 描述),read_file handler(892-976)返回 {path,content,size,lines,offset,returned_lines,has_more} 无 file_hash 字段。expected_hash 实际由 patch_file 内部 format!("{}_{}", modified_secs, len)(1180)即造即比,LLM 拿不到前置值只能瞎传或省略→校验常 mismatch bail 或被跳过。L3 防并发设计目标(防行号漂移)在实际 LLM 调用链上落空。
- **修法**:二选一:(a) read_file 返回 file_hash(同 modified_secs_len 格式)让 LLM 闭环回传;(b) 去掉 schema 描述的来源声明,改文档约定由前端审批卡透传(但当前路径无前端介入)。优先 (a)。
- **归因**:契约失配 | 已登记:no
### [P1] 审批状态字符串双轨:ai_approve/ai_authorize_dir 写 "executed",process_tool_calls/find_cached fallback 写 "completed",DTO 文档仅列 completed
- **位置**:src-tauri/src/commands/ai/commands/chat.rs:466,668 (写 "executed") vs src-tauri/src/commands/ai/audit/mod.rs:490,545 + audit/cache.rs:113 (写 "completed")DTO 文档列 completed 在 df-storage/src/models.rs:221
- **根因**:无 newtype/enum 约束 status 字段(对齐 SMELL-P1-6 String 滥用替 enum),两条写入路径各用不同字面量。审计表混存 executed/completed 两种『成功』态
- **修法**:抽 status newtype 或 enum(对齐 SMELL-P1-6),统一成功态字面量(process 全改 executed 或 chat 全改 completed),消除双轨。行为变更需评估 AuditLog.vue 显示与 find_cached 透传影响
- **归因**:类型/一致性 | 已登记:SMELL-P1-6(String 类型滥用,覆盖此根因,未单独登记 executed/completed 双轨)
### [P1] ai_approve 缺 60s 执行超时,与 ai_authorize_dir 不对称,卡死致 generating 永真+前端看门狗静默吞消息
- **位置**:src-tauri/src/commands/ai/commands/chat.rs:461
- **根因**:ai_authorize_dir 为 F-260620 加了 tokio::time::timeout(60s)(chat.rs:658),但同样执行用户审批工具的 ai_approve 路径(state.ai_tools.execute(chat.rs:461) / execute_run_workflow_for_tool(chat.rs:459))无超时。run_command 等慢命令或 list_directory 大目录在 ai_approve 执行路径卡住 → 到不了 audit_finalize/emit/try_continue → IPC 永挂 → per_conv.generating 永真 → 前端 130s 看门狗静默吞消息(F-260620 描述的同类故障在 ai_approve 重现)
- **修法**:抽 helper execute_with_timeout(tool, args),ai_approve 与 ai_authorize_dir 共用同一 60s 超时包裹(含 run_workflow 分支统一),超时走 failed 分支
- **归因**:并发/卡死 | 已登记:no
### [P1] switchConversation 不复位 state.streaming,真并发下切换非生成会话残留 stop 按钮 + 错停会话
- **位置**:src/composables/ai/useAiConversations.ts:91-206(switchConversation 全程不触碰 state.streaming;对比 newConversation:73 显式置 false)
- **根因**:F-09 决策e 真并发上线后,streaming 仍是全局单 boolean(state.ts:63)。switchConversation 加载历史时不复位 streaming,而 ChatInput.vue:88 v-if="store.state.streaming" 读此单值显示停止按钮。会话 A 生成中切到非生成的会话 B → streaming 仍 true → B 显示 stop 按钮 → 点击 store.stopChat 传 B 的 activeConversationId(:427)发出错会话的 stop IPC。AiChat.vue:204 showAgenticProgress(streaming && agentRound>0)同样残留。
- **修法**:streaming 改 per-conv 态(归入 F-260616-09 B 路线 per-conv state 改造);或 switchConversation 入口按 isGenerating(targetConvId) 重算 streaming(active 在生成则 true 否则 false),与 newConversation:73 对齐。后者轻量可在 A 路线内补。
- **归因**:竞态/状态残留 | 已登记:no
### [P1] pendingDirAuth/pendingMaxRounds 全局单 ref 非 per-conv,真并发下挂起弹窗可能错显于非挂起会话
- **位置**:src/composables/ai/useAiEvents.ts:99(pendingMaxRounds=ref(false)) + :113(pendingDirAuth=ref<...|null>(null));消费方 DirAuthDialog.vue:60-62 / MaxRoundsCard.vue:54-56 仅判 isViewingGenerating 未比对 conversationId
- **根因**:F-09 决策e 允许多会话并行,但挂起态仍是模块级单实例。pendingDirAuth 携 conversationId(:111/:239)但 DirAuthDialog:61 showDirAuthCard 守卫未用(pendingDirAuth.value.conversationId === activeConversationId);pendingMaxRounds 是纯 boolean 无 conversationId 字段,根本无法区分。会话 A 达 max 挂起(pendingMaxRounds=true)时切到同样在生成的会话 B → MaxRoundsCard 在 B 错显(因 isViewingGenerating(B)=true 且全局 pendingMaxRounds=true)。原注释 useAiEvents.ts:97『后端 AiSession 单例,达 max 后续轮次前用户必先决定』是单并发假设,真并发下失效。
- **修法**:pendingMaxRounds 改 ref<string|null>(挂起 convId);pendingDirAuth 守卫加 conversationId 比对。归入 F-260616-09 B 路线 per-conv state。
- **归因**:竞态/状态残留 | 已登记:no
## P2(25 项)
- **1-agentic-loop** | try_continue_agent_loop 中 pending_conv_id 跨会话收集,导致 stop 补发 AiCompleted 的 conversation_id 错绑其他会话 | `src-tauri/src/commands/ai/agentic/mod.rs:1314-1317` | todo:no
- **1-agentic-loop** | run_agentic_loop 函数 ~950 行(L318-1265),单函数承载:provider 解析+意图收敛+主题标记+自动压缩+tool_result 摘要+fallback 重试+审批分支+收敛退出,God 函数 | `src-tauri/src/commands/ai/agentic/mod.rs:318` | todo:no
- **2-stream-recv** | MidStream 保文路径 chunk.error / chunk.Err 返回 Partial 时未把 err_msg 透出,用户看不到中断原因(仅后端 warn! 日志),前端只补通用「响应不完整」系统提示,丢失具体错误诊断(如 GLM 1214 messages 非法) | `src-tauri/src/commands/ai/stream_recv.rs:275-281` | todo:no
- **2-stream-recv** | onStreamTimeout 反向遍历 messages 改 toolCall.status = 'rejected',在多会话并发下会误回滚其他会话的 running 工具卡(与 P1 全清问题同源,单遍历无 conv 过滤) | `src/composables/ai/useAiStream.ts:48-60` | todo:no
- **DevFlow AI Chat — 3-** | is_compressing 标志无 panic/cancel 兜底 — 任务被 cancel(runtime drop/进程关闭)或 compress_via_llm 内部 panic 时 set_compressing(false) 被跳过,标志永久滞留 true,该会话后续所有自动/手动压缩被 !prev_compressing(705)/is_compressing()(chat.rs:850) 永久拒绝 | `src-tauri/src/commands/ai/agentic/mod.rs:735 (set_compressin` | todo:no
- **DevFlow AI Chat — 3-** | 自动压缩(agentic loop)成功后未即时 save_conversation — 仅依赖 loop 退出 :1223 落库。若 loop 在压缩后、退出前被 cancel/panic/审批等待态 return,则 status=compressed + 摘要 system 仅存内存,会话恢复(restore_from_messages 从 DB 重建)时压缩成果丢失,history_tokens 重新虚高 | `src-tauri/src/commands/ai/agentic/mod.rs:778-794 (成功路径无 save` | todo:no
- **DevFlow AI Chat — 3-** | context.rs 1332+ 行 God 文件 — F-15/压缩/UX-09编辑/sanitize/主题检测多职责堆叠,F-15 链路仍频繁改动拆分窗口未到 | `crates/df-ai/src/context.rs:1-1552 (整文件,ContextManager impl ` | todo:REFACTOR-260619-09
- **AI Chat · tool_regis** | FILE_LOCKS HashMap 条目永不清理,每 patch 过的路径永久占槽位(内存无界增长) | `src-tauri/src/commands/ai/tool_registry.rs:1248` | todo:no
- **AI Chat · tool_regis** | expected_hash 指纹粒度仅秒级(mtime_as_secs + len),1 秒内连续外部修改检测不到,弱指纹 | `src-tauri/src/commands/ai/tool_registry.rs:1180` | todo:no
- **AI Chat · tool_regis** | patch_file 大小校验用独立 tokio::fs::metadata 后再 File::open,存在 TOCTOU(metadata→open 间文件被换/膨胀),与 read_file 单次 open 取 metadata 的 FR-S2 收口不一致 | `src-tauri/src/commands/ai/tool_registry.rs:1156` | todo:no
- **AI Chat · tool_regis** | bind_directory handler 注释承诺 reload 兜底但代码空头(tool_registry.rs:545-549),AI 绑定目录后白名单不刷新 | `src-tauri/src/commands/ai/tool_registry.rs:546` | todo:MED-1 已登记
- **AI Chat · tool_regis** | REFACTOR-260619-08 tool_registry.rs(2210 行)仍待按功能分组注册函数拆,register_* 部分超 200 行(register_file_tools ~760 行) | `src-tauri/src/commands/ai/tool_registry.rs:881` | todo:REFACTOR-260619-08
- **src-tauri/src/comman** | ai_approve 缺执行超时包装(ai_authorize_dir 有 60s 防卡死,ai_approve 裸 execute) | `src-tauri/src/commands/ai/commands/chat.rs:461 (ai_approve 裸` | todo:no
- **src-tauri/src/comman** | find_cached_high_risk_result 持 session &mut borrow 期间串行 await DB 查询,High risk 工具多时锁持有线性增长 | `src-tauri/src/commands/ai/audit/cache.rs:21-23 (性能注记自承) + ca` | todo:no(性能注记自承,CR-260618-11#5 衍生,未独立登 todo 条目)
- **src-tauri/src/comman** | audit_tool_call 插入用 let _ = 吞 insert 错误,审计记录写失败无任何日志/告警 | `src-tauri/src/commands/ai/audit/finalize.rs:35 (let _ = repo` | todo:no
- **6-session-state** | PerConvState.created_at 全死字段(0 写入) | `src/commands/ai/mod.rs:520` | todo:no
- **6-session-state** | PendingApproval.diff 结构体字段写而不读(dead payload) | `src/commands/ai/mod.rs:570` | todo:no
- **6-session-state** | process_tool_calls 持 session 锁做阻塞 fs canonicalize(粗粒度锁内同步 IO) | `src/commands/ai/audit/mod.rs:264` | todo:no
- **7-ipc-approval** | switch/delete 路径 retain 清 pending 未配套 finalize_pending_placeholders,占位 tool_result 残留 messages | `src-tauri/src/commands/ai/commands/conversation.rs:317` | todo:no
- **7-ipc-approval** | authz_debug 硬编码绝对路径 E:/wk-lab/devflow/authz-debug.log + 生产残留诊断代码(eprintln + 文件日志) | `src-tauri/src/commands/ai/commands/chat.rs:548` | todo:no
- **AI Chat【8-provider-p** | agentic/mod.rs run_agentic_loop 单函数超长(83390 字节/约 1231 行),fallback 循环 + 工具执行 + 审批 + 保文路径全在一函数 | `src-tauri/src/commands/ai/agentic/mod.rs:318 run_agentic_loo` | todo:ARC-260619-05(REFACTOR-260619-05 已部分完成,D 段待 F-09 批4)
- **9-fe-events-cards** | AiMessage.status 为 string cast(AiMessageWithStatus),消息级 status 无联合类型约束,archived_segment/compressed/truncated 全靠注释枚举 | `src/components/ai/MessageList.vue:686-688 (interface AiMessa` | todo:no
- **9-fe-events-cards** | onStreamTimeout 全清 generatingConvs(多会话并发下误杀其他正常生成会话) | `src/composables/ai/useAiStream.ts:30 (state.generatingConvs.` | todo:no
- **10-fe-conversation** | switchConversation 不复位 agentRound,切到已完成会话残留旧轮次(进度条门控依赖 streaming 可部分遮蔽) | `src/composables/ai/useAiConversations.ts:91-206(switchConver` | todo:no
- **10-fe-conversation** | loadConversations 失败时把错误气泡 push 进 state.messages,污染当前对话历史(而非用 toast) | `src/composables/ai/useAiConversations.ts:51-61 + switchConve` | todo:no
## P3(40 项 · 摘要)
- **1-agentic-loop**(5项): guard.rs 文档注释声称"双写顶层 session.generating=; guard.rs Drop 异步 spawn 复位与正常路径 reset().a; iteration 顶部 stop 检查(L581)的 save 用 None ; 候选链 candidate_chain 提前 clone primary(L97...
- **2-stream-recv**(3项): classify_status_or_class 与 extract_error; STREAM_TIMEOUT_MS=130s vs 后端 STREAM_IDLE; scheduleStreamParse 的 rAF 回调内 streamingB
- **DevFlow AI Chat — 3-comp**(5项): should_compress 检查与 set_compressing(true; messages_mut() 被迫用于只读读取 — 读取保护区外 active ; compress.rs #[allow(dead_code)] 过期残留 — c; 双监听器同通道('ai-chat-event')人工协调防双重处理 — useA...
- **AI Chat · tool_registry.**(1项): run_command 越界防线弱:working_dir 仅走 validat
- **src-tauri/src/commands/a**(3项): Low risk 失败工具硬编码中文错误文案『工具 {} 执行失败: {}』直送; build_approval_reason 三层 fallback(tool_d; path_auth_pending 挂起占位无去重保护(与 High risk
- **6-session-state**(4项): session_state() 读视图 0 消费者(P0 终态化未承接); guard.rs 注释声称双写顶层 session.generating 但代码; best_effort_canonicalize 文件不存在时连查父目录,失败回; releases/node_executions Repo 持久化就位但 IPC
- **7-ipc-approval**(3项): ai_authorize_dir 60s 超时与 generating 状态:超; ai_chat_clear 无 conversation_id 参数,只能清 a; ai_approve 拒绝路径 _recovered 读后未用,审批拒绝无 se
- **AI Chat【8-provider-pool】**(5项): release_conv 仅 ai_conversation_delete 调用; reload_provider_caps 给每 provider 设 cap=g; set_per_conv / set_global 缺内部 permits=0 ; set_per_conv 清空 HashMap 软收敛期间,同 conv 的在跑...
- **9-fe-events-cards**(7项): 双监听器同通道(useAiEvents + useAiContext 各 lis; AiStreamRetry 每次重试重置看门狗(不在 NO_RESET_WATC; _blockCache(300 条 Map)跨会话永不清理,长期会话累积陈旧条目; combineAndTruncateLines/formatCommandRes...
- **10-fe-conversation**(4项): switchConversation pendingApprovals 恢复修改; switchConversation 内局部变量 t 覆盖模块级 t impor; doSend/regenerate/editMessage 三处 streami; modelOverride 切换对话不同步清(注释自述需同步清但代码未做)