From 21e707d9c43b2db3622d21358546f1734d70b2db Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Mon, 14 Sep 2026 12:05:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=AE=80=E5=8C=96=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=E6=B8=A0=E9=81=93=E8=B4=A6=E5=8F=B7=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20260914-wechat-scan-owner-ml-b4169ea9.md | 65 ++ README.md | 2 +- electron/services/cloud-agent-operations.ts | 21 +- electron/services/cloud-agents.ts | 19 +- shared/cloud-agents.ts | 26 - src/lib/cloud-agents-api.ts | 2 +- src/pages/CloudAgents/ChannelAccounts.tsx | 6 +- src/pages/CloudAgents/CloudAccess.tsx | 2 +- .../CloudAgents/CloudChannelConversations.tsx | 12 +- .../CloudChannelEnableConfirmation.tsx | 30 - src/pages/CloudAgents/CloudChannelPanel.tsx | 710 ++---------------- src/pages/CloudAgents/CloudRecovery.tsx | 12 +- src/pages/CloudAgents/CloudSchedules.tsx | 12 +- src/pages/CloudAgents/DraftEditor.tsx | 2 +- tests/e2e/cloud-agent-channels.spec.ts | 13 +- tests/unit/channel-accounts-page.test.tsx | 7 +- tests/unit/cloud-agents-main.test.ts | 25 +- tests/unit/cloud-agents-page.test.tsx | 13 + tests/unit/cloud-agents-workflows.test.tsx | 2 +- tests/unit/cloud-channel-enable.test.tsx | 31 - tests/unit/cloud-channel-panel.test.tsx | 304 +------- 21 files changed, 238 insertions(+), 1078 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260914-wechat-scan-owner-ml-b4169ea9.md delete mode 100644 src/pages/CloudAgents/CloudChannelEnableConfirmation.tsx delete mode 100644 tests/unit/cloud-channel-enable.test.tsx diff --git a/.project-docs/30-worklog/tasks/20260914-wechat-scan-owner-ml-b4169ea9.md b/.project-docs/30-worklog/tasks/20260914-wechat-scan-owner-ml-b4169ea9.md new file mode 100644 index 0000000..3c35a1f --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260914-wechat-scan-owner-ml-b4169ea9.md @@ -0,0 +1,65 @@ +# Task: 简化微信渠道账号连接和对话界面 + +## Identity + +- Task ID: 20260914-wechat-scan-owner-ml-b4169ea9 +- Mode: Feature +- Branch: codex/20260914-wechat-scan-owner-ml-b4169ea9-wechat-scan-owner +- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260914-wechat-scan-owner-ml-b4169ea9 +- Base commit: a721b8fb7e99eb8979fcb9c8c21a6540ebef911d +- Owner: codex +- Status: Ready for Integration + +## Scope + +- Simplify the MakeLore personal WeChat channel to one scanning owner per account while preserving multi-account routing, connection, pause/enable, disconnect, activity, conversation, attachment, and approval flows. +- Remove the audience selector, contact invitations, caller authorization management, and their obsolete Renderer, shared operation, Electron Main route, recovery, and test surfaces. +- Update visible channel wording from creator/self-specific labels to account-scoped WeChat conversation wording. +- Own only `shared/cloud-agents.ts`, the cloud-agent Electron service/operation/journal surfaces, CloudAgents channel Renderer components, their focused unit/Electron tests, the root README product contract, and this task-scoped record. Yuxi and canonical project memory remain read-only. + +## Intent And Constraints + +- Concurrent Task Gate passed for the exact registered task, feature mode, branch, base, and isolated worktree. Planning Gate passed after reading the required project memory, accepted personal-cloud-agent decision, relevant indexes, and peer scopes. +- The scanning WeChat identity is the only caller for its account. Accounts remain independently connected and routed, multiple accounts may target one published Agent, account sessions stay isolated, and the creator remains the payer. +- Existing invited sessions are not reclassified as the scanning identity. Keep the existing self-history backend endpoint naming because it still projects only the actual scanning identity. +- Removed product operations must not remain callable from the typed Host/Main boundary or recoverable from the UI. A legacy recovery record for a removed operation must be explicitly discarded without invoking the removed API or blocking unrelated account operations; do not add a compatibility framework. +- Preserve all unrelated changes and do not modify, commit, merge, push, deploy, package, or claim live WeChat acceptance. + +## Outcome + +- Replaced the channel detail panel's connection/audience/caller-management surface with account-scoped activity and WeChat conversation tabs. The user-level channel page remains the single place for account creation, QR connection and verification, published-Agent routing, enable/pause, and disconnect. +- Removed the four retired Yuxi operations (`channelPolicy`, `createChannelPairing`, `channelCallers`, and `revokeChannelCaller`) from shared operation types and Electron Main route validation/projection. Removed their policy/pairing DTOs and account-level audience fields. +- Removed invitation, audience, caller revocation, and enable-confirmation UI/tests. Updated channel, conversation, schedule-notification, and Agent workspace copy to use account-scoped “微信对话” wording without creator/self or invited-contact labels. +- Preserved `channelSelfCallers` and its fixed self-only server query for scheduled notification targets, and preserved channel history, controls/approval, files, activity/delivery, and retry operations. Existing invited sessions remain excluded by the server's scanning-identity projection rather than being relabeled. +- Legacy journal entries for the four removed operations are deleted locally by Main with an explicit `retired` result before any operation plan or network request. The global recovery UI exposes only “丢弃旧记录” for them; the account detail panel excludes them from its file-retry gate. +- Updated the root README channel contract to describe one scanning identity per account, account-isolated conversations, no invitation/caller management, creator billing, retained account controls/activity/files/approval, and the existing deployment boundary. +- The authorized fresh read-only Reviewer completed Standards and Spec review with PASS. Both findings were remediated and rechecked, with no unresolved P1/P2 issue. +- No merge, push, package, deployment, production mutation, or live WeChat request was performed. Implementation is ready for integration. + +## Verification + +- `pnpm install --frozen-lockfile`: passed with repository-pinned pnpm 10.33.4; dependencies were linked from the local store. +- Focused Vitest: 6 files / 71 tests passed, covering channel UI, multi-account routing, conversation/approval/file behavior, Main operation removal, and legacy journal discard. +- `pnpm run typecheck`: passed. +- Changed-file ESLint: passed with no output. +- `pnpm run lint:check`: passed with 0 errors and 8 warnings in unchanged files. +- `pnpm run build:vite`: passed for Renderer, Electron Main, Preload, and utility targets after the final edits; only existing chunk/dynamic-import warnings were reported. +- `pnpm exec playwright test tests/e2e/cloud-agent-channels.spec.ts`: 1/1 passed after the final build. The Electron scenario covers QR verification, two accounts routed to one Agent, route switching without a new login, activity/conversation labels, approval, artifact save, compact layout, and absence of retired calls/UI. +- `pnpm test`: 235 files passed, 1 file failed; 2,048 tests passed, 2 skipped, 1 failed. The sole failure is outside this task in `tests/unit/pi-managed-worker-opener.test.ts`, which expects `makelore-runtime-v6.mjs` while the base product emits `makelore-runtime-v7.mjs`. An isolated rerun reproduced the same 1/2 failure. `tests/unit/coding-chat-pressure.test.tsx` passed 1/1 when run separately because the scripted full suite stopped before it. +- `git diff --check`: passed after the Reviewer-requested README and task-record update. +- `check_doc_drift.py --task-id 20260914-wechat-scan-owner-ml-b4169ea9`: passed after the Reviewer-requested README and task-record update. + +## Follow-ups + +- A compatible Yuxi implementation and real connected-WeChat smoke remain external release requirements. +- The authorized fresh read-only Reviewer identified and then closed the stale README contract finding. Final Standards/Spec verdict: PASS, with no unresolved P1/P2 issue. +- The unrelated stale Pi runtime filename assertion remains outside this task. + +## Promotion Candidates + +- Target: `.project-docs/10-decisions/ADR-2026-09-11-personal-cloud-agents.md`, `.project-docs/20-architecture/system-overview.md`, and `.project-docs/40-domain/business-rules.md`. +- Proposal: replace the invitation/audience policy with one scanning owner per independently connected WeChat account; retain multi-account routing, creator billing, account-isolated sessions, activity/delivery, and existing self-history endpoint naming. +- Evidence: this task's focused unit/Electron tests, typecheck, lint, and production build; live WeChat remains explicitly unverified. +- Future impact: future client work must not restore invitation, audience-policy, or caller-revocation product surfaces without a new decision. +- Semantic conflicts: this intentionally amends the accepted Personal WeChat Publication invitation behavior while preserving User-Owned WeChat Accounts routing and isolation. +- Human confirmation required: already confirmed for product behavior; canonical promotion still requires Integration Gate ownership. diff --git a/README.md b/README.md index 10bd15a..dadb467 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Makelore 是一个面向软件、视觉创作、智能机器人与个人云智 云知识库的向量模型由平台管理员配置:Yuxi 后台「智能体管理 → 模型供应商」启用 embedding 模型并填写实际维度;Works Square 运营后台「模型管理」从 one-api 刷新并激活相同模型 ID。one-api 必须支持该模型的向量接口。客户端区分目录加载中、加载失败与无可用模型,空状态提供管理员配置说明;「刷新知识库与模型」保留未提交名称,未确认的创建请求沿用原操作身份。 - Agents 侧栏的「渠道」统一管理个人微信账号。每个账号独立连接,可以在发布智能体前先扫码,之后选择或更换目标智能体;多个微信账号可以使用同一个已发布智能体,切换目标不需要重新扫码。账号可单独启用、暂停、重新连接或断开。Agent 的「发布与访问」只展示已关联渠道并提供定位入口。选中账号的二级详情保留联系人邀请与撤销、运行/投递活动、失败文件补发和本人微信会话;本人会话可审批、停止、新开一段并保存实际产物,受邀联系人的内容不会因创建者付费而对创建者开放。绑定时扫码的微信可直接发送消息,无需本人配对码,其他联系人仍需创建者邀请。自动任务可明确选择把结果发送到已经开始对话的本人微信,暂停保留目标;费用统计包含微信渠道。渠道需要配套版本的 AgentBus Core/微信 Adapter、Yuxi 和 Works Square,桌面用户无需填写服务凭据。Main 保管操作恢复记录,重开界面不会自动重发绑定或执行请求;验证码不写入恢复记录。 + Agents 侧栏的「渠道」统一管理个人微信账号。每个账号由唯一扫码微信身份连接,可以在发布智能体前先扫码,之后选择或更换目标智能体;多个微信账号可以使用同一个已发布智能体,切换目标不需要重新扫码。账号可单独启用、暂停、重新连接或断开,不提供联系人邀请或授权调用者管理。Agent 的「发布与访问」只展示已关联渠道并提供定位入口。选中账号的二级详情保留运行/投递活动、失败文件补发和微信对话;对话可审批、停止、新开一段并保存实际产物,不同账号的会话相互隔离。自动任务可明确选择把结果发送到已经开始对话的微信账号,暂停保留目标;所有模型调用仍由智能体创建者支付,费用统计包含微信渠道。渠道需要配套版本的 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-agent-operations.ts b/electron/services/cloud-agent-operations.ts index fa0194b..3ea600f 100644 --- a/electron/services/cloud-agent-operations.ts +++ b/electron/services/cloud-agent-operations.ts @@ -15,7 +15,7 @@ const mcpInput = ['name', 'description', 'transport', 'url', 'headers']; const mcpOutput = ['slug', 'name', 'description', 'transport', 'url', 'enabled', 'has_credentials']; const child = ['name', 'purpose', 'system_prompt']; const channelView = ['id', 'address', 'display_name', 'target_agent_address', 'route_revision', 'provider_generation', 'binding_id', 'enabled', 'status', 'worker_online', - 'agent_slug', 'published_version', 'desired_state', 'sync_state', 'health', 'last_confirmed_at', 'revision', 'blockers', 'access_mode', 'policy_revision', + 'agent_slug', 'published_version', 'desired_state', 'sync_state', 'health', 'last_confirmed_at', 'revision', 'blockers', 'adoption_state', 'managed_agent_slug', 'target_agent_slug', 'target_agent_name', 'target_agent_published_version']; const channelSession = ['session_id', 'caller_id', 'agent_slug', 'agent_name', 'binding_id', 'provider_generation', 'channel_account_id', 'core_conversation_id', 'access_mode', 'grant_state', 'session_state', 'sequence', 'content_uid', 'thread_id', 'created_at', 'closed_at']; const channelCaller = ['caller_id', 'binding_id', 'provider_generation', 'channel_account_id', 'agent_slug', 'core_conversation_id', 'access_mode', 'grant_state', 'paired_ws_account_id', 'session_id', 'content_uid', 'thread_id', 'session_state', 'created_at', 'revoked_at']; @@ -62,15 +62,6 @@ function projectMutation(value: unknown): unknown { ...(source.disconnected === undefined ? {} : { disconnected: source.disconnected === true }), }; } -function projectPolicy(value: unknown): unknown { - const source = object(value); - return { - ...copy(source, ['binding_id', 'channel_account_id', 'access_mode', 'policy_revision']), - revoked_caller_ids: Array.isArray(source.revoked_caller_ids) ? source.revoked_caller_ids.filter((item): item is string => typeof item === 'string').slice(0, 200) : [], - revoked_invitation_ids: Array.isArray(source.revoked_invitation_ids) ? source.revoked_invitation_ids.filter((item): item is string => typeof item === 'string').slice(0, 200) : [], - }; -} -function projectPairing(value: unknown): unknown { return copy(value, ['invitation_id', 'binding_id', 'provider_generation', 'agent_slug', 'kind', 'expires_at', 'consumed', 'code']); } function projectWechat(value: unknown): unknown { const source = object(value); const qrcode_url = safeHttps(source.qrcode_url); @@ -185,9 +176,9 @@ function validateChannelInput(operation: string, args: ObjectRecord): void { 'channelAccountWechatBindVerification', 'channelAccountWechatBindAccount', 'channelAccountWechatUnbind', 'channelAccountOperation', 'createChannelBinding', 'switchChannelBindingAgent', 'enableChannelBinding', 'pauseChannelBinding', - 'disconnectChannelBinding', 'channelPolicy', 'createChannelPairing', 'wechatBindStart', + 'disconnectChannelBinding', 'wechatBindStart', 'wechatBindStatus', 'wechatBindVerification', 'wechatBindAccount', 'wechatUnbind', 'channelSelfCallers', - 'channelCallers', 'revokeChannelCaller', 'channelActivity', 'channelConversations', 'channelConversation', + 'channelActivity', 'channelConversations', 'channelConversation', 'channelFiles', 'channelConversationControl', 'channelDelivery', 'retryChannelDeliveryPart', 'channelOperation', ]); if (channelOperations.has(operation)) { @@ -206,8 +197,6 @@ function validateChannelInput(operation: string, args: ObjectRecord): void { && (typeof args.target_agent_slug !== 'string' || !/^ml-[a-f0-9]{32}$/.test(args.target_agent_slug))) throw new Error('invalid_id'); if (operation === 'routeChannelAccount' && args.target_agent_slug === null && args.enabled !== false) throw new Error('invalid_input'); if (operation === 'switchChannelBindingAgent' && (typeof args.target_agent_slug !== 'string' || !/^ml-[a-f0-9]{32}$/.test(args.target_agent_slug))) throw new Error('invalid_id'); - if (operation === 'channelPolicy' && !['self_only', 'invited'].includes(String(args.access_mode))) throw new Error('invalid_input'); - if (operation === 'createChannelPairing' && !['self', 'invite'].includes(String(args.kind))) throw new Error('invalid_input'); if (operation === 'wechatBindStart' && typeof args.force !== 'boolean') throw new Error('invalid_input'); if (operation === 'channelAccountWechatBindStart' && typeof args.force !== 'boolean') throw new Error('invalid_input'); if (operation === 'wechatBindVerification' && (typeof args.verify_code !== 'string' || !/^[0-9A-Za-z-]{1,128}$/.test(args.verify_code))) throw new Error('invalid_input'); @@ -302,16 +291,12 @@ const operations: Record = { enableChannelBinding: { method: 'POST', path: '/agents/:slug/channel-bindings/:channel_account_id/enable', body: ['operation_id', 'expected_revision'], output: ['channel'], project: projectMutation }, pauseChannelBinding: { method: 'POST', path: '/agents/:slug/channel-bindings/:channel_account_id/pause', body: ['operation_id', 'expected_revision'], output: ['channel'], project: projectMutation }, disconnectChannelBinding: { method: 'POST', path: '/agents/:slug/channel-bindings/:channel_account_id/disconnect', body: ['operation_id', 'expected_revision'], output: ['channel', 'wechat', 'disconnected'], project: projectMutation }, - channelPolicy: { method: 'PATCH', path: '/agents/:slug/channel-bindings/:channel_account_id', body: ['operation_id', 'access_mode', 'expected_policy_revision'], output: ['binding_id', 'channel_account_id', 'access_mode', 'policy_revision', 'revoked_caller_ids', 'revoked_invitation_ids'], project: projectPolicy }, - createChannelPairing: { method: 'POST', path: '/agents/:slug/channel-bindings/:channel_account_id/pairings', body: ['operation_id', 'kind'], output: ['invitation_id', 'binding_id', 'provider_generation', 'agent_slug', 'kind', 'expires_at', 'consumed', 'code'], project: projectPairing }, wechatBindStart: { method: 'POST', path: '/agents/:slug/channel-bindings/:channel_account_id/wechat-bind/start', body: ['operation_id', 'force'], output: ['session_key', 'status', 'message', 'qrcode_url'], project: projectWechat }, wechatBindStatus: { method: 'GET', path: '/agents/:slug/channel-bindings/:channel_account_id/wechat-bind/status', query: { session_key: 'session_key' }, output: ['session_key', 'status', 'message', 'qrcode_url'], project: projectWechat }, wechatBindVerification: { method: 'POST', path: '/agents/:slug/channel-bindings/:channel_account_id/wechat-bind/verification', body: ['operation_id', 'session_key', 'verify_code'], output: ['session_key', 'status', 'message', 'qrcode_url'], project: projectWechat }, wechatBindAccount: { method: 'GET', path: '/agents/:slug/channel-bindings/:channel_account_id/wechat-bind/account', output: ['status', 'message'] }, wechatUnbind: { method: 'POST', path: '/agents/:slug/channel-bindings/:channel_account_id/wechat-bind/unbind', body: ['operation_id'], output: ['status', 'message'] }, channelSelfCallers: { method: 'GET', path: '/agents/:slug/channel-callers', fixedQuery: { access_mode: 'self_only' }, output: ['items'], project: projectCallers }, - channelCallers: { method: 'GET', path: '/agents/:slug/channel-bindings/:channel_account_id/callers', output: ['items'], project: projectCallers }, - revokeChannelCaller: { method: 'POST', path: '/agents/:slug/channel-bindings/:channel_account_id/callers/:caller_id/revoke', body: ['operation_id'], output: ['status', 'caller_id'] }, channelActivity: { method: 'GET', path: '/agents/:slug/channel-bindings/:channel_account_id/activity', query: { offset: 'offset', limit: 'limit' }, output: ['items', 'next_offset'], project: projectActivity }, channelConversations: { method: 'GET', path: '/channel-conversations', query: { slug: 'agent_slug', offset: 'offset' }, output: ['items', 'next_offset'], project: value => ({ items: list(object(value).items, projectSession), next_offset: object(value).next_offset ?? null }) }, channelConversation: { method: 'GET', path: '/channel-conversations/:session_id', query: { offset: 'offset' }, output: ['session_id', 'caller_id', 'agent_slug', 'messages', 'run', 'queued_requests', 'next_offset'], project: projectConversation }, diff --git a/electron/services/cloud-agents.ts b/electron/services/cloud-agents.ts index 56e3f96..e2e85b5 100644 --- a/electron/services/cloud-agents.ts +++ b/electron/services/cloud-agents.ts @@ -1,5 +1,12 @@ import { EMPTY_CLOUD_CONFIGURATION, type CloudAgentConfiguration, type CloudAgentDraft, type CloudAgentPage, type CreateCloudAgent, type SaveCloudAgentDraft, type CloudUpload } from '../../shared/cloud-agents'; import { operationPlan } from './cloud-agent-operations'; + +const retiredChannelOperations = new Set([ + 'channelPolicy', + 'createChannelPairing', + 'channelCallers', + 'revokeChannelCaller', +]); import { CloudAgentJournal } from './cloud-agent-journal'; import type { CloudRecent } from '../../shared/cloud-agents'; import { WORKS_SQUARE_CONFIG } from '../api/works-config'; @@ -45,8 +52,6 @@ const MESSAGES: Record = { channel_session_control_failed: '微信会话操作失败,请刷新后重试', channel_delivery_not_found: '渠道交付记录不存在', channel_delivery_retry_failed: '文件补发失败,请刷新交付记录后重试', - self_pairing_removed: '本人无需配对,请直接使用绑定时扫码的微信发送消息', - pairing_expired: '邀请码已过期,请重新生成', verification_required: '请先完成微信验证', artifact_unavailable: '文件已不可用,请刷新文件列表', operation_unavailable: '操作状态暂时无法确认,请稍后查询', @@ -61,10 +66,6 @@ const MESSAGES: Record = { run_active: '当前会话仍在执行,请稍后重试', run_not_active: '当前会话没有可操作的运行', channel_session_unavailable: '新的微信会话暂时无法建立', - pairing_invalid: '配对请求无效,请刷新后重试', - invitation_invalid: '邀请码无效或已撤销', - invitation_consumed: '邀请码已经使用', - invites_disabled: '当前渠道仅允许本人使用', agentbus_unavailable: '渠道服务暂时不可用,请稍后重试', }; @@ -253,6 +254,10 @@ export class CloudAgentsModule { await this.journal.remove(binding.accountKey, id, () => this.requireCurrent(binding)); return { discarded: true }; } + if (retiredChannelOperations.has(pending.operation)) { + await this.journal.remove(binding.accountKey, id, () => this.requireCurrent(binding)); + return { operation: pending.operation, input: pending.input, discarded: true, retired: true }; + } if (pending.operation === 'wechatBindVerification' || pending.operation === 'channelAccountWechatBindVerification') { // The verification code is intentionally never persisted. First recover the // durable provider receipt: the code may already have been consumed even if @@ -337,7 +342,7 @@ export class CloudAgentsModule { 'createChannelAccount', 'routeChannelAccount', 'enableChannelAccount', 'pauseChannelAccount', 'disconnectChannelAccount', 'channelAccountWechatBindStart', 'channelAccountWechatBindVerification', 'channelAccountWechatUnbind', 'createChannelBinding', 'switchChannelBindingAgent', 'enableChannelBinding', 'pauseChannelBinding', 'disconnectChannelBinding', - 'channelPolicy', 'createChannelPairing', 'wechatBindStart', 'wechatBindVerification', 'wechatUnbind', 'revokeChannelCaller', + 'wechatBindStart', 'wechatBindVerification', 'wechatUnbind', 'channelConversationControl', 'retryChannelDeliveryPart']; let result: unknown; if (recoverable.includes(plan.operation)) { diff --git a/shared/cloud-agents.ts b/shared/cloud-agents.ts index dafa870..ac34a02 100644 --- a/shared/cloud-agents.ts +++ b/shared/cloud-agents.ts @@ -173,8 +173,6 @@ export interface CloudChannelView { last_confirmed_at?: string | null; revision?: number; blockers?: string[]; - access_mode?: string; - policy_revision?: number; adoption_state?: 'available' | 'managed_elsewhere' | 'managed_external' | string; managed_agent_slug?: string | null; target_agent_slug?: string | null; @@ -205,8 +203,6 @@ export interface CloudUserChannelAccount { target_agent_slug: string | null; target_agent_name: string | null; target_agent_published_version: number | null; - access_mode?: string; - policy_revision?: number; provider_generation?: string; desired_state?: string; last_confirmed_at?: string | null; @@ -291,16 +287,6 @@ export interface CloudChannelActivity { input_preparation?: { state?: string; error_type?: string | null }; } export interface CloudChannelActivityPage { items: CloudChannelActivity[]; next_offset: number | null } -export interface CloudChannelPairing { - invitation_id?: string; - binding_id: string; - provider_generation: string; - agent_slug?: string; - kind: 'self' | 'invite' | string; - expires_at?: string; - consumed: boolean; - code?: string; -} export interface CloudWechatBindState { session_key: string; status: string; @@ -323,14 +309,6 @@ export interface CloudChannelDelivery { } export interface CloudChannelWorker { account_id: string; address: string } export interface CloudChannelMutation { channel: CloudChannelView; target_agent?: string | CloudChannelView; worker?: CloudChannelWorker; wechat?: Record; disconnected?: boolean } -export interface CloudChannelPolicy { - binding_id: string; - channel_account_id: string; - access_mode: 'self_only' | 'invited' | string; - policy_revision: number; - revoked_caller_ids: string[]; - revoked_invitation_ids: string[]; -} export interface CloudChannelControl { operation_id: string; status: string; session_id: string; action: 'resume' | 'stop' | 'new-session'; previous_session_id?: string; @@ -433,16 +411,12 @@ export interface CloudAgentOperations { enableChannelBinding: Op; pauseChannelBinding: Op; disconnectChannelBinding: Op; - channelPolicy: Op; - createChannelPairing: Op; wechatBindStart: Op; wechatBindStatus: Op; wechatBindVerification: Op; wechatBindAccount: Op; wechatUnbind: Op; channelSelfCallers: Op; - channelCallers: Op; - revokeChannelCaller: Op; channelActivity: Op; channelConversations: Op; channelConversation: Op<{ session_id: string; offset?: number }, CloudChannelConversation>; diff --git a/src/lib/cloud-agents-api.ts b/src/lib/cloud-agents-api.ts index d87acec..06da271 100644 --- a/src/lib/cloud-agents-api.ts +++ b/src/lib/cloud-agents-api.ts @@ -7,7 +7,7 @@ export const cloudAgentsApi = { remember: (recent: CloudRecent | null) => hostApiFetch(CLOUD_AGENTS_PATH + '/recovery/recent', { method: 'PUT', body: JSON.stringify(recent), }), - resolvePending: (id: string, discard = false) => hostApiFetch<{ result?: unknown; operation?: string; input?: Record; discarded?: boolean; requires_input?: boolean; pending?: boolean }>( + resolvePending: (id: string, discard = false) => hostApiFetch<{ result?: unknown; operation?: string; input?: Record; discarded?: boolean; retired?: boolean; requires_input?: boolean; pending?: boolean }>( CLOUD_AGENTS_PATH + '/recovery/pending', { method: 'POST', body: JSON.stringify({ id, discard }) }), uploadSkill: () => hostApiFetch(CLOUD_AGENTS_PATH + '/skills/pick', { method: 'POST' }), uploadKnowledge: (slug: string, kb_id: string, operation_id: string, replaces_file_id?: string) => diff --git a/src/pages/CloudAgents/ChannelAccounts.tsx b/src/pages/CloudAgents/ChannelAccounts.tsx index 1f83ebc..8f561d9 100644 --- a/src/pages/CloudAgents/ChannelAccounts.tsx +++ b/src/pages/CloudAgents/ChannelAccounts.tsx @@ -207,9 +207,9 @@ export function ChannelAccounts() { {qr.status === 'verification_required' && <> setVerifyCode(event.target.value)} placeholder="输入微信验证码" />} } - {confirmDisconnect &&

确认断开这个微信账号?

历史权限、活动和会话仍保留;再次使用需要重新扫码。

} - {selected.target_agent_slug && selected.binding_id ?
- :

选择已发布的目标智能体后,可在这里管理联系人权限、活动记录和本人微信会话。

+ {confirmDisconnect &&

确认断开这个微信账号?

历史活动和会话仍保留;再次使用需要重新扫码。

} + {selected.target_agent_slug && selected.binding_id ?
+ :

选择已发布的目标智能体后,可在这里查看活动记录和微信对话。

} }
} diff --git a/src/pages/CloudAgents/CloudAccess.tsx b/src/pages/CloudAgents/CloudAccess.tsx index 6dcc9a3..1aba5b1 100644 --- a/src/pages/CloudAgents/CloudAccess.tsx +++ b/src/pages/CloudAgents/CloudAccess.tsx @@ -120,7 +120,7 @@ function LinkedChannels({ slug, activeView }: { slug: string; activeView: boolea }, [activeView, slug]); return

已关联渠道

-

微信账号统一在“渠道”中连接、切换目标和管理使用权限。

+

微信账号统一在“渠道”中连接、切换目标和查看对话活动。

{error &&

{error}

} {!error && items.length === 0 &&

暂时没有关联的微信账号。打开渠道后会预选当前智能体。

} diff --git a/src/pages/CloudAgents/CloudChannelConversations.tsx b/src/pages/CloudAgents/CloudChannelConversations.tsx index 424b05f..7dbe958 100644 --- a/src/pages/CloudAgents/CloudChannelConversations.tsx +++ b/src/pages/CloudAgents/CloudChannelConversations.tsx @@ -51,14 +51,14 @@ export function CloudChannelConversations({ slug, channelAccountId, activeView = setSessions(items => [...items, ...next]); setOffset(page.next_offset); } catch (error) { setError(failureText(error)); } finally { setLoading(false); } }; - return
-

本人微信对话

-

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

+ return
+

微信对话

+

查看扫码账号的微信会话,处理确认和保存文件。新消息请从对应微信对话发送。

{error &&

{error}

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

正在读取本人会话…

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

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

} - {!!sessions.length &&
} - {bindings.length > 1 && } - {binding &&
-

{displayAccount(binding)}

- {binding.blockers?.length ?

完成启用前还需要:{binding.blockers.map(blockerLabel).join('、')}

: null} - {!connected && !qrOpen && } - {qrOpen && qr && } - {connected && !qrOpen &&
{binding.desired_state === 'enabled' || binding.enabled ? : }
} - {confirmEnable && binding && onConfirmEnable(false)} />} - {confirmUnbind &&

确认解绑这个个人微信?

尚未发送的渠道交付会停止,历史活动保留;微信登录状态会被撤销。

} - {connected &&

切换到另一个智能体

切换会先停止当前渠道的投递,再把这个微信账号路由到目标智能体。需要明确选择并确认。

{!routePickerOpen && }{routePickerOpen &&
{!routeAgents.length &&

没有其他已发布的个人智能体可切换。

}
}
} -
} - - ; -} - -function QrCard({ qr, verificationCode, busy, onClose, onRestart, onVerificationCode, onVerify }: { qr: CloudWechatBindState; verificationCode: string; busy: boolean; onClose: () => void; onRestart: () => void; onVerificationCode: (value: string) => void; onVerify: () => void }) { - return
{qr.qrcode_url ? : }

{statusLabel(qr.status)}

二维码只在当前页面轮询,关闭页面不会重复扫码。

{qr.message &&

{qr.message}

}
{qr.status === 'verification_required' &&
}
扫码成功后会自动确认连接
; -} - -function StepHeading({ number, title, description }: { number: string; title: string; description: string }) { - return
{number}

{title}

{description}

; -} - -function AccessStep({ binding, accessMode, callers, pairing, busy, busyAction, detailOnly = false, onMode, onSave, onInvite, onClearPairing, onRevoke }: { binding: CloudChannelView | null; accessMode: 'self_only' | 'invited'; callers: CloudChannelCaller[]; pairing: CloudChannelPairing | null; busy: boolean; busyAction: string; detailOnly?: boolean; 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
{!detailOnly && } - {!binding ?

请先完成微信连接。

: <> -
-
{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.code}`); }; - return

一次性邀请码

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

{value.code ?
{value.code}
:

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

}
; -} - function ActivityStep({ slug, channelAccountId, refreshKey, activity, nextOffset, busy, onRefresh, onMore, onRetry }: { slug: string; channelAccountId: string; refreshKey: number; activity: CloudChannelActivity[]; nextOffset: number | null; busy: boolean; onRefresh: () => void; onMore: () => void; onRetry: (partId: string, messageId: string) => void; }) { - return

活动记录

显示执行阶段、费用和微信投递阶段;受邀者的提问标题与正文不会出现在这里。

{!activity.length &&

还没有渠道活动。

}{activity.map((item, index) => )}{nextOffset != null && }
; + return

活动记录

显示这个微信账号的执行、费用和投递阶段。

{!activity.length &&

还没有渠道活动。

}{activity.map((item, index) => )}{nextOffset != null && }
; } function ActivityItem({ slug, channelAccountId, refreshKey, item, busy, onRetry }: { diff --git a/src/pages/CloudAgents/CloudRecovery.tsx b/src/pages/CloudAgents/CloudRecovery.tsx index 0aa650b..0cb1c84 100644 --- a/src/pages/CloudAgents/CloudRecovery.tsx +++ b/src/pages/CloudAgents/CloudRecovery.tsx @@ -13,6 +13,7 @@ const labels: Record = { pauseChannelAccount: '暂停渠道', disconnectChannelAccount: '断开微信账号', channelAccountWechatBindStart: '微信扫码连接', channelAccountWechatUnbind: '解除微信登录', }; +const retiredChannelOperations = new Set(['channelPolicy', 'createChannelPairing', 'channelCallers', 'revokeChannelCaller']); export function CloudRecovery({ pending, onResolved }: { pending: CloudPendingOperation[]; onResolved: () => void }) { const [busy, setBusy] = useState(''); @@ -26,6 +27,9 @@ export function CloudRecovery({ pending, onResolved }: { pending: CloudPendingOp if (response.requires_input) { setResult(null); setNotice('这次微信验证还需要验证码。请进入「渠道 → 微信」,选择对应账号后继续连接验证。'); + } else if (response.retired) { + setResult(null); + setNotice('旧版联系人权限操作已移除,本机恢复记录已丢弃,微信账号的其他操作不受影响。'); } else if (!discard) setResult({ operation: item.operation, value: response.result }); onResolved(); } catch (e) { setError(e instanceof Error ? e.message : '暂时无法确认结果'); } @@ -36,10 +40,12 @@ export function CloudRecovery({ pending, onResolved }: { pending: CloudPendingOp {pending.length > 0 && <>

有 {pending.length} 个操作的结果尚未确认

重试会确认原操作,不会新建一份任务。移除提醒只清除本机记录,云端已接受的任务仍会继续。

} {pending.map(item =>
-

{labels[item.operation] ?? '云端操作'} · {new Date(item.created_at).toLocaleString()}

+

{retiredChannelOperations.has(item.operation) ? '旧版联系人权限操作(已移除)' : labels[item.operation] ?? '云端操作'} · {new Date(item.created_at).toLocaleString()}

{typeof item.input.query === 'string' &&

{item.input.query}

}
-
-
+ {retiredChannelOperations.has(item.operation) + ? + :
+
}
)} {result &&

{labels[result.operation] ?? '操作'}的结果已确认。{result.operation === 'createKey' && '请保存下面的密钥,关闭后不再显示。'}

查看结果 diff --git a/src/pages/CloudAgents/CloudSchedules.tsx b/src/pages/CloudAgents/CloudSchedules.tsx index 9301d57..dcdfc14 100644 --- a/src/pages/CloudAgents/CloudSchedules.tsx +++ b/src/pages/CloudAgents/CloudSchedules.tsx @@ -41,7 +41,7 @@ export function CloudSchedules({ slug, onOpen, proposal, onProposalClosed }: { s

{job.name}

{job.prompt}

{job.enabled ? '已启用' : '已暂停'}

{scheduleLabel(job.cron_expression)} · {job.timezone} · 下次 {job.enabled && job.next_run_at ? new Date(job.next_run_at).toLocaleString() : '—'}

-

{job.result_notification?.enabled ? '结果同时发送到指定的本人微信对话' : '结果保存在客户端任务历史'}

+

{job.result_notification?.enabled ? '结果同时发送到指定的微信对话' : '结果保存在客户端任务历史'}

} @@ -148,7 +148,7 @@ function ScheduleNotification({ slug, value, onChange }: { const available = options.some(item => item.caller_id === selected && item.channel_account_id === value?.channel_account_id); return

结果通知

-
+
-

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

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

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

} +

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

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

还没有可用的微信对话。请先在“渠道 → 微信”中完成连接并启用,再用扫码的微信发送一条消息,然后回来刷新。

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

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

} {error &&

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

}
; diff --git a/src/pages/CloudAgents/DraftEditor.tsx b/src/pages/CloudAgents/DraftEditor.tsx index 0f576b4..bac5eb6 100644 --- a/src/pages/CloudAgents/DraftEditor.tsx +++ b/src/pages/CloudAgents/DraftEditor.tsx @@ -147,7 +147,7 @@ export function DraftEditor({ initial, recent, onBack, onSaved }: { {visited.has('chat') && (saved.published_version ?