17 KiB
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. 通用请求头
登录后所有请求的固定头:
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 获取二维码
POST /ilink/bot/get_bot_qrcode?bot_type=3
Content-Type: application/json
iLink-App-Id: bot
iLink-App-ClientVersion: 132104
{ "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结构一致。
响应:
{
"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 轮询扫码状态
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 换新码(注意「换码竞态」) |
确认后响应(关键凭据):
{
"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 内用微信扫码。若超过窗口,「生成 → 扫」之间已过期。
- 对策:要么把码做成"持续新鲜"(前端每
3045s 自动换新码 + 倒计时),要么保证扫码发生在码生成后的 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 请求
POST /ilink/bot/getupdates
{
"get_updates_buf": "<上次返回的游标,首次为空字符串>",
"base_info": { "channel_version": "1.0.2" }
}
5.2 响应
{
"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. 发消息
POST /ilink/bot/sendmessage
{
"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)
入站消息核心字段:
{
"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。
8. context_token:对话关联的核心
整个协议里最关键、最容易踩坑的细节:
- 每条入站消息都带
context_token,回复时必须原样带上,否则消息无法关联到正确的对话窗口 - 每条入站消息都应刷新本地保存的 token(Java SDK 的做法:
FileContextTokenStore按对话持久化,每条入站消息更新),这样主动发消息时也有最新 token 可用 - 有资料称:用户最后一条消息超过 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 的暂停与重新登录路径
- 退避重试策略