@chainlesschain/personal-data-hub 0.4.28 → 0.4.30
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/README.md +13 -5
- package/lib/adapters/social-douyin-adb/usage-profile-reader.js +253 -0
- package/lib/adapters/social-douyin-adb/watch-history-reader.js +104 -31
- package/lib/adapters/social-toutiao-adb/article-reader.js +202 -0
- package/lib/analysis-skills/overview.js +24 -4
- package/lib/analysis-skills/spending.js +63 -2
- package/lib/analysis-skills/timeline.js +11 -6
- package/lib/prompt-builder.js +15 -1
- package/lib/query-parser.js +38 -8
- package/package.json +4 -1
- 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-watch-history.test.js +0 -192
- 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-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 -679
- 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 -302
- 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,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
|
-
});
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect, vi } from "vitest";
|
|
4
|
-
|
|
5
|
-
const {
|
|
6
|
-
createToutiaoCookiesExtension,
|
|
7
|
-
TOUTIAO_COOKIES_REMOTE_PATH,
|
|
8
|
-
TOUTIAO_COOKIE_HOST_DOMAIN,
|
|
9
|
-
TOUTIAO_SESSION_COOKIES,
|
|
10
|
-
TOUTIAO_UID_COOKIES,
|
|
11
|
-
assembleToutiaoCookieHeader,
|
|
12
|
-
_internals,
|
|
13
|
-
} = require("../../lib/adapters/social-toutiao-adb/cookies-extension");
|
|
14
|
-
|
|
15
|
-
describe("constants", () => {
|
|
16
|
-
it("path points to com.ss.android.article.news WebView cookies", () => {
|
|
17
|
-
expect(TOUTIAO_COOKIES_REMOTE_PATH).toBe(
|
|
18
|
-
"/data/data/com.ss.android.article.news/app_webview/Default/Cookies",
|
|
19
|
-
);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("host domain is toutiao.com", () => {
|
|
23
|
-
expect(TOUTIAO_COOKIE_HOST_DOMAIN).toBe("toutiao.com");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("session cookies enumerated (lenient — sessionid OR sessionid_ss)", () => {
|
|
27
|
-
expect([...TOUTIAO_SESSION_COOKIES]).toEqual(["sessionid", "sessionid_ss"]);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("uid cookie candidates in priority order", () => {
|
|
31
|
-
expect([...TOUTIAO_UID_COOKIES]).toEqual([
|
|
32
|
-
"passport_uid",
|
|
33
|
-
"multi_sids",
|
|
34
|
-
"__ac_uid",
|
|
35
|
-
"tt_uid",
|
|
36
|
-
]);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe("pickUidFromCookieMap", () => {
|
|
41
|
-
const make = (entries) => new Map(entries.map(([k, v]) => [k, { value: v }]));
|
|
42
|
-
|
|
43
|
-
it("prefers passport_uid", () => {
|
|
44
|
-
const map = make([
|
|
45
|
-
["passport_uid", "12345"],
|
|
46
|
-
["__ac_uid", "99999"],
|
|
47
|
-
]);
|
|
48
|
-
expect(_internals.pickUidFromCookieMap(map)).toBe("12345");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("falls back to multi_sids first segment", () => {
|
|
52
|
-
const map = make([["multi_sids", "67890:abcd;11111:efgh"]]);
|
|
53
|
-
expect(_internals.pickUidFromCookieMap(map)).toBe("67890");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("falls back to __ac_uid when passport + multi missing", () => {
|
|
57
|
-
const map = make([["__ac_uid", "555"]]);
|
|
58
|
-
expect(_internals.pickUidFromCookieMap(map)).toBe("555");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("falls back to tt_uid (legacy) when above missing", () => {
|
|
62
|
-
const map = make([["tt_uid", "777"]]);
|
|
63
|
-
expect(_internals.pickUidFromCookieMap(map)).toBe("777");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("returns null for '0' values (guest sentinel)", () => {
|
|
67
|
-
const map = make([
|
|
68
|
-
["passport_uid", "0"],
|
|
69
|
-
["__ac_uid", "0"],
|
|
70
|
-
]);
|
|
71
|
-
expect(_internals.pickUidFromCookieMap(map)).toBe(null);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("returns null for non-numeric values", () => {
|
|
75
|
-
const map = make([["passport_uid", "notnumeric"]]);
|
|
76
|
-
expect(_internals.pickUidFromCookieMap(map)).toBe(null);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("returns null when none of the uid candidates present", () => {
|
|
80
|
-
const map = make([["sessionid", "abc"]]);
|
|
81
|
-
expect(_internals.pickUidFromCookieMap(map)).toBe(null);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe("assembleToutiaoCookieHeader", () => {
|
|
86
|
-
const mkCookie = (name, value, hostKey = ".toutiao.com") => ({
|
|
87
|
-
name,
|
|
88
|
-
value,
|
|
89
|
-
hostKey,
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("builds header from a session + uid cookie", () => {
|
|
93
|
-
const cookies = [
|
|
94
|
-
mkCookie("sessionid", "sessabc"),
|
|
95
|
-
mkCookie("passport_uid", "12345"),
|
|
96
|
-
];
|
|
97
|
-
const r = assembleToutiaoCookieHeader(cookies);
|
|
98
|
-
expect(r.header).toContain("sessionid=sessabc");
|
|
99
|
-
expect(r.header).toContain("passport_uid=12345");
|
|
100
|
-
expect(r.uid).toBe("12345");
|
|
101
|
-
expect(r.missing).toEqual([]);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("succeeds with only sessionid_ss (no sessionid)", () => {
|
|
105
|
-
const cookies = [
|
|
106
|
-
mkCookie("sessionid_ss", "altsess"),
|
|
107
|
-
mkCookie("passport_uid", "999"),
|
|
108
|
-
];
|
|
109
|
-
const r = assembleToutiaoCookieHeader(cookies);
|
|
110
|
-
expect(r.header).toContain("sessionid_ss=altsess");
|
|
111
|
-
expect(r.uid).toBe("999");
|
|
112
|
-
expect(r.missing).toEqual([]);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("returns null header when no session cookie present", () => {
|
|
116
|
-
const cookies = [mkCookie("passport_uid", "12345")];
|
|
117
|
-
const r = assembleToutiaoCookieHeader(cookies);
|
|
118
|
-
expect(r.header).toBe(null);
|
|
119
|
-
expect(r.uid).toBe(null);
|
|
120
|
-
expect(r.missing).toEqual(["sessionid", "sessionid_ss"]);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("succeeds without any uid cookie (uid=null, fetchProfile fills later)", () => {
|
|
124
|
-
const cookies = [
|
|
125
|
-
mkCookie("sessionid", "sess"),
|
|
126
|
-
mkCookie("ttwid", "anonid"),
|
|
127
|
-
];
|
|
128
|
-
const r = assembleToutiaoCookieHeader(cookies);
|
|
129
|
-
expect(r.header).toContain("sessionid=sess");
|
|
130
|
-
expect(r.uid).toBe(null);
|
|
131
|
-
expect(r.missing).toEqual([]);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("dedupes by longest hostKey when same name appears twice", () => {
|
|
135
|
-
const cookies = [
|
|
136
|
-
mkCookie("sessionid", "subdomain-val", "www.toutiao.com"),
|
|
137
|
-
mkCookie("sessionid", "wildcard-val", ".toutiao.com"),
|
|
138
|
-
mkCookie("passport_uid", "1"),
|
|
139
|
-
];
|
|
140
|
-
const r = assembleToutiaoCookieHeader(cookies);
|
|
141
|
-
// www.toutiao.com (16 chars) > .toutiao.com (12 chars) — longest wins
|
|
142
|
-
expect(r.header).toContain("sessionid=subdomain-val");
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("throws TypeError on non-array input", () => {
|
|
146
|
-
expect(() => assembleToutiaoCookieHeader(null)).toThrow(TypeError);
|
|
147
|
-
expect(() => assembleToutiaoCookieHeader("string")).toThrow(TypeError);
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
describe("createToutiaoCookiesExtension", () => {
|
|
152
|
-
it("rejects when ctx missing required functions", async () => {
|
|
153
|
-
const ext = createToutiaoCookiesExtension();
|
|
154
|
-
await expect(ext({}, {})).rejects.toThrow(/ctx must provide/);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("rejects when ctx.adb is not a function", async () => {
|
|
158
|
-
const ext = createToutiaoCookiesExtension();
|
|
159
|
-
await expect(
|
|
160
|
-
ext({}, { adb: null, pickDevice: () => "serial" }),
|
|
161
|
-
).rejects.toThrow(/ctx must provide/);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
describe("pullCookiesViaSu — installed-vs-not-installed diagnosis", () => {
|
|
166
|
-
function makeAdb({ ls, pm }) {
|
|
167
|
-
return async (args) => {
|
|
168
|
-
const cmd = args.join(" ");
|
|
169
|
-
if (cmd.includes("pm list packages")) return pm || "";
|
|
170
|
-
if (cmd.includes("ls ")) return ls;
|
|
171
|
-
throw new Error("fake adb: unexpected command " + cmd);
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
it("throws TOUTIAO_NOT_INSTALLED when cookies absent AND package not installed", async () => {
|
|
176
|
-
const adb = makeAdb({ ls: "NOT_FOUND\r\n", pm: "" });
|
|
177
|
-
await expect(
|
|
178
|
-
_internals.pullCookiesViaSu(adb, "serial", {}),
|
|
179
|
-
).rejects.toThrow(/TOUTIAO_NOT_INSTALLED/);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it("throws TOUTIAO_NO_WEBVIEW_COOKIES when cookies absent but package installed (real-device 2026-06-11)", async () => {
|
|
183
|
-
// Verified on device 5lhyaqu8lbwstc6x: com.ss.android.article.news
|
|
184
|
-
// installed but no webview cookie store → must NOT say NOT_INSTALLED.
|
|
185
|
-
const adb = makeAdb({
|
|
186
|
-
ls: "NOT_FOUND\r\n",
|
|
187
|
-
pm: "package:com.ss.android.article.news\r\n",
|
|
188
|
-
});
|
|
189
|
-
await expect(
|
|
190
|
-
_internals.pullCookiesViaSu(adb, "serial", {}),
|
|
191
|
-
).rejects.toThrow(/TOUTIAO_NO_WEBVIEW_COOKIES/);
|
|
192
|
-
});
|
|
193
|
-
});
|