478 lines
21 KiB
Rust
478 lines
21 KiB
Rust
//! 工作流相关命令 — 触发执行、查询执行记录
|
||
//!
|
||
//! 事件转发:run_workflow 在执行前订阅事件总线,
|
||
//! 将 WorkflowEvent 包装为 `{ execution_id, event }` 通过 `workflow-event` 事件发给前端。
|
||
|
||
use serde::Serialize;
|
||
use tauri::{AppHandle, Emitter, State};
|
||
use tokio::sync::broadcast::error::RecvError;
|
||
|
||
use df_core::events::{WorkflowEvent, HumanApprovalResponse, SelectType};
|
||
use df_core::types::{new_id, NodeStatus};
|
||
use df_nodes::task_advance_node::advance_task_atomic;
|
||
// F-260616-06 ①-1: 空 dag + target_status 时按目标态自动选推进链模板
|
||
use df_nodes::task_workflow_templates::template_for;
|
||
use df_storage::crud::{TaskRepo, WorkflowRepo};
|
||
use df_storage::models::WorkflowRecord;
|
||
use df_workflow::dag_def::DagDef;
|
||
use df_workflow::executor::DagExecutor;
|
||
|
||
use crate::state::AppState;
|
||
|
||
use super::{err_str, now_millis};
|
||
|
||
/// 转发到前端的事件载荷
|
||
#[derive(Debug, Clone, Serialize)]
|
||
struct WorkflowEventPayload {
|
||
/// 工作流执行记录 ID
|
||
execution_id: String,
|
||
/// 原始工作流事件(serde tag = "type")
|
||
event: WorkflowEvent,
|
||
}
|
||
|
||
/// F-260616-06 ②-4: 工作流失败时按 target_status 推算任务退回态。
|
||
///
|
||
/// 映射表(失败退一步):
|
||
/// - testing → in_review
|
||
/// - in_review → in_progress
|
||
/// - in_progress → None(状态机禁止→todo,留 in_progress 等人介入)
|
||
/// - 其他(done/blocked/cancelled/todo 等) → None(无退回映射,跳过回调)
|
||
///
|
||
/// 设计选择:in_progress → None(而非 todo)。理由:状态机禁止 in_progress→todo
|
||
/// (task_state_machine.rs backward_to_todo_rejected),失败退回 todo 会被
|
||
/// advance_task_atomic 的 InvalidState 拦截,任务原地保留 in_progress 且无信号;
|
||
/// 改为 None 则回调跳过推进,留 in_progress 等人介入。
|
||
/// 起点状态 todo 无可退态 → None。
|
||
fn regression_target(target: &str) -> Option<&str> {
|
||
match target {
|
||
"testing" => Some("in_review"),
|
||
"in_review" => Some("in_progress"),
|
||
// in_progress 失败不自动退回(状态机不允许→todo),留 in_progress 等人介入
|
||
"in_progress" => None,
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// 触发工作流执行(核心命令)
|
||
///
|
||
/// 流程:build_dag 校验 → 写入执行记录(status=running) → 后台异步执行 →
|
||
/// 事件经 EventBus 转发到前端 → 完成后更新执行记录状态。
|
||
/// 立即返回执行记录 ID,前端凭此关联后续事件。
|
||
///
|
||
/// F-260616-06 阶段2 工作流联动(②-2):
|
||
/// - `task_id` / `target_status` 同时 Some 时,完成后按 target_status 推进任务(②-3),
|
||
/// 失败时按 regression_target 退回(②-4)。
|
||
/// - Option 向后兼容:旧调用方不传即 None,不触发任何任务回调,零行为破坏。
|
||
#[tauri::command]
|
||
pub async fn run_workflow(
|
||
app: AppHandle,
|
||
state: State<'_, AppState>,
|
||
name: String,
|
||
dag: DagDef,
|
||
config: serde_json::Value,
|
||
// F-260616-06 ②-2: 工作流联动任务的关联 ID 与目标态(同时 Some 才联动)
|
||
task_id: Option<String>,
|
||
target_status: Option<String>,
|
||
) -> Result<String, String> {
|
||
// 0. F-260616-06 ①-1: DagDef 来源选模板(方案A·最小)。
|
||
// 规则:
|
||
// - dag.nodes 非空 → 用传入 dag(向后兼容,旧调用方/编辑器自定义工作流不变)
|
||
// - dag.nodes 为空 + target_status Some → 调 template_for 自动选推进链模板;
|
||
// 模板不存在(target 非三合法态) → Err「无对应工作流模板」
|
||
// - dag.nodes 为空 + target_status None → 原行为(空 dag 让 build_dag 自行报错,零新分支)
|
||
// 前端选 target_status 后传空 dag,零模板知识零新 IPC,后端收敛选模板逻辑。
|
||
let dag = if dag.nodes.is_empty() {
|
||
match target_status.as_deref() {
|
||
Some(target) => template_for(target)
|
||
.ok_or_else(|| format!("无对应工作流模板: target_status={}", target))?,
|
||
None => dag, // 空dag+无target: 走原路径由 build_dag 校验
|
||
}
|
||
} else {
|
||
dag
|
||
};
|
||
|
||
// 1. 先构建运行时 DAG,校验失败直接返回,不落库
|
||
let runtime_dag = state.registry.build_dag(&dag).map_err(err_str)?;
|
||
|
||
// 2. 写入执行记录(status=running)
|
||
let execution_id = new_id();
|
||
let dag_json = serde_json::to_string(&dag).map_err(err_str)?;
|
||
let record = WorkflowRecord {
|
||
id: execution_id.clone(),
|
||
name,
|
||
dag_json,
|
||
status: "running".to_string(),
|
||
triggered_by: Some("manual".to_string()),
|
||
project_id: None,
|
||
task_id: task_id.clone(),
|
||
created_at: now_millis(),
|
||
completed_at: None,
|
||
};
|
||
state
|
||
.workflows
|
||
.insert(record)
|
||
.await
|
||
.map_err(err_str)?;
|
||
|
||
// 3. 订阅事件总线,把事件转发给前端(收到完成/失败事件后退出)
|
||
let mut rx = state.event_bus.subscribe();
|
||
let forward_app = app.clone();
|
||
let forward_exec_id = execution_id.clone();
|
||
// B-260615-35: forward 任务内需查 DB 终态兜底 Lagged 丢终态事件,故 move 进 db 句柄
|
||
let forward_db = state.db.clone();
|
||
tauri::async_runtime::spawn(async move {
|
||
// Lagged 累计次数(用于"累计达阈值"触发兜底查询)
|
||
let mut lagged_total: u64 = 0;
|
||
// Lagged 单次/累计触发 DB 终态查询兜底的阈值
|
||
const LAGGED_PROBE_THRESHOLD: u64 = 8;
|
||
loop {
|
||
match rx.recv().await {
|
||
Ok(event) => {
|
||
let finished = matches!(
|
||
event,
|
||
WorkflowEvent::WorkflowCompleted { .. }
|
||
| WorkflowEvent::WorkflowFailed { .. }
|
||
);
|
||
let payload = WorkflowEventPayload {
|
||
execution_id: forward_exec_id.clone(),
|
||
event,
|
||
};
|
||
if let Err(e) = forward_app.emit("workflow-event", &payload) {
|
||
tracing::warn!("工作流事件转发失败: {}", e);
|
||
}
|
||
if finished {
|
||
break;
|
||
}
|
||
}
|
||
// R-PD-13: Lagged 表示消费过慢,broadcast 滑动窗口丢 n 条最旧事件。
|
||
// broadcast 不暴露被丢事件的具体类型,无法在此精确判断是否为关键终态事件。
|
||
// 风险:若关键终态事件(WorkflowCompleted/WorkflowFailed)被丢,finished 判定不触发,
|
||
// 循环不会 break,前端永久收不到工作流结束信号。
|
||
// B-260615-35 兜底:收到 Lagged 时,累计次数或单次 n 达阈值则查 DB 终态,
|
||
// 若工作流已终态则补发对应 workflow-event 并 break 退出 forward。
|
||
// 避免依赖实时性差的 DB 轮询兜底导致前端审批/完成弹窗卡死。
|
||
Err(RecvError::Lagged(n)) => {
|
||
lagged_total = lagged_total.saturating_add(n);
|
||
tracing::warn!(
|
||
execution_id = %forward_exec_id,
|
||
lagged = n,
|
||
lagged_total = lagged_total,
|
||
"工作流事件消费滞后,丢失 {} 条事件(broadcast 不暴露被丢事件类型,关键终态事件可能已丢失)",
|
||
n
|
||
);
|
||
if lagged_total < LAGGED_PROBE_THRESHOLD {
|
||
continue;
|
||
}
|
||
// 累计/单次达阈值:查 DB 终态兜底
|
||
let workflows = WorkflowRepo::new(&forward_db);
|
||
match workflows.get_by_id(&forward_exec_id).await {
|
||
Ok(Some(record)) => {
|
||
let terminal = match record.status.as_str() {
|
||
"completed" => Some(WorkflowEvent::WorkflowCompleted {
|
||
total_duration_ms: 0,
|
||
}),
|
||
"failed" => Some(WorkflowEvent::WorkflowFailed {
|
||
error: "工作流执行失败(Lagged 兜底补发,详情见 DB)"
|
||
.to_string(),
|
||
failed_node: String::new(),
|
||
}),
|
||
"cancelled" => Some(WorkflowEvent::WorkflowFailed {
|
||
error: "工作流被取消(Lagged 兜底补发)".to_string(),
|
||
failed_node: String::new(),
|
||
}),
|
||
_ => None,
|
||
};
|
||
if let Some(synth_event) = terminal {
|
||
let payload = WorkflowEventPayload {
|
||
execution_id: forward_exec_id.clone(),
|
||
event: synth_event.clone(),
|
||
};
|
||
if let Err(e) = forward_app.emit("workflow-event", &payload) {
|
||
tracing::warn!("Lagged 终态兜底事件转发失败: {}", e);
|
||
}
|
||
tracing::info!(
|
||
execution_id = %forward_exec_id,
|
||
status = %record.status,
|
||
"Lagged 兜底命中终态,补发 {} 并退出 forward",
|
||
match synth_event {
|
||
WorkflowEvent::WorkflowCompleted { .. } => "WorkflowCompleted",
|
||
_ => "WorkflowFailed",
|
||
}
|
||
);
|
||
break;
|
||
}
|
||
// DB 仍非终态(running),重置累计计数继续等待后续事件
|
||
lagged_total = 0;
|
||
}
|
||
Ok(None) => {
|
||
tracing::warn!(
|
||
execution_id = %forward_exec_id,
|
||
"Lagged 兜底查询未找到执行记录,继续等待事件"
|
||
);
|
||
lagged_total = 0;
|
||
}
|
||
Err(e) => {
|
||
tracing::warn!(
|
||
execution_id = %forward_exec_id,
|
||
error = %e,
|
||
"Lagged 兜底查询 DB 失败,继续等待事件"
|
||
);
|
||
lagged_total = 0;
|
||
}
|
||
}
|
||
}
|
||
Err(RecvError::Closed) => break,
|
||
}
|
||
}
|
||
});
|
||
|
||
// 4. 后台异步执行 DAG,完成后更新执行记录状态
|
||
let event_bus = state.event_bus.clone();
|
||
let db = state.db.clone();
|
||
let exec_id = execution_id.clone();
|
||
let state_registry = state.workflow_state_registry.clone();
|
||
// F-260616-06 ②-2: move task_id / target_status 进闭包供完成/失败回调使用
|
||
let cb_task_id = task_id.clone();
|
||
let cb_target_status = target_status.clone();
|
||
tauri::async_runtime::spawn(async move {
|
||
let mut executor = DagExecutor::new(event_bus.clone(), exec_id.clone());
|
||
// 注册执行器状态机:StateMachine 内部 Arc<Mutex>,clone 共享底层 HashMap,
|
||
// cancel_workflow_node IPC 经 execution_id 取此引用 set_cancelled,直达运行中 HumanNode
|
||
state_registry
|
||
.lock()
|
||
.await
|
||
.insert(exec_id.clone(), executor.state_machine());
|
||
let result = executor.run(&runtime_dag, config).await;
|
||
|
||
let (status, error) = match &result {
|
||
Ok(_) => ("completed", None),
|
||
Err(e) => ("failed", Some(format!("{:#}", e))),
|
||
};
|
||
|
||
// 更新执行记录(Repo 内部仅持有 Arc 连接,重建开销可忽略)
|
||
let workflows = WorkflowRepo::new(&db);
|
||
if let Err(e) = workflows.update_field(&exec_id, "status", status).await {
|
||
tracing::error!("更新工作流状态失败: {}", e);
|
||
}
|
||
if let Err(e) = workflows
|
||
.update_field(&exec_id, "completed_at", &now_millis())
|
||
.await
|
||
{
|
||
tracing::error!("更新工作流完成时间失败: {}", e);
|
||
}
|
||
|
||
// F-260616-06 ②-3 / ②-4: 工作流联动任务推进回调
|
||
// - 成功(completed): task_id 与 target_status 都 Some 时推进到 target_status
|
||
// - 失败(failed): 按 regression_target 推算退回态推进;无映射则跳过
|
||
// task_id / target_status 任一 None → 跳过(向后兼容,旧工作流不联动任务)
|
||
// 回调失败仅 tracing::warn!,不回滚(工作流成功语义与任务推进解耦 — 回调失败不撤销
|
||
// 已完成工作流,前端可后续手动处理)
|
||
match (cb_task_id.as_ref(), cb_target_status.as_deref()) {
|
||
(Some(tid), Some(target)) => {
|
||
let advance_target = match status {
|
||
"completed" => Some(target),
|
||
"failed" => regression_target(target),
|
||
_ => None,
|
||
};
|
||
if let Some(to) = advance_target {
|
||
let repo = TaskRepo::new(&db);
|
||
if let Err(e) = advance_task_atomic(&repo, tid, to).await {
|
||
tracing::warn!(
|
||
execution_id = %exec_id,
|
||
task_id = %tid,
|
||
target_status = %to,
|
||
workflow_status = %status,
|
||
"工作流联动推进任务失败(不回滚): {}",
|
||
e
|
||
);
|
||
} else {
|
||
tracing::info!(
|
||
execution_id = %exec_id,
|
||
task_id = %tid,
|
||
target_status = %to,
|
||
workflow_status = %status,
|
||
"工作流联动推进任务成功"
|
||
);
|
||
}
|
||
} else {
|
||
// failed 且无退回映射:warn 跳过(常见:target 是 todo 起点无可退态)
|
||
tracing::warn!(
|
||
execution_id = %exec_id,
|
||
task_id = %tid,
|
||
target_status = %target,
|
||
"工作流失败但 target_status 无退回映射,跳过任务回调"
|
||
);
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
|
||
// 执行结束(成功/失败)清理状态注册表,防内存泄漏
|
||
state_registry.lock().await.remove(&exec_id);
|
||
|
||
// 执行失败时补发 WorkflowFailed(执行器内部只发 NodeFailed)
|
||
// failed_node 从状态机取首个失败/取消节点(R9⑪: 原 String::new() 空值)
|
||
if let Some(error) = error {
|
||
let failed_node = executor
|
||
.state_machine()
|
||
.snapshot()
|
||
.into_iter()
|
||
.find(|(_, s)| matches!(s, NodeStatus::Failed | NodeStatus::Cancelled))
|
||
.map(|(id, _)| id)
|
||
.unwrap_or_default();
|
||
event_bus
|
||
.send(WorkflowEvent::WorkflowFailed {
|
||
error,
|
||
failed_node,
|
||
})
|
||
.await;
|
||
}
|
||
});
|
||
|
||
Ok(execution_id)
|
||
}
|
||
|
||
/// 列出全部工作流执行记录
|
||
#[tauri::command]
|
||
pub async fn list_workflow_executions(
|
||
state: State<'_, AppState>,
|
||
) -> Result<Vec<WorkflowRecord>, String> {
|
||
state.workflows.list_all().await.map_err(err_str)
|
||
}
|
||
|
||
/// 按 ID 查询工作流执行记录
|
||
#[tauri::command]
|
||
pub async fn get_workflow_execution(
|
||
state: State<'_, AppState>,
|
||
id: String,
|
||
) -> Result<Option<WorkflowRecord>, String> {
|
||
state
|
||
.workflows
|
||
.get_by_id(&id)
|
||
.await
|
||
.map_err(err_str)
|
||
}
|
||
|
||
/// 发送人工审批响应
|
||
///
|
||
/// 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,
|
||
state: State<'_, AppState>,
|
||
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> {
|
||
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: primary,
|
||
decisions: picked,
|
||
comment,
|
||
};
|
||
|
||
let event = WorkflowEvent::HumanApprovalResponse {
|
||
execution_id: response.execution_id,
|
||
node_id: response.node_id,
|
||
decision: response.decision,
|
||
decisions: response.decisions,
|
||
comment: response.comment,
|
||
};
|
||
|
||
// 直接发送到全局事件总线
|
||
state.event_bus.send(event).await;
|
||
Ok(())
|
||
}
|
||
|
||
/// 取消工作流节点(人工审批取消)
|
||
///
|
||
/// 从执行器状态机注册表取出共享引用,调 `set_cancelled` 置目标节点为 Cancelled。
|
||
/// StateMachine 内部 Arc<Mutex> 共享底层 HashMap,IPC 写入直达运行中阻塞节点(HumanNode)
|
||
/// 的 select! 轮询分支,其 is_cancelled 检测到后返回 Err "人工审批被取消"。
|
||
///
|
||
/// 终态前置守卫:仅 Pending(排队中)/ Running(执行中,含阻塞等审批)允许取消;
|
||
/// 终态节点(Completed/Failed/Skipped/Cancelled)返 Err,避免静默覆盖终态。
|
||
/// 守卫放 IPC 层(非 set_cancelled 内):保留 set_cancelled 作为 executor 内部受控旁路语义,
|
||
/// 让状态变更的合法入口收敛到 IPC 这一道。
|
||
#[tauri::command]
|
||
pub async fn cancel_workflow_node(
|
||
state: State<'_, AppState>,
|
||
execution_id: String,
|
||
node_id: String,
|
||
) -> Result<(), String> {
|
||
// 经 execution_id 取执行器状态机(与 NodeContext.node_status 共享同一 HashMap)
|
||
let registry = state.workflow_state_registry.lock().await;
|
||
let sm = match registry.get(&execution_id) {
|
||
Some(sm) => sm,
|
||
None => return Err(format!("工作流 {} 不存在或已结束", execution_id)),
|
||
};
|
||
// 终态守卫:get() 返回缺失条目默认 Pending(尚未执行)→ 允许取消
|
||
let current = sm.get(&node_id);
|
||
match current {
|
||
NodeStatus::Pending | NodeStatus::Running | NodeStatus::Waiting => {
|
||
sm.set_cancelled(node_id.clone());
|
||
tracing::info!(
|
||
"工作流节点取消:execution_id={}, node_id={}",
|
||
execution_id,
|
||
node_id
|
||
);
|
||
Ok(())
|
||
}
|
||
// 终态:不允许取消(避免静默覆盖终态)
|
||
NodeStatus::Completed | NodeStatus::Failed | NodeStatus::Skipped | NodeStatus::Cancelled => {
|
||
Err(format!(
|
||
"节点 {} 已终态({}),无法取消",
|
||
node_id,
|
||
current.as_str()
|
||
))
|
||
}
|
||
}
|
||
}
|