fix(cloud-agents): 移除微信本人配对入口
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
# Task: 移除微信本人配对界面并展示绑定身份状态
|
||||
|
||||
## Identity
|
||||
|
||||
- Task ID: 20260913-wechat-auto-self-ml-3679bce2
|
||||
- Mode: Feature
|
||||
- Branch: codex/20260913-wechat-auto-self-ml-3679bce2-wechat-auto-self-ml
|
||||
- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260913-wechat-auto-self-ml-3679bce2
|
||||
- Base commit: 4ca1f89a7e2d1a2c7e90af3b4aeb3541f3f0cd4f
|
||||
- Owner: codex
|
||||
- Status: Ready for Integration
|
||||
|
||||
## Scope
|
||||
|
||||
- Remove creator self pairing from personal WeChat publication. Explain automatic recognition of the QR login owner, retain explicit invitations for other contacts, and update conversation/schedule guidance, focused regressions and README.
|
||||
|
||||
## Intent And Constraints
|
||||
|
||||
- Resumed for the user-authorized main-branch merge. User explicitly confirmed one fresh read-only Reviewer and three primary registration handoffs. Source Concurrent/Planning Gates Passed on unchanged bases; only this feature task may record its remediation and commit evidence. Independent review is in progress.
|
||||
- Concurrent and Planning Gates Passed using official start/status and loaded entry, task, current-state, personal cloud ADR, architecture and peer records. Historical placeholder peer context remains unknown/read-only without a concrete semantic dependency. Related channel peers are completed integration or read-only diagnosis.
|
||||
- User explicitly replaces creator self-code behavior. Main remains the credential/network boundary; remote services determine owner identity. Preserve caller isolation, paid execution authorization, existing access revocation and invited contacts. No production actions, pushes, primary checkout edits or canonical memory writes.
|
||||
- Plan: remove self-code actions and stale recovery presentation; update only meaningful guidance/error projections; verify invitations still work, first-use UI has no pairing action, and Electron publication/conversation flows remain usable.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Removed the self-code action/card and old-self-code recovery display. Bound scanning WeChat can directly send messages; only invited contacts have an invite-code action. Conversation/history and scheduled target empty states explain sending the first message rather than pairing. Updated Main safe error guidance and README; existing privacy/revocation/approval and invitation transport remain.
|
||||
- Source implementation is complete and ready for authorized local integration. The user-authorized fresh independent Reviewer returned final PASS. No packaging, deployment or paid requests were performed.
|
||||
|
||||
## Verification
|
||||
|
||||
- Final independent review: PASS; review task `20260913-wechat-self-review-ml-a85f20` is complete in its own retained read-only worktree. No remaining findings. Product validation evidence below remains applicable; no live WeChat or production acceptance is claimed.
|
||||
- Pinned pnpm 10.33.4 install --frozen-lockfile passed. Focused panel/conversation/enable/workflow/Main suites: 64 passed, including absence of self pairing calls, explicit invitation command copy, binding switch and existing delivery behavior.
|
||||
- pnpm exec tsc --noEmit -p tsconfig.json, pnpm run build:vite and scoped ESLint passed. git diff --check passed.
|
||||
- Electron cloud-agent-channels.spec.ts: 1 passed, covering no self pairing action, QR/verification, owner approval/history/file delivery and notification target. Inspected wechat-access-without-pairing.png from the actual Electron test. Host API is fixture-backed; no live WeChat/model acceptance or new installer was performed.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- Integrate with matching AgentBus and Yuxi sources. Deployment and client packaging remain separate. A historical WeChat account without stored scanning identity needs one reconnect.
|
||||
|
||||
## Promotion Candidates
|
||||
|
||||
- Target: accepted personal cloud Agent ADR, current-state and channel architecture. Proposal: replace creator self pairing steps with automatic recognition of the scanning WeChat; first incoming owner message establishes history/notification session. Keep separate contact invitations and privacy. Evidence: focused unit, typecheck/build/lint and inspected Electron flow. Conflict: user explicitly removes previous self-code UX; canonical update belongs to integration, no further product confirmation.
|
||||
@@ -11,7 +11,7 @@ Makelore 是一个面向软件、视觉创作、智能机器人与个人云智
|
||||
|
||||
云知识库的向量模型由平台管理员配置:Yuxi 后台「智能体管理 → 模型供应商」启用 embedding 模型并填写实际维度;Works Square 运营后台「模型管理」从 one-api 刷新并激活相同模型 ID。one-api 必须支持该模型的向量接口。客户端区分目录加载中、加载失败与无可用模型,空状态提供管理员配置说明;「刷新知识库与模型」保留未提交名称,未确认的创建请求沿用原操作身份。
|
||||
|
||||
在「发布与访问」中可通过 AgentBus 连接个人微信:选用本人已有连接或扫码连接新微信,完成本人配对、设置联系人使用权限,再确认启用。渠道支持暂停、切换智能体、断开连接和查看运行/投递活动;失败的文件可单独补发。本人已配对的微信会话可在客户端查看、审批、停止、新开一段并保存实际产物;受邀联系人的内容不会因创建者付费而对创建者开放。自动任务可明确选择把结果发送到已配对的本人微信,暂停保留目标;费用统计包含微信渠道。渠道需要配套版本的 AgentBus Core/微信 Adapter、Yuxi 和 Works Square,桌面用户无需填写服务凭据。Main 保管操作恢复记录,重开界面不会自动重发绑定或执行请求;验证码不写入恢复记录。
|
||||
在「发布与访问」中可通过 AgentBus 连接个人微信:选用本人已有连接或扫码连接新微信,设置联系人使用权限并确认启用,绑定时扫码的微信可直接发送消息,无需本人配对码。其他联系人仍需创建者邀请。渠道支持暂停、切换智能体、断开连接和查看运行/投递活动;失败的文件可单独补发。本人的微信会话可在客户端查看、审批、停止、新开一段并保存实际产物;受邀联系人的内容不会因创建者付费而对创建者开放。自动任务可明确选择把结果发送到已经开始对话的本人微信,暂停保留目标;费用统计包含微信渠道。渠道需要配套版本的 AgentBus Core/微信 Adapter、Yuxi 和 Works Square,桌面用户无需填写服务凭据。Main 保管操作恢复记录,重开界面不会自动重发绑定或执行请求;验证码不写入恢复记录。
|
||||
|
||||
- `Makelore Canvas|AI 绘画`:每个设计项目(Workspace)维护一份从创建起就存在的 Living Form。左侧项目栏负责新建、切换和管理 Workspace,并在桌面设计模式下以 256px 宽度常驻展开;中央沿用 AI 编程的安静对话画布、自然消息流和底部悬浮输入器,AI 整理出的制作方案作为对话内的轻量可编辑稿持续更新;桌面端右侧同为 256px 的全高历史作品栏集中展示当前项目的制作记录与生成结果。紧凑窗口通过左侧抽屉访问项目列表,历史记录保留在时间线中。参考图从本地上传后以 `@图片N` 绑定,具体用法只写在创作提示词中。
|
||||
- `Makelore Robot|AI 机器`:管理机器人智能体、设备激活绑定、智能体配置与设备分配;机器人工作台的智能体位于 Robot 全局侧栏,选中后在内容区先查看绑定设备、再查看基础设置,当前智能体通过 URL 参数保持可分享选择;绑定设备时默认先选择“引导配网”或“已有激活码”。在 Windows 与 macOS 的引导路径中,Makelore 可在弹窗内扫描并连接附近开放的 `Xiaozhi-*` 配网热点,失败时仍可通过系统 Wi-Fi 手动连接;后续继续复用机器人现有热点配网页面,不修改固件,也不由 Makelore 接收 Wi-Fi 密码。
|
||||
|
||||
@@ -45,7 +45,8 @@ const MESSAGES: Record<string, string> = {
|
||||
channel_session_control_failed: '微信会话操作失败,请刷新后重试',
|
||||
channel_delivery_not_found: '渠道交付记录不存在',
|
||||
channel_delivery_retry_failed: '文件补发失败,请刷新交付记录后重试',
|
||||
pairing_expired: '配对码已过期,请重新生成',
|
||||
self_pairing_removed: '本人无需配对,请直接使用绑定时扫码的微信发送消息',
|
||||
pairing_expired: '邀请码已过期,请重新生成',
|
||||
verification_required: '请先完成微信验证',
|
||||
artifact_unavailable: '文件已不可用,请刷新文件列表',
|
||||
operation_unavailable: '操作状态暂时无法确认,请稍后查询',
|
||||
@@ -61,9 +62,9 @@ const MESSAGES: Record<string, string> = {
|
||||
run_not_active: '当前会话没有可操作的运行',
|
||||
channel_session_unavailable: '新的微信会话暂时无法建立',
|
||||
pairing_invalid: '配对请求无效,请刷新后重试',
|
||||
invitation_invalid: '配对码无效或已撤销',
|
||||
invitation_consumed: '配对码已经使用',
|
||||
invites_disabled: '当前渠道仅允许创建者本人配对',
|
||||
invitation_invalid: '邀请码无效或已撤销',
|
||||
invitation_consumed: '邀请码已经使用',
|
||||
invites_disabled: '当前渠道仅允许本人使用',
|
||||
agentbus_unavailable: '渠道服务暂时不可用,请稍后重试',
|
||||
};
|
||||
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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 }: {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -85,9 +85,10 @@ test('own WeChat conversation approval and generated PPT delivery in the desktop
|
||||
await expect.poll(async () => app.evaluate(() => (globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string; input: unknown }[] }).cloudChannelFixtureCalls.some(call => call.operation === 'wechatBindVerification'))).toBe(true);
|
||||
if (await page.getByRole('button', { name: '关闭二维码', exact: true }).count()) await page.getByRole('button', { name: '关闭二维码', exact: true }).click();
|
||||
await page.getByRole('button', { name: '使用权限', exact: true }).click();
|
||||
await page.getByRole('button', { name: '生成本人配对码', exact: true }).click();
|
||||
await expect(page.getByText('TEST1234', { exact: true })).toBeVisible();
|
||||
await page.getByRole('button', { name: '隐藏', exact: true }).click();
|
||||
await expect(page.getByText('使用绑定时扫码的微信,直接发送消息即可。', { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: '生成本人配对码', exact: true })).toHaveCount(0);
|
||||
expect(await app.evaluate(() => (globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string }[] }).cloudChannelFixtureCalls.some(call => call.operation === 'createChannelPairing'))).toBe(false);
|
||||
await page.screenshot({ path: testInfo.outputPath('wechat-access-without-pairing.png') });
|
||||
await page.getByRole('button', { name: '连接微信', exact: true }).click();
|
||||
await page.screenshot({ path: testInfo.outputPath('wechat-publication.png') });
|
||||
await app.evaluate(({ BrowserWindow }) => { const win = BrowserWindow.getAllWindows()[0]; win.setMinimumSize(800, 600); win.setSize(980, 680); });
|
||||
|
||||
@@ -67,14 +67,31 @@ it('requires an explicit enable confirmation and sends the typed binding operati
|
||||
expect(typeof input.operation_id).toBe('string');
|
||||
});
|
||||
|
||||
it('uses the pairing code command and does not mutate when the access tab is opened', async () => {
|
||||
it('uses the bound WeChat identity without offering or requesting a self pairing code', async () => {
|
||||
render(<CloudChannelPanel slug={slug} publishedVersion={2} />);
|
||||
await screen.findByRole('button', { name: '使用权限' });
|
||||
fireEvent.click(screen.getByRole('button', { name: '使用权限' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '生成本人配对码' }));
|
||||
await screen.findByText('ABC123');
|
||||
expect(api.call.mock.calls.some(call => call[0] === 'channelPolicy')).toBe(false);
|
||||
expect(api.call.mock.calls.find(call => call[0] === 'createChannelPairing')?.[1]).toEqual(expect.objectContaining({ kind: 'self', channel_account_id: 'wechat-1' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '使用权限' }));
|
||||
await screen.findByText('使用绑定时扫码的微信,直接发送消息即可。');
|
||||
expect(screen.queryByRole('button', { name: '生成本人配对码' })).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: '生成邀请码' })).toBeNull();
|
||||
expect(api.call.mock.calls.some(call => ['createChannelPairing', 'channelPolicy'].includes(call[0]))).toBe(false);
|
||||
});
|
||||
|
||||
it('still requires an explicit invitation for other contacts and copies the complete command', async () => {
|
||||
const base = api.call.getMockImplementation()!;
|
||||
api.call.mockImplementation(async (operation: string, input) => {
|
||||
if (operation === 'channelBindings') return { items: [{ ...binding, access_mode: 'invited' }], available_accounts: [] };
|
||||
if (operation === 'createChannelPairing') return { invitation_id: 'invite-a', binding_id: 'binding-1', provider_generation: 'provider-1', kind: 'invite', consumed: false, code: 'INVITE123' };
|
||||
return base(operation, input);
|
||||
});
|
||||
const copy = vi.fn().mockResolvedValue(undefined);
|
||||
Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText: copy } });
|
||||
render(<CloudChannelPanel slug={slug} publishedVersion={2} />);
|
||||
fireEvent.click(await screen.findByRole('button', { name: '使用权限' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '生成邀请码' }));
|
||||
await screen.findByText('INVITE123');
|
||||
fireEvent.click(screen.getByRole('button', { name: '复制邀请命令' }));
|
||||
expect(copy).toHaveBeenCalledWith('邀请 INVITE123');
|
||||
expect(api.call.mock.calls.find(call => call[0] === 'createChannelPairing')?.[1]).toEqual(expect.objectContaining({ kind: 'invite', channel_account_id: 'wechat-1' }));
|
||||
});
|
||||
|
||||
it('looks up delivery by the result message ID before offering file retry', async () => {
|
||||
@@ -151,7 +168,7 @@ it('drops an old QR session when switching bindings and ignores its late status'
|
||||
const oldStatus = new Promise(resolve => { releaseOldStatus = resolve; });
|
||||
let statusCalls = 0;
|
||||
api.call.mockImplementation(async (operation: string, input: Record<string, unknown>) => {
|
||||
if (operation === 'channelBindings') return { items: [binding, bindingB], available_accounts: [] };
|
||||
if (operation === 'channelBindings') return { items: [{ ...binding, access_mode: 'invited' }, bindingB], available_accounts: [] };
|
||||
if (operation === 'channelCallers') return { items: [] };
|
||||
if (operation === 'channelActivity') return { items: [], next_offset: null };
|
||||
if (operation === 'wechatBindStart') return { session_key: 'session-a', status: 'waiting', qrcode_url: 'https://provider.example/qr-a' };
|
||||
@@ -181,16 +198,16 @@ it('clears the previous binding pairing, callers, and activity when switching ac
|
||||
delivery: { state: 'known' }, timing: { created_at: '2026-09-13T00:00:00Z' },
|
||||
};
|
||||
api.call.mockImplementation(async (operation: string, input: Record<string, unknown>) => {
|
||||
if (operation === 'channelBindings') return { items: [binding, bindingB], available_accounts: [] };
|
||||
if (operation === 'channelBindings') return { items: [{ ...binding, access_mode: 'invited' }, bindingB], available_accounts: [] };
|
||||
if (operation === 'channelCallers') return { items: input.channel_account_id === 'wechat-1' ? [callerA] : [] };
|
||||
if (operation === 'channelActivity') return { items: input.channel_account_id === 'wechat-1' ? [activityA] : [], next_offset: null };
|
||||
if (operation === 'createChannelPairing') return { binding_id: 'binding-1', provider_generation: 'provider-1', kind: 'self', consumed: false, code: 'PAIR-A' };
|
||||
if (operation === 'createChannelPairing') return { binding_id: 'binding-1', provider_generation: 'provider-1', kind: 'invite', consumed: false, code: 'PAIR-A' };
|
||||
throw new Error(`Unexpected operation ${operation}`);
|
||||
});
|
||||
render(<CloudChannelPanel slug={slug} publishedVersion={2} />);
|
||||
fireEvent.click(await screen.findByRole('button', { name: '使用权限' }));
|
||||
await screen.findByText('受邀联系人', { exact: true });
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成本人配对码' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成邀请码' }));
|
||||
await screen.findByText('PAIR-A');
|
||||
fireEvent.click(screen.getByRole('button', { name: '活动记录' }));
|
||||
await screen.findByText('词元 7');
|
||||
@@ -198,7 +215,7 @@ it('clears the previous binding pairing, callers, and activity when switching ac
|
||||
fireEvent.change(await screen.findByLabelText('当前渠道'), { target: { value: 'wechat-2' } });
|
||||
await waitFor(() => expect(screen.queryByText('PAIR-A')).toBeNull());
|
||||
fireEvent.click(screen.getByRole('button', { name: '使用权限' }));
|
||||
await screen.findByText('还没有已配对的调用者。');
|
||||
await screen.findByText('首次收到你的微信消息后,会在这里显示本人会话。');
|
||||
expect(screen.queryByText('受邀联系人', { exact: true })).toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: '活动记录' }));
|
||||
await screen.findByText('还没有渠道活动。');
|
||||
|
||||
Reference in New Issue
Block a user