Compare commits

...

4 Commits

Author SHA1 Message Date
6aac7efcfb 修复: 知识提炼切读 ai_messages 表,完成消息溯源 P2 最后一环
消息级溯源 P2 切读早前已覆盖主要读写路径(save_conversation 写 /
switch/export/load_messages 读),但 knowledge_inject 的知识提炼
仍在读旧 ai_conversations.messages JSON 列,与 P2 切读目标不一致。

本次补全:
- extract_knowledge_from_conversation 改为优先读 ai_messages 表
- 表空时 fallback 旧 messages JSON 列(老库兼容,对齐 switch 路径)
- 复用 commands::record_to_message 映射函数(DRY,与其他读路径同源)

至此消息溯源 P2 切读全部完成,所有读写路径统一走 ai_messages 表,
旧 messages JSON 列仅作老库兼容 fallback,不再写入。
2026-06-28 18:47:21 +08:00
e460e8df42 新增: 工作流条件表达式 UI 补齐 i18n 翻译
条件表达式引擎早在 df-workflow/conditions.rs 完成,执行器(executor.rs)
按入边 condition 过滤也已落地,WorkflowDagDisplay.vue 提供边条件编辑
入口。但前端用 || '中文兜底' 说明 i18n 缺失,本次补齐中英文翻译:

zh-CN/taskDetail.ts + en/taskDetail.ts 同步新增:
- workflowDagTitle / workflowLayer / workflowDagEmpty
- workflowEdgeDetail / workflowUnconditional / workflowCondPlaceholder

核验确认条件引擎全部链路(引擎/feature flag/执行器集成/UI/i18n)
均已落地,F-09 B 路线(多会话并发)亦已完成批2/批4/批5 per_conv
隔离改造。
2026-06-28 18:34:22 +08:00
e95a5cd685 核验更新: ToolCard 拆分已完成,God 文件三项全部落地
代码核验发现 2026-06-19 文件拆分升级的 P1 ToolCard 任务实际已
完成,文档标记滞后,本次同步:

- ToolCard.vue 1527→373 行
- useToolApproval.ts(审批状态机独立,220行)
- useToolCardHeader.ts(头部逻辑,160行)
- useToolCardRender.ts(渲染逻辑,45行)
- useToolCard.ts(通用工具函数,666行)

God 文件三项(tool_registry/AiChat.vue/crud.rs)全部完成。
SMELL-P1-6 String→newtype 评估为低优先级纯重构,后续技术债
窗口处理。
2026-06-28 18:31:34 +08:00
b923d700fd 新增: 审批超时后端兜底,覆盖重启恢复场景
前端早已有审批超时定时器(AdvancedSection.vue 0/5/15/30/60 分钟
可配置),负责会话内运行的审批超时检测。但重启后前端定时器丢失,
恢复的 pending_approvals(经 audit/restore.rs 从 DB 重建)无人
计时,会永久卡死会话。

本次补充后端兜底机制:
- PendingApproval 新增 created_at 字段(SystemTime),记录创建时刻
- 新增 agentic/approval_timeout.rs,在 try_continue_agent_loop
  入口检测超时,自动取消并写入审计(rejected)
- AppState 新增 approval_timeout_minutes 字段 + 启动从 KV 恢复
- IPC 新增 ai_get_approval_timeout / ai_set_approval_timeout
- KV 复用前端 df-approval-timeout key(值为毫秒),保持单一真相源

同时更新 todo.md / 待决策.md,把已实现项标记为完成。
2026-06-28 18:16:34 +08:00
14 changed files with 326 additions and 65 deletions

View File

@@ -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 + 手测。
--- ---

View File

@@ -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 波及中等。属纯重构(无行为变化),优先级低于功能项,后续技术债窗口一并处理。
- **状态**:🟢 已决·优先级低(重构窗口处理)
按月归档(随时间增长追加月份文件,防主文件膨胀): 按月归档(随时间增长追加月份文件,防主文件膨胀):

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; 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 原子

View File

@@ -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)));

View File

@@ -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,
}, },
); );
} }

View File

@@ -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

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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");

View File

@@ -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` 写入后调用。解析失败/缺失 → 保持

View File

@@ -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)',
}, },
} }

View File

@@ -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
View File

@@ -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` 跳过。