Protocol documentation for the WeChat OpenClaw iLink messaging channel. This document describes the HTTP JSON API used for WeChat bot communication.
OpenClaw Weixin channel 通过 HTTP JSON API 与微信后端网关 (ilinkai.weixin.qq.com) 通信,实现:
- 扫码登录 — 获取 bot_token
- 长轮询收消息 — getUpdates 模式
- 发送消息 — 文本/图片/视频/文件
- CDN 媒体传输 — AES-128-ECB 加解密
- 输入状态指示 — typing indicator
| 常量 | 值 |
|---|---|
| API Base URL | https://ilinkai.weixin.qq.com |
| CDN Base URL | https://novac2c.cdn.weixin.qq.com/c2c |
| Bot Type (QR登录) | "3" |
| Long-poll 默认超时 | 35,000 ms |
| API 请求默认超时 | 15,000 ms |
| Config 请求超时 | 10,000 ms |
| Session 过期错误码 | -14 |
| Session 暂停时长 | 1 小时 |
所有 POST 请求携带以下头:
Content-Type: application/json
AuthorizationType: ilink_bot_token
Authorization: Bearer <bot_token>
Content-Length: <body字节长度>
X-WECHAT-UIN: <random_uint32的十进制字符串的base64编码>
SKRouteTag: <可选,路由标签>
1. 生成 4 字节随机数
2. 读取为 uint32 big-endian
3. 转为十进制字符串 (如 "3221225472")
4. 对该字符串做 base64 编码
GET {baseUrl}/ilink/bot/get_bot_qrcode?bot_type=3
Headers:
iLink-App-ClientVersion: 1 (仅在 status 轮询时)
SKRouteTag: <可选>
响应:
{
"qrcode": "<qrcode_id>",
"qrcode_img_content": "<qrcode_url_for_scan>"
}qrcode: 二维码标识符,用于后续轮询状态qrcode_img_content: 二维码图片URL,展示给用户扫描
GET {baseUrl}/ilink/bot/get_qrcode_status?qrcode=<qrcode_id>
Headers:
iLink-App-ClientVersion: 1
SKRouteTag: <可选>
响应:
{
"status": "wait" | "scaned" | "confirmed" | "expired",
"bot_token": "<token>", // confirmed 时返回
"ilink_bot_id": "<bot_id>", // confirmed 时返回,如 "hexid@im.bot"
"baseurl": "<api_base_url>", // confirmed 时返回(可能更新)
"ilink_user_id": "<user_id>" // confirmed 时返回,扫码人的用户ID
}状态流转: wait → scaned → confirmed (或 expired)
- 长轮询超时 35s,超时返回
{"status": "wait"} - QR码过期后可重新获取,最多刷新 3 次
- 总等待超时 480s (8分钟)
登录成功后保存:
bot_token— 后续所有API请求的认证令牌ilink_bot_id— 账号标识 (normalized:@→-,.→-)baseurl— 可能更新的API地址ilink_user_id— 绑定的微信用户ID
所有接口均为 POST,路径前缀:{baseUrl}/ilink/bot/
每个请求体包含 base_info: { channel_version: "<版本号>" }
POST {baseUrl}/ilink/bot/getupdates
请求体:
{
"get_updates_buf": "<同步游标,首次传空字符串>",
"base_info": { "channel_version": "2.0.1" }
}
响应:
{
"ret": 0,
"errcode": 0,
"errmsg": "",
"msgs": [WeixinMessage, ...],
"get_updates_buf": "<新游标>",
"longpolling_timeout_ms": 35000
}ret=0成功,非0失败errcode=-14会话过期,需暂停1小时get_updates_buf必须保存,下次请求回传- 客户端超时(AbortError)视为正常,返回空响应重试
- 连续失败3次后 backoff 30s
POST {baseUrl}/ilink/bot/sendmessage
请求体:
{
"msg": {
"from_user_id": "",
"to_user_id": "<目标用户ID>",
"client_id": "<客户端生成的唯一ID>",
"message_type": 2, // BOT=2
"message_state": 2, // FINISH=2
"item_list": [MessageItem, ...],
"context_token": "<从收到的消息中获取>"
},
"base_info": { "channel_version": "2.0.1" }
}
重要: context_token 从收到的用户消息中提取,回复时必须回传。
POST {baseUrl}/ilink/bot/getuploadurl
请求体:
{
"filekey": "<随机16字节hex>",
"media_type": 1, // 1=IMAGE, 2=VIDEO, 3=FILE, 4=VOICE
"to_user_id": "<目标用户ID>",
"rawsize": 12345, // 明文字节大小
"rawfilemd5": "<明文MD5 hex>",
"filesize": 12352, // AES-128-ECB加密后密文大小
"no_need_thumb": true, // 不需要缩略图
"aeskey": "<AES密钥hex>",
"base_info": { ... }
}
响应:
{
"upload_param": "<上传加密参数>",
"thumb_upload_param": "<缩略图上传参数>"
}POST {baseUrl}/ilink/bot/getconfig
请求体:
{
"ilink_user_id": "<用户ID>",
"context_token": "<可选>",
"base_info": { ... }
}
响应:
{
"ret": 0,
"typing_ticket": "<base64编码的typing ticket>"
}POST {baseUrl}/ilink/bot/sendtyping
请求体:
{
"ilink_user_id": "<用户ID>",
"typing_ticket": "<从getConfig获取>",
"status": 1, // 1=正在输入, 2=取消输入
"base_info": { ... }
}
{
seq?: number; // 消息序列号
message_id?: number; // 消息唯一ID
from_user_id?: string; // 发送者ID (如 "xxx@im.wechat")
to_user_id?: string; // 接收者ID
client_id?: string; // 客户端消息ID
create_time_ms?: number; // 创建时间戳(ms)
session_id?: string; // 会话ID
message_type?: number; // 1=USER, 2=BOT
message_state?: number; // 0=NEW, 1=GENERATING, 2=FINISH
item_list?: MessageItem[]; // 消息内容列表
context_token?: string; // 会话上下文令牌(必须回传)
}{
type?: number; // 1=TEXT, 2=IMAGE, 3=VOICE, 4=FILE, 5=VIDEO
text_item?: { text: string };
image_item?: ImageItem;
voice_item?: VoiceItem;
file_item?: FileItem;
video_item?: VideoItem;
ref_msg?: { message_item?: MessageItem; title?: string };
}{
encrypt_query_param?: string; // CDN下载/上传加密参数
aes_key?: string; // base64编码的AES-128密钥
encrypt_type?: number; // 0=只加密fileid, 1=打包缩略图等
}{
media?: CDNMedia;
thumb_media?: CDNMedia;
aeskey?: string; // hex字符串形式的AES key(优先于media.aes_key)
mid_size?: number; // 中图密文大小
hd_size?: number; // 高清密文大小
thumb_size?: number;
thumb_height?: number;
thumb_width?: number;
}{
media?: CDNMedia;
encode_type?: number; // 6=silk
sample_rate?: number;
playtime?: number; // 毫秒
text?: string; // 语音转文字(如有则直接用文字)
}{
media?: CDNMedia;
file_name?: string;
md5?: string;
len?: string; // 字节数(字符串)
}{
media?: CDNMedia;
video_size?: number;
play_length?: number;
video_md5?: string;
thumb_media?: CDNMedia;
thumb_size?: number;
thumb_height?: number;
thumb_width?: number;
}- 算法:AES-128-ECB,PKCS7 填充
- 密文大小计算:
ceil((plaintext_size + 1) / 16) * 16 - 密钥:随机16字节
1. 读取文件 → 计算 rawsize, rawfilemd5(hex)
2. 生成 filekey = random(16).hex()
3. 生成 aeskey = random(16)
4. 计算 filesize = aesEcbPaddedSize(rawsize)
5. 调用 getUploadUrl → 获取 upload_param
6. AES-128-ECB(文件内容, aeskey) → ciphertext
7. POST {cdnBaseUrl}/upload?encrypted_query_param={upload_param}&filekey={filekey}
Content-Type: application/octet-stream
Body: ciphertext
8. 从响应头 x-encrypted-param 获取 downloadParam
9. 构造 CDNMedia: { encrypt_query_param: downloadParam, aes_key: base64(hex(aeskey)) }
1. GET {cdnBaseUrl}/download?encrypted_query_param={encrypt_query_param}
2. 获取密文 bytes
3. 解析 aes_key:
- base64解码后16字节 → 直接用
- base64解码后32字节hex字符串 → hex解码为16字节
4. AES-128-ECB 解密 → 明文
- 上传:
{cdnBaseUrl}/upload?encrypted_query_param={upload_param}&filekey={filekey} - 下载:
{cdnBaseUrl}/download?encrypted_query_param={encrypt_query_param}
- QR Code 登录 — 获取QR码 + 轮询状态 + 保存token
- 长轮询消息接收 — getUpdates 循环,自动重连/退避
- 发送文本消息 — sendMessage
- 发送图片 — 上传CDN + sendMessage(ImageItem)
- 发送视频 — 上传CDN + sendMessage(VideoItem)
- 发送文件 — 上传CDN + sendMessage(FileItem)
- 接收媒体 — 下载CDN + AES解密
- 输入状态 — sendTyping(开始/取消)
- 获取配置 — getConfig(typing_ticket)
- AES-128-ECB 加解密
- X-WECHAT-UIN 生成
- context_token 管理 — 自动追踪每个用户的最新token
- 同步游标管理 — get_updates_buf 持久化
- 会话过期处理 — errcode=-14 自动暂停
- 消息回调/事件 — 收到消息时触发回调
- 多账号支持 — 支持多个微信号同时在线
这是最重要的协议约束,文档中未明确说明:
context_token不是登录时获取的,而是 用户从微信发送第一条消息后,由getUpdates响应中的WeixinMessage.context_token字段携带。- 没有
context_token的sendMessage调用会被服务端接受 (HTTP 200),但 消息不会投递到微信客户端。 - 因此正确的使用流程是:
1. 扫码绑定 → 获取 bot_token
2. 启动 getUpdates 长轮询
3. 等待用户从微信端发送第一条消息(建立会话通道)
4. 从收到的消息中提取 context_token 并缓存
5. 之后发消息时必须携带该 context_token
6. 每次收到新消息都应更新 context_token(可能会变)
get_qrcode_status在confirmed状态时返回ilink_user_id- 这是 扫码人的微信用户ID(格式:
xxx@im.wechat) - 可以直接用作后续
sendMessage的to_user_id - 但仍需等待该用户发来消息获取
context_token后才能真正投递
[扫码绑定] [微信用户]
| |
|-- get_bot_qrcode ------------> |
|<-------------- 扫码确认 -------|
| (获得 bot_token, |
| ilink_user_id) |
| |
|-- 启动 getUpdates 长轮询 ----> |
| |
| *** 此时无法发消息给微信 *** |
| |
|<-------- 用户发来第一条消息 ----|
| (获得 context_token) |
| |
| *** 会话通道已建立 *** |
| |
|-- sendMessage + context_token |
| -----> 微信收到消息 -->|
| |
|<-------- 用户回复消息 ---------|
| (更新 context_token) |
- 微信用户ID:
xxx@im.wechat - Bot ID:
xxx@im.bot(normalized:xxx-im-bot)