@chainlesschain/personal-data-hub 0.4.7 → 0.4.23
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/biz-tianyancha.test.js +159 -0
- package/__tests__/adapters/doc-baidu-netdisk.test.js +102 -0
- package/__tests__/adapters/doc-camscanner.test.js +147 -0
- package/__tests__/adapters/doc-platforms.test.js +177 -0
- package/__tests__/adapters/gov-ixiamen.test.js +150 -0
- package/__tests__/adapters/gov-tax.test.js +135 -0
- package/__tests__/adapters/health-meiyou.test.js +125 -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-dongchedi.test.js +165 -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/__tests__/adapters/video-xigua.test.js +106 -0
- package/__tests__/adapters/wework-pc.test.js +124 -0
- package/lib/adapter-guide.js +25 -3
- package/lib/adapters/_document-base.js +370 -0
- package/lib/adapters/_video-base.js +331 -0
- package/lib/adapters/biz-tianyancha/index.js +348 -0
- package/lib/adapters/doc-baidu-netdisk/index.js +91 -0
- package/lib/adapters/doc-camscanner/index.js +102 -0
- package/lib/adapters/doc-tencent-docs/index.js +94 -0
- package/lib/adapters/doc-wps/index.js +77 -0
- package/lib/adapters/gov-ixiamen/index.js +380 -0
- package/lib/adapters/gov-tax/index.js +451 -0
- package/lib/adapters/health-meiyou/index.js +393 -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-dongchedi/index.js +360 -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/adapters/video-xigua/index.js +68 -0
- package/lib/adapters/wework-pc/index.js +31 -0
- package/lib/index.js +40 -0
- package/package.json +1 -1
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §A11 — CSDN (net.csdn.csdnplus) adapter, dual-mode (snapshot + cookie-api).
|
|
3
|
+
* Phase 13+ §12.1 line-783 ROI ⭐⭐⭐ "技术阅读 + 收藏".
|
|
4
|
+
*
|
|
5
|
+
* CSDN's personal data is the user's tech-content footprint: blog posts they
|
|
6
|
+
* authored, articles they favourited (收藏 = strong interest signal), and authors
|
|
7
|
+
* they follow. Maps cleanly to the same answer/favourite/follow shape as
|
|
8
|
+
* social-zhihu (article↔answer), so it mirrors that adapter.
|
|
9
|
+
*
|
|
10
|
+
* 1. snapshot mode (opts.inputPath): JSON schemaVersion 1, stateless.
|
|
11
|
+
* 2. cookie-api mode (opts.account.cookies + account.username): fetch the
|
|
12
|
+
* user's articles / favourites / followees from csdn.net via the injected
|
|
13
|
+
* `fetchFn` (Android in-APK cc → OkHttp; desktop hub → Electron WebView net
|
|
14
|
+
* request). CSDN's home-api keys off the user's `username`, so
|
|
15
|
+
* account.username is REQUIRED in cookie mode. A sign seam (opts.signProvider)
|
|
16
|
+
* covers any anti-bot token; best-effort unsigned. 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": { "username": "...", "name": "..." },
|
|
23
|
+
* "events": [
|
|
24
|
+
* { "kind": "article", "id": "article-<id>", "articleId": "...", "title": "...",
|
|
25
|
+
* "url": "...", "viewCount": N, "collectCount": N, "createdTime": <s|ms> },
|
|
26
|
+
* { "kind": "favourite", "id": "fav-<id>", "itemId": "...", "title": "...",
|
|
27
|
+
* "url": "...", "source": "...", "capturedAt": <ms> },
|
|
28
|
+
* { "kind": "follow", "id": "follow-<u>", "username": "...", "name": "...",
|
|
29
|
+
* "url": "...", "capturedAt": <ms> }
|
|
30
|
+
* ]
|
|
31
|
+
* }
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
"use strict";
|
|
35
|
+
|
|
36
|
+
const fs = require("node:fs");
|
|
37
|
+
const { newId } = require("../../ids");
|
|
38
|
+
const {
|
|
39
|
+
ENTITY_TYPES,
|
|
40
|
+
PERSON_SUBTYPES,
|
|
41
|
+
EVENT_SUBTYPES,
|
|
42
|
+
CAPTURED_BY,
|
|
43
|
+
} = require("../../constants");
|
|
44
|
+
const { CookieAuth } = require("../shopping-base");
|
|
45
|
+
|
|
46
|
+
const NAME = "social-csdn";
|
|
47
|
+
const VERSION = "0.1.0";
|
|
48
|
+
const SNAPSHOT_SCHEMA_VERSION = 1;
|
|
49
|
+
|
|
50
|
+
const KIND_ARTICLE = "article";
|
|
51
|
+
const KIND_FAVOURITE = "favourite";
|
|
52
|
+
const KIND_FOLLOW = "follow";
|
|
53
|
+
const VALID_SNAPSHOT_KINDS = Object.freeze([KIND_ARTICLE, KIND_FAVOURITE, KIND_FOLLOW]);
|
|
54
|
+
|
|
55
|
+
// Best-effort CSDN home-api endpoints. `{user}` replaced with username.
|
|
56
|
+
const ARTICLES_URL =
|
|
57
|
+
"https://blog.csdn.net/community/home-api/v1/get-business-list";
|
|
58
|
+
const FAVOURITES_URL = "https://me.csdn.net/api/favorite/list";
|
|
59
|
+
const FOLLOWEES_URL =
|
|
60
|
+
"https://blog.csdn.net/community/home-api/v1/get-follow-list";
|
|
61
|
+
const PAGE_SIZE = 20;
|
|
62
|
+
|
|
63
|
+
function parseTime(v) {
|
|
64
|
+
if (Number.isFinite(v)) return v > 1e12 ? v : v >= 1e9 ? v * 1000 : v;
|
|
65
|
+
if (typeof v === "string") {
|
|
66
|
+
if (/^\d+$/.test(v)) {
|
|
67
|
+
const n = parseInt(v, 10);
|
|
68
|
+
return n > 1e12 ? n : n >= 1e9 ? n * 1000 : n;
|
|
69
|
+
}
|
|
70
|
+
const t = Date.parse(v);
|
|
71
|
+
return Number.isFinite(t) ? t : null;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function stableOriginalId(kind, id) {
|
|
77
|
+
const safe =
|
|
78
|
+
(typeof id === "string" && id.length > 0 && id) ||
|
|
79
|
+
(typeof id === "number" && Number.isFinite(id) && String(id)) ||
|
|
80
|
+
`unknown-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
81
|
+
return `csdn:${kind}:${safe}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function stripHtml(s) {
|
|
85
|
+
return typeof s === "string" ? s.replace(/<[^>]+>/g, "").trim() : "";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
class CsdnAdapter {
|
|
89
|
+
constructor(opts = {}) {
|
|
90
|
+
this.account = opts.account || null;
|
|
91
|
+
this._cookieAuth =
|
|
92
|
+
opts.account && opts.account.cookies
|
|
93
|
+
? new CookieAuth({ platform: "csdn", cookies: opts.account.cookies })
|
|
94
|
+
: null;
|
|
95
|
+
this._fetchFn = typeof opts.fetchFn === "function" ? opts.fetchFn : defaultFetch;
|
|
96
|
+
this._signProvider =
|
|
97
|
+
typeof opts.signProvider === "function" ? opts.signProvider : null;
|
|
98
|
+
this._urls = {
|
|
99
|
+
articles: opts.articlesUrl || ARTICLES_URL,
|
|
100
|
+
favourites: opts.favouritesUrl || FAVOURITES_URL,
|
|
101
|
+
followees: opts.followeesUrl || FOLLOWEES_URL,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
this.name = NAME;
|
|
105
|
+
this.version = VERSION;
|
|
106
|
+
this.capabilities = [
|
|
107
|
+
"sync:snapshot",
|
|
108
|
+
"sync:cookie-api",
|
|
109
|
+
"parse:csdn-article",
|
|
110
|
+
"parse:csdn-favourite",
|
|
111
|
+
"parse:csdn-follow",
|
|
112
|
+
];
|
|
113
|
+
this.extractMode = "web-api";
|
|
114
|
+
this.rateLimits = { perMinute: 8, perDay: 200 };
|
|
115
|
+
this.dataDisclosure = {
|
|
116
|
+
fields: [
|
|
117
|
+
"csdn:article (title / viewCount / collectCount)",
|
|
118
|
+
"csdn:favourite (title / url / source)",
|
|
119
|
+
"csdn:follow (username / name)",
|
|
120
|
+
],
|
|
121
|
+
sensitivity: "medium",
|
|
122
|
+
legalGate: false,
|
|
123
|
+
defaultInclude: { article: true, favourite: true, follow: true },
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
this._deps = { fs };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async authenticate(ctx = {}) {
|
|
130
|
+
if (ctx && typeof ctx.inputPath === "string" && ctx.inputPath.length > 0) {
|
|
131
|
+
try {
|
|
132
|
+
this._deps.fs.accessSync(ctx.inputPath, this._deps.fs.constants.R_OK);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
reason: "INPUT_PATH_UNREADABLE",
|
|
137
|
+
message: `snapshot not readable at ${ctx.inputPath}: ${err.message}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return { ok: true, mode: "snapshot-file" };
|
|
141
|
+
}
|
|
142
|
+
if (this._cookieAuth) {
|
|
143
|
+
const ok = await this._cookieAuth.validate();
|
|
144
|
+
if (!ok) return { ok: false, reason: "INVALID_COOKIE", error: "cookies missing" };
|
|
145
|
+
if (!this.account || !this.account.username) {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
reason: "NO_ACCOUNT_USERNAME",
|
|
149
|
+
message: "cookie-api mode requires account.username (CSDN blog username)",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return { ok: true, account: this.account.username, mode: "cookie" };
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
reason: "NO_INPUT",
|
|
157
|
+
message:
|
|
158
|
+
"social-csdn.authenticate: needs opts.inputPath (snapshot mode) OR opts.account.cookies + username (cookie-api mode)",
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async healthCheck() {
|
|
163
|
+
if (this._cookieAuth) {
|
|
164
|
+
const r = await this.authenticate();
|
|
165
|
+
return r.ok
|
|
166
|
+
? { ok: true, lastChecked: Date.now() }
|
|
167
|
+
: { ok: false, reason: r.reason, error: r.error };
|
|
168
|
+
}
|
|
169
|
+
return { ok: true, lastChecked: Date.now() };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async *sync(opts = {}) {
|
|
173
|
+
if (typeof opts.inputPath === "string" && opts.inputPath.length > 0) {
|
|
174
|
+
yield* this._syncViaSnapshot(opts);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (this._cookieAuth) {
|
|
178
|
+
yield* this._syncViaCookie(opts);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
throw new Error(
|
|
182
|
+
"social-csdn.sync: needs opts.inputPath (snapshot mode) OR opts.account.cookies + username (cookie-api mode)",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async *_syncViaSnapshot(opts) {
|
|
187
|
+
const raw = this._deps.fs.readFileSync(opts.inputPath, "utf-8");
|
|
188
|
+
const snapshot = JSON.parse(raw);
|
|
189
|
+
if (
|
|
190
|
+
!snapshot ||
|
|
191
|
+
typeof snapshot !== "object" ||
|
|
192
|
+
snapshot.schemaVersion !== SNAPSHOT_SCHEMA_VERSION
|
|
193
|
+
) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`social-csdn.sync: snapshot schemaVersion mismatch (got ${snapshot && snapshot.schemaVersion}, expected ${SNAPSHOT_SCHEMA_VERSION})`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
const fallbackCapturedAt =
|
|
199
|
+
Number.isFinite(snapshot.snapshottedAt) && snapshot.snapshottedAt > 0
|
|
200
|
+
? Math.floor(snapshot.snapshottedAt)
|
|
201
|
+
: Date.now();
|
|
202
|
+
const account =
|
|
203
|
+
snapshot.account && typeof snapshot.account === "object" ? snapshot.account : null;
|
|
204
|
+
const include = opts.include || {};
|
|
205
|
+
const limit = Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : Infinity;
|
|
206
|
+
|
|
207
|
+
const events = Array.isArray(snapshot.events) ? snapshot.events : [];
|
|
208
|
+
let emitted = 0;
|
|
209
|
+
for (const ev of events) {
|
|
210
|
+
if (emitted >= limit) return;
|
|
211
|
+
if (!ev || typeof ev !== "object") continue;
|
|
212
|
+
if (!VALID_SNAPSHOT_KINDS.includes(ev.kind)) continue;
|
|
213
|
+
if (include[ev.kind] === false) continue;
|
|
214
|
+
|
|
215
|
+
const capturedAt =
|
|
216
|
+
parseTime(ev.capturedAt) || parseTime(ev.createdTime) || fallbackCapturedAt;
|
|
217
|
+
const id =
|
|
218
|
+
(typeof ev.id === "string" && ev.id.length > 0 && ev.id) ||
|
|
219
|
+
ev.articleId ||
|
|
220
|
+
ev.itemId ||
|
|
221
|
+
ev.username ||
|
|
222
|
+
null;
|
|
223
|
+
|
|
224
|
+
yield {
|
|
225
|
+
adapter: NAME,
|
|
226
|
+
kind: ev.kind,
|
|
227
|
+
originalId: stableOriginalId(ev.kind, id),
|
|
228
|
+
capturedAt,
|
|
229
|
+
payload: { ...ev, account },
|
|
230
|
+
};
|
|
231
|
+
emitted += 1;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async *_syncViaCookie(opts = {}) {
|
|
236
|
+
if (!this.account || !this.account.username) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
"social-csdn._syncViaCookie: account.username required (set via new CsdnAdapter({ account: { username, cookies } }))",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (!(await this._cookieAuth.validate())) return;
|
|
242
|
+
const user = encodeURIComponent(this.account.username);
|
|
243
|
+
const include = opts.include || {};
|
|
244
|
+
const limit = Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : Infinity;
|
|
245
|
+
const maxPages =
|
|
246
|
+
Number.isInteger(opts.maxPages) && opts.maxPages > 0 ? opts.maxPages : 10;
|
|
247
|
+
|
|
248
|
+
const plan = [
|
|
249
|
+
{ kind: KIND_ARTICLE, url: this._urls.articles, mapId: (it) => `article-${it.articleId || it.id || it.url}` },
|
|
250
|
+
{ kind: KIND_FAVOURITE, url: this._urls.favourites, mapId: (it) => `fav-${it.id || it.source_id || it.url}` },
|
|
251
|
+
{ kind: KIND_FOLLOW, url: this._urls.followees, mapId: (it) => `follow-${it.username || it.userName || it.id}` },
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
let emitted = 0;
|
|
255
|
+
for (const step of plan) {
|
|
256
|
+
if (include[step.kind] === false) continue;
|
|
257
|
+
const baseUrl = step.url.replace("{user}", user);
|
|
258
|
+
let page = 1;
|
|
259
|
+
while (page <= maxPages) {
|
|
260
|
+
const query = { page, size: PAGE_SIZE, username: this.account.username };
|
|
261
|
+
let sign = null;
|
|
262
|
+
if (this._signProvider) {
|
|
263
|
+
sign = await this._signProvider({ url: baseUrl, query, cookies: this._cookieAuth.toHeader() });
|
|
264
|
+
}
|
|
265
|
+
const resp = await this._fetchFn({
|
|
266
|
+
url: baseUrl,
|
|
267
|
+
cookies: this._cookieAuth.toHeader(),
|
|
268
|
+
query,
|
|
269
|
+
sign,
|
|
270
|
+
});
|
|
271
|
+
const items = extractList(resp);
|
|
272
|
+
if (!items.length) break;
|
|
273
|
+
for (const it of items) {
|
|
274
|
+
if (!it || typeof it !== "object") continue;
|
|
275
|
+
if (emitted >= limit) return;
|
|
276
|
+
yield {
|
|
277
|
+
adapter: NAME,
|
|
278
|
+
kind: step.kind,
|
|
279
|
+
originalId: stableOriginalId(step.kind, step.mapId(it)),
|
|
280
|
+
capturedAt: cookieItemTime(step.kind, it),
|
|
281
|
+
payload: { item: it, kind: step.kind, cookie: true },
|
|
282
|
+
};
|
|
283
|
+
emitted += 1;
|
|
284
|
+
}
|
|
285
|
+
if (items.length < PAGE_SIZE) break;
|
|
286
|
+
page += 1;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
normalize(raw) {
|
|
292
|
+
if (!raw || !raw.payload) {
|
|
293
|
+
throw new Error("CsdnAdapter.normalize: payload missing");
|
|
294
|
+
}
|
|
295
|
+
const ingestedAt = Date.now();
|
|
296
|
+
const kind = raw.kind || raw.payload.kind;
|
|
297
|
+
if (kind === KIND_ARTICLE) return normalizeArticle(raw, ingestedAt);
|
|
298
|
+
if (kind === KIND_FAVOURITE) return normalizeFavourite(raw, ingestedAt);
|
|
299
|
+
if (kind === KIND_FOLLOW) return normalizeFollow(raw, ingestedAt);
|
|
300
|
+
throw new Error(`CsdnAdapter.normalize: unknown kind ${kind}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ─── cookie response helpers ─────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
function extractList(resp) {
|
|
307
|
+
if (!resp || typeof resp !== "object") return [];
|
|
308
|
+
if (Array.isArray(resp.list)) return resp.list;
|
|
309
|
+
if (Array.isArray(resp.data)) return resp.data;
|
|
310
|
+
const d = resp.data;
|
|
311
|
+
if (d && typeof d === "object") {
|
|
312
|
+
if (Array.isArray(d.list)) return d.list;
|
|
313
|
+
if (Array.isArray(d.records)) return d.records;
|
|
314
|
+
if (Array.isArray(d.result)) return d.result;
|
|
315
|
+
}
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function cookieItemTime(kind, it) {
|
|
320
|
+
if (kind === KIND_ARTICLE) {
|
|
321
|
+
return parseTime(it.createdTime || it.formatTime || it.postTime || it.created_at) || Date.now();
|
|
322
|
+
}
|
|
323
|
+
if (kind === KIND_FAVOURITE) {
|
|
324
|
+
return parseTime(it.created_at || it.create_time || it.favoriteTime) || Date.now();
|
|
325
|
+
}
|
|
326
|
+
return Date.now();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ─── per-kind normalizers ────────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
function buildSource(raw, occurredAt) {
|
|
332
|
+
return {
|
|
333
|
+
adapter: NAME,
|
|
334
|
+
adapterVersion: VERSION,
|
|
335
|
+
originalId: raw.originalId,
|
|
336
|
+
capturedAt: raw.capturedAt || occurredAt,
|
|
337
|
+
capturedBy: CAPTURED_BY.API,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function normalizeArticle(raw, ingestedAt) {
|
|
342
|
+
const p = raw.payload;
|
|
343
|
+
const it = p.cookie ? p.item : p;
|
|
344
|
+
const title = stripHtml(it.title || "");
|
|
345
|
+
const articleId = it.articleId || it.id || null;
|
|
346
|
+
const occurredAt =
|
|
347
|
+
parseTime(it.createdTime || it.created_at || it.postTime || raw.capturedAt) || ingestedAt;
|
|
348
|
+
const source = buildSource(raw, occurredAt);
|
|
349
|
+
return {
|
|
350
|
+
events: [
|
|
351
|
+
{
|
|
352
|
+
id: newId(),
|
|
353
|
+
type: ENTITY_TYPES.EVENT,
|
|
354
|
+
subtype: EVENT_SUBTYPES.POST,
|
|
355
|
+
occurredAt,
|
|
356
|
+
actor: "person-self",
|
|
357
|
+
content: { title: title.slice(0, 80) || "(无标题)", text: title },
|
|
358
|
+
ingestedAt,
|
|
359
|
+
source,
|
|
360
|
+
extra: {
|
|
361
|
+
platform: "csdn",
|
|
362
|
+
csdnArticleId: articleId != null ? String(articleId) : null,
|
|
363
|
+
viewCount: it.viewCount != null ? it.viewCount : it.view_count || 0,
|
|
364
|
+
collectCount: it.collectCount != null ? it.collectCount : it.favor_count || 0,
|
|
365
|
+
url: it.url || null,
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
persons: [],
|
|
370
|
+
places: [],
|
|
371
|
+
items: [],
|
|
372
|
+
topics: [],
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function normalizeFavourite(raw, ingestedAt) {
|
|
377
|
+
const p = raw.payload;
|
|
378
|
+
const it = p.cookie ? p.item : p;
|
|
379
|
+
const title = stripHtml(it.title || it.source_title || "");
|
|
380
|
+
const occurredAt =
|
|
381
|
+
parseTime(it.capturedAt || it.created_at || raw.capturedAt) || ingestedAt;
|
|
382
|
+
const source = buildSource(raw, occurredAt);
|
|
383
|
+
return {
|
|
384
|
+
events: [
|
|
385
|
+
{
|
|
386
|
+
id: newId(),
|
|
387
|
+
type: ENTITY_TYPES.EVENT,
|
|
388
|
+
subtype: EVENT_SUBTYPES.LIKE,
|
|
389
|
+
occurredAt,
|
|
390
|
+
actor: "person-self",
|
|
391
|
+
content: { title: title.slice(0, 80) || "(无标题)", text: title },
|
|
392
|
+
ingestedAt,
|
|
393
|
+
source,
|
|
394
|
+
extra: {
|
|
395
|
+
platform: "csdn",
|
|
396
|
+
csdnItemId: (it.itemId || it.id || it.source_id) != null ? String(it.itemId || it.id || it.source_id) : null,
|
|
397
|
+
source: it.source || it.url_source || null,
|
|
398
|
+
url: it.url || it.source_url || null,
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
persons: [],
|
|
403
|
+
places: [],
|
|
404
|
+
items: [],
|
|
405
|
+
topics: [],
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function normalizeFollow(raw, ingestedAt) {
|
|
410
|
+
const p = raw.payload;
|
|
411
|
+
const it = p.cookie ? p.item : p;
|
|
412
|
+
const uname = it.username || it.userName || it.id || `unknown-${newId()}`;
|
|
413
|
+
const name = it.name || it.nickname || it.nickName || uname;
|
|
414
|
+
const occurredAt = parseTime(it.capturedAt || raw.capturedAt) || ingestedAt;
|
|
415
|
+
const source = buildSource(raw, occurredAt);
|
|
416
|
+
const person = {
|
|
417
|
+
id: `person-csdn-${uname}`,
|
|
418
|
+
type: ENTITY_TYPES.PERSON,
|
|
419
|
+
subtype: PERSON_SUBTYPES.CONTACT,
|
|
420
|
+
names: [name],
|
|
421
|
+
ingestedAt,
|
|
422
|
+
source,
|
|
423
|
+
identifiers: { "csdn-username": [String(uname)] },
|
|
424
|
+
extra: {
|
|
425
|
+
platform: "csdn",
|
|
426
|
+
url: it.url || (it.username ? `https://blog.csdn.net/${it.username}` : null),
|
|
427
|
+
followedAt: occurredAt,
|
|
428
|
+
},
|
|
429
|
+
};
|
|
430
|
+
return { events: [], persons: [person], places: [], items: [], topics: [] };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async function defaultFetch(_opts) {
|
|
434
|
+
throw new Error("social-csdn: no fetchFn configured for cookie-api mode");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
module.exports = {
|
|
438
|
+
CsdnAdapter,
|
|
439
|
+
extractList,
|
|
440
|
+
NAME,
|
|
441
|
+
VERSION,
|
|
442
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
443
|
+
VALID_SNAPSHOT_KINDS,
|
|
444
|
+
};
|