Files
weixin_ClawBot_Bridge/docs/03-messages-and-media.md

6.9 KiB
Raw Permalink Blame History

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")

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. 常见坑

  1. 只取 item_list[0] → 图文混排丢内容。遍历整个 list。
  2. Markdown 直接发 → 用户看到一堆 * 和 #。发送前转纯文本。
  3. 长文本一条发 → 可能被截断或体验差。按字数/段落分片,带不同 client_id。
  4. 入站媒体密钥搞错 → 解密出一堆乱码还以为是 bug。先确认 key 派生方式(见 3.3)。
  5. 语音自己跑 ASR → 浪费算力,voice_item.text 已有微信转写。
  6. 大文件自动下载 → 用户发个 100MB 视频你的磁盘/带宽就炸了。惰性下载 + 大小上限。

参考资料