@chainlesschain/personal-data-hub 0.3.0 → 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 (61) 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 +163 -5
  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-12306/index.js +215 -29
  55. package/lib/adapters/travel-amap/index.js +16 -10
  56. package/lib/adapters/travel-ctrip/index.js +25 -9
  57. package/lib/adapters/vscode/vscode-reader.js +7 -1
  58. package/lib/sign-providers/index.js +20 -0
  59. package/lib/sign-providers/interface.js +82 -0
  60. package/lib/sign-providers/null-sign-provider.js +30 -0
  61. package/package.json +6 -1
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Phase 3a (Weibo C 路径 — 2026-05-25): weibo.cookies ADB extension factory.
5
+ *
6
+ * Mirror of `social-bilibili-adb/cookies-extension.js` (P1a). Pipeline:
7
+ *
8
+ * 1. ADB-pull /data/data/com.sina.weibo/app_webview/Default/Cookies
9
+ * via `su -c "base64 ..."` streaming (avoids MIUI FUSE SELinux trap)
10
+ * 2. Parse the chromium-shape sqlite via the shared
11
+ * chromium-cookies-reader (Phase 1a generic module)
12
+ * 3. Filter to host_key match `m.weibo.cn` (Weibo's actual API host;
13
+ * `weibo.com` chromium cookies exist on desktop but not on the
14
+ * mobile App where chromium-cookies lives)
15
+ * 4. Validate at minimum SUB cookie present (the session cookie —
16
+ * without it /api/config returns "not logged in")
17
+ * 5. Assemble Cookie header from all m.weibo.cn cookies (Weibo's API
18
+ * doesn't enforce a strict required-cookie list like Bilibili's
19
+ * 5-cookie requirement; pass everything through and let the server
20
+ * pick what it needs)
21
+ *
22
+ * Returns:
23
+ * {
24
+ * cookie: string, // full Cookie header
25
+ * extractedAt: number,
26
+ * diagnostic: {
27
+ * cookieCount: number,
28
+ * hadEncrypted: boolean,
29
+ * hasSub: boolean,
30
+ * cookieNames: string[],
31
+ * }
32
+ * }
33
+ *
34
+ * Failure modes (all throw, UI maps the typed reason to a banner):
35
+ * - WEIBO_NOT_INSTALLED — package not on device
36
+ * - WEIBO_NO_ROOT — su not available
37
+ * - WEIBO_COOKIES_EMPTY — base64 stream returned 0 bytes
38
+ * - WEIBO_COOKIES_TRUNCATED — decoded file too small
39
+ * - WEIBO_NOT_SQLITE — magic header check failed
40
+ * - WEIBO_COOKIES_INCOMPLETE — SUB cookie missing (user logged out
41
+ * on the Weibo App or app uses a non-standard storage path)
42
+ */
43
+
44
+ const fs = require("node:fs");
45
+ const path = require("node:path");
46
+ const os = require("node:os");
47
+ const crypto = require("node:crypto");
48
+
49
+ const {
50
+ readChromiumCookies,
51
+ } = require("../social-bilibili-adb/chromium-cookies-reader");
52
+
53
+ const WEIBO_COOKIES_REMOTE_PATH =
54
+ "/data/data/com.sina.weibo/app_webview/Default/Cookies";
55
+
56
+ const WEIBO_COOKIE_HOST_DOMAIN = "m.weibo.cn";
57
+
58
+ /** Minimum required cookie name — without SUB, /api/config returns login=false. */
59
+ const WEIBO_REQUIRED_COOKIE = "SUB";
60
+
61
+ async function pullCookiesViaSu(adb, serial, opts) {
62
+ const adbOpts = { serial, timeoutMs: opts?.timeoutMs || 60_000 };
63
+ const lsOut = await adb(
64
+ [
65
+ "shell",
66
+ "su",
67
+ "-c",
68
+ `ls ${WEIBO_COOKIES_REMOTE_PATH} 2>/dev/null || echo NOT_FOUND`,
69
+ ],
70
+ adbOpts,
71
+ );
72
+ const lsLine = lsOut.replace(/\r+$/gm, "").trim();
73
+ if (lsLine === "NOT_FOUND" || lsLine === "") {
74
+ throw new Error(
75
+ "WEIBO_NOT_INSTALLED: " +
76
+ WEIBO_COOKIES_REMOTE_PATH +
77
+ " not found. Install Weibo App + log in once on the phone, then retry. (Some Weibo App versions store cookies in a non-default WebView profile dir; if Weibo is installed but the path is missing, file a bug to track the actual path.)",
78
+ );
79
+ }
80
+ // Probe root.
81
+ const idOut = await adb(["shell", "su", "-c", "id -u"], adbOpts);
82
+ const idLine = idOut.replace(/\r+$/gm, "").trim();
83
+ if (idLine !== "0" && !idLine.includes("uid=0")) {
84
+ throw new Error(
85
+ "WEIBO_NO_ROOT: this phone isn't rooted (su returned `" +
86
+ idLine.substring(0, 60) +
87
+ "`). Weibo release APK isn't debuggable, so root is required to read its Cookies DB.",
88
+ );
89
+ }
90
+ // Stream base64 (avoids MIUI FUSE label remap trap).
91
+ const b64 = await adb(
92
+ [
93
+ "shell",
94
+ "su",
95
+ "-c",
96
+ `base64 ${WEIBO_COOKIES_REMOTE_PATH} | tr -d '\\n\\r'`,
97
+ ],
98
+ { ...adbOpts, timeoutMs: opts?.timeoutMs || 60_000 },
99
+ );
100
+ const b64Clean = b64.replace(/[\r\n\t ]+/g, "");
101
+ if (b64Clean.length === 0) {
102
+ throw new Error(
103
+ "WEIBO_COOKIES_EMPTY: base64 stream returned 0 bytes (su exec may have silently failed on MIUI / OEM ROM, retry or check `adb logcat`)",
104
+ );
105
+ }
106
+ let buf;
107
+ try {
108
+ buf = Buffer.from(b64Clean, "base64");
109
+ } catch (e) {
110
+ throw new Error(
111
+ "WEIBO_BASE64_PARSE: stream wasn't valid base64 (" +
112
+ (e.message || String(e)) +
113
+ ")",
114
+ );
115
+ }
116
+ if (buf.length < 1024) {
117
+ throw new Error(
118
+ "WEIBO_COOKIES_TRUNCATED: decoded file is only " +
119
+ buf.length +
120
+ " bytes — expected ≥4KB sqlite. Possible MIUI silent su fail; check `adb logcat`.",
121
+ );
122
+ }
123
+ const magic = buf.subarray(0, 16).toString("latin1");
124
+ if (!magic.startsWith("SQLite format 3")) {
125
+ throw new Error(
126
+ "WEIBO_NOT_SQLITE: decoded file lacks `SQLite format 3` magic header. Got bytes: " +
127
+ buf.subarray(0, 16).toString("hex"),
128
+ );
129
+ }
130
+ const tmpDir = os.tmpdir();
131
+ const tmpFile = path.join(
132
+ tmpDir,
133
+ `cc-weibo-cookies-${crypto.randomUUID()}.db`,
134
+ );
135
+ fs.writeFileSync(tmpFile, buf);
136
+ return tmpFile;
137
+ }
138
+
139
+ /**
140
+ * Build a Cookie header from the chromium-cookies array. Weibo doesn't
141
+ * have a strict required-cookie list like Bilibili's 5 — but SUB must
142
+ * be present (it's the session cookie). Everything else is best-effort
143
+ * passthrough.
144
+ */
145
+ function assembleWeiboCookieHeader(cookies) {
146
+ if (!Array.isArray(cookies)) {
147
+ throw new TypeError("assembleWeiboCookieHeader: cookies must be an array");
148
+ }
149
+ const byName = new Map();
150
+ for (const c of cookies) {
151
+ // Most-recently-set wins on duplicate names; prefer more-specific host
152
+ if (
153
+ !byName.has(c.name) ||
154
+ c.hostKey.length > (byName.get(c.name).hostKey || "").length
155
+ ) {
156
+ byName.set(c.name, c);
157
+ }
158
+ }
159
+ const hasSub = byName.has(WEIBO_REQUIRED_COOKIE);
160
+ if (!hasSub) {
161
+ return {
162
+ header: null,
163
+ present: new Set(byName.keys()),
164
+ missing: [WEIBO_REQUIRED_COOKIE],
165
+ hasSub: false,
166
+ };
167
+ }
168
+ // Pass everything through — Weibo's m.weibo.cn API picks what it needs
169
+ // (SUB / SUBP / _T_WM / MLOGIN / WEIBOCN_FROM / etc.)
170
+ const header = Array.from(byName.values())
171
+ .map((c) => `${c.name}=${c.value}`)
172
+ .join("; ");
173
+ return {
174
+ header,
175
+ present: new Set(byName.keys()),
176
+ missing: [],
177
+ hasSub: true,
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Factory: returns the extension handler. Same contract as Bilibili
183
+ * Phase 1a — stateless, no closure-captured device serial.
184
+ */
185
+ function createWeiboCookiesExtension(factoryOpts = {}) {
186
+ const timeoutMs = factoryOpts.timeoutMs || 60_000;
187
+ const onCleanupFailed = factoryOpts.onCleanupFailed || (() => {});
188
+
189
+ return async function weiboCookiesHandler(_params, ctx) {
190
+ if (
191
+ !ctx ||
192
+ typeof ctx.adb !== "function" ||
193
+ typeof ctx.pickDevice !== "function"
194
+ ) {
195
+ throw new TypeError(
196
+ "weibo.cookies extension: ctx must provide {adb, pickDevice} (got " +
197
+ typeof ctx +
198
+ ")",
199
+ );
200
+ }
201
+ const serial = await ctx.pickDevice();
202
+ let tmpFile = null;
203
+ try {
204
+ tmpFile = await pullCookiesViaSu(ctx.adb, serial, { timeoutMs });
205
+ const cookies = readChromiumCookies(tmpFile, WEIBO_COOKIE_HOST_DOMAIN);
206
+ const cookieCount = cookies.length;
207
+ const hadEncrypted = (cookies._skippedEncryptedCount || 0) > 0;
208
+ const { header, missing, present, hasSub } =
209
+ assembleWeiboCookieHeader(cookies);
210
+ if (header === null) {
211
+ throw new Error(
212
+ "WEIBO_COOKIES_INCOMPLETE: missing required cookie " +
213
+ JSON.stringify(missing) +
214
+ ". User probably logged out, or Weibo App uses a non-default WebView storage path (hadEncrypted=" +
215
+ hadEncrypted +
216
+ "). Tell user to relog on phone.",
217
+ );
218
+ }
219
+ return {
220
+ cookie: header,
221
+ extractedAt: Date.now(),
222
+ diagnostic: {
223
+ cookieCount,
224
+ hadEncrypted,
225
+ hasSub,
226
+ cookieNames: Array.from(present),
227
+ },
228
+ };
229
+ } finally {
230
+ if (tmpFile) {
231
+ try {
232
+ fs.unlinkSync(tmpFile);
233
+ } catch (_e) {
234
+ onCleanupFailed(tmpFile);
235
+ }
236
+ }
237
+ }
238
+ };
239
+ }
240
+
241
+ module.exports = {
242
+ createWeiboCookiesExtension,
243
+ WEIBO_COOKIES_REMOTE_PATH,
244
+ WEIBO_COOKIE_HOST_DOMAIN,
245
+ WEIBO_REQUIRED_COOKIE,
246
+ assembleWeiboCookieHeader,
247
+ // Exposed for tests
248
+ _internals: {
249
+ pullCookiesViaSu,
250
+ },
251
+ };
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * social-weibo-adb — Phase 3 (Weibo C 路径) entry.
5
+ *
6
+ * Phase 3a (this commit) — desktop ADB cookies + m.weibo.cn HTTP path:
7
+ * - weibo.cookies extension (pulls Chromium cookies from Weibo App)
8
+ * - WeiboApiClient (Node port, 4 endpoints, no signing)
9
+ * - buildSnapshot (post / favourite / follow → schemaVersion=1)
10
+ * - collect / collectAndSync
11
+ *
12
+ * Pipeline:
13
+ * bridge.invoke("weibo.cookies")
14
+ * → WeiboApiClient.fetchUid (cookie has no inline UID)
15
+ * → fetchPosts + fetchFavourites + fetchFollows
16
+ * → buildSnapshot + writeSnapshotJson
17
+ * → registry.syncAdapter("social-weibo", { inputPath })
18
+ *
19
+ * Reuses the existing `social-weibo` adapter's snapshot mode — same
20
+ * vault schema / dedup / event types. No 2nd adapter.
21
+ */
22
+
23
+ const {
24
+ createWeiboCookiesExtension,
25
+ WEIBO_COOKIES_REMOTE_PATH,
26
+ WEIBO_COOKIE_HOST_DOMAIN,
27
+ WEIBO_REQUIRED_COOKIE,
28
+ assembleWeiboCookieHeader,
29
+ } = require("./cookies-extension");
30
+ const { WeiboApiClient } = require("./api-client");
31
+ const {
32
+ buildSnapshot,
33
+ writeSnapshotJson,
34
+ cleanupSnapshotJson,
35
+ SNAPSHOT_SCHEMA_VERSION,
36
+ } = require("./snapshot-builder");
37
+ const { collect, collectAndSync } = require("./collector");
38
+
39
+ module.exports = {
40
+ // Extension factory (wiring registers this on the bridge)
41
+ createWeiboCookiesExtension,
42
+ WEIBO_COOKIES_REMOTE_PATH,
43
+ WEIBO_COOKIE_HOST_DOMAIN,
44
+ WEIBO_REQUIRED_COOKIE,
45
+ assembleWeiboCookieHeader,
46
+ // API client + builder
47
+ WeiboApiClient,
48
+ buildSnapshot,
49
+ writeSnapshotJson,
50
+ cleanupSnapshotJson,
51
+ SNAPSHOT_SCHEMA_VERSION,
52
+ // Collector orchestrator
53
+ collect,
54
+ collectAndSync,
55
+ };
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Phase 3a (Weibo C 路径 — 2026-05-25): API responses → snapshot JSON.
5
+ *
6
+ * Matches the existing `social-weibo` adapter's snapshot mode schema:
7
+ *
8
+ * {
9
+ * "schemaVersion": 1,
10
+ * "snapshottedAt": <ms>,
11
+ * "account": { "uid": "<numeric uid as string>", "displayName": "" },
12
+ * "events": [
13
+ * { "kind": "post", "id": "post-<mid>", "capturedAt": <ms>,
14
+ * "text", "mid", "source", "repostsCount", "commentsCount",
15
+ * "likesCount", "picCount" },
16
+ * { "kind": "favourite", "id": "fav-<mid>", "capturedAt": <ms>,
17
+ * "text", "mid", "authorScreenName" },
18
+ * { "kind": "follow", "id": "follow-<uid>", "capturedAt": <ms>,
19
+ * "uid", "screenName", "description", "avatarUrl" }
20
+ * ]
21
+ * }
22
+ *
23
+ * Note: `follow` items don't have an authoritative timestamp from
24
+ * m.weibo.cn's /api/friendships/friends — we use snapshottedAt as
25
+ * fallback so the timestamp is at least monotonic per sync.
26
+ */
27
+
28
+ const fs = require("node:fs");
29
+ const path = require("node:path");
30
+ const os = require("node:os");
31
+ const crypto = require("node:crypto");
32
+
33
+ const SNAPSHOT_SCHEMA_VERSION = 1;
34
+
35
+ function buildSnapshot(input) {
36
+ if (!input || typeof input !== "object") {
37
+ throw new TypeError("buildSnapshot: input must be an object");
38
+ }
39
+ const uid = input.uid;
40
+ if (!Number.isFinite(uid) || uid <= 0) {
41
+ throw new TypeError(
42
+ "buildSnapshot: input.uid must be a positive integer (was " + uid + ")",
43
+ );
44
+ }
45
+ const snapshottedAt =
46
+ Number.isFinite(input.snapshottedAt) && input.snapshottedAt > 0
47
+ ? input.snapshottedAt
48
+ : Date.now();
49
+ const account = {
50
+ uid: String(uid),
51
+ displayName:
52
+ typeof input.displayName === "string" ? input.displayName : "",
53
+ };
54
+ const events = [];
55
+
56
+ // posts
57
+ const posts = Array.isArray(input.posts) ? input.posts : [];
58
+ posts.forEach((p, idx) => {
59
+ if (!p || typeof p !== "object") return;
60
+ events.push({
61
+ kind: "post",
62
+ id: p.mid ? `post-${p.mid}` : `post-${idx}`,
63
+ capturedAt: typeof p.createdAt === "number" && p.createdAt > 0 ? p.createdAt : snapshottedAt,
64
+ text: p.text || null,
65
+ mid: p.mid || null,
66
+ source: p.source || null,
67
+ repostsCount: typeof p.repostsCount === "number" ? p.repostsCount : 0,
68
+ commentsCount:
69
+ typeof p.commentsCount === "number" ? p.commentsCount : 0,
70
+ likesCount: typeof p.likesCount === "number" ? p.likesCount : 0,
71
+ picCount: typeof p.picCount === "number" ? p.picCount : 0,
72
+ });
73
+ });
74
+
75
+ // favourites
76
+ const favs = Array.isArray(input.favourites) ? input.favourites : [];
77
+ favs.forEach((f, idx) => {
78
+ if (!f || typeof f !== "object") return;
79
+ events.push({
80
+ kind: "favourite",
81
+ id: f.mid ? `fav-${f.mid}` : `fav-${idx}`,
82
+ capturedAt: typeof f.favAt === "number" && f.favAt > 0 ? f.favAt : snapshottedAt,
83
+ text: f.text || null,
84
+ mid: f.mid || null,
85
+ authorScreenName: f.authorScreenName || null,
86
+ });
87
+ });
88
+
89
+ // follows
90
+ const fols = Array.isArray(input.follows) ? input.follows : [];
91
+ fols.forEach((fol, idx) => {
92
+ if (!fol || typeof fol !== "object") return;
93
+ const followUid = typeof fol.uid === "number" ? fol.uid : null;
94
+ events.push({
95
+ kind: "follow",
96
+ id: followUid != null ? `follow-${followUid}` : `follow-${idx}`,
97
+ // /api/friendships/friends doesn't return follow time → fall back
98
+ capturedAt:
99
+ typeof fol.followedAt === "number" && fol.followedAt > 0
100
+ ? fol.followedAt
101
+ : snapshottedAt,
102
+ uid: followUid != null ? followUid : null,
103
+ screenName: fol.screenName || null,
104
+ description: fol.description || null,
105
+ avatarUrl: fol.avatarUrl || null,
106
+ });
107
+ });
108
+
109
+ return {
110
+ schemaVersion: SNAPSHOT_SCHEMA_VERSION,
111
+ snapshottedAt,
112
+ account,
113
+ events,
114
+ };
115
+ }
116
+
117
+ function writeSnapshotJson(snapshot, opts = {}) {
118
+ const dir = opts.dir || os.tmpdir();
119
+ const fileName =
120
+ opts.fileName || `cc-weibo-snapshot-${crypto.randomUUID()}.json`;
121
+ if (fileName.includes("/") || fileName.includes("\\")) {
122
+ throw new Error(
123
+ "writeSnapshotJson: opts.fileName must be a basename, not a path",
124
+ );
125
+ }
126
+ const full = path.join(dir, fileName);
127
+ fs.writeFileSync(full, JSON.stringify(snapshot), "utf-8");
128
+ return full;
129
+ }
130
+
131
+ function cleanupSnapshotJson(filePath) {
132
+ if (!filePath) return;
133
+ try {
134
+ fs.unlinkSync(filePath);
135
+ } catch (_e) {
136
+ // ignore
137
+ }
138
+ }
139
+
140
+ module.exports = {
141
+ buildSnapshot,
142
+ writeSnapshotJson,
143
+ cleanupSnapshotJson,
144
+ SNAPSHOT_SCHEMA_VERSION,
145
+ };