Files
PRA-only-demo/src/main/engine/runtime.ts
T

479 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { FlowDefinition, FlowNode, RunError } from "../../shared/types";
import { evaluate, evaluateCondition, interpolate, toNumber } from "./expression";
import { getHandler } from "./registry";
import {
asRuntimeError,
BreakSignal,
ContinueSignal,
RuntimeError,
StopSignal,
type NodeState,
type RunContext,
type RunResult
} from "./types";
const MAX_LOOP = 10000;
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
if (ms <= 0) return Promise.resolve();
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
signal?.removeEventListener("abort", onAbort);
resolve();
}, ms);
const onAbort = () => {
clearTimeout(timer);
reject(new RuntimeError("cancelled", "运行已取消"));
};
if (signal?.aborted) return onAbort();
signal?.addEventListener("abort", onAbort, { once: true });
});
}
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, signal: AbortSignal, label: string): Promise<T> {
return new Promise<T>((resolve, reject) => {
let settled = false;
const finish = (fn: () => void) => {
if (settled) return;
settled = true;
clearTimeout(timer);
signal.removeEventListener("abort", onAbort);
fn();
};
const timer = setTimeout(() => {
finish(() => reject(new RuntimeError("timeout", `${label} 执行超时(${timeoutMs}ms)`, {
recommendation: "检查目标是否可达,或调大该节点的超时时间"
})));
}, timeoutMs > 0 ? timeoutMs : 30000);
const onAbort = () => finish(() => reject(new RuntimeError("cancelled", "运行已取消")));
if (signal.aborted) return onAbort();
signal.addEventListener("abort", onAbort, { once: true });
promise.then(
(value) => finish(() => resolve(value)),
(error) => finish(() => reject(error))
);
});
}
function isRetryable(code: string): boolean {
return !["validation_error", "capability_not_implemented", "blocked_url", "cancelled", "expression_error"].includes(code);
}
function resolveArgs(node: FlowNode, component: { fields: Array<{ key: string; defaultValue: string }> }, vars: Record<string, unknown>): Record<string, unknown> {
const args: Record<string, unknown> = {};
for (const field of component.fields) {
const raw = Object.prototype.hasOwnProperty.call(node.values, field.key) ? node.values[field.key] : field.defaultValue;
try {
args[field.key] = interpolate(raw, vars);
} catch (error) {
throw new RuntimeError("expression_error", `参数 ${field.key} 求值失败:${(error as Error).message}`, {
arg: field.key,
recommendation: "检查 ${...} 中的变量名与表达式语法"
});
}
}
for (const [key, value] of Object.entries(node.values || {})) {
if (key in args) continue;
try {
args[key] = interpolate(value, vars);
} catch (error) {
throw new RuntimeError("expression_error", `参数 ${key} 求值失败:${(error as Error).message}`, { arg: key });
}
}
return args;
}
function childChain(flow: FlowDefinition, parentId: string, slot: string): FlowNode[] {
return flow.nodes.filter((node) => node.parentId === parentId && node.slot === slot);
}
function asArray(value: unknown): unknown[] {
if (Array.isArray(value)) return value;
if (value == null || value === "") return [];
if (typeof value === "string") {
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed : [parsed];
} catch {
return value.split(/[,,\n]/).map((item) => item.trim()).filter(Boolean);
}
}
if (typeof value === "object") return Object.values(value as Record<string, unknown>);
return [value];
}
export async function runFlow(
flow: FlowDefinition,
options: {
runId: string;
evidenceDir: string;
signal: AbortSignal;
emit: RunContext["emit"];
log: RunContext["log"];
componentOf: RunContext["componentOf"];
browser?: RunContext["browser"];
variables?: Record<string, unknown>;
}
): Promise<RunResult> {
const startedAt = new Date().toISOString();
const startMs = Date.now();
const vars: Record<string, unknown> = { ...(options.variables || {}) };
const ctx: RunContext = {
runId: options.runId,
flowId: flow.id,
flow,
vars,
secrets: {},
signal: options.signal,
evidenceDir: options.evidenceDir,
browser: options.browser,
emit: options.emit,
log: options.log,
componentOf: options.componentOf
};
const states = new Map<string, NodeState>();
let status: RunResult["status"] = "succeeded";
let failure: RunError | undefined;
options.emit({ type: "run.started", startedAt });
try {
await executeTopLevelGraph(flow, ctx, states);
} catch (error) {
if (error instanceof StopSignal) {
options.log("流程被「退出流程」节点终止", "warn");
status = "succeeded";
} else if (error instanceof BreakSignal || error instanceof ContinueSignal) {
failure = { code: "validation_error", reason: "跳出/继续语句不在循环体内" };
status = "failed";
} else {
const runtimeError = asRuntimeError(error);
failure = runtimeError;
status = runtimeError.code === "cancelled" ? "cancelled" : "failed";
}
}
const finishedAt = new Date().toISOString();
const durationMs = Date.now() - startMs;
options.emit({
type: "run.finished",
status,
durationMs,
error: failure,
startedAt
});
return {
runId: options.runId,
status,
startedAt,
finishedAt,
durationMs,
nodes: [...states.values()],
variables: vars,
error: failure
};
}
async function executeTopLevelGraph(flow: FlowDefinition, ctx: RunContext, states: Map<string, NodeState>): Promise<void> {
const top = flow.nodes.filter((node) => !node.parentId);
if (!top.length) return;
if (!flow.edges.length) {
for (const node of [...top].sort((a, b) => a.position.y - b.position.y || a.position.x - b.position.x)) {
if (ctx.signal.aborted) throw new RuntimeError("cancelled", "运行已取消");
await executeNode(node, ctx, states);
}
return;
}
const topIds = new Set(top.map((node) => node.id));
const topById = new Map(top.map((node) => [node.id, node]));
const edges = flow.edges.filter((edge) => topIds.has(edge.source) && topIds.has(edge.target));
const incoming = new Set(edges.map((edge) => edge.target));
const start =
top.find((node) => !incoming.has(node.id) && node.componentId.startsWith("Trigger.")) ||
top.find((node) => !incoming.has(node.id)) ||
top[0];
const visited = new Set<string>();
const walk = async (node: FlowNode): Promise<void> => {
if (visited.has(node.id)) return;
if (ctx.signal.aborted) throw new RuntimeError("cancelled", "运行已取消");
visited.add(node.id);
try {
await executeNode(node, ctx, states);
} catch (error) {
const errorEdge = edges.find((edge) => edge.source === node.id && edge.sourceHandle === "error");
const target = errorEdge ? topById.get(errorEdge.target) : undefined;
if (!target) throw error;
ctx.vars.error = asRuntimeError(error, node, node.componentId);
ctx.log(`节点失败,转入异常分支:${target.label || target.componentId}`, "warn", node.id);
await walk(target);
return;
}
const normalEdge = edges.find((edge) => edge.source === node.id && edge.sourceHandle !== "error");
const target = normalEdge ? topById.get(normalEdge.target) : undefined;
if (target) await walk(target);
};
await walk(start);
}
async function executeNode(node: FlowNode, ctx: RunContext, states: Map<string, NodeState>): Promise<unknown> {
const component = ctx.componentOf(node);
const state: NodeState = {
nodeId: node.id,
componentId: node.componentId,
status: "running",
startedAt: new Date().toISOString(),
evidence: [],
attempt: 1
};
states.set(node.id, state);
ctx.emit({ type: "node.started", nodeId: node.id, attempt: 1 });
if (!component) {
const error = new RuntimeError("validation_error", `组件 ${node.componentId} 不在目录中`, {
nodeId: node.id,
componentId: node.componentId,
recommendation: "重新添加该节点"
});
state.status = "failed";
state.error = error.toRunError();
state.finishedAt = new Date().toISOString();
ctx.emit({ type: "node.finished", nodeId: node.id, status: "failed", error: state.error, durationMs: 0, evidence: [] });
throw error;
}
if (node.enabled === false) {
state.status = "skipped";
state.finishedAt = new Date().toISOString();
ctx.emit({ type: "node.finished", nodeId: node.id, status: "skipped", durationMs: 0, evidence: [] });
return undefined;
}
const startMs = Date.now();
try {
let result: unknown;
if (component.slots.length > 0) {
result = await executeContainer(node, component.id, ctx, states);
} else {
const handler = getHandler(node.componentId) || getHandler(component.functionName);
if (!handler) {
throw new RuntimeError(
"capability_not_implemented",
`组件「${component.name}」当前版本未接入执行器`,
{
nodeId: node.id,
componentId: node.componentId,
recommendation: "请检查组件配置或重新添加节点"
}
);
}
const args = resolveArgs(node, component, ctx.vars);
const delayBefore = Math.max(0, Number(node.options?.delayBeforeMs || 0));
const delayAfter = Math.max(0, Number(node.options?.delayAfterMs || 0));
const timeoutMs = Math.max(0, Number(node.options?.timeoutMs || 30000));
const retries = Math.max(0, Math.min(100, Number(node.options?.retryCount || 0)));
const interval = Math.max(1000, Number(node.options?.retryIntervalMs || 1000));
await sleep(delayBefore, ctx.signal);
for (let attempt = 1; attempt <= retries + 1; attempt += 1) {
state.attempt = attempt;
try {
result = await withTimeout(
Promise.resolve(handler(ctx, node, args, component)),
timeoutMs,
ctx.signal,
component.name
);
break;
} catch (error) {
if (error instanceof BreakSignal || error instanceof ContinueSignal || error instanceof StopSignal) throw error;
const runtimeError = asRuntimeError(error, node, component.id);
if (attempt > retries || !isRetryable(runtimeError.code)) {
throw new RuntimeError(runtimeError.code, runtimeError.reason, {
recommendation: runtimeError.recommendation,
nodeId: node.id,
componentId: component.id,
detail: runtimeError.detail
});
}
ctx.log(
`第 ${attempt} 次执行失败:${runtimeError.reason},${Math.round(interval / 1000)} 秒后重试`,
"warn",
node.id
);
await sleep(interval, ctx.signal);
}
}
if (node.options?.outputVar) ctx.vars[node.options.outputVar] = result;
await sleep(delayAfter, ctx.signal);
}
state.status = "succeeded";
state.output = result;
state.finishedAt = new Date().toISOString();
state.durationMs = Date.now() - startMs;
ctx.emit({
type: "node.finished",
nodeId: node.id,
status: "succeeded",
output: result,
durationMs: state.durationMs,
evidence: state.evidence
});
return result;
} catch (error) {
if (error instanceof BreakSignal || error instanceof ContinueSignal || error instanceof StopSignal) {
state.status = "skipped";
state.finishedAt = new Date().toISOString();
state.durationMs = Date.now() - startMs;
ctx.emit({ type: "node.finished", nodeId: node.id, status: "skipped", durationMs: state.durationMs, evidence: [] });
throw error;
}
const runtimeError = asRuntimeError(error, node, component.id);
state.error = runtimeError;
state.finishedAt = new Date().toISOString();
state.durationMs = Date.now() - startMs;
const skip = Boolean(node.options?.skipOnError);
state.status = skip ? "skipped" : "failed";
ctx.log(`${component.name} ${skip ? "失败但已跳过" : "失败"}:${runtimeError.reason}`, "error", node.id);
if (node.options?.logException && runtimeError.detail) ctx.log(runtimeError.detail, "error", node.id);
ctx.emit({
type: "node.finished",
nodeId: node.id,
status: state.status,
error: runtimeError,
durationMs: state.durationMs,
evidence: []
});
if (skip) return undefined;
throw error;
}
}
async function executeContainer(
node: FlowNode,
componentId: string,
ctx: RunContext,
states: Map<string, NodeState>
): Promise<unknown> {
const runSlot = async (slot: string) => {
for (const child of childChain(ctx.flow, node.id, slot)) {
if (ctx.signal.aborted) throw new RuntimeError("cancelled", "运行已取消");
await executeNode(child, ctx, states);
}
};
switch (componentId) {
case "logic_control_if_else": {
const condition = evaluateCondition(node.values?.condition, ctx.vars);
ctx.log(`条件 ${condition ? "成立" : "不成立"}:${String(node.values?.condition ?? "")}`, "info", node.id);
await runSlot(condition ? "statement_if" : "statement_else");
return condition;
}
case "logic_control_count_for": {
const start = toNumber(interpolate(node.values?.count_start_value, ctx.vars), 1);
const end = toNumber(interpolate(node.values?.count_end_value, ctx.vars), 1);
const step = toNumber(interpolate(node.values?.count_step, ctx.vars), 1) || 1;
const indexName = String(interpolate(node.values?.count_index, ctx.vars) || "index");
let iterations = 0;
for (let value = start; step > 0 ? value <= end : value >= end; value += step) {
if (iterations++ > MAX_LOOP) throw new RuntimeError("validation_error", "循环次数超过上限 10000");
ctx.vars[indexName] = value;
try {
await runSlot("count_for_fn");
} catch (error) {
if (error instanceof BreakSignal) break;
if (error instanceof ContinueSignal) continue;
throw error;
}
}
return iterations;
}
case "logic_control_array_for_v3":
case "logic_control_dictionary_for_v1": {
const raw = node.values?.array_name;
let value = interpolate(raw, ctx.vars);
if (typeof value === "string") {
const name = value.trim();
if (name in ctx.vars) value = ctx.vars[name];
else {
try {
value = JSON.parse(name);
} catch {
value = asArray(name);
}
}
}
const list = asArray(value);
const reverse = String(node.values?.reverse ?? "") === "true" || String(node.values?.reverse ?? "") === "1";
const start = Math.max(0, toNumber(node.values?.start_index, 0));
const requestedEnd = node.values?.end_index === "" || node.values?.end_index == null ? list.length : toNumber(node.values?.end_index, list.length);
const end = requestedEnd < 0 ? Math.max(start, list.length + requestedEnd + 1) : requestedEnd;
const slice = (reverse ? [...list].reverse() : list).slice(start, end);
let iterations = 0;
for (let index = 0; index < slice.length; index += 1) {
if (iterations++ > MAX_LOOP) throw new RuntimeError("validation_error", "循环次数超过上限 10000");
ctx.vars.item = slice[index];
ctx.vars.index = index;
ctx.vars.value = slice[index];
try {
await runSlot("array_for_fn");
} catch (error) {
if (error instanceof BreakSignal) break;
if (error instanceof ContinueSignal) continue;
throw error;
}
}
return iterations;
}
case "logic_control_while": {
let iterations = 0;
while (evaluateCondition(node.values?.if_for_condition, ctx.vars)) {
if (iterations++ > MAX_LOOP) throw new RuntimeError("validation_error", "循环次数超过上限 10000");
try {
await runSlot("if_for_fn");
} catch (error) {
if (error instanceof BreakSignal) break;
if (error instanceof ContinueSignal) continue;
throw error;
}
}
return iterations;
}
case "infinite_loop": {
for (let iterations = 0; ; iterations += 1) {
if (iterations > MAX_LOOP) throw new RuntimeError("validation_error", "循环次数超过上限 10000");
try {
await runSlot("loop_body");
} catch (error) {
if (error instanceof BreakSignal) break;
if (error instanceof ContinueSignal) continue;
throw error;
}
}
return undefined;
}
case "logic_control_try_catch": {
try {
await runSlot("try_fn");
await runSlot("else_fn");
return "else";
} catch (error) {
if (error instanceof BreakSignal || error instanceof ContinueSignal || error instanceof StopSignal) throw error;
ctx.vars.error = asRuntimeError(error, node, componentId);
await runSlot("except_fn");
return "catch";
}
}
default: {
const slot = componentId === "Element_exception_monitor_v2" ? "exception_monitor_params" : "body";
await runSlot(slot);
return undefined;
}
}
}
export { evaluate, evaluateCondition, interpolate };