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,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 type ItemType<T> = T extends (infer U)[] ? U : T;
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type shortcut_command } from '@coze-arch/bot-api/playground_api';
import { type DSL } from '../../types';
import { type DSLFormFieldCommonProps } from '../../components/short-cut-panel/widgets/types';
import {
getDSLTemplate,
getFormItemDSLMap,
getFormItemPlaceholderDSL,
getLayoutDSL,
} from './templates';
// 通过 Components 生成完整的 DSL
export const getDSLFromComponents = (
params: shortcut_command.Components[],
): DSL => {
const formItemsDSL = params.map(getFormElementFromComponent);
const layoutDSL = getElementsLayout(formItemsDSL);
const template = getDSLTemplate();
template.elements.form?.children?.unshift(...layoutDSL.map(item => item.id));
[...formItemsDSL, ...layoutDSL].forEach(
item => (template.elements[item.id] = item),
);
// @ts-expect-error 支持直接传递 props
template.elements.submitButton.props.formFields = formItemsDSL.map(
item => item.id,
);
return template;
};
type DSLElement = DSL['elements'][string];
// 通过 Components 创建 DSL 中对应的表单元素 Element
export const getFormElementFromComponent = (
param: shortcut_command.Components,
): DSLElement => {
if (param.input_type !== undefined) {
return getFormItemDSLMap[param.input_type](param);
}
return getFormItemPlaceholderDSL();
};
// 用于生成 DSL 语法中双列布局的容器元素
const ITEMS_PER_LINE = 2;
export const getElementsLayout = (elements: DSLElement[]): DSLElement[] => {
const res: DSLElement[] = [];
const elementsCopy = [...elements];
// 如出现奇数个,最后一行占满
while (elementsCopy.length) {
res.push(getLayoutDSL(elementsCopy.splice(0, ITEMS_PER_LINE)));
}
return res;
};
export enum ElementType {
Input = '@flowpd/cici-components/Input',
}
export const findInputElementsWithDefault = (dsl: DSL) => {
if (!dsl?.elements) {
return [];
}
return Object.values(dsl.elements)
.map(c => {
const defaultValue = c?.props
?.defaultValue as DSLFormFieldCommonProps['defaultValue'];
return {
defaultValue: defaultValue?.value,
type: c.type,
id: c.id,
};
})
.filter(e => e.type === ElementType.Input && !!e.defaultValue);
};
export const findInputElementById = (dsl: DSL, id: string) => {
if (!dsl?.elements) {
return null;
}
return (
Object.values(dsl.elements)
.filter(e => e.type === ElementType.Input)
.find(e => e.id === id) ?? null
);
};

View File

@@ -0,0 +1,211 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 { shortcut_command } from '@coze-arch/bot-api/playground_api';
import { shortid } from '../uuid';
import { getAcceptByUploadItemTypes } from '../file-const';
import { type DSL, ElementDirectiveType, ElementPropsType } from '../../types';
// @fixme DSL 类型定义需要更新
export const getDSLTemplate = (): DSL => ({
elements: {
root: {
id: 'root',
name: 'Root',
type: '@flowpd/cici-components/PageContainer',
children: ['form'],
directives: {},
},
form: {
id: 'form',
name: 'FlowpdCiciComponentsForm', // TODO 组件名 & type 最好也能事先就约定好
type: '@flowpd/cici-components/Form',
props: {
value: {
type: ElementPropsType.EXPRESSION,
value: '{{formValue}}',
},
onChange: {
type: ElementPropsType.ACTION,
value: '{{onChange}}',
},
ruls: {
// some rules value
},
},
children: ['submitButton'], // 根据 variables + layout 生成
},
// 通过 getFormElementsFromcomponent 生成表单元素
// ...inputElements,
// 通过 getElementsLayout 生成表单元素 layout
// ...inputLayoutElements,
submitButton: {
id: 'submitButton',
name: 'FlowpdCiciComponentsButton', // TODO 组件名 & type 最好也能事先就约定好
type: '@flowpd/cici-components/Button',
props: {
onClick: {
type: ElementPropsType.ACTION,
value: '{{onSubmit}}',
},
},
},
},
rootID: 'root',
variables: {
formValue: {
id: 'formValue',
defaultValue: {}, // 不需要缺省值
},
},
actions: {
onSubmit: {
id: 'onSubmit',
type: 'submit',
data: {
type: ElementDirectiveType.EXPRESSION,
value: '{{formValue}}',
},
},
onChange: {
id: 'onChange',
type: 'updateVar',
target: 'formValue',
},
},
});
type DSLElement = DSL['elements'][string];
export type GetFormItemTemplate = (
component: shortcut_command.Components,
) => DSLElement;
export const getInputElementDSL: GetFormItemTemplate = component => ({
id: component.name ?? shortid(),
name: 'FlowpdCiciComponentsInput', // TODO 组件名最好也能事先就约定好
type: '@flowpd/cici-components/Input',
props: {
name: component.name,
description: component.description,
defaultValue: component.default_value,
rules: [],
noErrorMessage: true,
// @FIXME DSL 解析逻辑实际支持传入任意值作为 props但是类型定义里没有兼容这种语法
} as unknown as DSLElement['props'],
});
export const getSelectElementDSL: GetFormItemTemplate = component => ({
id: component.name ?? shortid(),
name: 'FlowpdCiciComponentsSelect', // TODO 组件名最好也能事先就约定好
type: '@flowpd/cici-components/Select',
props: {
name: component.name,
description: component.description,
defaultValue: component.default_value,
optionList: component.options?.map(value => ({
label: value,
value,
})),
rules: [
{
required: true,
},
],
noErrorMessage: true,
// @FIXME DSL 解析逻辑实际支持传入任意值作为 props但是类型定义里没有兼容这种语法
} as unknown as DSLElement['props'],
});
// 单位 kb
export const IMAGE_MAX_SIZE = 500 * 1024; // 500 mb
export const FILE_MAX_SIZE = 500 * 1024; // 500 mb
const getAcceptByComponent = (component: shortcut_command.Components) => {
let uploadItemTypes = [];
const { input_type, upload_options } = component;
if (input_type === shortcut_command.InputType.MixUpload) {
uploadItemTypes = upload_options ?? [];
} else {
uploadItemTypes = input_type ? [input_type] : [];
}
return getAcceptByUploadItemTypes(uploadItemTypes);
};
export const getUploadElementDSL: GetFormItemTemplate = component => ({
id: component.name ?? shortid(),
name: 'FlowpdCiciComponentsUpload', // TODO 组件名最好也能事先就约定好
type: '@flowpd/cici-components/Upload',
props: {
name: component.name,
description: component.description,
maxSize:
component.input_type === shortcut_command.InputType.UploadImage
? IMAGE_MAX_SIZE
: FILE_MAX_SIZE,
inputType: component.input_type,
accept: getAcceptByComponent(component),
rules: [
{
required: true,
},
],
noErrorMessage: true,
// @FIXME DSL 解析逻辑实际支持传入任意值作为 props但是类型定义里没有兼容这种语法
} as unknown as DSLElement['props'],
});
// 配置 & 预览模式占位符
export const getFormItemPlaceholderDSL = () => ({
id: shortid(),
name: 'FlowpdCiciComponentsFormItemPlaceholder',
type: '@flowpd/cici-components/Placeholder',
});
export const getLayoutDSL = (items: DSLElement[]): DSLElement => ({
id: shortid(),
name: 'FlowpdCiciComponentsColumnLayout',
type: '@flowpd/cici-components/ColumnLayout',
props: {
// 如出现奇数个,最后一行占满
Columns: items.map(item => ({
children: [item.id],
config: { vertical: 'top', weight: 1, width: 'weighted' },
type: 'slot',
})),
action: 'enableUrl',
backgroundColor: 'transparent',
enableClickEvent: false,
// @FIXME card-buidler Layout 现有的数据结构,但是实际上类型非法
} as unknown as DSLElement['props'],
});
export const getFormItemDSLMap: Record<
shortcut_command.InputType,
GetFormItemTemplate
> = {
[shortcut_command.InputType.TextInput]: getInputElementDSL,
[shortcut_command.InputType.Select]: getSelectElementDSL,
[shortcut_command.InputType.UploadImage]: getUploadElementDSL,
[shortcut_command.InputType.UploadDoc]: getUploadElementDSL,
[shortcut_command.InputType.UploadTable]: getUploadElementDSL,
[shortcut_command.InputType.UploadAudio]: getUploadElementDSL,
[shortcut_command.InputType.MixUpload]: getUploadElementDSL,
[shortcut_command.InputType.PPT]: getUploadElementDSL,
[shortcut_command.InputType.ARCHIVE]: getUploadElementDSL,
[shortcut_command.InputType.CODE]: getUploadElementDSL,
[shortcut_command.InputType.TXT]: getUploadElementDSL,
[shortcut_command.InputType.VIDEO]: getUploadElementDSL,
};

View File

@@ -0,0 +1,163 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ZipIcon,
VideoIcon,
TextIcon as TxtIcon,
ImageIcon,
AudioIcon,
CodeIcon,
PptIcon,
DocxIcon as DocIcon,
XlsxIcon as TableIcon,
} from '@coze-common/chat-uikit';
import { FILE_TYPE_CONFIG, FileTypeEnum } from '@coze-common/chat-core';
import { I18n } from '@coze-arch/i18n';
import {
InputType,
shortcut_command,
} from '@coze-arch/bot-api/playground_api';
export type UploadItemType =
| InputType.UploadImage
| InputType.UploadDoc
| InputType.UploadTable
| InputType.UploadAudio
| InputType.CODE
| InputType.ARCHIVE
| InputType.PPT
| InputType.VIDEO
| InputType.TXT;
export const ACCEPT_UPLOAD_TYPES: {
type: UploadItemType;
label: string;
icon: string;
}[] = [
{
type: InputType.UploadImage,
label: I18n.t('shortcut_modal_upload_component_file_format_img'),
icon: ImageIcon,
},
{
type: InputType.UploadTable,
label: I18n.t('shortcut_modal_upload_component_file_format_table'),
icon: TableIcon,
},
{
type: InputType.UploadDoc,
label: I18n.t('shortcut_modal_upload_component_file_format_doc'),
icon: DocIcon,
},
{
type: InputType.UploadAudio,
label: I18n.t('shortcut_modal_upload_component_file_format_audio'),
icon: AudioIcon,
},
{
type: InputType.CODE,
label: I18n.t('shortcut_modal_upload_component_file_format_code'),
icon: CodeIcon,
},
{
type: InputType.ARCHIVE,
label: I18n.t('shortcut_modal_upload_component_file_format_zip'),
icon: ZipIcon,
},
{
type: InputType.PPT,
label: I18n.t('shortcut_modal_upload_component_file_format_ppt'),
icon: PptIcon,
},
{
type: InputType.VIDEO,
label: I18n.t('shortcut_modal_upload_component_file_format_video'),
icon: VideoIcon,
},
{
type: InputType.TXT,
label: I18n.t('shortcut_modal_upload_component_file_format_txt'),
icon: TxtIcon,
},
];
// 和chat支持的文件格式做映射
export const fileTypeToInputTypeMap = {
[FileTypeEnum.IMAGE]: shortcut_command.InputType.UploadImage,
[FileTypeEnum.AUDIO]: shortcut_command.InputType.UploadAudio,
[FileTypeEnum.PDF]: shortcut_command.InputType.UploadDoc,
[FileTypeEnum.DOCX]: shortcut_command.InputType.UploadDoc,
[FileTypeEnum.EXCEL]: shortcut_command.InputType.UploadTable,
[FileTypeEnum.CSV]: shortcut_command.InputType.UploadTable,
[FileTypeEnum.VIDEO]: shortcut_command.InputType.VIDEO,
[FileTypeEnum.PPT]: shortcut_command.InputType.PPT,
[FileTypeEnum.TXT]: shortcut_command.InputType.TXT,
[FileTypeEnum.ARCHIVE]: shortcut_command.InputType.ARCHIVE,
[FileTypeEnum.CODE]: shortcut_command.InputType.CODE,
[FileTypeEnum.DEFAULT_UNKNOWN]: shortcut_command.InputType.UploadDoc,
};
export const acceptMap = FILE_TYPE_CONFIG.reduce<{
[key in shortcut_command.InputType]?: string;
}>((acc, cur) => {
const inputType = fileTypeToInputTypeMap[cur.fileType];
if (inputType) {
const preAcc = acc[inputType];
if (preAcc) {
acc[inputType] = `${preAcc},${cur.accept.join(',')}`;
} else {
acc[inputType] = cur.accept.join(',');
}
}
return acc;
}, {});
// 根据acceptUploadItemTypes获取accept
export const getAcceptByUploadItemTypes = (
acceptUploadItemTypes: shortcut_command.InputType[],
) => {
const accept: string[] = [];
for (const type of acceptUploadItemTypes) {
if (!type) {
continue;
}
const acceptStr = acceptMap[type];
if (!acceptStr) {
continue;
}
accept.push(...acceptStr.split(','));
}
return accept.join(',');
};
// 根据fileType获取对应的fileInfo
export const getFileInfoByFileType = (fileType: FileTypeEnum) => {
const inputType = fileTypeToInputTypeMap[fileType];
if (!inputType) {
return null;
}
return ACCEPT_UPLOAD_TYPES.find(item => item.type === inputType);
};
// ACCEPT_UPLOAD_TYPES转化为map
export const getAcceptUploadItemTypesMap = () =>
ACCEPT_UPLOAD_TYPES.reduce<{
[key in shortcut_command.InputType]?: string;
}>((acc, cur) => {
acc[cur.type] = cur.label;
return acc;
}, {});

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 { exhaustiveCheckSimple } from '@coze-common/chat-area-utils';
import { type UIMode } from '../shortcut-bar/types';
export const getUIModeByBizScene: (props: {
bizScene: 'debug' | 'store' | 'home' | 'agentApp';
showBackground: boolean;
}) => UIMode = ({ bizScene, showBackground }) => {
if (bizScene === 'agentApp') {
return 'grey';
}
if (bizScene === 'home') {
if (showBackground) {
return 'blur';
}
return 'white';
}
if (bizScene === 'store' || bizScene === 'debug') {
if (showBackground) {
return 'blur';
}
return 'grey';
}
exhaustiveCheckSimple(bizScene);
return 'white';
};

View File

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

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 根据template_query和components拼接query
export const getQueryFromTemplate = (
templateQuery: string,
values: Record<string, unknown>,
) => {
let query = templateQuery;
// 替换模板中的{{key}}为values中key对应的值
Object.keys(values).forEach(key => {
query = query.replace(
new RegExp(`\\{\\{${key}\\}\\}`, 'g'),
values[key] as string,
);
});
return query;
};

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 {
shortcut_command,
type ToolParams,
} from '@coze-arch/bot-api/playground_api';
import { type ToolInfo } from '../shortcut-tool/types';
// 根据shortcut获取toolInfo
export const getToolInfoByShortcut = (
shortcut: shortcut_command.ShortcutCommand | undefined,
): ToolInfo => {
if (!shortcut) {
return {
tool_type: '',
tool_name: '',
plugin_id: '',
plugin_api_name: '',
tool_params_list: [],
};
}
const {
tool_info: { tool_params_list = [], tool_name = '' } = {},
tool_type,
plugin_id,
plugin_api_name,
work_flow_id,
} = shortcut;
return {
tool_type,
tool_name,
plugin_id,
plugin_api_name,
tool_params_list,
work_flow_id,
};
};
// 校验string数字 + 英文 + _ & 不能是纯数字
export const validateCmdString = (value: string) =>
/^[a-zA-Z0-9_]+$/.test(value) && !/^[0-9]+$/.test(value);
// 根据tool_type判断是否开启了tool
export const initToolEnabledByToolTYpe = (
toolType: shortcut_command.ToolType | undefined,
) =>
toolType !== undefined &&
[
shortcut_command.ToolType.ToolTypeWorkFlow,
shortcut_command.ToolType.ToolTypePlugin,
].includes(toolType);
// 校验plugin和workflow参数是否为string|integer类型,不支持复杂的对象类型
export const validatePluginAndWorkflowParams = (
params: ToolParams[],
enableEmpty = false,
): {
isSuccess: boolean;
inValidType: 'empty' | 'complex' | '';
} => {
if (!params.length) {
return {
isSuccess: enableEmpty,
inValidType: 'empty',
};
}
const isComplex = params.every(param => {
const { type } = param;
return type !== undefined && !['array', 'object'].includes(type);
});
return {
isSuccess: isComplex,
inValidType: isComplex ? '' : 'complex',
};
};
// 校验shortcut_command是否重复
export const validateCommandNameRepeat = (
checkShortcut: shortcut_command.ShortcutCommand,
shortcuts: shortcut_command.ShortcutCommand[],
): boolean => {
const { shortcut_command: shortcutCommand, command_id } = checkShortcut;
return !shortcuts.some(
shortcut =>
command_id !== shortcut.command_id &&
shortcut.shortcut_command === shortcutCommand,
);
};
// 校验按钮名称command_name是否重复
export const validateButtonNameRepeat = (
checkShortcut: shortcut_command.ShortcutCommand,
shortcuts: shortcut_command.ShortcutCommand[],
): boolean => {
const { command_name, command_id } = checkShortcut;
return !shortcuts.some(
shortcut =>
command_id !== shortcut.command_id &&
shortcut.command_name === command_name,
);
};

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 对齐 card-builder 生成 ID 的逻辑,暂时拷贝一份,未来计划直接使用 card-buidler 的底层能力
import { nanoid, customAlphabet } from 'nanoid';
/**
* @param prefix - id前缀
* @param options - alphabet: 字母表; length: 长度默认10;
*/
export const shortid = (
prefix = '',
options?: {
alphabet?: string;
length?: number;
},
) => {
const {
alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
length = 10,
} = options || {};
const genId = customAlphabet(alphabet, length);
return `${prefix}${genId()}`;
};
export const uuid = () => nanoid();
export const id = shortid;
export const generate = shortid;