fix(cloud-agents): 移除微信本人配对入口

This commit is contained in:
2026-09-13 23:24:08 +08:00
parent 4ca1f89a7e
commit 8c1b161d0e
8 changed files with 100 additions and 38 deletions

View File

@@ -50,11 +50,11 @@ export function CloudChannelConversations({ slug, activeView = true }: { slug: s
};
return <section aria-label="本人微信会话" className="space-y-4">
<div className="flex flex-wrap items-start justify-between gap-3"><div><h2 className="font-medium"></h2>
<p className="mt-1 text-sm leading-6 text-muted-foreground"></p></div>
<p className="mt-1 text-sm leading-6 text-muted-foreground"></p></div>
<Button variant="ghost" size="sm" disabled={loading || busy} onClick={() => setRevision(value => value + 1)}><RefreshCw className="mr-2 h-4 w-4" /></Button></div>
{error && <p role="alert" className="text-sm text-destructive">{error}</p>}
{loading && !sessions.length && <p role="status" className="text-sm text-muted-foreground"></p>}
{!loading && !error && !sessions.length && <p className="rounded-xl bg-muted/30 p-6 text-sm leading-6 text-muted-foreground">访</p>}
{!loading && !error && !sessions.length && <p className="rounded-xl bg-muted/30 p-6 text-sm leading-6 text-muted-foreground">访</p>}
{!!sessions.length && <label className="block space-y-2 text-sm"><span></span>
<select className="h-10 w-full rounded-md border border-input bg-background px-3" value={selected} disabled={busy} onChange={event => setSelected(event.target.value)}>
{!sessions.some(session => session.session_id === selected) && <option value={selected}></option>}

View File

@@ -328,7 +328,7 @@ export function CloudChannelPanel({ slug, publishedVersion, activeView = true }:
setQrBindingKey(currentBindingKeyRef.current);
setQr(payload);
setQrOpen(true);
} else if (isPairing(payload) && matchesCurrent) {
} else if (isPairing(payload) && payload.kind === 'invite' && matchesCurrent) {
setCreatedPairing(payload);
}
await refresh();
@@ -458,9 +458,10 @@ export function CloudChannelPanel({ slug, publishedVersion, activeView = true }:
await loadCallers();
};
const issuePairing = async (kind: 'self' | 'invite') => {
const issueInvite = async () => {
const kind = 'invite';
if (!binding) return;
if (kind === 'invite' && binding.access_mode !== 'invited') {
if (binding.access_mode !== 'invited') {
setNotice('请先保存“本人及受邀联系人”,再生成邀请码');
return;
}
@@ -471,7 +472,7 @@ export function CloudChannelPanel({ slug, publishedVersion, activeView = true }:
operation_id: operationId(),
};
const result = await runMutation(key, 'createChannelPairing', input,
kind === 'self' ? '本人配对码已生成,请在目标微信对话中发送' : '邀请码已生成,请手动发给受邀联系人');
'邀请码已生成,请手动发给受邀联系人');
if (requestedAccountId === currentIdRef.current && isPairing(result)) setCreatedPairing(result);
};
@@ -633,7 +634,7 @@ export function CloudChannelPanel({ slug, publishedVersion, activeView = true }:
onVerificationCode={setVerificationCode} onVerify={() => void verify()} onEnable={() => void stateAction(true)} onPause={() => void stateAction(false)}
onRouteTarget={setRouteTargetSlug} onOpenRoute={() => void openRoutePicker()} onSwitchRoute={() => void switchRoute()} onConfirmEnable={setConfirmEnable} onUnbind={() => void unbind()} onConfirmUnbind={setConfirmUnbind}
/>}
{tab === 'access' && <AccessStep binding={binding} accessMode={accessMode} callers={callers} pairing={activePairing} busy={busy} busyAction={busyAction} onMode={setAccessMode} onSave={() => void updateAccessMode()} onPair={kind => void issuePairing(kind)} onClearPairing={() => setCreatedPairing(null)} onRevoke={caller => void revoke(caller)} />}
{tab === 'access' && <AccessStep binding={binding} accessMode={accessMode} callers={callers} pairing={activePairing} busy={busy} busyAction={busyAction} onMode={setAccessMode} onSave={() => void updateAccessMode()} onInvite={() => void issueInvite()} onClearPairing={() => setCreatedPairing(null)} onRevoke={caller => void revoke(caller)} />}
{tab === 'activity' && <ActivityStep slug={slug} channelAccountId={currentId} refreshKey={activityRevision} activity={activity} nextOffset={activityOffset} busy={busy} onRefresh={() => void loadActivity()} onMore={() => void (activityOffset == null ? undefined : loadActivity(activityOffset))} onRetry={retryPart} />}
{tab === 'sessions' && <CloudChannelConversations slug={slug} activeView={activeView && tab === 'sessions'} />}
</section>;
@@ -685,20 +686,20 @@ function StepHeading({ number, title, description }: { number: string; title: st
return <div className="flex gap-3 border-t border-border/60 pt-5"><span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-brand/10 text-sm font-medium text-brand">{number}</span><div><h3 className="font-medium">{title}</h3><p className="mt-1 text-sm leading-6 text-muted-foreground">{description}</p></div></div>;
}
function AccessStep({ binding, accessMode, callers, pairing, busy, busyAction, onMode, onSave, onPair, onClearPairing, onRevoke }: { binding: CloudChannelView | null; accessMode: 'self_only' | 'invited'; callers: CloudChannelCaller[]; pairing: CloudChannelPairing | null; busy: boolean; busyAction: string; onMode: (value: 'self_only' | 'invited') => void; onSave: () => void; onPair: (kind: 'self' | 'invite') => void; onClearPairing: () => void; onRevoke: (caller: CloudChannelCaller) => void }) {
function AccessStep({ binding, accessMode, callers, pairing, busy, busyAction, onMode, onSave, onInvite, onClearPairing, onRevoke }: { binding: CloudChannelView | null; accessMode: 'self_only' | 'invited'; callers: CloudChannelCaller[]; pairing: CloudChannelPairing | null; busy: boolean; busyAction: string; onMode: (value: 'self_only' | 'invited') => void; onSave: () => void; onInvite: () => void; onClearPairing: () => void; onRevoke: (caller: CloudChannelCaller) => void }) {
const inviteEnabled = accessMode === 'invited' && binding?.access_mode === 'invited';
return <div className="space-y-5"><StepHeading number="2" title="谁能使用" description="默认只允许你本人。配对码和邀请都是短期、单次使用;创建者只能查看受邀者状态和费用,不读取其私聊内容。" />
return <div className="space-y-5"><StepHeading number="2" title="谁能使用" description="默认只允许绑定时扫码的微信使用。受邀联系人需要单独邀请;你只能查看受邀者状态和费用,不读取其私聊内容。" />
{!binding ? <p className="rounded-xl bg-muted/30 p-4 text-sm text-muted-foreground"></p> : <>
<div className="space-y-3 rounded-xl border border-border p-4"><label className="flex cursor-pointer gap-3 text-sm"><input type="radio" name="channel-access-mode" checked={accessMode === 'self_only'} onChange={() => onMode('self_only')} /><span><strong></strong><span className="mt-1 block text-muted-foreground"></span></span></label><label className="flex cursor-pointer gap-3 text-sm"><input type="radio" name="channel-access-mode" checked={accessMode === 'invited'} onChange={() => onMode('invited')} /><span><strong></strong><span className="mt-1 block text-muted-foreground"></span></span></label><Button variant="outline" disabled={busy || accessMode === (binding.access_mode === 'invited' ? 'invited' : 'self_only')} onClick={onSave}>{busyAction.startsWith('policy:') ? '保存中…' : '保存使用范围'}</Button></div>
<div className="grid gap-4 md:grid-cols-2"><div className="space-y-3 rounded-xl bg-muted/30 p-4"><h3 className="text-sm font-medium"></h3><p className="text-xs leading-5 text-muted-foreground"></p><Button variant="outline" disabled={busy} onClick={() => onPair('self')}>{busyAction.includes(':self') ? '生成中…' : '生成本人配对码'}</Button></div>{accessMode === 'invited' && <div className="space-y-3 rounded-xl bg-muted/30 p-4"><h3 className="text-sm font-medium"></h3><p className="text-xs leading-5 text-muted-foreground"></p><Button variant="outline" disabled={busy || !inviteEnabled} onClick={() => onPair('invite')}>{busyAction.includes(':invite') ? '生成中…' : '生成邀请码'}</Button>{!inviteEnabled && <p className="text-xs text-amber-800"></p>}</div>}</div>
{pairing && <PairingResult value={pairing} onClear={onClearPairing} />}
<div className="space-y-3 rounded-xl border border-border p-4"><div className="flex items-center justify-between gap-3"><h3 className="flex items-center gap-2 text-sm font-medium"><Users className="h-4 w-4" /></h3><span className="text-xs text-muted-foreground">{callers.length} </span></div>{!callers.length && <p className="text-sm text-muted-foreground"></p>}{callers.map(caller => <div key={caller.caller_id} className="flex items-center justify-between gap-3 border-t border-border/60 pt-3 text-sm"><div><p>{caller.access_mode === 'self_only' ? '本人微信会话' : '受邀联系人'}</p><p className="mt-1 text-xs text-muted-foreground">{statusLabel(caller.grant_state)} · {shortDate(caller.created_at)}</p></div><Button variant="ghost" size="sm" className="text-destructive" disabled={busy || caller.grant_state === 'revoked'} onClick={() => onRevoke(caller)}></Button></div>)}</div>
<div className="space-y-3 rounded-xl border border-border p-4"><label className="flex cursor-pointer gap-3 text-sm"><input type="radio" name="channel-access-mode" checked={accessMode === 'self_only'} onChange={() => onMode('self_only')} /><span><strong></strong><span className="mt-1 block text-muted-foreground">使</span></span></label><label className="flex cursor-pointer gap-3 text-sm"><input type="radio" name="channel-access-mode" checked={accessMode === 'invited'} onChange={() => onMode('invited')} /><span><strong></strong><span className="mt-1 block text-muted-foreground"></span></span></label><Button variant="outline" disabled={busy || accessMode === (binding.access_mode === 'invited' ? 'invited' : 'self_only')} onClick={onSave}>{busyAction.startsWith('policy:') ? '保存中…' : '保存使用范围'}</Button></div>
<div>{accessMode === 'invited' && <div className="space-y-3 rounded-xl bg-muted/30 p-4"><h3 className="text-sm font-medium"></h3><p className="text-xs leading-5 text-muted-foreground"></p><Button variant="outline" disabled={busy || !inviteEnabled} onClick={onInvite}>{busyAction.includes(':invite') ? '生成中…' : '生成邀请码'}</Button>{!inviteEnabled && <p className="text-xs text-amber-800"></p>}</div>}</div>
{pairing?.kind === 'invite' && <PairingResult value={pairing} onClear={onClearPairing} />}
<div className="space-y-3 rounded-xl border border-border p-4"><div className="flex items-center justify-between gap-3"><h3 className="flex items-center gap-2 text-sm font-medium"><Users className="h-4 w-4" /></h3><span className="text-xs text-muted-foreground">{callers.length} </span></div>{!callers.length && <p className="text-sm text-muted-foreground"></p>}{callers.map(caller => <div key={caller.caller_id} className="flex items-center justify-between gap-3 border-t border-border/60 pt-3 text-sm"><div><p>{caller.access_mode === 'self_only' ? '本人微信会话' : '受邀联系人'}</p><p className="mt-1 text-xs text-muted-foreground">{statusLabel(caller.grant_state)} · {shortDate(caller.created_at)}</p></div><Button variant="ghost" size="sm" className="text-destructive" disabled={busy || caller.grant_state === 'revoked'} onClick={() => onRevoke(caller)}></Button></div>)}</div>
</>}</div>;
}
function PairingResult({ value, onClear }: { value: CloudChannelPairing; onClear: () => void }) {
const copy = () => { if (value.code) void navigator.clipboard.writeText(`${value.kind === 'self' ? '配对' : '邀请'} ${value.code}`); };
return <div role="status" className="space-y-3 rounded-xl bg-amber-50 p-4 text-sm"><div className="flex items-start justify-between gap-3"><div><p className="font-medium">{value.kind === 'self' ? '本人一次性配对码' : '一次性邀请码'}</p><p className="mt-1 text-xs text-amber-900/70">{value.expires_at ? shortDate(value.expires_at) : '短期有效'}</p></div><Button variant="ghost" size="sm" onClick={onClear}></Button></div>{value.code ? <div className="flex items-center gap-2"><code className="min-w-0 flex-1 rounded-lg bg-white px-3 py-2 font-mono text-base tracking-widest">{value.code}</code><Button aria-label="复制配对命令" variant="outline" size="sm" onClick={copy}><Copy className="h-4 w-4" /></Button></div> : <p className="text-xs text-amber-900/70"></p>}</div>;
const copy = () => { if (value.code) void navigator.clipboard.writeText(`邀请 ${value.code}`); };
return <div role="status" className="space-y-3 rounded-xl bg-amber-50 p-4 text-sm"><div className="flex items-start justify-between gap-3"><div><p className="font-medium"></p><p className="mt-1 text-xs text-amber-900/70">{value.expires_at ? shortDate(value.expires_at) : '短期有效'}</p></div><Button variant="ghost" size="sm" onClick={onClear}></Button></div>{value.code ? <div className="flex items-center gap-2"><code className="min-w-0 flex-1 rounded-lg bg-white px-3 py-2 font-mono text-base tracking-widest">{value.code}</code><Button aria-label="复制邀请命令" variant="outline" size="sm" onClick={copy}><Copy className="h-4 w-4" /></Button></div> : <p className="text-xs text-amber-900/70"></p>}</div>;
}
function ActivityStep({ slug, channelAccountId, refreshKey, activity, nextOffset, busy, onRefresh, onMore, onRetry }: {

View File

@@ -56,7 +56,7 @@ export function CloudSchedules({ slug, onOpen, proposal, onProposalClosed }: { s
{job.runs?.map((run, i) => <div key={i} className="flex items-center justify-between gap-3 border-t border-border/50 pt-3 text-sm">
<div><p>{cloudStatus(run.status)}{run.error_message && <span className="ml-2 text-muted-foreground">{run.error_message}</span>}</p>
{run.result_notification && <p className="mt-1 text-xs text-muted-foreground">{run.result_notification.state === 'waiting_context'
? '微信通知待恢复:请确认连接,并在已配对的微信对话中发送一条消息。任务结果已独立保存。'
? '微信通知待恢复:请确认连接,并在本人微信对话中发送一条消息。任务结果已独立保存。'
: '微信通知已配置,实际送达情况可在发布渠道的活动记录中查看。'}</p>}</div>
{run.conversation_available && <Button variant="ghost" onClick={() => onOpen({ thread_id: run.thread_id, client_thread_id: null, agent_slug: slug,
title: job.name, status: run.status, mode: 'published', unread: false, updated_at: '', run_id: null })}></Button>}
@@ -160,8 +160,8 @@ function ScheduleNotification({ slug, value, onChange }: {
{channels.find(channel => channel.id === caller.channel_account_id)?.display_name ?? '个人微信'} · {index + 1}
</option>)}
</select></label>
<p className="text-xs leading-6 text-muted-foreground"></p>
{!loading && !error && !options.length && <p className="text-xs leading-6 text-muted-foreground">访</p>}
<p className="text-xs leading-6 text-muted-foreground"></p>
{!loading && !error && !options.length && <p className="text-xs leading-6 text-muted-foreground">访</p>}
{selected && !available && !loading && <p className="text-xs leading-6 text-muted-foreground"></p>}
{error && <p role="alert" className="text-xs text-destructive">{error}</p>}
</section>;