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

@ -40,13 +40,18 @@ function getFlowIdFromUrl(): string {
}
// 新增:针对后端的 design_json 兼容性转换
// - 将前端 UI 的 type: 'code' 映射为后端可识别并映射到 script_js 执行器的 'javascript'
// - 将前端 UI 的 type: 'code' 按语言映射javascript -> 'javascript'rhai -> 'script'(后端映射到 script_rhai
// - 其余字段保持不变
function transformDesignJsonForBackend(json: any): any {
try {
const clone = JSON.parse(JSON.stringify(json));
clone.nodes = (clone.nodes || []).map((n: any) => {
if (n && n.type === 'code') {
const lang = n?.data?.script?.language;
if (lang === 'rhai') {
return { ...n, type: 'script' };
}
// 默认或显式 javascript
return { ...n, type: 'javascript' };
}
return n;
@ -78,7 +83,7 @@ export class CustomService {
}
const json = this.document.toJSON() as any;
const yaml = stringifyFlowDoc(json);
// 使用转换后的 design_json以便后端将 code 节点识别为 javascript 并选择 script_js 执行器
// 使用转换后的 design_json以便后端根据语言选择正确的执行器
const designForBackend = transformDesignJsonForBackend(json);
const design_json = JSON.stringify(designForBackend);
const { data } = await api.put<ApiResp<{ saved: boolean }>>(`/flows/${id}`, { yaml, design_json });
@ -219,23 +224,24 @@ export class CustomService {
ws.onerror = (ev: Event) => {
if (!finished) {
handlers?.onFatal?.(new Error('WebSocket error'));
rejectDone(new Error('WebSocket error'));
}
};
ws.onclose = () => {
if (!finished) {
resolveDone(null);
handlers?.onFatal?.(new Error('WebSocket closed'));
rejectDone(new Error('WebSocket closed'));
}
};
const cancel = () => {
try { finished = true; ws?.close(); } catch {}
};
return { cancel, done } as const;
return {
cancel: () => { try { ws?.close(); } catch {} },
done,
} as const;
}
// 现有:SSE 流式运行
// SSE 方案保留
runStream(input: any = {}, handlers?: { onNode?: (e: StreamEvent & { type: 'node' }) => void; onDone?: (e: StreamEvent & { type: 'done' }) => void; onError?: (e: StreamEvent & { type: 'error' }) => void; onFatal?: (err: Error) => void; }) {
const id = getFlowIdFromUrl();
if (!id) {
@ -244,55 +250,20 @@ export class CustomService {
return { cancel: () => {}, done: Promise.resolve<RunResult | null>(null) } as const;
}
// 在开发环境通过 Vite 代理前缀 /sse 转发到 8866vite.config.ts 已配置 rewrite 到 /api
const useSseProxy = !!((import.meta as any).env?.DEV);
let url: string;
if (useSseProxy) {
url = `/sse/flows/${id}/run/stream`;
} else {
const base = (api.defaults.baseURL || '') as string;
// 参照 WSSSE 使用独立端口,默认 8866可通过 VITE_SSE_PORT 覆盖
const ssePort = (import.meta as any).env?.VITE_SSE_PORT || '8866';
let sseBase: string;
if (base.startsWith('http://') || base.startsWith('https://')) {
const res = postSSE(`/flows/${id}/run/stream`, { input }, {
onMessage: (evt: StreamEvent) => {
try {
const u = new URL(base);
// 协议保持与 base 一致,仅替换端口
u.port = ssePort;
sseBase = `${u.protocol}//${u.host}${u.pathname.replace(/\/$/, '')}`;
} catch {
const loc = window.location;
sseBase = `${loc.protocol}//${loc.hostname}:${ssePort}${base.startsWith('/') ? base : '/' + base}`;
const e = evt;
if (e.type === 'node') { handlers?.onNode?.(e as any); return; }
if (e.type === 'error') { handlers?.onError?.(e as any); return; }
if (e.type === 'done') { handlers?.onDone?.(e as any); return; }
} catch (err: unknown) {
// ignore
}
} else {
const loc = window.location;
sseBase = `${loc.protocol}//${loc.hostname}:${ssePort}${base.startsWith('/') ? base : '/' + base}`;
}
url = sseBase + `/flows/${id}/run/stream`;
}
const { cancel, done } = postSSE<RunResult | null>(url, { input }, {
onMessage: (json: any) => {
try {
const evt = json as StreamEvent
if (evt.type === 'node') {
handlers?.onNode?.(evt as any)
return undefined
}
if (evt.type === 'error') {
handlers?.onError?.(evt as any)
return undefined
}
if (evt.type === 'done') {
handlers?.onDone?.(evt as any)
return { ok: (evt as any).ok, ctx: (evt as any).ctx, logs: (evt as any).logs }
}
} catch (_) {}
return undefined
},
onFatal: (e: any) => handlers?.onFatal?.(e),
})
return { cancel, done } as const;
onFatal: (err: Error) => handlers?.onFatal?.(err),
});
return res;
}
}