161 lines
6.9 KiB
Markdown
161 lines
6.9 KiB
Markdown
# 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 入站
|
||
|
||
```json
|
||
{
|
||
"item_list": [
|
||
{ "type": 1, "text_item": { "text": "你好" } }
|
||
]
|
||
}
|
||
```
|
||
|
||
一条消息的 `item_list` 可能含多个 item(图文混排等场景),处理时应遍历而不是只取 `[0]`。
|
||
|
||
### 2.2 出站
|
||
|
||
```json
|
||
{
|
||
"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")`
|
||
|
||
## 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 消息规范化成统一内部模型,例如:
|
||
|
||
```json
|
||
{
|
||
"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. 常见坑
|
||
|
||
1. **只取 `item_list[0]`** → 图文混排丢内容。遍历整个 list。
|
||
2. **Markdown 直接发** → 用户看到一堆 `*` 和 `#`。发送前转纯文本。
|
||
3. **长文本一条发** → 可能被截断或体验差。按字数/段落分片,带不同 `client_id`。
|
||
4. **入站媒体密钥搞错** → 解密出一堆乱码还以为是 bug。先确认 key 派生方式(见 3.3)。
|
||
5. **语音自己跑 ASR** → 浪费算力,`voice_item.text` 已有微信转写。
|
||
6. **大文件自动下载** → 用户发个 100MB 视频你的磁盘/带宽就炸了。惰性下载 + 大小上限。
|
||
|
||
## 参考资料
|
||
|
||
- [微信 Bot API 技术解析 — 3.6 媒体文件 AES-128-ECB](https://github.com/hao-ji-xing/openclaw-weixin/blob/main/weixin-bot-api.md)
|
||
- [OpenAkita IM 通道指南 — 微信媒体处理与收发矩阵](https://github.com/openakita/openakita/blob/main/docs/im-channels.md)
|
||
- [openclaw-weixin-sdk (Java) — MediaUploadService / CdnUploader / CdnMediaDownloader](https://github.com/openclaw-java/openclaw-weixin-sdk)
|
||
- 腾讯官方协议包 `src/cdn/` 目录(字段细节的最终依据):`npm pack @tencent-weixin/openclaw-weixin` 下载查看
|