# 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()