72flow-nodejs 1.0.0 → 1.0.3
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/README.md +13 -13
- package/dist/index.cjs +120 -17
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +120 -17
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
# 72flow-nodejs
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/72flow-nodejs)
|
|
3
4
|
[](./LICENSE)
|
|
4
5
|
[](https://www.typescriptlang.org/)
|
|
5
6
|
[](https://nodejs.org/)
|
|
6
7
|
|
|
7
|
-
72flow
|
|
8
|
+
**72flow-nodejs** 是 72flow 工作流编排引擎的轻量级 JavaScript 实现。它旨在提供一个与 Java 后端行为完全一致的、可在浏览器和 Node.js 环境中无缝运行的流程执行核心。
|
|
8
9
|
|
|
9
10
|
---
|
|
10
11
|
|
|
11
|
-
##
|
|
12
|
+
## 核心特性
|
|
12
13
|
|
|
13
|
-
-
|
|
14
|
-
- ✅
|
|
15
|
-
- ✅
|
|
16
|
-
- ✅
|
|
17
|
-
- ✅
|
|
18
|
-
- ✅ **
|
|
19
|
-
- ✅
|
|
20
|
-
- ✅
|
|
21
|
-
- ✅ **ESM + CJS 双格式产物**:兼容各种打包场景
|
|
14
|
+
- 🚀 **极轻量**:无重度依赖,压缩后产物极小。
|
|
15
|
+
- ✅ **全能力节点**:支持 START、END、SCRIPT、DECISION、CONDITION、PARALLEL、LOOP、API、LLM 等 9 种核心节点。
|
|
16
|
+
- ✅ **双运行时**:天然支持现代浏览器与 Node.js 18+ 环境。
|
|
17
|
+
- ✅ **精准输出**:END 节点支持根据 `sourceCode` 路由特定节点的产物作为流程结果。
|
|
18
|
+
- ✅ **脚本捕获**:SCRIPT 节点支持捕获显式 `return` 的值。
|
|
19
|
+
- ✅ **X6 友好**:内置对 `@antv/x6` 导出的图形 JSON 的原生解析支持。
|
|
20
|
+
- ✅ **事件驱动**:完整的生命周期钩子(starting, completed, failed)。
|
|
21
|
+
- ✅ **零配置 LLM**:内置对 OpenAI 协议大模型的支持。
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
@@ -147,8 +147,8 @@ const result = await new FlowEngine().execute(flowDefinition, {});
|
|
|
147
147
|
| 节点类型 | 说明 | 关键配置字段 |
|
|
148
148
|
|-------------|--------------------------------------------------|-------------------------------------------------|
|
|
149
149
|
| `START` | 流程起始节点 | — |
|
|
150
|
-
| `END` |
|
|
151
|
-
| `SCRIPT` | 执行 JavaScript
|
|
150
|
+
| `END` | 流程结束周期。支持精准提取指定节点产物 | `config.outputResult.sourceCode` |
|
|
151
|
+
| `SCRIPT` | 执行 JavaScript 脚本。支持 `scriptResult` 返回值 | `config.script.scriptCode` |
|
|
152
152
|
| `DECISION` | 运行脚本计算返回值,与出边 `condition` 字段匹配 | `config.decision.scriptCode` |
|
|
153
153
|
| `CONDITION` | 对每条出边独立求值布尔表达式 | `config.condition.scriptCode` |
|
|
154
154
|
| `PARALLEL` | 并发触发所有出边分支,需配合 `convergeMap` 使用 | `convergeMap` (流程定义级) |
|
package/dist/index.cjs
CHANGED
|
@@ -75,6 +75,8 @@ var FlowContext = class {
|
|
|
75
75
|
nodeOutputs = {};
|
|
76
76
|
convergeStates = /* @__PURE__ */ new Map();
|
|
77
77
|
traces = [];
|
|
78
|
+
onStreamHandler;
|
|
79
|
+
streamingMode = false;
|
|
78
80
|
constructor(executionId, definition, variables) {
|
|
79
81
|
this.executionId = executionId;
|
|
80
82
|
this.definition = definition;
|
|
@@ -82,6 +84,12 @@ var FlowContext = class {
|
|
|
82
84
|
this.startTime = Date.now();
|
|
83
85
|
this.buildIndex();
|
|
84
86
|
}
|
|
87
|
+
setStreamingMode(enabled) {
|
|
88
|
+
this.streamingMode = enabled;
|
|
89
|
+
}
|
|
90
|
+
isStreamingEnabled() {
|
|
91
|
+
return this.streamingMode;
|
|
92
|
+
}
|
|
85
93
|
/** 构造时预建索引,所有后续查询均为 O(1) */
|
|
86
94
|
buildIndex() {
|
|
87
95
|
for (const node of this.definition.nodes) {
|
|
@@ -202,6 +210,7 @@ var FlowContext = class {
|
|
|
202
210
|
}
|
|
203
211
|
}
|
|
204
212
|
const durationMs = endTime - startTime;
|
|
213
|
+
const durationNs = durationMs * 1e6;
|
|
205
214
|
this.traces.push({
|
|
206
215
|
nodeId,
|
|
207
216
|
code: node?.code ?? nodeId,
|
|
@@ -209,10 +218,19 @@ var FlowContext = class {
|
|
|
209
218
|
startTime,
|
|
210
219
|
endTime,
|
|
211
220
|
duration: durationMs,
|
|
212
|
-
durationNs
|
|
221
|
+
durationNs,
|
|
213
222
|
// 毫秒 → 纳秒,与 Java 对齐
|
|
214
223
|
data: output
|
|
215
224
|
});
|
|
225
|
+
this.emitStream("__node_event__", {
|
|
226
|
+
type: "completed",
|
|
227
|
+
nodeId,
|
|
228
|
+
output,
|
|
229
|
+
startTime,
|
|
230
|
+
endTime,
|
|
231
|
+
duration: durationMs,
|
|
232
|
+
durationNs
|
|
233
|
+
});
|
|
216
234
|
}
|
|
217
235
|
fail(nodeId, error, startTime) {
|
|
218
236
|
const endTime = Date.now();
|
|
@@ -253,6 +271,16 @@ var FlowContext = class {
|
|
|
253
271
|
getLoopBodyPath(loopStartNodeId) {
|
|
254
272
|
return this.definition.loopBodyPaths?.[loopStartNodeId] ?? [];
|
|
255
273
|
}
|
|
274
|
+
/** 设置流式消息处理器(内部由 FlowEngine 调用以向外广播) */
|
|
275
|
+
setStreamHandler(handler) {
|
|
276
|
+
this.onStreamHandler = handler;
|
|
277
|
+
}
|
|
278
|
+
/** 发射流式消息块(由执行器调用) */
|
|
279
|
+
emitStream(nodeId, chunk) {
|
|
280
|
+
if (this.onStreamHandler) {
|
|
281
|
+
this.onStreamHandler(nodeId, chunk);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
256
284
|
};
|
|
257
285
|
|
|
258
286
|
// src/support/logger.ts
|
|
@@ -321,6 +349,18 @@ var StartExecutor = class {
|
|
|
321
349
|
};
|
|
322
350
|
var EndExecutor = class {
|
|
323
351
|
static execute(node, context) {
|
|
352
|
+
const cfg = node.config;
|
|
353
|
+
const sourceNodeCode = cfg?.outputResult?.sourceCode || cfg?.sourceCode || cfg?.end?.sourceCode;
|
|
354
|
+
if (sourceNodeCode) {
|
|
355
|
+
const allOutputs = context.getNodeOutputs();
|
|
356
|
+
const targetOutput = allOutputs[sourceNodeCode];
|
|
357
|
+
if (targetOutput !== void 0) {
|
|
358
|
+
log2.info(`END \u8282\u70B9 [${node.id}] \u5339\u914D\u5230 sourceCode=${sourceNodeCode}\uFF0C\u8FD4\u56DE\u6307\u5B9A\u8282\u70B9\u8F93\u51FA`);
|
|
359
|
+
return { success: true, data: targetOutput };
|
|
360
|
+
} else {
|
|
361
|
+
log2.warn(`END \u8282\u70B9 [${node.id}] sourceCode=${sourceNodeCode} \u672A\u627E\u5230\u76EE\u6807\u8282\u70B9\u8F93\u51FA\uFF0C\u56DE\u9000\u5230\u5168\u91CF\u53D8\u91CF`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
324
364
|
return { success: true, data: context.getVariables() };
|
|
325
365
|
}
|
|
326
366
|
};
|
|
@@ -328,7 +368,7 @@ var ScriptExecutor = class {
|
|
|
328
368
|
static execute(node, context) {
|
|
329
369
|
const cfg = node.config;
|
|
330
370
|
const scriptCfg = cfg?.script ?? {};
|
|
331
|
-
const scriptCode = typeof scriptCfg === "string" ? scriptCfg : scriptCfg?.scriptCode ?? scriptCfg?.code;
|
|
371
|
+
const scriptCode = typeof scriptCfg === "string" ? scriptCfg : scriptCfg?.scriptCode ?? scriptCfg?.code ?? cfg?.scriptCode ?? cfg?.code;
|
|
332
372
|
const scriptType = String(scriptCfg?.scriptType ?? "javascript").toLowerCase();
|
|
333
373
|
if (scriptType === "groovy") {
|
|
334
374
|
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`);
|
|
@@ -358,11 +398,15 @@ var ScriptExecutor = class {
|
|
|
358
398
|
// with(){} 需要 has 返回 true
|
|
359
399
|
});
|
|
360
400
|
const fn = new Function("__vars", `with(__vars) { ${scriptCode} }`);
|
|
361
|
-
fn(proxy);
|
|
401
|
+
const scriptResult = fn(proxy);
|
|
362
402
|
for (const [k, v] of Object.entries(writes)) {
|
|
363
403
|
context.setVariable(k, v);
|
|
364
404
|
}
|
|
365
|
-
|
|
405
|
+
const resultData = { ...writes };
|
|
406
|
+
if (scriptResult !== void 0) {
|
|
407
|
+
resultData.scriptResult = scriptResult;
|
|
408
|
+
}
|
|
409
|
+
return { success: true, data: Object.keys(resultData).length > 0 ? resultData : null };
|
|
366
410
|
} catch (e) {
|
|
367
411
|
log2.error(`SCRIPT \u8282\u70B9 [${node.id}] \u6267\u884C\u5931\u8D25: ${e.message}`);
|
|
368
412
|
return { success: false, message: e.message };
|
|
@@ -700,22 +744,59 @@ var LlmExecutor = class _LlmExecutor {
|
|
|
700
744
|
"Content-Type": "application/json",
|
|
701
745
|
"Authorization": `Bearer ${apiKey}`
|
|
702
746
|
},
|
|
703
|
-
body: JSON.stringify({
|
|
747
|
+
body: JSON.stringify({
|
|
748
|
+
model,
|
|
749
|
+
messages,
|
|
750
|
+
temperature,
|
|
751
|
+
max_tokens: maxTokens,
|
|
752
|
+
stream: true
|
|
753
|
+
// 强制开启流式以支持吐字效果
|
|
754
|
+
})
|
|
704
755
|
});
|
|
705
756
|
if (!response.ok) {
|
|
706
757
|
const err = await response.json().catch(() => ({ error: { message: response.statusText } }));
|
|
707
758
|
return { success: false, message: `LLM \u8C03\u7528\u5931\u8D25: ${err?.error?.message ?? response.status}` };
|
|
708
759
|
}
|
|
709
|
-
const
|
|
710
|
-
const
|
|
711
|
-
|
|
760
|
+
const reader = response.body?.getReader();
|
|
761
|
+
const decoder = new TextDecoder();
|
|
762
|
+
let fullContent = "";
|
|
763
|
+
let modelId = model;
|
|
764
|
+
if (reader) {
|
|
765
|
+
while (true) {
|
|
766
|
+
const { done, value } = await reader.read();
|
|
767
|
+
if (done) break;
|
|
768
|
+
const chunk = decoder.decode(value);
|
|
769
|
+
const lines = chunk.split("\n");
|
|
770
|
+
for (const line of lines) {
|
|
771
|
+
const trimmed = line.trim();
|
|
772
|
+
if (!trimmed || !trimmed.startsWith("data: ")) continue;
|
|
773
|
+
const dataStr = trimmed.slice(6);
|
|
774
|
+
if (dataStr === "[DONE]") break;
|
|
775
|
+
try {
|
|
776
|
+
const json = JSON.parse(dataStr);
|
|
777
|
+
const delta = json.choices?.[0]?.delta?.content ?? "";
|
|
778
|
+
if (delta) {
|
|
779
|
+
fullContent += delta;
|
|
780
|
+
context.emitStream(node.id, { delta, fullContent });
|
|
781
|
+
}
|
|
782
|
+
if (json.model) modelId = json.model;
|
|
783
|
+
} catch (e) {
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
} else {
|
|
788
|
+
const data = await response.json();
|
|
789
|
+
fullContent = data.choices?.[0]?.message?.content ?? "";
|
|
790
|
+
modelId = data.model ?? model;
|
|
791
|
+
}
|
|
712
792
|
return {
|
|
713
793
|
success: true,
|
|
714
794
|
data: {
|
|
715
|
-
llmResponse:
|
|
716
|
-
model:
|
|
717
|
-
inputTokens:
|
|
718
|
-
|
|
795
|
+
llmResponse: fullContent,
|
|
796
|
+
model: modelId,
|
|
797
|
+
inputTokens: 0,
|
|
798
|
+
// 流式协议通常不包含实时 usage,需后期计算或忽略
|
|
799
|
+
outputTokens: 0
|
|
719
800
|
}
|
|
720
801
|
};
|
|
721
802
|
} catch (e) {
|
|
@@ -783,10 +864,25 @@ var FlowEngine = class extends SimpleEmitter {
|
|
|
783
864
|
super();
|
|
784
865
|
}
|
|
785
866
|
// ─── 公开入口 ────────────────────────────────────────────
|
|
786
|
-
async execute(definition, variables = {}) {
|
|
867
|
+
async execute(definition, variables = {}, options = {}) {
|
|
787
868
|
const executionId = `exec-${Math.random().toString(36).slice(2, 10)}`;
|
|
788
869
|
const context = new FlowContext(executionId, definition, variables);
|
|
870
|
+
if (options.stream !== void 0) {
|
|
871
|
+
context.setStreamingMode(options.stream);
|
|
872
|
+
}
|
|
789
873
|
this.activeContexts.set(executionId, context);
|
|
874
|
+
context.setStreamHandler((nodeId, chunk) => {
|
|
875
|
+
if (nodeId === "__node_event__") {
|
|
876
|
+
const { type, ...payload } = chunk;
|
|
877
|
+
if (type === "completed") {
|
|
878
|
+
this.emit("node.completed", payload);
|
|
879
|
+
} else if (type === "failed") {
|
|
880
|
+
this.emit("node.failed", payload);
|
|
881
|
+
}
|
|
882
|
+
} else {
|
|
883
|
+
this.emit("node.stream", { executionId, nodeId, chunk });
|
|
884
|
+
}
|
|
885
|
+
});
|
|
790
886
|
return new Promise((resolve) => {
|
|
791
887
|
this.once(`flow.finished.${executionId}`, () => {
|
|
792
888
|
this.activeContexts.delete(executionId);
|
|
@@ -914,10 +1010,12 @@ var FlowEngine = class extends SimpleEmitter {
|
|
|
914
1010
|
}
|
|
915
1011
|
// ─── 结果构建 ─────────────────────────────────────────
|
|
916
1012
|
buildResult(context) {
|
|
917
|
-
const
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
1013
|
+
const traces = context.getTraces();
|
|
1014
|
+
const endTrace = [...traces].reverse().find((t) => {
|
|
1015
|
+
const node = context.getNode(t.nodeId);
|
|
1016
|
+
return (node?.type?.toUpperCase() === "END" || t.code?.startsWith("END")) && t.status === "COMPLETED" /* COMPLETED */;
|
|
1017
|
+
});
|
|
1018
|
+
const endOutput = endTrace ? endTrace.data : void 0;
|
|
921
1019
|
return {
|
|
922
1020
|
executionId: context.getExecutionId(),
|
|
923
1021
|
status: context.getStatus(),
|
|
@@ -996,6 +1094,11 @@ var X6Parser = class {
|
|
|
996
1094
|
cfg.api = this.normalizeApiField(cfg.api, "params");
|
|
997
1095
|
cfg.api = this.normalizeApiField(cfg.api, "headers");
|
|
998
1096
|
}
|
|
1097
|
+
if (type === "SCRIPT") {
|
|
1098
|
+
if (!cfg.script && cfg.scriptCode) {
|
|
1099
|
+
cfg.script = { scriptCode: cfg.scriptCode, scriptType: cfg.scriptType ?? "javascript" };
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
999
1102
|
return cfg;
|
|
1000
1103
|
}
|
|
1001
1104
|
normalizeApiField(api, field) {
|
package/dist/index.d.cts
CHANGED
|
@@ -137,7 +137,11 @@ declare class FlowContext {
|
|
|
137
137
|
private nodeOutputs;
|
|
138
138
|
private convergeStates;
|
|
139
139
|
private traces;
|
|
140
|
+
private onStreamHandler?;
|
|
141
|
+
private streamingMode;
|
|
140
142
|
constructor(executionId: string, definition: FlowDefinition, variables: Record<string, any>);
|
|
143
|
+
setStreamingMode(enabled: boolean): void;
|
|
144
|
+
isStreamingEnabled(): boolean;
|
|
141
145
|
/** 构造时预建索引,所有后续查询均为 O(1) */
|
|
142
146
|
private buildIndex;
|
|
143
147
|
getExecutionId(): string;
|
|
@@ -175,6 +179,10 @@ declare class FlowContext {
|
|
|
175
179
|
tryConverge(nodeId: string, fromNodeId: string): boolean;
|
|
176
180
|
/** 获取 LOOP_START 节点的循环体路径(来自解析器预计算的 loopBodyPaths) */
|
|
177
181
|
getLoopBodyPath(loopStartNodeId: string): string[];
|
|
182
|
+
/** 设置流式消息处理器(内部由 FlowEngine 调用以向外广播) */
|
|
183
|
+
setStreamHandler(handler: (nodeId: string, chunk: any) => void): void;
|
|
184
|
+
/** 发射流式消息块(由执行器调用) */
|
|
185
|
+
emitStream(nodeId: string, chunk: any): void;
|
|
178
186
|
}
|
|
179
187
|
|
|
180
188
|
type EventHandler = (...args: any[]) => void;
|
|
@@ -188,7 +196,9 @@ declare class SimpleEmitter {
|
|
|
188
196
|
declare class FlowEngine extends SimpleEmitter {
|
|
189
197
|
private activeContexts;
|
|
190
198
|
constructor();
|
|
191
|
-
execute(definition: FlowDefinition, variables?: Record<string, any
|
|
199
|
+
execute(definition: FlowDefinition, variables?: Record<string, any>, options?: {
|
|
200
|
+
stream?: boolean;
|
|
201
|
+
}): Promise<FlowResult>;
|
|
192
202
|
private scheduleNode;
|
|
193
203
|
private runNode;
|
|
194
204
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -137,7 +137,11 @@ declare class FlowContext {
|
|
|
137
137
|
private nodeOutputs;
|
|
138
138
|
private convergeStates;
|
|
139
139
|
private traces;
|
|
140
|
+
private onStreamHandler?;
|
|
141
|
+
private streamingMode;
|
|
140
142
|
constructor(executionId: string, definition: FlowDefinition, variables: Record<string, any>);
|
|
143
|
+
setStreamingMode(enabled: boolean): void;
|
|
144
|
+
isStreamingEnabled(): boolean;
|
|
141
145
|
/** 构造时预建索引,所有后续查询均为 O(1) */
|
|
142
146
|
private buildIndex;
|
|
143
147
|
getExecutionId(): string;
|
|
@@ -175,6 +179,10 @@ declare class FlowContext {
|
|
|
175
179
|
tryConverge(nodeId: string, fromNodeId: string): boolean;
|
|
176
180
|
/** 获取 LOOP_START 节点的循环体路径(来自解析器预计算的 loopBodyPaths) */
|
|
177
181
|
getLoopBodyPath(loopStartNodeId: string): string[];
|
|
182
|
+
/** 设置流式消息处理器(内部由 FlowEngine 调用以向外广播) */
|
|
183
|
+
setStreamHandler(handler: (nodeId: string, chunk: any) => void): void;
|
|
184
|
+
/** 发射流式消息块(由执行器调用) */
|
|
185
|
+
emitStream(nodeId: string, chunk: any): void;
|
|
178
186
|
}
|
|
179
187
|
|
|
180
188
|
type EventHandler = (...args: any[]) => void;
|
|
@@ -188,7 +196,9 @@ declare class SimpleEmitter {
|
|
|
188
196
|
declare class FlowEngine extends SimpleEmitter {
|
|
189
197
|
private activeContexts;
|
|
190
198
|
constructor();
|
|
191
|
-
execute(definition: FlowDefinition, variables?: Record<string, any
|
|
199
|
+
execute(definition: FlowDefinition, variables?: Record<string, any>, options?: {
|
|
200
|
+
stream?: boolean;
|
|
201
|
+
}): Promise<FlowResult>;
|
|
192
202
|
private scheduleNode;
|
|
193
203
|
private runNode;
|
|
194
204
|
/**
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,8 @@ var FlowContext = class {
|
|
|
42
42
|
nodeOutputs = {};
|
|
43
43
|
convergeStates = /* @__PURE__ */ new Map();
|
|
44
44
|
traces = [];
|
|
45
|
+
onStreamHandler;
|
|
46
|
+
streamingMode = false;
|
|
45
47
|
constructor(executionId, definition, variables) {
|
|
46
48
|
this.executionId = executionId;
|
|
47
49
|
this.definition = definition;
|
|
@@ -49,6 +51,12 @@ var FlowContext = class {
|
|
|
49
51
|
this.startTime = Date.now();
|
|
50
52
|
this.buildIndex();
|
|
51
53
|
}
|
|
54
|
+
setStreamingMode(enabled) {
|
|
55
|
+
this.streamingMode = enabled;
|
|
56
|
+
}
|
|
57
|
+
isStreamingEnabled() {
|
|
58
|
+
return this.streamingMode;
|
|
59
|
+
}
|
|
52
60
|
/** 构造时预建索引,所有后续查询均为 O(1) */
|
|
53
61
|
buildIndex() {
|
|
54
62
|
for (const node of this.definition.nodes) {
|
|
@@ -169,6 +177,7 @@ var FlowContext = class {
|
|
|
169
177
|
}
|
|
170
178
|
}
|
|
171
179
|
const durationMs = endTime - startTime;
|
|
180
|
+
const durationNs = durationMs * 1e6;
|
|
172
181
|
this.traces.push({
|
|
173
182
|
nodeId,
|
|
174
183
|
code: node?.code ?? nodeId,
|
|
@@ -176,10 +185,19 @@ var FlowContext = class {
|
|
|
176
185
|
startTime,
|
|
177
186
|
endTime,
|
|
178
187
|
duration: durationMs,
|
|
179
|
-
durationNs
|
|
188
|
+
durationNs,
|
|
180
189
|
// 毫秒 → 纳秒,与 Java 对齐
|
|
181
190
|
data: output
|
|
182
191
|
});
|
|
192
|
+
this.emitStream("__node_event__", {
|
|
193
|
+
type: "completed",
|
|
194
|
+
nodeId,
|
|
195
|
+
output,
|
|
196
|
+
startTime,
|
|
197
|
+
endTime,
|
|
198
|
+
duration: durationMs,
|
|
199
|
+
durationNs
|
|
200
|
+
});
|
|
183
201
|
}
|
|
184
202
|
fail(nodeId, error, startTime) {
|
|
185
203
|
const endTime = Date.now();
|
|
@@ -220,6 +238,16 @@ var FlowContext = class {
|
|
|
220
238
|
getLoopBodyPath(loopStartNodeId) {
|
|
221
239
|
return this.definition.loopBodyPaths?.[loopStartNodeId] ?? [];
|
|
222
240
|
}
|
|
241
|
+
/** 设置流式消息处理器(内部由 FlowEngine 调用以向外广播) */
|
|
242
|
+
setStreamHandler(handler) {
|
|
243
|
+
this.onStreamHandler = handler;
|
|
244
|
+
}
|
|
245
|
+
/** 发射流式消息块(由执行器调用) */
|
|
246
|
+
emitStream(nodeId, chunk) {
|
|
247
|
+
if (this.onStreamHandler) {
|
|
248
|
+
this.onStreamHandler(nodeId, chunk);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
223
251
|
};
|
|
224
252
|
|
|
225
253
|
// src/support/logger.ts
|
|
@@ -288,6 +316,18 @@ var StartExecutor = class {
|
|
|
288
316
|
};
|
|
289
317
|
var EndExecutor = class {
|
|
290
318
|
static execute(node, context) {
|
|
319
|
+
const cfg = node.config;
|
|
320
|
+
const sourceNodeCode = cfg?.outputResult?.sourceCode || cfg?.sourceCode || cfg?.end?.sourceCode;
|
|
321
|
+
if (sourceNodeCode) {
|
|
322
|
+
const allOutputs = context.getNodeOutputs();
|
|
323
|
+
const targetOutput = allOutputs[sourceNodeCode];
|
|
324
|
+
if (targetOutput !== void 0) {
|
|
325
|
+
log2.info(`END \u8282\u70B9 [${node.id}] \u5339\u914D\u5230 sourceCode=${sourceNodeCode}\uFF0C\u8FD4\u56DE\u6307\u5B9A\u8282\u70B9\u8F93\u51FA`);
|
|
326
|
+
return { success: true, data: targetOutput };
|
|
327
|
+
} else {
|
|
328
|
+
log2.warn(`END \u8282\u70B9 [${node.id}] sourceCode=${sourceNodeCode} \u672A\u627E\u5230\u76EE\u6807\u8282\u70B9\u8F93\u51FA\uFF0C\u56DE\u9000\u5230\u5168\u91CF\u53D8\u91CF`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
291
331
|
return { success: true, data: context.getVariables() };
|
|
292
332
|
}
|
|
293
333
|
};
|
|
@@ -295,7 +335,7 @@ var ScriptExecutor = class {
|
|
|
295
335
|
static execute(node, context) {
|
|
296
336
|
const cfg = node.config;
|
|
297
337
|
const scriptCfg = cfg?.script ?? {};
|
|
298
|
-
const scriptCode = typeof scriptCfg === "string" ? scriptCfg : scriptCfg?.scriptCode ?? scriptCfg?.code;
|
|
338
|
+
const scriptCode = typeof scriptCfg === "string" ? scriptCfg : scriptCfg?.scriptCode ?? scriptCfg?.code ?? cfg?.scriptCode ?? cfg?.code;
|
|
299
339
|
const scriptType = String(scriptCfg?.scriptType ?? "javascript").toLowerCase();
|
|
300
340
|
if (scriptType === "groovy") {
|
|
301
341
|
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`);
|
|
@@ -325,11 +365,15 @@ var ScriptExecutor = class {
|
|
|
325
365
|
// with(){} 需要 has 返回 true
|
|
326
366
|
});
|
|
327
367
|
const fn = new Function("__vars", `with(__vars) { ${scriptCode} }`);
|
|
328
|
-
fn(proxy);
|
|
368
|
+
const scriptResult = fn(proxy);
|
|
329
369
|
for (const [k, v] of Object.entries(writes)) {
|
|
330
370
|
context.setVariable(k, v);
|
|
331
371
|
}
|
|
332
|
-
|
|
372
|
+
const resultData = { ...writes };
|
|
373
|
+
if (scriptResult !== void 0) {
|
|
374
|
+
resultData.scriptResult = scriptResult;
|
|
375
|
+
}
|
|
376
|
+
return { success: true, data: Object.keys(resultData).length > 0 ? resultData : null };
|
|
333
377
|
} catch (e) {
|
|
334
378
|
log2.error(`SCRIPT \u8282\u70B9 [${node.id}] \u6267\u884C\u5931\u8D25: ${e.message}`);
|
|
335
379
|
return { success: false, message: e.message };
|
|
@@ -667,22 +711,59 @@ var LlmExecutor = class _LlmExecutor {
|
|
|
667
711
|
"Content-Type": "application/json",
|
|
668
712
|
"Authorization": `Bearer ${apiKey}`
|
|
669
713
|
},
|
|
670
|
-
body: JSON.stringify({
|
|
714
|
+
body: JSON.stringify({
|
|
715
|
+
model,
|
|
716
|
+
messages,
|
|
717
|
+
temperature,
|
|
718
|
+
max_tokens: maxTokens,
|
|
719
|
+
stream: true
|
|
720
|
+
// 强制开启流式以支持吐字效果
|
|
721
|
+
})
|
|
671
722
|
});
|
|
672
723
|
if (!response.ok) {
|
|
673
724
|
const err = await response.json().catch(() => ({ error: { message: response.statusText } }));
|
|
674
725
|
return { success: false, message: `LLM \u8C03\u7528\u5931\u8D25: ${err?.error?.message ?? response.status}` };
|
|
675
726
|
}
|
|
676
|
-
const
|
|
677
|
-
const
|
|
678
|
-
|
|
727
|
+
const reader = response.body?.getReader();
|
|
728
|
+
const decoder = new TextDecoder();
|
|
729
|
+
let fullContent = "";
|
|
730
|
+
let modelId = model;
|
|
731
|
+
if (reader) {
|
|
732
|
+
while (true) {
|
|
733
|
+
const { done, value } = await reader.read();
|
|
734
|
+
if (done) break;
|
|
735
|
+
const chunk = decoder.decode(value);
|
|
736
|
+
const lines = chunk.split("\n");
|
|
737
|
+
for (const line of lines) {
|
|
738
|
+
const trimmed = line.trim();
|
|
739
|
+
if (!trimmed || !trimmed.startsWith("data: ")) continue;
|
|
740
|
+
const dataStr = trimmed.slice(6);
|
|
741
|
+
if (dataStr === "[DONE]") break;
|
|
742
|
+
try {
|
|
743
|
+
const json = JSON.parse(dataStr);
|
|
744
|
+
const delta = json.choices?.[0]?.delta?.content ?? "";
|
|
745
|
+
if (delta) {
|
|
746
|
+
fullContent += delta;
|
|
747
|
+
context.emitStream(node.id, { delta, fullContent });
|
|
748
|
+
}
|
|
749
|
+
if (json.model) modelId = json.model;
|
|
750
|
+
} catch (e) {
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
} else {
|
|
755
|
+
const data = await response.json();
|
|
756
|
+
fullContent = data.choices?.[0]?.message?.content ?? "";
|
|
757
|
+
modelId = data.model ?? model;
|
|
758
|
+
}
|
|
679
759
|
return {
|
|
680
760
|
success: true,
|
|
681
761
|
data: {
|
|
682
|
-
llmResponse:
|
|
683
|
-
model:
|
|
684
|
-
inputTokens:
|
|
685
|
-
|
|
762
|
+
llmResponse: fullContent,
|
|
763
|
+
model: modelId,
|
|
764
|
+
inputTokens: 0,
|
|
765
|
+
// 流式协议通常不包含实时 usage,需后期计算或忽略
|
|
766
|
+
outputTokens: 0
|
|
686
767
|
}
|
|
687
768
|
};
|
|
688
769
|
} catch (e) {
|
|
@@ -750,10 +831,25 @@ var FlowEngine = class extends SimpleEmitter {
|
|
|
750
831
|
super();
|
|
751
832
|
}
|
|
752
833
|
// ─── 公开入口 ────────────────────────────────────────────
|
|
753
|
-
async execute(definition, variables = {}) {
|
|
834
|
+
async execute(definition, variables = {}, options = {}) {
|
|
754
835
|
const executionId = `exec-${Math.random().toString(36).slice(2, 10)}`;
|
|
755
836
|
const context = new FlowContext(executionId, definition, variables);
|
|
837
|
+
if (options.stream !== void 0) {
|
|
838
|
+
context.setStreamingMode(options.stream);
|
|
839
|
+
}
|
|
756
840
|
this.activeContexts.set(executionId, context);
|
|
841
|
+
context.setStreamHandler((nodeId, chunk) => {
|
|
842
|
+
if (nodeId === "__node_event__") {
|
|
843
|
+
const { type, ...payload } = chunk;
|
|
844
|
+
if (type === "completed") {
|
|
845
|
+
this.emit("node.completed", payload);
|
|
846
|
+
} else if (type === "failed") {
|
|
847
|
+
this.emit("node.failed", payload);
|
|
848
|
+
}
|
|
849
|
+
} else {
|
|
850
|
+
this.emit("node.stream", { executionId, nodeId, chunk });
|
|
851
|
+
}
|
|
852
|
+
});
|
|
757
853
|
return new Promise((resolve) => {
|
|
758
854
|
this.once(`flow.finished.${executionId}`, () => {
|
|
759
855
|
this.activeContexts.delete(executionId);
|
|
@@ -881,10 +977,12 @@ var FlowEngine = class extends SimpleEmitter {
|
|
|
881
977
|
}
|
|
882
978
|
// ─── 结果构建 ─────────────────────────────────────────
|
|
883
979
|
buildResult(context) {
|
|
884
|
-
const
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
980
|
+
const traces = context.getTraces();
|
|
981
|
+
const endTrace = [...traces].reverse().find((t) => {
|
|
982
|
+
const node = context.getNode(t.nodeId);
|
|
983
|
+
return (node?.type?.toUpperCase() === "END" || t.code?.startsWith("END")) && t.status === "COMPLETED" /* COMPLETED */;
|
|
984
|
+
});
|
|
985
|
+
const endOutput = endTrace ? endTrace.data : void 0;
|
|
888
986
|
return {
|
|
889
987
|
executionId: context.getExecutionId(),
|
|
890
988
|
status: context.getStatus(),
|
|
@@ -963,6 +1061,11 @@ var X6Parser = class {
|
|
|
963
1061
|
cfg.api = this.normalizeApiField(cfg.api, "params");
|
|
964
1062
|
cfg.api = this.normalizeApiField(cfg.api, "headers");
|
|
965
1063
|
}
|
|
1064
|
+
if (type === "SCRIPT") {
|
|
1065
|
+
if (!cfg.script && cfg.scriptCode) {
|
|
1066
|
+
cfg.script = { scriptCode: cfg.scriptCode, scriptType: cfg.scriptType ?? "javascript" };
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
966
1069
|
return cfg;
|
|
967
1070
|
}
|
|
968
1071
|
normalizeApiField(api, field) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "72flow-nodejs",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Node.js / browser implementation of the 72flow orchestration engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -52,4 +52,3 @@
|
|
|
52
52
|
"lodash-es": "^4.18.1"
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
|