docs: weixin ClawBot bridge protocol and examples
This commit is contained in:
commit
4ea3957da9
13 files changed
+2158
No files matched your search
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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` 下载查看
|
||||
@@ -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)
|
||||
@@ -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` |
|
||||
@@ -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 |
|
||||
Reference in new issue
Block a user