Files
DevFlow/src-tauri/src/commands/workflow.rs

478 lines
21 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 工作流相关命令 — 触发执行、查询执行记录
//!
//! 事件转发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> 共享底层 HashMapIPC 写入直达运行中阻塞节点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()
))
}
}
}