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 CHANGED
@@ -1,24 +1,24 @@
1
1
  # 72flow-nodejs
2
2
 
3
+ [![NPM Version](https://img.shields.io/npm/v/72flow-nodejs.svg)](https://www.npmjs.com/package/72flow-nodejs)
3
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
4
5
  [![TypeScript](https://img.shields.io/badge/TypeScript-6.x-blue)](https://www.typescriptlang.org/)
5
6
  [![Node.js](https://img.shields.io/badge/Node.js-18%2B-green)](https://nodejs.org/)
6
7
 
7
- 72flow 工作流编排引擎的 **Node.js / TypeScript** 实现,支持在浏览器和 Node.js 双环境中运行,与 Java 后端引擎保持行为一致。
8
+ **72flow-nodejs** 72flow 工作流编排引擎的轻量级 JavaScript 实现。它旨在提供一个与 Java 后端行为完全一致的、可在浏览器和 Node.js 环境中无缝运行的流程执行核心。
8
9
 
9
10
  ---
10
11
 
11
- ## 特性
12
+ ## 核心特性
12
13
 
13
- - **多节点类型**:START、END、SCRIPT、DECISION、CONDITION、PARALLEL、LOOP、API、LLM
14
- - ✅ **浏览器 & Node.js 双运行时**:无需修改代码,同一套引擎在两端行为一致
15
- - ✅ **并行分支**:`PARALLEL` 节点自动并发触发分支,汇聚后继续执行
16
- - ✅ **条件分支**:`DECISION` / `CONDITION` 节点支持 JavaScript 表达式路由
17
- - ✅ **循环执行**:`LOOP` 节点支持数组/次数迭代,内联同步执行循环体
18
- - ✅ **LLM 集成**:内置 OpenAI / Qwen / Claude / Ollama 兼容接口
19
- - ✅ **事件系统**:引擎发射 `node.starting`、`node.completed`、`node.failed` 等事件
20
- - ✅ **X6 格式解析**:内置 `@antv/x6` 画布 JSON 格式解析器,直接对接前端图形编辑器
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 脚本,可读写流程变量 | `config.script`(字符串) |
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: durationMs * 1e6,
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
- return { success: true, data: Object.keys(writes).length > 0 ? writes : null };
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({ model, messages, temperature, max_tokens: maxTokens })
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 data = await response.json();
710
- const content = data.choices?.[0]?.message?.content ?? "";
711
- const usage = data.usage ?? {};
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: content,
716
- model: data.model ?? model,
717
- inputTokens: usage.prompt_tokens ?? 0,
718
- outputTokens: usage.completion_tokens ?? 0
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 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];
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>): Promise<FlowResult>;
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>): Promise<FlowResult>;
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: durationMs * 1e6,
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
- return { success: true, data: Object.keys(writes).length > 0 ? writes : null };
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({ model, messages, temperature, max_tokens: maxTokens })
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 data = await response.json();
677
- const content = data.choices?.[0]?.message?.content ?? "";
678
- const usage = data.usage ?? {};
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: content,
683
- model: data.model ?? model,
684
- inputTokens: usage.prompt_tokens ?? 0,
685
- outputTokens: usage.completion_tokens ?? 0
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 outputs = context.getNodeOutputs();
885
- const endOutput = Object.entries(outputs).find(
886
- ([k]) => String(context.getNode(k)?.type ?? k).toUpperCase() === "END" || (k.startsWith("END") || k.startsWith("end"))
887
- )?.[1];
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.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
-