分析微信官方iLink(ClawBot)协议完整能力,对比平台现状,识别5项差距。核心:媒体内联发送(CDN AES-128-ECB加密)替代文本链接降级。分5阶段实施(P0-P2)。
This commit is contained in:
1 parent
d2d133f22a
commit
894eeff65a
1 file changed
+316
@@ -0,0 +1,316 @@
|
||||
# 微信 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`):
|
||||
```python
|
||||
def _wechat_send_media(...):
|
||||
# iLink 对 type=2/5 媒体 item 返回 ret=-2 invalid arguments
|
||||
# 降级为纯文本链接
|
||||
caption = "🖼️ 图片已生成,点击查看:" + relay_url
|
||||
return _wechat_send_message(...) # 发文本
|
||||
```
|
||||
|
||||
**根本原因**:iLink 媒体消息不是直接传 URL,而是需要走 **CDN 上传加密链路**:
|
||||
1. 下载 AGNES 生成的图片字节
|
||||
2. 用 `getuploadurl` 获取微信 CDN 上传地址
|
||||
3. **AES-128-ECB 加密**图片字节
|
||||
4. 上传到 `novac2c.cdn.weixin.qq.com`
|
||||
5. 发消息时带 `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`:
|
||||
```python
|
||||
"""
|
||||
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`**:
|
||||
```python
|
||||
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 正在输入
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
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 增强
|
||||
|
||||
```python
|
||||
def _mark_wechat_expired(account_id):
|
||||
"""增强:标记 expired 的同时,主动给用户发站内通知/邮件提醒重扫"""
|
||||
# 1. 标记 expired + 清 token(现有逻辑)
|
||||
# 2. 停掉接收线程
|
||||
# 3. 触发站内通知:'您的微信 Bot 会话已过期,请重新扫码绑定'
|
||||
# 4. 记录 audit log
|
||||
```
|
||||
|
||||
#### Phase 5(P2):接收侧媒体解密
|
||||
|
||||
```python
|
||||
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|download` |
|
||||
| 错误码 | `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`
|
||||
Reference in new issue
Block a user