Ver código fonte

feat(variable): remove sync-variable-plugin and use provide-json-schema-outputs effect (#400)

* feat(variable): provide json schema outputs effect

* fix(scope-chain): group variable chain

* fix: ts check

* test: variable layout
Yiwei Mao 6 meses atrás
pai
commit
7c43b0cfd0
24 arquivos alterados com 225 adições e 249 exclusões
  1. 1 6
      apps/demo-fixed-layout/src/hooks/use-editor-props.ts
  2. 7 1
      apps/demo-fixed-layout/src/nodes/default-form-meta.tsx
  3. 9 1
      apps/demo-fixed-layout/src/nodes/start/form-meta.tsx
  4. 0 1
      apps/demo-fixed-layout/src/plugins/index.ts
  5. 0 1
      apps/demo-fixed-layout/src/plugins/sync-variable-plugin/index.ts
  6. 0 78
      apps/demo-fixed-layout/src/plugins/sync-variable-plugin/sync-variable-plugin.ts
  7. 2 6
      apps/demo-free-layout/src/hooks/use-editor-props.tsx
  8. 7 1
      apps/demo-free-layout/src/nodes/default-form-meta.tsx
  9. 9 1
      apps/demo-free-layout/src/nodes/start/form-meta.tsx
  10. 0 1
      apps/demo-free-layout/src/plugins/index.ts
  11. 0 1
      apps/demo-free-layout/src/plugins/sync-variable-plugin/index.ts
  12. 0 78
      apps/demo-free-layout/src/plugins/sync-variable-plugin/sync-variable-plugin.ts
  13. 2 0
      packages/materials/form-materials/src/effects/index.ts
  14. 8 0
      packages/materials/form-materials/src/effects/provide-json-schema-outputs/config.json
  15. 23 0
      packages/materials/form-materials/src/effects/provide-json-schema-outputs/index.ts
  16. 5 0
      packages/materials/form-materials/src/effects/sync-variable-title/config.json
  17. 23 0
      packages/materials/form-materials/src/effects/sync-variable-title/index.ts
  18. 3 0
      packages/plugins/variable-plugin/src/create-variable-plugin.ts
  19. 3 0
      packages/variable-engine/variable-layout/__mocks__/container.ts
  20. 12 28
      packages/variable-engine/variable-layout/src/chains/fixed-layout-scope-chain.ts
  21. 32 33
      packages/variable-engine/variable-layout/src/chains/free-layout-scope-chain.ts
  22. 1 0
      packages/variable-engine/variable-layout/src/index.ts
  23. 71 0
      packages/variable-engine/variable-layout/src/services/scope-chain-transform-service.ts
  24. 7 12
      packages/variable-engine/variable-layout/src/variable-layout-config.ts

+ 1 - 6
apps/demo-fixed-layout/src/hooks/use-editor-props.ts

@@ -17,7 +17,7 @@ import { type FlowNodeRegistry } from '../typings';
 import { shortcutGetter } from '../shortcuts';
 import { shortcutGetter } from '../shortcuts';
 import { CustomService } from '../services';
 import { CustomService } from '../services';
 import { GroupBoxHeader, GroupNode } from '../plugins/group-plugin';
 import { GroupBoxHeader, GroupNode } from '../plugins/group-plugin';
-import { createSyncVariablePlugin, createClipboardPlugin } from '../plugins';
+import { createClipboardPlugin } from '../plugins';
 import { SelectorBoxPopover } from '../components/selector-box-popover';
 import { SelectorBoxPopover } from '../components/selector-box-popover';
 import NodeAdder from '../components/node-adder';
 import NodeAdder from '../components/node-adder';
 import BranchAdder from '../components/branch-adder';
 import BranchAdder from '../components/branch-adder';
@@ -254,11 +254,6 @@ export function useEditorProps(
             GroupNode,
             GroupNode,
           },
           },
         }),
         }),
-        /**
-         * Variable plugin
-         * 变量插件
-         */
-        createSyncVariablePlugin({}),
         /**
         /**
          * Clipboard plugin
          * Clipboard plugin
          * 剪切板插件
          * 剪切板插件

+ 7 - 1
apps/demo-fixed-layout/src/nodes/default-form-meta.tsx

@@ -1,4 +1,8 @@
-import { autoRenameRefEffect } from '@flowgram.ai/form-materials';
+import {
+  autoRenameRefEffect,
+  provideJsonSchemaOutputs,
+  syncVariableTitle,
+} from '@flowgram.ai/form-materials';
 import { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/fixed-layout-editor';
 import { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/fixed-layout-editor';
 
 
 import { FlowNodeJSON } from '../typings';
 import { FlowNodeJSON } from '../typings';
@@ -32,6 +36,8 @@ export const defaultFormMeta: FormMeta<FlowNodeJSON['data']> = {
     },
     },
   },
   },
   effect: {
   effect: {
+    title: syncVariableTitle,
+    outputs: provideJsonSchemaOutputs,
     inputsValues: autoRenameRefEffect,
     inputsValues: autoRenameRefEffect,
   },
   },
 };
 };

+ 9 - 1
apps/demo-fixed-layout/src/nodes/start/form-meta.tsx

@@ -1,4 +1,8 @@
-import { JsonSchemaEditor } from '@flowgram.ai/form-materials';
+import {
+  JsonSchemaEditor,
+  provideJsonSchemaOutputs,
+  syncVariableTitle,
+} from '@flowgram.ai/form-materials';
 import {
 import {
   Field,
   Field,
   FieldRenderProps,
   FieldRenderProps,
@@ -49,4 +53,8 @@ export const formMeta: FormMeta<FlowNodeJSON['data']> = {
   validate: {
   validate: {
     title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),
     title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),
   },
   },
+  effect: {
+    title: syncVariableTitle,
+    outputs: provideJsonSchemaOutputs,
+  },
 };
 };

+ 0 - 1
apps/demo-fixed-layout/src/plugins/index.ts

@@ -1,2 +1 @@
 export { createClipboardPlugin } from './clipboard-plugin/create-clipboard-plugin';
 export { createClipboardPlugin } from './clipboard-plugin/create-clipboard-plugin';
-export { createSyncVariablePlugin } from './sync-variable-plugin/sync-variable-plugin';

+ 0 - 1
apps/demo-fixed-layout/src/plugins/sync-variable-plugin/index.ts

@@ -1 +0,0 @@
-export * from './sync-variable-plugin';

+ 0 - 78
apps/demo-fixed-layout/src/plugins/sync-variable-plugin/sync-variable-plugin.ts

@@ -1,78 +0,0 @@
-import { JsonSchemaUtils } from '@flowgram.ai/form-materials';
-import {
-  definePluginCreator,
-  FlowNodeVariableData,
-  getNodeForm,
-  PluginCreator,
-  FixedLayoutPluginContext,
-  ASTFactory,
-} from '@flowgram.ai/fixed-layout-editor';
-
-export interface SyncVariablePluginOptions {}
-
-/**
- * Creates a plugin to synchronize output data to the variable engine when nodes are created or updated.
- * @param ctx - The plugin context, containing the document and other relevant information.
- * @param options - Plugin options, currently an empty object.
- */
-export const createSyncVariablePlugin: PluginCreator<SyncVariablePluginOptions> =
-  definePluginCreator<SyncVariablePluginOptions, FixedLayoutPluginContext>({
-    onInit(ctx, options) {
-      const flowDocument = ctx.document;
-
-      // Listen for node creation events
-      flowDocument.onNodeCreate(({ node }) => {
-        const form = getNodeForm(node);
-        const variableData = node.getData(FlowNodeVariableData);
-
-        /**
-         * Synchronizes output data to the variable engine.
-         * @param value - The output data to synchronize.
-         */
-        const syncOutputs = (value: any) => {
-          if (!value) {
-            // If the output data is empty, clear the variable
-            variableData.clearVar();
-            return;
-          }
-
-          // Create an Type AST from the output data's JSON schema
-          // NOTICE: You can create a new function to generate an AST based on YOUR CUSTOM DSL
-          const typeAST = JsonSchemaUtils.schemaToAST(value);
-
-          if (typeAST) {
-            // Use the node's title or its ID as the title for the variable
-            const title = form?.getValueIn('title') || node.id;
-
-            // Set the variable in the variable engine
-            variableData.setVar(
-              ASTFactory.createVariableDeclaration({
-                meta: {
-                  title: `${title}`,
-                  icon: node.getNodeRegistry()?.info?.icon,
-                  // NOTICE: You can add more metadata here as needed
-                },
-                key: `${node.id}`,
-                type: typeAST,
-              })
-            );
-          } else {
-            // If the AST cannot be created, clear the variable
-            variableData.clearVar();
-          }
-        };
-
-        if (form) {
-          // Initially synchronize the output data
-          syncOutputs(form.getValueIn('outputs'));
-
-          // Listen for changes in the form values and re-synchronize when outputs change
-          form.onFormValuesChange((props) => {
-            if (props.name.match(/^outputs/) || props.name.match(/^title/)) {
-              syncOutputs(form.getValueIn('outputs'));
-            }
-          });
-        }
-      });
-    },
-  });

+ 2 - 6
apps/demo-free-layout/src/hooks/use-editor-props.tsx

@@ -15,7 +15,7 @@ import { FlowNodeRegistry, FlowDocumentJSON } from '../typings';
 import { shortcuts } from '../shortcuts';
 import { shortcuts } from '../shortcuts';
 import { CustomService } from '../services';
 import { CustomService } from '../services';
 import { WorkflowRuntimeService } from '../plugins/runtime-plugin/runtime-service';
 import { WorkflowRuntimeService } from '../plugins/runtime-plugin/runtime-service';
-import { createSyncVariablePlugin, createRuntimePlugin, createContextMenuPlugin } from '../plugins';
+import { createRuntimePlugin, createContextMenuPlugin } from '../plugins';
 import { defaultFormMeta } from '../nodes/default-form-meta';
 import { defaultFormMeta } from '../nodes/default-form-meta';
 import { WorkflowNodeType } from '../nodes';
 import { WorkflowNodeType } from '../nodes';
 import { SelectorBoxPopover } from '../components/selector-box-popover';
 import { SelectorBoxPopover } from '../components/selector-box-popover';
@@ -237,11 +237,7 @@ export function useEditorProps(
           },
           },
           inactiveDebounceTime: 1,
           inactiveDebounceTime: 1,
         }),
         }),
-        /**
-         * Variable plugin
-         * 变量插件
-         */
-        createSyncVariablePlugin({}),
+
         /**
         /**
          * Snap plugin
          * Snap plugin
          * 自动对齐及辅助线插件
          * 自动对齐及辅助线插件

+ 7 - 1
apps/demo-free-layout/src/nodes/default-form-meta.tsx

@@ -1,5 +1,9 @@
 import { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/free-layout-editor';
 import { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/free-layout-editor';
-import { autoRenameRefEffect } from '@flowgram.ai/form-materials';
+import {
+  autoRenameRefEffect,
+  provideJsonSchemaOutputs,
+  syncVariableTitle,
+} from '@flowgram.ai/form-materials';
 
 
 import { FlowNodeJSON } from '../typings';
 import { FlowNodeJSON } from '../typings';
 import { FormHeader, FormContent, FormInputs, FormOutputs } from '../form-components';
 import { FormHeader, FormContent, FormInputs, FormOutputs } from '../form-components';
@@ -32,6 +36,8 @@ export const defaultFormMeta: FormMeta<FlowNodeJSON> = {
     },
     },
   },
   },
   effect: {
   effect: {
+    title: syncVariableTitle,
+    outputs: provideJsonSchemaOutputs,
     inputsValues: autoRenameRefEffect,
     inputsValues: autoRenameRefEffect,
   },
   },
 };
 };

+ 9 - 1
apps/demo-free-layout/src/nodes/start/form-meta.tsx

@@ -5,7 +5,11 @@ import {
   FormMeta,
   FormMeta,
   ValidateTrigger,
   ValidateTrigger,
 } from '@flowgram.ai/free-layout-editor';
 } from '@flowgram.ai/free-layout-editor';
-import { JsonSchemaEditor } from '@flowgram.ai/form-materials';
+import {
+  JsonSchemaEditor,
+  provideJsonSchemaOutputs,
+  syncVariableTitle,
+} from '@flowgram.ai/form-materials';
 
 
 import { FlowNodeJSON, JsonSchema } from '../../typings';
 import { FlowNodeJSON, JsonSchema } from '../../typings';
 import { useIsSidebar } from '../../hooks';
 import { useIsSidebar } from '../../hooks';
@@ -49,4 +53,8 @@ export const formMeta: FormMeta<FlowNodeJSON> = {
   validate: {
   validate: {
     title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),
     title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),
   },
   },
+  effect: {
+    title: syncVariableTitle,
+    outputs: provideJsonSchemaOutputs,
+  },
 };
 };

+ 0 - 1
apps/demo-free-layout/src/plugins/index.ts

@@ -1,3 +1,2 @@
-export { createSyncVariablePlugin } from './sync-variable-plugin/sync-variable-plugin';
 export { createContextMenuPlugin } from './context-menu-plugin';
 export { createContextMenuPlugin } from './context-menu-plugin';
 export { createRuntimePlugin } from './runtime-plugin';
 export { createRuntimePlugin } from './runtime-plugin';

+ 0 - 1
apps/demo-free-layout/src/plugins/sync-variable-plugin/index.ts

@@ -1 +0,0 @@
-export * from './sync-variable-plugin';

+ 0 - 78
apps/demo-free-layout/src/plugins/sync-variable-plugin/sync-variable-plugin.ts

@@ -1,78 +0,0 @@
-import {
-  definePluginCreator,
-  FlowNodeVariableData,
-  getNodeForm,
-  PluginCreator,
-  FreeLayoutPluginContext,
-  ASTFactory,
-} from '@flowgram.ai/free-layout-editor';
-import { JsonSchemaUtils } from '@flowgram.ai/form-materials';
-
-export interface SyncVariablePluginOptions {}
-
-/**
- * Creates a plugin to synchronize output data to the variable engine when nodes are created or updated.
- * @param ctx - The plugin context, containing the document and other relevant information.
- * @param options - Plugin options, currently an empty object.
- */
-export const createSyncVariablePlugin: PluginCreator<SyncVariablePluginOptions> =
-  definePluginCreator<SyncVariablePluginOptions, FreeLayoutPluginContext>({
-    onInit(ctx, options) {
-      const flowDocument = ctx.document;
-
-      // Listen for node creation events
-      flowDocument.onNodeCreate(({ node }) => {
-        const form = getNodeForm(node);
-        const variableData = node.getData(FlowNodeVariableData);
-
-        /**
-         * Synchronizes output data to the variable engine.
-         * @param value - The output data to synchronize.
-         */
-        const syncOutputs = (value: any) => {
-          if (!value) {
-            // If the output data is empty, clear the variable
-            variableData.clearVar();
-            return;
-          }
-
-          // Create an Type AST from the output data's JSON schema
-          // NOTICE: You can create a new function to generate an AST based on YOUR CUSTOM DSL
-          const typeAST = JsonSchemaUtils.schemaToAST(value);
-
-          if (typeAST) {
-            // Use the node's title or its ID as the title for the variable
-            const title = form?.getValueIn('title') || node.id;
-
-            // Set the variable in the variable engine
-            variableData.setVar(
-              ASTFactory.createVariableDeclaration({
-                meta: {
-                  title: `${title}`,
-                  icon: node.getNodeRegistry()?.info?.icon,
-                  // NOTICE: You can add more metadata here as needed
-                },
-                key: `${node.id}`,
-                type: typeAST,
-              })
-            );
-          } else {
-            // If the AST cannot be created, clear the variable
-            variableData.clearVar();
-          }
-        };
-
-        if (form) {
-          // Initially synchronize the output data
-          syncOutputs(form.getValueIn('outputs'));
-
-          // Listen for changes in the form values and re-synchronize when outputs change
-          form.onFormValuesChange((props) => {
-            if (props.name.match(/^outputs/) || props.name.match(/^title/)) {
-              syncOutputs(form.getValueIn('outputs'));
-            }
-          });
-        }
-      });
-    },
-  });

+ 2 - 0
packages/materials/form-materials/src/effects/index.ts

@@ -1,3 +1,5 @@
 export * from './provide-batch-input';
 export * from './provide-batch-input';
 export * from './provide-batch-outputs';
 export * from './provide-batch-outputs';
 export * from './auto-rename-ref';
 export * from './auto-rename-ref';
+export * from './provide-json-schema-outputs';
+export * from './sync-variable-title';

+ 8 - 0
packages/materials/form-materials/src/effects/provide-json-schema-outputs/config.json

@@ -0,0 +1,8 @@
+{
+  "name": "provide-json-schema-outputs",
+  "depMaterials": [
+    "typings/json-schema",
+    "utils/json-schema"
+  ],
+  "depPackages": []
+}

+ 23 - 0
packages/materials/form-materials/src/effects/provide-json-schema-outputs/index.ts

@@ -0,0 +1,23 @@
+import {
+  ASTFactory,
+  EffectOptions,
+  FlowNodeRegistry,
+  createEffectFromVariableProvider,
+  getNodeForm,
+} from '@flowgram.ai/editor';
+
+import { JsonSchemaUtils } from '../../utils';
+import { IJsonSchema } from '../../typings';
+
+export const provideJsonSchemaOutputs: EffectOptions[] = createEffectFromVariableProvider({
+  parse: (value: IJsonSchema, ctx) => [
+    ASTFactory.createVariableDeclaration({
+      key: `${ctx.node.id}`,
+      meta: {
+        title: getNodeForm(ctx.node)?.getValueIn('title') || ctx.node.id,
+        icon: ctx.node.getNodeRegistry<FlowNodeRegistry>().info?.icon,
+      },
+      type: JsonSchemaUtils.schemaToAST(value),
+    }),
+  ],
+});

+ 5 - 0
packages/materials/form-materials/src/effects/sync-variable-title/config.json

@@ -0,0 +1,5 @@
+{
+  "name": "sync-variable-title",
+  "depMaterials": [],
+  "depPackages": []
+}

+ 23 - 0
packages/materials/form-materials/src/effects/sync-variable-title/index.ts

@@ -0,0 +1,23 @@
+import {
+  DataEvent,
+  Effect,
+  EffectOptions,
+  FlowNodeRegistry,
+  FlowNodeVariableData,
+} from '@flowgram.ai/editor';
+
+export const syncVariableTitle: EffectOptions[] = [
+  {
+    event: DataEvent.onValueChange,
+    effect: (({ value, context }) => {
+      context.node.getData(FlowNodeVariableData).allScopes.forEach((_scope) => {
+        _scope.output.variables.forEach((_var) => {
+          _var.updateMeta({
+            title: value || context.node.id,
+            icon: context.node.getNodeRegistry<FlowNodeRegistry>().info?.icon,
+          });
+        });
+      });
+    }) as Effect,
+  },
+];

+ 3 - 0
packages/plugins/variable-plugin/src/create-variable-plugin.ts

@@ -4,6 +4,7 @@ import {
   FixedLayoutScopeChain,
   FixedLayoutScopeChain,
   VariableLayoutConfig,
   VariableLayoutConfig,
   bindGlobalScope,
   bindGlobalScope,
+  ScopeChainTransformService,
 } from '@flowgram.ai/variable-layout';
 } from '@flowgram.ai/variable-layout';
 import {
 import {
   VariableContainerModule,
   VariableContainerModule,
@@ -35,6 +36,8 @@ export const createVariablePlugin = definePluginCreator<VariablePluginOptions>({
   onBind({ bind }, opts) {
   onBind({ bind }, opts) {
     const { layout, layoutConfig } = opts;
     const { layout, layoutConfig } = opts;
 
 
+    bind(ScopeChainTransformService).toSelf().inSingletonScope();
+
     if (layout === 'free') {
     if (layout === 'free') {
       bind(ScopeChain).to(FreeLayoutScopeChain).inSingletonScope();
       bind(ScopeChain).to(FreeLayoutScopeChain).inSingletonScope();
     }
     }

+ 3 - 0
packages/variable-engine/variable-layout/__mocks__/container.ts

@@ -12,6 +12,7 @@ import {
   VariableLayoutConfig,
   VariableLayoutConfig,
   GlobalScope,
   GlobalScope,
   bindGlobalScope,
   bindGlobalScope,
+  ScopeChainTransformService,
 } from '../src';
 } from '../src';
 import { EntityManager } from '@flowgram.ai/core';
 import { EntityManager } from '@flowgram.ai/core';
 import { VariableEngine } from '@flowgram.ai/variable-core';
 import { VariableEngine } from '@flowgram.ai/variable-core';
@@ -48,6 +49,8 @@ export function getContainer(layout: 'free' | 'fixed', config?: TestConfig): Con
     container.bind(ScopeChain).to(FixedLayoutScopeChain).inSingletonScope();
     container.bind(ScopeChain).to(FixedLayoutScopeChain).inSingletonScope();
   }
   }
 
 
+  container.bind(ScopeChainTransformService).toSelf().inSingletonScope();
+
   bindGlobalScope(container.bind.bind(container))
   bindGlobalScope(container.bind.bind(container))
 
 
   const entityManager = container.get<EntityManager>(EntityManager);
   const entityManager = container.get<EntityManager>(EntityManager);

+ 12 - 28
packages/variable-engine/variable-layout/src/chains/fixed-layout-scope-chain.ts

@@ -5,6 +5,7 @@ import { FlowNodeEntity } from '@flowgram.ai/document';
 
 
 import { VariableLayoutConfig } from '../variable-layout-config';
 import { VariableLayoutConfig } from '../variable-layout-config';
 import { FlowNodeScope, FlowNodeScopeTypeEnum, ScopeChainNode } from '../types';
 import { FlowNodeScope, FlowNodeScopeTypeEnum, ScopeChainNode } from '../types';
+import { ScopeChainTransformService } from '../services/scope-chain-transform-service';
 import { GlobalScope } from '../scopes/global-scope';
 import { GlobalScope } from '../scopes/global-scope';
 import { FlowNodeVariableData } from '../flow-node-variable-data';
 import { FlowNodeVariableData } from '../flow-node-variable-data';
 
 
@@ -15,6 +16,9 @@ export class FixedLayoutScopeChain extends ScopeChain {
   // 增加  { id: string } 使得可以灵活添加自定义虚拟节点
   // 增加  { id: string } 使得可以灵活添加自定义虚拟节点
   tree: FlowVirtualTree<ScopeChainNode> | undefined;
   tree: FlowVirtualTree<ScopeChainNode> | undefined;
 
 
+  @inject(ScopeChainTransformService)
+  protected transformService: ScopeChainTransformService;
+
   constructor(
   constructor(
     @inject(FlowDocument)
     @inject(FlowDocument)
     protected flowDocument: FlowDocument,
     protected flowDocument: FlowDocument,
@@ -44,12 +48,12 @@ export class FixedLayoutScopeChain extends ScopeChain {
   // 获取依赖作用域
   // 获取依赖作用域
   getDeps(scope: FlowNodeScope): FlowNodeScope[] {
   getDeps(scope: FlowNodeScope): FlowNodeScope[] {
     if (!this.tree) {
     if (!this.tree) {
-      return this.transformDeps([], { scope });
+      return this.transformService.transformDeps([], { scope });
     }
     }
 
 
     const node = scope.meta.node;
     const node = scope.meta.node;
     if (!node) {
     if (!node) {
-      return this.transformDeps([], { scope });
+      return this.transformService.transformDeps([], { scope });
     }
     }
 
 
     const deps: FlowNodeScope[] = [];
     const deps: FlowNodeScope[] = [];
@@ -121,13 +125,13 @@ export class FixedLayoutScopeChain extends ScopeChain {
       deps.unshift(globalScope);
       deps.unshift(globalScope);
     }
     }
 
 
-    return this.transformDeps(deps, { scope });
+    return this.transformService.transformDeps(deps, { scope });
   }
   }
 
 
   // 获取覆盖作用域
   // 获取覆盖作用域
   getCovers(scope: FlowNodeScope): FlowNodeScope[] {
   getCovers(scope: FlowNodeScope): FlowNodeScope[] {
     if (!this.tree) {
     if (!this.tree) {
-      return this.transformCovers([], { scope });
+      return this.transformService.transformCovers([], { scope });
     }
     }
 
 
     // If scope is GlobalScope, return all scopes except GlobalScope
     // If scope is GlobalScope, return all scopes except GlobalScope
@@ -139,7 +143,7 @@ export class FixedLayoutScopeChain extends ScopeChain {
 
 
     const node = scope.meta.node;
     const node = scope.meta.node;
     if (!node) {
     if (!node) {
-      return this.transformCovers([], { scope });
+      return this.transformService.transformCovers([], { scope });
     }
     }
 
 
     const covers: FlowNodeScope[] = [];
     const covers: FlowNodeScope[] = [];
@@ -151,7 +155,7 @@ export class FixedLayoutScopeChain extends ScopeChain {
           addNodePrivateScope: true,
           addNodePrivateScope: true,
         })
         })
       );
       );
-      return this.transformCovers(covers, { scope });
+      return this.transformService.transformCovers(covers, { scope });
     }
     }
 
 
     let curr: ScopeChainNode | undefined = node;
     let curr: ScopeChainNode | undefined = node;
@@ -186,7 +190,7 @@ export class FixedLayoutScopeChain extends ScopeChain {
         while (currParent) {
         while (currParent) {
           // 私有作用域不能被后续节点访问
           // 私有作用域不能被后续节点访问
           if (this.isNodeChildrenPrivate(currParent)) {
           if (this.isNodeChildrenPrivate(currParent)) {
-            return this.transformCovers(covers, { scope });
+            return this.transformService.transformCovers(covers, { scope });
           }
           }
 
 
           // 当前 parent 有 next 节点,则停止向上查找
           // 当前 parent 有 next 节点,则停止向上查找
@@ -209,27 +213,7 @@ export class FixedLayoutScopeChain extends ScopeChain {
       curr = undefined;
       curr = undefined;
     }
     }
 
 
-    return this.transformCovers(covers, { scope });
-  }
-
-  protected transformCovers(covers: Scope[], { scope }: { scope: Scope }): Scope[] {
-    return this.configs?.transformCovers
-      ? this.configs.transformCovers(covers, {
-          scope,
-          document: this.flowDocument,
-          variableEngine: this.variableEngine,
-        })
-      : covers;
-  }
-
-  protected transformDeps(deps: Scope[], { scope }: { scope: Scope }): Scope[] {
-    return this.configs?.transformDeps
-      ? this.configs.transformDeps(deps, {
-          scope,
-          document: this.flowDocument,
-          variableEngine: this.variableEngine,
-        })
-      : deps;
+    return this.transformService.transformCovers(covers, { scope });
   }
   }
 
 
   // 排序所有作用域
   // 排序所有作用域

+ 32 - 33
packages/variable-engine/variable-layout/src/chains/free-layout-scope-chain.ts

@@ -1,11 +1,17 @@
 import { inject, optional, postConstruct } from 'inversify';
 import { inject, optional, postConstruct } from 'inversify';
 import { Scope, ScopeChain } from '@flowgram.ai/variable-core';
 import { Scope, ScopeChain } from '@flowgram.ai/variable-core';
 import { WorkflowNodeLinesData, WorkflowNodeMeta } from '@flowgram.ai/free-layout-core';
 import { WorkflowNodeLinesData, WorkflowNodeMeta } from '@flowgram.ai/free-layout-core';
-import { FlowNodeEntity, FlowDocument, FlowVirtualTree } from '@flowgram.ai/document';
+import {
+  FlowNodeEntity,
+  FlowDocument,
+  FlowVirtualTree,
+  FlowNodeBaseType,
+} from '@flowgram.ai/document';
 import { EntityManager } from '@flowgram.ai/core';
 import { EntityManager } from '@flowgram.ai/core';
 
 
 import { VariableLayoutConfig } from '../variable-layout-config';
 import { VariableLayoutConfig } from '../variable-layout-config';
 import { FlowNodeScope, FlowNodeScopeTypeEnum } from '../types';
 import { FlowNodeScope, FlowNodeScopeTypeEnum } from '../types';
+import { ScopeChainTransformService } from '../services/scope-chain-transform-service';
 import { GlobalScope } from '../scopes/global-scope';
 import { GlobalScope } from '../scopes/global-scope';
 import { FlowNodeVariableData } from '../flow-node-variable-data';
 import { FlowNodeVariableData } from '../flow-node-variable-data';
 
 
@@ -22,6 +28,9 @@ export class FreeLayoutScopeChain extends ScopeChain {
   @inject(VariableLayoutConfig)
   @inject(VariableLayoutConfig)
   protected configs?: VariableLayoutConfig;
   protected configs?: VariableLayoutConfig;
 
 
+  @inject(ScopeChainTransformService)
+  protected transformService: ScopeChainTransformService;
+
   get tree(): FlowVirtualTree<FlowNodeEntity> {
   get tree(): FlowVirtualTree<FlowNodeEntity> {
     return this.flowDocument.originTree;
     return this.flowDocument.originTree;
   }
   }
@@ -44,22 +53,26 @@ export class FreeLayoutScopeChain extends ScopeChain {
 
 
   // 获取同一层级所有输入节点
   // 获取同一层级所有输入节点
   protected getAllInputLayerNodes(curr: FlowNodeEntity): FlowNodeEntity[] {
   protected getAllInputLayerNodes(curr: FlowNodeEntity): FlowNodeEntity[] {
+    const currParent = this.getParent(curr);
+
     return (curr.getData(WorkflowNodeLinesData)?.allInputNodes || []).filter(
     return (curr.getData(WorkflowNodeLinesData)?.allInputNodes || []).filter(
-      (_node) => _node.parent === curr.parent
+      (_node) => this.getParent(_node) === currParent
     );
     );
   }
   }
 
 
   // 获取同一层级所有输出节点
   // 获取同一层级所有输出节点
   protected getAllOutputLayerNodes(curr: FlowNodeEntity): FlowNodeEntity[] {
   protected getAllOutputLayerNodes(curr: FlowNodeEntity): FlowNodeEntity[] {
+    const currParent = this.getParent(curr);
+
     return (curr.getData(WorkflowNodeLinesData)?.allOutputNodes || []).filter(
     return (curr.getData(WorkflowNodeLinesData)?.allOutputNodes || []).filter(
-      (_node) => _node.parent === curr.parent
+      (_node) => this.getParent(_node) === currParent
     );
     );
   }
   }
 
 
   getDeps(scope: FlowNodeScope): FlowNodeScope[] {
   getDeps(scope: FlowNodeScope): FlowNodeScope[] {
     const { node } = scope.meta || {};
     const { node } = scope.meta || {};
     if (!node) {
     if (!node) {
-      return this.transformDeps([], { scope });
+      return this.transformService.transformDeps([], { scope });
     }
     }
 
 
     const deps: FlowNodeScope[] = [];
     const deps: FlowNodeScope[] = [];
@@ -91,7 +104,7 @@ export class FreeLayoutScopeChain extends ScopeChain {
     }
     }
 
 
     const uniqDeps = Array.from(new Set(deps));
     const uniqDeps = Array.from(new Set(deps));
-    return this.transformDeps(uniqDeps, { scope });
+    return this.transformService.transformDeps(uniqDeps, { scope });
   }
   }
 
 
   getCovers(scope: FlowNodeScope): FlowNodeScope[] {
   getCovers(scope: FlowNodeScope): FlowNodeScope[] {
@@ -104,7 +117,7 @@ export class FreeLayoutScopeChain extends ScopeChain {
 
 
     const { node } = scope.meta || {};
     const { node } = scope.meta || {};
     if (!node) {
     if (!node) {
-      return this.transformCovers([], { scope });
+      return this.transformService.transformCovers([], { scope });
     }
     }
 
 
     const isPrivate = scope.meta.type === FlowNodeScopeTypeEnum.private;
     const isPrivate = scope.meta.type === FlowNodeScopeTypeEnum.private;
@@ -142,27 +155,7 @@ export class FreeLayoutScopeChain extends ScopeChain {
 
 
     const uniqScopes = Array.from(new Set(scopes));
     const uniqScopes = Array.from(new Set(scopes));
 
 
-    return this.transformCovers(uniqScopes, { scope });
-  }
-
-  protected transformCovers(covers: Scope[], { scope }: { scope: Scope }): Scope[] {
-    return this.configs?.transformCovers
-      ? this.configs.transformCovers(covers, {
-          scope,
-          document: this.flowDocument,
-          variableEngine: this.variableEngine,
-        })
-      : covers;
-  }
-
-  protected transformDeps(deps: Scope[], { scope }: { scope: Scope }): Scope[] {
-    return this.configs?.transformDeps
-      ? this.configs.transformDeps(deps, {
-          scope,
-          document: this.flowDocument,
-          variableEngine: this.variableEngine,
-        })
-      : deps;
+    return this.transformService.transformCovers(uniqScopes, { scope });
   }
   }
 
 
   getChildren(node: FlowNodeEntity): FlowNodeEntity[] {
   getChildren(node: FlowNodeEntity): FlowNodeEntity[] {
@@ -190,19 +183,25 @@ export class FreeLayoutScopeChain extends ScopeChain {
     if (this.configs?.getFreeParent) {
     if (this.configs?.getFreeParent) {
       return this.configs.getFreeParent(node);
       return this.configs.getFreeParent(node);
     }
     }
-    const initParent = node.document.originTree.getParent(node);
+    let parent = node.document.originTree.getParent(node);
+
+    // If currentParent is Group, get the parent of parent
+    while (parent?.flowNodeType === FlowNodeBaseType.GROUP) {
+      parent = parent.parent;
+    }
 
 
-    if (!initParent) {
-      return initParent;
+    if (!parent) {
+      return parent;
     }
     }
 
 
-    const nodeMeta = initParent.getNodeMeta<WorkflowNodeMeta>();
-    const subCanvas = nodeMeta.subCanvas?.(initParent);
+    const nodeMeta = parent.getNodeMeta<WorkflowNodeMeta>();
+    const subCanvas = nodeMeta.subCanvas?.(parent);
     if (subCanvas?.isCanvas) {
     if (subCanvas?.isCanvas) {
+      // Get real parent node by subCanvas Configuration
       return subCanvas.parentNode;
       return subCanvas.parentNode;
     }
     }
 
 
-    return initParent;
+    return parent;
   }
   }
 
 
   sortAll(): Scope[] {
   sortAll(): Scope[] {

+ 1 - 0
packages/variable-engine/variable-layout/src/index.ts

@@ -8,3 +8,4 @@ export {
   FlowNodeScopeTypeEnum as FlowNodeScopeType,
   FlowNodeScopeTypeEnum as FlowNodeScopeType,
 } from './types';
 } from './types';
 export { GlobalScope, bindGlobalScope } from './scopes/global-scope';
 export { GlobalScope, bindGlobalScope } from './scopes/global-scope';
+export { ScopeChainTransformService } from './services/scope-chain-transform-service';

+ 71 - 0
packages/variable-engine/variable-layout/src/services/scope-chain-transform-service.ts

@@ -0,0 +1,71 @@
+import { inject, injectable, optional } from 'inversify';
+import { Scope, VariableEngine } from '@flowgram.ai/variable-core';
+import { FlowDocument } from '@flowgram.ai/document';
+import { lazyInject } from '@flowgram.ai/core';
+
+import { VariableLayoutConfig } from '../variable-layout-config';
+import { FlowNodeScope } from '../types';
+
+export interface TransformerContext {
+  scope: FlowNodeScope;
+  document: FlowDocument;
+  variableEngine: VariableEngine;
+}
+
+export type IScopeTransformer = (scopes: Scope[], ctx: TransformerContext) => Scope[];
+
+@injectable()
+export class ScopeChainTransformService {
+  protected transformDepsFns: IScopeTransformer[] = [];
+
+  protected transformCoversFns: IScopeTransformer[] = [];
+
+  @lazyInject(FlowDocument) document: FlowDocument;
+
+  @lazyInject(VariableEngine) variableEngine: VariableEngine;
+
+  constructor(
+    @optional()
+    @inject(VariableLayoutConfig)
+    protected configs?: VariableLayoutConfig
+  ) {
+    if (this.configs?.transformDeps) {
+      this.transformDepsFns.push(this.configs.transformDeps);
+    }
+    if (this.configs?.transformCovers) {
+      this.transformCoversFns.push(this.configs.transformCovers);
+    }
+  }
+
+  registerTransformDeps(transformer: IScopeTransformer) {
+    this.transformDepsFns.push(transformer);
+  }
+
+  registerTransformCovers(transformer: IScopeTransformer) {
+    this.transformCoversFns.push(transformer);
+  }
+
+  transformDeps(scopes: Scope[], { scope }: { scope: Scope }): Scope[] {
+    return this.transformDepsFns.reduce(
+      (scopes, transformer) =>
+        transformer(scopes, {
+          scope,
+          document: this.document,
+          variableEngine: this.variableEngine,
+        }),
+      scopes
+    );
+  }
+
+  transformCovers(scopes: Scope[], { scope }: { scope: Scope }): Scope[] {
+    return this.transformCoversFns.reduce(
+      (scopes, transformer) =>
+        transformer(scopes, {
+          scope,
+          document: this.document,
+          variableEngine: this.variableEngine,
+        }),
+      scopes
+    );
+  }
+}

+ 7 - 12
packages/variable-engine/variable-layout/src/variable-layout-config.ts

@@ -1,14 +1,7 @@
-import { Scope } from '@flowgram.ai/variable-core';
-import { VariableEngine } from '@flowgram.ai/variable-core';
-import { FlowNodeEntity, FlowDocument } from '@flowgram.ai/document';
+import { FlowNodeEntity } from '@flowgram.ai/document';
 
 
-import { type FlowNodeScope, type ScopeChainNode } from './types';
-
-interface TransformerContext {
-  scope: FlowNodeScope;
-  document: FlowDocument;
-  variableEngine: VariableEngine;
-}
+import { type ScopeChainNode } from './types';
+import { IScopeTransformer } from './services/scope-chain-transform-service';
 
 
 export interface VariableLayoutConfig {
 export interface VariableLayoutConfig {
   /**
   /**
@@ -25,14 +18,16 @@ export interface VariableLayoutConfig {
   getFreeParent?: (node: FlowNodeEntity) => FlowNodeEntity | undefined;
   getFreeParent?: (node: FlowNodeEntity) => FlowNodeEntity | undefined;
 
 
   /**
   /**
+   * @deprecated
    * 对依赖作用域进行微调
    * 对依赖作用域进行微调
    */
    */
-  transformDeps?: (scopes: Scope[], ctx: TransformerContext) => Scope[];
+  transformDeps?: IScopeTransformer;
 
 
   /**
   /**
+   * @deprecated
    * 对依赖作用域进行微调
    * 对依赖作用域进行微调
    */
    */
-  transformCovers?: (scopes: Scope[], ctx: TransformerContext) => Scope[];
+  transformCovers?: IScopeTransformer;
 }
 }
 
 
 export const VariableLayoutConfig = Symbol('VariableLayoutConfig');
 export const VariableLayoutConfig = Symbol('VariableLayoutConfig');