CI / gate (push) Has been cancelled
分析微信官方iLink(ClawBot)协议完整能力,对比平台现状,识别5项差距。核心:媒体内联发送(CDN AES-128-ECB加密)替代文本链接降级。分5阶段实施(P0-P2)。
13 KiB
13 KiB
微信 Bot iLink 协议重构方案
版本: v1.0 | 日期: 2026-08-15 | 作者: 七虎AI 技术团队 背景: 参考微信官方 iLink (ClawBot) 协议完整解析,对平台现有个人微信 Bot 实现做差距分析并重构 关联:
im_bot_user.py(前台用户级)、admin/im_bot_admin.py(后台平台级)、im_llm.py(EngineCore 薄壳)
一、协议背景:微信官方 iLink (ClawBot)
1.1 定位
iLink(智联)是腾讯微信 2026 年官方开放的个人微信机器人通信协议,对外产品名 ClawBot。这是唯一合规的个人微信 Bot 通道,有《微信 ClawBot 功能使用条款》法律背书。
| 维度 | 逆向 iPad/PC Hook(旧灰产) | iLink Bot API(官方合规) |
|---|---|---|
| 合规性 | 违反微信用户协议 | 官方开放,签署使用条款 |
| 封号风险 | 极高,大号易永久封禁 | 正常使用无封号风险 |
| 协议稳定性 | 微信升级即失效 | 服务端 API,版本可控 |
| 登录方式 | 模拟设备/劫持会话 | 微信扫码授权,临时 token |
| 通信方式 | 模拟心跳,触发风控 | 长轮询,无需公网 IP |
1.2 核心架构(三层)
上层(逻辑): 业务代码 —— 消息收发、AI 应答、工具调用
中层(文件): CDN 媒体加解密 —— AES-128-ECB + PKCS7
底层(通道): iLink 长轮询 —— 消息通道(ilinkai.weixin.qq.com)
两个核心域名:
| 域名 | 用途 |
|---|---|
https://ilinkai.weixin.qq.com |
所有 API 请求 |
https://novac2c.cdn.weixin.qq.com/c2c |
图片/语音/视频媒体文件 |
1.3 七个 API 端点
| 端点 | 方法 | 功能 | 关键参数 |
|---|---|---|---|
/ilink/bot/get_bot_qrcode |
GET | 获取登录二维码 | ?bot_type=3 |
/ilink/bot/get_qrcode_status |
GET | 轮询扫码状态 | ?qrcode=xxx |
/ilink/bot/getupdates |
POST | 长轮询收消息(35s hold) | { get_updates_buf } |
/ilink/bot/sendmessage |
POST | 发送消息 | { msg: { context_token, item_list } } |
/ilink/bot/getuploadurl |
POST | 获取 CDN 上传地址 | { filekey, md5, len } |
/ilink/bot/getconfig |
POST | 获取配置 | { base_info } |
/ilink/bot/sendtyping |
POST | 发送"正在输入"状态 | { to_user_id, typing_ticket } |
1.4 鉴权机制
Authorization: Bearer {bot_token} # 扫码登录获取
AuthorizationType: ilink_bot_token # 固定值
X-WECHAT-UIN: {随机uint32 → base64} # 每次请求换新,防重放攻击
1.5 消息机制
- 长轮询:
get_updates_buf是游标/位置书签,每次请求必须用返回的新值替换旧值,否则重复收消息死循环 - context_token:每条消息的"身份证",回复必须原样回带,否则不关联到正确对话窗口
- 不能主动推送:必须由用户先发消息触发
- 消息类型:
type 1文本 /2图片 /3语音 /4文件 /5视频 - 语音自带 ASR:VoiceItem 含
text字段,微信已做好语音转文字
二、现状分析
2.1 已实现能力(im_bot_user.py)
| 能力 | 实现 | 状态 |
|---|---|---|
| 扫码登录 | get_bot_qrcode + get_qrcode_status 轮询 |
✅ 完整 |
| 长轮询收消息 | getupdates + get_updates_buf 游标推进 |
✅ 完整 |
| 发消息 | sendmessage + context_token 回带 |
✅ 完整 |
| 鉴权 | ilink_bot_token + X-WECHAT-UIN 随机防重放 |
✅ 完整 |
| 文本分段 | WECHAT_TEXT_MAX_LEN=1500 超长分段 |
✅ 完整 |
| 会话过期检测 | ret=-14/-2 区分过期/参数错误 |
✅ 完整 |
| 用户级隔离 | 前台用户各自绑定,im_bot_wechat_account 按 user_id 隔离 |
✅ 完整 |
| token 持久化 | bot_token/ilink_bot_id 存 PG |
✅ 完整 |
| 对话历史 | _log_chat + 镜像到 AI 聊天窗 |
✅ 完整 |
| 异常日志 | 接收器异常写 /tmp/wechat_receiver_err.log |
✅ 完整 |
| 双 Claw 单引擎 | im_llm.py 薄壳复用 EngineCore |
✅ 完整 |
2.2 差距分析(核心问题)
| # | 能力 | 当前状态 | 影响 | 优先级 |
|---|---|---|---|---|
| 1 | 媒体内联发送 | ❌ 降级为纯文本链接(🖼️ 图片已生成,点击查看:URL) |
图片/视频不能内联显示,用户体验差 | 🔴 P0 |
| 2 | sendtyping | ❌ 未实现 | AI 回复等待期无"正在输入"提示 | 🟡 P1 |
| 3 | 语音消息 | ❌ 未提取 VoiceItem ASR text | 语音消息无法应答 | 🟡 P1 |
| 4 | Session Guard 增强 | ⚠️ -14 仅标记 expired,无主动通知 |
会话过期用户无感知,需手动发现重扫 | 🟡 P1 |
| 5 | CDN 下载解密 | ❌ 未实现(收图片/语音/文件无法解密) | 无法处理用户发来的图片/文件 | 🟢 P2 |
2.3 核心痛点详述:媒体内联发送
当前实现(_wechat_send_media):
def _wechat_send_media(...):
# iLink 对 type=2/5 媒体 item 返回 ret=-2 invalid arguments
# 降级为纯文本链接
caption = "🖼️ 图片已生成,点击查看:" + relay_url
return _wechat_send_message(...) # 发文本
根本原因:iLink 媒体消息不是直接传 URL,而是需要走 CDN 上传加密链路:
- 下载 AGNES 生成的图片字节
- 用
getuploadurl获取微信 CDN 上传地址 - AES-128-ECB 加密图片字节
- 上传到
novac2c.cdn.weixin.qq.com - 发消息时带
download_param+aes_key(type=2 图片 / type=5 视频)
当前代码缺少整个 CDN 加密链路,所以只能降级为文本链接。
三、重构方案
3.1 总体设计
用户微信 ──(文本/图片/语音)──> iLink getupdates
│
▼
┌── im_bot_user.py 接收循环 ──┐
│ 提取 text / VoiceItem ASR │
│ (P1) 语音转文字 │
└──────────┬─────────────────┘
▼
EngineCore._send_message_core
(双 Claw 单引擎,复用 LLM/MCP)
│
┌──────────────┼──────────────┐
▼ ▼
文本回复 媒体回复(__media__)
│ │
│ ┌──────────────┴──────────────┐
│ │ (P0) 媒体内联发送链路 │
│ │ 1. 下载 AGNES 图片字节 │
│ │ 2. getuploadurl 拿地址 │
│ │ 3. AES-128-ECB 加密 │
│ │ 4. 上传微信 CDN │
│ │ 5. sendmessage type=2/5 │
│ └──────────────┬──────────────┘
▼ ▼
sendmessage (文本) sendmessage (媒体 item)
+ (P1) sendtyping 前置
3.2 分阶段实施
Phase 1(P0):媒体内联发送 —— 核心
目标:图片/视频真正内联显示在微信,替代当前文本链接降级。
新增模块 app/api/im_ilink_media.py:
"""
iLink CDN 媒体加解密 —— 参考微信官方 iLink 协议
核心:AES-128-ECB + PKCS7 加密上传,下载解密
"""
import hashlib, os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
def _aes_ecb_encrypt(plaintext: bytes, key: bytes) -> bytes:
"""AES-128-ECB + PKCS7 加密"""
padder = padding.PKCS7(128).padder()
padded = padder.update(plaintext) + padder.finalize()
cipher = Cipher(algorithms.AES(key), modes.ECB())
enc = cipher.encryptor()
return enc.update(padded) + enc.finalize()
def _aes_ecb_decrypt(ciphertext: bytes, key: bytes) -> bytes:
"""AES-128-ECB 解密"""
cipher = Cipher(algorithms.AES(key), modes.ECB())
dec = cipher.decryptor()
padded = dec.update(ciphertext) + dec.finalize()
unpadder = padding.PKCS7(128).unpadder()
return unpadder.update(padded) + unpadder.finalize()
def upload_media_to_wechat_cdn(bot_token: str, baseurl: str, media_bytes: bytes, filekey: str) -> dict:
"""六步上传流程:
1. 计算 MD5
2. 生成随机 AES-128 key (16字节)
3. POST /ilink/bot/getuploadurl → upload_param
4. AES-128-ECB 加密文件
5. POST novac2c.cdn.weixin.qq.com/c2c/upload
6. 返回 download_param + aes_key
"""
md5 = hashlib.md5(media_bytes).hexdigest()
aes_key = os.urandom(16)
# ... getuploadurl 调用
# ... 加密 + 上传
return {"download_param": "...", "aes_key": aes_key.hex()}
修改 _wechat_send_media:
def _wechat_send_media(baseurl, bot_token, to_user_id, context_token, media_type, url):
# 1. 下载 AGNES 图片字节(走 HK 代理,trust_env=False)
media_bytes = _download_media(url)
# 2. 上传微信 CDN + AES 加密
cdn = upload_media_to_wechat_cdn(bot_token, baseurl, media_bytes, filekey)
# 3. 发媒体消息(type=2 图片 / type=5 视频)
item = {
"type": 2 if media_type == "image" else 5,
"image_item": { # 或 video_item
"media": cdn["download_param"],
"aes_key": cdn["aes_key"],
}
}
return _wechat_post_msg(baseurl, bot_token, to_user_id, context_token, [item])
依赖:cryptography 库(需加入 requirements.txt)
Phase 2(P1):sendtyping 正在输入
def _wechat_send_typing(baseurl, bot_token, to_user_id, typing_ticket):
"""发消息前先发'正在输入'状态,改善等待体验"""
return _post(baseurl + "/ilink/bot/sendtyping",
{"to_user_id": to_user_id, "typing_ticket": typing_ticket})
# 接收循环中,调用 _qihua_reply 前:
_wechat_send_typing(baseurl, bot_token, sender, typing_ticket)
typing_ticket从getconfig获取,按用户缓存 24h
Phase 3(P1):语音消息 ASR
def _wechat_extract_text(msg):
"""扩展:支持语音消息自带 ASR 结果"""
for it in (msg.get("item_list") or []):
if it.get("type") == 1: # 文本
...
elif it.get("type") == 3: # 语音
voice = it.get("voice_item", {})
if voice.get("text"): # 微信已做好语音转文字
parts.append(voice["text"])
...
Phase 4(P1):Session Guard 增强
def _mark_wechat_expired(account_id):
"""增强:标记 expired 的同时,主动给用户发站内通知/邮件提醒重扫"""
# 1. 标记 expired + 清 token(现有逻辑)
# 2. 停掉接收线程
# 3. 触发站内通知:'您的微信 Bot 会话已过期,请重新扫码绑定'
# 4. 记录 audit log
Phase 5(P2):接收侧媒体解密
def _download_wechat_media(msg):
"""接收用户发来的图片/语音/文件:
1. GET novac2c.cdn.weixin.qq.com/c2c/download?encrypted_query_param=xxx
2. 用消息里的 aes_key 解密
"""
3.3 技术要点
| 要点 | 说明 |
|---|---|
| AES 模式 | AES-128-ECB(微信指定,非标准 CBC;一次性随机 key + HTTPS 保证安全) |
| 密钥长度 | 16 字节(128-bit) |
| 填充 | PKCS7 |
| 密文大小 | ceil((plaintext_size+1)/16)*16 |
| 媒体域名 | `novac2c.cdn.weixin.qq.com/c2c/upload |
| 错误码 | 0成功 / -14会话过期 / -2参数错误 |
3.4 风险与规避
| 风险 | 规避 |
|---|---|
| ECB 模式理论不安全 | 每文件一次性随机 key + HTTPS,风险可控 |
| 媒体上传超时 | 大图/视频走异步,超时重试 + 降级文本链接兜底 |
cryptography 依赖 |
需在容器 venv 安装,纳入 requirements.txt |
| iLink 媒体协议细节不明确 | 保留文本链接降级 fallback,逐步灰度 |
四、实施计划
| Phase | 内容 | 优先级 | 预估工作量 | 依赖 |
|---|---|---|---|---|
| P0 | 媒体内联发送(CDN AES 加密) | 🔴 高 | 2-3 天 | cryptography 库 |
| P1 | sendtyping 正在输入 | 🟡 中 | 0.5 天 | 无 |
| P1 | 语音 ASR | 🟡 中 | 0.5 天 | 无 |
| P1 | Session Guard 增强 | 🟡 中 | 1 天 | 站内通知 |
| P2 | 接收侧媒体解密 | 🟢 低 | 2 天 | P0 经验复用 |
五、验收标准
- P0:微信里生成的图片/视频内联显示,不再是文本链接
- P1:AI 回复前微信显示"正在输入"
- P1:发语音消息能正确应答(ASR 文本进入 LLM)
- P1:会话过期时用户收到明确重扫通知
- 双 Claw 单引擎不回归(
tests/test_dual_claw.py绿) - pytest + vitest 全绿
六、参考资料
- 微信官方 iLink (ClawBot) 协议(CSDN: 2401_88147261 / HiWangWenBing / bytenote)
- OpenClaw 框架
@tencent-weixin/openclaw-weixin增强:Session Guard / Config Cache / SILK 转码 / CDN 加解密 - 平台现状代码:
app/api/im_bot_user.py、app/api/admin/im_bot_admin.py、app/api/im_llm.py