feat(flows): 新增流程编辑器基础功能与相关组件
feat(backend): 添加流程模型与服务支持 feat(frontend): 实现流程编辑器UI与交互 feat(assets): 添加流程节点图标资源 feat(plugins): 实现上下文菜单和运行时插件 feat(components): 新增基础节点和侧边栏组件 feat(routes): 添加流程相关路由配置 feat(models): 创建流程和运行日志数据模型 feat(services): 实现流程服务层逻辑 feat(migration): 添加流程相关数据库迁移 feat(config): 更新前端配置支持流程编辑器 feat(utils): 增强axios错误处理和工具函数
This commit is contained in:
173
backend/src/flow/engine.rs
Normal file
173
backend/src/flow/engine.rs
Normal file
@ -0,0 +1,173 @@
|
||||
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 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 => {
|
||||
if let serde_json::Value::Object(obj) = &mut ctx { obj.insert("__current_node_id".to_string(), serde_json::Value::String(node.id.0.clone())); }
|
||||
task.execute(&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();
|
||||
if let serde_json::Value::Object(obj) = &mut task_ctx { obj.insert("__current_node_id".to_string(), serde_json::Value::String(node.id.0.clone())); }
|
||||
let task_arc = task.clone();
|
||||
let name_for_log = task_name.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = task_arc.execute(&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))
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user