From 8c1b161d0e14394beb62ae7f7e05c96cfed045b2 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Sun, 13 Sep 2026 23:24:08 +0800 Subject: [PATCH] =?UTF-8?q?fix(cloud-agents):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E6=9C=AC=E4=BA=BA=E9=85=8D=E5=AF=B9=E5=85=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20260913-wechat-auto-self-ml-3679bce2.md | 42 +++++++++++++++++++ README.md | 2 +- electron/services/cloud-agents.ts | 9 ++-- .../CloudAgents/CloudChannelConversations.tsx | 4 +- src/pages/CloudAgents/CloudChannelPanel.tsx | 27 ++++++------ src/pages/CloudAgents/CloudSchedules.tsx | 6 +-- tests/e2e/cloud-agent-channels.spec.ts | 7 ++-- tests/unit/cloud-channel-panel.test.tsx | 41 ++++++++++++------ 8 files changed, 100 insertions(+), 38 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260913-wechat-auto-self-ml-3679bce2.md diff --git a/.project-docs/30-worklog/tasks/20260913-wechat-auto-self-ml-3679bce2.md b/.project-docs/30-worklog/tasks/20260913-wechat-auto-self-ml-3679bce2.md new file mode 100644 index 0000000..0eee669 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260913-wechat-auto-self-ml-3679bce2.md @@ -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. diff --git a/README.md b/README.md index 42a730e..1e29689 100644 --- a/README.md +++ b/README.md @@ -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 密码。 diff --git a/electron/services/cloud-agents.ts b/electron/services/cloud-agents.ts index 59723b9..0326170 100644 --- a/electron/services/cloud-agents.ts +++ b/electron/services/cloud-agents.ts @@ -45,7 +45,8 @@ const MESSAGES: Record = { 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 = { run_not_active: '当前会话没有可操作的运行', channel_session_unavailable: '新的微信会话暂时无法建立', pairing_invalid: '配对请求无效,请刷新后重试', - invitation_invalid: '配对码无效或已撤销', - invitation_consumed: '配对码已经使用', - invites_disabled: '当前渠道仅允许创建者本人配对', + invitation_invalid: '邀请码无效或已撤销', + invitation_consumed: '邀请码已经使用', + invites_disabled: '当前渠道仅允许本人使用', agentbus_unavailable: '渠道服务暂时不可用,请稍后重试', }; diff --git a/src/pages/CloudAgents/CloudChannelConversations.tsx b/src/pages/CloudAgents/CloudChannelConversations.tsx index 1ed9423..34dce32 100644 --- a/src/pages/CloudAgents/CloudChannelConversations.tsx +++ b/src/pages/CloudAgents/CloudChannelConversations.tsx @@ -50,11 +50,11 @@ export function CloudChannelConversations({ slug, activeView = true }: { slug: s }; return

本人微信对话

-

查看你已配对的本人会话,处理确认和保存文件。新消息请从对应微信对话发送。

+

查看你的本人微信会话,处理确认和保存文件。新消息请从对应微信对话发送。

{error &&

{error}

} {loading && !sessions.length &&

正在读取本人会话…

} - {!loading && !error && !sessions.length &&

还没有本人微信会话。请先在发布与访问中连接个人微信,并完成本人配对。

} + {!loading && !error && !sessions.length &&

还没有本人微信会话。请先在发布与访问中连接并启用个人微信,然后用绑定时扫码的微信发送一条消息。

} {!!sessions.length && -

本人配对

生成一次性配对码后,在你的目标微信私聊中发送完整配对命令。未完成配对前不会调用模型。

{accessMode === 'invited' &&

邀请联系人

邀请码需要你手动发给联系人;平台不自动向微信联系人发消息。

{!inviteEnabled &&

请先保存“本人及受邀联系人”后生成邀请码。

}
}
- {pairing && } -

已授权调用者

{callers.length} 个
{!callers.length &&

还没有已配对的调用者。

}{callers.map(caller =>

{caller.access_mode === 'self_only' ? '本人微信会话' : '受邀联系人'}

{statusLabel(caller.grant_state)} · 配对于 {shortDate(caller.created_at)}

)}
+
+
{accessMode === 'invited' &&

邀请联系人

邀请码需要你手动发给联系人;平台不自动向微信联系人发消息。

{!inviteEnabled &&

请先保存“本人及受邀联系人”后生成邀请码。

}
}
+ {pairing?.kind === 'invite' && } +

已授权调用者

{callers.length} 个
{!callers.length &&

首次收到你的微信消息后,会在这里显示本人会话。

}{callers.map(caller =>

{caller.access_mode === 'self_only' ? '本人微信会话' : '受邀联系人'}

{statusLabel(caller.grant_state)} · 授权于 {shortDate(caller.created_at)}

)}
}; } function PairingResult({ value, onClear }: { value: CloudChannelPairing; onClear: () => void }) { - const copy = () => { if (value.code) void navigator.clipboard.writeText(`${value.kind === 'self' ? '配对' : '邀请'} ${value.code}`); }; - return

{value.kind === 'self' ? '本人一次性配对码' : '一次性邀请码'}

{value.expires_at ? shortDate(value.expires_at) : '短期有效'};关闭此面板后不会再次展示。

{value.code ?
{value.code}
:

服务已受理,请在活动记录中查看操作状态。

}
; + const copy = () => { if (value.code) void navigator.clipboard.writeText(`邀请 ${value.code}`); }; + return

一次性邀请码

{value.expires_at ? shortDate(value.expires_at) : '短期有效'};关闭此面板后不会再次展示。

{value.code ?
{value.code}
:

服务已受理,请在活动记录中查看操作状态。

}
; } function ActivityStep({ slug, channelAccountId, refreshKey, activity, nextOffset, busy, onRefresh, onMore, onRetry }: { diff --git a/src/pages/CloudAgents/CloudSchedules.tsx b/src/pages/CloudAgents/CloudSchedules.tsx index 8151021..9301d57 100644 --- a/src/pages/CloudAgents/CloudSchedules.tsx +++ b/src/pages/CloudAgents/CloudSchedules.tsx @@ -56,7 +56,7 @@ export function CloudSchedules({ slug, onOpen, proposal, onProposalClosed }: { s {job.runs?.map((run, i) =>

{cloudStatus(run.status)}{run.error_message && {run.error_message}}

{run.result_notification &&

{run.result_notification.state === 'waiting_context' - ? '微信通知待恢复:请确认连接,并在已配对的微信对话中发送一条消息。任务结果已独立保存。' + ? '微信通知待恢复:请确认连接,并在本人微信对话中发送一条消息。任务结果已独立保存。' : '微信通知已配置,实际送达情况可在发布渠道的活动记录中查看。'}

}
{run.conversation_available && } @@ -160,8 +160,8 @@ function ScheduleNotification({ slug, value, onChange }: { {channels.find(channel => channel.id === caller.channel_account_id)?.display_name ?? '个人微信'} · 本人对话 {index + 1} )} -

只可选择你已配对的本人微信对话。任务仍在云端运行,结果保留在历史中,通知不会再次执行任务。

- {!loading && !error && !options.length &&

还没有可用的本人微信对话。请先在“发布与访问”的个人微信渠道中完成连接、本人配对并启用,然后回来刷新。

} +

只可选择已经开始对话的本人微信。任务仍在云端运行,结果保留在历史中,通知不会再次执行任务。

+ {!loading && !error && !options.length &&

还没有可用的本人微信对话。请先在“发布与访问”的个人微信渠道中完成连接并启用,再用绑定时扫码的微信发送一条消息,然后回来刷新。

} {selected && !available && !loading &&

请恢复原连接,或重新选择通知目标。选择“仅保存在客户端”可关闭微信通知。

} {error &&

{error}。原通知配置已保留,可刷新后重试。

}
; diff --git a/tests/e2e/cloud-agent-channels.spec.ts b/tests/e2e/cloud-agent-channels.spec.ts index cf95095..6912508 100644 --- a/tests/e2e/cloud-agent-channels.spec.ts +++ b/tests/e2e/cloud-agent-channels.spec.ts @@ -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); }); diff --git a/tests/unit/cloud-channel-panel.test.tsx b/tests/unit/cloud-channel-panel.test.tsx index b0fbabb..2161fb3 100644 --- a/tests/unit/cloud-channel-panel.test.tsx +++ b/tests/unit/cloud-channel-panel.test.tsx @@ -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(); - 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(); + 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) => { - 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) => { - 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(); 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('还没有渠道活动。');