@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,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from "vitest";
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const os = require("node:os");
|
|
7
|
+
const crypto = require("node:crypto");
|
|
8
|
+
|
|
9
|
+
const fanqie = require("../../lib/adapters/reading-fanqie");
|
|
10
|
+
const qimao = require("../../lib/adapters/reading-qimao");
|
|
11
|
+
|
|
12
|
+
function writeTmp(content) {
|
|
13
|
+
const p = path.join(os.tmpdir(), `cc-read-${crypto.randomUUID()}.json`);
|
|
14
|
+
fs.writeFileSync(p, content, "utf-8");
|
|
15
|
+
return p;
|
|
16
|
+
}
|
|
17
|
+
async function collect(gen) {
|
|
18
|
+
const out = [];
|
|
19
|
+
for await (const x of gen) out.push(x);
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
const COOKIES = "novel_sid=abc; uid=1";
|
|
23
|
+
|
|
24
|
+
describe("reading wrappers identity + mappers", () => {
|
|
25
|
+
it("names", () => {
|
|
26
|
+
expect(fanqie.NAME).toBe("reading-fanqie");
|
|
27
|
+
expect(qimao.NAME).toBe("reading-qimao");
|
|
28
|
+
expect(new fanqie.FanqieReadingAdapter().name).toBe("reading-fanqie");
|
|
29
|
+
expect(new qimao.QimaoReadingAdapter().name).toBe("reading-qimao");
|
|
30
|
+
});
|
|
31
|
+
it("low sensitivity, no legalGate", () => {
|
|
32
|
+
for (const A of [new fanqie.FanqieReadingAdapter(), new qimao.QimaoReadingAdapter()]) {
|
|
33
|
+
expect(A.dataDisclosure.sensitivity).toBe("low");
|
|
34
|
+
expect(A.dataDisclosure.legalGate).toBe(false);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
it("fanqie mapItem (book_id / book_name / read_progress)", () => {
|
|
38
|
+
const r = fanqie.mapItem({ book_id: "B1", book_name: "诡秘之主", author: "爱潜水的乌贼", category: "玄幻", read_progress: "0.42", last_chapter_title: "第100章", read_time: 1716383000 });
|
|
39
|
+
expect(r).toMatchObject({ bookId: "B1", title: "诡秘之主", author: "爱潜水的乌贼", category: "玄幻", chapter: "第100章" });
|
|
40
|
+
expect(r.progress).toBeCloseTo(0.42, 2);
|
|
41
|
+
expect(r.occurredAt).toBe(1716383000000);
|
|
42
|
+
expect(r.url).toContain("fanqienovel.com/page/B1");
|
|
43
|
+
expect(fanqie.mapItem({ book_name: "x" })).toBe(null);
|
|
44
|
+
});
|
|
45
|
+
it("qimao mapItem (book_id / title / read_proportion)", () => {
|
|
46
|
+
const r = qimao.mapItem({ book_id: "Q1", title: "大奉打更人", author: "卖报小郎君", read_proportion: 0.8 });
|
|
47
|
+
expect(r).toMatchObject({ bookId: "Q1", title: "大奉打更人", author: "卖报小郎君" });
|
|
48
|
+
expect(r.progress).toBeCloseTo(0.8, 2);
|
|
49
|
+
expect(qimao.mapItem({ title: "x" })).toBe(null);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("FanqieReadingAdapter (snapshot + cookie-api via _reading-base)", () => {
|
|
54
|
+
const SNAP = JSON.stringify({
|
|
55
|
+
schemaVersion: 1,
|
|
56
|
+
snapshottedAt: 1716383000000,
|
|
57
|
+
account: { userId: "u1" },
|
|
58
|
+
events: [
|
|
59
|
+
{ kind: "read", id: "read-B1", bookId: "B1", title: "诡秘之主", author: "乌贼", category: "玄幻", progress: 0.42 },
|
|
60
|
+
{ kind: "favourite", id: "fav-B2", bookId: "B2", title: "全职高手", author: "蝴蝶蓝" },
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("read → MEDIA event + DOCUMENT item; favourite → LIKE event", async () => {
|
|
65
|
+
const p = writeTmp(SNAP);
|
|
66
|
+
try {
|
|
67
|
+
const a = new fanqie.FanqieReadingAdapter();
|
|
68
|
+
expect((await a.authenticate({ inputPath: p })).mode).toBe("snapshot-file");
|
|
69
|
+
const items = await collect(a.sync({ inputPath: p }));
|
|
70
|
+
expect(items).toHaveLength(2);
|
|
71
|
+
const read = a.normalize(items[0]);
|
|
72
|
+
expect(read.events[0].subtype).toBe("media");
|
|
73
|
+
expect(read.events[0].content.title).toContain("读了: 诡秘之主");
|
|
74
|
+
expect(read.items[0].subtype).toBe("document");
|
|
75
|
+
expect(read.items[0].extra.platform).toBe("fanqie");
|
|
76
|
+
expect(read.events[0].extra.progress).toBeCloseTo(0.42, 2);
|
|
77
|
+
expect(items[0].originalId).toBe("fanqie:read:read-B1");
|
|
78
|
+
const fav = a.normalize(items[1]);
|
|
79
|
+
expect(fav.events[0].subtype).toBe("like");
|
|
80
|
+
expect(fav.events[0].content.title).toContain("收藏: 全职高手");
|
|
81
|
+
} finally {
|
|
82
|
+
fs.unlinkSync(p);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("cookie-api: read + favourite fetch + sign seam", async () => {
|
|
87
|
+
let signed = 0;
|
|
88
|
+
const a = new fanqie.FanqieReadingAdapter({
|
|
89
|
+
account: { cookies: COOKIES, userId: "u1" },
|
|
90
|
+
signProvider: async () => { signed += 1; return "sig"; },
|
|
91
|
+
fetchFn: async ({ url, query }) => {
|
|
92
|
+
if (query.page > 1) return { list: [] };
|
|
93
|
+
if (url.includes("/history")) return { data: { list: [{ book_id: "B9", book_name: "斗破", read_time: 1716383000 }] } };
|
|
94
|
+
return { data: { books: [{ book_id: "B8", book_name: "遮天" }] } };
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
expect(await a.authenticate()).toEqual({ ok: true, account: "u1", mode: "cookie" });
|
|
98
|
+
const items = await collect(a.sync({}));
|
|
99
|
+
expect(items).toHaveLength(2);
|
|
100
|
+
expect(items.map((i) => i.originalId).sort()).toEqual(["fanqie:favourite:B8", "fanqie:read:B9"]);
|
|
101
|
+
expect(signed).toBeGreaterThan(0);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("default fetch / no input throw", async () => {
|
|
105
|
+
await expect(collect(new fanqie.FanqieReadingAdapter({ account: { cookies: COOKIES } }).sync({}))).rejects.toThrow(/no fetchFn/);
|
|
106
|
+
await expect(collect(new fanqie.FanqieReadingAdapter().sync({}))).rejects.toThrow(/needs opts.inputPath/);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from "vitest";
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const os = require("node:os");
|
|
7
|
+
const crypto = require("node:crypto");
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
DongchediAdapter,
|
|
11
|
+
extractData,
|
|
12
|
+
isEnd,
|
|
13
|
+
NAME,
|
|
14
|
+
VERSION,
|
|
15
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
16
|
+
} = require("../../lib/adapters/social-dongchedi");
|
|
17
|
+
|
|
18
|
+
function writeTmp(content) {
|
|
19
|
+
const p = path.join(os.tmpdir(), `cc-dcd-${crypto.randomUUID()}.json`);
|
|
20
|
+
fs.writeFileSync(p, content, "utf-8");
|
|
21
|
+
return p;
|
|
22
|
+
}
|
|
23
|
+
async function collect(gen) {
|
|
24
|
+
const out = [];
|
|
25
|
+
for await (const x of gen) out.push(x);
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const COOKIES = "tt_webid=abc; sessionid=xyz";
|
|
30
|
+
|
|
31
|
+
const SNAP = JSON.stringify({
|
|
32
|
+
schemaVersion: 1,
|
|
33
|
+
snapshottedAt: 1716383000000,
|
|
34
|
+
account: { userId: "u1" },
|
|
35
|
+
events: [
|
|
36
|
+
{ kind: "favourite", id: "fav-1", itemId: "G1", title: "2026 新能源车横评", contentType: "article", url: "https://x/G1", capturedAt: 1716300000000 },
|
|
37
|
+
{ kind: "follow", id: "follow-S1", followId: "S1", name: "理想 L 系列", followType: "series", capturedAt: 1716320000000 },
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("constants + helpers", () => {
|
|
42
|
+
it("name/version/schema", () => {
|
|
43
|
+
expect(NAME).toBe("social-dongchedi");
|
|
44
|
+
expect(VERSION).toBe("0.1.0");
|
|
45
|
+
expect(SNAPSHOT_SCHEMA_VERSION).toBe(1);
|
|
46
|
+
});
|
|
47
|
+
it("extractData tolerant", () => {
|
|
48
|
+
expect(extractData({ data: [{ id: 1 }] })).toHaveLength(1);
|
|
49
|
+
expect(extractData({ data: { favorite_list: [{ id: 1 }] } })).toHaveLength(1);
|
|
50
|
+
expect(extractData({ data: { follow_list: [{ id: 1 }] } })).toHaveLength(1);
|
|
51
|
+
expect(extractData({})).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
it("isEnd reads has_more", () => {
|
|
54
|
+
expect(isEnd({ data: { has_more: false } })).toBe(true);
|
|
55
|
+
expect(isEnd({ has_more: 0 })).toBe(true);
|
|
56
|
+
expect(isEnd({ data: { has_more: true } })).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("DongchediAdapter snapshot mode", () => {
|
|
61
|
+
it("authenticate validates inputPath", async () => {
|
|
62
|
+
const p = writeTmp(SNAP);
|
|
63
|
+
try {
|
|
64
|
+
const a = new DongchediAdapter();
|
|
65
|
+
expect((await a.authenticate({ inputPath: p })).mode).toBe("snapshot-file");
|
|
66
|
+
expect((await a.authenticate({ inputPath: path.join(os.tmpdir(), "no-dcd.json") })).reason).toBe("INPUT_PATH_UNREADABLE");
|
|
67
|
+
} finally {
|
|
68
|
+
fs.unlinkSync(p);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("sync 2 kinds + normalize favourite→like / follow→person", async () => {
|
|
73
|
+
const p = writeTmp(SNAP);
|
|
74
|
+
try {
|
|
75
|
+
const a = new DongchediAdapter();
|
|
76
|
+
const items = await collect(a.sync({ inputPath: p }));
|
|
77
|
+
expect(items.map((x) => x.kind)).toEqual(["favourite", "follow"]);
|
|
78
|
+
|
|
79
|
+
const fav = a.normalize(items[0]);
|
|
80
|
+
expect(fav.events[0].subtype).toBe("like");
|
|
81
|
+
expect(fav.events[0].content.title).toBe("收藏: 2026 新能源车横评");
|
|
82
|
+
expect(fav.events[0].extra.contentType).toBe("article");
|
|
83
|
+
|
|
84
|
+
const fol = a.normalize(items[1]);
|
|
85
|
+
expect(fol.persons[0].subtype).toBe("contact");
|
|
86
|
+
expect(fol.persons[0].names).toEqual(["理想 L 系列"]);
|
|
87
|
+
expect(fol.persons[0].identifiers["dongchedi-id"]).toEqual(["S1"]);
|
|
88
|
+
expect(fol.persons[0].extra.followType).toBe("series");
|
|
89
|
+
} finally {
|
|
90
|
+
fs.unlinkSync(p);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("include + limit + schema mismatch + unknown kind", async () => {
|
|
95
|
+
const p = writeTmp(SNAP);
|
|
96
|
+
try {
|
|
97
|
+
const a = new DongchediAdapter();
|
|
98
|
+
expect((await collect(a.sync({ inputPath: p, include: { favourite: false } }))).map((x) => x.kind)).toEqual(["follow"]);
|
|
99
|
+
expect(await collect(a.sync({ inputPath: p, limit: 1 }))).toHaveLength(1);
|
|
100
|
+
expect(() => a.normalize({ kind: "bogus", payload: {} })).toThrow(/unknown kind/);
|
|
101
|
+
} finally {
|
|
102
|
+
fs.unlinkSync(p);
|
|
103
|
+
}
|
|
104
|
+
const bad = writeTmp(JSON.stringify({ schemaVersion: 9, events: [] }));
|
|
105
|
+
try {
|
|
106
|
+
const a = new DongchediAdapter();
|
|
107
|
+
await expect(collect(a.sync({ inputPath: bad }))).rejects.toThrow(/schemaVersion mismatch/);
|
|
108
|
+
} finally {
|
|
109
|
+
fs.unlinkSync(bad);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("DongchediAdapter cookie-api mode", () => {
|
|
115
|
+
it("authenticate cookie (userId optional)", async () => {
|
|
116
|
+
const a = new DongchediAdapter({ account: { cookies: COOKIES } });
|
|
117
|
+
expect(await a.authenticate()).toEqual({ ok: true, account: null, mode: "cookie" });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("sync fetches favourites + follows, normalizes", async () => {
|
|
121
|
+
const byUrl = (u) => (u.includes("favorite") ? "favourite" : "follow");
|
|
122
|
+
const data = {
|
|
123
|
+
favourite: [{ group_id: "G1", title: "试驾视频", content_type: "video", create_time: 1716300000 }],
|
|
124
|
+
follow: [{ series_id: "S9", series_name: "比亚迪汉", follow_time: 1716320000 }],
|
|
125
|
+
};
|
|
126
|
+
const calls = [];
|
|
127
|
+
const a = new DongchediAdapter({
|
|
128
|
+
account: { cookies: COOKIES, userId: "u1" },
|
|
129
|
+
fetchFn: async ({ url, cookies, query, sign }) => {
|
|
130
|
+
const k = byUrl(url);
|
|
131
|
+
calls.push({ k, cookies, offset: query.offset, sign });
|
|
132
|
+
return { data: { list: query.offset === 0 ? data[k] : [], has_more: false } };
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
const items = await collect(a.sync({}));
|
|
136
|
+
expect(items.map((x) => x.kind).sort()).toEqual(["favourite", "follow"]);
|
|
137
|
+
expect(calls.every((c) => c.cookies === COOKIES && c.sign === null)).toBe(true);
|
|
138
|
+
const fav = a.normalize(items.find((x) => x.kind === "favourite"));
|
|
139
|
+
expect(fav.events[0].content.title).toBe("收藏: 试驾视频");
|
|
140
|
+
const fol = a.normalize(items.find((x) => x.kind === "follow"));
|
|
141
|
+
expect(fol.persons[0].names).toEqual(["比亚迪汉"]);
|
|
142
|
+
expect(fol.persons[0].extra.followType).toBe("series");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("invokes signProvider + limit + empty + default fetch + no input", async () => {
|
|
146
|
+
const signCalls = [];
|
|
147
|
+
const a = new DongchediAdapter({
|
|
148
|
+
account: { cookies: COOKIES },
|
|
149
|
+
fetchFn: async ({ query }) => ({ data: { list: query.offset === 0 ? [{ group_id: "G1", title: "x" }, { group_id: "G2", title: "y" }] : [], has_more: false } }),
|
|
150
|
+
signProvider: async (ctx) => { signCalls.push(ctx); return "x-bogus"; },
|
|
151
|
+
});
|
|
152
|
+
expect(await collect(a.sync({ limit: 1, include: { follow: false } }))).toHaveLength(1);
|
|
153
|
+
expect(signCalls.length).toBeGreaterThan(0);
|
|
154
|
+
expect(signCalls[0].cookies).toBe(COOKIES);
|
|
155
|
+
|
|
156
|
+
const a2 = new DongchediAdapter({ account: { cookies: COOKIES }, fetchFn: async () => "<html>login</html>" });
|
|
157
|
+
expect(await collect(a2.sync({}))).toEqual([]);
|
|
158
|
+
|
|
159
|
+
const a3 = new DongchediAdapter({ account: { cookies: COOKIES } });
|
|
160
|
+
await expect(collect(a3.sync({}))).rejects.toThrow(/no fetchFn configured/);
|
|
161
|
+
|
|
162
|
+
const a4 = new DongchediAdapter();
|
|
163
|
+
await expect(collect(a4.sync({}))).rejects.toThrow(/needs opts.inputPath/);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from "vitest";
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const os = require("node:os");
|
|
7
|
+
const crypto = require("node:crypto");
|
|
8
|
+
|
|
9
|
+
const dc = require("../../lib/adapters/travel-didi-consumer");
|
|
10
|
+
|
|
11
|
+
function writeTmp(content) {
|
|
12
|
+
const p = path.join(os.tmpdir(), `cc-didic-${crypto.randomUUID()}.json`);
|
|
13
|
+
fs.writeFileSync(p, content, "utf-8");
|
|
14
|
+
return p;
|
|
15
|
+
}
|
|
16
|
+
async function collect(gen) {
|
|
17
|
+
const out = [];
|
|
18
|
+
for await (const x of gen) out.push(x);
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
const COOKIES = "didi_token=abc";
|
|
22
|
+
|
|
23
|
+
describe("travel-didi-consumer", () => {
|
|
24
|
+
it("name distinct from enterprise", () => {
|
|
25
|
+
expect(dc.NAME).toBe("travel-didi-consumer");
|
|
26
|
+
expect(new dc.DidiConsumerAdapter().name).toBe("travel-didi-consumer");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("snapshot ride → car travel event (via travel-base normalize)", async () => {
|
|
30
|
+
const SNAP = JSON.stringify([
|
|
31
|
+
{ orderId: "O1", fromAddress: "家", toAddress: "公司", departTime: 1716383000, arriveTime: 1716385000, fare: 2350, productName: "快车" },
|
|
32
|
+
]);
|
|
33
|
+
const p = writeTmp(SNAP);
|
|
34
|
+
try {
|
|
35
|
+
const a = new dc.DidiConsumerAdapter();
|
|
36
|
+
expect((await a.authenticate({ inputPath: p })).mode).toBe("snapshot-file");
|
|
37
|
+
const items = await collect(a.sync({ inputPath: p }));
|
|
38
|
+
expect(items).toHaveLength(1);
|
|
39
|
+
expect(items[0].originalId).toBe("O1");
|
|
40
|
+
const b = a.normalize(items[0]);
|
|
41
|
+
expect(b.events.length).toBeGreaterThan(0);
|
|
42
|
+
expect(b.source ? true : b.events[0].source.adapter).toBe("travel-didi-consumer");
|
|
43
|
+
} finally {
|
|
44
|
+
fs.unlinkSync(p);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("cookie-api: unverified + sign seam + paginate", async () => {
|
|
49
|
+
let signed = 0;
|
|
50
|
+
const a = new dc.DidiConsumerAdapter({
|
|
51
|
+
account: { cookies: COOKIES, phone: "1" },
|
|
52
|
+
signProvider: async () => { signed += 1; return "sig"; },
|
|
53
|
+
fetchFn: async ({ query }) => (query.pageIndex > 1 ? { data: { list: [] } } : { data: { list: [{ orderId: "O9", fromAddress: "A", toAddress: "B", departTime: Date.now() }] } }),
|
|
54
|
+
});
|
|
55
|
+
expect(await a.authenticate()).toMatchObject({ ok: true, mode: "cookie", unverified: true });
|
|
56
|
+
const items = await collect(a.sync({}));
|
|
57
|
+
expect(items).toHaveLength(1);
|
|
58
|
+
expect(items[0].originalId).toBe("O9");
|
|
59
|
+
expect(signed).toBeGreaterThan(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("medium sensitivity; default fetch throws", async () => {
|
|
63
|
+
expect(new dc.DidiConsumerAdapter().dataDisclosure.sensitivity).toBe("medium");
|
|
64
|
+
await expect(collect(new dc.DidiConsumerAdapter({ account: { cookies: COOKIES } }).sync({}))).rejects.toThrow(/no fetchFn/);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from "vitest";
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const os = require("node:os");
|
|
7
|
+
const crypto = require("node:crypto");
|
|
8
|
+
|
|
9
|
+
const { XiguaVideoAdapter, extractItems, mapItem, NAME, VERSION } = require("../../lib/adapters/video-xigua");
|
|
10
|
+
|
|
11
|
+
function writeTmp(content) {
|
|
12
|
+
const p = path.join(os.tmpdir(), `cc-xig-${crypto.randomUUID()}.json`);
|
|
13
|
+
fs.writeFileSync(p, content, "utf-8");
|
|
14
|
+
return p;
|
|
15
|
+
}
|
|
16
|
+
async function collect(gen) {
|
|
17
|
+
const out = [];
|
|
18
|
+
for await (const x of gen) out.push(x);
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const COOKIES = "sid_tt=abc; ttwid=xyz";
|
|
23
|
+
|
|
24
|
+
describe("video-xigua mappers", () => {
|
|
25
|
+
it("name/version", () => {
|
|
26
|
+
expect(NAME).toBe("video-xigua");
|
|
27
|
+
expect(VERSION).toBe("0.1.0");
|
|
28
|
+
});
|
|
29
|
+
it("mapItem reads nested article + bytedance fields", () => {
|
|
30
|
+
const rec = mapItem({ behot_time: 1716300000, article: { group_id: "G1", title: "城市骑行 vlog", video_duration: 620, user_name: "骑行小王" } });
|
|
31
|
+
expect(rec).toMatchObject({ videoId: "G1", title: "城市骑行 vlog", durationSec: 620, channel: "骑行小王" });
|
|
32
|
+
expect(rec.occurredAt).toBe(1716300000000);
|
|
33
|
+
expect(rec.url).toContain("ixigua.com");
|
|
34
|
+
expect(mapItem({ article: { title: "noid" } })).toBe(null);
|
|
35
|
+
});
|
|
36
|
+
it("mapItem reads flat item too", () => {
|
|
37
|
+
const rec = mapItem({ group_id: "G2", title: "测评", duration: 300, create_time: 1716310000 });
|
|
38
|
+
expect(rec).toMatchObject({ videoId: "G2", title: "测评", durationSec: 300 });
|
|
39
|
+
});
|
|
40
|
+
it("extractItems tolerant", () => {
|
|
41
|
+
expect(extractItems({ data: { history: [{ group_id: 1 }] } })).toHaveLength(1);
|
|
42
|
+
expect(extractItems({ data: { favorites: [{ group_id: 1 }] } })).toHaveLength(1);
|
|
43
|
+
expect(extractItems({})).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("XiguaVideoAdapter (via _video-base)", () => {
|
|
48
|
+
const SNAP = JSON.stringify({
|
|
49
|
+
schemaVersion: 1,
|
|
50
|
+
snapshottedAt: 1716383000000,
|
|
51
|
+
account: { userId: "u1" },
|
|
52
|
+
events: [
|
|
53
|
+
{ kind: "watch", id: "w1", videoId: "V1", title: "纪录片:长江", category: "documentary", durationSec: 3600, capturedAt: 1716300000000 },
|
|
54
|
+
{ kind: "favourite", id: "fa1", videoId: "V2", title: "搞笑合集" },
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("snapshot sync 2 kinds + normalize watch→media / favourite→like", async () => {
|
|
59
|
+
const p = writeTmp(SNAP);
|
|
60
|
+
try {
|
|
61
|
+
const a = new XiguaVideoAdapter();
|
|
62
|
+
const items = await collect(a.sync({ inputPath: p }));
|
|
63
|
+
expect(items.map((x) => x.kind)).toEqual(["watch", "favourite"]);
|
|
64
|
+
const w = a.normalize(items[0]);
|
|
65
|
+
expect(w.events[0].subtype).toBe("media");
|
|
66
|
+
expect(w.events[0].content.title).toBe("观看: 纪录片:长江");
|
|
67
|
+
expect(w.items[0].subtype).toBe("media");
|
|
68
|
+
expect(w.items[0].extra.platform).toBe("xigua");
|
|
69
|
+
const fav = a.normalize(items[1]);
|
|
70
|
+
expect(fav.events[0].subtype).toBe("like");
|
|
71
|
+
expect(fav.events[0].content.title).toBe("收藏: 搞笑合集");
|
|
72
|
+
} finally {
|
|
73
|
+
fs.unlinkSync(p);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("cookie-api fetch + normalize", async () => {
|
|
78
|
+
const byUrl = (u) => (u.includes("history") ? "watch" : "favourite");
|
|
79
|
+
const data = {
|
|
80
|
+
watch: [{ behot_time: 1716300000, article: { group_id: "C1", title: "汽车评测", video_duration: 500 } }],
|
|
81
|
+
favourite: [{ group_id: "C2", title: "美食教程" }],
|
|
82
|
+
};
|
|
83
|
+
const calls = [];
|
|
84
|
+
const a = new XiguaVideoAdapter({
|
|
85
|
+
account: { cookies: COOKIES, userId: "u1" },
|
|
86
|
+
fetchFn: async ({ url, cookies, query, sign }) => {
|
|
87
|
+
const k = byUrl(url);
|
|
88
|
+
calls.push({ k, cookies, page: query.page, sign });
|
|
89
|
+
return { data: { list: query.page === 1 ? data[k] : [] } };
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
expect(await a.authenticate()).toEqual({ ok: true, account: "u1", mode: "cookie" });
|
|
93
|
+
const items = await collect(a.sync({}));
|
|
94
|
+
expect(items.map((x) => x.kind).sort()).toEqual(["favourite", "watch"]);
|
|
95
|
+
expect(calls.every((c) => c.cookies === COOKIES && c.sign === null)).toBe(true);
|
|
96
|
+
const w = a.normalize(items.find((x) => x.kind === "watch"));
|
|
97
|
+
expect(w.events[0].content.title).toBe("观看: 汽车评测");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("default fetch throws; no input throws", async () => {
|
|
101
|
+
const a = new XiguaVideoAdapter({ account: { cookies: COOKIES } });
|
|
102
|
+
await expect(collect(a.sync({}))).rejects.toThrow(/no fetchFn configured/);
|
|
103
|
+
const b = new XiguaVideoAdapter();
|
|
104
|
+
await expect(collect(b.sync({}))).rejects.toThrow(/needs opts.inputPath/);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from "vitest";
|
|
4
|
+
|
|
5
|
+
const { WeWorkPcAdapter, NAME, VERSION } = require("../../lib/adapters/wework-pc");
|
|
6
|
+
const { partitionBatch } = require("../../lib/batch");
|
|
7
|
+
|
|
8
|
+
// fake driver answering sqlite_master + table_info + SELECT * by table
|
|
9
|
+
function makeFakeDb(spec) {
|
|
10
|
+
class FakeStmt {
|
|
11
|
+
constructor(sql) {
|
|
12
|
+
this.sql = sql;
|
|
13
|
+
}
|
|
14
|
+
all() {
|
|
15
|
+
const s = this.sql;
|
|
16
|
+
if (/type='table'/.test(s)) return (spec.tables || []).map((n) => ({ name: n }));
|
|
17
|
+
const ti = s.match(/table_info\("(\w+)"\)/);
|
|
18
|
+
if (ti) return spec.cols[ti[1]] || [];
|
|
19
|
+
const fr = s.match(/FROM "(\w+)"/);
|
|
20
|
+
if (fr) return spec.rows[fr[1]] || [];
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
get() {
|
|
24
|
+
return { n: 1 };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return class FakeDb {
|
|
28
|
+
// eslint-disable-next-line no-unused-vars
|
|
29
|
+
constructor(_p, _o) {}
|
|
30
|
+
prepare(sql) {
|
|
31
|
+
return new FakeStmt(sql);
|
|
32
|
+
}
|
|
33
|
+
pragma() {}
|
|
34
|
+
exec() {}
|
|
35
|
+
close() {}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// WeChat Work-ish message table: matches pattern + has time/sender/peer/content.
|
|
40
|
+
const SPEC = {
|
|
41
|
+
tables: ["chat_message", "session_meta", "sqlite_sequence"],
|
|
42
|
+
cols: {
|
|
43
|
+
chat_message: [
|
|
44
|
+
{ name: "localId" },
|
|
45
|
+
{ name: "createTime" },
|
|
46
|
+
{ name: "sender" },
|
|
47
|
+
{ name: "conversationId" },
|
|
48
|
+
{ name: "content" },
|
|
49
|
+
],
|
|
50
|
+
session_meta: [{ name: "vid" }, { name: "name" }],
|
|
51
|
+
},
|
|
52
|
+
rows: {
|
|
53
|
+
chat_message: [
|
|
54
|
+
{ localId: "m1", createTime: 1700000000, sender: "u1", conversationId: "c1", content: "项目周会 10 点" },
|
|
55
|
+
{ localId: "m2", createTime: 1700000010, sender: "u2", conversationId: "c1", content: "收到" },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function adapter(spec, { exists = true } = {}) {
|
|
61
|
+
const a = new WeWorkPcAdapter({ dbPath: "/fake.db" });
|
|
62
|
+
a._deps.fs = { existsSync: () => exists, accessSync: () => {}, constants: { R_OK: 4 } };
|
|
63
|
+
a._deps.dbDriverFactory = () => makeFakeDb(spec);
|
|
64
|
+
return a;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function collect(iter) {
|
|
68
|
+
const out = [];
|
|
69
|
+
for await (const r of iter) out.push(r);
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe("WeWorkPcAdapter (企业微信 honest best-effort)", () => {
|
|
74
|
+
it("exposes name/version", () => {
|
|
75
|
+
expect(NAME).toBe("wework-pc");
|
|
76
|
+
expect(VERSION).toBe("0.1.0");
|
|
77
|
+
expect(new WeWorkPcAdapter().name).toBe("wework-pc");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("no-arg construct + device-pull + legalGate + APP_NOT_INSTALLED readiness", async () => {
|
|
81
|
+
const a = new WeWorkPcAdapter();
|
|
82
|
+
a._deps.discoveryDeps = {
|
|
83
|
+
fs: { existsSync: () => false, readdirSync: () => [], statSync: () => ({ size: 0 }), constants: { R_OK: 4 } },
|
|
84
|
+
home: "/no-home",
|
|
85
|
+
env: {},
|
|
86
|
+
};
|
|
87
|
+
expect(a.extractMode).toBe("device-pull");
|
|
88
|
+
expect(a.dataDisclosure.legalGate).toBe(true);
|
|
89
|
+
const r = await a.authenticate({ readinessOnly: true });
|
|
90
|
+
expect(r.reason).toBe("APP_NOT_INSTALLED");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("reads messages → valid events, platform=wework, raw preserved", async () => {
|
|
94
|
+
const a = adapter(SPEC);
|
|
95
|
+
const raws = await collect(a.sync({ dbPath: "/fake.db" }));
|
|
96
|
+
expect(raws).toHaveLength(2);
|
|
97
|
+
const merged = { events: [], persons: [], places: [], items: [], topics: [] };
|
|
98
|
+
for (const r of raws) {
|
|
99
|
+
const n = a.normalize(r);
|
|
100
|
+
for (const k of Object.keys(merged)) merged[k].push(...n[k]);
|
|
101
|
+
}
|
|
102
|
+
const { valid, invalidReasons } = partitionBatch(merged);
|
|
103
|
+
expect(invalidReasons).toHaveLength(0);
|
|
104
|
+
expect(valid.events).toHaveLength(2);
|
|
105
|
+
expect(valid.events[0].extra.platform).toBe("wework");
|
|
106
|
+
expect(valid.events[0].extra.textResolved).toBe(true);
|
|
107
|
+
expect(valid.events[0].extra.rawRow).toBeTruthy();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("emits local-im-read progress diagnostic", async () => {
|
|
111
|
+
const a = adapter(SPEC);
|
|
112
|
+
const ev = [];
|
|
113
|
+
await collect(a.sync({ dbPath: "/fake.db", onProgress: (e) => ev.push(e) }));
|
|
114
|
+
const d = ev.find((e) => e.phase === "local-im-read");
|
|
115
|
+
expect(d.messageTables).toContain("chat_message");
|
|
116
|
+
expect(d.messageCount).toBe(2);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("missing db yields nothing; unknown kind throws", async () => {
|
|
120
|
+
const a = adapter(SPEC, { exists: false });
|
|
121
|
+
expect(await collect(a.sync({ dbPath: "/no.db" }))).toHaveLength(0);
|
|
122
|
+
expect(() => new WeWorkPcAdapter().normalize({ kind: "x", payload: { kind: "x" } })).toThrow(/unknown kind/);
|
|
123
|
+
});
|
|
124
|
+
});
|