feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
81
frontend/packages/workflow/variable/README.md
Normal file
81
frontend/packages/workflow/variable/README.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# @coze-workflow/variable
|
||||
|
||||
workflow 变量包
|
||||
|
||||
## Overview
|
||||
|
||||
This package is part of the Coze Studio monorepo and provides workflow functionality. It includes component, hook, adapter and more.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installation
|
||||
|
||||
Add this package to your `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@coze-workflow/variable": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
rush update
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```typescript
|
||||
import { /* exported functions/components */ } from '@coze-workflow/variable';
|
||||
|
||||
// Example usage
|
||||
// TODO: Add specific usage examples
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Component
|
||||
- Hook
|
||||
- Adapter
|
||||
- Service
|
||||
- Editor
|
||||
- Plugin
|
||||
|
||||
## API Reference
|
||||
|
||||
### Exports
|
||||
|
||||
- `FlowNodeVariableData`
|
||||
- `*`
|
||||
- `*`
|
||||
- `*`
|
||||
- `*`
|
||||
- `*`
|
||||
- `*`
|
||||
- `*`
|
||||
- `*`
|
||||
- `*`
|
||||
|
||||
*And more...*
|
||||
|
||||
For detailed API documentation, please refer to the TypeScript definitions.
|
||||
|
||||
## Development
|
||||
|
||||
This package is built with:
|
||||
|
||||
- TypeScript
|
||||
- React
|
||||
|
||||
- ESLint for code quality
|
||||
|
||||
## Contributing
|
||||
|
||||
This package is part of the Coze Studio monorepo. Please follow the monorepo contribution guidelines.
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
@@ -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 { ContainerModule, injectable } from 'inversify';
|
||||
import {
|
||||
type PlaygroundContext,
|
||||
type FlowNodeEntity,
|
||||
type EntityManager,
|
||||
FlowDocumentContribution,
|
||||
EntityManagerContribution,
|
||||
createFreeHistoryPlugin,
|
||||
FormModelV2,
|
||||
FlowNodeFormData,
|
||||
createNodeContainerModules,
|
||||
createNodeEntityDatas,
|
||||
FlowDocumentContainerModule,
|
||||
PlaygroundMockTools,
|
||||
Playground,
|
||||
loadPlugins,
|
||||
bindContributions,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
type WorkflowDocument,
|
||||
WorkflowDocumentContainerModule,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { createWorkflowVariablePlugins } from '../src';
|
||||
import { MockNodeRegistry } from './node.mock';
|
||||
|
||||
@injectable()
|
||||
export class MockPlaygroundContext implements PlaygroundContext {
|
||||
getNodeTemplateInfoByType() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class MockWorkflowForm
|
||||
implements FlowDocumentContribution, EntityManagerContribution
|
||||
{
|
||||
registerDocument(document: WorkflowDocument): void {
|
||||
document.registerNodeDatas(...createNodeEntityDatas());
|
||||
}
|
||||
|
||||
registerEntityManager(entityManager: EntityManager): void {
|
||||
const formModelFactory = (entity: FlowNodeEntity) =>
|
||||
new FormModelV2(entity);
|
||||
entityManager.registerEntityData(
|
||||
FlowNodeFormData,
|
||||
() =>
|
||||
({
|
||||
formModelFactory,
|
||||
}) as any,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const testModule = new ContainerModule(bind => {
|
||||
bindContributions(bind, MockWorkflowForm, [
|
||||
FlowDocumentContribution,
|
||||
EntityManagerContribution,
|
||||
]);
|
||||
bindContributions(bind, MockNodeRegistry, [FlowDocumentContribution]);
|
||||
});
|
||||
|
||||
export function createContainer() {
|
||||
const container = PlaygroundMockTools.createContainer([
|
||||
FlowDocumentContainerModule,
|
||||
WorkflowDocumentContainerModule,
|
||||
...createNodeContainerModules(),
|
||||
testModule,
|
||||
]);
|
||||
const playground = container.get(Playground);
|
||||
|
||||
loadPlugins(
|
||||
[
|
||||
createFreeHistoryPlugin({ enable: true, limit: 50 }),
|
||||
...createWorkflowVariablePlugins(),
|
||||
],
|
||||
container,
|
||||
);
|
||||
playground.init();
|
||||
return container;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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 {
|
||||
WorkflowNodeInputVariablesData,
|
||||
WorkflowNodeOutputVariablesData,
|
||||
WorkflowNodeRefVariablesData,
|
||||
} from 'src';
|
||||
import { loopJSON } from '__tests__/workflow.mock';
|
||||
import { createContainer } from '__tests__/create-container';
|
||||
import { WorkflowDocument } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
describe('test workflow node variables entity data', () => {
|
||||
let workflowDocument: WorkflowDocument;
|
||||
|
||||
beforeEach(async () => {
|
||||
const container = createContainer();
|
||||
workflowDocument = container.get<WorkflowDocument>(WorkflowDocument);
|
||||
|
||||
await workflowDocument.fromJSON(loopJSON);
|
||||
});
|
||||
|
||||
test('test workflow-node-input-variables-data', () => {
|
||||
const nodes = workflowDocument.getAllNodes();
|
||||
|
||||
expect(
|
||||
nodes.map(_node => {
|
||||
const data = _node.getData(WorkflowNodeInputVariablesData);
|
||||
|
||||
return [
|
||||
_node.id,
|
||||
data.inputParameters,
|
||||
data.inputVariables.map(_input => _input.refVariable?.viewType),
|
||||
];
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('test workflow-node-output-variables-data', () => {
|
||||
const nodes = workflowDocument.getAllNodes();
|
||||
|
||||
expect(
|
||||
nodes.map(_node => {
|
||||
const data = _node.getData(WorkflowNodeOutputVariablesData);
|
||||
|
||||
return [
|
||||
_node.id,
|
||||
data.variables.map(
|
||||
_variable => data.getVariableByKey(_variable.key)?.viewType,
|
||||
),
|
||||
];
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('test workflow-node-ref-variables-data', () => {
|
||||
const nodes = workflowDocument.getAllNodes();
|
||||
|
||||
expect(
|
||||
nodes.map(_node => {
|
||||
const data = _node.getData(WorkflowNodeRefVariablesData);
|
||||
|
||||
return [
|
||||
_node.id,
|
||||
data.refs,
|
||||
Object.values(data.refVariables).map(_ref => _ref?.viewType),
|
||||
data.hasGlobalRef,
|
||||
];
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('test batchUpdateRefs', () => {
|
||||
const endNode = workflowDocument.getNode('end');
|
||||
|
||||
const data = endNode?.getData(WorkflowNodeRefVariablesData);
|
||||
|
||||
data?.batchUpdateRefs([
|
||||
{
|
||||
beforeKeyPath: ['llm_0'],
|
||||
afterKeyPath: ['llm_test'],
|
||||
},
|
||||
{
|
||||
beforeKeyPath: ['start', 'Object'],
|
||||
afterKeyPath: ['start', 'Drilldown', 'Object'],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(data?.refs).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -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 default {};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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-params */
|
||||
import { provideNodeOutputVariables } from 'src/form-extensions/variable-providers/provide-node-output-variables';
|
||||
import { provideNodeBatchVariables } from 'src/form-extensions/variable-providers/provide-node-batch-variables';
|
||||
import { provideMergeGroupVariables } from 'src/form-extensions/variable-providers/provide-merge-group-variables';
|
||||
import { provideLoopOutputsVariables } from 'src/form-extensions/variable-providers/provide-loop-output-variables';
|
||||
import { provideLoopInputsVariables } from 'src/form-extensions/variable-providers/provide-loop-input-variables';
|
||||
import { type VariableProviderAbilityOptions } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import {
|
||||
mockFullOutputs,
|
||||
mockRefInputs,
|
||||
mockMergeGroup,
|
||||
} from '../variable.mock';
|
||||
|
||||
const createCase = <V>(
|
||||
provider: VariableProviderAbilityOptions<V>,
|
||||
value: V,
|
||||
context: any = { node: { id: '12345' } },
|
||||
extraInfo = '',
|
||||
): {
|
||||
key: string;
|
||||
parse: (input: any, context: any) => any;
|
||||
value: V;
|
||||
context: any;
|
||||
extraInfo: string;
|
||||
} => ({
|
||||
key: provider.key || '',
|
||||
parse: provider.parse,
|
||||
value,
|
||||
context,
|
||||
extraInfo,
|
||||
});
|
||||
|
||||
describe('test variable providers', () => {
|
||||
test.each([
|
||||
createCase(provideLoopInputsVariables, {
|
||||
inputParameters: mockRefInputs,
|
||||
variableParameters: mockRefInputs,
|
||||
}),
|
||||
createCase(provideLoopOutputsVariables, mockRefInputs),
|
||||
createCase(provideMergeGroupVariables, mockMergeGroup),
|
||||
createCase(provideNodeBatchVariables, mockRefInputs, {
|
||||
node: { id: '123456' },
|
||||
formItem: { formModel: { getFormItemValueByPath: path => 'batch' } },
|
||||
}),
|
||||
createCase(provideNodeOutputVariables, mockFullOutputs),
|
||||
createCase(
|
||||
provideNodeBatchVariables,
|
||||
mockRefInputs,
|
||||
{
|
||||
node: { id: '123456' },
|
||||
formItem: {
|
||||
formModel: { getFormItemValueByPath: path => 'not_batch' },
|
||||
},
|
||||
},
|
||||
"shouldn't provide batch variables",
|
||||
),
|
||||
createCase(
|
||||
provideNodeBatchVariables,
|
||||
mockRefInputs,
|
||||
{
|
||||
node: { id: '123456' },
|
||||
formItem: {
|
||||
formModel: {
|
||||
getFormItemValueByPath: path =>
|
||||
path === '/batchMode' ? undefined : 'batch',
|
||||
},
|
||||
},
|
||||
},
|
||||
'batchMode in /inputs/batchMode',
|
||||
),
|
||||
])('test variable provider: $key $extraInfo', ({ parse, value, context }) => {
|
||||
expect(parse?.(value, context)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
86
frontend/packages/workflow/variable/__tests__/node.mock.tsx
Normal file
86
frontend/packages/workflow/variable/__tests__/node.mock.tsx
Normal 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 React from 'react';
|
||||
|
||||
import { createEffectFromVariableProvider } from 'src/utils/variable-provider';
|
||||
import { provideNodeOutputVariables } from 'src/form-extensions/variable-providers/provide-node-output-variables';
|
||||
import { provideLoopOutputsVariables } from 'src/form-extensions/variable-providers/provide-loop-output-variables';
|
||||
import { provideLoopInputsVariables } from 'src/form-extensions/variable-providers/provide-loop-input-variables';
|
||||
import { provideMergeGroupVariablesEffect } from 'src';
|
||||
import { injectable } from 'inversify';
|
||||
import {
|
||||
type WorkflowDocument,
|
||||
type FlowDocumentContribution,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { StandardNodeType } from '@coze-workflow/base/types';
|
||||
|
||||
@injectable()
|
||||
export class MockNodeRegistry implements FlowDocumentContribution {
|
||||
registerDocument(document: WorkflowDocument): void {
|
||||
// Register Nodes
|
||||
document.registerFlowNodes({
|
||||
type: StandardNodeType.LLM,
|
||||
formMeta: {
|
||||
render: () => <div></div>,
|
||||
effect: {
|
||||
outputs: createEffectFromVariableProvider(provideNodeOutputVariables),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
document.registerFlowNodes({
|
||||
type: StandardNodeType.Start,
|
||||
formMeta: {
|
||||
render: () => <div></div>,
|
||||
effect: {
|
||||
outputs: createEffectFromVariableProvider(provideNodeOutputVariables),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
document.registerFlowNodes({
|
||||
type: StandardNodeType.End,
|
||||
// getNodeInputParameters: () => [...allEndRefInputs, ...allConstantInputs],
|
||||
formMeta: {
|
||||
render: () => <div></div>,
|
||||
},
|
||||
});
|
||||
|
||||
document.registerFlowNodes({
|
||||
type: StandardNodeType.Loop,
|
||||
formMeta: {
|
||||
render: () => <div></div>,
|
||||
effect: {
|
||||
inputs: createEffectFromVariableProvider(provideLoopInputsVariables),
|
||||
outputs: createEffectFromVariableProvider(
|
||||
provideLoopOutputsVariables,
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
document.registerFlowNodes({
|
||||
type: StandardNodeType.VariableMerge,
|
||||
formMeta: {
|
||||
render: () => <div></div>,
|
||||
effect: {
|
||||
groups: provideMergeGroupVariablesEffect,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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 { WorkflowVariableService } from 'src';
|
||||
import { loopJSON } from '__tests__/workflow.mock';
|
||||
import { allKeyPaths } from '__tests__/variable.mock';
|
||||
import { createContainer } from '__tests__/create-container';
|
||||
import { WorkflowDocument } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
describe('test variable service', () => {
|
||||
let workflowDocument: WorkflowDocument;
|
||||
let variableService: WorkflowVariableService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const container = createContainer();
|
||||
workflowDocument = container.get<WorkflowDocument>(WorkflowDocument);
|
||||
variableService = container.get<WorkflowVariableService>(
|
||||
WorkflowVariableService,
|
||||
);
|
||||
|
||||
await workflowDocument.fromJSON(loopJSON);
|
||||
});
|
||||
|
||||
test('test get variable', () => {
|
||||
expect(
|
||||
allKeyPaths.map(_case => {
|
||||
const workflowVariable = variableService.getWorkflowVariableByKeyPath(
|
||||
_case,
|
||||
{},
|
||||
);
|
||||
|
||||
if (!workflowVariable) {
|
||||
return [variableService.getViewVariableByKeyPath(_case, {})];
|
||||
}
|
||||
|
||||
return [
|
||||
variableService.getViewVariableByKeyPath(_case, {}),
|
||||
workflowVariable.viewType,
|
||||
workflowVariable.renderType,
|
||||
workflowVariable.dtoMeta,
|
||||
workflowVariable.refExpressionDTO,
|
||||
workflowVariable.key,
|
||||
workflowVariable.parentVariables.map(_field => _field.key),
|
||||
workflowVariable.children.map(_field => _field.key),
|
||||
workflowVariable.expressionPath,
|
||||
workflowVariable.groupInfo,
|
||||
workflowVariable.node.id,
|
||||
];
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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 { variableUtils, WorkflowVariableService } from 'src';
|
||||
import { loopJSON } from '__tests__/workflow.mock';
|
||||
import { allConstantInputs, allEndRefInputs } from '__tests__/variable.mock';
|
||||
import { createContainer } from '__tests__/create-container';
|
||||
import { WorkflowDocument } from '@flowgram-adapter/free-layout-editor';
|
||||
import { ValueExpression } from '@coze-workflow/base';
|
||||
|
||||
import { mockFullOutputs } from './../variable.mock';
|
||||
|
||||
describe('test variable utils', () => {
|
||||
let workflowDocument: WorkflowDocument;
|
||||
let variableService: WorkflowVariableService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const container = createContainer();
|
||||
workflowDocument = container.get<WorkflowDocument>(WorkflowDocument);
|
||||
variableService = container.get<WorkflowVariableService>(
|
||||
WorkflowVariableService,
|
||||
);
|
||||
|
||||
await workflowDocument.fromJSON(loopJSON);
|
||||
});
|
||||
|
||||
test('Ref VO DTO convert', () => {
|
||||
const endNode = workflowDocument.getNode('end')!;
|
||||
|
||||
const allEndInputs = [...allEndRefInputs, ...allConstantInputs];
|
||||
|
||||
const dto = allEndInputs.map(_input =>
|
||||
variableUtils.inputValueToDTO(_input, variableService, {
|
||||
node: endNode,
|
||||
}),
|
||||
);
|
||||
expect(dto).toMatchSnapshot();
|
||||
|
||||
const voBack = dto
|
||||
.filter(Boolean)
|
||||
.map(_dto => variableUtils.inputValueToVO(_dto!, variableService));
|
||||
|
||||
const availableEndInputs = allEndInputs.filter(
|
||||
_ref => !ValueExpression.isEmpty(_ref.input),
|
||||
);
|
||||
|
||||
expect(availableEndInputs.length).toBeLessThan(allEndInputs.length);
|
||||
expect(voBack.length).toBe(availableEndInputs.length);
|
||||
voBack.forEach((_vo, idx) => {
|
||||
expect(_vo.name).toBe(availableEndInputs[idx].name);
|
||||
});
|
||||
});
|
||||
|
||||
test('Variable VO DTO convert', () => {
|
||||
const dto = mockFullOutputs.map(_output =>
|
||||
variableUtils.viewMetaToDTOMeta(_output),
|
||||
);
|
||||
|
||||
expect(dto).toMatchSnapshot();
|
||||
|
||||
const voBack = dto.map(_dto => variableUtils.dtoMetaToViewMeta(_dto));
|
||||
|
||||
voBack.forEach((_vo, idx) => {
|
||||
expect(_vo.type).toBe(mockFullOutputs[idx].type);
|
||||
expect(_vo.name).toBe(mockFullOutputs[idx].name);
|
||||
});
|
||||
});
|
||||
});
|
||||
39
frontend/packages/workflow/variable/__tests__/setup.ts
Normal file
39
frontend/packages/workflow/variable/__tests__/setup.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 'reflect-metadata';
|
||||
vi.mock('@coze-arch/i18n', () => ({
|
||||
I18n: {
|
||||
t: vi.fn(),
|
||||
},
|
||||
}));
|
||||
vi.mock('@coze-arch/bot-flags', () => ({
|
||||
getFlags: () => ({
|
||||
'bot.automation.encapsulate': true,
|
||||
}),
|
||||
}));
|
||||
vi.mock('@coze-arch/coze-design', () => ({
|
||||
Typography: {
|
||||
Text: vi.fn(),
|
||||
},
|
||||
withField: vi.fn(),
|
||||
}));
|
||||
vi.mock('@coze-workflow/components', () => ({}));
|
||||
|
||||
vi.stubGlobal('IS_DEV_MODE', true);
|
||||
vi.stubGlobal('IS_OVERSEA', false);
|
||||
vi.stubGlobal('IS_BOE', false);
|
||||
vi.stubGlobal('REGION', 'cn');
|
||||
147
frontend/packages/workflow/variable/__tests__/variable.mock.ts
Normal file
147
frontend/packages/workflow/variable/__tests__/variable.mock.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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 { type InputItem } from 'src/form-extensions/variable-providers/common';
|
||||
import {
|
||||
ValueExpressionType,
|
||||
ViewVariableType,
|
||||
type ViewVariableMeta,
|
||||
} from 'src';
|
||||
import { type InputValueVO } from '@coze-workflow/base/types';
|
||||
|
||||
export const genInput = (name: string, keyPath: string[]): InputItem => ({
|
||||
name,
|
||||
input: {
|
||||
type: ValueExpressionType.REF,
|
||||
content: { keyPath },
|
||||
},
|
||||
});
|
||||
|
||||
export const mockKeyPaths = [
|
||||
['start', 'String'],
|
||||
['llm_0', 'Object'],
|
||||
['start', 'Object', 'Image'],
|
||||
['llm_0', 'Object', 'Number'],
|
||||
['start', 'Array<Code>'],
|
||||
['llm_0', 'Array<String>'],
|
||||
['start', 'Array<Object>', 'Number'],
|
||||
// Not existed
|
||||
['llm_0', 'Array<String>', 'Number'],
|
||||
['start', 'Object', 'Undefined'],
|
||||
[],
|
||||
];
|
||||
|
||||
export const mockMergeGroupKeyPaths = [
|
||||
['merge_0', 'test_group_1'],
|
||||
['merge_0', 'test_group_2'],
|
||||
];
|
||||
|
||||
export const mockKeyPathsInsideLoop = [
|
||||
['loop_0', 'loop_batch_llm_0__Object'],
|
||||
['loop_0', 'loop_var_llm_0__Object'],
|
||||
['loop_0', 'loop_batch_start__Array<Object>__Number'],
|
||||
['loop_0', 'loop_var_start__Array<Code>'],
|
||||
];
|
||||
|
||||
export const mockLoopOutputKeyPaths = [
|
||||
['loop_0', 'loop_output_var_string'],
|
||||
['loop_0', 'loop_output_string'],
|
||||
['loop_0', 'loop_output_image'],
|
||||
];
|
||||
|
||||
export const allKeyPaths = [
|
||||
...mockKeyPaths,
|
||||
...mockMergeGroupKeyPaths,
|
||||
...mockKeyPathsInsideLoop,
|
||||
...mockLoopOutputKeyPaths,
|
||||
];
|
||||
|
||||
export const allEndRefInputs = [
|
||||
...mockKeyPaths,
|
||||
...mockMergeGroupKeyPaths,
|
||||
...mockLoopOutputKeyPaths,
|
||||
].map(_keyPath => genInput(_keyPath.join('__'), _keyPath));
|
||||
|
||||
export const allConstantInputs: InputValueVO[] = [
|
||||
{
|
||||
name: 'test_constant_no_raw_meta',
|
||||
input: {
|
||||
type: ValueExpressionType.LITERAL,
|
||||
content: 'test_constant',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'test_constant_image_url',
|
||||
input: {
|
||||
type: ValueExpressionType.LITERAL,
|
||||
content: ['image_url'],
|
||||
rawMeta: { type: ViewVariableType.ArrayImage },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'test_constant_array_object',
|
||||
input: {
|
||||
type: ValueExpressionType.LITERAL,
|
||||
content: [{ count: 0 }],
|
||||
rawMeta: {
|
||||
type: ViewVariableType.ArrayObject,
|
||||
children: [
|
||||
{
|
||||
key: 'count',
|
||||
name: 'count',
|
||||
type: ViewVariableType.Number,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const mockRefInputs: InputItem[] = mockKeyPaths.map(_keyPath =>
|
||||
genInput(mockKeyPaths.join('__'), _keyPath),
|
||||
);
|
||||
|
||||
export const mockRefValues = mockRefInputs.map(_input => _input.input);
|
||||
|
||||
export const mockMergeGroup = [
|
||||
{
|
||||
name: 'test_group_1',
|
||||
variables: mockRefValues,
|
||||
},
|
||||
{
|
||||
name: 'test_group_2',
|
||||
variables: mockRefValues.reverse(),
|
||||
},
|
||||
];
|
||||
|
||||
export const mockFullOutputs: ViewVariableMeta[] =
|
||||
ViewVariableType.getComplement([]).map(_type => ({
|
||||
key: ViewVariableType.LabelMap[_type],
|
||||
name: ViewVariableType.LabelMap[_type],
|
||||
type: _type,
|
||||
required: true,
|
||||
description: 'test_description',
|
||||
children: ViewVariableType.canDrilldown(_type)
|
||||
? ViewVariableType.getComplement([]).map(_childType => ({
|
||||
key: ViewVariableType.LabelMap[_childType],
|
||||
name: ViewVariableType.LabelMap[_childType],
|
||||
type: _childType,
|
||||
required: true,
|
||||
description: 'test_child_description',
|
||||
}))
|
||||
: [],
|
||||
}));
|
||||
210
frontend/packages/workflow/variable/__tests__/workflow.mock.ts
Normal file
210
frontend/packages/workflow/variable/__tests__/workflow.mock.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* 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 WorkflowJSON } from '@flowgram-adapter/free-layout-editor';
|
||||
import { StandardNodeType } from '@coze-workflow/base/types';
|
||||
|
||||
import {
|
||||
allEndRefInputs,
|
||||
genInput,
|
||||
mockFullOutputs,
|
||||
mockRefInputs,
|
||||
mockMergeGroup,
|
||||
} from './variable.mock';
|
||||
|
||||
/**
|
||||
* start(start) -> llm_0 -> variable_merge_0 -> end(end)
|
||||
* llm_2 -> llm_0
|
||||
* llm_2 -> llm_3
|
||||
*/
|
||||
export const complexMock: WorkflowJSON = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'start',
|
||||
type: StandardNodeType.Start,
|
||||
meta: {
|
||||
position: {
|
||||
x: 180,
|
||||
y: 26.700000000000017,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
outputs: mockFullOutputs,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'end',
|
||||
type: StandardNodeType.End,
|
||||
meta: {
|
||||
position: {
|
||||
x: 1674.1103135413448,
|
||||
y: 40.63341482104891,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
inputParameters: allEndRefInputs,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'llm_0',
|
||||
type: StandardNodeType.LLM,
|
||||
meta: {
|
||||
position: {
|
||||
x: 918.2411574431025,
|
||||
y: 109.52852376445134,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
inputParameters: mockRefInputs,
|
||||
outputs: mockFullOutputs,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'merge_0',
|
||||
type: StandardNodeType.VariableMerge,
|
||||
data: {
|
||||
groups: mockMergeGroup,
|
||||
},
|
||||
meta: {
|
||||
position: {
|
||||
x: 779.2423264779079,
|
||||
y: -161.54447093414998,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'llm_2',
|
||||
type: StandardNodeType.LLM,
|
||||
meta: {
|
||||
position: {
|
||||
x: -56.433897883820265,
|
||||
y: 221.56518397907752,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'llm_3',
|
||||
type: StandardNodeType.LLM,
|
||||
meta: {
|
||||
position: {
|
||||
x: 1072.169867226058,
|
||||
y: 401.6455814216276,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '156471',
|
||||
type: StandardNodeType.LLM,
|
||||
meta: {
|
||||
position: {
|
||||
x: 914.727084798963,
|
||||
y: 576.5804057333598,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
sourceNodeID: 'variable_merge_0',
|
||||
targetNodeID: 'end',
|
||||
},
|
||||
{
|
||||
sourceNodeID: 'start',
|
||||
targetNodeID: 'llm_0',
|
||||
},
|
||||
{
|
||||
sourceNodeID: 'llm_2',
|
||||
targetNodeID: 'llm_0',
|
||||
},
|
||||
{
|
||||
sourceNodeID: 'llm_0',
|
||||
targetNodeID: 'variable_merge_0',
|
||||
},
|
||||
{
|
||||
sourceNodeID: 'llm_2',
|
||||
targetNodeID: 'llm_3',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const loopJSON: WorkflowJSON = {
|
||||
nodes: [
|
||||
...complexMock.nodes,
|
||||
{
|
||||
id: 'loop_0',
|
||||
type: StandardNodeType.Loop,
|
||||
meta: {
|
||||
position: { x: 1200, y: 0 },
|
||||
},
|
||||
data: {
|
||||
inputs: {
|
||||
inputParameters: mockRefInputs.map(_input => ({
|
||||
..._input,
|
||||
name: `loop_batch_${_input.name}`,
|
||||
})),
|
||||
variableParameters: mockRefInputs.map(_input => ({
|
||||
..._input,
|
||||
name: `loop_var_${_input.name}`,
|
||||
})),
|
||||
},
|
||||
outputs: [
|
||||
genInput('loop_output_var_string', [
|
||||
'loop_0',
|
||||
'loop_var_llm_0__Object',
|
||||
]),
|
||||
genInput('loop_output_string', ['llm_in_loop_0', 'String']),
|
||||
genInput('loop_output_image', ['llm_in_loop_0', 'Object', 'Image']),
|
||||
],
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
id: 'llm_in_loop_0',
|
||||
type: StandardNodeType.LLM,
|
||||
meta: {
|
||||
position: { x: 400, y: 0 },
|
||||
},
|
||||
data: {
|
||||
outputs: mockFullOutputs,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'llm_in_loop_1',
|
||||
type: StandardNodeType.LLM,
|
||||
meta: {
|
||||
position: { x: 500, y: 0 },
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
sourceNodeID: 'llm_in_loop_0',
|
||||
targetNodeID: 'llm_in_loop_1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
...complexMock.edges,
|
||||
{
|
||||
sourceNodeID: 'llm_0',
|
||||
targetNodeID: 'loop_0',
|
||||
},
|
||||
{
|
||||
sourceNodeID: 'loop_0',
|
||||
targetNodeID: 'end',
|
||||
},
|
||||
],
|
||||
};
|
||||
12
frontend/packages/workflow/variable/config/rush-project.json
Normal file
12
frontend/packages/workflow/variable/config/rush-project.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["./dist"]
|
||||
},
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
}
|
||||
]
|
||||
}
|
||||
9
frontend/packages/workflow/variable/eslint.config.js
Normal file
9
frontend/packages/workflow/variable/eslint.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'node',
|
||||
rules: {
|
||||
'import/no-duplicates': 'off',
|
||||
},
|
||||
});
|
||||
60
frontend/packages/workflow/variable/package.json
Normal file
60
frontend/packages/workflow/variable/package.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "@coze-workflow/variable",
|
||||
"version": "0.0.1",
|
||||
"description": "workflow 变量包",
|
||||
"license": "Apache-2.0",
|
||||
"author": "wencheng.lwc@bytedance.com",
|
||||
"sideEffects": [
|
||||
"**/*.css",
|
||||
"**/*.less",
|
||||
"**/*.sass",
|
||||
"**/*.scss"
|
||||
],
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"lint": "eslint ./ --cache --quiet",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:cov": "npm run test -- --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coze-arch/bot-api": "workspace:*",
|
||||
"@coze-arch/bot-flags": "workspace:*",
|
||||
"@coze-arch/i18n": "workspace:*",
|
||||
"@coze-arch/logger": "workspace:*",
|
||||
"@coze-arch/report-events": "workspace:*",
|
||||
"@coze-workflow/base": "workspace:*",
|
||||
"@douyinfe/semi-ui": "~2.72.3",
|
||||
"@flowgram-adapter/common": "workspace:*",
|
||||
"@flowgram-adapter/free-layout-editor": "workspace:*",
|
||||
"ajv": "~8.12.0",
|
||||
"inversify": "^6.0.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"react": "~18.2.0",
|
||||
"react-dom": "~18.2.0",
|
||||
"zod": "3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coze-arch/bot-typings": "workspace:*",
|
||||
"@coze-arch/eslint-config": "workspace:*",
|
||||
"@coze-arch/ts-config": "workspace:*",
|
||||
"@coze-arch/vitest-config": "workspace:*",
|
||||
"@rsbuild/core": "1.1.13",
|
||||
"@types/lodash-es": "^4.17.10",
|
||||
"@types/node": "18.18.9",
|
||||
"@types/react": "18.2.37",
|
||||
"@types/react-dom": "18.2.15",
|
||||
"@vitest/coverage-v8": "~3.0.5",
|
||||
"acorn": "^8.12.1",
|
||||
"less": "^3.13.1",
|
||||
"react-is": ">= 16.8.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"scheduler": ">=0.19.0",
|
||||
"styled-components": ">=4",
|
||||
"typescript": "~5.8.2",
|
||||
"vitest": "~3.0.5",
|
||||
"webpack": "~5.91.0"
|
||||
}
|
||||
}
|
||||
|
||||
18
frontend/packages/workflow/variable/src/components/index.tsx
Normal file
18
frontend/packages/workflow/variable/src/components/index.tsx
Normal 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';
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
45
frontend/packages/workflow/variable/src/constants.ts
Normal file
45
frontend/packages/workflow/variable/src/constants.ts
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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,
|
||||
];
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
25
frontend/packages/workflow/variable/src/core/index.tsx
Normal file
25
frontend/packages/workflow/variable/src/core/index.tsx
Normal 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';
|
||||
47
frontend/packages/workflow/variable/src/core/types.tsx
Normal file
47
frontend/packages/workflow/variable/src/core/types.tsx
Normal 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;
|
||||
}
|
||||
171
frontend/packages/workflow/variable/src/core/utils/create-ast.ts
Normal file
171
frontend/packages/workflow/variable/src/core/utils/create-ast.ts
Normal 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 [];
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
118
frontend/packages/workflow/variable/src/core/utils/parse-ast.ts
Normal file
118
frontend/packages/workflow/variable/src/core/utils/parse-ast.ts
Normal 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[],
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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({}),
|
||||
];
|
||||
22
frontend/packages/workflow/variable/src/datas/index.ts
Normal file
22
frontend/packages/workflow/variable/src/datas/index.ts
Normal 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';
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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]),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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];
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
17
frontend/packages/workflow/variable/src/global.d.ts
vendored
Normal file
17
frontend/packages/workflow/variable/src/global.d.ts
vendored
Normal 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' />
|
||||
26
frontend/packages/workflow/variable/src/hooks/index.tsx
Normal file
26
frontend/packages/workflow/variable/src/hooks/index.tsx
Normal 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';
|
||||
@@ -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();
|
||||
}, []);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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],
|
||||
);
|
||||
}
|
||||
@@ -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],
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
30
frontend/packages/workflow/variable/src/index.ts
Normal file
30
frontend/packages/workflow/variable/src/index.ts
Normal 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';
|
||||
20
frontend/packages/workflow/variable/src/legacy/index.ts
Normal file
20
frontend/packages/workflow/variable/src/legacy/index.ts
Normal 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';
|
||||
670
frontend/packages/workflow/variable/src/legacy/variable-utils.ts
Normal file
670
frontend/packages/workflow/variable/src/legacy/variable-utils.ts
Normal 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),
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
17
frontend/packages/workflow/variable/src/services/index.tsx
Normal file
17
frontend/packages/workflow/variable/src/services/index.tsx
Normal 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';
|
||||
34
frontend/packages/workflow/variable/src/typings.tsx
Normal file
34
frontend/packages/workflow/variable/src/typings.tsx
Normal 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;
|
||||
}
|
||||
42
frontend/packages/workflow/variable/src/utils/form.ts
Normal file
42
frontend/packages/workflow/variable/src/utils/form.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
26
frontend/packages/workflow/variable/src/utils/json.ts
Normal file
26
frontend/packages/workflow/variable/src/utils/json.ts
Normal 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 {};
|
||||
}
|
||||
}
|
||||
24
frontend/packages/workflow/variable/src/utils/path.ts
Normal file
24
frontend/packages/workflow/variable/src/utils/path.ts
Normal 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;
|
||||
}
|
||||
102
frontend/packages/workflow/variable/src/utils/sub-canvas.tsx
Normal file
102
frontend/packages/workflow/variable/src/utils/sub-canvas.tsx
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
56
frontend/packages/workflow/variable/tsconfig.build.json
Normal file
56
frontend/packages/workflow/variable/tsconfig.build.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {},
|
||||
"types": ["react", "react-dom", "vitest/globals"],
|
||||
"jsx": "react",
|
||||
"isolatedModules": true,
|
||||
"preserveSymlinks": false,
|
||||
"strictPropertyInitialization": false,
|
||||
"strictNullChecks": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../arch/bot-api/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-flags/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-typings/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/i18n/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/logger/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/report-events/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../base/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../common/flowgram-adapter/common/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../common/flowgram-adapter/free-layout-editor/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/eslint-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/ts-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/vitest-config/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/packages/workflow/variable/tsconfig.json
Normal file
15
frontend/packages/workflow/variable/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
],
|
||||
"exclude": ["**/*"]
|
||||
}
|
||||
23
frontend/packages/workflow/variable/tsconfig.misc.json
Normal file
23
frontend/packages/workflow/variable/tsconfig.misc.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"include": ["__tests__", "vitest.config.ts"],
|
||||
"exclude": ["**/node_modules", "./dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {},
|
||||
"types": ["react", "react-dom", "vitest/globals"],
|
||||
"jsx": "react",
|
||||
"isolatedModules": true,
|
||||
"preserveSymlinks": false,
|
||||
"strictPropertyInitialization": false,
|
||||
"strictNullChecks": true,
|
||||
"rootDir": "./",
|
||||
"outDir": "./dist"
|
||||
}
|
||||
}
|
||||
62
frontend/packages/workflow/variable/vitest.config.ts
Normal file
62
frontend/packages/workflow/variable/vitest.config.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 { resolve } from 'path';
|
||||
|
||||
import { defineConfig } from '@coze-arch/vitest-config';
|
||||
|
||||
const alias = {
|
||||
'@coze-workflow/render': resolve('./__tests__/default.mock.ts'),
|
||||
'@coze-workflow/components': resolve('./__tests__/default.mock.ts'),
|
||||
'@coze-arch/bot-icons': resolve('./__tests__/default.mock.ts'),
|
||||
'@coze-studio/bot-detail-store/page-runtime': resolve(
|
||||
'./__tests__/default.mock.ts',
|
||||
),
|
||||
'@coze-studio/bot-detail-store/bot-info': resolve(
|
||||
'./__tests__/default.mock.ts',
|
||||
),
|
||||
'@coze-studio/bot-detail-store/bot-skill': resolve(
|
||||
'./__tests__/default.mock.ts',
|
||||
),
|
||||
'@coze-studio/bot-detail-store': resolve('./__tests__/default.mock.ts'),
|
||||
};
|
||||
|
||||
const esbuild = {
|
||||
tsconfigRaw: {
|
||||
compilerOptions: {
|
||||
experimentalDecorators: true,
|
||||
emitDecoratorMetadata: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default defineConfig(
|
||||
{
|
||||
dirname: __dirname,
|
||||
preset: 'web',
|
||||
esbuild,
|
||||
test: {
|
||||
setupFiles: ['./__tests__/setup.ts'],
|
||||
onConsoleLog: (log: string, type: 'stdout' | 'stderr') =>
|
||||
type === 'stdout',
|
||||
|
||||
alias,
|
||||
},
|
||||
},
|
||||
{
|
||||
fixSemi: true,
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user