Files
DevFlow/docs/05-代码审查/aichat三路走查报告-2026-10-02.md

122 lines
18 KiB
Markdown
Raw Permalink 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.
# aichat 三路走查报告(2026-10-02)
> 走查代理:rev-backend(src-tauri/src/commands/ai)/ rev-frontend(src/composables/ai + src/components/ai)/ rev-crates(crates/df-ai + df-storage)。
> 背景:上下文管理信息组织批收口期增量走查,为批次2(后端高危)/批次3(前端体验)提供输入。硬性排除清单(已修 B1-B5/D1-D7/已登记 todo/范围外决策项)均已遵守,结论以当前源码为准。
> 主代理核验说明:后端 P0-1 已亲核实锤(chat.rs:1697-1740 conv_id 分裂确认);crates P0-1 与其余 P0/P1 为代理独立源码复核(报告内标注),未逐项亲验——批次开工时以现场源码为准再确认。
---
## 一、后端走查(rev-backend)
范围:src-tauri/src/commands/ai/ 约 24 个核心文件全文精读(主循环/流式中继/审计工具执行/审批门控/路径授权/会话持久化/事件总线/子代理/上下文生命周期/通知),其余约 55 文件(tools/*/augmentation/* 等)按四类模式扫描(裸 emit/裸 conv()/锁内 unwrap/状态复位配对)。
### P0-1 ai_chat_compress_context 的 conv_id 分裂:跨会话压缩污染 + compressing 标志永久卡死【已亲核实锤】
- 位置:src-tauri/src/commands/ai/commands/chat.rs:1697-1736(读取段)vs :1743/:1762/:1800-1805/:1827-1830(写入段)
- 根因:读取段全部落在 IPC 参数 conversation_id 上(:1701 conv()/:1710 set_compressing(true)/:1712 protect_start/:1727 克隆消息);但 :1734 `let cid = session.active_conversation_id.clone().unwrap_or_else(...)` 之后所有突变(复位 compressing/compress_old_messages/insert_at(0, 摘要)/save)全落 cid(active 会话)。F-260616-09 批4 决策 e 使「压缩后台会话」成为受支持路径,非边角。
- 影响(压缩任一非当前会话 100% 触发):①目标 conv is_compressing 置 true 后无出口复位→手动/自动压缩从此永久跳过;②active 会话前 compress_end 条消息被误标 compressed 并插入由后台会话内容生成的摘要——跨会话数据污染,经 save 持久化进 DB;③:1716 protect_start==0 早退分支 cid 无 fallback 与主路径不一致。
- 修复:统一单一标识,:1734 改 `let cid = conversation_id.clone()`(AiCompressing/AiManualCompressed 事件已按 conversation_id 路由);约 4 行,收益/风险比极高,批次2首位。
### P1(3 项)
- **P1-1 remote_bridge 6 处 AiError 单路 emit**:remote_bridge.rs:812/:871/:951/:1000/:1034/:1066,miniapp 专属错误回执用裸 app.emit(桌面前端通道),miniapp 只能经 ai_event_bus 收——手机端发送失败完全无感知(「发了没反应」),桌面反收无关错误气泡。同模块 AiUserMessage 已按 BUG-260805-02 修为 emit_ai_chat_event 双写,此 6 处系同族漏网。修复:替换为 emit_ai_chat_event(同文件已有 import),机械改动可批量。
- **P1-2 只读工具结果缓存无写失效**:audit/cache.rs find_cached_readonly_result 按「工具名+参数」命中即回放,写类操作后同参数读命中旧缓存→LLM 写后读回验证拿到写前内容,可能据此二次「修复」或误报失败。与 G2 写间隔豁免的设计哲学矛盾(写后验证恰是最需要真实读回的场景)。修复:per_conv 维护「自上次写类成功后的脏标记」,写后缓存整体失效(不建议按参数精细失效)。
- **P1-3 ai_conversation_delete 不停 in-flight loop**:conversation.rs:608-640 无 stop_flag/无 epoch bump(对比 ai_chat_clear 先置停)。删除后当前轮(流式+工具执行+审批等待)完整跑完:继续烧 token/继续执行已授权高危工具;审批挂起中被删要等满 15 分钟外层超时;audit/mod.rs 9 处 `session.conv(conv_id).messages.push` 收尾回填惰性重建已删 conv 条目→zombie 复活(deleted_convs 只拦 DB 写拦不住内存重建)。修复:对齐 ai_chat_clear 删除时锁内置停+notify+bump epoch;execute_with_heartbeat 等待循环加「conv 已删即提前退出」。
### P2(5 项)
- **P2-1** agentic/mod.rs 4 处裸 emit 绕过正典(:1863 heartbeat/:1939 熔断求助/:2072 写删回环熔断/:2102 探索熔断)→miniapp 收不到心跳与熔断卡;AiCompleted :2331 双路分叉是有意设计勿动。修复:替换 emit_ai_chat_event。
- **P2-2** maybe_auto_compress protect_start 跨锁过期可致切片 panic:context_lifecycle.rs:102 锁内算 :118 锁外用,窗口内 clear_context 清到剩 6 条→`[..94]` index out of bounds,loop task 溃亡(仅 F2 看门狗兜底,而 F2 前移正是已登记待办)。修复:锁内 `let cut = protect_start.min(mgr.len())` 一行钳制。
- **P2-3** try_continue_agent_loop 裸 emit 零 token AiCompleted 绕过 emit_ai_completed_once(helpers.rs:100/:171)→挂起审批时被 continue 接管会残留占位 tool_result 与 pending 条目(__PENDING__: 标记风险)。修复:改调正典出口或补 finalize 两行。
- **P2-4** save_conversation 幽灵复活防线 Ok(Some) 分支缺二段复检(conversation.rs save 路径):get_by_id 返回 Some→删除事务提交→update_full/insert_batch 把已删会话消息写回(孤儿行)。修复:写库前再查 deleted_convs。
- **P2-5** stream_relay Fatal 路径 emit 先于 save(:454-458)与「先持久化后 emit」约定相反,emit 后进程退→前端有错误 DB 丢最后一轮;同文件全候选 Exhausted 文案把「provider 池饱和跳过」报成「重试耗尽」误导排障。修复:调换顺序;文案区分饱和/重试。
### P3(合并)
command_lock.rs 全仓零调用(spawn_agent 上线后建议接线或标注预留);update_pinned_goals 部分去重判定偏差;ai_approve None 分支持锁跨 DB await;clear_once_allowed_dirs 全清误伤多会话 once 授权;save_checkpoint 用 persist_msgs.len() 冒充 token 阈值+捷径下永不触发(疑似死代码);prev_compressing 双锁窗口 auto+manual 并发双压缩;保护区间 insert_at(0) 与后台压缩并发致压缩边界偏移一条;GOAL_VERIFY 续轮跨轮 Tool 段重复计数;HashMap.values() 乱序仅影响展示。
---
## 二、前端走查(rev-frontend)
范围:src/composables/ai/ 24 文件 + src/components/ai/ 16 文件全量,i18n ai/aiChat/aiTool 命名空间全扫。已对照排除清单,与 todo 第一批/9-28 走查无重复。
### P1(2 项)
- **P1-1 后台会话审批/路径授权事件被 isCurrent 守卫整体丢弃→挂起审批不可见、会话无限期挂死**:useAiEvents.ts:94-123 `!isCurrent` 分支只处理 Completed/Error/HelpRequired,其余事件一律 return——AiApprovalRequired/AiDirAuthRequired 落缝。后台会话 B 审批挂起时:不建审批卡/不进 pendingApprovals/不启超时计时器/不弹浮窗,后端 loop 阻塞等审批 IPC 而前端零信号→无限期挂死(用户切回经 pendingToolCalls 恢复才可见,但无「该切回」提示)。与已登记 #8 徽标计数不同源(#8 是已有项的计数,本条是上游路由丢失)。修复:!isCurrent 分支对 ApprovalRequired/DirAuthRequired 放行给 handleToolEvent(两 handler 已写 conversationId,findToolCall 找不到卡片自然跳过,无视图污染)。
- **P1-2 switchConversation「对话不存在」兜底分支漏置 activeConversationId→视图/高亮/发送目标三重错配**:useAiConversations.ts:294-309 该分支新建会话并持久化+清空视图,但没置 state.activeConversationId 即 return(对比 newConversation:101 与正常路径:354 都有)。触发=点击后端已不存在的会话(重启后未落库虚拟会话/跨窗口删除竞态):侧栏高亮指旧会话,sendMessage 把消息发进用户刚离开的会话,持久化与内存分叉。修复:补一行置 activeId+对齐 per-conv 游标重算,或直接复用 switchConversation 统一路径。
### P2(6 项)
- **P2-1** _lastDeltas 去重 Map 双路径漏清(aiShared.ts:400-420;漏清点 useAiEvents.ts:96-115 后台终止/:154-161 跨窗口删除)→无界残留+切回后首条 delta 被去重误丢(回复开头缺字符)。修复:两处补 clearLastDelta 或收敛进 clearConvStreamState 成对清理。
- **P2-2** startListener 部分失败→listener 泄漏+双注册(useAiEvents.ts:142-175):onEvent 成功后续 listen() 抛错不回滚,_startPromise 清零后下次全量重注册→流式文字双倍。修复:catch 内补偿 unlisten 或单数组统一回滚。
- **P2-3** 看门狗 legacy/per-conv 双轨迁移半途三错配(useAiSend.ts:101 vs :169/:241/:512):stopChat 清不到 per-conv timer(停止成功 90s 后补莫名「已收敛」气泡或误报中断);regenerate/editMessage 走 legacy forceConverge→convStates.clear() 抹掉其他并发会话生成态;_resumeAttempts 会话完成后不清,下次续跑额度缩水。修复:统一迁移传 convId(债务本就要还)+clear 时顺带删 _resumeAttempts。
- **P2-4** removeGoal 乐观更新不回写 store/失败不回滚→已删目标「复活」(TopBar.vue:530-539;loadGoals 读 conv.pinned_goals 重灌)。修复:成功同步写 pinned_goals 或触发 refresh,失败回滚+toast。
- **P2-5** 切换/新建会话整体覆写 state.pendingApprovals(useAiConversations.ts:419/:462 同族 newConversation:110/clearChat useAiPanel.ts:191)→聚合层 per-conv 共存被旁路,切走期间后台会话审批入口全盲(与 P1-1 叠加成「后台审批双盲」)。修复:恢复路径按 conversationId 合并(保留其他 conv 段)。
- **P2-6** i18n 地雷:searchFilesBlocked 值含裸 @(zh-CN/aiChat.ts:198 en/aiChat.ts:197,`@[项目]` 未按 {'@'} 转义)——当前死键无消费方不炸,接线即踩。修复:转义或删死键。
### P3(8 项合并)
clearAllApprovalTimers 全清误伤并发会话审批倒计时;整轮收尾 clearAllToolSlowTimers 全清丢后台慢执行 toast;删除会话双调 switchConversation(冗余非 bug);activeId 空窗被后台首事件抢占;切会话未重置 expandedLongIds/JsonIds/SegmentIds/ThinkIds(跨会话展开态残留+fallback id 跨会话撞 key);分离窗口 X 关不清 df-ai-gen-*/df-ai-text-*;clearChat 清审批不清计时器(到点自动拒+气泡进已清空视图);@任务联想缓存组件生命周期不失效。
### 排查干净面(免重查)
事件监听器清理全成对;per-conv Map 主干写收敛正确;useToolCard/纯函数层无状态问题;AiUserMessage 自回灌疑点排除;组件 i18n 除 P2-6 无特殊字符地雷。
---
## 三、crates 走查(rev-crates)
范围:df-ai 30 文件+df-storage 24 文件全量,主代理亲读迁移链/message_repo/conversation_repo/idea_repo/settings 白名单/ai_tools 注册/context/retry/lib,4 并行子审查覆盖 compat/context/storage/编排网络层。未覆盖 secret.rs(权限拒绝)。
### P0-1 V21 迁移 COUNT 幂等窗口:中途崩溃→ai_messages 永久残缺且被版本号掩盖【代理已核验】
- 位置:crates/df-storage/src/migrations.rs:481-488, :570
- 根因:批量迁移(每 50 对话一事务)先 SELECT COUNT(*) FROM ai_messages,>0 即跳过全部数据迁移并写版本号;版本号 21 在所有批次跑完后才写。批次中途崩溃/断电,重跑时 COUNT>0→剩余对话永不再迁,但版本号照样补齐,迁移链视为完成。
- 影响:该批对话 ai_messages 永久缺行(list_recent 在用读路径翻历史缺消息;原始 JSON 还在 conversations.messages 列,非真丢)。
- 修复:按对话进度标记(临时表记已完成 conv_id)替代全局 COUNT;或每批成功后写进度,重跑从断点续。
### P1(12 项,逐条)
1. **OpenAI 流式 error 帧/usage 尾帧因 choices 必填被整帧静默丢弃**(openai_helpers.rs:193-203 choices 无 default,:260,:349 Err→debug! 吞):标准 error 帧 `{"error":{...}}` 无 choices 键→serde 失败被吞,:265-280 error 处理对最常见形态是死代码,中转站流中途报错呈「无声截断」。修复:choices/delta/index 加 default+补无-choices 单测。
2. **同步响应 .json() 的 serde 结构错被判 Retryable**(anthropic_compat.rs:629-638/openai_compat.rs:527-535):reqwest 0.12 把 serde 错误包成 is_decode()==true 先命中 Retryable——确定性结构错重试 3 次白烧 30s。修复:先 resp.text() 再 from_str(读中断→Retryable,结构错→Fatal)。
3. **Anthropic usage/id 必填非 Option**(anthropic_helpers.rs:91-105/:66):网关 200+缺 usage→整响应失败,有效文本回复整体丢弃(叠加#2 还重试 3 次)。修复:usage 改 Option+缺失 warn。
4. **stop_reason 全链路不可见→max_tokens 4096 截断静默零痕迹**(anthropic_helpers.rs:71/:176-184;OpenAI 侧有 FinishReason 告警,不对称)。修复:stop_reason=="max_tokens" warn。
5. **TokenUsage 口径两栈漂移**(openai_compat.rs:542-549 缺失全零静默入库 vs Anthropic 缺失硬失败;anthropic cache 段少记 prompt 不含 cache_read/creation)。修复:None warn 留痕;cache 场景 prompt=input+creation+read。
6. **embed() 无超时+返回条数不校验**(openai_compat.rs:449-474):body 可无限 hang;data.len()<texts.len() 不校验→检索向量错位。修复:tokio timeout+len 断言。
7. **model_fetch 空 id 列表返回 Ok([])→上层写回清空用户模型池**(model_fetch.rs:169-186/:147-163;src-tauri 写回点待核):一次畸形/空响应静默清空 model_configs(含 UserSet 标注)。修复:空结果返回 Err/标记;写回点空列表不落库。
8. **ai_messages INSERT OR IGNORE 静默丢写**(message_repo.rs:86-102/:289-307):UNIQUE(conv,seq) 冲突整条 IGNORE 仍 Ok(()),调用方只判 Err;增量 insert_batch 路径 seq 重排/双写即静默丢消息。修复:核对 affected 与 records.len()。
9. **整行/CAS 更新缺软删防护**(task_repo.rs:800/project_repo.rs:452/宏 crud/mod.rs:183-196):fbd8fae 给 update_field_active 加了 deleted_at IS NULL,整行路径没跟上——回收站任务可被 df-mcp CAS 改写。修复:CAS/整行 UPDATE 补 deleted_at IS NULL。
10. **module_dependency_repo::list_by_field 列名直拼无白名单**(module_dependency_repo.rs:72-81,全 crate 唯一例外;当前调用方均字面量暂不可达注入,API 约定破口)。修复:白名单限定 5 列。
11. **truncate_after_user_message 标 truncated 不回收 history_tokens**(context/mod.rs:385-406):唯一不同步扣还的路径,编辑重生成后 history_tokens 永久虚高→过早裁剪+under-trim 超预算发送。修复:标记时逐条 saturating_sub。
12. **sanitize 合并连续同 role 丢多模态 parts**(context/sanitize.rs:406-425):合并只拼 content+tool_calls,parts 不迁移,空 content 纯图消息整个消失;触发路径现实(裁剪概要摘要与首个保留 user 相邻必合并,该 user 带图则图对 LLM 隐身)。修复:合并分支 extend parts。
### P2(34 项,一行一条)
df-storage:①from_row 吞列错误为默认值(conversation_repo.rs:25,58-63,100);②手写 BEGIN/COMMIT panic 残留打开事务(:259-296 改 guard.transaction());③insert_checkpoint OR IGNORE 静默跳过(:609-636);④limit=0 语义分裂(message_repo.rs:164);⑤父任务软删校验缺失致回收站父下隐形子任务(task_validation.rs:50-71);⑥project_service 宏绕过 validate_record/D10(project_service_repo.rs:128-159);⑦模块删除未清依赖边 FK 报错(project_module_repo.rs:172-205);⑧CAS 以 TEXT 等值比较 updated_at(task_repo.rs:800);⑨未知 status 静默变默认值+update_full 回写放大(task_repo.rs:30/idea_repo.rs:107/project_repo.rs:114);⑩V43 confidence='high' 精确小写匹配 vs 提炼侧无归一化(migrations.rs:1304,非小写条目滞留靠 include_candidates 兜底)。
df-ai:⑪intent "hi"/"api" 子串无词边界,history 等全判 Chat 0.9(intent.rs:186,194,420-424);⑫restore_from_messages 重放造假 pending_topic_marker(mod.rs:308-319,加载后首轮假话题锚点);⑬URL 图 token 地板被 ×0.35 稀释 1600→560(context_helpers.rs:73-80);⑭sync_after_compress 吞 JSON 解析失败(:881-902);⑮build_injection 脏窗口 off-by-one 多注一轮(:837);⑯TOOL_MISSING_PREFIX 前缀=已配对盲区(sanitize.rs:236/315);⑰manager_tests 恒零断言+同义反复(:401-405/:484-489);⑱begin_turn 喂变换后文本(src-tauri chat.rs:810/2112 调用方漂移);⑲tool_signature 漂移:read_file 缺 search、search_files/grep 缺 offset/limit/-i/output_mode、run_command 缺 working_dir(合法换参重调共享签名,与 dd0afcf 同类);⑳ai_tools.rs:241 SigValKind::Json 组只用 keys[0](latent)。
编排/网络(多 latent):㉑git_worktree merge-tree 传参预检失效(git_worktree.rs:174-188);㉒coordinator JoinSet 子任务 panic 结果静默缺项(coordinator.rs:511-520);㉓worktree/branch 清理吞错恒 Ok(:224-295);㉔router 全禁用静默回落 default_model(router.rs:96-101);㉕plan_executor 串行冒充并行+重复 id 假环(plan_executor.rs:65-91);㉖namespace_store 上限可永久突破(:144-157);㉗retry 位移溢出+预算最坏 121s≠文档 30s(retry.rs:77/:126-135);㉘未知 provider_type 静默回落 OpenAI(lib.rs:143-150);㉙sse_parser BUF_MAX 溢出截头致 JSON 破损(sse_parser.rs:150-157);㉚message_start 缺失 prompt_tokens 兜底 0;㉛同步 tool_use input 缺失 args="";㉜content_filter 不算 finished,无 [DONE] 流无终态(openai_helpers.rs:303-305);㉝model_probe "-v"/"vl" 误判 Vision(:118)。
### P3(合并)
u64→u32 as 截断(不可达);complete 内联 headers 未复用;同步 reasoning_tokens 恒 0;BOM 首事件;begin_turn 首行注释与实现矛盾(context_helpers.rs:857-862);retry 单测界宽于实现。
### 已核对无漂移(免重查)
V42 is_estimated/V33 pending_approvals/V41 module_id/V29 三列/V24 related_ids 迁移链全对齐;V43 迁移本身幂等可重入测试完备;KNOWLEDGE_COLS 与 KnowledgeRecord 15 字段一致;.optional() 用法全对;anthropic 双栈 SSE 多行拼接/跨 chunk UTF-8/thinking/tool_call 空 id fallback 自洽。
### 待下批核对
save_conversation 是否检查 needs_full_rewrite 再走增量路径(advance_persisted_count 无条件清 dirty,漏查即静默漏更新旧行);secret.rs(本次权限拒绝未读)。
---
## 四、批次归并建议(主代理整理)
- **批次2(后端高危)并入**:后端 P0-1(首位,4 行)+P1-3(删除不停 loop,与已登记 conv 删除族同域合并做)+P2-2(切片钳制一行,与 F2 前移同域);crates P0-1(V21 迁移进度标记)+P1-1/P1-2(流式 error 帧与 Retryable 误判,同文件同批)+P1-12(sanitize 丢图)。
- **批次3(前端体验)并入**:前端 P1-1(后台审批路由,与本批 incomplete/守卫同文件)+P1-2(兜底漏置 activeId 一行)+P2-5(审批数组 per-conv 合并,与 P1-1 同域)+P2-1/P2-3(看门狗双轨迁移,本就要还的债)。
- **机械批(可独立小批)**:remote_bridge 6 处+agentic 4 处裸 emit 换 emit_ai_chat_event;P2-6 死键转义。
- **登记观察**:crates P1-7(model_fetch 清空模型池,src-tauri 写回点待核后定级);其余 P2 按域分批消化。