6.3 KiB
6.3 KiB
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直接下载; - 原图失败后回退缩略图。
- 从消息 XML 自动提取
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)
常见链路如下:
- 插件层(如
AIChat、GrokVideo)拿到图片消息或引用消息里的 CDN 参数。 - 调用
ImageProcessor.download_image(...)或ImageProcessor.download_image_by_cdn(...)。 ImageProcessor调bot.download_wechat_media("image", ...)(bot即WechatHookClient)。WechatHookClient根据参数分发到:download_image(message, save_path)(从 XML 提取参数)- 或
download_image_by_cdn(file_id, aes_key, save_path)
- 最终都进入
HttpClient.cdn_download_image(...)。 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 候选顺序:
cdnbigimgurlcdnmidimgurlcdnhdimgurlfileid
- aeskey:
aeskey - 缩略图参数:
cdnthumburlcdnthumbaeskey(缺失时回退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指向路径是否可写、文件是否落盘
- XML 中是否真的有