docs: weixin ClawBot bridge protocol and examples

This commit is contained in:
AskaEth committed 2026-08-21 18:15:20 +08:00
commit 4ea3957da9
13 files changed
+2158

No files matched your search

+98
View File
@@ -0,0 +1,98 @@
# 01 — 概述:ClawBot、iLink、OpenClaw 是什么关系
## 1. 背景
2026 年 3 月,微信上线了官方插件 **微信 ClawBot**(入口:微信 → 我 → 设置 → 插件 → 微信 ClawBot),这是微信**首次官方开放个人账号的 Bot API**。在此之前,程序控制微信只有灰色手段:
| 方式 | 典型实现 | 性质 |
|---|---|---|
| 逆向 iPad/网页协议 | WeChatPadPro、itchat(-uos) | 违反用户协议,随时封号 |
| PC 客户端 Hook | DLL 注入、内存读写 | 违法风险,高封号率 |
| 企业微信 API | 官方开放 | 合法,但面向企业不是个人微信 |
| **ClawBot / iLink** | 微信内置官方插件 | **官方产品,有专项使用条款背书** |
腾讯为此发布了《微信 ClawBot 功能使用条款》(签订地深圳南山,适用中国大陆法律)。条款明确腾讯的定位:**只提供消息收发通道,不存储用户输入/输出内容,不提供 AI 服务**——AI 能力完全由开发者自己接。
## 2. 三个概念的分工
| 名词 | 是什么 | 谁的产品 |
|---|---|---|
| **ClawBot** | 微信客户端内置插件,用户在微信里看到的 AI 联系人(带 "AI" 标识的普通聊天窗口) | 腾讯 |
| **iLink(智联)** | Bot 通信协议,HTTP/JSON + 长轮询,服务端 `ilinkai.weixin.qq.com` | 腾讯 |
| **OpenClaw** | 开源 AI Agent 框架(2025-11 首发,昵称"小龙虾"),腾讯官方通道的默认宿主 | 第三方开源社区(非腾讯) |
关系链条:
```
用户 → 微信客户端(ClawBot 插件) → iLink 服务器 → Bot 后端(默认是 OpenClaw,可以是任何东西)
```
腾讯发布的 npm 包:
- `@tencent-weixin/openclaw-weixin-cli` — 一键安装工具(检测 openclaw → 装插件 → 引导扫码 → 重启 gateway)
- `@tencent-weixin/openclaw-weixin` — 真正的协议实现包(TypeScript,约 41 个源文件:`auth/`、`api/`、`cdn/`、`messaging/`、`monitor/`、`config/`、`storage/`)
## 3. 关键事实:iLink 不绑定 OpenClaw
**iLink 是标准 HTTP/JSON API,与 OpenClaw 框架零耦合。** 官方的 npm 包只是协议的一个封装实现。任何语言、任何进程都可以直接调 iLink:
- 无需公网 IP(长轮询,主动外连)
- 无需 SDK(`fetch` / `requests` 即可)
- 无需安装 OpenClaw
已有的非 OpenClaw 实践:
- [cc-weixin](https://github.com/hao-ji-xing/cc-weixin):把 Claude Code Agent 接进微信
- [OpenAkita](https://github.com/openakita/openakita/blob/main/docs/im-channels.md):Python Agent 框架的微信适配器,直调 iLink 原生 API
- [openclaw-weixin-sdk](https://github.com/openclaw-java/openclaw-weixin-sdk):Java SDK + CLI + Web Terminal
> 协议跟随情况:2026-04 官方包已迭代到 2.1.x,但 iLink HTTP 协议本体(`getupdates` / `sendmessage` / `getuploadurl` / AES-128-ECB / CDN)未变,第三方适配器无需改动。
## 4. 能力边界总览
下表合并了多个来源的观察,**存在版本差异和放量灰度,标注 ⚠️ 的项建议实测确认**:
| 项 | 状态 | 备注 |
|---|---|---|
| 私聊(1 对 1) | ✅ 支持 | 主要形态 |
| 群聊 | ⚠️ 未开放/存疑 | 协议源码有 `group_id` 字段,但多数资料称群聊未开放 |
| 文本收发 | ✅ | 微信侧不支持 Markdown 渲染,发送时应转纯文本 |
| 图片收发 | ✅ | CDN + AES-128-ECB 加密 |
| 语音 | ⚠️ 入站 ✅ / 出站存疑 | 入站 silk 编码且附 ASR 转写文本;有资料称 bot 暂不能主动发语音,也有适配器实现了语音上传 |
| 文件/视频 | ⚠️ 协议支持 | item type 有定义,CDN 链路完整;早期资料称"仅文本",后续适配器已实现 |
| 主动推送 | ⚠️ 受限 | 需携带入站消息返回的 `context_token`;有资料称超过 24h 窗口后送达不到 |
| 历史消息拉取 | ❌ | 只有游标机制,无历史 API |
| 一个微信号绑定 bot 数 | 1 个 | 一个微信号 = 一个 bot 实例 |
| 客户端版本要求 | iOS ≥ 8.0.70 | 有资料称安卓 ≥ 8.0.70、鸿蒙 ≥ 8.0.16.40 也已支持 ⚠️ |
| 开放状态 | 逐步放量 | 不是所有账号都能看到插件入口 |
## 5. 合规要点(详细条款解读见 06 文档)
- 这是官方通道,正常使用无封号风险——这是它相对 itchat/Wechaty 的核心价值。
- 腾讯保留限速、内容识别、拦截、终止连接的权利(条款 4.7)。
- 腾讯不存储消息内容,但收集 IP、操作日志、设备信息用于安全审计(条款 5.3)。
- 禁止用于绕过微信技术保护、违法违规、危害网络安全等用途(条款 4.6)。
- 腾讯可随时变更/终止服务(条款 7.2)——**核心业务不要单点依赖它,设计降级方案**。
## 6. 与其他 IM 通道的对比(选型参考)
如果你的 AI 项目不只接微信,可以横向比较(数据来自 OpenAkita 的多平台实践):
| 平台 | 接收方式 | 需要公网 IP | 群聊 | 合法性 |
|---|---|---|---|---|
| 微信个人号(iLink) | HTTP 长轮询 | 否 | ⚠️ 未开放 | 官方 |
| Telegram | 长轮询 / Webhook | 否 | ✅ | 官方 |
| 飞书 | WebSocket 长连接 | 否 | ✅ | 官方 |
| 钉钉 | Stream (WebSocket) | 否 | ✅ | 官方 |
| 企业微信 | HTTP 回调 | **是** | ✅ (@机器人) | 官方 |
| QQ 官方机器人 | WebSocket | 否 | ✅ | 官方 |
微信 iLink 的独特优势:用户在**自己的微信主界面**直接和 AI 聊,零安装、零跳转,体验最接近"真人联系人"。独特劣势:仅私聊、主动消息受限、灰度放量。
## 参考资料
- [微信 Bot API 技术解析:腾讯 iLink 协议首次合法开放](https://github.com/hao-ji-xing/openclaw-weixin/blob/main/weixin-bot-api.md)
- [WeChat ClawBot iLink 协议解析 — Charles Li](https://www.chihao.li/blog/zh/wechat-clawbot-ilink-protocol)
- [微信 ClawBot 是什么?OpenClaw 接入微信完整解读](https://xiaoman.lovetalkon.com/blog/openclaw-wechat-ai)
- [OpenAkita IM 通道集成指南](https://github.com/openakita/openakita/blob/main/docs/im-channels.md)
- [openclaw-weixin-sdk (Java) README](https://github.com/openclaw-java/openclaw-weixin-sdk)
+256
View File
@@ -0,0 +1,256 @@
# 02 — iLink 协议完整参考
> 本文档是对 iLink Bot API 的完整整理,供实现协议层时对照查阅。
> 来源:腾讯官方包 `@tencent-weixin/openclaw-weixin` 的逆向分析、Java SDK 实现、多个第三方适配器实践。字段级细节以实测为准。
## 1. 基本信息
| 项 | 值 |
|---|---|
| API 根地址 | `https://ilinkai.weixin.qq.com` |
| CDN 地址 | `https://novac2c.cdn.weixin.qq.com/c2c` |
| 协议 | HTTPS + JSON |
| 收消息模式 | HTTP 长轮询(无 Webhook,无需公网 IP) |
| 鉴权 | `Authorization: Bearer <bot_token>` |
## 2. 端点总表
| Endpoint | Method | 功能 | 需要鉴权 |
|---|---|---|---|
| `/ilink/bot/get_bot_qrcode` | GET | 获取登录二维码(`?bot_type=3`) | 否 |
| `/ilink/bot/get_qrcode_status` | GET | 轮询扫码状态(`?qrcode=xxx`) | 否 |
| `/ilink/bot/getupdates` | POST | 长轮询收消息(核心) | 是 |
| `/ilink/bot/sendmessage` | POST | 发送消息(文本/图片/文件/视频/语音) | 是 |
| `/ilink/bot/getconfig` | POST | 获取配置(typing_ticket 等) | 是 |
| `/ilink/bot/sendtyping` | POST | 发送「正在输入」状态 | 是 |
| `/ilink/bot/getuploadurl` | POST | 获取 CDN 预签名上传地址 | 是 |
## 3. 通用请求头
登录后所有请求的固定头:
```http
Content-Type: application/json
AuthorizationType: ilink_bot_token
Authorization: Bearer <bot_token>
X-WECHAT-UIN: <base64(十进制字符串(随机uint32))>
```
- `X-WECHAT-UIN`:每次请求生成一个随机 uint32 → 转十进制字符串 → base64 编码。每次都不一样,起防重放作用。
- 扫码登录阶段(`get_qrcode_status`)有实现带 `iLink-App-ClientVersion: 1` 头。
## 4. 登录流程(一次性扫码绑定)
### 4.1 时序
```
开发者 iLink 服务器 微信用户
│── GET get_bot_qrcode ──▶│ │
│◀── { qrcode, url } ─────│ │
│ │◀──── 用户扫码 ───────│
│── GET get_qrcode_status ─▶│ (长轮询/定时轮询) │
│◀── { status:"confirmed", │ │
│ bot_token, ... } ──│ │
│
▼ 持久化 bot_token,后续所有请求 Bearer 鉴权
```
### 4.2 获取二维码
```http
GET /ilink/bot/get_bot_qrcode?bot_type=3
```
响应:
```json
{
"qrcode": "xxx",
"qrcode_img_content": "https://liteapp.weixin.qq.com/q/..."
}
```
- `qrcode`:QR 标识符,轮询状态时用
- `qrcode_img_content`:二维码图片 URL,可在终端渲染(如 `qrcode-terminal`)或浏览器打开给用户扫
- `bot_type=3` 是官方包源码中硬编码的值,含义未公开,照抄即可
### 4.3 轮询扫码状态
```http
GET /ilink/bot/get_qrcode_status?qrcode=<qrcode>
```
状态流转:`wait` → `scaned` → `confirmed`(注意官方拼写是 `scaned`)。
确认后响应(关键凭据):
```json
{
"status": "confirmed",
"bot_token": "44b9b8ec@im.bot:060000b7...",
"ilink_bot_id": "44b9b8ec@im.bot",
"ilink_user_id": "o9cq809L...@im.wechat",
"baseurl": "https://ilinkai.weixin.qq.com"
}
```
- **`bot_token` 是之后所有 API 调用的鉴权凭据,一次扫码长期有效**(具体有效期官方未公布,社区观察为长期;过期会收到 errcode=-14,需重新扫码)
- 建议把 `bot_token`、`ilink_bot_id`、`ilink_user_id`、`baseurl` 全部持久化
- 轮询建议间隔 1s,整体超时给 5~8 分钟
## 5. 收消息:长轮询
### 5.1 请求
```http
POST /ilink/bot/getupdates
```
```json
{
"get_updates_buf": "<上次返回的游标,首次为空字符串>",
"base_info": { "channel_version": "1.0.2" }
}
```
### 5.2 响应
```json
{
"ret": 0,
"msgs": [ /* WeixinMessage[],见第 7 节 */ ],
"get_updates_buf": "Ck1...",
"longpolling_timeout_ms": 35000
}
```
### 5.3 机制要点
- 服务器 **hold 住连接最多约 30~35 秒**(`longpolling_timeout_ms` 会告知),有新消息立即返回,无消息超时返回空 `msgs`
- HTTP 客户端超时必须设得比 `longpolling_timeout_ms` 大(建议 +10s 余量)
- **`get_updates_buf` 是同步游标**(类似 Telegram getUpdates 的 offset):
- 每次请求带上次返回的值
- **收到响应后立刻持久化**,否则重启后会重复收消息
- 返回中没有该字段时保持旧值
- HTTP 超时/网络错误视为「本轮无新消息」,直接重试,不要中断循环
### 5.4 失败恢复与退避(来自 Java SDK 的实战策略)
| 情况 | 处理 |
|---|---|
| `errcode = -14`(会话过期 / token 失效) | 该账号进入暂停窗口(SDK 默认 1 小时),期间停止轮询和发送;提示用户重新扫码 |
| 其他失败 | 2s 后快速重试;连续失败 3 次后进入 30s 退避 |
| HTTP 超时 | 视为无新消息,立即进入下一轮 |
| 响应带 `longpolling_timeout_ms` | 动态调整下一轮轮询超时 |
## 6. 发消息
```http
POST /ilink/bot/sendmessage
```
```json
{
"msg": {
"to_user_id": "o9cq809L...@im.wechat",
"client_id": "unique-id-123",
"message_type": 2,
"message_state": 2,
"context_token": "CK3E...",
"item_list": [
{ "type": 1, "text_item": { "text": "你好!我是你的 AI。" } }
]
}
}
```
字段说明:
| 字段 | 说明 |
|---|---|
| `to_user_id` | 收件人,取入站消息的 `from_user_id` |
| `client_id` | 客户端生成的去重 ID(如 UUID),防止重发 |
| `message_type` | `2` = Bot 发出的消息(入站用户消息是 `1`) |
| `message_state` | `2` = FINISH 完成态(非流式)。协议还有 NEW(0?) → GENERATING → FINISH 的三段式状态,可用于流式分片发送(Java SDK 的 `sendTextStream` 即分片发送,但注意**那不是 LLM token 流**,而是把长文本切成多条消息) |
| `context_token` | **必填**,从要回复的那条入站消息原样取出 |
| `item_list` | 消息体,文本见上;媒体 item 构造见 03 文档 |
## 7. 消息结构(WeixinMessage)
入站消息核心字段:
```json
{
"from_user_id": "o9cq800kum_xxx@im.wechat",
"to_user_id": "e06c1ceea05e@im.bot",
"message_type": 1,
"message_state": 2,
"context_token": "AARzJWAFAAABAAAAAAAp...",
"item_list": [
{ "type": 1, "text_item": { "text": "你好" } }
]
}
```
### 7.1 ID 格式规律
- 用户 ID:`xxx@im.wechat`
- Bot ID:`xxx@im.bot`
### 7.2 常量表
| 字段 | 值 | 含义 |
|---|---|---|
| `message_type` | 1 | 用户消息(入站) |
| `message_type` | 2 | Bot 消息(出站) |
| `message_state` | 2 | FINISH(完整消息) |
| `item.type` | 1 | 文本(`text_item.text`) |
| `item.type` | 2 | 图片(CDN 加密存储) |
| `item.type` | 3 | 语音(silk 编码,`voice_item.text` 附 ASR 转写文字) |
| `item.type` | 4 | 文件附件 |
| `item.type` | 5 | 视频 |
> 媒体类 item 的详细字段(CDN 引用、aes_key 等)见 [03-messages-and-media.md](03-messages-and-media.md)。
## 8. context_token:对话关联的核心
整个协议里最关键、最容易踩坑的细节:
1. **每条入站消息都带 `context_token`**,回复时必须原样带上,否则消息无法关联到正确的对话窗口
2. 每条入站消息都应刷新本地保存的 token(Java SDK 的做法:`FileContextTokenStore` 按对话持久化,每条入站消息更新),这样主动发消息时也有最新 token 可用
3. 有资料称:**用户最后一条消息超过 24 小时后,bot 的消息可能送达不到**;在窗口内可用 `context_token` 主动追发消息。做定时推送类功能时必须考虑这个窗口(⚠️ 建议实测确认)
## 9. 输入中状态(typing)
```
POST /ilink/bot/getconfig → 获取 typing_ticket
POST /ilink/bot/sendtyping → 发送「正在输入」
```
用途:AI 后端处理耗时较长时,先发个 typing 让用户知道 bot 在干活。非必需,第一版可以不做。
## 10. 错误处理备忘
| 现象 | 含义 | 处理 |
|---|---|---|
| `errcode = -14` | 会话/token 过期 | 暂停轮询与发送,走重新扫码流程 |
| `ret != 0` | 业务错误 | 记录完整响应,按退避策略重试 |
| HTTP 层超时 | 长轮询正常超时或网络抖动 | 直接重试 |
| 重复收到同一条消息 | `get_updates_buf` 没更新/没持久化 | 检查游标保存逻辑;应用层按消息 ID 去重兜底 |
## 11. 协议层 Checklist(实现自查)
- [ ] 扫码登录:`get_bot_qrcode` → 渲染二维码 → 轮询 `get_qrcode_status` 到 `confirmed`
- [ ] 持久化:`bot_token` / `ilink_bot_id` / `ilink_user_id` / `baseurl` / `get_updates_buf` / 每对话 `context_token`
- [ ] 请求头:`Authorization`、`AuthorizationType`、随机 `X-WECHAT-UIN`
- [ ] 长轮询循环:超时 > 服务端 timeout、游标即时保存、异常不中断
- [ ] 回复带 `context_token`、带唯一 `client_id`
- [ ] errcode=-14 的暂停与重新登录路径
- [ ] 退避重试策略
## 参考资料
- [微信 Bot API 技术解析(含裸调 demo.mjs)](https://github.com/hao-ji-xing/openclaw-weixin/blob/main/weixin-bot-api.md)
- [WeChat ClawBot iLink 协议解析](https://www.chihao.li/blog/zh/wechat-clawbot-ilink-protocol)
- [openclaw-weixin-sdk (Java) — 失败恢复与退避策略一节](https://github.com/openclaw-java/openclaw-weixin-sdk)
- [OpenAkita 微信适配器说明](https://github.com/openakita/openakita/blob/main/docs/im-channels.md)
+160
View File
@@ -0,0 +1,160 @@
# 03 — 消息与媒体处理
> 本文档覆盖:五种消息类型的结构、媒体文件的 CDN 传输、AES-128-ECB 加解密、语音的特殊性。
> 媒体部分的字段级细节来自官方包逆向与第三方适配器实现,标注 ⚠️ 处建议对照官方包源码或实测确认。
## 1. 消息类型速览
| `item.type` | 类型 | 入站 | 出站 | 关键字段 |
|---|---|---|---|---|
| 1 | 文本 | ✅ | ✅ | `text_item.text` |
| 2 | 图片 | ✅ | ✅ | CDN 引用 + aes_key |
| 3 | 语音 | ✅(附转写) | ⚠️ 存疑 | silk 编码,`voice_item.text` 为 ASR 文本 |
| 4 | 文件 | ✅ | ✅ | CDN 引用 + aes_key |
| 5 | 视频 | ✅ | ⚠️ | CDN 引用 + aes_key |
> 关于出站语音/视频:早期协议解析称「仅文本」或「bot 暂不能主动发语音」,但 OpenAkita 等适配器后续实现了图片/文件/语音的 CDN+AES 出站。说明协议能力在逐步开放或官方未完整公开,**以你的账号实测为准**。
## 2. 文本消息
### 2.1 入站
```json
{
"item_list": [
{ "type": 1, "text_item": { "text": "你好" } }
]
}
```
一条消息的 `item_list` 可能含多个 item(图文混排等场景),处理时应遍历而不是只取 `[0]`。
### 2.2 出站
```json
{
"msg": {
"to_user_id": "...@im.wechat",
"client_id": "uuid",
"message_type": 2,
"message_state": 2,
"context_token": "...",
"item_list": [
{ "type": 1, "text_item": { "text": "回复内容" } }
]
}
}
```
注意:
- **微信侧不渲染 Markdown**。如果你的 AI 后端输出 Markdown,发送前应转纯文本(OpenAkita 的做法),或至少把 `**`、`` ` ``、标题符号等剥掉
- 超长文本考虑分片发送(多条 `sendmessage`)。Java SDK 提供 `sendTextStream(...)` 做分片——注意这只是「把长文本切成多条消息」,不是 LLM 流式输出
- 每条消息带唯一 `client_id` 防重
## 3. 媒体传输总流程
微信侧所有媒体文件不直接走 iLink API 的 JSON body,而是走独立 CDN:
```
CDN 域名: https://novac2c.cdn.weixin.qq.com/c2c
加密方式: AES-128-ECB
```
### 3.1 出站(Bot → 用户)完整流程
```
1. 生成随机 AES-128 key(16 字节)
2. plaintext = 原始文件字节
3. encrypted = AES-128-ECB-Encrypt(plaintext, key)
4. POST /ilink/bot/getuploadurl → 拿到 CDN 预签名上传 URL
5. PUT encrypted 到 CDN URL
6. POST /ilink/bot/sendmessage,item 中携带:
- CDN 引用参数(上传后返回/约定的 key)
- aes_key(base64 编码的 16 字节 key),供接收方解密
```
### 3.2 入站(用户 → Bot)完整流程
```
1. getupdates 收到媒体类 item(含 CDN 引用与密钥信息)
2. 从 CDN 下载 encrypted 字节
3. plaintext = AES-128-ECB-Decrypt(encrypted, key)
4. 交给业务层(存盘 / 送多模态模型 / ASR 等)
```
### 3.3 关于解密密钥的来源 ⚠️
- **出站**:key 由发送方(你)随机生成,随消息的 `aes_key` 字段(base64)带给接收方——这部分明确。
- **入站**:OpenAkita 的实现笔记记载「加密密钥从 Token 中提取(base64 解码后取后 16 字节)」。即入站媒体解密密钥与 `bot_token` 派生相关。⚠️ 此细节来源单一,实现时若解密失败,请对照 `@tencent-weixin/openclaw-weixin` 官方包 `src/cdn/` 目录的源码确认 key 的实际派生方式。
### 3.4 AES-128-ECB 实现要点
- ECB 模式无 IV,块大小 16 字节,需要 PKCS#7 padding(具体 padding 方案以官方包源码为准)
- 各语言实现:
- Node.js:`crypto.createCipheriv('aes-128-ecb', key, null)`
- Python:`Crypto.Cipher.AES.new(key, AES.MODE_ECB)`(pycryptodome)
- Java:`Cipher.getInstance("AES/ECB/PKCS5Padding")`
## 4. 语音消息的特殊性
- 入站语音是 **silk 编码**(微信语音专用格式)
- 好消息:`voice_item.text` 直接附带微信侧的 **ASR 转写文字**——如果你的场景只需要文本,直接用转写结果,不必自己解码 silk
- 如果需要原始音频做二次处理(自己的 ASR、声纹等),需 silk 解码。社区方案:
- `pilk`(Python silk 解码绑定)
- 或 silk → wav → ffmpeg 转任意格式
- 出站语音:⚠️ 开放状态存疑(见第 1 节)
## 5. 图片消息
- 入站:CDN 下载 + AES 解密得到原图(jpg/png 等),可直接送多模态模型
- 出站:按 3.1 流程加密上传
- OpenAkita 的实践经验:图片入站后由消息网关自动下载预处理;文件和视频**不自动下载**,由 Agent 按需主动拉取(节省带宽和存储)——这个策略值得借鉴
## 6. 文件与视频
- item type 4(文件)、5(视频),链路同为 CDN + AES
- 建议默认不自动下载大文件,在消息里记录元信息(文件名、大小、CDN 引用),业务层需要时再拉
- 注意给用户反馈:大文件处理慢时先发一条文本「收到文件 xxx,处理中」
## 7. 消息规范化建议(Bridge 内部模型)
不管后端是什么 AI 项目,建议在 Bridge 层把 iLink 消息规范化成统一内部模型,例如:
```json
{
"channel": "wechat-ilink",
"msg_id": "<client_id 或服务端 id>",
"direction": "inbound",
"peer_id": "o9cq809L...@im.wechat",
"context_token": "CK3E...",
"received_at": "2026-08-19T10:00:00Z",
"parts": [
{ "kind": "text", "text": "看这个图" },
{ "kind": "image", "cdn_ref": "...", "local_path": "/tmp/xxx.jpg", "mime": "image/jpeg" },
{ "kind": "voice", "transcript": "语音转写文字", "duration_ms": 3200 }
]
}
```
好处:
- AI 后端不感知 iLink 协议细节,以后加别的 IM 通道(Telegram/飞书等)复用同一模型
- 多 item(图文混排)天然支持
- 媒体下载策略(立即/惰性)在 Bridge 层统一控制
## 8. 常见坑
1. **只取 `item_list[0]`** → 图文混排丢内容。遍历整个 list。
2. **Markdown 直接发** → 用户看到一堆 `*` 和 `#`。发送前转纯文本。
3. **长文本一条发** → 可能被截断或体验差。按字数/段落分片,带不同 `client_id`。
4. **入站媒体密钥搞错** → 解密出一堆乱码还以为是 bug。先确认 key 派生方式(见 3.3)。
5. **语音自己跑 ASR** → 浪费算力,`voice_item.text` 已有微信转写。
6. **大文件自动下载** → 用户发个 100MB 视频你的磁盘/带宽就炸了。惰性下载 + 大小上限。
## 参考资料
- [微信 Bot API 技术解析 — 3.6 媒体文件 AES-128-ECB](https://github.com/hao-ji-xing/openclaw-weixin/blob/main/weixin-bot-api.md)
- [OpenAkita IM 通道指南 — 微信媒体处理与收发矩阵](https://github.com/openakita/openakita/blob/main/docs/im-channels.md)
- [openclaw-weixin-sdk (Java) — MediaUploadService / CdnUploader / CdnMediaDownloader](https://github.com/openclaw-java/openclaw-weixin-sdk)
- 腾讯官方协议包 `src/cdn/` 目录(字段细节的最终依据):`npm pack @tencent-weixin/openclaw-weixin` 下载查看
+169
View File
@@ -0,0 +1,169 @@
# 04 — Bridge(转接器)架构设计指南
> 目标读者:要把微信 ClawBot 接到自己已有 AI 项目的开发者。
> 本文档不绑定具体语言,给出模块划分、状态模型、对接模式和可靠性设计。
## 1. Bridge 的职责边界
Bridge 是一个**薄适配层**,职责只有:
```
iLink 长轮询 ──▶ 消息规范化 ──▶ 调用你的 AI 后端 ──▶ 回复格式化 ──▶ sendmessage
```
Bridge **不负责**:
- 对话历史/记忆管理(那是 AI 后端的事,除非后端无状态)
- 业务逻辑、工具调用编排
- 用户体系、计费
保持薄的好处:协议变了只改 Bridge;AI 后端可以独立演进、独立测试。
## 2. 推荐模块划分
```
bridge/
├── ilink/
│ ├── client.* # HTTP 封装:请求头、鉴权、X-WECHAT-UIN、超时
│ ├── auth.* # 扫码登录流程(get_bot_qrcode / get_qrcode_status)
│ ├── poller.* # 长轮询主循环、游标管理、退避重试
│ ├── sender.* # sendmessage / sendtyping / 分片发送
│ └── media.* # getuploadurl、CDN 上传下载、AES-128-ECB
├── store/
│ ├── account.* # bot_token / ilink_bot_id / baseurl 持久化
│ ├── cursor.* # get_updates_buf 持久化(每次更新立即落盘)
│ └── context.* # peer_id → context_token 映射持久化
├── normalize.* # WeixinMessage → 内部统一消息模型
├── backend.* # 你的 AI 项目的调用封装(HTTP / SDK / 进程)
└── main.* # 装配 + 启动 + 优雅退出
```
参考:官方包 `@tencent-weixin/openclaw-weixin` 的目录结构(`auth/ api/ cdn/ messaging/ monitor/ config/ storage/`)几乎就是这个划分;Java SDK 的门面模式(`OpenClawWeixinSdk` 统一封装 `QrLoginFlowService / WeixinApiClient / WeixinLongPollMonitor / MediaUploadService`)也值得对照。
## 3. 必须持久化的三类状态
| 状态 | 内容 | 更新时机 | 丢失后果 |
|---|---|---|---|
| 账号凭据 | `bot_token`、`ilink_bot_id`、`ilink_user_id`、`baseurl` | 扫码成功时 | 需重新扫码 |
| 同步游标 | `get_updates_buf` | **每次 getupdates 响应后立即** | 重复收到历史消息 |
| 上下文令牌 | `peer_id → context_token` | 每条入站消息 | 无法回复/主动消息送达不到 |
实现建议:
- 单机用 JSON 文件或 SQLite 就够(官方包就是文件存储,Java SDK 用 `FileAccountStore / FileContextTokenStore / FileSyncCursorStore`)
- 游标落盘要**同步写**(或至少 flush),崩溃丢游标 = 重启后消息轰炸
- 文件加个简易锁,防止多开实例写坏状态
## 4. 与 AI 后端的对接模式
按你的 AI 项目形态选一种:
### 模式 A:HTTP API 后端(最常见)
```
poller.onMessage(msg):
reply = http_post("http://127.0.0.1:8000/chat", {
"session_id": msg.peer_id, # 用微信用户 ID 当会话 ID
"text": msg.text,
"attachments": msg.media_paths
})
sender.send_text(msg.peer_id, reply.text, msg.context_token)
```
- 会话 ID 直接映射微信 `from_user_id`,天然一对一私聊
- 后端无状态的话,Bridge 也不存历史;有状态则后端自己管
### 模式 B:本地 SDK / 库调用
Bridge 与 AI 项目同进程(或同语言直接 import),把消息对象传给 Agent 函数,拿回字符串。参考 [cc-weixin](https://github.com/hao-ji-xing/cc-weixin) 把 Claude Agent SDK 的 `query()` 包一层。
### 模式 C:异步任务队列
AI 处理慢(深度研究、图像生成)时:
```
1. 收到消息 → 立即回复「收到,处理中」(可选:sendtyping)
2. 任务入队,worker 处理
3. 完成后用保存的最新 context_token 主动 sendmessage
```
⚠️ 注意 24h 窗口限制(见 06 文档):任务完成后主动推送必须在窗口内才送得到。慢任务一定要设计超时兜底文案。
### 模式 D:多通道网关(进阶)
如果以后还要接 Telegram/飞书/钉钉,把「规范化消息模型 + 统一网关」抽出来,iLink 只是其中一个 adapter。直接参考 [OpenAkita 的架构](https://github.com/openakita/openakita/blob/main/docs/im-channels.md):`Adapter → UnifiedMessage → Gateway → Agent`,发送路径反向。第一版不用做,但内部消息模型(见 03 文档第 7 节)建议一开始就用。
## 5. 可靠性设计
### 5.1 长轮询主循环
伪代码:
```
loop:
try:
resp = getupdates(cursor, timeout=server_timeout + 10s)
if resp.ret == 0:
if resp.get_updates_buf: cursor.save(resp.get_updates_buf)
for msg in resp.msgs: dispatch(msg)
backoff.reset()
elif resp.errcode == -14:
pause_account(1h); notify("需重新扫码")
else:
backoff.retry() # 2s 快速重试,连续 3 次后 30s
except HttpTimeout:
continue # 长轮询正常超时
except NetworkError:
backoff.retry()
```
### 5.2 消息去重
三层兜底:
1. 游标正确持久化(治本)
2. 应用层按消息 ID 去重(`client_id` / 服务端消息 ID,存最近 N 条的 set)
3. 发送侧带唯一 `client_id`(防自己重发)
### 5.3 回复超时
- AI 后端设硬超时(如 60s),超时回「处理超时,请重试」
- 有资料称 24h 窗口外消息送达不到——对「用户发问 → bot 回复」场景这不是问题(立即回复),只影响主动推送
### 5.4 优雅退出
- 捕获 SIGTERM/SIGINT:停 poller → 等正在处理的消息发完 → flush 状态 → 退出
- 不要在 dispatch 中途杀进程,游标已推进但回复没发 = 用户消息被吞
### 5.5 可观测性
- 结构化日志:收消息(peer、类型、长度)、发消息、游标推进、错误码、退避事件
- 关键指标:轮询错误率、-14 出现次数、AI 后端耗时分布
- token、context_token 不要打进日志(打 hash 前缀即可)
## 6. 安全注意
- `bot_token` 等于这个微信 bot 的全部权限:**不入 git、不进日志、文件权限 600**
- 谁扫码谁就是这个 bot 的「主人」,但任何拿到 token 的人都能收发消息——服务器上注意文件权限
- 入站消息内容视为不可信输入:如果 AI 后端有工具调用能力(执行命令、读写文件),提示词注入风险要自己评估
- 建议加白名单:只响应指定 `from_user_id`(比如就你自己),防止别人给 bot 发消息白嫖你的模型 API 额度
## 7. 容量与限制设计
- 微信侧不支持 Markdown:发送前转纯文本
- 长回复分片:按段落或固定字数(如 1500 字)切多条,注意保持顺序
- 媒体大小:入站大图/大文件设上限,超限回复提示而不是硬下载
- AI 后端并发:一个微信号只有一个 bot,但用户可能连发多条——dispatch 建议串行 per-peer(或按 peer 加锁),避免回复乱序
## 8. 测试策略
- 协议层:把 iLink client 抽象成接口,测试时用 mock server 回放抓包 JSON
- 规范化层:构造各类 WeixinMessage JSON(文本/图片/语音/图文混排/多 item)做单元测试
- 端到端:真机扫码,用另一个微信号发消息验证
- 回归重点:游标持久化(重启不重复收)、context_token 传递、-14 恢复流程
## 参考资料
- [openclaw-weixin-sdk 架构图与失败恢复策略](https://github.com/openclaw-java/openclaw-weixin-sdk)
- [OpenAkita 多通道网关架构](https://github.com/openakita/openakita/blob/main/docs/im-channels.md)
- [cc-weixin — 接入 Claude Code Agent 的完整范例](https://github.com/hao-ji-xing/cc-weixin)
+358
View File
@@ -0,0 +1,358 @@
# 05 — 实现指南与代码骨架
> 给出两个可直接改造的最小实现骨架:Node.js(零依赖,官方 demo 的整理版)和 Python(requests 版)。
> 均已按本文档的可靠性建议补强(游标持久化、退避、-14 处理)。媒体收发先略,接入文本跑通后再按 03 文档加。
## 1. 最小流程回顾
```
1. 扫码拿 bot_token(一次性) ← auth
2. loop: ← poller
resp = POST getupdates(cursor)
保存新 cursor
for msg in resp.msgs:
text = msg.item_list[*].text_item.text
reply = 你的AI后端(text) ← backend
POST sendmessage(reply, msg.context_token)
```
## 2. Node.js 骨架(bridge.mjs)
零第三方依赖(Node 18+ 自带 fetch)。二维码展示可加装 `qrcode-terminal`。
```js
// bridge.mjs — 微信 ClawBot iLink Bridge 最小骨架
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { randomBytes } from "node:crypto";
const BASE_URL = "https://ilinkai.weixin.qq.com";
const STATE_FILE = "./bridge-state.json";
// ---------- 状态持久化 ----------
const state = existsSync(STATE_FILE)
? JSON.parse(readFileSync(STATE_FILE, "utf8"))
: { bot_token: null, cursor: "", contexts: {} };
const saveState = () =>
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), { mode: 0o600 });
// ---------- iLink HTTP 封装 ----------
function headers() {
const uin = Buffer.from(String(randomBytes(4).readUInt32BE(0))).toString("base64");
return {
"Content-Type": "application/json",
AuthorizationType: "ilink_bot_token",
"X-WECHAT-UIN": uin,
...(state.bot_token ? { Authorization: `Bearer ${state.bot_token}` } : {}),
};
}
async function apiPost(path, body, timeoutMs = 45_000) {
const res = await fetch(`${BASE_URL}${path}`, {
method: "POST",
headers: headers(),
body: JSON.stringify(body),
signal: AbortSignal.timeout(timeoutMs),
});
return res.json();
}
// ---------- 1. 扫码登录(一次性) ----------
async function login() {
const qr = await fetch(`${BASE_URL}/ilink/bot/get_bot_qrcode?bot_type=3`)
.then((r) => r.json());
console.log("请用微信扫码:", qr.qrcode_img_content);
// 终端显示二维码: npm i qrcode-terminal,然后 qrcode.generate(qr.qrcode_img_content, {small:true})
const deadline = Date.now() + 8 * 60_000;
while (Date.now() < deadline) {
const s = await fetch(
`${BASE_URL}/ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qr.qrcode)}`,
{ headers: { "iLink-App-ClientVersion": "1" } }
).then((r) => r.json());
if (s.status === "confirmed") {
state.bot_token = s.bot_token;
state.bot_id = s.ilink_bot_id;
state.owner_id = s.ilink_user_id;
saveState();
console.log("登录成功, bot:", s.ilink_bot_id);
return;
}
process.stdout.write(`\r等待扫码... status=${s.status} `);
await new Promise((r) => setTimeout(r, 1000));
}
throw new Error("扫码超时,请重试");
}
// ---------- 2. AI 后端调用(换成你的项目) ----------
async function askYourAI(peerId, text) {
// 例:调本地 HTTP 后端
// const r = await fetch("http://127.0.0.1:8000/chat", {
// method: "POST",
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify({ session_id: peerId, text }),
// }).then(r => r.json());
// return r.reply;
return `你说的是:${text}`; // 回声占位
}
// ---------- 3. 发送回复 ----------
async function sendText(toUserId, text, contextToken) {
// 长文本简单分片
for (let i = 0; i < text.length; i += 1500) {
await apiPost("/ilink/bot/sendmessage", {
msg: {
to_user_id: toUserId,
client_id: crypto.randomUUID(),
message_type: 2, // bot 消息
message_state: 2, // FINISH
context_token: contextToken,
item_list: [{ type: 1, text_item: { text: text.slice(i, i + 1500) } }],
},
});
}
}
// ---------- 4. 长轮询主循环 ----------
async function pollLoop() {
let failures = 0;
for (;;) {
try {
const resp = await apiPost("/ilink/bot/getupdates", {
get_updates_buf: state.cursor,
base_info: { channel_version: "1.0.2" },
});
if (resp.errcode === -14) {
console.error("会话过期(errcode=-14),请删除 state 文件重新扫码");
process.exit(2);
}
if (resp.ret !== 0 && resp.errcode !== undefined) {
throw new Error(`getupdates 错误: ${JSON.stringify(resp)}`);
}
if (resp.get_updates_buf) {
state.cursor = resp.get_updates_buf;
saveState(); // 游标立即落盘
}
for (const msg of resp.msgs ?? []) {
if (msg.message_type !== 1) continue; // 只处理用户消息
const text = (msg.item_list ?? [])
.filter((it) => it.type === 1)
.map((it) => it.text_item?.text ?? "")
.join("\n")
.trim();
if (!text) continue; // 媒体消息此处略,按 03 文档扩展
state.contexts[msg.from_user_id] = msg.context_token;
saveState();
try {
const reply = await askYourAI(msg.from_user_id, text);
await sendText(msg.from_user_id, reply, msg.context_token);
} catch (e) {
console.error("处理消息失败:", e);
await sendText(msg.from_user_id, "处理时出错了,请重试", msg.context_token)
.catch(() => {});
}
}
failures = 0;
} catch (e) {
// AbortError(长轮询超时) 视为正常,直接下一轮
if (e.name === "TimeoutError" || e.name === "AbortError") continue;
failures++;
const wait = failures <= 3 ? 2000 : 30_000;
console.error(`轮询异常(${failures}次),${wait / 1000}s 后重试:`, e.message);
await new Promise((r) => setTimeout(r, wait));
}
}
}
// ---------- main ----------
if (!state.bot_token) await login();
await pollLoop();
```
运行:
```bash
node bridge.mjs # 首次会打印二维码链接,微信扫码确认
```
## 3. Python 骨架(bridge.py)
依赖只有 `requests`。二维码展示可加装 `qrcode` + 终端渲染。
```python
# bridge.py — 微信 ClawBot iLink Bridge 最小骨架
import json, os, time, uuid, base64, random, requests
BASE_URL = "https://ilinkai.weixin.qq.com"
STATE_FILE = "./bridge-state.json"
# ---------- 状态持久化 ----------
state = {"bot_token": None, "cursor": "", "contexts": {}}
if os.path.exists(STATE_FILE):
state.update(json.load(open(STATE_FILE)))
def save_state():
with open(STATE_FILE, "w") as f:
json.dump(state, f, indent=2)
os.chmod(STATE_FILE, 0o600)
# ---------- iLink HTTP 封装 ----------
def headers():
h = {
"Content-Type": "application/json",
"AuthorizationType": "ilink_bot_token",
"X-WECHAT-UIN": base64.b64encode(str(random.getrandbits(32)).encode()).decode(),
}
if state["bot_token"]:
h["Authorization"] = f"Bearer {state['bot_token']}"
return h
def api_post(path, body, timeout=45):
r = requests.post(BASE_URL + path, headers=headers(), json=body, timeout=timeout)
return r.json()
# ---------- 1. 扫码登录(一次性) ----------
def login():
qr = requests.get(f"{BASE_URL}/ilink/bot/get_bot_qrcode?bot_type=3").json()
print("请用微信扫码:", qr["qrcode_img_content"])
deadline = time.time() + 8 * 60
while time.time() < deadline:
s = requests.get(
f"{BASE_URL}/ilink/bot/get_qrcode_status",
params={"qrcode": qr["qrcode"]},
headers={"iLink-App-ClientVersion": "1"},
).json()
if s.get("status") == "confirmed":
state.update(bot_token=s["bot_token"], bot_id=s["ilink_bot_id"],
owner_id=s["ilink_user_id"])
save_state()
print("登录成功, bot:", s["ilink_bot_id"])
return
time.sleep(1)
raise RuntimeError("扫码超时")
# ---------- 2. AI 后端调用(换成你的项目) ----------
def ask_your_ai(peer_id: str, text: str) -> str:
# r = requests.post("http://127.0.0.1:8000/chat",
# json={"session_id": peer_id, "text": text}).json()
# return r["reply"]
return f"你说的是:{text}"
# ---------- 3. 发送回复 ----------
def send_text(to_user_id, text, context_token):
for i in range(0, len(text), 1500):
api_post("/ilink/bot/sendmessage", {
"msg": {
"to_user_id": to_user_id,
"client_id": str(uuid.uuid4()),
"message_type": 2,
"message_state": 2,
"context_token": context_token,
"item_list": [{"type": 1, "text_item": {"text": text[i:i+1500]}}],
}
})
# ---------- 4. 长轮询主循环 ----------
def poll_loop():
failures = 0
while True:
try:
resp = api_post("/ilink/bot/getupdates", {
"get_updates_buf": state["cursor"],
"base_info": {"channel_version": "1.0.2"},
})
if resp.get("errcode") == -14:
print("会话过期(errcode=-14),请删除 state 文件重新扫码")
raise SystemExit(2)
if resp.get("ret", 0) != 0:
raise RuntimeError(f"getupdates 错误: {resp}")
if resp.get("get_updates_buf"):
state["cursor"] = resp["get_updates_buf"]
save_state()
for msg in resp.get("msgs") or []:
if msg.get("message_type") != 1:
continue
text = "\n".join(
it.get("text_item", {}).get("text", "")
for it in msg.get("item_list", []) if it.get("type") == 1
).strip()
if not text:
continue
state["contexts"][msg["from_user_id"]] = msg["context_token"]
save_state()
try:
reply = ask_your_ai(msg["from_user_id"], text)
send_text(msg["from_user_id"], reply, msg["context_token"])
except Exception as e:
print("处理消息失败:", e)
try:
send_text(msg["from_user_id"], "处理时出错了,请重试",
msg["context_token"])
except Exception:
pass
failures = 0
except requests.exceptions.Timeout:
continue # 长轮询正常超时
except Exception as e:
failures += 1
wait = 2 if failures <= 3 else 30
print(f"轮询异常({failures}次),{wait}s 后重试:", e)
time.sleep(wait)
if __name__ == "__main__":
if not state["bot_token"]:
login()
poll_loop()
```
## 4. 接入你自己 AI 项目的改造点
只需替换 `askYourAI` / `ask_your_ai` 一个函数:
| 你的后端形态 | 改法 |
|---|---|
| HTTP API(FastAPI/Flask/Express…) | 函数里 POST 过去,`session_id` 传 `peer_id` |
| OpenAI 兼容接口 | 函数里组 messages 调 chat/completions,历史自己存(JSON/SQLite,按 peer_id 分键) |
| LangChain / LangGraph | 函数里调 chain.invoke,memory 用 peer_id 做会话键 |
| 本地 Agent 框架(Claude Agent SDK 等) | 参考 [cc-weixin](https://github.com/hao-ji-xing/cc-weixin) 的 `askClaude()` 包装 |
| 命令行工具 | `subprocess` 调一下取 stdout(注意超时与转义) |
## 5. 常见坑(按踩坑频率排序)
1. **游标没存/存晚了** → 重启后消息重复轰炸。收到响应立刻落盘。
2. **回复忘带 `context_token`** → 消息发不出或关联不上对话。
3. **HTTP 客户端超时 < 长轮询超时** → 每轮都假性超时。骨架里给了 45s。
4. **Markdown 直接发** → 微信不渲染,先转纯文本。
5. **只处理 `item_list[0]`** → 图文混排丢内容。
6. **token 提交进 git** → 泄漏=bot 被接管。state 文件加 `.gitignore`。
7. **消息处理抛异常没兜住** → 整个轮询循环挂掉。骨架里已包 try。
8. **多开实例** → 同 token 多处轮询互相抢游标,状态错乱。单实例跑。
## 6. 下一步扩展路线
1. 文本收发跑通(本骨架)
2. 接入你的 AI 后端 + 会话历史
3. 入站图片/语音(CDN 下载 + AES 解密,见 03 文档)
4. `sendtyping`(处理慢时提升体验)
5. 出站图片/文件(getuploadurl + CDN 上传)
6. 白名单、限流、多通道(按 04 文档模式 D 演进)
## 7. 参考项目清单
| 项目 | 值得借鉴的部分 |
|---|---|
| [hao-ji-xing/openclaw-weixin](https://github.com/hao-ji-xing/openclaw-weixin) | `demo.mjs` 裸调最小实现、协议逆向文档 |
| [hao-ji-xing/cc-weixin](https://github.com/hao-ji-xing/cc-weixin) | Agent SDK 的接入包装方式 |
| [openclaw-java/openclaw-weixin-sdk](https://github.com/openclaw-java/openclaw-weixin-sdk) | 完整工程化:状态存储、退避策略、-14 暂停窗口、typing、媒体服务 |
| [openakita/openakita](https://github.com/openakita/openakita) | Python 适配器、多通道网关抽象、媒体惰性下载策略 |
| `@tencent-weixin/openclaw-weixin`(npm) | 官方协议实现,字段有疑问时的最终依据:`npm pack @tencent-weixin/openclaw-weixin` |
+87
View File
@@ -0,0 +1,87 @@
# 06 — 平台限制、合规条款与 FAQ
## 1. 硬性限制汇总
| 限制 | 说明 | 影响 |
|---|---|---|
| 一个微信号 = 一个 bot 实例 | 扫码绑定关系一对一 | 不能多开 bot;多开轮询会互相抢游标 |
| 仅私聊 | 群聊未开放(协议有 `group_id` 字段,可能未来开放) | 做不了群助手,目前 |
| 无历史消息 API | 只有 `get_updates_buf` 游标 | 掉线期间的消息语义未明确,建议按「可能丢失」设计 |
| 回复时效窗口 ⚠️ | 有资料称用户最后一条消息后 24h 内 bot 回复才送得到 | 主动推送/慢任务受约束;务必实测 |
| 主动消息依赖 context_token | token 随入站消息下发 | 冷启动无法主动找人,得用户先开口 |
| 客户端版本 | iOS ≥ 8.0.70(安卓/鸿蒙版本要求各资料不一) | 用户侧看不到插件=没被放量或版本低 |
| 灰度放量 | 功能逐步开放 | 部分账号没有插件入口,无解,等 |
| 速率限制未公开 | 官方未给数字 | 别做高频轰炸,正常对话节奏不会触发 |
## 2. 官方条款要点(《微信 ClawBot 功能使用条款》)
> 以下为开发者视角的转述,不替代原文。做正式产品前请读条款原文。
- **腾讯只是管道(3.2)**:仅提供信息收发,不存储输入/输出内容,不提供 AI 服务。AI 输出由你负责。
- **腾讯保留控制权(4.7)**:可决定支持接入的 AI 服务类型、范围、收发规模与频率;可对内容与连接进行识别,采取提示、拦截、阻断。
- **数据(5.3)**:消息内容不存储;但 IP、操作记录、设备信息会被收集用于安全审计。
- **禁止行为(4.6)**:绕过/破解微信技术保护措施;违法违规;危害网络与数据安全;侵犯他人权益。
- **服务可终止(7.2)**:腾讯可根据业务发展变更、中断、终止本功能。
**工程含义**:
1. 核心业务不要单点依赖 iLink,留降级通道(如邮件、Telegram bot)
2. 内容合规责任在你(AI 输出的过滤/审核是你的事)
3. 别拿它做营销群发——高频主动消息既是条款雷区也受 24h 窗口限制
## 3. 资料间的矛盾与未证实事项
整理时发现以下说法在不同来源间不一致,使用前先实测:
| 事项 | 说法 A | 说法 B | 建议 |
|---|---|---|---|
| 客户端支持 | 仅 iOS 8.0.70+ | 安卓 ≥8.0.70、鸿蒙 ≥8.0.16.40 也支持 | 以目标用户实测为准 |
| 消息类型 | 仅文本 | 图片/语音/文件/视频均可 | 文本先行,媒体逐个实测 |
| 出站语音 | bot 不能发语音 | 适配器已实现语音上传 | 实测 |
| 群聊 | 未开放 | 协议有 `group_id` 字段 | 按不可用设计 |
| token 有效期 | 长期有效 | 未公布,会 -14 过期 | 实现重扫码路径即可 |
| 24h 回复窗口 | 存在 | 部分资料未提 | 主动推送功能先实测 |
## 4. FAQ
**Q:必须要装 OpenClaw 吗?**
A:不需要。iLink 是标准 HTTP API,官方 npm 包只是封装。直接 `fetch`/`requests` 调即可,见 05 文档骨架。
**Q:需要公网 IP / 域名 / 备案吗?**
A:都不需要。收消息是长轮询(你的程序主动连腾讯服务器),不是 webhook。
**Q:bot_token 会过期吗?过期了怎么办?**
A:官方未公布有效期。`getupdates` 返回 `errcode=-14` 即会话过期,此时应暂停轮询、删除本地凭据、重新走扫码流程。实现时把这条路径做好即可无忧。
**Q:能同时跑多个实例分摊负载吗?**
A:不能。一个微信号绑一个 bot;多个实例同时轮询会互相覆盖游标导致丢消息/重复消息。单实例部署,实例内做并发处理。
**Q:用户发的语音能直接拿到文字吗?**
A:能。语音消息的 `voice_item.text` 附带微信侧的 ASR 转写,大多数场景直接用转写文本即可,不用自己解码 silk。
**Q:AI 处理要几分钟(如深度研究),怎么回?**
A:先立即回一条「收到,处理中」(或 sendtyping),任务完成后用最新 context_token 主动发结果。注意 24h 窗口(见上),超窗消息可能送不到。
**Q:能给别人用吗(多用户 bot)?**
A:技术上任何用户给 bot 发消息你都能收到(`from_user_id` 区分),按 peer 建会话即可。但注意:你的模型 API 成本、内容合规责任、以及腾讯对「信息收发规模」的控制权。建议加白名单。
**Q:和公众号/企业微信比,选哪个?**
A:给自己或小圈子用、要微信原生体验 → ClawBot;要认证主体、群发、关注者体系 → 公众号;企业场景 → 企业微信机器人(见 01 文档对比表)。
**Q:封号风险?**
A:这是官方插件通道,正常使用不按灰产协议处理,封号风险远低于 itchat/Wechaty 方案。但违反条款(营销轰炸、违规内容、超高频)腾讯有权拦截/终止。
**Q:协议字段跟文档对不上怎么办?**
A:最终依据是官方包源码:`npm pack @tencent-weixin/openclaw-weixin` 下载后看 `src/`。协议在 1.0.2 → 2.1.x 间主体未变,但细节以你抓到的版本为准。
## 5. 官方与社区资源
| 资源 | 链接 |
|---|---|
| 官方 CLI 包 | `npm view @tencent-weixin/openclaw-weixin-cli` |
| 官方协议包 | `npm view @tencent-weixin/openclaw-weixin` |
| OpenClaw 文档 | https://docs.openclaw.ai |
| 协议逆向分析 | https://github.com/hao-ji-xing/openclaw-weixin |
| Java SDK | https://github.com/openclaw-java/openclaw-weixin-sdk |
| Python 多通道参考 | https://github.com/openakita/openakita |
| Claude Code 接入示例 | https://github.com/hao-ji-xing/cc-weixin |