feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
.error-container {
|
||||
.error-link {
|
||||
.error-link-underline {
|
||||
text-decoration: underline;
|
||||
color: var(--semi-color-danger);
|
||||
font-size: 14px;
|
||||
margin-left: 2px;
|
||||
max-width: 200px;
|
||||
|
||||
a {
|
||||
color: var(--semi-color-danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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 BindConnectorResponse,
|
||||
type GetBindConnectorConfigResponse,
|
||||
type SaveBindConnectorConfigResponse,
|
||||
} from '@coze-arch/idl/developer_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Form, Typography } from '@coze-arch/bot-semi';
|
||||
import { type ApiError } from '@coze-arch/bot-http';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
type ErrorResponse =
|
||||
| GetBindConnectorConfigResponse
|
||||
| SaveBindConnectorConfigResponse
|
||||
| BindConnectorResponse;
|
||||
|
||||
function isBindConnectorResponse(
|
||||
res: ErrorResponse,
|
||||
): res is BindConnectorResponse {
|
||||
return ['bind_bot_id', 'bind_bot_name', 'bind_space_id'].every(
|
||||
key => key in res,
|
||||
);
|
||||
}
|
||||
|
||||
export interface ConnectorErrorProps {
|
||||
errorMessage: ApiError;
|
||||
}
|
||||
|
||||
export const ConnectorError = ({ errorMessage }: ConnectorErrorProps) => {
|
||||
const res = (errorMessage?.raw ?? {}) as ErrorResponse;
|
||||
|
||||
return (
|
||||
<Form.ErrorMessage
|
||||
error={
|
||||
isBindConnectorResponse(res) ? (
|
||||
<div className={styles['error-link']}>
|
||||
{I18n.t('bot_publish_bind_error', {
|
||||
bot_name: (
|
||||
<Typography.Text
|
||||
className={styles['error-link-underline']}
|
||||
link={{
|
||||
href: `/space/${res.bind_space_id}/${res.bind_agent_type === 1 ? 'project-ide' : 'bot'}/${res.bind_bot_id}`,
|
||||
}}
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
opts: {
|
||||
content: res.bind_bot_name,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{res.bind_bot_name}
|
||||
</Typography.Text>
|
||||
),
|
||||
key_name: 'token',
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
errorMessage?.msg
|
||||
)
|
||||
}
|
||||
className={styles['error-container']}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
.disable-field {
|
||||
padding: 12px 0 24px;
|
||||
|
||||
.title {
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
&& {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-input-suffix {
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-button {
|
||||
&&& {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* 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 { logger } from '@coze-arch/logger';
|
||||
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
|
||||
import { IconCozTrashCan, IconCozPlus } from '@coze-arch/coze-design/icons';
|
||||
import { TagGroup, ArrayField, Button } from '@coze-arch/coze-design';
|
||||
import { typeSafeJSONParse } from '@coze-arch/bot-utils';
|
||||
import { type RuleItem } from '@coze-arch/bot-semi/Form';
|
||||
import { UIFormInput, Form, Typography } from '@coze-arch/bot-semi';
|
||||
import {
|
||||
type Options,
|
||||
type FormSchemaItem,
|
||||
} from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
import { type TFormData } from '../types';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
function formatMultiSelectValue(rawValue: string, enums?: Options[]) {
|
||||
const arrayValue = typeSafeJSONParse(rawValue) as string[] | undefined;
|
||||
if (!arrayValue) {
|
||||
return [];
|
||||
}
|
||||
return arrayValue.map(value => ({
|
||||
children: enums?.find(option => option.value === value)?.label ?? value,
|
||||
}));
|
||||
}
|
||||
|
||||
export interface ConnectorFieldProps {
|
||||
formItemSchema: FormSchemaItem;
|
||||
isReadOnly: boolean;
|
||||
initValue?: TFormData;
|
||||
}
|
||||
|
||||
export const ConnectorField = (props: ConnectorFieldProps) => {
|
||||
const { formItemSchema, isReadOnly, initValue } = props;
|
||||
const rawInitValue = initValue?.[formItemSchema.name];
|
||||
|
||||
if (isReadOnly) {
|
||||
return (
|
||||
<div className={styles['disable-field']}>
|
||||
<div className={styles.title}>{formItemSchema.title}</div>
|
||||
{formItemSchema.type === 'array' ? (
|
||||
<TagGroup
|
||||
tagList={formatMultiSelectValue(rawInitValue, formItemSchema.enums)}
|
||||
/>
|
||||
) : (
|
||||
<Typography.Text
|
||||
style={{ width: '100%' }}
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
opts: {
|
||||
content: rawInitValue,
|
||||
style: { wordBreak: 'break-word' },
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{rawInitValue}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function createRules(fieldSchema: FormSchemaItem): RuleItem[] {
|
||||
// 确保 formItemSchema.rules 是一个数组
|
||||
const itemRules = fieldSchema.rules ?? [];
|
||||
|
||||
const rules = itemRules.map(rule => {
|
||||
const ruleMessage = rule.message
|
||||
? I18n.t(rule.message as I18nKeysNoOptionsType, {
|
||||
field: fieldSchema.name,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return { ...rule, ...(ruleMessage && { message: ruleMessage }) };
|
||||
});
|
||||
|
||||
// 添加 'required' 规则
|
||||
rules.push({
|
||||
required: fieldSchema.required,
|
||||
message: I18n.t('bot_publish_field_placeholder', {
|
||||
field: fieldSchema.title ?? '',
|
||||
}),
|
||||
});
|
||||
|
||||
return rules as RuleItem[];
|
||||
}
|
||||
|
||||
if (!formItemSchema.name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (formItemSchema.component) {
|
||||
case 'Input':
|
||||
if (formItemSchema.type === 'array') {
|
||||
let values: string[] = [];
|
||||
try {
|
||||
values = JSON.parse(rawInitValue);
|
||||
} catch (e) {
|
||||
logger.error({ error: e as Error });
|
||||
values = [];
|
||||
}
|
||||
// 添加一个默认空值
|
||||
if (!values.length) {
|
||||
values.push('');
|
||||
}
|
||||
|
||||
return (
|
||||
<ArrayField field={formItemSchema.name} initValue={values}>
|
||||
{({ arrayFields, add }) => (
|
||||
<>
|
||||
{arrayFields.map(({ key, field, remove }, i) => (
|
||||
<UIFormInput
|
||||
key={key}
|
||||
placeholder={I18n.t('bot_publish_field_placeholder', {
|
||||
field: formItemSchema.title ?? '',
|
||||
})}
|
||||
field={field}
|
||||
label={formItemSchema.title}
|
||||
noLabel={i > 0}
|
||||
required={formItemSchema.required}
|
||||
rules={createRules(formItemSchema)}
|
||||
fieldClassName={styles.input}
|
||||
suffix={
|
||||
arrayFields.length <= 1 ? null : (
|
||||
<IconCozTrashCan onClick={remove} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
className={styles['link-button']}
|
||||
color="highlight"
|
||||
size="small"
|
||||
icon={<IconCozPlus />}
|
||||
onClick={add}
|
||||
>
|
||||
{I18n.t('binding_add_card')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</ArrayField>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<UIFormInput
|
||||
key={formItemSchema.name}
|
||||
placeholder={I18n.t('bot_publish_field_placeholder', {
|
||||
field: formItemSchema.title ?? '',
|
||||
})}
|
||||
field={formItemSchema.name}
|
||||
label={formItemSchema.title}
|
||||
required={formItemSchema.required}
|
||||
showClear
|
||||
rules={createRules(formItemSchema)}
|
||||
initValue={rawInitValue}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'Select': {
|
||||
const isMultiple = formItemSchema.type === 'array';
|
||||
const selectInitValue = isMultiple
|
||||
? (typeSafeJSONParse(rawInitValue) as string[] | undefined)
|
||||
: rawInitValue;
|
||||
return (
|
||||
<Form.Select
|
||||
key={formItemSchema.name}
|
||||
placeholder={`Enter ${formItemSchema.title}`}
|
||||
field={formItemSchema.name}
|
||||
label={formItemSchema.title}
|
||||
optionList={formItemSchema.enums}
|
||||
multiple={isMultiple}
|
||||
rules={createRules(formItemSchema)}
|
||||
initValue={selectInitValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
.step-order {
|
||||
margin-top: 2px;
|
||||
display: flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
background: var(--light-color-brand-brand-5, #4d53e8);
|
||||
color: var(--light-color-white-white, #fff);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.step-title {
|
||||
color: #000;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.markdown {
|
||||
color: var(--light-usage-text-color-text-0, #1D1C23);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable complexity -- ignore */
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import {
|
||||
forwardRef,
|
||||
type Ref,
|
||||
useRef,
|
||||
useImperativeHandle,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
|
||||
import { useUpdate } from 'ahooks';
|
||||
import type { FormApi } from '@coze-arch/bot-semi/Form';
|
||||
import { Space, Form } from '@coze-arch/bot-semi';
|
||||
import { type ApiError } from '@coze-arch/bot-http';
|
||||
import { type SchemaAreaInfo } from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
import { type FormActions, type TFormData } from '../types';
|
||||
import { ConnectorField } from '../connector-field';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface ConnectorFormProps {
|
||||
schemaAreaInfo?: SchemaAreaInfo;
|
||||
initValue?: TFormData;
|
||||
getFormDisable: (disable: boolean) => void;
|
||||
isReadOnly: boolean;
|
||||
setErrorMessage: (error?: ApiError) => void;
|
||||
}
|
||||
|
||||
const DEFAULT_FORM_STEP = 2;
|
||||
|
||||
// 多选 Select 在 Form 中的 value 是 string[],但提交到后端需要转换成 JSON string
|
||||
type FormValues = Record<string, string | string[]>;
|
||||
|
||||
export const ConnectorForm = forwardRef(
|
||||
(props: ConnectorFormProps, ref: Ref<FormActions>) => {
|
||||
const {
|
||||
schemaAreaInfo,
|
||||
initValue,
|
||||
getFormDisable,
|
||||
isReadOnly,
|
||||
setErrorMessage,
|
||||
} = props;
|
||||
|
||||
const formApiRef = useRef<FormApi<FormValues>>();
|
||||
const update = useUpdate();
|
||||
|
||||
useImperativeHandle<FormActions, FormActions>(ref, () => ({
|
||||
submit: async () => {
|
||||
const values = await formApiRef.current?.validate();
|
||||
return Object.fromEntries(
|
||||
Object.entries(values ?? {}).map(([key, value]) => [
|
||||
key,
|
||||
Array.isArray(value) ? JSON.stringify(value) : value,
|
||||
]),
|
||||
);
|
||||
},
|
||||
reset: () => formApiRef.current?.reset(),
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
// 解决formApiRef.current取值不实时问题
|
||||
update();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- ignore
|
||||
}, [schemaAreaInfo]);
|
||||
|
||||
const formDisabled =
|
||||
schemaAreaInfo?.schema_list
|
||||
?.filter(item => item.required)
|
||||
.some(field => {
|
||||
const value = formApiRef.current?.getValue(field.name);
|
||||
if (Array.isArray(value)) {
|
||||
return !value.length || (value.length === 1 && !value[0]);
|
||||
}
|
||||
return !value;
|
||||
}) || !schemaAreaInfo?.schema_list?.length;
|
||||
|
||||
useEffect(() => {
|
||||
getFormDisable(formDisabled);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- ignore
|
||||
}, [formDisabled]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{schemaAreaInfo?.title_text ? (
|
||||
<Space spacing={12} align="start">
|
||||
<span className={styles['step-order']}>
|
||||
{schemaAreaInfo.step_order || DEFAULT_FORM_STEP}
|
||||
</span>
|
||||
|
||||
<div className={styles['step-content']}>
|
||||
<div className={styles['step-title']}>
|
||||
{schemaAreaInfo.title_text}
|
||||
</div>
|
||||
</div>
|
||||
</Space>
|
||||
) : null}
|
||||
{schemaAreaInfo?.description ? (
|
||||
<ReactMarkdown skipHtml={true} className={styles.markdown}>
|
||||
{schemaAreaInfo?.description}
|
||||
</ReactMarkdown>
|
||||
) : null}
|
||||
|
||||
{schemaAreaInfo?.schema_list?.length ? (
|
||||
<Form<FormValues>
|
||||
initValues={initValue}
|
||||
className={styles['config-form']}
|
||||
onValueChange={() => {
|
||||
update();
|
||||
setErrorMessage(undefined);
|
||||
}}
|
||||
getFormApi={formApi => (formApiRef.current = formApi)}
|
||||
autoScrollToError
|
||||
allowEmpty
|
||||
>
|
||||
{schemaAreaInfo?.schema_list?.map(item => (
|
||||
<ConnectorField
|
||||
initValue={initValue}
|
||||
formItemSchema={item}
|
||||
isReadOnly={isReadOnly}
|
||||
key={item.name}
|
||||
/>
|
||||
))}
|
||||
</Form>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
.start-text {
|
||||
color: var(--light-usage-text-color-text-0, #1D1C23);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.config-link {
|
||||
color: var(--light-color-brand-brand-5, #4D53E8);
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
|
||||
}
|
||||
|
||||
.markdown {
|
||||
color: var(--light-usage-text-color-text-0, #1D1C23);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.guide {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 ReactMarkdown from 'react-markdown';
|
||||
|
||||
import { Typography } from '@coze-arch/bot-semi';
|
||||
import { type QuerySchemaConfig } from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const ConnectorGuide = ({
|
||||
connectorConfigInfo = {},
|
||||
}: {
|
||||
connectorConfigInfo?: QuerySchemaConfig;
|
||||
}) => (
|
||||
<div className={styles.guide}>
|
||||
{connectorConfigInfo?.start_text ? (
|
||||
<ReactMarkdown
|
||||
skipHtml={true}
|
||||
linkTarget="_blank"
|
||||
className={styles.markdown}
|
||||
>
|
||||
{connectorConfigInfo?.start_text}
|
||||
</ReactMarkdown>
|
||||
) : null}
|
||||
{connectorConfigInfo?.guide_link_url &&
|
||||
connectorConfigInfo?.guide_link_text ? (
|
||||
<div>
|
||||
<Typography.Text
|
||||
link={{
|
||||
href: connectorConfigInfo?.guide_link_url,
|
||||
}}
|
||||
className={styles['config-link']}
|
||||
>
|
||||
{connectorConfigInfo?.guide_link_text}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,49 @@
|
||||
.step-order {
|
||||
margin-top: 2px;
|
||||
display: flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
background: var(--light-color-brand-brand-5, #4d53e8);
|
||||
color: var(--light-color-white-white, #fff);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.step-title {
|
||||
color: #000;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.markdown {
|
||||
color: var(--light-usage-text-color-text-0, #1D1C23);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.link-area .link-list {
|
||||
margin-top: 16px;
|
||||
|
||||
.title {
|
||||
color: var(--light-usage-text-color-text-0, #1D1C23);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
|
||||
}
|
||||
|
||||
.link {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.semi-form-field-error-message {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
|
||||
import { Space, Typography } from '@coze-arch/bot-semi';
|
||||
import { type CopyLinkAreaInfo } from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
import { type TFormData } from '../types';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const ConnectorLink = ({
|
||||
copyLinkAreaInfo = {},
|
||||
agentType = 'bot',
|
||||
botId = '',
|
||||
initValue = {},
|
||||
}: {
|
||||
copyLinkAreaInfo?: CopyLinkAreaInfo;
|
||||
agentType?: 'bot' | 'project';
|
||||
botId: string;
|
||||
initValue?: TFormData;
|
||||
}) => {
|
||||
//支持通配URL
|
||||
const formatUrl = (url?: string) => {
|
||||
let newUrl = url ?? '';
|
||||
if (newUrl) {
|
||||
if (agentType === 'project') {
|
||||
newUrl = newUrl.replace(/{project_id}/g, botId);
|
||||
} else {
|
||||
newUrl = newUrl.replace(/{bot_id}/g, botId);
|
||||
}
|
||||
newUrl = newUrl
|
||||
.replace(/{hostname}/g, window.location.hostname)
|
||||
.replace(/{corp_id}/g, initValue.corp_id);
|
||||
}
|
||||
|
||||
return newUrl;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles['link-area']}>
|
||||
{copyLinkAreaInfo?.title_text ? (
|
||||
<Space spacing={12} align="start">
|
||||
<span className={styles['step-order']}>
|
||||
{copyLinkAreaInfo.step_order || 1}
|
||||
</span>
|
||||
|
||||
<div className={styles['step-content']}>
|
||||
<div className={styles['step-title']}>
|
||||
{copyLinkAreaInfo.title_text}
|
||||
</div>
|
||||
</div>
|
||||
</Space>
|
||||
) : null}
|
||||
{copyLinkAreaInfo?.description ? (
|
||||
<ReactMarkdown skipHtml={true} className={styles.markdown}>
|
||||
{copyLinkAreaInfo.description}
|
||||
</ReactMarkdown>
|
||||
) : null}
|
||||
|
||||
{copyLinkAreaInfo?.link_list?.length ? (
|
||||
<div className={styles['link-list']}>
|
||||
{copyLinkAreaInfo?.link_list.map(item => (
|
||||
<div key={item.link} style={{ marginBottom: 32 }}>
|
||||
<Typography.Title className={styles.title}>
|
||||
{item.title}
|
||||
</Typography.Title>
|
||||
<Typography.Text className={styles.link} copyable>
|
||||
{formatUrl(item.link)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import { useRequest } from 'ahooks';
|
||||
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
|
||||
import {
|
||||
type SchemaAreaPage,
|
||||
SchemaAreaPageApi,
|
||||
type GetBindConnectorConfigResponse,
|
||||
type SaveBindConnectorConfigResponse,
|
||||
type BindConnectorResponse,
|
||||
} from '@coze-arch/bot-api/developer_api';
|
||||
import { DeveloperApi } from '@coze-arch/bot-api';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
export type ActionResponse =
|
||||
| {
|
||||
action: SchemaAreaPageApi.BindConnector;
|
||||
data: BindConnectorResponse;
|
||||
}
|
||||
| {
|
||||
action: SchemaAreaPageApi.GetBindConnectorConfig;
|
||||
data: GetBindConnectorConfigResponse;
|
||||
}
|
||||
| {
|
||||
action: SchemaAreaPageApi.SaveBindConnectorConfig;
|
||||
data: SaveBindConnectorConfigResponse;
|
||||
}
|
||||
| {
|
||||
action: SchemaAreaPageApi.NotQuery;
|
||||
data: undefined;
|
||||
};
|
||||
|
||||
interface StepActionProps {
|
||||
botId: string;
|
||||
origin?: 'bot' | 'project';
|
||||
schemaPages: SchemaAreaPage[];
|
||||
onNextStepSuccess: (resp: ActionResponse) => void;
|
||||
onNextStepError: (error: Error) => void;
|
||||
}
|
||||
interface StepRunParams {
|
||||
connectorId: string;
|
||||
assignFormValue: Record<string, string>;
|
||||
}
|
||||
|
||||
export const useStepAction = ({
|
||||
botId,
|
||||
origin = 'bot',
|
||||
schemaPages,
|
||||
onNextStepSuccess,
|
||||
onNextStepError,
|
||||
}: StepActionProps) => {
|
||||
const [step, setStep] = useState(0);
|
||||
|
||||
const { space_id = '' } = useParams<DynamicParams>();
|
||||
|
||||
const agentType = origin === 'bot' ? 0 : 1;
|
||||
|
||||
const currentAction =
|
||||
schemaPages?.[step]?.api_action ?? SchemaAreaPageApi.BindConnector;
|
||||
|
||||
const SERVICE_MAP = {
|
||||
[SchemaAreaPageApi.NotQuery]: async () => await Promise.resolve(),
|
||||
[SchemaAreaPageApi.GetBindConnectorConfig]: async (
|
||||
params?: StepRunParams,
|
||||
) => {
|
||||
const data = await DeveloperApi.GetBindConnectorConfig({
|
||||
connector_id: params?.connectorId ?? '',
|
||||
detail: params?.assignFormValue ?? {},
|
||||
agent_type: agentType,
|
||||
bot_id: botId,
|
||||
space_id,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
[SchemaAreaPageApi.SaveBindConnectorConfig]: async (
|
||||
params?: StepRunParams,
|
||||
) => {
|
||||
const data = await DeveloperApi.SaveBindConnectorConfig({
|
||||
connector_id: params?.connectorId ?? '',
|
||||
detail: params?.assignFormValue ?? {},
|
||||
agent_type: agentType,
|
||||
bot_id: botId,
|
||||
space_id,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
[SchemaAreaPageApi.BindConnector]: async (params?: StepRunParams) => {
|
||||
const res = await DeveloperApi.BindConnector(
|
||||
{
|
||||
connector_id: params?.connectorId ?? '',
|
||||
connector_info: params?.assignFormValue ?? {},
|
||||
agent_type: agentType,
|
||||
bot_id: botId,
|
||||
space_id,
|
||||
},
|
||||
{ __disableErrorToast: true },
|
||||
);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
|
||||
const { run, loading } = useRequest(
|
||||
async (params?: StepRunParams) => await SERVICE_MAP[currentAction](params),
|
||||
{
|
||||
manual: true,
|
||||
ready: Object.keys(SERVICE_MAP).includes(String(currentAction)),
|
||||
onSuccess: data => {
|
||||
const action = currentAction as
|
||||
| SchemaAreaPageApi.BindConnector
|
||||
| SchemaAreaPageApi.GetBindConnectorConfig;
|
||||
onNextStepSuccess?.({ data, action });
|
||||
},
|
||||
onError: error => {
|
||||
onNextStepError(error);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
run,
|
||||
loading,
|
||||
step,
|
||||
setStep,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type TFormData = Record<string, string>;
|
||||
|
||||
export type TSubmitValue = Record<string, string>;
|
||||
|
||||
export interface FormActions {
|
||||
submit: () => Promise<TSubmitValue>;
|
||||
reset: () => void;
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
/*
|
||||
* 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 { useRef, useState } from 'react';
|
||||
|
||||
import { useRequest } from 'ahooks';
|
||||
import { type PublishConnectorInfo } from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { UIButton, useUIModal, UIToast, Spin } from '@coze-arch/bot-semi';
|
||||
import { isApiError, type ApiError } from '@coze-arch/bot-http';
|
||||
import {
|
||||
type PublishConnectorInfo as BotPublishConnectorInfo,
|
||||
type QuerySchemaConfig,
|
||||
BindType,
|
||||
SchemaAreaPageApi,
|
||||
type BindConnectorResponse,
|
||||
type SchemaAreaInfo,
|
||||
type CopyLinkAreaInfo,
|
||||
} from '@coze-arch/bot-api/developer_api';
|
||||
import { DeveloperApi } from '@coze-arch/bot-api';
|
||||
import { connector2Redirect } from '@coze-foundation/account-adapter';
|
||||
|
||||
import styles from '../../pages/publish/index.module.less';
|
||||
import { useUnbindPlatformModal } from '../../hook/use-unbind-platform';
|
||||
import { type FormActions, type TSubmitValue } from './types';
|
||||
import { type ActionResponse, useStepAction } from './hooks/use-step-action';
|
||||
import { ConnectorLink } from './connector-link';
|
||||
import { ConnectorGuide } from './connector-guide';
|
||||
import { ConnectorForm } from './connector-form';
|
||||
import { ConnectorError } from './connector-error';
|
||||
|
||||
interface ConnectorConfigureProps {
|
||||
botId: string;
|
||||
origin?: 'project' | 'bot';
|
||||
onSuccess: (
|
||||
val: BotPublishConnectorInfo | PublishConnectorInfo | undefined,
|
||||
) => void;
|
||||
onUnbind?: () => void;
|
||||
}
|
||||
|
||||
interface ConnectorConfigureValueType {
|
||||
initValue: BotPublishConnectorInfo | PublishConnectorInfo;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
export const useConnectorFormModal = ({
|
||||
botId,
|
||||
origin = 'bot',
|
||||
onSuccess,
|
||||
onUnbind,
|
||||
}: ConnectorConfigureProps) => {
|
||||
const formRef = useRef<FormActions>(null);
|
||||
|
||||
const [propsValue, setPropsValue] = useState<ConnectorConfigureValueType>();
|
||||
|
||||
const { initValue } = propsValue ?? {};
|
||||
const [errorMessage, setErrorMessage] = useState<ApiError>();
|
||||
|
||||
const [formDisabled, setFormDisabled] = useState(false);
|
||||
|
||||
const [assignValue, setAssignValue] = useState<TSubmitValue>();
|
||||
const bindId = useRef('');
|
||||
const handleClose = () => {
|
||||
setErrorMessage(undefined);
|
||||
setStep(0);
|
||||
setAssignValue(undefined);
|
||||
formRef.current?.reset();
|
||||
close();
|
||||
};
|
||||
|
||||
const handleUnbind = () => {
|
||||
handleClose();
|
||||
if (onUnbind) {
|
||||
onUnbind();
|
||||
} else {
|
||||
// 兼容历史逻辑,未传入 onUnbind 时,解绑后也调用 onSuccess
|
||||
onSuccess({
|
||||
...(initValue as BotPublishConnectorInfo),
|
||||
bind_info: {},
|
||||
bind_id: '',
|
||||
});
|
||||
}
|
||||
UIToast.success(I18n.t('bot_publish_disconnect_success'));
|
||||
};
|
||||
|
||||
const [connectorConfigInfo, setConnectorConfigInfo] =
|
||||
useState<QuerySchemaConfig>();
|
||||
|
||||
const lastConnectId = useRef<string>();
|
||||
|
||||
const { loading: formSchemaLoading } = useRequest(
|
||||
async () => {
|
||||
const data = await DeveloperApi.QuerySchemaList({
|
||||
connector_id: initValue?.id ?? '',
|
||||
scene: origin,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
{
|
||||
ready: Boolean(initValue?.id),
|
||||
refreshDeps: [initValue?.id],
|
||||
onBefore: () => {
|
||||
if (initValue?.id !== lastConnectId.current) {
|
||||
lastConnectId.current = initValue?.id;
|
||||
setConnectorConfigInfo({});
|
||||
}
|
||||
},
|
||||
onSuccess: data => {
|
||||
if (!data.schema_area_pages?.length) {
|
||||
data.schema_area_pages = [
|
||||
{
|
||||
schema_area: data.schema_area,
|
||||
copy_link_area: data.copy_link_area,
|
||||
},
|
||||
];
|
||||
}
|
||||
setConnectorConfigInfo(data);
|
||||
},
|
||||
onError: () => {
|
||||
setConnectorConfigInfo({});
|
||||
},
|
||||
},
|
||||
);
|
||||
const { schema_area_pages: schemaPages = [] } = connectorConfigInfo ?? {};
|
||||
|
||||
const bindCb = (data: BindConnectorResponse) => {
|
||||
/** 适用Kv+Auth授权场景:KvAuthBind = 4
|
||||
* reddit渠道:若成功返回client_id,则覆盖auth_login_info中的client_id,并附带加密state跳转授权页面
|
||||
* 其余渠道:若成功返回auth_params,则合并auth_login_info作为授权链接参数跳转
|
||||
* */
|
||||
if (
|
||||
initValue?.bind_type === BindType.KvAuthBind &&
|
||||
(data?.client_id || data?.auth_params)
|
||||
) {
|
||||
connector2Redirect(
|
||||
{
|
||||
navigatePath: `${location.pathname}${location.search}`,
|
||||
type: 'oauth',
|
||||
extra: {
|
||||
origin: 'publish',
|
||||
encrypt_state: data?.encrypt_state,
|
||||
},
|
||||
},
|
||||
initValue?.id || '',
|
||||
{
|
||||
...initValue?.auth_login_info,
|
||||
client_id: data?.client_id,
|
||||
...data.auth_params,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
bindId.current = data?.bind_id ?? '';
|
||||
}
|
||||
};
|
||||
|
||||
const stepCallback = () => {
|
||||
const isLastStep = step === schemaPages?.length - 1;
|
||||
if (isLastStep) {
|
||||
if (initValue) {
|
||||
onSuccess({
|
||||
...initValue,
|
||||
bind_info: { ...assignValue },
|
||||
bind_id: bindId.current,
|
||||
});
|
||||
}
|
||||
handleClose();
|
||||
} else {
|
||||
setStep(step + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
loading,
|
||||
run: nextStepRun,
|
||||
step,
|
||||
setStep,
|
||||
} = useStepAction({
|
||||
botId,
|
||||
origin,
|
||||
schemaPages,
|
||||
onNextStepSuccess: (resp: ActionResponse) => {
|
||||
if (resp.action === SchemaAreaPageApi.BindConnector) {
|
||||
bindCb(resp.data);
|
||||
}
|
||||
if (resp.action === SchemaAreaPageApi.GetBindConnectorConfig) {
|
||||
setAssignValue({
|
||||
...assignValue,
|
||||
...resp.data.config?.detail,
|
||||
});
|
||||
}
|
||||
stepCallback();
|
||||
},
|
||||
onNextStepError: error => {
|
||||
if (isApiError(error)) {
|
||||
setErrorMessage(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { node: unbindPlatformModal, open: openUnbindPlatformModal } =
|
||||
useUnbindPlatformModal({
|
||||
botId,
|
||||
origin,
|
||||
platformInfo: initValue as BotPublishConnectorInfo,
|
||||
onUnbind: () => {
|
||||
handleUnbind();
|
||||
},
|
||||
});
|
||||
|
||||
const nextBtnClick = async () => {
|
||||
const value = await formRef.current?.submit();
|
||||
setAssignValue({ ...assignValue, ...value });
|
||||
nextStepRun({
|
||||
connectorId: initValue?.id ?? '',
|
||||
assignFormValue: { ...assignValue, ...value },
|
||||
});
|
||||
};
|
||||
|
||||
const renderFooter = () =>
|
||||
initValue?.bind_id ? (
|
||||
<>
|
||||
<UIButton
|
||||
theme="light"
|
||||
type="tertiary"
|
||||
onClick={() => {
|
||||
close();
|
||||
setStep(0);
|
||||
}}
|
||||
>
|
||||
{I18n.t('Cancel')}
|
||||
</UIButton>
|
||||
<UIButton theme="solid" type="danger" onClick={openUnbindPlatformModal}>
|
||||
{I18n.t('bot_publish_disconnect', {
|
||||
platform: initValue?.name ?? '',
|
||||
})}
|
||||
</UIButton>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{schemaPages?.length &&
|
||||
step !== 0 &&
|
||||
schemaPages[step]?.api_action !== SchemaAreaPageApi.NotQuery ? (
|
||||
// 页面按钮不执行任何操作时 不展示上一步
|
||||
<UIButton
|
||||
theme="solid"
|
||||
onClick={() => {
|
||||
setErrorMessage(undefined);
|
||||
setStep(step - 1);
|
||||
}}
|
||||
>
|
||||
{I18n.t('Previous_1')}
|
||||
</UIButton>
|
||||
) : null}
|
||||
<UIButton
|
||||
theme="solid"
|
||||
onClick={nextBtnClick}
|
||||
disabled={formDisabled}
|
||||
loading={loading}
|
||||
>
|
||||
{step === (schemaPages?.length ?? 0) - 1
|
||||
? schemaPages[step]?.api_action !== SchemaAreaPageApi.NotQuery
|
||||
? I18n.t('Save')
|
||||
: I18n.t('Complete')
|
||||
: I18n.t('Next_1')}
|
||||
</UIButton>
|
||||
</>
|
||||
);
|
||||
|
||||
const { modal, open, close } = useUIModal({
|
||||
type: 'action-small',
|
||||
footer: renderFooter(),
|
||||
onCancel: handleClose,
|
||||
title: connectorConfigInfo?.title_text,
|
||||
});
|
||||
|
||||
const renderConnectorArea = (
|
||||
copyArea?: CopyLinkAreaInfo,
|
||||
schemaArea?: SchemaAreaInfo,
|
||||
) => (
|
||||
<>
|
||||
{copyArea ? (
|
||||
<ConnectorLink
|
||||
copyLinkAreaInfo={copyArea}
|
||||
agentType={origin}
|
||||
botId={botId}
|
||||
initValue={{ ...initValue?.bind_info, ...assignValue }}
|
||||
/>
|
||||
) : null}
|
||||
{schemaArea ? (
|
||||
<ConnectorForm
|
||||
schemaAreaInfo={schemaArea}
|
||||
initValue={{ ...initValue?.bind_info, ...assignValue }}
|
||||
ref={formRef}
|
||||
getFormDisable={disable => setFormDisabled(disable)}
|
||||
isReadOnly={Boolean(initValue?.bind_id)}
|
||||
setErrorMessage={setErrorMessage}
|
||||
/>
|
||||
) : null}
|
||||
{errorMessage ? <ConnectorError errorMessage={errorMessage} /> : null}
|
||||
</>
|
||||
);
|
||||
return {
|
||||
node: modal(
|
||||
<Spin
|
||||
wrapperClassName={styles['config-area']}
|
||||
spinning={formSchemaLoading}
|
||||
>
|
||||
<ConnectorGuide connectorConfigInfo={connectorConfigInfo} />
|
||||
|
||||
{schemaPages?.length && !initValue?.bind_id ? (
|
||||
<div>
|
||||
{renderConnectorArea(
|
||||
schemaPages[step]?.copy_link_area,
|
||||
schemaPages[step]?.schema_area,
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{initValue?.bind_id && schemaPages?.length ? (
|
||||
<>
|
||||
{schemaPages?.map((item, i) => (
|
||||
<div key={i}>
|
||||
{renderConnectorArea(item.copy_link_area, item.schema_area)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : null}
|
||||
{unbindPlatformModal}
|
||||
</Spin>,
|
||||
),
|
||||
open: (props: ConnectorConfigureValueType) => {
|
||||
setPropsValue(props);
|
||||
open();
|
||||
},
|
||||
close,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user