Explorar o código

feat(runtime): support default value (#820)

Louis Young hai 4 meses
pai
achega
517e38e23c

+ 9 - 2
apps/demo-free-layout/src/components/testrun/hooks/use-sync-default.ts

@@ -5,7 +5,14 @@
 
 import { useEffect } from 'react';
 
-import { TestRunFormMeta } from '../testrun-form/type';
+import { TestRunFormMeta, TestRunFormMetaItem } from '../testrun-form/type';
+
+const getDefaultValue = (meta: TestRunFormMetaItem) => {
+  if (['object', 'array', 'map'].includes(meta.type) && typeof meta.defaultValue === 'string') {
+    return JSON.parse(meta.defaultValue);
+  }
+  return meta.defaultValue;
+};
 
 export const useSyncDefault = (params: {
   formMeta: TestRunFormMeta;
@@ -19,7 +26,7 @@ export const useSyncDefault = (params: {
     formMeta.map((meta) => {
       // If there is no value in values but there is a default value, trigger onChange once
       if (!(meta.name in values) && meta.defaultValue !== undefined) {
-        formMetaValues = { ...formMetaValues, [meta.name]: meta.defaultValue };
+        formMetaValues = { ...formMetaValues, [meta.name]: getDefaultValue(meta) };
       }
     });
     setValues({

+ 3 - 1
packages/runtime/interface/src/schema/workflow.ts

@@ -4,11 +4,13 @@
  */
 
 import type { WorkflowNodeSchema } from './node';
-import { WorkflowGroupSchema } from './group';
+import type { IJsonSchema } from './json-schema';
+import type { WorkflowGroupSchema } from './group';
 import type { WorkflowEdgeSchema } from './edge';
 
 export interface WorkflowSchema {
   nodes: WorkflowNodeSchema[];
   edges: WorkflowEdgeSchema[];
   groups?: WorkflowGroupSchema[];
+  globalVariable?: IJsonSchema;
 }

+ 0 - 1
packages/runtime/js-core/src/domain/__tests__/schemas/branch-two-layers.ts

@@ -23,7 +23,6 @@ export const branchTwoLayersSchema: WorkflowSchema = {
           properties: {
             model_id: {
               type: 'integer',
-              default: 'Hello Flow.',
               extra: {
                 index: 0,
               },

+ 0 - 1
packages/runtime/js-core/src/domain/__tests__/schemas/branch.ts

@@ -23,7 +23,6 @@ export const branchSchema: WorkflowSchema = {
           properties: {
             model_id: {
               type: 'integer',
-              default: 'Hello Flow.',
               extra: {
                 index: 0,
               },

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

@@ -22,9 +22,6 @@ describe('WorkflowRuntime end constant schema', () => {
     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,

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

@@ -59,7 +59,7 @@ export const endConstantSchema: WorkflowSchema = {
           },
           map: {
             type: 'constant',
-            content: '{ "key": "value" }',
+            content: '{"key": "value"}',
           },
           arr_str: {
             type: 'constant',

+ 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 { startDefaultSchema } from './start-default';
 import { loopBreakContinueSchema } from './loop-break-continue';
 import { loopSchema } from './loop';
 import { llmRealSchema } from './llm-real';
@@ -27,4 +28,5 @@ export const TestSchemas = {
   httpSchema,
   codeSchema,
   endConstantSchema,
+  startDefaultSchema,
 };

+ 74 - 0
packages/runtime/js-core/src/domain/__tests__/schemas/start-default.test.ts

@@ -0,0 +1,74 @@
+/**
+ * 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 start default schema', () => {
+  it('should execute a workflow', async () => {
+    const engine = container.get<IEngine>(IEngine);
+    const { context, processing } = engine.invoke({
+      schema: TestSchemas.startDefaultSchema,
+      inputs: {},
+    });
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);
+    const result = await processing;
+    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);
+    expect(result).toStrictEqual({
+      s_str: 'ABC',
+      s_num: 123.123,
+      s_int: 123,
+      s_bool: false,
+      s_obj: {
+        key_str: 'value',
+        key_int: 123,
+        key_bool: true,
+      },
+      s_map: {
+        key: 'value',
+      },
+      s_arr_str: ['AAA', 'BBB', 'CCC'],
+      s_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: {
+          s_str: 'ABC',
+          s_num: 123.123,
+          s_int: 123,
+          s_bool: false,
+          s_obj: { key_str: 'value', key_int: 123, key_bool: true },
+          s_arr_str: ['AAA', 'BBB', 'CCC'],
+          s_map: { key: 'value' },
+          s_date: '2000-01-01T00:00:00.000Z',
+        },
+        outputs: {
+          s_str: 'ABC',
+          s_num: 123.123,
+          s_int: 123,
+          s_bool: false,
+          s_obj: { key_str: 'value', key_int: 123, key_bool: true },
+          s_arr_str: ['AAA', 'BBB', 'CCC'],
+          s_map: { key: 'value' },
+          s_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);
+  });
+});

+ 173 - 0
packages/runtime/js-core/src/domain/__tests__/schemas/start-default.ts

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

+ 0 - 6
packages/runtime/js-core/src/domain/__tests__/schemas/two-llm.ts

@@ -23,14 +23,12 @@ export const twoLLMSchema: WorkflowSchema = {
           properties: {
             query: {
               type: 'string',
-              default: 'Hello Flow.',
               extra: {
                 index: 0,
               },
             },
             enable: {
               type: 'boolean',
-              default: true,
               extra: {
                 index: 1,
               },
@@ -85,10 +83,6 @@ export const twoLLMSchema: WorkflowSchema = {
           properties: {
             result: {
               type: 'string',
-              default: {
-                type: 'ref',
-                content: ['llm_BjEpK', 'result'],
-              },
             },
           },
         },

+ 15 - 16
packages/runtime/js-core/src/domain/state/index.ts

@@ -16,7 +16,6 @@ import {
   WorkflowVariableType,
   IFlowTemplateValue,
   IJsonSchema,
-  IFlowConstantValue,
 } from '@flowgram.ai/runtime-interface';
 
 import { uuid, WorkflowRuntimeType } from '@infra/utils';
@@ -49,18 +48,18 @@ export class WorkflowRuntimeState implements IState {
 
   public setNodeOutputs(params: { node: INode; outputs: WorkflowOutputs }): void {
     const { node, outputs } = params;
-    const outputsDeclare = node.declare.outputs;
-    // TODO validation service type check, deeply compare input & schema
-    if (!outputsDeclare) {
+    const outputsDeclare = node.declare.outputs as IJsonSchema<'object'>;
+    if (outputsDeclare?.type !== 'object' || !outputsDeclare.properties) {
       return;
     }
-    Object.entries(outputs).forEach(([key, value]) => {
-      const typeInfo = outputsDeclare.properties?.[key];
-      if (!typeInfo) {
+    Object.entries(outputsDeclare.properties).forEach(([key, typeInfo]) => {
+      if (!key || !typeInfo) {
         return;
       }
       const type = typeInfo.type as WorkflowVariableType;
       const itemsType = typeInfo.items?.type as WorkflowVariableType;
+      const defaultValue = this.parseJSONContent(typeInfo.default, type);
+      const value = outputs[key] ?? defaultValue;
       // create variable
       this.variableStore.setVariable({
         nodeID: node.id,
@@ -152,7 +151,7 @@ export class WorkflowRuntimeState implements IState {
 
   public parseFlowValue<T = unknown>(params: {
     flowValue: IFlowValue;
-    declareType?: WorkflowVariableType;
+    declareType: WorkflowVariableType;
   }): IVariableParseResult<T> | null {
     const { flowValue, declareType } = params;
     if (!flowValue?.type) {
@@ -160,7 +159,7 @@ export class WorkflowRuntimeState implements IState {
     }
     // constant
     if (flowValue.type === 'constant') {
-      const value = this.parseContentValue<T>(flowValue, declareType);
+      const value = this.parseJSONContent<T>(flowValue.content, declareType);
       const type = declareType ?? WorkflowRuntimeType.getWorkflowType(value);
       if (isNil(value) || !type) {
         return null;
@@ -190,22 +189,22 @@ export class WorkflowRuntimeState implements IState {
     this.executedNodes.add(node.id);
   }
 
-  private parseContentValue<T = unknown>(
-    flowValue: IFlowConstantValue,
-    declareType?: WorkflowVariableType
+  private parseJSONContent<T = unknown>(
+    jsonContent: string | unknown,
+    declareType: WorkflowVariableType
   ): T {
     const JSONTypes = [
       WorkflowVariableType.Object,
       WorkflowVariableType.Array,
       WorkflowVariableType.Map,
     ];
-    if (declareType && JSONTypes.includes(declareType) && typeof flowValue.content === 'string') {
+    if (declareType && JSONTypes.includes(declareType) && typeof jsonContent === 'string') {
       try {
-        return JSON.parse(flowValue.content) as T;
+        return JSON.parse(jsonContent) as T;
       } catch (e) {
-        return flowValue.content as T;
+        return jsonContent as T;
       }
     }
-    return flowValue.content as T;
+    return jsonContent as T;
   }
 }