优化: 波2销账UX三件套+UX-04+CR-46登记+F-15需求细化

This commit is contained in:
2026-06-17 00:02:22 +08:00
parent 313459b8e4
commit afcdf5e17d
2 changed files with 25 additions and 7 deletions

View File

@@ -98,17 +98,25 @@
- [ ] UX-260616-01 [P2] — **工具调用失败时「重试」提示语义模糊**。用户场景:Run Command 执行失败(exit_code 255,`head` not recognized on Windows),UI 显示「⚠ 调用失败,正在重试(1/4)…」+「重试」按钮。**根因分析**:「正在重试(n/m)」是 `AiStreamRetry` 事件(F-260616-07 流式 LLM 重试),通过 `useAiEvents.ts:177-186` 更新错误气泡内容;工具执行失败(run_command 等 Low 风险工具)走 AR-6 路径(`audit.rs:640-652`)→ emit `AiToolCallCompleted`(result=错误信息)→ **不触发 AiError 不触发 AiStreamRetry**,错误包在 tool_result 回传 LLM 自行决策。**问题**:①用户看到的是「LLM 流重试」提示,非「工具执行失败」提示,语义错位 ②确定性失败(命令语法错/exit_code 非0)重试同命令必再败,「重试」按钮误导 ③工具实际结果(stdout/stderr/exit_code)已显示在 ToolCard 内,错误气泡的「重试」是消息级 regenerate(重新生成整条回复),非工具级重试。**改动方向**(待定):a)错误气泡区分两类——流式重试中显示「正在重试(n/m)…」(现有);工具执行失败不弹错误气泡(结果已在 ToolCard)或气泡文案改为「工具执行失败,查看上方结果」b)Fatal 类错误(4xx/鉴权/参数)气泡去掉「重试」按钮或改为「去设置」c)`AiStreamRetry` 事件更新气泡时附带 `retryable` 标识,前端据此显隐重试按钮。—— useAiEvents.ts(:177-186 AiStreamRetry case) + AiChat.vue(错误气泡 UX-03 操作栏) + i18n ai.aiStreamRetry - [ ] UX-260616-01 [P2] — **工具调用失败时「重试」提示语义模糊**。用户场景:Run Command 执行失败(exit_code 255,`head` not recognized on Windows),UI 显示「⚠ 调用失败,正在重试(1/4)…」+「重试」按钮。**根因分析**:「正在重试(n/m)」是 `AiStreamRetry` 事件(F-260616-07 流式 LLM 重试),通过 `useAiEvents.ts:177-186` 更新错误气泡内容;工具执行失败(run_command 等 Low 风险工具)走 AR-6 路径(`audit.rs:640-652`)→ emit `AiToolCallCompleted`(result=错误信息)→ **不触发 AiError 不触发 AiStreamRetry**,错误包在 tool_result 回传 LLM 自行决策。**问题**:①用户看到的是「LLM 流重试」提示,非「工具执行失败」提示,语义错位 ②确定性失败(命令语法错/exit_code 非0)重试同命令必再败,「重试」按钮误导 ③工具实际结果(stdout/stderr/exit_code)已显示在 ToolCard 内,错误气泡的「重试」是消息级 regenerate(重新生成整条回复),非工具级重试。**改动方向**(待定):a)错误气泡区分两类——流式重试中显示「正在重试(n/m)…」(现有);工具执行失败不弹错误气泡(结果已在 ToolCard)或气泡文案改为「工具执行失败,查看上方结果」b)Fatal 类错误(4xx/鉴权/参数)气泡去掉「重试」按钮或改为「去设置」c)`AiStreamRetry` 事件更新气泡时附带 `retryable` 标识,前端据此显隐重试按钮。—— useAiEvents.ts(:177-186 AiStreamRetry case) + AiChat.vue(错误气泡 UX-03 操作栏) + i18n ai.aiStreamRetry
- [ ] UX-260616-02 [P3] — **「全部收起」与搜索/技能区合并一行**。当前 ToolCardList.vue 有两个独立行:①batch-approve 栏(line 4-11,pendingCount>0 时显示)②global-toggle 栏(line 14-17,collapsibleGroupCount>0 时显示「▾ 全部收起」)。用户要求将「全部收起」与附近的操作元素(search files 搜索文件/技能触发等)放到同一行,减少垂直空间占用。**需确认**:「search files」具体指哪个 UI 元素(i18n `ai.searchFiles` 渲染位置需定位,可能在输入框上方 skill 栏或工具卡区域)。**改动方向**:global-toggle 从独占行改为 inline 元素,与相邻操作栏 flex 同行。—— ToolCardList.vue(:13-17 template + :278-295 CSS .ai-tool-global-toggle) - [ ] UX-260616-02 [P3] — **「全部收起」与搜索/技能区合并一行**。当前 ToolCardList.vue 有两个独立行:①batch-approve 栏(line 4-11,pendingCount>0 时显示)②global-toggle 栏(line 14-17,collapsibleGroupCount>0 时显示「▾ 全部收起」)。用户要求将「全部收起」与附近的操作元素(search files 搜索文件/技能触发等)放到同一行,减少垂直空间占用。**需确认**:「search files」具体指哪个 UI 元素(i18n `ai.searchFiles` 渲染位置需定位,可能在输入框上方 skill 栏或工具卡区域)。**改动方向**:global-toggle 从独占行改为 inline 元素,与相邻操作栏 flex 同行。—— ToolCardList.vue(:13-17 template + :278-295 CSS .ai-tool-global-toggle)
- [ ] UX-260616-03 [P2] — **对话内容输出时自动收起旧工具卡片分组**。需求:当 AI 输出新内容(流式 delta / 新工具调用 / 新轮次)滚动到下方时,上方已完成的旧消息中的工具卡片分组自动收起,保持视野聚焦当前内容。**现状**:机制已存在——`ToolCardList.collapseInactive(activeIds)`(:191-199)供父组件调用,`AiChat.vue:1657-1661` 已有 watch 调用(refs 数组逐实例 collapseInactive)。**增强方向**:a)触发时机扩展——当前可能仅在特定时机调用,可扩展到:`AiTextDelta` 新消息开始时 + `AiAgentRound` 新轮次时 + 用户滚动接近底部时(跟随阅读位置自动收起已读内容)b)平滑过渡——收起加 CSS transition(高度动画 200ms)避免内容突然消失跳变c)可选:记忆用户手动展开的分组不自动收起(expandedCards Set 区分用户主动展开 vs 默认态)。—— ToolCardList.vue(collapseInactive + CSS transition) + AiChat.vue(watch 触发时机扩展) - [ ] UX-260616-03 [P2] — **对话内容输出时自动收起旧工具卡片分组**。需求:当 AI 输出新内容(流式 delta / 新工具调用 / 新轮次)滚动到下方时,上方已完成的旧消息中的工具卡片分组自动收起,保持视野聚焦当前内容。**现状**:机制已存在——`ToolCardList.collapseInactive(activeIds)`(:191-199)供父组件调用,`AiChat.vue:1657-1661` 已有 watch 调用(refs 数组逐实例 collapseInactive)。**增强方向**:a)触发时机扩展——当前可能仅在特定时机调用,可扩展到:`AiTextDelta` 新消息开始时 + `AiAgentRound` 新轮次时 + 用户滚动接近底部时(跟随阅读位置自动收起已读内容)b)平滑过渡——收起加 CSS transition(高度动画 200ms)避免内容突然消失跳变c)可选:记忆用户手动展开的分组不自动收起(expandedCards Set 区分用户主动展开 vs 默认态)。—— ToolCardList.vue(collapseInactive + CSS transition) + AiChat.vue(watch 触发时机扩展)
- [ ] 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] ✅(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)
- [ ] 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) 逐条续发**(删清队列即可,零改 drainQueueAiCompleted 链式触发每条保各条上下文独立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,决策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) 逐条续发**(删清队列即可,零改 drainQueueAiCompleted 链式触发每条保各条上下文独立vs **(b) 合并成一条消息**(队列多条 text 拼接送,需改 drainQueue 为 bulkDrain 或合并 text 调一次 sendMessage。**倾向 (a)**(逐条保上下文独立+零改链路;"一并"理解为「都送出不丢弃」非字面合并),**待用户确认**。— src/composables/ai/useAiSend.ts(stopChat:365 + drainQueue:228) + 后端 agentic.rs(:178/:190 stop 收尾 emit AiCompleted 已就绪)。
- [ ] 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,决策只编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。
- [ ] 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 新增)。
- [ ] F-260616-15 [P1] — **AI Chat 上下文管理增强:会话分段 + 手动压缩 + 智能裁剪**。三个子需求,详见 [F-15-上下文管理增强设计-2026-06-16.md](./02-架构设计/F-15-上下文管理增强设计-2026-06-16.md)。**2026-06-16 多角度分析完成 + 设计文档定稿**。 - [ ] F-260616-15 [P1] — **AI Chat 上下文管理增强:会话分段 + 手动压缩 + 智能裁剪**。三个子需求,详见 [F-15-上下文管理增强设计-2026-06-16.md](./02-架构设计/F-15-上下文管理增强设计-2026-06-16.md)。**2026-06-16 多角度分析完成 + 设计文档定稿**。
**① 会话分段(清理上下文,保留记录)**:用户点「清理上下文」后,不清删 DB 消息,而是给当前消息打标记(`status="archived_segment"`),后续新消息作为新段继续同一对话。同一对话内产生 a'/a''/a''' 多段DB 全量保留可回溯LLM 上下文只看当前段。**现状**`ai_chat_clear`(commands.rs:360) 直接 `clear_messages``messages='[]'`(全删)。`ChatMessage.status`(df-ai-core/provider.rs:59) 已有 `None/"active"/"truncated"` 三态,`is_active()` 过滤 truncated。`ContextManager.build_for_request`(context.rs:219) 发送前已调 `sanitize_messages` 过滤 `!is_active()`。**改动方向**a) 新增 status 值 `"archived_segment"`(语义=被清理上下文标记,不进 LLM 上下文但前端可见可展开b) `is_active()` 扩展过滤 archived_segment不进 build_for_requestc) 新 IPC `ai_chat_clear_context`(区别于 `ai_chat_clear` 全删):给当前所有 active 消息标记 `status="archived_segment"` + 插入一条 system 消息「--- 上下文已清理 ---」做分隔线 + ContextManager 清内存只保留分隔线后的消息d) 前端:清空按钮拆为「清空对话」(全删,现有)+「清理上下文」分段标记e) 前端消息列表渲染 archived_segment 段为折叠的灰色分隔区点击展开看历史段active 段正常展示。**关键约束**:工具调用三元组不能跨段拆散(整组标记) **核心问题**:① `ai_chat_clear` 全删 DB 消息不可回溯 ② `build_for_request` 超预算直接丢弃旧消息零保留(`agentic.rs:226` `_trimmed` 被忽略) ③ 无压缩能力
**② 手动上下文压缩LLM 摘要)**:用户点「压缩上下文」后,把当前段内所有 active 消息发给 LLM 生成摘要,摘要替换原始消息作为新的上下文起点。**现状**:无压缩能力,`ContextManager` 超预算时直接丢弃旧消息(无摘要保留语义)。`LlmProvider::complete()`(df-ai-core/provider.rs:210) 可用于非流式摘要调用。**改动方向**a) 新 IPC `ai_chat_compress_context`:取当前 active 消息 → 构建 summary prompt「请将以下对话总结为关键上下文摘要保留用户意图/关键决策/已修改文件/重要约束」)→ `provider.complete()` 调用 → 摘要作为 system 消息替换原始消息(原始消息标记 `status="compressed"` 保留 DBb) ContextManager 清内存只保留摘要 system + 后续新消息c) 前端:压缩按钮 + loading 态 + 压缩后显示摘要卡片可展开看被压缩的原始消息d) 摘要 prompt 模板放 prompt.rs 可配。**关键约束**:压缩是单向的(不能"解压缩"恢复 LLM 上下文,但 DB 原始消息保留可查看);压缩后 token 计数重置 **统一方案**`ChatMessage.status` 扩展两个值 `archived_segment`(分段标记)/`compressed`(压缩标记)`is_active()` 改正面白名单 `matches!(status, None | Some("active"))`(零行为变化,新状态自动不 active`sanitize_messages` step 0 已是唯一发送过滤点改 is_active 即隔离旧段/压缩段。`push()`(context.rs:174) 不计 `!is_active()` 消息 token当前全量计入致 restore 后 token 虚高误判超预算)
**涉及文件**`df-ai-core/src/provider.rs`(ChatMessage.status 扩展) + `df-ai/src/context.rs`(build_for_request 过滤) + `src-tauri/src/commands/ai/commands.rs`(2 新 IPC) + `src-tauri/src/commands/ai/prompt.rs`(压缩 prompt 模板) + `src/api/ai.ts` + `src/composables/ai/useAiPanel.ts` + `src/components/AiChat.vue`(按钮 UI + 分段渲染) + `src/i18n/{zh-CN,en}/aiChat.ts` **① 会话分段**IPC `ai_chat_clear_context`:当前 active 消息标 `archived_segment` + 扣除 history_tokens + 落库。不插分隔线 system前端按 status 渲染折叠分隔条)。三元组原子标记(复用 `build_eviction_units` 分组)。
**② 手动压缩**IPC `ai_chat_compress_context`:取 active 消息 → `compress_prompt` 四段式(意图/决策/文件/约束) → `provider.complete()` 非流式调用(复用 `title.rs:111` `generate_title_via_llm` 模式 + `build_provider_for`) → 原始消息标 `compressed` + 插入摘要 system。压缩单向不可逆DB 原始消息保留。
**③ 智能裁剪(自动压缩)**agentic loop 循环体顶部 `build_for_request` 前检测:`history_tokens > budget×0.6` 且有可压缩消息 → `compress_old_messages`(保护区外消息 LLM 摘要 → 标 compressed → 插摘要 system→ emit `AiCompressing`/`AiCompressed`。LLM 失败降级原有裁剪(不阻塞 loop`compress_old_messages` 是手动/自动共用的公共函数。幂等:已 compressed 不二次压缩,`is_compressing()` 标志防重入。
**实施顺序**阶段1 基础(is_active 白名单+push token 修正+compress_prompt+compress_via_llm+辅助方法) → 阶段2 手动功能(2 IPC+前端按钮+渲染+i18n) → 阶段3 自动(loop 压缩+事件+前端 loading)。阶段1-2 完成用户已有完整能力。
**涉及文件**`df-ai-core/provider.rs`(is_active 白名单) + `df-ai/context.rs`(push token 修正+辅助方法) + `commands/ai/prompt.rs`(compress_prompt) + `commands/ai/title.rs 或新文件`(compress_via_llm) + `commands/ai/commands.rs`(2 IPC) + `commands/ai/agentic.rs`(loop 自动压缩) + `commands/ai/mod.rs`(2 事件) + `lib.rs` + `api/ai.ts` + `useAiPanel.ts` + `useAiConversations.ts:79`(过滤扩展) + `useAiEvents.ts` + `AiChat.vue` + `i18n/{zh-CN,en}/aiChat.ts`
- [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] ✅(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。

View File

@@ -223,6 +223,16 @@
--- ---
### CR-260616-46 波2 F-01阶段4路由器 + UX-05/06/07队列三件套 — 🟡 待审
- **范围**: 波2 workflow(w76d81dno,2 agent 并行不同域)。①**F-01 阶段4**(8dad15e):df-ai 新建 router.rs(329 行)+ lib.rs 注册 pub mod router。TaskRequirements 5 字段(modalities/needs_tool_use/min_intelligence/max_cost/estimated_context)+ ModelRouter 单元结构 + select 关联函数 `select<'a>(req,pool)->Option<&'a ModelConfig>` 7 步过滤链(enabled→模态全子集→needs_tool_use 含 ToolUse→intelligence>=min→max_cost<=→context_window>=→max_by_key (weight, Reverse(cost_tier)))。**设计偏离(已对齐任务规格)**:select 关联函数非 &self 方法(router.rs:9 注释注明);max_by_key 用 std::cmp::Reverse 取反(任务规格)非设计原文 -(as i32)。②**UX-05**(313459b):useAiSend.ts stopChat 删 state.queue=[](决策 a 逐条续发,后端 agentic.rs:190 AiCompleted 已保 drainQueue 续发)。③**UX-06**(313459b):editQueued(index,newText) 边界检查+trim+直接赋值 text(决策只编 text)+ AiChat.vue 队列项 inline input(editingQueueIdx/editingQueueText ref)+ 编辑/保存(@mousedown.prevent 防 blur)/取消按钮。④**UX-07**(313459b):sendQueuedNow(index) splice 先→stopChat→sendMessage 顺序关键(splice 前置避重复发,复用 UX-05 stop 链路)+ AiChat.vue 立即发按钮。⑤i18n 4 key(queueEdit/Save/Cancel/SendNow)。
- **维度**: ①select 7 步过滤链严格对齐设计 §6.1 L469-478 ②Reverse(cost_tier) 取反正确性(Free 胜 High,CostTier Ord Free<Low<Medium<High) ③max_by_key 元组排序(weight 优先,同 weight Reverse(cost) 次序) ④TaskRequirements 5 字段完备 ⑤13 单测覆盖(每步独立 + 组合 all_dimensions_match_picks_best 5 候选参差) ⑥UX-05 决策 a 落地(删 queue=[] 零改 drainQueue,后端就绪核验) ⑦UX-06 editQueued 边界(越界/空白 no-op)+ inline input 三路保存一致(回车/blur/@mousedown.prevent) ⑧UX-07 sendQueuedNow splice 前置顺序(避重复发)+ 决策 a 复用 stop 链路 ⑨i18n 转义(4 key 纯文本无 @/|/裸 {}) ⑩主代亲跑 cargo test df-ai router:: **13 passed** + vue-tsc exit 0。
- **commit**: 8dad15e(F-01 阶段4) + 313459b(UX 队列三件套)。
- **主代独立核查**: ✅ 全过(router.rs 329 行真实 + select 7 步 L55-63 严格对齐 §6.1 + Reverse(cost_tier) L62 + 关联函数 L54 + 13 单测 model()/req() 辅助 L71-95 / cargo test router:: **13 passed** 亲跑 EXIT 0 / useAiSend editQueued 边界+trim / sendQueuedNow splice→stop→send 顺序 / stopChat 删 queue=[] 决策 a / AiChat 队列 UI editingQueueIdx L1567 inline+4 按钮 L529-538+CSS L2977 / vue-tsc exit 0 亲跑 EXIT 0)。
- **审查 agent 待复审重点**: ①select 关联函数 vs &self 方法偏离是否需对齐设计文档(任务规格允许,router.rs:9 注明;阶段5 调用点用 ModelRouter::select(&req,&pool) 适配) ②Reverse(cost_tier) 与设计原文 -(as i32) 行为等价性(同 weight 选便宜,CostTier 4 值枚举 Reverse 安全) ③max_by_key 元组 (weight, Reverse(cost)) 同 weight 同 cost 时返回哪个(Rust max_by_key 返回最后最大,不影响路由正确性) ④UX-05 行为变更(打断不清队列)用户感知验证(后端 AiCompleted 就绪,前端实测 tauri dev:打断后队列续发) ⑤UX-07 sendQueuedNow 边界:stop_flag 复位速度,sendMessage 时后端仍 generating 走入队降级(可接受非阻塞) ⑥UX-06 inline input @mousedown.prevent 防 blur 三路保存路径覆盖(回车/blur/按钮) ⑦三件套共享 useAiSend/AiChat 队列域,无越界改后端 .rs。
---
## 已审归档 ## 已审归档
### CR-260616-34 batch58 F-260616-10 provider 默认值重启失效 bug — ⚠ 已审(PASS-WATCH) ### CR-260616-34 batch58 F-260616-10 provider 默认值重启失效 bug — ⚠ 已审(PASS-WATCH)