weflow-cli 1.9.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +62 -26
- package/CHANGELOG.md +1051 -0
- package/OPERATIONS.md +99 -13
- package/README.en.md +14 -5
- package/README.md +23 -8
- package/bin/weflow-cli.ts +364 -40
- package/dist/bin/weflow-cli.js +381 -69
- package/dist/bin/weflow-cli.js.map +1 -1
- package/dist/mcp-server/index.d.ts.map +1 -1
- package/dist/mcp-server/index.js +19 -1
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/src/core/dbPathService.d.ts +12 -0
- package/dist/src/core/dbPathService.d.ts.map +1 -1
- package/dist/src/core/dbPathService.js +20 -3
- package/dist/src/core/dbPathService.js.map +1 -1
- package/dist/src/panel/server.d.ts.map +1 -1
- package/dist/src/panel/server.js +35 -0
- package/dist/src/panel/server.js.map +1 -1
- package/dist/src/services/assistantService.d.ts +19 -0
- package/dist/src/services/assistantService.d.ts.map +1 -1
- package/dist/src/services/assistantService.js +20 -0
- package/dist/src/services/assistantService.js.map +1 -1
- package/dist/src/services/assistantTools.d.ts +27 -1
- package/dist/src/services/assistantTools.d.ts.map +1 -1
- package/dist/src/services/assistantTools.js +169 -35
- package/dist/src/services/assistantTools.js.map +1 -1
- package/dist/src/services/configService.d.ts +2 -0
- package/dist/src/services/configService.d.ts.map +1 -1
- package/dist/src/services/configService.js +3 -2
- package/dist/src/services/configService.js.map +1 -1
- package/dist/src/services/exportService.d.ts +43 -0
- package/dist/src/services/exportService.d.ts.map +1 -1
- package/dist/src/services/exportService.js +116 -11
- package/dist/src/services/exportService.js.map +1 -1
- package/dist/src/services/messageQuery.d.ts +18 -0
- package/dist/src/services/messageQuery.d.ts.map +1 -1
- package/dist/src/services/messageQuery.js +29 -0
- package/dist/src/services/messageQuery.js.map +1 -1
- package/dist/src/services/wechatClient.d.ts +125 -0
- package/dist/src/services/wechatClient.d.ts.map +1 -0
- package/dist/src/services/wechatClient.js +344 -0
- package/dist/src/services/wechatClient.js.map +1 -0
- package/dist/src/services/wechatMedia.d.ts +44 -0
- package/dist/src/services/wechatMedia.d.ts.map +1 -0
- package/dist/src/services/wechatMedia.js +155 -0
- package/dist/src/services/wechatMedia.js.map +1 -0
- package/dist/src/services/wechatMessageService.d.ts +11 -0
- package/dist/src/services/wechatMessageService.d.ts.map +1 -1
- package/dist/src/services/wechatMessageService.js +55 -11
- package/dist/src/services/wechatMessageService.js.map +1 -1
- package/dist/src/types.d.ts +4 -0
- package/dist/src/types.d.ts.map +1 -1
- package/docs/CONTACT_DB_SCHEMA.md +1085 -0
- package/docs/DECISIONS.md +1274 -0
- package/docs/EXTENDING.md +69 -7
- package/docs/FUNCTIONAL-IMPROVEMENT-SPEC.md +27 -0
- package/docs/HEALTH-CHECK.md +12 -7
- package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +2 -2
- package/docs/MCP.md +2 -2
- package/docs/PROJECT_STATE.md +64 -5
- package/docs/RELEASING.md +128 -1
- package/docs/images/weflow-architecture-gpt-image-2.png +0 -0
- package/logo.png +0 -0
- package/mcp-server/index.ts +18 -3
- package/package.json +5 -3
- package/resources/js/graph3d/NOTICE.txt +35 -0
- package/resources/js/graph3d/d3-binarytree.min.js +2 -0
- package/resources/js/graph3d/d3-dispatch.min.js +2 -0
- package/resources/js/graph3d/d3-force-3d.min.js +2 -0
- package/resources/js/graph3d/d3-octree.min.js +2 -0
- package/resources/js/graph3d/d3-quadtree.min.js +2 -0
- package/resources/js/graph3d/d3-timer.min.js +2 -0
- package/resources/js/graph3d/layout.mjs +63 -0
- package/resources/js/graph3d/three.min.js +6 -0
- package/resources/js/graph3d/viewer2d.js +448 -0
- package/resources/panel/index.html +12 -0
- package/resources/panel/mascot-blink-1.png +0 -0
- package/resources/panel/mascot-blink-2.png +0 -0
- package/resources/panel/mascot-lift-down-1.png +0 -0
- package/resources/panel/mascot-lift-down-2.png +0 -0
- package/resources/panel/mascot-lift-down-3.png +0 -0
- package/resources/panel/mascot-lift-down-4.png +0 -0
- package/resources/panel/mascot-lift-down-5.png +0 -0
- package/resources/panel/mascot-lift-down-6.png +0 -0
- package/resources/panel/mascot-lift-up-1.png +0 -0
- package/resources/panel/mascot-lift-up-2.png +0 -0
- package/resources/panel/mascot-lift-up-3.png +0 -0
- package/resources/panel/mascot-lift-up-4.png +0 -0
- package/resources/panel/mascot-lift-up-5.png +0 -0
- package/resources/panel/mascot-lift-up-6.png +0 -0
- package/resources/panel/mascot-tickle-1.png +0 -0
- package/resources/panel/mascot-tickle-2.png +0 -0
- package/resources/panel/mascot-tickle-3.png +0 -0
- package/resources/panel/mascot-tickle-4.png +0 -0
- package/resources/panel/panel.css +111 -0
- package/resources/panel/renderer.js +480 -8
- package/scripts/_utils.py +1203 -977
- package/scripts/biz_daily.py +25 -10
- package/scripts/chat_notes.py +15 -5
- package/scripts/check_readme_commands.py +426 -0
- package/scripts/compile_wiki.py +163 -57
- package/scripts/contact_schema.py +339 -0
- package/scripts/data/wechat_emoji.json +225 -0
- package/scripts/export_chat_html.py +346 -6
- package/scripts/fav_notes.py +4 -1
- package/scripts/graph_2d.py +151 -0
- package/scripts/graph_3d.py +646 -0
- package/scripts/health_check.py +163 -26
- package/scripts/nt_decrypt.py +3 -3
- package/scripts/panel_frames.py +683 -0
- package/scripts/pipeline.py +21 -10
- package/scripts/quality_eval.py +5 -0
- package/scripts/rag_chat.py +33 -16
- package/scripts/reply_debt.py +26 -6
- package/scripts/semantic_search.py +212 -58
- package/scripts/vault_rag.py +42 -16
- package/scripts/vault_search.py +44 -8
- package/scripts/wechat_emoticon.py +6 -0
- package/scripts/wiki_lint.py +94 -17
- package/src/core/dbPathService.ts +22 -4
- package/src/panel/server.ts +33 -0
- package/src/services/assistantService.ts +26 -0
- package/src/services/assistantTools.ts +2452 -2319
- package/src/services/configService.ts +5 -2
- package/src/services/exportService.ts +129 -12
- package/src/services/messageQuery.ts +29 -0
- package/src/services/wechatClient.ts +380 -0
- package/src/services/wechatMedia.ts +166 -0
- package/src/services/wechatMessageService.ts +62 -11
- package/src/types.ts +4 -0
- package/dist/src/utils/wechatEmoji.d.ts +0 -5
- package/dist/src/utils/wechatEmoji.d.ts.map +0 -1
- package/dist/src/utils/wechatEmoji.js +0 -35
- package/dist/src/utils/wechatEmoji.js.map +0 -1
|
@@ -0,0 +1,1085 @@
|
|
|
1
|
+
# contact.db 结构笔记(含 ext_buffer / extra_buffer)
|
|
2
|
+
|
|
3
|
+
> **口径**:以下全部在**一台机器的真实库**上跑出来的(Windows 微信 4.x,2026-10-03),
|
|
4
|
+
> 方法是只读地读表 + 手写 protobuf 走线(varint / length-delimited),**不依赖 protobuf 库**。
|
|
5
|
+
> 标 ✅ 的是验过的(有对照数据),标 ❓ 的是**没验出来**的 —— 后者不要当结论用。
|
|
6
|
+
> 复现方法见文末。
|
|
7
|
+
|
|
8
|
+
## 0. 研究状态表("这个库研究完了吗" —— 自己回答)
|
|
9
|
+
|
|
10
|
+
**结构层面基本到头;语义层面还有几张没名字的表。** 判定标准只有一条:**能不能给出可复跑的计数**。
|
|
11
|
+
✅ = 有对照数据验过;◐ = 形状/边界清楚但语义未定(或只到"强倾向");❌ = 已证明**本机 + 公开渠道两头都拿不到**。
|
|
12
|
+
|
|
13
|
+
| 对象 | 状态 | 现在能说到哪 / 卡在哪 |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| `chat_room.ext_buffer` 顶层 `#1`(成员表)、成员 `#1`/`#2`/`#4` | ✅ | §2;与关系表 77 群逐一对照(5 群 blob 更新,以 blob 为准) |
|
|
16
|
+
| 顶层 `#5` | ✅ | = 成员 `status` 位 11 的那批 id(77/77)§2、D-070 |
|
|
17
|
+
| 成员 `#3 status` | ◐ | 位的作用域已定 + 5 字段切法;`bit0`/`bit11` 已解,`bit3/4/13/20–22` **未解**(两条硬排斥已验)§6 |
|
|
18
|
+
| `#3` / `#4`(每房一对) | ◐ | 定性到"**本地每房计数器**、跟成员表历史走";**名字不给**(两个候选已撤回)§6.1、D-071 |
|
|
19
|
+
| `chat_room_info_detail.ext_buffer_` | ✅ | `#6` = 公告发布时间(23/25);`#3`/`#4` 是**另一套编号**,"差 1 = pending"只对它成立 §3、§6.1 |
|
|
20
|
+
| `chat_room_info_detail.ext_buffer_` 的 `#1` | ◐ | **不是"一坨二进制"**:是两层嵌套(`DetailList → Entry`),**72/77 空、5 行非空**;`Entry` 字段号 {1..7} 8/8,`#2` 是成员 `username`(8/8 命中 `contact`,其中 **7 个正是该房 `owner`**)、`#5` 是合法 UTF-8 自由文本 §3 |
|
|
21
|
+
| `contact.extra_buffer` | ◐ | **一列两型**:940 行 = `ContactExtData` 骨架(同 `stranger`),44 行 = **OpenIM 专属**(`#1` / `#2` 是 `openim_*` 的外键,43/44 逐字节命中);**`#13` 出现 ⟺ `verify_flag ≠ 0`**(已验);**`#4`/`#9` = "该账号自身资料文本的汇集"**(自/他对照 336 vs 7);**已定名 4 个**:`#5` = 国家/地区代码(ISO 3166-1 alpha-2)、`#13` = 该行 `biz_info.type`(687/687)、`#41` = 该行资料的最近更新时间(ρ=0.945 + 40 行反例否掉"创建时间")、**`#26` = `<bizuserattr>` XML(`lastlogintime`/`lastmsgoptime`/`lastaddroptime`,115 行、取值 2013–2015)**;**`#6`/`#7` = 英文词形分类标签**(~36~51 / ~120 个取值,但**本机与二进制里都找不到出处**,像服务端下发);其余仍未解 §3、§7 |
|
|
22
|
+
| `stranger.extra_buffer` | ◐ | 与 `contact` 主体**同型**(逐字段号+线格式一致)⇒ 按骨架归 `ContactExtData`(§3.1 原先挂的 OpenIM 名已改);名字未解 §3、§7 |
|
|
23
|
+
| `biz_info`(15 列) | ◐ | 列语义与外部键测绘完;`type` 0=订阅号/1=服务号是**推定**;日报线增量在 §1.1 |
|
|
24
|
+
| `ticket_info` / `contact_label` / `encrypt_name2id` | ◐ | 形状、目标类型、行数已定;**用途判不了**(无对照源)§1.1 |
|
|
25
|
+
| `contact` 22 列(非 blob) | ◐ | **`verify_flag` 已解**(位掩码:`bit3` ⟺ 在 `biz_info` 里(687/687)、`bit4` ⟺ 机构认证、`bit8` ⟺ 个人认证简介、`bit4 ⊥ bit9`);`delete_flag` 全 0;其余标志列只有分布 §6 |
|
|
26
|
+
| `openim_appid` / `openim_acct_type` / `openim_wording` | ◐ | 前两张的 `app_id` / `acct_type_id` / `wording_id` **就是那 44 行小形状的外键目标**(逐字节命中);它们的 `ext_buffer` 都是「`#1` + 若干 `{key, value}` 对」,且 **`acct_type.ext_buffer#1` 逐字节 == 该行 `acc_type_id`**;**`openim_wording.ext_buffer` 21/21 全 0 字节**("3~4 个字段"那格是错的,已拆)§3 |
|
|
27
|
+
| blob 里那些字段的**名字** | ◐ | **四条路走通了四条、共定名 4 个**:**(a) 跨表整值 join**(那 44 行的 `#1`/`#2`)、**(b) 自/他对照的内容刻画**(`#4`/`#9`)、**(c) 公开码表 + 不依赖码表的预测**(`#5`)、**(d) 内容自带键名**(`#26`)。依然 ❌ 的是**从二进制里读字段名**:类型名字符串**不被任何指针引用**、全文件 43,411 个名字槽穷举无果、字符串池(**41,913 条记录**)对 `#6`/`#7` 也 0 命中(且"在 DLL 字节里命中"是短词撞车,见 §7)。⇒ **有值可查的先做 (a)~(d) 这四问,别先去读二进制。** |
|
|
28
|
+
| `#3`/`#4` 的**步长含义**(一步 = 加一个成员?还是改一次成员表?) | ❌ | **三条独立否证围死了**:不是服务端 seq(§6.1 ⑦)、**不是成员变更事件**(⑨:库里自带的事件日志把"事件数/加入数/净变化"三种读法全否,5 个 2026-08 后新建的房当决定性反例)、**本地没有第二份**(⑩:24 个库 / 20.7 万行按"同对象"判据搜 `#3` 与 offset,0 条有意义命中)。剩下的唯一办法是**机外对照源**(另一台机器 / 另一个版本 / 一个更早的库)§6.2、D-072、D-078、D-079 |
|
|
29
|
+
|
|
30
|
+
**一句话**:能靠"读数据 + 计数"解出来的,基本解完了;剩下的分两类 —— **要第二时点**(本机没有)和
|
|
31
|
+
**要外面对照源**(字段名)。这两类都不是"再想想"能解决的,所以接着推的收益在别处(见 §9 的复跑清单)。
|
|
32
|
+
|
|
33
|
+
⚠️ **本机库是"活的",下面每个计数都是某个时刻的快照**(各条尽量带日期)。2026-10-07 用 `contact-schema --contacts`
|
|
34
|
+
全量复测时,非空行已从 986 变成 **987**、`#41` 非零从 564 变成 **566**(有一个账号新增、有行被更新过),
|
|
35
|
+
而 `#13` 与 `biz_info.type` 的逐行相等在 **688/688** 上仍然成立。
|
|
36
|
+
⇒ **数字对不上先怀疑"库变了",别先怀疑解码错**;要判定解码对不对,用"**逐行关系**"(如 `#13 == type`)而不是绝对值。
|
|
37
|
+
|
|
38
|
+
## 1. 库里有什么
|
|
39
|
+
|
|
40
|
+
`contact.db` 共 17 张表。有数据的:
|
|
41
|
+
|
|
42
|
+
| 表 | 行数(本机) | 说明 |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| `biz_info` | 698 | 公众号信息,`external_info` 是一大段 JSON 文本 |
|
|
45
|
+
| `chat_room` | 77 | 群;**`ext_buffer` 就这里**(每行都有,1500 ~ 21590 字节) |
|
|
46
|
+
| `chat_room_info_detail` | 77 | 群的公告等;另有自己的 `ext_buffer_`(6 ~ 2210 字节) |
|
|
47
|
+
| `chatroom_member` | 4146 | `(room_id, member_id)` —— 库自己的成员关系表 |
|
|
48
|
+
| `contact` | 4014 | 联系人,22 列;二进制列叫 **`extra_buffer`**(不是 `ext_buffer`) |
|
|
49
|
+
| `stranger` | 1 | 与 `contact` 同 22 列形状;`extra_buffer` 是**另一套** protobuf |
|
|
50
|
+
| `openim_appid` / `openim_acct_type` / `openim_wording` | 1 / 1 / 21 | 各有一列 `ext_buffer`,**schema 互不相同** |
|
|
51
|
+
| `name2id` / `encrypt_name2id` / `contact_label` / `ticket_info` / `sqlite_sequence` | 4025 / 1 / 1 / 92 / 1 | id 映射与杂项 |
|
|
52
|
+
| `oplog` / `room_verify_application` / `stranger_ticket_info` | 0 | 空表:`oplog(id, buffer)`、`room_verify_application(8 列)`、`stranger_ticket_info(id, ticket)` |
|
|
53
|
+
|
|
54
|
+
### 1.1 `biz_info`(公众号表,2026-10-05 首次测绘)
|
|
55
|
+
|
|
56
|
+
15 列。**代码从来没读过它**(全仓库 grep 下来只有这份文档提到过 `biz_info`/`external_info`),
|
|
57
|
+
但里面**有日报线现在拿不到的东西**。
|
|
58
|
+
|
|
59
|
+
- 核心列:`username`(身份 id)、`type` / `accept_type` / `child_type` / `brand_flag`、
|
|
60
|
+
`external_info`(JSON)、`brand_info`、`brand_icon_url`、`home_url`、`sync_version`;
|
|
61
|
+
`brand_list` / `ext_buffer` **全空**。
|
|
62
|
+
- ⚠️ **一个坑**:`sync_version` 声明是 TEXT,**实际是二进制 blob** ⇒ 直接 `SELECT *` 会在 UTF-8 解码上崩,
|
|
63
|
+
要 `text_factory=bytes`。(现在没代码读它,所以是隐患不是现伤。)
|
|
64
|
+
- `external_info`:根一律 dict、**0 解析失败**,共 **30 个键**。覆盖最高的几个:
|
|
65
|
+
`Appid` 698 / `MMBizMenu` 673 / `RegisterSource` 650 / `WxaAppInfo` 599 / `VerifySource` 271 /
|
|
66
|
+
`ServiceType` 257 / `PersonVerifyInfo` 72。
|
|
67
|
+
- **它是不是"我关注的公众号"**:准确说是"**客户端已知的公众号集合**"——698 = `Name2Id` 里的 683 个 `gh_`
|
|
68
|
+
+ 15 个系统号;**`biz_info` 的 683 个 `gh_` 与 `name2id` 的 683 个完全一致**,
|
|
69
|
+
**凡是 `contact` 里的 `gh_` 必在 `biz_info`(单向包含)**。**两个缺口都追到了"数从哪来"(2026-10-05)**:
|
|
70
|
+
- **10 个 `gh_` 在 biz / name2id 里而 `contact` 里没有**(取关是硬删行):其中**只有 1/10 有消息表**、
|
|
71
|
+
**8/10 的 `sync_version` 为空**(对照:683 个 biz `gh_` 里只 22% 空)⇒ 数据其实**更支持"只被登记过、
|
|
72
|
+
从未真正关注"**(扫码/分享遇到但没关注);"关注过又取关"是同样相容的另一种读法 —— **本机判不了**。
|
|
73
|
+
- ⚠️ **更正(初版那条"33 个号有消息表却不在 `biz_info`")**:**"有消息表"是错的** —— 主消息库 **139 张**
|
|
74
|
+
`Msg_` 表的 owner 里 `gh_` **0 个**;`biz_message_0.db` 的 **533 张**表里 owner 不在 `biz_info` 的**只 2 个**。
|
|
75
|
+
但那个 **33 是真数**,只是来源不同:**订阅号库自己的 `Name2Id`(573 行)里有 33 个 `gh_` 不在 `biz_info`**,
|
|
76
|
+
而这 33 个**一个都没有消息表**(`Msg_<md5>` 在所有库里 0 命中)、**也不在 contact-db 的 `name2id`**、
|
|
77
|
+
`is_session` 无一是 1 ⇒ 准确说法是"**被订阅号库登记过名字、但既无会话也无消息**"。
|
|
78
|
+
- 保留一条实测事实:**`biz_message_0.db` 才是公众号消息的家**,主分片 139 张表里没有公众号。
|
|
79
|
+
- **`type` / `accept_type` / `child_type` / `brand_flag` 的取值分布**(698 行为基):
|
|
80
|
+
`type` = 0(532)/1(109)/3(50)/5(4)/2(3);`accept_type` = 0(**696**)/415(2);
|
|
81
|
+
`child_type` = 0(683)/2(13)/1(2);`brand_flag` = 2(512)/0(182)/1(4)。
|
|
82
|
+
**推定:`type` 0 = 订阅号、1 = 服务号**(`type=1` 的 99% 有 `VerifySource`、100% 有 `MMBizMenu`;
|
|
83
|
+
`type=3` 有 0% 认证)。`accept_type` **基本恒 0 ⇒ 排除"是否接收消息"这条读法**;
|
|
84
|
+
`child_type` 只在 `type=3` 内区分;`brand_flag` 与"有没有消息"强相关(2→95.5% vs 0→21.4%),**语义仍未定**。
|
|
85
|
+
- **`brand_info`**:是 **TEXT 不是 blob**,600 行非空**全部是合法 JSON 对象**,而且**每条只有一个顶层键 `urls`**
|
|
86
|
+
(600/600 一致),众数长度 198。
|
|
87
|
+
- **另外三张从没碰过的表(2026-10-05)**:
|
|
88
|
+
- `ticket_info`(92 行):`id` **100% 是 `name2id.rowid`**;票长只有**两档 284 / 316**,字符集 `[A-Za-z0-9@_]`
|
|
89
|
+
(**不是 hex/base64**),92 条全唯一。**目标全是个人**(86 wxid + 6 短名;群 / 公众号 / openim **各 0**),
|
|
90
|
+
92/92 都在 `contact` 里、`local_type` 只 0/1。(票长与 `local_type` 只有 43/46 对齐,有 3 条对调 ⇒ 不是严格绑定。)
|
|
91
|
+
- `contact_label`(1 行):列 `label_id_` / `label_name_` / `sort_order_`;**全库逐列找 label/tag,
|
|
92
|
+
只有它自己** ⇒ 没有任何成员列引用标签。
|
|
93
|
+
- `encrypt_name2id`(1 行):**只有 `username` 一列、没有 id 列 ⇒ 不与 `name2id` 共享 rowid 空间**;
|
|
94
|
+
它的值长 176,恰等于 `name2id` 里**唯一**一个长度 >100 的 username,也等于某个 15 字符 `gh_` 联系人的
|
|
95
|
+
`encrypt_username` ⇒ 属"**公众号类加密名**"。`contact.encrypt_username` 一列也正是这一族(3251 行为空)。
|
|
96
|
+
- **对日报线的增量(现成的,不用联网)**:`RegisterSource.RegisterBody` = **主体名称(93.1% 覆盖)**、
|
|
97
|
+
`VerifySource.Description` / `VerifyBizType` = **认证类型**、`ServiceType` = **服务类型**、
|
|
98
|
+
`brand_icon_url` = **头像直链(691 行)**、`PersonVerifyInfo.VerifyDescribe` = 个人号简介(**64** 行)、
|
|
99
|
+
`VerifyIdentity` = 个人号分类(**68** 行 / 只有 **19 种短标签**)。⚠️ 初版把"68"挂在 `VerifyDescribe` 上,
|
|
100
|
+
**是这两个键串了**(2026-10-06 复核)。
|
|
101
|
+
现状是:日报的名字取自 `contact.remark` / `nick_name`、**不用头像**,而 `contact.description` **全表 4014 行全空**(2026-10-06 复核,不限于 gh_) ⇒ **今天根本没有"主体名称 / 简介"的来源**。
|
|
102
|
+
- **隐私**:键名扫 `token`/`session`/`cookie`/`password`/`login` **0 命中**;但确有
|
|
103
|
+
`PersonVerifyInfo.VerifyName`(**认证人真名**)、`ServicePhone`、`Location`(经纬度)、企业 `corp_id`
|
|
104
|
+
⇒ 取用时只取需要的字段,**别整包带出去**。
|
|
105
|
+
|
|
106
|
+
**`external_info` 其余键的全量清点(2026-10-06,分母 698;覆盖数我复核过)**:
|
|
107
|
+
|
|
108
|
+
- **日报线现在就能用的**:`RegisterSource.RegisterBody` **646**(主体名称,≈92.6%)、
|
|
109
|
+
`RegisterSource.AboutBizUrl` / `IntroUrl` **各 645**、`VerifySource.Description` **269**(认证描述)、
|
|
110
|
+
`VerifySource.VerifyBizType` **267**、`PersonVerifyInfo.VerifyIdentity` **68**(个人号分类,只有 19 种短标签)、
|
|
111
|
+
`PersonVerifyInfo.VerifyDescribe` **64**(个人号简介)、`Location.position` **151**(地区文本;与经纬度同包,
|
|
112
|
+
**只取 position**)、`ServicePhone` **72**、`TrademarkName` / `TrademarkUrl` **各 42**、
|
|
113
|
+
`BindWxaInfo.wxaEntryInfo`(关联小程序名,**12 个号**、全 `type=1`)、`brand_icon_url` **691**(头像直链)。
|
|
114
|
+
- **按 `type` 拆一下覆盖,别许愿**(订阅号 `type=0` 共 **532** 行):能用的是 `RegisterBody`(**100%**)、
|
|
115
|
+
`VerifySource`(30%)、`Location`(23%)、`PersonVerifyInfo`(13%)、`ServicePhone`(6%);
|
|
116
|
+
`BindWxaInfo` 对订阅号是 **0**,`EnterpriseBizInfo`(`exattr` **50** 行)**只在企业号 `type=3`**。
|
|
117
|
+
- **空壳(本轮查出来的,别指望)**:`PayShowInfo` 53 行有对象但叶子基本是空串(`scope_of_business` 33 行全空)、
|
|
118
|
+
`WxaAppInfo` **599** 行有对象但子键几乎全空(我量到非空的只有 `TcbCdnDomain` **14** 行、`Slogan` **1** 行)、
|
|
119
|
+
另有一串布尔/枚举键去重只有 1~3 种。
|
|
120
|
+
|
|
121
|
+
## 2. `chat_room.ext_buffer` = protobuf(已验)
|
|
122
|
+
|
|
123
|
+
顶层只有三种:`#1`(长度分隔,重复)× N、`#3`/`#4`(varint,**两者恒相等**)、`#5`(长度分隔,重复)。
|
|
124
|
+
本机 77 行里出现过的字段号只有 `1`(4151 次)、`3`(77)、`4`(77)、`5`(**28 条目,落在 15 个群**)
|
|
125
|
+
—— **没有 `#2`,也没有 `#6`**。(`#6` 是 `chat_room_info_detail.ext_buffer_` 的东西,见第 3 节;
|
|
126
|
+
两个 blob 长得很像,别混。)
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
RoomData {
|
|
130
|
+
repeated RoomDataUser users = 1; // ✅ 成员列表
|
|
131
|
+
// 下面是实测到的、上游定义里没有的:
|
|
132
|
+
// 3 = varint, 4 = varint(每个群 #3 == #4;见 6.1)
|
|
133
|
+
// 5 = 长度分隔,重复(**= 成员里 status 位 11 置位的那批 id**,77/77 与该集合完全相等;
|
|
134
|
+
// 不是"额外的参与者"——早前那么叫是错的,见 D-070 与第 6 节)
|
|
135
|
+
}
|
|
136
|
+
RoomDataUser {
|
|
137
|
+
string userName = 1; // ✅
|
|
138
|
+
string displayName = 2; // ✅ 群昵称。本机 **1252/4151 = 30.2%** 的成员有(其中 14 条是空串),
|
|
139
|
+
// 其余 2899 条**连字段都没有**(早前写的 85.2% 是错的)
|
|
140
|
+
int32 status = 3; // ⚠️ **不是 0-9**,是位标志:实测 0/1/9/17/25/2057/2073/
|
|
141
|
+
// 8193/2097153/3145729/6291457/7340049
|
|
142
|
+
string inviter = 4; // ✅ 邀请人(本机 4074/4151 = 98.1% 有)
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**验过的对照**:`#1` 的条数与关系表 `chatroom_member` 逐群对(77 个群):**72 个相等、5 个不相等,
|
|
147
|
+
且这 5 个一律是"blob 比表多 1 个人"**(`id` = 331 / 2007 / 2148 / 2869 / 3968),从不更少;
|
|
148
|
+
多出来的那个人在 `contact` / `name2id` 里都存在(不是脏数据)。**以 blob 为准 —— 它更新。**
|
|
149
|
+
(这 5 个群正好就是 6.1 里 `#3 != #4` 的那 5 个:两件事是同一个「已到达、还没落到关系表的
|
|
150
|
+
成员变更」的两个标志。)
|
|
151
|
+
|
|
152
|
+
**与上游定义的差异**([Wing900/chatlog-export](https://github.com/Wing900/chatlog-export) 的
|
|
153
|
+
`internal/model/wxproto/roomdata.proto`,声称 v3/v4 通用):成员那一段**完全对得上** ✅;
|
|
154
|
+
但它写的 `optional int32 roomCap = 5` ❌(实测 `#5` 是长度分隔的字符串列表);
|
|
155
|
+
顶层的 `#3`/`#4`/`#6` 它也没有 ❌。它的注释"syntax v3 & v4 通用,可能会有部分字段差异"
|
|
156
|
+
——差异比注释里说的大。(**客户端自己**给这个 blob 的类型叫 `micromsg.ChatroomExtData`,见 3.1。)
|
|
157
|
+
|
|
158
|
+
## 3. 其余几处 protobuf(形状已验,语义多半未定)
|
|
159
|
+
|
|
160
|
+
| 列 | 形状 | 状态 |
|
|
161
|
+
| --- | --- | --- |
|
|
162
|
+
| `chat_room_info_detail.ext_buffer_` | `#1`(wt2) + `#3`/`#4`(varint) + 有时 `#6`(varint),1 行里还见到 `#7` | ❓ **另一套编号**:`#3` 与该群 `chat_room.ext_buffer` 的 `#3` **从不相等**(0/77);`#3`/`#4` 大多相等、**5 行差 1**(见 6.1);`#6` **就是公告发布时间**(25 行里有 23 行 == `announcement_publish_time_`,另 2 行 blob 里是 0);`#1` 多数为空、少数是一大块二进制 |
|
|
163
|
+
| `contact.extra_buffer` | 主体是**一张 37 字段的 proto**(`#2`…`#38`,与 `stranger` 同型)+ **44 行 OpenIM 小形状** | ◐ **形状与归属已定,37 个字段的名字仍未解**:4014 行里 **3028 行的这一列是空的**(≈75%;这个数与 `local_type = 3` 的行数**恰好相等**——本机所有 `local_type = 3` 的行都空);**986 行非空**,其中 **940 行**是 37 字段的大 proto(长度中位约 115 字节)、**44 行**是 OpenIM 小形状(顶层字段号只 1..9;另有 2 行是字段号 `#10`/`#40` 的**大 proto 残行** —— 所以"46 行"这个数只在"字段数 ≤ 9"的口径下成立,**按字段号看真小形状是 44 行**)。⚠️ **2026-10-05 更正**:初版把这一列记成"`#3`(varint),2 字节上下"——**那是 46 行小形状的样子**,主体 940 行是大 proto(`#3` 只是其中一个小 varint)。两个旧判据现在对得上了:**984 行有顶层 `#3`**,其中 **942 行是 varint**(= 940 大 + 2 小)、42 行是 length-delimited、2 行没有 ⇒ 初版那个"942 / 44"就是「`#3` 是不是 varint」,与形状切法(940 / 46)差的正是那 2 行小形状。**942 行**里 `#3` 的取值只有 0(520)/3(418)/9(2)/1(1)/515(1)。与同行 21 列**没有一列一一对应** —— `local_type`/`flag`/`verify_flag`/`is_in_chat_room`/`chat_room_type` 逐张列联表都试过(`verify_flag` 的 0/8/24 三种取值下 `#3` 都同时有 0 和 3)。唯一稳定的关系是**存在性**:`local_type = 3` 的行全部为空,有值的基本只在别的类型上 —— 存在性与类型相关。**取值本身**见第 6 节(逐位后只解出两条:`bit0 == (值≠0)`、`bit1 ⊂ bit0`)。**2026-10-06 又解出三条(多 Agent 提出、我逐条复核)**:① **`#13` 出现 ⟺ `verify_flag ≠ 0`**(687 有 / 253 无、**两个错格都是 0**)—— 全表**第一条**「某个 blob 字段出现与否 == 本地列取值」的**双条件**(此前的"存在性"关系都只是单向蕴含);② 那 44 行**是 OpenIM 专属**:`#1` = `openim_appid.app_id`、`#2` = `openim_wording.wording_id`(各 **43/44** 逐字节命中)、`#4.#2` 的 JSON 键**恒为 `custom_info`**(43/43);承载行 **42/44 的 username 以 `@openim` 结尾、`local_type ∈ {5,6}`**,而**全库 `local_type ∈ {5,6}` 的 42 行恰好全在这族** ⇒ 这一列是**「一列两型」**(按 `local_type` 分);③ `#33` 非空 ⟺ `#36` 出现 ⟺ `#27` 子字段 3 出现(三者都是 **824** 行,各方向 0 例外);④ **`#9` 有"半命名"**:**213/567** 行整值等于该行 `biz_info` 的认证主体名(`RegisterSource.RegisterBody`)、另 21 行**把主体名当子串含住** ⇒ 合计 **234/567 = 41%**,而 `#4` 只有 **1/673**(不对称 ⇒ 不是撞车);但 **58% 的行与主体名无关 ⇒ `#9` 不是"主体名"这一列、不能当来源用**(详见 §7)。 |
|
|
164
|
+
| `stranger.extra_buffer` | **37 个顶层字段(`#2`…`#38`)**,varint 与 length-delimited 混排、含嵌套 | ❓ 名字**一个都没定**;**但骨架已证实与 `contact` 主体同型**:那 940 行与这 1 行**逐字段号 + 逐线格式完全一致**(2026-10-05 我复核)⇒ 两者是**同一张 proto**(`contact` 主体另多 `#41`(varint) / `#43`(ld))。⇒ §7 的取值目录不再只有 1 行样本,可依 940 行。⇒ 按骨架它也是 `ContactExtData`(§3.1 原先挂的 OpenIM 名已改到那 44 行) |
|
|
165
|
+
| `openim_appid.ext_buffer` / `openim_acct_type.ext_buffer` | 各约 200~340 字节,顶层 3~4 个字段、含嵌套 | ❓ 与上面几套**都不一样** |
|
|
166
|
+
|
|
167
|
+
### 3.1 这几个 blob 的**类型名**(2026-10-03 从客户端二进制里读出来的)
|
|
168
|
+
|
|
169
|
+
客户端安装目录里的 `Weixin.dll` 里有一张"类型名"表(生成的代码给每个 message 都留了全名)。
|
|
170
|
+
`local_proto/local_contact.pb.cc` 这个编译单元注册的,正好就是本地 contact 库那几个 blob 的类型:
|
|
171
|
+
|
|
172
|
+
| 我们的列 | 客户端里的类型名 |
|
|
173
|
+
| --- | --- |
|
|
174
|
+
| `chat_room.ext_buffer` | `micromsg.ChatroomExtData` |
|
|
175
|
+
| 每个成员子消息 | `micromsg.ChatroomMemberLocalData` |
|
|
176
|
+
| `chat_room_info_detail.ext_buffer_` | `micromsg.ChatroomDetailInfoExtData` |
|
|
177
|
+
| `contact.extra_buffer` | `micromsg.ContactExtData` |
|
|
178
|
+
| `stranger.extra_buffer` | **按骨架推到 `micromsg.ContactExtData`**(与 `contact` 主体同型)。⚠️ **2026-10-06 更正**:初版在这里挂的是 `micromsg.OpenIMContactExtData`,**大概率不对** —— 真正的 OpenIM blob 是 `contact.extra_buffer` 里那 **44 行**(`#1`/`#2` 逐字节命中 `openim_appid` / `openim_wording`),不是 `stranger`。两者同在一个编译单元,容易挂错。 |
|
|
179
|
+
|
|
180
|
+
同一编译单元还注册了 `WeclawExternalInfo` / `OpenIMKefuContactExtData` / `CustomInfoExtProfileInfo*` 等,
|
|
181
|
+
正好是 contact 库里"带 `ext_buffer` 的那几张表"那一整套。**所以 blob 是有类型的,我们能叫出类型的名字。**
|
|
182
|
+
|
|
183
|
+
**但字段名拿不到,而"客户端到底留没留名字"这件事现在说清了:**
|
|
184
|
+
|
|
185
|
+
- **明文只到"表的列名"这一层** —— 同一份 dll 里 `contact` 的列名就是明文数组
|
|
186
|
+
(`nick_name / quan_pin / … / extra_buffer / …`),`chat_room_info_detail` 的也是
|
|
187
|
+
(`room_id_ / … / ext_buffer_`);
|
|
188
|
+
- 这些**本地 message 的字段名不以明文出现**。挨着类型名的那几块是二进制(高熵、不可读),
|
|
189
|
+
把同一把密钥套上去还是乱的;
|
|
190
|
+
- ⚠️ **早前那版 3.1 里我把附近那批 `<长度><hex>` 记录当成"字段名被编码",是错的** ——
|
|
191
|
+
那批是客户端的**字符串池**,见 3.2。
|
|
192
|
+
|
|
193
|
+
**实际意义**:`#3`/`#4` 现在能追到「`micromsg.ChatroomExtData` 的第 3、4 号字段」这个粒度;
|
|
194
|
+
名字只剩 3.2 那条**候选**,要坐实还得换环境(另一个微信版本 / 另一台机器)。
|
|
195
|
+
|
|
196
|
+
### 3.2 客户端把一批字符串混淆了(2026-10-03 破掉)
|
|
197
|
+
|
|
198
|
+
`Weixin.dll` 里有一大批这种记录(本机数出 **41,913 条**长度自洽的;最长 580 字节):
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
0xa1 <4 个 ASCII 十六进制数字> 0xb1 <2N 个 ASCII 十六进制字符>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
(⚠️ 第一版我数出 41,557 条——那个正则的上限写成 200,把 **356 条 >200 字节的记录静默丢了**。
|
|
205
|
+
"数量对不上"的时候先怀疑自己的正则,别怀疑是别人漏了。)
|
|
206
|
+
|
|
207
|
+
前 4 位是小端 uint16,**正好等于**后面那串 hex 解出来的字节数(长度分布峰值在 8~21 字节)。
|
|
208
|
+
字节本身是 **明文 XOR 一条固定的逐位密钥**。判据:随便拿一条算出来的密钥去解全部 41k 条,
|
|
209
|
+
**每一位的可打印率都在 86%~100%**(随机密钥只会有约 37%;26 字节全可打印的概率约 1e-11)。
|
|
210
|
+
穷举式的"它是不是哈希"先做过一轮:拿已知列名做 md5/sha1/sha256/变体 895 组,**0 命中**。
|
|
211
|
+
|
|
212
|
+
密钥是这样拿到的:先用一条密文假设明文前缀是 `wechat::<表>::<字段>`,再用**逐位搜索**
|
|
213
|
+
(每位选"让解出来的字节最集中在标识符字符上"的那一档)——前 30 位就出人话了;剩下第 0 位
|
|
214
|
+
有个系统性偏差(差 `0x19`),用 crib 修掉(`rernel::`→`kernel::`、`jessionid:`→`sessionid:`、
|
|
215
|
+
`Iarameter`→`Parameter`);第 30 位之后按同样办法扩到 200 位。
|
|
216
|
+
|
|
217
|
+
解出来是**客户端的字符串池**:C++ 符号、源文件名、日志片段。抽样:
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
kernel::manager::ChatroomManager::GetChatroomMemberCount
|
|
221
|
+
kernel::manager::InitContactManager::CoRequestAndProcess
|
|
222
|
+
init_contact_manager.cc / contact_util.cc / windows_version.cc
|
|
223
|
+
'Parameter validation failed: P…'
|
|
224
|
+
',chatroom_seq=' ',contact_seq=' ' total_count_='
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**池里没有我们要的字段名**:`ChatroomExtData` / `ext_buffer` / `extra_buffer` / 任何 `micromsg.*`
|
|
228
|
+
全 **0 命中**;`member_seq` / `room_seq` / `member_version` / `room_version` 也全 0。
|
|
229
|
+
|
|
230
|
+
**对本文有用的是最后一行**:初始化联系人流程(`init_contact_manager.cc`)的日志键里有一个
|
|
231
|
+
**`chatroom_seq`**(和 `contact_seq`、`total_count_` 并排)。**但它跟 6.1 量到的 `#3`/`#4` 对不上**:
|
|
232
|
+
它出现在**账号级**的 InitContact 收尾日志里(同一段还打 `respCount`、` total_count_=`),
|
|
233
|
+
**日志里没有任何房间标识**;全池里"同时含 room 与 seq"的串**只有这一条**。⇒ 它更像
|
|
234
|
+
**"群列表整张表的游标"**,不该拿它给我们的"每群一个"的字段命名(两轮独立追查都指向这个结论)。
|
|
235
|
+
|
|
236
|
+
### 3.3 客户端**也有**明文字段名组 —— 但拿不到字段号
|
|
237
|
+
|
|
238
|
+
除了 3.2 那层被混淆的字符串池,客户端 `.rdata` 里还有**成组的明文"字段名清单"**
|
|
239
|
+
(本机数出约 3,764 组 / 76,022 个槽)。它们排成**"实体注册组"**:条目固定 0x20 字节
|
|
240
|
+
(`pool_ptr | tag | … | name_rva`),`tag ∈ {1,2,3,4}`,组内用 **4 字节"地址"** 指向名字串 ——
|
|
241
|
+
所以"哪些名字属于同一组"是**可复跑**的(**用名字的文件偏移**,全文件搜 `struct.pack('<I', off)`;
|
|
242
|
+
例:`InfoVersion` 有 4 处引用)。⚠️ **这个"地址"存的到底是什么,2026-10-06 量准了**:本版 `Weixin.dll` 是
|
|
243
|
+
**64 位**(imagebase `0x180000000`)、**每个节的 `VA ≠ PRAW`**(差 0x800~0x1800 不等),而表里存的**就是文件偏移** ——
|
|
244
|
+
按文件偏移搜能命中,按**真 RVA**(`段.VA + (off − 段.PRAW)`)或 **VA** 搜**全是 0**。所以本节此前写的"4 字节 RVA"**是叫法错**。
|
|
245
|
+
抽两组看形态(与 6.1 的 `InfoVersion` 同一片):
|
|
246
|
+
|
|
247
|
+
- `ChatRoomName | UserNameList | DisplayNameList | ChatRoomFlag | Owner | IsShowName | SelfDisplayName | RoomData`
|
|
248
|
+
- `strUsrName | nOrder | nUnReadCount | parentRef | strNickName | nStatus | nIsSend | nMsgType |
|
|
249
|
+
nMsgLocalID | nMsgStatus | nTime | editContent | othersAtMe | bytesXml`(**匈牙利命名** ⇒ 像 V3 时代/镜像
|
|
250
|
+
struct;这 14 个名字**跨两个实体**,边界在 `nUnReadCount` 之后)
|
|
251
|
+
|
|
252
|
+
**三条边界(前两条是负结果,第三条是纠错)**:
|
|
253
|
+
|
|
254
|
+
- **没有任何一张名字表带字段号**:全文件扫过 3,764 张表,"(名字, 小整数)"的配对里**没有编号表**;
|
|
255
|
+
名字旁的 `tag` 是 `{1,2,3,4}` 这种**结构标记**(从不出现 6/7,而 detail blob 实测字段号是 1/3/4/6/7)
|
|
256
|
+
⇒ **"名字表第 n 个 = 字段号 n"不成立**。
|
|
257
|
+
- **"名字表 → 具体 message 类型"这一层没有链**:类型名字符串本身几乎不被任何 RVA/VA 引用;而我们关心的
|
|
258
|
+
`ChatroomExtData` / `ChatroomMemberLocalData` / `ChatroomDetailInfoExtData` 在各自编译单元里
|
|
259
|
+
**根本没有字段名表**(那片区域只有类型名后缀 + `contact` 的 snake_case 列名)。
|
|
260
|
+
- ⚠️ **纠错(2026-10-04,根因 2026-10-06 才量准)**:本节早前一版写"全文件搜这些名字的指针引用都是 0"——**那是错的**。
|
|
261
|
+
根因:这张表**存的是文件偏移**,不是 PE 意义上的 RVA/VA(见上);**拿 RVA 口径去搜必然得到 0** ——
|
|
262
|
+
一次典型的**假阴性**(口径选错,输出跟真结论一模一样)。实际存在 **4 字节文件偏移引用**(`InfoVersion` 4 处),
|
|
263
|
+
以能一行复跑的那个版本为准。这也是两个子 Agent 互相核对出来的:一个报 0 引用,另一个复跑复现出 4 处。
|
|
264
|
+
|
|
265
|
+
**教训**:以后再看到"某个明文名字恰好和某个字段对得上",**先验证名字顺序能不能对上字段号**——
|
|
266
|
+
对不上就只能当线索,不能当名字(6.1 的 `InfoVersion` 候选就是这么撤掉的)。
|
|
267
|
+
|
|
268
|
+
**2026-10-04 又把范围扩到"本机所有腾讯系二进制"(84 个文件,含四个微信版本)**:
|
|
269
|
+
|
|
270
|
+
- 本机有四个**版本目录**:`4.1.15.11` / `4.1.15.13`(在 `D:\WeiXin\` 下)与 `4.1.12.26` / `4.1.13.12`
|
|
271
|
+
(在 `C:\微信\Weixin\` 下)。**都没有配套的数据目录**(数据只有一份,见 6.2)。
|
|
272
|
+
- ⚠️ **但 `4.1.15.11\` 不是"一个能跑的版本"**:2026-10-11 实测它只剩 **1 个文件**(`crashpad_handler.exe`,
|
|
273
|
+
2.1 MB),而 `4.1.15.13\` 是 29 个文件 / 867.8 MB。它是升级后没删干净的残留,任何指向它的加载都会失败。
|
|
274
|
+
所以下面那句"**三版** `Weixin.dll`"才是实际做过对比的数量 —— 别拿这个目录当"另一个版本"去对照。
|
|
275
|
+
- ⚠️ **判运行时版本,不要读 exe 里的版本字面量**:那是**兜底**(只在主路径加载失败时才走)。正确判据是
|
|
276
|
+
活进程命令行的 `--user-lib-dir`。2026-10-11 实测四个子进程都指向 `D:\WeiXin\4.1.15.13`。
|
|
277
|
+
- **三版 `Weixin.dll` 的明文名字组一字不差**,而且我们那三个类型
|
|
278
|
+
(`ChatroomExtData` / `ChatroomMemberLocalData` / `ChatroomDetailInfoExtData`)的**类型注册块逐字相同**
|
|
279
|
+
⇒ **老版本也没给我们那层字段名**。
|
|
280
|
+
- 84 个文件里**没有任何 `RoomData` / `RoomDataUser` 的字段名组**:`RoomData` 只作为那族 V3 名字组的**末位**出现。
|
|
281
|
+
- 唯一的跨版本**差异**:`roomCap`(那份公开 proto 里的字段名)在老版本 `4.1.12.26` 的 `ConfSdk.dll` 里有 **6 处**,
|
|
282
|
+
而 **4.1.15.13 的 22 个 exe/dll 里全是 0**。它是 V3/配置侧的遗留,**不能拿来给我们的字段命名**(何况
|
|
283
|
+
我们实测 `#5` 是成员 status 位 11 的投影,不是 `roomCap`)。
|
|
284
|
+
- 企业微信(WXWork)**只剩数据、主程序已卸载** ⇒ 那条路也不通。
|
|
285
|
+
|
|
286
|
+
**2026-10-06 把"读二进制拿名字"这条路封口(我复核过)**:
|
|
287
|
+
|
|
288
|
+
- **类型名不被引用**:`ContactExtData` / `ChatroomExtData` / `ChatroomMemberLocalData` / `WeclawExternalInfo`
|
|
289
|
+
的全文件 4 字节引用**都是 0**(只有 `ChatroomDetailInfoExtData` 有 2 处)。类型名那块里除了同族类型名
|
|
290
|
+
只有 `…\local_proto\local_contact.pb.cc` 的编译路径;每个类型名后跟的 `(VA,1)` 指针表指向的
|
|
291
|
+
要么是 **96 字节的密文块**(3 个种子 + 固定 XOR 链,跨版本不同),要么是**代码**(反汇编确认是方法 / vtable)
|
|
292
|
+
⇒ **"类型名 → 字段表"这一层没有可静态恢复的形式**。
|
|
293
|
+
- **全文件名字槽穷举**:43,411 个"对齐 + NUL 结尾"的标识符 → 1,729 个像字段名的连续串,其中
|
|
294
|
+
**没有一个是 4 个成员字段**(全文件唯一的 4 名字组是 `Ticket|AppId|Sex|DescWordingId`,与成员无关),
|
|
295
|
+
**也没有 ~37 个 contact 字段名**(长度 ≥25 的串全是别的领域:字体/样式令牌、支付、Qt 类名、Windows 常量)。
|
|
296
|
+
`RoomDataUser` / `users` / `roomCap` / `memberCount` **全文件 0 命中**。
|
|
297
|
+
- **硬负结果:连续挨着 ≠ 同一组**:`Announcement | InfoVersion | AnnouncementEditor | AnnouncementPublishTime | ChatRoomStatus`
|
|
298
|
+
这 5 个名字确实挨在一起,但 **`AnnouncementEditor` / `ChatRoomStatus` 的全文件引用数各为 0**
|
|
299
|
+
⇒ 组的真实边界由**描述符记录的槽**决定,**不能按"字符串相邻"划**(这也解释了为什么 44 个注册目标里只有 17~19 个含可读名)。
|
|
300
|
+
- ⇒ **`ContactExtData` 那 37 个字段名,从二进制里读(类型名引用 / 名字数组 / 字符串池 41,913 条记录)三条路都是 0**,
|
|
301
|
+
与 D-068 / D-072 的边界一致。**但"定名"不止这一条路**:2026-10-07 已凑齐四条并各自定名成功 ——
|
|
302
|
+
**(a) 跨表整值 join**(那 44 行的 `#1`/`#2`)、**(b) 自/他对照的内容刻画**(`#4`/`#9`)、
|
|
303
|
+
**(c) 公开码表 + 一条不依赖码表的预测**(`#5`)、**(d) 内容自带键名**(`#26`,见 §7)。
|
|
304
|
+
⇒ 有值可查的字段,**先按 (a)~(d) 问一遍**,再去谈读二进制(那条对本机是负的)。
|
|
305
|
+
|
|
306
|
+
**把 3,764 张表全看一遍之后,还得到三条**(都写在这里,省得后人重跑):
|
|
307
|
+
|
|
308
|
+
- **contact 库的"实体名"是明文**,而且是 snake_case:
|
|
309
|
+
`contact | group_contact | group_member | stranger | im_contact | im_group_contact | im_group_member |
|
|
310
|
+
im_stranger | chatroom | chatroom_detail` —— 我们的两张表在客户端里就叫 `chatroom` / `chatroom_detail`。
|
|
311
|
+
- **`chat_room_info_detail` 的列名有明文组**(`room_id_ | … | ext_buffer_`),但 **`chat_room`(= `chatroom` 实体)
|
|
312
|
+
的列名偏偏没有**;取而代之的是那族 **V3 风格**的 8 名字组
|
|
313
|
+
(`ChatRoomName | UserNameList | DisplayNameList | ChatRoomFlag | Owner | IsShowName | SelfDisplayName | RoomData`)。
|
|
314
|
+
⇒ **客户端自己给我们这个 blob 那个"成员"叫 `RoomData`**(V3 谱系:V3 时代成员在 `UserNameList` /
|
|
315
|
+
`DisplayNameList` 列里,V4 挪进了这个 blob)。
|
|
316
|
+
- **没有任何一组名字描述这个 blob 的顶层字段**:`users`、`roomCap`、`memberList`、`MemberVersion`、
|
|
317
|
+
`UserListVersion`、`MemberSeq`、`RoomVersion`(含 snake 变体)在 3,764 张表里**全 0 命中**。
|
|
318
|
+
⇒ `#3`/`#4` 的名字**不是"没找到",而是这一版客户端里就没有可读的存放处**(见 8 节的公开圈结论)。
|
|
319
|
+
|
|
320
|
+
## 4. 明确证伪的猜测(省得后人再试一遍)
|
|
321
|
+
|
|
322
|
+
对 `chat_room.ext_buffer` 顶层那个 `#3`(每个群一个数、与 `#4` 恒相等),验到这四条**否定**:
|
|
323
|
+
|
|
324
|
+
- ❌ 不是 `max/min(chatroom_member.member_id)`(20 个群 0 命中);
|
|
325
|
+
- ❌ 不是 `chat_room.id` —— 而且**厂商自己的 DLL 给出了这个字段的正解**:它内置的 SQL 是
|
|
326
|
+
`FROM name2id n LEFT JOIN chatroom_member c ON n.rowid = c.room_id`,也就是 `chat_room.id`
|
|
327
|
+
= `name2id.rowid`(本地行号),与 `#3` 那个大数字不是一个 id 空间;
|
|
328
|
+
- ❌ 不是群号(群 `username` 都是 `11 位数字@chatroom`,而 `#3` 是 9 位或 5 位);
|
|
329
|
+
- ❌ 不是消息会话表名 `Msg_<md5>` 里那个 md5 的输入(实测那 139 张表的输入是**群的 username**,
|
|
330
|
+
61/74 命中;`#3` 作为输入 0/74)。
|
|
331
|
+
|
|
332
|
+
**最强的一条**:把 24 个群的 `#3`(其中 **24 个是大值**,`≥ 10^8`)拿到 `db_storage` 下**全部 24 个
|
|
333
|
+
数据库**的**每一张表的每一个非二进制列**里找(文本列也按子串找),**大值命中 0 个**。
|
|
334
|
+
消息**内容**里也找过(第 8 节)—— 同样 0。
|
|
335
|
+
大整数不可能与别处的 id 撞车,所以结论是硬的:
|
|
336
|
+
|
|
337
|
+
> `#3` 只存在于 `chat_room.ext_buffer` 这个 blob 里;本机可见的任何表都不存它。
|
|
338
|
+
|
|
339
|
+
**能说到的程度**(2026-10-03 又往前推了一步,细节见 **6.1**):它**不是**每群固定的 id —— 同一天里
|
|
340
|
+
它会变(实测 +2);`#4` 与它恒等,是因为它俩是**一对版本号**(同一个计数器的两份:新版本 / 已落库版本,
|
|
341
|
+
见 6.1)。去掉底数之后的大小跟**成员数量/成员变动**走(秩相关 0.92 ~ 0.94),跟消息数几乎无关。
|
|
342
|
+
底数有两种(`10000`:47 个群;`7×10^8`:24 个群),跟记录进库的先后相关。
|
|
343
|
+
**它到底叫什么名字,本机判定不了** —— 那需要对照源(另一台机器 / 另一个微信版本 /
|
|
344
|
+
同一房间的两个时点快照)。**别再照着猜往下写代码。**
|
|
345
|
+
|
|
346
|
+
(`contact.extra_buffer` 的 `#3` 是同一种形状:取值在同行各列里找不到对应物。两张表的结论一样 ——
|
|
347
|
+
**这些 blob 字段不与本地 schema 镜像**,它们是厂商自己状态机里的位/标识,本地不存第二份。
|
|
348
|
+
所以"能不能解出来"这件事,卡点不在解码,而在**没有对照源**。)
|
|
349
|
+
(第一版扫描曾报"22 个库打不开",那是**我自己传错了钥匙**:把配置里的原始 key 当成了派生后的
|
|
350
|
+
hexKey。修对之后 24/24 全能打开 —— 这类"打不开"要先怀疑自己。)
|
|
351
|
+
|
|
352
|
+
## 5. 实用替代(不用解 blob 也能拿到大部分东西)
|
|
353
|
+
|
|
354
|
+
只要成员与显示名:`chatroom_member`(`room_id` → `member_id`)join `name2id` / `contact` 即可,
|
|
355
|
+
本地就能做。**blob 多出来的是两样**:每个人的 `status`(位标志)与 `inviter` —— 库里的表没有。
|
|
356
|
+
(`#5` **不算多出来的东西**:它等于"成员里 status 位 11 置位的那批 id"的投影,见第 6 节。)
|
|
357
|
+
|
|
358
|
+
本仓库现在**只解已验证的那部分**:`weflow-cli contact-schema`(脚本 `scripts/contact_schema.py`,只读本地)
|
|
359
|
+
输出每个群的成员(`userName` / `displayName` / `status` / `inviter`)与 `#5`
|
|
360
|
+
(JSON 键 `statusBit11Ids`),以及该群里**未识别**的字段号。
|
|
361
|
+
|
|
362
|
+
```
|
|
363
|
+
weflow-cli contact-schema -n 3 # 看 3 个群
|
|
364
|
+
weflow-cli contact-schema --json # 机器可读
|
|
365
|
+
weflow-cli contact-schema --contacts -n 5 # 改看**联系人**的 extra_buffer(见下)
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**联系人那条(`--contacts`,2026-10-07 加)**:同一个命令读 `contact.extra_buffer`,输出**同样只限已验证**的三个字段 ——
|
|
369
|
+
`region`(`#5` 国家/地区码,只在"恰好两个大写字母"时给)、`bizType`(`#13` = `biz_info.type`)、
|
|
370
|
+
`updatedAt`(`#41` 资料最近更新时间,`0` 当未设置)。**故意不打印 `#4`/`#9`**:它们是
|
|
371
|
+
"该账号自身资料文本的汇集"(含主体名/菜单按钮名),**是内容不是字段名**,只以长度出现在 `unrecognized` 里;
|
|
372
|
+
OpenIM 那 44 行的小形状只报 `kind='openim'`;其余字段未定名,原样进 `unrecognized`(取舍见 D-068 / D-088)。
|
|
373
|
+
|
|
374
|
+
两条边界写死在实现里(见 D-068):
|
|
375
|
+
|
|
376
|
+
- **未识别字段原样带出、不猜名字**:顶层 `#3`/`#4` 进 `unrecognized`,键就是字段号。一旦给它编个
|
|
377
|
+
像样的名字,下游迟早会拿一个猜出来的含义写逻辑。
|
|
378
|
+
- **解不动就报错**:坏 blob 抛异常并进 `failures`,**绝不返回空成员表** —— "解不动"和"这个群没人"
|
|
379
|
+
是相反的两件事。
|
|
380
|
+
|
|
381
|
+
群昵称那条路仍然走原生 DLL(`wcdb_get_chat_room_ext_buffer`,见 `src/core/wcdbCore.ts`);
|
|
382
|
+
`contact` 库的其余二进制列(`stranger.extra_buffer` 的 38 个字段等)**仍未解**,见上面第 3 节。
|
|
383
|
+
|
|
384
|
+
## 6. 位域:那些"未定"的字段,有一半能定性到形状
|
|
385
|
+
|
|
386
|
+
前面标 ❓ 的字段里,有一部分能再往前推一步:**它们是位标志,不是枚举**。
|
|
387
|
+
|
|
388
|
+
- **群成员 `status`**(`chat_room.ext_buffer` 成员 `#3`):实测 27 种取值,出现过 **8 个位** ——
|
|
389
|
+
`0, 3, 4, 11, 13, 20, 21, 22`;其中位 0 占绝大多数(4037 次),像"正常成员"。
|
|
390
|
+
⚠️ **"非零时最低位恒为 1" 这条假设是错的**(`8` = 只有位 3、`8192` = 只有位 13 单独出现过)——
|
|
391
|
+
这是我先提出来、再被自己的数据否掉的,记在这儿免得后人又当约定用。
|
|
392
|
+
- **`contact.extra_buffer` 的 `#3`**:5 种取值(0/3/9/1/515),出现过 4 个位 `0, 1, 3, 9`。
|
|
393
|
+
|
|
394
|
+
**已经定下来的两条"位 ↔ 含义"(2026-10-03,都由子 Agent 提出、我独立复核)**:
|
|
395
|
+
|
|
396
|
+
- **位 11(2048)↔ `#5`**:`chat_room.ext_buffer` 顶层 `#5` 的 id 集合与"该群成员里 `status & 2048`
|
|
397
|
+
的那批人"**逐群完全相等(77/77)**;全库 4151 个成员里带 bit11 的正好 **28 个**,与 `#5` 的
|
|
398
|
+
28 条目**一一对应,一个不多一个不少**。⇒ `#5` 是成员列表的**投影**,没有独立信息;
|
|
399
|
+
**早前把它当"额外的参与者 id"是错的**(见 D-070)。
|
|
400
|
+
- **`chat_room_info_detail.chat_room_status_` 的位 17(131072)↔ "本群与企业微信/openim 互通"**:
|
|
401
|
+
置位的 6 个群**全部**有 `@openim` 成员或群主;不置位的 71 个**全部**没有(77/77)。
|
|
402
|
+
|
|
403
|
+
**成员 `status` 的"字段"读法(2026-10-05,子 Agent 提出、我逐条复核)** —— 27 个取值写成位集合后,它
|
|
404
|
+
**恰好**切成几个**不重叠的位区间**(没有进位、没有混合进制):
|
|
405
|
+
|
|
406
|
+
| 字段 | 位 | 取值 | 人数 | 最好判别式 / 结论 |
|
|
407
|
+
| --- | --- | --- | --- | --- |
|
|
408
|
+
| F0 | bit0 | 0/1 | 100/4051 | **没解出成员级语义**:缺它的 100 人高度房间集中(一房占 64、3 个房整群缺)⇒ 读成"该房这些成员行处于另一套状态" |
|
|
409
|
+
| A | bits3–4 | 0/1/2/3 | 3625/226/254/46 | **是一个 2-bit 字段,不是两个 flag**(bit3&bit4 同现 **46**,独立期望 19.7 ⇒ 富集 2.3×)。最强倾向 = **有群昵称**(A=0→3 的有昵称率 0.26→0.43→0.68→0.78,单调),但**31% 载体没昵称、逐房集合相等 0/33 ⇒ 不是定义**。⚠️ **2026-10-06 再削一档**:控制房间后 raw OR 6.19 → **MH 2.05**(≈2/3 是"昵称房"混杂),**且 bit3 房内一样强(1.95)⇒ 不是 bit4 专属**;"偏老成员"在**房内不成立**(最老 1/3 = 0.353 vs 基线 0.333,p=0.42) |
|
|
410
|
+
| B | bit11 | 0/1 | 4123/28 | **= 顶层 `#5` 的投影**(已解,不重解) |
|
|
411
|
+
| C | bit13 | 0/1 | 3729/422 | 房间级相关 = openim(0.668 vs 0.264);**但成员级有一条(2026-10-06 新增,我复核)**:房内载体**系统性更沉默** —— P(在这个群发过言) **0.087 vs 0.208**、房内 MH OR **0.40**、19/20 个房同向。这是全轮唯一稳的成员级脚印(不给语义名,但"成员级没找到"这句**作废**) |
|
|
412
|
+
| G | bits20–22 | 2/3/6/7 | 4/3/5/18 | **没解出来**(列 8 房 / 30 人,画像全平);但**不是三个 flag**(见下) |
|
|
413
|
+
|
|
414
|
+
- **作用域**:位 `3/4/11/13/20/21/22` 全是**房间内成员**的状态,不是"这个人"的属性 —— 同一 `username`
|
|
415
|
+
在不同群里带同一位的一致率只有 **0~12%**(对照组:位 0 是 **93%**)⇒ "某位 = 这个人是 X"**整类被否掉**。
|
|
416
|
+
这一条比"某一位的含义"更有用:它把搜索空间从"人的属性"缩到"群内状态"。
|
|
417
|
+
- **两条硬排斥(0/4151,非小样本)**:`bit4 ⊥ bit13`(独立期望 30.5)、`bit11 ⊥ {bit13,bit20,bit21,bit22}`(四对全 0)。
|
|
418
|
+
⚠️ `bit4 ⊥ bit13` **不是**"同一字段的两个取值"——两者外延**相反**(bit4 载体多有群昵称 0.693、偏老房;
|
|
419
|
+
bit13 载体少昵称 0.123、偏 openim / 大房),不是共享语义的枚举。
|
|
420
|
+
- **G 是"有强制位的 3-bit 组"**:取值只出现 {2,3,6,7} ⇒ **G≠0 时 bit21 恒置位**(bit20 / bit22 可选);
|
|
421
|
+
bit20&bit22 同现 **18** 人,独立期望 0.12 ⇒ **富集 ~150×**。
|
|
422
|
+
⚠️ **更正**:本节此前写这三位的"取值范围只有 `1/3/5/7`"——那是**当独立标志读出的错值**
|
|
423
|
+
(`1` = 只 bit20、`5` = bit20+bit22 这两种本机**不存在**);正确的组值只有 {2,3,6,7}。
|
|
424
|
+
- **一条判据更正**(不是某一字段的结论):"分布集中 ⇒ 解出来了"**不成立** —— 5 个字段里分布最像枚举的
|
|
425
|
+
G,恰恰最解不出来。字段读法**没有**产生比按位更干净的判别式,它唯一多给的信息是 **A 是 2-bit、G 有强制位**。
|
|
426
|
+
- **2026-10-06 换外部信号重打一遍**(用"该成员**在这个群里发过言吗**"当分辨率;只数条数,不读文本;
|
|
427
|
+
发信人经该分片的 `Name2Id` 还原到 username 再与 blob 成员表对齐)。结论是**"某位 = 某种人"的行为读法
|
|
428
|
+
基本全落空**,只留一条:
|
|
429
|
+
- **bit13 是唯一有房内脚印的位**(见表):**19/20 房同向、MH OR 0.40**(我独立算过;另一路量到 0.090 / 0.187、0.46)。
|
|
430
|
+
- **bit3 的"更活跃"是房间级**:全样本 P(sent) 0.279 vs 0.170 看着像,但房内符号 **pos 15 / neg 13 / zero 11**、
|
|
431
|
+
不加权均值差 **−0.002** ⇒ 是"哪些房有 bit3 载体",不是"bit3 的人更活跃"。
|
|
432
|
+
- **G 的"更爱发言"全来自一个房**:10 个发过言的 G 载体里 **8 个在同一房**;另外 6 个 G 房载体**合计 0 条**
|
|
433
|
+
⇒ G 仍 UNKNOWN,且**不得外推**。
|
|
434
|
+
- **bit0 = 0 的足迹测不到**:那 100 人里 **74 人在没有消息表的房里**(`sent = 0` 是"量不到"不是"沉默"),
|
|
435
|
+
房内置换 p=0.26 ⇒ 既不支持也不否证。⚠️ 顺带**降级**"bit0 跨群一致率 93%"这条:缺位者本来就挤在 13 个房里,
|
|
436
|
+
这个一致率被"他们只在这几个房"抬高,**不能当"bit0 是人的属性"的证据**(A2 报告内部在这点上自相矛盾,本轮不裁决)。
|
|
437
|
+
- **两个"进群先后"代理其实是同一个排序**(`chatroom_member.rowid` 分位 vs blob 成员位置分位,
|
|
438
|
+
**76/77 房完全同序**)⇒ 别当两条独立证据用(此前把它们并列是重复计数)。
|
|
439
|
+
- **分母**:15/77 个房**没有任何消息表**,所有涉"发过言"的结论只到 **62 房**;另有 3.36% 的房内消息
|
|
440
|
+
归到的人**不在当前成员表**(已退群/其它身份)⇒ 个别成员的条数只会偏低,不会偏高。
|
|
441
|
+
- **已否掉的读法**(列出来省得再试):位 = 好友 / 位 = 群主 / 位 = 我自己
|
|
442
|
+
—— **群主与本机账号自己基本都只是 `status = 1`**。
|
|
443
|
+
|
|
444
|
+
**顺带核清的几列**(`contact` 共 22 列,其中这几列是标志型):`delete_flag` **全 0**(4014 行 ⇒ 取关是硬删行);
|
|
445
|
+
`is_in_chat_room` = `0/1/2`;`chat_room_type` = `0(4010)/2(4)`;`chat_room_notify` = `0(3960)/1(54)`。
|
|
446
|
+
|
|
447
|
+
**`verify_flag` 解出来了(2026-10-06,逐位列联 + 我复核)** —— 它是**位掩码**、**9 种**取值(此前的清单漏了 3 个单例):
|
|
448
|
+
`0(3327)/8(356)/24(241)/56(1)/264(1)/520(54)/776(11)/1048(22)/1053(1)`,用到位 `{0,2,3,4,5,8,9,10}`
|
|
449
|
+
(所谓"6 个档"其实是 5 个位的组合:`520 = 8+512`、`776 = 8+512+256`、`1048 = 8+16+1024`)。
|
|
450
|
+
**它不是"加过好友 / 待验证"** —— 能证伪的计数:`≠0 且不在 biz_info = 0`、`≠0 且有 remark = 0`、
|
|
451
|
+
`≠0 且是群成员 = 0`、2946 个 `wxid_` 联系人里 2941 个为 0、群验证申请表 `room_verify_application` 是**空表**。
|
|
452
|
+
逐位(都是我复核过的):
|
|
453
|
+
|
|
454
|
+
- **`bit3`(值 8)⟺ 该 `username` 在 `biz_info` 里**:**687/687 双向、两个错格都是 0**;
|
|
455
|
+
而且**所有非零值都带 bit3** ⇒ "8" 本身就是"非零"。
|
|
456
|
+
- **`bit4`(16)⟺ 有 `VerifySource`(机构认证)**:265/265,反向 2 个反例(0.75%)。
|
|
457
|
+
- **`bit8`(256)⟺ 有 `PersonVerifyInfo.VerifyDesc`**:12/12 双向精确。
|
|
458
|
+
- **`bit9`(512)⟹ 有 `PersonVerifyInfo`(个人主体)**:65/65,反向 7 例;且 **`bit4 ⊥ bit9`(0 行)**
|
|
459
|
+
⇒ 机构认证与个人认证互斥。
|
|
460
|
+
- `bit10 ⊆ bit4`(23 行、全是 `VerifyBizType = 2`),位名 UNKNOWN;`bit0/bit2/bit5` 各 1 行,UNKNOWN。
|
|
461
|
+
|
|
462
|
+
**并且三条是"同一批 687 行"的三面(都零反例)**:`contact.extra_buffer` 出现 `#13` ⟺ `verify_flag ≠ 0` ⟺ 在 `biz_info` 里
|
|
463
|
+
(`verify_flag ≠ 0` 的行**非大 proto 的 0 个**)。这也就是此前"`verify_flag != 0` ⟹ 不在任何群"那条的**原因** ——
|
|
464
|
+
它们全是 biz / 系统号,本来就不在群里(反过来不成立:非群成员里也有 545 行为 0)。
|
|
465
|
+
|
|
466
|
+
**`chat_room_status_`(房间级,32 位标志)—— 2026-10-05 又解出两位(含"众数"那一位)**:
|
|
467
|
+
|
|
468
|
+
- **位 19(524288)⟺「该房已初始化(`status ≠ 0`)**且**与 openim 不互通」**:**77/77 完全一致**(我独立复核过)。
|
|
469
|
+
这就解释了它为什么是**众数(51/77)而不是常量**——openim 群与 `status = 0` 的新群**都不带**这一位。
|
|
470
|
+
⇒ 文档里此前"位 19 只是众数、与 `#3` 的族没有干净关系"的写法可以收起来了:**它有确切含义,只是与 `#3` 无关**。
|
|
471
|
+
- **位 2 与位 31 恒同现**(77/77)⇒ 在本机数据上**这两位的含义分不开**,别单独解释其中一位。
|
|
472
|
+
- 位 `14 / 21 / 27` **没解出来**(各 1~3 个群,样本太小)。
|
|
473
|
+
- `chat_room_status_ = 0` 的 16 个群**全部**是 id ≥ 3327 的新群,但**不是**"最新 16 个"
|
|
474
|
+
(有 4 个更新的群取值不为 0)。
|
|
475
|
+
|
|
476
|
+
**`contact.extra_buffer` 的 `#3`(逐位重做后,2026-10-05)**:4 个位 `0/1/3/9` ——
|
|
477
|
+
|
|
478
|
+
- **`bit0` ⟺ 值 ≠ 0**(这是恒等式,不是发现);
|
|
479
|
+
- **`bit1 ⊂ bit0`**:`bit1 = 1` 的 **423 行全部是 `local_type = 1` 且不在任何群里**(423/423 单向干净);
|
|
480
|
+
- `bit3`(2 行)/ `bit9`(1 行)**没找到**判别式 —— 样本太小,且本机没有第二个参照实例。
|
|
481
|
+
- ⚠️ 早前一版写的"取值本身无从判定",是**拿整值(0/3/9/1/515)做列联表**得出的结论;
|
|
482
|
+
对位域而言粒度就是错的(两个位会互相抵消)。逐位重做后才有上面两条 —— **这是本轮的一处方法更正**。
|
|
483
|
+
|
|
484
|
+
**这解释了为什么它们找不到对应列**:位标志是**集合**,不会与任何单一列镜像 ——
|
|
485
|
+
所以"跟每列做列联表"这种找法注定失败,不是我没找到。
|
|
486
|
+
|
|
487
|
+
### 6.1 `#3` / `#4` 那一对(2026-10-03 追出来的)
|
|
488
|
+
|
|
489
|
+
这是全篇最想解的一个:每个群一个数、`#3 == #4`、只在 blob 里、跟本地任何列都对不上。
|
|
490
|
+
现在能说到下面这个程度 —— **下面每条都是本机 77 行实测,可复跑**(脚本见第 9 节)。
|
|
491
|
+
|
|
492
|
+
**① 它是"同一个量写了两遍"的一对数 —— 但这条是拿 `chat_room_info_detail` 证出来的。**
|
|
493
|
+
`chat_room` 自己的 `#3`/`#4` **77/77 恒等**(不等的 0 个),所以在它自己身上看不出"两份拷贝";
|
|
494
|
+
能看出的是结构相同的 `chat_room_info_detail.ext_buffer_`:同一对字段号 `#3`/`#4` 在 **5 行** 上
|
|
495
|
+
`#3` 比 `#4` **大 1**(`room_id_` = 331 / 2007 / 2148 / 2869 / 3968,其余 72 行相等)。
|
|
496
|
+
|
|
497
|
+
**② 那 5 个群另有一个独立标志,而且正好是同一批 —— 但"差 1"是 detail 的性质,不是 chat_room 的。**
|
|
498
|
+
`chat_room.ext_buffer` 的成员条数 vs 关系表 `chatroom_member`:**5 个群不相等**,一律是
|
|
499
|
+
"blob 比表**多 1 个人**"(从不更少),多出来的那个人在 `contact` / `name2id` 里都在;
|
|
500
|
+
**这 5 个群正好就是上面那 5 个**(77 里挑 5,两次挑中同一批)。
|
|
501
|
+
⚠️ **但把 detail 的配对语义搬回 `chat_room` 不成立**:在那 5 个"成员未落库"的群里,
|
|
502
|
+
`chat_room` 的 `#3`/`#4` **仍然恒等**;而且两组取值**从不相等**(0/77),本就是两套编号。
|
|
503
|
+
所以"`#3 != #4` 是 pending 标志"这条**只对 `chat_room_info_detail` 的那一对成立**。
|
|
504
|
+
(另一种同样与数据相容的读法:三个存储——chat_room blob / detail blob / `chatroom_member` 表——
|
|
505
|
+
落后程度不同;**只有一次快照时区分不了**。别把这条读成结论。)
|
|
506
|
+
|
|
507
|
+
**②b 但从这些数据能看到一件确定的事:它俩会变,而且库里自带"变更前/变更后"两份。**
|
|
508
|
+
不需要等下一次变更:同一时刻库里就并排存着"成员比关系表多 1 个"和"detail 的 `#3` 比 `#4` 大 1"
|
|
509
|
+
这两种落后状态(5 个群,两个独立 loci 同时命中)。
|
|
510
|
+
|
|
511
|
+
**③ 去掉底数之后,那个数跟着"成员"走,不跟消息走。**
|
|
512
|
+
|
|
513
|
+
| 分组 | 房间数 | 取值范围 | 与成员数的秩相关 | 与消息数的秩相关 |
|
|
514
|
+
| --- | --- | --- | --- | --- |
|
|
515
|
+
| `10000 + x` | 47 | 10000 ~ 11379(x = 0 ~ 1379) | **0.937** | 0.043 |
|
|
516
|
+
| `700000000 + x` | 24 | 700000010 ~ 700015304(x = 10 ~ 15304) | **0.749** | 0.395 |
|
|
517
|
+
| 小值 | 3 | 1254 / 1304 / 2021 | 样本太少 | — |
|
|
518
|
+
| `0` | 3 | 0 | 样本太少 | — |
|
|
519
|
+
|
|
520
|
+
**④ 底数有两种,而且是"梯度"不是硬分界;两种底数的年龄差得很开。**
|
|
521
|
+
|
|
522
|
+
- 47 个群底数 `10000`、24 个底数 `7×10^8`、3 个是 1~2k(1254/1304/2021)。按 `chat_room.id`
|
|
523
|
+
(≈ 进 `name2id` 的先后;`id == name2id.rowid` 已验 77/77)看:`7×10^8` 那批中位 id **1770**、
|
|
524
|
+
`10000` 那批 **2865**;`Spearman(#3 − 底数, id) = **−0.420**`,最高那档 id 里 `7×10^8` **0 个**、
|
|
525
|
+
`10000` **12 个**。⇒ 趋势明显,但**没有任何一条**(id / 公告时间 / 成员数 / status)能把两族干脆分开。
|
|
526
|
+
- **年龄**:用**公告发布时间**(真实墙钟;不用消息表的 `create_time`,那只是本机保留窗口的起点)——
|
|
527
|
+
`7×10^8` 族最早的公告在 **2019-01-27**,`10000` 族最早只到 **2024-04-13**。⇒ `7×10^8` 那批**更老**。
|
|
528
|
+
- **第三种来源那 3 个群 = 企业微信/openim 互通群**:它们的 `owner` 与公告编辑者**都是 `@openim`**,
|
|
529
|
+
且都带 `chat_room_status_` 位 17(= 与 openim 互通,见第 6 节)。但 openim **不是充分条件** ——
|
|
530
|
+
另外 3 个沾 openim 的群落在 `7×10^8` 族。**为什么分成这样,本机定不了。**
|
|
531
|
+
- 3 个 `#3 = 0` 的群**正好**是 3 个 `owner` 为空的群(没初始化)。
|
|
532
|
+
- **底数不是"某个计数器的历史读数",更像"固定种子/号段底"**:本机唯一另一处"服务端 seq 空间"是收藏库
|
|
533
|
+
(`fav_db_item.update_seq ∈ [7.54e8, 8.88e8]`、`fav_tag.seq = 8.41e8`),它**2021-11 就已经是 8.41e8**;
|
|
534
|
+
而一个**公告时间是 2024-05** 的房,底数仍是 `7×10^8`。⇒ 时间线对不上"历史读数",
|
|
535
|
+
只能读成**一个固定的号段底**(`10000` 与 `7×10^8` 同形:都是整值底)。
|
|
536
|
+
|
|
537
|
+
**⑦ 它不是服务端 seq,也不是"唯一序号"(2026-10-04,多智能体复核,两条都硬)。**
|
|
538
|
+
|
|
539
|
+
- **不是服务端 seq**(三条独立证据互相印证):
|
|
540
|
+
- 42/42 个 `10000` 族房满足 `#3 < 该房自己 MIN(非零 server_seq)`,差值中位 **8.72×10^8**;
|
|
541
|
+
45 个"最后成员变更落在本机保留窗口内"的房里,变更当时的 `server_seq` 已是 **8.7e8** 量级,
|
|
542
|
+
而它们的 `#3` 仍是 `10000~11379` —— 差 5 个数量级;
|
|
543
|
+
- **`server_seq` 的取值空间里根本没有这一段**:全库 **84,279 行**消息中,`server_seq ∈ [10^4, 8×10^8)`
|
|
544
|
+
的 **0 行**(全库唯一的小非零 seq 在 1 张非群会话表里,值 75~663);
|
|
545
|
+
- 2024~2026 年"出生"的房仍停在 `10000+x`,而这个账号的服务端 seq **2021-11 就已是 8.41e8**。
|
|
546
|
+
- **不是唯一序号**:`#3` **精确等于 `10000` 的房有 7 个**(外加其它重号)——服务端分配的序号不会这样。
|
|
547
|
+
⇒ 与"**本地从常量 `10000` 起算、随本地事件递增的每房计数器**"一致。
|
|
548
|
+
- 与它一致、**没被证伪**的读法是"每房一个本地计数器";被证伪的是"服务端每房唯一版本号"。
|
|
549
|
+
两个还算不上的(底数 `10000` 是客户端常量还是服务端给的种子、`7×10^8` 到底谁发的)**都需要机外对照源**。
|
|
550
|
+
|
|
551
|
+
**⑧ 它数的是"成员表的历史变更",不是人数、也不是当前规模(2026-10-04)。**
|
|
552
|
+
|
|
553
|
+
- **不是"曾经出现过的人数"(硬)**:`name2id` **无洞**(`rowid ∈ [1, 4025]`、`COUNT = 4025`),
|
|
554
|
+
且 `chatroom_member.member_id == name2id.rowid`(**4146/4146 全命中**)⇒ 本机历史上**可区分的人 ≤ 4025**;
|
|
555
|
+
可是 `chat_room.id = 332` 的 offset 是 **15304**(3.8 倍)⇒ 不可能是人数。
|
|
556
|
+
- **不是当前成员数的函数(硬)**:同族同规模能差 9 倍 —— `id = 3968`(28 人)offset **6** vs
|
|
557
|
+
`id = 326`(24 人)offset **54**;比值 `offset/成员数` 在同一族里从 **0 连续变到 6.38**。
|
|
558
|
+
- **正面证据(最硬的一条)**:offset **= 0 的 7 个房**(`#3` 精确等于底数),在 `chatroom_member` 里
|
|
559
|
+
**各自只有一段连续 rowid、段长正好等于成员数**,且保留窗口内**没有任何成员变动系统消息**
|
|
560
|
+
(唯一一条是"邀请你和…加入了群聊"——那是建群/入群通知)、**也没有公告**
|
|
561
|
+
⇒ **offset=0 正好落在"成员表自建群起从未动过"的房上**。这就是"计数器初值 = 底数"的正面证据。
|
|
562
|
+
- **它还随"变动跨的时间窗"增长**:用 `chatroom_member.rowid`(成员表插入先后)算时间窗,
|
|
563
|
+
`ρ(offset, spread) = 0.815 / 0.801`,**控制住成员数后仍有 +0.477 / +0.386**。
|
|
564
|
+
- ⚠️ **方法坑(记下来免得再踩)**:用 `name2id.rowid` 当"成员进群先后"是**错的代理** ——
|
|
565
|
+
那是"此人进**我的联系人库**"的时刻(很多房最早的成员是很早的老联系人),控制规模后 partial ≈ **0**,
|
|
566
|
+
会得出"只跟当前人数走"的错结论;**要改用 `chatroom_member.rowid`**。
|
|
567
|
+
- 顺带验掉一个隐患:`chatroom_member` 的行**确实会被删**(`rowid ∈ [1, 4603]`、`COUNT = 4146` ⇒ **457 个洞**;
|
|
568
|
+
表不是 AUTOINCREMENT)⇒ 它的 rowid-max 只能当"历史人数"的**下界**,不能当等号。
|
|
569
|
+
|
|
570
|
+
**⑤ 它会变。** 同一天里 `chat_room.id = 299` 那个群的值从 `700002082` 变成 `700002084`(约 1 小时,
|
|
571
|
+
+2);同一房间 9 秒内连读三次不变。⇒ 它是客户端在维护的状态,不是冻在库里的 id。
|
|
572
|
+
|
|
573
|
+
**⑥ 它仍然只在那个 blob 里。** 第 4 节那条"大值在全部 24 个库的每一列里命中 0"依然成立。
|
|
574
|
+
|
|
575
|
+
**没定的还是"名字"和"底数从哪来"。** 上面证的是**行为**(跟成员规模走、两种底数、会变、
|
|
576
|
+
`#3`/`#4` 是同一个量写两遍 —— 最后这条由 detail 的那对证出来);**类型的名字**有了
|
|
577
|
+
(`micromsg.ChatroomExtData`,见 3.1),但**字段的名字**在客户端里**不以明文出现**(见 3.1)。
|
|
578
|
+
|
|
579
|
+
**`chatroom_seq` 那条候选已经降级**:破掉字符串混淆后(3.2)它确实存在,但两轮独立追查都指向
|
|
580
|
+
**账号级的"群列表游标"**(出现在 `init_contact_manager.cc` 的 InitContact 收尾日志里,
|
|
581
|
+
与 `,contact_seq=`、` total_count_=`、`respCount` 并列,**日志里没有任何房间标识**);
|
|
582
|
+
全池里"同时含 room 与 seq"的串**只有这一条**,没有任何一处把某个群和一个 seq 一起打印。
|
|
583
|
+
⇒ **它与"每群一个"的实测对不上,不能当名字。**
|
|
584
|
+
|
|
585
|
+
**`InfoVersion` 那条候选也撤了**(2026-10-04)。客户端里确实有一组**明文**字段名
|
|
586
|
+
`Announcement / InfoVersion / AnnouncementEditor / AnnouncementPublishTime / ChatRoomStatus`,
|
|
587
|
+
其中 `AnnouncementPublishTime` 与我们**已独立验证**的 `chat_room_info_detail.ext_buffer_` 的 `#6` 吻合
|
|
588
|
+
(25 行里 23 行)——看上去"同结构里 `#3`/`#4` 就是 `InfoVersion`"。**这个推断被两条独立证据否掉**:
|
|
589
|
+
|
|
590
|
+
1. **名字顺序 ≠ 字段号**:把这 5 个名字读成字段号 1..5 的话,`AnnouncementPublishTime` 应落在 **`#4`**;
|
|
591
|
+
而库里它实际在 **`#6`**(`#4` 是个小 varint,且恒等于 `#3`)。⇒ 这张表**不能**用来给任何字段号命名。
|
|
592
|
+
(那片 `.rdata` 里旁边就是匈牙利命名的 `strUsrName / nOrder / nUnReadCount / parentRef / nStatus / …`,
|
|
593
|
+
像是 **V3 时代/镜像 message** 的字段清单,不是我们这个 blob 的。)
|
|
594
|
+
2. **行为对不上**:detail 那个 `#3` 跟**成员侧**走(ρ **0.889** / 0.773),跟公告侧 **≤0.55**;
|
|
595
|
+
**控制住成员数之后,公告侧的解释力塌到 ≈0**(偏相关 +0.204 / +0.282 / **−0.013**,成员侧仍 **+0.700**)。
|
|
596
|
+
唯一能抓到"多一次变更"的天然对照——`#3 = #4 + 1` 的那 5 个房——**正好**是
|
|
597
|
+
"blob 成员比关系表多 1 人"的那 5 个房,**且这 5 个房公告内容全为空**。
|
|
598
|
+
另外有**双向反例**:`id = 2764`(325 人、两条公告,`#3` 只有 **9**)同时反掉"跟成员数"和"跟公告数"
|
|
599
|
+
两种简单读法 ⇒ 它**不是当前状态的函数**,更像**事件累计计数**(`id = 329`:46 人、零公告、`#3 = 1897` 同理)。
|
|
600
|
+
|
|
601
|
+
要坐实字段名仍得换环境(另一台机器 / 另一个微信版本 / 同一房间的两个时点快照)。
|
|
602
|
+
所以它**继续留在 `unrecognized` 里只报字段号,不编名字**(见 D-068、D-069、D-070、D-071)。
|
|
603
|
+
|
|
604
|
+
**顺带**:成员列表的顺序不是按名字排的("我"在 76 个群里位置从 0 到 378 都有),
|
|
605
|
+
而"我在列表第一个"的 17 个群里 **16 个属于 `10000` 那一族**(按房间大小折算,随机也该有 5.4 个)——
|
|
606
|
+
顺序像是"加入先后",且底数与它相关。这条只是相关性,不够下结论,记在这儿备用。
|
|
607
|
+
|
|
608
|
+
**⑨ "一步"到底是什么?2026-10-07 试了一条新路,把三种候选读法**全部否掉**。**
|
|
609
|
+
群里那条"成员变更系统消息"(基础 `local_type == 10000`,全库 **1,764** 条)本身就带时间戳,
|
|
610
|
+
所以这题**不必等第二个快照** —— 直接对:`off = #3 − 底数` vs ① 事件条数(一步 = 改一次)
|
|
611
|
+
② 加入人数(一步 = 进一个人)③ 加入 − 退出(一步 = 净变化)。**三种全否**(n = 71):
|
|
612
|
+
|
|
613
|
+
| 对照 | 秩相关 | 差 0 的房 |
|
|
614
|
+
| --- | --- | --- |
|
|
615
|
+
| `off` vs **事件条数** | 0.168 | 3/71 |
|
|
616
|
+
| `off` vs **加入人数** | 0.143 | 4/71 |
|
|
617
|
+
| `off` vs **加入 − 退出** | 0.144 | 4/71 |
|
|
618
|
+
| `off` vs **当前成员数**(对照,既有结论) | **0.911** | 1/71 |
|
|
619
|
+
|
|
620
|
+
- **决定性反例(排掉"窗口截断"这个借口)**:5 个 **2026-08 之后才建**的房,整段历史都在保留窗口内,
|
|
621
|
+
而 `off` 仍然**大于它任何可能的加入总数**。我复核了其中的 `chat_room.id = 3906`:起止 **2026-08-31 → 2026-09-28**、
|
|
622
|
+
当前 **36** 人、类型 10000 的消息**全部**只有 **48** 条(子 Agent 按"成员变更"归类后是 **9** 条),而 **`off` = 104**
|
|
623
|
+
—— 用**最大**的那个分母(48)也远远不够。⇒ **"一步 = 进一个人"与"一步 = 净变化"硬否,且不是截断造成的**
|
|
624
|
+
(另 4 个:3778 `off` 65 / 24 条、3756 31 / 4、3928 12 / 3、3996 21 / 10 —— 一律"`off` > 全类型 10000 条数")。
|
|
625
|
+
- **差额跟"房多大"走,不跟"够不够老"走**:`off − 事件条数` 与**成员数**同向 **0.897**、与**窗口天数**只 **0.363**。
|
|
626
|
+
- **一条能省后人功夫的事实**:主动退群通知 **`left` = 0 / 1,622**(微信不给普通成员发退群通知),
|
|
627
|
+
`removed` 全库也只有 1 条 ⇒ "净变化"的**减半边从数据侧就不可测**,这条读法本来就立不住。
|
|
628
|
+
- **顺手核的两条**:`off` = 0 的房**恰好 7 个**(2024/2887/3348/3601/3668/3729/4025),每个只含"创建邀请"通知或 0 条
|
|
629
|
+
—— 与 ⑧ 那条"成员表没动过"独立吻合(两套数(子 Agent 的与我复核的)都是这 7 个);`#3 = 0` 的 3 个房
|
|
630
|
+
(2005/2006/2007)里,**2007 明明有一条事件(群主解散群聊)却仍 `#3 = 0`** ⇒ 计数器在未初始化的房里**不随事件走**。
|
|
631
|
+
- ⚠️ **一处口径差(免得后人以为两边矛盾)**:子 Agent 报的每房"事件条数"是**按成员变更归类后**的子集
|
|
632
|
+
(它的全局拆分:`join_qr` 645 / `join_inv` 353 / `join_you` 15 / `removed` 1 / `left` 0 / `dismiss` 3 / 其它 605),
|
|
633
|
+
我报的是**全部**类型 10000 的条数(全库 **1,764** = 它对得上房间的 1,622 + 对不上房间的 142)。**两套数都对**、
|
|
634
|
+
方向一致;用**更大**的那个分母,否证只会更强。
|
|
635
|
+
- ⇒ **结论**:`#3` 是**频率远高于成员变更通知**的每房计数器。"一步"对应的具体事件,
|
|
636
|
+
在"消息日志"这条路上**量不出来** —— 剩下的唯一办法是**看到计数器本身变化的两个时点**(机外对照源),本机无解。
|
|
637
|
+
|
|
638
|
+
**⑩ 它有没有"第二份"?2026-10-07 全库搜过 —— 没有(我做的)。**
|
|
639
|
+
判据是**同对象**:只有当某行**自己标识了这个房间**(`username` / `room_id` / `chat_name` 这类键列命中该房)时,
|
|
640
|
+
才看该行其它列是否等于该房的 `#3` 或 `offset`。
|
|
641
|
+
|
|
642
|
+
- **范围**:**24 个库**(`db_storage` 全树:contact / 4 个 message 分片 / `message_fts` / `message_resource` /
|
|
643
|
+
favorite / session / sns / emoticon / hardlink / general / media / head_image / bizchat …),**扫过 207,156 行**。
|
|
644
|
+
- **结果:0 条有意义的命中。** 其余 46 条全是**小整数撞车**(`#3 = 0` 的房撞上各种 0 列;`off = 1/2` 撞
|
|
645
|
+
`chat_room_notify` / `is_session` / `local_type`;`off = 10` 撞 `last_msg_locald_id`)。
|
|
646
|
+
- ⚠️ **一个值得记的巧合**:`session.SessionTable.last_msg_type == 10000` 撞上了**底数 10000** ——
|
|
647
|
+
而 `10000` **恰好也是成员变更系统消息的类型码**(见 ⑨)。两个无关的 10000,**别当成对应关系**。
|
|
648
|
+
(同一支里还有更粗的假阳性:第一版我拿"候选整数 `IN (...)`"扫全库,得 **279 条命中**,逐条看**全是撞车** ——
|
|
649
|
+
这里"同对象"这个筛子才是关键。)
|
|
650
|
+
- **正对照(证明"0 命中"不是脚本没跑起来)**:同一次扫描里,`#3 = 0` 的房能命中 0 列、
|
|
651
|
+
`session.last_msg_type = 10000` 能命中整数列 ⇒ 匹配机制确实在工作。
|
|
652
|
+
- **范围限制(写清免得后人以为搜全了)**:跳过 `Msg_*` 表(**735 张**;它们此前已按"文本 / varint 两种编码"
|
|
653
|
+
搜过、0 命中),以及**用哈希而不是用户名作键**的表(如 `message_fts.*.session_id`)—— 那类表里若有第二份,
|
|
654
|
+
这个判据**看不见**。
|
|
655
|
+
- ⇒ 结合 ⑦ 与 ⑨,这条路有**三条独立否证**:**不是服务端 seq**(⑦)、**不是成员变更事件**(⑨)、
|
|
656
|
+
**本地没有第二份**(⑩)。剩下的唯一可能是**机外对照源**(另一台机器 / 另一个版本 / 一个更早的库)。
|
|
657
|
+
|
|
658
|
+
### 6.2 想要"第二时点",试过这几条,都不通
|
|
659
|
+
|
|
660
|
+
要坐实"`#3` 跟着成员变动走",最缺的是**同一房间在两个时刻的快照**。本机能想到的免费来源都试了:
|
|
661
|
+
|
|
662
|
+
- **`contact.db-wal`(1,071,232 字节 = 260 帧,当日)里 `chat_room` 一行都没改**:
|
|
663
|
+
把 WAL 截到第 0 帧和第 260 帧各读一次,**77 个群的 `#3` 和成员列表完全相同**。
|
|
664
|
+
⇒ 这个窗口拿不到增量(也说明 `chat_room` 是"改一次就静默很久"的行)。复跑方式见第 9 节。
|
|
665
|
+
- **`contact.db-{first,last,incremental}.material`(4~8KB,头 16 字节与本库同一个盐)不是页快照**:
|
|
666
|
+
把它们贴进 contact.db 副本的**任一页号**(1366 个位置全试过),`chat_room` 的行数与 blob 长度
|
|
667
|
+
都不变、也不报错 ⇒ 里面装的不是这张表的数据。靠它拿"一年前"的旧快照这条路不通。
|
|
668
|
+
- **其余库没有这种字段**:`contact_fts.db` 的 `db_info` 只有 FTS 自己的 schema 版本
|
|
669
|
+
(`chatroom_member_fts_table_version = 3` 之类);`general.db` 的 `sqlite_sequence` 里只有
|
|
670
|
+
`revokebatchmessage`(顺带说明 `chat_room` 不是 AUTOINCREMENT 表)。
|
|
671
|
+
|
|
672
|
+
所以"第二时点"只能来自机外(另一台机器 / 另一个微信版本),或者等客户端自己再改一次并**当场逮住**。
|
|
673
|
+
|
|
674
|
+
**2026-10-04 把"本机有没有第二个实例"也扫了一遍(负结果,附一条真推断):**
|
|
675
|
+
|
|
676
|
+
- **本机只有一个真库**,而且它是**两条路径的同一份**:数据盘上那个目录是**用户目录下 `xwechat_files` 的软链**
|
|
677
|
+
(不是第二份数据)。`Backup\`、`msg\migrate\`、
|
|
678
|
+
`business\migrate\`、`temp\`、`config\` 里没有副本或导出;本机只有一个用户目录、只有 C:/D: 两个盘。
|
|
679
|
+
(顺带:仓库的 `find_nt_databases()` 是"取第一个存在的根就 break"、从不并集多根 ⇒ 软链**不会**造成重复计数。)
|
|
680
|
+
- **【实测】2026-10-11 把上一段引用的那个 `config\hardlink.dat` 本身也读了(32 字节)**:
|
|
681
|
+
它**不是**任何二进制记录格式 —— 内容是 **32 个十六进制字符**(`d287f5ffd3c5c48f283101a627d793eb`,
|
|
682
|
+
即 16 字节;32 字节里无 NUL、只有 16 个互不相同的字节、全部落在 `[0-9a-f]`)。全机**只有这一份**
|
|
683
|
+
(`%APPDATA%\Tencent\xwechat\config\` 下;数据根 `D:\微信\xwechat_files` 里连 `config\` 子目录都没有
|
|
684
|
+
—— 同一个 `-Recurse -Filter hardlink.dat` 搜法在 APPDATA 下能搜到,所以"没搜到"不是搜法失灵)。
|
|
685
|
+
mtime 是 **2026-01-22**,**早于**本机两个已安装的版本目录(`4.1.15.11` 2026-09-21、`4.1.15.13` 2026-10-10)
|
|
686
|
+
⇒ 不随版本/更新重写。
|
|
687
|
+
⇒ **16 字节装不下"395 组"那份清单**,所以报告那句"客户端拿它去重"只能读成"它是一个标记或种子",
|
|
688
|
+
而**不是链接清单本身** —— 上面 395 组是客户端运行时算出来的**结果**,不在这 32 字节里。
|
|
689
|
+
⚠️ **仍然未知:谁在消费它。** 一个可判、但**尚未做**的猜法:把这 16 字节当 AES-128 密钥去开
|
|
690
|
+
`radium\mmkv\` 的那几个存储,判据用 `DECISIONS` D-101 那条(crc32 自洽 **且** 整段结构化走完);
|
|
691
|
+
本机有 store、**没有**那套工具(`_re_weixin` 里只有报告),要重建才谈得上试。
|
|
692
|
+
- **【实测】2026-10-11 补一条同族、但软链那条覆盖不到的**:数据根里**真的有 NTFS 硬链接**(另一条线
|
|
693
|
+
的启动器逆向从 `config\hardlink.dat` 推断客户端用它去重,这里直接量了)。按真身走一遍
|
|
694
|
+
(`realpath` + `(st_dev, st_ino)` 去重,所以**不是**软链造成的):**26,767 个文件里,395 组文件实体
|
|
695
|
+
被 2 条以上路径共享**(最多一组 6 个链接),**436 个路径项、407 MB 是重复计入的**
|
|
696
|
+
—— 9,457.2 MB → 去重后 9,050.1 MB(**4.3%**)。395 组**全部组内大小相同**,所以是"同一份实体挂多条
|
|
697
|
+
路径",不是巧合。客户端自己的目录(`%APPDATA%\Tencent\xwechat`)里 **0 组**。
|
|
698
|
+
⇒ **任何按文件数或字节数"求和"的地方都会多算这几个百分点**,而症状是数字偏大、不报错;
|
|
699
|
+
按单个文件取大小的地方(`check` 里那些 `(N MB)`)不受影响。⚠️ 与软链那条**不是一个**:这条在
|
|
700
|
+
同一个真身内部,取第一个根、realpath 去重都躲不掉,只有按 `(dev, ino)` 去重才看得见。
|
|
701
|
+
- 企业微信(WXWork)**有本人 profile 的库**(3 个 corp + 1 个同日备份),但头不是 SQLite、
|
|
702
|
+
是**自定义加密**,且结构里**没有 `chat_room`/`ext_buffer`** ⇒ 帮不上(按红线未解库、未读内容)。
|
|
703
|
+
- 唯一的"第二份"是本次留的**活库工作副本**(相差 35 分钟):**77 个群的 `#3`/`#4` 与成员条数全等**,
|
|
704
|
+
而同一窗口里 `contact`(好友侧)动了 **13 行** ⇒ 一条真推断:**offset 不随普通好友侧更新走**
|
|
705
|
+
(排除"它是通用活动计数器")。⚠️ 但 35 分钟且窗口内无成员变更,所以"数值相同"**不构成**
|
|
706
|
+
"服务端同步"的证据 —— 只是与"只跟成员走"相容。
|
|
707
|
+
|
|
708
|
+
## 7. `stranger.extra_buffer` 的形状目录(= `contact.extra_buffer` 主体那张 proto)
|
|
709
|
+
|
|
710
|
+
本机 `stranger` 表只有 **1 行**;但 2026-10-05 证实它的骨架与 **`contact.extra_buffer` 主体那 940 行同型**
|
|
711
|
+
(逐字段号 + 逐线格式完全一致)⇒ **取值范围可以拿那 940 行来给**,不必只靠这 1 行。字段号与线格式:
|
|
712
|
+
|
|
713
|
+
- varint(wt0):`#2 #3 #8 #10 #11 #12 #13 #16 #17 #18 #19 #22 #23 #24 #37 #38`(这行里 `#13` 与 `#18` 取值为 1,其余 0)
|
|
714
|
+
- ~~可解析的嵌套消息(wt2):`#4 #5 #6 #7 #14 #15`~~ ⚠️ **2026-10-07 更正**:这一行是**照 `stranger` 那 1 行**记的,
|
|
715
|
+
套到 `contact` 主体的 940 行上**不成立** —— `#4`/`#5` 是**文本**("走通"只是随机字节撞成合法 tag),
|
|
716
|
+
`#6`/`#7` 的走通率只有 **555/940**、**473/940**。**形状全量一致的 message 只有 `#14`(830/830)与 `#27`(937/937)**,
|
|
717
|
+
见下面的"ld 字段的内层"。
|
|
718
|
+
- 非嵌套载荷(wt2):`#9`(50 字节),以及 `#20 #21 #25 #26 #…` 等
|
|
719
|
+
|
|
720
|
+
**拿那 940 行补出来的(2026-10-05)**:
|
|
721
|
+
|
|
722
|
+
- **可选字段**:940 行里 `#13` 出现在 687、`#36` 824、`#14` 830,其余 `#2`…`#38` 基本行行都有 ⇒ 这几个是 optional。
|
|
723
|
+
- **`#41`(varint,691 行)的取值形态是 epoch 秒**(样本落在 2021–2024),而 `contact` 的 22 列里**没有任何时间列**
|
|
724
|
+
⇒ 只能存在于 blob。**`#43`(ld,22 字节,628 行、去重仅 11 种)** 是跨行高度重复的 id 样值,列里同样没有对应。
|
|
725
|
+
- **同行"值对齐"判负(2026-10-05 独立复核)**:对**全部 986 行**、每个字段、6 种编码(raw / utf8 / md5 / sha1 / b64 / utf-16le)
|
|
726
|
+
与 22 列逐一比对,**总共只有 2 处命中**且都很稀疏:`nick_name` 整值 == 字段 `#4`(8 / 716 行)、== 字段 `#9`(12 / 953 行);
|
|
727
|
+
**没有任何哈希 / URL / wxid 级命中**(`head_img_md5` 80 行非空、`encrypt_username` 763 行非空,均 0 命中)。
|
|
728
|
+
⇒ 判为**偶发同值/撞车**(`#4` 中位 47 字符、与昵称长度也对不上),**不据以命名** —— 与第 4 节的既有结论同向,且更硬。
|
|
729
|
+
|
|
730
|
+
**逐字段取值轮廓(2026-10-06,多 Agent 提出、只写我复核过的)**:
|
|
731
|
+
|
|
732
|
+
- **`#13` 出现 ⟺ `contact.verify_flag ≠ 0`**:940 行里 **687 有 / 253 无,两个错格都是 0** —— 本表唯一一条精确双条件(见 §3)。
|
|
733
|
+
- **`#33` 非空 ⟺ `#36` 出现 ⟺ `#27` 的子字段 3 出现**:三者都是 **824** 行,各方向 0 例外。
|
|
734
|
+
- **`#5` = 国家/地区代码(ISO 3166-1 alpha-2)—— 37 个字段里第一个定下名字的(2026-10-07)。**
|
|
735
|
+
形态:**717 行是恰好 2 个大写 ASCII 字母**(**25 个**去重值),另 223 行是显式空串。三条证据:
|
|
736
|
+
① **25/25 全部落在 ISO 3166-1 的 249 个 alpha-2 码里** —— 若是随机 2 大写字母,概率 **(249/676)^25 ≈ 1.4e-11**;
|
|
737
|
+
② **众数就是 `CN`,占 94.0%**(与"本机账号绝大多数在中国"相符)—— 这条**不依赖码表**,是对 ① 的独立确认;
|
|
738
|
+
③ 按 **ISO 639-1 语言码**比对只有 **15/25**(且语言码是小写)⇒ **是地区码,不是语言码**。
|
|
739
|
+
⚠️ 裁判口径:① 的码表是**硬编码**的(249 条,与官方数量一致);正因为裁判可能自身有错,才必须配 ② 这种**不依赖码表**的预测。
|
|
740
|
+
- **`#41` = 该行资料的"最近更新时间"(epoch 秒)—— 第三个被定性的字段(2026-10-07)**,三条:
|
|
741
|
+
① 691 行出现、其中 **127 行显式写 0**,564 行非零;非零样本 **2019–2026、70% 落在 2026**;
|
|
742
|
+
② **与 `contact.id` 的 Spearman = 0.945**(n=564),且 **564 个非零值一个不重复**;
|
|
743
|
+
③ **它不是创建时间** —— 有 **40 行**是"`id < 1500` 却带 2025/2026 的时间戳"(创建时间不可能出现这种行)
|
|
744
|
+
⇒ 只能读成"**被更新过**"。多数行没被改过,所以看起来≈创建时间(这正是 ② 高但不到 1 的原因)。
|
|
745
|
+
⚠️ 口径:本机 22 列里**没有任何时间列**能跟它对应,所以这是**本机唯一的"行级时间"**(本地推断,非厂商命名)。
|
|
746
|
+
- **`#43`:不是"22 字节的 id 样值",是一个嵌套 message(2026-10-07 解开内层)。** 结构:
|
|
747
|
+
`#1 ∈ {0,1}`(0:121 / 1:507)、`#2` 是**又一个 message**(18 字节;121 行只有 `#1`、507 行有 `#1`+`#2`);
|
|
748
|
+
`#2` 的内层再解一层:`{1: 1, 2: ∈ {1,3,4}, 3: 3, 4: repeated {1: 10/11/12}}` —— **只有 10 种变体**,
|
|
749
|
+
最大的一种占 **448 行**。⇒ 它**像"一组能力/范围编号"**(重复的 `{1:10}{1:11}{1:12}` 是个小列表),
|
|
750
|
+
**不是不透明 id**;名字 UNKNOWN。跨表整值 join 已判负(本机 0 命中)。
|
|
751
|
+
- **`#14` 像一道闸门**:缺 `#14` 的 110 行里,`#33` / `#36` / `#43` 出现率 **0**、`#4` / `#9` 非空只有 6%。
|
|
752
|
+
- **这个 proto 显式序列化默认值**:`#15 #20 #21 #28..#32 #34 #35` 是 `ld` 长度 0、`#16 #23` 是 `varint 0` ⇒ 读的时候**别把"出现且为空"当成"有值"**。
|
|
753
|
+
|
|
754
|
+
**varint 字段的取值域(2026-10-07 全量补上,此前多处只写"值域未测")** —— 940 行;只列小数集字段:
|
|
755
|
+
|
|
756
|
+
| 字段 | 取值分布 |
|
|
757
|
+
| --- | --- |
|
|
758
|
+
| `#2` | 0:796 / 1:98 / 2:46 |
|
|
759
|
+
| `#3` | 0:515 / 3:422 / 9:2 / 515:1(见 §6:`bit0 ⟺ 值≠0`、`bit1 ⊂ bit0`) |
|
|
760
|
+
| `#8` | 23 种、散布(0:342,其余从 3 到 275)——**形态像位掩码**(多次出现 `255/247/251` 这种"满位减几"),无对照列 ⇒ UNKNOWN |
|
|
761
|
+
| `#10` / `#38` | 都是 **0 或 `4294967295`(= -1 的无符号写法)** 两态:`#10` 0:129 / -1:811;`#38` 0:242 / -1:698 ⇒ 像"未设置 / 有值"哨兵 |
|
|
762
|
+
| `#11` | 0:129 / 1:84 / 2:350 / 3:377(四态,分布较均);**`#11 = 0` ⟺ `#10 = 0`(940/940,两个错格 0)** |
|
|
763
|
+
| `#12` | 0:893 / 1:47 |
|
|
764
|
+
| **`#13`** | 0:530 / 1:106 / 3:44 / 5:4 / 2:3 —— **= 该行 `biz_info.type`,687/687 逐行相等、零反例(2026-10-07 命名)** |
|
|
765
|
+
| `#16` / `#23` | 恒 0(显式序列化) |
|
|
766
|
+
| `#17` | 0:813 / 1:127 |
|
|
767
|
+
| `#18` | 0:85 / 1:855 |
|
|
768
|
+
| `#19` | **0~10 的 11 个值**(0:667,1~10 各 20~37)——⚠️ 分布是**平坦**的(不是计数的衰减形,更像**枚举/序号**)。已否掉**四族**解释:①"菜单按钮数"(130/940)②"blob 内部子字段的重复条数"(最好 `#1` 只 104/940)③"同行 biz 元数据里的各种计数"(键数/叶子数/子菜单数/小程序条目数/URL 数…逐行比,最好 24/199)④"分类字段或本地文章数"(`type`/`child_type`/`VerifyBizType` 都非确定;== 该号 `Msg_` 表行数只 6 行)⇒ UNKNOWN |
|
|
769
|
+
| `#22` | 0:935 / 4:2 / 7:3 |
|
|
770
|
+
| `#24` | 0:939 / 3:1 |
|
|
771
|
+
| `#37` | 0:466 / 256:473 / 2048:1(只有 bit8 / bit11 两个位被用到) |
|
|
772
|
+
| `#41` | 691 行出现、**565 种**取值、0 ~ 1791025935(epoch 秒,众数落在 2026)——见下面那条 |
|
|
773
|
+
|
|
774
|
+
- **已验**(2026-10-07):`#10 = 0` 与 `#11 = 0` **是同一批 129 行** —— 940 行里 `(F,F) 811 / (T,T) 129`,
|
|
775
|
+
**两个错格都是 0** ⇒ 两者**共用同一个"未设置"状态**,别当两个独立标志读(与 `#10`/`#38` 那种 `0 / -1` 哨兵是同一族写法)。
|
|
776
|
+
- **一批小字段的"负结果"**(2026-10-07):把 `#2 / #8 / #10 / #11 / #12 / #17 / #18 / #19 / #22 / #24 / #37 / #38`
|
|
777
|
+
逐个与**行级属性**做交叉(账号类别 gh_/wxid_/群/openim、`local_type`、`flag`、`verify_flag≠0`、
|
|
778
|
+
`is_in_chat_room`、`chat_room_type`、`chat_room_notify`、有无 `encrypt_username`),**找"某属性在某取值下完全恒定"
|
|
779
|
+
的干净关系 —— 一个都没有**(唯一"干净"的是 `#13`,而那是 `verify_flag`/biz 身份的同义反复)。
|
|
780
|
+
⇒ **这批字段不挂在行的这些属性上**,别再从"行的类型/标志"这条线去找它们的名字。
|
|
781
|
+
|
|
782
|
+
**blob 内部两两扫描(2026-10-07)** —— 940 行的 `ContactExtData` 主体,问"A 的取值是否决定 B"。**筛掉平凡情形后的真结果**:
|
|
783
|
+
|
|
784
|
+
- **biz 行(`#13` 存在,687 行)上,`#2` / `#12` / `#17` / `#22` / `#24` 恒为 0、`#25` 恒缺** ——
|
|
785
|
+
⇒ 这批"小旗"**只长在非 biz 行上**。`#10 = 0 ⟺ #11 = 0` 也被独立再确认一次。
|
|
786
|
+
- **非 biz 的 253 行是什么**:**78 行是 `@chatroom`(群)且 `#2 = 0`**;其余 **175 行是个人**等,`#2` 取 0/1/2。
|
|
787
|
+
其中 **`#2 ∈ {1,2}` 的 144 行里 123 行带 `#27.#2` 那个 URL(85%)**,而 `#2 = 0` 的 109 行只 18 行带(**17%**)
|
|
788
|
+
⇒ **`#2` 与"有没有那个 URL"强相关,但不等价**(`#2` 的含义仍 UNKNOWN)。
|
|
789
|
+
- ⚠️ **"决定"/"相等"这种关系的平凡形态,至今踩到五种**(记下来省后人):
|
|
790
|
+
① **A 是常量**(任何 A 都能"决定"B);② **B 是常量**(谁都能"决定"它);
|
|
791
|
+
③ **A 的取值几乎唯一**(如 `#41` 有 565 个不同取值 ⇒ 每个取值只对应一两行,"决定"必然成立);
|
|
792
|
+
④ **双方同时为 0**(`#2 = 0` 与"小程序条目数 = 0"在大多数行同时成立 ⇒ 刷出 676/688 的假命中);
|
|
793
|
+
⑤ **同一张筛子只写在调用方**——第四种形态的教训当时写进了文档,可**新写的脚本照样再犯一次**
|
|
794
|
+
(`RegisterSource` 那一列整列都不存在 ⇒ B 恒为常量 ⇒ "完全确定")。**⇒ 排除常量的判断要写在判据函数内部,别指望每次调用时记得。**
|
|
795
|
+
本机未筛前 398 对"完全确定",**筛掉这几类后只剩 16 对**、其中真是信息的更少。
|
|
796
|
+
**判据要加两条**:A、B **都**得有 ≥2 种取值,且 **A 的每个取值都要有 ≥5 行**(非零比较时只在**两边都非零**的行上比)。
|
|
797
|
+
|
|
798
|
+
**两轮"小字段到底等于什么"的宽扫(2026-10-07,都判负,别重跑)**:
|
|
799
|
+
|
|
800
|
+
- **对 biz 行**:把 `#2/#8/#11/#12/#17/#18/#19/#22/#24/#37` 与同行 `external_info` 能提供的十来种计数逐个比
|
|
801
|
+
(键数 / 非空键数 / 叶子数 / 菜单按钮数 / 子按钮数 / 小程序条目数 / 品牌 URL 数 / 各子对象的键数…)——
|
|
802
|
+
**没有一个能对上**(最好的 `#11`↔`RegisterSource` 键数 256/612、`#18`↔品牌 URL 数 443/681、`#19` 24/199)。
|
|
803
|
+
另 `#2/#12/#17/#22/#24` 在 biz 行上**非零行为 0**(见上:这批旗标只长在非 biz 行)⇒ **这条路对它们天然无效**。
|
|
804
|
+
- **对 253 个非 biz 行**:把同一批字段与"非 biz 才有的特征"交叉(账号类别 / `local_type` / 有无 `encrypt_username` /
|
|
805
|
+
**是不是会话**(消息库 `Name2Id.is_session`,正对照 125/139 命中)/ 在不在 `chat_room` / 有没有那个 URL)——
|
|
806
|
+
**没有任何"某取值下完全恒定"的关系**。
|
|
807
|
+
- **第三轮(同一族):"是不是可选字段的出现情况"** —— 把 `#2/#8/#11/#12/#17/#18/#19/#22/#24/#37` 与该行
|
|
808
|
+
"**8 个可选字段(`#13/#14/#26/#27/#33/#36/#41/#43`)里出现了几个**"比(`#8` 另比"置位数"):
|
|
809
|
+
**值 == 个数的行数 0~25/941(偶然水平)**,`#8` 的置位数只对上 **8** 行 ⇒ **"出现情况位图/计数"这一族也出局**。
|
|
810
|
+
- **第四轮:字段值 × "该号有什么"(`external_info` 里每个键的有/无、`type`/`child_type`/`brand_flag`、
|
|
811
|
+
有没有 `Msg_` 表)** —— 没有任何"某取值下完全恒定"的判别式。唯一的**部分**信号:`#8` 与"有 `WxaAppInfo` 键"
|
|
812
|
+
相关(它 23 个取值里 39/57/243 三档 P=1.0、14/15/17 三档 P=0.0,其余 0.2~0.96),但**不是**判别式。
|
|
813
|
+
⚠️ 一个**混杂项**要小心:`ScanQRCodeType` / `IsShowHeadImgInMsg` / `IsHideInputToolbarInMsg` **三个键总是同时出现**,
|
|
814
|
+
于是它们在很多字段上都刷出"最大分离度"(0.7~0.86)——**那是"资料更全的号",不是字段的含义**。
|
|
815
|
+
- **第五轮:`#8` 是不是"哪些键存在"的位图?** —— **双向否掉**:同一个键集合对应多个 `#8`(51 个键集合里),
|
|
816
|
+
同一个 `#8` 对应多个键集合(15 个取值里 11 个)⇒ 既不是键集合的函数,也不是它的反函数。
|
|
817
|
+
|
|
818
|
+
**不是 message 的那几个 ld 字段,到底是什么(2026-10-07;先问"是不是 message",再问"不是的话是什么")**:
|
|
819
|
+
|
|
820
|
+
| 字段 | 形态(941 行为分母) | 结论 |
|
|
821
|
+
| --- | --- | --- |
|
|
822
|
+
| `#6` | 259 空;非空 **5~19 字节、全 UTF-8 可打印、纯 ASCII 字母**(673/682 行次全字母)、**只有 ~36~51 个去重值** | **是分类标签**(英文词形、带大写);**取值不在** `external_info`/`brand_info` 的任何叶子里(上一轮比过),也**不由** `type`/`child_type`/`brand_flag` 决定(只有 18/36 个取值唯一);**也不在客户端的字符串存储里**(见下) |
|
|
823
|
+
| `#7` | 300 空;2~19 字节,同类(619/641 全字母)、~120 个去重值 | **另一套分类标签**(同上,无本机对照) |
|
|
824
|
+
| `#26` | 826 空;非空 **100~118 字节、115 个值互不相同、首字符 `<`** | **是 XML,而且自带名字**:根标签 `<bizuserattr>`、里面 `<userop>`,三个属性 **`lastlogintime` / `lastmsgoptime` / `lastaddroptime`**(115 行全有,取值都是 **epoch 秒、落在 2013–2015**)⇒ 读成"**我对该号的用户侧属性:上次登录 / 上次发消息 / 上次加好友的时间**"(客户端自己起的名字,不是猜的)。样本只有 115 行、且时间很旧 |
|
|
825
|
+
| `#25` | 929 空;12 个值、31 字节、首字符是数字 | 像字符串型 id / 时间戳,样本 12 ⇒ **判不了** |
|
|
826
|
+
| `#33` / `#36` / `#15` / `#20` / `#21` / `#28` | **全空**(`#33` 是"含一个空子字段的壳") | 纯**空壳标记**:"存在"本身才是信息 |
|
|
827
|
+
|
|
828
|
+
- ⇒ `#6`/`#7` 要名字,只能像 `#5` 那样**找一份外部标签表**(英文词 + 语义)来对;**本机数据里没有对应列,别再找**
|
|
829
|
+
(已比过 `external_info` 全部叶子、22 列、以及 `type`/`child_type`/`brand_flag`)。
|
|
830
|
+
|
|
831
|
+
**第四条定名路:字段内容里"自带键名"(2026-10-07)** —— 有些字段的内容本身就是 **XML/JSON**,
|
|
832
|
+
键名是**客户端自己起的英文标识符**,等于名字白送。全量扫了一遍大 proto 的所有 ld 字段:
|
|
833
|
+
**只有 `#26` 是这种**(`<bizuserattr>` + `lastlogintime`/`lastmsgoptime`/`lastaddroptime`;没有别的 XML、没有顶层 JSON)。
|
|
834
|
+
⇒ 于是 blob 字段的定名路凑齐四条:**(a) 跨表整值 join**(那 44 行的 `#1`/`#2`)、
|
|
835
|
+
**(b) 自/他对照的内容刻画**(`#4`/`#9`)、**(c) 公开码表 + 不依赖码表的预测**(`#5`)、
|
|
836
|
+
**(d) 内容自带键名**(`#26`)。剩下的 `#6`/`#7` 走的是"像标签、但要外部词表"那一条 —— 本机没有。
|
|
837
|
+
|
|
838
|
+
**`#6`/`#7` 的取值找不到本地出处(2026-10-07:两处搜过,其中一处踩了坑)**:
|
|
839
|
+
|
|
840
|
+
- **不在混淆字符串池里**:池子 **41,913 条记录(≈35,951 个不同串)**,`#6` 的 51 个、`#7` 的 120 个取值 **全部 0 命中**
|
|
841
|
+
(整值、大小写无关、以及"被更长池串包含"三种口径都是 0)。
|
|
842
|
+
⚠️ 口径:我那次搜索是**按落盘文件的行**(41,922 行)扫的,比"记录"**多**几条 —— 当时有 8 条记录的明文**自带换行**、
|
|
843
|
+
一条跨两行(早先的 Agent 在报告里记过这一点)。**搜的是超集 ⇒ 这个否定只会更硬**,不是更软。
|
|
844
|
+
- **也不是 DLL 里独立存在的字面量**:在 `Weixin.dll`(192 MB)里逐字节找,表面上 `#6` **51/51**、`#7` **119/120** "命中" ——
|
|
845
|
+
**但加一步"是否独立成串"(前后不是字母/数字/下划线)之后**:`#6` 只有 **9/51** 独立、**42/51 嵌在更长的标识符里**;
|
|
846
|
+
`#7` 只有 **2/120** 独立、**117/120 嵌在别的词里**。短英文词在 192 MB 二进制里**必然**作为子串出现 ⇒ **那些"命中"是撞车**
|
|
847
|
+
(正对照:`micromsg.ContactExtData` / `Weixin.exe` / `wcdb` 各命中 1 处,机制没问题)。
|
|
848
|
+
- ⇒ **结论**:`#6`/`#7` 的取值**既不在混淆池里、也不作为独立字面量存在于二进制里** ⇒ 更像**服务端下发的标签**
|
|
849
|
+
(客户端只显示/透传),不是客户端自己的枚举名。**本机没有这份词表,别再找。**
|
|
850
|
+
|
|
851
|
+
**`contact` 里的"群行",以及 `#2` 与会话的关系(2026-10-07 新查)**:
|
|
852
|
+
|
|
853
|
+
- **`contact` 表里那 78 个 `@chatroom` 行,77 个就是 `chat_room` 的群**(其中 **76 个 `local_type = 2`**),
|
|
854
|
+
另有 **1 个不在 `chat_room` 里**(`local_type = 2`、`#2 = 0` —— 一个"像群但不在群表里"的行,单独留着)。
|
|
855
|
+
⇒ 上一段说的"非 biz 的 253 行里有 78 个群"= **表里同一批群**;读群成员不必绕道 `contact`。
|
|
856
|
+
- **`#2` 与"有没有会话"相关,但不等价**(对照源:消息库 `Name2Id.is_session`;**正对照 587/617 命中**):
|
|
857
|
+
非 biz 的 253 行里 **`#2 = 0` 有 75/109 是会话**(那 78 个群基本都在这里),`#2 = 1` 是 36/98、
|
|
858
|
+
`#2 = 2` 是 14/46 ⇒ **`#2 = 0` 偏向"有会话",`#2 ∈ {1,2}` 偏向"没有会话"**;
|
|
859
|
+
而 `#2 ∈ {1,2}` 同时与 `#27.#2` 那个 URL 强相关(123/144)。`#2` 的含义仍 UNKNOWN,但有了两个相关项。
|
|
860
|
+
- ⚠️ **一次几乎写错的负结论(同类错误第三次)**:第一次跑这条时得到"253 个非 biz 行**全都不是**会话"——
|
|
861
|
+
**是假的**:`contact` 那条连接设了 `text_factory = bytes`(bytes),消息库那条没设(str),
|
|
862
|
+
**bytes ∩ str 恒为空集**,于是"没有交集"看着像一个发现。修法是**归一到同一类型**,
|
|
863
|
+
并且**把正对照写进脚本**(交集为 0 就自己报"这次比较无效")。同族错误 D-076 已记过一次 ⇒
|
|
864
|
+
这次把规则升级到 `AGENTS.md`(见 D-089)。
|
|
865
|
+
**ld 字段的内层(2026-10-07,把所有 ld 字段再走一层)** —— 全部 940 行为分母:
|
|
866
|
+
|
|
867
|
+
| 字段 | 内层形状 | 能不能当 message 用 |
|
|
868
|
+
| --- | --- | --- |
|
|
869
|
+
| `#14` | `{#1: 0/1(829:1), #2: 13 字节}` | ✅ **830/830 形状一致** —— 是个真 message |
|
|
870
|
+
| `#27` | `{#1: 0/1, #2: 长度 84~141 字节(14 种长度), #3: 142 种取值, #4: 11 种, #5: 掩码 {0:699, 72:52, 720:25, 4320:31, -1:17}}`;**`#2` 只在 141 行出现、且 141/141 都是 `http://` 的 URL** | ✅ **937/937 形状一致**(但 `#2` 只在 141 行有值) |
|
|
871
|
+
| `#33` | 一直只有 `#1`(**长度 0**,即"含一个空子字段的壳") | ✅ 全量一致,但内容为空 ⇒ **是个标记** |
|
|
872
|
+
| `#36` / `#15` / `#20` / `#21` / `#28` / `#26` | **空壳**(无任何子字段;`#26` 825/940 空) | ✅ 是**标记**,不是数据 |
|
|
873
|
+
| `#6` / `#7` | "能走通"的行只有 **555/940** / **473/940** | ❌ **不能**当形状固定的 message(见下) |
|
|
874
|
+
| `#4` / `#5` / `#9` | "能走通" 294 / 252 / 393 行(**是文本,被撞成合法 tag**) | ❌ 走通是**假象** |
|
|
875
|
+
|
|
876
|
+
⚠️ **一条判据更正(重要)**:**"走线器能走通"不等于"它是 message"** —— 文本字段(`#4`/`#5`/`#9`)也有
|
|
877
|
+
两三成能"走通",因为随机字节会撞成合法 tag。**判据要用"形状在同一批行里全量重复"**:
|
|
878
|
+
`#14` 的 830/830、`#27` 的 937/937 才是 message;`#6`/`#7` 那种 555/940 的**不算**。
|
|
879
|
+
⇒ 这同时更正了 **§7 那张形状目录**里"`#4 #5 #6 #7 #14 #15` 可解析成嵌套消息"那行(它是照 `stranger` 的 1 行记的,证据不足)。
|
|
880
|
+
(`#27.#5` 的 `72 / 720 / 4320` = 72×1 / ×10 / ×60 —— 形态像**时长**,但没有对照源,**不据此命名**。)
|
|
881
|
+
|
|
882
|
+
**`#27.#2` 那个 URL 单独记一笔(2026-10-07)**:**141 行**有它(`local_type` 1 占 97、0 占 44,**全不在 `biz_info` 里**),
|
|
883
|
+
**全部是 `http://`、长度众数 107~110 字节、只有 3 个 host**;这 3 个 host 在 `contact`/`biz_info`/`chat_room_info_detail`
|
|
884
|
+
的**任何列里都不出现**(头像列 `big_head_url` 3914 行、`brand_icon_url` 691 行,命中 **0**)⇒ 它不是本机任何已知的 CDN。
|
|
885
|
+
与**同行**的列也不同(只有 4 行里含该行昵称/拼音、**0 行含 `encrypt_username`**)。**含义 UNKNOWN,不再往里挖**
|
|
886
|
+
(再解就要碰 URL 路径内容了,收益与隐私都不划算)。
|
|
887
|
+
⚠️ 更正我自己上一步的一句:先前写的"`#2` 只有 14 种"是**长度的种数**,不是取值数 —— 取值数按"141 行 / 3 个 host"另算。
|
|
888
|
+
|
|
889
|
+
- **`#13` 这条顺带解释了 §3 里那条硬事实**:`#13` 出现 ⟺ `verify_flag ≠ 0` ⟺ 在 `biz_info` 里 ——
|
|
890
|
+
因为 blob 里存的就是**该账号服务类型的一份副本**,非 biz 行根本没有这个值可写。
|
|
891
|
+
- **独立 join 判负**:`#43` 的去重值/内层 pattern 在 contact.db 全 17 表(**68** 个文本/blob 列、约 6.6 万行)里**只有源列自指**;在公众号消息库(539 表 / 24 万行)与 4 个消息分片(30 万+ 行)里 **0 命中**(长度 2 的 pattern 已剔除,避免字节撞车)。
|
|
892
|
+
- **`#4`/`#9` 的内容定性:它们是"该账号自身资料文本的汇集"(2026-10-07,自/他对照后成立)。**
|
|
893
|
+
做法:把 `biz_info` 的 `external_info` + `brand_info` **递归展成叶子**(共 16,294 条去重叶子),
|
|
894
|
+
再看某个 blob 字段的值里**含不含同一行自己的叶子**。判据是**自/他对照**:只算**≥8 字节**的叶子,
|
|
895
|
+
**本行叶子 self = 336 vs 换成下一行的叶子 other = 7(48×)** ⇒ 这些包含**不是撞车**。
|
|
896
|
+
分工很清楚:**`#9` 主要是主体名/认证描述**(`RegisterSource.RegisterBody` **147**、`VerifySource.Description` **67**),
|
|
897
|
+
**`#4` 主要是菜单按钮名**(`MMBizMenu.button_list[].name` **31** + `sub_button_list[].name` **21**),
|
|
898
|
+
两者都还含商标名 / 认证人名。⇒ **两列都是"该账号自身资料文本的汇集"**(像检索用文本/资料摘要)。
|
|
899
|
+
⚠️ 边界:**包含 ≠ 相等**(除了 `#9` 那 214 行整值命中外,其余只是"含"),具体的组织形式
|
|
900
|
+
(拼接?JSON?键值堆?)**未测**;仍然**不给名字**。
|
|
901
|
+
⚠️ 同一次里 **`#5`(2 字节)那 22 条"含菜单值"在做对照后消失** —— 2 字节的串几乎什么都"包含",
|
|
902
|
+
**短值必须配对照**才敢下结论(`#9` 的整值相等 **214 行**:148 归 `RegisterBody`、67 归 `VerifySource.Description`;
|
|
903
|
+
`#4` 整值相等只有 **1** ⇒ 不对称,`#9` 那条不是撞车)。
|
|
904
|
+
- **"跨表比对"这条路本身值得单记**:`#9` 这条、以及那 44 行的 `#1`/`#2`,都是靠**在别的表里逐字节命中**定的 ——
|
|
905
|
+
不是读二进制、不是猜。⇒ 以后遇到有值可查的 blob 字段,**先做跨表 join**。
|
|
906
|
+
- **一条被否掉的假线索**(值得留):在消息表里做"子串包含"时,`#9` 出现过若干命中,**全是 16~23 字节的撞车**
|
|
907
|
+
(命中数按层看:≥16B 有 49、≥24B 掉到 0)⇒ **子串匹配必须设长度门槛**,否则会得出"字段出现在聊天内容里"的假结论。
|
|
908
|
+
|
|
909
|
+
**三个候选名(全部纯推测、未验、不得写进代码)** —— 既然二进制这条路封口了,下面是**按形状猜的**,每个都带证伪办法:
|
|
910
|
+
|
|
911
|
+
| 字段 | 候选名 | 依据(是线索,不是证据) | 证伪办法 | 置信度 |
|
|
912
|
+
| --- | --- | --- | --- | --- |
|
|
913
|
+
| `#41` | ~~`UpdateTime`(推测)~~ → **已是结论:该行资料的最近更新时间(epoch 秒)** | 见上面那条:epoch 秒 + 与 `contact.id` 的 ρ=0.945 + **40 行"小 id 却带新时间戳"否掉了"创建时间"** | —— | **中高** |
|
|
914
|
+
| `#43` | ~~跨行共享的 id~~ → **只有 10 种变体的小结构**("能力/范围编号"形态) | 内层已解开:`{#1∈{0,1}, #2 = {1:1, 2:∈{1,3,4}, 3:3, 4:[{1:10},{1:11},{1:12}]}}`,最大一种占 448/628 | 跨表整值 join 本机**已判负**(0 命中);再推要找"哪类对象才带这个组合"的对照 | **形态:高;含义:UNKNOWN** |
|
|
915
|
+
| `#5` | ~~国家/地区码(推测)~~ → **已是结论:国家/地区代码(ISO 3166-1 alpha-2)** | 见上面那条:25/25 落在码表(随机 1.4e-11)+ 众数 `CN` 占 94%(不依赖码表的独立确认)+ 语言码口径只 15/25 | —— | **高**(三条独立证据) |
|
|
916
|
+
|
|
917
|
+
其余 `#2`…`#38` **继续只报字段号**(与 D-068 的边界一致)。
|
|
918
|
+
|
|
919
|
+
名字在没有对照源的情况下拿不到 —— 这是**结构性**的,不是没解:本机 24 个库里,
|
|
920
|
+
这些字段的值都不出现在别处(见第 4 节的同款结论)。
|
|
921
|
+
|
|
922
|
+
## 8. 公开圈研究到哪一步了(2026-10-03 查)
|
|
923
|
+
|
|
924
|
+
这类东西最容易白费力气,所以把"别人做到哪"记下来:
|
|
925
|
+
|
|
926
|
+
| 项目 | 星 | 与本表的关系 |
|
|
927
|
+
| --- | --- | --- |
|
|
928
|
+
| [Wing900/chatlog-export](https://github.com/Wing900/chatlog-export) | 31 | **唯一的公开 schema**:`internal/model/wxproto/{roomdata,bytesextra,packedinfo}.proto`。成员段与实测一致,`roomCap` 那行对不上(见第 2 节) |
|
|
929
|
+
| [sjzar/chatlog](https://github.com/sjzar/chatlog) | 9.2k | 支持 V4,但**不解析 ext_buffer**(文件树里没有任何 proto) |
|
|
930
|
+
| [LC044/WeChatMsg](https://github.com/LC044/WeChatMsg) | 42k | V3 时代:没有 proto/V4/schema 文件,README 不提 4.x |
|
|
931
|
+
| xaoyaoo/PyWxDump(9.7k)、0xlane/wechat-dump-rs | — | **已被 DMCA 下架**(`Repository access blocked`),取不到了 |
|
|
932
|
+
| [Ray0612/WeChat-v4-export-research](https://github.com/Ray0612/WeChat-v4-export-research) | 13 | 记的是**消息**级字段:记录区(键值对)里 `chatroom_id` = 键 `0x76`、`chatroom_name` = `0x77`(中文 UTF-8)。**已测:与本文的 `#3` 无关**(见下) |
|
|
933
|
+
|
|
934
|
+
**陷阱一处**:中文资料里那条"`ChatRoom` 表有 `RoomData`(BLOB) 列"说的是**微信机器人框架自己的库**,
|
|
935
|
+
不是微信的 `contact.db`(我们的表是 `chat_room(id, username, owner, ext_buffer)`)。别照抄那张表。
|
|
936
|
+
|
|
937
|
+
**据此新做的一条证伪**(比第 4 节更进一步,因为它扫的是**消息内容**而不只是表列):把 74 个群各自的
|
|
938
|
+
`#3` 拿到该群消息表 `Msg_<md5(username)>` 的每一行、每一个非数值列里,按**两种编码**找
|
|
939
|
+
(十进制文本 / varint)。结果:
|
|
940
|
+
|
|
941
|
+
- **大值(≥1e8)命中 0 / 0**(文本 0、varint 0)—— 大整数不会在忙 blob 里撞车,所以这是硬的;
|
|
942
|
+
- 小值(如 `2021`/`10001`/`10087`)有若干命中,但那是**字节撞车**(两三字节的序列在几 KB 的 blob 里
|
|
943
|
+
到处都是),不构成证据。
|
|
944
|
+
|
|
945
|
+
⇒ 同行记下的 `chatroom_id`(消息记录区 `0x76`)**不是**本文这个 `#3`。
|
|
946
|
+
|
|
947
|
+
**2026-10-04 补一次定向搜索**:我们后来才从客户端读出这层本地 message 的**类型名**,于是拿这些**很有辨识度**的
|
|
948
|
+
字符串去搜公开代码(`gh search code`):
|
|
949
|
+
|
|
950
|
+
| 搜什么 | 结果 |
|
|
951
|
+
| --- | --- |
|
|
952
|
+
| `ChatroomExtData`、`ChatroomMemberLocalData` | **只命中我们自己的仓库**(就是这份文档),别处 0 |
|
|
953
|
+
| `win_local_define`、`ClientChatroomMemberData` | **0 命中** |
|
|
954
|
+
| `ClientChatroomData` | 命中的全是 Pokémon Showdex(无关) |
|
|
955
|
+
|
|
956
|
+
⇒ **公开圈里没有这层本地 proto 的字段定义。** 结合 3.3 的结论(这一版客户端里也没有可读的字段名存放处),
|
|
957
|
+
`#3`/`#4` 的名字**在本机 + 公开渠道两头都拿不到** —— 要它只剩"换一个参照实例"
|
|
958
|
+
(另一台机器的库 / 另一个微信版本,理想是 V3 时代的,因为 V3 的 `RoomData` 才是它的直系祖先)。
|
|
959
|
+
|
|
960
|
+
## 9. 复现方法
|
|
961
|
+
|
|
962
|
+
1. 只读打开:`PRAGMA key = "x'<key_hex><库文件头 16 字节的 hex>'"`(同 `scripts/nt_decrypt.py` 里
|
|
963
|
+
`connect_message_shards` / `get_fav_schema` 的写法);
|
|
964
|
+
2. 取 `SELECT ext_buffer FROM chat_room WHERE length(ext_buffer) > 0`;
|
|
965
|
+
3. 手写走线器(不装 protobuf 库也能解,约 30 行):
|
|
966
|
+
|
|
967
|
+
```python
|
|
968
|
+
def varint(buf, i):
|
|
969
|
+
val, shift = 0, 0
|
|
970
|
+
while True:
|
|
971
|
+
b = buf[i]; i += 1
|
|
972
|
+
val |= (b & 0x7F) << shift
|
|
973
|
+
if not (b & 0x80): return val, i
|
|
974
|
+
shift += 7
|
|
975
|
+
|
|
976
|
+
def walk(buf): # [(字段号, wire type, 值)]
|
|
977
|
+
i, out = 0, []
|
|
978
|
+
while i < len(buf):
|
|
979
|
+
tag, i = varint(buf, i)
|
|
980
|
+
fno, wt = tag >> 3, tag & 7
|
|
981
|
+
if wt == 0:
|
|
982
|
+
v, i = varint(buf, i); out.append((fno, wt, v))
|
|
983
|
+
elif wt == 2:
|
|
984
|
+
ln, i = varint(buf, i); out.append((fno, wt, buf[i:i+ln])); i += ln
|
|
985
|
+
elif wt == 5:
|
|
986
|
+
out.append((fno, wt, buf[i:i+4])); i += 4
|
|
987
|
+
elif wt == 1:
|
|
988
|
+
out.append((fno, wt, buf[i:i+8])); i += 8
|
|
989
|
+
else:
|
|
990
|
+
raise ValueError('wt %d' % wt)
|
|
991
|
+
return out
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
字段号对不上时先怀疑**这一层不是 message**(`#5` 在 chat_room 里就是 `string` 而不是 `int32`),
|
|
995
|
+
别急着改数据。
|
|
996
|
+
|
|
997
|
+
**6.1 那几条怎么复跑**(都是只读,不需要联网):
|
|
998
|
+
|
|
999
|
+
4. 两族与取值:对所有行取顶层 `#3`,按 `v < 10^4` / `10^4 ≤ v < 10^5` / `v ≥ 10^8` 分桶;
|
|
1000
|
+
5. 秩相关:`offset`(`v` 减去该族的底数)= 自变量,成员数取 `#1` 的条数、消息数取
|
|
1001
|
+
`SELECT COUNT(*) FROM Msg_<md5(username)>`,用 Spearman(不假设线性);
|
|
1002
|
+
6. "差 1"那 5 个群:把 `chat_room.ext_buffer` 里每个成员子消息的 `#1`(userName)抽出来,
|
|
1003
|
+
与 `SELECT n.username FROM chatroom_member cm JOIN name2id n ON cm.member_id = n.rowid
|
|
1004
|
+
WHERE cm.room_id = ?` 求差集 —— 差集非空的行,再去 `chat_room_info_detail.ext_buffer_` 里
|
|
1005
|
+
看它的 `#3`/`#4` 是否差 1。(**注意:** 内层查询要用**另一个 cursor**,否则会把外层
|
|
1006
|
+
`for ... in cursor.execute(...)` 的迭代顶掉、只跑第一行 —— 我第一版就栽在这儿。)
|
|
1007
|
+
7. "它会变"那条靠**同一行隔一段时间重读**(本次是 ~1 小时)。
|
|
1008
|
+
8. 3.2 那层字符串混淆(与 DB 无关,纯离线):
|
|
1009
|
+
- 抓记录:正则 `\xa1([0-9A-F]{4})\xb1([0-9A-F]{2,400})`,长度字段是**小端**
|
|
1010
|
+
(`int(h[2:4]+h[0:2], 16)`,直接 `int(h,16)` 会把它当 0x0D00=3328,然后一条都取不到);
|
|
1011
|
+
- 定密钥:逐位搜索(每位 0..255 试一遍,取"解出的字节落在标识符字符集里比例最高"的那档),
|
|
1012
|
+
前 30 位就够出人话;**注意逐位搜索会在"两个候选都是字母"时选错**(本机栽在第 0 位:
|
|
1013
|
+
`kernel` 被解成 `rernel`)——用一眼能认出的 crib 修(`rernel::`→`kernel::` 定出该位差 `^0x19`),
|
|
1014
|
+
再按同样办法往后扩位。
|
|
1015
|
+
9. **6.1 ⑧ 那几条怎么复跑**(都只读):
|
|
1016
|
+
- "不是人数":`name2id` 查 `MIN(rowid)/MAX(rowid)/COUNT(*)` 看有没有洞;再确认
|
|
1017
|
+
`chatroom_member.member_id == name2id.rowid` 的命中率;拿 `offset` 最大的房跟 `COUNT(*)` 比;
|
|
1018
|
+
- "offset=0 的房 = 成员表没动过":对这些房取 `chatroom_member` 的 rowid,**按 rowid 排序后数连续段**
|
|
1019
|
+
(段数=1 且段长=成员数 ⇒ 一次性写入),再去消息库看该房有没有成员变动系统消息;
|
|
1020
|
+
- "随变动时间窗":用 `chatroom_member.rowid` 的 `max−min` 当 spread,与 `offset` 做 Spearman,
|
|
1021
|
+
**并同时算"控制成员数后的偏相关"**(只报 raw 相关会被"人多→跨度天然大"混淆)。
|
|
1022
|
+
10. **想要"第二时点"时的可复制做法**(本机没有,但方法通用):把库与它的 `-wal` **拷到临时目录**,
|
|
1023
|
+
按帧边界(`32 + k × (24 + page_size)`)**截断 WAL 副本**再打开 ⇒ 每截一次就是稍早的一个状态;
|
|
1024
|
+
或定期留一份**只读工作副本**,之后与现库对比(本机就是这么拿到"35 分钟内 77 房 `#3` 全等"的)。
|
|
1025
|
+
⚠️ 两条前提都记在 6.2:**先确认 WAL 里真的有目标表的帧**、**`.material` 不是页快照**。
|
|
1026
|
+
11. **成员 `status` 那 5 个字段怎么复跑**(§6,只读):
|
|
1027
|
+
- 位对:对每个成员子消息的 `#3` 直接数 `((v>>a)&1) and ((v>>b)&1)` —— 本机量到 `bit4&bit13 = 0`、
|
|
1028
|
+
`bit11&{13,20,21,22}` 四对全 0、`bit20&bit22 = 18`(4151 人里);
|
|
1029
|
+
- 强制位:把 bits20–22 当**组**读,看取值只有 {2,3,6,7}(等价于 bit20 ⊆ bit21、bit22 ⊆ bit21);
|
|
1030
|
+
- ⚠️ 这类 `⊤ / ⊥` 断言**必须先逐位重做**,别拿整值做列联表(两个位会互相抵消 —— §6 那处方法更正就是栽在这儿)。
|
|
1031
|
+
12. **`contact.extra_buffer` 的形状与"骨架同型"怎么复跑**(§3 / §7,只读):
|
|
1032
|
+
- 交叉表:每行取 `(顶层字段数 ≥ 33, #3 的线格式)` 四格计数 ⇒ 本机 `(大,varint)=940`、`(小,ld)=42`、
|
|
1033
|
+
`(小,varint)=2`、`(小,无 #3)=2`(合计 986);
|
|
1034
|
+
- 骨架:把大 proto 每字段的"字段号 → 线格式众数"与 `stranger` 那 1 行**逐项比** ⇒ 本机 `#2`…`#38` 完全一致;
|
|
1035
|
+
- 值对齐(判负那条):对每行把 blob 每个字段的原始值 / 它的 md5 / sha1 / b64 / hex / utf-16le 与该行 22 列逐一比,
|
|
1036
|
+
只统计**长字符串、哈希、URL、wxid 级**命中(短整数必然撞车,不算)。
|
|
1037
|
+
13. **`verify_flag` 那几条等价怎么复跑**(§6,只读):**逐位**而不是整值 —— 对 `contact` 每一行取 `verify_flag & 位`,
|
|
1038
|
+
与"该 `username` 在不在 `biz_info`"、"`external_info` 里有没有 `VerifySource` / `PersonVerifyInfo.VerifyDesc`"
|
|
1039
|
+
做四格。本机 `bit3(8) ⟺ 在 biz_info` 是 **687/687 双向、两个错格 0**。
|
|
1040
|
+
⚠️ 内层查 `biz_info` / `external_info` 要用**另一个 cursor**(同时 `external_info` 里的 JSON 要按 `bytes` 读,别被 `SELECT *` 的 UTF-8 解码坑到)。
|
|
1041
|
+
14. **跨表比对(§7 的 `#9` 那条)怎么复跑**:把目标字段的**去重值**取出来,与**每张表的每个文本/blob 列**
|
|
1042
|
+
做**整值相等**(不是子串),并且**必须区分"同行"与"异行"** —— 同行相等才是"本行属性",
|
|
1043
|
+
异行相等只是"两个对象共有同一个值"。本机结果:`#9` 同行相等 **213/567**、`#4` **1/673**。
|
|
1044
|
+
若一定要用子串,**必须设长度门槛**(本机 `#9` 的子串命中全是 16~23 字节撞车,≥24B 就掉到 0)。
|
|
1045
|
+
15. **二进制的名字搜索怎么复跑(§3.3)**:**先确认地址口径** —— 本机 `Weixin.dll` 是 **64 位**(imagebase `0x180000000`)、
|
|
1046
|
+
**每个节 `VA ≠ PRAW`**,而描述符表里**存的是文件偏移**:拿字符串的**文件偏移**做 `struct.pack('<I', off)` 全文件搜才有命中
|
|
1047
|
+
(用真 RVA 或 VA 搜**全是 0**,会得出假的"无引用")。判"挨着的一串名字是不是同一个 message 的字段表",
|
|
1048
|
+
**要看每个名字各自的引用数**,不能看相邻性(本机 `AnnouncementEditor`/`ChatRoomStatus` 引用数各 0)。
|
|
1049
|
+
23. **"字段值 = 某个可数量"这一族怎么复跑(§7)**:候选计数从**同行** `external_info` 现算
|
|
1050
|
+
(键数 / 非空键数 / 叶子数 / 菜单按钮数 / 子按钮数 / 小程序条目数 / 品牌 URL 数 / 各子对象的键数…),
|
|
1051
|
+
**只在字段非零的行上比**(否则"双方同时为 0"会刷出假命中,本机第一版就得到 676/688 的假象);
|
|
1052
|
+
另可比"该行 8 个可选字段(`#13/#14/#26/#27/#33/#36/#41/#43`)里出现了几个"。本机结果 0~25/941 ⇒ 这一族出局。
|
|
1053
|
+
24. **"某个值是不是客户端的字面量"怎么复跑(§7)**:两处都要搜,且**都要要求"独立成串"** ——
|
|
1054
|
+
①混淆字符串池(`A_pool.txt` 那套解码,**41,913 条记录**;注意落盘文件行数更多,因为有 8 条记录明文自带换行);
|
|
1055
|
+
②`Weixin.dll` 原始字节(`mmap.find`,命中的**前后字节不能是字母/数字/下划线**才算)。
|
|
1056
|
+
**必须带正对照**:`micromsg.ContactExtData` / `Weixin.exe` / `wcdb` 各应命中 1 处。
|
|
1057
|
+
少了"独立成串"这一步,本机会得到 `#6` 51/51、`#7` 119/120 的**假命中**(短英文词在 192 MB 里必然作为子串出现)。
|
|
1058
|
+
16. **§6.1 ⑨ 那条(用群事件日志打 `#3` 的步长)怎么复跑**:`off = #3 − 底数`(底数 10000 / 7×10⁸ / 1~2k,见 ③);
|
|
1059
|
+
事件条数 = 该房消息表里 `(local_type & 0xFFFFFFFF) = 10000` 的行数(**只数条数与时间,不读正文**)。
|
|
1060
|
+
判据:**挑历史起点就在保留窗口内的房**(最省事的判法是挑 2026-08 之后才建的房)——
|
|
1061
|
+
若 `off` 大于该房任何可能的加入总数,就否掉了"一步 = 一个人"。
|
|
1062
|
+
17. **§6.1 ⑩ 那条(搜"第二份")怎么复跑**:**必须用"同对象"严判据** —— 先找**键列**(`username` / `room_id` /
|
|
1063
|
+
`chat_name` 之类)命中该房的那一行,**再看这一行的其它列**是否等于该房的 `#3` / `offset`(整数相等,
|
|
1064
|
+
或 blob 里含 4/8 字节小端)。**别用"候选整数 `IN (...)` 全库扫"** —— 本机那么做得到 279 条命中、
|
|
1065
|
+
**全是小整数撞车**(0/1/2/10 这些值到处都是)。跑完要报**正对照**(同一次扫描里有没有本该命中的东西命中了)
|
|
1066
|
+
和**跳过了什么**(本机跳了 735 张 `Msg_*`、以及用哈希作键的表)。
|
|
1067
|
+
18. **§7 那条"资料文本汇集"怎么复跑**:把 `biz_info` 的 `external_info` / `brand_info` **递归展成叶子**,
|
|
1068
|
+
再查某字段的值里**含不含同一行自己的叶子**。**必须用"自/他对照"**:同一批文本,一次比**本行**叶子、
|
|
1069
|
+
一次比**换一行**的叶子;两者相近就是撞车。**叶子要设长度门槛**(本机用 ≥8 字节;2 字节的叶子几乎什么都"含",
|
|
1070
|
+
本机 `#5` 那 22 条在对照下全消失)。本机结果:本行 **336** vs 他行 **7**。
|
|
1071
|
+
19. **§7 那条 `#5 = 国家/地区码` 怎么复跑**:取 940 行大 proto 的顶层 `#5`(2 字节的那些),
|
|
1072
|
+
去重后与一份 **ISO 3166-1 alpha-2 码表**(249 条)比 —— 本机 **25/25 命中**(随机的期望命中率是 249/676 ≈ 0.37,
|
|
1073
|
+
全中概率 ≈ 1.4e-11)。**必须再补一条不依赖该码表的预测**:若真是地区码、且本机账号多在中国,**众数应为 `CN`** ——
|
|
1074
|
+
本机通过(占 94.0%)。反向口径也做一下:按 **ISO 639-1 语言码**(小写)比只有 15/25。
|
|
1075
|
+
20. **`#13 = biz_info.type` 怎么复跑**:按 `username` 把 `contact` 的 blob 与大 proto 的 `#13` 对齐到 `biz_info.type`,
|
|
1076
|
+
逐行比 —— 本机 **687/687 相等、零反例**(且 `#13` 出现 ⟺ 该 username 在 `biz_info` 里)。
|
|
1077
|
+
**注意判据的对称性**:这里"相等"是双向的(`#13` 不存在 ⟺ 不在 `biz_info`),比单看一个比例硬。
|
|
1078
|
+
21. **`#41 = 最近更新时间` 怎么复跑**:算 `#41`(非零的那些)与 `contact.id` 的 **Spearman**(本机 **0.945**),
|
|
1079
|
+
再数**反例**:`id` 小(比如 <1500)却带**新**时间戳(2025/2026)的行数 —— 本机 **40 行**。
|
|
1080
|
+
**只要有反例,"创建时间"就出局**(创建时间不可能让老 id 带新时间戳);高但不到 1 的相关正是"多数行没改过"的表现。
|
|
1081
|
+
⚠️ 这条判据依赖"id ≈ 进库先后",别在别处想当然。
|
|
1082
|
+
22. **"某个 ld 字段是不是 message"怎么判**:把它再走一层,**看形状在同一批行里是否全量重复** ——
|
|
1083
|
+
本机 `#14` **830/830**、`#27` **937/937** 一致(是 message);`#6` **555/940**、`#7` **473/940**、
|
|
1084
|
+
文本字段 `#4` **294/940**("走通"是随机字节撞 tag 的假象)⇒ **不算**。
|
|
1085
|
+
**别用"能走通"当判据**,那会把文本字段判成 message。
|