@chainlesschain/personal-data-hub 0.4.23 → 0.4.24
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/bank-family.test.js +125 -0
- package/__tests__/adapters/car-mercedesme.test.js +74 -0
- package/__tests__/adapters/finance-dcep.test.js +74 -0
- package/__tests__/adapters/fitness-joyrun.test.js +82 -0
- package/__tests__/adapters/gov-12123.test.js +103 -0
- package/__tests__/adapters/music-qq.test.js +112 -0
- package/__tests__/adapters/reading-family.test.js +108 -0
- package/__tests__/adapters/travel-didi-consumer.test.js +66 -0
- package/__tests__/audio-ximalaya-snapshot.test.js +279 -0
- package/__tests__/fitness-keep-snapshot.test.js +224 -0
- package/__tests__/shopping-eleme-snapshot.test.js +454 -0
- package/__tests__/shopping-vipshop-snapshot.test.js +425 -0
- package/__tests__/shopping-xianyu-snapshot.test.js +451 -0
- package/__tests__/social-douban-snapshot.test.js +351 -0
- package/lib/adapter-guide.js +19 -1
- package/lib/adapters/_bank-base.js +405 -0
- package/lib/adapters/_reading-base.js +315 -0
- package/lib/adapters/audio-ximalaya/index.js +414 -0
- package/lib/adapters/bank-bankcomm/index.js +27 -0
- package/lib/adapters/bank-boc/index.js +26 -0
- package/lib/adapters/bank-cmbc/index.js +26 -0
- package/lib/adapters/bank-icbc/index.js +27 -0
- package/lib/adapters/car-mercedesme/index.js +225 -0
- package/lib/adapters/finance-dcep/index.js +302 -0
- package/lib/adapters/fitness-joyrun/index.js +295 -0
- package/lib/adapters/fitness-keep/index.js +343 -0
- package/lib/adapters/gov-12123/index.js +391 -0
- package/lib/adapters/music-qq/index.js +372 -0
- package/lib/adapters/reading-fanqie/index.js +61 -0
- package/lib/adapters/reading-qimao/index.js +61 -0
- package/lib/adapters/shopping-eleme/index.js +441 -0
- package/lib/adapters/shopping-vipshop/index.js +429 -0
- package/lib/adapters/shopping-xianyu/index.js +454 -0
- package/lib/adapters/social-douban/index.js +564 -0
- package/lib/adapters/travel-didi-consumer/index.js +148 -0
- package/lib/index.js +36 -0
- package/package.json +1 -1
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 听书/播客 — Ximalaya 喜马拉雅 (com.ximalaya.ting.android) adapter, dual-mode
|
|
3
|
+
* (snapshot + cookie-api). Phase 13+ long-tail (user-requested), ROI ⭐⭐ "听书
|
|
4
|
+
* /播客收听历史".
|
|
5
|
+
*
|
|
6
|
+
* 喜马拉雅 is China's largest audio platform (有声书 / 播客 / 相声 / 课程). Mirrors
|
|
7
|
+
* music-kugou's three-kind shape — play(收听) / favorite(收藏) / subscribe(订阅专辑)
|
|
8
|
+
* — so the vault treats audio listening uniformly alongside music. A new `audio-`
|
|
9
|
+
* category prefix (distinct from music-* for songs and reading-* for novels).
|
|
10
|
+
* Web endpoints are fetched through a generic injected `fetchFn` + optional
|
|
11
|
+
* signProvider seam, keeping this module a pure-Node parser/orchestrator.
|
|
12
|
+
*
|
|
13
|
+
* 1. snapshot mode (opts.inputPath): JSON schemaVersion 1, stateless.
|
|
14
|
+
* 2. cookie-api mode (opts.account.cookies): fetch listen history / favourites /
|
|
15
|
+
* subscriptions from ximalaya web via the injected fetchFn, paginate; sign
|
|
16
|
+
* seam (opts.signProvider) for any anti-bot token; endpoints overridable via
|
|
17
|
+
* opts.*Url (best-effort, NOT field-verified — FAMILY-23 playbook).
|
|
18
|
+
*
|
|
19
|
+
* Snapshot schema (schemaVersion 1):
|
|
20
|
+
* {
|
|
21
|
+
* "schemaVersion": 1, "snapshottedAt": <ms>,
|
|
22
|
+
* "account": { "userId": "...", "name": "..." },
|
|
23
|
+
* "events": [
|
|
24
|
+
* { "kind": "play", "id": "...", "trackId": "...", "title": "...",
|
|
25
|
+
* "anchor": "...", "album": "...", "durationSec": N, "capturedAt": <ms> },
|
|
26
|
+
* { "kind": "favorite", "id": "...", "trackId": "...", "title": "...", "anchor": "..." },
|
|
27
|
+
* { "kind": "subscribe", "id": "...", "albumId": "...", "album": "...",
|
|
28
|
+
* "trackCount": N, "anchor": "..." }
|
|
29
|
+
* ]
|
|
30
|
+
* }
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
"use strict";
|
|
34
|
+
|
|
35
|
+
const fs = require("node:fs");
|
|
36
|
+
const { newId } = require("../../ids");
|
|
37
|
+
const {
|
|
38
|
+
ENTITY_TYPES,
|
|
39
|
+
EVENT_SUBTYPES,
|
|
40
|
+
ITEM_SUBTYPES,
|
|
41
|
+
CAPTURED_BY,
|
|
42
|
+
} = require("../../constants");
|
|
43
|
+
const { CookieAuth } = require("../shopping-base");
|
|
44
|
+
|
|
45
|
+
const NAME = "audio-ximalaya";
|
|
46
|
+
const VERSION = "0.1.0";
|
|
47
|
+
const SNAPSHOT_SCHEMA_VERSION = 1;
|
|
48
|
+
|
|
49
|
+
const KIND_PLAY = "play";
|
|
50
|
+
const KIND_FAVORITE = "favorite";
|
|
51
|
+
const KIND_SUBSCRIBE = "subscribe";
|
|
52
|
+
const VALID_KINDS = Object.freeze([KIND_PLAY, KIND_FAVORITE, KIND_SUBSCRIBE]);
|
|
53
|
+
|
|
54
|
+
// Best-effort Ximalaya web endpoints. Overridable via opts.*Url.
|
|
55
|
+
const PLAYS_URL = "https://www.ximalaya.com/revision/track/history";
|
|
56
|
+
const FAVORITES_URL = "https://www.ximalaya.com/revision/track/favorite/list";
|
|
57
|
+
const SUBSCRIBES_URL = "https://www.ximalaya.com/revision/album/subscribe/list";
|
|
58
|
+
const PAGE_SIZE = 30;
|
|
59
|
+
|
|
60
|
+
function parseTime(v) {
|
|
61
|
+
if (Number.isFinite(v)) return v > 1e12 ? v : v >= 1e9 ? v * 1000 : v;
|
|
62
|
+
if (typeof v === "string") {
|
|
63
|
+
if (/^\d+$/.test(v)) {
|
|
64
|
+
const n = parseInt(v, 10);
|
|
65
|
+
return n > 1e12 ? n : n >= 1e9 ? n * 1000 : n;
|
|
66
|
+
}
|
|
67
|
+
const t = Date.parse(v);
|
|
68
|
+
return Number.isFinite(t) ? t : null;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function stableOriginalId(kind, id) {
|
|
74
|
+
const safe =
|
|
75
|
+
(typeof id === "string" && id.length > 0 && id) ||
|
|
76
|
+
(typeof id === "number" && Number.isFinite(id) && String(id)) ||
|
|
77
|
+
`unknown-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
78
|
+
return `ximalaya:${kind}:${safe}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
class XimalayaAdapter {
|
|
82
|
+
constructor(opts = {}) {
|
|
83
|
+
this.account = opts.account || null;
|
|
84
|
+
this._cookieAuth =
|
|
85
|
+
opts.account && opts.account.cookies
|
|
86
|
+
? new CookieAuth({ platform: "ximalaya", cookies: opts.account.cookies })
|
|
87
|
+
: null;
|
|
88
|
+
this._fetchFn = typeof opts.fetchFn === "function" ? opts.fetchFn : defaultFetch;
|
|
89
|
+
this._signProvider =
|
|
90
|
+
typeof opts.signProvider === "function" ? opts.signProvider : null;
|
|
91
|
+
this._urls = {
|
|
92
|
+
play: opts.playsUrl || PLAYS_URL,
|
|
93
|
+
favorite: opts.favoritesUrl || FAVORITES_URL,
|
|
94
|
+
subscribe: opts.subscribesUrl || SUBSCRIBES_URL,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
this.name = NAME;
|
|
98
|
+
this.version = VERSION;
|
|
99
|
+
this.capabilities = [
|
|
100
|
+
"sync:snapshot",
|
|
101
|
+
"sync:cookie-api",
|
|
102
|
+
"parse:ximalaya-play",
|
|
103
|
+
"parse:ximalaya-favorite",
|
|
104
|
+
"parse:ximalaya-subscribe",
|
|
105
|
+
];
|
|
106
|
+
this.extractMode = "web-api";
|
|
107
|
+
this.rateLimits = {};
|
|
108
|
+
this.dataDisclosure = {
|
|
109
|
+
fields: [
|
|
110
|
+
"ximalaya:play (声音标题 / 主播 / 专辑)",
|
|
111
|
+
"ximalaya:favorite (收藏的声音)",
|
|
112
|
+
"ximalaya:subscribe (订阅专辑名 / 集数)",
|
|
113
|
+
],
|
|
114
|
+
sensitivity: "low",
|
|
115
|
+
legalGate: false,
|
|
116
|
+
defaultInclude: { play: true, favorite: true, subscribe: true },
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
this._deps = { fs };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async authenticate(ctx = {}) {
|
|
123
|
+
if (ctx && typeof ctx.inputPath === "string" && ctx.inputPath.length > 0) {
|
|
124
|
+
try {
|
|
125
|
+
this._deps.fs.accessSync(ctx.inputPath, this._deps.fs.constants.R_OK);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
reason: "INPUT_PATH_UNREADABLE",
|
|
130
|
+
message: `snapshot not readable at ${ctx.inputPath}: ${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) return { ok: false, reason: "INVALID_COOKIE", error: "cookies missing" };
|
|
138
|
+
return {
|
|
139
|
+
ok: true,
|
|
140
|
+
account: (this.account && this.account.userId) || null,
|
|
141
|
+
mode: "cookie",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
reason: "NO_INPUT",
|
|
147
|
+
message: "audio-ximalaya.authenticate: needs opts.inputPath (snapshot mode) OR opts.account.cookies (cookie-api mode)",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async healthCheck() {
|
|
152
|
+
if (this._cookieAuth) {
|
|
153
|
+
const r = await this.authenticate();
|
|
154
|
+
return r.ok ? { ok: true, lastChecked: Date.now() } : { ok: false, reason: r.reason, error: r.error };
|
|
155
|
+
}
|
|
156
|
+
return { ok: true, lastChecked: Date.now() };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async *sync(opts = {}) {
|
|
160
|
+
if (typeof opts.inputPath === "string" && opts.inputPath.length > 0) {
|
|
161
|
+
yield* this._syncViaSnapshot(opts);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (this._cookieAuth) {
|
|
165
|
+
yield* this._syncViaCookie(opts);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
throw new Error(
|
|
169
|
+
"audio-ximalaya.sync: needs opts.inputPath (snapshot mode) OR opts.account.cookies (cookie-api mode; ximalaya endpoints may need a sign via opts.signProvider)",
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async *_syncViaSnapshot(opts) {
|
|
174
|
+
const raw = this._deps.fs.readFileSync(opts.inputPath, "utf-8");
|
|
175
|
+
let snapshot;
|
|
176
|
+
try {
|
|
177
|
+
snapshot = JSON.parse(raw);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
throw new Error(`audio-ximalaya.sync: snapshot must be JSON. Got parse error: ${err.message}`);
|
|
180
|
+
}
|
|
181
|
+
if (!snapshot || typeof snapshot !== "object" || snapshot.schemaVersion !== SNAPSHOT_SCHEMA_VERSION) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`audio-ximalaya.sync: snapshot schemaVersion mismatch (got ${snapshot && snapshot.schemaVersion}, expected ${SNAPSHOT_SCHEMA_VERSION})`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const fallback =
|
|
187
|
+
Number.isFinite(snapshot.snapshottedAt) && snapshot.snapshottedAt > 0
|
|
188
|
+
? Math.floor(snapshot.snapshottedAt)
|
|
189
|
+
: Date.now();
|
|
190
|
+
const account = snapshot.account && typeof snapshot.account === "object" ? snapshot.account : null;
|
|
191
|
+
const include = opts.include || {};
|
|
192
|
+
const limit = Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : Infinity;
|
|
193
|
+
const events = Array.isArray(snapshot.events) ? snapshot.events : [];
|
|
194
|
+
let emitted = 0;
|
|
195
|
+
for (const ev of events) {
|
|
196
|
+
if (emitted >= limit) return;
|
|
197
|
+
if (!ev || typeof ev !== "object" || !VALID_KINDS.includes(ev.kind)) continue;
|
|
198
|
+
if (include[ev.kind] === false) continue;
|
|
199
|
+
const id = (typeof ev.id === "string" && ev.id) || ev.trackId || ev.albumId || null;
|
|
200
|
+
yield {
|
|
201
|
+
adapter: NAME,
|
|
202
|
+
kind: ev.kind,
|
|
203
|
+
originalId: stableOriginalId(ev.kind, id),
|
|
204
|
+
capturedAt: parseTime(ev.capturedAt) || fallback,
|
|
205
|
+
payload: { ...ev, account },
|
|
206
|
+
};
|
|
207
|
+
emitted += 1;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async *_syncViaCookie(opts = {}) {
|
|
212
|
+
if (!(await this._cookieAuth.validate())) return;
|
|
213
|
+
const cookies = this._cookieAuth.toHeader();
|
|
214
|
+
const include = opts.include || {};
|
|
215
|
+
const limit = Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : Infinity;
|
|
216
|
+
const maxPages = Number.isInteger(opts.maxPages) && opts.maxPages > 0 ? opts.maxPages : 10;
|
|
217
|
+
|
|
218
|
+
const plan = [
|
|
219
|
+
{ kind: KIND_PLAY, url: this._urls.play, map: trackItemToRecord },
|
|
220
|
+
{ kind: KIND_FAVORITE, url: this._urls.favorite, map: trackItemToRecord },
|
|
221
|
+
{ kind: KIND_SUBSCRIBE, url: this._urls.subscribe, map: albumItemToRecord },
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
let emitted = 0;
|
|
225
|
+
for (const step of plan) {
|
|
226
|
+
if (include[step.kind] === false) continue;
|
|
227
|
+
let page = 1;
|
|
228
|
+
while (page <= maxPages) {
|
|
229
|
+
const query = { page, pageSize: PAGE_SIZE };
|
|
230
|
+
let sign = null;
|
|
231
|
+
if (this._signProvider) {
|
|
232
|
+
sign = await this._signProvider({ url: step.url, query, cookies });
|
|
233
|
+
}
|
|
234
|
+
const resp = await this._fetchFn({ url: step.url, cookies, query, sign });
|
|
235
|
+
const items = extractList(resp);
|
|
236
|
+
if (!items.length) break;
|
|
237
|
+
for (const it of items) {
|
|
238
|
+
const rec = step.map(it);
|
|
239
|
+
if (!rec) continue;
|
|
240
|
+
if (emitted >= limit) return;
|
|
241
|
+
yield {
|
|
242
|
+
adapter: NAME,
|
|
243
|
+
kind: step.kind,
|
|
244
|
+
originalId: stableOriginalId(step.kind, rec.id),
|
|
245
|
+
capturedAt: rec.occurredAt || Date.now(),
|
|
246
|
+
payload: { ...rec, kind: step.kind, cookie: true },
|
|
247
|
+
};
|
|
248
|
+
emitted += 1;
|
|
249
|
+
}
|
|
250
|
+
if (items.length < PAGE_SIZE) break;
|
|
251
|
+
page += 1;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
normalize(raw) {
|
|
257
|
+
if (!raw || !raw.payload) throw new Error("XimalayaAdapter.normalize: payload missing");
|
|
258
|
+
const kind = raw.kind || raw.payload.kind;
|
|
259
|
+
const ingestedAt = Date.now();
|
|
260
|
+
if (kind === KIND_PLAY) return normalizeTrack(raw.payload, raw, ingestedAt, EVENT_SUBTYPES.MEDIA, "收听");
|
|
261
|
+
if (kind === KIND_FAVORITE) return normalizeTrack(raw.payload, raw, ingestedAt, EVENT_SUBTYPES.LIKE, "收藏");
|
|
262
|
+
if (kind === KIND_SUBSCRIBE) return normalizeSubscribe(raw.payload, raw, ingestedAt);
|
|
263
|
+
throw new Error(`XimalayaAdapter.normalize: unknown kind ${kind}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ─── cookie response → intermediate record ───────────────────────────────────
|
|
268
|
+
|
|
269
|
+
function extractList(resp) {
|
|
270
|
+
if (!resp || typeof resp !== "object") return [];
|
|
271
|
+
if (Array.isArray(resp.list)) return resp.list;
|
|
272
|
+
if (Array.isArray(resp.data)) return resp.data;
|
|
273
|
+
const d = resp.data;
|
|
274
|
+
if (d && typeof d === "object") {
|
|
275
|
+
if (Array.isArray(d.list)) return d.list;
|
|
276
|
+
if (Array.isArray(d.tracks)) return d.tracks;
|
|
277
|
+
if (Array.isArray(d.albums)) return d.albums;
|
|
278
|
+
if (Array.isArray(d.albumsInfo)) return d.albumsInfo;
|
|
279
|
+
}
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function trackItemToRecord(it) {
|
|
284
|
+
if (!it || typeof it !== "object") return null;
|
|
285
|
+
const id = it.trackId || it.track_id || it.id;
|
|
286
|
+
if (!id) return null;
|
|
287
|
+
return {
|
|
288
|
+
id: String(id),
|
|
289
|
+
trackId: String(id),
|
|
290
|
+
title: it.title || it.trackTitle || it.track_title || it.name || "(未知声音)",
|
|
291
|
+
anchor: it.nickname || it.anchorName || it.anchor_name || it.anchor || it.nickName || null,
|
|
292
|
+
album: it.albumTitle || it.album_title || it.albumName || it.album || null,
|
|
293
|
+
durationSec: Number.isFinite(it.duration) ? it.duration : Number.isFinite(it.durationSec) ? it.durationSec : null,
|
|
294
|
+
occurredAt: parseTime(it.startedAt || it.playedAt || it.updateTime || it.update_time || it.createTime || it.timestamp),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function albumItemToRecord(it) {
|
|
299
|
+
if (!it || typeof it !== "object") return null;
|
|
300
|
+
const id = it.albumId || it.album_id || it.id;
|
|
301
|
+
if (!id) return null;
|
|
302
|
+
return {
|
|
303
|
+
id: String(id),
|
|
304
|
+
albumId: String(id),
|
|
305
|
+
album: it.albumTitle || it.album_title || it.title || it.name || "(未命名专辑)",
|
|
306
|
+
trackCount:
|
|
307
|
+
it.includeTrackCount != null ? it.includeTrackCount
|
|
308
|
+
: it.tracks != null ? it.tracks
|
|
309
|
+
: it.trackCount != null ? it.trackCount
|
|
310
|
+
: null,
|
|
311
|
+
anchor: it.nickname || it.anchorName || it.anchor_name || it.anchor || null,
|
|
312
|
+
occurredAt: parseTime(it.subscribeTime || it.subscribe_time || it.createTime || it.updateTime),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ─── normalizers (mirror music-kugou) ────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
function buildSource(raw, occurredAt) {
|
|
319
|
+
return {
|
|
320
|
+
adapter: NAME,
|
|
321
|
+
adapterVersion: VERSION,
|
|
322
|
+
originalId: raw.originalId,
|
|
323
|
+
capturedAt: raw.capturedAt || occurredAt,
|
|
324
|
+
capturedBy: CAPTURED_BY.API,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function normalizeTrack(p, raw, ingestedAt, subtype, verb) {
|
|
329
|
+
const occurredAt = parseTime(p.occurredAt || p.capturedAt) || raw.capturedAt || ingestedAt;
|
|
330
|
+
const source = buildSource(raw, occurredAt);
|
|
331
|
+
const title = p.title || "(未知声音)";
|
|
332
|
+
const anchor = p.anchor || "";
|
|
333
|
+
const trackId = p.trackId != null ? String(p.trackId) : null;
|
|
334
|
+
const itemId = trackId ? `item-ximalaya-track-${trackId}` : `item-ximalaya-track-${newId()}`;
|
|
335
|
+
return {
|
|
336
|
+
events: [
|
|
337
|
+
{
|
|
338
|
+
id: newId(),
|
|
339
|
+
type: ENTITY_TYPES.EVENT,
|
|
340
|
+
subtype,
|
|
341
|
+
occurredAt,
|
|
342
|
+
actor: "person-self",
|
|
343
|
+
content: { title: `${verb}: ${title}${anchor ? " - " + anchor : ""}`, text: `${title} ${anchor}`.trim() },
|
|
344
|
+
ingestedAt,
|
|
345
|
+
source,
|
|
346
|
+
extra: {
|
|
347
|
+
platform: "ximalaya",
|
|
348
|
+
title,
|
|
349
|
+
anchor,
|
|
350
|
+
album: p.album || null,
|
|
351
|
+
trackId,
|
|
352
|
+
durationSec: p.durationSec != null ? p.durationSec : null,
|
|
353
|
+
itemRef: itemId,
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
items: [
|
|
358
|
+
{
|
|
359
|
+
id: itemId,
|
|
360
|
+
type: ENTITY_TYPES.ITEM,
|
|
361
|
+
subtype: ITEM_SUBTYPES.MEDIA,
|
|
362
|
+
name: anchor ? `${title} - ${anchor}` : title,
|
|
363
|
+
ingestedAt,
|
|
364
|
+
source,
|
|
365
|
+
extra: { platform: "ximalaya", kind: "track", title, anchor, album: p.album || null, trackId },
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
persons: [],
|
|
369
|
+
places: [],
|
|
370
|
+
topics: [],
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function normalizeSubscribe(p, raw, ingestedAt) {
|
|
375
|
+
const occurredAt = parseTime(p.occurredAt || p.capturedAt) || raw.capturedAt || ingestedAt;
|
|
376
|
+
const source = buildSource(raw, occurredAt);
|
|
377
|
+
const aid = p.albumId != null ? String(p.albumId) : null;
|
|
378
|
+
return {
|
|
379
|
+
events: [],
|
|
380
|
+
persons: [],
|
|
381
|
+
places: [],
|
|
382
|
+
items: [],
|
|
383
|
+
topics: [
|
|
384
|
+
{
|
|
385
|
+
id: aid ? `topic-ximalaya-album-${aid}` : `topic-ximalaya-album-${newId()}`,
|
|
386
|
+
type: ENTITY_TYPES.TOPIC,
|
|
387
|
+
name: p.album || "(未命名专辑)",
|
|
388
|
+
ingestedAt,
|
|
389
|
+
source,
|
|
390
|
+
extra: {
|
|
391
|
+
platform: "ximalaya",
|
|
392
|
+
albumId: aid,
|
|
393
|
+
trackCount: p.trackCount != null ? p.trackCount : null,
|
|
394
|
+
anchor: p.anchor || null,
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function defaultFetch(_opts) {
|
|
402
|
+
throw new Error("audio-ximalaya: no fetchFn configured for cookie-api mode");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
module.exports = {
|
|
406
|
+
XimalayaAdapter,
|
|
407
|
+
extractList,
|
|
408
|
+
trackItemToRecord,
|
|
409
|
+
albumItemToRecord,
|
|
410
|
+
NAME,
|
|
411
|
+
VERSION,
|
|
412
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
413
|
+
VALID_KINDS,
|
|
414
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §12.1 Phase 13+ ⭐⭐⭐ — 交通银行 (Bank of Communications,
|
|
3
|
+
* com.bankcomm.maidanba) adapter, "交易明细 + 信用卡". BEST-EFFORT SCAFFOLD
|
|
4
|
+
* (user-requested).
|
|
5
|
+
*
|
|
6
|
+
* Thin wrapper over _bank-base. ⚠️ MAXIMALLY SENSITIVE (real-name banking,
|
|
7
|
+
* strong-auth). Endpoints FABRICATED placeholders (overridable, NOT
|
|
8
|
+
* field-verified — FAMILY-23 playbook); snapshot mode is the reliable path,
|
|
9
|
+
* cookie path surfaces auth.unverified=true. Gated high sensitivity + legalGate.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
const { createBankAdapter, SNAPSHOT_SCHEMA_VERSION } = require("../_bank-base");
|
|
15
|
+
|
|
16
|
+
const NAME = "bank-bankcomm";
|
|
17
|
+
const VERSION = "0.1.0";
|
|
18
|
+
|
|
19
|
+
const BankcommBankAdapter = createBankAdapter({
|
|
20
|
+
NAME,
|
|
21
|
+
VERSION,
|
|
22
|
+
platform: "bankcomm",
|
|
23
|
+
defaultTxUrl: "https://m.bankcomm.com/api/v1/account/transactions",
|
|
24
|
+
defaultCardUrl: "https://m.bankcomm.com/api/v1/creditcard/bills",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
module.exports = { BankcommBankAdapter, NAME, VERSION, SNAPSHOT_SCHEMA_VERSION };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §12.1 Phase 13+ ⭐⭐⭐ — 中国银行 (Bank of China, com.chinamworld.bocmbci)
|
|
3
|
+
* adapter, "交易明细 + 信用卡". BEST-EFFORT SCAFFOLD (user-requested).
|
|
4
|
+
*
|
|
5
|
+
* Thin wrapper over _bank-base. ⚠️ MAXIMALLY SENSITIVE (real-name banking,
|
|
6
|
+
* strong-auth). Endpoints FABRICATED placeholders (overridable, NOT
|
|
7
|
+
* field-verified — FAMILY-23 playbook); snapshot mode is the reliable path,
|
|
8
|
+
* cookie path surfaces auth.unverified=true. Gated high sensitivity + legalGate.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
const { createBankAdapter, SNAPSHOT_SCHEMA_VERSION } = require("../_bank-base");
|
|
14
|
+
|
|
15
|
+
const NAME = "bank-boc";
|
|
16
|
+
const VERSION = "0.1.0";
|
|
17
|
+
|
|
18
|
+
const BocBankAdapter = createBankAdapter({
|
|
19
|
+
NAME,
|
|
20
|
+
VERSION,
|
|
21
|
+
platform: "boc",
|
|
22
|
+
defaultTxUrl: "https://ebsnew.boc.cn/api/v1/account/transactions",
|
|
23
|
+
defaultCardUrl: "https://ebsnew.boc.cn/api/v1/creditcard/bills",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
module.exports = { BocBankAdapter, NAME, VERSION, SNAPSHOT_SCHEMA_VERSION };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §12.1 Phase 13+ ⭐⭐⭐ — 民生银行 (China Minsheng Bank, com.com.cmbc.newmbank)
|
|
3
|
+
* adapter, "交易明细 + 信用卡". BEST-EFFORT SCAFFOLD (user-requested).
|
|
4
|
+
*
|
|
5
|
+
* Thin wrapper over _bank-base. ⚠️ MAXIMALLY SENSITIVE (real-name banking,
|
|
6
|
+
* strong-auth). Endpoints FABRICATED placeholders (overridable, NOT
|
|
7
|
+
* field-verified — FAMILY-23 playbook); snapshot mode is the reliable path,
|
|
8
|
+
* cookie path surfaces auth.unverified=true. Gated high sensitivity + legalGate.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
const { createBankAdapter, SNAPSHOT_SCHEMA_VERSION } = require("../_bank-base");
|
|
14
|
+
|
|
15
|
+
const NAME = "bank-cmbc";
|
|
16
|
+
const VERSION = "0.1.0";
|
|
17
|
+
|
|
18
|
+
const CmbcBankAdapter = createBankAdapter({
|
|
19
|
+
NAME,
|
|
20
|
+
VERSION,
|
|
21
|
+
platform: "cmbc",
|
|
22
|
+
defaultTxUrl: "https://mbank.cmbc.com.cn/api/v1/account/transactions",
|
|
23
|
+
defaultCardUrl: "https://mbank.cmbc.com.cn/api/v1/creditcard/bills",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
module.exports = { CmbcBankAdapter, NAME, VERSION, SNAPSHOT_SCHEMA_VERSION };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §12.1 Phase 13+ — 工商银行 (ICBC, com.icbc) adapter, "交易明细 + 信用卡".
|
|
3
|
+
* BEST-EFFORT SCAFFOLD. Discovered as a device-installed gap (2026-06-15) —
|
|
4
|
+
* not in the original §12.1 bank list (民生/中行/交行), added for completeness.
|
|
5
|
+
*
|
|
6
|
+
* Thin wrapper over _bank-base. ⚠️ MAXIMALLY SENSITIVE (real-name banking,
|
|
7
|
+
* strong-auth). Endpoints FABRICATED placeholders (overridable, NOT
|
|
8
|
+
* field-verified — FAMILY-23 playbook); snapshot mode is the reliable path,
|
|
9
|
+
* cookie path surfaces auth.unverified=true. Gated high sensitivity + legalGate.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
const { createBankAdapter, SNAPSHOT_SCHEMA_VERSION } = require("../_bank-base");
|
|
15
|
+
|
|
16
|
+
const NAME = "bank-icbc";
|
|
17
|
+
const VERSION = "0.1.0";
|
|
18
|
+
|
|
19
|
+
const IcbcBankAdapter = createBankAdapter({
|
|
20
|
+
NAME,
|
|
21
|
+
VERSION,
|
|
22
|
+
platform: "icbc",
|
|
23
|
+
defaultTxUrl: "https://mybank.icbc.com.cn/api/v1/account/transactions",
|
|
24
|
+
defaultCardUrl: "https://mybank.icbc.com.cn/api/v1/creditcard/bills",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
module.exports = { IcbcBankAdapter, NAME, VERSION, SNAPSHOT_SCHEMA_VERSION };
|