chore: extract wechat credentials to custom render form

This commit is contained in:
rdmclin2
2026-03-26 11:56:17 +08:00
parent aefb3d9788
commit a16471ffbe
10 changed files with 273 additions and 186 deletions

View File

@@ -13,18 +13,12 @@ import type {
SerializedPlatformDefinition,
} from '@/server/services/bot/platforms/types';
import { platformCredentialBodyMap } from '../platform/registry';
import type { ChannelFormValues } from './index';
import QrCodeAuth from './QrCodeAuth';
const prefixCls = 'ant';
const styles = createStaticStyles(({ css }) => ({
connectedInfoHeader: css`
display: flex;
align-items: center;
justify-content: space-between;
margin-block-end: 16px;
`,
form: css`
.${prefixCls}-form-item-control:has(.${prefixCls}-input, .${prefixCls}-select, .${prefixCls}-input-number) {
flex: none;
@@ -165,30 +159,6 @@ const ApplicationIdField = memo<{ field: FieldSchema }>(({ field }) => {
);
});
const ReadOnlyField = memo<{
description?: string;
divider?: boolean;
label: string;
password?: boolean;
tag: string;
value?: string;
}>(({ description, divider, label, password, tag, value }) => {
const InputComponent = password ? FormPassword : FormInput;
return (
<FormItem
desc={description}
divider={divider}
label={label}
minWidth={'max(50%, 400px)'}
tag={tag}
variant="borderless"
>
<InputComponent readOnly value={value || ''} />
</FormItem>
);
});
// --------------- Helper: flatten fields from schema ---------------
function getFields(schema: FieldSchema[], sectionKey: string): FieldSchema[] {
@@ -225,115 +195,66 @@ interface BodyProps {
};
form: FormInstance<ChannelFormValues>;
hasConfig?: boolean;
onQrAuthenticated?: (credentials: { botId: string; botToken: string; userId: string }) => void;
onAuthenticated?: (params: {
applicationId: string;
credentials: Record<string, string>;
}) => void;
platformDef: SerializedPlatformDefinition;
}
const Body = memo<BodyProps>(
({ platformDef, form, hasConfig, currentConfig, onQrAuthenticated }) => {
const { t: _t } = useTranslation('agent');
const t = _t as (key: string) => string;
const Body = memo<BodyProps>(({ platformDef, form, hasConfig, currentConfig, onAuthenticated }) => {
const { t: _t } = useTranslation('agent');
const t = _t as (key: string) => string;
const applicationIdField = useMemo(
() => platformDef.schema.find((f) => f.key === 'applicationId'),
[platformDef.schema],
);
const CustomCredentialBody = platformCredentialBodyMap[platformDef.id];
const credentialFields = useMemo(
() => getFields(platformDef.schema, 'credentials'),
[platformDef.schema],
);
const applicationIdField = useMemo(
() => platformDef.schema.find((f) => f.key === 'applicationId'),
[platformDef.schema],
);
const settingsFields = useMemo(
() => getFields(platformDef.schema, 'settings'),
[platformDef.schema],
);
const credentialFields = useMemo(
() => getFields(platformDef.schema, 'credentials'),
[platformDef.schema],
);
const [settingsActive, setSettingsActive] = useState(false);
const shouldShowWechatApplicationId =
!!currentConfig?.applicationId &&
currentConfig.applicationId !== currentConfig.credentials.botId;
const settingsFields = useMemo(
() => getFields(platformDef.schema, 'settings'),
[platformDef.schema],
);
const handleResetSettings = useCallback(() => {
const defaults: Record<string, any> = {};
for (const field of settingsFields) {
if (field.default !== undefined) {
defaults[field.key] = field.default;
}
const [settingsActive, setSettingsActive] = useState(false);
const handleResetSettings = useCallback(() => {
const defaults: Record<string, any> = {};
for (const field of settingsFields) {
if (field.default !== undefined) {
defaults[field.key] = field.default;
}
form.setFieldsValue({ settings: defaults });
}, [form, settingsFields]);
}
form.setFieldsValue({ settings: defaults });
}, [form, settingsFields]);
return (
<Form
className={styles.form}
form={form}
gap={0}
itemMinWidth={'max(50%, 400px)'}
requiredMark={false}
style={{ maxWidth: 1024, padding: '16px 0', width: '100%' }}
variant={'borderless'}
>
{platformDef.authFlow === 'qrcode' && hasConfig && currentConfig && (
<>
<div className={styles.connectedInfoHeader}>
<Flexbox gap={4}>
<div style={{ fontSize: 16, fontWeight: 600 }}>
{t('channel.wechatConnectedInfo')}
</div>
<div style={{ color: 'var(--ant-color-text-secondary)', fontSize: 13 }}>
{t('channel.wechatManagedCredentials')}
</div>
</Flexbox>
{onQrAuthenticated && (
<QrCodeAuth
buttonLabel={t('channel.wechatRebind')}
buttonType="default"
showTips={false}
onAuthenticated={onQrAuthenticated}
/>
)}
</div>
{shouldShowWechatApplicationId && (
<ReadOnlyField
description={t('channel.applicationIdHint')}
label={t('channel.applicationId')}
tag="applicationId"
value={currentConfig.applicationId}
/>
)}
<ReadOnlyField
description={t('channel.wechatBotIdHint')}
divider={shouldShowWechatApplicationId}
label={t('channel.wechatBotId')}
tag="botId"
value={currentConfig.credentials.botId}
/>
<ReadOnlyField
divider
password
description={t('channel.botTokenEncryptedHint')}
label={t('channel.botToken')}
tag="botToken"
value={currentConfig.credentials.botToken}
/>
<ReadOnlyField
divider
description={t('channel.wechatUserIdHint')}
label={t('channel.wechatUserId')}
tag="userId"
value={currentConfig.credentials.userId}
/>
</>
)}
{platformDef.authFlow === 'qrcode' && onQrAuthenticated && !hasConfig && (
<div style={{ display: 'flex', justifyContent: 'center', padding: '16px 0' }}>
<QrCodeAuth onAuthenticated={onQrAuthenticated} />
</div>
)}
{applicationIdField && <ApplicationIdField field={applicationIdField} />}
{!platformDef.authFlow &&
credentialFields.map((field, i) => (
return (
<Form
className={styles.form}
form={form}
gap={0}
itemMinWidth={'max(50%, 400px)'}
requiredMark={false}
style={{ maxWidth: 1024, padding: '16px 0', width: '100%' }}
variant={'borderless'}
>
{CustomCredentialBody ? (
<CustomCredentialBody
currentConfig={currentConfig}
hasConfig={hasConfig}
onAuthenticated={onAuthenticated}
/>
) : (
<>
{applicationIdField && <ApplicationIdField field={applicationIdField} />}
{credentialFields.map((field, i) => (
<SchemaField
divider={applicationIdField ? true : i !== 0}
field={field}
@@ -341,36 +262,34 @@ const Body = memo<BodyProps>(
parentKey="credentials"
/>
))}
{settingsFields.length > 0 && (
<FormGroup
collapsible
defaultActive={false}
keyValue={`settings-${platformDef.id}`}
style={{ marginBlockStart: 16 }}
title={<SettingsTitle schema={platformDef.schema} />}
variant="borderless"
extra={
settingsActive ? (
<Popconfirm
title={t('channel.settingsResetConfirm')}
onConfirm={handleResetSettings}
>
<Button icon={<RotateCcw size={14} />} size="small" type="default">
{t('channel.settingsResetDefault')}
</Button>
</Popconfirm>
) : undefined
}
onCollapse={setSettingsActive}
>
{settingsFields.map((field, i) => (
<SchemaField divider={i !== 0} field={field} key={field.key} parentKey="settings" />
))}
</FormGroup>
)}
</Form>
);
},
);
</>
)}
{settingsFields.length > 0 && (
<FormGroup
collapsible
defaultActive={false}
keyValue={`settings-${platformDef.id}`}
style={{ marginBlockStart: 16 }}
title={<SettingsTitle schema={platformDef.schema} />}
variant="borderless"
extra={
settingsActive ? (
<Popconfirm title={t('channel.settingsResetConfirm')} onConfirm={handleResetSettings}>
<Button icon={<RotateCcw size={14} />} size="small" type="default">
{t('channel.settingsResetDefault')}
</Button>
</Popconfirm>
) : undefined
}
onCollapse={setSettingsActive}
>
{settingsFields.map((field, i) => (
<SchemaField divider={i !== 0} field={field} key={field.key} parentKey="settings" />
))}
</FormGroup>
)}
</Form>
);
});
export default Body;

View File

@@ -291,25 +291,14 @@ const PlatformDetail = memo<PlatformDetailProps>(({ platformDef, agentId, curren
connectCurrentBot,
]);
const handleQrAuthenticated = useCallback(
async (creds: { botId: string; botToken: string; userId: string }) => {
const handleExternalAuth = useCallback(
async (params: { applicationId: string; credentials: Record<string, string> }) => {
setSaving(true);
setSaveResult(undefined);
setConnectResult(undefined);
try {
const botToken = creds.botToken?.trim();
if (!creds.botId && !botToken) {
throw new Error('Bot Token is required');
}
const credentials = {
botId: creds.botId,
botToken: creds.botToken,
userId: creds.userId,
};
const applicationId = creds.botId || botToken?.slice(0, 16) || '';
const { applicationId, credentials } = params;
const settings = form.getFieldValue('settings') || {};
if (currentConfig) {
@@ -429,7 +418,7 @@ const PlatformDetail = memo<PlatformDetailProps>(({ platformDef, agentId, curren
form={form}
hasConfig={!!currentConfig}
platformDef={platformDef}
onQrAuthenticated={platformDef.authFlow === 'qrcode' ? handleQrAuthenticated : undefined}
onAuthenticated={handleExternalAuth}
/>
<Footer
connectResult={connectResult}

View File

@@ -0,0 +1,11 @@
import type { ComponentType } from 'react';
import type { PlatformCredentialBodyProps } from './types';
import WechatCredentialBody from './wechat/CredentialBody';
export const platformCredentialBodyMap: Record<
string,
ComponentType<PlatformCredentialBodyProps>
> = {
wechat: WechatCredentialBody,
};

View File

@@ -0,0 +1,11 @@
export interface PlatformCredentialBodyProps {
currentConfig?: {
applicationId: string;
credentials: Record<string, string>;
};
hasConfig?: boolean;
onAuthenticated?: (params: {
applicationId: string;
credentials: Record<string, string>;
}) => void;
}

View File

@@ -0,0 +1,119 @@
'use client';
import { Flexbox, FormItem } from '@lobehub/ui';
import { createStaticStyles } from 'antd-style';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { FormInput, FormPassword } from '@/components/FormInput';
import QrCodeAuth from './QrCodeAuth';
const styles = createStaticStyles(({ css }) => ({
header: css`
display: flex;
align-items: center;
justify-content: space-between;
margin-block-end: 16px;
`,
}));
const ReadOnlyField = memo<{
description?: string;
divider?: boolean;
label: string;
password?: boolean;
tag: string;
value?: string;
}>(({ description, divider, label, password, tag, value }) => {
const InputComponent = password ? FormPassword : FormInput;
return (
<FormItem
desc={description}
divider={divider}
label={label}
minWidth={'max(50%, 400px)'}
tag={tag}
variant="borderless"
>
<InputComponent readOnly value={value || ''} />
</FormItem>
);
});
interface WechatConnectedInfoProps {
currentConfig: {
applicationId: string;
credentials: Record<string, string>;
};
onQrAuthenticated?: (credentials: { botId: string; botToken: string; userId: string }) => void;
}
const WechatConnectedInfo = memo<WechatConnectedInfoProps>(
({ currentConfig, onQrAuthenticated }) => {
const { t: _t } = useTranslation('agent');
const t = _t as (key: string) => string;
const shouldShowApplicationId =
!!currentConfig.applicationId &&
currentConfig.applicationId !== currentConfig.credentials.botId;
return (
<>
<div className={styles.header}>
<Flexbox gap={4}>
<div style={{ fontSize: 16, fontWeight: 600 }}>{t('channel.wechatConnectedInfo')}</div>
<div style={{ color: 'var(--ant-color-text-secondary)', fontSize: 13 }}>
{t('channel.wechatManagedCredentials')}
</div>
</Flexbox>
{onQrAuthenticated && (
<QrCodeAuth
buttonLabel={t('channel.wechatRebind')}
buttonType="default"
showTips={false}
onAuthenticated={onQrAuthenticated}
/>
)}
</div>
{shouldShowApplicationId && (
<ReadOnlyField
description={t('channel.applicationIdHint')}
label={t('channel.applicationId')}
tag="applicationId"
value={currentConfig.applicationId}
/>
)}
{process.env.NODE_ENV === 'development' && (
<>
<ReadOnlyField
description={t('channel.wechatBotIdHint')}
divider={shouldShowApplicationId}
label={t('channel.wechatBotId')}
tag="botId"
value={currentConfig.credentials.botId}
/>
<ReadOnlyField
divider
password
description={t('channel.botTokenEncryptedHint')}
label={t('channel.botToken')}
tag="botToken"
value={currentConfig.credentials.botToken}
/>
<ReadOnlyField
divider
description={t('channel.wechatUserIdHint')}
label={t('channel.wechatUserId')}
tag="userId"
value={currentConfig.credentials.userId}
/>
</>
)}
</>
);
},
);
export default WechatConnectedInfo;

View File

@@ -0,0 +1,47 @@
'use client';
import { memo, useCallback } from 'react';
import type { PlatformCredentialBodyProps } from '../types';
import ConnectedInfo from './ConnectedInfo';
import QrCodeAuth from './QrCodeAuth';
const CredentialBody = memo<PlatformCredentialBodyProps>(
({ currentConfig, hasConfig, onAuthenticated }) => {
const handleQrAuthenticated = useCallback(
(creds: { botId: string; botToken: string; userId: string }) => {
const botToken = creds.botToken?.trim();
if (!creds.botId && !botToken) return;
const applicationId = creds.botId || botToken?.slice(0, 16) || '';
onAuthenticated?.({
applicationId,
credentials: {
botId: creds.botId,
botToken: creds.botToken,
userId: creds.userId,
},
});
},
[onAuthenticated],
);
if (hasConfig && currentConfig) {
return (
<ConnectedInfo currentConfig={currentConfig} onQrAuthenticated={handleQrAuthenticated} />
);
}
if (onAuthenticated) {
return (
<div style={{ display: 'flex', justifyContent: 'center', padding: '16px 0' }}>
<QrCodeAuth onAuthenticated={handleQrAuthenticated} />
</div>
);
}
return null;
},
);
export default CredentialBody;

View File

@@ -241,13 +241,6 @@ export abstract class ClientFactory {
* Contains metadata, factory, and validation. All runtime operations go through PlatformClient.
*/
export interface PlatformDefinition {
/**
* Authentication flow for obtaining credentials.
* - 'qrcode': QR code scan flow (e.g. WeChat iLink)
* When set, the frontend renders a QR code auth UI instead of manual credential inputs.
*/
authFlow?: 'qrcode';
/** Factory for creating PlatformClient instances and validating credentials/settings. */
clientFactory: ClientFactory;

View File

@@ -3,7 +3,6 @@ import { WechatClientFactory } from './client';
import { schema } from './schema';
export const wechat: PlatformDefinition = {
authFlow: 'qrcode',
id: 'wechat',
name: 'WeChat',
connectionMode: 'persistent',

View File

@@ -2,8 +2,7 @@ import { DEFAULT_DEBOUNCE_MS, MAX_DEBOUNCE_MS } from '../const';
import type { FieldSchema } from '../types';
export const schema: FieldSchema[] = [
// No credentials fields — WeChat uses QR code auth flow (authFlow: 'qrcode').
// botToken, botId, and userId are populated automatically after QR scan.
// No credentials fields — credentials are populated automatically after QR scan.
{
key: 'settings',
label: 'channel.settings',