Simplify WeChat binding cards and guided setup
This commit is contained in:
1 parent
ad1d2f261d
commit
d823ddb8fa
5 files changed
+768
-303
No files matched your search
@@ -1,34 +1,42 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ArrowLeft, CheckCircle2, Loader2, MessageCircle, Pause, Play, Plus, QrCode, RefreshCw, Unplug, X } from 'lucide-react';
|
||||
import { ArrowRight, Check, CheckCircle2, ChevronLeft, Link2, Loader2, Pause, Play, Plus, QrCode, RefreshCw, Unplug } from 'lucide-react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import wechatMark from '@/assets/channels/wechat.svg';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select } from '@/components/ui/select';
|
||||
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { cloudAgentsApi } from '@/lib/cloud-agents-api';
|
||||
import type { CloudAgentDraft, CloudPendingOperation, CloudUserChannelAccount, CloudWechatBindState } from '../../../shared/cloud-agents';
|
||||
import { CloudChannelPanel } from './CloudChannelPanel';
|
||||
import { CloudAvatar } from './CloudAvatar';
|
||||
import { CloudRecovery } from './CloudRecovery';
|
||||
import { CloudWechatQr } from './CloudWechatQr';
|
||||
|
||||
const statusText: Record<string, string> = {
|
||||
connected: '已连接', active: '运行中', enabled: '已启用', paused: '已暂停', disabled: '已暂停',
|
||||
login_required: '需要扫码', offline: '暂时离线', provisioning: '准备中', unknown: '状态未知',
|
||||
verification_required: '需要验证码', pending: '处理中', confirmed: '已连接', expired: '已过期', failed: '失败',
|
||||
};
|
||||
type Flow = { mode: 'new' | 'manage'; step: 'choose' | 'connect' | 'result'; accountId: string; target: string; remark: string };
|
||||
type Confirmation = 'switch' | 'unlink' | 'disconnect' | null;
|
||||
const errorText = (error: unknown) => error instanceof Error ? error.message : '操作没有完成,请重试';
|
||||
const blockerText: Record<string, string> = {
|
||||
target_required: '尚未选择目标智能体', target_unavailable: '原目标智能体暂不可用', publish_required: '目标智能体需要先发布',
|
||||
login_required: '需要连接微信', worker_offline: '渠道服务暂时离线', channel_sync_degraded: '渠道状态同步异常',
|
||||
target_required: '还没有绑定智能体', target_unavailable: '绑定的智能体暂时不能使用,请选择另一个',
|
||||
publish_required: '请先在智能体页面完成配置并开始使用', login_required: '需要重新扫码连接微信',
|
||||
worker_offline: '微信连接暂时不可用,请稍后刷新', channel_sync_degraded: '连接状态正在确认,请稍后刷新',
|
||||
};
|
||||
const label = (value?: string | null) => statusText[value ?? 'unknown'] ?? value ?? '状态未知';
|
||||
const errorText = (error: unknown) => error instanceof Error ? error.message : '渠道操作失败,请重试';
|
||||
const operationId = () => crypto.randomUUID();
|
||||
|
||||
async function loadPublishedAgents(): Promise<CloudAgentDraft[]> {
|
||||
const qrLabels: Record<string, string> = {
|
||||
pending: '等待微信确认', scanned: '已扫码,请在微信中确认', verification_required: '需要微信验证码',
|
||||
expired: '二维码已过期', failed: '连接没有完成', provisioning: '正在准备二维码', unknown: '正在确认连接',
|
||||
};
|
||||
function accountState(account: CloudUserChannelAccount) {
|
||||
if (account.health !== 'connected') return { text: ['login_required', 'verification_required'].includes(account.health) ? '需要扫码' : '连接待确认', tone: 'bg-[#fff0d8] text-[#876033]' };
|
||||
if (!account.target_agent_slug) return { text: '未绑定智能体', tone: 'bg-[#f0edf8] text-[#75618e]' };
|
||||
if (account.blockers.length) return { text: '暂时不可用', tone: 'bg-[#fff0d8] text-[#876033]' };
|
||||
return account.enabled ? { text: '可以使用', tone: 'bg-[#e7f4ec] text-[#3f7458]' } : { text: '已暂停', tone: 'bg-[#f0edf8] text-[#75618e]' };
|
||||
}
|
||||
async function loadAvailableAgents(): Promise<CloudAgentDraft[]> {
|
||||
const items: CloudAgentDraft[] = [];
|
||||
let cursor: string | null = null;
|
||||
do {
|
||||
const page = await cloudAgentsApi.list(cursor);
|
||||
items.push(...page.agents.filter(agent => Boolean(agent.published_version)));
|
||||
items.push(...page.agents.filter(agent => Boolean(agent.published_version) && agent.enabled !== false));
|
||||
cursor = page.next_cursor;
|
||||
} while (cursor);
|
||||
return items;
|
||||
@@ -37,199 +45,301 @@ async function loadPublishedAgents(): Promise<CloudAgentDraft[]> {
|
||||
export function ChannelAccounts() {
|
||||
const navigate = useNavigate();
|
||||
const [params, setParams] = useSearchParams();
|
||||
const initialAccount = useRef(params.get('account') ?? '');
|
||||
const initialTarget = useRef(params.get('target') ?? '');
|
||||
const [accounts, setAccounts] = useState<CloudUserChannelAccount[]>([]);
|
||||
const [agents, setAgents] = useState<CloudAgentDraft[]>([]);
|
||||
const [selectedId, setSelectedId] = useState(params.get('account') ?? '');
|
||||
const [targetSlug, setTargetSlug] = useState(params.get('target') ?? '');
|
||||
const [newName, setNewName] = useState('');
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [flow, setFlow] = useState<Flow | null>(null);
|
||||
const [confirmation, setConfirmation] = useState<Confirmation>(null);
|
||||
const [busy, setBusy] = useState('');
|
||||
const busyRef = useRef(false);
|
||||
const [error, setError] = useState('');
|
||||
const [notice, setNotice] = useState('');
|
||||
const [qr, setQr] = useState<CloudWechatBindState | null>(null);
|
||||
const [qrAccountId, setQrAccountId] = useState('');
|
||||
const [connectedAccountId, setConnectedAccountId] = useState('');
|
||||
const [verifyCode, setVerifyCode] = useState('');
|
||||
const [confirmDisconnect, setConfirmDisconnect] = useState(false);
|
||||
const [pending, setPending] = useState<CloudPendingOperation[]>([]);
|
||||
const alive = useRef(true);
|
||||
const intents = useRef(new Map<string, { fingerprint: string; id: string }>());
|
||||
const selected = accounts.find(account => account.id === flow?.accountId) ?? null;
|
||||
const recoveryBlocked = pending.some(item => /channelaccount/iu.test(item.operation));
|
||||
const disabled = Boolean(busy) || recoveryBlocked;
|
||||
const targetAgent = useMemo(() => agents.find(agent => agent.slug === flow?.target), [agents, flow?.target]);
|
||||
|
||||
const selected = useMemo(() => accounts.find(item => item.id === selectedId) ?? null, [accounts, selectedId]);
|
||||
const refresh = useCallback(async () => {
|
||||
const [accountPage, published, recovery] = await Promise.all([
|
||||
cloudAgentsApi.call('channelAccounts', {}),
|
||||
loadPublishedAgents(),
|
||||
cloudAgentsApi.recovery(),
|
||||
const [accountPage, available, recovery] = await Promise.all([
|
||||
cloudAgentsApi.call('channelAccounts', {}), loadAvailableAgents(), cloudAgentsApi.recovery(),
|
||||
]);
|
||||
if (!alive.current) return;
|
||||
setAccounts(accountPage.items);
|
||||
setAgents(published);
|
||||
setPending(recovery.pending);
|
||||
setSelectedId(current => accountPage.items.some(item => item.id === current) ? current : accountPage.items[0]?.id ?? '');
|
||||
setAccounts(accountPage.items); setAgents(available); setPending(recovery.pending); setLoaded(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
alive.current = true;
|
||||
void refresh().catch(error => { if (alive.current) setError(errorText(error)); });
|
||||
void refresh().catch(failure => { if (alive.current) setError(errorText(failure)); })
|
||||
.finally(() => { if (alive.current) setLoading(false); });
|
||||
return () => { alive.current = false; };
|
||||
}, [refresh]);
|
||||
useEffect(() => {
|
||||
if (!selected) return;
|
||||
setTargetSlug(params.get('target') ?? selected.target_agent_slug ?? '');
|
||||
setQr(null); setQrAccountId(''); setConnectedAccountId(''); setVerifyCode(''); setConfirmDisconnect(false);
|
||||
const next = new URLSearchParams(params);
|
||||
next.set('account', selected.id);
|
||||
next.delete('target');
|
||||
if (loading || !initialAccount.current) return;
|
||||
const account = accounts.find(item => item.id === initialAccount.current);
|
||||
if (!account) return;
|
||||
initialAccount.current = '';
|
||||
const target = initialTarget.current || account.target_agent_slug || '';
|
||||
setFlow({ mode: 'manage', step: 'choose', accountId: account.id, target, remark: account.display_name });
|
||||
initialTarget.current = '';
|
||||
const next = new URLSearchParams(params); next.delete('target');
|
||||
setParams(next, { replace: true });
|
||||
// Query params are consumed once; account changes use authoritative account state.
|
||||
// Consume deep-link selection once; live refreshes must not replace user input.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selected?.id]);
|
||||
}, [accounts, loading]);
|
||||
|
||||
const run = async (key: string, task: () => Promise<unknown>, success: string) => {
|
||||
if (busy) return null;
|
||||
setBusy(key); setError(''); setNotice('');
|
||||
const operationId = (key: string, input: unknown) => {
|
||||
const fingerprint = JSON.stringify(input);
|
||||
const old = intents.current.get(key);
|
||||
if (old?.fingerprint === fingerprint) return old.id;
|
||||
const id = crypto.randomUUID(); intents.current.set(key, { fingerprint, id }); return id;
|
||||
};
|
||||
const mergeAccount = (account: CloudUserChannelAccount) => {
|
||||
if (!alive.current) return;
|
||||
setAccounts(current => current.some(item => item.id === account.id)
|
||||
? current.map(item => item.id === account.id ? account : item) : [...current, account]);
|
||||
};
|
||||
const run = async <T,>(key: string, task: () => Promise<T>, success = ''): Promise<T | null> => {
|
||||
if (busyRef.current || recoveryBlocked) return null;
|
||||
busyRef.current = true; setBusy(key); setError(''); setNotice('');
|
||||
try {
|
||||
const result = await task();
|
||||
await refresh();
|
||||
intents.current.delete(key);
|
||||
if (alive.current) setNotice(success);
|
||||
// A successful mutation stays successful even if the following read fails.
|
||||
await refresh().catch(() => { if (alive.current) setError('操作已完成,列表刷新失败。请刷新查看最新状态。'); });
|
||||
return result;
|
||||
} catch (failure) {
|
||||
if (alive.current) setError(errorText(failure));
|
||||
await cloudAgentsApi.recovery().then(state => { if (alive.current) setPending(state.pending); }).catch(() => undefined);
|
||||
return null;
|
||||
} finally {
|
||||
if (alive.current) setBusy('');
|
||||
busyRef.current = false; if (alive.current) setBusy('');
|
||||
}
|
||||
};
|
||||
const create = async () => {
|
||||
if (!newName.trim()) return;
|
||||
const result = await run('create', () => cloudAgentsApi.call('createChannelAccount', {
|
||||
operation_id: operationId(), display_name: newName.trim(),
|
||||
}), '微信账号已添加,可以先扫码连接,再选择目标智能体。');
|
||||
const channel = (result as { channel?: CloudUserChannelAccount } | null)?.channel;
|
||||
if (channel) { setSelectedId(channel.id); setCreating(false); setNewName(''); }
|
||||
const resetConnection = () => { setQr(null); setQrAccountId(''); setConnectedAccountId(''); setVerifyCode(''); };
|
||||
const close = () => {
|
||||
if (busyRef.current) return;
|
||||
setFlow(null); setConfirmation(null); resetConnection(); setError(''); setNotice('');
|
||||
const next = new URLSearchParams(params); next.delete('account'); next.delete('target'); setParams(next, { replace: true });
|
||||
};
|
||||
const saveRoute = async () => {
|
||||
if (!selected) return;
|
||||
await run('route', () => cloudAgentsApi.call('routeChannelAccount', {
|
||||
channel_account_id: selected.id, operation_id: operationId(), target_agent_slug: targetSlug || null,
|
||||
expected_revision: selected.revision, enabled: targetSlug ? selected.enabled : false,
|
||||
}), targetSlug ? '目标智能体已更新,微信连接保持不变。' : '已取消目标智能体,微信连接保持不变。');
|
||||
};
|
||||
const setEnabled = async (enabled: boolean) => {
|
||||
if (!selected) return;
|
||||
const operation = enabled ? 'enableChannelAccount' : 'pauseChannelAccount';
|
||||
await run(operation, () => cloudAgentsApi.call(operation, {
|
||||
channel_account_id: selected.id, operation_id: operationId(), expected_revision: selected.revision,
|
||||
}), enabled ? '渠道已启用。' : '渠道已暂停,账号连接仍保留。');
|
||||
const openAccount = (account: CloudUserChannelAccount) => {
|
||||
resetConnection(); setError(''); setNotice('');
|
||||
setFlow({ mode: 'manage', step: 'choose', accountId: account.id, target: account.target_agent_slug ?? '', remark: account.display_name });
|
||||
const next = new URLSearchParams(params); next.set('account', account.id); next.delete('target'); setParams(next, { replace: true });
|
||||
};
|
||||
const applyBindState = useCallback((result: CloudWechatBindState, accountId: string) => {
|
||||
if (result.status === 'confirmed') {
|
||||
setQr(null); setQrAccountId(''); setVerifyCode(''); setNotice(''); setError('');
|
||||
setConnectedAccountId(accountId);
|
||||
} else {
|
||||
setQr(result); setQrAccountId(accountId); setConnectedAccountId('');
|
||||
}
|
||||
}, []);
|
||||
const startQr = async (force: boolean) => {
|
||||
if (!selected) return;
|
||||
setQr(null); setQrAccountId(''); setVerifyCode(''); setConnectedAccountId(accountId); setNotice(''); setError('');
|
||||
setAccounts(current => current.map(item => item.id === accountId ? { ...item, health: 'connected' } : item));
|
||||
void refresh().catch(() => { if (alive.current) setError('微信已确认连接,请刷新连接状态后完成绑定。'); });
|
||||
} else { setQr(result); setQrAccountId(accountId); setConnectedAccountId(''); }
|
||||
}, [refresh]);
|
||||
const startQr = async (account: CloudUserChannelAccount, force = false) => {
|
||||
setConnectedAccountId(''); setVerifyCode('');
|
||||
const input = { channel_account_id: account.id, force };
|
||||
const result = await run('qr', () => cloudAgentsApi.call('channelAccountWechatBindStart', {
|
||||
channel_account_id: selected.id, operation_id: operationId(), force,
|
||||
}), '请使用个人微信扫码。');
|
||||
if (result && typeof result === 'object' && 'session_key' in result) {
|
||||
applyBindState(result as CloudWechatBindState, selected.id);
|
||||
...input, operation_id: operationId('qr', input),
|
||||
}));
|
||||
if (result && alive.current) applyBindState(result, account.id);
|
||||
};
|
||||
const route = async (account: CloudUserChannelAccount, target: string) => {
|
||||
const input = { channel_account_id: account.id, target_agent_slug: target || null, expected_revision: account.revision, enabled: target ? account.enabled : false };
|
||||
const result = await run('route', () => cloudAgentsApi.call('routeChannelAccount', { ...input, operation_id: operationId('route', input) }));
|
||||
if (result) mergeAccount(result.channel);
|
||||
if (result && result.channel.target_agent_slug !== input.target_agent_slug) { setError('绑定还没有完成,请刷新状态后再试。'); return null; }
|
||||
return result?.channel ?? null;
|
||||
};
|
||||
const next = async () => {
|
||||
if (!flow || !targetAgent || disabled) return;
|
||||
if (flow.accountId && !selected) { setError('这个微信绑定已发生变化,请关闭窗口并刷新列表。'); return; }
|
||||
let account = selected;
|
||||
if (!account) {
|
||||
const input = { display_name: flow.remark.trim() || `微信 ${accounts.length + 1}` };
|
||||
const result = await run('create', () => cloudAgentsApi.call('createChannelAccount', { ...input, operation_id: operationId('create', input) }));
|
||||
if (!result || !alive.current) return;
|
||||
account = result.channel; mergeAccount(account);
|
||||
const createdId = account.id;
|
||||
setFlow(current => current ? { ...current, accountId: createdId } : null);
|
||||
}
|
||||
if (account.target_agent_slug !== flow.target) {
|
||||
account = await route(account, flow.target);
|
||||
if (!account || !alive.current) return;
|
||||
}
|
||||
const connectedId = account.id;
|
||||
setFlow(current => current ? { ...current, step: 'connect', accountId: connectedId } : null);
|
||||
if (account.health !== 'connected') await startQr(account);
|
||||
};
|
||||
const verify = async () => {
|
||||
if (!selected || !qr || qrAccountId !== selected.id || !verifyCode.trim()) return;
|
||||
const result = await run('verify', () => cloudAgentsApi.call('channelAccountWechatBindVerification', {
|
||||
channel_account_id: selected.id, operation_id: operationId(), session_key: qr.session_key, verify_code: verifyCode.trim(),
|
||||
}), '验证码已提交,请等待微信确认。');
|
||||
if (result && typeof result === 'object' && 'session_key' in result) applyBindState(result as CloudWechatBindState, selected.id);
|
||||
const setEnabled = async (account: CloudUserChannelAccount, enabled: boolean) => {
|
||||
const key = enabled ? 'enable' : 'pause';
|
||||
const input = { channel_account_id: account.id, expected_revision: account.revision };
|
||||
const result = await run(key, () => enabled
|
||||
? cloudAgentsApi.call('enableChannelAccount', { ...input, operation_id: operationId(key, input) })
|
||||
: cloudAgentsApi.call('pauseChannelAccount', { ...input, operation_id: operationId(key, input) }));
|
||||
if (result) {
|
||||
mergeAccount(result.channel);
|
||||
if (result.channel.enabled === enabled) setNotice(enabled ? '微信绑定已启用。' : '已暂停,微信连接和绑定的智能体仍保留。');
|
||||
else setError('使用状态还没有确认,请刷新后再试。');
|
||||
}
|
||||
return result?.channel ?? null;
|
||||
};
|
||||
const pollingAccountId = selected?.id ?? '';
|
||||
const pollingSessionKey = qr?.session_key ?? '';
|
||||
const completeBinding = async () => {
|
||||
if (!selected || !selected.target_agent_slug) return;
|
||||
const page = await run('ready', () => cloudAgentsApi.call('channelAccounts', {}));
|
||||
if (!page) return;
|
||||
const fresh = page.items.find(item => item.id === selected.id);
|
||||
if (!fresh || fresh.target_agent_slug !== flow?.target) { setError('绑定的智能体已发生变化,请返回上一步确认。'); return; }
|
||||
mergeAccount(fresh);
|
||||
if (fresh.health !== 'connected' || fresh.blockers.length) { setError('微信还没有准备好,请刷新连接状态后再试。'); return; }
|
||||
const account = fresh.enabled ? fresh : await setEnabled(fresh, true);
|
||||
if (account?.enabled && account.health === 'connected' && !account.blockers.length) setFlow(current => current ? { ...current, step: 'result' } : null);
|
||||
else if (account) setError('微信还没有准备好,请刷新连接状态后再试。');
|
||||
};
|
||||
const confirm = async () => {
|
||||
if (!selected || !flow) return;
|
||||
if (confirmation === 'switch' || confirmation === 'unlink') {
|
||||
const target = confirmation === 'unlink' ? '' : flow.target;
|
||||
const account = await route(selected, target);
|
||||
if (!account) return;
|
||||
setFlow(current => current ? { ...current, target } : null);
|
||||
setNotice(target ? '已更换绑定的智能体,微信连接保留。' : '已解除智能体绑定,微信连接保留。');
|
||||
} else if (confirmation === 'disconnect') {
|
||||
const input = { channel_account_id: selected.id, expected_revision: selected.revision };
|
||||
const result = await run('disconnect', () => cloudAgentsApi.call('disconnectChannelAccount', { ...input, operation_id: operationId('disconnect', input) }), '微信连接已断开,再使用时需要扫码。');
|
||||
if (!result) return;
|
||||
mergeAccount(result.channel); resetConnection();
|
||||
}
|
||||
setConfirmation(null);
|
||||
};
|
||||
const pollingId = flow?.step === 'connect' ? flow.accountId : '';
|
||||
const pollingSession = qr?.session_key ?? '';
|
||||
const pollingStatus = qr?.status ?? '';
|
||||
useEffect(() => {
|
||||
if (!pollingAccountId || !pollingSessionKey || qrAccountId !== pollingAccountId || ['confirmed', 'failed', 'expired'].includes(pollingStatus)) return;
|
||||
if (!pollingId || pollingId !== qrAccountId || !pollingSession || ['confirmed', 'failed', 'expired'].includes(pollingStatus)) return;
|
||||
let stopped = false;
|
||||
const timer = window.setInterval(() => {
|
||||
if (document.visibilityState === 'hidden') return;
|
||||
void cloudAgentsApi.call('channelAccountWechatBindStatus', { channel_account_id: pollingAccountId, session_key: pollingSessionKey })
|
||||
.then(result => {
|
||||
if (stopped || !alive.current) return;
|
||||
applyBindState(result, pollingAccountId);
|
||||
if (result.status === 'confirmed') void refresh().catch(failure => { if (alive.current) setError(errorText(failure)); });
|
||||
}).catch(failure => { if (!stopped && alive.current) setError(errorText(failure)); });
|
||||
void cloudAgentsApi.call('channelAccountWechatBindStatus', { channel_account_id: pollingId, session_key: pollingSession })
|
||||
.then(result => { if (!stopped && alive.current) applyBindState(result, pollingId); })
|
||||
.catch(failure => { if (!stopped && alive.current) setError(errorText(failure)); });
|
||||
}, 2000);
|
||||
return () => { stopped = true; window.clearInterval(timer); };
|
||||
}, [pollingAccountId, pollingSessionKey, pollingStatus, qrAccountId, refresh, applyBindState]);
|
||||
const disconnect = async () => {
|
||||
if (!selected) return;
|
||||
await run('disconnect', () => cloudAgentsApi.call('disconnectChannelAccount', {
|
||||
channel_account_id: selected.id, operation_id: operationId(), expected_revision: selected.revision,
|
||||
}), '微信账号已断开。');
|
||||
setConfirmDisconnect(false); setQr(null); setConnectedAccountId('');
|
||||
}, [pollingId, pollingSession, pollingStatus, qrAccountId, applyBindState]);
|
||||
const verify = async () => {
|
||||
if (!selected || !qr || !verifyCode.trim()) return;
|
||||
const input = { channel_account_id: selected.id, session_key: qr.session_key, verify_code: verifyCode.trim() };
|
||||
const result = await run('verify', () => cloudAgentsApi.call('channelAccountWechatBindVerification', { ...input, operation_id: operationId('verify', input) }));
|
||||
if (result) applyBindState(result, selected.id);
|
||||
};
|
||||
const connectionReady = selected?.health === 'connected' || selected?.id === connectedAccountId;
|
||||
const selectedState = selected ? accountState(selected) : null;
|
||||
const canActivate = connectionReady && Boolean(selected?.target_agent_slug) && !selected?.blockers.some(item => item !== 'login_required');
|
||||
const messages = <>
|
||||
{error && <p role="alert" className="rounded-2xl border border-destructive/20 bg-destructive/5 px-4 py-3 text-sm text-destructive">{error}</p>}
|
||||
{notice && <p role="status" className="rounded-2xl bg-[#e7f4ec] px-4 py-3 text-sm text-[#3f7458]">{notice}</p>}
|
||||
</>;
|
||||
const options = <>
|
||||
<option value="">选择一个智能体</option>
|
||||
{flow?.target && !targetAgent && selected?.target_agent_slug === flow.target && <option value={flow.target} disabled>{selected.target_agent_name ?? '原智能体'}(暂不可用)</option>}
|
||||
{agents.map(agent => <option key={agent.slug} value={agent.slug}>{agent.name}</option>)}
|
||||
</>;
|
||||
|
||||
return <section className="h-full overflow-auto p-4 sm:p-6" data-testid="channel-accounts-page">
|
||||
<CloudRecovery pending={pending} onResolved={() => void refresh()} />
|
||||
<header className="mb-5 flex flex-wrap items-start justify-between gap-3 border-b border-border pb-5">
|
||||
<div><p className="text-sm text-muted-foreground">渠道</p><h1 className="mt-1 text-2xl font-semibold">微信</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">每个微信账号独立连接,可随时选择或更换它服务的智能体。</p></div>
|
||||
<Button onClick={() => setCreating(true)}><Plus className="mr-2 h-4 w-4" />添加微信账号</Button>
|
||||
</header>
|
||||
{creating && <div className="mb-5 flex flex-wrap items-end gap-3 rounded-xl border border-border bg-background p-4">
|
||||
<label className="min-w-56 flex-1 space-y-2 text-sm"><span>账号备注</span><Input autoFocus aria-label="账号备注" value={newName} maxLength={100} onChange={event => setNewName(event.target.value)} placeholder="例如:工作微信" /></label>
|
||||
<Button disabled={Boolean(busy) || !newName.trim()} onClick={() => void create()}>{busy === 'create' ? '添加中…' : '添加'}</Button>
|
||||
<Button variant="ghost" disabled={Boolean(busy)} onClick={() => { setCreating(false); setNewName(''); }}>取消</Button>
|
||||
</div>}
|
||||
{error && <div role="alert" className="mb-4 flex items-center justify-between gap-3 rounded-lg bg-destructive/10 p-3 text-sm text-destructive"><span>{error}</span><Button variant="ghost" size="sm" onClick={() => void refresh()}>刷新</Button></div>}
|
||||
{notice && <p role="status" className="mb-4 rounded-lg bg-emerald-50 p-3 text-sm text-emerald-800">{notice}</p>}
|
||||
{!accounts.length ? <div className="rounded-xl border border-dashed border-border p-10 text-center"><MessageCircle className="mx-auto h-8 w-8 text-brand" />
|
||||
<h2 className="mt-4 font-medium">还没有微信账号</h2><p className="mt-2 text-sm text-muted-foreground">即使还没有发布智能体,也可以先添加账号并扫码连接。</p>
|
||||
<Button variant="outline" className="mt-5" onClick={() => setCreating(true)}>添加第一个账号</Button></div> : <div className="grid gap-5 lg:grid-cols-[minmax(15rem,20rem)_minmax(0,1fr)]">
|
||||
<aside aria-label="微信账号" className="space-y-2">{accounts.map(account => <button key={account.id} type="button" onClick={() => setSelectedId(account.id)}
|
||||
className={`w-full rounded-xl border p-4 text-left transition-colors ${selected?.id === account.id ? 'border-brand/40 bg-brand-soft' : 'border-border bg-background hover:bg-muted/30'}`}>
|
||||
<span className="flex items-center justify-between gap-2"><strong className="truncate text-sm">{account.display_name}</strong><span className="shrink-0 text-xs text-muted-foreground">{label(account.health)}</span></span>
|
||||
<span className="mt-2 block truncate text-xs text-muted-foreground">{account.target_agent_name ?? '未分配智能体'}</span>
|
||||
<span className="mt-3 block text-xs">{account.enabled ? '已启用' : '已暂停'}</span>
|
||||
</button>)}</aside>
|
||||
{selected && <article aria-label={`${selected.display_name}账号详情`} className="min-w-0 space-y-5 rounded-xl border border-border bg-background p-4 sm:p-5">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3"><div><h2 className="text-lg font-semibold">{selected.display_name}</h2><p className="mt-1 text-sm text-muted-foreground">微信连接:{label(selected.health)} · 渠道路由:{selected.enabled ? '已启用' : '已暂停'}</p></div>
|
||||
<Button variant="ghost" size="sm" disabled={Boolean(busy)} onClick={() => void refresh()}><RefreshCw className="mr-2 h-4 w-4" />刷新</Button></div>
|
||||
{selected.blockers.length > 0 && <p className="rounded-lg bg-amber-50 p-3 text-sm text-amber-900">{selected.blockers.map(item => blockerText[item] ?? item).join(';')}</p>}
|
||||
<div className="grid gap-4 rounded-xl bg-muted/30 p-4 md:grid-cols-[minmax(0,1fr)_auto] md:items-end">
|
||||
<label className="space-y-2 text-sm"><span className="font-medium">目标智能体</span><Select aria-label="目标智能体" value={targetSlug} onChange={event => setTargetSlug(event.target.value)}>
|
||||
<option value="">暂不分配</option>{agents.map(agent => <option key={agent.slug} value={agent.slug}>{agent.name} · v{agent.published_version}</option>)}</Select></label>
|
||||
<Button variant="outline" disabled={Boolean(busy) || targetSlug === (selected.target_agent_slug ?? '')} onClick={() => void saveRoute()}>{busy === 'route' ? '保存中…' : '保存目标'}</Button>
|
||||
return <section className="h-full overflow-auto bg-[#fffcf7] p-4 sm:p-7 lg:p-9" data-testid="channel-accounts-page">
|
||||
<div className="mx-auto max-w-6xl space-y-6">
|
||||
<CloudRecovery pending={pending} onResolved={() => void refresh().catch(failure => setError(errorText(failure)))} />
|
||||
<header className="flex flex-wrap items-center justify-between gap-5 py-2">
|
||||
<div className="flex min-w-0 items-center gap-4">
|
||||
<span className="flex h-16 w-16 shrink-0 items-center justify-center rounded-[22px] border border-[#d8e9da] bg-[#eaf5ec]"><img src={wechatMark} alt="" className="h-9 w-9" /></span>
|
||||
<div><h1 className="text-2xl font-bold tracking-tight text-[#303747] sm:text-3xl">连接到微信</h1><p className="mt-2 text-sm leading-6 text-[#677286]">选一个智能体,把它带到微信里。</p></div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selected.health !== 'connected' ? <Button disabled={Boolean(busy)} onClick={() => void startQr(false)}><QrCode className="mr-2 h-4 w-4" />扫码连接</Button>
|
||||
: <Button variant="outline" disabled={Boolean(busy)} onClick={() => void startQr(true)}><RefreshCw className="mr-2 h-4 w-4" />重新连接</Button>}
|
||||
{selected.enabled ? <Button variant="outline" disabled={Boolean(busy)} onClick={() => void setEnabled(false)}><Pause className="mr-2 h-4 w-4" />暂停</Button>
|
||||
: <Button disabled={Boolean(busy) || selected.health !== 'connected' || !selected.target_agent_slug} onClick={() => void setEnabled(true)}><Play className="mr-2 h-4 w-4" />启用</Button>}
|
||||
<Button variant="ghost" className="text-destructive" disabled={Boolean(busy)} onClick={() => setConfirmDisconnect(true)}><Unplug className="mr-2 h-4 w-4" />断开连接</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="sm" aria-label="刷新微信列表" disabled={Boolean(busy)} onClick={() => void refresh().catch(failure => setError(errorText(failure)))}><RefreshCw className="h-4 w-4" /></Button>
|
||||
<Button className="h-11 rounded-2xl px-5" disabled={loading || !loaded || disabled} onClick={() => { resetConnection(); setError(''); setNotice(''); setFlow({ mode: 'new', step: 'choose', accountId: '', target: '', remark: '' }); }}><Plus className="mr-2 h-4 w-4" />添加微信</Button>
|
||||
</div>
|
||||
{connectedAccountId === selected.id && <div role="status" className="flex items-start gap-3 rounded-xl bg-emerald-50 p-4 text-emerald-800">
|
||||
<CheckCircle2 className="mt-0.5 h-5 w-5 shrink-0" aria-hidden="true" />
|
||||
<div><p className="font-medium">微信已连接</p><p className="mt-1 text-sm">
|
||||
{!selected.target_agent_slug ? '请选择目标智能体并保存,再启用渠道开始对话。'
|
||||
: !selected.enabled ? '点击“启用”开启渠道,之后就可以在微信中与智能体对话。'
|
||||
: '连接已完成,可以返回微信与智能体对话。'}
|
||||
</p></div>
|
||||
</header>
|
||||
{!flow && messages}
|
||||
{loading ? <div role="status" className="flex items-center justify-center gap-3 rounded-3xl bg-[#f4f1fb] p-12 text-[#75618e]"><Loader2 className="h-5 w-5 animate-spin" />正在查看你的微信绑定…</div>
|
||||
: !loaded ? <div className="rounded-3xl border border-[#e4e2de] bg-white p-10 text-center text-sm text-[#677286]"><p>暂时没能读到微信绑定,请刷新后再试。</p><Button variant="outline" className="mt-5 rounded-xl" onClick={() => void refresh().catch(failure => setError(errorText(failure)))}>重新加载</Button></div>
|
||||
: !accounts.length ? <div className="rounded-[28px] border border-[#dfe9e2] bg-[#f1f8f2] px-6 py-14 text-center">
|
||||
<Link2 className="mx-auto h-10 w-10 text-[#678b70]" /><h2 className="mt-5 text-xl font-bold text-[#303747]">把你的智能体带到微信吧</h2><p className="mx-auto mt-3 max-w-md text-sm leading-7 text-[#677286]">绑定后,就可以回到微信和它聊聊。每个微信绑定一个智能体,也可以把多个微信交给同一个智能体。</p>
|
||||
<Button className="mt-7 h-11 rounded-2xl px-6" onClick={() => { setError(''); setFlow({ mode: 'new', step: 'choose', accountId: '', target: '', remark: '' }); }}>添加第一个微信<ArrowRight className="ml-2 h-4 w-4" /></Button>
|
||||
</div> : <div className="grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3" aria-label="我的微信绑定">
|
||||
{accounts.map(account => {
|
||||
const agent = agents.find(item => item.slug === account.target_agent_slug);
|
||||
const state = accountState(account);
|
||||
return <article key={account.id} aria-label={`${account.display_name}微信`} className="flex min-w-0 flex-col rounded-[26px] border border-[#e4e2de] bg-white p-5 shadow-[0_3px_0_0_#f1eeea] sm:p-6">
|
||||
<div className="flex items-start justify-between gap-3"><div className="min-w-0"><p className="text-xs font-medium text-[#788271]">微信</p><h2 className="mt-1 break-words text-lg font-bold text-[#303747]">{account.display_name}</h2></div><span className={`shrink-0 rounded-full px-3 py-1.5 text-xs font-semibold ${state.tone}`}>{state.text}</span></div>
|
||||
<div className="my-5 flex min-h-20 items-center gap-3 rounded-2xl border border-[#e4def0] bg-[#f7f3fc] p-4">
|
||||
{account.target_agent_slug ? <><CloudAvatar src={agent?.icon_url} name={account.target_agent_name ?? agent?.name ?? '智能体'} className="h-11 w-11" /><div className="min-w-0"><p className="text-xs text-[#7c6d92]">绑定的智能体</p><p className="mt-1 break-words text-sm font-semibold text-[#3c3549]">{account.target_agent_name ?? agent?.name ?? '智能体暂不可用'}</p></div></>
|
||||
: <><Link2 className="h-7 w-7 shrink-0 text-[#9683b3]" /><p className="text-sm text-[#7c6d92]">还没有绑定智能体</p></>}
|
||||
</div>
|
||||
<Button variant="outline" className="mt-auto h-10 w-full rounded-xl" disabled={disabled} onClick={() => openAccount(account)}>{account.target_agent_slug ? '管理绑定' : '继续绑定'}<ArrowRight className="ml-2 h-4 w-4" /></Button>
|
||||
</article>;
|
||||
})}
|
||||
</div>}
|
||||
{qr && qrAccountId === selected.id && <div className="flex flex-wrap gap-5 rounded-xl border border-border p-4" aria-label="微信扫码连接">
|
||||
<div className="flex h-48 w-48 items-center justify-center rounded-lg bg-white p-3">{qr.qrcode_url ? <CloudWechatQr value={qr.qrcode_url} /> : <Loader2 className="h-7 w-7 animate-spin" />}</div>
|
||||
<div className="min-w-52 flex-1 space-y-3"><div className="flex items-start justify-between"><div><p className="font-medium">{label(qr.status)}</p><p className="mt-1 text-xs text-muted-foreground">扫码只负责账号连接,目标智能体可稍后选择。</p>{qr.message && <p className="mt-1 text-xs text-muted-foreground">{qr.message}</p>}</div>
|
||||
<Button aria-label="关闭二维码" variant="ghost" size="sm" onClick={() => setQr(null)}><X className="h-4 w-4" /></Button></div>
|
||||
{qr.status === 'verification_required' && <><Input aria-label="微信验证码" value={verifyCode} onChange={event => setVerifyCode(event.target.value)} placeholder="输入微信验证码" /><Button disabled={Boolean(busy) || !verifyCode.trim()} onClick={() => void verify()}>提交验证码</Button></>}
|
||||
</div></div>}
|
||||
{confirmDisconnect && <div role="dialog" aria-label="断开微信连接" className="rounded-xl bg-destructive/10 p-4 text-sm"><p className="font-medium">确认断开这个微信账号?</p><p className="mt-2 text-muted-foreground">历史活动和会话仍保留;再次使用需要重新扫码。</p><div className="mt-3 flex gap-2"><Button variant="destructive" disabled={Boolean(busy)} onClick={() => void disconnect()}>确认断开</Button><Button variant="ghost" onClick={() => setConfirmDisconnect(false)}>取消</Button></div></div>}
|
||||
{selected.target_agent_slug && selected.binding_id ? <div className="border-t border-border pt-5"><CloudChannelPanel key={selected.id} slug={selected.target_agent_slug} accountId={selected.id} /></div>
|
||||
: <div className="rounded-xl border border-dashed border-border p-5 text-sm text-muted-foreground"><p>选择已发布的目标智能体后,可在这里查看活动记录和微信对话。</p>
|
||||
<Button variant="ghost" className="mt-2 px-0" onClick={() => navigate('/cloud-agents')}><ArrowLeft className="mr-2 h-4 w-4" />前往我的智能体</Button></div>}
|
||||
</article>}
|
||||
</div>}
|
||||
</div>
|
||||
<Dialog open={Boolean(flow)} onOpenChange={open => { if (!open) close(); }}>
|
||||
<DialogContent className="max-h-[calc(100dvh-2rem)] max-w-xl gap-5 overflow-y-auto rounded-[28px] border-[#e5e0d9] bg-[#fffcf7] p-5 sm:p-7" onInteractOutside={event => event.preventDefault()} onEscapeKeyDown={event => { if (busy) event.preventDefault(); }}>
|
||||
<DialogHeader><DialogTitle className="text-xl font-bold">{flow?.mode === 'new' ? '添加微信' : '管理微信绑定'}</DialogTitle><DialogDescription>{flow?.mode === 'new' ? '给微信选一个智能体,再扫码连接。' : selected?.display_name ?? '微信绑定'}</DialogDescription></DialogHeader>
|
||||
{flow && <>
|
||||
{(flow.mode === 'new' || flow.step !== 'choose') && <ol aria-label="绑定步骤" className="grid grid-cols-3 gap-2">{['选择智能体', '扫码连接', '完成绑定'].map((text, index) => {
|
||||
const current = ['choose', 'connect', 'result'].indexOf(flow.step);
|
||||
return <li key={text} aria-current={current === index ? 'step' : undefined} className={`flex flex-col items-center gap-2 rounded-2xl px-2 py-3 text-center text-xs ${current === index ? 'bg-[#f0eafa] font-semibold text-[#755991]' : 'text-[#7b8290]'}`}><span className={`flex h-6 w-6 items-center justify-center rounded-full ${index <= current ? 'bg-[#8468a5] text-white' : 'bg-[#efede9]'}`}>{index < current ? <Check className="h-3.5 w-3.5" /> : index + 1}</span>{text}</li>;
|
||||
})}</ol>}
|
||||
{messages}
|
||||
{recoveryBlocked && <p className="rounded-2xl bg-[#fff0d8] p-3 text-sm text-[#876033]">有一次绑定操作的结果需要确认,请先关闭此窗口并处理页面上的恢复提醒。</p>}
|
||||
{flow.step === 'choose' && <>
|
||||
{flow.mode === 'manage' && selectedState && <div className="flex items-center justify-between rounded-2xl bg-[#eaf5ec] p-4 text-sm"><span>微信连接与绑定</span><span className={`rounded-full px-3 py-1.5 text-xs font-semibold ${selectedState.tone}`}>{selectedState.text}</span></div>}
|
||||
{!agents.length ? <div className="rounded-2xl border border-[#e5def0] bg-[#f6f2fb] p-5 text-sm leading-7 text-[#75618e]"><p>先准备一个可以使用的智能体,再把它带到微信里。</p><Button variant="outline" className="mt-4 rounded-xl" onClick={() => navigate('/cloud-agents?view=mine')}>去我的智能体<ArrowRight className="ml-2 h-4 w-4" /></Button></div>
|
||||
: <label className="space-y-2 text-sm font-medium text-[#3c3549]"><span>{flow.mode === 'new' ? '选择智能体' : '绑定的智能体'}</span><Select aria-label={flow.mode === 'new' ? '选择智能体' : '绑定的智能体'} className="h-12 rounded-xl bg-white" style={{ backgroundPosition: 'right 12px center', backgroundSize: '16px 16px', backgroundRepeat: 'no-repeat' }} value={flow.target} disabled={disabled} onChange={event => setFlow({ ...flow, target: event.target.value })}>{options}</Select></label>}
|
||||
{flow.mode === 'new' && !flow.accountId && <label className="space-y-2 text-sm font-medium text-[#3c3549]"><span>微信备注(可选)</span><Input aria-label="微信备注(可选)" className="h-12 rounded-xl bg-white" disabled={disabled} value={flow.remark} maxLength={100} onChange={event => setFlow({ ...flow, remark: event.target.value })} placeholder="给这个微信起个方便辨认的名字" /></label>}
|
||||
{flow.mode === 'manage' && selected?.blockers.length ? <p className="rounded-2xl bg-[#fff0d8] p-3 text-sm leading-6 text-[#876033]">{selected.blockers.map(item => blockerText[item] ?? '连接暂时不可用,请刷新后重试').join(';')}</p> : null}
|
||||
{flow.mode === 'new' || !selected?.target_agent_slug ? <Button className="h-12 rounded-2xl" disabled={disabled || !targetAgent} onClick={() => void next()}>{busy ? '正在准备…' : '下一步:扫码连接'}<ArrowRight className="ml-2 h-4 w-4" /></Button>
|
||||
: <div className="space-y-4">
|
||||
{flow.target !== selected.target_agent_slug && <Button className="h-11 w-full rounded-xl" disabled={disabled || !targetAgent} onClick={() => setConfirmation('switch')}>保存更换</Button>}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{!connectionReady && <Button className="rounded-xl" disabled={disabled} onClick={() => { setFlow({ ...flow, step: 'connect' }); void startQr(selected); }}><QrCode className="mr-2 h-4 w-4" />扫码连接</Button>}
|
||||
<Button variant="outline" className="rounded-xl" disabled={disabled || (!selected.enabled && (!connectionReady || !selected.target_agent_slug || selected.blockers.length > 0))} onClick={() => void setEnabled(selected, !selected.enabled)}>{selected.enabled ? <Pause className="mr-2 h-4 w-4" /> : <Play className="mr-2 h-4 w-4" />}{selected.enabled ? '暂停使用' : '恢复使用'}</Button>
|
||||
<Button variant="outline" className="rounded-xl" disabled={disabled} onClick={() => setConfirmation('unlink')}><Unplug className="mr-2 h-4 w-4" />解除绑定</Button>
|
||||
</div>
|
||||
<p className="text-xs leading-6 text-[#677286]">暂停会停止这个微信正在进行的任务,连接和绑定仍保留。</p>
|
||||
</div>}
|
||||
{flow.mode === 'manage' && selected && <Button variant="ghost" size="sm" className="justify-self-start text-[#89625b]" disabled={disabled} onClick={() => setConfirmation('disconnect')}>断开微信连接</Button>}
|
||||
<Button variant="ghost" disabled={Boolean(busy)} onClick={close}>关闭</Button>
|
||||
</>}
|
||||
{flow.step === 'connect' && selected && <>
|
||||
<div className="flex items-center gap-3 rounded-2xl border border-[#e5def0] bg-[#f6f2fb] p-4"><CloudAvatar src={targetAgent?.icon_url} name={targetAgent?.name ?? selected.target_agent_name ?? '智能体'} className="h-12 w-12" /><div className="min-w-0"><p className="text-xs text-[#7c6d92]">{selected.display_name}将使用</p><p className="mt-1 break-words text-sm font-semibold">{targetAgent?.name ?? selected.target_agent_name}</p></div></div>
|
||||
{connectionReady ? <div role="status" className="rounded-2xl bg-[#e7f4ec] p-5 text-center text-[#3f7458]"><CheckCircle2 className="mx-auto h-9 w-9" /><p className="mt-3 font-semibold">微信已连接</p><p className="mt-2 text-sm">完成绑定后,就可以回微信使用。</p><Button variant="ghost" size="sm" className="mt-3" disabled={disabled} onClick={() => void refresh().catch(failure => setError(errorText(failure)))}><RefreshCw className="mr-2 h-4 w-4" />刷新连接状态</Button></div>
|
||||
: <div className="rounded-2xl border border-[#dce8de] bg-white p-5 text-center">
|
||||
<div className="mx-auto flex min-h-48 w-48 items-center justify-center">{qr?.qrcode_url ? <CloudWechatQr value={qr.qrcode_url} /> : busy === 'qr' ? <Loader2 className="h-7 w-7 animate-spin text-[#678b70]" /> : <QrCode className="h-14 w-14 text-[#678b70]" />}</div>
|
||||
<p className="mt-3 text-sm font-semibold">{qr ? qrLabels[qr.status] ?? '请使用微信扫码' : '请使用微信扫码'}</p>
|
||||
{qr?.message && <p className="mt-2 text-xs leading-6 text-[#677286]">{qr.message}</p>}
|
||||
{(!qr || ['expired', 'failed'].includes(qr.status)) && <Button variant="outline" className="mt-4 rounded-xl" disabled={disabled} onClick={() => void startQr(selected, Boolean(qr))}><RefreshCw className="mr-2 h-4 w-4" />{qr ? '刷新二维码' : '获取二维码'}</Button>}
|
||||
</div>}
|
||||
{qr?.status === 'verification_required' && <div className="space-y-3"><label className="block space-y-2 text-sm"><span>微信验证码</span><Input aria-label="微信验证码" disabled={disabled} value={verifyCode} onChange={event => setVerifyCode(event.target.value)} placeholder="输入微信提示的验证码" /></label><Button className="w-full rounded-xl" disabled={disabled || !verifyCode.trim()} onClick={() => void verify()}>提交验证码</Button></div>}
|
||||
<Button className="h-12 rounded-2xl" disabled={disabled || !canActivate} onClick={() => void completeBinding()}>{busy === 'enable' || busy === 'ready' ? '正在完成…' : '完成绑定并启用'}</Button>
|
||||
<div className="flex items-center justify-between"><Button variant="ghost" disabled={Boolean(busy)} onClick={() => { resetConnection(); setFlow({ ...flow, step: 'choose' }); }}><ChevronLeft className="mr-1 h-4 w-4" />上一步</Button><Button variant="ghost" disabled={Boolean(busy)} onClick={close}>稍后继续</Button></div>
|
||||
</>}
|
||||
{flow.step === 'result' && selected && <>
|
||||
<div className="rounded-3xl border border-[#d8e9da] bg-[#edf7ef] p-7 text-center"><CheckCircle2 className="mx-auto h-12 w-12 text-[#548364]" /><h2 className="mt-4 text-xl font-bold text-[#365642]">绑定完成啦</h2><p className="mt-3 break-words text-sm leading-7 text-[#557062]">{selected.display_name}已绑定{selected.target_agent_name ?? targetAgent?.name ?? '智能体'},现在可以回微信和它聊聊。</p></div>
|
||||
<Button className="h-12 rounded-2xl" onClick={close}>完成</Button>
|
||||
</>}
|
||||
</>}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<ConfirmDialog open={Boolean(confirmation)} title={confirmation === 'switch' ? '更换绑定的智能体' : confirmation === 'unlink' ? '解除智能体绑定' : '断开微信连接'}
|
||||
message={confirmation === 'switch' ? `将这个微信改为使用“${targetAgent?.name ?? '新智能体'}”。正在进行的任务会停止,微信连接和历史仍保留。`
|
||||
: confirmation === 'unlink' ? '这个微信将不再使用当前智能体,正在进行的任务会停止。微信连接和历史仍保留。'
|
||||
: '正在进行的任务会停止,再使用时需要重新扫码。绑定的智能体和历史仍保留。'}
|
||||
confirmLabel={confirmation === 'switch' ? '确认更换' : confirmation === 'unlink' ? '确认解除' : '确认断开'} cancelLabel="取消"
|
||||
variant={confirmation === 'switch' ? 'default' : 'destructive'} onConfirm={confirm} onCancel={() => setConfirmation(null)} />
|
||||
</section>;
|
||||
}
|
||||
Reference in new issue
Block a user