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,296 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function */
import {
type Dispatch,
type SetStateAction,
useEffect,
useRef,
useState,
} from 'react';
import { useMemoizedFn } from 'ahooks';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { I18n } from '@coze-arch/i18n';
import { UIFormTextArea, Toast, Form } from '@coze-arch/bot-semi';
import {
APIMethod,
PluginType,
type UpdateAPIResponse,
type CreateAPIRequest,
type CreateAPIResponse,
} from '@coze-arch/bot-api/plugin_develop';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import { ERROR_CODE, type RenderEnhancedComponentProps } from './types';
import s from './index.module.less';
export interface UseBaseInfoRequest {
space_id: string;
pluginId: string;
apiId?: string;
baseInfo?: {
name?: string;
desc?: string;
};
setApiId?: (id: string) => void;
showSecurityCheckFailedMsg?: boolean;
setShowSecurityCheckFailedMsg?: Dispatch<SetStateAction<boolean>>;
showModal: boolean;
disabled: boolean;
editVersion?: number;
showFunctionName?: boolean;
pluginType?: PluginType;
onSuccess?: (params: UpdateAPIResponse | CreateAPIResponse) => void;
renderEnhancedComponent?: RenderEnhancedComponentProps['renderDescComponent'];
}
export interface UseBaseInfoReturnValue {
submitBaseInfo: () => Promise<boolean>;
baseInfoNode: JSX.Element;
}
const ENTER_KEY_CODE = 13;
export const useBaseInfo = ({
space_id,
pluginId,
apiId = '',
baseInfo = {},
setApiId,
showModal,
disabled,
showSecurityCheckFailedMsg,
setShowSecurityCheckFailedMsg,
editVersion,
showFunctionName = false,
pluginType,
onSuccess,
renderEnhancedComponent,
}: UseBaseInfoRequest): UseBaseInfoReturnValue => {
const formRef = useRef<Form>(null);
const [originDesc, setOriginDesc] = useState<string | undefined>(undefined);
useEffect(() => {
setOriginDesc(baseInfo?.desc);
formRef.current?.formApi.setValues({
name: baseInfo.name,
desc: baseInfo.desc,
});
}, [baseInfo.name, baseInfo.desc, showModal, disabled]);
const doSetDesc = useMemoizedFn((desc: string) => {
formRef.current?.formApi.setValue('desc', desc);
});
// 提交基础信息
const submitBaseInfo = async () => {
const status = await formRef.current?.formApi
.validate()
.then(() => true)
.catch(() => false);
if (!status) {
return false;
}
let baseResData;
const formValues = formRef.current?.formApi.getValues();
const params: CreateAPIRequest = {
plugin_id: pluginId,
name: formValues.name,
desc: formValues.desc,
edit_version: editVersion,
function_name: formValues.function_name,
};
try {
if (apiId) {
baseResData = await PluginDevelopApi.UpdateAPI(
{
...params,
api_id: apiId,
},
{
__disableErrorToast: true,
},
);
} else {
baseResData = await PluginDevelopApi.CreateAPI(
{
...params,
method: APIMethod.POST,
path: `/${params.name}`,
},
{
__disableErrorToast: true,
},
);
setApiId?.((baseResData as CreateAPIResponse).api_id || '');
}
onSuccess?.(baseResData);
return true;
} catch (error) {
// @ts-expect-error -- linter-disable-autofix
const { code, msg } = error;
if (Number(code) === ERROR_CODE.SAFE_CHECK) {
setShowSecurityCheckFailedMsg?.(true);
} else {
Toast.error({
content: withSlardarIdButton(msg),
});
}
return false;
}
};
const changeVal = () => {
if (showSecurityCheckFailedMsg) {
setShowSecurityCheckFailedMsg?.(false);
}
};
return {
submitBaseInfo,
baseInfoNode: (
<>
<Form<Record<string, unknown>>
showValidateIcon={false}
ref={formRef}
disabled={disabled}
className={s['base-info-form']}
>
{() =>
disabled ? (
<>
<Form.Slot
label={{
text: I18n.t('Create_newtool_s1_name'),
required: true,
}}
>
{baseInfo.name}
</Form.Slot>
<Form.Slot
label={{
text: I18n.t('Create_newtool_s1_dercribe'),
required: true,
}}
>
{baseInfo.desc}
</Form.Slot>
</>
) : (
<>
<UIFormTextArea
data-testid="plugin-create-tool-base-info-name"
className={s['textarea-single-line']}
field="name"
label={I18n.t('Create_newtool_s1_name')}
placeholder={I18n.t('Create_newtool_s1_title_empty')}
trigger={['blur', 'change']}
maxCount={30}
maxLength={30}
rows={1}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onKeyDown={(ele: any) => {
const e = window.event || ele;
if (
e.key === 'Enter' ||
e.code === 'Enter' ||
e.keyCode === ENTER_KEY_CODE
) {
e.returnValue = false;
return false;
}
}}
onChange={changeVal}
rules={[
{
required: true,
message: I18n.t('Create_newtool_s1_title_empty'),
},
{
pattern: /^[a-zA-Z0-9_]+$/,
message: I18n.t('Create_newtool_s1_title_error1'),
},
]}
/>
<div className="relative">
{renderEnhancedComponent?.({
disabled: !originDesc,
originDesc,
className: 'absolute right-[0] top-[12px]',
plugin_id: pluginId,
space_id,
onSetDescription: doSetDesc,
})}
<UIFormTextArea
data-testid="plugin-create-tool-base-info-desc"
field="desc"
label={I18n.t('Create_newtool_s1_dercribe')}
placeholder={I18n.t('Create_newtool_s1_dercribe_error')}
rows={2}
trigger={['blur', 'change']}
maxCount={600}
maxLength={600}
rules={[
{
required: true,
message: I18n.t('Create_newtool_s1_dercribe_empty'),
},
IS_OVERSEA && {
// eslint-disable-next-line no-control-regex
pattern: /^[\x00-\x7F]+$/,
message: I18n.t('create_plugin_modal_descrip_error'),
},
]}
// @ts-expect-error -- linter-disable-autofix
onChange={v => {
changeVal();
setOriginDesc(v);
}}
/>
{showFunctionName && pluginType === PluginType.LOCAL ? (
<UIFormTextArea
className={s['textarea-single-line']}
field="function_name"
label={I18n.t('create_local_plugin_basic_tool_function')}
placeholder={I18n.t(
'create_local_plugin_basic_tool_function_input_placeholder',
)}
rows={1}
trigger={['blur', 'change']}
maxCount={30}
maxLength={30}
rules={[
{
required: true,
message: I18n.t(
'create_local_plugin_basic_warning_no_tool_function_entered',
),
},
]}
onChange={changeVal}
/>
) : null}
</div>
</>
)
}
</Form>
</>
),
};
};

View File

@@ -0,0 +1,413 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 max-lines-per-function */
/* eslint-disable @coze-arch/max-line-per-function */
import { type Dispatch, type SetStateAction, useEffect, useRef } from 'react';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { I18n } from '@coze-arch/i18n';
import {
UIFormInput,
UIFormSelect,
UIFormTextArea,
Typography,
Toast,
Form,
Tooltip,
} from '@coze-arch/bot-semi';
import { IconInfo } from '@coze-arch/bot-icons';
import {
APIMethod,
type PluginMetaInfo,
AuthorizationType,
PluginToolAuthType,
type APIExtend,
PluginType,
type UpdateAPIRequest,
type UpdateAPIResponse,
} from '@coze-arch/bot-api/plugin_develop';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import { InfoPopover } from '../info_popover';
import { ERROR_CODE } from './types';
import { methodType } from './config';
import s from './index.module.less';
const { Option } = UIFormSelect;
export interface UseBaseInfoRequest {
pluginId: string;
pluginMeta: PluginMetaInfo;
apiId?: string;
step?: number;
baseInfo?: {
name?: string;
desc?: string;
path?: string;
method?: APIMethod;
api_extend?: APIExtend;
function_name?: string;
};
showSecurityCheckFailedMsg?: boolean;
setShowSecurityCheckFailedMsg?: Dispatch<SetStateAction<boolean>>;
showModal: boolean;
disabled: boolean;
editVersion?: number;
pluginType?: PluginType;
spaceId?: string;
onSuccess?: (params: UpdateAPIResponse) => void;
}
export interface UseBaseInfoReturnValue {
submitBaseInfo: () => Promise<boolean>;
baseInfoNode: JSX.Element;
}
export const useBaseMore = ({
pluginId,
pluginMeta,
apiId = '',
baseInfo = {},
showModal,
disabled,
showSecurityCheckFailedMsg,
setShowSecurityCheckFailedMsg,
editVersion,
pluginType,
onSuccess,
}: UseBaseInfoRequest): UseBaseInfoReturnValue => {
const { url: pluginUrl } = pluginMeta;
const formRef = useRef<Form>(null);
useEffect(() => {
formRef.current?.formApi.setValues({
path: baseInfo.path,
method: baseInfo.method || APIMethod.GET,
function_name: baseInfo.function_name,
auth_mode: baseInfo.api_extend?.auth_mode || PluginToolAuthType.Required,
});
}, [
baseInfo.path,
showModal,
disabled,
pluginMeta,
baseInfo.method,
baseInfo.function_name,
baseInfo.api_extend?.auth_mode,
]);
// 提交基础信息
const submitBaseInfo = async () => {
const status = await formRef.current?.formApi
.validate()
.then(() => true)
.catch(() => false);
if (!status || !apiId) {
return false;
}
let baseResData;
const formValues = formRef.current?.formApi.getValues();
const params: UpdateAPIRequest = {
api_id: apiId,
plugin_id: pluginId,
path: formValues.path,
method: formValues.method,
api_extend: {
auth_mode: formValues.auth_mode,
},
edit_version: editVersion,
function_name: formValues.function_name,
};
try {
baseResData = await PluginDevelopApi.UpdateAPI(params, {
__disableErrorToast: true,
});
onSuccess?.(baseResData);
return true;
} catch (error) {
// @ts-expect-error -- linter-disable-autofix
const { code, msg } = error;
if (Number(code) === ERROR_CODE.SAFE_CHECK) {
setShowSecurityCheckFailedMsg?.(true);
} else {
Toast.error({
content: withSlardarIdButton(msg),
});
}
return false;
}
};
const changeVal = () => {
if (showSecurityCheckFailedMsg) {
setShowSecurityCheckFailedMsg?.(false);
}
};
return {
submitBaseInfo,
baseInfoNode: (
<>
<Form<Record<string, unknown>>
showValidateIcon={false}
ref={formRef}
disabled={disabled}
className={s['base-info-form']}
>
{() =>
disabled ? (
<>
{pluginType === PluginType.LOCAL && (
<Form.Slot
label={{
text: I18n.t('create_local_plugin_basic_tool_function'),
required: true,
}}
>
{baseInfo.function_name ?? '-'}
</Form.Slot>
)}
{pluginType === PluginType.PLUGIN && (
<>
<Form.Slot
label={{
text: I18n.t('Create_newtool_s1_url'),
required: true,
}}
>
{String(pluginUrl) + baseInfo.path}
</Form.Slot>
<Form.Slot
label={{
text: I18n.t('Create_newtool_s1_method'),
required: true,
extra: <InfoPopover data={methodType} />,
}}
>
{API_METHOD_LABEL_MAP[baseInfo?.method || APIMethod.GET]}
</Form.Slot>
</>
)}
{pluginMeta?.auth_type?.includes(AuthorizationType.OAuth) ? (
<Form.Slot
label={{
text: I18n.t('plugin_edit_tool_oauth_enabled_title'),
required: true,
extra: (
<Tooltip
content={I18n.t(
'plugin_edit_tool_oauth_enabled_title_hover_tip',
)}
>
<IconInfo
style={{ color: 'rgba(28, 29, 35, 0.35)' }}
/>
</Tooltip>
),
}}
>
{
API_MODE_LABEL_MAP[
baseInfo.api_extend?.auth_mode ||
PluginToolAuthType.Required
]
}
</Form.Slot>
) : null}
</>
) : (
<>
{pluginType === PluginType.LOCAL && (
<UIFormTextArea
className={s['textarea-single-line']}
field="function_name"
label={I18n.t('create_local_plugin_basic_tool_function')}
placeholder={I18n.t(
'create_local_plugin_basic_tool_function_input_placeholder',
)}
rows={1}
trigger={['blur', 'change']}
maxCount={30}
maxLength={30}
rules={[
{
required: true,
message: I18n.t(
'create_local_plugin_basic_warning_no_tool_function_entered',
),
},
]}
onChange={changeVal}
/>
)}
{pluginType === PluginType.PLUGIN && (
<>
<UIFormInput
field="path"
label={{
text: I18n.t('Create_newtool_s1_url'),
}}
trigger={['blur', 'change']}
addonBefore={
<div className={s['plugin-url-prefix']}>
<Typography.Text
ellipsis={{
showTooltip: {
type: 'tooltip',
opts: {
content: pluginUrl,
style: {
wordBreak: 'break-word',
},
},
},
}}
>
{pluginUrl}
</Typography.Text>
</div>
}
style={{ width: '100%' }}
className={s['plugin-url-input']}
placeholder={I18n.t('Create_newtool_s1_url_empty')}
rules={[
{
required: true,
message: I18n.t('Create_newtool_s1_url_error2'),
},
{
pattern: /^\//,
message: I18n.t('Create_newtool_s1_url_error1'),
},
{
// eslint-disable-next-line no-control-regex
pattern: /^[\x00-\x7F]+$/,
message: I18n.t('tool_new_S1_URL_error'),
},
]}
></UIFormInput>
<UIFormSelect
field="method"
initValue={APIMethod.GET}
label={{
text: I18n.t('Create_newtool_s1_method'),
extra: <InfoPopover data={methodType} />,
}}
showClear
trigger={['blur', 'change']}
style={{ width: '100%', borderRadius: '8px' }}
placeholder={I18n.t(
'workflow_detail_condition_pleaseselect',
)}
rules={[
{
required: true,
message: I18n.t(
'workflow_detail_condition_pleaseselect',
),
},
]}
>
{[
APIMethod.GET,
APIMethod.POST,
APIMethod.PUT,
APIMethod.DELETE,
APIMethod.PATCH,
].map(method => (
<Option value={method} key={method}>
{API_METHOD_LABEL_MAP[method]}
</Option>
))}
</UIFormSelect>
{pluginMeta?.auth_type?.includes(
AuthorizationType.OAuth,
) ? (
<UIFormSelect
field="auth_mode"
initValue={PluginToolAuthType.Required}
label={{
text: I18n.t('plugin_edit_tool_oauth_enabled_title'),
extra: (
<Tooltip
content={I18n.t(
'plugin_edit_tool_oauth_enabled_title_hover_tip',
)}
>
<IconInfo
style={{ color: 'rgba(28, 29, 35, 0.35)' }}
/>
</Tooltip>
),
}}
showClear
trigger={['blur', 'change']}
style={{ width: '100%', borderRadius: '8px' }}
placeholder={I18n.t(
'workflow_detail_condition_pleaseselect',
)}
rules={[
{
required: true,
message: I18n.t(
'workflow_detail_condition_pleaseselect',
),
},
]}
>
{[
PluginToolAuthType.Required,
PluginToolAuthType.Supported,
PluginToolAuthType.Disable,
].map(mode => (
<Option value={mode} key={mode}>
{API_MODE_LABEL_MAP[mode]}
</Option>
))}
</UIFormSelect>
) : null}
</>
)}
</>
)
}
</Form>
</>
),
};
};
const API_METHOD_LABEL_MAP: Record<APIMethod, string> = {
[APIMethod.GET]: I18n.t('Create_newtool_s1_method_get'),
[APIMethod.POST]: I18n.t('Create_newtool_s1_method_post'),
[APIMethod.PUT]: I18n.t('Create_newtool_s1_method_put'),
[APIMethod.DELETE]: I18n.t('Create_newtool_s1_method_delete'),
[APIMethod.PATCH]: I18n.t('Create_tool_s1_method_patch_name'),
};
const API_MODE_LABEL_MAP: Record<PluginToolAuthType, string> = {
[PluginToolAuthType.Required]: I18n.t(
'plugin_edit_tool_oauth_enabled_status_auth_required',
),
[PluginToolAuthType.Supported]: I18n.t(
'plugin_edit_tool_oauth_enabled_status_auth_optional',
),
[PluginToolAuthType.Disable]: I18n.t(
'plugin_edit_tool_oauth_enabled_status_auth_disabled',
),
};

View File

@@ -0,0 +1,164 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 FC, useEffect, useState } from 'react';
import cl from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { type CascaderProps } from '@coze-arch/coze-design';
import { Typography, UICascader } from '@coze-arch/bot-semi';
import { IconAlertCircle } from '@douyinfe/semi-icons';
import {
type APIParameterRecord,
type CascaderOnChangValueType,
type CascaderValueType,
type InputItemProps,
} from '../../types/params';
import s from '../../index.module.less';
import {
ARRAYTAG,
assistToExtend,
extendToAssist,
getParameterTypeLabel,
getPluginParameterTypeOptions,
ParameterTypeExtend,
} from '../../config';
const getCascaderValueTypeFrom = (
record?: APIParameterRecord,
): CascaderValueType => {
if (record?.assist_type) {
return [ParameterTypeExtend.DEFAULT, assistToExtend(record.assist_type)];
}
// @ts-expect-error -- linter-disable-autofix
return [record.type];
};
const { Text } = Typography;
interface CProps extends Omit<InputItemProps, 'selectCallback'> {
selectCallback: (types: CascaderOnChangValueType) => void;
enableFileType?: boolean;
}
export const CascaderItem: FC<CProps> = ({
check = 0,
useBlockWrap = false,
record,
disabled,
selectCallback,
enableFileType = false,
}) => {
const [value, setValue] = useState<CascaderValueType>(
getCascaderValueTypeFrom(record),
);
const [errorStatus, setErrorStatus] = useState<number>(0);
// @ts-expect-error -- linter-disable-autofix
const isArrayType = record.name === ARRAYTAG;
// @ts-expect-error -- linter-disable-autofix
const isObjectField = (record.deep ?? 0) > 1 && record.name !== ARRAYTAG;
// 通过check触发校验提交时
useEffect(() => {
if (check === 0) {
return;
}
handleCheck(value);
}, [check]);
// 校验
const handleCheck = (val?: CascaderValueType) => {
const status = !val?.[0] ? 1 : 0;
setErrorStatus(status);
};
const onChange = (types: CascaderValueType) => {
if (types[1]) {
selectCallback([types[0], extendToAssist(types[1])]);
} else {
selectCallback([types[0]]);
}
setValue(types);
handleCheck(types);
};
const displayRender: CascaderProps['displayRender'] = (items, idx) => {
// @ts-expect-error -- linter-disable-autofix
let inputValue: string = items[0];
if (value[1]) {
if (value[1] === ParameterTypeExtend.DEFAULT) {
// @ts-expect-error -- linter-disable-autofix
inputValue = getParameterTypeLabel(
ParameterTypeExtend.DEFAULT,
isArrayType,
);
} else {
// @ts-expect-error -- linter-disable-autofix
inputValue = items[1];
}
}
return <Text ellipsis={{ showTooltip: true }}>{inputValue}</Text>;
};
const parameterTypeOptionsWithCustom = getPluginParameterTypeOptions(
isArrayType,
enableFileType && !isObjectField,
);
return (
<span
style={useBlockWrap ? { display: 'inline-block', width: '100%' } : {}}
>
<UICascader
treeData={parameterTypeOptionsWithCustom}
validateStatus={errorStatus ? 'error' : 'default'}
// @ts-expect-error -- linter-disable-autofix
value={value}
disabled={disabled}
onChange={onChange as CascaderProps['onChange']}
displayRender={displayRender}
dropdownClassName={s.cascaderDropdown}
style={{ width: '100%' }}
/>
<br />
{errorStatus !== 0 && (
<div style={{ position: 'relative' }}>
<span className={cl(s['form-check-tip'], 'errorClassTag', s.w110)}>
<IconAlertCircle className={s['plugin-icon-error']} />
<Text
component="span"
ellipsis={{
showTooltip: {
type: 'tooltip',
opts: { style: { maxWidth: '100%' } },
},
}}
className={s['plugin-tooltip-error']}
>
{errorStatus === 1 && (
<span>{I18n.t('plugin_Parameter_type')}</span>
)}
</Text>
</span>
</div>
)}
</span>
);
};

View File

@@ -0,0 +1,6 @@
.icon {
> svg {
width: 16px;
height: 15px;
}
}

View File

@@ -0,0 +1,168 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { type FC, useEffect, useState } from 'react';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { UIButton, Typography, UIIconButton } from '@coze-arch/bot-semi';
import { AssistParameterType } from '@coze-arch/bot-api/plugin_develop';
import { FileTypeEnum } from '@coze-studio/file-kit/logic';
import { ACCEPT_UPLOAD_TYPES } from '@coze-studio/file-kit/config';
import { IconDeleteOutline, IconUploadOutlined1 } from '@coze-arch/bot-icons';
import { ItemErrorTip } from '../item-error-tip';
import { getFileAccept, getFileTypeFromAssistType } from '../../file';
import { PluginFileUpload } from './upload';
import styles from './index.module.less';
const { Text } = Typography;
const fileUnknownIcon = ACCEPT_UPLOAD_TYPES[FileTypeEnum.DEFAULT_UNKNOWN].icon;
export const FileUploadItem: FC<{
assistParameterType: AssistParameterType;
onChange?: (uri: string) => void;
required?: boolean;
withDescription?: boolean;
defaultValue?: string;
check?: number;
disabled?: boolean;
}> = ({
onChange,
required = false,
withDescription = false,
check = 0,
defaultValue,
disabled = false,
assistParameterType,
}) => {
const [isErrorStatus, setIsErrorStatus] = useState(false);
const [value, setValue] = useState(defaultValue);
const defaultFileType = getFileTypeFromAssistType(assistParameterType);
const isImageString = assistParameterType === AssistParameterType.IMAGE;
const btnText = isImageString
? I18n.t('plugin_file_upload_image')
: I18n.t('plugin_file_upload');
const errorTip = isImageString
? I18n.t('plugin_file_upload_mention_image')
: I18n.t('plugin_file_upload_mention');
const accept = getFileAccept(assistParameterType);
useEffect(() => {
if (check === 0) {
return;
}
setIsErrorStatus(required && !value);
}, [check]);
const onChangeHandler = (uri: string) => {
setValue(uri);
onChange?.(uri);
setIsErrorStatus(required && !uri);
};
return (
<>
<PluginFileUpload
defaultUrl={value}
defaultFileType={defaultFileType}
onUploadSuccess={onChangeHandler}
uploadProps={{
accept,
disabled,
maxSize: 20480,
}}
render={({ fileState, clearFile }) => {
const { uploading, uri, url, name, type } = fileState;
/**
* 回显 只有一个url(string),需要兼容 => 不展示icon,url作为文件名
*/
const onlyUrlString = !!url && !uri;
const displayName = onlyUrlString ? value : name;
let icon: string | undefined = url;
const uploadButton = (
<UIButton
icon={<IconUploadOutlined1 className={styles.icon} />}
loading={uploading}
disabled={disabled}
className="w-full"
>
{uploading ? I18n.t('plugin_file_uploading') : btnText}
</UIButton>
);
if (uploading) {
return uploadButton;
} else if (onlyUrlString && type === FileTypeEnum.IMAGE) {
/** image不是即时上传的无法确认其为合法资源路径 */
icon = fileUnknownIcon;
} else if (!isImageString) {
// @ts-expect-error -- linter-disable-autofix
const typeIcon = ACCEPT_UPLOAD_TYPES[type]?.icon;
if (typeIcon) {
icon = typeIcon;
} else {
icon = undefined;
}
}
if (onlyUrlString || uri) {
return (
<div
className={classNames(
'flex items-center justify-between w-full h-[32px]',
disabled ? 'cursor-not-allowed' : '',
)}
>
<div className="flex items-center min-w-0">
{icon ? (
<img
src={icon}
className="w-[20px] h-[20px] mr-[5px] rounded-[0.5px]"
/>
) : null}
<Text ellipsis={{ showTooltip: true }} className="mr-[2px]">
{displayName}
</Text>
</div>
<UIIconButton
icon={<IconDeleteOutline />}
disabled={disabled}
onClick={e => {
e.stopPropagation();
clearFile();
onChangeHandler('');
}}
/>
</div>
);
}
return uploadButton;
}}
/>
{isErrorStatus ? (
<ItemErrorTip withDescription={withDescription} tip={errorTip} />
) : null}
</>
);
};

View File

@@ -0,0 +1,164 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 FC, type ReactNode, useReducer } from 'react';
import { merge } from 'lodash-es';
import { produce } from 'immer';
import { userStoreService } from '@coze-studio/user-store';
import { I18n } from '@coze-arch/i18n';
import { uploadFileV2 } from '@coze-arch/bot-utils';
import { FileTypeEnum, getFileInfo } from '@coze-studio/file-kit/logic';
import { Upload, Toast, type UploadProps } from '@coze-arch/coze-design';
interface PluginFileUploadProps {
render: (props: { fileState: FileState; clearFile: () => void }) => ReactNode;
onUploadSuccess?: (uri: string) => void;
uploadProps?: Partial<UploadProps>;
disabled?: boolean;
defaultUrl?: string;
defaultFileType: FileTypeEnum | null;
}
interface FileState {
uri: string;
url: string;
name: string;
type: FileTypeEnum | null;
uploading: boolean;
abortSignal: AbortSignal;
}
const getDefaultFileState = (states?: Partial<FileState>): FileState =>
merge(
{
uri: '',
url: '',
name: '',
type: null,
uploading: false,
abortSignal: new AbortController().signal,
} satisfies FileState,
states,
);
type Action = Partial<Omit<FileState, 'abortSignal'>>;
export const PluginFileUpload: FC<PluginFileUploadProps> = ({
disabled = false,
uploadProps,
render,
onUploadSuccess,
defaultUrl,
defaultFileType,
}) => {
// @ts-expect-error -- linter-disable-autofix
const userId = userStoreService.useUserInfo().user_id_str;
const [fileState, setFileState] = useReducer(
(states: FileState, payload: Action) =>
produce(states, draft => {
if (!payload) {
return;
}
Object.keys(payload).forEach(key => {
// @ts-expect-error -- linter-disable-autofix
draft[key] = payload[key] ?? draft[key];
});
}),
getDefaultFileState({
url: defaultUrl ?? '',
type: defaultFileType ?? null,
}),
);
const clearFile = () => setFileState(getDefaultFileState());
const customRequest: UploadProps['customRequest'] = async ({
file,
fileInstance,
}) => {
// @ts-expect-error -- linter-disable-autofix
const type = getFileInfo(fileInstance).fileType;
setFileState({
uploading: true,
url: file.url,
name: file.name,
});
await uploadFileV2({
userId,
fileItemList: [
{
file: fileInstance,
fileType: type === FileTypeEnum.IMAGE ? 'image' : 'object',
},
],
signal: fileState.abortSignal,
timeout: undefined,
onSuccess: info => {
const uri = info?.uploadResult?.Uri;
if (!uri) {
return;
}
setFileState({
uploading: false,
uri,
type,
});
onUploadSuccess?.(uri);
},
onUploadError: () => {
setFileState({
uploading: false,
});
},
});
};
if (typeof render !== 'function') {
return null;
}
return (
<Upload
className="w-full"
draggable
limit={1}
disabled={disabled}
onAcceptInvalid={() => {
Toast.error(I18n.t('shortcut_Illegal_file_format'));
}}
onSizeError={() => {
if (uploadProps?.maxSize) {
Toast.error(
I18n.t('file_too_large', {
max_size: `${uploadProps.maxSize / 1024}MB`,
}),
);
}
}}
customRequest={customRequest}
showUploadList={false}
{...uploadProps}
>
{render({ fileState, clearFile })}
</Upload>
);
};

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 { CascaderItem } from './cascader-item';
export { FileUploadItem } from './file-upload-item';

View File

@@ -0,0 +1,19 @@
/* stylelint-disable declaration-no-important */
.check-box {
position: absolute;
}
.form-check-tip {
position: absolute;
top: 4px;
right: 0;
left: 0;
transform-origin: left;
display: inline-block;
font-size: 12px !important;
line-height: 16px;
color: var(--semi-color-danger);
}

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 React, { type FC } from 'react';
import cl from 'classnames';
import { I18n } from '@coze-arch/i18n';
import s from './index.module.less';
export const ItemErrorTip: FC<{ withDescription?: boolean; tip?: string }> = ({
withDescription = false,
tip = I18n.t('plugin_empty'),
}) => (
<div className={s['check-box']}>
<span
className={cl(
'whitespace-nowrap',
s['form-check-tip'],
withDescription ? '!top-[16px]' : '!top-0',
'errorDebugClassTag',
)}
>
{tip}
</span>
</div>
);

View File

@@ -0,0 +1,431 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { cloneDeep } from 'lodash-es';
import { I18n } from '@coze-arch/i18n';
import {
type AssistParameterType,
ParameterLocation,
ParameterType,
} from '@coze-arch/bot-api/plugin_develop';
import { type ExtInfoText } from '@coze-studio/plugin-shared';
import { FileTypeEnum } from '@coze-studio/file-kit/logic';
import { type APIParameterRecord } from './types/params';
export const childrenRecordName = 'sub_parameters'; // 子节点名称
export const ROWKEY = 'id'; // 唯一标识符
export const ARRAYTAG = '[Array Item]'; // 数组元素标识符
export const ROOTTAG = '[Root Item]'; // root为数组的标识符
export const STARTNODE = 0;
export const REQUESTNODE = 1;
export const RESPONSENODE = 2;
export const DEBUGNODE = 3;
export const ENDSTEP = 4;
// 传入方法options
export const parameterLocationOptions = [
{
label: 'Body',
value: ParameterLocation.Body,
},
{
label: 'Path',
value: ParameterLocation.Path,
},
{
label: 'Query',
value: ParameterLocation.Query,
},
{
label: 'Header',
value: ParameterLocation.Header,
},
];
export enum ParameterTypeExtend {
/**
* 扩展类型
* 与 AssistParameterType 一一对应
*/
DEFAULT = 10001,
IMAGE,
DOC,
CODE,
PPT,
TXT,
EXCEL,
AUDIO,
ZIP,
VIDEO,
}
const enumDomain = 10000;
export const assistToExtend = (
type: AssistParameterType,
): ParameterTypeExtend => type + enumDomain;
export const extendToAssist = (
type: ParameterTypeExtend,
): AssistParameterType => type - enumDomain;
export type PluginParameterType = ParameterType | ParameterTypeExtend;
interface ParameterTypeOption {
label: string;
value: ParameterType | ParameterTypeExtend;
children?: Array<{
label: string;
value: ParameterTypeExtend;
}>;
}
/**
* 未扩展File类型前的 基础类型,多处使用 需要保留 start
*/
export const parameterTypeOptions: Array<ParameterTypeOption> = [
{
label: 'String',
value: ParameterType.String,
},
{
label: 'Integer',
value: ParameterType.Integer,
},
{
label: 'Number',
value: ParameterType.Number,
},
{
label: 'Object',
value: ParameterType.Object,
},
{
label: 'Array',
value: ParameterType.Array,
},
{
label: 'Boolean',
value: ParameterType.Bool,
},
];
export const parameterTypeOptionsSub: Array<ParameterTypeOption> = [
{
label: 'Array<String>',
value: ParameterType.String,
},
{
label: 'Array<Integer>',
value: ParameterType.Integer,
},
{
label: 'Array<Number>',
value: ParameterType.Number,
},
{
label: 'Array<Object>',
value: ParameterType.Object,
},
{
label: 'Array<Boolean>',
value: ParameterType.Bool,
},
];
/**
* 未扩展File类型前的 基础类型,多处使用 需要保留 end
*/
export const parameterTypeExtendMap: Record<
ParameterTypeExtend,
{
label: string;
listLabel: string;
fileTypes: FileTypeEnum[];
}
> = {
[ParameterTypeExtend.DEFAULT]: {
label: 'File',
listLabel: 'Array<File>',
fileTypes: [FileTypeEnum.DEFAULT_UNKNOWN],
},
[ParameterTypeExtend.IMAGE]: {
label: 'Image',
listLabel: 'Array<Image>',
fileTypes: [FileTypeEnum.IMAGE],
},
[ParameterTypeExtend.DOC]: {
label: 'Doc',
listLabel: 'Array<Doc>',
fileTypes: [FileTypeEnum.DOCX, FileTypeEnum.PDF],
},
[ParameterTypeExtend.CODE]: {
label: 'Code',
listLabel: 'Array<Code>',
fileTypes: [FileTypeEnum.CODE],
},
[ParameterTypeExtend.PPT]: {
label: 'PPT',
listLabel: 'Array<PPT>',
fileTypes: [FileTypeEnum.PPT],
},
[ParameterTypeExtend.TXT]: {
label: 'TXT',
listLabel: 'Array<TXT>',
fileTypes: [FileTypeEnum.TXT],
},
[ParameterTypeExtend.EXCEL]: {
label: 'Excel',
listLabel: 'Array<Excel>',
fileTypes: [FileTypeEnum.EXCEL, FileTypeEnum.CSV],
},
[ParameterTypeExtend.AUDIO]: {
label: 'Audio',
listLabel: 'Array<Audio>',
fileTypes: [FileTypeEnum.AUDIO],
},
[ParameterTypeExtend.ZIP]: {
label: 'Zip',
listLabel: 'Array<Zip>',
fileTypes: [FileTypeEnum.ARCHIVE],
},
[ParameterTypeExtend.VIDEO]: {
label: 'Video',
listLabel: 'Array<Video>',
fileTypes: [FileTypeEnum.VIDEO],
},
};
const getParameterTypeOptionsWithCustom = (enableFileType = false) => {
if (!enableFileType) {
return parameterTypeOptions;
}
const parameterTypeOptionsWithCustom = cloneDeep(parameterTypeOptions);
parameterTypeOptionsWithCustom.splice(1, 0, {
label: 'File',
value: ParameterTypeExtend.DEFAULT,
children: Object.entries(parameterTypeExtendMap).map(
([type, { label }]) => ({
label,
value: Number(type) as ParameterTypeExtend,
}),
),
});
return parameterTypeOptionsWithCustom;
};
const getParameterTypeOptionsSubWithCustom = (enableFileType = false) => {
if (!enableFileType) {
return parameterTypeOptionsSub;
}
const parameterTypeOptionsSubWithCustom = cloneDeep(parameterTypeOptionsSub);
parameterTypeOptionsSubWithCustom.splice(1, 0, {
label: 'Array<File>',
value: ParameterTypeExtend.DEFAULT,
children: Object.entries(parameterTypeExtendMap).map(
([type, { listLabel }]) => ({
label: listLabel,
value: Number(type) as ParameterTypeExtend,
}),
),
});
return parameterTypeOptionsSubWithCustom;
};
export const getPluginParameterTypeOptions = (
isArrayType: boolean,
enableFileType: boolean,
) =>
isArrayType
? getParameterTypeOptionsSubWithCustom(enableFileType)
: getParameterTypeOptionsWithCustom(enableFileType);
const parameterTypeOptionsMap = parameterTypeOptions.reduce(
(prev: Partial<Record<PluginParameterType, string>>, curr) => {
prev[curr.value] = curr.label;
return prev;
},
{
...Object.entries(parameterTypeExtendMap).reduce(
(prev, [type, { label }]) => {
// @ts-expect-error -- linter-disable-autofix
prev[type] = label;
return prev;
},
{},
),
[ParameterTypeExtend.DEFAULT]: 'File',
},
);
const parameterTypeOptionsSubMap = parameterTypeOptionsSub.reduce(
(prev: Partial<Record<PluginParameterType, string>>, curr) => {
prev[curr.value] = curr.label;
return prev;
},
{
...Object.entries(parameterTypeExtendMap).reduce(
(prev, [type, { listLabel }]) => {
// @ts-expect-error -- linter-disable-autofix
prev[type] = listLabel;
return prev;
},
{},
),
[ParameterTypeExtend.DEFAULT]: 'Array<File>',
},
);
export const getParameterTypeLabel = (
type: PluginParameterType,
isArrayType = false,
) =>
isArrayType
? parameterTypeOptionsSubMap[type]
: parameterTypeOptionsMap[type];
export const getParameterTypeLabelFromRecord = (
record: APIParameterRecord,
isArrayType = false,
) => {
let type: PluginParameterType = record.type as PluginParameterType;
if (record?.assist_type) {
type = assistToExtend(record.assist_type);
}
return getParameterTypeLabel(type, isArrayType);
};
export const methodType: ExtInfoText[] = [
{
type: 'title',
text: 'Get',
},
{
type: 'text',
text: I18n.t('plugin_tooltip_url'),
},
{
type: 'demo',
text: 'GET /users?userId=123',
},
{
type: 'text',
text: I18n.t('used_to_obtain_user_information_with_id_123'),
},
{
type: 'br',
},
{
type: 'title',
text: 'Post',
},
{
type: 'text',
text: I18n.t(
'submit_data_to_a_specified_resource__often_used_to_submit_forms_or_upload_files_',
),
},
{
type: 'demo',
text: 'POST /users',
},
{
type: 'text',
text: I18n.t('attach_user_data_to_create_a_new_user_'),
},
{
type: 'title',
text: 'Put',
},
{
type: 'text',
text: I18n.t(
'upload_data_or_resources_to_a_specified_location__often_used_to_update_existing_',
),
},
{
type: 'demo',
text: 'PUT /users/123',
},
{
type: 'text',
text: I18n.t('used_to_update_user_information_with_id_123_'),
},
{
type: 'title',
text: 'Delete',
},
{
type: 'text',
text: I18n.t(
'requests_the_server_to_delete_the_specified_resource__example_',
),
},
{
type: 'demo',
text: 'DELETE /users/123',
},
{
type: 'text',
text: I18n.t('used_to_delete_the_user_with_id_123_'),
},
{
type: 'title',
text: I18n.t('Create_tool_s1_method_patch_tooltip_title'),
},
{
type: 'text',
text: I18n.t('Create_tool_s1_method_patch_tooltip_desp'),
},
{
type: 'demo',
text: I18n.t('Create_tool_s1_method_patch_tooltip_url'),
},
{
type: 'text',
text: I18n.t('Create_tool_s1_method_patch_tooltip_explain'),
},
];
export enum ParamsFormErrorStatus {
NO_ERROR = 0,
NAME_EMPTY = 1,
// 中文
CHINESE = 2,
// 重复
REPEAT = 3,
ASCII = 4,
// 未填写
DESC_EMPTY = 5,
}
export const paramsFormErrorStatusText = {
[ParamsFormErrorStatus.NO_ERROR]: '',
[ParamsFormErrorStatus.NAME_EMPTY]: I18n.t(
'Create_newtool_s2_table_name_error1',
),
[ParamsFormErrorStatus.CHINESE]: I18n.t(
'Create_newtool_s2_table_name_error2',
),
[ParamsFormErrorStatus.REPEAT]: I18n.t('plugin_Parameter_name_error'),
[ParamsFormErrorStatus.ASCII]: I18n.t('create_plugin_modal_descrip_error'),
[ParamsFormErrorStatus.DESC_EMPTY]: I18n.t(
'Create_newtool_s3_table_des_empty',
),
};

View File

@@ -0,0 +1,182 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 { useState, type PropsWithChildren, type FC } from 'react';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { Banner, Space } from '@coze-arch/bot-semi';
import { IconPullDown } from '@coze-arch/bot-icons';
import { type CheckParamsProps } from '../types';
import { DiyMdBox, HeadingType } from './diy-mdbox';
import s from './index.module.less';
const Header = ({
// @ts-expect-error -- linter-disable-autofix
activeTab,
// @ts-expect-error -- linter-disable-autofix
setActiveTab,
// @ts-expect-error -- linter-disable-autofix
hideRawResponse,
// @ts-expect-error -- linter-disable-autofix
showRaw,
// @ts-expect-error -- linter-disable-autofix
setShowRaw,
}) => {
const handleOpenRawResponse = () => {
setShowRaw(!showRaw);
};
return (
<div className={s['debug-check-header']}>
<div className={s['debug-check-tab']}>
<div
className={classNames(s['debug-check-tab-item'], {
[s['debug-check-tab-item-active']]:
activeTab === HeadingType.Request,
})}
onClick={() => setActiveTab(HeadingType.Request)}
>
Request
</div>
<div className={s['debug-check-tab-line']}></div>
<div
className={classNames(s['debug-check-tab-item'], {
[s['debug-check-tab-item-active']]:
activeTab === HeadingType.Response,
})}
onClick={() => setActiveTab(HeadingType.Response)}
>
Response
</div>
</div>
{activeTab === HeadingType.Response && !hideRawResponse ? (
<Space spacing={8}>
<span>Raw Response</span>
<IconPullDown
className={classNames(s.icon, {
[s.open]: showRaw,
})}
onClick={handleOpenRawResponse}
></IconPullDown>
</Space>
) : null}
</div>
);
};
const ProcessContent: FC<PropsWithChildren> = ({ children }) => (
<div className={s['process-content']}>{children}</div>
);
/** stringify 缩进 */
const INDENTATION_SPACES = 2;
const LLMAndAPIContent: FC<{
toolMessageUnit: CheckParamsProps;
}> = ({ toolMessageUnit }) => {
const { request, response, failReason, rawResp } = toolMessageUnit;
const [activeTab, setActiveTab] = useState(1);
const [showRaw, setShowRaw] = useState(false);
return (
<>
{!request && !response ? (
<div className={s['llm-debug-empty']}>
<div className={s['llm-debug-empty-content']}>
{I18n.t('plugin_s4_debug_empty')}
</div>
</div>
) : (
<div className={s['debug-result-content']}>
<Header
activeTab={activeTab}
setActiveTab={setActiveTab}
hideRawResponse={!(!failReason && rawResp)}
showRaw={showRaw}
setShowRaw={setShowRaw}
/>
{activeTab === 1 ? (
<>
<div className={s['llm-api-content']}>
<DiyMdBox
markDown={
request
? JSON.stringify(
JSON.parse(request || '{}'),
null,
INDENTATION_SPACES,
)
: ''
}
headingType={activeTab}
showRaw={showRaw}
/>
</div>
</>
) : (
<>
<div className={s['llm-api-content']}>
{failReason ? (
<div className={s['error-reason-box']}>
<Banner
className={s['error-reason']}
fullMode={false}
icon={null}
closeIcon={null}
type="danger"
description={
<div>
<div>{I18n.t('plugin_s4_debug_detail')}</div>
<div style={{ wordBreak: 'break-word' }}>
{failReason}
</div>
</div>
}
/>
</div>
) : (
<DiyMdBox
headingType={activeTab}
markDown={JSON.stringify(
JSON.parse(response || '{}'),
null,
INDENTATION_SPACES,
)}
rawResponse={JSON.stringify(
JSON.parse(rawResp || '{}'),
null,
INDENTATION_SPACES,
)}
showRaw={showRaw}
/>
)}
</div>
</>
)}
</div>
)}
</>
);
};
export const DebugCheck: FC<{
checkParams: CheckParamsProps;
}> = ({ checkParams }) => (
<ProcessContent>
<LLMAndAPIContent toolMessageUnit={checkParams} />
</ProcessContent>
);

View File

@@ -0,0 +1,160 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useMemo, useRef, useState } from 'react';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { I18n } from '@coze-arch/i18n';
import { UIButton, Toast } from '@coze-arch/bot-semi';
import {
DebugExampleStatus,
PluginType,
type APIParameter,
} from '@coze-arch/bot-api/plugin_develop';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import {
transformTreeToObj,
sleep,
scrollToErrorElement,
transformParamsToTree,
} from '../utils';
import { type CheckParamsProps, STATUS } from '../types';
import s from '../index.module.less';
import ParamsForm from './params-form';
/** stringify 缩进 */
const INDENTATION_SPACES = 2;
const SLEEP_NUM = 100;
export const DebugParams: React.FC<{
requestParams: APIParameter[] | undefined;
pluginId: string;
apiId: string;
operation?: number;
btnText?: string;
callback?: (val: CheckParamsProps) => void;
disabled: boolean;
debugExampleStatus?: DebugExampleStatus;
showExampleTag?: boolean;
pluginType?: PluginType;
}> = ({
requestParams = [],
pluginId,
apiId,
operation = 1,
btnText = I18n.t('Create_newtool_s4_run'),
callback,
disabled,
debugExampleStatus = DebugExampleStatus.Default,
showExampleTag = false,
pluginType,
}) => {
const [loading, setLoading] = useState<boolean>(false);
const [check, setCheck] = useState<number>(0);
const paramsFormRef = useRef<{ data: Array<APIParameter> }>(null);
const handleAction = async () => {
// 校验是否必填
setCheck(check + 1);
await sleep(SLEEP_NUM);
const errorEle = document.getElementsByClassName('errorDebugClassTag');
if (!apiId || errorEle.length > 0) {
scrollToErrorElement('.errorDebugClassTag');
Toast.error({
content: withSlardarIdButton(I18n.t('tool_new_S2_feedback_failed')),
duration: 3,
theme: 'light',
showClose: false,
});
return false;
}
let reqParams = {};
setLoading(true);
if (
Array.isArray(paramsFormRef.current?.data) &&
(paramsFormRef.current?.data || []).length > 0
) {
reqParams = transformTreeToObj(paramsFormRef.current?.data);
}
try {
const resData = await PluginDevelopApi.DebugAPI({
plugin_id: pluginId,
api_id: apiId,
parameters: JSON.stringify(reqParams),
operation,
});
callback?.({
status: resData.success ? STATUS.PASS : STATUS.FAIL,
request: resData.raw_req,
response: resData.resp,
failReason: resData.reason,
response_params: resData.response_params,
rawResp: resData.raw_resp,
});
} catch (e) {
callback?.({
status: STATUS.FAIL,
request: JSON.stringify(reqParams, null, INDENTATION_SPACES),
response: I18n.t('plugin_exception'),
failReason: I18n.t('plugin_exception'),
});
}
setLoading(false);
};
const requestParamsData = useMemo(
() => transformParamsToTree(requestParams),
[requestParams],
);
return (
<div className={s['debug-params-box']}>
<ParamsForm
height={443}
ref={paramsFormRef}
requestParams={requestParamsData}
defaultKey="global_default"
disabled={disabled}
check={check}
debugExampleStatus={debugExampleStatus}
showExampleTag={showExampleTag}
supportFileTypeUpload
/>
{!disabled && (
<div className={s.runbtn}>
<UIButton
disabled={disabled || pluginType === PluginType.LOCAL}
style={{ width: 98 }}
loading={loading}
// theme="solid"
type="tertiary"
onClick={handleAction}
>
{btnText === I18n.t('Create_newtool_s3_button_auto') &&
(loading
? I18n.t('plugin_s3_Parsing')
: I18n.t('Create_newtool_s3_button_auto'))}
{btnText === I18n.t('Create_newtool_s4_run') &&
(loading
? I18n.t('plugin_s3_running')
: I18n.t('Create_newtool_s4_run'))}
</UIButton>
</div>
)}
</div>
);
};

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 copy from 'copy-to-clipboard';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { Space, Toast } from '@coze-arch/bot-semi';
import { MdBoxLazy } from '@coze-arch/bot-md-box-adapter/lazy';
import { IconCopy } from '@coze-arch/bot-icons';
import s from './index.module.less';
export enum HeadingType {
Request = 1,
Response = 2,
}
interface MdBoxProps {
markDown: string;
headingType: HeadingType;
rawResponse?: string;
showRaw: boolean;
}
const MAX_LENGTH = 30000;
export const DiyMdBox = ({
markDown,
headingType,
rawResponse,
showRaw,
}: MdBoxProps) => {
const getContent = () => {
if (!rawResponse) {
return '{}';
}
if (rawResponse.length < MAX_LENGTH) {
return rawResponse;
}
return `${rawResponse.slice(0, MAX_LENGTH)}...`;
};
return (
<div className={s['mb-content']}>
<div className={s['mb-header']}>
<Space spacing={8}>
<span>Json</span>
<IconCopy
className={s['icon-copy']}
onClick={() => {
copy(markDown);
Toast.success(I18n.t('copy_success'));
}}
></IconCopy>
</Space>
</div>
<div className={s['mb-main']}>
<div
className={classNames(s['mb-left'], {
[s['half-width']]: showRaw && headingType === HeadingType.Response,
})}
>
<MdBoxLazy markDown={`\`\`\`json\n${markDown}\n\`\`\``} />
</div>
{showRaw && headingType === HeadingType.Response ? (
<div className={s['mb-right']}>
<MdBoxLazy markDown={`\`\`\`json\n${getContent()}\n\`\`\``} />
</div>
) : null}
</div>
</div>
);
};

View File

@@ -0,0 +1,271 @@
/* stylelint-disable no-descending-specificity */
/* stylelint-disable max-nesting-depth */
/* stylelint-disable selector-class-pattern */
/* stylelint-disable declaration-no-important */
.process-content {
width: 100%;
height: 100%;
font-size: 12px;
font-weight: 400;
line-height: 15px;
/* 125% */
white-space: break-spaces;
background: var(--light-color-white-white, #fff);
.debug-result-content {
display: flex;
flex-direction: column;
height: 100%;
.llm-api-content {
height: calc(100% - 40px);
word-break: break-word;
}
}
}
.llm-debug-empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 14px;
color: var(--light-usage-text-color-text-3,
var(--light-usage-disabled-color-disabled-text, rgb(28 31 35 / 35%)));
.llm-debug-empty-content {
padding: 16px;
}
}
.error-reason-box {
padding: 12px;
border-top: 1px solid rgba(29, 28, 36, 8%);
}
.mb-content {
display: flex;
flex-direction: column;
height: 100%;
:global {
.auto-hide-last-sibling-br>div {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.auto-hide-last-sibling-br>div>div:first-child {
display: none;
}
.flow-markdown-body {
flex: 1;
}
}
.mb-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
padding: 0 12px;
font-size: 12px;
line-height: 40px;
color: #f7f7fa;
background-color: #41414d;
.icon-copy {
cursor: pointer;
padding: 6px;
svg {
width: 16px;
height: 16px;
path {
fill: #fff;
fill-opacity: 1;
}
}
}
}
.mb-main {
overflow-y: auto;
display: flex;
height: 100%;
background-color: #12131b;
border-top-left-radius: 0;
border-top-right-radius: 0;
.mb-left {
width: 100%;
&.half-width {
width: 50%;
}
}
.mb-right {
position: relative;
flex: none;
width: 50%;
&::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 1px;
background-color: #565563;
}
}
}
}
.debug-params-table {
overflow: auto;
display: flex;
flex: 1;
width: 100%;
.empty {
margin-top: 90px;
}
:global {
.semi-spin-block.semi-spin,
.semi-spin-children,
.semi-table-fixed-header {
display: flex;
}
.semi-table-body {
max-height: calc(100% - 40px) !important;
padding-bottom: 12px;
}
.semi-table-row {
&:has(.disable) {
display: none;
}
}
.semi-table-header {
position: relative;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 16px;
width: calc(100% - 32px);
height: 1px;
background: var(--semi-color-border);
}
}
.semi-table-placeholder {
border-bottom: 0;
}
.semi-table-tbody>.semi-table-row>.semi-table-row-cell {
padding: 8px 16px;
vertical-align: top;
border-bottom: none;
}
.semi-table-thead>.semi-table-row>.semi-table-row-head {
padding-top: 9px;
padding-bottom: 9px;
border-bottom: none;
}
.semi-table-tbody>.semi-table-row:hover>.semi-table-row-cell {
background-color: transparent;
background-image: none;
}
}
}
.debug-check-header {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: space-between;
width: 100%;
height: 40px;
padding: 0 16px;
.debug-check-tab {
display: flex;
gap: 12px;
align-items: center;
font-size: 14px;
font-weight: 600;
font-style: normal;
line-height: 20px;
.debug-check-tab-line {
width: 1px;
height: 16px;
background: var(--Light-usage-border---color-border, rgba(28, 29, 37, 12%));
}
.debug-check-tab-item {
cursor: pointer;
color: var(--Light-usage-text---color-text-2, rgba(29, 28, 36, 60%));
}
.debug-check-tab-item-active {
color: var(--Light-color-brand---brand-5, #4C54F0);
}
}
.icon {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 24px;
border: 0.75px solid rgba(29, 28, 36, 12%);
border-radius: 6px;
svg {
transform: rotate(-90deg);
width: 12px;
height: 12px;
}
&.open {
svg {
transform: rotate(90deg);
}
}
}
}

View File

@@ -0,0 +1,190 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { type FC, useEffect, useState } from 'react';
import { Typography, UIInput } from '@coze-arch/bot-semi';
import {
type APIParameter,
ParameterType,
} from '@coze-arch/bot-api/plugin_develop';
import { updateNodeById } from '../../../utils';
import { type APIParameterRecord } from '../../../types/params';
import { ARRAYTAG, ROOTTAG, ROWKEY } from '../../../config';
import { ItemErrorTip } from '../../../components/item-error-tip';
import { FileUploadItem } from '../../../components/file-upload-item';
import { getColumnClass } from './utils';
interface InputItemProps {
val?: string;
width?: number | string;
height?: number;
check?: number;
callback: (val: string) => void;
useCheck?: boolean;
useBlockWrap?: boolean;
disabled: boolean;
desc: string;
}
const InputItem = ({
val = '',
callback,
check = 0,
width = '100%',
useCheck = false,
useBlockWrap = false,
disabled,
desc,
}: InputItemProps): JSX.Element => {
const [value, setValue] = useState(val);
const [errorStatus, setErrorStatus] = useState(false);
// 通过check触发校验提交时
useEffect(() => {
if (check === 0 || value === ARRAYTAG || value === ROOTTAG) {
return;
}
handleCheck(value);
}, [check]);
const handleCheck = (v: string) => {
if (!useCheck) {
return;
}
const filterVal = v ? String(v).replace(/\s+/g, '') : '';
setErrorStatus(filterVal === '');
};
return (
<span
style={{ width, ...(useBlockWrap ? { display: 'inline-block' } : {}) }}
>
<UIInput
disabled={disabled}
value={value}
validateStatus={errorStatus ? 'error' : 'default'}
onChange={(e: string) => {
setValue(e);
callback(e);
handleCheck(e);
}}
/>
<br />
{errorStatus ? <ItemErrorTip withDescription={!!desc} /> : null}
</span>
);
};
export const ValueColRender: FC<{
record: APIParameterRecord;
disabled?: boolean;
check: number;
needCheck: boolean;
defaultKey: string;
data: Array<APIParameter>;
supportFileTypeUpload: boolean;
}> = ({
record,
data,
disabled = false,
check,
needCheck,
defaultKey,
supportFileTypeUpload = false,
}) => {
const showInput = !(
record?.type === ParameterType.Object ||
record?.type === ParameterType.Array ||
(disabled && record.value === undefined)
);
const showFile =
record?.type === ParameterType.String && !!record?.assist_type;
let renderItem = <></>;
if (supportFileTypeUpload && showFile) {
renderItem = (
<FileUploadItem
// @ts-expect-error -- linter-disable-autofix
defaultValue={record.value || record?.[defaultKey]}
// @ts-expect-error -- linter-disable-autofix
assistParameterType={record.assist_type}
onChange={uri => {
updateNodeById({
data,
targetKey: record[ROWKEY] as string,
field: 'value',
value: uri ? uri : null,
});
}}
withDescription={!!record?.desc}
required={needCheck || record?.is_required}
check={check}
disabled={disabled}
/>
);
} else if (showInput) {
renderItem = (
<div className={getColumnClass(record)}>
<InputItem
disabled={disabled}
useBlockWrap={true}
// @ts-expect-error -- linter-disable-autofix
val={record.value || record?.[defaultKey]}
check={check}
useCheck={needCheck || record?.is_required}
callback={(e: string) => {
updateNodeById({
data,
targetKey: record[ROWKEY] as string,
field: 'value',
value: e,
});
updateNodeById({
data,
targetKey: record[ROWKEY] as string,
field: defaultKey,
value: e,
});
}}
// @ts-expect-error -- linter-disable-autofix
desc={record.desc}
/>
</div>
);
}
return (
<div className="mr-[3px]">
{renderItem}
{record.desc ? (
<Typography.Text
size="small"
ellipsis={{
showTooltip: {
opts: { content: record.desc },
},
}}
style={{ verticalAlign: showInput ? 'top' : 'middle' }}
>
{record.desc}
</Typography.Text>
) : null}
</div>
);
};

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 APIParameterRecord } from '../../../types/params';
export const getColumnClass = (record: APIParameterRecord) =>
record.global_disable ? 'disable' : 'normal';

View File

@@ -0,0 +1,294 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {
type Ref,
forwardRef,
useImperativeHandle,
useMemo,
useState,
useEffect,
} from 'react';
import { set as ObjectSet, get as ObjectGet, cloneDeep } from 'lodash-es';
import { I18n } from '@coze-arch/i18n';
import { Tag } from '@coze-arch/coze-design';
import {
UIButton,
Table,
Typography,
UITag,
Space,
} from '@coze-arch/bot-semi';
import { IconAddChildOutlined } from '@coze-arch/bot-icons';
import {
type APIParameter,
ParameterType,
DebugExampleStatus,
} from '@coze-arch/bot-api/plugin_develop';
import { IconDeleteStroked } from '@douyinfe/semi-icons';
import styles from '../index.module.less';
import {
findPathById,
deleteNode,
findTemplateNodeByPath,
cloneWithRandomKey,
handleIsShowDelete,
checkHasArray,
maxDeep,
} from '../../utils';
import { type APIParameterRecord } from '../../types/params';
import {
ARRAYTAG,
ROWKEY,
childrenRecordName,
getParameterTypeLabelFromRecord,
} from '../../config';
import { getColumnClass } from './columns/utils';
import { ValueColRender } from './columns/param-value-col';
const getName = (record: APIParameterRecord) => {
const paramType = getParameterTypeLabelFromRecord(record);
return (
<span className={getColumnClass(record)}>
<Typography.Text
component="span"
ellipsis={{
showTooltip: {
type: 'tooltip',
opts: { style: { maxWidth: '100%' } },
},
}}
style={{
maxWidth: `calc(100% - ${20 * (record.deep || 1) + 49}px)`,
}}
>
{record?.name}
</Typography.Text>
{record?.is_required ? (
<Typography.Text style={{ color: 'red' }}>{' * '}</Typography.Text>
) : null}
{paramType ? (
<Tag
size="mini"
prefixIcon={null}
className="!coz-fg-color-blue !coz-mg-color-blue shrink-0 font-normal px-6px rounded-[36px] ml-4px align-middle"
>
{paramType}
</Tag>
) : null}
</span>
);
};
export interface ParamsFormProps {
requestParams?: Array<APIParameter>;
disabled: boolean;
check: number;
needCheck?: boolean;
height?: number;
defaultKey?: 'global_default' | 'local_default';
debugExampleStatus?: DebugExampleStatus;
showExampleTag?: boolean;
supportFileTypeUpload?: boolean;
}
const getParamsTitle = (isShowExampleTag: boolean, disabled: boolean) =>
isShowExampleTag ? (
<Space>
<div>
{I18n.t(
disabled
? 'mkpl_plugin_tool_parameter_description'
: 'Create_newtool_s4_value',
)}
</div>
<UITag>{I18n.t('plugin_edit_tool_test_run_example_tip')}</UITag>
</Space>
) : (
I18n.t(
disabled
? 'mkpl_plugin_tool_parameter_description'
: 'Create_newtool_s4_value',
)
);
// eslint-disable-next-line @coze-arch/max-line-per-function -- 已经在拆了
const ParamsForm = (
props: ParamsFormProps,
ref: Ref<{ data: Array<APIParameter> } | null>,
) => {
const {
requestParams,
disabled,
check,
needCheck = false,
height = 236,
defaultKey = 'global_default',
debugExampleStatus = DebugExampleStatus.Default,
showExampleTag = false,
supportFileTypeUpload = false,
} = props;
const [data, setData] = useState(
cloneDeep(requestParams ? requestParams : []),
);
const [resourceData, setResourceData] = useState(
cloneDeep(requestParams ? requestParams : []),
);
useEffect(() => {
setData(requestParams ? cloneDeep(requestParams) : []);
setResourceData(requestParams ? cloneDeep(requestParams) : []);
}, [requestParams]);
useImperativeHandle(ref, () => ({
data,
}));
const [flag, setFlag] = useState<boolean>(false);
// 添加子节点
const addChildNode = (record: APIParameter) => {
if (!data) {
return;
}
let result: APIParameter & {
path?: Array<number>;
} = {};
// 1.查找路径
findPathById({
data,
callback: (item: APIParameter, path: Array<number>) => {
if (item[ROWKEY] === record[ROWKEY]) {
result = { ...item, path };
}
},
});
// 2.拼接路径
const path = (result?.path || [])
.map((v: number) => [v, childrenRecordName])
.flat();
// newPath是模版的路径下面添加节点newNode可以直接从该路径引用
const newPath = findTemplateNodeByPath(resourceData, path);
// 3.添加节点
const newData = cloneDeep(data);
if (Array.isArray(ObjectGet(newData, path))) {
// 这一步是为了根据newPath找到对应的根节点并且克隆一个新节点
const newNode = cloneWithRandomKey(ObjectGet(resourceData, newPath)[0]);
ObjectSet(newData, path, [...ObjectGet(newData, path), newNode]);
}
setData(newData);
};
const isShowExampleTag =
disabled &&
showExampleTag &&
debugExampleStatus === DebugExampleStatus.Enable;
const maxNum = maxDeep(data);
const columns = [
{
title: I18n.t('Create_newtool_s4_name'),
key: 'name',
className: styles['no-wrap'],
width: 180 + 20 * (maxNum - 1),
minWidth: 220,
render: (record: APIParameterRecord) => getName(record),
},
{
title: getParamsTitle(isShowExampleTag, disabled),
key: 'value',
className: styles['no-wrap'],
width: 200,
// @ts-expect-error -- linter-disable-autofix
render: record => (
<ValueColRender
record={record}
data={data}
disabled={disabled}
check={check}
needCheck={needCheck}
defaultKey={defaultKey}
supportFileTypeUpload={supportFileTypeUpload}
/>
),
},
{
title: I18n.t('dataset_detail_tableTitle_actions'),
key: 'operation',
width: 120,
render: (record: APIParameter) => (
<div className={getColumnClass(record)}>
{record?.type === ParameterType.Array && (
<UIButton
onClick={() => {
addChildNode(record);
setFlag(!flag);
}}
icon={<IconAddChildOutlined />}
type="secondary"
theme="borderless"
/>
)}
{record?.name === ARRAYTAG &&
handleIsShowDelete(data, record[ROWKEY]) && (
<UIButton
onClick={() => {
const clone = cloneDeep(data);
if (record?.id) {
deleteNode(clone, record?.id);
setData(clone);
}
}}
icon={<IconDeleteStroked />}
type="secondary"
theme="borderless"
/>
)}
</div>
),
},
];
const filterColumns =
disabled || !checkHasArray(requestParams)
? columns.filter(item => item.key !== 'operation')
: columns;
const scroll = useMemo(() => ({ y: height, x: '100%' }), []);
return (
<Table
className={styles['debug-params-table']}
pagination={false}
columns={filterColumns}
dataSource={data}
rowKey={ROWKEY}
childrenRecordName={childrenRecordName}
expandAllRows={true}
scroll={scroll}
empty={
!disabled && (
<div className={styles.empty}>
{I18n.t('plugin_form_no_result_desc')}
</div>
)
}
/>
);
};
export default forwardRef(ParamsForm);

View File

@@ -0,0 +1,186 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 { useEffect, useState } from 'react';
import { I18n } from '@coze-arch/i18n';
import { UITag, Typography, Space, Col, Row } from '@coze-arch/bot-semi';
import {
type DebugExample,
type PluginType,
type PluginAPIInfo,
} from '@coze-arch/bot-api/plugin_develop';
import { type CheckParamsProps, STATUS } from './types';
import { DebugParams } from './debug-components/debug-params';
import { DebugCheck } from './debug-components/debug-check';
import s from './index.module.less';
const { Text } = Typography;
// @ts-expect-error -- linter-disable-autofix
const getApiTitle = (pluginName, name, labelKey) => (
<Text
className={s['card-title']}
ellipsis={{
showTooltip: {
opts: {
content: `${pluginName}.${name}`,
style: { wordBreak: 'break-word' },
},
},
}}
>
{pluginName}.{name} {I18n.t(labelKey)}
</Text>
);
export const Debug: React.FC<{
pluginType?: PluginType;
disabled: boolean;
apiInfo: PluginAPIInfo;
pluginId: string;
apiId: string;
pluginName: string;
debugExample?: DebugExample;
setDebugStatus?: (status: STATUS | undefined) => void;
setDebugExample?: (v: DebugExample) => void;
isViewExample?: boolean; // 查看 example 模式 标题不一样
onSuccessCallback?: () => void;
}> = ({
disabled,
apiInfo,
pluginId,
apiId,
pluginName,
setDebugStatus,
debugExample,
setDebugExample,
isViewExample = false,
pluginType,
onSuccessCallback,
}) => {
const [checkParams, setCheckParams] = useState<CheckParamsProps>({});
const [status, setStatus] = useState<STATUS | undefined>();
const handleAction = ({
status: innerStatus,
request,
response,
failReason,
rawResp,
}: CheckParamsProps) => {
setStatus(innerStatus);
setCheckParams({
status: innerStatus,
request,
response,
failReason,
rawResp,
});
setDebugStatus?.(innerStatus);
innerStatus === STATUS.PASS &&
setDebugExample?.({ req_example: request, resp_example: response });
// 调试成功后回调
innerStatus === STATUS.PASS && onSuccessCallback?.();
};
useEffect(() => {
if (debugExample) {
setCheckParams({
...checkParams,
request: debugExample?.req_example,
response: debugExample?.resp_example,
failReason: '',
});
} else {
setCheckParams({});
}
}, [debugExample]);
return (
<div
className={s['debug-check']}
data-testid="plugin.tool.debug-modal-content"
>
<Row gutter={16}>
<Col span={12}>
<div className={s['main-container']}>
<div className={s['card-header']}>
{isViewExample ? (
<Text className={s['card-title']}>
{I18n.t('Create_newtool_s4_title')}
</Text>
) : (
getApiTitle(pluginName, apiInfo.name, 'Create_newtool_s4_title')
)}
</div>
<div
style={{
maxHeight: isViewExample ? 'calc(100% - 55px)' : 542,
display: 'flex',
}}
>
<DebugParams
pluginType={pluginType}
disabled={disabled}
pluginId={pluginId}
apiId={apiId}
requestParams={apiInfo?.request_params}
callback={handleAction}
debugExampleStatus={apiInfo?.debug_example_status}
showExampleTag={!isViewExample}
/>
</div>
</div>
</Col>
<Col span={12}>
<div className={s['main-container']}>
<div className={s['card-header']}>
<Space style={{ width: '100%' }}>
{isViewExample ? (
<Text className={s['card-title']}>
{I18n.t('plugin_edit_tool_test_run_debugging_example')}
</Text>
) : (
getApiTitle(
pluginName,
apiInfo.name,
'Create_newtool_s4_result',
)
)}
{status === STATUS.PASS && (
<UITag color="green">{I18n.t('plugin_s4_debug_pass')}</UITag>
)}
{status === STATUS.FAIL && (
<UITag color="red">{I18n.t('plugin_s4_debug_failed')}</UITag>
)}
</Space>
</div>
<div
className={s['card-debug-check']}
style={{
height: isViewExample ? '100%' : 542,
}}
>
<DebugCheck checkParams={checkParams} />
</div>
</div>
</Col>
</Row>
</div>
);
};

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 { type AssistParameterType } from '@coze-arch/bot-api/plugin_develop';
import {
FILE_TYPE_CONFIG,
type FileTypeEnum,
} from '@coze-studio/file-kit/logic';
import { ACCEPT_UPLOAD_TYPES } from '@coze-studio/file-kit/config';
import { assistToExtend, parameterTypeExtendMap } from './config';
export const getFileAccept = (type: AssistParameterType) => {
const { fileTypes } = parameterTypeExtendMap[assistToExtend(type)];
const accept = fileTypes?.reduce((prev, curr) => {
const config = FILE_TYPE_CONFIG.find(c => c.fileType === curr);
if (!config) {
return prev;
}
prev = `${prev}${prev ? ',' : ''}${config.accept.join(',')}`;
return prev;
}, '');
if (!accept || accept === '*') {
return undefined;
}
return accept;
};
export const getFileTypeFromAssistType = (
type: AssistParameterType,
): FileTypeEnum | null => {
if (!type) {
return null;
}
const extendType = assistToExtend(type);
const config = Object.entries(parameterTypeExtendMap).find(
([key]) => Number(key) === extendType,
);
if (!config) {
return null;
}
for (const fileType of config[1].fileTypes) {
const iconConfig = ACCEPT_UPLOAD_TYPES[fileType];
if (iconConfig) {
return fileType;
}
}
return null;
};

View File

@@ -0,0 +1,372 @@
/* stylelint-disable declaration-no-important */
.create-modal {
:global {
.semi-modal {
max-width: 1800px;
}
.semi-table-row-cell {
overflow: hidden;
}
}
&.big-modal {
.modal-steps {
width: 810px;
margin: 0 auto;
margin-bottom: 24px;
}
}
}
.textarea-single-line {
:global {
.semi-input-textarea-counter {
position: absolute;
top: 3px;
right: 0;
}
}
}
.no-wrap {
white-space: nowrap;
}
.no-wrap-min-width {
white-space: nowrap;
}
.params-layout {
display: flex;
justify-content: space-between;
}
.params-tag {
margin-bottom: 18px;
padding-top: 22px;
font-size: 18px;
font-weight: 600;
}
.request-params,
.response-params {
:global {
.semi-table-placeholder {
padding: 1px 12px;
border-bottom: 0;
}
}
}
.request-params-edit,
.response-params-edit {
:global {
.semi-table-thead .semi-table-row-head:first-child {
padding-left: 32px !important;
}
.semi-table-placeholder {
padding: 1px 12px;
border-bottom: 0;
}
}
}
.check-box {
position: absolute;
}
.form-check-tip {
position: absolute;
top: 4px;
right: 0;
left: 0;
transform-origin: left;
display: inline-block;
font-size: 12px !important;
line-height: 16px;
color: var(--semi-color-danger);
}
.w110 {
width: 110%;
}
.plugin-icon-error {
position: relative;
top: 2px;
margin-right: 4px;
font-size: 13px;
}
.plugin-tooltip-error {
width: calc(100% - 20px);
font-size: 12px !important;
line-height: 16px !important;
color: var(--semi-color-danger) !important;
}
.add-params-btn-wrap {
margin: 0 24px;
padding-bottom: 12px;
border-top: 1px solid var(--semi-color-border);
}
.empty-content {
margin: 36px 0 54px;
font-size: 14px;
color: var(--light-usage-text-color-text-2, rgb(28 31 35 / 60%));
text-align: center;
}
// hover统一样式
.table-style-list {
:global {
.semi-table-body {
padding: 12px 0;
}
.semi-select {
border-radius: 8px;
}
.semi-table-row-cell {
padding: 12px 2px !important;
}
.semi-table-expand-icon {
margin-right: 8px;
}
.semi-table-header {
position: relative;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 24px;
width: calc(100% - 48px);
height: 1px;
background: var(--semi-color-border);
}
}
.semi-table-thead .semi-table-row-head:first-child {
padding-left: 32px !important;
}
.semi-table-tbody > .semi-table-row > .semi-table-row-cell {
border-bottom-color: transparent;
}
.semi-table-thead > .semi-table-row > .semi-table-row-head {
padding-right: 10px;
padding-left: 10px;
font-size: 12px;
font-weight: 600;
color: var(--light-usage-text-color-text-1, rgb(28 29 35 / 80%));
background: #f7f7fa;
border-bottom: 1px solid transparent;
}
.semi-table-row:hover > .semi-table-row-cell {
background: transparent !important;
border-bottom: 1px solid transparent !important;
}
.semi-table-tbody > .semi-table-row,
.semi-table-tbody > .semi-table-row > .semi-table-cell-fixed-left,
.semi-table-tbody > .semi-table-row > .semi-table-cell-fixed-right,
.semi-table-thead
> .semi-table-row
> .semi-table-row-head.semi-table-cell-fixed-left::before,
.semi-table-thead
> .semi-table-row
> .semi-table-row-head.semi-table-cell-fixed-right::before {
cursor: pointer;
font-size: 12px;
font-weight: 400;
font-style: normal;
color: var(--light-usage-text-color-text-2, rgb(28 29 35 / 60%));
background: #f7f7fa;
}
.semi-spin-block.semi-spin {
height: 100%;
}
.semi-table-row:hover > .semi-table-row-cell:first-child {
border-top-left-radius: 8px !important;
border-bottom-left-radius: 8px !important;
}
.semi-table-row:hover > .semi-table-row-cell:last-child {
border-top-right-radius: 8px !important;
border-bottom-right-radius: 8px !important;
}
.semi-icon-chevron_down {
opacity: 0.6;
}
}
&.request-params,
&.response-params {
:global {
.semi-table-tbody > .semi-table-row > .semi-table-row-cell {
padding-left: 16px !important;
}
}
}
}
.input-modal {
.runbtn {
padding: 12px;
text-align: right;
}
:global {
.semi-modal-footer {
margin: 0 0 12px;
}
}
.debug-params-box {
:global {
.semi-table-thead > .semi-table-row > .semi-table-row-head {
border-bottom-width: 1px;
}
.semi-table-tbody > .semi-table-row:hover > .semi-table-row-cell {
background: transparent !important;
border-bottom: 1px solid transparent !important;
}
}
}
}
.debug-check {
overflow: hidden;
height: 100%;
padding-bottom: 11px;
:global{
.semi-row, .semi-col{
height: 100%;
}
}
.main-container {
display: flex;
flex-direction: column;
max-width: 100vw;
height: 100%;
}
.card-header {
margin-bottom: 14px;
padding: 8px 0;
}
.card-title {
font-size: 18px;
font-weight: 600;
font-style: normal;
color: var(--light-usage-text-color-text-0, #1c1f23);
text-overflow: ellipsis;
}
.card-debug-check {
overflow: auto;
height: 100%;
max-height: 542px;
background: #fff;
border: 1px solid var(--Light-usage-border---color-border, rgba(29, 28, 37, 8%));
border-radius: 8px;
:global {
.markdown-body {
overflow: hidden;
}
}
}
.debug-params-box {
display: flex;
flex-direction: column;
width: 100%;
border: 1px solid rgb(29 28 35 / 8%);
border-radius: 8px;
.runbtn {
margin: 0 16px;
padding: 12px 0;
text-align: right;
border-top: 1px solid var(--semi-color-border);
:global {
.semi-button.semi-button-loading {
color: rgb(29 28 35 / 20%);
}
}
}
}
}
.safe-check-error {
color: #f93920;
a {
color: #4d53e8;
}
}
.base-info-form {
:global {
.semi-icon-chevron_down {
opacity: 0.6;
}
}
.plugin-url-input {
:global {
.semi-input-prepend {
border: none;
}
}
}
.plugin-url-prefix {
max-width: 480px;
}
}
.table-wrapper {
border: 1px solid rgb(29 28 35 / 8%);
border-radius: 8px;
}
.cascader-dropdown {
:global {
.semi-cascader-option-label {
font-weight: 400;
color: #1d1c23;
}
}
}

View File

@@ -0,0 +1,21 @@
.action-input-value-pre{
width: 115px;
border-right: none;
}
.action-input-value-content{
flex: 1;
}
.reference-option-item{
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
width: 200px;
.reference-option-subtext{
color:rgba(29, 28, 35, 35%)
}
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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';
import { type OptionProps } from '@coze-arch/bot-semi/Select';
import {
UIInput,
UISelect,
InputGroup,
Typography,
} from '@coze-arch/bot-semi';
import {
DefaultParamSource,
type APIParameter,
} from '@coze-arch/bot-api/plugin_develop';
import styles from './index.module.less';
interface InputAndVariableItemProps {
record: APIParameter;
disabled?: boolean;
onSourceChange?: (val: number) => void;
onReferenceChange?: (val: string) => void;
onValueChange?: (val: string) => void;
referenceOption?: OptionProps[];
}
export const InputAndVariableItem = ({
record,
disabled,
onSourceChange,
onReferenceChange,
onValueChange,
referenceOption,
}: InputAndVariableItemProps) => (
<InputGroup style={{ width: '100%', flexWrap: 'nowrap' }}>
<UISelect
theme="light"
className={styles['action-input-value-pre']}
value={record.default_param_source || DefaultParamSource.Input}
disabled={disabled}
optionList={[
{
label: I18n.t(
'bot_ide_plugin_setting_modal_default_value_select_mode_reference',
),
value: DefaultParamSource.Variable,
},
{
label: I18n.t(
'bot_ide_plugin_setting_modal_default_value_select_mode_input',
),
value: DefaultParamSource.Input,
},
]}
onChange={val => {
onSourceChange?.(Number(val));
// 切换来源,清空默认值
onReferenceChange?.('');
onValueChange?.('');
}}
/>
{record.default_param_source === DefaultParamSource.Variable ? (
<UISelect
theme="light"
disabled={disabled}
style={{ width: '100%', overflow: 'hidden' }}
className={styles['action-input-value-content']}
placeholder={I18n.t(
'bot_ide_plugin_setting_modal_default_value_select_mode_reference_placeholder',
)}
value={record.variable_ref}
onChange={val => {
onReferenceChange?.(String(val));
}}
>
{referenceOption?.map(item => (
<UISelect.Option key={String(item.label)} value={String(item.label)}>
<div className={styles['reference-option-item']}>
<Typography.Text
className={styles['reference-option-text']}
ellipsis={{
showTooltip: {
opts: {
content: item.label,
style: { wordBreak: 'break-word' },
},
},
}}
>
{item.label}
</Typography.Text>
<Typography.Text
className={styles['reference-option-subtext']}
ellipsis={{
showTooltip: {
opts: {
content: item.value,
style: { wordBreak: 'break-word' },
},
},
}}
>
{item.value}
</Typography.Text>
</div>
</UISelect.Option>
))}
</UISelect>
) : (
<UIInput
disabled={disabled}
className={styles['action-input-value-content']}
placeholder={I18n.t(
'bot_ide_plugin_setting_modal_default_value_select_mode_input_placeholder',
)}
value={record.local_default}
onChange={val => {
onValueChange?.(String(val));
}}
/>
)}
</InputGroup>
);

View File

@@ -0,0 +1,138 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 FC } from 'react';
import { cloneDeep } from 'lodash-es';
import {
type APIParameter,
ParameterType,
} from '@coze-arch/bot-api/plugin_develop';
import { deleteAllChildNode } from '../../utils';
import {
type AddChildNodeFn,
type APIParameterRecord,
type UpdateNodeWithDataFn,
} from '../../types/params';
import {
ARRAYTAG,
getParameterTypeLabelFromRecord,
ParameterTypeExtend,
ROWKEY,
} from '../../config';
import { CascaderItem } from '../../components/cascader-item';
import { type ColumnsProps } from '..';
interface ParamTypeProps
extends Pick<
ColumnsProps,
'data' | 'setData' | 'disabled' | 'checkFlag' | 'isResponse'
> {
record: APIParameterRecord;
updateNodeWithData: UpdateNodeWithDataFn;
addChildNode: AddChildNodeFn;
enableFileType?: boolean;
}
const ParamTypeColRender: FC<ParamTypeProps> = ({
record,
disabled,
data,
setData,
checkFlag,
isResponse,
updateNodeWithData,
addChildNode,
enableFileType = false,
}) => {
// 删除全部子节点;
const handleDeleteAllChildNode = (r: APIParameter) => {
const cloneData = cloneDeep(data);
const delStatus = deleteAllChildNode(cloneData, r[ROWKEY] as string);
if (delStatus) {
setData(cloneData);
}
};
if (disabled) {
return (
<>{getParameterTypeLabelFromRecord(record, record.name === ARRAYTAG)}</>
);
}
return (
<CascaderItem
check={checkFlag}
record={record}
enableFileType={enableFileType}
selectCallback={([cascaderType, assistType]) => {
let type = cascaderType;
if (cascaderType === ParameterTypeExtend.DEFAULT) {
type = ParameterType.String;
}
if (!isResponse) {
// 切换类型重置default value
if (record.global_default) {
updateNodeWithData({
record,
key: ['global_default', 'global_disable'],
value: ['', false],
updateData: true,
});
}
}
const payload = {
record,
key: ['type', 'assist_type'],
value: [type, assistType ?? null],
};
// updateNodeWithData 会变更type类型保留原始的type
const recordType = record?.type;
if (type === ParameterType.Array) {
updateNodeWithData({
...payload,
updateData: true,
});
addChildNode({ record, isArray: true, type, recordType });
} else if (type === ParameterType.Object) {
updateNodeWithData({
...payload,
updateData: true,
});
addChildNode({ record, isArray: false, type, recordType });
} else if (
record?.type === ParameterType.Array ||
record?.type === ParameterType.Object
) {
updateNodeWithData(payload);
handleDeleteAllChildNode(record);
} else {
updateNodeWithData({
...payload,
updateData: true,
});
}
}}
/>
);
};
export default ParamTypeColRender;

View File

@@ -0,0 +1,224 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 { useCallback, useRef, useState } from 'react';
import { cloneDeep } from 'lodash-es';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { I18n } from '@coze-arch/i18n';
import { type OptionProps } from '@coze-arch/bot-semi/Select';
import { Toast, UIButton, UIModal } from '@coze-arch/bot-semi';
import { IconEdit } from '@coze-arch/bot-icons';
import {
ParameterType,
type APIParameter,
} from '@coze-arch/bot-api/plugin_develop';
import {
scrollToErrorElement,
transformArrayToTree,
transformTreeToObj,
updateNodeById,
} from '../utils';
import { InputAndVariableItem } from '../input-and-variable';
import ParamsForm from '../debug-components/params-form';
import { ROWKEY } from '../config';
import { InputItem } from './form-components';
import styles from './index.module.less';
interface DefaultValueInputProps {
record: APIParameter;
data: Array<APIParameter>;
defaultKey?: 'global_default' | 'local_default';
disableKey?: 'global_disable' | 'local_disable';
setData: (val: Array<APIParameter>) => void;
canReference?: boolean;
referenceOption?: OptionProps[];
}
interface DefaultModalProps {
record: APIParameter;
defaultKey: 'global_default' | 'local_default';
disableKey: 'global_disable' | 'local_disable';
updateNodeAndData: (key: string, value: string) => void;
}
const DefaultValueModal = ({
record,
defaultKey,
disableKey,
updateNodeAndData,
}: DefaultModalProps) => {
const [check, setCheck] = useState(0);
const [visible, setVisible] = useState(false);
const paramsFormRef = useRef<{ data: Array<APIParameter> }>(null);
const [defRecord, setDefRecord] = useState<APIParameter>(
{} satisfies APIParameter,
);
const handleOpen = useCallback(() => {
setVisible(true);
const r = cloneDeep(record);
if (r[defaultKey]) {
const tree = transformArrayToTree(
JSON.parse(r[defaultKey] || '[]'),
r.sub_parameters || [],
);
r.sub_parameters = tree;
}
setDefRecord(r);
}, [record]);
const handleClose = () => {
setVisible(false);
setDefRecord({} satisfies APIParameter);
};
const handleSave = () => {
// 校验是否必填
setCheck(check + 1);
const errorEle = document.getElementsByClassName('errorDebugClassTag');
if (errorEle.length > 0) {
scrollToErrorElement('.errorDebugClassTag');
Toast.error({
content: withSlardarIdButton(I18n.t('tool_new_S2_feedback_failed')),
duration: 3,
theme: 'light',
showClose: false,
});
return;
}
const reqParams = Object.values(
transformTreeToObj(paramsFormRef.current?.data, false),
);
updateNodeAndData(defaultKey, JSON.stringify(reqParams[0]));
handleClose();
};
return (
<>
<UIButton
disabled={record.is_required && record[disableKey]}
icon={<IconEdit />}
className={styles['arr-edit-btn']}
style={{ width: '100%' }}
onClick={handleOpen}
>
{I18n.t('plugin_edit_tool_default_value_array_edit_button')}
</UIButton>
{visible ? (
<UIModal
title={I18n.t(
'plugin_edit_tool_default_value_array_edit_modal_title',
)}
width={792}
okText={I18n.t('Save')}
visible={visible}
onCancel={handleClose}
hasCancel={false}
onOk={handleSave}
zIndex={1050}
>
<ParamsForm
ref={paramsFormRef}
requestParams={[defRecord]}
defaultKey={defaultKey}
disabled={false}
check={check}
needCheck={false}
height={400}
/>
</UIModal>
) : null}
</>
);
};
export const DefaultValueInput = ({
record,
data,
setData,
canReference = false,
defaultKey = 'global_default', //输入框的key
disableKey = 'global_disable', //开启按钮key
referenceOption,
}: DefaultValueInputProps) => {
// @ts-expect-error -- linter-disable-autofix
const updateNodeAndData = (key, value) => {
updateNodeById({
data,
targetKey: record[ROWKEY] as string,
field: key,
value,
});
const cloneData = cloneDeep(data);
setData(cloneData);
};
if (record[defaultKey] === undefined) {
return <></>;
}
// 复杂类型暂不支持引用变量
if (record.type === ParameterType.Array) {
return (
<div className={styles['modal-wrapper']}>
<DefaultValueModal
record={record}
defaultKey={defaultKey}
disableKey={disableKey}
updateNodeAndData={updateNodeAndData}
/>
</div>
);
}
return (
<>
{canReference ? (
<InputAndVariableItem
record={record}
disabled={!!record[disableKey]}
referenceOption={referenceOption}
onSourceChange={val => {
updateNodeAndData('default_param_source', val);
}}
onReferenceChange={val => {
updateNodeAndData('variable_ref', val);
}}
onValueChange={val => {
updateNodeAndData(defaultKey, val);
}}
/>
) : (
<InputItem
width="100%"
placeholder={I18n.t(
'plugin_edit_tool_default_value_input_placeholder',
)}
max={2000}
val={record[defaultKey]}
useCheck={false}
filterSpace={false}
disabled={!!record[disableKey]}
callback={(e: string) => {
updateNodeAndData(defaultKey, e);
}}
/>
)}
</>
);
};

View File

@@ -0,0 +1,306 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function */
import { useEffect, useState } from 'react';
import cl from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { UIInput, UISelect, Typography, Tooltip } from '@coze-arch/bot-semi';
import { IconInfo } from '@coze-arch/bot-icons';
import { ParameterType } from '@coze-arch/bot-api/plugin_develop';
import { IconAlertCircle } from '@douyinfe/semi-icons';
import { checkSameName } from '../utils';
import { type InputItemProps } from '../types';
import s from '../index.module.less';
import {
ARRAYTAG,
ParamsFormErrorStatus,
paramsFormErrorStatusText,
ROOTTAG,
} from '../config';
const DEEP_INDENT_NUM = 20;
export const InputItem = ({
val = '',
max = 500,
check = 0,
width = 200,
useCheck = true,
filterSpace = true,
placeholder,
callback,
targetKey = '',
checkSame = false,
checkAscii = false,
isRequired = false,
data,
useBlockWrap = false,
disabled,
dynamicWidth = false,
deep = 1,
}: InputItemProps): JSX.Element => {
const [value, setValue] = useState(val);
const [errorStatus, setErrorStatus] = useState<number>(0);
useEffect(() => {
setValue(val);
}, [val]);
// 通过check触发校验提交时
useEffect(() => {
if (check === 0 || value === ARRAYTAG || value === ROOTTAG) {
return;
}
handleCheck(value);
}, [check]);
// 校验
const handleCheck = (checkVal: string) => {
let status =
checkVal === ''
? ParamsFormErrorStatus.NAME_EMPTY
: ParamsFormErrorStatus.NO_ERROR;
if (isRequired && checkVal === '') {
setErrorStatus(ParamsFormErrorStatus.DESC_EMPTY);
return;
}
if (checkAscii) {
if (!IS_OVERSEA) {
setErrorStatus(ParamsFormErrorStatus.NO_ERROR);
return;
}
// eslint-disable-next-line no-control-regex
status = /^[\x00-\x7F]+$/.test(checkVal)
? ParamsFormErrorStatus.NO_ERROR
: ParamsFormErrorStatus.ASCII;
status = checkVal === '' ? ParamsFormErrorStatus.NO_ERROR : status;
setErrorStatus(status);
}
if (!useCheck) {
return;
}
if (!status) {
status = !/^[\w-]+$/.test(checkVal)
? ParamsFormErrorStatus.CHINESE
: ParamsFormErrorStatus.NO_ERROR;
}
if (!status && data && checkSame) {
status = checkSameName(data, targetKey, checkVal)
? ParamsFormErrorStatus.REPEAT
: ParamsFormErrorStatus.NO_ERROR;
}
setErrorStatus(status);
};
// 过滤空格、限制输入长度
const handleFilter = (v: string) => {
if (filterSpace) {
v = v.replace(/\s+/g, '');
}
if (max > 0) {
v = v.slice(0, max);
}
return v;
};
const hasSub =
deep === 1
? data?.some(
item =>
item.type === ParameterType.Array ||
item.type === ParameterType.Object,
)
: true;
// 每增加一层因为有展开icon宽度减少20
const vWidth = dynamicWidth
? `calc(100% - ${DEEP_INDENT_NUM * deep}px)`
: width;
const tipWidth = dynamicWidth
? `calc(100% - ${DEEP_INDENT_NUM * deep}px - 8px)`
: width;
const errorStatusMsg = () => (
<>
{dynamicWidth && !hasSub ? (
<span style={{ display: 'inline-block', width: 22 }}></span>
) : null}
<Typography.Text
component="span"
ellipsis={{
showTooltip: {
type: 'tooltip',
opts: { style: { maxWidth: '100%' } },
},
}}
className={s['plugin-tooltip-error']}
>
{/* @ts-expect-error -- linter-disable-autofix */}
<span>{paramsFormErrorStatusText[errorStatus]}</span>
</Typography.Text>
</>
);
return (
<span
style={useBlockWrap ? { display: 'inline-block', width: '100%' } : {}}
>
{dynamicWidth && !hasSub ? (
<span style={{ display: 'inline-block', width: 20 }}></span>
) : null}
<UIInput
placeholder={placeholder}
disabled={disabled || value === ARRAYTAG || value === ROOTTAG}
style={{ width: vWidth }}
value={value}
validateStatus={errorStatus ? 'error' : 'default'}
onChange={(e: string) => {
const newVal = handleFilter(e);
callback?.(newVal);
setValue(newVal);
handleCheck(newVal);
}}
onBlur={() => {
handleCheck(value);
}}
/>
<br />
{/* 参数名称设置动态列宽 */}
{errorStatus !== 0 && dynamicWidth ? (
<div className={s['check-box']} style={{ width: tipWidth }}>
<span className={cl(s['form-check-tip'], 'errorClassTag', s.w110)}>
{errorStatusMsg()}
</span>
</div>
) : null}
{/* 非参数列表设置固定最大宽 */}
{errorStatus !== 0 && !dynamicWidth && (
<div className={s['check-box']} style={{ width: tipWidth }}>
<span
style={{
marginLeft: 4,
right: -15,
}}
className={cl(s['form-check-tip'], 'errorClassTag')}
>
{errorStatusMsg()}
</span>
</div>
)}
</span>
);
};
export const SelectItem = ({
check = 0,
useBlockWrap = false,
record,
disabled,
typeOptions,
selectCallback,
}: InputItemProps): JSX.Element => {
const [value, setValue] = useState(!record?.type ? undefined : record?.type);
const [errorStatus, setErrorStatus] = useState<number>(0);
// 通过check触发校验提交时
useEffect(() => {
if (check === 0) {
return;
}
handleCheck(value);
}, [check]);
// 校验
const handleCheck = (val: string | ParameterType | undefined) => {
const status = val === undefined ? 1 : 0;
setErrorStatus(status);
};
return (
<span
style={useBlockWrap ? { display: 'inline-block', width: '100%' } : {}}
>
<UISelect
theme="light"
validateStatus={errorStatus ? 'error' : 'default'}
value={value}
disabled={disabled}
onChange={e => {
selectCallback?.(e);
setValue(e as ParameterType);
handleCheck(e as ParameterType);
}}
style={{ width: '100%' }}
>
{typeOptions?.map(item => (
<UISelect.Option
key={(record?.id || '') + item.label}
value={item.value}
>
{item.label}
</UISelect.Option>
))}
</UISelect>
<br />
{errorStatus !== 0 && (
<div style={{ position: 'relative' }}>
<span className={cl(s['form-check-tip'], 'errorClassTag', s.w110)}>
<IconAlertCircle className={s['plugin-icon-error']} />
<Typography.Text
component="span"
ellipsis={{
showTooltip: {
type: 'tooltip',
opts: { style: { maxWidth: '100%' } },
},
}}
className={s['plugin-tooltip-error']}
>
{errorStatus === 1 && (
<span>{I18n.t('plugin_Parameter_type')}</span>
)}
</Typography.Text>
</span>
</div>
)}
</span>
);
};
interface FormTitle {
name: string;
required?: boolean;
toolTipText?: string;
}
export const FormTitle = (titleInfo: FormTitle) => (
<div className="whitespace-nowrap">
{titleInfo.name}
{titleInfo.required ? (
<Typography.Text style={{ color: 'red', marginLeft: -3 }}>
{' * '}
</Typography.Text>
) : null}
{titleInfo.toolTipText ? (
<Tooltip content={titleInfo.toolTipText}>
<IconInfo
style={{
color: '#5f5f5f9e',
position: 'relative',
top: 3,
left: 2,
}}
/>
</Tooltip>
) : null}
</div>
);

View File

@@ -0,0 +1,36 @@
/* stylelint-disable declaration-no-important */
.modal-wrapper {
.arr-edit-btn {
background-color: #f7f7fa !important;
border-color: rgba(29, 28, 37, 12%) !important;
svg {
path:first-child {
stroke: #4D53E8;
stroke-opacity: 1;
}
path:last-child {
fill: #4D53E8;
fill-opacity: 1;
}
}
}
:global {
.semi-button-disabled {
color: rgba(56, 55, 67, 20%) !important;
background-color: rgba(75, 74, 88, 4%) !important;
svg {
path:first-child {
stroke: rgba(56, 55, 67, 20%);
}
path:last-child {
fill: rgba(56, 55, 67, 20%);
}
}
}
}
}

View File

@@ -0,0 +1,565 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function -- 历史逻辑 陆续在拆 */
/* eslint-disable max-lines -- 历史逻辑 陆续在拆 */
import { cloneDeep, flow, get as ObjectGet, set as ObjectSet } from 'lodash-es';
import { I18n } from '@coze-arch/i18n';
import { ArrayUtil } from '@coze-arch/bot-utils';
import { type ColumnProps } from '@coze-arch/bot-semi/Table';
import {
UIIconButton,
UISelect,
Typography,
Tooltip,
Switch,
Space,
Checkbox,
} from '@coze-arch/bot-semi';
import { IconAddChildOutlined, IconDeleteOutline } from '@coze-arch/bot-icons';
import {
type APIParameter,
ParameterType,
} from '@coze-arch/bot-api/plugin_develop';
import {
defaultNode,
deleteAllChildNode,
deleteNode,
findPathById,
handleIsShowDelete,
updateNodeById,
} from '../utils';
import {
type UpdateNodeWithDataFn,
type APIParameterRecord,
type AddChildNodeFn,
} from '../types';
import s from '../index.module.less';
import {
childrenRecordName,
parameterLocationOptions,
ROWKEY,
} from '../config';
import { FormTitle, InputItem } from './form-components';
import { DefaultValueInput } from './default-value-input';
import ParamTypeColRender from './columns/param-type-col';
const DEEP_INDENT_NUM = 20;
const DISABLED_REQ_SLICE = -4;
const DISABLED_RES_SLICE = -3;
export interface ColumnsProps {
data: Array<APIParameter>;
flag: boolean;
checkFlag: number;
isResponse?: boolean;
disabled: boolean;
setCheckFlag: (val: number) => void;
setFlag: (val: boolean) => void;
setData: (val: Array<APIParameter>, checkDefault?: boolean) => void;
showSecurityCheckFailedMsg: boolean;
setShowSecurityCheckFailedMsg: (flag: boolean) => void;
/**
* 是否支持扩展的文件类型
*/
enableFileType?: boolean;
}
// eslint-disable-next-line max-lines-per-function
export const getColumns = ({
data,
checkFlag,
isResponse = false,
disabled,
setCheckFlag,
setData,
showSecurityCheckFailedMsg,
setShowSecurityCheckFailedMsg,
enableFileType = false,
}: ColumnsProps) => {
// 添加子节点
const addChildNode: AddChildNodeFn = ({
record,
isArray = false,
type,
recordType,
}) => {
const newData = cloneDeep(data);
// @ts-expect-error -- linter-disable-autofix
const deleteArrayGlobalDefaultByPath = (obj: APIParameter[], path) => {
const index = path[0];
const child = obj[index];
if (child && child.type === ParameterType.Array) {
child.global_default = '';
child.global_disable = false;
} else {
if (child && child.sub_parameters) {
deleteArrayGlobalDefaultByPath(child.sub_parameters, path.slice(1));
}
}
};
setCheckFlag(0);
let result: APIParameter & {
path?: Array<number>;
} = {};
// 1.查找路径
findPathById({
data,
callback: (item: APIParameter, path: Array<number>) => {
if (item[ROWKEY] === record[ROWKEY]) {
result = { ...item, path };
// 修改复杂类型结构,需要重置数组的默认值
deleteArrayGlobalDefaultByPath(newData, path);
}
},
});
// 2.拼接路径
const path = (result?.path || [])
.map((v: number) => [v, childrenRecordName])
.flat();
// 如果是添加子节点,则更新父节点中的类型
if (recordType) {
const typePath = cloneDeep(path);
typePath.pop();
typePath.push('type');
// type 为4/5切换节点的时候需要先删除子节点
// recordType 原节点的类型
// newData 新节点数据
if (ObjectGet(newData, typePath) !== recordType) {
deleteAllChildNode(newData, record[ROWKEY] as string);
}
ObjectSet(newData, typePath, type);
}
// 3.添加节点
if (Array.isArray(ObjectGet(newData, path))) {
ObjectSet(newData, path, [
...ObjectGet(newData, path),
// @ts-expect-error -- linter-disable-autofix
defaultNode({ isArray, iscChildren: true, deep: record.deep + 1 }),
]);
} else {
ObjectSet(newData, path, [
defaultNode({
isArray,
iscChildren: true,
// @ts-expect-error -- linter-disable-autofix
deep: record.deep + 1,
}),
]);
}
setData(newData);
};
// 删除子节点
const deleteChildNode = (record: APIParameter) => {
const cloneData = cloneDeep(data);
const delStatsu = deleteNode(cloneData, record[ROWKEY] as string);
if (delStatsu) {
setData(cloneData);
}
};
const updateNodeWithData: UpdateNodeWithDataFn = ({
record,
key,
value,
updateData = false,
inherit = false,
}) => {
if (Array.isArray(key)) {
key.forEach((item, idx) => {
updateNodeById({
data,
targetKey: record[ROWKEY] as string,
field: item,
// @ts-expect-error -- linter-disable-autofix
value: value[idx],
});
});
} else {
updateNodeById({
data,
targetKey: record[ROWKEY] as string,
field: key,
value,
inherit,
});
}
if (updateData) {
const cloneData = cloneDeep(data);
setData(cloneData);
}
};
const columns: Array<ColumnProps<APIParameter>> = [
{
title: () => (
<FormTitle
name={I18n.t('Create_newtool_s3_table_name')}
required
toolTipText={
isResponse
? I18n.t('Create_newtool_s3_table_name_tooltip')
: I18n.t('Create_newtool_s2_table_name_tooltip')
}
/>
),
key: 'name',
className: s['no-wrap-min-width'],
render: (record: APIParameter & { deep?: number }) =>
disabled ? (
<Typography.Text
component="span"
ellipsis={{
showTooltip: {
type: 'tooltip',
opts: { style: { maxWidth: '100%' } },
},
}}
style={{
maxWidth: `calc(100% - ${
DEEP_INDENT_NUM * (record.deep || 1)
}px)`,
}}
>
{record.name}
</Typography.Text>
) : (
<InputItem
check={checkFlag}
val={record?.name}
data={data}
placeholder={I18n.t('Create_newtool_s2_table_name_empty')}
useBlockWrap={true}
checkSame={true}
targetKey={record[ROWKEY]}
dynamicWidth={true}
deep={record.deep}
callback={(e: string) => {
// record.name = e;
updateNodeWithData({
record,
key: 'name',
value: e,
updateData: true,
});
if (showSecurityCheckFailedMsg) {
setShowSecurityCheckFailedMsg?.(false);
}
}}
/>
),
},
{
title: () => (
<FormTitle
name={I18n.t('Create_newtool_s2_table_des')}
required={!isResponse}
toolTipText={
isResponse
? I18n.t('Create_newtool_s3_table_des_tooltip')
: I18n.t('Create_newtool_s2_table_des_tooltip')
}
/>
),
key: 'desc',
render: (record: APIParameter) =>
// ,帮助用户/大模型更好地理解。
disabled ? (
<Typography.Text
component="div"
ellipsis={{
showTooltip: {
opts: {
style: { wordBreak: 'break-word' },
},
},
}}
style={{ maxWidth: '100%' }}
>
{record.desc}
</Typography.Text>
) : (
<InputItem
check={checkFlag}
width="100%"
placeholder={I18n.t('plugin_Parameter_des')}
val={record?.desc}
useCheck={false}
checkAscii={true}
filterSpace={false}
max={300}
isRequired={isResponse ? false : true}
callback={(e: string) => {
updateNodeWithData({
record,
key: 'desc',
value: e,
});
if (showSecurityCheckFailedMsg) {
setShowSecurityCheckFailedMsg?.(false);
}
}}
/>
),
},
{
title: () => (
<FormTitle name={I18n.t('Create_newtool_s3_table_type')} required />
),
key: 'type',
width: 120,
render: (record: APIParameterRecord) => (
<ParamTypeColRender
record={record}
disabled={disabled}
data={data}
setData={setData}
checkFlag={checkFlag}
updateNodeWithData={updateNodeWithData}
addChildNode={addChildNode}
enableFileType={enableFileType}
/>
),
},
{
title: () => (
<FormTitle name={I18n.t('Create_newtool_s2_table_method')} required />
),
key: 'location',
width: 120,
render: (record: APIParameter) => {
if (record.location === undefined) {
return <></>;
}
const methodLabelMap = ArrayUtil.array2Map(
parameterLocationOptions,
'value',
'label',
);
return disabled ? (
methodLabelMap[record.location]
) : (
<UISelect
theme="light"
defaultValue={record.location}
onChange={e => {
updateNodeWithData({
record,
key: 'location',
value: e,
updateData: true,
inherit: true,
});
}}
style={{ width: '100%' }}
>
{parameterLocationOptions.map(item => (
<UISelect.Option key={record?.id + item.label} value={item.value}>
{item.label}
</UISelect.Option>
))}
</UISelect>
);
},
},
{
title: I18n.t('Create_newtool_s2_table_required'),
width: 80,
key: 'default',
render: (record: APIParameter) => (
<Checkbox
style={{ position: 'relative', left: 18 }}
disabled={disabled}
defaultChecked={record.is_required}
onChange={e => {
// 必填 + 没有默认值 = 可见
if (e.target.checked && !record.global_default) {
updateNodeWithData({
record,
key: 'global_disable',
value: false,
updateData: true,
inherit: true,
});
}
updateNodeWithData({
record,
key: 'is_required',
value: e.target.checked,
updateData: true,
inherit: true,
});
}}
></Checkbox>
),
},
{
title: I18n.t('plugin_api_list_table_action'),
key: 'addChild',
width: 107,
render: (record: APIParameter & { deep: number }) => (
<Space>
{record.type === ParameterType.Object && (
<Tooltip content={I18n.t('plugin_form_add_child_tooltip')}>
<UIIconButton
disabled={disabled}
style={{ marginLeft: '8px' }}
onClick={() => addChildNode({ record })}
icon={<IconAddChildOutlined />}
type="secondary"
/>
</Tooltip>
)}
{handleIsShowDelete(data, record[ROWKEY]) && (
<Tooltip content={I18n.t('Delete')}>
<UIIconButton
disabled={disabled}
style={{ marginLeft: '8px' }}
onClick={() => deleteChildNode(record)}
icon={<IconDeleteOutline />}
type="secondary"
/>
</Tooltip>
)}
</Space>
),
},
];
if (!isResponse) {
columns.splice(
-1,
0,
...[
{
title: () => (
<FormTitle
name={I18n.t(
'plugin_edit_tool_default_value_config_item_default_value',
)}
/>
),
key: 'global_default',
width: 120,
render: (record: APIParameter) => (
<DefaultValueInput record={record} data={data} setData={setData} />
),
},
{
title: (
<FormTitle
name={I18n.t('plugin_edit_tool_default_value_config_item_enable')}
toolTipText={I18n.t(
'plugin_edit_tool_default_value_config_item_enable_tip',
)}
/>
),
key: 'global_disable',
width: 78,
render: (record: APIParameter) => {
if (record.global_default === undefined) {
return <></>;
}
const switchNode = (
<Switch
style={{ position: 'relative', top: 3, left: 12 }}
defaultChecked={!record.global_disable}
disabled={record.is_required && !record.global_default}
onChange={e => {
updateNodeWithData({
record,
key: 'global_disable',
value: !e,
updateData: true,
inherit: true,
});
}}
/>
);
return record.is_required && !record.global_default ? (
<Tooltip
content={I18n.t(
'plugin_edit_tool_default_value_config_item_enable_disable_tip',
)}
>
{switchNode}
</Tooltip>
) : (
switchNode
);
},
},
],
);
}
//出参场景,移除 required增加 enabled 开关
if (isResponse) {
const targetIndex = columns.findIndex(c => c.key === 'default');
columns.splice(targetIndex, 1);
columns.splice(-1, 0, {
title: (
<FormTitle
name={I18n.t('plugin_edit_tool_default_value_config_item_enable')}
toolTipText={I18n.t('plugin_edit_tool_output_param_enable_tip')}
/>
),
key: 'global_disable',
width: 78,
render: (record: APIParameter) => {
if (record.global_default === undefined) {
return <></>;
}
const switchNode = (
<Switch
style={{ position: 'relative', top: 3, left: 12 }}
defaultChecked={!record.global_disable}
onChange={e => {
updateNodeWithData({
record,
key: 'global_disable',
value: !e,
updateData: true,
inherit: true,
});
}}
/>
);
return switchNode;
},
});
}
return flow(
// 将 columns 以函数参数形式传入,而非直接传给组合函数(`flow(...)(columns)`)是为了利于类型推导
() => columns,
// 只读状态不展示后四项操作列
newColumns => {
const len = isResponse ? DISABLED_RES_SLICE : DISABLED_REQ_SLICE;
return disabled ? newColumns.slice(0, len) : newColumns;
},
// response不需要location字段
newColumns =>
isResponse
? newColumns.filter(item => item.key !== 'location')
: newColumns,
)();
};

View File

@@ -0,0 +1,245 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function */
import {
type Dispatch,
type ReactNode,
type SetStateAction,
useEffect,
useState,
} from 'react';
import { cloneDeep } from 'lodash-es';
import classNames from 'classnames';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { I18n } from '@coze-arch/i18n';
import { UIButton, Toast, Table } from '@coze-arch/bot-semi';
import { IconAdd } from '@coze-arch/bot-icons';
import {
type APIParameter,
type UpdateAPIRequest,
type PluginAPIInfo,
type UpdateAPIResponse,
} from '@coze-arch/bot-api/plugin_develop';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import {
initParamsDefault,
defaultNode,
maxDeep,
scrollToBottom,
scrollToErrorElement,
sleep,
} from './utils';
import { ERROR_CODE, type RenderEnhancedComponentProps } from './types';
import { getColumns } from './params-components';
import { ROWKEY, childrenRecordName } from './config';
import s from './index.module.less';
const STARTNUM = 4;
const CHANGENUM = 13;
const SMALLGAP = 19;
const MAXZGAP = 40;
const TIMER = 100;
export interface UseRequestParamsProps {
pluginId: string;
apiId?: string;
requestParams: Array<APIParameter> | undefined;
step?: number;
disabled: boolean;
showSecurityCheckFailedMsg?: boolean;
setShowSecurityCheckFailedMsg?: Dispatch<SetStateAction<boolean>>;
editVersion?: number;
functionName?: string;
apiInfo?: PluginAPIInfo;
spaceID: string;
onSuccess?: (params: UpdateAPIResponse) => void;
renderEnhancedComponent?: RenderEnhancedComponentProps['renderParamsComponent'];
}
export interface UseRequestParamsReturnValue {
submitRequestParams: () => Promise<boolean>;
requestParamsNode: JSX.Element;
nlTool?: ReactNode;
}
export const useRequestParams = ({
apiInfo,
pluginId,
apiId,
requestParams,
disabled,
showSecurityCheckFailedMsg,
setShowSecurityCheckFailedMsg,
editVersion,
functionName,
spaceID,
onSuccess,
renderEnhancedComponent,
}: UseRequestParamsProps): UseRequestParamsReturnValue => {
const [data, setFormData] = useState<Array<APIParameter>>(
requestParams ? requestParams : [],
);
// @ts-expect-error -- linter-disable-autofix
const setData = (formData, checkDefault = true) => {
let fd = formData;
if (checkDefault) {
fd = initParamsDefault(formData, 'global_default');
}
setFormData(fd);
};
const [flag, setFlag] = useState<boolean>(false); // 为了更新视图
const [checkFlag, setCheckFlag] = useState<number>(0); // 全局校验用
const columns = getColumns({
data,
flag,
checkFlag,
setCheckFlag,
setFlag,
setData,
disabled,
// @ts-expect-error -- linter-disable-autofix
showSecurityCheckFailedMsg,
// @ts-expect-error -- linter-disable-autofix
setShowSecurityCheckFailedMsg,
enableFileType: true,
});
useEffect(() => {
if (
Array.isArray(requestParams) &&
requestParams.length === 0 &&
Array.isArray(data) &&
data.length === 0
) {
return;
}
setData(requestParams ? requestParams : []);
}, [disabled, requestParams]);
const submitRequestParams = async () => {
setCheckFlag(checkFlag + 1);
const sleepTime = 100;
await sleep(sleepTime);
if (!apiId || document.getElementsByClassName('errorClassTag').length > 0) {
scrollToErrorElement('.errorClassTag');
Toast.error({
content: I18n.t('tool_new_S2_feedback_failed'),
duration: 3,
theme: 'light',
showClose: false,
});
return false;
}
try {
const params: UpdateAPIRequest = {
plugin_id: pluginId,
api_id: apiId,
request_params: data,
edit_version: editVersion,
function_name: functionName,
};
const resData = await PluginDevelopApi.UpdateAPI(params, {
__disableErrorToast: true,
});
onSuccess?.(resData);
return true;
} catch (error) {
// @ts-expect-error -- linter-disable-autofix
const { code, msg } = error;
if (Number(code) === ERROR_CODE.SAFE_CHECK) {
setShowSecurityCheckFailedMsg?.(true);
} else {
Toast.error({
content: withSlardarIdButton(msg),
});
}
return false;
}
};
const addParams = () => {
setCheckFlag(0);
const cloneData = cloneDeep(data);
cloneData.push(defaultNode());
setData(cloneData);
setTimeout(() => {
scrollToBottom(document.getElementsByClassName('semi-table-body')[0]);
}, TIMER);
};
const maxNum = maxDeep(data);
return {
submitRequestParams,
requestParamsNode: (
<div>
<div
className={s['table-wrapper']}
style={{ minWidth: 1008, overflowY: 'auto' }}
>
<Table
// 最小宽度为了兼容多层级场景最大层级可支持超过50层
// 最小宽度 = 模块最小宽度 + (当前层级数 - 宽度变化起始层级) * (当前层级数 < 宽度变化起始层级 ? 小间隔数 : 大间隔数)
style={{
minWidth: `calc(1008px + ${
(maxNum - STARTNUM) * (maxNum < CHANGENUM ? SMALLGAP : MAXZGAP)
}px)`,
}} // 从第4层开始每多一层增加19px
pagination={false}
columns={columns}
dataSource={data}
rowKey={ROWKEY}
childrenRecordName={childrenRecordName}
expandAllRows={true}
className={classNames(
disabled ? s['request-params'] : s['request-params-edit'],
s['table-style-list'],
)}
empty={<div></div>}
/>
{!disabled && (
<div
style={
Array.isArray(data) && data.length === 0 ? { borderTop: 0 } : {}
}
className={s['add-params-btn-wrap']}
>
<UIButton
disabled={disabled}
icon={<IconAdd />}
style={{ marginTop: 12 }}
type="tertiary"
onClick={addParams}
>
{I18n.t('Create_newtool_s3_table_new')}
</UIButton>
</div>
)}
</div>
</div>
),
nlTool: renderEnhancedComponent?.({
disabled: !data?.length || disabled,
src: 'request',
originParams: data,
apiInfo,
onSetParams: p => setFormData(p),
spaceID,
pluginId,
}),
};
};

View File

@@ -0,0 +1,371 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function */
import {
type Dispatch,
type ReactNode,
type SetStateAction,
useEffect,
useState,
} from 'react';
import { cloneDeep } from 'lodash-es';
import classNames from 'classnames';
import { useMemoizedFn } from 'ahooks';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { logger } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { Button } from '@coze-arch/coze-design';
import { UIButton, Toast, UIModal, Table } from '@coze-arch/bot-semi';
import { IconAdd } from '@coze-arch/bot-icons';
import {
PluginType,
type APIParameter,
type UpdateAPIRequest,
type PluginAPIInfo,
type UpdateAPIResponse,
} from '@coze-arch/bot-api/plugin_develop';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import {
addDepthAndValue,
defaultNode,
sleep,
maxDeep,
scrollToErrorElement,
scrollToBottom,
initParamsDefault,
doRemoveDefaultFromResponseParams,
} from './utils';
import {
type RenderEnhancedComponentProps,
type CheckParamsProps,
ERROR_CODE,
STATUS,
} from './types';
import { getColumns } from './params-components';
import { DebugParams } from './debug-components/debug-params';
import { ROWKEY, childrenRecordName } from './config';
import s from './index.module.less';
export interface UseRequestParamsProps {
pluginId: string;
apiId: string;
requestParams: Array<APIParameter> | undefined;
responseParams: Array<APIParameter> | undefined;
step?: number;
disabled: boolean;
showSecurityCheckFailedMsg?: boolean;
setShowSecurityCheckFailedMsg?: Dispatch<SetStateAction<boolean>>;
editVersion?: number;
pluginType?: PluginType;
functionName?: string;
apiInfo?: PluginAPIInfo;
spaceID: string;
onSuccess?: (params: UpdateAPIResponse) => void;
renderEnhancedComponent?: RenderEnhancedComponentProps['renderParamsComponent'];
}
export interface UseRequestParamsReturnValue {
submitResponseParams: () => Promise<boolean>;
responseParamsNode: JSX.Element;
extra?: ReactNode;
}
const SLEEP_TIME = 100;
const TIMER = 100;
export const useResponseParams = ({
apiInfo,
pluginId,
requestParams,
responseParams,
apiId,
disabled,
showSecurityCheckFailedMsg,
setShowSecurityCheckFailedMsg,
editVersion,
pluginType,
functionName,
spaceID,
onSuccess,
renderEnhancedComponent,
}: UseRequestParamsProps): UseRequestParamsReturnValue => {
const [data, setFormData] = useState<Array<APIParameter>>(
responseParams || [],
);
const [flag, setFlag] = useState<boolean>(false); // 为了更新视图
const [checkFlag, setCheckFlag] = useState<number>(0); // 全局校验用
const [inputModal, setInputModal] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const setData = useMemoizedFn((formData, checkDefault = true) => {
let fd = formData;
if (checkDefault) {
fd = initParamsDefault(formData, 'global_default');
}
setFormData(fd);
});
useEffect(() => {
if (
Array.isArray(responseParams) &&
responseParams.length === 0 &&
Array.isArray(data) &&
data.length === 0
) {
return;
}
setData(responseParams || []);
}, [disabled, responseParams]);
const columns = getColumns({
data,
flag,
checkFlag,
setCheckFlag,
setFlag,
setData,
isResponse: true,
disabled,
// @ts-expect-error -- linter-disable-autofix
showSecurityCheckFailedMsg,
// @ts-expect-error -- linter-disable-autofix
setShowSecurityCheckFailedMsg,
enableFileType: true,
});
const submitResponseParams = async () => {
setCheckFlag(checkFlag + 1);
await sleep(SLEEP_TIME);
if (!apiId || document.getElementsByClassName('errorClassTag').length > 0) {
scrollToErrorElement('.errorClassTag');
Toast.error({
content: withSlardarIdButton(I18n.t('tool_new_S2_feedback_failed')),
duration: 3,
theme: 'light',
showClose: false,
});
return false;
}
if (!apiId) {
return false;
}
try {
const params: UpdateAPIRequest = {
plugin_id: pluginId,
api_id: apiId,
response_params: doRemoveDefaultFromResponseParams(data, false),
edit_version: editVersion,
function_name: functionName,
};
const resData = await PluginDevelopApi.UpdateAPI(params, {
__disableErrorToast: true,
});
onSuccess?.(resData);
return true;
} catch (error) {
// @ts-expect-error -- linter-disable-autofix
const { code, msg } = error;
if (Number(code) === ERROR_CODE.SAFE_CHECK) {
setShowSecurityCheckFailedMsg?.(true);
} else {
Toast.error({
content: withSlardarIdButton(msg),
});
}
return false;
}
};
const handleAction = ({
response_params,
status,
failReason,
}: CheckParamsProps) => {
if (status === STATUS.PASS && response_params) {
addDepthAndValue(response_params);
setData(response_params);
Toast.success({
content: I18n.t('plugin_s3_success'),
duration: 3,
theme: 'light',
showClose: false,
});
} else {
Toast.error({
content: withSlardarIdButton(failReason ?? I18n.t('plugin_s3_failed')),
duration: 3,
theme: 'light',
showClose: false,
});
}
setInputModal(false);
};
const handleActionNoParams = async () => {
try {
setLoading(true);
const resData = await PluginDevelopApi.DebugAPI({
plugin_id: pluginId,
api_id: apiId,
parameters: JSON.stringify({}),
operation: 2,
});
if (resData?.success && resData?.response_params) {
setData(resData.response_params);
Toast.success({
content: I18n.t('plugin_s3_success'),
duration: 3,
theme: 'light',
showClose: false,
});
} else {
Toast.error({
content: withSlardarIdButton(
resData?.reason ?? I18n.t('plugin_s3_failed'),
),
duration: 3,
theme: 'light',
showClose: false,
});
}
} catch (error) {
Toast.error({
content: withSlardarIdButton(I18n.t('plugin_s3_failed')),
duration: 3,
theme: 'light',
showClose: false,
});
logger.persist.error({
message: 'Custom Error: debug api failed',
// @ts-expect-error -- linter-disable-autofix
error,
});
}
setLoading(false);
};
const addParams = () => {
setCheckFlag(0);
const cloneData = cloneDeep(data);
cloneData.push(defaultNode());
setData(cloneData);
setFlag(!flag);
setTimeout(() => {
scrollToBottom(document.getElementsByClassName('semi-table-body')[0]);
}, TIMER);
};
const maxNum = maxDeep(data);
return {
submitResponseParams,
responseParamsNode: (
<div>
<div
className={s['table-wrapper']}
style={{ minWidth: 1008, overflowY: 'auto' }}
>
<Table
// eslint-disable-next-line @typescript-eslint/no-magic-numbers -- ui
style={{ minWidth: `calc(1008px + ${(maxNum - 6) * 20}px)` }} // 从第6层开始每多一层增加20px
pagination={false}
columns={columns}
dataSource={data}
rowKey={ROWKEY}
childrenRecordName={childrenRecordName}
expandAllRows={true}
className={classNames(
disabled ? s['request-params'] : s['request-params-edit'],
s['table-style-list'],
)}
empty={<div></div>}
/>
{!disabled && (
<div
className={s['add-params-btn-wrap']}
style={
Array.isArray(data) && data.length === 0 ? { borderTop: 0 } : {}
}
>
<UIButton
disabled={disabled}
icon={<IconAdd />}
style={{ marginTop: 12 }}
type="tertiary"
onClick={addParams}
>
{I18n.t('Create_newtool_s3_table_new')}
</UIButton>
</div>
)}
</div>
<UIModal
visible={inputModal}
title={I18n.t('plugin_s3_Parse')}
className={s['input-modal']}
keepDOM={false}
footer={<></>}
width={800}
maskClosable={false}
onCancel={() => setInputModal(false)}
>
<DebugParams
disabled={disabled}
pluginId={pluginId}
apiId={apiId}
requestParams={requestParams}
operation={2}
btnText={I18n.t('Create_newtool_s3_button_auto')}
callback={handleAction}
/>
</UIModal>
</div>
),
extra: (
<>
{renderEnhancedComponent?.({
disabled: !data?.length || disabled,
src: 'response',
originParams: data,
apiInfo,
onSetParams: p => setData(p),
spaceID,
pluginId,
})}
<Button
disabled={disabled || pluginType === PluginType.LOCAL}
className="!mr-2"
color="primary"
loading={loading}
onClick={e => {
e.stopPropagation();
if (Array.isArray(requestParams) && requestParams.length > 0) {
setInputModal(true);
} else {
handleActionNoParams();
}
}}
>
{loading
? I18n.t('plugin_s3_Parsing')
: I18n.t('Create_newtool_s3_button_auto')}
</Button>
</>
),
};
};

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.
*/
/* eslint-disable @coze-arch/no-batch-import-or-export */
export * from './modal';
export * from './params';

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 { type APIParameter } from '@coze-arch/bot-api/plugin_develop';
export enum STATUS {
PASS = 'PASS',
FAIL = 'FAIL',
WAIT = 'WAIT',
}
export interface CheckParamsProps {
status?: STATUS;
request?: string;
response?: string;
failReason?: string;
response_params?: Array<APIParameter>;
rawResp?: string;
}
export interface StepUpdateApiRes {
code: string | number;
msg: string;
}
export const ERROR_CODE = {
SAFE_CHECK: 720092020,
};

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ReactNode } from 'react';
import {
type APIParameter,
type AssistParameterType,
type ParameterType,
type PluginAPIInfo,
} from '@coze-arch/bot-api/plugin_develop';
import { type ParameterTypeExtend, type PluginParameterType } from '../config';
export interface APIParameterRecord extends APIParameter {
deep?: number;
value?: string;
}
export interface UpdateNodeWithDataFn {
(params: {
record: APIParameter;
key: string | Array<string>;
value: unknown;
updateData?: boolean;
checkDefault?: boolean;
inherit?: boolean;
}): void;
}
export interface AddChildNodeFn {
(params: {
record: APIParameterRecord;
isArray?: boolean;
isObj?: boolean;
type?: ParameterType;
recordType?: ParameterType;
}): void;
}
export interface InputItemProps {
val?: string;
max?: number;
check?: number;
width?: number | string;
useCheck?: boolean;
checkAscii?: boolean;
isRequired?: boolean;
placeholder?: string;
filterSpace?: boolean;
callback?: (val: string) => void;
selectCallback?: (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
val: string | number | any[] | Record<string, any> | undefined,
) => void;
targetKey?: string;
data?: Array<APIParameter>;
checkSame?: boolean;
useBlockWrap?: boolean;
disabled?: boolean;
record?: APIParameterRecord;
dynamicWidth?: boolean;
deep?: number;
typeOptions?: Array<Record<string, string | number>>;
}
export type CascaderValueType = [PluginParameterType, ParameterTypeExtend?];
export type CascaderOnChangValueType = [
PluginParameterType,
AssistParameterType?,
];
export interface RenderEnhancedComponentProps {
renderDescComponent: (props: {
onSetDescription: (desc: string) => void;
originDesc: string | undefined;
className: string;
disabled?: boolean;
plugin_id: string;
space_id: string;
}) => ReactNode;
renderParamsComponent: (props: {
size?: 'small' | 'default';
src: 'request' | 'response';
apiInfo: PluginAPIInfo | undefined;
originParams: APIParameter[];
onSetParams: (params: APIParameter[]) => void;
disabled?: boolean;
spaceID: string;
pluginId: string;
}) => ReactNode;
}

View File

@@ -0,0 +1,662 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 complexity */
/* eslint-disable max-lines */
/* eslint-disable @typescript-eslint/no-explicit-any -- 一些历史any 改不动 */
import { nanoid } from 'nanoid';
import { cloneDeep, has, isEmpty, isNumber, isObject } from 'lodash-es';
import {
type APIParameter,
ParameterLocation,
ParameterType,
DefaultParamSource,
} from '@coze-arch/bot-api/plugin_develop';
import { ARRAYTAG, ROWKEY, childrenRecordName } from './config';
// 遍历树返回目标id路径
export const findPathById = ({
data,
callback,
childrenName = childrenRecordName,
path = [],
}: {
data: any;
callback: (item: APIParameter, path: Array<number>) => void;
childrenName?: string;
path?: Array<number>;
}) => {
for (let i = 0; i < data.length; i++) {
const clonePath = JSON.parse(JSON.stringify(path));
clonePath.push(i);
callback(data[i], clonePath);
if (data[i][childrenName] && data[i][childrenName].length > 0) {
findPathById({
data: data[i][childrenName],
callback,
childrenName,
path: clonePath,
});
}
}
};
// 给每层对象增加层级深度标识
export const addDepthAndValue = (
tree: any,
valKey: 'global_default' | 'local_default' = 'global_default',
depth = 1,
) => {
if (!Array.isArray(tree)) {
return;
}
// 遍历树中的每个节点
for (const node of tree) {
// 为当前节点添加深度标识符
node.deep = depth;
if (node[valKey]) {
node.value = node[valKey];
}
// 如果当前节点有子节点,则递归地为子节点添加深度标识符
if (node[childrenRecordName]) {
addDepthAndValue(node[childrenRecordName], valKey, depth + 1);
}
}
};
// 将深度信息push到一个数组里最后取最大值
export const handleDeepArr = (tree: any, deepArr: Array<number> = []) => {
if (!Array.isArray(tree)) {
return;
}
// 遍历树中的每个节点
for (const node of tree) {
// 为当前节点添加深度标识符
if (isNumber(node.deep)) {
deepArr.push(node.deep);
}
if (node[childrenRecordName]) {
handleDeepArr(node[childrenRecordName], deepArr);
}
}
};
// 返回最大深度
export const maxDeep = (tree: any) => {
if (!Array.isArray(tree) || tree.length === 0) {
return 0;
}
const arr: Array<number> = [];
handleDeepArr(tree, arr);
return Math.max.apply(null, arr);
};
interface DefaultNode {
isArray?: boolean;
iscChildren?: boolean;
deep?: number;
}
// 默认子节点
export const defaultNode = ({
isArray = false,
iscChildren = false,
deep = 1,
}: DefaultNode = {}) => ({
[ROWKEY]: nanoid(),
name: isArray ? ARRAYTAG : '',
desc: '',
type: ParameterType.String,
location: iscChildren ? undefined : ParameterLocation.Query,
is_required: true,
sub_parameters: [],
deep,
});
// 删除当前节点
export const deleteNode = (data: any, targetKey: string) => {
for (let i = 0; i < data.length; i++) {
if (data[i][ROWKEY] === targetKey) {
data.splice(i, 1);
return true;
} else if (
data[i][childrenRecordName] &&
data[i][childrenRecordName].length > 0
) {
if (deleteNode(data[i][childrenRecordName], targetKey)) {
return true;
}
}
}
return false;
};
// 删除全部子节点
export const deleteAllChildNode = (data: any, targetKey: string) => {
for (const item of data) {
if (item[ROWKEY] === targetKey) {
item[childrenRecordName] = [];
return true;
} else if (
Array.isArray(item[childrenRecordName]) &&
item[childrenRecordName].length > 0
) {
if (deleteAllChildNode(item[childrenRecordName], targetKey)) {
return true;
}
}
}
return false;
};
interface UpdateNodeById {
data: APIParameter[];
targetKey: string;
field: string;
value: any;
/** 数组的子节点是否需要继承父节点的字段值,当前只有可见性开关需要继承 */
inherit?: boolean;
}
const updateNodeByVal = (data: any, field: any, val: any) => {
for (const item of data) {
item[field] = val;
if (Array.isArray(item[childrenRecordName])) {
updateNodeByVal(item[childrenRecordName], field, val);
}
}
};
// 更新节点信息
export const updateNodeById = ({
data,
targetKey,
field,
value,
inherit = false,
}: UpdateNodeById) => {
for (const item of data) {
if (item[ROWKEY] === targetKey) {
// @ts-expect-error -- linter-disable-autofix
item[field] = value;
if (
inherit &&
Array.isArray(item[childrenRecordName]) &&
item[childrenRecordName].length > 0
) {
updateNodeByVal(item[childrenRecordName], field, value);
}
return;
} else if (
Array.isArray(item[childrenRecordName]) &&
item[childrenRecordName].length > 0
) {
updateNodeById({
data: item[childrenRecordName],
targetKey,
field,
value,
});
}
}
};
// 根据路径找对应模版值
export const findTemplateNodeByPath = (
dsl: any,
path: Array<string | number>,
) => {
let node = cloneDeep(dsl);
const newPath = [...path]; //创建新的路径,避免修改原路径
for (let i = 0; i < path.length; i++) {
// 如果存在节点,说明是源数据节点上增加子节点
if (node[path[i]]) {
node = node[path[i]];
} else {
// 如果不存在,说明是新增的节点增加子节点,这时需要将路径指向原始节点(第一个节点)
node = node[0];
newPath[i] = 0;
}
}
return newPath;
};
// 树转换成对象
export const transformTreeToObj = (tree: any, checkType = true): any =>
// 树的每一层级表示一个对象的属性集
tree.reduce((acc: any, item: any) => {
let arrTemp = [];
switch (item.type) {
case ParameterType.String:
if (item.value) {
acc[item.name] = item.value;
}
if (!checkType) {
acc[item.name] = item.value;
}
break;
case ParameterType.Integer:
case ParameterType.Number:
if (item.value) {
acc[item.name] = Number(item.value);
}
if (!checkType) {
acc[item.name] = item.value;
}
break;
case ParameterType.Bool:
if (item.value) {
acc[item.name] = item.value === 'true';
}
if (!checkType) {
acc[item.name] = item.value;
}
break;
case ParameterType.Object:
if (item.sub_parameters) {
const obj = transformTreeToObj(item.sub_parameters, checkType);
if (!isEmpty(obj)) {
acc[item.name] = obj;
}
}
break;
case ParameterType.Array:
/**
* 如果是数组需要过滤掉空的项且数组的子项非object和array
* 这里用temp接收过滤后的子项避免直接修改原数组因为原数组和页面数据绑定不能直接删除空项
*/
arrTemp = item.sub_parameters;
if (
[
ParameterType.Bool,
ParameterType.Integer,
ParameterType.Number,
ParameterType.String,
].includes(item.sub_parameters[0].type)
) {
arrTemp = item.sub_parameters.filter((subItem: any) => subItem.value);
}
if (isEmpty(arrTemp)) {
break;
}
acc[item.name] = arrTemp.map((subItem: any) => {
// boolean类型匹配字符串true/false
if ([ParameterType.Bool].includes(subItem.type)) {
return checkType ? subItem.value === 'true' : subItem.value;
}
// 数字类型转为number
if (
[ParameterType.Integer, ParameterType.Number].includes(subItem.type)
) {
return checkType ? Number(subItem.value) : subItem.value;
}
// 字符串类型直接返回(进到这里的已经是过滤完空值的数组)
if ([ParameterType.String].includes(subItem.type)) {
return subItem.value;
}
// 如果是对象,递归遍历
if (subItem.type === ParameterType.Object) {
return transformTreeToObj(subItem.sub_parameters, checkType);
}
});
break;
default:
break;
}
return acc;
}, {});
// 克隆节点修改key及清空value
export const cloneWithRandomKey = (obj: any) => {
// 创建新对象储存值
const clone: any = {};
// 遍历原对象的所有属性
for (const prop in obj) {
// 如果原对象的这个属性是一个对象,递归调用 cloneWithRandomKey 函数
if (obj[prop]?.constructor === Object) {
clone[prop] = cloneWithRandomKey(obj[prop]);
} else {
// 否则,直接复制这个属性
clone[prop] = obj[prop];
}
}
// 如果这个对象有 sub_parameters 属性,需要遍历它
if ('sub_parameters' in clone) {
clone.sub_parameters = clone.sub_parameters?.map(cloneWithRandomKey);
}
// 生成一个新的随机 key
if (clone[ROWKEY]) {
clone[ROWKEY] = nanoid();
}
if (clone.value) {
clone.value = null;
}
// 返回克隆的对象
return clone;
};
// 判断参数是否显示删除按钮 先判断是否是根节点,根节点允许删除
export const handleIsShowDelete = (
data: any,
targetKey: string | undefined,
) => {
const rootIds = data.map((d: any) => d[ROWKEY]);
if (rootIds.includes(targetKey)) {
return true;
}
return isShowDelete(data, targetKey);
};
// 检查是否存在相同名称
export const checkSameName = (
data: Array<APIParameter>,
targetKey: string,
val: string,
): boolean | undefined => {
for (const item of data) {
if (item[ROWKEY] === targetKey) {
const items = data.filter(
(dataItem: APIParameter) => dataItem.name === val,
);
return items.length > 1;
} else if (
Array.isArray(item[childrenRecordName]) &&
item[childrenRecordName].length > 0
) {
if (checkSameName(item[childrenRecordName], targetKey, val)) {
return true;
}
}
}
};
// 检查是否有array类型用来判断response是否需要操作列
export const checkHasArray = (data: unknown) => {
if (!Array.isArray(data)) {
return false;
}
for (const item of data) {
if (item.type === ParameterType.Array) {
return true;
} else if (
Array.isArray(item[childrenRecordName]) &&
item[childrenRecordName].length > 0
) {
// 调整 循环退出时机
if (checkHasArray(item[childrenRecordName])) {
return true;
}
}
}
return false;
};
// 判断参数是否显示删除按钮object类型最后一个不允许删除
export const isShowDelete = (data: any, targetKey: string | undefined) => {
for (const item of data) {
if (item[ROWKEY] === targetKey) {
return data.length > 1;
} else if (
Array.isArray(item[childrenRecordName]) &&
item[childrenRecordName].length > 0
) {
if (isShowDelete(item[childrenRecordName], targetKey)) {
return true;
}
}
}
};
export const sleep = (time: number) =>
new Promise(resolve => {
setTimeout(() => {
resolve(0);
}, time);
});
// 该方法兼容chrome、Arch、Safari浏览器及iPad增加兼容firefox
export const scrollToErrorElement = (className: string) => {
const errorElement = document.querySelector(className);
if (errorElement) {
if (typeof (errorElement as any).scrollIntoViewIfNeeded === 'function') {
(errorElement as any).scrollIntoViewIfNeeded();
} else {
// 兼容性处理,如 Firefox
errorElement.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
}
};
export const scrollToBottom = (ele: Element) => {
const scrollEle = ele;
scrollEle.scrollTo({
left: 0,
top: scrollEle.scrollHeight,
behavior: 'smooth',
});
};
export const initParamsDefault = (
data: Array<APIParameter>,
keyDefault: 'global_default' | 'local_default',
) => {
const result = cloneDeep(data);
const init = (obj: APIParameter) => {
if (keyDefault === 'local_default' && !has(obj, 'local_default')) {
obj[keyDefault] = obj.global_default;
}
if (!obj[keyDefault]) {
obj[keyDefault] = '';
}
// bot非引用+必填+local默认值为空+不可见,是异常场景,需手动拨正
const isUnusual =
obj.default_param_source === DefaultParamSource.Input &&
keyDefault === 'local_default' &&
obj.is_required &&
!obj.local_default &&
obj.local_disable;
if (isUnusual) {
obj.local_disable = false;
}
};
const addDefault = (res: Array<APIParameter>, isArray = false) => {
for (let i = 0, len = res.length; i < len; i++) {
const obj = res[i];
if (isArray) {
obj[keyDefault] = undefined;
if (obj.sub_parameters && obj.sub_parameters.length > 0) {
addDefault(obj.sub_parameters, true);
}
} else if (obj.type === ParameterType.Array) {
init(obj);
if (obj.sub_parameters && obj.sub_parameters.length > 0) {
addDefault(obj.sub_parameters, true);
}
} else {
if (obj.type === ParameterType.Object) {
obj[keyDefault] = undefined;
} else {
init(obj);
}
if (obj.sub_parameters && obj.sub_parameters.length > 0) {
addDefault(obj.sub_parameters);
}
}
}
};
addDefault(result);
return result;
};
// @ts-expect-error -- linter-disable-autofix
export const transformArrayToTree = (array, template: Array<APIParameter>) => {
const arrObj = array;
const tree: Array<APIParameter> = [];
if (Array.isArray(arrObj)) {
arrObj.forEach(item => {
const subTree = createSubTree(item, template[0]);
tree.push(subTree);
});
}
return tree;
};
const createSubTree = (arrItem: any, tem: any) => {
let subTree: APIParameter & { value?: unknown } = {};
// 数组
if (Array.isArray(arrItem)) {
subTree = {
...tem,
id: nanoid(),
sub_parameters: [],
};
arrItem.forEach(item => {
const arrItemSubTree = createSubTree(item, tem.sub_parameters[0]);
subTree.sub_parameters?.push(arrItemSubTree);
});
} else if (isObject(arrItem)) {
subTree = {
...tem,
id: nanoid(),
sub_parameters: [],
};
let childTree: APIParameter & { value?: unknown } = {};
Object.keys(arrItem).map(key => {
if (Object.prototype.hasOwnProperty.call(arrItem, key)) {
// @ts-expect-error -- linter-disable-autofix
const value = arrItem[key];
if (Array.isArray(value) || typeof value === 'object') {
const nestedSubTree = createSubTree(
value,
// @ts-expect-error -- linter-disable-autofix
tem.sub_parameters.find(item => item.name === key),
);
subTree.sub_parameters?.push(nestedSubTree);
} else {
childTree = {
// @ts-expect-error -- linter-disable-autofix
...tem.sub_parameters.find(item => item.name === key),
id: nanoid(),
sub_parameters: [],
};
childTree.value = String(value);
subTree.sub_parameters?.push(childTree);
}
}
});
} else {
subTree = {
...tem,
id: nanoid(),
sub_parameters: [],
};
subTree.value = String(arrItem);
}
return subTree;
};
export const transformParamsToTree = (params: Array<APIParameter>) => {
const result = cloneDeep(params);
for (let i = 0, len = result.length; i < len; i++) {
if (result[i].type === ParameterType.Array) {
const arr = JSON.parse(result[i].global_default || '[]');
if (arr.length > 0) {
const tree = transformArrayToTree(arr, result[i].sub_parameters || []);
result[i].sub_parameters = tree;
}
} else {
// 对象嵌数组有问题 被覆盖了 需要重置
result[i].sub_parameters = transformParamsToTree(
result[i].sub_parameters || [],
);
}
}
return result;
};
// data 额外加工 / 如果本身没有 global_default === undefined 就设置 global_disable 也是 undefined最后将所有的 global_default 设置成 undefined
export const doRemoveDefaultFromResponseParams = (
data: APIParameter[],
hasRequired = false,
) => {
if (!data.length) {
return [];
}
const returnData = cloneDeep(data);
for (let i = 0, len = returnData.length; i < len; i++) {
const current = returnData[i];
if (current.global_default === undefined) {
current.global_disable = undefined;
}
current.global_default = undefined;
if (!hasRequired) {
current.is_required = undefined;
}
current.sub_parameters = doRemoveDefaultFromResponseParams(
current.sub_parameters ?? [],
hasRequired,
);
}
return returnData;
};
// @ts-expect-error -- linter-disable-autofix
export const doValidParams = (
params: APIParameter[],
targetKey: keyof APIParameter,
) => {
if (!params?.length || !targetKey) {
return !!0;
}
for (let i = 0, j = params.length; i < j; i++) {
const target = params[i];
if (!target[targetKey]) {
return !!0;
}
// @ts-expect-error -- linter-disable-autofix
if (target.sub_parameters.length > 0) {
// @ts-expect-error -- linter-disable-autofix
const sub = doValidParams(target.sub_parameters, targetKey);
if (sub === !!0) {
return sub;
}
}
}
return !0;
};