feat: manually mirror opencoze's code from bytedance

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

View File

@@ -0,0 +1,75 @@
# @coze-workflow/base
workflow 基础包
## Overview
This package is part of the Coze Studio monorepo and provides workflow functionality. It includes hook, store, api.
## Getting Started
### Installation
Add this package to your `package.json`:
```json
{
"dependencies": {
"@coze-workflow/base": "workspace:*"
}
}
```
Then run:
```bash
rush update
```
### Usage
```typescript
import { /* exported functions/components */ } from '@coze-workflow/base';
// Example usage
// TODO: Add specific usage examples
```
## Features
- Hook
- Store
- Api
## API Reference
### Exports
- `*`
- `*`
- `*`
- `*`
- `*`
- `*`
- `WorkflowNode`
- `WorkflowNodeContext`
For detailed API documentation, please refer to the TypeScript definitions.
## Development
This package is built with:
- TypeScript
- React
- Vitest for testing
- ESLint for code quality
## Contributing
This package is part of the Coze Studio monorepo. Please follow the monorepo contribution guidelines.
## License
Apache-2.0

View File

@@ -0,0 +1,100 @@
/*
* 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 { describe, it, expect, vi, beforeEach } from 'vitest';
import { workflowApi as archWorkflowApi } from '@coze-arch/bot-api';
import { workflowApi, workflowQueryClient } from '../../src/api';
vi.mock('@coze-arch/bot-api', () => ({
workflowApi: {
GetHistorySchema: vi.fn(),
GetWorkFlowProcess: vi.fn(),
GetCanvasInfo: vi.fn(),
OPGetHistorySchema: vi.fn(),
OPGetWorkFlowProcess: vi.fn(),
OPGetCanvasInfo: vi.fn(),
},
}));
const mockParams = {
space_id: '123',
workflow_id: '456',
commit_id: '789',
type: 1,
};
describe('api/index.ts', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should export workflowApi', () => {
expect(workflowApi).toBeDefined();
});
it('should export workflowQueryClient', () => {
expect(workflowQueryClient).toBeDefined();
});
describe('workflowApi proxy', () => {
it('should call original API methods in non-OP environment', () => {
// @ts-expect-error IS_BOT_OP is a global variable defined in runtime
global.IS_BOT_OP = false;
workflowApi.GetHistorySchema(mockParams);
expect(archWorkflowApi.GetHistorySchema).toHaveBeenCalledWith(mockParams);
workflowApi.GetWorkFlowProcess({
space_id: mockParams.space_id,
workflow_id: mockParams.workflow_id,
});
expect(archWorkflowApi.GetWorkFlowProcess).toHaveBeenCalledWith({
space_id: mockParams.space_id,
workflow_id: mockParams.workflow_id,
});
workflowApi.GetCanvasInfo({ space_id: mockParams.space_id });
expect(archWorkflowApi.GetCanvasInfo).toHaveBeenCalledWith({
space_id: mockParams.space_id,
});
});
it('should call OP API methods in OP environment', () => {
// @ts-expect-error IS_BOT_OP is a global variable defined in runtime
global.IS_BOT_OP = true;
workflowApi.GetHistorySchema(mockParams);
expect(archWorkflowApi.OPGetHistorySchema).toHaveBeenCalledWith(
mockParams,
);
workflowApi.GetWorkFlowProcess({
space_id: mockParams.space_id,
workflow_id: mockParams.workflow_id,
});
expect(archWorkflowApi.OPGetWorkFlowProcess).toHaveBeenCalledWith({
space_id: mockParams.space_id,
workflow_id: mockParams.workflow_id,
});
workflowApi.GetCanvasInfo({ space_id: mockParams.space_id });
expect(archWorkflowApi.OPGetCanvasInfo).toHaveBeenCalledWith({
space_id: mockParams.space_id,
});
});
});
});

View File

@@ -0,0 +1,214 @@
/*
* 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 { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, waitFor } from '@testing-library/react';
import { useQuery } from '@tanstack/react-query';
import {
workflowQueryClient,
withQueryClient,
} from '../../src/api/with-query-client';
describe('with-query-client.tsx', () => {
beforeEach(() => {
// 清理 QueryClient 缓存
workflowQueryClient.clear();
});
describe('workflowQueryClient', () => {
it('should export a QueryClient instance', () => {
expect(workflowQueryClient).toBeDefined();
expect(workflowQueryClient.constructor.name).toBe('QueryClient');
});
it('should have default configuration', () => {
const defaultOptions = workflowQueryClient.getDefaultOptions();
expect(defaultOptions).toBeDefined();
});
it('should be able to manage queries', async () => {
const queryKey = ['test'];
const queryFn = vi.fn().mockResolvedValue('test data');
const result = await workflowQueryClient.fetchQuery({
queryKey,
queryFn,
});
expect(result).toBe('test data');
expect(queryFn).toHaveBeenCalled();
expect(workflowQueryClient.getQueryData(queryKey)).toBe('test data');
});
});
describe('withQueryClient', () => {
it('should wrap component with QueryClientProvider', () => {
const TestComponent = () => <div>Test Component</div>;
const WrappedComponent = withQueryClient(TestComponent);
const { container } = render(<WrappedComponent />);
expect(container.innerHTML).toContain('Test Component');
});
it('should pass props to wrapped component', () => {
const TestComponent = ({ text }: { text: string }) => <div>{text}</div>;
const WrappedComponent = withQueryClient(TestComponent);
const { container } = render(<WrappedComponent text="Hello" />);
expect(container.innerHTML).toContain('Hello');
});
it('should provide QueryClient context to wrapped component', async () => {
const queryKey = ['test'];
const queryFn = vi.fn().mockResolvedValue('test data');
const TestComponent = () => {
const { data, isLoading } = useQuery({ queryKey, queryFn });
if (isLoading) {
return <div>Loading...</div>;
}
return <div>{data}</div>;
};
const WrappedComponent = withQueryClient(TestComponent);
const { container } = render(<WrappedComponent />);
// 初始加载状态
expect(container.innerHTML).toContain('Loading...');
// 等待数据加载完成
await waitFor(() => {
expect(container.innerHTML).toContain('test data');
});
expect(queryFn).toHaveBeenCalledTimes(1);
});
it('should handle query errors gracefully', () => {
const queryKey = ['test-error'];
const queryFn = vi.fn().mockRejectedValue(new Error('Test error'));
const TestComponent = () => {
const { error, isError } = useQuery({ queryKey, queryFn });
if (isError) {
return <div>Error: {error.message}</div>;
}
return <div>Loading...</div>;
};
const WrappedComponent = withQueryClient(TestComponent);
render(<WrappedComponent />);
expect(queryFn).toHaveBeenCalledTimes(1);
});
it('should maintain component type and handle complex props', () => {
interface ComplexProps {
text: string;
count: number;
items: string[];
onClick: () => void;
}
const TestComponent: React.FC<ComplexProps> = ({
text,
count,
items,
onClick,
}) => (
<div onClick={onClick}>
<span>{text}</span>
<span>{count}</span>
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
const WrappedComponent = withQueryClient(TestComponent);
const handleClick = vi.fn();
const { container } = render(
<WrappedComponent
text="Complex Test"
count={42}
items={['a', 'b', 'c']}
onClick={handleClick}
/>,
);
expect(container.innerHTML).toContain('Complex Test');
expect(container.innerHTML).toContain('42');
expect(container.innerHTML).toContain('<li>a</li>');
expect(container.innerHTML).toContain('<li>b</li>');
expect(container.innerHTML).toContain('<li>c</li>');
});
it('should work with nested queries', async () => {
const parentQueryKey = ['parent'];
const childQueryKey = ['child'];
const parentQueryFn = vi.fn().mockResolvedValue('parent data');
const childQueryFn = vi.fn().mockResolvedValue('child data');
const ChildComponent = () => {
const { data, isLoading } = useQuery({
queryKey: childQueryKey,
queryFn: childQueryFn,
});
if (isLoading) {
return <div>Loading Child...</div>;
}
return <div className="child">{data}</div>;
};
const ParentComponent = () => {
const { data, isLoading } = useQuery({
queryKey: parentQueryKey,
queryFn: parentQueryFn,
});
if (isLoading) {
return <div>Loading Parent...</div>;
}
return (
<div className="parent">
{data}
<ChildComponent />
</div>
);
};
const WrappedComponent = withQueryClient(ParentComponent);
const { container } = render(<WrappedComponent />);
// 初始加载状态
expect(container.innerHTML).toContain('Loading Parent...');
// 等待所有查询完成
await waitFor(() => {
expect(container.innerHTML).toContain('parent data');
expect(container.innerHTML).toContain('child data');
});
expect(parentQueryFn).toHaveBeenCalledTimes(1);
expect(childQueryFn).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -0,0 +1,184 @@
/*
* 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, { useContext } from 'react';
import { describe, it, expect, vi } from 'vitest';
import { render } from '@testing-library/react';
import {
FlowNodeFormData,
FlowNodeErrorData,
} from '@flowgram-adapter/free-layout-editor';
import { StandardNodeType } from '../../src/types';
import { type WorkflowNode } from '../../src/entities';
import { WorkflowNodeContext } from '../../src/contexts/workflow-node-context';
describe('WorkflowNodeContext', () => {
const createMockWorkflowNode = (id: string): WorkflowNode => {
const dispose = vi.fn();
const mockFormModel = {
initialized: true,
getFormItemValueByPath: vi.fn(),
getFormItemByPath: vi.fn(),
formItemPathMap: new Map(),
onInitialized: vi.fn().mockReturnValue({ dispose }),
};
const mockFormData = {
formModel: mockFormModel,
onDataChange: vi.fn().mockReturnValue({ dispose }),
};
const mockRegistry = {
getNodeInputParameters: vi.fn().mockReturnValue([]),
getNodeOutputs: vi.fn().mockReturnValue([]),
};
const mockNode = {
id,
flowNodeType: StandardNodeType.Start,
getNodeRegistry: vi.fn().mockReturnValue(mockRegistry),
getNodeMeta: vi.fn().mockReturnValue({
icon: '',
title: '',
description: '',
}),
getData: vi.fn(dataType => {
if (dataType === FlowNodeFormData) {
return mockFormData;
}
if (dataType === FlowNodeErrorData) {
return {
getError: vi.fn().mockReturnValue(null),
};
}
return null;
}),
_formModel: mockFormModel,
};
const workflowNode = {
type: StandardNodeType.Start,
registry: mockRegistry,
inputParameters: [],
outputs: [],
data: {},
icon: '',
title: '',
description: '',
isError: false,
isInitialized: true,
setData: vi.fn(),
onDataChange: vi.fn().mockReturnValue({ dispose }),
onInitialized: vi.fn().mockReturnValue({ dispose }),
node: mockNode,
getFormValueByPathEnds: vi.fn(),
form: mockFormModel,
};
return workflowNode as unknown as WorkflowNode;
};
it('应该创建一个默认值为 undefined 的 Context', () => {
const TestComponent = () => {
const context = useContext(WorkflowNodeContext);
expect(context).toBeUndefined();
return null;
};
render(<TestComponent />);
});
it('应该能够通过 Provider 提供 WorkflowNode 实例', () => {
const mockWorkflowNode = createMockWorkflowNode('1');
const TestComponent = () => {
const context = useContext(WorkflowNodeContext);
expect(context).toBe(mockWorkflowNode);
return null;
};
render(
<WorkflowNodeContext.Provider value={mockWorkflowNode}>
<TestComponent />
</WorkflowNodeContext.Provider>,
);
});
it('应该能够在嵌套组件中访问 Context', () => {
const mockWorkflowNode = createMockWorkflowNode('1');
const ChildComponent = () => {
const context = useContext(WorkflowNodeContext);
expect(context).toBe(mockWorkflowNode);
return null;
};
const ParentComponent = () => (
<div>
<ChildComponent />
</div>
);
render(
<WorkflowNodeContext.Provider value={mockWorkflowNode}>
<ParentComponent />
</WorkflowNodeContext.Provider>,
);
});
it('应该能够处理 undefined 值', () => {
const TestComponent = () => {
const context = useContext(WorkflowNodeContext);
expect(context).toBeUndefined();
return null;
};
render(
<WorkflowNodeContext.Provider value={undefined}>
<TestComponent />
</WorkflowNodeContext.Provider>,
);
});
it('应该能够在多层 Provider 中正确获取最近的值', () => {
const mockWorkflowNode1 = createMockWorkflowNode('1');
const mockWorkflowNode2 = createMockWorkflowNode('2');
const InnerComponent = () => {
const context = useContext(WorkflowNodeContext);
expect(context).toBe(mockWorkflowNode2);
return null;
};
const OuterComponent = () => {
const context = useContext(WorkflowNodeContext);
expect(context).toBe(mockWorkflowNode1);
return (
<WorkflowNodeContext.Provider value={mockWorkflowNode2}>
<InnerComponent />
</WorkflowNodeContext.Provider>
);
};
render(
<WorkflowNodeContext.Provider value={mockWorkflowNode1}>
<OuterComponent />
</WorkflowNodeContext.Provider>,
);
});
});

View File

@@ -0,0 +1,337 @@
/*
* 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 { describe, it, expect, vi } from 'vitest';
import {
FlowNodeFormData,
FlowNodeErrorData,
} from '@flowgram-adapter/free-layout-editor';
import { StandardNodeType } from '../../src/types';
import { WorkflowNode } from '../../src/entities';
// Mock entities
vi.mock('../../src/entities', async () => {
const actual = await vi.importActual('../../src/entities');
return {
...actual,
// 如果需要mock其他实体可以在这里添加
};
});
vi.mock('../../src/utils', () => ({
getFormValueByPathEnds: vi.fn(),
}));
describe('WorkflowNode', () => {
beforeEach(() => {
vi.clearAllMocks();
});
const createMockFormModel = () => ({
initialized: true,
getFormItemValueByPath: vi.fn(),
getFormItemByPath: vi.fn(),
formItemPathMap: new Map(),
onInitialized: vi.fn().mockReturnValue({ dispose: vi.fn() }),
});
const createMockNode = () => {
const mockFormModel = createMockFormModel();
const mockFormData = {
formModel: mockFormModel,
onDataChange: vi.fn().mockReturnValue({ dispose: vi.fn() }),
};
return {
flowNodeType: StandardNodeType.Start,
getNodeRegistry: vi.fn().mockReturnValue({
getNodeInputParameters: vi.fn().mockReturnValue([]),
getNodeOutputs: vi.fn().mockReturnValue([]),
}),
getNodeMeta: vi.fn(),
getData: vi.fn(dataType => {
if (dataType === FlowNodeFormData) {
return mockFormData;
}
if (dataType === FlowNodeErrorData) {
return {
getError: vi.fn().mockReturnValue(null),
};
}
return null;
}),
_formModel: mockFormModel,
};
};
describe('基本属性', () => {
it('should get node type', () => {
const mockNode = createMockNode();
const workflowNode = new WorkflowNode(mockNode as any);
expect(workflowNode.type).toBe(StandardNodeType.Start);
});
it('should get node registry', () => {
const mockRegistry = { test: 'registry' };
const mockNode = createMockNode();
mockNode.getNodeRegistry.mockReturnValue(mockRegistry);
const workflowNode = new WorkflowNode(mockNode as any);
expect(workflowNode.registry).toBe(mockRegistry);
});
it('should check if node has error', () => {
const mockNode = createMockNode();
const workflowNode = new WorkflowNode(mockNode as any);
expect(workflowNode.isError).toBe(false);
mockNode.getData.mockImplementation(dataType => {
if (dataType === FlowNodeErrorData) {
return {
getError: vi.fn().mockReturnValue(new Error('Test error')),
};
}
return null;
});
expect(workflowNode.isError).toBe(true);
});
it('should check if node is initialized', () => {
const mockNode = createMockNode();
const workflowNode = new WorkflowNode(mockNode as any);
expect(workflowNode.isInitialized).toBe(true);
mockNode._formModel.initialized = false;
expect(workflowNode.isInitialized).toBe(false);
});
it('should handle undefined node type', () => {
const mockNode = createMockNode();
mockNode.flowNodeType = StandardNodeType.Start;
const workflowNode = new WorkflowNode(mockNode as any);
expect(workflowNode.type).toBe(StandardNodeType.Start);
});
it('should handle null registry', () => {
const mockNode = createMockNode();
mockNode.getNodeRegistry.mockReturnValue(null);
const workflowNode = new WorkflowNode(mockNode as any);
expect(workflowNode.registry).toBeNull();
});
it('should handle error data access failure', () => {
const mockNode = createMockNode();
mockNode.getData.mockReturnValue({
getError: vi.fn().mockReturnValue(undefined),
});
const workflowNode = new WorkflowNode(mockNode as any);
expect(workflowNode.isError).toBe(false);
});
});
describe('表单数据管理', () => {
it('should get form data', () => {
const mockNode = createMockNode();
mockNode._formModel.getFormItemValueByPath.mockReturnValue({
test: 'data',
});
const workflowNode = new WorkflowNode(mockNode as any);
expect(workflowNode.data).toEqual({ test: 'data' });
expect(mockNode._formModel.getFormItemValueByPath).toHaveBeenCalledWith(
'/',
);
});
it('should set form data', () => {
const mockNode = createMockNode();
const mockFormItem = { value: null };
mockNode._formModel.getFormItemByPath.mockReturnValue(mockFormItem);
const workflowNode = new WorkflowNode(mockNode as any);
const newData = { test: 'new data' };
workflowNode.setData(newData);
expect(mockNode._formModel.getFormItemByPath).toHaveBeenCalledWith('/');
expect(mockFormItem.value).toEqual(newData);
});
it('should handle setting data when form item does not exist', () => {
const mockNode = createMockNode();
mockNode._formModel.getFormItemByPath.mockReturnValue(null);
const workflowNode = new WorkflowNode(mockNode as any);
workflowNode.setData({ test: 'data' });
expect(mockNode._formModel.getFormItemByPath).toHaveBeenCalledWith('/');
});
it('should handle invalid form data', () => {
const mockNode = createMockNode();
mockNode._formModel.getFormItemValueByPath.mockReturnValue(undefined);
const workflowNode = new WorkflowNode(mockNode as any);
expect(workflowNode.data).toBeUndefined();
});
it('should handle form item value mutation', () => {
const mockNode = createMockNode();
const mockFormItem = { value: { test: 'original' } };
mockNode._formModel.getFormItemByPath.mockReturnValue(mockFormItem);
const workflowNode = new WorkflowNode(mockNode as any);
const newData = { test: 'new data' };
workflowNode.setData(newData);
// 验证原始数据没有被修改
expect(newData).toEqual({ test: 'new data' });
expect(mockFormItem.value).toEqual({ test: 'new data' });
});
});
describe('输入输出参数', () => {
it('should get input parameters from registry', () => {
const mockInputParams = [{ name: 'test', type: 'string' }];
const mockNode = createMockNode();
mockNode.getNodeRegistry.mockReturnValue({
getNodeInputParameters: vi.fn().mockReturnValue(mockInputParams),
});
const workflowNode = new WorkflowNode(mockNode as any);
expect(workflowNode.inputParameters).toEqual(mockInputParams);
});
it('should get outputs from registry', () => {
const mockOutputs = [{ name: 'test', type: 'string' }];
const mockNode = createMockNode();
mockNode.getNodeRegistry.mockReturnValue({
getNodeOutputs: vi.fn().mockReturnValue(mockOutputs),
});
const workflowNode = new WorkflowNode(mockNode as any);
expect(workflowNode.outputs).toEqual(mockOutputs);
});
it('should get outputs from form', () => {
const mockOutputs = [{ name: 'test', type: 'string' }];
const mockNode = createMockNode();
mockNode.getNodeRegistry.mockReturnValue({
getNodeInputParameters: vi.fn().mockReturnValue([]),
getNodeOutputs: vi.fn().mockReturnValue(mockOutputs),
});
const workflowNode = new WorkflowNode(mockNode as any);
expect(workflowNode.outputs).toEqual(mockOutputs);
});
it('should handle invalid input parameters path', () => {
const mockNode = createMockNode();
mockNode.getNodeMeta.mockReturnValue({
inputParametersPath: null,
});
const workflowNode = new WorkflowNode(mockNode as any);
expect(workflowNode.inputParameters).toEqual([]);
});
it('should handle circular references in parameters', () => {
const mockNode = createMockNode();
const circularObj: any = { name: 'test' };
circularObj.self = circularObj;
mockNode.getNodeRegistry.mockReturnValue({
getNodeInputParameters: vi.fn().mockReturnValue([circularObj]),
});
const workflowNode = new WorkflowNode(mockNode as any);
expect(() => JSON.stringify(workflowNode.inputParameters)).toThrow();
});
it('should handle empty outputs path', () => {
const mockNode = createMockNode();
mockNode._formModel.getFormItemValueByPath.mockReturnValue(null);
const workflowNode = new WorkflowNode(mockNode as any);
expect(workflowNode.outputs).toEqual([]);
});
});
describe('元数据', () => {
it('should get node metadata', () => {
const mockNode = createMockNode();
const mockData = {
nodeMeta: {
icon: 'test-icon',
title: 'Test Node',
description: 'Test Description',
},
};
mockNode._formModel.getFormItemValueByPath.mockReturnValue(mockData);
const workflowNode = new WorkflowNode(mockNode as any);
expect(workflowNode.icon).toBe(mockData.nodeMeta.icon);
expect(workflowNode.title).toBe(mockData.nodeMeta.title);
expect(workflowNode.description).toBe(mockData.nodeMeta.description);
});
it('should handle missing metadata', () => {
const mockNode = createMockNode();
mockNode._formModel.getFormItemValueByPath.mockReturnValue({});
const workflowNode = new WorkflowNode(mockNode as any);
expect(workflowNode.icon).toBeUndefined();
expect(workflowNode.title).toBeUndefined();
expect(workflowNode.description).toBeUndefined();
});
it('should handle null metadata', () => {
const mockNode = createMockNode();
mockNode._formModel.getFormItemValueByPath.mockReturnValue(null);
const workflowNode = new WorkflowNode(mockNode as any);
expect(workflowNode.icon).toBeUndefined();
expect(workflowNode.title).toBeUndefined();
expect(workflowNode.description).toBeUndefined();
});
it('should handle metadata access error', () => {
const mockNode = createMockNode();
mockNode._formModel.getFormItemValueByPath.mockReturnValue(null);
const workflowNode = new WorkflowNode(mockNode as any);
expect(workflowNode.icon).toBeUndefined();
expect(workflowNode.title).toBeUndefined();
expect(workflowNode.description).toBeUndefined();
});
it('should handle partial metadata', () => {
const mockNode = createMockNode();
mockNode._formModel.getFormItemValueByPath.mockReturnValue({
nodeMeta: {
icon: 'test-icon',
// title and description missing
},
});
const workflowNode = new WorkflowNode(mockNode as any);
expect(workflowNode.icon).toBe('test-icon');
expect(workflowNode.title).toBeUndefined();
expect(workflowNode.description).toBeUndefined();
});
});
});

View File

@@ -0,0 +1,203 @@
/*
* 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 { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook } from '@testing-library/react';
import { useNodeTestId } from '../../src/hooks/use-node-test-id';
const mockUseCurrentEntity = vi.fn();
vi.mock('@flowgram-adapter/free-layout-editor', () => ({
useCurrentEntity: () => mockUseCurrentEntity(),
}));
// Mock @coze-arch/bot-error
class MockCustomError extends Error {
code: string;
constructor(message: string, code: string) {
super(message);
this.name = 'CustomError';
this.code = code;
}
}
vi.mock('@coze-arch/bot-error', () => ({
CustomError: vi
.fn()
.mockImplementation(
() =>
new MockCustomError(
'useNodeTestId must be called in a workflow node',
'',
),
),
}));
// Mock utils
const mockConcatTestId = vi.fn();
vi.mock('../../src/utils', () => ({
concatTestId: (...args: string[]) => mockConcatTestId(...args),
}));
describe('useNodeTestId', () => {
beforeEach(() => {
vi.clearAllMocks();
// 设置 concatTestId 的默认行为
mockConcatTestId.mockImplementation((...args: string[]) => args.join('.'));
});
it('应该在没有当前节点时抛出错误', () => {
mockUseCurrentEntity.mockReturnValue(null);
expect(() => {
renderHook(() => useNodeTestId());
}).toThrow(
new MockCustomError(
'useNodeTestId must be called in a workflow node',
'',
),
);
});
it('应该在节点没有 id 时抛出错误', () => {
mockUseCurrentEntity.mockReturnValue({});
expect(() => {
renderHook(() => useNodeTestId());
}).toThrow(
new MockCustomError(
'useNodeTestId must be called in a workflow node',
'',
),
);
});
it('应该返回正确的节点测试 ID', () => {
mockUseCurrentEntity.mockReturnValue({ id: '123' });
mockConcatTestId.mockReturnValueOnce('playground.node.123');
const { result } = renderHook(() => useNodeTestId());
expect(result.current.getNodeTestId()).toBe('playground.node.123');
expect(mockConcatTestId).toHaveBeenCalledWith('playground.node', '123');
});
it('应该返回正确的节点设置器 ID', () => {
mockUseCurrentEntity.mockReturnValue({ id: '123' });
mockConcatTestId
.mockReturnValueOnce('playground.node.123') // 用于 getNodeTestId
.mockReturnValueOnce('playground.node.123.llm'); // 用于 getNodeSetterId
const { result } = renderHook(() => useNodeTestId());
expect(result.current.getNodeSetterId('llm')).toBe(
'playground.node.123.llm',
);
expect(mockConcatTestId).toHaveBeenCalledTimes(2);
expect(mockConcatTestId).toHaveBeenNthCalledWith(
1,
'playground.node',
'123',
);
expect(mockConcatTestId).toHaveBeenNthCalledWith(
2,
'playground.node.123',
'llm',
);
});
it('应该正确连接测试 ID', () => {
mockUseCurrentEntity.mockReturnValue({ id: '123' });
mockConcatTestId
.mockReturnValueOnce('a.b')
.mockReturnValueOnce('a.b.c')
.mockReturnValueOnce('a.b.c');
const { result } = renderHook(() => useNodeTestId());
expect(result.current.concatTestId('a', 'b')).toBe('a.b');
expect(mockConcatTestId).toHaveBeenCalledWith('a', 'b');
expect(result.current.concatTestId('a.b', 'c')).toBe('a.b.c');
expect(mockConcatTestId).toHaveBeenCalledWith('a.b', 'c');
expect(result.current.concatTestId('a', 'b', 'c')).toBe('a.b.c');
expect(mockConcatTestId).toHaveBeenCalledWith('a', 'b', 'c');
});
it('应该在多个组件中返回不同的节点测试 ID', () => {
// 第一个组件
mockUseCurrentEntity.mockReturnValue({ id: '123' });
mockConcatTestId.mockReturnValueOnce('playground.node.123');
const { result: result1 } = renderHook(() => useNodeTestId());
expect(result1.current.getNodeTestId()).toBe('playground.node.123');
expect(mockConcatTestId).toHaveBeenCalledWith('playground.node', '123');
// 第二个组件
mockUseCurrentEntity.mockReturnValue({ id: '456' });
mockConcatTestId.mockReturnValueOnce('playground.node.456');
const { result: result2 } = renderHook(() => useNodeTestId());
expect(result2.current.getNodeTestId()).toBe('playground.node.456');
expect(mockConcatTestId).toHaveBeenCalledWith('playground.node', '456');
});
it('应该在多个组件中返回不同的节点设置器 ID', () => {
// 第一个组件
mockUseCurrentEntity.mockReturnValue({ id: '123' });
mockConcatTestId
.mockReturnValueOnce('playground.node.123') // 用于 getNodeTestId
.mockReturnValueOnce('playground.node.123.llm'); // 用于 getNodeSetterId
const { result: result1 } = renderHook(() => useNodeTestId());
expect(result1.current.getNodeSetterId('llm')).toBe(
'playground.node.123.llm',
);
expect(mockConcatTestId).toHaveBeenNthCalledWith(
1,
'playground.node',
'123',
);
expect(mockConcatTestId).toHaveBeenNthCalledWith(
2,
'playground.node.123',
'llm',
);
// 第二个组件
mockUseCurrentEntity.mockReturnValue({ id: '456' });
mockConcatTestId
.mockReturnValueOnce('playground.node.456') // 用于 getNodeTestId
.mockReturnValueOnce('playground.node.456.llm'); // 用于 getNodeSetterId
const { result: result2 } = renderHook(() => useNodeTestId());
expect(result2.current.getNodeSetterId('llm')).toBe(
'playground.node.456.llm',
);
expect(mockConcatTestId).toHaveBeenNthCalledWith(
3,
'playground.node',
'456',
);
expect(mockConcatTestId).toHaveBeenNthCalledWith(
4,
'playground.node.456',
'llm',
);
});
});

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act, cleanup } from '@testing-library/react';
import { StandardNodeType } from '../../src/types';
import { useWorkflowNode } from '../../src/hooks/use-workflow-node';
import { type WorkflowNode } from '../../src/entities';
import { WorkflowNodeContext } from '../../src/contexts';
describe('useWorkflowNode', () => {
const createMockWorkflowNode = (id: string): WorkflowNode => {
const mockRegistry = {
getNodeInputParameters: vi.fn().mockReturnValue([]),
getNodeOutputs: vi.fn().mockReturnValue([]),
};
const workflowNode = {
type: StandardNodeType.Start,
registry: mockRegistry,
inputParameters: [],
outputs: [],
data: {},
icon: '',
title: '',
description: '',
isError: false,
isInitialized: true,
setData: vi.fn(),
};
return workflowNode as unknown as WorkflowNode;
};
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
cleanup();
});
it('应该返回 WorkflowNode 的观察值', () => {
const mockWorkflowNode = createMockWorkflowNode('1');
const wrapper = ({ children }: { children: React.ReactNode }) => (
<WorkflowNodeContext.Provider value={mockWorkflowNode}>
{children}
</WorkflowNodeContext.Provider>
);
const { result } = renderHook(() => useWorkflowNode(), { wrapper });
expect(result.current).toEqual({
type: StandardNodeType.Start,
inputParameters: [],
outputs: [],
data: {},
icon: '',
title: '',
description: '',
isError: false,
isInitialized: true,
registry: mockWorkflowNode.registry,
setData: expect.any(Function),
});
});
it('应该正确绑定 setData 方法', async () => {
const mockWorkflowNode = createMockWorkflowNode('1');
const wrapper = ({ children }: { children: React.ReactNode }) => (
<WorkflowNodeContext.Provider value={mockWorkflowNode}>
{children}
</WorkflowNodeContext.Provider>
);
const { result } = renderHook(() => useWorkflowNode(), { wrapper });
const newData = { test: 'new data' };
await act(() => {
result.current.setData(newData);
return Promise.resolve();
});
expect(mockWorkflowNode.setData).toHaveBeenCalledWith(newData);
});
});

View File

@@ -0,0 +1,160 @@
/*
* 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 { describe, it, expect, beforeEach } from 'vitest';
import type { WorkflowEdgeJSON } from '@flowgram-adapter/free-layout-editor';
import type { WorkflowNodeJSON } from '../../../src/types';
import { useWorkflowStore } from '../../../src/store/workflow';
describe('useWorkflowStore', () => {
beforeEach(() => {
// 重置 store 状态
useWorkflowStore.setState({
nodes: [],
edges: [],
isCreatingWorkflow: false,
});
});
it('应该有正确的初始状态', () => {
const state = useWorkflowStore.getState();
expect(state.nodes).toEqual([]);
expect(state.edges).toEqual([]);
expect(state.isCreatingWorkflow).toBe(false);
});
describe('setNodes', () => {
it('应该正确设置节点数据', () => {
const mockNodes: WorkflowNodeJSON[] = [
{
id: '1',
type: 'test',
data: {},
meta: {
position: { x: 0, y: 0 },
},
},
{
id: '2',
type: 'test',
data: {},
meta: {
position: { x: 100, y: 100 },
},
},
];
useWorkflowStore.getState().setNodes(mockNodes);
expect(useWorkflowStore.getState().nodes).toEqual(mockNodes);
});
it('应该能够重置为空数组', () => {
// 先设置一些数据
useWorkflowStore.getState().setNodes([
{
id: '1',
type: 'test',
data: {},
meta: {
position: { x: 0, y: 0 },
},
},
]);
// 设置为空数组
useWorkflowStore.getState().setNodes([]);
expect(useWorkflowStore.getState().nodes).toEqual([]);
});
});
describe('setEdges', () => {
it('应该正确设置边数据', () => {
const mockEdges: WorkflowEdgeJSON[] = [
{
sourceNodeID: '1',
targetNodeID: '2',
},
{
sourceNodeID: '2',
targetNodeID: '3',
},
];
useWorkflowStore.getState().setEdges(mockEdges);
expect(useWorkflowStore.getState().edges).toEqual(mockEdges);
});
it('应该能够重置为空数组', () => {
// 先设置一些数据
useWorkflowStore.getState().setEdges([
{
sourceNodeID: '1',
targetNodeID: '2',
},
]);
// 设置为空数组
useWorkflowStore.getState().setEdges([]);
expect(useWorkflowStore.getState().edges).toEqual([]);
});
});
describe('setIsCreatingWorkflow', () => {
it('应该正确设置创建状态', () => {
// 设置为 true
useWorkflowStore.getState().setIsCreatingWorkflow(true);
expect(useWorkflowStore.getState().isCreatingWorkflow).toBe(true);
// 设置为 false
useWorkflowStore.getState().setIsCreatingWorkflow(false);
expect(useWorkflowStore.getState().isCreatingWorkflow).toBe(false);
});
});
describe('状态更新', () => {
it('应该能够同时更新多个状态', () => {
const mockNodes: WorkflowNodeJSON[] = [
{
id: '1',
type: 'test',
data: {},
meta: {
position: { x: 0, y: 0 },
},
},
];
const mockEdges: WorkflowEdgeJSON[] = [
{
sourceNodeID: '1',
targetNodeID: '2',
},
];
useWorkflowStore.setState({
nodes: mockNodes,
edges: mockEdges,
isCreatingWorkflow: true,
});
const state = useWorkflowStore.getState();
expect(state.nodes).toEqual(mockNodes);
expect(state.edges).toEqual(mockEdges);
expect(state.isCreatingWorkflow).toBe(true);
});
});
});

View File

@@ -0,0 +1,228 @@
/*
* 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 { describe, it, expect } from 'vitest';
import { BlockInput } from '../../src/types/block-input-dto';
import { ViewVariableType } from '../../src/types';
describe('block-input-dto', () => {
describe('create', () => {
it('应该能够创建基本的 BlockInput', () => {
const input = BlockInput.create('testName', 'testValue');
expect(input).toEqual({
name: 'testName',
input: {
type: 'string',
value: {
type: 'literal',
content: 'testValue',
rawMeta: {
type: ViewVariableType.String,
},
},
},
});
});
it('应该能够创建指定类型的 BlockInput', () => {
const input = BlockInput.create('testName', '42', 'integer');
expect(input).toEqual({
name: 'testName',
input: {
type: 'integer',
value: {
type: 'literal',
content: '42',
rawMeta: {
type: ViewVariableType.Integer,
},
},
},
});
});
it('应该在没有提供值时使用空字符串', () => {
const input = BlockInput.create('testName');
expect(input.input.value.content).toBe('');
});
});
describe('createString', () => {
it('应该创建字符串类型的 BlockInput', () => {
const input = BlockInput.createString('testName', 'testValue');
expect(input).toEqual({
name: 'testName',
input: {
type: 'string',
value: {
type: 'literal',
content: 'testValue',
rawMeta: {
type: ViewVariableType.String,
},
},
},
});
});
});
describe('createInteger', () => {
it('应该创建整数类型的 BlockInput', () => {
const input = BlockInput.createInteger('testName', '42');
expect(input).toEqual({
name: 'testName',
input: {
type: 'integer',
value: {
type: 'literal',
content: '42',
rawMeta: {
type: ViewVariableType.Integer,
},
},
},
});
});
});
describe('createFloat', () => {
it('应该创建浮点数类型的 BlockInput', () => {
const input = BlockInput.createFloat('testName', '3.14');
expect(input).toEqual({
name: 'testName',
input: {
type: 'float',
value: {
type: 'literal',
content: '3.14',
rawMeta: {
type: ViewVariableType.Number,
},
},
},
});
});
});
describe('createArray', () => {
it('应该创建数组类型的 BlockInput', () => {
const schema = { type: 'string' };
const input = BlockInput.createArray('testName', ['a', 'b', 'c'], schema);
expect(input).toEqual({
name: 'testName',
input: {
type: 'list',
schema: { type: 'string' },
value: {
type: 'literal',
content: ['a', 'b', 'c'],
},
},
});
});
it('应该支持不同类型的数组元素', () => {
const schema = { type: 'number' };
const input = BlockInput.createArray('testName', [1, 2, 3], schema);
expect(input.input.value.content).toEqual([1, 2, 3]);
});
});
describe('createBoolean', () => {
it('应该创建布尔类型的 BlockInput', () => {
const input = BlockInput.createBoolean('testName', true);
expect(input).toEqual({
name: 'testName',
input: {
type: 'boolean',
value: {
type: 'literal',
content: true,
rawMeta: {
type: ViewVariableType.Boolean,
},
},
},
});
});
it('应该正确处理 false 值', () => {
const input = BlockInput.createBoolean('testName', false);
expect(input.input.value.content).toBe(false);
});
});
describe('toLiteral', () => {
it('应该提取 BlockInput 的内容值', () => {
const input = BlockInput.createString('testName', 'testValue');
const value = BlockInput.toLiteral<string>(input);
expect(value).toBe('testValue');
});
it('应该能够提取不同类型的值', () => {
const boolInput = BlockInput.createBoolean('testName', true);
const boolValue = BlockInput.toLiteral<boolean>(boolInput);
expect(boolValue).toBe(true);
const arrayInput = BlockInput.createArray('testName', [1, 2, 3], {
type: 'number',
});
const arrayValue = BlockInput.toLiteral<number[]>(arrayInput);
expect(arrayValue).toEqual([1, 2, 3]);
});
});
describe('isBlockInput', () => {
it('应该正确识别有效的 BlockInput', () => {
const input = BlockInput.createString('testName', 'testValue');
expect(BlockInput.isBlockInput(input)).toBe(true);
});
it('应该正确识别无效的 BlockInput', () => {
expect(BlockInput.isBlockInput(null)).toBe(false);
expect(BlockInput.isBlockInput(undefined)).toBe(false);
expect(BlockInput.isBlockInput({})).toBe(false);
expect(
BlockInput.isBlockInput({
name: 'test',
input: { value: { content: undefined } },
}),
).toBe(true);
expect(
BlockInput.isBlockInput({
input: { value: { content: 'test' } },
}),
).toBe(false);
});
it('应该正确识别所有类型的有效 BlockInput', () => {
const stringInput = BlockInput.createString('test', 'value');
const integerInput = BlockInput.createInteger('test', '42');
const floatInput = BlockInput.createFloat('test', '3.14');
const booleanInput = BlockInput.createBoolean('test', true);
const arrayInput = BlockInput.createArray('test', [1, 2, 3], {
type: 'number',
});
expect(BlockInput.isBlockInput(stringInput)).toBe(true);
expect(BlockInput.isBlockInput(integerInput)).toBe(true);
expect(BlockInput.isBlockInput(floatInput)).toBe(true);
expect(BlockInput.isBlockInput(booleanInput)).toBe(true);
expect(BlockInput.isBlockInput(arrayInput)).toBe(true);
});
});
});

View File

@@ -0,0 +1,131 @@
/*
* 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 { describe, it, expect } from 'vitest';
import {
StandardNodeType,
NODE_ORDER,
CONVERSATION_NODES,
} from '../../src/types/node-type';
describe('node-type', () => {
describe('StandardNodeType', () => {
it('应该包含所有预定义的节点类型', () => {
// 测试一些关键的节点类型
expect(StandardNodeType.Start).toBe('1');
expect(StandardNodeType.End).toBe('2');
expect(StandardNodeType.LLM).toBe('3');
expect(StandardNodeType.Api).toBe('4');
expect(StandardNodeType.Code).toBe('5');
expect(StandardNodeType.Dataset).toBe('6');
expect(StandardNodeType.If).toBe('8');
expect(StandardNodeType.SubWorkflow).toBe('9');
expect(StandardNodeType.Variable).toBe('11');
expect(StandardNodeType.Database).toBe('12');
expect(StandardNodeType.Output).toBe('13');
expect(StandardNodeType.Http).toBe('45');
});
it('应该不包含已废弃的节点类型', () => {
const nodeTypeValues = Object.values(StandardNodeType);
expect(nodeTypeValues).not.toContain('33'); // TriggerCreate
});
});
describe('NODE_ORDER', () => {
it('应该为每个节点类型定义正确的顺序', () => {
expect(NODE_ORDER[StandardNodeType.Start]).toBe(1);
expect(NODE_ORDER[StandardNodeType.End]).toBe(2);
expect(NODE_ORDER[StandardNodeType.Api]).toBe(3);
expect(NODE_ORDER[StandardNodeType.LLM]).toBe(4);
expect(NODE_ORDER[StandardNodeType.Code]).toBe(5);
expect(NODE_ORDER[StandardNodeType.Dataset]).toBe(6);
expect(NODE_ORDER[StandardNodeType.SubWorkflow]).toBe(7);
expect(NODE_ORDER[StandardNodeType.Imageflow]).toBe(8);
expect(NODE_ORDER[StandardNodeType.If]).toBe(9);
expect(NODE_ORDER[StandardNodeType.Loop]).toBe(10);
});
it('应该不包含已废弃节点类型的顺序', () => {
expect(NODE_ORDER).not.toHaveProperty('33'); // TriggerCreate
});
it('应该为所有需要排序的节点类型定义顺序', () => {
const nodeTypesWithOrder = Object.keys(NODE_ORDER);
const expectedNodeTypes = [
StandardNodeType.Start,
StandardNodeType.End,
StandardNodeType.Api,
StandardNodeType.LLM,
StandardNodeType.Code,
StandardNodeType.Dataset,
StandardNodeType.SubWorkflow,
StandardNodeType.Imageflow,
StandardNodeType.If,
StandardNodeType.Loop,
StandardNodeType.Intent,
StandardNodeType.Text,
StandardNodeType.Output,
StandardNodeType.Question,
StandardNodeType.Variable,
StandardNodeType.Database,
StandardNodeType.LTM,
StandardNodeType.Batch,
StandardNodeType.Input,
StandardNodeType.SetVariable,
StandardNodeType.Break,
StandardNodeType.Continue,
StandardNodeType.SceneChat,
StandardNodeType.SceneVariable,
StandardNodeType.TriggerUpsert,
StandardNodeType.TriggerRead,
StandardNodeType.TriggerDelete,
];
expectedNodeTypes.forEach(nodeType => {
expect(nodeTypesWithOrder).toContain(nodeType);
expect(NODE_ORDER[nodeType]).toBeGreaterThan(0);
});
});
});
describe('CONVERSATION_NODES', () => {
it('应该包含所有会话相关的节点类型', () => {
expect(CONVERSATION_NODES).toEqual([
StandardNodeType.CreateConversation,
StandardNodeType.UpdateConversation,
StandardNodeType.DeleteConversation,
StandardNodeType.QueryConversationList,
]);
});
it('应该只包含会话相关的节点类型', () => {
CONVERSATION_NODES.forEach(nodeType => {
expect([
StandardNodeType.CreateConversation,
StandardNodeType.UpdateConversation,
StandardNodeType.DeleteConversation,
StandardNodeType.QueryConversationList,
]).toContain(nodeType);
});
});
it('应该是一个数组', () => {
expect(Array.isArray(CONVERSATION_NODES)).toBe(true);
});
});
});

View File

@@ -0,0 +1,160 @@
/*
* 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 { describe, it, expect } from 'vitest';
import { StandardNodeType } from '../../src/types/node-type';
import type { WorkflowNodeJSON, WorkflowJSON } from '../../src/types/node';
describe('node types', () => {
describe('WorkflowNodeJSON', () => {
it('应该能够创建基本的节点 JSON', () => {
const node: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.Start,
data: {},
};
expect(node).toEqual({
id: '1',
type: StandardNodeType.Start,
data: {},
});
});
it('应该能够创建带有元数据的节点 JSON', () => {
const node: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.Start,
meta: {
position: { x: 100, y: 100 },
},
data: {},
};
expect(node.meta).toBeDefined();
expect(node.meta?.position).toEqual({ x: 100, y: 100 });
});
it('应该能够创建带有版本的节点 JSON', () => {
const node: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.Start,
data: {},
version: '1.0.0',
};
expect(node.version).toBe('1.0.0');
});
it('应该能够创建带有子节点的节点 JSON', () => {
const node: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.Start,
data: {},
blocks: [
{
id: '2',
type: StandardNodeType.Code,
data: {},
},
],
};
expect(node.blocks).toHaveLength(1);
expect(node.blocks?.[0].type).toBe(StandardNodeType.Code);
});
it('应该能够创建带有边的节点 JSON', () => {
const node: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.Start,
data: {},
edges: [
{
sourceNodeID: '1',
targetNodeID: '2',
},
],
};
expect(node.edges).toHaveLength(1);
expect(node.edges?.[0].sourceNodeID).toBe('1');
expect(node.edges?.[0].targetNodeID).toBe('2');
});
it('应该能够使用泛型数据类型', () => {
interface CustomData {
name: string;
value: number;
}
const node: WorkflowNodeJSON<CustomData> = {
id: '1',
type: StandardNodeType.Start,
data: {
name: 'test',
value: 42,
},
};
expect(node.data.name).toBe('test');
expect(node.data.value).toBe(42);
});
});
describe('WorkflowJSON', () => {
it('应该能够创建基本的工作流 JSON', () => {
const workflow: WorkflowJSON = {
nodes: [],
edges: [],
};
expect(workflow.nodes).toEqual([]);
expect(workflow.edges).toEqual([]);
});
it('应该能够创建包含节点和边的工作流 JSON', () => {
const workflow: WorkflowJSON = {
nodes: [
{
id: '1',
type: StandardNodeType.Start,
data: {},
},
{
id: '2',
type: StandardNodeType.End,
data: {},
},
],
edges: [
{
sourceNodeID: '1',
targetNodeID: '2',
},
],
};
expect(workflow.nodes).toHaveLength(2);
expect(workflow.edges).toHaveLength(1);
expect(workflow.nodes[0].type).toBe(StandardNodeType.Start);
expect(workflow.nodes[1].type).toBe(StandardNodeType.End);
expect(workflow.edges[0].sourceNodeID).toBe('1');
expect(workflow.edges[0].targetNodeID).toBe('2');
});
});
});

View File

@@ -0,0 +1,116 @@
/*
* 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 { describe, it, expect } from 'vitest';
import { ViewVariableType } from '../../src/types/view-variable-type';
import type { RecursedParamDefinition } from '../../src/types/param-definition';
import { ParamValueType } from '../../src/types/param-definition';
describe('param-definition', () => {
describe('RecursedParamDefinition', () => {
it('应该能够创建基本的参数定义', () => {
const param: RecursedParamDefinition = {
name: 'testParam',
type: ViewVariableType.String,
};
expect(param.name).toBe('testParam');
expect(param.type).toBe(ViewVariableType.String);
});
it('应该能够创建带有随机键的参数定义', () => {
const param: RecursedParamDefinition = {
name: 'testParam',
fieldRandomKey: 'random123',
type: ViewVariableType.String,
};
expect(param.fieldRandomKey).toBe('random123');
});
it('应该能够创建带有描述和必填标记的参数定义', () => {
const param: RecursedParamDefinition = {
name: 'testParam',
desc: 'This is a test parameter',
required: true,
type: ViewVariableType.String,
};
expect(param.desc).toBe('This is a test parameter');
expect(param.required).toBe(true);
});
it('应该能够创建带有子参数的参数定义', () => {
const param: RecursedParamDefinition = {
name: 'parentParam',
type: ViewVariableType.Object,
children: [
{
name: 'childParam1',
type: ViewVariableType.String,
},
{
name: 'childParam2',
type: ViewVariableType.Number,
},
],
};
expect(param.children).toHaveLength(2);
expect(param.children?.[0].name).toBe('childParam1');
expect(param.children?.[1].name).toBe('childParam2');
});
it('应该能够创建带有固定值的参数定义', () => {
const param: RecursedParamDefinition = {
name: 'testParam',
type: ViewVariableType.String,
isQuote: ParamValueType.FIXED,
fixedValue: 'test value',
};
expect(param.isQuote).toBe(ParamValueType.FIXED);
expect(param.fixedValue).toBe('test value');
});
it('应该能够创建带有引用值的参数定义', () => {
const param: RecursedParamDefinition = {
name: 'testParam',
type: ViewVariableType.String,
isQuote: ParamValueType.QUOTE,
quotedValue: ['node1', 'path', 'to', 'value'],
};
expect(param.isQuote).toBe(ParamValueType.QUOTE);
expect(param.quotedValue).toEqual(['node1', 'path', 'to', 'value']);
});
});
describe('ParamValueType', () => {
it('应该定义正确的参数值类型', () => {
expect(ParamValueType.QUOTE).toBe('quote');
expect(ParamValueType.FIXED).toBe('fixed');
});
it('应该只包含 QUOTE 和 FIXED 两种类型', () => {
const valueTypes = Object.values(ParamValueType);
expect(valueTypes).toHaveLength(2);
expect(valueTypes).toContain('quote');
expect(valueTypes).toContain('fixed');
});
});
});

View File

@@ -0,0 +1,244 @@
/*
* 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 { describe, it, expect } from 'vitest';
import { ViewVariableType } from '../../src/types/view-variable-type';
import {
ViewVariableTreeNode,
type ViewVariableMeta,
} from '../../src/types/view-variable-tree';
describe('view-variable-tree', () => {
describe('ViewVariableTreeNode 类型', () => {
it('应该能够创建基本的变量树节点', () => {
const node: ViewVariableTreeNode = {
key: 'test',
type: ViewVariableType.String,
name: 'Test Node',
};
expect(node.key).toBe('test');
expect(node.type).toBe(ViewVariableType.String);
expect(node.name).toBe('Test Node');
});
it('应该能够创建带有可选属性的变量树节点', () => {
const node: ViewVariableTreeNode = {
key: 'test',
type: ViewVariableType.String,
name: 'Test Node',
required: true,
description: 'Test Description',
isPreset: true,
enabled: true,
label: 'Custom Label',
defaultValue: 'default',
};
expect(node.required).toBe(true);
expect(node.description).toBe('Test Description');
expect(node.isPreset).toBe(true);
expect(node.enabled).toBe(true);
expect(node.label).toBe('Custom Label');
expect(node.defaultValue).toBe('default');
});
it('应该能够创建带有子节点的变量树节点', () => {
const node: ViewVariableTreeNode = {
key: 'parent',
type: ViewVariableType.Object,
name: 'Parent Node',
children: [
{
key: 'child1',
type: ViewVariableType.String,
name: 'Child Node 1',
},
{
key: 'child2',
type: ViewVariableType.Number,
name: 'Child Node 2',
},
],
};
expect(node.children).toHaveLength(2);
expect(node.children?.[0].key).toBe('child1');
expect(node.children?.[1].type).toBe(ViewVariableType.Number);
});
});
describe('ViewVariableMeta 类型', () => {
it('应该能够创建基本的变量元数据', () => {
const meta: ViewVariableMeta = {
key: 'test',
type: ViewVariableType.String,
name: 'Test Meta',
required: true,
description: 'Test Description',
readonly: true,
mutable: false,
};
expect(meta.key).toBe('test');
expect(meta.type).toBe(ViewVariableType.String);
expect(meta.name).toBe('Test Meta');
expect(meta.required).toBe(true);
expect(meta.description).toBe('Test Description');
expect(meta.readonly).toBe(true);
expect(meta.mutable).toBe(false);
});
it('应该继承 ViewVariableTreeNode 的所有属性', () => {
const meta: ViewVariableMeta = {
key: 'test',
type: ViewVariableType.String,
name: 'Test Meta',
isPreset: true,
enabled: true,
label: 'Custom Label',
defaultValue: 'default',
readonly: true,
mutable: false,
};
expect(meta.isPreset).toBe(true);
expect(meta.enabled).toBe(true);
expect(meta.label).toBe('Custom Label');
expect(meta.defaultValue).toBe('default');
});
});
describe('ViewVariableTreeNode 命名空间函数', () => {
const createTestTree = (): ViewVariableTreeNode => ({
key: 'root',
type: ViewVariableType.Object,
name: 'Root',
children: [
{
key: 'child1',
type: ViewVariableType.Object,
name: 'Child1',
children: [
{
key: 'grandchild1',
type: ViewVariableType.String,
name: 'GrandChild1',
},
],
},
{
key: 'child2',
type: ViewVariableType.String,
name: 'Child2',
},
],
});
describe('getVariableTreeNodeByPath', () => {
it('应该能够通过路径找到正确的节点', () => {
const tree = createTestTree();
const node = ViewVariableTreeNode.getVariableTreeNodeByPath(tree, [
'root',
'child1',
'grandchild1',
]);
expect(node).toBeDefined();
expect(node?.key).toBe('grandchild1');
expect(node?.name).toBe('GrandChild1');
});
it('应该在路径无效时返回 undefined', () => {
const tree = createTestTree();
const node = ViewVariableTreeNode.getVariableTreeNodeByPath(tree, [
'root',
'nonexistent',
]);
expect(node).toBeUndefined();
});
it('应该能处理空路径', () => {
const tree = createTestTree();
const node = ViewVariableTreeNode.getVariableTreeNodeByPath(tree, []);
expect(node).toBeDefined();
expect(node?.key).toBe('root');
});
});
describe('keyPathToNameString', () => {
it('应该能够将路径转换为名称字符串', () => {
const tree = createTestTree();
const nameStr = ViewVariableTreeNode.keyPathToNameString(tree, [
'root',
'child1',
'grandchild1',
]);
expect(nameStr).toBe('Root.Child1.GrandChild1');
});
it('应该在路径无效时返回空字符串', () => {
const tree = createTestTree();
const nameStr = ViewVariableTreeNode.keyPathToNameString(tree, [
'root',
'nonexistent',
]);
expect(nameStr).toBe('');
});
it('应该能处理空路径', () => {
const tree = createTestTree();
const nameStr = ViewVariableTreeNode.keyPathToNameString(tree, []);
expect(nameStr).toBe('');
});
});
describe('nameStringToKeyPath', () => {
it('应该能够将名称字符串转换为路径', () => {
const tree = createTestTree();
const keyPath = ViewVariableTreeNode.nameStringToKeyPath(
tree,
'Root.Child1.GrandChild1',
);
expect(keyPath).toEqual(['root', 'child1', 'grandchild1']);
});
it('应该在名称无效时返回空数组', () => {
const tree = createTestTree();
const keyPath = ViewVariableTreeNode.nameStringToKeyPath(
tree,
'Root.NonExistent',
);
expect(keyPath).toEqual([]);
});
it('应该能处理空字符串', () => {
const tree = createTestTree();
const keyPath = ViewVariableTreeNode.nameStringToKeyPath(tree, '');
expect(keyPath).toEqual([]);
});
});
});
});

View File

@@ -0,0 +1,372 @@
/*
* 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 { describe, it, expect } from 'vitest';
import {
ValueExpressionType,
ValueExpression,
type RefExpressionContent,
type LiteralExpression,
type RefExpression,
type InputValueVO,
type InputTypeValueVO,
BatchMode,
type BatchVOInputList,
type BatchVO,
type OutputValueVO,
} from '../../src/types/vo';
import { ViewVariableType } from '../../src/types/view-variable-type';
describe('vo', () => {
describe('RefExpressionContent', () => {
it('应该能够创建引用表达式内容', () => {
const content: RefExpressionContent = {
keyPath: ['node1', 'output1'],
};
expect(content.keyPath).toEqual(['node1', 'output1']);
});
});
describe('ValueExpressionType', () => {
it('应该定义正确的表达式类型', () => {
expect(ValueExpressionType.LITERAL).toBe('literal');
expect(ValueExpressionType.REF).toBe('ref');
});
});
describe('LiteralExpression', () => {
it('应该能够创建字符串字面量表达式', () => {
const expr: LiteralExpression = {
type: ValueExpressionType.LITERAL,
content: 'test string',
};
expect(expr.type).toBe(ValueExpressionType.LITERAL);
expect(expr.content).toBe('test string');
});
it('应该能够创建数字字面量表达式', () => {
const expr: LiteralExpression = {
type: ValueExpressionType.LITERAL,
content: 42,
};
expect(expr.content).toBe(42);
});
it('应该能够创建布尔字面量表达式', () => {
const expr: LiteralExpression = {
type: ValueExpressionType.LITERAL,
content: true,
};
expect(expr.content).toBe(true);
});
it('应该能够创建数组字面量表达式', () => {
const expr: LiteralExpression = {
type: ValueExpressionType.LITERAL,
content: [1, 'two', true],
};
expect(expr.content).toEqual([1, 'two', true]);
});
it('应该能够包含原始元数据', () => {
const expr: LiteralExpression = {
type: ValueExpressionType.LITERAL,
content: 'test',
rawMeta: {
source: 'user input',
timestamp: 123456789,
},
};
expect(expr.rawMeta).toEqual({
source: 'user input',
timestamp: 123456789,
});
});
});
describe('RefExpression', () => {
it('应该能够创建引用表达式', () => {
const expr: RefExpression = {
type: ValueExpressionType.REF,
content: {
keyPath: ['node1', 'output1'],
},
};
expect(expr.type).toBe(ValueExpressionType.REF);
expect(expr.content?.keyPath).toEqual(['node1', 'output1']);
});
it('应该允许内容为可选', () => {
const expr: RefExpression = {
type: ValueExpressionType.REF,
};
expect(expr.content).toBeUndefined();
});
});
describe('ValueExpression 命名空间函数', () => {
describe('isRef', () => {
it('应该正确识别引用表达式', () => {
const refExpr: ValueExpression = {
type: ValueExpressionType.REF,
content: { keyPath: ['node1', 'output1'] },
};
const literalExpr: ValueExpression = {
type: ValueExpressionType.LITERAL,
content: 'test',
};
expect(ValueExpression.isRef(refExpr)).toBe(true);
expect(ValueExpression.isRef(literalExpr)).toBe(false);
});
});
describe('isLiteral', () => {
it('应该正确识别字面量表达式', () => {
const refExpr: ValueExpression = {
type: ValueExpressionType.REF,
content: { keyPath: ['node1', 'output1'] },
};
const literalExpr: ValueExpression = {
type: ValueExpressionType.LITERAL,
content: 'test',
};
expect(ValueExpression.isLiteral(literalExpr)).toBe(true);
expect(ValueExpression.isLiteral(refExpr)).toBe(false);
});
});
describe('isExpression', () => {
it('应该正确识别有效的表达式', () => {
const refExpr: ValueExpression = {
type: ValueExpressionType.REF,
content: { keyPath: ['node1', 'output1'] },
};
const literalExpr: ValueExpression = {
type: ValueExpressionType.LITERAL,
content: 'test',
};
expect(ValueExpression.isExpression(refExpr)).toBe(true);
expect(ValueExpression.isExpression(literalExpr)).toBe(true);
expect(ValueExpression.isExpression(undefined)).toBe(false);
});
});
describe('isEmpty', () => {
it('应该正确识别空的字面量表达式', () => {
expect(
ValueExpression.isEmpty({
type: ValueExpressionType.LITERAL,
content: '',
}),
).toBe(true);
expect(
ValueExpression.isEmpty({
type: ValueExpressionType.LITERAL,
content: undefined,
}),
).toBe(true);
expect(
ValueExpression.isEmpty({
type: ValueExpressionType.LITERAL,
content: 'test',
}),
).toBe(false);
expect(
ValueExpression.isEmpty({
type: ValueExpressionType.LITERAL,
content: false,
}),
).toBe(false);
});
it('应该正确识别空的引用表达式', () => {
expect(
ValueExpression.isEmpty({
type: ValueExpressionType.REF,
content: { keyPath: [] },
}),
).toBe(true);
expect(
ValueExpression.isEmpty({
type: ValueExpressionType.REF,
content: { keyPath: ['node1'] },
}),
).toBe(false);
});
it('应该正确处理 undefined 值', () => {
expect(ValueExpression.isEmpty(undefined)).toBe(true);
});
});
});
describe('InputValueVO', () => {
it('应该能够创建输入值对象', () => {
const vo: InputValueVO = {
name: 'test input',
input: {
type: ValueExpressionType.LITERAL,
content: 'test value',
},
};
expect(vo.name).toBe('test input');
expect(vo.input.type).toBe(ValueExpressionType.LITERAL);
expect(vo.input.content).toBe('test value');
});
it('应该允许名称为可选', () => {
const vo: InputValueVO = {
input: {
type: ValueExpressionType.LITERAL,
content: 'test value',
},
};
expect(vo.name).toBeUndefined();
});
});
describe('InputTypeValueVO', () => {
it('应该能够创建带类型的输入值对象', () => {
const vo: InputTypeValueVO = {
name: 'test input',
type: ViewVariableType.String,
input: {
type: ValueExpressionType.LITERAL,
content: 'test value',
},
};
expect(vo.name).toBe('test input');
expect(vo.type).toBe(ViewVariableType.String);
expect(vo.input.type).toBe(ValueExpressionType.LITERAL);
});
});
describe('BatchMode', () => {
it('应该定义正确的批处理模式', () => {
expect(BatchMode.Single).toBe('single');
expect(BatchMode.Batch).toBe('batch');
});
});
describe('BatchVOInputList', () => {
it('应该能够创建批处理输入列表项', () => {
const inputList: BatchVOInputList = {
id: '1',
name: 'test batch input',
input: {
type: ValueExpressionType.REF,
content: { keyPath: ['node1', 'output1'] },
},
};
expect(inputList.id).toBe('1');
expect(inputList.name).toBe('test batch input');
expect(inputList.input.type).toBe(ValueExpressionType.REF);
});
});
describe('BatchVO', () => {
it('应该能够创建批处理配置对象', () => {
const vo: BatchVO = {
batchSize: 10,
concurrentSize: 2,
inputLists: [
{
id: '1',
name: 'input1',
input: {
type: ValueExpressionType.REF,
content: { keyPath: ['node1', 'output1'] },
},
},
],
};
expect(vo.batchSize).toBe(10);
expect(vo.concurrentSize).toBe(2);
expect(vo.inputLists).toHaveLength(1);
expect(vo.inputLists[0].id).toBe('1');
});
});
describe('OutputValueVO', () => {
it('应该能够创建基本的输出值对象', () => {
const vo: OutputValueVO = {
key: 'output1',
name: 'Output 1',
type: ViewVariableType.String,
};
expect(vo.key).toBe('output1');
expect(vo.name).toBe('Output 1');
expect(vo.type).toBe(ViewVariableType.String);
});
it('应该能够创建带可选属性的输出值对象', () => {
const vo: OutputValueVO = {
key: 'output1',
name: 'Output 1',
type: ViewVariableType.String,
description: 'Test output',
readonly: true,
required: true,
};
expect(vo.description).toBe('Test output');
expect(vo.readonly).toBe(true);
expect(vo.required).toBe(true);
});
it('应该能够创建带子节点的输出值对象', () => {
const vo: OutputValueVO = {
key: 'parent',
name: 'Parent Output',
type: ViewVariableType.Object,
children: [
{
key: 'child1',
name: 'Child 1',
type: ViewVariableType.String,
},
{
key: 'child2',
name: 'Child 2',
type: ViewVariableType.Number,
},
],
};
expect(vo.children).toHaveLength(2);
expect(vo.children?.[0].key).toBe('child1');
expect(vo.children?.[1].type).toBe(ViewVariableType.Number);
});
});
});

View File

@@ -0,0 +1,61 @@
/*
* 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 { describe, it, expect } from 'vitest';
import { concatTestId } from '../../src/utils/concat-test-id';
describe('concat-test-id', () => {
it('应该正确连接多个测试 ID', () => {
const result = concatTestId('a', 'b', 'c');
expect(result).toBe('a.b.c');
});
it('应该过滤掉空字符串', () => {
const result = concatTestId('a', '', 'c');
expect(result).toBe('a.c');
});
it('应该过滤掉 undefined 和 null', () => {
const result = concatTestId('a', undefined as any, 'c', null as any);
expect(result).toBe('a.c');
});
it('应该在只有一个有效 ID 时正确返回', () => {
const result = concatTestId('a');
expect(result).toBe('a');
});
it('应该在所有 ID 都无效时返回空字符串', () => {
const result = concatTestId('', undefined as any, null as any);
expect(result).toBe('');
});
it('应该在没有参数时返回空字符串', () => {
const result = concatTestId();
expect(result).toBe('');
});
it('应该正确处理包含点号的 ID', () => {
const result = concatTestId('a.x', 'b', 'c.y');
expect(result).toBe('a.x.b.c.y');
});
it('应该正确处理数字 ID', () => {
const result = concatTestId('1', '2', '3');
expect(result).toBe('1.2.3');
});
});

View File

@@ -0,0 +1,157 @@
/*
* 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 { describe, it, expect, vi } from 'vitest';
import { FlowNodeFormData } from '@flowgram-adapter/free-layout-editor';
import type { FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import { getFormValueByPathEnds } from '../../src/utils/form-helpers';
// Mock lodash-es
vi.mock('lodash-es', () => ({
cloneDeep: vi.fn(val => {
if (val === undefined) {
return undefined;
}
return JSON.parse(JSON.stringify(val));
}),
}));
describe('form-helpers', () => {
describe('getFormValueByPathEnds', () => {
const createMockFormModel = (paths: Record<string, unknown>) => {
const formItemPathMap = new Map<string, unknown>();
Object.entries(paths).forEach(([path, value]) => {
formItemPathMap.set(path, value);
});
return {
formItemPathMap,
getFormItemValueByPath: vi.fn(path => paths[path]),
};
};
const createMockNode = (
formModel: ReturnType<typeof createMockFormModel>,
) =>
({
getData: vi.fn((dataType: symbol) => {
if (Object.is(dataType, FlowNodeFormData)) {
return { formModel };
}
return null;
}),
getNodeRegistry: () => ({}),
}) as unknown as FlowNodeEntity;
it('应该返回匹配路径结尾的表单项值', () => {
const formModel = createMockFormModel({
'/form/test/value': 'test value',
'/form/other/path': 'other value',
});
const node = createMockNode(formModel);
const result = getFormValueByPathEnds(node, 'test/value');
expect(result).toBe('test value');
expect(formModel.getFormItemValueByPath).toHaveBeenCalledWith(
'/form/test/value',
);
});
it('应该在找不到匹配路径时返回 undefined', () => {
const formModel = createMockFormModel({
'/form/test/value': 'test value',
});
const node = createMockNode(formModel);
const result = getFormValueByPathEnds(node, 'non/existent');
expect(result).toBeUndefined();
expect(formModel.getFormItemValueByPath).not.toHaveBeenCalled();
});
it('应该正确处理复杂对象值', () => {
const complexValue = {
name: 'test',
value: 42,
nested: {
field: 'nested value',
},
};
const formModel = createMockFormModel({
'/form/complex/value': complexValue,
});
const node = createMockNode(formModel);
const result = getFormValueByPathEnds(node, 'complex/value');
expect(result).toEqual(complexValue);
expect(formModel.getFormItemValueByPath).toHaveBeenCalledWith(
'/form/complex/value',
);
});
it('应该返回深拷贝的值而不是引用', () => {
const originalValue = { name: 'test' };
const formModel = createMockFormModel({
'/form/object/value': originalValue,
});
const node = createMockNode(formModel);
const result = getFormValueByPathEnds(node, 'object/value');
expect(result).toEqual(originalValue);
expect(result).not.toBe(originalValue);
});
it('应该在有多个匹配路径时返回第一个匹配的值', () => {
const formModel = createMockFormModel({
'/form/path/test/value': 'first value',
'/other/path/test/value': 'second value',
});
const node = createMockNode(formModel);
const result = getFormValueByPathEnds(node, 'test/value');
expect(result).toBe('first value');
expect(formModel.getFormItemValueByPath).toHaveBeenCalledWith(
'/form/path/test/value',
);
});
it('应该正确处理空值', () => {
const formModel = createMockFormModel({
'/form/empty/value': null,
'/form/undefined/value': undefined,
});
const node = createMockNode(formModel);
const nullResult = getFormValueByPathEnds(node, 'empty/value');
expect(nullResult).toBeNull();
const undefinedResult = getFormValueByPathEnds(node, 'undefined/value');
expect(undefinedResult).toBeUndefined();
});
it('应该正确处理数组值', () => {
const arrayValue = [1, 2, { name: 'test' }];
const formModel = createMockFormModel({
'/form/array/value': arrayValue,
});
const node = createMockNode(formModel);
const result = getFormValueByPathEnds(node, 'array/value');
expect(result).toEqual(arrayValue);
expect(Array.isArray(result)).toBe(true);
});
});
});

View File

@@ -0,0 +1,35 @@
/*
* 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 { describe, it, expect } from 'vitest';
import { WorkflowMode } from '@coze-arch/bot-api/workflow_api';
import { isGeneralWorkflow } from '../../src/utils/is-general-workflow';
describe('is-general-workflow', () => {
it('应该在 flowMode 为 Workflow 时返回 true', () => {
expect(isGeneralWorkflow(WorkflowMode.Workflow)).toBe(true);
});
it('应该在 flowMode 为 ChatFlow 时返回 true', () => {
expect(isGeneralWorkflow(WorkflowMode.ChatFlow)).toBe(true);
});
it('应该在 flowMode 为其他值时返回 false', () => {
// 测试其他可能的 WorkflowMode 值
expect(isGeneralWorkflow(WorkflowMode.Imageflow)).toBe(false);
});
});

View File

@@ -0,0 +1,282 @@
/*
* 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 { describe, it, expect, vi } from 'vitest';
import { defaultParser } from '../../../../src/utils/node-result-extractor/parsers';
import { StandardNodeType } from '../../../../src/types';
import type { WorkflowJSON } from '../../../../src/types';
import { TerminatePlanType } from '../../../../src/api';
import type { NodeResult } from '../../../../src/api';
// Mock @coze-arch/bot-utils
vi.mock('@coze-arch/bot-utils', () => ({
typeSafeJSONParse: (str: string) => {
try {
const result = JSON.parse(str);
// 如果是批处理数据,确保返回数组类型
if (str === 'invalid json') {
return str;
}
// 如果是批处理数据,确保返回数组类型
if (str.includes('batch')) {
return Array.isArray(result) ? result : [];
}
return result;
} catch {
// 如果是批处理数据,返回空数组
if (str.includes('batch')) {
return [];
}
return str;
}
},
}));
// Mock parseImagesFromOutputData
vi.mock('../../../../src/utils/output-image-parser', () => ({
parseImagesFromOutputData: vi.fn(({ outputData }) => {
if (typeof outputData === 'string' && outputData.includes('image')) {
return ['https://example.com/image.png'];
}
if (typeof outputData === 'object' && outputData !== null) {
const str = JSON.stringify(outputData);
if (str.includes('image')) {
return ['https://example.com/image.png'];
}
}
return [];
}),
}));
vi.mock('../../../../src/api', () => ({
TerminatePlanType: {
USESETTING: 2,
},
}));
describe('default-parser', () => {
const createMockNodeResult = (
nodeId: string,
overrides: Partial<NodeResult> = {},
): NodeResult => ({
nodeId,
isBatch: false,
input: 'test input',
output: 'test output',
raw_output: 'test raw output',
extra: '{}',
items: '[]',
...overrides,
});
const createMockWorkflowSchema = (
nodeId: string,
nodeType = StandardNodeType.LLM,
): WorkflowJSON => ({
nodes: [
{
id: nodeId,
type: nodeType,
data: {},
},
],
edges: [],
});
describe('非批处理节点', () => {
it('应该正确解析 LLM 节点结果', () => {
const nodeResult = createMockNodeResult('1');
const workflowSchema = createMockWorkflowSchema(
'1',
StandardNodeType.LLM,
);
const result = defaultParser(nodeResult, workflowSchema);
expect(result.nodeId).toBe('1');
expect(result.nodeType).toBe(StandardNodeType.LLM);
expect(result.isBatch).toBe(false);
expect(result.caseResult).toHaveLength(1);
expect(result.caseResult?.[0].dataList).toHaveLength(3); // 输入、原始输出、最终输出
});
it('应该正确解析 Code 节点结果', () => {
const nodeResult = createMockNodeResult('1');
const workflowSchema = createMockWorkflowSchema(
'1',
StandardNodeType.Code,
);
const result = defaultParser(nodeResult, workflowSchema);
expect(result.nodeType).toBe(StandardNodeType.Code);
expect(result.caseResult?.[0].dataList).toHaveLength(3); // 输入、原始输出、最终输出
});
it('应该正确解析 Start 节点结果', () => {
const nodeResult = createMockNodeResult('1');
const workflowSchema = createMockWorkflowSchema(
'1',
StandardNodeType.Start,
);
const result = defaultParser(nodeResult, workflowSchema);
expect(result.nodeType).toBe(StandardNodeType.Start);
expect(result.caseResult?.[0].dataList).toHaveLength(1); // 只有输入
});
it('应该正确解析 End 节点结果', () => {
const nodeResult = createMockNodeResult('1', {
extra: JSON.stringify({
response_extra: {
terminal_plan: TerminatePlanType.USESETTING,
},
}),
output: JSON.stringify({ content: 'test content' }),
raw_output: JSON.stringify({ content: 'test raw content' }),
});
const workflowSchema = createMockWorkflowSchema(
'1',
StandardNodeType.End,
);
const result = defaultParser(nodeResult, workflowSchema);
expect(result.nodeType).toBe(StandardNodeType.End);
expect(result.caseResult?.[0].dataList).toHaveLength(2); // 输出变量、回答内容
});
it('应该正确解析 Message 节点结果', () => {
const nodeResult = createMockNodeResult('1');
const workflowSchema = createMockWorkflowSchema(
'1',
StandardNodeType.Output,
);
const result = defaultParser(nodeResult, workflowSchema);
expect(result.nodeType).toBe(StandardNodeType.Output);
expect(result.caseResult?.[0].dataList).toHaveLength(2); // 输出变量、回答内容
});
it('应该正确解析包含图片的输出', () => {
const nodeResult = createMockNodeResult('1', {
output: JSON.stringify({ content: 'test output with image' }),
});
const workflowSchema = createMockWorkflowSchema('1');
const result = defaultParser(nodeResult, workflowSchema);
expect(result.caseResult?.[0].imgList).toEqual([
'https://example.com/image.png',
]);
});
});
describe('批处理节点', () => {
it('应该正确解析批处理节点结果', () => {
const nodeResult = createMockNodeResult('1', {
isBatch: true,
batch: JSON.stringify([
createMockNodeResult('1'),
createMockNodeResult('1'),
]),
});
const workflowSchema = createMockWorkflowSchema('1');
const result = defaultParser(nodeResult, workflowSchema);
expect(result.isBatch).toBe(true);
expect(result.caseResult).toHaveLength(2);
expect(result.caseResult?.[0].dataList).toBeDefined();
expect(result.caseResult?.[1].dataList).toBeDefined();
});
it('应该正确处理空的批处理结果', () => {
const nodeResult = createMockNodeResult('1', {
isBatch: true,
batch: '[]',
});
const workflowSchema = createMockWorkflowSchema('1');
const result = defaultParser(nodeResult, workflowSchema);
expect(result.isBatch).toBe(true);
expect(result.caseResult).toEqual([]);
});
it('应该正确处理无效的批处理 JSON', () => {
const nodeResult = createMockNodeResult('1', {
isBatch: true,
batch: 'invalid batch json',
});
const workflowSchema = createMockWorkflowSchema('1');
const result = defaultParser(nodeResult, workflowSchema);
expect(result.isBatch).toBe(true);
expect(result.caseResult).toEqual([]);
});
it('应该正确处理批处理中的 null 或 undefined 结果', () => {
const nodeResult = createMockNodeResult('1', {
isBatch: true,
batch: JSON.stringify([null, createMockNodeResult('1'), undefined]),
});
const workflowSchema = createMockWorkflowSchema('1');
const result = defaultParser(nodeResult, workflowSchema);
expect(result.isBatch).toBe(true);
expect(result.caseResult).toHaveLength(1);
});
});
describe('特殊情况处理', () => {
it('应该正确处理无效的 JSON 输入', () => {
const nodeResult = createMockNodeResult('1', {
input: 'invalid json',
output: 'invalid json',
raw_output: 'invalid json',
});
const workflowSchema = createMockWorkflowSchema('1');
const result = defaultParser(nodeResult, workflowSchema);
expect(
result.caseResult?.[0].dataList?.some(
item => item.data === 'invalid json',
),
).toBe(true);
});
it('应该正确处理 Text 节点的原始输出', () => {
const nodeResult = createMockNodeResult('1', {
raw_output: '{"key": "value"}', // 即使是有效的 JSON 字符串也应该保持原样
});
const workflowSchema = createMockWorkflowSchema(
'1',
StandardNodeType.Text,
);
const result = defaultParser(nodeResult, workflowSchema);
const rawOutput = result.caseResult?.[0].dataList?.find(
item => item.title === '原始输出',
);
expect(rawOutput?.data).toBe('{"key": "value"}');
});
it('应该正确处理不存在的节点类型', () => {
const nodeResult = createMockNodeResult('1');
const workflowSchema = createMockWorkflowSchema('2'); // 不同的节点 ID
const result = defaultParser(nodeResult, workflowSchema);
expect(result.nodeType).toBeUndefined();
expect(result.caseResult).toBeDefined();
});
});
});

View File

@@ -0,0 +1,130 @@
/*
* 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 { describe, it, expect } from 'vitest';
import {
type CaseResultData,
type NodeResultExtracted,
} from '../../../src/utils/node-result-extractor/type';
import { StandardNodeType } from '../../../src/types';
describe('node-result-extractor/type', () => {
describe('CaseResultData', () => {
it('应该能够创建基本的 CaseResultData 对象', () => {
const data: CaseResultData = {
dataList: [
{
title: 'test',
data: 'test data',
},
],
imgList: ['https://example.com/image.png'],
};
expect(data.dataList).toHaveLength(1);
expect(data.dataList?.[0].title).toBe('test');
expect(data.dataList?.[0].data).toBe('test data');
expect(data.imgList).toHaveLength(1);
expect(data.imgList?.[0]).toBe('https://example.com/image.png');
});
it('应该允许所有属性为可选', () => {
const data: CaseResultData = {};
expect(data.dataList).toBeUndefined();
expect(data.imgList).toBeUndefined();
});
it('应该允许 dataList 中的 data 为任意类型', () => {
const data: CaseResultData = {
dataList: [
{ title: 'string', data: 'string data' },
{ title: 'number', data: 123 },
{ title: 'boolean', data: true },
{ title: 'object', data: { key: 'value' } },
{ title: 'array', data: [1, 2, 3] },
],
};
expect(data.dataList).toHaveLength(5);
expect(typeof data.dataList?.[0].data).toBe('string');
expect(typeof data.dataList?.[1].data).toBe('number');
expect(typeof data.dataList?.[2].data).toBe('boolean');
expect(typeof data.dataList?.[3].data).toBe('object');
expect(Array.isArray(data.dataList?.[4].data)).toBe(true);
});
});
describe('NodeResultExtracted', () => {
it('应该能够创建基本的 NodeResultExtracted 对象', () => {
const result: NodeResultExtracted = {
nodeId: '123',
nodeType: StandardNodeType.LLM,
isBatch: false,
caseResult: [
{
dataList: [{ title: 'test', data: 'test data' }],
imgList: ['https://example.com/image.png'],
},
],
};
expect(result.nodeId).toBe('123');
expect(result.nodeType).toBe(StandardNodeType.LLM);
expect(result.isBatch).toBe(false);
expect(result.caseResult).toHaveLength(1);
});
it('应该允许所有属性为可选', () => {
const result: NodeResultExtracted = {};
expect(result.nodeId).toBeUndefined();
expect(result.nodeType).toBeUndefined();
expect(result.isBatch).toBeUndefined();
expect(result.caseResult).toBeUndefined();
});
it('应该允许 caseResult 为空数组', () => {
const result: NodeResultExtracted = {
nodeId: '123',
nodeType: StandardNodeType.LLM,
isBatch: false,
caseResult: [],
};
expect(result.caseResult).toHaveLength(0);
});
it('应该允许多个 caseResult', () => {
const result: NodeResultExtracted = {
nodeId: '123',
nodeType: StandardNodeType.LLM,
isBatch: true,
caseResult: [
{
dataList: [{ title: 'case1', data: 'data1' }],
},
{
dataList: [{ title: 'case2', data: 'data2' }],
},
],
};
expect(result.caseResult).toHaveLength(2);
expect(result.caseResult?.[0].dataList?.[0].title).toBe('case1');
expect(result.caseResult?.[1].dataList?.[0].title).toBe('case2');
});
});
});

View File

@@ -0,0 +1,300 @@
/*
* 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 { describe, it, expect } from 'vitest';
import type { WorkflowNodeJSON } from '@flowgram-adapter/free-layout-editor';
import { parseImagesFromOutputData } from '../../src/utils/output-image-parser';
import {
StandardNodeType,
VariableTypeDTO,
AssistTypeDTO,
} from '../../src/types';
describe('output-image-parser', () => {
describe('parseImagesFromOutputData', () => {
it('应该在没有 nodeSchema 或 outputData 时返回空数组', () => {
expect(parseImagesFromOutputData({})).toEqual([]);
expect(
parseImagesFromOutputData({
outputData: 'test',
nodeSchema: undefined,
}),
).toEqual([]);
expect(
parseImagesFromOutputData({
outputData: undefined,
nodeSchema: {},
}),
).toEqual([]);
});
it('应该在节点类型被排除时返回空数组', () => {
const nodeSchema: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.LLM,
data: {
outputs: [
{
name: 'image',
type: VariableTypeDTO.string,
assistType: AssistTypeDTO.image,
},
],
},
};
expect(
parseImagesFromOutputData({
outputData: { image: 'https://example.com/image.png' },
nodeSchema,
excludeNodeTypes: [StandardNodeType.LLM],
}),
).toEqual([]);
});
it('应该正确解析 End 节点的图片链接', () => {
const nodeSchema: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.End,
data: {
inputs: {
inputParameters: [
{
name: 'image',
input: {
type: VariableTypeDTO.string,
assistType: AssistTypeDTO.image,
},
},
],
},
},
};
const result = parseImagesFromOutputData({
outputData: { image: 'https://example.com/image.png' },
nodeSchema,
});
expect(result).toEqual(['https://example.com/image.png']);
});
it('应该正确解析 Message 节点的图片链接', () => {
const nodeSchema: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.Output,
data: {
inputs: {
inputParameters: [
{
name: 'image',
input: {
type: VariableTypeDTO.string,
assistType: AssistTypeDTO.image,
},
},
],
},
},
};
const result = parseImagesFromOutputData({
outputData: { image: 'https://example.com/image.png' },
nodeSchema,
});
expect(result).toEqual(['https://example.com/image.png']);
});
it('应该正确解析其他节点类型的图片链接', () => {
const nodeSchema: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.LLM,
data: {
outputs: [
{
name: 'image',
type: VariableTypeDTO.string,
assistType: AssistTypeDTO.image,
},
],
},
};
const result = parseImagesFromOutputData({
outputData: { image: 'https://example.com/image.png' },
nodeSchema,
});
expect(result).toEqual(['https://example.com/image.png']);
});
it('应该正确解析图片列表', () => {
const nodeSchema: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.LLM,
data: {
outputs: [
{
name: 'images',
type: VariableTypeDTO.list,
schema: {
type: VariableTypeDTO.string,
assistType: AssistTypeDTO.image,
},
},
],
},
};
const result = parseImagesFromOutputData({
outputData: {
images: [
'https://example.com/image1.png',
'https://example.com/image2.png',
],
},
nodeSchema,
});
expect(result).toEqual([
'https://example.com/image1.png',
'https://example.com/image2.png',
]);
});
it('应该正确解析对象中的图片', () => {
const nodeSchema: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.LLM,
data: {
outputs: [
{
name: 'data',
type: VariableTypeDTO.object,
schema: [
{
name: 'image',
type: VariableTypeDTO.string,
assistType: AssistTypeDTO.image,
},
{
name: 'text',
type: VariableTypeDTO.string,
},
],
},
],
},
};
const result = parseImagesFromOutputData({
outputData: {
data: {
image: 'https://example.com/image.png',
text: 'Some text',
},
},
nodeSchema,
});
expect(result).toEqual(['https://example.com/image.png']);
});
it('应该正确处理 SVG 类型的图片', () => {
const nodeSchema: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.LLM,
data: {
outputs: [
{
name: 'svg',
type: VariableTypeDTO.string,
assistType: AssistTypeDTO.svg,
},
],
},
};
const result = parseImagesFromOutputData({
outputData: { svg: 'https://example.com/image.svg' },
nodeSchema,
});
expect(result).toEqual(['https://example.com/image.svg']);
});
it('应该正确处理原生图片类型', () => {
const nodeSchema: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.LLM,
data: {
outputs: [
{
name: 'image',
type: VariableTypeDTO.image,
},
],
},
};
const result = parseImagesFromOutputData({
outputData: { image: 'https://example.com/image.png' },
nodeSchema,
});
expect(result).toEqual(['https://example.com/image.png']);
});
it('应该过滤掉空的图片链接', () => {
const nodeSchema: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.LLM,
data: {
outputs: [
{
name: 'images',
type: VariableTypeDTO.list,
schema: {
type: VariableTypeDTO.string,
assistType: AssistTypeDTO.image,
},
},
],
},
};
const result = parseImagesFromOutputData({
outputData: {
images: [
'https://example.com/image1.png',
'',
null,
undefined,
'https://example.com/image2.png',
],
},
nodeSchema,
});
expect(result).toEqual([
'https://example.com/image1.png',
'https://example.com/image2.png',
]);
});
});
});

View File

@@ -0,0 +1,121 @@
/*
* 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 { describe, it, expect } from 'vitest';
import { expressionParser } from '../../../../src/utils/schema-extractor/parsers/expression-parser';
import type { ValueExpressionDTO } from '../../../../src/types/dto';
describe('expression-parser', () => {
it('should handle empty input', () => {
const result = expressionParser([]);
expect(result).toEqual([]);
});
it('should parse string literal expression', () => {
const expression: ValueExpressionDTO = {
type: 'string',
value: {
type: 'literal',
content: 'hello',
},
};
const result = expressionParser(expression);
expect(result).toEqual([
{
value: 'hello',
isImage: false,
},
]);
});
it('should parse image url expression', () => {
const expression: ValueExpressionDTO = {
type: 'string',
value: {
type: 'literal',
content: 'https://example.com/tos-cn-i-mdko3gqilj/test.png',
},
};
const result = expressionParser(expression);
expect(result).toEqual([
{
value: 'https://example.com/tos-cn-i-mdko3gqilj/test.png',
isImage: false,
},
]);
});
it('should parse block output expression', () => {
const expression: ValueExpressionDTO = {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: 'block1',
name: 'output',
},
},
};
const result = expressionParser(expression);
expect(result).toEqual([
{
value: 'output',
isImage: false,
},
]);
});
it('should parse global variable expression', () => {
const expression: ValueExpressionDTO = {
type: 'string',
value: {
type: 'ref',
content: {
source: 'global_variable_test',
path: ['user', 'name'],
blockID: 'global',
name: 'user.name',
},
},
};
const result = expressionParser(expression);
expect(result).toEqual([
{
value: 'user.name',
isImage: false,
},
]);
});
it('should handle invalid expressions', () => {
const expression: ValueExpressionDTO = {
type: 'string',
value: {
type: 'literal',
content: undefined,
},
};
const result = expressionParser(expression);
expect(result).toEqual([]);
});
it('should filter out invalid inputs', () => {
const result = expressionParser(undefined as any);
expect(result).toEqual([]);
});
});

View File

@@ -0,0 +1,134 @@
/*
* 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 { describe, it, expect } from 'vitest';
import { imageReferenceParser } from '../../../../src/utils/schema-extractor/parsers/image-reference';
import type { ValueExpressionDTO } from '../../../../src/types/dto';
interface ImageReferenceDTO {
url: ValueExpressionDTO;
}
describe('image-reference-parser', () => {
it('应该处理空输入', () => {
const result = imageReferenceParser([]);
expect(result).toEqual([]);
});
it('应该处理非数组输入', () => {
const result = imageReferenceParser(undefined as any);
expect(result).toEqual([]);
const result2 = imageReferenceParser({} as any);
expect(result2).toEqual([]);
const result3 = imageReferenceParser(null as any);
expect(result3).toEqual([]);
});
it('应该正确解析图片引用', () => {
const references: ImageReferenceDTO[] = [
{
url: {
type: 'string',
value: {
type: 'literal',
content: 'https://example.com/test.png',
},
},
},
];
const result = imageReferenceParser(references);
expect(result).toEqual([
{
name: '-',
value: 'https://example.com/test.png',
isImage: false,
},
]);
});
it('应该正确解析多个图片引用', () => {
const references: ImageReferenceDTO[] = [
{
url: {
type: 'string',
value: {
type: 'literal',
content: 'https://example.com/test1.png',
},
},
},
{
url: {
type: 'string',
value: {
type: 'literal',
content: 'https://example.com/test2.png',
},
},
},
];
const result = imageReferenceParser(references);
expect(result).toEqual([
{
name: '-',
value: 'https://example.com/test1.png',
isImage: false,
},
{
name: '-',
value: 'https://example.com/test2.png',
isImage: false,
},
]);
});
it('应该过滤掉无效的图片引用', () => {
const references: ImageReferenceDTO[] = [
{
url: {
type: 'string',
value: {
type: 'literal',
content: undefined,
},
},
},
{
url: {
type: 'string',
value: {
type: 'literal',
content: 'https://example.com/test.png',
},
},
},
];
const result = imageReferenceParser(references);
expect(result).toEqual([
{
name: '-',
value: 'https://example.com/test.png',
isImage: false,
},
]);
});
});

View File

@@ -0,0 +1,278 @@
/*
* 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 { describe, it, expect, vi } from 'vitest';
import { outputsParser } from '../../../../src/utils/schema-extractor/parsers/output';
import { AssistTypeDTO, VariableTypeDTO } from '../../../../src/types/dto';
// Mock isWorkflowImageTypeURL
vi.mock('../../../../src/utils/schema-extractor/utils', () => ({
isWorkflowImageTypeURL: (url: string) =>
url.startsWith('https://example.com/'),
}));
describe('output-parser', () => {
it('应该处理空输入', () => {
const result = outputsParser([]);
expect(result).toEqual([]);
});
it('应该处理非数组输入', () => {
const result = outputsParser(undefined as any);
expect(result).toEqual([]);
const result2 = outputsParser({} as any);
expect(result2).toEqual([]);
const result3 = outputsParser(null as any);
expect(result3).toEqual([]);
});
it('应该正确解析基本输出', () => {
const outputs = [
{
name: 'test',
description: 'test description',
type: VariableTypeDTO.string,
},
];
const result = outputsParser(outputs);
expect(result).toEqual([
{
name: 'test',
description: 'test description',
},
]);
});
it('应该正确解析对象类型输出', () => {
const outputs = [
{
name: 'obj',
type: VariableTypeDTO.object,
schema: [
{
name: 'field1',
type: VariableTypeDTO.string,
},
{
name: 'field2',
type: VariableTypeDTO.float,
},
],
},
];
const result = outputsParser(outputs);
expect(result).toEqual([
{
name: 'obj',
children: [
{
name: 'field1',
},
{
name: 'field2',
},
],
},
]);
});
it('应该正确解析列表类型输出', () => {
const outputs = [
{
name: 'list',
type: VariableTypeDTO.list,
schema: {
schema: [
{
name: 'item1',
type: VariableTypeDTO.string,
},
{
name: 'item2',
type: VariableTypeDTO.float,
},
],
},
},
];
const result = outputsParser(outputs);
expect(result).toEqual([
{
name: 'list',
children: [
{
name: 'item1',
},
{
name: 'item2',
},
],
},
]);
});
it('应该正确处理带有默认值的字符串输出', () => {
const outputs = [
{
name: 'text',
type: VariableTypeDTO.string,
defaultValue: 'hello world',
},
];
const result = outputsParser(outputs);
expect(result).toEqual([
{
name: 'text',
value: 'hello world',
isImage: false,
},
]);
});
it('应该正确处理图片 URL 输出', () => {
const outputs = [
{
name: 'image',
type: VariableTypeDTO.string,
defaultValue: 'https://example.com/test.png',
},
];
const result = outputsParser(outputs);
expect(result).toEqual([
{
name: 'image',
value: 'https://example.com/test.png',
images: ['https://example.com/test.png'],
isImage: true,
},
]);
});
it('应该正确处理图片类型输出', () => {
const outputs = [
{
name: 'image',
type: VariableTypeDTO.string,
assistType: AssistTypeDTO.image,
defaultValue: 'https://example.com/test.png',
},
];
const result = outputsParser(outputs);
expect(result).toEqual([
{
name: 'image',
value: 'https://example.com/test.png',
images: ['https://example.com/test.png'],
isImage: true,
},
]);
});
it('应该正确处理图片列表输出', () => {
const outputs = [
{
name: 'images',
type: VariableTypeDTO.list,
schema: {
assistType: AssistTypeDTO.image,
},
defaultValue: JSON.stringify([
'https://example.com/test1.png',
'https://example.com/test2.png',
]),
},
];
const result = outputsParser(outputs);
expect(result).toEqual([
{
name: 'images',
value: JSON.stringify([
'https://example.com/test1.png',
'https://example.com/test2.png',
]),
images: [
'https://example.com/test1.png',
'https://example.com/test2.png',
],
isImage: true,
},
]);
});
it('应该正确处理文件列表中的图片', () => {
const outputs = [
{
name: 'files',
type: VariableTypeDTO.list,
schema: {
assistType: AssistTypeDTO.file,
},
defaultValue: JSON.stringify([
'https://example.com/test.png',
'https://example1.com/document.pdf',
]),
},
];
const result = outputsParser(outputs);
console.log(result);
expect(result).toEqual([
{
name: 'files',
value: JSON.stringify([
'https://example.com/test.png',
'https://example1.com/document.pdf',
]),
images: ['https://example.com/test.png'],
isImage: true,
},
]);
});
it('应该正确处理无效的 JSON 字符串', () => {
const consoleError = vi.spyOn(console, 'error');
const outputs = [
{
name: 'invalid',
type: VariableTypeDTO.list,
schema: {
assistType: AssistTypeDTO.image,
},
defaultValue: 'invalid json',
},
];
const result = outputsParser(outputs);
expect(result).toEqual([
{
name: 'invalid',
value: 'invalid json',
isImage: false,
},
]);
expect(consoleError).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,206 @@
/*
* 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 { describe, it, expect } from 'vitest';
import { refInputParametersParser } from '../../../../src/utils/schema-extractor/parsers/ref-input-parameters';
// Mock isWorkflowImageTypeURL
vi.mock('../../../../src/utils/schema-extractor/utils', () => ({
isWorkflowImageTypeURL: (url: string) =>
typeof url === 'string' && url.startsWith('https://example.com/'),
}));
describe('ref-input-parameters-parser', () => {
it('应该处理空输入', () => {
const result = refInputParametersParser([]);
expect(result).toEqual([]);
});
it('应该正确解析单个引用参数', () => {
const references = [
{
param1: {
type: 'string',
value: {
content: 'test value',
},
},
},
];
const result = refInputParametersParser(references);
expect(result).toEqual([
{
name: 'param1',
value: 'test value',
isImage: false,
},
]);
});
it('应该正确解析多个引用参数', () => {
const references = [
{
param1: {
type: 'string',
value: {
content: 'value1',
},
},
param2: {
type: 'string',
value: {
content: 'value2',
},
},
},
];
const result = refInputParametersParser(references);
expect(result).toEqual([
{
name: 'param1',
value: 'value1',
isImage: false,
},
{
name: 'param2',
value: 'value2',
isImage: false,
},
]);
});
it('应该正确解析多个引用对象', () => {
const references = [
{
param1: {
type: 'string',
value: {
content: 'value1',
},
},
},
{
param2: {
type: 'string',
value: {
content: 'value2',
},
},
},
];
const result = refInputParametersParser(references);
expect(result).toEqual([
{
name: 'param1',
value: 'value1',
isImage: false,
},
{
name: 'param2',
value: 'value2',
isImage: false,
},
]);
});
it('应该正确识别图片 URL', () => {
const references = [
{
image: {
type: 'string',
value: {
content: 'https://example.com/test.png',
},
},
},
];
const result = refInputParametersParser(references);
expect(result).toEqual([
{
name: 'image',
value: 'https://example.com/test.png',
isImage: true,
},
]);
});
it('应该忽略非字符串类型的参数', () => {
const references = [
{
param1: {
type: 'number',
value: {
content: '123',
},
},
param2: {
type: 'string',
value: {
content: 'valid',
},
},
},
];
const result = refInputParametersParser(references);
expect(result).toEqual([
{
name: 'param2',
value: 'valid',
isImage: false,
},
]);
});
it('应该忽略无效的参数结构', () => {
const references = [
{
param1: 'invalid',
param2: {
type: 'string',
value: {
content: 'valid',
},
},
param3: null,
param4: undefined,
param5: {
type: 'string',
value: {}, // 空的 value 对象
},
},
];
const result = refInputParametersParser(references);
expect(result).toEqual([
{
name: 'param2',
value: 'valid',
isImage: false,
},
{
name: 'param5',
value: undefined,
isImage: false,
},
]);
});
});

View File

@@ -0,0 +1,214 @@
/*
* 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 { describe, it, expect, vi } from 'vitest';
import { variableMergeGroupsParser } from '../../../../src/utils/schema-extractor/parsers/variable-merge-groups-parser';
import type { ValueExpressionDTO } from '../../../../src/types/dto';
// Mock expressionParser
vi.mock(
'../../../../src/utils/schema-extractor/parsers/expression-parser',
() => ({
expressionParser: vi.fn(variables => {
if (!Array.isArray(variables)) {
return [];
}
return variables.map(variable => ({
value: variable.value?.content,
isImage:
typeof variable.value?.content === 'string' &&
variable.value?.content.startsWith('https://example.com/'),
}));
}),
}),
);
describe('variable-merge-groups-parser', () => {
it('应该处理空输入', () => {
const result = variableMergeGroupsParser([]);
expect(result).toEqual([]);
});
it('应该正确解析单个变量组', () => {
const mergeGroups = [
{
name: 'group1',
variables: [
{
type: 'string',
value: {
type: 'literal',
content: 'test value',
},
} as ValueExpressionDTO,
],
},
];
const result = variableMergeGroupsParser(mergeGroups);
expect(result).toEqual([
{
groupName: 'group1',
variables: [
{
value: 'test value',
isImage: false,
},
],
},
]);
});
it('应该正确解析多个变量组', () => {
const mergeGroups = [
{
name: 'group1',
variables: [
{
type: 'string',
value: {
type: 'literal',
content: 'value1',
},
} as ValueExpressionDTO,
],
},
{
name: 'group2',
variables: [
{
type: 'string',
value: {
type: 'literal',
content: 'value2',
},
} as ValueExpressionDTO,
],
},
];
const result = variableMergeGroupsParser(mergeGroups);
expect(result).toEqual([
{
groupName: 'group1',
variables: [
{
value: 'value1',
isImage: false,
},
],
},
{
groupName: 'group2',
variables: [
{
value: 'value2',
isImage: false,
},
],
},
]);
});
it('应该正确处理包含图片 URL 的变量组', () => {
const mergeGroups = [
{
name: 'images',
variables: [
{
type: 'string',
value: {
type: 'literal',
content: 'https://example.com/test.png',
},
} as ValueExpressionDTO,
],
},
];
const result = variableMergeGroupsParser(mergeGroups);
expect(result).toEqual([
{
groupName: 'images',
variables: [
{
value: 'https://example.com/test.png',
isImage: true,
},
],
},
]);
});
it('应该正确处理空变量组', () => {
const mergeGroups = [
{
name: 'emptyGroup',
variables: [],
},
];
const result = variableMergeGroupsParser(mergeGroups);
expect(result).toEqual([
{
groupName: 'emptyGroup',
variables: [],
},
]);
});
it('应该正确处理包含多个变量的组', () => {
const mergeGroups = [
{
name: 'mixedGroup',
variables: [
{
type: 'string',
value: {
type: 'literal',
content: 'text value',
},
} as ValueExpressionDTO,
{
type: 'string',
value: {
type: 'literal',
content: 'https://example.com/test.png',
},
} as ValueExpressionDTO,
],
},
];
const result = variableMergeGroupsParser(mergeGroups);
expect(result).toEqual([
{
groupName: 'mixedGroup',
variables: [
{
value: 'text value',
isImage: false,
},
{
value: 'https://example.com/test.png',
isImage: true,
},
],
},
]);
});
});

View File

@@ -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 { describe, it, expect } from 'vitest';
import {
isPresetStartParams,
isUserInputStartParams,
} from '../../src/utils/start-params';
import {
BOT_USER_INPUT,
USER_INPUT,
CONVERSATION_NAME,
} from '../../src/constants';
describe('start-params', () => {
describe('isPresetStartParams', () => {
it('应该正确识别预设的开始节点参数', () => {
expect(isPresetStartParams(BOT_USER_INPUT)).toBe(true);
expect(isPresetStartParams(USER_INPUT)).toBe(true);
expect(isPresetStartParams(CONVERSATION_NAME)).toBe(true);
});
it('应该正确识别非预设的开始节点参数', () => {
expect(isPresetStartParams('other_param')).toBe(false);
expect(isPresetStartParams('custom_input')).toBe(false);
});
it('应该正确处理 undefined 和空字符串', () => {
expect(isPresetStartParams(undefined)).toBe(false);
expect(isPresetStartParams('')).toBe(false);
});
});
describe('isUserInputStartParams', () => {
it('应该正确识别用户输入的开始节点参数', () => {
expect(isUserInputStartParams(BOT_USER_INPUT)).toBe(true);
expect(isUserInputStartParams(USER_INPUT)).toBe(true);
});
it('应该正确识别非用户输入的开始节点参数', () => {
expect(isUserInputStartParams(CONVERSATION_NAME)).toBe(false);
expect(isUserInputStartParams('other_param')).toBe(false);
expect(isUserInputStartParams('custom_input')).toBe(false);
});
it('应该正确处理 undefined 和空字符串', () => {
expect(isUserInputStartParams(undefined)).toBe(false);
expect(isUserInputStartParams('')).toBe(false);
});
});
});

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["./dist"]
}
]
}

View File

@@ -0,0 +1,19 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'node',
overrides: [
{
files: ['**/__tests__/**/*.{js,ts,jsx,tsx}', '**/*.test.{js,ts,jsx,tsx}'],
rules: {
'@coze-arch/no-deep-relative-import': 'off',
'@typescript-eslint/consistent-type-assertions': 'off',
},
},
],
rules: {
'@typescript-eslint/naming-convention': 'off',
'import/no-duplicates': 'off',
},
});

View File

@@ -0,0 +1,84 @@
{
"name": "@coze-workflow/base",
"version": "0.0.1",
"description": "workflow 基础包",
"license": "Apache-2.0",
"author": "lvxinsheng@bytedance.com",
"maintainers": [],
"exports": {
".": "./src/index.ts",
"./api": "./src/api/index.ts",
"./types": "./src/types/index.ts",
"./store": "./src/store/index.ts",
"./constants": "./src/constants/index.ts",
"./services": "./src/services/index.ts"
},
"main": "src/index.ts",
"typesVersions": {
"*": {
".": [
"./src/index.ts"
],
"api": [
"./src/api/index.ts"
],
"types": [
"./src/types/index.ts"
],
"store": [
"./src/store/index.ts"
],
"constants": [
"./src/constants/index.ts"
],
"services": [
"./src/services/index.ts"
]
}
},
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@coze-arch/bot-api": "workspace:*",
"@coze-arch/bot-error": "workspace:*",
"@coze-arch/bot-flags": "workspace:*",
"@coze-arch/bot-utils": "workspace:*",
"@coze-arch/logger": "workspace:*",
"@flowgram-adapter/common": "workspace:*",
"@flowgram-adapter/free-layout-editor": "workspace:*",
"@tanstack/react-query": "~5.13.4",
"io-ts": "2.2.20",
"lodash-es": "^4.17.21",
"react": "~18.2.0",
"zustand": "^4.4.7"
},
"devDependencies": {
"@babel/core": "^7.26.0",
"@coze-arch/bot-env": "workspace:*",
"@coze-arch/bot-typings": "workspace:*",
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@types/lodash-es": "^4.17.10",
"@types/node": "^18",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"@vitest/coverage-v8": "~3.0.5",
"fp-ts": "^2.5.0",
"less": "^3.13.1",
"react-dom": "~18.2.0",
"react-is": ">= 16.8.0",
"scheduler": ">=0.19.0",
"styled-components": ">=4",
"typescript": "~5.8.2",
"vitest": "~3.0.5"
},
"// deps": "immer@^10.0.3 为脚本自动补齐,请勿改动"
}

View File

@@ -0,0 +1,63 @@
/*
* 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 { workflowApi as archWorkflowApi } from '@coze-arch/bot-api';
// eslint-disable-next-line @coze-arch/no-batch-import-or-export
export * from '@coze-arch/bot-api/workflow_api';
export { withQueryClient, workflowQueryClient } from './with-query-client';
/** 运营接口平台,会替换权限验证 */
const workflowOperationApiNameMap = {
GetHistorySchema: 'OPGetHistorySchema',
GetWorkFlowProcess: 'OPGetWorkFlowProcess',
GetCanvasInfo: 'OPGetCanvasInfo',
GetWorkflowReferences: 'OPGetWorkflowReferences',
GetReleasedWorkflows: 'OPGetReleasedWorkflows',
GetApiDetail: 'OPGetApiDetail',
NodeTemplateList: 'OPNodeTemplateList',
GetWorkflowGrayFeature: 'OPGetWorkflowGrayFeature',
CheckLatestSubmitVersion: 'OPCheckLatestSubmitVersion',
GetImageflowBasicNodeList: 'OPGetImageflowBasicNodeList',
GetWorkflowDetail: 'OPGetWorkflowDetail',
GetLLMNodeFCSettingDetail: 'OPGetLLMNodeFCSettingDetail',
ListTriggerAppEvents: 'OPListTriggerAppEvents',
GetTrigger: 'OPGetTrigger',
GetWorkflowDetailInfo: 'OPGetWorkflowDetailInfo',
GetNodeExecuteHistory: 'OPGetNodeExecuteHistory',
VersionHistoryList: 'OPVersionHistoryList',
GetChatFlowRole: 'OPGetChatFlowRole',
ListRootSpans: 'OPListRootSpans',
GetTraceSDK: 'OPGetTraceSDK',
};
const workflowApi: typeof archWorkflowApi = new Proxy(
{} as unknown as typeof archWorkflowApi,
{
get: (target, name: string) => {
if (IS_BOT_OP && workflowOperationApiNameMap[name]) {
return archWorkflowApi[workflowOperationApiNameMap[name]].bind(
archWorkflowApi,
);
} else {
return archWorkflowApi[name].bind(archWorkflowApi);
}
},
},
);
export { workflowApi };

View File

@@ -0,0 +1,32 @@
/*
* 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 { QueryClient, QueryClientProvider } from '@tanstack/react-query';
export const workflowQueryClient = new QueryClient();
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-explicit-any
export function withQueryClient<T extends FC<any>>(Component: T): T {
return function WrappedComponent(props) {
return (
<QueryClientProvider client={workflowQueryClient}>
<Component {...props} />
</QueryClientProvider>
);
} as T;
}

View File

@@ -0,0 +1,48 @@
/*
* 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 const EmptyFunction = () => {
/** 空方法 */
};
export const EmptyAsyncFunction = () => Promise.resolve();
/** 公共空间 ID */
export const PUBLIC_SPACE_ID = '999999';
/** BOT_USER_INPUT 变量名 */
export const BOT_USER_INPUT = 'BOT_USER_INPUT';
/** USER_INPUT 参数,新版 BOT_USER_INPUT 参数,作用和 BOT_USER_INPUT 相同Coze2.0 Chatflow 需求引入 */
export const USER_INPUT = 'USER_INPUT';
/** CONVERSATION_NAME 变量名start 节点会话名称入参 */
export const CONVERSATION_NAME = 'CONVERSATION_NAME';
/**
* workflow 名称最大字符数
*/
export const WORKFLOW_NAME_MAX_LEN = 30;
/**
* 工作流命名正则
*/
export const WORKFLOW_NAME_REGEX = /^[a-zA-Z][a-zA-Z0-9_]{0,63}$/;
/**
* 节点测试ID前缀
*/
export const NODE_TEST_ID_PREFIX = 'playground.node';

View File

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

View File

@@ -0,0 +1,23 @@
/*
* 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 { createContext } from 'react';
import { type WorkflowNode } from '../entities';
export const WorkflowNodeContext = createContext<WorkflowNode | undefined>(
undefined,
);

View File

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

View File

@@ -0,0 +1,149 @@
/*
* 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 {
FlowNodeErrorData,
FlowNodeFormData,
isFormV2,
type FormModelV2,
type FlowNodeEntity,
} from '@flowgram-adapter/free-layout-editor';
import { getFormValueByPathEnds } from '../utils';
import {
type StandardNodeType,
type WorkflowNodeRegistry,
type InputValueVO,
type OutputValueVO,
} from '../types';
export class WorkflowNode {
private node: FlowNodeEntity;
constructor(node: FlowNodeEntity) {
this.node = node;
this.setData = this.setData.bind(this);
this.setError = this.setError.bind(this);
}
public get registry(): WorkflowNodeRegistry {
return this.node.getNodeRegistry() as WorkflowNodeRegistry;
}
public get inputParameters(): InputValueVO[] | undefined {
if (this.registry.getNodeInputParameters) {
return this.registry.getNodeInputParameters(this.node);
}
if (this.node.getNodeMeta()?.inputParametersPath) {
return this.node
.getData<FlowNodeFormData>(FlowNodeFormData)
.formModel.getFormItemValueByPath(
this.node.getNodeMeta()?.inputParametersPath,
);
}
return this.getFormValueByPathEnds<InputValueVO[]>('/inputParameters');
}
public get outputs(): OutputValueVO[] | undefined {
if (this.registry.getNodeOutputs) {
return this.registry.getNodeOutputs(this.node);
} else {
return this.data?.outputs;
}
}
// 这个方法建议暂时先不要使用 原因是该方法没有体现业务逻辑 只是底层方法的简化
protected getFormValueByPathEnds<T = unknown>(
pathEnds: string,
): T | undefined {
return getFormValueByPathEnds(this.node, pathEnds);
}
get type() {
return this.node.flowNodeType as StandardNodeType;
}
get isError() {
return !!this.error;
}
get error() {
const errorData = this.node.getData<FlowNodeErrorData>(FlowNodeErrorData);
const error = errorData.getError();
return error;
}
public setError(error: Error) {
const errorData = this.node.getData<FlowNodeErrorData>(FlowNodeErrorData);
errorData.setError(error);
}
get isInitialized() {
return this.form.initialized;
}
get data() {
return this.form.getFormItemValueByPath('/');
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public setData(data: any) {
if (data === undefined) {
return;
}
const { form } = this;
if (isFormV2(this.node)) {
Object.keys(data).forEach(key => {
(form as FormModelV2).setValueIn(key, data[key]);
});
} else {
const formItem = form.getFormItemByPath('/');
if (formItem) {
formItem.value = data;
}
}
}
get icon() {
return this?.data?.nodeMeta?.icon;
}
get title() {
return this?.data?.nodeMeta?.title;
}
get description() {
return this?.data?.nodeMeta?.description;
}
private get form() {
return this.node.getData(FlowNodeFormData).formModel;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public getValueByPath<T = any>(pathname: string): T {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const value = (this.form as any).getValueIn(pathname);
return value;
}
}

View File

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

View File

@@ -0,0 +1,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 { useNodeTestId } from './use-node-test-id';
export { useWorkflowNode } from './use-workflow-node';

View File

@@ -0,0 +1,73 @@
/*
* 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 { CustomError } from '@coze-arch/bot-error';
import { useCurrentEntity } from '@flowgram-adapter/free-layout-editor';
import { concatTestId } from '../utils';
import { NODE_TEST_ID_PREFIX } from '../constants';
/**
* 仅限在 node 内使用
*/
type UseNodeTestId = () => {
/**
* 返回当前节点的 test-id也就是当前节点的 node id
* 'playground.11001
*/
getNodeTestId: () => string;
/**
* 返回当前 setter 的 test-id会自动带上节点的 test-id
* 'playground.11001.llm'
*/
getNodeSetterId: (setterName: string) => string;
/**
* 连接两个 test-id生成一个新的 test-id
* ('a', 'b') => 'a.b'
*/
concatTestId: typeof concatTestId;
};
export const useNodeTestId: UseNodeTestId = () => {
const node = useCurrentEntity();
if (!node?.id) {
throw new CustomError(
'useNodeTestId must be called in a workflow node',
'',
);
}
const getNodeTestId = () => concatTestId(NODE_TEST_ID_PREFIX, node.id);
return {
/**
* 返回当前节点的 test-id也就是当前节点的 node id
* 'playground.11001
*/
getNodeTestId,
/**
* 返回当前 setter 的 test-id会自动带上节点的 test-id
* 'playground.11001.llm'
*/
getNodeSetterId: setterName => concatTestId(getNodeTestId(), setterName),
/**
* 连接两个 test-id生成一个新的 test-id
* ('a', 'b') => 'a.b'
*/
concatTestId,
};
};

View File

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

View File

@@ -0,0 +1,28 @@
/*
* 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 * from './types';
export * from './utils';
export * from './api';
export * from './store';
export * from './constants';
export * from './hooks';
export { WorkflowNode } from './entities';
export { WorkflowNodeContext } from './contexts';

View File

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

View File

@@ -0,0 +1,123 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type UseBoundStoreWithEqualityFn } from 'zustand/traditional';
import { type StoreApi } from 'zustand';
import { type FeedbackStatus } from '@flowgram-adapter/free-layout-editor';
import { useService } from '@flowgram-adapter/free-layout-editor';
import { type WorkflowNodeEntity } from '@flowgram-adapter/free-layout-editor';
export const ValidationService = Symbol('ValidationService');
export const useValidationService = () =>
useService<ValidationService>(ValidationService);
export const useValidationServiceStore = <T>(
selector: (s: ValidationState) => T,
) => useValidationService().store(selector);
export interface ValidateError {
// 错误描述
errorInfo: string;
// 错误等级
errorLevel: FeedbackStatus;
// 错误类型: 节点 / 连线
errorType: 'node' | 'line';
// 节点id
nodeId: string;
// 若为连线错误,还需要目标节点来确认这条连线
targetNodeId?: string;
}
export interface WorkflowValidateError {
workflowId: string;
/** 流程名 */
name?: string;
errors: ValidateErrorMap;
}
export type ValidateErrorMap = Record<string, ValidateError[]>;
export type WorkflowValidateErrorMap = Record<string, WorkflowValidateError>;
export interface ValidateResult {
hasError: boolean;
nodeErrorMap: ValidateErrorMap;
}
export interface ValidateResultV2 {
hasError: boolean;
errors: WorkflowValidateErrorMap;
}
export interface ValidationState {
/**
* @deprecated 请使用 errorsV2
*/
errors: ValidateErrorMap;
/** 按照流程归属分类的错误 */
errorsV2: WorkflowValidateErrorMap;
/** 正在校验中 */
validating: boolean;
}
export interface ValidationService {
store: UseBoundStoreWithEqualityFn<StoreApi<ValidationState>>;
/**
* 前端流程校验,包括节点、表单、端口等
*/
validateWorkflow: () => Promise<ValidateResult>;
/**
* 对节点的校验,包括表单、端口等
*/
validateNode: (node: WorkflowNodeEntity) => Promise<ValidateResult>;
/**
* @deprecated 请使用 validateSchemaV2
* 流程定义合法性校验,通常为后端校验
*/
validateSchema: () => Promise<ValidateResult>;
/**
* 流程定义合法性校验,通常为后端校验
*/
validateSchemaV2: () => Promise<ValidateResultV2>;
/**
* 获取指定 id 的错误列表
*/
getErrors: (id: string) => ValidateError[];
/**
* @deprecated 请使用 setErrorsV2
* 设置错误
* @param errors
* @param force 强制覆盖所有错误
*/
setErrors: (errors: ValidationState['errors'], force?: boolean) => void;
/**
* 设置错误
* @param errors
* @returns
*/
setErrorsV2: (errors: ValidationState['errorsV2']) => void;
/**
* 清空所有错误
*/
clearErrors: () => void;
/** 线条是否存在错误 */
isLineError: (fromId: string, toId?: string) => boolean;
get validating(): boolean;
set validating(value: boolean);
}

View File

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

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// workflow store目前保存 flow 的 nodes 和 edges 数据
import { devtools } from 'zustand/middleware';
import { create } from 'zustand';
import {
type WorkflowEdgeJSON,
type WorkflowNodeJSON,
} from '@flowgram-adapter/free-layout-editor';
interface WorkflowStoreState {
/** 节点数据 */
nodes: WorkflowNodeJSON[];
/** 边数据 */
edges: WorkflowEdgeJSON[];
/** 是否在创建 workflow */
isCreatingWorkflow: boolean;
}
interface WorkflowStoreAction {
setNodes: (value: WorkflowNodeJSON[]) => void;
setEdges: (value: WorkflowEdgeJSON[]) => void;
setIsCreatingWorkflow: (value: boolean) => void;
}
const initialStore: WorkflowStoreState = {
nodes: [],
edges: [],
isCreatingWorkflow: false,
};
export const useWorkflowStore = create<
WorkflowStoreState & WorkflowStoreAction
>()(
devtools(set => ({
...initialStore,
setNodes: nodes => set({ nodes: nodes ?? [] }),
setEdges: edges => set({ edges: edges ?? [] }),
setIsCreatingWorkflow: value => set({ isCreatingWorkflow: value }),
})),
);

View File

@@ -0,0 +1,125 @@
/*
* 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 { ViewVariableType } from './view-variable-type';
import { type DTODefine, type InputValueDTO } from './dto';
/**
* BlockInput是后端定义的类型对应的就是 InputValueDTO
*/
export type BlockInput = InputValueDTO;
/**
* BlockInput 转换方法
*/
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace BlockInput {
/**
* @param name
* @param value
* @param type
* @example
* {
* name: 'apiName',
* input: {
* type: 'string',
* value: {
* type: 'literal',
* content: 'xxxxxxxxx'
* }
* }
* }
*/
export function create(
name: string,
value = '',
type: DTODefine.BasicVariableType = 'string',
): BlockInput {
const blockInput: BlockInput = {
name,
input: {
type,
value: {
type: 'literal',
content: String(value),
},
},
};
let rawMetaType: ViewVariableType = ViewVariableType.String;
switch (type) {
case 'string':
rawMetaType = ViewVariableType.String;
break;
case 'integer':
rawMetaType = ViewVariableType.Integer;
break;
case 'float':
rawMetaType = ViewVariableType.Number;
break;
case 'boolean':
rawMetaType = ViewVariableType.Boolean;
break;
default:
break;
}
blockInput.input.value.rawMeta = { type: rawMetaType };
return blockInput;
}
export function createString(name: string, value: string): BlockInput {
return create(name, value, 'string');
}
export function createInteger(name: string, value: string): BlockInput {
return create(name, value, 'integer');
}
export function createFloat(name: string, value: string): BlockInput {
return create(name, value, 'float');
}
export function createArray<T>(
name: string,
value: Array<T>,
schema: unknown,
): BlockInput {
return {
name,
input: {
type: 'list',
schema,
value: {
type: 'literal',
content: value,
},
},
};
}
export function createBoolean(name: string, value: boolean): BlockInput {
const booleanInput = create(name, value as never, 'boolean');
booleanInput.input.value.content = value;
return booleanInput;
}
export function toLiteral<T>(blockInput: BlockInput): T {
return blockInput.input.value.content as unknown as T;
}
export function isBlockInput(d: unknown): d is BlockInput {
return (
Boolean((d as BlockInput)?.name) &&
typeof (d as BlockInput)?.input?.value?.content !== undefined
);
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export enum ConditionLogic {
OR = 1,
AND = 2,
}
export enum ConditionLogicDTO {
OR = 'OR',
AND = 'AND',
}
export type ConditionOperator =
| 'EQUAL' // "="
| 'NOT_EQUAL' // "<>" 或 "!="
| 'GREATER_THAN' // ">"
| 'LESS_THAN' // "<"
| 'GREATER_EQUAL' // ">="
| 'LESS_EQUAL' // "<="
| 'IN' // "IN"
| 'NOT_IN' // "NOT IN"
| 'IS_NULL' // "IS NULL"
| 'IS_NOT_NULL' // "IS NOT NULL"
| 'LIKE' // "LIKE" 模糊匹配字符串
| 'NOT_LIKE' // "NOT LIKE" 反向模糊匹配
| 'BE_TRUE' // "BE TRUE" 布尔值为 true
| 'BE_FALSE'; // "BE FALSE" 布尔值为 false

View File

@@ -0,0 +1,68 @@
/*
* 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-next-line @coze-arch/no-batch-import-or-export
import * as t from 'io-ts';
export const datasetParams = t.array(
t.union([
t.type({
name: t.literal('datasetList'),
input: t.type({
type: t.literal('list'),
schema: t.type({
type: t.literal('string'),
}),
value: t.type({
type: t.literal('literal'),
content: t.array(t.string),
}),
}),
}),
t.type({
name: t.literal('topK'),
input: t.type({
type: t.literal('integer'),
value: t.type({
type: t.literal('literal'),
content: t.number,
}),
}),
}),
t.type({
name: t.literal('minScore'),
input: t.type({
type: t.literal('number'),
value: t.type({
type: t.literal('literal'),
content: t.number,
}),
}),
}),
t.type({
name: t.literal('strategy'),
input: t.type({
type: t.literal('number'),
value: t.type({
type: t.literal('literal'),
content: t.number,
}),
}),
}),
]),
);
export type DatasetParams = t.TypeOf<typeof datasetParams>;

View File

@@ -0,0 +1,112 @@
/*
* 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 ValueExpression, type ValueExpressionType } from './vo';
import { type ViewVariableType } from './view-variable-type';
import { type ValueExpressionDTO } from './dto';
import { type ConditionOperator } from './condition';
export interface DatabaseField {
id: number;
name?: string;
type?: ViewVariableType;
required?: boolean;
description?: string;
isSystemField?: boolean;
}
export interface WorkflowDatabase {
id: string;
fields?: DatabaseField[];
iconUrl?: string;
tableName?: string;
tableDesc?: string;
}
/**
* 数据库配置字段
*/
export interface DatabaseSettingField {
fieldID: number;
fieldValue?: ValueExpression;
}
export interface DatabaseSettingFieldIDDTO {
name: 'fieldID';
input: {
type: 'string';
value: {
type: 'literal';
content: string;
};
};
}
export interface DatabaseSettingFieldValueDTO {
name: 'fieldValue';
input?: ValueExpressionDTO;
}
export type DatabaseSettingFieldDTO = [
DatabaseSettingFieldIDDTO,
DatabaseSettingFieldValueDTO | undefined,
];
/**
* 数据库条件
*/
export type DatabaseConditionOperator = ConditionOperator;
export type DatabaseConditionLeft = string;
export type DatabaseConditionRight = ValueExpression;
export interface DatabaseCondition {
left?: DatabaseConditionLeft;
operator?: DatabaseConditionOperator;
right?: DatabaseConditionRight;
}
export interface DatabaseConditionLeftDTO {
name: 'left';
input: {
type: 'string';
value: {
type: ValueExpressionType.LITERAL;
content: string;
};
};
}
export interface DatabaseConditionOperatorDTO {
// 对操作符的翻译前后端没有统一
name: 'operation';
input: {
type: 'string';
value: {
type: ValueExpressionType.LITERAL;
content: string;
};
};
}
export interface DatabaseConditionRightDTO {
name: 'right';
input?: ValueExpressionDTO;
}
export type DatabaseConditionDTO = [
DatabaseConditionLeftDTO | undefined,
DatabaseConditionOperatorDTO | undefined,
DatabaseConditionRightDTO | undefined,
];

View File

@@ -0,0 +1,332 @@
/*
* 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 ValueExpressionRawMeta } from './vo';
import { type StandardNodeType } from './node-type';
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace DTODefine {
export type LiteralExpressionContent =
| string
| number
| boolean
| Array<unknown>;
export interface LiteralExpression {
type: 'literal';
content?: LiteralExpressionContent;
rawMeta?: ValueExpressionRawMeta;
}
export type RefExpressionContent =
| {
source: 'variable';
blockID: undefined;
name: string;
}
| {
source: 'block-output';
blockID: string;
name: string;
}
| {
source: `global_variable_${string}`;
path: string[];
blockID: string;
name: string;
};
export interface RefExpression {
type: 'ref';
content?: RefExpressionContent;
rawMeta?: ValueExpressionRawMeta;
}
export interface ObjectRefExpression {
type: 'object_ref';
content?: unknown;
rawMeta?: ValueExpressionRawMeta;
}
// 当 schema 为 string 时表示原始的 FDL 类型export type ListVariableSchema = VariableTypeDef & Omit<VariableOption, 'name'>
export type ObjectVariableSchema = InputVariableDTO[];
export type ListVariableSchema = VariableTypeDef &
Omit<VariableOption, 'name'>;
export type VariableSchema = ListVariableSchema | ObjectVariableSchema;
export type BasicVariableType =
| 'string'
| 'integer'
| 'float'
| 'boolean'
| 'image'
| 'unknown';
export type ComplexVariableType = 'object' | 'list';
export type VariableType = BasicVariableType | ComplexVariableType;
export interface BasicVarTypeDef {
type: BasicVariableType;
assistType?: AssistTypeDTO;
}
export interface ObjectVarTypeDef {
type: VariableTypeDTO.object;
schema?: ObjectVariableSchema;
assistType?: AssistTypeDTO;
}
export interface ListVarTypeDef {
type: VariableTypeDTO.list;
schema: ListVariableSchema;
assistType?: AssistTypeDTO;
}
export type VariableTypeDef =
| BasicVarTypeDef
| ObjectVarTypeDef
| ListVarTypeDef;
interface VariableOption {
name: string;
label?: string;
defaultValue?: LiteralExpressionContent;
description?: string;
required?: boolean;
}
export type InputVariableDTO = VariableOption & VariableTypeDef;
}
/**
* 后端定义的表达式格式
* @example
* - literal
* {
* type: 'string',
* value: {
* type: 'liteal',
* content: '浙江'
* }
* }
*
* - ref
* // 普通引用类型
* {
* type: 'string', // 由引用的变量类型判断
* value: {
* type: 'ref',
* content: {
* source: 'block-output',
* blockID: '1002',
* name: 'result'
* }
* }
* }
*
* // list or object 引用类型
* {
* type: 'list', // 由引用的变量路径的最后一个值类型判断, 如果list.a.c, 则为c的格式
* schema: { // 只有list和object有schema
* type: 'object',
* schema: [
* { name: 'role', type: 'string' },
* { name: 'content', type: 'string' }
* ]
* }
* value: {
* type: 'ref',
* content: {
* source: 'block-output',
* blockID: '1002',
* name: 'list.a.c' // 这里存的是引用的路径
* },
* }
* }
*/
export interface ValueExpressionDTO {
type?: string;
assistType?: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
schema?: any;
value:
| DTODefine.LiteralExpression
| DTODefine.RefExpression
| DTODefine.ObjectRefExpression;
}
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace ValueExpressionDTO {
/**
* 空引用
*/
export function createEmpty(): ValueExpressionDTO {
return {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '',
name: '',
},
},
};
}
}
export interface InputValueDTO {
name?: string;
input: ValueExpressionDTO;
schema?: InputValueDTO[];
id?: string;
}
export enum VariableTypeDTO {
object = 'object',
list = 'list',
string = 'string',
integer = 'integer',
float = 'float',
boolean = 'boolean',
image = 'image',
time = 'time',
}
export enum AssistTypeDTO {
file = 1,
image = 2,
doc = 3,
code = 4,
ppt = 5,
txt = 6,
excel = 7,
audio = 8,
zip = 9,
video = 10,
svg = 11,
voice = 12,
time = 10000,
}
/**
* 后端的变量格式
* @example
* 1. simple
* {
* name: 'message',
* type: 'string'
* }
* 2. object
* {
* name: 'someObj'
* type: 'object',
* schema: [
* {
* name: 'role',
* type: 'string'
* },
* {
* name: 'content',
* type: 'string'
* }
* ]
* }
* 3. list<object>
* {
* name: 'history'
* type: 'list',
* schema: {
* type: 'object',
* schema: [
* { name: 'role', type: 'string' },
* { name: 'content', type: 'string' }
* ]
* }
* }
*/
export interface VariableMetaDTO {
/**
* 变量类型
*/
type: VariableTypeDTO;
/**
* 辅助类型string 类型的变量可以是 file 或者 image
*/
assistType?: AssistTypeDTO;
/**
* 变量名,在节点内不可重复
*/
name: string;
/**
* 变量数据结构仅object 和 list 类型变量有
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
schema?: any; // BlockVariableDefine.ObjectVariableSchema | BlockVariableDefine.ListVariableSchema
required?: boolean;
description?: string;
readonly?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
defaultValue?: any;
}
export interface BatchDTOInputList {
name?: string;
input: ValueExpressionDTO;
// 初始化后存在,服务端数据不存在
id?: string;
}
export interface BatchDTO {
batchSize: number;
concurrentSize: number;
inputLists: BatchDTOInputList[];
}
export interface NodeDataDTO {
inputs: {
inputParameters?: InputValueDTO[];
settingOnError?: { switch: boolean; dataOnErr: string };
[key: string]: unknown;
};
nodeMeta: {
description: string;
icon: string;
subTitle: string;
title: string;
mainColor: string;
};
outputs: VariableMetaDTO[];
version?: string;
}
export interface NodeDTO {
id: string;
type: StandardNodeType;
meta?: {
position: { x: number; y: number };
[key: string]: unknown;
};
data: NodeDataDTO;
edges?: {
sourceNodeId: string;
targetNodeId: string;
sourcePortId: string;
}[];
blocks?: NodeDataDTO[];
}
export interface InputTypeValueDTO {
name: string;
type: VariableTypeDTO;
input: ValueExpressionDTO;
}

View File

@@ -0,0 +1,143 @@
/*
* 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 {
ViewVariableType,
VARIABLE_TYPE_ALIAS_MAP,
type InputVariable,
FILE_TYPES,
} from './view-variable-type';
export {
type RecursedParamDefinition,
ParamValueType,
} from './param-definition';
export {
ViewVariableTreeNode,
type ViewVariableMeta,
} from './view-variable-tree';
export {
type DTODefine,
ValueExpressionDTO,
type InputValueDTO,
VariableTypeDTO,
AssistTypeDTO,
type VariableMetaDTO,
type BatchDTOInputList,
type BatchDTO,
type NodeDTO,
type NodeDataDTO,
type InputTypeValueDTO,
} from './dto';
export { BlockInput } from './block-input-dto';
export {
type RefExpressionContent,
ValueExpressionType,
type LiteralExpression,
type RefExpression,
type ObjectRefExpression,
ValueExpression,
type InputValueVO,
type OutputValueVO,
BatchMode,
type BatchVOInputList,
type BatchVO,
type InputTypeValueVO,
} from './vo';
export {
StandardNodeType,
NODE_ORDER,
CONVERSATION_NODES,
MESSAGE_NODES,
CONVERSATION_HISTORY_NODES,
type BasicStandardNodeTypes,
} from './node-type';
export { type WorkflowJSON, type WorkflowNodeJSON } from './node';
// !Notice data-set.ts 在用 io-ts 做运行时类型检查,禁止直接导出,避免 io-ts 被打到其它页面中
// export { datasetParams, type DatasetParams } from './data-set';
import { InputType } from '@coze-arch/bot-api/developer_api';
import {
VARIABLE_TYPE_ALIAS_MAP,
ViewVariableType,
} from './view-variable-type';
import { AssistTypeDTO } from './dto';
export { type ValueOf, type WithCustomStyle } from './utils';
// 和后端定义撞了,注释
export { type WorkflowInfo as FrontWorkflowInfo } from './workflow';
export {
type WorkflowNodeRegistry,
WorkflowNodeVariablesMeta,
type NodeMeta,
} from './registry';
/**
* 参数类型展示文案
* @tips workflow 编辑页请使用 {PARAM_TYPE_ALIAS_MAP}
*/
export const PARAM_TYPE_LABEL_MAP: Record<InputType, string> = {
[InputType.String]: 'String',
[InputType.Integer]: 'Integer',
[InputType.Boolean]: 'Boolean',
[InputType.Double]: 'Double',
[InputType.List]: 'List',
[InputType.Object]: 'Object',
};
export const STRING_ASSIST_TYPE_LABEL_MAP = {
[AssistTypeDTO.file]: VARIABLE_TYPE_ALIAS_MAP[ViewVariableType.File],
[AssistTypeDTO.image]: VARIABLE_TYPE_ALIAS_MAP[ViewVariableType.Image],
[AssistTypeDTO.doc]: VARIABLE_TYPE_ALIAS_MAP[ViewVariableType.Doc],
[AssistTypeDTO.code]: VARIABLE_TYPE_ALIAS_MAP[ViewVariableType.Code],
[AssistTypeDTO.ppt]: VARIABLE_TYPE_ALIAS_MAP[ViewVariableType.Ppt],
[AssistTypeDTO.txt]: VARIABLE_TYPE_ALIAS_MAP[ViewVariableType.Txt],
[AssistTypeDTO.excel]: VARIABLE_TYPE_ALIAS_MAP[ViewVariableType.Excel],
[AssistTypeDTO.audio]: VARIABLE_TYPE_ALIAS_MAP[ViewVariableType.Audio],
[AssistTypeDTO.zip]: VARIABLE_TYPE_ALIAS_MAP[ViewVariableType.Zip],
[AssistTypeDTO.video]: VARIABLE_TYPE_ALIAS_MAP[ViewVariableType.Video],
[AssistTypeDTO.svg]: VARIABLE_TYPE_ALIAS_MAP[ViewVariableType.Svg],
};
export enum WorkflowExecStatus {
DEFAULT = 'default',
/** 执行中 */
EXECUTING = 'executing',
/** 执行结束(此时依然有执行结束 banner且工作流为 disable 状态) */
DONE = 'done',
}
export * from './llm';
export {
type WorkflowDatabase,
type DatabaseField,
type DatabaseSettingField,
type DatabaseSettingFieldDTO,
type DatabaseCondition,
type DatabaseConditionOperator,
type DatabaseConditionLeft,
type DatabaseConditionRight,
type DatabaseConditionDTO,
} from './database';
export {
ConditionLogic,
type ConditionOperator,
ConditionLogicDTO,
} from './condition';

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 跟后端约定好的协议workflow 后端不感知。对应 api/bot/get_type_list 接口中 response.data.modal_list[*].model_params[*].default 的 key
export enum GenerationDiversity {
Customize = 'default_val',
Creative = 'creative',
Balance = 'balance',
Precise = 'precise',
}
export const RESPONSE_FORMAT_NAME = 'response_format';

View File

@@ -0,0 +1,171 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* 节点基础类型定义
*/
export enum StandardNodeType {
Start = '1',
End = '2',
LLM = '3',
Api = '4',
Code = '5',
Dataset = '6',
If = '8',
SubWorkflow = '9',
Variable = '11',
Database = '12',
Output = '13',
Imageflow = '14',
Text = '15',
ImageGenerate = '16',
ImageReference = '17',
Question = '18',
Break = '19',
SetVariable = '20',
Loop = '21',
Intent = '22',
// 对于新节点,可以使用文本的形式来编写 StandardNodeType不再依赖 NodeTemplateType
ImageCanvas = '23',
SceneChat = '25',
SceneVariable = '24',
/** 长期记忆long term memory */
LTM = '26',
/** 数据库写入节点 */
DatasetWrite = '27',
Batch = '28',
Continue = '29',
// 输入节点
Input = '30',
Comment = '31',
// 变量聚合
VariableMerge = '32',
// 查询消息列表
QueryMessageList = '37',
// 清空上下文节点
ClearContext = '38',
// 创建会话节点
CreateConversation = '39',
// 触发器 CURD 4个节点
// TriggerCreate = '33',
// TriggerUpdate = '34',
TriggerUpsert = '34',
TriggerDelete = '35',
TriggerRead = '36',
VariableAssign = '40',
// http 节点
Http = '45',
// 数据库 crud 节点
DatabaseUpdate = '42',
DatabaseQuery = '43',
DatabaseDelete = '44',
DatabaseCreate = '46',
// 更新会话
UpdateConversation = '51',
// 删除会话
DeleteConversation = '52',
// 查询会话列表
QueryConversationList = '53',
// 查询会话历史
QueryConversationHistory = '54',
// 创建消息(某个会话)
CreateMessage = '55',
// 更新消息(某个会话的某个消息)
UpdateMessage = '56',
// 删除消息(某个会话的某个消息)
DeleteMessage = '57',
JsonStringify = '58',
JsonParser = '59',
}
/**
* 除了 Api、SubWorkflow、Imageflow 之外的基础节点类型
*/
export type BasicStandardNodeTypes = Exclude<
StandardNodeType,
| StandardNodeType.Api
| StandardNodeType.Imageflow
| StandardNodeType.SubWorkflow
>;
/** 节点展示排序 */
export const NODE_ORDER = {
[StandardNodeType.Start]: 1,
[StandardNodeType.End]: 2,
[StandardNodeType.Api]: 3,
[StandardNodeType.LLM]: 4,
[StandardNodeType.Code]: 5,
[StandardNodeType.Dataset]: 6,
[StandardNodeType.SubWorkflow]: 7,
[StandardNodeType.Imageflow]: 8,
[StandardNodeType.If]: 9,
[StandardNodeType.Loop]: 10,
[StandardNodeType.Intent]: 11,
[StandardNodeType.Text]: 12,
[StandardNodeType.Output]: 13,
[StandardNodeType.Question]: 14,
[StandardNodeType.Variable]: 15,
[StandardNodeType.Database]: 16,
[StandardNodeType.LTM]: 17,
[StandardNodeType.Batch]: 18,
[StandardNodeType.Input]: 19,
[StandardNodeType.SetVariable]: 20,
[StandardNodeType.Break]: 21,
[StandardNodeType.Continue]: 22,
[StandardNodeType.SceneChat]: 23,
[StandardNodeType.SceneVariable]: 24,
// [StandardNodeType.TriggerCreate]: 25,
[StandardNodeType.TriggerUpsert]: 26,
[StandardNodeType.TriggerRead]: 27,
[StandardNodeType.TriggerDelete]: 28,
};
/** 会话类节点 */
export const CONVERSATION_NODES = [
StandardNodeType.CreateConversation,
StandardNodeType.UpdateConversation,
StandardNodeType.DeleteConversation,
StandardNodeType.QueryConversationList,
];
/** 消息类节点 */
export const MESSAGE_NODES = [
StandardNodeType.CreateMessage,
StandardNodeType.UpdateMessage,
StandardNodeType.DeleteMessage,
StandardNodeType.QueryMessageList,
];
/** 会话历史类节点 */
export const CONVERSATION_HISTORY_NODES = [
StandardNodeType.QueryConversationHistory,
StandardNodeType.ClearContext,
];

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { FlowNodeBaseType } from '@flowgram-adapter/free-layout-editor';
import type {
WorkflowEdgeJSON,
WorkflowNodeMeta,
} from '@flowgram-adapter/free-layout-editor';
import type { StandardNodeType } from './node-type';
export interface WorkflowNodeJSON<T = Record<string, unknown>> {
id: string;
type: StandardNodeType | FlowNodeBaseType | string;
meta?: WorkflowNodeMeta;
data: T;
version?: string;
blocks?: WorkflowNodeJSON[];
edges?: WorkflowEdgeJSON[];
}
export interface WorkflowJSON {
nodes: WorkflowNodeJSON[];
edges: WorkflowEdgeJSON[];
}
// 节点模版类型
export { NodeTemplateType } from '@coze-arch/bot-api/workflow_api';

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ViewVariableType } from './view-variable-type';
/**
* 参数定义
*
* 递归定义,包含了复杂类型的层级结构
*/
export interface RecursedParamDefinition {
name?: string;
/** Tree 组件要求每一个节点都有 key而 key 不适合用名称(前后缀)等任何方式赋值,最终确定由接口转换层一次性提供随机 key */
fieldRandomKey?: string;
desc?: string;
required?: boolean;
type: ViewVariableType;
children?: RecursedParamDefinition[];
// region 参数值定义
// 输入参数的值可以来自上游变量引用,也可以是用户输入的定值(复杂类型则只允许引用)
// 如果是定值,传 fixedValue
// 如果是引用,传 quotedValue
isQuote?: ParamValueType;
/** 参数定值 */
fixedValue?: string;
/** 参数引用 */
quotedValue?: [nodeId: string, ...path: string[]]; // string[]
// endregion
}
export enum ParamValueType {
QUOTE = 'quote',
FIXED = 'fixed',
}

View File

@@ -0,0 +1,184 @@
/*
* 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-namespace */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import {
type WorkflowNodeEntity,
type WorkflowSubCanvas,
type WorkflowNodeRegistry as WorkflowOriginNodeRegistry,
type WorkflowNodeJSON,
} from '@flowgram-adapter/free-layout-editor';
import {
type OutputValueVO,
type InputValueVO,
type ValueExpression,
} from './vo';
import { type VariableTagProps } from './view-variable-type';
import { type StandardNodeType } from './node-type';
export interface WorkflowNodeVariablesMeta {
/**
* 输出变量路径, 默认 ['outputs']
*/
outputsPathList?: string[];
/**
* 输入变量路径,默认 ['inputs.inputParameters']
*/
inputsPathList?: string[];
batchInputListPath?: string;
}
export namespace WorkflowNodeVariablesMeta {
export const DEFAULT: WorkflowNodeVariablesMeta = {
outputsPathList: ['outputs'],
inputsPathList: ['inputs.inputParameters'],
batchInputListPath: 'inputs.batch.inputLists',
};
}
export interface NodeMeta<NodeTest = any> {
isStart?: boolean;
isNodeEnd?: boolean;
deleteDisable?: boolean;
copyDisable?: boolean; // 无法复制
nodeDTOType: StandardNodeType;
nodeMetaPath?: string;
batchPath?: string;
outputsPath?: string;
inputParametersPath?: string;
renderKey?: string;
style?: any;
errorStyle?: any;
size?: { width: number; height: number }; // 初始化时候的默认大小
defaultPorts?: Array<any>;
useDynamicPort?: boolean;
disableSideSheet?: boolean;
subCanvas?: (node: WorkflowNodeEntity) => WorkflowSubCanvas | undefined;
/** 是否展示触发器 icon */
showTrigger?: (props: { projectId?: string }) => boolean;
/** 是否隐藏测试 */
hideTest?: boolean;
/** 获取卡片输入变量标签 */
getInputVariableTag?: (
/** 变量名称 */
variableName: string | undefined,
/** 输入值或变量 */
input: ValueExpression,
/** 附加参数 */
extra?: {
/**
* 变量服务,从 '@coze-workflow/variable' 导入的 WorkflowVariableService
* 这里不写上类型,是为了避免循环引用,另外 base 包引用 variable 包内容也不合理
*/
variableService: any;
/** 当前节点 */
node: WorkflowNodeEntity;
},
) => VariableTagProps;
/** 是否支持单节点调试copilot生成表单参数 */
enableCopilotGenerateTestNodeForm?: boolean;
/**
* 获取llm ids所有需要用到大模型选择的的节点必须实现该方法
* 因为用户可能存在账户升级或者模型下架正常拉到的列表数据可能没有该id
* 所以模型列表获取需要遍历所有节点拿到被用过的模型id
* @param nodeJSON
* @returns
*/
getLLMModelIdsByNodeJSON?: (
nodeJSON: WorkflowNodeJSON,
) => string[] | undefined | string | number | number[];
/**
* 节点 header 不可编辑 & 隐藏 ... 按钮
*/
headerReadonly?: boolean;
headerReadonlyAllowDeleteOperation?: boolean;
/**
* 节点帮助文档
*/
helpLink?: string | ((props: { apiName: string }) => string);
/**
* 节点测试相关数据
*/
test?: NodeTest;
}
export interface WorkflowNodeRegistry<NodeTestMeta = any>
extends WorkflowOriginNodeRegistry {
variablesMeta?: WorkflowNodeVariablesMeta;
meta: NodeMeta<NodeTestMeta>;
/**
* 特化:根据节点获取输入 Parameters 的值,默认通过 /inputParameters 字段判断
* @param node
* @returns
*/
getNodeInputParameters?: (node: FlowNodeEntity) => InputValueVO[] | undefined;
/**
* 获取舞台节点右侧更多菜单的额外操作,目前在子流程节点和插件节点中使用
* @returns
*/
getHeaderExtraOperation?: (
formValues: any,
node: FlowNodeEntity,
) => React.ReactNode;
/**
* 特化:根据节点获取输出 Outputs 的值,默认通过 /outputs 字段判断
*/
getNodeOutputs?: (node: FlowNodeEntity) => OutputValueVO[] | undefined;
/**
* 节点提交前最后一次转化,这个方法在 `formMeta.transformOnSubmit` 之后执行
* @param node 节点数据
* @param 转化后的节点数据
*/
beforeNodeSubmit?: (node: WorkflowNodeJSON) => WorkflowNodeJSON;
/**
* - 节点 Registry 初始化,可以在这里获取初始化数据,然后再进行表单渲染,注意此时 node 还没有创建
* - 目前适用于 v2 表单引擎节点
* @param nodeJSON 节点初始化数据(点击添加时,或者从后端获取时的节点数据)
* @param context WorkflowPlaygroundContext参见 packages/workflow/playground/src/workflow-playground-context.ts
* @returns Promise<void>
*/
onInit?: (nodeJSON: WorkflowNodeJSON, context: any) => Promise<void>;
/**
* - 是否有错误信息(从 service 中拿),如果有错误信息返回错误信息,否则返回空串或者 undefined
* - 目前在 node-context-provider.tsx 中消费,监听当前节点错误信息,如果有,更新 FlowNodeErrorData 数据,从而触发节点渲染错误状态
* - 目前适用于 v2 表单引擎节点
* @param nodeJSON 节点初始化数据(点击添加时,或者从后端获取时的节点数据)
* @param context WorkflowPlaygroundContext参见 packages/workflow/playground/src/workflow-playground-context.ts
*/
checkError?: (nodeJSON: WorkflowNodeJSON, context: any) => string | undefined;
/**
* - 节点销毁时调用,用于做一些清理工作,例如将相关节点的错误信息清空,回收资源
* - 目前适用于 v2 表单引擎节点
* @param node 节点
* @param context workflow-playground-context
* @returns
*/
onDispose?: (nodeJSON: WorkflowNodeJSON, context: any) => void;
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type ValueOf<T, K = keyof T> = K extends keyof T ? T[K] : never;
import { type CSSProperties } from 'react';
export type WithCustomStyle<T = object> = {
className?: string;
style?: CSSProperties;
} & T;

View File

@@ -0,0 +1,124 @@
/*
* 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 ViewVariableType } from './view-variable-type';
export interface ViewVariableTreeNode {
key: string;
type: ViewVariableType;
name: string;
children?: ViewVariableTreeNode[];
required?: boolean;
description?: string;
// 标识该条参数是内置参数,默认为 false
isPreset?: boolean;
// 标识该条参数是否启用,默认为 false
enabled?: boolean;
// 用户自定义节点名展示
label?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
defaultValue?: any;
}
export interface ViewVariableMeta extends ViewVariableTreeNode {
required?: boolean;
description?: string;
readonly?: boolean;
mutable?: boolean; // 是否可以被 Set 节点的左值选中
}
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace ViewVariableTreeNode {
/**
* 通过path 查询 子节点
* @param node
*/
export function getVariableTreeNodeByPath(
node: ViewVariableTreeNode,
keyPath: string[],
): ViewVariableTreeNode | undefined {
keyPath = keyPath.slice();
let currentNode: ViewVariableTreeNode | undefined = node;
let key: string | undefined = keyPath.shift();
while (key && currentNode) {
key = keyPath.shift();
if (key) {
currentNode = (currentNode.children || []).find(
child => child.key === key,
) as ViewVariableTreeNode;
if (!currentNode) {
return undefined;
}
}
}
return currentNode;
}
/**
* ['xxx', 'xxx'] -> 'a.b.c'
* @param keyPath
* @deprecated
*/
export function keyPathToNameString(
node: ViewVariableTreeNode,
keyPath: string[],
): string {
const result: string[] = [];
keyPath = keyPath.slice();
let currentNode: ViewVariableTreeNode | undefined = node;
let key: string | undefined = keyPath.shift();
while (key && currentNode) {
result.push(currentNode.name);
key = keyPath.shift();
if (key) {
currentNode = (currentNode.children || []).find(
child => child.key === key,
) as ViewVariableTreeNode;
if (!currentNode) {
return '';
}
}
}
return result.join('.');
}
/**
* 'a.b.c' -> ['xxx', 'xxx']
* @param nameStr
* @deprecated
*/
export function nameStringToKeyPath(
node: ViewVariableTreeNode,
nameStr: string,
): string[] {
const result: string[] = [];
const nameList = nameStr.split('.');
let currentNode: ViewVariableTreeNode | undefined = node;
let name = nameList.shift();
while (name && currentNode) {
result.push(currentNode.key);
name = nameList.shift();
if (name) {
currentNode = (currentNode.children || []).find(
child => child.name === name,
) as ViewVariableTreeNode;
if (!currentNode) {
return [];
}
}
}
return result;
}
}

View File

@@ -0,0 +1,273 @@
/*
* 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 ReactNode } from 'react';
/**
* 前端变量类型
*/
export enum ViewVariableType {
String = 1,
Integer,
Boolean,
Number,
Object = 6,
Image,
File,
Doc,
Code,
Ppt,
Txt,
Excel,
Audio,
Zip,
Video,
Svg,
Voice,
Time,
// 上面是 api 中定义的 InputType。下面是整合后的。从 99 开始,避免和后端定义撞车
ArrayString = 99,
ArrayInteger,
ArrayBoolean,
ArrayNumber,
ArrayObject,
ArrayImage,
ArrayFile,
ArrayDoc,
ArrayCode,
ArrayPpt,
ArrayTxt,
ArrayExcel,
ArrayAudio,
ArrayZip,
ArrayVideo,
ArraySvg,
ArrayVoice,
ArrayTime,
}
/**
* 使用 JSON 表示值的变量类型
*/
export const JSON_INPUT_TYPES = [
ViewVariableType.Object,
ViewVariableType.ArrayString,
ViewVariableType.ArrayInteger,
ViewVariableType.ArrayBoolean,
ViewVariableType.ArrayNumber,
ViewVariableType.ArrayObject,
ViewVariableType.ArrayTime,
];
export const FILE_TYPES = [
ViewVariableType.File,
ViewVariableType.Image,
ViewVariableType.Doc,
ViewVariableType.Code,
ViewVariableType.Ppt,
ViewVariableType.Txt,
ViewVariableType.Excel,
ViewVariableType.Audio,
ViewVariableType.Zip,
ViewVariableType.Video,
ViewVariableType.Svg,
ViewVariableType.Voice,
ViewVariableType.ArrayImage,
ViewVariableType.ArrayFile,
ViewVariableType.ArrayDoc,
ViewVariableType.ArrayCode,
ViewVariableType.ArrayPpt,
ViewVariableType.ArrayTxt,
ViewVariableType.ArrayExcel,
ViewVariableType.ArrayAudio,
ViewVariableType.ArrayZip,
ViewVariableType.ArrayVideo,
ViewVariableType.ArraySvg,
ViewVariableType.ArrayVoice,
];
// 基础类型,及其对应数组类型的配对
export const BASE_ARRAY_PAIR: [ViewVariableType, ViewVariableType][] = [
[ViewVariableType.String, ViewVariableType.ArrayString],
[ViewVariableType.Integer, ViewVariableType.ArrayInteger],
[ViewVariableType.Boolean, ViewVariableType.ArrayBoolean],
[ViewVariableType.Number, ViewVariableType.ArrayNumber],
[ViewVariableType.Object, ViewVariableType.ArrayObject],
[ViewVariableType.Image, ViewVariableType.ArrayImage],
[ViewVariableType.File, ViewVariableType.ArrayFile],
[ViewVariableType.Doc, ViewVariableType.ArrayDoc],
[ViewVariableType.Code, ViewVariableType.ArrayCode],
[ViewVariableType.Ppt, ViewVariableType.ArrayPpt],
[ViewVariableType.Txt, ViewVariableType.ArrayTxt],
[ViewVariableType.Excel, ViewVariableType.ArrayExcel],
[ViewVariableType.Audio, ViewVariableType.ArrayAudio],
[ViewVariableType.Zip, ViewVariableType.ArrayZip],
[ViewVariableType.Video, ViewVariableType.ArrayVideo],
[ViewVariableType.Svg, ViewVariableType.ArraySvg],
[ViewVariableType.Voice, ViewVariableType.ArrayVoice],
[ViewVariableType.Time, ViewVariableType.ArrayTime],
];
export const VARIABLE_TYPE_ALIAS_MAP: Record<ViewVariableType, string> = {
[ViewVariableType.String]: 'String',
[ViewVariableType.Integer]: 'Integer',
[ViewVariableType.Boolean]: 'Boolean',
[ViewVariableType.Number]: 'Number',
[ViewVariableType.Object]: 'Object',
[ViewVariableType.Image]: 'Image',
[ViewVariableType.File]: 'File',
[ViewVariableType.Doc]: 'Doc',
[ViewVariableType.Code]: 'Code',
[ViewVariableType.Ppt]: 'PPT',
[ViewVariableType.Txt]: 'Txt',
[ViewVariableType.Excel]: 'Excel',
[ViewVariableType.Audio]: 'Audio',
[ViewVariableType.Zip]: 'Zip',
[ViewVariableType.Video]: 'Video',
[ViewVariableType.Svg]: 'Svg',
[ViewVariableType.Voice]: 'Voice',
[ViewVariableType.Time]: 'Time',
[ViewVariableType.ArrayString]: 'Array<String>',
[ViewVariableType.ArrayInteger]: 'Array<Integer>',
[ViewVariableType.ArrayBoolean]: 'Array<Boolean>',
[ViewVariableType.ArrayNumber]: 'Array<Number>',
[ViewVariableType.ArrayObject]: 'Array<Object>',
[ViewVariableType.ArrayImage]: 'Array<Image>',
[ViewVariableType.ArrayFile]: 'Array<File>',
[ViewVariableType.ArrayDoc]: 'Array<Doc>',
[ViewVariableType.ArrayCode]: 'Array<Code>',
[ViewVariableType.ArrayPpt]: 'Array<PPT>',
[ViewVariableType.ArrayTxt]: 'Array<Txt>',
[ViewVariableType.ArrayExcel]: 'Array<Excel>',
[ViewVariableType.ArrayAudio]: 'Array<Audio>',
[ViewVariableType.ArrayZip]: 'Array<Zip>',
[ViewVariableType.ArrayVideo]: 'Array<Video>',
[ViewVariableType.ArraySvg]: 'Array<Svg>',
[ViewVariableType.ArrayVoice]: 'Array<Voice>',
[ViewVariableType.ArrayTime]: 'Array<Time>',
};
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace ViewVariableType {
export const LabelMap = VARIABLE_TYPE_ALIAS_MAP;
export const ArrayTypes = BASE_ARRAY_PAIR.map(_pair => _pair[1]);
export function getLabel(type: ViewVariableType): string {
return LabelMap[type];
}
/**
* 获取所有变量类型的补集
* 该函数由GPT生成
* @param inputTypes
*/
export function getComplement(inputTypes: ViewVariableType[]) {
const allTypes: ViewVariableType[] = [
...BASE_ARRAY_PAIR.map(_pair => _pair[0]),
...BASE_ARRAY_PAIR.map(_pair => _pair[1]),
];
return allTypes.filter(type => !inputTypes.includes(type));
}
export function canDrilldown(type: ViewVariableType): boolean {
return [ViewVariableType.Object, ViewVariableType.ArrayObject].includes(
type,
);
}
export function isArrayType(type: ViewVariableType): boolean {
const arrayTypes = BASE_ARRAY_PAIR.map(_pair => _pair[1]);
return arrayTypes.includes(type);
}
export function isFileType(type: ViewVariableType): boolean {
return FILE_TYPES.includes(type);
}
export function isVoiceType(type: ViewVariableType): boolean {
return [ViewVariableType.Voice].includes(type);
}
/**
* 使用 JSON 表示值的变量类型
* @param type
* @returns
*/
export function isJSONInputType(type: ViewVariableType): boolean {
return JSON_INPUT_TYPES.includes(type);
}
export function getArraySubType(type: ViewVariableType): ViewVariableType {
const subType = BASE_ARRAY_PAIR.find(_pair => _pair[1] === type)?.[0];
if (!subType) {
throw new Error('WorkflowVariableEntity Error: Unknown Variable Type');
}
return subType;
}
export function wrapToArrayType(type: ViewVariableType): ViewVariableType {
const arrayType = BASE_ARRAY_PAIR.find(_pair => _pair[0] === type)?.[1];
if (!arrayType) {
throw new Error('WorkflowVariableEntity Error: Unknown Variable Type');
}
return arrayType;
}
export function getAllArrayType(): ViewVariableType[] {
const allTypes: ViewVariableType[] = [
...BASE_ARRAY_PAIR.map(_pair => _pair[0]),
...BASE_ARRAY_PAIR.map(_pair => _pair[1]),
];
return allTypes.filter(isArrayType);
}
}
export interface InputVariable {
name: string;
/**
* id 不一定有非前后端约定前端强加上的。风险workflowSchema 结构化时可能会删掉此属性
* 如需 id ,要在 nodes schema 中定义
* getDefaultAppendValue: () => ({
id: nanoid(),
}),
* 参考 packages/workflow/nodes/src/workflow-nodes/image-canvas/index.ts
* 只能保证同一节点内唯一,只能保证同一节点内唯一,只能保证同一节点内唯一
* 因为节点可以创建副本
*/
id?: string;
type: ViewVariableType;
/**
* 索引
*/
index: number;
}
/** 卡片上的变量展示标签 */
export interface VariableTagProps {
key?: string;
/* 变量类型 */
type?: ViewVariableType;
/* 变量名,为空时会展示为 Undefined/未定义 */
label?: ReactNode;
/** 是否有效 */
invalid?: boolean;
}

View File

@@ -0,0 +1,191 @@
/*
* 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.
*/
/**
* 这个文件定义的是前端消费的数据, 用 VO (view object) 来表示
*/
import { isNil, isUndefined } from 'lodash-es';
import type { ViewVariableType } from './view-variable-type';
export interface RefExpressionContent {
/**
* 引用值是一个变量的 key 路径
*/
keyPath: string[];
}
/**
* 值表达式类型
*/
export enum ValueExpressionType {
LITERAL = 'literal',
REF = 'ref',
OBJECT_REF = 'object_ref',
}
/**
* 用来存储 ValueExpression 的原始数据供前端消费
*/
export interface ValueExpressionRawMeta {
type?: ViewVariableType;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}
/**
* 文本表达式
*/
export interface LiteralExpression {
type: ValueExpressionType.LITERAL;
content?: string | number | boolean | Array<unknown>;
rawMeta?: ValueExpressionRawMeta;
}
/**
* 对象表达式
*/
export interface ObjectRefExpression {
type: ValueExpressionType.OBJECT_REF;
content?: unknown;
rawMeta?: ValueExpressionRawMeta;
}
/**
* 引用变量
*/
export interface RefExpression {
type: ValueExpressionType.REF;
content?: RefExpressionContent;
/**
* rawMeta 记录了该 ref expression 的类型
* 可能和所引用变量类型不同,此时会触发类型自动转换: [Workflow 类型自动转换]()
*/
rawMeta?: ValueExpressionRawMeta;
}
/**
* 前端输入值表达式
*/
export type ValueExpression =
| RefExpression
| LiteralExpression
| ObjectRefExpression;
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace ValueExpression {
export function isRef(value: ValueExpression): value is RefExpression {
return value.type === ValueExpressionType.REF;
}
export function isLiteral(
value: ValueExpression,
): value is LiteralExpression {
return value.type === ValueExpressionType.LITERAL;
}
export function isObjectRef(
value: ValueExpression,
): value is ObjectRefExpression {
return value?.type === ValueExpressionType.OBJECT_REF;
}
export function isExpression(value?: ValueExpression): boolean {
if (isUndefined(value)) {
return false;
}
return isRef(value) || isLiteral(value);
}
export function isEmpty(value: ValueExpression | undefined): boolean {
if (value?.type === ValueExpressionType.OBJECT_REF) {
return false;
}
if (value === null) {
return true;
}
// 如果 value 不是对象或者函数,也就是原生类型,在插件自定义组件中会存在
if (typeof value !== 'object' && typeof value !== 'function') {
return isNil(value);
}
// value.content 有多种类型,可能是 false
if (value?.content === '' || isNil(value?.content)) {
return true;
}
if (Array.isArray(value?.content)) {
return value?.content.length === 0;
}
if (value?.type === ValueExpressionType.REF) {
return !(value?.content as RefExpressionContent)?.keyPath?.length;
}
return false;
}
}
/**
* 前端的value 输入值
* {
* name: '',
* input: {
* type: ValueExpressionType.REF,
* content: {
* keyPath: ['nodeId', 'out3']
* }
*
* }
* }
*/
export interface InputValueVO {
name?: string;
input: ValueExpression;
children?: InputValueVO[];
key?: string;
}
export interface InputTypeValueVO {
name: string;
type: ViewVariableType;
input: ValueExpression;
}
export enum BatchMode {
Single = 'single',
Batch = 'batch',
}
export interface BatchVOInputList {
id: string;
name: string;
input: ValueExpression;
}
export interface BatchVO {
batchSize: number;
concurrentSize: number;
inputLists: BatchVOInputList[];
}
export interface OutputValueVO {
key: string;
name: string;
type: ViewVariableType;
description?: string;
readonly?: boolean;
required?: boolean;
children?: OutputValueVO[];
}

View File

@@ -0,0 +1,29 @@
/*
* 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 Workflow,
type WorkFlowDevStatus,
type VCSCanvasData,
type OperationInfo,
type WorkFlowStatus,
} from '@coze-arch/bot-api/workflow_api';
export type WorkflowInfo = Omit<Workflow, 'status'> & {
status?: WorkFlowDevStatus | WorkFlowStatus;
vcsData?: VCSCanvasData;
operationInfo?: OperationInfo;
};

View File

@@ -0,0 +1,35 @@
/*
* 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 { NODE_TEST_ID_PREFIX } from '../constants';
export const concatTestId = (...testIds: string[]) =>
testIds.filter(id => !!id).join('.');
/**
* 生成节点的测试id
* @example concatNodeTestId(node, 'right-panel') => playground.node.100001.right-panel
* @param node 节点
* @param testIds 其它id
* @returns
*/
export const concatNodeTestId = (node: FlowNodeEntity, ...testIds: string[]) =>
concatTestId(
node?.id ? concatTestId(NODE_TEST_ID_PREFIX, node.id) : '',
...testIds,
);

View File

@@ -0,0 +1,121 @@
/*
* 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 { cloneDeep } from 'lodash-es';
import { isFormV2 } from '@flowgram-adapter/free-layout-editor';
import { FlowNodeFormData } from '@flowgram-adapter/free-layout-editor';
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
/**
* 找到以 pathEnds 为结尾的 FormItem并获取它的值
* @param node
* @param pathEnds
* @returns
*/
export function getFormValueByPathEnds<T = unknown>(
node: FlowNodeEntity,
pathEnds: string,
): T | undefined {
return isFormV2(node)
? getFormValueByPathEndsV2<T>(node, pathEnds)
: getFormValueByPathEndsV1<T>(node, pathEnds);
}
function getFormValueByPathEndsV1<T = unknown>(
node: FlowNodeEntity,
pathEnds: string,
): T | undefined {
const form = node.getData(FlowNodeFormData).formModel;
const paths: string[] = [...form.formItemPathMap.keys()];
const formPath = paths.find(path => path.endsWith(pathEnds));
if (!formPath) {
return undefined;
}
const formValue = cloneDeep<T>(form.getFormItemValueByPath(formPath));
return formValue;
}
function getFormValueByPathEndsV2<T = unknown>(
node: FlowNodeEntity,
pathEnds: string,
): T | undefined {
const form = node.getData(FlowNodeFormData).formModel;
const data = form.getFormItemValueByPath('/');
if (!data || typeof data !== 'object') {
return undefined;
}
const value = findValueByPathEnds<T>(data, pathEnds);
return cloneDeep(value);
}
const findValueByPathEnds = <T = unknown>(
obj: unknown,
pathEnds: string,
currentPath = '',
): T | undefined => {
if (!obj) {
return undefined;
}
// 检查当前路径是否以 pathEnds 结尾
if (currentPath.endsWith(pathEnds)) {
return obj as T;
}
// 处理对象
if (typeof obj === 'object' && !Array.isArray(obj)) {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const newPath = currentPath ? `${currentPath}/${key}` : `/${key}`;
if (newPath.endsWith(pathEnds)) {
return obj[key] as T;
}
// 递归查找子对象
const result = findValueByPathEnds(obj[key], pathEnds, newPath);
if (result !== undefined) {
return result as T;
}
}
}
}
// 处理数组
if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
const newPath = `${currentPath}/${i}`;
if (newPath.endsWith(pathEnds)) {
return obj[i] as T;
}
// 递归查找数组元素
const result = findValueByPathEnds(obj[i], pathEnds, newPath);
if (result !== undefined) {
return result as T;
}
}
}
return undefined;
};

View File

@@ -0,0 +1,68 @@
/*
* 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 { ViewVariableType } from '../types/view-variable-type';
export const ACCEPT_MAP = {
[ViewVariableType.Image]: ['image/*'],
[ViewVariableType.Doc]: ['.docx', '.doc', '.pdf'],
[ViewVariableType.Audio]: [
'.mp3',
'.wav',
'.aac',
'.flac',
'.ogg',
'.wma',
'.alac',
'.mid',
'.midi',
'.ac3',
'.dsd',
],
[ViewVariableType.Excel]: ['.xls', '.xlsx', '.csv'],
[ViewVariableType.Video]: ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv'],
[ViewVariableType.Zip]: ['.zip', '.rar', '.7z', '.tar', '.gz', '.bz2'],
[ViewVariableType.Code]: ['.py', '.java', '.c', '.cpp', '.js', '.css'],
[ViewVariableType.Txt]: ['.txt'],
[ViewVariableType.Ppt]: ['.ppt', '.pptx'],
[ViewVariableType.Svg]: ['.svg'],
};
export const getFileAccept = (
inputType: ViewVariableType,
availableFileTypes?: ViewVariableType[],
) => {
let accept: string;
const itemType = ViewVariableType.isArrayType(inputType)
? ViewVariableType.getArraySubType(inputType)
: inputType;
if (itemType === ViewVariableType.File) {
if (availableFileTypes?.length) {
accept = availableFileTypes
.map(type => ACCEPT_MAP[type]?.join(','))
.join(',');
} else {
accept = Object.values(ACCEPT_MAP)
.map(items => items.join(','))
.join(',');
}
} else {
accept = (ACCEPT_MAP[itemType] || []).join(',');
}
return accept;
};

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {
SchemaExtractorParserName,
SchemaExtractor,
type SchemaExtractorConfig,
type SchemaExtracted,
type SchemaExtractorNodeConfig,
type ParsedVariableMergeGroups,
} from './schema-extractor';
export { concatTestId, concatNodeTestId } from './concat-test-id';
export {
type NodeResultExtracted,
type CaseResultData,
NodeResultExtractor,
} from './node-result-extractor';
export { parseImagesFromOutputData } from './output-image-parser';
export { reporter, captureException } from './slardar-reporter';
export { getFormValueByPathEnds } from './form-helpers';
export { isGeneralWorkflow } from './is-general-workflow';
export { isPresetStartParams, isUserInputStartParams } from './start-params';
export {
type TraverseValue,
type TraverseNode,
type TraverseContext,
type TraverseHandler,
traverse,
} from './traverse';
export { getFileAccept } from './get-file-accept';

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { WorkflowMode } from '@coze-arch/bot-api/workflow_api';
/**
*
* @param flowMode 是否广义上的 workflow包含原来的 Workflow 和 Coze 2.0 新增的 Chatflow
* @returns
*/
export const isGeneralWorkflow = (flowMode: WorkflowMode) =>
flowMode === WorkflowMode.Workflow || flowMode === WorkflowMode.ChatFlow;

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type WorkflowJSON } from '../../types';
import { type NodeResult } from '../../api';
import {
type NodeResultExtracted,
type NodeResultExtractorParser,
} from './type';
import { defaultParser } from './parsers';
export { type NodeResultExtracted, type CaseResultData } from './type';
export class NodeResultExtractor {
private readonly parser: NodeResultExtractorParser;
public constructor(
private readonly nodeResults: NodeResult[],
private readonly workflowSchema: WorkflowJSON,
) {
this.parser = defaultParser;
}
public extract(): NodeResultExtracted[] {
return (
this.nodeResults
?.filter(Boolean)
?.map(item => this.parser(item, this.workflowSchema)) || []
);
}
}

View File

@@ -0,0 +1,163 @@
/*
* 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 */
/* eslint-disable complexity */
import { isString } from 'lodash-es';
import { typeSafeJSONParse } from '@coze-arch/bot-utils';
import {
type NodeResultExtractorParser,
type NodeResultExtracted,
type CaseResultData,
} from '../type';
import { parseImagesFromOutputData } from '../../output-image-parser';
import { StandardNodeType, type WorkflowNodeJSON } from '../../../types';
import { type NodeResult, TerminatePlanType } from '../../../api';
export const defaultParser: NodeResultExtractorParser = (
nodeResult,
workflowSchema,
): NodeResultExtracted => {
const { nodeId, isBatch, batch } = nodeResult;
const node = workflowSchema.nodes.find(it => it.id === nodeId);
const nodeType = node?.type as StandardNodeType;
if (!isBatch) {
return {
nodeId,
nodeType,
isBatch,
caseResult: [parseData(nodeResult, node)],
};
}
const batchNodeResult = typeSafeJSONParse(batch) as NodeResult[];
const caseResult = (batchNodeResult
?.filter(Boolean)
?.map(item => parseData(item, node, isBatch))
?.filter(Boolean) || []) as CaseResultData[];
return {
nodeId,
nodeType,
isBatch,
caseResult,
};
};
const parseData = (
nodeResult: NodeResult,
nodeSchema?: WorkflowNodeJSON,
isBatch = false,
): CaseResultData => {
const { input, output, raw_output: rawOutput, extra, items } = nodeResult;
const dataList: CaseResultData['dataList'] = [];
const inputAsOutput = [
StandardNodeType.End,
StandardNodeType.Output,
].includes(nodeSchema?.type as StandardNodeType);
if (isBatch) {
dataList.push({
title: '本次批处理变量',
data: typeSafeJSONParse(items),
});
}
if (!inputAsOutput) {
dataList.push({
title: '输入',
data: typeSafeJSONParse(input) || input?.toString?.(),
});
}
const finalOutput = inputAsOutput ? input : output;
const outputData =
typeSafeJSONParse(finalOutput) || finalOutput?.toString?.();
const textHasRawout =
nodeSchema?.type === StandardNodeType.Text &&
isString(rawOutput) &&
rawOutput?.length > 0;
// 文本节点的 raw_out 不需要反序列化,一定是 stringbadcase用户拼接的 json 字符串如 '{}'、'123',反序列化后会变成object 和 number
const rawOutputData = textHasRawout
? rawOutput?.toString?.()
: rawOutput
? typeSafeJSONParse(rawOutput) || rawOutput?.toString?.()
: undefined;
/** Code、Llm 节点需要展示 raw */
const hasRawOutput =
(Boolean(nodeSchema?.type) &&
[
StandardNodeType.Code,
StandardNodeType.LLM,
StandardNodeType.Question,
].includes(nodeSchema?.type as StandardNodeType)) ||
textHasRawout;
// Start、Input 节点只展示输入
const hasOutput =
nodeSchema?.type !== StandardNodeType.Start &&
nodeSchema?.type !== StandardNodeType.Input;
if (hasOutput) {
hasRawOutput &&
rawOutputData &&
dataList.push({
title: '原始输出',
data: rawOutputData,
});
const outputTitle = inputAsOutput
? '输出变量'
: rawOutputData
? '最终输出'
: '输出';
outputData &&
dataList.push({
title: outputTitle,
data: outputData,
});
}
if (nodeSchema?.type === StandardNodeType.End) {
const isReturnText =
(typeSafeJSONParse(extra) as any)?.response_extra?.terminal_plan ===
TerminatePlanType.USESETTING;
if (isReturnText) {
dataList.push({
title: '回答内容',
data: typeSafeJSONParse(output) || output?.toString?.(),
});
}
} else if (nodeSchema?.type === StandardNodeType.Output) {
dataList.push({
title: '回答内容',
data: typeSafeJSONParse(output) || output?.toString?.(),
});
}
const outputJsonString = finalOutput ?? '';
return {
dataList,
imgList: parseImagesFromOutputData({
// batch data 的 output 下钻了一层,需要再包一层和 output 的 schema 保持一致
outputData: isBatch
? {
outputList: [typeSafeJSONParse(outputJsonString)].filter(Boolean),
}
: typeSafeJSONParse(outputJsonString),
nodeSchema,
}),
};
};

View File

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

View File

@@ -0,0 +1,35 @@
/*
* 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 { type StandardNodeType, type WorkflowJSON } from '../../types';
import { type NodeResult } from '../../api';
export interface CaseResultData {
dataList?: Array<{ title: string; data: any }>;
imgList?: string[];
}
export interface NodeResultExtracted {
nodeId?: string;
nodeType?: StandardNodeType;
isBatch?: boolean;
caseResult?: CaseResultData[];
}
export type NodeResultExtractorParser = (
nodeResult: NodeResult,
workflowSchema: WorkflowJSON,
) => NodeResultExtracted;

View File

@@ -0,0 +1,120 @@
/*
* 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 { flatten, get } from 'lodash-es';
import { type WorkflowNodeJSON } from '@flowgram-adapter/free-layout-editor';
import { StandardNodeType, VariableTypeDTO, AssistTypeDTO } from '../types';
interface Schema {
type: VariableTypeDTO;
name: string;
schema: Schema | Schema[];
assistType: AssistTypeDTO;
}
const isImageType = (schema: Schema) => {
if (
schema?.type === VariableTypeDTO.image ||
(schema?.type === VariableTypeDTO.string &&
[AssistTypeDTO.image, AssistTypeDTO.svg].includes(schema?.assistType))
) {
return true;
}
};
function getImgList(data: unknown, schema: Schema): string[] {
let imgList: string[] = [];
if (isImageType(schema) && typeof data === 'string') {
imgList.push(data);
}
if (schema?.type === VariableTypeDTO.list && Array.isArray(data)) {
const imgListInItems = (data as unknown[]).map(item =>
getImgList(item, schema.schema as Schema),
);
imgList = imgList.concat(...imgListInItems);
}
if (schema?.type === VariableTypeDTO.object) {
const imgListInObject = Object.entries(
(data as Record<string, unknown>) || {},
).map(([key, value]) =>
getImgList(
value,
((schema.schema as Schema[]) || []).find(
item => item.name === key,
) as Schema,
),
);
imgList = imgList.concat(...imgListInObject);
}
return imgList;
}
/**
* 从节点 output data 中解析图片链接
* @param outputData 节点输出数据 JSON 序列化后的字符串
* @param nodeSchema 节点 schema
* @param excludeNodeTypes 不解析该类型节点的图片链接
*/
export function parseImagesFromOutputData({
outputData,
nodeSchema,
excludeNodeTypes = [],
}: {
outputData?: any;
nodeSchema?: WorkflowNodeJSON;
excludeNodeTypes?: StandardNodeType[];
}): string[] {
if (!nodeSchema || !outputData) {
return [];
}
if (excludeNodeTypes.includes(nodeSchema?.type as StandardNodeType)) {
return [];
}
let outputParameters: any[] = [];
if (
nodeSchema?.type === StandardNodeType.End ||
nodeSchema?.type === StandardNodeType.Output
) {
outputParameters = (nodeSchema?.data?.inputs as any)?.inputParameters;
} else {
outputParameters = nodeSchema?.data?.outputs as any[];
}
if (!outputParameters) {
return [];
}
const imgListInOutput = flatten(
outputParameters.map(p =>
getImgList(
get(outputData, p?.name),
p?.input
? {
...p.input,
name: p.name,
}
: p,
),
),
).filter(url => !!url);
return imgListInOutput;
}

View File

@@ -0,0 +1,309 @@
/*
* 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 { describe, expect, it } from 'vitest';
import { StandardNodeType, type WorkflowJSON } from '../../../types';
import {
type SchemaExtracted,
SchemaExtractor,
type SchemaExtractorConfig,
SchemaExtractorParserName,
} from '..';
describe('SchemaExtractor', () => {
it('should create instance', () => {
const schema: WorkflowJSON = {
nodes: [],
edges: [],
};
const schemaExtractor = new SchemaExtractor(schema);
expect(schemaExtractor).toBeInstanceOf(SchemaExtractor);
});
it('should create instance with empty or undefined schema', () => {
const schemaExtractorEmpty = new SchemaExtractor(
{} as unknown as WorkflowJSON,
);
expect(schemaExtractorEmpty).toBeInstanceOf(SchemaExtractor);
const schemaExtractorUndefined = new SchemaExtractor(
undefined as unknown as WorkflowJSON,
);
expect(schemaExtractorUndefined).toBeInstanceOf(SchemaExtractor);
});
it('should extract schema with config', () => {
const schema: WorkflowJSON = {
nodes: [
{
id: '1',
type: StandardNodeType.Api,
data: {
nodeMeta: {
title: 'nodeName1',
},
},
},
{
id: '2',
type: StandardNodeType.LLM,
data: {
nodeMeta: {
title: 'nodeName2',
},
inputs: {
llmParam: [
{
name: 'prompt',
input: {
type: 'string',
value: {
type: 'literal',
content: 'you should test {{here}}',
},
},
},
{
name: 'systemPrompt',
input: {
type: 'string',
value: {
type: 'literal',
content: 'this is systemPrompt',
},
},
},
],
},
prompt: 'you should test here',
},
},
],
edges: [],
};
const config: SchemaExtractorConfig = {
[StandardNodeType.Api]: [
{
name: 'title',
path: 'nodeMeta.title',
},
],
[StandardNodeType.LLM]: [
{
name: 'title',
path: 'nodeMeta.title',
},
{
name: 'llmParam',
path: 'inputs.llmParam',
parser: SchemaExtractorParserName.LLM_PARAM,
},
],
};
const schemaExtractor = new SchemaExtractor(schema);
const extractedSchema: SchemaExtracted[] = schemaExtractor.extract(config);
expect(extractedSchema).toStrictEqual([
{
nodeId: '1',
nodeType: StandardNodeType.Api,
properties: {
title: 'nodeName1',
},
},
{
nodeId: '2',
nodeType: StandardNodeType.LLM,
properties: {
title: 'nodeName2',
llmParam: {
prompt: 'you should test {{here}}',
systemPrompt: 'this is systemPrompt',
},
},
},
]);
});
it('should use parser in the config', () => {
const schema: WorkflowJSON = {
nodes: [
{
id: '1',
type: StandardNodeType.Api,
data: {
content: {
prefix: 'hello',
suffix: 'world',
},
},
},
],
edges: [],
};
const config: SchemaExtractorConfig = {
[StandardNodeType.Api]: [
{
name: 'content',
path: 'content',
parser: (content: { prefix: string; suffix: string }) =>
`${content.prefix},${content.suffix}!`,
},
],
};
const schemaExtractor = new SchemaExtractor(schema);
const extractedSchema: SchemaExtracted[] = schemaExtractor.extract(config);
expect(extractedSchema).toStrictEqual([
{
nodeId: '1',
nodeType: StandardNodeType.Api,
properties: {
content: 'hello,world!',
},
},
]);
});
it('should flat multi-layer schema', () => {
const schema: WorkflowJSON = {
nodes: [
{
id: '1',
type: StandardNodeType.Loop,
data: {
nodeMeta: {
title: 'nodeName1',
},
},
blocks: [
{
id: '1.1',
type: StandardNodeType.Api,
data: {
nodeMeta: {
title: 'nodeName1.1',
},
},
},
{
id: '1.2',
type: StandardNodeType.Api,
data: {
nodeMeta: {
title: 'nodeName1.2',
},
},
},
{
id: '1.3',
type: StandardNodeType.Api,
data: {
nodeMeta: {
title: 'nodeName1.3',
},
},
},
],
edges: [
{
sourceNodeID: '1.1',
sourcePortID: 'output',
targetNodeID: '1.2',
targetPortID: 'input',
},
{
sourceNodeID: '1.2',
sourcePortID: 'output',
targetNodeID: '1.3',
targetPortID: 'input',
},
{
sourceNodeID: '1.1',
sourcePortID: 'output',
targetNodeID: '1.3',
targetPortID: 'input',
},
],
},
{
id: '2',
type: StandardNodeType.Api,
data: {
nodeMeta: {
title: 'nodeName2',
},
},
},
],
edges: [
{
sourceNodeID: '1',
sourcePortID: 'output',
targetNodeID: '2',
targetPortID: 'input',
},
],
};
const config: SchemaExtractorConfig = {
[StandardNodeType.Loop]: [
{
name: 'title',
path: 'nodeMeta.title',
},
],
[StandardNodeType.Api]: [
{
name: 'title',
path: 'nodeMeta.title',
},
],
};
const schemaExtractor = new SchemaExtractor(schema);
const extractedSchema: SchemaExtracted[] = schemaExtractor.extract(config);
expect(extractedSchema).toStrictEqual([
{
nodeId: '1',
nodeType: StandardNodeType.Loop,
properties: {
title: 'nodeName1',
},
},
{
nodeId: '2',
nodeType: StandardNodeType.Api,
properties: {
title: 'nodeName2',
},
},
{
nodeId: '1.1',
nodeType: StandardNodeType.Api,
properties: {
title: 'nodeName1.1',
},
},
{
nodeId: '1.2',
nodeType: StandardNodeType.Api,
properties: {
title: 'nodeName1.2',
},
},
{
nodeId: '1.3',
nodeType: StandardNodeType.Api,
properties: {
title: 'nodeName1.3',
},
},
]);
});
});

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, it } from 'vitest';
import { SchemaExtractorParserName } from '../constant';
import { StandardNodeType } from '../../../types';
import { SchemaExtractor } from '..';
it('extract schema with dataset param parser', () => {
const schemaExtractor = new SchemaExtractor({
edges: [],
nodes: [
{
id: '111943',
type: '6',
data: {
inputs: {
datasetParam: [
{
name: 'datasetList',
input: {
type: 'list',
schema: { type: 'string' },
value: {
type: 'literal',
content: ['7330215302133268524', '7330215302133268524'],
},
},
},
{
name: 'topK',
input: {
type: 'integer',
value: { type: 'literal', content: 6 },
},
},
{
name: 'minScore',
input: {
type: 'number',
value: { type: 'literal', content: 0.5 },
},
},
{
name: 'strategy',
input: {
type: 'integer',
value: { type: 'literal', content: 1 },
},
},
],
},
},
},
],
});
const extractedSchema = schemaExtractor.extract({
// knowledge 知识库节点 6
[StandardNodeType.Dataset]: [
{
// 对应知识库名称
name: 'datasetParam',
path: 'inputs.datasetParam',
parser: SchemaExtractorParserName.DATASET_PARAM,
},
],
});
expect(extractedSchema).toStrictEqual([
{
nodeId: '111943',
nodeType: '6',
properties: {
datasetParam: {
datasetList: ['7330215302133268524', '7330215302133268524'],
},
},
},
]);
});

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, it } from 'vitest';
import { StandardNodeType } from '../../../types';
import { SchemaExtractor } from '..';
it('extract schema with default parser', () => {
const schemaExtractor = new SchemaExtractor({
edges: [],
nodes: [
{
id: '900001',
type: '2',
data: {
inputs: {
content: {
type: 'string',
value: {
type: 'literal',
content: '{{output_a}} and {{output_b}}',
},
},
},
},
},
],
});
const extractedSchema = schemaExtractor.extract({
// end 结束节点 2
[StandardNodeType.End]: [
{
// 对应输出指定内容
name: 'content',
path: 'inputs.content.value.content',
},
],
});
expect(extractedSchema).toStrictEqual([
{
nodeId: '900001',
nodeType: '2',
properties: { content: '{{output_a}} and {{output_b}}' },
},
]);
});

View File

@@ -0,0 +1,99 @@
/*
* 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 { expect, it } from 'vitest';
import { SchemaExtractorParserName } from '../constant';
import { StandardNodeType } from '../../../types';
import { SchemaExtractor } from '..';
it('extract schema with inputParameters parser', () => {
const schemaExtractor = new SchemaExtractor({
edges: [],
nodes: [
{
id: '154650',
type: '3',
data: {
inputs: {
inputParameters: [
{
name: 'input_a',
input: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '190950',
name: 'key0',
},
},
},
},
{
name: 'input_b',
input: {
type: 'list',
schema: { type: 'string' },
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '154650',
name: 'batch_a',
},
},
},
},
{
name: 'const_c',
input: {
type: 'string',
value: { type: 'literal', content: '1234' },
},
},
],
},
},
},
],
});
const extractedSchema = schemaExtractor.extract({
// llm 大模型节点 3
[StandardNodeType.LLM]: [
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
],
});
expect(extractedSchema).toStrictEqual([
{
nodeId: '154650',
nodeType: '3',
properties: {
inputs: [
{ name: 'input_a', value: 'key0', isImage: false },
{ name: 'input_b', value: 'batch_a', isImage: false },
{ name: 'const_c', value: '1234', isImage: false },
],
},
},
]);
});

View File

@@ -0,0 +1,130 @@
/*
* 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 { expect, it } from 'vitest';
import { SchemaExtractorParserName } from '../constant';
import { StandardNodeType } from '../../../types';
import { SchemaExtractor } from '..';
it('extract schema with intents param parser', () => {
const schemaExtractor = new SchemaExtractor({
edges: [],
nodes: [
{
id: '159306',
type: '22',
data: {
inputs: {
inputParameters: [
{
name: 'query',
input: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '100001',
name: 'BOT_USER_INPUT',
},
},
},
},
],
llmParam: {
modelType: 113,
generationDiversity: 'balance',
temperature: 0.5,
topP: 1,
frequencyPenalty: 0,
presencePenalty: 0,
maxTokens: 2048,
responseFormat: 2,
modelName: 'GPT-3.5',
prompt: {
type: 'string',
value: {
type: 'literal',
content: '{{query}}',
},
},
systemPrompt: {
type: 'string',
value: {
type: 'literal',
content: '你好, {{query}}',
},
},
enableChatHistory: false,
},
intents: [
{
name: '北京',
},
{
name: '上海',
},
{
name: '武汉',
},
{
name: '深圳',
},
{
name: '长沙2',
},
],
},
},
},
],
});
const extractedSchema = schemaExtractor.extract({
// end 结束节点 2
[StandardNodeType.Intent]: [
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// intents
name: 'intents',
path: 'inputs.intents',
parser: SchemaExtractorParserName.INTENTS,
},
{
// system prompt
name: 'systemPrompt',
path: 'inputs.llmParam.systemPrompt.value.content',
},
],
});
expect(extractedSchema).toStrictEqual([
{
nodeId: '159306',
nodeType: '22',
properties: {
inputs: [{ isImage: false, name: 'query', value: 'BOT_USER_INPUT' }],
intents: { intent: '1. 北京 2. 上海 3. 武汉 4. 深圳 5. 长沙2' },
systemPrompt: '你好, {{query}}',
},
},
]);
});

View File

@@ -0,0 +1,107 @@
/*
* 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 { expect, it } from 'vitest';
import { StandardNodeType } from '../../../types';
import { SchemaExtractor, SchemaExtractorParserName } from '..';
it('extract schema with json string parser', () => {
const schemaExtractor = new SchemaExtractor({
edges: [],
nodes: [
{
id: '176450',
type: '25',
data: {
inputs: {
Messages:
'{"visibility":{"visibility":"3","user_settings":[{"biz_role_id":"7398508237160677420","role":"host","nickname":"host","role_type":1,"description":""},{"biz_role_id":"7402058670241185836","role":"juese2","nickname":"","role_type":3,"description":""},{"biz_role_id":"7405794345170763820","role":"bot1","nickname":"majiang","role_type":2,"description":"麻将高手"}]},"order":"1","contentMode":"1","messages":[{"biz_role_id":"7398508237160677420","role":"host","nickname":"host","role_type":1,"generate_mode":0,"content":"这是一条示例消息,点击可修改"},{"biz_role_id":"7402058670241185836","role":"juese2","nickname":"","role_type":3,"content":"","generate_mode":1}]}',
Roles:
'[{"biz_role_id":"7398508237160677420","role":"host","nickname":"host","role_type":1,"generate_mode":0},{"biz_role_id":"7402058670241185836","role":"juese2","nickname":"","role_type":3,"generate_mode":1}]',
},
},
},
],
});
const extractedSchema = schemaExtractor.extract({
// end 结束节点 2
[StandardNodeType.SceneChat]: [
{
// 对应输出指定内容
name: 'messages',
path: 'inputs.Messages',
parser: SchemaExtractorParserName.JSON_STRING_PARSER,
},
],
});
expect(extractedSchema).toStrictEqual([
{
nodeId: '176450',
nodeType: '25',
properties: {
messages: {
visibility: {
visibility: '3',
user_settings: [
{
biz_role_id: '7398508237160677420',
role: 'host',
nickname: 'host',
role_type: 1,
description: '',
},
{
biz_role_id: '7402058670241185836',
role: 'juese2',
nickname: '',
role_type: 3,
description: '',
},
{
biz_role_id: '7405794345170763820',
role: 'bot1',
nickname: 'majiang',
role_type: 2,
description: '麻将高手',
},
],
},
order: '1',
contentMode: '1',
messages: [
{
biz_role_id: '7398508237160677420',
role: 'host',
nickname: 'host',
role_type: 1,
generate_mode: 0,
content: '这是一条示例消息,点击可修改',
},
{
biz_role_id: '7402058670241185836',
role: 'juese2',
nickname: '',
role_type: 3,
content: '',
generate_mode: 1,
},
],
},
},
},
]);
});

View File

@@ -0,0 +1,99 @@
/*
* 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 { expect, it } from 'vitest';
import { SchemaExtractorParserName } from '../constant';
import { StandardNodeType } from '../../../types';
import { SchemaExtractor } from '..';
it('extract schema with outputs parser', () => {
const schemaExtractor = new SchemaExtractor({
edges: [],
nodes: [
{
id: '190950',
type: '5',
data: {
outputs: [
{ type: 'string', name: 'key0', description: 'test desc' },
{ type: 'string', name: 'key1' },
{ type: 'list', name: 'key2', schema: { type: 'string' } },
{
type: 'object',
name: 'key3',
schema: [{ type: 'string', name: 'key31' }],
},
{
type: 'list',
name: 'key4',
schema: {
type: 'object',
schema: [
{ type: 'boolean', name: 'key41' },
{ type: 'integer', name: 'key42' },
{ type: 'float', name: 'key43' },
{ type: 'list', name: 'key44', schema: { type: 'string' } },
{
type: 'object',
name: 'key45',
schema: [{ type: 'string', name: 'key451' }],
},
],
},
},
],
},
},
],
});
const extractedSchema = schemaExtractor.extract({
// code 代码节点 5
[StandardNodeType.Code]: [
{
// 对应output name
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
});
expect(extractedSchema).toStrictEqual([
{
nodeId: '190950',
nodeType: '5',
properties: {
outputs: [
{ name: 'key0', description: 'test desc' },
{ name: 'key1' },
{ name: 'key2' },
{ name: 'key3', children: [{ name: 'key31' }] },
{
name: 'key4',
children: [
{ name: 'key41' },
{ name: 'key42' },
{ name: 'key43' },
{ name: 'key44' },
{ name: 'key45', children: [{ name: 'key451' }] },
],
},
],
},
},
]);
});

View File

@@ -0,0 +1,289 @@
/*
* 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 { expect, it } from 'vitest';
import { SchemaExtractorParserName } from '../constant';
import { StandardNodeType } from '../../../types';
import { SchemaExtractor } from '..';
it('extract schema with variableAssign parser', () => {
const schemaExtractor = new SchemaExtractor({
edges: [],
nodes: [
{
id: '184010',
type: '21',
data: {
inputs: {
inputParameters: [],
variableParameters: [],
},
outputs: [],
},
blocks: [
{
id: '149710',
type: '20',
data: {
inputs: {
inputParameters: [
{
left: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '184010',
name: 'var_str',
},
},
},
right: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '146923',
name: 'new_str',
},
},
},
},
{
left: {
type: 'float',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '184010',
name: 'var_num',
},
},
},
right: {
type: 'float',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '146923',
name: 'new_num',
},
},
},
},
{
left: {
type: 'boolean',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '184010',
name: 'var_bool',
},
},
},
right: {
type: 'boolean',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '146923',
name: 'new_bool',
},
},
},
},
],
},
},
},
],
edges: [],
},
],
});
const extractedSchema = schemaExtractor.extract({
[StandardNodeType.SetVariable]: [
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.VARIABLE_ASSIGN,
},
],
});
expect(extractedSchema).toStrictEqual([
{
nodeId: '149710',
nodeType: '20',
properties: {
inputs: [
{ name: 'var_str', value: 'new_str' },
{ name: 'var_num', value: 'new_num' },
{ name: 'var_bool', value: 'new_bool' },
],
},
},
]);
});
it('variableAssign parser with empty inputParameters', () => {
const schemaExtractor = new SchemaExtractor({
edges: [],
nodes: [
{
id: '149710',
type: '20',
data: {
inputs: {
inputParameters: undefined,
},
},
},
],
});
const extractedSchema = schemaExtractor.extract({
[StandardNodeType.SetVariable]: [
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.VARIABLE_ASSIGN,
},
],
});
expect(extractedSchema).toStrictEqual([
{
nodeId: '149710',
nodeType: '20',
properties: {
inputs: [],
},
},
]);
});
it('variableAssign parser with invalid schema', () => {
const schemaExtractor = new SchemaExtractor({
edges: [],
nodes: [
{
id: '149710',
type: '20',
data: {
inputs: {
inputParameters: [
{}, // INVALID
{
left: {
type: 'string',
value: {
type: 'ref',
content: undefined, // INVALID
},
},
right: {
type: 'string',
value: {
type: 'ref',
content: 'new_str', // INVALID
},
},
},
{
left: {
type: 'boolean',
value: {}, // INVALID
},
right: {
type: 'boolean',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '146923',
name: 'new_bool',
},
},
},
},
{
left: {
type: 'boolean',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '184010',
name: 'var_bool',
},
},
},
right: {
type: 'boolean',
value: {}, // INVALID
},
},
],
},
},
},
],
});
const extractedSchema = schemaExtractor.extract({
[StandardNodeType.SetVariable]: [
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.VARIABLE_ASSIGN,
},
],
});
expect(extractedSchema).toStrictEqual([
{
nodeId: '149710',
nodeType: '20',
properties: {
inputs: [
{
name: '',
value: '',
},
{
name: '',
value: 'new_str',
},
{
name: '',
value: 'new_bool',
},
{
name: 'var_bool',
value: '',
},
],
},
},
]);
});

View File

@@ -0,0 +1,32 @@
/*
* 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-deep-relative-import */
import { type SchemaExtractorConfig } from '../../type';
import { SchemaExtractorParserName } from '../../constant';
import { StandardNodeType } from '../../../../types';
export const imageflowExtractorConfig: SchemaExtractorConfig = {
// api 节点 4
[StandardNodeType.Api]: [
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
],
};

View File

@@ -0,0 +1,503 @@
/*
* 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 const imageflowSchemaJSON = {
nodes: [
{
id: '100001',
type: '1',
meta: { position: { x: 0, y: 0 } },
data: {
outputs: [
{ type: 'string', name: 'ss', required: true },
{ type: 'float', name: 'sss', required: true },
],
nodeMeta: {
title: '开始',
icon: 'icon-Start.png',
description: '工作流的起始节点,用于设定启动工作流需要的信息',
subTitle: '',
},
},
},
{
id: '900001',
type: '2',
meta: { position: { x: 305.67163461538473, y: 600.1374999999998 } },
data: {
nodeMeta: {
title: '结束',
icon: 'icon-End.png',
description: '工作流的最终节点,用于返回工作流运行后的结果信息',
subTitle: '',
},
inputs: {
terminatePlan: 'returnVariables',
inputParameters: [
{
name: 'output',
input: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '100001',
name: 'ss',
},
},
},
},
{
name: 'sss',
input: {
type: 'string',
value: { type: 'literal', content: 'ss' },
},
},
],
streamingOutput: false,
},
},
},
{
id: '164069',
type: '4',
meta: { position: { x: -455, y: 338.06874999999997 } },
data: {
nodeMeta: {
title: '文生图',
icon: 'icon_Text-to-Image-CN.png',
isFromImageflow: true,
description: '通过文字描述生成图片',
},
inputs: {
apiParam: [
{
name: 'apiID',
input: {
type: 'string',
value: { type: 'literal', content: '7352834806217981963' },
},
},
{
name: 'apiName',
input: {
type: 'string',
value: { type: 'literal', content: 'text2image' },
},
},
{
name: 'pluginID',
input: {
type: 'string',
value: { type: 'literal', content: '7352834694330794023' },
},
},
{
name: 'pluginName',
input: {
type: 'string',
value: { type: 'literal', content: '文生图' },
},
},
{
name: 'pluginVersion',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
{
name: 'tips',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
{
name: 'outDocLink',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
],
inputParameters: [
{
name: 'prompt',
input: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '100001',
name: 'ss',
},
},
},
},
{
name: 'ratio',
input: {
type: 'float',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '100001',
name: 'sss',
},
},
},
},
{
name: 'width',
input: {
type: 'float',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '100001',
name: 'sss',
},
},
},
},
{
name: 'height',
input: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '100001',
name: 'ss',
},
},
},
},
],
},
outputs: [
{ type: 'image', name: 'data', required: false },
{ type: 'string', name: 'msg', required: false },
],
},
},
{
id: '164578',
type: '4',
meta: { position: { x: -543, y: 937.0687499999999 } },
data: {
nodeMeta: {
title: '智能换脸',
icon: 'icon_AI-FaceSwap.png',
isFromImageflow: true,
description: '为图片替换参考图的人脸',
},
inputs: {
apiParam: [
{
name: 'apiID',
input: {
type: 'string',
value: { type: 'literal', content: '7352888732107915305' },
},
},
{
name: 'apiName',
input: {
type: 'string',
value: { type: 'literal', content: 'smartFaceChanging' },
},
},
{
name: 'pluginID',
input: {
type: 'string',
value: { type: 'literal', content: '7352887570142969875' },
},
},
{
name: 'pluginName',
input: {
type: 'string',
value: { type: 'literal', content: '智能换脸' },
},
},
{
name: 'pluginVersion',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
{
name: 'tips',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
{
name: 'outDocLink',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
],
inputParameters: [
{
name: 'reference_picture_url',
input: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '100001',
name: 'ss',
},
},
},
},
{
name: 'skin',
input: {
type: 'image',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '164069',
name: 'data',
},
},
},
},
{
name: 'template_picture_url',
input: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '164069',
name: 'msg',
},
},
},
},
],
},
outputs: [{ type: 'image', name: 'data', required: false }],
},
},
{
id: '146804',
type: '4',
meta: { position: { x: -17, y: 1182.06875 } },
data: {
nodeMeta: {
title: '提示词优化',
icon: 'icon_PromptOptimization.png',
isFromImageflow: true,
description: '智能优化图像提示词',
},
inputs: {
apiParam: [
{
name: 'apiID',
input: {
type: 'string',
value: { type: 'literal', content: '7360989981134864399' },
},
},
{
name: 'apiName',
input: {
type: 'string',
value: { type: 'literal', content: 'sd_better_prompt' },
},
},
{
name: 'pluginID',
input: {
type: 'string',
value: { type: 'literal', content: '7360989829062230050' },
},
},
{
name: 'pluginName',
input: {
type: 'string',
value: { type: 'literal', content: '提示词优化' },
},
},
{
name: 'pluginVersion',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
{
name: 'tips',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
{
name: 'outDocLink',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
],
inputParameters: [
{
name: 'prompt',
input: {
type: 'image',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '140741',
name: 'data',
},
},
},
},
],
},
outputs: [{ type: 'string', name: 'data', required: false }],
},
},
{
id: '140741',
type: '4',
meta: { position: { x: -379.44467504743835, y: 1532.3798149905122 } },
data: {
nodeMeta: {
title: '亮度',
icon: 'icon_Brightness.png',
isFromImageflow: true,
description: '改变图片亮度',
},
inputs: {
apiParam: [
{
name: 'apiID',
input: {
type: 'string',
value: { type: 'literal', content: '7355822909170073600' },
},
},
{
name: 'apiName',
input: {
type: 'string',
value: { type: 'literal', content: 'image_light' },
},
},
{
name: 'pluginID',
input: {
type: 'string',
value: { type: 'literal', content: '7355822909170057216' },
},
},
{
name: 'pluginName',
input: {
type: 'string',
value: { type: 'literal', content: '亮度' },
},
},
{
name: 'pluginVersion',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
{
name: 'tips',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
{
name: 'outDocLink',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
],
inputParameters: [
{
name: 'bright',
input: {
type: 'float',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '100001',
name: 'sss',
},
},
},
},
{
name: 'origin_url',
input: {
type: 'image',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '164069',
name: 'data',
},
},
},
},
],
},
outputs: [{ type: 'image', name: 'data', required: false }],
},
},
],
edges: [
{ sourceNodeID: '100001', targetNodeID: '164069' },
{ sourceNodeID: '164069', targetNodeID: '164578' },
{ sourceNodeID: '164578', targetNodeID: '140741' },
{ sourceNodeID: '140741', targetNodeID: '146804' },
{ sourceNodeID: '146804', targetNodeID: '900001' },
],
};

View File

@@ -0,0 +1,442 @@
/*
* 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 SchemaExtractorConfig } from '../../type';
import { SchemaExtractorParserName } from '../../constant';
import { StandardNodeType } from '../../../../types';
export const workflowExtractorConfig: SchemaExtractorConfig = {
// Start 开始节点 1
[StandardNodeType.Start]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应 input name / description
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
// End 结束节点 2
[StandardNodeType.End]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应输出指定内容
name: 'content',
path: 'inputs.content.value.content',
},
],
// LLM 大模型节点 3
[StandardNodeType.LLM]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应batch value / batch description
name: 'batch',
path: 'inputs.batch.inputLists',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应提示词
name: 'llmParam',
path: 'inputs.llmParam',
parser: SchemaExtractorParserName.LLM_PARAM,
},
{
// 对应output name
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
// Plugin 节点 4
[StandardNodeType.Api]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应batch value / batch description
name: 'batch',
path: 'inputs.batch.inputLists',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应input value / input description
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应output name
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
// Code 代码节点 5
[StandardNodeType.Code]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input value / input description
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应code内容
name: 'code',
path: 'inputs.code',
},
{
// 对应output name
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
// Knowledge 知识库节点 6
[StandardNodeType.Dataset]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应知识库名称
name: 'datasetParam',
path: 'inputs.datasetParam',
parser: SchemaExtractorParserName.DATASET_PARAM,
},
],
// If 判断节点 8
[StandardNodeType.If]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'branches',
path: 'inputs.branches',
parser: SchemaExtractorParserName.DEFAULT,
},
],
// Sub Workflow 工作流节点 9
[StandardNodeType.SubWorkflow]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应batch value / batch description
name: 'batch',
path: 'inputs.batch.inputLists',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应input value / input description
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应output name
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
// Variable 变量节点 11
[StandardNodeType.Variable]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应output name
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
// Database 数据库节点 12
[StandardNodeType.Database]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// sql
name: 'sql',
path: 'inputs.sql',
},
{
// 对应output name
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
// Message 消息节点 13
[StandardNodeType.Output]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// content name
name: 'content',
path: 'inputs.content.value.content',
},
],
// Sub Imageflow 图像流节点 14
[StandardNodeType.Imageflow]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应batch value / batch description
name: 'batch',
path: 'inputs.batch.inputLists',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应input value / input description
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应output name
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
// Text 文本处理节点 15
[StandardNodeType.Text]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 拼接结果,以及拼接字符串
name: 'concatResult',
path: 'inputs.concatParams',
parser: SchemaExtractorParserName.CONCAT_RESULT,
},
{
// 自定义数组拼接符号
name: 'arrayConcatChar',
path: 'inputs.concatParams',
parser: SchemaExtractorParserName.CUSTOM_ARRAY_CONCAT_CHAR,
},
{
// 自定义分隔符
name: 'splitChar',
path: 'inputs.splitParams',
parser: SchemaExtractorParserName.CUSTOM_SPLIT_CHAR,
},
],
// Question 问题节点 18
[StandardNodeType.Question]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// question 问题
name: 'question',
path: 'inputs.question',
},
{
// answer_type 回答类型 option|text
name: 'answerType',
path: 'inputs.answer_type',
},
{
// options
name: 'options',
path: 'inputs.options',
},
{
// 对应output name
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
// Break 终止循环节点 19
[StandardNodeType.Break]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
],
// Set Variable 设置变量节点 20
[StandardNodeType.SetVariable]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.VARIABLE_ASSIGN,
},
],
// Loop 循环节点 21
[StandardNodeType.Loop]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应variable name
name: 'variables',
path: 'inputs.variableParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应output name
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
// Intent 意图识别节点 22
[StandardNodeType.Intent]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// intents
name: 'intents',
path: 'inputs.intents',
parser: SchemaExtractorParserName.INTENTS,
},
{
// system prompt
name: 'systemPrompt',
path: 'inputs.llmParam.systemPrompt.value.content',
},
],
// Knowledge Write 知识库写入节点 27
[StandardNodeType.DatasetWrite]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应知识库名称
name: 'datasetParam',
path: 'inputs.datasetParam',
parser: SchemaExtractorParserName.DATASET_PARAM,
},
],
};

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, it } from 'vitest';
import { SchemaExtractor } from '..';
import { imageflowSchemaJSON } from './resource/imageflow-schema';
import { imageflowExtractorConfig } from './resource/imageflow-config';
it('extract imageflow schema', () => {
const schemaExtractor = new SchemaExtractor(imageflowSchemaJSON);
const extractedImageflowSchema = schemaExtractor.extract(
imageflowExtractorConfig,
);
expect(extractedImageflowSchema).toStrictEqual([
{
nodeId: '164069',
nodeType: '4',
properties: {
inputs: [
{ name: 'prompt', value: 'ss', isImage: false },
{ name: 'ratio', value: 'sss', isImage: false },
{ name: 'width', value: 'sss', isImage: false },
{ name: 'height', value: 'ss', isImage: false },
],
},
},
{
nodeId: '164578',
nodeType: '4',
properties: {
inputs: [
{ name: 'reference_picture_url', value: 'ss', isImage: false },
{ name: 'skin', value: 'data', isImage: false },
{ name: 'template_picture_url', value: 'msg', isImage: false },
],
},
},
{
nodeId: '146804',
nodeType: '4',
properties: {
inputs: [{ name: 'prompt', value: 'data', isImage: false }],
},
},
{
nodeId: '140741',
nodeType: '4',
properties: {
inputs: [
{ name: 'bright', value: 'sss', isImage: false },
{ name: 'origin_url', value: 'data', isImage: false },
],
},
},
]);
});

View File

@@ -0,0 +1,489 @@
/*
* 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 { expect, it } from 'vitest';
import { SchemaExtractor } from '..';
import { workflowSchemaJSON } from './resource/workflow-schema';
import { workflowExtractorConfig } from './resource/workflow-config';
it('extract workflow schema', () => {
const schemaExtractor = new SchemaExtractor(workflowSchemaJSON);
const extractedWorkflowSchema = schemaExtractor.extract(
workflowExtractorConfig,
);
expect(extractedWorkflowSchema).toStrictEqual([
{
nodeId: '100001',
nodeType: '1',
properties: {
title: '开始',
outputs: [
{ name: 'start_input_a', description: 'test desc' },
{ name: 'start_input_b' },
],
},
},
{
nodeId: '900001',
nodeType: '2',
properties: {
title: '结束',
inputs: [
{ name: 'output_a', value: 'outputList.output_b', isImage: false },
],
content: '{{output_a}} and {{output_b}}',
},
},
{
nodeId: '154650',
nodeType: '3',
properties: {
title: '大模型',
batch: [
{ name: 'batch_a', value: 'key2', isImage: false },
{ name: 'batch_b', value: 'key4', isImage: false },
],
inputs: [
{ name: 'input_a', value: 'key0', isImage: false },
{ name: 'input_b', value: 'batch_a', isImage: false },
{ name: 'const_c', value: '1234', isImage: false },
],
llmParam: {
prompt: '{{input_a}} and {{input_b}}',
systemPrompt: 'this is systemPrompt',
},
outputs: [
{
name: 'outputList',
children: [
{ name: 'output_a', description: 'desc output_a' },
{ name: 'output_b' },
],
},
],
},
},
{
nodeId: '190950',
nodeType: '5',
properties: {
title: '代码',
inputs: [
{ name: 'code_input_a', value: 'start_input_a', isImage: false },
{ name: 'code_const_b', value: 'test const', isImage: false },
],
code: 'async function main({ params }: Args): Promise<Output> {\n return params; \n}',
outputs: [
{ name: 'key0' },
{ name: 'key1' },
{ name: 'key2' },
{ name: 'key3', children: [{ name: 'key31' }] },
{
name: 'key4',
children: [
{ name: 'key41' },
{ name: 'key42' },
{ name: 'key43' },
{ name: 'key44' },
{ name: 'key45', children: [{ name: 'key451' }] },
],
},
],
},
},
{
nodeId: '111943',
nodeType: '6',
properties: {
title: '知识库',
inputs: [{ name: 'Query', value: 'start_input_b', isImage: false }],
datasetParam: {
datasetList: ['7330215302133268524', '7330215302133268524'],
},
},
},
{
nodeId: '183818',
nodeType: '8',
properties: {
title: '选择器',
branches: [
{
condition: {
logic: 2,
conditions: [
{
operator: 1,
left: {
input: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '100001',
name: 'start_input_a',
},
},
},
},
right: {
input: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '190950',
name: 'key0',
},
},
},
},
},
{
operator: 2,
left: {
input: {
type: 'integer',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '100001',
name: 'start_input_b',
},
},
},
},
right: {
input: {
type: 'integer',
value: { type: 'literal', content: '2' },
},
},
},
{
operator: 1,
left: {
input: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '190950',
name: 'key1',
},
},
},
},
right: {
input: {
type: 'string',
value: { type: 'literal', content: 'constant_a' },
},
},
},
],
},
},
],
},
},
{
nodeId: '163608',
nodeType: '11',
properties: {
title: '变量设置',
inputs: [
{ name: 'variable_a', value: 'start_input_a', isImage: false },
],
outputs: [{ name: 'isSuccess' }],
},
},
{
nodeId: '150706',
nodeType: '11',
properties: {
title: '变量获取',
inputs: [{ name: 'Key', value: 'workflow_variable_a', isImage: false }],
outputs: [{ name: 'bot_variable_b' }],
},
},
{
nodeId: '193063',
nodeType: '4',
properties: {
title: 'playOrRecommendMusic',
batch: [
{ name: 'item1', value: 'key2', isImage: false },
{ name: 'item2', value: 'key4.key44', isImage: false },
],
inputs: [
{ name: 'artist', value: 'item1', isImage: false },
{ name: 'user_question', value: 'key0', isImage: false },
{ name: 'song_name', value: 'start_input_b', isImage: false },
{ name: 'description', value: 'start_input_a', isImage: false },
],
outputs: [
{
name: 'outputList',
children: [
{ name: 'response_for_model', description: 'response for model' },
{ name: 'response_type', description: 'response type' },
{ name: 'template_id', description: 'use card template 3' },
{
name: 'type_for_model',
description: 'how to treat response, 2 means directly return',
},
{
name: 'music_list',
description: 'music data list, usually single item',
children: [
{ name: 'song_name', description: 'name of the music' },
{
name: 'start',
description: 'the beginning of the material',
},
{
name: 'source_from',
description: 'id of the source, 1 is soda',
},
{ name: 'vid', description: 'vid of the video model' },
{ name: 'video_auto_play', description: 'if auto play' },
{
name: 'source_id',
description: 'the unique id from source',
},
{
name: 'album_image',
description: 'album image of the music',
},
{
name: 'ref_score',
description: 'confidence score that match user intention',
},
{
name: 'video_model',
description: 'video model to get material of the music',
},
{
name: 'artist_name',
description: 'artist name of the music',
},
{
name: 'duration',
description: 'play duration of the material',
},
{
name: 'source_app_icon',
description: 'icon of the music source',
},
{
name: 'source_app_name',
description: 'name of the music source',
},
],
},
{
name: 'recommend_reason',
description: 'if music is recommended, give reason',
},
{
name: 'rel_score',
description: 'confidence score of found music',
},
],
},
],
},
},
{
nodeId: '139426',
nodeType: '9',
properties: {
title: 'test_ref',
batch: [{ name: 'item1', value: 'key2', isImage: false }],
inputs: [{ name: 'input_a', value: 'item1', isImage: false }],
outputs: [{ name: 'outputList', children: [{ name: 'output_a' }] }],
},
},
{
nodeId: '122146',
nodeType: '11',
properties: {
title: '变量',
inputs: [{ name: 'arr_str', value: 'arr_str', isImage: false }],
outputs: [{ name: 'isSuccess' }],
},
},
{
nodeId: '124687',
nodeType: '11',
properties: {
title: '变量_1',
inputs: [{ name: 'Key', value: 'sss', isImage: false }],
outputs: [{ name: 'dddd' }],
},
},
{
nodeId: '184010',
nodeType: '21',
properties: {
title: '循环',
inputs: [{ name: 'arr_str', value: 'arr_str', isImage: false }],
variables: [
{ name: 'var_str', value: 'str', isImage: false },
{ name: 'var_num', value: 'num', isImage: false },
{ name: 'var_bool', value: 'bool', isImage: false },
],
outputs: [{ name: 'output_list' }],
},
},
{
nodeId: '149710',
nodeType: '20',
properties: {
title: '循环变量',
inputs: [
{ name: 'var_str', value: 'new_str' },
{ name: 'var_num', value: 'new_num' },
{ name: 'var_bool', value: 'new_bool' },
],
},
},
{
nodeId: '185397',
nodeType: '13',
properties: {
title: '消息',
inputs: [
{ name: 'str', value: 'var_str', isImage: false },
{ name: 'num', value: 'var_num', isImage: false },
{ name: 'bool', value: 'var_bool', isImage: false },
],
content: 'str: {{str}}\nnum: {{num}}\nbool: {{bool}}',
},
},
{
nodeId: '146923',
nodeType: '5',
properties: {
title: '代码',
inputs: [
{ name: 'var_str', value: 'var_str', isImage: false },
{ name: 'var_num', value: 'var_num', isImage: false },
{ name: 'var_bool', value: 'var_bool', isImage: false },
],
code: 'async function main({ params }: Args): Promise<Output> {\n return {\n "new_str": params.var_str + \'✅\',\n "new_num": params.var_num + 1,\n "new_bool": !params.var_bool,\n };\n}',
outputs: [
{ name: 'new_str' },
{ name: 'new_num' },
{ name: 'new_bool' },
],
},
},
{
nodeId: '123896',
nodeType: '15',
properties: {
title: '文本处理',
concatResult: 'str: {{String1}}\nnum: {{String2}}\nbool: {{String3}}',
arrayConcatChar: '',
splitChar: '',
},
},
{
nodeId: '179778',
nodeType: '8',
properties: {
title: '选择器',
branches: [
{
condition: {
logic: 2,
conditions: [
{
operator: 1,
left: {
input: {
type: 'boolean',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '184010',
name: 'var_bool',
},
},
},
},
right: {
input: {
type: 'boolean',
value: { type: 'literal', content: 'true' },
},
},
},
],
},
},
],
},
},
{
nodeId: '177333',
nodeType: '8',
properties: {
title: '选择器_1',
branches: [
{
condition: {
logic: 2,
conditions: [
{
operator: 13,
left: {
input: {
type: 'float',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '184010',
name: 'var_num',
},
},
},
},
right: {
input: {
type: 'integer',
value: { type: 'literal', content: '5' },
},
},
},
],
},
},
],
},
},
{ nodeId: '194199', nodeType: '19', properties: { title: '终止循环' } },
]);
});

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export enum SchemaExtractorParserName {
DEFAULT = 'default',
INPUT_PARAMETERS = 'inputParameters',
OUTPUTS = 'outputs',
DATASET_PARAM = 'datasetParam',
LLM_PARAM = 'llmParam',
INTENTS = 'intents',
CONCAT_RESULT = 'concatResult',
CUSTOM_ARRAY_CONCAT_CHAR = 'customArrayConcatChar',
CUSTOM_SPLIT_CHAR = 'customSplitChar',
REF_INPUT_PARAMETER = 'refInputParameter',
VARIABLE_ASSIGN = 'variableAssign',
JSON_STRING_PARSER = 'jsonStringParser',
IMAGE_REFERENCE_PARSER = 'imageReferenceParser',
EXPRESSION_PARSER = 'expressionParser',
VARIABLE_MERGE_GROUPS_PARSER = 'variableMergeGroupsParser',
DB_FIELDS_PARSER = 'dbFieldsParser',
DB_CONDITIONS_PARSER = 'dbConditionsParser',
}
export const SYSTEM_DELIMITERS = [
'\n',
'\t',
'.',
'。',
',',
'',
';',
'',
' ',
];

View File

@@ -0,0 +1,153 @@
/*
* 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 { get, cloneDeep } from 'lodash-es';
import type { WorkflowEdgeJSON } from '@flowgram-adapter/free-layout-editor';
import type {
StandardNodeType,
WorkflowJSON,
WorkflowNodeJSON,
} from '../../types';
import type {
SchemaExtracted,
SchemaExtractorConfig,
SchemaExtractorNodeConfig,
SchemaExtractorParser,
} from './type';
import { schemaExtractorParsers } from './parsers';
import { SchemaExtractorParserName } from './constant';
export { SchemaExtractorParserName } from './constant';
export type {
SchemaExtractorConfig,
SchemaExtracted,
SchemaExtractorNodeConfig,
ParsedVariableMergeGroups,
} from './type';
export class SchemaExtractor {
private readonly schema: WorkflowJSON;
private readonly parser: Record<
SchemaExtractorParserName,
SchemaExtractorParser
>;
constructor(schema: WorkflowJSON) {
this.schema = this.flatSchema(cloneDeep(schema));
this.parser = schemaExtractorParsers;
}
public extract(config: SchemaExtractorConfig): SchemaExtracted[] {
this.bindParser(config);
// 1. 遍历schema中node数组对每个node做处理
return this.schema.nodes
.map((node: WorkflowNodeJSON): SchemaExtracted | null => {
// 2. 获取节点对应的配置
const nodeConfigs: SchemaExtractorNodeConfig[] = config[node.type];
if (!nodeConfigs) {
return null;
}
return {
nodeId: node.id,
nodeType: node.type as StandardNodeType,
properties: this.extractNode(nodeConfigs, node.data),
};
})
.filter(Boolean) as SchemaExtracted[];
}
private extractNode(
nodeConfigs: SchemaExtractorNodeConfig[],
nodeData: Record<string, unknown>,
): Record<string, unknown> {
return nodeConfigs.reduce(
(
extractedConfig: Record<string, unknown>,
nodeConfig: SchemaExtractorNodeConfig,
): Record<string, unknown> => {
// 3. 根据节点配置路径获取属性值
const rawData: unknown = this.extractProperties(
nodeData,
nodeConfig.path,
);
if (nodeConfig.parser && typeof nodeConfig.parser === 'function') {
// 4. 使用解析器对属性值进行转换
extractedConfig[nodeConfig.name] = nodeConfig.parser(rawData);
}
return extractedConfig;
},
{},
);
}
private extractProperties(properties: Record<string, unknown>, path: string) {
return get(properties, path);
}
private bindParser(config: SchemaExtractorConfig) {
Object.entries(config).forEach(([nodeType, nodeConfigs]) => {
nodeConfigs.forEach(nodeConfig => {
if (!nodeConfig.parser) {
nodeConfig.parser = SchemaExtractorParserName.DEFAULT;
}
if (typeof nodeConfig.parser === 'string') {
nodeConfig.parser = this.parser[nodeConfig.parser];
}
});
});
}
private getEdgeID(edge: WorkflowEdgeJSON): string {
const from = edge.sourceNodeID;
const to = edge.targetNodeID;
const fromPort = edge.sourcePortID;
const toPort = edge.targetPortID;
return `${from}_${fromPort || ''}-${to || ''}_${toPort || ''}`;
}
private flatSchema(
json: WorkflowJSON = { nodes: [], edges: [] },
): WorkflowJSON {
const rootNodes = json.nodes ?? [];
const rootEdges = json.edges ?? [];
const flattenNodeJSONs: WorkflowNodeJSON[] = [...rootNodes];
const flattenEdgeJSONs: WorkflowEdgeJSON[] = [...rootEdges];
// 如需支持多层结构,以下部分改为递归
rootNodes.forEach(nodeJSON => {
const { blocks, edges } = nodeJSON;
if (blocks) {
flattenNodeJSONs.push(...blocks);
const blockIDs: string[] = [];
blocks.forEach(block => {
blockIDs.push(block.id);
});
delete nodeJSON.blocks;
}
if (edges) {
flattenEdgeJSONs.push(...edges);
const edgeIDs: string[] = [];
edges.forEach(edge => {
const edgeID = this.getEdgeID(edge);
edgeIDs.push(edgeID);
});
delete nodeJSON.edges;
}
});
const flattenSchema: WorkflowJSON = {
nodes: flattenNodeJSONs,
edges: flattenEdgeJSONs,
};
return flattenSchema;
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { get } from 'lodash-es';
import { type SchemaExtractorConcatResultParser } from '../type';
export const concatResultParser: SchemaExtractorConcatResultParser =
concatParams => {
const concatResult = (concatParams || []).find(
v => v.name === 'concatResult',
);
return get(concatResult, 'input.value.content', '') as string;
};

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { get } from 'lodash-es';
import { type SchemaExtractorArrayConcatCharParser } from '../type';
import { SYSTEM_DELIMITERS } from '../constant';
export const arrayConcatCharParser: SchemaExtractorArrayConcatCharParser =
concatParams => {
const allArrayItemConcatChars = (concatParams || []).find(
v => v.name === 'allArrayItemConcatChars',
);
let customConcatChars = '';
if (allArrayItemConcatChars) {
const list = get(allArrayItemConcatChars, 'input.value.content', []) as {
value: string;
}[];
const customItems = list.filter(
v => !SYSTEM_DELIMITERS.includes(v.value),
);
customConcatChars = customItems.map(v => v.value).join(', ');
}
return customConcatChars;
};

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { get } from 'lodash-es';
import { type SchemaExtractorSplitCharParser } from '../type';
import { SYSTEM_DELIMITERS } from '../constant';
export const splitCharParser: SchemaExtractorSplitCharParser = splitParams => {
const allDelimiters = (splitParams || []).find(
v => v.name === 'allDelimiters',
);
let customDelimiters = '';
if (allDelimiters) {
const list = get(allDelimiters, 'input.value.content', []) as {
value: string;
}[];
const customItems = list.filter(v => !SYSTEM_DELIMITERS.includes(v.value));
customDelimiters = customItems.map(v => v.value).join(', ');
}
return customDelimiters;
};

View File

@@ -0,0 +1,35 @@
/*
* 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 { get } from 'lodash-es';
import { type SchemaExtractorDatasetParamParser } from '../type';
export const datasetParamParser: SchemaExtractorDatasetParamParser =
datasetParam => {
const datasetListItem = datasetParam.find(
param => param.name === 'datasetList',
);
const datasetList = get(datasetListItem, 'input.value.content');
if (!datasetList || !Array.isArray(datasetList)) {
return {
datasetList: [],
};
}
return {
datasetList,
};
};

View File

@@ -0,0 +1,23 @@
/*
* 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 SchemaExtractorDbConditionsParser } from '../type';
import { inputParametersParser } from './input-parameters';
export const dbConditionsParser: SchemaExtractorDbConditionsParser =
conditionList =>
conditionList
?.flatMap(conditions => inputParametersParser(conditions || []))
?.filter(Boolean) as ReturnType<SchemaExtractorDbConditionsParser>;

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { parseExpression } from '../utils';
import { type SchemaExtractorDbFieldsParser } from '../type';
export const dbFieldsParser: SchemaExtractorDbFieldsParser = dbFields =>
dbFields
?.map(([fieldID, fieldValue]) => {
const parsedFieldID = parseExpression(fieldID?.input);
const parsedFieldValue = parseExpression(fieldValue?.input);
if (!parsedFieldValue) {
return null;
}
return {
name: parsedFieldID?.value,
value: parsedFieldValue?.value,
isImage: parsedFieldValue?.isImage,
};
})
?.filter(Boolean) as ReturnType<SchemaExtractorDbFieldsParser>;

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { parseExpression } from '../utils';
import { type SchemaExtractorExpressionParser } from '../type';
import type { ValueExpressionDTO } from '../../../types';
export const expressionParser: SchemaExtractorExpressionParser = expression => {
const expressions = ([] as ValueExpressionDTO[])
.concat(expression)
.filter(Boolean);
return expressions
.map(parseExpression)
.filter(Boolean) as ReturnType<SchemaExtractorExpressionParser>;
};

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { SchemaExtractorImageReferenceParser } from '../type';
import { inputParametersParser } from './input-parameters';
export const imageReferenceParser: SchemaExtractorImageReferenceParser =
references => {
if (!Array.isArray(references)) {
return [];
}
return inputParametersParser(
references.map(ref => ({
name: '-',
input: ref.url,
})),
);
};

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type SchemaExtractorParser } from '../type';
import { SchemaExtractorParserName } from '../constant';
import { variableMergeGroupsParser } from './variable-merge-groups-parser';
import { variableAssignParser } from './variable-assign';
import { refInputParametersParser } from './ref-input-parameters';
import { outputsParser } from './output';
import { llmParamParser } from './llm-param';
import { jsonStringParser } from './json-string-parser';
import { intentsParser } from './intents';
import { inputParametersParser } from './input-parameters';
import { imageReferenceParser } from './image-reference';
import { expressionParser } from './expression-parser';
import { dbFieldsParser } from './db-fields';
import { dbConditionsParser } from './db-conditions';
import { datasetParamParser } from './dataset-param';
import { splitCharParser } from './custom-split-char';
import { arrayConcatCharParser } from './custom-array-concat-char';
import { concatResultParser } from './concat-result';
export const schemaExtractorParsers: Record<
SchemaExtractorParserName,
SchemaExtractorParser
> = {
[SchemaExtractorParserName.DEFAULT]: t => t,
[SchemaExtractorParserName.INPUT_PARAMETERS]: inputParametersParser,
[SchemaExtractorParserName.OUTPUTS]: outputsParser,
[SchemaExtractorParserName.DATASET_PARAM]: datasetParamParser,
[SchemaExtractorParserName.LLM_PARAM]: llmParamParser,
[SchemaExtractorParserName.INTENTS]: intentsParser,
[SchemaExtractorParserName.CONCAT_RESULT]: concatResultParser,
[SchemaExtractorParserName.CUSTOM_ARRAY_CONCAT_CHAR]: arrayConcatCharParser,
[SchemaExtractorParserName.CUSTOM_SPLIT_CHAR]: splitCharParser,
[SchemaExtractorParserName.REF_INPUT_PARAMETER]: refInputParametersParser,
[SchemaExtractorParserName.VARIABLE_ASSIGN]: variableAssignParser,
[SchemaExtractorParserName.JSON_STRING_PARSER]: jsonStringParser,
[SchemaExtractorParserName.IMAGE_REFERENCE_PARSER]: imageReferenceParser,
[SchemaExtractorParserName.EXPRESSION_PARSER]: expressionParser,
[SchemaExtractorParserName.VARIABLE_MERGE_GROUPS_PARSER]:
variableMergeGroupsParser,
[SchemaExtractorParserName.DB_FIELDS_PARSER]: dbFieldsParser,
[SchemaExtractorParserName.DB_CONDITIONS_PARSER]: dbConditionsParser,
};

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { get } from 'lodash-es';
import { parseExpression } from '../utils';
import { type SchemaExtractorInputParametersParser } from '../type';
import type { InputValueDTO } from '../../../types';
export const inputParametersParser: SchemaExtractorInputParametersParser =
inputParameters => {
let parameters: InputValueDTO[] = [];
if (!Array.isArray(inputParameters)) {
if (typeof inputParameters === 'object') {
Object.keys(inputParameters || {}).forEach(key => {
parameters.push({
name: key,
input: inputParameters[key],
});
});
}
} else {
parameters = inputParameters;
}
return parameters
.map(inputParameter => {
const expression = get(inputParameter, 'input');
const parsedExpression = parseExpression(expression);
if (!parsedExpression) {
return null;
}
return {
name: inputParameter.name,
...parsedExpression,
};
})
.filter(Boolean) as ReturnType<SchemaExtractorInputParametersParser>;
};

View File

@@ -0,0 +1,21 @@
/*
* 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 SchemaExtractorIntentsParamParser } from '../type';
export const intentsParser: SchemaExtractorIntentsParamParser = intents => ({
intent: intents.map((item, idx) => `${idx + 1}. ${item.name}`).join(' '),
});

View File

@@ -0,0 +1,21 @@
/*
* 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 SchemaExtractorJSONStringParser } from '../type';
export const jsonStringParser: SchemaExtractorJSONStringParser = (
jsonString: string,
) => JSON.parse(jsonString || '{}') as object | object[] | undefined;

Some files were not shown because too many files have changed in this diff Show More