feat(flow): 新增流式执行模式与SSE支持

新增流式执行模式,通过SSE实时推送节点执行事件与日志
重构HTTP执行器与中间件,提取通用HTTP客户端组件
优化前端测试面板,支持流式模式切换与实时日志展示
更新依赖版本并修复密码哈希的随机数生成器问题
修复前端节点类型映射问题,确保Code节点表单可用
This commit is contained in:
2025-09-21 01:48:24 +08:00
parent 296f0ae9f6
commit dd7857940f
24 changed files with 1695 additions and 885 deletions

View File

@ -11,7 +11,17 @@ use std::cell::RefCell;
use rhai::AST;
use regex::Regex;
// 将常用的正则匹配暴露给表达式使用
// 模块流程执行引擎engine.rs
// 作用:驱动 ChainDef 流程图,支持:
// - 同步/异步Fire-and-Forget任务执行
// - 条件路由Rhai 表达式与 JSON 条件)与无条件回退
// - 并发分支 fan-out 与 join_all 等待
// - SSE 实时事件推送(逐行增量 + 节点级切片)
// 设计要点:
// - 表达式执行使用 thread_local 的 Rhai Engine 与 AST 缓存,避免全局 Send/Sync 限制
// - 共享上下文使用 RwLock 包裹 serde_json::Value日志聚合使用 Mutex<Vec<String>>
// - 不做冲突校验:允许并发修改;最后写回/写入按代码路径覆盖
//
fn regex_match(s: &str, pat: &str) -> bool {
Regex::new(pat).map(|re| re.is_match(s)).unwrap_or(false)
}
@ -135,7 +145,7 @@ impl FlowEngine {
pub fn builder() -> FlowEngineBuilder { FlowEngineBuilder::default() }
pub async fn drive(&self, chain: &ChainDef, ctx: serde_json::Value, opts: DriveOptions) -> anyhow::Result<(serde_json::Value, Vec<String>)> {
// 1) 选取起点
// 1) 选取起点:优先 Start否则入度为 0再否则第一个节点
// 查找 start优先 Start 节点;否则选择入度为 0 的第一个节点;再否则回退第一个节点
let start = if let Some(n) = chain
.nodes
@ -208,10 +218,30 @@ async fn drive_from(
// 进入节点:打点
let node_id_str = node.id.0.clone();
let node_start = Instant::now();
{
let mut lg = logs.lock().await;
lg.push(format!("enter node: {}", node.id.0));
// 进入节点前记录当前日志长度,便于节点结束时做切片
let pre_len = { logs.lock().await.len() };
// 在每次追加日志时同步发送一条增量 SSE 事件(仅 1 行日志),以提升实时性
// push_and_emit
// - 先将单行日志 push 到共享日志
// - 若存在 SSE 通道,截取上下文快照并发送单行增量事件
async fn push_and_emit(
logs: &std::sync::Arc<tokio::sync::Mutex<Vec<String>>>,
opts: &super::context::DriveOptions,
node_id: &str,
ctx: &std::sync::Arc<tokio::sync::RwLock<serde_json::Value>>,
msg: String,
) {
{
let mut lg = logs.lock().await;
lg.push(msg.clone());
}
if let Some(tx) = opts.event_tx.as_ref() {
let ctx_snapshot = { ctx.read().await.clone() };
crate::middlewares::sse::emit_node(&tx, node_id.to_string(), vec![msg], ctx_snapshot).await;
}
}
// enter 节点也实时推送
push_and_emit(&logs, &opts, &node_id_str, &ctx, format!("enter node: {}", node.id.0)).await;
info!(target: "udmin.flow", "enter node: {}", node.id.0);
// 执行任务
@ -221,13 +251,21 @@ async fn drive_from(
ExecutionMode::Sync => {
// 使用快照执行,结束后整体写回(允许最后写入覆盖并发修改;程序端不做冲突校验)
let mut local_ctx = { ctx.read().await.clone() };
task.execute(&node.id, node, &mut local_ctx).await?;
{ let mut w = ctx.write().await; *w = local_ctx; }
{
let mut lg = logs.lock().await;
lg.push(format!("exec task: {} (sync)", task_name));
match task.execute(&node.id, node, &mut local_ctx).await {
Ok(_) => {
{ let mut w = ctx.write().await; *w = local_ctx; }
push_and_emit(&logs, &opts, &node_id_str, &ctx, format!("exec task: {} (sync)", task_name)).await;
info!(target: "udmin.flow", "exec task: {} (sync)", task_name);
}
Err(e) => {
let err_msg = format!("task error: {}: {}", task_name, e);
push_and_emit(&logs, &opts, &node_id_str, &ctx, err_msg.clone()).await;
// 捕获快照并返回 DriveError
let ctx_snapshot = { ctx.read().await.clone() };
let logs_snapshot = { logs.lock().await.clone() };
return Err(anyhow::Error::new(DriveError { node_id: node_id_str.clone(), ctx: ctx_snapshot, logs: logs_snapshot, message: err_msg }));
}
}
info!(target: "udmin.flow", "exec task: {} (sync)", task_name);
}
ExecutionMode::AsyncFireAndForget => {
// fire-and-forget基于快照执行不写回共享 ctx变量任务除外做有界差异写回
@ -238,6 +276,7 @@ async fn drive_from(
let node_def = node.clone();
let logs_clone = logs.clone();
let ctx_clone = ctx.clone();
let event_tx_opt = opts.event_tx.clone();
tokio::spawn(async move {
let mut c = task_ctx.clone();
let _ = task_arc.execute(&node_id, &node_def, &mut c).await;
@ -268,25 +307,31 @@ async fn drive_from(
let mut lg = logs_clone.lock().await;
lg.push(format!("exec task done (async): {} (writeback variable)", name_for_log));
}
// 实时推送异步完成日志
if let Some(tx) = event_tx_opt.as_ref() {
let ctx_snapshot = { ctx_clone.read().await.clone() };
crate::middlewares::sse::emit_node(&tx, node_id.0.clone(), vec![format!("exec task done (async): {} (writeback variable)", name_for_log)], ctx_snapshot).await;
}
info!(target: "udmin.flow", "exec task done (async): {} (writeback variable)", name_for_log);
} else {
{
let mut lg = logs_clone.lock().await;
lg.push(format!("exec task done (async): {}", name_for_log));
}
// 实时推送异步完成日志
if let Some(tx) = event_tx_opt.as_ref() {
let ctx_snapshot = { ctx_clone.read().await.clone() };
crate::middlewares::sse::emit_node(&tx, node_id.0.clone(), vec![format!("exec task done (async): {}", name_for_log)], ctx_snapshot).await;
}
info!(target: "udmin.flow", "exec task done (async): {}", name_for_log);
}
});
{
let mut lg = logs.lock().await;
lg.push(format!("spawn task: {} (async)", task_name));
}
push_and_emit(&logs, &opts, &node_id_str, &ctx, format!("spawn task: {} (async)", task_name)).await;
info!(target: "udmin.flow", "spawn task: {} (async)", task_name);
}
}
} else {
let mut lg = logs.lock().await;
lg.push(format!("task not found: {} (skip)", task_name));
push_and_emit(&logs, &opts, &node_id_str, &ctx, format!("task not found: {} (skip)", task_name)).await;
info!(target: "udmin.flow", "task not found: {} (skip)", task_name);
}
}
@ -294,11 +339,13 @@ async fn drive_from(
// End 节点:记录耗时后结束
if matches!(node.kind, NodeKind::End) {
let duration = node_start.elapsed().as_millis();
{
let mut lg = logs.lock().await;
lg.push(format!("leave node: {} {} ms", node_id_str, duration));
}
push_and_emit(&logs, &opts, &node_id_str, &ctx, format!("leave node: {} {} ms", node_id_str, duration)).await;
info!(target: "udmin.flow", "leave node: {} {} ms", node_id_str, duration);
if let Some(tx) = opts.event_tx.as_ref() {
let node_logs = { let lg = logs.lock().await; lg[pre_len..].to_vec() };
let ctx_snapshot = { ctx.read().await.clone() };
crate::middlewares::sse::emit_node(&tx, node_id_str.clone(), node_logs, ctx_snapshot).await;
}
break;
}
@ -367,7 +414,13 @@ async fn drive_from(
let mut lg = logs.lock().await;
lg.push(format!("leave node: {} {} ms", node_id_str, duration));
}
push_and_emit(&logs, &opts, &node_id_str, &ctx, format!("leave node: {} {} ms", node_id_str, duration)).await;
info!(target: "udmin.flow", "leave node: {} {} ms", node_id_str, duration);
if let Some(tx) = opts.event_tx.as_ref() {
let node_logs = { let lg = logs.lock().await; lg[pre_len..].to_vec() };
let ctx_snapshot = { ctx.read().await.clone() };
crate::middlewares::sse::emit_node(&tx, node_id_str.clone(), node_logs, ctx_snapshot).await;
}
break;
}
@ -379,6 +432,11 @@ async fn drive_from(
lg.push(format!("leave node: {} {} ms", node_id_str, duration));
}
info!(target: "udmin.flow", "leave node: {} {} ms", node_id_str, duration);
if let Some(tx) = opts.event_tx.as_ref() {
let node_logs = { let lg = logs.lock().await; lg[pre_len..].to_vec() };
let ctx_snapshot = { ctx.read().await.clone() };
crate::middlewares::sse::emit_node(&tx, node_id_str.clone(), node_logs, ctx_snapshot).await;
}
current = nexts.remove(0);
continue;
}
@ -405,6 +463,11 @@ async fn drive_from(
lg.push(format!("leave node: {} {} ms", node_id_str, duration));
}
info!(target: "udmin.flow", "leave node: {} {} ms", node_id_str, duration);
if let Some(tx) = opts.event_tx.as_ref() {
let node_logs = { let lg = logs.lock().await; lg[pre_len..].to_vec() };
let ctx_snapshot = { ctx.read().await.clone() };
crate::middlewares::sse::emit_node(&tx, node_id_str.clone(), node_logs, ctx_snapshot).await;
}
}
Ok(())
@ -427,138 +490,19 @@ impl Default for FlowEngine {
fn default() -> Self { Self { tasks: crate::flow::task::default_registry() } }
}
/* moved to executors::condition
fn eval_condition_json(ctx: &serde_json::Value, cond: &serde_json::Value) -> anyhow::Result<bool> {
-fn eval_condition_json(ctx: &serde_json::Value, cond: &serde_json::Value) -> anyhow::Result<bool> {
- // 支持前端 Condition 组件导出的: { left:{type, content}, operator, right? }
- use serde_json::Value as V;
-
- let left = cond.get("left").ok_or_else(|| anyhow::anyhow!("missing left"))?;
- let op_raw = 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 };
-
- // 归一化操作符:忽略大小写,替换下划线为空格
- let op = op_raw.trim().to_lowercase().replace('_', " ");
-
- // 工具函数
- fn to_f64(v: &V) -> Option<f64> {
- match v {
- V::Number(n) => n.as_f64(),
- V::String(s) => s.parse::<f64>().ok(),
- _ => None,
- }
- }
- fn is_empty_val(v: &V) -> bool {
- match v {
- V::Null => true,
- V::String(s) => s.trim().is_empty(),
- V::Array(a) => a.is_empty(),
- V::Object(m) => m.is_empty(),
- _ => false,
- }
- }
- fn json_equal(a: &V, b: &V) -> bool {
- match (a, b) {
- (V::Number(_), V::Number(_)) | (V::Number(_), V::String(_)) | (V::String(_), V::Number(_)) => {
- match (to_f64(a), to_f64(b)) { (Some(x), Some(y)) => x == y, _ => a == b }
- }
- _ => a == b,
- }
- }
- fn contains(left: &V, right: &V) -> bool {
- match (left, right) {
- (V::String(s), V::String(t)) => s.contains(t),
- (V::Array(arr), r) => arr.iter().any(|x| json_equal(x, r)),
- (V::Object(map), V::String(key)) => map.contains_key(key),
- _ => false,
- }
- }
- fn in_op(left: &V, right: &V) -> bool {
- match right {
- V::Array(arr) => arr.iter().any(|x| json_equal(left, x)),
- V::Object(map) => match left { V::String(k) => map.contains_key(k), _ => false },
- V::String(hay) => match left { V::String(needle) => hay.contains(needle), _ => false },
- _ => false,
- }
- }
- fn bool_like(v: &V) -> bool {
- match v {
- V::Bool(b) => *b,
- V::Null => false,
- V::Number(n) => n.as_f64().map(|x| x != 0.0).unwrap_or(false),
- V::String(s) => {
- let s_l = s.trim().to_lowercase();
- if s_l == "true" { true } else if s_l == "false" { false } else { !s_l.is_empty() }
- }
- V::Array(a) => !a.is_empty(),
- V::Object(m) => !m.is_empty(),
- }
- }
-
- let res = match (op.as_str(), &lval, &rval) {
- // 等于 / 不等于(适配所有 JSON 类型;数字按 f64 比较,其他走深度相等)
- ("equal" | "equals" | "==" | "eq", l, Some(r)) => json_equal(l, r),
- ("not equal" | "!=" | "not equals" | "neq", l, Some(r)) => !json_equal(l, r),
-
- // 数字比较
- ("greater than" | ">" | "gt", l, Some(r)) => match (to_f64(l), to_f64(r)) { (Some(a), Some(b)) => a > b, _ => false },
- ("greater than or equal" | ">=" | "gte" | "ge", l, Some(r)) => match (to_f64(l), to_f64(r)) { (Some(a), Some(b)) => a >= b, _ => false },
- ("less than" | "<" | "lt", l, Some(r)) => match (to_f64(l), to_f64(r)) { (Some(a), Some(b)) => a < b, _ => false },
- ("less than or equal" | "<=" | "lte" | "le", l, Some(r)) => match (to_f64(l), to_f64(r)) { (Some(a), Some(b)) => a <= b, _ => false },
-
- // 包含 / 不包含(字符串、数组、对象(键)
- ("contains", l, Some(r)) => contains(l, r),
- ("not contains", l, Some(r)) => !contains(l, r),
-
- // 成员关系left in right / not in
- ("in", l, Some(r)) => in_op(l, r),
- ("not in" | "nin", l, Some(r)) => !in_op(l, r),
-
- // 为空 / 非空字符串、数组、对象、null
- ("is empty" | "empty" | "isempty", l, _) => is_empty_val(l),
- ("is not empty" | "not empty" | "notempty", l, _) => !is_empty_val(l),
-
- // 布尔判断(对各类型进行布尔化)
- ("is true" | "is true?" | "istrue", l, _) => bool_like(l),
- ("is false" | "isfalse", l, _) => !bool_like(l),
-
- _ => 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)
- }
- "expression" => {
- let expr = v.get("content").and_then(|x| x.as_str()).unwrap_or("");
- if expr.trim().is_empty() { return Ok(V::Null); }
- Ok(super::engine::eval_rhai_expr_json(expr, ctx).unwrap_or(V::Null))
- }
- _ => Ok(V::Null),
#[derive(Debug, Clone)]
pub struct DriveError {
pub node_id: String,
pub ctx: serde_json::Value,
pub logs: Vec<String>,
pub message: String,
}
impl std::fmt::Display for DriveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
*/
impl std::error::Error for DriveError {}