@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
@@ -0,0 +1,190 @@
1
+ 'use strict';
2
+ /**
3
+ * qq-nt-collect — on-device-ready QQNT (nt_msg.db) decrypt + protobuf-parse.
4
+ *
5
+ * Pure Node (crypto + a caller-provided better-sqlite3 ctor) — NO frida, NO adb.
6
+ * This is the bundle-shipped core behind `cc hub collect-qq`, which the Android
7
+ * `CollectQqNativeTool` invokes after su-staging the encrypted DB + uid list. The
8
+ * exact same logic runs on PC (USB pull) and on-device (app su-read).
9
+ *
10
+ * Method (key DERIVED, no frida): `key = MD5(MD5(uid) + rand)` → SQLCipher
11
+ * PBKDF2-HMAC-SHA512/1 (iter 4000) → AES-256-CBC. `rand` is read from the 1024-byte
12
+ * plaintext header; `uid` (QQNT `u_...`) is brute-forced from candidates — page 1
13
+ * decrypting to a valid SQLite header identifies the self uid. (See
14
+ * scripts/android/pdh-qq-android-decrypt.mjs for the standalone PC tool this was
15
+ * extracted from; behaviour is byte-identical.)
16
+ */
17
+ const crypto = require('crypto');
18
+ const md5 = (s) => crypto.createHash('md5').update(s).digest('hex');
19
+
20
+ /** Read `rand` (protobuf field after "QQ_NT DB") from the plaintext header. */
21
+ function extractRand(raw) {
22
+ const head = raw.subarray(0, 256).toString('latin1');
23
+ const i = head.indexOf('QQ_NT DB');
24
+ if (i < 0) return null;
25
+ const m = head.slice(i + 8, i + 40).match(/[A-Za-z0-9]{6,12}/);
26
+ return m ? m[0] : null;
27
+ }
28
+
29
+ /** Detect HMAC algo named in the header (informational). */
30
+ function headerHmac(raw) {
31
+ return (/HMAC_SHA\d+/.exec(raw.subarray(0, 256).toString('latin1')) || [])[0] || null;
32
+ }
33
+
34
+ // decrypt one page slice. p1=true → page 1 (first 16 bytes = salt, skipped).
35
+ function decPage(page, encKey, reserve, ivOff, p1) {
36
+ const ro = page.length - reserve;
37
+ const ct = page.subarray(p1 ? 16 : 0, ro);
38
+ const iv = page.subarray(ro + ivOff, ro + ivOff + 16);
39
+ if (iv.length < 16) return null;
40
+ try {
41
+ const d = crypto.createDecipheriv('aes-256-cbc', encKey, iv);
42
+ d.setAutoPadding(false);
43
+ return Buffer.concat([d.update(ct), d.final()]);
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+ const validHdr = (pt) => pt && pt.length > 8 && pt[5] === 64 && pt[6] === 32 && pt[7] === 32;
49
+ const CFG = [[4096, 48, 0], [4096, 80, 0], [4096, 48, 20]];
50
+
51
+ /**
52
+ * Brute the key over uid candidates, then decrypt every page.
53
+ * @returns {{decrypted: Buffer, uid: string, kdf: string}|null} null = no uid matched.
54
+ */
55
+ function deriveAndDecrypt(raw, uids, rand) {
56
+ if (!raw || raw.length <= 1024 || !rand || !uids || !uids.length) return null;
57
+ const body = raw.subarray(1024);
58
+ const salt = body.subarray(0, 16);
59
+ let hit = null;
60
+ for (const uid of uids) {
61
+ const pass = md5(md5(uid) + rand);
62
+ for (const algo of ['sha512', 'sha1']) {
63
+ const ek = crypto.pbkdf2Sync(Buffer.from(pass, 'utf8'), salt, 4000, 32, algo);
64
+ for (const [pg, rs, iv] of CFG) {
65
+ if (validHdr(decPage(body.subarray(0, pg), ek, rs, iv, true))) {
66
+ hit = { uid, algo, page: pg, reserve: rs, ivOff: iv, ek };
67
+ break;
68
+ }
69
+ }
70
+ if (hit) break;
71
+ }
72
+ if (hit) break;
73
+ }
74
+ if (!hit) return null;
75
+ const { ek, page, reserve, ivOff } = hit;
76
+ const n = Math.floor(body.length / page);
77
+ const out = [];
78
+ for (let i = 0; i < n; i++) {
79
+ const pt = decPage(body.subarray(i * page, (i + 1) * page), ek, reserve, ivOff, i === 0);
80
+ const full = Buffer.alloc(page);
81
+ if (i === 0) {
82
+ Buffer.from('SQLite format 3\0').copy(full, 0);
83
+ if (pt) pt.copy(full, 16);
84
+ } else if (pt) {
85
+ pt.copy(full, 0);
86
+ }
87
+ out.push(full);
88
+ }
89
+ return { decrypted: Buffer.concat(out), uid: hit.uid, kdf: hit.algo };
90
+ }
91
+
92
+ // ── protobuf message-body → readable text (from pdh-qq-ingest.mjs) ──────────
93
+ function readVarint(buf, p) {
94
+ let shift = 0, r = 0n;
95
+ while (p < buf.length) {
96
+ const b = buf[p++];
97
+ r |= BigInt(b & 0x7f) << BigInt(shift);
98
+ if (!(b & 0x80)) break;
99
+ shift += 7;
100
+ }
101
+ return [r, p];
102
+ }
103
+ function* fields(buf) {
104
+ let p = 0;
105
+ while (p < buf.length) {
106
+ let tag;[tag, p] = readVarint(buf, p); tag = Number(tag);
107
+ const wire = tag & 7;
108
+ if (wire === 0) { let v;[v, p] = readVarint(buf, p); } else if (wire === 2) {
109
+ let len;[len, p] = readVarint(buf, p); len = Number(len);
110
+ const data = buf.subarray(p, p + len); p += len; yield data;
111
+ } else if (wire === 5) p += 4; else if (wire === 1) p += 8; else return;
112
+ }
113
+ }
114
+ const readable = (s) => s && /[一-鿿 -ヿA-Za-z0-9]/.test(s) && !/[�]/.test(s) &&
115
+ [...s].every((c) => c.charCodeAt(0) >= 32 || c === '\n');
116
+ function extractTexts(buf, depth, out) {
117
+ if (depth > 6 || !buf || buf.length === 0) return;
118
+ for (const data of fields(buf)) {
119
+ if (!data || data.length === 0) continue;
120
+ let s = null; try { s = data.toString('utf8'); } catch {}
121
+ if (s && Buffer.from(s, 'utf8').equals(data) && readable(s) && s.length <= 1000 &&
122
+ !/^https?:\/\/\S+$/.test(s)) out.push(s);
123
+ if (data.length >= 2) extractTexts(data, depth + 1, out);
124
+ }
125
+ }
126
+ function bodyText(blob) {
127
+ if (!Buffer.isBuffer(blob)) return null;
128
+ const out = []; extractTexts(blob, 0, out);
129
+ const cjk = out.filter((t) => /[一-鿿]/.test(t)).sort((a, b) => b.length - a.length);
130
+ return cjk[0] || out.sort((a, b) => b.length - a.length)[0] || null;
131
+ }
132
+
133
+ /**
134
+ * Parse a DECRYPTED nt_msg.db into vault events (qq-pc adapter shape).
135
+ * @param Database a better-sqlite3 constructor (injected by the caller/bundle)
136
+ * @param dbPath path to the decrypted nt_msg.db
137
+ * @param self the user's own QQ number (attribution fallback)
138
+ * @returns {Array} event objects ready for vault.putEvent
139
+ */
140
+ function parseEvents(Database, dbPath, self) {
141
+ const src = new Database(dbPath, { readonly: true });
142
+ const events = [];
143
+ const num = (v) => (typeof v === 'bigint' ? Number(v) : v);
144
+ const ingestTable = (table, isGroup) => {
145
+ let rows;
146
+ try {
147
+ rows = src.prepare(
148
+ `SELECT [40001] msgId,[40020] uid,[40011] type,[40033] sender,[40021] peer,` +
149
+ `[40050] t,[40800] body FROM ${table}`,
150
+ ).safeIntegers().all();
151
+ } catch { return; }
152
+ for (const r of rows) {
153
+ const type = num(r.type);
154
+ if (type === 5 || type === 9) continue; // grey-bar / system
155
+ const text = bodyText(r.body);
156
+ if (!text || !/[一-鿿A-Za-z0-9]/.test(text)) continue;
157
+ // QQ service/gray-tip config messages (e.g. com.tencent.* push configs) —
158
+ // not real chat; drop so the vault holds conversation, not service noise.
159
+ if (text.includes('com.tencent.') || text.includes('public desc')) continue;
160
+ const msgId = typeof r.msgId === 'bigint' ? r.msgId.toString() : String(r.msgId);
161
+ const sender = String(num(r.sender) || '');
162
+ const peer = String(num(r.peer) || '');
163
+ const occurredAt = num(r.t) * 1000;
164
+ if (!occurredAt) continue;
165
+ const actor = sender ? `person-qq-${sender}` : `person-qq-${self}`;
166
+ const participants = [actor];
167
+ participants.push(isGroup ? `group-qq-${peer}` : `person-qq-${peer}`);
168
+ events.push({
169
+ type: 'event', subtype: 'message', id: `qq:${table}:${msgId}`,
170
+ occurredAt, actor, participants,
171
+ content: { text: isGroup ? `[群${peer}] ${text}` : text },
172
+ topics: isGroup ? [`group-qq-${peer}`] : undefined,
173
+ source: {
174
+ adapter: 'qq-pc', adapterVersion: '0.1.0', originalId: `${table}:${msgId}`,
175
+ capturedAt: occurredAt, capturedBy: 'sqlite',
176
+ },
177
+ ingestedAt: Date.now(),
178
+ });
179
+ }
180
+ };
181
+ try {
182
+ ingestTable('c2c_msg_table', false);
183
+ ingestTable('group_msg_table', true);
184
+ } finally {
185
+ src.close();
186
+ }
187
+ return events;
188
+ }
189
+
190
+ module.exports = { extractRand, headerHmac, deriveAndDecrypt, bodyText, parseEvents };
@@ -48,11 +48,25 @@ const CROSS_APP_HEADER = "CROSS_APP_OVERVIEW (跨 app 汇聚画像 — 各 app
48
48
  * Trim an event down to the fields the LLM actually needs. Saves tokens +
49
49
  * reduces prompt injection surface (no raw `extra` blob).
50
50
  */
51
+ // Local-time "YYYY-MM-DD HH:mm" for the LLM. Passing the raw epoch-ms integer
52
+ // (e.g. 1781706182375) made the model unreliable on "when did I…" questions —
53
+ // it can't dependably convert epoch ms to a date. buildPrompt runs on the
54
+ // user's own machine (cc hub / desktop), so local getters are the user's TZ.
55
+ function fmtLocalDateTime(ms) {
56
+ const d = new Date(ms);
57
+ if (!Number.isFinite(d.getTime())) return null;
58
+ const p = (n) => String(n).padStart(2, "0");
59
+ return (
60
+ `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ` +
61
+ `${p(d.getHours())}:${p(d.getMinutes())}`
62
+ );
63
+ }
64
+
51
65
  function summarizeEvent(e) {
52
66
  const out = {
53
67
  id: e.id,
54
68
  type: e.subtype,
55
- at: e.occurredAt,
69
+ at: fmtLocalDateTime(e.occurredAt) || e.occurredAt,
56
70
  source: e.source && e.source.adapter,
57
71
  };
58
72
  if (e.actor) out.actor = e.actor;
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@chainlesschain/personal-data-hub",
3
- "version": "0.4.29",
3
+ "version": "0.4.31",
4
4
  "description": "Personal Data Hub — UnifiedSchema + validators + KG ingest helpers for the data-back-to-the-individual middleware",
5
5
  "type": "commonjs",
6
6
  "main": "lib/index.js",
7
+ "files": [
8
+ "lib/"
9
+ ],
7
10
  "exports": {
8
11
  ".": "./lib/index.js",
9
12
  "./constants": "./lib/constants.js",
@@ -70,7 +73,8 @@
70
73
  "./adapters/messaging-whatsapp": "./lib/adapters/messaging-whatsapp/index.js",
71
74
  "./sidecar": "./lib/sidecar/index.js",
72
75
  "./forensics/leaf-salvage": "./lib/forensics/leaf-salvage.js",
73
- "./forensics/salvage-ingest": "./lib/forensics/salvage-ingest.js"
76
+ "./forensics/salvage-ingest": "./lib/forensics/salvage-ingest.js",
77
+ "./forensics/qq-nt-collect": "./lib/forensics/qq-nt-collect.js"
74
78
  },
75
79
  "scripts": {
76
80
  "test": "vitest run",
@@ -101,7 +105,8 @@
101
105
  "optionalDependencies": {
102
106
  "imapflow": "^1.0.183",
103
107
  "adm-zip": "^0.5.16",
104
- "iconv-lite": "^0.6.3"
108
+ "iconv-lite": "^0.6.3",
109
+ "pdf-parse": "^1.1.1"
105
110
  },
106
111
  "devDependencies": {
107
112
  "vitest": "^4.1.5"
@@ -1,47 +0,0 @@
1
- "use strict";
2
-
3
- import { describe, it, expect } from "vitest";
4
-
5
- const { getAdapterGuide, ADAPTER_OVERRIDES } = require("../lib/adapter-guide");
6
-
7
- describe("adapter-guide", () => {
8
- it("wechat-pc guide reflects the 4.0 one-click reality (no manual PyWxDump as primary)", () => {
9
- const g = getAdapterGuide("wechat-pc", "device");
10
- // primary method is the automatic one-click, not manual decryption
11
- const primary = g.methods[0];
12
- expect(primary.recommended).toBe(true);
13
- expect(primary.label).toMatch(/一键|自动/);
14
- expect(primary.steps.join(" ")).toMatch(/一键采集|自动/);
15
- // summary mentions the full coverage we now capture
16
- expect(g.summary).toMatch(/公众号/);
17
- expect(g.summary).toMatch(/朋友圈/);
18
- expect(g.summary).toMatch(/收藏/);
19
- // manual 3.x path is still offered as a fallback
20
- expect(g.methods.some((m) => /3\.x|PyWxDump|手动/.test(m.label + m.steps.join(" ")))).toBe(true);
21
- });
22
-
23
- it("the 6 social platforms all have a tailored one-click ADB guide", () => {
24
- for (const name of [
25
- "social-bilibili",
26
- "social-weibo",
27
- "social-douyin",
28
- "social-xiaohongshu",
29
- "social-toutiao",
30
- "social-kuaishou",
31
- ]) {
32
- expect(ADAPTER_OVERRIDES[name]).toBeTruthy();
33
- const g = getAdapterGuide(name, "device");
34
- const primary = g.methods[0];
35
- expect(primary.recommended).toBe(true);
36
- // recommended path is root-phone + one-click, not "go log in on the web"
37
- expect(primary.label + primary.steps.join(" ")).toMatch(/一键|ADB|USB|root/i);
38
- }
39
- });
40
-
41
- it("unknown adapter falls back to a category guide without throwing", () => {
42
- const g = getAdapterGuide("totally-unknown", "snapshot");
43
- expect(g.category).toBe("snapshot");
44
- expect(Array.isArray(g.methods)).toBe(true);
45
- expect(g.methods.length).toBeGreaterThan(0);
46
- });
47
- });
@@ -1,78 +0,0 @@
1
- "use strict";
2
-
3
- import { describe, it, expect } from "vitest";
4
-
5
- const { assertAdapter, SENSITIVITY_LEVELS } = require("../lib/adapter-spec");
6
- const { MockAdapter } = require("../lib/mock-adapter");
7
-
8
- describe("assertAdapter", () => {
9
- it("accepts a fully-valid adapter (MockAdapter)", () => {
10
- const r = assertAdapter(new MockAdapter());
11
- expect(r.ok).toBe(true);
12
- });
13
-
14
- it("rejects non-object input", () => {
15
- expect(assertAdapter(null).ok).toBe(false);
16
- expect(assertAdapter(undefined).ok).toBe(false);
17
- expect(assertAdapter("oops").ok).toBe(false);
18
- });
19
-
20
- it("rejects missing required fields (collects all errors, no throw)", () => {
21
- const r = assertAdapter({});
22
- expect(r.ok).toBe(false);
23
- // Many fields missing — at least name + version + capabilities + dataDisclosure + methods.
24
- expect(r.errors.length).toBeGreaterThan(4);
25
- expect(r.errors.some((e) => e.includes("name"))).toBe(true);
26
- expect(r.errors.some((e) => e.includes("version"))).toBe(true);
27
- expect(r.errors.some((e) => e.includes("authenticate"))).toBe(true);
28
- expect(r.errors.some((e) => e.includes("sync"))).toBe(true);
29
- });
30
-
31
- it("rejects invalid sensitivity", () => {
32
- const a = new MockAdapter();
33
- a.dataDisclosure = { ...a.dataDisclosure, sensitivity: "extreme" };
34
- const r = assertAdapter(a);
35
- expect(r.ok).toBe(false);
36
- expect(r.errors.some((e) => e.includes("sensitivity"))).toBe(true);
37
- });
38
-
39
- it("rejects non-boolean legalGate", () => {
40
- const a = new MockAdapter();
41
- a.dataDisclosure = { ...a.dataDisclosure, legalGate: "yes" };
42
- const r = assertAdapter(a);
43
- expect(r.ok).toBe(false);
44
- });
45
-
46
- it("rejects non-array capabilities", () => {
47
- const a = new MockAdapter();
48
- a.capabilities = "sync";
49
- const r = assertAdapter(a);
50
- expect(r.ok).toBe(false);
51
- });
52
-
53
- it("rejects rateLimits with negative value", () => {
54
- const a = new MockAdapter();
55
- a.rateLimits = { perMinute: -1 };
56
- expect(assertAdapter(a).ok).toBe(false);
57
- });
58
-
59
- it("accepts adapter without rateLimits (optional field)", () => {
60
- const a = new MockAdapter();
61
- delete a.rateLimits;
62
- expect(assertAdapter(a).ok).toBe(true);
63
- });
64
-
65
- it("rejects non-function authenticate / sync / normalize / healthCheck", () => {
66
- const a = new MockAdapter();
67
- a.authenticate = "not a function";
68
- expect(assertAdapter(a).ok).toBe(false);
69
-
70
- const b = new MockAdapter();
71
- b.normalize = 42;
72
- expect(assertAdapter(b).ok).toBe(false);
73
- });
74
-
75
- it("SENSITIVITY_LEVELS lists low/medium/high", () => {
76
- expect(SENSITIVITY_LEVELS).toEqual(["low", "medium", "high"]);
77
- });
78
- });
@@ -1,211 +0,0 @@
1
- "use strict";
2
-
3
- import { describe, it, expect } from "vitest";
4
-
5
- const {
6
- COOKIE_SPEC_VERSION,
7
- KNOWN_VENDORS,
8
- COOKIE_CAPTURE_SPECS,
9
- getSpec,
10
- listVendors,
11
- classifyProbedCookies,
12
- validateCookieCaptureSpec,
13
- _internal,
14
- } = require("../../lib/adapters/ai-chat-history/cookie-capture-spec");
15
-
16
- describe("cookie-capture-spec — Phase 10.3.1 matrix", () => {
17
- it("exposes a positive integer COOKIE_SPEC_VERSION", () => {
18
- expect(Number.isInteger(COOKIE_SPEC_VERSION)).toBe(true);
19
- expect(COOKIE_SPEC_VERSION).toBeGreaterThanOrEqual(1);
20
- });
21
-
22
- it("KNOWN_VENDORS contains exactly the 9 wired vendors", () => {
23
- expect(KNOWN_VENDORS).toEqual([
24
- "deepseek",
25
- "kimi",
26
- "tongyi",
27
- "zhipu",
28
- "hunyuan",
29
- "qianfan",
30
- "coze",
31
- "dreamina",
32
- "doubao",
33
- ]);
34
- // Defensive — frozen so contributors don't accidentally mutate at runtime.
35
- expect(Object.isFrozen(KNOWN_VENDORS)).toBe(true);
36
- });
37
-
38
- it("ships 9 specs, one per KNOWN_VENDORS entry, no duplicates", () => {
39
- expect(COOKIE_CAPTURE_SPECS.length).toBe(KNOWN_VENDORS.length);
40
- const seen = new Set();
41
- for (const s of COOKIE_CAPTURE_SPECS) {
42
- expect(KNOWN_VENDORS).toContain(s.vendor);
43
- expect(seen.has(s.vendor)).toBe(false);
44
- seen.add(s.vendor);
45
- }
46
- });
47
-
48
- it("validates the shipped spec set without errors", () => {
49
- const r = validateCookieCaptureSpec(undefined, { throwOnError: false });
50
- expect(r.errors).toEqual([]);
51
- expect(r.ok).toBe(true);
52
- });
53
-
54
- it("each loginUrl host matches at least one cookieDomains entry", () => {
55
- for (const s of COOKIE_CAPTURE_SPECS) {
56
- const host = new URL(s.loginUrl).host;
57
- const matched = s.cookieDomains.some((d) =>
58
- d.startsWith(".") ? host.endsWith(d.slice(1)) : host === d,
59
- );
60
- expect({ vendor: s.vendor, host, matched }).toEqual({ vendor: s.vendor, host, matched: true });
61
- }
62
- });
63
-
64
- it("each spec has non-empty requiredCookies + postLoginPathHints + positive maxAge", () => {
65
- for (const s of COOKIE_CAPTURE_SPECS) {
66
- expect(Array.isArray(s.requiredCookies)).toBe(true);
67
- expect(s.requiredCookies.length).toBeGreaterThan(0);
68
- expect(Array.isArray(s.postLoginPathHints)).toBe(true);
69
- expect(s.postLoginPathHints.length).toBeGreaterThan(0);
70
- expect(Number.isInteger(s.cookieMaxAgeHintDays)).toBe(true);
71
- expect(s.cookieMaxAgeHintDays).toBeGreaterThan(0);
72
- expect(typeof s.notes).toBe("string");
73
- expect(s.notes.length).toBeGreaterThan(0);
74
- }
75
- });
76
-
77
- it("getSpec returns the right spec for a known vendor and null for unknown", () => {
78
- const ds = getSpec("deepseek");
79
- expect(ds).toBeTruthy();
80
- expect(ds.vendor).toBe("deepseek");
81
- expect(getSpec("notarealvendor")).toBeNull();
82
- expect(getSpec("")).toBeNull();
83
- expect(getSpec(undefined)).toBeNull();
84
- expect(getSpec(null)).toBeNull();
85
- });
86
-
87
- it("listVendors returns a copy (mutation does not affect KNOWN_VENDORS)", () => {
88
- const arr = listVendors();
89
- expect(arr).toEqual([...KNOWN_VENDORS]);
90
- arr.push("hacked");
91
- expect(KNOWN_VENDORS.includes("hacked")).toBe(false);
92
- });
93
- });
94
-
95
- describe("classifyProbedCookies — required vs optional vs missing", () => {
96
- it("returns ok=true when all required cookies present (object input)", () => {
97
- const r = classifyProbedCookies("deepseek", {
98
- userToken: "abc",
99
- "intercom-session-deepseek": "xyz",
100
- });
101
- expect(r.ok).toBe(true);
102
- expect(r.foundRequired).toEqual(["userToken"]);
103
- expect(r.missingRequired).toEqual([]);
104
- expect(r.foundOptional).toEqual(["intercom-session-deepseek"]);
105
- });
106
-
107
- it("returns ok=false when a required cookie is missing", () => {
108
- const r = classifyProbedCookies("kimi", { refresh_token: "rt", session_id: "sid" });
109
- expect(r.ok).toBe(false);
110
- expect(r.missingRequired).toEqual(["access_token"]);
111
- expect(r.foundOptional.sort()).toEqual(["refresh_token", "session_id"]);
112
- });
113
-
114
- it("accepts Electron Cookie[] shape (array of { name, value })", () => {
115
- const r = classifyProbedCookies("zhipu", [
116
- { name: "chatglm_token", value: "tok" },
117
- { name: "cgsessionid", value: "sid" },
118
- { name: "unrelated", value: "x" },
119
- ]);
120
- expect(r.ok).toBe(true);
121
- expect(r.foundRequired).toEqual(["chatglm_token"]);
122
- expect(r.foundOptional).toContain("cgsessionid");
123
- });
124
-
125
- it("accepts raw 'k=v; k=v' string (web-shell paste fallback)", () => {
126
- const raw = "sessionid=abc; sid_guard=xyz; passport_csrf_token=csrf; ;junk";
127
- const r = classifyProbedCookies("doubao", raw);
128
- expect(r.ok).toBe(true);
129
- expect(r.foundRequired).toEqual(["sessionid"]);
130
- expect(r.foundOptional.sort()).toEqual(["passport_csrf_token", "sid_guard"]);
131
- });
132
-
133
- it("string parser tolerates values containing '=' (e.g. base64)", () => {
134
- // sessionid is base64 with '=' padding; only the FIRST '=' is the delimiter.
135
- const raw = "sessionid=YWJjZGVmZ2g=; sid_guard=v1=";
136
- const r = classifyProbedCookies("coze", raw);
137
- expect(r.ok).toBe(true);
138
- expect(r.foundRequired).toEqual(["sessionid"]);
139
- // raw cookie value must be preserved verbatim including the trailing '='
140
- const jar = _internal._normalizeCookieJar(raw);
141
- expect(jar.sessionid).toBe("YWJjZGVmZ2g=");
142
- });
143
-
144
- it("empty string / null / undefined / wrong type all produce ok=false", () => {
145
- for (const input of ["", null, undefined, 42, true]) {
146
- const r = classifyProbedCookies("doubao", input);
147
- expect(r.ok).toBe(false);
148
- expect(r.missingRequired).toEqual(["sessionid"]);
149
- }
150
- });
151
-
152
- it("returns UNKNOWN_VENDOR reason for an unregistered vendor name", () => {
153
- const r = classifyProbedCookies("notarealvendor", { anything: "x" });
154
- expect(r.ok).toBe(false);
155
- expect(r.reason).toBe("UNKNOWN_VENDOR");
156
- });
157
-
158
- it("treats empty-string cookie value as missing (not present)", () => {
159
- const r = classifyProbedCookies("deepseek", { userToken: "" });
160
- expect(r.ok).toBe(false);
161
- expect(r.foundRequired).toEqual([]);
162
- expect(r.missingRequired).toEqual(["userToken"]);
163
- });
164
- });
165
-
166
- describe("validateCookieCaptureSpec — defensive guard catches malformed specs", () => {
167
- it("flags unknown vendor", () => {
168
- const { ok, errors } = validateCookieCaptureSpec(
169
- [{ ...COOKIE_CAPTURE_SPECS[0], vendor: "ghostvendor" }],
170
- { throwOnError: false },
171
- );
172
- expect(ok).toBe(false);
173
- expect(errors.join(" ")).toMatch(/unknown vendor/);
174
- });
175
-
176
- it("flags loginUrl host not matching any cookieDomain", () => {
177
- const broken = {
178
- ...COOKIE_CAPTURE_SPECS[0],
179
- loginUrl: "https://malicious.example.com/",
180
- };
181
- const { ok, errors } = validateCookieCaptureSpec([broken], { throwOnError: false });
182
- expect(ok).toBe(false);
183
- expect(errors.join(" ")).toMatch(/does not match any cookieDomain/);
184
- });
185
-
186
- it("flags empty requiredCookies / postLoginPathHints / invalid maxAge", () => {
187
- const broken = {
188
- ...COOKIE_CAPTURE_SPECS[0],
189
- requiredCookies: [],
190
- postLoginPathHints: [],
191
- cookieMaxAgeHintDays: 0,
192
- };
193
- const { ok, errors } = validateCookieCaptureSpec([broken], { throwOnError: false });
194
- expect(ok).toBe(false);
195
- const joined = errors.join(" ");
196
- expect(joined).toMatch(/requiredCookies/);
197
- expect(joined).toMatch(/postLoginPathHints/);
198
- expect(joined).toMatch(/cookieMaxAgeHintDays/);
199
- });
200
-
201
- it("flags duplicate vendor entries", () => {
202
- const dup = [COOKIE_CAPTURE_SPECS[0], { ...COOKIE_CAPTURE_SPECS[0] }];
203
- const { ok, errors } = validateCookieCaptureSpec(dup, { throwOnError: false });
204
- expect(ok).toBe(false);
205
- expect(errors.join(" ")).toMatch(/duplicate vendor/);
206
- });
207
-
208
- it("throws by default when malformed (no opts)", () => {
209
- expect(() => validateCookieCaptureSpec([{ vendor: "ghost" }])).toThrow(/Invalid cookie capture spec/);
210
- });
211
- });