mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
chore: extract wechat credentials to custom render form
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
11
src/routes/(main)/agent/channel/platform/registry.ts
Normal file
11
src/routes/(main)/agent/channel/platform/registry.ts
Normal 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,
|
||||
};
|
||||
11
src/routes/(main)/agent/channel/platform/types.ts
Normal file
11
src/routes/(main)/agent/channel/platform/types.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { WechatClientFactory } from './client';
|
||||
import { schema } from './schema';
|
||||
|
||||
export const wechat: PlatformDefinition = {
|
||||
authFlow: 'qrcode',
|
||||
id: 'wechat',
|
||||
name: 'WeChat',
|
||||
connectionMode: 'persistent',
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user