@chainlesschain/personal-data-hub 0.4.28 → 0.4.30

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 (204) hide show
  1. package/README.md +13 -5
  2. package/lib/adapters/social-douyin-adb/usage-profile-reader.js +253 -0
  3. package/lib/adapters/social-douyin-adb/watch-history-reader.js +104 -31
  4. package/lib/adapters/social-toutiao-adb/article-reader.js +202 -0
  5. package/lib/analysis-skills/overview.js +24 -4
  6. package/lib/analysis-skills/spending.js +63 -2
  7. package/lib/analysis-skills/timeline.js +11 -6
  8. package/lib/prompt-builder.js +15 -1
  9. package/lib/query-parser.js +38 -8
  10. package/package.json +4 -1
  11. package/__tests__/adapter-guide.test.js +0 -47
  12. package/__tests__/adapter-spec.test.js +0 -78
  13. package/__tests__/adapters/ai-chat-cookie-capture-spec.test.js +0 -211
  14. package/__tests__/adapters/ai-chat-health-checker.test.js +0 -262
  15. package/__tests__/adapters/ai-chat-history.test.js +0 -396
  16. package/__tests__/adapters/ai-chat-http-client.test.js +0 -242
  17. package/__tests__/adapters/ai-chat-vendors.test.js +0 -874
  18. package/__tests__/adapters/alipay-bill-adapter.test.js +0 -538
  19. package/__tests__/adapters/apple-health.test.js +0 -95
  20. package/__tests__/adapters/bank-family.test.js +0 -125
  21. package/__tests__/adapters/biz-tianyancha.test.js +0 -159
  22. package/__tests__/adapters/browser-history-chrome.test.js +0 -377
  23. package/__tests__/adapters/browser-history-edge.test.js +0 -159
  24. package/__tests__/adapters/car-mercedesme.test.js +0 -74
  25. package/__tests__/adapters/doc-baidu-netdisk.test.js +0 -102
  26. package/__tests__/adapters/doc-camscanner.test.js +0 -147
  27. package/__tests__/adapters/doc-platforms.test.js +0 -177
  28. package/__tests__/adapters/edu-huawei-learning-live.test.js +0 -198
  29. package/__tests__/adapters/edu-zuoyebang-live.test.js +0 -226
  30. package/__tests__/adapters/email-adapter-snapshot.test.js +0 -237
  31. package/__tests__/adapters/email-adapter.test.js +0 -742
  32. package/__tests__/adapters/email-classifier.test.js +0 -347
  33. package/__tests__/adapters/email-imap-session.test.js +0 -334
  34. package/__tests__/adapters/email-parser.test.js +0 -244
  35. package/__tests__/adapters/email-pdf-extractor.test.js +0 -529
  36. package/__tests__/adapters/email-providers.test.js +0 -84
  37. package/__tests__/adapters/email-retry-progress.test.js +0 -294
  38. package/__tests__/adapters/email-templates.test.js +0 -822
  39. package/__tests__/adapters/family-23-collectors-scaffold.test.js +0 -182
  40. package/__tests__/adapters/finance-alipay-live.test.js +0 -258
  41. package/__tests__/adapters/finance-dcep.test.js +0 -74
  42. package/__tests__/adapters/fitness-joyrun.test.js +0 -82
  43. package/__tests__/adapters/game-genshin-live.test.js +0 -238
  44. package/__tests__/adapters/game-genshin-scaffold.test.js +0 -108
  45. package/__tests__/adapters/game-honor-of-kings-live.test.js +0 -230
  46. package/__tests__/adapters/git-activity.test.js +0 -222
  47. package/__tests__/adapters/gov-12123.test.js +0 -103
  48. package/__tests__/adapters/gov-ixiamen.test.js +0 -150
  49. package/__tests__/adapters/gov-tax.test.js +0 -135
  50. package/__tests__/adapters/health-meiyou.test.js +0 -125
  51. package/__tests__/adapters/local-files.test.js +0 -264
  52. package/__tests__/adapters/local-im-pc.test.js +0 -154
  53. package/__tests__/adapters/messaging-whatsapp.test.js +0 -289
  54. package/__tests__/adapters/music-kugou.test.js +0 -187
  55. package/__tests__/adapters/music-qq.test.js +0 -112
  56. package/__tests__/adapters/netease-music-live.test.js +0 -244
  57. package/__tests__/adapters/netease-music.test.js +0 -74
  58. package/__tests__/adapters/pc-local-discovery.test.js +0 -141
  59. package/__tests__/adapters/qq-pc-direct-read.test.js +0 -227
  60. package/__tests__/adapters/reading-family.test.js +0 -108
  61. package/__tests__/adapters/recruit-boss.test.js +0 -180
  62. package/__tests__/adapters/shell-history.test.js +0 -180
  63. package/__tests__/adapters/shopping-base.test.js +0 -179
  64. package/__tests__/adapters/shopping-dianping.test.js +0 -239
  65. package/__tests__/adapters/social-bilibili-adb-api-client.test.js +0 -721
  66. package/__tests__/adapters/social-bilibili-adb-chromium-cookies-reader.test.js +0 -346
  67. package/__tests__/adapters/social-bilibili-adb-collector.test.js +0 -284
  68. package/__tests__/adapters/social-bilibili-adb-cookies-extension.test.js +0 -343
  69. package/__tests__/adapters/social-bilibili-adb-snapshot-builder.test.js +0 -296
  70. package/__tests__/adapters/social-csdn.test.js +0 -175
  71. package/__tests__/adapters/social-dongchedi.test.js +0 -165
  72. package/__tests__/adapters/social-douyin-adb-aweme-detail.test.js +0 -165
  73. package/__tests__/adapters/social-douyin-adb-collector.test.js +0 -254
  74. package/__tests__/adapters/social-douyin-adb-db-extension.test.js +0 -114
  75. package/__tests__/adapters/social-douyin-adb-im-db-parser.test.js +0 -304
  76. package/__tests__/adapters/social-douyin-adb-snapshot-builder.test.js +0 -216
  77. package/__tests__/adapters/social-douyin-adb-watch-history.test.js +0 -192
  78. package/__tests__/adapters/social-kuaishou-adb-api-client.test.js +0 -496
  79. package/__tests__/adapters/social-kuaishou-adb-collector.test.js +0 -276
  80. package/__tests__/adapters/social-kuaishou-adb-cookies-extension.test.js +0 -152
  81. package/__tests__/adapters/social-kuaishou-adb-snapshot-builder.test.js +0 -178
  82. package/__tests__/adapters/social-toutiao-adb-account-reader.test.js +0 -135
  83. package/__tests__/adapters/social-toutiao-adb-api-client.test.js +0 -626
  84. package/__tests__/adapters/social-toutiao-adb-collector.test.js +0 -378
  85. package/__tests__/adapters/social-toutiao-adb-cookies-extension.test.js +0 -193
  86. package/__tests__/adapters/social-toutiao-adb-snapshot-builder.test.js +0 -196
  87. package/__tests__/adapters/social-toutiao-kuaishou-scaffold.test.js +0 -311
  88. package/__tests__/adapters/social-weibo-adb-api-client.test.js +0 -362
  89. package/__tests__/adapters/social-weibo-adb-collector.test.js +0 -201
  90. package/__tests__/adapters/social-weibo-adb-cookies-extension.test.js +0 -167
  91. package/__tests__/adapters/social-weibo-adb-snapshot-builder.test.js +0 -189
  92. package/__tests__/adapters/social-xiaohongshu-adb-api-client.test.js +0 -431
  93. package/__tests__/adapters/social-xiaohongshu-adb-collector.test.js +0 -207
  94. package/__tests__/adapters/social-xiaohongshu-adb-cookies-extension.test.js +0 -0
  95. package/__tests__/adapters/social-xiaohongshu-adb-sign-provider-injection.test.js +0 -351
  96. package/__tests__/adapters/social-xiaohongshu-adb-sign.test.js +0 -130
  97. package/__tests__/adapters/social-xiaohongshu-adb-snapshot-builder.test.js +0 -200
  98. package/__tests__/adapters/social-zhihu.test.js +0 -246
  99. package/__tests__/adapters/system-data-adapter.test.js +0 -443
  100. package/__tests__/adapters/system-data-android-ingest.test.js +0 -144
  101. package/__tests__/adapters/system-data-android.test.js +0 -519
  102. package/__tests__/adapters/system-data-disclosure.test.js +0 -153
  103. package/__tests__/adapters/travel-12306.test.js +0 -512
  104. package/__tests__/adapters/travel-amap.test.js +0 -219
  105. package/__tests__/adapters/travel-baidu-map.test.js +0 -305
  106. package/__tests__/adapters/travel-base.test.js +0 -205
  107. package/__tests__/adapters/travel-ctrip.test.js +0 -377
  108. package/__tests__/adapters/travel-didi-consumer.test.js +0 -66
  109. package/__tests__/adapters/travel-didi.test.js +0 -204
  110. package/__tests__/adapters/travel-tencent-map.test.js +0 -207
  111. package/__tests__/adapters/travel-tongcheng.test.js +0 -289
  112. package/__tests__/adapters/video-platforms.test.js +0 -152
  113. package/__tests__/adapters/video-xigua.test.js +0 -106
  114. package/__tests__/adapters/vscode.test.js +0 -299
  115. package/__tests__/adapters/wechat-bootstrap.test.js +0 -240
  116. package/__tests__/adapters/wechat-env-probe.test.js +0 -162
  117. package/__tests__/adapters/wechat-frida-agent.test.js +0 -322
  118. package/__tests__/adapters/wechat-frida-integration.test.js +0 -149
  119. package/__tests__/adapters/wechat-frida-key-provider.test.js +0 -188
  120. package/__tests__/adapters/wechat-md5-key-provider.test.js +0 -101
  121. package/__tests__/adapters/wechat-pc-direct-read.test.js +0 -365
  122. package/__tests__/adapters/wechat-pc-group-topic.test.js +0 -63
  123. package/__tests__/adapters/wechat-pc-v4-sidecar.test.js +0 -72
  124. package/__tests__/adapters/weread.test.js +0 -123
  125. package/__tests__/adapters/wework-pc.test.js +0 -124
  126. package/__tests__/adapters/win-recent.test.js +0 -192
  127. package/__tests__/analysis-skills.test.js +0 -679
  128. package/__tests__/analysis.test.js +0 -1845
  129. package/__tests__/audio-ximalaya-snapshot.test.js +0 -279
  130. package/__tests__/batch.test.js +0 -133
  131. package/__tests__/bridges-cc-kg.test.js +0 -231
  132. package/__tests__/bridges-cc-llm.test.js +0 -191
  133. package/__tests__/bridges-cc-rag.test.js +0 -162
  134. package/__tests__/categories.test.js +0 -92
  135. package/__tests__/e2e/ai-chat-cross-source-journey.test.js +0 -213
  136. package/__tests__/e2e/full-user-journey.test.js +0 -188
  137. package/__tests__/e2e/local-data-adapters-cli.e2e.test.js +0 -146
  138. package/__tests__/entity-resolver-ingest-hook.test.js +0 -177
  139. package/__tests__/entity-resolver-stages.test.js +0 -411
  140. package/__tests__/entity-resolver-vault.test.js +0 -249
  141. package/__tests__/entity-resolver.test.js +0 -526
  142. package/__tests__/fitness-keep-snapshot.test.js +0 -224
  143. package/__tests__/fixtures/entity-resolver-200-mock.json +0 -96
  144. package/__tests__/ids.test.js +0 -45
  145. package/__tests__/integration/ai-chat-history-registry.test.js +0 -228
  146. package/__tests__/integration/aichat-wizard-end-to-end.test.js +0 -282
  147. package/__tests__/integration/cross-adapter-pipelines.test.js +0 -396
  148. package/__tests__/integration/local-data-adapters-pipeline.test.js +0 -373
  149. package/__tests__/integration/social-bilibili-pipeline.test.js +0 -261
  150. package/__tests__/integration/wechat-bootstrap-end-to-end.test.js +0 -390
  151. package/__tests__/key-providers.test.js +0 -126
  152. package/__tests__/kg-derive.test.js +0 -219
  153. package/__tests__/llm-client.test.js +0 -122
  154. package/__tests__/longtail-adapters.test.js +0 -281
  155. package/__tests__/messaging-qq-snapshot.test.js +0 -294
  156. package/__tests__/mobile-extractor-encrypted.test.js +0 -460
  157. package/__tests__/mobile-extractor.test.js +0 -288
  158. package/__tests__/mock-adapter.test.js +0 -93
  159. package/__tests__/prompt-builder.test.js +0 -249
  160. package/__tests__/query-parser.test.js +0 -302
  161. package/__tests__/rag-derive.test.js +0 -169
  162. package/__tests__/registry-readiness.test.js +0 -292
  163. package/__tests__/registry.test.js +0 -420
  164. package/__tests__/salvage-ingest.test.js +0 -97
  165. package/__tests__/schemas.test.js +0 -331
  166. package/__tests__/shopping-adapters.test.js +0 -392
  167. package/__tests__/shopping-eleme-snapshot.test.js +0 -454
  168. package/__tests__/shopping-pinduoduo-snapshot.test.js +0 -484
  169. package/__tests__/shopping-snapshot.test.js +0 -438
  170. package/__tests__/shopping-vipshop-snapshot.test.js +0 -425
  171. package/__tests__/shopping-xianyu-snapshot.test.js +0 -451
  172. package/__tests__/sidecar-contacts-cross-validate.test.js +0 -186
  173. package/__tests__/sidecar-supervisor.test.js +0 -128
  174. package/__tests__/sign-providers.test.js +0 -62
  175. package/__tests__/social-adapters.test.js +0 -280
  176. package/__tests__/social-bilibili-snapshot.test.js +0 -278
  177. package/__tests__/social-douban-snapshot.test.js +0 -351
  178. package/__tests__/social-douyin-im-direct-read.test.js +0 -377
  179. package/__tests__/social-douyin-salvage-collector.test.js +0 -98
  180. package/__tests__/social-douyin-salvage-mapper.test.js +0 -90
  181. package/__tests__/social-douyin-snapshot.test.js +0 -256
  182. package/__tests__/social-kuaishou-snapshot.test.js +0 -362
  183. package/__tests__/social-toutiao-snapshot.test.js +0 -366
  184. package/__tests__/social-weibo-snapshot.test.js +0 -234
  185. package/__tests__/social-weibo-sqlite-device.test.js +0 -174
  186. package/__tests__/social-xiaohongshu-snapshot.test.js +0 -232
  187. package/__tests__/sqlite-leaf-salvage.test.js +0 -97
  188. package/__tests__/travel-adapters.test.js +0 -483
  189. package/__tests__/travel-maps-snapshot.test.js +0 -426
  190. package/__tests__/vault-driver-error.test.js +0 -74
  191. package/__tests__/vault-search-helpers.test.js +0 -104
  192. package/__tests__/vault-search.test.js +0 -423
  193. package/__tests__/vault.test.js +0 -767
  194. package/__tests__/wechat-adapter.test.js +0 -594
  195. package/__tests__/whatsapp-adapter.test.js +0 -138
  196. package/scripts/_make-fixture-all.js +0 -126
  197. package/scripts/_make-fixture-contacts.js +0 -84
  198. package/scripts/evaluate-entity-resolver.js +0 -213
  199. package/scripts/run-native-tests-sandbox.sh +0 -55
  200. package/scripts/smoke-phase-5-5.js +0 -196
  201. package/scripts/smoke-phase-5-7.js +0 -181
  202. package/scripts/smoke-system-data-contacts.js +0 -309
  203. package/scripts/smoke-system-data.js +0 -312
  204. package/vitest.config.js +0 -88
@@ -39,6 +39,18 @@ class OverviewSkill extends AnalysisSkill {
39
39
  if (until != null) q.until = until;
40
40
  const events = this.vault.queryEvents(q) || [];
41
41
 
42
+ // Accurate, uncapped app/type/total counts via SQL GROUP BY. queryEvents
43
+ // hard-caps at 10k rows, so deriving byApp/byType/total from `events`
44
+ // silently undercounts any app whose data is older than the recent-10k
45
+ // window — e.g. on a vault where one chat app dominates recent events,
46
+ // social-douyin showed 10 instead of its true 232. facetCounts honors the
47
+ // same since/until. (Row-derived spend/contacts/monthly stay sample-based —
48
+ // they need actual rows.)
49
+ const facets =
50
+ typeof this.vault.facetCounts === "function"
51
+ ? this.vault.facetCounts({ since, until })
52
+ : null;
53
+
42
54
  const byApp = new Map();
43
55
  const byType = new Map();
44
56
  const byMonth = new Map();
@@ -81,7 +93,11 @@ class OverviewSkill extends AnalysisSkill {
81
93
  if (citations.length < 50) citations.push(e.id);
82
94
  }
83
95
 
84
- const byAppArr = [...byApp.entries()].map(([app, count]) => ({ app, count })).sort((a, b) => b.count - a.count);
96
+ const byAppArr = (
97
+ facets
98
+ ? Object.entries(facets.byAdapter).map(([app, count]) => ({ app, count }))
99
+ : [...byApp.entries()].map(([app, count]) => ({ app, count }))
100
+ ).sort((a, b) => b.count - a.count);
85
101
  const topContacts = [...contacts.entries()]
86
102
  .map(([personId, v]) => ({
87
103
  personId,
@@ -93,8 +109,8 @@ class OverviewSkill extends AnalysisSkill {
93
109
  .slice(0, topN);
94
110
 
95
111
  const summary = {
96
- totalEvents: events.length,
97
- appsActive: byApp.size,
112
+ totalEvents: facets ? facets.total : events.length,
113
+ appsActive: facets ? Object.keys(facets.byAdapter).length : byApp.size,
98
114
  period: { since: since || null, until: until || null },
99
115
  topAppName: byAppArr.length ? byAppArr[0].app : null,
100
116
  };
@@ -103,7 +119,11 @@ class OverviewSkill extends AnalysisSkill {
103
119
  skill: "analysis.overview",
104
120
  summary,
105
121
  byApp: byAppArr,
106
- byType: [...byType.entries()].map(([type, count]) => ({ type, count })).sort((a, b) => b.count - a.count),
122
+ byType: (
123
+ facets
124
+ ? Object.entries(facets.bySubtype).map(([type, count]) => ({ type, count }))
125
+ : [...byType.entries()].map(([type, count]) => ({ type, count }))
126
+ ).sort((a, b) => b.count - a.count),
107
127
  monthlyActivity: [...byMonth.entries()].map(([monthKey, count]) => ({ monthKey, count })).sort((a, b) => a.monthKey.localeCompare(b.monthKey)),
108
128
  topContacts,
109
129
  spending: {
@@ -30,6 +30,13 @@ const { AnalysisSkill } = require("./base");
30
30
 
31
31
  const SUPPORTED_DIMENSIONS = new Set(["merchant", "category", "counterparty", "month"]);
32
32
 
33
+ // Event subtypes that carry content.amount (shared by the row fetch + the
34
+ // accurate SQL-sum path). Phase 7 shopping adapters emit "order".
35
+ const PAYMENT_SUBTYPES = [
36
+ "payment", "transfer", "refund", "utility",
37
+ "redenvelope", "investment", "income", "order",
38
+ ];
39
+
33
40
  class SpendingSkill extends AnalysisSkill {
34
41
  constructor(opts) {
35
42
  super({ ...opts, name: "analysis.spending" });
@@ -48,6 +55,19 @@ class SpendingSkill extends AnalysisSkill {
48
55
  const filtered = this._applyFilters(events, options);
49
56
 
50
57
  const summary = this._summarize(filtered, since, until);
58
+ // The row fetch caps at 5000 events PER subtype — a heavy alipay/wechat-pay
59
+ // user with >5000 payments would have their TOTAL silently undercounted.
60
+ // When no row-only filter is active (merchant text / personId / direction),
61
+ // recompute the headline totals from the uncapped SQL SUM. Breakdown / trend
62
+ // / citations stay row-sampled (they need actual rows).
63
+ const accurate = this._accurateTotals({ since, until }, options);
64
+ if (accurate) {
65
+ summary.totalSpend = accurate.totalSpend;
66
+ summary.totalIncome = accurate.totalIncome;
67
+ summary.netFlow = accurate.netFlow;
68
+ summary.eventCount = accurate.eventCount;
69
+ if (accurate.currency) summary.currency = accurate.currency;
70
+ }
51
71
  const breakdown = this._breakdown(filtered, dimension, topN);
52
72
  const trend = this._monthlyTrend(filtered);
53
73
  const citations = filtered.slice(0, 50).map((e) => e.id);
@@ -72,8 +92,7 @@ class SpendingSkill extends AnalysisSkill {
72
92
  // Phase 7 shopping adapters emit subtype="order" — must include so
73
93
  // spending aggregates cover Taobao/JD/Meituan along with Alipay
74
94
  // (payment/transfer) + Email (refund) etc.
75
- const subtypes = ["payment", "transfer", "refund", "utility", "redenvelope", "investment", "income", "order"];
76
- for (const subtype of subtypes) {
95
+ for (const subtype of PAYMENT_SUBTYPES) {
77
96
  const q = { subtype, limit: 5000 };
78
97
  if (since != null) q.since = since;
79
98
  if (until != null) q.until = until;
@@ -88,6 +107,48 @@ class SpendingSkill extends AnalysisSkill {
88
107
  return events;
89
108
  }
90
109
 
110
+ /**
111
+ * Accurate (uncapped) headline totals via vault.sumEventAmount — used only
112
+ * when the query has no filter SQL can't express. merchantFilter (text match
113
+ * on title/counterparty) and personId (participant expansion) need rows, and
114
+ * a direction filter changes which total/count is meaningful, so any of them
115
+ * → return null and fall back to the row-sampled summary. Returns null when
116
+ * the vault lacks sumEventAmount (older vault → original behavior).
117
+ */
118
+ _accurateTotals({ since, until }, options) {
119
+ if (
120
+ (typeof options.merchantFilter === "string" && options.merchantFilter.length > 0) ||
121
+ (typeof options.personId === "string" && options.personId.length > 0) ||
122
+ options.direction === "out" ||
123
+ options.direction === "in"
124
+ ) {
125
+ return null;
126
+ }
127
+ if (typeof this.vault.sumEventAmount !== "function") return null;
128
+ let totalSpend = 0;
129
+ let totalIncome = 0;
130
+ let eventCount = 0;
131
+ let currency = null;
132
+ for (const subtype of PAYMENT_SUBTYPES) {
133
+ const q = { subtype };
134
+ if (since != null) q.since = since;
135
+ if (until != null) q.until = until;
136
+ const r = this.vault.sumEventAmount(q);
137
+ if (!r) continue;
138
+ totalSpend += (r.byDirection && r.byDirection.out) || 0;
139
+ totalIncome += (r.byDirection && r.byDirection.in) || 0;
140
+ eventCount += r.count || 0;
141
+ if (!currency && r.count > 0 && r.currency) currency = r.currency;
142
+ }
143
+ return {
144
+ totalSpend: Math.round(totalSpend * 100) / 100,
145
+ totalIncome: Math.round(totalIncome * 100) / 100,
146
+ netFlow: Math.round((totalIncome - totalSpend) * 100) / 100,
147
+ eventCount,
148
+ currency,
149
+ };
150
+ }
151
+
91
152
  _applyFilters(events, options) {
92
153
  let out = events;
93
154
  if (typeof options.merchantFilter === "string" && options.merchantFilter.length > 0) {
@@ -63,12 +63,17 @@ class TimelineSkill extends AnalysisSkill {
63
63
  }
64
64
 
65
65
  _fetchEvents({ since, until }, limit) {
66
- // Exclude inventory-snapshot events (installed-app roster + contact
67
- // roster from system-data-android). They carry a synthetic
68
- // collection-time occurredAt — tens of thousands of them cluster at one
69
- // recent timestamp and would otherwise crowd out real activity from this
70
- // chronological narrative. They remain in the vault for facet counts.
71
- const q = { limit, excludeExtraKinds: ["app-snapshot", "contact-snapshot"] };
66
+ // Exclude inventory-snapshot + aggregate-baseline events. The snapshots
67
+ // (installed-app / contact roster from system-data-android) carry a
68
+ // synthetic collection-time occurredAt — tens of thousands cluster at one
69
+ // recent timestamp and would crowd out real activity. `app-usage-profile`
70
+ // is a single rolling aggregate (e.g. douyin "24天/108h" baseline), not a
71
+ // discrete activity, so it doesn't belong in a chronological narrative.
72
+ // All remain in the vault for facet counts / overview.
73
+ const q = {
74
+ limit,
75
+ excludeExtraKinds: ["app-snapshot", "contact-snapshot", "app-usage-profile"],
76
+ };
72
77
  if (since != null) q.since = since;
73
78
  if (until != null) q.until = until;
74
79
  const events = this.vault.queryEvents(q) || [];
@@ -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;
@@ -127,8 +127,16 @@ function parseTimeWindow(text, now = Date.now()) {
127
127
  if (m) {
128
128
  const n = parseInt(m[1], 10);
129
129
  if (Number.isFinite(n) && n > 0) {
130
+ // Safe month subtraction. Naive `setMonth(getMonth()-n)` overflows on a
131
+ // month-end day into a shorter month (e.g. Mar 31 −1mo → "Feb 31" → Mar 3),
132
+ // which silently DROPS the whole previous month from the window. Pin to
133
+ // day 1 first, then clamp the day to the target month's length.
130
134
  const target = new Date(now);
135
+ const day = target.getDate();
136
+ target.setDate(1);
131
137
  target.setMonth(target.getMonth() - n);
138
+ const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
139
+ target.setDate(Math.min(day, lastDay));
132
140
  return { since: target.getTime(), until: now };
133
141
  }
134
142
  }
@@ -152,7 +160,10 @@ const SUBTYPE_KEYWORDS = [
152
160
  { subtype: "order", patterns: [/(订单|下单|买了|购买|下了几单|下了多少单|order)/i] },
153
161
  { subtype: "payment", patterns: [/(支付|付款|花了|花费|消费|开销|payment|spent|spend)/i] },
154
162
  { subtype: "transfer", patterns: [/(转账|转给|转钱|transfer)/i] },
155
- { subtype: "income", patterns: [/(收入|工资|进账|收到|income)/i] },
163
+ // NOTE: bare 收到 ("receive") is deliberately NOT here — you 收到 messages /
164
+ // packages / 红包 too, so it stole "收到多少消息" → income (income is checked
165
+ // before message). 收到转账 still classifies as transfer (checked earlier).
166
+ { subtype: "income", patterns: [/(收入|工资|进账|入账|income)/i] },
156
167
  { subtype: "message", patterns: [/(聊天|消息|聊了|对话|message|chat)/i] },
157
168
  { subtype: "post", patterns: [/(朋友圈|发了|动态|moment|post)/i] },
158
169
  { subtype: "visit", patterns: [/(去过|到过|visited|去了|来到)/i] },
@@ -201,20 +212,39 @@ function parseFilters(text) {
201
212
 
202
213
  // ─── Intent detection (sum / count / list / latest / ...) ────────────────
203
214
 
215
+ // Amount/money words — BOTH the spend side (花/消费/开销/spent/金额) and the
216
+ // income side (收入/进账/到账/赚/挣). A question carrying one of these plus a
217
+ // "多少/how much" wants a SUM (sumEventAmount), not a row list.
218
+ const AMOUNT_HINT =
219
+ /(花|花了|花费|消费|开销|spent|金额|多少钱|amount|收入|进账|到账|入账|赚|挣)/;
220
+ // Count quantifier: "多少X" or "几X" for a measure word. 钱 is deliberately
221
+ // EXCLUDED so "多少钱" routes to sum-amount, not count. Symmetric 多少/几 (the
222
+ // old pattern had 几条/几单 but not 多少条/多少单, and 多少部 but not 几部).
223
+ const COUNT_QUANTIFIER =
224
+ /(多少|几)(次|条|单|个|家|人|张|部|篇|集|本|件|笔|顿|杯)|how\s+many|count\s+of/i;
225
+ const HOW_MUCH = /(多少钱|多少|how\s+much)/i;
226
+
204
227
  function parseIntent(text) {
205
228
  if (typeof text !== "string") return "list";
206
229
  if (/(总共|共多少|加起来|sum|total|合计)/.test(text)) {
207
- // Distinguish amount vs count by presence of currency words.
208
- if (/(花|花了|花费|消费|开销|spent|金额|多少钱|amount)/.test(text)) return "sum-amount";
230
+ // Distinguish amount vs count by presence of amount words (incl. income,
231
+ // so "总共收入多少" is sum-amount, not count).
232
+ if (AMOUNT_HINT.test(text)) return "sum-amount";
209
233
  return "count";
210
234
  }
211
- // Count intents: 几次/条/单/个 / 多少个/家/人/张/部 / how many / count of
212
- // 2026-05-21: extended "几个 X" / "多少个 X" needed for "几个联系人"
213
- // and "几个 app" which prior pattern missed (returned "list" → LLM had no
214
- // hint to read authoritative TOTALS instead of the FACTS sample length).
215
- if (/(多少次|几次|几条|几单|几个|多少个|多少家|多少人|多少张|多少部|how\s+many|count\s+of)/i.test(text)) {
235
+ // Count: 多少X / 几X for a measure word ("多少条朋友圈" / "下了几单" /
236
+ // "几个联系人"). Runs BEFORE the bare-sum rule so "消费了多少次" → count.
237
+ if (COUNT_QUANTIFIER.test(text)) {
216
238
  return "count";
217
239
  }
240
+ // Spend/income question without an explicit 总共/合计 — "(这个月)花了多少钱" /
241
+ // "在淘宝花了多少" / "这个月收入多少" / "赚了多少". The amount word + a
242
+ // "多少/how much" ⇒ a TOTAL. Without this these common phrasings fell through
243
+ // to intent=list and the engine returned a row sample, not the authoritative
244
+ // sumEventAmount total.
245
+ if (AMOUNT_HINT.test(text) && HOW_MUCH.test(text)) {
246
+ return "sum-amount";
247
+ }
218
248
  if (/(最近|最新|latest|recent)/i.test(text)) return "latest";
219
249
  return "list";
220
250
  }
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@chainlesschain/personal-data-hub",
3
- "version": "0.4.28",
3
+ "version": "0.4.30",
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",
@@ -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
- });