@chainlesschain/personal-data-hub 0.4.18 → 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/biz-tianyancha.test.js +159 -0
- package/__tests__/adapters/car-mercedesme.test.js +74 -0
- package/__tests__/adapters/doc-camscanner.test.js +147 -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/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-qq.test.js +112 -0
- package/__tests__/adapters/reading-family.test.js +108 -0
- package/__tests__/adapters/social-dongchedi.test.js +165 -0
- package/__tests__/adapters/travel-didi-consumer.test.js +66 -0
- package/__tests__/adapters/video-xigua.test.js +106 -0
- package/__tests__/adapters/wework-pc.test.js +124 -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 +31 -3
- 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/biz-tianyancha/index.js +348 -0
- package/lib/adapters/car-mercedesme/index.js +225 -0
- package/lib/adapters/doc-camscanner/index.js +102 -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/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-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-dongchedi/index.js +360 -0
- package/lib/adapters/social-douban/index.js +564 -0
- package/lib/adapters/travel-didi-consumer/index.js +148 -0
- package/lib/adapters/video-xigua/index.js +68 -0
- package/lib/adapters/wework-pc/index.js +31 -0
- package/lib/index.js +52 -0
- package/package.json +1 -1
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
4
|
+
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const os = require("node:os");
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
DoubanAdapter,
|
|
11
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
12
|
+
VALID_SNAPSHOT_KINDS,
|
|
13
|
+
extractData,
|
|
14
|
+
} = require("../lib/adapters/social-douban");
|
|
15
|
+
const { assertAdapter } = require("../lib/adapter-spec");
|
|
16
|
+
const { validateBatch } = require("../lib/batch");
|
|
17
|
+
|
|
18
|
+
// 豆瓣 (Douban / Frodo) — 书影音 interest graph. Mirrors social-zhihu's two-mode
|
|
19
|
+
// custom-normalize shape + video-base MEDIA event+item for marks. Frodo signing
|
|
20
|
+
// is injected (signProvider) so the adapter stays pure-Node.
|
|
21
|
+
|
|
22
|
+
function writeSnapshot(dir, snapshot) {
|
|
23
|
+
const p = path.join(dir, "social-douban.json");
|
|
24
|
+
fs.writeFileSync(p, JSON.stringify(snapshot), "utf-8");
|
|
25
|
+
return p;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("DoubanAdapter snapshot mode", () => {
|
|
29
|
+
let tmpDir;
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "douban-snap-"));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("exports schema constants", () => {
|
|
35
|
+
expect(SNAPSHOT_SCHEMA_VERSION).toBe(1);
|
|
36
|
+
expect(VALID_SNAPSHOT_KINDS).toEqual(["interest", "review", "follow"]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("authenticate(inputPath) ok when readable", async () => {
|
|
40
|
+
const p = writeSnapshot(tmpDir, { schemaVersion: 1, snapshottedAt: Date.now(), events: [] });
|
|
41
|
+
const a = new DoubanAdapter();
|
|
42
|
+
const res = await a.authenticate({ inputPath: p });
|
|
43
|
+
expect(res.ok).toBe(true);
|
|
44
|
+
expect(res.mode).toBe("snapshot-file");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("authenticate() no input → NO_INPUT", async () => {
|
|
48
|
+
const a = new DoubanAdapter();
|
|
49
|
+
const res = await a.authenticate({});
|
|
50
|
+
expect(res.ok).toBe(false);
|
|
51
|
+
expect(res.reason).toBe("NO_INPUT");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("sync() without input throws with signProvider hint", async () => {
|
|
55
|
+
const a = new DoubanAdapter();
|
|
56
|
+
let threw = null;
|
|
57
|
+
try {
|
|
58
|
+
for await (const _r of a.sync({})) { /* drain */ }
|
|
59
|
+
} catch (err) {
|
|
60
|
+
threw = err;
|
|
61
|
+
}
|
|
62
|
+
expect(threw).toBeTruthy();
|
|
63
|
+
expect(String(threw.message)).toMatch(/signProvider/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("rejects schemaVersion mismatch", async () => {
|
|
67
|
+
const p = writeSnapshot(tmpDir, { schemaVersion: 99, snapshottedAt: Date.now(), events: [] });
|
|
68
|
+
const a = new DoubanAdapter();
|
|
69
|
+
let threw = null;
|
|
70
|
+
try {
|
|
71
|
+
for await (const _r of a.sync({ inputPath: p })) { /* drain */ }
|
|
72
|
+
} catch (err) {
|
|
73
|
+
threw = err;
|
|
74
|
+
}
|
|
75
|
+
expect(String(threw.message)).toMatch(/schemaVersion mismatch/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("interest (看过电影) → MEDIA event + MEDIA item, normalizes cleanly", async () => {
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
const p = writeSnapshot(tmpDir, {
|
|
81
|
+
schemaVersion: 1,
|
|
82
|
+
snapshottedAt: now,
|
|
83
|
+
account: { userId: "12345", name: "alice" },
|
|
84
|
+
events: [
|
|
85
|
+
{
|
|
86
|
+
kind: "interest", id: "interest-m1", subjectId: "26266893",
|
|
87
|
+
subjectType: "movie", title: "瞬息全宇宙", status: "done",
|
|
88
|
+
myRating: 5, comment: "好看", createdTime: 1700000000, url: "https://movie.douban.com/subject/26266893/",
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
});
|
|
92
|
+
const a = new DoubanAdapter();
|
|
93
|
+
const raws = [];
|
|
94
|
+
for await (const r of a.sync({ inputPath: p })) raws.push(r);
|
|
95
|
+
expect(raws.length).toBe(1);
|
|
96
|
+
expect(raws[0].kind).toBe("interest");
|
|
97
|
+
expect(raws[0].originalId).toBe("douban:interest:interest-m1");
|
|
98
|
+
|
|
99
|
+
const batch = a.normalize(raws[0]);
|
|
100
|
+
expect(validateBatch(batch).valid).toBe(true);
|
|
101
|
+
expect(batch.events.length).toBe(1);
|
|
102
|
+
expect(batch.items.length).toBe(1);
|
|
103
|
+
expect(batch.events[0].subtype).toBe("media");
|
|
104
|
+
expect(batch.events[0].content.title).toContain("看过电影: 瞬息全宇宙");
|
|
105
|
+
expect(batch.events[0].extra.myRating).toBe(5);
|
|
106
|
+
expect(batch.items[0].name).toBe("瞬息全宇宙");
|
|
107
|
+
expect(JSON.stringify(batch)).toContain("好看");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("interest status maps to verb (mark=想看, doing=在看, done=看过)", async () => {
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
const cases = [
|
|
113
|
+
{ status: "mark", verb: "想看" },
|
|
114
|
+
{ status: "doing", verb: "在看" },
|
|
115
|
+
{ status: "done", verb: "看过" },
|
|
116
|
+
];
|
|
117
|
+
for (const c of cases) {
|
|
118
|
+
const p = writeSnapshot(tmpDir, {
|
|
119
|
+
schemaVersion: 1, snapshottedAt: now,
|
|
120
|
+
events: [
|
|
121
|
+
{ kind: "interest", id: `i-${c.status}`, subjectId: "1", subjectType: "book",
|
|
122
|
+
title: "三体", status: c.status, createdTime: now },
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
const a = new DoubanAdapter();
|
|
126
|
+
const raws = [];
|
|
127
|
+
for await (const r of a.sync({ inputPath: p })) raws.push(r);
|
|
128
|
+
const batch = a.normalize(raws[0]);
|
|
129
|
+
expect(batch.events[0].content.title).toContain(`${c.verb}图书: 三体`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("review → POST event", async () => {
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
const p = writeSnapshot(tmpDir, {
|
|
136
|
+
schemaVersion: 1, snapshottedAt: now,
|
|
137
|
+
events: [
|
|
138
|
+
{ kind: "review", id: "review-1", reviewId: "9001", title: "一篇影评",
|
|
139
|
+
abstract: "<p>写得不错</p>", subjectTitle: "瞬息全宇宙", rating: 4, createdTime: now },
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
const a = new DoubanAdapter();
|
|
143
|
+
const raws = [];
|
|
144
|
+
for await (const r of a.sync({ inputPath: p })) raws.push(r);
|
|
145
|
+
const batch = a.normalize(raws[0]);
|
|
146
|
+
expect(validateBatch(batch).valid).toBe(true);
|
|
147
|
+
expect(batch.events[0].subtype).toBe("post");
|
|
148
|
+
expect(batch.events[0].content.title).toBe("一篇影评");
|
|
149
|
+
expect(batch.events[0].content.text).toBe("写得不错"); // html stripped
|
|
150
|
+
expect(batch.events[0].extra.rating).toBe(4);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("follow → CONTACT person with douban-id identifier", async () => {
|
|
154
|
+
const now = Date.now();
|
|
155
|
+
const p = writeSnapshot(tmpDir, {
|
|
156
|
+
schemaVersion: 1, snapshottedAt: now,
|
|
157
|
+
events: [
|
|
158
|
+
{ kind: "follow", id: "follow-u1", memberId: "67890", name: "豆友小张",
|
|
159
|
+
url: "https://www.douban.com/people/67890/", capturedAt: now },
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
const a = new DoubanAdapter();
|
|
163
|
+
const raws = [];
|
|
164
|
+
for await (const r of a.sync({ inputPath: p })) raws.push(r);
|
|
165
|
+
const batch = a.normalize(raws[0]);
|
|
166
|
+
expect(validateBatch(batch).valid).toBe(true);
|
|
167
|
+
expect(batch.persons.length).toBe(1);
|
|
168
|
+
expect(batch.persons[0].id).toBe("person-douban-67890");
|
|
169
|
+
expect(batch.persons[0].names).toEqual(["豆友小张"]);
|
|
170
|
+
expect(batch.persons[0].identifiers["douban-id"]).toEqual(["67890"]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("respects per-kind include opt-out + limit", async () => {
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
const events = [
|
|
176
|
+
{ kind: "interest", id: "i1", subjectId: "1", subjectType: "movie", title: "a", status: "done", createdTime: now },
|
|
177
|
+
{ kind: "review", id: "r1", reviewId: "2", title: "b", createdTime: now },
|
|
178
|
+
{ kind: "follow", id: "f1", memberId: "3", name: "c", capturedAt: now },
|
|
179
|
+
];
|
|
180
|
+
const p = writeSnapshot(tmpDir, { schemaVersion: 1, snapshottedAt: now, events });
|
|
181
|
+
const a = new DoubanAdapter();
|
|
182
|
+
const raws = [];
|
|
183
|
+
for await (const r of a.sync({ inputPath: p, include: { review: false, follow: false } })) raws.push(r);
|
|
184
|
+
expect(raws.length).toBe(1);
|
|
185
|
+
expect(raws[0].kind).toBe("interest");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("filters unknown kinds (forward compat)", async () => {
|
|
189
|
+
const now = Date.now();
|
|
190
|
+
const p = writeSnapshot(tmpDir, {
|
|
191
|
+
schemaVersion: 1, snapshottedAt: now,
|
|
192
|
+
events: [
|
|
193
|
+
{ kind: "interest", id: "i1", subjectId: "1", subjectType: "movie", title: "a", status: "done", createdTime: now },
|
|
194
|
+
{ kind: "status", id: "s1" },
|
|
195
|
+
],
|
|
196
|
+
});
|
|
197
|
+
const a = new DoubanAdapter();
|
|
198
|
+
const raws = [];
|
|
199
|
+
for await (const r of a.sync({ inputPath: p })) raws.push(r);
|
|
200
|
+
expect(raws.length).toBe(1);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("advertises snapshot + cookie-api capabilities; passes assertAdapter", () => {
|
|
204
|
+
const a = new DoubanAdapter();
|
|
205
|
+
expect(a.capabilities).toContain("sync:snapshot");
|
|
206
|
+
expect(a.capabilities).toContain("sync:cookie-api");
|
|
207
|
+
expect(assertAdapter(a).ok).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("DoubanAdapter cookie-api mode", () => {
|
|
212
|
+
it("authenticate(cookie) requires account.userId", async () => {
|
|
213
|
+
const a = new DoubanAdapter({ account: { cookies: "bid=ok" } });
|
|
214
|
+
const res = await a.authenticate();
|
|
215
|
+
expect(res.ok).toBe(false);
|
|
216
|
+
expect(res.reason).toBe("NO_ACCOUNT_USER_ID");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("authenticate(cookie) ok when userId + cookies present", async () => {
|
|
220
|
+
const a = new DoubanAdapter({ account: { userId: "12345", cookies: "bid=ok" } });
|
|
221
|
+
const res = await a.authenticate();
|
|
222
|
+
expect(res.ok).toBe(true);
|
|
223
|
+
expect(res.mode).toBe("cookie");
|
|
224
|
+
expect(res.account).toBe("12345");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("fetches interests/reviews/following across the plan and normalizes", async () => {
|
|
228
|
+
const byUrl = (url) => {
|
|
229
|
+
if (url.includes("/interests")) {
|
|
230
|
+
return {
|
|
231
|
+
interests: [
|
|
232
|
+
{ id: "int1", status: "done", create_time: "2024-01-02 10:00:00",
|
|
233
|
+
rating: { value: 4 }, comment: "不错",
|
|
234
|
+
subject: { id: "26266893", type: "movie", title: "瞬息全宇宙", url: "u" } },
|
|
235
|
+
],
|
|
236
|
+
total: 1,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (url.includes("/reviews")) {
|
|
240
|
+
return { reviews: [{ id: "rev1", title: "影评", abstract: "好", create_time: "2024-01-03 10:00:00", subject: { title: "瞬息全宇宙" } }], total: 1 };
|
|
241
|
+
}
|
|
242
|
+
if (url.includes("/following")) {
|
|
243
|
+
return { users: [{ id: "u9", name: "豆友" }], total: 1 };
|
|
244
|
+
}
|
|
245
|
+
return { total: 0 };
|
|
246
|
+
};
|
|
247
|
+
const fetchFn = async (opts) => byUrl(opts.url);
|
|
248
|
+
const a = new DoubanAdapter({ account: { userId: "12345", cookies: "bid=ok" }, fetchFn });
|
|
249
|
+
const raws = [];
|
|
250
|
+
for await (const r of a.sync({})) raws.push(r);
|
|
251
|
+
expect(raws.map((r) => r.kind).sort()).toEqual(["follow", "interest", "review"]);
|
|
252
|
+
|
|
253
|
+
const interest = raws.find((r) => r.kind === "interest");
|
|
254
|
+
const ib = a.normalize(interest);
|
|
255
|
+
expect(validateBatch(ib).valid).toBe(true);
|
|
256
|
+
expect(ib.events[0].subtype).toBe("media");
|
|
257
|
+
expect(ib.events[0].content.title).toContain("看过电影: 瞬息全宇宙");
|
|
258
|
+
expect(ib.events[0].extra.myRating).toBe(4);
|
|
259
|
+
expect(ib.items[0].name).toBe("瞬息全宇宙");
|
|
260
|
+
|
|
261
|
+
const follow = raws.find((r) => r.kind === "follow");
|
|
262
|
+
const fb = a.normalize(follow);
|
|
263
|
+
expect(fb.persons[0].identifiers["douban-id"]).toEqual(["u9"]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("invokes signProvider and passes sign to fetchFn", async () => {
|
|
267
|
+
let seenSign = null;
|
|
268
|
+
const signProvider = async () => "SIG-1";
|
|
269
|
+
const fetchFn = async (opts) => {
|
|
270
|
+
seenSign = opts.sign;
|
|
271
|
+
return { total: 0 };
|
|
272
|
+
};
|
|
273
|
+
const a = new DoubanAdapter({
|
|
274
|
+
account: { userId: "12345", cookies: "bid=ok" },
|
|
275
|
+
fetchFn,
|
|
276
|
+
signProvider,
|
|
277
|
+
});
|
|
278
|
+
for await (const _r of a.sync({ include: { review: false, follow: false } })) { /* drain */ }
|
|
279
|
+
expect(seenSign).toBe("SIG-1");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("passes sign: null when no signProvider", async () => {
|
|
283
|
+
let seen = "unset";
|
|
284
|
+
const fetchFn = async (opts) => {
|
|
285
|
+
seen = opts.sign;
|
|
286
|
+
return { total: 0 };
|
|
287
|
+
};
|
|
288
|
+
const a = new DoubanAdapter({ account: { userId: "12345", cookies: "bid=ok" }, fetchFn });
|
|
289
|
+
for await (const _r of a.sync({ include: { review: false, follow: false } })) { /* drain */ }
|
|
290
|
+
expect(seen).toBe(null);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("paginates interests with start cursor until total reached", async () => {
|
|
294
|
+
const seenStarts = [];
|
|
295
|
+
const all = Array.from({ length: 25 }, (_, i) => ({
|
|
296
|
+
id: `int${i}`, status: "done", create_time: "2024-01-01 00:00:00",
|
|
297
|
+
subject: { id: String(i), type: "book", title: `书${i}` },
|
|
298
|
+
}));
|
|
299
|
+
const fetchFn = async (opts) => {
|
|
300
|
+
if (!opts.url.includes("/interests")) return { total: 0 };
|
|
301
|
+
const start = opts.query.start;
|
|
302
|
+
seenStarts.push(start);
|
|
303
|
+
return { interests: all.slice(start, start + 20), total: all.length };
|
|
304
|
+
};
|
|
305
|
+
const a = new DoubanAdapter({ account: { userId: "12345", cookies: "bid=ok" }, fetchFn });
|
|
306
|
+
const raws = [];
|
|
307
|
+
for await (const r of a.sync({ include: { review: false, follow: false } })) raws.push(r);
|
|
308
|
+
expect(raws.length).toBe(25);
|
|
309
|
+
expect(seenStarts).toEqual([0, 20]);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("extractData tolerates Frodo response shapes", () => {
|
|
313
|
+
expect(extractData({ interests: [1] }, "interest")).toEqual([1]);
|
|
314
|
+
expect(extractData({ reviews: [2] }, "review")).toEqual([2]);
|
|
315
|
+
expect(extractData({ users: [3] }, "follow")).toEqual([3]);
|
|
316
|
+
expect(extractData({ items: [4] })).toEqual([4]);
|
|
317
|
+
expect(extractData([5])).toEqual([5]);
|
|
318
|
+
expect(extractData({})).toEqual([]);
|
|
319
|
+
expect(extractData(null)).toEqual([]);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("uses opts.*Url overrides", async () => {
|
|
323
|
+
const seen = [];
|
|
324
|
+
const fetchFn = async (opts) => {
|
|
325
|
+
seen.push(opts.url);
|
|
326
|
+
return { total: 0 };
|
|
327
|
+
};
|
|
328
|
+
const a = new DoubanAdapter({
|
|
329
|
+
account: { userId: "12345", cookies: "bid=ok" },
|
|
330
|
+
fetchFn,
|
|
331
|
+
interestsUrl: "https://x/i/{id}",
|
|
332
|
+
reviewsUrl: "https://x/r/{id}",
|
|
333
|
+
followingUrl: "https://x/f/{id}",
|
|
334
|
+
});
|
|
335
|
+
for await (const _r of a.sync({})) { /* drain */ }
|
|
336
|
+
expect(seen).toContain("https://x/i/12345");
|
|
337
|
+
expect(seen).toContain("https://x/r/12345");
|
|
338
|
+
expect(seen).toContain("https://x/f/12345");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("default fetchFn throws legible error in cookie mode without injection", async () => {
|
|
342
|
+
const a = new DoubanAdapter({ account: { userId: "12345", cookies: "bid=ok" } });
|
|
343
|
+
let threw = null;
|
|
344
|
+
try {
|
|
345
|
+
for await (const _r of a.sync({})) { /* drain */ }
|
|
346
|
+
} catch (err) {
|
|
347
|
+
threw = err;
|
|
348
|
+
}
|
|
349
|
+
expect(String(threw.message)).toMatch(/no fetchFn configured/);
|
|
350
|
+
});
|
|
351
|
+
});
|
package/lib/adapter-guide.js
CHANGED
|
@@ -33,8 +33,11 @@ const DISPLAY_NAMES = Object.freeze({
|
|
|
33
33
|
"social-bilibili": "哔哩哔哩",
|
|
34
34
|
"social-weibo": "微博",
|
|
35
35
|
"social-zhihu": "知乎",
|
|
36
|
+
"social-douban": "豆瓣",
|
|
36
37
|
"recruit-boss": "BOSS 直聘",
|
|
37
38
|
"social-csdn": "CSDN",
|
|
39
|
+
"social-dongchedi": "懂车帝",
|
|
40
|
+
"biz-tianyancha": "天眼查",
|
|
38
41
|
"social-douyin": "抖音",
|
|
39
42
|
"social-xiaohongshu": "小红书",
|
|
40
43
|
"social-toutiao": "今日头条",
|
|
@@ -47,14 +50,18 @@ const DISPLAY_NAMES = Object.freeze({
|
|
|
47
50
|
"qq-pc": "QQ(电脑版 NT)",
|
|
48
51
|
"dingtalk-pc": "钉钉(电脑版)",
|
|
49
52
|
"feishu-pc": "飞书(电脑版)",
|
|
53
|
+
"wework-pc": "企业微信(电脑版)",
|
|
50
54
|
"email-imap": "邮箱(IMAP)",
|
|
51
55
|
"finance-alipay": "支付宝",
|
|
52
56
|
"alipay-bill": "支付宝账单",
|
|
53
57
|
"shopping-taobao": "淘宝",
|
|
54
58
|
"shopping-jd": "京东",
|
|
55
59
|
"shopping-meituan": "美团",
|
|
60
|
+
"shopping-eleme": "饿了么",
|
|
56
61
|
"shopping-pinduoduo": "拼多多",
|
|
57
62
|
"shopping-dianping": "大众点评",
|
|
63
|
+
"shopping-xianyu": "闲鱼",
|
|
64
|
+
"shopping-vipshop": "唯品会",
|
|
58
65
|
"travel-12306": "12306 铁路",
|
|
59
66
|
"travel-ctrip": "携程",
|
|
60
67
|
"travel-tongcheng": "同程旅行",
|
|
@@ -70,12 +77,31 @@ const DISPLAY_NAMES = Object.freeze({
|
|
|
70
77
|
"apple-health": "Apple 健康",
|
|
71
78
|
"netease-music": "网易云音乐",
|
|
72
79
|
"music-kugou": "酷狗音乐",
|
|
80
|
+
"music-qq": "QQ音乐",
|
|
81
|
+
"audio-ximalaya": "喜马拉雅",
|
|
82
|
+
"reading-fanqie": "番茄小说",
|
|
83
|
+
"reading-qimao": "七猫小说",
|
|
84
|
+
"fitness-joyrun": "悦跑圈",
|
|
85
|
+
"fitness-keep": "Keep",
|
|
86
|
+
"travel-didi-consumer": "滴滴出行",
|
|
87
|
+
"car-mercedesme": "奔驰 Mercedes me",
|
|
73
88
|
"video-iqiyi": "爱奇艺",
|
|
74
89
|
"video-tencent": "腾讯视频",
|
|
90
|
+
"video-xigua": "西瓜视频",
|
|
75
91
|
"weread": "微信读书",
|
|
76
92
|
"doc-wps": "WPS 云文档",
|
|
77
93
|
"doc-tencent-docs": "腾讯文档",
|
|
78
94
|
"doc-baidu-netdisk": "百度网盘",
|
|
95
|
+
"doc-camscanner": "扫描全能王",
|
|
96
|
+
"gov-ixiamen": "i厦门",
|
|
97
|
+
"health-meiyou": "美柚",
|
|
98
|
+
"gov-tax": "个人所得税",
|
|
99
|
+
"bank-cmbc": "民生银行",
|
|
100
|
+
"bank-boc": "中国银行",
|
|
101
|
+
"bank-bankcomm": "交通银行",
|
|
102
|
+
"bank-icbc": "工商银行",
|
|
103
|
+
"finance-dcep": "数字人民币",
|
|
104
|
+
"gov-12123": "交管12123",
|
|
79
105
|
"browser-history-chrome": "Chrome 浏览历史",
|
|
80
106
|
"browser-history-edge": "Edge 浏览历史",
|
|
81
107
|
"vscode": "VS Code",
|
|
@@ -88,7 +114,8 @@ const DISPLAY_NAMES = Object.freeze({
|
|
|
88
114
|
|
|
89
115
|
// Shared guide for honest best-effort desktop IM local-DB sources (钉钉/飞书).
|
|
90
116
|
function localImPcGuide(platform) {
|
|
91
|
-
const adapterName =
|
|
117
|
+
const adapterName =
|
|
118
|
+
platform === "钉钉" ? "dingtalk-pc" : platform === "企业微信" ? "wework-pc" : "feishu-pc";
|
|
92
119
|
return {
|
|
93
120
|
summary: `采集${platform}电脑版的聊天记录(来自本地数据库)。⚠️ v0.1 实验性:${platform}桌面库为私有结构、可能加密、随版本变化,文本解析为尽力而为,原始行会完整保留以便后续解析。`,
|
|
94
121
|
methods: [
|
|
@@ -406,6 +433,7 @@ const ADAPTER_OVERRIDES = Object.freeze({
|
|
|
406
433
|
|
|
407
434
|
"dingtalk-pc": localImPcGuide("钉钉"),
|
|
408
435
|
"feishu-pc": localImPcGuide("飞书"),
|
|
436
|
+
"wework-pc": localImPcGuide("企业微信"),
|
|
409
437
|
|
|
410
438
|
"social-bilibili": socialAdbGuide("哔哩哔哩", "观看历史 / 收藏 / 动态 / 关注"),
|
|
411
439
|
"social-weibo": socialAdbGuide("微博", "微博 / 收藏 / 关注"),
|
|
@@ -545,9 +573,9 @@ function getAdapterGuide(name, category) {
|
|
|
545
573
|
// usable standalone, e.g. CLI without a live readiness probe).
|
|
546
574
|
function _inferCategory(name) {
|
|
547
575
|
if (ADAPTER_OVERRIDES[name] && name === "wechat") return READINESS_CATEGORY.DEVICE;
|
|
548
|
-
if (/^(email-imap|finance-alipay|alipay-bill|ai-chat-history|weread|doc-wps|doc-tencent-docs|doc-baidu-netdisk|recruit-boss|social-csdn)$/.test(name))
|
|
576
|
+
if (/^(email-imap|finance-alipay|alipay-bill|ai-chat-history|weread|doc-wps|doc-tencent-docs|doc-baidu-netdisk|doc-camscanner|recruit-boss|social-csdn|social-douban|social-dongchedi|biz-tianyancha|gov-ixiamen|health-meiyou|gov-tax|bank-cmbc|bank-boc|bank-bankcomm|finance-dcep|gov-12123|bank-icbc)$/.test(name))
|
|
549
577
|
return READINESS_CATEGORY.CREDENTIAL;
|
|
550
|
-
if (/^(messaging-(telegram|whatsapp)|wechat|wechat-pc|messaging-qq|qq-pc|dingtalk-pc|feishu-pc|travel-amap)$/.test(name))
|
|
578
|
+
if (/^(messaging-(telegram|whatsapp)|wechat|wechat-pc|messaging-qq|qq-pc|dingtalk-pc|feishu-pc|wework-pc|travel-amap)$/.test(name))
|
|
551
579
|
return READINESS_CATEGORY.DEVICE;
|
|
552
580
|
if (
|
|
553
581
|
/^(browser-history-|vscode|win-recent|git-activity|shell-history|local-files|apple-health)/.test(
|