# 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` 指向路径是否可写、文件是否落盘