@chainlesschain/personal-data-hub 0.4.29 → 0.4.31

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.
Files changed (199) hide show
  1. package/lib/forensics/qq-nt-collect.js +190 -0
  2. package/lib/prompt-builder.js +15 -1
  3. package/package.json +8 -3
  4. package/__tests__/adapter-guide.test.js +0 -47
  5. package/__tests__/adapter-spec.test.js +0 -78
  6. package/__tests__/adapters/ai-chat-cookie-capture-spec.test.js +0 -211
  7. package/__tests__/adapters/ai-chat-health-checker.test.js +0 -262
  8. package/__tests__/adapters/ai-chat-history.test.js +0 -396
  9. package/__tests__/adapters/ai-chat-http-client.test.js +0 -242
  10. package/__tests__/adapters/ai-chat-vendors.test.js +0 -874
  11. package/__tests__/adapters/alipay-bill-adapter.test.js +0 -538
  12. package/__tests__/adapters/apple-health.test.js +0 -95
  13. package/__tests__/adapters/bank-family.test.js +0 -125
  14. package/__tests__/adapters/biz-tianyancha.test.js +0 -159
  15. package/__tests__/adapters/browser-history-chrome.test.js +0 -377
  16. package/__tests__/adapters/browser-history-edge.test.js +0 -159
  17. package/__tests__/adapters/car-mercedesme.test.js +0 -74
  18. package/__tests__/adapters/doc-baidu-netdisk.test.js +0 -102
  19. package/__tests__/adapters/doc-camscanner.test.js +0 -147
  20. package/__tests__/adapters/doc-platforms.test.js +0 -177
  21. package/__tests__/adapters/edu-huawei-learning-live.test.js +0 -198
  22. package/__tests__/adapters/edu-zuoyebang-live.test.js +0 -226
  23. package/__tests__/adapters/email-adapter-snapshot.test.js +0 -237
  24. package/__tests__/adapters/email-adapter.test.js +0 -742
  25. package/__tests__/adapters/email-classifier.test.js +0 -347
  26. package/__tests__/adapters/email-imap-session.test.js +0 -334
  27. package/__tests__/adapters/email-parser.test.js +0 -244
  28. package/__tests__/adapters/email-pdf-extractor.test.js +0 -529
  29. package/__tests__/adapters/email-providers.test.js +0 -84
  30. package/__tests__/adapters/email-retry-progress.test.js +0 -294
  31. package/__tests__/adapters/email-templates.test.js +0 -822
  32. package/__tests__/adapters/family-23-collectors-scaffold.test.js +0 -182
  33. package/__tests__/adapters/finance-alipay-live.test.js +0 -258
  34. package/__tests__/adapters/finance-dcep.test.js +0 -74
  35. package/__tests__/adapters/fitness-joyrun.test.js +0 -82
  36. package/__tests__/adapters/game-genshin-live.test.js +0 -238
  37. package/__tests__/adapters/game-genshin-scaffold.test.js +0 -108
  38. package/__tests__/adapters/game-honor-of-kings-live.test.js +0 -230
  39. package/__tests__/adapters/git-activity.test.js +0 -222
  40. package/__tests__/adapters/gov-12123.test.js +0 -103
  41. package/__tests__/adapters/gov-ixiamen.test.js +0 -150
  42. package/__tests__/adapters/gov-tax.test.js +0 -135
  43. package/__tests__/adapters/health-meiyou.test.js +0 -125
  44. package/__tests__/adapters/local-files.test.js +0 -264
  45. package/__tests__/adapters/local-im-pc.test.js +0 -154
  46. package/__tests__/adapters/messaging-whatsapp.test.js +0 -289
  47. package/__tests__/adapters/music-kugou.test.js +0 -187
  48. package/__tests__/adapters/music-qq.test.js +0 -112
  49. package/__tests__/adapters/netease-music-live.test.js +0 -244
  50. package/__tests__/adapters/netease-music.test.js +0 -74
  51. package/__tests__/adapters/pc-local-discovery.test.js +0 -141
  52. package/__tests__/adapters/qq-pc-direct-read.test.js +0 -227
  53. package/__tests__/adapters/reading-family.test.js +0 -108
  54. package/__tests__/adapters/recruit-boss.test.js +0 -180
  55. package/__tests__/adapters/shell-history.test.js +0 -180
  56. package/__tests__/adapters/shopping-base.test.js +0 -179
  57. package/__tests__/adapters/shopping-dianping.test.js +0 -239
  58. package/__tests__/adapters/social-bilibili-adb-api-client.test.js +0 -721
  59. package/__tests__/adapters/social-bilibili-adb-chromium-cookies-reader.test.js +0 -346
  60. package/__tests__/adapters/social-bilibili-adb-collector.test.js +0 -284
  61. package/__tests__/adapters/social-bilibili-adb-cookies-extension.test.js +0 -343
  62. package/__tests__/adapters/social-bilibili-adb-snapshot-builder.test.js +0 -296
  63. package/__tests__/adapters/social-csdn.test.js +0 -175
  64. package/__tests__/adapters/social-dongchedi.test.js +0 -165
  65. package/__tests__/adapters/social-douyin-adb-aweme-detail.test.js +0 -165
  66. package/__tests__/adapters/social-douyin-adb-collector.test.js +0 -254
  67. package/__tests__/adapters/social-douyin-adb-db-extension.test.js +0 -114
  68. package/__tests__/adapters/social-douyin-adb-im-db-parser.test.js +0 -304
  69. package/__tests__/adapters/social-douyin-adb-snapshot-builder.test.js +0 -216
  70. package/__tests__/adapters/social-douyin-adb-usage-profile.test.js +0 -229
  71. package/__tests__/adapters/social-douyin-adb-watch-history.test.js +0 -269
  72. package/__tests__/adapters/social-kuaishou-adb-api-client.test.js +0 -496
  73. package/__tests__/adapters/social-kuaishou-adb-collector.test.js +0 -276
  74. package/__tests__/adapters/social-kuaishou-adb-cookies-extension.test.js +0 -152
  75. package/__tests__/adapters/social-kuaishou-adb-snapshot-builder.test.js +0 -178
  76. package/__tests__/adapters/social-toutiao-adb-account-reader.test.js +0 -135
  77. package/__tests__/adapters/social-toutiao-adb-api-client.test.js +0 -626
  78. package/__tests__/adapters/social-toutiao-adb-article.test.js +0 -155
  79. package/__tests__/adapters/social-toutiao-adb-collector.test.js +0 -378
  80. package/__tests__/adapters/social-toutiao-adb-cookies-extension.test.js +0 -193
  81. package/__tests__/adapters/social-toutiao-adb-snapshot-builder.test.js +0 -196
  82. package/__tests__/adapters/social-toutiao-kuaishou-scaffold.test.js +0 -311
  83. package/__tests__/adapters/social-weibo-adb-api-client.test.js +0 -362
  84. package/__tests__/adapters/social-weibo-adb-collector.test.js +0 -201
  85. package/__tests__/adapters/social-weibo-adb-cookies-extension.test.js +0 -167
  86. package/__tests__/adapters/social-weibo-adb-snapshot-builder.test.js +0 -189
  87. package/__tests__/adapters/social-xiaohongshu-adb-api-client.test.js +0 -431
  88. package/__tests__/adapters/social-xiaohongshu-adb-collector.test.js +0 -207
  89. package/__tests__/adapters/social-xiaohongshu-adb-cookies-extension.test.js +0 -0
  90. package/__tests__/adapters/social-xiaohongshu-adb-sign-provider-injection.test.js +0 -351
  91. package/__tests__/adapters/social-xiaohongshu-adb-sign.test.js +0 -130
  92. package/__tests__/adapters/social-xiaohongshu-adb-snapshot-builder.test.js +0 -200
  93. package/__tests__/adapters/social-zhihu.test.js +0 -246
  94. package/__tests__/adapters/system-data-adapter.test.js +0 -443
  95. package/__tests__/adapters/system-data-android-ingest.test.js +0 -144
  96. package/__tests__/adapters/system-data-android.test.js +0 -519
  97. package/__tests__/adapters/system-data-disclosure.test.js +0 -153
  98. package/__tests__/adapters/travel-12306.test.js +0 -512
  99. package/__tests__/adapters/travel-amap.test.js +0 -219
  100. package/__tests__/adapters/travel-baidu-map.test.js +0 -305
  101. package/__tests__/adapters/travel-base.test.js +0 -205
  102. package/__tests__/adapters/travel-ctrip.test.js +0 -377
  103. package/__tests__/adapters/travel-didi-consumer.test.js +0 -66
  104. package/__tests__/adapters/travel-didi.test.js +0 -204
  105. package/__tests__/adapters/travel-tencent-map.test.js +0 -207
  106. package/__tests__/adapters/travel-tongcheng.test.js +0 -289
  107. package/__tests__/adapters/video-platforms.test.js +0 -152
  108. package/__tests__/adapters/video-xigua.test.js +0 -106
  109. package/__tests__/adapters/vscode.test.js +0 -299
  110. package/__tests__/adapters/wechat-bootstrap.test.js +0 -240
  111. package/__tests__/adapters/wechat-env-probe.test.js +0 -162
  112. package/__tests__/adapters/wechat-frida-agent.test.js +0 -322
  113. package/__tests__/adapters/wechat-frida-integration.test.js +0 -149
  114. package/__tests__/adapters/wechat-frida-key-provider.test.js +0 -188
  115. package/__tests__/adapters/wechat-md5-key-provider.test.js +0 -101
  116. package/__tests__/adapters/wechat-pc-direct-read.test.js +0 -365
  117. package/__tests__/adapters/wechat-pc-group-topic.test.js +0 -63
  118. package/__tests__/adapters/wechat-pc-v4-sidecar.test.js +0 -72
  119. package/__tests__/adapters/weread.test.js +0 -123
  120. package/__tests__/adapters/wework-pc.test.js +0 -124
  121. package/__tests__/adapters/win-recent.test.js +0 -192
  122. package/__tests__/analysis-skills.test.js +0 -754
  123. package/__tests__/analysis.test.js +0 -1845
  124. package/__tests__/audio-ximalaya-snapshot.test.js +0 -279
  125. package/__tests__/batch.test.js +0 -133
  126. package/__tests__/bridges-cc-kg.test.js +0 -231
  127. package/__tests__/bridges-cc-llm.test.js +0 -191
  128. package/__tests__/bridges-cc-rag.test.js +0 -162
  129. package/__tests__/categories.test.js +0 -92
  130. package/__tests__/e2e/ai-chat-cross-source-journey.test.js +0 -213
  131. package/__tests__/e2e/full-user-journey.test.js +0 -188
  132. package/__tests__/e2e/local-data-adapters-cli.e2e.test.js +0 -146
  133. package/__tests__/entity-resolver-ingest-hook.test.js +0 -177
  134. package/__tests__/entity-resolver-stages.test.js +0 -411
  135. package/__tests__/entity-resolver-vault.test.js +0 -249
  136. package/__tests__/entity-resolver.test.js +0 -526
  137. package/__tests__/fitness-keep-snapshot.test.js +0 -224
  138. package/__tests__/fixtures/entity-resolver-200-mock.json +0 -96
  139. package/__tests__/ids.test.js +0 -45
  140. package/__tests__/integration/ai-chat-history-registry.test.js +0 -228
  141. package/__tests__/integration/aichat-wizard-end-to-end.test.js +0 -282
  142. package/__tests__/integration/cross-adapter-pipelines.test.js +0 -396
  143. package/__tests__/integration/local-data-adapters-pipeline.test.js +0 -373
  144. package/__tests__/integration/social-bilibili-pipeline.test.js +0 -261
  145. package/__tests__/integration/wechat-bootstrap-end-to-end.test.js +0 -390
  146. package/__tests__/key-providers.test.js +0 -126
  147. package/__tests__/kg-derive.test.js +0 -219
  148. package/__tests__/llm-client.test.js +0 -122
  149. package/__tests__/longtail-adapters.test.js +0 -281
  150. package/__tests__/messaging-qq-snapshot.test.js +0 -294
  151. package/__tests__/mobile-extractor-encrypted.test.js +0 -460
  152. package/__tests__/mobile-extractor.test.js +0 -288
  153. package/__tests__/mock-adapter.test.js +0 -93
  154. package/__tests__/prompt-builder.test.js +0 -249
  155. package/__tests__/query-parser.test.js +0 -365
  156. package/__tests__/rag-derive.test.js +0 -169
  157. package/__tests__/registry-readiness.test.js +0 -292
  158. package/__tests__/registry.test.js +0 -420
  159. package/__tests__/salvage-ingest.test.js +0 -97
  160. package/__tests__/schemas.test.js +0 -331
  161. package/__tests__/shopping-adapters.test.js +0 -392
  162. package/__tests__/shopping-eleme-snapshot.test.js +0 -454
  163. package/__tests__/shopping-pinduoduo-snapshot.test.js +0 -484
  164. package/__tests__/shopping-snapshot.test.js +0 -438
  165. package/__tests__/shopping-vipshop-snapshot.test.js +0 -425
  166. package/__tests__/shopping-xianyu-snapshot.test.js +0 -451
  167. package/__tests__/sidecar-contacts-cross-validate.test.js +0 -186
  168. package/__tests__/sidecar-supervisor.test.js +0 -128
  169. package/__tests__/sign-providers.test.js +0 -62
  170. package/__tests__/social-adapters.test.js +0 -280
  171. package/__tests__/social-bilibili-snapshot.test.js +0 -278
  172. package/__tests__/social-douban-snapshot.test.js +0 -351
  173. package/__tests__/social-douyin-im-direct-read.test.js +0 -377
  174. package/__tests__/social-douyin-salvage-collector.test.js +0 -98
  175. package/__tests__/social-douyin-salvage-mapper.test.js +0 -90
  176. package/__tests__/social-douyin-snapshot.test.js +0 -256
  177. package/__tests__/social-kuaishou-snapshot.test.js +0 -362
  178. package/__tests__/social-toutiao-snapshot.test.js +0 -366
  179. package/__tests__/social-weibo-snapshot.test.js +0 -234
  180. package/__tests__/social-weibo-sqlite-device.test.js +0 -174
  181. package/__tests__/social-xiaohongshu-snapshot.test.js +0 -232
  182. package/__tests__/sqlite-leaf-salvage.test.js +0 -97
  183. package/__tests__/travel-adapters.test.js +0 -483
  184. package/__tests__/travel-maps-snapshot.test.js +0 -426
  185. package/__tests__/vault-driver-error.test.js +0 -74
  186. package/__tests__/vault-search-helpers.test.js +0 -104
  187. package/__tests__/vault-search.test.js +0 -423
  188. package/__tests__/vault.test.js +0 -767
  189. package/__tests__/wechat-adapter.test.js +0 -594
  190. package/__tests__/whatsapp-adapter.test.js +0 -138
  191. package/scripts/_make-fixture-all.js +0 -126
  192. package/scripts/_make-fixture-contacts.js +0 -84
  193. package/scripts/evaluate-entity-resolver.js +0 -213
  194. package/scripts/run-native-tests-sandbox.sh +0 -55
  195. package/scripts/smoke-phase-5-5.js +0 -196
  196. package/scripts/smoke-phase-5-7.js +0 -181
  197. package/scripts/smoke-system-data-contacts.js +0 -309
  198. package/scripts/smoke-system-data.js +0 -312
  199. package/vitest.config.js +0 -88
@@ -1,594 +0,0 @@
1
- "use strict";
2
-
3
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
4
-
5
- const fs = require("node:fs");
6
- const path = require("node:path");
7
- const os = require("node:os");
8
- const crypto = require("node:crypto");
9
-
10
- const {
11
- WechatAdapter,
12
- parseWeChatContent,
13
- extractWeChatKey,
14
- deriveWeChatLegacyKey,
15
- WeChatDBReader,
16
- normalizeWeChatMessage,
17
- normalizeWeChatContact,
18
- wxidToWeChatPersonId,
19
- isWeChatGroupTalker,
20
- WECHAT_VERSION,
21
- } = require("../lib/adapters/wechat");
22
- const { assertAdapter } = require("../lib/adapter-spec");
23
- const { validateBatch } = require("../lib/batch");
24
-
25
- // ─── content-parser ──────────────────────────────────────────────────────
26
-
27
- describe("parseWeChatContent — type 1 text", () => {
28
- it("plain text message", () => {
29
- const r = parseWeChatContent({ type: 1, content: "你好", talker: "wxid_friend" });
30
- expect(r.kind).toBe("text");
31
- expect(r.text).toBe("你好");
32
- });
33
-
34
- it("group message strips sender prefix", () => {
35
- const r = parseWeChatContent({
36
- type: 1,
37
- content: "wxid_someone:\n大家好",
38
- talker: "12345@chatroom",
39
- });
40
- expect(r.text).toBe("大家好");
41
- expect(r.structured.senderWxid).toBe("wxid_someone");
42
- });
43
- });
44
-
45
- describe("parseWeChatContent — media types", () => {
46
- it("type 3 image", () => {
47
- const xml = '<img cdnbigimgurl="https://x.cn/a" md5="abc123" length="12345" />';
48
- const r = parseWeChatContent({ type: 3, content: xml, talker: "wxid_friend" });
49
- expect(r.kind).toBe("image");
50
- expect(r.text).toBe("[图片]");
51
- expect(r.structured.cdnUrl).toBe("https://x.cn/a");
52
- expect(r.structured.md5).toBe("abc123");
53
- expect(r.structured.length).toBe(12345);
54
- });
55
-
56
- it("type 34 voice", () => {
57
- const xml = '<voicemsg voicelength="3000" clientmsgid="voice123" />';
58
- const r = parseWeChatContent({ type: 34, content: xml, talker: "wxid_friend" });
59
- expect(r.kind).toBe("voice");
60
- expect(r.structured.voiceLength).toBe(3000);
61
- });
62
-
63
- it("type 48 location", () => {
64
- const xml = '<location x="31.23" y="121.47" label="上海" poiname="外滩" />';
65
- const r = parseWeChatContent({ type: 48, content: xml, talker: "wxid_friend" });
66
- expect(r.kind).toBe("location");
67
- expect(r.structured.x).toBe(31.23);
68
- expect(r.structured.y).toBe(121.47);
69
- expect(r.structured.label).toBe("上海");
70
- });
71
- });
72
-
73
- describe("parseWeChatContent — type 49 appmsg sub-types", () => {
74
- it("sub 5 link", () => {
75
- const xml = '<msg><appmsg type="5"><title>趣文一篇</title><des>简介</des><url>https://x.cn</url></appmsg></msg>';
76
- const r = parseWeChatContent({ type: 49, content: xml, talker: "wxid_friend" });
77
- expect(r.kind).toBe("link");
78
- expect(r.structured.title).toBe("趣文一篇");
79
- expect(r.structured.url).toBe("https://x.cn");
80
- });
81
-
82
- it("sub 21 redpacket", () => {
83
- const xml = '<msg><appmsg type="21"><title>恭喜发财</title></appmsg></msg>';
84
- const r = parseWeChatContent({ type: 49, content: xml, talker: "wxid_friend" });
85
- expect(r.kind).toBe("redpacket");
86
- expect(r.structured.redPacketTitle).toBe("恭喜发财");
87
- });
88
-
89
- it("sub 6 file", () => {
90
- const xml = '<msg><appmsg type="6"><title>合同.pdf</title><totallen>523456</totallen></appmsg></msg>';
91
- const r = parseWeChatContent({ type: 49, content: xml, talker: "wxid_friend" });
92
- expect(r.kind).toBe("file");
93
- expect(r.structured.fileName).toBe("合同.pdf");
94
- expect(r.structured.fileSize).toBe("523456");
95
- });
96
- });
97
-
98
- describe("parseWeChatContent — system + unknown", () => {
99
- it("type 10000 system", () => {
100
- const r = parseWeChatContent({ type: 10000, content: '<msg>"张三"加入了群聊</msg>', talker: "12345@chatroom" });
101
- expect(r.kind).toBe("system");
102
- expect(r.text).toContain("加入了群聊");
103
- });
104
-
105
- it("unknown type falls through with kind=type-N", () => {
106
- const r = parseWeChatContent({ type: 99999, content: "x", talker: "wxid_f" });
107
- expect(r.kind).toBe("type-99999");
108
- });
109
-
110
- it("invalid input is safe", () => {
111
- const r = parseWeChatContent(null);
112
- expect(r.kind).toBe("unknown");
113
- });
114
- });
115
-
116
- // ─── key-extractor ───────────────────────────────────────────────────────
117
-
118
- describe("deriveWeChatLegacyKey", () => {
119
- it("matches MD5(IMEI+UIN)[:7]", () => {
120
- const imei = "123456789012345";
121
- const uin = "987654321";
122
- const expected = crypto.createHash("md5").update(imei + uin, "utf-8").digest("hex").slice(0, 7);
123
- expect(deriveWeChatLegacyKey(imei, uin)).toBe(expected);
124
- });
125
-
126
- it("lowercase hex", () => {
127
- const k = deriveWeChatLegacyKey("INFRA", "user123");
128
- expect(k).toMatch(/^[0-9a-f]{7}$/);
129
- });
130
-
131
- it("throws on missing inputs", () => {
132
- expect(() => deriveWeChatLegacyKey("", "uin")).toThrow();
133
- expect(() => deriveWeChatLegacyKey("imei", null)).toThrow();
134
- });
135
- });
136
-
137
- describe("extractWeChatKey", () => {
138
- let dir;
139
- beforeEach(() => {
140
- dir = fs.mkdtempSync(path.join(os.tmpdir(), "wechat-key-"));
141
- });
142
- afterEach(() => {
143
- try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_e) {}
144
- });
145
-
146
- function writeAuthXml(uin = "1234567890") {
147
- fs.mkdirSync(path.join(dir, "shared_prefs"), { recursive: true });
148
- fs.writeFileSync(
149
- path.join(dir, "shared_prefs", "auth_info_key_prefs.xml"),
150
- `<?xml version='1.0' encoding='utf-8'?><map><int name="_auth_uin" value="${uin}" /></map>`,
151
- );
152
- }
153
-
154
- function writeCompatibleInfo(imei = "123456789012345") {
155
- fs.mkdirSync(path.join(dir, "MicroMsg"), { recursive: true });
156
- // CompatibleInfo.cfg is a Java HashMap serialization; we just want
157
- // to embed an IMEI in the binary so the regex finds it.
158
- const padding = Buffer.from("padding-before-imei-\x00\x01\x02", "binary");
159
- const imeiPart = Buffer.from(`\x00${imei}\x00more-after`, "binary");
160
- fs.writeFileSync(
161
- path.join(dir, "MicroMsg", "CompatibleInfo.cfg"),
162
- Buffer.concat([padding, imeiPart]),
163
- );
164
- }
165
-
166
- it("happy path: extracts UIN + IMEI + derives key", () => {
167
- writeAuthXml("888888888");
168
- writeCompatibleInfo("864000000000000");
169
- const r = extractWeChatKey({ wechatDataPath: dir });
170
- expect(r.uin).toBe("888888888");
171
- expect(r.imei).toBe("864000000000000");
172
- expect(r.key).toBeTruthy();
173
- expect(r.key.length).toBe(7);
174
- });
175
-
176
- it("missing UIN → key null + warning", () => {
177
- writeCompatibleInfo("864000000000000");
178
- const r = extractWeChatKey({ wechatDataPath: dir });
179
- expect(r.key).toBeNull();
180
- expect(r.warnings.some((w) => /UIN/.test(w))).toBe(true);
181
- });
182
-
183
- it("manual override skips XML", () => {
184
- const r = extractWeChatKey({
185
- wechatDataPath: dir,
186
- uin: "manual-uin",
187
- imei: "manual-imei",
188
- });
189
- expect(r.uin).toBe("manual-uin");
190
- expect(r.imei).toBe("manual-imei");
191
- expect(r.key).toBeTruthy();
192
- });
193
-
194
- it("requires wechatDataPath", () => {
195
- expect(() => extractWeChatKey({})).toThrow();
196
- });
197
- });
198
-
199
- // ─── isWeChatGroupTalker ────────────────────────────────────────────────
200
-
201
- describe("isWeChatGroupTalker", () => {
202
- it("@chatroom suffix is group", () => {
203
- expect(isWeChatGroupTalker("12345678@chatroom")).toBe(true);
204
- });
205
- it("wxid_xxx is 1-on-1", () => {
206
- expect(isWeChatGroupTalker("wxid_friend")).toBe(false);
207
- });
208
- it("invalid input → false", () => {
209
- expect(isWeChatGroupTalker(null)).toBe(false);
210
- });
211
- });
212
-
213
- // ─── wxidToPersonId ──────────────────────────────────────────────────────
214
-
215
- describe("wxidToWeChatPersonId", () => {
216
- it("stable id format", () => {
217
- expect(wxidToWeChatPersonId("wxid_friend")).toBe("person-wechat-wxid_friend");
218
- });
219
- it("null wxid → null", () => {
220
- expect(wxidToWeChatPersonId(null)).toBeNull();
221
- });
222
- });
223
-
224
- // ─── normalizeMessage ────────────────────────────────────────────────────
225
-
226
- describe("normalizeWeChatMessage", () => {
227
- it("1-on-1 text inbound", () => {
228
- const row = {
229
- msgId: 1, msgSvrId: 100, talker: "wxid_friend",
230
- content: "你好", type: 1, createTime: Date.now(), isSend: 0,
231
- };
232
- const b = normalizeWeChatMessage(row, { accountUin: "self123" });
233
- expect(b.events).toHaveLength(1);
234
- expect(b.events[0].subtype).toBe("message");
235
- expect(b.events[0].actor).toBe("person-wechat-wxid_friend");
236
- expect(b.events[0].content.text).toBe("你好");
237
- expect(b.persons).toHaveLength(1);
238
- expect(b.persons[0].identifiers.wechatId).toBe("wxid_friend");
239
- const v = validateBatch(b);
240
- expect(v.valid).toBe(true);
241
- });
242
-
243
- it("1-on-1 text outbound", () => {
244
- const row = {
245
- msgSvrId: 101, talker: "wxid_friend",
246
- content: "你好", type: 1, createTime: Date.now(), isSend: 1,
247
- };
248
- const b = normalizeWeChatMessage(row, { accountUin: "self123" });
249
- expect(b.events[0].actor).toBe("person-wechat-self123");
250
- });
251
-
252
- it("group message produces Topic + isGroup extra", () => {
253
- const row = {
254
- msgSvrId: 102, talker: "1234@chatroom",
255
- content: "wxid_friend:\n大家好", type: 1, createTime: Date.now(), isSend: 0,
256
- };
257
- const b = normalizeWeChatMessage(row, {
258
- accountUin: "self123",
259
- chatroomByName: { "1234@chatroom": "测试群" },
260
- });
261
- expect(b.events[0].extra.isGroup).toBe(true);
262
- expect(b.topics).toHaveLength(1);
263
- expect(b.topics[0].name).toBe("测试群");
264
- expect(b.events[0].actor).toBe("person-wechat-wxid_friend");
265
- // 2 persons: the peer chatroom (subtype:unknown — not really a person but kept for ref) + sender
266
- expect(b.persons.length).toBeGreaterThanOrEqual(1);
267
- });
268
-
269
- it("media types map to media subtype", () => {
270
- const row = {
271
- msgSvrId: 103, talker: "wxid_friend",
272
- content: '<img cdnbigimgurl="https://x.cn/i" md5="x" />',
273
- type: 3, createTime: Date.now(), isSend: 0,
274
- };
275
- const b = normalizeWeChatMessage(row, { accountUin: "self123" });
276
- expect(b.events[0].subtype).toBe("media");
277
- });
278
-
279
- it("redpacket appmsg maps to redenvelope", () => {
280
- const row = {
281
- msgSvrId: 104, talker: "wxid_friend",
282
- content: '<msg><appmsg type="21"><title>红包</title></appmsg></msg>',
283
- type: 49, createTime: Date.now(), isSend: 0,
284
- };
285
- const b = normalizeWeChatMessage(row, { accountUin: "self123" });
286
- expect(b.events[0].subtype).toBe("redenvelope");
287
- });
288
-
289
- it("system messages map to interaction subtype", () => {
290
- const row = {
291
- msgSvrId: 105, talker: "1234@chatroom",
292
- content: '"张三"加入了群聊',
293
- type: 10000, createTime: Date.now(), isSend: 0,
294
- };
295
- const b = normalizeWeChatMessage(row, { accountUin: "self123" });
296
- expect(b.events[0].subtype).toBe("interaction");
297
- });
298
-
299
- it("rejects missing row", () => {
300
- expect(() => normalizeWeChatMessage(null)).toThrow();
301
- });
302
- });
303
-
304
- // ─── normalizeContact ───────────────────────────────────────────────────
305
-
306
- describe("normalizeWeChatContact", () => {
307
- it("produces Person with names from conRemark / nickname / alias", () => {
308
- const b = normalizeWeChatContact({
309
- username: "wxid_mom",
310
- alias: "mom123",
311
- nickname: "妈",
312
- conRemark: "亲妈",
313
- type: 1,
314
- });
315
- expect(b.persons).toHaveLength(1);
316
- expect(b.persons[0].names).toContain("亲妈");
317
- expect(b.persons[0].names).toContain("妈");
318
- expect(b.persons[0].identifiers.wechatId).toBe("wxid_mom");
319
- });
320
-
321
- it("chatroom username → subtype unknown (not a real Person)", () => {
322
- const b = normalizeWeChatContact({
323
- username: "12345@chatroom",
324
- nickname: "群名",
325
- type: 2,
326
- });
327
- expect(b.persons[0].subtype).toBe("unknown");
328
- });
329
-
330
- it("missing username → empty batch", () => {
331
- const b = normalizeWeChatContact({});
332
- expect(b.persons).toHaveLength(0);
333
- });
334
-
335
- // sjqz parity audit follow-up (post-Phase 12.6.10) — classify
336
- // 公众号 / Official Accounts (gh_*) as merchant subtype so the Ask
337
- // flow / EntityResolver can filter them out of human contacts.
338
- it("gh_* username → subtype merchant (公众号 / Official Account)", () => {
339
- const b = normalizeWeChatContact({
340
- username: "gh_abc123def",
341
- nickname: "某品牌官方",
342
- type: 3,
343
- });
344
- expect(b.persons).toHaveLength(1);
345
- expect(b.persons[0].subtype).toBe("merchant");
346
- expect(b.persons[0].identifiers.wechatId).toBe("gh_abc123def");
347
- });
348
-
349
- it("regular wxid_* → subtype contact (default)", () => {
350
- const b = normalizeWeChatContact({
351
- username: "wxid_realfriend",
352
- nickname: "好友",
353
- type: 1,
354
- });
355
- expect(b.persons[0].subtype).toBe("contact");
356
- });
357
- });
358
-
359
- // ─── WechatAdapter contract + sync flow ──────────────────────────────────
360
-
361
- describe("WechatAdapter contract", () => {
362
- it("conforms to PersonalDataAdapter spec", () => {
363
- const a = new WechatAdapter({ account: { uin: "test-uin" } });
364
- const r = assertAdapter(a);
365
- expect(r.ok).toBe(true);
366
- if (!r.ok) console.log(r.errors);
367
- });
368
-
369
- it("name/version/extractMode/capabilities", () => {
370
- const a = new WechatAdapter({ account: { uin: "test-uin" } });
371
- expect(a.name).toBe("wechat");
372
- expect(a.version).toBe(WECHAT_VERSION);
373
- expect(a.extractMode).toBe("device-pull");
374
- expect(a.capabilities).toContain("sync:sqlite");
375
- expect(a.capabilities).toContain("decrypt:sqlcipher-v1");
376
- expect(a.dataDisclosure.legalGate).toBe(true);
377
- });
378
-
379
- it("rejects missing account", () => {
380
- expect(() => new WechatAdapter()).toThrow();
381
- expect(() => new WechatAdapter({})).toThrow(/account/);
382
- expect(() => new WechatAdapter({ account: {} })).toThrow(/uin/);
383
- });
384
-
385
- it("authenticate fails without DB", async () => {
386
- const a = new WechatAdapter({
387
- account: { uin: "test-uin" },
388
- keyProvider: { getKey: async () => "fakekey" },
389
- });
390
- const r = await a.authenticate();
391
- expect(r.ok).toBe(false);
392
- expect(r.reason).toBe("DB_NOT_PULLED");
393
- });
394
-
395
- it("authenticate fails without keyProvider", async () => {
396
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "wechat-"));
397
- const dbPath = path.join(dir, "fake.db");
398
- fs.writeFileSync(dbPath, "fake");
399
- try {
400
- const a = new WechatAdapter({ account: { uin: "test-uin" }, dbPath });
401
- const r = await a.authenticate();
402
- expect(r.ok).toBe(false);
403
- expect(r.reason).toBe("NO_KEY_PROVIDER");
404
- } finally {
405
- fs.rmSync(dir, { recursive: true, force: true });
406
- }
407
- });
408
- });
409
-
410
- describe("WechatAdapter.sync with mocked DB reader", () => {
411
- it("yields contact + message raw events", async () => {
412
- const fakeMessages = [
413
- { msgId: 1, msgSvrId: 100, talker: "wxid_friend", content: "你好", type: 1, createTime: 1700000000000, isSend: 0 },
414
- { msgId: 2, msgSvrId: 101, talker: "wxid_friend", content: "再见", type: 1, createTime: 1700000001000, isSend: 1 },
415
- ];
416
- const fakeContacts = [
417
- { username: "wxid_friend", alias: "x", nickname: "好友", conRemark: "", type: 1 },
418
- ];
419
- const fakeChatrooms = [];
420
-
421
- let openCalled = false;
422
- const dbReaderFactory = (opts) => ({
423
- open: async () => { openCalled = true; return { profile: "wcdb-legacy", tables: 5 }; },
424
- isEnMicroMsg: () => true,
425
- listTables: () => ["message", "rcontact"],
426
- fetchContacts: () => fakeContacts,
427
- fetchChatrooms: () => fakeChatrooms,
428
- fetchMessages: () => fakeMessages,
429
- close: () => {},
430
- profile: () => "wcdb-legacy",
431
- });
432
-
433
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "wechat-"));
434
- const dbPath = path.join(dir, "EnMicroMsg.db");
435
- fs.writeFileSync(dbPath, "fake-db-bytes");
436
-
437
- try {
438
- const events = [];
439
- const a = new WechatAdapter({
440
- account: { uin: "self123" },
441
- dbPath,
442
- keyProvider: { getKey: async () => "fakekey" },
443
- dbReaderFactory,
444
- });
445
- const raws = [];
446
- for await (const r of a.sync({ onProgress: (e) => events.push(e.phase) })) raws.push(r);
447
- expect(openCalled).toBe(true);
448
- // 1 contact + 2 messages
449
- expect(raws).toHaveLength(3);
450
- expect(raws[0].payload.kind).toBe("contact");
451
- expect(raws[1].payload.kind).toBe("message");
452
- expect(events).toContain("opening");
453
- expect(events).toContain("opened");
454
- expect(events).toContain("done");
455
-
456
- // Now normalize each raw and verify they pass schema
457
- for (const raw of raws) {
458
- const batch = a.normalize(raw);
459
- const v = validateBatch(batch);
460
- expect(v.valid).toBe(true);
461
- }
462
- } finally {
463
- fs.rmSync(dir, { recursive: true, force: true });
464
- }
465
- });
466
-
467
- // sjqz parity audit follow-up — fetchContacts must exclude
468
- // @stranger and fake_* by default (vault pollution prevention).
469
- it("fetchContacts excludes @stranger and fake_* by default", async () => {
470
- // Pure DI smoke — capture the SQL passed to .prepare() to verify the
471
- // junk filter is in the query. We mock just enough of better-sqlite3's
472
- // shape: db.prepare(sql) → { all(limit) → rows }, exec, pragma.
473
- const seenSql = [];
474
- const fakeDriver = function Database(_path, _opts) {
475
- return {
476
- pragma: () => undefined,
477
- exec: () => undefined,
478
- prepare(sql) {
479
- seenSql.push(sql);
480
- return {
481
- all: () => {
482
- if (sql.startsWith("PRAGMA table_info")) {
483
- return [
484
- { name: "username" },
485
- { name: "alias" },
486
- { name: "nickname" },
487
- { name: "conRemark" },
488
- { name: "type" },
489
- ];
490
- }
491
- if (sql.startsWith("SELECT count")) return [{ n: 5 }];
492
- if (sql.includes("FROM rcontact")) return [];
493
- return [];
494
- },
495
- get: () => ({ n: 5 }),
496
- };
497
- },
498
- close: () => undefined,
499
- };
500
- };
501
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "wechat-junkfilt-"));
502
- const dbPath = path.join(dir, "EnMicroMsg.db");
503
- fs.writeFileSync(dbPath, "fake");
504
- try {
505
- const reader = new WeChatDBReader({
506
- dbPath,
507
- keyProvider: { getKey: async () => "0".repeat(64) },
508
- driver: fakeDriver,
509
- });
510
- await reader.open();
511
- reader.fetchContacts({ limit: 100 });
512
- const contactsSql = seenSql.find((s) => s.includes("FROM rcontact"));
513
- expect(contactsSql).toBeDefined();
514
- expect(contactsSql).toMatch(/NOT LIKE '%@stranger'/);
515
- expect(contactsSql).toMatch(/NOT LIKE 'fake_%'/);
516
- } finally {
517
- fs.rmSync(dir, { recursive: true, force: true });
518
- }
519
- });
520
-
521
- it("fetchContacts with includeJunk:true drops the filter (forensic mode)", async () => {
522
- const seenSql = [];
523
- const fakeDriver = function Database() {
524
- return {
525
- pragma: () => undefined,
526
- exec: () => undefined,
527
- prepare(sql) {
528
- seenSql.push(sql);
529
- return {
530
- all: () => {
531
- if (sql.startsWith("PRAGMA table_info")) {
532
- return ["username", "alias", "nickname", "conRemark", "type"].map((name) => ({ name }));
533
- }
534
- if (sql.startsWith("SELECT count")) return [{ n: 5 }];
535
- return [];
536
- },
537
- get: () => ({ n: 5 }),
538
- };
539
- },
540
- close: () => undefined,
541
- };
542
- };
543
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "wechat-incljunk-"));
544
- const dbPath = path.join(dir, "EnMicroMsg.db");
545
- fs.writeFileSync(dbPath, "fake");
546
- try {
547
- const reader = new WeChatDBReader({
548
- dbPath,
549
- keyProvider: { getKey: async () => "0".repeat(64) },
550
- driver: fakeDriver,
551
- });
552
- await reader.open();
553
- reader.fetchContacts({ limit: 100, includeJunk: true });
554
- const contactsSql = seenSql.find((s) => s.includes("FROM rcontact"));
555
- expect(contactsSql).toBeDefined();
556
- expect(contactsSql).not.toMatch(/NOT LIKE/);
557
- } finally {
558
- fs.rmSync(dir, { recursive: true, force: true });
559
- }
560
- });
561
-
562
- it("idle no-op when DB path missing", async () => {
563
- const a = new WechatAdapter({
564
- account: { uin: "self123" },
565
- keyProvider: { getKey: async () => "fakekey" },
566
- });
567
- const raws = [];
568
- for await (const r of a.sync()) raws.push(r);
569
- expect(raws).toHaveLength(0);
570
- });
571
-
572
- it("aborts gracefully when DB doesn't look like EnMicroMsg", async () => {
573
- const dbReaderFactory = () => ({
574
- open: async () => ({ profile: "wcdb-legacy", tables: 0 }),
575
- isEnMicroMsg: () => false,
576
- close: () => {},
577
- });
578
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "wechat-"));
579
- const dbPath = path.join(dir, "not-wechat.db");
580
- fs.writeFileSync(dbPath, "fake");
581
- try {
582
- const a = new WechatAdapter({
583
- account: { uin: "self123" },
584
- dbPath, keyProvider: { getKey: async () => "fakekey" },
585
- dbReaderFactory,
586
- });
587
- const raws = [];
588
- for await (const r of a.sync()) raws.push(r);
589
- expect(raws).toHaveLength(0);
590
- } finally {
591
- fs.rmSync(dir, { recursive: true, force: true });
592
- }
593
- });
594
- });