feat: manually mirror opencoze's code from bytedance

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

View File

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

View File

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

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 { 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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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