feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { PrivateScopeProvider } from './private-scope-provider';
export { PublicScopeProvider } from './public-scope-provider';

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useMemo, type ReactElement } from 'react';
import {
FlowNodeVariableData,
type Scope,
ScopeProvider,
} from '@flowgram-adapter/free-layout-editor';
import { useEntityFromContext } from '@flowgram-adapter/free-layout-editor';
interface VariableProviderProps {
children: ReactElement | ReactElement[];
}
export const PrivateScopeProvider = ({ children }: VariableProviderProps) => {
const node = useEntityFromContext();
const privateScope: Scope = useMemo(() => {
const variableData: FlowNodeVariableData =
node.getData(FlowNodeVariableData);
if (!variableData.private) {
variableData.initPrivate();
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return variableData.private!;
}, [node]);
return (
<ScopeProvider value={{ scope: privateScope }}>{children}</ScopeProvider>
);
};

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useMemo } from 'react';
import {
FlowNodeVariableData,
type Scope,
ScopeProvider,
} from '@flowgram-adapter/free-layout-editor';
import { useEntityFromContext } from '@flowgram-adapter/free-layout-editor';
interface VariableProviderProps {
children: React.ReactElement;
}
export const PublicScopeProvider = ({ children }: VariableProviderProps) => {
const node = useEntityFromContext();
const publicScope: Scope = useMemo(
() => node.getData(FlowNodeVariableData).public,
[node],
);
return (
<ScopeProvider value={{ scope: publicScope }}>{children}</ScopeProvider>
);
};

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { startTransition, useEffect } from 'react';
import { VariableEngine } from '@flowgram-adapter/free-layout-editor';
import { FlowNodeFormData } from '@flowgram-adapter/free-layout-editor';
import { useRefresh, useService } from '@flowgram-adapter/free-layout-editor';
import { Tag, Tree } from '@douyinfe/semi-ui';
import { ViewVariableType } from '@coze-workflow/base/types';
import { getViewVariableTypeByAST } from '../../core/utils/parse-ast';
import { type WorkflowVariableField } from '../../core/types';
import { isGlobalVariableKey } from '../../constants';
const getRootFieldTitle = (field: WorkflowVariableField) => {
if (isGlobalVariableKey(field.key)) {
return 'Global';
}
return field.scope.meta?.node
?.getData(FlowNodeFormData)
?.formModel?.getFormItemValueByPath('/nodeMeta')?.title;
};
const getTreeDataByField = (field: WorkflowVariableField, path = '/') => {
const { type, childFields } = getViewVariableTypeByAST(field.type);
const currTreeKey = `${path}${field.key}/`;
const isRoot = path === '/';
const tag = isRoot
? getRootFieldTitle(field)
: type && ViewVariableType.getLabel(type);
return {
key: currTreeKey,
label: (
<>
{field.key}
<Tag
style={{ marginLeft: 5 }}
size="small"
color={isRoot ? 'violet' : 'light-blue'}
>
{tag}
</Tag>
</>
),
children: childFields?.map(_child =>
getTreeDataByField(_child, currTreeKey),
),
};
};
export const VariableDebugPanel = (): JSX.Element => {
const variableEngine: VariableEngine = useService(VariableEngine);
const refresh = useRefresh();
const { variables } = variableEngine.globalVariableTable;
const treeData = variables.map(_v => getTreeDataByField(_v));
useEffect(() => {
const subscription = variableEngine.globalEvent$.subscribe(_v => {
startTransition(() => {
refresh();
});
});
return () => subscription.unsubscribe();
}, []);
return (
<div style={{ minWidth: 350, maxHeight: 500, overflowY: 'auto' }}>
<p>Debug panel for variable, only visible in BOE. </p>
<Tree showLine={true} treeData={treeData} autoExpandParent />
</div>
);
};

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Layer } from '@flowgram-adapter/free-layout-editor';
import { Collapse } from '@douyinfe/semi-ui';
import { VariableDebugPanel } from './content';
export class VariableDebugLayer extends Layer {
render(): JSX.Element {
return (
<div
style={{
position: 'fixed',
right: 50,
top: 100,
background: '#fff',
borderRadius: 5,
boxShadow: '0px 2px 4px 0px rgba(0, 0, 0, 0.1)',
zIndex: 999,
}}
>
<Collapse>
<Collapse.Panel header="Variable (Debug)" itemKey="1">
<VariableDebugPanel />
</Collapse.Panel>
</Collapse>
</div>
);
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
export const GLOBAL_VARIABLE_SCOPE_ID = 'globalVariableScope';
export const WORKFLOW_VARIABLE_SOURCE = 'block-output_';
export const TRANS_WORKFLOW_VARIABLE_SOURCE = 'block_output_';
export enum GlobalVariableKey {
System = 'global_variable_system',
User = 'global_variable_user',
App = 'global_variable_app',
}
export const allGlobalVariableKeys = [
GlobalVariableKey.System,
GlobalVariableKey.User,
GlobalVariableKey.App,
];
export const GLOBAL_VAR_ALIAS_MAP: Record<string, string> = {
[GlobalVariableKey.App]: I18n.t('variable_app_name'),
[GlobalVariableKey.User]: I18n.t('variable_user_name'),
[GlobalVariableKey.System]: I18n.t('variable_system_name'),
};
export const isGlobalVariableKey = (key: string) =>
allGlobalVariableKeys.includes(key as GlobalVariableKey);
export const getGlobalVariableAlias = (key = '') =>
isGlobalVariableKey(key) ? GLOBAL_VAR_ALIAS_MAP[key] : undefined;

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ArrayType,
type BaseVariableField,
} from '@flowgram-adapter/free-layout-editor';
export class CustomArrayType extends ArrayType {
getByKeyPath(keyPath: string[]): BaseVariableField<unknown> | undefined {
// const [curr, ...rest] = keyPath || [];
// if (curr === '0' && this.canDrilldownItems) {
// // 数组第 0 项
// return this.items.getByKeyPath(rest);
// }
if (this.canDrilldownItems) {
// Coze 中兜底为第 0 项
return this.items.getByKeyPath(keyPath);
}
return;
}
}

View File

@@ -0,0 +1,182 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/naming-convention */
import {
type BaseType,
KeyPathExpression,
type CreateASTParams,
type VariableFieldKeyRenameService,
type BaseVariableField,
type ASTNodeJSON,
ASTKind,
type KeyPathExpressionJSON,
} from '@flowgram-adapter/free-layout-editor';
import { Disposable } from '@flowgram-adapter/common';
import { getViewVariableTypeByAST } from '../utils/parse-ast';
import { getByNamePath } from '../utils/name-path';
import { checkRefCycle, getParentFields } from '../utils/expression';
import { createASTFromType } from '../utils/create-ast';
import { type WorkflowVariableField } from '../types';
import { ViewVariableType } from '../../typings';
export interface RefExpressionJSON extends KeyPathExpressionJSON {
rawMeta?: {
type?: ViewVariableType;
};
}
/**
* 业务重新定义 KeyPath 的实现方式
*/
export class CustomKeyPathExpression extends KeyPathExpression<RefExpressionJSON> {
get renameService(): VariableFieldKeyRenameService {
return this.opts.renameService;
}
_rawMeta?: RefExpressionJSON['rawMeta'];
/**
* 重载 getRefFields 方法
* @returns
*/
getRefFields(): WorkflowVariableField[] {
const ref = getByNamePath(this._keyPath, {
variableEngine: this.scope.variableEngine,
node: this.scope.meta?.node,
});
// 刷新引用时,检测循环引用,如果存在循环引用则不引用该变量
if (checkRefCycle(this, [ref])) {
// 提示存在循环引用
console.warn(
'[CustomKeyPathExpression] checkRefCycle: Reference Cycle Existed',
getParentFields(this)
.map(_field => _field.key)
.reverse(),
);
return [];
}
return ref ? [ref] : [];
}
fromJSON(json: RefExpressionJSON): void {
if (json.rawMeta?.type !== this._rawMeta?.type) {
this._rawMeta = json.rawMeta;
this.refreshReturnType();
this.fireChange();
}
super.fromJSON(json);
}
// 直接生成新的 returnType 节点而不是直接复用
// 确保不同的 keyPath 不指向同一个 Field
_returnType: BaseType;
get returnType() {
return this._returnType;
}
getReturnTypeJSONByRef(
_ref: BaseVariableField | undefined,
): ASTNodeJSON | undefined {
return _ref?.type?.toJSON();
}
refreshReturnType() {
const [ref] = this._refs;
const updateTypeByRef = () => {
if (this.prevRefTypeHash !== ref?.type?.hash) {
this.prevRefTypeHash = ref?.type?.hash;
this.updateChildNodeByKey(
'_returnType',
this.getReturnTypeJSONByRef(ref),
);
}
};
if (this._rawMeta?.type) {
const shouldUseRawMeta =
// 1. 没有引用变量时,使用 rawMeta 的类型
!ref ||
// 2. 非 Object 和 Array<Object>,使用 rawMeta 的类型
!ViewVariableType.canDrilldown(this._rawMeta.type) ||
// 3. 如果是可下钻的类型,需要判断引用的变量类型和 rawMeta 的类型是否一致,不一致时使用 rawMeta 的数据
getViewVariableTypeByAST(ref.type).type !== this._rawMeta.type;
if (shouldUseRawMeta) {
this.updateChildNodeByKey(
'_returnType',
createASTFromType(this._rawMeta.type),
);
return;
}
}
updateTypeByRef();
}
toJSON(): ASTNodeJSON {
return {
kind: this.kind,
keyPath: this._keyPath,
rawMeta: this._rawMeta,
};
}
protected prevRefTypeHash: string | undefined;
constructor(
params: CreateASTParams,
opts: { renameService: VariableFieldKeyRenameService },
) {
super(params, opts);
// do nothing
const subscription = this.refs$.subscribe(_type => {
this.refreshReturnType();
});
this.toDispose.pushAll([
Disposable.create(() => subscription.unsubscribe()),
// 当前引用被 rename 时,刷新一下引用
this.renameService.onRename(({ before, after }) => {
const field = this.refs?.[0];
if (!field) {
return;
}
const allFields = field.parentFields.reverse().concat(field);
const changedIndex = allFields.indexOf(before);
if (changedIndex >= 0) {
this._keyPath[changedIndex] = after.key;
this.refreshRefs();
}
}),
]);
}
}
export const createRefExpression = (json: RefExpressionJSON) => ({
kind: ASTKind.KeyPathExpression,
...json,
});

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
type ASTNodeJSON,
BaseType,
} from '@flowgram-adapter/free-layout-editor';
import { type ViewVariableType } from '@coze-workflow/base/types';
import { ExtendASTKind } from '../types';
interface ExtendBaseTypeJSON {
type: ViewVariableType;
}
export class ExtendBaseType extends BaseType<ExtendBaseTypeJSON> {
static kind: string = ExtendASTKind.ExtendBaseType;
type: ViewVariableType;
fromJSON(json: ExtendBaseTypeJSON): void {
if (this.extendType !== json.type) {
this.type = json.type;
this.fireChange();
}
// do nothing
}
toJSON(): ExtendBaseTypeJSON & { kind: string } {
return {
kind: ExtendASTKind.ExtendBaseType,
type: this.type,
};
}
public isTypeEqual(targetTypeJSON: ASTNodeJSON | undefined): boolean {
const isSuperEqual = super.isTypeEqual(targetTypeJSON);
return (
isSuperEqual && this.type === (targetTypeJSON as ExtendBaseTypeJSON)?.type
);
}
}
export const createExtendBaseType = (json: ExtendBaseTypeJSON) => ({
kind: ExtendASTKind.ExtendBaseType,
...json,
});

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
VariableFieldKeyRenameService,
type VariablePluginOptions,
} from '@flowgram-adapter/free-layout-editor';
import { WorkflowVariableFacadeService } from '../workflow-variable-facade-service';
import { WrapArrayExpression } from './wrap-array-expression';
import { MergeGroupExpression } from './merge-group-expression';
import { ExtendBaseType } from './extend-base-type';
import { CustomKeyPathExpression } from './custom-key-path-expression';
import { CustomArrayType } from './custom-array-type';
export const extendASTNodes: VariablePluginOptions['extendASTNodes'] = [
[
CustomKeyPathExpression,
ctx => ({
facadeService: ctx.get(WorkflowVariableFacadeService),
renameService: ctx.get(VariableFieldKeyRenameService),
}),
],
[
WrapArrayExpression,
ctx => ({
facadeService: ctx.get(WorkflowVariableFacadeService),
renameService: ctx.get(VariableFieldKeyRenameService),
}),
],
CustomArrayType,
ExtendBaseType,
MergeGroupExpression,
];

View File

@@ -0,0 +1,165 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/naming-convention */
import {
ASTKind,
type ASTNodeJSON,
BaseExpression,
type BaseType,
type BaseVariableField,
postConstructAST,
} from '@flowgram-adapter/free-layout-editor';
export enum MergeStrategy {
FirstNotEmpty = 'FirstNotEmpty',
}
export interface MergeGroupExpressionJSON {
mergeStrategy?: MergeStrategy;
expressions?: ASTNodeJSON[];
}
export class MergeGroupExpression extends BaseExpression {
static kind = 'MergeGroupExpression';
protected _mergeStrategy: MergeStrategy = MergeStrategy.FirstNotEmpty;
protected _expressions: BaseExpression[] = [];
protected _returnType: BaseType | undefined;
protected _error: string | false = false;
get error(): string | false {
return this._error;
}
get returnType(): BaseType | undefined {
return this._returnType;
}
getRefFields(): (BaseVariableField | undefined)[] {
return [];
}
fromJSON(json: MergeGroupExpressionJSON): void {
const {
// 默认使用“首个非空”策略
mergeStrategy = MergeStrategy.FirstNotEmpty,
expressions = [],
} = json || {};
if (mergeStrategy !== this._mergeStrategy) {
this._mergeStrategy = mergeStrategy;
this.fireChange();
}
// 超出长度的 expressions 需要被销毁
this._expressions.slice(expressions.length).forEach(_item => {
_item.dispose();
this.fireChange();
});
// 剩余 expressions 的处理
this._expressions = expressions.map((_item, idx) => {
const prevItem = this._expressions[idx];
if (prevItem?.kind !== _item.kind) {
prevItem?.dispose();
this.fireChange();
return this.createChildNode(_item);
}
prevItem.fromJSON(_item);
return prevItem;
});
}
// 获取聚合变量的类型
protected syncReturnType(): ASTNodeJSON | undefined {
if (this._mergeStrategy === MergeStrategy.FirstNotEmpty) {
const nextTypeJSON = this._expressions[0]?.returnType?.toJSON();
if (!nextTypeJSON?.kind) {
return;
}
const nextWeakTypeJSON = this.getWeakTypeJSON(nextTypeJSON);
for (const _expr of this._expressions.slice(1)) {
const _returnType = _expr.returnType;
// 该引用没有类型,则聚合类型为首个变量类型不下钻
if (!_returnType) {
return nextWeakTypeJSON;
}
// 该引用和第一个变量没有强一致,则聚合类型为首个变量类型不下钻
if (!_returnType.isTypeEqual(nextTypeJSON)) {
return nextWeakTypeJSON;
}
}
return nextTypeJSON;
}
return;
}
getWeakTypeJSON(fullType?: ASTNodeJSON | undefined) {
if (fullType?.kind === ASTKind.Object) {
// Object 不下钻
return { kind: ASTKind.Object, weak: true };
}
if (fullType?.kind === ASTKind.Array) {
return { ...fullType, items: this.getWeakTypeJSON(fullType.items) };
}
return fullType;
}
@postConstructAST()
init() {
this.toDispose.pushAll([
this.subscribe(
() => {
this.updateChildNodeByKey('_returnType', this.syncReturnType());
},
{
triggerOnInit: true,
selector: curr => [
curr._mergeStrategy,
// 表达式 hash 是否发生变更
...curr._expressions.map(_expr => _expr.hash),
],
},
),
]);
}
toJSON() {
return {
kind: this.kind,
mergeStrategy: this._mergeStrategy,
expressions: this._expressions.map(_expr => _expr.toJSON()),
};
}
}
export const createMergeGroupExpression = (json: MergeGroupExpressionJSON) => ({
kind: MergeGroupExpression.kind,
...json,
});

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ASTFactory,
type ASTNodeJSON,
type BaseVariableField,
} from '@flowgram-adapter/free-layout-editor';
import {
CustomKeyPathExpression,
type RefExpressionJSON,
} from './custom-key-path-expression';
/**
* 遍历表达式,对列表进行遍历,获取遍历后的变量类型
*/
export class WrapArrayExpression extends CustomKeyPathExpression {
static kind = 'WrapArrayExpression';
getReturnTypeJSONByRef(
_ref: BaseVariableField | undefined,
): ASTNodeJSON | undefined {
return ASTFactory.createArray({
items: _ref?.type?.toJSON(),
});
}
toJSON() {
return {
kind: this.kind,
keyPath: this._keyPath,
rawMeta: this._rawMeta,
};
}
}
export const createWrapArrayExpression = ({
keyPath,
rawMeta,
}: RefExpressionJSON) => ({
kind: WrapArrayExpression.kind,
keyPath,
rawMeta,
});

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { extendASTNodes } from './extend-ast';
export {
parseNodeOutputByViewVariableMeta,
parseNodeBatchByInputList,
} from './utils/create-ast';
export { WorkflowVariableFacadeService } from './workflow-variable-facade-service';
// 重命名为 WorkflowVariable便于业务理解
export { WorkflowVariableFacade as WorkflowVariable } from './workflow-variable-facade';

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type BaseVariableField } from '@flowgram-adapter/free-layout-editor';
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import { type ViewVariableMeta } from '@coze-workflow/base/types';
export enum ExtendASTKind {
Image = 'Image',
File = 'File',
ExtendBaseType = 'ExtendBaseType',
MergeGroupExpression = 'MergeGroupExpression',
SyncBackOutputs = 'SyncBackOutputs',
}
export type WorkflowVariableField = BaseVariableField<
Partial<ViewVariableMeta>
>;
export interface RenameInfo {
prevKeyPath: string[];
nextKeyPath: string[];
// rename 的位置,及对应的 key 值
modifyIndex: number;
modifyKey: string;
}
export interface GetKeyPathCtx {
// 当前所在的节点
node?: FlowNodeEntity;
// 验证变量是否在作用域内
checkScope?: boolean;
}

View File

@@ -0,0 +1,171 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { isArray, uniqBy } from 'lodash-es';
import {
ASTFactory,
type PropertyJSON,
type ASTNodeJSON,
type VariableDeclarationJSON,
} from '@flowgram-adapter/free-layout-editor';
import {
type ViewVariableMeta,
ViewVariableType,
type ViewVariableTreeNode,
type BatchVOInputList,
type RefExpression,
} from '@coze-workflow/base/types';
import { createExtendBaseType } from '../extend-ast/extend-base-type';
import { createRefExpression } from '../extend-ast/custom-key-path-expression';
/**
* ViewVariableType 转 AST
* @param type 类型
* @param properties 下钻字段
* @returns
*/
export const createASTFromType = (
type: ViewVariableType,
// 下钻字段
properties?: PropertyJSON[],
): ASTNodeJSON | undefined => {
if (ViewVariableType.isArrayType(type)) {
return ASTFactory.createArray({
items: createASTFromType(
ViewVariableType.getArraySubType(type),
properties,
),
});
}
switch (type) {
case ViewVariableType.Boolean:
return ASTFactory.createBoolean();
case ViewVariableType.String:
return ASTFactory.createString();
case ViewVariableType.Number:
return ASTFactory.createNumber();
case ViewVariableType.Integer:
return ASTFactory.createInteger();
case ViewVariableType.Object:
return ASTFactory.createObject({
properties,
});
default:
// 其余扩展的基础类型
return createExtendBaseType({ type });
}
};
/**
* ViewVariableTreeNode 转属性
* @param treeNode
* @returns
*/
export const createASTPropertyFromViewVariable = (
treeNode: ViewVariableTreeNode,
): PropertyJSON | undefined => {
if (!treeNode?.name) {
return;
}
const drilldownProperties = uniqBy(
treeNode.children || [],
_child => _child?.name,
)
.filter(_child => _child && _child?.name)
?.map(createASTPropertyFromViewVariable)
.filter(Boolean) as PropertyJSON[];
return ASTFactory.createProperty({
key: treeNode.name,
meta: treeNode,
type: createASTFromType(treeNode.type, drilldownProperties),
});
};
/**
* 节点输出变量生成
* @param rootKey
* @param variables
* @returns
*/
export const parseNodeOutputByViewVariableMeta = (
nodeId: string,
value: ViewVariableMeta | ViewVariableMeta[],
): VariableDeclarationJSON[] => {
const list = uniqBy(
isArray(value) ? value : [value],
_child => _child?.name,
// Preset 变量没有开启 enable 时不生成变量
).filter(v => v && v.name && !(v.isPreset && !v.enabled));
if (list.length > 0) {
return [
ASTFactory.createVariableDeclaration({
key: `${nodeId}.outputs`,
type: ASTFactory.createObject({
properties: list
.map(createASTPropertyFromViewVariable)
.filter(Boolean) as PropertyJSON[],
}),
}),
];
}
return [];
};
/**
* Batch 输出变量生成
* @param rootKey
* @param inputList
* @returns
*/
export const parseNodeBatchByInputList = (
nodeId: string,
inputList: BatchVOInputList[] = [],
): VariableDeclarationJSON[] => {
const list = uniqBy(
inputList.filter(_input => _input && _input?.name),
_child => _child?.name,
);
if (list.length > 0) {
return [
ASTFactory.createVariableDeclaration({
key: `${nodeId}.locals`,
type: ASTFactory.createObject({
properties: list.map(_input =>
ASTFactory.createProperty({
key: _input?.name,
initializer: ASTFactory.createEnumerateExpression({
enumerateFor: createRefExpression({
keyPath:
(_input?.input as RefExpression)?.content?.keyPath || [],
rawMeta: _input?.input?.rawMeta,
}),
}),
}),
),
}),
}),
];
}
return [];
};

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { intersection } from 'lodash-es';
import {
type ASTNode,
type BaseVariableField,
type BaseExpression,
ASTNodeFlags,
} from '@flowgram-adapter/free-layout-editor';
// 获取所有的子 AST 节点
export function getAllChildren(ast: ASTNode): ASTNode[] {
return [
...ast.children,
...ast.children.map(_child => getAllChildren(_child)).flat(),
];
}
// 获取父 Fields
export function getParentFields(ast: ASTNode): BaseVariableField[] {
let curr = ast.parent;
const res: BaseVariableField[] = [];
while (curr) {
if (curr.flags & ASTNodeFlags.VariableField) {
res.push(curr as BaseVariableField);
}
curr = curr.parent;
}
return res;
}
// 获取所有子 AST 引用的变量
export function getAllRefs(ast: ASTNode): BaseVariableField[] {
return getAllChildren(ast)
.filter(_child => _child.flags & ASTNodeFlags.Expression)
.map(_child => (_child as BaseExpression).refs)
.flat()
.filter(Boolean) as BaseVariableField[];
}
/**
* 检测是否成环
* @param curr 当前表达式
* @param refNode 引用的变量节点
* @returns 是否成环
*/
export function checkRefCycle(
curr: BaseExpression,
refNodes: (BaseVariableField | undefined)[],
): boolean {
// 作用域没有成环,则不可能成环
if (
intersection(
curr.scope.coverScopes,
refNodes.map(_ref => _ref?.scope).filter(Boolean),
).length === 0
) {
return false;
}
// BFS 遍历
const visited = new Set<BaseVariableField>();
const queue = [...refNodes];
while (queue.length) {
const currNode = queue.shift();
if (!currNode) {
continue;
}
visited.add(currNode);
for (const ref of getAllRefs(currNode).filter(_ref => !visited.has(_ref))) {
queue.push(ref);
}
}
// 引用的变量中,包含表达式的父变量,则成环
return intersection(Array.from(visited), getParentFields(curr)).length > 0;
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable complexity */
/* eslint-disable security/detect-object-injection */
import { uniq } from 'lodash-es';
import {
FlowNodeVariableData,
type VariableEngine,
} from '@flowgram-adapter/free-layout-editor';
import { type GetKeyPathCtx, type WorkflowVariableField } from '../types';
import { isGlobalVariableKey } from '../../constants';
export function getByNamePath(
namePath: string[],
{
variableEngine,
node,
checkScope,
}: { variableEngine: VariableEngine } & GetKeyPathCtx,
): WorkflowVariableField | undefined {
const nodeId = namePath[0];
if (isGlobalVariableKey(nodeId)) {
return variableEngine.globalVariableTable.getByKeyPath(namePath);
}
const subPath = namePath.slice(1);
const nodeDepScopes = uniq([
...(node?.getData(FlowNodeVariableData)?.public?.depScopes || []),
...(node?.getData(FlowNodeVariableData)?.private?.depScopes || []),
]);
// 节点的依赖作用域中是否存在 nodeId 的 private
if (nodeDepScopes?.find(_scope => _scope.id === `${nodeId}_private`)) {
return variableEngine.globalVariableTable.getByKeyPath([
`${nodeId}.locals`,
...subPath,
]);
}
// 节点的依赖作用域是否存在 nodeId 的 public
if (nodeDepScopes?.find(_scope => _scope.id === `${nodeId}`)) {
return variableEngine.globalVariableTable.getByKeyPath([
`${nodeId}.outputs`,
...subPath,
]);
}
// 如果业务验证是否在作用域内,不在作用域内直接返回结果
if (checkScope) {
return;
}
return (
variableEngine.globalVariableTable.getByKeyPath([
`${nodeId}.outputs`,
...subPath,
]) ||
variableEngine.globalVariableTable.getByKeyPath([
`${nodeId}.locals`,
...subPath,
])
);
}
export function getNamePathByField(field: WorkflowVariableField) {
return field.parentFields
.reverse()
.map((_field, idx) => {
if (idx === 0) {
return _field.key.split('.')[0];
}
return _field.key;
})
.concat([field.key]);
}
export function matchPath(target: string[], source: string[]) {
return target.every((_path, idx) => _path === source[idx]);
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable no-case-declarations */
import {
ASTKind,
type ObjectType,
type ArrayType,
type BaseType,
} from '@flowgram-adapter/free-layout-editor';
import {
type ViewVariableTreeNode,
ViewVariableType,
type ViewVariableMeta,
} from '@coze-workflow/base/types';
import { ExtendASTKind, type WorkflowVariableField } from '../types';
import { type ExtendBaseType } from '../extend-ast/extend-base-type';
export const getViewVariableTypeByAST = (
ast: BaseType,
): { type?: ViewVariableType; childFields?: WorkflowVariableField[] } => {
switch (ast?.kind) {
case ASTKind.Array:
const { type, childFields } = getViewVariableTypeByAST(
(ast as ArrayType).items,
);
return {
type:
// 暂时不支持二维数组
type && !ViewVariableType.isArrayType(type)
? ViewVariableType.wrapToArrayType(type)
: type,
childFields,
};
case ASTKind.Object:
return {
type: ViewVariableType.Object,
childFields: (ast as ObjectType).properties,
};
case ASTKind.String:
return { type: ViewVariableType.String };
case ASTKind.Number:
return { type: ViewVariableType.Number };
case ASTKind.Boolean:
return { type: ViewVariableType.Boolean };
case ASTKind.Integer:
return { type: ViewVariableType.Integer };
case ExtendASTKind.ExtendBaseType:
return { type: (ast as ExtendBaseType).type };
default:
break;
}
return {};
};
export const getViewVariableByField = (
field: WorkflowVariableField,
): ViewVariableMeta | undefined => {
const { type, childFields } = getViewVariableTypeByAST(field.type);
if (!type) {
return undefined;
}
return {
...field.meta,
type,
name: field.key,
key: field.key,
children: childFields
?.map(getViewVariableByField)
.filter(Boolean) as ViewVariableTreeNode[],
};
};
export const getViewVariableTWithUniqKey = (
viewMeta: ViewVariableMeta | undefined,
parentKeyPath?: string,
): ViewVariableMeta | undefined => {
if (!viewMeta) {
return viewMeta;
}
const currKey = parentKeyPath
? `${parentKeyPath}.${viewMeta.key}`
: `${viewMeta.key}`;
return {
...viewMeta,
key: currKey,
children: viewMeta.children
?.map(_child => getViewVariableTWithUniqKey(_child, currKey))
.filter(Boolean) as ViewVariableMeta[],
};
};

View File

@@ -0,0 +1,123 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { z, type ZodSchema } from 'zod';
import { isArray, isObject } from 'lodash-es';
import { type BaseVariableField } from '@flowgram-adapter/free-layout-editor';
import {
type FormModelV2,
FlowNodeFormData,
isFormV2,
type FlowNodeEntity,
} from '@flowgram-adapter/free-layout-editor';
import {
ValueExpressionType,
type RefExpression,
} from '@coze-workflow/base/types';
import { convertGlobPath } from '../../utils/path';
import { getNamePathByField, matchPath } from './name-path';
const refExpressionSchema: ZodSchema<RefExpression> = z.lazy(() =>
z.object({
type: z.literal(ValueExpressionType.REF),
content: z.object({
keyPath: z.array(z.string()),
}),
rawMeta: z
.object({
type: z.number().int(),
})
.optional(),
}),
);
export function isRefExpression(data: any) {
return refExpressionSchema.safeParse(data).success;
}
export function traverseAllRefExpressions(
data: any,
cb: (_ref: RefExpression, _path: string) => void,
path = '/',
): any {
if (isObject(data)) {
if (isRefExpression(data)) {
return cb(data as RefExpression, path);
}
return Object.entries(data).reduce<any>((acm, [_key, _val]) => {
acm[_key] = traverseAllRefExpressions(_val, cb, `${path}${_key}/`);
return acm;
}, {});
} else if (isArray(data)) {
return data.map((_item, _idx) =>
traverseAllRefExpressions(_item, cb, `${path}${_idx}/`),
);
}
return data;
}
export function traverseUpdateRefExpressionByRename(
fullData: any,
info: {
after: BaseVariableField;
before: BaseVariableField;
},
ctx?: {
onDataRenamed?: (_newData?: any) => void;
node?: FlowNodeEntity;
},
): any {
const { before, after } = info;
const { onDataRenamed, node } = ctx || {};
const prevKeyPath = getNamePathByField(before);
let renamed = false;
traverseAllRefExpressions(fullData, (_ref, _dataPath) => {
const keyPath = _ref?.content?.keyPath;
if (!keyPath?.length) {
return _ref;
}
if (matchPath(prevKeyPath, keyPath)) {
// Match Prev Key Path And Replace it to new KeyPath
if (node && isFormV2(node)) {
const formModel = node
.getData<FlowNodeFormData>(FlowNodeFormData)
.getFormModel<FormModelV2>();
formModel.setValueIn(
`${convertGlobPath(_dataPath)}.content.keyPath.${
prevKeyPath.length - 1
}`,
after.key,
);
} else {
keyPath[prevKeyPath.length - 1] = after.key;
renamed = true;
}
}
return _ref;
});
if (renamed) {
onDataRenamed?.(fullData);
}
return fullData;
}

View File

@@ -0,0 +1,213 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { uniq } from 'lodash-es';
import { inject, injectable, postConstruct } from 'inversify';
import {
type Scope,
VariableEngine,
VariableFieldKeyRenameService,
type ObjectType,
} from '@flowgram-adapter/free-layout-editor';
import { FlowNodeFormData } from '@flowgram-adapter/free-layout-editor';
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import { Disposable, DisposableCollection } from '@flowgram-adapter/common';
import { type ViewVariableMeta } from '@coze-workflow/base/types';
import { WorkflowVariableFacade } from './workflow-variable-facade';
import { traverseUpdateRefExpressionByRename } from './utils/traverse-refs';
import { getByNamePath } from './utils/name-path';
import { type GetKeyPathCtx, type WorkflowVariableField } from './types';
/**
* 引擎内部接口,针对 Coze Workflow 封装变量外观接口
*/
@injectable()
export class WorkflowVariableFacadeService {
protected readonly cache: WeakMap<
WorkflowVariableField,
WorkflowVariableFacade
> = new WeakMap();
@inject(VariableEngine) public variableEngine: VariableEngine;
@inject(VariableFieldKeyRenameService)
public fieldRenameService: VariableFieldKeyRenameService;
@postConstruct()
init() {
// 变量引用 rename, 确保节点 UI 不渲染时也 rename 变量引用
this.fieldRenameService.onRename(({ before, after }) => {
// 覆盖的节点
const coverNodes: FlowNodeEntity[] = uniq(
before.scope.coverScopes.map(_scope => _scope.meta?.node),
);
// 所有覆盖节点表单中引用的变量更新
coverNodes.forEach(_node => {
const formData = _node.getData(FlowNodeFormData);
const fullData = formData.formModel.getFormItemValueByPath('/');
if (fullData) {
traverseUpdateRefExpressionByRename(
fullData,
{
before,
after,
},
{
onDataRenamed: () => {
// rename 触发当前节点表单 onChange
formData.fireChange();
},
node: _node,
},
);
}
});
});
}
/**
* 根据变量的 AST 查找其 Facade
* @param field
* @returns
*/
getVariableFacadeByField(
field: WorkflowVariableField,
): WorkflowVariableFacade {
const cache = this.cache.get(field);
if (cache) {
return cache;
}
// 新建该变量对应的 Facade
const facade = new WorkflowVariableFacade(field, this);
// 被删除的节点,清空其缓存
field.toDispose.push(
Disposable.create(() => {
this.cache.delete(field);
}),
);
this.cache.set(field, facade);
return facade;
}
protected getVariableFieldByKeyPath(
keyPath?: string[],
ctx?: GetKeyPathCtx,
): WorkflowVariableField | undefined {
if (!keyPath) {
return;
}
return getByNamePath(keyPath, {
...(ctx || {}),
variableEngine: this.variableEngine,
});
}
/**
* 根据 keyPath 找到变量的外观
* @param keyPath
* @returns
*/
getVariableFacadeByKeyPath(
keyPath?: string[],
ctx?: GetKeyPathCtx,
): WorkflowVariableFacade | undefined {
if (!keyPath) {
return;
}
const field = this.getVariableFieldByKeyPath(keyPath, ctx);
if (field) {
return this.getVariableFacadeByField(field);
}
}
/**
* @deprecated 变量销毁存在部分 Bad Case
* - 全局变量因切换 Project 销毁后,变量引用会被置空,导致变量引用失效
*
* 监听变量删除
*/
listenKeyPathDispose(
keyPath?: string[],
cb?: () => void,
ctx?: GetKeyPathCtx,
) {
const facade = this.getVariableFacadeByKeyPath(keyPath, ctx);
if (facade) {
// 所有在 keyPath 链路上的 Field
return facade.onDispose(cb);
}
return Disposable.create(() => null);
}
// 监听类型变化
listenKeyPathTypeChange(
keyPath?: string[],
cb?: (v?: ViewVariableMeta | null) => void,
ctx?: GetKeyPathCtx,
) {
const facade = this.getVariableFacadeByKeyPath(keyPath, ctx);
if (facade) {
const toDispose = new DisposableCollection();
toDispose.pushAll([
facade.onTypeChange(() => cb?.(facade.viewMeta)),
facade.onDispose(() => cb?.()),
]);
return toDispose;
}
return Disposable.create(() => null);
}
// 监听任意变量变化
listenKeyPathVarChange(
keyPath?: string[],
cb?: (v?: ViewVariableMeta | null) => void,
ctx?: GetKeyPathCtx,
) {
const facade = this.getVariableFacadeByKeyPath(keyPath, ctx);
if (facade) {
const toDispose = new DisposableCollection();
toDispose.pushAll([
facade.onDataChange(() => cb?.(facade.viewMeta)),
facade.onDispose(() => cb?.()),
]);
return toDispose;
}
return Disposable.create(() => null);
}
// 根据 Scope 获取 Scope 上所有的 VariableFacade
getVariableFacadesByScope(scope: Scope): WorkflowVariableFacade[] {
return scope.output.variables
.map(_variable => {
const properties = (_variable.type as ObjectType)?.properties || [];
return properties.map(_property =>
this.getVariableFacadeByField(_property),
);
})
.flat();
}
}

View File

@@ -0,0 +1,281 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/naming-convention */
import { last } from 'lodash-es';
import { ASTKind, type ASTNode } from '@flowgram-adapter/free-layout-editor';
import { FlowNodeFormData } from '@flowgram-adapter/free-layout-editor';
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import {
DisposableCollection,
type Disposable,
} from '@flowgram-adapter/common';
import {
type ViewVariableType,
type VariableMetaDTO,
type ViewVariableMeta,
VARIABLE_TYPE_ALIAS_MAP,
ValueExpressionDTO,
} from '@coze-workflow/base/types';
import { variableUtils } from '../legacy/variable-utils';
import { type GlobalVariableKey, isGlobalVariableKey } from '../constants';
import { WORKFLOW_VARIABLE_SOURCE, GLOBAL_VAR_ALIAS_MAP } from '../constants';
import { type WorkflowVariableFacadeService } from './workflow-variable-facade-service';
import {
getViewVariableByField,
getViewVariableTypeByAST,
getViewVariableTWithUniqKey,
} from './utils/parse-ast';
import { getNamePathByField } from './utils/name-path';
import { type RenameInfo, type WorkflowVariableField } from './types';
export class WorkflowVariableFacade {
protected _fieldVersion: number;
protected _variableMeta: ViewVariableMeta | undefined;
protected _keyPath: string[];
constructor(
public readonly field: WorkflowVariableField,
protected readonly _facadeService: WorkflowVariableFacadeService,
) {
// do nothing
}
// 获取 variableMeta 结构
get viewMeta(): ViewVariableMeta | undefined {
if (this._fieldVersion !== this.field.version) {
this._variableMeta = getViewVariableByField(this.field);
}
return this._variableMeta;
}
get viewMetaWithUniqKey(): ViewVariableMeta | undefined {
return getViewVariableTWithUniqKey(this.viewMeta, this.field.parent?.key);
}
get viewType(): ViewVariableType | undefined {
return getViewVariableTypeByAST(this.field.type)?.type;
}
get renderType(): JSX.Element | string | undefined {
if (!this.viewType) {
return 'Unknown';
}
return VARIABLE_TYPE_ALIAS_MAP[this.viewType];
}
get key(): string {
return this.field.key;
}
get children(): WorkflowVariableFacade[] {
const { childFields } = getViewVariableTypeByAST(this.field.type);
return (childFields || []).map(_field =>
this._facadeService.getVariableFacadeByField(_field),
);
}
get parentVariables(): WorkflowVariableFacade[] {
const { parentFields } = this.field;
return parentFields
.reverse()
.map(_field => this._facadeService.getVariableFacadeByField(_field));
}
get dtoMeta(): VariableMetaDTO | undefined {
return this.viewMeta
? variableUtils.viewMetaToDTOMeta(this.viewMeta)
: undefined;
}
get expressionPath(): {
source: string;
keyPath: string[];
} {
return {
source: this.globalVariableKey ?? WORKFLOW_VARIABLE_SOURCE,
keyPath: this.keyPath,
};
}
get groupInfo(): {
label: string;
key: string;
icon: string;
} {
if (this.globalVariableKey) {
return {
key: this.globalVariableKey,
label: GLOBAL_VAR_ALIAS_MAP[this.globalVariableKey],
// 全局变量 icon 无 url
icon: '',
};
}
const DEFAULT_NODE_META_PATH = '/nodeMeta';
const formData = this.node?.getData<FlowNodeFormData>(FlowNodeFormData);
const nodeMeta = formData.formModel.getFormItemValueByPath<{
title: string;
icon: string;
description: string;
subTitle?: string;
}>(DEFAULT_NODE_META_PATH);
return {
key: this.node?.id ?? '',
label: nodeMeta?.title ?? '',
icon: nodeMeta?.icon ?? '',
};
}
get refExpressionDTO(): ValueExpressionDTO {
const { dtoMeta } = this;
if (!dtoMeta) {
return ValueExpressionDTO.createEmpty();
}
return {
type: dtoMeta.type,
schema: dtoMeta.schema,
assistType: dtoMeta.assistType,
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: this.keyPath[0] || '',
name: this.keyPath.slice(1).join('.'),
},
},
};
}
// 当前变量所处的节点
get node(): FlowNodeEntity {
return this.field.scope.meta?.node;
}
get globalVariableKey(): GlobalVariableKey | undefined {
const lastField = last(this.field.parentFields);
if (lastField?.key && isGlobalVariableKey(lastField?.key)) {
return lastField?.key as GlobalVariableKey;
}
return;
}
// 获取 keyPath 路径
get keyPath(): string[] {
if (!this._keyPath) {
this._keyPath = getNamePathByField(this.field);
}
return this._keyPath;
}
// 对应节点是否可以访问它
canAccessByNode(nodeId: string) {
return !!this.field.scope.coverScopes.find(_scope => _scope.id === nodeId);
}
/**
* @deprecated 变量销毁存在部分 Bad Case
* - 全局变量因切换 Project 销毁后,变量引用会被置空,导致变量引用失效
*
* 监听变量删除
*/
onDispose(cb?: () => void): Disposable {
const toDispose = new DisposableCollection();
// 删除回调只要执行一次
let cbCalled = false;
const cbOnce = () => {
if (!cbCalled) {
cbCalled = true;
cb?.();
}
};
const allASTs: ASTNode[] = [this.field];
let curr = this.field.parent;
while (curr) {
allASTs.push(curr);
curr = curr.parent;
}
toDispose.pushAll([
// 遍历除 Rename 外的所有 Dispose 情况
this._facadeService.fieldRenameService.onDisposeInList(_disposeField => {
if (allASTs.includes(_disposeField)) {
cbOnce();
}
}),
this.field.scope.event.on('DisposeAST', ({ ast }) => {
if (
ast &&
// TODO Object 删除也有可能是 Rename 导致的,需要重新判断
[ASTKind.VariableDeclarationList].includes(ast?.kind as ASTKind) &&
allASTs.includes(ast)
) {
cbOnce();
}
}),
]);
return toDispose;
}
onRename(cb?: (params: RenameInfo) => void): Disposable {
const allFields = this.field.parentFields.reverse().concat(this.field);
return this._facadeService.fieldRenameService.onRename(
({ before, after }) => {
const changedIndex = allFields.indexOf(before);
if (changedIndex >= 0) {
const nextKeyPath = [...this.keyPath];
nextKeyPath[changedIndex] = after.key;
const _info = {
prevKeyPath: this._keyPath,
nextKeyPath,
modifyIndex: changedIndex,
modifyKey: after.key,
};
cb?.(_info);
}
},
);
}
onTypeChange(cb?: (facade: WorkflowVariableFacade) => void): Disposable {
return this.field.subscribe(() => cb?.(this), {
// 当前层级的类型发时变化时,才触发 onTypeChange
selector: field => {
const { type } = getViewVariableTypeByAST(field.type);
return type;
},
});
}
onDataChange(cb?: (facade: WorkflowVariableFacade) => void): Disposable {
return this.field.subscribe(() => cb?.(this));
}
}

View File

@@ -0,0 +1,168 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createVariablePlugin } from '@flowgram-adapter/free-layout-editor';
import { createNodeVariablePlugin } from '@flowgram-adapter/free-layout-editor';
import {
DecoratorAbility,
FormManager,
} from '@flowgram-adapter/free-layout-editor';
import { FlowDocument } from '@flowgram-adapter/free-layout-editor';
import {
Playground,
definePluginCreator,
type PluginCreator,
} from '@flowgram-adapter/free-layout-editor';
import {
getChildrenNode,
getHasChildCanvasNodePublicDeps,
getParentNode,
getParentPublic,
hasChildCanvas,
} from './utils/sub-canvas';
import { GlobalVariableService } from './services/global-variable-service';
import {
WorkflowBatchService,
WorkflowVariableService,
WorkflowVariableValidationService,
} from './legacy';
import {
variableConsumers,
variableDecorators,
variableProviders,
} from './form-extensions';
import {
WorkflowNodeInputVariablesData,
WorkflowNodeOutputVariablesData,
WorkflowNodeRefVariablesData,
} from './datas';
import { extendASTNodes, WorkflowVariableFacadeService } from './core';
import { GLOBAL_VARIABLE_SCOPE_ID } from './constants';
import { VariableDebugLayer } from './components/variable-debug-panel/variable-debug-layer';
export const createWorkflowVariablePlugin: PluginCreator<object> =
definePluginCreator<object>({
onBind({ bind }) {
bind(WorkflowVariableFacadeService).toSelf().inSingletonScope();
bind(WorkflowVariableService).toSelf().inSingletonScope();
bind(WorkflowBatchService).toSelf().inSingletonScope();
bind(WorkflowVariableValidationService).toSelf().inSingletonScope();
bind(GlobalVariableService).toSelf().inSingletonScope();
},
onInit(ctx) {
const playground: Playground = ctx.get(Playground);
const formManager: FormManager = ctx.get(FormManager);
const document: FlowDocument = ctx.get(FlowDocument);
// Trigger @postConstruct for GlobalVariableService
ctx.get(GlobalVariableService);
document.registerNodeDatas(
WorkflowNodeOutputVariablesData,
WorkflowNodeInputVariablesData,
WorkflowNodeRefVariablesData,
);
if (IS_DEV_MODE) {
playground.registerLayer(VariableDebugLayer);
}
variableProviders.forEach(_provider =>
formManager.registerAbilityExtension('variable-provider', _provider),
);
variableConsumers.forEach(_consumer =>
formManager.registerAbilityExtension('variable-consumer', _consumer),
);
variableDecorators.forEach(_decorator =>
formManager.registerAbilityExtension(DecoratorAbility.type, _decorator),
);
},
});
export const createWorkflowVariablePlugins = () => [
createVariablePlugin({
enable: true,
layout: 'free',
extendASTNodes,
layoutConfig: {
transformCovers(scopes, { scope, variableEngine }) {
// 全局变量作用域覆盖所有其他作用域
if (scope.id === GLOBAL_VARIABLE_SCOPE_ID) {
return variableEngine
.getAllScopes()
.filter(_scope => _scope.id !== GLOBAL_VARIABLE_SCOPE_ID);
}
const node = scope.meta?.node;
if (!node) {
return scopes;
}
// private 只能访问当前节点的子节点和自己的 public
// if (scope.meta?.type === 'private' && scope.meta?.node) {
// const visibleNodes = [
// scope.meta?.node,
// ...getChildrenNode(scope.meta?.node),
// ];
// return scopes.filter(_scope =>
// visibleNodes.includes(_scope.meta?.node),
// );
// }
// 特化:父节点的 public 可以访问子节点的 public用于聚合输出
const parentPublic = getParentPublic(node);
if (parentPublic) {
return [...scopes, parentPublic];
}
return scopes;
},
transformDeps(scopes, { scope, variableEngine }) {
const node = scope.meta?.node;
const globalScope = variableEngine.getScopeById(
GLOBAL_VARIABLE_SCOPE_ID,
);
if (globalScope) {
scopes.unshift(globalScope);
}
if (!node) {
return scopes;
}
// 特化:父节点的 public 可以访问子节点的 public用于聚合输出, 且不能选择全局变量
if (scope.meta?.type === 'public' && hasChildCanvas(node)) {
return getHasChildCanvasNodePublicDeps(node);
}
return scopes;
},
getFreeParent(node) {
return getParentNode(node);
},
getFreeChildren(node) {
return getChildrenNode(node);
},
},
}),
createNodeVariablePlugin({}),
createWorkflowVariablePlugin({}),
];

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { WorkflowNodeOutputVariablesData } from './workflow-node-output-variables-data';
export { WorkflowNodeInputVariablesData } from './workflow-node-input-variables-data';
export {
WorkflowNodeRefVariablesData,
type UpdateRefInfo,
} from './workflow-node-ref-variables-data';

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import { EntityData } from '@flowgram-adapter/free-layout-editor';
import {
getFormValueByPathEnds,
type RefExpressionContent,
type InputValueVO,
type WorkflowNodeRegistry,
} from '@coze-workflow/base';
import { type WorkflowVariable, WorkflowVariableFacadeService } from '../core';
interface InputVariable {
name?: string;
refVariable?: WorkflowVariable;
}
/**
* Represents the data for ref variables of a flow node.
*/
export class WorkflowNodeInputVariablesData extends EntityData {
static readonly type = 'WorkflowNodeInputVariablesData';
declare entity: FlowNodeEntity;
getDefaultData() {
return {};
}
protected get facadeService() {
return this.entity.getService(WorkflowVariableFacadeService);
}
/**
* 获取输入的表单值
*/
get inputParameters(): InputValueVO[] {
const registry = this.entity.getNodeRegister() as WorkflowNodeRegistry;
if (registry.getNodeInputParameters) {
return registry.getNodeInputParameters(this.entity) || [];
} else {
return (
getFormValueByPathEnds<InputValueVO[]>(
this.entity,
'/inputParameters',
) || []
);
}
}
/**
* 获取所有的输入变量,包括变量名和引用的变量实例
*/
get inputVariables(): InputVariable[] {
return this.inputParameters.map(_input => {
const { name } = _input;
const refVariable = this.facadeService.getVariableFacadeByKeyPath(
(_input.input?.content as RefExpressionContent)?.keyPath,
{ node: this.entity, checkScope: true },
);
return {
name,
refVariable,
};
});
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ASTKind,
FlowNodeVariableData,
type ObjectType,
} from '@flowgram-adapter/free-layout-editor';
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import { EntityData } from '@flowgram-adapter/free-layout-editor';
import { type Disposable } from '@flowgram-adapter/common';
import { type WorkflowVariable, WorkflowVariableFacadeService } from '../core';
/**
* Represents the data for output variables of a flow node.
*/
export class WorkflowNodeOutputVariablesData extends EntityData {
static readonly type = 'WorkflowNodeOutputVariablesData';
declare entity: FlowNodeEntity;
getDefaultData() {
return {};
}
protected get variableData(): FlowNodeVariableData {
return this.entity.getData(FlowNodeVariableData);
}
protected get facadeService() {
return this.entity.getService(WorkflowVariableFacadeService);
}
protected get outputObjectType(): ObjectType | undefined {
const output = this.variableData.public.output.variables[0];
if (output?.type?.kind !== ASTKind.Object) {
return undefined;
}
return output.type as ObjectType;
}
/**
* Retrieves the list of workflow variables based on the output object type properties.
* @returns An array of workflow variables.
*/
get variables(): WorkflowVariable[] {
return (this.outputObjectType?.properties || []).map(_property =>
this.facadeService.getVariableFacadeByField(_property),
);
}
/**
* Retrieves a workflow variable by its key.
* @param key - The key of the variable.
* @returns The workflow variable or undefined if not found.
*/
getVariableByKey(key: string): WorkflowVariable | undefined {
const field = this.outputObjectType?.propertyTable.get(key);
return field
? this.facadeService.getVariableFacadeByField(field)
: undefined;
}
/**
* Registers a callback function that will be invoked whenever any variable changes.
*
* @param cb - The callback function to be executed on any variable change.
* @returns A `Disposable` object that can be used to unregister the callback.
*/
onAnyVariablesChange(cb: () => void): Disposable {
return this.variableData.public.ast.subscribe(cb);
}
}

View File

@@ -0,0 +1,179 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { set } from 'lodash-es';
import { FlowNodeVariableData } from '@flowgram-adapter/free-layout-editor';
import {
Emitter,
type FormModelV2,
isFormV2,
} from '@flowgram-adapter/free-layout-editor';
import { FlowNodeFormData } from '@flowgram-adapter/free-layout-editor';
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import { EntityData } from '@flowgram-adapter/free-layout-editor';
import { convertGlobPath } from '../utils/path';
import { type ValueExpression } from '../typings';
import { traverseAllRefExpressions } from '../core/utils/traverse-refs';
import { matchPath } from '../core/utils/name-path';
import { type WorkflowVariable, WorkflowVariableFacadeService } from '../core';
import { allGlobalVariableKeys } from '../constants';
type KeyPath = string[];
type DataPath = string;
type Refs = Record<DataPath, KeyPath>;
type RefVariables = Record<DataPath, WorkflowVariable | undefined>;
export interface UpdateRefInfo {
beforeKeyPath: KeyPath;
afterKeyPath?: KeyPath;
afterExpression?: ValueExpression;
}
/**
* Represents the data for ref variables of a flow node.
*/
export class WorkflowNodeRefVariablesData extends EntityData {
static readonly type = 'WorkflowNodeRefVariablesData';
declare entity: FlowNodeEntity;
protected onBatchUpdateRefsEmitter = new Emitter<UpdateRefInfo[]>();
onBatchUpdateRefs = this.onBatchUpdateRefsEmitter.event;
constructor(entity: FlowNodeEntity) {
super(entity);
this.toDispose.push(this.onBatchUpdateRefsEmitter);
}
getDefaultData() {
return {};
}
protected get formData(): FlowNodeFormData {
return this.entity.getData(FlowNodeFormData);
}
protected get variableData(): FlowNodeVariableData {
return this.entity.getData(FlowNodeVariableData);
}
protected get facadeService() {
return this.entity.getService(WorkflowVariableFacadeService);
}
get refs(): Refs {
const refs: Refs = {};
const fullData = this.formData.formModel.getFormItemValueByPath('/');
if (fullData) {
traverseAllRefExpressions(fullData, (_ref, _dataPath) => {
const keyPath = _ref?.content?.keyPath;
if (!keyPath?.length) {
return;
}
refs[convertGlobPath(_dataPath)] = keyPath;
});
}
return refs;
}
get refVariables(): RefVariables {
return Object.entries(this.refs).reduce((_acm, _curr) => {
const [dataPath, keyPath] = _curr;
return {
..._acm,
[dataPath]: this.facadeService.getVariableFacadeByKeyPath(keyPath, {
node: this.entity,
checkScope: true,
}),
};
}, {} satisfies RefVariables);
}
/**
* 批量更新变量引用
* @param updateInfos 变更的 KeyPath 信息
*/
batchUpdateRefs(updateInfos: UpdateRefInfo[]) {
let needUpdate = false;
const fullData = this.formData.formModel.getFormItemValueByPath('/');
const setValueIn = (path: string, nextValue: unknown) => {
// 新表单引擎更新数据
if (isFormV2(this.entity)) {
(this.formData.formModel as FormModelV2).setValueIn(path, nextValue);
return;
}
// 老表单引擎更新数据
set(fullData, path, nextValue);
return;
};
Object.entries(this.refs).forEach(_entry => {
const [dataPath, keyPath] = _entry;
const updateInfo = updateInfos.find(_info =>
matchPath(_info.beforeKeyPath, keyPath),
);
if (updateInfo) {
needUpdate = true;
// 没有传入更新后的 KeyPath则更新 content
if (!updateInfo.afterKeyPath) {
// rehaje 更新 bug设置值时需要 setter 内值局部更新,不能更改 setter 整体值
setValueIn(
`${dataPath}.content`,
updateInfo.afterExpression?.content,
);
setValueIn(`${dataPath}.type`, updateInfo.afterExpression?.type);
return;
}
/**
* 获取更新后的 KeyPath
* 假设要替换:[A, B] -> [C, D, E]
* 当前 KeyPath 为 [A, B, F, G]
* 则 nextPath 为 [C, D, E] + [F, G] = [C, D, E, F, G]
*/
const nextPath = [
...updateInfo.afterKeyPath,
...keyPath.slice(updateInfo.beforeKeyPath.length),
];
setValueIn(`${dataPath}.content.keyPath`, nextPath);
}
});
if (needUpdate) {
this.onBatchUpdateRefsEmitter.fire(updateInfos);
this.formData.fireChange();
}
}
// 拥有全局变量的引用
get hasGlobalRef(): boolean {
return Object.values(this.refs).some(_keyPath =>
(allGlobalVariableKeys as string[]).includes(_keyPath[0]),
);
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { type FC } from 'react';
import {
FlowNodeVariableData,
ScopeProvider,
} from '@flowgram-adapter/free-layout-editor';
import { type DecoratorComponentProps } from '@flowgram-adapter/free-layout-editor';
const PrivateScopeDecorator: FC<DecoratorComponentProps> = props => {
const { context, children } = props;
const privateScope = context.node?.getData(FlowNodeVariableData)?.private;
if (privateScope) {
return (
<ScopeProvider value={{ scope: privateScope }}>{children}</ScopeProvider>
);
}
return <>{children}</>;
};
export const privateScopeDecorator = {
key: 'PrivateScopeDecorator',
component: PrivateScopeDecorator,
};

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type VariableProviderAbilityOptions } from '@flowgram-adapter/free-layout-editor';
import { provideNodeOutputVariables } from './variable-providers/provide-node-output-variables';
import { provideNodeBatchVariables } from './variable-providers/provide-node-batch-variables';
import { provideLoopOutputsVariables } from './variable-providers/provide-loop-output-variables';
import { provideLoopInputsVariables } from './variable-providers/provide-loop-input-variables';
import { consumeRefValueExpression } from './variable-consumers/consume-ref-value-expression';
import { privateScopeDecorator } from './decorators/private-scope-decorator';
export { provideMergeGroupVariablesEffect } from './variable-providers/provide-merge-group-variables';
export const variableProviders: VariableProviderAbilityOptions[] = [
provideNodeOutputVariables,
provideNodeBatchVariables,
provideLoopInputsVariables,
provideLoopOutputsVariables,
];
export const variableConsumers = [consumeRefValueExpression];
export const variableDecorators = [privateScopeDecorator];

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ASTFactory, type ASTNode } from '@flowgram-adapter/free-layout-editor';
import { type VariableConsumerAbilityOptions } from '@flowgram-adapter/free-layout-editor';
/**
* TODO 数组内 variable-consumer 拿不到 value 值
*/
export const consumeRefValueExpression: VariableConsumerAbilityOptions = {
key: 'consume-ref-value-expression',
parse(v, ctx) {
console.log(
'[ debugger test change ] > ',
ctx.formItem?.formModel,
ctx.formItem?.path,
v,
);
return ASTFactory.createKeyPathExpression({
keyPath: v?.content?.keyPath,
});
},
onInit(ctx) {
const { options, scope, formItem } = ctx;
const astKey = options?.namespace || formItem?.path || '';
return scope.ast.subscribe<ASTNode>(
_type => {
console.log('[ debugger type ] >', _type);
},
{
selector: _ast => _ast.get(astKey)?.returnType as ASTNode,
},
);
},
};

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { uniqBy } from 'lodash-es';
import { type PropertyJSON } from '@flowgram-adapter/free-layout-editor';
import { type RefExpression } from '@coze-workflow/base/types';
export interface InputItem {
name: string;
input: RefExpression;
}
export const uniqInputs = (inputs?: InputItem[]): InputItem[] =>
uniqBy(
(inputs || []).filter(_input => _input && _input?.name),
_child => _child?.name,
);
export const uniqProperties = (properties?: PropertyJSON[]): PropertyJSON[] =>
uniqBy(
(properties || []).filter(_input => _input && _input?.key),
_child => _child?.key,
);

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ASTFactory } from '@flowgram-adapter/free-layout-editor';
import { type VariableProviderAbilityOptions } from '@flowgram-adapter/free-layout-editor';
import { ValueExpressionType } from '../../typings';
import { createRefExpression } from '../../core/extend-ast/custom-key-path-expression';
import { type InputItem, uniqInputs, uniqProperties } from './common';
interface ValueType {
inputParameters?: InputItem[];
variableParameters?: InputItem[];
}
export const parseLoopInputsByViewVariableMeta = (
nodeId: string,
value: ValueType,
) => {
const { inputParameters, variableParameters } = value || {};
const batchProperties = uniqInputs(inputParameters).map(_input =>
ASTFactory.createProperty({
key: _input?.name,
meta: {
label: `item (in ${_input?.name})`,
},
initializer: ASTFactory.createEnumerateExpression({
enumerateFor: createRefExpression({
keyPath: _input?.input?.content?.keyPath || [],
rawMeta: _input?.input?.rawMeta,
}),
}),
}),
);
const variableProperties = uniqInputs(variableParameters).map(_input => {
// 没有 rawMeta 时,可能是历史数据,走下面的兜底逻辑
if (_input?.input?.rawMeta?.type) {
return ASTFactory.createProperty({
key: _input?.name,
meta: {
mutable: true,
},
initializer: createRefExpression({
keyPath: _input?.input?.content?.keyPath || [],
rawMeta: _input?.input?.rawMeta,
}),
});
}
if (_input?.input?.type === ValueExpressionType.REF) {
return ASTFactory.createProperty({
key: _input?.name,
meta: {
mutable: true,
},
// 直接引用变量
initializer: ASTFactory.createKeyPathExpression({
keyPath: _input?.input?.content?.keyPath || [],
}),
});
}
return ASTFactory.createProperty({
key: _input?.name,
meta: {
mutable: true,
},
type: ASTFactory.createString(),
});
});
const indexProperties = [
ASTFactory.createProperty({
key: 'index',
type: ASTFactory.createInteger(),
}),
];
const properties = uniqProperties([
...batchProperties,
...indexProperties,
...variableProperties,
]);
return [
ASTFactory.createVariableDeclaration({
key: `${nodeId}.locals`,
type: ASTFactory.createObject({
properties,
}),
}),
];
};
/**
* 循环输入变量同步
*/
export const provideLoopInputsVariables: VariableProviderAbilityOptions = {
key: 'provide-loop-input-variables',
namespace: '/node/locals',
private: true,
scope: 'private',
parse(value: ValueType, context) {
return parseLoopInputsByViewVariableMeta(context.node.id, value);
},
};

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type VariableProviderAbilityOptions } from '@flowgram-adapter/free-layout-editor';
import { ASTFactory } from '@flowgram-adapter/free-layout-editor';
import { createWrapArrayExpression } from '../../core/extend-ast/wrap-array-expression';
import { type InputItem, uniqInputs } from './common';
export const parseLoopOutputsByViewVariableMeta = (
nodeId: string,
value: InputItem[],
) => {
const properties = uniqInputs(value || []).map(_input => {
const keyPath = _input?.input?.content?.keyPath;
// 如果选择的是 Loop 的 Variable 内的变量
if (keyPath?.[0] === nodeId) {
return ASTFactory.createProperty({
key: _input?.name,
// 直接引用变量
initializer: ASTFactory.createKeyPathExpression({
keyPath: _input?.input?.content?.keyPath || [],
}),
});
}
return ASTFactory.createProperty({
key: _input?.name,
// 输出类型包一层 Array
initializer: createWrapArrayExpression({
keyPath: _input?.input?.content?.keyPath || [],
}),
});
});
return [
ASTFactory.createVariableDeclaration({
key: `${nodeId}.outputs`,
type: ASTFactory.createObject({
properties,
}),
}),
];
};
/**
* 循环输出变量同步
*/
export const provideLoopOutputsVariables: VariableProviderAbilityOptions = {
key: 'provide-loop-output-variables',
namespace: '/node/outputs',
private: false,
scope: 'public',
parse(value, context) {
return parseLoopOutputsByViewVariableMeta(context.node.id, value);
},
};

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ASTFactory,
ASTKind,
type ObjectType,
} from '@flowgram-adapter/free-layout-editor';
import { type VariableProviderAbilityOptions } from '@flowgram-adapter/free-layout-editor';
import { type EffectOptions } from '@flowgram-adapter/free-layout-editor';
import { createEffectFromVariableProvider } from '../../utils/variable-provider';
import { setValueIn } from '../../utils/form';
import { type RefExpression } from '../../typings';
import {
createMergeGroupExpression,
MergeStrategy,
} from '../../core/extend-ast/merge-group-expression';
import { createRefExpression } from '../../core/extend-ast/custom-key-path-expression';
import { WorkflowVariableFacadeService } from '../../core';
interface MergeGroup {
name: string;
variables: RefExpression[];
}
/**
* 合并组变量同步
*/
export const provideMergeGroupVariables: VariableProviderAbilityOptions = {
key: 'provide-merge-group-variables',
namespace: '/node/outputs',
parse(value: MergeGroup[], context) {
const nodeId = context.node.id;
return [
ASTFactory.createVariableDeclaration({
key: `${nodeId}.outputs`,
type: ASTFactory.createObject({
properties: value?.map(_item =>
ASTFactory.createProperty({
key: _item?.name,
initializer: createMergeGroupExpression({
mergeStrategy: MergeStrategy.FirstNotEmpty,
expressions: _item.variables.map(_v =>
createRefExpression({
keyPath: _v?.content?.keyPath || [],
rawMeta: _v?.rawMeta,
}),
),
}),
}),
),
}),
}),
];
},
onInit(ctx) {
const facadeService = ctx.node.getService(WorkflowVariableFacadeService);
return ctx.scope.ast.subscribe(() => {
// 监听输出变量变化,回填到表单的 outputs
const outputVariable = ctx.scope.output.variables[0];
if (outputVariable?.type?.kind === ASTKind.Object) {
const { properties } = outputVariable.type as ObjectType;
const nextOutputs = properties
.map(
_property =>
// OutputTree 组件中,所有树节点的 key 需要保证是唯一的
facadeService.getVariableFacadeByField(_property)
.viewMetaWithUniqKey,
)
.filter(Boolean);
setValueIn(ctx.node, 'outputs', nextOutputs);
}
});
},
};
export const provideMergeGroupVariablesEffect: EffectOptions[] =
createEffectFromVariableProvider(provideMergeGroupVariables);

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type VariableProviderAbilityOptions } from '@flowgram-adapter/free-layout-editor';
import { FlowNodeFormData } from '@flowgram-adapter/free-layout-editor';
import { Disposable } from '@flowgram-adapter/common';
import { parseNodeBatchByInputList } from '../../core';
export const provideNodeBatchVariables: VariableProviderAbilityOptions = {
key: 'provide-node-batch-variables',
namespace: '/node/locals',
scope: 'private',
parse(value, context) {
const batchMode =
context.formItem?.formModel.getFormItemValueByPath('/batchMode') ||
context.formItem?.formModel.getFormItemValueByPath('/inputs/batchMode');
if (batchMode !== 'batch') {
return [];
}
return parseNodeBatchByInputList(context.node.id, value);
},
onInit(context) {
const formData = context.node.getData(FlowNodeFormData);
if (!formData) {
return Disposable.create(() => null);
}
return formData.onDetailChange(_detail => {
if (_detail.path.includes('/batchMode')) {
context.triggerSync();
}
});
},
};

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type VariableProviderAbilityOptions } from '@flowgram-adapter/free-layout-editor';
import { parseNodeOutputByViewVariableMeta } from '../../core';
export const provideNodeOutputVariables: VariableProviderAbilityOptions = {
key: 'provide-node-output-variables',
namespace: '/node/outputs',
parse(value, context) {
return parseNodeOutputByViewVariableMeta(context.node.id, value);
},
};

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference types='@coze-arch/bot-typings' />

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { useVariableDispose } from './use-variable-dispose';
export { useVariableTypeChange } from './use-variable-type-change';
export { useVariableChange } from './use-variable-change';
export { useVariableRename } from './use-variable-rename';
export { useAvailableWorkflowVariables } from './use-available-workflow-variables';
export { useAutoSyncRenameData } from './use-auto-sync-rename-data';
export { useWorkflowVariableByKeyPath } from './use-workflow-variable-by-keypath';
export { useVariableType } from './use-variable-type';
export { useGetWorkflowVariableByKeyPath } from './use-get-workflow-variable-by-keypath';
export { useGlobalVariableServiceState } from './use-global-variable-service-state';

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect, useRef } from 'react';
import { VariableFieldKeyRenameService } from '@flowgram-adapter/free-layout-editor';
import { useService } from '@flowgram-adapter/free-layout-editor';
import { traverseUpdateRefExpressionByRename } from '../core/utils/traverse-refs';
export function useAutoSyncRenameData(
data: any,
ctx: {
onDataRenamed?: (_newData?: any) => void;
} = {},
) {
const { onDataRenamed } = ctx || {};
const fieldRenameService: VariableFieldKeyRenameService = useService(
VariableFieldKeyRenameService,
);
const latest = useRef(data);
latest.current = data;
useEffect(() => {
const disposable = fieldRenameService.onRename(({ before, after }) => {
traverseUpdateRefExpressionByRename(
latest.current,
{ before, after },
{
onDataRenamed,
},
);
});
return () => disposable.dispose();
}, []);
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, startTransition } from 'react';
import {
ASTKind,
type ObjectType,
useCurrentScope,
} from '@flowgram-adapter/free-layout-editor';
import { useRefresh, useService } from '@flowgram-adapter/free-layout-editor';
import { WorkflowVariableFacadeService, type WorkflowVariable } from '../core';
export function useAvailableWorkflowVariables(): WorkflowVariable[] {
const scope = useCurrentScope();
const facadeService: WorkflowVariableFacadeService = useService(
WorkflowVariableFacadeService,
);
const refresh = useRefresh();
useEffect(() => {
const disposable = scope.available.onDataChange(() => {
startTransition(() => refresh());
});
return () => disposable.dispose();
}, []);
return scope.available.variables
.map(_variable => {
// 第一层为变量,因此需要分层处理
if (_variable.type.kind === ASTKind.Object) {
return ((_variable.type as ObjectType)?.properties || []).map(
_property => facadeService.getVariableFacadeByField(_property),
);
}
return [];
})
.flat();
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useCallback } from 'react';
import {
useCurrentEntity,
useService,
} from '@flowgram-adapter/free-layout-editor';
import { WorkflowVariableFacadeService } from '../core';
export function useGetWorkflowVariableByKeyPath() {
const node = useCurrentEntity();
const facadeService: WorkflowVariableFacadeService = useService(
WorkflowVariableFacadeService,
);
return useCallback(
(keyPath: string[]) =>
facadeService.getVariableFacadeByKeyPath(keyPath, { node }),
[node],
);
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useMemo } from 'react';
import { useRefresh, useService } from '@flowgram-adapter/free-layout-editor';
import { DisposableCollection } from '@flowgram-adapter/common';
import {
GlobalVariableService,
type State as GlobalVariableServiceState,
} from '../services/global-variable-service';
interface Params {
// 是否监听变量加载完成事件(变量下钻可能发生变化)
listenVariableLoaded?: boolean;
}
export function useGlobalVariableServiceState(
params: Params = {},
): GlobalVariableServiceState {
const { listenVariableLoaded } = params;
const globalVariableService = useService<GlobalVariableService>(
GlobalVariableService,
);
const refresh = useRefresh();
useEffect(() => {
const toDispose = new DisposableCollection();
toDispose.push(
globalVariableService.onBeforeLoad(() => {
refresh();
}),
);
if (listenVariableLoaded) {
toDispose.push(
globalVariableService.onLoaded(() => {
refresh();
}),
);
}
return () => toDispose.dispose();
}, []);
return useMemo(
() => globalVariableService.state,
[globalVariableService.state],
);
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect } from 'react';
import {
useCurrentEntity,
useService,
} from '@flowgram-adapter/free-layout-editor';
import { type ViewVariableMeta } from '@coze-workflow/base';
import { WorkflowVariableService } from '../legacy';
interface HooksParams {
keyPath?: string[];
onChange?: (params: { variableMeta?: ViewVariableMeta | null }) => void;
}
export function useVariableChange(params: HooksParams) {
const { keyPath, onChange } = params;
const node = useCurrentEntity();
const variableService: WorkflowVariableService = useService(
WorkflowVariableService,
);
useEffect(() => {
if (!keyPath) {
return () => null;
}
const disposable = variableService.onListenVariableChange(
keyPath,
meta => {
onChange?.({ variableMeta: meta });
},
{ node },
);
return () => disposable.dispose();
}, [keyPath?.join('.')]);
return;
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect } from 'react';
import {
useCurrentEntity,
useService,
} from '@flowgram-adapter/free-layout-editor';
import { WorkflowVariableService } from '../legacy';
interface HooksParams {
keyPath?: string[];
onDispose?: () => void;
}
/**
* @deprecated 变量销毁存在部分 Bad Case
* - 全局变量因切换 Project 销毁后,变量引用会被置空,导致变量引用失效
*/
export function useVariableDispose(params: HooksParams) {
const { keyPath, onDispose } = params;
const node = useCurrentEntity();
const variableService: WorkflowVariableService = useService(
WorkflowVariableService,
);
useEffect(() => {
if (!keyPath) {
return () => null;
}
const disposable = variableService.onListenVariableDispose(
keyPath,
() => {
onDispose?.();
},
{ node },
);
return () => disposable.dispose();
}, [keyPath?.join('.')]);
return;
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect } from 'react';
import { useService } from '@flowgram-adapter/free-layout-editor';
import { useCurrentEntity } from '@flowgram-adapter/free-layout-editor';
import { type RenameInfo } from '../core/types';
import { WorkflowVariableFacadeService } from '../core';
interface HooksParams {
keyPath?: string[];
onRename?: (params: RenameInfo) => void;
}
export function useVariableRename({ keyPath, onRename }: HooksParams) {
const node = useCurrentEntity();
const facadeService: WorkflowVariableFacadeService = useService(
WorkflowVariableFacadeService,
);
useEffect(() => {
if (!keyPath) {
return;
}
const variable = facadeService.getVariableFacadeByKeyPath(keyPath, {
node,
});
const disposable = variable?.onRename(_params => {
onRename?.(_params);
});
return () => disposable?.dispose();
}, [keyPath?.join('.')]);
return;
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable security/detect-object-injection */
import { useEffect, useRef } from 'react';
import {
useCurrentEntity,
useRefresh,
useService,
} from '@flowgram-adapter/free-layout-editor';
import { DisposableCollection } from '@flowgram-adapter/common';
import { type ViewVariableMeta } from '@coze-workflow/base';
import { WorkflowVariableFacadeService } from '../core';
type TypeChange = (params: { variableMeta?: ViewVariableMeta | null }) => void;
interface HooksParams {
keyPath?: string[];
onTypeChange?: TypeChange;
}
export function useVariableTypeChange(params: HooksParams) {
const { keyPath, onTypeChange } = params;
const node = useCurrentEntity();
const keyPathRef = useRef<string[] | undefined>([]);
keyPathRef.current = keyPath;
const refresh = useRefresh();
const facadeService: WorkflowVariableFacadeService = useService(
WorkflowVariableFacadeService,
);
const callbackRef = useRef<TypeChange | undefined>();
callbackRef.current = onTypeChange;
useEffect(() => {
if (!keyPath) {
return () => null;
}
const toDispose = new DisposableCollection();
const variable = facadeService.getVariableFacadeByKeyPath(keyPath, {
node,
});
toDispose.push(
facadeService.listenKeyPathTypeChange(keyPath, meta => {
callbackRef.current?.({ variableMeta: meta });
}),
);
if (variable) {
toDispose.push(
variable.onRename(({ modifyIndex, modifyKey }) => {
if (keyPathRef.current) {
// 更改 keyPath 并刷新,重新监听变量变化
keyPathRef.current[modifyIndex] = modifyKey;
}
refresh();
}),
);
}
return () => toDispose.dispose();
}, [keyPathRef.current?.join('.')]);
return;
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState } from 'react';
import {
useCurrentEntity,
useService,
} from '@flowgram-adapter/free-layout-editor';
import { type ViewVariableType } from '@coze-workflow/base/types';
import { WorkflowVariableService } from '../legacy';
import { useVariableTypeChange } from './use-variable-type-change';
export const useVariableType = (
keyPath: string[],
): ViewVariableType | undefined => {
const node = useCurrentEntity();
const variableService: WorkflowVariableService = useService(
WorkflowVariableService,
);
const originType = variableService.getWorkflowVariableByKeyPath(keyPath, {
node,
})?.viewType;
const [variableType, setVariableType] = useState<
ViewVariableType | undefined
>(originType);
useVariableTypeChange({
keyPath,
onTypeChange: ({ variableMeta }) => {
setVariableType(variableMeta?.type);
},
});
return variableType;
};

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
useCurrentEntity,
useService,
} from '@flowgram-adapter/free-layout-editor';
import { WorkflowVariableFacadeService } from '../core';
export function useWorkflowVariableByKeyPath(keyPath?: string[]) {
const node = useCurrentEntity();
const facadeService: WorkflowVariableFacadeService = useService(
WorkflowVariableFacadeService,
);
return facadeService.getVariableFacadeByKeyPath(keyPath, {
node,
checkScope: true,
});
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/no-batch-import-or-export */
export { FlowNodeVariableData } from '@flowgram-adapter/free-layout-editor';
export * from './hooks';
// 老变量引擎代码,等待替换中。。。
export * from './legacy';
export * from './typings';
export * from './core';
export * from './components';
export * from './datas';
export * from './form-extensions';
export * from './constants';
export * from './services';
export { generateInputJsonSchema } from './utils/generate-input-json-schema';
export { createWorkflowVariablePlugins } from './create-workflow-variable-plugin';

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { variableUtils } from './variable-utils';
export { WorkflowVariableService } from './workflow-variable-service';
export { WorkflowBatchService } from './workflow-batch-service';
export { WorkflowVariableValidationService } from './workflow-variable-validation-service';

View File

@@ -0,0 +1,670 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable max-lines */
/* eslint-disable complexity */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { isBoolean, isInteger, isNil, isNumber } from 'lodash-es';
import { nanoid } from '@flowgram-adapter/free-layout-editor';
import type {
InputTypeValueDTO,
ObjectRefExpression,
} from '@coze-workflow/base/src/types';
import {
BatchMode,
type InputValueDTO,
type InputValueVO,
type RefExpression,
ValueExpression,
type ValueExpressionDTO,
ValueExpressionType,
type VariableMetaDTO,
VariableTypeDTO,
AssistTypeDTO,
type ViewVariableMeta,
ViewVariableType,
type LiteralExpression,
type InputTypeValueVO,
reporter,
} from '@coze-workflow/base';
import { type GetKeyPathCtx } from '../core/types';
import { type WorkflowVariableService } from './workflow-variable-service';
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace variableUtils {
export const ASSIST_TYPE_TO_VIEW_TYPE: Record<
AssistTypeDTO,
ViewVariableType
> = {
[AssistTypeDTO.file]: ViewVariableType.File,
[AssistTypeDTO.image]: ViewVariableType.Image,
[AssistTypeDTO.doc]: ViewVariableType.Doc,
[AssistTypeDTO.code]: ViewVariableType.Code,
[AssistTypeDTO.ppt]: ViewVariableType.Ppt,
[AssistTypeDTO.txt]: ViewVariableType.Txt,
[AssistTypeDTO.excel]: ViewVariableType.Excel,
[AssistTypeDTO.audio]: ViewVariableType.Audio,
[AssistTypeDTO.zip]: ViewVariableType.Zip,
[AssistTypeDTO.video]: ViewVariableType.Video,
[AssistTypeDTO.svg]: ViewVariableType.Svg,
[AssistTypeDTO.voice]: ViewVariableType.Voice,
[AssistTypeDTO.time]: ViewVariableType.Time,
};
export const VIEW_TYPE_TO_ASSIST_TYPE: Partial<
Record<ViewVariableType, AssistTypeDTO>
> = Object.entries(ASSIST_TYPE_TO_VIEW_TYPE).reduce((acc, [key, value]) => {
acc[value] = Number(key);
return acc;
}, {});
/**
* 转换处 list 之外的类型
* @param type·
* @private
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
export function DTOTypeToViewType(
type: VariableTypeDTO,
{
arrayItemType,
assistType,
}: {
arrayItemType?: VariableTypeDTO;
assistType?: AssistTypeDTO;
} = {},
): ViewVariableType {
switch (type) {
case VariableTypeDTO.boolean:
return ViewVariableType.Boolean;
case VariableTypeDTO.float:
return ViewVariableType.Number;
case VariableTypeDTO.integer:
return ViewVariableType.Integer;
case VariableTypeDTO.string:
if (assistType) {
const targetType = ASSIST_TYPE_TO_VIEW_TYPE[assistType];
if (targetType) {
return targetType;
}
}
return ViewVariableType.String;
case VariableTypeDTO.object:
return ViewVariableType.Object;
// 原后端 type: image 兼容
case VariableTypeDTO.image:
return ViewVariableType.Image;
case VariableTypeDTO.list:
if (!arrayItemType) {
throw new Error(
`Unkown variable DTO list need sub type but get ${arrayItemType}`,
);
}
switch (arrayItemType) {
case VariableTypeDTO.boolean:
return ViewVariableType.ArrayBoolean;
case VariableTypeDTO.float:
return ViewVariableType.ArrayNumber;
case VariableTypeDTO.integer:
return ViewVariableType.ArrayInteger;
case VariableTypeDTO.string:
if (assistType) {
const targetType = ASSIST_TYPE_TO_VIEW_TYPE[assistType];
if (targetType) {
return ViewVariableType.wrapToArrayType(targetType);
}
}
return ViewVariableType.ArrayString;
case VariableTypeDTO.object:
return ViewVariableType.ArrayObject;
case VariableTypeDTO.image:
return ViewVariableType.ArrayImage;
default:
throw new Error(
`Unknown variable DTO Type: ${type}:${arrayItemType}`,
);
}
default:
throw new Error(`Unknown variable DTO Type: ${type}:${arrayItemType}`);
}
}
export function viewTypeToDTOType(type: ViewVariableType): {
type: VariableTypeDTO;
subType?: VariableTypeDTO;
assistType?: AssistTypeDTO;
subAssistType?: AssistTypeDTO;
} {
// 如果是数组类型的变量
if (ViewVariableType.isArrayType(type)) {
const subViewType = ViewVariableType.getArraySubType(type);
const { type: subType, assistType: subAssistType } =
viewTypeToDTOType(subViewType);
return {
type: VariableTypeDTO.list,
subType,
subAssistType,
};
}
// AssistType 映射
const assistType = VIEW_TYPE_TO_ASSIST_TYPE[type];
if (assistType) {
return {
type: VariableTypeDTO.string,
assistType: Number(assistType) as AssistTypeDTO,
};
}
// 普通类型映射
switch (type) {
case ViewVariableType.String:
return { type: VariableTypeDTO.string };
case ViewVariableType.Integer:
return { type: VariableTypeDTO.integer };
case ViewVariableType.Number:
return { type: VariableTypeDTO.float };
case ViewVariableType.Boolean:
return { type: VariableTypeDTO.boolean };
case ViewVariableType.Object:
return { type: VariableTypeDTO.object };
// case ViewVariableType.Image:
// // return { type: VariableTypeDTO.image };
default:
throw new Error(`Unkonwn variable view type: ${type}`);
}
}
export const DEFAULT_OUTPUT_NAME = {
[BatchMode.Batch]: 'outputList',
[BatchMode.Single]: 'output',
};
export const ARRAY_TYPES = ViewVariableType.ArrayTypes;
/**
* 校验下Meta合法性不合法上报错误
* @param meta
*/
function checkDtoMetaValid(meta: VariableMetaDTO) {
if (!meta?.type) {
return;
}
// 非object和list类型schema有值的场景上报, 比如 { type: 'string', schema: []}
if (
![VariableTypeDTO.list, VariableTypeDTO.object].includes(meta.type) &&
meta.schema
) {
reporter.event({
eventName: 'workflow_invalid_variable_meta',
meta: {
name: meta.name,
},
});
}
}
/**
* 后端变量转前端变量,并补齐 key
* @param meta
*/
export function dtoMetaToViewMeta(meta: VariableMetaDTO): ViewVariableMeta {
checkDtoMetaValid(meta);
switch (meta.type) {
case VariableTypeDTO.list:
return {
key: nanoid(),
type: DTOTypeToViewType(meta.type, {
arrayItemType: meta.schema?.type,
assistType: meta.schema?.assistType,
}),
name: meta.name,
// 数组要多下钻一层
children: meta.schema?.schema?.map(subMeta =>
dtoMetaToViewMeta(subMeta),
),
required: meta.required,
description: meta.description,
readonly: meta.readonly,
defaultValue: meta.defaultValue,
};
default:
return {
key: nanoid(),
type: DTOTypeToViewType(meta.type, {
assistType: meta.assistType,
}),
name: meta.name,
children: meta.schema?.map(subMeta => dtoMetaToViewMeta(subMeta)),
required: meta.required,
description: meta.description,
readonly: meta.readonly,
defaultValue: meta.defaultValue,
};
// default:
// throw new Error(`Unknown variable type: ${meta.type}`);
}
}
export function viewMetaToDTOMeta(meta: ViewVariableMeta): VariableMetaDTO {
const { type, subType, assistType, subAssistType } = viewTypeToDTOType(
meta.type,
);
let schema: any = meta.children?.map(child => viewMetaToDTOMeta(child));
if (subType) {
if (!schema || schema.length === 0) {
// 空的object 需要加上空数组
if (subType === VariableTypeDTO.object) {
schema = [];
} else {
schema = undefined;
}
}
schema = {
type: subType,
assistType: subAssistType,
schema,
};
} else if (type === VariableTypeDTO.object && !schema) {
// 空 object 需要加上空数组
schema = [];
}
return {
type,
assistType,
name: meta.name,
schema,
readonly: meta.readonly,
required: meta.required,
description: meta.description,
defaultValue: meta.defaultValue,
};
}
/**
* @deprecated 使用 viewTypeToDTOType
* @param type
* @returns
*/
function getAssistTypeByViewType(
type?: ViewVariableType,
): AssistTypeDTO | undefined {
if (isNil(type)) {
return undefined;
}
return VIEW_TYPE_TO_ASSIST_TYPE[
ViewVariableType.isArrayType(type)
? ViewVariableType.getArraySubType(type)
: type
];
}
/**
* 前端表达式转后端数据
* @param value
*/
export function valueExpressionToDTO(
value: ValueExpression | undefined,
service: WorkflowVariableService,
ctx: GetKeyPathCtx,
): ValueExpressionDTO {
if (value?.rawMeta?.type) {
const viewType = value?.rawMeta?.type as ViewVariableType;
const {
type: dtoType,
assistType,
subType,
subAssistType,
} = viewTypeToDTOType(viewType);
if (value.type === ValueExpressionType.LITERAL) {
let schema: any = undefined;
// Array<T> 类型的 schema 指定 array 的泛型类型
if (dtoType === VariableTypeDTO.list) {
schema = {
type: subType,
assistType: subAssistType,
};
if (subType === VariableTypeDTO.object) {
schema.schema = [];
}
// object 类型的 schema 指定成空数组,字面量没有下钻字段信息
} else if (dtoType === VariableTypeDTO.object) {
schema = [];
}
// 其他基础类型string、int、number、boolean以及 image 等带 assistType 额类型,不传 schema。
const res: ValueExpressionDTO = {
type: dtoType,
assistType,
value: {
type: 'literal',
content: value.content ?? '',
rawMeta: value.rawMeta,
},
};
if (schema) {
res.schema = schema;
}
return res;
} else {
const refExpression = service.refExpressionToDTO(
value as RefExpression,
ctx,
);
let schema = subType
? {
...refExpression.schema,
type: subType,
assistType: subAssistType,
}
: refExpression.schema;
// 变量选择复杂类型再将类型手动改成简单类型会有schema残留
// 只有 object 和 list 类型才需要 schema
if (![VariableTypeDTO.object, VariableTypeDTO.list].includes(dtoType)) {
schema = undefined;
}
// rawMeta 里有类型时,使用 rawMeta 里的类型,后端会对引用变量进行类型转换
return {
type: dtoType,
assistType,
schema,
value: {
...refExpression.value,
rawMeta: value.rawMeta,
},
};
}
}
// rawMeta 不存在时,需要走兜底逻辑
if (value && value.type === ValueExpressionType.LITERAL) {
const assistType = getAssistTypeByViewType(value?.rawMeta?.type);
// TODO 这里获取不到变量类型,只能简单先这么处理,需要重构解决
if (Array.isArray(value.content)) {
const listRes: ValueExpressionDTO = {
type: 'list',
schema: {
type: 'string',
},
value: {
type: 'literal',
content: value.content ?? '',
rawMeta: value.rawMeta,
},
};
if (!isNil(assistType)) {
listRes.schema.assistType = assistType;
}
return listRes;
}
const res: ValueExpressionDTO = {
type: getLiteralExpressionValueDTOType(value.content),
value: {
type: 'literal',
content: !isNil(value.content) ? String(value.content) : '',
rawMeta: value.rawMeta,
},
};
if (!isNil(assistType)) {
res.assistType = assistType;
}
return res;
}
return service.refExpressionToDTO(value as RefExpression, ctx);
}
export function getValueExpressionViewType(
value: ValueExpression,
service: WorkflowVariableService,
ctx: GetKeyPathCtx,
): ViewVariableType | undefined {
if (ValueExpression.isEmpty(value)) {
return undefined;
}
const rawMetaType = value.rawMeta?.type;
if (rawMetaType) {
return rawMetaType;
}
if (ValueExpression.isRef(value)) {
return service.getWorkflowVariableByKeyPath(value.content?.keyPath, ctx)
?.viewType;
}
if (ValueExpression.isLiteral(value)) {
const dtoType = getLiteralExpressionValueDTOType(value.content);
return dtoType ? DTOTypeToViewType(dtoType) : undefined;
}
}
export function getValueExpressionDTOMeta(
value: ValueExpression,
service: WorkflowVariableService,
ctx: GetKeyPathCtx,
): VariableMetaDTO | undefined {
if (ValueExpression.isEmpty(value)) {
return undefined;
}
const rawMetaType = value.rawMeta?.type;
if (ValueExpression.isRef(value)) {
const workflowVariable = service.getWorkflowVariableByKeyPath(
value.content?.keyPath,
ctx,
);
const refVariableType = workflowVariable?.viewType;
// 如果 rawMetaType 不存在或者 rawMetaType 与 refVariableType 相同,则直接返回 workflowVariable?.dtoMeta
if (!rawMetaType || refVariableType === rawMetaType) {
return workflowVariable?.dtoMeta;
}
}
if (!rawMetaType) {
return undefined;
}
// 如果 rawMetaType 存在但与 refVariableType 不同,说明发生了类型转换,则需要根据 rawMetaType 转换为 VariableMetaDTO
return viewMetaToDTOMeta({
key: nanoid(),
name: String(value.content ?? ''),
type: rawMetaType,
});
}
/**
* 优先使用 literalExpression rawMeta.type 字段获取 literal 类型, 参考 variableUtils.valueExpressionToDTO
* @param content
* @returns
*/
export function getLiteralExpressionValueDTOType(
content: LiteralExpression['content'],
) {
if (isNil(content)) {
return VariableTypeDTO.string;
}
if (isInteger(content)) {
return VariableTypeDTO.integer;
} else if (isNumber(content)) {
return VariableTypeDTO.float;
} else if (isBoolean(content)) {
return VariableTypeDTO.boolean;
} else {
return VariableTypeDTO.string;
}
}
export function getLiteralValueWithType(
type: VariableTypeDTO,
content?: any,
) {
if (type === VariableTypeDTO.float || type === VariableTypeDTO.integer) {
return isNumber(Number(content)) ? Number(content) : content;
} else if (type === VariableTypeDTO.boolean) {
return ![false, 'false'].includes(content);
} else {
return content;
}
}
/**
* 后端表达式转前端数据
* @param value
*/
export function valueExpressionToVO(
value: ValueExpressionDTO,
service: WorkflowVariableService,
): ValueExpression {
// 空数据兜底
if (!value?.value?.type) {
return {} as any;
}
if (value.value.type === 'literal') {
return {
type: ValueExpressionType.LITERAL,
content: getLiteralValueWithType(
value.type as VariableTypeDTO,
value.value.content as string,
),
rawMeta: value.value.rawMeta,
};
}
const refExpression = service.refExpressionToVO(value);
refExpression.rawMeta = value.value.rawMeta;
return refExpression;
}
export function inputObjectRefToDTO(
value: InputValueVO,
service: WorkflowVariableService,
ctx: GetKeyPathCtx,
): InputValueDTO | undefined {
const schema = value.children
?.map(child => inputValueToDTO(child, service, ctx))
.filter(Boolean) as InputValueDTO[] | undefined;
const dto: InputValueDTO = {
name: value.name,
input: {
value: {
type: 'object_ref',
},
type: 'object',
schema,
},
};
return dto;
}
export function inputValueToDTO(
value: InputValueVO,
service: WorkflowVariableService,
ctx: GetKeyPathCtx,
): InputValueDTO | undefined {
if (ValueExpression.isObjectRef(value.input)) {
return inputObjectRefToDTO(value, service, ctx);
}
if (ValueExpression.isEmpty(value.input)) {
return undefined;
}
const dto: InputValueDTO = {
name: value.name,
input: valueExpressionToDTO(value.input, service, ctx),
};
return dto;
}
export function inputObjectRefToVO(
value: InputValueDTO,
service: WorkflowVariableService,
): InputValueVO {
const input: ObjectRefExpression = {
type: ValueExpressionType.OBJECT_REF,
rawMeta: { type: ViewVariableType.Object },
};
const vo: InputValueVO = {
name: value.name,
key: nanoid(),
input,
children: (value.input?.schema || [])
.map(child => inputValueToVO(child, service))
.filter(Boolean),
};
return vo;
}
export function inputValueToVO(
value: InputValueDTO,
service: WorkflowVariableService,
): InputValueVO {
if (value.input?.value?.type === 'object_ref') {
return inputObjectRefToVO(value, service);
}
const vo: InputValueVO = {
name: value.name,
input: valueExpressionToVO(value.input, service) as any,
};
return vo;
}
/**
* input-type-value 前端格式转后端格式
*/
export function inputTypeValueVOToDTO(
value: InputTypeValueVO[],
service: WorkflowVariableService,
ctx: GetKeyPathCtx,
): InputTypeValueDTO[] {
return value.map(param => {
const transType = variableUtils.viewTypeToDTOType(param.type);
return {
name: param.name,
input: variableUtils.valueExpressionToDTO(param.input, service, ctx),
type: transType.type,
};
});
}
/**
* input-type-value 后端格式转前端格式
*/
export function inputTypeValueDTOToVO(
value: InputTypeValueDTO[],
service: WorkflowVariableService,
ctx: GetKeyPathCtx,
): InputTypeValueVO[] {
return value.map(param => ({
name: param.name,
input: variableUtils.valueExpressionToVO(param.input, service),
type: variableUtils.DTOTypeToViewType(param.type),
}));
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { inject, injectable } from 'inversify';
import { EntityManager } from '@flowgram-adapter/free-layout-editor';
import { nanoid } from '@flowgram-adapter/free-layout-editor';
import { BatchMode } from '@coze-workflow/base';
import { type ViewVariableTreeNode, ViewVariableType } from '../typings';
import { WorkflowVariableService } from './workflow-variable-service';
import { variableUtils } from './variable-utils';
@injectable()
export class WorkflowBatchService {
@inject(WorkflowVariableService)
readonly variablesService: WorkflowVariableService;
@inject(EntityManager) readonly entityManager: EntityManager;
static singleOutputMetasToList(
metas: ViewVariableTreeNode[] | undefined,
): ViewVariableTreeNode[] {
const singleMetas = metas || [
WorkflowBatchService.getDefaultBatchModeOutputMeta(BatchMode.Single),
];
return [
{
key: nanoid(),
type: ViewVariableType.ArrayObject,
name: variableUtils.DEFAULT_OUTPUT_NAME[BatchMode.Batch],
children: singleMetas,
},
];
}
static listOutputMetasToSingle(
metas: ViewVariableTreeNode[] | undefined,
): ViewVariableTreeNode[] | undefined {
const listMetas = metas || [
WorkflowBatchService.getDefaultBatchModeOutputMeta(BatchMode.Batch),
];
return listMetas[0].children;
}
static getDefaultBatchModeOutputMeta = (
batchMode: BatchMode,
): ViewVariableTreeNode => {
if (batchMode === BatchMode.Batch) {
return {
key: nanoid(),
type: ViewVariableType.ArrayObject,
name: variableUtils.DEFAULT_OUTPUT_NAME[BatchMode.Batch],
children: [
{
key: nanoid(),
type: ViewVariableType.ArrayString,
name: variableUtils.DEFAULT_OUTPUT_NAME[BatchMode.Single],
},
],
};
}
if (batchMode === BatchMode.Single) {
return {
key: nanoid(),
type: ViewVariableType.String,
name: variableUtils.DEFAULT_OUTPUT_NAME[BatchMode.Single],
};
}
throw new Error('WorkflowBatchService Error: Unknown batchMode');
};
}

View File

@@ -0,0 +1,230 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { inject, injectable } from 'inversify';
import { EntityManager } from '@flowgram-adapter/free-layout-editor';
import { type Disposable } from '@flowgram-adapter/common';
import {
type DTODefine,
type RefExpression,
type ValueExpressionDTO,
ValueExpressionType,
type ViewVariableMeta,
ViewVariableType,
} from '@coze-workflow/base';
import { type GetKeyPathCtx } from '../core/types';
import { type WorkflowVariable, WorkflowVariableFacadeService } from '../core';
import { type GlobalVariableKey, isGlobalVariableKey } from '../constants';
/**
* 变量相关服务
*/
@injectable()
export class WorkflowVariableService {
@inject(EntityManager) protected readonly entityManager: EntityManager;
@inject(WorkflowVariableFacadeService)
protected readonly variableFacadeService: WorkflowVariableFacadeService;
/**
* 表达式引用转后端,如果无数据,默认给一个空的 ref 引用
* 输入:
* {
* type: ValueExpressionType.REF,
* content: {
* keyPath: ['nodeId', 'xxx', 'xxx']
* }
*
* }
*/
refExpressionToDTO(
refExpression: RefExpression | undefined,
ctx: GetKeyPathCtx,
): ValueExpressionDTO {
const keyPath = refExpression?.content?.keyPath || [];
const workflowVariable =
this.variableFacadeService.getVariableFacadeByKeyPath(keyPath, ctx);
const dtoMeta = workflowVariable?.dtoMeta;
// 如果引用的变量,属于全局变量
if (isGlobalVariableKey(keyPath[0])) {
const path = workflowVariable
? [
...workflowVariable.parentVariables
.slice(1)
.map(_variable => {
// Hack: 全局变量特化逻辑,后端要求数组下钻把数组的下标也带上
if (
_variable.viewType &&
ViewVariableType.isArrayType(_variable.viewType)
) {
return [_variable.key, '[0]'];
}
return [_variable.key];
})
.flat(),
workflowVariable.key,
]
: // 没有 workflowVariable则直接使用原来的 keyPath
keyPath.slice(1);
return {
type: dtoMeta?.type || 'string',
schema: dtoMeta?.schema,
assistType: dtoMeta?.assistType,
value: {
type: 'ref',
content: {
source: keyPath[0] as GlobalVariableKey,
path,
blockID: '',
name: '',
},
},
};
}
return {
type: dtoMeta?.type || 'string',
schema: dtoMeta?.schema,
assistType: dtoMeta?.assistType,
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: keyPath[0] || '',
name: keyPath.slice(1).join('.'),
},
},
};
}
/**
* 表达式引用转前端
* @param value
*/
refExpressionToVO(valueDTO: ValueExpressionDTO): RefExpression {
const value = valueDTO?.value as DTODefine.RefExpression;
if (!value) {
return {
type: ValueExpressionType.REF,
content: {
keyPath: [],
},
};
}
if (value.content?.source?.startsWith('global_variable_')) {
const { source, path } =
(value.content as {
source: `global_variable_${string}`;
path: string[];
}) || {};
return {
type: ValueExpressionType.REF,
content: {
keyPath: [
source,
// Hack: 全局变量特化逻辑,后端要求数组下钻把数组的下标也带上,前端不需要 [0] 下钻,因此转化时过滤掉
...(path || []).filter(_v => !['[0]'].includes(_v)),
],
},
};
}
const name = value.content?.name || '';
const nameList = name.split('.').filter(Boolean); // 过滤空字符串
// 灰度命中时,直接使用 namePath
return {
type: ValueExpressionType.REF,
content: {
keyPath: [value.content?.blockID || '', ...nameList],
},
};
}
/**
* 直接返回 Variable 或者 SubVariable 的 ViewVariableMeta
* @param keyPath ViewVariableMeta 的 keyPath 路径
* @returns
*/
getViewVariableByKeyPath(
keyPath: string[] | undefined,
ctx: GetKeyPathCtx,
): ViewVariableMeta | null {
return (
this.variableFacadeService.getVariableFacadeByKeyPath(keyPath, ctx)
?.viewMeta || null
);
}
getWorkflowVariableByKeyPath(
keyPath: string[] | undefined,
ctx: GetKeyPathCtx,
): WorkflowVariable | undefined {
return this.variableFacadeService.getVariableFacadeByKeyPath(keyPath, ctx);
}
/**
* 监听指定 keyPath 变量类型变化
* @param keyPath
* @param cb
* @returns
*/
onListenVariableTypeChange(
keyPath: string[],
cb: (v?: ViewVariableMeta | null) => void,
ctx: GetKeyPathCtx,
): Disposable {
return this.variableFacadeService.listenKeyPathTypeChange(keyPath, cb, ctx);
}
/**
* @deprecated 变量销毁存在部分 Bad Case
* - 全局变量因切换 Project 销毁后,变量引用会被置空,导致变量引用失效
*
* 监听指定 keyPath 变量类型变化
* @param keyPath
* @param cb
* @returns
*/
onListenVariableDispose(
keyPath: string[],
cb: () => void,
ctx: GetKeyPathCtx,
): Disposable {
return this.variableFacadeService.listenKeyPathDispose(keyPath, cb, ctx);
}
/**
* 监听指定 keyPath 变量的变化
* @param keyPath
* @param cb
* @returns
*/
onListenVariableChange(
keyPath: string[],
cb: (v?: ViewVariableMeta | null) => void,
ctx: GetKeyPathCtx,
): Disposable {
return this.variableFacadeService.listenKeyPathVarChange(keyPath, cb, ctx);
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { inject, injectable } from 'inversify';
import { type WorkflowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import { type RefExpression } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { WorkflowVariableFacadeService } from '../core';
import { WorkflowVariableService } from './workflow-variable-service';
@injectable()
export class WorkflowVariableValidationService {
@inject(WorkflowVariableService)
protected readonly variableService: WorkflowVariableService;
@inject(WorkflowVariableFacadeService)
protected readonly variableFacadeService: WorkflowVariableFacadeService;
isRefVariableEligible(value: RefExpression, node: WorkflowNodeEntity) {
const variable = this.variableFacadeService.getVariableFacadeByKeyPath(
value?.content?.keyPath,
{ node },
);
if (!variable || !variable.canAccessByNode(node.id)) {
return I18n.t('workflow_detail_variable_referenced_error');
}
return;
}
}

View File

@@ -0,0 +1,249 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/naming-convention */
import { isArray } from 'lodash-es';
import { inject, injectable, postConstruct, preDestroy } from 'inversify';
import {
ASTFactory,
type ASTNodeJSON,
type PropertyJSON,
type Scope,
VariableEngine,
} from '@flowgram-adapter/free-layout-editor';
import { DisposableCollection, Emitter } from '@flowgram-adapter/common';
import {
VariableChannel,
VariableConnector,
type Variable as GlobalVariableType,
} from '@coze-arch/bot-api/memory';
import { MemoryApi } from '@coze-arch/bot-api';
import { safeJSONParse } from '../utils/json';
import {
GlobalVariableKey,
allGlobalVariableKeys,
GLOBAL_VARIABLE_SCOPE_ID,
} from '../constants';
type FetchGlobalVariablesType = Partial<
Record<GlobalVariableKey, PropertyJSON[]>
>;
interface GlobalVariableTreeNode {
name?: string;
type?: string;
schema?: GlobalVariableTreeNode;
readonly?: boolean;
}
export interface State {
type?: 'project' | 'bot';
id?: string;
}
@injectable()
export class GlobalVariableService {
@inject(VariableEngine) variableEngine: VariableEngine;
protected onLoadedEmitter = new Emitter<void>();
protected onBeforeLoadEmitter = new Emitter<void>();
protected toDispose = new DisposableCollection();
protected globalScope: Scope;
protected connectorTypeMapping = {
bot: VariableConnector.Bot,
project: VariableConnector.Project,
};
protected variableChannelMapping = {
[GlobalVariableKey.System]: VariableChannel.System,
[GlobalVariableKey.User]: VariableChannel.Custom,
[GlobalVariableKey.App]: VariableChannel.APP,
};
onLoaded = this.onLoadedEmitter.event;
onBeforeLoad = this.onBeforeLoadEmitter.event;
protected _state: State = {};
get state(): State {
return this._state;
}
/**
* 拉取最新的变量数据
*/
protected async fetchGlobalVariableMetas(
connectorType: 'project' | 'bot',
connectorId?: string,
): Promise<FetchGlobalVariablesType> {
try {
const res = await MemoryApi.GetMemoryVariableMeta({
ConnectorID: connectorId,
ConnectorType: this.connectorTypeMapping[connectorType],
});
return {
[GlobalVariableKey.System]: this.parseGlobalVariableList(
res?.VariableMap?.[VariableChannel.System],
),
[GlobalVariableKey.App]: this.parseGlobalVariableList(
res?.VariableMap?.[VariableChannel.APP],
),
[GlobalVariableKey.User]: this.parseGlobalVariableList(
res?.VariableMap?.[VariableChannel.Custom],
),
};
} catch (err) {
console.error(err);
return Promise.resolve({});
}
}
_latestFetchId = 0;
/**
* 触发刷新事件
*/
async loadGlobalVariables(
connectorType: 'project' | 'bot',
connectorId?: string,
) {
if (!connectorId) {
return;
}
const fetchId = ++this._latestFetchId;
this._state = Object.assign(this._state, {
type: connectorType,
id: connectorId,
});
this.onBeforeLoadEmitter.fire();
const res = await this.fetchGlobalVariableMetas(connectorType, connectorId);
// 有新的请求,则直接丢弃结果
if (fetchId !== this._latestFetchId) {
return;
}
// 同步最新的变量数据到 AST
allGlobalVariableKeys.forEach(_key => {
if (!res[_key]?.length) {
this.globalScope.ast.remove(_key);
return;
}
this.globalScope.ast.set(
_key,
ASTFactory.createVariableDeclaration({
key: _key,
type: ASTFactory.createObject({
properties: (res[_key] || []).filter(Boolean),
}),
}),
);
});
this.onLoadedEmitter.fire();
}
@postConstruct()
init() {
this.globalScope = this.variableEngine.createScope(
GLOBAL_VARIABLE_SCOPE_ID,
);
}
protected parseGlobalVariableList(
vList?: GlobalVariableType[],
): PropertyJSON[] {
if (!vList?.length) {
return [];
}
return vList.map(_v =>
this.createASTPropertyFromGlobalVariableSchema({
name: _v.Keyword,
readonly: _v.IsReadOnly,
...safeJSONParse(_v.Schema),
}),
);
}
protected createASTPropertyFromGlobalVariableSchema(
globalVariable: GlobalVariableTreeNode,
): PropertyJSON {
const { name, readonly, type, schema } = globalVariable;
return ASTFactory.createProperty({
key: name || '',
meta: { readonly },
type: this.createASTTypeFromGlobalVariableType(type || '', schema),
});
}
protected createASTTypeFromGlobalVariableType(
type: string,
schema?: GlobalVariableTreeNode,
): ASTNodeJSON | undefined {
// 参考协议:
switch (type) {
case 'string':
return ASTFactory.createString();
case 'boolean':
return ASTFactory.createBoolean();
case 'integer':
return ASTFactory.createInteger();
case 'float':
case 'number': // number 为历史数据,标准为 float
return ASTFactory.createNumber();
case 'object':
return ASTFactory.createObject({
properties: isArray(schema)
? schema.map(_schema =>
this.createASTPropertyFromGlobalVariableSchema(_schema),
)
: [],
});
case 'list':
return ASTFactory.createArray({
items: this.createASTTypeFromGlobalVariableType(
schema?.type || '',
schema?.schema,
),
});
default:
return;
}
}
@preDestroy()
dispose() {
this.toDispose.dispose();
this.onLoadedEmitter.dispose();
}
}

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { GlobalVariableService } from './global-variable-service';

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type VariableProviderAbilityOptions } from '@flowgram-adapter/free-layout-editor';
import { type ViewVariableType } from '@coze-workflow/base';
export {
ValueExpressionType,
ViewVariableType,
type ViewVariableMeta,
type ViewVariableTreeNode,
type ValueExpression,
type ValueExpressionDTO,
type RefExpression,
} from '@coze-workflow/base';
export type VariableProviderParser = VariableProviderAbilityOptions['parse'];
export interface TypeDefinition {
type: ViewVariableType;
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { set } from 'lodash-es';
import {
type FormModelV2,
isFormV2,
type FlowNodeEntity,
} from '@flowgram-adapter/free-layout-editor';
import { FlowNodeFormData } from '@flowgram-adapter/free-layout-editor';
export function setValueIn(
node: FlowNodeEntity,
path: string,
nextValue: unknown,
) {
const formData = node.getData(FlowNodeFormData);
// 新表单引擎更新数据
if (isFormV2(node)) {
(formData.formModel as FormModelV2).setValueIn(path, nextValue);
return;
}
// 老表单引擎更新数据
const fullData = formData.formModel.getFormItemValueByPath('/');
set(fullData, path, nextValue);
return;
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/naming-convention */
import { type SchemaObject } from 'ajv';
import {
VariableTypeDTO,
type VariableMetaDTO,
AssistTypeDTO,
} from '@coze-workflow/base';
// 需要转化的类型映射
const VariableType2JsonSchemaProps = {
[VariableTypeDTO.object]: {
type: 'object',
},
[VariableTypeDTO.list]: {
type: 'array',
},
[VariableTypeDTO.float]: {
type: 'number',
},
[VariableTypeDTO.integer]: {
type: 'integer',
},
[VariableTypeDTO.boolean]: {
type: 'boolean',
},
[VariableTypeDTO.string]: {
type: 'string',
},
[VariableTypeDTO.time]: {
type: 'string',
},
};
const inputToJsonSchema = (
input,
level = 0,
transformer?: (input: unknown) => VariableMetaDTO,
): SchemaObject | undefined => {
const _input = transformer ? transformer(input) : input;
const { type, description } = _input;
const props = VariableType2JsonSchemaProps[type];
if (type === VariableTypeDTO.object) {
const properties = {};
const required: string[] = [];
for (const field of _input.schema) {
properties[field.name] = inputToJsonSchema(field, level + 1, transformer);
if (field.required) {
required.push(field.name);
}
}
return {
...props,
description,
required,
properties,
};
} else if (type === VariableTypeDTO.list) {
return {
...props,
description,
items: inputToJsonSchema(_input.schema, level + 1, transformer),
};
}
// 基础类型不需要生成jsonSchema, 图片类型不需要jsonSchema, 直接抛异常跳出递归
if (
level === 0 ||
type === 'image' ||
(_input.assistType && _input.assistType !== AssistTypeDTO.time)
) {
throw Error('not json type');
}
return { ...props, description };
};
export const generateInputJsonSchema = (
input: VariableMetaDTO,
transformer?: (input: unknown) => VariableMetaDTO,
): SchemaObject | undefined => {
try {
const jsonSchema = inputToJsonSchema(input, 0, transformer);
return jsonSchema;
} catch {
return undefined;
}
};

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export function safeJSONParse(jsonStr?: string) {
try {
if (!jsonStr) {
return {};
}
return JSON.parse(jsonStr);
} catch (e) {
return {};
}
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FormPathService } from '@flowgram-adapter/free-layout-editor';
export function convertGlobPath(path: string) {
if (path.startsWith('/')) {
const parts = FormPathService.normalize(path).slice(1).split('/');
return parts.join('.');
}
return path;
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
FlowNodeVariableData,
type Scope,
} from '@flowgram-adapter/free-layout-editor';
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import { type WorkflowNodeMeta } from '@flowgram-adapter/free-layout-editor';
/**
* 获取实际的父节点
* @param node
* @returns
*/
export function getParentNode(
node: FlowNodeEntity,
): FlowNodeEntity | undefined {
const initParent = node.document.originTree.getParent(node);
if (!initParent) {
return initParent;
}
const nodeMeta = initParent.getNodeMeta<WorkflowNodeMeta>();
const subCanvas = nodeMeta.subCanvas?.(initParent);
if (subCanvas?.isCanvas) {
return subCanvas.parentNode;
}
return initParent;
}
/**
* 获取实际的子节点
* @param node
* @returns
*/
export function getChildrenNode(node: FlowNodeEntity): FlowNodeEntity[] {
const nodeMeta = node.getNodeMeta<WorkflowNodeMeta>();
const subCanvas = nodeMeta.subCanvas?.(node);
if (subCanvas) {
// 子画布本身不存在 children
if (subCanvas.isCanvas) {
return [];
} else {
return subCanvas.canvasNode.collapsedChildren;
}
}
return node.document.originTree.getChildren(node);
}
/**
* 节点是否包含子画布
* @param node
* @returns
*/
export function hasChildCanvas(node: FlowNodeEntity): boolean {
const nodeMeta = node.getNodeMeta<WorkflowNodeMeta>();
const subCanvas = nodeMeta.subCanvas?.(node);
return !!subCanvas?.canvasNode;
}
/**
* 获取子节点所有输出变量的作用域链
* @param node
* @returns
*/
export function getHasChildCanvasNodePublicDeps(
node: FlowNodeEntity,
includePrivate = true,
): Scope[] {
const _private = node.getData(FlowNodeVariableData)?.private;
return getChildrenNode(node)
.map(_node => _node.getData(FlowNodeVariableData).public)
.concat(_private && includePrivate ? [_private] : []);
}
/**
* 获取父节点的
* @param node
* @returns
*/
export function getParentPublic(node: FlowNodeEntity): Scope | undefined {
return getParentNode(node)?.getData(FlowNodeVariableData)?.public;
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type VariableProviderAbilityOptions } from '@flowgram-adapter/free-layout-editor';
import {
ASTKind,
DataEvent,
type FlowNodeEntity,
FlowNodeVariableData,
type Effect,
type Scope,
type EffectOptions,
} from '@flowgram-adapter/free-layout-editor';
/**
* 根据 VariableProvider 生成 FormV2 的 Effect
* @param options
* @returns
*/
export function createEffectFromVariableProvider(
options: VariableProviderAbilityOptions,
): EffectOptions[] {
const getScope = (node: FlowNodeEntity): Scope => {
const variableData: FlowNodeVariableData =
node.getData(FlowNodeVariableData);
if (options.private) {
return variableData.initPrivate();
}
return variableData.public;
};
const transformValueToAST: Effect = ({ value, context }) => {
if (!context) {
return;
}
const { node } = context;
const scope = getScope(node);
const defaultNamespace = options.private ? '/node/locals' : '/node/outputs';
scope.ast.set(options.namespace || defaultNamespace, {
kind: ASTKind.VariableDeclarationList,
declarations: options.parse(value, {
node,
scope,
options,
formItem: undefined,
}),
});
};
return [
{
event: DataEvent.onValueInit,
effect: (params => {
const { context } = params;
const scope = getScope(context.node);
const disposable = options.onInit?.({
node: context.node,
scope,
options,
formItem: undefined,
// @ts-expect-error 新表单引擎不支持
triggerSync: undefined,
});
if (disposable) {
// 作用域销毁时同时销毁该监听
scope.toDispose.push(disposable);
}
transformValueToAST(params);
}) as Effect,
},
{
event: DataEvent.onValueChange,
effect: (params => {
transformValueToAST(params);
}) as Effect,
},
];
}