Files
DevFlow/crates/df-workflow/src/executor.rs
绝尘 bd6a41fe6e 新增: 批次工作落地(推进链/评估闭环/事件总线/并发/加固) + 技术债清理 + 文档整理
后端:
- 工作流推进链(D-03):advance_task/状态机/闸门走 df-nodes Node trait,conditions 条件引擎扩展
- 想法评估闭环:启发式评分+对抗评估,df-ideas/scoring + df-storage/idea_eval_repo + idea 前端打通
- 全局事件数据总线:df-ai/context+context_helpers+augmentation 跨模块解耦
- AI planner/plan_hint/intent:aichat B 路线并行多轮基础
- patch_file 加固(TD-03/04):读改写整体锁防 lost update,expected_hash 合约闭环
- 压缩超时兜底(F-15 卡死根治)
- F-09 多会话并发:LlmConcurrency per-conv + streamingGuard 前端守护 + verify 脚本
- 知识注入 DRY/skills/audit 扩展

清理:
- aichat 技术债(误报 allow/死导入/过时注释 30 项)
- URGENT.md 删除(11 项加急全解决/迁 todo)
- 文档整理(todo/待决策/待审查/ARCHITECTURE/INDEX + 总线/技术债审查新文档)
2026-06-21 20:51:26 +08:00

259 lines
13 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.
//! DAG 执行器 — 按拓扑层级调度执行
use std::collections::HashMap;
use df_types::events::WorkflowEvent;
use df_types::types::NodeId;
use crate::conditions::ConditionEngine;
use crate::dag::Dag;
use crate::eventbus::EventBus;
use crate::node::{NodeContext, NodeOutput};
use crate::state::StateMachine;
/// DAG 执行器
pub struct DagExecutor {
/// 事件总线
event_bus: EventBus,
/// 节点状态机
state_machine: StateMachine,
/// 工作流执行 ID由调用方传入下沉到每个 NodeContext
execution_id: String,
}
impl DagExecutor {
/// 创建执行器
///
/// `execution_id` 为本次工作流执行的唯一标识,会下沉到每个节点的 NodeContext
/// 用于节点内的事件关联、审计追踪等。
pub fn new(event_bus: EventBus, execution_id: String) -> Self {
Self {
event_bus,
state_machine: StateMachine::new(),
execution_id,
}
}
/// 返回状态机的共享引用Arc clone与 NodeContext.node_status 共享底层 HashMap
///
/// run_workflow 创建执行器后将其注册到 AppState 全局表,
/// cancel_workflow_node IPC 经 execution_id 取出此引用调 set_cancelled
/// 写入直达运行中阻塞节点HumanNode的 is_cancelled 轮询。
pub fn state_machine(&self) -> StateMachine {
self.state_machine.clone()
}
/// 执行 DAG 工作流
///
/// 同一拓扑层内的节点并发执行,层与层之间串行;
/// 本层全部节点完成后统一更新状态,任一失败则中止后续层。
pub async fn run(
&mut self,
dag: &Dag,
initial_config: serde_json::Value,
) -> anyhow::Result<HashMap<NodeId, NodeOutput>> {
let layers = dag.topological_layers()?;
let mut outputs: HashMap<NodeId, NodeOutput> = HashMap::new();
let start = std::time::Instant::now();
tracing::info!("DAG 执行开始,共 {} 层", layers.len());
// 预建入边索引target → 直接前驱列表 + 边条件表达式(O(E) 一次构建)
// 避免在内层按节点循环中调用 dag.predecessors()(每次 O(E) 全表扫描,整体 O(V·E))。
// 含 condition 是为 conditions-eval feature 接入:flag 开时按入边 condition 过滤前驱 input。
// flag 关时 condition 字段被忽略,行为与增强前完全一致(零破坏)。
let mut adjacency_in: HashMap<NodeId, Vec<(NodeId, Option<String>)>> = HashMap::new();
for edge in &dag.edges {
adjacency_in
.entry(edge.target.clone())
.or_default()
.push((edge.source.clone(), edge.condition.clone()));
}
for (layer_idx, layer) in layers.iter().enumerate() {
tracing::info!("执行第 {} 层,共 {} 个节点", layer_idx, layer.len());
// 阶段一:逐节点发 NodeStarted、置为运行中并构建执行 future
let mut node_futures = Vec::with_capacity(layer.len());
for node_id in layer {
let node = dag.nodes.get(node_id).ok_or_else(|| {
anyhow::anyhow!("节点 {} 不存在于 DAG 中", node_id)
})?;
// 构建节点上下文:通过入边索引 O(入度) 取前驱,而非 O(E) 全表扫描。
//
// conditions-eval feature(默认关):
// 关:所有入边 inputs 全收集,condition 字段完全忽略 = 旧行为(零破坏)。
// 开:对带 condition 的入边以 source output.data 为 context 求值;
// false 则不收集该前驱 input。若节点【存在带 condition 的入边,且无任何
// 入边(条件或无条件)放行】,则跳过该节点执行(不入 outputs、保持 Pending、
// 不 emit),实现条件路由。无条件边 source 有 output 即视为放行。
// 求值失败保守 false(对齐引擎语义)。
//
// 注:跳过判定在 set_running 之前 —— 跳过的节点不应被标记 Running/触发事件,
// 保持 Pending 终态(与"条件未命中"语义一致)。
let eval_conditions = cfg!(feature = "conditions-eval");
let mut inputs: HashMap<NodeId, NodeOutput> = HashMap::new();
let mut has_any_cond_edge = false;
let mut any_edge_passed = false; // 任一入边放行(含无条件边)
if let Some(preds) = adjacency_in.get(node_id) {
for (pred_id, cond) in preds {
if let Some(out) = outputs.get(pred_id) {
if eval_conditions {
if let Some(cond) = cond {
has_any_cond_edge = true;
// 以 source output.data 为 context 求值;失败保守 false
let passed = ConditionEngine::evaluate(cond, &out.data)
.unwrap_or(false);
if passed {
inputs.insert(pred_id.clone(), out.clone());
any_edge_passed = true;
}
} else {
// 无 condition 的入边:无条件放行(必收集)
inputs.insert(pred_id.clone(), out.clone());
any_edge_passed = true;
}
} else {
// flag 关:condition 完全忽略,旧行为全收集
inputs.insert(pred_id.clone(), out.clone());
any_edge_passed = true;
}
}
}
} else {
// 根节点(无入边):不参与条件跳过判定,正常执行
any_edge_passed = true;
}
// 条件路由:flag 开 + 存在条件边 + 没有任何入边放行 → 跳过执行
if eval_conditions && has_any_cond_edge && !any_edge_passed {
tracing::info!(
"节点 {} 所有入边条件均不满足,跳过执行(条件路由)",
node_id
);
continue;
}
// 发送 NodeStarted 事件
self.event_bus
.send(WorkflowEvent::NodeStarted {
node_id: node_id.clone(),
})
.await;
self.state_machine.set_running(node_id.clone())?;
let ctx = NodeContext {
node_id: node_id.clone(),
inputs,
// 节点级覆盖全局级deep_merge(initial_config, node_configs[id])。
// 节点 NodeDef.config 未定义node_configs 缺该 key→ 直接用全局 initial_config
// 与旧行为一致(零行为破坏现有 HumanNode/AiNode 等读全局 config 的调用方)。
config: match dag.node_configs.get(node_id) {
Some(node_cfg) => crate::dag::deep_merge(&initial_config, node_cfg),
None => initial_config.clone(),
},
execution_id: self.execution_id.clone(),
event_bus: self.event_bus.clone(),
// 共享执行器状态机,使节点(如 HumanNode能读取真实状态而非空状态机
node_status: self.state_machine.clone(),
};
// 仅捕获节点共享引用与所有权上下文,避免与 self 借用冲突
let id = node_id.clone();
node_futures.push(async move {
let node_start = std::time::Instant::now();
let result = node.execute(ctx).await;
(id, result, node_start.elapsed().as_millis() as u64)
});
}
// 阶段二:同层节点并发执行,等待全部完成
let results = futures::future::join_all(node_futures).await;
// 阶段三:统一更新状态并发送事件,任一失败则整体返回 Err
let mut first_err: Option<anyhow::Error> = None;
for (node_id, result, duration) in results {
match result {
Ok(output) => {
// 已取消的节点跳过 set_completed:
// Ok 后 join_all 让出执行权,窗口内 cancel_workflow_node IPC → set_cancelled 改 Cancelled,
// 随后此处 set_completed 走 transition 命中(Cancelled→Completed)非法 → bail 致已批准审批报失败(R-P1-3 TOCTOU)
// 与 Err 分支 is_cancelled 短路对称:已取消节点保持 Cancelled 终态
if !self.state_machine.is_cancelled(&node_id) {
self.state_machine.set_completed(node_id.clone())?;
// 未取消才 emit NodeCompleted,取消时 emit NodeCancelled(对齐 Err 分支语义):
// 避免 状态机=Cancelled 但事件=NodeCompleted 的矛盾信号
self.event_bus
.send(WorkflowEvent::NodeCompleted {
node_id: node_id.clone(),
duration_ms: duration,
})
.await;
} else {
// 对齐 Err 分支:取消节点 emit NodeCancelled(非 NodeCompleted),
// 前端按 type 归「取消」,与状态机 Cancelled 一致
self.event_bus
.send(WorkflowEvent::NodeCancelled {
node_id: node_id.clone(),
})
.await;
}
// outputs.insert 保留现状不动:取消时仍写入 output 供下游消费,
// 当前测试锁定"取消不中止后续层",改 insert 会变行为致回归
outputs.insert(node_id, output);
}
Err(e) => {
let error_msg = e.to_string();
// 已取消的节点(如 HumanNode 审批取消)跳过 set_failed:
// Cancelled 已是终态,transition(Cancelled→Failed) 非法会 bail 致工作流崩溃(B-03b-R1)
if self.state_machine.is_cancelled(&node_id) {
// emit NodeCancelled(非 NodeFailed):取消语义有别于失败,前端按 type 归「取消」非「失败」
self.event_bus
.send(WorkflowEvent::NodeCancelled {
node_id: node_id.clone(),
})
.await;
} else {
self.state_machine.set_failed(node_id.clone())?;
self.event_bus
.send(WorkflowEvent::NodeFailed {
node_id: node_id.clone(),
error: error_msg,
})
.await;
}
// 取消与失败一致:节点返回 Err 即中止后续层(run 返回 Err);
// 仅事件类型区分,工作流结果语义保持不变(零行为破坏)。
if first_err.is_none() {
first_err = Some(e.context(format!("节点 {} 执行失败", node_id)));
}
}
}
}
if let Some(e) = first_err {
return Err(e);
}
}
let total = start.elapsed().as_millis() as u64;
// B-03b-R10 ③: WorkflowCompleted 携带 execution_id,供 forward 循环按 exec_id 匹配
// (全局 event_bus 单例下并发工作流的终态事件不再误触发他人 forward 的完成判定)。
self.event_bus
.send(WorkflowEvent::WorkflowCompleted {
execution_id: self.execution_id.clone(),
total_duration_ms: total,
})
.await;
tracing::info!("DAG 执行完成,耗时 {}ms", total);
Ok(outputs)
}
}
#[cfg(test)]
#[path = "executor_helpers.rs"]
mod tests;