重构: 后端 df-ai/commands 拆分+df-nodes/workflow 改造+P0 bug 修复

- df-ai: context 历史中毒三档自愈 sanitize_messages(AC3)+anthropic_compat tool_use_id None 跳过(AC1/AC2)+删 router/stream 死码
- df-core: events 加 select_type+decisions 多选审批契约(F-260615-01)
- df-execute: shell run_command 工具复用(F-260615-05)
- df-nodes: human_node 多选校验+2 端到端测(F-01)+取消跳 set_failed(B-03b-R1/R2/R8)
- df-workflow: executor/dag/state cancel 闭环(B-06/07/03a/b)+provider approve options(R-PD-5)
- df-storage: find_path_conflict 抽公共(R-PD-11)+COLS 常量断言
- df-ideas: 删 IdeaPromoter/PromotionPolicy 死码(R-PD-14)
- src-tauri/commands/ai: secret keyring 迁移(FR-S1/R-PD-4)+GeneratingGuard RAII+disarm(B-09/26)+newConversation 软复位(B-10)+stream 心跳/stop select/空回复判错(B-02/04/05/15)+run_command(F-05)+mask audit(AR-3)
- src-tauri/commands/{project,task,workflow,mod,lib,state}: task detail IPC(F-02)+approve decisions+task list 联动(B-29)
- Cargo.lock+Cargo.toml 依赖同步
This commit is contained in:
2026-06-15 05:14:42 +08:00
parent 04032a2a8d
commit 2de0c6ecb7
37 changed files with 2457 additions and 484 deletions

View File

@@ -26,6 +26,7 @@ pub mod commands;
pub mod conversation;
pub mod knowledge_inject;
pub mod prompt;
pub mod secret;
pub mod skills;
pub mod stream_recv;
pub mod title;
@@ -84,12 +85,32 @@ pub enum AiChatEvent {
AiError { error: String, conversation_id: Option<String> },
/// Agent 循环新一轮(前端需新建 assistant 消息)
AiAgentRound { round: u32, conversation_id: Option<String> },
/// 流式心跳(静默期报活,前端 watchdog reset区分「LLM 在跑」与「真断」)
AiHeartbeat { conversation_id: Option<String> },
}
// ============================================================
// 会话状态(放 mod.rs,各子文件经 use super::* 拿到)
// ============================================================
/// AI 会话读侧状态视图(只读枚举,由 [`AiSession::session_state`] 推导)
///
/// 这是 **只读视图**,不替代 `AiSession` 上 `generating` / `pending_approvals` /
/// `stop_flag` 等字段定义——字段仍是唯一真相源,调用方写状态时继续写字段,
/// 读状态时统一走 `session_state()` 收敛判断逻辑(避免散布的 `if generating` /
/// `if !pending_approvals.is_empty()` 三路组合在各调用点各自实现)。
///
/// 优先级:`pending_approvals` 非空(有挂起审批优先报 AwaitingApproval
/// 即使同时在流式)> `generating`Streaming> 都不满足Idle
pub enum SessionState {
/// 空闲:既未生成、也无挂起审批
Idle,
/// 流式生成中generating 为 true 且无挂起审批
Streaming,
/// 等待审批pending_approvals 非空
AwaitingApproval,
}
/// AI 会话内状态Mutex 保护)
pub struct AiSession {
/// 对话历史ContextManager唯一消息真相源裁剪仅影响发送视图不影响持久化
@@ -100,7 +121,17 @@ pub struct AiSession {
pub active_conversation_id: Option<String>,
/// 活跃对话创建时间(懒创建:首条消息落库前仅存内存,upsert 时用作 created_at)
pub active_conv_created_at: Option<String>,
/// 挂起的审批tool_call_id → 审批信息)
/// 挂起的审批
///
/// **结构**:单层 `HashMap<tool_call_id, PendingApproval>`,按 `tool_call_id`
/// 路由审批结果(`tool_call_id` 是路由键)。`PendingApproval.conversation_id`
/// 是业务语义哪个对话产生的审批而非路由键——IPC 端按 `tool_call_id` 精确命中。
///
/// **现状**`ai_pending_tool_calls` 等读取路径按 `conversation_id` 做 O(n) 线性
/// 过滤。在典型场景(单对话、少量并发审批)下 n 极小O(n) 可接受,无需二级索引。
///
/// **未来**:若多会话并发、审批量显著增大,可考虑加二级索引
/// `conversation_id → Vec<tool_call_id>`)把按会话过滤降到 O(1)。
pub pending_approvals: HashMap<String, PendingApproval>,
/// 是否正在生成
pub generating: bool,
@@ -123,6 +154,31 @@ impl AiSession {
stop_flag: Arc::new(AtomicBool::new(false)),
}
}
/// 推导会话读侧状态视图(只读,不写任何字段)。
///
/// 优先级:`pending_approvals` 非空 → [`SessionState::AwaitingApproval`]
/// 否则 `generating` → [`SessionState::Streaming`];否则 → [`SessionState::Idle`]。
///
/// 收敛点:调用方读「会话处于什么阶段」时统一走本方法,替代散布的
/// `if !pending_approvals.is_empty() / if generating` 三路组合。
///
/// # 待替换调用点(本次仅新增视图,不替换调用点)
///
/// - `agentic.rs:283` — agent loop 入口/恢复处对 generating + 审批的组合判断
/// - `commands.rs:250` — `ai_pending_tool_calls` 等读取前的状态门控
/// - `commands.rs:451` — 审批提交/会话复位路径的状态判断
///
/// 上述三处替换由 P0 任务承接,本方法先就位供其切换。
pub fn session_state(&self) -> SessionState {
if !self.pending_approvals.is_empty() {
SessionState::AwaitingApproval
} else if self.generating {
SessionState::Streaming
} else {
SessionState::Idle
}
}
}
/// 待审批的工具调用