Explorar el Código

feat(runtime): break & continue node (#560)

Louis Young hace 5 meses
padre
commit
348249f1f7

+ 11 - 0
packages/runtime/interface/src/node/break/index.ts

@@ -0,0 +1,11 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { WorkflowNodeSchema } from '@schema/node';
+import { FlowGramNode } from '@node/constant';
+
+interface BreakNodeData {}
+
+export type BreakNodeSchema = WorkflowNodeSchema<FlowGramNode.Break, BreakNodeData>;

+ 2 - 0
packages/runtime/interface/src/node/constant.ts

@@ -16,4 +16,6 @@ export enum FlowGramNode {
   BlockStart = 'block-start',
   BlockEnd = 'block-end',
   HTTP = 'http',
+  Break = 'break',
+  Continue = 'continue',
 }

+ 11 - 0
packages/runtime/interface/src/node/continue/index.ts

@@ -0,0 +1,11 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { WorkflowNodeSchema } from '@schema/node';
+import { FlowGramNode } from '@node/constant';
+
+interface ContinueNodeData {}
+
+export type ContinueNodeSchema = WorkflowNodeSchema<FlowGramNode.Continue, ContinueNodeData>;

+ 2 - 0
packages/runtime/interface/src/node/index.ts

@@ -11,3 +11,5 @@ export { LoopNodeSchema } from './loop';
 export { ConditionNodeSchema, ConditionOperation, ConditionItem } from './condition';
 export { HTTPNodeSchema, HTTPMethod, HTTPBodyType } from './http';
 export { CodeNodeSchema } from './code';
+export { BreakNodeSchema } from './break';
+export { ContinueNodeSchema } from './continue';

+ 13 - 0
packages/runtime/interface/src/runtime/cache/index.ts

@@ -0,0 +1,13 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+export interface ICache<K = string, V = any> {
+  init(): void;
+  dispose(): void;
+  get(key: K): V;
+  set(key: K, value: V): this;
+  delete(key: K): boolean;
+  has(key: K): boolean;
+}

+ 2 - 0
packages/runtime/interface/src/runtime/context/index.ts

@@ -11,9 +11,11 @@ import { IReporter } from '@runtime/reporter';
 import { IMessageCenter } from '@runtime/message';
 import { IIOCenter } from '@runtime/io-center';
 import { IDocument } from '@runtime/document';
+import { ICache } from '@runtime/cache';
 import { InvokeParams } from '@runtime/base';
 
 export interface ContextData {
+  cache: ICache;
   variableStore: IVariableStore;
   state: IState;
   document: IDocument;

+ 1 - 0
packages/runtime/interface/src/runtime/index.ts

@@ -18,3 +18,4 @@ export * from './task';
 export * from './validation';
 export * from './variable';
 export * from './message';
+export * from './cache';

+ 2 - 0
packages/runtime/js-core/src/domain/__tests__/schemas/index.ts

@@ -5,6 +5,7 @@
 
 import { validateInputsSchema } from './validate-inputs';
 import { twoLLMSchema } from './two-llm';
+import { loopBreakContinueSchema } from './loop-break-continue';
 import { loopSchema } from './loop';
 import { llmRealSchema } from './llm-real';
 import { httpSchema } from './http';
@@ -19,6 +20,7 @@ export const TestSchemas = {
   branchSchema,
   llmRealSchema,
   loopSchema,
+  loopBreakContinueSchema,
   branchTwoLayersSchema,
   validateInputsSchema,
   httpSchema,

+ 119 - 0
packages/runtime/js-core/src/domain/__tests__/schemas/loop-break-continue.test.ts

@@ -0,0 +1,119 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { describe, expect, it } from 'vitest';
+import { IContainer, IEngine, WorkflowStatus } from '@flowgram.ai/runtime-interface';
+
+import { snapshotsToVOData } from '../utils';
+import { WorkflowRuntimeContainer } from '../../container';
+import { TestSchemas } from '.';
+
+const container: IContainer = WorkflowRuntimeContainer.instance;
+
+describe('WorkflowRuntime loop break continue schema', () => {
+  it('should execute a workflow with break and continue logic', async () => {
+    const engine = container.get<IEngine>(IEngine);
+    const { context, processing } = engine.invoke({
+      schema: TestSchemas.loopBreakContinueSchema,
+      inputs: {
+        tasks: [
+          'TASK - A', // index 0, continue
+          'TASK - B', // index 1, continue
+          'TASK - C', // index 2, continue
+          'TASK - D', // index 3, execute
+          'TASK - E', // index 4, execute
+          'TASK - F', // index 5, execute
+          'TASK - G', // index 6, execute
+          'TASK - H', // index 7, break
+          'TASK - I', // index 8, should not reach
+        ],
+      },
+    });
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);
+    const result = await processing;
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);
+
+    // Only tasks with index 3-6 should be processed (index > 2 and <= 6)
+    expect(result).toStrictEqual({
+      outputs: [
+        'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is "You are a helpful assistant No.3", prompt is "TASK - D"',
+        'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is "You are a helpful assistant No.4", prompt is "TASK - E"',
+        'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is "You are a helpful assistant No.5", prompt is "TASK - F"',
+        'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is "You are a helpful assistant No.6", prompt is "TASK - G"',
+      ],
+    });
+
+    const snapshots = snapshotsToVOData(context.snapshotCenter.exportAll());
+
+    // Verify that start node executed correctly
+    const startSnapshot = snapshots.find((s) => s.nodeID === 'start_0');
+    expect(startSnapshot).toBeDefined();
+    expect(startSnapshot?.outputs.tasks).toEqual([
+      'TASK - A',
+      'TASK - B',
+      'TASK - C',
+      'TASK - D',
+      'TASK - E',
+      'TASK - F',
+      'TASK - G',
+      'TASK - H',
+      'TASK - I',
+    ]);
+
+    // Verify that loop node executed correctly
+    const loopSnapshot = snapshots.find((s) => s.nodeID === 'loop_0');
+    expect(loopSnapshot).toBeDefined();
+    expect(loopSnapshot?.outputs.results).toEqual([
+      'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is "You are a helpful assistant No.3", prompt is "TASK - D"',
+      'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is "You are a helpful assistant No.4", prompt is "TASK - E"',
+      'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is "You are a helpful assistant No.5", prompt is "TASK - F"',
+      'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is "You are a helpful assistant No.6", prompt is "TASK - G"',
+    ]);
+
+    // Verify that only the expected items and indexes were processed
+    expect(loopSnapshot?.outputs.items).toEqual(['TASK - D', 'TASK - E', 'TASK - F', 'TASK - G']);
+    expect(loopSnapshot?.outputs.indexes).toEqual([3, 4, 5, 6]);
+
+    // Verify that LLM node was executed exactly 4 times (for indexes 3-6)
+    const llmSnapshots = snapshots.filter((s) => s.nodeID === 'llm_0');
+    expect(llmSnapshots).toHaveLength(4);
+
+    // Verify the LLM executions
+    expect(llmSnapshots[0].inputs.systemPrompt).toBe('You are a helpful assistant No.3');
+    expect(llmSnapshots[0].inputs.prompt).toBe('TASK - D');
+    expect(llmSnapshots[1].inputs.systemPrompt).toBe('You are a helpful assistant No.4');
+    expect(llmSnapshots[1].inputs.prompt).toBe('TASK - E');
+    expect(llmSnapshots[2].inputs.systemPrompt).toBe('You are a helpful assistant No.5');
+    expect(llmSnapshots[2].inputs.prompt).toBe('TASK - F');
+    expect(llmSnapshots[3].inputs.systemPrompt).toBe('You are a helpful assistant No.6');
+    expect(llmSnapshots[3].inputs.prompt).toBe('TASK - G');
+
+    // Verify that continue and break nodes were executed
+    const continueSnapshots = snapshots.filter((s) => s.nodeID === 'continue_0');
+    const breakSnapshots = snapshots.filter((s) => s.nodeID === 'break_0');
+
+    // Continue should be executed 3 times (for indexes 0, 1, 2)
+    expect(continueSnapshots).toHaveLength(3);
+    // Break should be executed 1 time (for index 7)
+    expect(breakSnapshots).toHaveLength(1);
+
+    // Verify workflow status
+    const report = context.reporter.export();
+    expect(report.workflowStatus.status).toBe(WorkflowStatus.Succeeded);
+    expect(report.reports.start_0.status).toBe(WorkflowStatus.Succeeded);
+    expect(report.reports.loop_0.status).toBe(WorkflowStatus.Succeeded);
+    expect(report.reports.llm_0.status).toBe(WorkflowStatus.Succeeded);
+    expect(report.reports.end_0.status).toBe(WorkflowStatus.Succeeded);
+    expect(report.reports.condition_0.status).toBe(WorkflowStatus.Succeeded);
+    expect(report.reports.continue_0.status).toBe(WorkflowStatus.Succeeded);
+    expect(report.reports.break_0.status).toBe(WorkflowStatus.Succeeded);
+
+    // Verify execution counts
+    expect(report.reports.llm_0.snapshots.length).toBe(4);
+    expect(report.reports.condition_0.snapshots.length).toBe(8); // Condition checked for each iteration
+    expect(report.reports.continue_0.snapshots.length).toBe(3);
+    expect(report.reports.break_0.snapshots.length).toBe(1);
+  });
+});

+ 317 - 0
packages/runtime/js-core/src/domain/__tests__/schemas/loop-break-continue.ts

@@ -0,0 +1,317 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { WorkflowSchema } from '@flowgram.ai/runtime-interface';
+
+export const loopBreakContinueSchema: WorkflowSchema = {
+  nodes: [
+    {
+      id: 'start_0',
+      type: 'start',
+      meta: {
+        position: {
+          x: 181,
+          y: 337.5,
+        },
+      },
+      data: {
+        title: 'Start',
+        outputs: {
+          type: 'object',
+          properties: {
+            tasks: {
+              type: 'array',
+              extra: {
+                index: 0,
+              },
+              items: {
+                type: 'string',
+              },
+            },
+          },
+          required: ['tasks'],
+        },
+      },
+    },
+    {
+      id: 'end_0',
+      type: 'end',
+      meta: {
+        position: {
+          x: 2017,
+          y: 337.4,
+        },
+      },
+      data: {
+        title: 'End',
+        inputs: {
+          type: 'object',
+          properties: {
+            outputs: {
+              type: 'array',
+              items: {
+                type: 'string',
+              },
+            },
+          },
+        },
+        inputsValues: {
+          outputs: {
+            type: 'ref',
+            content: ['loop_0', 'results'],
+          },
+        },
+      },
+    },
+    {
+      id: 'loop_0',
+      type: 'loop',
+      meta: {
+        position: {
+          x: 520,
+          y: 90,
+        },
+      },
+      data: {
+        title: 'Loop_1',
+        loopFor: {
+          type: 'ref',
+          content: ['start_0', 'tasks'],
+        },
+        loopOutputs: {
+          results: {
+            type: 'ref',
+            content: ['llm_0', 'result'],
+          },
+          items: {
+            type: 'ref',
+            content: ['loop_0_locals', 'item'],
+          },
+          indexes: {
+            type: 'ref',
+            content: ['loop_0_locals', 'index'],
+          },
+        },
+      },
+      blocks: [
+        {
+          id: 'block_start_0',
+          type: 'block-start',
+          meta: {
+            position: {
+              x: 32,
+              y: 149,
+            },
+          },
+          data: {},
+        },
+        {
+          id: 'block_end_0',
+          type: 'block-end',
+          meta: {
+            position: {
+              x: 1126,
+              y: 371,
+            },
+          },
+          data: {},
+        },
+        {
+          id: 'llm_0',
+          type: 'llm',
+          meta: {
+            position: {
+              x: 804,
+              y: 213,
+            },
+          },
+          data: {
+            title: 'LLM_0',
+            inputsValues: {
+              modelName: {
+                type: 'constant',
+                content: 'AI_MODEL_1',
+              },
+              apiKey: {
+                type: 'constant',
+                content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
+              },
+              apiHost: {
+                type: 'constant',
+                content: 'https://mock-ai-url/api/v3',
+              },
+              temperature: {
+                type: 'constant',
+                content: 0.6,
+              },
+              systemPrompt: {
+                type: 'template',
+                content: 'You are a helpful assistant No.{{loop_0_locals.index}}',
+              },
+              prompt: {
+                type: 'template',
+                content: '{{loop_0_locals.item}}',
+              },
+            },
+            inputs: {
+              type: 'object',
+              required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'],
+              properties: {
+                modelName: {
+                  type: 'string',
+                },
+                apiKey: {
+                  type: 'string',
+                },
+                apiHost: {
+                  type: 'string',
+                },
+                temperature: {
+                  type: 'number',
+                },
+                systemPrompt: {
+                  type: 'string',
+                  extra: {
+                    formComponent: 'prompt-editor',
+                  },
+                },
+                prompt: {
+                  type: 'string',
+                  extra: {
+                    formComponent: 'prompt-editor',
+                  },
+                },
+              },
+            },
+            outputs: {
+              type: 'object',
+              properties: {
+                result: {
+                  type: 'string',
+                },
+              },
+            },
+          },
+        },
+        {
+          id: 'condition_0',
+          type: 'condition',
+          meta: {
+            position: {
+              x: 344,
+              y: 45,
+            },
+          },
+          data: {
+            title: 'Condition',
+            conditions: [
+              {
+                value: {
+                  left: {
+                    type: 'ref',
+                    content: ['loop_0_locals', 'index'],
+                  },
+                  operator: 'lte',
+                  right: {
+                    type: 'constant',
+                    content: 2,
+                  },
+                },
+                key: 'if_1',
+              },
+              {
+                value: {
+                  left: {
+                    type: 'ref',
+                    content: ['loop_0_locals', 'index'],
+                  },
+                  operator: 'gt',
+                  right: {
+                    type: 'constant',
+                    content: 6,
+                  },
+                },
+                key: 'if_2',
+              },
+              {
+                value: {
+                  type: 'expression',
+                  content: '',
+                  left: {
+                    type: 'ref',
+                    content: ['loop_0_locals', 'index'],
+                  },
+                  operator: 'is_not_empty',
+                },
+                key: 'if_3',
+              },
+            ],
+          },
+        },
+        {
+          id: 'continue_0',
+          type: 'continue',
+          meta: {
+            position: {
+              x: 804,
+              y: 84.3,
+            },
+          },
+          data: {
+            title: 'Continue_0',
+          },
+        },
+        {
+          id: 'break_0',
+          type: 'break',
+          meta: {
+            position: {
+              x: 804,
+              y: 149,
+            },
+          },
+          data: {
+            title: 'Break_0',
+          },
+        },
+      ],
+      edges: [
+        {
+          sourceNodeID: 'block_start_0',
+          targetNodeID: 'condition_0',
+        },
+        {
+          sourceNodeID: 'llm_0',
+          targetNodeID: 'block_end_0',
+        },
+        {
+          sourceNodeID: 'condition_0',
+          targetNodeID: 'llm_0',
+          sourcePortID: 'if_3',
+        },
+        {
+          sourceNodeID: 'condition_0',
+          targetNodeID: 'continue_0',
+          sourcePortID: 'if_1',
+        },
+        {
+          sourceNodeID: 'condition_0',
+          targetNodeID: 'break_0',
+          sourcePortID: 'if_2',
+        },
+      ],
+    },
+  ],
+  edges: [
+    {
+      sourceNodeID: 'start_0',
+      targetNodeID: 'loop_0',
+    },
+    {
+      sourceNodeID: 'loop_0',
+      targetNodeID: 'end_0',
+    },
+  ],
+};

+ 35 - 0
packages/runtime/js-core/src/domain/cache/index.ts

@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { ICache } from '@flowgram.ai/runtime-interface';
+
+export class WorkflowRuntimeCache implements ICache {
+  private map: Map<string, any>;
+
+  public init(): void {
+    this.map = new Map();
+  }
+
+  public dispose(): void {
+    this.map.clear();
+  }
+
+  public get(key: string): any {
+    return this.map.get(key);
+  }
+
+  public set(key: string, value: any): this {
+    this.map.set(key, value);
+    return this;
+  }
+
+  public delete(key: string): boolean {
+    return this.map.delete(key);
+  }
+
+  public has(key: string): boolean {
+    return this.map.has(key);
+  }
+}

+ 12 - 0
packages/runtime/js-core/src/domain/context/index.ts

@@ -15,9 +15,11 @@ import {
   IIOCenter,
   ContextData,
   IMessageCenter,
+  ICache,
 } from '@flowgram.ai/runtime-interface';
 
 import { WorkflowRuntimeMessageCenter } from '@workflow/message';
+import { WorkflowRuntimeCache } from '@workflow/cache';
 import { uuid } from '@infra/utils';
 import { WorkflowRuntimeVariableStore } from '../variable';
 import { WorkflowRuntimeStatusCenter } from '../status';
@@ -30,6 +32,8 @@ import { WorkflowRuntimeDocument } from '../document';
 export class WorkflowRuntimeContext implements IContext {
   public readonly id: string;
 
+  public readonly cache: ICache;
+
   public readonly document: IDocument;
 
   public readonly variableStore: IVariableStore;
@@ -50,6 +54,7 @@ export class WorkflowRuntimeContext implements IContext {
 
   constructor(data: ContextData) {
     this.id = uuid();
+    this.cache = data.cache;
     this.document = data.document;
     this.variableStore = data.variableStore;
     this.state = data.state;
@@ -62,6 +67,7 @@ export class WorkflowRuntimeContext implements IContext {
 
   public init(params: InvokeParams): void {
     const { schema, inputs } = params;
+    this.cache.init();
     this.document.init(schema);
     this.variableStore.init();
     this.state.init();
@@ -77,6 +83,7 @@ export class WorkflowRuntimeContext implements IContext {
       subContext.dispose();
     });
     this.subContexts = [];
+    this.cache.dispose();
     this.document.dispose();
     this.variableStore.dispose();
     this.state.dispose();
@@ -88,10 +95,12 @@ export class WorkflowRuntimeContext implements IContext {
   }
 
   public sub(): IContext {
+    const cache = new WorkflowRuntimeCache();
     const variableStore = new WorkflowRuntimeVariableStore();
     variableStore.setParent(this.variableStore);
     const state = new WorkflowRuntimeState(variableStore);
     const contextData: ContextData = {
+      cache,
       document: this.document,
       ioCenter: this.ioCenter,
       snapshotCenter: this.snapshotCenter,
@@ -103,12 +112,14 @@ export class WorkflowRuntimeContext implements IContext {
     };
     const subContext = new WorkflowRuntimeContext(contextData);
     this.subContexts.push(subContext);
+    subContext.cache.init();
     subContext.variableStore.init();
     subContext.state.init();
     return subContext;
   }
 
   public static create(): IContext {
+    const cache = new WorkflowRuntimeCache();
     const document = new WorkflowRuntimeDocument();
     const variableStore = new WorkflowRuntimeVariableStore();
     const state = new WorkflowRuntimeState(variableStore);
@@ -123,6 +134,7 @@ export class WorkflowRuntimeContext implements IContext {
       messageCenter
     );
     return new WorkflowRuntimeContext({
+      cache,
       document,
       variableStore,
       state,

+ 7 - 1
packages/runtime/js-core/src/domain/engine/index.ts

@@ -158,7 +158,13 @@ export class WorkflowRuntimeEngine implements IEngine {
 
   private async executeNext(params: { context: IContext; node: INode; nextNodes: INode[] }) {
     const { context, node, nextNodes } = params;
-    if (node.type === FlowGramNode.End || node.type === FlowGramNode.BlockEnd) {
+    const terminatingNodeTypes = [
+      FlowGramNode.End,
+      FlowGramNode.BlockEnd,
+      FlowGramNode.Break,
+      FlowGramNode.Continue,
+    ];
+    if (terminatingNodeTypes.includes(node.type)) {
       return;
     }
     if (nextNodes.length === 0) {

+ 22 - 0
packages/runtime/js-core/src/nodes/break/index.ts

@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import {
+  ExecutionContext,
+  ExecutionResult,
+  FlowGramNode,
+  INodeExecutor,
+} from '@flowgram.ai/runtime-interface';
+
+export class BreakExecutor implements INodeExecutor {
+  public type = FlowGramNode.Break;
+
+  public async execute(context: ExecutionContext): Promise<ExecutionResult> {
+    context.runtime.cache.set('loop-break', true);
+    return {
+      outputs: {},
+    };
+  }
+}

+ 2 - 1
packages/runtime/js-core/src/nodes/condition/index.ts

@@ -13,6 +13,7 @@ import {
   WorkflowVariableType,
 } from '@flowgram.ai/runtime-interface';
 
+import { WorkflowRuntimeType } from '@infra/index';
 import { ConditionValue, Conditions } from './type';
 import { conditionRules } from './rules';
 import { conditionHandlers } from './handlers';
@@ -70,7 +71,7 @@ export class ConditionExecutor implements INodeExecutor {
         `Condition left type "${condition.leftType}" has no operator "${condition.operator}"`
       );
     }
-    if (ruleType !== condition.rightType) {
+    if (!WorkflowRuntimeType.isTypeEqual(ruleType, condition.rightType)) {
       return false;
     }
     return true;

+ 22 - 0
packages/runtime/js-core/src/nodes/continue/index.ts

@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import {
+  ExecutionContext,
+  ExecutionResult,
+  FlowGramNode,
+  INodeExecutor,
+} from '@flowgram.ai/runtime-interface';
+
+export class ContinueExecutor implements INodeExecutor {
+  public type = FlowGramNode.Continue;
+
+  public async execute(context: ExecutionContext): Promise<ExecutionResult> {
+    context.runtime.cache.set('loop-continue', true);
+    return {
+      outputs: {},
+    };
+  }
+}

+ 4 - 0
packages/runtime/js-core/src/nodes/index.ts

@@ -11,8 +11,10 @@ import { LLMExecutor } from './llm';
 import { HTTPExecutor } from './http';
 import { EndExecutor } from './end';
 import { BlockEndExecutor, BlockStartExecutor } from './empty';
+import { ContinueExecutor } from './continue';
 import { ConditionExecutor } from './condition';
 import { CodeExecutor } from './code';
+import { BreakExecutor } from './break';
 
 export const WorkflowRuntimeNodeExecutors: INodeExecutorFactory[] = [
   StartExecutor,
@@ -24,4 +26,6 @@ export const WorkflowRuntimeNodeExecutors: INodeExecutorFactory[] = [
   BlockEndExecutor,
   HTTPExecutor,
   CodeExecutor,
+  BreakExecutor,
+  ContinueExecutor,
 ];

+ 14 - 0
packages/runtime/js-core/src/nodes/loop/index.ts

@@ -69,6 +69,12 @@ export class LoopExecutor implements INodeExecutor {
       } catch (e) {
         throw new Error(`Loop block execute error`);
       }
+      if (this.isBreak(subContext)) {
+        break;
+      }
+      if (this.isContinue(subContext)) {
+        continue;
+      }
       const blockOutput = this.getBlockOutput(context, subContext);
       blockOutputs.push(blockOutput);
     }
@@ -175,4 +181,12 @@ export class LoopExecutor implements INodeExecutor {
     const loopOutputsDeclare = loopNodeData.loopOutputs ?? {};
     return loopOutputsDeclare;
   }
+
+  private isBreak(subContext: IContext): boolean {
+    return subContext.cache.get('loop-break') === true;
+  }
+
+  private isContinue(subContext: IContext): boolean {
+    return subContext.cache.get('loop-continue') === true;
+  }
 }