万象-微信文件与视频号取件
一句话流程:
> 读 ini 找数据根 → 按 mtime 时间窗定位 → 只读复制到项目目录 → 零依赖解析成文本 → 摘要 + 脱敏扫描 → 输出确认卡 → 停,等用户拍板
微信不会把内容交给 Agent,必须自己到本地数据目录里捞。
0. 四条铁律(违反即失败)
| 铁律 | 原因 |
|---|---|
| 源目录只读 | 微信数据目录是运行时缓存,删改会导致微信异常。只 copy2 出来,绝不 move/rename/delete |
| 不编造内容 | 抽不到就写「未提取到」+ 给出替代路径。绝不能凭文件名猜内容 |
| 总结后必停 | 输出确认卡即结束本轮,等用户明确确认才做下一步 |
| ★ 先对齐标准再动手 | 用户给了参考文件时,先读完再动手,不许边试边摸索;动手前显式确认一件事:要「封面层」还是「内容层」(决定 30 秒还是 10 分钟)。做完被退回 = 标准没对齐,属可避免的失败 |
0.1 资源纪律(2026-09-20 立为长期标准)
> 缘起:用户批评「已经给了参考还是能出问题,这种来回折腾的方式不是我想要的……把方法写入自有技能,避免浪费资源浪费时间」。
- 先定层级再动手:只要封面层,30 秒交,别启动 E 路线;确认要内容层,才启动播放→扫内存→下载→解密→转写全链。
- 一次给足预算,同时设止损线:单卡全链 ≈ 5–10 分钟。15 分钟没出本体 → 停,转 A 路线并如实告知(请用户手机复制链接)。不许无限重试。
- 不重试已证伪的路:裸 URL(400)/
_b.dat(全加密)/ 磁盘找视频(零写入)—— 三条已证伪,每次跳过,不要在同一个坑里反复摔。 - 方法当场沉淀:本次新踩的坑与判据,当轮就写回本技能(含
references/视频号卡片提取速查卡.md),不许留在临时脚本里 —— 换窗口即归零。 - 交付分两层标注:封面层(作者/标题)与内容层(转写/读帧)必须分开写;拿不到内容写「未提取到」+ 替代路径。
1. 先问清楚三件事(能省一半时间)
| 问 | 为什么 |
|---|---|
| 文件、图片、还是笔记? | 「笔记」=聊天里的卡片(标题+缩略图+"笔记"角标),正文在加密消息库,只有图片能取出;「文件」是真文件,可直接读 |
| 大概几点发的? | 用 mtime 精确定位,比按名字猜快得多 |
| 发到哪个会话? | 文件传输助手 / 某个群 / 某个好友 —— 决定 msg\attach\<hash> 里哪个 hash 目录 |
先让用户发一张聊天界面截图,看清卡片样式与时间戳,最省事。
2. 数据根定位(90% 的失败在这一步)
微信允许把数据目录挪到任意盘,所以不要只看 C 盘 Documents。
2.1 常见位置(参考,不作依据)
C:\Users\<用户>\Documents\WeChat Files\ # 3.x 默认
C:\Users\<用户>\Documents\xwechat_files\ # 4.x 默认
<任意盘>:\WeChat Files\ / <任意盘>:\xwechat_files\ # 用户自定义(很常见!)
2.2 ★ 权威做法:读 ini,不猜盘符
# 4.x:C:\Users\<用户>\AppData\Roaming\Tencent\xwechat\config\<32位hex>.ini
# 整个文件内容就是一行盘符/路径(例:E:\)
# 3.x:C:\Users\<用户>\AppData\Roaming\Tencent\WeChat\All Users\config\<8位hex>.ini
# 内容为路径(例:E:\微信及企业微信)
本机实测(2026-09-18):
- 4.x 根 = E:\
- 3.x 根 = E:\微信及企业微信
- ⚠️ 同一个微信会同时存在 3.x 与 4.x 两套目录,都要扫
实现要点(locate.py 已实现):
- ini 文件名是 <32位/8位hex>.ini,用 glob 按 config\*.ini 枚举,不要枚举用户名;
- 读出来的内容可能带 BOM/杂字节,先 utf-8 再 gb18030 双重解码兜底,取第一行 strip 后即路径;
- 正则只认 [A-Za-z]:\\ 开头的盘符路径,其余内容一律不信;
- 都会把数据根写进 All Users / config 下的 ini,直接读它,别靠猜;找不到或解析失败时,把候选盘符列出来问用户,绝不默认 C 盘。
2.3 验证
print([d for d in os.listdir(root)]) # 期望看到 WeChat Files / xwechat_files
3. 目录结构:什么内容落在哪
3.1 真·文件(可直接读)
<根>\WeChat Files\<wxid>\FileStorage\File\YYYY-MM\ # 3.x
<根>\xwechat_files\<wxid>_<hex>\msg\file\YYYY-MM\ # 4.x
<根>\WeChat Files\<wxid>\FileStorage\Fav\ # 3.x 收藏
- 同名文件冲突会自动加
(1)(2)→ 判"最新一版"要看 mtime,不看序号 - 同一文件常被多次复制(手机端 + 电脑端各存一份),需按 MD5 去重
3.2 ★ 聊天图片 / 附件
<根>\xwechat_files\<账号>\msg\attach\<32位hex>\YYYY-MM\Img\
...\Rec\ # 语音
<根>\xwechat_files\<账号>\cache\YYYY-MM\Message\<hash>\Bubble\ # 气泡(加密 .dat)
...\Thumb\ # ★ 卡片/图片封面缩略图(真 jpg)
<32位hex>是会话指纹(每个联系人/群一个),同一会话每次收图都进同一个 hash 目录
3.2.1 ★★★ 会话反解:<hash> 就是 md5(会话 username)
实测(2026-09-19,已用 md5 字典对 12 个 hash 验证):
md5("filehelper") = 9e20f478899dc29eb19741386f9343c8 # ← 文件传输助手
⇒ 不要再靠"时间+内容像不像"猜某个 hash 属于哪个会话。
拿到对方 wxid / 群 id 就能直接算出 hash 并精准定位;
特别地「文件传输助手」的 username 恒为 filehelper,可与用户口述的会话名一一对上。
3.2.2 卡片封面怎么取(视频号 / 图文卡片)
| 文件 | 位置 | 内容 |
|---|---|---|
NNN_<epoch>_thumb.jpg |
Message\<hash>\Thumb\ |
卡片封面,真 JPEG,竖版多为 540×720 |
NNN_<epoch>_b.dat |
Message\<hash>\Bubble\ |
气泡渲染数据,加密,同名配对 |
- 配对判据:同名
_thumb.jpg与_b.dat同时存在 ⇒ 是卡片;只有 thumb ⇒ 是普通图片。 - 封面上的标题可以读全(见
scripts/ocr_cards.ps1);
视频本体、链接、作者、发布时间一律读不到(微信不落视频号视频本体,元数据在加密库)。 - 一张典型卡片能拿到的信息量:主题 + 封面上的卖点/数据,
足够判断"要不要点开看",但回答不了"视频里说了什么"。对用户必须这么讲清楚。
3.2.3 要"博主讲了什么"怎么办(2026-09-19,四条路线)
封面只是入口,用户真正要的是内容。按代价从低到高:
| 路线 | 做法 | 代价 | 拿到什么 |
|---|---|---|---|
| C 同源还原 | 封面 OCR 出标题 → 公网检索博主原文(很多卡片标题=公众号/开源项目标题) | 零(无需用户操作) | 博主的原话/方法/数据,但是同源文字,不是视频转写 |
| D 屏幕通道 | omni-digital-world/scripts/screen_record_av.py 录屏(画面+系统声),或 screen_capture.py 只留变化帧 |
用户点播放 + 别切窗口 | 画面 + 声音。2026-09-19 订正:原写「本机无音频采集设备、只有画面」已过时——dshow 枚举不到设备,但 WASAPI loopback 走渲染端点绕得开,已实测打通(peak=0.2000 / 72.4% 有声样本)。⚠️ 抓的是默认渲染端点,用户切到别的声卡就录不到 |
| A 链接直解 | 用户在手机上从分享面板「复制链接」,再用 channels_fetch.py 下载 |
用户复制一次 | 视频本体 → 可转写 + 读帧(最完整) |
| ~~B 缓存捞取~~ | ~~电脑微信播完 → 从 msg\video\ 捞~~ |
— | ❌ 已证伪:播放期间该目录与 cache 零写入,见坑清单 |
| ~~F 网页版 filehelper~~ | ~~用网页版 filehelper.weixin.qq.com(手机扫码登录)直读卡片~~ |
— | ❌ 已证伪(2026-09-21,代码级):其 _appMsgProcess 只处理 appmsg type 5(链接)/6(文件),APPMSGTYPE_ 常量表根本没有 51(表=0/1/2/3/4/5/6/7/8/9/10/13/15/16/17/2001/100001),而视频号卡片是 <type>51</type> ⇒ 必落 default → 只渲染「无法显示此消息,你目前使用的微信版本暂时不支持此类型的信息。」;且全包 cdnthumburl/thumburl/appmsg.sourcedisplayname 命中 0 ⇒ 连标题和封面都拿不到。实现包=res.wx.qq.com/t/wx_fed/base/webwx-transfer/<ver>/main.*.js(入口 49 B 是 loader,必须浏览器里 reload 录包才拿得到真包) |
A 路线的重要前提:PC 端视频号播放器没有「复制链接」入口(用户 2026-09-19 实测并截图)。
链接只能从手机端拿 —— 别让用户在电脑上找,那是白费工夫。
纪律:
- 四条路线拿到的信息必须分层标注(封面 / 同源二手 / 屏幕读图 / 视频转写),不能混起来当"读全了"。
- C 路线找到的内容可能只是"同题不同文",要在产出里写明出处与置信度,并提示可核对原始仓库。
- 拿不到视频时绝不脑补内容——写"未提取到"再给替代路径。
3.2.4 ★★★ E 路线:进程内存直取(唯一能拿到视频本体的磁盘路线,2026-09-20 实战定型)
> 定位:A 路线(手机复制链接)最完整但需用户操作;E 路线不需要用户碰手机 —— 在电脑端播放卡片后,从 Weixin.exe 内存里捞出带完整鉴权的下载 URL,直接下载视频本体,再解密 + 转写。这是目前唯一"全自动拿视频内容"的路。
前提认知(先看这三条,否则必走弯路)
- 微信播视频号时视频不落盘(
msg\video\零写入)—— 别去磁盘找视频文件。 _b.dat气泡数据全加密(实测头07 08 56 32 08 07 00 04,可见中文 0 条)—— 不要试图从中提取标题/作者,纯浪费时间。- 裸 encfilekey 下载 = HTTP 400 —— feed 里的裸 URL 缺鉴权参数。必须凑齐
token+svrbypass+sign+ftype=666才是完整 URL。
完整流程(五步,缺一不可)
① 置前微信 → ② 点播放 → ③ 高频扫内存抓完整 URL(+**邻近数字串=候选 key**)
→ ④ 下载 → ⑤ 解密(**ENCLEN = 明文 free box 起点**,由 `wxchannels_unpack.py` 自动定)
→ ⑥ 抽帧验归属 → ⑦ 转写 → ⑧ **关播放器**(缺 ⑧ 必污染)
① 置前(否则点击落空):WorkBuddy 窗口会抢焦点,d.activate() 会抛错。用四步法强制置前:
user32.ShowWindow(hwnd, 9) # SW_RESTORE
user32.SetWindowPos(hwnd, -1, 0,0,0,0, 0x0001|0x0002) # TOPMOST
user32.SetWindowPos(hwnd, -2, 0,0,0,0, 0x0001|0x0002) # NOTOPMOST
user32.AttachThreadInput(t_fg, t_me, True)
user32.SetForegroundWindow(hwnd)
user32.AttachThreadInput(t_fg, t_me, False)
# 验证 user32.GetForegroundWindow() == hwnd
> wxd.pick_main() 返回 dict({hwnd,cls,title,rect,vis}),用 m["hwnd"],别用 m[0]。
② 点播放:直接 SetCursorPos(r.left+rx, r.top+ry) + mouse_event(0x0002/0x0004)。
> 别用 d.mouse_tap()(本机抛 TypeError: not all arguments converted)。
③ 高频扫内存(核心):遍历所有 Weixin.exe 进程的可读区域(State==0x1000 且 Protect in (0x04,0x20,0x40,0x80,0x02)),用正则抓:
URLP = re.compile(rb'https://finder\.video\.qq\.com/251/20302/stodownload\?encfilekey=[A-Za-z0-9]{40,200}[^\s"\'<>\\]{0,2000}')
- 按 0.3s 间隔连拍 6–8 次(本卡 URL 可能在某一瞬出现)。
- 判据是「带 token+svrbypass+ftype=666」,不是「encfilekey 存在」 —— 后者全是噪声。
④ 下载:Range: bytes=0-1023 先探,Content-Range 给出真实总长。UA 必须带微信特征串。
> 尺寸是归属的第一道筛:卡片封面才几十 KB(如 40–80 KB),若探到 >100 MB,基本是推荐流污染,不是本卡 —— 直接换候选,别下。
⑤ 解密:decode.js <输入> <key> <输出> [加密字节数]
- 🔴 ENCLEN ≠ 全文件长度(2026-09-21 订正,旧判据是错的):加密段只覆盖头部 ftyp+moov,free+mdat 本来就是明文。传全长度会把明文 mdat 用 XOR 破坏掉。
- 正解:ENCLEN = 密文中 size==8 的明文 free box 起点偏移(实测 t0=209772 / t1=196243)。
- 定位法:在密文里搜 b"free",其前 4 字节若为 00 00 00 08 ⇒ 该 box 起点即 ENCLEN。
- 验证:解出后 box 链须为 ftyp → moov → free(8) → mdat,且 mdat 长度恰好抵到文件尾。
- 🔴 密钥 = 目标 URL 邻近的数字串(2026-09-21 新判据):不必靠 key 池试解 —— 在内存里以目标 encfilekey 的 URL 为锚,取其前后 ±4000 字节内的 8–12 位数字串,按与 URL 的字节距离升序排候选,第一个命中率最高(实测 272905582,dist=59 字节)。
- 🔴 参数顺序是 <输入> <密钥> <输出>(我曾写成 <输入> <输出> <密钥> → 全部 RuntimeError: Aborted())。
- ftyp 检测通过(解密后 buf[4:8] == 'ftyp')只是最低门槛,不等于产物完整 —— 必须接着做下面的自洽性核对。
★★★ 产物完整性验收(2026-09-21 定型,两步都要做)
> 背景:137 的 t1 曾一度误判为「已解出」(box 链完全正确、能抽帧、能转写开头),但实际只覆盖前 11.26 秒。事后查明真正的判据,避免再次把半成品当成品。
第一步 · ffmpeg 实测解码时长 vs 容器 Duration(唯一可信的"能播多长"判据)
ffmpeg -i <解密件> -f null - # 看末尾的 time= 与头部的 Duration 是否对齐
- ✅ 完好件:
Duration=00:08:26.85→ 解出frame=15184 time=00:08:26.83(对齐) - ❌ 半成品:
Duration=00:04:13.20→ 只解出frame=336 time=00:00:11.26(差 22 倍) - ⚠️ 不要用「mdat 内 NAL 走链计数」当判据:实测已完整解码的
card124与 126 件也只走 1 个 NAL 就断(hvc1/avc1的 mdat 不是纯长度前缀流),该判据对完好件同样"不过",无区分度。
第二步 · moov 自洽性核对(判 mdat 是"真载荷"还是"按需窗口")
读 stsz 表(注意:stsz 的 sample_count 在 box 偏移 +16,样本表从 +20 起,容易错位 4 字节):
样本字节合计 vs (mdat box size - 8)
- ✅ 完好件(card124):42,746,952 vs 47,020,925 → 确实偏小 ~9%(存在 chunk 空洞,这是正常的)
- ⚠️ 但比例须落在合理区间:若偏小超过 ~90%(如 137 的 t1:32,208,939 vs 33,903,187 → 偏小 5.00%,看似"更吻合"),反而是危险信号
- 🔴 决定性证据是「按 stsz 把样本拼出来时长多少」:137 t1 的
stszcount=7596 ≈ 正好 11.3 秒(7596/672 ≈ 11.3s,与实测可播 336 帧 ≈ 11.2s 吻合),而mvhd声明 4:13 ⇒ moov 的样本表本身就是按"约 11.3 秒"写的。
⇒ 结论性判据:若 mvhd 声明的 duration 远大于 stsz 样本能拼出的时长,则该文件是「按需窗口流」(只有播放缓冲窗口内的那段有效),不是完整本体。此类候选只能给"画面层",给不了"内容层",立刻止损换路线,别在它上面继续试 key / 试对齐。
路线优先级(2026-09-21 依据实测重排)
| 优先级 | 路线 | 判据 |
|---|---|---|
| 1 | D 屏幕通道(录屏 + WASAPI loopback) | 在"产物完整性"无法保证时,录屏是唯一能拿到完整内容的路(画面+声音都在渲染端,不受分片布局影响) |
| 2 | E 进程内存直取 | 仅在产物通过上面两步验收时才算成功;否则只取画面层 |
| 3 | A 手机复制链接 | 最完整,但需用户操作(PC 播放器无「复制链接」入口,别让用户在电脑上找) |
| 4 | C 同源还原 | 零成本,但拿到的是同源二手文字,须标注出处与置信度 |
> 已证伪勿再试:三假设(mdat 另起密钥流 / 丢弃 N 字节 / 含 free 头)全部测过,全部失败(起始码 0、解码 0 帧)⇒ 不要再在「mdat 段另起密钥流」方向花时间。视频号不落盘;消息库与 Bubble/*_b.dat 全加密;decode_key 不在静态 feed 载荷里。
⑥ 抽帧验归属(不可跳过):解密后抽 t=0.2/0.8/1.4 帧,Agent 亲眼看画面与封面比对。
> 「竖屏=本卡」是错的:续播推荐流里也有竖屏。归属只能抽帧看内容。
⑦ 转写:faster-whisper,见 §3.2.5。
关键坑(每条都踩过)
| 坑 | 现象 | 解法 |
|---|---|---|
| ★ ENCLEN 传「全文件长度」(旧判据是错的,2026-09-21 已订正) | 明文 mdat 被 XOR 破坏,抽帧大量失败 | ENCLEN = 密文中 size==8 的明文 free box 起点(搜 b"free" 且前 4 字节 00 00 00 08) |
| ★ 用 NAL 走链计数判完整性 | 完好件也只走 1 个 NAL 就断 ⇒ 判据无区分度 | 改用 ffmpeg 实测解码时长 vs Duration |
| ★ box 链正确就认为解好了 | 137 t1 box 链完美、能抽帧,实际只覆盖 11.26s | 补做 moov 自洽性核对(见上节);再看 mvhd duration vs stsz 样本可拼时长 |
| ★ 在「按需窗口流」上死磕 | t0/t1 试 6 种密钥流对齐方案全失败 | 判据一现即止损,换 D 录屏路线 |
| decode.js 参数顺序错 | RuntimeError: Aborted() |
<输入> <key> <输出> |
| 裸 URL 直接下 | HTTP 400 Bad Request | 必须带 token+svrbypass+sign+ftype=666 |
| URL 尾部粘内存噪声 | ...X-snsvideoflag=xWT126_isDecrypt:true}_feedId:... |
截断到 X-snsvideoflag= 值末尾再请求 |
| 续播污染 | 单次扫描 59–86 个 feed,无一本卡 | 播放后立刻扫;尺寸 + 抽帧双筛 |
| decode_key 与视频不同区 | 配对 264 组,目标 encfilekey 配对 0 组 | decode_key 只挂图片类前缀(KGDRibp2wkic…/2fG3V4WwQP…/rjD5jyTuFrIp…);视频是 rgjNqbibdAo…/6xykWLEnztLc…,从不相邻 |
| 密文熵判断 | 熵 7.99–8.00、无 ftyp/moov/mdat | 确认全加密,只能靠正确 key 解 |
| 下载物截断 | moov 声明 54s 但只 49 帧可解 |
该资产是截断的 H5 预览碎片;remux 能过、decode 报 Invalid NAL unit size |
| ★ 用「竖版」当归属筛子 | 我按竖版排除,把真正的本体(横版 1280×720)误判成污染 | 「竖版=本卡」和「横版=不是本卡」都不成立。本卡可能是横版(如 card137)。筛尺寸可以,筛朝向不行 —— 归属只能抽帧看画面(见⑥) |
| ★ 卡只有片头就以为拿错了 | card137 解出 35.01s,抽帧是「两个 SKILL 提升你的 AI 审美」片头,转写仅 1 句 | 这是 E 路线拿到的本体就是「无音轨 / 截断的 H5 预览片段」 —— 画面是真的、时长是真的,但内容不完整。判据:ffmpeg -i 看流信息,若 Audio 流存在但抽出的音频远短于视频时长(实测 35.01s 视频只抽出 2.11s 音频),即 mdat 截断 ⇒ 该候选只能给画面层,给不了内容层,别在它上面继续花时间 |
| ★ 已解出 ≠ 是本卡 | 164 个 key × 19 个候选试解,5 个命中(56.24s / 10.87s / 176.53s / 35.01s / 12.47s),其中 4 个是推荐流污染 | 试解命中只是第一关,必须过「抽帧看画面」才认。命中率高≠成功率高,反而说明播放器只放在推荐流首页(本卡 URL 根本没进内存) |
| ffmpeg 读中文路径 | Invalid argument |
复制到 ASCII 路径再处理 |
| 批量试解产生海量临时文件 | 被 SAFE_DELETE_BULK_CONFIRM_REQUIRED 拦 |
试解不落盘:一次性加载 wasm,在内存里试全 key 池(trykeys_batch.js),只输出命中 key |
| bash 环境 | ls/tail/head/dirname → exit 127 |
一律 Write .py 再跑,不写 shell 管道 |
配套脚本(scripts/,均零第三方依赖)
| 脚本 | 用途 |
|---|---|
wxchannels_decode.js |
WxIsaac64 解密(ENCLEN = 明文 free box 起点,不是全长度);放 _wasm/,需同级 wasm_video_decode.{js,wasm} |
xorrange.js |
任意区间 XOR(<in> <key> <out> "<start:len:discard,...>"),用于验证"mdat 段是否另起密钥流"这类对齐假设 |
trykeys_batch.js |
不落盘批量试 key:一次加载 wasm,对一个文件的整个 key 池试解,只输出 ftyp 命中的 key(避免临时文件爆炸) |
wxchannels_scan.py |
扫 Weixin.exe 内存,抓带完整鉴权的 stodownload URL |
wxchannels_pull.py |
窗口置前 / 点击播放 / 关播放器(nav / play / close) |
★ 密钥邻近提取(省掉整个试解环节):以目标 encfilekey URL 为锚,在其前后 ±4000 字节内提取 8–12 位纯数字串,按与 URL 的字节距离升序输出候选。实测第一个候选即命中(272905582,dist=59)。
🚨 续播铁律(用户已三次强调 · 违反=产物直接报废)
每张卡读完必须当场关闭播放器,否则视频号自动接播下一条无关视频,把录像、录音、内存扫描一起污染。
实证(137):正片 216.3s 讲完("…今天跟大家分享,拜拜"),播放器不关闭就自动接播下一条(「墨米·WorkBuddy+微信读书skill」3:15),录像尾部 216–253s 全是别人的内容。记录过 ≠ 会执行——所以必须让判据自动化,不靠记性。
关闭方式(wxd.py 里没有 close_player,别去找它):
u.PostMessageW(hwnd, 0x0010, None, None) # WM_CLOSE
🔴🔴 关窗的三个致命细节(2026-09-21 全部踩过,缺一个就会漏关)
① 播放器不是 Weixin.exe 的窗口 —— 它由 WeChatAppEx.exe(CEF 宿主)渲染!
进程家族:Weixin.exe(主程序,1 个)
WeChatAppEx.exe(视频号 / 播放器 / 小程序宿主,实测 3 个)
⇒ 按 IMAGENAME eq Weixin.exe 过滤 PID 会既找不到也关不掉播放器(现象:日志全程 waiting、播放器不被置顶、画面被别的全屏窗盖住只录到声音)。PID 集合必须同时含 Weixin.exe + WeChatAppEx.exe。
② 必须 include_hidden=True(枚举不可见窗口)
视频号自动接播的瞬间窗口会瞬时不可见。只枚举"可见"窗口会:(a) 误判「播放器已关闭」→ 提前停录;(b) 清场时漏掉它 → 无关视频继续挂播。⇒ 关闭判据要连续 3 次(≈0.6s)都找不到才认定关闭;句柄变了要跟随新句柄而不是当"已关闭"。
③ 关闭必须"验证到零",且按标题白名单保护小程序窗
Chrome_WidgetWin_0 是通用 CEF 类名(本机十几个无关进程都用),盲关会误伤。判据:
cls.startswith("Chrome_WidgetWin") and PID ∈ {Weixin.exe, WeChatAppEx.exe}
and title ∈ {"微信", "视频号", ""} # 播放器 / 视频号独立窗 / 无标题壳
非白名单标题记 skip 日志(便于事后审计是否漏关),不要静默跳过。
🛡️ 常驻守卫(把"记得关"变成机制 · 本机已部署)
脚本被强杀(点停止/超时)时 finally 不会执行 —— 这就是污染反复出现的真因。解法=独立守卫进程
(scripts/wxchannels_guardian.py,用 WMI 启动才能跨命令存活,见下文"进程逃逸"):
| 判据 | 行为 |
|---|---|
存在 _rec/_guardian.stop |
永久休眠(用户要自己看视频时用) |
_rec/_guardian.hb 不存在 或 mtime>15min |
休眠(防止几小时后误关用户的视频) |
_rec/_rec.lock 存在(正在录制) |
不动手 |
| 以上都不触发,且发现播放器窗 | 立即 WM_CLOSE + 复核残留 |
⇒ 录制脚本负责:开录前清场、结束时关、finally 兜底;并写 _rec.lock;退出时删 lock、刷新 _hb。
⇒ 守卫负责:收尸(脚本被强杀留下的播放器)。
⚠️ 窗口恢复必须校验"尺寸"而不只是"位置"
微信被挤下线 / 停在紧凑态时,主窗可能变成 296×388(rect 正常、不在屏幕外,但内容全错位)。
⇒ 恢复后校验 宽≥800 且 高≥800,不足则 SW_MAXIMIZE → SW_RESTORE → SetWindowPos 重试 6 次;仍不达标必须中止(宁可不动,也不能按错坐标乱点)。
⚠️ 微信会被其他设备挤下线(硬阻断,需用户介入)
现象:主窗尺寸突变 + 标题不再是账号昵称,弹窗「当前账号于 XX:XX 在 <设备名> 设备上登录。此客户端已退出登录」。
⇒ 此时任何点击都无意义,唯一出路是用户重新扫码。先截图确认状态再动手,别把"窗口尺寸异常"当成 bug 反复重试。
★ 自动识别"正片已结束"(2026-09-21 用 137 录像离线标定)
| 信号 | 正片内 | 切条瞬间 |
|---|---|---|
| 视频区帧差 MAD(缩 64×38 灰度,0.4s 采样) | 中位 0.022 | t=216.5 → 0.0848 |
| 音频 RMS(0.2s 块) | 0.03–0.08 | t=216.0 → 0.0060 |
⇒ 判据:RMS 骤降至 <0.006 且此前 2s 内出现过 >0.03 + 其后 1.2s 内 MAD ≥0.05 → 判正片结束 → 立即关播放器。
- 必须设 warmup ≥15s:开头"未开播/加载中"同时有静音与画面剧变,会误触发。
- MAD 单用不可靠:正片内转场 MAD 也能到 0.15+(137 在 61.5s/66.5s 处即如此),必须与音频骤降配对。
3.2.4b ★★ D 路线:录屏+系统声(兜底通道,2026-09-21 实测跑通)
何时走 D:E 路线拿到的只是「按需窗口流」——样本表只覆盖播放缓冲窗口内的那一段(137 t1 容器声明 253s,但 stsz 只写了 7596 帧 ≈ 11.26s,且无音轨)⇒ 产物完整性无法保证时,转 D,一次拿全。
两条流分开采:
| 流 | 工具 | 要点 |
|---|---|---|
| 系统声 | soundcard(WASAPI loopback) |
sc.all_microphones(include_loopback=True),取名字含 DELL/NVIDIA 的那个(=默认扬声器);48kHz/2ch,float32→int16 写 wav |
| 画面 | ffmpeg -f gdigrab |
-framerate 8 -i desktop -c:v libx264 -preset ultrafast -crf 28 -pix_fmt yuv420p -g 16 out.mkv(用 mkv,被中断也不废) |
- 本机实测:
-f dshow枚举不到任何设备(Could not enumerate audio only devices)⇒ dshow 这条路是死的,别耗;系统声只能靠 soundcard 的 loopback。 - 完整性校验(必做):录得时长 vs 容器声明——137 录到 253.6s,
mvhd声明 253.20s ⇒ 完整 ✓ - 录制期间微信主窗与播放器都要
SetWindowPos(hwnd, HWND_TOPMOST=-1, …, SWP_NOSIZE|SWP_NOMOVE)置顶:gdigrab 抓的是屏幕像素,被遮挡就录到遮挡物。 - 结束即关播放器,见上「🚨 续播铁律」。
⚠️ 坐标与滚动(两个必踩的坑)
1. 微信窗口被最小化后(rect 为 -32000)必须先 ShowWindow(hwnd, SW_RESTORE),否则所有点击落空;恢复后回读 rect 校验(宽须 >400),并校验目标坐标落在 rect 内,再动手。
2. 微信"窗口恢复"会把自己滚到最新消息(底部) ⇒ 滚动定位必须与"恢复窗口"在同一次执行内完成:恢复 → 向下滚 6 次到底 → 向上滚 N 次 → 点击。窗口尺寸没变时 move_resize 不会重置滚动位置,但最小化恢复一定会。
⚠️ 会话区滚动:滚轮事件被微信忽略 ⇒ 只能触摸拖动(wxd.drag)。向上翻旧消息 = 手指向下拖。
(2026-09-27 复测:WM_MOUSEWHEEL 投递给主窗对会话列表同样无效,只有"一次性滚到底"偶尔生效;且滚轮会误落在会话列表上,把当前会话切走。定位坐标时务必把滚轮发到聊天区坐标,或在空会话上操作。)
🔴🔴 铁律:任何情况下都不要自动重启用户的微信(2026-09-27 二次踩坑)
- 微信 4.x 重启后必须手机扫码/确认才能登录(一键进入无效)—— 是硬阻断。
- 2026-09-21:因多次合成点击失效而重启 ⇒ 卡扫码页、任务中断。(第一次踩)
- 2026-09-27:用户明确选择"重启微信后由我重试",我写了 _tool_restart_wx.py 执行:
WM_CLOSE + taskkill /F 关掉 Weixin.exe + 8 个 WeChatAppEx.exe ⇒ 之后 subprocess.Popen 与 cmd //c start 启动均为「进程在(217MB)、窗口不出」(只有一个 -100,-100 隐藏窗),疑似被火绒安全拦截或版本对非交互启动有校验。任务因此中断。(第二次踩)
- ✅ 正确做法:先请用户手动关闭;若确需重启,由用户手动双击图标启动。
- ✅ 合成输入失效时的升级顺序(到此为止,不穷举):
清理残留播放器/僵尸窗 → 清指针(发一次 up) → 换真实鼠标(SetCursorPos+mouse_event) → 请用户手动点一下 → 停。
已证全部无效的通道(2026-09-27 穷举记录,不要再试):PostMessage 点击列表项/真实鼠标单击/真实鼠标双击(双击会开独立会话窗,不是切换列表)/长按 150ms/搜索框+剪贴板(GlobalAlloc 返回 0)/WM_MOUSEWHEEL 滚动/键盘 Down/Ctrl+Tab。
🔴🔴 d.shot() 是整屏截图,不是窗口截图(2026-09-27 最贵的一个坑)
- wxd.WeChat.shot() 不做任何裁剪,返回 1920×1080 整屏;d.rect 只用于别处,与返回图无关。
- 后果:拿整屏图像的像素坐标当窗口内相对坐标用 ⇒ 窗口在 x=940 时点击整体错位 940px,表现为"点 A 却激活 B""会话列表毫无反应"。
- 自检判据:im.size == (1920,1080) 而 d.rect 宽仅 960 ⇒ 立即断定未裁剪。
- 正确做法:所有取点一律以「整屏截图像素 ≡ 屏幕像素」为准;若需要窗口内相对坐标,自己加 rect 左/上偏移,不要假定 shot() 已裁。
- 配套标定工具:_tool_zoom.py(截取局部放大 3 倍并画 20px 刻度标尺),用它量出目标行精确 y 值再点。
3.2.5 音频转写(faster-whisper,本机实测可用)
解释器与模型(唯一正确位置):
解释器 $env:USERPROFILE\.workbuddy\binaries\python\envs\default\Scripts\python.exe
模型根 D:\AZ\faster-whisper-models\faster-whisper-medium # ★ 唯一位置,别到处找
🔴 CUDA 必须用 PowerShell 前置 PATH(bash export PATH 不生效,会报 cublas64_12.dll is not found):
$env:PATH = "$env:USERPROFILE\.workbuddy\binaries\python\envs\default\Lib\site-packages\nvidia\cublas\bin;" +
"$env:USERPROFILE\.workbuddy\binaries\python\envs\default\Lib\site-packages\nvidia\cudnn\bin;" + $env:PATH
$env:HF_HUB_OFFLINE = "1"
& "$env:USERPROFILE\.workbuddy\binaries\python\envs\default\Scripts\python.exe" asr.py
参数(中文必须加):
m = WhisperModel(MODEL, device="cuda", compute_type="float16", download_root=MODEL_ROOT)
segs, info = m.transcribe(audio, language="zh",
initial_prompt="以下是普通话的句子,请使用简体中文转写。") # ★ 不加会输出繁体
- 不带
--model即 medium;实测 172s 音频 ≈ 14s 转写,2099 字。
提取规则表(先看这张表,再决定走哪条 —— 避免无效折腾)
| 目标 | 能否拿到 | 走哪条 | 要点 |
|---|---|---|---|
| 封面上的标题/作者 | ✅ | OCR + Agent 看图(交叉印证) | 封面 JPEG 真文件,最省事 |
| 卡片正文元数据(标题/作者/时长) | ❌ | —— | _b.dat 全加密,别试 |
| 视频本体 | ⚠️ 有条件 | E 路线(首页选)或 A 路线 | E 需播放 + 扫内存;必须凑齐 token+svrbypass+sign+ftype=666 |
| 视频讲了什么(内容层) | ⚠️ 得本体后 | 转写(§3.2.5)+ 抽帧读图 | 只有拿到本体才谈得上 |
| 链接 | ❌ | 手机端复制 | PC 播放器无「复制链接」入口 |
| 字幕 | ❌ | —— | 微信不落字幕 |
🔴 ASR 转写为空 ≠ 拿不到内容(2026-09-27 card138 实证,必读)
视频号有一类纯文字展示型短视频:正片全程只有 BGM + 画面上的文字卡片,无人声。
- 症状:faster-whisper 输出只有 请使用简体中文转写。(那只 initial_prompt 的回声),JSON 里 segments 为空。
- 此时不要判定"取不到",立即改走读帧:ffmpeg -ss <t> -i rec.mkv -frames:v 1 out.png,在正片时间窗内逐秒抽 10~14 帧,Agent 亲眼读画面文字。
- 实战效果:card138 正片仅 ~7s,抽 t=2..16 共 13 帧后逐字读全了一整段提示词(比转写更准,因为文字是排版好的、无同音误识别)。
- 判据:_rec/<id>_transcript.txt 字节数 < 200 ⇒ 先怀疑"这是展示型",去抽帧,不要去调 ASR 参数。
资源纪律(用户明确要求,避免浪费):
1. 先定层级再动手:明确要「封面层」还是「内容层」。只要封面层,30 秒能交;要内容层才启动 E/A 路线。
2. E 路线一次给足时间预算:单卡「播放→扫→下载→解密→验帧→转写」≈ 5–10 分钟。卡上 15 分钟没出本体就转 A 路线并告知用户,不要无限重试。
3. 不重复试已证伪的路:裸 URL、_b.dat、磁盘找视频 —— 三次已证伪,别再试。
4. 交付分两层标注:封面层(作者/标题)与内容层(转写/读帧)必须分开写,拿不到内容就写「未提取到」+ 给替代路径。
E 路线命中后的三级验收(2026-09-20 card137 实战补)
试解命中一个 key ≠ 拿到本卡内容。 必须按顺序过三关,任一关不过就判该候选出局:
| 关 | 动作 | 判据(唯一) |
|---|---|---|
| 1 抽帧看画面 | ffmpeg -ss 1 -i cand.mp4 -frames:v 1 out.jpg,Agent 亲眼看 |
画面与卡片封面对得上 ⇒ 过;否则污染,出局。不看分辨率、不看时长 |
| 2 查音轨完整性 | ffmpeg -i cand.mp4 看流信息 + ffmpeg -i cand.mp4 -vn -ac 1 -ar 16000 a.wav 看抽出音频实际时长 |
音频时长 ≈ 视频时长 ⇒ 过;音频远短于视频(如 35s 视频只出 2.1s)⇒ mdat 截断,只能给画面层 |
| 3 转写 | faster-whisper(§3.2.5) | 段数 ≥ 2 且有实质内容 ⇒ 内容层成立;只有 1 句片头话 ⇒ 内容层不成立,如实写「仅拿到片头片段」 |
> 过不了第 1 关就立刻换候选,不要对污染候选做第 2、3 关(那是最费时间的地方)。
> 过不了第 2 关=这张卡E 路线到此为止,转 A 路线或只交封面层 —— 这不是"没试够",是路本身断了,止损线在这里是正确动作。
3.3 ★★ 笔记(Note,最容易漏)
<根>\xwechat_files\<账号>\business\favorite\temp\NoteCache\
- 用户转发一条笔记卡片时,全部图片都落在这里,mtime = 那条消息的时间戳
- 同时
business\xeditor\mmkv\(笔记编辑器)会在同一秒写入 - 实测:只有图片,没有 .txt/.html —— 笔记正文与标题存在加密 SQLite(
msg\*.db,无免费解),别浪费时间找正文;标题只能靠用户截图或让用户口述
> 图片命名规律:微信图片_YYYYMMDDHHMMSS<毫秒>_N.jpg(高清原图)+ 一批 32 位 hash 文件名(同图低清副本) ⇒ 去重后会少一半。
3.4 微信自建的低价值目录(直接跳过)
Cache CustomEmotion Temp TempFromPhone PAG Sns MPPageFastLoad General
Image Video MsgAttach ResUpdate ResUpdateV2 Backup BackupFiles config
Applet WMPF publicLib publicTestLib
3b. 企业微信转发聊天归档:在线引用类资源(2026-09-19 新增)
微信 / 企微转发出来的聊天归档里混着两类载荷,处置完全不同:
| 载荷 | 形态 | 处置 |
|---|---|---|
| 离线附件 | 图片 / 视频 / 文档,物理存在于归档里 | 本地直接读(见本篇零依赖解析) |
| 在线引用 | 转录里只有类型标签 + 一个 id | 必须联网经连接器取回(见下) |
权威清单:宿主会把找到的在线引用逐条列在一个专门的块里。那份清单是权威的 —— 不要从转录文本里自行推导,也不要漏掉其中任何一条。
路由(按类型走):
| 转录里的标签 | 取回通道 |
|---|---|
| 文档 | 文档类通道 |
| 微盘文件 | 微盘类通道 |
| 日程 | 日程类通道 |
| 待办 | 待办类通道 |
| 会议 / 智能纪要 | 会议类通道(纪要可能另带会议码) |
每条引用本身已带上"该用哪个能力",直接用它,并先读该能力的参数说明再调用。
授权纪律(三条,全是踩过的):
- 授权由宿主管理:第一条命令先用只读的状态探测,不要一上来就调业务命令。
- 探测到未授权:此时宿主已经在弹授权对话框了 —— 你要做的是等(轮询状态,约 3 分钟上限),并且只对用户说一句"正在等你在对话框里完成授权",不要每次轮询都重复催。
- 绝不自行发起授权(任何形式):自行发起会打印自己的链接/二维码、甚至拉起外部浏览器,与用户正在看的那个对话框重复并互相竞争。唯一允许的授权命令是只读的状态探测。
取回纪律:
- 先全部取回,再写摘要。在线条目是必需的源材料,不是可选参考。
- 没取回就不许描述 —— 不要根据标签猜它的内容。
- 单条失败重试一次;仍失败就继续取其余条目,并把它记为缺失。
- 取回途中断在授权上(如凭证过期)→ 回到探测那一步一次,然后从断点继续。
- 任何在线条目未取回时,不得声称"归档已完整摘要"。
被拒绝 / 超时:不要重试、不要在本轮再问一次授权;只总结归档自身的内容(转录 + 离线附件),并点名说明哪些在线条目因未授权被跳过。
输出红线:指代条目要用可读的标签 / 主题 / 标题 / 时间,绝不暴露 id 字段。这一条优先于本节其他内容。
4. ★ 性能实测数据(决定扫描策略)
本机实测(E 盘,2026-09-18):
| 操作 | 耗时 | 说明 |
|---|---|---|
| 单账号 FileStorage 全树 walk | 10.2s | 3083 目录 / 15071 文件 |
| 同上 + 逐文件算 md5 | 超时被杀 | ← 这是 SIGTERM 的真凶 |
| 时间窗筛选后再算 md5 | <1s | 候选通常只剩个位数 |
三条结论:
1. md5 去重只能作用于「已通过时间窗筛选的候选」,绝不能对全量文件做 —— 这是最容易踩的坑
2. 多账号遍历必须带 deadline 预算 + 账号数上限
3. 顶层可按「目录 mtime 早于窗口 → 整棵跳过」预筛剪枝
正确的时间窗定位法(只列有改动的目录,别全盘 walk):
cut = time.time() - 86400
hits = []
for dp, dn, fn in os.walk(R):
if dp[len(R):].count(os.sep) > 6: # 剪枝:太深就不进
dn[:] = []; continue
try:
dm = os.path.getmtime(dp)
except OSError:
continue
if dm > cut:
hits.append((dm, dp, len(fn)))
hits.sort(reverse=True)
这一步就能一眼看出用户说的那条东西落在哪(哪一分钟写入的、在哪个子目录)。
5. 活跃账号判定
一个数据根下有多个 wxid_*(历史账号)。用 Msg\*.db 的 mtime 判活跃度,只扫最活跃的 1-3 个,别全扫。
实测本机 5 个账号,最活跃为 wxid_jqzfv82qtkek11。
6. 零依赖解析(本机无任何文档库)
实测环境:仅有 PIL 12.3.0;docx/pptx/openpyxl/pypdf 全部缺失,且本机倾向不装 pip 包。
解法:.docx/.pptx/.xlsx 本质是 zip+XML,用标准库 zipfile 直读。
# docx:word/document.xml(+header/footer)
# pptx:ppt/slides/slideN.xml,抽 <a:t> 文本,按页分组
# xlsx:xl/sharedStrings.xml + xl/worksheets/sheetN.xml,还原单元格
实测效果:0.0 秒,中文完全正确,比走 COM 快两个数量级。优先走这条路。
老格式(.doc/.xls/.ppt)才需要 COM 兜底(WPS/Office 已装)。
7. ★ 两个必踩的编码坑
7.1 读文本必须做编码探测
微信收到的 .txt 大量是 GBK(中文记事本默认)。硬按 utf-8 读 → 全篇乱码。
BOM 优先 → utf-8 严格试 → gb18030 → utf-16 → latin-1 兜底,
用「U+FFFD + C0/C1 控制字符」数量择优
7.2 stdout 必须强制 UTF-8
实测:Windows 控制台默认 GBK,print("定位") 输出成 瀹氫綅。Agent 读 stdout 会拿到垃圾。
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
8. ★ PDF 乱码闸门(必须做,否则污染整份摘要)
中文 PDF 常用自定义字体编码表,正则抽出的"文本"会是完全无意义的乱码,例:
2'4*9)'6+*+9/-4 5, ?/).'4-6;(2/) 9+);8/:?9;6+8</9/54)+4:+8
必须加质量闸门:可读字符(CJK + ASCII 可打印)占比 < 0.75 或 CJK 占比 < 0.15 → 判定失败并返回明确原因,而不是把乱码当摘要。
> 宁可不给结论,也不能给错结论。
9. 读图技巧(技术类资料的关键)
营销/规划图上的小字在缩略图里读不出,必须先切块放大再读:
from PIL import Image
im = Image.open(r'<大图>'); W, H = im.size
ox, oy = int(W*0.08), int(H*0.08) # 8% 重叠,防止切掉字
for k, b in {'TL': (0, 0, W//2+ox, H//2+oy),
'TR': (W//2-ox, 0, W, H//2+oy),
'BL': (0, H//2-oy, W//2+ox, H),
'BR': (W//2-ox, H//2-oy, W, H)}.items():
c = im.crop(b)
c = c.resize((int(c.width*2.2), int(c.height*2.2)), Image.LANCZOS)
c.save(r'<输出目录>\%s.png' % k)
经验值:
- 四象限 2.0~2.5× 足以读清图注(楼栋号、层数、面积、路名)
- 单点小字(路名、编号)用 4~7×
- 旋转读数:路名沿斜向道路排布时,img.rotate(-90, expand=True) 转水平再读;再按道路倾角补 rotate(±9)
- 长条图别整张读 —— Read 工具会缩放,字符反而更糊;改切小段(如 (40,560,200,780) 这种 160×220 的小块)
- 一次可并行发多个 Read(同一条消息里多个 tool call),批量看图更快
10. 脱敏护栏(硬红线)
摘要前扫描并主动提示:
- 身份证号 / 手机号 / 银行卡号 / 邮箱
- 机构名(XX有限公司|设计院|规划院|置业|地产)
- 甲方/业主/委托方:XX
实测真实命中:微信里确实存在带真实姓名 + 身份证号的证明材料。
凡发现即主动列出,并明确"任何要上传/分享/参赛的产出中均不得出现"。
11. 区分「实证」与「推定」(消化营销资料时)
营销资料(区位图、效果图)精度与权威性低于设计文件与官方公告。消化时:
- 逐条标注来源(哪张图、哪个位置)
- 凡"我推出来的"必须写成推定并汇总成待核项(例:由"挂牌栋数"反推"一二期分界"=推定)
- 与既有档案冲突时不要私自改 —— 并列两说,给出验证路径(例:地图标"孟新路" vs 档案"群建里")
- 反向验算最有力:拿官方已备案数据(如各栋总套数)反推图上数字,吻合即互证(例:96 套=24×4 ⇒ 坐实 25F)
12. 能力边界(必须对用户说清,不要过度承诺)
| 形态 | 可提取 | 说明 |
|---|---|---|
| ✅ 有条件 | 有文本层且非乱码才行;否则转图片读 | |
| PPT / PPTX | ✅ 高 | 按页抽文本,零依赖,效果最好 |
| Word / Excel | ✅ 高 | 同上 |
| CAD(.dwg/.dxf) | ⚠️ 需转 | 走 万象-工程建设标准核查 技能的 DWG 解析段 |
| .skp/.rvt/.3dm | ❌ | 需在源软件导出 PDF/图片 |
| 图片 | ⚠️ 中 | 需读图;小字要切块放大 |
| zip | ✅ 列清单 | rar/7z 无标准库支持 |
| 视频号转发视频 | ⚠️ 分两层 | 封面 + 封面标题 ✅ 可读(Thumb 真 JPEG + OCR,见 §3.2.2);<br>视频本体 / 字幕 / 链接 ❌ 读不到(微信不落视频本体,元数据在加密库)。实测 9/19 窗口媒体文件命中 0 |
| 微信「笔记」卡片 | ❌ 正文 | 正文在加密 SQLite,仅能取其中图片 |
| 聊天文字 | ❌ | 在加密库,Agent 读不到 |
> 对恢复不了的类型,要明说"读不到"并给替代路径,不要静默失败。
13. 脚本清单
> 🔴 <技能目录> 怎么取(2026-09-20 实测踩坑):技能加载时头部给的 Base directory 是
> .registry\history\<技能名>\<seq>_<时间>\files\ 这类登记路径,那里没有 scripts\ 目录,
> 按它拼路径必然 File does not exist。实盘在 ~/.workbuddy/skills/<技能名>/scripts/。
> ⇒ 拿不到路径时先用 Glob 搜 **/ocr_cards.ps1 找出真身,别硬套头部那个目录。
> (本机实测:$env:USERPROFILE\.workbuddy\skills\omni-wechat-fetch\scripts\ocr_cards.ps1)
── 通用取件(文件 / 图片 / 笔记 / 封面)────────────────────────
scripts/locate.py # 定位:ini 找根 + 账号活跃度 + 时间窗扫描 + md5 去重
scripts/extract.py # 提取:只读复制 + 零依赖解析 + COM 兜底 + 编码探测 + 乱码闸门
scripts/digest.py # 摘要:压缩 + 要点抽取 + 脱敏扫描 + 确认卡渲染
scripts/harvest.py # 编排:一键跑完四步并打印确认卡
scripts/ocr_cards.ps1 # ★ 卡片封面 OCR:WinRT Windows.Media.Ocr,零第三方依赖
── 视频号卡片「内容层」专用(见 §3.2.4)──────────────────────
scripts/wxchannels_status.py # ★★ 开工前体检:进程/窗口/尺寸/播放器残留/数据根/产物(2026-09-21 新增)
scripts/wxchannels_keyprobe.py # ★★ 密钥探测:按「与 URL 的字节距离」列出邻近数字串(2026-09-21 新增)
scripts/wxchannels_fetch.py # ★★ 探尺寸+下载:`Range: 0-0` 拿真实总长,**超阈值判污染不下载**(2026-09-21 新增)
scripts/wxchannels_unpack.py # ★★ 解密+验箱:**自动反推 ENCLEN**,逐个候选试解(2026-09-21 新增)
scripts/wxchannels_record.py # ★ 录制:点卡片 → 录屏+系统声 → **自动判切条并关播放器**
scripts/wxchannels_guardian.py # ★ 常驻守卫:非录制中自动关播放器(兜住被强杀的情况)
scripts/wxchannels_mad_cal.py # 标定工具:从一段已知录像标出「切条」的帧差/音量阈值
scripts/wxchannels_scan.py # 扫微信进程内存,抓带完整鉴权的 stodownload URL(--shots/--gap/--out)
scripts/wxchannels_decode.js # WxIsaac64 解密(**ENCLEN 由 unpack.py 自动传入**,别手填全长度)
scripts/trykey.js # 单 key 试解,只输出解密后头 16 字节 hex(不落盘)
scripts/trykeys_batch.js # ★ 不落盘批量试 key:一次加载 wasm 试完整 key 池,只输出命中 key
scripts/xorrange.js # 有界实验:对指定字节区间做 XOR(用于验证"某段是否加密")
scripts/wasm_video_decode.js # 官方 wasm 载荷(被上面几个 js 引用)
scripts/wasm_video_decode.wasm
> ⚠️ 旧版这里写「ENCLEN 传全长度」——那是错判据,已于 2026-09-21 推翻。
> 加密只覆盖头部 ftyp+moov,free+mdat 本来明文;传全长度会把正确的 mdat 用 XOR 破坏。
> 现在统一走 wxchannels_unpack.py,它自己从密文反推(见 §3.2.4「⑤ 解密」)。
13.1 主流水线(一条链走完 · 每步都有判据,不过就别往下走)
> 为什么要有这张表:全流程的失败几乎都不是"技术不会",而是跳步——没体检就动手、
> 拿到流就交付、没关播放器就换下一张。顺序不可跳,判据不过不往下走。
| 步 | 动作 | 命令 / 手法 | 判据(不过就停) | 不过怎么办 |
|---|---|---|---|---|
| 0 | 体检 | python <技能目录>/scripts/wxchannels_status.py --dir <产物目录> |
退出码 0,无阻断项 | 按提示逐项处理:启动微信/恢复窗口/关残留 |
| 1 | 定位卡片 | 会话区触摸拖动滚动 + 截图核对 | 目标卡可见,且坐标落在卡片内(截图核过) | 重新滚动定位;坐标错=点击全落空 |
| 2 | 播放 | wxchannels_record.py <tag> <滚动次数> <RX> <RY> <最大秒> <预热秒> |
播放器出现,或有音频信号 | 改用 wxchannels_keyprobe.py --tap RX,RY 单独点一次再扫 |
| 3 | 探 URL + 密钥 | wxchannels_keyprobe.py --prefix <enc前12位> --tap RX,RY |
拿到目标 URL 且有候选 key(dist 升序) | 多扫几轮(--shots);修正 --prefix 重定位 |
| 4 | 下载 | wxchannels_fetch.py <candkeys.json>(Range: 0-0 先探尺寸) |
Content-Range 给真实总长;>80MB 判推荐流污染 |
换候选,别下 |
| 5 | 解密 | wxchannels_unpack.py <密文> --keys <候选> |
退出码 0,箱链 ftyp→moov→free→mdat |
换候选 key/回第 3 步重扫 |
| 6 | 验收 | ffmpeg 比「解码时长 vs Duration」+抽 1 帧对封面 | 时长吻合 且 画面归属正确 | 时长不等 ⇒ 是窗口流,转 D 路线;画面不对 ⇒ 污染,换候选 |
| 7 | 转写 | faster-whisper(§3.2.5) | 段数 ≥2 且有实质内容 | 只有片头一句 ⇒ 内容层不成立,如实写 |
| 8 | 收尾 | finally 兜底 + 常驻守卫 |
播放器残留 = 0 | 手动 PostMessageW(WM_CLOSE),回读复验 |
路线分工(第 2 步之前先判):
| 路线 | 何时走 | 产物 | 代价 |
|---|---|---|---|
| E 内存直取 | 默认首选;要"可验证归属的干净本体" | 解密 mp4(可能是"按需窗口流") | 快,但可能拿不全 |
| D 录屏 + 系统声 | E 判为截断流/E 失败/要完整全片 | 录屏 mkv(含 UI,时长=真实时长) | 慢(≈视频时长),需音频通道 |
> 判「E 拿不全」的唯一硬证据(2026-09-21 实测):E 解出的文件 stsz 样本表只写了前 11 秒、且无音轨,
> 而 mvhd 声明的是 253 秒 ⇒ 该资产只覆盖播放缓冲窗口,不是全片。此时必须转 D,别硬交付。
E 路线一键链(视频本体 → 内容):
python <技能目录>/scripts/wxchannels_status.py --dir <产物目录> # 0 体检,退出码必须 0
# 1 定位 + 2 播放:滚动会话到目标卡(触摸拖动)→ 点开(见 §3.2.4 / wxchannels_record.py)
python <技能目录>/scripts/wxchannels_keyprobe.py --prefix <enc前12位> --tap RX,RY # 3 拿 URL+候选key
python <技能目录>/scripts/wxchannels_fetch.py candkeys.json --max-mb 80 # 4 探尺寸+下载
python <技能目录>/scripts/wxchannels_unpack.py <密文mp4> --keys "k1,k2,k3" # 5 自动定 ENCLEN+验箱
# 6 ffmpeg 比时长 + 抽帧验归属 → 7 faster-whisper 转写(§3.2.5)→ 8 关播放器
> 速查卡:references/视频号卡片提取速查卡.md —— 动手前先看它(分层选择 / 能拿什么 / 五步判据 / 止损线)。
卡片封面 OCR 单独用法(配合 §3.2.2):
powershell -NoProfile -ExecutionPolicy Bypass `
-File <技能目录>/scripts/ocr_cards.ps1 -InDir <封面目录> -OutFile <输出.txt>
- 建议先放大 3× 再 OCR(封面字小,原图直接认会掉字)。
- 输出
LANG=zh-Hans-CN+ 每张一段,无字输出(no text)。 - OCR 只是辅助:Agent 自己看图往往比 OCR 更准(OCR 会把「墊」认成「史」这类),两者交叉印证再落笔。
一键运行:
python <技能目录>/scripts/harvest.py <窗口小时> <输出目录> [--max N] [--kind pdf,ppt] [--no-com]
--kind可只取指定类型(pdf,ppt,word,excel,cad,image,archive,video,text)--no-com跳过 WPS/Office 转换(纯零依赖,更快)--accounts N最多扫 N 个活跃账号(默认 3)--budget N扫描预算秒数(默认 90)- 产出:
raw/(复制的原件)+确认卡.md+manifest.json
14. 坑清单
14.0 视频号取卡专项坑(2026-09-21 全部实测 · 优先看这段)
| 坑 | 症状 | 解法 |
|---|---|---|
🔴 播放器由 WeChatAppEx.exe 承载 |
按 Weixin.exe 过滤进程 ⇒ 播放器既找不到也不置顶,只录到声音 |
进程集合=Weixin + WeChatAppEx + WeChat |
| 🔴 只枚举「可见」窗口 | 接播瞬间窗口瞬时隐藏 ⇒ 误判"已关闭"提前停录;清场也漏掉它 | 枚举含不可见;连续 3 次找不到才算关闭 |
| 🔴 正常路径不关播放器 | 自动接播下一条,录像/录音/内存扫描一起污染 | 判据「音频 RMS 骤降 + 画面帧差突增」=切条 ⇒ 当场关;finally 兜底;常驻守卫 |
🔴 脚本被强杀 ⇒ finally 不执行 |
残留播放器继续播 | 靠常驻守卫(非录制中、15 分钟心跳内自动关)兜住 |
🔴 微信窗口被最小化(rect 为负值 / -32000) |
所有点击静默落空,界面像"没反应" | 动手前先读 rect;恢复后还要校验尺寸(见下条) |
| ⚠️ 只校验位置、不校验尺寸 | 窗口恢复成 296×388 ⇒ 坐标全错、点空 | 恢复后校验 W>500 and H>500,不达标重试 |
| ⚠️ 坐标算错(落在卡片外侧空白) | 点击无任何反应,界面零变化 | 用大图+十字标注核对落点;卡片位置会随布局变 |
| ⚠️ 微信被其他设备挤下线 | 主窗消失,提示"此客户端已退出登录";重启微信还可能要求重新扫码 | 硬阻断,必须用户介入;不要反复重启(会触发二次登录) |
| ⚠️ 置顶后又被 IDE 抢前台 | 点击落到别的窗口 | 点击前一刻再强制置前一次,并回读 GetForegroundWindow() |
| ⚠️ 拿「朝向」当归属筛子 | 把竖版/横版当污染判据 ⇒ 误杀真卡 | 朝向不作筛子;归属只能抽帧看画面 |
| ⚠️ 拿「NAL 走链计数」判完整性 | 已完整可播的文件也只走 1 个 NAL ⇒ 判据无区分度 | 改用 ffmpeg「解码时长 vs 容器 Duration」 |
⚠️ 把 mvhd 声明时长当实际时长 |
声明 253 秒但只有 11 秒可解码 | 两个都要读:stsz 样本表长度 ≈ 实际;两者差很大 ⇒ 是按需窗口流,转 D 路线 |
14.1 通用取件坑
| 坑 | 现象 | 解法 |
|---|---|---|
| 数据根不在 C 盘 | 默认路径下 0 文件 | 读 ini 拿真实盘符(§2.2) |
| md5 对全量文件算 | SIGTERM 无输出 | 只在时间窗候选上算(§4) |
| 全盘 walk 超时 | SIGTERM,无输出 | 改成"只列有改动的目录"+深度剪枝(§4) |
| 中文 stdout 乱码 | 定位→瀹氫綅 |
reconfigure(utf-8)(§7.2) |
| TXT 读成乱码 | 全篇问号/方块 | 编码探测(§7.1) |
| PDF 乱码当正文 | 摘要全是 2'4*9) |
质量闸门(§8) |
~$xxx.docx 被当文档 |
抽出乱码、白跑一轮 | 过滤 ~$ / .dwl / .tmp / .bak |
| 以为笔记里有正文 | 目录里只有图片 | 正文加密,让用户口述 |
| 把副本当不同文件 | 文件数翻倍 | MD5 去重(§3.1) |
| 用序号判最新 | (1) 反而更旧 |
看 mtime |
| 缩略图读不清 | 字糊成一片 | 切块+2~7× 放大(§9) |
| 微信「文件传输助手」无独立目录 | 找不到 | 附件按会话 hash 存于 msg/attach/<hash>/,靠 mtime 定位 |
| bash 缺 coreutils | tail/head/cat/dirname → Exit 127;2026-09-19 起连 ls 都没有 |
别在 bash 里做文件操作;改用 Python 或 PowerShell |
在 python -c 里用反引号 |
文本被 bash 吃掉 | 脚本先落盘为 .py 再执行 |
在 Python 源码里写 \n 转义 |
经工具写入后变成真实换行 → SyntaxError |
改用三引号 / chr(10) |
bash shim 报 cd: null directory |
命令整体 SIGTERM、输出为空、看不出错因 | 直接用 PowerShell 调 python.exe,别走 bash |
PowerShell *> 重定向 |
写出 UTF-16 → Read 报 "binary file" 读不了 | 用 2>&1 \| Out-File -Encoding utf8,或让 Python 自己 open(..., encoding="utf-8") 写日志 |
| PS 5.1 回显中文乱码 | 日志里 文件传输助手 → 鏂囦欢浼犺緭鍔╂墜 |
不影响 ASCII 关键信息(路径/时间/大小);要中文就 Python 内 reconfigure(utf-8) 自己写文件 |
| 时间窗扫描按父目录 mtime 剪枝 | 漏文件(实测 9 张只扫出 7 张) | 对 cache/YYYY-MM/Message 这种小规模目录直接全 walk,别提前剪枝 |
用 % 格式化生成 HTML |
CSS 里的 % 触发 ValueError: unsupported format character |
用 .replace("__PLACEHOLDER__", ...) 占位替换 |
| 把「所有会话」当成「某个会话」 | 上一轮 23 张卡片其实跨了 5 个会话 | 先用 §3.2.1 反解 hash 锁会话,再谈"读全" |
15. 交付话术(收尾必做)
输出确认卡后,明确列出三个待确认项:
1. 命中的文件是否就是目标那批?
2. 未提取项是否要走替代方案(读图 / CAD / 转图片)?
3. 下一步动作:归类归档 / 生成台账 / 出结构化文档 / 其他?
> 在用户给出选择之前,不要动手做任何后续动作。
常见误用与常见问题(FAQ)
常见误用(反模式)
| 误用 | 后果 | 正解 |
|---|---|---|
| 猜盘符、枚举用户名去找数据根 | 90% 的失败在这一步 | 读 ini,不猜盘符;用 glob 按 config\*.ini 枚举,不要枚举用户名 |
| 一上来就调业务命令 | 授权纪律被绕 | 授权由宿主管理 —— 第一条命令先用只读的状态探测 |
| 没取回就描述内容 | 编造 | 没取回就不许描述 —— 不要根据标签猜它的内容 |
| 在线条目未取回却声称「归档已完整摘要」 | 虚假完整 | 任何在线条目未取回时,不得声称「归档已完整摘要」 |
| 静默失败 | 用户误以为是空的 | 对恢复不了的类型要明说「读不到」并给替代路径,不要静默失败 |
| 重复试已证伪的路 | 浪费时间 | 不重复试已证伪的路:裸 URL、_b.dat、磁盘找视频 —— 三次已证伪,别再试 |
| 自动重启用户的微信 | 高风险 | 任何情况下都不要自动重启用户的微信 |
| 从转录文本里自行推导在线引用 | 漏项、错项 | 宿主给出的那份清单是权威的 —— 不要自行推导,也不要漏掉其中任何一条 |
| 与既有档案冲突时私自改 | 篡改记录 | 并列两说,给出验证路径(例:地图标「孟新路」vs 档案「群建里」) |
| 上传/分享/参赛产出里带项目名 | 违规 | 凡发现即主动列出,并明确「任何要上传/分享/参赛的产出中均不得出现」 |
常见问题
- Q:先问清什么? A:见「1. 先问清楚三件事(能省一半时间)」。
- Q:四条铁律是什么? A:见「0. 四条铁律(违反即失败)」;资源纪律见「0.1」。
- Q:提取路线怎么选? A:先看「提取规则表」,再决定走哪条 —— 避免无效折腾。
- Q:什么内容落在哪? A:见「3. 目录结构:什么内容落在哪」—— 真·文件/聊天图片与附件/卡片封面/笔记(最容易漏)。
- Q:能直接回答「博主讲了什么」吗? A:封面层足以判断「要不要点开看」,但回答不了「视频里说了什么」 —— 对用户必须这么讲清楚。
- Q:授权纪律有哪几条? A:三条,全是踩过的,见「授权纪律」。
附:融合说明
本技能由两部技能融合(2026-09-18):
- wechat-doc-digest —— 提供解析链(零依赖 zip-XML、编码探测、乱码闸门、脱敏扫描、摘要确认卡)与 4 个脚本
- wechat-local-file-harvest —— 提供落点知识(NoteCache 笔记缓存、目录类型图谱、读图放大量化经验、实证/推定分离)
删除的重复内容:两份技能各自写了一遍「数据根定位」(重叠约 80%),已合并为第 2 节;「性能实测」与「时间窗扫描」两处重复叙述合并为第 4 节。