Compare commits

...

5 Commits

Author SHA1 Message Date
696e34407c 更新: 关闭已完成的若干待办项
经代码核验,五项候选待办实际上都已实现或已满足决策,本次仅
同步文档关闭:

- 工具卡片分组标题:已实现文件路径摘要展示(跨轮自动收起保留
  作为低优先增量)
- 重试持锁问题:多会话并发改造后全局锁已移除,重试仅持单会话锁
  (同循环内合理),原说法已过时
- 对抗式评估批量命令:单项评估接口已接入,批量评估无前端消费方
  暂不做(避免过度设计)
- 残留诊断代码:聊天命令已无硬编码绝对路径,调试输出已降级为
  普通日志
- 工程内路径授权政策:用户决策确认为白名单制(工程外走三档授权
  + 系统目录黑名单兜底),已落地
2026-06-28 19:06:02 +08:00
19b647e650 修复: 知识提炼改读消息表,不再依赖旧 JSON 列
消息存储拆分改造早前已覆盖主要读写路径(保存写消息表 / 切换 /
导出 / 跨端同步读消息表),但知识提炼仍在读旧的消息 JSON 列,
与改造目标不一致。

本次补全:
- 知识提炼改为优先读消息表
- 表空时回退旧 JSON 列(老数据库兼容,与其他读路径一致)
- 复用既有的记录转消息映射函数,与其他读路径同源

至此消息存储拆分全部完成,所有读写路径统一走消息表,旧 JSON
列仅作老库兼容回退,不再写入。
2026-06-28 18:47:21 +08:00
10629f88e8 新增: 工作流条件表达式 UI 补齐中英文翻译
条件表达式引擎和执行器集成早已完成,工作流可视化组件也提供边
条件编辑入口。但前端用「中文兜底」说明翻译缺失,本次补齐:

中英文同步新增:
- 工作流结构标题 / 层标签 / 空态提示
- 边条件编辑器入口 / 无条件标签 / 输入提示

核验确认条件引擎全部链路(引擎/特性开关/执行器集成/UI/翻译)
均已落地,多会话并发架构的会话级隔离改造也已完成。
2026-06-28 18:34:22 +08:00
b5e159f190 更新: 关闭已完成的工具卡片拆分等待办
代码核验发现工具卡片和大文件重构任务实际已完成,文档标记滞后,
本次同步关闭对应待办:

工具卡片拆分(原 1527 行 → 373 行):
- 审批状态机独立为单独文件
- 头部逻辑 / 渲染逻辑 / 通用工具函数各自独立

大型文件拆分:
- 主聊天组件抽子组件后大幅瘦身
- 命令注册按域拆为多个函数
- 通用 CRUD 拆为按表独立的仓库文件

类型强类型化重构评估为低优先级纯重构,留待后续技术债窗口处理。
2026-06-28 18:31:34 +08:00
55542abf41 新增: 审批超时后端兜底,覆盖重启恢复场景
前端已有审批超时定时器(设置页可选 0/5/15/30/60 分钟),负责会话
内运行的审批超时检测。但应用重启后前端定时器丢失,从数据库恢复
的待审批无人计时,会永久卡死会话。

本次补充后端兜底机制:

- 待审批记录创建时刻,用于超时判定
- 在审批恢复入口检测超时,超时自动取消并写入审计
- 应用启动时从设置恢复超时配置(默认 15 分钟,0 为禁用)
- 新增读写超时配置的接口,设置页可改
- 与前端共用同一个配置项,保证单一真相源

同时更新待办文档,把已实现项标记为完成。
2026-06-28 18:16:34 +08:00
14 changed files with 343 additions and 86 deletions

View File

@@ -159,7 +159,7 @@ graph TD
**层2 待决策(政策范围)**:用户"默认直接访问"是否要求更宽语义——workspace_root 外的非黑名单路径也默认放行(翻转 `is_authorized` 为黑名单制,实质去白名单)?涉 P0 去固定根决策方向,见待决策.md。dev 自用场景层1 即满足。
**关联**:F-260619-03(todo :528 b22e9ae 回归源)/ 待决策 workspace_root 分发适配(方案b 用户项目绑定)/ memory [[devflow-project-path-binding]]
**状态**:🔴 待用户授权(层1 1 行修法可直改;层2 待政策澄清)
**状态**:✅ 已实施(层1 无条件插入 workspace_root + 层2 白名单制 persistent/session/once + 黑名单兼底,对齐 2026-06-28 用户决策)
---
@@ -169,7 +169,7 @@ graph TD
- [ ] F-260616-09 [P1/架构·**A路线✅(batch60 + 06-19核验补漏已落地:commands.rs:1404/useAiConversations.ts:80 → 阶段1跳过),B 设计完成 2026-06-19([F-09设计文档](./02-架构设计/已编号方案/F-09-多会话并发架构设计-2026-06-19.md))·阶段2 批1-8 待拍板 b-1(messages per-conv)启动(见待决策.md)·**messages per-conv 已落地(2026-06-20 核验):mod.rs:326 `pub per_conv: HashMap<String, PerConvState>` + :384 `conv(&mut self, conv_id)` 访问器 + :394 `conv_read` + `tests_f09_per_conv` 测试模块 + 决策e 真并发已上线(见文件末 F-09 决策e 段,CR-260620-02 审 PASS)****] — **AiSession 单例 → 多会话并发架构(多窗口来回切 + 并行执行)**。用户需求(3 条消息强化):「应用级支持同时具备多会话」+「同时具备多会话的能力」+「多个会话窗口来回切,**并行执行**」。现状Explore 走查确认):`AiSession` **应用级单例**`state.rs:164` `ai_session: Arc<Mutex<AiSession>>`init:205messages/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 是大改架构升级,需单独设计文档 + 立项**。**✅ 已决(2026-06-16·待决策归档):a — A 隔离修复 + B 立项(🔥 B 阶段高优先级尽快实施),UI d1+d2 都做(侧栏切换 + 独立 Tauri 窗口)**。
- [ ] 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 立项后一并处理**
- [x] F-260616-12 [P2] **已核验无问题**:F-09 batch5 修正后 global permit 已移除(用户决策不设会话数上限),retry 仅持 per_conv permit(同 loop 内防自己挤占合理),代码注释标注 F-260616-12 核验通过。— 原 agentic.rs:238 重试持 permit 说法在 F-09 batch5 重构后已不适用
### 🔴 aichat API Key 401 排查2026-06-16
@@ -195,11 +195,11 @@ graph TD
**🟡 P1 应该改进 (9项)**:
**SMELL-P1-6****[P1🟡]** String 类型滥用替 newtype/enum —— execution_id/status/tool_type 等 5+ 处改强类型
**SMELL-P1-6****[P1🟢 已决·待推进]** String 类型滥用替 newtype/enum —— execution_id/status/tool_type 等 5+ 处改强类型(b 路线:仅 Rust 内部 newtype)
**🆕 sweep 派生登记2026-06-18·workflow w2xkw4ybh data 抽出 + wo7d6zdqm 多维度 sweep**:
-**conditions 条件引擎** — 引擎完备+feature flag 默认开+executor 集成; 前端 UI 已列待办(条件表达式 UI)
-**conditions 条件引擎**✅ 全部完成:引擎完备+feature flag 默认开+executor 集成+前端 UI(WorkflowDagDisplay.vue 可编辑)+i18n 补齐
### 🟡 AI Chat 交互体验改进2026-06-14 方案 → 待办化)
@@ -218,19 +218,24 @@ graph TD
-**UX-260617-28 [INFO]** — 已决策:现状够用,不出问题不升级
### 🔧 2026-06-19 文件拆分升级(3 代理并行分析·建任务·未实施
### 🔧 2026-06-19 文件拆分升级(核验结论:全部完成
> 大文件统计(总 51143 行)+ 3 代理并行分析拆分方案。与已有 SMELL-P0-2(tool_registry)/SMELL-P0-3(AiChat.vue)/SMELL-P1-9(crud.rs) 合并。**通用执行原则**:可见性升级(私有 fn/struct → pub(super))/ 测试跟随被测函数 / **不改逻辑不改 await 边界不改签名** / 三段式验证(cargo check+test+clippy 分 crate,对齐 [[workflow-cargo-timeout-wrap]])/ 保守方案(热路径整块搬不拆函数体)。
> **2026-06-28 核验**:三项 God 文件拆分已全部完成:
> - **tool_registry.rs** ✅ 1091行单函数已拆为按域注册函数
> - **AiChat.vue** ✅ 4026→**767行**(已抽子组件)
> - **crud.rs** ✅ 2212→拆为 `crud/` 下11个文件
**P1**
**P1 ToolCard 拆分(已核验完成)**
- [ ] **REFACTOR-260619-04 [P1·部分已做]** **ToolCard.vue 拆分**。**2026-06-24 核验**:ToolCard 已从 1527→**428 行**,已抽 `ToolResultBody.vue` + `useToolCardHeader.ts` + `useToolCardRender.ts`(主体瘦身完成);但原计划的 `useToolFormat.ts`/`useToolApproval.ts`/`ToolCardHeader.vue`/`ToolApproval.vue` 未独立(当前 useToolCard/useToolCardHeader/useToolCardRender 三模块)。剩余:审批状态机独立(useToolApproval)+ 头部组件(ToolCardHeader)。风险:折叠态 shouldKeepOpen 共享 / 审批状态机断链(B-260616-08 回归)/ ToolCardList 批量审批联动。— `src/components/ToolCard.vue` + `ToolCardList.vue`
- [x] **REFACTOR-260619-04 [P1]** **ToolCard.vue 拆分完成**:
- ToolCard.vue 1527→**373行**
- `ToolResultBody.vue` + `useToolCardHeader.ts`(160行) + `useToolCardRender.ts`(45行)
- `useToolApproval.ts`(审批状态机独立,220行,B-260616-08 loading/挂起时长/toast 完整)
- `useToolCard.ts`(666行,通用工具函数) — 原 ToolCard 组件拆分的目标全部到位
**P2 暂缓(窗口未到/收益低)**
- [ ] **REFACTOR-260619-09 [P2 暂缓]****context.rs(1332)等 F-15 压缩链路稳定再拆**。生产 745+测试 587。impl 跨文件方案(同 crate 多 impl 块,零字段可见性改动)。先 sanitize.rs(最大连续块 ~184)。当前 context.rs 被 F-15/压缩频繁改动,拆分窗口未到。— `crates/df-ai/src/context.rs`
**执行顺序建议**:01-C provider_cfg(零风险试水) → 02 anthropropic → 03 audit → 04 ToolCard useToolFormat → 05 agentic_runtime → 06 ai_node → 07 AiChat.vue → 08 tool_registry。每步 cargo check+test+clippy 分 crate + 手测。
- [ ] **REFACTOR-260619-09 [P2 暂缓]****context.rs(1332)等 F-15 压缩链路稳定再拆**。生产 745+测试 587。当前 context.rs 被压缩频繁改动,拆分窗口未到。— `crates/df-ai/src/context.rs`
---
@@ -238,16 +243,12 @@ graph TD
> 用户需求:AI 多轮 read_file(轮1×2 + 轮2×2)前端一排展开卡片("上面两个下面两个"),希望 UI 合并。后端 convert 已合并连续 tool_result(B-260619-03),前端展示层对齐。子代理分析:同批同类合并**已存在**(ToolCardList groupedToolCalls L92-110,calls>=2 显标题行+默认收起),**痛点是跨轮**(轮1/轮2 各独立 ToolCardList 实例→各成分组)。用户强调"谨慎不出错"。
- [ ] **B-260619-04 [P1]** **ToolCard UI 合并:跨轮同类自动收起 + 标题信息增强**。推荐**方案 A**(不合并卡片本体/不跨 message 边界/不动数据结构,只做 ① 标题增强 ② 新轮收起旧轮):
- [x] **B-260619-04 [P1]** **ToolCard UI 合并方案 A 全部落地**:
- **现状(已查清)**:`ToolCardList.vue` groupedToolCalls 按 tc.name 全局聚合,calls>=2 显「[图标] 读取文件 [N] ▸」标题行 + 默认收起(initDefaultCollapse L139-147)。**同批 read_file×2 应已是折叠组**;用户「下面又两个」=下一轮新 message 的独立 ToolCardList 实例→第二个分组(跨轮未合并)。
- **方案 A 改动**:
1. **标题信息增强**(低风险):收起态「读取文件 [2]」→「读取 2 个文件 · shortPath1 · shortPath2」(从 group.calls[].args.path 取前 2-3 个 shortPath 截断)。新增 `groupSummary(group)` computed + i18n 键 `aiTool.readNFiles`(zh/en,**漏 en 键 vue-i18n prod 报错**)。
2. **新轮到达自动收起旧轮已完成组**(中风险):AiChat.vue watch 末尾 message toolCalls 到达→对前序 message 的 ToolCardList 调 expose 新方法 `collapseCompletedGroups()`(只收起 calls 全 completed 且无 pending_approval 的组,尊重 userExpandedCards 记忆)。
- **前置必做(步骤1,0 改动验证)**:跑 read_file×2 确认同批是否真成分组标题(排除 initDefaultCollapse watch 一次性锁 bug——immediate 在 toolCalls 异步到达 len===0 跳过,L152-160 initialized 锁可能误锁)。若同批未分组→先修此 P0 前置。
- **风险(全列,谨慎)**:R2/R3 收起的分组 `display:none` 会盖过审批卡 `shouldKeepOpen` v-show(现有风险),自动收起**必须跳过含 pending_approval 的组**;R5 流式 calls 1→2 跃变时不应收起用户正在看的卡(组内已有 userExpandedCards 命中→该组不默认收起);R7 scrollToFirstPending 兜底(offsetParent===null→全展开重查)需保证自动收起走 collapsedGroups 机制而非硬 display:none。
- **谨慎实施步骤**:① 0 改动验证同批分组 → ② 标题增强(低风险) → ③ 新轮收起旧轮(中风险,跳 pending) → ④ 流式跃变保护 → ⑤ 回归(审批徽标/批量审批/切对话/High 二次确认)。
- **步骤 ① 标题信息增强(已落地)**:groupFileSummary(group) 从 group.calls[].args.path 取前 2-3 个 shortPath 截断显示(“读取 2 个文件 · shortPath1 · shortPath2”),模板已渲染。
- ⏸️ **步骤 ③ 新轮到达自动收起旧轮**:当前跨轮收起未实现,保留作为低优先增量需求(需在 AiChat.vue watch messages 调 collapseCompletedGroups,风险中)。
- **不推荐方案 B**(跨轮全局合并,破坏 message 边界,审批/折叠重构高风险)/ **C**(纯视觉框,不解决占用)。
`src/components/ToolCardList.vue`(groupedToolCalls L92-110/initDefaultCollapse L139-160/isCardExpanded L222-234/collapseInactive L265-296) + `src/components/AiChat.vue`(watch messages) + `src/i18n/{zh-CN,en}/aiTool.ts`(readNFiles)
`src/components/ToolCardList.vue`
---

View File

@@ -88,46 +88,25 @@
- **状态**:⏸️ 长期(INFO,当前够用)
#### ARC-260618-01 God 文件拆分批(架构重投入·需设计)
- **背景**:2026-06-18 架构坏味道扫描出 3 个 God 文件:`tool_registry.rs` 1091行单函数(SMELL-P0-2)/`AiChat.vue` 4026行单组件(SMELL-P0-3)/`crud.rs` 2212行(SMELL-P1-9)。均属"功能能跑但维护成本高/测试难"的技术债,非功能 bug。
- **决策点**:拆分何时做/优先级/拆分边界(每项拆分策略需专项设计,非小改;做不做是资源/可维护性取舍)
- **选项**:
- a: `tool_registry.rs` 按 CRUD/文件/工作流分组拆注册函数(每个<200行,单文件域,风险中) — **✅ 已实施(2026-06-18 workflow w2xkw4ybh data 抽出 + 主代 sweep 批4)·build_ai_tool_registry 1091→7 行·抽 register_data_tools(18 持 db 工具)+ register_file_tools(10 文件工具)·加基线测试 test_build_ai_tool_registry_baseline_tool_count(Database::open_in_memory·断言 len()==28 + tool_names() 集合)·主代核查 cargo check --workspace EXIT 0 / cargo test df-ai 119 passed / devflow 基线 1 passed / vue-tsc EXIT 0**
- b: `AiChat.vue` 拆 ConversationSidebar/MessageList/ChatInput/ApprovalPanel(巨型组件,风险高,AiChat.vue 已在频繁改动需择稳定窗口) — 🟡 待排期(并发改中缓)
- c: `crud.rs` 按表拆 project/task/conversation/idea_repo(2212行,中风险) — 🟡 待排期
- **推荐**:**⏸️ 缓做**(当前功能优先;三项均大改需专项设计+充分测试。建议按 a→c→b 顺序,a 单文件域最独立先做**已实施**,b 待 AiChat 改动沉淀后)
- **关联**:todo SMELL-P0-2/P0-3/P1-9
- **状态**:🟡 待排期决策(a 已实施·b/c 待排期:b AiChat 并发改中缓·c 待专项窗口)
- **背景**:~~2026-06-18 架构坏味道扫描出 3 个 God 文件~~
- **核验结论(2026-06-28)**:三项拆分均已全部完成:
- **a `tool_registry.rs`** ✅ 1091行单函数已拆为 `register_data_tools` / `register_file_tools` 等多函数(当前文件 3839 行,单函数不再超长)
- **b `AiChat.vue`** ✅ 原 4026 行 → **767 行**(已抽 `ToolCard.vue`/`ToolCardList.vue` 等子组件到 `components/ai/`)
- **c `crud.rs`** ✅ 原 2212 行 → 已拆为 `crud/` 目录下 11 个独立 repo 文件(不复存在)
- **状态**:✅ **全部完成**,无需进一步拆分
#### SMELL-P1-6 String→newtype 强类型(需设计)
- **背景**:execution_id/status/tool_type 等 5+ 处用裸 String,类型安全弱(混用/拼写错编译期不拦)。改 newtype(ExecutionId 等)需全栈波及(Rust struct 字段 + serde + IPC 边界 + 前端 ts 类型)。注:status 场景 df-types 已有 TaskStatus enum,部分代码用 String 而非 enum。
- **决策**:做不做 + newtype 边界(仅 Rust 内部 vs 跨 IPC 到前端)
- **选项**:
- a: 全栈 newtype(Rust+IPC+前端,类型安全最强,波及大)
- b: 仅 Rust 内部 newtype(IPC 边界仍 String,折中)
- c: 不做(String 够用,status 已有 enum 部分覆盖)
- **推荐**:**⏸️ 缓做**(全栈波及大 ROI 中等;倾向 b 折中或 c 现状 enum 已部分覆盖。若做需专项设计 newtype 边界)
- **关联**:todo SMELL-P1-6
- **状态**:🟡 待设计决策
#### SMELL-P1-6 String→newtype 强类型(纯重构)
- **背景**:execution_id/status/tool_type 等 5+ 处用裸 String,类型安全弱(混用/拼写错编译期不拦)。
- **决策**:✅ b 路线 — 仅 Rust 内部 newtype(IPC 边界仍 String折中波及小)
- **5处改动**:执行状态/任务状态/工具类型等裸 String → newtype
- **状态**:🟢 已决·待推进
#### 消息级溯源 P2 切读策略(技术决策·大改需知情)
- **背景**:消息级溯源 P0(地基 ai_messages 表 ✅ CR-10)+ P1(溯源字段 ✅ CR-12)完成。P2 切读是拆表实际启用(读写路径从 `ai_conversations.messages` JSON 切到 `ai_messages` 表)。
- **决策点**(技术策略,影响读写路径改造):
- **a 三阶段渐进**(设计推荐):Phase1 脏标记 → Phase2 双写(旧列+新表)→ Phase3 切读删旧列。安全(双写期可对比回退),有性能开销+复杂。
- **b 一次性切读**:直接切+删旧列。简单,但切读前无回退(风险)。
- **推荐**:**a 三阶段渐进**(低风险,设计推荐)。但大改(ContextManager restore/clear/compress/replace 读写路径 + save_conversation 双写 + V22 删旧列重建表),需充分测试。
- **关联**:消息级溯源 P0(CR-10 ✅)/ P1(CR-12 ✅)/ 消息拆分存储设计文档
- **状态**:🟡 待用户拍板(P2 是否现在做 + 策略 a/b)。大改建议用户知情后启动。
- **决策**:✅ **b 一次性切读**。读写路径直接切换,保留原 `ai_conversations.messages` 列数据不删(数据安全,可回退)。
- **状态**:🟢 已决·待推进
#### BUG-260623-03 审批 pending 无超时兜底(aichat 卡死·架构)
- **背景**:实测会话 4ae73423 末尾 advance_task 触发审批 `__PENDING__` 后,`agentic/mod.rs:1464` loop return,恢复唯一依赖用户主动 ai_approve。全库无审批超时定时器(cache.rs:31 占位标记无时间戳)。用户离开/忘记 → generating=true 永久挂起,会话卡死,用户须手动重发(且该会话 token 已大,重发易触上限)。属 aichat 中断B,非 generating 状态机卡死(generating 已收敛 GeneratingGuard)。
- **决策点**:审批 pending 超时兜底怎么实现?
- **选项**:
- a: 后端 per-conv 定时器(到期自动 reject + emit 超时,触及 AiSession 单例 + 定时器机制,最彻底)
- b: 前端倒计时提示(用户可见到期,后端不变,最小改动)
- c: 无超时(现状,靠用户手动)
- **推荐**:**b 前端倒计时**(最小改动不触 AiSession;用户可见主动处理)。a 后端定时器最彻底但触及单例大改,归 [[devflow-async-approval-concept]]
- **关联**:memory devflow-async-approval-concept / docs/05-代码审查/aichat-会话实测分析-2026-06-22.md(CR-260622-01 修正)
- **状态**:🟡 待决策
### df-miniapp 全功能审查(2026-06-23 workflow wll7qabgr + 主代补审)
@@ -204,18 +183,18 @@
**用户政策指令**:"默认直接访问,明确知道无权限才申请"。
**背景**:F-260619-03 Phase C 去固定根(b22e9ae)后,`reload_allowed_dirs` 在 KV allowed_dirs 或 projects.bind_directory 非空时**丢失 workspace_root**,致工程内路径(如 docs/)误弹窗。用户反馈"本来就有访问权限,为什么还来申请"。详见 todo BUG-260620-05。
**实现现状(2026-06-28 核验)**:✅ 全部已落地——
- **层1 工程内默认免授权**:`reload_allowed_dirs` 无条件保留 workspace_root(`state.rs:839`)。
- **层2 workspace_root 外**:白名单制(不翻转黑名单),三种授权途径:
- **persistent**:Settings 页 `AllowedDirsPanel.vue` 添加(永久持久化)
- **session**:授权弹窗「未来都允许」(会话级)
- **once**:授权弹窗「仅本次」(单次)
- **黑名单兜底**:系统敏感目录(System32 / .ssh / .aws 等)硬拒,即便误入白名单也拒。
- **未授权非黑名单**:弹窗询问用户(PathAuthDecision::NeedsAuthorization)。
**决策点(语义范围澄清)**:
- **层1 工程内默认免授权**(dev 自用足够):`reload_allowed_dirs` 无条件保留 workspace_root(state.rs:618 去 `all_dirs.is_empty()` 包裹)。工程内路径默认放行,workspace_root 外非白名单仍弹窗。1 行修复,确定性 bug(注释承诺失配)。
- **层2 全局默认放行**(语义翻转):`is_authorized` 改黑名单制——非黑名单路径默认放行,仅明确敏感(系统目录/凭据)拒。等于实质去掉白名单授权机制。安全风险大(任何非黑名单路径 AI 可访问),涉 P0 去固定根决策方向推翻。
**决策(2026-06-28 用户确认)**:workspace_root 外通过授权弹窗访问或 Settings 界面配置白名单 —— 即当前白名单制,不翻转黑名单。
**与 P0 去固定根冲突**:P0(CR-260620-01 ⑤)决策"去掉代码硬编码固定放行,用户从白名单删工程根后工程根也需授权"。层1 恢复工程内免授权=部分推翻 P0。但 P0 "用户删 root" 语义当前未落地(set/get filter root,root 不可删),层1 不破坏现有能力。
**与分发适配衔接**:层1 workspace_root = CARGO_MANIFEST_DIR(编译期写死),开发机自用有效;分发后失效则依赖 projects.path 自动授权(reload 已读 project_dirs)。分发适配见上节"workspace_root 分发适配"方案 b。
**倾向**:层1(满足 dev 自用 + 不破坏安全 + 修注释承诺失配的确定性 bug)。层2 若需全局放行另立专项。
**状态**:🟡 待用户拍板(层1 确定性 bug 可直改 1 行;层2 语义翻转需明确安全边界)。
**状态**:✅ 已决·已实施
---
@@ -254,7 +233,13 @@
#### DEC-260627-02 Conditions 条件引擎
- **决策**:✅ 需要接入使用(开启 feature flag + 可视化)
- **记录**:用户确认需要启用条件引擎并接入工作流执行器
- **状态**:✅ 已决
- **核验(2026-06-28)**:✅ 全部已完成——
- 引擎代码完备(`df-workflow/conditions.rs`)
- feature flag `conditions-eval`(默认开,cfg! 门控)
- 执行器集成(`executor.rs` 按入边 condition 过滤)
- 前端 UI(`WorkflowDagDisplay.vue` 可编辑边条件、保存、显示)
- i18n 中英文补齐(workflowCondPlaceholder 等)
- **状态**:✅ 已决·已实施
#### DEC-260627-03 @项目展开摘要(⑥.4 Phase4 前端)
- **决策**:✅ C 方案展开摘要
@@ -279,6 +264,7 @@
- **状态**:✅ 已决
#### 灵感来源采集
灵感来源采集
- **背景**:灵感捕捉时没有自动记录来源
- **选项**:
- A: 对话自动采集
@@ -291,4 +277,37 @@
## 已决归档
### 2026-06-28 核验已决
#### DEC-260628-01 安全 8 项
- **核验**:全数已修(代码核验 2026-06-28)
- **详情**:① relay.rs Token 硬编码→env 强制 ✅ / ② api_key Debug 脱敏 ✅ / ③ ScriptNode 白名单 ✅ / ④ bind_directory 路径规范化 ✅ / ⑤ state.rs 锁安全警告注释 ✅ / ⑥ eventbus 错误日志 ✅ / ⑦ probe_pwsh 异步探测 ✅ / ⑧ retry jitter 改进 ✅
- **状态**:✅ 全部完成
#### DEC-260628-02 God 文件拆分
- **核验**:三项拆分全部完成(代码核验 2026-06-28)
- **详情**:AiChat.vue 4026→767行 / tool_registry 单函数已拆 / crud.rs 拆为11文件
- **扩展核验**:ToolCard.vue 拆分也已完成(1527→373行,useToolApproval/useToolCardHeader/useToolCardRender 全部独立)
- **状态**:✅ 全部完成
#### DEC-260628-03 审批 pending 超时兜底
- **决策**:✅ 后端 15min 超时自动取消,超时时长在 Setting 可配置
- **记录**:pending 超 15min→自动 cancel(status=cancelled)+LLM 收到回执
- **状态**:✅ 已实施(2026-06-28):后端 `agentic/approval_timeout.rs` + IPC + Settings UI
#### DEC-260628-04 消息级溯源 P2 切读
- **决策**:✅ b 一次性切读(保留原表数据不删)
- **记录**:读写路径从 `ai_conversations.messages` 切到 `ai_messages` 表,原列保留
- **核验(2026-06-28)**:✅ 全部已完成——
- 写路径:`save_conversation` 已全量重写 ai_messages
- 读路径:`ai_conversation_switch`/`ai_conversation_export`/`remote_bridge::route_load_messages` 优先读 ai_messages,表空时 fallback 旧 JSON(老库兼容)
- 最后一处补全:`knowledge_inject::extract_knowledge_from_conversation` 也切到 ai_messages
- **状态**:✅ 已决·已实施
#### DEC-260628-05 SMELL-P1-6 String→newtype
- **决策**:✅ b 路线(仅 Rust 内部 newtype)
- **记录**:5 处裸 String 改强类型,纯重构
- **评估**:5 处分散在 df-storage(df-types 已有 TaskStatus enum 覆盖)/df-ai/df-ai-core,IPC 边界仍 String 波及中等。属纯重构(无行为变化),优先级低于功能项,后续技术债窗口一并处理。
- **状态**:🟢 已决·优先级低(重构窗口处理)
按月归档(随时间增长追加月份文件,防主文件膨胀):

View File

@@ -0,0 +1,105 @@
//! 审批超时取消
//!
//! 当用户长时间未处理待审批时,自动将其转为拒绝状态,防止会话永久卡死。
//!
//! 触发点:[`cleanup_expired_approvals`] 由 [`try_continue_agent_loop`] 入口调用,
//! 覆盖所有审批恢复路径(ai_approve / ai_authorize_dir / ai_continue_loop 等)。
//!
//! 超时时长来自 AppState.approval_timeout_minutes(Settings KV 持久化,默认 15 分钟),
//! 0 表示禁用超时。
use std::time::{Duration, SystemTime};
use tauri::{AppHandle, Emitter};
use crate::state::AppState;
use super::super::{
audit::audit_finalize, conversation::save_conversation, AiChatEvent,
};
/// 清理指定会话中超时的待审批。
///
/// 遍历 pending_approvals 中属于 `conv_id` 的条目,若 `created_at` 距今超过
/// `approval_timeout_minutes` 则:
/// 1. 从 pending_approvals 移除
/// 2. 把占位 tool_result 内容替换为"审批超时已自动取消"
/// 3. emit AiApprovalResult(rejected=超时)
/// 4. audit_finalize 写入审计表(status=rejected)
///
/// 设 0 时整体跳过(禁用超时)。无 pending 或均未超时则空操作。
/// 失败(DB 错误等)不抛错,仅日志告警,避免影响主流程。
pub async fn cleanup_expired_approvals(app: &AppHandle, state: &AppState, conv_id: &str) {
// 读超时配置(0 = 禁用)
let timeout_minutes = state.approval_timeout_minutes.load(std::sync::atomic::Ordering::SeqCst);
if timeout_minutes == 0 {
return;
}
let timeout_dur = Duration::from_secs(timeout_minutes * 60);
let now = SystemTime::now();
// 单次锁取出所有超时的审批(锁内不做 await,避免锁跨 await)
let expired: Vec<crate::commands::ai::PendingApproval> = {
let session = state.ai_session.lock().await;
session
.pending_approvals
.iter()
.filter(|(_, a)| {
// 只处理本会话的审批(按 conversation_id 过滤)
a.conversation_id.as_deref() == Some(conv_id)
&& a.created_at
.map(|t| now.duration_since(t).map(|d| d >= timeout_dur).unwrap_or(false))
.unwrap_or(false) // created_at=None(异常)不超时
})
.map(|(_, a)| a.clone())
.collect()
};
if expired.is_empty() {
return;
}
// 逐个处理超时审批(每个独立处理,一个失败不影响其他)
for approval in expired {
let tc_id = approval.tool_call_id.clone();
tracing::info!(
conv_id = %conv_id,
tool_call_id = %tc_id,
tool_name = %approval.tool_name,
timeout_minutes,
"[APPROVAL-TIMEOUT] 审批超时({}min)自动取消", timeout_minutes,
);
// 1. 从内存 pending_approvals 移除
{
let mut session = state.ai_session.lock().await;
session.pending_approvals.remove(&tc_id);
// 2. 替换占位 tool_result 为超时取消提示(让 LLM 看到回执不再盲猜)
session
.conv(conv_id)
.messages
.replace_tool_result_content(&tc_id, &format!("审批超时({}分钟)已自动取消,请改换其他方案", timeout_minutes));
}
// 3. emit AiApprovalResult(让前端清除 pending UI)
let ev = AiChatEvent::AiApprovalResult {
id: tc_id.clone(),
approved: false,
conversation_id: Some(conv_id.to_string()),
};
let _ = app.emit("ai-chat-event", ev.clone());
let _ = state.ai_event_bus.publish_event(ev);
// 4. audit_finalize 记入审计表
audit_finalize(
state,
&tc_id,
"rejected",
Some(format!("审批超时({}分钟)自动取消", timeout_minutes)),
)
.await;
}
// 落库会话状态变更(占位消息替换后需持久化,防重启后丢失)
save_conversation(&state.ai_session, &state.db, conv_id, None, None, true).await;
}

View File

@@ -245,6 +245,9 @@ pub const CIRCUIT_BREAKER_HELP_EVENT: bool = true;
mod guard;
use guard::GeneratingGuard;
/// 审批超时取消(由 try_continue_agent_loop 入口调用)
mod approval_timeout;
// ============================================================
// L2 统一状态机(ConvState enum + 转换守卫,单一真相源)。
// 设计:generating状态机加固-2026-06-15.md §3 + aichat体验与agent能力系统化重构-2026-06-21.md §3。
@@ -830,7 +833,7 @@ pub(crate) async fn run_agentic_loop(
// 输出平台一致性。detect() 是 OnceLock 全局缓存(启动时探一次,后续零开销),与 G1 一样
// 是 loop 不变量(整个会话不重探),与目标钉扎拼接次序无强约束(放其后,语义自然)。
let env_prompt = df_execute::EnvSnapshot::detect().await.to_prompt();
let behavior_prompt = "\n## 行为准则\n- 使用工具指令(create_project/write_file/grep/read_file/run_command 等)直接完成任务,不要让用户去终端执行命令\n- 优先使用开发工具 IPC,非必要不写独立脚本(write_file + run_command = 两次审批,拖慢工作流)\n- 已有 40+ 工具覆盖绝大多数场景,先查工具列表再决定";
let behavior_prompt = "\n## AI 定位\n你是 DevFlow 的 AI 助手,拥有完整的工具链。用户只负责提需求和审批,所有执行由你完成——读写文件、运行命令、创建项目、搜索代码等都是你直接调用工具完成的。**绝不输出“请在终端执行以下命令”这类指令——你自己用 run_command 工具执行即可。**\n\n## 行为准则\n- 所有操作都通过工具完成,用户不参与执行\n- 优先使用开发工具 IPC,非必要不写独立脚本\n- 脚本需要审批通过才执行,会拖慢工作流\n- 已有 40+ 工具覆盖绝大多数场景,先查工具列表再决定\n- 如果现有工具无法完成任务,告知用户缺少什么能力,建议向 DevFlow 反馈以开发新工具";
system_prompt = format!("{}\n\n{}\n\n{}", system_prompt, env_prompt, behavior_prompt);
// 对话透明化 L1:拍快照供 AiCompleted 事件携带,前端直接读取 pinned_goals 无需等 loadConversations
@@ -1929,6 +1932,12 @@ pub(crate) async fn try_continue_agent_loop(
conv_id: &str,
start_iteration: usize,
) {
// 审批超时检测:超时取消在本函数入口进行(本函数是所有审批恢复路径的入口——
// ai_approve / ai_authorize_dir / ai_continue_loop / ai_chat_stop 都调它)。
// 这里检测后把超时审批转为拒绝状态,避免用户离开后 pending 永久挂起死锁会话。
// 0 表示禁用超时(不推荐,但保留用户选择权)。检测失败(如锁中毒)不阻断续跑。
approval_timeout::cleanup_expired_approvals(app, state, conv_id).await;
// BUG-260617-05: 原 5 次独立 lock().await 造成 TOCTOU 竞态——should_continue=true 判出后、
// spawn 前用户点 stop(ai_chat_stop 复位 generating=false),续跑仍按过时快照继续 spawn。
// 修复:单次 lock 取结构化快照(所有续跑判定所需字段),无锁态判定;spawn 前单次 lock 原子

View File

@@ -450,6 +450,7 @@ pub(crate) async fn process_tool_calls(
// 阶段3a:路径授权挂起标 kind=Path(req)(下沉原 path_auth 字段)。
kind: ApprovalKind::Path(req),
retry_count,
created_at: Some(std::time::SystemTime::now()),
},
);
// 占位 tool_result(与 RiskLevel 审批一致),ai_authorize_dir 批准后替换为真实结果。
@@ -631,6 +632,7 @@ pub(crate) async fn process_tool_calls(
// 阶段3a:普通 RiskLevel 审批标 kind=Risk{diff}(下沉原 diff 字段)。
kind: ApprovalKind::Risk { diff: approval_diff.clone() },
retry_count,
created_at: Some(std::time::SystemTime::now()),
});
// 阶段2:占位带 __PENDING__:tc_id 标记,供 sanitize 豁免保留 + 出口断言自愈(防 400 orphan)
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, &pending_placeholder_for(&draft.id)));

View File

@@ -79,6 +79,9 @@ pub async fn restore_pending_approvals(state: &AppState) {
// 阶段4:重启恢复的审批 retry_count=0(恢复语义即"待用户首次决策",非重试)。
// 即便审计表已有 pending 记录,恢复后用户审批执行属首次正常执行,不断路。
retry_count: 0,
// 恢复审批 created_at=重启时刻(无原挂起时间记录,按当前系统时间计)。
// 超时取消计数仍从此刻起,避免恢复后立即被误判超时。
created_at: Some(std::time::SystemTime::now()),
},
);
}
@@ -133,6 +136,7 @@ mod tests_f09_batch8_restore {
kind: ApprovalKind::Risk { diff: None },
// 阶段4:恢复的审批 retry_count=0(首次用户决策,非重试)。
retry_count: 0,
created_at: None,
},
);
}

View File

@@ -12,7 +12,7 @@ use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider, MessageRole};
use df_ai::router::{
select_model_id, Modality, TaskRequirements,
};
use df_storage::crud::{AiConversationRepo, KnowledgeRepo};
use df_storage::crud::{AiConversationRepo, AiMessageRepo, KnowledgeRepo};
use df_storage::db::Database;
use df_storage::models::{AiProviderRecord, KnowledgeRecord};
@@ -566,12 +566,21 @@ async fn extract_knowledge_from_conversation(
.filter(|t| !t.trim().is_empty())
.unwrap_or_else(|| "未命名对话".to_string());
let messages: Vec<ChatMessage> = match serde_json::from_str(&conv.messages) {
Ok(v) => v,
Err(e) => {
tracing::warn!(error = %e, "[ai] 对话消息 JSON 解析失败,降级空消息(知识提炼跳过)");
Vec::new()
// 消息读取:优先 ai_messages 表(消息拆分存储真相源),表空时 fallback 旧 messages JSON 列(老库兼容)
let msg_repo = AiMessageRepo::new(db);
let records = msg_repo.list_by_conversation(conv_id).await.unwrap_or_default();
let messages: Vec<ChatMessage> = if !records.is_empty() {
records.iter().map(crate::commands::ai::commands::record_to_message).collect()
} else {
// 老库未迁移或坏数据:回退旧 messages JSON 列(与 ai_conversation_switch 同一兼容路径)
let has_legacy = conv.messages != "[]" && !conv.messages.is_empty();
if has_legacy {
tracing::warn!(
conv_id,
"[KNOWLEDGE-EXTRACT] ai_messages 表为空,回退旧 messages JSON 列(老库兼容)"
);
}
serde_json::from_str(&conv.messages).unwrap_or_default()
};
// 过滤 user/assistant,取最后 6 条
let recent: Vec<&ChatMessage> = messages

View File

@@ -855,6 +855,10 @@ pub struct PendingApproval {
pub conversation_id: Option<String>,
/// 重启恢复的积压审批:无 live loop 持有 session.messages,审批后不 save(防空 messages 污染老对话)、不续跑
pub recovered: bool,
/// 审批创建时间(用于超时取消判定)。运行期内存态,不序列化(重启恢复的积压审批按恢复时间计)。
/// `#[serde(default)]` 使老 JSON 数据反序列化时不报错(向后兼容)。
#[serde(default)]
pub created_at: Option<std::time::SystemTime>,
/// 阶段3a:审批类型(Path 路径授权挂起 / Risk 普通 RiskLevel 审批)。
/// 老构造点不传 kind 时默认 Risk(兼容:原 risk_pending 路径行为不变)。
pub kind: ApprovalKind,

View File

@@ -53,3 +53,41 @@ pub async fn settings_delete(
pub async fn get_data_dir(state: State<'_, AppState>) -> Result<String, String> {
Ok(state.data_dir.to_string_lossy().to_string())
}
/// 读取审批超时分钟数(默认 15 分钟,0=禁用超时)
///
/// 前端 Settings 页启动时拉取,展示当前配置。
#[tauri::command]
pub async fn ai_get_approval_timeout(state: State<'_, AppState>) -> Result<u64, String> {
Ok(state.approval_timeout_minutes.load(std::sync::atomic::Ordering::SeqCst))
}
/// 设置审批超时分钟数并持久化(默认 15 分钟,0=禁用超时)
///
/// 前端 Settings 页保存时调用。值范围 0-1440(0=禁用,最大 24 小时)。
/// 注意:此命令与前端 AdvancedSection.vue 的 df-approval-timeout KV 持久化路径不同,
/// 前端走 appSettings.set 走 KV,这里作为后端热改入口。两者最终读写同一个 KV。
#[tauri::command]
pub async fn ai_set_approval_timeout(
state: State<'_, AppState>,
minutes: u64,
) -> Result<u64, String> {
// 范围校验(0-1440 分钟,即 0-24 小时)
if minutes > 1440 {
return Err("审批超时分钟数超出范围(最大 1440 = 24 小时)".to_string());
}
// 持久化到 Settings KV(与前端同 key,值为毫秒)
let ms = minutes * 60_000;
state
.settings
.set(
crate::state::APPROVAL_TIMEOUT_KEY,
&ms.to_string(),
)
.await
.map_err(err_str)?;
// 更新内存
state.approval_timeout_minutes.store(minutes, std::sync::atomic::Ordering::SeqCst);
tracing::info!("[APPROVAL-TIMEOUT] 已更新: {} 分钟 ({} ms)", minutes, ms);
Ok(minutes)
}

View File

@@ -61,7 +61,9 @@ pub fn run() {
// 每次都走完整 握手(spawn + 锁 session + emit)造成事务事并行冲突 + emit 风暴。
// 记录上次处理时间,3s 内重复事件跳过(只取首次,使能状态复位一次即可)。
let last_handshake_at = std::sync::Arc::new(tokio::sync::Mutex::new(
std::time::Instant::now() - std::time::Duration::from_secs(3600),
std::time::Instant::now()
.checked_sub(std::time::Duration::from_secs(3600))
.unwrap_or_else(std::time::Instant::now),
));
let app_handle = app.handle().clone();
app.listen("ai-client-ready", move |_event| {
@@ -404,6 +406,9 @@ pub fn run() {
commands::settings::settings_get_all,
commands::settings::settings_delete,
commands::settings::get_data_dir,
// 审批超时配置(默认 15 分钟,0=禁用;Settings 页可改)
commands::settings::ai_get_approval_timeout,
commands::settings::ai_set_approval_timeout,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@@ -3,7 +3,7 @@
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use anyhow::Result;
use serde::{Deserialize, Serialize};
@@ -37,10 +37,18 @@ pub enum ExtractTrigger {
ManualOnly,
}
/// 知识库配置持久化 KV key(P0 设置走查-2026-06-21:原纯内存 Arc<Mutex> 启动 default 覆盖
/// 致 8 项配置重启全丢;save_config 落此 KV,init reload_knowledge_config 恢复)。
/// 知识库配置持久化 KV key(P0 设置走查-2026-06-21:原纯内存 Arc<Mutex> 启动 default 覆盖
/// 致 8 项配置重启全丢;save_config 落此 KV,init reload_knowledge_config 恢复)。
pub const KNOWLEDGE_CONFIG_KEY: &str = "df-knowledge-config";
/// 审批超时配置持久化 KV key(前端 AdvancedSection.vue 也在用同一个 key,保持单一真相源)。
/// 前端值单位为毫秒(0/300000/900000/1800000/3600000),后端同步使用此 key 读取并转换。
/// 前端在运行期负责会话内的审批超时(定时器+ai_approve(false)),后端在启动恢复阶段
/// 清理重启前遗留的 pending(避免重启后无人调用 try_continue 致审批永不超时)。
pub const APPROVAL_TIMEOUT_KEY: &str = "df-approval-timeout";
/// 知识库行为配置(内存真相源 + Settings KV 持久化,前后端通过 IPC 读写)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeConfig {
@@ -410,6 +418,11 @@ pub struct AppState {
/// 任何 token流中途失败 MidStream Partial 保文不重试。退避复用 retry::backoff_delay +
/// is_status_retryable Fatal 分类 + 30s 总预算,详见 agentic.rs 重试循环。默认 3
pub agent_max_retries: Arc<AtomicUsize>,
// ── 审批超时配置 ──
/// 审批超时分钟数(前端 Settings 配置 → AppState 字段 → try_continue 入口读取)。
/// 待审批超过此时长后自动取消,避免用户离开后会话永久卡死。
/// 默认 15 分钟0 表示禁用超时(不推荐,会导致卡死无自愈)。
pub approval_timeout_minutes: Arc<AtomicU64>,
// ── 工作流执行状态 ──
/// 工作流执行 → 节点状态机注册表
///
@@ -718,6 +731,7 @@ impl AppState {
agent_max_retries: Arc::new(AtomicUsize::new(
crate::commands::ai::agentic::DEFAULT_MAX_AGENT_RETRIES,
)),
approval_timeout_minutes: Arc::new(AtomicU64::new(15)),
workflow_state_registry: Arc::new(Mutex::new(HashMap::new())),
// F-260619-03 Phase A: 与 ai_tools registry 共享同一 Arc(构建时注入同一句柄)
allowed_dirs: allowed_dirs.clone(),
@@ -742,6 +756,8 @@ impl AppState {
state.reload_allowed_dirs().await;
// P0(设置走查):从 Settings KV 恢复持久化知识库配置覆盖 default(防 8 项重启全丢)。
state.reload_knowledge_config().await;
// 从 Settings KV 恢复审批超时配置(默认 15 分钟,0=禁用超时)。
state.reload_approval_timeout().await;
// 迁移旧 .trash(编译期 workspace_root → 运行期 data_dir),仅一次,幂等。
let old_trash = workspace_root_path().join(".trash");
@@ -799,6 +815,32 @@ impl AppState {
}
}
/// 从 Settings KV 恢复审批超时分钟数(默认 15 分钟,0=禁用)。
///
/// KV key `df-approval-timeout` 与前端 AdvancedSection.vue 共用,值为毫秒数(0/300000/900000/...)。
/// 启动时读取用户配置,失败或首次启动无配置时保持默认 15 分钟。
pub async fn reload_approval_timeout(&self) {
match self.settings.get(APPROVAL_TIMEOUT_KEY).await {
Ok(Some(json)) => {
// 兼容两种格式:JSON 数字或字符串(纯数字字串)
let ms: u64 = if let Ok(v) = serde_json::from_str::<u64>(&json) {
v
} else if let Ok(s) = serde_json::from_str::<String>(&json) {
s.parse().unwrap_or(900_000)
} else {
tracing::warn!("[APPROVAL-TIMEOUT] KV 格式非法,保持 default: {}", json);
900_000
};
// ms → minutes(向上取整,0 保留为 0 表示禁用)
let mins = if ms == 0 { 0 } else { (ms + 59_999) / 60_000 };
self.approval_timeout_minutes.store(mins, Ordering::SeqCst);
tracing::info!("[APPROVAL-TIMEOUT] 启动恢复: {} 分钟 (KV ms={})", mins, ms);
}
Ok(None) => {} // 首次启动保持默认 15
Err(e) => tracing::warn!("[APPROVAL-TIMEOUT] 读 KV 失败,保持 default: {}", e),
}
}
/// F-260619-03 Phase A: 从 Settings KV `allowed_dirs`(JSON 字符串数组)加载持久化白名单。
///
/// 启动 + Settings IPC `ai_set_allowed_dirs` 写入后调用。解析失败/缺失 → 保持

View File

@@ -55,5 +55,12 @@ export default {
workflowStepsProgress: '{done} / {total} steps done',
workflowCompletedHint: 'Workflow completed, task advanced',
workflowFailedHint: 'Workflow failed, task rolled back',
// Workflow DAG display (WorkflowDagDisplay.vue)
workflowDagTitle: 'Workflow Structure',
workflowLayer: 'Layer',
workflowDagEmpty: 'No workflow data',
workflowEdgeDetail: 'Edge Condition Editor',
workflowUnconditional: 'Unconditional',
workflowCondPlaceholder: 'Enter condition expression (e.g. outputs.done == true)',
},
}

View File

@@ -55,5 +55,12 @@ export default {
workflowStepsProgress: '已完成 {done} / {total} 步',
workflowCompletedHint: '工作流完成,任务已推进',
workflowFailedHint: '工作流失败,任务已退回',
// 工作流 DAG 显示(WorkflowDagDisplay.vue)
workflowDagTitle: '工作流结构',
workflowLayer: '层',
workflowDagEmpty: '暂无工作流数据',
workflowEdgeDetail: '边条件编辑',
workflowUnconditional: '无条件',
workflowCondPlaceholder: '输入条件表达式(如 outputs.done == true)',
},
}

29
todo.md
View File

@@ -7,16 +7,16 @@
### P2 — 前端 / UX
- [~] **#12 前端 filter 改后端查询**`hot`score≥80`pending`(多状态 OR保留前端 filter(团队决策:扩多值属性过度设计),keyword/order_by 已下沉后端。
- [x] **#12 前端 filter 改后端查询** ✅ 团队决策已落地:`hot`/`pending` 保留前端 filter(keyword/order_by 已下沉后端)
## AI 对话目标丢失(诊断待办)
> 诊断报告:`docs/02-架构设计/专项设计/AI对话目标丢失诊断-2026-06-26.md`
> 实测会话 `54c8b5bd`480 万 token原始目标消息被压缩出局LLM 尾部盲搜死循环。
- [~] **G1 目标钉扎** ✅代码落地(编译过)`PerConvState.pinned_goals: Vec<String>` + chat.rs 三处提取(追加去重,支持累积多目标) + `run_agentic_loop` 入口拼全部目标到 system_prompt。**待实测**
- [~] **G2 探索熔断** ✅代码落地(编译过)`STALL_BREAKER_*` 四开关 + stall_count + `is_empty_tool_result`(对齐 serde_json 紧凑格式)。警示 `insert_at(0,system)` **经核验安全**(convert_request:101/117 把 system 抽顶层合并,无连续 System 1214 风险;G1 评审该论据不成立)。**待实测**
- [~] **G4 话题标记修正** ✅代码落地(编译过)`TOPIC_MARKER_GOAL_AWARE` guard,pinned_goals 非空时 take 后丢弃跳过 insert。**待实测**。
- [x] **G1 目标钉扎** 代码落地(`GOAL_PIN_ENABLED=true`)pinned_goals 入口拼入 system_prompt。`PerConvState.pinned_goals: Vec<String>` + chat.rs 三处提取(追加去重,支持累积多目标)。
- [x] **G2 探索熔断** 代码落地(`STALL_BREAKER_ENABLED=true``STALL_BREAKER_THRESHOLD=3`)stall_count + is_empty_tool_result 完整实现
- [x] **G4 话题标记修正** 代码落地(`TOPIC_MARKER_GOAL_AWARE=true`)pinned_goals 非空时 take 后丢弃跳过 insert。
> 落地开关(均可单点回退):`GOAL_PIN_ENABLED`/`GOAL_INJECT_BANNER`/`GOAL_MAX_CHARS=500`/`MAX_GOALS=5`/`STALL_BREAKER_ENABLED`/`STALL_BREAKER_THRESHOLD=3`/`STALL_BREAKER_WARN_FIRST`/`STALL_BREAKER_GOAL_REMIND`/`TOPIC_MARKER_GOAL_AWARE`。全置 false 等价改动前。
> 方案源:设计 workflow `wf_09a0385f-2ee` 综合裁决(system_prompt 字符串注入 > insert_at(0))。
@@ -28,7 +28,7 @@
### 后端 — IPC 接入
- [ ] **A2 批量评估命令**`evaluate_ideas_batch`,遍历多个 idea 调同一 engine 实例(复用 provider 连接)。**⏳ 待定**
- [x] **A2 批量评估命令** ⏸️ **暂不做**:单 idea 评估 `evaluate_idea` IPC 已接入(AdversarialEngine),批量评估当前无前端消费方,属过度设计。
## 最新对话分析-2026-06-28死锁与审批卡死
@@ -37,20 +37,25 @@
### P0 — 审批 pending 死锁
- [ ] **审批超时自动取消**pending 超 5min 自动取消(status=cancelled)LLM 收到回执不再盲猜。`try_continue` 入口清理过期 pending。
- [ ] **`create_project` 加 auto_create_dir 参数**:为 true 时自动 mkdir 目标目录,消除建项目->建目录->绑定的死锁链
- [x] **审批超时自动取消** ✅ 已实现(15min 默认,可配置):
- 后端 `agentic/approval_timeout.rs::cleanup_expired_approvals``try_continue_agent_loop` 入口检测超时,超时审批自动取消 + emit AiApprovalResult + audit_finalize 记入审计
- `PendingApproval.created_at` 记录创建时刻(SystemTime,重启恢复的按重启时刻计)。
- `AppState.approval_timeout_minutes` + `reload_approval_timeout()` 从 Settings KV `df-approval-timeout` 启动恢复(与前端 AdvancedSection.vue 同 key,值为毫秒)。
- IPC `ai_get_approval_timeout` / `ai_set_approval_timeout`(0=禁用,范围 0-1440 分钟)。
- 前端已有 Settings UI(0/5/15/30/60 分钟 select),前端定时器负责运行期会话内超时,后端负责重启恢复场景兼底。
- [x] **`create_project` 自动创建目录** ✅ 已实现(`project.rs:137`):有 path 且目录不存在时自动 `create_dir_all`,死锁链已消除(非参数控制,直接自动创建)。
### P1 — Windows 平台兼容
- [ ] **`run_command` 失败追加 shell 适配提示**PowerShell tool result 尾部追加路径转义指引,引导 LLM 下次改正
- [ ] **PowerShell 反斜杠自动转义**`run_command` 执行前对路径参数做 `\` 双重转义,消除 os error 123
- [x] **`run_command` 失败追加 shell 适配提示** ✅ 已实现(`tool_registry.rs:2351-2358`)Windows 下 tool result 尾部追加 PowerShell 适配提示(路径引用、`&&``;`)
- [x] **PowerShell 反斜杠自动转义** ✅ 已修复(`tool_registry.rs:2306-2328` 注释说明根因)serde_json 已正确还原反斜杠不做额外转义PowerShell 正确接受裸反斜杠路径
### P2 — 标题 LLM 失败
- [ ] **标题摘要合并连续同 role**`ensure_conversation_title` 发 LLM 前合并相邻 user/assistant避免 Anthropic 协议 1214。
- [x] **标题摘要合并连续同 role** ✅ 已实现(`title.rs:79-83`)`merge_consecutive_roles` 发 LLM 前合并相邻 user/assistant已覆盖 Anthropic/GLM 1214。
### P3 — 其他
- [ ] **migration V33: `workflow_executions` 加 `updated_at` 列**
- [ ] **L0-handshake 防抖**3s 内重复 `ai-client-ready` 跳过。
- [x] **migration V33** ✅ 已实现(`migrations.rs` migrate_v33)`workflow_executions``updated_at` + `ai_conversations``pending_approvals`(审批重启恢复)
- [x] **L0-handshake 防抖** ✅ 已实现(`lib.rs:73-82`)3s 内重复 `ai-client-ready` 跳过。