Merge always-on healthy WeChat replies and device-only card removal
This commit is contained in:
commit
8d1a53a12c
5 files changed
+984
-182
No files matched your search
@@ -6,11 +6,10 @@ import {
|
||||
Link2,
|
||||
Loader2,
|
||||
MoreHorizontal,
|
||||
Pause,
|
||||
Play,
|
||||
Plus,
|
||||
QrCode,
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
Unplug,
|
||||
} from 'lucide-react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
@@ -25,6 +24,7 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { cloudAgentsApi } from '@/lib/cloud-agents-api';
|
||||
import { useAuthStore, type AuthUser } from '@/stores/auth';
|
||||
import type {
|
||||
CloudAgentDraft,
|
||||
CloudPendingOperation,
|
||||
@@ -36,13 +36,30 @@ import { CloudRecovery } from './CloudRecovery';
|
||||
import { CloudWechatQr } from './CloudWechatQr';
|
||||
|
||||
type Flow = {
|
||||
mode: 'new' | 'switch' | 'reconnect' | 'unbind';
|
||||
mode: 'new' | 'switch' | 'reconnect' | 'unbind' | 'delete';
|
||||
step: 'choose' | 'connect' | 'result';
|
||||
accountId: string;
|
||||
target: string;
|
||||
};
|
||||
type Expected = { target: string | null; enabled: boolean; disconnected?: boolean };
|
||||
const errorText = (error: unknown) => (error instanceof Error ? error.message : '操作没有完成,请重试');
|
||||
const isChannelManagementOperation = (operation: string) =>
|
||||
/channelaccount|channelbinding|^wechat/iu.test(operation);
|
||||
function cardPreferenceKey(user: AuthUser | null) {
|
||||
return user?.userId
|
||||
? 'makelore.wechat-removed-cards:' + JSON.stringify([user.tenantId, user.userId])
|
||||
: '';
|
||||
}
|
||||
function readRemovedCards(key: string): Record<string, number> {
|
||||
if (!key) return {};
|
||||
try {
|
||||
const value: unknown = JSON.parse(localStorage.getItem(key) ?? '{}');
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
||||
return Object.fromEntries(Object.entries(value).filter(([, revision]) =>
|
||||
typeof revision === 'number' && Number.isSafeInteger(revision) && revision >= 0
|
||||
));
|
||||
} catch { return {}; }
|
||||
}
|
||||
const usableBlockers = (account: CloudUserChannelAccount) =>
|
||||
account.blockers.filter((item) => item !== 'binding_paused');
|
||||
const matches = (account: CloudUserChannelAccount | undefined, expected: Expected) =>
|
||||
@@ -83,6 +100,9 @@ const unsettled = (account: CloudUserChannelAccount) =>
|
||||
account.blockers.includes('channel_sync_degraded') ||
|
||||
(['paused', 'disabled'].includes(account.desired_state ?? '') && account.enabled) ||
|
||||
(account.desired_state === 'enabled' && !account.enabled);
|
||||
const canRemoveCard = (account: CloudUserChannelAccount) =>
|
||||
!account.target_agent_slug && !account.enabled && account.health === 'login_required' &&
|
||||
account.sync_state === 'ready' && !unsettled(account);
|
||||
function accountState(account: CloudUserChannelAccount, uncertain: boolean, targetDisabled = false) {
|
||||
if (uncertain || unsettled(account)) return { text: '状态待确认', tone: 'bg-[#fff0d8] text-[#876033]' };
|
||||
if (!account.target_agent_slug) return { text: '未绑定', tone: 'bg-[#f0edf8] text-[#75618e]' };
|
||||
@@ -95,7 +115,7 @@ function accountState(account: CloudUserChannelAccount, uncertain: boolean, targ
|
||||
return { text: '状态待确认', tone: 'bg-[#fff0d8] text-[#876033]' };
|
||||
return account.enabled
|
||||
? { text: '使用中', tone: 'bg-[#e7f4ec] text-[#3f7458]' }
|
||||
: { text: '已暂停', tone: 'bg-[#f0edf8] text-[#75618e]' };
|
||||
: { text: '绑定待完成', tone: 'bg-[#f0edf8] text-[#75618e]' };
|
||||
}
|
||||
function bindingReadinessMessage(account: CloudUserChannelAccount) {
|
||||
if (unsettled(account)) return '微信使用状态尚未确认,请刷新状态后再继续。';
|
||||
@@ -106,6 +126,20 @@ function bindingReadinessMessage(account: CloudUserChannelAccount) {
|
||||
? blockerMessage(blocker, account)
|
||||
: '暂时无法完成微信绑定,请检查连接后再继续。';
|
||||
}
|
||||
function canRestoreReplies(account: CloudUserChannelAccount, agents: CloudAgentDraft[]) {
|
||||
const target = agents.find((agent) => agent.slug === account.target_agent_slug);
|
||||
return Boolean(
|
||||
!account.enabled &&
|
||||
account.health === 'connected' &&
|
||||
account.worker_online === true &&
|
||||
account.sync_state === 'ready' &&
|
||||
!unsettled(account) &&
|
||||
!usableBlockers(account).length &&
|
||||
target?.published_version &&
|
||||
target.enabled !== false &&
|
||||
target.archived !== true
|
||||
);
|
||||
}
|
||||
async function loadAgents(): Promise<CloudAgentDraft[]> {
|
||||
const items: CloudAgentDraft[] = [];
|
||||
let cursor: string | null = null;
|
||||
@@ -119,9 +153,21 @@ async function loadAgents(): Promise<CloudAgentDraft[]> {
|
||||
|
||||
export function ChannelAccounts() {
|
||||
const navigate = useNavigate();
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const cardStorageKey = cardPreferenceKey(user);
|
||||
const [removedCards, setRemovedCards] = useState(() => ({
|
||||
key: cardStorageKey, revisions: readRemovedCards(cardStorageKey),
|
||||
}));
|
||||
useEffect(() => {
|
||||
setRemovedCards({ key: cardStorageKey, revisions: readRemovedCards(cardStorageKey) });
|
||||
}, [cardStorageKey]);
|
||||
const [params, setParams] = useSearchParams();
|
||||
const initial = useRef({ account: params.get('account') ?? '', target: params.get('target') ?? '' });
|
||||
const [accounts, setAccounts] = useState<CloudUserChannelAccount[]>([]);
|
||||
const visibleAccounts = accounts.filter((account) =>
|
||||
removedCards.key !== cardStorageKey ||
|
||||
removedCards.revisions[account.id] !== account.revision || !canRemoveCard(account)
|
||||
);
|
||||
const [agents, setAgents] = useState<CloudAgentDraft[]>([]);
|
||||
const agentsRef = useRef<CloudAgentDraft[]>([]);
|
||||
const availableAgents = agents.filter(
|
||||
@@ -152,25 +198,30 @@ export function ChannelAccounts() {
|
||||
const readGeneration = useRef(0);
|
||||
const ownerPending = useRef(false);
|
||||
const refreshGeneration = useRef(0);
|
||||
const replyRestoreAttempts = useRef(new Set<string>());
|
||||
const replyRestorePreflightSkipped = useRef(new Set<string>());
|
||||
const replyRestoreStopped = useRef(false);
|
||||
const replyPolling = useRef(false);
|
||||
const [restoringReplies, setRestoringReplies] = useState(false);
|
||||
const [checkingIds, setCheckingIds] = useState<string[]>([]);
|
||||
const selected = accounts.find((account) => account.id === flow?.accountId);
|
||||
const recoveryBlocked = pending.some((item) => /channelaccount/iu.test(item.operation));
|
||||
const recoveryBlocked = pending.some((item) => isChannelManagementOperation(item.operation));
|
||||
const ownerLogoutPending = accounts.some((account) =>
|
||||
account.blockers.includes('wechat_disconnect_pending')
|
||||
);
|
||||
const disabled = Boolean(busy) || recoveryBlocked || !recoveryLoaded || ownerLogoutPending;
|
||||
const disabled = Boolean(busy) || restoringReplies || recoveryBlocked || !recoveryLoaded || ownerLogoutPending;
|
||||
const pendingFor = (id: string) =>
|
||||
pending.some(
|
||||
(item) => /channelaccount/iu.test(item.operation) && item.input.channel_account_id === id
|
||||
(item) => isChannelManagementOperation(item.operation) && item.input.channel_account_id === id
|
||||
);
|
||||
const updateFlow = (next: Flow | null) => {
|
||||
flowRef.current = next;
|
||||
setFlow(next);
|
||||
};
|
||||
const setCheck = (id: string, expected: Expected) => {
|
||||
const setCheck = useCallback((id: string, expected: Expected) => {
|
||||
checks.current.set(id, expected);
|
||||
setCheckingIds([...checks.current.keys()]);
|
||||
};
|
||||
}, []);
|
||||
const readAccounts = useCallback(async () => {
|
||||
const generation = ++readGeneration.current;
|
||||
const page = await cloudAgentsApi.call('channelAccounts', {});
|
||||
@@ -192,7 +243,12 @@ export function ChannelAccounts() {
|
||||
}
|
||||
return page.items;
|
||||
}, []);
|
||||
const refresh = useCallback(async () => {
|
||||
const refresh = useCallback(async (retryReplyRestore = false) => {
|
||||
if (!busyRef.current) replyRestorePreflightSkipped.current.clear();
|
||||
if (retryReplyRestore && !busyRef.current) {
|
||||
replyRestoreStopped.current = false;
|
||||
replyRestoreAttempts.current.clear();
|
||||
}
|
||||
const generation = ++refreshGeneration.current;
|
||||
const [accountResult, agentResult, recoveryResult] = await Promise.allSettled([
|
||||
readAccounts(),
|
||||
@@ -245,15 +301,15 @@ export function ChannelAccounts() {
|
||||
// Consume the deep link once; refresh must preserve the user's selection.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [accounts, loading]);
|
||||
const operationId = (key: string, input: unknown) => {
|
||||
const operationId = useCallback((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 run = async <T,>(key: string, accountId: string, task: () => Promise<T>): Promise<T | null> => {
|
||||
}, []);
|
||||
const run = useCallback(async <T,>(key: string, accountId: string, task: () => Promise<T>): Promise<T | null> => {
|
||||
if (busyRef.current || recoveryBlocked || !recoveryLoaded || ownerPending.current) return null;
|
||||
busyRef.current = true;
|
||||
const generation = ++refreshGeneration.current;
|
||||
@@ -293,7 +349,7 @@ export function ChannelAccounts() {
|
||||
setBusyAccount('');
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [readAccounts, recoveryBlocked, recoveryLoaded]);
|
||||
const resetConnection = () => {
|
||||
setQr(null);
|
||||
setQrAccountId('');
|
||||
@@ -331,25 +387,25 @@ export function ChannelAccounts() {
|
||||
setParams(next, { replace: true });
|
||||
if (mode === 'reconnect') void startQr(account);
|
||||
};
|
||||
const verifyFinal = async (id: string, expected: Expected) => {
|
||||
const verifyFinal = useCallback(async (id: string, expected: Expected) => {
|
||||
setCheck(id, expected);
|
||||
const fresh = (await readAccounts()).find((item) => item.id === id);
|
||||
if (!matches(fresh, expected)) throw new Error('操作结果还没有确认,请刷新状态后再试。');
|
||||
return fresh!;
|
||||
};
|
||||
}, [readAccounts, setCheck]);
|
||||
const route = async (account: CloudUserChannelAccount, target: string) => {
|
||||
if (ownerPending.current) throw new Error('微信退出还在确认中,请先处理原来的操作。');
|
||||
const input = {
|
||||
channel_account_id: account.id,
|
||||
target_agent_slug: target,
|
||||
expected_revision: account.revision,
|
||||
enabled: account.enabled,
|
||||
enabled: account.health === 'connected',
|
||||
};
|
||||
await cloudAgentsApi.call('routeChannelAccount', {
|
||||
...input,
|
||||
operation_id: operationId('route', input),
|
||||
});
|
||||
const fresh = await verifyFinal(account.id, { target, enabled: account.enabled });
|
||||
const fresh = await verifyFinal(account.id, { target, enabled: input.enabled });
|
||||
intents.current.delete('route');
|
||||
return fresh;
|
||||
};
|
||||
@@ -371,18 +427,17 @@ export function ChannelAccounts() {
|
||||
if (ownerPending.current) throw new Error('微信退出还在确认中,请先处理原来的操作。');
|
||||
if (agentsRef.current.some((agent) => agent.slug === target && agent.enabled === false))
|
||||
throw new Error('当前智能体已停用,请更换智能体后再使用。');
|
||||
const activate = flowRef.current?.mode !== 'reconnect';
|
||||
if (!account.enabled && activate) {
|
||||
if (!account.enabled) {
|
||||
const input = { channel_account_id: id, expected_revision: account.revision };
|
||||
await cloudAgentsApi.call('enableChannelAccount', {
|
||||
...input,
|
||||
operation_id: operationId('enable', input),
|
||||
operation_id: operationId('enable:' + id, input),
|
||||
});
|
||||
account = await verifyFinal(id, { target, enabled: true });
|
||||
}
|
||||
if (account.health !== 'connected' || unsettled(account) || usableBlockers(account).length)
|
||||
throw new Error(bindingReadinessMessage(account));
|
||||
intents.current.delete('enable');
|
||||
intents.current.delete('enable:' + id);
|
||||
return account;
|
||||
});
|
||||
if (!alive.current || flowRef.current?.accountId !== id) return;
|
||||
@@ -460,30 +515,6 @@ export function ChannelAccounts() {
|
||||
else void startQr(result);
|
||||
}
|
||||
};
|
||||
const setEnabled = async (account: CloudUserChannelAccount) => {
|
||||
setMore('');
|
||||
const enabled = !account.enabled;
|
||||
if (
|
||||
enabled &&
|
||||
agentsRef.current.some(
|
||||
(agent) => agent.slug === account.target_agent_slug && agent.enabled === false
|
||||
)
|
||||
)
|
||||
return;
|
||||
const key = enabled ? 'enable' : 'pause';
|
||||
const fresh = await run(key, account.id, async () => {
|
||||
const input = { channel_account_id: account.id, expected_revision: account.revision };
|
||||
await cloudAgentsApi.call(enabled ? 'enableChannelAccount' : 'pauseChannelAccount', {
|
||||
...input,
|
||||
operation_id: operationId(key, input),
|
||||
});
|
||||
const result = await verifyFinal(account.id, { target: account.target_agent_slug, enabled });
|
||||
intents.current.delete(key);
|
||||
return result;
|
||||
});
|
||||
if (fresh && alive.current)
|
||||
setNotice(enabled ? '已恢复自动回复。' : '已暂停自动回复,微信连接和智能体保留。');
|
||||
};
|
||||
const unbind = async () => {
|
||||
if (!selected) return;
|
||||
const fresh = await run('unbind', selected.id, async () => {
|
||||
@@ -513,6 +544,96 @@ export function ChannelAccounts() {
|
||||
close(false);
|
||||
}
|
||||
};
|
||||
const removeCard = async () => {
|
||||
if (!selected || !cardStorageKey || !canRemoveCard(selected)) return;
|
||||
const result = await run('remove-card', selected.id, async () => {
|
||||
const recovery = await cloudAgentsApi.recovery();
|
||||
if (!alive.current) return false;
|
||||
setPending(recovery.pending);
|
||||
if (recovery.pending.some((item) => isChannelManagementOperation(item.operation)))
|
||||
throw new Error('上次微信操作还没有确认,请先处理后再删除卡片。');
|
||||
const observed = await readAccounts();
|
||||
const account = observed.find((item) => item.id === selected.id);
|
||||
if (!alive.current || cardPreferenceKey(useAuthStore.getState().user) !== cardStorageKey)
|
||||
return false;
|
||||
if (ownerPending.current || !account || account.revision !== selected.revision || !canRemoveCard(account))
|
||||
throw new Error('这个微信的状态已变化,请刷新并确认解绑后再删除卡片。');
|
||||
const revisions = { ...readRemovedCards(cardStorageKey), [account.id]: account.revision };
|
||||
try { localStorage.setItem(cardStorageKey, JSON.stringify(revisions)); }
|
||||
catch { throw new Error('卡片没有删除:暂时无法保存到这台设备,请重试。'); }
|
||||
setRemovedCards({ key: cardStorageKey, revisions });
|
||||
return true;
|
||||
});
|
||||
if (result && alive.current) {
|
||||
setNotice('微信卡片已从这台设备移除,聊天历史保留。');
|
||||
close(false);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (
|
||||
loading || !accountsLoaded || !agentsLoaded || agentsError ||
|
||||
!recoveryLoaded || recoveryBlocked || ownerLogoutPending ||
|
||||
busy || busyRef.current || restoringReplies || flow || more ||
|
||||
checkingIds.length || replyRestoreStopped.current
|
||||
) return;
|
||||
const candidate = accounts.find((account) =>
|
||||
canRestoreReplies(account, agents) &&
|
||||
!replyRestoreAttempts.current.has(`${account.id}:${account.revision}:${account.target_agent_slug}`) &&
|
||||
!replyRestorePreflightSkipped.current.has(`${account.id}:${account.revision}:${account.target_agent_slug}`)
|
||||
);
|
||||
if (!candidate) return;
|
||||
// A zero-write preflight can retry after the next read without spinning in this render.
|
||||
replyRestorePreflightSkipped.current.add(`${candidate.id}:${candidate.revision}:${candidate.target_agent_slug}`);
|
||||
setRestoringReplies(true);
|
||||
void run('restore-replies', candidate.id, async () => {
|
||||
const [catalog, recovery] = await Promise.all([loadAgents(), cloudAgentsApi.recovery()]);
|
||||
if (!alive.current || flowRef.current) return { restored: false };
|
||||
agentsRef.current = catalog;
|
||||
setAgents(catalog);
|
||||
setPending(recovery.pending);
|
||||
setRecoveryLoaded(true);
|
||||
if (recovery.pending.some((item) => isChannelManagementOperation(item.operation)))
|
||||
return { restored: false };
|
||||
const observed = await readAccounts();
|
||||
const account = observed.find((item) => item.id === candidate.id);
|
||||
if (!account || account.target_agent_slug !== candidate.target_agent_slug)
|
||||
return { restored: false, stop: true };
|
||||
if (
|
||||
!alive.current || flowRef.current || ownerPending.current ||
|
||||
observed.some((item) => item.blockers.includes('wechat_disconnect_pending')) ||
|
||||
!canRestoreReplies(account, catalog)
|
||||
) return { restored: false };
|
||||
const key = 'enable:' + account.id;
|
||||
const input = { channel_account_id: account.id, expected_revision: account.revision };
|
||||
replyRestoreAttempts.current.add(`${account.id}:${account.revision}:${account.target_agent_slug}`);
|
||||
await cloudAgentsApi.call('enableChannelAccount', {
|
||||
...input,
|
||||
operation_id: operationId(key, input),
|
||||
});
|
||||
await verifyFinal(account.id, { target: account.target_agent_slug, enabled: true });
|
||||
intents.current.delete(key);
|
||||
return { restored: true };
|
||||
}).then((result) => {
|
||||
if (!result || result.stop) replyRestoreStopped.current = true;
|
||||
}).finally(() => {
|
||||
if (alive.current) setRestoringReplies(false);
|
||||
});
|
||||
}, [accounts, accountsLoaded, agents, agentsLoaded, agentsError, recoveryLoaded,
|
||||
recoveryBlocked, ownerLogoutPending, busy, restoringReplies, flow, more,
|
||||
checkingIds, loading, run, readAccounts, operationId, verifyFinal]);
|
||||
useEffect(() => {
|
||||
if (flow || recoveryBlocked || ownerLogoutPending || !accounts.some((account) =>
|
||||
account.target_agent_slug && (!account.enabled || usableBlockers(account).length)
|
||||
)) return;
|
||||
const timer = window.setInterval(() => {
|
||||
if (document.visibilityState === 'hidden' || busyRef.current || replyPolling.current) return;
|
||||
replyPolling.current = true;
|
||||
void refresh()
|
||||
.catch((failure) => { if (alive.current) setError(errorText(failure)); })
|
||||
.finally(() => { replyPolling.current = false; });
|
||||
}, 10000);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [accounts, flow, recoveryBlocked, ownerLogoutPending, refresh]);
|
||||
const pollingId = flow?.step === 'connect' ? flow.accountId : '';
|
||||
const pollingSession = qr?.session_key ?? '';
|
||||
const pollingStatus = qr?.status ?? '';
|
||||
@@ -586,7 +707,7 @@ export function ChannelAccounts() {
|
||||
role="alert"
|
||||
className="rounded-2xl border border-destructive/20 bg-destructive/5 px-4 py-3 text-sm text-destructive"
|
||||
>
|
||||
{agentsError}。已有微信仍可以暂停或解除绑定。
|
||||
{agentsError}。已有微信仍可以解除绑定。
|
||||
</p>
|
||||
)}
|
||||
{ownerLogoutPending && (
|
||||
@@ -604,7 +725,7 @@ export function ChannelAccounts() {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={Boolean(busy)}
|
||||
onClick={() => void refresh().catch((failure) => setError(errorText(failure)))}
|
||||
onClick={() => void refresh(true).catch((failure) => setError(errorText(failure)))}
|
||||
>
|
||||
刷新确认
|
||||
</Button>
|
||||
@@ -639,7 +760,7 @@ export function ChannelAccounts() {
|
||||
<div className="mx-auto max-w-6xl space-y-6">
|
||||
<CloudRecovery
|
||||
pending={pending}
|
||||
onResolved={() => void refresh().catch((failure) => setError(errorText(failure)))}
|
||||
onResolved={() => void refresh(true).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">
|
||||
@@ -659,7 +780,7 @@ export function ChannelAccounts() {
|
||||
size="sm"
|
||||
aria-label="刷新微信列表"
|
||||
disabled={Boolean(busy)}
|
||||
onClick={() => void refresh().catch((failure) => setError(errorText(failure)))}
|
||||
onClick={() => void refresh(true).catch((failure) => setError(errorText(failure)))}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -699,7 +820,7 @@ export function ChannelAccounts() {
|
||||
<div className="rounded-3xl border border-[#e4e2de] bg-white p-10 text-center text-sm text-[#677286]">
|
||||
暂时没能读到微信绑定,请刷新后再试。
|
||||
</div>
|
||||
) : !accounts.length ? (
|
||||
) : !visibleAccounts.length ? (
|
||||
agentsLoaded &&
|
||||
!agentsError &&
|
||||
availableAgents.length > 0 && (
|
||||
@@ -725,7 +846,7 @@ export function ChannelAccounts() {
|
||||
className="grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3"
|
||||
aria-label="我的微信绑定"
|
||||
>
|
||||
{accounts.map((account) => {
|
||||
{visibleAccounts.map((account) => {
|
||||
const agent = agents.find((item) => item.slug === account.target_agent_slug);
|
||||
const uncertain =
|
||||
!recoveryLoaded || checkingIds.includes(account.id) || pendingFor(account.id);
|
||||
@@ -744,12 +865,6 @@ export function ChannelAccounts() {
|
||||
const blocked = disabled || uncertain;
|
||||
const changeBlocked =
|
||||
blocked || unsettled(account) || !agentsLoaded || Boolean(agentsError);
|
||||
const canResume =
|
||||
!targetDisabled &&
|
||||
!unsettled(account) &&
|
||||
account.health === 'connected' &&
|
||||
Boolean(account.target_agent_slug) &&
|
||||
!usableBlockers(account).length;
|
||||
return (
|
||||
<article
|
||||
key={account.id}
|
||||
@@ -802,7 +917,7 @@ export function ChannelAccounts() {
|
||||
</p>
|
||||
)}
|
||||
{usableBlockers(account)
|
||||
.filter((item) => blockerText[item])
|
||||
.filter((item) => item !== 'worker_offline' && blockerText[item])
|
||||
.map((item) => (
|
||||
<p key={item} className="mb-3 text-xs leading-5 text-[#876033]">
|
||||
{blockerMessage(item, account)}
|
||||
@@ -848,22 +963,7 @@ export function ChannelAccounts() {
|
||||
更换智能体
|
||||
</Button>
|
||||
)}
|
||||
{account.target_agent_slug && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="justify-start"
|
||||
disabled={blocked || (!account.enabled && !canResume)}
|
||||
onClick={() => void setEnabled(account)}
|
||||
>
|
||||
{account.enabled ? (
|
||||
<Pause className="mr-2 h-4 w-4" />
|
||||
) : (
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{account.enabled ? '暂停自动回复' : '恢复自动回复'}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
{(account.target_agent_slug || !canRemoveCard(account)) && <Button
|
||||
variant="ghost"
|
||||
className="justify-start text-[#89625b]"
|
||||
disabled={blocked}
|
||||
@@ -871,7 +971,16 @@ export function ChannelAccounts() {
|
||||
>
|
||||
<Unplug className="mr-2 h-4 w-4" />
|
||||
解除微信绑定
|
||||
</Button>
|
||||
</Button>}
|
||||
{!account.target_agent_slug && <Button
|
||||
variant="ghost"
|
||||
className="justify-start text-[#89625b]"
|
||||
disabled={blocked || !cardStorageKey || !canRemoveCard(account)}
|
||||
onClick={() => open(account, 'delete')}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
删除微信卡片
|
||||
</Button>}
|
||||
</div>
|
||||
)}
|
||||
{uncertain && (
|
||||
@@ -879,7 +988,7 @@ export function ChannelAccounts() {
|
||||
variant="ghost"
|
||||
className="mt-2 text-xs"
|
||||
disabled={Boolean(busy)}
|
||||
onClick={() => void refresh().catch((failure) => setError(errorText(failure)))}
|
||||
onClick={() => void refresh(true).catch((failure) => setError(errorText(failure)))}
|
||||
>
|
||||
刷新确认
|
||||
</Button>
|
||||
@@ -905,7 +1014,9 @@ export function ChannelAccounts() {
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-bold">
|
||||
{flow?.mode === 'unbind'
|
||||
{flow?.mode === 'delete'
|
||||
? '删除微信卡片'
|
||||
: flow?.mode === 'unbind'
|
||||
? '解除微信绑定'
|
||||
: flow?.mode === 'reconnect'
|
||||
? '重新连接微信'
|
||||
@@ -925,7 +1036,24 @@ export function ChannelAccounts() {
|
||||
上次操作的结果还需要确认。请关闭窗口,处理页面上的恢复提醒。
|
||||
</p>
|
||||
)}
|
||||
{flow?.mode === 'unbind' ? (
|
||||
{flow?.mode === 'delete' ? (
|
||||
<>
|
||||
<p className="text-sm leading-7 text-[#677286]">
|
||||
删除后,这张卡片将从这台设备的微信列表移除。其他设备的记录和聊天历史保留;以后重新绑定时会再次显示。
|
||||
</p>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="h-11 rounded-xl"
|
||||
disabled={disabled || !selected || !canRemoveCard(selected) || checkingIds.includes(flow.accountId)}
|
||||
onClick={() => void removeCard()}
|
||||
>
|
||||
{busy ? '正在删除…' : '确认删除卡片'}
|
||||
</Button>
|
||||
<Button variant="ghost" disabled={Boolean(busy)} onClick={() => close()}>
|
||||
取消
|
||||
</Button>
|
||||
</>
|
||||
) : flow?.mode === 'unbind' ? (
|
||||
<>
|
||||
<p className="text-sm leading-7 text-[#677286]">
|
||||
解除后,这个微信将停止自动回复,当前任务会停止,并断开微信登录。智能体聊天历史仍保留,再使用需要重新扫码。
|
||||
@@ -979,7 +1107,7 @@ export function ChannelAccounts() {
|
||||
)}
|
||||
{flow.mode === 'switch' && selected?.target_agent_slug && (
|
||||
<p className="text-xs leading-6 text-[#677286]">
|
||||
更换会停止当前任务,微信连接和历史保留。{!selected.enabled && '这个微信仍保持暂停。'}
|
||||
更换会停止当前任务,微信连接和历史保留。连接正常后将使用新的智能体回复。
|
||||
</p>
|
||||
)}
|
||||
<Button
|
||||
@@ -1085,7 +1213,7 @@ export function ChannelAccounts() {
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={Boolean(busy)}
|
||||
onClick={() => void refresh().catch((failure) => setError(errorText(failure)))}
|
||||
onClick={() => void refresh(true).catch((failure) => setError(errorText(failure)))}
|
||||
>
|
||||
刷新确认
|
||||
</Button>
|
||||
@@ -1115,9 +1243,7 @@ export function ChannelAccounts() {
|
||||
{flow.mode === 'reconnect' ? '微信重新连接成功' : '微信绑定成功'}
|
||||
</h2>
|
||||
<p className="mt-3 break-words text-sm leading-7 text-[#557062]">
|
||||
{selected?.enabled
|
||||
? `现在可以回微信和${selected.target_agent_name ?? '智能体'}聊聊。`
|
||||
: '微信已连接,自动回复仍保持暂停。'}
|
||||
{`现在可以回微信和${selected?.target_agent_name ?? '智能体'}聊聊。`}
|
||||
</p>
|
||||
</div>
|
||||
<Button className="h-12 rounded-2xl" onClick={() => close()}>
|
||||
|
||||
Reference in new issue
Block a user