Skip to content

Commit 577662e

Browse files
authored
wait for captcha solve after navigation (#504)
* add waitforcaptcha method * add waitForCaptcha method * add env warning * dont expose wait function & parameterize * handle captchas on `act()` navigation * changeset
1 parent 9ba4b0b commit 577662e

File tree

5 files changed

+98
-3
lines changed

5 files changed

+98
-3
lines changed

Diff for: .changeset/polite-rivers-sip.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand": minor
3+
---
4+
5+
Enabled support for Browserbase captcha solving after page navigations. This can be enabled with the new constructor parameter: `waitForCaptchaSolves`.

Diff for: lib/StagehandPage.ts

+70-3
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export class StagehandPage {
4040
private cdpClient: CDPSession | null = null;
4141
private api: StagehandAPI;
4242
private userProvidedInstructions?: string;
43+
private waitForCaptchaSolves: boolean;
4344

4445
constructor(
4546
page: PlaywrightPage,
@@ -48,6 +49,7 @@ export class StagehandPage {
4849
llmClient: LLMClient,
4950
userProvidedInstructions?: string,
5051
api?: StagehandAPI,
52+
waitForCaptchaSolves?: boolean,
5153
) {
5254
this.intPage = Object.assign(page, {
5355
act: () => {
@@ -71,11 +73,14 @@ export class StagehandPage {
7173
);
7274
},
7375
});
76+
7477
this.stagehand = stagehand;
7578
this.intContext = context;
7679
this.llmClient = llmClient;
7780
this.api = api;
7881
this.userProvidedInstructions = userProvidedInstructions;
82+
this.waitForCaptchaSolves = waitForCaptchaSolves ?? false;
83+
7984
if (this.llmClient) {
8085
this.actHandler = new StagehandActHandler({
8186
verbose: this.stagehand.verbose,
@@ -87,6 +92,7 @@ export class StagehandPage {
8792
llmClient: llmClient,
8893
userProvidedInstructions,
8994
selfHeal: this.stagehand.selfHeal,
95+
waitForCaptchaSolves: this.waitForCaptchaSolves,
9096
});
9197
this.extractHandler = new StagehandExtractHandler({
9298
stagehand: this.stagehand,
@@ -146,17 +152,79 @@ export class StagehandPage {
146152
await this._waitForSettledDom();
147153
}
148154

155+
/**
156+
* Waits for a captcha to be solved when using Browserbase environment.
157+
*
158+
* @param timeoutMs - Optional timeout in milliseconds. If provided, the promise will reject if the captcha solving hasn't started within the given time.
159+
* @throws Error if called in a LOCAL environment
160+
* @throws Error if the timeout is reached before captcha solving starts
161+
* @returns Promise that resolves when the captcha is solved
162+
*/
163+
public async waitForCaptchaSolve(timeoutMs?: number) {
164+
if (this.stagehand.env === "LOCAL") {
165+
throw new Error(
166+
"The waitForCaptcha method may only be used when using the Browserbase environment.",
167+
);
168+
}
169+
170+
this.stagehand.log({
171+
category: "captcha",
172+
message: "Waiting for captcha",
173+
level: 1,
174+
});
175+
176+
return new Promise<void>((resolve, reject) => {
177+
let started = false;
178+
let timeoutId: NodeJS.Timeout;
179+
180+
if (timeoutMs) {
181+
timeoutId = setTimeout(() => {
182+
if (!started) {
183+
reject(new Error("Captcha timeout"));
184+
}
185+
}, timeoutMs);
186+
}
187+
188+
this.intPage.on("console", (msg) => {
189+
if (msg.text() === "browserbase-solving-finished") {
190+
this.stagehand.log({
191+
category: "captcha",
192+
message: "Captcha solving finished",
193+
level: 1,
194+
});
195+
if (timeoutId) clearTimeout(timeoutId);
196+
resolve();
197+
} else if (msg.text() === "browserbase-solving-started") {
198+
started = true;
199+
this.stagehand.log({
200+
category: "captcha",
201+
message: "Captcha solving started",
202+
level: 1,
203+
});
204+
}
205+
});
206+
});
207+
}
208+
149209
async init(): Promise<StagehandPage> {
150210
const page = this.intPage;
151211
const stagehand = this.stagehand;
152212
this.intPage = new Proxy(page, {
153213
get: (target, prop) => {
154-
if (prop === "goto")
214+
if (prop === "goto") {
155215
return async (url: string, options: GotoOptions) => {
156216
const result = this.api
157217
? await this.api.goto(url, options)
158218
: await page.goto(url, options);
159219

220+
if (this.waitForCaptchaSolves) {
221+
try {
222+
await this.waitForCaptchaSolve(1000);
223+
} catch {
224+
// ignore
225+
}
226+
}
227+
160228
if (this.api) {
161229
await this._refreshPageFromAPI();
162230
} else {
@@ -171,8 +239,7 @@ export class StagehandPage {
171239
}
172240
return result;
173241
};
174-
175-
if (this.llmClient) {
242+
} else if (this.llmClient) {
176243
if (prop === "act") {
177244
return async (options: ActOptions) => {
178245
return this.act(options);

Diff for: lib/handlers/actHandler.ts

+12
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export class StagehandActHandler {
3030
};
3131
private readonly userProvidedInstructions?: string;
3232
private readonly selfHeal: boolean;
33+
private readonly waitForCaptchaSolves: boolean;
3334

3435
constructor({
3536
verbose,
@@ -39,6 +40,7 @@ export class StagehandActHandler {
3940
stagehandPage,
4041
userProvidedInstructions,
4142
selfHeal,
43+
waitForCaptchaSolves,
4244
}: {
4345
verbose: 0 | 1 | 2;
4446
llmProvider: LLMProvider;
@@ -49,6 +51,7 @@ export class StagehandActHandler {
4951
stagehandContext: StagehandContext;
5052
userProvidedInstructions?: string;
5153
selfHeal: boolean;
54+
waitForCaptchaSolves: boolean;
5255
}) {
5356
this.verbose = verbose;
5457
this.llmProvider = llmProvider;
@@ -59,6 +62,7 @@ export class StagehandActHandler {
5962
this.stagehandPage = stagehandPage;
6063
this.userProvidedInstructions = userProvidedInstructions;
6164
this.selfHeal = selfHeal;
65+
this.waitForCaptchaSolves = waitForCaptchaSolves;
6266
}
6367

6468
/**
@@ -1379,6 +1383,14 @@ export class StagehandActHandler {
13791383

13801384
if (this.stagehandPage.page.url() !== initialUrl) {
13811385
steps += ` Result (Important): Page URL changed from ${initialUrl} to ${this.stagehandPage.page.url()}\n\n`;
1386+
1387+
if (this.waitForCaptchaSolves) {
1388+
try {
1389+
await this.stagehandPage.waitForCaptchaSolve(1000);
1390+
} catch {
1391+
// ignore
1392+
}
1393+
}
13821394
}
13831395

13841396
const actionCompleted = await this._verifyActionCompletion({

Diff for: lib/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@ export class Stagehand {
373373
private usingAPI: boolean;
374374
private modelName: AvailableModel;
375375
private apiClient: StagehandAPI | undefined;
376+
private waitForCaptchaSolves: boolean;
376377
private localBrowserLaunchOptions?: LocalBrowserLaunchOptions;
377378
public readonly selfHeal: boolean;
378379

@@ -397,6 +398,7 @@ export class Stagehand {
397398
useAPI,
398399
localBrowserLaunchOptions,
399400
selfHeal = true,
401+
waitForCaptchaSolves = false,
400402
}: ConstructorParams = {
401403
env: "BROWSERBASE",
402404
},
@@ -442,6 +444,8 @@ export class Stagehand {
442444
);
443445
}
444446

447+
this.waitForCaptchaSolves = waitForCaptchaSolves;
448+
445449
this.selfHeal = selfHeal;
446450
this.localBrowserLaunchOptions = localBrowserLaunchOptions;
447451
}
@@ -543,6 +547,7 @@ export class Stagehand {
543547
this.llmClient,
544548
this.userProvidedInstructions,
545549
this.apiClient,
550+
this.waitForCaptchaSolves,
546551
).init();
547552

548553
// Set the browser to headless mode if specified

Diff for: types/stagehand.ts

+6
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ export interface ConstructorParams {
3333
*/
3434
useAPI?: boolean;
3535
selfHeal?: boolean;
36+
/**
37+
* Wait for captchas to be solved after navigation when using Browserbase environment.
38+
*
39+
* @default false
40+
*/
41+
waitForCaptchaSolves?: boolean;
3642
localBrowserLaunchOptions?: LocalBrowserLaunchOptions;
3743
}
3844

0 commit comments

Comments
 (0)