@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.
Files changed (53) hide show
  1. package/__tests__/adapters/bank-family.test.js +125 -0
  2. package/__tests__/adapters/biz-tianyancha.test.js +159 -0
  3. package/__tests__/adapters/car-mercedesme.test.js +74 -0
  4. package/__tests__/adapters/doc-camscanner.test.js +147 -0
  5. package/__tests__/adapters/finance-dcep.test.js +74 -0
  6. package/__tests__/adapters/fitness-joyrun.test.js +82 -0
  7. package/__tests__/adapters/gov-12123.test.js +103 -0
  8. package/__tests__/adapters/gov-ixiamen.test.js +150 -0
  9. package/__tests__/adapters/gov-tax.test.js +135 -0
  10. package/__tests__/adapters/health-meiyou.test.js +125 -0
  11. package/__tests__/adapters/music-qq.test.js +112 -0
  12. package/__tests__/adapters/reading-family.test.js +108 -0
  13. package/__tests__/adapters/social-dongchedi.test.js +165 -0
  14. package/__tests__/adapters/travel-didi-consumer.test.js +66 -0
  15. package/__tests__/adapters/video-xigua.test.js +106 -0
  16. package/__tests__/adapters/wework-pc.test.js +124 -0
  17. package/__tests__/audio-ximalaya-snapshot.test.js +279 -0
  18. package/__tests__/fitness-keep-snapshot.test.js +224 -0
  19. package/__tests__/shopping-eleme-snapshot.test.js +454 -0
  20. package/__tests__/shopping-vipshop-snapshot.test.js +425 -0
  21. package/__tests__/shopping-xianyu-snapshot.test.js +451 -0
  22. package/__tests__/social-douban-snapshot.test.js +351 -0
  23. package/lib/adapter-guide.js +31 -3
  24. package/lib/adapters/_bank-base.js +405 -0
  25. package/lib/adapters/_reading-base.js +315 -0
  26. package/lib/adapters/audio-ximalaya/index.js +414 -0
  27. package/lib/adapters/bank-bankcomm/index.js +27 -0
  28. package/lib/adapters/bank-boc/index.js +26 -0
  29. package/lib/adapters/bank-cmbc/index.js +26 -0
  30. package/lib/adapters/bank-icbc/index.js +27 -0
  31. package/lib/adapters/biz-tianyancha/index.js +348 -0
  32. package/lib/adapters/car-mercedesme/index.js +225 -0
  33. package/lib/adapters/doc-camscanner/index.js +102 -0
  34. package/lib/adapters/finance-dcep/index.js +302 -0
  35. package/lib/adapters/fitness-joyrun/index.js +295 -0
  36. package/lib/adapters/fitness-keep/index.js +343 -0
  37. package/lib/adapters/gov-12123/index.js +391 -0
  38. package/lib/adapters/gov-ixiamen/index.js +380 -0
  39. package/lib/adapters/gov-tax/index.js +451 -0
  40. package/lib/adapters/health-meiyou/index.js +393 -0
  41. package/lib/adapters/music-qq/index.js +372 -0
  42. package/lib/adapters/reading-fanqie/index.js +61 -0
  43. package/lib/adapters/reading-qimao/index.js +61 -0
  44. package/lib/adapters/shopping-eleme/index.js +441 -0
  45. package/lib/adapters/shopping-vipshop/index.js +429 -0
  46. package/lib/adapters/shopping-xianyu/index.js +454 -0
  47. package/lib/adapters/social-dongchedi/index.js +360 -0
  48. package/lib/adapters/social-douban/index.js +564 -0
  49. package/lib/adapters/travel-didi-consumer/index.js +148 -0
  50. package/lib/adapters/video-xigua/index.js +68 -0
  51. package/lib/adapters/wework-pc/index.js +31 -0
  52. package/lib/index.js +52 -0
  53. 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
+ });
@@ -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 = platform === "钉钉" ? "dingtalk-pc" : "feishu-pc";
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(