@chainlesschain/personal-data-hub 0.4.6 → 0.4.18

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 (31) hide show
  1. package/__tests__/adapters/doc-baidu-netdisk.test.js +102 -0
  2. package/__tests__/adapters/doc-platforms.test.js +177 -0
  3. package/__tests__/adapters/music-kugou.test.js +187 -0
  4. package/__tests__/adapters/recruit-boss.test.js +180 -0
  5. package/__tests__/adapters/shopping-dianping.test.js +239 -0
  6. package/__tests__/adapters/social-csdn.test.js +175 -0
  7. package/__tests__/adapters/social-zhihu.test.js +246 -0
  8. package/__tests__/adapters/travel-12306.test.js +234 -1
  9. package/__tests__/adapters/travel-ctrip.test.js +175 -1
  10. package/__tests__/adapters/travel-didi.test.js +204 -0
  11. package/__tests__/adapters/travel-tongcheng.test.js +289 -0
  12. package/__tests__/adapters/video-platforms.test.js +152 -0
  13. package/lib/adapter-guide.js +13 -1
  14. package/lib/adapters/_document-base.js +370 -0
  15. package/lib/adapters/_video-base.js +331 -0
  16. package/lib/adapters/doc-baidu-netdisk/index.js +91 -0
  17. package/lib/adapters/doc-tencent-docs/index.js +94 -0
  18. package/lib/adapters/doc-wps/index.js +77 -0
  19. package/lib/adapters/music-kugou/index.js +418 -0
  20. package/lib/adapters/recruit-boss/index.js +442 -0
  21. package/lib/adapters/shopping-dianping/index.js +473 -0
  22. package/lib/adapters/social-csdn/index.js +444 -0
  23. package/lib/adapters/social-zhihu/index.js +488 -0
  24. package/lib/adapters/travel-12306/index.js +279 -5
  25. package/lib/adapters/travel-ctrip/index.js +255 -40
  26. package/lib/adapters/travel-didi/index.js +327 -0
  27. package/lib/adapters/travel-tongcheng/index.js +393 -0
  28. package/lib/adapters/video-iqiyi/index.js +75 -0
  29. package/lib/adapters/video-tencent/index.js +78 -0
  30. package/lib/index.js +24 -0
  31. package/package.json +1 -1
@@ -0,0 +1,393 @@
1
+ /**
2
+ * §9.3d — Tongcheng (同程旅行) order adapter, dual-mode (snapshot + cookie-api).
3
+ *
4
+ * 同程 (com.tongcheng.android) is a Phase 9 travel platform (ROI ⭐⭐⭐ in
5
+ * docs/design/Personal_Data_Hub_Architecture.md §12.1, "与携程互补") that was
6
+ * skipped when the travel 四件套 (amap/baidu/ctrip/12306) shipped. It is an OTA
7
+ * like 携程, so this adapter mirrors travel-ctrip: orders span flight / hotel /
8
+ * train / bus / scenery (门票) / cruise / car, each mapped to the appropriate
9
+ * `vehicleType` in the vendor-neutral TravelRecord.
10
+ *
11
+ * 1. snapshot / file-import mode (opts.inputPath | opts.dataPath): ingest a
12
+ * JSON dump from an Android in-APK collector / browser extension / curated
13
+ * file. account OPTIONAL (file-import is stateless).
14
+ *
15
+ * 2. cookie-api mode (opts.account.cookies, v0.1): fetch the Tongcheng order
16
+ * centre directly from the hub. After login on the ly.com domain the order
17
+ * list is reachable under the `.ly.com` cookie. As with the other travel /
18
+ * shopping adapters the actual HTTP call is delegated to an injected
19
+ * `fetchFn` (Android in-APK cc → OkHttp; desktop hub → Electron WebView net
20
+ * request) so this module stays a pure-Node parser + orchestrator. account
21
+ * OPTIONAL — the cookie carries identity.
22
+ *
23
+ * ── sign seam ──────────────────────────────────────────────────────────
24
+ * Tongcheng's H5 / m-API requests carry an anti-bot signature computed by
25
+ * client-side JS (analogous to 携程 mtgsig / 拼多多 anti_token). No pure-Node
26
+ * implementation survives the rotation, so signing is injected via
27
+ * `opts.signProvider` (or constructor `signProvider`). When absent the
28
+ * request is still issued unsigned — best-effort, the endpoint may reject
29
+ * it, which surfaces as zero events rather than a crash. The endpoint
30
+ * constant is best-effort and overridable via `opts.ordersUrl`; Tongcheng
31
+ * rotates H5 paths, so adjust / pass opts.ordersUrl if it drifts
32
+ * (FAMILY-23 playbook — endpoints are not field-verified here).
33
+ */
34
+
35
+ "use strict";
36
+
37
+ const fs = require("node:fs");
38
+ const { normalizeTravelRecord, parseChineseDateTime } = require("../travel-base");
39
+ const { CookieAuth } = require("../shopping-base");
40
+
41
+ const NAME = "travel-tongcheng";
42
+ const VERSION = "0.1.0";
43
+
44
+ // Best-effort Tongcheng order-centre list endpoint. Overridable via
45
+ // opts.ordersUrl (Tongcheng rotates H5 paths; the injected fetchFn host may
46
+ // also point at whichever order API the captured cookie is currently scoped to).
47
+ const TONGCHENG_ORDERS_URL = "https://m.ly.com/order/orderList";
48
+ const DEFAULT_PAGE_SIZE = 20;
49
+ const DEFAULT_MAX_PAGES = 10;
50
+
51
+ const TYPE_MAP = {
52
+ flight: "flight",
53
+ airline: "flight",
54
+ jipiao: "flight",
55
+ 机票: "flight",
56
+ hotel: "hotel",
57
+ jiudian: "hotel",
58
+ 酒店: "hotel",
59
+ train: "train",
60
+ huoche: "train",
61
+ 火车: "train",
62
+ 火车票: "train",
63
+ bus: "bus",
64
+ qiche: "bus",
65
+ 汽车: "bus",
66
+ 汽车票: "bus",
67
+ coach: "bus",
68
+ scenery: "attraction",
69
+ jingdian: "attraction",
70
+ 景点: "attraction",
71
+ 门票: "attraction",
72
+ ticket: "attraction",
73
+ cruise: "cruise",
74
+ youlun: "cruise",
75
+ 邮轮: "cruise",
76
+ car: "car",
77
+ yongche: "car",
78
+ 用车: "car",
79
+ };
80
+
81
+ class TongchengAdapter {
82
+ constructor(opts = {}) {
83
+ this.account = opts.account || null;
84
+ this._dataPath = opts.dataPath || null;
85
+
86
+ // cookie-api mode — activates when account.cookies is supplied.
87
+ this._cookieAuth =
88
+ opts.account && opts.account.cookies
89
+ ? new CookieAuth({ platform: "tongcheng", cookies: opts.account.cookies })
90
+ : null;
91
+ this._fetchFn = typeof opts.fetchFn === "function" ? opts.fetchFn : defaultFetch;
92
+ this._signProvider =
93
+ typeof opts.signProvider === "function" ? opts.signProvider : null;
94
+ this._ordersUrl =
95
+ typeof opts.ordersUrl === "string" && opts.ordersUrl.length > 0
96
+ ? opts.ordersUrl
97
+ : TONGCHENG_ORDERS_URL;
98
+
99
+ this.name = NAME;
100
+ this.version = VERSION;
101
+ this.capabilities = [
102
+ "import:json",
103
+ "sync:snapshot",
104
+ "sync:cookie-api",
105
+ "parse:tongcheng-orders",
106
+ ];
107
+ this.extractMode = "file-import";
108
+ this.rateLimits = {};
109
+ this.dataDisclosure = {
110
+ fields: [
111
+ "tongcheng:orderId / type / fromCity / toCity / dates / passengerName / price / carrier",
112
+ ],
113
+ sensitivity: "medium",
114
+ legalGate: false,
115
+ };
116
+
117
+ // _deps injection seam — vi.mock fs doesn't intercept inlined CJS require.
118
+ this._deps = { fs };
119
+ }
120
+
121
+ async authenticate(ctx = {}) {
122
+ const filePath = (ctx && ctx.inputPath) || ctx.dataPath || this._dataPath;
123
+ if (filePath) {
124
+ try {
125
+ this._deps.fs.accessSync(filePath, this._deps.fs.constants.R_OK);
126
+ } catch (err) {
127
+ return {
128
+ ok: false,
129
+ reason: "INPUT_PATH_UNREADABLE",
130
+ message: `not readable at ${filePath}: ${err.message}`,
131
+ };
132
+ }
133
+ return { ok: true, mode: "snapshot-file" };
134
+ }
135
+ if (this._cookieAuth) {
136
+ const ok = await this._cookieAuth.validate();
137
+ if (!ok) {
138
+ return { ok: false, reason: "INVALID_COOKIE", error: "cookies missing" };
139
+ }
140
+ // account is OPTIONAL in cookie mode — the .ly.com cookie carries identity.
141
+ return {
142
+ ok: true,
143
+ account: (this.account && this.account.email) || null,
144
+ mode: "cookie",
145
+ };
146
+ }
147
+ return {
148
+ ok: true,
149
+ account: this.account ? this.account.email : null,
150
+ mode: "ready",
151
+ };
152
+ }
153
+
154
+ async healthCheck() {
155
+ if (this._cookieAuth) {
156
+ const r = await this.authenticate();
157
+ return r.ok
158
+ ? { ok: true, lastChecked: Date.now() }
159
+ : { ok: false, reason: r.reason, error: r.error };
160
+ }
161
+ return { ok: true, lastChecked: Date.now() };
162
+ }
163
+
164
+ async *sync(opts = {}) {
165
+ const dataPath = opts.inputPath || opts.dataPath || this._dataPath;
166
+ if (dataPath) {
167
+ if (!this._deps.fs.existsSync(dataPath)) return;
168
+ const text = this._deps.fs.readFileSync(dataPath, "utf-8");
169
+ let records;
170
+ try {
171
+ records = parseRecords(text);
172
+ } catch (err) {
173
+ throw new Error(`TongchengAdapter: parse failed: ${err.message}`);
174
+ }
175
+ for (const r of records) {
176
+ yield {
177
+ adapter: NAME,
178
+ originalId: r.recordId,
179
+ capturedAt: r.bookedAt || r.departureMs || Date.now(),
180
+ payload: { record: r },
181
+ };
182
+ }
183
+ return;
184
+ }
185
+ if (this._cookieAuth) {
186
+ yield* this._syncViaCookie(opts);
187
+ }
188
+ }
189
+
190
+ /**
191
+ * §9.3d — cookie-api live fetch. Hits the Tongcheng order-centre list
192
+ * endpoint via the injected fetchFn, paginates with a pageIndex cursor, stops
193
+ * at the sinceWatermark / maxPages, maps each order through orderToRecord (so
194
+ * the existing normalize path applies unchanged) and yields it.
195
+ */
196
+ async *_syncViaCookie(opts = {}) {
197
+ if (!(await this._cookieAuth.validate())) return;
198
+ const cookies = this._cookieAuth.toHeader();
199
+ const sinceMs =
200
+ opts.sinceWatermark != null
201
+ ? parseInt(String(opts.sinceWatermark), 10) || 0
202
+ : Date.now() - 365 * 24 * 3600_000; // default last year
203
+ const pageSize = Number.isFinite(opts.pageSize) ? opts.pageSize : DEFAULT_PAGE_SIZE;
204
+ const maxPages =
205
+ Number.isInteger(opts.maxPages) && opts.maxPages > 0
206
+ ? opts.maxPages
207
+ : DEFAULT_MAX_PAGES;
208
+ const limit =
209
+ Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : Infinity;
210
+
211
+ let emitted = 0;
212
+ let pageIndex = 1;
213
+ while (pageIndex <= maxPages) {
214
+ const query = { pageIndex, pageSize, ts: Date.now() };
215
+ // sign seam — best-effort. null when no signProvider.
216
+ let sign = null;
217
+ if (this._signProvider) {
218
+ sign = await this._signProvider({ url: this._ordersUrl, query, cookies });
219
+ }
220
+ const resp = await this._fetchFn({ url: this._ordersUrl, cookies, query, sign });
221
+ const orders = extractOrders(resp);
222
+ if (!orders.length) break;
223
+
224
+ let pageHasNew = false;
225
+ let reachedWatermark = false;
226
+ for (const raw of orders) {
227
+ const rec = orderToRecord(raw, { capturedVia: "cookie-api" });
228
+ if (!rec) continue;
229
+ const ts = rec.bookedAt || rec.departureMs || null;
230
+ if (ts && ts < sinceMs) {
231
+ reachedWatermark = true; // remaining orders are older
232
+ break;
233
+ }
234
+ pageHasNew = true;
235
+ if (emitted >= limit) return;
236
+ yield {
237
+ adapter: NAME,
238
+ originalId: rec.recordId,
239
+ capturedAt: ts || Date.now(),
240
+ payload: { record: rec },
241
+ };
242
+ emitted += 1;
243
+ }
244
+ if (reachedWatermark || !pageHasNew || orders.length < pageSize) break;
245
+ pageIndex += 1;
246
+ }
247
+ }
248
+
249
+ normalize(raw) {
250
+ if (!raw || !raw.payload || !raw.payload.record) {
251
+ throw new Error("TongchengAdapter.normalize: raw.payload.record missing");
252
+ }
253
+ return normalizeTravelRecord(raw.payload.record, {
254
+ adapterName: NAME,
255
+ adapterVersion: VERSION,
256
+ });
257
+ }
258
+ }
259
+
260
+ function parseRecords(text) {
261
+ let raw;
262
+ try {
263
+ raw = JSON.parse(text);
264
+ } catch (_e) {
265
+ // Try JSONL
266
+ raw = text
267
+ .split(/\r?\n/)
268
+ .filter((l) => l.trim().startsWith("{"))
269
+ .map((l) => JSON.parse(l));
270
+ }
271
+ const orders = Array.isArray(raw) ? raw : raw.orders || [];
272
+ return orders.map((o) => orderToRecord(o)).filter(Boolean);
273
+ }
274
+
275
+ /**
276
+ * Map one Tongcheng order object → vendor-neutral TravelRecord. Field names are
277
+ * best-effort across H5 versions (camelCase + snake_case + Chinese fallbacks).
278
+ */
279
+ function orderToRecord(o, opts = {}) {
280
+ if (!o || typeof o !== "object") return null;
281
+ const recordId =
282
+ o.orderId || o.orderSerialId || o.serialId || o.id || o.order_no || o.orderNo;
283
+ if (!recordId) return null;
284
+ const type = (
285
+ o.type ||
286
+ o.orderType ||
287
+ o.projectType ||
288
+ o.projectTag ||
289
+ o.bizType ||
290
+ o.businessType ||
291
+ ""
292
+ )
293
+ .toString()
294
+ .toLowerCase();
295
+ const vehicleType = TYPE_MAP[type] || "trip";
296
+
297
+ const priceRaw =
298
+ o.price != null
299
+ ? o.price
300
+ : o.amount != null
301
+ ? o.amount
302
+ : o.orderAmount != null
303
+ ? o.orderAmount
304
+ : o.totalAmount != null
305
+ ? o.totalAmount
306
+ : o.payAmount != null
307
+ ? o.payAmount
308
+ : o.totalPrice != null
309
+ ? o.totalPrice
310
+ : null;
311
+
312
+ return {
313
+ vendorId: "tongcheng",
314
+ recordId: String(recordId),
315
+ vehicleType,
316
+ from:
317
+ o.fromCity || o.from_city || o.depCity || o.departureCity
318
+ ? { city: o.fromCity || o.from_city || o.depCity || o.departureCity }
319
+ : null,
320
+ to:
321
+ o.toCity || o.to_city || o.arrCity || o.arrivalCity || o.hotelCity || o.sceneryName
322
+ ? { city: o.toCity || o.to_city || o.arrCity || o.arrivalCity || o.hotelCity || o.sceneryName }
323
+ : null,
324
+ departureMs: numberOrParse(
325
+ o.departureTime || o.dep_time || o.departureDate || o.useDate || o.checkIn || o.check_in || o.startDate,
326
+ ),
327
+ arrivalMs: numberOrParse(
328
+ o.arrivalTime || o.arr_time || o.arrivalDate || o.checkOut || o.check_out || o.endDate,
329
+ ),
330
+ carrier:
331
+ o.carrier || o.airline || o.airlineName || o.hotelName || o.hotel_name || o.sceneryName || o.title || "同程",
332
+ vehicleNumber: o.flightNumber || o.flight_no || o.trainNumber || o.train_no || o.trainNo,
333
+ totalCost:
334
+ priceRaw != null
335
+ ? { value: parseFloat(priceRaw), currency: o.currency || "CNY" }
336
+ : null,
337
+ traveler:
338
+ o.passengerName || o.passenger || o.guestName || o.guest_name || o.linkName || o.contactName,
339
+ confirmationCode: o.confirmationCode || o.pnr || o.confirmation_no || o.serialId,
340
+ bookedAt: numberOrParse(o.bookedAt || o.order_time || o.orderDate || o.createDate || o.createTime),
341
+ extras: {
342
+ type,
343
+ ...(o.nights != null ? { nights: o.nights } : {}),
344
+ ...(opts.capturedVia ? { capturedVia: opts.capturedVia } : {}),
345
+ },
346
+ };
347
+ }
348
+
349
+ /**
350
+ * Pull the order array out of a Tongcheng order-centre response. Tongcheng
351
+ * nests the list under different keys across H5 versions; the injected fetchFn
352
+ * may also pre-flatten to `{ orders }`. Tolerant of all common shapes.
353
+ */
354
+ function extractOrders(resp) {
355
+ if (!resp || typeof resp !== "object") return [];
356
+ if (Array.isArray(resp.orders)) return resp.orders;
357
+ if (Array.isArray(resp.orderList)) return resp.orderList;
358
+ if (Array.isArray(resp.list)) return resp.list;
359
+ const data = resp.data && typeof resp.data === "object" ? resp.data : null;
360
+ if (data) {
361
+ if (Array.isArray(data.orders)) return data.orders;
362
+ if (Array.isArray(data.orderList)) return data.orderList;
363
+ if (Array.isArray(data.list)) return data.list;
364
+ if (Array.isArray(data.records)) return data.records;
365
+ }
366
+ return [];
367
+ }
368
+
369
+ function numberOrParse(v) {
370
+ if (Number.isFinite(v)) return v;
371
+ if (typeof v === "string") {
372
+ if (/^\d+$/.test(v) && v.length >= 10) return parseInt(v, 10);
373
+ return parseChineseDateTime(v);
374
+ }
375
+ return null;
376
+ }
377
+
378
+ async function defaultFetch(_opts) {
379
+ // Pure-Node has no HTTP layer; the host (Android cc → OkHttp; desktop hub →
380
+ // Electron WebView net) injects a real fetchFn. A missing fetchFn is a wiring
381
+ // bug, not a runtime data condition, so it throws loudly (mirrors travel-ctrip).
382
+ throw new Error("travel-tongcheng: no fetchFn configured for cookie-api mode");
383
+ }
384
+
385
+ module.exports = {
386
+ TongchengAdapter,
387
+ parseRecords,
388
+ orderToRecord,
389
+ extractOrders,
390
+ TYPE_MAP,
391
+ NAME,
392
+ VERSION,
393
+ };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * §13+ — 爱奇艺 (iQiyi, com.qiyi.video) adapter. §12.1 Phase 13+ ROI ⭐⭐
3
+ * "观看历史". Thin wrapper over _video-base.
4
+ *
5
+ * iQiyi exposes the user's watch history + favourites (追剧) via iqiyi.com APIs;
6
+ * this adapter supplies the endpoints + field mapping, the base handles snapshot
7
+ * + cookie-api orchestration + normalize (MEDIA / LIKE event + MEDIA item).
8
+ * Endpoints best-effort + overridable (not field-verified — FAMILY-23 playbook).
9
+ */
10
+
11
+ "use strict";
12
+
13
+ const { createVideoAdapter, parseTime, SNAPSHOT_SCHEMA_VERSION } = require("../_video-base");
14
+
15
+ const NAME = "video-iqiyi";
16
+ const VERSION = "0.1.0";
17
+
18
+ const WATCH_URL = "https://l.rcd.iqiyi.com/apis/rcd/myRC";
19
+ const FAVOURITE_URL = "https://collect.if.iqiyi.com/japi/collect/list";
20
+
21
+ // iQiyi channel/category code → label (best-effort).
22
+ const CHANNEL_MAP = {
23
+ 1: "movie",
24
+ 2: "tv",
25
+ 3: "variety",
26
+ 4: "anime",
27
+ 6: "documentary",
28
+ };
29
+
30
+ function mapCategory(it) {
31
+ const c = it.channelId != null ? it.channelId : it.chnId;
32
+ if (c != null && CHANNEL_MAP[c]) return CHANNEL_MAP[c];
33
+ return it.channelName || it.categoryName || it.category || null;
34
+ }
35
+
36
+ function extractItems(resp) {
37
+ if (!resp || typeof resp !== "object") return [];
38
+ if (Array.isArray(resp.data)) return resp.data;
39
+ if (Array.isArray(resp.list)) return resp.list;
40
+ const d = resp.data;
41
+ if (d && typeof d === "object") {
42
+ if (Array.isArray(d.list)) return d.list;
43
+ if (Array.isArray(d.records)) return d.records;
44
+ if (Array.isArray(d.rc)) return d.rc;
45
+ }
46
+ return [];
47
+ }
48
+
49
+ function mapItem(it) {
50
+ if (!it || typeof it !== "object") return null;
51
+ const videoId = it.tvId || it.tvid || it.albumId || it.qipuId || it.id;
52
+ if (!videoId) return null;
53
+ return {
54
+ videoId: String(videoId),
55
+ title: it.albumName || it.videoName || it.title || it.name || "(未知视频)",
56
+ category: mapCategory(it),
57
+ episode: it.videoName && it.albumName && it.videoName !== it.albumName ? it.videoName : it.order ? `第${it.order}集` : null,
58
+ channel: it.channelName || null,
59
+ durationSec: Number.isFinite(it.videoDuration) ? it.videoDuration : Number.isFinite(it.duration) ? it.duration : null,
60
+ url: it.pageUrl || it.url || (it.tvId ? `https://www.iqiyi.com/v_${it.tvId}.html` : null),
61
+ occurredAt: parseTime(it.addtime || it.playTime || it.updateTime || it.timestamp),
62
+ };
63
+ }
64
+
65
+ const IqiyiVideoAdapter = createVideoAdapter({
66
+ NAME,
67
+ VERSION,
68
+ platform: "iqiyi",
69
+ watchUrl: WATCH_URL,
70
+ favouriteUrl: FAVOURITE_URL,
71
+ extractItems,
72
+ mapItem,
73
+ });
74
+
75
+ module.exports = { IqiyiVideoAdapter, extractItems, mapItem, CHANNEL_MAP, NAME, VERSION, SNAPSHOT_SCHEMA_VERSION };
@@ -0,0 +1,78 @@
1
+ /**
2
+ * §13+ — 腾讯视频 (Tencent Video, com.tencent.qqlive) adapter. §12.1 Phase 13+
3
+ * ROI ⭐⭐ "观看历史". Thin wrapper over _video-base.
4
+ *
5
+ * Tencent Video exposes the user's watch history + 追剧/收藏 via v.qq.com APIs;
6
+ * this adapter supplies the endpoints + field mapping, the base handles snapshot
7
+ * + cookie-api orchestration + normalize. Endpoints best-effort + overridable
8
+ * (not field-verified — FAMILY-23 playbook).
9
+ */
10
+
11
+ "use strict";
12
+
13
+ const { createVideoAdapter, parseTime, SNAPSHOT_SCHEMA_VERSION } = require("../_video-base");
14
+
15
+ const NAME = "video-tencent";
16
+ const VERSION = "0.1.0";
17
+
18
+ const WATCH_URL = "https://pbaccess.video.qq.com/trpc.v...history.HistoryServer/GetHistory";
19
+ const FAVOURITE_URL = "https://pbaccess.video.qq.com/trpc.v...favorite.FavoriteServer/GetFavorite";
20
+
21
+ const TYPE_MAP = {
22
+ 1: "tv",
23
+ 2: "movie",
24
+ 3: "variety",
25
+ 4: "anime",
26
+ 10: "documentary",
27
+ movie: "movie",
28
+ tv: "tv",
29
+ variety: "variety",
30
+ anime: "anime",
31
+ };
32
+
33
+ function mapCategory(it) {
34
+ const raw = it.cTypeId != null ? it.cTypeId : it.typeId != null ? it.typeId : it.category;
35
+ const key = String(raw == null ? "" : raw).toLowerCase();
36
+ return TYPE_MAP[key] || TYPE_MAP[raw] || it.categoryName || null;
37
+ }
38
+
39
+ function extractItems(resp) {
40
+ if (!resp || typeof resp !== "object") return [];
41
+ if (Array.isArray(resp.list)) return resp.list;
42
+ if (Array.isArray(resp.data)) return resp.data;
43
+ const d = resp.data;
44
+ if (d && typeof d === "object") {
45
+ if (Array.isArray(d.list)) return d.list;
46
+ if (Array.isArray(d.records)) return d.records;
47
+ if (Array.isArray(d.videoList)) return d.videoList;
48
+ }
49
+ return [];
50
+ }
51
+
52
+ function mapItem(it) {
53
+ if (!it || typeof it !== "object") return null;
54
+ const videoId = it.cid || it.vid || it.lid || it.id;
55
+ if (!videoId) return null;
56
+ return {
57
+ videoId: String(videoId),
58
+ title: it.cTitle || it.title || it.videoTitle || it.name || "(未知视频)",
59
+ category: mapCategory(it),
60
+ episode: it.episode || it.vTitle || (it.episodeNum ? `第${it.episodeNum}集` : null),
61
+ channel: it.channelName || null,
62
+ durationSec: Number.isFinite(it.duration) ? it.duration : Number.isFinite(it.totalTime) ? it.totalTime : null,
63
+ url: it.url || (it.cid ? `https://v.qq.com/x/cover/${it.cid}.html` : null),
64
+ occurredAt: parseTime(it.viewTime || it.updateTime || it.markTime || it.time),
65
+ };
66
+ }
67
+
68
+ const TencentVideoAdapter = createVideoAdapter({
69
+ NAME,
70
+ VERSION,
71
+ platform: "tencent-video",
72
+ watchUrl: WATCH_URL,
73
+ favouriteUrl: FAVOURITE_URL,
74
+ extractItems,
75
+ mapItem,
76
+ });
77
+
78
+ module.exports = { TencentVideoAdapter, extractItems, mapItem, TYPE_MAP, NAME, VERSION, SNAPSHOT_SCHEMA_VERSION };
package/lib/index.js CHANGED
@@ -39,6 +39,8 @@ const wechatAdapter = require("./adapters/wechat");
39
39
  const travelBase = require("./adapters/travel-base");
40
40
  const { Train12306Adapter } = require("./adapters/travel-12306");
41
41
  const { CtripAdapter } = require("./adapters/travel-ctrip");
42
+ const { TongchengAdapter } = require("./adapters/travel-tongcheng");
43
+ const { DidiAdapter } = require("./adapters/travel-didi");
42
44
  const { AmapAdapter } = require("./adapters/travel-amap");
43
45
  const { BaiduMapAdapter } = require("./adapters/travel-baidu-map");
44
46
  const { TencentMapAdapter } = require("./adapters/travel-tencent-map");
@@ -47,8 +49,12 @@ const { TaobaoAdapter } = require("./adapters/shopping-taobao");
47
49
  const { JdAdapter } = require("./adapters/shopping-jd");
48
50
  const { MeituanAdapter } = require("./adapters/shopping-meituan");
49
51
  const { PinduoduoAdapter } = require("./adapters/shopping-pinduoduo");
52
+ const { DianpingAdapter } = require("./adapters/shopping-dianping");
50
53
  const { BilibiliAdapter } = require("./adapters/social-bilibili");
51
54
  const { WeiboAdapter } = require("./adapters/social-weibo");
55
+ const { ZhihuAdapter } = require("./adapters/social-zhihu");
56
+ const { BossZhipinAdapter } = require("./adapters/recruit-boss");
57
+ const { CsdnAdapter } = require("./adapters/social-csdn");
52
58
  const { DouyinAdapter } = require("./adapters/social-douyin");
53
59
  const { XiaohongshuAdapter } = require("./adapters/social-xiaohongshu");
54
60
  const { ToutiaoAdapter } = require("./adapters/social-toutiao");
@@ -64,7 +70,13 @@ const { WeChatPcAdapter } = require("./adapters/wechat-pc");
64
70
  const { QQPcAdapter } = require("./adapters/qq-pc");
65
71
  const { AppleHealthAdapter } = require("./adapters/apple-health");
66
72
  const { NeteaseMusicAdapter } = require("./adapters/netease-music");
73
+ const { KugouMusicAdapter } = require("./adapters/music-kugou");
74
+ const { IqiyiVideoAdapter } = require("./adapters/video-iqiyi");
75
+ const { TencentVideoAdapter } = require("./adapters/video-tencent");
67
76
  const { WeReadAdapter } = require("./adapters/weread");
77
+ const { WpsDocAdapter } = require("./adapters/doc-wps");
78
+ const { TencentDocsAdapter } = require("./adapters/doc-tencent-docs");
79
+ const { BaiduNetdiskAdapter } = require("./adapters/doc-baidu-netdisk");
68
80
  const { DingTalkPcAdapter } = require("./adapters/dingtalk-pc");
69
81
  const { FeishuPcAdapter } = require("./adapters/feishu-pc");
70
82
  const { TelegramAdapter } = require("./adapters/messaging-telegram");
@@ -264,6 +276,8 @@ module.exports = {
264
276
  parseChineseDateTime: travelBase.parseChineseDateTime,
265
277
  Train12306Adapter,
266
278
  CtripAdapter,
279
+ TongchengAdapter,
280
+ DidiAdapter,
267
281
  AmapAdapter,
268
282
  BaiduMapAdapter,
269
283
  TencentMapAdapter,
@@ -275,10 +289,14 @@ module.exports = {
275
289
  JdAdapter,
276
290
  MeituanAdapter,
277
291
  PinduoduoAdapter,
292
+ DianpingAdapter,
278
293
 
279
294
  // Phase 13+ — long-tail social + messaging (借 sjqz parsers)
280
295
  BilibiliAdapter,
281
296
  WeiboAdapter,
297
+ ZhihuAdapter,
298
+ BossZhipinAdapter,
299
+ CsdnAdapter,
282
300
  DouyinAdapter,
283
301
  XiaohongshuAdapter,
284
302
  ToutiaoAdapter,
@@ -293,7 +311,13 @@ module.exports = {
293
311
  QQPcAdapter,
294
312
  AppleHealthAdapter,
295
313
  NeteaseMusicAdapter,
314
+ KugouMusicAdapter,
315
+ IqiyiVideoAdapter,
316
+ TencentVideoAdapter,
296
317
  WeReadAdapter,
318
+ WpsDocAdapter,
319
+ TencentDocsAdapter,
320
+ BaiduNetdiskAdapter,
297
321
  DingTalkPcAdapter,
298
322
  FeishuPcAdapter,
299
323
  TelegramAdapter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chainlesschain/personal-data-hub",
3
- "version": "0.4.6",
3
+ "version": "0.4.18",
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",