# WeBridge 服务端 WebSocket 接入 本文档版次为 `8080`,沿用 `8079` 的业务接口约定;当前插件主版本为 `8.0.80`,本次构建为 `8.0.80-2`。用户已确认现有全部业务功能在微信 `8.0.80` 验证通过;下文保留旧版本的具体真机记录。新增的设置页检查更新仍需单独完成真机弹窗与浏览器下载联调。示例中的当前版本和构建标记用于说明格式,排错时以设备实际返回为准。 本文适用插件 `8.0.80`。选择本交互模式后,仅阅读本文即可完成配置、鉴权、命令调用、事件接收及结果处理;所有业务参数、返回与重试规则均在本篇。本地 HTTP 专用能力的限制及本模式替代方式也在本文说明。 ## 目录 **接入与基础协议** - [1. 这个模式是什么](#1-这个模式是什么) - [2. 你要实现什么](#2-你要实现什么) - [3. 连接与地址](#3-连接与地址) - [4. 连接生命周期](#4-连接生命周期) - [5. 帧总览](#5-帧总览) - [6. `hello` 帧详解](#6-hello-帧详解) - [7. `hello_ack` 帧详解](#7-hello_ack-帧详解) - [8. 心跳](#8-心跳) - [9. `event` 帧详解](#9-event-帧详解) - [10. `command` 帧详解](#10-command-帧详解) - [11. `command_result` 帧详解](#11-command_result-帧详解) - [12. 公共字段](#12-公共字段) - [13. 能力清单](#13-能力清单) **业务命令** - [14. 命令参数与返回](#14-命令参数与返回) - [14.1 参数通用规则](#141-参数通用规则) - [14.2 分类命令索引](#142-分类命令索引) - [14.3 消息发送与操作](#143-消息发送与操作) - [14.4 联系人查询与添加](#144-联系人查询与添加) - [好友支付姓名末字](#friend-pay-name) - [14.5 群聊信息与群昵称](#145-群聊信息与群昵称) - [指定群二维码](#get-chatroom-qrcode) - [14.6 红包与转账](#146-红包与转账) - [确认收款](#receive-transfer) - [退还与资金状态](#transfer-operations) - [14.7 资料设置与地区代码](#147-资料设置与地区代码) - [14.8 状态查询与后台保活](#148-状态查询与后台保活) - [后台保活诊断](#keepalive-diagnostics) - [14.9 本地 HTTP 专用入口与替代方式](#149-本地-http-专用入口与替代方式) - [15. 好友备注、星标和黑名单](#15-好友备注星标和黑名单) - [16. 创建群聊、直接加人与发送邀请](#16-创建群聊直接加人与发送邀请) - [17. 入群邀请:查询、接受与诊断](#17-入群邀请查询接受与诊断) - [18. 群管理员、群免打扰与个人二维码](#18-群管理员群免打扰与个人二维码) - [19. 媒体下载与上传](#19-媒体下载与上传) - [20. 公众号:搜索、关注与历史文章](#20-公众号搜索关注与历史文章) - [21. 朋友圈:调用、异步结果与后续操作](#21-朋友圈调用异步结果与后续操作) - [22. 收藏:分页读取、发送与删除](#22-收藏分页读取发送与删除) **事件、可靠性与排错** - [23. 事件 body 字段](#23-事件-body-字段) - [24. 离线队列与补发](#24-离线队列与补发) - [25. 断线重连](#25-断线重连) - [26. 幂等与去重](#26-幂等与去重) - [27. 常见问题排查](#27-常见问题排查) **兼容说明** - [附录:兼容命令与已移除功能](#附录兼容命令与已移除功能) --- ## 1. 这个模式是什么 插件作为 **WebSocket 客户端**主动连到你的服务端,在**一条持久连接**上双向收发: - 插件 → 你:事件上报(收到消息等)、命令执行结果、心跳 - 你 → 插件:命令下发、心跳 连接正常时可直接下发命令;实际完成时间还取决于网络、插件排队和业务等待。 **代价**:你的服务端必须实现 WebSocket 协议。 > **和"插件内置 HTTP 接口"的关系**:不冲突。你仍然可以同时通过手机上那个 HTTP 端口主动调插件。不同模式的选型对比可另看《接入总览与模式选择》,不影响按本文独立接入。 > **和"长轮询"的关系**:**两者互斥**,插件同一时刻只用一种传输模式。 --- ## 2. 你要实现什么 一个 WebSocket 端点,能: 1. 接受连接,**不需要你主动发任何东西** 2. 收到 `hello` 后回复 `hello_ack` 3. 随时推送 `command` 帧(可选,有命令时才推) 4. 接收 `event` 与 `command_result` 帧,**不需要回响应** 5. 正确处理 Ping/Pong(大多数 WebSocket 库自动处理) 6. 可选:发应用层 `ping` 帧,插件会回 `pong` **路径随便你定**,插件连的是你在设置页填的那个完整地址。 --- ## 3. 连接与地址 ### 3.1 地址怎么写 在插件设置页的「WebSocket 服务端地址」里填写。插件的处理规则: | 你填写的 | 插件实际连接的 | |---------|--------------| | `ws://192.168.0.100:8765` | `ws://192.168.0.100:8765` | | `wss://your-server.com` | `wss://your-server.com` | | `http://192.168.0.100:8765` | `ws://192.168.0.100:8765` | | `https://your-server.com` | `wss://your-server.com` | | `ws://192.168.0.100:8765/api` | `ws://192.168.0.100:8765/api` | **地址规范化规则**: | 规则 | 说明 | |------|------| | `http://` → `ws://`,`https://` → `wss://` | 只认这四种前缀,**其他一律原样使用** | | **去掉查询串和锚点** | `ws://h:8765/api?token=1#x` → `ws://h:8765/api` | | **去掉尾部斜杠** | `ws://h:8765/api/` → `ws://h:8765/api` | | **剥掉 `/agent` 或 `/agent/<端点名>` 后缀** | 你把 `ws://h:8765/agent/poll` 填进去,插件会自动变成 `ws://h:8765` | | **不补协议、不补端口** | 填 `192.168.0.1:8765`(没有 `ws://`)会被判为**地址无效**,插件不会自动帮你补 | > ⚠️ **两个最容易踩的坑**: > 1. **不要带 `/agent/poll` 之类的路径**。那是长轮询的端点名,填在 WS 地址里会被自动剥掉,可能让你以为"填对了"其实连到了别处。WS 只需填到你自己的端点路径为止。 > 2. **必须写全 `ws://` 或 `wss://`**。漏了协议前缀,插件只会静默记录"连接地址无效",不会给你任何界面提示。 ### 3.2 地址无效时会发生什么 | 情况 | 插件行为 | |------|---------| | 地址为空 | 记为"未配置连接地址",不连接 | | 地址无法解析 | 记为"连接地址无效",**不连接、不重试** | | 插件总开关关闭 | 记为"插件已关闭",不连接 | 这三种情况**都只体现在插件日志与诊断快照里**,不会主动通知你的服务端——因为根本连不上。 ### 3.3 单帧大小上限 插件接收单条 WebSocket 消息的上限是 **10 MiB**(10 × 1024² 字节),超过会被协议层拒绝。 > 下发命令须遵守接收上限。聊天媒体通过独立 HTTP 上传传输字节,不应把文件 Base64 放入命令帧。二维码业务可能返回 PNG Base64,按对应字段读取。 ### 3.4 设置与鉴权 | 项目 | 配置与判断 | |---|---| | 插件总开关 | 默认关闭;需在设置页启用,且插件授权有效,运行态 `plugin_enabled` 才为 true | | 传输模式 | 选择 WebSocket;默认模式为长轮询,两者互斥 | | 服务端地址 | 填写本节规则要求的完整 `ws://` 或 `wss://` 地址;默认空值不连接 | | 发送、转发与接收状态 | 当前都跟随插件总开关及有效授权,不再作为独立设置;旧诊断/兼容字段不表示仍有独立开关 | | 暂存 | 默认开启、500 条、120 秒;容量、有效期及关闭后的行为见离线队列章节 | 插件授权检查在手机端完成;失效或未启用时不会建立正常连接。命令分发器的授权/启用拒绝沿用 `command_result.ok:false`,`error` 可能为 `authorization required` 或 `plugin is disabled`,`result` 可保留 `authorized`、`auth_status`;不要假设所有前置失败都有完整业务字段。 当前 WS 握手不附带可配置的 `Authorization` 请求头,也没有 `hello.token` 或命令级 token 字段;配置地址的查询串会被移除,不能通过 `?token=...` 传鉴权。`device_id`、`session_id` 和 `account_id` 是关联信息,不能视为登录凭证。后端若要求凭据鉴权,应使用当前插件实际可用的网络入口策略,不能要求插件发送本协议未实现的字段。 --- ## 4. 连接生命周期 ``` 插件启动 / 传输模式切到 WebSocket │ ▼ 读取地址 → 规范化 → 校验 │ 地址无效 → 停止(只写日志) ▼ 建立 TCP + WS 握手(超时 15 秒) │ │ 连接成功(收到 Open) ├──► ① 立即发送 hello ├──► ② 立即启动心跳定时器(30 秒) ├──► ③ 立即尝试补发离线队列 └──► ④ 开始接收消息 │ ▼ 等待 hello_ack(超时 10 秒) │ 超时 → 断开 → 退避重连 │ 收到 → 握手完成(但注意:①②③ 早已执行) ▼ 正常运行:收 command / 发 event、command_result │ │ 断开(见第 25 节的全部触发条件) ▼ 退避重连 2s → 5s → 10s → 20s → 30s(封顶) ``` > ⚠️ **一个反直觉但很重要的细节**:上面 ①②③ 三步是在**连接建立(Open)时立即执行**的,**不是等 `hello_ack` 之后**。这意味着: > - 你的服务端可能在回 `hello_ack` 之前就先收到 `event` / `command_result` 帧。 > - 你的服务端也可能在回 `hello_ack` 之前就收到补发的离线数据。 > - **不要写"必须先发 hello_ack 才能处理后续帧"的逻辑**,那会丢数据。 --- ## 5. 帧总览 所有消息都是 **文本帧(Text Frame)**,内容是 UTF-8 编码的 JSON 对象,靠 `type` 字段区分。 ### 5.1 插件 → 服务端 | `type` | 说明 | 何时发 | |--------|------|--------| | `hello` | 握手 | 连接建立后立即 | | `event` | 事件上报 | 微信里发生事件时 | | `command_result` | 命令执行结果 | 执行完一条命令后 | | `pong` | 心跳响应 | 收到你的应用层 `ping` 时 | ### 5.2 服务端 → 插件 | `type` | 说明 | 是否必需 | |--------|------|---------| | `hello_ack` | 握手确认 | ✅ **必需**,不发会在 10 秒后被断开 | | `command` | 命令下发 | 有命令时才发 | | `ping` | 应用层心跳 | 可选 | ### 5.3 未知帧的处理 **插件收到不认识的 `type` 时:只记日志,不响应、不断开。** 这包括: - 你发了一个 `type` 拼错或插件不支持的帧 → 被忽略 - 你发了一段非法 JSON → 被当作未知类型忽略 > 后果:**这类错误插件不会报错给你**。你的服务端如果发现自己推的命令没被执行,先检查 `type` 和 JSON 拼写。 > 还有一个连带效应:如果你一直在发插件不认识的帧、始终没发 `hello_ack`,握手计时照走,**10 秒后连接会被断开**。 ### 5.4 二进制帧 插件发的是文本帧。接收时,插件也接受二进制帧并尝试按 UTF-8 解码——**但你没必要用二进制**,请统一用文本帧。 --- ## 6. `hello` 帧详解 连接建立后插件发送的第一条消息。 ```json { "type": "hello", "device_id": "webridge_device_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "session_id": "sess_1775000000123_a1b2c3d4e5f6", "account_id": "wxid_self", "runtime_version": "8.0.80-2", "build_marker": "webridge_build_20261106_settings_update_8080", "ts_ms": 1775000000123, "capabilities": ["send_message", "send_image", "…"], "state": { "is_logged_in": true, "account_id": "wxid_self", "auth_authorized": true, "plugin_enabled": true } } ``` | 字段 | 类型 | 恒有 | 说明 | |------|------|------|------| | `type` | string | ✅ | 固定 `"hello"` | | `device_id` | string | ✅ | 设备标识,格式 `webridge_device_`,**首次启动生成后持久化,重启不变** | | `session_id` | string | ✅ | 本次运行标识,格式 `sess_<毫秒>_<12位十六进制>`,**每次进程启动重新生成** | | `account_id` | string | ✅ | 当前登录的微信 wxid;未登录时可能为空串 | | `runtime_version` | string | ✅ | 插件版本号 | | `build_marker` | string | ✅ | 构建标记 | | `ts_ms` | number | ✅ | 发送时刻的 Unix 毫秒时间戳 | | `capabilities` | string[] | ✅ | 插件能力清单,见第 13 节 | | `state` | object | ✅ | 状态快照,**只有以下 4 个字段** | ### `state` 的 4 个字段 | 字段 | 类型 | 说明 | |------|------|------| | `is_logged_in` | bool | 微信是否已登录 | | `account_id` | string | 当前账号 wxid | | `auth_authorized` | bool | 插件授权是否有效 | | `plugin_enabled` | bool | 插件总开关 | `hello.state` 仅携带上表 4 项。需要有效发送状态、待发事件数量或队列诊断时,下发 `bridge_status` 命令;能直连手机的环境也可选用本地 `GET /health` 或 `GET /debug/bridge_status`。 ### 服务端该怎么处理 hello 1. 记录 `device_id`(长期身份)和 `session_id`(本次运行) 2. 记录 `capabilities`,避免下发它不支持的命令 3. **回复 `hello_ack`** 4. 用同一个 `session_id` 关联本次进程运行中的 `event` / `command_result`;同进程断线重连后继续使用该值 > `device_id` 是同一台手机的稳定标识,**适合做你的设备主键**;`session_id` 每次插件重启都会变,用于关联本次进程运行,不能据它区分同进程内的多次连接。 --- ## 7. `hello_ack` 帧详解 **这是你唯一必须主动发的帧。** 收到 `hello` 后回复: ```json { "type": "hello_ack", "server_version": "1.0.0", "ts_ms": 1775000000500 } ``` | 字段 | 类型 | 必需 | 说明 | |------|------|------|------| | `type` | string | ✅ | 固定 `"hello_ack"` | | `server_version` | string | — | 你的服务端版本号,插件会记录并显示在日志里 | | `ts_ms` | number | — | 你的服务器时间戳 | **除 `type` 外都不必需。** 最简写法: ```json { "type": "hello_ack" } ``` ### 超时后果 **10 秒内没收到 `hello_ack` → 插件断开连接并进入退避重连。** ### 想把连接踢掉的正确做法 **当前协议没有"拒绝握手"的帧类型。** 如果你校验 `device_id` / 鉴权失败,想拒绝这个连接: - ✅ 直接发 **WebSocket Close 帧**关闭连接。插件会把它当作"连接被关闭",进入重连。 - ⚠️ 不要"什么都不回"——那只会让插件等 10 秒超时,行为一样但更慢,且日志里只会显示"hello_ack 超时",看不出真实原因。 - 💡 建议在 Close 的原因(reason)里写明原因,插件会把它记进日志,便于现场排查。 --- ## 8. 心跳 本节心跳只维持和检查 WebSocket 连接,依赖微信进程获得执行时间,不能替代 iOS 后台保活。音频及定位保活另由设置页控制,见 [保活诊断与测试](#keepalive-diagnostics)。 ### 8.1 插件 → 服务端:协议层 Ping 插件每 **30 秒**发送一次 **WebSocket 协议层 Ping 帧**(不是 JSON 消息)。 **你必须让 Pong 正常返回**——绝大多数 WebSocket 库会自动处理,一般无需写代码。 **心跳在连接建立时就启动**(早于 `hello_ack`)。如果 Ping 收不到 Pong,插件会断开并重连。 插件发出 Ping 后最多等待 **10 秒**;未取得本次 Pong 回调或底层回调报错时,断开并退避重连。微信回到前台时会立即探测现有连接;探测成功后继续补发暂存队列。iOS 挂起期间计时及回调可能延后,不能用这两个时长承诺后台持续在线。 ### 8.2 服务端 → 插件:应用层 ping(可选) 你也可以发应用层的心跳: ```json { "type": "ping", "ts_ms": 1775000000123 } ``` 插件会立即回: ```json { "type": "pong", "ts_ms": 1775000000200 } ``` > `ts_ms` 是**插件回 pong 的时刻**,不是回显你发来的值。别用它算 RTT,请用协议层 Ping/Pong。 ### 8.3 建议 **用 WebSocket 库自带的协议层 Ping/Pong 就够了**,不需要再实现应用层 `ping`。 另外注意:插件**不处理**你发来的 `pong`。你发 `{"type":"pong"}` 会被当作未知类型静默忽略。 --- ## 9. `event` 帧详解 微信里发生事情时推送。 ```json { "type": "event", "device_id": "webridge_device_xxxx", "session_id": "sess_xxx", "account_id": "wxid_self", "runtime_version": "8.0.80-2", "build_marker": "webridge_build_20261106_settings_update_8080", "ts_ms": 1775000000123, "event_name": "message", "event_id": "msg:7307587870543245070", "captured_at_ms": 1775000000100, "enqueued_at_ms": 1775000000120, "body": { "…": "…" } } ``` | 字段 | 类型 | 恒有 | 说明 | |------|------|------|------| | `type` | string | ✅ | 固定 `"event"` | | 公共字段 | — | ✅ | `device_id` / `session_id` / `account_id` / `runtime_version` / `build_marker` / `ts_ms`,见第 12 节 | | `event_name` | string | ✅ | 事件类型,见第 23 节 | | `event_id` | string | ✅ | 不透明的去重键;业务 ID 无法生成时使用 `evt:<毫秒>` 兜底 | | `captured_at_ms` | number | ✅ | 事件被捕获的时刻(毫秒) | | `enqueued_at_ms` | number | ✅ | 进入发送队列的时刻(毫秒) | | `body` | object | ✅ | 事件内容,见第 23 节 | > ⚠️ **注意**:WebSocket 的事件帧**没有 `upload_started_at_ms`**(长轮询模式的事件有)。你只能做两段时延分析: > - `captured_at_ms → enqueued_at_ms`:插件内部处理耗时 > - `enqueued_at_ms → 你收到的时间`:排队 + 网络耗时 ### `event_id` 与去重 `event_id` 优先由事件类型及关键业务 ID 生成;业务 ID 不足时,WebSocket 传输层生成 `evt:<毫秒>` 兜底,当前事件帧仍携带此字段。毫秒兜底不等于稳定业务身份,后端需保留自己的关联字段。 判定按**固定优先级短路**,命中第一条就返回: | 优先级 | 条件 | `event_id` 格式 | |-------|------|----------------| | 0 | 事件为 transfer_refund_result 且 refund_result_id 非空 | `transfer_refund:` | | 1 | 事件是红包且红包 ID 非空 | `red_packet:<红包ID>` | | 2 | 事件是好友申请且申请 ID 非空 | `friend_request:<申请ID>` | | 3 | 事件是转账且转账 ID 非空 | `transfer:<转账ID>` | | 4 | 有 `message_id` 或 `msg_id` 非空 | `msg:<消息ID>` | | 5 | 事件名以 `group_member_` 开头且群 ID、成员 ID 非空 | `group_member:<事件名>:<群ID>:<成员ID>:<变动前>:<变动后>` | | 6 | 都不满足 | 兜底为 `evt:<毫秒时间戳>` | **由此推出的实用结论**: | 事件类型 | 实际 `event_id` 形态 | |---------|-------------------| | 普通消息 | `msg:<消息ID>` | | 拍一拍 | `msg:<消息ID>`(消息 ID 取不到时退化为 `msg:pat_<毫秒>`) | | 群成员加入/离开/昵称变更 | **`msg:` 开头**,形如 `msg:group_member_join:<群ID>:<成员ID>:<毫秒>`。因为这类事件必然带消息 ID,会先命中优先级 4,**优先级 5 实际上用不到** | | 红包 | `red_packet:<红包ID>`;红包 ID 本身形如 `rp_msg_<消息ID>` / `rp_send_<发送ID>` / `rp_<毫秒>` | | 转账 | `transfer:<转账ID>` | | 转账退还结果 | `transfer_refund:`;独立结果 ID,不能按 transfer_id 去重 | | 好友申请 | `friend_request:<申请ID>`(申请 ID 形如 `fr_msg_<消息ID>` 或 `fr_<加密用户名>__<场景>`) | > ⚠️ **实战建议**:**不要把 `event_id` 当格式化字符串去解析**(别试图从中提取消息 ID 或群 ID)。它是一个**不透明的去重键**,格式可能随版本变化。需要具体字段请从 `body` 里取。 **服务端必须这样处理**: 1. 使用完整 `event_id` 去重,不拆解其格式。 2. 若兼容旧构建或异常数据时收到缺失 ID 的帧,不要丢弃;可用 `(event_name, captured_at_ms, body.message_id)` 等组合字段辅助判断,没有可靠键时接受可能重复。 ### `body` 里的关键字段 `body` 的结构随 `event_name` 变化,详见 [23. 事件 body 字段](#23-事件-body-字段)。 --- ## 10. `command` 帧详解 你有命令要执行时,直接推一帧: ```json { "type": "command", "request_id": "req-send-message-001", "command_name": "send_message", "args": { "receiver": "wxid_friend", "content": "你好" } } ``` | 字段 | 类型 | 必需 | 说明 | |------|------|------|------| | `type` | string | ✅ | 固定 `"command"` | | `request_id` | string | ✅ | **你自己生成的唯一标识**,插件会在 `command_result` 里原样回传 | | `command_name` | string | ✅ | 命令名,见第 14 节 | | `args` | object | — | 命令参数;不传或传空对象都行 | ### 关键行为 | 行为 | 说明 | |------|------| | **参数缺失** | `args` 缺失或不是对象 → 当作空对象,**命令仍会被执行**,然后按"缺参数"返回失败结果 | | **`request_id` 或 `command_name` 为空** | 插件**记日志后直接忽略这一帧,不回任何结果**(你会一直等不到 `command_result`)。⚠️ 这是 WebSocket 模式独有行为——长轮询模式下同样情况会返回一个 `request_id` 为空串的失败结果,两种模式不一致 | | **`command_name` 拼错/不认识** | ✅ **会回结果**:`ok: false`,`error: "unsupported command_name: <你写的名字>"`。所以"完全没收到结果"和"收到失败结果"要分开排查 | | **执行顺序** | 普通业务命令与发送命令分别在各自队列串行;收藏、媒体、群操作等可在后台并发等待。结果可能交错,须按 `request_id` 关联 | | **握手前** | ⚠️ 如果插件在收到 `hello_ack` 之前就收到 `command`,**它照样会执行**。不要依赖"握手完成"作为发命令的前提 | | **重复 `request_id`** | 命中缓存则**复用上次结果、不重新执行**,见第 26 节 | > ⚠️ **最容易踩的坑**:`request_id` 或 `command_name` 写空 → **静默丢弃,没有任何反馈**。请在你的服务端做校验,或对超时未回结果的命令做告警。 ### 一次推多条 可以连续发送多帧;插件将业务执行放入后台队列,收包和心跳继续运行。发送类与普通业务类分别串行,媒体、收藏、群操作等等待型能力可并发执行,不能把所有命令视为同一条串行队列。通信层最多登记 32 条等待中的命令,满额返回失败;共享业务执行器也有 32 个槽位。 按 `request_id` 关联结果。后一项依赖前一项结果时,必须等前一项回包后再下发。切换模式、停止通信或变更地址后,尚未执行的旧命令会取消;已提交业务可能继续生效,旧生命周期不保证回传结果,不能据此自动重发。 --- ## 11. `command_result` 帧详解 ```json { "type": "command_result", "device_id": "webridge_device_xxxx", "session_id": "sess_xxx", "account_id": "wxid_self", "runtime_version": "8.0.80-2", "build_marker": "webridge_build_20261106_settings_update_8080", "ts_ms": 1775000003456, "request_id": "req-send-message-001", "command_name": "send_message", "ok": true, "duration_ms": 320, "result": { "receiver": "wxid_friend" }, "error": "" } ``` | 字段 | 类型 | 恒有 | 说明 | |------|------|------|------| | `type` | string | ✅ | 固定 `"command_result"` | | 公共字段 | — | ✅ | 见第 12 节 | | `request_id` | string | ✅ | 与下发时一致;下发时为空会被丢弃,不会产生结果帧 | | `command_name` | string | ✅ | 命令名 | | `ok` | bool | ✅ | 是否成功 | | `duration_ms` | number | ✅ | 插件处理本命令的耗时,包含内部排队和业务等待(毫秒) | | `result` | object | ✅ | **恒为对象**;无内容时是 `{}` | | `error` | string | ✅ | 失败原因;成功时为**空串 `""`**(不是缺失) | ### 判断成功 `friend_pay_name` 单次命令等待终态后回包,详见[阻塞查询](#friend-pay-name);群二维码见 [`get_chatroom_qrcode`](#get-chatroom-qrcode)。 `ok` 表示本次命令是否执行成功,不要用 `error` 是否为空或 `result` 是否有内容代替它。 朋友圈首次命令ok=true仅表示受理,必须按result.operation_id等待moments_result或继续查询;详见[本篇完整流程](#moments-start)。对于资料设置,`ok:true` 表示已提交或当前值已一致,仍需核对 `result.accepted`、`result.local_verified`;拍一拍的首次 `ok:true` 只表示受理,最终成功须查询探针并确认 `last_operation.status:"succeeded"` 和 `server_confirmed:true`。详见[资料设置命令](#profile-commands)。 ### `duration_ms` 的精确含义 从**插件接收并登记这条命令**到**业务返回、生成结果信封**的耗时(毫秒),包含插件内部排队及单次业务等待。命中缓存时回放原结果及原耗时。 它**不包含**: - 从你发出 `command` 帧到插件收到的网络时间 - 结果帧回传给你的网络时间 对包含下载/转码的命令(如 `send_voice`、`send_video`、`send_emoji`),这个值可能达到数秒甚至更长。 ### `ts_ms` 的精确含义 `ts_ms` 是**结果信封被生成的那一刻**,不是发送成功的时刻。 > ⚠️ **一个真实存在的现象**:如果结果帧发送失败,插件会把**同一个结果信封**暂存起来重发。重发时 `ts_ms` 仍是**原来生成时的值**,所以你会看到一个"明显早于当前时间"的 `ts_ms`。这是正常的,不是时钟错误。 ### 失败时 `result` 里有什么 **不是所有失败都是空 `result`。** 部分命令失败时仍会返回诊断信息,便于你定位: | 命令 | 失败时 `result` | |------|----------------| | 大部分发送类命令 | `{}` 空对象 | | `send_contact_card`、`send_location`、`location_geocode_probe` | 透传业务结果;`ok`、`error` 移至外层 | | `send_xml` | 透传底层结果 | | `friend_pay_name` | 保留 `account_id`、`pay_name_last_char`、`pay_name_status`,具体原因在外层 `error` | | `receive_red_packet` / `red_packet_detail` | **带业务诊断信息**(例如"已被领完") | | `receive_transfer` | 保留精简的编号、accepted、confirmed、status、callback_received、ret_code;ok/error 在外层,实际查询确认时另有 native_status | | `refund_transfer` / `get_transfer_status` | 保留 accepted、confirmed、status、native_status、ret_code、error_code 与编号;ok/error 在外层 | | `room_members` / `friends` / `friends_all` / `groups_all` | 空对象,`error` 里是原因 | | `kick_group_member` | 带 `room_id`、`member_ids` | | `rename_group` / `set_group_self_nickname`(同一命令) | 带 `room_id`、`new_name`、`clears_group_nickname` | | `set_group_name` | 带 `room_id`、`new_name`、`accepted`、`local_verified`、`already_named`、`local_name`;具体原因在 `error` | | `get_group_notice` | 带 `room_id`、`cached_notice_found`、`summary`、`html`、`html_cached`;具体原因在 `error` | | `upload_message_image` | 带上传诊断字段 | **结论**:失败时**不要假设 `result` 是空的**,把它记进日志会大幅加快排错。 --- ## 12. 公共字段 以下字段出现在 `hello`、`event`、`command_result` 中,含义与生成规则完全相同。 下表包含本模式所需的全部公共字段;不需要另查其他协议文档。 | 字段 | 类型 | 恒有 | 说明 | |------|------|------|------| | `device_id` | string | ✅ | 设备唯一标识,格式 `webridge_device_`。**首次启动生成后持久化到磁盘,插件重装前不变、重启不变** | | `session_id` | string | ✅ | 本次运行会话标识,格式 `sess_<毫秒时间戳>_<12位十六进制>`。**每次插件进程启动重新生成** | | `account_id` | string | ✅ | 当前登录的微信 wxid。取值回退链:运行时当前用户名 → 账号信息的当前用户 → 账号信息的 wxid;都取不到时为空串 | | `runtime_version` | string | ✅ | 插件完整版本号,包含构建号,示例为 `8.0.80-2` | | `build_marker` | string | ✅ | 构建标记,当前为 `webridge_build_20261106_settings_update_8080` | | `ts_ms` | number | ✅ | 该帧/该请求生成时刻的 Unix **毫秒**时间戳 | ### 各帧携带情况 | 帧 | 公共字段 | `capabilities` | `state` | |----|---------|---------------|---------| | `hello` | ✅ 全部 | ✅ | ✅(4 字段) | | `event` | ✅ 全部 | ❌ | ❌ | | `command_result` | ✅ 全部 | ❌ | ❌ | | `pong` | ❌ 只有 `ts_ms` | ❌ | ❌ | > **注意**:`capabilities` **只在 `hello` 里出现一次**。如果你想在中途重新获取能力清单,只能断开重连,或改用 HTTP 接口查询。 --- ## 13. 能力清单 `hello.capabilities` 是插件当前声明支持的命令集合,当前共 **118 项**。已移除能力见[兼容与移除说明](#compatibility);现有业务功能已由用户在微信 `8.0.80` 验证通过;切换到其他微信版本时仍需核对实际运行能力: ``` send_message, send_signal, send_pat, receive_red_packet, red_packet_detail, receive_transfer, refund_transfer, get_transfer_status, clear_gender_region, profile_update, profile_gender, profile_region, profile_signature, profile_pat_suffix, profile_region_codes, moments_list, moments_user, moments_detail, moments_refresh, moments_like, moments_unlike, moments_comment, moments_delete_comment, moments_publish, moments_delete, moments_permissions_query, moments_permissions_update, moments_operation, moments_probe, moments_media_download, profile_nickname, send_image, upload_message_image, download_message_media, upload_message_media, upload_file_to_wechat_cdn, message_media_probe, send_emoji, send_voice, send_video, send_video_from_url, send_link_card, send_xml, send_contact_card, send_location, location_geocode_probe, send_music_card, send_file, send_file_from_url, forward_message, revoke_message, delete_friend, delete_session, quit_group, create_group, add_group_member, invite_group_member, kick_group_member, kick_group_members, rename_group, set_group_name, get_group_notice, get_chatroom_qrcode, get_self_qrcode, group_admin_operate, set_group_msg_silence, set_friend_remark, get_group_invitation, accept_group_invitation, group_invitation_probe, friend_request_probe, search_contact, official_accounts, official_account_search, official_account_follow, official_account_unfollow, official_account_articles, add_friend, add_room_friend, friend_add_probe, accept_friend_request, room_members, account_status, bridge_status, device_status, friends, friends_all, friend_pay_name, groups_all, quote_build_probe, signal_send_probe, pat_send_probe, red_packet_probe, red_packet_detail_probe, transfer_probe, clear_gender_region_probe, profile_probe, profile_region_probe, profile_pat_suffix_probe, profile_nickname_probe, image_send_probe, emoji_send_probe, voice_send_probe, video_send_probe, video_send_url_probe, link_send_probe, file_send_probe, file_send_url_probe, revoke_probe, delete_friend_probe, quit_group_probe, invite_group_member_probe, kick_group_member_probe, rename_group_probe, set_group_name_probe, send_livephoto, livephoto_probe, livephoto_diagnostics ``` 当前分发器可执行但尚未列入 `capabilities` 的业务命令为 `favorites_list`、`favorites_send`、`favorites_delete`、`set_friend_star`、`set_friend_blacklist`;另有兼容别名 `group_invite_member`、`group_invite_member_probe`、`set_group_nickname`、`set_group_self_nickname`、`set_group_nickname_probe`、`set_group_self_nickname_probe`、`update_nickname`、`update_nickname_probe` 未声明。后端若只按清单过滤会误判不支持;按本篇命令表、别名表及实际插件构建处理这些缺口。清单只代表声明,不能代替实际业务能力判断;用户已确认现有业务功能在微信 `8.0.80` 验证通过。 ### 三个重要提醒 1. **不要硬编码这份清单。** 它随插件版本变化,请以每次 `hello` 实际收到的为准。 2. **声明清单仍有上文列出的业务及兼容别名缺口。** 历史构建也曾漏声明实况照片等命令;不要仅凭版本号或清单缺省判断可调用范围。 3. **`capabilities` 不会中途更新。** 插件运行期间能力不变(实际是进程内固定的),所以一次 `hello` 就够了。 --- ## 14. 命令参数与返回 ### 14.1 参数通用规则 下表仅描述未另作规定的旧命令转换方式;各业务明确的参数类型、范围及校验规则优先。好友设置、群操作、收藏、公众号、资料设置及消息 ID、坐标等字段可能采用严格校验,不能套用宽松转换。 | 规则 | 说明 | |------|------| | **类型转换宽松** | 该传字符串时传了数字会被自动转成字符串;该传数字时传了数字字符串会被解析为数字 | | **布尔值** | 接受 `1/0`、`true/false`、`yes/no`、`y/n`,大小写不敏感 | | **字符串** | 自动去掉首尾空白与换行 | | **单值→数组** | 部分数组参数接受单个值并自动包装 | | **`receiver` 别名** | 部分命令还接受 `toUser`、`to`、`chatId`、`target`。**新接入请只用 `receiver`** | 收藏命令也使用严格类型校验,并且不接受目标别名,详见[收藏用法](#favorites)。公众号五项同样采用严格类型校验,未知字段报错,见[公众号](#official-accounts)。 资料设置命令与资料预校验探针采用严格类型校验;签名保留原始空白,拍一拍使用微信原生文本处理规则,详见本章资料设置说明。 > 旧转换路径可能将错误数字字符串(如 `"abc"`)转成 `0`,不保证返回参数错误。请始终按各命令声明的类型传值;严格校验命令会拒绝不符合要求的参数。 ### 14.2 分类命令索引 #### 消息发送 | 命令 | 用途 | 必需参数 | 可选参数 | |------|------|---------|---------| | `send_message` | 发送文本(支持 @ / 引用 / @所有人) | `receiver`, `content` | `quote_message_id`, `mention_user_list` | | `send_image` | 发送图片 | `receiver`, `image_path` | — | | `send_emoji` | 发送表情 | `receiver`, `source` | `emoji_path`(别名) | | `send_voice` | 发送语音 | `receiver`, `source` | `duration_ms` | | `send_video` | 发送视频 | `receiver`, `source` | `max_size_mb`, `max_size_bytes` | | `send_video_from_url` | 发送视频(URL 入口) | `receiver`, `source` | `max_size_mb`, `max_size_bytes` | | `send_livephoto` | 发送实况照片 | `receiver`, `video_path` | `image_path`, `still_image_time_ms`, `prefered_hevc` | | `send_link_card` | 发送链接卡片 | `receiver`, `title`, `url` | `description`, `thumb_url`, `thumb_path`, `source_user_name`, `source_display_name` | | `send_music_card` | 发送音乐卡片 | `receiver`, `title` | `desc`, `url`, `data_url`, `song_album_url`, `app_id`, `app_name`, `song_lyric` | | `send_file` | 发送文件 | `receiver`, `file_path` | — | | `send_file_from_url` | 发送文件(URL) | `receiver`, `file_url` | — | | `send_contact_card` | 发送联系人原生名片 | `receiver`, `contact_id` | 无 | | `send_location` | 发送单次位置 | `receiver`, `latitude`, `longitude` | `title`, `address` | | `location_geocode_probe` | 指定坐标执行微信原生反查,不发送消息 | `latitude`, `longitude` | 无 | | `send_xml` | 发送原始 appmsg XML | `receiver`, `xml`, `type` | `forward`(默认 `false`) | | `send_signal` | 发送小信号 | `receiver` | — | | `send_pat` | 发送拍一拍 | `receiver` | `room_id`(群聊需要) | #### 消息操作 | 命令 | 用途 | 必需参数 | 可选参数 | |------|------|---------|---------| | `forward_message` | 转发消息 | `receiver`, `message_id` | — | | `revoke_message` | 撤回消息 | `message_id` | — | | `upload_message_image` | 已废弃,保留兼容;改用 `upload_message_media` | `message_id` | `upload_url`(默认业务地址加 `/agent/upload_media`) | | `download_message_media` | 准备消息媒体,详见[完整用法](#message-media) | `message_id`(字符串) | `media_type`、`timeout_seconds`(默认 60,1~180) | | `upload_message_media` | 准备并上传消息媒体,详见[完整用法](#message-media) | `message_id`(字符串) | `upload_url`、`media_type`、`timeout_seconds`(默认 60,1~180) | | `upload_file_to_wechat_cdn` | 上传普通文件到微信 CDN,详见[完整用法](#message-media) | `file_path` / `file_url` 二选一;任务 ID 取外层 `request_id` | `timeout_seconds`(默认 120,1~180) | | `message_media_probe` | 读取消息媒体或 CDN 原任务状态 | `message_id` / `request_id` 二选一,放在 `args` | — | #### 联系人与群组 | 命令 | 用途 | 必需参数 | 可选参数 | |------|------|---------|---------| | `official_account_search` | 联网搜索公众号首批候选 | `keyword` | — | | `official_accounts` | 已关注公众号分页列表 | — | `type`、`keyword`、`limit`、`offset` | | `official_account_follow` | 关注公众号,单次等待结果 | `id` | — | | `official_account_unfollow` | 取消关注,可能清理本地会话和消息 | `id` | — | | `official_account_articles` | 按篇数合并历史文章,详见[公众号](#official-accounts) | `id` | `limit`(默认 100,1–1000)、`cursor` | | `friends` | 好友列表(分页),含添加时间 | — | `limit`(默认 `100`)、`offset`(默认 `0`)、`keyword` | | `friend_pay_name` | 阻塞查询好友或群成员支付姓名末字,见[完整流程](#friend-pay-name) | `id` | `refresh`(默认 `false`) | | `friends_all` | 全部普通好友,含添加时间及支付姓名缓存 | — | `keyword` | | `groups_all` | 全部群聊 | — | `keyword` | | `room_members` | 群成员列表 | `room_id` | — | | `get_group_notice` | 读取本机缓存的群公告 | `room_id` | — | | `get_chatroom_qrcode` | 阻塞获取指定群二维码,返回 URL/可选图片与原始限制 | `room_id` | — | | `group_admin_operate` | 添加/移除管理员、转让群主,见[完整说明](#group-management-self-qrcode) | `room_id`、`oper_type`、`member_ids` | — | | `set_group_msg_silence` | 设置当前账号的群免打扰 | `room_id`、`silence` | — | | `get_self_qrcode` | 获取当前登录账号二维码 | —(`args:{}`) | — | | `get_group_invitation` | 查询入群邀请 | `message_id`(字符串) | — | | `accept_group_invitation` | 静默接受入群邀请 | `message_id`(字符串) | — | | `group_invitation_probe` | 只读邀请探针 | `message_id`(字符串) | `stage` | | `search_contact` | 搜索普通个人联系人 | `keyword` | — | | `add_friend` | 搜索结果添加好友或发送验证申请 | `contact_ref` | `say_content` | | `add_room_friend` | 通过共同群添加成员好友 | `room_id`, `wxid` | `say_content` | | `friend_add_probe` | 只读检查搜索/群添加链路 | — | `contact_ref`, `room_id`, `wxid` | | `accept_friend_request` | 同意好友申请 | `encrypt_username`, `ticket` | `request_id`, `scene`, `say_content` | | `set_friend_remark` | 设置/清空好友备注,详见[好友设置](#friend-settings) | `id`、`remark`(字符串,可为空) | — | | `set_friend_star` | 设置/取消好友星标,详见[好友设置](#friend-settings) | `id`、`starred`(JSON 布尔) | — | | `set_friend_blacklist` | 加入/移出黑名单,详见[好友设置](#friend-settings) | `id`、`blacklisted`(JSON 布尔) | — | | `delete_friend` | 删除好友 | `id` | — | | `quit_group` | 退出群聊 | `id` | — | | `create_group` | [创建普通群聊](#group-member-operations) | 信封 `request_id`,`member_ids`(至少2位其他好友) | `member_id`(单人无法达到建群范围) | | `add_group_member` | [直接添加群成员](#group-member-operations) | 信封 `request_id`、`room_id`,`member_id`/`member_ids`二选一 | `reason` | | `invite_group_member` | [发送入群邀请](#group-member-operations) | 信封 `request_id`、`room_id`、至少一个成员字段 | `member_id`、`member_ids`及兼容别名可合并 | | `kick_group_member` | 踢出群成员 | `room_id` + (`member_id` 或 `member_ids`) | `wxid`, `wxids` | | `kick_group_members` | 同上(别名) | 同上 | 同上 | | `rename_group` | 修改**我在本群的群昵称**(不是群名称) | `room_id` | `new_name`(**可传空串表示清空**), `nickname`, `room_name`, `group_name` | | `set_group_self_nickname` | 同上(语义正确的别名) | 同上 | 同上 | | `set_group_nickname` | 同上(别名) | 同上 | 同上 | | `set_group_name` | 修改整个群聊的名称 | `room_id`, `new_name` | — | > ⚠️ **重名陷阱**:`accept_friend_request` 的 `args.request_id` 是**好友申请的业务 ID**,与命令信封上的 `request_id`(用于结果关联)**同名但完全是两回事**。你有两个 `request_id` 出现在不同层级,别搞混。 #### 会话管理 | 命令 | 用途 | 必需参数 | 可选参数 | |------|------|---------|---------| | `delete_session` | 删除会话 | `id` | — | #### 红包与转账 | 命令 | 用途 | 必需参数 | |------|------|---------| | `receive_red_packet` | 领取红包 | `red_packet_id` | | `red_packet_detail` | 红包详情 | `red_packet_id` | | `receive_transfer` | [确认收款](#receive-transfer) | `transfer_id`/`message_id` 至少一项 | | `refund_transfer` | [退还待收取转账](#transfer-operations) | `transfer_id`/`message_id` 至少一项 | | `get_transfer_status` | [查询转账资金状态,不执行收款或退还](#transfer-operations) | `transfer_id`/`message_id` 至少一项 | #### 资料管理 | 命令 | 用途 | 必需参数 | |------|------|---------| | `clear_gender_region` | 清除性别地区 | 无 | | `profile_nickname` | 修改账号昵称 | `nickname`(别名 `new_nickname`、`name`) | | `profile_update` | 批量设置性别、地区、签名 | 至少一项 `sex`、`region_code`/`region`、`signature` | | `profile_gender` | 设置或清空性别 | `sex`:整数 0/1/2 | | `profile_region` | 设置或清空地区 | `region_code` 或 `region` 二选一 | | `profile_signature` | 设置或清空签名 | `signature`;空串清空 | | `profile_pat_suffix` | 设置或清空拍一拍后缀 | `pat_suffix`;空串清空 | | `profile_region_codes` | 只读查询国家、省、市地区代码 | 可选 `parent_code`;省略或空串查国家 | | `profile_probe` | 当前资料与候选参数预校验 | `{}` 或资料设置参数 | | `profile_region_probe` | 地区代码查询探针 | 同 `profile_region_codes` | | `profile_pat_suffix_probe` | 后缀限制、候选校验及最近操作状态 | `{}` 查状态;可传 `pat_suffix` 预校验 | #### 收藏 | 命令 | 用途 | 参数 | |---|---|---| | `favorites_list` | 收藏列表,最新在前的游标分页 | 可选 `limit`、`refresh`、`cursor` | | `favorites_send` | 发送指定收藏到好友或群 | `fav_id`、`receiver`;业务请求标识取外层 `request_id` | | `favorites_delete` | 删除指定收藏,等待服务器确认 | `fav_id`;业务请求标识取外层 `request_id` | 完整参数、结果和重试规则见[本篇收藏用法](#favorites)。 #### 朋友圈 | 命令 | 用途 | 参数 | |---|---|---| | `moments_list` | 获取好友朋友圈时间线与历史 | 完整参数、返回和后续调用见[本篇朋友圈完整用法](#moments-start) | | `moments_user` | 获取指定用户朋友圈与可见期限 | 完整参数、返回和后续调用见[本篇朋友圈完整用法](#moments-start) | | `moments_detail` | 联网获取一条朋友圈详情 | 完整参数、返回和后续调用见[本篇朋友圈完整用法](#moments-start) | | `moments_refresh` | 服务端主动刷新朋友圈并回调 | 完整参数、返回和后续调用见[本篇朋友圈完整用法](#moments-start) | | `moments_like` | 点赞指定朋友圈 | 完整参数、返回和后续调用见[本篇朋友圈完整用法](#moments-start) | | `moments_unlike` | 取消朋友圈点赞 | 完整参数、返回和后续调用见[本篇朋友圈完整用法](#moments-start) | | `moments_comment` | 评论或回复指定朋友圈 | 完整参数、返回和后续调用见[本篇朋友圈完整用法](#moments-start) | | `moments_delete_comment` | 删除朋友圈评论 | 完整参数、返回和后续调用见[本篇朋友圈完整用法](#moments-start) | | `moments_publish` | 发布文字、图文或视频朋友圈 | 完整参数、返回和后续调用见[本篇朋友圈完整用法](#moments-start) | | `moments_delete` | 删除本人已发布朋友圈 | 完整参数、返回和后续调用见[本篇朋友圈完整用法](#moments-start) | | `moments_permissions_query` | 查询朋友圈好友、期限或单帖权限 | 完整参数、返回和后续调用见[本篇朋友圈完整用法](#moments-start) | | `moments_permissions_update` | 设置朋友圈好友、期限或单帖权限 | 完整参数、返回和后续调用见[本篇朋友圈完整用法](#moments-start) | | `moments_operation` | 查询朋友圈异步操作结果 | 完整参数、返回和后续调用见[本篇朋友圈完整用法](#moments-start) | | `moments_probe` | 朋友圈运行时/媒体诊断探针 | 完整参数、返回和后续调用见[本篇朋友圈完整用法](#moments-start) | | `moments_media_download` | 准备图片/视频并主动上传后端 | sns_id、media_id;远程模式加upload,详见[本篇完整用法](#moments-media-download) | #### 状态查询 | 命令 | 用途 | 参数 | |------|------|------| | `account_status` | 账号状态 | 无 | | `bridge_status` | 插件完整诊断快照 | 无 | | `device_status` | 设备运行状态 | 无 | #### 调试探针 以下命令主要用于返回运行时和候选参数诊断;是否触发资源准备或原生查询以对应说明为准,不将诊断成功当作业务完成。出现兼容或参数问题时,可用对应探针检查运行条件: `friend_add_probe`、`quote_build_probe`、`signal_send_probe`、`pat_send_probe`、`red_packet_probe`、`red_packet_detail_probe`、`transfer_probe`、`clear_gender_region_probe`、`profile_probe`、`profile_region_probe`、`profile_pat_suffix_probe`、`profile_nickname_probe`、`image_send_probe`、`emoji_send_probe`、`voice_send_probe`、`video_send_probe`、`video_send_url_probe`、`link_send_probe`、`file_send_probe`、`file_send_url_probe`、`revoke_probe`、`delete_friend_probe`、`quit_group_probe`、`invite_group_member_probe`、`kick_group_member_probe`、`rename_group_probe`、`set_group_name_probe`、`friend_request_probe`、`livephoto_probe`、`livephoto_diagnostics` > ⚠️ **探针名不要带 `/debug/` 前缀**。命令通道用的是上面这些**裸名字**;写成 `/debug/xxx_probe`(带斜杠)会得到 `unsupported command_name`——那是插件 HTTP 接口的路径写法,两个通道不通用。 ### 14.3 消息发送与操作 **表情大小与转发等待**: `send_emoji`转换前按当前微信动态配置检查大小,实际限制见`emoji_send_probe`的`limits.max_file_size/max_dimension`,不能把静态默认5MiB写死。字节有效上限为原生两种配置较大值与20MiB插件保护上限的较小值,超限明确失败。 `forward_message`转发表情时,在同一命令内检查/下载原生资源,再转发并等待目标消息最终发送确认,无需另加下载命令,也不需要先打开手机对话。下载、发送各最多等待30秒。下载失败不转发;已提交但确认超时会返回“结果未知,请勿自动重试”,不能将失败回包当作对方一定未收到。HTTP、长轮询、WS共用该实现;成功字段保持既有简洁结构。 #### `send_message` ```json { "receiver": "wxid_friend" } ``` 传了 `quote_message_id` 时多一个 `quote_message_id` 字段;有 @ 时多一个 `mention_user_list` 数组。 **@ 相关规则**: - `mention_user_list` 也接受别名 `at_user_list`;`at_user`(单个 wxid)会被并入名单最前面 - `content` 里可用 `{{mention:wxid}}` 占位符,在正文任意位置插入 @ - **即使不传 `mention_user_list`**,只要正文里有 `{{mention:wxid}}`,插件也会自动提取并走 @ 链路 - ⚠️ 占位符里的 wxid **解析不出备注名或昵称时,整条消息会被拒绝发送** - **@所有人**:`mention_user_list: ["notify@all"]`,仅群聊可用。`notify@all` 是协议标记不是 wxid,**不要展开成全部成员** - 插件**不会**自动往 `content` 里补 `@所有人`,要在界面上看到这四个字必须自己写进 `content` - 是否允许发送仍受微信群主/管理员权限限制,插件不绕过微信校验 #### `send_image` ```json { "resolved_image_path": "/var/mobile/.../photo.jpg", "receiver": "wxid_friend", "downloaded": false } ``` #### `send_emoji` **用途**:发送表情(gif / png / jpg / jpeg 均可)。 大小限制读取当前微信动态配置,实际值见命令 `emoji_send_probe` 的`limits.max_file_size/max_dimension`。字节上限为两种原生配置较大值与20MiB保护上限的较小值;静态默认5MiB不能替代真机配置。转换前和读取数据后均检查,超限明确报错,不提交发送。 **`args` 字段**: | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `receiver` | string | ✅ | 接收方 | | `source` | string | ✅ | 本地路径或远程 URL(**推荐用这个字段名**) | | `emoji_path` | string | — | `source` 的兼容别名 | **说明**: - 远程图片会自动下载到插件沙盒,缓存 1 天后自动清理。 - 不是 gif 的图片会被转换成微信表情格式。 **成功时的 `result`**: ```json { "receiver": "wxid_friend", "source": "https://example.com/demo.png", "source_type": "remote_url", "emoji_path": "/var/mobile/.../demo.gif", "resolved_emoji_path": "/var/mobile/.../demo.gif", "downloaded": true, "emoji_md5": "…", "message_type": 47, "message_type_name": "emoji" } ``` #### `send_voice` ```json { "receiver": "wxid_friend", "source": "https://example.com/audio.mp3", "source_type": "remote_url", "resolved_local_path": "…", "resolved_pcm_path": "…", "resolved_wechat_aud_path": "…", "duration_ms": 3240, "downloaded": true, "message_type": 34, "message_type_name": "voice" } ``` - `source` 兼容别名:`voice_url`、`voice_path`、`audio_url`、`audio_path`、`video_url`、`video_path`、`aud_url`、`aud_path` - `duration_ms` 兼容 `voice_length_ms`、`voice_duration_ms`;推荐使用数字毫秒值 - 非 `.aud` 文件会被自动转码(提取音频 → PCM → SILK),**耗时较长,请把超时设宽** #### `send_video` ```json { "source": "…", "source_type": "remote_url", "resolved_video_path": "…", "resolved_thumb_path": "…", "receiver": "…", "video_duration": 12.5, "generated_thumb": true, "downloaded": true, "max_size_bytes": 10485760, "max_size_mb": 10, "within_size_limit": true, "remote_size_known": true, "remote_size_bytes": 8123456 } ``` - **默认大小上限 `10 MiB`**;`max_size_mb` 优先级**高于** `max_size_bytes` - `source` 也接受 `video_url`、`video_path` #### `send_livephoto` `args` 必填字符串 `receiver`、`video_path`(手机路径或手机可访问的 URL);可选字符串 `image_path`、数字 `still_image_time_ms`、布尔 `prefered_hevc`(默认 false)。`prefered_hevc` 就是此拼法。命令通道会转换数字/布尔字符串,新接入仍应发送原生 JSON 类型。 成功时的 `result`: ```json {"video_path":"/var/mobile/example.mov","image_path":"/var/mobile/example.jpg","receiver":"filehelper"} ``` #### `send_link_card` `args` 必填 `receiver`、`title`、`url`;可选 `description`、`thumb_url`、`thumb_path`、`source_user_name`、`source_display_name`,均为字符串。缩略图和跳转链接须为手机可访问的资源。 成功时的 `result`: ```json {"receiver":"wxid_friend","title":"标题","url":"https://example.com","thumb_url":""} ``` #### `send_music_card` **用途**:发送音乐卡片。**注意:这个接口内部走的是转发链路**(`relay_*` 字段会体现在响应里),不是直接构造消息。 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `receiver` | string | ✅ | 接收方 | | `title` | string | ✅ | 歌曲名 | | `desc` | string | — | 描述 | | `url` | string | — | 音乐链接 | | `data_url` | string | — | 音频数据地址 | | `song_album_url` | string | — | 封面图 | | `app_id` | string | — | 来源 App ID | | `app_name` | string | — | 来源 App 名称 | | `song_lyric` | string | — | 歌词;兼容 `songlyric`、`songLyric` | **别名表**(这个接口为了兼容历史调用方,别名特别多): | 主字段 | 兼容别名 | |--------|---------| | `receiver` | `toUser`、`to`、`chatId`、`target` | | `desc` | `description` | | `url` | `musicUrl`、`music_url` | | `data_url` | `dataurl`、`dataUrl` | | `song_album_url` | `songalbumurl`、`songAlbumUrl`、`thumbUrl`、`thumb_url` | | `app_id` | `appId` | | `app_name` | `appName` | | `song_lyric` | `songlyric`、`songLyric` | **重要**:`url`、`data_url`、`song_album_url`、`app_id`、`app_name` 这五个字段**不传会用内置默认值兜底**(默认指向一个示例音乐)。也就是说**缺参数不会报错,会发出去一张指向默认歌曲的卡片**。要发自己的音乐,这五个字段请全部显式传入。 **`args` 示例**(URL 和应用信息均为占位值,需替换): ```json { "receiver": "filehelper", "title": "歌曲标题", "desc": "歌手 - 专辑", "url": "https://music.example.com/song/1", "data_url": "https://media.example.com/song/1.mp3", "song_album_url": "https://media.example.com/song/1.jpg", "app_id": "wx_example_music_app", "app_name": "音乐应用" } ``` **成功时的 `result`**: ```json { "receiver": "wxid_friend", "title": "歌曲名", "desc": "", "url": "https://music.example.com/song", "dataurl": "...", "songalbumurl": "...", "app_id": "wx...", "app_name": "网易云音乐", "songlyric_length": 0, "relay_forward": true, "relay_chat": "filehelper", "relay_delete_staged": true } ``` `songlyric_length` 为实际歌词的 UTF-16 长度;未提供歌词时为 0。 > `relay_*` 字段说明该卡片是通过"先发到文件传输助手再转发"的链路实现的。如果你在文件传输助手里看到过残留,属正常现象,插件会清理。 #### `send_file` ```json { "resolved_file_path": "…", "receiver": "…", "file_name": "report.pdf", "file_size": 204800, "extension": "pdf" } ``` `send_file_from_url` 会额外返回 `source_url` 和 `downloaded`。 #### `send_contact_card` 发送已有联系人的原生名片。`receiver` 是收件人或群,`contact_id` 可传被推荐联系人的真实 ID,或本机已有好友的完整微信号(alias),不能传群 ID。微信号精确匹配,匹配不唯一则拒绝;不按昵称、备注模糊选择,不自动联网搜索陌生人。成功 `result.contact_id` 统一返回解析后的真实 user_name。头像、昵称等取微信联系人资料,不接受自定义头像或昵称。 ```json {"type":"command","request_id":"card-001","command_name":"send_contact_card","args":{"receiver":"filehelper","contact_id":"wxid_example"}} ``` 成功 `result`:`{"submitted":true,"receiver":"filehelper","contact_id":"wxid_example","message_type":42}`。普通名片类型 42,OpenIM / OpenIM 客服按原生判断为 66 / 67。`submitted:true` 仅表示原生提交,不能视为服务器确认送达;名片发送已由用户在微信 `8.0.80` 验证通过,特殊联系人仍按微信实际返回处理。 #### `send_location` 发送单次位置消息。必填 `receiver`、`latitude`、`longitude`;经纬度必须是有限 JSON 数值,范围分别 -90~90、-180~180,不接受数字字符串、布尔值、null。选填字符串 `title`、`address`,空白串按省略处理,null 为参数错误。 ```json {"type":"command","request_id":"location-001","command_name":"send_location","args":{"receiver":"filehelper","latitude":23.012345,"longitude":113.123456}} ``` 有非空标题直接使用;只传地址时地址也用作标题。两者均省略时执行微信原生反查,最多等 15 秒;优先采用带方向的地理描述,再尝试地址或 POI 名称,不能保证得到指定商户名称。需精确文案时传 `title`。经纬度同时为 0 会被原生反查拒绝,显式标题可跳过反查。插件原样使用坐标,不做坐标系转换,外部 GPS 输入须先对点。 成功 `result` 包含 `submitted:true`、`receiver`、`message_type:48`、`latitude`、`longitude`、最终 `title`、`address`、`title_source`。来源为 `provided`、`address` 或 `wechat_geocode`。失败时外层 `ok:false`、`error` 为中文说明,`result` 保留 `error_code` 和反查诊断;反查失败、超时、无可用文字或账号/登录会话变化均不提交消息。成功只表示原生提交,尚未确认送达。 位置等待使用后台执行,不占用心跳和收包队列。`send_location` 与普通发送命令一样使用已有 request_id 结果缓存;缓存存在时重复 ID 会回放结果,重试应使用新的 ID,并先确认上次是否已经提交。名片和位置发送均遵循授权、插件启用及有效发送状态。 #### `location_geocode_probe` 实际测试指定坐标的原生反查,不发送消息、不需要收件人,不受有效发送状态限制;仍需授权、启用插件并登录微信。 ```json {"type":"command","request_id":"geocode-001","command_name":"location_geocode_probe","args":{"latitude":23.012345,"longitude":113.123456}} ``` `result` 保留以下诊断(流程未到达的字段可能缺省): | 字段 | 说明 | | --- | --- | | `probe` / `message_sent` | true / false | | `request_invoked` / `callback_received` | 原生入口是否调用、是否收到本次回调;调用入口也可能命中缓存,不等于发起网络请求 | | `selector_signature` / `signature_supported` | 原生签名及 ABI 检查,通过时为 true | | `native_params` | 正常默认 `type:1, scene:0, language_scene:0` | | `elapsed_ms` / `timeout_seconds` | 耗时、15 秒等待上限 | | `native_result_class` | 原生结果类型 | | `title` / `address` / `title_source` | 与发送共用规则选出的文字 | | `fields` | `poi_name`、`nearest_poi_name`、`rough_address`、`rough_address_with_direction`、`address_description`、`city`、`route`、`detail`,可能为空 | | `native_error_domain` / `native_error_code` / `native_error_message` | 原生失败时的 NSError 信息 | | `error_code` | `INVALID_COORDINATE`、`NOT_LOGGED_IN`、`NATIVE_UNAVAILABLE`、`GEOCODE_FAILED`、`GEOCODE_EMPTY`、`GEOCODE_TIMEOUT`、`ACCOUNT_CHANGED` 等 | 探针和坐标发送共用反查实现,完成、失败和超时都会清理本次回调及扩展注册,迟到回调不会发送消息。探针每次执行,不复用通用 request_id 结果缓存。建议先检查回调与原生地址字段,再执行位置发送。位置发送及反查功能已由用户在微信 `8.0.80` 验证通过,接收端展示以实际微信界面为准。 #### `send_signal` ```json { "receiver": "…", "cgi": 976, "event_flag": 5, "client_msg_id": 4976413716, "create_time": 1777500000 } ``` #### `send_pat` ```json { "receiver": "…", "room_id": "…", "is_group": true, "scene": 0, "selector": "…" } ``` #### `forward_message` / `revoke_message` `forward_message` 必填字符串 `receiver`、`message_id`,需开启有效转发状态;`revoke_message` 必填字符串 `message_id`,受微信自身撤回时限约束。成功时的 `result` 分别为: ```json {"message_id":"1234567890123456789","receiver":"filehelper"} ``` ```json {"message_id":"1234567890123456789"} ``` 下载/发送确认和结果未知时的处理见本节表情转发说明;失败时检查外层 `ok`、`error`,不能因为未拿到确认就自动重发。 ### 14.4 联系人查询与添加 #### 联系人摘要对象 `friends`、`friends_all`、`groups_all`、`room_members` 使用相同的基础联系人字段,并按场景增加好友或群成员字段。本模式好友和群列表位于成功响应的 `result.items[]`;群成员数据位于 `result.members[]`。 | 字段 | 类型 | 出现条件 | 含义 | | --- | --- | --- | --- | | `account_id` | string | 恒有 | 内部微信 ID(wxid);群聊时为群 ID。好友接口的 `id` 通常使用此值 | | `display_name` | string | 恒有 | 好友展示名依次取备注、昵称、微信 ID;群成员场景可使用群内显示名 | | `remark` | string | 恒有 | 当前账号给对方设置的备注;无备注或取不到时为空串 | | `alias` | string | 恒有 | 对方设置的微信号;未设置或取不到时为空串,与内部 wxid 分开使用 | | `nick_name` | string | 恒有 | 微信昵称(群聊时为群名);取不到时为空串 | | `avatar_url` | string | 恒有 | 头像图片地址;取不到时为空串。JSON 中的 `\/` 解析后就是 `/` | | `sex` | int | 恒有 | `0` 未知、`1` 男、`2` 女;取不到时为 `0` | | `province` | string | 恒有 | 本地资料中的省份;没有有效值时为空串 | | `city` | string | 恒有 | 本地资料中的城市;没有有效值时为空串 | | `signature` | string | 恒有 | 微信个性签名;未设置或取不到时为空串 | | `contact_found` | bool | 恒有 | 是否在本地找到联系人对象;不能据此判断对方是否删除了你 | | `is_starred` | bool | 可读取状态时 | 当前账号是否将对方设为星标;`false` 为未设星标,未知时省略字段 | | `is_blacklisted` | bool | 可读取状态时 | 当前账号是否将对方加入黑名单;`false` 为未拉黑,未知时省略字段 | | `add_time` | int / null | 好友列表恒有 | 微信资料页使用的添加时间,Unix 秒级时间戳;没有有效记录或读取失败为 `null` | | `add_time_text` | string / null | 好友列表恒有 | 同一添加时间的公历文本,固定北京时间(UTC+8)、24 小时制,格式 `YYYY-MM-DD HH:mm:ss`;`add_time=null` 时同为 `null` | | `pay_name_last_char` | string | 好友列表恒有 | 微信支付脱敏姓名末尾可见的一个组合字符,例如“明”;没有有效结果时为空串,不提供完整姓名 | | `pay_name_status` | string | 好友列表恒有 | 支付姓名查询状态,取值见下表;好友列表只读取缓存 | | `is_owner` | bool | 群成员场景,可读取时 | 是否群主;未知时省略字段 | | `is_admin` | bool | 群成员场景,可读取时 | 是否管理员;未知时省略字段 | | `is_self` | bool | 群成员场景,可读取时 | 是否当前登录账号;未知时省略字段 | **空值与省略规则:** - 普通字符串的 `""` 表示未设置或本地没有取得有效值;不能据此推断对方资料一定为空。 - `add_time` 与 `add_time_text` 总是成对返回:有值时分别为秒级整数和 UTC+8 文本,未知时均为 `null`。不以 `0`、空串或 1970 年日期代替未知,也不使用本地添加流程时间兜底。 - `is_starred`、`is_blacklisted` 及群成员角色字段可能整个省略。字段缺失表示未知,不能按 `false` 处理。 添加时间来自微信当前保存的记录,与微信资料页使用的字段一致;不承诺删除后重加时仍是终身首次添加时间。文本不随手机时区、语言或日历设置变化。两个好友查询及同名命令均无需新增请求参数。 **支付姓名状态:** | `pay_name_status` | 含义 | | --- | --- | | `not_queried` | 尚未查询;姓名末字为空,不代表未实名 | | `querying` | 排队或查询中,尚无最终结果 | | `ok` | 已取得有效姓名末字,`pay_name_last_char` 非空 | | `unavailable` | 微信业务响应成功,但没有可用末字;不能据此判断未实名或被单删 | | `failed` | 查询失败 | `friends`、`friends_all` 本身不发起支付姓名查询,需要时调用 [`friend_pay_name`](#friend-pay-name);其结果不会改变昵称、备注或 `display_name`。 **完整好友条目示例(虚构数据,共 17 个字段):** ```json { "account_id": "wxid_example", "display_name": "示例备注", "remark": "示例备注", "alias": "wechat_example", "nick_name": "示例好友", "avatar_url": "", "sex": 0, "province": "", "city": "", "signature": "", "contact_found": true, "is_starred": false, "is_blacklisted": false, "add_time": 1704067200, "add_time_text": "2024-01-01 08:00:00", "pay_name_last_char": "", "pay_name_status": "not_queried" } ``` 时间未知时的字段节选: ```json {"add_time": null, "add_time_text": null} ``` #### `friends` `result.items[]` 为好友摘要,每条恒含 `add_time` 与 `add_time_text`,无需新增参数;完整字段与示例见[联系人摘要对象](#contact-summary)。 ```json { "keyword": "", "limit": 100, "offset": 0, "total": 256, "has_more": true, "items": [ "…" ] } ``` #### `friends_all` 一次性返回关键词过滤后的普通好友。`result.items[]` 的字段、添加时间时区及空值规则与 `friends` 相同。 ```json { "keyword": "", "total": 256, "friend_count": 256, "official_account_count": 12, "work_account_count": 0, "system_account_count": 4, "chatroom_count": 38, "stranger_count": 0, "cached_contact_count": 310, "items": [ "…" ] } ``` > `items` 只包含关键词过滤后的普通好友;`total`、`friend_count` 与 `items` 长度一致。公众号、企业微信、系统号、群聊和陌生人只参与各自分类计数,不混入好友数组。`cached_contact_count` 是未经过滤的原始缓存联系人条数。 #### `groups_all` ```json { "keyword": "", "total": 38, "items": [ "…" ] } ``` 无分页字段。 #### `room_members` ```json { "room_id": "…", "room_name": "…", "room_remark": "", "current_user": "wxid_self", "owner_user_name": "wxid_owner", "admin_count": 2, "admin_user_names": [ "…" ], "admin_members": [ "…" ], "self_is_owner": false, "self_is_admin": false, "member_count": 24, "members": [ "…" ] } ``` #### `friend_pay_name`:阻塞查询好友或群成员支付姓名末字 单次调用等待微信回调后返回最终结果,不返回 `querying` 中间结果,无需再次查询任务状态。微信主线程保持异步运行。 这里的“实名相关查询”指微信支付脱敏姓名的末字查询,不提供完整实名,也不能用查询结果判断对方是否已实名认证。 - `id` 必填,使用好友列表或 `room_members` 返回的个人 `account_id`。好友直接查询;非好友自动查找双方均在其中的普通微信群,无需传 `room_id`。不支持自己、群 ID、公众号、企业微信或系统号。 - `refresh` 为布尔值,默认 `false`。当前好友或共同群场景有缓存时立即返回终态(包括已缓存的失败);`true` 强制刷新或失败重试,并等待本次结果。同一目标、同一场景正在查询时合并等待;关系或群上下文变化后重新查询。 - 自动查找依赖本地群列表和成员资料;未找到可用共同群或资料未就绪时返回 `failed` 和中文原因。存在多个共同群时选择一个已核实的普通群,不轮询所有群发起支付查询。 - 原生查询串行执行,最多 32 个待处理请求;30 秒超时包含排队时间,接口等待保护上限约 31 秒。调用方超时建议至少 40 秒。 - 仅查询转账前信息,不提交金额或付款确认。结果只保留在当前账号会话内存中,账号或联系人服务会话变化后清空。 | `pay_name_status` | 命令外层 `ok` | 含义 | | --- | --- | --- | | `ok` | `true / 1` | 已取得有效的姓名末字,`pay_name_last_char` 非空 | | `unavailable` | `true / 1` | 微信业务响应成功,但没有可用末字;不能据此判断未实名或被单删 | | `failed` | `false / 0` | 参数、账号、兼容、网络、业务或超时失败,`error` 给出原因 | `friends`、`friends_all` 只附带好友场景缓存,本身不发起支付查询,群内非好友不会因此加入好友列表。列表仍可能显示 `not_queried` 或正在处理的 `querying`;单人查询接口和命令只返回上表中的终态。支付姓名字段不改变昵称、备注或 `display_name`,不提供完整姓名。 自动选择共同群的功能已由用户在微信 `8.0.80` 验证通过。 下发一次命令: ```json { "type": "command", "request_id": "pay-name-001", "command_name": "friend_pay_name", "args": { "id": "wxid_example", "refresh": false } } ``` 插件等到终态后,使用同一 `request_id` 返回一个 `command_result` 帧: ```json { "type": "command_result", "request_id": "pay-name-001", "command_name": "friend_pay_name", "ok": true, "result": { "account_id": "wxid_example", "pay_name_last_char": "明", "pay_name_status": "ok" }, "error": "" } ``` 不再下发状态查询命令;业务失败时外层 `ok=false`、`error` 给出原因,`result` 保留目标好友和姓名状态。外层身份字段 `account_id` 是登录账号,`result.account_id` 才是目标好友。 #### 主动添加好友 命令 `search_contact`、`add_friend`、`friend_add_probe` 的参数放在 `args`,结果使用既有 `request_id/command_name/ok/result/error` 信封;业务字段在 `result`,顶层 `ok/error` 不再重复放入 `result`。 下发搜索和添加的完整 WebSocket 帧: ```json {"type":"command","request_id":"search_demo_1","command_name":"search_contact","args":{"keyword":"示例微信号"}} ``` ```json {"type":"command","request_id":"add_demo_1","command_name":"add_friend","args":{"contact_ref":"fc_example_ref","say_content":"你好,我是某某"}} ``` 添加结果信封示例: ```json { "type": "command_result", "request_id":"add_demo_1", "command_name":"add_friend", "ok":true, "result":{ "status":"request_sent","username":"example_user","is_friend":false, "contact_ref":"fc_example_ref","native_ret":0,"stage":"send_verification", "request_submitted":true,"event_id":null }, "error":"" } ``` 以下为三个命令共用的业务字段与本模式结果示例,`ok/error` 位于结果帧外层。 主动添加使用两步调用:先搜索获得 `contact_ref`,再用该引用提交验证消息。支持普通个人联系人;公众号、群、OpenIM 和当前账号不提供可添加的引用。`friends.keyword` 仍只过滤本地好友列表。 搜索参数为 `{"keyword":"示例微信号"}`,`keyword` 必须是 1–256 个字符的字符串。支持微信实际允许搜索的微信号、手机号等,不保证任意原始 wxid 可搜索。搜索结果帧示例(由 HTTP 真机业务回包转换为本模式格式,身份和头像链接已替换): ```json { "type": "command_result", "request_id": "search_demo_1", "command_name": "search_contact", "ok": true, "result": { "status": "found", "contacts": [ { "contact_ref": "fc_example_ref", "username": "example_user", "nick_name": "示例昵称", "alias": "example_alias", "avatar_url": "https://example.com/avatar/132", "avatar_hd_url": "https://example.com/avatar/0", "sex": 1, "country": "", "province": "", "city": "", "signature": "", "supported": true, "is_friend": true, "source": "wechat_id" } ], "expires_in": 300, "native_ret": 0, "stage": "search", "request_submitted": true, "event_id": 42, "request_cgi": 106, "message_type": 0, "transport_error_type": 0, "transport_error_code": 0 }, "error": "" } ``` `contacts[]` 同时返回本次搜索取得的公开资料,无需先加好友,也不会额外发起资料查询: | 字段 | 类型 | 说明 | |---|---|---| | `avatar_url` | string | 头像链接;微信未返回时为空字符串 | | `avatar_hd_url` | string | 大图头像链接;微信未返回时为空字符串 | | `sex` | integer | 性别:0 未知、1 男、2 女;未取得时为 0 | | `country` | string | 国家/地区,沿用微信返回值;未取得时为空字符串 | | `province` | string | 省份,沿用微信返回值;未取得时为空字符串 | | `city` | string | 城市,沿用微信返回值;未取得时为空字符串 | | `signature` | string | 个性签名;未取得时为空字符串 | 以上字段在每个候选中均返回,是否有值取决于微信本次回包及对方公开资料;地区是个人资料填写值,不代表实时位置。它们不改变 `supported` 或 `contact_ref` 的规则。 `country`、`province`、`city`、`signature` 为空字符串时,表示本次搜索未取得对应资料,不影响 `status:found`;仅凭该结果无法区分对方未设置资料,还是微信搜索回包未提供。 `is_friend` 来自本地普通好友判断。为 `true` 时无需再发好友申请,正常调用 `add_friend` 会返回 `already_friend`。`supported:true` 表示候选类型符合当前添加流程,不代表对方是陌生人;已有好友也可能同时返回 `supported:true` 和 `contact_ref`。 `source` 为 `wechat_id`、`phone` 或 `qq`,由微信搜索匹配类型决定。`username` 是微信原生返回的标识,可能是加密标识,不能一律当成稳定 wxid。多候选由调用方选择;`supported:false` 的候选没有 `contact_ref`,不能提交添加。确认未找到时返回 `ok:true,status:not_found,contacts:[]`;传输错误、解析错误和业务失败返回 `ok:false`,不能当成“用户不存在”。 添加参数: ```json {"contact_ref":"fc_example_ref","say_content":"你好,我是某某"} ``` `contact_ref` 必填,只能使用本账号本次登录会话的搜索结果;`say_content` 可省略,默认为空字符串,不接受数字或 null。输入会去除首尾空白,并按微信原生长度规则校验;过长时返回 `INVALID_SAY_CONTENT`,不会悄悄截断发送。不能直接传 `username`、scene、opcode 或票据替代引用。好友添加遵循插件有效发送状态。 添加成功示例: ```json { "type": "command_result", "request_id": "add_demo_1", "command_name": "add_friend", "ok": true, "result": { "status": "request_sent", "username": "example_user", "is_friend": false, "contact_ref": "fc_example_ref", "native_ret": 0, "stage": "send_verification", "request_submitted": true, "event_id": 43, "request_cgi": 137 }, "error": "" } ``` | status | ok | 含义及下一步 | |---|---|---| | `already_friend` | true | 本地联系人服务确认已经是普通好友,本次未发添加请求,`native_ret:null` | | `request_sent` | true | 微信确认验证申请发送成功,等待对方同意;不能当成已加好友 | | `added` | true | 微信添加成功且原生后处理完成,本地普通好友判断通过 | | `pending_sync` | true | 微信添加成功且原生后处理已运行,本地好友状态尚未确认;稍后通过 `friends`/`friends_all` 核对 | | `needs_user_action` | false | 微信要求人工处理或预检查失败;在微信内查看,不自动确认或绕过限制 | | `timeout` | false | 30 秒内未取得最终结果,结果不确定;先核对好友状态,不自动重发 | | `failed` | false | 参数、会话、运行时或微信请求失败,读取 `error_code`、`error`、`native_ret` | 引用在插件内存保留 300 秒,最多 50 个;这个时间是本地缓存策略,不代表微信票据寿命。重启、退出登录或账号/上下文变化后必须重新搜索。相同引用及相同规范化消息的并发调用共享操作,完成后在引用有效期内复用结果;消息不同返回 `CONTACT_REF_CONFLICT`。同目标的另一引用正在添加时返回 `ADD_IN_PROGRESS`。超时后的相同引用仍返回原结果,不会自动重发。跨引用、跨重启和超过缓存期不承诺去重,后端也应避免重复下发。 两项操作均在当前请求内等待,插件操作期限为 30 秒,调用方建议至少等待 40 秒。`native_ret` 未取得微信业务结果时为 null;不能把 null 当 0。搜索成功或微信添加成功为 0,部分未找到搜索结果保留微信 `-4`。`stage` 为 `preparing`、`search`、`precheck`、`add_contact` 或 `send_verification`。`request_submitted` 只表示搜索/添加请求获原生受理(预检查不单独计入),不代表服务器已成功处理;`request_cgi` 为当前请求/最近回包的 CGI(搜索 106、添加 137、补取联系人 182、预检查 11568);搜索的 `event_id` 为提交返回的 ID,添加为最近原生回调的 ID,尚无回包时为 null。它们仅用于诊断。 常见错误码:`INVALID_KEYWORD`、`INVALID_PARAMETERS`、`INVALID_SAY_CONTENT`、`SEND_DISABLED`、`ACCOUNT_UNAVAILABLE`、`ACCOUNT_CHANGED`、`CONTACT_REF_EXPIRED`、`CONTACT_REF_CONFLICT`、`ADD_IN_PROGRESS`、`CANDIDATE_CACHE_FULL`、`TOO_MANY_REQUESTS`、`NATIVE_API_UNAVAILABLE`、`REQUEST_NOT_STARTED`、`PRECHECK_NOT_STARTED`、`PRECHECK_FAILED`、`WECHAT_INTERACTION_REQUIRED`、`WECHAT_REQUEST_FAILED`、`SEARCH_FAILED`、`WECHAT_TRANSPORT_FAILED`、`RESPONSE_INCOMPLETE`、`RESPONSE_INVALID`、`FRIEND_OPERATION_TIMEOUT`。微信返回普通 `-44` 时最多一次进入原生发送验证消息入口;交互限制和预检查失败不会自动继续。 探针接受 `{}` 或 `{"contact_ref":"fc_example_ref"}`。它不发送搜索/好友申请,返回 `search_runtime`、`add_runtime`(`ready` 及各 selector 的 `compatible/encoding`)、`logged_in`、`send_enabled`、`cached_candidates`、`pending_operations`、`contact_ref_valid`、`expires_in`、`has_antispam_ticket`、`reference_status` 和 `last_operation`。`last_operation` 只保留最近一次操作的状态、阶段、返回码和受理信息;不包含账号、关键词、验证消息或原始票据。出现 `NATIVE_API_UNAVAILABLE` 时先调用探针定位不兼容项。 已取得用户提供的 HTTP 真机搜索成功回包,确认本次已有好友场景返回头像、大图头像、性别和 `is_friend:true`,地区及签名为空。用户现已确认搜索与添加好友功能在微信 `8.0.80` 验证通过;资料字段是否有值仍以微信本次回包为准,人工处理和超时按本节状态规则处理。 `-44` 和发送状态的判断:`stage:add_contact` 表示直接添加阶段;`request_submitted:true` 只表示原生请求受理,不能据此认定验证申请已经发出。`verification_attempted` 表示是否进入过发送验证消息入口;只有最终 `status:request_sent` 才表示微信确认验证申请发送成功。旧包在 `add_contact` 返回 `needs_user_action,-44` 时可能尚未进入验证消息阶段。 交互条件按现有原生链路判断:spam 提示标题、正文及 ctx 同时非空才要求人工确认;仅存在空的 `verifyUserSpamInfo` 对象或仅有 ctx,不会拦住普通 `-44` 的一次验证消息转换。微信原生可处理的 XML 提示、预检查失败及其他明确限制仍停止自动继续。 脱敏 `failure_detail`,保留最近一次错误分类,即使随后发送验证成功也可能保留此前的 `verification_required`:`reason` 为 `spam_challenge`、`native_error_dialog`、`error_xml_unreadable`、`precheck_failed`、`verification_required` 或 `wechat_error`;`native_ret` 为该次错误码;并提供 `has_spam_info`、`has_spam_title`、`has_spam_detail`、`has_context`、`spam_challenge`、`has_error_xml`、`xml_show_type`、`xml_handled`、`xml_unreadable`。不返回原始 ctx、XML、票据或提示文本。最终结果以外层 `status/native_ret` 为准,不能把历史 failure_detail 当成最终失败。 `friend_add_probe` 的 `last_operation` 同时返回 `verification_attempted` 和 `failure_detail`;群场景也可在 `room_candidate.last_result` 查看。该判定和诊断同时用于 `add_friend` 与 `add_room_friend`,不新增接口。 成功回包按实际请求区分:CGI137 的 opcode 1(直接添加)仍要求联系人标识;opcode 2(发送验证消息)允许回包缺少用户名,插件继续执行微信原生申请记录保存和成功回调,收到 `contactSendRequestOk:` 后才返回 `request_sent`。不会仅凭 `native_ret:0` 合成成功。旧包在 `send_verification` 阶段可能将这种正常回包误报为 `RESPONSE_INCOMPLETE`,并把已取得的 0 丢成 null;该旧结果不能证明服务器拒绝申请,应先核对状态,避免重复发送。 取得 CGI137 回包后,可选脱敏 `response_detail`:`native_ret` 为本次业务返回码,`request_opcode` 为实际请求操作码(无法读取时为 null),`has_response_username` 表示回包用户名是否非空。添加结果、探针 `last_operation` 及群候选 `last_result` 均可查看。若微信返回 0 后插件本地校验或后处理失败,外层仍保留 `native_ret:0`,须同时检查 `ok/status/error_code`;0 不代表插件已完成全部处理。没有本次回包时不提供该详情;探针未发生操作时为 `{}`。详情不包含用户名、消息正文或票据。 #### 添加群成员好友 命令为 `add_room_friend`,参数放在 `args`,完整 WebSocket 帧: ```json {"type":"command","request_id":"room_add_demo_1","command_name":"add_room_friend","args":{"room_id":"123456789@chatroom","wxid":"wxid_example_member","say_content":"你好,我们在同一个群里"}} ``` 结果沿用 `command_result` 信封,`ok/error` 在外层,其余业务字段位于 `result`。`friend_add_probe` 同样接受 `args.room_id` 和 `args.wxid`,且不发送申请。 已知共同群和成员原始 wxid 时,使用独立的 `add_room_friend`。搜索添加仍为 `search_contact → add_friend`,不把 wxid 塞进搜索流程,也不要求调用方提供手机号、微信号、scene 或票据。 请求参数: ```json {"room_id":"123456789@chatroom","wxid":"wxid_example_member","say_content":"你好,我们在同一个群里"} ``` - `room_id`、`wxid` 必填,均为 1–256 字符的字符串,不含空白。只支持普通 `@chatroom` 群和个人联系人,暂不支持 OpenIM、公众号、群联系人或本人。 - `wxid` 使用 `/room_members`/`room_members` 成员结果的 `account_id`(原始成员标识),也可使用已取得的原始 wxid;不能传昵称或群昵称。`room_id` 必须是该成员所在的共同群。 - `say_content` 可省略,默认为空;仅接受字符串,会去除首尾空白,沿用 `add_friend` 的原生长度校验,过长拒绝发送。 插件先复用当前账号状态、群联系人和群成员列表入口,确认本人和目标都在该群;再按原生群资料链路请求 CGI182,携带目标和 `fromChatRoom`,按用户名匹配本次响应的群成员票据。只有取得有效联系人和票据后,才将群 ID、scene 14 和完整联系人交给 `add_friend` 共用的原生预检/添加执行器;普通 `-44` 最多一次转换为发送验证消息。调用方无需点击页面,也无需传递原始票据。列表缺失或成员关系未就绪会失败,不自动同步整个群。 WebSocket 结果帧格式示例(身份字段为示意值): ```json { "type": "command_result", "request_id": "room_add_demo_1", "command_name": "add_room_friend", "ok": true, "result": { "status": "request_sent", "source": "room", "room_id": "123456789@chatroom", "username": "wxid_example_member", "is_friend": false, "native_ret": 0, "stage": "send_verification", "contact_fetch_submitted": true, "has_group_member_ticket": true, "request_submitted": true, "request_cgi": 137, "event_id": 42 }, "error": "" } ``` `status` 沿用搜索添加的 `already_friend`、`request_sent`、`added`、`pending_sync`、`needs_user_action`、`timeout`、`failed`;`request_sent` 仅表示微信确认申请已发送。已有好友不取群票据、不发添加请求。`native_ret:null` 表示没有可用的微信业务结果。群路径不返回 `contact_ref`;前置校验失败可能只有公共错误字段。 `stage` 增加 `room_contact`。`contact_fetch_submitted` 仅表示群资料请求获原生受理,`has_group_member_ticket` 仅表示本次取得的票据非空,二者都不表示好友申请成功。`request_submitted` 仍仅表示共享添加链路获原生受理,不包含群资料请求和独立预检查。`request_cgi`/`event_id` 为当前或最近回包诊断,CGI182 为群资料、11568 为预检查、137 为添加。发生超时、回包不完整或后处理失败时,先核对好友状态,不自动重发。 同一登录会话、同一群、同一成员、相同规范化文字,在首次请求创建起 300 秒内共享在途操作或复用最终结果(包括失败和超时);不同文字返回 `ROOM_ADD_CONFLICT`。最多保留 50 个群操作记录;共享在途上限 16 个。同一目标经另一群或搜索路径正在添加时返回 `ADD_IN_PROGRESS`。跨群、跨重启、切换账号或超过窗口不承诺去重,后端应记录操作结果,避免重复下发。整个流程单次等待,操作期限 30 秒,调用方超时建议至少 40 秒。 群相关错误包括 `INVALID_PARAMETERS`、`ROOM_NOT_FOUND`、`ROOM_MEMBERS_UNAVAILABLE`、`NOT_IN_ROOM`、`CANNOT_ADD_SELF`、`TARGET_NOT_IN_ROOM`、`UNSUPPORTED_CONTACT`、`ROOM_CONTACT_FETCH_FAILED`、`ROOM_CONTACT_NOT_FOUND`、`GROUP_MEMBER_TICKET_MISSING`、`ROOM_ADD_CONFLICT`;通用运行时、有效发送状态、切号、超时、预检及微信限制错误沿用 `add_friend`。`GROUP_MEMBER_TICKET_MISSING` 明确表示未发起好友申请,不会退回无来源 wxid 添加。 排障继续使用只读 `friend_add_probe` 命令,`args` 可传: ```json {"room_id":"123456789@chatroom","wxid":"wxid_example_member"} ``` `room_runtime`(群链路签名 `ready/checks`)、`room_preflight`(本地成员校验结果或明确错误)、`room_candidate`(`cached/status/has_contact/has_group_member_ticket/last_result`);`last_result` 为该群成员缓存的完整业务结果,仅在调用过添加后存在。`last_operation` 增加 `source:search|room`,仍为脱敏诊断。探针不联网取资料、不发送申请,不返回原始票据。首次使用先检查 `add_runtime.ready` 和 `room_runtime.ready`,再检查 `room_preflight.ok`;添加失败后用同一参数查看阶段与原始返回码。 群成员添加好友已由用户在微信 `8.0.80` 验证通过;受限/人工处理和重复请求仍按本节约定处理。 #### `delete_friend` / `delete_session` / `quit_group` ```json { "id": "…" } ``` ### 14.5 群聊信息与群昵称 #### `kick_group_member` ```json { "room_id": "…", "member_ids": ["wxid_a", "wxid_b"], "member_count": 2 } ``` `member_id` 与 `member_ids` 可同时传,自动合并去重。 #### `rename_group` / `set_group_self_nickname`(同一命令的两个名字) 修改**当前账号在该群里的群昵称**(微信里「我在本群的昵称」),不是修改群名称。 ```json { "room_id": "…", "new_name": "我在本群的新昵称" } ``` `new_name` **允许空串**,表示清空群昵称、恢复默认(显示微信昵称): ```json { "room_id": "…", "new_name": "" } ``` 返回:`{"room_id": "…", "new_name": "", "clears_group_nickname": true}`。整个字段缺失才报 `new_name is required`。 #### `set_group_name` 修改**整个群聊的名称**。与上面的群内个人昵称命令不同,`room_id` 必须是以 `@chatroom` 结尾的字符串,`new_name` 必须是去除首尾空白后非空的字符串;不支持传空串清空群名。 下发时使用 `command_name: "set_group_name"`,`args` 示例: ```json { "room_id": "123456@chatroom", "new_name": "新的群聊名称" } ``` 成功时,命令结果帧的 `result` 示例: ```json { "room_id": "123456@chatroom", "new_name": "新的群聊名称", "accepted": true, "local_verified": true, "already_named": false, "local_name": "新的群聊名称" } ``` `accepted` 只表示微信底层接受;`local_verified` 表示本机群联系人的 `nick_name` 已读回目标值。`ok: true` 时 `local_verified` 仍可能为 `false`。如果本地群名已是目标值,返回 `ok: true`、`already_named: true`、`accepted: false`、`local_verified: true`,不重复提交。以上结果不代表其他成员端已经同步。 失败时结果帧的 `ok: false`,`error` 为具体原因,`result` 保留请求字段和状态字段。只读探针命令 `set_group_name_probe` 的 `args` 只需 `room_id`;它不修改群名,需检查结果中的 `set_group_name_ready`,不能只看帧的 `ok`。 #### `get_group_notice` `args` 只需 `{"room_id":"123456@chatroom"}`。成功时 `command_result.result` 示例: ```json { "room_id": "123456@chatroom", "cached_notice_found": true, "summary": "微信提供的文本摘要", "html": "

公告正文

", "html_cached": true } ``` `summary` 只是微信提供的文本摘要;`html` 是本地缓存文件中的完整原始 HTML。正文文件未缓存或无法读取时,`html` 为 `null`、`html_cached` 为 `false`,仍可能有摘要。本地未读到公告项时,`cached_notice_found` 为 `false`,`summary` 和 `html` 均为 `null`;这不能证明群里没有公告。接口不触发下载或发布,缓存也可能不是最新内容。请求失败时看结果帧的 `ok: false` 和 `error`。 #### `get_chatroom_qrcode`:阻塞获取指定群二维码 每次调用按微信原生协议创建独立请求,通过当前登录会话的 `EventService` 发起取码,在同一次调用中等待微信本次响应;不返回查询中状态,不需要任务 ID。同群并发共用正在处理的请求,完成后再次调用会重新取码。最多 32 个不同群的待处理请求,约 30 秒超时,接口等待保护上限约 31 秒;调用方超时建议至少 40 秒。 唯一参数 `room_id` 必须是实际群 ID 字符串,例如 `example@chatroom`,不能传群名。当前账号须能找到该群联系人,最终权限以微信响应为准。 | 字段 | 类型 | 含义 | | --- | --- | --- | | `room_id` | string | 目标群 ID | | `status` | string | `ok` / `restricted` / `failed` / `timeout` | | `qr_code_url` | string / null | 本次响应里的二维码内容,即扫码承载的入群链接;不是 PNG 下载地址 | | `has_qrcode` | bool / 0或1 | 是否取得 URL 或可解码图片;受限提示图也会为真,不代表可扫码入群 | | `has_image` | bool / 0或1 | 是否含可解码图片,可能是受限提示图 | | `image_base64` | string / null | 服务器返回图片经解码后统一编码的 PNG base64,不含 data URI 前缀 | | `image_mime_type` | string / null | 有图片时为 `image/png` | | `qrcode_status` | uint32 / null | 微信二维码原始状态;不是 BOOL,缺失时不补 0 | | `restricted` | bool / 0或1 / null | 已知二维码状态是否非零;未知时为 null | | `footer_wording` | string | 微信本次返回的说明,可能包含有效期或限制文字,不自行推算 | | `wechat_ret` | int32 / null | 微信业务原始错误码 | | `wechat_error_message` | string | 微信 `baseResponse.errMsg.string` 原文,可能为空 | | `message_type` | uint32 / null | 微信网络消息类型,1 视作失败 | | `transport_error_type` / `transport_error_code` | int32 / null | 微信传输错误分类及原码,保留负数 | | `error_code` | string | 失败或受限时的插件错误分类 | | `request_submitted` | bool / 0或1 | 本次原生事件服务是否返回非零事件 ID,不等于服务器业务成功 | | `request_cgi` | uint32 / null | 本次请求 CGI:普通群 168、OpenIM 890,准备前失败为 null | | `event_id` | uint32 / null | 本次已受理的原生事件 ID,仅用于诊断,无需用它轮询 | `status=ok` 表示本次业务响应成功且取得二维码内容,不能保证扫码立即入群。`restricted` 对应微信非零二维码状态,业务 `ok=false`,`error` 提供中文分类说明;具体原因可能在可读文本字段或返回图片中;即使有图或 URL 也不作为普通成功处理。没有恢复具体状态枚举,不写死“1=超员、2=开启验证”。网络失败、微信 `ret != 0`、响应不完整和超时均返回失败信息,不返回旧缓存的码。 人数阈值由微信配置控制(静态样本默认 200,客户端判断人数达到阈值);开启管理员进群验证也不一定禁止取码,新管理方式可以扫码申请审批。因此不按人数或验证开关在本地直接拒绝,以本次微信原码与文案为准。 普通群可能只有 URL,没有服务器图片;调用方可用 `qr_code_url` 自行生成二维码,无需手机下载再上传。接口不会读取微信磁盘缓存,也不返回本地文件路径。微信保存的完整分享卡片还会组合群头像、群名和有效期提示,本接口不拼装该卡片。 OpenIM 按微信原生群类型判断分流至 CGI890;该分支可能只有图片,没有 URL,也没有二维码状态,此时 `qrcode_status`、`restricted` 为 null。仍校验本次原始业务响应,不能依赖原生固定为 0 的完成通知。 常见 `error_code`:`INVALID_ROOM_ID`、`ACCOUNT_UNAVAILABLE`、`ACCOUNT_CHANGED`、`ROOM_NOT_FOUND`、`NATIVE_API_UNAVAILABLE`、`TOO_MANY_REQUESTS`、`REQUEST_NOT_STARTED`、`NATIVE_REQUEST_FAILED`、`RESPONSE_INVALID`、`WECHAT_REQUEST_FAILED`、`RESPONSE_INCOMPLETE`、`QRCODE_RESTRICTED`、`QRCODE_TIMEOUT`。`REQUEST_NOT_STARTED` 仅在原生事件服务返回事件 ID 为 0 时使用,不能据此推断群人数或验证限制。`request_submitted=true` 仅表示微信事件服务已受理,不代表服务器成功;若之后返回 `QRCODE_TIMEOUT`,表示尚未取得本次最终响应。 中文原因取自微信可读字段:`footer_wording` 和 `wechat_error_message`。受限时优先使用非空 `footer_wording` 作为 `error`;没有该文案时返回通用中文“微信返回二维码受限状态”,并原样保留 `wechat_error_message`。微信可能将具体原因直接写在返回图片里,两个文本字段均为空;本接口不做图片文字识别,也不根据 `qrcode_status=1` 等原始值硬编码具体原因。 用户提供的普通群真机响应已确认:请求受理并收到 `wechat_ret=0`,`qrcode_status=1`,返回 PNG,`qr_code_url=null`,`footer_wording`、`wechat_error_message` 均为空;人工查看图片文字为“该群已开启进群验证,只可通过邀请进群”。这只说明该次图片的含义,不是所有状态值 1 的固定映射。 `has_qrcode`、`has_image`、`restricted`、`request_submitted` 可能序列化为布尔值或数字 `0/1`,`restricted` 还可能为 null。`has_image=1`、`has_qrcode=1` 只表示有返回内容,不保证图片是可用的入群码。业务是否成功应先检查 `ok` 和 `status`。 群二维码功能已由用户在微信 `8.0.80` 验证通过;取码成功不保证立即入群,实际扫码仍受群设置及微信返回限制。上述受限图片记录不构成固定状态码映射,也不能据此扩大 OpenIM 支持范围。 受限响应示例(节选;群 ID 和事件 ID 为占位值,实际 `image_base64` 是 PNG Base64 字符串,本文省略图片内容): ```json { "type": "command_result", "request_id": "group-qr-001", "command_name": "get_chatroom_qrcode", "ok": false, "result": { "room_id": "example@chatroom", "status": "restricted", "qr_code_url": null, "has_qrcode": 1, "has_image": 1, "image_mime_type": "image/png", "qrcode_status": 1, "restricted": 1, "footer_wording": "", "wechat_ret": 0, "wechat_error_message": "", "message_type": 0, "transport_error_type": 0, "transport_error_code": 0, "request_submitted": 1, "request_cgi": 168, "event_id": 42, "error_code": "QRCODE_RESTRICTED" }, "error": "微信返回二维码受限状态" } ``` 下发一次命令: ```json { "type": "command", "request_id": "group-qr-001", "command_name": "get_chatroom_qrcode", "args": { "room_id": "example@chatroom" } } ``` 最终结果示例(省略公共身份与时间字段): ```json { "type": "command_result", "request_id": "group-qr-001", "command_name": "get_chatroom_qrcode", "ok": true, "result": { "room_id": "example@chatroom", "status": "ok", "qr_code_url": "https://weixin.qq.com/g/example", "has_qrcode": true, "has_image": false, "image_base64": null, "image_mime_type": null, "qrcode_status": 0, "restricted": false, "footer_wording": "", "wechat_ret": 0, "wechat_error_message": "", "message_type": 0, "transport_error_type": 0, "transport_error_code": 0, "request_submitted": true, "request_cgi": 168, "event_id": 42 }, "error": "" } ``` `ok`、`error` 在命令信封外层,`result` 保留原始状态、错误码与文案。调用方等待最终命令结果的期限建议至少 40 秒,等待期间插件继续收包和心跳。支付姓名与群二维码查询都按当前账号会话合并在途请求:支付姓名复用终态缓存,群二维码每次重新取码,二者均不使用仅按 `request_id` 保存的通用旧结果。 ### 14.6 红包与转账 领取红包和红包详情的参数见上方分类索引;转账操作按本节保存编号并判断资金状态。 #### 确认收款 `receive_transfer` 参数 `transfer_id`、`message_id` 均为 string,至少一项非空;推荐两个都传且对应同一笔转账。结果使用本模式的 `command_result` 信封,`ok`、`error` 位于外层,精简业务字段位于 `result`。进入收款业务后的失败同样保留这些字段;插件/有效发送状态、权限或队列等前置拒绝沿用通用失败信封,不保证业务字段齐全。 | `result` 字段 | 类型 | 说明 | |---|---|---| | `transfer_id`、`message_id` | string | 本笔转账与消息编号,未能解析时可能为空字符串 | | `status` | string | 本次收款状态;见下方说明 | | `accepted` | boolean | 已进入现有收款流程的状态标记;不能代替 `confirmed` 判断到账 | | `confirmed` | boolean | 是否已确认本笔收款成功 | | `callback_received` | boolean | 是否收到匹配本次操作的确认收款回包;独立查询完成确认时也可能为 `false` | | `ret_code` | integer / null | 确认收款回包返回码;没有取得合法返回码时为 `null`,不以 `0` 代填 | | `native_status` | integer(可选) | 仅在真实资金状态查询完成本次收款操作时出现;不根据 `callback_ok` 推测为 2001 | 收到正常确认收款回包的结果示例(省略公共设备/会话字段): ```json { "type": "command_result", "request_id": "receive_example_1", "command_name": "receive_transfer", "ok": true, "result": { "transfer_id": "transfer_example", "message_id": "message_example", "status": "callback_ok", "accepted": true, "confirmed": true, "callback_received": true, "ret_code": 0 }, "error": "" } ``` 命令等待原生回包约 5 秒。`status="confirm_invoked"` 表示已发起收款、等待已超时,返回外层 `ok=true`,业务字段为 `accepted=true, confirmed=false, callback_received=false, ret_code=null`,不能认定到账。`callback_received` 仅是辅助信号;匹配实际确认收款回包且成功时为 `callback_ok`,或独立查询取得原生状态 2001 后为 `confirmed`,才会得到外层 `ok=true, result.confirmed=true`。 收到回包却缺少合法返回码时,失败结果示例: ```json { "type": "command_result", "request_id": "receive_example_1", "command_name": "receive_transfer", "ok": false, "result": { "transfer_id": "transfer_example", "message_id": "message_example", "status": "callback_unknown", "accepted": true, "confirmed": false, "callback_received": true, "ret_code": null }, "error": "转账回调缺少状态码,收款结果未知,请勿自动重试" } ``` `callback_unknown` 或等待超时应先用 `get_transfer_status` 查询,原生状态 2001 才确认已收取;不要直接重试。接收与退还同笔互斥,未知结果先查终态。 业务结果不再包含整份 `content`/`content_xml`、`native_url`、`callback`、`pay_info_probe` 或 `runtime_probe`。排错可使用本模式已有的 `transfer_probe` 命令,`args` 传 `transfer_id`/`message_id`,或通过 `bridge_status` 读取完整诊断;探针和诊断快照不能代替业务成功判断。 #### 转账:退还与查询状态 **命令**:`refund_transfer` 退还待收取转账;`get_transfer_status` 只查询服务器资金状态,均已加入 `capabilities`。退还要求插件已启用且有效发送状态开启;查询不受有效发送状态限制,仍需通过本模式配置、鉴权和插件全局校验。 WebSocket 下行 `command` 帧示例;查询时只把 `command_name` 改为 `get_transfer_status`,使用新的外层 request_id: ```json { "type": "command", "request_id": "refund_example_1", "command_name": "refund_transfer", "args": { "transfer_id": "transfer_example", "message_id": "message_example" } } ``` 结果沿本模式 `command_result` 外壳,`ok`、`error` 放外层,其他基础字段(包括 `error_code`)保留在 `result`;下面省略公共设备/会话字段。失败也要读取 result,不能从 diagnostics 猜测结果。 ```json { "type": "command_result", "request_id": "refund_example_1", "command_name": "refund_transfer", "ok": true, "result": { "transfer_id": "transfer_example", "message_id": "message_example", "accepted": true, "confirmed": false, "status": "refund_pending", "native_status": null, "ret_code": null, "error_code": "" }, "error": "" } ``` ```json { "type": "command_result", "request_id": "query_example_1", "command_name": "get_transfer_status", "ok": true, "result": { "transfer_id": "transfer_example", "message_id": "message_example", "accepted": false, "confirmed": false, "status": "waiting_receive", "native_status": 2000, "ret_code": 0, "error_code": "", "can_refund": true, "status_description": "等待收款", "native_status_name": "waiting_receive" }, "error": "" } ``` ```json { "type": "command_result", "request_id": "refund_example_1", "command_name": "refund_transfer", "ok": true, "result": { "transfer_id": "transfer_example", "message_id": "message_example", "accepted": true, "confirmed": true, "status": "refunded", "native_status": 2002, "ret_code": 0, "error_code": "" }, "error": "" } ``` ```json { "type": "command_result", "request_id": "refund_example_1", "command_name": "refund_transfer", "ok": false, "result": { "transfer_id": "", "message_id": "", "accepted": false, "confirmed": false, "status": "failed", "native_status": null, "ret_code": null, "error_code": "CACHE_NOT_FOUND" }, "error": "转账上下文已失效,请使用本次运行收到的转账消息" } ``` **本轮状态(2026-11-05)**:用户此前已确认普通微信群内指定给自己的个人转账可领取;在 `0.8.79-79` 交付后,用户回传同笔转账查询 `waiting_receive / native_status=2000 / can_refund=true`,以及退还后 `refunded / native_status=2002 / accepted=true / confirmed=true / ret_code=0`,群内个人转账的查询和退还正常路径已实测通过。接口参数及结果结构不变,群 ID 从原消息读取,无需调用方补传。标准 Ubuntu 构建通过,新包为 `0.8.79-79`;旧包 `0.8.79-78` 的查询/退还仍保留群限制。此次回包未单独覆盖 WebSocket/长轮询外层收发、结果事件、重复操作、断网与账号切换,也未重新回传接收结果。 **范围与前置条件**:本轮源码支持当前登录账号作为收款人的普通微信个人转账(`app_msg_type=2000`),包括普通微信群内他人指定给当前账号的个人转账;不包括群收款/AA、企业互通群、任一方为 `@openim` 的交易、已收取后的退款或部分金额退还。群内转账须能读取原消息的个人付款人、接收人及群 ID,并确认本地群联系人属于普通微信群;群资料缺失或类型无法确认时仍拒绝。查询与退还都依赖本次运行收到的原转账消息,并要求缓存包含付款人、交易编号和已明确读取的非负整数有效期(允许 0,不等同于字段缺失);本版不提供按历史交易号恢复上下文的入口。上下文按当前账号保存在内存中,最多 120 笔;重启、账号会话变化或淘汰后可能返回 `CACHE_NOT_FOUND`,持久化 ID 不能恢复该缓存。 **请求参数**:两个接口使用同一组参数,无其他业务输入;不能补传付款人、金额、交易号或有效期绕过原消息核验。 | 参数 | 类型 | 必填与默认值 | 说明 | |---|---|---|---| | `transfer_id` | string | 与 `message_id` 至少一项非空;无默认值 | 原转账事件的转账 ID,按字符串原样保存 | | `message_id` | string | 与 `transfer_id` 至少一项非空;无默认值 | 原转账消息 ID,按字符串原样保存 | 可以省略未提供的字段;不得传数字、`null`、数组或对象。去除首尾空白后两个字段都为空时失败;同时提供时必须指向同一笔,否则 `ID_MISMATCH`。 **结果字段**:下表说明业务结果,外层封装按本节本模式的示例读取。进入业务服务后固定返回这些基础字段;通道鉴权、插件禁用等更早的拒绝可采用本模式通用错误外壳,不能假设总有业务字段。 | 字段 | 类型 | 说明 | |---|---|---| | `ok` | boolean | 本次调用是否按该操作的约定完成;查询成功或退还已进入等待都可能为 true,不代表已退还 | | `transfer_id` | string | 解析到的转账 ID;前置失败可能为空串 | | `message_id` | string | 解析到的消息 ID;前置失败可能为空串 | | `accepted` | boolean | 退还动作已进入原生提交阶段;查询恒为 false。true 时即使发生异常,也不能排除已发出交易 | | `confirmed` | boolean | 已取得原生终态 2001/2002/2003;不是退还成功专属标志 | | `status` | string | 规范化业务状态,见下表 | | `native_status` | integer 或 null | 微信原生资金状态;未取得为 null,未知数值保留原值 | | `ret_code` | integer 或 null | 微信回包错误码;未取得为 null;非零时结合 error_code 判断 | | `error_code` | string | 业务错误码;成功时为空字符串,不省略 | | `error` | string | 错误说明;成功时为空字符串 | 查询结果还可能返回 `can_refund`(boolean)、`status_description`(string)和 `native_status_name`(string,微信回包原生名称,可能为空);微信错误回包可能带 `wx_error_type`(integer 或 null;回包未提供时为 null)。`can_refund=true` 仅表示服务器状态待收取、普通交易、双方匹配且无同笔未决收款/退还占用;**不包含有效发送状态,也不保证后续风控允许退还**。 | `status` | `native_status` | `confirmed` | 含义与处理 | |---|---|---|---| | `waiting_receive` | 2000 | false | 等待收款;查询不会主动收款或退还 | | `received` | 2001 | true | 已收取;本版不能再退还,退还调用返回 `NOT_REFUNDABLE` | | `refunded` | 2002 | true | **唯一确认已退还的资金状态**;退还预检发现已退还时 `ok=true, accepted=false` | | `expired` | 2003 | true | 已过期;退还调用返回 `NOT_REFUNDABLE` | | `unknown` | 2004/2005/其他原值 | false | 含义未核实;查询保留原值,退还预检返回 `NOT_REFUNDABLE` 且不提交。已提交后的确认查询遇到未知/非终态时保持 `refund_pending` | | `refund_pending` | null | false | 退还已提交,约 5 秒短等待结束仍无终态;继续查询 | | `refund_unknown` | 通常 null | false | 提交后出现异常或无法验证回包;不能直接认定失败或自动重提 | | `failed` | null 或已取得原值 | 由原值决定 | 参数、前置或微信回包错误;必须一起读取 accepted、error_code 和 ret_code | **成功判定与等待**:只有 `status="refunded"` 且 `native_status=2002` 才代表已退还。`ok=true`、`accepted=true`、HTTP/命令传输成功或收到事件,均不能单独作为退还成功判据。两个新操作在当前请求/命令中短等待约 5 秒;排队、主线程调度和网络时间另计,客户端建议至少等待 15 秒。预检/查询超时且未提交时返回 `QUERY_TIMEOUT, accepted=false`;已提交但仍无终态时返回 `refund_pending, accepted=true`。提交后的确认查询可能返回 `WECHAT_ERROR, accepted=true`,仍须继续查询,不能认定退还确定失败。 **重试与互斥**:同一笔转账的接收与退还互斥,未决操作会返回 `TRANSFER_BUSY`;结果未知时保留占用,查询到终态后可回收占用。退还请求被微信明确拒绝才解除该退款提交的占用。重复调用不是按 `request_id` 持久幂等:ID 用于定位同笔转账,通道 request_id 只关联命令结果。网络超时、`refund_pending`、`refund_unknown` 或 `accepted=true` 的失败,都应先调用状态查询,**不要改 ID、重启或自动重提退还/收款**。未决保护不保证跨进程或跨账号会话延续。 **业务错误码**:失败保留结构化结果,不要只记录错误文字。 | `error_code` | 说明 | |---|---| | `INVALID_ARGUMENT` | 编号类型错误,或两个编号都为空白;本次未提交退还。 | | `CACHE_NOT_FOUND` | 本次运行没有该转账上下文,可能已重启或被淘汰;不能补传交易信息绕过。 | | `ID_MISMATCH` | 两个编号未指向同一笔转账。 | | `NOT_LOGGED_IN` | 当前微信账号未登录。 | | `NOT_RECEIVER` | 当前账号不是该转账的接收方。 | | `UNSUPPORTED_TRANSFER` | 群收款/AA、企业互通等不支持的交易,或无法确认群资料/普通群类型;普通微信群内指定给当前账号的个人转账不因来自群而拒绝。 | | `INCOMPLETE_CONTEXT` | 原消息缺少付款人、交易编号或可确认的非负整数有效期;明确读取到 0 可以使用,字段缺失仍拒绝。 | | `UNSUPPORTED_RUNTIME` | 当前微信运行时的请求关联或回包能力未就绪。 | | `TRANSFER_BUSY` | 同笔转账存在未决收款/退还操作;先查询,不要重提。 | | `CAPACITY_REACHED` | 未决操作容量已满;先查询已有操作。 | | `ACCOUNT_CHANGED` | 等待期间账号或账号会话发生变化;旧操作不能在新账号继续。 | | `WRITE_DISABLED` | 插件或有效发送状态不允许退还;查询不受有效发送状态限制。 | | `REQUEST_NOT_CAPTURED` | 未捕获到可以安全关联的微信请求;根据 accepted 判断是否已进入提交。 | | `NATIVE_EXCEPTION` | 调用微信发生异常;accepted=true 时结果未知,禁止直接重提。 | | `INVALID_RESPONSE` | 微信回包格式不完整或无法关联;提交后按未知处理。 | | `WECHAT_ERROR` | 微信返回错误;读取 ret_code 和可选 wx_error_type。提交后的确认查询报错不等于退还确定失败。 | | `IDENTITY_MISMATCH` | 服务器交易双方与消息上下文不一致;预检时不会提交,accepted=true 时则表示退还已提交、后续确认结果未知,先查询。 | | `NOT_REFUNDABLE` | 服务器确认当前状态不能退还;保留真实 status/native_status。 | | `QUERY_TIMEOUT` | 约 5 秒内未取得查询/预检结果,本次未提交退还。 | | `MAIN_THREAD_CALL` | 内部调用线程不符合等待要求;应通过本模式的正常入口调用。 | **验证范围**:2026-11-04 用户已确认普通个人转账的查询、收款及退还正常路径通过,包含待收款 2000、退还后 2002 及收款 `callback_ok / confirmed=true`。用户现已确认转账相关功能在微信 `8.0.80` 验证通过;重复操作、网络异常与账号切换仍按本节状态和重试规则处理。 退款完成/失败/未知时还可能收到 [transfer_refund_result 事件](#event-transfer-refund);事件不可替代主动查询。 ### 14.7 资料设置与地区代码 #### `clear_gender_region` ```json { "fields_cleared": ["sex", "country", "province", "city"], "restart_recommended": true } ``` #### 资料设置与地区代码查询 上述资料设置及查询命令复用已通过 HTTP 真机测试的原生资料链;相关业务已由用户在微信 `8.0.80` 验证通过。资料写命令要求有效发送状态开启;地区查询和探针不要求有效发送状态。均受命令通道的插件授权/启用检查。头像仍延后,没有头像写命令。 **写入参数**: | 命令 | `args` 示例 | |------|-------------| | `profile_gender` | `{"sex":1}` | | `profile_region` | `{"region_code":"CN_Guangdong_Shenzhen"}` | | `profile_region` | `{"region":{"country":"CN","province":"Guangdong","city":"Shenzhen"}}` | | `profile_signature` | `{"signature":"今天也要开心"}` | | `profile_update` | `{"sex":2,"region_code":"CN_Guangdong_Shenzhen","signature":"今天也要开心"}` | | `profile_update` | `{"sex":0,"region_code":"","signature":""}`(清空三项) | | `profile_pat_suffix` | `{"pat_suffix":"的小脑袋"}`;空字符串清空 | 资料字段采用严格 JSON 类型:`sex` 只接受整数 0/1/2,拒绝布尔值、小数(包括 `1.0`)、字符串、null 和越界值。文本必须是字符串,未知字段拒绝。未提供的资料字段保持原值;单字段命令只接受各自字段。地区整组替换,省市缺失就清空;子级不能脱离父级。`region_code` 和 `region` 不可同时传,空代码或空对象清空地区。 签名保留原始空白,微信原生加权计数最多 **30**:ASCII UTF-16 单元权重 1,其他单元权重 2,总权重/2 向上取整;例如 30 中文或 60 ASCII 可通过。超限拒绝、不截断。拍一拍不能放进 `profile_update`,需独立提交;其文本按微信原生规则处理空白/换行、计数和禁止字符。限制动态读取,本次真机上限 10、禁止字符 `<>/`,以 `profile_pat_suffix_probe` 的 `capabilities.max_length` 和 `capabilities.forbidden_characters` 为准。 **地区代码查询**: 1. `profile_region_codes`,`args:{}` 或 `{"parent_code":""}`:查询国家/地区列表。 2. 从 `result.items[].code` 取得国家代码,如 `CN`,传 `{"parent_code":"CN"}` 查询省级。 3. 从省级列表取得 `CN_Guangdong`,传 `{"parent_code":"CN_Guangdong"}` 查询市级。 4. 取得完整代码 `CN_Guangdong_Shenzhen` 后,用 `profile_region` 设置 `region_code`。 `result` 示例(仅展示一项): ```json { "parent_code": "CN_Guangdong", "count": 1, "items": [{"code":"CN_Guangdong_Shenzhen","name":"深圳","parent_code":"CN_Guangdong","type":2}], "selector_probe": {} } ``` `count` 为实际列表数量;`code` 是完整代码,`name` 随微信语言变化,`type` 取不到时为 null。不要拼中文名称;叶节点或不存在的父代码可能返回空列表。`profile_region_probe` 复用同一查询能力。 **提交与完成状态**: - 性别/地区/签名结果的 `status` 为 `submitted` 或 `unchanged`;`accepted` 是微信受理,`local_verified` 是本地读回,`server_confirmed:false`。最终展示以微信为准。失败时 `result` 保留受理状态和读回值,错误信息在信封顶层 `error`。`submitted_with_error` 可能已入队,先核对状态再处理。 - 拍一拍立即返回 `result.status:"pending"`、`operation_id`、数字 `event_id`、`requested`、`normalized`、`length`、`max_length` 等;顶层 `ok:true` 只表示提交受理。`duration_ms` 不包含后续等待微信服务器的时间。 - 随后用 **新的 `request_id`** 下发 `profile_pat_suffix_probe`、`args:{}`,检查 `result.last_operation`。`status:"succeeded"` 且 `server_confirmed:true` 才是服务器确认;`failed` 查看内部 `error`;`unknown` 表示 120 秒无回调或提交后异常,不要盲目重试。`pending` 期间拒绝第二项修改。 - 结果只通过探针查询,**不会再主动推送一条拍一拍完成的 `command_result`**。相同 `request_id` 会复用已缓存的初次结果,即使原操作后来完成也仍可能拿到 `pending`;每次状态查询必须生成新的 ID。 - 只保留最近一次拍一拍操作,重启微信后记录消失。用 `operation_id` / `event_id` 对照本次结果;该数字 `event_id` 是微信 oplog ID,与消息事件的去重键不同。 `profile_probe` 和 `profile_pat_suffix_probe` 可传候选参数预校验,返回 `validation.valid`、`normalized`、`error`、`writes_performed:false`,不会提交。探针顶层 `ok:true` 只说明查询执行完毕,应继续检查 `profile_ready`、`pat_suffix_ready` 或 `validation.valid`。 **下发示例**: ```json { "type": "command", "request_id": "req-region-codes-001", "command_name": "profile_region_codes", "args": { "parent_code": "CN_Guangdong" } } ``` 拍一拍状态查询时更换 `request_id` 和 `command_name`,`args` 置空: ```json { "type": "command", "request_id": "req-pat-status-002", "command_name": "profile_pat_suffix_probe", "args": {} } ``` **批量设置资料的完整 command 帧**:服务端在插件 WebSocket 连接上发送以下 JSON 文本帧。 ```json { "type": "command", "request_id": "req-profile-update-001", "command_name": "profile_update", "args": { "sex": 2, "region_code": "CN_Guangdong_Shenzhen", "signature": "今天也要开心" } } ``` **对应的资料提交结果帧**:插件返回 `command_result`,示例中本地读回匹配目标值。 ```json { "type": "command_result", "device_id": "webridge_device_xxxx", "session_id": "sess_xxx", "account_id": "wxid_self", "runtime_version": "8.0.80-2", "build_marker": "webridge_build_20261106_settings_update_8080", "ts_ms": 1791936000000, "request_id": "req-profile-update-001", "command_name": "profile_update", "ok": true, "duration_ms": 120, "result": { "accepted": true, "status": "submitted", "local_verified": true, "server_confirmed": false, "requested": {"sex":2,"country":"CN","province":"Guangdong","city":"Shenzhen","signature":"今天也要开心"}, "before": {"sex":0,"country":"","province":"","city":"","signature":"","current_user":"wxid_self"}, "current": {"sex":2,"country":"CN","province":"Guangdong","city":"Shenzhen","signature":"今天也要开心","current_user":"wxid_self"}, "changed_fields": ["city", "country", "province", "sex", "signature"], "readback_error": "", "message": "资料已提交微信同步队列;本地读回不代表服务器确认" }, "error": "" } ``` 资料操作失败时,命令信封顶层为 `ok:false`,具体错误在顶层 `error`,`result` 继续保留可取得的受理状态和读回值。探针预校验不通过时,探针命令本身仍可能 `ok:true`,应检查 `result.validation.valid` 和 `result.validation.error`。部分原生判断会以 `0/1` 输出,客户端应按布尔语义读取。 ### 14.8 状态查询与后台保活 #### `account_status` ```json { "is_logged_in": true, "account_id": "wxid_self", "user_name": "wxid_self", "display_name": "测试账号", "remark": "", "alias": "", "nick_name": "测试账号", "avatar_url": "", "sex": 0, "province": "", "city": "", "signature": "", "mobile": "", "country": "", "email": "", "has_profile_data": true, "wechat_version": "8.0.80" } ``` #### `device_status` 命令无参数(`args:{}`),外层 `ok/error` 按命令结果信封判断;成功时 `result` 示例: ```json { "ts_ms": 1780018858960, "version": "8.0.80-2", "build_marker": "webridge_build_20261106_settings_update_8080", "device": { "ios_version": "16.1.1", "device_model": "iPhone14,3" }, "process_memory": { "phys_footprint_mb": 537.1, "resident_mb": 753.5, "internal_mb": 296.2 }, "system_memory": { "available_estimate_mb": 1411.6, "used_estimate_percent": 74.9, "physical_total_mb": 5618.6 }, "battery": { "percent": 89, "state": "unplugged" }, "power": { "low_power_mode": false }, "thermal": { "state": "nominal", "raw": 0 }, "network": { "connected": true, "network_type": "wifi", "cellular_generation": "" }, "application": { "state": "background", "foreground": false, "active": false, "background": true } } ``` | 分组 | 字段 | 说明 | |------|------|------| | 顶层 | `ts_ms` | 快照时间(毫秒) | | 顶层 | `version` / `build_marker` | 插件版本与构建标记 | | `device` | `ios_version` | 设备 iOS 版本,如 `16.1.1`。**变长格式**(`16.1` 就是两段)。读取失败时为空串 | | `device` | `device_model` | 机型标识符,取自 `uname` 的 `machine`,如 `iPhone14,3`。**插件不做商名映射**,需要商名请自行映射;模拟器上可能是 `arm64` / `x86_64` | | `process_memory` | 12 个字段 | 6 个字节值(`resident_bytes`、`virtual_bytes`、`phys_footprint_bytes`、`internal_bytes`、`compressed_bytes`、`reusable_bytes`)+ 对应的 6 个 `*_mb` 值。**取不到时输出 `0`,字段恒在** | | `system_memory` | `ok` | 是否成功采集到系统内存 | | `system_memory` | 24 个测量字段 | `page_size`、`physical_total_*`、`free_*`、`active_*`、`inactive_*`、`wired_*`、`compressed_*`、`speculative_*`、`purgeable_*`、`external_*`、`available_estimate_*`(字节与 MB 双份)、`used_estimate_ratio`(0–1)、`used_estimate_percent`(0–100)、`note` | | `system_memory` | ⚠️ 采集失败时 | **只有** `ok: false` 和 `error`,上述 24 个字段**全部不出现**——你的解析代码必须容忍这种退化的响应 | | `battery` | `level` / `percent` | `level` 为 0–1,`percent` 为 0–100。**读不到时值为 `null`(不是 0)** | | `battery` | `state` | `unplugged` / `charging` / `full` / `unknown` | | `power` | `low_power_mode` | 是否低电量模式 | | `thermal` | `state` / `raw` | `nominal` / `fair` / `serious` / `critical` / `unknown`,及原始值 | | `network` | `connected` | 网络是否可达 | | `network` | `network_type` | `wifi` / `cellular` / `none` / `unknown` | | `network` | `cellular_generation` | `5g` / `4g` / `3g` / `2g` / `unknown`;**非蜂窝网络时为空串 `""`** | | `application` | `state` | `active` / `inactive` / `background` / `unknown` | | `application` | `foreground` / `active` / `background` | 由 `state` 推导的三个布尔量 | > ⚠️ **内存字段请用字节版或 MB 版,不要自己换算**:`process_memory` 和 `system_memory` 都同时提供字节与 MB 两份,MB 版是 `字节 / 1024 / 1024`。 > ⚠️ `system_memory` 里的 `available_estimate` 是估算值(空闲 + 非活跃 + 推测 + 可回收),插件自己在 `note` 字段里也写了"仅作趋势参考"。 字节与 MB 字段同时提供,MB 为字节除以 1024²。`system_memory.available_estimate_*` 仅作趋势参考,不是系统可用内存的精确承诺。 #### `bridge_status` 插件的**完整诊断快照**,字段很多且随内部状态变化。用途是排障,**不要依赖其中任何字段做业务判断**。里面包含 `version`、`build_marker` 以及内部运行诊断。 **保活诊断与测试**:通过已有命令获取共享状态,无参数。服务端向插件发送: ```json { "type": "command", "request_id": "req-keepalive-status-001", "command_name": "bridge_status", "args": {} } ``` 在对应 `command_result.result` 中读取下表字段,**均带 `background_keepalive_` 前缀**;表中为省略前缀后的名称。例如策略的完整路径为 `result.background_keepalive_strategy`。每次查询使用新的 `request_id`。 | 后缀 | 判断方法 | | --- | --- | | `enabled`、`audio_enabled`、`location_enabled` | 保活总开关及保存的子配置;关闭总开关后子配置保留,可能仍为 `true` | | `strategy`、`active` | 后台有效通道为 `audio` / `location` / `audio+location`;`degraded` 为有检查定时器但无有效通道;`foreground` 为前台定位;`stopped` 为停止。`active` 仅在后台且至少一路有效时为 `true` | | `audio_playing`、`audio_wait_reason`、`last_error` | 音频实际播放、等待原因及原有保活错误 | | `location_updates_requested`、`location_active` | 已请求定位与本次启动收到有效定位;只发起请求不算成功,前台也可能定位有效 | | `location_background_mode`、`location_authorization` | 宿主定位声明及授权;启动需声明存在,权限为 `when_in_use` 或已有的 `always` | | `location_wait_reason`、`location_last_error` | 定位等待原因及独立错误;重点检查 `waiting_foreground`、`waiting_permission`、`waiting_fix`、`permission_denied`、`missing_background_mode` | | `last_check_at`、`last_state_update_at`、`app_state` | 后台检查与状态更新的 Unix 秒,以及当时前后台枚举(`0` 活跃、`1` 非活跃、`2` 后台) | | `location_last_callback_at`、`location_last_update_age_seconds` | 最近定位回调的 Unix 秒及有效数据年龄;未取得有效数据时年龄为 `null`,静止时不保证频繁回调 | | `available_memory_bytes`、`footprint_bytes` | 进程剩余内存预算及物理占用;剩余预算不等于系统空闲内存 | | `timer_active`、`stop_reason` | 检查定时器是否存在;停止原因仅为历史最近停止音频和检查的原因,不能单凭它判断当前定位状态 | 设置页“后台保活”默认关闭,“音频保活”默认开,“持续定位保活”默认关。开启微桥及保活总开关,在微信前台开启定位并等待有效回调后转后台;仅请求“使用 App 期间”权限,无需修改 IPA。 按以下顺序验证,并在每组测试中实际执行一条文本命令和检查一次消息事件上报: 1. **单定位**:关音频、开定位;后台预期 `strategy=location`、`active=true`、`location_active=true`、`audio_playing=false`。 2. **单音频**:关定位、开音频;后台预期 `strategy=audio`、`active=true`、`audio_playing=true`、`location_updates_requested=false`。 3. **双开**:两路都开且定位已有效;后台预期 `strategy=audio+location`。通话时音频可能让出,定位有效时策略可暂变为 `location`,挂断后观察恢复。 4. **关闭总开关**:子开关保留选择并置灰;预期 `enabled/active/timer_active/audio_playing/location_updates_requested/location_active` 均为 `false`、`strategy=stopped`。再次开启后按保留配置运行。 该命令读取共享快照,不会强制刷新保活状态或启动通道,结合 `last_state_update_at` 判断时效。相隔 30 秒、2 分钟、10 分钟采样,比较后台 `last_check_at`、回调时间和业务结果;查询成功、一次 `location_active=true` 或网络心跳都不能证明此前持续运行。两路同属微信进程,系统结束微信时都会停止。 需要即时刷新保活及内存时,可从能访问手机的电脑调用 `GET http://手机IP:19088/debug/keepalive_status`,该接口的字段位于 `keepalive`,去掉上述前缀;**没有 `keepalive_status` WS 命令**。完整设备内存使用已有 `device_status` 命令。保活与专用诊断已由用户在微信 `8.0.80` 验证通过;长期锁屏、通话及低内存下的运行仍受系统调度影响。可选的详细采样及失败排查见 [巨魔后台保活测试说明](../../构建与运维/巨魔后台保活测试说明.md)。 ### 14.9 本地 HTTP 专用入口与替代方式 | HTTP 专用入口 | 当前命令通道的处理方式 | | --- | --- | | 旧 `/download_message_image`、`/debug/download_message_image` | 不存在同名命令;改用 `download_message_media`,参数传 `message_id`,从 `result.resolved_local_path` 读手机路径。支持图片、SILK 语音、视频、文件和表情 | | `POST /events` | 不下发 `events` 命令;使用本模式的事件接收链路,具体封装及字段均在本篇事件章节 | | `GET /`、`GET /health` | 不存在 `health` 命令;用本模式连接状态及 `account_status`、`bridge_status` 判断运行状态 | | `GET /debug/keepalive_status` | 不存在 `keepalive_status` 命令;用 `bridge_status` 查询保活共享快照、`device_status` 查询设备内存。本模式的完整读取规则见[保活诊断](#keepalive-diagnostics);本模式不能强制刷新 HTTP 专用快照 | | `GET /moments/media/file/{operation_id}` | 没有文件字节读取命令;让 `moments_media_download` 携带 `upload`,手机主动上传后端,见[本篇媒体准备](#moments-media-download)。聊天媒体用 `upload_message_media` 上传 | | `/debug/transfer_latest` | 不存在 `transfer_latest` 命令;诊断可使用本篇 `transfer_probe`,业务转账 ID 从本模式事件保存 | | `/debug/mock_incoming`、`/debug/contact_mgr_probe`、`/debug/contact_friend_probe`、`/debug/group_member_nickname_probe`、`/debug/web_entry_probe`、`/debug/web_dom_probe`、`/debug/image_runtime_probe`、`/debug/quote_runtime_probe`、`/debug/message_probe`、`/debug/forward_runtime_probe`、`/debug/inline_mention_send` | 没有同名命令。这些是可选本地诊断入口,不是本模式正常接入的前置步骤;需要本地专用诊断时才额外调用手机 HTTP | 不要把 HTTP 路径直接放入 `command_name`。未支持的名字返回 `unsupported command_name`。下载成功仅准备手机文件;后端要取得字节,应继续调用 `upload_message_media`,无需后端能直连手机。 --- ## 15. 好友备注、星标和黑名单 通过 `command_name` 使用下表同名命令,参数原样放入 `args`;WebSocket 和长轮询共享分发器。 仅支持已登录账号的普通微信好友,`id` 必须是其真实 `user_name` / `wxid`。不支持自己、群、公众号、系统号、OpenIM 或已注销好友;已拉黑的好友仍可取消黑名单。沿用鉴权和有效发送状态,拒绝未知字段。 | 接口/命令 | 参数 | 设置 | 清空/取消 | | --- | --- | --- | --- | | `set_friend_remark` | `id`, `remark` | `{"id":"wxid_example","remark":"张三"}` | `{"id":"wxid_example","remark":""}` | | `set_friend_star` | `id`, `starred` | `{"id":"wxid_example","starred":true}` | `{"id":"wxid_example","starred":false}` | | `set_friend_blacklist` | `id`, `blacklisted` | `{"id":"wxid_example","blacklisted":true}` | `{"id":"wxid_example","blacklisted":false}` | `remark` 必须显式传入字符串,使用微信 formatter 规范化,再按微信 `charLength <= 32` 校验,超限拒绝;这不是通用 Unicode 字符数限制。`starred` / `blacklisted` 必须显式传入 JSON 布尔值,`0/1`、`"false"`、`null`、缺省均拒绝。相同目标值返回 `unchanged`,不再次提交原生修改。 | `status` | `ok` | 说明 | | --- | --- | --- | | `applied_locally` | true | 原生方法返回成功,重新查询的本地值与目标一致 | | `unchanged` | true | 原值已等于目标;`already_set=true`,`accepted=false`、`sync_requested=false` | | `failed` | false | 参数、账号、好友类型、有效写入状态或 ABI 校验失败,未进入修改调用 | | `unknown` | false | 进入调用后返回失败、发生异常、账号变化或回读不一致,可能已有本地变化 | `accepted` 仅代表原生 BOOL 返回成功;`sync_requested` 仅说明已进入原生同步调用路径,不保证入队成功。`local_verified` 是本地联系人回读;`server_confirmed` **固定 false**,当前实现不等待服务器 ACK。`ok=true` 不保证另一台设备已同步,`unknown` 不宜自动重提。 响应中的 `remark` / `starred` / `blacklisted` 是请求目标值(备注为规范化值),不是失败后的实际值;非法或缺失字段可能使用空串/false 占位,必须结合 `ok` / `status`。联系人摘要在 getter 可读时附带 `is_starred` / `is_blacklisted`,未知则省略。原有 `remark` / `display_name` 继续可读,清空备注后显示名回退昵称。 好友设置已由用户在微信 `8.0.80` 验证通过;Unicode 长度限制及断网/跨设备同步仍按本节约定和微信实际状态处理。 下发的命令对象示例(外层任务结构沿用本协议): ```json {"type":"command","request_id":"friend_star_001","command_name":"set_friend_star","args":{"id":"wxid_example","starred":true}} ``` 响应沿用命令结果信封:`ok` / `error` 位于外层,`result` 包含 `id`、目标字段及其余状态字段。失败时同样保留 `result`,不能只按外层 `ok` 丢弃 `unknown` 的状态信息。 --- ## 16. 创建群聊、直接加人与发送邀请 命令为 `create_group`、`add_group_member`、`invite_group_member`,均已加入能力清单;邀请保留 `group_invite_member` 别名。沿用本篇连接/注册鉴权和命令信封,以下完整列出业务约定。 三项操作分别表示创建普通微信群、尝试直接加入群成员、发送需要对方接受的入群邀请。插件不按“40 人”硬切换,也不把添加失败自动转成发送邀请;是否可直接加入取决于微信服务端和群设置。所有结果均须查看业务状态,原生方法返回成功不等于成员已经入群。2026-10-29 用户通过 Apifox 完成 HTTP 真机联调:两位好友建群、直接添加已在群成员、移出后重新直接添加、邀请已在群成员、移出后发送邀请,五项结果均符合预期。邀请成功仅确认发送,不代表对方已接受。用户现已确认建群、添加与邀请成员功能在微信 `8.0.80` 验证通过。是否可直接加人仍由微信与群设置决定,审批、部分成功、超时及账号切换按本节状态处理。`request_submitted` 为布尔值。 ### 使用范围与完整参数 必须已登录并启用插件并保持有效发送状态。待操作对象必须是当前账号的普通微信好友,传真实内部 ID;不支持昵称、完整微信号 alias、企业微信用户或群 ID 充当成员。添加和邀请只支持普通 `@chatroom` 群,并须能从当前群成员快照确认本人在群;无须由插件预设“只有群主能加人”,实际权限和人数限制由微信返回。 | 参数 | 类型 | 创建 `create_group` | 添加 `add_group_member` | 邀请 `invite_group_member` | | --- | --- | --- | --- | --- | | `request_id` | string | 必填 | 必填 | 强烈建议传;具体位置与兼容默认值见本节通道说明 | | `member_id` | string | 与 `member_ids` 二选一,但单个其他好友无法达到首版建群最低人数 | 与 `member_ids` 二选一 | 单人邀请;与旧字段合并规则见下文 | | `member_ids` | string[] | 非空数组;去重并排除本人后至少 2 位其他好友 | 非空数组;与 `member_id` 二选一 | 非空数组,支持批量 | | `room_id` | string | 不接受 | 必填 | 必填,兼容群 ID 别名见下文 | | `reason` | string | 不接受 | 选填,默认空串,表示入群申请说明 | 不接受 | `request_id` 去除首尾空白后须为 1–128 个字符;成员和群 ID 也去除首尾空白,内部不能含空白。成员去重后按 ID 排序。创建时自动排除本人;至少两位其他好友是本接口首版支持范围,不是对微信服务端所有建群方式最低人数的断言。创建不接受 `topic`/群名,群创建后如需命名,使用已有 `set_group_name`,其结果独立判断。 添加/邀请先排除本机快照中已在群的成员,只向微信提交剩余普通好友;已在群者放入 `already_in_room_member_ids`。全部已在群时返回 `already_in_room`,不提交新请求;仅本机快照确认,`server_confirmed:false`。若任一待提交成员不是普通好友,整次请求在提交前失败,不偷偷跳过该人。 邀请保留单值别名 `user_name`/`wxid`、数组别名 `user_names`/`wxids`;可以和 `member_id`/`member_ids` 同时提供,合并、校验、去重排序。群 ID 可用 `chatroom_id`/`group_id`/`to_user_name`,同时传多个群字段时规范化后须一致。创建和添加不支持这些旧别名,也不允许同时传多个成员字段。未列出的参数直接拒绝,不静默忽略;`null` 不能代替字符串或数组。 ### 返回字段与成功判断 下表列出进入群操作服务后的完整业务字段;`ok`/`error` 所在位置见本节通道说明。服务内初始失败可能尚未解析到成员或群 ID,因此对应字段为空。`message`、`error`、`error_code` 和 `replayed` 为按情况出现的字段。命令信封、插件开关、队列容量等前置拒绝尚未进入群操作服务,`result` 可能为空或只有部分字段;例如 `args.request_id` 冲突只返回 `status` 和 `error_code`,此时不要假定成员或提交状态字段存在。 | 字段 | 类型/默认值 | 含义 | | --- | --- | --- | | `ok` | boolean,false | 创建时表示已确认建成群;添加时表示目标均已加入或已在群;邀请时表示本批邀请已发送,或目标本来全已在群 | | `status` | string,`failed` | 当前业务状态,见下表 | | `mode` | string | `create`/`add`/`invite` | | `request_id` | string,空串 | 本次规范化后的业务请求 ID,须保存用于查询和重试去重 | | `room_id` | string,空串 | 添加/邀请的目标群;创建成功后为微信实际返回的新群 ID | | `member_ids`、`member_count` | string[]/integer,`[]`/0 | 去重排序后的目标成员和数量;创建时已排除本人 | | `submitted_member_ids` | string[],`[]` | 筛选出的本次原生目标成员;是否实际提交还须看 `request_submitted` | | `already_in_room_member_ids` | string[],`[]` | 提交前本机快照已确认在群的目标成员 | | `accepted` | boolean,false | 微信原生方法的布尔返回值,不代表发出、成功或成员已入群 | | `request_submitted` | boolean,false | 已取得本次原生事件 ID;不代表微信服务端接受 | | `response_received` | boolean,false | 已收到与本请求匹配的原生回包;内容仍可能不完整 | | `server_confirmed` | boolean,false | 本动作已由微信服务端确认成功:创建得到有效新群ID、添加确认所有目标已入群、邀请批次发送成功;拒绝、需邀请、待审批或全部原已在群时为false | | `request_cgi` | integer/null,null | 本次原生请求类型:创建119、添加120、邀请610;尚未提交时可能为 null | | `event_id` | integer/null,null | 本次原生事件编号,用于诊断;不是事件拉取编号或单独查询入口 | | `wechat_ret` | integer/null,null | 微信业务返回码,不能把缺失值按0处理 | | `wechat_error_message` | string,空串 | 微信原始错误说明 | | `message_type`、`transport_error_type`、`transport_error_code` | integer/null,null | 原生消息状态与传输错误字段 | | `created` | boolean,false | 是否确认本次创建了群;其他两种操作为false | | `all_members_added` | boolean,false | 是否确认所有目标均已加入或已在群;发出邀请不会因此变成true | | `member_results` | object[],`[]` | 创建/添加的逐成员结果;每项有 `member_id:string`、`status:string`、`native_status:integer/null` | | `invite_required_member_ids` | string[],`[]` | 完整回包确认可进一步发送邀请的成员;由调用方显式发起新邀请操作 | | `replayed` | boolean,可省略 | 为true时表示读取了同ID的已登记操作,没有重新提交原生请求 | | `message` | string,可省略 | 成功后的补充说明 | | `error_code`、`error` | string,可省略 | 失败、需后续处理或结果未知时的错误分类与中文说明 | | `status` | `ok` | 如何处理 | | --- | --- | --- | | `created` | true | 群已创建,读取新 `room_id`;仍须看 `all_members_added` 与逐成员结果,不能因部分成员失败再次建群 | | `added` | true | 所有目标都已加入或本来就在群 | | `invitation_sent` | true | 仅确认本批邀请发送,对方接受后才会入群;不提供伪造的逐人成功结果 | | `already_in_room` | true | 全部目标在提交前已存在于本机群快照,没有新请求 | | `requires_invitation` | false | 直接加人未完成;只对 `invite_required_member_ids` 按业务需要显式调用邀请,并用新的业务ID | | `requires_verification` | false | 成员需要好友验证,先处理好友关系 | | `approval_pending` | false | 已进入群主/管理员确认流程,等待确认;尚未加入 | | `partial` | false | 添加批次有多种结果,包括需验证等已知结果与unknown混合;逐人查看 `member_results`,含unknown时仍保留同群占用,不自动重复整批 | | `processing` | false | 同ID操作仍在等待回包,稍后以相同参数、同ID读取 | | `failed` | false | 参数、环境或业务失败;结合是否提交、错误码和微信原始提示处理 | | `unknown` | false | 无法确认是否生效;以同ID读取迟到结果,必要时核实真实群状态,不自动重发 | 逐成员 `status` 可能为 `added`、`already_in_room`、`requires_invitation`、`requires_verification`、`approval_pending`、`failed`、`unknown`。原生 `native_status=0` 不能独立判断加入成功,必须与总返回码共同解释:例如添加回包 `wechat_ret=-2012` 表示需要邀请的分支,`-2028` 可表示等待群主/管理员确认。`wechat_ret=-2012` 下 `native_status=3` 表示拒绝加入,逐成员为failed;状态10含义尚不能确认,保留unknown。创建成功但成员信息不完整时仍返回 `created`,相关成员为unknown、`all_members_added:false`。正常邀请回包只确认批次发送,`member_results:[]`;全部目标已在群而未发送时可返回逐人的 `already_in_room`。 ### 错误、等待、去重与重试 | `error_code` | 含义与处理 | | --- | --- | | `INVALID_PARAMETERS`、`INVALID_REQUEST_ID`、`INVALID_ROOM_ID`、`INVALID_MEMBER_IDS`、`INVALID_REASON`、`TOO_FEW_MEMBERS` | 参数不符合本节契约,未启动业务请求 | | `REQUEST_ID_CONFLICT` | 同ID已经对应其他规范化参数,或命令args与信封ID冲突;不得用改参数方式查询原操作 | | `ACCOUNT_UNAVAILABLE`、`WRITE_DISABLED`、`ROOM_STATE_UNAVAILABLE`、`FRIEND_REQUIRED` | 登录/开关/群快照/好友条件不足,未发起新请求 | | `UNSUPPORTED_ROOM` | 无法确认目标是普通微信群,或属于不支持的企业互通/升级群,未发起新请求 | | `OPERATION_BUSY` | 同群,或当前账号的建群操作,存在等待中或未解决的未知结果;先处理原操作 | | `REQUEST_RECORD_LIMIT` | 当前登录上下文已登记512项操作,新操作被拒绝;已有ID仍可查询,不淘汰旧记录后冒险重发 | | `NATIVE_API_UNAVAILABLE`、`INVALID_CALL_THREAD` | 原生接口不兼容或调用线程错误;保留结果用于排查 | | `REQUEST_NOT_CONFIRMED`、`NATIVE_REQUEST_FAILED` | 原生调用或提交确认异常;检查 `status`、`accepted`、`request_submitted`,可能未知,不自动重试 | | `WECHAT_REQUEST_REJECTED` | 微信明确拒绝本次业务,查看 `wechat_ret` 与原始说明 | | `MEMBERS_NOT_ALL_ADDED`、`MEMBER_RESPONSE_INCOMPLETE` | 添加未确认所有目标入群,逐人处理;成员信息不完整不能当作全部成功 | | `RESPONSE_MISMATCH`、`RESPONSE_INCOMPLETE`、`RESPONSE_INVALID`、`ROOM_ID_MISSING` | 回包错配/缺失/异常,或建群成功码缺少群ID,结果未知 | | `OPERATION_TIMEOUT`、`ACCOUNT_CHANGED` | 等待超时或登录上下文改变,核实原业务结果 | 服务等待真实回包约30秒,外层等待保护约31秒;客户端建议至少40秒,iOS挂起、通信排队或断线可能更久,不能把连接超时直接当业务失败。该能力不新增完成事件或单独查询接口;迟到回包会更新当前进程内记录,使用原接口/命令、原ID和相同规范化参数读取即可。 三种接入方式共用当前账号登录上下文内的操作记录。登记后相同ID、相同操作/群/去重排序成员/reason只读取记录,不再次提交;在途返回processing,结束后返回当前结果并带 `replayed:true`。同ID不同参数返回冲突。参数或环境校验阶段尚未登记的失败不等于已建立去重记录。新业务必须用新ID;同ID只能查询/重放原业务,不用于强制重新执行。 最多保留512项已登记操作,达到上限拒绝新操作,不按最近条目淘汰。添加操作只要仍有待添加成员结果为 `unknown`,即使回包字段齐全或顶层为 `partial`,也继续占用同群操作槽位,并等待迟到回包;已知结果与未知结果混合时逐人处理,不能据此重发整批。唯一可继续的同群操作是显式邀请:新邀请的全部目标必须属于该未完成添加记录的 `invite_required_member_ids` 已确认名单,并使用新的业务ID;不在名单内的邀请和其他添加仍返回OPERATION_BUSY,原添加记录继续等待。只有成员结果完整且均已明确,才解除该添加占用。邀请本身结果未知也保留同群占用,上述例外不会绕过其他未完成的邀请。建群尚未取得有效新群ID时的未知结果会占用当前账号建群槽位;已取得有效新群ID则返回 `created` 并解除建群占用,成员部分未知也不能重复创建原业务群。记录仅在当前微信进程和登录上下文内有效,进程重启、重新登录或服务上下文变化(含群服务重新初始化)后不保证去重;不要通过重启或换ID绕过未知结果。后端应保存业务ID及收到的结果,跨重启先核实群状态,再由业务决定下一步。 ### 本模式封装与调用示例 `request_id` 必须在命令信封提供;`args.request_id` 可以省略,若提供必须与信封ID一致。参数置于args。外层 `ok`/`error` 为业务结果,其他字段置于 `result`,其中不再重复 `ok`/`error`;失败码读取 `result.error_code`。 这些命令及邀请别名绕过通用“最近100条”命令缓存,由群操作服务执行账号校验、参数冲突检查及上述512项记录规则;本节不适用“失败后换新ID自动重试”的通用建议。 通过现有连接下发command帧;后台等待时连接仍可处理心跳和接收其他命令。相同ID也进入共享服务检查参数,不会被通信层静默吞掉;排队及等待中的命令受通信层上限约束。下例为直接添加: ```json { "type": "command", "request_id": "req-add-001", "command_name": "add_group_member", "args": { "room_id": "123456789@chatroom", "member_id": "wxid_member_a", "reason": "" } } ``` 创建和邀请的完整 WebSocket 帧如下: ```json { "type": "command", "request_id": "req-create-001", "command_name": "create_group", "args": { "member_ids": [ "wxid_member_a", "wxid_member_b" ] } } ``` ```json { "type": "command", "request_id": "req-invite-001", "command_name": "invite_group_member", "args": { "room_id": "123456789@chatroom", "member_ids": [ "wxid_member_a" ] } } ``` 建群成功示例(字段格式示例,不是真机记录): 以下省略公共设备/时间字段,通过command_result帧返回: ```json { "type": "command_result", "request_id": "req-create-001", "command_name": "create_group", "ok": true, "error": "", "result": { "status": "created", "mode": "create", "request_id": "req-create-001", "room_id": "123456789@chatroom", "member_ids": [ "wxid_member_a", "wxid_member_b" ], "member_count": 2, "submitted_member_ids": [ "wxid_member_a", "wxid_member_b" ], "already_in_room_member_ids": [], "accepted": true, "request_submitted": true, "response_received": true, "server_confirmed": true, "request_cgi": 119, "event_id": 42, "wechat_ret": 0, "wechat_error_message": "", "message_type": 0, "transport_error_type": 0, "transport_error_code": 0, "created": true, "all_members_added": true, "member_results": [ { "member_id": "wxid_member_a", "status": "added", "native_status": 0 }, { "member_id": "wxid_member_b", "status": "added", "native_status": 0 } ], "invite_required_member_ids": [] } } ``` 直接添加需要改走邀请示例(字段格式示例,不是真机记录): 以下省略公共设备/时间字段,通过command_result帧返回: ```json { "type": "command_result", "request_id": "req-add-001", "command_name": "add_group_member", "ok": false, "error": "成员未全部确认加入,请查看 member_results;需要邀请时显式调用 invite_group_member", "result": { "status": "requires_invitation", "mode": "add", "request_id": "req-add-001", "room_id": "123456789@chatroom", "member_ids": [ "wxid_member_a" ], "member_count": 1, "submitted_member_ids": [ "wxid_member_a" ], "already_in_room_member_ids": [], "accepted": true, "request_submitted": true, "response_received": true, "server_confirmed": false, "request_cgi": 120, "event_id": 42, "wechat_ret": -2012, "wechat_error_message": "", "message_type": 0, "transport_error_type": 0, "transport_error_code": 0, "created": false, "all_members_added": false, "member_results": [ { "member_id": "wxid_member_a", "status": "requires_invitation", "native_status": 0 } ], "invite_required_member_ids": [ "wxid_member_a" ], "error_code": "MEMBERS_NOT_ALL_ADDED" } } ``` 邀请批次已发送示例(字段格式示例,不是真机记录): 以下省略公共设备/时间字段,通过command_result帧返回: ```json { "type": "command_result", "request_id": "req-invite-001", "command_name": "invite_group_member", "ok": true, "error": "", "result": { "status": "invitation_sent", "mode": "invite", "request_id": "req-invite-001", "room_id": "123456789@chatroom", "member_ids": [ "wxid_member_a" ], "member_count": 1, "submitted_member_ids": [ "wxid_member_a" ], "already_in_room_member_ids": [], "accepted": true, "request_submitted": true, "response_received": true, "server_confirmed": true, "request_cgi": 610, "event_id": 42, "wechat_ret": 0, "wechat_error_message": "", "message_type": 0, "transport_error_type": 0, "transport_error_code": 0, "created": false, "all_members_added": false, "member_results": [], "invite_required_member_ids": [] } } ``` 已提交但超时未知示例(字段格式示例,不是真机记录): 以下省略公共设备/时间字段,通过command_result帧返回: ```json { "type": "command_result", "request_id": "req-add-001", "command_name": "add_group_member", "ok": false, "error": "等待微信回包超时,可能已生效;请用相同 request_id 查询,勿换 ID 重发", "result": { "status": "unknown", "mode": "add", "request_id": "req-add-001", "room_id": "123456789@chatroom", "member_ids": [ "wxid_member_a" ], "member_count": 1, "submitted_member_ids": [ "wxid_member_a" ], "already_in_room_member_ids": [], "accepted": true, "request_submitted": true, "response_received": false, "server_confirmed": false, "request_cgi": 120, "event_id": 42, "wechat_ret": null, "wechat_error_message": "", "message_type": null, "transport_error_type": null, "transport_error_code": null, "created": false, "all_members_added": false, "member_results": [], "invite_required_member_ids": [], "error_code": "OPERATION_TIMEOUT" } } ``` --- ## 17. 入群邀请:查询、接受与诊断 用户已确认微信 **8.0.80** 的邀请查询与接受入群功能验证通过。收到普通私聊邀请链接卡片后,插件从原消息取得 URL、发送方和消息上下文,经微信原生 A8Key 鉴权查询邀请或接受入群,不创建网页、不点击界面。查询时群名或目标群 ID 可缺省,目标群 ID 可在接受成功后才取得。验证范围见本节末尾。 | 命令 | 行为 | |---|---| | `get_group_invitation` | 只查详情和限制,不接受 | | `accept_group_invitation` | 内部重新查询,满足条件才提交接受,无需先调用查询 | | `group_invitation_probe` | 只读探针,`stage=context/auth/info`,默认 `info` | 业务请求统一为: ```json {"message_id":"123456789012345678"} ``` `message_id` 取收到卡片事件的 `body.message_id`,必须是正整数十进制**字符串**(最多 20 位且不超过 uint64,无前导零)。不接受 JSON 数字,以免大整数丢精度。不需要 `invite_url`、`chat_id`、`room_id`、账号凭据或 ticket。账号选择沿用既有设备/账号通道;本地复核消息属于当前登录账号。回调里的 `chat_id` 是来源会话,私聊的 `room_id` 为空,不是邀请目标群。群名等来自服务器详情;目标群 ID 在查询时可为空,接受成功后从提交回包取得。 仅处理当前账号收到的普通私聊 `49/5` 卡片及 `support.weixin.qq.com/cgi-bin/mmsupport-bin/addchatroombyinvite` 邀请。 鉴权后的地址允许微信内置的 7 个 support 域名(`support/szsupport/mlsupport/hksupport.weixin.qq.com`、`support/sgsupport/ussupport.wechat.com`),仍严格检查 HTTPS、原路径、原 ticket、端口和凭据;详情/接受请求及 Origin 使用本次鉴权域名,不跟随重定向。原消息必须仍在近期缓存(当前最多 **200 条**),否则返回 `MESSAGE_CONTEXT_NOT_FOUND`,需重新取得一条新邀请。切换账号或登录上下文时停止操作。微信被系统挂起或进程结束时不能保证后台执行;“静默”指进程有运行时间时无界面地执行协议。 三项命令均先通过插件启用与有效授权检查;接受在真正提交前额外复核有效发送状态,查询和探针不额外做发送判断。 ### 结果判定 | `status` | `ok` | 含义 | |---|---|---| | `ready` | true | 查询成功,本次条件允许接受;不承诺后续提交必成功 | | `restricted` | true | 查询成功但不允许接受,读取 `restriction_code` / `warning` | | `already_joined` | true | 本次服务器详情确认已经在群,不提交接受 | | `accepted` | true | 微信明确接受成功并取得有效群 ID,已有群 ID 时核对一致;后续详情尚未确认成员状态 | | `joined` | true | 微信接受成功,且本次后续服务器详情确认在同一群 | | `unknown` | false | 已开始提交,但断网、坏响应或账号变化导致无法确认结果;先查状态 | | `failed` | false | 前置条件失败,或微信明确拒绝 | | `context_ready` / `authenticated` | true | 仅供对应阶段探针,分别表示上下文检查通过 / 取得合法鉴权结果 | `ticket_used=true` 单独不能证明当前还在群。`openurl`、额外交互或不支持的 A8Key action 会明确受限/失败。不会转到页面继续操作。查询阶段允许服务器暂不返回群 ID,仍可返回群名、人数和是否可接受;接受成功必须取得有效群 ID。后续成员核实按同一 ticket 的本次服务器详情判断,若返回群 ID 则必须匹配。旧本地群联系人或成员缓存不作为本次在群证据。 查询结果帧示例(依据 HTTP 真机业务回包整理,消息 ID 与群名已替换): ```json { "type": "command_result", "request_id": "invite-query-001", "command_name": "get_group_invitation", "ok": true, "result": { "status": "ready", "message_id": "123456789012345678", "room_id": "", "room_name": "示例群", "member_count": 198, "can_accept": true, "in_chatroom": false, "ticket_used": false, "restriction_code": "", "warning": "为维护微信平台绿色网络环境,请勿在群内传播违法违规内容。", "request_submitted": false, "membership_verified": false, "wechat_ret": 0, "a8key_action": 6, "wechat_err_code": 0, "wechat_error_type": 0, "http_status": 200, "stage": "info", "error_code": "", "elapsed_ms": 1417 }, "error": "" } ``` 接受响应主要看 `status`、`request_submitted`、`membership_verified`。`request_submitted=true` 只表示本次或此前同票据的接受请求已开始,不能证明服务器已接收;`accepted` 不等于已确认在群。接受操作的 `can_accept` 固定为 false,避免把已处理结果再次当作可提交邀请。 ### 字段来源与快照语义 接受返回整合了接受前详情、接受 POST 和接受后核实三次请求: - `room_name`、`member_count`、`ticket_used`、`warning` 保留**接受前详情快照**;核实阶段不刷新这些字段。因此成功回包中 `ticket_used=false`、`member_count=198` 仍可与 `status=joined` 同时出现,不能据此判断票据仍可用或入群后人数仍为 198。 - `room_id` 在接受成功时从 POST 回包取得;`membership_verified` 与 `in_chatroom=true` 表示后续同一票据的服务器详情已确认在群。`verification_*` 只记录这次核实请求,不覆盖接受 POST 的状态码。 - `warning` 来自微信详情的 `data.antispamwarning`,为空时取 `data.securitytips`,再隐藏其中的凭据和链接。当前返回没有记录具体采用了哪一个源字段。这条“为维护微信平台绿色网络环境,请勿在群内传播违法违规内容。”是微信返回的普通提醒;仅有该文案不表示违规、受限或失败,限制看 `status/restriction_code`。 后端判断已确认在群,使用 `ok=true`、`status` 为 `joined` 或 `already_joined`,并且 `membership_verified=true`。`request_submitted=true` 仅说明开始过提交;`accepted` 表示微信已确认接受但成员核实尚未成功,应查询详情确认,不能自动重提。接受操作结束时 `can_accept` 固定为 false,表示该操作结果不再作为可接受邀请,不能把它解释成本次接受被拒绝。 实测 `wechat_ret=0` 表示 A8Key 鉴权成功,`a8key_action=6` 是原生 URL 处理分支编号。接受回包的 `http_status=201` 是**微信接受 POST** 的成功 HTTP 状态;再结合 `wechat_err_code=0/wechat_error_type=0` 和有效群 ID 判断接受成功。`verification_http_status=200` 与核实双零业务码来自随后的详情 GET,成员是否核实仍看 `membership_verified`。这些 HTTP 状态来自插件向微信的业务请求;本模式按结果帧外层 `ok` 及 `result.status` 判断,不能把 JSON 内的 `http_status` 当作 WS 传输状态。 | 字段 | 类型与约定 | |---|---| | `ok`, `status`, `error_code`, `error` | 业务成败、状态、机器错误码、中文说明;成功错误字段为空串 | | `message_id` | 原消息 ID 字符串 | | `room_id`, `room_name` | 群名来自详情;群 ID 在查询时可能为空,接受成功后从提交回包取得;未知为空串 | | `member_count` | 非负整数或 null;接受结果保留接受前详情人数,不是入群后实时人数 | | `can_accept` | 查询是否允许接受;不能作为跨请求授权凭证;接受操作结束时固定 false | | `in_chatroom` | 布尔或 null;详情中的在群状态;接受后核实成功更新为 true | | `ticket_used` | 布尔或 null;接受结果保留接受前详情的票据状态;单独 true 也不能证明当前在群 | | `restriction_code` | 限制分类,未受限为空串;取值见下方限制码 | | `warning` | 微信详情 antispamwarning 优先、securitytips 兜底,脱敏后返回;接受结果保留接受前快照;普通提醒不等于受限 | | `request_submitted`, `membership_verified` | 是否开始过接受提交、本次是否由服务器确认在群 | | `wechat_ret` | 原生 A8Key baseResponse.ret,0 表示鉴权无业务错误,未到达时 null | | `a8key_action` | 原生 actionCode 分支号,当前允许 0/2/6 继续校验目标地址及密钥;实测为 6,未到达时 null | | `wechat_err_code`, `wechat_error_type` | 最近详情/接受 POST 的顶层 errCode、data.errorType,双零表示无业务错误;未取得为 null;不被核实请求覆盖 | | `http_status` | 微信详情/接受 POST 的 HTTP 状态,接受成功实测 201;未知为 null,未取得响应时可为 0;不是手机本地 API 的 HTTP 状态 | | `verification_err_code`, `verification_error_type`, `verification_http_status` | 可选;提交成功后成员核实请求的结果,不覆盖接受结果 | | `stage` | `context/auth/info/submit/verify`,最后到达的阶段 | | `elapsed_ms` | 可选;整个操作从创建到结束的总耗时,毫秒,含鉴权、详情、提交和核实中已执行的阶段 | | `version` | 手机 HTTP 出口附加的 WeBridge 插件完整版本,例如 8.0.80-2,包含 deb 构建号;不是原生微信版本;本功能 WS/轮询命令结果不附加此字段 | | `replayed` | 可选;true 表示此前提交过,本次只查不再 POST | | `diagnostics` | 仅探针在创建操作后返回;早期参数/缓存失败可能没有 | 错误码包括 `INVALID_ARGUMENT`、`INVALID_CALL_THREAD`、`NOT_LOGGED_IN`、`ACCOUNT_CHANGED`、`MESSAGE_CONTEXT_NOT_FOUND`、`MESSAGE_CONTEXT_MISMATCH`、`NATIVE_UNAVAILABLE`、`NATIVE_EXCEPTION`、`A8KEY_FAILED`、`A8KEY_TIMEOUT`、`RESPONSE_INVALID`、`INVITATION_TIMEOUT`、`INVITATION_EXPIRED`、`INVITATION_INVALID`、`WECHAT_REJECTED`、`SEND_DISABLED`、`BUSY`、`INVITATION_LIMIT`、`RESULT_UNKNOWN`。限制码为 `INVITATION_USED`、`INVITATION_RESTRICTED`、`UNSUPPORTED_INVITATION_FLOW`,接受受限时也作为错误码。 ### 去重、超时与重试 同账号登录上下文、同票据、同操作及探针阶段的并发请求合并等待;创建操作上限 16。接受提交前登记内存记录,同一票据以后调用接受会重新查询、不重复 POST;明确拒绝可移除提交记录。提交记录当前最多 64,满时拒绝新提交,避免淘汰未知记录导致误重试。账号/会话变化或进程重启后记录不保留,**不保证跨重启幂等**;后端也应按账号和原消息记录已提交/未知结果,重启后先查询,不自动重发接受。 原生鉴权等待约 18 秒,插件向微信发起的单次业务 HTTP 请求最多 12 秒,总操作约 55 秒。调用方等待建议 **75 秒**。本模式在工作线程等待后回传最终 `command_result`,按 `request_id` 关联,并读取外层 `ok/error` 与 `result.status`;业务字段 `http_status` 是微信请求的诊断信息。等待期间仍可处理心跳和其他命令,不要因 30 秒内没有业务结果就自动重投。 ### 邀请诊断命令与验证范围 下发 `group_invitation_probe`,`stage` 可取 `context`、`auth`、`info`,默认 `info`: ```json {"type":"command","request_id":"invite-probe-001","command_name":"group_invitation_probe","args":{"message_id":"123456789012345678","stage":"context"}} ``` 1. `context`:检查原消息、当前账号、原生类和 ABI,不按固定微信版本号拦截,不调用 A8Key 或业务 HTTP。 2. `auth`:在前述基础上调用原生鉴权并核对 fullUrl、ticket、exportkey,不请求邀请详情。 3. `info`(默认):完成鉴权并 GET 详情,不 POST 接受。三阶段 `request_submitted` 均为 false。 诊断含 `native_ready`、`native_signatures`、`wechat_version`、`message_context_found`、`message_type`、`app_msg_type`、`a8key_scene`、`a8key_opcode`、`callback_received`;收到回调后可有 `event_id`、`cgi`、`a8key_message`、`transport_error_type/code`、`exportkey_present`、`full_url_allowed`、`full_url_same_host/path/ticket`、`full_url_https`、`full_url_has_fragment`、`full_url_host`(仅 hostname)、`full_url_host_allowed`、`full_url_port`(未显式设置时 null)、`full_url_has_credentials`、`a8key_header_names`;HTTP 后可有 `business_http_host`(应与鉴权 hostname 相同)、`http_response_mime`、`last_response_bytes`、`network_error_code`、`http_json_object`、`data_object`、`room_info_object` 及固定业务字段名称列表 `invitation_fields`。业务解析后还有 `response_field_types`(固定字段的 JSON 类型,含 missing/null)、`response_string_lengths`(群信息字符串长度)和 `response_validation`(信封、业务层、布尔字段、roomInfo 和群 ID 校验结果),不返回这些字段的原始字符串值。`full_url_same_host` 仅表示与原入口主域名相同;官方地区域名可使其为 false,应结合 `full_url_host_allowed/full_url_allowed` 判断。字段按到达阶段提供。探针不返回原始链接、ticket、exportkey、Cookie 或鉴权 header 值,也不提供任意 URL、原生方法执行能力。 若另有能直连手机的环境,可选用本地 HTTP 验证脚本;它不是 WebSocket 接入的前置步骤(真实 ID 由调用方在命令行传入,不写入文件): ```powershell python tests/group_invitation_smoke.py --base-url http://<手机IP>:19088 --message-id <刚收到的消息ID> ``` 排障按 context → auth → info 检查;确认 `status=ready` 后按业务需要调用接受接口。脱敏探针 JSON 可定位原消息、ABI、A8Key 或 HTTP 问题,避免盲目重建。若 auth 成功但 info 失败,检查 `http_status`、双层业务码、网络错误与 header 名称。用户已确认邀请查询与接受入群在微信 `8.0.80` 验证通过。实现不再按固定版本号拦截,仍检查实际原生类、方法和参数类型。过期/受限邀请、网络异常、账号切换及跨重启幂等按本节状态和重试边界处理。 命令下发示例(WS 帧): ```json {"type":"command","request_id":"invite-001","command_name":"accept_group_invitation","args":{"message_id":"123456789012345678"}} ``` 命令回包沿用既有信封:业务 `ok/error` 在顶层,其余字段位于 `result`,本功能不附加 HTTP 出口的 `version`。上方查询示例展示业务字段;下面是接受命令结果: 接受并核实入群成功的命令结果格式(将用户提供的 HTTP 真机回包按既有命令信封转换,身份字段已替换;WebSocket通道尚未实际执行该业务): ```json { "type": "command_result", "request_id": "invite-001", "command_name": "accept_group_invitation", "ok": true, "result": { "status": "joined", "message_id": "123456789012345678", "room_id": "123456789@chatroom", "room_name": "示例群", "member_count": 198, "can_accept": false, "in_chatroom": true, "ticket_used": false, "restriction_code": "", "warning": "为维护微信平台绿色网络环境,请勿在群内传播违法违规内容。", "request_submitted": true, "membership_verified": true, "wechat_ret": 0, "a8key_action": 6, "wechat_err_code": 0, "wechat_error_type": 0, "http_status": 201, "stage": "verify", "error_code": "", "elapsed_ms": 3876, "verification_http_status": 200, "verification_err_code": 0, "verification_error_type": 0 }, "error": "" } ``` 传输层继续携带既有 `type/session_id/device_id/ts_ms` 等公共字段;按 `request_id` 匹配结果。 > **验证范围**:邀请查询和分阶段探针已有 HTTP 真机结果;用户提供的接受回包为 `ok=true/status=joined`、`membership_verified=true/in_chatroom=true`。用户现已确认相关业务在微信 `8.0.80` 验证通过。 --- ## 18. 群管理员、群免打扰与个人二维码 三项支持 HTTP、长轮询和 WebSocket,使用相同参数和业务状态。示例 ID 和二维码 URL 均为虚构。 | 用途 | 命令名 | 必填参数 | | --- | --- | --- | | 添加/移除管理员、转让群主 | `group_admin_operate` | `room_id`、`oper_type`、`member_ids` | | 设置当前账号的群免打扰 | `set_group_msg_silence` | `room_id`、`silence` | | 获取当前登录账号二维码 | `get_self_qrcode` | 无,请求为 `{}` | 需要正常鉴权和登录,拒绝未知字段。群管理与免打扰受有效发送状态约束;个人二维码不受有效发送状态约束。群 ID 必须是普通微信群真实 `...@chatroom`,不接受群名或 OpenIM 群。 ### 群管理员操作 ```json {"room_id":"123456789@chatroom","oper_type":1,"member_ids":["wxid_member_a"]} ``` | `oper_type` | 操作 | CGI | `member_ids` | | --- | --- | --- | --- | | 1 | 添加管理员 | 889 | 非空数组,可批量 | | 2 | 移除管理员 | 259 | 非空数组,可批量 | | 3 | 转让群主 | 990 | 去重后恰好一人 | `oper_type` 必须是 JSON 整数,拒绝布尔值、字符串。成员必须是群内其他普通微信成员的真实 `user_name`/`wxid`,不接受昵称或本人;数组去重并保留顺序。当前账号必须为群主,无法从本地群资料确认权限或成员时先刷新群资料。同一群同时只处理一个权限变更,全局最多 32 项待处理。 | `status` | `ok` | 含义 | | --- | --- | --- | | `succeeded` | true | 本次匹配的微信响应明确成功,`wechat_ret=0`、`server_confirmed=true` | | `failed` | false | 前置校验失败,或微信明确拒绝;查看 `error_code`/`error` | | `unknown` | false | 已调用但未取得完整确认、超时或账号变化,操作可能已经生效 | `accepted` 仅为原生方法的 BOOL 返回;`request_submitted` 仅表示取得本次原生事件 ID,均不能单独判断成功。每次调用等待约 30 秒并一次返回结果,调用方超时建议至少 40 秒,无需业务状态轮询。超时、断线或 `unknown` 时先核实群状态,**不要自动重新提交**;转让成功后原群主不再具备群主权限。 返回保留 `room_id`、`oper_type`、去重后的 `member_ids`,以及以下诊断字段: | 字段 | 含义 | | --- | --- | | `accepted`、`request_submitted`、`server_confirmed` | 分别表示原生接受、取得事件 ID、服务端明确确认 | | `request_cgi`、`event_id` | 本次 CGI 与事件 ID,未取得时为 null;不是业务轮询凭证 | | `wechat_ret`、`wechat_error_message` | 微信原始业务错误码及说明,错误码未知为 null | | `message_type` | 原生消息状态,未取得为 null | | `transport_error_type`、`transport_error_code` | 原生传输错误类型与错误码,未取得为 null | 常见 `error_code`:`OWNER_REQUIRED`、`ROOM_STATE_UNAVAILABLE`、`OPERATION_BUSY`、`WRITE_DISABLED`、`REQUEST_NOT_CONFIRMED`、`WECHAT_REQUEST_REJECTED`、`OPERATION_TIMEOUT`、`ACCOUNT_CHANGED`。不要把 null 错误码按 0 处理。 ### 群聊消息免打扰 ```json {"room_id":"123456789@chatroom","silence":true} ``` `silence:true` 开启免打扰,`false` 恢复通知。必须显式传入 JSON 布尔值,拒绝 `0/1`、字符串、null 和缺省。普通群成员即可操作,只影响当前账号的消息提醒设置。 | `status` | `ok` | 含义 | | --- | --- | --- | | `applied_locally` | true | 原生接受修改,联系人数据库回读与目标一致,`local_verified=true` | | `unchanged` | true | 数据库中原值已一致,`already_set=true`、`accepted=false`、`sync_requested=false`、`local_verified=true` | | `failed` | false | 参数、账号、群状态或有效写入状态等前置检查失败 | | `unknown` | false | 已进入修改路径,但原生返回失败、回读不一致或账号变化,结果未知 | 响应保留 `room_id`、目标值 `silence`、`accepted`、`sync_requested`、`already_set`、`local_verified`、`server_confirmed`、`status`;非法或缺失的 `silence` 可为 null。`accepted` 是原生 BOOL,`sync_requested` 只表示进入原生同步路径;**`server_confirmed` 固定为 false**,本地数据库确认不等于服务端同步 ACK。失败或未知时附 `error_code`/`error`,未知结果先核实设置,不自动重提。 ### 获取自己的二维码 请求体必须为 `{}`;自动使用当前登录账号,不接受 `appId`、`wxid`、`style`、`refresh` 或其他字段。本次请求二维码 URL,必要时生成 PNG;不读取旧磁盘图片,也不撤销旧二维码。同账号并发查询合并,等待约 30 秒,调用方超时建议至少 40 秒。 | 字段 | 类型 | 含义 | | --- | --- | --- | | `status` | string | `ok`、`restricted`、`failed` 或 `timeout`,结合外层 `ok` 判断成功 | | `wxid` | string | 当前账号 ID,前置失败时可为空 | | `qr_code_url` | string | 二维码内容 URL,可为空;不是图片下载地址 | | `image_base64` | string/null | PNG Base64,不含 data URI 前缀;无图为 null | | `image_mime_type` | string/null | 有图片时为 `image/png`,否则 null | | `has_qrcode`、`has_image` | boolean | 本次是否有二维码 URL/可解码图片;有 URL 无图片也可成功 | | `qrcode_status` | integer/null | 微信原始二维码状态,未知为 null,不硬编码具体限制含义 | | `restricted` | boolean/null | 已知状态是否非零,未知为 null | | `footer_wording` | string | 微信原始提示,可为空 | | `request_submitted` | boolean | 是否取得本次原生事件 ID | | `request_cgi`、`event_id` | integer/null | 本次 CGI(168)与事件 ID,未取得为 null | | `wechat_ret`、`wechat_error_message` | integer/null、string | 微信原始业务结果与说明 | | `message_type`、`transport_error_type`、`transport_error_code` | integer/null | 原生消息/传输状态,未取得为 null | 受限或失败时保留原始状态、提示及可用图片,不把受限结果按成功处理。失败时结合 `error_code`/`error`,不能只检查图片是否存在。 ### 命令信封、等待与重试 参数放在 `args`,个人二维码使用 `args:{}`。回包外层 `ok`/`error` 表示业务结果,`status`、`error_code` 和其余业务字段在 `result` 中;读取错误码使用 `result.error_code`,`result` 不再包含 `ok`/`error`。 `group_admin_operate`、`get_self_qrcode` 复用后台等待路径,一次返回终态,等待不占用通信队列;每次进入共享服务重新校验当前账号,不使用通用 `request_id` 结果缓存。`request_id` 用于关联结果,不能保证操作去重。免打扰沿用普通命令的结果缓存规则。每项新操作使用新的 `request_id`,结果丢失、超时或未知时先核实当前状态,不能靠重发群权限命令查询结果。 等待期间继续处理心跳及其他命令;同一等待中的 `request_id` 不重复启动,等待结束后不保证去重;连接同时等待的查询上限为 32 个。 通过现有 WebSocket 连接下发: ```json { "type": "command", "request_id": "req-admin-001", "command_name": "group_admin_operate", "args": { "room_id": "123456789@chatroom", "oper_type": 1, "member_ids": [ "wxid_member_a" ] } } ``` 通过 `type:command_result` 帧接收结果。以下省略传输公共字段,仅展示本条业务结果: ```json { "type": "command_result", "request_id": "req-admin-001", "command_name": "group_admin_operate", "ok": true, "error": "", "result": { "room_id": "123456789@chatroom", "oper_type": 1, "member_ids": [ "wxid_member_a" ], "status": "succeeded", "accepted": true, "request_submitted": true, "server_confirmed": true, "request_cgi": 889, "event_id": 42, "wechat_ret": 0, "wechat_error_message": "", "message_type": 0, "transport_error_type": 0, "transport_error_code": 0 } } ``` ### 验证范围 用户已确认群免打扰正常;管理员添加、移除和群主转让均收到 `ok=true`、`status=succeeded`、`wechat_ret=0`、`server_confirmed=true` 的真机回包。用户现已确认个人二维码、好友备注及相关业务在微信 `8.0.80` 验证通过。这些群管理接口不包含进群申请审核。 --- ## 19. 媒体下载与上传 适用插件:`8.0.80`。既有版本的图片、SILK 语音、视频、普通文件、表情的 HTTP 下载与上传,以及微信 CDN 上传已通过联调;视频和动画表情已验证缓存复用。三种通道共用同一媒体业务服务;本节分别给出当前通道的完整调用约定。 | command_name | 用途 | 默认等待 | | --- | --- | --- | | `download_message_media` | 准备消息媒体,返回手机本地副本 | 60 秒 | | `upload_message_media` | 自动准备媒体,再上传我们的服务器 | 60 秒 | | `upload_file_to_wechat_cdn` | 本地文件或外链文件上传微信 CDN | 120 秒 | | `message_media_probe` | 查看媒体或已有 CDN 任务状态 | 读取本地状态 | 通过 `type:"command"` 帧下发,结果以 `type:"command_result"` 帧返回。等待媒体准备和上传不阻塞 WebSocket 心跳与收包。文件字节仍由手机通过 HTTP 上传到 `upload_url`,不放进 WebSocket 帧。 参数放在 `args`,外层 `request_id` 为本次真实命令 ID;微信 CDN 上传使用这个 ID,不在 `args` 重复填写。业务 `ok/error` 移到结果外层,其余字段位于 `result`,业务机器码位于 `result.error_code`,中文说明位于 `result.message`。开关或协议校验失败时可能仅有外层 `error`、`result:{}`。每次执行重新校验当前账号,不使用通用命令结果缓存;CDN 任务复用由媒体服务处理。后端等待结果建议至少 **200 秒**,业务接口 `timeout_seconds` 范围 1~180 秒。 支持 `image` 图片、`voice` 语音、`video` 视频、`file` 普通文件(49/6)、`emoji` 普通表情。微信 WXGF/WXAM 表情转换为 GIF,保留动画;原本为 GIF、PNG、JPEG、WebP 的表情保留原格式。转换失败返回错误,不输出 `.bin`;游戏表情不支持导出。语音只输出微信原始 SILK,不转 MP3;Opus 返回 `unsupported_voice_codec`。 原事件为 `49/74`(或 `49/131`)时,文件完成后微信可能保留同一本地记录、把数据库内层类型更新为 `6/130`,并更换服务器消息 ID。插件只有在当前账号、会话、localID、发送方/接收方、消息类、创建时间及外层类型均匹配,旧/新服务器 ID 非零且不同、创建时间有效,并且完成文件的 `m_overwriteNewMsgId` 明确等于原服务器 ID 时,才接受这种覆盖恢复。内层类型和覆盖编号优先读取 app 扩展,不能由 wrap 上的 0 遮住。 仍使用原事件 `body.message_id` 调用即可;业务恢复成功后会把原事件 ID 的定位更新为完成记录,并保存最终服务器 ID 别名。两个 ID 均可继续定位该消息;没有登记过的任意历史消息不能因此恢复。此处 `6/130` 是覆盖定位的接受条件,**实际媒体下载/上传白名单仍只有普通文件 `49/6`**:仍为 `74/131` 的记录尚未进入可下载类型,`130` 也未在当前媒体白名单中开放。`media_type:"file"` 只校验类型,不能强制转换;是否能下载以恢复后的实际消息及附件状态为准。 当前数据库记录仍为 `49/74` 或 `49/131` 时,下载/上传返回 `error_code:"unsupported_media_type"`,中文说明为“该文件处于对方或我们,上传尚未完成(上传中)阶段,或完成消息尚未同步,请稍后使用同一 message_id 重试”。等待本机或对方上传完成、且完成记录同步到本机后,继续用同一个原事件 ID 重试即可;仅增大 `timeout_seconds` 不会让当前仍为 74/131 的请求等待类型变化。若长期没有完成记录,先用 `message_media_probe` 查看数据库类型、附件字段存在性及覆盖编号,不能据该提示认定本机或对方一定尚未完成,也不要把 `media_type:"file"` 当作强制下载开关。其他不支持类型仍返回原有通用提示;当前 130 文件的定位可恢复,但未开放媒体下载。 旧 `/download_message_image`(含 `/debug/download_message_image` 别名)和 `/upload_message_image` 已废弃,仅保留兼容。新接入分别使用 `/download_message_media` 和 `/upload_message_media`;图片仍传 `message_id`,类型自动识别,返回字段按统一媒体接口读取。长轮询 / WebSocket 的旧 `upload_message_image` 命令同样保留兼容,改用 `upload_message_media`。 上传接口检查有效发送状态;下载和探针不额外检查发送状态。授权沿用当前通道约定。以下消息 ID、文件路径、资源 ID 和域名均为示意,需替换成实际值。 ### 19.1 下载媒体 **下发 WebSocket 帧:** ```json { "type": "command", "request_id": "media_download_001", "command_name": "download_message_media", "args": { "message_id": "1234567890123456789" } } ``` | 参数 | 必填 | 说明 | | --- | --- | --- | | `message_id` | 是 | 当前账号已登记消息事件的 `body.message_id`,必须为字符串,避免大整数精度丢失;原 74 文件事件可按本节覆盖规则恢复完成记录,无需自行改为新 ID | | `media_type` | 否 | 指定时校验是否与原消息匹配;可省略 | | `timeout_seconds` | 否 | 本次总等待时间,默认 60,范围 1~180 秒,数字 | 只支持当前账号已登记且定位可恢复的消息,不增加任意历史消息查询。下载成功结果示例(数据、路径均为示意): ```json { "type": "command_result", "device_id": "webridge_device_example", "session_id": "sess_example", "account_id": "wxid_self", "runtime_version": "8.0.80-2", "build_marker": "webridge_build_20261106_settings_update_8080", "ts_ms": 1792713600000, "request_id": "media_download_001", "command_name": "download_message_media", "ok": true, "duration_ms": 42, "result": { "message": "已使用缓存文件", "message_id": "1234567890123456789", "media_type": "voice", "downloaded": true, "from_cache": true, "resolved_local_path": "/var/mobile/Containers/Data/Application/EXAMPLE/tmp/webridge_media/ACCOUNT_HASH/1234567890123456789/voice.silk", "resolved_file_name": "voice.silk", "file_size": 18576, "mime_type": "application/octet-stream", "md5": "0123456789abcdef0123456789abcdef", "codec": "silk", "voice_length_ms": 3260, "duration_ms": 42 }, "error": "" } ``` `resolved_local_path` 是手机沙盒路径,服务器无法直接 GET。需要服务器文件时调用上传接口。`voice_length_ms` 为语音时长,取不到为 0;`duration_ms` 为接口耗时。语音保持原字节,不播放、不主动设置已听状态。 缓存命中(插件副本或请求开始时微信已具备完整文件)返回 `from_cache:true`、`message:"已使用缓存文件"`;新下载返回 `false`、`"下载完成"`。图片沿用现有原生下载和解码策略,可能回退微信提供的标准清晰度,不承诺原图。 视频同时检查下载队列、声明大小和副本可读性;原生状态缺失时,必须核对完整文件 MD5 才允许导出,不能仅凭大小一致放行。表情转换后的 `file_size`、`md5` 和 `mime_type` 对应实际导出文件;GIF 的 MIME 为 `image/gif`。旧 `emoji.bin` 缓存会自动重新准备。 本次等待超时返回 `media_timeout`;后台资源准备最多继续到 180 秒。重复相同消息请求会合并正在进行的准备任务;可稍后重试同一个 `message_id`。 ### 19.2 上传到自有服务器 **下发 WebSocket 帧:** ```json { "type": "command", "request_id": "media_upload_001", "command_name": "upload_message_media", "args": { "message_id": "1234567890123456789", "timeout_seconds": 120 } } ``` 沿用下载命令的 `args` 参数;`timeout_seconds` 默认 60,覆盖本次等待准备和 HTTP 上传。另有可选 `args.upload_url`。`command_request_id` 使用命令外层真实 `request_id`。 默认 `upload_media` 就是设置里的业务服务器 base URL 加 `/agent/upload_media`,保留 base URL 的路径前缀。未配置业务服务器时须传入完整 HTTP/HTTPS `upload_url`。 请求体为 `multipart/form-data`,文件字段为 `file`。文字字段: | 字段 | 内容 | | --- | --- | | `message_id` / `resolved_source_message_id` | 来源消息 ID | | `command_request_id` | 本次真实 HTTP / 远程请求 ID | | `file_name` / `file_size` / `mime_type` / `md5` | 实际上传副本的文件名、大小、MIME、MD5 | | `media_type` | image / voice / video / file / emoji | | `account_id` / `device_id` / `session_id` | 沿用现有账号、设备和会话字段 | | `sender_id` / `chat_id` / `room_id` / `is_group` | 来源用户和会话,`is_group` 为文字 true / false | | `codec` / `voice_length_ms` | 仅语音,分别为 silk 和时长毫秒数 | 服务器必须返回 HTTP 2xx 和 JSON `{"ok":true,"asset_id":"实际资源ID"}`,其中 `asset_id` 为非空字符串。可以额外返回 `url`、`local_path`,分别映射为 `server_url`、`server_local_path`。仅有 HTTP 200 状态、HTML 页面、空响应或缺少 `asset_id` 都判为失败。视频、文件通过磁盘 multipart 文件上传,不把整个请求体装入内存。 成功返回下载元数据,另加: ```json { "type": "command_result", "device_id": "webridge_device_example", "session_id": "sess_example", "account_id": "wxid_self", "runtime_version": "8.0.80-2", "build_marker": "webridge_build_20261106_settings_update_8080", "ts_ms": 1792713600000, "request_id": "media_upload_001", "command_name": "upload_message_media", "ok": true, "duration_ms": 42, "result": { "message": "上传成功", "message_id": "1234567890123456789", "media_type": "voice", "downloaded": true, "from_cache": true, "uploaded": true, "asset_id": "asset_voice_example", "upload_http_status": 200, "server_url": "https://example.com/media/asset_voice_example" }, "error": "" } ``` `from_cache` 仅说明源媒体来自本地;每次调用仍向服务器发起上传。上传失败可复用缓存重试;服务器是否按 `command_request_id` 去重由服务器实现。旧 `/upload_message_image` 也复用新的磁盘 multipart helper,成功同样要求 `ok:true` 和非空 `asset_id`;旧图片接口其他字段保持原约定。 ### 19.3 上传微信 CDN **下发 WebSocket 帧:** ```json { "type": "command", "request_id": "cdn_example_001", "command_name": "upload_file_to_wechat_cdn", "args": { "file_path": "/var/mobile/Containers/Data/Application/EXAMPLE/tmp/webridge_media/ACCOUNT_HASH/1234567890123456789/voice.silk", "timeout_seconds": 120 } } ``` 也可以把 `args.file_path` 换成 `args.file_url:"https://example.com/demo.pdf"`,二者只传一个;外层 `request_id` 仍必填。 | 参数 | 必填 | 说明 | | --- | --- | --- | | 外层 `request_id` | 是 | 1~128 字符字符串;不要在 args 重复填写 | | `file_path` / `file_url` | 二选一 | 手机绝对路径 / 可下载的 HTTP(S) 文件地址 | | `timeout_seconds` | 否 | 默认 120,范围 1~180 秒 | 首版统一通过“给文件传输助手发送普通文件”完成上传,因此允许产生一条 filehelper 文件消息。下载得到的语音、图片等字节也可提交,但返回的都是**普通文件附件票据**,不能直接当作图片、语音、视频或表情专用发送票据,也不是公开下载 URL。 只有本次确切消息完成发送,且账号、localID、文件大小、MD5、服务端消息 ID、file_id、AES key 均满足校验时返回成功: ```json { "type": "command_result", "device_id": "webridge_device_example", "session_id": "sess_example", "account_id": "wxid_self", "runtime_version": "8.0.80-2", "build_marker": "webridge_build_20261106_settings_update_8080", "ts_ms": 1792713600000, "request_id": "cdn_example_001", "command_name": "upload_file_to_wechat_cdn", "ok": true, "duration_ms": 42, "result": { "message": "微信 CDN 上传成功", "request_id": "cdn_example_001", "uploaded": true, "pending": false, "stage": "uploaded", "cdn_type": "file", "receiver": "filehelper", "file_name": "voice.silk", "file_size": 18576, "md5": "0123456789abcdef0123456789abcdef", "local_id": 123, "server_id": "1234567890123456790", "cdn": { "file_id": "微信实际返回值", "aes_key": "微信实际返回值", "attach_id": "有则返回" }, "staging_deleted": false, "cleanup_status": "pending_ticket_validation" }, "error": "" } ``` 当前保留临时消息,`staging_deleted:false`。不启用按条清理,以保留后续发送所需的票据;当前实现不会因超时删除发送中的消息。CDN 密钥只出现在成功业务响应中,探针不返回密钥或完整 XML。 同账号相同 `request_id` 绑定原任务,重复参数复用等待或结果,不再次发送;本地文件还存在时会核对内容变化,参数变化返回 `request_id_conflict`。同 URL 重试指原任务的下载结果,不重新抓取 URL。**幂等记录保存在当前微信进程内**,重启后失效;结果未知时不要重启后盲目重提。 等待超时返回 外层 `ok:false,error:"media_timeout"`,`result.pending:true`;保留同一个 `request_id` 和文件参数继续调用,或用探针查询。后台最多观察 15 分钟;其后业务接口仍会按原消息重查迟到结果。最多保留 128 个 CDN 请求 ID,达到上限返回 `cdn_task_limit`,不会淘汰旧任务而隐式重传。 外链文件单次上限 512MB,开始前需预留相应缓存配额;本地文件受全局 1GB 配额限制。源文件、媒体缓存和上传体并存时,实际可用单文件大小会更低。空间不足明确返回错误,不扩大配额。 ### 19.4 调试探针 查询某条消息,不触发下载、播放或发送: **下发 WebSocket 帧:** ```json { "type": "command", "request_id": "media_probe_001", "command_name": "message_media_probe", "args": { "message_id": "1234567890123456789" } } ``` 消息探针 v2 在媒体白名单检查前执行,因此可检查 `49/74`、`49/130`、`49/131` 等当前不能下载的类型。`ok:true` 只表示取得可诊断的消息对象,不代表媒体可下载、文件完整或定位关系已更新。它不会调用 `ChangeForDisplay`、触发下载/播放/发送,不回写消息索引或覆盖关系;缓存检查不创建目录、不触发清理,也不更新缓存使用时间。 `cached_message` 是缓存对象快照,`database_message` 是按已登记 localID 读出的、其余身份字段已核对的数据库候选快照;两者可能引用同一个原生对象,不能视作不可变的历史前后对照。顶层消息状态优先取数据库候选,没有候选才取缓存,以 `probe_source` 区分;即使候选因服务器 ID 变化被业务恢复拒绝,也会优先用于诊断。两者均不可用时返回 `message_not_found`;诊断期间账号变化返回 `account_changed`,业务机器码在 `result.error_code`/外层 `error`,原因在 `result.message`。 以下字段位于本次命令结果的 `result`;字段类型中的布尔值在部分原生回包中可能序列化为 `0/1`,读取时应兼容。`null` 表示未取得/不适用,与已读取到数值 `0` 不同。 | 顶层字段 | 类型 | 含义 | | --- | --- | --- | | `probe_version` / `message_id` | 整数 / 字符串 | 当前消息探针版本为 `2`;消息 ID 仍是本次传入的原事件 ID,不会被替换为最终服务器 ID | | `probe_source` | 字符串 | `database` 或 `cache`,决定顶层消息快照来源 | | `cached_message` / `database_message` | 对象或 null | 各自的消息快照;没有相应对象为 null,快照字段见下表 | | `lookup` | 对象 | 定位诊断,包含 `database_status`、`refresh_accepted`,取得登记记录时另有两个 `registered_*_id` 字符串 | | `lookup.registered_local_id` / `lookup.registered_server_id` | 十进制字符串,可缺省 | 探针开始时已登记的 localID/服务器 ID;保留无符号 64 位值,禁止转为 JavaScript Number | | `lookup.refresh_accepted` | 布尔 | 仅表示当前候选通过业务定位规则,包括服务器 ID 相同或满足明确文件覆盖条件;仍为 `74` 时也可能为 true,不代表可下载,也不表示探针已保存更新 | | `cache_ready` | 布尔 | 以当前账号、传入消息 ID 和诊断媒体类型,只读检查插件副本是否可用;false 不等于微信没有原生文件 | | `message_manager_class` / `signatures` | 字符串 / 对象 | 原生消息管理器类及下载入口签名诊断,保留 `responds`、`type_encoding`、返回类型和参数信息;不能仅凭方法存在认定下载成功 | | `emoji` | 对象,可缺省 | 仅普通表情保留原有资源诊断;不代表已导出或上传 | | `message` | 字符串 | 中文诊断说明;成功通常为“已读取媒体状态,未触发下载或发送” | `lookup.database_status` 的完整取值: | 值 | 含义 | | --- | --- | | `ok` | 数据库候选通过定位规则,`refresh_accepted:true`;可接受的明确文件覆盖也属于此状态,但仍不保证类型受支持或文件完整 | | `lookup_not_found` | 当前账号没有可用的已登记定位信息;若缓存对象还在,仍可返回缓存诊断 | | `native_unavailable` | 原生消息管理器或数据库重读入口不可用 | | `server_id_changed` | 其余身份匹配,但服务器 ID 不同且没有可接受的赋号/文件覆盖证据;`refresh_accepted:false`,候选仅供诊断 | | `message_not_found_or_identity_mismatch` | 没找到数据库对象,或 localID、会话、双方、消息类、时间、外层类型等身份不匹配;不返回该候选 | | `account_changed` | 诊断期间当前账号变化;本次业务结果失败,不沿用原账号对象 | 以下为顶层、`cached_message`、`database_message` 共用的消息快照字段;顶层另带上述探针与定位字段: | 快照字段 | 类型 | 含义 | | --- | --- | --- | | `message_class` / `extension_class` | 字符串 | 消息对象/扩展对象类;扩展不存在时为空字符串 | | `raw_message_type` | 整数或 null | 原生外层类型,例如 `49` | | `app_msg_type` / `wrap_app_msg_type` / `extension_app_msg_type` | 整数或 null | `app_msg_type` 是实际按扩展优先规则读取的 app 内层类型,非外层 49 时为 null;后两项分别直接读取 wrap 与扩展。wrap 为 0、扩展为 6 时不能把 wrap 的 0 当成最终类型 | | `media_type` / `type_supported` | 字符串 / 布尔 | 当前业务识别类型及是否在下载白名单;不支持时 `media_type:""`、`type_supported:false` | | `diagnostic_media_type` | 字符串 | 仅用于读取状态;外层 49 且内层 6/74/130/131 时为 `file`,不扩大业务支持范围 | | `m_uiMesLocalID` / `m_n64MesSvrID` / `m_overwriteNewMsgId` | 十进制字符串或 null | 本地 ID、当前服务器 ID、覆盖的原服务器 ID;取得数值 0 时返回 `"0"`,缺失为 null | | `bAppMsgCompleteFlag` / `m_bAppAttachExistInSvr` | 数字或 null | 原生完成标记/附件已存在于服务器的标记,常见 0/1;保留现值,不单独据此判定本地文件完整 | | `m_uiDownloadStatus` / `m_uiUploadStatus` | 整数或 null | 原生字段现值;未取得为 null,0 不是缺失,也不能把上传状态当成下载完成 | | `IsFileMsg` / `IsFileUploadingMsg` | 布尔或 null | 微信原生分类结果;方法不可用或签名不符为 null。分类名不代替附件完成/可下载证据 | | `has_m_nsAppAttachID` / `has_m_nsMsgAttachUrl` / `has_m_nsAesKey` / `has_m_authkey` | 布尔 | 相应原生附件标识、地址、密钥/鉴权字段是否非空,只返回存在性,不输出原值 | | `native_path` | 字符串 | 微信原生文件路径;无法取得时为空字符串,不是服务器可直接 GET 的 URL | | `native_file_exists` / `native_file_readable` | 布尔 | 原生路径是否存在/是否可读;不表示已完整下载 | | `native_size` / `expected_size` | 非负整数 | 原生本地大小/原生声明大小,单位字节;没有可用大小时为 0 | | `size_matches` | 布尔 | 声明大小大于 0 且与本地大小相同;仅比较大小,未做完整文件校验,不能据此认定下载完成 | | `download_status` | 整数 | 兼容的原生下载状态,缺失时回退 0;需要区分缺失时读取上面的 `m_uiDownloadStatus` | | `native_complete` / `native_downloading` | 布尔 | 原生完成/队列进行中观察值;文件完成状态为 2,普通视频为 4、原始视频为 11,语音读取原生结束标记。false 不代替完整性结论;队列入口可用性另看 `signatures`/视频的 `video_queue_known` | | `video_queue_known` / `has_expected_md5` / `raw_video` | 布尔 | 视频下载队列查询是否可用、是否有 32 位声明 MD5、是否原始视频;不返回声明 MD5 原文 | | `voice_cancelled` / `voice_length_ms` / `voice_format` | 非负整数 | 原生语音取消标记、毫秒时长及格式值;未取得时为 0,只用于语音诊断 | | `xml` | 对象,可缺省 | 仅外层 49 的 XML 安全摘要,字段如下;不返回完整 XML | `xml.parsed` 为布尔;无法识别或解析时仅返回 `{"parsed":false}`。解析成功时,`xml.type`、`xml.totallen`、`xml.overwrite_newmsgid` 均为十进制字符串或 null,分别对应 app 类型、XML 声明字节数和覆盖的原服务器 ID;缺失或不是纯数字为 null,与字符串 `"0"` 不同。`xml.has_attachid`、`xml.has_cdnattachurl`、`xml.has_aeskey`、`xml.has_authkey` 为布尔,表示 XML 对应附件字段非空,只提供存在性。 探针不执行完整文件 MD5、视频解码或媒体导出校验,也不通过会修正数据库的状态接口恢复下载状态。因此 `native_complete:false` 不一定表示业务下载必定失败,`size_matches:true` 也不表示业务下载必定成功;最终以下载/上传接口返回的校验结果为准。 成功结果示意(节选,编号与路径均为虚构,省略类名、签名及部分通用快照字段): ```json { "type": "command_result", "device_id": "webridge_device_example", "session_id": "sess_example", "account_id": "wxid_self", "runtime_version": "8.0.80-2", "build_marker": "webridge_build_20261106_settings_update_8080", "ts_ms": 1793836800000, "request_id": "media_probe_001", "command_name": "message_media_probe", "ok": true, "result": { "probe_version": 2, "message": "已读取媒体状态,未触发下载或发送", "message_id": "1234567890123456789", "probe_source": "database", "lookup": { "database_status": "ok", "refresh_accepted": 1, "registered_local_id": "4096", "registered_server_id": "1234567890123456789" }, "cache_ready": false, "raw_message_type": 49, "app_msg_type": 6, "media_type": "file", "type_supported": 1, "diagnostic_media_type": "file", "m_uiMesLocalID": "4096", "m_n64MesSvrID": "1234567890123456790", "m_overwriteNewMsgId": "1234567890123456789", "native_size": 0, "expected_size": 8126, "native_complete": false, "cached_message": { "raw_message_type": 49, "app_msg_type": 74, "wrap_app_msg_type": 0, "extension_app_msg_type": 74, "media_type": "", "type_supported": 0, "diagnostic_media_type": "file", "m_uiMesLocalID": "4096", "m_n64MesSvrID": "1234567890123456789", "m_overwriteNewMsgId": "0", "bAppMsgCompleteFlag": 0, "m_bAppAttachExistInSvr": 0, "m_uiDownloadStatus": 0, "m_uiUploadStatus": null, "has_m_nsAppAttachID": false, "has_m_nsMsgAttachUrl": false, "has_m_nsAesKey": false, "has_m_authkey": false, "native_path": "/var/mobile/Containers/Data/Application/EXAMPLE/Documents/appdata/4096.csv", "native_size": 0, "expected_size": 8126, "native_file_exists": false, "native_file_readable": false, "size_matches": false, "download_status": 0, "native_complete": false, "native_downloading": false, "xml": { "parsed": true, "type": "74", "totallen": "8126", "overwrite_newmsgid": "0", "has_attachid": false, "has_cdnattachurl": false, "has_aeskey": false, "has_authkey": false } }, "database_message": { "raw_message_type": 49, "app_msg_type": 6, "wrap_app_msg_type": 0, "extension_app_msg_type": 6, "media_type": "file", "type_supported": 1, "diagnostic_media_type": "file", "m_uiMesLocalID": "4096", "m_n64MesSvrID": "1234567890123456790", "m_overwriteNewMsgId": "1234567890123456789", "bAppMsgCompleteFlag": 1, "m_bAppAttachExistInSvr": 1, "m_uiDownloadStatus": 0, "m_uiUploadStatus": null, "has_m_nsAppAttachID": true, "has_m_nsMsgAttachUrl": true, "has_m_nsAesKey": true, "has_m_authkey": false, "native_path": "/var/mobile/Containers/Data/Application/EXAMPLE/Documents/appdata/4096.csv", "native_size": 0, "expected_size": 8126, "native_file_exists": false, "native_file_readable": false, "size_matches": false, "download_status": 0, "native_complete": false, "native_downloading": false, "xml": { "parsed": true, "type": "6", "totallen": "8126", "overwrite_newmsgid": "1234567890123456789", "has_attachid": true, "has_cdnattachurl": true, "has_aeskey": true, "has_authkey": false } } }, "error": "" } ``` 示例中数据库文件已完成并明确覆盖原 ID,因此定位 `ok` 且 `refresh_accepted:1`;本地仍没有 8126 字节文件,需调用 `download_message_media` 才会准备资源。探针本身不会保存这次覆盖恢复。若数据库仍为 74,即使 `refresh_accepted:1`,也只表示其身份可接受,不能当作已具备可下载附件。 查看已有 CDN 任务,不创建新任务,也不返回 AES / XML: **下发 WebSocket 帧:** ```json { "type": "command", "request_id": "cdn_probe_001", "command_name": "message_media_probe", "args": { "request_id": "cdn_example_001" } } ``` 结果外层 `ok:true` 表示读取成功;`result` 内的 `operation_ok`、`stage`、`pending`、`task_finished`、`task_error` 表示上传状态。`has_file_id` / `has_aes_key` 仅表示字段是否存在。业务成功结果及票据仍从 `upload_file_to_wechat_cdn` 命令的 `result` 获取。 探针的 `message_id` 和 `request_id` 二选一。查询 CDN 时,参数里的 `request_id` 是被查询的原上传任务 ID。外层 `request_id` 则是本次探针调用 ID,二者不同。 ### 19.5 缓存文件规则 1. 顺序:插件完整副本 → 微信完整本地文件 → 原生下载。同账号、消息和媒体类型的并发准备合并;同时最多准备 32 项。 2. 插件副本位于 `tmp/webridge_media/<账号摘要>/<消息ID>/`;微信原文件不移动、不删除。写 `.part`,校验大小、必要 MD5 和 SILK 头后原子改名;只返回正式副本。 3. 记录文件名、大小、实际 MD5、类型和最近使用时间,不记录 AES 或 XML。命中前检查文件是否存在、可读、非空、大小一致,语音另核对 SILK 头;旧 `emoji.bin` 不再直接命中,会从微信资源重新导出。 4. 最近使用后保留 1 小时,所有账号共用 1GB 上限,包含本次新增副本、CDN 来源暂存和 multipart 临时文件。空间不足先淘汰最久未使用的空闲项,在用项不删,仍不足返回 `cache_space_insufficient`。已有其他发送目录继续使用其原有规则。 5. 启动及新任务开始时清理;上传成功也不马上删除。系统可提前回收 tmp,文件丢失后会重新准备。 ### 19.6 复用后端接收服务 **聊天媒体与朋友圈媒体可以使用同一个后端应用、同一个端口和同一套存储。** 当前上传格式不同,推荐在现有业务后端提供两个接收入口,共用保存文件和生成访问地址的逻辑: | 用途 | 推荐接收路由 | 手机发出的请求 | 成功条件 | | --- | --- | --- | --- | | 聊天媒体回传 | `POST /agent/upload_media` | `multipart/form-data`,文件字段 `file`,其余字段见本节上传约定 | HTTP 2xx,JSON `ok:true` 且非空 `asset_id` | | 朋友圈媒体回传 | `PUT /uploads/{resource_id}` | 原始文件正文,Content-Type 为实际图片/视频 MIME;插件也支持原始正文 POST | 完整接收并保存后返回 HTTP 2xx;插件还核对发送字节数 | 朋友圈使用本篇 [朋友圈媒体下载](#moments-media-download) 中的 `upload` 对象:`url` 指向上述接收地址,`resource_id` 由后端分配,`method` 默认 `PUT`,`headers` 可携带鉴权。插件不解析朋友圈上传响应正文,只读取状态码;响应不要超过 64KiB。朋友圈首次受理不等于文件上传完成,仍需等 `moments_result` 或查询任务,确认 `status:"succeeded"`、`media_ready:true`、`delivery:"upload"`、`upload_completed:true`。上传结果未知时先按 `resource_id` 核对后端,插件不会自动重传。 仅处理 multipart 的 `/agent/upload_media` 不能直接接收朋友圈原始正文,把朋友圈 method 改成 POST 也不会改变正文格式。后端增加原始正文入口即可,**插件不需要为了共用后端而改动**。两个入口可以共用资源表、文件目录和 `GET /files/...`;若沿用朋友圈网页示例,需支持按 `resource_id` 读取,视频读取需支持 Range。 `upload_file_to_wechat_cdn` 是手机通过微信原生链路上传微信 CDN,不需要自有上传接收服务。其 `file_url` 可以使用上述后端已有文件地址;返回的普通文件附件票据不能代替朋友圈发布专用上传结果。 仓库中的 `demo/消息媒体上传接收端/media_upload_server.py` 与 `demo/朋友圈媒体上传接收端/server.py` 目前仍是两个独立示例,尚未合并;正式接入推荐复用同一业务后端,按本节支持两个入口。 ### 19.7 调用检查与常见错误 WebSocket 结果读取 `command_result.result`。不要把结果信封中的业务字段当作文件字节。 1. 用当前账号已登记事件的 `body.message_id`(含满足覆盖恢复条件的原 74 文件事件 ID)调下载,再调一次检查 `from_cache:true`;核对文件名、MIME、大小和 MD5。语音应为 SILK,WXGF/WXAM 动画表情应导出 GIF。 2. 调上传我们的服务器,确认文件实际落盘、`asset_id` 非空;`from_cache:true` 只表示手机复用源文件,每次请求仍会上传。 3. 下载或上传等待超时可重试同一 `message_id`;微信 CDN 超时则必须保留原 `request_id` 和文件参数,避免重复发送。CDN 临时消息目前保留,删除后的票据消费不在已确认范围内。 4. 下载失败时同时提供业务响应和媒体探针响应;CDN 结果未知时用原上传 ID 查询,探针不含可消费票据。 常见错误:`invalid_message_id`、`media_type_mismatch`、`message_not_found`、`not_logged_in`、`account_changed`、`unsupported_media_type`、`unsupported_voice_codec`、`media_timeout`、`cache_space_insufficient`、`media_checksum_mismatch`、`server_upload_failed`、`upload_url_required`、`request_id_conflict`、`cdn_task_limit`、`cdn_result_unknown`。 等待超时结果示例: ```json { "type": "command_result", "device_id": "webridge_device_example", "session_id": "sess_example", "account_id": "wxid_self", "runtime_version": "8.0.80-2", "build_marker": "webridge_build_20261106_settings_update_8080", "ts_ms": 1792713600000, "request_id": "media_download_001", "command_name": "download_message_media", "ok": false, "duration_ms": 10001, "result": { "error_code": "media_timeout", "message": "本次等待超时,原下载继续;可用同一 message_id 重试", "message_id": "1234567890123456789", "media_type": "video", "duration_ms": 10001 }, "error": "media_timeout" } ``` 业务失败优先读取 `result.error_code` / 外层 `error`,中文原因读取 `result.message`;协议或开关失败按外层 `error` 处理。 **本次验证范围:** 本次文件覆盖恢复已由用户在 HTTP 真机确认:使用原 `49/74` 事件 ID,首次下载 8126 字节 CSV 成功,`from_cache:false`;同一个 ZIP 未完成时数据库仍为 74 且无 AES/附件/CDN 字段,完成后数据库变为 6 并以覆盖编号指向原 ID,随后首次下载 12,155,230 字节 ZIP 成功,`from_cache:false`。这些历史结果记录了共享媒体服务的实际文件链路;用户现已确认相关媒体功能在微信 `8.0.80` 验证通过。媒体白名单、索引保存及文件完整性条件仍适用。 --- ## 20. 公众号:搜索、关注与历史文章 公众号命令统一管理订阅号与服务号。公众号相关业务已由用户在微信 `8.0.80` 验证通过。目标必须使用真实公众号 username,不接受昵称或 alias,也不依赖 `gh_` 前缀判断类型。下方示例均为虚构。 | 命令 | 必需参数 | 可选参数 | | --- | --- | --- | | `official_account_search` | `/official_account_search` | `keyword` | — | | `official_accounts` | `/official_accounts` | — | `type`、`keyword`、`limit`、`offset` | | `official_account_follow` | `/official_account_follow` | `id` | — | | `official_account_unfollow` | `/official_account_unfollow` | `id` | — | | `official_account_articles` | `/official_account_articles` | `id` | `limit`、`cursor` | 三种通道使用相同参数及业务语义。参数采用严格类型校验,未知字段报 `INVALID_ARGUMENT`;整数不接受数字字符串或布尔值。需正常授权和登录;搜索、列表、文章查询不受有效发送状态限制,关注/取关需要有效发送状态开启,关闭时报 `SEND_DISABLED`。 ### 已关注列表 ```json {"type":"all","keyword":"","limit":100,"offset":0} ``` 所有参数可省略。`type` 支持 `all`(默认)、`subscription`、`service`,区分大小写;`keyword` 是按账号、展示名、昵称、备注、alias 做不区分大小写的本地包含匹配。`limit` 默认 100、范围 1–1000;`offset` 默认 0、范围 0–4294967295。 返回 `status:"ok"`、`type`、`keyword`、`items`、`total`、`limit`、`offset`、`has_more`。`total` 是筛选后的数量;按展示名和账号排序,偏移超出范围时返回空页。每项沿用联系人摘要中的 `account_id`、`display_name`、`nick_name`、`remark`、`alias`、`avatar_url` 等字段,并增加 `account_type`(`subscription`/`service`)及 `is_followed:true`。这是本机已同步的名单,关注/取关后的关系可能延迟更新。 未登录返回 `ACCOUNT_UNAVAILABLE`,联系人尚未就绪返回 `CONTACTS_NOT_READY`,不能把失败当作空名单。它不是全网公众号搜索。 ### 公众号关键词搜索 ```json {"keyword":"示例"} ``` 调用 `official_account_search`,`args` 仅接受 `keyword` 字符串,去除首尾空白后长度为 1–256 个 UTF-16 代码单元。输入公众号名称、微信号或关键词,联网返回微信原生首批候选,支持发现未关注公众号。 成功返回 `status:"ok"`、`keyword`、`items`、`returned_count` 和 `request_submitted`;取得原生返回码时包含 `native_ret`。候选复用联系人摘要中的 `account_id`、`display_name`、`nick_name`、`alias`、`avatar_url` 等字段,增加 `account_type`(本地已知时为 `subscription`/`service`,未取得为 `null`)与 `is_followed`(本地当前关注关系,可能延迟同步)。`contact_found` 仅表示本地联系人是否存在;未缓存的联网候选仍会返回。保留微信顺序并按真实 username 去重。无匹配时为 `ok:true`、`items:[]`、`returned_count:0`;超时、断网和微信业务错误不能当作无结果。 选中候选后,将其 `account_id` 作为 `official_account_follow` 的 `id`,例如 `{"id":"official_account_example"}`。搜索不会自动关注,也不受有效发送状态限制。它仅返回首批候选,不支持 `limit`、`offset`、`cursor`、`page`,不返回 `total` 或 `has_more`;匹配范围、排序及条数由微信决定。 约 30 秒业务超时,客户端等待建议至少 40 秒。 响应包含 `search_diagnostics`:当前 `request_cgi` 为 719,`has_content` 表示是否取得非空 JSON,`json_byte_count` 为其 UTF-8 字节数,`json_ret` 为 JSON 业务状态,`section_count` 为结果分组数。`raw_item_count`、`parsed_item_count`、`contact_item_count`、`missing_username_count`、`duplicate_count` 用于定位候选解析;`missing_contact_count`、`unsupported_type_count` 为旧诊断保留字段,当前为 0。`native_business_type` 为请求的公众号类型 1,`native_continue_flag`、`native_offset` 仅用于诊断,不开放分页。原生错误、JSON 业务错误、缺失内容或格式不兼容均返回失败,不能当成无匹配;只有有效成功的空列表才返回 `ok:true`、`items:[]`。诊断从开始原生请求时即可出现,不包含原始 JSON 或账号凭据。 ### 关注与取消关注 两项均只传 `{"id":"official_account_example"}`。关注会先核对目标身份,再走公众号关注流程;返回单次结果,无需查询任务。 | 操作 | `status` | `ok` | 含义 | | --- | --- | --- | --- | | 关注 | `already_followed` | true | 本地已关注,`request_submitted=false` | | 关注 | `followed` | true | 原生成功回调,且本地已观察到关注 | | 关注 | `pending_sync` | true | 原生成功回调,本地关系尚未同步;`is_followed=false` 不代表关注失败 | | 关注 | `unknown` | false | 已调用原生关注入口,尚无确定结果;先查列表,不自动重发 | | 关注 | `timeout` | false | 前置资料查询等阶段超时,尚未调用关注入口 | | 取关 | `already_unfollowed` | true | 本地已无关注关系,`request_submitted=false`,不重复清理 | | 取关 | `submitted` | true | 本地流程及同步提交已执行,`request_submitted=true`;未确认服务器最终结果 | | 取关 | `unknown` | false | 原生调用中发生异常,可能已有副作用,`request_submitted=null` | | 两项 | `failed` | false | 参数、前置校验或微信业务失败,查看错误码及中文错误 | 响应包含目标 `account_id`,并按取得的信息返回 `is_followed`、`request_submitted`。`is_followed` 只表示本地观察值,未知可为 null 或省略;校验失败时也可省略目标字段。关注无回包时 `native_invoked=true`、`request_submitted=null`,不能视为确认提交;收到本次回包才可标为 true。`profile_request_submitted=true` 只表示前置资料查询已提交。 **取关可能清理对应本地会话、聊天消息和其他关联信息。** 进入原生取关调用后返回 `chat_history_may_be_deleted=true`;`submitted` 后本地 `is_followed` 仍可能暂为 true。结果未知时先查询关注关系,避免重复清理。 同一公众号仍在关注时返回 `OPERATION_IN_PROGRESS`;搜索、关注及文章请求合计最多同时保留 16 个,超出返回 `TOO_MANY_REQUESTS`。搜索、关注与文章查询整次约 30 秒超时、约 32 秒等待保护,客户端等待建议至少 40 秒。 ### 历史文章按篇数批量续取 ```json {"id":"official_account_example","limit":100} ``` `limit` 默认 100,范围 1–1000,按多图文展开后的文章篇数计数。插件自动连续读取微信原生小页并合并,最多返回指定篇数;不支持 `page`、`page_size`、`offset`,不提供历史总文章数,也不提供完整正文。 成功返回以下字段: | 字段 | 类型 | 含义 | | --- | --- | --- | | `status` | string | `ok` | | `account_id` | string | 目标公众号 username | | `limit` | integer | 本次最多返回篇数 | | `returned_count` | integer | 等于 `items.length`,不超过 `limit` | | `items` | array | 本批展开并合并后的文章 | | `has_more` | boolean | 仍有缓存余量,或微信原生历史尚未结束 | | `next_cursor` | string/null | 有后续时原样传回;全部结束时为 null | | `request_submitted` | boolean | 本次是否提交过微信原生资料请求;仅使用缓存余量时为 false | 每篇文章包含 `account_id`、`app_msg_id`(十进制字符串)、`item_index`(原生篇目位置)、`title`、`digest`、`cover_url`、`url`、`author`、`publish_time`(Unix 秒,未知为 null)、`is_paid`(boolean/null)。`account_id + app_msg_id + item_index` 共同定位文章;付费标记不能证明当前账号拥有阅读权限。 需要更早历史时,传入上一批的游标: ```json {"id":"official_account_example","limit":100,"cursor":"上一批返回的next_cursor"} ``` 原生一页超过本批额度的文章会随游标保留,下一批先返回余量,不跳篇。微信已经结束但缓存未取尽时 `has_more` 仍为 true;空原生页会继续翻,不等于历史结束。成功不足 `limit` 时表示已到微信允许展示的末尾。整批失败返回 `ok=false`,不把部分结果伪装成完整成功;在游标有效期内可从原输入游标重试读取,或降低 `limit`。 `cursor` 可省略或传空字符串,从最新文章开始;不能传 null。它是进程内不透明 token,绑定当前账号、登录会话和目标公众号,单个 token 从生成起有效 10 分钟;全局最多保留 128 个,超出时淘汰最旧者。切换账号/会话、重启插件都会使旧游标失效,返回 `INVALID_CURSOR` 后应从最新文章重新读取。 ### 错误、账号变化与发送文本 错误沿用中文 `error` 和机器可读 `error_code`。常见错误包括 `INVALID_ARGUMENT`、`INVALID_ACCOUNT_ID`、`INVALID_ACCOUNT_TYPE`、`ACCOUNT_UNAVAILABLE`、`CONTACTS_NOT_READY`、`NATIVE_API_UNAVAILABLE`、`REQUEST_NOT_STARTED`、`REQUEST_TIMEOUT`、`WECHAT_REQUEST_FAILED`、`RESPONSE_INVALID`。文章另有 `INVALID_CURSOR`、`ARTICLES_RESTRICTED`、`CURSOR_NOT_ADVANCING`;同一请求期间账号或登录会话变化返回 `ACCOUNT_CHANGED`。失败时不保证有 `items`、`has_more`、`next_cursor` 等成功字段。 向公众号发送文本仍使用已有 `send_message`,将同一个 username 放入 `receiver`,例如 `{"receiver":"official_account_example","content":"你好"}`。沿用原发送结果与公众号私信权限限制,成功不代表对方已收到或已读。 ### 命令回包、等待与去重 请求字段放在 `args`。回包沿用 `command_result`:外层 `ok`/`error` 表示结果,业务的 `status`、`error_code`、`request_submitted`、`items`、`next_cursor` 等保留在 `result`,读取错误码用 `result.error_code`。外层 `account_id` 是当前登录账号,`result.account_id` 才是目标公众号;前置校验失败可省略后者。 五项命令在后台执行并一次返回终态,不阻塞通信队列;仍需等待约 30 秒业务超时,客户端建议至少 40 秒。每次都会进入共享服务重新校验当前账号,**公众号五项不走通用的最近 100 条 `request_id` 结果缓存**。`request_id` 用于关联回包,不能充当操作去重保证。不要因结果丢失而盲目重发关注/取关,先查询本地关系。 通过已建立的 WebSocket 连接下发: ```json {"type":"command","request_id":"req-official-articles-001","command_name":"official_account_articles","args":{"id":"official_account_example","limit":100}} ``` 通过 `type:"command_result"` 帧取得结果。等待期间连接继续处理心跳及其他命令;等待中的同一 `request_id` 不重复启动,等待完成后不能依赖通用结果缓存去重。连接同时等待的查询上限为 32 个,超出返回失败。 --- ## 21. 朋友圈:调用、异步结果与后续操作 你的后端实现WebSocket服务,手机主动连接;以下下发JSON通过已连接的WS发送,不是HTTP请求。无需让后端访问手机HTTP。示例ID、上传域名和Token均需替换,所有数据均为虚构。 最短读取流程: 1. 握手后确认capabilities包含所需`moments_*`命令。 2. 下发`moments_list`,保存`command_result.result.operation_id`;首次`ok=true`不表示页面已返回。 3. 等`moments_result`完成事件,或下发`moments_operation`查询原编号。 4. 从完成结果`items[]`继续详情、历史、互动或媒体上传。 ```json { "type": "command", "request_id": "demo-list-001", "command_name": "moments_list", "args": {} } ``` 查询同一操作: ```json { "type": "command", "request_id": "demo-operation-query-001", "command_name": "moments_operation", "args": { "operation_id": "11111111-1111-4111-8111-111111111111" } } ``` **每次查询必须使用新的外层request_id**(例如query-002、query-003),args.operation_id保持原值。重复request_id会命中命令结果缓存,可能一直看到第一次pending。查询命令的外层request_id对应本次查询,result.request_id仍属于原任务。 ### 返回结果怎么判断 | status/情况 | 意义 | 接下来怎么做 | |---|---|---| | preparing | 准备资源或构造任务 | 等完成事件或查询原operation_id | | pending | 微信请求或媒体上传进行中 | 查询原操作,不能再调用入口来“催进度” | | succeeded | 本次业务完成 | 按下表读取对应业务结果 | | failed | 本次失败 | 看error与诊断;保留operation_id;上传可能需要核对后端 | | unknown | 未取得可信最终确认 | 尤其write_accepted=true时不重发写入;继续查询原操作或核实实际状态 | | 同步返回且没有operation_id | 本地权限读取或空参数状态探针 | 直接使用当前结果,不再查询不存在的操作 | | 业务 | 成功判断 | |---|---| | 页面/详情/联网写操作 | status=succeeded,通常server_confirmed=true;unchanged表示联网确认无需写入 | | friend/recent权限查询 | 同步ok=true、source=local_settings;server_confirmed=false正常 | | 视频准备探针 | status=succeeded且media_ready=true;没有发帖,server_confirmed=false正常 | | 本地媒体准备 | status=succeeded且media_ready=true,delivery=local,读取media_url | | 手机主动上传 | status=succeeded且media_ready=true且upload_completed=true,delivery=upload,按resource_id读取后端 | accepted只表示微信查询/任务受理。互动和单帖权限先查详情,所以accepted=true可能只是详情查询已发出。write_accepted才表示提交了微信写操作;它不用于表达上传文件。server_confirmed只描述微信结果,不描述上传目标;媒体即使上传成功仍为false。 `request_id` 用于关联,最长 128 个 UTF-16 单位,由本模式命令外层写入。它不是跨重启的幂等保证。操作记录最多48项,完成记录在后续请求进入时可能淘汰,通常最多保留10分钟;重启即丢失,不是永久任务数据库。账号切换后不能查询旧账号操作,旧账号事件也不会包成新账号事件。 一般查询/资源准备120秒超时且未提交微信写入时失败;已提交写入却未确认时可能unknown,晚回包仍可用更高revision更新原操作。相同范围处理中或unknown锁未释放时会拒绝新任务。网络断开也不能证明微信写入或后端上传没有发生。 首次`command_result`帧只代表受理(以下省略通用设备和时间字段): ```json { "request_id": "demo-list-001", "command_name": "moments_list", "ok": true, "error": "", "result": { "status": "pending", "accepted": true, "write_accepted": false, "server_confirmed": false, "operation_id": "11111111-1111-4111-8111-111111111111", "request_id": "demo-request-001", "action": "list" }, "type": "command_result" } ``` `ok/error`在命令结果外层,result内会剥掉这两个字段;其他业务字段留在result。最终完成通过`event`帧到达,完整业务结果在body内,body含自己的ok/error。媒体上传最长可能约300秒,首次命令结果的duration_ms不包含整个异步操作耗时。 ```json { "event_name": "moments_result", "event_id": "msg:moments:11111111-1111-4111-8111-111111111111:1", "account_id": "wxid_self", "body": { "ok": true, "status": "succeeded", "operation_id": "11111111-1111-4111-8111-111111111111", "request_id": "demo-request-001", "action": "list", "accepted": true, "write_accepted": false, "server_confirmed": true, "revision": 1, "error": "", "items": [], "next_cursor": "12345678901234567890", "has_more": null, "snapshot_complete": true, "snapshot_source": "network", "missing_ids": [], "raw_count": 1, "truncated": false }, "type": "event" } ``` 命令结果和完成事件可能先后交错;按account_id、body.operation_id关联,用revision合并较新状态,按event_id去重,不按到达顺序猜测。收到结果帧或完成事件本身不能替代业务成功判断,须检查操作终态。 ### 朋友圈数据怎么继续使用 | 取到的字段 | 含义 | 下一步 | |---|---|---| | operation_id | 本次异步操作UUID | 用operation查询,不能传给detail | | items[].sns_id/item.sns_id | 服务器动态ID | detail、点赞、评论、删除、单帖权限 | | next_cursor | 下一页边界 | 同一时间线/同一用户下一次请求的cursor | | media[].mid | 本帖媒体标识 | media/download的media_id;不是URL或数组下标 | | comments[].comment_id/comment64_id | 本帖评论标识 | 回复时传reply_comment_id,删除时传comment_id | | resource_id | 你的后端分配的上传资源标识 | 查询后端存储或生成后端读取地址 | | media_url | 仅本地模式的手机临时文件地址 | 拼接手机HTTP基址GET,远程上传模式不依赖它 | 提交的动态ID、评论ID和cursor必须是非零UInt64十进制字符串,不接受JSON数字或溢出。输出sns_id和游标为字符串;评论ID沿用微信原生类型,可能为数字或字符串,后端需要无损解析大整数再转字符串。不要让64位ID经过JavaScript Number;网页demo已有无损保护。media_id保持mid原值,不要求它是十进制数。所有时间戳字段按字段名区分秒(create_time)与毫秒(*_ms)。 动态对象字段包括sns_id、wxid、create_time、content、type、is_mine、liked、like_count、comment_count、private、visibility_type、media、likes、comments。type和visibility_type保留微信原始值,不要把它们直接当发布接口的type/visibility枚举。普通图片动态type通常1,普通视频通常15。 媒体包含mid、type、subType、totalSize、videoDuration、width、height、preview_urls及原生可用的dataUrl/hdUrl/uhdUrl/lowBandUrl/attachUrl/attachThumbUrl。某字段不存在表示原生未提供。CDN地址不保证电脑直接可用;HTTP200也可能是占位图。列表不自动下载整页资源。 评论包含comment_id、comment64_id、wxid、nickname、content、create_time、is_mine及可用的reply_comment_id/reply_comment64_id/reply_wxid;likes沿用类似摘要结构。每条动态媒体最多32项、赞与评论各200项,media_truncated/interactions_truncated为true表示已截断;接口没有继续分页获取全部评论的入口。 ### 历史和快照的正确判断 direction默认head。显式head不能带cursor;传cursor且省略direction时按tail;tail必须带上一页next_cursor,不能传0,不能自行减1,也不接受自定义page_size。页大小由微信控制。 has_more=false才表示原始历史网络空页已确认到头;has_more=null表示暂不确定,有next_cursor可以继续。items不足40条、筛选后空页都不等于到头。has_more不明确但next_cursor=null时,停止本轮继续翻页,不要编造游标;可以保留结果后稍后重新从最新页核实。 snapshot_complete表示本页是否补齐,不是全量历史完成。snapshot_source=network/network_merged_cache区分网络完整对象与网络/缓存合并。missing_ids列出无法补齐的动态,可查detail。unchanged=true也要检查完整性。单页最多200条,truncated表示原始页超出保护上限。 好友时间线是微信当前账号按权限筛选后的信息流,不保证是所有好友相册并集或无限历史。用户相册的 visible_period 使用明确原生字段或三天/半年提示;提示为空仍为 unknown。空页、缓存计数或 ret_tips 均不足以证明被屏蔽;可用 check_visibility 获取首屏诊断。后端自行保存、去重、比较历史;插件不自动定时刷新。 ### 远程后端:手机主动上传媒体 推荐远程接入使用这一流程:后端先分配resource_id和上传地址 → 下发media/download并附upload → 手机调用微信下载、解密或复用缓存 → 手机主动HTTP上传 → 接收moments_result → 后端按resource_id取自己保存的文件。全程无需后端访问手机HTTP,也不会把文件Base64塞进长轮询/WS。 upload字段: | 字段 | 必填/默认 | 使用说明 | |---|---|---| | url | 必填 | 手机可达的HTTP(S)上传完整地址;可含签名查询参数,不允许URL账号密码、片段或重定向 | | resource_id | 必填 | 后端自分配的非空标识,最多128个UTF-16单位;用于关联和核实重复上传 | | method | PUT | 可选PUT或POST;请求正文是原始文件二进制,不是JSON、Base64或multipart | | headers | 空对象 | 可传Authorization或签名所需头;最多16项、单值4096字符、合计16KiB,不接收控制字符或重复大小写头名 | 插件自动设置Content-Type=image/jpeg或video/mp4、Content-Length。不能覆盖这两个头、Host、Transfer-Encoding、Connection、Cookie、Proxy-Authorization。使用签名上传时,后端需按对应文件类型签名;resource_id不会自动变成HTTP头,后端自行在上传路径或自己的签名头中关联。 后端接收完整文件并成功存储后才返回2xx;推荐201或204。插件不解析上传响应正文,因此响应正文控制在64KiB内;文件访问URL由你的后端通过resource_id自行生成。返回2xx且上传字节完整后才upload_completed=true;30x不跟随,4xx/5xx失败,不进行插件层自动重传。 准备阶段最多120秒;开始上传后另有180秒总上限(请求等待上限90秒),一次上传模式操作最多约300秒。上传过程按需读文件,不整体加载视频。上传中断、超时或成功状态但缺完整传输确认时upload_outcome_unknown=true:后端可能已经存储,先按resource_id核实,再决定是否重传;接收端应对同一resource_id幂等保存。重传由后端显式发起,插件不提供断点续传。 上传URL与鉴权头只用于当前任务,不进入结果、事件或完成后的操作记录。切号、微信退出或进程重启可能中止任务;这不是跨进程持久后台上传。上传模式成功后没有手机media_url,也没有后端文件的expires_at_ms;保留期限由你的后端决定。微信原生缓存可复用;插件生成的JPEG在上传终态清理,不删除微信视频缓存。 已有零依赖示例位于demo/朋友圈媒体上传接收端/server.py:PUT /uploads/{小写UUID}接收文件,要求Authorization: Bearer ;GET /files/{同UUID}读取临时文件,支持Range。后端用UUID同时作为resource_id和路径即可。示例保留10分钟、最多16个资源、重启清空;GET链接含不可猜测UUID,知道链接即可读取,生产环境应按自己的账号鉴权和保留策略实现。 网页demo/朋友圈网页工作台可展开“手机主动上传媒体”,填写已部署接收端地址和Token;随后每次点击媒体下载会生成独立UUID并上传,网页从接收端读取。手机不能访问电脑的127.0.0.1,需要使用手机可达的接收端地址。网页仍用HTTP测试业务;长轮询/WS业务后端直接使用本节命令参数即可。 ### 发布、权限与排错边界 发布visibility=public(公开)、private(仅自己)、include(仅指定用户)、exclude(不给指定用户看)。include/exclude须带非空wxids,最多200项;public/private不能带非空名单。新发布和修改已有本人动态均支持这四种范围;不临时修改微信UI全局发帖设置。 | 资源/场景 | 当前保护限制 | |---|---| | 发布正文 | 微信动态text_limit及原生加权计数校验;接口输入另限32768个UTF-16单位,不等于固定可发32768字 | | 发布图片 | 1~9张;单张输入20MiB,处理后JPEG合计20MiB,最长边2048 | | 发布视频 | 输入100MiB;时长读当前微信配置video_duration_limit_seconds;检查有效视频轨道、时长、可播放性;thumb省略自动取封面 | | 下载已发布媒体 | 单文件120MiB,最多2个并发准备;图片转最长边2048 JPEG,不保证原图字节;视频保留MP4,不转码 | | 视频准备探针 | 每账号最多1项,不占发布锁;视频100MiB,显式封面20MiB,不创建发布任务 | | 评论 | 非空,接口最多4096个UTF-16单位;微信服务器仍可能拒绝 | 这些MiB是插件保护上限,不是微信通用服务器限额。视频时长、正文配置以当前账号探针为准,不能固定写成30秒或2000字。媒体下载成功也不保证所有电脑浏览器支持该视频编码。 好友权限friend查询为本地hide_from/dont_see;recent查询为本人period。修改recent支持three_days/one_month/half_year/all;查看别人的可见期限使用user。post查询是联网详情,返回item、visible_users、excluded_users;visibility_type是原生字段,修改参数仍使用字符串visibility。只允许修改本人动态。 权限写入的server_confirmed=true而local_verified=false表示服务器已成功、本地读回异常,local_update_error给出原因,不应因此再写一次。dont_see完整服务器确认缺失会保留unknown。不要用“本地开关已变化”代替服务器确认。 视频发布前下载支持无文件后缀URL,结合文件头/响应文件名处理,并异步等待AVFoundation元数据。video_diagnostics.stage为download/metadata/thumbnail/ready;保留file_bytes、extension、duration_seconds、video_track_count、playable、thumb_ready等已取得信息。下载及可选thumb_download含http_status、received_bytes、expected_bytes(-1未知)、elapsed_ms、phase及底层error_domain/error_code;系统提供时还有DNS/连接/响应/传输metrics。准备失败不发布。下载/元数据/封面问题可先使用probe检查,不能把probe成功当发布成功。 评论错误保留raw_ret、server_error及comment_diagnostics;引用不存在、本人评论或无权限对象会明确拒绝。点赞、评论、删除和修改已有单帖都先联网详情;一个帖子的处理中任务可能阻止同帖新请求,按原operation_id等结果。 验证范围:读取、发布互动、单帖权限及本地媒体预览已有 HTTP 真机验收;用户现已确认朋友圈相关功能(含主动上传)在微信 `8.0.80` 验证通过。实现标识为 `moments_20261016_upload_v1`。 ### 接口/命令完整用法 下列每项包含请求字段、可直接使用的场景示例、结果含义与下一步。重复字段含义以本章前面的表格为准;所有示例响应均为虚构,省略部分可选字段。 #### 好友最新时间线与历史:`moments_list` 每次请求一页当前账号能看到的好友时间线。默认最新页;提供cursor且省略direction时按历史页处理。 | 参数 | 类型 | 要求 | 含义 | |---|---|---|---| | `direction` | string | 可选 | `head` 最新页;`tail` 历史页。省略时,有 `cursor` 按 `tail`,否则按 `head`;显式 `head` 不能带 `cursor`。 | | `cursor` | string | `direction=tail` 时必填 | 上一页返回的 `next_cursor`,原样传入,不自行减 1。非零 UInt64 十进制字符串,最大 18446744073709551615;不能提交 JSON 数字。 | | `request_id` | string | 可选 | 可选原任务关联标识,最多128个UTF-16单位;命令通道取外层request_id。不是持久幂等键。 | **最新页** ```json { "type": "command", "request_id": "demo-list-001", "command_name": "moments_list", "args": {} } ``` **历史下一页** ```json { "type": "command", "request_id": "demo-list-002", "command_name": "moments_list", "args": { "direction": "tail", "cursor": "12345678901234567890" } } ``` 结果字段示例(等待终态后): ```json { "ok": true, "status": "succeeded", "operation_id": "11111111-1111-4111-8111-111111111111", "request_id": "demo-request-001", "action": "list", "accepted": true, "write_accepted": false, "server_confirmed": true, "revision": 1, "error": "", "items": [ { "sns_id": "12345678901234567890", "wxid": "wxid_friend", "create_time": 1775000000, "content": "示例动态", "type": 1, "is_mine": false, "liked": false, "like_count": 0, "comment_count": 1, "private": false, "visibility_type": 1, "media": [ { "mid": "example-media-id", "type": 2, "width": 1080, "height": 1440, "preview_urls": [] } ], "likes": [], "comments": [ { "comment_id": 67, "comment64_id": "670000000000000001", "wxid": "wxid_friend", "content": "示例评论", "is_mine": false } ], "media_truncated": false, "interactions_truncated": false } ], "next_cursor": "12345678901234567890", "has_more": null, "snapshot_complete": true, "snapshot_source": "network", "missing_ids": [], "raw_count": 1, "truncated": false } ``` **下一步:** 取items[].sns_id可查详情或互动;取next_cursor请求下一页;取media[].mid准备媒体。 #### 指定用户相册与可见期限:`moments_user` 传 wxid 获取本人或好友相册。`visible_period` 优先读取原生期限字段;字段不足时仅从明确的三天/半年提示补充,空提示仍为 `unknown`。`visibility_status` 是本次响应的可见性判断,不能据此确认被屏蔽、拉黑或删除。 | 参数 | 类型 | 要求 | 含义 | |---|---|---|---| | `wxid` | string | 必填 | 用户 wxid,不能是 @chatroom 群 ID。 | | `direction` | string | 可选 | `head` 最新页;`tail` 历史页。省略时,有 `cursor` 按 `tail`,否则按 `head`;显式 `head` 不能带 `cursor`。 | | `cursor` | string | `direction=tail` 时必填 | 上一页返回的 `next_cursor`,原样传入,不自行减 1。非零 UInt64 十进制字符串,最大 18446744073709551615;不能提交 JSON 数字。 | | `check_visibility` | boolean | 可选 | 默认 false;仅指定用户相册的 head 请求可传。true 时清空本次请求的首屏 MD5,尝试获取完整首屏用于诊断,不保证服务器一定返回完整数据;不支持 timeline 或 tail。 | | `request_id` | string | 可选 | 可选原任务关联标识,最多128个UTF-16单位;命令通道取外层request_id。不是持久幂等键。 | **可见性检查(命令 `args`)** ```json {"wxid":"wxid_xxx","check_visibility":true} ``` 普通请求继续使用微信的 MD5 增量策略。诊断仍复用同一个相册入口;刷新接口在 `scope=user`、`direction=head` 时也支持 `check_visibility`。按原 operation_id 等待终态,`items=[]` 本身不能证明空相册或被屏蔽。 **用户最新页** ```json { "type": "command", "request_id": "demo-user-001", "command_name": "moments_user", "args": { "wxid": "wxid_xxx" } } ``` **用户历史页** ```json { "type": "command", "request_id": "demo-user-002", "command_name": "moments_user", "args": { "wxid": "wxid_xxx", "cursor": "12345678901234567890" } } ``` 结果字段示例(等待终态后): ```json { "ok": true, "status": "succeeded", "operation_id": "11111111-1111-4111-8111-111111111111", "request_id": "demo-request-001", "action": "user", "accepted": true, "write_accepted": false, "server_confirmed": true, "revision": 1, "error": "", "items": [ { "sns_id": "12345678901234567890", "wxid": "wxid_friend", "create_time": 1775000000, "content": "示例动态", "type": 1, "is_mine": false, "liked": false, "like_count": 0, "comment_count": 1, "private": false, "visibility_type": 1, "media": [ { "mid": "example-media-id", "type": 2, "width": 1080, "height": 1440, "preview_urls": [] } ], "likes": [], "comments": [ { "comment_id": 67, "comment64_id": "670000000000000001", "wxid": "wxid_friend", "content": "示例评论", "is_mine": false } ], "media_truncated": false, "interactions_truncated": false } ], "next_cursor": "12345678901234567890", "has_more": null, "snapshot_complete": true, "snapshot_source": "network", "missing_ids": [], "raw_count": 1, "truncated": false, "visible_period": "all", "visible_period_source": "sns_user_info", "visibility_status": "visible", "check_visibility": false, "request_md5_empty": true, "top_raw_count": 0, "same_md5_count": 0, "raw_ret": 0, "privacy_flags_raw": 4096, "privacy_recent_raw": 4294967295, "ret_tips": "", "limited_id": "0", "continue_id": "0" } ``` **可见性诊断字段** | 字段 | 类型 | 含义 | |---|---|---| | `visibility_status` | string | `visible`:本次响应含有效 ID 且非 noChange 的普通或置顶对象;`empty_or_restricted`:新首屏确认无内容,但无法区分从未发布、无权查看或非好友;`unknown`:证据不足。 | | `visible_period_source` | string | `sns_user_info` 原生期限字段;`ret_tips` 明确提示补充;`unknown` 未确定。 | | `check_visibility` | boolean | 本次是否请求清空首屏 MD5。 | | `request_md5_empty` | boolean / null | 实际请求 MD5 是否为空;不可读取时为 null,不能仅凭 check_visibility 判断请求内容。 | | `raw_count` | integer / null | 普通 objectList 的原始条数;不可读取时为 null,可能与 items 长度不同。 | | `top_raw_count` | integer / null | 置顶 topListObjectList 的原始条数;置顶对象目前仅用于诊断,不新增到 items。 | | `same_md5_count` | integer / null | objectCountForSameMd5 原始计数;大于零仅说明同 MD5 计数,不能单独证明当前可见。 | | `privacy_recent_raw` / `privacy_flags_raw` | integer / null | 原生期限小时数和标志位;不可读取时为 null。 | | `continue_id` / `limited_id` | string / null | 原生 UInt64 继续/限制边界;不可读取时为 null,翻页仍用 next_cursor。 | 只有原生返回码为 0 或 207、head 请求实际 MD5 为空、普通和置顶条数及同 MD5 计数均明确为零、continue_id 明确为 "0" 时,才报告 `empty_or_restricted`。同 MD5 未变化、仅有缓存计数、字段缺失或首屏未完成时不能据空数组下结论。 操作状态保持兼容:非零 `raw_ret` 仍返回 `status=failed`、`ok=false`、`server_confirmed=false`,同时保留取得的诊断字段;`visibility_status` 独立描述响应证据。没有原生响应的网络失败中,`visibility_status` 保持 `unknown`,不输出确定的 blocked 标志。受理结果及失败回调可能只有未知值,必须等待原 operation_id 的终态或对应 moments_result。 **下一步:** 继续历史时保留同一个wxid并使用本页next_cursor;历史页visible_period=unknown不能覆盖此前已确认的期限。 #### 联网获取动态详情:`moments_detail` 根据列表、发布结果或已知sns_id联网查询,返回item。媒体、评论及权限信息仍受当前账号服务器可见范围限制。 | 参数 | 类型 | 要求 | 含义 | |---|---|---|---| | `sns_id` | string | 必填 | 目标动态的 `sns_id`,取自列表、详情或发布结果。非零 UInt64 十进制字符串,最大 18446744073709551615;不能提交 JSON 数字。 | | `request_id` | string | 可选 | 可选原任务关联标识,最多128个UTF-16单位;命令通道取外层request_id。不是持久幂等键。 | **动态详情** ```json { "type": "command", "request_id": "demo-detail-001", "command_name": "moments_detail", "args": { "sns_id": "12345678901234567890" } } ``` 结果字段示例(等待终态后): ```json { "ok": true, "status": "succeeded", "operation_id": "11111111-1111-4111-8111-111111111111", "request_id": "demo-request-001", "action": "detail", "accepted": true, "write_accepted": false, "server_confirmed": true, "revision": 1, "error": "", "item": { "sns_id": "12345678901234567890", "wxid": "wxid_friend", "create_time": 1775000000, "content": "示例动态", "type": 1, "is_mine": false, "liked": false, "like_count": 0, "comment_count": 1, "private": false, "visibility_type": 1, "media": [ { "mid": "example-media-id", "type": 2, "width": 1080, "height": 1440, "preview_urls": [] } ], "likes": [], "comments": [ { "comment_id": 67, "comment64_id": "670000000000000001", "wxid": "wxid_friend", "content": "示例评论", "is_mine": false } ], "media_truncated": false, "interactions_truncated": false } } ``` **下一步:** 点赞、评论使用item.sns_id;回复取本帖comments中的comment_id或comment64_id;媒体下载取本帖media中的mid。 #### 服务端触发刷新并接收结果:`moments_refresh` scope省略时为timeline;scope=user时必须传wxid。刷新获取本次页面快照并产生moments_result,不启动定时任务,也不自动计算全局增删差异。支持与list/user相同的历史参数。 | 参数 | 类型 | 要求 | 含义 | |---|---|---|---| | `scope` | string | 可选 | timeline 当前账号好友时间线,user 指定用户相册。;枚举:timeline, user;默认:timeline | | `wxid` | string | `scope=user` 时必填 | 目标用户的 wxid,不能是群 ID;`scope=timeline` 无需传入。 | | `direction` | string | 可选 | `head` 最新页;`tail` 历史页。省略时,有 `cursor` 按 `tail`,否则按 `head`;显式 `head` 不能带 `cursor`。 | | `cursor` | string | `direction=tail` 时必填 | 上一页返回的 `next_cursor`,原样传入,不自行减 1。非零 UInt64 十进制字符串,最大 18446744073709551615;不能提交 JSON 数字。 | | `check_visibility` | boolean | 可选 | 默认 false;仅指定用户相册的 head 请求可传。true 时清空本次请求的首屏 MD5,尝试获取完整首屏用于诊断,不保证服务器一定返回完整数据;不支持 timeline 或 tail。 | | `request_id` | string | 可选 | 可选原任务关联标识,最多128个UTF-16单位;命令通道取外层request_id。不是持久幂等键。 | **刷新好友最新页** ```json { "type": "command", "request_id": "demo-refresh-001", "command_name": "moments_refresh", "args": { "scope": "timeline" } } ``` **刷新用户最新页** ```json { "type": "command", "request_id": "demo-refresh-002", "command_name": "moments_refresh", "args": { "scope": "user", "wxid": "wxid_xxx" } } ``` 结果字段示例(等待终态后): ```json { "ok": true, "status": "succeeded", "operation_id": "11111111-1111-4111-8111-111111111111", "request_id": "demo-request-001", "action": "refresh", "accepted": true, "write_accepted": false, "server_confirmed": true, "revision": 1, "error": "", "scope": "timeline", "direction": "head", "items": [ { "sns_id": "12345678901234567890", "wxid": "wxid_friend", "create_time": 1775000000, "content": "示例动态", "type": 1, "is_mine": false, "liked": false, "like_count": 0, "comment_count": 1, "private": false, "visibility_type": 1, "media": [ { "mid": "example-media-id", "type": 2, "width": 1080, "height": 1440, "preview_urls": [] } ], "likes": [], "comments": [ { "comment_id": 67, "comment64_id": "670000000000000001", "wxid": "wxid_friend", "content": "示例评论", "is_mine": false } ], "media_truncated": false, "interactions_truncated": false } ], "next_cursor": "12345678901234567890", "has_more": null, "snapshot_complete": true, "snapshot_source": "network", "missing_ids": [], "raw_count": 1, "truncated": false } ``` **下一步:** 按operation_id关联本次页面;后端自行按sns_id去重、保存历史和比较变化。持续监听事件不会主动刷新。 #### 点赞:`moments_like` 先联网查详情,再提交点赞并等待原生确认。已经点赞返回unchanged=true。需启用插件并保持有效写入状态。 | 参数 | 类型 | 要求 | 含义 | |---|---|---|---| | `sns_id` | string | 必填 | 目标动态的 `sns_id`,取自列表、详情或发布结果。非零 UInt64 十进制字符串,最大 18446744073709551615;不能提交 JSON 数字。 | | `request_id` | string | 可选 | 可选原任务关联标识,最多128个UTF-16单位;命令通道取外层request_id。不是持久幂等键。 | **点赞** ```json { "type": "command", "request_id": "demo-like-001", "command_name": "moments_like", "args": { "sns_id": "12345678901234567890" } } ``` 结果字段示例(等待终态后): ```json { "ok": true, "status": "succeeded", "operation_id": "11111111-1111-4111-8111-111111111111", "request_id": "demo-request-001", "action": "like", "accepted": true, "write_accepted": true, "server_confirmed": true, "revision": 1, "error": "", "sns_id": "12345678901234567890" } ``` **下一步:** 最终成功后可重新查详情显示最新赞和评论;不要因第一次pending再提交一次点赞。 #### 取消本人点赞:`moments_unlike` 只取消当前账号的点赞;原本未点赞返回unchanged=true。需启用插件并保持有效写入状态。 | 参数 | 类型 | 要求 | 含义 | |---|---|---|---| | `sns_id` | string | 必填 | 目标动态的 `sns_id`,取自列表、详情或发布结果。非零 UInt64 十进制字符串,最大 18446744073709551615;不能提交 JSON 数字。 | | `request_id` | string | 可选 | 可选原任务关联标识,最多128个UTF-16单位;命令通道取外层request_id。不是持久幂等键。 | **取消点赞** ```json { "type": "command", "request_id": "demo-unlike-001", "command_name": "moments_unlike", "args": { "sns_id": "12345678901234567890" } } ``` 结果字段示例(等待终态后): ```json { "ok": true, "status": "succeeded", "operation_id": "11111111-1111-4111-8111-111111111111", "request_id": "demo-request-001", "action": "unlike", "accepted": true, "write_accepted": false, "server_confirmed": true, "revision": 1, "error": "", "sns_id": "12345678901234567890", "unchanged": true } ``` **下一步:** 最终成功后可查详情更新显示。unchanged=true表示无需再次写入。 #### 评论与回复他人评论:`moments_comment` 普通评论只传sns_id/content;回复增加本帖可见评论的reply_comment_id,可匹配comment_id或comment64_id。当前微信原生实测不能回复本人评论,提交前明确拒绝,不降级为普通评论。content非空,接口最多4096个UTF-16单位,这不是已验证的微信服务器字数上限。 | 参数 | 类型 | 要求 | 含义 | |---|---|---|---| | `sns_id` | string | 必填 | 目标动态的 `sns_id`,取自列表、详情或发布结果。非零 UInt64 十进制字符串,最大 18446744073709551615;不能提交 JSON 数字。 | | `content` | string | 必填 | 非空评论,插件输入上限4096个UTF-16单位;不是已验证微信服务器字数上限。 | | `reply_comment_id` | string | 回复他人评论时必填 | 本帖可见评论的 `comment_id` 或 `comment64_id`,无损转成字符串;普通评论省略。非零 UInt64 十进制字符串,最大 18446744073709551615;不能提交 JSON 数字。 | | `request_id` | string | 可选 | 可选原任务关联标识,最多128个UTF-16单位;命令通道取外层request_id。不是持久幂等键。 | **评论** ```json { "type": "command", "request_id": "demo-comment-001", "command_name": "moments_comment", "args": { "sns_id": "12345678901234567890", "content": "测试评论" } } ``` **回复评论** ```json { "type": "command", "request_id": "demo-comment-002", "command_name": "moments_comment", "args": { "sns_id": "12345678901234567890", "content": "测试回复", "reply_comment_id": "12345" } } ``` 结果字段示例(等待终态后): ```json { "ok": true, "status": "succeeded", "operation_id": "11111111-1111-4111-8111-111111111111", "request_id": "demo-request-001", "action": "comment", "accepted": true, "write_accepted": true, "server_confirmed": true, "revision": 1, "error": "", "sns_id": "12345678901234567890", "comment_id": "", "raw_ret": 0 } ``` **下一步:** 最终成功后重新查detail取得评论列表和可用于后续删除/回复的评论ID;即使成功结果comment_id为空,也不要重发。 #### 删除评论:`moments_delete_comment` 允许删除本人评论,或本人动态下的可见评论;不能删除他人动态中他人的评论。comment_id可使用本帖comment_id或comment64_id。需启用插件并保持有效写入状态。 | 参数 | 类型 | 要求 | 含义 | |---|---|---|---| | `sns_id` | string | 必填 | 目标动态的 `sns_id`,取自列表、详情或发布结果。非零 UInt64 十进制字符串,最大 18446744073709551615;不能提交 JSON 数字。 | | `comment_id` | string | 必填 | 本帖待删除评论的 `comment_id` 或 `comment64_id`,无损转成字符串。非零 UInt64 十进制字符串,最大 18446744073709551615;不能提交 JSON 数字。 | | `request_id` | string | 可选 | 可选原任务关联标识,最多128个UTF-16单位;命令通道取外层request_id。不是持久幂等键。 | **删除本人评论** ```json { "type": "command", "request_id": "demo-delete_comment-001", "command_name": "moments_delete_comment", "args": { "sns_id": "12345678901234567890", "comment_id": "12345" } } ``` 结果字段示例(等待终态后): ```json { "ok": true, "status": "succeeded", "operation_id": "11111111-1111-4111-8111-111111111111", "request_id": "demo-request-001", "action": "delete_comment", "accepted": true, "write_accepted": true, "server_confirmed": true, "revision": 1, "error": "", "sns_id": "12345678901234567890" } ``` **下一步:** 最终成功后查detail核对列表。若原评论已不在可见列表,会明确失败,不推断为已经删除成功。 #### 发布文字、图文或视频:`moments_publish` type=text/images/video,默认text;visibility默认public。媒体来源为手机可访问的HTTP(S)地址或手机绝对路径,不能传电脑C盘路径。下载、解码、封面完成后才提交微信发布。需启用插件并保持有效写入状态。 | 参数 | 类型 | 要求 | 含义 | |---|---|---|---| | `type` | string | 可选,默认 `text` | `text` 纯文字、`images` 图文、`video` 视频。 | | `content` | string | `type=text` 时必填且非空 | 图文/视频可省略。按微信当前动态配置和原生加权计数校验,接口最多 32768 个 UTF-16 单位。 | | `visibility` | string | 可选 | public 公开、private 仅自己、include 指定人可见、exclude 不给指定人看。;枚举:public, private, include, exclude;默认:public | | `wxids` | array | include/exclude时必填 | include/exclude 必须非空,其他可见范围须为空或省略;重复 wxid 去重。 | | `images` | array | `type=images` 时必填 | 1~9 个手机本地绝对路径或 HTTP(S) 图片 URL。 | | `video` | string | `type=video` 时必填 | 手机视频绝对路径或 HTTP(S) URL。 | | `thumb` | string | 可选 | 可选封面手机绝对路径或HTTP(S) URL;省略则从视频生成。 | | `request_id` | string | 可选 | 可选原任务关联标识,最多128个UTF-16单位;命令通道取外层request_id。不是持久幂等键。 | **文字(仅自己)** ```json { "type": "command", "request_id": "demo-publish-001", "command_name": "moments_publish", "args": { "type": "text", "content": "朋友圈测试", "visibility": "private" } } ``` **图文(仅自己)** ```json { "type": "command", "request_id": "demo-publish-002", "command_name": "moments_publish", "args": { "type": "images", "content": "图文测试", "images": [ "https://example.com/image.jpg" ], "visibility": "private" } } ``` **视频(仅自己)** ```json { "type": "command", "request_id": "demo-publish-003", "command_name": "moments_publish", "args": { "type": "video", "content": "视频测试", "video": "https://example.com/video.mp4", "visibility": "private" } } ``` **指定人可见** ```json { "type": "command", "request_id": "demo-publish-004", "command_name": "moments_publish", "args": { "type": "text", "content": "指定可见测试", "visibility": "include", "wxids": [ "wxid_xxx" ] } } ``` **不给指定人看** ```json { "type": "command", "request_id": "demo-publish-005", "command_name": "moments_publish", "args": { "type": "text", "content": "示例", "visibility": "exclude", "wxids": [ "wxid_friend" ] } } ``` 结果字段示例(等待终态后): ```json { "ok": true, "status": "succeeded", "operation_id": "11111111-1111-4111-8111-111111111111", "request_id": "demo-request-001", "action": "publish", "accepted": true, "write_accepted": true, "server_confirmed": true, "revision": 1, "error": "", "sns_id": "12345678901234567890", "local_sns_id": "local-example" } ``` **下一步:** 等待最终服务器确认后使用sns_id查详情、修改权限或删除。local_sns_id只是本地编号,不能代替服务器sns_id。 #### 删除本人动态:`moments_delete` 联网确认作者为当前账号后删除,仅支持本人已发布动态。需启用插件并保持有效写入状态。 | 参数 | 类型 | 要求 | 含义 | |---|---|---|---| | `sns_id` | string | 必填 | 目标动态的 `sns_id`,取自列表、详情或发布结果。非零 UInt64 十进制字符串,最大 18446744073709551615;不能提交 JSON 数字。 | | `request_id` | string | 可选 | 可选原任务关联标识,最多128个UTF-16单位;命令通道取外层request_id。不是持久幂等键。 | **删除本人动态** ```json { "type": "command", "request_id": "demo-delete-001", "command_name": "moments_delete", "args": { "sns_id": "12345678901234567890" } } ``` 结果字段示例(等待终态后): ```json { "ok": true, "status": "succeeded", "operation_id": "11111111-1111-4111-8111-111111111111", "request_id": "demo-request-001", "action": "delete", "accepted": true, "write_accepted": true, "server_confirmed": true, "revision": 1, "error": "", "sns_id": "12345678901234567890" } ``` **下一步:** 等待服务器确认;结果未知时保留原操作核实,不能只因界面消失就重新删除。 #### 查询好友、期限、单帖权限:`moments_permissions_query` type=friend/recent同步读取本地设置,不产生operation_id和完成事件;server_confirmed=false正常。friend中dont_see=null表示原生分组尚未加载,不能当false。type=post会联网查询并返回操作编号。 | 参数 | 类型 | 要求 | 含义 | |---|---|---|---| | `type` | string | 必填,无默认值 | `friend` 好友权限、`recent` 本人可见期限、`post` 单条动态可见范围。 | | `wxid` | string | `type=friend` 时必填 | 目标用户的 wxid,不能是群 ID;`type=recent/post` 无需传入。 | | `sns_id` | string | `type=post` 时必填 | 目标动态的 `sns_id`;`type=friend/recent` 无需传入。非零 UInt64 十进制字符串,最大 18446744073709551615;不能提交 JSON 数字。 | | `request_id` | string | 可选 | 可选原任务关联标识,最多128个UTF-16单位;命令通道取外层request_id。不是持久幂等键。 | **好友权限** ```json { "type": "command", "request_id": "demo-permissions-query-001", "command_name": "moments_permissions_query", "args": { "type": "friend", "wxid": "wxid_xxx" } } ``` **本人可见期限** ```json { "type": "command", "request_id": "demo-permissions-query-002", "command_name": "moments_permissions_query", "args": { "type": "recent" } } ``` **单帖可见范围** ```json { "type": "command", "request_id": "demo-permissions-query-003", "command_name": "moments_permissions_query", "args": { "type": "post", "sns_id": "12345678901234567890" } } ``` 结果字段示例(同步返回): ```json { "ok": true, "type": "friend", "wxid": "wxid_friend", "source": "local_settings", "server_confirmed": false, "hide_from": false, "dont_see": null } ``` recent同步返回`{ok:true,type:"recent",source:"local_settings",server_confirmed:false,hours:72,period:"three_days"}`;post须等待终态,返回item及visible_users/excluded_users。以上三种场景不能统一假设都有operation_id。 **下一步:** 修改好友权限用type=friend;改本人对外期限用type=recent;改已发布本人动态用type=post。查看对方期限则使用moments_user。 #### 修改好友、期限、单帖权限:`moments_permissions_update` friend的hide_from(不让他看我)与dont_see(我不看他)为独立开关;enabled必须是真正JSON布尔值,false解除。recent修改本人可见期限;post只修改本人动态的范围。需启用插件并保持有效写入状态。 | 参数 | 类型 | 要求 | 含义 | |---|---|---|---| | `type` | string | 必填,无默认值 | `friend` 好友权限、`recent` 本人可见期限、`post` 单条动态可见范围。 | | `wxid` | string | `type=friend` 时必填 | 目标用户的 wxid,不能是群 ID;`type=recent/post` 无需传入。 | | `direction` | string | `type=friend` 时必填,无默认值 | `hide_from` 不让他看我;`dont_see` 我不看他。 | | `enabled` | boolean | 修改好友权限时必填 | 严格 JSON 布尔值;true 开启,false 解除。 | | `period` | string | 修改本人期限时必填 | 修改本人期限时必填;枚举:three_days, one_month, half_year, all | | `sns_id` | string | `type=post` 时必填 | 本人动态的 `sns_id`;`type=friend/recent` 无需传入。非零 UInt64 十进制字符串,最大 18446744073709551615;不能提交 JSON 数字。 | | `visibility` | string | 可选 | public 公开、private 仅自己、include 指定人可见、exclude 不给指定人看。;枚举:public, private, include, exclude;默认:public | | `wxids` | array | include/exclude时必填 | include/exclude 必须非空,其他可见范围须为空或省略;重复 wxid 去重。 | | `request_id` | string | 可选 | 可选原任务关联标识,最多128个UTF-16单位;命令通道取外层request_id。不是持久幂等键。 | **不让他看我** ```json { "type": "command", "request_id": "demo-permissions-update-001", "command_name": "moments_permissions_update", "args": { "type": "friend", "wxid": "wxid_xxx", "direction": "hide_from", "enabled": true } } ``` **我不看他** ```json { "type": "command", "request_id": "demo-permissions-update-002", "command_name": "moments_permissions_update", "args": { "type": "friend", "wxid": "wxid_xxx", "direction": "dont_see", "enabled": true } } ``` **解除我不看他** ```json { "type": "command", "request_id": "demo-permissions-update-003", "command_name": "moments_permissions_update", "args": { "type": "friend", "wxid": "wxid_xxx", "direction": "dont_see", "enabled": false } } ``` **本人仅三天可见** ```json { "type": "command", "request_id": "demo-permissions-update-004", "command_name": "moments_permissions_update", "args": { "type": "recent", "period": "three_days" } } ``` **本人全部可见** ```json { "type": "command", "request_id": "demo-permissions-update-005", "command_name": "moments_permissions_update", "args": { "type": "recent", "period": "all" } } ``` **本人单帖仅自己** ```json { "type": "command", "request_id": "demo-permissions-update-006", "command_name": "moments_permissions_update", "args": { "type": "post", "sns_id": "12345678901234567890", "visibility": "private" } } ``` **本人单帖不给指定人看** ```json { "type": "command", "request_id": "demo-permissions-update-007", "command_name": "moments_permissions_update", "args": { "type": "post", "sns_id": "12345678901234567890", "visibility": "exclude", "wxids": [ "wxid_xxx" ] } } ``` 结果字段示例(等待终态后): ```json { "ok": true, "status": "succeeded", "operation_id": "11111111-1111-4111-8111-111111111111", "request_id": "demo-request-001", "action": "permissions/update", "accepted": true, "write_accepted": true, "server_confirmed": true, "revision": 1, "error": "", "type": "friend", "direction": "dont_see", "enabled": false, "local_verified": true } ``` **下一步:** 等待服务器确认后再查询对应权限。服务器已确认但local_verified=false时先核对本地同步,不要重复修改服务器。dont_see缺完整确认时可能unknown,查询原操作。 #### 查询原异步操作:`moments_operation` 传原operation_id读取当前账号内存操作记录,不创建新业务任务。返回的action/request_id仍属于原任务。查到记录时读取原操作的 `status`;`failed`/`unknown` 仍可能得到失败业务结果。不存在、跨账号或已淘汰返回失败命令结果;本模式不使用 HTTP 状态码判断查询结果。 | 参数 | 类型 | 要求 | 含义 | |---|---|---|---| | `operation_id` | string | 必填 | 上一步返回的操作UUID,查询时始终保持同一值。 | | `request_id` | string | 可选 | 可选原任务关联标识,最多128个UTF-16单位;命令通道取外层request_id。不是持久幂等键。 | **操作结果** ```json { "type": "command", "request_id": "demo-operation-001", "command_name": "moments_operation", "args": { "operation_id": "替换为实际operation_id" } } ``` 结果字段示例(等待终态后): ```json { "ok": true, "status": "succeeded", "operation_id": "11111111-1111-4111-8111-111111111111", "request_id": "demo-request-001", "action": "list", "accepted": true, "write_accepted": false, "server_confirmed": true, "revision": 1, "error": "", "items": [ { "sns_id": "12345678901234567890", "wxid": "wxid_friend", "create_time": 1775000000, "content": "示例动态", "type": 1, "is_mine": false, "liked": false, "like_count": 0, "comment_count": 1, "private": false, "visibility_type": 1, "media": [ { "mid": "example-media-id", "type": 2, "width": 1080, "height": 1440, "preview_urls": [] } ], "likes": [], "comments": [ { "comment_id": 67, "comment64_id": "670000000000000001", "wxid": "wxid_friend", "content": "示例评论", "is_mine": false } ], "media_truncated": false, "interactions_truncated": false } ], "next_cursor": "12345678901234567890", "has_more": null, "snapshot_complete": true, "snapshot_source": "network", "missing_ids": [], "raw_count": 1, "truncated": false } ``` **下一步:** preparing/pending继续查询同一编号;succeeded读取业务结果;failed看error;unknown尤其write_accepted=true时禁止自动重新写入。 #### 运行能力与视频准备诊断:`moments_probe` {}同步读取实现标识、hooks、配置和operations,不刷新、不下载、不发布。传video及可选thumb则异步检查完整下载、视频元数据与封面,仍不发布,成功后清理本次临时资源。 | 参数 | 类型 | 要求 | 含义 | |---|---|---|---| | `request_id` | string | 可选 | 可选原任务关联标识,最多128个UTF-16单位;命令通道取外层request_id。不是持久幂等键。 | | `video` | string | 可选 | 传入后异步检查视频,不发帖;省略则同步返回能力和状态快照。 | | `thumb` | string | 可选 | 选填封面;仅与video一起使用,省略自动生成。 | **运行时能力与任务状态** ```json { "type": "command", "request_id": "demo-probe-001", "command_name": "moments_probe", "args": {} } ``` **视频下载与解码诊断(不发布)** ```json { "type": "command", "request_id": "demo-probe-002", "command_name": "moments_probe", "args": { "video": "https://example.com/video.mp4" } } ``` 结果字段示例(同步返回): ```json { "ok": true, "implementation_revision": "moments_20261016_upload_v1", "media_upload_supported": true, "text_limit": 2000, "video_duration_limit_seconds": 300, "operations": [] } ``` 传video时为异步操作,最终`status=succeeded, probe_only=true, media_ready=true, accepted=false, write_accepted=false, server_confirmed=false`。探针中的text_limit和video_duration_limit_seconds只是示例数值,实际以当前配置为准。 **下一步:** 视频准备探针通过后,如需发布,显式调用publish;探针没有创建可供media/file读取的朋友圈资源。 #### 准备媒体并可主动上传:`moments_media_download` 传sns_id和本帖media[].mid。省略upload为本地读取模式;提供upload则手机在下载、解密/复用缓存后主动上传到指定后端。只支持普通图片和视频朋友圈,不支持视频号、链接附件。 | 参数 | 类型 | 要求 | 含义 | |---|---|---|---| | `sns_id` | string | 必填 | 目标动态的 `sns_id`,取自列表、详情或发布结果。非零 UInt64 十进制字符串,最大 18446744073709551615;不能提交 JSON 数字。 | | `media_id` | string | 必填 | 本动态media[]中mid原值,不能传URL或数组下标。 | | `request_id` | string | 可选 | 可选原任务关联标识,最多128个UTF-16单位;命令通道取外层request_id。不是持久幂等键。 | | `upload` | object | 可选 | 手机出站上传目标;只有上传完成整个操作才成功。省略此对象沿用本地读取。 | **远程后端:手机主动上传** ```json { "type": "command", "request_id": "demo-media-download-001", "command_name": "moments_media_download", "args": { "sns_id": "12345678901234567890", "media_id": "example-media-id", "upload": { "url": "https://upload.example.com/uploads/22222222-2222-4222-8222-222222222222", "resource_id": "22222222-2222-4222-8222-222222222222", "method": "PUT", "headers": { "Authorization": "Bearer REPLACE_WITH_SHORT_LIVED_TOKEN" } } } } ``` **本地模式:手机临时文件** ```json { "type": "command", "request_id": "demo-media-download-002", "command_name": "moments_media_download", "args": { "sns_id": "12345678901234567890", "media_id": "example-media-id" } } ``` 上传模式最终结果(`server_confirmed=false`正常): ```json { "ok": true, "status": "succeeded", "operation_id": "11111111-1111-4111-8111-111111111111", "request_id": "demo-request-001", "action": "media/download", "accepted": true, "write_accepted": false, "server_confirmed": false, "revision": 1, "error": "", "sns_id": "12345678901234567890", "media_id": "example-media-id", "media_ready": true, "delivery": "upload", "upload_completed": true, "upload_http_status": 201, "upload_outcome_unknown": false, "resource_id": "22222222-2222-4222-8222-222222222222", "content_type": "image/jpeg", "file_bytes": 297446, "cached": true, "source": "wechat_native" } ``` 省略upload时的本地模式结果: ```json { "ok": true, "status": "succeeded", "operation_id": "11111111-1111-4111-8111-111111111111", "request_id": "demo-request-001", "action": "media/download", "accepted": true, "write_accepted": false, "server_confirmed": false, "revision": 1, "error": "", "sns_id": "12345678901234567890", "media_id": "example-media-id", "media_ready": true, "delivery": "local", "content_type": "image/jpeg", "file_bytes": 297446, "cached": true, "source": "wechat_native", "media_url": "/moments/media/file/11111111-1111-4111-8111-111111111111", "expires_at_ms": 1775000600000 } ``` **下一步:** 远程后端推荐upload模式:等upload_completed=true后,按后端自分配的resource_id读取后端文件;无需访问手机HTTP。省略upload时才使用返回的media_url。 #### 可选的本地 HTTP 文件读取:`GET /moments/media/file/{operation_id}` 只有省略upload且本地媒体准备成功后才能使用。远程后端使用主动上传,不需要访问此入口。media_url为相对路径,拼接手机HTTP基址;operation_id为这次媒体准备的UUID,不是sns_id。链接最多10分钟,切号、重启、操作表淘汰、微信清缓存可提前失效。 ```bash curl --request GET 'http://192.0.2.10:19088/moments/media/file/11111111-1111-4111-8111-111111111111' --output preview.jpg ``` 这是二进制响应,按Content-Type选择文件格式。无Range返回200整文件;Range单段返回206,支持bytes=0-65535、bytes=65536-、bytes=-65536;无效/越界/多段返回416与Content-Range: bytes */文件总长。非法UUID为400;未授权403;未就绪/过期/账号切换/缓存失效404。读取不触发下载,不支持任意手机文件路径。 ### 常用业务按这个顺序调用 | 目标 | 调用顺序 | |---|---| | 获取最新好友动态 | list → 等本次完成 → items | | 采集当前可见历史 | list最新页 → 保存并去重 → next_cursor请求下一页 → 逐页等待;不能一个pending未完成就抢同范围下一页 | | 获取指定用户历史 | user带wxid → 等结果 → 同wxid加next_cursor翻页 | | 回复评论 | detail → comments选他人评论 → comment带reply_comment_id → 等完成 → detail更新显示 | | 发布后修改范围 | publish → 等服务器sns_id → permissions/update(type=post) → 等完成 → permissions/query(type=post) | | 删除刚发布动态 | publish → 等服务器sns_id → delete → 等完成 | | 远程读取图片/视频 | list/detail取sns_id和mid → 后端准备上传目标 → media/download带upload → 等upload_completed → 从后端按resource_id读取 | | 诊断发布视频 | probe带video → 等media_ready → 需要发布时再显式publish,不会自动发帖 | --- ## 22. 收藏:分页读取、发送与删除 用户已确认收藏功能在微信 `8.0.80` 验证通过;当前响应中的插件完整版本以设备实际返回为准。历史 `0.8.79-13` 曾用于早期收藏验证,不作为当前接入的版本条件。 收藏使用本篇命令通道,无需后端访问手机 HTTP。业务结果一次等待后回传;下面的结果示例省略设备、会话、账号、版本及时间等公共字段。 收藏三项命令可执行但尚未出现在当前 `capabilities`;接入时按本篇收藏参数与结果处理,不因清单缺省而拦截。其他声明缺口见本篇能力清单。 | 功能 | 命令名 | |---|---| | 分页读取收藏 | `favorites_list` | | 发送指定收藏 | `favorites_send` | | 删除指定收藏 | `favorites_delete` | 收藏参数严格校验,不适用本文其他接口的宽松类型转换、字符串自动裁剪或目标别名规则。 `favorites_list` 的 `args` 只允许 `limit`、`refresh`、`cursor`;发送/删除的业务 `request_id` 统一取命令外层,不要把它放进 `args`。 ### `favorites_list`:最新在前的游标分页 | 参数 | 类型 | 必填 | 说明 | |---|---|---|---| | `limit` | integer | 否 | 默认 `50`,范围 `1–100`;不接受数字字符串、布尔值或非整数 | | `refresh` | boolean | 否 | 默认 `false`,读取本机已同步内容;`true` 先等待微信同步及正文补齐,再返回第一页。只允许用于首请求,`cursor` 须省略或为空字符串 | | `cursor` | string | 否 | 上一页返回的 `next_cursor`,最多 128 个字符;首请求省略或传空字符串 | **顺序为最新收藏在前、最早收藏在后**,当前实现按本机递增的 `local_id` 从大到小扫描。下一页继续向更早的 ID 读取,不要按 `fav_id` 的字符串大小或 `create_time` 自行重排后再推算游标。 分页由插件控制,底层读取微信本地收藏库;微信原生同步分批与对外分页是两层机制。首次建议 `refresh:true`,后续页只带 `limit`、`cursor`,直到 `has_more:false`。 - 单页最多返回 `limit` 条;扫描和正文预算可能使一页不足 `limit`,甚至 `items:[]` 但 `has_more:true`。是否继续以 `has_more` 为准,继续时原样使用本页 `next_cursor`。 - `has_more:true` 表示还有本地 ID 范围未扫描,不保证下一页一定有条目。结束时 `next_cursor` 为空字符串。 - 游标绑定当前账号和登录周期,最长保留 30 分钟;内存最多保存 256 个游标,满额时旧游标也可能提前失效。账号切换、微信重启或游标失效后重新从第一页获取。 - 第一页固定本轮扫描的最大本地 ID,翻页期间新收藏不插入本轮结果;要看新增项,重新请求第一页。期间删除、编辑或同步仍可能改变已有内容,这不是不可变快照。 - 不支持 `page`、`offset`、排序参数、总数查询或一次返回全部。ID 有空洞,不能用返回条数推算下一页。 服务端发送 `command` 帧: ```json { "type": "command", "request_id": "fav-list-demo-001", "command_name": "favorites_list", "args": { "limit": 50, "refresh": true } } ``` 下一页使用新的命令请求 ID,`args` 为以下内容(复制上页 `next_cursor`,不再刷新): ```json { "limit": 50, "cursor": "11111111-1111-4111-8111-111111111111" } ``` 插件回传 `command_result` 帧: ```json { "type": "command_result", "request_id": "fav-list-demo-001", "command_name": "favorites_list", "ok": true, "result": { "account_id": "wxid_self_example", "sync_state": "completed", "items": [ { "fav_id": "123456", "local_id": "801", "type": 1, "title": "", "description": "收藏的示例文本", "url": "", "create_time": 1792112400, "update_time": 1792112400, "tags": [], "content_state": "ready", "forward_state": "ready", "block_reason": "", "data_list": [], "content_truncated": false } ], "has_more": true, "next_cursor": "11111111-1111-4111-8111-111111111111" }, "error": "" } ``` 业务 `ok` 和 `error` 位于命令结果外层,`result` 中不重复这两个字段;不要读取 `result.ok` 判断成功。 ### 收藏列表结果字段 | 字段 | 类型 | 含义 | |---|---|---| | `account_id` | string | 当前登录账号的微信 ID | | `sync_state` | string | 本次 `refresh:true` 成功后为 `completed`;未请求刷新为 `not_requested`,不代表账号从未同步 | | `items` | array | 本页收藏,按本地 ID 递减排列 | | `has_more` / `next_cursor` | boolean / string | 是否还有扫描范围及继续读取的游标 | `items[]` 的字段: | 字段 | 类型 | 含义 | |---|---|---| | `fav_id` | string | 微信收藏 ID;发送、删除使用此值。可能为 `"0"` 的未就绪条目不能用于这两个操作 | | `local_id` | string | 本机数据库 ID,仅用于本地排序,不可替代 `fav_id` | | `type` | integer | 微信原生收藏类型;`3` 为语音,当前不支持转发语音收藏 | | `title` / `description` / `url` | string | 当前可读取的标题、正文或说明、链接;文本收藏正文在 `description`,缺失时为空字符串 | | `create_time` / `update_time` | number | 微信原生 `srcCreateTime` / `updateTime`,Unix 秒时间;不是翻页位置 | | `tags` | string[] | 可读取的文本标签 | | `content_state` | string | `ready` 已解析;`metadata_pending` 未同步或未解析完成;`invalid` 正文解析失败 | | `forward_state` | string | `ready` 当前可进入转发;`needs_download` 转发前需要原生补齐附件;`forbidden` 禁止转发;`unknown` 尚不能确定 | | `block_reason` | string | 不可转发的原因,可能为空 | | `content_truncated` | boolean | 正文、标签或嵌套内容触及保护上限,不能把已返回内容视为完整正文 | | `data_list` | array | 复合收藏的嵌套内容摘要,结构见下表 | `data_list[]` 字段为 `data_id`(string)、`data_type`(integer)、`title`、`description`、`format`(string)、`size`(number,字节)、`duration`(number,微信原生值,单位依类型而定)、`resource_state`(`local` / `remote` / `missing` / `unknown`)和可递归的 `data_list`。资源状态仅描述微信本机可用情况,不提供可下载地址;`data_id` 不能用于发送或删除收藏。 列表不主动下载媒体。返回内容有每页正文预算、单字段长度、标签数量和嵌套节点保护;`content_truncated` 与分页的 `has_more` 含义不同。当前收藏能力只包含列表、发送和删除,没有独立的收藏详情、附件下载或任务查询接口。 ### `favorites_send`:发送到好友或群 | 参数 | 类型 | 必填 | 说明 | |---|---|---|---| | `request_id` | string | 外层必填,不放入 `args` | 调用方生成的业务请求标识,1–128 字符,不含首尾空白或控制字符 | | `fav_id` | string | 是 | 列表返回的收藏 ID,ASCII 十进制字符串,范围 `1–4294967295`,不允许前导零 | | `receiver` | string | 是 | 完整好友微信 ID,或含 `@chatroom` 后缀的完整群 ID;1–128 字符,不含首尾空白或控制字符 | 目标只使用 `receiver`,不支持另传 `wxid`、`roomid`、`room_id` 或接收人别名。好友须为当前账号可用的普通好友;群须仍有当前账号在群内。也支持当前账号自身及 `filehelper`。 正文须已解析且微信允许转发,语音收藏不支持。若原生转发要求附件,插件会让微信补齐所需资源并等待,可能增加耗时;这不提供独立的附件下载接口。列表的 `forward_state` 是读取当时的检查结果,发送时会再次检查。 ```json { "type": "command", "request_id": "fav-send-demo-001", "command_name": "favorites_send", "args": { "fav_id": "123456", "receiver": "wxid_friend_example" } } ``` 群目标仅将 `args.receiver` 换成完整群 ID,例如 `12345678901@chatroom`。 命令回包示例: ```json { "type": "command_result", "request_id": "fav-send-demo-001", "command_name": "favorites_send", "ok": true, "result": { "status": "accepted", "server_confirmed": false, "request_id": "fav-send-demo-001", "fav_id": "123456", "receiver": "wxid_friend_example" }, "error": "" } ``` 发送结果未知时: ```json { "type": "command_result", "request_id": "fav-send-demo-001", "command_name": "favorites_send", "ok": false, "result": { "status": "unknown", "server_confirmed": false, "request_id": "fav-send-demo-001", "error_code": "SEND_RESULT_UNKNOWN" }, "error": "收藏操作超时,勿自动重发" } ``` ### `favorites_delete`:等待服务器确认删除 | 参数 | 类型 | 必填 | 说明 | |---|---|---|---| | `request_id` | string | 外层必填,不放入 `args` | 业务请求标识,格式与发送相同 | | `fav_id` | string | 是 | 列表返回的收藏 ID,范围和格式与发送相同;不可传 `local_id` 或 `data_id` | 删除的是**当前登录账号的微信收藏**,会进入微信原生同步删除流程。仅在微信服务器对该收藏返回明确成功后才返回 `deleted`。删除不要求收藏正文已解析、可转发或附件已下载。 `ok:true`、`status:"deleted"`、`server_confirmed:true` 三者同时成立才表示服务器确认删除。收藏已从列表消失、原生方法接收请求或本地待删除,都不能单独证明云端已删除。 ```json { "type": "command", "request_id": "fav-delete-demo-001", "command_name": "favorites_delete", "args": { "fav_id": "123456" } } ``` 服务器确认删除的命令回包: ```json { "type": "command_result", "request_id": "fav-delete-demo-001", "command_name": "favorites_delete", "ok": true, "result": { "status": "deleted", "server_confirmed": true, "request_id": "fav-delete-demo-001", "fav_id": "123456" }, "error": "" } ``` 删除结果未知的命令回包: ```json { "type": "command_result", "request_id": "fav-delete-demo-001", "command_name": "favorites_delete", "ok": false, "result": { "status": "unknown", "server_confirmed": false, "request_id": "fav-delete-demo-001", "fav_id": "123456", "error_code": "DELETE_RESULT_UNKNOWN" }, "error": "等待收藏删除确认超时,微信队列可能继续处理,请勿自动重新提交" } ``` 命令外层 `ok` 表示本次业务判断;WS 帧传输成功只表示收到回包,仍需按下表判断收藏发送或删除的实际状态。 ### 等待、幂等与错误处理 收藏接口采用单次调用等待结果,不返回需要另行轮询的 `operation_id`,也不产生专门的收藏完成事件。列表无刷新时直接读取本地;刷新约 20 秒、删除约 20 秒、发送准备及提交约 40 秒业务期限,另有约 45 秒后台等待保护。客户端业务结果等待建议至少 60 秒;代理和后端也需允许该时长。连接心跳和网络超时不等于这段业务等待时长。 发送成功返回的 `accepted` 表示收藏已进入微信本地原生转发链路,**没有微信服务器送达确认**,所以 `server_confirmed:false` 是预期值。不要据此判为失败或自动重发;接口也不保证接收方已收到或已读。发送和删除的 `unknown` 都表示结果不能确认,断线或命令结果未收到也不能据此认定操作未执行。 - 对已登记的同一发送或删除操作,相同账号和登录周期内,相同 `request_id` 与相同参数会复用原结果;仍在处理中返回 `REQUEST_IN_PROGRESS`,不重复提交。相同 ID 改参数返回 `REQUEST_ID_CONFLICT`。参数校验等登记前失败不占用幂等记录。 - 发送和删除各最多保留 512 条内存幂等记录;满额拒绝新请求,不通过淘汰旧记录再次执行。微信重启、账号或登录周期变化后记录失效,后端仍需持久保存业务请求及结果。 - 相同 ID 返回已有结果并不是重新查询微信最终状态。已经返回 `unknown` 时,不要通过更换 ID 自动重发或重删,先人工核实微信中的实际结果。 - 当前同一账号只允许一个等待中的发送、一个等待中的删除;同一收藏的发送准备与删除互斥。收藏删除后列表会跳过本地待删除项,但列表缺项不能代替服务器确认。 三种接入方式使用相同业务 `error_code`。列表失败可不含 `status`;发送、删除通常返回 `failed` 或 `unknown`。优先按错误码分支,不要精确匹配中文错误文案。 | `error_code` | 含义与处理 | |---|---| | `INVALID_ARGUMENT` | 参数名、类型或范围不合法;收藏采用严格校验,拒绝未支持的字段 | | `ACCOUNT_CHANGED` | 未登录、账号或登录周期已变化;重新确认账号,重取列表和游标 | | `FEATURE_DISABLED` | 插件关闭,或有效发送/转发状态关闭 | | `NATIVE_API_UNAVAILABLE` | 当前微信原生方法、回调或签名不可用,检查插件与微信版本 | | `FAVORITES_SYNC_BUSY` / `FAVORITES_SYNC_FAILED` / `FAVORITES_SYNC_TIMEOUT` | 刷新正在进行、失败或超时;失败可能带 `sync_diagnostics`,没有返回本次“刷新完成”的列表 | | `FAVORITE_NOT_FOUND` | 本机无法找到该收藏或已删除;删除时不能据此认定云端删除成功 | | `FAVORITE_METADATA_PENDING` | 发送所需正文未就绪或解析失败,先刷新收藏列表 | | `FAVORITE_NOT_FORWARDABLE` | 微信禁止转发,如语音收藏,原因见 `error` | | `FAVORITE_DOWNLOAD_FAILED` / `FAVORITE_DOWNLOAD_TIMEOUT` | 发送准备时所需资源补齐失败或超时 | | `NATIVE_CONVERSION_FAILED` / `SEND_FAILED` | 原生消息转换失败或转发取消;是否已经进入提交仍看 `status` | | `TARGET_NOT_FOUND` / `TARGET_UNAVAILABLE` | 目标不存在、不可用、不是普通好友或当前账号不在群内 | | `FAVORITES_SEND_BUSY` / `FAVORITES_DELETE_BUSY` / `FAVORITES_OPERATION_BUSY` | 有在途操作或同一收藏操作冲突,先等已有操作结果 | | `REQUEST_IN_PROGRESS` / `REQUEST_ID_CONFLICT` / `IDEMPOTENCY_CAPACITY` | 同一请求仍在处理、ID 与参数冲突或本登录周期幂等记录已满 | | `FAVORITE_DELETE_PENDING` | 收藏已在微信删除队列中,返回 `unknown`,不会再次提交 | | `SEND_RESULT_UNKNOWN` / `DELETE_RESULT_UNKNOWN` | 已提交或等待超时后结果未知;不要自动重新提交 | 删除未知响应可能额外包含 `native_error_code`,表示等待期间观察到的微信原始错误码;微信原生队列可能继续重试,不能只用它推导最终成功或失败。 收藏绕过命令通道通用的“最近 100 条”结果缓存,由收藏服务按当前账号、登录周期及完整参数校验。列表不提供 `request_id` 结果缓存;要获取某页,应使用其游标重新读取。 WS 收包队列在收藏等待期间仍会处理心跳和其他命令,结果可能与下发顺序不同;始终按 `request_id` 关联,依赖上一条结果的操作必须等回包后再下发。 --- ## 23. 事件 body 字段 ### 23.1 事件类型总表 | `event_name` | 说明 | |-------------|------| | `moments_result` | 本次接口/命令对应的朋友圈页面或写入结果;含 operation_id/request_id/status/revision,见 [本篇朋友圈完整用法](#moments-start) | | `message` | 普通聊天消息(文本、图片、语音、视频、表情、文件、链接、拍一拍、系统消息等) | | `red_packet_received` | 收到红包 | | `transfer_received` | 收到转账 | | `transfer_refund_result` | [转账退还结果:成功、失败或未知,按 native_status 判断](#event-transfer-refund) | | `group_member_join` | 群成员加入 | | `group_member_leave` | 群成员离开 | | `group_member_nickname_change` | 群成员修改或清除群内昵称 | | `friend_request` | 收到好友申请 | ### 23.2 `message` 事件 ```json { "event_name": "message", "event_id": "msg:7307587870543245070", "body": { "message_id": "7307587870543245070", "msg_type": "text", "raw_message_type": 1, "chat_id": "50136399923@chatroom", "room_id": "50136399923@chatroom", "sender_id": "wxid_member", "is_group": true, "is_at": false, "at_user_list": [], "at_all": false, "create_time": 1774772029, "content": "你好" } } ``` #### 恒有字段(一定会出现) | 字段 | 类型 | 说明 | |------|------|------| | `message_id` | string | 消息 ID。取不到服务端消息 ID 时会退化为**毫秒时间戳字符串** | | `msg_type` | string | 插件映射的消息类型,见下表;**可能为 `unknown`** | | `raw_message_type` | int | 微信原生消息类型数字,见下表 | | `content` | string | 消息内容。**对文本是文字,对图片/文件等是原始 XML 串,也可能是空串 `""`** | | `chat_id` | string | 会话 ID(群聊为群 ID,私聊为对方或自己 wxid)。**可能为空串** | | `room_id` | string | 群 ID;**私聊时为空串 `""`(字段仍在)** | | `sender_id` | string | 发送者 wxid | | `is_group` | bool | 是否群聊 | | `is_at` | bool | 是否 @ 了当前账号本人;**私聊恒为 `false`** | | `at_user_list` | string[] | 被 @ 的 wxid 列表;**无 @ 或私聊时为空数组 `[]`** | | `at_all` | bool | 是否 @ 所有人 | | `create_time` | int | 微信消息时间,**单位:秒**(注意与 `ts_ms` 的毫秒不同) | #### 消息类型对照 | `msg_type` | `raw_message_type` | 说明 | |-----------|-------------------|------| | `text` | 1 | 文本 | | `image` | 3 | 图片 | | `voice` | 34 | 语音 | | `video` | 43 | 视频 | | `contact_card` | 42 | 名片 | | `emoji` | 47 | 表情 | | `app` | 49 | App 消息(链接、文件、小程序、合并转发、引用等都在这一类) | | `pat` | 0 | 拍一拍 | | `system` | 10000 | 系统消息(撤回、群名修改等) | | `unknown` | 其他 | 未能识别的类型 | > ⚠️ **注意一个真实差异**:代码里的 `msg_type` 实际取值包含 **`contact_card` 和 `app`,没有 `file` 和 `link`**。文件和链接是 `app` 类型下的子分类,靠 `msg_subtype` 区分。请**不要按 `msg_type == "file"` 来判断文件消息**。 #### 条件字段(只在满足条件时出现,不满足则**整个字段不存在**) | 出现条件 | 字段 | 类型 | 说明 | |---------|------|------|------| | `content` 能被识别为 XML | `content_xml` | string | 归一化后的完整 XML | | 同上 | `content_prefix` | string | XML 之前的非空文本前缀 | | `msg_type == image` | `image_md5`、`image_aes_key`、`image_thumb_url`、`image_mid_url`、`image_origin_md5` | string | 图片解密与多档清晰度地址——**下载图片需要 `image_aes_key`** | | 同上 | `image_length` | int | 字节 | | `msg_type == voice` | `voice_url`、`voice_aes_key`、`voice_md5`、`voice_from_user`、`voice_client_msg_id` | string | 语音下载信息 | | 同上 | `voice_length_ms` | int | **毫秒** | | 同上 | `voice_data_length` | int | 字节 | | `msg_type == emoji` | `emoji_md5`、`emoji_aes_key`、`emoji_cdn_url`、`emoji_encrypt_url` | string | | | 同上 | `emoji_length`、`emoji_width`、`emoji_height` | int | 字节 / 像素 | | `msg_type == contact_card` | `card_username`、`card_nickname`、`card_alias`、`card_province`、`card_city`、`card_sign`、`card_big_head_img_url`、`card_small_head_img_url` | string | | | 同上 | `card_sex`、`card_scene` | int | | | `msg_type == app` | `app_msg_type` | int | App 消息的**内层类型**:`5` 链接、`6` 文件、`8` 上传、`19` 合并转发、`33` 小程序、`57` 引用,其他为未知 | | 同上 | `msg_subtype` | string | 对应的可读名:`link` / `file` / `upload` / `merged_records` / `mini_program` / `quote` / `app_unknown` | | 同上 | `app_msg_title`、`app_msg_desc`、`app_msg_url`、`app_source_username`、`app_source_display_name`、`app_thumb_url`、`app_thumb_md5`、`app_aes_key` | string | | | `msg_subtype == quote` | `quote_msg_id`、`quote_from_user`、`quote_chat_user`、`quote_display_name`、`quote_content` | string | 引用消息的原文信息 | | 同上 | `quote_msg_type`、`quote_create_time` | int | 引用消息的类型与时间 | | 拍一拍专属 | `pat_from_user`、`pat_patted_user`、`pat_template` | string | 见[本篇拍一拍事件](#event-pat) | > ⚠️ **判断文件消息的正确方式**:`msg_type == "app"` 且 `msg_subtype == "file"`。 #### 被主动剥掉的字段(不会出现在 body 里) `from_user`、`to_user`、`sender_id_type`、`real_chat_user`、`source`、`hook_selector`、`hook_type`、`self_user`、以及内部的 `msg_id`(已被改名为 `message_id`)。 **这意味着服务端拿不到"这条消息是发给谁的"直接的收发方向字段**,只能靠 `chat_id` / `room_id` / `sender_id` / `is_group` 自己推断方向。这是设计如此,不是 bug。 #### 群内 @ 字段的精确语义 | 字段 | 说明 | |------|------| | `is_at` | 本条消息是否 @ 了当前登录账号本人 | | `at_user_list` | 本条消息里所有被 @ 的 wxid。**取自微信消息自带的 @ 名单,插件只做去空白和去重,不做昵称解析** | | `at_all` | 是否 @ 了所有人;等价于 `at_user_list` 中含协议标记 `notify@all` | 三条必须知道的约定: 1. @所有人 时微信写入的是**协议标记 `notify@all`** 而不是 wxid,插件按原样透出并同时给出 `at_all: true`,**不会**展开成全部群成员(消息时刻的成员列表无法还原,展开会失真) 2. **`is_at` 与 `at_user_list` 相互独立**:`is_at` 以微信自身判断为准,`at_user_list` 是消息里的原始名单。@所有人 或消息未携带相关数据时,**可能出现 `is_at: true` 而 `at_user_list` 为空** 3. 群消息中 `chat_id` 与 `room_id` 相同且 `is_group` 为 `true`;私聊中 `room_id` 为空串、`is_group` 为 `false` ### 23.3 拍一拍 `msg_type` 为 `pat`,走系统消息通道,**`raw_message_type` 为 `0`**: ```json { "event_name": "message", "event_id": "msg:7307587870543245070", "body": { "message_id": "7307587870543245070", "msg_type": "pat", "raw_message_type": 0, "chat_id": "wxid_friend", "room_id": "", "sender_id": "wxid_friend", "is_group": false, "create_time": 1774772029, "content": "你拍了拍自己", "pat_from_user": "wxid_friend", "pat_patted_user": "wxid_self", "pat_template": "你拍了拍自己" } } ``` | 字段 | 说明 | |------|------| | `pat_from_user` | 拍的人的 wxid | | `pat_patted_user` | 被拍的人的 wxid | | `pat_template` | 原始模板文本(如"你拍了拍xxx") | > **注意两个字段的区别**:`content` 是**把 `${wxid}` 占位符替换成显示名之后**的文案(给人看的);`pat_template` 是**未解析的原始模板**。要展示请用 `content`。 ### 23.4 红包事件 ```json { "event_name": "red_packet_received", "event_id": "red_packet:rp_msg_7307587870543245070", "body": { "red_packet_id": "rp_msg_7307587870543245070", "message_id": "7307587870543245070", "chat_id": "50136399923@chatroom", "room_id": "50136399923@chatroom", "sender_id": "wxid_sender", "from_user": "wxid_sender", "to_user": "wxid_self", "create_time": 1774772029, "raw_message_type": 49, "send_id": "1000039501202603311234567890", "channel_id": "1", "msg_type": "1", "native_url": "…", "sign": "", "scene_id": 1002, "session_user_name": "…", "nick_name": "…", "head_img": "", "is_group": true, "is_openim": false, "is_group_sender": false, "in_way": "0", "timing_identifier": "", "status": "received", "created_at_ms": 1774772029000, "updated_at_ms": 1774772029000 } } ``` | 字段 | 说明 | |------|------| | `red_packet_id` | **领取红包时唯一需要回传的参数**(`receive_red_packet` / `red_packet_detail`)。**去重请用它** | | `send_id`、`channel_id`、`native_url` | 红包领取链路必需。**这三者任取不到时,整个事件不会生成** | | `timing_identifier` | 事件发出时**恒为空串** `""`(领取流程中才回填) | | `status` | 事件发出时**恒为 `"received"`**。后续领取状态只更新本地缓存,**不会重新上报成事件** | | `msg_type` | ⚠️ **语义冲突**:这里不是微信消息类型名,而是红包链路的 `msgtype` 查询参数(空则 `"1"`)。**别和 `message` 事件的 `msg_type` 混用** | | `created_at_ms` / `updated_at_ms` | **毫秒**(注意 `create_time` 是秒) | | `nick_name` / `head_img` | **发红包的人**的昵称与头像 | | `session_user_name` | 群内自己发时为 `to_user`,否则为 `from_user` | > ⚠️ **红包事件 body 里没有 `content` / `content_xml`**,也没有 `app_msg_*` 系列字段——它是独立构造的,不复用消息字段。 ### 23.5 转账事件 ```json { "event_name": "transfer_received", "event_id": "transfer:1000050001202604140530193566598", "body": { "transfer_id": "1000050001202604140530193566598", "message_id": "8871556727500760296", "chat_id": "wxid_demo_account", "room_id": "", "sender_id": "wxid_demo_account", "from_user": "wxid_demo_account", "to_user": "wxid_demo_friend", "real_chat_user": "wxid_demo_account", "current_user": "wxid_demo_friend", "receiver_username": "wxid_demo_friend", "payer_username": "", "control_transaction_id": "53010002669213202604143932510122", "amount_text": "¥0.02", "amount_value": "0.02", "amount_fen": 2, "memo": "", "remark": "", "nickname": "…", "app_msg_type": 2000, "app_msg_title": "微信转账", "app_msg_desc": "", "app_msg_url": "", "msg_subtype": "…", "content": "", "content_xml": "...", "native_url": "", "create_time": 1774772029, "raw_message_type": 49, "status": "received", "created_at_ms": 1774772029000, "updated_at_ms": 1774772029000, "pay_info_class": "…", "pay_info_probe": {} } } ``` **金额字段说明**(三个都有,别搞混): | 字段 | 类型 | 说明 | |------|------|------| | `amount_text` | string | 带符号的展示文本,如 `"¥0.02"` | | `amount_value` | string | **字符串形式**的元值,如 `"0.02"` | | `amount_fen` | int | **整数分**,如 `2` → **推荐用它做金额计算,避免浮点误差** | **身份字段说明**(六个高度重叠,按需取用): | 字段 | 含义 | |------|------| | `sender_id` | 消息发送者 | | `from_user` | 转账付款方 | | `to_user` | 转账收款方 | | `receiver_username` | 收款方用户名 | | `payer_username` | 付款方用户名(**可能为空串**) | | `current_user` | 历史接收方上下文字段,不是独立登录凭证;不要据此覆盖原生付款方/收款方参数 | **建议至少持久化**:`transfer_id`、`message_id`、`control_transaction_id`、`amount_text`、`amount_fen`、`sender_id`、`chat_id`。 **推荐用 `transfer_id` 去重。** **注意**: | 事项 | 说明 | |------|------| | `status` | 首次解析通常为 `"received"`,这里只表示收到了转账消息,不代表资金已收取;同笔上下文重读时可保留已有状态。资金状态请用 `get_transfer_status` 的 native_status 判断;退还结果另有独立事件 | | `create_time` | **秒**;**取不到时为 `0`**(不是当前时间) | | `app_msg_type` / `msg_subtype` | **可能是 JSON `null`**,解析时要容忍 | | `room_id` | 有,但**没有 `is_group` 字段**(与 `message` 事件不同) | | `content_xml` | 原始 XML,字段很长,不需要的话别存 | | 防误报 | 如果这条消息被识别成红包证据,插件**不会**生成转账事件 | #### 转账退还结果事件 `transfer_refund_result` 本事件使用 WebSocket `event` 帧。下面展示本节相关字段,公共时刻、设备、账号与队列字段按本模式事件约定携带: ```json { "event_name": "transfer_refund_result", "event_id": "transfer_refund:refund_result_example", "body": { "ok": true, "transfer_id": "transfer_example", "message_id": "message_example", "accepted": true, "confirmed": true, "status": "refunded", "native_status": 2002, "ret_code": 0, "error_code": "", "error": "", "refund_result_id": "refund_result_example", "source": "webridge_live" } } ``` `body` 保留退还业务结果的全部基础字段:`ok`、`transfer_id`、`message_id`、`accepted`、`confirmed`、`status`、`native_status`、`ret_code`、`error_code`、`error`,另有非空 string `refund_result_id`(本次结果事件的独立标识)及 string `source`(来源诊断字段,不用于业务判断)。微信错误回包还可能带 `wx_error_type`(integer 或 null)。字段类型、所有状态与错误码见本文 [转账退还与查询](#transfer-operations)。`event_id` 为 `transfer_refund:`,消费时按完整 event_id 去重,不按 transfer_id 去重,以免把查询或后续结果吞掉。 事件名不代表成功:只有 `native_status=2002` 且 `status="refunded"` 才确认已退还。已进入退还提交且操作结束时才可能产生事件,可能是成功、失败或 `refund_unknown`;仅返回 `refund_pending` 时还未结束,后续回包或主动查询查到终态可能完成原动作并产生事件。**在 refund_unknown/failed 等已结束动作之后再查询,只保证返回状态和回收占用,不保证补发第二个退款结果事件。** 未提交就被拒绝、预检发现已经退还均不发此事件;账号会话变化后的旧结果不投递给新账号。因此客户端必须保留主动查询路径,不能只等事件。 ### 23.6 群成员变更事件 ```json { "event_name": "group_member_join", "event_id": "msg:group_member_join:50136399923@chatroom:wxid_member:1775000001123", "body": { "room_id": "50136399923@chatroom", "member_id": "wxid_member", "member_nick_name": "张三", "member_remark": "", "member_alias": "", "member_display_name": "张三", "room_nick_name": "项目群", "room_remark": "", "message_id": "group_member_join:50136399923@chatroom:wxid_member:1775000001123", "snapshot_before_count": 23, "snapshot_after_count": 24, "create_time": 1775000001, "is_group": true } } ``` `group_member_leave` 使用相同的群/成员字段,表示成员离开;`snapshot_before_count`、`snapshot_after_count` 分别为变更前后成员数。 昵称变更事件额外带: ```json { "event_name": "group_member_nickname_change", "body": { "old_room_nick_name": "lumen", "new_room_nick_name": "lumen123", "snapshot_before_count": 23, "snapshot_after_count": 23 } } ``` **注意**: | 事项 | 说明 | |------|------| | 群成员事件**没有** `msg_type` / `raw_message_type` / `sender_id` / `chat_id` | 它们被主动剥掉了,请用 `room_id` + `member_id` | | `create_time` | **秒** | | `snapshot_before_count` / `snapshot_after_count` | 加入/离开事件里是**群成员数**;昵称变更事件里是**昵称快照的条目数**。⚠️ **同一字段名两种语义**,别跨事件类型复用判断逻辑 | | 首次建群快照**不会**产生 `group_member_join` | 插件第一次看到某个群时不报加入 | | 首次建立昵称快照**不会**产生 `group_member_nickname_change` | 只有后续修改/清除才上报 | | 退群事件有短暂确认延迟 | 避免同步抖动误报 | | 昵称变更的确认机制 | 会按 150ms / 500ms / 1500ms 三次延迟复查确认,所以**事件到达会略有延迟**,且 `member_display_name` 等字段是复查后的值 | ### 23.7 好友申请事件 `event_name` 为 `friend_request`。 **这个事件的 body 结构有明显的不确定性**:`event_id` 会按以下优先级生成,说明至少存在三种数据形态: 1. 有申请 ID → `friend_request:<申请ID>` 2. 否则有消息 ID → `msg:<消息ID>` 3. 都取不到 → 兜底 `evt:<毫秒>` **恒有字段**: | 字段 | 类型 | 说明 | |------|------|------| | `message_id` | string | 消息 ID(可能是兜底的时间戳字符串) | | `msg_type` | string | 固定 `"friend_request"` | | `content` | string | **可能为空串** | | `room_id` | string | **强制为空串 `""`** | | `is_group` | bool | **强制为 `false`** | | `source_xml` | string | 归一化后的完整 XML | | `create_time` / `is_at` / `at_user_list` / `at_all` | — | 继承自消息基础字段 | **条件字段**(取不到则**不出现**): | 字段 | 说明 | |------|------| | `request_id` | 好友申请业务 ID | | `encrypt_username` | 加密用户名,**同意申请时必须** | | `ticket` | 凭证,**同意申请时必须** | | `scene` | 场景值 | | `from_user` / `sender_id` / `chat_id` | **仅当解析出申请方用户名时才写入这三个键** | | `nick_name`、`alias`、`sex`、`country`、`province`、`city`、`signature`、`big_head_img_url`、`small_head_img_url` | 申请方资料,按可取到的填 | > ⚠️ **同意好友申请前请先校验 `encrypt_username` 和 `ticket` 是否都存在**,缺失时不要盲调 `accept_friend_request`。 > > **识别条件**:只有满足特定特征的消息才会成为 `friend_request` 事件,否则会退回普通 `message` 事件。所以**不要假设每个好友申请都一定会以 `friend_request` 出现**。 ### 23.8 事件的可靠性语义 | 事项 | 说明 | |------|------| | 成功判定 | 你**不需要回响应**。插件只关心帧是否成功写入连接 | | 发送失败 | 进入离线队列等待重发(见第 24 节) | | 重复可能 | **存在重复**。连接抖动、重连补发都会导致同一事件被推两次。**必须按 `event_id` 去重** | | 丢失可能 | 见第 24 节的容量/TTL 淘汰。**插件不会告诉你丢了多少条** | --- ## 24. 离线队列与补发 连接不可用时,插件把待发的 `event` 和 `command_result` **写到手机上暂存**,连接恢复后补发。 | 项目 | 值 | |------|-----| | 存储位置 | 插件沙盒的缓存目录下 `webridge/ws_outbox.json` | | 最大条数 | 默认 `500`,设置页可调,**硬上限 `5000`** | | 超出容量 | 从**队头**(最旧的)开始丢弃 | | 最大暂存时间(TTL) | 默认 `120` 秒;设为空或 `0` 表示**不过期** | | TTL 检查时机 | **入队前**检查一次(且先清过期再判重)、**每次补发前**再检查一次 | | 去重键 | `stable_id`:事件用 `event_id`(缺失时用 `evt:<毫秒>`),命令结果用 `request_id` | | 重复时的行为 | **丢弃新的那条**(保留队列里已有的) | | 落盘时机 | **入队时立即全量落盘**;移除/TTL 清理走节流(累计 20 条或 500ms 兜底);清空立即落盘 | | 补发触发 | 连接建立时(不等 `hello_ack`),以及协议层 Pong 成功后 | | 补发顺序 | 从队头开始先进先出,**发送成功才移除** | | 补发失败 | 立即暂停并落盘;连接恢复或后续心跳成功时再尝试 | ### 关键含义 1. **连接建立及协议层 Pong 成功后都会尝试补发。** 发送成功仅表示帧写入连接;服务端业务处理失败但未断开连接时,插件无法据此判断并重投。 2. **TTL 默认只有 2 分钟。** 服务端停机超过 2 分钟,这期间的事件**会丢且不会被通知**。需要长停机不丢数据,请把 TTL 调大或置空,并提高条数上限。 3. **暂存开关关闭时**,连接不可用期间的内容**直接丢弃**,不入队。 4. **命令结果也会被暂存。** 所以你可能在重连后收到一批"迟到的"命令结果——**用 `request_id` 关联,不要假设结果一定紧跟命令**。 5. **插件总开关关闭时**,既不发送也不入队。 > ⚠️ **去重是你的责任**:容量淘汰、TTL 淘汰、去重丢新,这几种情况组合起来意味着**你既可能收到重复,也可能收不到某些事件,且插件不会主动告知**。 > **强烈建议**:在服务端做基于 `event_id` 的去重,并监控"实际收到的事件数 vs 微信侧应有的事件数"的差异。 --- ## 25. 断线重连 ### 25.1 全部重连触发条件 以下每一种都会导致断开并尝试重连,且都会让失败计数 +1: | # | 触发条件 | 超时/说明 | |---|---------|----------| | 1 | 连接建立超时 | **15 秒**内没连上 | | 2 | `hello_ack` 超时 | **10 秒**内没收到 | | 3 | 接收消息出错 | — | | 4 | **发送任何帧失败** | 任一帧发送失败就断开整条连接 | | 5 | 心跳 Ping 回调报错或超时 | 发出 Ping 后 10 秒未取得本次 Pong 回调 | | 6 | 收到服务端 Close 帧 | **不区分正常/异常关闭**,任何 Close Code 都走同一路径 | ### 25.2 退避序列 | 连续失败次数 | 重连等待 | |-------------|---------| | 1 | 2 秒 | | 2 | 5 秒 | | 3 | 10 秒 | | 4 | 20 秒 | | 5 及以上 | **30 秒**(封顶) | ### 25.3 计数何时归零 **连接建立(Open)时立即归零——不是收到 `hello_ack` 时。** > 如果连接每次都能建立,但服务端始终不回 `hello_ack`,失败计数每次都会在 Open 时清零,因此每次等到 10 秒确认超时后,重连退避仍只有 **2 秒**。实际建连周期还包含握手与确认等待,不能理解为每 2 秒建立一次连接。 > **规避方法**:服务端务必处理 `hello` 并回 `hello_ack`;如果决定拒绝,请在 10 秒内主动 Close,不要拖着不回。 ### 25.4 不会重连的情况 | 情况 | 行为 | |------|------| | 插件总开关关闭 | **不排重连,停在原地** | | 地址为空 | 不排重连 | | 地址无法解析 | 不排重连(只写日志,无界面提示) | | 插件主动停止 | 不重连 | ### 25.5 改了地址会怎样 插件在下一次心跳、命令接收、待发数据处理或连接尝试中检查地址变化;发现与当前地址不一致时,关闭旧连接并连接新地址。微信回到前台也会触发连接探测。此检查依赖进程获得执行时间,不承诺设置保存后立刻完成切换。 变更地址会使旧通信生命周期失效,尚未执行的旧命令取消,已经提交的业务仍可能生效。切换后先核实未知操作,避免换 ID 重复发送。 --- ## 26. 幂等与去重 ### 26.1 命令侧(你 → 插件) 以下“最近 100 条”规则描述通用命令缓存。创建群聊、直接加人和发送群邀请(含 `group_invite_member`)使用[独立的群操作去重规则](#group-member-operations),不适用本节换新ID重试建议。好友支付姓名、群二维码、个人二维码、群管理员操作、统一媒体四项、入群邀请三项、收藏三项、公众号五项及位置反查探针每次进入共享服务重新校验账号,不使用此缓存。收藏发送/删除按[收藏专用幂等规则](#favorites)处理;公众号五项不走此缓存,关注/取关结果未知时先查关系,见[公众号](#official-accounts)。 插件对**最近 100 条** `request_id` 缓存了执行结果(内存缓存,**进程重启后失效**)。 收到重复 `request_id` 时的行为: | 情况 | 行为 | |------|------| | 命中缓存 | **直接回上次的结果,不重新执行** | | 旧 ID 已被后续不同 ID 淘汰,或进程已重启 | 当作新命令,**重新执行**;通用缓存没有按时间过期的规则 | **对你的含义**: - 同一个 `request_id` **仍在通用缓存内**时,重复下发会复用结果;同 ID 应始终对应同一业务和参数 - 后续不同 `request_id` 会逐步挤出旧结果;某个 ID 被淘汰或进程重启后,重发会再次执行,不能依赖它做无限重试 - ⚠️ **由于缓存会失效,重发命令时请确保业务上可接受"可能执行第二次"**(例如发消息类命令用业务层的去重表兜底) ### 26.2 事件侧(插件 → 你) | 事项 | 说明 | |------|------| | 插件是否去重 | **不保证**。连接抖动、重连补发都可能产生重复 | | 你的责任 | **按 `event_id` 去重** | | 业务 ID 缺失时 | 当前 WS 帧使用 `evt:<毫秒>` 兜底;后端可辅以组合字段,不能把毫秒 ID 当作业务 ID | ### 26.3 补发导致的"时间乱序" 离线队列补发时,事件的 `ts_ms` / `enqueued_at_ms` 都是**入队时的时间**,而 `captured_at_ms` 是**更早的捕获时间**。 **所以你会收到"过去的"事件,且它们可能夹在当前事件中间。** **建议**:用 `captured_at_ms` 做业务时间排序,用 `event_id` 做去重,**不要用收到时刻做业务时间**。 --- ## 27. 常见问题排查 ### 插件日志显示「未配置连接地址」 设置页的「WebSocket 服务端地址」为空。填上即可。 ### 插件日志显示「连接地址无效」 地址无法解析。最常见原因:**漏了 `ws://` 或 `wss://` 前缀**。插件**不会**自动补协议,请写全。 ### 一直是"连接超时" 1. 手机和服务器网络是否互通 2. 端口是否开放、服务端是否真的在监听 WebSocket 3. 是不是把 HTTP 服务当成 WebSocket 服务了(反向代理尤其容易配错,注意 `Upgrade` / `Connection` 头要透传) 4. `ws://` 与 `wss://` 是否用对(开发用 `ws://`,生产用 `wss://`) 排查工具: ```bash wscat -c ws://你的地址:端口 ``` ### 连上了但反复断开 **先看是不是 `hello_ack` 超时**(日志里会写"hello_ack 超时")。服务端必须回 `hello_ack`。 如果日志显示"连接被关闭",检查是不是你的服务端主动关掉了连接(例如自己的心跳超时策略过激)。 ### 收不到命令执行结果 按可能性排序: 1. **`request_id` 或 `command_name` 写空了** → 插件**静默丢弃**,不回结果。这是最常见原因 2. 帧的 `type` 不是 `"command"` → 被当作未知类型忽略 3. JSON 非法 → 被忽略 4. `command_name` 拼错 → 会返回 `ok:false` + `unsupported command_name: xxx`,看看是不是这条 5. 连接已断 → 看日志 ### 收到了事件,但服务端日志里时间很旧 正常。这是离线队列补发的历史事件。用 `captured_at_ms` 判断真实发生时间。 ### 事件重复 预期行为。按 `event_id` 去重。 ### 反复重连,退避一直只有 2 秒 先检查 `hello_ack` 是否及时回复。失败计数在 Open 时清零,服务端不回确认时,每次 10 秒确认超时后的退避仍只有 2 秒;实际建连周期还包含握手和确认等待。 ### 服务端没收到补发的数据 先检查暂存开关、容量和有效期。连接建立及协议层 Pong 成功后都会尝试补发;服务端已收帧但处理失败时,插件没有业务 ACK,不能据此自动重投。 ### 怎么确认插件到底连的哪个地址 连接仍可用时下发 `bridge_status`,在 `result` 中读取 `ws_connected`、`ws_connecting`、`ws_last_disconnect_reason`、`ws_last_received_type`、`ws_last_received_at_ms`、`ws_last_ping_at_ms` 和 `ws_last_pong_at_ms`。远程命令无法到达时,需从手机设置页或本地日志排查;可直连手机的环境也可调用 `GET /debug/bridge_status`。 --- ## 附录:兼容命令与已移除功能 ### 兼容命令别名 以下均由当前分发器实际支持,与右侧主命令共用参数和结果;新接入使用主命令。别名未必出现在 `capabilities`,不能据清单缺省推断分发器不支持。 | 兼容命令 | 主命令 | | --- | --- | | `group_invite_member` | `invite_group_member` | | `group_invite_member_probe` | `invite_group_member_probe` | | `update_nickname` | `profile_nickname` | | `update_nickname_probe` | `profile_nickname_probe` | | `set_group_self_nickname` | `rename_group` | | `set_group_nickname` | `rename_group` | | `set_group_self_nickname_probe` | `rename_group_probe` | | `set_group_nickname_probe` | `rename_group_probe` | ### 已移除:已读与主动发送红包 `mark_session_read` 的单个、批量和全部已读,以及 `mark_session_read_probe` 探针均已移除,不再执行,当前无替代命令。 通过前置鉴权和插件状态校验、进入命令分发后,返回既有 `command_result` 信封,保留原 `request_id` 和 `command_name`,业务字段为: ```json { "ok": false, "result": {}, "error": "unsupported command_name: mark_session_read" } ``` 探针的 `error` 为 `unsupported command_name: mark_session_read_probe`。这是命令已移除的失败结果,无异步完成事件;请停止下发,重试或更换 `request_id` 不会恢复该能力。旧版本的 `mode`、`marked`、`failed` 等成功字段不再适用。 发送后自动已读及其设置项也已移除。`mark_session_read` 与 `mark_session_read_probe` 不再列入能力清单,无替代命令。主动发送红包及其探针已移除;领取已有红包及查询详情仍按本文业务命令使用。