@chainlesschain/personal-data-hub 0.3.1 → 0.3.6

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 (60) hide show
  1. package/__tests__/adapters/email-adapter-snapshot.test.js +237 -0
  2. package/__tests__/adapters/email-adapter.test.js +1 -1
  3. package/__tests__/adapters/email-pdf-extractor.test.js +1 -1
  4. package/__tests__/adapters/email-retry-progress.test.js +1 -1
  5. package/__tests__/adapters/email-templates.test.js +1 -1
  6. package/__tests__/adapters/social-bilibili-adb-api-client.test.js +721 -0
  7. package/__tests__/adapters/social-bilibili-adb-chromium-cookies-reader.test.js +346 -0
  8. package/__tests__/adapters/social-bilibili-adb-collector.test.js +284 -0
  9. package/__tests__/adapters/social-bilibili-adb-cookies-extension.test.js +343 -0
  10. package/__tests__/adapters/social-bilibili-adb-snapshot-builder.test.js +296 -0
  11. package/__tests__/adapters/social-douyin-adb-collector.test.js +254 -0
  12. package/__tests__/adapters/social-douyin-adb-im-db-parser.test.js +304 -0
  13. package/__tests__/adapters/social-douyin-adb-snapshot-builder.test.js +216 -0
  14. package/__tests__/adapters/social-weibo-adb-api-client.test.js +362 -0
  15. package/__tests__/adapters/social-weibo-adb-collector.test.js +201 -0
  16. package/__tests__/adapters/social-weibo-adb-snapshot-builder.test.js +189 -0
  17. package/__tests__/adapters/social-xiaohongshu-adb-collector.test.js +207 -0
  18. package/__tests__/adapters/social-xiaohongshu-adb-sign.test.js +130 -0
  19. package/__tests__/adapters/system-data-android.test.js +32 -1
  20. package/__tests__/longtail-adapters.test.js +15 -2
  21. package/__tests__/shopping-adapters.test.js +96 -0
  22. package/__tests__/sign-providers.test.js +62 -0
  23. package/__tests__/travel-adapters.test.js +66 -0
  24. package/__tests__/whatsapp-adapter.test.js +5 -2
  25. package/lib/adapters/browser-history-chrome/chrome-db-reader.js +11 -1
  26. package/lib/adapters/email-imap/email-adapter.js +224 -17
  27. package/lib/adapters/messaging-telegram/index.js +15 -12
  28. package/lib/adapters/messaging-whatsapp/index.js +15 -12
  29. package/lib/adapters/shopping-taobao/index.js +161 -21
  30. package/lib/adapters/social-bilibili-adb/api-client.js +555 -0
  31. package/lib/adapters/social-bilibili-adb/chromium-cookies-reader.js +296 -0
  32. package/lib/adapters/social-bilibili-adb/collector.js +190 -0
  33. package/lib/adapters/social-bilibili-adb/cookies-extension.js +250 -0
  34. package/lib/adapters/social-bilibili-adb/index.js +51 -0
  35. package/lib/adapters/social-bilibili-adb/snapshot-builder.js +197 -0
  36. package/lib/adapters/social-douyin/index.js +4 -0
  37. package/lib/adapters/social-douyin-adb/collector.js +165 -0
  38. package/lib/adapters/social-douyin-adb/db-extension.js +281 -0
  39. package/lib/adapters/social-douyin-adb/im-db-parser.js +287 -0
  40. package/lib/adapters/social-douyin-adb/index.js +57 -0
  41. package/lib/adapters/social-douyin-adb/snapshot-builder.js +174 -0
  42. package/lib/adapters/social-weibo-adb/api-client.js +281 -0
  43. package/lib/adapters/social-weibo-adb/collector.js +169 -0
  44. package/lib/adapters/social-weibo-adb/cookies-extension.js +251 -0
  45. package/lib/adapters/social-weibo-adb/index.js +55 -0
  46. package/lib/adapters/social-weibo-adb/snapshot-builder.js +145 -0
  47. package/lib/adapters/social-xiaohongshu-adb/api-client.js +278 -0
  48. package/lib/adapters/social-xiaohongshu-adb/collector.js +158 -0
  49. package/lib/adapters/social-xiaohongshu-adb/cookies-extension.js +211 -0
  50. package/lib/adapters/social-xiaohongshu-adb/index.js +50 -0
  51. package/lib/adapters/social-xiaohongshu-adb/sign.js +90 -0
  52. package/lib/adapters/social-xiaohongshu-adb/snapshot-builder.js +126 -0
  53. package/lib/adapters/system-data-android/adapter.js +77 -3
  54. package/lib/adapters/travel-amap/index.js +16 -10
  55. package/lib/adapters/travel-ctrip/index.js +25 -9
  56. package/lib/adapters/vscode/vscode-reader.js +7 -1
  57. package/lib/sign-providers/index.js +20 -0
  58. package/lib/sign-providers/interface.js +82 -0
  59. package/lib/sign-providers/null-sign-provider.js +30 -0
  60. package/package.json +6 -1
@@ -0,0 +1,237 @@
1
+ "use strict";
2
+
3
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
4
+
5
+ const fs = require("node:fs");
6
+ const os = require("node:os");
7
+ const path = require("node:path");
8
+
9
+ const {
10
+ EmailAdapter,
11
+ } = require("../../lib/adapters/email-imap/email-adapter");
12
+ const { assertAdapter } = require("../../lib/adapter-spec");
13
+
14
+ /**
15
+ * Phase 5.8 — snapshot mode for Android EmailLocalCollector ingestion.
16
+ *
17
+ * EmailLocalCollector.kt (android-app) does the IMAP fetch on-device with
18
+ * Jakarta Mail, then writes filesDir/staging/email-<vendor>-<ts>.json with
19
+ * shape `{vendor, user, fetchedAt, records: [{messageNumber, subject, from,
20
+ * to, sentDateMs, bodyPreview, hasAttachments}]}`. The desktop EmailAdapter
21
+ * must consume that JSON via syncAdapter("email-imap", path) — without it
22
+ * the UI shows "v0.2 补齐 (邮件已成功抓 X 封到本机临时区)" misleading hint
23
+ * because the local fetch worked but cc couldn't ingest it.
24
+ *
25
+ * snapshotMode opt:
26
+ * - Relaxes opts.account.email + authCode constructor validation
27
+ * - Switches authenticate(ctx.inputPath) to file-readability check
28
+ * - Switches sync(opts.inputPath) to read JSON + emit raw events
29
+ * - Classifier + extractor still fire (text-only, no PDF since attachment
30
+ * buffers never crossed Android → desktop boundary)
31
+ */
32
+ describe("EmailAdapter snapshot mode", () => {
33
+ let tmpDir;
34
+ beforeEach(() => {
35
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "email-snap-"));
36
+ });
37
+ afterEach(() => {
38
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_e) {}
39
+ });
40
+
41
+ it("snapshotMode constructor accepts no opts.account", () => {
42
+ const a = new EmailAdapter({ snapshotMode: true });
43
+ expect(a.name).toBe("email-imap");
44
+ expect(a.capabilities).toContain("sync:snapshot");
45
+ expect(a.capabilities).not.toContain("sync:imap");
46
+ expect(a.capabilities).not.toContain("auth:authcode");
47
+ // Classifier + extractor capabilities preserved (snapshot still classifies)
48
+ expect(a.capabilities).toContain("classify:layer1-rules");
49
+ expect(a.capabilities).toContain("extract:6-templates");
50
+ });
51
+
52
+ it("snapshotMode adapter passes contract assertion", () => {
53
+ const a = new EmailAdapter({ snapshotMode: true });
54
+ const r = assertAdapter(a);
55
+ if (!r.ok) console.log("assertAdapter errors:", r.errors);
56
+ expect(r.ok).toBe(true);
57
+ });
58
+
59
+ it("authenticate(ctx.inputPath) returns ok when file readable", async () => {
60
+ const inputPath = path.join(tmpDir, "snap.json");
61
+ fs.writeFileSync(inputPath, "{}", "utf-8");
62
+ const a = new EmailAdapter({ snapshotMode: true });
63
+ const auth = await a.authenticate({ inputPath });
64
+ expect(auth.ok).toBe(true);
65
+ expect(auth.mode).toBe("snapshot-file");
66
+ });
67
+
68
+ it("authenticate without inputPath in snapshotMode returns NO_INPUT", async () => {
69
+ const a = new EmailAdapter({ snapshotMode: true });
70
+ const auth = await a.authenticate({});
71
+ expect(auth.ok).toBe(false);
72
+ expect(auth.reason).toBe("NO_INPUT");
73
+ });
74
+
75
+ it("authenticate with unreadable inputPath returns INPUT_PATH_UNREADABLE", async () => {
76
+ const a = new EmailAdapter({ snapshotMode: true });
77
+ const auth = await a.authenticate({ inputPath: path.join(tmpDir, "nope.json") });
78
+ expect(auth.ok).toBe(false);
79
+ expect(auth.reason).toBe("INPUT_PATH_UNREADABLE");
80
+ });
81
+
82
+ it("sync(inputPath) yields one raw event per record", async () => {
83
+ const inputPath = path.join(tmpDir, "snap.json");
84
+ fs.writeFileSync(inputPath, JSON.stringify({
85
+ vendor: "qq",
86
+ user: "user@qq.com",
87
+ fetchedAt: 1_700_000_000_000,
88
+ records: [
89
+ {
90
+ messageNumber: 1,
91
+ subject: "Test subject 1",
92
+ from: "Alice <alice@x.com>",
93
+ to: "user@qq.com",
94
+ sentDateMs: 1_700_000_100_000,
95
+ bodyPreview: "hello world",
96
+ hasAttachments: false,
97
+ },
98
+ {
99
+ messageNumber: 2,
100
+ subject: "Order confirmation",
101
+ from: "noreply@shop.com",
102
+ to: "user@qq.com",
103
+ sentDateMs: 1_700_000_200_000,
104
+ bodyPreview: "your order ABC123 has shipped",
105
+ hasAttachments: true,
106
+ },
107
+ ],
108
+ }), "utf-8");
109
+
110
+ const a = new EmailAdapter({ snapshotMode: true });
111
+ const raws = [];
112
+ for await (const r of a.sync({ inputPath })) raws.push(r);
113
+ expect(raws).toHaveLength(2);
114
+
115
+ expect(raws[0].adapter).toBe("email-imap");
116
+ expect(raws[0].originalId).toBe("android-snapshot:qq:user@qq.com:1");
117
+ expect(raws[0].capturedAt).toBe(1_700_000_100_000);
118
+ expect(raws[0].payload.subject).toBe("Test subject 1");
119
+ expect(raws[0].payload.from[0].address).toBe("alice@x.com");
120
+ expect(raws[0].payload.from[0].name).toBe("Alice");
121
+ expect(raws[0].payload.to[0].address).toBe("user@qq.com");
122
+ expect(raws[0].payload.folder).toBe("INBOX");
123
+ // Classification fires even on envelope-only data
124
+ expect(raws[0].payload.classification).toBeDefined();
125
+ expect(raws[0].payload.classification.category).toBeDefined();
126
+
127
+ expect(raws[1].originalId).toBe("android-snapshot:qq:user@qq.com:2");
128
+ expect(raws[1].payload.from[0].address).toBe("noreply@shop.com");
129
+ // hasAttachments=true → parsedBody.attachments has placeholder entry
130
+ expect(raws[1].payload.parsedBody.attachments).toHaveLength(1);
131
+ });
132
+
133
+ it("sync(inputPath) on empty records emits nothing", async () => {
134
+ const inputPath = path.join(tmpDir, "empty.json");
135
+ fs.writeFileSync(inputPath, JSON.stringify({
136
+ vendor: "163",
137
+ user: "u@163.com",
138
+ fetchedAt: Date.now(),
139
+ records: [],
140
+ }), "utf-8");
141
+
142
+ const a = new EmailAdapter({ snapshotMode: true });
143
+ const raws = [];
144
+ for await (const r of a.sync({ inputPath })) raws.push(r);
145
+ expect(raws).toHaveLength(0);
146
+ });
147
+
148
+ it("sync(inputPath) on malformed JSON throws clear error", async () => {
149
+ const inputPath = path.join(tmpDir, "bad.json");
150
+ fs.writeFileSync(inputPath, "{not json", "utf-8");
151
+
152
+ const a = new EmailAdapter({ snapshotMode: true });
153
+ let threw = null;
154
+ try {
155
+ for await (const _r of a.sync({ inputPath })) { /* drain */ }
156
+ } catch (err) {
157
+ threw = err;
158
+ }
159
+ expect(threw).toBeTruthy();
160
+ expect(threw.message).toMatch(/bad JSON/);
161
+ });
162
+
163
+ it("sync(inputPath) without records[] throws shape error", async () => {
164
+ const inputPath = path.join(tmpDir, "noshape.json");
165
+ fs.writeFileSync(inputPath, JSON.stringify({ vendor: "qq" }), "utf-8");
166
+
167
+ const a = new EmailAdapter({ snapshotMode: true });
168
+ let threw = null;
169
+ try {
170
+ for await (const _r of a.sync({ inputPath })) { /* drain */ }
171
+ } catch (err) {
172
+ threw = err;
173
+ }
174
+ expect(threw).toBeTruthy();
175
+ expect(threw.message).toMatch(/records/);
176
+ });
177
+
178
+ it("sync(opts.limit) respected on snapshot record iteration", async () => {
179
+ const inputPath = path.join(tmpDir, "many.json");
180
+ const records = [];
181
+ for (let i = 1; i <= 10; i += 1) {
182
+ records.push({
183
+ messageNumber: i,
184
+ subject: `msg ${i}`,
185
+ from: `s${i}@x.com`,
186
+ to: "u@q.com",
187
+ sentDateMs: 1_700_000_000_000 + i * 1000,
188
+ bodyPreview: `body ${i}`,
189
+ hasAttachments: false,
190
+ });
191
+ }
192
+ fs.writeFileSync(inputPath, JSON.stringify({
193
+ vendor: "qq",
194
+ user: "u@q.com",
195
+ fetchedAt: Date.now(),
196
+ records,
197
+ }), "utf-8");
198
+
199
+ const a = new EmailAdapter({ snapshotMode: true });
200
+ const raws = [];
201
+ for await (const r of a.sync({ inputPath, limit: 3 })) raws.push(r);
202
+ expect(raws).toHaveLength(3);
203
+ });
204
+
205
+ it("sync(inputPath) handles records with no sentDateMs (falls back to fetchedAt)", async () => {
206
+ const inputPath = path.join(tmpDir, "nodate.json");
207
+ fs.writeFileSync(inputPath, JSON.stringify({
208
+ vendor: "qq",
209
+ user: "u@q.com",
210
+ fetchedAt: 1_700_500_000_000,
211
+ records: [
212
+ {
213
+ messageNumber: 1,
214
+ subject: "no date",
215
+ from: "x@x.com",
216
+ to: "u@q.com",
217
+ // sentDateMs intentionally omitted
218
+ bodyPreview: "",
219
+ hasAttachments: false,
220
+ },
221
+ ],
222
+ }), "utf-8");
223
+
224
+ const a = new EmailAdapter({ snapshotMode: true });
225
+ const raws = [];
226
+ for await (const r of a.sync({ inputPath })) raws.push(r);
227
+ expect(raws).toHaveLength(1);
228
+ expect(raws[0].capturedAt).toBe(1_700_500_000_000);
229
+ });
230
+
231
+ it("non-snapshot mode still requires opts.account (preserves Phase 5.1 invariant)", () => {
232
+ expect(() => new EmailAdapter({})).toThrow(/account/);
233
+ expect(() => new EmailAdapter({ account: { email: "u@x.com" } })).toThrow(/authCode/);
234
+ // But snapshot mode bypasses both:
235
+ expect(() => new EmailAdapter({ snapshotMode: true })).not.toThrow();
236
+ });
237
+ });
@@ -100,7 +100,7 @@ describe("EmailAdapter contract", () => {
100
100
  sessionFactory: makeMockSession({}).factory,
101
101
  });
102
102
  expect(a.name).toBe("email-imap");
103
- expect(a.version).toBe("0.6.0"); // Phase 5.7retry + progress streaming
103
+ expect(a.version).toBe("0.7.0"); // Phase 5.8snapshot mode for Android in-APK IMAP fetch
104
104
  expect(a.capabilities).toContain("sync:imap");
105
105
  expect(a.capabilities).toContain("auth:authcode");
106
106
  expect(a.capabilities).toContain("parse:mime-body");
@@ -524,6 +524,6 @@ describe("EmailAdapter — Phase 5.5 PDF extraction integration", () => {
524
524
  account: { provider: "qq", email: "u@qq.com", authCode: "x" },
525
525
  sessionFactory: makeSession([]),
526
526
  });
527
- expect(a.version).toBe("0.6.0");
527
+ expect(a.version).toBe("0.7.0");
528
528
  });
529
529
  });
@@ -280,7 +280,7 @@ describe("EmailAdapter — Phase 5.7 surface advertising", () => {
280
280
  account: { provider: "qq", email: "u@qq.com", authCode: "x" },
281
281
  sessionFactory: makeFlakySession({}).factory,
282
282
  });
283
- expect(a.version).toBe("0.6.0");
283
+ expect(a.version).toBe("0.7.0");
284
284
  });
285
285
 
286
286
  it("capabilities advertise sync:retry-backoff + sync:progress-stream", () => {
@@ -694,6 +694,6 @@ describe("EmailAdapter — Phase 5.4 extraction integration", () => {
694
694
  account: { provider: "qq", email: "u@qq.com", authCode: "x" },
695
695
  sessionFactory: makeSession([]),
696
696
  });
697
- expect(a.version).toBe("0.6.0");
697
+ expect(a.version).toBe("0.7.0");
698
698
  });
699
699
  });