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

chore(material): add flow-value utils and fix some bugs (#686)

Yiwei Mao 4 месяцев назад
Родитель
Сommit
5d9242e2a0

+ 4 - 4
apps/demo-fixed-layout/src/initial-data.ts

@@ -71,11 +71,11 @@ export const initialData: FlowDocumentJSON = {
                     content: 0.5,
                   },
                   systemPrompt: {
-                    type: 'constant',
+                    type: 'template',
                     content: '# Role\nYou are an AI assistant.\n',
                   },
                   prompt: {
-                    type: 'constant',
+                    type: 'template',
                     content: '',
                   },
                 },
@@ -163,11 +163,11 @@ export const initialData: FlowDocumentJSON = {
             content: 0.5,
           },
           systemPrompt: {
-            type: 'constant',
+            type: 'template',
             content: '# Role\nYou are an AI assistant.\n',
           },
           prompt: {
-            type: 'constant',
+            type: 'template',
             content: '',
           },
         },

+ 2 - 2
apps/demo-fixed-layout/src/nodes/llm/index.ts

@@ -41,11 +41,11 @@ export const LLMNodeRegistry: FlowNodeRegistry = {
             content: 0.5,
           },
           systemPrompt: {
-            type: 'constant',
+            type: 'template',
             content: '# Role\nYou are an AI assistant.\n',
           },
           prompt: {
-            type: 'constant',
+            type: 'template',
             content: '',
           },
         },

+ 4 - 4
apps/demo-free-layout/src/initial-data.ts

@@ -419,11 +419,11 @@ export const initialData: FlowDocumentJSON = {
             content: 0.5,
           },
           systemPrompt: {
-            type: 'constant',
+            type: 'template',
             content: '# Role\nYou are an AI assistant.\n',
           },
           prompt: {
-            type: 'constant',
+            type: 'template',
             content: '# User Input\nquery:{{start_0.query}}\nenable:{{start_0.enable}}',
           },
         },
@@ -496,11 +496,11 @@ export const initialData: FlowDocumentJSON = {
             content: 0.5,
           },
           systemPrompt: {
-            type: 'constant',
+            type: 'template',
             content: '# Role\nYou are an AI assistant.\n',
           },
           prompt: {
-            type: 'constant',
+            type: 'template',
             content: '# LLM Input\nresult:{{llm_8--A3.result}}',
           },
         },

+ 2 - 2
apps/demo-free-layout/src/nodes/llm/index.ts

@@ -47,11 +47,11 @@ export const LLMNodeRegistry: FlowNodeRegistry = {
             content: 0.5,
           },
           systemPrompt: {
-            type: 'constant',
+            type: 'template',
             content: '# Role\nYou are an AI assistant.\n',
           },
           prompt: {
-            type: 'constant',
+            type: 'template',
             content: '',
           },
         },

+ 3 - 0
common/config/rush/pnpm-lock.yaml

@@ -2169,6 +2169,9 @@ importers:
       typescript:
         specifier: ^5.8.3
         version: 5.8.3
+      zod:
+        specifier: ^3.24.4
+        version: 3.25.56
     devDependencies:
       '@flowgram.ai/eslint-config':
         specifier: workspace:*

+ 2 - 1
packages/materials/form-materials/package.json

@@ -46,7 +46,8 @@
     "@coze-editor/editor": "0.1.0-alpha.879fbb",
     "@codemirror/view": "~6.38.0",
     "@codemirror/state": "~6.5.2",
-    "typescript": "^5.8.3"
+    "typescript": "^5.8.3",
+    "zod": "^3.24.4"
   },
   "devDependencies": {
     "@flowgram.ai/eslint-config": "workspace:*",

+ 1 - 1
packages/materials/form-materials/src/components/dynamic-value-input/hooks.ts

@@ -8,7 +8,7 @@ import { useMemo, useState } from 'react';
 import { IJsonSchema } from '@flowgram.ai/json-schema';
 import { useScopeAvailable } from '@flowgram.ai/editor';
 
-import { IFlowConstantRefValue } from '@/typings/flow-value';
+import { IFlowConstantRefValue } from '@/typings';
 
 export function useRefVariable(value?: IFlowConstantRefValue) {
   const available = useScopeAvailable();

+ 1 - 1
packages/materials/form-materials/src/components/dynamic-value-input/index.tsx

@@ -9,7 +9,7 @@ import { JsonSchemaUtils, IJsonSchema } from '@flowgram.ai/json-schema';
 import { IconButton } from '@douyinfe/semi-ui';
 import { IconSetting } from '@douyinfe/semi-icons';
 
-import { IFlowConstantRefValue } from '@/typings/flow-value';
+import { IFlowConstantRefValue } from '@/typings';
 import { createInjectMaterial } from '@/shared';
 import { InjectVariableSelector } from '@/components/variable-selector';
 import { TypeSelector } from '@/components/type-selector';

+ 7 - 54
packages/materials/form-materials/src/effects/auto-rename-ref/index.ts

@@ -3,7 +3,6 @@
  * SPDX-License-Identifier: MIT
  */
 
-import { isArray, isObject, uniq } from 'lodash';
 import {
   DataEvent,
   Effect,
@@ -12,6 +11,7 @@ import {
 } from '@flowgram.ai/editor';
 
 import { IFlowRefValue, IFlowTemplateValue } from '@/typings';
+import { FlowValueUtils } from '@/shared';
 
 /**
  * Auto rename ref when form item's key is renamed
@@ -52,7 +52,7 @@ export const autoRenameRefEffect: EffectOptions[] = [
             }
           } else if (_v.type === 'template') {
             // template auto rename
-            const templateKeyPaths = getTemplateKeyPaths(_v);
+            const templateKeyPaths = FlowValueUtils.getTemplateKeyPaths(_v);
             let hasMatch = false;
 
             templateKeyPaths.forEach((_keyPath) => {
@@ -93,34 +93,6 @@ function isKeyPathMatch(keyPath: string[] = [], targetKeyPath: string[]) {
   return targetKeyPath.every((_key, index) => _key === keyPath[index]);
 }
 
-/**
- * get template key paths
- * @param value
- * @returns
- */
-function getTemplateKeyPaths(value: IFlowTemplateValue) {
-  // find all keyPath wrapped in {{}}
-  const keyPathReg = /{{(.*?)}}/g;
-  return uniq(value.content?.match(keyPathReg) || []).map((_keyPath) =>
-    _keyPath.slice(2, -2).split('.')
-  );
-}
-
-/**
- * If value is ref
- * @param value
- * @returns
- */
-function isRef(value: any): value is IFlowRefValue {
-  return (
-    value?.type === 'ref' && Array.isArray(value?.content) && typeof value?.content[0] === 'string'
-  );
-}
-
-function isTemplate(value: any): value is IFlowTemplateValue {
-  return value?.type === 'template' && typeof value?.content === 'string';
-}
-
 /**
  * Traverse value to find ref
  * @param value
@@ -132,29 +104,10 @@ function traverseRef(
   value: any,
   cb: (name: string, _v: IFlowRefValue | IFlowTemplateValue) => void
 ) {
-  if (isObject(value)) {
-    if (isRef(value)) {
-      cb(name, value);
-      return;
-    }
-
-    if (isTemplate(value)) {
-      cb(name, value);
-      return;
-    }
-
-    Object.entries(value).forEach(([_key, _value]) => {
-      traverseRef(`${name}.${_key}`, _value, cb);
-    });
-    return;
-  }
-
-  if (isArray(value)) {
-    value.forEach((_value, idx) => {
-      traverseRef(`${name}[${idx}]`, _value, cb);
-    });
-    return;
+  for (const { value: _v, path } of FlowValueUtils.traverse(value, {
+    includeTypes: ['ref', 'template'],
+    path: name,
+  })) {
+    cb(path, _v as IFlowRefValue | IFlowTemplateValue);
   }
-
-  return;
 }

+ 3 - 75
packages/materials/form-materials/src/form-plugins/infer-inputs-plugin/index.ts

@@ -4,15 +4,9 @@
  */
 
 import { get, set } from 'lodash';
-import { JsonSchemaUtils, IJsonSchema } from '@flowgram.ai/json-schema';
-import {
-  defineFormPluginCreator,
-  getNodePrivateScope,
-  getNodeScope,
-  Scope,
-} from '@flowgram.ai/editor';
+import { defineFormPluginCreator, getNodePrivateScope, getNodeScope } from '@flowgram.ai/editor';
 
-import { IFlowConstantValue, IFlowRefValue, IFlowTemplateValue } from '@/typings';
+import { FlowValueUtils } from '@/shared';
 
 interface InputConfig {
   sourceKey: string;
@@ -30,7 +24,7 @@ export const createInferInputsPlugin = defineFormPluginCreator<InputConfig>({
       set(
         formData,
         targetKey,
-        infer(
+        FlowValueUtils.inferJsonSchema(
           get(formData, sourceKey),
           scope === 'private' ? getNodePrivateScope(ctx.node) : getNodeScope(ctx.node)
         )
@@ -40,69 +34,3 @@ export const createInferInputsPlugin = defineFormPluginCreator<InputConfig>({
     });
   },
 });
-
-function isRef(value: any): value is IFlowRefValue {
-  return (
-    value?.type === 'ref' && Array.isArray(value?.content) && typeof value?.content[0] === 'string'
-  );
-}
-
-function isTemplate(value: any): value is IFlowTemplateValue {
-  return value?.type === 'template' && typeof value?.content === 'string';
-}
-
-function isConstant(value: any): value is IFlowConstantValue {
-  return value?.type === 'constant' && typeof value?.content !== 'undefined';
-}
-
-const infer = (values: any, scope: Scope): IJsonSchema | undefined => {
-  if (typeof values === 'object') {
-    if (isConstant(values)) {
-      if (values?.schema) {
-        return values.schema;
-      }
-
-      if (typeof values.content === 'string') {
-        return {
-          type: 'string',
-        };
-      }
-
-      if (typeof values.content === 'number') {
-        return {
-          type: 'number',
-        };
-      }
-
-      if (typeof values.content === 'boolean') {
-        return {
-          type: 'boolean',
-        };
-      }
-    }
-
-    if (isRef(values)) {
-      const variable = scope.available.getByKeyPath(values?.content);
-      const schema = variable?.type ? JsonSchemaUtils.astToSchema(variable?.type) : undefined;
-
-      return schema;
-    }
-
-    if (isTemplate(values)) {
-      return {
-        type: 'string',
-      };
-    }
-
-    return {
-      type: 'object',
-      properties: Object.keys(values).reduce((acc, key) => {
-        const schema = infer(values[key], scope);
-        if (schema) {
-          acc[key] = schema;
-        }
-        return acc;
-      }, {} as Record<string, IJsonSchema>),
-    };
-  }
-};

+ 6 - 0
packages/materials/form-materials/src/shared/flow-value/index.ts

@@ -0,0 +1,6 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+export * from './utils';

+ 38 - 0
packages/materials/form-materials/src/shared/flow-value/schema.ts

@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import z from 'zod';
+
+// Shared extra schema for flow value types
+export const extraSchema = z
+  .object({
+    index: z.number().optional(),
+  })
+  .optional();
+
+export const constantSchema = z.object({
+  type: z.literal('constant'),
+  content: z.union([z.string(), z.number(), z.boolean()]).optional(),
+  schema: z.any().optional(),
+  extra: extraSchema,
+});
+
+export const refSchema = z.object({
+  type: z.literal('ref'),
+  content: z.array(z.string()).optional(),
+  extra: extraSchema,
+});
+
+export const expressionSchema = z.object({
+  type: z.literal('expression'),
+  content: z.string().optional(),
+  extra: extraSchema,
+});
+
+export const templateSchema = z.object({
+  type: z.literal('template'),
+  content: z.string().optional(),
+  extra: extraSchema,
+});

+ 201 - 0
packages/materials/form-materials/src/shared/flow-value/utils.ts

@@ -0,0 +1,201 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { isArray, isObject, uniq } from 'lodash';
+import { IJsonSchema, JsonSchemaUtils } from '@flowgram.ai/json-schema';
+import { Scope } from '@flowgram.ai/editor';
+
+import {
+  IFlowConstantValue,
+  IFlowRefValue,
+  IFlowExpressionValue,
+  IFlowTemplateValue,
+  IFlowValue,
+  IFlowConstantRefValue,
+  FlowValueType,
+} from '@/typings';
+
+import { constantSchema, refSchema, expressionSchema, templateSchema } from './schema';
+
+export namespace FlowValueUtils {
+  /**
+   * Check if the value is a constant type
+   */
+  export function isConstant(value: any): value is IFlowConstantValue {
+    return constantSchema.safeParse(value).success;
+  }
+
+  /**
+   * Check if the value is a reference type
+   */
+  export function isRef(value: any): value is IFlowRefValue {
+    return refSchema.safeParse(value).success;
+  }
+
+  /**
+   * Check if the value is an expression type
+   */
+  export function isExpression(value: any): value is IFlowExpressionValue {
+    return expressionSchema.safeParse(value).success;
+  }
+
+  /**
+   * Check if the value is a template type
+   */
+  export function isTemplate(value: any): value is IFlowTemplateValue {
+    return templateSchema.safeParse(value).success;
+  }
+
+  /**
+   * Check if the value is either a constant or reference type
+   */
+  export function isConstantOrRef(value: any): value is IFlowConstantRefValue {
+    return isConstant(value) || isRef(value);
+  }
+
+  /**
+   * Check if the value is a valid flow value type
+   */
+  export function isFlowValue(value: any): value is IFlowValue {
+    return isConstant(value) || isRef(value) || isExpression(value) || isTemplate(value);
+  }
+
+  /**
+   * Traverse all flow values in the given value
+   * @param value The value to traverse
+   * @param options The options to traverse
+   * @returns A generator of flow values
+   */
+  export function* traverse(
+    value: any,
+    options: {
+      includeTypes?: FlowValueType[];
+      path?: string;
+    }
+  ): Generator<{ value: IFlowValue; path: string }> {
+    const { includeTypes = ['ref', 'template'], path = '' } = options;
+
+    if (isObject(value)) {
+      if (isRef(value) && includeTypes.includes('ref')) {
+        yield { value, path };
+        return;
+      }
+
+      if (isTemplate(value) && includeTypes.includes('template')) {
+        yield { value, path };
+        return;
+      }
+
+      if (isExpression(value) && includeTypes.includes('expression')) {
+        yield { value, path };
+        return;
+      }
+
+      if (isConstant(value) && includeTypes.includes('constant')) {
+        yield { value, path };
+        return;
+      }
+
+      for (const [_key, _value] of Object.entries(value)) {
+        yield* traverse(_value, { ...options, path: `${path}.${_key}` });
+      }
+      return;
+    }
+
+    if (isArray(value)) {
+      for (const [_idx, _value] of value.entries()) {
+        yield* traverse(_value, { ...options, path: `${path}[${_idx}]` });
+      }
+      return;
+    }
+
+    return;
+  }
+
+  /**
+   * Get all key paths in the template value
+   * @param value The template value
+   * @returns A list of key paths
+   */
+  export function getTemplateKeyPaths(value: IFlowTemplateValue) {
+    // find all keyPath wrapped in {{}}
+    const keyPathReg = /{{(.*?)}}/g;
+    return uniq(value.content?.match(keyPathReg) || []).map((_keyPath) =>
+      _keyPath.slice(2, -2).split('.')
+    );
+  }
+
+  /**
+   * Infer the schema of the constant value
+   * @param value
+   * @returns
+   */
+  export function inferConstantJsonSchema(value: IFlowConstantValue): IJsonSchema | undefined {
+    if (value?.schema) {
+      return value.schema;
+    }
+
+    if (typeof value.content === 'string') {
+      return {
+        type: 'string',
+      };
+    }
+
+    if (typeof value.content === 'number') {
+      return {
+        type: 'number',
+      };
+    }
+
+    if (typeof value.content === 'boolean') {
+      return {
+        type: 'boolean',
+      };
+    }
+
+    if (isObject(value.content)) {
+      return {
+        type: 'object',
+      };
+    }
+    return undefined;
+  }
+
+  /**
+   * Infer the schema of the flow value
+   * @param values The flow value or object contains flow value
+   * @param scope
+   * @returns
+   */
+  export function inferJsonSchema(values: any, scope: Scope): IJsonSchema | undefined {
+    if (isObject(values)) {
+      if (isConstant(values)) {
+        return inferConstantJsonSchema(values);
+      }
+
+      if (isRef(values)) {
+        const variable = scope.available.getByKeyPath(values?.content);
+        const schema = variable?.type ? JsonSchemaUtils.astToSchema(variable?.type) : undefined;
+
+        return schema;
+      }
+
+      if (isTemplate(values)) {
+        return { type: 'string' };
+      }
+
+      return {
+        type: 'object',
+        properties: Object.keys(values).reduce((acc, key) => {
+          const schema = inferJsonSchema((values as any)[key], scope);
+          if (schema) {
+            acc[key] = schema;
+          }
+          return acc;
+        }, {} as Record<string, IJsonSchema>),
+      };
+    }
+  }
+}

+ 1 - 0
packages/materials/form-materials/src/shared/index.ts

@@ -5,3 +5,4 @@
 
 export * from './format-legacy-refs';
 export * from './inject-material';
+export * from './flow-value';

+ 2 - 0
packages/materials/form-materials/src/typings/flow-value/index.ts

@@ -9,6 +9,8 @@ export interface IFlowValueExtra {
   index?: number;
 }
 
+export type FlowValueType = 'constant' | 'ref' | 'expression' | 'template';
+
 export interface IFlowConstantValue {
   type: 'constant';
   content?: string | number | boolean;