重构流程引擎核心组件,引入执行器接口Executor替代原有TaskComponent,优化节点配置映射逻辑: 1. 新增mappers模块集中处理节点配置提取 2. 为存储层添加Storage trait抽象 3. 移除对ctx魔法字段的依赖,直接传递节点信息 4. 增加构建器模式支持引擎创建 5. 完善DSL解析的输入校验 同时标记部分未使用代码为allow(dead_code)
193 lines
8.5 KiB
Rust
193 lines
8.5 KiB
Rust
use std::collections::HashMap;
|
||
|
||
use rhai::Engine;
|
||
use tracing::info;
|
||
|
||
use super::{context::{DriveOptions, ExecutionMode}, domain::{ChainDef, NodeKind}, task::TaskRegistry};
|
||
|
||
pub struct FlowEngine {
|
||
pub tasks: TaskRegistry,
|
||
}
|
||
|
||
impl FlowEngine {
|
||
pub fn new(tasks: TaskRegistry) -> Self { Self { tasks } }
|
||
|
||
pub fn builder() -> FlowEngineBuilder { FlowEngineBuilder::default() }
|
||
|
||
pub async fn drive(&self, chain: &ChainDef, mut ctx: serde_json::Value, opts: DriveOptions) -> anyhow::Result<(serde_json::Value, Vec<String>)> {
|
||
let mut logs = Vec::new();
|
||
|
||
// 查找 start:优先 Start 节点;否则选择入度为 0 的第一个节点;再否则回退第一个节点
|
||
let start = if let Some(n) = chain
|
||
.nodes
|
||
.iter()
|
||
.find(|n| matches!(n.kind, NodeKind::Start))
|
||
{
|
||
n.id.0.clone()
|
||
} else {
|
||
// 计算入度
|
||
let mut indeg: HashMap<&str, usize> = HashMap::new();
|
||
for n in &chain.nodes { indeg.entry(n.id.0.as_str()).or_insert(0); }
|
||
for l in &chain.links { *indeg.entry(l.to.0.as_str()).or_insert(0) += 1; }
|
||
if let Some(n) = chain.nodes.iter().find(|n| indeg.get(n.id.0.as_str()).copied().unwrap_or(0) == 0) {
|
||
n.id.0.clone()
|
||
} else {
|
||
chain
|
||
.nodes
|
||
.first()
|
||
.ok_or_else(|| anyhow::anyhow!("empty chain"))?
|
||
.id
|
||
.0
|
||
.clone()
|
||
}
|
||
};
|
||
|
||
// 邻接表(按 links 的原始顺序保序)
|
||
let mut adj: HashMap<&str, Vec<&super::domain::LinkDef>> = HashMap::new();
|
||
for l in &chain.links { adj.entry(&l.from.0).or_default().push(l); }
|
||
let node_map: HashMap<&str, &super::domain::NodeDef> = chain.nodes.iter().map(|n| (n.id.0.as_str(), n)).collect();
|
||
|
||
let mut current = start;
|
||
let mut steps = 0usize;
|
||
while steps < opts.max_steps {
|
||
steps += 1;
|
||
let node = node_map.get(current.as_str()).ok_or_else(|| anyhow::anyhow!("node not found"))?;
|
||
logs.push(format!("enter node: {}", node.id.0));
|
||
info!(target: "udmin.flow", "enter node: {}", node.id.0);
|
||
|
||
// 任务执行
|
||
if let Some(task_name) = &node.task {
|
||
if let Some(task) = self.tasks.get(task_name) {
|
||
match opts.execution_mode {
|
||
ExecutionMode::Sync => {
|
||
// 直接传入 node_id 与 node,避免对 ctx 魔法字段的依赖
|
||
task.execute(&node.id, node, &mut ctx).await?;
|
||
logs.push(format!("exec task: {} (sync)", task_name));
|
||
info!(target: "udmin.flow", "exec task: {} (sync)", task_name);
|
||
}
|
||
ExecutionMode::AsyncFireAndForget => {
|
||
// fire-and-forget: 复制一份上下文供该任务使用,主流程不等待
|
||
let mut task_ctx = ctx.clone();
|
||
let task_arc = task.clone();
|
||
let name_for_log = task_name.clone();
|
||
let node_id = node.id.clone();
|
||
let node_def = (*node).clone();
|
||
tokio::spawn(async move {
|
||
let _ = task_arc.execute(&node_id, &node_def, &mut task_ctx).await;
|
||
info!(target: "udmin.flow", "exec task done (async): {}", name_for_log);
|
||
});
|
||
logs.push(format!("spawn task: {} (async)", task_name));
|
||
info!(target: "udmin.flow", "spawn task: {} (async)", task_name);
|
||
}
|
||
}
|
||
} else {
|
||
logs.push(format!("task not found: {} (skip)", task_name));
|
||
info!(target: "udmin.flow", "task not found: {} (skip)", task_name);
|
||
}
|
||
}
|
||
|
||
if matches!(node.kind, NodeKind::End) { break; }
|
||
|
||
// 选择下一条 link:优先有条件的且为真;否则保序选择第一条无条件边
|
||
let mut next: Option<String> = None;
|
||
if let Some(links) = adj.get(node.id.0.as_str()) {
|
||
// 先检测条件边
|
||
for link in links.iter() {
|
||
if let Some(cond_str) = &link.condition {
|
||
// 两种情况:
|
||
// 1) 前端序列化的 JSON,形如 { left: {type, content}, operator, right? }
|
||
// 2) 直接是 rhai 表达式字符串
|
||
let ok = if cond_str.trim_start().starts_with('{') {
|
||
match serde_json::from_str::<serde_json::Value>(cond_str) {
|
||
Ok(v) => eval_condition_json(&ctx, &v).unwrap_or(false),
|
||
Err(_) => false,
|
||
}
|
||
} else {
|
||
let mut scope = rhai::Scope::new();
|
||
scope.push("ctx", rhai::serde::to_dynamic(ctx.clone()).map_err(|e| anyhow::anyhow!(e.to_string()))?);
|
||
let engine = Engine::new();
|
||
engine.eval_with_scope::<bool>(&mut scope, cond_str).unwrap_or(false)
|
||
};
|
||
if ok { next = Some(link.to.0.clone()); break; }
|
||
}
|
||
}
|
||
// 若没有命中条件边,则取第一条无条件边
|
||
if next.is_none() {
|
||
for link in links.iter() {
|
||
if link.condition.is_none() { next = Some(link.to.0.clone()); break; }
|
||
}
|
||
}
|
||
}
|
||
match next { Some(n) => current = n, None => break }
|
||
}
|
||
|
||
Ok((ctx, logs))
|
||
}
|
||
}
|
||
|
||
#[derive(Default)]
|
||
pub struct FlowEngineBuilder {
|
||
tasks: Option<TaskRegistry>,
|
||
}
|
||
|
||
impl FlowEngineBuilder {
|
||
pub fn tasks(mut self, reg: TaskRegistry) -> Self { self.tasks = Some(reg); self }
|
||
pub fn build(self) -> FlowEngine {
|
||
let tasks = self.tasks.unwrap_or_else(|| crate::flow::task::default_registry());
|
||
FlowEngine { tasks }
|
||
}
|
||
}
|
||
|
||
impl Default for FlowEngine {
|
||
fn default() -> Self { Self { tasks: crate::flow::task::default_registry() } }
|
||
}
|
||
|
||
fn eval_condition_json(ctx: &serde_json::Value, cond: &serde_json::Value) -> anyhow::Result<bool> {
|
||
// 目前支持前端 Condition 组件导出的: { left:{type, content}, operator, right? }
|
||
let left = cond.get("left").ok_or_else(|| anyhow::anyhow!("missing left"))?;
|
||
let op = cond.get("operator").and_then(|v| v.as_str()).unwrap_or("");
|
||
let right = cond.get("right");
|
||
|
||
let lval = resolve_value(ctx, left)?;
|
||
let rval = match right { Some(v) => Some(resolve_value(ctx, v)?), None => None };
|
||
|
||
use serde_json::Value as V;
|
||
let res = match (op, &lval, &rval) {
|
||
("contains", V::String(s), Some(V::String(t))) => s.contains(t),
|
||
("equals", V::String(s), Some(V::String(t))) => s == t,
|
||
("equals", V::Number(a), Some(V::Number(b))) => a == b,
|
||
("is_true", V::Bool(b), _) => *b,
|
||
("is_false", V::Bool(b), _) => !*b,
|
||
("gt", V::Number(a), Some(V::Number(b))) => a.as_f64().unwrap_or(0.0) > b.as_f64().unwrap_or(0.0),
|
||
("lt", V::Number(a), Some(V::Number(b))) => a.as_f64().unwrap_or(0.0) < b.as_f64().unwrap_or(0.0),
|
||
_ => false,
|
||
};
|
||
Ok(res)
|
||
}
|
||
|
||
fn resolve_value(ctx: &serde_json::Value, v: &serde_json::Value) -> anyhow::Result<serde_json::Value> {
|
||
use serde_json::Value as V;
|
||
let t = v.get("type").and_then(|v| v.as_str()).unwrap_or("");
|
||
match t {
|
||
"constant" => Ok(v.get("content").cloned().unwrap_or(V::Null)),
|
||
"ref" => {
|
||
// content: [nodeId, field]
|
||
if let Some(arr) = v.get("content").and_then(|v| v.as_array()) {
|
||
if arr.len() >= 2 {
|
||
if let (Some(node), Some(field)) = (arr[0].as_str(), arr[1].as_str()) {
|
||
let val = ctx
|
||
.get("nodes")
|
||
.and_then(|n| n.get(node))
|
||
.and_then(|m| m.get(field))
|
||
.cloned()
|
||
.or_else(|| ctx.get(field).cloned())
|
||
.unwrap_or(V::Null);
|
||
return Ok(val);
|
||
}
|
||
}
|
||
}
|
||
Ok(V::Null)
|
||
}
|
||
_ => Ok(V::Null),
|
||
}
|
||
} |