- 节点尝试级 AbortController:run 取消与节点超时汇聚到同一信号, handler 收到的 ctx.signal 即尝试信号,其内部 sleep/HTTP/子进程可被真正中止 - 超时中止底层后 handler 先抛 cancelled 的竞态归一为 timeout - executeNode 取消映射 NodeState=cancelled:不适用 skipOnError、不进异常分支 - 重试门控:CapabilityMeta.idempotent===false 时无视 retryCount(防重复副作用) - 新增 validateFlowGraph:重复ID/悬空边/自环/顶层环/孤儿节点/不可达节点, 阻断性错误在 runFlow 内统一收口为 validation_error - 新增 11 项测试;既有 error-route/integration 回归通过
118 lines
4.3 KiB
TypeScript
118 lines
4.3 KiB
TypeScript
import type { FlowDefinition } from "../types";
|
||
|
||
/**
|
||
* 流程图静态校验(任务:审查报告 P0-03)。
|
||
* 在执行前拦截结构性错误,避免运行期以诡异方式失败。
|
||
* 只做纯结构检查,不读目录/注册表(组件是否存在由运行时校验并给出友好错误)。
|
||
*/
|
||
|
||
export interface FlowIssue {
|
||
severity: "error" | "warning";
|
||
code: string;
|
||
nodeId?: string;
|
||
edgeId?: string;
|
||
message: string;
|
||
}
|
||
|
||
const CONTROL_FLOW_COMPONENTS = new Set([
|
||
"logic_control_if_else",
|
||
"logic_control_count_for",
|
||
"logic_control_array_for_v3",
|
||
"logic_control_dictionary_for_v1",
|
||
"logic_control_while",
|
||
"infinite_loop",
|
||
"logic_control_try_catch"
|
||
]);
|
||
|
||
export function validateFlowGraph(flow: FlowDefinition): FlowIssue[] {
|
||
const issues: FlowIssue[] = [];
|
||
const nodeIds = new Set<string>();
|
||
|
||
for (const node of flow.nodes) {
|
||
if (nodeIds.has(node.id)) {
|
||
issues.push({ severity: "error", code: "duplicate_node_id", nodeId: node.id, message: `节点 ID 重复:${node.id}` });
|
||
}
|
||
nodeIds.add(node.id);
|
||
if (node.parentId && !flow.nodes.some((candidate) => candidate.id === node.parentId)) {
|
||
issues.push({
|
||
severity: "error",
|
||
code: "orphan_child",
|
||
nodeId: node.id,
|
||
message: `节点「${node.label || node.id}」挂在不存在的父节点 ${node.parentId} 下`
|
||
});
|
||
}
|
||
}
|
||
|
||
for (const edge of flow.edges) {
|
||
if (!nodeIds.has(edge.source)) {
|
||
issues.push({ severity: "error", code: "dangling_edge", edgeId: edge.id, message: `连线 ${edge.id} 的起点 ${edge.source} 不存在于画布` });
|
||
}
|
||
if (!nodeIds.has(edge.target)) {
|
||
issues.push({ severity: "error", code: "dangling_edge", edgeId: edge.id, message: `连线 ${edge.id} 的终点 ${edge.target} 不存在于画布` });
|
||
}
|
||
if (edge.source === edge.target && nodeIds.has(edge.source)) {
|
||
issues.push({
|
||
severity: "error",
|
||
code: "self_loop",
|
||
edgeId: edge.id,
|
||
nodeId: edge.source,
|
||
message: `节点「${edge.source}」连接到自己,执行时会立即跳过`
|
||
});
|
||
}
|
||
}
|
||
|
||
// 顶层图环检测:执行引擎从起点沿边游走且不重访,环意味着存在永远不会执行的分支或意图不明的回边
|
||
const top = flow.nodes.filter((node) => !node.parentId);
|
||
const topIds = new Set(top.map((node) => node.id));
|
||
const edges = flow.edges.filter((edge) => topIds.has(edge.source) && topIds.has(edge.target));
|
||
const adjacency = new Map<string, string[]>();
|
||
for (const edge of edges) {
|
||
if (edge.source === edge.target) continue;
|
||
adjacency.set(edge.source, [...(adjacency.get(edge.source) || []), 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];
|
||
|
||
if (start) {
|
||
const state = new Map<string, "visiting" | "done">();
|
||
const reachable = new Set<string>();
|
||
const visit = (nodeId: string, path: string[]): void => {
|
||
if (state.get(nodeId) === "done") return;
|
||
if (state.get(nodeId) === "visiting") {
|
||
const cycleStart = path.indexOf(nodeId);
|
||
const cycle = [...path.slice(cycleStart), nodeId].join(" → ");
|
||
issues.push({ severity: "error", code: "cycle", nodeId, message: `顶层流程存在环,环上的节点执行顺序不明确:${cycle}` });
|
||
return;
|
||
}
|
||
state.set(nodeId, "visiting");
|
||
reachable.add(nodeId);
|
||
for (const next of adjacency.get(nodeId) || []) visit(next, [...path, nodeId]);
|
||
state.set(nodeId, "done");
|
||
};
|
||
visit(start.id, [start.id]);
|
||
|
||
for (const node of top) {
|
||
if (!reachable.has(node.id)) {
|
||
const knownContainer = CONTROL_FLOW_COMPONENTS.has(node.componentId);
|
||
if (!knownContainer && !flow.edges.some((edge) => edge.target === node.id)) {
|
||
issues.push({
|
||
severity: "warning",
|
||
code: "unreachable_node",
|
||
nodeId: node.id,
|
||
message: `节点「${node.label || node.id}」没有从起点可达的连线,执行时不会运行`
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return issues;
|
||
}
|
||
|
||
export function hasBlockingIssues(issues: FlowIssue[]): boolean {
|
||
return issues.some((issue) => issue.severity === "error");
|
||
}
|