# 原始 CDN 下载实现指南(给第三方框架) 这份文档只讲“原始 Hook 协议怎么下图”,不依赖本项目的 `WechatHookClient` 或 `ImageProcessor` 封装。 ## 1) 接口结论(按本仓库实际代码验证) - 接口:`POST /api/cdn_download` - 请求体字段: - `fileid`:CDN 文件标识 - `asekey`:AES 密钥(注意字段名是 `asekey`,不是 `aeskey`) - `imgType`:`1` 原图,`2` 缩略图 - `out`:本地保存路径(Hook 所在机器可写路径) - 成功判定:响应 JSON 中 `errCode == 1` 已在代码中看到的依据: - `WechatHookBot/WechatHook/http_client.py:752` - `WechatHookBot/WechatHook/http_client.py:775` - `WechatHookBot/WechatHook/http_client.py:793` - `WechatHookBot/WechatHook/http_client.py:801` --- ## 2) 参数从哪里来(消息 XML) 图片消息里一般在 `` 标签: - 原图 fileid 候选:`cdnbigimgurl` / `cdnmidimgurl` / `cdnhdimgurl` / `fileid` - 原图 key:`aeskey` - 缩略图 fileid:`cdnthumburl` - 缩略图 key:`cdnthumbaeskey`(若没有可回退 `aeskey`) 本仓库提取逻辑: - `WechatHookBot/WechatHook/client.py:1386` - `WechatHookBot/WechatHook/client.py:1393` --- ## 3) 你朋友可直接照抄的实现流程 1. 解析微信消息 XML,拿到: - 原图:`fileid = cdnbigimgurl(或候选)`,`aeskey` - 缩略图:`thumb_fileid = cdnthumburl`,`thumb_key = cdnthumbaeskey or aeskey` 2. 先调用一次原图:`imgType=1` 3. 如果失败,再调用缩略图:`imgType=2` 4. 成功后检查 `out` 文件存在且大小 `> 0` 推荐这么做的原因:有些消息原图拉不到,但缩略图能拉到。 --- ## 4) 最小请求示例(curl) ```bash curl -X POST "http://127.0.0.1:8888/api/cdn_download" \ -H "Content-Type: application/json" \ -d '{ "fileid": "", "asekey": "", "imgType": 1, "out": "D:/temp/wx_img_001.jpg" }' ``` --- ## 5) Python 最小实现(可直接给朋友) ```python import os import requests import xml.etree.ElementTree as ET def parse_img_xml(xml_text: str): root = ET.fromstring(xml_text) img = root.find(".//img") if img is None: raise ValueError("xml里没有标签") fileid = ( img.get("cdnbigimgurl", "") or img.get("cdnmidimgurl", "") or img.get("cdnhdimgurl", "") or img.get("fileid", "") ) aeskey = img.get("aeskey", "") thumb_fileid = img.get("cdnthumburl", "") thumb_key = img.get("cdnthumbaeskey", "") or aeskey return fileid, aeskey, thumb_fileid, thumb_key def cdn_download(base_url: str, fileid: str, aeskey: str, out_path: str, img_type: int = 1, timeout: int = 60): payload = { "fileid": fileid, "asekey": aeskey, # 注意这里是 asekey "imgType": img_type, "out": out_path, } resp = requests.post(f"{base_url}/api/cdn_download", json=payload, timeout=timeout) resp.raise_for_status() data = resp.json() ok = isinstance(data, dict) and data.get("errCode") == 1 if not ok: return False, data if not os.path.exists(out_path) or os.path.getsize(out_path) <= 0: return False, {"error": "hook返回成功但文件未落盘", "resp": data} return True, data def download_image_with_fallback(base_url: str, xml_text: str, out_path: str): fileid, aeskey, thumb_fileid, thumb_key = parse_img_xml(xml_text) if fileid and aeskey: ok, data = cdn_download(base_url, fileid, aeskey, out_path, img_type=1) if ok: return out_path if thumb_fileid and thumb_key: ok, data = cdn_download(base_url, thumb_fileid, thumb_key, out_path, img_type=2) if ok: return out_path raise RuntimeError("原图/缩略图都下载失败") ``` --- ## 6) 常见坑(你教朋友时重点强调) - `asekey` 字段名拼错:写成 `aeskey` 会直接失败。 - `fileid` 取错:优先 `cdnbigimgurl`,不要只盯一个字段。 - 只试原图不试缩略图:很多“偶发失败”是这么来的。 - 路径不可写:`out` 必须是 Hook 进程有权限写入的本机路径。 - 仅看 HTTP 200:必须再看 `errCode` 和文件是否真正写出来。 --- ## 7) 与 `download_img` 的区别(避免混淆) - `/api/cdn_download`:走 `fileid + asekey` 这条 CDN 参数下载链路(你现在要的)。 - `/api/download_img`:走 `MsgId/to_user/from_user/total_len...` 这条“按消息参数”下载链路。 `/api/download_img` 的官方文档在: - `新接口/下载图片.md:1` 如果你朋友框架已经能拿到 `` 里的 `cdnbigimgurl/aeskey`,优先实现 `/api/cdn_download` 即可。