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

View File

@@ -291,25 +291,14 @@ const PlatformDetail = memo<PlatformDetailProps>(({ platformDef, agentId, curren
connectCurrentBot, connectCurrentBot,
]); ]);
const handleQrAuthenticated = useCallback( const handleExternalAuth = useCallback(
async (creds: { botId: string; botToken: string; userId: string }) => { async (params: { applicationId: string; credentials: Record<string, string> }) => {
setSaving(true); setSaving(true);
setSaveResult(undefined); setSaveResult(undefined);
setConnectResult(undefined); setConnectResult(undefined);
try { try {
const botToken = creds.botToken?.trim(); const { applicationId, credentials } = params;
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 settings = form.getFieldValue('settings') || {}; const settings = form.getFieldValue('settings') || {};
if (currentConfig) { if (currentConfig) {
@@ -429,7 +418,7 @@ const PlatformDetail = memo<PlatformDetailProps>(({ platformDef, agentId, curren
form={form} form={form}
hasConfig={!!currentConfig} hasConfig={!!currentConfig}
platformDef={platformDef} platformDef={platformDef}
onQrAuthenticated={platformDef.authFlow === 'qrcode' ? handleQrAuthenticated : undefined} onAuthenticated={handleExternalAuth}
/> />
<Footer <Footer
connectResult={connectResult} 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. * Contains metadata, factory, and validation. All runtime operations go through PlatformClient.
*/ */
export interface PlatformDefinition { 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. */ /** Factory for creating PlatformClient instances and validating credentials/settings. */
clientFactory: ClientFactory; clientFactory: ClientFactory;

View File

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

View File

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