重构: 后端 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

@@ -7,7 +7,7 @@ use serde::Serialize;
use tauri::{AppHandle, Emitter, State};
use tokio::sync::broadcast::error::RecvError;
use df_core::events::{WorkflowEvent, HumanApprovalResponse};
use df_core::events::{WorkflowEvent, HumanApprovalResponse, SelectType};
use df_core::types::new_id;
use df_storage::crud::WorkflowRepo;
use df_storage::models::WorkflowRecord;
@@ -87,9 +87,23 @@ pub async fn run_workflow(
break;
}
}
// 消费过慢丢失部分事件时继续接收
// R-PD-13: Lagged 表示消费过慢broadcast 滑动窗口丢 n 条最旧事件。
// broadcast 不暴露被丢事件的具体类型,无法在此精确判断是否为关键终态事件
// NodeCompleted/NodeFailed/HumanApprovalRequest/WorkflowCompleted/WorkflowFailed
// 风险:若关键终态事件被丢,下面 emit 不到finished 判定不触发,循环不会 break
// 前端永久收不到工作流结束信号(依赖 DB 状态轮询兜底,但实时性差)。
// 最小兜底warn 记录丢失条数 + execution_id便于事后从 tracing 追溯。
// 后续优化方向(不在此 todo 范围):
// 1. 提升广播容量(目前常量值偏小,事件突发易满);
// 2. 关键终态事件经持久化队列/DB 重发receiver 启动时回放未消费事件;
// 3. forward 循环加 watchdog 超时(长期无事件则主动查 DB 终态并退出)。
Err(RecvError::Lagged(n)) => {
tracing::warn!("工作流事件消费滞后,丢失 {} 条", n);
tracing::warn!(
execution_id = %forward_exec_id,
lagged = n,
"工作流事件消费滞后,丢失 {} 条事件broadcast 不暴露被丢事件类型,关键终态事件可能永久丢失,依赖 DB 轮询兜底)",
n
);
}
Err(RecvError::Closed) => break,
}
@@ -176,6 +190,11 @@ pub async fn get_workflow_execution(
}
/// 发送人工审批响应
///
/// F-260615-01: 支持 single/multiple。
/// - single: decision 单值,decisions 为空(向后兼容);
/// - multiple: decisions 数组(≥1 项),decision 留空。
/// IPC 早校验规则与 HumanNode 下游兜底一致,避免「IPC 成功+工作流失败」割裂。
#[tauri::command]
pub async fn approve_human_approval(
app: AppHandle,
@@ -183,17 +202,55 @@ pub async fn approve_human_approval(
execution_id: String,
node_id: String,
decision: String,
// F-260615-01: 多选结果数组,缺省空数组(单选调用方不传)
decisions: Option<Vec<String>>,
comment: Option<String>,
// R-PD-5: options 由前端从收到的 HumanApprovalRequest 事件透传IPC 无法访问节点 config
options: Vec<String>,
// F-260615-01: 选择类型,缺省 single
select_type: Option<String>,
) -> Result<(), String> {
// decision 非空校验(FR-S3: 防 "" 透传;HumanNode options 非空时还校验 ∈ options)
if decision.trim().is_empty() {
return Err("审批决策不能为空".to_string());
let _ = &app; // 原签名含 app 参数(未使用),保留避免 invoke_handler 注册签名变化
let select_type = match select_type.as_deref() {
Some("multiple") => SelectType::Multiple,
_ => SelectType::Single,
};
let decisions = decisions.unwrap_or_default();
// 归一化决策集合(与 HumanNode 下游一致)
let mut picked: Vec<String> = decisions;
if picked.is_empty() && !decision.is_empty() {
picked.push(decision.clone());
}
// 发送审批响应到事件总线
// 数量校验
match select_type {
SelectType::Single if picked.len() != 1 => {
return Err("单选审批只能提交一个决策".to_string());
}
SelectType::Multiple if picked.is_empty() => {
return Err("多选审批至少提交一个决策".to_string());
}
_ => {}
}
// 每项非空 + ∈options(options 非空时)
for d in &picked {
if d.trim().is_empty() {
return Err("审批决策不能为空".to_string());
}
if !options.is_empty() && !options.contains(d) {
return Err(format!("审批决策非法: {}", d));
}
}
// 发送审批响应到事件总线(decision 取首项保留,decisions 透传全量)
let primary = picked.first().cloned().unwrap_or_default();
let response = HumanApprovalResponse {
execution_id: execution_id.clone(),
node_id: node_id.clone(),
decision,
decision: primary,
decisions: picked,
comment,
};
@@ -201,6 +258,7 @@ pub async fn approve_human_approval(
execution_id: response.execution_id,
node_id: response.node_id,
decision: response.decision,
decisions: response.decisions,
comment: response.comment,
};