Install
openclaw skills install @lingyu9495-source/wechat-chat-export-mac当用户要把微信里某个联系人、某段时间的文件图片视频批量下载到本地时用。指定联系人+日期范围,自动定位会话、批量点开原图、V2三段解密转码、按项目归档并验收缺口为0;实战单次导出1216文件163.9MB。适用微信桌面版4.x,仅macOS。
openclaw skills install @lingyu9495-source/wechat-chat-export-mac把指定联系人 + 日期范围内的文件 / 图片原图 / 视频批量下载到本地,解密转码后归档或导入知识库。
实战基准:单次导出 1216 个文件 / 163.9MB(79 文档 + 525 原图 + 504 缩略图 + 25 视频),缺口 0。
| 项 | 要求 |
|---|---|
| 系统 | macOS(Intel/Apple Silicon 均可),微信 桌面版 4.x |
| Python | 3.10+,装 pyobjc-framework-Quartz、Pillow、pycryptodome |
| 外部工具 | ffmpeg(转码 wxgf)、osascript(AppleScript) |
| 权限 | 辅助功能 + 屏幕录制(系统设置 → 隐私与安全性),给 Python/终端 |
| 供电 | caffeinate -d -i -u -s 或关自动锁屏(长任务会被锁屏打断) |
| 微信状态 | 目标联系人会话已打开、窗口存在;登录态在本地 |
Windows 版路径与加密不同,本 skill 仅验证 macOS。
BASE=~/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/<你的wxid>/
# 1) 图片(原始加密块)
$BASE/msg/attach/<会话hash>/<YYYY-MM>/Img/*.dat
# 2) 视频
$BASE/msg/video/<YYYY-MM>/*.mp4 + 封面 *.jpg
# 3) 图片缓存(点开过的完整图)
$BASE/cache/<YYYY-MM>/Message/<会话hash>/Bubble/*.dat
关键规则:微信按「消息月份」分目录,不是按文件 mtime。目录名 2026-10 = 消息发于 10 月。
定位参数(scripts/wx_locate.py 自动算出前两个):
ls BASE 上一级,形如 wxid_xxxxxxxx_yyywxid_abc123def456_7f2a → wxid_abc123def456(已实测:只有这个命中,带后缀或微信号 your_wechat_id 都解不开)$BASE/msg/attach/ 下找有目标月份数据的目录references/code-finding.mdimport hashlib, struct
from Crypto.Cipher import AES
from Crypto.Util import Padding
CODE = "<你的账号码>" # 本例 1234567890,不同账号不同
WXID = "<你的wxid>" # 不含 _xxx 后缀
KEY = hashlib.md5((CODE + WXID).encode()).hexdigest()[:16].encode()
XOR = int(CODE) & 0xFF
V2 = b'\x07\x08V2\x08\x07'
HEADER = 0x0F # 15 字节头
def align(n): return n - ~(~n % 16) # 16 对齐
def decrypt(data):
if len(data) < HEADER or data[:6] != V2:
return None, None, "非V2头"
aes_size, xor_size = struct.unpack_from('<LL', data, 6) # 头后两个 uint32 LE
a = align(aes_size)
aes_c = data[HEADER:HEADER + a] # ① AES 段
off = HEADER + a
raw_end = len(data) - xor_size if xor_size > 0 else len(data)
raw = data[off:raw_end] # ② 明文段(不加密,直接用)
xord = data[raw_end:] # ③ XOR 段
dec = AES.new(KEY, AES.MODE_ECB).decrypt(aes_c)
try: dec = Padding.unpad(dec, 16)
except ValueError: pass
out = dec + raw + bytes(b ^ XOR for b in xord)
for m, f in {b'\xFF\xD8\xFF':'jpg', b'\x89PNG':'png', b'GIF8':'gif',
b'RIFF':'webp', b'wxgf':'hevc', b'II\x2A\x00':'tif', b'BM':'bmp'}.items():
if out[:len(m)] == m: return out, f, None
return None, None, "magic未知:" + out[:8].hex()
三条铁律:
_t.dat 判断就丢弃 —— 部分 _t.dat 本身就是完整 JPEG(70KB+),能直接解出原图wxgf 是微信裸 HEVC 封装CODE/WXID 校验法:用 scripts/wx_probe.py 拿一个真实 dat 试解,解出 FF D8/89 PNG/wxgf 即参数正确;报「magic未知」说明 CODE 或 WXID 不对。
| 脚本 | 作用 | 实测结果 |
|---|---|---|
scripts/wx_locate.py | 定位 wxid/会话hash/月份,自动算解密用WXID | ✅ 识别出 106 个会话,目标 hash 自动排到第一 |
scripts/wx_probe.py | 验证 CODE+WXID 对不对 | ✅ full→wxgf、_t→jpg 双命中 |
scripts/wx_download.py | GUI 批量下载(动态窗口测量+盲点网格) | ✅ 编译+单元测试,坐标=实战值 |
scripts/wx_decrypt.py | 批量解密 + wxgf→jpg 转码 | ✅ 4/4 零失败 |
scripts/wx_verify.py | 验收(stem 比对,缺口=0) | ✅ 真实交付目录缺口 0 |
python3 scripts/wx_locate.py # 1 定位(拿到 WXID+hash)
python3 scripts/wx_probe.py --code <CODE> --wxid <WXID> --dat <任一.dat> # 2 验参
python3 scripts/wx_download.py --hash <hash> --month 2026-10 --wxid <账号目录> # 3 GUI下载
python3 scripts/wx_decrypt.py --code <CODE> --wxid <WXID> --src <Img目录> --dst ./out # 4 解密转码
python3 scripts/wx_verify.py --src <Img目录> --dst ./out # 5 验收缺口=0
CODE 获取三途径见 references/code-finding.md;完整操作手册见 references/sop.md。
ffmpeg -y -f hevc -i in.wxgf -frames:v 1 out.jpg # 不加 -f 会报 moov atom not found
本地只存 120px 缩略图(*_t.dat),原图必须点开才从服务器拉。这是整个任务最耗时的根因。
打开查看器(点聊天流图片) → RAISE 查看器窗口 → 方向键翻页 → 文件落盘
# 方向键 code:右=124(更晚),左=123(更早)
def key(code):
for d in (True, False):
CGEventPost(kCGHIDEventTap, CGEventCreateKeyboardEvent(None, code, d))
time.sleep(0.03)
# ✅ 有效:pixel 单位
CGEventPost(kCGHIDEventTap, CGEventCreateScrollWheelEvent(
None, kCGScrollEventUnitPixel, 1, px)) # 负=更早,正=回最新
# ❌ 无效:line 单位对微信 0% 像素变化
每轮必须先回底部再加深:Cmd+↓(keycode 121 + mod 55) + 正向滚动兜底,否则滚过头到聊天顶部 → 盲点全 opened=False。
-- ❌ key code 53 (ESC) 微信查看器不响应
-- ✅ 走菜单
tell application "System Events" to tell process "WeChat"
click menu item "关闭" of menu "文件" of menu bar 1
end tell
| # | 步骤 | 产出/验收 |
|---|---|---|
| 1 | 清场:关 .doc 预览窗、聊天记录窗、查看器、权限弹窗、System Settings | 微信只剩主窗口 |
| 2 | 定位:wxid、会话 hash、CODE、目标月份目录 | 三个路径可读 |
| 3 | 试解:取 1 个 dat 解密验证 magic | 出 FFD8/PNG/wxgf |
| 4 | 回底:切到目标会话 → Cmd+↓ → 滚到底(日期=最新) | 标题栏=目标联系人 |
| 5 | 批量下载:循环「盲点开查看器 → 方向键翻页 → 关闭 → 滚一层」 | full/dat 数持续增长 |
| 6 | 解密+转码:V2 分段解密 → ffmpeg -f hevc → jpg/png | 0 失败 |
| 7 | 验收:stem 集合比对(见下) | 缺口 = 0 |
盲点坐标(本例窗口 (63,112) 900×700,消息区 x303-963, y152-760):
图片消息中心 x ∈ {420,460,500,540},y = 200,270,...,620(步长 70)。步长改成 48 会全点空 —— 必须对准图片行中心。
2026-09/ → 监控该目录,一旦增长=越界,立即停full_stems = {f[:-4] for f in dat if not f.endswith('_t.dat')}
thumb_stems = {f[:-6] for f in dat if f.endswith('_t.dat')} # 去 _t.dat
need = thumb_stems - {能直接从 _t 解出的那些}
delivered = {os.path.splitext(f)[0] for f in os.listdir(DEST)}
print("缺口:", len(need - delivered)) # 目标 = 0
❌ 不要用感知哈希验收:缩略图 120px vs 原图 1080px,resize 到 16×16 后哈希对不上,会误报 75~104 张缺口(假的)。
视频同理:msg/video/<月>/ 封面含 xxx.jpg 和 xxx_thumb.jpg 两个变体,_thumb 不是独立视频,否则会虚报缺口。
| 坑 | 现象 | 解法 |
|---|---|---|
| 查看器遮挡 | 点击全落空、0 增长 | 查看器 z-index 高于主窗口,先关它/先 RAISE |
| line 滚动无效 | 截图 0% 像素变化 | 改 kCGScrollEventUnitPixel |
| ESC 关不掉查看器 | 按了没反应 | 走菜单「文件→关闭」 |
| 整文件 AES | 报「非V2头」/magic 未知 | 按 15 字节头分三段 |
| 滚过头 | opened=False 连续多轮 | 每轮先 Cmd+↓ 回底 |
| 盲点步长错 | 打不开查看器 | y 步长用 70 对准图片行 |
| 第二个 Enter | 文件没选上,反而把输入框文字发送出去 | 文件对话框只按 1 次 Enter,之后用鼠标点「打开」 |
| 遮挡窗口 | 系统设置/权限弹窗/doc 预览盖住聊天区 | 跑批前统一清场(见 SOP 第 1 步) |
| vision 坐标尺度 | 三次报不同基准(2560/2880/1920) | 截图后先 Image.open().size 拿真实尺寸,再用「裁剪图内坐标 ÷ 放大倍数 + 裁剪偏移」换算 |
<项目名>/
1-文件/ # MD5 去重后的文档
2-图片/原图/ # 全分辨率 jpg/png(知识库用这个)
2-图片/缩略图/
3-视频/ # mp4 + 封面
_进度台账.md # 跨会话恢复用
导入知识库:打包 zip -r 或在 IMA/飞书知识库点上传;长文本指令先发、文件随后,让 AI 先建立归档意图。
~/.config/ 下的私密文件)本技能由「九品锦锂e」制作。
使用中遇到问题、想要定制版本(Windows 适配、其他 IM 的同类导出)或需要配套支持,可通过上面任一方式联系,备注「SkillHub」优先通过。