refactor(executors): 将 Rhai 引擎评估逻辑迁移至 script_rhai 模块 docs: 添加 Flow 架构文档与示例 JSON feat(i18n): 新增前端多语言支持 perf(axios): 优化 token 刷新与 401 处理逻辑 style: 统一代码格式化与简化条件判断
217 lines
8.8 KiB
Rust
217 lines
8.8 KiB
Rust
// third-party
|
||
use anyhow::Result;
|
||
use serde_json::Value as V;
|
||
use tracing::info;
|
||
|
||
// 业务函数
|
||
pub(crate) fn eval_condition_json(ctx: &serde_json::Value, cond: &serde_json::Value) -> Result<bool> {
|
||
// 新增:若 cond 为数组,按 AND 语义评估(全部为 true 才为 true)
|
||
if let Some(arr) = cond.as_array() {
|
||
let mut all_true = true;
|
||
for (idx, item) in arr.iter().enumerate() {
|
||
let ok = eval_condition_json(ctx, item)?;
|
||
info!(target = "udmin.flow", index = idx, result = %ok, "condition group item (AND)");
|
||
if !ok { all_true = false; }
|
||
}
|
||
info!(target = "udmin.flow", count = arr.len(), result = %all_true, "condition group evaluated (AND)");
|
||
return Ok(all_true);
|
||
}
|
||
|
||
// 支持前端 Condition 组件导出的: { left:{type, content}, operator, right? }
|
||
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_raw = cond.get("right");
|
||
|
||
// 解析弱等于标记:当右值 schema.extra.weak 为 true 时,对字符串比较采用忽略大小写与首尾空白的弱等于
|
||
let weak_eq = right_raw
|
||
.and_then(|r| r.get("schema"))
|
||
.and_then(|s| s.get("extra"))
|
||
.and_then(|e| e.get("weak"))
|
||
.and_then(|b| b.as_bool())
|
||
.unwrap_or(false);
|
||
|
||
let lval = resolve_value(ctx, left)?;
|
||
let rval = match right_raw { 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 norm_str(s: &str) -> String { s.trim().to_lowercase() }
|
||
fn json_equal(a: &V, b: &V, weak: bool) -> 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 }
|
||
}
|
||
// 字符串:若 weak 则忽略大小写与首尾空白
|
||
(V::String(sa), V::String(sb)) if weak => norm_str(sa) == norm_str(sb),
|
||
_ => a == b,
|
||
}
|
||
}
|
||
fn contains(left: &V, right: &V, weak: bool) -> bool {
|
||
match (left, right) {
|
||
(V::String(s), V::String(t)) => {
|
||
if weak { norm_str(s).contains(&norm_str(t)) } else { s.contains(t) }
|
||
}
|
||
(V::Array(arr), r) => arr.iter().any(|x| json_equal(x, r, weak)),
|
||
(V::Object(map), V::String(key)) => {
|
||
if weak { map.keys().any(|k| norm_str(k) == norm_str(key)) } else { map.contains_key(key) }
|
||
}
|
||
_ => false,
|
||
}
|
||
}
|
||
fn in_op(left: &V, right: &V, weak: bool) -> bool {
|
||
match right {
|
||
V::Array(arr) => arr.iter().any(|x| json_equal(left, x, weak)),
|
||
V::Object(map) => match left { V::String(k) => {
|
||
if weak { map.keys().any(|kk| norm_str(kk) == norm_str(k)) } else { map.contains_key(k) }
|
||
}, _ => false },
|
||
V::String(hay) => match left { V::String(needle) => {
|
||
if weak { norm_str(hay).contains(&norm_str(needle)) } else { 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, weak_eq),
|
||
("not equal" | "!=" | "not equals" | "neq", l, Some(r)) => !json_equal(l, r, weak_eq),
|
||
|
||
// 数字比较
|
||
("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, weak_eq),
|
||
("not contains", l, Some(r)) => !contains(l, r, weak_eq),
|
||
|
||
// 成员关系:left in right / not in
|
||
("in", l, Some(r)) => in_op(l, r, weak_eq),
|
||
("not in" | "nin", l, Some(r)) => !in_op(l, r, weak_eq),
|
||
|
||
// 为空 / 非空(字符串、数组、对象、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,
|
||
};
|
||
|
||
// 记录调试日志,便于定位条件为何未命中
|
||
let l_dbg = match &lval { V::String(s) => format!("\"{}\"", s), _ => format!("{}", lval) };
|
||
let r_dbg = match &rval { Some(V::String(s)) => format!("\"{}\"", s), Some(v) => format!("{}", v), None => "<none>".to_string() };
|
||
info!(target = "udmin.flow", op=%op, weak=%weak_eq, left=%l_dbg, right=%r_dbg, result=%res, "condition eval");
|
||
|
||
Ok(res)
|
||
}
|
||
|
||
pub(crate) fn resolve_value(ctx: &serde_json::Value, v: &serde_json::Value) -> Result<serde_json::Value> {
|
||
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(crate::flow::executors::script_rhai::eval_rhai_expr_json(expr, ctx).unwrap_or_else(|_| V::Null))
|
||
}
|
||
_ => Ok(V::Null),
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use serde_json::json;
|
||
|
||
fn cond_eq_const(left: serde_json::Value, right: serde_json::Value) -> serde_json::Value {
|
||
json!({
|
||
"left": {"type": "constant", "content": left},
|
||
"operator": "eq",
|
||
"right": {"type": "constant", "content": right}
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn and_group_all_true() {
|
||
let ctx = json!({});
|
||
let group = json!([
|
||
cond_eq_const(json!(100), json!(100)),
|
||
json!({
|
||
"left": {"type": "constant", "content": 100},
|
||
"operator": ">",
|
||
"right": {"type": "constant", "content": 10}
|
||
})
|
||
]);
|
||
let ok = eval_condition_json(&ctx, &group).unwrap();
|
||
assert!(ok);
|
||
}
|
||
|
||
#[test]
|
||
fn and_group_has_false() {
|
||
let ctx = json!({});
|
||
let group = json!([
|
||
cond_eq_const(json!(100), json!(10)), // false
|
||
json!({
|
||
"left": {"type": "constant", "content": 100},
|
||
"operator": ">",
|
||
"right": {"type": "constant", "content": 10}
|
||
})
|
||
]);
|
||
let ok = eval_condition_json(&ctx, &group).unwrap();
|
||
assert!(!ok);
|
||
}
|
||
} |