177 lines
6.3 KiB
Markdown
177 lines
6.3 KiB
Markdown
# CDN 图片下载接口实现说明(WechatHookBot)
|
||
|
||
本文档说明当前项目中“通过 CDN 参数下载图片”的真实实现路径、参数来源、容错策略与缓存行为,便于排障和二次开发。
|
||
|
||
## 1. 结论概览
|
||
|
||
- 当前项目的核心 CDN 下载接口是 `HttpClient.cdn_download_image(...)`。
|
||
- 实际请求的 Hook API 端点是 `POST /api/cdn_download`。
|
||
- 下载成功判定条件是响应中 `errCode == 1`。
|
||
- 上层(`WechatHookClient`)提供了统一入口 `download_wechat_media(...)`,并在图片场景支持:
|
||
- 从消息 XML 自动提取 `fileid/aeskey`;
|
||
- 已知 `file_id + aes_key` 直接下载;
|
||
- 原图失败后回退缩略图。
|
||
|
||
---
|
||
|
||
## 2. 关键代码位置
|
||
|
||
- 低层 HTTP 下载实现:`WechatHookBot/WechatHook/http_client.py:752`
|
||
- 统一媒体下载入口:`WechatHookBot/WechatHook/client.py:1232`
|
||
- XML 解析 + CDN 下载(原图/缩略图回退):`WechatHookBot/WechatHook/client.py:1352`
|
||
- 直接 CDN 参数下载(`file_id + aes_key`):`WechatHookBot/WechatHook/client.py:1452`
|
||
- 图片处理器封装(下载后转 base64):`WechatHookBot/utils/image_processor.py:255`
|
||
|
||
---
|
||
|
||
## 3. 调用链(从插件到 Hook API)
|
||
|
||
常见链路如下:
|
||
|
||
1. 插件层(如 `AIChat`、`GrokVideo`)拿到图片消息或引用消息里的 CDN 参数。
|
||
2. 调用 `ImageProcessor.download_image(...)` 或 `ImageProcessor.download_image_by_cdn(...)`。
|
||
3. `ImageProcessor` 调 `bot.download_wechat_media("image", ...)`(`bot` 即 `WechatHookClient`)。
|
||
4. `WechatHookClient` 根据参数分发到:
|
||
- `download_image(message, save_path)`(从 XML 提取参数)
|
||
- 或 `download_image_by_cdn(file_id, aes_key, save_path)`
|
||
5. 最终都进入 `HttpClient.cdn_download_image(...)`。
|
||
6. `HttpClient` 发起 `POST /api/cdn_download` 到 Hook 端。
|
||
|
||
---
|
||
|
||
## 4. `POST /api/cdn_download` 请求细节
|
||
|
||
`HttpClient.cdn_download_image(...)` 组装的请求体(关键字段):
|
||
|
||
- `fileid`: CDN 文件标识
|
||
- `asekey`: AES 密钥
|
||
- `imgType`: 图片类型(`1=原图`, `2=缩略图`)
|
||
- `out`: 本地保存路径
|
||
|
||
注意:
|
||
|
||
- 字段名是 `asekey`(不是 `aeskey`),这是按 Hook API 的实际参数约定来的。
|
||
- 代码位置:`WechatHookBot/WechatHook/http_client.py:773`。
|
||
|
||
成功判定:
|
||
|
||
- 当响应存在且 `errCode == 1` 时判定成功并返回 `save_path`。
|
||
- 代码位置:`WechatHookBot/WechatHook/http_client.py:801`。
|
||
|
||
---
|
||
|
||
## 5. 参数来源与提取策略
|
||
|
||
### 5.1 从图片消息 XML 自动提取
|
||
|
||
`WechatHookClient.download_image(...)` 会解析 `message["Content"]` 的 XML:
|
||
|
||
- 原图 fileid 候选顺序:
|
||
- `cdnbigimgurl`
|
||
- `cdnmidimgurl`
|
||
- `cdnhdimgurl`
|
||
- `fileid`
|
||
- aeskey:`aeskey`
|
||
- 缩略图参数:
|
||
- `cdnthumburl`
|
||
- `cdnthumbaeskey`(缺失时回退 `aeskey`)
|
||
|
||
代码位置:`WechatHookBot/WechatHook/client.py:1386`、`WechatHookBot/WechatHook/client.py:1393`。
|
||
|
||
### 5.2 已知 CDN 参数直接下载
|
||
|
||
调用 `download_image_by_cdn(file_id, aes_key, save_path, ...)` 时,不解析 XML,直接走 CDN 下载。
|
||
|
||
代码位置:`WechatHookBot/WechatHook/client.py:1452`。
|
||
|
||
---
|
||
|
||
## 6. 失败回退与重试机制
|
||
|
||
### 6.1 上层回退(原图 -> 缩略图)
|
||
|
||
- 在 `download_image(...)` 中,先尝试 `imgType=1` 原图。
|
||
- 失败后尝试 `imgType=2` 缩略图。
|
||
- 代码位置:`WechatHookBot/WechatHook/client.py:1405`、`WechatHookBot/WechatHook/client.py:1424`。
|
||
|
||
### 6.2 网络重试
|
||
|
||
- `HttpClient.cdn_download_image(...)` 对 `httpx.ConnectError` 最多重试 2 次(总计最多 3 次尝试)。
|
||
- 重试间隔为 `0.2 * (attempt + 1)` 秒。
|
||
- 代码位置:`WechatHookBot/WechatHook/http_client.py:787`、`WechatHookBot/WechatHook/http_client.py:806`。
|
||
|
||
### 6.3 下载完成确认
|
||
|
||
- 上层会轮询文件是否存在且大小 `> 0`,避免“接口返回成功但文件尚未落盘”的时序问题。
|
||
- 轮询次数 20 次、每次 0.5 秒。
|
||
- 代码位置:`WechatHookBot/WechatHook/client.py:1416`、`WechatHookBot/WechatHook/client.py:1489`。
|
||
|
||
---
|
||
|
||
## 7. 缓存策略(两层)
|
||
|
||
### 7.1 WechatHookClient 文件缓存(磁盘)
|
||
|
||
- 路径:`WechatHookBot/temp/wechat_media_cache/`
|
||
- TTL:3600 秒(1 小时)
|
||
- 缓存 key 包含媒体类型 + `msg_id` 或 `cdn:file_id:aes_key`,再做 SHA1。
|
||
- 同 key 使用 `asyncio.Lock` 防并发重复下载。
|
||
|
||
代码位置:
|
||
|
||
- 初始化:`WechatHookBot/WechatHook/client.py:52`
|
||
- key 构建:`WechatHookBot/WechatHook/client.py:1151`
|
||
- TTL 校验:`WechatHookBot/WechatHook/client.py:1187`
|
||
|
||
### 7.2 ImageProcessor 的 Redis base64 缓存(可选)
|
||
|
||
- `download_image(...)`(消息图)默认 `use_cache=True`,可用 `image:{msgId}` 读写缓存。
|
||
- `download_image_by_cdn(...)` 默认 `use_cache=False`,只有显式开启才会用 `image:cdn:{file_id}`。
|
||
- Redis 媒体缓存默认 TTL 常见为 900 秒(调用处指定)。
|
||
|
||
代码位置:
|
||
|
||
- 消息图缓存:`WechatHookBot/utils/image_processor.py:188`
|
||
- CDN 图缓存 key:`WechatHookBot/utils/image_processor.py:281`
|
||
- Redis 媒体缓存接口:`WechatHookBot/utils/redis_cache.py:668`
|
||
|
||
---
|
||
|
||
## 8. 并发与节流行为
|
||
|
||
`HttpClient` 对 Hook API 使用全局串行信号量:
|
||
|
||
- `self._hook_request_semaphore = asyncio.Semaphore(1)`
|
||
- 这意味着同一时刻只有一个 Hook HTTP 请求在飞行中(包括 CDN 下载)。
|
||
|
||
代码位置:`WechatHookBot/WechatHook/http_client.py:37`。
|
||
|
||
影响:
|
||
|
||
- 高并发场景下更稳定,但单次吞吐会受限。
|
||
- 日志可能出现“Hook API 排队中,等待串行执行”。
|
||
|
||
---
|
||
|
||
## 9. 兼容接口说明
|
||
|
||
`WechatHookClient` 仍保留了旧风格 `cdn_init/cdn_download/cdn_upload` 兼容方法,但在新协议里不推荐使用:
|
||
|
||
- `cdn_init()`:直接返回成功(无需初始化)
|
||
- `cdn_download()`:提示不可用,建议改用 `download_image/download_video`
|
||
|
||
代码位置:`WechatHookBot/WechatHook/client.py:430`。
|
||
|
||
---
|
||
|
||
## 10. 实际开发建议
|
||
|
||
- 插件中优先使用 `bot.download_wechat_media("image", ...)` 作为统一入口,不要直接拼 `/api/cdn_download`。
|
||
- 如果已有完整消息对象(含 XML),优先传 `message`,让框架自动处理原图/缩略图回退。
|
||
- 如果只有 `file_id + aes_key`,调用 `download_image_by_cdn(...)`。
|
||
- 排障时先看三项:
|
||
- XML 中是否真的有 `cdnbigimgurl/aeskey`
|
||
- Hook 返回是否 `errCode == 1`
|
||
- 本地 `out` 指向路径是否可写、文件是否落盘
|
||
|