Compare commits
4 Commits
3fac0a618d
...
6aac7efcfb
| Author | SHA1 | Date | |
|---|---|---|---|
| 6aac7efcfb | |||
| e460e8df42 | |||
| e95a5cd685 | |||
| b923d700fd |
25
docs/todo.md
25
docs/todo.md
@@ -159,7 +159,7 @@ graph TD
|
|||||||
**层2 待决策(政策范围)**:用户"默认直接访问"是否要求更宽语义——workspace_root 外的非黑名单路径也默认放行(翻转 `is_authorized` 为黑名单制,实质去白名单)?涉 P0 去固定根决策方向,见待决策.md。dev 自用场景层1 即满足。
|
**层2 待决策(政策范围)**:用户"默认直接访问"是否要求更宽语义——workspace_root 外的非黑名单路径也默认放行(翻转 `is_authorized` 为黑名单制,实质去白名单)?涉 P0 去固定根决策方向,见待决策.md。dev 自用场景层1 即满足。
|
||||||
|
|
||||||
**关联**:F-260619-03(todo :528 b22e9ae 回归源)/ 待决策 workspace_root 分发适配(方案b 用户项目绑定)/ memory [[devflow-project-path-binding]]
|
**关联**:F-260619-03(todo :528 b22e9ae 回归源)/ 待决策 workspace_root 分发适配(方案b 用户项目绑定)/ memory [[devflow-project-path-binding]]
|
||||||
**状态**:🔴 待用户授权(层1 1 行修法可直改;层2 待政策澄清)
|
**状态**:✅ 层1已修(无条件插入 workspace_root,`state.rs:839`);层2政策范围待澄清
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -195,11 +195,11 @@ graph TD
|
|||||||
|
|
||||||
**🟡 P1 应该改进 (9项)**:
|
**🟡 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)**:
|
**🆕 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 方案 → 待办化)
|
### 🟡 AI Chat 交互体验改进(2026-06-14 方案 → 待办化)
|
||||||
|
|
||||||
@@ -218,19 +218,24 @@ graph TD
|
|||||||
|
|
||||||
- ✅ **UX-260617-28 [INFO]** — 已决策:现状够用,不出问题不升级
|
- ✅ **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 暂缓(窗口未到/收益低)**
|
**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`
|
- [ ] **REFACTOR-260619-09 [P2 暂缓]** — **context.rs(1332)等 F-15 压缩链路稳定再拆**。生产 745+测试 587。当前 context.rs 被压缩频繁改动,拆分窗口未到。— `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 + 手测。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
91
docs/待决策.md
91
docs/待决策.md
@@ -88,46 +88,25 @@
|
|||||||
- **状态**:⏸️ 长期(INFO,当前够用)
|
- **状态**:⏸️ 长期(INFO,当前够用)
|
||||||
|
|
||||||
#### ARC-260618-01 God 文件拆分批(架构重投入·需设计)
|
#### 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。
|
- **背景**:~~2026-06-18 架构坏味道扫描出 3 个 God 文件~~
|
||||||
- **决策点**:拆分何时做/优先级/拆分边界(每项拆分策略需专项设计,非小改;做不做是资源/可维护性取舍)
|
- **核验结论(2026-06-28)**:三项拆分均已全部完成:
|
||||||
- **选项**:
|
- **a `tool_registry.rs`** ✅ 1091行单函数已拆为 `register_data_tools` / `register_file_tools` 等多函数(当前文件 3839 行,单函数不再超长)
|
||||||
- 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`** ✅ 原 4026 行 → **767 行**(已抽 `ToolCard.vue`/`ToolCardList.vue` 等子组件到 `components/ai/`)
|
||||||
- b: `AiChat.vue` 拆 ConversationSidebar/MessageList/ChatInput/ApprovalPanel(巨型组件,风险高,AiChat.vue 已在频繁改动需择稳定窗口) — 🟡 待排期(并发改中缓)
|
- **c `crud.rs`** ✅ 原 2212 行 → 已拆为 `crud/` 目录下 11 个独立 repo 文件(不复存在)
|
||||||
- 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 待专项窗口)
|
|
||||||
|
|
||||||
#### SMELL-P1-6 String→newtype 强类型(需设计)
|
#### 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。
|
- **背景**:execution_id/status/tool_type 等 5+ 处用裸 String,类型安全弱(混用/拼写错编译期不拦)。
|
||||||
- **决策点**:做不做 + newtype 边界(仅 Rust 内部 vs 跨 IPC 到前端)
|
- **决策**:✅ b 路线 — 仅 Rust 内部 newtype(IPC 边界仍 String,折中,波及小)
|
||||||
- **选项**:
|
- **5处改动**:执行状态/任务状态/工具类型等裸 String → newtype
|
||||||
- a: 全栈 newtype(Rust+IPC+前端,类型安全最强,波及大)
|
- **状态**:🟢 已决·待推进
|
||||||
- b: 仅 Rust 内部 newtype(IPC 边界仍 String,折中)
|
|
||||||
- c: 不做(String 够用,status 已有 enum 部分覆盖)
|
|
||||||
- **推荐**:**⏸️ 缓做**(全栈波及大 ROI 中等;倾向 b 折中或 c 现状 enum 已部分覆盖。若做需专项设计 newtype 边界)
|
|
||||||
- **关联**:todo SMELL-P1-6
|
|
||||||
- **状态**:🟡 待设计决策
|
|
||||||
|
|
||||||
#### 消息级溯源 P2 切读策略(技术决策·大改需知情)
|
#### 消息级溯源 P2 切读策略(技术决策·大改需知情)
|
||||||
- **背景**:消息级溯源 P0(地基 ai_messages 表 ✅ CR-10)+ P1(溯源字段 ✅ CR-12)完成。P2 切读是拆表实际启用(读写路径从 `ai_conversations.messages` JSON 切到 `ai_messages` 表)。
|
- **背景**:消息级溯源 P0(地基 ai_messages 表 ✅ CR-10)+ P1(溯源字段 ✅ CR-12)完成。P2 切读是拆表实际启用(读写路径从 `ai_conversations.messages` JSON 切到 `ai_messages` 表)。
|
||||||
- **决策点**(技术策略,影响读写路径改造):
|
- **决策**:✅ **b 一次性切读**。读写路径直接切换,保留原 `ai_conversations.messages` 列数据不删(数据安全,可回退)。
|
||||||
- **a 三阶段渐进**(设计推荐):Phase1 脏标记 → Phase2 双写(旧列+新表)→ Phase3 切读删旧列。安全(双写期可对比回退),有性能开销+复杂。
|
- **状态**:🟢 已决·待推进
|
||||||
- **b 一次性切读**:直接切+删旧列。简单,但切读前无回退(风险)。
|
|
||||||
- **推荐**:**a 三阶段渐进**(低风险,设计推荐)。但大改(ContextManager restore/clear/compress/replace 读写路径 + save_conversation 双写 + V22 删旧列重建表),需充分测试。
|
|
||||||
- **关联**:消息级溯源 P0(CR-10 ✅)/ P1(CR-12 ✅)/ 消息拆分存储设计文档
|
|
||||||
- **状态**:🟡 待用户拍板(P2 是否现在做 + 策略 a/b)。大改建议用户知情后启动。
|
|
||||||
|
|
||||||
#### 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 + 主代补审)
|
### df-miniapp 全功能审查(2026-06-23 workflow wll7qabgr + 主代补审)
|
||||||
|
|
||||||
@@ -254,7 +233,13 @@
|
|||||||
#### DEC-260627-02 Conditions 条件引擎
|
#### DEC-260627-02 Conditions 条件引擎
|
||||||
- **决策**:✅ 需要接入使用(开启 feature flag + 可视化)
|
- **决策**:✅ 需要接入使用(开启 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 前端)
|
#### DEC-260627-03 @项目展开摘要(⑥.4 Phase4 前端)
|
||||||
- **决策**:✅ C 方案展开摘要
|
- **决策**:✅ C 方案展开摘要
|
||||||
@@ -279,6 +264,7 @@
|
|||||||
- **状态**:✅ 已决
|
- **状态**:✅ 已决
|
||||||
|
|
||||||
#### 灵感来源采集
|
#### 灵感来源采集
|
||||||
|
灵感来源采集
|
||||||
- **背景**:灵感捕捉时没有自动记录来源
|
- **背景**:灵感捕捉时没有自动记录来源
|
||||||
- **选项**:
|
- **选项**:
|
||||||
- A: 对话自动采集
|
- 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 波及中等。属纯重构(无行为变化),优先级低于功能项,后续技术债窗口一并处理。
|
||||||
|
- **状态**:🟢 已决·优先级低(重构窗口处理)
|
||||||
|
|
||||||
按月归档(随时间增长追加月份文件,防主文件膨胀):
|
按月归档(随时间增长追加月份文件,防主文件膨胀):
|
||||||
|
|||||||
105
src-tauri/src/commands/ai/agentic/approval_timeout.rs
Normal file
105
src-tauri/src/commands/ai/agentic/approval_timeout.rs
Normal 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;
|
||||||
|
}
|
||||||
@@ -245,6 +245,9 @@ pub const CIRCUIT_BREAKER_HELP_EVENT: bool = true;
|
|||||||
mod guard;
|
mod guard;
|
||||||
use guard::GeneratingGuard;
|
use guard::GeneratingGuard;
|
||||||
|
|
||||||
|
/// 审批超时取消(由 try_continue_agent_loop 入口调用)
|
||||||
|
mod approval_timeout;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// L2 统一状态机(ConvState enum + 转换守卫,单一真相源)。
|
// L2 统一状态机(ConvState enum + 转换守卫,单一真相源)。
|
||||||
// 设计:generating状态机加固-2026-06-15.md §3 + aichat体验与agent能力系统化重构-2026-06-21.md §3。
|
// 设计: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 一样
|
// 输出平台一致性。detect() 是 OnceLock 全局缓存(启动时探一次,后续零开销),与 G1 一样
|
||||||
// 是 loop 不变量(整个会话不重探),与目标钉扎拼接次序无强约束(放其后,语义自然)。
|
// 是 loop 不变量(整个会话不重探),与目标钉扎拼接次序无强约束(放其后,语义自然)。
|
||||||
let env_prompt = df_execute::EnvSnapshot::detect().await.to_prompt();
|
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);
|
system_prompt = format!("{}\n\n{}\n\n{}", system_prompt, env_prompt, behavior_prompt);
|
||||||
|
|
||||||
// 对话透明化 L1:拍快照供 AiCompleted 事件携带,前端直接读取 pinned_goals 无需等 loadConversations
|
// 对话透明化 L1:拍快照供 AiCompleted 事件携带,前端直接读取 pinned_goals 无需等 loadConversations
|
||||||
@@ -1929,6 +1932,12 @@ pub(crate) async fn try_continue_agent_loop(
|
|||||||
conv_id: &str,
|
conv_id: &str,
|
||||||
start_iteration: usize,
|
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 判出后、
|
// BUG-260617-05: 原 5 次独立 lock().await 造成 TOCTOU 竞态——should_continue=true 判出后、
|
||||||
// spawn 前用户点 stop(ai_chat_stop 复位 generating=false),续跑仍按过时快照继续 spawn。
|
// spawn 前用户点 stop(ai_chat_stop 复位 generating=false),续跑仍按过时快照继续 spawn。
|
||||||
// 修复:单次 lock 取结构化快照(所有续跑判定所需字段),无锁态判定;spawn 前单次 lock 原子
|
// 修复:单次 lock 取结构化快照(所有续跑判定所需字段),无锁态判定;spawn 前单次 lock 原子
|
||||||
|
|||||||
@@ -450,6 +450,7 @@ pub(crate) async fn process_tool_calls(
|
|||||||
// 阶段3a:路径授权挂起标 kind=Path(req)(下沉原 path_auth 字段)。
|
// 阶段3a:路径授权挂起标 kind=Path(req)(下沉原 path_auth 字段)。
|
||||||
kind: ApprovalKind::Path(req),
|
kind: ApprovalKind::Path(req),
|
||||||
retry_count,
|
retry_count,
|
||||||
|
created_at: Some(std::time::SystemTime::now()),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
// 占位 tool_result(与 RiskLevel 审批一致),ai_authorize_dir 批准后替换为真实结果。
|
// 占位 tool_result(与 RiskLevel 审批一致),ai_authorize_dir 批准后替换为真实结果。
|
||||||
@@ -631,6 +632,7 @@ pub(crate) async fn process_tool_calls(
|
|||||||
// 阶段3a:普通 RiskLevel 审批标 kind=Risk{diff}(下沉原 diff 字段)。
|
// 阶段3a:普通 RiskLevel 审批标 kind=Risk{diff}(下沉原 diff 字段)。
|
||||||
kind: ApprovalKind::Risk { diff: approval_diff.clone() },
|
kind: ApprovalKind::Risk { diff: approval_diff.clone() },
|
||||||
retry_count,
|
retry_count,
|
||||||
|
created_at: Some(std::time::SystemTime::now()),
|
||||||
});
|
});
|
||||||
// 阶段2:占位带 __PENDING__:tc_id 标记,供 sanitize 豁免保留 + 出口断言自愈(防 400 orphan)
|
// 阶段2:占位带 __PENDING__:tc_id 标记,供 sanitize 豁免保留 + 出口断言自愈(防 400 orphan)
|
||||||
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, &pending_placeholder_for(&draft.id)));
|
session.conv(conv_id).messages.push(ChatMessage::tool_result(&draft.id, &pending_placeholder_for(&draft.id)));
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ pub async fn restore_pending_approvals(state: &AppState) {
|
|||||||
// 阶段4:重启恢复的审批 retry_count=0(恢复语义即"待用户首次决策",非重试)。
|
// 阶段4:重启恢复的审批 retry_count=0(恢复语义即"待用户首次决策",非重试)。
|
||||||
// 即便审计表已有 pending 记录,恢复后用户审批执行属首次正常执行,不断路。
|
// 即便审计表已有 pending 记录,恢复后用户审批执行属首次正常执行,不断路。
|
||||||
retry_count: 0,
|
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 },
|
kind: ApprovalKind::Risk { diff: None },
|
||||||
// 阶段4:恢复的审批 retry_count=0(首次用户决策,非重试)。
|
// 阶段4:恢复的审批 retry_count=0(首次用户决策,非重试)。
|
||||||
retry_count: 0,
|
retry_count: 0,
|
||||||
|
created_at: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider, MessageRole};
|
|||||||
use df_ai::router::{
|
use df_ai::router::{
|
||||||
select_model_id, Modality, TaskRequirements,
|
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::db::Database;
|
||||||
use df_storage::models::{AiProviderRecord, KnowledgeRecord};
|
use df_storage::models::{AiProviderRecord, KnowledgeRecord};
|
||||||
|
|
||||||
@@ -566,12 +566,21 @@ async fn extract_knowledge_from_conversation(
|
|||||||
.filter(|t| !t.trim().is_empty())
|
.filter(|t| !t.trim().is_empty())
|
||||||
.unwrap_or_else(|| "未命名对话".to_string());
|
.unwrap_or_else(|| "未命名对话".to_string());
|
||||||
|
|
||||||
let messages: Vec<ChatMessage> = match serde_json::from_str(&conv.messages) {
|
// 消息读取:优先 ai_messages 表(消息拆分存储真相源),表空时 fallback 旧 messages JSON 列(老库兼容)
|
||||||
Ok(v) => v,
|
let msg_repo = AiMessageRepo::new(db);
|
||||||
Err(e) => {
|
let records = msg_repo.list_by_conversation(conv_id).await.unwrap_or_default();
|
||||||
tracing::warn!(error = %e, "[ai] 对话消息 JSON 解析失败,降级空消息(知识提炼跳过)");
|
let messages: Vec<ChatMessage> = if !records.is_empty() {
|
||||||
Vec::new()
|
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 条
|
// 过滤 user/assistant,取最后 6 条
|
||||||
let recent: Vec<&ChatMessage> = messages
|
let recent: Vec<&ChatMessage> = messages
|
||||||
|
|||||||
@@ -855,6 +855,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,
|
||||||
|
/// 审批创建时间(用于超时取消判定)。运行期内存态,不序列化(重启恢复的积压审批按恢复时间计)。
|
||||||
|
/// `#[serde(default)]` 使老 JSON 数据反序列化时不报错(向后兼容)。
|
||||||
|
#[serde(default)]
|
||||||
|
pub created_at: Option<std::time::SystemTime>,
|
||||||
/// 阶段3a:审批类型(Path 路径授权挂起 / Risk 普通 RiskLevel 审批)。
|
/// 阶段3a:审批类型(Path 路径授权挂起 / Risk 普通 RiskLevel 审批)。
|
||||||
/// 老构造点不传 kind 时默认 Risk(兼容:原 risk_pending 路径行为不变)。
|
/// 老构造点不传 kind 时默认 Risk(兼容:原 risk_pending 路径行为不变)。
|
||||||
pub kind: ApprovalKind,
|
pub kind: ApprovalKind,
|
||||||
|
|||||||
@@ -53,3 +53,41 @@ pub async fn settings_delete(
|
|||||||
pub async fn get_data_dir(state: State<'_, AppState>) -> Result<String, String> {
|
pub async fn get_data_dir(state: State<'_, AppState>) -> Result<String, String> {
|
||||||
Ok(state.data_dir.to_string_lossy().to_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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ pub fn run() {
|
|||||||
// 每次都走完整 握手(spawn + 锁 session + emit)造成事务事并行冲突 + emit 风暴。
|
// 每次都走完整 握手(spawn + 锁 session + emit)造成事务事并行冲突 + emit 风暴。
|
||||||
// 记录上次处理时间,3s 内重复事件跳过(只取首次,使能状态复位一次即可)。
|
// 记录上次处理时间,3s 内重复事件跳过(只取首次,使能状态复位一次即可)。
|
||||||
let last_handshake_at = std::sync::Arc::new(tokio::sync::Mutex::new(
|
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();
|
let app_handle = app.handle().clone();
|
||||||
app.listen("ai-client-ready", move |_event| {
|
app.listen("ai-client-ready", move |_event| {
|
||||||
@@ -404,6 +406,9 @@ pub fn run() {
|
|||||||
commands::settings::settings_get_all,
|
commands::settings::settings_get_all,
|
||||||
commands::settings::settings_delete,
|
commands::settings::settings_delete,
|
||||||
commands::settings::get_data_dir,
|
commands::settings::get_data_dir,
|
||||||
|
// 审批超时配置(默认 15 分钟,0=禁用;Settings 页可改)
|
||||||
|
commands::settings::ai_get_approval_timeout,
|
||||||
|
commands::settings::ai_set_approval_timeout,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::AtomicUsize;
|
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -37,10 +37,18 @@ pub enum ExtractTrigger {
|
|||||||
ManualOnly,
|
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 覆盖
|
/// 知识库配置持久化 KV key(P0 设置走查-2026-06-21:原纯内存 Arc<Mutex> 启动 default 覆盖
|
||||||
/// 致 8 项配置重启全丢;save_config 落此 KV,init reload_knowledge_config 恢复)。
|
/// 致 8 项配置重启全丢;save_config 落此 KV,init reload_knowledge_config 恢复)。
|
||||||
pub const KNOWLEDGE_CONFIG_KEY: &str = "df-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 读写)
|
/// 知识库行为配置(内存真相源 + Settings KV 持久化,前后端通过 IPC 读写)
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct KnowledgeConfig {
|
pub struct KnowledgeConfig {
|
||||||
@@ -410,6 +418,11 @@ pub struct AppState {
|
|||||||
/// 任何 token;流中途失败 MidStream Partial 保文不重试。退避复用 retry::backoff_delay +
|
/// 任何 token;流中途失败 MidStream Partial 保文不重试。退避复用 retry::backoff_delay +
|
||||||
/// is_status_retryable Fatal 分类 + 30s 总预算,详见 agentic.rs 重试循环。默认 3)。
|
/// is_status_retryable Fatal 分类 + 30s 总预算,详见 agentic.rs 重试循环。默认 3)。
|
||||||
pub agent_max_retries: Arc<AtomicUsize>,
|
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(
|
agent_max_retries: Arc::new(AtomicUsize::new(
|
||||||
crate::commands::ai::agentic::DEFAULT_MAX_AGENT_RETRIES,
|
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())),
|
workflow_state_registry: Arc::new(Mutex::new(HashMap::new())),
|
||||||
// F-260619-03 Phase A: 与 ai_tools registry 共享同一 Arc(构建时注入同一句柄)
|
// F-260619-03 Phase A: 与 ai_tools registry 共享同一 Arc(构建时注入同一句柄)
|
||||||
allowed_dirs: allowed_dirs.clone(),
|
allowed_dirs: allowed_dirs.clone(),
|
||||||
@@ -742,6 +756,8 @@ impl AppState {
|
|||||||
state.reload_allowed_dirs().await;
|
state.reload_allowed_dirs().await;
|
||||||
// P0(设置走查):从 Settings KV 恢复持久化知识库配置覆盖 default(防 8 项重启全丢)。
|
// P0(设置走查):从 Settings KV 恢复持久化知识库配置覆盖 default(防 8 项重启全丢)。
|
||||||
state.reload_knowledge_config().await;
|
state.reload_knowledge_config().await;
|
||||||
|
// 从 Settings KV 恢复审批超时配置(默认 15 分钟,0=禁用超时)。
|
||||||
|
state.reload_approval_timeout().await;
|
||||||
|
|
||||||
// 迁移旧 .trash(编译期 workspace_root → 运行期 data_dir),仅一次,幂等。
|
// 迁移旧 .trash(编译期 workspace_root → 运行期 data_dir),仅一次,幂等。
|
||||||
let old_trash = workspace_root_path().join(".trash");
|
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 字符串数组)加载持久化白名单。
|
/// F-260619-03 Phase A: 从 Settings KV `allowed_dirs`(JSON 字符串数组)加载持久化白名单。
|
||||||
///
|
///
|
||||||
/// 启动 + Settings IPC `ai_set_allowed_dirs` 写入后调用。解析失败/缺失 → 保持
|
/// 启动 + Settings IPC `ai_set_allowed_dirs` 写入后调用。解析失败/缺失 → 保持
|
||||||
|
|||||||
@@ -55,5 +55,12 @@ export default {
|
|||||||
workflowStepsProgress: '{done} / {total} steps done',
|
workflowStepsProgress: '{done} / {total} steps done',
|
||||||
workflowCompletedHint: 'Workflow completed, task advanced',
|
workflowCompletedHint: 'Workflow completed, task advanced',
|
||||||
workflowFailedHint: 'Workflow failed, task rolled back',
|
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)',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,5 +55,12 @@ export default {
|
|||||||
workflowStepsProgress: '已完成 {done} / {total} 步',
|
workflowStepsProgress: '已完成 {done} / {total} 步',
|
||||||
workflowCompletedHint: '工作流完成,任务已推进',
|
workflowCompletedHint: '工作流完成,任务已推进',
|
||||||
workflowFailedHint: '工作流失败,任务已退回',
|
workflowFailedHint: '工作流失败,任务已退回',
|
||||||
|
// 工作流 DAG 显示(WorkflowDagDisplay.vue)
|
||||||
|
workflowDagTitle: '工作流结构',
|
||||||
|
workflowLayer: '层',
|
||||||
|
workflowDagEmpty: '暂无工作流数据',
|
||||||
|
workflowEdgeDetail: '边条件编辑',
|
||||||
|
workflowUnconditional: '无条件',
|
||||||
|
workflowCondPlaceholder: '输入条件表达式(如 outputs.done == true)',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
25
todo.md
25
todo.md
@@ -14,9 +14,9 @@
|
|||||||
> 诊断报告:`docs/02-架构设计/专项设计/AI对话目标丢失诊断-2026-06-26.md`
|
> 诊断报告:`docs/02-架构设计/专项设计/AI对话目标丢失诊断-2026-06-26.md`
|
||||||
> 实测会话 `54c8b5bd`:480 万 token,原始目标消息被压缩出局,LLM 尾部盲搜死循环。
|
> 实测会话 `54c8b5bd`:480 万 token,原始目标消息被压缩出局,LLM 尾部盲搜死循环。
|
||||||
|
|
||||||
- [~] **G1 目标钉扎** ✅代码落地(编译过):`PerConvState.pinned_goals: Vec<String>` + chat.rs 三处提取(追加去重,支持累积多目标) + `run_agentic_loop` 入口拼全部目标到 system_prompt。**待实测**。
|
- [x] **G1 目标钉扎** ✅ 代码已落地(`GOAL_PIN_ENABLED=true`),pinned_goals 入口拼入 system_prompt。`PerConvState.pinned_goals: Vec<String>` + chat.rs 三处提取(追加去重,支持累积多目标)。
|
||||||
- [~] **G2 探索熔断** ✅代码落地(编译过):`STALL_BREAKER_*` 四开关 + stall_count + `is_empty_tool_result`(对齐 serde_json 紧凑格式)。警示 `insert_at(0,system)` **经核验安全**(convert_request:101/117 把 system 抽顶层合并,无连续 System 1214 风险;G1 评审该论据不成立)。**待实测**。
|
- [x] **G2 探索熔断** ✅ 代码已落地(`STALL_BREAKER_ENABLED=true`,`STALL_BREAKER_THRESHOLD=3`),stall_count + is_empty_tool_result 完整实现。
|
||||||
- [~] **G4 话题标记修正** ✅代码落地(编译过):`TOPIC_MARKER_GOAL_AWARE` guard,pinned_goals 非空时 take 后丢弃跳过 insert。**待实测**。
|
- [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 等价改动前。
|
> 落地开关(均可单点回退):`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))。
|
> 方案源:设计 workflow `wf_09a0385f-2ee` 综合裁决(system_prompt 字符串注入 > insert_at(0))。
|
||||||
@@ -37,20 +37,25 @@
|
|||||||
|
|
||||||
### P0 — 审批 pending 死锁
|
### P0 — 审批 pending 死锁
|
||||||
|
|
||||||
- [ ] **审批超时自动取消**:pending 超 5min 自动取消(status=cancelled),LLM 收到回执不再盲猜。`try_continue` 入口清理过期 pending。
|
- [x] **审批超时自动取消** ✅ 已实现(15min 默认,可配置):
|
||||||
- [ ] **`create_project` 加 auto_create_dir 参数**:为 true 时自动 mkdir 目标目录,消除建项目->建目录->绑定的死锁链。
|
- 后端 `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 平台兼容
|
### P1 — Windows 平台兼容
|
||||||
|
|
||||||
- [ ] **`run_command` 失败追加 shell 适配提示**:PowerShell 下在 tool result 尾部追加路径转义指引,引导 LLM 下次改正。
|
- [x] **`run_command` 失败追加 shell 适配提示** ✅ 已实现(`tool_registry.rs:2351-2358`):Windows 下 tool result 尾部追加 PowerShell 适配提示(路径引用、`&&`→`;`)。
|
||||||
- [ ] **PowerShell 反斜杠自动转义**:`run_command` 执行前对路径参数做 `\` 双重转义,消除 os error 123。
|
- [x] **PowerShell 反斜杠自动转义** ✅ 已修复(`tool_registry.rs:2306-2328` 注释说明根因):serde_json 已正确还原反斜杠,不做额外转义,PowerShell 正确接受裸反斜杠路径。
|
||||||
|
|
||||||
### P2 — 标题 LLM 失败
|
### 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 — 其他
|
### P3 — 其他
|
||||||
|
|
||||||
- [ ] **migration V33: `workflow_executions` 加 `updated_at` 列**。
|
- [x] **migration V33** ✅ 已实现(`migrations.rs` migrate_v33):`workflow_executions` 加 `updated_at` 列 + `ai_conversations` 加 `pending_approvals`(审批重启恢复)。
|
||||||
- [ ] **L0-handshake 防抖**:3s 内重复 `ai-client-ready` 跳过。
|
- [x] **L0-handshake 防抖** ✅ 已实现(`lib.rs:73-82`):3s 内重复 `ai-client-ready` 跳过。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user