Refactor channel account management: move binding/editing to Channels, align Agents display, and simplify UX (#523)

This commit is contained in:
Felix
2026-03-16 18:20:11 +08:00
committed by GitHub
Unverified
parent db480dff17
commit 4be679ac56
20 changed files with 1192 additions and 346 deletions

View File

@@ -47,7 +47,11 @@ interface ChannelConfigModalProps {
configuredTypes?: string[];
showChannelName?: boolean;
allowExistingConfig?: boolean;
allowEditAccountId?: boolean;
existingAccountIds?: string[];
initialConfigValues?: Record<string, string>;
agentId?: string;
accountId?: string;
onClose: () => void;
onChannelSaved?: (channelType: ChannelType) => void | Promise<void>;
}
@@ -62,7 +66,11 @@ export function ChannelConfigModal({
configuredTypes = [],
showChannelName = true,
allowExistingConfig = true,
allowEditAccountId = false,
existingAccountIds = [],
initialConfigValues,
agentId,
accountId,
onClose,
onChannelSaved,
}: ChannelConfigModalProps) {
@@ -71,6 +79,7 @@ export function ChannelConfigModal({
const [selectedType, setSelectedType] = useState<ChannelType | null>(initialSelectedType);
const [configValues, setConfigValues] = useState<Record<string, string>>({});
const [channelName, setChannelName] = useState('');
const [accountIdInput, setAccountIdInput] = useState(accountId || '');
const [connecting, setConnecting] = useState(false);
const [showSecrets, setShowSecrets] = useState<Record<string, boolean>>({});
const [qrCode, setQrCode] = useState<string | null>(null);
@@ -86,11 +95,18 @@ export function ChannelConfigModal({
const meta: ChannelMeta | null = selectedType ? CHANNEL_META[selectedType] : null;
const shouldUseCredentialValidation = selectedType !== 'feishu';
const resolvedAccountId = allowEditAccountId
? accountIdInput.trim()
: (accountId ?? (agentId ? (agentId === 'main' ? 'default' : agentId) : undefined));
useEffect(() => {
setSelectedType(initialSelectedType);
}, [initialSelectedType]);
useEffect(() => {
setAccountIdInput(accountId || '');
}, [accountId]);
useEffect(() => {
if (!selectedType) {
setConfigValues({});
@@ -112,13 +128,21 @@ export function ChannelConfigModal({
return;
}
if (initialConfigValues) {
setConfigValues(initialConfigValues);
setIsExistingConfig(Object.keys(initialConfigValues).length > 0);
setLoadingConfig(false);
setChannelName(showChannelName ? CHANNEL_NAMES[selectedType] : '');
return;
}
let cancelled = false;
setLoadingConfig(true);
setChannelName(showChannelName ? CHANNEL_NAMES[selectedType] : '');
(async () => {
try {
const accountParam = agentId ? `?accountId=${encodeURIComponent(agentId === 'main' ? 'default' : agentId)}` : '';
const accountParam = resolvedAccountId ? `?accountId=${encodeURIComponent(resolvedAccountId)}` : '';
const result = await hostApiFetch<{ success: boolean; values?: Record<string, string> }>(
`/api/channels/config/${encodeURIComponent(selectedType)}${accountParam}`
);
@@ -144,7 +168,7 @@ export function ChannelConfigModal({
return () => {
cancelled = true;
};
}, [agentId, allowExistingConfig, configuredTypes, selectedType, showChannelName]);
}, [allowExistingConfig, configuredTypes, initialConfigValues, resolvedAccountId, selectedType, showChannelName]);
useEffect(() => {
if (selectedType && !loadingConfig && showChannelName && firstInputRef.current) {
@@ -187,13 +211,18 @@ export function ChannelConfigModal({
try {
const saveResult = await hostApiFetch<{ success?: boolean; error?: string }>('/api/channels/config', {
method: 'POST',
body: JSON.stringify({ channelType: 'whatsapp', config: { enabled: true } }),
body: JSON.stringify({ channelType: 'whatsapp', config: { enabled: true }, accountId: resolvedAccountId }),
});
if (!saveResult?.success) {
throw new Error(saveResult?.error || 'Failed to save WhatsApp config');
}
await finishSave('whatsapp');
try {
await finishSave('whatsapp');
} catch (postSaveError) {
toast.warning(t('toast.savedButRefreshFailed'));
console.warn('Channel saved but post-save refresh failed:', postSaveError);
}
// Gateway restart is already triggered by scheduleGatewayChannelRestart
// in the POST /api/channels/config route handler (debounced). Calling
// restart() here directly races with that debounced restart and the
@@ -222,7 +251,7 @@ export function ChannelConfigModal({
removeErrorListener();
hostApiFetch('/api/channels/whatsapp/cancel', { method: 'POST' }).catch(() => { });
};
}, [selectedType, finishSave, onClose, t]);
}, [finishSave, onClose, resolvedAccountId, selectedType, t]);
const handleValidate = async () => {
if (!selectedType || !shouldUseCredentialValidation) return;
@@ -273,10 +302,25 @@ export function ChannelConfigModal({
setValidationResult(null);
try {
if (allowEditAccountId) {
const nextAccountId = accountIdInput.trim();
if (!nextAccountId) {
toast.error(t('account.invalidId'));
setConnecting(false);
return;
}
const duplicateExists = existingAccountIds.some((id) => id === nextAccountId && id !== (accountId || '').trim());
if (duplicateExists) {
toast.error(t('account.accountIdExists', { accountId: nextAccountId }));
setConnecting(false);
return;
}
}
if (meta.connectionType === 'qr') {
await hostApiFetch('/api/channels/whatsapp/start', {
method: 'POST',
body: JSON.stringify({ accountId: 'default' }),
body: JSON.stringify({ accountId: resolvedAccountId || 'default' }),
});
return;
}
@@ -319,7 +363,6 @@ export function ChannelConfigModal({
}
const config: Record<string, unknown> = { ...configValues };
const resolvedAccountId = agentId ? (agentId === 'main' ? 'default' : agentId) : undefined;
const saveResult = await hostApiFetch<{
success?: boolean;
error?: string;
@@ -335,7 +378,12 @@ export function ChannelConfigModal({
toast.warning(saveResult.warning);
}
await finishSave(selectedType);
try {
await finishSave(selectedType);
} catch (postSaveError) {
toast.warning(t('toast.savedButRefreshFailed'));
console.warn('Channel saved but post-save refresh failed:', postSaveError);
}
toast.success(t('toast.channelSaved', { name: meta.name }));
toast.success(t('toast.channelConnecting', { name: meta.name }));
@@ -534,6 +582,20 @@ export function ChannelConfigModal({
</div>
)}
{allowEditAccountId && (
<div className="space-y-2.5">
<Label htmlFor="account-id" className={labelClasses}>{t('account.customIdLabel')}</Label>
<Input
id="account-id"
value={accountIdInput}
onChange={(event) => setAccountIdInput(event.target.value)}
placeholder={t('account.customIdPlaceholder')}
className={inputClasses}
/>
<p className="text-[12px] text-muted-foreground">{t('account.customIdHint')}</p>
</div>
)}
<div className="space-y-4">
{meta?.configFields.map((field) => (
<ConfigField
@@ -623,7 +685,7 @@ export function ChannelConfigModal({
onClick={() => {
void handleConnect();
}}
disabled={connecting || !isFormValid()}
disabled={connecting || !isFormValid() || (allowEditAccountId && !accountIdInput.trim())}
className={primaryButtonClasses}
>
{connecting ? (