@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.
- package/__tests__/adapters/email-adapter-snapshot.test.js +237 -0
- package/__tests__/adapters/email-adapter.test.js +1 -1
- package/__tests__/adapters/email-pdf-extractor.test.js +1 -1
- package/__tests__/adapters/email-retry-progress.test.js +1 -1
- package/__tests__/adapters/email-templates.test.js +1 -1
- package/__tests__/adapters/social-bilibili-adb-api-client.test.js +721 -0
- package/__tests__/adapters/social-bilibili-adb-chromium-cookies-reader.test.js +346 -0
- package/__tests__/adapters/social-bilibili-adb-collector.test.js +284 -0
- package/__tests__/adapters/social-bilibili-adb-cookies-extension.test.js +343 -0
- package/__tests__/adapters/social-bilibili-adb-snapshot-builder.test.js +296 -0
- package/__tests__/adapters/social-douyin-adb-collector.test.js +254 -0
- package/__tests__/adapters/social-douyin-adb-im-db-parser.test.js +304 -0
- package/__tests__/adapters/social-douyin-adb-snapshot-builder.test.js +216 -0
- package/__tests__/adapters/social-weibo-adb-api-client.test.js +362 -0
- package/__tests__/adapters/social-weibo-adb-collector.test.js +201 -0
- package/__tests__/adapters/social-weibo-adb-snapshot-builder.test.js +189 -0
- package/__tests__/adapters/social-xiaohongshu-adb-collector.test.js +207 -0
- package/__tests__/adapters/social-xiaohongshu-adb-sign.test.js +130 -0
- package/__tests__/adapters/system-data-android.test.js +32 -1
- package/__tests__/longtail-adapters.test.js +15 -2
- package/__tests__/shopping-adapters.test.js +96 -0
- package/__tests__/sign-providers.test.js +62 -0
- package/__tests__/travel-adapters.test.js +66 -0
- package/__tests__/whatsapp-adapter.test.js +5 -2
- package/lib/adapters/browser-history-chrome/chrome-db-reader.js +11 -1
- package/lib/adapters/email-imap/email-adapter.js +224 -17
- package/lib/adapters/messaging-telegram/index.js +15 -12
- package/lib/adapters/messaging-whatsapp/index.js +15 -12
- package/lib/adapters/shopping-taobao/index.js +161 -21
- package/lib/adapters/social-bilibili-adb/api-client.js +555 -0
- package/lib/adapters/social-bilibili-adb/chromium-cookies-reader.js +296 -0
- package/lib/adapters/social-bilibili-adb/collector.js +190 -0
- package/lib/adapters/social-bilibili-adb/cookies-extension.js +250 -0
- package/lib/adapters/social-bilibili-adb/index.js +51 -0
- package/lib/adapters/social-bilibili-adb/snapshot-builder.js +197 -0
- package/lib/adapters/social-douyin/index.js +4 -0
- package/lib/adapters/social-douyin-adb/collector.js +165 -0
- package/lib/adapters/social-douyin-adb/db-extension.js +281 -0
- package/lib/adapters/social-douyin-adb/im-db-parser.js +287 -0
- package/lib/adapters/social-douyin-adb/index.js +57 -0
- package/lib/adapters/social-douyin-adb/snapshot-builder.js +174 -0
- package/lib/adapters/social-weibo-adb/api-client.js +281 -0
- package/lib/adapters/social-weibo-adb/collector.js +169 -0
- package/lib/adapters/social-weibo-adb/cookies-extension.js +251 -0
- package/lib/adapters/social-weibo-adb/index.js +55 -0
- package/lib/adapters/social-weibo-adb/snapshot-builder.js +145 -0
- package/lib/adapters/social-xiaohongshu-adb/api-client.js +278 -0
- package/lib/adapters/social-xiaohongshu-adb/collector.js +158 -0
- package/lib/adapters/social-xiaohongshu-adb/cookies-extension.js +211 -0
- package/lib/adapters/social-xiaohongshu-adb/index.js +50 -0
- package/lib/adapters/social-xiaohongshu-adb/sign.js +90 -0
- package/lib/adapters/social-xiaohongshu-adb/snapshot-builder.js +126 -0
- package/lib/adapters/system-data-android/adapter.js +77 -3
- package/lib/adapters/travel-amap/index.js +16 -10
- package/lib/adapters/travel-ctrip/index.js +25 -9
- package/lib/adapters/vscode/vscode-reader.js +7 -1
- package/lib/sign-providers/index.js +20 -0
- package/lib/sign-providers/interface.js +82 -0
- package/lib/sign-providers/null-sign-provider.js +30 -0
- 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.
|
|
103
|
+
expect(a.version).toBe("0.7.0"); // Phase 5.8 — snapshot 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.
|
|
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.
|
|
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.
|
|
697
|
+
expect(a.version).toBe("0.7.0");
|
|
698
698
|
});
|
|
699
699
|
});
|