@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.
Files changed (45) hide show
  1. package/__tests__/adapters/biz-tianyancha.test.js +159 -0
  2. package/__tests__/adapters/doc-baidu-netdisk.test.js +102 -0
  3. package/__tests__/adapters/doc-camscanner.test.js +147 -0
  4. package/__tests__/adapters/doc-platforms.test.js +177 -0
  5. package/__tests__/adapters/gov-ixiamen.test.js +150 -0
  6. package/__tests__/adapters/gov-tax.test.js +135 -0
  7. package/__tests__/adapters/health-meiyou.test.js +125 -0
  8. package/__tests__/adapters/music-kugou.test.js +187 -0
  9. package/__tests__/adapters/recruit-boss.test.js +180 -0
  10. package/__tests__/adapters/shopping-dianping.test.js +239 -0
  11. package/__tests__/adapters/social-csdn.test.js +175 -0
  12. package/__tests__/adapters/social-dongchedi.test.js +165 -0
  13. package/__tests__/adapters/social-zhihu.test.js +246 -0
  14. package/__tests__/adapters/travel-ctrip.test.js +175 -1
  15. package/__tests__/adapters/travel-didi.test.js +204 -0
  16. package/__tests__/adapters/travel-tongcheng.test.js +289 -0
  17. package/__tests__/adapters/video-platforms.test.js +152 -0
  18. package/__tests__/adapters/video-xigua.test.js +106 -0
  19. package/__tests__/adapters/wework-pc.test.js +124 -0
  20. package/lib/adapter-guide.js +25 -3
  21. package/lib/adapters/_document-base.js +370 -0
  22. package/lib/adapters/_video-base.js +331 -0
  23. package/lib/adapters/biz-tianyancha/index.js +348 -0
  24. package/lib/adapters/doc-baidu-netdisk/index.js +91 -0
  25. package/lib/adapters/doc-camscanner/index.js +102 -0
  26. package/lib/adapters/doc-tencent-docs/index.js +94 -0
  27. package/lib/adapters/doc-wps/index.js +77 -0
  28. package/lib/adapters/gov-ixiamen/index.js +380 -0
  29. package/lib/adapters/gov-tax/index.js +451 -0
  30. package/lib/adapters/health-meiyou/index.js +393 -0
  31. package/lib/adapters/music-kugou/index.js +418 -0
  32. package/lib/adapters/recruit-boss/index.js +442 -0
  33. package/lib/adapters/shopping-dianping/index.js +473 -0
  34. package/lib/adapters/social-csdn/index.js +444 -0
  35. package/lib/adapters/social-dongchedi/index.js +360 -0
  36. package/lib/adapters/social-zhihu/index.js +488 -0
  37. package/lib/adapters/travel-ctrip/index.js +255 -40
  38. package/lib/adapters/travel-didi/index.js +327 -0
  39. package/lib/adapters/travel-tongcheng/index.js +393 -0
  40. package/lib/adapters/video-iqiyi/index.js +75 -0
  41. package/lib/adapters/video-tencent/index.js +78 -0
  42. package/lib/adapters/video-xigua/index.js +68 -0
  43. package/lib/adapters/wework-pc/index.js +31 -0
  44. package/lib/index.js +40 -0
  45. package/package.json +1 -1
@@ -0,0 +1,159 @@
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
+ TianyanchaAdapter,
11
+ extractData,
12
+ NAME,
13
+ VERSION,
14
+ SNAPSHOT_SCHEMA_VERSION,
15
+ } = require("../../lib/adapters/biz-tianyancha");
16
+
17
+ function writeTmp(content) {
18
+ const p = path.join(os.tmpdir(), `cc-tyc-${crypto.randomUUID()}.json`);
19
+ fs.writeFileSync(p, content, "utf-8");
20
+ return p;
21
+ }
22
+ async function collect(gen) {
23
+ const out = [];
24
+ for await (const x of gen) out.push(x);
25
+ return out;
26
+ }
27
+
28
+ const COOKIES = "auth_token=abc; TYCID=xyz";
29
+
30
+ const SNAP = JSON.stringify({
31
+ schemaVersion: 1,
32
+ snapshottedAt: 1716383000000,
33
+ account: { userId: "u1" },
34
+ events: [
35
+ { kind: "monitor", id: "mon-G1", companyId: "G1", companyName: "字节跳动有限公司", legalPerson: "张利东", regStatus: "存续", capturedAt: 1716300000000 },
36
+ { kind: "search", id: "s-1", query: "小米科技", companyName: "小米科技有限责任公司", capturedAt: 1716320000000 },
37
+ ],
38
+ });
39
+
40
+ describe("constants + extractData", () => {
41
+ it("name/version/schema", () => {
42
+ expect(NAME).toBe("biz-tianyancha");
43
+ expect(VERSION).toBe("0.1.0");
44
+ expect(SNAPSHOT_SCHEMA_VERSION).toBe(1);
45
+ });
46
+ it("extractData tolerant", () => {
47
+ expect(extractData({ data: [{ id: 1 }] })).toHaveLength(1);
48
+ expect(extractData({ data: { resultList: [{ id: 1 }] } })).toHaveLength(1);
49
+ expect(extractData({ list: [{ id: 1 }] })).toHaveLength(1);
50
+ expect(extractData({})).toEqual([]);
51
+ });
52
+ });
53
+
54
+ describe("TianyanchaAdapter snapshot mode", () => {
55
+ it("authenticate validates inputPath", async () => {
56
+ const p = writeTmp(SNAP);
57
+ try {
58
+ const a = new TianyanchaAdapter();
59
+ expect((await a.authenticate({ inputPath: p })).mode).toBe("snapshot-file");
60
+ expect((await a.authenticate({ inputPath: path.join(os.tmpdir(), "no-tyc.json") })).reason).toBe("INPUT_PATH_UNREADABLE");
61
+ } finally {
62
+ fs.unlinkSync(p);
63
+ }
64
+ });
65
+
66
+ it("sync 2 kinds + normalize monitor→like / search→interaction", async () => {
67
+ const p = writeTmp(SNAP);
68
+ try {
69
+ const a = new TianyanchaAdapter();
70
+ const items = await collect(a.sync({ inputPath: p }));
71
+ expect(items.map((x) => x.kind)).toEqual(["monitor", "search"]);
72
+
73
+ const mon = a.normalize(items[0]);
74
+ expect(mon.events[0].subtype).toBe("like");
75
+ expect(mon.events[0].content.title).toBe("关注公司: 字节跳动有限公司");
76
+ expect(mon.events[0].extra.legalPerson).toBe("张利东");
77
+ expect(mon.events[0].extra.regStatus).toBe("存续");
78
+
79
+ const search = a.normalize(items[1]);
80
+ expect(search.events[0].subtype).toBe("interaction");
81
+ expect(search.events[0].content.title).toBe("搜索企业: 小米科技");
82
+ expect(search.events[0].extra.companyName).toBe("小米科技有限责任公司");
83
+ } finally {
84
+ fs.unlinkSync(p);
85
+ }
86
+ });
87
+
88
+ it("include + limit + schema mismatch + unknown kind", async () => {
89
+ const p = writeTmp(SNAP);
90
+ try {
91
+ const a = new TianyanchaAdapter();
92
+ expect((await collect(a.sync({ inputPath: p, include: { monitor: false } }))).map((x) => x.kind)).toEqual(["search"]);
93
+ expect(await collect(a.sync({ inputPath: p, limit: 1 }))).toHaveLength(1);
94
+ expect(() => a.normalize({ kind: "bogus", payload: {} })).toThrow(/unknown kind/);
95
+ } finally {
96
+ fs.unlinkSync(p);
97
+ }
98
+ const bad = writeTmp(JSON.stringify({ schemaVersion: 9, events: [] }));
99
+ try {
100
+ const a = new TianyanchaAdapter();
101
+ await expect(collect(a.sync({ inputPath: bad }))).rejects.toThrow(/schemaVersion mismatch/);
102
+ } finally {
103
+ fs.unlinkSync(bad);
104
+ }
105
+ });
106
+ });
107
+
108
+ describe("TianyanchaAdapter cookie-api mode", () => {
109
+ it("authenticate cookie (userId optional)", async () => {
110
+ const a = new TianyanchaAdapter({ account: { cookies: COOKIES } });
111
+ expect(await a.authenticate()).toEqual({ ok: true, account: null, mode: "cookie" });
112
+ });
113
+
114
+ it("sync fetches monitor + search, normalizes", async () => {
115
+ const byUrl = (u) => (u.includes("monitor") ? "monitor" : "search");
116
+ const data = {
117
+ monitor: [{ graphId: "G9", companyName: "腾讯科技", legalPersonName: "马化腾", createTime: 1716300000 }],
118
+ search: [{ id: "h1", keyword: "阿里巴巴", searchTime: 1716320000 }],
119
+ };
120
+ const calls = [];
121
+ const a = new TianyanchaAdapter({
122
+ account: { cookies: COOKIES, userId: "u1" },
123
+ fetchFn: async ({ url, cookies, query, sign }) => {
124
+ const k = byUrl(url);
125
+ calls.push({ k, cookies, pageNum: query.pageNum, sign });
126
+ return { data: { list: query.pageNum === 1 ? data[k] : [] } };
127
+ },
128
+ });
129
+ const items = await collect(a.sync({}));
130
+ expect(items.map((x) => x.kind).sort()).toEqual(["monitor", "search"]);
131
+ expect(calls.every((c) => c.cookies === COOKIES && c.sign === null)).toBe(true);
132
+ const mon = a.normalize(items.find((x) => x.kind === "monitor"));
133
+ expect(mon.events[0].content.title).toBe("关注公司: 腾讯科技");
134
+ expect(mon.events[0].extra.legalPerson).toBe("马化腾");
135
+ const search = a.normalize(items.find((x) => x.kind === "search"));
136
+ expect(search.events[0].content.title).toBe("搜索企业: 阿里巴巴");
137
+ });
138
+
139
+ it("invokes signProvider + limit + empty + default fetch + no input", async () => {
140
+ const signCalls = [];
141
+ const a = new TianyanchaAdapter({
142
+ account: { cookies: COOKIES },
143
+ fetchFn: async ({ query }) => ({ data: { list: query.pageNum === 1 ? [{ graphId: "G1", companyName: "a" }, { graphId: "G2", companyName: "b" }] : [] } }),
144
+ signProvider: async (ctx) => { signCalls.push(ctx); return "sig"; },
145
+ });
146
+ expect(await collect(a.sync({ limit: 1, include: { search: false } }))).toHaveLength(1);
147
+ expect(signCalls.length).toBeGreaterThan(0);
148
+ expect(signCalls[0].cookies).toBe(COOKIES);
149
+
150
+ const a2 = new TianyanchaAdapter({ account: { cookies: COOKIES }, fetchFn: async () => "<html>login</html>" });
151
+ expect(await collect(a2.sync({}))).toEqual([]);
152
+
153
+ const a3 = new TianyanchaAdapter({ account: { cookies: COOKIES } });
154
+ await expect(collect(a3.sync({}))).rejects.toThrow(/no fetchFn configured/);
155
+
156
+ const a4 = new TianyanchaAdapter();
157
+ await expect(collect(a4.sync({}))).rejects.toThrow(/needs opts.inputPath/);
158
+ });
159
+ });
@@ -0,0 +1,102 @@
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 nd = require("../../lib/adapters/doc-baidu-netdisk");
10
+
11
+ function writeTmp(content) {
12
+ const p = path.join(os.tmpdir(), `cc-nd-${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 = "BDUSS=abc; STOKEN=xyz";
23
+
24
+ describe("doc-baidu-netdisk mappers", () => {
25
+ it("name/version", () => {
26
+ expect(nd.NAME).toBe("doc-baidu-netdisk");
27
+ expect(nd.VERSION).toBe("0.1.0");
28
+ });
29
+ it("mapDoc maps netdisk fields; category + isdir → docType", () => {
30
+ const rec = nd.mapDoc({ fs_id: 123, server_filename: "电影.mp4", category: 1, size: 999, server_mtime: 1716383000, path: "/影视/电影.mp4" });
31
+ expect(rec).toMatchObject({ docId: "123", title: "电影.mp4", docType: "video" });
32
+ expect(rec.updatedMs).toBe(1716383000000);
33
+ expect(rec.extra.path).toBe("/影视/电影.mp4");
34
+ expect(nd.mapDoc({ fs_id: 1, isdir: 1, server_filename: "我的资料" }).docType).toBe("folder");
35
+ expect(nd.mapDoc({ fs_id: 2, server_filename: "报告.pdf" }).docType).toBe("doc"); // extension fallback
36
+ expect(nd.mapDoc({ server_filename: "noid" })).toBe(null);
37
+ });
38
+ it("extractDocs tolerant", () => {
39
+ expect(nd.extractDocs({ list: [{ fs_id: 1 }] })).toHaveLength(1);
40
+ expect(nd.extractDocs({ data: { list: [{ fs_id: 1 }] } })).toHaveLength(1);
41
+ expect(nd.extractDocs({})).toEqual([]);
42
+ });
43
+ });
44
+
45
+ describe("BaiduNetdiskAdapter (via _document-base)", () => {
46
+ const SNAP = JSON.stringify({
47
+ schemaVersion: 1,
48
+ snapshottedAt: 1716383000000,
49
+ account: { userId: "u1" },
50
+ events: [
51
+ { kind: "document", id: "doc-F1", docId: "F1", title: "合同.pdf", docType: "doc", updatedTime: 1716383000, url: "/工作/合同.pdf" },
52
+ ],
53
+ });
54
+
55
+ it("snapshot sync + normalize → event(post)+item(document)", async () => {
56
+ const p = writeTmp(SNAP);
57
+ try {
58
+ const a = new nd.BaiduNetdiskAdapter();
59
+ expect((await a.authenticate({ inputPath: p })).mode).toBe("snapshot-file");
60
+ const items = await collect(a.sync({ inputPath: p }));
61
+ expect(items).toHaveLength(1);
62
+ const batch = a.normalize(items[0]);
63
+ expect(batch.events[0].subtype).toBe("post");
64
+ expect(batch.items[0].subtype).toBe("document");
65
+ expect(batch.items[0].name).toBe("合同.pdf");
66
+ expect(batch.items[0].extra.platform).toBe("baidu-netdisk");
67
+ } finally {
68
+ fs.unlinkSync(p);
69
+ }
70
+ });
71
+
72
+ it("cookie-api: fetch + paginate + normalize", async () => {
73
+ const pages = [
74
+ { list: [{ fs_id: 7, server_filename: "照片.jpg", category: 3, server_mtime: 1716383000 }] },
75
+ { list: [] },
76
+ ];
77
+ const calls = [];
78
+ const a = new nd.BaiduNetdiskAdapter({
79
+ account: { cookies: COOKIES, userId: "u1" },
80
+ fetchFn: async ({ url, cookies, query, sign }) => {
81
+ calls.push({ url, cookies, offset: query.offset, sign });
82
+ return query.offset === 0 ? pages[0] : pages[1];
83
+ },
84
+ });
85
+ expect(await a.authenticate()).toEqual({ ok: true, account: "u1", mode: "cookie" });
86
+ const items = await collect(a.sync({}));
87
+ expect(items).toHaveLength(1);
88
+ expect(items[0].originalId).toBe("baidu-netdisk:document:7");
89
+ expect(calls[0].cookies).toBe(COOKIES);
90
+ expect(calls[0].sign).toBe(null);
91
+ const batch = a.normalize(items[0]);
92
+ expect(batch.items[0].name).toBe("照片.jpg");
93
+ expect(batch.items[0].extra.docType).toBe("image");
94
+ });
95
+
96
+ it("default fetch throws; no input throws", async () => {
97
+ const a = new nd.BaiduNetdiskAdapter({ account: { cookies: COOKIES } });
98
+ await expect(collect(a.sync({}))).rejects.toThrow(/no fetchFn configured/);
99
+ const b = new nd.BaiduNetdiskAdapter();
100
+ await expect(collect(b.sync({}))).rejects.toThrow(/needs opts.inputPath/);
101
+ });
102
+ });
@@ -0,0 +1,147 @@
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 cs = require("../../lib/adapters/doc-camscanner");
10
+
11
+ function writeTmp(content) {
12
+ const p = path.join(os.tmpdir(), `cc-cs-${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 = "INTSIG_TOKEN=abc; PHPSESSID=xyz";
23
+
24
+ describe("doc-camscanner mappers", () => {
25
+ it("name/version", () => {
26
+ expect(cs.NAME).toBe("doc-camscanner");
27
+ expect(cs.VERSION).toBe("0.1.0");
28
+ });
29
+ it("mapDoc maps CamScanner fields; doc_type + extension → docType", () => {
30
+ const rec = cs.mapDoc({
31
+ sync_doc_id: "DOC123",
32
+ title: "营业执照",
33
+ doc_type: 2,
34
+ page_num: 3,
35
+ create_time: 1716383000,
36
+ modify_time: 1716390000,
37
+ pdf_url: "https://cs/DOC123.pdf",
38
+ tags: ["证件"],
39
+ });
40
+ expect(rec).toMatchObject({ docId: "DOC123", title: "营业执照", docType: "certificate" });
41
+ expect(rec.createdMs).toBe(1716383000000);
42
+ expect(rec.updatedMs).toBe(1716390000000);
43
+ expect(rec.url).toBe("https://cs/DOC123.pdf");
44
+ expect(rec.extra.pageNum).toBe(3);
45
+ expect(rec.extra.tags).toEqual(["证件"]);
46
+ // extension fallback when no doc_type
47
+ expect(cs.mapDoc({ doc_id: 1, title: "报表.xlsx" }).docType).toBe("excel");
48
+ expect(cs.mapDoc({ doc_id: 2, title: "合同.pdf" }).docType).toBe("pdf");
49
+ // default → scan
50
+ expect(cs.mapDoc({ doc_id: 3, title: "随手拍" }).docType).toBe("scan");
51
+ // no id → null
52
+ expect(cs.mapDoc({ title: "noid" })).toBe(null);
53
+ });
54
+ it("extractDocs tolerant across shapes", () => {
55
+ expect(cs.extractDocs({ docs: [{ doc_id: 1 }] })).toHaveLength(1);
56
+ expect(cs.extractDocs({ list: [{ doc_id: 1 }] })).toHaveLength(1);
57
+ expect(cs.extractDocs({ data: { docs: [{ doc_id: 1 }] } })).toHaveLength(1);
58
+ expect(cs.extractDocs({})).toEqual([]);
59
+ });
60
+ });
61
+
62
+ describe("CamScannerDocAdapter (via _document-base)", () => {
63
+ const SNAP = JSON.stringify({
64
+ schemaVersion: 1,
65
+ snapshottedAt: 1716383000000,
66
+ account: { userId: "u1" },
67
+ events: [
68
+ {
69
+ kind: "document",
70
+ id: "doc-S1",
71
+ docId: "S1",
72
+ title: "身份证扫描",
73
+ docType: "certificate",
74
+ updatedTime: 1716383000,
75
+ url: "https://cs/S1.pdf",
76
+ },
77
+ ],
78
+ });
79
+
80
+ it("snapshot sync + normalize → event(post)+item(document)", async () => {
81
+ const p = writeTmp(SNAP);
82
+ try {
83
+ const a = new cs.CamScannerDocAdapter();
84
+ expect((await a.authenticate({ inputPath: p })).mode).toBe("snapshot-file");
85
+ const items = await collect(a.sync({ inputPath: p }));
86
+ expect(items).toHaveLength(1);
87
+ const batch = a.normalize(items[0]);
88
+ expect(batch.events[0].subtype).toBe("post");
89
+ expect(batch.items[0].subtype).toBe("document");
90
+ expect(batch.items[0].name).toBe("身份证扫描");
91
+ expect(batch.items[0].extra.platform).toBe("camscanner");
92
+ } finally {
93
+ fs.unlinkSync(p);
94
+ }
95
+ });
96
+
97
+ it("cookie-api: fetch + paginate + normalize", async () => {
98
+ const pages = [
99
+ { docs: [{ sync_doc_id: 7, title: "发票.pdf", doc_type: "pdf", modify_time: 1716383000 }] },
100
+ { docs: [] },
101
+ ];
102
+ const calls = [];
103
+ const a = new cs.CamScannerDocAdapter({
104
+ account: { cookies: COOKIES, userId: "u1" },
105
+ fetchFn: async ({ url, cookies, query, sign }) => {
106
+ calls.push({ url, cookies, offset: query.offset, sign });
107
+ return query.offset === 0 ? pages[0] : pages[1];
108
+ },
109
+ });
110
+ expect(await a.authenticate()).toEqual({ ok: true, account: "u1", mode: "cookie" });
111
+ const items = await collect(a.sync({}));
112
+ expect(items).toHaveLength(1);
113
+ expect(items[0].originalId).toBe("camscanner:document:7");
114
+ expect(calls[0].cookies).toBe(COOKIES);
115
+ expect(calls[0].sign).toBe(null);
116
+ const batch = a.normalize(items[0]);
117
+ expect(batch.items[0].name).toBe("发票.pdf");
118
+ expect(batch.items[0].extra.docType).toBe("pdf");
119
+ });
120
+
121
+ it("cookie-api: signProvider seam invoked when present", async () => {
122
+ let seen = null;
123
+ const a = new cs.CamScannerDocAdapter({
124
+ account: { cookies: COOKIES, userId: "u1" },
125
+ signProvider: async ({ url, query }) => {
126
+ seen = { url, offset: query.offset };
127
+ return "sig-abc";
128
+ },
129
+ fetchFn: async ({ sign, query }) => {
130
+ return query.offset === 0
131
+ ? { docs: [{ sync_doc_id: "X", title: "t", _sign: sign }] }
132
+ : { docs: [] };
133
+ },
134
+ });
135
+ const items = await collect(a.sync({}));
136
+ expect(items).toHaveLength(1);
137
+ expect(seen.offset).toBe(0);
138
+ expect(seen.url).toContain("intsig");
139
+ });
140
+
141
+ it("default fetch throws; no input throws", async () => {
142
+ const a = new cs.CamScannerDocAdapter({ account: { cookies: COOKIES } });
143
+ await expect(collect(a.sync({}))).rejects.toThrow(/no fetchFn configured/);
144
+ const b = new cs.CamScannerDocAdapter();
145
+ await expect(collect(b.sync({}))).rejects.toThrow(/needs opts.inputPath/);
146
+ });
147
+ });
@@ -0,0 +1,177 @@
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 wps = require("../../lib/adapters/doc-wps");
10
+ const tdocs = require("../../lib/adapters/doc-tencent-docs");
11
+
12
+ function writeTmp(content) {
13
+ const p = path.join(os.tmpdir(), `cc-doc-${crypto.randomUUID()}.json`);
14
+ fs.writeFileSync(p, content, "utf-8");
15
+ return p;
16
+ }
17
+
18
+ async function collect(gen) {
19
+ const out = [];
20
+ for await (const x of gen) out.push(x);
21
+ return out;
22
+ }
23
+
24
+ const COOKIES = "wps_sid=abc; uid=1";
25
+
26
+ describe("doc-wps constants + mappers", () => {
27
+ it("exposes name/version", () => {
28
+ expect(wps.NAME).toBe("doc-wps");
29
+ expect(wps.VERSION).toBe("0.1.0");
30
+ });
31
+ it("mapDoc maps WPS fields + infers docType from extension", () => {
32
+ const rec = wps.mapDoc({ id: "F1", fname: "预算.xlsx", ctime: 1716300000, mtime: 1716383000 });
33
+ expect(rec).toMatchObject({ docId: "F1", title: "预算.xlsx", docType: "sheet" });
34
+ expect(rec.createdMs).toBe(1716300000000);
35
+ expect(rec.updatedMs).toBe(1716383000000);
36
+ expect(rec.url).toContain("kdocs.cn");
37
+ expect(wps.mapDoc({ id: "F2", fname: "方案.pptx" }).docType).toBe("slide");
38
+ expect(wps.mapDoc({ id: "F3", fname: "说明.docx" }).docType).toBe("doc");
39
+ expect(wps.mapDoc({ fname: "noid" })).toBe(null);
40
+ });
41
+ it("extractDocs tolerant of shapes", () => {
42
+ expect(wps.extractDocs({ files: [{ id: 1 }] })).toHaveLength(1);
43
+ expect(wps.extractDocs({ data: { files: [{ id: 1 }] } })).toHaveLength(1);
44
+ expect(wps.extractDocs({})).toEqual([]);
45
+ });
46
+ });
47
+
48
+ describe("doc-tencent-docs constants + mappers", () => {
49
+ it("exposes name/version", () => {
50
+ expect(tdocs.NAME).toBe("doc-tencent-docs");
51
+ expect(tdocs.VERSION).toBe("0.1.0");
52
+ });
53
+ it("mapDoc maps Tencent fields + type codes", () => {
54
+ const rec = tdocs.mapDoc({ id: "T1", title: "周报", type: "sheet", createTime: 1716300000, lastModifyTime: 1716383000 });
55
+ expect(rec).toMatchObject({ docId: "T1", title: "周报", docType: "sheet" });
56
+ expect(rec.url).toContain("docs.qq.com");
57
+ expect(tdocs.mapDoc({ id: "T2", type: 2 }).docType).toBe("sheet");
58
+ expect(tdocs.mapDoc({ id: "T3", type: "presentation" }).docType).toBe("slide");
59
+ expect(tdocs.mapDoc({ title: "noid" })).toBe(null);
60
+ });
61
+ });
62
+
63
+ describe("WpsDocAdapter snapshot mode", () => {
64
+ const SNAP = JSON.stringify({
65
+ schemaVersion: 1,
66
+ snapshottedAt: 1716383000000,
67
+ account: { userId: "u1" },
68
+ events: [
69
+ { kind: "document", id: "doc-D1", docId: "D1", title: "我的文档", docType: "doc", createdTime: 1716300000, updatedTime: 1716383000, url: "https://kdocs.cn/p/D1" },
70
+ ],
71
+ });
72
+
73
+ it("authenticate validates inputPath", async () => {
74
+ const p = writeTmp(SNAP);
75
+ try {
76
+ const a = new wps.WpsDocAdapter();
77
+ expect((await a.authenticate({ inputPath: p })).mode).toBe("snapshot-file");
78
+ expect((await a.authenticate({ inputPath: path.join(os.tmpdir(), "nope.json") })).reason).toBe("INPUT_PATH_UNREADABLE");
79
+ } finally {
80
+ fs.unlinkSync(p);
81
+ }
82
+ });
83
+
84
+ it("sync yields doc + normalize → event(post)+item(document)", async () => {
85
+ const p = writeTmp(SNAP);
86
+ try {
87
+ const a = new wps.WpsDocAdapter();
88
+ const items = await collect(a.sync({ inputPath: p }));
89
+ expect(items).toHaveLength(1);
90
+ expect(items[0].originalId).toBe("wps:document:doc-D1");
91
+ const batch = a.normalize(items[0]);
92
+ expect(batch.events[0].subtype).toBe("post");
93
+ expect(batch.events[0].content.title).toBe("文档: 我的文档");
94
+ expect(batch.items[0].subtype).toBe("document");
95
+ expect(batch.items[0].name).toBe("我的文档");
96
+ expect(batch.items[0].extra.platform).toBe("wps");
97
+ // event references the item
98
+ expect(batch.events[0].extra.itemRef).toBe(batch.items[0].id);
99
+ } finally {
100
+ fs.unlinkSync(p);
101
+ }
102
+ });
103
+
104
+ it("schemaVersion mismatch throws; normalize missing record throws", async () => {
105
+ const p = writeTmp(JSON.stringify({ schemaVersion: 9, events: [] }));
106
+ try {
107
+ const a = new wps.WpsDocAdapter();
108
+ await expect(collect(a.sync({ inputPath: p }))).rejects.toThrow(/schemaVersion mismatch/);
109
+ expect(() => a.normalize({ payload: {} })).toThrow(/payload.record missing/);
110
+ } finally {
111
+ fs.unlinkSync(p);
112
+ }
113
+ });
114
+ });
115
+
116
+ describe("TencentDocsAdapter cookie-api mode", () => {
117
+ it("authenticate cookie mode (userId optional)", async () => {
118
+ const a = new tdocs.TencentDocsAdapter({ account: { cookies: COOKIES } });
119
+ expect(await a.authenticate()).toEqual({ ok: true, account: null, mode: "cookie" });
120
+ });
121
+
122
+ it("sync fetches, paginates, normalizes", async () => {
123
+ const pages = [
124
+ { data: { files: [{ id: "T1", title: "项目计划", type: "doc", lastModifyTime: 1716383000 }] } },
125
+ { data: { files: [] } },
126
+ ];
127
+ const calls = [];
128
+ const fetchFn = async ({ url, cookies, query, sign }) => {
129
+ calls.push({ url, cookies, offset: query.offset, sign });
130
+ return query.offset === 0 ? pages[0] : pages[1];
131
+ };
132
+ const a = new tdocs.TencentDocsAdapter({ account: { cookies: COOKIES, userId: "u1" }, fetchFn });
133
+ const items = await collect(a.sync({}));
134
+ expect(items).toHaveLength(1);
135
+ expect(items[0].originalId).toBe("tencent-docs:document:T1");
136
+ expect(calls[0].cookies).toBe(COOKIES);
137
+ expect(calls[0].sign).toBe(null);
138
+ const batch = a.normalize(items[0]);
139
+ expect(batch.items[0].name).toBe("项目计划");
140
+ expect(batch.items[0].extra.platform).toBe("tencent-docs");
141
+ });
142
+
143
+ it("invokes signProvider when configured", async () => {
144
+ const signCalls = [];
145
+ const a = new tdocs.TencentDocsAdapter({
146
+ account: { cookies: COOKIES },
147
+ fetchFn: async ({ query }) => (query.offset === 0 ? { files: [{ id: "S1", title: "x" }] } : { files: [] }),
148
+ signProvider: async (ctx) => { signCalls.push(ctx); return "SIG"; },
149
+ });
150
+ const items = await collect(a.sync({}));
151
+ expect(items).toHaveLength(1);
152
+ expect(signCalls.length).toBeGreaterThan(0);
153
+ expect(signCalls[0].cookies).toBe(COOKIES);
154
+ });
155
+
156
+ it("sinceWatermark + limit + empty response", async () => {
157
+ const a1 = new tdocs.TencentDocsAdapter({
158
+ account: { cookies: COOKIES },
159
+ fetchFn: async ({ query }) => query.offset === 0 ? { files: [
160
+ { id: "NEW", title: "n", lastModifyTime: 2_000_000_000 },
161
+ { id: "OLD", title: "o", lastModifyTime: 1_000_000_000 },
162
+ ] } : { files: [] },
163
+ });
164
+ const got = await collect(a1.sync({ sinceWatermark: 1_500_000_000_000 }));
165
+ expect(got.map((x) => x.originalId)).toEqual(["tencent-docs:document:NEW"]);
166
+
167
+ const a2 = new tdocs.TencentDocsAdapter({ account: { cookies: COOKIES }, fetchFn: async () => "<html>login</html>" });
168
+ expect(await collect(a2.sync({}))).toEqual([]);
169
+ });
170
+
171
+ it("default fetch throws; no input throws", async () => {
172
+ const a = new tdocs.TencentDocsAdapter({ account: { cookies: COOKIES } });
173
+ await expect(collect(a.sync({}))).rejects.toThrow(/no fetchFn configured/);
174
+ const b = new tdocs.TencentDocsAdapter();
175
+ await expect(collect(b.sync({}))).rejects.toThrow(/needs opts.inputPath/);
176
+ });
177
+ });