@chainlesschain/personal-data-hub 0.4.29 → 0.4.31
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/lib/forensics/qq-nt-collect.js +190 -0
- package/lib/prompt-builder.js +15 -1
- package/package.json +8 -3
- package/__tests__/adapter-guide.test.js +0 -47
- package/__tests__/adapter-spec.test.js +0 -78
- package/__tests__/adapters/ai-chat-cookie-capture-spec.test.js +0 -211
- package/__tests__/adapters/ai-chat-health-checker.test.js +0 -262
- package/__tests__/adapters/ai-chat-history.test.js +0 -396
- package/__tests__/adapters/ai-chat-http-client.test.js +0 -242
- package/__tests__/adapters/ai-chat-vendors.test.js +0 -874
- package/__tests__/adapters/alipay-bill-adapter.test.js +0 -538
- package/__tests__/adapters/apple-health.test.js +0 -95
- package/__tests__/adapters/bank-family.test.js +0 -125
- package/__tests__/adapters/biz-tianyancha.test.js +0 -159
- package/__tests__/adapters/browser-history-chrome.test.js +0 -377
- package/__tests__/adapters/browser-history-edge.test.js +0 -159
- package/__tests__/adapters/car-mercedesme.test.js +0 -74
- package/__tests__/adapters/doc-baidu-netdisk.test.js +0 -102
- package/__tests__/adapters/doc-camscanner.test.js +0 -147
- package/__tests__/adapters/doc-platforms.test.js +0 -177
- package/__tests__/adapters/edu-huawei-learning-live.test.js +0 -198
- package/__tests__/adapters/edu-zuoyebang-live.test.js +0 -226
- package/__tests__/adapters/email-adapter-snapshot.test.js +0 -237
- package/__tests__/adapters/email-adapter.test.js +0 -742
- package/__tests__/adapters/email-classifier.test.js +0 -347
- package/__tests__/adapters/email-imap-session.test.js +0 -334
- package/__tests__/adapters/email-parser.test.js +0 -244
- package/__tests__/adapters/email-pdf-extractor.test.js +0 -529
- package/__tests__/adapters/email-providers.test.js +0 -84
- package/__tests__/adapters/email-retry-progress.test.js +0 -294
- package/__tests__/adapters/email-templates.test.js +0 -822
- package/__tests__/adapters/family-23-collectors-scaffold.test.js +0 -182
- package/__tests__/adapters/finance-alipay-live.test.js +0 -258
- package/__tests__/adapters/finance-dcep.test.js +0 -74
- package/__tests__/adapters/fitness-joyrun.test.js +0 -82
- package/__tests__/adapters/game-genshin-live.test.js +0 -238
- package/__tests__/adapters/game-genshin-scaffold.test.js +0 -108
- package/__tests__/adapters/game-honor-of-kings-live.test.js +0 -230
- package/__tests__/adapters/git-activity.test.js +0 -222
- package/__tests__/adapters/gov-12123.test.js +0 -103
- package/__tests__/adapters/gov-ixiamen.test.js +0 -150
- package/__tests__/adapters/gov-tax.test.js +0 -135
- package/__tests__/adapters/health-meiyou.test.js +0 -125
- package/__tests__/adapters/local-files.test.js +0 -264
- package/__tests__/adapters/local-im-pc.test.js +0 -154
- package/__tests__/adapters/messaging-whatsapp.test.js +0 -289
- package/__tests__/adapters/music-kugou.test.js +0 -187
- package/__tests__/adapters/music-qq.test.js +0 -112
- package/__tests__/adapters/netease-music-live.test.js +0 -244
- package/__tests__/adapters/netease-music.test.js +0 -74
- package/__tests__/adapters/pc-local-discovery.test.js +0 -141
- package/__tests__/adapters/qq-pc-direct-read.test.js +0 -227
- package/__tests__/adapters/reading-family.test.js +0 -108
- package/__tests__/adapters/recruit-boss.test.js +0 -180
- package/__tests__/adapters/shell-history.test.js +0 -180
- package/__tests__/adapters/shopping-base.test.js +0 -179
- package/__tests__/adapters/shopping-dianping.test.js +0 -239
- package/__tests__/adapters/social-bilibili-adb-api-client.test.js +0 -721
- package/__tests__/adapters/social-bilibili-adb-chromium-cookies-reader.test.js +0 -346
- package/__tests__/adapters/social-bilibili-adb-collector.test.js +0 -284
- package/__tests__/adapters/social-bilibili-adb-cookies-extension.test.js +0 -343
- package/__tests__/adapters/social-bilibili-adb-snapshot-builder.test.js +0 -296
- package/__tests__/adapters/social-csdn.test.js +0 -175
- package/__tests__/adapters/social-dongchedi.test.js +0 -165
- package/__tests__/adapters/social-douyin-adb-aweme-detail.test.js +0 -165
- package/__tests__/adapters/social-douyin-adb-collector.test.js +0 -254
- package/__tests__/adapters/social-douyin-adb-db-extension.test.js +0 -114
- package/__tests__/adapters/social-douyin-adb-im-db-parser.test.js +0 -304
- package/__tests__/adapters/social-douyin-adb-snapshot-builder.test.js +0 -216
- package/__tests__/adapters/social-douyin-adb-usage-profile.test.js +0 -229
- package/__tests__/adapters/social-douyin-adb-watch-history.test.js +0 -269
- package/__tests__/adapters/social-kuaishou-adb-api-client.test.js +0 -496
- package/__tests__/adapters/social-kuaishou-adb-collector.test.js +0 -276
- package/__tests__/adapters/social-kuaishou-adb-cookies-extension.test.js +0 -152
- package/__tests__/adapters/social-kuaishou-adb-snapshot-builder.test.js +0 -178
- package/__tests__/adapters/social-toutiao-adb-account-reader.test.js +0 -135
- package/__tests__/adapters/social-toutiao-adb-api-client.test.js +0 -626
- package/__tests__/adapters/social-toutiao-adb-article.test.js +0 -155
- package/__tests__/adapters/social-toutiao-adb-collector.test.js +0 -378
- package/__tests__/adapters/social-toutiao-adb-cookies-extension.test.js +0 -193
- package/__tests__/adapters/social-toutiao-adb-snapshot-builder.test.js +0 -196
- package/__tests__/adapters/social-toutiao-kuaishou-scaffold.test.js +0 -311
- package/__tests__/adapters/social-weibo-adb-api-client.test.js +0 -362
- package/__tests__/adapters/social-weibo-adb-collector.test.js +0 -201
- package/__tests__/adapters/social-weibo-adb-cookies-extension.test.js +0 -167
- package/__tests__/adapters/social-weibo-adb-snapshot-builder.test.js +0 -189
- package/__tests__/adapters/social-xiaohongshu-adb-api-client.test.js +0 -431
- package/__tests__/adapters/social-xiaohongshu-adb-collector.test.js +0 -207
- package/__tests__/adapters/social-xiaohongshu-adb-cookies-extension.test.js +0 -0
- package/__tests__/adapters/social-xiaohongshu-adb-sign-provider-injection.test.js +0 -351
- package/__tests__/adapters/social-xiaohongshu-adb-sign.test.js +0 -130
- package/__tests__/adapters/social-xiaohongshu-adb-snapshot-builder.test.js +0 -200
- package/__tests__/adapters/social-zhihu.test.js +0 -246
- package/__tests__/adapters/system-data-adapter.test.js +0 -443
- package/__tests__/adapters/system-data-android-ingest.test.js +0 -144
- package/__tests__/adapters/system-data-android.test.js +0 -519
- package/__tests__/adapters/system-data-disclosure.test.js +0 -153
- package/__tests__/adapters/travel-12306.test.js +0 -512
- package/__tests__/adapters/travel-amap.test.js +0 -219
- package/__tests__/adapters/travel-baidu-map.test.js +0 -305
- package/__tests__/adapters/travel-base.test.js +0 -205
- package/__tests__/adapters/travel-ctrip.test.js +0 -377
- package/__tests__/adapters/travel-didi-consumer.test.js +0 -66
- package/__tests__/adapters/travel-didi.test.js +0 -204
- package/__tests__/adapters/travel-tencent-map.test.js +0 -207
- package/__tests__/adapters/travel-tongcheng.test.js +0 -289
- package/__tests__/adapters/video-platforms.test.js +0 -152
- package/__tests__/adapters/video-xigua.test.js +0 -106
- package/__tests__/adapters/vscode.test.js +0 -299
- package/__tests__/adapters/wechat-bootstrap.test.js +0 -240
- package/__tests__/adapters/wechat-env-probe.test.js +0 -162
- package/__tests__/adapters/wechat-frida-agent.test.js +0 -322
- package/__tests__/adapters/wechat-frida-integration.test.js +0 -149
- package/__tests__/adapters/wechat-frida-key-provider.test.js +0 -188
- package/__tests__/adapters/wechat-md5-key-provider.test.js +0 -101
- package/__tests__/adapters/wechat-pc-direct-read.test.js +0 -365
- package/__tests__/adapters/wechat-pc-group-topic.test.js +0 -63
- package/__tests__/adapters/wechat-pc-v4-sidecar.test.js +0 -72
- package/__tests__/adapters/weread.test.js +0 -123
- package/__tests__/adapters/wework-pc.test.js +0 -124
- package/__tests__/adapters/win-recent.test.js +0 -192
- package/__tests__/analysis-skills.test.js +0 -754
- package/__tests__/analysis.test.js +0 -1845
- package/__tests__/audio-ximalaya-snapshot.test.js +0 -279
- package/__tests__/batch.test.js +0 -133
- package/__tests__/bridges-cc-kg.test.js +0 -231
- package/__tests__/bridges-cc-llm.test.js +0 -191
- package/__tests__/bridges-cc-rag.test.js +0 -162
- package/__tests__/categories.test.js +0 -92
- package/__tests__/e2e/ai-chat-cross-source-journey.test.js +0 -213
- package/__tests__/e2e/full-user-journey.test.js +0 -188
- package/__tests__/e2e/local-data-adapters-cli.e2e.test.js +0 -146
- package/__tests__/entity-resolver-ingest-hook.test.js +0 -177
- package/__tests__/entity-resolver-stages.test.js +0 -411
- package/__tests__/entity-resolver-vault.test.js +0 -249
- package/__tests__/entity-resolver.test.js +0 -526
- package/__tests__/fitness-keep-snapshot.test.js +0 -224
- package/__tests__/fixtures/entity-resolver-200-mock.json +0 -96
- package/__tests__/ids.test.js +0 -45
- package/__tests__/integration/ai-chat-history-registry.test.js +0 -228
- package/__tests__/integration/aichat-wizard-end-to-end.test.js +0 -282
- package/__tests__/integration/cross-adapter-pipelines.test.js +0 -396
- package/__tests__/integration/local-data-adapters-pipeline.test.js +0 -373
- package/__tests__/integration/social-bilibili-pipeline.test.js +0 -261
- package/__tests__/integration/wechat-bootstrap-end-to-end.test.js +0 -390
- package/__tests__/key-providers.test.js +0 -126
- package/__tests__/kg-derive.test.js +0 -219
- package/__tests__/llm-client.test.js +0 -122
- package/__tests__/longtail-adapters.test.js +0 -281
- package/__tests__/messaging-qq-snapshot.test.js +0 -294
- package/__tests__/mobile-extractor-encrypted.test.js +0 -460
- package/__tests__/mobile-extractor.test.js +0 -288
- package/__tests__/mock-adapter.test.js +0 -93
- package/__tests__/prompt-builder.test.js +0 -249
- package/__tests__/query-parser.test.js +0 -365
- package/__tests__/rag-derive.test.js +0 -169
- package/__tests__/registry-readiness.test.js +0 -292
- package/__tests__/registry.test.js +0 -420
- package/__tests__/salvage-ingest.test.js +0 -97
- package/__tests__/schemas.test.js +0 -331
- package/__tests__/shopping-adapters.test.js +0 -392
- package/__tests__/shopping-eleme-snapshot.test.js +0 -454
- package/__tests__/shopping-pinduoduo-snapshot.test.js +0 -484
- package/__tests__/shopping-snapshot.test.js +0 -438
- package/__tests__/shopping-vipshop-snapshot.test.js +0 -425
- package/__tests__/shopping-xianyu-snapshot.test.js +0 -451
- package/__tests__/sidecar-contacts-cross-validate.test.js +0 -186
- package/__tests__/sidecar-supervisor.test.js +0 -128
- package/__tests__/sign-providers.test.js +0 -62
- package/__tests__/social-adapters.test.js +0 -280
- package/__tests__/social-bilibili-snapshot.test.js +0 -278
- package/__tests__/social-douban-snapshot.test.js +0 -351
- package/__tests__/social-douyin-im-direct-read.test.js +0 -377
- package/__tests__/social-douyin-salvage-collector.test.js +0 -98
- package/__tests__/social-douyin-salvage-mapper.test.js +0 -90
- package/__tests__/social-douyin-snapshot.test.js +0 -256
- package/__tests__/social-kuaishou-snapshot.test.js +0 -362
- package/__tests__/social-toutiao-snapshot.test.js +0 -366
- package/__tests__/social-weibo-snapshot.test.js +0 -234
- package/__tests__/social-weibo-sqlite-device.test.js +0 -174
- package/__tests__/social-xiaohongshu-snapshot.test.js +0 -232
- package/__tests__/sqlite-leaf-salvage.test.js +0 -97
- package/__tests__/travel-adapters.test.js +0 -483
- package/__tests__/travel-maps-snapshot.test.js +0 -426
- package/__tests__/vault-driver-error.test.js +0 -74
- package/__tests__/vault-search-helpers.test.js +0 -104
- package/__tests__/vault-search.test.js +0 -423
- package/__tests__/vault.test.js +0 -767
- package/__tests__/wechat-adapter.test.js +0 -594
- package/__tests__/whatsapp-adapter.test.js +0 -138
- package/scripts/_make-fixture-all.js +0 -126
- package/scripts/_make-fixture-contacts.js +0 -84
- package/scripts/evaluate-entity-resolver.js +0 -213
- package/scripts/run-native-tests-sandbox.sh +0 -55
- package/scripts/smoke-phase-5-5.js +0 -196
- package/scripts/smoke-phase-5-7.js +0 -181
- package/scripts/smoke-system-data-contacts.js +0 -309
- package/scripts/smoke-system-data.js +0 -312
- package/vitest.config.js +0 -88
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Toutiao article reader tests (real-device-driven 2026-06-18: the user's
|
|
3
|
-
* exported news_article.db `article` table = 48 feed-cache rows; title lives in
|
|
4
|
-
* the share_info JSON blob, not a column).
|
|
5
|
-
*
|
|
6
|
-
* Two layers: pure parsing via injected fake Database, + a real better-sqlite3
|
|
7
|
-
* db + real LocalVault round-trip proving the hand-built BROWSE events pass
|
|
8
|
-
* schema validation, are searchable, and re-ingest dedups on the stable
|
|
9
|
-
* originalId.
|
|
10
|
-
*/
|
|
11
|
-
"use strict";
|
|
12
|
-
|
|
13
|
-
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
14
|
-
|
|
15
|
-
const fs = require("node:fs");
|
|
16
|
-
const path = require("node:path");
|
|
17
|
-
const os = require("node:os");
|
|
18
|
-
|
|
19
|
-
const { LocalVault } = require("../../lib/vault");
|
|
20
|
-
const { generateKeyHex } = require("../../lib/key-providers");
|
|
21
|
-
const {
|
|
22
|
-
ARTICLE_TABLE,
|
|
23
|
-
readToutiaoArticles,
|
|
24
|
-
buildArticleEvents,
|
|
25
|
-
articlesToVault,
|
|
26
|
-
_internals,
|
|
27
|
-
} = require("../../lib/adapters/social-toutiao-adb/article-reader");
|
|
28
|
-
|
|
29
|
-
function makeFakeDb(rows, { table = ARTICLE_TABLE } = {}) {
|
|
30
|
-
const cols = [
|
|
31
|
-
"group_id", "item_id", "share_info", "ext_json", "share_url",
|
|
32
|
-
"behot_time", "read_timestamp", "is_user_digg", "is_user_repin",
|
|
33
|
-
];
|
|
34
|
-
return class FakeDb {
|
|
35
|
-
constructor() {}
|
|
36
|
-
prepare(sql) {
|
|
37
|
-
return {
|
|
38
|
-
get: (arg) => (/sqlite_master/.test(sql) ? (arg === table ? { name: table } : undefined) : undefined),
|
|
39
|
-
all: () => {
|
|
40
|
-
if (/table_info/.test(sql)) return cols.map((name) => ({ name }));
|
|
41
|
-
if (/FROM "/.test(sql)) return rows;
|
|
42
|
-
return [];
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
close() {}
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
describe("readToutiaoArticles (injected fake db)", () => {
|
|
51
|
-
it("parses title from share_info, strips the brand suffix, drops url tracking query", () => {
|
|
52
|
-
const Db = makeFakeDb([
|
|
53
|
-
{
|
|
54
|
-
group_id: 100, behot_time: 1781700000, read_timestamp: 0, is_user_digg: 1, is_user_repin: 0,
|
|
55
|
-
share_info: JSON.stringify({ title: "5月汽车出口延续快速增长态势 - 今日头条", share_url: "https://m.toutiao.com/g/100/?app=x&category_new=headline" }),
|
|
56
|
-
share_url: "https://m.toutiao.com/g/100/?category_new=headline",
|
|
57
|
-
},
|
|
58
|
-
]);
|
|
59
|
-
const { articles } = readToutiaoArticles("x.db", { _databaseClass: Db });
|
|
60
|
-
expect(articles).toHaveLength(1);
|
|
61
|
-
expect(articles[0].title).toBe("5月汽车出口延续快速增长态势"); // suffix stripped
|
|
62
|
-
expect(articles[0].url).toBe("https://m.toutiao.com/g/100/"); // query dropped
|
|
63
|
-
expect(articles[0].category).toBe("headline");
|
|
64
|
-
expect(articles[0].digg).toBe(true);
|
|
65
|
-
expect(articles[0].behotTime).toBe(1781700000 * 1000);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("falls back to ext_json.title when share_info has none, and skips untitled rows", () => {
|
|
69
|
-
const Db = makeFakeDb([
|
|
70
|
-
{ group_id: 1, ext_json: JSON.stringify({ title: "来自 ext_json 的标题" }), share_info: "{}" },
|
|
71
|
-
{ group_id: 2, share_info: "{}", ext_json: "{}" }, // untitled → dropped
|
|
72
|
-
]);
|
|
73
|
-
const { articles } = readToutiaoArticles("x.db", { _databaseClass: Db });
|
|
74
|
-
expect(articles).toHaveLength(1);
|
|
75
|
-
expect(articles[0].title).toBe("来自 ext_json 的标题");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("returns no articles when the table is absent", () => {
|
|
79
|
-
const Db = makeFakeDb([], { table: "other" });
|
|
80
|
-
expect(readToutiaoArticles("x.db", { _databaseClass: Db }).articles).toEqual([]);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("buildArticleEvents → BROWSE events, social-toutiao source, stable originalId, read flag", () => {
|
|
84
|
-
const { events } = buildArticleEvents(
|
|
85
|
-
[{ groupId: "55", title: "标题", url: "u", category: "headline", behotTime: 2, readTimestamp: 1781700000000, digg: true, repin: false }],
|
|
86
|
-
{ now: 1781800000000 },
|
|
87
|
-
);
|
|
88
|
-
expect(events).toHaveLength(1);
|
|
89
|
-
const e = events[0];
|
|
90
|
-
expect(e.subtype).toBe("browse");
|
|
91
|
-
expect(e.source.adapter).toBe("social-toutiao");
|
|
92
|
-
expect(e.source.originalId).toBe("social-toutiao:article:55");
|
|
93
|
-
expect(e.occurredAt).toBe(1781700000000); // read_timestamp wins over behot
|
|
94
|
-
expect(e.extra.kind).toBe("article");
|
|
95
|
-
expect(e.extra.read).toBe(true);
|
|
96
|
-
expect(e.extra.digg).toBe(true);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("extractCategory / extractUrl helpers", () => {
|
|
100
|
-
expect(_internals.extractCategory({ share_url: "x?a=1&category_new=my_tabs_digg&b=2" })).toBe("my_tabs_digg");
|
|
101
|
-
expect(_internals.extractUrl({ share_info: JSON.stringify({ share_url: "https://h/g/1/?t=1" }) })).toBe("https://h/g/1/");
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
describe("articlesToVault — real sqlite + real vault", () => {
|
|
106
|
-
let dir, dbPath, vdir, vault;
|
|
107
|
-
|
|
108
|
-
beforeAll(() => {
|
|
109
|
-
const Database = require("better-sqlite3-multiple-ciphers");
|
|
110
|
-
dir = fs.mkdtempSync(path.join(os.tmpdir(), "tt-article-"));
|
|
111
|
-
dbPath = path.join(dir, "news_article.db");
|
|
112
|
-
const db = new Database(dbPath);
|
|
113
|
-
db.exec(
|
|
114
|
-
"CREATE TABLE article (group_id INTEGER, item_id INTEGER, share_info TEXT, ext_json TEXT, " +
|
|
115
|
-
"share_url TEXT, behot_time INTEGER, read_timestamp INTEGER, is_user_digg INTEGER, is_user_repin INTEGER)",
|
|
116
|
-
);
|
|
117
|
-
const ins = db.prepare(
|
|
118
|
-
"INSERT INTO article (group_id, share_info, share_url, behot_time, read_timestamp, is_user_digg, is_user_repin) VALUES (?,?,?,?,?,?,?)",
|
|
119
|
-
);
|
|
120
|
-
ins.run(101, JSON.stringify({ title: "新华视点丨三峡水运新通道 - 今日头条", share_url: "https://m.toutiao.com/g/101/?x=1&category_new=headline" }), "https://m.toutiao.com/g/101/?category_new=headline", 1781700000, 0, 0, 0);
|
|
121
|
-
ins.run(102, JSON.stringify({ title: "5月汽车出口延续快速增长态势 - 今日头条", share_url: "https://m.toutiao.com/g/102/" }), "https://m.toutiao.com/g/102/?category_new=my_tabs_digg", 1781700100, 1781700200, 1, 0);
|
|
122
|
-
ins.run(103, "{}", "https://m.toutiao.com/g/103/", 1781700300, 0, 0, 0); // untitled → not ingested
|
|
123
|
-
db.close();
|
|
124
|
-
|
|
125
|
-
vdir = fs.mkdtempSync(path.join(os.tmpdir(), "tt-article-vault-"));
|
|
126
|
-
vault = new LocalVault({ path: path.join(vdir, "v.db"), key: generateKeyHex() });
|
|
127
|
-
vault.open();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
afterAll(() => {
|
|
131
|
-
try { vault.close(); } catch (_e) { /* best-effort */ }
|
|
132
|
-
try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_e) { /* best-effort */ }
|
|
133
|
-
try { fs.rmSync(vdir, { recursive: true, force: true }); } catch (_e) { /* best-effort */ }
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("reads titled articles and ingests valid BROWSE events", () => {
|
|
137
|
-
const r = articlesToVault(vault, dbPath, { now: 1781900000000 });
|
|
138
|
-
expect(r.articles).toBe(2); // the untitled row is skipped
|
|
139
|
-
expect(r.ingested).toBe(2); // both passed schema validation
|
|
140
|
-
expect(r.digg).toBe(1);
|
|
141
|
-
expect(r.read).toBe(1);
|
|
142
|
-
|
|
143
|
-
const events = vault.queryEvents({ limit: 100 }) || [];
|
|
144
|
-
const mine = events.filter((e) => e.extra && e.extra.kind === "article");
|
|
145
|
-
expect(mine.length).toBe(2);
|
|
146
|
-
expect(mine.every((e) => e.source.adapter === "social-toutiao")).toBe(true);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("re-ingest dedups on the stable per-article originalId", () => {
|
|
150
|
-
articlesToVault(vault, dbPath, { now: 1781999999999 });
|
|
151
|
-
const events = vault.queryEvents({ limit: 100 }) || [];
|
|
152
|
-
const mine = events.filter((e) => e.extra && e.extra.kind === "article");
|
|
153
|
-
expect(mine.length).toBe(2); // still two — updated, not duplicated
|
|
154
|
-
});
|
|
155
|
-
});
|
|
@@ -1,378 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect, vi } from "vitest";
|
|
4
|
-
const os = require("node:os");
|
|
5
|
-
|
|
6
|
-
const {
|
|
7
|
-
collect,
|
|
8
|
-
collectAndSync,
|
|
9
|
-
} = require("../../lib/adapters/social-toutiao-adb/collector");
|
|
10
|
-
const {
|
|
11
|
-
ToutiaoApiClient,
|
|
12
|
-
} = require("../../lib/adapters/social-toutiao-adb/api-client");
|
|
13
|
-
|
|
14
|
-
function makeFakeFetch(responses) {
|
|
15
|
-
const calls = [];
|
|
16
|
-
const fakeFetch = async (urlStr, opts) => {
|
|
17
|
-
calls.push({ url: urlStr, opts });
|
|
18
|
-
for (const [pattern, payload] of responses) {
|
|
19
|
-
if (urlStr.includes(pattern)) {
|
|
20
|
-
const resolved =
|
|
21
|
-
typeof payload === "function" ? await payload(urlStr, opts) : payload;
|
|
22
|
-
return {
|
|
23
|
-
ok: resolved.status == null || resolved.status === 200,
|
|
24
|
-
status: resolved.status || 200,
|
|
25
|
-
text: async () => resolved.body,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
throw new Error("fake fetch: no response for " + urlStr);
|
|
30
|
-
};
|
|
31
|
-
return { fakeFetch, calls };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const HAPPY_RESPONSES = [
|
|
35
|
-
[
|
|
36
|
-
"passport/account/info/v2",
|
|
37
|
-
{
|
|
38
|
-
body: JSON.stringify({
|
|
39
|
-
status_code: 0,
|
|
40
|
-
data: {
|
|
41
|
-
user_id: "12345",
|
|
42
|
-
screen_name: "Alice",
|
|
43
|
-
avatar_url: "https://a/x.jpg",
|
|
44
|
-
},
|
|
45
|
-
}),
|
|
46
|
-
},
|
|
47
|
-
],
|
|
48
|
-
[
|
|
49
|
-
"api/news/feed/v90",
|
|
50
|
-
{
|
|
51
|
-
body: JSON.stringify({
|
|
52
|
-
data: [{ group_id: "G1", title: "T1", behot_time: 1700000000 }],
|
|
53
|
-
}),
|
|
54
|
-
},
|
|
55
|
-
],
|
|
56
|
-
[
|
|
57
|
-
"article/v2/tab_comments",
|
|
58
|
-
{
|
|
59
|
-
body: JSON.stringify({
|
|
60
|
-
data: [{ group_id: "C1", title: "Saved", behot_time: 1700001000 }],
|
|
61
|
-
}),
|
|
62
|
-
},
|
|
63
|
-
],
|
|
64
|
-
[
|
|
65
|
-
"api/search/content",
|
|
66
|
-
{
|
|
67
|
-
body: JSON.stringify({
|
|
68
|
-
data: { user_search_history: [{ keyword: "kw", time: 1700002000 }] },
|
|
69
|
-
}),
|
|
70
|
-
},
|
|
71
|
-
],
|
|
72
|
-
];
|
|
73
|
-
|
|
74
|
-
function makeBridge(invokeResult, accountResult) {
|
|
75
|
-
return {
|
|
76
|
-
invoke: vi.fn(async (method) => {
|
|
77
|
-
if (method === "toutiao.account") {
|
|
78
|
-
// Mirror real wiring: a separate extension. Tests that don't wire it
|
|
79
|
-
// get a throw (collector falls through gracefully).
|
|
80
|
-
if (accountResult === undefined) {
|
|
81
|
-
throw new Error("toutiao.account not wired in this test");
|
|
82
|
-
}
|
|
83
|
-
return accountResult;
|
|
84
|
-
}
|
|
85
|
-
return invokeResult;
|
|
86
|
-
}),
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const COOKIE_PAYLOAD = {
|
|
91
|
-
cookie: "sessionid=abc; passport_uid=12345",
|
|
92
|
-
uid: "12345",
|
|
93
|
-
diagnostic: { cookieCount: 2, hadEncrypted: false, cookieNames: ["sessionid", "passport_uid"] },
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
describe("collect — happy path with signProvider", () => {
|
|
97
|
-
it("warmUp → signed endpoints → shutdown", async () => {
|
|
98
|
-
const { fakeFetch } = makeFakeFetch(HAPPY_RESPONSES);
|
|
99
|
-
const calls = [];
|
|
100
|
-
const sign = {
|
|
101
|
-
warmUp: vi.fn(async (c) => calls.push({ warmUp: c })),
|
|
102
|
-
signUrl: vi.fn(async (url) => {
|
|
103
|
-
const u = new URL(String(url));
|
|
104
|
-
u.searchParams.set("_signature", "BRIDGE_SIG");
|
|
105
|
-
return u;
|
|
106
|
-
}),
|
|
107
|
-
shutdown: vi.fn(async () => calls.push("shutdown")),
|
|
108
|
-
};
|
|
109
|
-
const client = new ToutiaoApiClient({
|
|
110
|
-
fetch: fakeFetch,
|
|
111
|
-
signProvider: sign,
|
|
112
|
-
});
|
|
113
|
-
const r = await collect(makeBridge(COOKIE_PAYLOAD), {
|
|
114
|
-
apiClient: client,
|
|
115
|
-
signProvider: sign,
|
|
116
|
-
stagingDir: os.tmpdir(),
|
|
117
|
-
});
|
|
118
|
-
expect(sign.warmUp).toHaveBeenCalledWith(COOKIE_PAYLOAD.cookie);
|
|
119
|
-
expect(sign.shutdown).toHaveBeenCalledOnce();
|
|
120
|
-
expect(r.uid).toBe("12345");
|
|
121
|
-
expect(r.nickname).toBe("Alice");
|
|
122
|
-
expect(r.profileFetchFailed).toBe(false);
|
|
123
|
-
expect(r.eventCounts.feed).toBe(1);
|
|
124
|
-
expect(r.eventCounts.collection).toBe(1);
|
|
125
|
-
expect(r.eventCounts.search).toBe(1);
|
|
126
|
-
expect(r.eventCounts.profile).toBe(1);
|
|
127
|
-
expect(r.signProviderHits).toBe(3); // 3 signed endpoints
|
|
128
|
-
expect(r.signProviderFallbacks).toBe(0);
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
describe("collect — fallback path (no signProvider)", () => {
|
|
133
|
-
it("3 signed endpoints short-circuit; profile still emitted", async () => {
|
|
134
|
-
const { fakeFetch } = makeFakeFetch(HAPPY_RESPONSES);
|
|
135
|
-
const client = new ToutiaoApiClient({ fetch: fakeFetch });
|
|
136
|
-
const r = await collect(makeBridge(COOKIE_PAYLOAD), {
|
|
137
|
-
apiClient: client,
|
|
138
|
-
stagingDir: os.tmpdir(),
|
|
139
|
-
});
|
|
140
|
-
expect(r.uid).toBe("12345");
|
|
141
|
-
expect(r.profileFetchFailed).toBe(false);
|
|
142
|
-
expect(r.eventCounts.profile).toBe(1);
|
|
143
|
-
expect(r.eventCounts.feed).toBe(0); // short-circuit
|
|
144
|
-
expect(r.eventCounts.collection).toBe(0);
|
|
145
|
-
expect(r.eventCounts.search).toBe(0);
|
|
146
|
-
expect(r.signProviderUsed).toBe("none");
|
|
147
|
-
expect(r.signProviderHits).toBe(0);
|
|
148
|
-
expect(r.signProviderFallbacks).toBe(3);
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
describe("collect — profile fetch fails", () => {
|
|
153
|
-
it("emits empty snapshot with cookie-derived uid + profileFetchFailed=true", async () => {
|
|
154
|
-
const { fakeFetch } = makeFakeFetch([
|
|
155
|
-
[
|
|
156
|
-
"passport/account/info/v2",
|
|
157
|
-
{
|
|
158
|
-
body: JSON.stringify({
|
|
159
|
-
status_code: 1,
|
|
160
|
-
status_msg: "token expired",
|
|
161
|
-
}),
|
|
162
|
-
},
|
|
163
|
-
],
|
|
164
|
-
]);
|
|
165
|
-
const client = new ToutiaoApiClient({ fetch: fakeFetch });
|
|
166
|
-
const r = await collect(makeBridge(COOKIE_PAYLOAD), {
|
|
167
|
-
apiClient: client,
|
|
168
|
-
stagingDir: os.tmpdir(),
|
|
169
|
-
});
|
|
170
|
-
expect(r.profileFetchFailed).toBe(true);
|
|
171
|
-
expect(r.uid).toBe("12345"); // from cookie pre-extract
|
|
172
|
-
expect(r.eventCounts.total).toBe(0);
|
|
173
|
-
expect(r.lastErrorCode).toBe(1);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("falls back to 'unknown-user' uid when cookie pre-extract also empty", async () => {
|
|
177
|
-
const { fakeFetch } = makeFakeFetch([
|
|
178
|
-
[
|
|
179
|
-
"passport/account/info/v2",
|
|
180
|
-
{ body: JSON.stringify({ status_code: 1, status_msg: "expired" }) },
|
|
181
|
-
],
|
|
182
|
-
]);
|
|
183
|
-
const client = new ToutiaoApiClient({ fetch: fakeFetch });
|
|
184
|
-
const r = await collect(
|
|
185
|
-
makeBridge({
|
|
186
|
-
cookie: "sessionid=abc",
|
|
187
|
-
uid: null,
|
|
188
|
-
diagnostic: {},
|
|
189
|
-
}),
|
|
190
|
-
{ apiClient: client, stagingDir: os.tmpdir() },
|
|
191
|
-
);
|
|
192
|
-
expect(r.uid).toBe(null);
|
|
193
|
-
expect(r.profileFetchFailed).toBe(true);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("profile permission-denied (error_code 16) BUT cookie uid + signer → feed/collection/search still collect", async () => {
|
|
197
|
-
// Real-device 2026-06-11: logged-in Toutiao returns passport error_code 16
|
|
198
|
-
// 该应用无权限. We must NOT abort — feed is cookie-identified, so with a
|
|
199
|
-
// SignBridge the signed endpoints still flow. Profile event is skipped, but
|
|
200
|
-
// the headline error (16) is surfaced and feed/collection/search collect.
|
|
201
|
-
const { fakeFetch } = makeFakeFetch([
|
|
202
|
-
[
|
|
203
|
-
"passport/account/info/v2",
|
|
204
|
-
{
|
|
205
|
-
body: JSON.stringify({
|
|
206
|
-
message: "error",
|
|
207
|
-
data: { error_code: 16, description: "该应用无权限" },
|
|
208
|
-
}),
|
|
209
|
-
},
|
|
210
|
-
],
|
|
211
|
-
...HAPPY_RESPONSES.slice(1), // feed / comments / search responses
|
|
212
|
-
]);
|
|
213
|
-
const sign = {
|
|
214
|
-
warmUp: vi.fn(async () => {}),
|
|
215
|
-
signUrl: vi.fn(async (url) => {
|
|
216
|
-
const u = new URL(String(url));
|
|
217
|
-
u.searchParams.set("_signature", "BRIDGE_SIG");
|
|
218
|
-
return u;
|
|
219
|
-
}),
|
|
220
|
-
shutdown: vi.fn(async () => {}),
|
|
221
|
-
};
|
|
222
|
-
const client = new ToutiaoApiClient({ fetch: fakeFetch, signProvider: sign });
|
|
223
|
-
const r = await collect(makeBridge(COOKIE_PAYLOAD), {
|
|
224
|
-
apiClient: client,
|
|
225
|
-
signProvider: sign,
|
|
226
|
-
stagingDir: os.tmpdir(),
|
|
227
|
-
});
|
|
228
|
-
expect(r.profileFetchFailed).toBe(true);
|
|
229
|
-
expect(r.uid).toBe("12345"); // cookie-derived
|
|
230
|
-
expect(r.lastErrorCode).toBe(16); // headline profile error preserved
|
|
231
|
-
expect(r.lastErrorMessage).toBe("该应用无权限");
|
|
232
|
-
expect(r.eventCounts.profile).toBe(0); // no profile event
|
|
233
|
-
expect(r.eventCounts.feed).toBe(1); // ← previously 0 (aborted before signing)
|
|
234
|
-
expect(r.eventCounts.collection).toBe(1);
|
|
235
|
-
expect(r.eventCounts.search).toBe(1);
|
|
236
|
-
expect(r.eventCounts.total).toBe(3);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it("profile error_code 16 + NO cookie uid → recovers uid from local account_db, collects signed endpoints", async () => {
|
|
240
|
-
// Real-device 2026-06-11: web profile permission-denied AND the WebView
|
|
241
|
-
// cookie jar has no numeric uid. The collector asks the bridge for
|
|
242
|
-
// 'toutiao.account' (local account_db) and proceeds with that uid.
|
|
243
|
-
const { fakeFetch } = makeFakeFetch([
|
|
244
|
-
[
|
|
245
|
-
"passport/account/info/v2",
|
|
246
|
-
{ body: JSON.stringify({ message: "error", data: { error_code: 16, description: "该应用无权限" } }) },
|
|
247
|
-
],
|
|
248
|
-
...HAPPY_RESPONSES.slice(1),
|
|
249
|
-
]);
|
|
250
|
-
const sign = {
|
|
251
|
-
warmUp: vi.fn(async () => {}),
|
|
252
|
-
signUrl: vi.fn(async (url) => {
|
|
253
|
-
const u = new URL(String(url));
|
|
254
|
-
u.searchParams.set("_signature", "BRIDGE_SIG");
|
|
255
|
-
return u;
|
|
256
|
-
}),
|
|
257
|
-
shutdown: vi.fn(async () => {}),
|
|
258
|
-
};
|
|
259
|
-
const bridge = {
|
|
260
|
-
invoke: vi.fn(async (m) => {
|
|
261
|
-
if (m === "toutiao.cookies") return { cookie: "sessionid=abc", uid: null, diagnostic: {} };
|
|
262
|
-
if (m === "toutiao.account") return { uid: "92585279158", nickname: "小明", secUid: "MS4w" };
|
|
263
|
-
throw new Error("unknown " + m);
|
|
264
|
-
}),
|
|
265
|
-
};
|
|
266
|
-
const client = new ToutiaoApiClient({ fetch: fakeFetch, signProvider: sign });
|
|
267
|
-
const r = await collect(bridge, { apiClient: client, signProvider: sign, stagingDir: os.tmpdir() });
|
|
268
|
-
expect(r.profileFetchFailed).toBe(true);
|
|
269
|
-
expect(r.profileSource).toBe("local-account-db");
|
|
270
|
-
expect(r.uid).toBe("92585279158");
|
|
271
|
-
expect(r.nickname).toBe("小明");
|
|
272
|
-
expect(r.lastErrorCode).toBe(16); // headline web error preserved
|
|
273
|
-
expect(r.eventCounts.profile).toBe(1); // profile event from local account
|
|
274
|
-
expect(r.eventCounts.feed).toBe(1);
|
|
275
|
-
expect(r.eventCounts.collection).toBe(1);
|
|
276
|
-
expect(r.eventCounts.search).toBe(1);
|
|
277
|
-
});
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
describe("collect — bridge warmUp failure", () => {
|
|
281
|
-
it("tolerates warmUp throw (falls through to fallback path)", async () => {
|
|
282
|
-
const { fakeFetch } = makeFakeFetch(HAPPY_RESPONSES);
|
|
283
|
-
const sign = {
|
|
284
|
-
warmUp: vi.fn(async () => {
|
|
285
|
-
throw new Error("toutiao.com 403 — anti-bot blocked");
|
|
286
|
-
}),
|
|
287
|
-
signUrl: vi.fn(async () => null),
|
|
288
|
-
shutdown: vi.fn(async () => {}),
|
|
289
|
-
};
|
|
290
|
-
const client = new ToutiaoApiClient({
|
|
291
|
-
fetch: fakeFetch,
|
|
292
|
-
signProvider: sign,
|
|
293
|
-
});
|
|
294
|
-
const r = await collect(makeBridge(COOKIE_PAYLOAD), {
|
|
295
|
-
apiClient: client,
|
|
296
|
-
signProvider: sign,
|
|
297
|
-
stagingDir: os.tmpdir(),
|
|
298
|
-
});
|
|
299
|
-
expect(r.profileFetchFailed).toBe(false); // profile uses no _sig
|
|
300
|
-
expect(r.eventCounts.feed).toBe(0); // signed endpoints fall through
|
|
301
|
-
expect(client._fallbackHits).toBe(3);
|
|
302
|
-
expect(sign.shutdown).toHaveBeenCalledOnce();
|
|
303
|
-
});
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
describe("collect — malformed bridge payload", () => {
|
|
307
|
-
it("throws when bridge.invoke returns no cookie", async () => {
|
|
308
|
-
const bridge = { invoke: vi.fn(async () => ({ uid: "1" })) };
|
|
309
|
-
await expect(
|
|
310
|
-
collect(bridge, { stagingDir: os.tmpdir() }),
|
|
311
|
-
).rejects.toThrow(/malformed payload/);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it("throws when bridge missing invoke", async () => {
|
|
315
|
-
await expect(collect({}, {})).rejects.toThrow(
|
|
316
|
-
/bridge must expose invoke/,
|
|
317
|
-
);
|
|
318
|
-
});
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
describe("collect — signProviderUsed diagnostic", () => {
|
|
322
|
-
it("reports class name when bridge present", async () => {
|
|
323
|
-
const { fakeFetch } = makeFakeFetch(HAPPY_RESPONSES);
|
|
324
|
-
class ToutiaoSignBridge {
|
|
325
|
-
constructor() {
|
|
326
|
-
this.warmUp = vi.fn(async () => {});
|
|
327
|
-
this.signUrl = vi.fn(async (url) => {
|
|
328
|
-
const u = new URL(String(url));
|
|
329
|
-
u.searchParams.set("_signature", "X");
|
|
330
|
-
return u;
|
|
331
|
-
});
|
|
332
|
-
this.shutdown = vi.fn(async () => {});
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
const sign = new ToutiaoSignBridge();
|
|
336
|
-
const client = new ToutiaoApiClient({
|
|
337
|
-
fetch: fakeFetch,
|
|
338
|
-
signProvider: sign,
|
|
339
|
-
});
|
|
340
|
-
const r = await collect(makeBridge(COOKIE_PAYLOAD), {
|
|
341
|
-
apiClient: client,
|
|
342
|
-
signProvider: sign,
|
|
343
|
-
stagingDir: os.tmpdir(),
|
|
344
|
-
});
|
|
345
|
-
expect(r.signProviderUsed).toBe("ToutiaoSignBridge");
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
it("reports 'none' when no bridge", async () => {
|
|
349
|
-
const { fakeFetch } = makeFakeFetch(HAPPY_RESPONSES);
|
|
350
|
-
const client = new ToutiaoApiClient({ fetch: fakeFetch });
|
|
351
|
-
const r = await collect(makeBridge(COOKIE_PAYLOAD), {
|
|
352
|
-
apiClient: client,
|
|
353
|
-
stagingDir: os.tmpdir(),
|
|
354
|
-
});
|
|
355
|
-
expect(r.signProviderUsed).toBe("none");
|
|
356
|
-
});
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
describe("collectAndSync", () => {
|
|
360
|
-
it("orchestrates collect + registry.syncAdapter + cleanup", async () => {
|
|
361
|
-
const { fakeFetch } = makeFakeFetch(HAPPY_RESPONSES);
|
|
362
|
-
const client = new ToutiaoApiClient({ fetch: fakeFetch });
|
|
363
|
-
const registry = {
|
|
364
|
-
syncAdapter: vi.fn(async (name) => ({ adapter: name, status: "ok" })),
|
|
365
|
-
};
|
|
366
|
-
const r = await collectAndSync(makeBridge(COOKIE_PAYLOAD), registry, {
|
|
367
|
-
apiClient: client,
|
|
368
|
-
stagingDir: os.tmpdir(),
|
|
369
|
-
});
|
|
370
|
-
expect(registry.syncAdapter).toHaveBeenCalledWith(
|
|
371
|
-
"social-toutiao",
|
|
372
|
-
expect.objectContaining({ inputPath: expect.stringContaining(".json") }),
|
|
373
|
-
);
|
|
374
|
-
expect(r.adapter).toBe("social-toutiao");
|
|
375
|
-
expect(r.toutiao.uid).toBe("12345");
|
|
376
|
-
expect(r.toutiao.eventCounts.profile).toBe(1);
|
|
377
|
-
});
|
|
378
|
-
});
|