Files
qihuaiflow/docs/wechat-ilink-refactor.md
T
qihu-bot 894eeff65a
CI / gate (push) Has been cancelled
docs: 微信Bot iLink协议重构方案 v1.0
分析微信官方iLink(ClawBot)协议完整能力,对比平台现状,识别5项差距。核心:媒体内联发送(CDN AES-128-ECB加密)替代文本链接降级。分5阶段实施(P0-P2)。
2026-08-15 17:14:07 +08:00

317 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 微信 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`