6.9 KiB
6.9 KiB
03 — 消息与媒体处理
本文档覆盖:五种消息类型的结构、媒体文件的 CDN 传输、AES-128-ECB 加解密、语音的特殊性。 媒体部分的字段级细节来自官方包逆向与第三方适配器实现,标注 ⚠️ 处建议对照官方包源码或实测确认。
1. 消息类型速览
item.type |
类型 | 入站 | 出站 | 关键字段 |
|---|---|---|---|---|
| 1 | 文本 | ✅ | ✅ | text_item.text |
| 2 | 图片 | ✅ | ✅ | CDN 引用 + aes_key |
| 3 | 语音 | ✅(附转写) | ⚠️ 存疑 | silk 编码,voice_item.text 为 ASR 文本 |
| 4 | 文件 | ✅ | ✅ | CDN 引用 + aes_key |
| 5 | 视频 | ✅ | ⚠️ | CDN 引用 + aes_key |
关于出站语音/视频:早期协议解析称「仅文本」或「bot 暂不能主动发语音」,但 OpenAkita 等适配器后续实现了图片/文件/语音的 CDN+AES 出站。说明协议能力在逐步开放或官方未完整公开,以你的账号实测为准。
2. 文本消息
2.1 入站
{
"item_list": [
{ "type": 1, "text_item": { "text": "你好" } }
]
}
一条消息的 item_list 可能含多个 item(图文混排等场景),处理时应遍历而不是只取 [0]。
2.2 出站
{
"msg": {
"to_user_id": "...@im.wechat",
"client_id": "uuid",
"message_type": 2,
"message_state": 2,
"context_token": "...",
"item_list": [
{ "type": 1, "text_item": { "text": "回复内容" } }
]
}
}
注意:
- 微信侧不渲染 Markdown。如果你的 AI 后端输出 Markdown,发送前应转纯文本(OpenAkita 的做法),或至少把
**、`、标题符号等剥掉 - 超长文本考虑分片发送(多条
sendmessage)。Java SDK 提供sendTextStream(...)做分片——注意这只是「把长文本切成多条消息」,不是 LLM 流式输出 - 每条消息带唯一
client_id防重
3. 媒体传输总流程
微信侧所有媒体文件不直接走 iLink API 的 JSON body,而是走独立 CDN:
CDN 域名: https://novac2c.cdn.weixin.qq.com/c2c
加密方式: AES-128-ECB
3.1 出站(Bot → 用户)完整流程
1. 生成随机 AES-128 key(16 字节)
2. plaintext = 原始文件字节
3. encrypted = AES-128-ECB-Encrypt(plaintext, key)
4. POST /ilink/bot/getuploadurl → 拿到 CDN 预签名上传 URL
5. PUT encrypted 到 CDN URL
6. POST /ilink/bot/sendmessage,item 中携带:
- CDN 引用参数(上传后返回/约定的 key)
- aes_key(base64 编码的 16 字节 key),供接收方解密
3.2 入站(用户 → Bot)完整流程
1. getupdates 收到媒体类 item(含 CDN 引用与密钥信息)
2. 从 CDN 下载 encrypted 字节
3. plaintext = AES-128-ECB-Decrypt(encrypted, key)
4. 交给业务层(存盘 / 送多模态模型 / ASR 等)
3.3 关于解密密钥的来源 ⚠️
- 出站:key 由发送方(你)随机生成,随消息的
aes_key字段(base64)带给接收方——这部分明确。 - 入站:OpenAkita 的实现笔记记载「加密密钥从 Token 中提取(base64 解码后取后 16 字节)」。即入站媒体解密密钥与
bot_token派生相关。⚠️ 此细节来源单一,实现时若解密失败,请对照@tencent-weixin/openclaw-weixin官方包src/cdn/目录的源码确认 key 的实际派生方式。
3.4 AES-128-ECB 实现要点
- ECB 模式无 IV,块大小 16 字节,需要 PKCS#7 padding(具体 padding 方案以官方包源码为准)
- 各语言实现:
- Node.js:
crypto.createCipheriv('aes-128-ecb', key, null) - Python:
Crypto.Cipher.AES.new(key, AES.MODE_ECB)(pycryptodome) - Java:
Cipher.getInstance("AES/ECB/PKCS5Padding")
- Node.js:
4. 语音消息的特殊性
- 入站语音是 silk 编码(微信语音专用格式)
- 好消息:
voice_item.text直接附带微信侧的 ASR 转写文字——如果你的场景只需要文本,直接用转写结果,不必自己解码 silk - 如果需要原始音频做二次处理(自己的 ASR、声纹等),需 silk 解码。社区方案:
pilk(Python silk 解码绑定)- 或 silk → wav → ffmpeg 转任意格式
- 出站语音:⚠️ 开放状态存疑(见第 1 节)
5. 图片消息
- 入站:CDN 下载 + AES 解密得到原图(jpg/png 等),可直接送多模态模型
- 出站:按 3.1 流程加密上传
- OpenAkita 的实践经验:图片入站后由消息网关自动下载预处理;文件和视频不自动下载,由 Agent 按需主动拉取(节省带宽和存储)——这个策略值得借鉴
6. 文件与视频
- item type 4(文件)、5(视频),链路同为 CDN + AES
- 建议默认不自动下载大文件,在消息里记录元信息(文件名、大小、CDN 引用),业务层需要时再拉
- 注意给用户反馈:大文件处理慢时先发一条文本「收到文件 xxx,处理中」
7. 消息规范化建议(Bridge 内部模型)
不管后端是什么 AI 项目,建议在 Bridge 层把 iLink 消息规范化成统一内部模型,例如:
{
"channel": "wechat-ilink",
"msg_id": "<client_id 或服务端 id>",
"direction": "inbound",
"peer_id": "o9cq809L...@im.wechat",
"context_token": "CK3E...",
"received_at": "2026-08-19T10:00:00Z",
"parts": [
{ "kind": "text", "text": "看这个图" },
{ "kind": "image", "cdn_ref": "...", "local_path": "/tmp/xxx.jpg", "mime": "image/jpeg" },
{ "kind": "voice", "transcript": "语音转写文字", "duration_ms": 3200 }
]
}
好处:
- AI 后端不感知 iLink 协议细节,以后加别的 IM 通道(Telegram/飞书等)复用同一模型
- 多 item(图文混排)天然支持
- 媒体下载策略(立即/惰性)在 Bridge 层统一控制
8. 常见坑
- 只取
item_list[0]→ 图文混排丢内容。遍历整个 list。 - Markdown 直接发 → 用户看到一堆
*和#。发送前转纯文本。 - 长文本一条发 → 可能被截断或体验差。按字数/段落分片,带不同
client_id。 - 入站媒体密钥搞错 → 解密出一堆乱码还以为是 bug。先确认 key 派生方式(见 3.3)。
- 语音自己跑 ASR → 浪费算力,
voice_item.text已有微信转写。 - 大文件自动下载 → 用户发个 100MB 视频你的磁盘/带宽就炸了。惰性下载 + 大小上限。
参考资料
- 微信 Bot API 技术解析 — 3.6 媒体文件 AES-128-ECB
- OpenAkita IM 通道指南 — 微信媒体处理与收发矩阵
- openclaw-weixin-sdk (Java) — MediaUploadService / CdnUploader / CdnMediaDownloader
- 腾讯官方协议包
src/cdn/目录(字段细节的最终依据):npm pack @tencent-weixin/openclaw-weixin下载查看