Files
weixin_ClawBot_Bridge/docs/02-ilink-protocol.md
T

330 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 02 — iLink 协议完整参考
> 本文档是对 iLink Bot API 的完整整理,供实现协议层时对照查阅。
> 来源:腾讯官方包 `@tencent-weixin/openclaw-weixin` 的逆向分析、Java SDK 实现、多个第三方适配器实践。字段级细节以实测为准。
> ⚠️ **2026-09-08 实测 + 官方源码修正**:本节 `get_bot_qrcode`(官方为 **POST + `local_token_list`**,非裸 GET)、登录请求头(官方还带 **`iLink-App-Id`** 与 **`iLink-App-ClientVersion`** 数字编码)、扫码状态机(不止 `wait/scaned/confirmed`)、以及两大实战坑(**码服务端 TTL ≈60s**、**换码竞态**)均已按腾讯官方包 `@tencent-weixin/openclaw-weixin@2.4.8`(`src/auth/login-qr.ts`、`src/api/api.ts`)源码 + 本仓库真机绑定实测修订。此前把它们写成裸 GET / 字符串 `"1"` / 三项状态,均已作废。
## 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` | **POST**(`?bot_type=3`) | 获取登录二维码(**官方带 body `{"local_token_list":[...]}`**;裸 GET 也能拿到码但非官方姿势) | 否 |
| `/ilink/bot/get_qrcode_status` | GET | 轮询扫码状态(`?qrcode=xxx`,可带 `?verify_code=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))>
iLink-App-Id: bot
iLink-App-ClientVersion: <数字编码>
```
- `X-WECHAT-UIN`:每次请求生成一个随机 uint32 → 转十进制字符串 → base64 编码。每次都不一样,起防重放作用。
- **`iLink-App-Id`**:固定值 `"bot"`(取自官方包 `package.json` 的 `ilink_appid`)。
- **`iLink-App-ClientVersion`**:是**数字**,不是字符串 `"1"`!按官方 `buildClientVersion` 编码:`(major&0xff)<<16 | (minor&0xff)<<8 | (patch&0xff)`。例:`2.4.8` → `0x020408` = **`132104`**。
- `get_bot_qrcode` / `get_qrcode_status`(预绑定阶段,尚未有 token)不带 `Authorization`,但**仍带** `iLink-App-Id` / `iLink-App-ClientVersion` / `Content-Type` / `AuthorizationType` / `X-WECHAT-UIN`。
- 可选 `SKRouteTag` 头(来自 `openclaw.json` 的 `channels.<id>.routeTag`,数字/字符串,可缺省;实测不会影响拿码内容)。
## 4. 登录流程(一次性扫码绑定)
> ⚠️ 本节为实测 + 官方源码修正版。**两大实战坑**(码 TTL / 换码竞态)见 4.5,务必读完再写登录循环。
### 4.1 时序
```
开发者 iLink 服务器 微信用户
│── POST get_bot_qrcode ─▶│ │
│ body {local_token_list:[]} │
│◀── { qrcode, qrcode_img_content } ─┘ │
│ │
│ │◀──── 用户扫码 ─────│
│── GET get_qrcode_status ─▶│ (长轮询/定时轮询) │
│◀── { status:"scaned", ... }──┘ │
│── GET get_qrcode_status ─▶│ (扫码成功后继续等) │
│◀── { status:"confirmed", bot_token, ... }──┘ │
│
▼ 持久化 bot_token,后续所有请求 Bearer 鉴权
```
### 4.2 获取二维码
```http
POST /ilink/bot/get_bot_qrcode?bot_type=3
Content-Type: application/json
iLink-App-Id: bot
iLink-App-ClientVersion: 132104
```
```json
{ "local_token_list": [] }
```
- `local_token_list`:本实例**已登录的 bot token 列表**(最多 10 个,取自本地已持久化的账号)。**首次绑定传空数组 `[]`**(不要传 `null`);若已有已绑定的 bot,传其 token 列表,官方据此识别(可触发 `binded_redirect`,见 4.4)。
- **不要单独依赖裸 GET**:官方源码用 `POST + local_token_list`(`apiPostFetch`)。裸 GET 也能 HTTP 200 拿到码,但**不是官方绑定姿势**。实测两者返回的 `qrcode_img_content` 结构一致。
响应:
```json
{
"qrcode": "xxx",
"qrcode_img_content": "https://liteapp.weixin.qq.com/q/7GiQu1?qrcode=<qrcode>&bot_type=3"
}
```
- `qrcode`:QR 标识符(32 位 hex),轮询状态时用的**参数**。
- `qrcode_img_content`:**一个 liteapp 跳转 URL**(`liteapp.weixin.qq.com/q/...`),**不是一张可直接 `<img src>` 的图片**。要拿它**作为二维码内容**用二维码库编码(`qrcode-terminal` / `qrcode.toDataURL`),微信扫开后会在微信内打开该 liteapp 页完成确认。注意该 URL **内嵌了 `qrcode` 参数**,别把 `qrcode` 当独立二维码去扫(会扫出一串纯文本)。
- `bot_type=3` 是官方包源码中硬编码的值,含义未公开,照抄即可。
### 4.3 轮询扫码状态
```http
GET /ilink/bot/get_qrcode_status?qrcode=<qrcode>
# 若服务端要求配对码:
GET /ilink/bot/get_qrcode_status?qrcode=<qrcode>&verify_code=<用户输入的数字串>
```
状态流转(**完整集合**):
| status | 含义 | 处理 |
|---|---|---|
| `wait` | 还没被扫 | 继续轮询 |
| `scaned` | **已被微信扫**,正在确认 | 继续等(重要:此时**绝不换码**,见 4.5) |
| `scaned_but_redirect` | 扫码后 IDC 重定向,要切换 `redirect_host` | 把后续轮询 base URL 改为 `https://<redirect_host>`,继续 |
| `need_verifycode` | 要用户输入手机微信显示的数字 | 提示用户输数字,把 `verify_code` 传回;输错重输 |
| `verify_code_blocked` | 配对码多次输入错误被封 | 结束本轮,重新开始(再次 `get_bot_qrcode`) |
| `binded_redirect` | 该 bot 已绑定过本实例 | 视为**已连接**(等价成功,无需新凭据;保留本地已有 token) |
| `confirmed` | **绑定成功** | 取 `bot_token` / `ilink_bot_id` / `ilink_user_id` / `baseurl`,持久化 |
| `expired` | **码已过期**(服务端判定) | 重新 `get_bot_qrcode` 换新码(注意「换码竞态」) |
确认后响应(关键凭据):
```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 分钟;轮询请求带 `iLink-App-Id` / `iLink-App-ClientVersion` 头,可作为长轮询 hold(官方 35s)处理。
### 4.4 `get_qrcode_status` 状态机总结
```
wait ──(用户扫)──▶ scaned ──(确认)──▶ confirmed
│ │
│ └─(IDC重定向)─▶ scaned_but_redirect ──▶ 切 host 继续
└─(要求数字)─▶ need_verifycode ──(输对)──▶ scaned
└─(输错×N)──▶ verify_code_blocked ──▶ 重新开始
confirmed 的另一形态:binded_redirect(已绑过,直接成功)
任何时刻:expired(服务端把码作废,需换新码)
```
### 4.5 两大实战坑(务必读)
**(1) 码的服务端 TTL ≈ 60 秒**(实测:生成后约 67s 服务端自己返回 `expired`)。
- 不是客户端 `5 分钟` session 超时,而是**服务端给二维码本身的寿命**。
- 用户必须**在生成后约 60s 内**用微信扫码。若超过窗口,「生成 → 扫」之间已过期。
- 对策:要么把码**做成"持续新鲜"**(前端每 ~30~45s 自动换新码 + 倒计时),要么**保证扫码发生在码生成后的 60s 窗口内**。做长期展示必须自动刷新。
**(2) 「换码竞态」——已扫码未确认时换码会砍会话**(本项目实测踩坑最久的一条,务必避开)。
- 现象:用户在微信扫码后**尚未确认**时,若再次 **`get_bot_qrcode` 换新码并 cancel 旧轮询**,旧码的「扫码 → 确认」会话被**彻底丢弃**——微信端显示"连接成功",但后端轮询的是**另一个码**,永远收不到 `confirmed`。
- 根因:二维码绑定是一个「码 → 会话」的一次性流程,**换码即续一个新的、独立的会话**,旧会话的确认事件不会出现在新码上。
- **对策**:**一旦 `get_qrcode_status` 返回 `scaned`(或 `scaned_but_redirect` / `need_verifycode`),就绝不再换码**,一直轮询到 `confirmed` / `expired` 为止;换码只允许在「码处于 `wait`(未扫)且已超过其 TTL」时发生。前端自动刷新码也一样——**已扫到的码必须"冻结"**,禁止把它换掉。
### 4.6 一次性扫码绑定 Checklist
- [ ] `get_bot_qrcode` 用 **POST** + body `{"local_token_list":[]}` + 头 `iLink-App-Id: bot` / `iLink-App-ClientVersion:<数字>`
- [ ] 用 `qrcode_img_content`(liteapp URL)**作为二维码内容**编码后给用户扫,勿把 `qrcode` 直接当二维码
- [ ] 轮询 `get_qrcode_status` 处理**全部**状态:`wait/scaned/scaned_but_redirect/need_verifycode/verify_code_blocked/binded_redirect/confirmed/expired`
- [ ] 若收到 `scaned_but_redirect`,把后续轮询切到 `https://<redirect_host>`
- [ ] 若收到 `need_verifycode`,提示用户输数字,用 `verify_code` 回传
- [ ] **`scaned` 后绝不换码**,等 `confirmed` / `expired`
- [ ] 码过期(`expired`)→ 换新码;但换码不砍掉已扫会话(见 4.5)
- [ ] 持久化 `bot_token` / `ilink_bot_id` / `ilink_user_id` / `baseurl`
## 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`(**POST + local_token_list**)→ 编码 `qrcode_img_content` → 轮询 `get_qrcode_status` 到 `confirmed`
- [ ] 持久化:`bot_token` / `ilink_bot_id` / `ilink_user_id` / `baseurl` / `get_updates_buf` / 每对话 `context_token`
- [ ] 请求头:`Authorization`、`AuthorizationType`、随机 `X-WECHAT-UIN`、`iLink-App-Id`、`iLink-App-ClientVersion`(数字)
- [ ] 扫码状态机:处理 `scaned` / `scaned_but_redirect` / `need_verifycode` / `verify_code_blocked` / `binded_redirect` / `expired`
- [ ] **`scaned` 后不换码**;码 TTL ~60s,需保证"生成 → 扫"在窗口内(或自动换新码但不砍已扫会话)
- [ ] 长轮询循环:超时 > 服务端 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)
- [腾讯官方包 `@tencent-weixin/openclaw-weixin`(npm 2.4.8,`src/auth/login-qr.ts` + `src/api/api.ts` 为最权威协议源)](https://www.npmjs.com/package/@tencent-weixin/openclaw-weixin)