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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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