feat: 新增条件节点和多语言脚本支持

refactor(flow): 将Decision节点重命名为Condition节点
feat(flow): 新增多语言脚本执行器(Rhai/JS/Python)
feat(flow): 实现变量映射和执行功能
feat(flow): 添加条件节点执行逻辑
feat(frontend): 为开始/结束节点添加多语言描述
test: 添加yaml条件转换测试
chore: 移除废弃的storage模块
This commit is contained in:
2025-09-19 13:41:52 +08:00
parent 81757eecf5
commit 62789fce42
25 changed files with 1651 additions and 313 deletions

View File

@ -1,9 +1,128 @@
use std::collections::HashMap;
use tokio::sync::{RwLock, Mutex};
use futures::future::join_all;
use rhai::Engine;
use tracing::info;
// === 表达式评估支持thread_local 引擎与 AST 缓存,避免全局 Sync/Send 限制 ===
use std::cell::RefCell;
use rhai::AST;
use regex::Regex;
// 将常用的正则匹配暴露给表达式使用
fn regex_match(s: &str, pat: &str) -> bool {
Regex::new(pat).map(|re| re.is_match(s)).unwrap_or(false)
}
// 常用字符串函数,便于在表达式中直接调用(函数式写法)
fn contains(s: &str, sub: &str) -> bool { s.contains(sub) }
fn starts_with(s: &str, prefix: &str) -> bool { s.starts_with(prefix) }
fn ends_with(s: &str, suffix: &str) -> bool { s.ends_with(suffix) }
// 新增:判空/判不空(支持任意 Dynamic 类型)
fn is_empty(v: rhai::Dynamic) -> bool {
if v.is_unit() { return true; }
if let Some(s) = v.clone().try_cast::<rhai::ImmutableString>() {
return s.is_empty();
}
if let Some(a) = v.clone().try_cast::<rhai::Array>() {
return a.is_empty();
}
if let Some(m) = v.clone().try_cast::<rhai::Map>() {
return m.is_empty();
}
false
}
fn not_empty(v: rhai::Dynamic) -> bool { !is_empty(v) }
thread_local! {
static RHIA_ENGINE: RefCell<Engine> = RefCell::new({
let mut eng = Engine::new();
// 限制执行步数,防止复杂表达式消耗过多计算资源
eng.set_max_operations(100_000);
// 严格变量模式,避免拼写错误导致静默为 null
eng.set_strict_variables(true);
// 注册常用工具函数
eng.register_fn("regex_match", regex_match);
eng.register_fn("contains", contains);
eng.register_fn("starts_with", starts_with);
eng.register_fn("ends_with", ends_with);
// 新增:注册判空/判不空函数(既可函数式调用,也可方法式调用)
eng.register_fn("is_empty", is_empty);
eng.register_fn("not_empty", not_empty);
eng
});
// 简单的 AST 缓存:以表达式字符串为键存储编译结果(线程本地)
static AST_CACHE: RefCell<HashMap<String, AST>> = RefCell::new(HashMap::new());
}
// 评估 Rhai 表达式为 bool提供 ctx 变量serde_json::Value
fn eval_rhai_expr_bool(expr: &str, ctx: &serde_json::Value) -> bool {
// 构造作用域并注入 ctx
let mut scope = rhai::Scope::new();
let dyn_ctx = match rhai::serde::to_dynamic(ctx.clone()) { Ok(d) => d, Err(_) => rhai::Dynamic::UNIT };
scope.push("ctx", dyn_ctx);
// 先从缓存读取 AST未命中则编译并写入缓存然后执行
let cached = AST_CACHE.with(|c| c.borrow().get(expr).cloned());
if let Some(ast) = cached {
return RHIA_ENGINE.with(|eng| eng.borrow().eval_ast_with_scope::<bool>(&mut scope, &ast).unwrap_or(false));
}
let compiled = RHIA_ENGINE.with(|eng| eng.borrow().compile(expr));
match compiled {
Ok(ast) => {
// 简单容量控制:超过 1024 条时清空,避免无限增长
AST_CACHE.with(|c| {
let mut cache = c.borrow_mut();
if cache.len() > 1024 { cache.clear(); }
cache.insert(expr.to_string(), ast.clone());
});
RHIA_ENGINE.with(|eng| eng.borrow().eval_ast_with_scope::<bool>(&mut scope, &ast).unwrap_or(false))
}
Err(_) => false,
}
}
// 通用:评估 Rhai 表达式并转换为 serde_json::Value失败返回 None
pub(crate) fn eval_rhai_expr_json(expr: &str, ctx: &serde_json::Value) -> Option<serde_json::Value> {
// 构造作用域并注入 ctx
let mut scope = rhai::Scope::new();
let dyn_ctx = match rhai::serde::to_dynamic(ctx.clone()) { Ok(d) => d, Err(_) => rhai::Dynamic::UNIT };
scope.push("ctx", dyn_ctx);
// 先从缓存读取 AST未命中则编译并写入缓存然后执行
let cached = AST_CACHE.with(|c| c.borrow().get(expr).cloned());
let eval = |ast: &AST, scope: &mut rhai::Scope| -> Option<serde_json::Value> {
RHIA_ENGINE.with(|eng| {
eng.borrow()
.eval_ast_with_scope::<rhai::Dynamic>(scope, ast)
.ok()
.and_then(|d| rhai::serde::from_dynamic(&d).ok())
})
};
if let Some(ast) = cached {
return eval(&ast, &mut scope);
}
let compiled = RHIA_ENGINE.with(|eng| eng.borrow().compile(expr));
match compiled {
Ok(ast) => {
AST_CACHE.with(|c| {
let mut cache = c.borrow_mut();
if cache.len() > 1024 { cache.clear(); }
cache.insert(expr.to_string(), ast.clone());
});
eval(&ast, &mut scope)
}
Err(_) => None,
}
}
use super::{context::{DriveOptions, ExecutionMode}, domain::{ChainDef, NodeKind}, task::TaskRegistry};
use crate::flow::executors::condition::eval_condition_json;
pub struct FlowEngine {
pub tasks: TaskRegistry,
@ -14,9 +133,8 @@ impl FlowEngine {
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();
pub async fn drive(&self, chain: &ChainDef, ctx: serde_json::Value, opts: DriveOptions) -> anyhow::Result<(serde_json::Value, Vec<String>)> {
// 1) 选取起点
// 查找 start优先 Start 节点;否则选择入度为 0 的第一个节点;再否则回退第一个节点
let start = if let Some(n) = chain
.nodes
@ -42,87 +160,218 @@ impl FlowEngine {
}
};
// 邻接表(按 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();
// 2) 构建可并发共享的数据结构
// 拷贝节点与边(保持原有顺序)到拥有所有权的 HashMap供并发分支安全使用
let node_map_owned: HashMap<String, super::domain::NodeDef> = chain.nodes.iter().map(|n| (n.id.0.clone(), n.clone())).collect();
let mut adj_owned: HashMap<String, Vec<super::domain::LinkDef>> = HashMap::new();
for l in &chain.links { adj_owned.entry(l.from.0.clone()).or_default().push(l.clone()); }
let node_map = std::sync::Arc::new(node_map_owned);
let adj = std::sync::Arc::new(adj_owned);
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);
// 共享上下文(允许并发修改,程序端不做冲突校验)
let shared_ctx = std::sync::Arc::new(RwLock::new(ctx));
// 共享日志聚合
let logs_shared = std::sync::Arc::new(Mutex::new(Vec::<String>::new()));
// 任务执行
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);
// 3) 并发驱动从起点开始
let tasks = self.tasks.clone();
drive_from(tasks, node_map.clone(), adj.clone(), start, shared_ctx.clone(), opts.clone(), logs_shared.clone()).await?;
// 4) 汇总返回
let final_ctx = { shared_ctx.read().await.clone() };
let logs = { logs_shared.lock().await.clone() };
Ok((final_ctx, logs))
}
}
// 从指定节点开始驱动,遇到多条满足条件的边时:
// - 第一条在当前任务内继续
// - 其余分支并行 spawn等待全部分支执行完毕后返回
async fn drive_from(
tasks: TaskRegistry,
node_map: std::sync::Arc<HashMap<String, super::domain::NodeDef>>,
adj: std::sync::Arc<HashMap<String, Vec<super::domain::LinkDef>>>,
start: String,
ctx: std::sync::Arc<RwLock<serde_json::Value>>, // 共享上下文(并发写入通过写锁串行化,不做冲突校验)
opts: DriveOptions,
logs: std::sync::Arc<Mutex<Vec<String>>>,
) -> anyhow::Result<()> {
let mut current = start;
let mut steps = 0usize;
loop {
if steps >= opts.max_steps { break; }
steps += 1;
// 读取节点
let node = match node_map.get(&current) { Some(n) => n, None => break };
{
let mut lg = logs.lock().await;
lg.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) = tasks.get(task_name) {
match opts.execution_mode {
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));
}
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: {} (sync)", task_name);
}
ExecutionMode::AsyncFireAndForget => {
// fire-and-forget基于快照执行不写回共享 ctx变量任务除外做有界差异写回
let task_ctx = { ctx.read().await.clone() };
let task_arc = task.clone();
let name_for_log = task_name.clone();
let node_id = node.id.clone();
let node_def = node.clone();
let logs_clone = logs.clone();
let ctx_clone = ctx.clone();
tokio::spawn(async move {
let mut c = task_ctx.clone();
let _ = task_arc.execute(&node_id, &node_def, &mut c).await;
// 对 variable 任务执行写回:将顶层新增/修改的键写回共享 ctx并移除对应 variable 节点
if node_def.task.as_deref() == Some("variable") {
// 计算顶层差异(排除 nodes仅在不同或新增时写回
let mut changed: Vec<(String, serde_json::Value)> = Vec::new();
if let (serde_json::Value::Object(before_map), serde_json::Value::Object(after_map)) = (&task_ctx, &c) {
for (k, v_after) in after_map.iter() {
if k == "nodes" { continue; }
match before_map.get(k) {
Some(v_before) if v_before == v_after => {}
_ => changed.push((k.clone(), v_after.clone())),
}
}
}
{
let mut w = ctx_clone.write().await;
if let serde_json::Value::Object(map) = &mut *w {
for (k, v) in changed.into_iter() { map.insert(k, v); }
if let Some(serde_json::Value::Object(nodes)) = map.get_mut("nodes") {
nodes.remove(node_id.0.as_str());
}
}
}
{
let mut lg = logs_clone.lock().await;
lg.push(format!("exec task done (async): {} (writeback variable)", name_for_log));
}
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));
}
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; }
});
{
let mut lg = logs.lock().await;
lg.push(format!("spawn task: {} (async)", task_name));
}
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));
info!(target: "udmin.flow", "task not found: {} (skip)", task_name);
}
match next { Some(n) => current = n, None => break }
}
Ok((ctx, logs))
if matches!(node.kind, NodeKind::End) { break; }
// 选择下一批 link仅在 Condition 节点上评估条件;其他节点忽略条件,直接沿第一条边前进
let mut nexts: Vec<String> = Vec::new();
if let Some(links) = adj.get(node.id.0.as_str()) {
if matches!(node.kind, NodeKind::Condition) {
// 条件边:全部评估为真者加入 nexts空字符串条件视为无条件不在此处评估
for link in links.iter() {
if let Some(cond_str) = &link.condition {
if cond_str.trim().is_empty() {
// 空条件:视为无条件边,留待后续回退逻辑处理
info!(target: "udmin.flow", from=%node.id.0, to=%link.to.0, "condition link: empty (unconditional candidate)");
continue;
}
let trimmed = cond_str.trim_start();
let (kind, ok) = if trimmed.starts_with('{') || trimmed.starts_with('[') {
match serde_json::from_str::<serde_json::Value>(cond_str) {
Ok(v) => {
let snapshot = { ctx.read().await.clone() };
("json", eval_condition_json(&snapshot, &v).unwrap_or(false))
}
Err(_) => ("json_parse_error", false),
}
} else {
let snapshot = { ctx.read().await.clone() };
("rhai", eval_rhai_expr_bool(cond_str, &snapshot))
};
info!(target: "udmin.flow", from=%node.id.0, to=%link.to.0, cond_kind=%kind, cond_len=%cond_str.len(), result=%ok, "condition link evaluated");
if ok { nexts.push(link.to.0.clone()); }
} else {
// 无 condition 字段:视为无条件边
info!(target: "udmin.flow", from=%node.id.0, to=%link.to.0, "condition link: none (unconditional candidate)");
}
}
// 若没有命中条件边,则取第一条无条件边(无条件 = 无 condition 或 空字符串)
if nexts.is_empty() {
let mut picked = None;
for link in links.iter() {
match &link.condition {
None => { picked = Some(link.to.0.clone()); break; }
Some(s) if s.trim().is_empty() => { picked = Some(link.to.0.clone()); break; }
_ => {}
}
}
if let Some(to) = picked {
info!(target: "udmin.flow", from=%node.id.0, to=%to, "condition fallback: pick unconditional");
nexts.push(to);
} else {
info!(target: "udmin.flow", node=%node.id.0, "condition: no matched and no unconditional, stop");
}
}
} else {
// 非条件节点忽略条件fan-out 所有出边(全部并行执行)
for link in links.iter() {
nexts.push(link.to.0.clone());
info!(target: "udmin.flow", from=%node.id.0, to=%link.to.0, "fan-out from non-condition node");
}
}
}
if nexts.is_empty() { break; }
if nexts.len() == 1 {
current = nexts.remove(0);
continue;
}
// 多分支:主分支沿第一条继续,其余分支并行执行并等待完成
let mut futs = Vec::new();
for to_id in nexts.iter().skip(1).cloned() {
let tasks_c = tasks.clone();
let node_map_c = node_map.clone();
let adj_c = adj.clone();
let ctx_c = ctx.clone();
let opts_c = opts.clone();
let logs_c = logs.clone();
futs.push(drive_from(tasks_c, node_map_c, adj_c, to_id, ctx_c, opts_c, logs_c));
}
// 当前分支继续第一条
current = nexts.into_iter().next().unwrap();
// 在一个安全点等待已分支的完成(这里选择在下一轮进入前等待)
let _ = join_all(futs).await;
}
Ok(())
}
#[derive(Default)]
@ -142,52 +391,138 @@ 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> {
// 目前支持前端 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),
-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),
}
}
}
*/