Skip to content

Commit f3a5ce8

Browse files
authored
Merge pull request #2721 from alectimison-maker/fix/captcha-additive-matcher
fix(captcha): make English matcher additive
2 parents 6510822 + 18c2d65 commit f3a5ce8

9 files changed

Lines changed: 283 additions & 131 deletions

File tree

src/chrome/src/agent/agent.js

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ import { normalizeRuntimeTraceConfig } from '../trace/runtime-config.js';
5252
import { tracesToMarkdown } from './trace-export.js';
5353
import { solveCaptcha, detectCaptcha, injectToken, captchaParamError, captchaTypesMatch, captchaWebsiteUrl } from './captcha-solver.js';
5454
import { isCapsolverEnabled, normalizeCapsolverApiKey } from './capsolver-config.js';
55-
import { captchaChallengeKey, detectChallengeDialog, detectChallengeDialogInPage } from './captcha-gate.js';
55+
import { captchaChallengeKey, captchaChallengeMatcherOptions, detectChallengeDialog, detectChallengeDialogInPage } from './captcha-gate.js';
5656
import { applyCaptchaFrameVisibility } from './captcha-frame-runtime.js';
5757
import { getRecordingStateFresh as recorderStateFresh } from '../recorder/host.js';
5858
import { Capability, CAPABILITY_LABEL, capabilitiesFor, requiredHosts, frameHostMatches, isNetworkMutation, normalizeHost, PermissionManager, UNTRUSTED_CONTENT_TOOLS } from './permission-gate.js';
@@ -4205,7 +4205,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
42054205
func: detectChallengeDialogInPage,
42064206
args: [{
42074207
includeFrameContext: true,
4208-
allowGenericFailure: options?.allowGenericFailure === true,
4208+
...captchaChallengeMatcherOptions(),
42094209
}],
42104210
});
42114211
const inspected = (results) => {
@@ -4218,7 +4218,6 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
42184218
if (!label) return [];
42194219
const normalized = detectChallengeDialog(
42204220
`dialog ${JSON.stringify(label.slice(0, 200))}`,
4221-
{ allowGenericFailure: options?.allowGenericFailure === true },
42224221
);
42234222
if (!normalized?.normalizedLabel) return [];
42244223
return [{
@@ -4323,13 +4322,10 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
43234322
? toolResult.captchaChallengeFrameId
43244323
: null;
43254324
let observedChallengeFrameUrl = String(toolResult.captchaChallengeFrameUrl || '');
4326-
let challenge = detectChallengeDialog(toolResult.pageContent, {
4327-
allowGenericFailure: !!activeGate,
4328-
});
4325+
let challenge = detectChallengeDialog(toolResult.pageContent);
43294326
if (!challenge && toolResult.pageGate?.surface === 'dialog' && toolResult.pageGate?.label) {
43304327
challenge = detectChallengeDialog(
43314328
`dialog ${JSON.stringify(String(toolResult.pageGate.label).slice(0, 200))}`,
4332-
{ allowGenericFailure: !!activeGate },
43334329
);
43344330
}
43354331
if (
@@ -4340,12 +4336,10 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
43404336
const recheck = await this._detectChallengeDialogBeforeMutation(tabId, {
43414337
includeStatus: true,
43424338
expectedFrameId: observedChallengeFrameId,
4343-
allowGenericFailure: !!activeGate,
43444339
});
43454340
if (recheck.challenge?.label) {
43464341
challenge = detectChallengeDialog(
43474342
`dialog ${JSON.stringify(String(recheck.challenge.label).slice(0, 200))}`,
4348-
{ allowGenericFailure: !!activeGate },
43494343
);
43504344
observedChallengeFrameId = Number.isInteger(recheck.challenge.frameId)
43514345
? recheck.challenge.frameId
@@ -4412,20 +4406,67 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
44124406
}
44134407
}
44144408
}
4409+
const requestedPage = toolArgs?.page;
4410+
const requestedMaxDepth = toolArgs?.maxDepth;
4411+
const parsedMaxDepth = Number(requestedMaxDepth);
4412+
const authoritativeRootRead = !toolArgs?.ref_id
4413+
&& (
4414+
requestedPage === undefined
4415+
|| requestedPage === null
4416+
|| requestedPage === ''
4417+
|| Number(requestedPage) === 1
4418+
)
4419+
&& treeFilter !== 'interactive'
4420+
&& (
4421+
requestedMaxDepth === undefined
4422+
|| requestedMaxDepth === null
4423+
|| requestedMaxDepth === ''
4424+
|| (Number.isFinite(parsedMaxDepth) && parsedMaxDepth >= 15)
4425+
)
4426+
&& toolResult.truncated !== true
4427+
&& toolResult.hasMore !== true
4428+
&& toolResult.autoDegraded !== true;
4429+
if (
4430+
!challenge
4431+
&& activeGate
4432+
&& authoritativeRootRead
4433+
&& Number.isInteger(activeGate.challengeFrameId)
4434+
) {
4435+
const frameInspection = await this._detectChallengeDialogBeforeMutation(tabId, {
4436+
includeStatus: true,
4437+
expectedFrameId: activeGate.challengeFrameId,
4438+
});
4439+
if (frameInspection.challenge?.label) {
4440+
challenge = detectChallengeDialog(
4441+
`dialog ${JSON.stringify(String(frameInspection.challenge.label).slice(0, 200))}`
4442+
);
4443+
} else if (!frameInspection.inspectionComplete) {
4444+
const guardedState = normalizeCaptchaGateState(activeGate);
4445+
this._captchaGateStates.set(tabId, guardedState);
4446+
toolResult.captchaGate = guardedState.publicGate;
4447+
return { gate: guardedState.publicGate, loopCheck: { kind: 'none' } };
4448+
}
4449+
}
4450+
const correlatedCaptchaCandidateIdentity =
4451+
activeGate?.publicGate?.candidateNotCorrelated === true
4452+
? null
4453+
: activeGate?.captchaCandidateIdentity;
44154454
let postSolveTokenState = null;
44164455
if (
44174456
['verification_pending', 'manual_required', 'cleared'].includes(activeGate?.status)
4418-
&& activeGate.captchaCandidateIdentity
4457+
&& correlatedCaptchaCandidateIdentity
44194458
) {
44204459
await inspectCaptchaFrames();
44214460
if (!detectionFailed && detection && typeof detection === 'object') {
44224461
postSolveTokenState = captchaPostSolveTokenState(
44234462
detection,
4424-
activeGate.captchaCandidateIdentity,
4463+
correlatedCaptchaCandidateIdentity,
44254464
);
44264465
}
44274466
}
4428-
const loopCheck = challenge
4467+
const directCaptchaEvidence = !!correlatedCaptchaCandidateIdentity
4468+
|| activeGate?.publicGate?.languageNeutralFrameTrigger === true;
4469+
const loopCheck = challenge || (authoritativeRootRead && !directCaptchaEvidence)
44294470
? this._checkVerificationChallengeLoop(tabId, {
44304471
pageUrl,
44314472
dialogLabel: challenge?.normalizedLabel || '',
@@ -4493,14 +4534,9 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
44934534
return { gate: pendingGate, loopCheck };
44944535
}
44954536
if (!challenge) {
4496-
if (
4497-
activeGate?.status === 'manual_required'
4498-
&& !activeGate.captchaCandidateIdentity
4499-
&& activeGate.publicGate?.languageNeutralFrameTrigger !== true
4500-
) {
4537+
if (activeGate?.status === 'manual_required' && !directCaptchaEvidence) {
45014538
const manualInspection = await this._detectChallengeDialogBeforeMutation(tabId, {
45024539
includeStatus: true,
4503-
allowGenericFailure: true,
45044540
...(Number.isInteger(activeGate.challengeFrameId)
45054541
? { expectedFrameId: activeGate.challengeFrameId }
45064542
: {}),
@@ -4516,6 +4552,12 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
45164552
return { gate: clearedGate, loopCheck };
45174553
}
45184554
}
4555+
if (activeGate && authoritativeRootRead && activeGate.status !== 'cleared') {
4556+
const guardedState = normalizeCaptchaGateState(activeGate);
4557+
this._captchaGateStates.set(tabId, guardedState);
4558+
toolResult.captchaGate = guardedState.publicGate;
4559+
return { gate: guardedState.publicGate, loopCheck };
4560+
}
45194561
if (activeGate?.status === 'cleared') {
45204562
return { gate: null, loopCheck };
45214563
}
@@ -4609,7 +4651,9 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
46094651
...(detection?.selected && !selectedCorrelated ? { candidateNotCorrelated: true } : {}),
46104652
...(languageNeutralFrameTrigger ? { languageNeutralFrameTrigger: true } : {}),
46114653
};
4612-
const captchaCandidateIdentity = captchaGateCandidateIdentity(detection?.selected);
4654+
const captchaCandidateIdentity = selectedCorrelated
4655+
? captchaGateCandidateIdentity(detection?.selected)
4656+
: null;
46134657
this._captchaGateStates.set(tabId, {
46144658
key,
46154659
status: publicGate.status,

src/chrome/src/agent/captcha-frame-runtime.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,7 @@ function selectedReason(candidate, constraints) {
602602

603603
// This function is serialized and executed in the web page. It must not
604604
// reference module-scope values.
605-
export function detectCaptchaCandidatesInPage(scope = null) {
605+
export function detectCaptchaCandidatesInPage(scope = null, matcherOptions = null) {
606606
const candidates = [];
607607
const pageWindow = scope?.window
608608
|| (typeof window !== 'undefined' ? window : null);
@@ -695,10 +695,15 @@ export function detectCaptchaCandidatesInPage(scope = null) {
695695
return false;
696696
}
697697
};
698-
const challengeDialogRe = /\b(?:(?:re|h|fun)?captcha|security verification|human verification|verify (?:that )?you(?:'|\u2019)re (?:a )?human|verify (?:that )?you are (?:a )?human|are you (?:a )?human|robot check|challenge verification)\b/i;
698+
let challengeDialogRe = null;
699+
try {
700+
const source = String(matcherOptions?.challengeLabelPatternSource || '');
701+
if (source) challengeDialogRe = new RegExp(source, 'i');
702+
} catch (_) {}
699703
const challengeDialogs = Array.from(
700704
pageDocument.querySelectorAll('dialog, [role="dialog"], [role="alertdialog"]')
701705
).filter((element) => {
706+
if (!challengeDialogRe) return false;
702707
if (!visibleElement(element)) return false;
703708
let labelledBy = '';
704709
try {

src/chrome/src/agent/captcha-gate.js

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
import { applyCaptchaFrameVisibility } from './captcha-frame-runtime.js';
22

3-
const CHALLENGE_DIALOG_RE = /\b(?:(?:re|h|fun)?captcha|security verification|human verification|verify (?:that )?you(?:'|\u2019)re (?:a )?human|verify (?:that )?you are (?:a )?human|are you (?:a )?human|robot check|challenge verification)\b/i;
4-
const CHALLENGE_FAILURE_RE = /\b(?:verification (?:failed|error|unsuccessful|expired|timed out)|could not verify|unable to verify)\b/i;
5-
const CHALLENGE_CONTEXT_RE = /\b(?:(?:re|h|fun)?captcha|human|robot|challenge)\b/i;
3+
export const CAPTCHA_CHALLENGE_LABEL_PATTERN_SOURCE = String.raw`\b(?:(?:re|h|fun)?captcha|security verification|human verification|verify (?:that )?you(?:'|\u2019)re (?:a )?human|verify (?:that )?you are (?:a )?human|are you (?:a )?human|robot check|challenge verification)\b`;
4+
const CHALLENGE_DIALOG_RE = new RegExp(CAPTCHA_CHALLENGE_LABEL_PATTERN_SOURCE, 'i');
65

7-
function matchesChallengeLabel(value, allowGenericFailure = false) {
8-
const text = String(value || '');
9-
return CHALLENGE_DIALOG_RE.test(text)
10-
|| (
11-
CHALLENGE_FAILURE_RE.test(text)
12-
&& (allowGenericFailure || CHALLENGE_CONTEXT_RE.test(text))
13-
);
6+
export function captchaChallengeMatcherOptions() {
7+
return { challengeLabelPatternSource: CAPTCHA_CHALLENGE_LABEL_PATTERN_SOURCE };
8+
}
9+
10+
function matchesChallengeLabel(value) {
11+
return CHALLENGE_DIALOG_RE.test(String(value || ''));
1412
}
1513

1614
function normalizeChallengeLabel(value) {
@@ -47,16 +45,15 @@ function parseSerializedTreeLabel(line) {
4745
return '';
4846
}
4947

50-
export function detectChallengeDialog(pageContent, options = null) {
51-
const allowGenericFailure = options?.allowGenericFailure === true;
48+
export function detectChallengeDialog(pageContent) {
5249
const lines = String(pageContent || '').split(/\r?\n/);
5350
for (let index = 0; index < lines.length; index += 1) {
5451
const line = lines[index];
5552
const dialogMatch = line.match(/^(\s*)(?:dialog|alertdialog)(?=\s|$)/i);
5653
if (!dialogMatch) continue;
5754
const dialogIndent = dialogMatch[1].length;
5855
const ownLabel = parseSerializedTreeLabel(line);
59-
if (ownLabel && matchesChallengeLabel(ownLabel, allowGenericFailure)) {
56+
if (ownLabel && matchesChallengeLabel(ownLabel)) {
6057
return {
6158
label: ownLabel,
6259
normalizedLabel: normalizeChallengeLabel(ownLabel),
@@ -68,7 +65,7 @@ export function detectChallengeDialog(pageContent, options = null) {
6865
const childIndent = childLine.match(/^\s*/)?.[0].length || 0;
6966
if (childIndent <= dialogIndent) break;
7067
const childLabel = parseSerializedTreeLabel(childLine);
71-
if (!childLabel || !matchesChallengeLabel(childLabel, allowGenericFailure)) continue;
68+
if (!childLabel || !matchesChallengeLabel(childLabel)) continue;
7269
return {
7370
label: childLabel,
7471
normalizedLabel: normalizeChallengeLabel(childLabel),
@@ -82,7 +79,6 @@ export function detectChallengeDialog(pageContent, options = null) {
8279
// model-authored mutations. Keep this function self-contained.
8380
export function detectChallengeDialogInPage(options = null) {
8481
const includeFrameContext = options?.includeFrameContext === true;
85-
const allowGenericFailure = options?.allowGenericFailure === true;
8682
const pageWindow = typeof window !== 'undefined' ? window : null;
8783
const pageLocation = pageWindow?.location
8884
|| (typeof location !== 'undefined' ? location : null);
@@ -96,16 +92,13 @@ export function detectChallengeDialogInPage(options = null) {
9692
? { challenge: null, frameContext: { frameUrl, frameName, childFrames: [] } }
9793
: null;
9894
}
99-
const challengeRe = /\b(?:(?:re|h|fun)?captcha|security verification|human verification|verify (?:that )?you(?:'|\u2019)re (?:a )?human|verify (?:that )?you are (?:a )?human|are you (?:a )?human|robot check|challenge verification)\b/i;
100-
const challengeFailureRe = /\b(?:verification (?:failed|error|unsuccessful|expired|timed out)|could not verify|unable to verify)\b/i;
101-
const challengeContextRe = /\b(?:(?:re|h|fun)?captcha|human|robot|challenge)\b/i;
95+
let challengeRe = null;
96+
try {
97+
const source = String(options?.challengeLabelPatternSource || '');
98+
if (source) challengeRe = new RegExp(source, 'i');
99+
} catch {}
102100
const matchesChallenge = value => {
103-
const text = String(value || '');
104-
return challengeRe.test(text)
105-
|| (
106-
challengeFailureRe.test(text)
107-
&& (allowGenericFailure || challengeContextRe.test(text))
108-
);
101+
return challengeRe?.test(String(value || '')) === true;
109102
};
110103
const visible = (element) => {
111104
try {

src/chrome/src/agent/captcha-solver.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
normalizeCaptchaType,
2323
selectCaptchaCandidate,
2424
} from './captcha-frame-runtime.js';
25-
import { buildCaptchaDiagnostics } from './captcha-gate.js';
25+
import { buildCaptchaDiagnostics, captchaChallengeMatcherOptions } from './captcha-gate.js';
2626

2727
export { captchaTypesMatch, captchaWebsiteUrl, normalizeCaptchaType, selectCaptchaCandidate };
2828

@@ -258,6 +258,7 @@ export async function detectCaptcha(tabId, constraints = {}) {
258258
chrome.scripting.executeScript({
259259
target: { tabId, allFrames: true },
260260
func: detectCaptchaCandidatesInPage,
261+
args: [null, captchaChallengeMatcherOptions()],
261262
}),
262263
frameTreePromise,
263264
]);

0 commit comments

Comments
 (0)