browser-use 0.4.0 → 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/dist/agent/service.js +2 -0
- package/dist/agent/system_prompt.md +269 -0
- package/dist/agent/system_prompt_anthropic_flash.md +240 -0
- package/dist/agent/system_prompt_browser_use.md +18 -0
- package/dist/agent/system_prompt_browser_use_flash.md +15 -0
- package/dist/agent/system_prompt_browser_use_no_thinking.md +17 -0
- package/dist/agent/system_prompt_flash.md +16 -0
- package/dist/agent/system_prompt_flash_anthropic.md +30 -0
- package/dist/agent/system_prompt_no_thinking.md +245 -0
- package/dist/browser/cloud/index.d.ts +1 -0
- package/dist/browser/cloud/index.js +1 -0
- package/dist/browser/cloud/management.d.ts +130 -0
- package/dist/browser/cloud/management.js +140 -0
- package/dist/browser/events.d.ts +61 -3
- package/dist/browser/events.js +66 -0
- package/dist/browser/profile.d.ts +1 -0
- package/dist/browser/profile.js +25 -8
- package/dist/browser/session.d.ts +59 -2
- package/dist/browser/session.js +943 -131
- package/dist/browser/watchdogs/base.js +34 -1
- package/dist/browser/watchdogs/captcha-watchdog.d.ts +26 -0
- package/dist/browser/watchdogs/captcha-watchdog.js +151 -0
- package/dist/browser/watchdogs/index.d.ts +1 -0
- package/dist/browser/watchdogs/index.js +1 -0
- package/dist/browser/watchdogs/screenshot-watchdog.js +4 -3
- package/dist/cli.d.ts +120 -0
- package/dist/cli.js +1816 -4
- package/dist/controller/service.js +106 -362
- package/dist/controller/views.d.ts +9 -6
- package/dist/controller/views.js +8 -5
- package/dist/dom/dom_tree/index.js +24 -11
- package/dist/filesystem/file-system.js +1 -1
- package/dist/llm/litellm/chat.d.ts +11 -0
- package/dist/llm/litellm/chat.js +16 -0
- package/dist/llm/litellm/index.d.ts +1 -0
- package/dist/llm/litellm/index.js +1 -0
- package/dist/llm/models.js +29 -3
- package/dist/llm/oci-raw/chat.d.ts +64 -0
- package/dist/llm/oci-raw/chat.js +350 -0
- package/dist/llm/oci-raw/index.d.ts +2 -0
- package/dist/llm/oci-raw/index.js +2 -0
- package/dist/llm/oci-raw/serializer.d.ts +12 -0
- package/dist/llm/oci-raw/serializer.js +128 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +62 -13
- package/dist/skill-cli/direct.d.ts +100 -0
- package/dist/skill-cli/direct.js +984 -0
- package/dist/skill-cli/index.d.ts +2 -0
- package/dist/skill-cli/index.js +2 -0
- package/dist/skill-cli/server.d.ts +2 -0
- package/dist/skill-cli/server.js +472 -11
- package/dist/skill-cli/tunnel.d.ts +61 -0
- package/dist/skill-cli/tunnel.js +257 -0
- package/dist/sync/auth.d.ts +8 -0
- package/dist/sync/auth.js +12 -0
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +2 -1
- package/package.json +22 -4
package/dist/browser/session.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { isIP } from 'node:net';
|
|
5
|
-
import { execFile } from 'node:child_process';
|
|
5
|
+
import { execFile, execFileSync, } from 'node:child_process';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
7
|
import { createLogger } from '../logging-config.js';
|
|
8
8
|
import { match_url_with_domain_pattern, uuid7str } from '../utils.js';
|
|
@@ -10,13 +10,14 @@ import { EventBus, } from '../event-bus.js';
|
|
|
10
10
|
import { async_playwright, } from './types.js';
|
|
11
11
|
import { BrowserProfile, CHROME_DOCKER_ARGS, DEFAULT_BROWSER_PROFILE, } from './profile.js';
|
|
12
12
|
import { BrowserStateSummary, BrowserError, URLNotAllowedError, } from './views.js';
|
|
13
|
-
import { AgentFocusChangedEvent, BrowserConnectedEvent, BrowserLaunchEvent, BrowserStartEvent, BrowserStoppedEvent, BrowserStopEvent, DialogOpenedEvent, DownloadProgressEvent, DownloadStartedEvent, FileDownloadedEvent, TabClosedEvent, TabCreatedEvent, } from './events.js';
|
|
13
|
+
import { AgentFocusChangedEvent, BrowserConnectedEvent, BrowserErrorEvent, BrowserLaunchEvent, BrowserReconnectedEvent, BrowserReconnectingEvent, BrowserStartEvent, BrowserStoppedEvent, BrowserStopEvent, DialogOpenedEvent, DownloadProgressEvent, DownloadStartedEvent, FileDownloadedEvent, TabClosedEvent, TabCreatedEvent, } from './events.js';
|
|
14
14
|
import { DOMElementNode, DOMState } from '../dom/views.js';
|
|
15
15
|
import { normalize_url } from './utils.js';
|
|
16
16
|
import { DomService } from '../dom/service.js';
|
|
17
17
|
import { showDVDScreensaver, showSpinner, withDVDScreensaver, } from './dvd-screensaver.js';
|
|
18
18
|
import { SessionManager } from './session-manager.js';
|
|
19
19
|
import { AboutBlankWatchdog } from './watchdogs/aboutblank-watchdog.js';
|
|
20
|
+
import { CaptchaWatchdog, } from './watchdogs/captcha-watchdog.js';
|
|
20
21
|
import { CDPSessionWatchdog } from './watchdogs/cdp-session-watchdog.js';
|
|
21
22
|
import { CrashWatchdog } from './watchdogs/crash-watchdog.js';
|
|
22
23
|
import { DefaultActionWatchdog } from './watchdogs/default-action-watchdog.js';
|
|
@@ -31,6 +32,147 @@ import { ScreenshotWatchdog } from './watchdogs/screenshot-watchdog.js';
|
|
|
31
32
|
import { SecurityWatchdog } from './watchdogs/security-watchdog.js';
|
|
32
33
|
import { StorageStateWatchdog } from './watchdogs/storage-state-watchdog.js';
|
|
33
34
|
const execFileAsync = promisify(execFile);
|
|
35
|
+
const PLAYWRIGHT_OPTION_KEY_OVERRIDES = {
|
|
36
|
+
extra_http_headers: 'extraHTTPHeaders',
|
|
37
|
+
};
|
|
38
|
+
const EMPTY_DOM_RETRY_DELAY_MS = 250;
|
|
39
|
+
const REMOTE_RECONNECT_DELAYS_MS = [1000, 2000, 4000];
|
|
40
|
+
const REMOTE_RECONNECT_ATTEMPT_TIMEOUT_MS = 15_000;
|
|
41
|
+
const cloneBrowserProfileConfig = (profile) => typeof structuredClone === 'function'
|
|
42
|
+
? structuredClone(profile.config)
|
|
43
|
+
: JSON.parse(JSON.stringify(profile.config));
|
|
44
|
+
const detectSystemChromeVariant = (executablePath) => {
|
|
45
|
+
const normalizedPath = String(executablePath ?? '')
|
|
46
|
+
.trim()
|
|
47
|
+
.toLowerCase();
|
|
48
|
+
if (!normalizedPath) {
|
|
49
|
+
return 'chrome';
|
|
50
|
+
}
|
|
51
|
+
if (normalizedPath.includes('chromium')) {
|
|
52
|
+
return 'chromium';
|
|
53
|
+
}
|
|
54
|
+
if (normalizedPath.includes('chrome canary') ||
|
|
55
|
+
normalizedPath.includes('chrome sxs')) {
|
|
56
|
+
return 'chrome-canary';
|
|
57
|
+
}
|
|
58
|
+
if (normalizedPath.includes('google-chrome-beta')) {
|
|
59
|
+
return 'chrome-beta';
|
|
60
|
+
}
|
|
61
|
+
if (normalizedPath.includes('google-chrome-unstable')) {
|
|
62
|
+
return 'chrome-unstable';
|
|
63
|
+
}
|
|
64
|
+
return 'chrome';
|
|
65
|
+
};
|
|
66
|
+
export const systemChrome = {
|
|
67
|
+
findExecutable() {
|
|
68
|
+
if (process.platform === 'darwin') {
|
|
69
|
+
const candidates = [
|
|
70
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
71
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
72
|
+
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
|
73
|
+
];
|
|
74
|
+
return candidates.find((candidate) => fs.existsSync(candidate)) ?? null;
|
|
75
|
+
}
|
|
76
|
+
if (process.platform === 'linux') {
|
|
77
|
+
const commands = [
|
|
78
|
+
'google-chrome',
|
|
79
|
+
'google-chrome-stable',
|
|
80
|
+
'google-chrome-beta',
|
|
81
|
+
'google-chrome-unstable',
|
|
82
|
+
'chromium',
|
|
83
|
+
'chromium-browser',
|
|
84
|
+
];
|
|
85
|
+
for (const command of commands) {
|
|
86
|
+
try {
|
|
87
|
+
const resolved = execFileSync('which', [command], {
|
|
88
|
+
encoding: 'utf8',
|
|
89
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
90
|
+
}).trim();
|
|
91
|
+
if (resolved) {
|
|
92
|
+
return resolved;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Ignore missing commands and try the next candidate.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
if (process.platform === 'win32') {
|
|
102
|
+
const candidates = [
|
|
103
|
+
path.join(process.env.ProgramFiles ?? 'C:\\Program Files', 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
104
|
+
path.join(process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)', 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
105
|
+
path.join(process.env.LOCALAPPDATA ?? '', 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
106
|
+
path.join(process.env.LOCALAPPDATA ?? '', 'Google', 'Chrome SxS', 'Application', 'chrome.exe'),
|
|
107
|
+
path.join(process.env.LOCALAPPDATA ?? '', 'Chromium', 'Application', 'chrome.exe'),
|
|
108
|
+
path.join(process.env.ProgramFiles ?? 'C:\\Program Files', 'Chromium', 'Application', 'chrome.exe'),
|
|
109
|
+
path.join(process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)', 'Chromium', 'Application', 'chrome.exe'),
|
|
110
|
+
];
|
|
111
|
+
return candidates.find((candidate) => fs.existsSync(candidate)) ?? null;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
},
|
|
115
|
+
getUserDataDir(executablePath = systemChrome.findExecutable()) {
|
|
116
|
+
const variant = detectSystemChromeVariant(executablePath);
|
|
117
|
+
if (process.platform === 'darwin') {
|
|
118
|
+
const applicationSupportDir = path.join(os.homedir(), 'Library', 'Application Support');
|
|
119
|
+
if (variant === 'chromium') {
|
|
120
|
+
return path.join(applicationSupportDir, 'Chromium');
|
|
121
|
+
}
|
|
122
|
+
if (variant === 'chrome-canary') {
|
|
123
|
+
return path.join(applicationSupportDir, 'Google', 'Chrome Canary');
|
|
124
|
+
}
|
|
125
|
+
return path.join(applicationSupportDir, 'Google', 'Chrome');
|
|
126
|
+
}
|
|
127
|
+
if (process.platform === 'linux') {
|
|
128
|
+
if (variant === 'chromium') {
|
|
129
|
+
return path.join(os.homedir(), '.config', 'chromium');
|
|
130
|
+
}
|
|
131
|
+
if (variant === 'chrome-beta') {
|
|
132
|
+
return path.join(os.homedir(), '.config', 'google-chrome-beta');
|
|
133
|
+
}
|
|
134
|
+
if (variant === 'chrome-unstable') {
|
|
135
|
+
return path.join(os.homedir(), '.config', 'google-chrome-unstable');
|
|
136
|
+
}
|
|
137
|
+
return path.join(os.homedir(), '.config', 'google-chrome');
|
|
138
|
+
}
|
|
139
|
+
if (process.platform === 'win32') {
|
|
140
|
+
const localAppData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local');
|
|
141
|
+
if (variant === 'chromium') {
|
|
142
|
+
return path.join(localAppData, 'Chromium', 'User Data');
|
|
143
|
+
}
|
|
144
|
+
if (variant === 'chrome-canary') {
|
|
145
|
+
return path.join(localAppData, 'Google', 'Chrome SxS', 'User Data');
|
|
146
|
+
}
|
|
147
|
+
return path.join(localAppData, 'Google', 'Chrome', 'User Data');
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
},
|
|
151
|
+
listProfiles(userDataDir = systemChrome.getUserDataDir()) {
|
|
152
|
+
if (!userDataDir) {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
const localStatePath = path.join(userDataDir, 'Local State');
|
|
156
|
+
if (!fs.existsSync(localStatePath)) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const raw = fs.readFileSync(localStatePath, 'utf8');
|
|
161
|
+
const localState = JSON.parse(raw);
|
|
162
|
+
const infoCache = localState.profile?.info_cache ?? {};
|
|
163
|
+
return Object.entries(infoCache)
|
|
164
|
+
.map(([directory, info]) => ({
|
|
165
|
+
directory,
|
|
166
|
+
name: info?.name || directory,
|
|
167
|
+
email: info?.user_name || '',
|
|
168
|
+
}))
|
|
169
|
+
.sort((left, right) => left.directory.localeCompare(right.directory));
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
};
|
|
34
176
|
const createEmptyDomState = () => {
|
|
35
177
|
const root = new DOMElementNode(true, null, 'html', '/html[1]', {}, []);
|
|
36
178
|
return new DOMState(root, {});
|
|
@@ -77,11 +219,18 @@ export class BrowserSession {
|
|
|
77
219
|
_maxRecentEvents = 100;
|
|
78
220
|
_watchdogs = new Set();
|
|
79
221
|
_defaultWatchdogsAttached = false;
|
|
222
|
+
_captchaWatchdog = null;
|
|
223
|
+
RECONNECT_WAIT_TIMEOUT = 54;
|
|
224
|
+
_reconnecting = false;
|
|
225
|
+
_reconnectTask = null;
|
|
226
|
+
_reconnectWaitPromise = Promise.resolve();
|
|
227
|
+
_resolveReconnectWait = null;
|
|
228
|
+
_intentionalStop = false;
|
|
229
|
+
_disconnectAwareBrowser = null;
|
|
230
|
+
_browserDisconnectHandler = null;
|
|
80
231
|
constructor(init = {}) {
|
|
81
232
|
const sourceProfileConfig = init.browser_profile
|
|
82
|
-
?
|
|
83
|
-
? structuredClone(init.browser_profile.config)
|
|
84
|
-
: JSON.parse(JSON.stringify(init.browser_profile.config))
|
|
233
|
+
? cloneBrowserProfileConfig(init.browser_profile)
|
|
85
234
|
: (init.profile ?? {});
|
|
86
235
|
this.browser_profile = new BrowserProfile(sourceProfileConfig);
|
|
87
236
|
this.id = init.id ?? uuid7str();
|
|
@@ -121,12 +270,56 @@ export class BrowserSession {
|
|
|
121
270
|
this._attachDialogHandler(this.agent_current_page);
|
|
122
271
|
this._recordRecentEvent('session_initialized', { url: this.currentUrl });
|
|
123
272
|
}
|
|
273
|
+
static from_system_chrome(init = {}) {
|
|
274
|
+
const executablePath = systemChrome.findExecutable();
|
|
275
|
+
if (!executablePath) {
|
|
276
|
+
throw new Error('Chrome not found. Please install Chrome or use BrowserSession with an explicit executable_path.\n' +
|
|
277
|
+
'Expected locations:\n' +
|
|
278
|
+
' macOS: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome\n' +
|
|
279
|
+
' Linux: /usr/bin/google-chrome or /usr/bin/chromium\n' +
|
|
280
|
+
' Windows: C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe');
|
|
281
|
+
}
|
|
282
|
+
const userDataDir = systemChrome.getUserDataDir(executablePath);
|
|
283
|
+
if (!userDataDir) {
|
|
284
|
+
throw new Error('Could not detect Chrome profile directory for your platform.\n' +
|
|
285
|
+
'Expected locations:\n' +
|
|
286
|
+
' macOS: ~/Library/Application Support/Google/Chrome\n' +
|
|
287
|
+
' Linux: ~/.config/google-chrome\n' +
|
|
288
|
+
' Windows: %LocalAppData%\\Google\\Chrome\\User Data');
|
|
289
|
+
}
|
|
290
|
+
const availableProfiles = systemChrome.listProfiles(userDataDir);
|
|
291
|
+
const selectedProfileDirectory = init.profile_directory ?? availableProfiles[0]?.directory ?? 'Default';
|
|
292
|
+
if (typeof init.profile_directory === 'undefined' && availableProfiles[0]) {
|
|
293
|
+
createLogger('browser_use.browser.session').info(`Auto-selected Chrome profile: ${availableProfiles[0].name} (${availableProfiles[0].directory})`);
|
|
294
|
+
}
|
|
295
|
+
const sourceProfileConfig = init.browser_profile
|
|
296
|
+
? cloneBrowserProfileConfig(init.browser_profile)
|
|
297
|
+
: (init.profile ?? {});
|
|
298
|
+
const { browser_profile: _browserProfile, profile: _profile, profile_directory: _profileDirectory, ...sessionInit } = init;
|
|
299
|
+
return new BrowserSession({
|
|
300
|
+
...sessionInit,
|
|
301
|
+
browser_profile: new BrowserProfile({
|
|
302
|
+
...sourceProfileConfig,
|
|
303
|
+
executable_path: executablePath,
|
|
304
|
+
user_data_dir: userDataDir,
|
|
305
|
+
profile_directory: selectedProfileDirectory,
|
|
306
|
+
}),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
static list_chrome_profiles() {
|
|
310
|
+
const executablePath = systemChrome.findExecutable();
|
|
311
|
+
const userDataDir = systemChrome.getUserDataDir(executablePath);
|
|
312
|
+
return systemChrome.listProfiles(userDataDir);
|
|
313
|
+
}
|
|
124
314
|
attach_watchdog(watchdog) {
|
|
125
315
|
if (this._watchdogs.has(watchdog)) {
|
|
126
316
|
return;
|
|
127
317
|
}
|
|
128
318
|
watchdog.attach_to_session();
|
|
129
319
|
this._watchdogs.add(watchdog);
|
|
320
|
+
if (watchdog instanceof CaptchaWatchdog) {
|
|
321
|
+
this._captchaWatchdog = watchdog;
|
|
322
|
+
}
|
|
130
323
|
}
|
|
131
324
|
attach_watchdogs(watchdogs) {
|
|
132
325
|
for (const watchdog of watchdogs) {
|
|
@@ -139,12 +332,16 @@ export class BrowserSession {
|
|
|
139
332
|
}
|
|
140
333
|
watchdog.detach_from_session();
|
|
141
334
|
this._watchdogs.delete(watchdog);
|
|
335
|
+
if (watchdog === this._captchaWatchdog) {
|
|
336
|
+
this._captchaWatchdog = null;
|
|
337
|
+
}
|
|
142
338
|
}
|
|
143
339
|
detach_all_watchdogs() {
|
|
144
340
|
for (const watchdog of [...this._watchdogs]) {
|
|
145
341
|
this.detach_watchdog(watchdog);
|
|
146
342
|
}
|
|
147
343
|
this._defaultWatchdogsAttached = false;
|
|
344
|
+
this._captchaWatchdog = null;
|
|
148
345
|
}
|
|
149
346
|
get_watchdogs() {
|
|
150
347
|
return [...this._watchdogs];
|
|
@@ -179,6 +376,10 @@ export class BrowserSession {
|
|
|
179
376
|
new StorageStateWatchdog({ browser_session: this }),
|
|
180
377
|
new DefaultActionWatchdog({ browser_session: this }),
|
|
181
378
|
];
|
|
379
|
+
if (this.browser_profile.config.captcha_solver) {
|
|
380
|
+
this._captchaWatchdog = new CaptchaWatchdog({ browser_session: this });
|
|
381
|
+
watchdogs.push(this._captchaWatchdog);
|
|
382
|
+
}
|
|
182
383
|
const configuredHarPath = this.browser_profile.config.record_har_path;
|
|
183
384
|
if (typeof configuredHarPath === 'string' &&
|
|
184
385
|
configuredHarPath.trim().length > 0) {
|
|
@@ -187,6 +388,9 @@ export class BrowserSession {
|
|
|
187
388
|
this.attach_watchdogs(watchdogs);
|
|
188
389
|
this._defaultWatchdogsAttached = true;
|
|
189
390
|
}
|
|
391
|
+
async wait_if_captcha_solving(timeoutSeconds) {
|
|
392
|
+
return (this._captchaWatchdog?.wait_if_captcha_solving(timeoutSeconds) ?? null);
|
|
393
|
+
}
|
|
190
394
|
_formatTabId(pageId) {
|
|
191
395
|
const normalized = Number.isFinite(pageId) && pageId >= 0 ? Math.floor(pageId) : 0;
|
|
192
396
|
return String(normalized).padStart(4, '0').slice(-4);
|
|
@@ -400,6 +604,118 @@ export class BrowserSession {
|
|
|
400
604
|
this._attachDialogHandler(page);
|
|
401
605
|
this.agent_current_page = page ?? null;
|
|
402
606
|
}
|
|
607
|
+
async _syncCurrentTabFromPage(page) {
|
|
608
|
+
if (!page) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
let resolvedUrl = null;
|
|
612
|
+
try {
|
|
613
|
+
const rawUrl = page.url();
|
|
614
|
+
if (typeof rawUrl === 'string' && rawUrl.trim()) {
|
|
615
|
+
resolvedUrl = normalize_url(rawUrl);
|
|
616
|
+
this.currentUrl = resolvedUrl;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
// Ignore transient URL read failures.
|
|
621
|
+
}
|
|
622
|
+
let resolvedTitle = null;
|
|
623
|
+
if (typeof page.title === 'function') {
|
|
624
|
+
try {
|
|
625
|
+
const title = await page.title();
|
|
626
|
+
if (typeof title === 'string' && title.trim()) {
|
|
627
|
+
resolvedTitle = title;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
631
|
+
// Ignore transient title read failures.
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (!resolvedTitle) {
|
|
635
|
+
resolvedTitle = resolvedUrl ?? this.currentTitle ?? this.currentUrl;
|
|
636
|
+
}
|
|
637
|
+
this.currentTitle = resolvedTitle;
|
|
638
|
+
const currentTab = this._tabs[this.currentTabIndex];
|
|
639
|
+
if (currentTab) {
|
|
640
|
+
if (resolvedUrl) {
|
|
641
|
+
currentTab.url = resolvedUrl;
|
|
642
|
+
}
|
|
643
|
+
currentTab.title = resolvedTitle;
|
|
644
|
+
this._syncSessionManagerFromTabs();
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
_syncTabsWithBrowserPages() {
|
|
648
|
+
const pages = this.browser_context?.pages?.() ?? [];
|
|
649
|
+
if (!pages.length) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const nextTabs = [];
|
|
653
|
+
const nextTabPages = new Map();
|
|
654
|
+
const usedPageIds = new Set();
|
|
655
|
+
const knownPageMappings = Array.from(this.tabPages.entries());
|
|
656
|
+
for (const page of pages) {
|
|
657
|
+
this._attachDialogHandler(page ?? null);
|
|
658
|
+
let pageId = null;
|
|
659
|
+
for (const [candidateId, candidatePage] of knownPageMappings) {
|
|
660
|
+
if (candidatePage === page && !usedPageIds.has(candidateId)) {
|
|
661
|
+
pageId = candidateId;
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (pageId === null) {
|
|
666
|
+
pageId = this._tabCounter++;
|
|
667
|
+
}
|
|
668
|
+
usedPageIds.add(pageId);
|
|
669
|
+
const existingTab = this._tabs.find((tab) => tab.page_id === pageId);
|
|
670
|
+
const tab = existingTab
|
|
671
|
+
? { ...existingTab }
|
|
672
|
+
: this._createTabInfo({
|
|
673
|
+
page_id: pageId,
|
|
674
|
+
url: 'about:blank',
|
|
675
|
+
title: 'about:blank',
|
|
676
|
+
});
|
|
677
|
+
try {
|
|
678
|
+
const rawUrl = page.url();
|
|
679
|
+
if (typeof rawUrl === 'string' && rawUrl.trim()) {
|
|
680
|
+
tab.url = normalize_url(rawUrl);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
catch {
|
|
684
|
+
// Keep existing tab url when page url is not readable.
|
|
685
|
+
}
|
|
686
|
+
if (!existingTab || !tab.title || tab.title === 'about:blank') {
|
|
687
|
+
tab.title = tab.url;
|
|
688
|
+
}
|
|
689
|
+
nextTabs.push(tab);
|
|
690
|
+
nextTabPages.set(pageId, page);
|
|
691
|
+
}
|
|
692
|
+
if (!nextTabs.length) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
this._tabs = nextTabs;
|
|
696
|
+
this.tabPages = nextTabPages;
|
|
697
|
+
const activePage = this.agent_current_page && pages.includes(this.agent_current_page)
|
|
698
|
+
? this.agent_current_page
|
|
699
|
+
: (pages[0] ?? null);
|
|
700
|
+
if (activePage) {
|
|
701
|
+
const activeIndex = this._tabs.findIndex((tab) => this.tabPages.get(tab.page_id) === activePage);
|
|
702
|
+
if (activeIndex !== -1) {
|
|
703
|
+
this.currentTabIndex = activeIndex;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (this.currentTabIndex < 0 || this.currentTabIndex >= this._tabs.length) {
|
|
707
|
+
this.currentTabIndex = Math.max(0, this._tabs.length - 1);
|
|
708
|
+
}
|
|
709
|
+
const activeTab = this._tabs[this.currentTabIndex] ?? null;
|
|
710
|
+
if (activeTab) {
|
|
711
|
+
this.currentUrl = activeTab.url;
|
|
712
|
+
this.currentTitle = activeTab.title || activeTab.url;
|
|
713
|
+
this.agent_current_page = this.tabPages.get(activeTab.page_id) ?? null;
|
|
714
|
+
this.human_current_page =
|
|
715
|
+
this.human_current_page ?? this.agent_current_page;
|
|
716
|
+
}
|
|
717
|
+
this._syncSessionManagerFromTabs();
|
|
718
|
+
}
|
|
403
719
|
_captureClosedPopupMessage(dialogType, message) {
|
|
404
720
|
const normalizedType = String(dialogType || 'alert').trim() || 'alert';
|
|
405
721
|
const normalizedMessage = String(message || '').trim();
|
|
@@ -600,6 +916,65 @@ export class BrowserSession {
|
|
|
600
916
|
get is_stopping() {
|
|
601
917
|
return this._stoppingPromise !== null;
|
|
602
918
|
}
|
|
919
|
+
get is_reconnecting() {
|
|
920
|
+
return this._reconnecting;
|
|
921
|
+
}
|
|
922
|
+
get should_gate_watchdog_events() {
|
|
923
|
+
return Boolean(this.initialized ||
|
|
924
|
+
this.browser ||
|
|
925
|
+
this.browser_context ||
|
|
926
|
+
this.cdp_url ||
|
|
927
|
+
this.wss_url ||
|
|
928
|
+
this._reconnecting);
|
|
929
|
+
}
|
|
930
|
+
get is_cdp_connected() {
|
|
931
|
+
try {
|
|
932
|
+
if (this.browser) {
|
|
933
|
+
const browser = this.browser;
|
|
934
|
+
if (typeof browser.isConnected === 'function' &&
|
|
935
|
+
!browser.isConnected()) {
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
if (this.browser_context) {
|
|
940
|
+
const contextBrowser = this.browser_context.browser?.();
|
|
941
|
+
if (contextBrowser &&
|
|
942
|
+
typeof contextBrowser.isConnected === 'function' &&
|
|
943
|
+
!contextBrowser.isConnected()) {
|
|
944
|
+
return false;
|
|
945
|
+
}
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
return Boolean(this.browser);
|
|
949
|
+
}
|
|
950
|
+
catch {
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
async wait_for_reconnect(timeoutSeconds = this.RECONNECT_WAIT_TIMEOUT) {
|
|
955
|
+
if (!this._reconnecting) {
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
const timeoutMs = Number.isFinite(timeoutSeconds) && timeoutSeconds > 0
|
|
959
|
+
? timeoutSeconds * 1000
|
|
960
|
+
: this.RECONNECT_WAIT_TIMEOUT * 1000;
|
|
961
|
+
let timeoutHandle = null;
|
|
962
|
+
try {
|
|
963
|
+
await Promise.race([
|
|
964
|
+
this._reconnectWaitPromise,
|
|
965
|
+
new Promise((_, reject) => {
|
|
966
|
+
timeoutHandle = setTimeout(() => {
|
|
967
|
+
reject(new Error(`Reconnection wait timed out after ${Math.round(timeoutMs / 1000)}s`));
|
|
968
|
+
}, timeoutMs);
|
|
969
|
+
}),
|
|
970
|
+
]);
|
|
971
|
+
}
|
|
972
|
+
finally {
|
|
973
|
+
if (timeoutHandle) {
|
|
974
|
+
clearTimeout(timeoutHandle);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
603
978
|
claim_agent(agentId, mode = 'exclusive') {
|
|
604
979
|
if (!agentId) {
|
|
605
980
|
return false;
|
|
@@ -785,11 +1160,276 @@ export class BrowserSession {
|
|
|
785
1160
|
if (convertedValue === undefined) {
|
|
786
1161
|
continue;
|
|
787
1162
|
}
|
|
788
|
-
const normalizedKey = rawKey
|
|
1163
|
+
const normalizedKey = PLAYWRIGHT_OPTION_KEY_OVERRIDES[rawKey] ??
|
|
1164
|
+
rawKey.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
789
1165
|
result[normalizedKey] = convertedValue;
|
|
790
1166
|
}
|
|
791
1167
|
return result;
|
|
792
1168
|
}
|
|
1169
|
+
async set_extra_headers(headers) {
|
|
1170
|
+
const normalizedHeaders = Object.fromEntries(Object.entries(headers)
|
|
1171
|
+
.map(([key, value]) => [String(key).trim(), String(value)])
|
|
1172
|
+
.filter(([key]) => key.length > 0));
|
|
1173
|
+
if (!this.browser_context ||
|
|
1174
|
+
Object.keys(normalizedHeaders).length === 0 ||
|
|
1175
|
+
typeof this.browser_context.setExtraHTTPHeaders !== 'function') {
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
await this.browser_context.setExtraHTTPHeaders(normalizedHeaders);
|
|
1179
|
+
}
|
|
1180
|
+
async _applyConfiguredExtraHttpHeaders() {
|
|
1181
|
+
const configuredHeaders = this.browser_profile.config.extra_http_headers;
|
|
1182
|
+
if (!configuredHeaders || Object.keys(configuredHeaders).length === 0) {
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
await this.set_extra_headers(configuredHeaders);
|
|
1186
|
+
}
|
|
1187
|
+
_usesRemoteBrowserConnection() {
|
|
1188
|
+
return Boolean(this.cdp_url || this.wss_url);
|
|
1189
|
+
}
|
|
1190
|
+
async _connectToConfiguredBrowser(playwright) {
|
|
1191
|
+
const connectOptions = this._toPlaywrightOptions(this.browser_profile.kwargs_for_connect());
|
|
1192
|
+
if (this.cdp_url) {
|
|
1193
|
+
return await playwright.chromium.connectOverCDP(this.cdp_url, connectOptions ?? {});
|
|
1194
|
+
}
|
|
1195
|
+
if (this.wss_url) {
|
|
1196
|
+
return await playwright.chromium.connect(this.wss_url, connectOptions ?? {});
|
|
1197
|
+
}
|
|
1198
|
+
throw new Error('Cannot connect to a remote browser without cdp_url or wss_url');
|
|
1199
|
+
}
|
|
1200
|
+
async _ensureBrowserContextFromBrowser(browser) {
|
|
1201
|
+
const existingContexts = (typeof browser?.contexts === 'function' ? browser.contexts() : []) ?? [];
|
|
1202
|
+
if (existingContexts.length > 0) {
|
|
1203
|
+
return existingContexts[0] ?? null;
|
|
1204
|
+
}
|
|
1205
|
+
if (typeof browser?.newContext === 'function') {
|
|
1206
|
+
const contextOptions = this._toPlaywrightOptions(this.browser_profile.kwargs_for_new_context());
|
|
1207
|
+
return await browser.newContext(contextOptions ?? {});
|
|
1208
|
+
}
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
_beginReconnectWait() {
|
|
1212
|
+
this._reconnectWaitPromise = new Promise((resolve) => {
|
|
1213
|
+
this._resolveReconnectWait = resolve;
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
_endReconnectWait() {
|
|
1217
|
+
this._resolveReconnectWait?.();
|
|
1218
|
+
this._resolveReconnectWait = null;
|
|
1219
|
+
this._reconnectWaitPromise = Promise.resolve();
|
|
1220
|
+
}
|
|
1221
|
+
_detachRemoteDisconnectHandler() {
|
|
1222
|
+
if (!this._disconnectAwareBrowser || !this._browserDisconnectHandler) {
|
|
1223
|
+
this._disconnectAwareBrowser = null;
|
|
1224
|
+
this._browserDisconnectHandler = null;
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
if (typeof this._disconnectAwareBrowser.off === 'function') {
|
|
1228
|
+
this._disconnectAwareBrowser.off('disconnected', this._browserDisconnectHandler);
|
|
1229
|
+
}
|
|
1230
|
+
else if (typeof this._disconnectAwareBrowser.removeListener === 'function') {
|
|
1231
|
+
this._disconnectAwareBrowser.removeListener('disconnected', this._browserDisconnectHandler);
|
|
1232
|
+
}
|
|
1233
|
+
this._disconnectAwareBrowser = null;
|
|
1234
|
+
this._browserDisconnectHandler = null;
|
|
1235
|
+
}
|
|
1236
|
+
_attachRemoteDisconnectHandler(browser) {
|
|
1237
|
+
this._detachRemoteDisconnectHandler();
|
|
1238
|
+
if (!this._usesRemoteBrowserConnection()) {
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
const browserWithEvents = browser;
|
|
1242
|
+
if (!browserWithEvents || typeof browserWithEvents.on !== 'function') {
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
const onDisconnected = () => {
|
|
1246
|
+
this._handleUnexpectedRemoteDisconnect();
|
|
1247
|
+
};
|
|
1248
|
+
browserWithEvents.on('disconnected', onDisconnected);
|
|
1249
|
+
this._disconnectAwareBrowser = browserWithEvents;
|
|
1250
|
+
this._browserDisconnectHandler = onDisconnected;
|
|
1251
|
+
}
|
|
1252
|
+
_handleUnexpectedRemoteDisconnect() {
|
|
1253
|
+
if (this._intentionalStop ||
|
|
1254
|
+
this._reconnecting ||
|
|
1255
|
+
!this._usesRemoteBrowserConnection()) {
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
this.logger.warning('Remote browser connection closed unexpectedly; attempting to reconnect');
|
|
1259
|
+
this._recordRecentEvent('browser_disconnected', {
|
|
1260
|
+
url: this.currentUrl,
|
|
1261
|
+
});
|
|
1262
|
+
const reconnectTask = this._auto_reconnect();
|
|
1263
|
+
this._reconnectTask = reconnectTask;
|
|
1264
|
+
void reconnectTask.finally(() => {
|
|
1265
|
+
if (this._reconnectTask === reconnectTask) {
|
|
1266
|
+
this._reconnectTask = null;
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
async _restorePagesAfterReconnect(preferredUrl, preferredTabIndex) {
|
|
1271
|
+
if (!this.browser_context) {
|
|
1272
|
+
this.agent_current_page = null;
|
|
1273
|
+
this.human_current_page = null;
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
let pages = this.browser_context.pages?.() ?? [];
|
|
1277
|
+
if (!pages.length && typeof this.browser_context.newPage === 'function') {
|
|
1278
|
+
const createdPage = await this.browser_context.newPage();
|
|
1279
|
+
if (createdPage) {
|
|
1280
|
+
pages = this.browser_context.pages?.() ?? [createdPage];
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
this.tabPages = new Map();
|
|
1284
|
+
this.agent_current_page = null;
|
|
1285
|
+
this.human_current_page = null;
|
|
1286
|
+
this._syncTabsWithBrowserPages();
|
|
1287
|
+
if (!pages.length) {
|
|
1288
|
+
this.currentTabIndex = 0;
|
|
1289
|
+
this.currentUrl = normalize_url(preferredUrl ?? 'about:blank');
|
|
1290
|
+
this.currentTitle = this.currentUrl;
|
|
1291
|
+
if (!this._tabs.length) {
|
|
1292
|
+
this._tabs = [
|
|
1293
|
+
this._createTabInfo({
|
|
1294
|
+
page_id: this._tabCounter++,
|
|
1295
|
+
url: this.currentUrl,
|
|
1296
|
+
title: this.currentTitle,
|
|
1297
|
+
}),
|
|
1298
|
+
];
|
|
1299
|
+
}
|
|
1300
|
+
this._syncSessionManagerFromTabs();
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
const normalizedPreferredUrl = typeof preferredUrl === 'string' && preferredUrl.trim().length > 0
|
|
1304
|
+
? normalize_url(preferredUrl)
|
|
1305
|
+
: null;
|
|
1306
|
+
const pageByUrl = normalizedPreferredUrl == null
|
|
1307
|
+
? null
|
|
1308
|
+
: (pages.find((page) => {
|
|
1309
|
+
try {
|
|
1310
|
+
return normalize_url(page.url()) === normalizedPreferredUrl;
|
|
1311
|
+
}
|
|
1312
|
+
catch {
|
|
1313
|
+
return false;
|
|
1314
|
+
}
|
|
1315
|
+
}) ?? null);
|
|
1316
|
+
const clampedIndex = preferredTabIndex >= 0 && preferredTabIndex < pages.length
|
|
1317
|
+
? preferredTabIndex
|
|
1318
|
+
: 0;
|
|
1319
|
+
const nextPage = pageByUrl ?? pages[clampedIndex] ?? pages[0] ?? null;
|
|
1320
|
+
const nextTabIndex = nextPage
|
|
1321
|
+
? this._tabs.findIndex((tab) => this.tabPages.get(tab.page_id) === nextPage)
|
|
1322
|
+
: -1;
|
|
1323
|
+
if (nextTabIndex >= 0) {
|
|
1324
|
+
this.currentTabIndex = nextTabIndex;
|
|
1325
|
+
}
|
|
1326
|
+
this._setActivePage(nextPage);
|
|
1327
|
+
this.human_current_page = nextPage;
|
|
1328
|
+
await this._syncCurrentTabFromPage(nextPage);
|
|
1329
|
+
}
|
|
1330
|
+
async reconnect(options = {}) {
|
|
1331
|
+
if (!this._usesRemoteBrowserConnection()) {
|
|
1332
|
+
throw new Error('Cannot reconnect without a remote browser connection');
|
|
1333
|
+
}
|
|
1334
|
+
const preferredUrl = typeof options.preferred_url === 'string'
|
|
1335
|
+
? options.preferred_url
|
|
1336
|
+
: this.currentUrl;
|
|
1337
|
+
const preferredTabIndex = typeof options.preferred_tab_index === 'number'
|
|
1338
|
+
? options.preferred_tab_index
|
|
1339
|
+
: this.currentTabIndex;
|
|
1340
|
+
this._detachRemoteDisconnectHandler();
|
|
1341
|
+
this.cachedBrowserState = null;
|
|
1342
|
+
this.currentPageLoadingStatus = null;
|
|
1343
|
+
this.browser = null;
|
|
1344
|
+
this.browser_context = null;
|
|
1345
|
+
this.agent_current_page = null;
|
|
1346
|
+
this.human_current_page = null;
|
|
1347
|
+
this._dialogHandlersAttached = new WeakSet();
|
|
1348
|
+
this.session_manager.clear();
|
|
1349
|
+
const playwright = this.playwright ?? (await async_playwright());
|
|
1350
|
+
this.playwright = playwright;
|
|
1351
|
+
this.browser = await this._connectToConfiguredBrowser(playwright);
|
|
1352
|
+
this.ownsBrowserResources = false;
|
|
1353
|
+
this.browser_context = await this._ensureBrowserContextFromBrowser(this.browser);
|
|
1354
|
+
await this._applyConfiguredExtraHttpHeaders();
|
|
1355
|
+
await this._restorePagesAfterReconnect(preferredUrl, preferredTabIndex);
|
|
1356
|
+
this._attachRemoteDisconnectHandler(this.browser);
|
|
1357
|
+
this.initialized = true;
|
|
1358
|
+
this._recordRecentEvent('browser_reconnected', {
|
|
1359
|
+
url: this.currentUrl,
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
async _auto_reconnect(maxAttempts = 3) {
|
|
1363
|
+
if (this._reconnecting || !this._usesRemoteBrowserConnection()) {
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
this._reconnecting = true;
|
|
1367
|
+
this._beginReconnectWait();
|
|
1368
|
+
const startTime = Date.now();
|
|
1369
|
+
const preferredUrl = this.currentUrl;
|
|
1370
|
+
const preferredTabIndex = this.currentTabIndex;
|
|
1371
|
+
try {
|
|
1372
|
+
await this.event_bus.dispatch(new BrowserStoppedEvent({
|
|
1373
|
+
reason: 'connection_lost',
|
|
1374
|
+
}));
|
|
1375
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
1376
|
+
if (this._intentionalStop) {
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
await this.event_bus.dispatch(new BrowserReconnectingEvent({
|
|
1380
|
+
cdp_url: this.cdp_url ?? this.wss_url ?? 'remote',
|
|
1381
|
+
attempt,
|
|
1382
|
+
max_attempts: maxAttempts,
|
|
1383
|
+
}));
|
|
1384
|
+
try {
|
|
1385
|
+
await Promise.race([
|
|
1386
|
+
this.reconnect({
|
|
1387
|
+
preferred_url: preferredUrl,
|
|
1388
|
+
preferred_tab_index: preferredTabIndex,
|
|
1389
|
+
}),
|
|
1390
|
+
new Promise((_, reject) => {
|
|
1391
|
+
setTimeout(() => {
|
|
1392
|
+
reject(new Error(`Reconnect attempt timed out after ${Math.round(REMOTE_RECONNECT_ATTEMPT_TIMEOUT_MS / 1000)}s`));
|
|
1393
|
+
}, REMOTE_RECONNECT_ATTEMPT_TIMEOUT_MS);
|
|
1394
|
+
}),
|
|
1395
|
+
]);
|
|
1396
|
+
if (this._intentionalStop) {
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
await this.event_bus.dispatch(new BrowserConnectedEvent({
|
|
1400
|
+
cdp_url: this.cdp_url ?? this.wss_url ?? 'remote',
|
|
1401
|
+
}));
|
|
1402
|
+
await this.event_bus.dispatch(new BrowserReconnectedEvent({
|
|
1403
|
+
cdp_url: this.cdp_url ?? this.wss_url ?? 'remote',
|
|
1404
|
+
attempt,
|
|
1405
|
+
downtime_seconds: (Date.now() - startTime) / 1000,
|
|
1406
|
+
}));
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
catch (error) {
|
|
1410
|
+
this.logger.warning(`Reconnect attempt ${attempt}/${maxAttempts} failed: ${error.message}`);
|
|
1411
|
+
if (attempt >= maxAttempts) {
|
|
1412
|
+
break;
|
|
1413
|
+
}
|
|
1414
|
+
const delayMs = REMOTE_RECONNECT_DELAYS_MS[attempt - 1] ??
|
|
1415
|
+
REMOTE_RECONNECT_DELAYS_MS[REMOTE_RECONNECT_DELAYS_MS.length - 1];
|
|
1416
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
await this.event_bus.dispatch(new BrowserErrorEvent({
|
|
1420
|
+
error_type: 'ReconnectionFailed',
|
|
1421
|
+
message: `Failed to reconnect after ${maxAttempts} attempts (${((Date.now() - startTime) / 1000).toFixed(1)}s)`,
|
|
1422
|
+
details: {
|
|
1423
|
+
cdp_url: this.cdp_url ?? this.wss_url ?? 'remote',
|
|
1424
|
+
max_attempts: maxAttempts,
|
|
1425
|
+
},
|
|
1426
|
+
}));
|
|
1427
|
+
}
|
|
1428
|
+
finally {
|
|
1429
|
+
this._reconnecting = false;
|
|
1430
|
+
this._endReconnectWait();
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
793
1433
|
_isSandboxLaunchError(error) {
|
|
794
1434
|
const message = error instanceof Error ? error.message : String(error);
|
|
795
1435
|
return (/no usable sandbox/i.test(message) ||
|
|
@@ -846,6 +1486,7 @@ export class BrowserSession {
|
|
|
846
1486
|
}
|
|
847
1487
|
async start() {
|
|
848
1488
|
this.attach_default_watchdogs();
|
|
1489
|
+
this._intentionalStop = false;
|
|
849
1490
|
if (this.initialized) {
|
|
850
1491
|
return this;
|
|
851
1492
|
}
|
|
@@ -878,12 +1519,11 @@ export class BrowserSession {
|
|
|
878
1519
|
const playwright = this.playwright ?? (await async_playwright());
|
|
879
1520
|
this.playwright = playwright;
|
|
880
1521
|
if (this.cdp_url) {
|
|
881
|
-
this.browser = await
|
|
1522
|
+
this.browser = await this._connectToConfiguredBrowser(playwright);
|
|
882
1523
|
this.ownsBrowserResources = false;
|
|
883
1524
|
}
|
|
884
1525
|
else if (this.wss_url) {
|
|
885
|
-
|
|
886
|
-
this.browser = await playwright.chromium.connect(this.wss_url, connectOptions ?? {});
|
|
1526
|
+
this.browser = await this._connectToConfiguredBrowser(playwright);
|
|
887
1527
|
this.ownsBrowserResources = false;
|
|
888
1528
|
}
|
|
889
1529
|
else {
|
|
@@ -905,14 +1545,11 @@ export class BrowserSession {
|
|
|
905
1545
|
if (existingContexts.length > 0) {
|
|
906
1546
|
this.browser_context = existingContexts[0] ?? null;
|
|
907
1547
|
}
|
|
908
|
-
else if (typeof this.browser?.newContext === 'function') {
|
|
909
|
-
const contextOptions = this._toPlaywrightOptions(this.browser_profile.kwargs_for_new_context());
|
|
910
|
-
this.browser_context = await this.browser.newContext(contextOptions ?? {});
|
|
911
|
-
}
|
|
912
1548
|
else {
|
|
913
|
-
this.browser_context =
|
|
1549
|
+
this.browser_context = await this._ensureBrowserContextFromBrowser(this.browser);
|
|
914
1550
|
}
|
|
915
1551
|
}
|
|
1552
|
+
await this._applyConfiguredExtraHttpHeaders();
|
|
916
1553
|
await ensurePage();
|
|
917
1554
|
if (!this.human_current_page ||
|
|
918
1555
|
this.human_current_page.isClosed?.()) {
|
|
@@ -938,6 +1575,7 @@ export class BrowserSession {
|
|
|
938
1575
|
this.initialized = true;
|
|
939
1576
|
this._recordRecentEvent('browser_started', { url: this.currentUrl });
|
|
940
1577
|
this.logger.debug(`Started ${this.describe()} with profile ${this.browser_profile.toString()}`);
|
|
1578
|
+
this._attachRemoteDisconnectHandler(this.browser);
|
|
941
1579
|
await this.event_bus.dispatch(new BrowserConnectedEvent({
|
|
942
1580
|
cdp_url: this.cdp_url ?? this.wss_url ?? 'playwright',
|
|
943
1581
|
}));
|
|
@@ -964,7 +1602,7 @@ export class BrowserSession {
|
|
|
964
1602
|
// Connect to browser via CDP
|
|
965
1603
|
try {
|
|
966
1604
|
const playwright = await import('playwright');
|
|
967
|
-
const browser = await
|
|
1605
|
+
const browser = await this._connectToConfiguredBrowser(playwright);
|
|
968
1606
|
this.browser = browser;
|
|
969
1607
|
this.playwright = playwright;
|
|
970
1608
|
// Get or create context
|
|
@@ -975,6 +1613,7 @@ export class BrowserSession {
|
|
|
975
1613
|
else {
|
|
976
1614
|
this.browser_context = (await browser.newContext());
|
|
977
1615
|
}
|
|
1616
|
+
await this._applyConfiguredExtraHttpHeaders();
|
|
978
1617
|
// Get or create page
|
|
979
1618
|
if (!this.browser_context) {
|
|
980
1619
|
throw new Error('Browser context not available');
|
|
@@ -991,6 +1630,7 @@ export class BrowserSession {
|
|
|
991
1630
|
}
|
|
992
1631
|
// We don't own this browser since we're connecting to existing one
|
|
993
1632
|
this.ownsBrowserResources = false;
|
|
1633
|
+
this._attachRemoteDisconnectHandler(this.browser);
|
|
994
1634
|
this.initialized = true;
|
|
995
1635
|
this.logger.info(`Successfully connected to browser PID ${browserPid}`);
|
|
996
1636
|
}
|
|
@@ -1025,6 +1665,11 @@ export class BrowserSession {
|
|
|
1025
1665
|
}
|
|
1026
1666
|
async _shutdown_browser_session() {
|
|
1027
1667
|
this.initialized = false;
|
|
1668
|
+
this._intentionalStop = true;
|
|
1669
|
+
this._reconnecting = false;
|
|
1670
|
+
this._endReconnectWait();
|
|
1671
|
+
this._reconnectTask = null;
|
|
1672
|
+
this._detachRemoteDisconnectHandler();
|
|
1028
1673
|
this.attachedAgentId = null;
|
|
1029
1674
|
this.attachedSharedAgentIds.clear();
|
|
1030
1675
|
const closeWithTimeout = async (label, operation, timeoutMs = 3000) => {
|
|
@@ -1101,7 +1746,7 @@ export class BrowserSession {
|
|
|
1101
1746
|
else {
|
|
1102
1747
|
try {
|
|
1103
1748
|
const domService = new DomService(page, this.logger);
|
|
1104
|
-
domState = await this._withAbort(domService.get_clickable_elements(), signal);
|
|
1749
|
+
domState = await this._withAbort(domService.get_clickable_elements(this.browser_profile.highlight_elements, -1, this.browser_profile.viewport_expansion), signal);
|
|
1105
1750
|
}
|
|
1106
1751
|
catch (error) {
|
|
1107
1752
|
if (this._isAbortError(error)) {
|
|
@@ -1110,6 +1755,29 @@ export class BrowserSession {
|
|
|
1110
1755
|
this.logger.debug(`Failed to build DOM tree: ${error.message}`);
|
|
1111
1756
|
domState = createEmptyDomState();
|
|
1112
1757
|
}
|
|
1758
|
+
const liveUrl = typeof page.url === 'function'
|
|
1759
|
+
? normalize_url(page.url())
|
|
1760
|
+
: this.currentUrl;
|
|
1761
|
+
const shouldRetryEmptyDom = Object.keys(domState.selector_map).length === 0 &&
|
|
1762
|
+
!this._is_new_tab_page(liveUrl) &&
|
|
1763
|
+
!liveUrl.toLowerCase().endsWith('.pdf');
|
|
1764
|
+
if (shouldRetryEmptyDom) {
|
|
1765
|
+
this.logger.debug(`Empty DOM detected for ${liveUrl}; retrying once`);
|
|
1766
|
+
await this._waitWithAbort(EMPTY_DOM_RETRY_DELAY_MS, signal);
|
|
1767
|
+
try {
|
|
1768
|
+
const retryDomService = new DomService(page, this.logger);
|
|
1769
|
+
const retriedDomState = await this._withAbort(retryDomService.get_clickable_elements(this.browser_profile.highlight_elements, -1, this.browser_profile.viewport_expansion), signal);
|
|
1770
|
+
if (Object.keys(retriedDomState.selector_map).length > 0) {
|
|
1771
|
+
domState = retriedDomState;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
catch (error) {
|
|
1775
|
+
if (this._isAbortError(error)) {
|
|
1776
|
+
throw error;
|
|
1777
|
+
}
|
|
1778
|
+
this.logger.debug(`Retry after empty DOM failed: ${error.message}`);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1113
1781
|
}
|
|
1114
1782
|
let screenshot = null;
|
|
1115
1783
|
if (options.include_screenshot && page?.screenshot) {
|
|
@@ -1176,6 +1844,9 @@ export class BrowserSession {
|
|
|
1176
1844
|
}
|
|
1177
1845
|
}
|
|
1178
1846
|
const pendingNetworkRequests = await this._getPendingNetworkRequests(page);
|
|
1847
|
+
if (page) {
|
|
1848
|
+
await this._syncCurrentTabFromPage(page);
|
|
1849
|
+
}
|
|
1179
1850
|
if (pageInfo &&
|
|
1180
1851
|
Number.isFinite(pageInfo.viewport_width) &&
|
|
1181
1852
|
Number.isFinite(pageInfo.viewport_height)) {
|
|
@@ -1225,6 +1896,7 @@ export class BrowserSession {
|
|
|
1225
1896
|
return summary;
|
|
1226
1897
|
}
|
|
1227
1898
|
async get_current_page() {
|
|
1899
|
+
this._syncTabsWithBrowserPages();
|
|
1228
1900
|
if (this.agent_current_page) {
|
|
1229
1901
|
return this.agent_current_page;
|
|
1230
1902
|
}
|
|
@@ -1275,6 +1947,7 @@ export class BrowserSession {
|
|
|
1275
1947
|
this._throwIfAborted(signal);
|
|
1276
1948
|
this._assert_url_allowed(url);
|
|
1277
1949
|
const normalized = normalize_url(url);
|
|
1950
|
+
let completedUrl = normalized;
|
|
1278
1951
|
const waitUntil = options.wait_until ?? 'domcontentloaded';
|
|
1279
1952
|
const timeoutMs = typeof options.timeout_ms === 'number' &&
|
|
1280
1953
|
Number.isFinite(options.timeout_ms)
|
|
@@ -1294,6 +1967,7 @@ export class BrowserSession {
|
|
|
1294
1967
|
await this._withAbort(page.goto(normalized, gotoOptions), signal);
|
|
1295
1968
|
const finalUrl = page.url();
|
|
1296
1969
|
this._assert_url_allowed(finalUrl);
|
|
1970
|
+
completedUrl = normalize_url(finalUrl);
|
|
1297
1971
|
await this._waitForStableNetwork(page, signal);
|
|
1298
1972
|
}
|
|
1299
1973
|
catch (error) {
|
|
@@ -1309,16 +1983,24 @@ export class BrowserSession {
|
|
|
1309
1983
|
}
|
|
1310
1984
|
}
|
|
1311
1985
|
this._throwIfAborted(signal);
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
this.
|
|
1986
|
+
if (page) {
|
|
1987
|
+
await this._syncCurrentTabFromPage(page);
|
|
1988
|
+
completedUrl = this.currentUrl || completedUrl;
|
|
1989
|
+
}
|
|
1990
|
+
else {
|
|
1991
|
+
this.currentUrl = normalized;
|
|
1992
|
+
this.currentTitle = normalized;
|
|
1993
|
+
if (this._tabs[this.currentTabIndex]) {
|
|
1994
|
+
this._tabs[this.currentTabIndex].url = normalized;
|
|
1995
|
+
this._tabs[this.currentTabIndex].title = normalized;
|
|
1996
|
+
}
|
|
1997
|
+
this._syncSessionManagerFromTabs();
|
|
1998
|
+
}
|
|
1999
|
+
if (this.historyStack[this.historyStack.length - 1] !== completedUrl) {
|
|
2000
|
+
this.historyStack.push(completedUrl);
|
|
1318
2001
|
}
|
|
1319
|
-
this._syncSessionManagerFromTabs();
|
|
1320
2002
|
this._setActivePage(page ?? null);
|
|
1321
|
-
this._recordRecentEvent('navigation_completed', { url:
|
|
2003
|
+
this._recordRecentEvent('navigation_completed', { url: completedUrl });
|
|
1322
2004
|
this.cachedBrowserState = null;
|
|
1323
2005
|
return this.agent_current_page;
|
|
1324
2006
|
}
|
|
@@ -1327,11 +2009,14 @@ export class BrowserSession {
|
|
|
1327
2009
|
this._throwIfAborted(signal);
|
|
1328
2010
|
this._assert_url_allowed(url);
|
|
1329
2011
|
const normalized = normalize_url(url);
|
|
2012
|
+
let completedUrl = normalized;
|
|
1330
2013
|
const waitUntil = options.wait_until ?? 'domcontentloaded';
|
|
1331
2014
|
const timeoutMs = typeof options.timeout_ms === 'number' &&
|
|
1332
2015
|
Number.isFinite(options.timeout_ms)
|
|
1333
2016
|
? Math.max(0, options.timeout_ms)
|
|
1334
2017
|
: null;
|
|
2018
|
+
const previousTabIndex = this.currentTabIndex;
|
|
2019
|
+
const previousTab = this._tabs[this.currentTabIndex] ?? null;
|
|
1335
2020
|
const newTab = this._createTabInfo({
|
|
1336
2021
|
page_id: this._tabCounter++,
|
|
1337
2022
|
url: normalized,
|
|
@@ -1342,11 +2027,6 @@ export class BrowserSession {
|
|
|
1342
2027
|
this.currentUrl = normalized;
|
|
1343
2028
|
this.currentTitle = normalized;
|
|
1344
2029
|
this.historyStack.push(normalized);
|
|
1345
|
-
this._recordRecentEvent('tab_created', {
|
|
1346
|
-
url: normalized,
|
|
1347
|
-
page_id: newTab.page_id,
|
|
1348
|
-
tab_id: newTab.tab_id,
|
|
1349
|
-
});
|
|
1350
2030
|
let page = null;
|
|
1351
2031
|
try {
|
|
1352
2032
|
page =
|
|
@@ -1362,6 +2042,7 @@ export class BrowserSession {
|
|
|
1362
2042
|
await this._withAbort(page.goto(normalized, gotoOptions), signal);
|
|
1363
2043
|
const finalUrl = page.url();
|
|
1364
2044
|
this._assert_url_allowed(finalUrl);
|
|
2045
|
+
completedUrl = normalize_url(finalUrl);
|
|
1365
2046
|
await this._waitForStableNetwork(page, signal);
|
|
1366
2047
|
}
|
|
1367
2048
|
}
|
|
@@ -1369,29 +2050,82 @@ export class BrowserSession {
|
|
|
1369
2050
|
if (this._isAbortError(error)) {
|
|
1370
2051
|
throw error;
|
|
1371
2052
|
}
|
|
2053
|
+
const message = error.message ?? 'Failed to open new tab';
|
|
1372
2054
|
this._recordRecentEvent('tab_navigation_failed', {
|
|
1373
2055
|
url: normalized,
|
|
1374
2056
|
page_id: newTab.page_id,
|
|
1375
2057
|
tab_id: newTab.tab_id,
|
|
1376
|
-
error_message:
|
|
2058
|
+
error_message: message,
|
|
1377
2059
|
});
|
|
1378
|
-
this.logger.debug(`Failed to open new tab via Playwright: ${
|
|
2060
|
+
this.logger.debug(`Failed to open new tab via Playwright: ${message}`);
|
|
2061
|
+
if (page?.close) {
|
|
2062
|
+
try {
|
|
2063
|
+
await page.close();
|
|
2064
|
+
}
|
|
2065
|
+
catch {
|
|
2066
|
+
// Ignore best-effort tab close failures during rollback.
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
this._tabs = this._tabs.filter((tab) => tab.page_id !== newTab.page_id);
|
|
2070
|
+
this.tabPages.delete(newTab.page_id);
|
|
2071
|
+
if (this.historyStack[this.historyStack.length - 1] === normalized) {
|
|
2072
|
+
this.historyStack.pop();
|
|
2073
|
+
}
|
|
2074
|
+
if (this._tabs.length > 0) {
|
|
2075
|
+
let restoredIndex = previousTab
|
|
2076
|
+
? this._tabs.findIndex((tab) => tab.page_id === previousTab.page_id)
|
|
2077
|
+
: -1;
|
|
2078
|
+
if (restoredIndex === -1) {
|
|
2079
|
+
restoredIndex = Math.min(previousTabIndex, this._tabs.length - 1);
|
|
2080
|
+
}
|
|
2081
|
+
this.currentTabIndex = Math.max(0, restoredIndex);
|
|
2082
|
+
const restoredTab = this._tabs[this.currentTabIndex];
|
|
2083
|
+
this.currentUrl = restoredTab.url;
|
|
2084
|
+
this.currentTitle = restoredTab.title;
|
|
2085
|
+
const restoredPage = this.tabPages.get(restoredTab.page_id) ?? null;
|
|
2086
|
+
this._setActivePage(restoredPage);
|
|
2087
|
+
await this._syncCurrentTabFromPage(restoredPage);
|
|
2088
|
+
}
|
|
2089
|
+
else {
|
|
2090
|
+
this.currentTabIndex = 0;
|
|
2091
|
+
this.currentUrl = 'about:blank';
|
|
2092
|
+
this.currentTitle = 'about:blank';
|
|
2093
|
+
this._setActivePage(null);
|
|
2094
|
+
}
|
|
2095
|
+
this._syncSessionManagerFromTabs();
|
|
2096
|
+
this.cachedBrowserState = null;
|
|
2097
|
+
throw new BrowserError(message);
|
|
1379
2098
|
}
|
|
1380
2099
|
this.tabPages.set(newTab.page_id, page);
|
|
1381
2100
|
this._syncSessionManagerFromTabs();
|
|
1382
2101
|
this._setActivePage(page);
|
|
2102
|
+
if (page) {
|
|
2103
|
+
await this._syncCurrentTabFromPage(page);
|
|
2104
|
+
completedUrl = this.currentUrl || completedUrl;
|
|
2105
|
+
}
|
|
2106
|
+
if (this.historyStack[this.historyStack.length - 1] === normalized) {
|
|
2107
|
+
this.historyStack[this.historyStack.length - 1] = completedUrl;
|
|
2108
|
+
}
|
|
2109
|
+
else if (this.historyStack[this.historyStack.length - 1] !== completedUrl) {
|
|
2110
|
+
this.historyStack.push(completedUrl);
|
|
2111
|
+
}
|
|
1383
2112
|
this.currentPageLoadingStatus = null;
|
|
1384
2113
|
if (!this.human_current_page) {
|
|
1385
2114
|
this.human_current_page = page;
|
|
1386
2115
|
}
|
|
2116
|
+
this._recordRecentEvent('tab_created', {
|
|
2117
|
+
url: completedUrl,
|
|
2118
|
+
page_id: newTab.page_id,
|
|
2119
|
+
tab_id: newTab.tab_id,
|
|
2120
|
+
});
|
|
1387
2121
|
this._recordRecentEvent('tab_ready', {
|
|
1388
|
-
url:
|
|
2122
|
+
url: completedUrl,
|
|
1389
2123
|
page_id: newTab.page_id,
|
|
1390
2124
|
tab_id: newTab.tab_id,
|
|
1391
2125
|
});
|
|
1392
2126
|
await this.event_bus.dispatch(new TabCreatedEvent({
|
|
1393
2127
|
target_id: newTab.target_id ?? newTab.tab_id ?? 'unknown_target',
|
|
1394
|
-
url:
|
|
2128
|
+
url: completedUrl,
|
|
1395
2129
|
}));
|
|
1396
2130
|
this.cachedBrowserState = null;
|
|
1397
2131
|
return this.agent_current_page;
|
|
@@ -1434,6 +2168,7 @@ export class BrowserSession {
|
|
|
1434
2168
|
async switch_to_tab(identifier, options = {}) {
|
|
1435
2169
|
const signal = options.signal ?? null;
|
|
1436
2170
|
this._throwIfAborted(signal);
|
|
2171
|
+
this._syncTabsWithBrowserPages();
|
|
1437
2172
|
const index = this._resolveTabIndex(identifier);
|
|
1438
2173
|
const tab = index >= 0 ? (this._tabs[index] ?? null) : null;
|
|
1439
2174
|
if (!tab) {
|
|
@@ -1473,6 +2208,7 @@ export class BrowserSession {
|
|
|
1473
2208
|
return page;
|
|
1474
2209
|
}
|
|
1475
2210
|
async close_tab(identifier) {
|
|
2211
|
+
this._syncTabsWithBrowserPages();
|
|
1476
2212
|
const index = this._resolveTabIndex(identifier);
|
|
1477
2213
|
if (index < 0 || index >= this._tabs.length) {
|
|
1478
2214
|
throw new Error(`Tab '${identifier}' does not exist`);
|
|
@@ -1887,31 +2623,34 @@ export class BrowserSession {
|
|
|
1887
2623
|
async go_back(options = {}) {
|
|
1888
2624
|
const signal = options.signal ?? null;
|
|
1889
2625
|
this._throwIfAborted(signal);
|
|
1890
|
-
|
|
2626
|
+
const page = await this._withAbort(this.get_current_page(), signal);
|
|
2627
|
+
if (!page?.goBack) {
|
|
1891
2628
|
return;
|
|
1892
2629
|
}
|
|
1893
|
-
const
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
throw error;
|
|
1901
|
-
}
|
|
1902
|
-
this.logger.debug(`Failed to navigate back: ${error.message}`);
|
|
2630
|
+
const previousUrl = this.currentUrl;
|
|
2631
|
+
try {
|
|
2632
|
+
await this._withAbort(page.goBack(), signal);
|
|
2633
|
+
}
|
|
2634
|
+
catch (error) {
|
|
2635
|
+
if (this._isAbortError(error)) {
|
|
2636
|
+
throw error;
|
|
1903
2637
|
}
|
|
2638
|
+
this.logger.debug(`Failed to navigate back: ${error.message}`);
|
|
1904
2639
|
}
|
|
1905
2640
|
this._throwIfAborted(signal);
|
|
1906
|
-
this.
|
|
1907
|
-
const
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
2641
|
+
await this._syncCurrentTabFromPage(page);
|
|
2642
|
+
const currentUrl = this.currentUrl;
|
|
2643
|
+
if (currentUrl && currentUrl !== previousUrl) {
|
|
2644
|
+
const existingIndex = this.historyStack.lastIndexOf(currentUrl);
|
|
2645
|
+
if (existingIndex !== -1) {
|
|
2646
|
+
this.historyStack = this.historyStack.slice(0, existingIndex + 1);
|
|
2647
|
+
}
|
|
2648
|
+
else if (this.historyStack[this.historyStack.length - 1] !== currentUrl) {
|
|
2649
|
+
this.historyStack.push(currentUrl);
|
|
2650
|
+
}
|
|
1913
2651
|
}
|
|
1914
|
-
this.
|
|
2652
|
+
this.cachedBrowserState = null;
|
|
2653
|
+
this._recordRecentEvent('navigation_back', { url: currentUrl });
|
|
1915
2654
|
}
|
|
1916
2655
|
async get_dom_element_by_index(_index, options = {}) {
|
|
1917
2656
|
const selectorMap = await this.get_selector_map(options);
|
|
@@ -2123,6 +2862,13 @@ export class BrowserSession {
|
|
|
2123
2862
|
await performClick();
|
|
2124
2863
|
}
|
|
2125
2864
|
await this._waitForLoad(page, 5000, signal);
|
|
2865
|
+
if (page) {
|
|
2866
|
+
await this._syncCurrentTabFromPage(page);
|
|
2867
|
+
if (this.historyStack[this.historyStack.length - 1] !== this.currentUrl) {
|
|
2868
|
+
this.historyStack.push(this.currentUrl);
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
this.cachedBrowserState = null;
|
|
2126
2872
|
return null;
|
|
2127
2873
|
}
|
|
2128
2874
|
async _waitForLoad(page, timeout = 5000, signal = null) {
|
|
@@ -2435,11 +3181,12 @@ export class BrowserSession {
|
|
|
2435
3181
|
}
|
|
2436
3182
|
// ==================== Screenshots ====================
|
|
2437
3183
|
/**
|
|
2438
|
-
* Take a screenshot of the current page
|
|
3184
|
+
* Take a screenshot of the current page.
|
|
2439
3185
|
* @param full_page Whether to capture the full scrollable page
|
|
3186
|
+
* @param clip Optional clip region for partial screenshots
|
|
2440
3187
|
* @returns Base64 encoded PNG screenshot
|
|
2441
3188
|
*/
|
|
2442
|
-
async take_screenshot(full_page = false) {
|
|
3189
|
+
async take_screenshot(full_page = false, clip = null) {
|
|
2443
3190
|
const page = await this.get_current_page();
|
|
2444
3191
|
if (!page) {
|
|
2445
3192
|
throw new Error('No page available for screenshot');
|
|
@@ -2470,11 +3217,21 @@ export class BrowserSession {
|
|
|
2470
3217
|
// Create CDP session for the screenshot
|
|
2471
3218
|
cdp_session = await this.get_or_create_cdp_session(page);
|
|
2472
3219
|
// Capture screenshot via CDP
|
|
2473
|
-
const
|
|
2474
|
-
captureBeyondViewport:
|
|
3220
|
+
const screenshotParams = {
|
|
3221
|
+
captureBeyondViewport: full_page,
|
|
2475
3222
|
fromSurface: true,
|
|
2476
3223
|
format: 'png',
|
|
2477
|
-
}
|
|
3224
|
+
};
|
|
3225
|
+
if (clip) {
|
|
3226
|
+
screenshotParams.clip = {
|
|
3227
|
+
x: clip.x,
|
|
3228
|
+
y: clip.y,
|
|
3229
|
+
width: clip.width,
|
|
3230
|
+
height: clip.height,
|
|
3231
|
+
scale: 1,
|
|
3232
|
+
};
|
|
3233
|
+
}
|
|
3234
|
+
const screenshot_response = await cdp_session.send('Page.captureScreenshot', screenshotParams);
|
|
2478
3235
|
const screenshot_b64 = screenshot_response.data;
|
|
2479
3236
|
if (!screenshot_b64) {
|
|
2480
3237
|
throw new Error(`CDP returned empty screenshot data for page ${url}`);
|
|
@@ -2548,22 +3305,34 @@ export class BrowserSession {
|
|
|
2548
3305
|
if (!this.browser_context) {
|
|
2549
3306
|
return [];
|
|
2550
3307
|
}
|
|
3308
|
+
this._syncTabsWithBrowserPages();
|
|
2551
3309
|
const tabs_info = [];
|
|
2552
|
-
const
|
|
2553
|
-
|
|
2554
|
-
const page =
|
|
2555
|
-
const tab_id =
|
|
2556
|
-
|
|
2557
|
-
|
|
3310
|
+
for (const tab of this._tabs) {
|
|
3311
|
+
const page_id = tab.page_id;
|
|
3312
|
+
const page = this.tabPages.get(page_id) ?? null;
|
|
3313
|
+
const tab_id = tab.tab_id || this._formatTabId(page_id);
|
|
3314
|
+
if (!tab.tab_id) {
|
|
3315
|
+
tab.tab_id = tab_id;
|
|
3316
|
+
}
|
|
3317
|
+
this._attachDialogHandler(page);
|
|
3318
|
+
let currentUrl = tab.url;
|
|
3319
|
+
if (page?.url) {
|
|
3320
|
+
try {
|
|
3321
|
+
currentUrl = normalize_url(page.url());
|
|
3322
|
+
}
|
|
3323
|
+
catch {
|
|
3324
|
+
// Keep tab url fallback when page url is unavailable.
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
2558
3327
|
// Skip chrome:// pages and new tab pages
|
|
2559
|
-
const isNewTab =
|
|
2560
|
-
|
|
2561
|
-
if (isNewTab ||
|
|
3328
|
+
const isNewTab = currentUrl === 'about:blank' ||
|
|
3329
|
+
currentUrl.startsWith('chrome://newtab');
|
|
3330
|
+
if (isNewTab || currentUrl.startsWith('chrome://')) {
|
|
2562
3331
|
if (isNewTab) {
|
|
2563
3332
|
tabs_info.push({
|
|
2564
3333
|
page_id,
|
|
2565
3334
|
tab_id,
|
|
2566
|
-
url:
|
|
3335
|
+
url: currentUrl,
|
|
2567
3336
|
title: 'ignore this tab and do not use it',
|
|
2568
3337
|
});
|
|
2569
3338
|
}
|
|
@@ -2571,28 +3340,31 @@ export class BrowserSession {
|
|
|
2571
3340
|
tabs_info.push({
|
|
2572
3341
|
page_id,
|
|
2573
3342
|
tab_id,
|
|
2574
|
-
url:
|
|
2575
|
-
title:
|
|
3343
|
+
url: currentUrl,
|
|
3344
|
+
title: currentUrl,
|
|
2576
3345
|
});
|
|
2577
3346
|
}
|
|
2578
3347
|
continue;
|
|
2579
3348
|
}
|
|
2580
3349
|
// Normal pages - try to get title with timeout
|
|
2581
3350
|
try {
|
|
3351
|
+
if (!page?.title) {
|
|
3352
|
+
throw new Error('page_title_unavailable');
|
|
3353
|
+
}
|
|
2582
3354
|
const titlePromise = page.title();
|
|
2583
3355
|
const timeoutPromise = new Promise((_, reject) => {
|
|
2584
3356
|
setTimeout(() => reject(new Error('timeout')), 2000);
|
|
2585
3357
|
});
|
|
2586
3358
|
const title = await Promise.race([titlePromise, timeoutPromise]);
|
|
2587
|
-
tabs_info.push({ page_id, tab_id, url:
|
|
3359
|
+
tabs_info.push({ page_id, tab_id, url: currentUrl, title });
|
|
2588
3360
|
}
|
|
2589
3361
|
catch (error) {
|
|
2590
|
-
this.logger.debug(`⚠️ Failed to get tab info for tab #${page_id}: ${
|
|
3362
|
+
this.logger.debug(`⚠️ Failed to get tab info for tab #${page_id}: ${currentUrl} (using fallback title)`);
|
|
2591
3363
|
if (isNewTab) {
|
|
2592
3364
|
tabs_info.push({
|
|
2593
3365
|
page_id,
|
|
2594
3366
|
tab_id,
|
|
2595
|
-
url:
|
|
3367
|
+
url: currentUrl,
|
|
2596
3368
|
title: 'ignore this tab and do not use it',
|
|
2597
3369
|
});
|
|
2598
3370
|
}
|
|
@@ -2600,8 +3372,8 @@ export class BrowserSession {
|
|
|
2600
3372
|
tabs_info.push({
|
|
2601
3373
|
page_id,
|
|
2602
3374
|
tab_id,
|
|
2603
|
-
url:
|
|
2604
|
-
title:
|
|
3375
|
+
url: currentUrl,
|
|
3376
|
+
title: tab.title || currentUrl,
|
|
2605
3377
|
});
|
|
2606
3378
|
}
|
|
2607
3379
|
}
|
|
@@ -2966,6 +3738,15 @@ export class BrowserSession {
|
|
|
2966
3738
|
}
|
|
2967
3739
|
return [host, `www.${host}`];
|
|
2968
3740
|
}
|
|
3741
|
+
_setEntryMatchesUrl(domains, hostVariant, hostAlt, protocol) {
|
|
3742
|
+
const matchedHost = domains.has(hostVariant) || domains.has(hostAlt);
|
|
3743
|
+
if (!matchedHost) {
|
|
3744
|
+
return false;
|
|
3745
|
+
}
|
|
3746
|
+
// Set-optimized entries are exact hostnames without explicit schemes,
|
|
3747
|
+
// so keep parity with pattern matching default: https-only.
|
|
3748
|
+
return protocol.toLowerCase() === 'https:';
|
|
3749
|
+
}
|
|
2969
3750
|
/**
|
|
2970
3751
|
* Check if page is displaying a PDF
|
|
2971
3752
|
*/
|
|
@@ -3258,7 +4039,7 @@ export class BrowserSession {
|
|
|
3258
4039
|
((Array.isArray(allowedDomains) && allowedDomains.length > 0) ||
|
|
3259
4040
|
(allowedDomains instanceof Set && allowedDomains.size > 0))) {
|
|
3260
4041
|
if (allowedDomains instanceof Set) {
|
|
3261
|
-
if (
|
|
4042
|
+
if (this._setEntryMatchesUrl(allowedDomains, hostVariant, hostAlt, parsed.protocol)) {
|
|
3262
4043
|
return null;
|
|
3263
4044
|
}
|
|
3264
4045
|
}
|
|
@@ -3281,8 +4062,7 @@ export class BrowserSession {
|
|
|
3281
4062
|
((Array.isArray(prohibitedDomains) && prohibitedDomains.length > 0) ||
|
|
3282
4063
|
(prohibitedDomains instanceof Set && prohibitedDomains.size > 0))) {
|
|
3283
4064
|
if (prohibitedDomains instanceof Set) {
|
|
3284
|
-
if (
|
|
3285
|
-
prohibitedDomains.has(hostAlt)) {
|
|
4065
|
+
if (this._setEntryMatchesUrl(prohibitedDomains, hostVariant, hostAlt, parsed.protocol)) {
|
|
3286
4066
|
return 'in_prohibited_domains';
|
|
3287
4067
|
}
|
|
3288
4068
|
}
|
|
@@ -3405,57 +4185,31 @@ export class BrowserSession {
|
|
|
3405
4185
|
// Check if downloads are enabled
|
|
3406
4186
|
const downloads_path = this.browser_profile.downloads_path;
|
|
3407
4187
|
if (downloads_path) {
|
|
4188
|
+
fs.mkdirSync(downloads_path, { recursive: true });
|
|
4189
|
+
// Try to detect file download.
|
|
4190
|
+
const download_promise = page.waitForEvent('download', {
|
|
4191
|
+
timeout: 5000,
|
|
4192
|
+
});
|
|
4193
|
+
// Click failures should bubble to the caller.
|
|
3408
4194
|
try {
|
|
3409
|
-
// Try to detect file download
|
|
3410
|
-
const download_promise = page.waitForEvent('download', {
|
|
3411
|
-
timeout: 5000,
|
|
3412
|
-
});
|
|
3413
|
-
// Perform the click
|
|
3414
4195
|
await element_handle.click();
|
|
3415
|
-
// Wait for download or timeout
|
|
3416
|
-
const download = await download_promise;
|
|
3417
|
-
// Save the downloaded file
|
|
3418
|
-
const suggested_filename = download.suggestedFilename();
|
|
3419
|
-
const unique_filename = await BrowserSession.get_unique_filename(downloads_path, suggested_filename);
|
|
3420
|
-
const download_path = path.join(downloads_path, unique_filename);
|
|
3421
|
-
const download_guid = uuid7str();
|
|
3422
|
-
const download_url = typeof download.url === 'function'
|
|
3423
|
-
? download.url()
|
|
3424
|
-
: (this.currentUrl ?? '');
|
|
3425
|
-
await this.event_bus.dispatch(new DownloadStartedEvent({
|
|
3426
|
-
guid: download_guid,
|
|
3427
|
-
url: download_url,
|
|
3428
|
-
suggested_filename,
|
|
3429
|
-
auto_download: false,
|
|
3430
|
-
}));
|
|
3431
|
-
await download.saveAs(download_path);
|
|
3432
|
-
this.logger.info(`⬇️ Downloaded file to: ${download_path}`);
|
|
3433
|
-
const stats = fs.existsSync(download_path)
|
|
3434
|
-
? fs.statSync(download_path)
|
|
3435
|
-
: null;
|
|
3436
|
-
await this.event_bus.dispatch(new DownloadProgressEvent({
|
|
3437
|
-
guid: download_guid,
|
|
3438
|
-
received_bytes: stats?.size ?? 0,
|
|
3439
|
-
total_bytes: stats?.size ?? 0,
|
|
3440
|
-
state: 'completed',
|
|
3441
|
-
}));
|
|
3442
|
-
const fileDownloadedResult = await this.event_bus.dispatch(new FileDownloadedEvent({
|
|
3443
|
-
guid: download_guid,
|
|
3444
|
-
url: download_url,
|
|
3445
|
-
path: download_path,
|
|
3446
|
-
file_name: unique_filename,
|
|
3447
|
-
file_size: stats?.size ?? 0,
|
|
3448
|
-
file_type: path.extname(unique_filename).replace('.', '') || null,
|
|
3449
|
-
mime_type: null,
|
|
3450
|
-
auto_download: false,
|
|
3451
|
-
}));
|
|
3452
|
-
if (fileDownloadedResult.handler_results.length === 0) {
|
|
3453
|
-
this.add_downloaded_file(download_path);
|
|
3454
|
-
}
|
|
3455
|
-
return download_path;
|
|
3456
4196
|
}
|
|
3457
4197
|
catch (error) {
|
|
3458
|
-
|
|
4198
|
+
void download_promise.catch(() => undefined);
|
|
4199
|
+
throw error;
|
|
4200
|
+
}
|
|
4201
|
+
let download;
|
|
4202
|
+
try {
|
|
4203
|
+
download = await download_promise;
|
|
4204
|
+
}
|
|
4205
|
+
catch (error) {
|
|
4206
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4207
|
+
const isDownloadTimeout = error instanceof Error &&
|
|
4208
|
+
(error.name === 'TimeoutError' ||
|
|
4209
|
+
message.toLowerCase().includes('timeout'));
|
|
4210
|
+
if (!isDownloadTimeout) {
|
|
4211
|
+
throw error;
|
|
4212
|
+
}
|
|
3459
4213
|
this.logger.debug('No download triggered within timeout. Checking navigation...');
|
|
3460
4214
|
try {
|
|
3461
4215
|
await page.waitForLoadState();
|
|
@@ -3463,7 +4217,47 @@ export class BrowserSession {
|
|
|
3463
4217
|
catch (e) {
|
|
3464
4218
|
this.logger.warning(`Navigation check failed: ${e.message}`);
|
|
3465
4219
|
}
|
|
4220
|
+
return null;
|
|
3466
4221
|
}
|
|
4222
|
+
// Save the downloaded file.
|
|
4223
|
+
const suggested_filename = download.suggestedFilename();
|
|
4224
|
+
const unique_filename = await BrowserSession.get_unique_filename(downloads_path, suggested_filename);
|
|
4225
|
+
const download_path = path.join(downloads_path, unique_filename);
|
|
4226
|
+
const download_guid = uuid7str();
|
|
4227
|
+
const download_url = typeof download.url === 'function'
|
|
4228
|
+
? download.url()
|
|
4229
|
+
: (this.currentUrl ?? '');
|
|
4230
|
+
await this.event_bus.dispatch(new DownloadStartedEvent({
|
|
4231
|
+
guid: download_guid,
|
|
4232
|
+
url: download_url,
|
|
4233
|
+
suggested_filename,
|
|
4234
|
+
auto_download: false,
|
|
4235
|
+
}));
|
|
4236
|
+
await download.saveAs(download_path);
|
|
4237
|
+
this.logger.info(`⬇️ Downloaded file to: ${download_path}`);
|
|
4238
|
+
const stats = fs.existsSync(download_path)
|
|
4239
|
+
? fs.statSync(download_path)
|
|
4240
|
+
: null;
|
|
4241
|
+
await this.event_bus.dispatch(new DownloadProgressEvent({
|
|
4242
|
+
guid: download_guid,
|
|
4243
|
+
received_bytes: stats?.size ?? 0,
|
|
4244
|
+
total_bytes: stats?.size ?? 0,
|
|
4245
|
+
state: 'completed',
|
|
4246
|
+
}));
|
|
4247
|
+
const fileDownloadedResult = await this.event_bus.dispatch(new FileDownloadedEvent({
|
|
4248
|
+
guid: download_guid,
|
|
4249
|
+
url: download_url,
|
|
4250
|
+
path: download_path,
|
|
4251
|
+
file_name: unique_filename,
|
|
4252
|
+
file_size: stats?.size ?? 0,
|
|
4253
|
+
file_type: path.extname(unique_filename).replace('.', '') || null,
|
|
4254
|
+
mime_type: null,
|
|
4255
|
+
auto_download: false,
|
|
4256
|
+
}));
|
|
4257
|
+
if (fileDownloadedResult.handler_results.length === 0) {
|
|
4258
|
+
this.add_downloaded_file(download_path);
|
|
4259
|
+
}
|
|
4260
|
+
return download_path;
|
|
3467
4261
|
}
|
|
3468
4262
|
else {
|
|
3469
4263
|
// No downloads path configured, just click
|
|
@@ -3481,15 +4275,33 @@ export class BrowserSession {
|
|
|
3481
4275
|
}
|
|
3482
4276
|
try {
|
|
3483
4277
|
await page.evaluate(() => {
|
|
3484
|
-
|
|
4278
|
+
const pageWindow = window;
|
|
4279
|
+
const cleanupFunctions = Array.isArray(pageWindow._highlightCleanupFunctions)
|
|
4280
|
+
? pageWindow._highlightCleanupFunctions
|
|
4281
|
+
: [];
|
|
4282
|
+
for (const cleanupFn of cleanupFunctions) {
|
|
4283
|
+
try {
|
|
4284
|
+
if (typeof cleanupFn === 'function') {
|
|
4285
|
+
cleanupFn();
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
catch {
|
|
4289
|
+
// Ignore callback cleanup failures.
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4292
|
+
pageWindow._highlightCleanupFunctions = [];
|
|
4293
|
+
const containers = document.querySelectorAll('#playwright-highlight-container');
|
|
4294
|
+
containers.forEach((element) => element.remove());
|
|
4295
|
+
const labels = document.querySelectorAll('.playwright-highlight-label');
|
|
4296
|
+
labels.forEach((element) => element.remove());
|
|
4297
|
+
// Backward compatibility with legacy selectors.
|
|
3485
4298
|
const highlights = document.querySelectorAll('.browser-use-highlight');
|
|
3486
|
-
highlights.forEach((
|
|
3487
|
-
// Remove inline highlight styles
|
|
4299
|
+
highlights.forEach((element) => element.remove());
|
|
3488
4300
|
const styled = document.querySelectorAll('[style*="browser-use"]');
|
|
3489
|
-
styled.forEach((
|
|
3490
|
-
if (
|
|
3491
|
-
|
|
3492
|
-
|
|
4301
|
+
styled.forEach((element) => {
|
|
4302
|
+
if (element.style) {
|
|
4303
|
+
element.style.outline = '';
|
|
4304
|
+
element.style.border = '';
|
|
3493
4305
|
}
|
|
3494
4306
|
});
|
|
3495
4307
|
});
|