@chainlesschain/personal-data-hub 0.2.2 → 0.2.3
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/social-toutiao-kuaishou-scaffold.test.js +58 -16
- package/__tests__/longtail-adapters.test.js +60 -14
- package/__tests__/messaging-qq-snapshot.test.js +294 -0
- package/__tests__/shopping-pinduoduo-snapshot.test.js +302 -0
- package/__tests__/shopping-snapshot.test.js +438 -0
- package/__tests__/social-adapters.test.js +28 -3
- package/__tests__/social-douyin-snapshot.test.js +253 -0
- package/__tests__/social-kuaishou-snapshot.test.js +309 -0
- package/__tests__/social-toutiao-snapshot.test.js +314 -0
- package/__tests__/social-weibo-snapshot.test.js +234 -0
- package/__tests__/social-xiaohongshu-snapshot.test.js +232 -0
- package/__tests__/travel-maps-snapshot.test.js +426 -0
- package/__tests__/vault-driver-error.test.js +74 -0
- package/lib/adapters/messaging-qq/index.js +498 -92
- package/lib/adapters/shopping-jd/index.js +228 -25
- package/lib/adapters/shopping-meituan/index.js +222 -26
- package/lib/adapters/shopping-pinduoduo/index.js +275 -0
- package/lib/adapters/social-douyin/index.js +454 -63
- package/lib/adapters/social-kuaishou/index.js +379 -127
- package/lib/adapters/social-toutiao/index.js +400 -130
- package/lib/adapters/social-weibo/index.js +393 -95
- package/lib/adapters/social-xiaohongshu/index.js +389 -49
- package/lib/adapters/travel-baidu-map/index.js +286 -26
- package/lib/adapters/travel-tencent-map/index.js +414 -0
- package/lib/index.js +5 -1
- package/lib/vault.js +60 -8
- package/package.json +2 -1
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §2.5b 地图三联 v0.2 — Tencent Map (腾讯地图) adapter, dual-mode (snapshot + sqlite).
|
|
3
|
+
*
|
|
4
|
+
* 新增本 adapter 把地图三联补齐 (amap / baidu-map / tencent-map)。两条路径
|
|
5
|
+
* 与 travel-baidu-map / travel-amap 同 pattern:
|
|
6
|
+
*
|
|
7
|
+
* 1. snapshot mode (opts.inputPath): in-APK Android cc reads a snapshot
|
|
8
|
+
* JSON produced by TencentMapLocalCollector (WebView cookie scrape on
|
|
9
|
+
* map.qq.com). Desktop-independent. Adapter stateless — account.
|
|
10
|
+
* deviceId OPTIONAL at construction.
|
|
11
|
+
*
|
|
12
|
+
* 2. sqlite mode (opts.dbPath, future device-pull): scaffold for completeness
|
|
13
|
+
* — table names are educated guess (sjqz/parsers does not yet have a
|
|
14
|
+
* tencent-map parser). Mode runs but trySelect tolerates missing tables.
|
|
15
|
+
* account.deviceId REQUIRED in this mode (checked at sync, not
|
|
16
|
+
* construction).
|
|
17
|
+
*
|
|
18
|
+
* Snapshot schema (mirrors TencentMapLocalCollector.SNAPSHOT_SCHEMA_VERSION):
|
|
19
|
+
*
|
|
20
|
+
* {
|
|
21
|
+
* "schemaVersion": 1,
|
|
22
|
+
* "snapshottedAt": <epoch-ms>,
|
|
23
|
+
* "vendor": "tencent-map",
|
|
24
|
+
* "account": { "uid": "...", "displayName": "..." },
|
|
25
|
+
* "events": [
|
|
26
|
+
* { "kind": "favourite", "id": "fav-<rid>", "capturedAt": <ms>,
|
|
27
|
+
* "name": "...", "address": "...", "lat": .., "lng": .., "category": "home|company|other" },
|
|
28
|
+
* { "kind": "search", "id": "search-<sid>","capturedAt": <ms>,
|
|
29
|
+
* "query": "...", "city": "..." },
|
|
30
|
+
* { "kind": "route", "id": "route-<rid>", "capturedAt": <ms>,
|
|
31
|
+
* "from": {...}, "to": {...}, "mode": "drive|walk|bus|bike|trip" }
|
|
32
|
+
* ]
|
|
33
|
+
* }
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
"use strict";
|
|
37
|
+
|
|
38
|
+
const fs = require("node:fs");
|
|
39
|
+
const { normalizeTravelRecord, parseChineseDateTime } = require("../travel-base");
|
|
40
|
+
|
|
41
|
+
const NAME = "travel-tencent-map";
|
|
42
|
+
const VERSION = "0.2.0";
|
|
43
|
+
const SNAPSHOT_SCHEMA_VERSION = 1;
|
|
44
|
+
|
|
45
|
+
const KIND_FAVOURITE = "favourite";
|
|
46
|
+
const KIND_SEARCH = "search";
|
|
47
|
+
const KIND_ROUTE = "route";
|
|
48
|
+
const VALID_SNAPSHOT_KINDS = Object.freeze([KIND_FAVOURITE, KIND_SEARCH, KIND_ROUTE]);
|
|
49
|
+
|
|
50
|
+
class TencentMapAdapter {
|
|
51
|
+
constructor(opts = {}) {
|
|
52
|
+
// §2.5b v0.2: account.deviceId OPTIONAL — snapshot mode is stateless.
|
|
53
|
+
// Sqlite mode requires it; checked at sync time.
|
|
54
|
+
this.account = opts.account || null;
|
|
55
|
+
this._dbPath = opts.dbPath || null;
|
|
56
|
+
|
|
57
|
+
this.name = NAME;
|
|
58
|
+
this.version = VERSION;
|
|
59
|
+
this.capabilities = [
|
|
60
|
+
"sync:snapshot",
|
|
61
|
+
"sync:sqlite",
|
|
62
|
+
"parse:tencent-map-favourite",
|
|
63
|
+
"parse:tencent-map-history",
|
|
64
|
+
];
|
|
65
|
+
this.extractMode = "device-pull";
|
|
66
|
+
this.rateLimits = {};
|
|
67
|
+
this.dataDisclosure = {
|
|
68
|
+
fields: [
|
|
69
|
+
"tencent:account (uid / displayName, cookie scrape)",
|
|
70
|
+
"tencent:favourite (saved places — home / company / other)",
|
|
71
|
+
"tencent:search_history (queries, scaffold sqlite mode)",
|
|
72
|
+
"tencent:route_history (planned routes, scaffold sqlite mode)",
|
|
73
|
+
],
|
|
74
|
+
sensitivity: "medium",
|
|
75
|
+
legalGate: false,
|
|
76
|
+
defaultInclude: {
|
|
77
|
+
favourite: true,
|
|
78
|
+
search: true,
|
|
79
|
+
route: true,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
this._deps = {
|
|
84
|
+
fs,
|
|
85
|
+
dbDriverFactory: opts.dbDriverFactory || null,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async authenticate(ctx = {}) {
|
|
90
|
+
if (ctx && typeof ctx.inputPath === "string" && ctx.inputPath.length > 0) {
|
|
91
|
+
try {
|
|
92
|
+
this._deps.fs.accessSync(ctx.inputPath, this._deps.fs.constants.R_OK);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
reason: "INPUT_PATH_UNREADABLE",
|
|
97
|
+
message: `snapshot not readable at ${ctx.inputPath}: ${err.message}`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { ok: true, mode: "snapshot-file" };
|
|
101
|
+
}
|
|
102
|
+
if (this._dbPath || (ctx && typeof ctx.dbPath === "string")) {
|
|
103
|
+
if (!this.account || !this.account.deviceId) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
reason: "NO_ACCOUNT_DEVICE_ID",
|
|
107
|
+
message: "travel-tencent-map.authenticate: sqlite mode requires account.deviceId",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return { ok: true, account: this.account.deviceId, mode: "sqlite" };
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
reason: "NO_INPUT",
|
|
115
|
+
message:
|
|
116
|
+
"travel-tencent-map.authenticate: needs opts.inputPath (snapshot mode) OR opts.dbPath (sqlite mode)",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async healthCheck() {
|
|
121
|
+
return { ok: true, lastChecked: Date.now() };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async *sync(opts = {}) {
|
|
125
|
+
if (typeof opts.inputPath === "string" && opts.inputPath.length > 0) {
|
|
126
|
+
yield* this._syncViaSnapshot(opts);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const dbPath = opts.dbPath || this._dbPath;
|
|
130
|
+
if (dbPath) {
|
|
131
|
+
yield* this._syncViaSqlite({ ...opts, dbPath });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
throw new Error(
|
|
135
|
+
"travel-tencent-map.sync: needs opts.inputPath (snapshot mode, Android in-APK cc) OR opts.dbPath (sqlite mode)",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async *_syncViaSnapshot(opts) {
|
|
140
|
+
const raw = this._deps.fs.readFileSync(opts.inputPath, "utf-8");
|
|
141
|
+
const snapshot = JSON.parse(raw);
|
|
142
|
+
if (
|
|
143
|
+
!snapshot ||
|
|
144
|
+
typeof snapshot !== "object" ||
|
|
145
|
+
snapshot.schemaVersion !== SNAPSHOT_SCHEMA_VERSION
|
|
146
|
+
) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`travel-tencent-map.sync: snapshot schemaVersion mismatch (got ${snapshot && snapshot.schemaVersion}, expected ${SNAPSHOT_SCHEMA_VERSION})`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const fallbackCapturedAt =
|
|
152
|
+
Number.isFinite(snapshot.snapshottedAt) && snapshot.snapshottedAt > 0
|
|
153
|
+
? Math.floor(snapshot.snapshottedAt)
|
|
154
|
+
: Date.now();
|
|
155
|
+
const account =
|
|
156
|
+
snapshot.account && typeof snapshot.account === "object"
|
|
157
|
+
? snapshot.account
|
|
158
|
+
: null;
|
|
159
|
+
const include = opts.include || {};
|
|
160
|
+
const limit =
|
|
161
|
+
Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : Infinity;
|
|
162
|
+
|
|
163
|
+
const events = Array.isArray(snapshot.events) ? snapshot.events : [];
|
|
164
|
+
let emitted = 0;
|
|
165
|
+
for (const ev of events) {
|
|
166
|
+
if (emitted >= limit) return;
|
|
167
|
+
if (!ev || typeof ev !== "object") continue;
|
|
168
|
+
const kind = ev.kind;
|
|
169
|
+
if (!VALID_SNAPSHOT_KINDS.includes(kind)) continue;
|
|
170
|
+
if (include[kind] === false) continue;
|
|
171
|
+
|
|
172
|
+
const capturedAt =
|
|
173
|
+
parseTime(ev.capturedAt) ||
|
|
174
|
+
parseTime(ev.time) ||
|
|
175
|
+
fallbackCapturedAt;
|
|
176
|
+
const id =
|
|
177
|
+
(typeof ev.id === "string" && ev.id.length > 0 && ev.id) ||
|
|
178
|
+
ev.rid ||
|
|
179
|
+
null;
|
|
180
|
+
|
|
181
|
+
yield {
|
|
182
|
+
adapter: NAME,
|
|
183
|
+
kind,
|
|
184
|
+
originalId: stableOriginalId(kind, id),
|
|
185
|
+
capturedAt,
|
|
186
|
+
payload: { ...ev, account },
|
|
187
|
+
};
|
|
188
|
+
emitted += 1;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async *_syncViaSqlite(opts) {
|
|
193
|
+
if (!this.account || !this.account.deviceId) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
"travel-tencent-map._syncViaSqlite: account.deviceId required (set via new TencentMapAdapter({ account: { deviceId } }))",
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
const dbPath = opts.dbPath;
|
|
199
|
+
if (!dbPath || !this._deps.fs.existsSync(dbPath)) return;
|
|
200
|
+
const Driver = this._deps.dbDriverFactory
|
|
201
|
+
? this._deps.dbDriverFactory()
|
|
202
|
+
: require("better-sqlite3-multiple-ciphers");
|
|
203
|
+
const db = new Driver(dbPath, { readonly: true });
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
// Tencent Map Android app table names (educated guess — sjqz has no
|
|
207
|
+
// parser yet; trySelect tolerates missing tables for forward-compat).
|
|
208
|
+
const routes =
|
|
209
|
+
trySelect(db, "SELECT * FROM route_history LIMIT 5000")
|
|
210
|
+
|| trySelect(db, "SELECT * FROM tencent_route_history LIMIT 5000")
|
|
211
|
+
|| [];
|
|
212
|
+
for (const r of routes) {
|
|
213
|
+
const rec = routeRowToRecord(r);
|
|
214
|
+
if (rec) {
|
|
215
|
+
yield {
|
|
216
|
+
adapter: NAME,
|
|
217
|
+
originalId: rec.recordId,
|
|
218
|
+
capturedAt: rec.bookedAt || Date.now(),
|
|
219
|
+
payload: { record: rec, kind: KIND_ROUTE },
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const searches =
|
|
224
|
+
trySelect(db, "SELECT * FROM search_history LIMIT 5000")
|
|
225
|
+
|| trySelect(db, "SELECT * FROM tencent_search_history LIMIT 5000")
|
|
226
|
+
|| [];
|
|
227
|
+
for (const r of searches) {
|
|
228
|
+
const rec = searchRowToRecord(r);
|
|
229
|
+
if (rec) {
|
|
230
|
+
yield {
|
|
231
|
+
adapter: NAME,
|
|
232
|
+
originalId: rec.recordId,
|
|
233
|
+
capturedAt: rec.bookedAt || Date.now(),
|
|
234
|
+
payload: { record: rec, kind: KIND_SEARCH },
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} finally {
|
|
239
|
+
try { db.close(); } catch (_e) { /* ignore */ }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
normalize(raw) {
|
|
244
|
+
if (!raw || !raw.payload) {
|
|
245
|
+
throw new Error("TencentMapAdapter.normalize: payload missing");
|
|
246
|
+
}
|
|
247
|
+
const kind = raw.kind || raw.payload.kind;
|
|
248
|
+
const p = raw.payload;
|
|
249
|
+
|
|
250
|
+
if (p.record) {
|
|
251
|
+
return normalizeTravelRecord(p.record, {
|
|
252
|
+
adapterName: NAME,
|
|
253
|
+
adapterVersion: VERSION,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
const rec = snapshotEventToRecord(kind, p, raw.originalId);
|
|
257
|
+
return normalizeTravelRecord(rec, {
|
|
258
|
+
adapterName: NAME,
|
|
259
|
+
adapterVersion: VERSION,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function stableOriginalId(kind, id) {
|
|
265
|
+
const stringified =
|
|
266
|
+
(typeof id === "string" && id.length > 0 && id) ||
|
|
267
|
+
(typeof id === "number" && Number.isFinite(id) && String(id)) ||
|
|
268
|
+
null;
|
|
269
|
+
const safe =
|
|
270
|
+
stringified ||
|
|
271
|
+
`unknown-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
272
|
+
return `tencent-map:${kind}:${safe}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function snapshotEventToRecord(kind, p, originalId) {
|
|
276
|
+
if (kind === KIND_FAVOURITE) {
|
|
277
|
+
return {
|
|
278
|
+
vendorId: "tencentmap",
|
|
279
|
+
recordId: originalId,
|
|
280
|
+
vehicleType: "visit",
|
|
281
|
+
to: {
|
|
282
|
+
name: p.name || p.address || null,
|
|
283
|
+
lat: numberOrNull(p.lat),
|
|
284
|
+
lng: numberOrNull(p.lng),
|
|
285
|
+
city: p.city || null,
|
|
286
|
+
},
|
|
287
|
+
departureMs: parseTime(p.capturedAt),
|
|
288
|
+
carrier: "腾讯地图",
|
|
289
|
+
extras: { category: p.category || null, kind: KIND_FAVOURITE },
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (kind === KIND_SEARCH) {
|
|
293
|
+
return {
|
|
294
|
+
vendorId: "tencentmap",
|
|
295
|
+
recordId: originalId,
|
|
296
|
+
vehicleType: "visit",
|
|
297
|
+
to: {
|
|
298
|
+
name: p.query || null,
|
|
299
|
+
lat: numberOrNull(p.lat),
|
|
300
|
+
lng: numberOrNull(p.lng),
|
|
301
|
+
city: p.city || null,
|
|
302
|
+
},
|
|
303
|
+
departureMs: parseTime(p.capturedAt),
|
|
304
|
+
carrier: "腾讯地图",
|
|
305
|
+
extras: { query: p.query || null, kind: KIND_SEARCH },
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (kind === KIND_ROUTE) {
|
|
309
|
+
return {
|
|
310
|
+
vendorId: "tencentmap",
|
|
311
|
+
recordId: originalId,
|
|
312
|
+
vehicleType: detectVehicle(p.mode),
|
|
313
|
+
from: p.from
|
|
314
|
+
? { name: p.from.name || null, lat: numberOrNull(p.from.lat), lng: numberOrNull(p.from.lng) }
|
|
315
|
+
: undefined,
|
|
316
|
+
to: p.to
|
|
317
|
+
? { name: p.to.name || null, lat: numberOrNull(p.to.lat), lng: numberOrNull(p.to.lng) }
|
|
318
|
+
: undefined,
|
|
319
|
+
departureMs: parseTime(p.capturedAt),
|
|
320
|
+
carrier: "腾讯地图",
|
|
321
|
+
extras: { mode: p.mode || null, kind: KIND_ROUTE },
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
vendorId: "tencentmap",
|
|
326
|
+
recordId: originalId,
|
|
327
|
+
vehicleType: "visit",
|
|
328
|
+
carrier: "腾讯地图",
|
|
329
|
+
extras: { kind, raw: p },
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function trySelect(db, sql) {
|
|
334
|
+
try {
|
|
335
|
+
return db.prepare(sql).all();
|
|
336
|
+
} catch (_e) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function routeRowToRecord(row) {
|
|
342
|
+
if (!row) return null;
|
|
343
|
+
const id = row._id || row.id || row.uid;
|
|
344
|
+
if (!id) return null;
|
|
345
|
+
return {
|
|
346
|
+
vendorId: "tencentmap",
|
|
347
|
+
recordId: `route-${id}`,
|
|
348
|
+
vehicleType: detectVehicle(row.type || row.mode),
|
|
349
|
+
from: { name: row.start_name || row.from_name, lat: row.start_lat || null, lng: row.start_lng || null },
|
|
350
|
+
to: { name: row.end_name || row.to_name, lat: row.end_lat || null, lng: row.end_lng || null },
|
|
351
|
+
departureMs: numberOrParse(row.time || row.create_time),
|
|
352
|
+
carrier: "腾讯地图",
|
|
353
|
+
extras: { mode: row.type || row.mode },
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function searchRowToRecord(row) {
|
|
358
|
+
if (!row) return null;
|
|
359
|
+
const id = row._id || row.id;
|
|
360
|
+
if (!id) return null;
|
|
361
|
+
return {
|
|
362
|
+
vendorId: "tencentmap",
|
|
363
|
+
recordId: `search-${id}`,
|
|
364
|
+
vehicleType: "visit",
|
|
365
|
+
to: { name: row.key || row.query || row.keyword, lat: row.lat || null, lng: row.lng || null, city: row.city },
|
|
366
|
+
departureMs: numberOrParse(row.time || row.create_time),
|
|
367
|
+
carrier: "腾讯地图",
|
|
368
|
+
extras: { query: row.key || row.query || row.keyword },
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function detectVehicle(v) {
|
|
373
|
+
const s = String(v || "").toLowerCase();
|
|
374
|
+
if (s.includes("drive") || s.includes("car")) return "car";
|
|
375
|
+
if (s.includes("walk")) return "walk";
|
|
376
|
+
if (s.includes("bike") || s.includes("cycle")) return "bike";
|
|
377
|
+
if (s.includes("bus") || s.includes("transit")) return "bus";
|
|
378
|
+
return "trip";
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function numberOrNull(v) {
|
|
382
|
+
if (Number.isFinite(v)) return v;
|
|
383
|
+
if (typeof v === "string" && /^-?\d+(\.\d+)?$/.test(v)) return parseFloat(v);
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function parseTime(v) {
|
|
388
|
+
if (Number.isFinite(v)) return v > 1e12 ? v : v * 1000;
|
|
389
|
+
if (typeof v === "string") {
|
|
390
|
+
if (/^\d+$/.test(v)) {
|
|
391
|
+
const n = parseInt(v, 10);
|
|
392
|
+
return n > 1e12 ? n : n * 1000;
|
|
393
|
+
}
|
|
394
|
+
const parsed = parseChineseDateTime(v);
|
|
395
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
396
|
+
const t = Date.parse(v);
|
|
397
|
+
return Number.isFinite(t) ? t : null;
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function numberOrParse(v) {
|
|
403
|
+
if (Number.isFinite(v)) return v > 1e12 ? v : v * 1000;
|
|
404
|
+
if (typeof v === "string") {
|
|
405
|
+
if (/^\d+$/.test(v)) {
|
|
406
|
+
const n = parseInt(v, 10);
|
|
407
|
+
return n > 1e12 ? n : n * 1000;
|
|
408
|
+
}
|
|
409
|
+
return parseChineseDateTime(v);
|
|
410
|
+
}
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
module.exports = { TencentMapAdapter, NAME, VERSION, SNAPSHOT_SCHEMA_VERSION };
|
package/lib/index.js
CHANGED
|
@@ -39,10 +39,12 @@ const { Train12306Adapter } = require("./adapters/travel-12306");
|
|
|
39
39
|
const { CtripAdapter } = require("./adapters/travel-ctrip");
|
|
40
40
|
const { AmapAdapter } = require("./adapters/travel-amap");
|
|
41
41
|
const { BaiduMapAdapter } = require("./adapters/travel-baidu-map");
|
|
42
|
+
const { TencentMapAdapter } = require("./adapters/travel-tencent-map");
|
|
42
43
|
const shoppingBase = require("./adapters/shopping-base");
|
|
43
44
|
const { TaobaoAdapter } = require("./adapters/shopping-taobao");
|
|
44
45
|
const { JdAdapter } = require("./adapters/shopping-jd");
|
|
45
46
|
const { MeituanAdapter } = require("./adapters/shopping-meituan");
|
|
47
|
+
const { PinduoduoAdapter } = require("./adapters/shopping-pinduoduo");
|
|
46
48
|
const { BilibiliAdapter } = require("./adapters/social-bilibili");
|
|
47
49
|
const { WeiboAdapter } = require("./adapters/social-weibo");
|
|
48
50
|
const { DouyinAdapter } = require("./adapters/social-douyin");
|
|
@@ -221,13 +223,14 @@ module.exports = {
|
|
|
221
223
|
wxidToWeChatPersonId: wechatAdapter.wxidToWeChatPersonId,
|
|
222
224
|
WECHAT_PRAGMA_PROFILES: wechatAdapter.WECHAT_PRAGMA_PROFILES,
|
|
223
225
|
|
|
224
|
-
// Phase 9 — Travel
|
|
226
|
+
// Phase 9 + §2.5b 地图三联 — Travel five-pack
|
|
225
227
|
normalizeTravelRecord: travelBase.normalizeTravelRecord,
|
|
226
228
|
parseChineseDateTime: travelBase.parseChineseDateTime,
|
|
227
229
|
Train12306Adapter,
|
|
228
230
|
CtripAdapter,
|
|
229
231
|
AmapAdapter,
|
|
230
232
|
BaiduMapAdapter,
|
|
233
|
+
TencentMapAdapter,
|
|
231
234
|
|
|
232
235
|
// Phase 7 — Shopping three-pack
|
|
233
236
|
normalizeOrderRecord: shoppingBase.normalizeOrderRecord,
|
|
@@ -235,6 +238,7 @@ module.exports = {
|
|
|
235
238
|
TaobaoAdapter,
|
|
236
239
|
JdAdapter,
|
|
237
240
|
MeituanAdapter,
|
|
241
|
+
PinduoduoAdapter,
|
|
238
242
|
|
|
239
243
|
// Phase 13+ — long-tail social + messaging (借 sjqz parsers)
|
|
240
244
|
BilibiliAdapter,
|
package/lib/vault.js
CHANGED
|
@@ -44,19 +44,71 @@ function newGroupId() {
|
|
|
44
44
|
return `mg-${r()}${r()}-${Date.now().toString(36)}`;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Translate a bs3mc load-failure error into an actionable, user-readable
|
|
49
|
+
* message. Detects NODE_MODULE_VERSION mismatch (the single most common
|
|
50
|
+
* failure: Node 23/24/25 has no prebuild — bs3mc upstream only ships for
|
|
51
|
+
* Node LTS ABIs 108/115/127). See memory `node_23_native_dep_trap.md`.
|
|
52
|
+
*
|
|
53
|
+
* Pure function so it can be unit-tested without poisoning require cache.
|
|
54
|
+
*
|
|
55
|
+
* @param {Error|unknown} err Original throw from `require("better-sqlite3-multiple-ciphers")`.
|
|
56
|
+
* @param {string} [nodeVer] process.versions.node (override for tests).
|
|
57
|
+
* @returns {Error} Wrapped Error with `cause` and (when ABI-related) `code: "BS3MC_ABI_MISMATCH"`.
|
|
58
|
+
*/
|
|
59
|
+
function formatDriverLoadError(err, nodeVer) {
|
|
60
|
+
const originalMsg = err && err.message ? err.message : String(err);
|
|
61
|
+
const runtimeNodeVer = nodeVer || process.versions.node;
|
|
62
|
+
|
|
63
|
+
const abiMatch = originalMsg.match(
|
|
64
|
+
/NODE_MODULE_VERSION\s+(\d+)[\s\S]+?requires\s+NODE_MODULE_VERSION\s+(\d+)/,
|
|
65
|
+
);
|
|
66
|
+
if (abiMatch) {
|
|
67
|
+
const compiledAbi = abiMatch[1];
|
|
68
|
+
const runtimeAbi = abiMatch[2];
|
|
69
|
+
const lines = [
|
|
70
|
+
"better-sqlite3-multiple-ciphers ABI mismatch — Node " +
|
|
71
|
+
runtimeNodeVer +
|
|
72
|
+
" has ABI " +
|
|
73
|
+
runtimeAbi +
|
|
74
|
+
" but bs3mc prebuild is ABI " +
|
|
75
|
+
compiledAbi +
|
|
76
|
+
".",
|
|
77
|
+
"",
|
|
78
|
+
"修法(任选其一):",
|
|
79
|
+
" 1. 切 Node 22 LTS (推荐) — nvm-windows: `nvm install 22.12.0 && nvm use 22.12.0`",
|
|
80
|
+
" 2. 源码重编 — `npm rebuild better-sqlite3-multiple-ciphers --build-from-source`",
|
|
81
|
+
" (需要本机有 Visual Studio Build Tools / node-gyp toolchain,慢且不推荐)",
|
|
82
|
+
"",
|
|
83
|
+
"为什么 bs3mc 没 ABI " + runtimeAbi + " prebuild:",
|
|
84
|
+
" bs3mc 上游只 ship 主流 Node LTS 的 prebuild (ABI 108/115/127)。",
|
|
85
|
+
" Node 23/24/25 是 Current 系列,上游不给 prebuild。",
|
|
86
|
+
"",
|
|
87
|
+
"项目 engines.node 允许 >=22.12 是为了兼容未来 LTS,但实际推荐 22.x。",
|
|
88
|
+
];
|
|
89
|
+
const wrapped = new Error(lines.join("\n"));
|
|
90
|
+
wrapped.cause = err;
|
|
91
|
+
wrapped.code = "BS3MC_ABI_MISMATCH";
|
|
92
|
+
return wrapped;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const wrapped = new Error(
|
|
96
|
+
"Failed to load better-sqlite3-multiple-ciphers. " +
|
|
97
|
+
"Install it as a workspace dep or pin the version in your package. " +
|
|
98
|
+
"Original error: " +
|
|
99
|
+
originalMsg,
|
|
100
|
+
);
|
|
101
|
+
wrapped.cause = err;
|
|
102
|
+
return wrapped;
|
|
103
|
+
}
|
|
104
|
+
|
|
47
105
|
function loadDriver() {
|
|
48
106
|
// Lazy require so consumers that only need schemas don't pay for the
|
|
49
107
|
// native binding load. Errors surface here with a precise message.
|
|
50
108
|
try {
|
|
51
109
|
return require("better-sqlite3-multiple-ciphers");
|
|
52
110
|
} catch (err) {
|
|
53
|
-
|
|
54
|
-
"Failed to load better-sqlite3-multiple-ciphers. " +
|
|
55
|
-
"Install it as a workspace dep or pin the version in your package. " +
|
|
56
|
-
"Original error: " + (err && err.message ? err.message : String(err));
|
|
57
|
-
const wrapped = new Error(msg);
|
|
58
|
-
wrapped.cause = err;
|
|
59
|
-
throw wrapped;
|
|
111
|
+
throw formatDriverLoadError(err);
|
|
60
112
|
}
|
|
61
113
|
}
|
|
62
114
|
|
|
@@ -1223,4 +1275,4 @@ class LocalVault {
|
|
|
1223
1275
|
}
|
|
1224
1276
|
}
|
|
1225
1277
|
|
|
1226
|
-
module.exports = { LocalVault };
|
|
1278
|
+
module.exports = { LocalVault, _internal: { loadDriver, formatDriverLoadError } };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chainlesschain/personal-data-hub",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"./adapters/travel-ctrip": "./lib/adapters/travel-ctrip/index.js",
|
|
47
47
|
"./adapters/travel-amap": "./lib/adapters/travel-amap/index.js",
|
|
48
48
|
"./adapters/travel-baidu-map": "./lib/adapters/travel-baidu-map/index.js",
|
|
49
|
+
"./adapters/travel-tencent-map": "./lib/adapters/travel-tencent-map/index.js",
|
|
49
50
|
"./adapters/shopping-base": "./lib/adapters/shopping-base/index.js",
|
|
50
51
|
"./adapters/shopping-taobao": "./lib/adapters/shopping-taobao/index.js",
|
|
51
52
|
"./adapters/shopping-jd": "./lib/adapters/shopping-jd/index.js",
|