dsh-browser-playwright-codex 0.5.4 → 0.6.0
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/CHANGELOG.md +93 -2
- package/NOTICE.md +39 -0
- package/README.md +72 -11
- package/SECURITY.md +22 -0
- package/cordis.patch.yml +6 -0
- package/lib/compat.d.ts +9 -9
- package/lib/compat.js +0 -20
- package/lib/contract.d.ts +2 -2
- package/lib/contract.js +0 -4
- package/lib/login-sites.d.ts +136 -0
- package/lib/login-sites.js +303 -0
- package/lib/page-actions.d.ts +2 -2
- package/lib/page-actions.js +1 -1
- package/lib/provider.js +45 -1
- package/lib/runtime-state.d.ts +39 -0
- package/lib/runtime-state.js +20 -26
- package/lib/session.d.ts +40 -22
- package/lib/session.js +65 -1
- package/lib/site-aliases.d.ts +33 -0
- package/lib/site-aliases.js +132 -0
- package/lib/tool.js +95 -91
- package/lib/types.d.ts +13 -0
- package/package.json +5 -2
- package/scripts/preflight.mjs +192 -0
package/lib/session.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { BrowserContext, CDPSession, Locator, Page, Request } from 'playwri
|
|
|
7
7
|
import type { PlaywrightProvider } from './provider.ts';
|
|
8
8
|
import type { BrowserNode, BrowserSnapshot, DiagnosticsEntry, LoadState } from './types.ts';
|
|
9
9
|
import type { RefSignature } from './page-snapshot.ts';
|
|
10
|
+
import { type CookieRow } from './login-sites.ts';
|
|
10
11
|
import type { ActionResult, PendingDialogRecord } from './provider.ts';
|
|
11
12
|
/** Live session over one browser context owned by one caller. */
|
|
12
13
|
export declare class PlaywrightSession {
|
|
@@ -50,6 +51,23 @@ export declare class PlaywrightSession {
|
|
|
50
51
|
/** Nonce of the baseline ref space; a change means navigation reset. */
|
|
51
52
|
lastRefNonce: string | undefined;
|
|
52
53
|
constructor(provider: PlaywrightProvider, owner: string, context: BrowserContext);
|
|
54
|
+
/**
|
|
55
|
+
* The session's cookies, reduced to what the login inference needs. `value` is dropped here,
|
|
56
|
+
* at the only place cookies enter the summary path, so nothing downstream can leak it.
|
|
57
|
+
*/
|
|
58
|
+
cookieJar(): Promise<readonly CookieRow[]>;
|
|
59
|
+
/**
|
|
60
|
+
* Registrable domains of the pages this browser currently has open — the "first-party" set the
|
|
61
|
+
* login inference attributes cookies to. Taken from the live pages rather than the browser's
|
|
62
|
+
* history database: the plugin only claims what it can see, and never reads a new data source.
|
|
63
|
+
*/
|
|
64
|
+
/**
|
|
65
|
+
* Page titles by registrable domain for the pages currently open — the site's own name for
|
|
66
|
+
* itself, which is what browsers and password managers show instead of a bare hostname. Titles
|
|
67
|
+
* are display-only and trimmed; a domain with no open page simply has no title.
|
|
68
|
+
*/
|
|
69
|
+
siteTitles(): Promise<Map<string, string>>;
|
|
70
|
+
visitedHosts(): string[];
|
|
53
71
|
/** Run one operation under busy tracking: idle disposal defers until it settles. */
|
|
54
72
|
run<T>(operation: () => Promise<T>): Promise<T>;
|
|
55
73
|
/** Fast actionability pre-check (visible/enabled/editable) before an action. */
|
|
@@ -87,7 +105,7 @@ export declare class PlaywrightSession {
|
|
|
87
105
|
* Answer the pending native dialog and let the action blocked behind it
|
|
88
106
|
* finish. This is the only place a dialog is ever accepted or dismissed.
|
|
89
107
|
*/
|
|
90
|
-
resolveDialog(accept: boolean, promptText: string | undefined, signal
|
|
108
|
+
resolveDialog(accept: boolean, promptText: string | undefined, signal?: AbortSignal): Promise<BrowserSnapshot>;
|
|
91
109
|
/** Read the snapshot tree from one page (must run inside a run()). */
|
|
92
110
|
capture(page: Page, opts: {
|
|
93
111
|
interactiveOnly?: boolean;
|
|
@@ -135,7 +153,7 @@ export declare class PlaywrightSession {
|
|
|
135
153
|
buildRefSignatures(nodes: readonly BrowserNode[]): Map<string, RefSignature>;
|
|
136
154
|
/** The document nonce shared by this snapshot's refs, or undefined. */
|
|
137
155
|
refNonce(nodes: readonly BrowserNode[]): string | undefined;
|
|
138
|
-
navigate(url: string, waitUntil: LoadState, signal
|
|
156
|
+
navigate(url: string, waitUntil: LoadState, signal?: AbortSignal): Promise<BrowserSnapshot | {
|
|
139
157
|
url: string;
|
|
140
158
|
title: string;
|
|
141
159
|
nodes: never[];
|
|
@@ -162,13 +180,13 @@ export declare class PlaywrightSession {
|
|
|
162
180
|
* an overlaying element can never swallow the click silently. Returns the
|
|
163
181
|
* first unobstructed {x, y}, or throws describing what intercepted it.
|
|
164
182
|
*/
|
|
165
|
-
resolveClickPoint(locator: Locator, signal
|
|
183
|
+
resolveClickPoint(locator: Locator, signal?: AbortSignal): Promise<{
|
|
166
184
|
x: number;
|
|
167
185
|
y: number;
|
|
168
186
|
hitDesc: string;
|
|
169
187
|
hitSelf: boolean;
|
|
170
188
|
}>;
|
|
171
|
-
click(ref: string, signal
|
|
189
|
+
click(ref: string, signal?: AbortSignal): Promise<BrowserSnapshot | {
|
|
172
190
|
url: string;
|
|
173
191
|
title: string;
|
|
174
192
|
nodes: never[];
|
|
@@ -178,12 +196,12 @@ export declare class PlaywrightSession {
|
|
|
178
196
|
}>;
|
|
179
197
|
/**
|
|
180
198
|
* Click an element the snapshot gave no ref, addressed by its visible label
|
|
181
|
-
* (e.g. a menu item revealed by
|
|
199
|
+
* (e.g. a menu item revealed by browser_click with mode "hover" that is plain text). The
|
|
182
200
|
* label must equal an element's own direct text; the deepest match wins,
|
|
183
201
|
* and the landing note names what was actually clicked plus how many
|
|
184
202
|
* elements shared the label.
|
|
185
203
|
*/
|
|
186
|
-
clickText(text: string, signal
|
|
204
|
+
clickText(text: string, signal?: AbortSignal): Promise<BrowserSnapshot | {
|
|
187
205
|
url: string;
|
|
188
206
|
title: string;
|
|
189
207
|
nodes: never[];
|
|
@@ -230,7 +248,7 @@ export declare class PlaywrightSession {
|
|
|
230
248
|
truncated: boolean;
|
|
231
249
|
dialogNote: string;
|
|
232
250
|
}>;
|
|
233
|
-
clickAt(x: number, y: number, signal
|
|
251
|
+
clickAt(x: number, y: number, signal?: AbortSignal): Promise<BrowserSnapshot | {
|
|
234
252
|
url: string;
|
|
235
253
|
title: string;
|
|
236
254
|
nodes: never[];
|
|
@@ -238,7 +256,7 @@ export declare class PlaywrightSession {
|
|
|
238
256
|
truncated: boolean;
|
|
239
257
|
dialogNote: string;
|
|
240
258
|
}>;
|
|
241
|
-
clickAtRef(ref: string, signal
|
|
259
|
+
clickAtRef(ref: string, signal?: AbortSignal): Promise<BrowserSnapshot | {
|
|
242
260
|
url: string;
|
|
243
261
|
title: string;
|
|
244
262
|
nodes: never[];
|
|
@@ -246,10 +264,10 @@ export declare class PlaywrightSession {
|
|
|
246
264
|
truncated: boolean;
|
|
247
265
|
dialogNote: string;
|
|
248
266
|
}>;
|
|
249
|
-
fill(ref: string, text: string, signal
|
|
267
|
+
fill(ref: string, text: string, signal?: AbortSignal): Promise<BrowserSnapshot>;
|
|
250
268
|
/** Read the field back and decide whether the fill landed. */
|
|
251
269
|
verifyFill(locator: Locator, tag: string, text: string): Promise<boolean>;
|
|
252
|
-
press(ref: string, key: string, signal
|
|
270
|
+
press(ref: string, key: string, signal?: AbortSignal): Promise<BrowserSnapshot | {
|
|
253
271
|
url: string;
|
|
254
272
|
title: string;
|
|
255
273
|
nodes: never[];
|
|
@@ -265,14 +283,14 @@ export declare class PlaywrightSession {
|
|
|
265
283
|
* moves is the only cheap proof that something appeared, so the returned
|
|
266
284
|
* snapshot carries a note comparing them.
|
|
267
285
|
*/
|
|
268
|
-
hover(ref: string, signal
|
|
286
|
+
hover(ref: string, signal?: AbortSignal): Promise<BrowserSnapshot>;
|
|
269
287
|
/**
|
|
270
288
|
* Hover a trigger the snapshot gave no ref, addressed by its visible label
|
|
271
289
|
* (e.g. a plain span that reveals a menu on mouseenter). The label must
|
|
272
290
|
* equal an element's own direct text; the deepest match wins, and the
|
|
273
291
|
* landing note names what was hovered plus how many elements shared it.
|
|
274
292
|
*/
|
|
275
|
-
hoverText(text: string, signal
|
|
293
|
+
hoverText(text: string, signal?: AbortSignal): Promise<BrowserSnapshot>;
|
|
276
294
|
/**
|
|
277
295
|
* Shared hover core: rest the pointer on a resolved locator, then report
|
|
278
296
|
* what the actionable ref count did. The label names the target in the
|
|
@@ -280,17 +298,17 @@ export declare class PlaywrightSession {
|
|
|
280
298
|
* target came from a ref).
|
|
281
299
|
*/
|
|
282
300
|
hoverLocator(locator: Locator, signal: AbortSignal | undefined, label: string, shared: number): Promise<BrowserSnapshot>;
|
|
283
|
-
scroll(direction: 'up' | 'down', amount: number, ref: string, signal
|
|
284
|
-
back(signal
|
|
285
|
-
forward(signal
|
|
286
|
-
wait(ms: number, signal
|
|
301
|
+
scroll(direction: 'up' | 'down', amount: number, ref: string | undefined, signal?: AbortSignal): Promise<BrowserSnapshot>;
|
|
302
|
+
back(signal?: AbortSignal): Promise<BrowserSnapshot>;
|
|
303
|
+
forward(signal?: AbortSignal): Promise<BrowserSnapshot>;
|
|
304
|
+
wait(ms: number, signal?: AbortSignal): Promise<void>;
|
|
287
305
|
tabs(): Promise<{
|
|
288
306
|
index: number;
|
|
289
307
|
url: string;
|
|
290
308
|
title: string;
|
|
291
309
|
active: boolean;
|
|
292
310
|
}[]>;
|
|
293
|
-
switchTab(index: number, signal
|
|
311
|
+
switchTab(index: number, signal?: AbortSignal): Promise<BrowserSnapshot | {
|
|
294
312
|
url: string;
|
|
295
313
|
title: string;
|
|
296
314
|
nodes: never[];
|
|
@@ -298,8 +316,8 @@ export declare class PlaywrightSession {
|
|
|
298
316
|
truncated: boolean;
|
|
299
317
|
dialogNote: string;
|
|
300
318
|
}>;
|
|
301
|
-
openTab(url: string, waitUntil: LoadState, signal
|
|
302
|
-
closeTab(index: number, signal
|
|
319
|
+
openTab(url: string, waitUntil: LoadState, signal?: AbortSignal): Promise<BrowserSnapshot>;
|
|
320
|
+
closeTab(index: number, signal?: AbortSignal): Promise<void>;
|
|
303
321
|
/**
|
|
304
322
|
* Screenshots get their own floor instead of reusing the navigation budget: a
|
|
305
323
|
* full-page capture is sized by the page and encoded by the CPU, so a loaded machine
|
|
@@ -311,12 +329,12 @@ export declare class PlaywrightSession {
|
|
|
311
329
|
screenshot(opts: {
|
|
312
330
|
fullPage?: boolean;
|
|
313
331
|
ref?: string;
|
|
314
|
-
} | undefined, signal
|
|
332
|
+
} | undefined, signal?: AbortSignal): Promise<{
|
|
315
333
|
mime: "image/png";
|
|
316
334
|
bytes: Buffer<ArrayBufferLike>;
|
|
317
335
|
}>;
|
|
318
336
|
pageData(): Promise<unknown>;
|
|
319
|
-
evaluate(expression: string, signal
|
|
337
|
+
evaluate(expression: string, signal?: AbortSignal): Promise<unknown>;
|
|
320
338
|
/**
|
|
321
339
|
* Send one raw CDP command on the current page's session and return its
|
|
322
340
|
* result. This is the "direct protocol" escape hatch behind every other
|
|
@@ -325,7 +343,7 @@ export declare class PlaywrightSession {
|
|
|
325
343
|
* are allowed) lives in the tool layer via lib/cdp-policy.js; this method
|
|
326
344
|
* itself performs no filtering so internal callers keep full control.
|
|
327
345
|
*/
|
|
328
|
-
cdp(method: string, params: Record<string, unknown>, signal
|
|
346
|
+
cdp(method: string, params: Record<string, unknown>, signal?: AbortSignal): Promise<unknown>;
|
|
329
347
|
close(): Promise<void>;
|
|
330
348
|
timeoutMs(): number;
|
|
331
349
|
ensurePage(): Promise<Page>;
|
package/lib/session.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { looksLikeToken, registrableDomain } from "./login-sites.js";
|
|
1
2
|
import { BrowserError } from "./errors.js";
|
|
2
3
|
import { asTimeoutError, isAbortError, isContextDestroyed, isCrashError } from "./page-diagnostics.js";
|
|
3
4
|
import { consoleMessages, networkRequests, pageData, trackPage } from "./page-diagnostics.js";
|
|
@@ -45,6 +46,69 @@ export class PlaywrightSession {
|
|
|
45
46
|
this.owner = owner;
|
|
46
47
|
this.context = context;
|
|
47
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* The session's cookies, reduced to what the login inference needs. `value` is dropped here,
|
|
51
|
+
* at the only place cookies enter the summary path, so nothing downstream can leak it.
|
|
52
|
+
*/
|
|
53
|
+
async cookieJar() {
|
|
54
|
+
const raw = await this.context.cookies();
|
|
55
|
+
return raw.map((cookie) => ({
|
|
56
|
+
name: cookie.name,
|
|
57
|
+
domain: cookie.domain,
|
|
58
|
+
...(typeof cookie.expires === 'number' ? { expires: cookie.expires } : {}),
|
|
59
|
+
...(cookie.httpOnly === true ? { httpOnly: true } : {}),
|
|
60
|
+
...(cookie.secure === true ? { secure: true } : {}),
|
|
61
|
+
// The value is inspected here and reduced to one boolean; it never leaves this function.
|
|
62
|
+
...(looksLikeToken(cookie.value) ? { tokenLike: true } : {}),
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Registrable domains of the pages this browser currently has open — the "first-party" set the
|
|
67
|
+
* login inference attributes cookies to. Taken from the live pages rather than the browser's
|
|
68
|
+
* history database: the plugin only claims what it can see, and never reads a new data source.
|
|
69
|
+
*/
|
|
70
|
+
/**
|
|
71
|
+
* Page titles by registrable domain for the pages currently open — the site's own name for
|
|
72
|
+
* itself, which is what browsers and password managers show instead of a bare hostname. Titles
|
|
73
|
+
* are display-only and trimmed; a domain with no open page simply has no title.
|
|
74
|
+
*/
|
|
75
|
+
async siteTitles() {
|
|
76
|
+
const titles = new Map();
|
|
77
|
+
for (const page of this.context.pages()) {
|
|
78
|
+
try {
|
|
79
|
+
const url = page.url();
|
|
80
|
+
if (!url.startsWith('http://') && !url.startsWith('https://'))
|
|
81
|
+
continue;
|
|
82
|
+
const domain = registrableDomain(new URL(url).host);
|
|
83
|
+
if (domain === '' || titles.has(domain))
|
|
84
|
+
continue;
|
|
85
|
+
const title = (await page.title()).replace(/\s+/g, ' ').trim();
|
|
86
|
+
if (title !== '')
|
|
87
|
+
titles.set(domain, title.slice(0, 80));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
/* a page can be mid-navigation or already closed */
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return titles;
|
|
94
|
+
}
|
|
95
|
+
visitedHosts() {
|
|
96
|
+
const hosts = new Set();
|
|
97
|
+
for (const page of this.context.pages()) {
|
|
98
|
+
try {
|
|
99
|
+
const url = page.url();
|
|
100
|
+
if (!url.startsWith('http://') && !url.startsWith('https://'))
|
|
101
|
+
continue;
|
|
102
|
+
const domain = registrableDomain(new URL(url).host);
|
|
103
|
+
if (domain !== '')
|
|
104
|
+
hosts.add(domain);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
/* a page can be mid-navigation or already closed */
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return [...hosts];
|
|
111
|
+
}
|
|
48
112
|
/** Run one operation under busy tracking: idle disposal defers until it settles. */
|
|
49
113
|
run(operation) {
|
|
50
114
|
this.provider.beginOp(this.owner);
|
|
@@ -223,7 +287,7 @@ export class PlaywrightSession {
|
|
|
223
287
|
}
|
|
224
288
|
/**
|
|
225
289
|
* Click an element the snapshot gave no ref, addressed by its visible label
|
|
226
|
-
* (e.g. a menu item revealed by
|
|
290
|
+
* (e.g. a menu item revealed by browser_click with mode "hover" that is plain text). The
|
|
227
291
|
* label must equal an element's own direct text; the deepest match wins,
|
|
228
292
|
* and the landing note names what was actually clicked plus how many
|
|
229
293
|
* elements shared the label.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 域名对照表(升级版):每个域名一条「说明 + 归属公司」,卡片按公司折叠成一行。
|
|
3
|
+
*
|
|
4
|
+
* 用户的原话:"这种一个类型的肯定要放一起啊" —— 京东系二十来个域各自占一行,小半屏都是它,
|
|
5
|
+
* 既看不出重点、也找不到别的站。所以这份表不只回答"这是什么",还回答"它属于谁"。
|
|
6
|
+
*
|
|
7
|
+
* 两条来源,按可靠性排序:
|
|
8
|
+
* 1. **网页自己的标题**(浏览器/密码管理器的做法):网站自己报的名字,不出网、不会过期;
|
|
9
|
+
* 由 provider 提供,只在那个域当前开着页面时才有。
|
|
10
|
+
* 2. **这份表**:离线、可测、覆盖常见的国内服务与大厂;**会被时代淘汰**,所以只在标题缺失时兜底,
|
|
11
|
+
* 而且**宁可留空也不瞎猜**(认不出的就显示域名本身,也没有归属)。
|
|
12
|
+
*
|
|
13
|
+
* 附带的好处:广告/统计类域名在表里被标出来,而且**整族归到"广告/统计平台"一组** —— 它们成片出现在
|
|
14
|
+
* "像登录态"里,本身就是判据被数据相似性骗了的信号。
|
|
15
|
+
* @module dsh-browser-playwright-codex/site-aliases
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* A short Chinese explanation of what a domain is, or undefined when we do not know.
|
|
19
|
+
* @param domain - registrable domain, e.g. `jinritemai.com`.
|
|
20
|
+
* @returns the explanation, or undefined (never a guess).
|
|
21
|
+
*/
|
|
22
|
+
export declare function siteAlias(domain: string): string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Which company a domain belongs to, for collapsing sibling domains into one row.
|
|
25
|
+
* @param domain - registrable domain.
|
|
26
|
+
* @returns the group key and its 中文 label, or undefined when the domain is not part of a group.
|
|
27
|
+
*/
|
|
28
|
+
export declare function siteGroup(domain: string): {
|
|
29
|
+
key: string;
|
|
30
|
+
label: string;
|
|
31
|
+
} | undefined;
|
|
32
|
+
/** How many domains this table explains — tests and docs use it to stay honest about coverage. */
|
|
33
|
+
export declare const ALIAS_COUNT: number;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 域名对照表(升级版):每个域名一条「说明 + 归属公司」,卡片按公司折叠成一行。
|
|
3
|
+
*
|
|
4
|
+
* 用户的原话:"这种一个类型的肯定要放一起啊" —— 京东系二十来个域各自占一行,小半屏都是它,
|
|
5
|
+
* 既看不出重点、也找不到别的站。所以这份表不只回答"这是什么",还回答"它属于谁"。
|
|
6
|
+
*
|
|
7
|
+
* 两条来源,按可靠性排序:
|
|
8
|
+
* 1. **网页自己的标题**(浏览器/密码管理器的做法):网站自己报的名字,不出网、不会过期;
|
|
9
|
+
* 由 provider 提供,只在那个域当前开着页面时才有。
|
|
10
|
+
* 2. **这份表**:离线、可测、覆盖常见的国内服务与大厂;**会被时代淘汰**,所以只在标题缺失时兜底,
|
|
11
|
+
* 而且**宁可留空也不瞎猜**(认不出的就显示域名本身,也没有归属)。
|
|
12
|
+
*
|
|
13
|
+
* 附带的好处:广告/统计类域名在表里被标出来,而且**整族归到"广告/统计平台"一组** —— 它们成片出现在
|
|
14
|
+
* "像登录态"里,本身就是判据被数据相似性骗了的信号。
|
|
15
|
+
* @module dsh-browser-playwright-codex/site-aliases
|
|
16
|
+
*/
|
|
17
|
+
/** Group key → 折叠行显示的中文名. */
|
|
18
|
+
const GROUP_LABELS = {
|
|
19
|
+
jd: '京东系',
|
|
20
|
+
alibaba: '阿里系',
|
|
21
|
+
bytedance: '字节系',
|
|
22
|
+
microsoft: '微软系',
|
|
23
|
+
meta: 'Meta 系',
|
|
24
|
+
google: 'Google 系',
|
|
25
|
+
ads: '广告 / 统计平台',
|
|
26
|
+
tencent: '腾讯系',
|
|
27
|
+
netease: '网易系',
|
|
28
|
+
};
|
|
29
|
+
/** Domain → what it is and who owns it. Keys are registrable domains. */
|
|
30
|
+
const SITES = {
|
|
31
|
+
// ── 京东系(真实名单里占了二十来个域,所以单独成组)──────────────────────────
|
|
32
|
+
'jd.com': { name: '京东', group: 'jd' },
|
|
33
|
+
'jingdong.com': { name: '京东(旧域名)', group: 'jd' },
|
|
34
|
+
'jd.hk': { name: '京东香港', group: 'jd' },
|
|
35
|
+
'jddj.com': { name: '京东到家', group: 'jd' },
|
|
36
|
+
'jdpay.com': { name: '京东支付', group: 'jd' },
|
|
37
|
+
'jcloud.com': { name: '京东云', group: 'jd' },
|
|
38
|
+
'jdcloud.com': { name: '京东云', group: 'jd' },
|
|
39
|
+
'jdbusiness.com': { name: '京东企业购', group: 'jd' },
|
|
40
|
+
'jdindustry.com': { name: '京东工业', group: 'jd' },
|
|
41
|
+
'jdl.cn': { name: '京东物流', group: 'jd' },
|
|
42
|
+
'jdl.com': { name: '京东物流', group: 'jd' },
|
|
43
|
+
'jdwl.com': { name: '京东物流', group: 'jd' },
|
|
44
|
+
'healthjd.com': { name: '京东健康', group: 'jd' },
|
|
45
|
+
'jingdonghealth.cn': { name: '京东健康', group: 'jd' },
|
|
46
|
+
'7fresh.com': { name: '七鲜(京东生鲜超市)', group: 'jd' },
|
|
47
|
+
'baitiao.com': { name: '京东白条', group: 'jd' },
|
|
48
|
+
'jingxi.com': { name: '京喜', group: 'jd' },
|
|
49
|
+
'five-star.cn': { name: '京东五星电器', group: 'jd' },
|
|
50
|
+
// ── 阿里系 ──────────────────────────────────────────────────────────────────
|
|
51
|
+
'taobao.com': { name: '淘宝', group: 'alibaba' },
|
|
52
|
+
'tmall.com': { name: '天猫', group: 'alibaba' },
|
|
53
|
+
'goofish.com': { name: '闲鱼(二手交易)', group: 'alibaba' },
|
|
54
|
+
'qianwen.com': { name: '通义千问(阿里的 AI 助手)', group: 'alibaba' },
|
|
55
|
+
'qianwenai.com': { name: '通义千问(阿里的 AI 助手)', group: 'alibaba' },
|
|
56
|
+
'a-mo.net': { name: '阿里妈妈(广告投放域名)', group: 'alibaba' },
|
|
57
|
+
'a-mx.com': { name: '阿里妈妈(广告投放域名)', group: 'alibaba' },
|
|
58
|
+
'yangkeduo.com': { name: '拼多多', group: 'alibaba' },
|
|
59
|
+
// ── 字节系 ──────────────────────────────────────────────────────────────────
|
|
60
|
+
'douyin.com': { name: '抖音', group: 'bytedance' },
|
|
61
|
+
'doubao.com': { name: '豆包(字节的 AI 助手)', group: 'bytedance' },
|
|
62
|
+
'toutiao.com': { name: '今日头条', group: 'bytedance' },
|
|
63
|
+
'volcengine.com': { name: '火山引擎(字节云服务)', group: 'bytedance' },
|
|
64
|
+
'jinritemai.com': { name: '巨量千川(抖音电商广告后台)', group: 'bytedance' },
|
|
65
|
+
'feishu.cn': { name: '飞书', group: 'bytedance' },
|
|
66
|
+
'larksuite.com': { name: '飞书(国际版)', group: 'bytedance' },
|
|
67
|
+
// ── 其他国内 ────────────────────────────────────────────────────────────────
|
|
68
|
+
'chaoxing.com': { name: '超星学习通(网课平台)' },
|
|
69
|
+
'juejin.cn': { name: '掘金(开发者社区)' },
|
|
70
|
+
'baidu.com': { name: '百度' },
|
|
71
|
+
'kuaishou.com': { name: '快手' },
|
|
72
|
+
'deepseek.com': { name: 'DeepSeek(深度求索)' },
|
|
73
|
+
// ── 微软系 ──────────────────────────────────────────────────────────────────
|
|
74
|
+
'live.com': { name: '微软账号(Outlook 等)', group: 'microsoft' },
|
|
75
|
+
'microsoftonline.com': { name: '微软企业账号登录', group: 'microsoft' },
|
|
76
|
+
'bing.com': { name: '必应(微软搜索)', group: 'microsoft' },
|
|
77
|
+
'clarity.ms': { name: 'Microsoft Clarity(行为统计)', group: 'microsoft' },
|
|
78
|
+
// ── Meta 系 ─────────────────────────────────────────────────────────────────
|
|
79
|
+
'meta.com': { name: 'Meta(Facebook 母公司)', group: 'meta' },
|
|
80
|
+
'facebook.com': { name: 'Facebook', group: 'meta' },
|
|
81
|
+
// ── Google 系 ───────────────────────────────────────────────────────────────
|
|
82
|
+
'doubleclick.net': { name: 'Google 广告', group: 'google' },
|
|
83
|
+
'google-analytics.com': { name: 'Google Analytics(统计)', group: 'google' },
|
|
84
|
+
'googletagmanager.com': { name: 'Google Tag Manager(统计)', group: 'google' },
|
|
85
|
+
// ── 其他国外服务 ────────────────────────────────────────────────────────────
|
|
86
|
+
'github.com': { name: 'GitHub(代码托管)' },
|
|
87
|
+
'openrouter.ai': { name: 'OpenRouter(AI 模型聚合服务)' },
|
|
88
|
+
'fearlessrevolution.com': { name: 'FearlessRevolution(游戏修改器社区)' },
|
|
89
|
+
// ── 广告 / 统计平台(成片出现时就是"判据被骗了"的信号)────────────────────────
|
|
90
|
+
'adroll.com': { name: 'AdRoll(广告再营销)', group: 'ads' },
|
|
91
|
+
'pubmatic.com': { name: 'PubMatic(广告交易平台)', group: 'ads' },
|
|
92
|
+
'criteo.com': { name: 'Criteo(广告再营销)', group: 'ads' },
|
|
93
|
+
'adnxs.com': { name: 'Xandr/AppNexus(广告交易平台)', group: 'ads' },
|
|
94
|
+
'rubiconproject.com': { name: 'Magnite(广告交易平台)', group: 'ads' },
|
|
95
|
+
'casalemedia.com': { name: 'Index Exchange(广告交易平台)', group: 'ads' },
|
|
96
|
+
'scorecardresearch.com': { name: 'Comscore(受众统计)', group: 'ads' },
|
|
97
|
+
'adsrvr.org': { name: 'The Trade Desk(广告投放)', group: 'ads' },
|
|
98
|
+
'taboola.com': { name: 'Taboola(内容推荐广告)', group: 'ads' },
|
|
99
|
+
'outbrain.com': { name: 'Outbrain(内容推荐广告)', group: 'ads' },
|
|
100
|
+
'mmstat.com': { name: '阿里统计', group: 'ads' },
|
|
101
|
+
'umeng.com': { name: '友盟(移动统计)', group: 'ads' },
|
|
102
|
+
'umengcloud.com': { name: '友盟(移动统计)', group: 'ads' },
|
|
103
|
+
'cnzz.com': { name: 'CNZZ 站长统计', group: 'ads' },
|
|
104
|
+
'hotjar.com': { name: 'Hotjar(行为统计)', group: 'ads' },
|
|
105
|
+
'sentry.io': { name: 'Sentry(错误监控)', group: 'ads' },
|
|
106
|
+
sajssdk: { name: '神策数据(统计)', group: 'ads' },
|
|
107
|
+
sensorsdata: { name: '神策数据(统计)', group: 'ads' },
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* A short Chinese explanation of what a domain is, or undefined when we do not know.
|
|
111
|
+
* @param domain - registrable domain, e.g. `jinritemai.com`.
|
|
112
|
+
* @returns the explanation, or undefined (never a guess).
|
|
113
|
+
*/
|
|
114
|
+
export function siteAlias(domain) {
|
|
115
|
+
return SITES[normalize(domain)]?.name;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Which company a domain belongs to, for collapsing sibling domains into one row.
|
|
119
|
+
* @param domain - registrable domain.
|
|
120
|
+
* @returns the group key and its 中文 label, or undefined when the domain is not part of a group.
|
|
121
|
+
*/
|
|
122
|
+
export function siteGroup(domain) {
|
|
123
|
+
const key = SITES[normalize(domain)]?.group;
|
|
124
|
+
if (key === undefined)
|
|
125
|
+
return undefined;
|
|
126
|
+
return { key, label: GROUP_LABELS[key] ?? key };
|
|
127
|
+
}
|
|
128
|
+
/** How many domains this table explains — tests and docs use it to stay honest about coverage. */
|
|
129
|
+
export const ALIAS_COUNT = Object.keys(SITES).length;
|
|
130
|
+
function normalize(domain) {
|
|
131
|
+
return domain.trim().replace(/^\./, '').toLowerCase();
|
|
132
|
+
}
|