122 lines
18 KiB
Markdown
122 lines
18 KiB
Markdown
# 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 按域分批消化。
|