@chainlesschain/personal-data-hub 0.4.7 → 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.
- package/__tests__/adapters/doc-baidu-netdisk.test.js +102 -0
- package/__tests__/adapters/doc-platforms.test.js +177 -0
- package/__tests__/adapters/music-kugou.test.js +187 -0
- package/__tests__/adapters/recruit-boss.test.js +180 -0
- package/__tests__/adapters/shopping-dianping.test.js +239 -0
- package/__tests__/adapters/social-csdn.test.js +175 -0
- package/__tests__/adapters/social-zhihu.test.js +246 -0
- package/__tests__/adapters/travel-ctrip.test.js +175 -1
- package/__tests__/adapters/travel-didi.test.js +204 -0
- package/__tests__/adapters/travel-tongcheng.test.js +289 -0
- package/__tests__/adapters/video-platforms.test.js +152 -0
- package/lib/adapter-guide.js +13 -1
- package/lib/adapters/_document-base.js +370 -0
- package/lib/adapters/_video-base.js +331 -0
- package/lib/adapters/doc-baidu-netdisk/index.js +91 -0
- package/lib/adapters/doc-tencent-docs/index.js +94 -0
- package/lib/adapters/doc-wps/index.js +77 -0
- package/lib/adapters/music-kugou/index.js +418 -0
- package/lib/adapters/recruit-boss/index.js +442 -0
- package/lib/adapters/shopping-dianping/index.js +473 -0
- package/lib/adapters/social-csdn/index.js +444 -0
- package/lib/adapters/social-zhihu/index.js +488 -0
- package/lib/adapters/travel-ctrip/index.js +255 -40
- package/lib/adapters/travel-didi/index.js +327 -0
- package/lib/adapters/travel-tongcheng/index.js +393 -0
- package/lib/adapters/video-iqiyi/index.js +75 -0
- package/lib/adapters/video-tencent/index.js +78 -0
- package/lib/index.js +24 -0
- package/package.json +1 -1
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* _video-base — shared infrastructure for "video watch-history" adapters
|
|
3
|
+
* (爱奇艺 / 腾讯视频 / etc.), Phase 13+ §12.1 (ROI ⭐⭐ each).
|
|
4
|
+
*
|
|
5
|
+
* These platforms expose the same shape of personal data: a paginated list of
|
|
6
|
+
* videos the user watched (观看记录) + optionally favourited/追剧 (收藏). Rather
|
|
7
|
+
* than copy ~300 lines per platform (mirroring _document-base / shopping-base /
|
|
8
|
+
* travel-base), `createVideoAdapter(config)` returns a fully-formed adapter
|
|
9
|
+
* class with snapshot + cookie-api modes; each platform supplies only its
|
|
10
|
+
* endpoints + field mapping.
|
|
11
|
+
*
|
|
12
|
+
* 1. snapshot mode (opts.inputPath): JSON schemaVersion 1, stateless.
|
|
13
|
+
* 2. cookie-api mode (opts.account.cookies): fetch watch / favourite lists via
|
|
14
|
+
* the injected `fetchFn` (Android in-APK cc → OkHttp; desktop hub →
|
|
15
|
+
* Electron WebView net request), paginate. A sign seam (opts.signProvider)
|
|
16
|
+
* covers anti-bot tokens; best-effort unsigned. Endpoints overridable via
|
|
17
|
+
* opts.watchUrl / opts.favouriteUrl (best-effort, not field-verified —
|
|
18
|
+
* FAMILY-23 playbook).
|
|
19
|
+
*
|
|
20
|
+
* normalize() emits, per item: a MEDIA event (watch) or LIKE event (favourite)
|
|
21
|
+
* + a MEDIA item, mirroring netease-music / music-kugou so the vault can both
|
|
22
|
+
* timeline "我看了 X" and list the video entity.
|
|
23
|
+
*
|
|
24
|
+
* Snapshot schema (schemaVersion 1):
|
|
25
|
+
* {
|
|
26
|
+
* "schemaVersion": 1, "snapshottedAt": <ms>,
|
|
27
|
+
* "account": { "userId": "...", "name": "..." },
|
|
28
|
+
* "events": [
|
|
29
|
+
* { "kind": "watch", "id": "...", "videoId": "...", "title": "...",
|
|
30
|
+
* "category": "movie|tv|variety|anime|...", "episode": "...",
|
|
31
|
+
* "channel": "...", "durationSec": N, "capturedAt": <s|ms> },
|
|
32
|
+
* { "kind": "favourite", "id": "...", "videoId": "...", "title": "...",
|
|
33
|
+
* "category": "...", "capturedAt": <ms> }
|
|
34
|
+
* ]
|
|
35
|
+
* }
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
"use strict";
|
|
39
|
+
|
|
40
|
+
const fs = require("node:fs");
|
|
41
|
+
const { newId } = require("../ids");
|
|
42
|
+
const {
|
|
43
|
+
ENTITY_TYPES,
|
|
44
|
+
EVENT_SUBTYPES,
|
|
45
|
+
ITEM_SUBTYPES,
|
|
46
|
+
CAPTURED_BY,
|
|
47
|
+
} = require("../constants");
|
|
48
|
+
|
|
49
|
+
const SNAPSHOT_SCHEMA_VERSION = 1;
|
|
50
|
+
const KIND_WATCH = "watch";
|
|
51
|
+
const KIND_FAVOURITE = "favourite";
|
|
52
|
+
const VALID_SNAPSHOT_KINDS = Object.freeze([KIND_WATCH, KIND_FAVOURITE]);
|
|
53
|
+
const PAGE_SIZE = 30;
|
|
54
|
+
|
|
55
|
+
function parseTime(v) {
|
|
56
|
+
if (Number.isFinite(v)) return v > 1e12 ? v : v >= 1e9 ? v * 1000 : v;
|
|
57
|
+
if (typeof v === "string") {
|
|
58
|
+
if (/^\d+$/.test(v)) {
|
|
59
|
+
const n = parseInt(v, 10);
|
|
60
|
+
return n > 1e12 ? n : n >= 1e9 ? n * 1000 : n;
|
|
61
|
+
}
|
|
62
|
+
const t = Date.parse(v);
|
|
63
|
+
return Number.isFinite(t) ? t : null;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {object} config
|
|
70
|
+
* @param {string} config.NAME e.g. "video-iqiyi"
|
|
71
|
+
* @param {string} config.VERSION
|
|
72
|
+
* @param {string} config.platform e.g. "iqiyi"
|
|
73
|
+
* @param {string} config.watchUrl best-effort watch-history endpoint
|
|
74
|
+
* @param {string} config.favouriteUrl best-effort favourite/追剧 endpoint
|
|
75
|
+
* @param {(resp:any)=>any[]} config.extractItems
|
|
76
|
+
* @param {(raw:any)=>object|null} config.mapItem
|
|
77
|
+
* VideoRecord = { videoId, title, category, episode, channel, durationSec, url, occurredAt? }
|
|
78
|
+
*/
|
|
79
|
+
function createVideoAdapter(config) {
|
|
80
|
+
const { NAME, VERSION, platform, watchUrl, favouriteUrl, extractItems, mapItem } = config;
|
|
81
|
+
const { CookieAuth } = require("./shopping-base");
|
|
82
|
+
|
|
83
|
+
function stableOriginalId(kind, id) {
|
|
84
|
+
const safe =
|
|
85
|
+
(typeof id === "string" && id.length > 0 && id) ||
|
|
86
|
+
(typeof id === "number" && Number.isFinite(id) && String(id)) ||
|
|
87
|
+
`unknown-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
88
|
+
return `${platform}:${kind}:${safe}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class VideoAdapter {
|
|
92
|
+
constructor(opts = {}) {
|
|
93
|
+
this.account = opts.account || null;
|
|
94
|
+
this._cookieAuth =
|
|
95
|
+
opts.account && opts.account.cookies
|
|
96
|
+
? new CookieAuth({ platform, cookies: opts.account.cookies })
|
|
97
|
+
: null;
|
|
98
|
+
this._fetchFn = typeof opts.fetchFn === "function" ? opts.fetchFn : defaultFetch;
|
|
99
|
+
this._signProvider =
|
|
100
|
+
typeof opts.signProvider === "function" ? opts.signProvider : null;
|
|
101
|
+
this._urls = {
|
|
102
|
+
watch: opts.watchUrl || watchUrl,
|
|
103
|
+
favourite: opts.favouriteUrl || favouriteUrl,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
this.name = NAME;
|
|
107
|
+
this.version = VERSION;
|
|
108
|
+
this.capabilities = ["sync:snapshot", "sync:cookie-api", `parse:${platform}-watch`, `parse:${platform}-favourite`];
|
|
109
|
+
this.extractMode = "web-api";
|
|
110
|
+
this.rateLimits = {};
|
|
111
|
+
this.dataDisclosure = {
|
|
112
|
+
fields: [`${platform}:watch (title / category / episode / channel)`, `${platform}:favourite (title / category)`],
|
|
113
|
+
sensitivity: "low",
|
|
114
|
+
legalGate: false,
|
|
115
|
+
defaultInclude: { watch: true, favourite: true },
|
|
116
|
+
};
|
|
117
|
+
this._deps = { fs };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async authenticate(ctx = {}) {
|
|
121
|
+
if (ctx && typeof ctx.inputPath === "string" && ctx.inputPath.length > 0) {
|
|
122
|
+
try {
|
|
123
|
+
this._deps.fs.accessSync(ctx.inputPath, this._deps.fs.constants.R_OK);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
return { ok: false, reason: "INPUT_PATH_UNREADABLE", message: `snapshot not readable at ${ctx.inputPath}: ${err.message}` };
|
|
126
|
+
}
|
|
127
|
+
return { ok: true, mode: "snapshot-file" };
|
|
128
|
+
}
|
|
129
|
+
if (this._cookieAuth) {
|
|
130
|
+
const ok = await this._cookieAuth.validate();
|
|
131
|
+
if (!ok) return { ok: false, reason: "INVALID_COOKIE", error: "cookies missing" };
|
|
132
|
+
return { ok: true, account: (this.account && this.account.userId) || null, mode: "cookie" };
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
reason: "NO_INPUT",
|
|
137
|
+
message: `${NAME}.authenticate: needs opts.inputPath (snapshot mode) OR opts.account.cookies (cookie-api mode)`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async healthCheck() {
|
|
142
|
+
if (this._cookieAuth) {
|
|
143
|
+
const r = await this.authenticate();
|
|
144
|
+
return r.ok ? { ok: true, lastChecked: Date.now() } : { ok: false, reason: r.reason, error: r.error };
|
|
145
|
+
}
|
|
146
|
+
return { ok: true, lastChecked: Date.now() };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async *sync(opts = {}) {
|
|
150
|
+
if (typeof opts.inputPath === "string" && opts.inputPath.length > 0) {
|
|
151
|
+
yield* this._syncViaSnapshot(opts);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (this._cookieAuth) {
|
|
155
|
+
yield* this._syncViaCookie(opts);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
throw new Error(`${NAME}.sync: needs opts.inputPath (snapshot mode) OR opts.account.cookies (cookie-api mode)`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async *_syncViaSnapshot(opts) {
|
|
162
|
+
const raw = this._deps.fs.readFileSync(opts.inputPath, "utf-8");
|
|
163
|
+
const snapshot = JSON.parse(raw);
|
|
164
|
+
if (!snapshot || typeof snapshot !== "object" || snapshot.schemaVersion !== SNAPSHOT_SCHEMA_VERSION) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`${NAME}.sync: snapshot schemaVersion mismatch (got ${snapshot && snapshot.schemaVersion}, expected ${SNAPSHOT_SCHEMA_VERSION})`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const fallback =
|
|
170
|
+
Number.isFinite(snapshot.snapshottedAt) && snapshot.snapshottedAt > 0
|
|
171
|
+
? Math.floor(snapshot.snapshottedAt)
|
|
172
|
+
: Date.now();
|
|
173
|
+
const account = snapshot.account && typeof snapshot.account === "object" ? snapshot.account : null;
|
|
174
|
+
const include = opts.include || {};
|
|
175
|
+
const limit = Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : Infinity;
|
|
176
|
+
const events = Array.isArray(snapshot.events) ? snapshot.events : [];
|
|
177
|
+
let emitted = 0;
|
|
178
|
+
for (const ev of events) {
|
|
179
|
+
if (emitted >= limit) return;
|
|
180
|
+
if (!ev || typeof ev !== "object" || !VALID_SNAPSHOT_KINDS.includes(ev.kind)) continue;
|
|
181
|
+
if (include[ev.kind] === false) continue;
|
|
182
|
+
const id = (typeof ev.id === "string" && ev.id) || ev.videoId || null;
|
|
183
|
+
yield {
|
|
184
|
+
adapter: NAME,
|
|
185
|
+
kind: ev.kind,
|
|
186
|
+
originalId: stableOriginalId(ev.kind, id),
|
|
187
|
+
capturedAt: parseTime(ev.capturedAt) || fallback,
|
|
188
|
+
payload: { record: snapshotEventToRecord(ev), kind: ev.kind, account },
|
|
189
|
+
};
|
|
190
|
+
emitted += 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async *_syncViaCookie(opts = {}) {
|
|
195
|
+
if (!(await this._cookieAuth.validate())) return;
|
|
196
|
+
const cookies = this._cookieAuth.toHeader();
|
|
197
|
+
const include = opts.include || {};
|
|
198
|
+
const limit = Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : Infinity;
|
|
199
|
+
const maxPages = Number.isInteger(opts.maxPages) && opts.maxPages > 0 ? opts.maxPages : 10;
|
|
200
|
+
|
|
201
|
+
const plan = [
|
|
202
|
+
{ kind: KIND_WATCH, url: this._urls.watch },
|
|
203
|
+
{ kind: KIND_FAVOURITE, url: this._urls.favourite },
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
let emitted = 0;
|
|
207
|
+
for (const step of plan) {
|
|
208
|
+
if (include[step.kind] === false) continue;
|
|
209
|
+
if (!step.url) continue;
|
|
210
|
+
let page = 1;
|
|
211
|
+
while (page <= maxPages) {
|
|
212
|
+
const query = { page, pageSize: PAGE_SIZE };
|
|
213
|
+
let sign = null;
|
|
214
|
+
if (this._signProvider) {
|
|
215
|
+
sign = await this._signProvider({ url: step.url, query, cookies });
|
|
216
|
+
}
|
|
217
|
+
const resp = await this._fetchFn({ url: step.url, cookies, query, sign });
|
|
218
|
+
const items = extractItems(resp) || [];
|
|
219
|
+
if (!items.length) break;
|
|
220
|
+
for (const it of items) {
|
|
221
|
+
const rec = mapItem(it);
|
|
222
|
+
if (!rec || !rec.videoId) continue;
|
|
223
|
+
if (emitted >= limit) return;
|
|
224
|
+
yield {
|
|
225
|
+
adapter: NAME,
|
|
226
|
+
kind: step.kind,
|
|
227
|
+
originalId: stableOriginalId(step.kind, rec.videoId),
|
|
228
|
+
capturedAt: rec.occurredAt || Date.now(),
|
|
229
|
+
payload: { record: rec, kind: step.kind },
|
|
230
|
+
};
|
|
231
|
+
emitted += 1;
|
|
232
|
+
}
|
|
233
|
+
if (items.length < PAGE_SIZE) break;
|
|
234
|
+
page += 1;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
normalize(raw) {
|
|
240
|
+
if (!raw || !raw.payload || !raw.payload.record) {
|
|
241
|
+
throw new Error(`${NAME}.normalize: payload.record missing`);
|
|
242
|
+
}
|
|
243
|
+
const kind = raw.kind || raw.payload.kind;
|
|
244
|
+
const subtype = kind === KIND_FAVOURITE ? EVENT_SUBTYPES.LIKE : EVENT_SUBTYPES.MEDIA;
|
|
245
|
+
const verb = kind === KIND_FAVOURITE ? "收藏" : "观看";
|
|
246
|
+
return normalizeVideoRecord(raw.payload.record, raw, platform, NAME, VERSION, subtype, verb);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return VideoAdapter;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function snapshotEventToRecord(ev) {
|
|
254
|
+
return {
|
|
255
|
+
videoId: String(ev.videoId || ev.id || "unknown"),
|
|
256
|
+
title: ev.title || "(未知视频)",
|
|
257
|
+
category: ev.category || ev.type || null,
|
|
258
|
+
episode: ev.episode || null,
|
|
259
|
+
channel: ev.channel || ev.uploader || null,
|
|
260
|
+
durationSec: Number.isFinite(ev.durationSec) ? ev.durationSec : null,
|
|
261
|
+
url: ev.url || null,
|
|
262
|
+
occurredAt: parseTime(ev.capturedAt),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function normalizeVideoRecord(rec, raw, platform, NAME, VERSION, subtype, verb) {
|
|
267
|
+
const ingestedAt = Date.now();
|
|
268
|
+
const occurredAt = rec.occurredAt || raw.capturedAt || ingestedAt;
|
|
269
|
+
const source = {
|
|
270
|
+
adapter: NAME,
|
|
271
|
+
adapterVersion: VERSION,
|
|
272
|
+
originalId: raw.originalId,
|
|
273
|
+
capturedAt: raw.capturedAt || occurredAt,
|
|
274
|
+
capturedBy: CAPTURED_BY.API,
|
|
275
|
+
};
|
|
276
|
+
const title = rec.title || "(未知视频)";
|
|
277
|
+
const epSuffix = rec.episode ? ` ${rec.episode}` : "";
|
|
278
|
+
const itemId = `item-${platform}-video-${rec.videoId}`;
|
|
279
|
+
return {
|
|
280
|
+
events: [
|
|
281
|
+
{
|
|
282
|
+
id: newId(),
|
|
283
|
+
type: ENTITY_TYPES.EVENT,
|
|
284
|
+
subtype,
|
|
285
|
+
occurredAt,
|
|
286
|
+
actor: "person-self",
|
|
287
|
+
content: { title: `${verb}: ${title}${epSuffix}`, text: title },
|
|
288
|
+
ingestedAt,
|
|
289
|
+
source,
|
|
290
|
+
extra: {
|
|
291
|
+
platform,
|
|
292
|
+
videoId: rec.videoId,
|
|
293
|
+
category: rec.category || null,
|
|
294
|
+
episode: rec.episode || null,
|
|
295
|
+
channel: rec.channel || null,
|
|
296
|
+
durationSec: rec.durationSec != null ? rec.durationSec : null,
|
|
297
|
+
url: rec.url || null,
|
|
298
|
+
itemRef: itemId,
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
items: [
|
|
303
|
+
{
|
|
304
|
+
id: itemId,
|
|
305
|
+
type: ENTITY_TYPES.ITEM,
|
|
306
|
+
subtype: ITEM_SUBTYPES.MEDIA,
|
|
307
|
+
name: title,
|
|
308
|
+
ingestedAt,
|
|
309
|
+
source,
|
|
310
|
+
extra: { platform, kind: "video", videoId: rec.videoId, category: rec.category || null, channel: rec.channel || null },
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
persons: [],
|
|
314
|
+
places: [],
|
|
315
|
+
topics: [],
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function defaultFetch(_opts) {
|
|
320
|
+
throw new Error("video-base: no fetchFn configured for cookie-api mode");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
module.exports = {
|
|
324
|
+
createVideoAdapter,
|
|
325
|
+
normalizeVideoRecord,
|
|
326
|
+
parseTime,
|
|
327
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
328
|
+
KIND_WATCH,
|
|
329
|
+
KIND_FAVOURITE,
|
|
330
|
+
VALID_SNAPSHOT_KINDS,
|
|
331
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §13+ — 百度网盘 (Baidu Netdisk, com.baidu.netdisk) adapter. §12.1 ROI ⭐⭐⭐
|
|
3
|
+
* "文件 + 外链".
|
|
4
|
+
*
|
|
5
|
+
* Thin wrapper over _document-base — a cloud-drive file list is the same shape
|
|
6
|
+
* as the doc-wps / doc-tencent-docs "own-document list". Baidu Netdisk exposes
|
|
7
|
+
* the owner's files via pan.baidu.com/api/list (BDUSS cookie); this adapter
|
|
8
|
+
* supplies the endpoint + field mapping, the base handles snapshot + cookie-api
|
|
9
|
+
* orchestration + normalize (event POST + item DOCUMENT). Endpoint best-effort +
|
|
10
|
+
* overridable via opts.listUrl (not field-verified — FAMILY-23 playbook).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
"use strict";
|
|
14
|
+
|
|
15
|
+
const { createDocumentAdapter, parseTime, SNAPSHOT_SCHEMA_VERSION } = require("../_document-base");
|
|
16
|
+
|
|
17
|
+
const NAME = "doc-baidu-netdisk";
|
|
18
|
+
const VERSION = "0.1.0";
|
|
19
|
+
|
|
20
|
+
// Best-effort Baidu Netdisk file-list endpoint. Overridable via opts.listUrl.
|
|
21
|
+
const NETDISK_LIST_URL = "https://pan.baidu.com/api/list";
|
|
22
|
+
|
|
23
|
+
// Baidu Netdisk `category` codes → normalized docType.
|
|
24
|
+
const CATEGORY_MAP = {
|
|
25
|
+
1: "video",
|
|
26
|
+
2: "audio",
|
|
27
|
+
3: "image",
|
|
28
|
+
4: "doc",
|
|
29
|
+
5: "app",
|
|
30
|
+
6: "other",
|
|
31
|
+
7: "seed",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function mapNetdiskType(d) {
|
|
35
|
+
if (d.isdir === 1 || d.isdir === true) return "folder";
|
|
36
|
+
const cat = d.category != null ? d.category : d.file_category;
|
|
37
|
+
if (cat != null && CATEGORY_MAP[cat]) return CATEGORY_MAP[cat];
|
|
38
|
+
const name = String(d.server_filename || d.filename || "").toLowerCase();
|
|
39
|
+
if (/\.(mp4|mkv|avi|mov)$/.test(name)) return "video";
|
|
40
|
+
if (/\.(mp3|flac|wav|m4a)$/.test(name)) return "audio";
|
|
41
|
+
if (/\.(jpg|jpeg|png|gif|webp)$/.test(name)) return "image";
|
|
42
|
+
if (/\.(docx?|xlsx?|pptx?|pdf|txt)$/.test(name)) return "doc";
|
|
43
|
+
return "file";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function extractDocs(resp) {
|
|
47
|
+
if (!resp || typeof resp !== "object") return [];
|
|
48
|
+
if (Array.isArray(resp.list)) return resp.list;
|
|
49
|
+
if (Array.isArray(resp.data)) return resp.data;
|
|
50
|
+
if (resp.data && Array.isArray(resp.data.list)) return resp.data.list;
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function mapDoc(d) {
|
|
55
|
+
if (!d || typeof d !== "object") return null;
|
|
56
|
+
const docId = d.fs_id || d.fsId || d.id || d.path;
|
|
57
|
+
if (!docId) return null;
|
|
58
|
+
return {
|
|
59
|
+
docId: String(docId),
|
|
60
|
+
title: d.server_filename || d.filename || d.title || "(未命名)",
|
|
61
|
+
docType: mapNetdiskType(d),
|
|
62
|
+
url: d.path || d.dlink || null,
|
|
63
|
+
createdMs: parseTime(d.server_ctime || d.local_ctime || d.ctime),
|
|
64
|
+
updatedMs: parseTime(d.server_mtime || d.local_mtime || d.mtime),
|
|
65
|
+
extra: {
|
|
66
|
+
size: d.size != null ? d.size : null,
|
|
67
|
+
isDir: d.isdir === 1 || d.isdir === true ? true : false,
|
|
68
|
+
path: d.path || null,
|
|
69
|
+
category: d.category != null ? d.category : null,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const BaiduNetdiskAdapter = createDocumentAdapter({
|
|
75
|
+
NAME,
|
|
76
|
+
VERSION,
|
|
77
|
+
platform: "baidu-netdisk",
|
|
78
|
+
defaultListUrl: NETDISK_LIST_URL,
|
|
79
|
+
extractDocs,
|
|
80
|
+
mapDoc,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
BaiduNetdiskAdapter,
|
|
85
|
+
extractDocs,
|
|
86
|
+
mapDoc,
|
|
87
|
+
CATEGORY_MAP,
|
|
88
|
+
NAME,
|
|
89
|
+
VERSION,
|
|
90
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
91
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §13+ — 腾讯文档 (Tencent Docs) adapter. "自创文档列表" (§12.1, ROI ⭐⭐⭐).
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper over _document-base. 腾讯文档 exposes the owner's documents via
|
|
5
|
+
* docs.qq.com dop-api; this adapter supplies the endpoint + field mapping, the
|
|
6
|
+
* base handles snapshot + cookie-api orchestration + normalize. Endpoint is
|
|
7
|
+
* best-effort + overridable via opts.listUrl (docs.qq.com rotates its dop-api;
|
|
8
|
+
* some calls need a sign — opts.signProvider seam — FAMILY-23 playbook, not
|
|
9
|
+
* field-verified).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
const { createDocumentAdapter, parseTime, SNAPSHOT_SCHEMA_VERSION } = require("../_document-base");
|
|
15
|
+
|
|
16
|
+
const NAME = "doc-tencent-docs";
|
|
17
|
+
const VERSION = "0.1.0";
|
|
18
|
+
|
|
19
|
+
// Best-effort Tencent Docs "my documents" list. Overridable via opts.listUrl.
|
|
20
|
+
const TENCENT_DOCS_LIST_URL = "https://docs.qq.com/dop-api/get/personal/files";
|
|
21
|
+
|
|
22
|
+
// Tencent Docs type codes → normalized docType.
|
|
23
|
+
const TYPE_MAP = {
|
|
24
|
+
doc: "doc",
|
|
25
|
+
document: "doc",
|
|
26
|
+
sheet: "sheet",
|
|
27
|
+
spreadsheet: "sheet",
|
|
28
|
+
slide: "slide",
|
|
29
|
+
presentation: "slide",
|
|
30
|
+
pdf: "pdf",
|
|
31
|
+
form: "form",
|
|
32
|
+
mind: "mind",
|
|
33
|
+
1: "doc",
|
|
34
|
+
2: "sheet",
|
|
35
|
+
3: "slide",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function mapTencentType(d) {
|
|
39
|
+
const raw = d.type != null ? d.type : d.docType != null ? d.docType : d.fileType;
|
|
40
|
+
const key = String(raw == null ? "" : raw).toLowerCase();
|
|
41
|
+
return TYPE_MAP[key] || TYPE_MAP[raw] || "doc";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function extractDocs(resp) {
|
|
45
|
+
if (!resp || typeof resp !== "object") return [];
|
|
46
|
+
if (Array.isArray(resp.files)) return resp.files;
|
|
47
|
+
if (Array.isArray(resp.list)) return resp.list;
|
|
48
|
+
const data = resp.data && typeof resp.data === "object" ? resp.data : null;
|
|
49
|
+
if (data) {
|
|
50
|
+
if (Array.isArray(data.files)) return data.files;
|
|
51
|
+
if (Array.isArray(data.list)) return data.list;
|
|
52
|
+
if (Array.isArray(data.records)) return data.records;
|
|
53
|
+
}
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function mapDoc(d) {
|
|
58
|
+
if (!d || typeof d !== "object") return null;
|
|
59
|
+
const docId = d.id || d.fileId || d.file_id || d.docId || d.url;
|
|
60
|
+
if (!docId) return null;
|
|
61
|
+
return {
|
|
62
|
+
docId: String(docId),
|
|
63
|
+
title: d.title || d.name || d.fileName || "(无标题)",
|
|
64
|
+
docType: mapTencentType(d),
|
|
65
|
+
url:
|
|
66
|
+
d.url ||
|
|
67
|
+
(d.id ? `https://docs.qq.com/doc/${d.id}` : null),
|
|
68
|
+
createdMs: parseTime(d.createTime || d.create_time || d.gmtCreate),
|
|
69
|
+
updatedMs: parseTime(d.lastModifyTime || d.modifyTime || d.updateTime || d.gmtModify),
|
|
70
|
+
extra: {
|
|
71
|
+
ownerName: d.ownerName || d.creatorName || null,
|
|
72
|
+
starred: d.isStar != null ? d.isStar : undefined,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const TencentDocsAdapter = createDocumentAdapter({
|
|
78
|
+
NAME,
|
|
79
|
+
VERSION,
|
|
80
|
+
platform: "tencent-docs",
|
|
81
|
+
defaultListUrl: TENCENT_DOCS_LIST_URL,
|
|
82
|
+
extractDocs,
|
|
83
|
+
mapDoc,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
TencentDocsAdapter,
|
|
88
|
+
extractDocs,
|
|
89
|
+
mapDoc,
|
|
90
|
+
TYPE_MAP,
|
|
91
|
+
NAME,
|
|
92
|
+
VERSION,
|
|
93
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
94
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §13+ — WPS 云文档 (Kingsoft Office) adapter. "自创文档列表" (§12.1, ROI ⭐⭐⭐).
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper over _document-base. WPS exposes the owner's cloud documents via
|
|
5
|
+
* drive.wps.cn; this adapter supplies the endpoint + field mapping, the base
|
|
6
|
+
* handles snapshot + cookie-api orchestration + normalize (event POST + item
|
|
7
|
+
* DOCUMENT). Endpoint is best-effort + overridable via opts.listUrl (WPS rotates
|
|
8
|
+
* its drive API — FAMILY-23 playbook, not field-verified).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
const { createDocumentAdapter, parseTime, SNAPSHOT_SCHEMA_VERSION } = require("../_document-base");
|
|
14
|
+
|
|
15
|
+
const NAME = "doc-wps";
|
|
16
|
+
const VERSION = "0.1.0";
|
|
17
|
+
|
|
18
|
+
// Best-effort WPS cloud-drive file list. Overridable via opts.listUrl.
|
|
19
|
+
const WPS_LIST_URL = "https://drive.wps.cn/api/v5/groups/special/files";
|
|
20
|
+
|
|
21
|
+
// WPS doc-type codes/names → normalized docType.
|
|
22
|
+
function mapWpsType(d) {
|
|
23
|
+
const t = String(d.fname || d.name || "").toLowerCase();
|
|
24
|
+
if (d.ftype && typeof d.ftype === "string") return d.ftype;
|
|
25
|
+
if (/\.(xlsx?|et|csv)$/.test(t)) return "sheet";
|
|
26
|
+
if (/\.(pptx?|dps)$/.test(t)) return "slide";
|
|
27
|
+
if (/\.pdf$/.test(t)) return "pdf";
|
|
28
|
+
if (/\.(docx?|wps)$/.test(t)) return "doc";
|
|
29
|
+
return "doc";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function extractDocs(resp) {
|
|
33
|
+
if (!resp || typeof resp !== "object") return [];
|
|
34
|
+
if (Array.isArray(resp.files)) return resp.files;
|
|
35
|
+
if (Array.isArray(resp.data)) return resp.data;
|
|
36
|
+
if (resp.data && Array.isArray(resp.data.files)) return resp.data.files;
|
|
37
|
+
if (Array.isArray(resp.list)) return resp.list;
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function mapDoc(d) {
|
|
42
|
+
if (!d || typeof d !== "object") return null;
|
|
43
|
+
const docId = d.id || d.fileid || d.file_id || d.fid;
|
|
44
|
+
if (!docId) return null;
|
|
45
|
+
return {
|
|
46
|
+
docId: String(docId),
|
|
47
|
+
title: d.fname || d.name || d.title || "(无标题)",
|
|
48
|
+
docType: mapWpsType(d),
|
|
49
|
+
url:
|
|
50
|
+
d.url ||
|
|
51
|
+
(d.id ? `https://www.kdocs.cn/p/${d.id}` : null),
|
|
52
|
+
createdMs: parseTime(d.ctime || d.create_time || d.created),
|
|
53
|
+
updatedMs: parseTime(d.mtime || d.modify_time || d.updated || d.utime),
|
|
54
|
+
extra: {
|
|
55
|
+
size: d.fsize || d.size || null,
|
|
56
|
+
groupId: d.group_id || d.groupid || null,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const WpsDocAdapter = createDocumentAdapter({
|
|
62
|
+
NAME,
|
|
63
|
+
VERSION,
|
|
64
|
+
platform: "wps",
|
|
65
|
+
defaultListUrl: WPS_LIST_URL,
|
|
66
|
+
extractDocs,
|
|
67
|
+
mapDoc,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
WpsDocAdapter,
|
|
72
|
+
extractDocs,
|
|
73
|
+
mapDoc,
|
|
74
|
+
NAME,
|
|
75
|
+
VERSION,
|
|
76
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
77
|
+
};
|