fix(flow): 修复Rhai脚本执行错误处理并优化变量解析逻辑
refactor(engine): 重构Rhai表达式错误处理为枚举类型 fix(script_rhai): 修正脚本文件读取和执行失败的错误返回 perf(testrun): 优化前端测试面板日志去重和显示逻辑
This commit is contained in:
@ -122,8 +122,27 @@ 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) -> Result<serde_json::Value, String> {
|
||||
// 通用:评估 Rhai 表达式并转换为 serde_json::Value,失败返回错误
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RhaiExecError {
|
||||
Compile { message: String },
|
||||
Runtime { message: String },
|
||||
Serde { message: String },
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RhaiExecError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
RhaiExecError::Compile { message } => write!(f, "compile error: {}", message),
|
||||
RhaiExecError::Runtime { message } => write!(f, "runtime error: {}", message),
|
||||
RhaiExecError::Serde { message } => write!(f, "serde error: {}", message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RhaiExecError {}
|
||||
|
||||
pub(crate) fn eval_rhai_expr_json(expr: &str, ctx: &serde_json::Value) -> Result<serde_json::Value, RhaiExecError> {
|
||||
// 构造作用域并注入 ctx
|
||||
let mut scope = rhai::Scope::new();
|
||||
let dyn_ctx = match rhai::serde::to_dynamic(ctx.clone()) { Ok(d) => d, Err(_) => rhai::Dynamic::UNIT };
|
||||
@ -131,12 +150,12 @@ pub(crate) fn eval_rhai_expr_json(expr: &str, ctx: &serde_json::Value) -> Result
|
||||
|
||||
// 先从缓存读取 AST;未命中则编译并写入缓存,然后执行
|
||||
let cached = AST_CACHE.with(|c| c.borrow().get(expr).cloned());
|
||||
let eval = |ast: &AST, scope: &mut rhai::Scope| -> Result<serde_json::Value, String> {
|
||||
let eval = |ast: &AST, scope: &mut rhai::Scope| -> Result<serde_json::Value, RhaiExecError> {
|
||||
RHIA_ENGINE.with(|eng| {
|
||||
eng.borrow()
|
||||
.eval_ast_with_scope::<rhai::Dynamic>(scope, ast)
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|d| rhai::serde::from_dynamic(&d).map_err(|e| e.to_string()))
|
||||
.map_err(|e| RhaiExecError::Runtime { message: e.to_string() })
|
||||
.and_then(|d| rhai::serde::from_dynamic(&d).map_err(|e| RhaiExecError::Serde { message: e.to_string() }))
|
||||
})
|
||||
};
|
||||
|
||||
@ -154,7 +173,7 @@ pub(crate) fn eval_rhai_expr_json(expr: &str, ctx: &serde_json::Value) -> Result
|
||||
});
|
||||
eval(&ast, &mut scope)
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
Err(e) => Err(RhaiExecError::Compile { message: e.to_string() }),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ use std::time::Instant;
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use tracing::{debug, info};
|
||||
use anyhow::anyhow;
|
||||
|
||||
// crate
|
||||
use crate::flow::domain::{NodeDef, NodeId};
|
||||
@ -46,7 +47,7 @@ pub fn exec_rhai_file(node_id: &NodeId, path: &str, ctx: &mut Value) -> anyhow::
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
info!(target = "udmin.flow", node=%node_id.0, err=%e.to_string(), "script task: failed to read Rhai file");
|
||||
return Ok(());
|
||||
return Err(anyhow!("failed to read Rhai file: {}", e));
|
||||
}
|
||||
};
|
||||
let script = code;
|
||||
@ -71,7 +72,8 @@ pub fn exec_rhai_file(node_id: &NodeId, path: &str, ctx: &mut Value) -> anyhow::
|
||||
}
|
||||
}
|
||||
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");
|
||||
info!(target = "udmin.flow", node=%node_id.0, ms=%dur_ms, preview=%preview, err=%err.to_string(), "script task: Rhai file execution failed, ctx unchanged");
|
||||
return Err(anyhow!("Rhai file execution failed: {}", err));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@ -126,7 +128,8 @@ impl Executor for ScriptRhaiTask {
|
||||
}
|
||||
}
|
||||
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");
|
||||
info!(target = "udmin.flow", node=%node_id.0, ms=%dur_ms, preview=%script_preview, err=%err.to_string(), "script_rhai task: inline execution failed, ctx unchanged");
|
||||
return Err(anyhow!("Rhai inline execution failed: {}", err));
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
|
||||
@ -55,9 +55,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_else(|_| V::Null);
|
||||
}
|
||||
return eval_rhai_expr_json(s_trim, ctx).unwrap_or_else(|_| V::Null);
|
||||
}
|
||||
v.get("content").cloned().unwrap_or(V::Null)
|
||||
}
|
||||
|
||||
@ -108,7 +108,23 @@ export const TestRunSidePanel: FC<TestRunSidePanelProps> = ({ visible, onCancel
|
||||
? customService.runStreamWS(values, {
|
||||
onNode: (evt) => {
|
||||
if (evt.ctx) setStreamCtx((prev: any) => ({ ...(prev || {}), ...(evt.ctx || {}) }));
|
||||
if (evt.logs && evt.logs.length) setStreamLogs((prev: string[]) => [...prev, ...evt.logs!]);
|
||||
if (evt.logs && evt.logs.length) {
|
||||
const normalizeLog = (s: string) => s.replace(/\r/g, '').trim();
|
||||
const dedupLogs = (arr: string[]) => {
|
||||
const seen = new Set<string>();
|
||||
const res: string[] = [];
|
||||
for (const s of arr) {
|
||||
const key = normalizeLog(s);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
res.push(s);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
const incoming = evt.logs!;
|
||||
setStreamLogs((prev) => dedupLogs([...(prev || []), ...incoming]));
|
||||
}
|
||||
},
|
||||
onError: (evt) => {
|
||||
const msg = evt.message || I18n.t('Run failed');
|
||||
@ -282,13 +298,18 @@ export const TestRunSidePanel: FC<TestRunSidePanelProps> = ({ visible, onCancel
|
||||
{/* 运行中(流式)时,直接在表单区域下方展示实时输出,而不是覆盖整块内容 */}
|
||||
{streamMode && isRunning && (
|
||||
<>
|
||||
<NodeStatusGroup title={I18n.t('Context') + ' (Live)'} data={streamCtx} optional disableCollapse />
|
||||
<NodeStatusGroup title={I18n.t('Logs') + ' (Live)'} data={streamLogs} optional disableCollapse />
|
||||
<NodeStatusGroup title={I18n.t('Context') + ' (Live)'} data={streamCtx} optional disableCollapse />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 展示后端返回的执行信息:仅在非流式或流式已结束时显示,避免与实时输出重复 */}
|
||||
{(!streamMode || !isRunning) && (
|
||||
<>
|
||||
<NodeStatusGroup title={I18n.t('Logs')} data={result?.logs} optional disableCollapse />
|
||||
<NodeStatusGroup title={I18n.t('Context')} data={result?.ctx} optional disableCollapse />
|
||||
</>
|
||||
)}
|
||||
{/* 展示后端返回的执行信息 */}
|
||||
<NodeStatusGroup title={I18n.t('Context')} data={result?.ctx} optional disableCollapse />
|
||||
<NodeStatusGroup title={I18n.t('Logs')} data={result?.logs} optional disableCollapse />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user