Просмотр исходного кода

fix(runtime): parse constant json value (#811)

* fix(runtime): parse constant json value

* chore(runtime): parse flow value must fill declare type
Louis Young 4 месяцев назад
Родитель
Сommit
788368cafe

+ 4 - 4
packages/runtime/interface/src/runtime/state/index.ts

@@ -24,10 +24,10 @@ export interface IState {
   parseInputs(params: { values: Record<string, IFlowValue>; declare: IJsonSchema }): WorkflowInputs;
   parseRef<T = unknown>(ref: IFlowRefValue): IVariableParseResult<T> | null;
   parseTemplate(template: IFlowTemplateValue): IVariableParseResult<string> | null;
-  parseValue<T = unknown>(
-    flowValue: IFlowValue,
-    type?: WorkflowVariableType
-  ): IVariableParseResult<T> | null;
+  parseFlowValue<T = unknown>(params: {
+    flowValue: IFlowValue;
+    declareType: WorkflowVariableType;
+  }): IVariableParseResult<T> | null;
   isExecutedNode(node: INode): boolean;
   addExecutedNode(node: INode): void;
 }

+ 77 - 0
packages/runtime/js-core/src/domain/__tests__/schemas/end-constant.test.ts

@@ -0,0 +1,77 @@
+/**
+ * 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 end constant schema', () => {
+  it('should execute a workflow', async () => {
+    const engine = container.get<IEngine>(IEngine);
+    const { context, processing } = engine.invoke({
+      schema: TestSchemas.endConstantSchema,
+      inputs: {},
+    });
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);
+    const result = await processing;
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);
+    // eslint-disable-next-line no-console
+    console.log('@result', JSON.stringify(result));
+    // const c = { str: 'ABC', num: 123.123, int: 123, bool: false };
+    expect(result).toStrictEqual({
+      str: 'ABC',
+      num: 123.123,
+      int: 123,
+      bool: false,
+      obj: {
+        key_str: 'value',
+        key_int: 123,
+        key_bool: true,
+      },
+      map: {
+        key: 'value',
+      },
+      arr_str: ['AAA', 'BBB', 'CCC'],
+      date: '2000-01-01T00:00:00.000Z',
+    });
+    const snapshots = snapshotsToVOData(context.snapshotCenter.exportAll());
+    expect(snapshots).toStrictEqual([
+      { nodeID: 'start_0', inputs: {}, outputs: {}, data: {} },
+      {
+        nodeID: 'end_0',
+        inputs: {
+          str: 'ABC',
+          num: 123.123,
+          int: 123,
+          bool: false,
+          obj: { key_str: 'value', key_int: 123, key_bool: true },
+          map: { key: 'value' },
+          arr_str: ['AAA', 'BBB', 'CCC'],
+          date: '2000-01-01T00:00:00.000Z',
+        },
+        outputs: {
+          str: 'ABC',
+          num: 123.123,
+          int: 123,
+          bool: false,
+          obj: { key_str: 'value', key_int: 123, key_bool: true },
+          map: { key: 'value' },
+          arr_str: ['AAA', 'BBB', 'CCC'],
+          date: '2000-01-01T00:00:00.000Z',
+        },
+        data: {},
+      },
+    ]);
+    const report = context.reporter.export();
+    expect(report.workflowStatus.status).toBe(WorkflowStatus.Succeeded);
+    expect(report.reports.start_0.status).toBe(WorkflowStatus.Succeeded);
+    expect(report.reports.end_0.status).toBe(WorkflowStatus.Succeeded);
+  });
+});

+ 114 - 0
packages/runtime/js-core/src/domain/__tests__/schemas/end-constant.ts

@@ -0,0 +1,114 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import type { WorkflowSchema } from '@flowgram.ai/runtime-interface';
+
+export const endConstantSchema: WorkflowSchema = {
+  nodes: [
+    {
+      id: 'start_0',
+      type: 'start',
+      meta: {
+        position: {
+          x: 180,
+          y: 22.5,
+        },
+      },
+      data: {
+        title: 'Start',
+        outputs: {
+          type: 'object',
+          properties: {},
+          required: [],
+        },
+      },
+    },
+    {
+      id: 'end_0',
+      type: 'end',
+      meta: {
+        position: {
+          x: 640,
+          y: 0,
+        },
+      },
+      data: {
+        title: 'End',
+        inputsValues: {
+          str: {
+            type: 'constant',
+            content: 'ABC',
+          },
+          num: {
+            type: 'constant',
+            content: 123.123,
+          },
+          int: {
+            type: 'constant',
+            content: 123,
+          },
+          bool: {
+            type: 'constant',
+            content: false,
+          },
+          obj: {
+            type: 'constant',
+            content: '{"key_str": "value","key_int": 123,"key_bool": true}',
+          },
+          map: {
+            type: 'constant',
+            content: '{ "key": "value" }',
+          },
+          arr_str: {
+            type: 'constant',
+            content: '["AAA", "BBB", "CCC"]',
+          },
+          date: {
+            type: 'constant',
+            content: '2000-01-01T00:00:00.000Z',
+          },
+        },
+        inputs: {
+          type: 'object',
+          properties: {
+            str: {
+              type: 'string',
+            },
+            num: {
+              type: 'number',
+            },
+            int: {
+              type: 'integer',
+            },
+            bool: {
+              type: 'boolean',
+            },
+            obj: {
+              type: 'object',
+            },
+            map: {
+              type: 'map',
+            },
+            arr_str: {
+              type: 'array',
+              items: {
+                type: 'string',
+              },
+            },
+            date: {
+              type: 'date-time',
+            },
+          },
+        },
+      },
+    },
+  ],
+  edges: [
+    {
+      sourceNodeID: 'start_0',
+      targetNodeID: 'end_0',
+    },
+  ],
+};

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

@@ -9,6 +9,7 @@ import { loopBreakContinueSchema } from './loop-break-continue';
 import { loopSchema } from './loop';
 import { llmRealSchema } from './llm-real';
 import { httpSchema } from './http';
+import { endConstantSchema } from './end-constant';
 import { codeSchema } from './code';
 import { branchTwoLayersSchema } from './branch-two-layers';
 import { branchSchema } from './branch';
@@ -25,4 +26,5 @@ export const TestSchemas = {
   validateInputsSchema,
   httpSchema,
   codeSchema,
+  endConstantSchema,
 };

+ 31 - 7
packages/runtime/js-core/src/domain/state/index.ts

@@ -16,6 +16,7 @@ import {
   WorkflowVariableType,
   IFlowTemplateValue,
   IJsonSchema,
+  IFlowConstantValue,
 } from '@flowgram.ai/runtime-interface';
 
 import { uuid, WorkflowRuntimeType } from '@infra/utils';
@@ -79,19 +80,19 @@ export class WorkflowRuntimeState implements IState {
     if (!declare || !values) {
       return {};
     }
-    return Object.entries(values).reduce((prev, [key, inputValue]) => {
+    return Object.entries(values).reduce((prev, [key, flowValue]) => {
       const typeInfo = declare.properties?.[key];
       if (!typeInfo) {
         return prev;
       }
-      const expectType = typeInfo.type as WorkflowVariableType;
+      const declareType = typeInfo.type as WorkflowVariableType;
       // get value
-      const result = this.parseValue(inputValue);
+      const result = this.parseFlowValue({ flowValue, declareType });
       if (!result) {
         return prev;
       }
       const { value, type } = result;
-      if (!WorkflowRuntimeType.isTypeEqual(type, expectType)) {
+      if (!WorkflowRuntimeType.isTypeEqual(type, declareType)) {
         return prev;
       }
       prev[key] = value;
@@ -149,14 +150,18 @@ export class WorkflowRuntimeState implements IState {
     };
   }
 
-  public parseValue<T = unknown>(flowValue: IFlowValue): IVariableParseResult<T> | null {
+  public parseFlowValue<T = unknown>(params: {
+    flowValue: IFlowValue;
+    declareType?: WorkflowVariableType;
+  }): IVariableParseResult<T> | null {
+    const { flowValue, declareType } = params;
     if (!flowValue?.type) {
       throw new Error(`Invalid flow value type: ${(flowValue as any).type}`);
     }
     // constant
     if (flowValue.type === 'constant') {
-      const value = flowValue.content as T;
-      const type = WorkflowRuntimeType.getWorkflowType(value);
+      const value = this.parseContentValue<T>(flowValue, declareType);
+      const type = declareType ?? WorkflowRuntimeType.getWorkflowType(value);
       if (isNil(value) || !type) {
         return null;
       }
@@ -184,4 +189,23 @@ export class WorkflowRuntimeState implements IState {
   public addExecutedNode(node: INode): void {
     this.executedNodes.add(node.id);
   }
+
+  private parseContentValue<T = unknown>(
+    flowValue: IFlowConstantValue,
+    declareType?: WorkflowVariableType
+  ): T {
+    const JSONTypes = [
+      WorkflowVariableType.Object,
+      WorkflowVariableType.Array,
+      WorkflowVariableType.Map,
+    ];
+    if (declareType && JSONTypes.includes(declareType) && typeof flowValue.content === 'string') {
+      try {
+        return JSON.parse(flowValue.content) as T;
+      } catch (e) {
+        return flowValue.content as T;
+      }
+    }
+    return flowValue.content as T;
+  }
 }

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

@@ -6,6 +6,7 @@
 import { isNil } from 'lodash-es';
 import {
   ConditionItem,
+  ConditionOperator,
   ExecutionContext,
   ExecutionResult,
   FlowGramNode,
@@ -50,7 +51,13 @@ export class ConditionExecutor implements INodeExecutor {
     const parsedLeft = context.runtime.state.parseRef(left);
     const leftValue = parsedLeft?.value ?? null;
     const leftType = parsedLeft?.type ?? WorkflowVariableType.Null;
-    const parsedRight = Boolean(right) ? context.runtime.state.parseValue(right) : null;
+    const expectedRightType = this.getRuleType({ leftType, operator });
+    const parsedRight = Boolean(right)
+      ? context.runtime.state.parseFlowValue({
+          flowValue: right,
+          declareType: expectedRightType,
+        })
+      : null;
     const rightValue = parsedRight?.value ?? null;
     const rightType = parsedRight?.type ?? WorkflowVariableType.Null;
     return {
@@ -88,4 +95,20 @@ export class ConditionExecutor implements INodeExecutor {
     const isActive = handler(condition);
     return isActive;
   }
+
+  private getRuleType(params: {
+    leftType: WorkflowVariableType;
+    operator: ConditionOperator;
+  }): WorkflowVariableType {
+    const { leftType, operator } = params;
+    const rule = conditionRules[leftType];
+    if (isNil(rule)) {
+      return WorkflowVariableType.Null;
+    }
+    const ruleType = rule[operator];
+    if (isNil(ruleType)) {
+      return WorkflowVariableType.Null;
+    }
+    return ruleType;
+  }
 }