docs: 修正 iLink 扫码登录——get_bot_qrcode 为 POST+local_token_list、iLink-App-Id/ClientVersion 头、完整状态机、码TTL≈60s与'换码竞态'两坑(实测+官方源码)
This commit is contained in:
1 parent
6bc0827b82
commit
38b776705d
2 files changed
+95
-20
No files matched your search
@@ -23,12 +23,14 @@
|
||||
|
||||
- 接入域名:`https://ilinkai.weixin.qq.com`
|
||||
- CDN 域名:`https://novac2c.cdn.weixin.qq.com/c2c`
|
||||
- 核心端点:`get_bot_qrcode` / `get_qrcode_status`(登录)、`getupdates`(长轮询收)、`sendmessage`(发)、`getconfig` / `sendtyping`(输入中)、`getuploadurl`(媒体上传)
|
||||
- 鉴权:`Authorization: Bearer <bot_token>`,token 由扫码登录获得,长期有效
|
||||
- 三个最容易踩的坑:
|
||||
- 核心端点:`get_bot_qrcode`(扫码登录,**POST + body `{"local_token_list":[]}`**,非裸 GET)、`get_qrcode_status`(轮询状态)、`getupdates`(长轮询收)、`sendmessage`(发)、`getconfig` / `sendtyping`(输入中)、`getuploadurl`(媒体上传)
|
||||
- 鉴权:`Authorization: Bearer <bot_token>`,token 由扫码登录获得,长期有效;登录请求还带 `iLink-App-Id: bot` + `iLink-App-ClientVersion`(**数字编码**如 `132104`,非字符串 `"1"`),见 02 §3
|
||||
- 五个最容易踩的坑:
|
||||
1. `get_updates_buf` 游标必须持久化并每次更新,否则重复收消息
|
||||
2. 回复必须带原消息的 `context_token`,否则关联不到对话
|
||||
3. 媒体文件全部是 AES-128-ECB 加密传输,密钥处理方式见 03 文档
|
||||
4. **扫码二维码服务端 TTL ≈ 60 秒**——必须"生成→扫"在窗口内,或自动换新码(见 02 §4.5)
|
||||
5. **已扫码未确认时绝不能再换码**——换码会砍掉确认会话,微信端显示成功但后端永远收不到 `confirmed`(见 02 §4.5「换码竞态」,本机实测最痛的坑)
|
||||
|
||||
## 典型接入架构(Bridge 模式)
|
||||
|
||||
|
||||
+90
-17
@@ -3,6 +3,8 @@
|
||||
> 本文档是对 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. 基本信息
|
||||
|
||||
| 项 | 值 |
|
||||
@@ -17,8 +19,8 @@
|
||||
|
||||
| Endpoint | Method | 功能 | 需要鉴权 |
|
||||
|---|---|---|---|
|
||||
| `/ilink/bot/get_bot_qrcode` | GET | 获取登录二维码(`?bot_type=3`) | 否 |
|
||||
| `/ilink/bot/get_qrcode_status` | GET | 轮询扫码状态(`?qrcode=xxx`) | 否 |
|
||||
| `/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 等) | 是 |
|
||||
@@ -34,23 +36,33 @@ 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 编码。每次都不一样,起防重放作用。
|
||||
- 扫码登录阶段(`get_qrcode_status`)有实现带 `iLink-App-ClientVersion: 1` 头。
|
||||
- **`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 服务器 微信用户
|
||||
│── GET get_bot_qrcode ──▶│ │
|
||||
│◀── { qrcode, url } ─────│ │
|
||||
│ │◀──── 用户扫码 ───────│
|
||||
│── POST get_bot_qrcode ─▶│ │
|
||||
│ body {local_token_list:[]} │
|
||||
│◀── { qrcode, qrcode_img_content } ─┘ │
|
||||
│ │
|
||||
│ │◀──── 用户扫码 ─────│
|
||||
│── GET get_qrcode_status ─▶│ (长轮询/定时轮询) │
|
||||
│◀── { status:"confirmed", │ │
|
||||
│ bot_token, ... } ──│ │
|
||||
│◀── { status:"scaned", ... }──┘ │
|
||||
│── GET get_qrcode_status ─▶│ (扫码成功后继续等) │
|
||||
│◀── { status:"confirmed", bot_token, ... }──┘ │
|
||||
│
|
||||
▼ 持久化 bot_token,后续所有请求 Bearer 鉴权
|
||||
```
|
||||
@@ -58,29 +70,52 @@ X-WECHAT-UIN: <base64(十进制字符串(随机uint32))>
|
||||
### 4.2 获取二维码
|
||||
|
||||
```http
|
||||
GET /ilink/bot/get_bot_qrcode?bot_type=3
|
||||
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/..."
|
||||
"qrcode_img_content": "https://liteapp.weixin.qq.com/q/7GiQu1?qrcode=<qrcode>&bot_type=3"
|
||||
}
|
||||
```
|
||||
|
||||
- `qrcode`:QR 标识符,轮询状态时用
|
||||
- `qrcode_img_content`:二维码图片 URL,可在终端渲染(如 `qrcode-terminal`)或浏览器打开给用户扫
|
||||
- `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=<用户输入的数字串>
|
||||
```
|
||||
|
||||
状态流转:`wait` → `scaned` → `confirmed`(注意官方拼写是 `scaned`)。
|
||||
状态流转(**完整集合**):
|
||||
|
||||
| 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` 换新码(注意「换码竞态」) |
|
||||
|
||||
确认后响应(关键凭据):
|
||||
|
||||
@@ -96,7 +131,42 @@ GET /ilink/bot/get_qrcode_status?qrcode=<qrcode>
|
||||
|
||||
- **`bot_token` 是之后所有 API 调用的鉴权凭据,一次扫码长期有效**(具体有效期官方未公布,社区观察为长期;过期会收到 errcode=-14,需重新扫码)
|
||||
- 建议把 `bot_token`、`ilink_bot_id`、`ilink_user_id`、`baseurl` 全部持久化
|
||||
- 轮询建议间隔 1s,整体超时给 5~8 分钟
|
||||
- 轮询建议间隔 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. 收消息:长轮询
|
||||
|
||||
@@ -240,9 +310,11 @@ POST /ilink/bot/sendtyping → 发送「正在输入」
|
||||
|
||||
## 11. 协议层 Checklist(实现自查)
|
||||
|
||||
- [ ] 扫码登录:`get_bot_qrcode` → 渲染二维码 → 轮询 `get_qrcode_status` 到 `confirmed`
|
||||
- [ ] 扫码登录:`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`
|
||||
- [ ] 请求头:`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 的暂停与重新登录路径
|
||||
@@ -254,3 +326,4 @@ POST /ilink/bot/sendtyping → 发送「正在输入」
|
||||
- [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)
|
||||
Reference in new issue
Block a user