Skip to content

Commit 62647f5

Browse files
authored
Merge pull request #202 from esokullu/agent/frame-aware-captcha-solving
Fix frame-aware CAPTCHA solving
2 parents 969381e + c621d15 commit 62647f5

9 files changed

Lines changed: 4835 additions & 564 deletions

File tree

src/chrome/src/agent/agent.js

Lines changed: 138 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
} from './pdf-tools.js';
3030
import * as trace from '../trace/recorder.js';
3131
import { tracesToMarkdown } from './trace-export.js';
32-
import { solveCaptcha, detectCaptcha, injectToken, captchaParamError } from './captcha-solver.js';
32+
import { solveCaptcha, detectCaptcha, injectToken, captchaParamError, captchaTypesMatch, captchaWebsiteUrl } from './captcha-solver.js';
3333
import { getRecordingStateFresh as recorderStateFresh } from '../recorder/host.js';
3434
import { Capability, CAPABILITY_LABEL, capabilitiesFor, requiredHosts, frameHostMatches, isNetworkMutation, normalizeHost, PermissionManager, UNTRUSTED_CONTENT_TOOLS } from './permission-gate.js';
3535
import {
@@ -13810,32 +13810,120 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
1381013810
return noDispatchFailure('CapSolver is enabled but no API key is configured. Ask the user to set one in Settings → General → Advanced, or fall back to asking them to solve the captcha manually.');
1381113811
}
1381213812

13813-
// Resolve the website URL — the active tab's URL is what the
13814-
// captcha vendor needs to match its sitekey allowlist.
13813+
// Use the top-level URL as a fallback. Frame-aware detection below
13814+
// replaces it with the exact frame URL that loaded the selected
13815+
// widget, which is the URL CapSolver needs for nested challenges.
1381513816
let websiteURL = '';
1381613817
try {
1381713818
const tab = await chrome.tabs.get(tabId);
1381813819
websiteURL = tab?.url || '';
1381913820
} catch {}
1382013821

13821-
// Detect when the model didn't pre-specify a captcha type. Image-
13822-
// to-text is a special case that needs an explicit imageBase64.
13823-
let { type, websiteKey, isInvisible, isEnterprise, pageAction, minScore, imageBase64 } = args || {};
13822+
let {
13823+
type,
13824+
websiteKey,
13825+
frameUrl,
13826+
frameId,
13827+
framePath,
13828+
isInvisible,
13829+
isEnterprise,
13830+
pageAction,
13831+
minScore,
13832+
imageBase64,
13833+
enterprisePayload,
13834+
recaptchaDataSValue,
13835+
} = args || {};
1382413836
// Detection notes explain *why* a field is missing (e.g. a v3 widget
1382513837
// that never exposed its action name). Carry it into the failure so
1382613838
// the model gets the remedy, not just the rejection.
1382713839
let detectionNote = null;
13828-
if (!type) {
13829-
const detected = await detectCaptcha(tabId);
13830-
if (!detected) {
13840+
let detected = null;
13841+
if (type !== 'image_to_text') {
13842+
let detection = null;
13843+
try {
13844+
detection = await detectCaptcha(tabId, {
13845+
type,
13846+
frameUrl,
13847+
frameId,
13848+
framePath,
13849+
websiteKey,
13850+
});
13851+
} catch (detectionError) {
13852+
const explicitTokenOnlyFallback = args?.inject === false && !!type && !!websiteKey;
13853+
if (!explicitTokenOnlyFallback) {
13854+
return noDispatchFailure(
13855+
`solve_captcha: CAPTCHA frame detection failed before dispatch: ${detectionError?.message || String(detectionError)}`
13856+
);
13857+
}
13858+
detectionNote = `CAPTCHA frame detection was unavailable: ${detectionError?.message || String(detectionError)}`;
13859+
}
13860+
if (detection?.error) {
13861+
const hasDetectedCandidates = Array.isArray(detection.candidates) && detection.candidates.length > 0;
13862+
const needsDetection = !type
13863+
|| !websiteKey
13864+
|| !!frameUrl
13865+
|| Number.isInteger(frameId)
13866+
|| Array.isArray(framePath);
13867+
const explicitTokenOnlyFallback = args?.inject === false
13868+
&& !!type
13869+
&& !!websiteKey
13870+
&& !(detection.candidates || []).some(candidate =>
13871+
candidate?.websiteKey === websiteKey
13872+
);
13873+
if (!explicitTokenOnlyFallback
13874+
&& (detection.ambiguous || hasDetectedCandidates || needsDetection)) {
13875+
const candidates = hasDetectedCandidates ? ` Candidates: ${JSON.stringify(detection.candidates)}` : '';
13876+
return noDispatchFailure(`solve_captcha: ${detection.error}${candidates}`);
13877+
}
13878+
}
13879+
detected = detection?.selected || null;
13880+
if (!detected && (!type || !websiteKey)) {
1383113881
return noDispatchFailure('No CAPTCHA detected on the page. If the captcha lives inside a cross-origin iframe or uses a non-standard widget, pass `type` and `websiteKey` explicitly.');
1383213882
}
13833-
type = detected.type;
13834-
detectionNote = detected.note || null;
13835-
if (!websiteKey) websiteKey = detected.websiteKey;
13836-
if (isInvisible == null && detected.isInvisible != null) isInvisible = detected.isInvisible;
13837-
if (isEnterprise == null && detected.isEnterprise != null) isEnterprise = detected.isEnterprise;
13838-
if (!pageAction && detected.pageAction) pageAction = detected.pageAction;
13883+
if (detected) {
13884+
if (type && !captchaTypesMatch(type, detected.type, isEnterprise, detected.isEnterprise)) {
13885+
return noDispatchFailure(
13886+
`solve_captcha: requested type "${type}" conflicts with the active detected candidate "${detected.type}" in ${detected.frameUrl}. Retry without type or use the detected type.`
13887+
);
13888+
}
13889+
const visibilityModeApplies = /^(?:recaptcha_v2(?:_enterprise)?|hcaptcha)$/.test(
13890+
String(detected.type || '')
13891+
);
13892+
if (visibilityModeApplies
13893+
&& isInvisible != null
13894+
&& detected.isInvisible != null
13895+
&& Boolean(isInvisible) !== Boolean(detected.isInvisible)) {
13896+
return noDispatchFailure(
13897+
`solve_captcha: requested isInvisible=${Boolean(isInvisible)} conflicts with the active detected candidate isInvisible=${Boolean(detected.isInvisible)} in ${detected.frameUrl}. Retry without isInvisible or use the detected value.`
13898+
);
13899+
}
13900+
if (/^recaptcha_v[23](?:_enterprise)?$/.test(String(detected.type || ''))
13901+
&& isEnterprise != null
13902+
&& detected.isEnterprise != null
13903+
&& Boolean(isEnterprise) !== Boolean(detected.isEnterprise)) {
13904+
return noDispatchFailure(
13905+
`solve_captcha: requested isEnterprise=${Boolean(isEnterprise)} conflicts with the active detected candidate isEnterprise=${Boolean(detected.isEnterprise)} in ${detected.frameUrl}. Retry without isEnterprise or use the detected value.`
13906+
);
13907+
}
13908+
if (pageAction && detected.pageAction && String(pageAction) !== String(detected.pageAction)) {
13909+
return noDispatchFailure(
13910+
`solve_captcha: requested pageAction "${pageAction}" conflicts with the active detected candidate action "${detected.pageAction}" in ${detected.frameUrl}. Retry without pageAction or use the detected action.`
13911+
);
13912+
}
13913+
type = type || detected.type;
13914+
websiteURL = detected.websiteURL || captchaWebsiteUrl(detected.frameUrl, websiteURL);
13915+
frameUrl = detected.frameUrl || frameUrl;
13916+
detectionNote = detected.note || null;
13917+
if (!websiteKey) websiteKey = detected.websiteKey;
13918+
if (isInvisible == null && detected.isInvisible != null) isInvisible = detected.isInvisible;
13919+
if (isEnterprise == null && detected.isEnterprise != null) isEnterprise = detected.isEnterprise;
13920+
if (!pageAction && detected.pageAction) pageAction = detected.pageAction;
13921+
if (!enterprisePayload && detected.enterprisePayload) enterprisePayload = detected.enterprisePayload;
13922+
if (!recaptchaDataSValue && detected.recaptchaDataSValue) recaptchaDataSValue = detected.recaptchaDataSValue;
13923+
}
13924+
if (!detected && args?.inject === false && frameUrl) {
13925+
websiteURL = captchaWebsiteUrl(frameUrl, websiteURL);
13926+
}
1383913927
}
1384013928

1384113929
const params = {
@@ -13846,6 +13934,8 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
1384613934
...(isEnterprise != null ? { isEnterprise } : {}),
1384713935
...(pageAction ? { pageAction } : {}),
1384813936
...(minScore ? { minScore } : {}),
13937+
...(enterprisePayload ? { enterprisePayload } : {}),
13938+
...(recaptchaDataSValue ? { recaptchaDataSValue } : {}),
1384913939
...(imageBase64 ? { body: imageBase64 } : {}),
1385013940
};
1385113941

@@ -13864,19 +13954,39 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
1386413954
return noDispatchFailure(detectionNote ? `${paramError} ${detectionNote}` : paramError);
1386513955
}
1386613956

13957+
const wantInject = args?.inject !== false && type !== 'image_to_text';
13958+
if (wantInject && !detected) {
13959+
return noDispatchFailure(
13960+
'solve_captcha: token injection requires a detected CAPTCHA frame. The explicit type and websiteKey were not sent to CapSolver because no safe injection target could be verified. Retry with `inject: false` to receive the token without page injection, or ask the user to solve the challenge manually.'
13961+
);
13962+
}
13963+
1386713964
dispatched = true;
1386813965
const result = await solveCaptcha(apiKey, params);
1386913966

1387013967
// For non-image types, push the token into the page response field
1387113968
// unless the caller explicitly opted out.
13872-
const wantInject = args?.inject !== false && type !== 'image_to_text';
1387313969
let injection = null;
1387413970
if (wantInject && result.fieldName && result.token) {
1387513971
try {
1387613972
injection = await injectToken(tabId, {
1387713973
fieldName: result.fieldName,
1387813974
alsoSet: result.alsoSet,
1387913975
token: result.token,
13976+
callbackHint: detected.callbackName || null,
13977+
target: detected ? {
13978+
frameId: detected.frameId,
13979+
framePath: detected.framePath,
13980+
frameUrl: detected.frameUrl,
13981+
websiteKey: detected.websiteKey,
13982+
type: detected.type,
13983+
explicitWebsiteKey: detected.explicitWebsiteKey === true,
13984+
responseFieldId: detected.responseFieldId,
13985+
responseFieldIndex: detected.responseFieldIndex,
13986+
alsoResponseFieldId: detected.alsoResponseFieldId,
13987+
alsoResponseFieldIndex: detected.alsoResponseFieldIndex,
13988+
documentTimeOrigin: detected.documentTimeOrigin,
13989+
} : null,
1388013990
});
1388113991
} catch (e) {
1388213992
injection = { success: false, error: e.message };
@@ -13890,11 +14000,20 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
1389014000
taskId: result.taskId,
1389114001
token: result.token,
1389214002
tokenPreview: result.token ? `${String(result.token).slice(0, 24)}…(${String(result.token).length} chars)` : null,
14003+
selectedFrameId: Number.isInteger(detected?.frameId) ? detected.frameId : null,
14004+
selectedFramePath: Array.isArray(detected?.framePath) ? detected.framePath : null,
14005+
selectedFrameUrl: detected?.frameUrl || null,
14006+
selectionReason: detected?.selectionReason || null,
14007+
detectedType: detected?.type || null,
1389314008
injected: injection?.success === true,
1389414009
injection,
13895-
note: wantInject
13896-
? 'Token was injected into the page response field. Click the form\'s submit button next; do NOT call solve_captcha again.'
13897-
: 'Token returned, not injected. Pass it to the form via type_text on the response field, then submit.',
14010+
note: !wantInject
14011+
? 'Token returned without injection. Apply this token to the intended response field; do not request another paid solve for the same challenge.'
14012+
: injection?.success
14013+
? (injection.calledCallback
14014+
? 'Token was inserted into the selected frame and its callback was invoked. Wait for the page to react, then verify whether the challenge cleared.'
14015+
: 'Token was inserted into the selected frame, but no unique callback was invoked. Verify page progress before submitting or taking another action; do not request another paid solve.')
14016+
: 'CapSolver returned a token, but targeted injection failed. The challenge is not confirmed cleared; do not request another paid solve for the same token.',
1389814017
};
1389914018
} catch (e) {
1390014019
const error = `solve_captcha failed: ${e.message}`;

0 commit comments

Comments
 (0)