72flow-nodejs 1.0.2 → 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
@@ -716,22 +744,59 @@ var LlmExecutor = class _LlmExecutor {
716
744
  "Content-Type": "application/json",
717
745
  "Authorization": `Bearer ${apiKey}`
718
746
  },
719
- 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
+ })
720
755
  });
721
756
  if (!response.ok) {
722
757
  const err = await response.json().catch(() => ({ error: { message: response.statusText } }));
723
758
  return { success: false, message: `LLM \u8C03\u7528\u5931\u8D25: ${err?.error?.message ?? response.status}` };
724
759
  }
725
- const data = await response.json();
726
- const content = data.choices?.[0]?.message?.content ?? "";
727
- 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
+ }
728
792
  return {
729
793
  success: true,
730
794
  data: {
731
- llmResponse: content,
732
- model: data.model ?? model,
733
- inputTokens: usage.prompt_tokens ?? 0,
734
- outputTokens: usage.completion_tokens ?? 0
795
+ llmResponse: fullContent,
796
+ model: modelId,
797
+ inputTokens: 0,
798
+ // 流式协议通常不包含实时 usage,需后期计算或忽略
799
+ outputTokens: 0
735
800
  }
736
801
  };
737
802
  } catch (e) {
@@ -799,10 +864,25 @@ var FlowEngine = class extends SimpleEmitter {
799
864
  super();
800
865
  }
801
866
  // ─── 公开入口 ────────────────────────────────────────────
802
- async execute(definition, variables = {}) {
867
+ async execute(definition, variables = {}, options = {}) {
803
868
  const executionId = `exec-${Math.random().toString(36).slice(2, 10)}`;
804
869
  const context = new FlowContext(executionId, definition, variables);
870
+ if (options.stream !== void 0) {
871
+ context.setStreamingMode(options.stream);
872
+ }
805
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
+ });
806
886
  return new Promise((resolve) => {
807
887
  this.once(`flow.finished.${executionId}`, () => {
808
888
  this.activeContexts.delete(executionId);
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
@@ -683,22 +711,59 @@ var LlmExecutor = class _LlmExecutor {
683
711
  "Content-Type": "application/json",
684
712
  "Authorization": `Bearer ${apiKey}`
685
713
  },
686
- 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
+ })
687
722
  });
688
723
  if (!response.ok) {
689
724
  const err = await response.json().catch(() => ({ error: { message: response.statusText } }));
690
725
  return { success: false, message: `LLM \u8C03\u7528\u5931\u8D25: ${err?.error?.message ?? response.status}` };
691
726
  }
692
- const data = await response.json();
693
- const content = data.choices?.[0]?.message?.content ?? "";
694
- 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
+ }
695
759
  return {
696
760
  success: true,
697
761
  data: {
698
- llmResponse: content,
699
- model: data.model ?? model,
700
- inputTokens: usage.prompt_tokens ?? 0,
701
- outputTokens: usage.completion_tokens ?? 0
762
+ llmResponse: fullContent,
763
+ model: modelId,
764
+ inputTokens: 0,
765
+ // 流式协议通常不包含实时 usage,需后期计算或忽略
766
+ outputTokens: 0
702
767
  }
703
768
  };
704
769
  } catch (e) {
@@ -766,10 +831,25 @@ var FlowEngine = class extends SimpleEmitter {
766
831
  super();
767
832
  }
768
833
  // ─── 公开入口 ────────────────────────────────────────────
769
- async execute(definition, variables = {}) {
834
+ async execute(definition, variables = {}, options = {}) {
770
835
  const executionId = `exec-${Math.random().toString(36).slice(2, 10)}`;
771
836
  const context = new FlowContext(executionId, definition, variables);
837
+ if (options.stream !== void 0) {
838
+ context.setStreamingMode(options.stream);
839
+ }
772
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
+ });
773
853
  return new Promise((resolve) => {
774
854
  this.once(`flow.finished.${executionId}`, () => {
775
855
  this.activeContexts.delete(executionId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "72flow-nodejs",
3
- "version": "1.0.2",
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",