dsh-browser-playwright-codex 0.6.1 → 0.6.6
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 +57 -0
- package/NOTICE.md +5 -6
- package/README.md +130 -38
- package/SECURITY.md +18 -11
- package/lib/challenge-signatures.d.ts +114 -0
- package/lib/challenge-signatures.js +157 -0
- package/lib/challenge.d.ts +68 -8
- package/lib/challenge.js +287 -26
- package/lib/injected.js +13 -0
- package/lib/page-actions.d.ts +27 -56
- package/lib/page-actions.js +50 -18
- package/lib/page-diagnostics.d.ts +5 -0
- package/lib/page-diagnostics.js +60 -6
- package/lib/page-probes.d.ts +12 -0
- package/lib/page-probes.js +33 -0
- package/lib/page-snapshot.d.ts +14 -8
- package/lib/page-snapshot.js +50 -3
- package/lib/session.d.ts +21 -82
- package/lib/session.js +15 -3
- package/lib/snapshot-aria.js +16 -0
- package/lib/tool-render.js +58 -3
- package/lib/tool-types.d.ts +16 -0
- package/lib/types.d.ts +23 -0
- package/package.json +8 -7
- package/scripts/dsh-browser-verify.mjs +24 -12
- package/scripts/analysis/hl-compare.mjs +0 -149
- package/scripts/analysis/live-handoff-probe.mjs +0 -97
- package/scripts/analysis/needs-human-probe.mjs +0 -83
- package/scripts/analysis/repro-minimize.mjs +0 -112
- package/scripts/autocommit.ps1 +0 -23
- package/scripts/check-doc-links.py +0 -112
- package/scripts/measure-prompt-cost.mjs +0 -57
- package/scripts/preflight.mjs +0 -192
- package/scripts/prompt-cost.mjs +0 -73
- package/scripts/shipped-gates.mjs +0 -43
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 挑战页签名表:把「这页是不是拦截页」的判据放在厂商自有信号上,而不是页面文案上。
|
|
3
|
+
*
|
|
4
|
+
* 为什么分层,以及为什么是这个顺序(稳定性递减):
|
|
5
|
+
*
|
|
6
|
+
* 1. 响应头是厂商对外的 API。Cloudflare 专门发文教用户读 `cf-mitigated: challenge`
|
|
7
|
+
* 来识别挑战页,AWS WAF 同理(`x-amzn-waf-action: challenge`)。厂商改一个字的代价
|
|
8
|
+
* 是打断所有客户的集成,不是打断我们一个插件。
|
|
9
|
+
* 2. 厂商控件的无障碍名是次稳的:reCAPTCHA / hCaptcha / Turnstile 的 iframe 标题就是
|
|
10
|
+
* 他们自己的产品名,出现在每一张用了他们的页面上。
|
|
11
|
+
* 3. 文案最脆。FlareSolverr 就是死在依赖页面 title 和元素 class 上(上游 issue #1734:
|
|
12
|
+
* 「元素的 class 和 id 被改过,所以现有的检测规则失效」)。
|
|
13
|
+
*
|
|
14
|
+
* 因此新增一家厂商 = 在这张表里加一条签名 + 在 tests/challenge.test.ts 加一条用例,
|
|
15
|
+
* 判定逻辑本身不用改。
|
|
16
|
+
* @module dsh-browser-playwright-codex/challenge-signatures
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* How a signature was proven. `response-header` is the vendor's own API,
|
|
20
|
+
* `widget` is the vendor's own widget, `wording` is the fallback we trust least.
|
|
21
|
+
*/
|
|
22
|
+
export type ChallengeTier = 'response-header' | 'widget' | 'wording';
|
|
23
|
+
/**
|
|
24
|
+
* One response-header rule. Matching is case-insensitive; a rule with neither
|
|
25
|
+
* `equals` nor `contains` fires on the header's presence alone.
|
|
26
|
+
*/
|
|
27
|
+
export interface HeaderRule {
|
|
28
|
+
/** Exact header name. Ignored when `namePrefix` is set. */
|
|
29
|
+
readonly name: string;
|
|
30
|
+
/** Exact value the header must carry. */
|
|
31
|
+
readonly equals?: string;
|
|
32
|
+
/** Substring the value must contain. */
|
|
33
|
+
readonly contains?: string;
|
|
34
|
+
}
|
|
35
|
+
/** One vendor's interception signature. */
|
|
36
|
+
export interface ChallengeSignature {
|
|
37
|
+
/** Stable id, used in tests and diagnostics. */
|
|
38
|
+
readonly id: string;
|
|
39
|
+
/** Engine label shown to the model. */
|
|
40
|
+
readonly engine: string;
|
|
41
|
+
/** One-clause explanation of what is happening. */
|
|
42
|
+
readonly hint: string;
|
|
43
|
+
/**
|
|
44
|
+
* Header rules proven against the MAIN document response. These are the
|
|
45
|
+
* vendor's published detection contract, so they are checked first.
|
|
46
|
+
*/
|
|
47
|
+
readonly headers?: readonly HeaderRule[];
|
|
48
|
+
/** Case-insensitive header-name prefixes (e.g. DataDome's `x-datadome*`). */
|
|
49
|
+
readonly headerPrefixes?: readonly string[];
|
|
50
|
+
/**
|
|
51
|
+
* Status codes this signature must be corroborated by. Needed for vendors whose
|
|
52
|
+
* header is *not* challenge-specific: DataDome adds `x-datadome*` to every
|
|
53
|
+
* response (their docs say so, and they return `NA` when detection is off), and
|
|
54
|
+
* Imperva adds `x-iinfo` to everything it fronts. Measured 2026-10-17: a normal
|
|
55
|
+
* DataDome-fronted page answered 200 with `x-datadome: protected`, which an
|
|
56
|
+
* ungated rule would have reported as a wall.
|
|
57
|
+
*/
|
|
58
|
+
readonly statuses?: readonly number[];
|
|
59
|
+
/**
|
|
60
|
+
* Accessible-name tokens only this vendor's widget carries. On a non-iframe
|
|
61
|
+
* node a token must ALSO be corroborated by {@link WIDGET_CORROBORATION}.
|
|
62
|
+
*/
|
|
63
|
+
readonly widgetTokens?: readonly RegExp[];
|
|
64
|
+
/**
|
|
65
|
+
* Wording this vendor puts in the TITLE of its own interstitial. Vendor
|
|
66
|
+
* wording is stronger than generic wording, so it is checked first and keeps
|
|
67
|
+
* the vendor's name on the result.
|
|
68
|
+
*/
|
|
69
|
+
readonly titleWording?: readonly RegExp[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Wording that corroborates a widget token found on a non-iframe node.
|
|
73
|
+
*
|
|
74
|
+
* Shared by every vendor on purpose, and it contains no vendor token: three of
|
|
75
|
+
* the tokens below ("reCAPTCHA", "FunCaptcha", "px-captcha") literally contain
|
|
76
|
+
* the word "captcha", so a list that accepted "captcha" would let the token
|
|
77
|
+
* corroborate itself and the guard would be vacuous.
|
|
78
|
+
*/
|
|
79
|
+
export declare const WIDGET_CORROBORATION: readonly RegExp[];
|
|
80
|
+
/**
|
|
81
|
+
* Every vendor we can prove from machine signals. Adding a vendor here is the
|
|
82
|
+
* whole change: the detector walks the table, it does not know any of these names.
|
|
83
|
+
*/
|
|
84
|
+
export declare const CHALLENGE_SIGNATURES: readonly ChallengeSignature[];
|
|
85
|
+
/**
|
|
86
|
+
* Wording a challenge page puts in its TITLE, with no vendor to attribute it to.
|
|
87
|
+
* A title is short and authored by the interceptor, so a match here is safe on
|
|
88
|
+
* its own. Kept combinatorial ("verification" + "human/safety/…") rather than
|
|
89
|
+
* one phrase per language, so a reworded page still matches without a new
|
|
90
|
+
* signature.
|
|
91
|
+
*/
|
|
92
|
+
export declare const WORDING_TITLE: readonly RegExp[];
|
|
93
|
+
/**
|
|
94
|
+
* Wording a challenge page puts on a CONTROL (a button, a link, an input).
|
|
95
|
+
* Command forms only: a help page that explains captchas says "验证码说明",
|
|
96
|
+
* a challenge says "请输入验证码". The bare word for captcha is deliberately NOT
|
|
97
|
+
* here — it is gated on a control role in {@link CAPTCHA_ROLES} instead, so a
|
|
98
|
+
* link to the documentation cannot fire this tier.
|
|
99
|
+
*/
|
|
100
|
+
export declare const WORDING_COMMAND: readonly RegExp[];
|
|
101
|
+
/**
|
|
102
|
+
* Controls that mention verification but are NOT a bot wall: sending yourself a
|
|
103
|
+
* code is an ordinary sign-in step, and labelling it an anti-bot challenge
|
|
104
|
+
* would send the user looking for a wall that is not there.
|
|
105
|
+
*/
|
|
106
|
+
export declare const WORDING_EXCLUDE: readonly RegExp[];
|
|
107
|
+
/** The word for "captcha" itself; only counts on a {@link CAPTCHA_ROLES} node. */
|
|
108
|
+
export declare const CAPTCHA_WORD: RegExp;
|
|
109
|
+
/**
|
|
110
|
+
* Roles that make {@link CAPTCHA_WORD} count. A captcha input, image or button
|
|
111
|
+
* is a challenge; a link or heading named "验证码说明" is documentation, and
|
|
112
|
+
* firing on it would send the user chasing a wall that is not there.
|
|
113
|
+
*/
|
|
114
|
+
export declare const CAPTCHA_ROLES: ReadonlySet<string>;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 挑战页签名表:把「这页是不是拦截页」的判据放在厂商自有信号上,而不是页面文案上。
|
|
3
|
+
*
|
|
4
|
+
* 为什么分层,以及为什么是这个顺序(稳定性递减):
|
|
5
|
+
*
|
|
6
|
+
* 1. 响应头是厂商对外的 API。Cloudflare 专门发文教用户读 `cf-mitigated: challenge`
|
|
7
|
+
* 来识别挑战页,AWS WAF 同理(`x-amzn-waf-action: challenge`)。厂商改一个字的代价
|
|
8
|
+
* 是打断所有客户的集成,不是打断我们一个插件。
|
|
9
|
+
* 2. 厂商控件的无障碍名是次稳的:reCAPTCHA / hCaptcha / Turnstile 的 iframe 标题就是
|
|
10
|
+
* 他们自己的产品名,出现在每一张用了他们的页面上。
|
|
11
|
+
* 3. 文案最脆。FlareSolverr 就是死在依赖页面 title 和元素 class 上(上游 issue #1734:
|
|
12
|
+
* 「元素的 class 和 id 被改过,所以现有的检测规则失效」)。
|
|
13
|
+
*
|
|
14
|
+
* 因此新增一家厂商 = 在这张表里加一条签名 + 在 tests/challenge.test.ts 加一条用例,
|
|
15
|
+
* 判定逻辑本身不用改。
|
|
16
|
+
* @module dsh-browser-playwright-codex/challenge-signatures
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Wording that corroborates a widget token found on a non-iframe node.
|
|
20
|
+
*
|
|
21
|
+
* Shared by every vendor on purpose, and it contains no vendor token: three of
|
|
22
|
+
* the tokens below ("reCAPTCHA", "FunCaptcha", "px-captcha") literally contain
|
|
23
|
+
* the word "captcha", so a list that accepted "captcha" would let the token
|
|
24
|
+
* corroborate itself and the guard would be vacuous.
|
|
25
|
+
*/
|
|
26
|
+
export const WIDGET_CORROBORATION = Object.freeze([
|
|
27
|
+
/verify|challenge|robot|human|verification/i,
|
|
28
|
+
/验证|校验|人机|真人/,
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* Every vendor we can prove from machine signals. Adding a vendor here is the
|
|
32
|
+
* whole change: the detector walks the table, it does not know any of these names.
|
|
33
|
+
*/
|
|
34
|
+
export const CHALLENGE_SIGNATURES = Object.freeze([
|
|
35
|
+
{
|
|
36
|
+
id: 'cloudflare-challenge',
|
|
37
|
+
engine: 'Cloudflare',
|
|
38
|
+
hint: 'Cloudflare answered with a challenge page instead of the page itself',
|
|
39
|
+
headers: [{ name: 'cf-mitigated', equals: 'challenge' }],
|
|
40
|
+
// The interstitial's own title, in both languages. Only reached when the
|
|
41
|
+
// header is missing (a cached page, a prefetch, a client-side fetch).
|
|
42
|
+
titleWording: [
|
|
43
|
+
/just a moment|attention required|checking (your )?browser|enable javascript and cookies|cf-chl-/i,
|
|
44
|
+
/请稍候|验证你的浏览器|正在进行安全验证/,
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'aws-waf',
|
|
49
|
+
engine: 'AWS WAF',
|
|
50
|
+
hint: 'AWS WAF answered with a challenge instead of the page itself',
|
|
51
|
+
headers: [{ name: 'x-amzn-waf-action', equals: 'challenge' }],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'imperva-incapsula',
|
|
55
|
+
engine: 'Imperva',
|
|
56
|
+
hint: 'Imperva (Incapsula) blocked this request from its edge',
|
|
57
|
+
// Presence only: X-IInfo is an opaque per-response trace that Imperva adds to
|
|
58
|
+
// everything it fronts, so the header alone says nothing about a block — the
|
|
59
|
+
// status code has to agree.
|
|
60
|
+
headers: [{ name: 'x-iinfo' }],
|
|
61
|
+
statuses: [403, 429, 503],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'datadome',
|
|
65
|
+
engine: 'DataDome',
|
|
66
|
+
hint: 'DataDome blocked this request',
|
|
67
|
+
headerPrefixes: ['x-datadome'],
|
|
68
|
+
statuses: [403, 429, 503],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'recaptcha',
|
|
72
|
+
engine: 'captcha',
|
|
73
|
+
hint: 'the page embedded a reCAPTCHA widget, which automation must not attempt to solve',
|
|
74
|
+
widgetTokens: [/recaptcha/i],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'hcaptcha',
|
|
78
|
+
engine: 'captcha',
|
|
79
|
+
hint: 'the page embedded an hCaptcha widget, which automation must not attempt to solve',
|
|
80
|
+
widgetTokens: [/hcaptcha/i],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'turnstile',
|
|
84
|
+
engine: 'Cloudflare',
|
|
85
|
+
hint: 'Cloudflare Turnstile is asking the visitor to prove they are human',
|
|
86
|
+
widgetTokens: [/turnstile/i, /widget containing a cloudflare security challenge/i],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'arkose-funcaptcha',
|
|
90
|
+
engine: 'captcha',
|
|
91
|
+
hint: 'the page embedded an Arkose / FunCaptcha challenge, which automation must not attempt to solve',
|
|
92
|
+
widgetTokens: [/arkoselabs/i, /funcaptcha/i, /arkose/i],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'perimeterx-human',
|
|
96
|
+
engine: 'PerimeterX',
|
|
97
|
+
hint: 'PerimeterX is asking the visitor to press and hold, which automation must not attempt',
|
|
98
|
+
widgetTokens: [/px-captcha/i, /perimeterx/i, /press (and|&) hold/i],
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
/**
|
|
102
|
+
* Wording a challenge page puts in its TITLE, with no vendor to attribute it to.
|
|
103
|
+
* A title is short and authored by the interceptor, so a match here is safe on
|
|
104
|
+
* its own. Kept combinatorial ("verification" + "human/safety/…") rather than
|
|
105
|
+
* one phrase per language, so a reworded page still matches without a new
|
|
106
|
+
* signature.
|
|
107
|
+
*/
|
|
108
|
+
export const WORDING_TITLE = Object.freeze([
|
|
109
|
+
/verify (you are|you're|that you are) (a )?human|are you a robot/i,
|
|
110
|
+
/人机验证|验证您(是|为)人类|真人验证|安全验证|身份验证|访客验证/,
|
|
111
|
+
/(滑块|拼图)(验证|校验)/,
|
|
112
|
+
]);
|
|
113
|
+
/**
|
|
114
|
+
* Wording a challenge page puts on a CONTROL (a button, a link, an input).
|
|
115
|
+
* Command forms only: a help page that explains captchas says "验证码说明",
|
|
116
|
+
* a challenge says "请输入验证码". The bare word for captcha is deliberately NOT
|
|
117
|
+
* here — it is gated on a control role in {@link CAPTCHA_ROLES} instead, so a
|
|
118
|
+
* link to the documentation cannot fire this tier.
|
|
119
|
+
*/
|
|
120
|
+
export const WORDING_COMMAND = Object.freeze([
|
|
121
|
+
/(请|点击|输入|拖动|按住|滑动|完成|进行|通过)[^。!?]{0,8}(验证|校验|拼图|滑块)/,
|
|
122
|
+
/(验证|校验)(您|你)(是|为)/,
|
|
123
|
+
/(是|为)(真人|人类)(吗|?|\?)?$/,
|
|
124
|
+
/(拖动|按住|滑动)[^。!?]{0,8}(滑块|拼图)/,
|
|
125
|
+
/(verify|click|press|complete|solve|check|confirm)[^.!?]{0,24}(human|robot|browser|captcha|challenge|you are)/i,
|
|
126
|
+
/i'?m not a robot|not a robot/i,
|
|
127
|
+
]);
|
|
128
|
+
/**
|
|
129
|
+
* Controls that mention verification but are NOT a bot wall: sending yourself a
|
|
130
|
+
* code is an ordinary sign-in step, and labelling it an anti-bot challenge
|
|
131
|
+
* would send the user looking for a wall that is not there.
|
|
132
|
+
*/
|
|
133
|
+
export const WORDING_EXCLUDE = Object.freeze([
|
|
134
|
+
/(获取|发送|重新获取|重新发送|接收)[^。!?]{0,4}(验证码|校验码|短信码)/,
|
|
135
|
+
/send (the )?(code|sms)|resend/i,
|
|
136
|
+
]);
|
|
137
|
+
/** The word for "captcha" itself; only counts on a {@link CAPTCHA_ROLES} node. */
|
|
138
|
+
export const CAPTCHA_WORD = /captcha|验证码/i;
|
|
139
|
+
/**
|
|
140
|
+
* Roles that make {@link CAPTCHA_WORD} count. A captcha input, image or button
|
|
141
|
+
* is a challenge; a link or heading named "验证码说明" is documentation, and
|
|
142
|
+
* firing on it would send the user chasing a wall that is not there.
|
|
143
|
+
*/
|
|
144
|
+
export const CAPTCHA_ROLES = new Set([
|
|
145
|
+
'textbox',
|
|
146
|
+
'searchbox',
|
|
147
|
+
'combobox',
|
|
148
|
+
'spinbutton',
|
|
149
|
+
'button',
|
|
150
|
+
'checkbox',
|
|
151
|
+
'radio',
|
|
152
|
+
'slider',
|
|
153
|
+
'img',
|
|
154
|
+
'image',
|
|
155
|
+
'iframe',
|
|
156
|
+
'figure',
|
|
157
|
+
]);
|
package/lib/challenge.d.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 挑战页识别与「无进展」兜底。
|
|
3
|
+
*
|
|
4
|
+
* 判定顺序 = 稳定性顺序:先读厂商自己的响应头(他们的公开 API),再看厂商控件的无障碍名,
|
|
5
|
+
* 最后才看文案。三层都不认识的时候不猜厂商,改问一个与厂商无关的问题——「这个动作之后
|
|
6
|
+
* 页面变了吗」。所以新出的验证方式没有签名也能被接住,代价只是多问用户一句。
|
|
3
7
|
* @module dsh-browser-playwright-codex/challenge
|
|
4
8
|
*/
|
|
5
|
-
import type { ChallengeMatch, ChallengeSnapshot } from './tool-types.ts';
|
|
9
|
+
import type { ChallengeMatch, ChallengeNode, ChallengeSnapshot } from './tool-types.ts';
|
|
6
10
|
/**
|
|
7
|
-
* Detect anti-bot / human-verification
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Detect an anti-bot / human-verification surface from the MAIN document
|
|
12
|
+
* response metadata, the page's accessible names and, as a last resort, its
|
|
13
|
+
* wording. We never try to solve one: the correct move for an agent is to hand
|
|
14
|
+
* it to the human who can see the open window.
|
|
15
|
+
* @param snapshot - title, url, accessible tree and (when available) the main document response.
|
|
16
|
+
* @returns the matched engine with its tier and the concrete evidence, else null.
|
|
13
17
|
*/
|
|
14
18
|
export declare function detectChallenge(snapshot: ChallengeSnapshot): ChallengeMatch | null;
|
|
15
19
|
/**
|
|
@@ -18,3 +22,59 @@ export declare function detectChallenge(snapshot: ChallengeSnapshot): ChallengeM
|
|
|
18
22
|
* should pause and ask the user to complete it by hand, then re-snapshot.
|
|
19
23
|
*/
|
|
20
24
|
export declare const CHALLENGE_NOTE = "[human verification] The page triggered an anti-bot challenge; it is NOT solvable by automation and must NOT be retried blindly. The browser window is open on the user's desktop \u2014 tell the user a verification appeared, wait for them to finish it (a few seconds), then call browser_snapshot again. The clearance cookie is saved to the profile automatically and keeps working for the session.";
|
|
25
|
+
/**
|
|
26
|
+
* Appended after an action that left the page byte-for-byte unchanged on a page
|
|
27
|
+
* with almost nothing else to interact with. Worded as a guess, because that is
|
|
28
|
+
* what it is: it is the shape an unrecognised wall has, and the whole point of
|
|
29
|
+
* this tier is to cover the vendors we have no signature for.
|
|
30
|
+
*/
|
|
31
|
+
export declare const NO_PROGRESS_NOTE = "[no visible change] that action left the page identical \u2014 same URL, same title, same elements \u2014 and the page offers almost nothing else to click. This is a guess, not a diagnosis: it is what an unrecognised anti-bot wall looks like. Take one browser_snapshot to confirm; if the page still looks unchanged, stop and tell the user what you were trying to do and ask them to look at the open window, instead of retrying the same action.";
|
|
32
|
+
/** A page with at most this many actionable elements counts as "nearly empty". */
|
|
33
|
+
export declare const NO_PROGRESS_MAX_REFS = 3;
|
|
34
|
+
/**
|
|
35
|
+
* Identity of one page state: URL, title, actionable-element count and the
|
|
36
|
+
* accessible labels. Two captures with the same fingerprint look the same to a
|
|
37
|
+
* human glancing at the window, which is exactly the question this tier asks.
|
|
38
|
+
* @param input - the slice of a snapshot the backstop compares.
|
|
39
|
+
* @returns an opaque fingerprint string.
|
|
40
|
+
*/
|
|
41
|
+
export declare function snapshotFingerprint(input: {
|
|
42
|
+
readonly url?: string | undefined;
|
|
43
|
+
readonly title?: string | undefined;
|
|
44
|
+
readonly refs?: number | undefined;
|
|
45
|
+
readonly nodes?: readonly ChallengeNode[] | undefined;
|
|
46
|
+
}): string;
|
|
47
|
+
/**
|
|
48
|
+
* The no-progress backstop: a vendor we have never seen leaves no signature, so
|
|
49
|
+
* we stop asking "which wall is this" and ask "did anything happen". Deliberately
|
|
50
|
+
* conservative — it needs an identical page AND (a nearly empty page, or a second
|
|
51
|
+
* identical result), so an ordinary click that is not meant to change anything
|
|
52
|
+
* stays quiet.
|
|
53
|
+
* @param input - whether an action just ran, the identical-capture streak, and the ref count.
|
|
54
|
+
* @returns the advisory note, or null when the page is still plausibly alive.
|
|
55
|
+
*/
|
|
56
|
+
export declare function detectNoProgress(input: {
|
|
57
|
+
readonly afterAction: boolean;
|
|
58
|
+
readonly unchangedStreak: number;
|
|
59
|
+
readonly refs: number;
|
|
60
|
+
}): string | null;
|
|
61
|
+
/**
|
|
62
|
+
* Appended when a page has nothing to click and almost nothing to read. This is
|
|
63
|
+
* the blind spot the no-progress backstop cannot cover: that tier needs an action
|
|
64
|
+
* to compare before and after, and on a page with zero actionable elements the
|
|
65
|
+
* agent has no action to take — so the emptiest, most wall-like pages used to
|
|
66
|
+
* produce no message at all. Measured 2026-10-17 on 微博's visitor gate: the model
|
|
67
|
+
* received 364 characters, all of them "URL / Title / Refs: 0 / (no visible
|
|
68
|
+
* elements)", and nothing said the page was a gate rather than an empty document.
|
|
69
|
+
*/
|
|
70
|
+
export declare const SILENT_PAGE_NOTE = "[nothing here] this page has no clickable elements and almost no text: it may be a gate or visitor check that resolves by itself, a page that is still loading, or a page that needs a person. Wait a moment and take one more browser_snapshot; if it still looks like this, tell the user what you were trying to open and ask them to look at the open window, instead of re-sending the same navigation.";
|
|
71
|
+
/**
|
|
72
|
+
* Whether the page is a silent one: nothing to click and (almost) nothing to read.
|
|
73
|
+
* @param input - the action ref count and the accessible tree.
|
|
74
|
+
* @returns the advisory note, or null when the page has content worth reading.
|
|
75
|
+
*/
|
|
76
|
+
export declare function detectSilentPage(input: {
|
|
77
|
+
readonly refs: number;
|
|
78
|
+
readonly nodes?: readonly ChallengeNode[] | undefined;
|
|
79
|
+
readonly title?: string | undefined;
|
|
80
|
+
}): string | null;
|