feat(flow): 改进 Rhai 脚本执行错误处理和前后端代码节点映射

- 修改 eval_rhai_expr_json 返回 Result 以提供错误信息
- 统一使用 unwrap_or_else 处理 Rhai 表达式执行错误
- 前后端代码节点类型映射支持 JavaScript 和 Rhai 语言
- 前端代码编辑器添加语言选择器
- 优化 WebSocket 错误处理和关闭逻辑
This commit is contained in:
2025-09-22 06:52:22 +08:00
parent 067c6829f0
commit 3362268575
7 changed files with 95 additions and 87 deletions

View File

@ -99,7 +99,7 @@ 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);
scope.push_dynamic("ctx", dyn_ctx);
// 先从缓存读取 AST未命中则编译并写入缓存然后执行
let cached = AST_CACHE.with(|c| c.borrow().get(expr).cloned());
@ -107,7 +107,7 @@ fn eval_rhai_expr_bool(expr: &str, ctx: &serde_json::Value) -> bool {
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));
let compiled = RHIA_ENGINE.with(|eng| eng.borrow().compile_with_scope(&mut scope, expr));
match compiled {
Ok(ast) => {
// 简单容量控制:超过 1024 条时清空,避免无限增长
@ -123,20 +123,20 @@ fn eval_rhai_expr_bool(expr: &str, ctx: &serde_json::Value) -> bool {
}
// 通用:评估 Rhai 表达式并转换为 serde_json::Value失败返回 None
pub(crate) fn eval_rhai_expr_json(expr: &str, ctx: &serde_json::Value) -> Option<serde_json::Value> {
pub(crate) fn eval_rhai_expr_json(expr: &str, ctx: &serde_json::Value) -> Result<serde_json::Value, String> {
// 构造作用域并注入 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);
scope.push_dynamic("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> {
let eval = |ast: &AST, scope: &mut rhai::Scope| -> Result<serde_json::Value, String> {
RHIA_ENGINE.with(|eng| {
eng.borrow()
.eval_ast_with_scope::<rhai::Dynamic>(scope, ast)
.ok()
.and_then(|d| rhai::serde::from_dynamic(&d).ok())
.map_err(|e| e.to_string())
.and_then(|d| rhai::serde::from_dynamic(&d).map_err(|e| e.to_string()))
})
};
@ -144,7 +144,7 @@ pub(crate) fn eval_rhai_expr_json(expr: &str, ctx: &serde_json::Value) -> Option
return eval(&ast, &mut scope);
}
let compiled = RHIA_ENGINE.with(|eng| eng.borrow().compile(expr));
let compiled = RHIA_ENGINE.with(|eng| eng.borrow().compile_with_scope(&mut scope, expr));
match compiled {
Ok(ast) => {
AST_CACHE.with(|c| {
@ -154,7 +154,7 @@ pub(crate) fn eval_rhai_expr_json(expr: &str, ctx: &serde_json::Value) -> Option
});
eval(&ast, &mut scope)
}
Err(_) => None,
Err(e) => Err(e.to_string()),
}
}

View File

@ -166,7 +166,7 @@ pub(crate) fn resolve_value(ctx: &serde_json::Value, v: &serde_json::Value) -> R
"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::engine::eval_rhai_expr_json(expr, ctx).unwrap_or(V::Null))
Ok(crate::flow::engine::eval_rhai_expr_json(expr, ctx).unwrap_or_else(|_| V::Null))
}
_ => Ok(V::Null),
}

View File

@ -62,7 +62,7 @@ pub fn exec_rhai_file(node_id: &NodeId, path: &str, ctx: &mut Value) -> anyhow::
let res = eval_rhai_expr_json(&wrapped, ctx);
let dur_ms = start.elapsed().as_millis();
match res {
Some(new_ctx) => {
Ok(new_ctx) => {
let (added, removed, modified) = shallow_diff(&before_ctx, &new_ctx);
*ctx = new_ctx;
info!(target = "udmin.flow", node=%node_id.0, ms=%dur_ms, added=%added.len(), removed=%removed.len(), modified=%modified.len(), "script task: Rhai file executed and ctx updated");
@ -70,8 +70,8 @@ pub fn exec_rhai_file(node_id: &NodeId, path: &str, ctx: &mut Value) -> anyhow::
debug!(target = "udmin.flow", node=%node_id.0, ?added, ?removed, ?modified, "script task: ctx shallow diff");
}
}
None => {
info!(target = "udmin.flow", node=%node_id.0, ms=%dur_ms, preview=%preview, "script task: Rhai file execution failed, ctx unchanged");
Err(err) => {
info!(target = "udmin.flow", node=%node_id.0, ms=%dur_ms, preview=%preview, err=%err, "script task: Rhai file execution failed, ctx unchanged");
}
}
Ok(())
@ -114,10 +114,10 @@ impl Executor for ScriptRhaiTask {
let before_ctx = ctx.clone();
let wrapped = format!("{{ {} ; ctx }}", script);
let res = super::super::engine::eval_rhai_expr_json(&wrapped, ctx);
let res = eval_rhai_expr_json(&wrapped, ctx);
let dur_ms = start.elapsed().as_millis();
match res {
Some(new_ctx) => {
Ok(new_ctx) => {
let (added, removed, modified) = shallow_diff(&before_ctx, &new_ctx);
*ctx = new_ctx;
info!(target = "udmin.flow", node=%node_id.0, ms=%dur_ms, added=%added.len(), removed=%removed.len(), modified=%modified.len(), "script_rhai task: inline executed and ctx updated");
@ -125,8 +125,8 @@ impl Executor for ScriptRhaiTask {
debug!(target = "udmin.flow", node=%node_id.0, ?added, ?removed, ?modified, "script_rhai task: ctx shallow diff");
}
}
None => {
info!(target = "udmin.flow", node=%node_id.0, ms=%dur_ms, preview=%script_preview, "script_rhai task: inline execution failed, ctx unchanged");
Err(err) => {
info!(target = "udmin.flow", node=%node_id.0, ms=%dur_ms, preview=%script_preview, err=%err, "script_rhai task: inline execution failed, ctx unchanged");
}
}
return Ok(());

View File

@ -56,7 +56,7 @@ fn resolve_assign_value(ctx: &Value, v: &Value) -> Value {
}
// ctx[...] / ctx. 前缀 -> 表达式求值
if s_trim.starts_with("ctx[") || s_trim.starts_with("ctx.") {
return eval_rhai_expr_json(s_trim, ctx).unwrap_or(V::Null);
return eval_rhai_expr_json(s_trim, ctx).unwrap_or_else(|_| V::Null);
}
}
v.get("content").cloned().unwrap_or(V::Null)
@ -97,7 +97,7 @@ fn resolve_assign_value(ctx: &Value, v: &Value) -> Value {
"expression" => {
let expr = v.get("content").and_then(|x| x.as_str()).unwrap_or("");
if expr.trim().is_empty() { return V::Null; }
eval_rhai_expr_json(expr, ctx).unwrap_or(V::Null)
eval_rhai_expr_json(expr, ctx).unwrap_or_else(|_| V::Null)
}
_ => {
// fallback: if content exists, treat as constant