重构: AI聊天可靠性批次(审批累计计数/write_file diff预览/会话隔离补清/token缓存)
This commit is contained in:
32
docs/todo.md
32
docs/todo.md
@@ -76,15 +76,15 @@
|
|||||||
|
|
||||||
- [x] ✅(batch56·2026-06-16·workflow wsxxurgbr+主代核查) F-260616-08 [P2] — **AI 数据获取工具 offset 分页 + 探总量**。**全闭环落地**:①6 工具(list_projects/tasks/ideas/trash/read_file搜索模式/search_files)全部补 offset+limit 参数,返回 items/total/has_more 替代 truncated ②新增 `get_project_count`+`get_task_count` 探总量工具 ③knowledge_search 解钳 `min(3)`→`min(20)`。— src-tauri/src/commands/ai/tool_registry.rs + src-tauri/src/commands/knowledge.rs
|
- [x] ✅(batch56·2026-06-16·workflow wsxxurgbr+主代核查) F-260616-08 [P2] — **AI 数据获取工具 offset 分页 + 探总量**。**全闭环落地**:①6 工具(list_projects/tasks/ideas/trash/read_file搜索模式/search_files)全部补 offset+limit 参数,返回 items/total/has_more 替代 truncated ②新增 `get_project_count`+`get_task_count` 探总量工具 ③knowledge_search 解钳 `min(3)`→`min(20)`。— src-tauri/src/commands/ai/tool_registry.rs + src-tauri/src/commands/knowledge.rs
|
||||||
|
|
||||||
- [ ] F-260616-09 [P1/架构] — **AiSession 单例 → 多会话并发架构(多窗口来回切 + 并行执行)**。用户需求(3 条消息强化):「应用级支持同时具备多会话」+「同时具备多会话的能力」+「多个会话窗口来回切,**并行执行**」。现状(Explore 走查确认):`AiSession` **应用级单例**(`state.rs:164` `ai_session: Arc<Mutex<AiSession>>`,init:205),messages/generating/stop_flag/pending_approvals/agent_language 全一份。设计「单例 + 软隔离」——generating 互斥(同一时刻**只一个对话能跑**) + 切换旧 loop 退出(`agentic.rs:177-190` B-260615-11) + 切换时 generating→readonly(`commands.rs:873-881`)。`llm_concurrency` per_conv 因此退化为单对话内并发(`state.rs:93-96` 注释已标注预留改 HashMap)。**memory 已标记「AiSession 单例未动」架构债**(aichat-arch-extensibility)。**残留 bug**(表象=新建会话上下文残留):前端 `newConversation`(`useAiConversations.ts:31-41`) 漏清 queue/generatingConvId/agentRound/searchQuery;后端 `ai_conversation_create`(`commands.rs:784-821`) 漏清 agent_language/stop_flag(设 true 停旧 loop 没复位 false)。**用户「并行执行」明确=B 方向**(A 只解切换残留不满足并行)。**分档**:**A 短期隔离修复**(补漏清字段,维持单例软隔离,解残留,低风险立即做,**B 前置**) + **B 多会话并发架构**(单例 → `HashMap<conv_id, SessionState>`,每会话独立 messages/generating/stop_flag/pending_approvals + llm_concurrency per_conv 改真 per-conv HashMap + loop 内校验改 conv_id 索引各自 session + 切换不退出旧 loop 各自跑完真并发 + 事件路由核对 conversation_id 全覆盖)。**决策点(待定)**:(a) A 先做解燃眉 vs 直接 B——倾向 **A 先**(残留真 bug 立即解,B 架构另立项) (b) B 形态——AiSession 整体 HashMap vs 拆字段(messages/pending 已可按 conv reload,只拆 generating/stop_flag per-conv)——倾向**后者侵入小** (c) B 并发上限——复用 llm_concurrency.global 限并发会话数防 token 暴增——倾向是 (d) **多窗口 UI 形态**——(d1) 单面板侧栏会话列表切换+后台并行(改动小) vs (d2) 每会话独立 Tauri 窗口(`useAiWindow.ts` detached 已有基础可扩,改动大但「窗口来回切」语义最贴)——**待用户定**,倾向先 d1 后 d2 (e) 旧 loop 切换处理——不退出各自跑完(真并发,倾向) vs 仍退出。— state.rs(AiSession 去单例化) + commands.rs(create/switch 重构 + 漏清补) + agentic.rs(:177-190 校验改 conv_id 索引) + context.rs(ContextManager per-conv) + useAiConversations.ts(漏清补) + stores/ai.ts(per-conv state) + llm_concurrency per_conv + useAiWindow.ts(若 d2 多窗口)。**B 是大改架构升级,需单独设计文档 + 立项**。**待用户定 A 先做 / B 立项 / 多窗口 d1·d2 方向**。
|
- [ ] F-260616-09 [P1/架构·**A路线✅(batch60·2026-06-16·workflow whae812z5+主代核查),B多会话并发待立项**] — **AiSession 单例 → 多会话并发架构(多窗口来回切 + 并行执行)**。用户需求(3 条消息强化):「应用级支持同时具备多会话」+「同时具备多会话的能力」+「多个会话窗口来回切,**并行执行**」。现状(Explore 走查确认):`AiSession` **应用级单例**(`state.rs:164` `ai_session: Arc<Mutex<AiSession>>`,init:205),messages/generating/stop_flag/pending_approvals/agent_language 全一份。设计「单例 + 软隔离」——generating 互斥(同一时刻**只一个对话能跑**) + 切换旧 loop 退出(`agentic.rs:177-190` B-260615-11) + 切换时 generating→readonly(`commands.rs:873-881`)。`llm_concurrency` per_conv 因此退化为单对话内并发(`state.rs:93-96` 注释已标注预留改 HashMap)。**memory 已标记「AiSession 单例未动」架构债**(aichat-arch-extensibility)。**残留 bug**(表象=新建会话上下文残留):前端 `newConversation`(`useAiConversations.ts:31-41`) 漏清 queue/generatingConvId/agentRound/searchQuery;后端 `ai_conversation_create`(`commands.rs:784-821`) 漏清 agent_language/stop_flag(设 true 停旧 loop 没复位 false)。**用户「并行执行」明确=B 方向**(A 只解切换残留不满足并行)。**分档**:**A 短期隔离修复**(补漏清字段,维持单例软隔离,解残留,低风险立即做,**B 前置**) + **B 多会话并发架构**(单例 → `HashMap<conv_id, SessionState>`,每会话独立 messages/generating/stop_flag/pending_approvals + llm_concurrency per_conv 改真 per-conv HashMap + loop 内校验改 conv_id 索引各自 session + 切换不退出旧 loop 各自跑完真并发 + 事件路由核对 conversation_id 全覆盖)。**决策点(待定)**:(a) A 先做解燃眉 vs 直接 B——倾向 **A 先**(残留真 bug 立即解,B 架构另立项) (b) B 形态——AiSession 整体 HashMap vs 拆字段(messages/pending 已可按 conv reload,只拆 generating/stop_flag per-conv)——倾向**后者侵入小** (c) B 并发上限——复用 llm_concurrency.global 限并发会话数防 token 暴增——倾向是 (d) **多窗口 UI 形态**——(d1) 单面板侧栏会话列表切换+后台并行(改动小) vs (d2) 每会话独立 Tauri 窗口(`useAiWindow.ts` detached 已有基础可扩,改动大但「窗口来回切」语义最贴)——**待用户定**,倾向先 d1 后 d2 (e) 旧 loop 切换处理——不退出各自跑完(真并发,倾向) vs 仍退出。— state.rs(AiSession 去单例化) + commands.rs(create/switch 重构 + 漏清补) + agentic.rs(:177-190 校验改 conv_id 索引) + context.rs(ContextManager per-conv) + useAiConversations.ts(漏清补) + stores/ai.ts(per-conv state) + llm_concurrency per_conv + useAiWindow.ts(若 d2 多窗口)。**B 是大改架构升级,需单独设计文档 + 立项**。**待用户定 A 先做 / B 立项 / 多窗口 d1·d2 方向**。
|
||||||
|
|
||||||
- [x] ✅(batch58·2026-06-16·workflow w2dw65maz+主代核查,vue-tsc 0err) F-260616-10 [P1·bug] — **provider 切换/默认值重启·刷新后失效(前端 activeProvider 不同步 DB 默认)**。用户实测:Settings 设 A 为默认 → 重启 → 默认变 B(列表第一个)。**根因全在前端,后端链路正确**(独立核验不信 PROGRESS.md:「已修复」声明不可信):`ai_set_provider`(`commands.rs:747-774`) 互斥落库✅ + `update_full`(`crud.rs:1127`) SQL UPDATE 含 `is_default`✅ + `get_active_provider`(`prompt.rs:9-32`) 走 `DB.is_default`✅。**根因链**:①重启后前端 `store.activeProvider = null`(`stores/ai.ts:53`,无 localStorage 持久化 + 无启动恢复) ②`loadProviders`(`useAiPanel.ts:91-93`) 只拉列表**不同步 DB 默认** → activeProvider 仍 null ③**UI 错觉**:`activeProviderName`(`AiChat.vue:1169-1172`) null 时 fallback `providers[0]`(列表第一个) ≠ 用户设的 A → 「默认变另一个」 ④**真覆盖**:用户点 provider bar 切换 `cycleProvider`(`AiChat.vue:1180-1185`),`findIndex(activeProvider=null)` 返回 **-1** → `next = ps[(−1+1)%len] = ps[0]` = 列表第一个 B → `setProvider(B)` → 后端互斥落库把 `DB.is_default` 从 A **真改成 B**。即使用户不点切换,UI 也显示 providers[0] 而非 A。**修复方向**:①`loadProviders` 后同步 `activeProvider` = DB.is_default 的 provider——后端加 `ai_get_active_provider` IPC 返回当前默认 id,或 `listProviders` 返回值带 `active_provider_id` ②`cycleProvider` findIndex=-1 陷阱修正(null 时应从 DB 默认起算,非 ps[0]) ③前端 activeProvider 持久化(localStorage `df-ai-active-provider`,对齐 `df-ai-active-conv` 模式)。— useAiPanel.ts(loadProviders 同步默认) + AiChat.vue(:1169 activeProviderName fallback + :1180 cycleProvider findIndex 陷阱) + commands.rs(新 ai_get_active_provider IPC) + api/ai.ts + stores/ai.ts(activeProvider 持久化)。**确定性 bug,低风险,可立即修**。
|
- [x] ✅(batch58·2026-06-16·workflow w2dw65maz+主代核查,vue-tsc 0err) F-260616-10 [P1·bug] — **provider 切换/默认值重启·刷新后失效(前端 activeProvider 不同步 DB 默认)**。用户实测:Settings 设 A 为默认 → 重启 → 默认变 B(列表第一个)。**根因全在前端,后端链路正确**(独立核验不信 PROGRESS.md:「已修复」声明不可信):`ai_set_provider`(`commands.rs:747-774`) 互斥落库✅ + `update_full`(`crud.rs:1127`) SQL UPDATE 含 `is_default`✅ + `get_active_provider`(`prompt.rs:9-32`) 走 `DB.is_default`✅。**根因链**:①重启后前端 `store.activeProvider = null`(`stores/ai.ts:53`,无 localStorage 持久化 + 无启动恢复) ②`loadProviders`(`useAiPanel.ts:91-93`) 只拉列表**不同步 DB 默认** → activeProvider 仍 null ③**UI 错觉**:`activeProviderName`(`AiChat.vue:1169-1172`) null 时 fallback `providers[0]`(列表第一个) ≠ 用户设的 A → 「默认变另一个」 ④**真覆盖**:用户点 provider bar 切换 `cycleProvider`(`AiChat.vue:1180-1185`),`findIndex(activeProvider=null)` 返回 **-1** → `next = ps[(−1+1)%len] = ps[0]` = 列表第一个 B → `setProvider(B)` → 后端互斥落库把 `DB.is_default` 从 A **真改成 B**。即使用户不点切换,UI 也显示 providers[0] 而非 A。**修复方向**:①`loadProviders` 后同步 `activeProvider` = DB.is_default 的 provider——后端加 `ai_get_active_provider` IPC 返回当前默认 id,或 `listProviders` 返回值带 `active_provider_id` ②`cycleProvider` findIndex=-1 陷阱修正(null 时应从 DB 默认起算,非 ps[0]) ③前端 activeProvider 持久化(localStorage `df-ai-active-provider`,对齐 `df-ai-active-conv` 模式)。— useAiPanel.ts(loadProviders 同步默认) + AiChat.vue(:1169 activeProviderName fallback + :1180 cycleProvider findIndex 陷阱) + commands.rs(新 ai_get_active_provider IPC) + api/ai.ts + stores/ai.ts(activeProvider 持久化)。**确定性 bug,低风险,可立即修**。
|
||||||
|
|
||||||
- [ ] F-260616-11 [P1/设计风险] — **审批后 iteration 重置致 max_iterations/token 超配**。性能分析批次发现:`try_continue_agent_loop` 重新 spawn `run_agentic_loop`,**iteration 从 0 重计**(`agentic.rs:418` 注释,F-260616-03 续跑决策 a 的延续,审批续跑复用此入口)。每次审批(Med/High 工具)后配额重置,多次审批=反复重跑 max_iterations 轮,实际可跑远超配额,**token 消耗不受 max_iterations 严格控制**。影响:长对话多次审批致 token 失控 + 对话变慢(每审批一次重跑 LLM 轮次)。**方向(待评估)**:①审批不重置 iteration(累计计数)②审批是用户决策点重置合理但需独立「审批次数」配额 ③至少 token 总量兜底。— agentic.rs(try_continue iteration 计数策略)。**需决策**:iteration 累计 vs 重置。
|
- [x] ✅(batch60合批·2026-06-16·主代核查,cargo 0err) F-260616-11 [P1·决策a累计计数] — **审批后 iteration 重置致 max_iterations/token 超配**。决策(2026-06-16):**a 累计计数**(try_continue 不重置 iteration,跨审批续跑延续累计)。**已实施**:`iteration_used:usize` 累计字段(`mod.rs:215/230` AiSession+new init 0)+`start_iteration:usize` 参数(`agentic.rs:114` run_agentic_loop signature +`:176` loop 边界 `start_iteration..max_iterations` +`:210` 一致性块 `iteration_used=iteration+1` +`:578` try_continue 加参 +`:675` spawn 透传)+三处 reset(`commands.rs:55/152/412` ai_chat_send/ai_regenerate/ai_edit_last 新生命周期)+ai_approve 两处累计读 iteration_used(`:260-262` 拒绝续跑 +`:313-315` 通过续跑)+`ai_continue_loop:601` reset+传0(F-03 决策a 达max重计区分)。两路径严格:审批累计(ai_approve) vs 达max重计(ai_continue_loop,F-03 a 用户授权重来) vs 新消息 reset。边界case:start≥max loop 空区间→converged=false→AiMaxRoundsReached(防无限审批烧 token,正 F-11 语义)。原:`try_continue_agent_loop` 重 spawn `run_agentic_loop` iteration 从 0 重计致多次审批反复重跑 max_iterations 轮 token 失控。— agentic.rs + commands.rs + mod.rs。
|
||||||
|
|
||||||
- [ ] F-260616-12 [P2/依赖F-09] — **retry 持 permit 不释放(多会话隐患)**。F-260616-07 落地的流式重试循环(`agentic.rs:238-239`)重试期间持有 global+per_conv permit 不释放(注释「防新请求挤占」)。当前 AiSession 单例 + 主 loop 串行无影响,但 **F-260616-09 多会话并发后**,重试期间阻塞其他对话 LLM 调用。**方向**:多会话落地时核对——主 loop 串行下重试持 permit 防自己挤占无意义,倾向重试不持 permit 或仅持 per_conv。— agentic.rs:238 + F-260616-09 多会话架构。**依赖 F-260616-09 立项后一并处理**。
|
- [ ] F-260616-12 [P2/依赖F-09] — **retry 持 permit 不释放(多会话隐患)**。F-260616-07 落地的流式重试循环(`agentic.rs:238-239`)重试期间持有 global+per_conv permit 不释放(注释「防新请求挤占」)。当前 AiSession 单例 + 主 loop 串行无影响,但 **F-260616-09 多会话并发后**,重试期间阻塞其他对话 LLM 调用。**方向**:多会话落地时核对——主 loop 串行下重试持 permit 防自己挤占无意义,倾向重试不持 permit 或仅持 per_conv。— agentic.rs:238 + F-260616-09 多会话架构。**依赖 F-260616-09 立项后一并处理**。
|
||||||
|
|
||||||
- [ ] F-260616-13 [P2/小优化] — **build_for_request 持锁重活 + system_prompt token 每轮重估**。性能分析批次发现:每轮 `stream_llm` 前 `session_arc.lock()`(`agentic.rs:251`)持锁期间做 `TokenEstimator::estimate_text(system_prompt)`(L252)+ `build_for_request`(L253 history clone + 裁剪)。system_prompt loop 外固定传入,**每轮重估其 token 是浪费**(可缓存);build_for_request 持锁做 history clone 是重活,消息多时(200 cap)锁持有期长。**方向**:①system_prompt token loop 外算一次缓存 ②build_for_request 先 clone messages 释放锁再裁剪。低收益优化。— agentic.rs:251-253。
|
- [x] ✅(batch60·2026-06-16·workflow whae812z5+主代核查,cargo 0err) F-260616-13 [P2] — **build_for_request 持锁重活 + system_prompt token 每轮重估**。性能分析批次发现:每轮 `stream_llm` 前 `session_arc.lock()`(`agentic.rs:251`)持锁期间做 `TokenEstimator::estimate_text(system_prompt)`(L252)+ `build_for_request`(L253 history clone + 裁剪)。system_prompt loop 外固定传入,**每轮重估其 token 是浪费**(可缓存);build_for_request 持锁做 history clone 是重活,消息多时(200 cap)锁持有期长。**方向**:①system_prompt token loop 外算一次缓存 ②build_for_request 先 clone messages 释放锁再裁剪。低收益优化。— agentic.rs:251-253。
|
||||||
|
|
||||||
- [x] ✅(主代核验·2026-06-16) F-260616-14 [P2/核验] — **max_tokens 8192 截断 + 前端批量审批核验**。核验结论:①`max_tokens=8192`(实际 `agentic.rs:270`,行号漂移)≈6-8k 字日常够用,长回复截断由 batch59 MidStream 保文兜底不丢文,**可配低优先非阻塞**(登记可选优化)②前端批量审批 **AE-2025-01 已完整闭环**(ToolCardList.vue:4-11 `ai-batch-approve` 栏 + AiChat.vue:444 `@batch-approve`→`store.batchApprove` + useAiSend.ts:338 遍历 pendingApprovals 逐个 approveToolCall + i18n approveAll/rejectAll),满足「一轮多 Med/High 全 insert pending→一次批量批全部」③audit.rs:496 `process_tool_calls` 一轮多工具 Med/High 进审批门控 insert pending + Low 并行执行链路通。无阻塞 bug,销账。— agentic.rs:270 + ToolCardList.vue + audit.rs:496。
|
- [x] ✅(主代核验·2026-06-16) F-260616-14 [P2/核验] — **max_tokens 8192 截断 + 前端批量审批核验**。核验结论:①`max_tokens=8192`(实际 `agentic.rs:270`,行号漂移)≈6-8k 字日常够用,长回复截断由 batch59 MidStream 保文兜底不丢文,**可配低优先非阻塞**(登记可选优化)②前端批量审批 **AE-2025-01 已完整闭环**(ToolCardList.vue:4-11 `ai-batch-approve` 栏 + AiChat.vue:444 `@batch-approve`→`store.batchApprove` + useAiSend.ts:338 遍历 pendingApprovals 逐个 approveToolCall + i18n approveAll/rejectAll),满足「一轮多 Med/High 全 insert pending→一次批量批全部」③audit.rs:496 `process_tool_calls` 一轮多工具 Med/High 进审批门控 insert pending + Low 并行执行链路通。无阻塞 bug,销账。— agentic.rs:270 + ToolCardList.vue + audit.rs:496。
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
- [x] ✅(batch35·2026-06-16·workflow) **①-3** i18n 文案 — `src/i18n/{zh-CN,en}/taskDetail.ts`(taskDetail.* 命名空间,8 key 对称:workflowAdvanceTitle/Advancing/AdvanceFailed{msg}/StepRunning{node}/StepsProgress{done}{total}/CompletedHint/FailedHint)。注:todo 原路径 `locales/tasks.ts` 不准,实为 `i18n/taskDetail.ts`
|
- [x] ✅(batch35·2026-06-16·workflow) **①-3** i18n 文案 — `src/i18n/{zh-CN,en}/taskDetail.ts`(taskDetail.* 命名空间,8 key 对称:workflowAdvanceTitle/Advancing/AdvanceFailed{msg}/StepRunning{node}/StepsProgress{done}{total}/CompletedHint/FailedHint)。注:todo 原路径 `locales/tasks.ts` 不准,实为 `i18n/taskDetail.ts`
|
||||||
- **实施顺序**:④-1→②-1→②-5→②-2→②-3→②-4→②-6→①-1/①-3。**最小里程碑**(④-1+②-1+②-2+②-3):run_workflow 单 task_advance 节点 DAG 端到端推进 todo→in_progress
|
- **实施顺序**:④-1→②-1→②-5→②-2→②-3→②-4→②-6→①-1/①-3。**最小里程碑**(④-1+②-1+②-2+②-3):run_workflow 单 task_advance 节点 DAG 端到端推进 todo→in_progress
|
||||||
- [ ] F-260616-07 阶段3 AI 执行闭环(advance_task/run_workflow 工具+AiNode+自审)+ **F-03收口三件**(advance_task注册AI工具+update_task描述/schema移status+df-storage tasks白名单移status,同步落地防AI工具行为不一致)
|
- [ ] F-260616-07 阶段3 AI 执行闭环(advance_task/run_workflow 工具+AiNode+自审)+ **F-03收口三件**(advance_task注册AI工具+update_task描述/schema移status+df-storage tasks白名单移status,同步落地防AI工具行为不一致)
|
||||||
- [ ] F-260616-08 阶段4 Git 集成(kind+git闸门+worktree)
|
- [x] ⏸️(待决策.md已决b暂缓·2026-06-16) F-260616-08 阶段4 Git 集成(kind+git闸门+worktree)
|
||||||
- [x] ✅ **CR-260616-01 代码审查完成** → 审查登记已迁 [待审查.md](./待审查.md)(职责分离:审查队列独立,不进 todo)。结论 🔴0 🟡6 ⚪4 质量优,8 维度全过。**待修项 CR-01-A~I 见下方推进区**。
|
- [x] ✅ **CR-260616-01 代码审查完成** → 审查登记已迁 [待审查.md](./待审查.md)(职责分离:审查队列独立,不进 todo)。结论 🔴0 🟡6 ⚪4 质量优,8 维度全过。**待修项 CR-01-A~I 见下方推进区**。
|
||||||
|
|
||||||
> **审查发现待修项(2026-06-16 · CR-260616-01)**:
|
> **审查发现待修项(2026-06-16 · CR-260616-01)**:
|
||||||
@@ -150,9 +150,9 @@
|
|||||||
- [x] ✅(batch50·2026-06-16·workflow w6nqio4o5) **CR-15-1** [med] `crates/df-execute/src/shell.rs:60-79` 缺 `.kill_on_drop(true)` → 已修复(cmd.kill_on_drop(true) 三类型覆盖 + 注释配合 tokio::time::timeout 防孤儿,文案「进程已终止」名副其实)
|
- [x] ✅(batch50·2026-06-16·workflow w6nqio4o5) **CR-15-1** [med] `crates/df-execute/src/shell.rs:60-79` 缺 `.kill_on_drop(true)` → 已修复(cmd.kill_on_drop(true) 三类型覆盖 + 注释配合 tokio::time::timeout 防孤儿,文案「进程已终止」名副其实)
|
||||||
- [x] ✅(主代理验证:不成立·workspace cargo check exit 0) 🔴 **CR-23-1** [high·紧急·阻断构建] `crates/df-ai/src/context.rs:491` E0594 → **不成立**:审查代理跑 batch51 中间态 filter 版(EXIT 101),batch51 最终改 for 循环(EXIT 0,df-ai + workspace 双验证,代理自报属实)。context.rs:488 当前是 `for t in iter_mut()` 单层引用可写 status,无 filter 闭包。审查锚定滞后(跑 batch51 中间态)— CR-260616-23 归档
|
- [x] ✅(主代理验证:不成立·workspace cargo check exit 0) 🔴 **CR-23-1** [high·紧急·阻断构建] `crates/df-ai/src/context.rs:491` E0594 → **不成立**:审查代理跑 batch51 中间态 filter 版(EXIT 101),batch51 最终改 for 循环(EXIT 0,df-ai + workspace 双验证,代理自报属实)。context.rs:488 当前是 `for t in iter_mut()` 单层引用可写 status,无 filter 闭包。审查锚定滞后(跑 batch51 中间态)— CR-260616-23 归档
|
||||||
- [x] ✅(主代理修·CSS :has item 左边线) **CR-22-1** [med·UX] `AiChat.vue` 已置顶非 hover 不可见 → 已修复(`.ai-conv-item:has(.ai-conv-item-act--pinned){ box-shadow: inset 2px 0 0 accent }` 左边线强调,不破坏 hover 浮出 toggle,原方案图钉常驻与 actions display:none 矛盾,改 :has item 左线最简)— CR-260616-22 归档
|
- [x] ✅(主代理修·CSS :has item 左边线) **CR-22-1** [med·UX] `AiChat.vue` 已置顶非 hover 不可见 → 已修复(`.ai-conv-item:has(.ai-conv-item-act--pinned){ box-shadow: inset 2px 0 0 accent }` 左边线强调,不破坏 hover 浮出 toggle,原方案图钉常驻与 actions display:none 矛盾,改 :has item 左线最简)— CR-260616-22 归档
|
||||||
- [ ] **CR-34-1** [med] useAiPanel.ts:101 loadProviders 守卫 `if(!state.activeProvider)` 只查 null 不查有效性 — localStorage savedProvider 指向已删除 provider 时 activeProvider 残留无效 id → cycleProvider:1185 findIndex=-1 跳过 ps[0] + 发消息消费方用无效 id。修法:守卫加 `|| !list.some(p=>p.id===state.activeProvider)` 残留无效时回落 DB is_default — CR-260616-34 归档
|
- [x] ✅(CR-260616-35 复审 PASS·主线程预核验) **CR-34-1** [med] useAiPanel.ts:102 loadProviders 守卫 `if(!state.activeProvider)` 只查 null 不查有效性 — localStorage savedProvider 指向已删除 provider 时 activeProvider 残留无效 id → cycleProvider:1185 findIndex=-1 跳过 ps[0] + 发消息消费方用无效 id。修法:守卫加 `|| !list.some(p=>p.id===state.activeProvider)` 残留无效时回落 DB is_default — CR-260616-34 归档
|
||||||
- [ ] **CR-30-1** [med] agentic.rs:298 退避 `1<<retry_attempt` 纯指数无 jitter/无总预算/一律重试(含 MidStream),三处注释(agentic.rs:37/commands.rs:1106/state.rs:185)误称「对齐 retry.rs(1s→2s→4s+jitter)」。retry.rs:67-78 实有 ±20% jitter+30s 预算+4xx 分类(:102-104 Fatal 立即放弃)。修法:agentic 加 jitter+总预算+错误分类真对齐,或删注释改「独立简化策略」— CR-260616-30 归档
|
- [x] ✅(CR-260616-35 复审 PASS·agent cargo 6+20 passed) **CR-30-1** [med] agentic.rs:298 退避 `1<<retry_attempt` 纯指数无 jitter/无总预算/一律重试(含 MidStream),三处注释(agentic.rs:37/commands.rs:1106/state.rs:185)误称「对齐 retry.rs(1s→2s→4s+jitter)」。retry.rs:67-78 实有 ±20% jitter+30s 预算+4xx 分类(:102-104 Fatal 立即放弃)。修法:agentic 加 jitter+总预算+错误分类真对齐,或删注释改「独立简化策略」— CR-260616-30 归档
|
||||||
- [ ] **CR-30-2** [med] AiStreamRetry 死事件 + MidStream 重试文本混乱 — 后端 emit(agentic.rs:310)+types.ts:214 variant 有,但 useAiEvents.ts switch 无 case 穿透忽略;MidStream 失败重试时已 emit AiTextDelta 累积 currentText,重试新 delta 追加→文本重复。修法:useAiEvents 补 case+重试前清 currentText;或后端 MidStream 不重试只重试 Init(对齐决策a)— CR-260616-30 归档
|
- [x] ✅(CR-260616-35 复审 PASS·agent cargo 6+20 passed) **CR-30-2** [med] AiStreamRetry 死事件 + MidStream 重试文本混乱 — 后端 emit(agentic.rs:310)+types.ts:214 variant 有,但 useAiEvents.ts switch 无 case 穿透忽略;MidStream 失败重试时已 emit AiTextDelta 累积 currentText,重试新 delta 追加→文本重复。修法:useAiEvents 补 case+重试前清 currentText;或后端 MidStream 不重试只重试 Init(对齐决策a)— CR-260616-30 归档
|
||||||
|
|
||||||
### ⏸️ 待用户授权/确认(巡检悬挂项·2026-06-16)
|
### ⏸️ 待用户授权/确认(巡检悬挂项·2026-06-16)
|
||||||
|
|
||||||
@@ -274,7 +274,7 @@
|
|||||||
- [x] ARC-260615-02 ✅(2026-06-16,待commit) — ~~`/decisions` 路由死链~~ 删 nav 项(App.vue secondaryNav)+ 删 `nav.decisions` i18n key(zh/en)。决策治理 = F-260614-08 长期项未实现,入口提前占位成死链;`dashboard.recentDecisions`(Dashboard.vue)不同命名空间保留
|
- [x] ARC-260615-02 ✅(2026-06-16,待commit) — ~~`/decisions` 路由死链~~ 删 nav 项(App.vue secondaryNav)+ 删 `nav.decisions` i18n key(zh/en)。决策治理 = F-260614-08 长期项未实现,入口提前占位成死链;`dashboard.recentDecisions`(Dashboard.vue)不同命名空间保留
|
||||||
|
|
||||||
**短期(低成本)**
|
**短期(低成本)**
|
||||||
- [ ] ARC-260615-03 — **重评降级(2026-06-16)**:df-execute **非空壳**——shell.rs `execute()` 已完整实现(cmd/sh 跨平台 + kill_on_drop + timeout + env)+ 被 tool_registry.rs `run_command` 复用(F-260615-05)。原 todo「76 行/1 函数 + TODO」基于 lib.rs 空判,过时。合并进 df-nodes 破坏职责分离(节点定义 vs 执行运行时)+ 动依赖树,**转架构维护决策,非清债**
|
- [x] ✅(决策a·2026-06-16·转架构维护记录无代码) ARC-260615-03 — **重评降级(2026-06-16)**:df-execute **非空壳**——shell.rs `execute()` 已完整实现(cmd/sh 跨平台 + kill_on_drop + timeout + env)+ 被 tool_registry.rs `run_command` 复用(F-260615-05)。原 todo「76 行/1 函数 + TODO」基于 lib.rs 空判,过时。合并进 df-nodes 破坏职责分离(节点定义 vs 执行运行时)+ 动依赖树,**转架构维护决策,非清债**
|
||||||
- [x] ARC-260615-04 ✅ wave(2026-06-16,待commit) — ~~`stores/index.ts` 补 `export useAiStore`~~ barrel 补导出(3 处 view 直连可选迁移,非强制)
|
- [x] ARC-260615-04 ✅ wave(2026-06-16,待commit) — ~~`stores/index.ts` 补 `export useAiStore`~~ barrel 补导出(3 处 view 直连可选迁移,非强制)
|
||||||
|
|
||||||
**中期(技术债)**
|
**中期(技术债)**
|
||||||
@@ -505,7 +505,7 @@
|
|||||||
> 用户需求:AiChat 消息气泡支持一键拷贝——**文本拷贝**(content 原文→剪贴板) + **截图拷贝**(气泡 DOM 渲染→图片→剪贴板,粘贴出图片)。**现状**:全项目零 clipboard 实现(grep `navigator.clipboard`/`copy`/`clipboard`/`拷贝`/`复制` 零命中)。气泡结构:用户消息 `.ai-msg-bubble--user`(纯文本 `{{msg.content}}`, :185);AI 消息 `.ai-msg-bubble--ai.ai-md`(v-html 渲染 MD, :197)。**Tauri WebView2(Edge)**:Clipboard API 完整支持(`writeText`/`write([ClipboardItem])`),无需额外 plugin(做减法,不用 `@tauri-apps/plugin-clipboard-manager`)。**安全约束**:必须在用户点击事件(手势)内调用剪贴板 API。
|
> 用户需求:AiChat 消息气泡支持一键拷贝——**文本拷贝**(content 原文→剪贴板) + **截图拷贝**(气泡 DOM 渲染→图片→剪贴板,粘贴出图片)。**现状**:全项目零 clipboard 实现(grep `navigator.clipboard`/`copy`/`clipboard`/`拷贝`/`复制` 零命中)。气泡结构:用户消息 `.ai-msg-bubble--user`(纯文本 `{{msg.content}}`, :185);AI 消息 `.ai-msg-bubble--ai.ai-md`(v-html 渲染 MD, :197)。**Tauri WebView2(Edge)**:Clipboard API 完整支持(`writeText`/`write([ClipboardItem])`),无需额外 plugin(做减法,不用 `@tauri-apps/plugin-clipboard-manager`)。**安全约束**:必须在用户点击事件(手势)内调用剪贴板 API。
|
||||||
|
|
||||||
- [x] F-260615-13 [P2] — **AiChat 气泡文本拷贝**。每条消息气泡右上角加拷贝按钮(📋 icon,hover 显示)。点击 → `navigator.clipboard.writeText(msg.content)` → 短暂 toast「已复制」。AI 气泡拷原始 content(非 v-html 渲染后 HTML,防 HTML 标签污染);用户气泡同。按钮位置:`.ai-msg-bubble` 内右下角或 hover 时右上角浮现(不占空间,信息密度优先)。i18n 加 `aiChat.copied` / `aiChat.copyMsg`。— src/components/AiChat.vue(:185 用户/:197 AI) + src/i18n (第⑫批销账 06-15)
|
- [x] F-260615-13 [P2] — **AiChat 气泡文本拷贝**。每条消息气泡右上角加拷贝按钮(📋 icon,hover 显示)。点击 → `navigator.clipboard.writeText(msg.content)` → 短暂 toast「已复制」。AI 气泡拷原始 content(非 v-html 渲染后 HTML,防 HTML 标签污染);用户气泡同。按钮位置:`.ai-msg-bubble` 内右下角或 hover 时右上角浮现(不占空间,信息密度优先)。i18n 加 `aiChat.copied` / `aiChat.copyMsg`。— src/components/AiChat.vue(:185 用户/:197 AI) + src/i18n (第⑫批销账 06-15)
|
||||||
- [ ] F-260615-14 [P2] — **AiChat 气泡截图拷贝(图片)**。同位置第二按钮(🖼️ 或与文本拷贝合并为下拉)。技术链:`html2canvas(bubbleElement)` → canvas → `canvas.toBlob('image/png')` → `new ClipboardItem({ 'image/png': blob })` → `navigator.clipboard.write([item])`。需加依赖 `html2canvas`(~90KB,或轻量替代 `dom-to-image-more` ~3KB,待选型)。**注意**:html2canvas 对 CSS 支持有限(阴影/渐变/CSS 变量可能不全),但气泡样式简单(ai-md 基本色+圆框),风险低。粘贴目标:Word/微信/钉钉等富文本编辑器可接图片;纯文本框不行(正常行为)。— src/components/AiChat.vue + 新依赖(html2canvas/dom-to-image-more 待选)
|
- [x] ✅(待决策.md已决c不做·2026-06-16) F-260615-14 [P2] — **AiChat 气泡截图拷贝(图片)**。同位置第二按钮(🖼️ 或与文本拷贝合并为下拉)。技术链:`html2canvas(bubbleElement)` → canvas → `canvas.toBlob('image/png')` → `new ClipboardItem({ 'image/png': blob })` → `navigator.clipboard.write([item])`。需加依赖 `html2canvas`(~90KB,或轻量替代 `dom-to-image-more` ~3KB,待选型)。**注意**:html2canvas 对 CSS 支持有限(阴影/渐变/CSS 变量可能不全),但气泡样式简单(ai-md 基本色+圆框),风险低。粘贴目标:Word/微信/钉钉等富文本编辑器可接图片;纯文本框不行(正常行为)。— src/components/AiChat.vue + 新依赖(html2canvas/dom-to-image-more 待选)
|
||||||
|
|
||||||
### 🟡 AI Chat 交互体验改进(2026-06-14 方案 → 待办化)
|
### 🟡 AI Chat 交互体验改进(2026-06-14 方案 → 待办化)
|
||||||
|
|
||||||
@@ -548,7 +548,7 @@
|
|||||||
|
|
||||||
- [x] AE-2025-01 [P0] — **批量审批**。同一轮多 pending 时,ToolCardList 顶部显示「全部批准(N) / 全部拒绝」按钮。改动:①`ToolCardList.vue` 加批量操作栏(监听 pending_approval 数量)②`useAiSend.ts` 新增 `approveAll(rejectAll)` 循环调 `ai_approve`。预估 0.5 天 — src/components/ToolCardList.vue + src/composables/ai/useAiSend.ts (第⑫批销账 06-15)
|
- [x] AE-2025-01 [P0] — **批量审批**。同一轮多 pending 时,ToolCardList 顶部显示「全部批准(N) / 全部拒绝」按钮。改动:①`ToolCardList.vue` 加批量操作栏(监听 pending_approval 数量)②`useAiSend.ts` 新增 `approveAll(rejectAll)` 循环调 `ai_approve`。预估 0.5 天 — src/components/ToolCardList.vue + src/composables/ai/useAiSend.ts (第⑫批销账 06-15)
|
||||||
- [x] AE-2025-02 [P0] — **审批计数器 + 跳转**。输入框上方/header 显示 `⏳ N 项待审批` 徽标,点击跳转到第一个 pending 卡片。改动:①`AiChat.vue` header 区域加审批徽标 ②`ToolCardList.vue` 暴露 `scrollToFirstPending` 方法。预估 0.5 天 — src/components/AiChat.vue + src/components/ToolCardList.vue (第⑬批销账 06-15)
|
- [x] AE-2025-02 [P0] — **审批计数器 + 跳转**。输入框上方/header 显示 `⏳ N 项待审批` 徽标,点击跳转到第一个 pending 卡片。改动:①`AiChat.vue` header 区域加审批徽标 ②`ToolCardList.vue` 暴露 `scrollToFirstPending` 方法。预估 0.5 天 — src/components/AiChat.vue + src/components/ToolCardList.vue (第⑬批销账 06-15)
|
||||||
- [ ] AE-2025-03 [P0] — **write_file diff 预览** ⚠️**降级阻塞(2026-06-16·batch54 workflow 探索+主代核验)**:后端 write_file **未返 diff**(`tool_registry.rs:694` 结果 payload `{path,bytes_written,old_size,encoding}` 无 diff 字段,`generate_diff` 仅 patch_file 调用),前端 ToolCard 无 diff 可渲染,审批 payload `AiApprovalRequired`/`PendingApproval` 亦无 diff(只有 `args={path,content,encoding}` 纯新内容,拿不到旧文件)。需**后端先补**再追加前端渲染。**两条路径**(均 src-tauri 改动,跨 mod.rs/audit.rs/types.ts/AiChat.vue 领地):路径 B(推荐,满足审批预览诉求)审批 payload 加 `diff` 字段 + audit.rs:555 挂起审批前预读旧文件 + `generate_diff` 注入事件;路径 A 仅 write_file handler 补 diff 但结果阶段才有(审批时看不到,不满足)。前端部分待后端 diff 返回后追加 `<pre>` 红删绿增渲染。原诉求:write_file 审批时若文件已存在,展示前后对比 diff 而非裸 content(content 可能几百行代码截断到 300 字符)。改动:①后端 `tool_registry.rs` write_file handler 执行前读旧文件返回 diff ②`ToolCard.vue` pending_approval + name=write_file 时渲染 diff 视图(红删绿增)。关联 F-260615-10(diff 工具)可复用。预估 1 天 — src-tauri/src/commands/ai/tool_registry.rs + src/components/ToolCard.vue
|
- [x] ✅(batch60·2026-06-16·workflow whae812z5+主代核查·路径B,cargo+vue-tsc 0err) AE-2025-03 [P0] — **write_file diff 预览** ✅**已落地(batch60 路径B)**:后端 write_file **未返 diff**(`tool_registry.rs:694` 结果 payload `{path,bytes_written,old_size,encoding}` 无 diff 字段,`generate_diff` 仅 patch_file 调用),前端 ToolCard 无 diff 可渲染,审批 payload `AiApprovalRequired`/`PendingApproval` 亦无 diff(只有 `args={path,content,encoding}` 纯新内容,拿不到旧文件)。需**后端先补**再追加前端渲染。**两条路径**(均 src-tauri 改动,跨 mod.rs/audit.rs/types.ts/AiChat.vue 领地):路径 B(推荐,满足审批预览诉求)审批 payload 加 `diff` 字段 + audit.rs:555 挂起审批前预读旧文件 + `generate_diff` 注入事件;路径 A 仅 write_file handler 补 diff 但结果阶段才有(审批时看不到,不满足)。前端部分待后端 diff 返回后追加 `<pre>` 红删绿增渲染。原诉求:write_file 审批时若文件已存在,展示前后对比 diff 而非裸 content(content 可能几百行代码截断到 300 字符)。改动:①后端 `tool_registry.rs` write_file handler 执行前读旧文件返回 diff ②`ToolCard.vue` pending_approval + name=write_file 时渲染 diff 视图(红删绿增)。关联 F-260615-10(diff 工具)可复用。预估 1 天 — src-tauri/src/commands/ai/tool_registry.rs + src/components/ToolCard.vue
|
||||||
|
|
||||||
**P1 — 增强控制力**
|
**P1 — 增强控制力**
|
||||||
|
|
||||||
@@ -570,7 +570,7 @@
|
|||||||
|
|
||||||
- [ ] B-260614-05 — **[P2→降级]** 分离窗口(detached)跨窗口状态 — **核对修正**:reattachPanel 已接线(不再死代码)+ `tauri://destroyed` 监听已复位状态,"detached 永真卡死"已修复;剩余 localStorage `df-ai-gen`/`df-ai-text` 是 Sprint 19 **有意保留**(流式临时快照高频写),非 bug。仅在出现新场景失效时再评估改全局 emit/listen — **去重**:与 AR-M5(跨窗口 state 完全隔离)同类,aichat 审查描述更深 — source:代码审查 + Sprint 19 (06-14)
|
- [ ] B-260614-05 — **[P2→降级]** 分离窗口(detached)跨窗口状态 — **核对修正**:reattachPanel 已接线(不再死代码)+ `tauri://destroyed` 监听已复位状态,"detached 永真卡死"已修复;剩余 localStorage `df-ai-gen`/`df-ai-text` 是 Sprint 19 **有意保留**(流式临时快照高频写),非 bug。仅在出现新场景失效时再评估改全局 emit/listen — **去重**:与 AR-M5(跨窗口 state 完全隔离)同类,aichat 审查描述更深 — source:代码审查 + Sprint 19 (06-14)
|
||||||
- [x] T-260614-03 — ~~工具层白名单与 crud 白名单双份去重~~ ✅ WF-B ⑤ 已解决(tool_registry update_project 复用 is_allowed_column,与 CRUD 白名单同源,删硬编码 match)(06-14)
|
- [x] T-260614-03 — ~~工具层白名单与 crud 白名单双份去重~~ ✅ WF-B ⑤ 已解决(tool_registry update_project 复用 is_allowed_column,与 CRUD 白名单同源,删硬编码 match)(06-14)
|
||||||
- [ ] F-260614-02 — 技能联想「使用」 — 首批 3 类联想已做,联想后实际触发/执行技能未实现 (06-14)
|
- [ ] F-260614-02 — 技能联想「使用」·**决策ai调用已定待设计**(2026-06-16) — 首批 3 类联想已做,「使用」=**AI 调用技能**(联想选中→技能内容/指令注入 AI context→AI 按指令执行,非执行本机 claude 技能)。实施机制待设计(技能→AI context 注入 vs 技能注册 AI 工具,依赖 F-07 trait 下沉)
|
||||||
- [ ] F-260614-03 — **[需设计]** 灵感对抗评估接 LLM — 核对 adversarial.rs:`evaluate()` 纯启发式,接 LLM 需改签名注入 provider。3 种注入方式待定:A.trait 解耦(df-ideas 定义 IdeaAnalyzer trait,推荐)/ B.df-ideas 直接依赖 df-ai / C.closure。属架构决策非直接做 (06-14)
|
- [ ] F-260614-03 — **[需设计]** 灵感对抗评估接 LLM — 核对 adversarial.rs:`evaluate()` 纯启发式,接 LLM 需改签名注入 provider。3 种注入方式待定:A.trait 解耦(df-ideas 定义 IdeaAnalyzer trait,推荐)/ B.df-ideas 直接依赖 df-ai / C.closure。属架构决策非直接做 (06-14)
|
||||||
- [x] T-260614-04 — ~~路径校验根治~~ ✅ 已完成(resolve_workspace_path 加 canonicalize 防 symlink 逃逸 + 词法 starts_with 兜底;仅校验、返回词法路径保持前端友好;cargo check 0 err / 22 test pass)(06-14)
|
- [x] T-260614-04 — ~~路径校验根治~~ ✅ 已完成(resolve_workspace_path 加 canonicalize 防 symlink 逃逸 + 词法 starts_with 兜底;仅校验、返回词法路径保持前端友好;cargo check 0 err / 22 test pass)(06-14)
|
||||||
- [ ] F-260614-04 — 多 Provider 负载均衡池 — 备用模型/多账号聚合,全局容量=min(各 provider 上限之和, global_cap) (06-14)
|
- [ ] F-260614-04 — 多 Provider 负载均衡池 — 备用模型/多账号聚合,全局容量=min(各 provider 上限之和, global_cap) (06-14)
|
||||||
@@ -579,10 +579,10 @@
|
|||||||
- [x] T-260614-09 — ~~idea.rs 物理删不级联~~ ✅ WF-E 完成(idea.rs:149 delete→purge_with_descendants,1 行,签名兼容)(06-14, commit 89da9fa)
|
- [x] T-260614-09 — ~~idea.rs 物理删不级联~~ ✅ WF-E 完成(idea.rs:149 delete→purge_with_descendants,1 行,签名兼容)(06-14, commit 89da9fa)
|
||||||
- [x] T-260614-10 — ~~findBinding canonicalize~~ ✅ WF-E 判定已解决(normalize_path 已含 canonicalize 优先 + 词法回退,find_binding_conflict 两端对称已用,无需重复加;零改动)(06-14, commit 89da9fa)
|
- [x] T-260614-10 — ~~findBinding canonicalize~~ ✅ WF-E 判定已解决(normalize_path 已含 canonicalize 优先 + 词法回退,find_binding_conflict 两端对称已用,无需重复加;零改动)(06-14, commit 89da9fa)
|
||||||
- [ ] F-260614-07 — **[架构前置]** df-ai-core trait 下沉拆 crate — F-03 注入方式前置(决策记录已收敛:原 F-03 的 A/B/C 选型作废,统一为全局 AI trait 下沉独立 crate,df-ideas/df-nodes 依赖 trait 而非 df-ai 具体 impl)。解锁 F-03 — source:功能决策记录 (06-14)
|
- [ ] F-260614-07 — **[架构前置]** df-ai-core trait 下沉拆 crate — F-03 注入方式前置(决策记录已收敛:原 F-03 的 A/B/C 选型作废,统一为全局 AI trait 下沉独立 crate,df-ideas/df-nodes 依赖 trait 而非 df-ai 具体 impl)。解锁 F-03 — source:功能决策记录 (06-14)
|
||||||
- [ ] F-260614-08 — 决策治理产品化 — 活契约/规格契约自检机制产品化为可操作功能(当前散落文档机制) — source:功能决策记录缺口
|
- [x] ⏸️(待决策.md已决c暂缓·2026-06-16) F-260614-08 — 决策治理产品化 — 活契约/规格契约自检机制产品化为可操作功能(当前散落文档机制) — source:功能决策记录缺口
|
||||||
- [ ] F-260614-09 — 项目 status 字段治理 — status 状态机规范化(planning/active/archived 等枚举约束) — source:功能决策记录缺口
|
- [x] ⏸️(待决策.md已决c暂缓·2026-06-16) F-260614-09 — 项目 status 字段治理 — status 状态机规范化(planning/active/archived 等枚举约束) — source:功能决策记录缺口
|
||||||
- [ ] F-260614-10 — 知识库 MCP Server + Tier 2/3 — 对外 MCP 暴露 + 分层存储(当前仅 Tier 1 全栈) — source:PROGRESS Sprint15
|
- [x] ⏸️(待决策.md已决c暂缓·2026-06-16) F-260614-10 — 知识库 MCP Server + Tier 2/3 — 对外 MCP 暴露 + 分层存储(当前仅 Tier 1 全栈) — source:PROGRESS Sprint15
|
||||||
- [ ] T-260614-11 — 全局#6 条件表达式引擎升级 — df-workflow conditions 仅支持 true/false 字面量,升级为真表达式求值 — source:PROGRESS 全局问题
|
- [x] ⏸️(待决策.md已决暂缓·2026-06-16) T-260614-11 — 全局#6 条件表达式引擎升级 — df-workflow conditions 仅支持 true/false 字面量,升级为真表达式求值 — source:PROGRESS 全局问题
|
||||||
- [x] T-260614-12 — ~~df-ideas 死代码~~ ✅ WF-E 部分完成(capture.rs 删 CaptureInput/IdeaCapture 死码,保留 Idea/IdeaScores 共享实体;promotion/scoring/adversarial 内"两套 Recommendation/PromotionPolicy 死枚举"嫌疑 agent 未确认存在/保留为对外契约,本次未动,待复查)(06-14, commit 89da9fa)
|
- [x] T-260614-12 — ~~df-ideas 死代码~~ ✅ WF-E 部分完成(capture.rs 删 CaptureInput/IdeaCapture 死码,保留 Idea/IdeaScores 共享实体;promotion/scoring/adversarial 内"两套 Recommendation/PromotionPolicy 死枚举"嫌疑 agent 未确认存在/保留为对外契约,本次未动,待复查)(06-14, commit 89da9fa)
|
||||||
- [x] F-260615-01 ✅(2026-06-16,待commit a3cccb070fe9c8821) — **[P1 功能增强]** HumanNode 审批节点支持自定义选项 + 单选/多选类型。现状:config `options: Vec<String>` 已支持任意数量(2/3/…数量扩展已通),但 `decision` 是单 `String` 仅单选语义,`options` 空=自由文本。增强目标:不止「同意/拒绝」二选一,可配置 N 个候选项 + 单选(single)/多选(multiple)两种类型。**改动面**:①`df-core/events.rs` `WorkflowEvent::HumanApprovalRequest` 加 `select_type`、`HumanApprovalResponse` decision 单值→多值(`decisions: Vec<String>` 或保留 decision 兼容 + 加 decisions)②`human_node.rs` config 解析 `select_type` + 校验(多选时每项 ∈ options,可加 min/max 选中数约束)③IPC `approve_human_approval` 签名 ④前端 `stores/project.ts` approve + `api/types.ts` 事件类型 + 审批弹窗 UI(单选 radio / 多选 checkbox)⑤单测改断言 + 新增多选/超限测。**注意**:向后兼容现 single 调用方,`select_type` 缺省 = single — source:用户需求(06-15),crates/df-nodes/src/human_node.rs + src-tauri/src/commands/workflow.rs + src/stores/project.ts — **✅已实施(a3cccb070fe9c8821,2026-06-16,待commit)**:6 文件契约向后兼容(events.rs SelectType 枚举 Single/Multiple 缺省 Single+HumanApprovalResponse decision+decisions 双字段/human_node.rs 校验 single len==1·multiple len≥1·∈options/workflow.rs IPC 加 decisions+select_type Option 缺省兼容/types.ts/project.ts approve/ProjectDetail.vue checkbox 多选 UI)。cargo check 0 err/df-nodes 21 test(含 4 新增多选)/vue-tsc 0 err。主代理核查契约向后兼容 + 6 文件边界
|
- [x] F-260615-01 ✅(2026-06-16,待commit a3cccb070fe9c8821) — **[P1 功能增强]** HumanNode 审批节点支持自定义选项 + 单选/多选类型。现状:config `options: Vec<String>` 已支持任意数量(2/3/…数量扩展已通),但 `decision` 是单 `String` 仅单选语义,`options` 空=自由文本。增强目标:不止「同意/拒绝」二选一,可配置 N 个候选项 + 单选(single)/多选(multiple)两种类型。**改动面**:①`df-core/events.rs` `WorkflowEvent::HumanApprovalRequest` 加 `select_type`、`HumanApprovalResponse` decision 单值→多值(`decisions: Vec<String>` 或保留 decision 兼容 + 加 decisions)②`human_node.rs` config 解析 `select_type` + 校验(多选时每项 ∈ options,可加 min/max 选中数约束)③IPC `approve_human_approval` 签名 ④前端 `stores/project.ts` approve + `api/types.ts` 事件类型 + 审批弹窗 UI(单选 radio / 多选 checkbox)⑤单测改断言 + 新增多选/超限测。**注意**:向后兼容现 single 调用方,`select_type` 缺省 = single — source:用户需求(06-15),crates/df-nodes/src/human_node.rs + src-tauri/src/commands/workflow.rs + src/stores/project.ts — **✅已实施(a3cccb070fe9c8821,2026-06-16,待commit)**:6 文件契约向后兼容(events.rs SelectType 枚举 Single/Multiple 缺省 Single+HumanApprovalResponse decision+decisions 双字段/human_node.rs 校验 single len==1·multiple len≥1·∈options/workflow.rs IPC 加 decisions+select_type Option 缺省兼容/types.ts/project.ts approve/ProjectDetail.vue checkbox 多选 UI)。cargo check 0 err/df-nodes 21 test(含 4 新增多选)/vue-tsc 0 err。主代理核查契约向后兼容 + 6 文件边界
|
||||||
- [x] F-260615-02 ✅(2026-06-16,待commit) — **[P1 功能]** task 详情查看。✅已实施:get_task_by_id IPC(复用 TaskRepo::get_by_id ok_or_else 转 Result)+lib.rs 注册+taskApi.get+/tasks/:id 路由+TaskDetail.vue(11 字段:标题/描述/状态/优先级/关联项目 router-link 解析名/分支标签/负责人/基础分支/工作流定义/创建更新时间;复用 constants/project 标签+formatDate+watch route.params.id 重载)+Tasks.vue 列表项 @click router.push;TaskRecord TS 类型已存在无需新增;主代理核查 cargo check 0 err/vue-tsc 0 err/git diff 6 文件边界干净。现状:`src/views/Tasks.vue` 仅列表,无独立 TaskDetail 视图/路由(grep 仅 `ProjectDetail.vue` 嵌套任务命中,无独立详情页)。需求:点击 task 查看详情。**数据模型已就绪**(`df-storage/src/models.rs:53` `TaskRecord` 12 字段:id / project_id / title / description / status / priority / branch_name / assignee / workflow_def_id / base_branch / created_at / updated_at)。**改动面**:①新建 `src/views/TaskDetail.vue` 视图 + 路由(`router/index.ts` `/tasks/:id`)②task 详情 IPC(`get_task_by_id`,核对 `commands/task.rs` 现有 IPC 是否已有,无则补)③`Tasks.vue` 列表项点击 → 跳详情 ④详情页字段展示(title/description 渲染、status/priority 标签、关联项目名解析 project_id→name、branch/assignee 信息、时间戳)⑤可选:详情页内编辑(`update_task` IPC 已存在,FR-D6)。— source:用户需求(06-15),src/views/Tasks.vue + src/router/index.ts + src-tauri/src/commands/task.rs
|
- [x] F-260615-02 ✅(2026-06-16,待commit) — **[P1 功能]** task 详情查看。✅已实施:get_task_by_id IPC(复用 TaskRepo::get_by_id ok_or_else 转 Result)+lib.rs 注册+taskApi.get+/tasks/:id 路由+TaskDetail.vue(11 字段:标题/描述/状态/优先级/关联项目 router-link 解析名/分支标签/负责人/基础分支/工作流定义/创建更新时间;复用 constants/project 标签+formatDate+watch route.params.id 重载)+Tasks.vue 列表项 @click router.push;TaskRecord TS 类型已存在无需新增;主代理核查 cargo check 0 err/vue-tsc 0 err/git diff 6 文件边界干净。现状:`src/views/Tasks.vue` 仅列表,无独立 TaskDetail 视图/路由(grep 仅 `ProjectDetail.vue` 嵌套任务命中,无独立详情页)。需求:点击 task 查看详情。**数据模型已就绪**(`df-storage/src/models.rs:53` `TaskRecord` 12 字段:id / project_id / title / description / status / priority / branch_name / assignee / workflow_def_id / base_branch / created_at / updated_at)。**改动面**:①新建 `src/views/TaskDetail.vue` 视图 + 路由(`router/index.ts` `/tasks/:id`)②task 详情 IPC(`get_task_by_id`,核对 `commands/task.rs` 现有 IPC 是否已有,无则补)③`Tasks.vue` 列表项点击 → 跳详情 ④详情页字段展示(title/description 渲染、status/priority 标签、关联项目名解析 project_id→name、branch/assignee 信息、时间戳)⑤可选:详情页内编辑(`update_task` IPC 已存在,FR-D6)。— source:用户需求(06-15),src/views/Tasks.vue + src/router/index.ts + src-tauri/src/commands/task.rs
|
||||||
@@ -693,7 +693,7 @@
|
|||||||
- [x] ✅(batch34·2026-06-16·workflow wvr7mdo0p) B-260615-42 [P1] — ~~错误信息不一致~~ 同类错误在不同场景下返回的信息格式不统一。根因:错误处理分散在不同模块,缺乏统一规范。影响:用户和调试者难以理解错误含义。修法:制定错误码规范 + 统一错误格式 + 前端错误码映射 — src-tauri/src/commands/ai/mod.rs + src/api/types.ts
|
- [x] ✅(batch34·2026-06-16·workflow wvr7mdo0p) B-260615-42 [P1] — ~~错误信息不一致~~ 同类错误在不同场景下返回的信息格式不统一。根因:错误处理分散在不同模块,缺乏统一规范。影响:用户和调试者难以理解错误含义。修法:制定错误码规范 + 统一错误格式 + 前端错误码映射 — src-tauri/src/commands/ai/mod.rs + src/api/types.ts
|
||||||
|
|
||||||
**P2 — 中等问题**
|
**P2 — 中等问题**
|
||||||
- [ ] B-260615-43 [P2] — ~~无撤销重做功能~~ AI 操作(如删除项目、修改任务)无撤销功能。根因:缺乏操作历史记录和回滚机制。影响:误操作无法恢复。修法:实现操作历史栈 + 撤销/重做 UI + 关键操作二次确认 — src/stores/project.ts + src/views/ProjectDetail.vue
|
- [x] ⏸️(决策c暂缓·2026-06-16) B-260615-43 [P2] — ~~无撤销重做功能~~ AI 操作(如删除项目、修改任务)无撤销功能。根因:缺乏操作历史记录和回滚机制。影响:误操作无法恢复。修法:实现操作历史栈 + 撤销/重做 UI + 关键操作二次确认 — src/stores/project.ts + src/views/ProjectDetail.vue
|
||||||
- [x] B-260615-44 [P2] — ~~工具响应时间无反馈~~ 工具执行(如 read_file、write_file)长时间无响应时用户不知道卡在哪里。根因:缺乏工具执行进度反馈。修法:添加工具执行进度提示 + 超时警告 + 取消按钮 — src/composables/ai/useAiStream.ts
|
- [x] B-260615-44 [P2] — ~~工具响应时间无反馈~~ 工具执行(如 read_file、write_file)长时间无响应时用户不知道卡在哪里。根因:缺乏工具执行进度反馈。修法:添加工具执行进度提示 + 超时警告 + 取消按钮 — src/composables/ai/useAiStream.ts
|
||||||
- [x] ✅(batch41·2026-06-16·workflow w9vtei15z) B-260615-45 [P2] — ~~内存泄漏隐患~~ 已核查覆盖(useAiEvents stopListener 三 listener+计时器全清 + AiChat onBeforeUnmount + MESSAGE_CAP=200 滚动淘汰),无泄漏不强改 长时间使用后内存占用持续增长。根因:消息缓存、事件监听器未及时清理。修法:优化缓存策略 + 定期清理机制 + 内存使用监控 — src/stores/ai.ts + src/composables/ai/useAiEvents.ts
|
- [x] ✅(batch41·2026-06-16·workflow w9vtei15z) B-260615-45 [P2] — ~~内存泄漏隐患~~ 已核查覆盖(useAiEvents stopListener 三 listener+计时器全清 + AiChat onBeforeUnmount + MESSAGE_CAP=200 滚动淘汰),无泄漏不强改 长时间使用后内存占用持续增长。根因:消息缓存、事件监听器未及时清理。修法:优化缓存策略 + 定期清理机制 + 内存使用监控 — src/stores/ai.ts + src/composables/ai/useAiEvents.ts
|
||||||
|
|
||||||
|
|||||||
17
docs/待决策.md
17
docs/待决策.md
@@ -118,6 +118,17 @@
|
|||||||
- **关联**:todo F-260615-14
|
- **关联**:todo F-260615-14
|
||||||
- **状态**:✅ 已决(2026-06-16)— **决策:c 不做**。文本拷贝+代码块复制已覆盖核心需求,等用户真实反馈再定。
|
- **状态**:✅ 已决(2026-06-16)— **决策:c 不做**。文本拷贝+代码块复制已覆盖核心需求,等用户真实反馈再定。
|
||||||
|
|
||||||
|
#### F-260616-11 审批续跑 iteration 计数策略
|
||||||
|
- **背景**:`try_continue_agent_loop` 审批后续跑 iteration 从 0 重计(agentic.rs:418 注释,F-260616-03 续跑决策 a 延伸),多次审批(Med/High 工具)=反复重跑 max_iterations 轮,token 消耗不受 max_iterations 严格控制。
|
||||||
|
- **决策点**:iteration 累计 vs 重置?
|
||||||
|
- **选项**:
|
||||||
|
- a: 累计计数(try_continue 不重置 iteration,跨审批续跑延续累计)
|
||||||
|
- b: 重置 + 独立「审批次数」配额
|
||||||
|
- c: token 总量兜底(不碰 iteration,加 token 硬上限)
|
||||||
|
- **推荐**:**a + c**(累计防超配 + token 兜底)
|
||||||
|
- **关联**:todo F-260616-11 / F-260616-03
|
||||||
|
- **状态**:✅ 已决(2026-06-16)— **决策:a**。累计计数,try_continue 不重置 iteration 跨审批累计,防多次审批 token 失控。token 兜底 c 暂不加(避免过度设计,iteration 累计已控)。实施待 batch60 完成(agentic.rs 锁释放)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### ④类 — 设计方向(方案未定,需人定 A/B/C 选型)
|
### ④类 — 设计方向(方案未定,需人定 A/B/C 选型)
|
||||||
@@ -233,7 +244,7 @@
|
|||||||
- **决策点**:「使用」语义?(执行本机 claude 技能?AI 调用?需明确)
|
- **决策点**:「使用」语义?(执行本机 claude 技能?AI 调用?需明确)
|
||||||
- **推荐**:需先澄清语义(关联 memory aichat-skill-slash-autocomplete)
|
- **推荐**:需先澄清语义(关联 memory aichat-skill-slash-autocomplete)
|
||||||
- **关联**:todo F-260614-02
|
- **关联**:todo F-260614-02
|
||||||
- **状态**:🟡 待澄清语义
|
- **状态**:✅ 已决(2026-06-16)— **决策:ai调用**(联想选中技能→AI 调用执行,非执行本机 claude 技能。实施机制待设计:技能内容注入 AI context 或技能注册为 AI 工具,依赖 F-07 trait 下沉)。
|
||||||
|
|
||||||
#### F-260614-04 多 Provider 负载均衡池
|
#### F-260614-04 多 Provider 负载均衡池
|
||||||
- **背景**:备用模型/多账号聚合,全局容量=min(各 provider 上限之和, global_cap)。
|
- **背景**:备用模型/多账号聚合,全局容量=min(各 provider 上限之和, global_cap)。
|
||||||
@@ -282,7 +293,7 @@
|
|||||||
- **选项**: a:维持独立(职责清晰:节点定义vs执行运行时) / b:合并df-nodes(减少crate数)
|
- **选项**: a:维持独立(职责清晰:节点定义vs执行运行时) / b:合并df-nodes(减少crate数)
|
||||||
- **推荐**:**a**(维持独立,转架构维护记录非清债)
|
- **推荐**:**a**(维持独立,转架构维护记录非清债)
|
||||||
- **关联**:todo ARC-260615-03
|
- **关联**:todo ARC-260615-03
|
||||||
- **状态**:🟡 待决
|
- **状态**:✅ 已决(2026-06-16)— **决策:a**(维持独立,df-execute 非空壳 shell.rs execute() 已完整实现 + 被 run_command 复用,转架构维护记录非清债)。
|
||||||
|
|
||||||
#### ARC-260615-07 中期技术债7件
|
#### ARC-260615-07 中期技术债7件
|
||||||
- **背景**:df-core改名df-types / IPC层抽取df-app / AI loop下沉df-ai / 3 view绕store调api / 类型契约ts-rs代码生成 / AiSession多会话 / AppState分组 / IPC命名统一。
|
- **背景**:df-core改名df-types / IPC层抽取df-app / AI loop下沉df-ai / 3 view绕store调api / 类型契约ts-rs代码生成 / AiSession多会话 / AppState分组 / IPC命名统一。
|
||||||
@@ -306,7 +317,7 @@
|
|||||||
- **选项**: a:仅AI工具操作(delete_task/delete_project/restore/run_command等) / b:全局(含项目/任务CRUD) / c:暂缓(二次确认已部分覆盖)
|
- **选项**: a:仅AI工具操作(delete_task/delete_project/restore/run_command等) / b:全局(含项目/任务CRUD) / c:暂缓(二次确认已部分覆盖)
|
||||||
- **推荐**:**c 暂缓**(High-risk工具已有二次确认AE-05,撤销复杂度高)
|
- **推荐**:**c 暂缓**(High-risk工具已有二次确认AE-05,撤销复杂度高)
|
||||||
- **关联**:todo B-260615-43
|
- **关联**:todo B-260615-43
|
||||||
- **状态**:🟡 待决
|
- **状态**:✅ 已决(2026-06-16)— **决策:c 暂缓**(High-risk 已有二次确认 AE-05,撤销复杂度高)。
|
||||||
|
|
||||||
### C类 — 用户实测/澄清(需用户操作给反馈,主会话无法代办)
|
### C类 — 用户实测/澄清(需用户操作给反馈,主会话无法代办)
|
||||||
|
|
||||||
|
|||||||
26
docs/待审查.md
26
docs/待审查.md
@@ -23,11 +23,35 @@
|
|||||||
|
|
||||||
## 当前队列
|
## 当前队列
|
||||||
|
|
||||||
### CR-260616-35 batch59 CR-34-1 守卫 + CR-30 流式重试对齐决策a1 + UX-2025-04 断线保文 — 待审
|
### CR-260616-36 batch60 F-09A 隔离补清 + F-13 性能优化 + AE-03 write_file diff 预览(路径B) — ⏳ 待审
|
||||||
|
|
||||||
|
- **范围**: 3 agent 合并批(workflow whae812z5,主代独立核查全过 + cargo check --workspace EXIT 0 + vue-tsc EXIT 0)。①**F-09 A 路线**(单例软隔离补漏清字段,不动 B 多会话架构):前端 `useAiConversations.ts:42-45` newConversation 补清 `queue=[]/generatingConvId=null/agentRound=0/searchQuery=''`(防旧会话排队消息带进新会话 + 后台事件错路由 + 侧栏旧搜索过滤);后端 `commands.rs:842-843` ai_conversation_create 补清 `stop_flag.store(false,SeqCst)`(关键:上方生成中分支置 true 停旧 loop 不复位则新会话 loop 启动即见 stop 退出) + `agent_language=None`(防沿用旧语言设置)。②**F-13 性能**:`agentic.rs:173` system_prompt token loop 外算一次缓存为 sys_tokens(整个 loop 不变参数,原每轮+每次重试重算);`:219-225` 外层 messages 构建用 sys_tokens;`:262-270` 重试循环改 `messages: messages.clone()` 复用外层 Vec(删每次重试的 session_arc.lock + build_for_request 全量 clone + estimate_text)。安全前提核验:stream_llm 不接 session_arc + 重试块到 process_tool_calls 间无 push,重试内 session.messages 与外层一致,重建等价复用。③**AE-03 路径 B**(审批 payload 加 diff,非路径 A):`tool_registry.rs:32` generate_diff fn→pub(crate)(复用 F-260615-10 LCS);`audit.rs:508-520` 新增 build_write_file_diff(从 args 取 path+content → 预读旧文件 tokio::fs::read_to_string → 无变化/旧文件缺失/读失败均 None → generate_diff);`audit.rs:588-609` 仅 write_file draft 挂起审批前预计算 approval_diff 注入 PendingApproval.diff(:600 clone)+AiApprovalRequired.diff(:609 move);恢复路径 diff=None(文件可能已变);`mod.rs:101/269` AiApprovalRequired+PendingApproval 加 `diff: Option<String>`;`types.ts:198` AiApprovalRequired 加 `diff?: string`;`useAiEvents.ts` event.diff 传 toolCall 信息;`ToolCard.vue:40-41` 模板 `v-if="tc.diff"` diff 块 + :394 diffLines computed(按 +/-/space 前缀拆分)+ :677-705 样式(add 绿 del 红 ctx 灰 + token 复用)。
|
||||||
|
- **维度**: ①F-09 单例字段补清完整性(stop_flag 复位是否覆盖所有生成中→新建路径/agent_language 影响面) + B 路线边界(确认未误动 AiSession 单例) ②F-13 行为不变性核验(sys_tokens 缓存是否真不变/messages.clone() 复用是否等价重建/stream_llm 不持 session_arc 前提是否成立) + 锁持有期缩短收益确认 ③AE-03 路径 B 安全性(预读旧文件只读不改/审批拒绝不执行 handler/路径不校验风险——恶意路径读失败仅 None) + diff 注入完整性(实时挂起+恢复路径双覆盖/前端消费链 event→toolCall→模板) + generate_diff pub(crate) 暴露面。
|
||||||
|
- **commit**: 待提交(累积 ~12 文件攒批 + 4 决策回填)。
|
||||||
|
|
||||||
|
### CR-260616-37 F-11 审批续跑 iteration 累计计数(跨审批不重置) — ⏳ 待审
|
||||||
|
|
||||||
|
- **范围**: F-260616-11 决策a 落地(与 batch60 三项合批 commit)。`mod.rs:215/230` AiSession 加 `iteration_used:usize` 字段+new() init 0;`agentic.rs:114` run_agentic_loop 加 `start_iteration:usize` 参数 +`:176` loop 边界改 `start_iteration..max_iterations` +`:210` 一致性校验块更新 `session.iteration_used=iteration+1` +`:578` try_continue_agent_loop 加 start_iteration 参数 +`:675` spawn 透传 +`:499-501` 注释区分两路径;`commands.rs:55/152/412` ai_chat_send/ai_regenerate/ai_edit_last reset iteration_used=0(新生命周期)+`:260-262` ai_approve 拒绝续跑读 session.iteration_used 累计传 +`:313-315` ai_approve 通过续跑累计传 +`:601` ai_continue_loop reset iteration_used=0 传 0(F-03 决策a 达max重计区分)。两路径严格区分:审批续跑(ai_approve 累计透传 iteration_used) vs 达max续跑(ai_continue_loop reset+传0,F-03 决策a 用户授权重来) vs 新消息(reset 0)。
|
||||||
|
- **维度**: ①iteration_used 字段生命周期完备性(所有会话生命周期入口 reset 覆盖——create/send/regenerate/edit_last/continue_loop 是否齐全无遗漏路径) ②start_iteration 透传链完整(agentic run_agentic_loop←try_continue_agent_loop←ai_approve 两个调用点,无断链) ③两路径区分正确性(ai_approve 累计 vs ai_continue_loop 重计,F-11/F-03 决策 a 分别归属无混淆) ④边界 case(start≥max loop 空区间→converged=false→AiMaxRoundsReached,防无限审批烧 token,符合 F-11 语义) ⑤并发安全(iteration_used 在一致性 lock 块更新,与现有 lock 模式一致)。
|
||||||
|
- **commit**: 待提交(同 CR-36 合批)。
|
||||||
|
- **主代独立核查**: ✅ 全过(useAiConversations:42-45 补清 / commands:842-843 stop_flag.store(false)+agent_language=None / agentic:173 sys_tokens loop 外缓存 + :219-225 外层用 + :262-270 重试 messages.clone() 复用 + 保文退避逻辑未动(Partial 不重试/retry_deadline 30s) / tool_registry:32 generate_diff pub(crate) / audit:508-520 build_write_file_diff 预读+无变化/缺失 None+复用 generate_diff / audit:588-609 仅 write_file 生成 diff 注入 PendingApproval(clone)+AiApprovalRequired(move) / mod:101+269 diff 字段 / types:198 diff? / ToolCard:40-41 模板 + :394 diffLines + :677-705 样式 / cargo+vue-tsc 双 EXIT 0)。
|
||||||
|
- **审查 agent 待复审重点**: ①F-13 重试 messages.clone() 复用等价性——核验 stream_llm 签名(stream_recv.rs:137 不接 session_arc) + 重试块到 process_tool_calls 间确实无 session.messages.push(若遗漏则重试复用旧 messages 致 tool_calls 丢失) ②F-13 sys_tokens 缓存——核验 system_prompt 确为 run_agentic_loop 不变参数(整个 loop 期间无 mutate) ③AE-03 build_write_file_diff 安全——预读旧文件 `tokio::fs::read_to_string(path)` 路径未校验(恶意 path 读失败仅 None 不产生危害,但核验无 symlink 逃逸读敏感文件风险——审批只读,write_file handler 自身 validate_path 执行时兜底) ④AE-03 diff 注入双路径——实时挂起(audit:588-609 有 diff)+恢复路径(PendingApproval diff 字段是否持久化,若仅内存则重启恢复审批无 diff) ⑤文件锁独立性——3 agent 改动文件无重叠(F-09: useAiConversations/commands / F-13: agentic / AE-03: mod/tool_registry/audit/types/useAiEvents/ToolCard),useAiEvents.ts Agent C 独占(A/B 未碰)确认无冲突。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CR-260616-35 batch59 CR-34-1 守卫 + CR-30 流式重试对齐决策a1 + UX-2025-04 断线保文 — ✅ 已审(PASS)
|
||||||
|
|
||||||
- **范围**: 3 agent 合并批(workflow wdlt0xfl6,主代独立核查全过)。①**CR-34-1** useAiPanel.ts:102 守卫加有效性检查 `|| !list.some(p=>p.id===activeProvider)`(deleted provider 残留回落 DB is_default) ②**CR-30-1/CR-30-2 + UX-2025-04 合并**:stream_recv.rs `StreamResult` enum(Complete/Partial/InitFailed 三分支) + `classify_status_or_class`(4xx Fatal/5xx+429+timeout Retryable 镜像 retry::is_status_retryable) + agentic.rs 重试对齐决策 a1(Init 失败重试复用 retry::backoff_delay 1s→2s→4s±20%jitter + 30s 总预算 retry_deadline + Fatal 分类 4xx 立即放弃; MidStream Partial 不重试保文→入库 + AiCompleted(incomplete=true) + 系统提示「响应因网络中断不完整」) + 删 3 处误导注释(agentic.rs:12/commands.rs/state.rs) + retry.rs `pub mod` + `backoff_delay`/`is_status_retryable` 改 `pub` 暴露供 src-tauri 复用 + useAiEvents.ts:177 补 `case 'AiStreamRetry'`(消除死事件穿透) + AiCompleted case 处理 incomplete + types.ts:204/mod.rs `incomplete?:boolean` 字段(所有 emit 点补 None 向后兼容) + i18n aiStreamRetry/responseIncomplete 双语。
|
- **范围**: 3 agent 合并批(workflow wdlt0xfl6,主代独立核查全过)。①**CR-34-1** useAiPanel.ts:102 守卫加有效性检查 `|| !list.some(p=>p.id===activeProvider)`(deleted provider 残留回落 DB is_default) ②**CR-30-1/CR-30-2 + UX-2025-04 合并**:stream_recv.rs `StreamResult` enum(Complete/Partial/InitFailed 三分支) + `classify_status_or_class`(4xx Fatal/5xx+429+timeout Retryable 镜像 retry::is_status_retryable) + agentic.rs 重试对齐决策 a1(Init 失败重试复用 retry::backoff_delay 1s→2s→4s±20%jitter + 30s 总预算 retry_deadline + Fatal 分类 4xx 立即放弃; MidStream Partial 不重试保文→入库 + AiCompleted(incomplete=true) + 系统提示「响应因网络中断不完整」) + 删 3 处误导注释(agentic.rs:12/commands.rs/state.rs) + retry.rs `pub mod` + `backoff_delay`/`is_status_retryable` 改 `pub` 暴露供 src-tauri 复用 + useAiEvents.ts:177 补 `case 'AiStreamRetry'`(消除死事件穿透) + AiCompleted case 处理 incomplete + types.ts:204/mod.rs `incomplete?:boolean` 字段(所有 emit 点补 None 向后兼容) + i18n aiStreamRetry/responseIncomplete 双语。
|
||||||
- **维度**: 流式核心中风险(重试循环正确性/保文语义/跨 crate pub 暴露影响面) + 前端事件消费完整性 + 决策 a1 对齐度(Init 可重试 ≤max_retries+jitter+30s+Fatal, MidStream 不重试) + B-260615-05 保留(空文本无 finished 仍 InitFailed 交重试)。
|
- **维度**: 流式核心中风险(重试循环正确性/保文语义/跨 crate pub 暴露影响面) + 前端事件消费完整性 + 决策 a1 对齐度(Init 可重试 ≤max_retries+jitter+30s+Fatal, MidStream 不重试) + B-260615-05 保留(空文本无 finished 仍 InitFailed 交重试)。
|
||||||
- **commit**: 待提交(累积 ~30 文件攒批)。
|
- **commit**: 待提交(累积 ~30 文件攒批)。
|
||||||
|
- **复审结论(2026-06-16·CR-30 agent 亲跑 cargo + 主线程 CR-34-1 预核验)**: ✅ **PASS** — 🔴0 🟡0 ⚪0
|
||||||
|
- **验证**: `cargo check --workspace` EXIT 0(5 warning 全 pre-existing dead_code)/ `cargo test -p df-ai retry` **6 passed**(backoff_is_monotonic_within_jitter/status_classification/fatal_aborts_immediately/first_attempt_ok/retry_once_then_ok/all_retryable_exhausts)/ `cargo test stream_recv` **20 passed**(classify_429_retryable/classify_4xx_fatal 等)。
|
||||||
|
- **CR-30-1(7 项全 PASS)**:agentic.rs:320 真用 `retry::backoff_delay(retry_attempt+1)`(非自写,旧 `1<<retry_attempt` 仅留注释)/ backoff 含 ±20% jitter(retry.rs:74-85 SystemTime nanos)/ 30s 总预算(agentic.rs:242 retry_deadline + :290 budget_exhausted + :321 min 截断)/ 4xx Fatal 立即放弃(agentic.rs:293 `!retryable` return + stream_recv.rs:411-434 classify_status_or_class 镜像 is_status_retryable)/ 三处注释修正(commands.rs:1114 / state.rs:184 / agentic.rs:38)/ DRY 无重写(retry_with_backoff 仍 pub(crate) 流式不走)/ AttemptOutcome+is_reqwest_error_retryable 暴露合理。
|
||||||
|
- **CR-30-2(7 项全 PASS)**:useAiEvents.ts:177-186 `case 'AiStreamRetry'` 完整(取最后 isError 消息 + i18n t 更新「重试 n/m」,break 非穿透)/ 看门狗 reset 正确(AiStreamRetry 不在 NO_RESET_WATCHDOG Set)/ MidStream 决策 agentic.rs:274-285 Partial→incomplete=true + break 不重试,:286-342 InitFailed retryable 分支重试 / currentText 清空(useAiEvents.ts:316 AiError case `state.currentText=''` 再 AiStreamRetry 更新气泡,无累积重复)/ AiCompleted incomplete 系统提示(后端 agentic.rs:384 push session.messages 落库 + 前端 useAiEvents.ts:278-285 气泡)/ types.ts:215-217 variant + :202-204 incomplete?:boolean / i18n aiStreamRetry+responseIncomplete 双语齐。
|
||||||
|
- **回归(4 项无回归)**:permit RAII 释放(agentic.rs:405-406 显式 drop + loop 作用域 Drop)/ guard.reset 耗尽复位(8 exit 点全覆盖 :137/:300/:314/:352/:393/:457/:480/:543)/ request 每轮重建(:251-266 messages 从 session lock 重构)/ 无新死代码(AttemptOutcome/is_reqwest_error_retryable/is_status_retryable/backoff_delay/AiStreamRetry/classify_status_or_class 全有消费)。
|
||||||
|
- **CR-34-1(主线程预核验 PASS)**:useAiPanel.ts:102 守卫 `|| !list.some(p=>p.id===activeProvider)` 残留无效回落 DB is_default,cycleProvider 不受影响(findIndex 不再 -1 跳过 ps[0])。
|
||||||
|
- **UX-2025-04 断线保文**:核心 ✅(MidStream Partial 保文入库 + incomplete 系统提示);「继续生成」按钮未做(可复用 UX-2025-03 重试能力按需补,非阻断)。
|
||||||
|
- **待修项回流 todo**: **无**(CR-34-1/30-1/30-2 三条确认修复,todo 勾完成)。
|
||||||
- **主代独立核查**: ✅ 全过(useAiPanel:102 守卫 / stream_recv:97 enum 三分支 + 注释 / agentic:242 retry_deadline + :274 Partial 不重试 + :286 InitFailed 重试 + :320 backoff_delay 复用 + :360 if incomplete 保文入库 + :400 emit incomplete:Some(true) / useAiEvents:177 AiStreamRetry case / types:204 incomplete / retry.rs:62+74 pub + agentic:14 `use df_ai::retry` 跨 crate 通)。
|
- **主代独立核查**: ✅ 全过(useAiPanel:102 守卫 / stream_recv:97 enum 三分支 + 注释 / agentic:242 retry_deadline + :274 Partial 不重试 + :286 InitFailed 重试 + :320 backoff_delay 复用 + :360 if incomplete 保文入库 + :400 emit incomplete:Some(true) / useAiEvents:177 AiStreamRetry case / types:204 incomplete / retry.rs:62+74 pub + agentic:14 `use df_ai::retry` 跨 crate 通)。
|
||||||
- **审查 agent 待复审重点**: ①stream_recv StreamResult 三分支边界(Partial vs InitFailed 空文本路径,B-260615-05 保留点) ②agentic 重试循环 permit RAII 释放/边界(max_retries=0/guard.reset) ③跨 crate retry pub 暴露是否过度(retry_with_backoff 仍 pub(crate)) ④AiCompleted incomplete 前端消费完整性(系统提示气泡镜像后端) ⑤UX-2025-04「继续生成」按钮**未做**(保文核心 ✅,按钮可复用 UX-2025-03 重试能力或按需补)。
|
- **审查 agent 待复审重点**: ①stream_recv StreamResult 三分支边界(Partial vs InitFailed 空文本路径,B-260615-05 保留点) ②agentic 重试循环 permit RAII 释放/边界(max_retries=0/guard.reset) ③跨 crate retry pub 暴露是否过度(retry_with_backoff 仍 pub(crate)) ④AiCompleted incomplete 前端消费完整性(系统提示气泡镜像后端) ⑤UX-2025-04「继续生成」按钮**未做**(保文核心 ✅,按钮可复用 UX-2025-03 重试能力或按需补)。
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
llm_concurrency: LlmConcurrency,
|
llm_concurrency: LlmConcurrency,
|
||||||
max_iterations: usize,
|
max_iterations: usize,
|
||||||
max_retries: usize,
|
max_retries: usize,
|
||||||
|
start_iteration: usize,
|
||||||
) {
|
) {
|
||||||
// B-260615-09: generating 状态由 RAII guard 收敛复位(正常 exit 显式 reset;panic/异常 Drop 兜底)
|
// B-260615-09: generating 状态由 RAII guard 收敛复位(正常 exit 显式 reset;panic/异常 Drop 兜底)
|
||||||
let mut guard = GeneratingGuard::new(session_arc.clone());
|
let mut guard = GeneratingGuard::new(session_arc.clone());
|
||||||
@@ -168,7 +169,11 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
// 区分"正常收敛退出"与"达 MAX 被截断退出"——后者末轮 tool_calls 仍非空(tool_result 不再回传 LLM),属异常
|
// 区分"正常收敛退出"与"达 MAX 被截断退出"——后者末轮 tool_calls 仍非空(tool_result 不再回传 LLM),属异常
|
||||||
let mut converged = false;
|
let mut converged = false;
|
||||||
|
|
||||||
for iteration in 0..max_iterations {
|
// F-260616-13: system_prompt 是 run_agentic_loop 的不变参数(整个 loop 期间文本不变),
|
||||||
|
// 其 token 估算在 loop 外算一次缓存复用,避免每轮/每次重试重复 estimate_text(低收益优化,行为不变)。
|
||||||
|
let sys_tokens = TokenEstimator::default().estimate_text(&system_prompt);
|
||||||
|
|
||||||
|
for iteration in start_iteration..max_iterations {
|
||||||
// 用户请求停止 → 收尾退出(已生成文本已在上一轮入库)
|
// 用户请求停止 → 收尾退出(已生成文本已在上一轮入库)
|
||||||
if stop_flag.load(Ordering::SeqCst) {
|
if stop_flag.load(Ordering::SeqCst) {
|
||||||
let usage = df_ai::provider::TokenUsage {
|
let usage = df_ai::provider::TokenUsage {
|
||||||
@@ -190,7 +195,7 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
// 用户新建/切换对话后 active_conversation_id 变更,本 loop(conv_id 快照)成陈旧,
|
// 用户新建/切换对话后 active_conversation_id 变更,本 loop(conv_id 快照)成陈旧,
|
||||||
// 继续跑会往新对话 push 消息/pending 造成污染。检测到即退出(guard Drop 复位 generating)。
|
// 继续跑会往新对话 push 消息/pending 造成污染。检测到即退出(guard Drop 复位 generating)。
|
||||||
{
|
{
|
||||||
let session = session_arc.lock().await;
|
let mut session = session_arc.lock().await;
|
||||||
if session.active_conversation_id.as_deref() != Some(conv_id.as_str()) {
|
if session.active_conversation_id.as_deref() != Some(conv_id.as_str()) {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
stale_conv = %conv_id,
|
stale_conv = %conv_id,
|
||||||
@@ -199,6 +204,10 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// F-260616-11 决策 a: 累计 iteration 计数(「下一轮起算值」= 当前轮+1)。
|
||||||
|
// 审批等待/达 max 暂停退出时,本字段即「当前轮+1」;审批续跑 ai_approve 读此值作
|
||||||
|
// start_iteration 透传,实现 iteration 累计不重置(防多次审批反复跑满 max 致 token 失控)。
|
||||||
|
session.iteration_used = iteration + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新一轮通知前端(第二轮起),前端需新建 assistant 消息
|
// 新一轮通知前端(第二轮起),前端需新建 assistant 消息
|
||||||
@@ -210,9 +219,10 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 构建请求消息(超预算时自动裁剪旧消息,保护工具调用三元组 + 最近 6 条)
|
// 构建请求消息(超预算时自动裁剪旧消息,保护工具调用三元组 + 最近 6 条)
|
||||||
|
// F-260616-13: sys_tokens 已在 loop 外缓存;本块构建的 messages 在本轮重试循环中复用
|
||||||
|
// (本轮 stream_llm 不持 session_arc、不改 messages,重试无 push 发生,重建等价于复用)。
|
||||||
let messages = {
|
let messages = {
|
||||||
let session = session_arc.lock().await;
|
let session = session_arc.lock().await;
|
||||||
let sys_tokens = TokenEstimator::default().estimate_text(&system_prompt);
|
|
||||||
let (history_msgs, _trimmed) = session.messages.build_for_request(sys_tokens);
|
let (history_msgs, _trimmed) = session.messages.build_for_request(sys_tokens);
|
||||||
let mut msgs = vec![ChatMessage::system(&system_prompt)];
|
let mut msgs = vec![ChatMessage::system(&system_prompt)];
|
||||||
msgs.extend(history_msgs);
|
msgs.extend(history_msgs);
|
||||||
@@ -248,16 +258,15 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
|
|
||||||
for retry_attempt in 0..=max_retries {
|
for retry_attempt in 0..=max_retries {
|
||||||
// 每次重试重建 request(CompletionRequest 无状态,但 provider.stream() 内部可能消费 body)
|
// 每次重试重建 request(CompletionRequest 无状态,但 provider.stream() 内部可能消费 body)
|
||||||
|
//
|
||||||
|
// F-260616-13: messages 复用本轮外层构建的快照(不再持 session_arc.lock() 重建)。
|
||||||
|
// 安全前提:本轮 stream_llm 不接收 session_arc、不 push 消息;重试期间 tool 执行
|
||||||
|
// (process_tool_calls)在重试块之后,故重试内 session.messages 必然与外层构建时
|
||||||
|
// 一致,重建与复用等价。收益:省去每次重试的 lock() + build_for_request(含
|
||||||
|
// all_messages_clone 全量 clone)。
|
||||||
let retry_request = CompletionRequest {
|
let retry_request = CompletionRequest {
|
||||||
model: provider_config.default_model.clone(),
|
model: provider_config.default_model.clone(),
|
||||||
messages: {
|
messages: messages.clone(),
|
||||||
let session = session_arc.lock().await;
|
|
||||||
let sys_tokens = TokenEstimator::default().estimate_text(&system_prompt);
|
|
||||||
let (history_msgs, _trimmed) = session.messages.build_for_request(sys_tokens);
|
|
||||||
let mut msgs = vec![ChatMessage::system(&system_prompt)];
|
|
||||||
msgs.extend(history_msgs);
|
|
||||||
msgs
|
|
||||||
},
|
|
||||||
temperature: Some(0.7),
|
temperature: Some(0.7),
|
||||||
max_tokens: Some(8192),
|
max_tokens: Some(8192),
|
||||||
stream: true,
|
stream: true,
|
||||||
@@ -487,8 +496,9 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
// 达 MAX 未收敛(LLM 末轮仍想调工具被截断,末轮 tool_result 不再回传 LLM):转入暂停态询问用户
|
// 达 MAX 未收敛(LLM 末轮仍想调工具被截断,末轮 tool_result 不再回传 LLM):转入暂停态询问用户
|
||||||
// F-260616-03:不再 emit AiError + 走完成流程,改为 emit AiMaxRoundsReached + 保持 generating=true
|
// F-260616-03:不再 emit AiError + 走完成流程,改为 emit AiMaxRoundsReached + 保持 generating=true
|
||||||
// (仿审批等待 L313-316),等用户点继续(ai_continue_loop → try_continue_agent_loop 再跑 max_iterations 轮)
|
// (仿审批等待 L313-316),等用户点继续(ai_continue_loop → try_continue_agent_loop 再跑 max_iterations 轮)
|
||||||
// 或点停止(ai_stop_loop → 走完成流程)。try_continue 重新 spawn run_agentic_loop,iteration 从 0 重计,
|
// 或点停止(ai_stop_loop → 走完成流程)。try_continue 续跑 iteration 由调用方传 start_iteration 决定:
|
||||||
// 故续跑天然再跑 max_iterations 轮(决策 a),无需 reset 任何计数器。
|
// 审批续跑累计(F-260616-11 决策 a,防多次审批反复跑满 max 致 token 失控,传 session.iteration_used),
|
||||||
|
// 达 max 续跑重计(F-260616-03 决策 a,用户点继续=授权重来,传 0 + 重置 iteration_used)。
|
||||||
if !converged {
|
if !converged {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
conv_id = %conv_id,
|
conv_id = %conv_id,
|
||||||
@@ -565,7 +575,7 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
/// ai_chat_stop/clear/switch 并发改写,属竞态耦合。has_pending=false(全部审批已处理,续生成)
|
/// ai_chat_stop/clear/switch 并发改写,属竞态耦合。has_pending=false(全部审批已处理,续生成)
|
||||||
/// 分支:审批已被 remove,改为以剩余 pending_approvals 任一 conversation_id 做一致性校验
|
/// 分支:审批已被 remove,改为以剩余 pending_approvals 任一 conversation_id 做一致性校验
|
||||||
/// (此处空,校验通过即沿用全局值,该期 generating=true 且 switch 为 readonly 不并发)。
|
/// (此处空,校验通过即沿用全局值,该期 generating=true 且 switch 为 readonly 不并发)。
|
||||||
pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState) {
|
pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState, start_iteration: usize) {
|
||||||
let (is_generating, has_pending, pending_conv_id) = {
|
let (is_generating, has_pending, pending_conv_id) = {
|
||||||
let session = state.ai_session.lock().await;
|
let session = state.ai_session.lock().await;
|
||||||
// pending_approvals 中任一审批的 conversation_id:审批等待态(has_pending)下作为 conv_id 来源,
|
// pending_approvals 中任一审批的 conversation_id:审批等待态(has_pending)下作为 conv_id 来源,
|
||||||
@@ -662,6 +672,6 @@ pub(crate) async fn try_continue_agent_loop(app: &AppHandle, state: &AppState) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries).await;
|
run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries, start_iteration).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use crate::state::AppState;
|
|||||||
use crate::commands::{err_str, now_millis};
|
use crate::commands::{err_str, now_millis};
|
||||||
|
|
||||||
use super::{AiChatEvent, AiSession, PendingApproval, ToolCallDraft};
|
use super::{AiChatEvent, AiSession, PendingApproval, ToolCallDraft};
|
||||||
|
use super::tool_registry::generate_diff;
|
||||||
|
|
||||||
/// RiskLevel → 审计记录字符串(low/medium/high)
|
/// RiskLevel → 审计记录字符串(low/medium/high)
|
||||||
pub(crate) fn risk_str(r: RiskLevel) -> &'static str {
|
pub(crate) fn risk_str(r: RiskLevel) -> &'static str {
|
||||||
@@ -274,6 +275,11 @@ pub async fn restore_pending_approvals(state: &AppState) {
|
|||||||
risk_level: risk,
|
risk_level: risk,
|
||||||
conversation_id: rec.conversation_id,
|
conversation_id: rec.conversation_id,
|
||||||
recovered: true,
|
recovered: true,
|
||||||
|
// AE-2025-03: 重启恢复的审批不重读旧文件——审批可能跨重启,
|
||||||
|
// 期间文件可能已被外部改动,重读生成 diff 反映的不是当初决策时的状态,
|
||||||
|
// 且恢复路径在 session.lock 内做 async IO 复杂度高,预览价值低。
|
||||||
|
// 前端见 diff=None 时回退显新 content。
|
||||||
|
diff: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -491,6 +497,28 @@ fn sort_object_keys(v: &mut serde_json::Value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// AE-2025-03(路径 B):write_file 审批预览 diff 生成。
|
||||||
|
///
|
||||||
|
/// 从 write_file args 取 path(旧文件路径)+ content(新内容),
|
||||||
|
/// 预读旧文件 → 复用 `generate_diff`(F-260615-10 LCS 行级 diff)注入审批事件。
|
||||||
|
/// 旧文件不存在(新建)/ 读失败 / args 缺字段 → None(前端回退显新 content)。
|
||||||
|
///
|
||||||
|
/// **仅读不改**:审批未通过前不动文件;读路径不校验(write_file handler 自身会校验
|
||||||
|
/// validate_path,审批拒绝则 handler 不执行,恶意路径读失败仅得到 None 不产生危害)。
|
||||||
|
async fn build_write_file_diff(args: &serde_json::Value) -> Option<String> {
|
||||||
|
let path = args.get("path")?.as_str()?;
|
||||||
|
let new_content = args.get("content")?.as_str()?;
|
||||||
|
match tokio::fs::read_to_string(path).await {
|
||||||
|
Ok(old_content) => {
|
||||||
|
if old_content == new_content {
|
||||||
|
return None; // 无变化不生成 diff(理论上 write_file 不会如此,容错)
|
||||||
|
}
|
||||||
|
Some(generate_diff(&old_content, new_content))
|
||||||
|
}
|
||||||
|
Err(_) => None, // 文件不存在(新建)/ 无读权限 → 无 diff,前端显新 content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 处理流式接收的工具调用:Low 风险并行执行(join_all),Med/High 进审批门控
|
/// 处理流式接收的工具调用:Low 风险并行执行(join_all),Med/High 进审批门控
|
||||||
/// 返回待审批的工具数量(0 = 全部自动执行完成)
|
/// 返回待审批的工具数量(0 = 全部自动执行完成)
|
||||||
pub(crate) async fn process_tool_calls(
|
pub(crate) async fn process_tool_calls(
|
||||||
@@ -553,6 +581,15 @@ pub(crate) async fn process_tool_calls(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pending_count += 1;
|
pending_count += 1;
|
||||||
|
// AE-2025-03(路径 B):write_file 挂起审批前预读旧文件生成 diff。
|
||||||
|
// 仅 write_file(覆盖整文件,有完整新旧内容可对比);其他工具 diff=None。
|
||||||
|
// 旧文件不存在(新建)→ diff=None,前端回退显新 content。
|
||||||
|
// 读失败不阻断审批(容错:文件无读权限等极端情况降级为无 diff 预览)。
|
||||||
|
let approval_diff: Option<String> = if draft.name == "write_file" {
|
||||||
|
build_write_file_diff(&args).await
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
session.pending_approvals.insert(draft.id.clone(), PendingApproval {
|
session.pending_approvals.insert(draft.id.clone(), PendingApproval {
|
||||||
tool_call_id: draft.id.clone(),
|
tool_call_id: draft.id.clone(),
|
||||||
tool_name: draft.name.clone(),
|
tool_name: draft.name.clone(),
|
||||||
@@ -560,6 +597,7 @@ pub(crate) async fn process_tool_calls(
|
|||||||
risk_level,
|
risk_level,
|
||||||
conversation_id: Some(conv_id.to_string()),
|
conversation_id: Some(conv_id.to_string()),
|
||||||
recovered: false,
|
recovered: false,
|
||||||
|
diff: approval_diff.clone(),
|
||||||
});
|
});
|
||||||
session.messages.push(ChatMessage::tool_result(&draft.id, "需要用户审批,等待确认"));
|
session.messages.push(ChatMessage::tool_result(&draft.id, "需要用户审批,等待确认"));
|
||||||
let reason = build_approval_reason(&draft.name, &args, risk_level, db).await;
|
let reason = build_approval_reason(&draft.name, &args, risk_level, db).await;
|
||||||
@@ -568,6 +606,7 @@ pub(crate) async fn process_tool_calls(
|
|||||||
name: draft.name.clone(),
|
name: draft.name.clone(),
|
||||||
args: args.clone(),
|
args: args.clone(),
|
||||||
reason,
|
reason,
|
||||||
|
diff: approval_diff,
|
||||||
conversation_id: Some(conv_id.to_string()),
|
conversation_id: Some(conv_id.to_string()),
|
||||||
});
|
});
|
||||||
audit_tool_call(&audit_repo, conv_id, &draft.id, &draft.name, &draft.args, "pending", risk_level, None, None).await;
|
audit_tool_call(&audit_repo, conv_id, &draft.id, &draft.name, &draft.args, "pending", risk_level, None, None).await;
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ pub async fn ai_regenerate(
|
|||||||
session.generating = true;
|
session.generating = true;
|
||||||
session.stop_flag.store(false, Ordering::SeqCst);
|
session.stop_flag.store(false, Ordering::SeqCst);
|
||||||
session.agent_language = language.clone();
|
session.agent_language = language.clone();
|
||||||
|
// F-260616-11: 重生成 = 新生命周期起点,iteration 从头计数。
|
||||||
|
session.iteration_used = 0;
|
||||||
let popped = session.messages.pop_last_assistant_round();
|
let popped = session.messages.pop_last_assistant_round();
|
||||||
if !popped {
|
if !popped {
|
||||||
// 历史末尾无 AI 回复可弹(空对话/末尾是 user 错误态等),复位 generating 报错
|
// 历史末尾无 AI 回复可弹(空对话/末尾是 user 错误态等),复位 generating 报错
|
||||||
@@ -105,7 +107,7 @@ pub async fn ai_regenerate(
|
|||||||
let max_retries = state.agent_max_retries.load(Ordering::SeqCst);
|
let max_retries = state.agent_max_retries.load(Ordering::SeqCst);
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries).await;
|
run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries, 0).await;
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok("ok".to_string())
|
Ok("ok".to_string())
|
||||||
@@ -146,6 +148,8 @@ pub async fn ai_chat_send(
|
|||||||
session.generating = true;
|
session.generating = true;
|
||||||
session.stop_flag.store(false, Ordering::SeqCst);
|
session.stop_flag.store(false, Ordering::SeqCst);
|
||||||
session.agent_language = language.clone();
|
session.agent_language = language.clone();
|
||||||
|
// F-260616-11: 新对话生命周期 iteration 从头计数(累计计数器复位)。
|
||||||
|
session.iteration_used = 0;
|
||||||
session.messages.push(ChatMessage::user(&message));
|
session.messages.push(ChatMessage::user(&message));
|
||||||
|
|
||||||
// 首次发送时生成对话 id(懒创建:不立即落库,避免空对话残留;
|
// 首次发送时生成对话 id(懒创建:不立即落库,避免空对话残留;
|
||||||
@@ -201,7 +205,7 @@ pub async fn ai_chat_send(
|
|||||||
let max_retries = state.agent_max_retries.load(Ordering::SeqCst);
|
let max_retries = state.agent_max_retries.load(Ordering::SeqCst);
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries).await;
|
run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id, knowledge_config, llm_concurrency, max_iterations, max_retries, 0).await;
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok("ok".to_string())
|
Ok("ok".to_string())
|
||||||
@@ -251,8 +255,11 @@ pub async fn ai_approve(
|
|||||||
}
|
}
|
||||||
// 审计:拒绝(决策者=human)
|
// 审计:拒绝(决策者=human)
|
||||||
audit_finalize(&state, &tool_call_id, "rejected", None).await;
|
audit_finalize(&state, &tool_call_id, "rejected", None).await;
|
||||||
|
// F-260616-11 决策 a: 审批续跑 iteration 累计(不重置)——读 session.iteration_used 透传
|
||||||
|
// 作 start_iteration,防多次审批反复跑满 max_iterations 致 token 失控。
|
||||||
|
let start_iter = state.ai_session.lock().await.iteration_used;
|
||||||
// 所有待审批处理完毕后恢复 agentic 循环
|
// 所有待审批处理完毕后恢复 agentic 循环
|
||||||
try_continue_agent_loop(&app, &state).await;
|
try_continue_agent_loop(&app, &state, start_iter).await;
|
||||||
return Ok("rejected".to_string());
|
return Ok("rejected".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,8 +308,11 @@ pub async fn ai_approve(
|
|||||||
save_conversation(&state.ai_session, &state.db, cid, None, None).await;
|
save_conversation(&state.ai_session, &state.db, cid, None, None).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// F-260616-11 决策 a: 审批续跑 iteration 累计(不重置)——读 session.iteration_used 透传
|
||||||
|
// 作 start_iteration,防多次审批反复跑满 max_iterations 致 token 失控。
|
||||||
|
let start_iter = state.ai_session.lock().await.iteration_used;
|
||||||
// 所有待审批处理完毕后恢复 agentic 循环(recovered 无 live loop,try_continue 因 generating=false 自然不续)
|
// 所有待审批处理完毕后恢复 agentic 循环(recovered 无 live loop,try_continue 因 generating=false 自然不续)
|
||||||
try_continue_agent_loop(&app, &state).await;
|
try_continue_agent_loop(&app, &state, start_iter).await;
|
||||||
|
|
||||||
Ok("executed".to_string())
|
Ok("executed".to_string())
|
||||||
}
|
}
|
||||||
@@ -398,6 +408,8 @@ pub async fn ai_chat_edit(
|
|||||||
session.generating = true;
|
session.generating = true;
|
||||||
session.stop_flag.store(false, Ordering::SeqCst);
|
session.stop_flag.store(false, Ordering::SeqCst);
|
||||||
session.agent_language = language.clone();
|
session.agent_language = language.clone();
|
||||||
|
// F-260616-11: 编辑重生成 = 新生命周期起点,iteration 从头计数。
|
||||||
|
session.iteration_used = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let _tool_defs = state.ai_tools.tool_definitions();
|
let _tool_defs = state.ai_tools.tool_definitions();
|
||||||
@@ -453,6 +465,7 @@ pub async fn ai_chat_edit(
|
|||||||
llm_concurrency,
|
llm_concurrency,
|
||||||
max_iterations,
|
max_iterations,
|
||||||
max_retries,
|
max_retries,
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
});
|
});
|
||||||
@@ -560,8 +573,10 @@ pub async fn ai_chat_stop(state: State<'_, AppState>, app: AppHandle) -> Result<
|
|||||||
///
|
///
|
||||||
/// 场景:run_agentic_loop 达 max_iterations 未收敛 → emit AiMaxRoundsReached + 保持
|
/// 场景:run_agentic_loop 达 max_iterations 未收敛 → emit AiMaxRoundsReached + 保持
|
||||||
/// generating=true 暂停。用户点继续调本命令 → 复位 stop_flag(防上一轮残留致续跑入口即退出)
|
/// generating=true 暂停。用户点继续调本命令 → 复位 stop_flag(防上一轮残留致续跑入口即退出)
|
||||||
/// → 调 try_continue_agent_loop 重新 spawn run_agentic_loop(iteration 从 0 重计,天然再跑
|
/// → 调 try_continue_agent_loop 重新 spawn run_agentic_loop。iteration 从 0 重计(天然再跑
|
||||||
/// max_iterations 轮,决策 a)。
|
/// max_iterations 轮,决策 a),F-260616-11 落地后:重置 session.iteration_used=0 + 传
|
||||||
|
/// start_iteration=0(达 max 续跑重计);审批续跑(ai_approve)则累计传 session.iteration_used,
|
||||||
|
/// 两路径区分见 run_agentic_loop 达 max 分支注释。
|
||||||
///
|
///
|
||||||
/// 校验复用 ai_approve 模式:generating 必须为 true(暂停态)+ active_conversation_id 一致性
|
/// 校验复用 ai_approve 模式:generating 必须为 true(暂停态)+ active_conversation_id 一致性
|
||||||
/// (防陈旧 loop 续跑污染新对话)。无硬上限防无限续(决策 b:用户主动授权 = 同意烧 token)。
|
/// (防陈旧 loop 续跑污染新对话)。无硬上限防无限续(决策 b:用户主动授权 = 同意烧 token)。
|
||||||
@@ -581,9 +596,12 @@ pub async fn ai_continue_loop(
|
|||||||
}
|
}
|
||||||
// 复位停止信号:暂停态可能因上一轮 stop_flag 残留为 true,续跑 loop 入口会立即退出走完成流程
|
// 复位停止信号:暂停态可能因上一轮 stop_flag 残留为 true,续跑 loop 入口会立即退出走完成流程
|
||||||
session.stop_flag.store(false, Ordering::SeqCst);
|
session.stop_flag.store(false, Ordering::SeqCst);
|
||||||
|
// F-260616-11 决策 a: 达 max 续跑重计 iteration(F-260616-03 决策 a,用户点继续=授权重来)。
|
||||||
|
// 审批续跑(ai_approve)累计不重置见另路径;本路径 start_iteration 传 0,loop 从头计数。
|
||||||
|
session.iteration_used = 0;
|
||||||
}
|
}
|
||||||
// 复用审批恢复续 loop 入口(不重写 loop),其内部 spawn run_agentic_loop
|
// 复用审批恢复续 loop 入口(不重写 loop),其内部 spawn run_agentic_loop
|
||||||
try_continue_agent_loop(&app, &state).await;
|
try_continue_agent_loop(&app, &state, 0).await;
|
||||||
Ok("ok".to_string())
|
Ok("ok".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -836,6 +854,11 @@ pub async fn ai_conversation_create(
|
|||||||
session.active_conv_created_at = Some(now);
|
session.active_conv_created_at = Some(now);
|
||||||
session.messages.clear();
|
session.messages.clear();
|
||||||
session.pending_approvals.clear();
|
session.pending_approvals.clear();
|
||||||
|
// F-260616-09(A 路线):补漏清字段维持单例软隔离,解「新建会话上下文残留」。
|
||||||
|
// stop_flag 复位 false:上方生成中分支曾置 true 停旧 loop,不复位则新会话 loop
|
||||||
|
// 启动即见 stop_flag=true 异常退出;agent_language 清空防新会话沿用旧会话语言设置。
|
||||||
|
session.stop_flag.store(false, Ordering::SeqCst);
|
||||||
|
session.agent_language = None;
|
||||||
|
|
||||||
Ok(serde_json::json!({ "id": id }))
|
Ok(serde_json::json!({ "id": id }))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,9 @@ pub enum AiChatEvent {
|
|||||||
/// 工具调用完成
|
/// 工具调用完成
|
||||||
AiToolCallCompleted { id: String, result: serde_json::Value, conversation_id: Option<String> },
|
AiToolCallCompleted { id: String, result: serde_json::Value, conversation_id: Option<String> },
|
||||||
/// 需要人工审批
|
/// 需要人工审批
|
||||||
AiApprovalRequired { id: String, name: String, args: serde_json::Value, reason: String, conversation_id: Option<String> },
|
/// diff(AE-2025-03,路径 B):仅 write_file 注入(旧文件 vs 新内容行级 unified diff),
|
||||||
|
/// 供审批卡即时预览。旧文件不存在(新建)或非 write_file 工具为 None。
|
||||||
|
AiApprovalRequired { id: String, name: String, args: serde_json::Value, reason: String, diff: Option<String>, conversation_id: Option<String> },
|
||||||
/// 审批结果
|
/// 审批结果
|
||||||
AiApprovalResult { id: String, approved: bool, conversation_id: Option<String> },
|
AiApprovalResult { id: String, approved: bool, conversation_id: Option<String> },
|
||||||
/// AI 响应完成
|
/// AI 响应完成
|
||||||
@@ -199,6 +201,18 @@ pub struct AiSession {
|
|||||||
/// `Arc<Notify>` 可 Clone(Notify 内部用 Atomic 计数,Clone 等价于 Arc 引用 +1),
|
/// `Arc<Notify>` 可 Clone(Notify 内部用 Atomic 计数,Clone 等价于 Arc 引用 +1),
|
||||||
/// 不影响 AiSession 其他字段语义。
|
/// 不影响 AiSession 其他字段语义。
|
||||||
pub notify: Arc<tokio::sync::Notify>,
|
pub notify: Arc<tokio::sync::Notify>,
|
||||||
|
/// agentic loop 累计已跑 iteration 计数(F-260616-11 决策 a)。
|
||||||
|
///
|
||||||
|
/// **语义**:「下一轮起算值」。run_agentic_loop 每轮写 `iteration + 1`,审批等待/
|
||||||
|
/// 达 max 暂停退出时即「当前轮+1」。续跑时由调用方决定如何接:
|
||||||
|
/// - **审批续跑**(ai_approve):传本字段作 start_iteration(**累计**,防多次审批反复
|
||||||
|
/// 跑满 max_iterations 致 token 失控);
|
||||||
|
/// - **达 max 续跑**(ai_continue_loop):重置本字段=0 + 传 0(**重计**,F-260616-03
|
||||||
|
/// 决策 a,用户点继续=授权重来);
|
||||||
|
/// - **新消息首次发送**(ai_chat_send):重置本字段=0(新对话生命周期从头计数)。
|
||||||
|
///
|
||||||
|
/// 单例一份:当前 AiSession 是全局单例(F-09 B 多会话架构落地时改 per-conv)。
|
||||||
|
pub iteration_used: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AiSession {
|
impl AiSession {
|
||||||
@@ -213,6 +227,7 @@ impl AiSession {
|
|||||||
agent_language: None,
|
agent_language: None,
|
||||||
stop_flag: Arc::new(AtomicBool::new(false)),
|
stop_flag: Arc::new(AtomicBool::new(false)),
|
||||||
notify: Arc::new(tokio::sync::Notify::new()),
|
notify: Arc::new(tokio::sync::Notify::new()),
|
||||||
|
iteration_used: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,6 +276,10 @@ pub struct PendingApproval {
|
|||||||
pub conversation_id: Option<String>,
|
pub conversation_id: Option<String>,
|
||||||
/// 重启恢复的积压审批:无 live loop 持有 session.messages,审批后不 save(防空 messages 污染老对话)、不续跑
|
/// 重启恢复的积压审批:无 live loop 持有 session.messages,审批后不 save(防空 messages 污染老对话)、不续跑
|
||||||
pub recovered: bool,
|
pub recovered: bool,
|
||||||
|
/// AE-2025-03(路径 B):write_file 的行级 unified diff(旧文件 vs 新内容)。
|
||||||
|
/// 仅挂起审批前预读注入,供前端审批卡预览;旧文件不存在(新建)或非 write_file 为 None。
|
||||||
|
/// recovered 审批(启动重建)无 diff(文件可能已变,重读无意义)。
|
||||||
|
pub diff: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 工具调用草稿(流式收集时的临时结构)
|
/// 工具调用草稿(流式收集时的临时结构)
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ const DEFAULT_RUN_COMMAND_TIMEOUT_SECS: u64 = 60;
|
|||||||
/// 生成行级 unified diff(无外部依赖,基于 LCS)。
|
/// 生成行级 unified diff(无外部依赖,基于 LCS)。
|
||||||
/// 仅标 +/- 前缀,不做 hunk header(足够审批卡/审计留痕可读)。
|
/// 仅标 +/- 前缀,不做 hunk header(足够审批卡/审计留痕可读)。
|
||||||
/// 文件改动通常集中在 old_text/new_text 局部,整体行对比可直观呈现。
|
/// 文件改动通常集中在 old_text/new_text 局部,整体行对比可直观呈现。
|
||||||
fn generate_diff(old: &str, new: &str) -> String {
|
///
|
||||||
|
/// AE-2025-03(路径 B):audit.rs 挂起审批前预读旧文件复用此函数生成 diff,
|
||||||
|
/// 供前端审批卡即时预览(write_file 审批不再只看裸 content)。故 pub(crate)。
|
||||||
|
pub(crate) fn generate_diff(old: &str, new: &str) -> String {
|
||||||
let a: Vec<&str> = old.lines().collect();
|
let a: Vec<&str> = old.lines().collect();
|
||||||
let b: Vec<&str> = new.lines().collect();
|
let b: Vec<&str> = new.lines().collect();
|
||||||
let (n, m) = (a.len(), b.len());
|
let (n, m) = (a.len(), b.len());
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ export type AiChatEvent = ({
|
|||||||
} | {
|
} | {
|
||||||
type: 'AiToolCallCompleted'; id: string; result: unknown
|
type: 'AiToolCallCompleted'; id: string; result: unknown
|
||||||
} | {
|
} | {
|
||||||
type: 'AiApprovalRequired'; id: string; name: string; args: unknown; reason: string
|
type: 'AiApprovalRequired'; id: string; name: string; args: unknown; reason: string; diff?: string
|
||||||
} | {
|
} | {
|
||||||
type: 'AiApprovalResult'; id: string; approved: boolean
|
type: 'AiApprovalResult'; id: string; approved: boolean
|
||||||
} | {
|
} | {
|
||||||
@@ -239,6 +239,9 @@ export interface AiToolCallInfo {
|
|||||||
result?: unknown
|
result?: unknown
|
||||||
/** 审批风险说明(AiApprovalRequired 时填充) */
|
/** 审批风险说明(AiApprovalRequired 时填充) */
|
||||||
reason?: string
|
reason?: string
|
||||||
|
/** AE-2025-03:write_file 行级 unified diff(旧文件 vs 新内容),审批卡即时预览。
|
||||||
|
* 旧文件不存在(新建)或非 write_file 为 undefined,前端回退显新 content。 */
|
||||||
|
diff?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 对话列表摘要 */
|
/** 对话列表摘要 */
|
||||||
|
|||||||
@@ -36,6 +36,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="tc.reason" class="ai-tool-approval-reason">⚠ {{ tc.reason }}</div>
|
<div v-if="tc.reason" class="ai-tool-approval-reason">⚠ {{ tc.reason }}</div>
|
||||||
|
<!-- AE-2025-03: write_file 审批 diff 预览(红删绿增,行级)。旧文件不存在→tc.diff 为空,回退显上方 args content -->
|
||||||
|
<div v-if="tc.diff" class="ai-tool-approval-diff">
|
||||||
|
<pre class="ai-tool-diff-pre"><code><span v-for="(ln, idx) in diffLines" :key="idx" class="ai-tool-diff-line" :class="'ai-tool-diff-line--' + ln.kind">{{ ln.text }}{{ '\n' }}</span></code></pre>
|
||||||
|
</div>
|
||||||
<div class="ai-tool-actions">
|
<div class="ai-tool-actions">
|
||||||
<button class="ai-tool-btn ai-tool-btn--approve"
|
<button class="ai-tool-btn ai-tool-btn--approve"
|
||||||
:disabled="approving"
|
:disabled="approving"
|
||||||
@@ -382,6 +386,23 @@ onBeforeUnmount(() => {
|
|||||||
/** 一次解析结果供模板多次读(模板引用 6 次,computed 避免每次 patch 重 parse) */
|
/** 一次解析结果供模板多次读(模板引用 6 次,computed 避免每次 patch 重 parse) */
|
||||||
const parsed = computed(() => parseResult(props.tc.result))
|
const parsed = computed(() => parseResult(props.tc.result))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AE-2025-03: write_file 审批 diff 行解析。
|
||||||
|
* 后端 generate_diff 输出 '+新行/-旧行/ 上下文行',逐行拆 → {kind, text} 供模板按 kind 着色。
|
||||||
|
* kind: 'add'(+, 绿)/'del'(-, 红)/'ctx'(空格, 灰)。computed 在 diff 变化(新审批)时重算。
|
||||||
|
*/
|
||||||
|
const diffLines = computed(() => {
|
||||||
|
const d = props.tc.diff
|
||||||
|
if (!d) return []
|
||||||
|
return d.split('\n').map(line => {
|
||||||
|
// 注意 generate_diff 末尾保留单尾换行 → split 末尾有空串元素,过滤
|
||||||
|
if (line === '') return null
|
||||||
|
if (line.startsWith('+')) return { kind: 'add', text: line }
|
||||||
|
if (line.startsWith('-')) return { kind: 'del', text: line }
|
||||||
|
return { kind: 'ctx', text: line }
|
||||||
|
}).filter((x): x is { kind: string; text: string } => x !== null)
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按 id 查项目名(覆盖活跃 + 回收站):审批 delete/restore/purge 各阶段都可能引用项目,
|
* 按 id 查项目名(覆盖活跃 + 回收站):审批 delete/restore/purge 各阶段都可能引用项目,
|
||||||
* 故先查 projects 再查 deletedProjects,找到即返回(提前退出),无则返回 undefined(调用方走 fallback)。
|
* 故先查 projects 再查 deletedProjects,找到即返回(提前退出),无则返回 undefined(调用方走 fallback)。
|
||||||
@@ -652,6 +673,37 @@ function toolResultSummary(tc: AiToolCallInfo): string {
|
|||||||
border-radius: var(--df-radius-sm, 4px);
|
border-radius: var(--df-radius-sm, 4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* AE-2025-03: write_file 审批 diff 预览区(红删绿增行级,复用 --df-danger/--df-success token) */
|
||||||
|
.ai-tool-approval-diff {
|
||||||
|
margin: 0 10px 8px;
|
||||||
|
border: 0.5px solid var(--df-border);
|
||||||
|
border-radius: var(--df-radius-sm, 4px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.ai-tool-diff-pre {
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px 0;
|
||||||
|
max-height: 240px;
|
||||||
|
overflow: auto;
|
||||||
|
font-family: var(--df-font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.5;
|
||||||
|
background: var(--df-bg-card);
|
||||||
|
}
|
||||||
|
.ai-tool-diff-pre code {
|
||||||
|
display: block;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
.ai-tool-diff-line {
|
||||||
|
display: block;
|
||||||
|
padding: 0 10px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.ai-tool-diff-line--add { color: var(--df-success, #3ddb9c); background: var(--df-success-bg, rgba(61, 219, 160, 0.08)); }
|
||||||
|
.ai-tool-diff-line--del { color: var(--df-danger, #f06565); background: var(--df-danger-bg, rgba(240, 101, 101, 0.08)); }
|
||||||
|
.ai-tool-diff-line--ctx { color: var(--df-text-dim, #888); }
|
||||||
|
|
||||||
/* -- 审批按钮 -- */
|
/* -- 审批按钮 -- */
|
||||||
.ai-tool-actions {
|
.ai-tool-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ async function newConversation() {
|
|||||||
state.currentText = ''
|
state.currentText = ''
|
||||||
state.pendingApprovals = []
|
state.pendingApprovals = []
|
||||||
state.streaming = false
|
state.streaming = false
|
||||||
|
// F-260616-09(A 路线):补漏清字段维持单例软隔离,解「新建会话上下文残留」。
|
||||||
|
// queue 清空防旧会话排队消息被带进新会话 drain;generatingConvId 清空防后台事件
|
||||||
|
// 错误路由;agentRound 复位 0;searchQuery 清空防新会话侧栏被旧搜索过滤。
|
||||||
|
state.queue = []
|
||||||
|
state.generatingConvId = null
|
||||||
|
state.agentRound = 0
|
||||||
|
state.searchQuery = ''
|
||||||
await loadConversations()
|
await loadConversations()
|
||||||
notifyConversationChanged()
|
notifyConversationChanged()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,6 +235,8 @@ export function handleEvent(event: AiChatEvent) {
|
|||||||
if (tc) {
|
if (tc) {
|
||||||
tc.status = 'pending_approval'
|
tc.status = 'pending_approval'
|
||||||
tc.reason = event.reason
|
tc.reason = event.reason
|
||||||
|
// AE-2025-03: write_file 审批注入行级 diff(旧文件 vs 新内容),前端审批卡预览
|
||||||
|
if (event.diff) tc.diff = event.diff
|
||||||
}
|
}
|
||||||
// B-260616-12: 进入审批等待→取消该工具慢执行计时器(审批耗时由用户主导,非执行慢)
|
// B-260616-12: 进入审批等待→取消该工具慢执行计时器(审批耗时由用户主导,非执行慢)
|
||||||
clearToolSlowTimer(event.id)
|
clearToolSlowTimer(event.id)
|
||||||
|
|||||||
Reference in New Issue
Block a user