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:
2025-09-15 00:27:13 +08:00
parent 9da3978f91
commit b0963e5e37
291 changed files with 17947 additions and 86 deletions

173
backend/src/flow/engine.rs Normal file
View 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),
}
}