@baipeng139/72flow-nodejs 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +272 -0
- package/dist/index.cjs +1141 -0
- package/dist/index.d.cts +270 -0
- package/dist/index.d.ts +270 -0
- package/dist/index.js +1107 -0
- package/package.json +55 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
FlowContext: () => FlowContext,
|
|
24
|
+
FlowEngine: () => FlowEngine,
|
|
25
|
+
NodeExecutorFactory: () => NodeExecutorFactory,
|
|
26
|
+
NodeStatus: () => NodeStatus,
|
|
27
|
+
NodeType: () => NodeType,
|
|
28
|
+
X6Parser: () => X6Parser,
|
|
29
|
+
createLogger: () => createLogger,
|
|
30
|
+
parseX6: () => parseX6
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
|
|
34
|
+
// src/types/models.ts
|
|
35
|
+
var NodeStatus = /* @__PURE__ */ ((NodeStatus2) => {
|
|
36
|
+
NodeStatus2["PENDING"] = "PENDING";
|
|
37
|
+
NodeStatus2["RUNNING"] = "RUNNING";
|
|
38
|
+
NodeStatus2["COMPLETED"] = "COMPLETED";
|
|
39
|
+
NodeStatus2["FAILED"] = "FAILED";
|
|
40
|
+
NodeStatus2["SKIPPED"] = "SKIPPED";
|
|
41
|
+
NodeStatus2["CANCELLED"] = "CANCELLED";
|
|
42
|
+
return NodeStatus2;
|
|
43
|
+
})(NodeStatus || {});
|
|
44
|
+
var NodeType = /* @__PURE__ */ ((NodeType3) => {
|
|
45
|
+
NodeType3["START"] = "START";
|
|
46
|
+
NodeType3["END"] = "END";
|
|
47
|
+
NodeType3["SCRIPT"] = "SCRIPT";
|
|
48
|
+
NodeType3["DECISION"] = "DECISION";
|
|
49
|
+
NodeType3["CONDITION"] = "CONDITION";
|
|
50
|
+
NodeType3["PARALLEL"] = "PARALLEL";
|
|
51
|
+
NodeType3["LOOP"] = "LOOP";
|
|
52
|
+
NodeType3["API"] = "API";
|
|
53
|
+
NodeType3["SUBFLOW"] = "SUBFLOW";
|
|
54
|
+
NodeType3["LLM"] = "LLM";
|
|
55
|
+
NodeType3["BUSINESS"] = "BUSINESS";
|
|
56
|
+
return NodeType3;
|
|
57
|
+
})(NodeType || {});
|
|
58
|
+
|
|
59
|
+
// src/core/flow-context.ts
|
|
60
|
+
var FlowContext = class {
|
|
61
|
+
executionId;
|
|
62
|
+
definition;
|
|
63
|
+
variables;
|
|
64
|
+
status = "PENDING" /* PENDING */;
|
|
65
|
+
startTime;
|
|
66
|
+
endTime;
|
|
67
|
+
// ── 性能索引(O(1) 查询)────────────────
|
|
68
|
+
nodeMap = /* @__PURE__ */ new Map();
|
|
69
|
+
outgoingMap = /* @__PURE__ */ new Map();
|
|
70
|
+
incomingMap = /* @__PURE__ */ new Map();
|
|
71
|
+
// ── 执行状态 ─────────────────────────────
|
|
72
|
+
completedNodes = /* @__PURE__ */ new Set();
|
|
73
|
+
executingNodes = /* @__PURE__ */ new Set();
|
|
74
|
+
skippedNodes = /* @__PURE__ */ new Set();
|
|
75
|
+
nodeOutputs = {};
|
|
76
|
+
convergeStates = /* @__PURE__ */ new Map();
|
|
77
|
+
traces = [];
|
|
78
|
+
constructor(executionId, definition, variables) {
|
|
79
|
+
this.executionId = executionId;
|
|
80
|
+
this.definition = definition;
|
|
81
|
+
this.variables = { ...variables };
|
|
82
|
+
this.startTime = Date.now();
|
|
83
|
+
this.buildIndex();
|
|
84
|
+
}
|
|
85
|
+
/** 构造时预建索引,所有后续查询均为 O(1) */
|
|
86
|
+
buildIndex() {
|
|
87
|
+
for (const node of this.definition.nodes) {
|
|
88
|
+
this.nodeMap.set(node.id, node);
|
|
89
|
+
}
|
|
90
|
+
for (const edge of this.definition.edges) {
|
|
91
|
+
if (!this.outgoingMap.has(edge.from)) this.outgoingMap.set(edge.from, []);
|
|
92
|
+
if (!this.incomingMap.has(edge.to)) this.incomingMap.set(edge.to, []);
|
|
93
|
+
this.outgoingMap.get(edge.from).push(edge);
|
|
94
|
+
this.incomingMap.get(edge.to).push(edge);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// ── Getters ──────────────────────────────
|
|
98
|
+
getExecutionId() {
|
|
99
|
+
return this.executionId;
|
|
100
|
+
}
|
|
101
|
+
getDefinition() {
|
|
102
|
+
return this.definition;
|
|
103
|
+
}
|
|
104
|
+
getVariables() {
|
|
105
|
+
return { ...this.variables };
|
|
106
|
+
}
|
|
107
|
+
getStatus() {
|
|
108
|
+
return this.status;
|
|
109
|
+
}
|
|
110
|
+
getStartTime() {
|
|
111
|
+
return this.startTime;
|
|
112
|
+
}
|
|
113
|
+
getEndTime() {
|
|
114
|
+
return this.endTime;
|
|
115
|
+
}
|
|
116
|
+
getDuration() {
|
|
117
|
+
return (this.endTime ?? Date.now()) - this.startTime;
|
|
118
|
+
}
|
|
119
|
+
getNodes() {
|
|
120
|
+
return this.definition.nodes;
|
|
121
|
+
}
|
|
122
|
+
getNode(id) {
|
|
123
|
+
return this.nodeMap.get(id);
|
|
124
|
+
}
|
|
125
|
+
getOutgoing(nodeId) {
|
|
126
|
+
return this.outgoingMap.get(nodeId) ?? [];
|
|
127
|
+
}
|
|
128
|
+
getIncoming(nodeId) {
|
|
129
|
+
return this.incomingMap.get(nodeId) ?? [];
|
|
130
|
+
}
|
|
131
|
+
getCompletedNodes() {
|
|
132
|
+
return this.completedNodes;
|
|
133
|
+
}
|
|
134
|
+
getSkippedNodes() {
|
|
135
|
+
return this.skippedNodes;
|
|
136
|
+
}
|
|
137
|
+
getExecutingNodes() {
|
|
138
|
+
return this.executingNodes;
|
|
139
|
+
}
|
|
140
|
+
getNodeOutputs() {
|
|
141
|
+
return { ...this.nodeOutputs };
|
|
142
|
+
}
|
|
143
|
+
getTraces() {
|
|
144
|
+
return [...this.traces];
|
|
145
|
+
}
|
|
146
|
+
setStatus(status) {
|
|
147
|
+
this.status = status;
|
|
148
|
+
if (["COMPLETED" /* COMPLETED */, "FAILED" /* FAILED */, "CANCELLED" /* CANCELLED */].includes(status)) {
|
|
149
|
+
this.endTime = Date.now();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** 设置单个变量(供 LoopExecutor 写入 item/index 等迭代变量) */
|
|
153
|
+
setVariable(key, value) {
|
|
154
|
+
this.variables[key] = value;
|
|
155
|
+
}
|
|
156
|
+
skipNode(nodeId) {
|
|
157
|
+
this.skippedNodes.add(nodeId);
|
|
158
|
+
}
|
|
159
|
+
/** 注册并行汇聚点,expected = 分支数 */
|
|
160
|
+
registerConvergence(nodeId, expected) {
|
|
161
|
+
this.convergeStates.set(nodeId, { arrived: /* @__PURE__ */ new Set(), expected });
|
|
162
|
+
}
|
|
163
|
+
// ── 前置依赖检查(带跳过传播)────────────
|
|
164
|
+
arePrerequisitesMet(nodeId) {
|
|
165
|
+
const incoming = this.getIncoming(nodeId);
|
|
166
|
+
if (incoming.length === 0) return true;
|
|
167
|
+
return incoming.every(
|
|
168
|
+
(e) => this.completedNodes.has(e.from) || this.isEffectivelySkipped(e.from, /* @__PURE__ */ new Set())
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
isEffectivelySkipped(nodeId, visited) {
|
|
172
|
+
if (this.skippedNodes.has(nodeId)) return true;
|
|
173
|
+
if (this.completedNodes.has(nodeId)) return false;
|
|
174
|
+
if (visited.has(nodeId)) return false;
|
|
175
|
+
visited.add(nodeId);
|
|
176
|
+
const incoming = this.getIncoming(nodeId);
|
|
177
|
+
if (incoming.length === 0) return false;
|
|
178
|
+
return incoming.every((e) => this.isEffectivelySkipped(e.from, visited));
|
|
179
|
+
}
|
|
180
|
+
// ── 执行状态管理 ─────────────────────────
|
|
181
|
+
tryExecute(nodeId) {
|
|
182
|
+
if (this.executingNodes.has(nodeId)) return false;
|
|
183
|
+
this.executingNodes.add(nodeId);
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
complete(nodeId, output, startTime) {
|
|
187
|
+
const endTime = Date.now();
|
|
188
|
+
this.executingNodes.delete(nodeId);
|
|
189
|
+
this.completedNodes.add(nodeId);
|
|
190
|
+
const node = this.getNode(nodeId);
|
|
191
|
+
if (node) {
|
|
192
|
+
const key = node.code ?? nodeId;
|
|
193
|
+
if (output !== void 0 && output !== null) {
|
|
194
|
+
this.nodeOutputs[key] = output;
|
|
195
|
+
}
|
|
196
|
+
if (output && typeof output === "object" && !Array.isArray(output)) {
|
|
197
|
+
for (const [k, v] of Object.entries(output)) {
|
|
198
|
+
if (!k.startsWith("__")) {
|
|
199
|
+
this.variables[k] = v;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const durationMs = endTime - startTime;
|
|
205
|
+
this.traces.push({
|
|
206
|
+
nodeId,
|
|
207
|
+
code: node?.code ?? nodeId,
|
|
208
|
+
status: "COMPLETED" /* COMPLETED */,
|
|
209
|
+
startTime,
|
|
210
|
+
endTime,
|
|
211
|
+
duration: durationMs,
|
|
212
|
+
durationNs: durationMs * 1e6,
|
|
213
|
+
// 毫秒 → 纳秒,与 Java 对齐
|
|
214
|
+
data: output
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
fail(nodeId, error, startTime) {
|
|
218
|
+
const endTime = Date.now();
|
|
219
|
+
this.executingNodes.delete(nodeId);
|
|
220
|
+
const message = error instanceof Error ? error.message : error;
|
|
221
|
+
const durationMs = endTime - startTime;
|
|
222
|
+
const failNode = this.getNode(nodeId);
|
|
223
|
+
this.traces.push({
|
|
224
|
+
nodeId,
|
|
225
|
+
code: failNode?.code ?? nodeId,
|
|
226
|
+
status: "FAILED" /* FAILED */,
|
|
227
|
+
startTime,
|
|
228
|
+
endTime,
|
|
229
|
+
duration: durationMs,
|
|
230
|
+
durationNs: durationMs * 1e6,
|
|
231
|
+
error: message
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
/** 并行汇聚:返回 true 代表当前节点所有分支已全部到达 */
|
|
235
|
+
tryConverge(nodeId, fromNodeId) {
|
|
236
|
+
const state = this.convergeStates.get(nodeId);
|
|
237
|
+
if (state) {
|
|
238
|
+
state.arrived.add(fromNodeId);
|
|
239
|
+
return state.arrived.size >= state.expected;
|
|
240
|
+
}
|
|
241
|
+
const incoming = this.getIncoming(nodeId);
|
|
242
|
+
if (incoming.length > 1) {
|
|
243
|
+
if (!this.convergeStates.has(nodeId)) {
|
|
244
|
+
this.convergeStates.set(nodeId, { arrived: /* @__PURE__ */ new Set(), expected: incoming.length });
|
|
245
|
+
}
|
|
246
|
+
const s = this.convergeStates.get(nodeId);
|
|
247
|
+
s.arrived.add(fromNodeId);
|
|
248
|
+
return s.arrived.size >= s.expected;
|
|
249
|
+
}
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
/** 获取 LOOP_START 节点的循环体路径(来自解析器预计算的 loopBodyPaths) */
|
|
253
|
+
getLoopBodyPath(loopStartNodeId) {
|
|
254
|
+
return this.definition.loopBodyPaths?.[loopStartNodeId] ?? [];
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// src/support/logger.ts
|
|
259
|
+
var isDev = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
|
|
260
|
+
function log(level, tag, message, ...args) {
|
|
261
|
+
if (level === "debug" && !isDev) return;
|
|
262
|
+
const prefix = `[72flow][${tag}]`;
|
|
263
|
+
switch (level) {
|
|
264
|
+
case "debug":
|
|
265
|
+
console.debug(prefix, message, ...args);
|
|
266
|
+
break;
|
|
267
|
+
case "info":
|
|
268
|
+
console.info(prefix, message, ...args);
|
|
269
|
+
break;
|
|
270
|
+
case "warn":
|
|
271
|
+
console.warn(prefix, message, ...args);
|
|
272
|
+
break;
|
|
273
|
+
case "error":
|
|
274
|
+
console.error(prefix, message, ...args);
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function createLogger(tag) {
|
|
279
|
+
return {
|
|
280
|
+
debug: (msg, ...args) => log("debug", tag, msg, ...args),
|
|
281
|
+
info: (msg, ...args) => log("info", tag, msg, ...args),
|
|
282
|
+
warn: (msg, ...args) => log("warn", tag, msg, ...args),
|
|
283
|
+
error: (msg, ...args) => log("error", tag, msg, ...args)
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/executors/factory.ts
|
|
288
|
+
var log2 = createLogger("Executor");
|
|
289
|
+
var NodeExecutorFactory = class {
|
|
290
|
+
static async execute(node, context) {
|
|
291
|
+
const type = String(node.type).toUpperCase();
|
|
292
|
+
log2.info(`\u6267\u884C\u8282\u70B9 [${node.id}] \u7C7B\u578B=${type}`);
|
|
293
|
+
switch (type) {
|
|
294
|
+
case "START":
|
|
295
|
+
return StartExecutor.execute(node, context);
|
|
296
|
+
case "END":
|
|
297
|
+
return EndExecutor.execute(node, context);
|
|
298
|
+
case "SCRIPT":
|
|
299
|
+
return ScriptExecutor.execute(node, context);
|
|
300
|
+
case "DECISION":
|
|
301
|
+
case "CONDITION":
|
|
302
|
+
return DecisionExecutor.execute(node, context);
|
|
303
|
+
case "PARALLEL":
|
|
304
|
+
return ParallelExecutor.execute(node, context);
|
|
305
|
+
case "LOOP":
|
|
306
|
+
return await LoopExecutor.execute(node, context);
|
|
307
|
+
case "API":
|
|
308
|
+
return await ApiExecutor.execute(node, context);
|
|
309
|
+
case "LLM":
|
|
310
|
+
return await LlmExecutor.execute(node, context);
|
|
311
|
+
default:
|
|
312
|
+
log2.warn(`\u4E0D\u652F\u6301\u7684\u8282\u70B9\u7C7B\u578B: ${type}`);
|
|
313
|
+
return { success: false, message: `Unsupported node type: ${type}` };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
var StartExecutor = class {
|
|
318
|
+
static execute(node, context) {
|
|
319
|
+
return { success: true };
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
var EndExecutor = class {
|
|
323
|
+
static execute(node, context) {
|
|
324
|
+
return { success: true, data: context.getVariables() };
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
var ScriptExecutor = class {
|
|
328
|
+
static execute(node, context) {
|
|
329
|
+
const cfg = node.config;
|
|
330
|
+
const scriptCfg = cfg?.script ?? {};
|
|
331
|
+
const scriptCode = typeof scriptCfg === "string" ? scriptCfg : scriptCfg?.scriptCode ?? scriptCfg?.code;
|
|
332
|
+
const scriptType = String(scriptCfg?.scriptType ?? "javascript").toLowerCase();
|
|
333
|
+
if (scriptType === "groovy") {
|
|
334
|
+
log2.warn(`SCRIPT \u8282\u70B9 [${node.id}] \u4F7F\u7528 Groovy \u8BED\u6CD5\uFF0C\u6D4F\u89C8\u5668\u5F15\u64CE\u4E0D\u652F\u6301\uFF0C\u8BF7\u5207\u6362\u5230 Java \u6267\u884C\u6A21\u5F0F`);
|
|
335
|
+
return {
|
|
336
|
+
success: false,
|
|
337
|
+
message: "\u6D4F\u89C8\u5668\u5F15\u64CE\u4E0D\u652F\u6301 Groovy \u811A\u672C\uFF0C\u8BF7\u5728\u8282\u70B9\u914D\u7F6E\u4E2D\u5C06\u8BED\u8A00\u6539\u4E3A JavaScript\uFF0C\u6216\u5207\u6362\u5230 Java \u6267\u884C\u6A21\u5F0F"
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
if (!scriptCode?.trim()) {
|
|
341
|
+
log2.warn(`SCRIPT \u8282\u70B9 [${node.id}] \u65E0\u811A\u672C\uFF0C\u8DF3\u8FC7`);
|
|
342
|
+
return { success: true };
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
const vars = context.getVariables();
|
|
346
|
+
const writes = {};
|
|
347
|
+
const proxy = new Proxy(vars, {
|
|
348
|
+
get(target, key) {
|
|
349
|
+
return key in writes ? writes[key] : target[key];
|
|
350
|
+
},
|
|
351
|
+
set(_target, key, value) {
|
|
352
|
+
writes[key] = value;
|
|
353
|
+
return true;
|
|
354
|
+
},
|
|
355
|
+
has(_target, key) {
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
// with(){} 需要 has 返回 true
|
|
359
|
+
});
|
|
360
|
+
const fn = new Function("__vars", `with(__vars) { ${scriptCode} }`);
|
|
361
|
+
fn(proxy);
|
|
362
|
+
for (const [k, v] of Object.entries(writes)) {
|
|
363
|
+
context.setVariable(k, v);
|
|
364
|
+
}
|
|
365
|
+
return { success: true, data: Object.keys(writes).length > 0 ? writes : null };
|
|
366
|
+
} catch (e) {
|
|
367
|
+
log2.error(`SCRIPT \u8282\u70B9 [${node.id}] \u6267\u884C\u5931\u8D25: ${e.message}`);
|
|
368
|
+
return { success: false, message: e.message };
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
var DecisionExecutor = class {
|
|
373
|
+
static execute(node, context) {
|
|
374
|
+
const cfg = node.config;
|
|
375
|
+
const type = String(node.type).toUpperCase();
|
|
376
|
+
const vars = context.getVariables();
|
|
377
|
+
const proxy = new Proxy(vars, { has: () => true });
|
|
378
|
+
if (type === "DECISION") {
|
|
379
|
+
const decCfg = cfg?.decision ?? {};
|
|
380
|
+
const scriptCode2 = decCfg.scriptCode ?? decCfg.script ?? "";
|
|
381
|
+
const scriptType2 = String(decCfg.scriptType ?? "javascript").toLowerCase();
|
|
382
|
+
if (scriptType2 === "groovy") {
|
|
383
|
+
return { success: false, message: "\u6D4F\u89C8\u5668\u5F15\u64CE\u4E0D\u652F\u6301 Groovy\uFF0C\u8BF7\u5207\u6362 Java \u6A21\u5F0F" };
|
|
384
|
+
}
|
|
385
|
+
const outgoing2 = context.getOutgoing(node.id);
|
|
386
|
+
if (scriptCode2.trim()) {
|
|
387
|
+
let exprResult = void 0;
|
|
388
|
+
try {
|
|
389
|
+
const code = scriptCode2.includes("return") ? scriptCode2 : `return (${scriptCode2})`;
|
|
390
|
+
exprResult = new Function("__vars", `with(__vars){ ${code} }`)(proxy);
|
|
391
|
+
} catch (e) {
|
|
392
|
+
log2.warn(`DECISION [${node.id}] scriptCode \u6C42\u503C\u5931\u8D25: ${e.message}`);
|
|
393
|
+
}
|
|
394
|
+
const resultStr = String(exprResult ?? "");
|
|
395
|
+
log2.info(`DECISION [${node.id}] scriptCode \u2192 ${resultStr}`);
|
|
396
|
+
let selectedBranch3 = null;
|
|
397
|
+
let defaultBranch = null;
|
|
398
|
+
for (const edge of outgoing2) {
|
|
399
|
+
const branchResult = edge.condition;
|
|
400
|
+
if (!branchResult?.trim()) defaultBranch = edge.to;
|
|
401
|
+
else if (branchResult === resultStr) selectedBranch3 = edge.to;
|
|
402
|
+
}
|
|
403
|
+
if (!selectedBranch3) selectedBranch3 = defaultBranch;
|
|
404
|
+
for (const edge of outgoing2) {
|
|
405
|
+
if (edge.to !== selectedBranch3) context.skipNode(edge.to);
|
|
406
|
+
}
|
|
407
|
+
if (selectedBranch3) {
|
|
408
|
+
return { success: true, data: { selectedBranch: selectedBranch3, decisionResult: exprResult } };
|
|
409
|
+
}
|
|
410
|
+
return { success: false, message: "DECISION: \u6CA1\u6709\u5339\u914D\u7684\u5206\u652F" };
|
|
411
|
+
}
|
|
412
|
+
let selectedBranch2 = null;
|
|
413
|
+
for (const edge of outgoing2) {
|
|
414
|
+
if (selectedBranch2) {
|
|
415
|
+
context.skipNode(edge.to);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
let matched = false;
|
|
419
|
+
if (!edge.condition?.trim()) {
|
|
420
|
+
matched = true;
|
|
421
|
+
} else {
|
|
422
|
+
try {
|
|
423
|
+
matched = !!new Function("__vars", `with(__vars){ return !!(${edge.condition}); }`)(proxy);
|
|
424
|
+
} catch (e) {
|
|
425
|
+
log2.warn(`DECISION [${node.id}] \u8FB9\u6761\u4EF6\u6C42\u503C\u5931\u8D25: ${edge.condition}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (matched) selectedBranch2 = edge.to;
|
|
429
|
+
else context.skipNode(edge.to);
|
|
430
|
+
}
|
|
431
|
+
log2.info(`DECISION [${node.id}] \u2192 ${selectedBranch2}`);
|
|
432
|
+
if (selectedBranch2) return { success: true, data: { selectedBranch: selectedBranch2 } };
|
|
433
|
+
return { success: false, message: "DECISION: \u6CA1\u6709\u5339\u914D\u7684\u5206\u652F" };
|
|
434
|
+
}
|
|
435
|
+
const condCfg = cfg?.condition ?? {};
|
|
436
|
+
const scriptCode = condCfg.scriptCode ?? condCfg.script ?? "";
|
|
437
|
+
const scriptType = String(condCfg.scriptType ?? "javascript").toLowerCase();
|
|
438
|
+
if (scriptType === "groovy") {
|
|
439
|
+
return { success: false, message: "\u6D4F\u89C8\u5668\u5F15\u64CE\u4E0D\u652F\u6301 Groovy\uFF0C\u8BF7\u5207\u6362 Java \u6A21\u5F0F" };
|
|
440
|
+
}
|
|
441
|
+
const outgoing = context.getOutgoing(node.id);
|
|
442
|
+
let selectedBranch = null;
|
|
443
|
+
if (scriptCode.trim()) {
|
|
444
|
+
let condResult = false;
|
|
445
|
+
try {
|
|
446
|
+
const code = scriptCode.includes("return") ? scriptCode : `return !!(${scriptCode})`;
|
|
447
|
+
condResult = !!new Function("__vars", `with(__vars){ ${code} }`)(proxy);
|
|
448
|
+
} catch (e) {
|
|
449
|
+
log2.warn(`CONDITION [${node.id}] scriptCode \u6C42\u503C\u5931\u8D25: ${e.message}`);
|
|
450
|
+
}
|
|
451
|
+
log2.info(`CONDITION [${node.id}] \u2192 ${condResult}`);
|
|
452
|
+
for (const edge of outgoing) {
|
|
453
|
+
const br = edge.condition?.trim();
|
|
454
|
+
const edgeExpected = !br || br === "true";
|
|
455
|
+
const match = condResult ? br === "true" || !br : br === "false" || !br;
|
|
456
|
+
if (!selectedBranch && (br === String(condResult) || !br)) {
|
|
457
|
+
selectedBranch = edge.to;
|
|
458
|
+
} else {
|
|
459
|
+
context.skipNode(edge.to);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
for (const edge of outgoing) {
|
|
464
|
+
if (selectedBranch) {
|
|
465
|
+
context.skipNode(edge.to);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
let matched = false;
|
|
469
|
+
if (!edge.condition?.trim()) {
|
|
470
|
+
matched = true;
|
|
471
|
+
} else {
|
|
472
|
+
try {
|
|
473
|
+
matched = !!new Function("__vars", `with(__vars){ return !!(${edge.condition}); }`)(proxy);
|
|
474
|
+
} catch (e) {
|
|
475
|
+
log2.warn(`CONDITION [${node.id}] \u8FB9\u6761\u4EF6\u6C42\u503C\u5931\u8D25: ${edge.condition}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (matched) selectedBranch = edge.to;
|
|
479
|
+
else context.skipNode(edge.to);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (selectedBranch) {
|
|
483
|
+
log2.info(`CONDITION [${node.id}] \u2192 ${selectedBranch}`);
|
|
484
|
+
return { success: true, data: { selectedBranch } };
|
|
485
|
+
}
|
|
486
|
+
return { success: false, message: "CONDITION: \u6CA1\u6709\u5339\u914D\u7684\u5206\u652F" };
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
var ParallelExecutor = class {
|
|
490
|
+
static execute(node, context) {
|
|
491
|
+
const outgoing = context.getOutgoing(node.id);
|
|
492
|
+
if (outgoing.length === 0) {
|
|
493
|
+
return { success: true, data: { branches: 0 } };
|
|
494
|
+
}
|
|
495
|
+
const convergeNodeId = context.getDefinition().convergeMap?.[node.id];
|
|
496
|
+
if (convergeNodeId) {
|
|
497
|
+
context.registerConvergence(convergeNodeId, outgoing.length);
|
|
498
|
+
log2.info(`PARALLEL [${node.id}] \u6CE8\u518C\u6C47\u805A\u8282\u70B9 ${convergeNodeId}`);
|
|
499
|
+
} else {
|
|
500
|
+
log2.warn(`PARALLEL [${node.id}] \u672A\u627E\u5230\u6C47\u805A\u70B9\uFF08convergeMap \u7F3A\u5931\uFF09`);
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
success: true,
|
|
504
|
+
data: { branches: outgoing.length, convergeTo: convergeNodeId }
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
var LoopExecutor = class _LoopExecutor {
|
|
509
|
+
static async execute(node, context) {
|
|
510
|
+
const cfg = node.config?.loop;
|
|
511
|
+
if (!cfg?.loopType) {
|
|
512
|
+
return { success: false, message: "LOOP \u8282\u70B9\u7F3A\u5C11 loopType (START|END)" };
|
|
513
|
+
}
|
|
514
|
+
const loopType = String(cfg.loopType).toUpperCase();
|
|
515
|
+
if (loopType === "START") return _LoopExecutor.executeStart(node, cfg, context);
|
|
516
|
+
if (loopType === "END") return { success: true, data: { action: "collected" } };
|
|
517
|
+
return { success: false, message: `\u672A\u77E5 loopType: ${cfg.loopType}` };
|
|
518
|
+
}
|
|
519
|
+
static async executeStart(node, cfg, context) {
|
|
520
|
+
const itemsExpr = cfg.itemsExpr ?? cfg.arrayExpression;
|
|
521
|
+
const maxIter = cfg.maxIterations ?? Infinity;
|
|
522
|
+
const itemVar = cfg.itemVar ?? "item";
|
|
523
|
+
const indexVar = cfg.indexVar ?? "index";
|
|
524
|
+
const items = _LoopExecutor.evalItems(itemsExpr, context);
|
|
525
|
+
const bodyPath = context.getLoopBodyPath(node.id);
|
|
526
|
+
const loopEndNodeId = bodyPath.length > 0 ? bodyPath[bodyPath.length - 1] : null;
|
|
527
|
+
log2.info(`LOOP_START [${node.id}] items=${items.length}, bodyPath=${bodyPath.join("->")}`);
|
|
528
|
+
if (items.length === 0) {
|
|
529
|
+
if (loopEndNodeId) {
|
|
530
|
+
context.complete(loopEndNodeId, { results: [] }, Date.now());
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
success: true,
|
|
534
|
+
data: { action: "exit", totalIterations: 0, results: [], __loopEndNodeId: loopEndNodeId }
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
const allResults = [];
|
|
538
|
+
let iteration = 0;
|
|
539
|
+
for (let i = 0; i < items.length && i < maxIter; i++) {
|
|
540
|
+
context.setVariable(itemVar, items[i]);
|
|
541
|
+
context.setVariable(indexVar, i);
|
|
542
|
+
iteration++;
|
|
543
|
+
for (const nodeId of bodyPath) {
|
|
544
|
+
const bodyNode = context.getNode(nodeId);
|
|
545
|
+
if (!bodyNode) continue;
|
|
546
|
+
context.getCompletedNodes().delete(nodeId);
|
|
547
|
+
context.getExecutingNodes().delete(nodeId);
|
|
548
|
+
const t0 = Date.now();
|
|
549
|
+
const result = await NodeExecutorFactory.execute(bodyNode, context);
|
|
550
|
+
context.complete(nodeId, result.data, t0);
|
|
551
|
+
if (String(bodyNode.type).toUpperCase() === "LOOP") {
|
|
552
|
+
const endCfg = bodyNode.config?.loop;
|
|
553
|
+
if (endCfg?.loopType?.toUpperCase() === "END") {
|
|
554
|
+
const resultExpr = endCfg.resultExpr ?? endCfg.resultExpression;
|
|
555
|
+
if (resultExpr) {
|
|
556
|
+
try {
|
|
557
|
+
const val = _LoopExecutor.evalExpr(resultExpr, context);
|
|
558
|
+
if (val !== void 0) allResults.push(val);
|
|
559
|
+
} catch (e) {
|
|
560
|
+
}
|
|
561
|
+
} else if (result.data !== void 0) {
|
|
562
|
+
allResults.push(result.data);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const resultData = {
|
|
569
|
+
action: "exit",
|
|
570
|
+
totalIterations: iteration,
|
|
571
|
+
results: allResults
|
|
572
|
+
};
|
|
573
|
+
if (loopEndNodeId) resultData["__loopEndNodeId"] = loopEndNodeId;
|
|
574
|
+
log2.info(`LOOP_START [${node.id}] \u6267\u884C\u5B8C\u6210\uFF0CtotalIterations=${iteration}`);
|
|
575
|
+
return { success: true, data: resultData };
|
|
576
|
+
}
|
|
577
|
+
static evalItems(expr, context) {
|
|
578
|
+
if (!expr?.trim()) return [];
|
|
579
|
+
try {
|
|
580
|
+
const vars = context.getVariables();
|
|
581
|
+
const proxy = new Proxy(vars, { has: () => true });
|
|
582
|
+
const result = new Function("__vars", `with(__vars){ return (${expr}); }`)(proxy);
|
|
583
|
+
if (Array.isArray(result)) return result;
|
|
584
|
+
if (typeof result === "number") return Array.from({ length: result }, (_, i) => i);
|
|
585
|
+
return [];
|
|
586
|
+
} catch (e) {
|
|
587
|
+
log2.error(`LOOP items \u8868\u8FBE\u5F0F\u6C42\u503C\u5931\u8D25: ${expr}`);
|
|
588
|
+
return [];
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
static evalExpr(expr, context) {
|
|
592
|
+
const vars = { ...context.getVariables(), outputs: context.getNodeOutputs() };
|
|
593
|
+
const proxy = new Proxy(vars, { has: () => true });
|
|
594
|
+
return new Function("__vars", `with(__vars){ return (${expr}); }`)(proxy);
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
var ApiExecutor = class _ApiExecutor {
|
|
598
|
+
static async execute(node, context) {
|
|
599
|
+
const cfg = node.config?.api;
|
|
600
|
+
if (!cfg?.url) {
|
|
601
|
+
return { success: false, message: "API \u8282\u70B9\u7F3A\u5C11 url \u914D\u7F6E" };
|
|
602
|
+
}
|
|
603
|
+
const vars = context.getVariables();
|
|
604
|
+
let url = _ApiExecutor.interpolate(cfg.url, vars);
|
|
605
|
+
const method = (cfg.method ?? "GET").toUpperCase();
|
|
606
|
+
const timeoutMs = (cfg.timeout ?? 30) * 1e3;
|
|
607
|
+
const rawHeaders = {
|
|
608
|
+
"Content-Type": "application/json",
|
|
609
|
+
...cfg.headers ?? {}
|
|
610
|
+
};
|
|
611
|
+
if (cfg.params && Object.keys(cfg.params).length > 0) {
|
|
612
|
+
const qs = new URLSearchParams(
|
|
613
|
+
Object.entries(cfg.params).map(([k, v]) => [k, String(v)])
|
|
614
|
+
).toString();
|
|
615
|
+
url = url + (url.includes("?") ? "&" : "?") + qs;
|
|
616
|
+
}
|
|
617
|
+
let body;
|
|
618
|
+
if (cfg.body && ["POST", "PUT", "PATCH"].includes(method)) {
|
|
619
|
+
const rawBody = typeof cfg.body === "string" ? _ApiExecutor.interpolate(cfg.body, vars) : JSON.stringify(cfg.body);
|
|
620
|
+
body = rawBody;
|
|
621
|
+
}
|
|
622
|
+
log2.info(`API [${node.id}] ${method} ${url}`);
|
|
623
|
+
const controller = new AbortController();
|
|
624
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
625
|
+
try {
|
|
626
|
+
let response;
|
|
627
|
+
const isBrowser = typeof window !== "undefined";
|
|
628
|
+
if (isBrowser) {
|
|
629
|
+
const proxyBase = window.location.origin;
|
|
630
|
+
response = await fetch(`${proxyBase}/api-proxy`, {
|
|
631
|
+
method: "POST",
|
|
632
|
+
headers: { "Content-Type": "application/json" },
|
|
633
|
+
body: JSON.stringify({ url, method, headers: rawHeaders, body: body ?? null }),
|
|
634
|
+
signal: controller.signal
|
|
635
|
+
});
|
|
636
|
+
} else {
|
|
637
|
+
response = await fetch(url, {
|
|
638
|
+
method,
|
|
639
|
+
headers: rawHeaders,
|
|
640
|
+
body,
|
|
641
|
+
signal: controller.signal
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
clearTimeout(timer);
|
|
645
|
+
if (!response.ok) {
|
|
646
|
+
const errText = await response.text().catch(() => "");
|
|
647
|
+
return { success: false, message: `HTTP ${response.status}: ${errText}` };
|
|
648
|
+
}
|
|
649
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
650
|
+
let data;
|
|
651
|
+
if (contentType.includes("application/json")) {
|
|
652
|
+
data = await response.json();
|
|
653
|
+
} else {
|
|
654
|
+
data = { rawResponse: await response.text() };
|
|
655
|
+
}
|
|
656
|
+
return { success: true, data: { apiResponse: data } };
|
|
657
|
+
} catch (e) {
|
|
658
|
+
clearTimeout(timer);
|
|
659
|
+
if (e.name === "AbortError") {
|
|
660
|
+
return { success: false, message: `API \u8BF7\u6C42\u8D85\u65F6\uFF08${cfg.timeout ?? 30}s\uFF09` };
|
|
661
|
+
}
|
|
662
|
+
log2.error(`API [${node.id}] \u8BF7\u6C42\u5931\u8D25: ${e.message}`);
|
|
663
|
+
return { success: false, message: `API call failed: ${e.message}` };
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
/** 简单字符串插值:{{变量名}} → 变量值 */
|
|
667
|
+
static interpolate(template, vars) {
|
|
668
|
+
return template.replace(
|
|
669
|
+
/\{\{(\w+)\}\}/g,
|
|
670
|
+
(_, key) => key in vars ? String(vars[key]) : `{{${key}}}`
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
var LlmExecutor = class _LlmExecutor {
|
|
675
|
+
static async execute(node, context) {
|
|
676
|
+
const cfg = node.config?.llm;
|
|
677
|
+
if (!cfg) {
|
|
678
|
+
return { success: false, message: "LLM \u8282\u70B9\u7F3A\u5C11\u914D\u7F6E" };
|
|
679
|
+
}
|
|
680
|
+
if (!cfg.userPrompt?.trim()) {
|
|
681
|
+
return { success: false, message: "\u7F3A\u5C11 userPrompt\uFF08\u7528\u6237\u63D0\u793A\u8BCD\uFF09" };
|
|
682
|
+
}
|
|
683
|
+
const vars = context.getVariables();
|
|
684
|
+
const apiKey = cfg.apiKey ?? "";
|
|
685
|
+
const provider = cfg.provider ?? "openai";
|
|
686
|
+
const model = cfg.modelName ?? "gpt-3.5-turbo";
|
|
687
|
+
const endpoint = cfg.endpoint ?? _LlmExecutor.defaultEndpoint(provider);
|
|
688
|
+
const temperature = cfg.temperature ?? 0.7;
|
|
689
|
+
const maxTokens = cfg.maxTokens ?? 2048;
|
|
690
|
+
const userPrompt = _LlmExecutor.interpolate(cfg.userPrompt, vars);
|
|
691
|
+
const systemPrompt = _LlmExecutor.interpolate(cfg.systemPrompt ?? "", vars);
|
|
692
|
+
const messages = [];
|
|
693
|
+
if (systemPrompt.trim()) messages.push({ role: "system", content: systemPrompt });
|
|
694
|
+
messages.push({ role: "user", content: userPrompt });
|
|
695
|
+
log2.info(`LLM [${node.id}] provider=${provider} model=${model}`);
|
|
696
|
+
try {
|
|
697
|
+
const response = await fetch(`${endpoint}/chat/completions`, {
|
|
698
|
+
method: "POST",
|
|
699
|
+
headers: {
|
|
700
|
+
"Content-Type": "application/json",
|
|
701
|
+
"Authorization": `Bearer ${apiKey}`
|
|
702
|
+
},
|
|
703
|
+
body: JSON.stringify({ model, messages, temperature, max_tokens: maxTokens })
|
|
704
|
+
});
|
|
705
|
+
if (!response.ok) {
|
|
706
|
+
const err = await response.json().catch(() => ({ error: { message: response.statusText } }));
|
|
707
|
+
return { success: false, message: `LLM \u8C03\u7528\u5931\u8D25: ${err?.error?.message ?? response.status}` };
|
|
708
|
+
}
|
|
709
|
+
const data = await response.json();
|
|
710
|
+
const content = data.choices?.[0]?.message?.content ?? "";
|
|
711
|
+
const usage = data.usage ?? {};
|
|
712
|
+
return {
|
|
713
|
+
success: true,
|
|
714
|
+
data: {
|
|
715
|
+
llmResponse: content,
|
|
716
|
+
model: data.model ?? model,
|
|
717
|
+
inputTokens: usage.prompt_tokens ?? 0,
|
|
718
|
+
outputTokens: usage.completion_tokens ?? 0
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
} catch (e) {
|
|
722
|
+
log2.error(`LLM [${node.id}] \u8BF7\u6C42\u5931\u8D25: ${e.message}`);
|
|
723
|
+
return { success: false, message: `LLM call failed: ${e.message}` };
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
static defaultEndpoint(provider) {
|
|
727
|
+
switch (provider.toLowerCase()) {
|
|
728
|
+
case "openai":
|
|
729
|
+
return "https://api.openai.com/v1";
|
|
730
|
+
case "qwen":
|
|
731
|
+
return "https://dashscope.aliyuncs.com/compatible-mode/v1";
|
|
732
|
+
case "claude":
|
|
733
|
+
return "https://api.anthropic.com/v1";
|
|
734
|
+
case "ollama":
|
|
735
|
+
return "http://localhost:11434/v1";
|
|
736
|
+
default:
|
|
737
|
+
return "https://api.openai.com/v1";
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
static interpolate(template, vars) {
|
|
741
|
+
return template.replace(
|
|
742
|
+
/\{\{(\w+)\}\}/g,
|
|
743
|
+
(_, key) => key in vars ? String(vars[key]) : `{{${key}}}`
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
// src/core/flow-engine.ts
|
|
749
|
+
var log3 = createLogger("Engine");
|
|
750
|
+
var SimpleEmitter = class {
|
|
751
|
+
handlers = /* @__PURE__ */ new Map();
|
|
752
|
+
on(event, handler) {
|
|
753
|
+
if (!this.handlers.has(event)) this.handlers.set(event, []);
|
|
754
|
+
this.handlers.get(event).push(handler);
|
|
755
|
+
return this;
|
|
756
|
+
}
|
|
757
|
+
once(event, handler) {
|
|
758
|
+
const wrapper = (...args) => {
|
|
759
|
+
handler(...args);
|
|
760
|
+
this.off(event, wrapper);
|
|
761
|
+
};
|
|
762
|
+
return this.on(event, wrapper);
|
|
763
|
+
}
|
|
764
|
+
off(event, handler) {
|
|
765
|
+
const list = this.handlers.get(event) ?? [];
|
|
766
|
+
this.handlers.set(event, list.filter((h) => h !== handler));
|
|
767
|
+
return this;
|
|
768
|
+
}
|
|
769
|
+
emit(event, ...args) {
|
|
770
|
+
const list = this.handlers.get(event) ?? [];
|
|
771
|
+
for (const h of list) {
|
|
772
|
+
try {
|
|
773
|
+
h(...args);
|
|
774
|
+
} catch (e) {
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return this;
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
var FlowEngine = class extends SimpleEmitter {
|
|
781
|
+
activeContexts = /* @__PURE__ */ new Map();
|
|
782
|
+
constructor() {
|
|
783
|
+
super();
|
|
784
|
+
}
|
|
785
|
+
// ─── 公开入口 ────────────────────────────────────────────
|
|
786
|
+
async execute(definition, variables = {}) {
|
|
787
|
+
const executionId = `exec-${Math.random().toString(36).slice(2, 10)}`;
|
|
788
|
+
const context = new FlowContext(executionId, definition, variables);
|
|
789
|
+
this.activeContexts.set(executionId, context);
|
|
790
|
+
return new Promise((resolve) => {
|
|
791
|
+
this.once(`flow.finished.${executionId}`, () => {
|
|
792
|
+
this.activeContexts.delete(executionId);
|
|
793
|
+
resolve(this.buildResult(context));
|
|
794
|
+
});
|
|
795
|
+
const startNode = context.getNodes().find(
|
|
796
|
+
(n) => String(n.type).toUpperCase() === "START"
|
|
797
|
+
);
|
|
798
|
+
if (!startNode) {
|
|
799
|
+
context.setStatus("FAILED" /* FAILED */);
|
|
800
|
+
this.emit(`flow.finished.${executionId}`);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
this.scheduleNode(context, startNode.id);
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
// ─── 调度单个节点(setTimeout(0) 让并行可以真正并发,兼容浏览器)────
|
|
807
|
+
scheduleNode(context, nodeId) {
|
|
808
|
+
setTimeout(() => this.runNode(context, nodeId), 0);
|
|
809
|
+
}
|
|
810
|
+
// ─── 执行单个节点 ────────────────────────────────────────
|
|
811
|
+
async runNode(context, nodeId) {
|
|
812
|
+
const executionId = context.getExecutionId();
|
|
813
|
+
if (context.getCompletedNodes().has(nodeId)) return;
|
|
814
|
+
if (context.getSkippedNodes().has(nodeId)) return;
|
|
815
|
+
if (!context.tryExecute(nodeId)) return;
|
|
816
|
+
if (!context.arePrerequisitesMet(nodeId)) {
|
|
817
|
+
context.getExecutingNodes().delete(nodeId);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const node = context.getNode(nodeId);
|
|
821
|
+
if (!node) {
|
|
822
|
+
log3.warn(`\u8282\u70B9 [${nodeId}] \u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7`);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const type = String(node.type).toUpperCase();
|
|
826
|
+
log3.debug(`RUN [${nodeId}] type=${type}`);
|
|
827
|
+
this.emit("node.starting", { executionId, nodeId });
|
|
828
|
+
const t0 = Date.now();
|
|
829
|
+
try {
|
|
830
|
+
const result = await NodeExecutorFactory.execute(node, context);
|
|
831
|
+
if (!result.success) {
|
|
832
|
+
context.fail(nodeId, result.message ?? "Unknown error", t0);
|
|
833
|
+
this.handleFailure(context, nodeId, result.message ?? "");
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
context.complete(nodeId, result.data, t0);
|
|
837
|
+
this.emit("node.completed", { executionId, nodeId, output: result.data });
|
|
838
|
+
if (type === "END") {
|
|
839
|
+
context.setStatus("COMPLETED" /* COMPLETED */);
|
|
840
|
+
this.emit(`flow.finished.${executionId}`);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (type === "LOOP") {
|
|
844
|
+
const loopType = String(node.config?.loop?.loopType ?? "").toUpperCase();
|
|
845
|
+
if (loopType === "START") {
|
|
846
|
+
const loopEndNodeId = result.data?.__loopEndNodeId;
|
|
847
|
+
if (loopEndNodeId) {
|
|
848
|
+
for (const e of context.getOutgoing(loopEndNodeId)) {
|
|
849
|
+
this.triggerDownstream(context, loopEndNodeId, e.to);
|
|
850
|
+
}
|
|
851
|
+
} else {
|
|
852
|
+
for (const e of context.getOutgoing(nodeId)) {
|
|
853
|
+
this.triggerDownstream(context, nodeId, e.to);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (type === "DECISION" || type === "CONDITION") {
|
|
860
|
+
const selected = result.data?.selectedBranch;
|
|
861
|
+
if (selected) {
|
|
862
|
+
this.scheduleNode(context, selected);
|
|
863
|
+
} else {
|
|
864
|
+
this.handleFailure(context, nodeId, "No branch matched");
|
|
865
|
+
}
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if (type === "PARALLEL") {
|
|
869
|
+
for (const e of context.getOutgoing(nodeId)) {
|
|
870
|
+
this.scheduleNode(context, e.to);
|
|
871
|
+
}
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
for (const e of context.getOutgoing(nodeId)) {
|
|
875
|
+
this.triggerDownstream(context, nodeId, e.to);
|
|
876
|
+
}
|
|
877
|
+
} catch (e) {
|
|
878
|
+
context.fail(nodeId, e.message, t0);
|
|
879
|
+
this.handleFailure(context, nodeId, e.message);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* 触发下游节点。有汇聚时,等所有【非跳过】入边到达后才 schedule。
|
|
884
|
+
*/
|
|
885
|
+
triggerDownstream(context, fromNodeId, toNodeId) {
|
|
886
|
+
const incoming = context.getIncoming(toNodeId);
|
|
887
|
+
if (incoming.length <= 1) {
|
|
888
|
+
this.scheduleNode(context, toNodeId);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
const effectiveDone = incoming.filter(
|
|
892
|
+
(e) => context.getCompletedNodes().has(e.from) || context.getSkippedNodes().has(e.from)
|
|
893
|
+
).length;
|
|
894
|
+
if (effectiveDone >= incoming.length) {
|
|
895
|
+
this.scheduleNode(context, toNodeId);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
// ─── 失败处理 ─────────────────────────────────────────
|
|
899
|
+
handleFailure(context, nodeId, error) {
|
|
900
|
+
const executionId = context.getExecutionId();
|
|
901
|
+
const node = context.getNode(nodeId);
|
|
902
|
+
const errorMode = node?.config?.error?.mode;
|
|
903
|
+
log3.error(`\u8282\u70B9 [${nodeId}] \u5931\u8D25: ${error}`);
|
|
904
|
+
this.emit("node.failed", { executionId, nodeId, error });
|
|
905
|
+
if (errorMode && errorMode !== "throw" && errorMode !== "fail") {
|
|
906
|
+
const target = context.getNode(errorMode);
|
|
907
|
+
if (target) {
|
|
908
|
+
this.scheduleNode(context, target.id);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
context.setStatus("FAILED" /* FAILED */);
|
|
913
|
+
this.emit(`flow.finished.${executionId}`);
|
|
914
|
+
}
|
|
915
|
+
// ─── 结果构建 ─────────────────────────────────────────
|
|
916
|
+
buildResult(context) {
|
|
917
|
+
const outputs = context.getNodeOutputs();
|
|
918
|
+
const endOutput = Object.entries(outputs).find(
|
|
919
|
+
([k]) => String(context.getNode(k)?.type ?? k).toUpperCase() === "END" || (k.startsWith("END") || k.startsWith("end"))
|
|
920
|
+
)?.[1];
|
|
921
|
+
return {
|
|
922
|
+
executionId: context.getExecutionId(),
|
|
923
|
+
status: context.getStatus(),
|
|
924
|
+
output: endOutput,
|
|
925
|
+
error: context.getTraces().find((t) => t.status === "FAILED" /* FAILED */)?.error,
|
|
926
|
+
duration: context.getDuration(),
|
|
927
|
+
startTime: context.getStartTime(),
|
|
928
|
+
endTime: context.getEndTime(),
|
|
929
|
+
variables: context.getVariables(),
|
|
930
|
+
traces: context.getTraces()
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
// src/parser/x6-parser.ts
|
|
936
|
+
var log4 = createLogger("X6Parser");
|
|
937
|
+
var X6Parser = class {
|
|
938
|
+
/**
|
|
939
|
+
* 将前端 X6 序列化的 JSON 解析为引擎可执行的 FlowDefinition
|
|
940
|
+
*/
|
|
941
|
+
parse(x6Json) {
|
|
942
|
+
const raw = typeof x6Json === "string" ? JSON.parse(x6Json) : x6Json;
|
|
943
|
+
const nodes = this.parseNodes(raw.nodes ?? []);
|
|
944
|
+
const edges = this.parseEdges(raw.edges ?? []);
|
|
945
|
+
const convergeMap = this.computeConvergeMap(nodes, edges);
|
|
946
|
+
const loopBodyPaths = this.computeLoopBodyPaths(nodes, edges);
|
|
947
|
+
log4.debug(`\u89E3\u6790\u5B8C\u6210: nodes=${nodes.length}, edges=${edges.length}, convergeMap keys=${Object.keys(convergeMap).length}`);
|
|
948
|
+
return {
|
|
949
|
+
id: `flow-${Date.now()}`,
|
|
950
|
+
name: "x6-flow",
|
|
951
|
+
version: "1.0",
|
|
952
|
+
nodes,
|
|
953
|
+
edges,
|
|
954
|
+
convergeMap,
|
|
955
|
+
loopBodyPaths
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
// ── 节点解析 ──────────────────────────────────────────
|
|
959
|
+
parseNodes(x6Nodes) {
|
|
960
|
+
return x6Nodes.map((n) => {
|
|
961
|
+
const rawConfig = n.data?.config ?? {};
|
|
962
|
+
const meta = n.data?.meta ?? {};
|
|
963
|
+
const type = (meta.type ?? n.shape ?? "UNKNOWN").toUpperCase();
|
|
964
|
+
const code = rawConfig.code ?? type;
|
|
965
|
+
const config = this.normalizeConfig(rawConfig, type);
|
|
966
|
+
return {
|
|
967
|
+
id: n.id,
|
|
968
|
+
type,
|
|
969
|
+
name: rawConfig.title ?? meta.label ?? type,
|
|
970
|
+
code,
|
|
971
|
+
config,
|
|
972
|
+
metadata: meta
|
|
973
|
+
};
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
normalizeConfig(raw, type) {
|
|
977
|
+
const cfg = { ...raw };
|
|
978
|
+
if (cfg.loop) {
|
|
979
|
+
const loop = cfg.loop;
|
|
980
|
+
if (!loop.loopType && loop.nodeType) {
|
|
981
|
+
loop.loopType = String(loop.nodeType).toUpperCase();
|
|
982
|
+
delete loop.nodeType;
|
|
983
|
+
} else if (loop.loopType) {
|
|
984
|
+
loop.loopType = String(loop.loopType).toUpperCase();
|
|
985
|
+
}
|
|
986
|
+
if (!loop.itemsExpr && loop.arrayExpression) {
|
|
987
|
+
loop.itemsExpr = loop.arrayExpression;
|
|
988
|
+
delete loop.arrayExpression;
|
|
989
|
+
}
|
|
990
|
+
if (!loop.resultExpr && loop.resultExpression) {
|
|
991
|
+
loop.resultExpr = loop.resultExpression;
|
|
992
|
+
delete loop.resultExpression;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
if (cfg.api) {
|
|
996
|
+
cfg.api = this.normalizeApiField(cfg.api, "params");
|
|
997
|
+
cfg.api = this.normalizeApiField(cfg.api, "headers");
|
|
998
|
+
}
|
|
999
|
+
return cfg;
|
|
1000
|
+
}
|
|
1001
|
+
normalizeApiField(api, field) {
|
|
1002
|
+
const value = api[field];
|
|
1003
|
+
if (typeof value === "string") {
|
|
1004
|
+
const trimmed = value.trim();
|
|
1005
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
1006
|
+
try {
|
|
1007
|
+
api[field] = JSON.parse(trimmed);
|
|
1008
|
+
} catch {
|
|
1009
|
+
log4.warn(`api.${field} \u4E0D\u662F\u5408\u6CD5 JSON\uFF0C\u5DF2\u5FFD\u7565`);
|
|
1010
|
+
delete api[field];
|
|
1011
|
+
}
|
|
1012
|
+
} else if (!trimmed || trimmed === "null") {
|
|
1013
|
+
delete api[field];
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return api;
|
|
1017
|
+
}
|
|
1018
|
+
// ── 边解析 ────────────────────────────────────────────
|
|
1019
|
+
parseEdges(x6Edges) {
|
|
1020
|
+
const result = [];
|
|
1021
|
+
for (const e of x6Edges) {
|
|
1022
|
+
const from = this.extractCellId(e.source);
|
|
1023
|
+
const to = this.extractCellId(e.target);
|
|
1024
|
+
if (!from || !to) {
|
|
1025
|
+
log4.warn(`\u8FB9\u7F3A\u5C11 source \u6216 target\uFF0C\u5DF2\u8DF3\u8FC7`);
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1028
|
+
const condition = e.data?.config?.branchResult ?? void 0;
|
|
1029
|
+
result.push({
|
|
1030
|
+
id: e.id ?? `edge-${from}-${to}`,
|
|
1031
|
+
from,
|
|
1032
|
+
to,
|
|
1033
|
+
condition,
|
|
1034
|
+
metadata: { priority: e.data?.priority ?? 0 }
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
return result;
|
|
1038
|
+
}
|
|
1039
|
+
extractCellId(value) {
|
|
1040
|
+
if (!value) return null;
|
|
1041
|
+
if (typeof value === "string") return value.trim() || null;
|
|
1042
|
+
return value.cell ?? null;
|
|
1043
|
+
}
|
|
1044
|
+
// ── 预计算:PARALLEL 汇聚点(BFS 找公共后继)────────
|
|
1045
|
+
computeConvergeMap(nodes, edges) {
|
|
1046
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
1047
|
+
const outgoingMap = this.buildOutgoingMap(edges);
|
|
1048
|
+
const result = {};
|
|
1049
|
+
for (const node of nodes) {
|
|
1050
|
+
if (String(node.type).toUpperCase() !== "PARALLEL") continue;
|
|
1051
|
+
const outgoing = outgoingMap.get(node.id) ?? [];
|
|
1052
|
+
if (outgoing.length === 0) continue;
|
|
1053
|
+
const convergeId = this.findConvergeNode(outgoing.map((e) => e.to), outgoingMap);
|
|
1054
|
+
if (convergeId) {
|
|
1055
|
+
result[node.id] = convergeId;
|
|
1056
|
+
log4.debug(`PARALLEL ${node.id} \u2192 converge ${convergeId}`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return result;
|
|
1060
|
+
}
|
|
1061
|
+
/** BFS 找所有分支的第一个公共后继节点 */
|
|
1062
|
+
findConvergeNode(starts, outgoingMap) {
|
|
1063
|
+
const reachCount = /* @__PURE__ */ new Map();
|
|
1064
|
+
for (const start of starts) {
|
|
1065
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1066
|
+
const queue = [start];
|
|
1067
|
+
while (queue.length > 0) {
|
|
1068
|
+
const cur = queue.shift();
|
|
1069
|
+
if (visited.has(cur)) continue;
|
|
1070
|
+
visited.add(cur);
|
|
1071
|
+
reachCount.set(cur, (reachCount.get(cur) ?? 0) + 1);
|
|
1072
|
+
if (reachCount.get(cur) === starts.length && !starts.includes(cur)) {
|
|
1073
|
+
return cur;
|
|
1074
|
+
}
|
|
1075
|
+
for (const e of outgoingMap.get(cur) ?? []) queue.push(e.to);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
1080
|
+
// ── 预计算:LOOP_START 循环体路径(BFS)──────────────
|
|
1081
|
+
computeLoopBodyPaths(nodes, edges) {
|
|
1082
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
1083
|
+
const outgoingMap = this.buildOutgoingMap(edges);
|
|
1084
|
+
const result = {};
|
|
1085
|
+
for (const node of nodes) {
|
|
1086
|
+
if (String(node.type).toUpperCase() !== "LOOP") continue;
|
|
1087
|
+
const loopType = String(node.config?.loop?.loopType ?? "").toUpperCase();
|
|
1088
|
+
if (loopType !== "START") continue;
|
|
1089
|
+
const path = this.computeSingleLoopPath(node, nodeMap, outgoingMap);
|
|
1090
|
+
result[node.id] = path;
|
|
1091
|
+
log4.debug(`LOOP_START ${node.id} bodyPath=[${path.join(",")}]`);
|
|
1092
|
+
}
|
|
1093
|
+
return result;
|
|
1094
|
+
}
|
|
1095
|
+
computeSingleLoopPath(loopStart, nodeMap, outgoingMap) {
|
|
1096
|
+
const path = [];
|
|
1097
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1098
|
+
const queue = [];
|
|
1099
|
+
for (const e of outgoingMap.get(loopStart.id) ?? []) {
|
|
1100
|
+
const toNode = nodeMap.get(e.to);
|
|
1101
|
+
if (!toNode || String(toNode.type).toUpperCase() === "END") continue;
|
|
1102
|
+
if (visited.add(e.to)) queue.push(e.to);
|
|
1103
|
+
}
|
|
1104
|
+
while (queue.length > 0) {
|
|
1105
|
+
const cur = queue.shift();
|
|
1106
|
+
path.push(cur);
|
|
1107
|
+
const curNode = nodeMap.get(cur);
|
|
1108
|
+
if (!curNode) continue;
|
|
1109
|
+
if (String(curNode.type).toUpperCase() === "LOOP" && String(curNode.config?.loop?.loopType ?? "").toUpperCase() === "END") break;
|
|
1110
|
+
for (const e of outgoingMap.get(cur) ?? []) {
|
|
1111
|
+
if (e.to === loopStart.id) continue;
|
|
1112
|
+
const toNode = nodeMap.get(e.to);
|
|
1113
|
+
if (!toNode || String(toNode.type).toUpperCase() === "END") continue;
|
|
1114
|
+
if (visited.add(e.to)) queue.push(e.to);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
return path;
|
|
1118
|
+
}
|
|
1119
|
+
buildOutgoingMap(edges) {
|
|
1120
|
+
const map = /* @__PURE__ */ new Map();
|
|
1121
|
+
for (const e of edges) {
|
|
1122
|
+
if (!map.has(e.from)) map.set(e.from, []);
|
|
1123
|
+
map.get(e.from).push(e);
|
|
1124
|
+
}
|
|
1125
|
+
return map;
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
function parseX6(x6Json) {
|
|
1129
|
+
return new X6Parser().parse(x6Json);
|
|
1130
|
+
}
|
|
1131
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1132
|
+
0 && (module.exports = {
|
|
1133
|
+
FlowContext,
|
|
1134
|
+
FlowEngine,
|
|
1135
|
+
NodeExecutorFactory,
|
|
1136
|
+
NodeStatus,
|
|
1137
|
+
NodeType,
|
|
1138
|
+
X6Parser,
|
|
1139
|
+
createLogger,
|
|
1140
|
+
parseX6
|
|
1141
|
+
});
|