feat: manually mirror opencoze's code from bytedance

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

View File

@@ -0,0 +1,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个中文字符',
'- 节点IDep-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能力提供更准确、稳定的工具调用能力',
'- 节点IDep-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,
},
};

View File

@@ -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',
},
};

View File

@@ -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);
});
});

View File

@@ -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,
});
});
});

View File

@@ -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']);
});
});

View File

@@ -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',
});
});
});

View File

@@ -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,
);
});
});

View File

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

View File

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

View File

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

View 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);
};

View File

@@ -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;
}

View 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 [];
}
};

View File

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

View File

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

View 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];
};

View 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;
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* 为什么要这么维护 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];