feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
18
frontend/packages/workflow/nodes/src/__mocks__/speeck-sdk.ts
Normal file
18
frontend/packages/workflow/nodes/src/__mocks__/speeck-sdk.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const SpeechSDK = {};
|
||||
export default SpeechSDK;
|
||||
23
frontend/packages/workflow/nodes/src/__tests__/index.test.ts
Normal file
23
frontend/packages/workflow/nodes/src/__tests__/index.test.ts
Normal 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 { it, expect, describe } from 'vitest';
|
||||
|
||||
describe('Hello World', () => {
|
||||
it('test', () => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest';
|
||||
import { noop } from 'lodash-es';
|
||||
|
||||
vi.mock('@byted-sami/speech-sdk', () => ({
|
||||
ServerEventName: '',
|
||||
ClientEventName: '',
|
||||
EventType: '',
|
||||
TTSPlayer: vi.fn(() => ({
|
||||
playerAudioClient: {
|
||||
onAudioData(_: unknown) {
|
||||
noop();
|
||||
},
|
||||
},
|
||||
})),
|
||||
PlayerAudioClient: vi.fn(() => {
|
||||
noop();
|
||||
}),
|
||||
AssistantSpeechClient: vi.fn(() => {
|
||||
noop();
|
||||
}),
|
||||
RecorderAudioClient: vi.fn(() => {
|
||||
noop();
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.stubGlobal('AudioWorkletNode', vi.fn());
|
||||
vi.stubGlobal('AudioContext', vi.fn());
|
||||
vi.stubGlobal('RecorderAudioClient', vi.fn());
|
||||
vi.stubGlobal('SAMI_WS_ORIGIN', vi.fn());
|
||||
vi.stubGlobal('SAMI_CHAT_WS_URL', vi.fn());
|
||||
vi.stubGlobal('SAMI_APP_KEY', vi.fn());
|
||||
vi.stubGlobal('IS_DEV_MODE', false);
|
||||
vi.stubGlobal('REGION', 'cn');
|
||||
vi.stubGlobal('IS_RELEASE_VERSION', false);
|
||||
vi.stubGlobal('IS_OVERSEA', true);
|
||||
vi.stubGlobal('IS_CN_REGION', true);
|
||||
116
frontend/packages/workflow/nodes/src/constants.ts
Normal file
116
frontend/packages/workflow/nodes/src/constants.ts
Normal 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 { I18n } from '@coze-arch/i18n';
|
||||
|
||||
export enum LLMModel {
|
||||
GPT_4O = 124,
|
||||
YunqueModel = 1706077826,
|
||||
}
|
||||
|
||||
/** 海外默认选择 GPT_4O,国内默认选择豆包 Function Call 模型,专业版则默认选择第一个 */
|
||||
export const DEFAULT_MODEL_TYPE = IS_OVERSEA
|
||||
? LLMModel.GPT_4O
|
||||
: LLMModel.YunqueModel;
|
||||
|
||||
export const VARIABLE_NAME_REGEX =
|
||||
/^(?!.*\b(true|false|and|AND|or|OR|not|NOT|null|nil|If|Switch)\b)[a-zA-Z_][a-zA-Z_$0-9]*$/;
|
||||
|
||||
export const DEFAULT_NODE_META_PATH = '/nodeMeta';
|
||||
export const DEFAULT_BATCH_PATH = '/inputs/batch';
|
||||
export const DEFAULT_INPUT_PARAMETERS_PATH = '/inputs/inputParameters';
|
||||
export const DEFAULT_OUTPUTS_PATH = '/outputs';
|
||||
|
||||
export const BATCH_CONCURRENT_SIZE_MIN = 1;
|
||||
export const BATCH_CONCURRENT_SIZE_MAX = 10;
|
||||
export const BATCH_SIZE_MIN = 1;
|
||||
export const BATCH_SIZE_MAX = 200;
|
||||
|
||||
export const DEFAULT_BATCH_CONCURRENT_SIZE = BATCH_CONCURRENT_SIZE_MAX;
|
||||
export const DEFAULT_BATCH_SIZE = 100;
|
||||
|
||||
export const INTENT_NODE_MODE = {
|
||||
/** 意图识别节点极简模式 */
|
||||
MINIMAL: 'top_speed',
|
||||
|
||||
/** 意图识别完整模式 */
|
||||
STANDARD: 'all',
|
||||
};
|
||||
|
||||
/** 极速版意图识别选项最大数目 */
|
||||
export const MINIMAL_INTENT_ITEMS = 10;
|
||||
/** 标准版意图识别选项最大数目 */
|
||||
export const STANDARD_INTENT_ITEMS = 50;
|
||||
|
||||
export const SYSTEM_DELIMITER = {
|
||||
lineBreak: '\n',
|
||||
tab: '\t',
|
||||
period: IS_OVERSEA ? '.' : '。',
|
||||
comma: IS_OVERSEA ? ',' : ',',
|
||||
semicolon: IS_OVERSEA ? ';' : ';',
|
||||
space: ' ',
|
||||
};
|
||||
|
||||
export const DEFAULT_DELIMITER_OPTIONS = [
|
||||
{
|
||||
label: I18n.t('workflow_stringprocess_concat_symbol_lineBreak'),
|
||||
value: SYSTEM_DELIMITER.lineBreak,
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
label: I18n.t('workflow_stringprocess_concat_symbol_tab'),
|
||||
value: SYSTEM_DELIMITER.tab,
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
label: I18n.t('workflow_stringprocess_concat_symbol_period'),
|
||||
value: SYSTEM_DELIMITER.period,
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
label: I18n.t('workflow_stringprocess_concat_symbol_comma'),
|
||||
value: SYSTEM_DELIMITER.comma,
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
label: I18n.t('workflow_stringprocess_concat_symbol_semicolon'),
|
||||
value: SYSTEM_DELIMITER.semicolon,
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
label: I18n.t('workflow_stringprocess_concat_symbol_space'),
|
||||
value: SYSTEM_DELIMITER.space,
|
||||
isDefault: true,
|
||||
},
|
||||
];
|
||||
|
||||
/** 场景工作流角色信息关键字 */
|
||||
export const roleInformationKeyword = 'role_information';
|
||||
|
||||
/** 节点默认宽高(带有输入/输出) */
|
||||
export const DEFAULT_NODE_SIZE = {
|
||||
width: 360,
|
||||
height: 104.7,
|
||||
};
|
||||
|
||||
/** 输入参数 label 样式 */
|
||||
export const DEFAULT_INPUT_LABEL_STYLE = {
|
||||
width: '40%',
|
||||
};
|
||||
|
||||
export const NARROW_INPUT_LABEL_STYLE = {
|
||||
width: '36%',
|
||||
};
|
||||
18
frontend/packages/workflow/nodes/src/entity-datas/index.ts
Normal file
18
frontend/packages/workflow/nodes/src/entity-datas/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './workflow-node-test-run-data';
|
||||
export * from './workflow-node-data';
|
||||
@@ -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 * from './workflow-node-data';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* 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 StandardNodeType,
|
||||
type BasicStandardNodeTypes,
|
||||
type DTODefine,
|
||||
} from '@coze-workflow/base/types';
|
||||
import { type ReleasedWorkflow } from '@coze-arch/bot-api/workflow_api';
|
||||
|
||||
import { type ApiNodeDetailDTO } from '../../typings';
|
||||
|
||||
/**
|
||||
* api 节点上存储的 api 的相关数据
|
||||
*/
|
||||
export type ApiNodeData = CommonNodeData &
|
||||
Readonly<
|
||||
Partial<ApiNodeDetailDTO> & {
|
||||
/**
|
||||
* 项目内插件节点需要保存 projectId
|
||||
*/
|
||||
projectId?: string;
|
||||
/** 该插件的最新版本的时间戳 */
|
||||
latestVersionTs?: string;
|
||||
/** 插件最新版本的展示名称,形如 v1.0.0 */
|
||||
latestVersionName?: string;
|
||||
/** 该插件当前版本的展示名称,形如 v1.0.0 */
|
||||
versionName?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* 子流程节点上存储的相关数据
|
||||
*/
|
||||
export type SubWorkflowNodeData = CommonNodeData &
|
||||
Readonly<Omit<ReleasedWorkflow, 'inputs' | 'outputs'>> & {
|
||||
/** 子流程节点输入定义 */
|
||||
inputsDefinition: DTODefine.InputVariableDTO[];
|
||||
/**
|
||||
* 项目内子流程需要保存 projectId
|
||||
*/
|
||||
projectId?: string;
|
||||
/** 该子流程的最新版本 */
|
||||
latestVersion?: string;
|
||||
};
|
||||
|
||||
export type QuestionNodeData = CommonNodeData & {
|
||||
question: string;
|
||||
options: any;
|
||||
answerType: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 通用节点类型的相关数据
|
||||
* 基础节点定义见类型 BasicStandardNodeTypes
|
||||
*/
|
||||
export interface CommonNodeData {
|
||||
/**
|
||||
*
|
||||
* 节点图标
|
||||
*/
|
||||
readonly icon: string;
|
||||
/**
|
||||
* 节点描述
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* 节点标题
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* 节点主色
|
||||
*/
|
||||
mainColor: string;
|
||||
}
|
||||
|
||||
export enum LLMNodeDataSkillType {
|
||||
Plugin = 1,
|
||||
Workflow = 4,
|
||||
Dataset = 3,
|
||||
}
|
||||
|
||||
export interface LLMNodeDataPluginSkill {
|
||||
type: LLMNodeDataSkillType.Plugin;
|
||||
pluginId?: string;
|
||||
pluginName?: string;
|
||||
apiId?: string;
|
||||
apiName?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface LLMNodeDataWorkflowSkill {
|
||||
type: LLMNodeDataSkillType.Workflow;
|
||||
workflowId: string;
|
||||
pluginId?: string;
|
||||
name?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface LLMNodeDataDatasetSkill {
|
||||
type: LLMNodeDataSkillType.Dataset;
|
||||
id: string;
|
||||
name?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export type LLMNodeDataSkill =
|
||||
| LLMNodeDataPluginSkill
|
||||
| LLMNodeDataWorkflowSkill
|
||||
| LLMNodeDataDatasetSkill;
|
||||
|
||||
export interface LLMNodeData extends CommonNodeData {
|
||||
skills: LLMNodeDataSkill[];
|
||||
}
|
||||
|
||||
type BasicNodeDataMap = {
|
||||
[K in BasicStandardNodeTypes]: CommonNodeData;
|
||||
};
|
||||
|
||||
export interface NodeData extends BasicNodeDataMap {
|
||||
[StandardNodeType.Api]: ApiNodeData;
|
||||
[StandardNodeType.SubWorkflow]: SubWorkflowNodeData;
|
||||
[StandardNodeType.Question]: QuestionNodeData;
|
||||
[StandardNodeType.LLM]: LLMNodeData;
|
||||
}
|
||||
|
||||
type IfEquals<X, Y, A = X, B = never> =
|
||||
(<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? A : B;
|
||||
|
||||
type EditAbleProperties<T> = {
|
||||
[P in keyof T]: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P>;
|
||||
}[keyof T];
|
||||
|
||||
export type EditAbleNodeData<T extends keyof NodeData> = Pick<
|
||||
NodeData[T],
|
||||
EditAbleProperties<NodeData[T]>
|
||||
>;
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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 的 node 模型中,默认对于数据的存储只有表单,即 formMeta。这部分数据实际上代表的是提交给后端用来做 workflow 运行的数据
|
||||
* 然而实际业务场景中,我们需要的不仅是提交给后端用来做运行的数据,还有一些前端业务场景下需要消费,而后端用不到的数据。比如:
|
||||
* 1. api 节点的 spaceId、发布状态
|
||||
* 2. 子流程节点的 spaceId、发布状态
|
||||
* 所以这个模块增加一个 NodeData 实体,来管理每一个node 上的一些数据,让业务层消费使用
|
||||
*/
|
||||
import { EntityData } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { type EditAbleNodeData, type NodeData } from './types';
|
||||
|
||||
export class WorkflowNodeData extends EntityData {
|
||||
private nodeData;
|
||||
|
||||
private hasSetNodeData = false;
|
||||
|
||||
init() {
|
||||
this.hasSetNodeData = false;
|
||||
this.nodeData = undefined;
|
||||
}
|
||||
|
||||
getDefaultData() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param data
|
||||
* 设置节点上除form之外的数据
|
||||
* 泛型必须传入节点类型 StandardNodeType
|
||||
*/
|
||||
setNodeData<T extends keyof NodeData = never>(data: NodeData[T]) {
|
||||
if (this.hasSetNodeData) {
|
||||
// 撤销重做时会重复设置,没必要报错
|
||||
console.warn(`node ${this.entity.id} has already set WorkflowNodeData`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.nodeData = { ...data };
|
||||
this.hasSetNodeData = true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param data
|
||||
* 更新数据,只放非readonly字段的更新
|
||||
* 泛型必须传入节点类型 StandardNodeType
|
||||
*/
|
||||
updateNodeData<T extends keyof NodeData = never>(
|
||||
data: Partial<EditAbleNodeData<T>>,
|
||||
) {
|
||||
this.nodeData = { ...this.nodeData, ...data };
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
* 获取节点上除form之外的数据
|
||||
*/
|
||||
getNodeData<T extends keyof NodeData>(): NodeData[T] {
|
||||
return this.nodeData;
|
||||
}
|
||||
}
|
||||
@@ -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 * from './workflow-node-test-run-data';
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
export enum TestRunStatus {
|
||||
success = 'success',
|
||||
failed = 'failed',
|
||||
}
|
||||
|
||||
export interface NodeTestRunResult {
|
||||
status: TestRunStatus;
|
||||
duration: number;
|
||||
input: string | null;
|
||||
output: string | null;
|
||||
}
|
||||
@@ -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 { EntityData } from '@flowgram-adapter/free-layout-editor';
|
||||
import { type WorkflowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { type NodeTestRunResult } from './typings';
|
||||
|
||||
export class WorkflowNodeTestRunData extends EntityData<NodeTestRunResult | null> {
|
||||
static readonly type = 'WorkflowNodeTestRunData';
|
||||
entity: WorkflowNodeEntity;
|
||||
|
||||
get result(): NodeTestRunResult | null {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
set result(result: NodeTestRunResult | null) {
|
||||
this.update(result);
|
||||
}
|
||||
|
||||
getDefaultData(): NodeTestRunResult | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
17
frontend/packages/workflow/nodes/src/global.d.ts
vendored
Normal file
17
frontend/packages/workflow/nodes/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
29
frontend/packages/workflow/nodes/src/index.ts
Normal file
29
frontend/packages/workflow/nodes/src/index.ts
Normal 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.
|
||||
*/
|
||||
|
||||
export * from './typings';
|
||||
export * from './workflow-nodes-container-module';
|
||||
export * from './entity-datas';
|
||||
export * from './service';
|
||||
export * from './utils';
|
||||
export * from './constants';
|
||||
export {
|
||||
nodeMetaValidator,
|
||||
settingOnErrorValidator,
|
||||
outputTreeValidator,
|
||||
inputTreeValidator,
|
||||
} from './validators';
|
||||
export * from './setting-on-error';
|
||||
17
frontend/packages/workflow/nodes/src/service/index.ts
Normal file
17
frontend/packages/workflow/nodes/src/service/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './workflow-nodes-service';
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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 { customAlphabet } from 'nanoid';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { FlowNodeFormData } from '@flowgram-adapter/free-layout-editor';
|
||||
import { EntityManager } from '@flowgram-adapter/free-layout-editor';
|
||||
import { WorkflowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
import { Emitter } from '@flowgram-adapter/common';
|
||||
|
||||
import { type FormNodeMeta } from '../typings';
|
||||
import { DEFAULT_NODE_META_PATH } from '../constants';
|
||||
|
||||
@injectable()
|
||||
export class WorkflowNodesService {
|
||||
protected onNodesTitleChangeEmitter = new Emitter<void>();
|
||||
@inject(EntityManager) protected readonly entityManager: EntityManager;
|
||||
private nanoid = customAlphabet('1234567890', 5);
|
||||
/**
|
||||
* 节点标题变化
|
||||
*/
|
||||
readonly onNodesTitleChange = this.onNodesTitleChangeEmitter.event;
|
||||
|
||||
/**
|
||||
* 节点标题更新
|
||||
* @param node
|
||||
*/
|
||||
getNodeTitle(node: WorkflowNodeEntity): string {
|
||||
const formData = node.getData<FlowNodeFormData>(FlowNodeFormData);
|
||||
const nodeMeta = formData.formModel.getFormItemValueByPath<FormNodeMeta>(
|
||||
DEFAULT_NODE_META_PATH,
|
||||
);
|
||||
|
||||
return nodeMeta?.title || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有节点
|
||||
*/
|
||||
getAllNodes(ignoreNode?: WorkflowNodeEntity): WorkflowNodeEntity[] {
|
||||
return this.entityManager
|
||||
.getEntities<WorkflowNodeEntity>(WorkflowNodeEntity)
|
||||
.filter(n => n.id !== 'root' && n !== ignoreNode);
|
||||
}
|
||||
/**
|
||||
* 获取所有节点的标题
|
||||
*/
|
||||
getAllTitles(ignoreNode?: WorkflowNodeEntity): string[] {
|
||||
return this.getAllNodes(ignoreNode).map(node => this.getNodeTitle(node));
|
||||
}
|
||||
/**
|
||||
* 获取开始节点
|
||||
*/
|
||||
getStartNode(): WorkflowNodeEntity {
|
||||
return this.entityManager
|
||||
.getEntities<WorkflowNodeEntity>(WorkflowNodeEntity)
|
||||
.find(node => node.isStart) as WorkflowNodeEntity;
|
||||
}
|
||||
/**
|
||||
* 触发节点标题更新事件
|
||||
*/
|
||||
fireNodesTitleChange(): void {
|
||||
this.onNodesTitleChangeEmitter.fire();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建不会重复的title
|
||||
*
|
||||
* abc_1 -> abc_2
|
||||
*/
|
||||
|
||||
createUniqTitle(
|
||||
titlePrefix: string,
|
||||
ignoreNode?: WorkflowNodeEntity | undefined,
|
||||
ignoreTitles?: string[],
|
||||
): string {
|
||||
const allTitles = this.getAllTitles(ignoreNode);
|
||||
if (ignoreTitles) {
|
||||
allTitles.push(...ignoreTitles);
|
||||
}
|
||||
|
||||
const allTitlesSet = new Set(allTitles);
|
||||
|
||||
let startIndex = 0;
|
||||
let newTitle = `${titlePrefix}`;
|
||||
|
||||
const matched = titlePrefix.match(/_([0-9]+)$/);
|
||||
if (matched) {
|
||||
startIndex = Number(matched[1]);
|
||||
titlePrefix = titlePrefix.slice(0, matched.index);
|
||||
}
|
||||
|
||||
while (allTitlesSet.has(newTitle)) {
|
||||
startIndex += 1;
|
||||
newTitle = `${titlePrefix}_${startIndex}`;
|
||||
}
|
||||
return newTitle;
|
||||
}
|
||||
|
||||
/** 创建唯一ID */
|
||||
createUniqID() {
|
||||
let id: string;
|
||||
do {
|
||||
// 防止 id 以 0 开头,后端会转成 int64 导致 0 丢失,从而一些 id 匹配不上
|
||||
id = `1${this.nanoid()}`;
|
||||
} while (this.entityManager.getEntityById(id));
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-magic-numbers */
|
||||
import { StandardNodeType } from '@coze-workflow/base';
|
||||
|
||||
/**
|
||||
* 使用 V2 版本异常设置的节点
|
||||
*/
|
||||
export const SETTING_ON_ERROR_V2_NODES = [
|
||||
StandardNodeType.Code,
|
||||
StandardNodeType.LLM,
|
||||
StandardNodeType.Api,
|
||||
StandardNodeType.Database,
|
||||
StandardNodeType.ImageGenerate,
|
||||
StandardNodeType.DatabaseCreate,
|
||||
StandardNodeType.DatabaseUpdate,
|
||||
StandardNodeType.DatabaseQuery,
|
||||
StandardNodeType.DatabaseDelete,
|
||||
StandardNodeType.ImageCanvas,
|
||||
StandardNodeType.Intent,
|
||||
];
|
||||
|
||||
/**
|
||||
* 使用V1的版本异常设置
|
||||
*/
|
||||
export const SETTING_ON_ERROR_V1_NODES = [StandardNodeType.Http];
|
||||
|
||||
/**
|
||||
* 有动态port的节点
|
||||
*/
|
||||
export const SETTING_ON_ERROR_DYNAMIC_PORT_NODES = [StandardNodeType.Intent];
|
||||
|
||||
/**
|
||||
* 开启异常的节点
|
||||
*/
|
||||
export const SETTING_ON_ERROR_NODES = [
|
||||
...SETTING_ON_ERROR_V1_NODES,
|
||||
...SETTING_ON_ERROR_V2_NODES,
|
||||
];
|
||||
|
||||
/**
|
||||
* 异常端口
|
||||
*/
|
||||
export const SETTING_ON_ERROR_PORT = 'branch_error';
|
||||
|
||||
/**
|
||||
* 超时最小100ms;
|
||||
*/
|
||||
export const SETTING_ON_ERROR_MIN_TIMEOUT = 100;
|
||||
|
||||
/**
|
||||
* 其他节点:默认1分钟,最大1分钟;
|
||||
*/
|
||||
export const SETTING_ON_ERROR_DEFAULT_TIMEOUT = {
|
||||
default: 60 * 1000,
|
||||
max: 60 * 1000,
|
||||
};
|
||||
|
||||
/**
|
||||
* 节点配置
|
||||
* LLM:默认3分钟,最大10分钟;
|
||||
* 插件:默认3分钟,最大3分钟;
|
||||
*/
|
||||
export const SETTING_ON_ERROR_NODES_CONFIG = {
|
||||
[StandardNodeType.LLM]: {
|
||||
timeout: {
|
||||
default: 3 * 60 * 1000,
|
||||
max: 10 * 60 * 1000,
|
||||
init: 10 * 60 * 1000,
|
||||
},
|
||||
enableBackupModel: true,
|
||||
},
|
||||
[StandardNodeType.Api]: {
|
||||
timeout: {
|
||||
default: 3 * 60 * 1000,
|
||||
max: 3 * 60 * 1000,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ERROR_BODY_NAME = 'errorBody';
|
||||
export const IS_SUCCESS_NAME = 'isSuccess';
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* 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 ViewVariableTreeNode,
|
||||
type StandardNodeType,
|
||||
} from '@coze-workflow/base';
|
||||
import { type NodeContext } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { formatModelData } from '../utils';
|
||||
import { getOutputsWithErrorBody } from './utils/outputs';
|
||||
import { getTimeoutConfig } from './utils/get-timeout-config';
|
||||
import { isSettingOnErrorV2 } from './utils';
|
||||
import {
|
||||
type SettingOnErrorDTO,
|
||||
SettingOnErrorProcessType,
|
||||
type SettingOnErrorExt,
|
||||
type NodeValueWithSettingOnErrorDTO,
|
||||
type NodeValueWithSettingOnErrorVO,
|
||||
type SettingOnErrorVO,
|
||||
} from './types';
|
||||
|
||||
const formatExtOnInit = (ext: SettingOnErrorDTO['ext']) => {
|
||||
if (!ext) {
|
||||
return ext;
|
||||
}
|
||||
const llmParam = ext?.backupLLmParam;
|
||||
|
||||
return {
|
||||
backupLLmParam: llmParam ? JSON.parse(llmParam) : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const getDTOProcessType = (settingOnError?: SettingOnErrorDTO) =>
|
||||
settingOnError?.processType ||
|
||||
(settingOnError?.switch
|
||||
? SettingOnErrorProcessType.RETURN
|
||||
: SettingOnErrorProcessType.BREAK);
|
||||
|
||||
const settingOnErrorInitV2 = (
|
||||
settingOnError?: SettingOnErrorDTO,
|
||||
context?: NodeContext,
|
||||
value?: NodeValueWithSettingOnErrorDTO,
|
||||
) => {
|
||||
if (!isNodeContextV2(context)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
let timeoutMs = settingOnError?.timeoutMs;
|
||||
const timeoutConfig = getTimeoutConfig(context?.node);
|
||||
|
||||
if (!timeoutMs) {
|
||||
// 如果没有设置超时时间,且有初始值配置,则设置初始值
|
||||
// 如LLM后端默认是10min,历史数据需要设置为init的10min,新加的节点显示默认的default的3min
|
||||
if (value && timeoutConfig?.init) {
|
||||
timeoutMs = timeoutConfig.init;
|
||||
} else {
|
||||
timeoutMs = timeoutConfig?.default;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
processType: getDTOProcessType(settingOnError),
|
||||
timeoutMs,
|
||||
retryTimes: settingOnError?.retryTimes ?? 0,
|
||||
ext: formatExtOnInit(settingOnError?.ext),
|
||||
};
|
||||
};
|
||||
|
||||
const formatExtOnSave = (
|
||||
ext: SettingOnErrorExt | undefined,
|
||||
playgroundContext,
|
||||
) => {
|
||||
if (!ext) {
|
||||
return ext;
|
||||
}
|
||||
const models = playgroundContext?.models || [];
|
||||
const llmParam = ext.backupLLmParam;
|
||||
const modelMeta = models.find(m => m.model_type === llmParam?.modelType);
|
||||
|
||||
return {
|
||||
backupLLmParam: llmParam
|
||||
? JSON.stringify(formatModelData(llmParam, modelMeta))
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const settingOnErrorSaveV2 = (
|
||||
settingOnError: SettingOnErrorVO,
|
||||
context?: NodeContext,
|
||||
) => {
|
||||
const playgroundContext = context?.playgroundContext;
|
||||
const res: Partial<SettingOnErrorDTO> = {
|
||||
processType:
|
||||
settingOnError?.processType ||
|
||||
(settingOnError?.settingOnErrorIsOpen
|
||||
? SettingOnErrorProcessType.RETURN
|
||||
: undefined),
|
||||
timeoutMs: settingOnError?.timeoutMs,
|
||||
retryTimes: settingOnError?.retryTimes,
|
||||
};
|
||||
|
||||
if (settingOnError?.retryTimes) {
|
||||
res.ext = formatExtOnSave(settingOnError?.ext, playgroundContext);
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
const isNodeContextV2 = (context?: NodeContext) =>
|
||||
isSettingOnErrorV2(context?.node?.flowNodeType as StandardNodeType);
|
||||
|
||||
export const settingOnErrorToVO = (
|
||||
settingOnError?: SettingOnErrorDTO,
|
||||
context?: NodeContext,
|
||||
value?: NodeValueWithSettingOnErrorDTO,
|
||||
): SettingOnErrorVO => ({
|
||||
settingOnErrorIsOpen: settingOnError?.switch,
|
||||
settingOnErrorJSON: settingOnError?.dataOnErr,
|
||||
...settingOnErrorInitV2(settingOnError, context, value),
|
||||
});
|
||||
|
||||
export const settingOnErrorToDTO = (
|
||||
settingOnError?: SettingOnErrorVO,
|
||||
context?: NodeContext,
|
||||
) => {
|
||||
if (!settingOnError) {
|
||||
return settingOnError;
|
||||
}
|
||||
|
||||
return {
|
||||
switch: settingOnError?.settingOnErrorIsOpen,
|
||||
dataOnErr: settingOnError?.settingOnErrorJSON,
|
||||
...(isNodeContextV2(context)
|
||||
? settingOnErrorSaveV2(settingOnError, context)
|
||||
: {}),
|
||||
};
|
||||
};
|
||||
|
||||
export const formatOutputsOnInit = (
|
||||
value?: NodeValueWithSettingOnErrorDTO,
|
||||
context?: NodeContext,
|
||||
) => {
|
||||
const outputs = value?.outputs;
|
||||
const isV2 = isNodeContextV2(context);
|
||||
const isOpen = !!value?.inputs?.settingOnError?.switch;
|
||||
|
||||
if (!outputs || !isSettingOnErrorV2 || !isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isBatch = !!value?.inputs?.batch?.batchEnable;
|
||||
return getOutputsWithErrorBody({
|
||||
value: outputs,
|
||||
isBatch,
|
||||
isOpen,
|
||||
isSettingOnErrorV2: isV2,
|
||||
});
|
||||
};
|
||||
|
||||
export const settingOnErrorInit = (
|
||||
value?: NodeValueWithSettingOnErrorDTO,
|
||||
context?: NodeContext,
|
||||
): {
|
||||
settingOnError?: SettingOnErrorVO;
|
||||
outputs?: ViewVariableTreeNode[];
|
||||
} => {
|
||||
const outputs = formatOutputsOnInit(value, context);
|
||||
|
||||
return {
|
||||
settingOnError: settingOnErrorToVO(
|
||||
value?.inputs?.settingOnError,
|
||||
context,
|
||||
value,
|
||||
),
|
||||
...(outputs ? { outputs } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
export const settingOnErrorSave = (
|
||||
value: NodeValueWithSettingOnErrorVO,
|
||||
context: NodeContext | undefined = undefined,
|
||||
) => {
|
||||
const settingOnError = value?.settingOnError;
|
||||
if (value?.settingOnError) {
|
||||
delete value.settingOnError;
|
||||
}
|
||||
|
||||
return {
|
||||
settingOnError: settingOnErrorToDTO(settingOnError, context),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type StandardNodeType } from '@coze-workflow/base/types';
|
||||
import { useCurrentEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { isSettingOnErrorV2, isSettingOnError } from '../utils';
|
||||
|
||||
export const useIsSettingOnErrorV2 = () => {
|
||||
const node = useCurrentEntity();
|
||||
|
||||
return isSettingOnErrorV2(node.flowNodeType as StandardNodeType);
|
||||
};
|
||||
|
||||
export const useIsSettingOnError = () => {
|
||||
const node = useCurrentEntity();
|
||||
|
||||
return isSettingOnError(node.flowNodeType as StandardNodeType);
|
||||
};
|
||||
|
||||
export { useTimeoutConfig } from './use-timeout-config';
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useCurrentEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { getTimeoutConfig } from '../utils/get-timeout-config';
|
||||
|
||||
/**
|
||||
* 获取超时配置
|
||||
* @returns
|
||||
*/
|
||||
export const useTimeoutConfig = (): {
|
||||
default: number;
|
||||
max: number;
|
||||
min: number;
|
||||
disabled: boolean;
|
||||
} => {
|
||||
const node = useCurrentEntity();
|
||||
return getTimeoutConfig(node);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export {
|
||||
SettingOnErrorProcessType,
|
||||
type SettingOnErrorExt,
|
||||
type SettingOnErrorVO,
|
||||
type SettingOnErrorValue,
|
||||
} from './types';
|
||||
|
||||
export {
|
||||
settingOnErrorInit,
|
||||
settingOnErrorSave,
|
||||
settingOnErrorToDTO,
|
||||
settingOnErrorToVO,
|
||||
} from './data-transformer';
|
||||
export {
|
||||
isSettingOnError,
|
||||
isSettingOnErrorV2,
|
||||
isSettingOnErrorDynamicPort,
|
||||
} from './utils';
|
||||
export {
|
||||
generateErrorBodyMeta,
|
||||
generateIsSuccessMeta,
|
||||
} from './utils/generate-meta';
|
||||
export {
|
||||
useIsSettingOnError,
|
||||
useIsSettingOnErrorV2,
|
||||
useTimeoutConfig,
|
||||
} from './hooks';
|
||||
export {
|
||||
SETTING_ON_ERROR_PORT,
|
||||
SETTING_ON_ERROR_NODES_CONFIG,
|
||||
ERROR_BODY_NAME,
|
||||
IS_SUCCESS_NAME,
|
||||
} from './constants';
|
||||
export {
|
||||
getOutputsWithErrorBody,
|
||||
sortErrorBody,
|
||||
getExcludeErrorBody,
|
||||
} from './utils/outputs';
|
||||
124
frontend/packages/workflow/nodes/src/setting-on-error/types.ts
Normal file
124
frontend/packages/workflow/nodes/src/setting-on-error/types.ts
Normal 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 ViewVariableTreeNode } from '@coze-workflow/base';
|
||||
|
||||
/**
|
||||
* 异常处理类型
|
||||
* 1 直接中断
|
||||
* 2 返回设定内容,
|
||||
* 3 执行异常流程
|
||||
*/
|
||||
export enum SettingOnErrorProcessType {
|
||||
BREAK = 1,
|
||||
RETURN = 2,
|
||||
EXCEPTION = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常处理额外数据
|
||||
*/
|
||||
export interface SettingOnErrorExt {
|
||||
/**
|
||||
* LLM节点重试的备选模型
|
||||
*/
|
||||
backupLLmParam?: {
|
||||
modelName?: string;
|
||||
modelType?: number;
|
||||
temperature?: number;
|
||||
frequencyPenalty?: number;
|
||||
presencePenalty?: number;
|
||||
topP?: number;
|
||||
topK?: number;
|
||||
maxTokens?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface SettingOnErrorBase {
|
||||
/**
|
||||
* 处理类型
|
||||
*/
|
||||
processType?: SettingOnErrorProcessType;
|
||||
/**
|
||||
* 超时时间 毫秒
|
||||
*/
|
||||
timeoutMs?: number;
|
||||
/**
|
||||
* 重试次数 0 表示不重试
|
||||
*/
|
||||
retryTimes?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常处理前端结构
|
||||
*/
|
||||
export interface SettingOnErrorVO extends SettingOnErrorBase {
|
||||
/**
|
||||
* 是否开启异常处理
|
||||
*/
|
||||
settingOnErrorIsOpen?: boolean;
|
||||
/**
|
||||
* 发生异常处理的默认值
|
||||
*/
|
||||
settingOnErrorJSON?: string;
|
||||
|
||||
/**
|
||||
* 其他设置
|
||||
*/
|
||||
ext?: SettingOnErrorExt;
|
||||
}
|
||||
|
||||
export type SettingOnErrorValue = SettingOnErrorVO;
|
||||
|
||||
/**
|
||||
* 异常处理后端结构
|
||||
*/
|
||||
export interface SettingOnErrorDTO extends SettingOnErrorBase {
|
||||
/**
|
||||
* 是否开启异常处理
|
||||
*/
|
||||
switch: boolean;
|
||||
/**
|
||||
* 发生异常处理的默认值
|
||||
*/
|
||||
dataOnErr: string;
|
||||
/**
|
||||
* 其他设置
|
||||
*/
|
||||
ext?: {
|
||||
backupLLmParam?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 有异常设置的后端节点数据
|
||||
*/
|
||||
export interface NodeValueWithSettingOnErrorDTO {
|
||||
inputs?: {
|
||||
settingOnError?: SettingOnErrorDTO;
|
||||
batch?: {
|
||||
batchEnable?: boolean;
|
||||
};
|
||||
};
|
||||
outputs?: ViewVariableTreeNode[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 有异常设置的前端节点数据
|
||||
*/
|
||||
export interface NodeValueWithSettingOnErrorVO {
|
||||
settingOnError?: SettingOnErrorVO;
|
||||
}
|
||||
@@ -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 StandardNodeType } from '@coze-workflow/base';
|
||||
|
||||
import {
|
||||
SETTING_ON_ERROR_DYNAMIC_PORT_NODES,
|
||||
SETTING_ON_ERROR_NODES,
|
||||
SETTING_ON_ERROR_V2_NODES,
|
||||
} from './constants';
|
||||
|
||||
/**
|
||||
* 是不是v2版本的节点
|
||||
* @param type
|
||||
* @returns
|
||||
*/
|
||||
export const isSettingOnErrorV2 = (type?: StandardNodeType) =>
|
||||
type && SETTING_ON_ERROR_V2_NODES.includes(type);
|
||||
|
||||
/**
|
||||
* 是不是开启异常设置的节点
|
||||
* @param type
|
||||
* @returns
|
||||
*/
|
||||
export const isSettingOnError = (type?: StandardNodeType) =>
|
||||
type && SETTING_ON_ERROR_NODES.includes(type);
|
||||
|
||||
/**
|
||||
* 是不是动态通道的节点
|
||||
* @param type
|
||||
* @returns
|
||||
*/
|
||||
export const isSettingOnErrorDynamicPort = (type?: StandardNodeType) =>
|
||||
type && SETTING_ON_ERROR_DYNAMIC_PORT_NODES.includes(type);
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import { ViewVariableType } from '@coze-workflow/base';
|
||||
|
||||
import { ERROR_BODY_NAME, IS_SUCCESS_NAME } from '../constants';
|
||||
|
||||
export const generateErrorBodyMeta = () => ({
|
||||
key: nanoid(),
|
||||
name: ERROR_BODY_NAME,
|
||||
type: ViewVariableType.Object,
|
||||
readonly: true,
|
||||
children: [
|
||||
{
|
||||
key: nanoid(),
|
||||
name: 'errorMessage',
|
||||
type: ViewVariableType.String,
|
||||
readonly: true,
|
||||
},
|
||||
{
|
||||
key: nanoid(),
|
||||
name: 'errorCode',
|
||||
type: ViewVariableType.String,
|
||||
readonly: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const generateIsSuccessMeta = () => ({
|
||||
key: nanoid(),
|
||||
name: IS_SUCCESS_NAME,
|
||||
type: ViewVariableType.Boolean,
|
||||
readonly: true,
|
||||
});
|
||||
@@ -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 { get } from 'lodash-es';
|
||||
import { PluginType, StandardNodeType } from '@coze-workflow/base';
|
||||
import { type WorkflowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import {
|
||||
SETTING_ON_ERROR_DEFAULT_TIMEOUT,
|
||||
SETTING_ON_ERROR_MIN_TIMEOUT,
|
||||
SETTING_ON_ERROR_NODES_CONFIG,
|
||||
} from '../constants';
|
||||
import { WorkflowNodeData } from '../../entity-datas';
|
||||
|
||||
/**
|
||||
* 是不是端插件
|
||||
* @param node
|
||||
* @returns
|
||||
*/
|
||||
const isLocalPlugin = (node?: WorkflowNodeEntity) => {
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nodeDataEntity = node.getData<WorkflowNodeData>(WorkflowNodeData);
|
||||
const nodeData = nodeDataEntity?.getNodeData();
|
||||
|
||||
return !!(
|
||||
node?.flowNodeType === StandardNodeType.Api &&
|
||||
get(nodeData, 'pluginType') === PluginType.LOCAL
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取节点超时配置
|
||||
*/
|
||||
export const getTimeoutConfig = (
|
||||
node?: WorkflowNodeEntity,
|
||||
): {
|
||||
max: number;
|
||||
default: number;
|
||||
min: number;
|
||||
init?: number;
|
||||
disabled: boolean;
|
||||
} => {
|
||||
let timeoutConfig = SETTING_ON_ERROR_DEFAULT_TIMEOUT;
|
||||
|
||||
if (
|
||||
node?.flowNodeType &&
|
||||
SETTING_ON_ERROR_NODES_CONFIG[node.flowNodeType]?.timeout
|
||||
) {
|
||||
timeoutConfig = SETTING_ON_ERROR_NODES_CONFIG[node.flowNodeType].timeout;
|
||||
}
|
||||
|
||||
return {
|
||||
...timeoutConfig,
|
||||
min: SETTING_ON_ERROR_MIN_TIMEOUT,
|
||||
disabled: isLocalPlugin(node),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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 ViewVariableTreeNode } from '@coze-workflow/base';
|
||||
|
||||
import { ERROR_BODY_NAME, IS_SUCCESS_NAME } from '../constants';
|
||||
import { generateErrorBodyMeta, generateIsSuccessMeta } from './generate-meta';
|
||||
|
||||
const settingOnErrorNames = isSettingOnErrorV2 => [
|
||||
ERROR_BODY_NAME,
|
||||
...(isSettingOnErrorV2 ? [IS_SUCCESS_NAME] : []),
|
||||
];
|
||||
|
||||
const excludeFn = isSettingOnErrorV2 => v =>
|
||||
!settingOnErrorNames(isSettingOnErrorV2).includes(v.name);
|
||||
|
||||
const includeFn = isSettingOnErrorV2 => v =>
|
||||
settingOnErrorNames(isSettingOnErrorV2).includes(v.name);
|
||||
|
||||
/**
|
||||
* 向 output 中添加/剔除 errorBody
|
||||
*/
|
||||
export const getOutputsWithErrorBody = ({
|
||||
value,
|
||||
isBatch,
|
||||
isOpen,
|
||||
isSettingOnErrorV2,
|
||||
}: {
|
||||
value?: ViewVariableTreeNode[];
|
||||
isBatch: boolean;
|
||||
isOpen: boolean;
|
||||
isSettingOnErrorV2?: boolean;
|
||||
}) => {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
// 添加 errorBody
|
||||
if (isOpen) {
|
||||
// batch 模式下,在第一层的 children 里追加 errorBody
|
||||
if (isBatch) {
|
||||
return [
|
||||
{
|
||||
...value[0],
|
||||
children: [
|
||||
...(value[0]?.children ?? []).filter(excludeFn(isSettingOnErrorV2)),
|
||||
generateErrorBodyMeta(),
|
||||
...(isSettingOnErrorV2 ? [generateIsSuccessMeta()] : []),
|
||||
],
|
||||
},
|
||||
];
|
||||
// single 模式下,在第一层追加 errorBody
|
||||
} else {
|
||||
return [
|
||||
...(value ?? []).filter(excludeFn(isSettingOnErrorV2)),
|
||||
generateErrorBodyMeta(),
|
||||
...(isSettingOnErrorV2 ? [generateIsSuccessMeta()] : []),
|
||||
];
|
||||
}
|
||||
// 剔除 errorBody
|
||||
} else {
|
||||
// batch 模式下,从第一层的 children 中剔除
|
||||
if (isBatch) {
|
||||
const [one, ...rest] = value;
|
||||
return [
|
||||
{
|
||||
...one,
|
||||
children: [
|
||||
...(one?.children ?? []).filter(excludeFn(isSettingOnErrorV2)),
|
||||
],
|
||||
},
|
||||
...rest,
|
||||
];
|
||||
// single 模式下,从第一层的 children 中剔除
|
||||
} else {
|
||||
return [...(value ?? []).filter(excludeFn(isSettingOnErrorV2))];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* output 属性排序,保证 errorBody 在最下面
|
||||
*/
|
||||
export const sortErrorBody = ({
|
||||
value,
|
||||
isBatch,
|
||||
isSettingOnErrorV2,
|
||||
}: {
|
||||
value?: ViewVariableTreeNode[];
|
||||
isBatch: boolean;
|
||||
isSettingOnErrorV2?: boolean;
|
||||
}) => {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (isBatch) {
|
||||
const [one, ...rest] = value;
|
||||
return [
|
||||
{
|
||||
...one,
|
||||
children: [
|
||||
...(one?.children ?? []).filter(excludeFn(isSettingOnErrorV2)),
|
||||
...(one?.children ?? []).filter(includeFn(isSettingOnErrorV2)),
|
||||
],
|
||||
},
|
||||
...rest,
|
||||
];
|
||||
}
|
||||
return [
|
||||
...value.filter(excludeFn(isSettingOnErrorV2)),
|
||||
...value.filter(includeFn(isSettingOnErrorV2)),
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* 把 value 中的 errorBody 删除掉
|
||||
*/
|
||||
export const getExcludeErrorBody = ({
|
||||
value,
|
||||
isBatch,
|
||||
isSettingOnErrorV2,
|
||||
}: {
|
||||
value?: ViewVariableTreeNode[];
|
||||
isBatch: boolean;
|
||||
isSettingOnErrorV2?: boolean;
|
||||
}) =>
|
||||
getOutputsWithErrorBody({
|
||||
value,
|
||||
isBatch,
|
||||
isOpen: false,
|
||||
isSettingOnErrorV2,
|
||||
});
|
||||
@@ -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 { get, isEmpty } from 'lodash-es';
|
||||
import { VariableTypeDTO } from '@coze-workflow/base';
|
||||
|
||||
interface ListRefSchema {
|
||||
type: 'list';
|
||||
value: {
|
||||
type: 'ref';
|
||||
content: {
|
||||
source: string;
|
||||
blockID: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const toListRefSchema = (value: string[]): ListRefSchema => {
|
||||
const [nodeId, ...keyPaths] = value;
|
||||
return {
|
||||
type: VariableTypeDTO.list,
|
||||
value: {
|
||||
type: 'ref',
|
||||
content: {
|
||||
source: 'block-output',
|
||||
blockID: `${nodeId}`,
|
||||
name: keyPaths.join('.'), // 这是使用当前循环的变量,固定名字叫item
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const listRefSchemaToValue = (
|
||||
listRefSchema: ListRefSchema,
|
||||
): string[] => {
|
||||
if (!listRefSchema || isEmpty(listRefSchema)) {
|
||||
return [];
|
||||
}
|
||||
const nodeId = get(listRefSchema, 'value.content.blockID', '');
|
||||
const keys = get(listRefSchema, 'value.content.name', '');
|
||||
|
||||
if (!nodeId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [nodeId].concat(keys ? keys.split('.') : []);
|
||||
};
|
||||
31
frontend/packages/workflow/nodes/src/typings/index.ts
Normal file
31
frontend/packages/workflow/nodes/src/typings/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 WorkflowNodeMeta,
|
||||
type WorkflowNodeRenderProps,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
export {
|
||||
type ViewVariableMeta,
|
||||
ViewVariableTreeNode,
|
||||
ViewVariableType,
|
||||
} from '@coze-workflow/base';
|
||||
export * from './node';
|
||||
export * from './playground-context';
|
||||
export * from './form-value-to-dto';
|
||||
export * from './test-run';
|
||||
export * from './services';
|
||||
export * from './trigger';
|
||||
116
frontend/packages/workflow/nodes/src/typings/node.ts
Normal file
116
frontend/packages/workflow/nodes/src/typings/node.ts
Normal 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 type {
|
||||
DTODefine,
|
||||
VariableMetaDTO,
|
||||
ApiDetailData,
|
||||
BlockInput,
|
||||
} from '@coze-workflow/base';
|
||||
import type {
|
||||
PluginProductStatus,
|
||||
ProductUnlistType,
|
||||
DebugExample as OriginDebugExample,
|
||||
} from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
export {
|
||||
WorkflowNodeVariablesMeta,
|
||||
type NodeMeta,
|
||||
type WorkflowNodeRegistry,
|
||||
} from '@coze-workflow/base';
|
||||
|
||||
export interface FormNodeMeta {
|
||||
title: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
subTitle?: string;
|
||||
}
|
||||
|
||||
export interface NodeTemplateInfo {
|
||||
title: string;
|
||||
icon: string;
|
||||
subTitle: string;
|
||||
description: string;
|
||||
mainColor: string;
|
||||
}
|
||||
|
||||
export interface ApiNodeIdentifier {
|
||||
api_id?: string;
|
||||
pluginID: string;
|
||||
apiName: string;
|
||||
plugin_version?: string;
|
||||
}
|
||||
|
||||
export type DebugExample = OriginDebugExample;
|
||||
|
||||
/**
|
||||
* Plugin扩展协议新增的属性
|
||||
*
|
||||
*/
|
||||
export interface PluginExtendProps {
|
||||
title?: string;
|
||||
label?: string;
|
||||
enum?: string[];
|
||||
enumVarNames?: string[];
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
exclusiveMinimum?: boolean;
|
||||
exclusiveMaximum?: boolean;
|
||||
defaultValue?: DTODefine.LiteralExpressionContent;
|
||||
bizExtend?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 接口 /api/workflow_api/apiDetail 返回的插件数据结构
|
||||
* 由于 inputs 和 outputs 等参数类型后端没有定义清楚,这里补充完善下
|
||||
*/
|
||||
export type ApiNodeDetailDTO = Required<ApiDetailData> & {
|
||||
inputs: (VariableMetaDTO & PluginExtendProps)[]; // name, type, schema, required, description
|
||||
outputs: VariableMetaDTO[]; // name, type, schema, required, description
|
||||
pluginProductStatus: PluginProductStatus;
|
||||
pluginProductUnlistType: ProductUnlistType;
|
||||
};
|
||||
|
||||
/**
|
||||
* 插件节点数据部分结构定义后端
|
||||
*/
|
||||
export interface ApiNodeDataDTO {
|
||||
data: {
|
||||
nodeMeta: {
|
||||
title?: string;
|
||||
icon?: string;
|
||||
subtitle?: string;
|
||||
description?: string;
|
||||
};
|
||||
inputs: {
|
||||
apiParam: BlockInput[];
|
||||
inputParameters?: BlockInput[];
|
||||
inputDefs?: DTODefine.InputVariableDTO[];
|
||||
batch?: {
|
||||
batchEnable: boolean;
|
||||
batchSize: number;
|
||||
concurrentSize: number;
|
||||
inputLists: BlockInput[];
|
||||
};
|
||||
batchMode?: string;
|
||||
settingOnError?: {
|
||||
switch?: boolean;
|
||||
dataOnErr?: string;
|
||||
};
|
||||
};
|
||||
outputs?: VariableMetaDTO[];
|
||||
};
|
||||
}
|
||||
@@ -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 { PlaygroundContext as PlaygroudContextOrigin } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
type WorkflowBatchService,
|
||||
type WorkflowVariableService,
|
||||
type WorkflowVariableValidationService,
|
||||
} from '@coze-workflow/variable';
|
||||
import { type StandardNodeType } from '@coze-workflow/base';
|
||||
|
||||
import { type WorkflowNodesService } from '../service';
|
||||
import { type NodeTemplateInfo } from './node';
|
||||
|
||||
export const PlaygroundContext = PlaygroudContextOrigin;
|
||||
|
||||
export interface PlaygroundContext {
|
||||
readonly variableService: WorkflowVariableService;
|
||||
readonly batchService: WorkflowBatchService;
|
||||
readonly nodesService: WorkflowNodesService;
|
||||
readonly variableValidationService: WorkflowVariableValidationService;
|
||||
|
||||
/**
|
||||
* 根据meta 类型获取信息
|
||||
* @param type
|
||||
*/
|
||||
getNodeTemplateInfoByType: (
|
||||
type: StandardNodeType,
|
||||
) => NodeTemplateInfo | undefined;
|
||||
/**
|
||||
* 是否为 不可编辑模式
|
||||
*/
|
||||
disabled: boolean;
|
||||
}
|
||||
21
frontend/packages/workflow/nodes/src/typings/services.ts
Normal file
21
frontend/packages/workflow/nodes/src/typings/services.ts
Normal 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.
|
||||
*/
|
||||
|
||||
export const DeveloperApiService = Symbol('DeveloperAPIService');
|
||||
|
||||
export interface DeveloperApiService {
|
||||
GetReleasedWorkflows: (req?: unknown) => Promise<{ data: any }>;
|
||||
}
|
||||
19
frontend/packages/workflow/nodes/src/typings/test-run.ts
Normal file
19
frontend/packages/workflow/nodes/src/typings/test-run.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 WorkflowRenderKey {
|
||||
EXECUTE_STATUS_BAR = 'execute_status_bar', // test run 顶部 bar
|
||||
}
|
||||
96
frontend/packages/workflow/nodes/src/typings/trigger.ts
Normal file
96
frontend/packages/workflow/nodes/src/typings/trigger.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type ValueExpression,
|
||||
type OutputValueVO,
|
||||
} from '@coze-workflow/base/types';
|
||||
import {
|
||||
type FormDataTypeName,
|
||||
type IFormItemMeta,
|
||||
type ValidatorProps,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
export namespace TriggerForm {
|
||||
export const TabName = 'tab';
|
||||
export enum Tab {
|
||||
Basic = 'basic',
|
||||
Trigger = 'trigger',
|
||||
}
|
||||
export const TriggerFormName = 'trigger';
|
||||
export const TriggerFormIsOpenName = 'isOpen';
|
||||
export const TriggerFormEventTypeName = 'event';
|
||||
export const TriggerFormEventIdName = 'eventID';
|
||||
export const TriggerFormAppIdName = 'appID';
|
||||
export const TriggerFormParametersName = 'parameters';
|
||||
export const TriggerFormCronjobName = 'crontab';
|
||||
export const TriggerFormCronjobTypeName = 'crontabType';
|
||||
export const TriggerFormBindWorkflowName = 'workflowId';
|
||||
export enum TriggerFormEventType {
|
||||
Time = 'time',
|
||||
Event = 'event',
|
||||
}
|
||||
|
||||
export const getVariableName = (variable: OutputValueVO): string =>
|
||||
`${variable?.type},${variable?.key ?? variable?.name}`;
|
||||
|
||||
export type Validation =
|
||||
| 'requiredWhenTriggerOpenedAndSelectedTime'
|
||||
| 'requiredWhenTriggerOpenedAndSelectedEvent'
|
||||
| 'required'
|
||||
| 'cronValidateWhenTriggerOpenedAndSelectedTime'
|
||||
| 'cronValidate';
|
||||
export type ValidationFn = (props: ValidatorProps<any, any>) => string | true;
|
||||
|
||||
export interface FormItemMeta {
|
||||
name: string;
|
||||
label: string;
|
||||
required?: boolean;
|
||||
type: FormDataTypeName;
|
||||
isInTriggerNode?: boolean;
|
||||
setter: string;
|
||||
setterProps?: {
|
||||
defaultValue?: any;
|
||||
size?: string;
|
||||
[k: string]: any;
|
||||
};
|
||||
tooltip?: string;
|
||||
hidden?: string | boolean; // '{{$values.tab === "time"}}'
|
||||
validation?: Validation | ValidationFn;
|
||||
otherAbilities?: IFormItemMeta['abilities'];
|
||||
otherMetaProps?: { [k: string]: any };
|
||||
otherDecoratorProps?: { [k: string]: any };
|
||||
}
|
||||
|
||||
export type FormMeta = FormItemMeta[];
|
||||
|
||||
// 表单值
|
||||
export interface FormValue {
|
||||
[TriggerFormIsOpenName]?: boolean;
|
||||
[TriggerFormEventTypeName]?: TriggerFormEventType;
|
||||
[k: string]: any;
|
||||
}
|
||||
}
|
||||
|
||||
export enum CronJobType {
|
||||
Cronjob = 'cronjob',
|
||||
Selecting = 'selecting',
|
||||
}
|
||||
|
||||
export interface CronJobValue {
|
||||
type?: CronJobType;
|
||||
content?: ValueExpression;
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
export const mockLLMModels = {
|
||||
code: 0,
|
||||
msg: 'success',
|
||||
data: {
|
||||
model_list: [
|
||||
{
|
||||
name: '豆包·1.5·Pro·32k',
|
||||
model_type: 1737521813,
|
||||
model_class: 2,
|
||||
model_icon: 'doubao_v2.png',
|
||||
model_input_price: 0,
|
||||
model_output_price: 0,
|
||||
model_quota: {
|
||||
token_limit: 32768,
|
||||
token_resp: 4096,
|
||||
token_system: 0,
|
||||
token_user_in: 32768,
|
||||
token_tools_in: 0,
|
||||
token_tools_out: 0,
|
||||
token_data: 0,
|
||||
token_history: 0,
|
||||
token_cut_switch: false,
|
||||
price_in: 0,
|
||||
price_out: 0,
|
||||
},
|
||||
model_name: 'ep-20250122125445-ck9wp',
|
||||
model_class_name: '豆包系列模型',
|
||||
is_offline: false,
|
||||
model_params: [
|
||||
{
|
||||
name: 'temperature',
|
||||
label: '生成随机性',
|
||||
desc: '- **temperature**: 调高温度会使得模型的输出更多样性和创新性,反之,降低温度会使输出内容更加遵循指令要求但减少多样性。建议不要与“Top p”同时调整。',
|
||||
type: 1,
|
||||
min: '0',
|
||||
max: '1',
|
||||
precision: 1,
|
||||
default_val: {
|
||||
default_val: '1',
|
||||
creative: '1',
|
||||
balance: '0.8',
|
||||
precise: '0.3',
|
||||
},
|
||||
options: [],
|
||||
param_class: {
|
||||
class_id: 1,
|
||||
label: '生成多样性',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: '最大回复长度',
|
||||
desc: '控制模型输出的Tokens 长度上限。通常 100 Tokens 约等于 150 个中文汉字。',
|
||||
type: 2,
|
||||
min: '1',
|
||||
max: '12288',
|
||||
precision: 0,
|
||||
default_val: {
|
||||
default_val: '4096',
|
||||
},
|
||||
options: [],
|
||||
param_class: {
|
||||
class_id: 2,
|
||||
label: '输入及输出设置',
|
||||
},
|
||||
},
|
||||
],
|
||||
model_desc: [
|
||||
{
|
||||
group_name: '### 功能特点:',
|
||||
desc: [
|
||||
'- 支持Function calling能力(提供更准确、稳定的工具调用能力)',
|
||||
'- 输入的长度支持最长32768个Tokens(约49152个中文字符)',
|
||||
'- 节点ID:ep-20250122125445-ck9wp',
|
||||
],
|
||||
},
|
||||
],
|
||||
model_tag_list: [
|
||||
{
|
||||
tag_name: '文本模型',
|
||||
tag_class: 1,
|
||||
tag_icon: '',
|
||||
tag_descriptions: '',
|
||||
},
|
||||
{
|
||||
tag_name: '旗舰',
|
||||
tag_class: 3,
|
||||
tag_icon: '',
|
||||
tag_descriptions: '',
|
||||
},
|
||||
{
|
||||
tag_name: '工具调用',
|
||||
tag_class: 16,
|
||||
tag_icon: '',
|
||||
tag_descriptions: '',
|
||||
},
|
||||
],
|
||||
is_up_required: false,
|
||||
model_brief_desc:
|
||||
'Doubao-1.5-pro-32k,全新一代主力模型,性能全面升级,在知识、代码、推理、等方面表现卓越。支持32k上下文窗口,输出长度支持最大12k tokens。',
|
||||
model_series: {
|
||||
series_name: '热门模型',
|
||||
icon_url: 'doubao_v2.png',
|
||||
model_vendor: '扣子',
|
||||
},
|
||||
model_status_details: {
|
||||
is_new_model: false,
|
||||
is_advanced_model: false,
|
||||
is_free_model: false,
|
||||
is_upcoming_deprecated: false,
|
||||
deprecated_date: '',
|
||||
replace_model_name: '',
|
||||
update_info: '',
|
||||
model_feature: 1,
|
||||
},
|
||||
model_ability: {
|
||||
function_call: true,
|
||||
image_understanding: false,
|
||||
video_understanding: false,
|
||||
audio_understanding: false,
|
||||
},
|
||||
model_show_family_id: '7287388636726274',
|
||||
hot_flag: 1,
|
||||
hot_ranking: 100,
|
||||
online_time: 1737874220,
|
||||
offline_time: 0,
|
||||
},
|
||||
{
|
||||
name: '豆包·工具调用',
|
||||
model_type: 1706077826,
|
||||
model_class: 2,
|
||||
model_icon: 'doubao_v2.png',
|
||||
model_input_price: 0,
|
||||
model_output_price: 0,
|
||||
model_quota: {
|
||||
token_limit: 32768,
|
||||
token_resp: 4096,
|
||||
token_system: 0,
|
||||
token_user_in: 0,
|
||||
token_tools_in: 0,
|
||||
token_tools_out: 0,
|
||||
token_data: 0,
|
||||
token_history: 0,
|
||||
token_cut_switch: false,
|
||||
price_in: 0,
|
||||
price_out: 0,
|
||||
},
|
||||
model_name: 'ep-20250103114050-hgnz5',
|
||||
model_class_name: '豆包系列模型',
|
||||
is_offline: false,
|
||||
model_params: [
|
||||
{
|
||||
name: 'temperature',
|
||||
label: '生成随机性',
|
||||
desc: '- **temperature**: 调高温度会使得模型的输出更多样性和创新性,反之,降低温度会使输出内容更加遵循指令要求但减少多样性。建议不要与“Top p”同时调整。',
|
||||
type: 1,
|
||||
min: '0',
|
||||
max: '1',
|
||||
precision: 2,
|
||||
default_val: {
|
||||
default_val: '1',
|
||||
creative: '1',
|
||||
balance: '1',
|
||||
precise: '0.1',
|
||||
},
|
||||
options: [],
|
||||
param_class: {
|
||||
class_id: 1,
|
||||
label: '生成多样性',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'top_p',
|
||||
label: 'Top P',
|
||||
desc: '- **Top p 为累计概率**: 模型在生成输出时会从概率最高的词汇开始选择,直到这些词汇的总概率累积达到Top p 值。这样可以限制模型只选择这些高概率的词汇,从而控制输出内容的多样性。建议不要与“生成随机性”同时调整。',
|
||||
type: 1,
|
||||
min: '0',
|
||||
max: '1',
|
||||
precision: 2,
|
||||
default_val: {
|
||||
default_val: '0.7',
|
||||
creative: '0.8',
|
||||
balance: '0.7',
|
||||
precise: '0.7',
|
||||
},
|
||||
options: [],
|
||||
param_class: {
|
||||
class_id: 1,
|
||||
label: '生成多样性',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'response_format',
|
||||
label: '输出格式',
|
||||
desc: '- **文本**: 使用普通文本格式回复\n- **Markdown**: 将引导模型使用Markdown格式输出回复\n- **JSON**: 将引导模型使用JSON格式输出',
|
||||
type: 2,
|
||||
min: '',
|
||||
max: '',
|
||||
precision: 0,
|
||||
default_val: {
|
||||
default_val: '0',
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: '文本',
|
||||
value: '0',
|
||||
},
|
||||
{
|
||||
label: 'Markdown',
|
||||
value: '1',
|
||||
},
|
||||
],
|
||||
param_class: {
|
||||
class_id: 2,
|
||||
label: '输入及输出设置',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: '最大回复长度',
|
||||
desc: '控制模型输出的Tokens 长度上限。通常 100 Tokens 约等于 150 个中文汉字。',
|
||||
type: 2,
|
||||
min: '5',
|
||||
max: '4096',
|
||||
precision: 0,
|
||||
default_val: {
|
||||
default_val: '1024',
|
||||
},
|
||||
options: [],
|
||||
param_class: {
|
||||
class_id: 2,
|
||||
label: '输入及输出设置',
|
||||
},
|
||||
},
|
||||
],
|
||||
model_desc: [
|
||||
{
|
||||
group_name: '### 功能特点:',
|
||||
desc: [
|
||||
'- 支持Function calling能力(提供更准确、稳定的工具调用能力)',
|
||||
'- 节点ID:ep-20250103114050-hgnz5',
|
||||
],
|
||||
},
|
||||
],
|
||||
model_tag_list: [
|
||||
{
|
||||
tag_name: '文本模型',
|
||||
tag_class: 1,
|
||||
tag_icon: '',
|
||||
tag_descriptions: '',
|
||||
},
|
||||
{
|
||||
tag_name: '工具调用',
|
||||
tag_class: 3,
|
||||
tag_icon: '',
|
||||
tag_descriptions: '',
|
||||
},
|
||||
{
|
||||
tag_name: '支持微调',
|
||||
tag_class: 4,
|
||||
tag_icon: '',
|
||||
tag_descriptions: '',
|
||||
},
|
||||
{
|
||||
tag_name: 'functionCall',
|
||||
tag_class: 4,
|
||||
tag_icon: '',
|
||||
tag_descriptions: '',
|
||||
},
|
||||
{
|
||||
tag_name: '工具调用',
|
||||
tag_class: 16,
|
||||
tag_icon: '',
|
||||
tag_descriptions: '',
|
||||
},
|
||||
],
|
||||
is_up_required: false,
|
||||
model_brief_desc:
|
||||
'Doubao-pro-32k/241215,主力模型,适合处理复杂任务,在参考问答、总结摘要、创作、文本分类、角色扮演等场景都有很好的效果。支持32k上下文窗口的推理和精调。',
|
||||
model_series: {
|
||||
series_name: '豆包系列',
|
||||
icon_url: 'doubao_v2.png',
|
||||
model_vendor: '字节跳动',
|
||||
},
|
||||
model_status_details: {
|
||||
is_new_model: false,
|
||||
is_advanced_model: false,
|
||||
is_free_model: false,
|
||||
is_upcoming_deprecated: false,
|
||||
deprecated_date: '',
|
||||
replace_model_name: '',
|
||||
update_info: '',
|
||||
model_feature: 3,
|
||||
},
|
||||
model_ability: {
|
||||
function_call: true,
|
||||
image_understanding: false,
|
||||
video_understanding: false,
|
||||
audio_understanding: false,
|
||||
},
|
||||
model_show_family_id: '7287388636726274',
|
||||
hot_flag: 0,
|
||||
hot_ranking: 0,
|
||||
online_time: 1706247332,
|
||||
offline_time: 0,
|
||||
},
|
||||
],
|
||||
voice_list: null,
|
||||
raw_model_list: null,
|
||||
model_show_family_list: [
|
||||
{
|
||||
id: 7287388636726274,
|
||||
icon: 'MODEL_ICON/CUSTOM/af087406da7641beacf2b33c538d64fd_豆包.png',
|
||||
iconUrl: 'CUSTOM/af087406da7641beacf2b33c538d64fd_豆包.png',
|
||||
name: '豆包大模型',
|
||||
ranking: 1,
|
||||
},
|
||||
],
|
||||
default_model_id: 1737521813,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,661 @@
|
||||
/*
|
||||
* 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 mockSchemaForLLM = {
|
||||
nodes: [
|
||||
{
|
||||
blocks: [],
|
||||
data: {
|
||||
nodeMeta: {
|
||||
description: '工作流的起始节点,用于设定启动工作流需要的信息',
|
||||
icon: 'icon-Start-v2.jpg',
|
||||
subTitle: '',
|
||||
title: '开始',
|
||||
},
|
||||
outputs: [
|
||||
{
|
||||
name: 'input',
|
||||
required: false,
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'arr_input',
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
type: 'list',
|
||||
},
|
||||
{
|
||||
assistType: 2,
|
||||
name: 'img',
|
||||
required: false,
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
trigger_parameters: [
|
||||
{
|
||||
name: 'input',
|
||||
required: false,
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'arr_input',
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
type: 'list',
|
||||
},
|
||||
{
|
||||
assistType: 2,
|
||||
name: 'img',
|
||||
required: false,
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
},
|
||||
edges: null,
|
||||
id: '100001',
|
||||
meta: {
|
||||
position: {
|
||||
x: 180,
|
||||
y: 39,
|
||||
},
|
||||
},
|
||||
type: '1',
|
||||
},
|
||||
{
|
||||
blocks: [],
|
||||
data: {
|
||||
inputs: {
|
||||
inputParameters: [
|
||||
{
|
||||
input: {
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
type: 'list',
|
||||
value: {
|
||||
content: {
|
||||
blockID: '160281',
|
||||
name: 'output',
|
||||
source: 'block-output',
|
||||
},
|
||||
rawMeta: {
|
||||
type: 99,
|
||||
},
|
||||
type: 'ref',
|
||||
},
|
||||
},
|
||||
name: 'output',
|
||||
},
|
||||
],
|
||||
terminatePlan: 'returnVariables',
|
||||
},
|
||||
nodeMeta: {
|
||||
description: '工作流的最终节点,用于返回工作流运行后的结果信息',
|
||||
icon: 'icon-End-v2.jpg',
|
||||
subTitle: '',
|
||||
title: '结束',
|
||||
},
|
||||
},
|
||||
edges: null,
|
||||
id: '900001',
|
||||
meta: {
|
||||
position: {
|
||||
x: 1760,
|
||||
y: 26,
|
||||
},
|
||||
},
|
||||
type: '2',
|
||||
},
|
||||
{
|
||||
blocks: [],
|
||||
data: {
|
||||
inputs: {
|
||||
inputParameters: [
|
||||
{
|
||||
input: {
|
||||
type: 'string',
|
||||
value: {
|
||||
content: {
|
||||
blockID: '100001',
|
||||
name: 'input',
|
||||
source: 'block-output',
|
||||
},
|
||||
rawMeta: {
|
||||
type: 1,
|
||||
},
|
||||
type: 'ref',
|
||||
},
|
||||
},
|
||||
name: 'input',
|
||||
},
|
||||
],
|
||||
llmParam: [
|
||||
{
|
||||
input: {
|
||||
type: 'float',
|
||||
value: {
|
||||
content: '0.8',
|
||||
rawMeta: {
|
||||
type: 4,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'temperature',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
type: 'integer',
|
||||
value: {
|
||||
content: '4096',
|
||||
rawMeta: {
|
||||
type: 2,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'maxTokens',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
type: 'integer',
|
||||
value: {
|
||||
content: '2',
|
||||
rawMeta: {
|
||||
type: 2,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'responseFormat',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
type: 'string',
|
||||
value: {
|
||||
content: '豆包·1.5·Pro·32k',
|
||||
rawMeta: {
|
||||
type: 1,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'modleName',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
type: 'integer',
|
||||
value: {
|
||||
content: '1737521813',
|
||||
rawMeta: {
|
||||
type: 2,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'modelType',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
type: 'string',
|
||||
value: {
|
||||
content: 'balance',
|
||||
rawMeta: {
|
||||
type: 1,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'generationDiversity',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
type: 'string',
|
||||
value: {
|
||||
content: '{{input}}',
|
||||
rawMeta: {
|
||||
type: 1,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'prompt',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
type: 'boolean',
|
||||
value: {
|
||||
content: false,
|
||||
rawMeta: {
|
||||
type: 3,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'enableChatHistory',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
type: 'integer',
|
||||
value: {
|
||||
content: '3',
|
||||
rawMeta: {
|
||||
type: 2,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'chatHistoryRound',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
type: 'string',
|
||||
value: {
|
||||
content: '',
|
||||
rawMeta: {
|
||||
type: 1,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'systemPrompt',
|
||||
},
|
||||
],
|
||||
settingOnError: {
|
||||
processType: 1,
|
||||
retryTimes: 0,
|
||||
timeoutMs: 180000,
|
||||
},
|
||||
},
|
||||
nodeMeta: {
|
||||
description: '调用大语言模型,使用变量和提示词生成回复',
|
||||
icon: 'icon-LLM-v2.jpg',
|
||||
mainColor: '#5C62FF',
|
||||
subTitle: '大模型',
|
||||
title: '大模型',
|
||||
},
|
||||
outputs: [
|
||||
{
|
||||
name: 'output',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
version: '3',
|
||||
},
|
||||
edges: null,
|
||||
id: '181515',
|
||||
meta: {
|
||||
position: {
|
||||
x: 640,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
type: '3',
|
||||
},
|
||||
{
|
||||
blocks: [
|
||||
{
|
||||
blocks: [],
|
||||
data: {
|
||||
inputs: {
|
||||
inputParameters: [
|
||||
{
|
||||
input: {
|
||||
type: 'string',
|
||||
value: {
|
||||
content: {
|
||||
blockID: '100001',
|
||||
name: 'input',
|
||||
source: 'block-output',
|
||||
},
|
||||
rawMeta: {
|
||||
type: 1,
|
||||
},
|
||||
type: 'ref',
|
||||
},
|
||||
},
|
||||
name: 'input',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
assistType: 2,
|
||||
type: 'string',
|
||||
value: {
|
||||
content: {
|
||||
blockID: '100001',
|
||||
name: 'img',
|
||||
source: 'block-output',
|
||||
},
|
||||
rawMeta: {
|
||||
isVision: true,
|
||||
type: 7,
|
||||
},
|
||||
type: 'ref',
|
||||
},
|
||||
},
|
||||
name: 'img',
|
||||
},
|
||||
],
|
||||
llmParam: [
|
||||
{
|
||||
input: {
|
||||
type: 'float',
|
||||
value: {
|
||||
content: '0.8',
|
||||
rawMeta: {
|
||||
type: 4,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'temperature',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
type: 'float',
|
||||
value: {
|
||||
content: '0.7',
|
||||
rawMeta: {
|
||||
type: 4,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'topP',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
type: 'float',
|
||||
value: {
|
||||
content: '0',
|
||||
rawMeta: {
|
||||
type: 4,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'frequencyPenalty',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
type: 'integer',
|
||||
value: {
|
||||
content: '4096',
|
||||
rawMeta: {
|
||||
type: 2,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'maxTokens',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
type: 'integer',
|
||||
value: {
|
||||
content: '2',
|
||||
rawMeta: {
|
||||
type: 2,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'responseFormat',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
type: 'string',
|
||||
value: {
|
||||
content: '豆包·1.5·Pro·视觉推理·128K\t',
|
||||
rawMeta: {
|
||||
type: 1,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'modleName',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
type: 'integer',
|
||||
value: {
|
||||
content: '1745219190',
|
||||
rawMeta: {
|
||||
type: 2,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'modelType',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
type: 'string',
|
||||
value: {
|
||||
content: 'balance',
|
||||
rawMeta: {
|
||||
type: 1,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'generationDiversity',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
type: 'string',
|
||||
value: {
|
||||
content: '图片{{img}}中有什么?',
|
||||
rawMeta: {
|
||||
type: 1,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'prompt',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
type: 'boolean',
|
||||
value: {
|
||||
content: false,
|
||||
rawMeta: {
|
||||
type: 3,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'enableChatHistory',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
type: 'integer',
|
||||
value: {
|
||||
content: '3',
|
||||
rawMeta: {
|
||||
type: 2,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'chatHistoryRound',
|
||||
},
|
||||
{
|
||||
input: {
|
||||
type: 'string',
|
||||
value: {
|
||||
content: '',
|
||||
rawMeta: {
|
||||
type: 1,
|
||||
},
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
name: 'systemPrompt',
|
||||
},
|
||||
],
|
||||
settingOnError: {
|
||||
processType: 1,
|
||||
retryTimes: 0,
|
||||
timeoutMs: 180000,
|
||||
},
|
||||
},
|
||||
nodeMeta: {
|
||||
description: '调用大语言模型,使用变量和提示词生成回复',
|
||||
icon: 'icon-LLM-v2.jpg',
|
||||
mainColor: '#5C62FF',
|
||||
subTitle: '大模型',
|
||||
title: '大模型_1',
|
||||
},
|
||||
outputs: [
|
||||
{
|
||||
name: 'output',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'reasoning_content',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
version: '3',
|
||||
},
|
||||
edges: null,
|
||||
id: '189239',
|
||||
meta: {
|
||||
position: {
|
||||
x: 180,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
type: '3',
|
||||
},
|
||||
],
|
||||
data: {
|
||||
inputs: {
|
||||
inputParameters: [
|
||||
{
|
||||
input: {
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
type: 'list',
|
||||
value: {
|
||||
content: {
|
||||
blockID: '100001',
|
||||
name: 'arr_input',
|
||||
source: 'block-output',
|
||||
},
|
||||
rawMeta: {
|
||||
type: 99,
|
||||
},
|
||||
type: 'ref',
|
||||
},
|
||||
},
|
||||
name: 'input',
|
||||
},
|
||||
],
|
||||
loopCount: {
|
||||
type: 'integer',
|
||||
value: {
|
||||
content: '10',
|
||||
type: 'literal',
|
||||
},
|
||||
},
|
||||
loopType: 'array',
|
||||
variableParameters: [],
|
||||
},
|
||||
nodeMeta: {
|
||||
description: '用于通过设定循环次数和逻辑,重复执行一系列任务',
|
||||
icon: 'icon-Loop-v2.jpg',
|
||||
mainColor: '#00B2B2',
|
||||
subTitle: '循环',
|
||||
title: '循环',
|
||||
},
|
||||
outputs: [
|
||||
{
|
||||
input: {
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
type: 'list',
|
||||
value: {
|
||||
content: {
|
||||
blockID: '189239',
|
||||
name: 'output',
|
||||
source: 'block-output',
|
||||
},
|
||||
rawMeta: {
|
||||
type: 1,
|
||||
},
|
||||
type: 'ref',
|
||||
},
|
||||
},
|
||||
name: 'output',
|
||||
},
|
||||
],
|
||||
},
|
||||
edges: [
|
||||
{
|
||||
sourceNodeID: '160281',
|
||||
targetNodeID: '189239',
|
||||
sourcePortID: 'loop-function-inline-output',
|
||||
},
|
||||
{
|
||||
sourceNodeID: '189239',
|
||||
targetNodeID: '160281',
|
||||
sourcePortID: '',
|
||||
targetPortID: 'loop-function-inline-input',
|
||||
},
|
||||
],
|
||||
id: '160281',
|
||||
meta: {
|
||||
canvasPosition: {
|
||||
x: 1020,
|
||||
y: 331,
|
||||
},
|
||||
position: {
|
||||
x: 1200,
|
||||
y: 13,
|
||||
},
|
||||
},
|
||||
type: '21',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
sourceNodeID: '100001',
|
||||
targetNodeID: '181515',
|
||||
sourcePortID: '',
|
||||
},
|
||||
{
|
||||
sourceNodeID: '160281',
|
||||
targetNodeID: '900001',
|
||||
sourcePortID: 'loop-output',
|
||||
},
|
||||
{
|
||||
sourceNodeID: '181515',
|
||||
targetNodeID: '160281',
|
||||
sourcePortID: '',
|
||||
},
|
||||
],
|
||||
versions: {
|
||||
loop: 'v2',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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, type Mock } from 'vitest';
|
||||
import type { FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
import type { StandardNodeType } from '@coze-workflow/base/types';
|
||||
|
||||
import { addBasicNodeData } from '../add-node-data';
|
||||
import type { PlaygroundContext } from '../../typings';
|
||||
import { WorkflowNodeData } from '../../entity-datas';
|
||||
|
||||
// Mocks
|
||||
vi.mock('../../entity-datas', () => {
|
||||
const WorkflowNodeData1 = vi.fn();
|
||||
WorkflowNodeData1.prototype.getNodeData = vi.fn();
|
||||
WorkflowNodeData1.prototype.setNodeData = vi.fn();
|
||||
return { WorkflowNodeData: WorkflowNodeData1 };
|
||||
});
|
||||
|
||||
const mockGetNodeTemplateInfoByType = vi.fn();
|
||||
|
||||
describe('addBasicNodeData', () => {
|
||||
let mockNode: Partial<FlowNodeEntity>;
|
||||
let mockPlaygroundContext: Partial<PlaygroundContext>;
|
||||
let mockNodeDataEntity: WorkflowNodeData;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Re-instantiate mocks for WorkflowNodeData for each test
|
||||
mockNodeDataEntity = new WorkflowNodeData({} as any, {} as any);
|
||||
|
||||
mockNode = {
|
||||
flowNodeType: 'start' as StandardNodeType,
|
||||
getData: vi.fn().mockReturnValue(mockNodeDataEntity),
|
||||
};
|
||||
|
||||
mockPlaygroundContext = {
|
||||
getNodeTemplateInfoByType: mockGetNodeTemplateInfoByType,
|
||||
};
|
||||
});
|
||||
|
||||
it('should not set node data if nodeData already exists', () => {
|
||||
(mockNodeDataEntity.getNodeData as Mock).mockReturnValue({}); // Simulate existing data
|
||||
mockGetNodeTemplateInfoByType.mockReturnValue({
|
||||
icon: 'icon-path',
|
||||
description: 'description',
|
||||
title: 'title',
|
||||
mainColor: 'color',
|
||||
});
|
||||
|
||||
addBasicNodeData(
|
||||
mockNode as FlowNodeEntity,
|
||||
mockPlaygroundContext as PlaygroundContext,
|
||||
);
|
||||
|
||||
expect(mockNode.getData).toHaveBeenCalledWith(WorkflowNodeData);
|
||||
expect(mockNodeDataEntity.getNodeData).toHaveBeenCalled();
|
||||
expect(
|
||||
mockPlaygroundContext.getNodeTemplateInfoByType,
|
||||
).toHaveBeenCalledWith('start');
|
||||
expect(mockNodeDataEntity.setNodeData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not set node data if meta is undefined', () => {
|
||||
(mockNodeDataEntity.getNodeData as Mock).mockReturnValue(undefined); // Simulate no existing data
|
||||
mockGetNodeTemplateInfoByType.mockReturnValue(undefined); // Simulate meta not found
|
||||
|
||||
addBasicNodeData(
|
||||
mockNode as FlowNodeEntity,
|
||||
mockPlaygroundContext as PlaygroundContext,
|
||||
);
|
||||
|
||||
expect(mockNodeDataEntity.setNodeData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set node data if nodeData is undefined and meta is provided', () => {
|
||||
(mockNodeDataEntity.getNodeData as Mock).mockReturnValue(undefined); // Simulate no existing data
|
||||
const metaInfo = {
|
||||
icon: 'icon-path-new',
|
||||
description: 'new description',
|
||||
title: 'new title',
|
||||
mainColor: 'new-color',
|
||||
};
|
||||
mockGetNodeTemplateInfoByType.mockReturnValue(metaInfo);
|
||||
|
||||
addBasicNodeData(
|
||||
mockNode as FlowNodeEntity,
|
||||
mockPlaygroundContext as PlaygroundContext,
|
||||
);
|
||||
|
||||
expect(mockNodeDataEntity.setNodeData).toHaveBeenCalledWith({
|
||||
icon: metaInfo.icon,
|
||||
description: metaInfo.description,
|
||||
title: metaInfo.title,
|
||||
mainColor: metaInfo.mainColor,
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly get node type from node.flowNodeType', () => {
|
||||
(mockNodeDataEntity.getNodeData as Mock).mockReturnValue(undefined);
|
||||
const metaInfo = {
|
||||
icon: 'test',
|
||||
description: 'test',
|
||||
title: 'test',
|
||||
mainColor: 'test',
|
||||
};
|
||||
mockGetNodeTemplateInfoByType.mockReturnValue(metaInfo);
|
||||
(mockNode as FlowNodeEntity).flowNodeType =
|
||||
'customType' as StandardNodeType;
|
||||
|
||||
addBasicNodeData(
|
||||
mockNode as FlowNodeEntity,
|
||||
mockPlaygroundContext as PlaygroundContext,
|
||||
);
|
||||
|
||||
expect(
|
||||
mockPlaygroundContext.getNodeTemplateInfoByType,
|
||||
).toHaveBeenCalledWith('customType');
|
||||
expect(mockNodeDataEntity.setNodeData).toHaveBeenCalledWith(metaInfo);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { variableUtils } from '@coze-workflow/variable';
|
||||
import { ViewVariableType, type DTODefine } from '@coze-workflow/base';
|
||||
|
||||
import { getInputTypeBase, getInputType } from '../get-input-type';
|
||||
|
||||
// Mock @coze-workflow/variable
|
||||
vi.mock('@coze-workflow/variable', () => ({
|
||||
variableUtils: {
|
||||
dtoMetaToViewMeta: vi.fn(),
|
||||
// Mock other functions from variableUtils if needed by the tests or the module
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock @coze-workflow/base specifically for ViewVariableType.getLabel if it's complex
|
||||
// Otherwise, direct usage is fine if it's simple enum/object lookup
|
||||
vi.mock('@coze-workflow/base', async importOriginal => {
|
||||
const actual: object = await importOriginal();
|
||||
return {
|
||||
...actual, // Preserve other exports from @coze-workflow/base
|
||||
ViewVariableType: {
|
||||
...(actual as any).ViewVariableType,
|
||||
getLabel: vi.fn(type => `Label for ${type}`), // Simple mock for getLabel
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('getInputTypeBase', () => {
|
||||
it('should return correct structure for a given ViewVariableType', () => {
|
||||
const inputType = ViewVariableType.String;
|
||||
const result = getInputTypeBase(inputType);
|
||||
|
||||
expect(ViewVariableType.getLabel).toHaveBeenCalledWith(inputType);
|
||||
expect(result).toEqual({
|
||||
inputType: ViewVariableType.String,
|
||||
viewType: `Label for ${ViewVariableType.String}`,
|
||||
disabledTypes: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with different ViewVariableTypes', () => {
|
||||
const inputType = ViewVariableType.Number;
|
||||
getInputTypeBase(inputType);
|
||||
expect(ViewVariableType.getLabel).toHaveBeenCalledWith(inputType);
|
||||
|
||||
const inputTypeBool = ViewVariableType.Boolean;
|
||||
const resultBool = getInputTypeBase(inputTypeBool);
|
||||
expect(ViewVariableType.getLabel).toHaveBeenCalledWith(inputTypeBool);
|
||||
expect(resultBool.inputType).toBe(ViewVariableType.Boolean);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInputType', () => {
|
||||
it('should call dtoMetaToViewMeta and return result from getInputTypeBase', () => {
|
||||
const mockInputDTO = {
|
||||
id: 'test-id',
|
||||
name: 'test-name',
|
||||
} as unknown as DTODefine.InputVariableDTO;
|
||||
const mockViewMetaType = ViewVariableType.Integer;
|
||||
|
||||
(variableUtils.dtoMetaToViewMeta as any).mockReturnValue({
|
||||
type: mockViewMetaType,
|
||||
});
|
||||
|
||||
const result = getInputType(mockInputDTO);
|
||||
|
||||
expect(variableUtils.dtoMetaToViewMeta).toHaveBeenCalledWith(mockInputDTO);
|
||||
expect(ViewVariableType.getLabel).toHaveBeenCalledWith(mockViewMetaType);
|
||||
expect(result).toEqual({
|
||||
inputType: mockViewMetaType,
|
||||
viewType: `Label for ${mockViewMetaType}`,
|
||||
disabledTypes: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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, type Mock } from 'vitest';
|
||||
import type {
|
||||
WorkflowJSON,
|
||||
WorkflowDocument,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { StandardNodeType } from '@coze-workflow/base/types';
|
||||
|
||||
import { getLLMModelIds } from '../get-llm-model-ids';
|
||||
import { mockSchemaForLLM } from './__mocks__/mock-schema';
|
||||
|
||||
describe('getLLMModelIds (implicitly testing getLLMModelIdsByNodeJSON)', () => {
|
||||
let mockDocument: WorkflowDocument;
|
||||
let mockGetNodeRegistry: Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockGetNodeRegistry = vi.fn().mockReturnValue({
|
||||
meta: {
|
||||
getLLMModelIdsByNodeJSON: nodeJSON => {
|
||||
if (nodeJSON.type === StandardNodeType.Intent) {
|
||||
return nodeJSON?.data?.inputs?.llmParam?.modelType;
|
||||
}
|
||||
|
||||
if (nodeJSON.type === StandardNodeType.Question) {
|
||||
return nodeJSON?.data?.inputs?.llmParam?.modelType;
|
||||
}
|
||||
|
||||
if (nodeJSON.type === StandardNodeType.LLM) {
|
||||
return nodeJSON.data.inputs.llmParam.find(
|
||||
p => p.name === 'modelType',
|
||||
)?.input.value.content;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
});
|
||||
mockDocument = {
|
||||
getNodeRegistry: mockGetNodeRegistry,
|
||||
} as unknown as WorkflowDocument;
|
||||
});
|
||||
|
||||
it('should return empty array if document is empty', () => {
|
||||
const json: WorkflowJSON = { nodes: [], edges: [] };
|
||||
expect(getLLMModelIds(json, mockDocument)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array if json.nodes is empty', () => {
|
||||
const json: WorkflowJSON = { nodes: [], edges: [] };
|
||||
expect(getLLMModelIds(json, mockDocument)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return correct llm ids if json.nodes is not empty', () => {
|
||||
expect(
|
||||
getLLMModelIds(mockSchemaForLLM as unknown as WorkflowJSON, mockDocument),
|
||||
).toEqual(['1737521813', '1745219190']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* 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,
|
||||
afterEach,
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import type { WorkflowDocument } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
captureException,
|
||||
RESPONSE_FORMAT_NAME,
|
||||
ResponseFormat,
|
||||
StandardNodeType,
|
||||
} from '@coze-workflow/base';
|
||||
import { logger } from '@coze-arch/logger';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { DeveloperApi as developerApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { getLLMModels } from '../get-llm-models';
|
||||
import { mockSchemaForLLM } from './__mocks__/mock-schema';
|
||||
import { mockLLMModels } from './__mocks__/mock-models';
|
||||
|
||||
vi.mock('@coze-arch/bot-api', () => ({
|
||||
DeveloperApi: {
|
||||
GetTypeList: vi.fn(),
|
||||
},
|
||||
ModelScene: {
|
||||
Douyin: 'douyin_scene',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@coze-arch/logger', () => {
|
||||
const mockCreatedLogger = {
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
log: vi.fn(),
|
||||
};
|
||||
const mockLogger = {
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
log: vi.fn(),
|
||||
|
||||
createLoggerWith: vi.fn(() => mockCreatedLogger),
|
||||
};
|
||||
return {
|
||||
__esModule: true, // Indicates that this is an ES module mock
|
||||
default: mockLogger, // Mock for `import logger from '@coze-arch/logger'`
|
||||
logger: mockLogger, // Mock for `import { logger } from '@coze-arch/logger'`
|
||||
reporter: {
|
||||
createReporterWithPreset: vi.fn(),
|
||||
slardarInstance: vi.fn(),
|
||||
},
|
||||
createLoggerWith: vi.fn(() => mockCreatedLogger), // Mock for `import { createLoggerWith } from '@coze-arch/logger'`
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@coze-arch/i18n', () => ({
|
||||
I18n: {
|
||||
t: vi.fn(key => key), // Simple mock for I18n.t
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@coze-workflow/base', async () => {
|
||||
const actual = await vi.importActual('@coze-workflow/base');
|
||||
return {
|
||||
...actual,
|
||||
captureException: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockSpaceId = 'space-123';
|
||||
|
||||
describe('getLLMModels', () => {
|
||||
let mockDocument: WorkflowDocument;
|
||||
let mockGetNodeRegistry: Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetNodeRegistry = vi.fn().mockReturnValue({
|
||||
meta: {
|
||||
getLLMModelIdsByNodeJSON: nodeJSON => {
|
||||
if (nodeJSON.type === StandardNodeType.Intent) {
|
||||
return nodeJSON?.data?.inputs?.llmParam?.modelType;
|
||||
}
|
||||
|
||||
if (nodeJSON.type === StandardNodeType.Question) {
|
||||
return nodeJSON?.data?.inputs?.llmParam?.modelType;
|
||||
}
|
||||
|
||||
if (nodeJSON.type === StandardNodeType.LLM) {
|
||||
return nodeJSON.data.inputs.llmParam.find(
|
||||
p => p.name === 'modelType',
|
||||
)?.input.value.content;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
});
|
||||
mockDocument = {
|
||||
getNodeRegistry: mockGetNodeRegistry,
|
||||
} as unknown as WorkflowDocument;
|
||||
vi.mocked(developerApi.GetTypeList).mockResolvedValue(
|
||||
JSON.parse(JSON.stringify(mockLLMModels)),
|
||||
);
|
||||
vi.mocked(I18n.t).mockImplementation(key => key);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should fetch and process model list correctly', async () => {
|
||||
const mockInfo = { schema_json: JSON.stringify(mockSchemaForLLM) };
|
||||
|
||||
// Act
|
||||
const models = await getLLMModels({
|
||||
info: mockInfo,
|
||||
spaceId: mockSpaceId,
|
||||
document: mockDocument,
|
||||
isBindDouyin: false,
|
||||
});
|
||||
|
||||
expect(developerApi.GetTypeList).toHaveBeenCalledWith({
|
||||
space_id: mockSpaceId,
|
||||
model: true,
|
||||
cur_model_ids: ['1737521813', '1745219190'],
|
||||
});
|
||||
expect(models).toBeInstanceOf(Array);
|
||||
expect(models.length).toBeGreaterThan(0);
|
||||
|
||||
// Check repairResponseFormatInModelList logic
|
||||
models.forEach(model => {
|
||||
const responseFormatParam = model.model_params?.find(
|
||||
p => p.name === RESPONSE_FORMAT_NAME,
|
||||
);
|
||||
expect(responseFormatParam).toBeDefined();
|
||||
expect(responseFormatParam?.default_val?.default_val).toBe(
|
||||
ResponseFormat.JSON,
|
||||
);
|
||||
expect(responseFormatParam?.options).toEqual([
|
||||
{ label: 'model_config_history_text', value: ResponseFormat.Text },
|
||||
{
|
||||
label: 'model_config_history_markdown',
|
||||
value: ResponseFormat.Markdown,
|
||||
},
|
||||
{ label: 'model_config_history_json', value: ResponseFormat.JSON },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should set model_scene when isBindDouyin is true', async () => {
|
||||
const mockInfo = { schema_json: JSON.stringify(mockSchemaForLLM) };
|
||||
|
||||
// 上一个接口有 3s 缓存,需要等待 3s 后再调用
|
||||
await new Promise(resolve => setTimeout(resolve, 3100));
|
||||
|
||||
// Act
|
||||
await getLLMModels({
|
||||
info: mockInfo,
|
||||
spaceId: mockSpaceId,
|
||||
document: mockDocument,
|
||||
isBindDouyin: true,
|
||||
});
|
||||
|
||||
expect(developerApi.GetTypeList).toHaveBeenCalledWith({
|
||||
space_id: mockSpaceId,
|
||||
model: true,
|
||||
cur_model_ids: ['1737521813', '1745219190'],
|
||||
model_scene: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle API error gracefully', async () => {
|
||||
const mockInfo = { schema_json: JSON.stringify(mockSchemaForLLM) };
|
||||
const apiError = new Error('API Error');
|
||||
vi.mocked(developerApi.GetTypeList).mockRejectedValue(apiError);
|
||||
|
||||
// 上一个接口有 3s 缓存,需要等待 3s 后再调用
|
||||
await new Promise(resolve => setTimeout(resolve, 3100));
|
||||
|
||||
const models = await getLLMModels({
|
||||
info: mockInfo as any,
|
||||
spaceId: mockSpaceId,
|
||||
document: mockDocument,
|
||||
isBindDouyin: false,
|
||||
});
|
||||
|
||||
expect(models).toEqual([]);
|
||||
expect(logger.error).toHaveBeenCalledWith({
|
||||
error: apiError,
|
||||
eventName: 'api/bot/get_type_list fetch error',
|
||||
});
|
||||
expect(captureException).toHaveBeenCalledWith(expect.any(Error));
|
||||
expect(I18n.t).toHaveBeenCalledWith('workflow_detail_error_message', {
|
||||
msg: 'fetch error',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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 { getSortedInputParameters } from '../get-sorted-input-parameters';
|
||||
|
||||
interface TestInputItem {
|
||||
name?: string;
|
||||
required?: boolean;
|
||||
id: number; // Additional property for stable sort testing if names are same
|
||||
}
|
||||
|
||||
describe('getSortedInputParameters', () => {
|
||||
it('should return an empty array if inputs is null or undefined', () => {
|
||||
expect(getSortedInputParameters(null as any)).toEqual([]);
|
||||
expect(getSortedInputParameters(undefined as any)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an empty array if inputs is an empty array', () => {
|
||||
expect(getSortedInputParameters([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('should sort items with required=true before required=false', () => {
|
||||
const inputs: TestInputItem[] = [
|
||||
{ id: 1, name: 'a', required: false },
|
||||
{ id: 2, name: 'b', required: true },
|
||||
];
|
||||
const expected: TestInputItem[] = [
|
||||
{ id: 2, name: 'b', required: true },
|
||||
{ id: 1, name: 'a', required: false },
|
||||
];
|
||||
expect(getSortedInputParameters(inputs)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should sort items by name within each required group (true then false)', () => {
|
||||
const inputs: TestInputItem[] = [
|
||||
{ id: 1, name: 'z', required: false },
|
||||
{ id: 2, name: 'a', required: true },
|
||||
{ id: 3, name: 'x', required: false },
|
||||
{ id: 4, name: 'b', required: true },
|
||||
];
|
||||
const expected: TestInputItem[] = [
|
||||
{ id: 2, name: 'a', required: true },
|
||||
{ id: 4, name: 'b', required: true },
|
||||
{ id: 3, name: 'x', required: false },
|
||||
{ id: 1, name: 'z', required: false },
|
||||
];
|
||||
expect(getSortedInputParameters(inputs)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should treat items with undefined required as false by default', () => {
|
||||
const inputs: TestInputItem[] = [
|
||||
{ id: 1, name: 'a' }, // required is undefined
|
||||
{ id: 2, name: 'b', required: true },
|
||||
];
|
||||
const expected: TestInputItem[] = [
|
||||
{ id: 2, name: 'b', required: true },
|
||||
{ id: 1, name: 'a', required: false }, // Processed to required: false
|
||||
];
|
||||
expect(getSortedInputParameters(inputs)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle items with undefined names (they should be sorted according to lodash sortBy behavior)', () => {
|
||||
const inputs: TestInputItem[] = [
|
||||
{ id: 1, name: 'a', required: true },
|
||||
{ id: 2, required: true }, // name is undefined
|
||||
{ id: 3, name: 'b', required: false },
|
||||
{ id: 4, required: false }, // name is undefined
|
||||
];
|
||||
// Lodash sortBy typically places undefined values first when sorting in ascending order.
|
||||
const expected: TestInputItem[] = [
|
||||
{ id: 1, name: 'a', required: true },
|
||||
{ id: 2, required: true },
|
||||
{ id: 3, name: 'b', required: false },
|
||||
{ id: 4, required: false },
|
||||
];
|
||||
expect(getSortedInputParameters(inputs)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should maintain original properties of items', () => {
|
||||
const inputs: TestInputItem[] = [
|
||||
{ id: 1, name: 'a', required: false, otherProp: 'value1' } as any,
|
||||
{ id: 2, name: 'b', required: true, otherProp: 'value2' } as any,
|
||||
];
|
||||
const result = getSortedInputParameters(inputs);
|
||||
expect(result[0]).toHaveProperty('otherProp', 'value2');
|
||||
expect(result[1]).toHaveProperty('otherProp', 'value1');
|
||||
});
|
||||
|
||||
it('should use custom groupKey and sortKey if provided (though the function signature does not expose this)', () => {
|
||||
const inputs: TestInputItem[] = [
|
||||
{ id: 1, name: 'a', required: false },
|
||||
{ id: 2, name: 'b', required: true },
|
||||
];
|
||||
const expected: TestInputItem[] = [
|
||||
{ id: 2, name: 'b', required: true },
|
||||
{ id: 1, name: 'a', required: false },
|
||||
];
|
||||
expect(getSortedInputParameters(inputs, 'required', 'name')).toEqual(
|
||||
expected,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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 } from 'vitest';
|
||||
import {
|
||||
GenerationDiversity,
|
||||
type InputValueDTO,
|
||||
VariableTypeDTO,
|
||||
} from '@coze-workflow/base';
|
||||
|
||||
import {
|
||||
formatModelData,
|
||||
getDefaultLLMParams,
|
||||
reviseLLMParamPair,
|
||||
} from '../llm-utils';
|
||||
import { mockLLMModels } from './__mocks__/mock-models';
|
||||
|
||||
const mockModels = mockLLMModels.data.model_list;
|
||||
|
||||
describe('llm-utils', () => {
|
||||
describe('formatModelData', () => {
|
||||
it('should convert string values to number based on modelMeta', () => {
|
||||
const model = {
|
||||
temperature: '0.8',
|
||||
maxTokens: '1024',
|
||||
modelType: '1737521813',
|
||||
otherParam: '保持字符串',
|
||||
};
|
||||
const modelMeta = mockModels[0];
|
||||
|
||||
const result = formatModelData(model, modelMeta);
|
||||
|
||||
expect(result.temperature).toBe(0.8);
|
||||
expect(result.maxTokens).toBe(1024);
|
||||
expect(result.modelType).toBe('1737521813');
|
||||
expect(result.otherParam).toBe('保持字符串');
|
||||
});
|
||||
|
||||
it('should return original value when modelMeta is undefined', () => {
|
||||
const model = { temperature: '0.8' };
|
||||
const result = formatModelData(model, undefined);
|
||||
expect(result.temperature).toBe('0.8');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefaultLLMParams', () => {
|
||||
it('should select default model by DEFAULT_MODEL_TYPE', () => {
|
||||
const params = getDefaultLLMParams(mockModels);
|
||||
|
||||
expect(params.modelType).toBe(mockModels[0].model_type);
|
||||
expect(params.modelName).toBe(mockModels[0].name);
|
||||
expect(params.generationDiversity).toBe(GenerationDiversity.Balance);
|
||||
expect(params.temperature).toBe(0.8); // 来自mockModels[0]的balance默认值
|
||||
});
|
||||
});
|
||||
|
||||
describe('reviseLLMParamPair', () => {
|
||||
it('should fix typo "modleName" to "modelName"', () => {
|
||||
const input = {
|
||||
name: 'modleName',
|
||||
input: {
|
||||
type: VariableTypeDTO.string,
|
||||
value: { content: '豆包·1.5·Pro·32k' },
|
||||
},
|
||||
};
|
||||
const [key, value] = reviseLLMParamPair(input as InputValueDTO);
|
||||
expect(key).toBe('modelName');
|
||||
expect(value).toBe('豆包·1.5·Pro·32k');
|
||||
});
|
||||
|
||||
it('should convert number types to number', () => {
|
||||
const floatInput = {
|
||||
name: 'temperature',
|
||||
input: { type: VariableTypeDTO.float, value: { content: '0.8' } },
|
||||
};
|
||||
const [_, floatValue] = reviseLLMParamPair(floatInput as InputValueDTO);
|
||||
expect(floatValue).toBe(0.8);
|
||||
|
||||
const intInput = {
|
||||
name: 'maxTokens',
|
||||
input: { type: VariableTypeDTO.integer, value: { content: '1024' } },
|
||||
};
|
||||
const [__, intValue] = reviseLLMParamPair(intInput as InputValueDTO);
|
||||
expect(intValue).toBe(1024);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { getTriggerId, setTriggerId } from '../trigger-form';
|
||||
|
||||
describe('trigger-form', () => {
|
||||
it('should set and get a triggerId for a given workflowId', () => {
|
||||
const wfId = 'workflow123';
|
||||
const triggerId = 'triggerABC';
|
||||
|
||||
setTriggerId(wfId, triggerId);
|
||||
const retrievedTriggerId = getTriggerId(wfId);
|
||||
|
||||
expect(retrievedTriggerId).toBe(triggerId);
|
||||
});
|
||||
|
||||
it('should return undefined if a triggerId is not set for a workflowId', () => {
|
||||
const wfId = 'workflowUnset';
|
||||
const retrievedTriggerId = getTriggerId(wfId);
|
||||
|
||||
expect(retrievedTriggerId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should overwrite an existing triggerId if set again for the same workflowId', () => {
|
||||
const wfId = 'workflowOverwrite';
|
||||
const initialTriggerId = 'triggerInitial';
|
||||
const newTriggerId = 'triggerNew';
|
||||
|
||||
setTriggerId(wfId, initialTriggerId);
|
||||
expect(getTriggerId(wfId)).toBe(initialTriggerId); // Verify initial set
|
||||
|
||||
setTriggerId(wfId, newTriggerId);
|
||||
const retrievedTriggerId = getTriggerId(wfId);
|
||||
|
||||
expect(retrievedTriggerId).toBe(newTriggerId);
|
||||
});
|
||||
|
||||
it('should handle multiple workflowIds independently', () => {
|
||||
const wfId1 = 'workflowA';
|
||||
const triggerId1 = 'triggerA';
|
||||
const wfId2 = 'workflowB';
|
||||
const triggerId2 = 'triggerB';
|
||||
|
||||
setTriggerId(wfId1, triggerId1);
|
||||
setTriggerId(wfId2, triggerId2);
|
||||
|
||||
expect(getTriggerId(wfId1)).toBe(triggerId1);
|
||||
expect(getTriggerId(wfId2)).toBe(triggerId2);
|
||||
});
|
||||
|
||||
it('should handle empty string as workflowId and triggerId', () => {
|
||||
const wfId = '';
|
||||
const triggerId = '';
|
||||
|
||||
setTriggerId(wfId, triggerId);
|
||||
expect(getTriggerId(wfId)).toBe(triggerId);
|
||||
|
||||
const wfId2 = 'workflowC';
|
||||
const triggerId2 = '';
|
||||
setTriggerId(wfId2, triggerId2);
|
||||
expect(getTriggerId(wfId2)).toBe(triggerId2);
|
||||
|
||||
const wfId3 = '';
|
||||
const triggerId3 = 'triggerD';
|
||||
setTriggerId(wfId3, triggerId3); // This will overwrite the previous '' wfId
|
||||
expect(getTriggerId('')).toBe(triggerId3);
|
||||
});
|
||||
});
|
||||
51
frontend/packages/workflow/nodes/src/utils/add-node-data.ts
Normal file
51
frontend/packages/workflow/nodes/src/utils/add-node-data.ts
Normal 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 { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
type BasicStandardNodeTypes,
|
||||
type StandardNodeType,
|
||||
} from '@coze-workflow/base/types';
|
||||
|
||||
import { type PlaygroundContext } from '../typings';
|
||||
import { type NodeData, WorkflowNodeData } from '../entity-datas';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param node
|
||||
* @param data
|
||||
* 给基础类型节点设置节点数据,不要随意修改
|
||||
*/
|
||||
export const addBasicNodeData = (
|
||||
node: FlowNodeEntity,
|
||||
playgroundContext: PlaygroundContext,
|
||||
) => {
|
||||
const nodeDataEntity = node.getData<WorkflowNodeData>(WorkflowNodeData);
|
||||
const meta = playgroundContext.getNodeTemplateInfoByType(
|
||||
node.flowNodeType as StandardNodeType,
|
||||
);
|
||||
const nodeData = nodeDataEntity.getNodeData<keyof NodeData>();
|
||||
|
||||
// 在部分节点的 formMeta 方法,会重复执行,因此这里加个检测
|
||||
if (!nodeData && meta) {
|
||||
nodeDataEntity.setNodeData<BasicStandardNodeTypes>({
|
||||
icon: meta.icon,
|
||||
description: meta.description,
|
||||
title: meta.title,
|
||||
mainColor: meta.mainColor,
|
||||
});
|
||||
}
|
||||
};
|
||||
40
frontend/packages/workflow/nodes/src/utils/get-input-type.ts
Normal file
40
frontend/packages/workflow/nodes/src/utils/get-input-type.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { variableUtils } from '@coze-workflow/variable';
|
||||
import {
|
||||
type DTODefine,
|
||||
type VariableMetaDTO,
|
||||
ViewVariableType,
|
||||
} from '@coze-workflow/base';
|
||||
|
||||
export const getInputTypeBase = (inputType: ViewVariableType) => {
|
||||
const viewType = ViewVariableType.getLabel(inputType);
|
||||
|
||||
return {
|
||||
inputType,
|
||||
viewType,
|
||||
disabledTypes: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const getInputType = (input: DTODefine.InputVariableDTO) => {
|
||||
const { type: inputType } = variableUtils.dtoMetaToViewMeta(
|
||||
input as VariableMetaDTO,
|
||||
);
|
||||
|
||||
return getInputTypeBase(inputType);
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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 WorkflowNodeJSON,
|
||||
type WorkflowJSON,
|
||||
type WorkflowDocument,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { type WorkflowNodeRegistry } from '@coze-workflow/base';
|
||||
|
||||
/**
|
||||
* 根据node meta中定义的getLLMModelIdsByNodeJSON方法获取大模型id
|
||||
* @param nodeJSON
|
||||
* @param ids
|
||||
* @param document
|
||||
*/
|
||||
function getLLMModelIdsByNodeJSON(
|
||||
nodeJSON: WorkflowNodeJSON,
|
||||
ids: string[],
|
||||
document: WorkflowDocument,
|
||||
) {
|
||||
const registry = document.getNodeRegistry(
|
||||
nodeJSON.type,
|
||||
) as WorkflowNodeRegistry;
|
||||
|
||||
const res = registry?.meta?.getLLMModelIdsByNodeJSON?.(nodeJSON);
|
||||
|
||||
if (res) {
|
||||
const modelIds = Array.isArray(res) ? res : [res];
|
||||
modelIds.filter(Boolean).forEach(modelId => {
|
||||
const idstr = `${modelId}`;
|
||||
if (!ids.includes(idstr)) {
|
||||
ids.push(idstr);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (nodeJSON.blocks) {
|
||||
nodeJSON.blocks.forEach(block =>
|
||||
getLLMModelIdsByNodeJSON(block, ids, document),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型ids
|
||||
* @param json
|
||||
* @param document
|
||||
* @returns
|
||||
*/
|
||||
export function getLLMModelIds(
|
||||
json: WorkflowJSON,
|
||||
document: WorkflowDocument,
|
||||
): string[] {
|
||||
const ids = [];
|
||||
|
||||
if (!document) {
|
||||
return ids;
|
||||
}
|
||||
|
||||
json.nodes.forEach(node => {
|
||||
getLLMModelIdsByNodeJSON(node, ids, document);
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
180
frontend/packages/workflow/nodes/src/utils/get-llm-models.ts
Normal file
180
frontend/packages/workflow/nodes/src/utils/get-llm-models.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* 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 { QueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
captureException,
|
||||
RESPONSE_FORMAT_NAME,
|
||||
ResponseFormat,
|
||||
} from '@coze-workflow/base';
|
||||
import { logger } from '@coze-arch/logger';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { ModelScene } from '@coze-arch/bot-api/playground_api';
|
||||
import {
|
||||
type GetTypeListRequest,
|
||||
type Model,
|
||||
type ModelParameter,
|
||||
} from '@coze-arch/bot-api/developer_api';
|
||||
import { DeveloperApi as developerApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { getLLMModelIds } from './get-llm-model-ids';
|
||||
|
||||
/** 默认的 response format 值 */
|
||||
export const getDefaultResponseFormat = () => ({
|
||||
name: RESPONSE_FORMAT_NAME,
|
||||
label: I18n.t('model_config_response_format'),
|
||||
desc: I18n.t('model_config_response_format_explain'),
|
||||
type: 2,
|
||||
min: '',
|
||||
max: '',
|
||||
precision: 0,
|
||||
default_val: {
|
||||
default_val: '0',
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: I18n.t('model_config_history_text'),
|
||||
value: '0',
|
||||
},
|
||||
{
|
||||
label: I18n.t('model_config_history_markdown'),
|
||||
value: '1',
|
||||
},
|
||||
],
|
||||
param_class: {
|
||||
class_id: 2,
|
||||
},
|
||||
});
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: Infinity,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 1. 给模型列表中每个模型的 response_format 参数项补全
|
||||
* 2. 硬编码设置 response_format 的默认值为 JSON
|
||||
* @param modelList 模型列表
|
||||
* @returns 补全 response_format 参数后的模型列表
|
||||
*/
|
||||
const repairResponseFormatInModelList = (modelList: Model[]) => {
|
||||
// 找到模型列表中 model_params 的第一个 response_format 参数项
|
||||
// 这段代码从下边循环中提出来,不需要每次循环计算一次
|
||||
const modelHasResponseFormatItem = modelList
|
||||
.find(_m => _m.model_params?.find(p => p.name === RESPONSE_FORMAT_NAME))
|
||||
?.model_params?.find(p => p.name === RESPONSE_FORMAT_NAME);
|
||||
|
||||
return modelList.map(m => {
|
||||
// 兼容后端未刷带的数据,没有 responseFormat 就补上
|
||||
const responseFormat = m.model_params?.find(
|
||||
p => p?.name === RESPONSE_FORMAT_NAME,
|
||||
) as ModelParameter;
|
||||
|
||||
if (!responseFormat) {
|
||||
if (modelHasResponseFormatItem) {
|
||||
m.model_params?.push(modelHasResponseFormatItem as ModelParameter);
|
||||
} else {
|
||||
// 填充一个默认的 response_format 参数
|
||||
m.model_params?.push(getDefaultResponseFormat());
|
||||
}
|
||||
}
|
||||
|
||||
// 此时再找一次 responseFormat,因为上边补全了 responseFormat
|
||||
const newResponseFormat = m.model_params?.find(
|
||||
p => p?.name === RESPONSE_FORMAT_NAME,
|
||||
) as ModelParameter;
|
||||
|
||||
// 重置默认值为 JSON
|
||||
Object.keys(newResponseFormat?.default_val ?? {}).forEach(k => {
|
||||
newResponseFormat.default_val[k] = ResponseFormat.JSON;
|
||||
});
|
||||
|
||||
if (newResponseFormat) {
|
||||
// 重置选项,text markdown json 都要支持
|
||||
newResponseFormat.options = [
|
||||
{
|
||||
label: I18n.t('model_config_history_text'),
|
||||
value: ResponseFormat.Text,
|
||||
},
|
||||
{
|
||||
label: I18n.t('model_config_history_markdown'),
|
||||
value: ResponseFormat.Markdown,
|
||||
},
|
||||
{
|
||||
label: I18n.t('model_config_history_json'),
|
||||
value: ResponseFormat.JSON,
|
||||
},
|
||||
] as unknown as ModelParameter[];
|
||||
}
|
||||
|
||||
return m;
|
||||
});
|
||||
};
|
||||
|
||||
export const getLLMModels = async ({
|
||||
info,
|
||||
spaceId,
|
||||
document,
|
||||
isBindDouyin,
|
||||
}): Promise<Model[]> => {
|
||||
try {
|
||||
const modelList = await queryClient.fetchQuery({
|
||||
queryKey: ['llm-model'],
|
||||
queryFn: async () => {
|
||||
const schema = JSON.parse(info?.schema_json || '{}');
|
||||
|
||||
const llmModelIds = getLLMModelIds(schema, document);
|
||||
|
||||
const getTypeListParams: GetTypeListRequest = {
|
||||
space_id: spaceId,
|
||||
model: true,
|
||||
cur_model_ids: llmModelIds,
|
||||
};
|
||||
|
||||
if (isBindDouyin) {
|
||||
getTypeListParams.model_scene = ModelScene.Douyin;
|
||||
}
|
||||
|
||||
const resp = await developerApi.GetTypeList(getTypeListParams);
|
||||
const _modelList: Model[] = resp?.data?.model_list ?? [];
|
||||
|
||||
// 从这里开始到 return modelList 全是给后端擦屁股
|
||||
// 这里有 hard code ,需要把输出格式的默认值设置为 JSON
|
||||
return repairResponseFormatInModelList(_modelList);
|
||||
},
|
||||
staleTime: 3000,
|
||||
});
|
||||
return modelList;
|
||||
} catch (error) {
|
||||
logger.error({
|
||||
error: error as Error,
|
||||
eventName: 'api/bot/get_type_list fetch error',
|
||||
});
|
||||
// 上报js错误
|
||||
captureException(
|
||||
new Error(
|
||||
I18n.t('workflow_detail_error_message', {
|
||||
msg: 'fetch error',
|
||||
}),
|
||||
),
|
||||
);
|
||||
// 兜底返回空数组
|
||||
return [];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { groupBy, sortBy } from 'lodash-es';
|
||||
import { type DTODefine } from '@coze-workflow/base';
|
||||
|
||||
export type InputVariableDTO = DTODefine.InputVariableDTO;
|
||||
|
||||
/**
|
||||
* 对输入参数进行排序,然后按照 required 字段进行分组,必填的放最前边
|
||||
* @param inputs
|
||||
* @param groupKey
|
||||
* @param sortKey
|
||||
* @returns
|
||||
*/
|
||||
export const getSortedInputParameters = <
|
||||
T extends { name?: string; required?: boolean },
|
||||
>(
|
||||
inputs: T[],
|
||||
groupKey = 'required',
|
||||
sortKey = 'name',
|
||||
): T[] => {
|
||||
const processedItems = (inputs || []).map(item => ({
|
||||
...item,
|
||||
required: item.required !== undefined ? item.required : false, // 默认设置为 false
|
||||
}));
|
||||
|
||||
// 先按照 required 属性分组
|
||||
const grouped = groupBy(processedItems, groupKey);
|
||||
|
||||
// 在每个组内按照 name 属性进行排序
|
||||
const sortedTrueGroup = sortBy(grouped.true, sortKey) || [];
|
||||
const sortedFalseGroup = sortBy(grouped.false, sortKey) || [];
|
||||
|
||||
// 合并 true 分组和 false 分组
|
||||
const mergedArray = [...sortedTrueGroup, ...sortedFalseGroup];
|
||||
|
||||
return mergedArray;
|
||||
};
|
||||
30
frontend/packages/workflow/nodes/src/utils/index.ts
Normal file
30
frontend/packages/workflow/nodes/src/utils/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './node-utils';
|
||||
|
||||
export { getLLMModels } from './get-llm-models';
|
||||
|
||||
export { getInputType } from './get-input-type';
|
||||
export { addBasicNodeData } from './add-node-data';
|
||||
export { getTriggerId, setTriggerId } from './trigger-form';
|
||||
|
||||
export { getSortedInputParameters } from './get-sorted-input-parameters';
|
||||
export {
|
||||
formatModelData,
|
||||
getDefaultLLMParams,
|
||||
reviseLLMParamPair,
|
||||
} from './llm-utils';
|
||||
114
frontend/packages/workflow/nodes/src/utils/llm-utils.ts
Normal file
114
frontend/packages/workflow/nodes/src/utils/llm-utils.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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 { mapValues, keyBy, snakeCase, isString, camelCase } from 'lodash-es';
|
||||
import {
|
||||
GenerationDiversity,
|
||||
VariableTypeDTO,
|
||||
type InputValueDTO,
|
||||
} from '@coze-workflow/base';
|
||||
import { ModelParamType, type Model } from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
import { DEFAULT_MODEL_TYPE } from '../constants';
|
||||
|
||||
const getDefaultModels = (modelMeta: Model): Record<string, unknown> => {
|
||||
const defaultModel: Record<string, unknown> = {};
|
||||
|
||||
modelMeta?.model_params?.forEach(p => {
|
||||
const k = camelCase(p.name) as string;
|
||||
const { type } = p;
|
||||
|
||||
// 优先取平衡,自定义兜底
|
||||
const defaultValue =
|
||||
p.default_val[GenerationDiversity.Balance] ??
|
||||
p.default_val[GenerationDiversity.Customize];
|
||||
|
||||
if (defaultValue !== undefined) {
|
||||
if (
|
||||
[ModelParamType.Float, ModelParamType.Int].includes(type) ||
|
||||
['modelType'].includes(k)
|
||||
) {
|
||||
defaultModel[k] = Number(defaultValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return defaultModel;
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化模型数据,根据 modelMeta 将特定字符串转化成数字
|
||||
* @param model
|
||||
* @param modelMeta
|
||||
* @returns
|
||||
*/
|
||||
export const formatModelData = (
|
||||
model: Record<string, unknown>,
|
||||
modelMeta: Model | undefined,
|
||||
): Record<string, unknown> => {
|
||||
const modelParamMap = keyBy(modelMeta?.model_params ?? [], 'name');
|
||||
return mapValues(model, (value, key) => {
|
||||
const modelParam = modelParamMap[snakeCase(key)];
|
||||
if (!modelParam || !isString(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const { type } = modelParam;
|
||||
|
||||
if (
|
||||
[ModelParamType.Float, ModelParamType.Int].includes(type) ||
|
||||
['modelType'].includes(key)
|
||||
) {
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
};
|
||||
|
||||
export const getDefaultLLMParams = (
|
||||
models: Model[],
|
||||
): Record<string, unknown> => {
|
||||
const modelMeta =
|
||||
models.find(m => m.model_type === DEFAULT_MODEL_TYPE) ?? models[0];
|
||||
|
||||
const llmParam = {
|
||||
modelType: modelMeta?.model_type,
|
||||
modelName: modelMeta?.name,
|
||||
generationDiversity: GenerationDiversity.Balance,
|
||||
...getDefaultModels(modelMeta),
|
||||
};
|
||||
|
||||
return llmParam;
|
||||
};
|
||||
|
||||
export const reviseLLMParamPair = (d: InputValueDTO): [string, unknown] => {
|
||||
let k = d?.name || '';
|
||||
|
||||
if (k === 'modleName') {
|
||||
k = 'modelName';
|
||||
}
|
||||
let v = d.input.value.content;
|
||||
if (
|
||||
[VariableTypeDTO.float, VariableTypeDTO.integer].includes(
|
||||
d.input.type as VariableTypeDTO,
|
||||
)
|
||||
) {
|
||||
v = Number(d.input.value.content);
|
||||
}
|
||||
|
||||
return [k, v];
|
||||
};
|
||||
356
frontend/packages/workflow/nodes/src/utils/node-utils.ts
Normal file
356
frontend/packages/workflow/nodes/src/utils/node-utils.ts
Normal file
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
* 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 { isBoolean, isInteger, isNumber, isNil, get, set } from 'lodash-es';
|
||||
import {
|
||||
type SetterOrDecoratorContext,
|
||||
type IFormItemMeta,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { FlowNodeBaseType } from '@flowgram-adapter/free-layout-editor';
|
||||
import { nanoid } from '@flowgram-adapter/free-layout-editor';
|
||||
import { variableUtils } from '@coze-workflow/variable';
|
||||
import {
|
||||
type InputValueVO,
|
||||
type LiteralExpression,
|
||||
ValueExpressionType,
|
||||
BatchMode,
|
||||
type BatchDTO,
|
||||
type BatchVO,
|
||||
ViewVariableType,
|
||||
type BatchVOInputList,
|
||||
type ValueExpression,
|
||||
} from '@coze-workflow/base';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { settingOnErrorInit, settingOnErrorSave } from '../setting-on-error';
|
||||
import {
|
||||
DEFAULT_BATCH_CONCURRENT_SIZE,
|
||||
DEFAULT_BATCH_SIZE,
|
||||
} from '../constants';
|
||||
export namespace nodeUtils {
|
||||
export const INPUT_PARAMS_PATH = 'inputs.inputParameters';
|
||||
const BATCH_MODE_PATH = 'inputs.batchMode';
|
||||
const BATCH_PATH = 'inputs.batch';
|
||||
const SETTING_ON_ERROR_PATH = 'inputs.settingOnError';
|
||||
const NODE_SETTING_ON_ERROR_PATH = 'settingOnError';
|
||||
|
||||
export type MapToArrayHandler<MapItem, ArrayItem> = (
|
||||
key: string,
|
||||
value: MapItem,
|
||||
) => ArrayItem;
|
||||
export type ArrayToMapHandler<ArrayItem, MapItem> = (
|
||||
item: ArrayItem,
|
||||
) => MapItem;
|
||||
|
||||
export function mapToArray<
|
||||
MapItem = InputValueVO['input'],
|
||||
ArrayItem = InputValueVO,
|
||||
>(
|
||||
map: Record<string, MapItem>,
|
||||
handle: MapToArrayHandler<MapItem, ArrayItem>,
|
||||
) {
|
||||
return Object.keys(map).map((key: string) => handle(key, map[key]));
|
||||
}
|
||||
|
||||
export function arrayToMap<
|
||||
ArrayItem = InputValueVO,
|
||||
MapItem = InputValueVO['input'],
|
||||
>(
|
||||
array: ArrayItem[],
|
||||
key: keyof ArrayItem,
|
||||
handler: ArrayToMapHandler<ArrayItem, MapItem>,
|
||||
) {
|
||||
const map: Record<string, MapItem> = {};
|
||||
array.forEach((item: ArrayItem): void => {
|
||||
map[item[key] as string] = handler(item);
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
export function batchToDTO(
|
||||
batchVO: BatchVO | undefined,
|
||||
nodeFormContext: any,
|
||||
): BatchDTO | undefined {
|
||||
if (!batchVO) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
playgroundContext: { variableService },
|
||||
} = nodeFormContext;
|
||||
|
||||
const {
|
||||
batchSize = DEFAULT_BATCH_SIZE,
|
||||
concurrentSize = DEFAULT_BATCH_CONCURRENT_SIZE,
|
||||
inputLists,
|
||||
} = batchVO;
|
||||
const inputListsDTO = inputLists.map(inputList => ({
|
||||
name: inputList.name,
|
||||
input: variableUtils.valueExpressionToDTO(
|
||||
inputList.input,
|
||||
variableService,
|
||||
{
|
||||
node: nodeFormContext?.node,
|
||||
},
|
||||
),
|
||||
}));
|
||||
return {
|
||||
batchSize,
|
||||
concurrentSize,
|
||||
inputLists: inputListsDTO,
|
||||
};
|
||||
}
|
||||
|
||||
export function batchToVO(
|
||||
batchDTO: BatchDTO | undefined,
|
||||
nodeFormContext: any,
|
||||
): BatchVO | undefined {
|
||||
if (!batchDTO) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
playgroundContext: { variableService },
|
||||
} = nodeFormContext;
|
||||
const { batchSize, concurrentSize, inputLists } = batchDTO;
|
||||
const inputListsVO = (inputLists || []).map(inputList => ({
|
||||
name: inputList.name,
|
||||
id: inputList.id,
|
||||
input: variableUtils.valueExpressionToVO(
|
||||
inputList.input,
|
||||
variableService,
|
||||
),
|
||||
}));
|
||||
return {
|
||||
batchSize,
|
||||
concurrentSize,
|
||||
inputLists: inputListsVO as BatchVOInputList[],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 使用 variableUtils.valueExpressionToDTO)
|
||||
* @param value
|
||||
* @param nodeFormContext
|
||||
* @returns
|
||||
*/
|
||||
export function refExpressionToValueDTO(
|
||||
value: ValueExpression,
|
||||
nodeFormContext: any,
|
||||
) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
playgroundContext: { variableService },
|
||||
} = nodeFormContext;
|
||||
|
||||
return {
|
||||
input: variableUtils.valueExpressionToDTO(value, variableService, {
|
||||
node: nodeFormContext?.node,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 使用 variableUtils.valueExpressionToDTO
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
export function literalExpressionToValueDTO(value: LiteralExpression) {
|
||||
if (isNil(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
type: variableUtils.getLiteralExpressionValueDTOType(value.content),
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: !isNil(value.content) ? String(value.content) : '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getLiteralExpressionViewVariableType(
|
||||
content: LiteralExpression['content'],
|
||||
) {
|
||||
if (isNil(content)) {
|
||||
return ViewVariableType.String;
|
||||
}
|
||||
if (isInteger(content)) {
|
||||
return ViewVariableType.Integer;
|
||||
} else if (isNumber(content)) {
|
||||
return ViewVariableType.Number;
|
||||
} else if (isBoolean(content)) {
|
||||
return ViewVariableType.Boolean;
|
||||
} else {
|
||||
return ViewVariableType.String;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 使用 variableUtils.valueExpressionToVO
|
||||
* @param value
|
||||
* @param nodeFormContext
|
||||
* @returns
|
||||
*/
|
||||
export function refExpressionDTOToVO(value: any, nodeFormContext: any) {
|
||||
if (isNil(value)) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
playgroundContext: { variableService },
|
||||
} = nodeFormContext;
|
||||
return variableUtils.valueExpressionToVO(value.input, variableService);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 使用 variableUtils.valueExpressionToVO
|
||||
* @param input
|
||||
* @returns
|
||||
*/
|
||||
export function literalExpressionDTOToVO(input: any) {
|
||||
if (isNil(input)) {
|
||||
return;
|
||||
}
|
||||
const { type, value } = input;
|
||||
|
||||
return {
|
||||
type: 'literal',
|
||||
content: variableUtils.getLiteralValueWithType(type, value?.content),
|
||||
};
|
||||
}
|
||||
|
||||
// 获取batch表单项默认值
|
||||
export function getBatchInputListFormDefaultValue(index: number) {
|
||||
return {
|
||||
name: `item${index}`,
|
||||
id: nanoid(),
|
||||
input: {
|
||||
type: ValueExpressionType.REF,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 节点支持批量
|
||||
export function getBatchModeFormMeta(isBatchV2: boolean): IFormItemMeta {
|
||||
// TODO DELETE schemaGray 临时字段,后端灰度刷数据标记,全量后删除
|
||||
return {
|
||||
name: 'batchMode',
|
||||
type: 'string',
|
||||
default: 'single',
|
||||
abilities: [
|
||||
{
|
||||
type: 'setter',
|
||||
options: {
|
||||
key: 'Radio',
|
||||
type: 'button',
|
||||
options: [
|
||||
{
|
||||
value: 'single',
|
||||
label: I18n.t('workflow_batch_tab_single_radio'),
|
||||
},
|
||||
{
|
||||
value: 'batch',
|
||||
label: I18n.t('workflow_batch_tab_batch_radio'),
|
||||
disabled: (context: SetterOrDecoratorContext) => {
|
||||
const { node } = context;
|
||||
if (
|
||||
node.parent?.flowNodeType === FlowNodeBaseType.SUB_CANVAS
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'decorator',
|
||||
options: {
|
||||
key: 'FormCard',
|
||||
collapsible: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'visibility',
|
||||
options: {
|
||||
hidden: isBatchV2,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// formValueToDto & dtoToFormValue 只迁移了api-node中对inputParameters、batch的适配
|
||||
export function formValueToDto(value: any, context) {
|
||||
const inputParams = get(value, INPUT_PARAMS_PATH);
|
||||
const formattedInputParams = inputParams
|
||||
? nodeUtils.mapToArray(inputParams, (key, mapValue) => ({
|
||||
name: key,
|
||||
input: mapValue,
|
||||
}))
|
||||
: [];
|
||||
|
||||
const batchMode = get(value, BATCH_MODE_PATH);
|
||||
const batch = get(value, BATCH_PATH);
|
||||
|
||||
const formattedBatch =
|
||||
batchMode === BatchMode.Batch
|
||||
? {
|
||||
batchEnable: true,
|
||||
...nodeUtils.batchToDTO(batch, context),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
set(value, INPUT_PARAMS_PATH, formattedInputParams);
|
||||
set(value, BATCH_PATH, formattedBatch);
|
||||
set(value, BATCH_MODE_PATH, undefined);
|
||||
set(value, SETTING_ON_ERROR_PATH, settingOnErrorSave(value).settingOnError);
|
||||
return value;
|
||||
}
|
||||
|
||||
export function dtoToformValue(value, context) {
|
||||
const inputParams = get(value, INPUT_PARAMS_PATH);
|
||||
if (!inputParams || !Array.isArray(inputParams)) {
|
||||
return value;
|
||||
}
|
||||
const formattedInputParams = nodeUtils.arrayToMap(
|
||||
inputParams,
|
||||
'name',
|
||||
(arrayItem: InputValueVO) => arrayItem.input,
|
||||
);
|
||||
|
||||
const batch = get(value, BATCH_PATH);
|
||||
|
||||
const formattedBatchMode = batch?.batchEnable
|
||||
? BatchMode.Batch
|
||||
: BatchMode.Single;
|
||||
const formattedBatch = batch?.batchEnable
|
||||
? nodeUtils.batchToVO(batch, context)
|
||||
: undefined;
|
||||
|
||||
set(value, INPUT_PARAMS_PATH, formattedInputParams);
|
||||
set(value, BATCH_MODE_PATH, formattedBatchMode);
|
||||
set(value, BATCH_PATH, formattedBatch);
|
||||
set(
|
||||
value,
|
||||
NODE_SETTING_ON_ERROR_PATH,
|
||||
settingOnErrorInit(value).settingOnError,
|
||||
);
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
28
frontend/packages/workflow/nodes/src/utils/trigger-form.ts
Normal file
28
frontend/packages/workflow/nodes/src/utils/trigger-form.ts
Normal 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 为什么要这么维护 triggerId?
|
||||
* 新的流程 fetchStartNodeTriggerFormValue 时没有 triggerId ,初次保存后,后端返回 triggerId
|
||||
* 已经保存过的流程, fetchStartNodeTriggerFormValue 时,会返回 triggerId
|
||||
* 获取时机不同,把 triggerId 硬塞到 formData 中比较麻烦,所以直接维护在 cacheTriggerId 中
|
||||
*/
|
||||
const cacheTriggerId: Record<string, string> = {};
|
||||
export const setTriggerId = (wfId: string, triggerId: string) => {
|
||||
cacheTriggerId[wfId] = triggerId;
|
||||
};
|
||||
|
||||
export const getTriggerId = (wfId: string) => cacheTriggerId[wfId];
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, test, expect, vi } from 'vitest';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { codeEmptyValidator } from '../code-empty-validator';
|
||||
|
||||
// 模拟I18n.t方法
|
||||
vi.mock('@coze-arch/i18n', () => ({
|
||||
I18n: { t: vi.fn(key => `translated_${key}`) },
|
||||
}));
|
||||
|
||||
describe('codeEmptyValidator', () => {
|
||||
test('当value.code存在时返回true', () => {
|
||||
const result = codeEmptyValidator({ value: { code: 'some code' } });
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('当value.code不存在时返回翻译后的错误信息', () => {
|
||||
const result = codeEmptyValidator({ value: {} });
|
||||
expect(I18n.t).toHaveBeenCalledWith('workflow_running_results_error_code');
|
||||
expect(result).toBe('translated_workflow_running_results_error_code');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* 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 { type FormItemMaterialContext } from '@flowgram-adapter/free-layout-editor';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { nodeMetaValidator } from '../node-meta-validator';
|
||||
|
||||
// 模拟 I18n.t 方法
|
||||
vi.mock('@coze-arch/i18n', () => ({
|
||||
I18n: { t: vi.fn(key => `translated_${key}`) },
|
||||
}));
|
||||
|
||||
const mockNodesService = {
|
||||
getAllNodes: vi.fn(),
|
||||
getNodeTitle: vi.fn(node => node.title),
|
||||
};
|
||||
|
||||
const baseContext = {
|
||||
playgroundContext: {
|
||||
nodesService: mockNodesService,
|
||||
},
|
||||
} as FormItemMaterialContext;
|
||||
|
||||
describe('nodeMetaValidator', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return true for valid metadata with a unique title', () => {
|
||||
mockNodesService.getAllNodes.mockReturnValue([
|
||||
{ id: 'node2', title: 'AnotherNode' },
|
||||
]);
|
||||
const result = nodeMetaValidator({
|
||||
value: { title: 'ValidTitle' },
|
||||
context: baseContext,
|
||||
options: {},
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return an error string for an empty title', () => {
|
||||
// Ensure isTitleRepeated returns false to isolate schema validation
|
||||
mockNodesService.getAllNodes.mockReturnValue([]);
|
||||
const result = nodeMetaValidator({
|
||||
value: { title: '' },
|
||||
context: baseContext,
|
||||
options: {},
|
||||
});
|
||||
// The I18n.t mock for 'workflow_detail_node_name_error_empty' is associated with the .string({...}) definition.
|
||||
// This key is used when Zod creates the error message for the .min(1) rule.
|
||||
// The I18n.t call for 'workflow_node_title_duplicated' happens inside nodeMetaValidator
|
||||
// when the refine schema is built.
|
||||
expect(I18n.t).toHaveBeenCalledWith('workflow_node_title_duplicated');
|
||||
const parsedResult = JSON.parse(result as string);
|
||||
expect(parsedResult.issues[0].message).toBe(
|
||||
I18n.t('workflow_detail_node_name_error_empty'),
|
||||
);
|
||||
expect(parsedResult.issues[0].path).toEqual(['title']);
|
||||
});
|
||||
|
||||
it('should return an error string for a title exceeding max length', () => {
|
||||
// Ensure isTitleRepeated returns false to isolate schema validation
|
||||
mockNodesService.getAllNodes.mockReturnValue([]);
|
||||
const longTitle = 'a'.repeat(64);
|
||||
const result = nodeMetaValidator({
|
||||
value: { title: longTitle },
|
||||
context: baseContext,
|
||||
options: {},
|
||||
});
|
||||
// The I18n.t mock for 'workflow_derail_node_detail_title_max' is associated with the .regex({...}) definition.
|
||||
// This key is used when Zod creates the error message for the regex rule.
|
||||
// The I18n.t call for 'workflow_node_title_duplicated' happens inside nodeMetaValidator
|
||||
// when the refine schema is built.
|
||||
expect(I18n.t).toHaveBeenCalledWith('workflow_node_title_duplicated');
|
||||
const parsedResult = JSON.parse(result as string);
|
||||
expect(parsedResult.issues[0].message).toBe(
|
||||
I18n.t('workflow_derail_node_detail_title_max', { max: '63' }),
|
||||
);
|
||||
expect(parsedResult.issues[0].path).toEqual(['title']);
|
||||
});
|
||||
|
||||
it('should return an error string for a duplicated title', () => {
|
||||
mockNodesService.getAllNodes.mockReturnValue([
|
||||
{ id: 'node1', title: 'ExistingTitle' },
|
||||
{ id: 'node2', title: 'AnotherNode' },
|
||||
{ id: 'node2', title: 'ExistingTitle' }, // 这里模拟一个重复的标题
|
||||
]);
|
||||
const result = nodeMetaValidator({
|
||||
value: { title: 'ExistingTitle' },
|
||||
context: baseContext,
|
||||
options: {},
|
||||
});
|
||||
// The validator returns a stringified Zod error object when parsed.success is false.
|
||||
// The 'workflow_node_title_duplicated' message comes from the refine function.
|
||||
const parsedResult = JSON.parse(result as string);
|
||||
expect(parsedResult.issues[0].message).toBe(
|
||||
I18n.t('workflow_node_title_duplicated'),
|
||||
);
|
||||
expect(parsedResult.issues[0].path).toEqual(['title']);
|
||||
// Ensure I18n.t was called for the duplication message within the refine logic.
|
||||
expect(I18n.t).toHaveBeenCalledWith('workflow_node_title_duplicated');
|
||||
});
|
||||
|
||||
it('should return true for valid metadata with optional fields', () => {
|
||||
mockNodesService.getAllNodes.mockReturnValue([]);
|
||||
const result = nodeMetaValidator({
|
||||
value: {
|
||||
title: 'ValidTitleWithExtras',
|
||||
icon: 'icon.png',
|
||||
subtitle: 'A subtitle',
|
||||
description: 'A description',
|
||||
},
|
||||
context: baseContext,
|
||||
options: {},
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if title is empty when checking for duplicates (isTitleRepeated returns false)', () => {
|
||||
// isTitleRepeated returns false if title is empty, so it should pass this specific check
|
||||
// but it will fail the .min(1) check from Zod schema
|
||||
mockNodesService.getAllNodes.mockReturnValue([
|
||||
{ id: 'node1', title: 'ExistingTitle' },
|
||||
]);
|
||||
const result = nodeMetaValidator({
|
||||
value: { title: '' }, // Empty title
|
||||
context: baseContext,
|
||||
options: {},
|
||||
});
|
||||
// This will fail the zod schema's .min(1) check first
|
||||
const parsedResult = JSON.parse(result as string);
|
||||
expect(parsedResult.issues[0].message).toBe(
|
||||
'translated_workflow_detail_node_name_error_empty',
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly handle nodesService.getNodeTitle returning different structure if applicable', () => {
|
||||
// This test is to ensure getNodeTitle mock is robust or to highlight if it needs adjustment
|
||||
// For example, if getNodeTitle actually expects a more complex node object
|
||||
const mockComplexNode = { id: 'nodeC', data: { name: 'ComplexNodeTitle' } };
|
||||
mockNodesService.getAllNodes.mockReturnValue([mockComplexNode]);
|
||||
// Adjusting the mock for getNodeTitle if it's more complex than just node.title
|
||||
const originalGetNodeTitle = mockNodesService.getNodeTitle;
|
||||
mockNodesService.getNodeTitle = vi.fn(node => node.data.name);
|
||||
|
||||
const result = nodeMetaValidator({
|
||||
value: { title: 'TestComplex' },
|
||||
context: baseContext,
|
||||
options: {},
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
|
||||
// Restore original mock
|
||||
mockNodesService.getNodeTitle = originalGetNodeTitle;
|
||||
});
|
||||
|
||||
it('should return true when title is not duplicated and nodes exist', () => {
|
||||
mockNodesService.getAllNodes.mockReturnValue([
|
||||
{ id: 'node1', title: 'AnotherTitle1' },
|
||||
{ id: 'node2', title: 'AnotherTitle2' },
|
||||
]);
|
||||
const result = nodeMetaValidator({
|
||||
value: { title: 'UniqueTitle' },
|
||||
context: baseContext,
|
||||
options: {},
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle when getAllNodes returns an empty array (no nodes)', () => {
|
||||
mockNodesService.getAllNodes.mockReturnValue([]);
|
||||
const result = nodeMetaValidator({
|
||||
value: { title: 'FirstNodeTitle' },
|
||||
context: baseContext,
|
||||
options: {},
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* 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 { ZodIssueCode } from 'zod';
|
||||
import { describe, it, vi, expect } from 'vitest';
|
||||
|
||||
import { questionOptionValidator } from '../question-option-validator';
|
||||
|
||||
// 模拟 I18n.t 方法
|
||||
vi.mock('@coze-arch/i18n', () => ({
|
||||
I18n: { t: vi.fn(key => `translated_${key}`) },
|
||||
}));
|
||||
|
||||
describe('questionOptionValidator', () => {
|
||||
it('should return true for a valid non-empty unique array', () => {
|
||||
const value = [
|
||||
{ name: 'Option 1', id: '1' },
|
||||
{ name: 'Option 2', id: '2' },
|
||||
];
|
||||
expect(
|
||||
questionOptionValidator({ value, context: {} as any, options: {} }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for an empty array', () => {
|
||||
const value: Array<{ name?: string; id: string }> = [];
|
||||
expect(
|
||||
questionOptionValidator({ value, context: {} as any, options: {} }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return error JSON for array with empty name', () => {
|
||||
const value = [
|
||||
{ name: 'Option 1', id: '1' },
|
||||
{ name: '', id: '2' },
|
||||
];
|
||||
const result = questionOptionValidator({
|
||||
value,
|
||||
context: {} as any,
|
||||
options: {},
|
||||
});
|
||||
expect(typeof result).toBe('string');
|
||||
const errors = JSON.parse(result as string);
|
||||
expect(errors.issues).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: ZodIssueCode.custom,
|
||||
message: 'translated_workflow_ques_option_notempty',
|
||||
path: [1],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return error JSON for array with whitespace name', () => {
|
||||
const value = [
|
||||
{ name: 'Option 1', id: '1' },
|
||||
{ name: ' ', id: '2' },
|
||||
];
|
||||
const result = questionOptionValidator({
|
||||
value,
|
||||
context: {} as any,
|
||||
options: {},
|
||||
});
|
||||
expect(typeof result).toBe('string');
|
||||
const errors = JSON.parse(result as string);
|
||||
expect(errors.issues).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: ZodIssueCode.custom,
|
||||
message: 'translated_workflow_ques_option_notempty',
|
||||
path: [1],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return error JSON for array with duplicate names', () => {
|
||||
const value = [
|
||||
{ name: 'Option 1', id: '1' },
|
||||
{ name: 'Option 1', id: '2' },
|
||||
];
|
||||
const result = questionOptionValidator({
|
||||
value,
|
||||
context: {} as any,
|
||||
options: {},
|
||||
});
|
||||
expect(typeof result).toBe('string');
|
||||
const errors = JSON.parse(result as string);
|
||||
expect(errors.issues).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: ZodIssueCode.custom,
|
||||
message: 'translated_workflow_ques_ans_testrun_dulpicate',
|
||||
path: [1],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return error JSON for array with both empty and duplicate names', () => {
|
||||
const value = [
|
||||
{ name: 'Option 1', id: '1' },
|
||||
{ name: '', id: '2' },
|
||||
{ name: 'Option 1', id: '3' },
|
||||
];
|
||||
const result = questionOptionValidator({
|
||||
value,
|
||||
context: {} as any,
|
||||
options: {},
|
||||
});
|
||||
expect(typeof result).toBe('string');
|
||||
const errors = JSON.parse(result as string);
|
||||
expect(errors.issues).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: ZodIssueCode.custom,
|
||||
message: 'translated_workflow_ques_option_notempty',
|
||||
path: [1],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: ZodIssueCode.custom,
|
||||
message: 'translated_workflow_ques_ans_testrun_dulpicate',
|
||||
path: [2],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return error JSON when value is undefined', () => {
|
||||
const value = undefined;
|
||||
const result = questionOptionValidator({
|
||||
value: value as any,
|
||||
context: {} as any,
|
||||
options: {},
|
||||
}); // Cast to any to bypass TS check for test
|
||||
expect(typeof result).toBe('string');
|
||||
const errors = JSON.parse(result as string);
|
||||
// Zod will throw a different type of error for undefined input on a non-optional schema
|
||||
expect(errors.issues).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: ZodIssueCode.invalid_type,
|
||||
expected: 'array',
|
||||
received: 'undefined',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return error JSON when value is null', () => {
|
||||
const value = null;
|
||||
const result = questionOptionValidator({
|
||||
value: value as any,
|
||||
context: {} as any,
|
||||
options: {},
|
||||
}); // Cast to any to bypass TS check for test
|
||||
expect(typeof result).toBe('string');
|
||||
const errors = JSON.parse(result as string);
|
||||
expect(errors.issues).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: ZodIssueCode.invalid_type,
|
||||
expected: 'array',
|
||||
received: 'null',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return error JSON for array with name missing (undefined)', () => {
|
||||
const value = [{ id: '1' }, { name: 'Option 2', id: '2' }]; // name is undefined for the first item
|
||||
const result = questionOptionValidator({
|
||||
value: value as any,
|
||||
context: {} as any,
|
||||
options: {},
|
||||
});
|
||||
expect(typeof result).toBe('string');
|
||||
const errors = JSON.parse(result as string);
|
||||
expect(errors.issues).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: ZodIssueCode.invalid_type, // Zod expects a string for name
|
||||
expected: 'string',
|
||||
received: 'undefined',
|
||||
path: [0, 'name'],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* 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 { settingOnErrorValidator } from '../setting-on-error-validator';
|
||||
|
||||
vi.mock('@flowgram-adapter/free-layout-editor', () => ({}));
|
||||
|
||||
vi.mock('../setting-on-error', () => ({
|
||||
SettingOnErrorProcessType: {
|
||||
STOP: 1,
|
||||
RETURN: 2,
|
||||
BACKUP: 3,
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock I18n.t
|
||||
vi.mock('@coze-arch/i18n', () => ({
|
||||
I18n: {
|
||||
t: vi.fn(key => key),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('settingOnErrorValidator', () => {
|
||||
it('should return true if value is undefined', () => {
|
||||
expect(settingOnErrorValidator({ value: undefined } as any)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if value is null', () => {
|
||||
expect(settingOnErrorValidator({ value: null } as any)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if settingOnErrorIsOpen is false', () => {
|
||||
const value = {
|
||||
settingOnErrorIsOpen: false,
|
||||
settingOnErrorJSON: '{"key":"value"}',
|
||||
processType: 1,
|
||||
};
|
||||
expect(settingOnErrorValidator({ value } as any)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if settingOnErrorIsOpen is not presented', () => {
|
||||
const value = {
|
||||
processType: 1,
|
||||
timeoutMs: 180000,
|
||||
retryTimes: 0,
|
||||
};
|
||||
expect(settingOnErrorValidator({ value } as any)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if settingOnErrorIsOpen is not presented, and selected a backup model', () => {
|
||||
const value = {
|
||||
processType: 1,
|
||||
timeoutMs: 180000,
|
||||
retryTimes: 1,
|
||||
ext: {
|
||||
backupLLmParam: {
|
||||
temperature: '1',
|
||||
maxTokens: '2200',
|
||||
responseFormat: 2,
|
||||
modelName: 'DeepSeek-R1/250528',
|
||||
modelType: 1748588801,
|
||||
generationDiversity: 'default_val',
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(settingOnErrorValidator({ value } as any)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return error string if settingOnErrorIsOpen is true, processType is RETURN, and settingOnErrorJSON is invalid JSON', () => {
|
||||
const value = {
|
||||
settingOnErrorIsOpen: true,
|
||||
settingOnErrorJSON: 'invalid-json',
|
||||
processType: 2,
|
||||
timeoutMs: 180000,
|
||||
retryTimes: 1,
|
||||
ext: {
|
||||
backupLLmParam: {
|
||||
temperature: '1',
|
||||
maxTokens: '2200',
|
||||
responseFormat: 2,
|
||||
modelName: 'DeepSeek-R1/250528',
|
||||
modelType: 1748588801,
|
||||
generationDiversity: 'default_val',
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = settingOnErrorValidator({ value } as any);
|
||||
expect(result).toBeTypeOf('string');
|
||||
if (typeof result === 'string') {
|
||||
const parsedResult = JSON.parse(result);
|
||||
expect(parsedResult.issues[0].message).toBe(
|
||||
'workflow_exception_ignore_json_error',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return error string if settingOnErrorIsOpen is true, processType is RETURN, and settingOnErrorJSON is valid JSON', () => {
|
||||
const value = {
|
||||
settingOnErrorIsOpen: true,
|
||||
settingOnErrorJSON: '{\n "output": "hello"\n}',
|
||||
processType: 2,
|
||||
timeoutMs: 180000,
|
||||
retryTimes: 1,
|
||||
ext: {
|
||||
backupLLmParam: {
|
||||
temperature: '1',
|
||||
maxTokens: '2200',
|
||||
responseFormat: 2,
|
||||
modelName: 'DeepSeek-R1/250528',
|
||||
modelType: 1748588801,
|
||||
generationDiversity: 'default_val',
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = settingOnErrorValidator({ value } as any);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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 { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { systemVariableValidator } from '../system-variable-validator';
|
||||
|
||||
// Mock I18n
|
||||
vi.mock('@coze-arch/i18n', () => ({
|
||||
I18n: {
|
||||
t: vi.fn(key => {
|
||||
if (key === 'variable_240416_01') {
|
||||
return 'System variables cannot start with sys_';
|
||||
}
|
||||
return `translated_${key}`;
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockContext = {} as any; // ValidatorProps context is not used by this validator
|
||||
|
||||
describe('systemVariableValidator', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return true for a valid variable name', () => {
|
||||
const result = systemVariableValidator({
|
||||
value: 'my_variable',
|
||||
context: mockContext,
|
||||
options: {},
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for a variable name with leading/trailing spaces that is otherwise valid', () => {
|
||||
const result = systemVariableValidator({
|
||||
value: ' my_variable ',
|
||||
context: mockContext,
|
||||
options: {},
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return an error message for a variable name starting with "sys_"', () => {
|
||||
const result = systemVariableValidator({
|
||||
value: 'sys_variable',
|
||||
context: mockContext,
|
||||
options: {},
|
||||
});
|
||||
expect(result).toBe('System variables cannot start with sys_');
|
||||
expect(I18n.t).toHaveBeenCalledWith('variable_240416_01');
|
||||
});
|
||||
|
||||
it('should return an error message for a variable name starting with "sys_" even with leading/trailing spaces', () => {
|
||||
const result = systemVariableValidator({
|
||||
value: ' sys_variable ',
|
||||
context: mockContext,
|
||||
options: {},
|
||||
});
|
||||
expect(result).toBe('System variables cannot start with sys_');
|
||||
expect(I18n.t).toHaveBeenCalledWith('variable_240416_01');
|
||||
});
|
||||
|
||||
it('should return true for an empty string after trimming', () => {
|
||||
const result = systemVariableValidator({
|
||||
value: ' ',
|
||||
context: mockContext,
|
||||
options: {},
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for an empty string input', () => {
|
||||
const result = systemVariableValidator({
|
||||
value: '',
|
||||
context: mockContext,
|
||||
options: {},
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for a null input value', () => {
|
||||
const result = systemVariableValidator({
|
||||
value: null as any,
|
||||
context: mockContext,
|
||||
options: {},
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for an undefined input value', () => {
|
||||
const result = systemVariableValidator({
|
||||
value: undefined as any,
|
||||
context: mockContext,
|
||||
options: {},
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -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 { I18n } from '@coze-arch/i18n';
|
||||
|
||||
export function codeEmptyValidator({ value }) {
|
||||
const code = value?.code;
|
||||
if (!code) {
|
||||
return I18n.t('workflow_running_results_error_code');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
23
frontend/packages/workflow/nodes/src/validators/index.ts
Normal file
23
frontend/packages/workflow/nodes/src/validators/index.ts
Normal 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.
|
||||
*/
|
||||
|
||||
export { nodeMetaValidator } from './node-meta-validator';
|
||||
export { outputTreeValidator } from './output-tree-validator';
|
||||
export { systemVariableValidator } from './system-variable-validator';
|
||||
export { codeEmptyValidator } from './code-empty-validator';
|
||||
export { questionOptionValidator } from './question-option-validator';
|
||||
export { settingOnErrorValidator } from './setting-on-error-validator';
|
||||
export { inputTreeValidator } from './input-tree-validator';
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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 { z, type ZodSchema } from 'zod';
|
||||
import {
|
||||
ValueExpression,
|
||||
ValueExpressionType,
|
||||
type InputValueVO,
|
||||
} from '@coze-workflow/base/types';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type ValidatorProps } from '@flowgram-adapter/free-layout-editor';
|
||||
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
import { type PlaygroundContext } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { VARIABLE_NAME_REGEX } from '../constants';
|
||||
|
||||
type Path = string | number;
|
||||
|
||||
interface Issue {
|
||||
path: Path[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入树校验器
|
||||
*/
|
||||
export class InputTreeValidator {
|
||||
private node: FlowNodeEntity;
|
||||
private playgroundContext: PlaygroundContext;
|
||||
private issues: Issue[] = [];
|
||||
constructor(node: FlowNodeEntity, playgroundContext: PlaygroundContext) {
|
||||
this.node = node;
|
||||
this.playgroundContext = playgroundContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验函数
|
||||
* @param inputalues
|
||||
* @reurns
|
||||
*/
|
||||
validate(inputValues: InputValueVO[]): Issue[] {
|
||||
this.issues = [];
|
||||
this.validateInputValues(inputValues);
|
||||
return this.issues;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验多个输入
|
||||
* @param inputValues
|
||||
* @param path
|
||||
* @returns
|
||||
*/
|
||||
private validateInputValues(inputValues: InputValueVO[], path: Path[] = []) {
|
||||
if (!inputValues) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < inputValues.length; i++) {
|
||||
const inputValue = inputValues[i] || {};
|
||||
const rules = {
|
||||
name: this.validateName,
|
||||
input: this.validateInput,
|
||||
};
|
||||
|
||||
Object.keys(rules).forEach(key => {
|
||||
const message = rules[key].bind(this)({
|
||||
value: inputValue[key],
|
||||
values: inputValues,
|
||||
});
|
||||
|
||||
if (message) {
|
||||
this.issues.push({
|
||||
message,
|
||||
path: path.concat(i, key),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const children = inputValues[i]?.children || [];
|
||||
// 递归检查子节点
|
||||
this.validateInputValues(children, path.concat(i, 'children'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入名称校验
|
||||
*/
|
||||
private validateName({ value, values }) {
|
||||
if (!value) {
|
||||
return I18n.t('workflow_detail_node_error_name_empty');
|
||||
}
|
||||
|
||||
const names = values.map(v => v.name).filter(Boolean);
|
||||
// 名称格式校验
|
||||
if (!VARIABLE_NAME_REGEX.test(value)) {
|
||||
return I18n.t('workflow_detail_node_error_format');
|
||||
}
|
||||
|
||||
// 重名校验
|
||||
const foundSames = names.filter((name: string) => name === value);
|
||||
|
||||
return foundSames.length > 1
|
||||
? I18n.t('workflow_detail_node_input_duplicated')
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入值校验
|
||||
*/
|
||||
private validateInput({ value }) {
|
||||
const { variableValidationService } = this.playgroundContext;
|
||||
|
||||
// 校验空值
|
||||
if (ValueExpression.isEmpty(value)) {
|
||||
return I18n.t('workflow_detail_node_error_empty');
|
||||
}
|
||||
|
||||
if (value?.type === ValueExpressionType.REF) {
|
||||
return variableValidationService.isRefVariableEligible(value, this.node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function inputTreeValidator(params: ValidatorProps<InputValueVO>) {
|
||||
const {
|
||||
value,
|
||||
context: { playgroundContext, node },
|
||||
} = params;
|
||||
const validator = new InputTreeValidator(node, playgroundContext);
|
||||
|
||||
const InputTreeNodeSchema: ZodSchema<any> = z.lazy(() =>
|
||||
z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
input: z.any(),
|
||||
children: z.array(InputTreeNodeSchema).optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
);
|
||||
|
||||
const InputTreeSchema = z
|
||||
.array(InputTreeNodeSchema)
|
||||
.superRefine((data, ctx) => {
|
||||
const issues = validator.validate(data);
|
||||
|
||||
issues.forEach(issue => {
|
||||
ctx.addIssue({
|
||||
path: issue.path,
|
||||
message: issue.message,
|
||||
// FIXME: 表单校验底层依赖了 validation / code,去掉就跑不通了
|
||||
validation: 'regex',
|
||||
code: 'invalid_string',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const parsed = InputTreeSchema.safeParse(value);
|
||||
|
||||
if (!parsed.success) {
|
||||
return JSON.stringify((parsed as any).error);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 Ajv from 'ajv';
|
||||
import {
|
||||
variableUtils,
|
||||
generateInputJsonSchema,
|
||||
} from '@coze-workflow/variable';
|
||||
import { type ViewVariableMeta } from '@coze-workflow/base';
|
||||
let ajv;
|
||||
export const jsonSchemaValidator = (
|
||||
v: string,
|
||||
viewVariableMeta: ViewVariableMeta,
|
||||
): boolean => {
|
||||
if (!viewVariableMeta || !v) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const dtoMeta = variableUtils.viewMetaToDTOMeta(viewVariableMeta);
|
||||
const jsonSchema = generateInputJsonSchema(dtoMeta);
|
||||
if (!jsonSchema) {
|
||||
return true;
|
||||
}
|
||||
if (!ajv) {
|
||||
ajv = new Ajv();
|
||||
}
|
||||
try {
|
||||
const validate = ajv.compile(jsonSchema);
|
||||
const valid = validate(JSON.parse(v));
|
||||
return valid;
|
||||
// eslint-disable-next-line @coze-arch/use-error-in-catch
|
||||
} catch (error) {
|
||||
// parse失败说明不是合法值
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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 { z } from 'zod';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type ValidatorProps } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
const NodeMetaSchema = z.object({
|
||||
title: z
|
||||
.string({
|
||||
required_error: I18n.t('workflow_detail_node_name_error_empty'),
|
||||
})
|
||||
.min(1, I18n.t('workflow_detail_node_name_error_empty'))
|
||||
// .regex(
|
||||
// /^[a-zA-Z][a-zA-Z0-9_-]{0,}$/,
|
||||
// I18n.t('workflow_detail_node_error_format'),
|
||||
// )
|
||||
.regex(
|
||||
/^.{0,63}$/,
|
||||
I18n.t('workflow_derail_node_detail_title_max', {
|
||||
max: '63',
|
||||
}),
|
||||
),
|
||||
icon: z.string().optional(),
|
||||
subtitle: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
type NodeMeta = z.infer<typeof NodeMetaSchema>;
|
||||
|
||||
export const nodeMetaValidator = ({
|
||||
value,
|
||||
context,
|
||||
}: ValidatorProps<NodeMeta>) => {
|
||||
const { playgroundContext } = context;
|
||||
function isTitleRepeated(title: string) {
|
||||
if (!title) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { nodesService } = playgroundContext;
|
||||
const nodes = nodesService
|
||||
.getAllNodes()
|
||||
.filter(node => nodesService.getNodeTitle(node) === title);
|
||||
|
||||
return nodes?.length > 1;
|
||||
}
|
||||
|
||||
// 增加节点名重复校验
|
||||
const schema = NodeMetaSchema.refine(
|
||||
({ title }: NodeMeta) => !isTitleRepeated(title),
|
||||
{
|
||||
message: I18n.t('workflow_node_title_duplicated'),
|
||||
path: ['title'],
|
||||
},
|
||||
);
|
||||
const parsed = schema.safeParse(value);
|
||||
|
||||
if (!parsed.success) {
|
||||
return JSON.stringify((parsed as any).error);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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 ValidatorProps } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import {
|
||||
OutputTreeSchema,
|
||||
OutputTreeUniqueNameSchema,
|
||||
type OutputTree,
|
||||
} from './schema';
|
||||
|
||||
export function outputTreeValidator(
|
||||
params: ValidatorProps<
|
||||
OutputTree,
|
||||
{
|
||||
uniqueName?: boolean;
|
||||
}
|
||||
>,
|
||||
) {
|
||||
const { value, options } = params;
|
||||
const { uniqueName = false } = options;
|
||||
const parsed = uniqueName
|
||||
? OutputTreeUniqueNameSchema.safeParse(value)
|
||||
: OutputTreeSchema.safeParse(value);
|
||||
|
||||
if (!parsed.success) {
|
||||
return JSON.stringify((parsed as any).error);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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 { z, type ZodSchema } from 'zod';
|
||||
import { ViewVariableType } from '@coze-workflow/base';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { jsonSchemaValidator } from '../json-schema-validator';
|
||||
// 定义节点Schema
|
||||
const OutputTreeNodeSchema: ZodSchema<any> = z.lazy(() =>
|
||||
z
|
||||
.object({
|
||||
name: z
|
||||
.string({
|
||||
required_error: I18n.t('workflow_detail_node_error_name_empty'),
|
||||
})
|
||||
.min(1, I18n.t('workflow_detail_node_error_name_empty'))
|
||||
.regex(
|
||||
/^(?!.*\b(true|false|and|AND|or|OR|not|NOT|null|nil|If|Switch)\b)[a-zA-Z_][a-zA-Z_$0-9]*$/,
|
||||
I18n.t('workflow_detail_node_error_format'),
|
||||
),
|
||||
type: z.number(),
|
||||
children: z.array(OutputTreeNodeSchema).optional(),
|
||||
defaultValue: z.any().optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
);
|
||||
|
||||
export const OutputTreeSchema = z.array(OutputTreeNodeSchema);
|
||||
|
||||
// 定义一个辅助函数,用于查找重复名字的节点并返回错误路径
|
||||
const findDuplicates = (nodes, path = []) => {
|
||||
const seen = new Set();
|
||||
let result: {
|
||||
path: (string | number)[];
|
||||
message: string;
|
||||
};
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const { name } = nodes[i];
|
||||
if (seen.has(name)) {
|
||||
// 找到重复项时返回路径和错误信息
|
||||
result = {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
path: path.concat(i, 'name'),
|
||||
message: I18n.t('workflow_detail_node_error_variablename_duplicated'),
|
||||
};
|
||||
break;
|
||||
}
|
||||
seen.add(name);
|
||||
if (nodes[i].children) {
|
||||
// 递归检查子节点
|
||||
const found = findDuplicates(
|
||||
nodes[i].children,
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
path.concat(i, 'children'),
|
||||
);
|
||||
if (found) {
|
||||
result = found;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
return result;
|
||||
};
|
||||
|
||||
// 定义同级命名唯一的树结构Schema
|
||||
export const OutputTreeUniqueNameSchema = z
|
||||
.array(OutputTreeNodeSchema)
|
||||
.refine(
|
||||
data => {
|
||||
// 使用自定义函数进行检查
|
||||
const duplicate = findDuplicates(data);
|
||||
return !duplicate;
|
||||
},
|
||||
data => {
|
||||
// 使用自定义函数进行检查
|
||||
const duplicate = findDuplicates(data);
|
||||
return {
|
||||
path: duplicate.path,
|
||||
message: duplicate.message,
|
||||
// FIXME: 表单校验底层依赖了 validation / code,去掉就跑不通了
|
||||
validation: 'regex',
|
||||
code: 'invalid_string',
|
||||
};
|
||||
},
|
||||
)
|
||||
.superRefine((data, ctx) => {
|
||||
// 使用自定义函数进行检查
|
||||
const issues = checkObjectDefaultValue(data);
|
||||
issues.forEach(issue => {
|
||||
ctx.addIssue({
|
||||
path: issue.path,
|
||||
message: issue.message,
|
||||
// FIXME: 表单校验底层依赖了 validation / code,去掉就跑不通了
|
||||
validation: 'regex',
|
||||
code: 'invalid_string',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const checkObjectDefaultValue = nodes => {
|
||||
const result: Array<{
|
||||
path: (string | number)[];
|
||||
message: string;
|
||||
}> = [];
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const { defaultValue, type } = nodes[i];
|
||||
if (typeof defaultValue !== 'string' || !defaultValue) {
|
||||
continue;
|
||||
}
|
||||
if (!ViewVariableType.isJSONInputType(type)) {
|
||||
continue;
|
||||
}
|
||||
if (!jsonSchemaValidator(defaultValue, nodes[i])) {
|
||||
// 找到重复项时返回路径和错误信息
|
||||
result.push({
|
||||
path: [i, 'defaultValue'],
|
||||
message: I18n.t('workflow_debug_wrong_json'),
|
||||
});
|
||||
}
|
||||
// json 类型只检查第一层,不需要递归检查
|
||||
}
|
||||
return result;
|
||||
};
|
||||
// 导出类型别名
|
||||
export type OutputTree = z.infer<typeof OutputTreeSchema>;
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 { z, ZodIssueCode } from 'zod';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type ValidatorProps } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
// 自定义验证器,检查数组是否为空,并且没有重复的值
|
||||
const nonEmptyUniqueArray = z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
}),
|
||||
)
|
||||
.superRefine((val, ctx) => {
|
||||
const seenValues = new Set();
|
||||
|
||||
val.forEach((item, idx) => {
|
||||
// 检查非空
|
||||
if (item.name.trim() === '') {
|
||||
ctx.addIssue({
|
||||
code: ZodIssueCode.custom,
|
||||
message: I18n.t(
|
||||
'workflow_ques_option_notempty',
|
||||
{},
|
||||
'选项内容不可为空',
|
||||
),
|
||||
path: [idx],
|
||||
});
|
||||
}
|
||||
|
||||
// 检查重复
|
||||
if (seenValues.has(item.name)) {
|
||||
ctx.addIssue({
|
||||
code: ZodIssueCode.custom,
|
||||
message: I18n.t(
|
||||
'workflow_ques_ans_testrun_dulpicate',
|
||||
{},
|
||||
'选项内容不可重复',
|
||||
),
|
||||
path: [idx],
|
||||
});
|
||||
} else {
|
||||
seenValues.add(item.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export function questionOptionValidator({
|
||||
value,
|
||||
}: ValidatorProps<Array<{ name?: string; id: string }>>) {
|
||||
try {
|
||||
nonEmptyUniqueArray.parse(value);
|
||||
} catch (error) {
|
||||
return JSON.stringify(error);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -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 { z } from 'zod';
|
||||
import { type ValidatorProps } from '@flowgram-adapter/free-layout-editor';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { SettingOnErrorProcessType } from '../setting-on-error/types';
|
||||
|
||||
const SettingOnErrorSchema = z.object({
|
||||
settingOnErrorIsOpen: z.boolean().optional(),
|
||||
settingOnErrorJSON: z.string().optional(),
|
||||
processType: z.number().optional(),
|
||||
});
|
||||
|
||||
type SettingOnError = z.infer<typeof SettingOnErrorSchema>;
|
||||
|
||||
export const settingOnErrorValidator = ({
|
||||
value,
|
||||
}: ValidatorProps<SettingOnError>) => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function isJSONVerified(settingOnError: SettingOnError) {
|
||||
if (settingOnError?.settingOnErrorIsOpen) {
|
||||
if (
|
||||
settingOnError?.processType &&
|
||||
settingOnError?.processType !== SettingOnErrorProcessType.RETURN
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
JSON.parse(settingOnError?.settingOnErrorJSON as string);
|
||||
// eslint-disable-next-line @coze-arch/use-error-in-catch
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// json 合法性校验
|
||||
const schemeParesd = SettingOnErrorSchema.refine(
|
||||
settingOnError => isJSONVerified(settingOnError),
|
||||
{
|
||||
message: I18n.t('workflow_exception_ignore_json_error'),
|
||||
},
|
||||
).safeParse(value);
|
||||
|
||||
if (!schemeParesd.success) {
|
||||
return JSON.stringify((schemeParesd as any).error);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ValidatorProps } from '@flowgram-adapter/free-layout-editor';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
export function systemVariableValidator({ value }: ValidatorProps<string>) {
|
||||
const trimmed = value?.trim() || '';
|
||||
if (trimmed.startsWith('sys_')) {
|
||||
return I18n.t('variable_240416_01') || ' ';
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* 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 { injectable, multiInject, optional } from 'inversify';
|
||||
import {
|
||||
WorkflowDocument,
|
||||
FlowNodeBaseType,
|
||||
WorkflowLineEntity,
|
||||
type WorkflowNodeEntity,
|
||||
type WorkflowEdgeJSON,
|
||||
type WorkflowJSON,
|
||||
type WorkflowNodeJSON,
|
||||
WorkflowJSONFormatContribution,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { compose } from '@flowgram-adapter/common';
|
||||
|
||||
@injectable()
|
||||
export class WorkflowDocumentWithFormat extends WorkflowDocument {
|
||||
@multiInject(WorkflowJSONFormatContribution)
|
||||
@optional()
|
||||
protected jsonFormats: WorkflowJSONFormatContribution[] = [];
|
||||
|
||||
/**
|
||||
* 从数据加载
|
||||
* @param json
|
||||
*/
|
||||
fromJSON(json: Partial<WorkflowJSON>, fireRender = true): void {
|
||||
const { flattenJSON, nodeBlocks, nodeEdges } = this.flatJSON(json);
|
||||
const formattedJSON: WorkflowJSON = this.formatWorkflowJSON<WorkflowJSON>(
|
||||
flattenJSON,
|
||||
'formatOnInit',
|
||||
this,
|
||||
);
|
||||
const nestedJSON = this.nestJSON(formattedJSON, nodeBlocks, nodeEdges);
|
||||
super.fromJSON(nestedJSON, fireRender);
|
||||
}
|
||||
|
||||
private _formatCache = new Map<string | number | symbol, any>();
|
||||
|
||||
/**
|
||||
* 转换 json
|
||||
* @param json
|
||||
* @param formatKey
|
||||
* @param args
|
||||
* @protected
|
||||
*/
|
||||
protected formatWorkflowJSON<T>(
|
||||
json: T,
|
||||
formatKey: keyof WorkflowJSONFormatContribution,
|
||||
...args: any[]
|
||||
): T {
|
||||
if (this._formatCache.has(formatKey)) {
|
||||
return this._formatCache.get(formatKey)(json, ...args);
|
||||
}
|
||||
const fns: any[] = this.jsonFormats
|
||||
.map(format =>
|
||||
format[formatKey] ? format[formatKey]!.bind(format) : undefined,
|
||||
)
|
||||
.filter(f => Boolean(f));
|
||||
const fn = compose<T>(...fns);
|
||||
this._formatCache.set(formatKey, fn);
|
||||
return fn(json, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建流程节点
|
||||
* @param json
|
||||
*/
|
||||
createWorkflowNode(
|
||||
json: WorkflowNodeJSON,
|
||||
isClone?: boolean,
|
||||
parentId?: string,
|
||||
): WorkflowNodeEntity {
|
||||
json = this.formatWorkflowJSON<WorkflowNodeJSON>(
|
||||
json,
|
||||
'formatNodeOnInit',
|
||||
this,
|
||||
isClone,
|
||||
);
|
||||
return super.createWorkflowNode(json, isClone, parentId);
|
||||
}
|
||||
|
||||
toNodeJSON(node: WorkflowNodeEntity): WorkflowNodeJSON {
|
||||
const json = super.toNodeJSON(node);
|
||||
// 格式化
|
||||
const formattedJSON = this.formatWorkflowJSON<WorkflowNodeJSON>(
|
||||
json,
|
||||
'formatNodeOnSubmit',
|
||||
this,
|
||||
node,
|
||||
);
|
||||
return formattedJSON;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出数据
|
||||
*/
|
||||
toJSON(): WorkflowJSON {
|
||||
const rootJSON = this.toNodeJSON(this.root);
|
||||
const json = this.formatWorkflowJSON(
|
||||
{
|
||||
nodes: rootJSON.blocks ?? [],
|
||||
edges: rootJSON.edges ?? [],
|
||||
},
|
||||
'formatOnSubmit',
|
||||
this,
|
||||
);
|
||||
return json;
|
||||
}
|
||||
|
||||
private getEdgeID(edge: WorkflowEdgeJSON): string {
|
||||
return WorkflowLineEntity.portInfoToLineId({
|
||||
from: edge.sourceNodeID,
|
||||
to: edge.targetNodeID,
|
||||
fromPort: edge.sourcePortID,
|
||||
toPort: edge.targetPortID,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 拍平树形json结构,将结构信息提取到map
|
||||
*/
|
||||
private flatJSON(json: Partial<WorkflowJSON> = { nodes: [], edges: [] }): {
|
||||
flattenJSON: WorkflowJSON;
|
||||
nodeBlocks: Map<string, string[]>;
|
||||
nodeEdges: Map<string, string[]>;
|
||||
} {
|
||||
const nodeBlocks = new Map<string, string[]>();
|
||||
const nodeEdges = new Map<string, string[]>();
|
||||
const rootNodes = json.nodes ?? [];
|
||||
const rootEdges = json.edges ?? [];
|
||||
const flattenNodeJSONs: WorkflowNodeJSON[] = [...rootNodes];
|
||||
const flattenEdgeJSONs: WorkflowEdgeJSON[] = [...rootEdges];
|
||||
|
||||
const rootBlockIDs: string[] = rootNodes.map(node => node.id);
|
||||
const rootEdgeIDs: string[] = rootEdges.map(edge => this.getEdgeID(edge));
|
||||
|
||||
nodeBlocks.set(FlowNodeBaseType.ROOT, rootBlockIDs);
|
||||
nodeEdges.set(FlowNodeBaseType.ROOT, rootEdgeIDs);
|
||||
|
||||
// 如需支持多层结构,以下部分改为递归
|
||||
rootNodes.forEach(nodeJSON => {
|
||||
const { blocks, edges } = nodeJSON;
|
||||
if (blocks) {
|
||||
flattenNodeJSONs.push(...blocks);
|
||||
const blockIDs: string[] = [];
|
||||
blocks.forEach(block => {
|
||||
blockIDs.push(block.id);
|
||||
});
|
||||
nodeBlocks.set(nodeJSON.id, blockIDs);
|
||||
delete nodeJSON.blocks;
|
||||
}
|
||||
if (edges) {
|
||||
flattenEdgeJSONs.push(...edges);
|
||||
const edgeIDs: string[] = [];
|
||||
edges.forEach(edge => {
|
||||
const edgeID = this.getEdgeID(edge);
|
||||
edgeIDs.push(edgeID);
|
||||
});
|
||||
nodeEdges.set(nodeJSON.id, edgeIDs);
|
||||
delete nodeJSON.edges;
|
||||
}
|
||||
});
|
||||
|
||||
const flattenJSON: WorkflowJSON = {
|
||||
nodes: flattenNodeJSONs,
|
||||
edges: flattenEdgeJSONs,
|
||||
};
|
||||
|
||||
return {
|
||||
flattenJSON,
|
||||
nodeBlocks,
|
||||
nodeEdges,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 对JSON进行分层
|
||||
*/
|
||||
private nestJSON(
|
||||
flattenJSON: WorkflowJSON,
|
||||
nodeBlocks: Map<string, string[]>,
|
||||
nodeEdges: Map<string, string[]>,
|
||||
): WorkflowJSON {
|
||||
const nestJSON: WorkflowJSON = {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
};
|
||||
const nodeMap = new Map<string, WorkflowNodeJSON>();
|
||||
const edgeMap = new Map<string, WorkflowEdgeJSON>();
|
||||
const rootBlockSet = new Set<string>(
|
||||
nodeBlocks.get(FlowNodeBaseType.ROOT) ?? [],
|
||||
);
|
||||
const rootEdgeSet = new Set<string>(
|
||||
nodeEdges.get(FlowNodeBaseType.ROOT) ?? [],
|
||||
);
|
||||
|
||||
// 构造缓存
|
||||
flattenJSON.nodes.forEach(nodeJSON => {
|
||||
nodeMap.set(nodeJSON.id, nodeJSON);
|
||||
});
|
||||
|
||||
flattenJSON.edges.forEach(edgeJSON => {
|
||||
const edgeID = this.getEdgeID(edgeJSON);
|
||||
edgeMap.set(edgeID, edgeJSON);
|
||||
});
|
||||
|
||||
// 恢复层级数据
|
||||
flattenJSON.nodes.forEach(nodeJSON => {
|
||||
if (rootBlockSet.has(nodeJSON.id)) {
|
||||
nestJSON.nodes.push(nodeJSON);
|
||||
}
|
||||
// 恢复blocks
|
||||
if (nodeBlocks.has(nodeJSON.id)) {
|
||||
const blockIDs = nodeBlocks.get(nodeJSON.id)!;
|
||||
const blockJSONs: WorkflowNodeJSON[] = blockIDs
|
||||
.map(blockID => nodeMap.get(blockID)!)
|
||||
.filter(Boolean);
|
||||
nodeJSON.blocks = blockJSONs;
|
||||
}
|
||||
// 恢复edges
|
||||
if (nodeEdges.has(nodeJSON.id)) {
|
||||
const edgeIDs = nodeEdges.get(nodeJSON.id)!;
|
||||
const edgeJSONs: WorkflowEdgeJSON[] = edgeIDs
|
||||
.map(edgeID => edgeMap.get(edgeID)!)
|
||||
.filter(Boolean);
|
||||
nodeJSON.edges = edgeJSONs;
|
||||
}
|
||||
});
|
||||
|
||||
flattenJSON.edges.forEach(edgeJSON => {
|
||||
const edgeID = this.getEdgeID(edgeJSON);
|
||||
if (rootEdgeSet.has(edgeID)) {
|
||||
nestJSON.edges.push(edgeJSON);
|
||||
}
|
||||
});
|
||||
|
||||
return nestJSON;
|
||||
}
|
||||
}
|
||||
303
frontend/packages/workflow/nodes/src/workflow-json-format.ts
Normal file
303
frontend/packages/workflow/nodes/src/workflow-json-format.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
* 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, isEmpty, pick, set, isFunction } from 'lodash-es';
|
||||
import { injectable } from 'inversify';
|
||||
import {
|
||||
PlaygroundContext,
|
||||
lazyInject,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
type WorkflowNodeEntity,
|
||||
type WorkflowDocument,
|
||||
type WorkflowJSON,
|
||||
type WorkflowJSONFormatContribution,
|
||||
type WorkflowNodeJSON,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
WorkflowBatchService,
|
||||
WorkflowVariableService,
|
||||
variableUtils,
|
||||
type ViewVariableMeta,
|
||||
} from '@coze-workflow/variable';
|
||||
import {
|
||||
StandardNodeType,
|
||||
type BatchDTOInputList,
|
||||
type InputValueDTO,
|
||||
type InputValueVO,
|
||||
type ValueExpressionDTO,
|
||||
type VariableMetaDTO,
|
||||
} from '@coze-workflow/base';
|
||||
|
||||
import {
|
||||
WorkflowNodeVariablesMeta,
|
||||
type WorkflowNodeRegistry,
|
||||
} from './typings';
|
||||
|
||||
/**
|
||||
* 全局转换表单数据,这里主要处理变量生成逻辑
|
||||
*/
|
||||
@injectable()
|
||||
export class WorkflowJSONFormat implements WorkflowJSONFormatContribution {
|
||||
@lazyInject(WorkflowVariableService)
|
||||
declare variableService: WorkflowVariableService;
|
||||
@lazyInject(PlaygroundContext) declare playgroundContext: PlaygroundContext;
|
||||
@lazyInject(WorkflowBatchService) batchService: WorkflowBatchService;
|
||||
|
||||
protected getVariablesMeta(
|
||||
nodeType: string | number,
|
||||
document: WorkflowDocument,
|
||||
): WorkflowNodeVariablesMeta {
|
||||
const { variablesMeta = WorkflowNodeVariablesMeta.DEFAULT } =
|
||||
document.getNodeRegister<WorkflowNodeRegistry>(nodeType);
|
||||
return variablesMeta || WorkflowNodeVariablesMeta.DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换节点变量
|
||||
* @param json
|
||||
* @protected
|
||||
*/
|
||||
protected formatOutputVariables(
|
||||
json: WorkflowNodeJSON,
|
||||
doc: WorkflowDocument,
|
||||
): void {
|
||||
const variablesMeta = this.getVariablesMeta(json.type, doc);
|
||||
if (json.data) {
|
||||
variablesMeta.outputsPathList!.forEach(outputsPath => {
|
||||
const variableMetas = get(json.data, outputsPath) as VariableMetaDTO[];
|
||||
if (variableMetas && Array.isArray(variableMetas)) {
|
||||
variableMetas.forEach((meta: VariableMetaDTO, index) => {
|
||||
if (!meta.type) {
|
||||
return;
|
||||
}
|
||||
const newData = variableUtils.dtoMetaToViewMeta(meta);
|
||||
set(variableMetas, index, newData);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容仅有单个batch变量的历史数据
|
||||
*/
|
||||
protected transformBatchVariable(
|
||||
json: WorkflowNodeJSON,
|
||||
doc: WorkflowDocument,
|
||||
): void {
|
||||
if (!json.data) {
|
||||
return;
|
||||
}
|
||||
const input: ValueExpressionDTO = get(json.data, 'inputs.batch.inputList');
|
||||
if (!input || isEmpty(input)) {
|
||||
// 无需兼容
|
||||
return;
|
||||
}
|
||||
const inputList: BatchDTOInputList = {
|
||||
name: 'item', // 按照PRD,写死即可
|
||||
input,
|
||||
};
|
||||
// 在json中填充新数据
|
||||
json.data.inputs.batch.inputLists = [inputList];
|
||||
// 删掉旧数据
|
||||
delete json.data.inputs.batch.inputList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复变量引用逻辑
|
||||
* @param json
|
||||
* @param doc
|
||||
* @protected
|
||||
*/
|
||||
protected formatInputVariables(
|
||||
json: WorkflowNodeJSON,
|
||||
doc: WorkflowDocument,
|
||||
): void {
|
||||
const variablesMeta = this.getVariablesMeta(json.type, doc);
|
||||
if (json.data) {
|
||||
variablesMeta.inputsPathList!.forEach(inputsPath => {
|
||||
const inputValues: InputValueDTO[] = get(
|
||||
json.data,
|
||||
inputsPath,
|
||||
) as InputValueDTO[];
|
||||
if (inputValues && Array.isArray(inputValues)) {
|
||||
inputValues.map((inputValue, index) => {
|
||||
set(
|
||||
inputValues,
|
||||
index,
|
||||
variableUtils.inputValueToVO(inputValue, this.variableService),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理节点元数据 & 静态文案
|
||||
* @param json
|
||||
* @param doc
|
||||
* @protected
|
||||
*/
|
||||
protected formatNodeMeta(
|
||||
json: WorkflowNodeJSON,
|
||||
doc: WorkflowDocument,
|
||||
): void {
|
||||
// API 插件节点暂时不需要处理文案
|
||||
if (
|
||||
json?.type &&
|
||||
[StandardNodeType.Api, StandardNodeType.SubWorkflow].includes(
|
||||
json.type as StandardNodeType,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeMeta = get(json, 'data.nodeMeta');
|
||||
// 拉取的最新配置数据
|
||||
const meta = this.playgroundContext.getNodeTemplateInfoByType(json.type);
|
||||
|
||||
if (
|
||||
nodeMeta &&
|
||||
typeof nodeMeta === 'object' &&
|
||||
meta &&
|
||||
typeof meta === 'object'
|
||||
) {
|
||||
// 根据后端配置确定,不由用户控制节点元数据
|
||||
const staticMeta = pick(meta, ['icon', 'subTitle']);
|
||||
|
||||
set(json, 'data.nodeMeta', {
|
||||
...nodeMeta,
|
||||
...staticMeta,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化节点
|
||||
* @param json
|
||||
* @param doc
|
||||
* @param isClone
|
||||
*/
|
||||
formatNodeOnInit(
|
||||
json: WorkflowNodeJSON,
|
||||
doc: WorkflowDocument,
|
||||
isClone?: boolean,
|
||||
): WorkflowNodeJSON {
|
||||
// 非 clone 在 formatOnInit 已经触发
|
||||
if (!isClone) {
|
||||
return json;
|
||||
}
|
||||
this.formatOutputVariables(json, doc);
|
||||
this.formatInputVariables(json, doc);
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交节点
|
||||
* @param json
|
||||
* @param doc
|
||||
*/
|
||||
formatNodeOnSubmit(
|
||||
json: WorkflowNodeJSON,
|
||||
doc: WorkflowDocument,
|
||||
node: WorkflowNodeEntity,
|
||||
): WorkflowNodeJSON {
|
||||
const { nodeDTOType } = doc.getNodeRegister<WorkflowNodeRegistry>(
|
||||
json.type,
|
||||
).meta;
|
||||
const variablesMeta = this.getVariablesMeta(json.type, doc);
|
||||
if (json.data) {
|
||||
// 转换 output
|
||||
variablesMeta.outputsPathList!.forEach(outputsPath => {
|
||||
const variableMetas = get(json.data, outputsPath) as ViewVariableMeta[];
|
||||
if (variableMetas && Array.isArray(variableMetas)) {
|
||||
variableMetas.forEach((meta: ViewVariableMeta, index) => {
|
||||
if (!meta.type) {
|
||||
return;
|
||||
}
|
||||
const newData = variableUtils.viewMetaToDTOMeta(meta);
|
||||
set(variableMetas, index, newData);
|
||||
});
|
||||
}
|
||||
});
|
||||
// 转换 input
|
||||
variablesMeta.inputsPathList!.forEach(inputsPath => {
|
||||
const inputValues: InputValueVO[] = get(
|
||||
json.data,
|
||||
inputsPath,
|
||||
) as InputValueVO[];
|
||||
if (inputValues && Array.isArray(inputValues)) {
|
||||
inputValues.map((inputValue, index) => {
|
||||
if (!inputValue) {
|
||||
return;
|
||||
}
|
||||
set(
|
||||
inputValues,
|
||||
index,
|
||||
variableUtils.inputValueToDTO(inputValue, this.variableService, {
|
||||
node,
|
||||
}),
|
||||
);
|
||||
});
|
||||
// 过滤掉空值
|
||||
set(json.data, inputsPath, inputValues.filter(Boolean));
|
||||
}
|
||||
});
|
||||
}
|
||||
json.type = String(nodeDTOType || json.type);
|
||||
return json;
|
||||
}
|
||||
/**
|
||||
* 初始化时候转换变量数据
|
||||
* @param json
|
||||
* @param document
|
||||
*/
|
||||
formatOnInit(json: WorkflowJSON, document: WorkflowDocument): WorkflowJSON {
|
||||
// Step0: batch 兼容处理
|
||||
json.nodes.forEach(node => this.transformBatchVariable(node, document));
|
||||
|
||||
// Step1: 创建输出变量
|
||||
json.nodes.forEach(node => this.formatOutputVariables(node, document));
|
||||
|
||||
// Step2: 处理input中值转换
|
||||
json.nodes.forEach(node => this.formatInputVariables(node, document));
|
||||
|
||||
// Step3: 处理节点 description & subTitle 等静态文案数据 (不从落库数据中取,而是根据最新节点数据中获取)
|
||||
json.nodes.forEach(node => this.formatNodeMeta(node, document));
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交时候转换变量数据
|
||||
* @param json
|
||||
*/
|
||||
formatOnSubmit(json: WorkflowJSON, document: WorkflowDocument): WorkflowJSON {
|
||||
json.nodes = (json.nodes || []).map(node => {
|
||||
const registry = document.getNodeRegister<WorkflowNodeRegistry>(
|
||||
node.type,
|
||||
);
|
||||
if (isFunction(registry?.beforeNodeSubmit)) {
|
||||
return registry.beforeNodeSubmit(node);
|
||||
}
|
||||
return node;
|
||||
});
|
||||
|
||||
console.log('------------------ save ----------------------', json);
|
||||
return json;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ContainerModule } from 'inversify';
|
||||
import {
|
||||
WorkflowJSONFormatContribution,
|
||||
WorkflowDocument,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { bindContributions } from '@flowgram-adapter/common';
|
||||
|
||||
import { WorkflowJSONFormat } from './workflow-json-format';
|
||||
import { WorkflowDocumentWithFormat } from './workflow-document-with-format';
|
||||
import { WorkflowNodesService } from './service';
|
||||
|
||||
export const WorkflowNodesContainerModule = new ContainerModule(
|
||||
(bind, unbind, isBound, rebind) => {
|
||||
bind(WorkflowNodesService).toSelf().inSingletonScope();
|
||||
bindContributions(bind, WorkflowJSONFormat, [
|
||||
WorkflowJSONFormatContribution,
|
||||
]);
|
||||
// 这里兼容老的 画布 document
|
||||
bind(WorkflowDocumentWithFormat).toSelf().inSingletonScope();
|
||||
rebind(WorkflowDocument).toService(WorkflowDocumentWithFormat);
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user