diff --git a/docs/todo.md b/docs/todo.md index 69627fa..b8f0759 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -77,6 +77,18 @@ - [ ] B-260616-21 [P1] — **aichat 工具卡片「重复读取同一文件」表象(read_file 出现两条卡:一 0 行·7.1KB running、一 183 行·7.1KB completed)**。用户报对话记录里 `读取 .../api/ai.ts` 出现两次。**根因分析**(已走查前端流转 + 后端 emit 点):①前端 `useAiEvents.ts:202-206` `AiToolCallStarted` 分支直接 `lastMsg.toolCalls.push(info)`,**无 id 幂等守卫**(对比同文件 `startToolSlowTimer:58` 有 `_toolTimers.has(callId)` 守卫,Started 漏了同款判重)②`findToolCall`(`aiShared.ts:41-49`) 按 `id` 命中**首个**返回,`AiToolCallCompleted`(`:213`) 只 update 第一张卡 → 同 id 若被 push 两次,第二张卡永远 running,`ToolCard.vue:72` `parsed?.lines||0` 无 result 时显示 `0 行` ③后端 `audit.rs:538-549` `process_tool_calls` 对 tc_list 每 draft emit 一次 Started;read_file=Low risk(`:561`) **无去重**(去重仅 High,`:553-556` 注释明确 Low 不进)。**两候选根因**:**(A 最贴合现象)** 同一 `tool_call_id` 被**重复 emit Started**(后端 `process_tool_calls` 重入 / `stream_recv.rs` 生成重复 id / `agentic.rs` loop 重 process 同轮)→ push 两张卡 → Completed 只更首张(183 行)→ 次张残留 running(0 行);**(B)** agent loop 多轮 LLM 真读两次(不同 id)→ 但应两卡皆 183 行,与「0 行 + 183 行」现象不符,**排除为主因**。**待查**:后端同 id 重复 emit 的确切路径(`audit.rs:532` tc_list 来源 `tool_calls_acc` + `stream_recv.rs` id 生成 + `agentic.rs` 是否对同轮 tool_calls 重复 process)。**修复方向**(待定):①前端 Started push 前加 `findToolCall(event.id)` 幂等守卫(确定性低风险,对齐 startToolSlowTimer 守卫,治标兜底) ②后端查重复 emit 源治本(定位 stream_recv/process/loop 哪环生重复)。**关联** F-260616-05(agentic loop 工具去重缺失,batch53 仅 High 去重,read_file/search_files 等 Low 未去重;本现象若实为候选 A 则与 F-05 不同维度——F-05 是不同 id 重复调用,本条是同 id 重复 emit)。— src/composables/ai/useAiEvents.ts(:195-209 Started 分支) + src/composables/ai/aiShared.ts(:41 findToolCall) + src/components/ToolCard.vue(:72 lines 展示) + src-tauri/src/commands/ai/audit.rs(:538-549 emit Started)。 +### 🔧 2026-06-17 useAiSend.ts 代码审查(仅走查·未实施) + +> 排查性质(session-role-diagnose-only),对照代码走查 `src/composables/ai/useAiSend.ts`(456 行)。波2 已实施 UX-05/06/07 + F-01 modelOverride,本次基于**当前代码**核验(不信 todo 声明:UX-05/06/07 经代码确认一致 ✅)。仅记录待办,未改代码。 + +- [ ] B-260617-01 [P2] — **sendQueuedNow「立即发送」可能退化为入队**。审查发现:sendQueuedNow(:406-411) `await stopChat()` 仅等 stop IPC 发出,不等后端 loop 退出 / `ai_is_generating` 复位。紧接 `await sendMessage(spliced)` 时,后端 stop_flag 已置但 generating 可能仍 true(loop 未跑到 agentic.rs:178 检测点 / guard.reset():188 未执行),sendMessage 命中 L1(:281 `backendGenerating || streaming`) → spliced 被**入队而非立即发**,与「立即」语义不符。UX-07 条目原标「stop_flag 复位降级可接受」,此审查**质疑该结论**——降级实为功能不达预期。**待核验**:`ai_is_generating` 复位时机(stop_flag→loop:178→guard.reset:188 链路延迟);若滞后,sendQueuedNow 应走 forceMode 跳过预检或等 AiCompleted 再发。— src/composables/ai/useAiSend.ts:406-411 +- [ ] B-260617-02 [P2] — **drainQueue 单条失败致队列卡死 + 错误吞没**。drainQueue(:247-251) `void sendMessage(...)` fire-and-forget;doSend IPC 失败 throw(:113) 被 void 忽略 → 无 AiCompleted 触发下次 drain → 剩余队列**永久卡住** + 用户无错误反馈。**修法**:drainQueue catch 失败,emit 错误提示 + 决定续发下一条或终止。— src/composables/ai/useAiSend.ts:247-251 +- [ ] B-260617-03 [P3] — **lang 解析三处重复(DRY)**。doSend(:94-97) / regenerate(:151-154) / editMessage(:224-227) 各一份相同 `df-ai-language`→auto 回落 `df-language` 解析。**修法**:提取 `resolveLang(): string` 辅助函数,三处复用。— src/composables/ai/useAiSend.ts +- [ ] B-260617-04 [P2] — **tryForceSend 失败后消息丢失无提示**。tryForceSend(:301-318) force_send 失败 catch 返 false(:316),但队首已 shift(:307) → 消息**丢失**,调用方是否提示用户未核验。**修法**:失败回填输入框或 toast(对齐 doSend 失败回填:108-113 模式)。— src/composables/ai/useAiSend.ts:301-318 +- [ ] B-260617-05 [P2/需核验] — **modelOverride 切换对话清逻辑缺失**。注释(:50-51)明确「切换对话前端应同步清 modelOverride.value=null」,但本文件无清逻辑(切换在 useAiConversations.ts)。若未清,切对话后 modelOverride 残留 → 新对话误用旧模型。**待核验** useAiConversations.ts switchConversation/newConversation 是否清;未清则补。— src/composables/ai/useAiSend.ts:53 + src/composables/ai/useAiConversations.ts + +> ⚪ 可选优化(未单列 todo):① cancelQueued(:372)/editQueued(:390)/sendQueuedNow(:407) 越界检查不一致——按「确定性>防御」风格 UI 调用方必合法,三处可统一(都删或都留)② doSend/regenerate/editMessage 的 streaming+watchdog+占位前置模式重复,可提取 `beginStream()`,但回滚差异大收益中等 ③ `_pendingApprovalIds`(:62) 无超时清理——approveToolCall 卡在 await 永不返回时 id 永驻 Set(边缘态,watchdog 不清此 Set)。 + ### 💡 2026-06-16 新需求(已分析·待实施) > 功能需求,已走查定位链路 + 方案记录,待用户确认后实施。 @@ -113,7 +125,7 @@ - [x] ✅(batch66·2026-06-16·22d866d,formatToolResult 12工具覆盖+i18n 18key) UX-260616-04 [P1] — **审批通过后工具卡片仅显示裸 JSON**。用户实测:审批前 `pending_approval` 态渲染友好(参数键值对+风险提示+审批按钮),审批通过后 `completed` 态 body 仅显示一段 JSON 数据。**根因**:`ToolCard.vue` 模板渲染链对 `read_file`/`list_directory`/`write_file` 有专门 UI 分支,其余工具全部走通用兜底(L110 `v-else-if tc.result && tc.status === 'completed'`)→ `formatToolResult(tc)` → 兜底 `return formatJson(r)` 裸 JSON 美化输出。**代码核对**:①`formatToolResult`(L270-289)只特化 `write_file`,其余全 `formatJson` ②`toolResultSummary`(L476-498,header 摘要)覆盖 `list_tasks`/`list_projects`/`list_ideas`/`create_project`/`create_task`/`create_idea`/`update_project`/`delete_project`/`restore_project`/`purge_project`/`run_workflow` 共 11 工具,但 body 的 `formatToolResult` 不共享此逻辑 ③两函数覆盖范围严重不一致(header 有摘要但 body 展开仍裸 JSON)。**后端返回值核对**(tool_registry.rs):`update_task`→`{id,field,updated}` / `delete_task`→`{deleted,id}` / `delete_file`→`{path,deleted,...}` / `run_command`→`{command,exit_code,stdout,stderr,...}`(可能很大) / `patch_file`→`{path,changed,size_diff,...}` / `append_file`→`{path,bytes_written,new_size}` / `rename_file`→`{from,to,renamed,...}` / `search_files`→`{path,pattern,results,total,...}` / `file_info`→`{path,exists,size,lines,...}` / `bind_directory`→`{id,path,stack,bound}` / `advance_task`→`{...}` ——全部走裸 JSON。**修复方向**:统一 `formatToolResult` 的 switch 覆盖全部工具产出人类可读摘要(复用 `toolResultSummary` 已有 i18n key + 补缺失 key 如 `commandOk`/`commandFailed`/`patchedFile`/`appendedTo`/`renamed`/`foundN` 等),同时补 `toolResultSummary` 缺失 case(`update_task`/`delete_task`/`delete_file`/`run_command`/`patch_file`/`append_file`/`rename_file`/`search_files`/`file_info`/`bind_directory`/`advance_task`)。—— `src/components/ToolCard.vue`(`formatToolResult` L270 + `toolResultSummary` L476) + `src/i18n/{zh-CN,en}/aiTool.ts`(补 key) - [x] ✅(波2·2026-06-16·w76d81dno+主代核查,vue-tsc,决策a逐条续发) UX-260616-05 [P1] — **打断按钮只停当前回复 + 队列保留续发(不清队列)**。用户需求:发送队列有消息时,打断按钮应只打断当前回复,并把队列中的多条消息一并发送给后续对话。现状:`stopChat`(useAiSend.ts:365-370) L368 `state.queue = []` **清空队列** → 用户生成中排队的消息随打断被**静默丢弃**。技术底座已就绪(已核后端):`stopChat`→`aiApi.stopChat`→后端 `stop_flag.load`(agentic.rs:178)→收尾 `save_conversation`(:185 已生成文本入库)+emit **`AiCompleted`**(agentic.rs:190,注释明确「保证前端收事件时后端已可接下一条,发送队列续发不被拒」)→前端 `ai-drain-queue`→`drainQueue`(useAiSend.ts:228) shift 队首续发。**最小修复(确定)**:删 stopChat L368 `state.queue=[]`,打断后 AiCompleted 自然触发 drainQueue 续发队首;当前未完成回复已入库(:185)保留为截断回复,无需额外处理。**决策点(待定)——「队列多条一并发送」语义**:**(a) 逐条续发**(删清队列即可,零改 drainQueue,AiCompleted 链式触发每条,保各条上下文独立)vs **(b) 合并成一条消息**(队列多条 text 拼接送,需改 drainQueue 为 bulkDrain 或合并 text 调一次 sendMessage)。**倾向 (a)**(逐条保上下文独立+零改链路;"一并"理解为「都送出不丢弃」非字面合并),**待用户确认**。— src/composables/ai/useAiSend.ts(stopChat:365 + drainQueue:228) + 后端 agentic.rs(:178/:190 stop 收尾 emit AiCompleted 已就绪)。 - [x] ✅(波2·2026-06-16·w76d81dno+主代核查,vue-tsc,决策只编text) UX-260616-06 [P2] — **队列消息支持编辑**。用户需求:队列中的消息支持编辑。现状:队列 UI(AiChat.vue:511-516) 每项只有 × 删除(cancelQueued:353),无编辑入口;队列项结构 `{ text, skill?, enqueuedAt }`(stores/ai.ts:70)。**改动(纯前端低风险)**:队列项加「编辑」按钮 → 点击切 inline input(单行 textarea)→ 回车/失焦写回 `state.queue[idx].text`、ESC 取消;新增 `editQueued(index, newText)`(useAiSend.ts) splice 写回 + store 导出 + composable return。**决策点(待定)**:skill 是否可编辑(**倾向只编 text**——skill 来自发送时技能联想,编辑态不暴露 skill 改,简化交互)。i18n 补 edit/save/cancel key。— src/components/AiChat.vue(:511-516 队列项 UI + .ai-queue-item CSS:2856) + src/composables/ai/useAiSend.ts(editQueued 新增) + stores/ai.ts + i18n。 -- [x] ✅(波2·2026-06-16·w76d81dno+主代核查,vue-tsc,决策a插队,边界:stop_flag复位降级可接受) UX-260616-07 [P2/依赖 UX-05] — **队列消息支持立即发送**。用户需求:队列中的消息支持立即发送。现状:队列项只能 × 删除或等 drainQueue 串行轮到,无插队。**决策点(待定)——「立即发送」语义**:**(a) 插队=打断当前+立即发这条**(splice 出该项→stopChat→sendMessage 该条;复用 UX-05 stop→AiCompleted→drain 链路,当前未完成回复走 stop 截断保留)vs **(b) 移到队首等当前完成优先**(splice 出→unshift 队首,drainQueue 下一轮先发它,不打断当前)。**倾向 (a)**("立即"字面=马上发,且复用 UX-05 链路;但须注意 splice 后调 stopChat 已不清队列(UX-05),避免已发送项重复入队)。**待 UX-05 打断语义定后同批设计**(共享 stop+drain 链路)。— src/components/AiChat.vue(:511-516 队列项加「立即发送」按钮) + src/composables/ai/useAiSend.ts(sendQueuedNow 新增)。 +- [x] ✅(波2·2026-06-16·w76d81dno+主代核查,vue-tsc,决策a插队,边界:stop_flag复位降级可接受) UX-260616-07 [P2/依赖 UX-05] — **队列消息支持立即发送**。用户需求:队列中的消息支持立即发送。现状:队列项只能 × 删除或等 drainQueue 串行轮到,无插队。**决策点(待定)——「立即发送」语义**:**(a) 插队=打断当前+立即发这条**(splice 出该项→stopChat→sendMessage 该条;复用 UX-05 stop→AiCompleted→drain 链路,当前未完成回复走 stop 截断保留)vs **(b) 移到队首等当前完成优先**(splice 出→unshift 队首,drainQueue 下一轮先发它,不打断当前)。**倾向 (a)**("立即"字面=马上发,且复用 UX-05 链路;但须注意 splice 后调 stopChat 已不清队列(UX-05),避免已发送项重复入队)。**待 UX-05 打断语义定后同批设计**(共享 stop+drain 链路)。— src/components/AiChat.vue(:511-516 队列项加「立即发送」按钮) + src/composables/ai/useAiSend.ts(sendQueuedNow 新增)。**⚠ 2026-06-17 审查质疑**:sendQueuedNow 存在时序隐患(`await stopChat()` 后 `ai_is_generating` 未及时复位,紧接的 sendMessage 可能命中 L1 入队而非立即发),原结论「stop_flag 复位降级可接受」存疑,详见 B-260617-01(待核验后端复位时机)。 - [ ] F-260616-15 [P1] — **AI Chat 上下文管理增强:会话分段 + 手动压缩 + 智能裁剪**。三个子需求,详见 [F-15-上下文管理增强设计-2026-06-16.md](./02-架构设计/F-15-上下文管理增强设计-2026-06-16.md)。**阶段1 基础部分 ✅(波3·8a142c2 is_active 白名单+push token 修正)**;阶段1 剩余 ✅(波5·63bff8b compress_prompt四段式+compress_via_llm+context 9辅助方法,幂等/单向不可逆/纯函数/零行为变化,df-ai 107pass);阶段2 手动功能 ✅(波6·4194842+主代核查,cargo0err+vue-tsc0+devflow79pass,2 IPC ai_chat_clear_context/compress_context+3事件 AiContextCleared/AiCompressing/AiCompressed+前端2按钮+status分组渲染+useAiContext composable,clear标archived_segment不删DB/compress LLM失败不阻塞);阶段3 自动 ✅(波8·0202b51+主代核查,cargo0err+vue-tsc0+devflow79pass,loop顶部检测budget×0.6+压缩+降级原裁剪,set_compressing成对,延迟mutate失败零副作用)。**F-15 全阶段完成 ✅**。 **核心问题**:① `ai_chat_clear` 全删 DB 消息不可回溯 ② `build_for_request` 超预算直接丢弃旧消息零保留(`agentic.rs:226` `_trimmed` 被忽略) ③ 无压缩能力。 @@ -637,7 +649,7 @@ ### 待澄清 / A-B 待定 -- [ ] S-260614-01 — 「显示多开」需求待澄清 — 用户报"设置勾选显示多开但 AiChat 未显示",全 src grep 零命中,疑似旧版本/指分离窗口/想新增开关,待用户截图确认 (06-14) +- [ ] S-260614-01 — **[用户已澄清 2026-06-17]** 「显示多开」= 多个会话来回切都可对话(非 AI Chat 窗口多开)。关联 T-260614-02(切对话不中断实测)+ F-09 多会话架构。**A 路线(单例软隔离,已做)**:切会话软复位+DB 加载上下文+active 切换,来回切应能对话(需实测 T-260614-02)。**B 路线(真多会话,未做)**:切走会话 generating 不打断,每会话独立 AiSession(memory aichat-arch-extensibility 单例未动)。**待决策 A/B 路线 → 见待决策.md** - [ ] S-260614-02 — 审批可见性 A/B 待定 — B-01 修复后 pending_approvals 内存态不再被 switch 清空,前端 `ai_pending_tool_calls` 查询有数据,但 `state.pendingApprovals` 在 AiChat.vue 是否有兜底渲染仍需实测确认。A. 加兜底渲染 / B. 实测 tc 卡片是否渲染 — **去重**:与 AR-3 审批卡片可读化重叠,AR-3 修完(reason 拼对象名 + id 特化 + 卡片完整)后改实测重评 (06-14) ### P2 — 不阻断缺陷 / 增强 diff --git a/docs/待决策.md b/docs/待决策.md index ee52e3f..56fdd15 100644 --- a/docs/待决策.md +++ b/docs/待决策.md @@ -41,6 +41,16 @@ - **关联**:todo B-260617-01 / tool_registry.rs:530-546 / commands.rs:329-334 / audit.rs:590-630 - **状态**:✅ 已决(2026-06-17·波9 e23b316)— **决策:a 细化为 a2(后端分支)**。ai_approve 已持 app+state,后端直接调 run_workflow_inner 最干净(无需新 IPC/前端分支/工作流知识泄漏),符合 a 大方向(绕开 ai_tools.execute 必 Err)。问题 2 随 a2 消解(成功 tool_result + High 去重缓存双层断循环);问题 3 渲染区分记观察项(tool_display_hint 已含「触发工作流」语义)。详单见待审查 CR-52。 +### 🟡 S-260614-01 多会话来回切对话(A/B 路线决策) +- **背景**:用户澄清(2026-06-17)「显示多开」原意 = 多个会话来回切都可对话(非 AI Chat 窗口多开)。关联 T-260614-02(切对话不中断实测)+ F-09 多会话架构。 +- **决策点**:A 路线(单例软隔离)实测够 vs 需 B 路线(真多会话并行)架构升级? +- **选项**: + - a: **A 路线实测确认**(T-260614-02 跑 tauri dev 验证切会话不中断、来回切可对话)。够 → 销账;不够 → 升 b。 + - b: **B 路线架构升级**(AiSession 单例 → 多实例,每会话独立 generating/AiSession,切走不打断)。memory aichat-arch-extensibility 记录单例未动。大改(触 ai/mod.rs AiSession + state.rs AppState + agentic.rs loop)。 +- **推荐**:**a 先实测**(T-260614-02 验证 A 路线是否满足"来回切都可对话")。A 路线设计目标即切会话不丢上下文 + active 切换,理论支持。若实测发现切走 generating 被打断(单例限制)→ 升 b。避免盲目大改。 +- **关联**:todo S-260614-01 / T-260614-02 / F-09 / memory aichat-arch-extensibility +- **状态**:🟡 待用户实测(T-260614-02)确认 A 路线是否够 + ### ⏸️ 暂缓(待条件触发) #### T-260614-11 条件表达式引擎升级