重构: 后端 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:
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user