browser-use 0.6.1 → 0.7.1
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/README.md +24 -18
- package/dist/actor/element.js +24 -3
- package/dist/actor/mouse.js +21 -3
- package/dist/actor/page.js +33 -11
- package/dist/agent/gif.js +28 -3
- package/dist/agent/message-manager/service.js +2 -22
- package/dist/agent/message-manager/utils.js +15 -2
- package/dist/agent/message-manager/views.d.ts +7 -7
- package/dist/agent/message-manager/views.js +1 -0
- package/dist/agent/prompts.d.ts +3 -0
- package/dist/agent/prompts.js +22 -12
- package/dist/agent/service.d.ts +9 -1
- package/dist/agent/service.js +204 -79
- package/dist/agent/system_prompt.md +12 -11
- package/dist/agent/system_prompt_anthropic_flash.md +6 -5
- package/dist/agent/system_prompt_no_thinking.md +12 -11
- package/dist/agent/views.d.ts +2 -0
- package/dist/agent/views.js +48 -36
- package/dist/browser/extensions.js +20 -10
- package/dist/browser/profile.d.ts +4 -0
- package/dist/browser/profile.js +107 -4
- package/dist/browser/session.d.ts +28 -1
- package/dist/browser/session.js +1436 -528
- package/dist/browser/watchdogs/default-action-watchdog.js +32 -3
- package/dist/browser/watchdogs/downloads-watchdog.d.ts +4 -0
- package/dist/browser/watchdogs/downloads-watchdog.js +105 -9
- package/dist/browser/watchdogs/har-recording-watchdog.d.ts +1 -0
- package/dist/browser/watchdogs/har-recording-watchdog.js +54 -2
- package/dist/browser/watchdogs/permissions-watchdog.d.ts +5 -0
- package/dist/browser/watchdogs/permissions-watchdog.js +106 -3
- package/dist/browser/watchdogs/recording-watchdog.d.ts +2 -0
- package/dist/browser/watchdogs/recording-watchdog.js +54 -2
- package/dist/browser/watchdogs/security-watchdog.d.ts +1 -0
- package/dist/browser/watchdogs/security-watchdog.js +47 -7
- package/dist/browser/watchdogs/storage-state-watchdog.d.ts +6 -0
- package/dist/browser/watchdogs/storage-state-watchdog.js +206 -14
- package/dist/cli.d.ts +13 -2
- package/dist/cli.js +190 -9
- package/dist/code-use/namespace.js +52 -7
- package/dist/code-use/notebook-export.js +18 -2
- package/dist/code-use/service.js +1 -0
- package/dist/config.js +26 -4
- package/dist/controller/action-timeout.d.ts +9 -0
- package/dist/controller/action-timeout.js +95 -0
- package/dist/controller/registry/service.d.ts +1 -0
- package/dist/controller/registry/service.js +28 -1
- package/dist/controller/service.d.ts +2 -1
- package/dist/controller/service.js +494 -329
- package/dist/entrypoint.d.ts +1 -0
- package/dist/entrypoint.js +27 -0
- package/dist/filesystem/file-system.js +38 -8
- package/dist/integrations/gmail/service.js +30 -6
- package/dist/llm/browser-use/chat.js +2 -2
- package/dist/llm/codex/auth.d.ts +118 -0
- package/dist/llm/codex/auth.js +599 -0
- package/dist/llm/codex/chat.d.ts +70 -0
- package/dist/llm/codex/chat.js +392 -0
- package/dist/llm/codex/index.d.ts +2 -0
- package/dist/llm/codex/index.js +2 -0
- package/dist/llm/google/chat.js +18 -1
- package/dist/logging-config.js +22 -11
- package/dist/mcp/client.d.ts +1 -0
- package/dist/mcp/client.js +12 -10
- package/dist/mcp/redaction.d.ts +3 -0
- package/dist/mcp/redaction.js +132 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +64 -22
- package/dist/screenshots/service.js +25 -2
- package/dist/skill-cli/direct.d.ts +4 -1
- package/dist/skill-cli/direct.js +263 -66
- package/dist/skill-cli/server.d.ts +1 -0
- package/dist/skill-cli/server.js +115 -25
- package/dist/skill-cli/tunnel.d.ts +1 -0
- package/dist/skill-cli/tunnel.js +16 -4
- package/dist/sync/auth.js +22 -9
- package/dist/telemetry/service.js +21 -2
- package/dist/telemetry/views.js +31 -8
- package/dist/tokens/custom-pricing.js +2 -2
- package/dist/tokens/openrouter-pricing.d.ts +11 -0
- package/dist/tokens/openrouter-pricing.js +102 -0
- package/dist/tokens/service.js +20 -16
- package/dist/utils.d.ts +3 -1
- package/dist/utils.js +3 -1
- package/package.json +68 -27
package/dist/browser/session.js
CHANGED
|
@@ -35,9 +35,48 @@ const execFileAsync = promisify(execFile);
|
|
|
35
35
|
const PLAYWRIGHT_OPTION_KEY_OVERRIDES = {
|
|
36
36
|
extra_http_headers: 'extraHTTPHeaders',
|
|
37
37
|
};
|
|
38
|
+
const DOMAIN_POLICY_UNFILTERABLE_RECORDING_KEYS = new Set([
|
|
39
|
+
'record_video_dir',
|
|
40
|
+
'record_video_size',
|
|
41
|
+
'record_video',
|
|
42
|
+
'recordVideo',
|
|
43
|
+
]);
|
|
38
44
|
const EMPTY_DOM_RETRY_DELAY_MS = 250;
|
|
39
45
|
const REMOTE_RECONNECT_DELAYS_MS = [1000, 2000, 4000];
|
|
40
46
|
const REMOTE_RECONNECT_ATTEMPT_TIMEOUT_MS = 15_000;
|
|
47
|
+
const chmodPrivateStorageFile = (filePath) => {
|
|
48
|
+
if (process.platform === 'win32') {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
fs.chmodSync(filePath, 0o600);
|
|
52
|
+
};
|
|
53
|
+
const writePrivateStorageFile = (filePath, contents) => {
|
|
54
|
+
fs.writeFileSync(filePath, contents, { encoding: 'utf-8', mode: 0o600 });
|
|
55
|
+
chmodPrivateStorageFile(filePath);
|
|
56
|
+
};
|
|
57
|
+
const chmodPrivateFileBestEffort = (filePath) => {
|
|
58
|
+
if (process.platform === 'win32') {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
fs.chmodSync(filePath, 0o600);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* best effort */
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const ensurePrivateDirectoryIfCreated = (dirPath) => {
|
|
69
|
+
const existed = fs.existsSync(dirPath);
|
|
70
|
+
fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
|
|
71
|
+
if (!existed && process.platform !== 'win32') {
|
|
72
|
+
try {
|
|
73
|
+
fs.chmodSync(dirPath, 0o700);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* best effort */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
41
80
|
const cloneBrowserProfileConfig = (profile) => typeof structuredClone === 'function'
|
|
42
81
|
? structuredClone(profile.config)
|
|
43
82
|
: JSON.parse(JSON.stringify(profile.config));
|
|
@@ -220,6 +259,8 @@ export class BrowserSession {
|
|
|
220
259
|
_watchdogs = new Set();
|
|
221
260
|
_defaultWatchdogsAttached = false;
|
|
222
261
|
_captchaWatchdog = null;
|
|
262
|
+
_scopedExtraHeadersRouteContext = null;
|
|
263
|
+
_scopedExtraHeadersRouteHandler = null;
|
|
223
264
|
RECONNECT_WAIT_TIMEOUT = 54;
|
|
224
265
|
_reconnecting = false;
|
|
225
266
|
_reconnectTask = null;
|
|
@@ -548,7 +589,10 @@ export class BrowserSession {
|
|
|
548
589
|
// Check response size (if available)
|
|
549
590
|
const contentLength = headers['content-length'] || headers['Content-Length'];
|
|
550
591
|
if (contentLength && parseInt(contentLength, 10) > maxResponseSize) {
|
|
551
|
-
|
|
592
|
+
const requestUrl = typeof request?.url === 'function'
|
|
593
|
+
? request.url()
|
|
594
|
+
: String(request?.url ?? '');
|
|
595
|
+
this.logger.debug(`Skipping large response (${contentLength} bytes): ${BrowserSession._redact_url_for_logging(requestUrl)}`);
|
|
552
596
|
pendingRequests.delete(request);
|
|
553
597
|
return;
|
|
554
598
|
}
|
|
@@ -737,11 +781,11 @@ export class BrowserSession {
|
|
|
737
781
|
timestamp: new Date().toISOString(),
|
|
738
782
|
};
|
|
739
783
|
if (typeof details.url === 'string' && details.url.trim()) {
|
|
740
|
-
event.url = details.url.trim();
|
|
784
|
+
event.url = BrowserSession._redact_url_for_logging(details.url.trim());
|
|
741
785
|
}
|
|
742
786
|
if (typeof details.error_message === 'string' &&
|
|
743
787
|
details.error_message.trim()) {
|
|
744
|
-
event.error_message = details.error_message.trim();
|
|
788
|
+
event.error_message = BrowserSession._redact_urls_in_text(details.error_message.trim());
|
|
745
789
|
}
|
|
746
790
|
if (typeof details.page_id === 'number' &&
|
|
747
791
|
Number.isFinite(details.page_id)) {
|
|
@@ -813,6 +857,7 @@ export class BrowserSession {
|
|
|
813
857
|
if (!page || typeof page.evaluate !== 'function') {
|
|
814
858
|
return [];
|
|
815
859
|
}
|
|
860
|
+
await this.validate_page_after_action(page);
|
|
816
861
|
try {
|
|
817
862
|
const pending = await page.evaluate(() => {
|
|
818
863
|
const perf = window.performance;
|
|
@@ -897,6 +942,9 @@ export class BrowserSession {
|
|
|
897
942
|
this.logger.debug(`Failed to gather pending network requests: ${error.message}`);
|
|
898
943
|
return [];
|
|
899
944
|
}
|
|
945
|
+
finally {
|
|
946
|
+
await this.validate_page_after_action(page);
|
|
947
|
+
}
|
|
900
948
|
}
|
|
901
949
|
get tabs() {
|
|
902
950
|
return this._tabs.slice();
|
|
@@ -1156,6 +1204,21 @@ export class BrowserSession {
|
|
|
1156
1204
|
}
|
|
1157
1205
|
const result = {};
|
|
1158
1206
|
for (const [rawKey, rawVal] of Object.entries(value)) {
|
|
1207
|
+
if (rawKey === 'extra_http_headers' &&
|
|
1208
|
+
this._has_url_access_restrictions()) {
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
if (rawKey === 'permissions' && this._has_url_access_restrictions()) {
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
if (DOMAIN_POLICY_UNFILTERABLE_RECORDING_KEYS.has(rawKey) &&
|
|
1215
|
+
this._has_url_access_restrictions()) {
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
if (rawKey === 'http_credentials' &&
|
|
1219
|
+
this._has_url_access_restrictions()) {
|
|
1220
|
+
this._assertHttpCredentialsScoped(rawVal);
|
|
1221
|
+
}
|
|
1159
1222
|
const convertedValue = this._toPlaywrightOptions(rawVal);
|
|
1160
1223
|
if (convertedValue === undefined) {
|
|
1161
1224
|
continue;
|
|
@@ -1166,6 +1229,16 @@ export class BrowserSession {
|
|
|
1166
1229
|
}
|
|
1167
1230
|
return result;
|
|
1168
1231
|
}
|
|
1232
|
+
_assertHttpCredentialsScoped(value) {
|
|
1233
|
+
if (!value || typeof value !== 'object') {
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
const origin = value.origin;
|
|
1237
|
+
if (typeof origin !== 'string' || origin.trim().length === 0) {
|
|
1238
|
+
throw new BrowserError('http_credentials must include an origin when domain restrictions are configured.');
|
|
1239
|
+
}
|
|
1240
|
+
this._assert_url_allowed(origin.trim());
|
|
1241
|
+
}
|
|
1169
1242
|
async set_extra_headers(headers) {
|
|
1170
1243
|
const normalizedHeaders = Object.fromEntries(Object.entries(headers)
|
|
1171
1244
|
.map(([key, value]) => [String(key).trim(), String(value)])
|
|
@@ -1175,8 +1248,78 @@ export class BrowserSession {
|
|
|
1175
1248
|
typeof this.browser_context.setExtraHTTPHeaders !== 'function') {
|
|
1176
1249
|
return;
|
|
1177
1250
|
}
|
|
1251
|
+
if (this._has_url_access_restrictions()) {
|
|
1252
|
+
await this._installScopedExtraHeaders(normalizedHeaders);
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
await this._removeScopedExtraHeadersRoute();
|
|
1178
1256
|
await this.browser_context.setExtraHTTPHeaders(normalizedHeaders);
|
|
1179
1257
|
}
|
|
1258
|
+
async _removeScopedExtraHeadersRoute() {
|
|
1259
|
+
const context = this._scopedExtraHeadersRouteContext;
|
|
1260
|
+
const handler = this._scopedExtraHeadersRouteHandler;
|
|
1261
|
+
this._scopedExtraHeadersRouteContext = null;
|
|
1262
|
+
this._scopedExtraHeadersRouteHandler = null;
|
|
1263
|
+
if (!context || !handler || typeof context.unroute !== 'function') {
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
try {
|
|
1267
|
+
await context.unroute('**/*', handler);
|
|
1268
|
+
}
|
|
1269
|
+
catch (error) {
|
|
1270
|
+
this.logger.debug(`Failed to remove scoped extra_http_headers route: ${error.message}`);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
async _installScopedExtraHeaders(headers) {
|
|
1274
|
+
const context = this.browser_context;
|
|
1275
|
+
if (!context || typeof context.route !== 'function') {
|
|
1276
|
+
throw new BrowserError('Cannot safely apply extra_http_headers with domain restrictions because BrowserContext.route is unavailable.');
|
|
1277
|
+
}
|
|
1278
|
+
await this._removeScopedExtraHeadersRoute();
|
|
1279
|
+
if (typeof context.setExtraHTTPHeaders === 'function') {
|
|
1280
|
+
await context.setExtraHTTPHeaders({});
|
|
1281
|
+
}
|
|
1282
|
+
const routeHandler = async (route) => {
|
|
1283
|
+
const continueRoute = async (overrides) => {
|
|
1284
|
+
if (typeof route?.fallback === 'function') {
|
|
1285
|
+
return await route.fallback(overrides);
|
|
1286
|
+
}
|
|
1287
|
+
if (typeof route?.continue === 'function') {
|
|
1288
|
+
return await route.continue(overrides);
|
|
1289
|
+
}
|
|
1290
|
+
throw new BrowserError('Cannot continue routed request while applying scoped extra_http_headers.');
|
|
1291
|
+
};
|
|
1292
|
+
const request = typeof route?.request === 'function' ? route.request() : null;
|
|
1293
|
+
const url = typeof request?.url === 'function'
|
|
1294
|
+
? String(request.url())
|
|
1295
|
+
: typeof request?.url === 'string'
|
|
1296
|
+
? request.url
|
|
1297
|
+
: '';
|
|
1298
|
+
let denialReason = 'invalid_url';
|
|
1299
|
+
if (url) {
|
|
1300
|
+
try {
|
|
1301
|
+
denialReason = this._get_url_access_denial_reason(url);
|
|
1302
|
+
}
|
|
1303
|
+
catch {
|
|
1304
|
+
denialReason = 'blocked';
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
if (denialReason) {
|
|
1308
|
+
await continueRoute();
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
const requestHeaders = typeof request?.headers === 'function' ? request.headers() : {};
|
|
1312
|
+
await continueRoute({
|
|
1313
|
+
headers: {
|
|
1314
|
+
...(requestHeaders ?? {}),
|
|
1315
|
+
...headers,
|
|
1316
|
+
},
|
|
1317
|
+
});
|
|
1318
|
+
};
|
|
1319
|
+
await context.route('**/*', routeHandler);
|
|
1320
|
+
this._scopedExtraHeadersRouteContext = context;
|
|
1321
|
+
this._scopedExtraHeadersRouteHandler = routeHandler;
|
|
1322
|
+
}
|
|
1180
1323
|
async _applyConfiguredExtraHttpHeaders() {
|
|
1181
1324
|
const configuredHeaders = this.browser_profile.config.extra_http_headers;
|
|
1182
1325
|
if (!configuredHeaders || Object.keys(configuredHeaders).length === 0) {
|
|
@@ -1325,6 +1468,9 @@ export class BrowserSession {
|
|
|
1325
1468
|
}
|
|
1326
1469
|
this._setActivePage(nextPage);
|
|
1327
1470
|
this.human_current_page = nextPage;
|
|
1471
|
+
if (nextPage) {
|
|
1472
|
+
await this._assert_page_url_allowed_or_rollback(nextPage);
|
|
1473
|
+
}
|
|
1328
1474
|
await this._syncCurrentTabFromPage(nextPage);
|
|
1329
1475
|
}
|
|
1330
1476
|
async reconnect(options = {}) {
|
|
@@ -1557,6 +1703,8 @@ export class BrowserSession {
|
|
|
1557
1703
|
}
|
|
1558
1704
|
const activePage = await this.get_current_page();
|
|
1559
1705
|
if (activePage) {
|
|
1706
|
+
await this._assert_page_url_allowed_or_rollback(activePage);
|
|
1707
|
+
await this._syncCurrentTabFromPage(activePage);
|
|
1560
1708
|
try {
|
|
1561
1709
|
this.currentUrl = normalize_url(activePage.url());
|
|
1562
1710
|
}
|
|
@@ -1598,7 +1746,7 @@ export class BrowserSession {
|
|
|
1598
1746
|
throw new Error(`Could not discover CDP URL for browser PID ${browserPid}`);
|
|
1599
1747
|
}
|
|
1600
1748
|
this.cdp_url = cdpUrl;
|
|
1601
|
-
this.logger.info(`Discovered CDP URL: ${cdpUrl}`);
|
|
1749
|
+
this.logger.info(`Discovered CDP URL: ${BrowserSession._redact_url_for_logging(cdpUrl)}`);
|
|
1602
1750
|
// Connect to browser via CDP
|
|
1603
1751
|
try {
|
|
1604
1752
|
const playwright = await import('playwright');
|
|
@@ -1744,6 +1892,7 @@ export class BrowserSession {
|
|
|
1744
1892
|
domState = createEmptyDomState();
|
|
1745
1893
|
}
|
|
1746
1894
|
else {
|
|
1895
|
+
await this.validate_page_after_action(page, signal);
|
|
1747
1896
|
try {
|
|
1748
1897
|
const domService = new DomService(page, this.logger);
|
|
1749
1898
|
domState = await this._withAbort(domService.get_clickable_elements(this.browser_profile.highlight_elements, -1, this.browser_profile.viewport_expansion), signal);
|
|
@@ -1762,7 +1911,7 @@ export class BrowserSession {
|
|
|
1762
1911
|
!this._is_new_tab_page(liveUrl) &&
|
|
1763
1912
|
!liveUrl.toLowerCase().endsWith('.pdf');
|
|
1764
1913
|
if (shouldRetryEmptyDom) {
|
|
1765
|
-
this.logger.debug(`Empty DOM detected for ${liveUrl}; retrying once`);
|
|
1914
|
+
this.logger.debug(`Empty DOM detected for ${BrowserSession._redact_url_for_logging(liveUrl)}; retrying once`);
|
|
1766
1915
|
await this._waitWithAbort(EMPTY_DOM_RETRY_DELAY_MS, signal);
|
|
1767
1916
|
try {
|
|
1768
1917
|
const retryDomService = new DomService(page, this.logger);
|
|
@@ -1778,6 +1927,7 @@ export class BrowserSession {
|
|
|
1778
1927
|
this.logger.debug(`Retry after empty DOM failed: ${error.message}`);
|
|
1779
1928
|
}
|
|
1780
1929
|
}
|
|
1930
|
+
await this.validate_page_after_action(page, signal);
|
|
1781
1931
|
}
|
|
1782
1932
|
let screenshot = null;
|
|
1783
1933
|
if (options.include_screenshot && page?.screenshot) {
|
|
@@ -1797,6 +1947,7 @@ export class BrowserSession {
|
|
|
1797
1947
|
}
|
|
1798
1948
|
this.logger.debug(`Failed to capture screenshot: ${error.message}`);
|
|
1799
1949
|
}
|
|
1950
|
+
await this.validate_page_after_action(page, signal);
|
|
1800
1951
|
}
|
|
1801
1952
|
let pageInfo = null;
|
|
1802
1953
|
let pixelsAbove = 0;
|
|
@@ -1842,10 +1993,11 @@ export class BrowserSession {
|
|
|
1842
1993
|
}
|
|
1843
1994
|
this.logger.debug(`Failed to compute page metrics: ${error.message}`);
|
|
1844
1995
|
}
|
|
1996
|
+
await this.validate_page_after_action(page, signal);
|
|
1845
1997
|
}
|
|
1846
1998
|
const pendingNetworkRequests = await this._getPendingNetworkRequests(page);
|
|
1847
1999
|
if (page) {
|
|
1848
|
-
await this.
|
|
2000
|
+
await this.validate_page_after_action(page, signal);
|
|
1849
2001
|
}
|
|
1850
2002
|
if (pageInfo &&
|
|
1851
2003
|
Number.isFinite(pageInfo.viewport_width) &&
|
|
@@ -1878,6 +2030,7 @@ export class BrowserSession {
|
|
|
1878
2030
|
});
|
|
1879
2031
|
// Implement clickable element hash caching to detect new elements
|
|
1880
2032
|
if (options.cache_clickable_elements_hashes && page) {
|
|
2033
|
+
await this.validate_page_after_action(page, signal);
|
|
1881
2034
|
const currentUrl = page.url();
|
|
1882
2035
|
const currentHashes = this._computeElementHashes(domState.selector_map);
|
|
1883
2036
|
// Mark new elements if we have cached hashes for this URL
|
|
@@ -1892,6 +2045,9 @@ export class BrowserSession {
|
|
|
1892
2045
|
};
|
|
1893
2046
|
}
|
|
1894
2047
|
this._throwIfAborted(signal);
|
|
2048
|
+
if (page) {
|
|
2049
|
+
await this.validate_page_after_action(page, signal);
|
|
2050
|
+
}
|
|
1895
2051
|
this.cachedBrowserState = summary;
|
|
1896
2052
|
return summary;
|
|
1897
2053
|
}
|
|
@@ -1940,7 +2096,7 @@ export class BrowserSession {
|
|
|
1940
2096
|
tab.title = this.currentTitle || this.currentUrl;
|
|
1941
2097
|
}
|
|
1942
2098
|
this._syncSessionManagerFromTabs();
|
|
1943
|
-
return this._tabs.
|
|
2099
|
+
return this._tabs.map((tab) => this._sanitize_tab_for_exposure(tab));
|
|
1944
2100
|
}
|
|
1945
2101
|
async navigate_to(url, options = {}) {
|
|
1946
2102
|
const signal = options.signal ?? null;
|
|
@@ -1966,14 +2122,44 @@ export class BrowserSession {
|
|
|
1966
2122
|
}
|
|
1967
2123
|
await this._withAbort(page.goto(normalized, gotoOptions), signal);
|
|
1968
2124
|
const finalUrl = page.url();
|
|
1969
|
-
|
|
2125
|
+
try {
|
|
2126
|
+
this._assert_url_allowed(finalUrl);
|
|
2127
|
+
}
|
|
2128
|
+
catch (error) {
|
|
2129
|
+
if (error instanceof URLNotAllowedError) {
|
|
2130
|
+
await this._rollback_disallowed_navigation(page, finalUrl);
|
|
2131
|
+
}
|
|
2132
|
+
throw error;
|
|
2133
|
+
}
|
|
1970
2134
|
completedUrl = normalize_url(finalUrl);
|
|
1971
2135
|
await this._waitForStableNetwork(page, signal);
|
|
2136
|
+
const settledUrl = page.url();
|
|
2137
|
+
try {
|
|
2138
|
+
this._assert_url_allowed(settledUrl);
|
|
2139
|
+
}
|
|
2140
|
+
catch (error) {
|
|
2141
|
+
if (error instanceof URLNotAllowedError) {
|
|
2142
|
+
await this._rollback_disallowed_navigation(page, settledUrl);
|
|
2143
|
+
}
|
|
2144
|
+
throw error;
|
|
2145
|
+
}
|
|
2146
|
+
completedUrl = normalize_url(settledUrl);
|
|
1972
2147
|
}
|
|
1973
2148
|
catch (error) {
|
|
1974
2149
|
if (this._isAbortError(error)) {
|
|
2150
|
+
const disallowedPageError = await this._get_disallowed_page_error_after_navigation_error(page);
|
|
2151
|
+
if (disallowedPageError) {
|
|
2152
|
+
throw disallowedPageError;
|
|
2153
|
+
}
|
|
2154
|
+
throw error;
|
|
2155
|
+
}
|
|
2156
|
+
if (error instanceof URLNotAllowedError) {
|
|
1975
2157
|
throw error;
|
|
1976
2158
|
}
|
|
2159
|
+
const disallowedPageError = await this._get_disallowed_page_error_after_navigation_error(page);
|
|
2160
|
+
if (disallowedPageError) {
|
|
2161
|
+
throw disallowedPageError;
|
|
2162
|
+
}
|
|
1977
2163
|
const message = error.message ?? 'Navigation failed';
|
|
1978
2164
|
this._recordRecentEvent('navigation_failed', {
|
|
1979
2165
|
url: normalized,
|
|
@@ -2044,13 +2230,22 @@ export class BrowserSession {
|
|
|
2044
2230
|
this._assert_url_allowed(finalUrl);
|
|
2045
2231
|
completedUrl = normalize_url(finalUrl);
|
|
2046
2232
|
await this._waitForStableNetwork(page, signal);
|
|
2233
|
+
const settledUrl = page.url();
|
|
2234
|
+
this._assert_url_allowed(settledUrl);
|
|
2235
|
+
completedUrl = normalize_url(settledUrl);
|
|
2047
2236
|
}
|
|
2048
2237
|
}
|
|
2049
2238
|
catch (error) {
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2239
|
+
const isAbortError = this._isAbortError(error);
|
|
2240
|
+
let finalError = error;
|
|
2241
|
+
if (!(finalError instanceof URLNotAllowedError)) {
|
|
2242
|
+
finalError =
|
|
2243
|
+
(await this._get_disallowed_page_error_after_navigation_error(page, {
|
|
2244
|
+
replace_on_failure: false,
|
|
2245
|
+
})) ?? finalError;
|
|
2246
|
+
}
|
|
2247
|
+
const isUrlNotAllowed = finalError instanceof URLNotAllowedError;
|
|
2248
|
+
const message = finalError.message ?? 'Failed to open new tab';
|
|
2054
2249
|
this._recordRecentEvent('tab_navigation_failed', {
|
|
2055
2250
|
url: normalized,
|
|
2056
2251
|
page_id: newTab.page_id,
|
|
@@ -2094,6 +2289,12 @@ export class BrowserSession {
|
|
|
2094
2289
|
}
|
|
2095
2290
|
this._syncSessionManagerFromTabs();
|
|
2096
2291
|
this.cachedBrowserState = null;
|
|
2292
|
+
if (isUrlNotAllowed) {
|
|
2293
|
+
throw finalError;
|
|
2294
|
+
}
|
|
2295
|
+
if (isAbortError) {
|
|
2296
|
+
throw finalError;
|
|
2297
|
+
}
|
|
2097
2298
|
throw new BrowserError(message);
|
|
2098
2299
|
}
|
|
2099
2300
|
this.tabPages.set(newTab.page_id, page);
|
|
@@ -2195,14 +2396,18 @@ export class BrowserSession {
|
|
|
2195
2396
|
}
|
|
2196
2397
|
}
|
|
2197
2398
|
await this._waitForLoad(page, 5000, signal);
|
|
2399
|
+
if (page) {
|
|
2400
|
+
await this._assert_page_url_allowed_or_rollback(page);
|
|
2401
|
+
await this._syncCurrentTabFromPage(page);
|
|
2402
|
+
}
|
|
2198
2403
|
this._recordRecentEvent('tab_switched', {
|
|
2199
|
-
url: tab.url,
|
|
2404
|
+
url: this.active_tab?.url ?? tab.url,
|
|
2200
2405
|
page_id: tab.page_id,
|
|
2201
2406
|
tab_id: tab.tab_id,
|
|
2202
2407
|
});
|
|
2203
2408
|
await this.event_bus.dispatch(new AgentFocusChangedEvent({
|
|
2204
2409
|
target_id: tab.target_id ?? tab.tab_id,
|
|
2205
|
-
url: tab.url,
|
|
2410
|
+
url: this.active_tab?.url ?? tab.url,
|
|
2206
2411
|
}));
|
|
2207
2412
|
this.cachedBrowserState = null;
|
|
2208
2413
|
return page;
|
|
@@ -2268,39 +2473,62 @@ export class BrowserSession {
|
|
|
2268
2473
|
if (delayMs <= 0) {
|
|
2269
2474
|
return;
|
|
2270
2475
|
}
|
|
2271
|
-
await this.
|
|
2476
|
+
const page = await this.get_current_page();
|
|
2477
|
+
try {
|
|
2478
|
+
await this._waitWithAbort(delayMs, signal);
|
|
2479
|
+
}
|
|
2480
|
+
finally {
|
|
2481
|
+
await this.validate_page_after_action(page, signal);
|
|
2482
|
+
}
|
|
2272
2483
|
}
|
|
2273
2484
|
async send_keys(keys, options = {}) {
|
|
2274
2485
|
const signal = options.signal ?? null;
|
|
2275
2486
|
this._throwIfAborted(signal);
|
|
2276
2487
|
const page = await this._withAbort(this.get_current_page(), signal);
|
|
2488
|
+
await this.validate_page_after_action(page, signal);
|
|
2277
2489
|
const keyboard = page?.keyboard;
|
|
2278
2490
|
if (!keyboard) {
|
|
2279
2491
|
throw new BrowserError('Keyboard input is not available on the current page.');
|
|
2280
2492
|
}
|
|
2281
2493
|
try {
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2494
|
+
try {
|
|
2495
|
+
await this._withAbort(keyboard.press(keys), signal);
|
|
2496
|
+
}
|
|
2497
|
+
catch (error) {
|
|
2498
|
+
if (error instanceof Error && error.message.includes('Unknown key')) {
|
|
2499
|
+
for (const char of keys) {
|
|
2500
|
+
await this._withAbort(keyboard.press(char), signal);
|
|
2501
|
+
}
|
|
2502
|
+
return;
|
|
2288
2503
|
}
|
|
2289
|
-
|
|
2504
|
+
throw error;
|
|
2290
2505
|
}
|
|
2291
|
-
|
|
2506
|
+
}
|
|
2507
|
+
finally {
|
|
2508
|
+
await this._waitForLoad(page, 5000, signal);
|
|
2509
|
+
if (page) {
|
|
2510
|
+
await this._assert_page_url_allowed_or_rollback(page);
|
|
2511
|
+
await this._syncCurrentTabFromPage(page);
|
|
2512
|
+
}
|
|
2513
|
+
this.cachedBrowserState = null;
|
|
2292
2514
|
}
|
|
2293
2515
|
}
|
|
2294
2516
|
async click_coordinates(coordinate_x, coordinate_y, options = {}) {
|
|
2295
2517
|
const signal = options.signal ?? null;
|
|
2296
2518
|
this._throwIfAborted(signal);
|
|
2297
2519
|
const page = await this._withAbort(this.get_current_page(), signal);
|
|
2520
|
+
await this.validate_page_after_action(page, signal);
|
|
2298
2521
|
if (!page?.mouse?.click) {
|
|
2299
2522
|
throw new BrowserError('Unable to perform coordinate click on the current page.');
|
|
2300
2523
|
}
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2524
|
+
try {
|
|
2525
|
+
await this._withAbort(page.mouse.click(coordinate_x, coordinate_y, {
|
|
2526
|
+
button: options.button ?? 'left',
|
|
2527
|
+
}), signal);
|
|
2528
|
+
}
|
|
2529
|
+
finally {
|
|
2530
|
+
await this.validate_page_after_action(page, signal);
|
|
2531
|
+
}
|
|
2304
2532
|
}
|
|
2305
2533
|
async scroll(direction, amount, options = {}) {
|
|
2306
2534
|
const signal = options.signal ?? null;
|
|
@@ -2310,110 +2538,129 @@ export class BrowserSession {
|
|
|
2310
2538
|
return;
|
|
2311
2539
|
}
|
|
2312
2540
|
const page = await this._withAbort(this.get_current_page(), signal);
|
|
2541
|
+
await this.validate_page_after_action(page, signal);
|
|
2313
2542
|
if (!page?.evaluate) {
|
|
2314
2543
|
throw new BrowserError('Unable to access current page for scrolling.');
|
|
2315
2544
|
}
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
const
|
|
2320
|
-
|
|
2321
|
-
|
|
2545
|
+
try {
|
|
2546
|
+
const node = options.node ?? null;
|
|
2547
|
+
if (node?.xpath) {
|
|
2548
|
+
const scrolled = await this._withAbort(page.evaluate((payload) => {
|
|
2549
|
+
const root = document.evaluate(payload.xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
|
2550
|
+
if (!root) {
|
|
2551
|
+
return false;
|
|
2552
|
+
}
|
|
2553
|
+
const topDelta = payload.direction === 'up'
|
|
2554
|
+
? -payload.amount
|
|
2555
|
+
: payload.direction === 'down'
|
|
2556
|
+
? payload.amount
|
|
2557
|
+
: 0;
|
|
2558
|
+
const leftDelta = payload.direction === 'left'
|
|
2559
|
+
? -payload.amount
|
|
2560
|
+
: payload.direction === 'right'
|
|
2561
|
+
? payload.amount
|
|
2562
|
+
: 0;
|
|
2563
|
+
root.scrollBy({
|
|
2564
|
+
top: topDelta,
|
|
2565
|
+
left: leftDelta,
|
|
2566
|
+
behavior: 'auto',
|
|
2567
|
+
});
|
|
2568
|
+
return true;
|
|
2569
|
+
}, { xpath: node.xpath, direction, amount: normalizedAmount }), signal);
|
|
2570
|
+
if (scrolled) {
|
|
2571
|
+
return;
|
|
2322
2572
|
}
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
: 0;
|
|
2328
|
-
const leftDelta = payload.direction === 'left'
|
|
2329
|
-
? -payload.amount
|
|
2330
|
-
: payload.direction === 'right'
|
|
2331
|
-
? payload.amount
|
|
2332
|
-
: 0;
|
|
2333
|
-
root.scrollBy({
|
|
2334
|
-
top: topDelta,
|
|
2335
|
-
left: leftDelta,
|
|
2336
|
-
behavior: 'auto',
|
|
2337
|
-
});
|
|
2338
|
-
return true;
|
|
2339
|
-
}, { xpath: node.xpath, direction, amount: normalizedAmount }), signal);
|
|
2340
|
-
if (scrolled) {
|
|
2573
|
+
}
|
|
2574
|
+
if (direction === 'up' || direction === 'down') {
|
|
2575
|
+
const pixels = direction === 'down' ? -normalizedAmount : normalizedAmount;
|
|
2576
|
+
await this._withAbort(this._scrollContainer(pixels), signal);
|
|
2341
2577
|
return;
|
|
2342
2578
|
}
|
|
2579
|
+
const horizontalDelta = direction === 'left' ? -normalizedAmount : normalizedAmount;
|
|
2580
|
+
await this._withAbort(page.evaluate((x) => window.scrollBy(x, 0), horizontalDelta), signal);
|
|
2343
2581
|
}
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
await this._withAbort(this._scrollContainer(pixels), signal);
|
|
2347
|
-
return;
|
|
2582
|
+
finally {
|
|
2583
|
+
await this.validate_page_after_action(page, signal);
|
|
2348
2584
|
}
|
|
2349
|
-
const horizontalDelta = direction === 'left' ? -normalizedAmount : normalizedAmount;
|
|
2350
|
-
await this._withAbort(page.evaluate((x) => window.scrollBy(x, 0), horizontalDelta), signal);
|
|
2351
2585
|
}
|
|
2352
2586
|
async scroll_to_text(text, options = {}) {
|
|
2353
2587
|
const signal = options.signal ?? null;
|
|
2354
2588
|
this._throwIfAborted(signal);
|
|
2355
2589
|
const page = await this._withAbort(this.get_current_page(), signal);
|
|
2590
|
+
await this.validate_page_after_action(page, signal);
|
|
2356
2591
|
if (!page?.evaluate) {
|
|
2357
2592
|
throw new BrowserError('Unable to access page for scrolling.');
|
|
2358
2593
|
}
|
|
2359
|
-
|
|
2360
|
-
const
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
el.
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2594
|
+
try {
|
|
2595
|
+
const success = await this._withAbort(page.evaluate((payload) => {
|
|
2596
|
+
const query = payload.text.toLowerCase();
|
|
2597
|
+
const iterator = document.createNodeIterator(document.body, NodeFilter.SHOW_ELEMENT);
|
|
2598
|
+
let node;
|
|
2599
|
+
while ((node = iterator.nextNode())) {
|
|
2600
|
+
const el = node;
|
|
2601
|
+
if (!el || !el.textContent) {
|
|
2602
|
+
continue;
|
|
2603
|
+
}
|
|
2604
|
+
if (el.textContent.toLowerCase().includes(query)) {
|
|
2605
|
+
el.scrollIntoView({
|
|
2606
|
+
behavior: 'smooth',
|
|
2607
|
+
block: payload.direction === 'up' ? 'start' : 'center',
|
|
2608
|
+
});
|
|
2609
|
+
return true;
|
|
2610
|
+
}
|
|
2374
2611
|
}
|
|
2612
|
+
return false;
|
|
2613
|
+
}, { text, direction: options.direction ?? 'down' }), signal);
|
|
2614
|
+
if (!success) {
|
|
2615
|
+
throw new BrowserError(`Text '${text}' not found on page`);
|
|
2375
2616
|
}
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
throw new BrowserError(`Text '${text}' not found on page`);
|
|
2617
|
+
}
|
|
2618
|
+
finally {
|
|
2619
|
+
await this.validate_page_after_action(page, signal);
|
|
2380
2620
|
}
|
|
2381
2621
|
}
|
|
2382
2622
|
async get_dropdown_options(element_node, options = {}) {
|
|
2383
2623
|
const signal = options.signal ?? null;
|
|
2384
2624
|
this._throwIfAborted(signal);
|
|
2385
2625
|
const page = await this._withAbort(this.get_current_page(), signal);
|
|
2626
|
+
await this.validate_page_after_action(page, signal);
|
|
2386
2627
|
if (!page?.evaluate) {
|
|
2387
2628
|
throw new BrowserError('Unable to evaluate dropdown options on current page.');
|
|
2388
2629
|
}
|
|
2389
2630
|
if (!element_node?.xpath) {
|
|
2390
2631
|
throw new BrowserError('DOM element does not include an XPath selector.');
|
|
2391
2632
|
}
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2633
|
+
let payload;
|
|
2634
|
+
try {
|
|
2635
|
+
payload = await this._withAbort(page.evaluate(({ xpath }) => {
|
|
2636
|
+
const element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
|
2637
|
+
if (!element)
|
|
2638
|
+
return null;
|
|
2639
|
+
if (element.tagName?.toLowerCase() === 'select') {
|
|
2640
|
+
const options = Array.from(element.options).map((opt, index) => ({
|
|
2641
|
+
text: opt.textContent?.trim() ?? '',
|
|
2642
|
+
value: (opt.value ?? '').trim(),
|
|
2643
|
+
index,
|
|
2644
|
+
}));
|
|
2645
|
+
return { type: 'select', options };
|
|
2646
|
+
}
|
|
2647
|
+
const ariaRoles = new Set(['menu', 'listbox', 'combobox']);
|
|
2648
|
+
const role = element.getAttribute('role');
|
|
2649
|
+
if (role && ariaRoles.has(role)) {
|
|
2650
|
+
const nodes = element.querySelectorAll('[role="menuitem"],[role="option"]');
|
|
2651
|
+
const options = Array.from(nodes).map((node, index) => ({
|
|
2652
|
+
text: node.textContent?.trim() ?? '',
|
|
2653
|
+
value: node.textContent?.trim() ?? '',
|
|
2654
|
+
index,
|
|
2655
|
+
}));
|
|
2656
|
+
return { type: 'aria', options };
|
|
2657
|
+
}
|
|
2395
2658
|
return null;
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
}));
|
|
2402
|
-
return { type: 'select', options };
|
|
2403
|
-
}
|
|
2404
|
-
const ariaRoles = new Set(['menu', 'listbox', 'combobox']);
|
|
2405
|
-
const role = element.getAttribute('role');
|
|
2406
|
-
if (role && ariaRoles.has(role)) {
|
|
2407
|
-
const nodes = element.querySelectorAll('[role="menuitem"],[role="option"]');
|
|
2408
|
-
const options = Array.from(nodes).map((node, index) => ({
|
|
2409
|
-
text: node.textContent?.trim() ?? '',
|
|
2410
|
-
value: node.textContent?.trim() ?? '',
|
|
2411
|
-
index,
|
|
2412
|
-
}));
|
|
2413
|
-
return { type: 'aria', options };
|
|
2414
|
-
}
|
|
2415
|
-
return null;
|
|
2416
|
-
}, { xpath: element_node.xpath }), signal);
|
|
2659
|
+
}, { xpath: element_node.xpath }), signal);
|
|
2660
|
+
}
|
|
2661
|
+
finally {
|
|
2662
|
+
await this.validate_page_after_action(page, signal);
|
|
2663
|
+
}
|
|
2417
2664
|
if (!payload || !Array.isArray(payload.options)) {
|
|
2418
2665
|
throw new BrowserError('No options found for the specified dropdown.');
|
|
2419
2666
|
}
|
|
@@ -2452,49 +2699,117 @@ export class BrowserSession {
|
|
|
2452
2699
|
if (!page) {
|
|
2453
2700
|
throw new BrowserError('No active page for selection.');
|
|
2454
2701
|
}
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
})();
|
|
2471
|
-
for (const frame of pageFrames) {
|
|
2472
|
-
try {
|
|
2473
|
-
const typeInfo = await this._withAbort(frame.evaluate((xpath) => {
|
|
2474
|
-
const element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
|
2475
|
-
if (!element)
|
|
2476
|
-
return { found: false };
|
|
2477
|
-
const tagName = element.tagName?.toLowerCase();
|
|
2478
|
-
const role = element.getAttribute?.('role');
|
|
2479
|
-
if (tagName === 'select')
|
|
2480
|
-
return { found: true, type: 'select' };
|
|
2481
|
-
if (role && ['menu', 'listbox', 'combobox'].includes(role))
|
|
2482
|
-
return { found: true, type: 'aria' };
|
|
2483
|
-
return { found: false };
|
|
2484
|
-
}, element_node.xpath), signal);
|
|
2485
|
-
if (!typeInfo?.found) {
|
|
2486
|
-
continue;
|
|
2702
|
+
await this.validate_page_after_action(page, signal);
|
|
2703
|
+
try {
|
|
2704
|
+
const formatAvailableOptions = (opts) => opts
|
|
2705
|
+
.map((opt) => ` - [${opt.index}] text=${JSON.stringify(opt.text)} value=${JSON.stringify(opt.value)}`)
|
|
2706
|
+
.join('\n');
|
|
2707
|
+
const pageFrames = (() => {
|
|
2708
|
+
const framesAccessor = page.frames;
|
|
2709
|
+
if (typeof framesAccessor === 'function') {
|
|
2710
|
+
try {
|
|
2711
|
+
const result = framesAccessor.call(page);
|
|
2712
|
+
return Array.isArray(result) ? result : [];
|
|
2713
|
+
}
|
|
2714
|
+
catch {
|
|
2715
|
+
return [];
|
|
2716
|
+
}
|
|
2487
2717
|
}
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2718
|
+
return Array.isArray(framesAccessor) ? framesAccessor : [];
|
|
2719
|
+
})();
|
|
2720
|
+
for (const frame of pageFrames) {
|
|
2721
|
+
try {
|
|
2722
|
+
const typeInfo = await this._withAbort(frame.evaluate((xpath) => {
|
|
2723
|
+
const element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
|
2724
|
+
if (!element)
|
|
2492
2725
|
return { found: false };
|
|
2726
|
+
const tagName = element.tagName?.toLowerCase();
|
|
2727
|
+
const role = element.getAttribute?.('role');
|
|
2728
|
+
if (tagName === 'select')
|
|
2729
|
+
return { found: true, type: 'select' };
|
|
2730
|
+
if (role && ['menu', 'listbox', 'combobox'].includes(role))
|
|
2731
|
+
return { found: true, type: 'aria' };
|
|
2732
|
+
return { found: false };
|
|
2733
|
+
}, element_node.xpath), signal);
|
|
2734
|
+
if (!typeInfo?.found) {
|
|
2735
|
+
continue;
|
|
2736
|
+
}
|
|
2737
|
+
if (typeInfo.type === 'select') {
|
|
2738
|
+
const selection = await this._withAbort(frame.evaluate(({ xpath, optionText, }) => {
|
|
2739
|
+
const root = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
|
2740
|
+
if (!root || root.tagName?.toLowerCase() !== 'select') {
|
|
2741
|
+
return { found: false };
|
|
2742
|
+
}
|
|
2743
|
+
const options = Array.from(root.options).map((opt, index) => ({
|
|
2744
|
+
index,
|
|
2745
|
+
text: opt.textContent?.trim() ?? '',
|
|
2746
|
+
value: (opt.value ?? '').trim(),
|
|
2747
|
+
}));
|
|
2748
|
+
const targetRaw = optionText.trim();
|
|
2749
|
+
const targetLower = optionText.trim().toLowerCase();
|
|
2750
|
+
let matchedIndex = options.findIndex((opt) => opt.text === targetRaw || opt.value === targetRaw);
|
|
2751
|
+
if (matchedIndex < 0) {
|
|
2752
|
+
matchedIndex = options.findIndex((opt) => opt.text.trim().toLowerCase() === targetLower ||
|
|
2753
|
+
opt.value.trim().toLowerCase() === targetLower);
|
|
2754
|
+
}
|
|
2755
|
+
if (matchedIndex < 0) {
|
|
2756
|
+
return { found: true, success: false, options };
|
|
2757
|
+
}
|
|
2758
|
+
const matched = options[matchedIndex];
|
|
2759
|
+
root.value = matched.value;
|
|
2760
|
+
root.dispatchEvent(new Event('input', { bubbles: true }));
|
|
2761
|
+
root.dispatchEvent(new Event('change', { bubbles: true }));
|
|
2762
|
+
const selectedOption = root.selectedIndex >= 0
|
|
2763
|
+
? root.options[root.selectedIndex]
|
|
2764
|
+
: null;
|
|
2765
|
+
const selectedText = selectedOption?.textContent?.trim() ?? '';
|
|
2766
|
+
const selectedValue = (root.value ?? '').trim();
|
|
2767
|
+
const selectedValueLower = selectedValue.trim().toLowerCase();
|
|
2768
|
+
const selectedTextLower = selectedText.trim().toLowerCase();
|
|
2769
|
+
const matchedValueLower = String(matched.value ?? '')
|
|
2770
|
+
.trim()
|
|
2771
|
+
.toLowerCase();
|
|
2772
|
+
const matchedTextLower = String(matched.text ?? '')
|
|
2773
|
+
.trim()
|
|
2774
|
+
.toLowerCase();
|
|
2775
|
+
const verified = selectedValueLower === matchedValueLower ||
|
|
2776
|
+
selectedTextLower === matchedTextLower;
|
|
2777
|
+
return {
|
|
2778
|
+
found: true,
|
|
2779
|
+
success: verified,
|
|
2780
|
+
options,
|
|
2781
|
+
selectedText,
|
|
2782
|
+
selectedValue,
|
|
2783
|
+
matched,
|
|
2784
|
+
};
|
|
2785
|
+
}, { xpath: element_node.xpath, optionText: text }), signal);
|
|
2786
|
+
if (selection?.found && selection.success) {
|
|
2787
|
+
const matchedText = selection.matched?.text ?? text;
|
|
2788
|
+
const matchedValue = selection.matched?.value ?? '';
|
|
2789
|
+
const msg = `Selected option ${matchedText} (${matchedValue})`;
|
|
2790
|
+
return {
|
|
2791
|
+
message: msg,
|
|
2792
|
+
short_term_memory: msg,
|
|
2793
|
+
long_term_memory: msg,
|
|
2794
|
+
matched_text: String(matchedText),
|
|
2795
|
+
matched_value: String(matchedValue),
|
|
2796
|
+
};
|
|
2493
2797
|
}
|
|
2494
|
-
|
|
2798
|
+
if (selection?.found) {
|
|
2799
|
+
const details = formatAvailableOptions(selection.options ?? []);
|
|
2800
|
+
throw new BrowserError(`Could not select option '${text}' for index ${element_node.highlight_index ?? 'unknown'}.\nAvailable options:\n${details}`);
|
|
2801
|
+
}
|
|
2802
|
+
continue;
|
|
2803
|
+
}
|
|
2804
|
+
const clicked = await this._withAbort(frame.evaluate(({ xpath, optionText, }) => {
|
|
2805
|
+
const root = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
|
2806
|
+
if (!root)
|
|
2807
|
+
return false;
|
|
2808
|
+
const nodes = root.querySelectorAll('[role="menuitem"],[role="option"]');
|
|
2809
|
+
const options = Array.from(nodes).map((node, index) => ({
|
|
2495
2810
|
index,
|
|
2496
|
-
text:
|
|
2497
|
-
value: (
|
|
2811
|
+
text: node.textContent?.trim() ?? '',
|
|
2812
|
+
value: node.textContent?.trim() ?? '',
|
|
2498
2813
|
}));
|
|
2499
2814
|
const targetRaw = optionText.trim();
|
|
2500
2815
|
const targetLower = optionText.trim().toLowerCase();
|
|
@@ -2506,103 +2821,41 @@ export class BrowserSession {
|
|
|
2506
2821
|
if (matchedIndex < 0) {
|
|
2507
2822
|
return { found: true, success: false, options };
|
|
2508
2823
|
}
|
|
2509
|
-
|
|
2510
|
-
root.value = matched.value;
|
|
2511
|
-
root.dispatchEvent(new Event('input', { bubbles: true }));
|
|
2512
|
-
root.dispatchEvent(new Event('change', { bubbles: true }));
|
|
2513
|
-
const selectedOption = root.selectedIndex >= 0
|
|
2514
|
-
? root.options[root.selectedIndex]
|
|
2515
|
-
: null;
|
|
2516
|
-
const selectedText = selectedOption?.textContent?.trim() ?? '';
|
|
2517
|
-
const selectedValue = (root.value ?? '').trim();
|
|
2518
|
-
const selectedValueLower = selectedValue.trim().toLowerCase();
|
|
2519
|
-
const selectedTextLower = selectedText.trim().toLowerCase();
|
|
2520
|
-
const matchedValueLower = String(matched.value ?? '')
|
|
2521
|
-
.trim()
|
|
2522
|
-
.toLowerCase();
|
|
2523
|
-
const matchedTextLower = String(matched.text ?? '')
|
|
2524
|
-
.trim()
|
|
2525
|
-
.toLowerCase();
|
|
2526
|
-
const verified = selectedValueLower === matchedValueLower ||
|
|
2527
|
-
selectedTextLower === matchedTextLower;
|
|
2824
|
+
nodes[matchedIndex].click();
|
|
2528
2825
|
return {
|
|
2529
2826
|
found: true,
|
|
2530
|
-
success:
|
|
2827
|
+
success: true,
|
|
2531
2828
|
options,
|
|
2532
|
-
|
|
2533
|
-
selectedValue,
|
|
2534
|
-
matched,
|
|
2829
|
+
matched: options[matchedIndex],
|
|
2535
2830
|
};
|
|
2536
2831
|
}, { xpath: element_node.xpath, optionText: text }), signal);
|
|
2537
|
-
if (
|
|
2538
|
-
const matchedText =
|
|
2539
|
-
const
|
|
2540
|
-
const msg = `Selected option ${matchedText} (${matchedValue})`;
|
|
2832
|
+
if (clicked?.found && clicked.success) {
|
|
2833
|
+
const matchedText = clicked.matched?.text ?? text;
|
|
2834
|
+
const msg = `Selected menu item ${matchedText}`;
|
|
2541
2835
|
return {
|
|
2542
2836
|
message: msg,
|
|
2543
2837
|
short_term_memory: msg,
|
|
2544
2838
|
long_term_memory: msg,
|
|
2545
2839
|
matched_text: String(matchedText),
|
|
2546
|
-
matched_value: String(matchedValue),
|
|
2547
2840
|
};
|
|
2548
2841
|
}
|
|
2549
|
-
if (
|
|
2550
|
-
const details = formatAvailableOptions(
|
|
2842
|
+
if (clicked?.found) {
|
|
2843
|
+
const details = formatAvailableOptions(clicked.options ?? []);
|
|
2551
2844
|
throw new BrowserError(`Could not select option '${text}' for index ${element_node.highlight_index ?? 'unknown'}.\nAvailable options:\n${details}`);
|
|
2552
2845
|
}
|
|
2553
|
-
continue;
|
|
2554
2846
|
}
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
return false;
|
|
2559
|
-
const nodes = root.querySelectorAll('[role="menuitem"],[role="option"]');
|
|
2560
|
-
const options = Array.from(nodes).map((node, index) => ({
|
|
2561
|
-
index,
|
|
2562
|
-
text: node.textContent?.trim() ?? '',
|
|
2563
|
-
value: node.textContent?.trim() ?? '',
|
|
2564
|
-
}));
|
|
2565
|
-
const targetRaw = optionText.trim();
|
|
2566
|
-
const targetLower = optionText.trim().toLowerCase();
|
|
2567
|
-
let matchedIndex = options.findIndex((opt) => opt.text === targetRaw || opt.value === targetRaw);
|
|
2568
|
-
if (matchedIndex < 0) {
|
|
2569
|
-
matchedIndex = options.findIndex((opt) => opt.text.trim().toLowerCase() === targetLower ||
|
|
2570
|
-
opt.value.trim().toLowerCase() === targetLower);
|
|
2571
|
-
}
|
|
2572
|
-
if (matchedIndex < 0) {
|
|
2573
|
-
return { found: true, success: false, options };
|
|
2847
|
+
catch (error) {
|
|
2848
|
+
if (error instanceof BrowserError) {
|
|
2849
|
+
throw error;
|
|
2574
2850
|
}
|
|
2575
|
-
|
|
2576
|
-
return {
|
|
2577
|
-
found: true,
|
|
2578
|
-
success: true,
|
|
2579
|
-
options,
|
|
2580
|
-
matched: options[matchedIndex],
|
|
2581
|
-
};
|
|
2582
|
-
}, { xpath: element_node.xpath, optionText: text }), signal);
|
|
2583
|
-
if (clicked?.found && clicked.success) {
|
|
2584
|
-
const matchedText = clicked.matched?.text ?? text;
|
|
2585
|
-
const msg = `Selected menu item ${matchedText}`;
|
|
2586
|
-
return {
|
|
2587
|
-
message: msg,
|
|
2588
|
-
short_term_memory: msg,
|
|
2589
|
-
long_term_memory: msg,
|
|
2590
|
-
matched_text: String(matchedText),
|
|
2591
|
-
};
|
|
2592
|
-
}
|
|
2593
|
-
if (clicked?.found) {
|
|
2594
|
-
const details = formatAvailableOptions(clicked.options ?? []);
|
|
2595
|
-
throw new BrowserError(`Could not select option '${text}' for index ${element_node.highlight_index ?? 'unknown'}.\nAvailable options:\n${details}`);
|
|
2596
|
-
}
|
|
2597
|
-
}
|
|
2598
|
-
catch (error) {
|
|
2599
|
-
if (error instanceof BrowserError) {
|
|
2600
|
-
throw error;
|
|
2851
|
+
continue;
|
|
2601
2852
|
}
|
|
2602
|
-
continue;
|
|
2603
2853
|
}
|
|
2854
|
+
throw new BrowserError(`Could not select option '${text}' for index ${element_node.highlight_index ?? 'unknown'}`);
|
|
2855
|
+
}
|
|
2856
|
+
finally {
|
|
2857
|
+
await this.validate_page_after_action(page, signal);
|
|
2604
2858
|
}
|
|
2605
|
-
throw new BrowserError(`Could not select option '${text}' for index ${element_node.highlight_index ?? 'unknown'}`);
|
|
2606
2859
|
}
|
|
2607
2860
|
async upload_file(element_node, file_path, options = {}) {
|
|
2608
2861
|
const signal = options.signal ?? null;
|
|
@@ -2618,7 +2871,14 @@ export class BrowserSession {
|
|
|
2618
2871
|
if (typeof locatorWithUpload.setInputFiles !== 'function') {
|
|
2619
2872
|
throw new Error('Element does not support file upload');
|
|
2620
2873
|
}
|
|
2621
|
-
await this._withAbort(
|
|
2874
|
+
const page = await this._withAbort(this.get_current_page(), signal);
|
|
2875
|
+
await this.validate_page_after_action(page, signal);
|
|
2876
|
+
try {
|
|
2877
|
+
await this._withAbort(locatorWithUpload.setInputFiles(file_path, { timeout: 5000 }), signal);
|
|
2878
|
+
}
|
|
2879
|
+
finally {
|
|
2880
|
+
await this.validate_page_after_action(page, signal);
|
|
2881
|
+
}
|
|
2622
2882
|
}
|
|
2623
2883
|
async go_back(options = {}) {
|
|
2624
2884
|
const signal = options.signal ?? null;
|
|
@@ -2627,16 +2887,25 @@ export class BrowserSession {
|
|
|
2627
2887
|
if (!page?.goBack) {
|
|
2628
2888
|
return;
|
|
2629
2889
|
}
|
|
2890
|
+
await this.validate_page_after_action(page, signal);
|
|
2630
2891
|
const previousUrl = this.currentUrl;
|
|
2631
2892
|
try {
|
|
2632
2893
|
await this._withAbort(page.goBack(), signal);
|
|
2894
|
+
await this._waitForStableNetwork(page, signal);
|
|
2895
|
+
await this._assert_page_url_allowed_or_rollback(page);
|
|
2633
2896
|
}
|
|
2634
2897
|
catch (error) {
|
|
2635
2898
|
if (this._isAbortError(error)) {
|
|
2636
2899
|
throw error;
|
|
2637
2900
|
}
|
|
2901
|
+
if (error instanceof URLNotAllowedError) {
|
|
2902
|
+
throw error;
|
|
2903
|
+
}
|
|
2638
2904
|
this.logger.debug(`Failed to navigate back: ${error.message}`);
|
|
2639
2905
|
}
|
|
2906
|
+
finally {
|
|
2907
|
+
await this.validate_page_after_action(page, signal);
|
|
2908
|
+
}
|
|
2640
2909
|
this._throwIfAborted(signal);
|
|
2641
2910
|
await this._syncCurrentTabFromPage(page);
|
|
2642
2911
|
const currentUrl = this.currentUrl;
|
|
@@ -2684,8 +2953,9 @@ export class BrowserSession {
|
|
|
2684
2953
|
}
|
|
2685
2954
|
static async get_unique_filename(directory, filename) {
|
|
2686
2955
|
const resolvedDir = path.resolve(directory);
|
|
2687
|
-
const
|
|
2688
|
-
|
|
2956
|
+
const safeFilename = BrowserSession._sanitize_download_filename(filename);
|
|
2957
|
+
const parsed = path.parse(safeFilename);
|
|
2958
|
+
let candidate = safeFilename;
|
|
2689
2959
|
let counter = 1;
|
|
2690
2960
|
while (fs.existsSync(path.join(resolvedDir, candidate))) {
|
|
2691
2961
|
candidate = `${parsed.name} (${counter})${parsed.ext}`;
|
|
@@ -2693,6 +2963,81 @@ export class BrowserSession {
|
|
|
2693
2963
|
}
|
|
2694
2964
|
return candidate;
|
|
2695
2965
|
}
|
|
2966
|
+
async _cancel_download_best_effort(download) {
|
|
2967
|
+
if (typeof download?.cancel !== 'function') {
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
try {
|
|
2971
|
+
await download.cancel();
|
|
2972
|
+
}
|
|
2973
|
+
catch (error) {
|
|
2974
|
+
this.logger.debug(`Failed to cancel blocked download: ${error.message}`);
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
async _assert_download_url_allowed(download, downloadUrl) {
|
|
2978
|
+
try {
|
|
2979
|
+
this._assert_url_allowed(downloadUrl);
|
|
2980
|
+
}
|
|
2981
|
+
catch (error) {
|
|
2982
|
+
await this._cancel_download_best_effort(download);
|
|
2983
|
+
throw error;
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
static _sanitize_download_filename(filename) {
|
|
2987
|
+
const basename = String(filename || '')
|
|
2988
|
+
.replace(/\0/g, '')
|
|
2989
|
+
.replace(/\\/g, '/')
|
|
2990
|
+
.split('/')
|
|
2991
|
+
.pop()
|
|
2992
|
+
?.trim();
|
|
2993
|
+
if (!basename || basename === '.' || basename === '..') {
|
|
2994
|
+
return 'download';
|
|
2995
|
+
}
|
|
2996
|
+
const sanitized = basename
|
|
2997
|
+
.replace(/[:*?"<>|]+/g, '_')
|
|
2998
|
+
.replace(/\s+/g, ' ')
|
|
2999
|
+
.trim();
|
|
3000
|
+
return sanitized || 'download';
|
|
3001
|
+
}
|
|
3002
|
+
static _redact_url_for_logging(url) {
|
|
3003
|
+
const raw = String(url ?? '');
|
|
3004
|
+
if (!raw) {
|
|
3005
|
+
return raw;
|
|
3006
|
+
}
|
|
3007
|
+
if (/^data:/i.test(raw)) {
|
|
3008
|
+
return 'data:<redacted>';
|
|
3009
|
+
}
|
|
3010
|
+
try {
|
|
3011
|
+
const parsed = new URL(raw);
|
|
3012
|
+
parsed.username = '';
|
|
3013
|
+
parsed.password = '';
|
|
3014
|
+
const [withoutHash] = parsed.href.split('#', 1);
|
|
3015
|
+
const [withoutQuery] = withoutHash.split('?', 1);
|
|
3016
|
+
return `${withoutQuery}${parsed.search ? '?<redacted>' : ''}${parsed.hash ? '#<redacted>' : ''}`;
|
|
3017
|
+
}
|
|
3018
|
+
catch {
|
|
3019
|
+
const queryIndex = raw.indexOf('?');
|
|
3020
|
+
const hashIndex = raw.indexOf('#');
|
|
3021
|
+
const firstSensitiveIndex = [queryIndex, hashIndex]
|
|
3022
|
+
.filter((index) => index >= 0)
|
|
3023
|
+
.sort((a, b) => a - b)[0];
|
|
3024
|
+
if (firstSensitiveIndex === undefined) {
|
|
3025
|
+
return raw;
|
|
3026
|
+
}
|
|
3027
|
+
return `${raw.slice(0, firstSensitiveIndex)}${queryIndex >= 0 ? '?<redacted>' : ''}${hashIndex >= 0 ? '#<redacted>' : ''}`;
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
static _redact_urls_in_text(value) {
|
|
3031
|
+
return value.replace(/\b(?:https?|blob):[^\s"'<>]+/gi, (match) => BrowserSession._redact_url_for_logging(match));
|
|
3032
|
+
}
|
|
3033
|
+
static _same_origin(left, right) {
|
|
3034
|
+
try {
|
|
3035
|
+
return new URL(left).origin === new URL(right).origin;
|
|
3036
|
+
}
|
|
3037
|
+
catch {
|
|
3038
|
+
return false;
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
2696
3041
|
async get_selector_map(options = {}) {
|
|
2697
3042
|
if (!this.cachedBrowserState) {
|
|
2698
3043
|
await this.get_browser_state_with_recovery({
|
|
@@ -2763,6 +3108,7 @@ export class BrowserSession {
|
|
|
2763
3108
|
if (!page || !node?.xpath) {
|
|
2764
3109
|
return null;
|
|
2765
3110
|
}
|
|
3111
|
+
await this.validate_page_after_action(page);
|
|
2766
3112
|
try {
|
|
2767
3113
|
const locator = page.locator(`xpath=${node.xpath}`);
|
|
2768
3114
|
const count = await locator.count();
|
|
@@ -2784,12 +3130,19 @@ export class BrowserSession {
|
|
|
2784
3130
|
if (!locator) {
|
|
2785
3131
|
throw new Error('Element not found');
|
|
2786
3132
|
}
|
|
2787
|
-
await this._withAbort(
|
|
2788
|
-
|
|
2789
|
-
|
|
3133
|
+
const page = await this._withAbort(this.get_current_page(), signal);
|
|
3134
|
+
await this.validate_page_after_action(page, signal);
|
|
3135
|
+
try {
|
|
3136
|
+
await this._withAbort(locator.click({ timeout: 5000 }), signal);
|
|
3137
|
+
if (clear) {
|
|
3138
|
+
await this._withAbort(locator.fill(text, { timeout: 5000 }), signal);
|
|
3139
|
+
}
|
|
3140
|
+
else {
|
|
3141
|
+
await this._withAbort(locator.type(text, { timeout: 5000 }), signal);
|
|
3142
|
+
}
|
|
2790
3143
|
}
|
|
2791
|
-
|
|
2792
|
-
await this.
|
|
3144
|
+
finally {
|
|
3145
|
+
await this.validate_page_after_action(page, signal);
|
|
2793
3146
|
}
|
|
2794
3147
|
}
|
|
2795
3148
|
async _click_element_node(node, options = {}) {
|
|
@@ -2800,76 +3153,87 @@ export class BrowserSession {
|
|
|
2800
3153
|
throw new Error('Element not found');
|
|
2801
3154
|
}
|
|
2802
3155
|
const page = await this._withAbort(this.get_current_page(), signal);
|
|
3156
|
+
await this.validate_page_after_action(page, signal);
|
|
2803
3157
|
const performClick = async () => {
|
|
2804
3158
|
await this._withAbort(locator.click({ timeout: 5000 }), signal);
|
|
2805
3159
|
};
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
const
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
const
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
url
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
3160
|
+
let result = null;
|
|
3161
|
+
try {
|
|
3162
|
+
const downloadsDir = this.browser_profile.downloads_path;
|
|
3163
|
+
if (downloadsDir && page?.waitForEvent) {
|
|
3164
|
+
ensurePrivateDirectoryIfCreated(downloadsDir);
|
|
3165
|
+
const downloadPromise = page.waitForEvent('download', {
|
|
3166
|
+
timeout: 5000,
|
|
3167
|
+
});
|
|
3168
|
+
await performClick();
|
|
3169
|
+
try {
|
|
3170
|
+
const download = await this._withAbort(downloadPromise, signal);
|
|
3171
|
+
const downloadGuid = uuid7str();
|
|
3172
|
+
const suggested = typeof download.suggestedFilename === 'function'
|
|
3173
|
+
? download.suggestedFilename()
|
|
3174
|
+
: 'download';
|
|
3175
|
+
const downloadUrl = typeof download.url === 'function'
|
|
3176
|
+
? download.url()
|
|
3177
|
+
: (this.currentUrl ?? '');
|
|
3178
|
+
await this._assert_download_url_allowed(download, downloadUrl);
|
|
3179
|
+
await this.event_bus.dispatch(new DownloadStartedEvent({
|
|
3180
|
+
guid: downloadGuid,
|
|
3181
|
+
url: downloadUrl,
|
|
3182
|
+
suggested_filename: suggested,
|
|
3183
|
+
auto_download: false,
|
|
3184
|
+
}));
|
|
3185
|
+
const uniqueFilename = await BrowserSession.get_unique_filename(downloadsDir, suggested);
|
|
3186
|
+
const downloadPath = path.join(downloadsDir, uniqueFilename);
|
|
3187
|
+
if (typeof download.saveAs === 'function') {
|
|
3188
|
+
await download.saveAs(downloadPath);
|
|
3189
|
+
chmodPrivateFileBestEffort(downloadPath);
|
|
3190
|
+
}
|
|
3191
|
+
const stats = fs.existsSync(downloadPath)
|
|
3192
|
+
? fs.statSync(downloadPath)
|
|
3193
|
+
: null;
|
|
3194
|
+
await this.event_bus.dispatch(new DownloadProgressEvent({
|
|
3195
|
+
guid: downloadGuid,
|
|
3196
|
+
received_bytes: stats?.size ?? 0,
|
|
3197
|
+
total_bytes: stats?.size ?? 0,
|
|
3198
|
+
state: 'completed',
|
|
3199
|
+
}));
|
|
3200
|
+
const fileDownloadedResult = await this.event_bus.dispatch(new FileDownloadedEvent({
|
|
3201
|
+
guid: downloadGuid,
|
|
3202
|
+
url: downloadUrl,
|
|
3203
|
+
path: downloadPath,
|
|
3204
|
+
file_name: uniqueFilename,
|
|
3205
|
+
file_size: stats?.size ?? 0,
|
|
3206
|
+
file_type: path.extname(uniqueFilename).replace('.', '') || null,
|
|
3207
|
+
mime_type: null,
|
|
3208
|
+
auto_download: false,
|
|
3209
|
+
}));
|
|
3210
|
+
if (fileDownloadedResult.handler_results.length === 0) {
|
|
3211
|
+
this.add_downloaded_file(downloadPath);
|
|
3212
|
+
}
|
|
3213
|
+
result = downloadPath;
|
|
2851
3214
|
}
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
3215
|
+
catch (error) {
|
|
3216
|
+
if (this._isAbortError(error)) {
|
|
3217
|
+
throw error;
|
|
3218
|
+
}
|
|
3219
|
+
if (error instanceof URLNotAllowedError) {
|
|
3220
|
+
throw error;
|
|
3221
|
+
}
|
|
3222
|
+
this.logger.debug(`No download triggered within timeout: ${error.message}`);
|
|
2857
3223
|
}
|
|
2858
|
-
|
|
3224
|
+
}
|
|
3225
|
+
else {
|
|
3226
|
+
await performClick();
|
|
2859
3227
|
}
|
|
2860
3228
|
}
|
|
2861
|
-
|
|
2862
|
-
await
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
if (page) {
|
|
2866
|
-
await this._syncCurrentTabFromPage(page);
|
|
2867
|
-
if (this.historyStack[this.historyStack.length - 1] !== this.currentUrl) {
|
|
3229
|
+
finally {
|
|
3230
|
+
await this.validate_page_after_action(page, signal);
|
|
3231
|
+
if (page &&
|
|
3232
|
+
this.historyStack[this.historyStack.length - 1] !== this.currentUrl) {
|
|
2868
3233
|
this.historyStack.push(this.currentUrl);
|
|
2869
3234
|
}
|
|
2870
3235
|
}
|
|
2871
|
-
|
|
2872
|
-
return null;
|
|
3236
|
+
return result;
|
|
2873
3237
|
}
|
|
2874
3238
|
async _waitForLoad(page, timeout = 5000, signal = null) {
|
|
2875
3239
|
if (!page || typeof page.waitForLoadState !== 'function') {
|
|
@@ -2889,11 +3253,14 @@ export class BrowserSession {
|
|
|
2889
3253
|
/**
|
|
2890
3254
|
* Get all cookies from the current browser context
|
|
2891
3255
|
*/
|
|
2892
|
-
async get_cookies() {
|
|
2893
|
-
|
|
2894
|
-
|
|
3256
|
+
async get_cookies(options = {}) {
|
|
3257
|
+
const cookies = this.browser_context?.cookies
|
|
3258
|
+
? await this.browser_context.cookies()
|
|
3259
|
+
: [];
|
|
3260
|
+
if (options.include_blocked || !this._has_url_access_restrictions()) {
|
|
3261
|
+
return cookies;
|
|
2895
3262
|
}
|
|
2896
|
-
return
|
|
3263
|
+
return this._filter_cookies_for_exposure(cookies);
|
|
2897
3264
|
}
|
|
2898
3265
|
/**
|
|
2899
3266
|
* Save cookies to a file (deprecated, use save_storage_state instead)
|
|
@@ -2925,19 +3292,19 @@ export class BrowserSession {
|
|
|
2925
3292
|
const resolvedPath = path.resolve(targetPath);
|
|
2926
3293
|
const dirPath = path.dirname(resolvedPath);
|
|
2927
3294
|
// Create directory if it doesn't exist
|
|
2928
|
-
|
|
2929
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
2930
|
-
}
|
|
3295
|
+
ensurePrivateDirectoryIfCreated(dirPath);
|
|
2931
3296
|
// Get storage state from browser context
|
|
2932
|
-
const
|
|
3297
|
+
const rawStorageState = await this.browser_context.storageState();
|
|
3298
|
+
const storageState = this._sanitize_storage_state_for_save(rawStorageState);
|
|
2933
3299
|
// Write to temporary file first
|
|
2934
3300
|
const tempPath = `${resolvedPath}.tmp`;
|
|
2935
|
-
|
|
3301
|
+
writePrivateStorageFile(tempPath, JSON.stringify(storageState, null, 2));
|
|
2936
3302
|
// Backup existing file if present
|
|
2937
3303
|
if (fs.existsSync(resolvedPath)) {
|
|
2938
3304
|
const backupPath = `${resolvedPath}.bak`;
|
|
2939
3305
|
try {
|
|
2940
3306
|
fs.renameSync(resolvedPath, backupPath);
|
|
3307
|
+
chmodPrivateStorageFile(backupPath);
|
|
2941
3308
|
}
|
|
2942
3309
|
catch (error) {
|
|
2943
3310
|
// Ignore backup errors
|
|
@@ -2945,6 +3312,7 @@ export class BrowserSession {
|
|
|
2945
3312
|
}
|
|
2946
3313
|
// Move temp file to target
|
|
2947
3314
|
fs.renameSync(tempPath, resolvedPath);
|
|
3315
|
+
chmodPrivateStorageFile(resolvedPath);
|
|
2948
3316
|
const cookieCount = storageState.cookies?.length || 0;
|
|
2949
3317
|
this.logger.info(`🍪 Saved ${cookieCount} cookies to ${path.basename(resolvedPath)}`);
|
|
2950
3318
|
}
|
|
@@ -2971,15 +3339,216 @@ export class BrowserSession {
|
|
|
2971
3339
|
if (this.browser_context?.addCookies) {
|
|
2972
3340
|
// Add cookies to context
|
|
2973
3341
|
if (storageState.cookies && Array.isArray(storageState.cookies)) {
|
|
2974
|
-
|
|
2975
|
-
|
|
3342
|
+
const allowedCookies = this._filter_storage_state_cookies(storageState.cookies);
|
|
3343
|
+
if (allowedCookies.length > 0) {
|
|
3344
|
+
await this.browser_context.addCookies(allowedCookies);
|
|
3345
|
+
}
|
|
3346
|
+
this.logger.info(`🍪 Loaded ${allowedCookies.length} cookies from ${path.basename(resolvedPath)}`);
|
|
2976
3347
|
}
|
|
2977
3348
|
}
|
|
3349
|
+
if (Array.isArray(storageState.origins)) {
|
|
3350
|
+
await this._apply_storage_state_origins(storageState.origins);
|
|
3351
|
+
}
|
|
2978
3352
|
}
|
|
2979
3353
|
catch (error) {
|
|
2980
3354
|
this.logger.warning(`❌ Failed to load storage state: ${error.message}`);
|
|
2981
3355
|
}
|
|
2982
3356
|
}
|
|
3357
|
+
async _apply_storage_state_origins(origins) {
|
|
3358
|
+
const browserContext = this.browser_context;
|
|
3359
|
+
if (!browserContext?.newPage) {
|
|
3360
|
+
return;
|
|
3361
|
+
}
|
|
3362
|
+
for (const originState of origins) {
|
|
3363
|
+
const origin = originState &&
|
|
3364
|
+
typeof originState === 'object' &&
|
|
3365
|
+
'origin' in originState &&
|
|
3366
|
+
typeof originState.origin === 'string'
|
|
3367
|
+
? originState.origin.trim()
|
|
3368
|
+
: '';
|
|
3369
|
+
if (!origin || !/^https?:\/\//i.test(origin)) {
|
|
3370
|
+
continue;
|
|
3371
|
+
}
|
|
3372
|
+
const denialReason = this._get_url_access_denial_reason(origin);
|
|
3373
|
+
if (denialReason) {
|
|
3374
|
+
this.logger.warning(`Skipping storage origin ${BrowserSession._redact_url_for_logging(origin)}: ${denialReason}`);
|
|
3375
|
+
continue;
|
|
3376
|
+
}
|
|
3377
|
+
const localStorageEntries = this._normalize_storage_entries(originState.localStorage);
|
|
3378
|
+
const sessionStorageEntries = this._normalize_storage_entries(originState.sessionStorage);
|
|
3379
|
+
if (localStorageEntries.length === 0 &&
|
|
3380
|
+
sessionStorageEntries.length === 0) {
|
|
3381
|
+
continue;
|
|
3382
|
+
}
|
|
3383
|
+
let page = null;
|
|
3384
|
+
try {
|
|
3385
|
+
page = await browserContext.newPage();
|
|
3386
|
+
await page.goto?.(origin, {
|
|
3387
|
+
waitUntil: 'domcontentloaded',
|
|
3388
|
+
timeout: 5_000,
|
|
3389
|
+
});
|
|
3390
|
+
const finalUrl = typeof page.url === 'function' ? page.url() : origin;
|
|
3391
|
+
const finalDenialReason = this._get_url_access_denial_reason(finalUrl);
|
|
3392
|
+
if (finalDenialReason) {
|
|
3393
|
+
this.logger.warning(`Skipping storage origin ${BrowserSession._redact_url_for_logging(origin)} after redirect to blocked URL: ${finalDenialReason}`);
|
|
3394
|
+
try {
|
|
3395
|
+
await page.goto?.('about:blank', {
|
|
3396
|
+
waitUntil: 'load',
|
|
3397
|
+
timeout: 5_000,
|
|
3398
|
+
});
|
|
3399
|
+
}
|
|
3400
|
+
catch {
|
|
3401
|
+
// The temporary page is closed below; resetting first is best effort.
|
|
3402
|
+
}
|
|
3403
|
+
continue;
|
|
3404
|
+
}
|
|
3405
|
+
if (!BrowserSession._same_origin(origin, finalUrl)) {
|
|
3406
|
+
this.logger.warning(`Skipping storage origin ${BrowserSession._redact_url_for_logging(origin)} after redirect to a different origin`);
|
|
3407
|
+
try {
|
|
3408
|
+
await page.goto?.('about:blank', {
|
|
3409
|
+
waitUntil: 'load',
|
|
3410
|
+
timeout: 5_000,
|
|
3411
|
+
});
|
|
3412
|
+
}
|
|
3413
|
+
catch {
|
|
3414
|
+
// The temporary page is closed below; resetting first is best effort.
|
|
3415
|
+
}
|
|
3416
|
+
continue;
|
|
3417
|
+
}
|
|
3418
|
+
await page.evaluate?.((payload) => {
|
|
3419
|
+
for (const entry of payload.localStorageEntries) {
|
|
3420
|
+
window.localStorage.setItem(entry.name, entry.value);
|
|
3421
|
+
}
|
|
3422
|
+
for (const entry of payload.sessionStorageEntries) {
|
|
3423
|
+
window.sessionStorage.setItem(entry.name, entry.value);
|
|
3424
|
+
}
|
|
3425
|
+
}, {
|
|
3426
|
+
localStorageEntries,
|
|
3427
|
+
sessionStorageEntries,
|
|
3428
|
+
});
|
|
3429
|
+
}
|
|
3430
|
+
catch (error) {
|
|
3431
|
+
this.logger.debug(`Failed to apply origin storage for ${BrowserSession._redact_url_for_logging(origin)}: ${error.message}`);
|
|
3432
|
+
}
|
|
3433
|
+
finally {
|
|
3434
|
+
try {
|
|
3435
|
+
await page?.close?.();
|
|
3436
|
+
}
|
|
3437
|
+
catch {
|
|
3438
|
+
// Ignore cleanup errors.
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
_normalize_storage_entries(entries) {
|
|
3444
|
+
if (!Array.isArray(entries)) {
|
|
3445
|
+
return [];
|
|
3446
|
+
}
|
|
3447
|
+
const normalized = [];
|
|
3448
|
+
for (const entry of entries) {
|
|
3449
|
+
const name = entry && typeof entry === 'object' && 'name' in entry
|
|
3450
|
+
? String(entry.name ?? '')
|
|
3451
|
+
: '';
|
|
3452
|
+
if (!name) {
|
|
3453
|
+
continue;
|
|
3454
|
+
}
|
|
3455
|
+
const value = entry && typeof entry === 'object' && 'value' in entry
|
|
3456
|
+
? String(entry.value ?? '')
|
|
3457
|
+
: '';
|
|
3458
|
+
normalized.push({ name, value });
|
|
3459
|
+
}
|
|
3460
|
+
return normalized;
|
|
3461
|
+
}
|
|
3462
|
+
_sanitize_storage_state_for_save(storageState) {
|
|
3463
|
+
if (!this._has_url_access_restrictions()) {
|
|
3464
|
+
return storageState;
|
|
3465
|
+
}
|
|
3466
|
+
const normalized = storageState && typeof storageState === 'object'
|
|
3467
|
+
? { ...storageState }
|
|
3468
|
+
: {};
|
|
3469
|
+
const cookies = Array.isArray(normalized.cookies)
|
|
3470
|
+
? this._filter_storage_state_cookies(normalized.cookies)
|
|
3471
|
+
: [];
|
|
3472
|
+
const origins = Array.isArray(normalized.origins)
|
|
3473
|
+
? normalized.origins.filter((originState) => {
|
|
3474
|
+
const origin = originState &&
|
|
3475
|
+
typeof originState === 'object' &&
|
|
3476
|
+
'origin' in originState &&
|
|
3477
|
+
typeof originState.origin === 'string'
|
|
3478
|
+
? originState.origin.trim()
|
|
3479
|
+
: '';
|
|
3480
|
+
const denialReason = origin
|
|
3481
|
+
? this._get_url_access_denial_reason(origin)
|
|
3482
|
+
: 'invalid_url';
|
|
3483
|
+
if (!denialReason) {
|
|
3484
|
+
return true;
|
|
3485
|
+
}
|
|
3486
|
+
this.logger.warning(`Skipping saved storage origin ${origin
|
|
3487
|
+
? BrowserSession._redact_url_for_logging(origin)
|
|
3488
|
+
: '<invalid>'}: ${denialReason}`);
|
|
3489
|
+
return false;
|
|
3490
|
+
})
|
|
3491
|
+
: [];
|
|
3492
|
+
return {
|
|
3493
|
+
...normalized,
|
|
3494
|
+
cookies,
|
|
3495
|
+
origins,
|
|
3496
|
+
};
|
|
3497
|
+
}
|
|
3498
|
+
_filter_storage_state_cookies(cookies) {
|
|
3499
|
+
return cookies.filter((cookie) => {
|
|
3500
|
+
const denialReason = this._get_cookie_access_denial_reason(cookie);
|
|
3501
|
+
if (!denialReason) {
|
|
3502
|
+
return true;
|
|
3503
|
+
}
|
|
3504
|
+
const cookieName = cookie && typeof cookie === 'object' && 'name' in cookie
|
|
3505
|
+
? String(cookie.name ?? '')
|
|
3506
|
+
: '';
|
|
3507
|
+
this.logger.warning(`Skipping storage cookie ${cookieName || '<unnamed>'}: ${denialReason}`);
|
|
3508
|
+
return false;
|
|
3509
|
+
});
|
|
3510
|
+
}
|
|
3511
|
+
_filter_cookies_for_exposure(cookies) {
|
|
3512
|
+
return cookies.filter((cookie) => {
|
|
3513
|
+
const denialReason = this._get_cookie_access_denial_reason(cookie);
|
|
3514
|
+
if (!denialReason) {
|
|
3515
|
+
return true;
|
|
3516
|
+
}
|
|
3517
|
+
const cookieName = cookie && typeof cookie === 'object' && 'name' in cookie
|
|
3518
|
+
? String(cookie.name ?? '')
|
|
3519
|
+
: '';
|
|
3520
|
+
this.logger.debug(`Skipping exposed cookie ${cookieName || '<unnamed>'}: ${denialReason}`);
|
|
3521
|
+
return false;
|
|
3522
|
+
});
|
|
3523
|
+
}
|
|
3524
|
+
_get_cookie_access_denial_reason(cookie) {
|
|
3525
|
+
if (!cookie || typeof cookie !== 'object') {
|
|
3526
|
+
return null;
|
|
3527
|
+
}
|
|
3528
|
+
const cookieLike = cookie;
|
|
3529
|
+
const explicitUrl = typeof cookieLike.url === 'string' ? cookieLike.url.trim() : '';
|
|
3530
|
+
if (explicitUrl) {
|
|
3531
|
+
return this._get_url_access_denial_reason(explicitUrl);
|
|
3532
|
+
}
|
|
3533
|
+
const rawDomain = typeof cookieLike.domain === 'string' ? cookieLike.domain.trim() : '';
|
|
3534
|
+
const host = rawDomain.replace(/^\./, '');
|
|
3535
|
+
if (!host) {
|
|
3536
|
+
return null;
|
|
3537
|
+
}
|
|
3538
|
+
const protocols = cookieLike.secure === true ? ['https:'] : ['https:', 'http:'];
|
|
3539
|
+
let lastReason = null;
|
|
3540
|
+
for (const protocol of protocols) {
|
|
3541
|
+
const reason = this._get_url_access_denial_reason(`${protocol}//${host}`);
|
|
3542
|
+
if (!reason) {
|
|
3543
|
+
return null;
|
|
3544
|
+
}
|
|
3545
|
+
if (reason === 'in_prohibited_domains') {
|
|
3546
|
+
return reason;
|
|
3547
|
+
}
|
|
3548
|
+
lastReason = reason;
|
|
3549
|
+
}
|
|
3550
|
+
return lastReason;
|
|
3551
|
+
}
|
|
2983
3552
|
// ==================== JavaScript Execution ====================
|
|
2984
3553
|
/**
|
|
2985
3554
|
* Execute JavaScript in the current page context
|
|
@@ -2989,7 +3558,24 @@ export class BrowserSession {
|
|
|
2989
3558
|
if (!page) {
|
|
2990
3559
|
throw new Error('No page available to execute JavaScript');
|
|
2991
3560
|
}
|
|
2992
|
-
|
|
3561
|
+
await this.validate_page_after_action(page);
|
|
3562
|
+
try {
|
|
3563
|
+
return await page.evaluate(script);
|
|
3564
|
+
}
|
|
3565
|
+
finally {
|
|
3566
|
+
await this._waitForLoad(page, 5000);
|
|
3567
|
+
await this._assert_page_url_allowed_or_rollback(page);
|
|
3568
|
+
await this._syncCurrentTabFromPage(page);
|
|
3569
|
+
this.cachedBrowserState = null;
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
async validate_page_after_action(page, signal = null) {
|
|
3573
|
+
await this._waitForLoad(page, 5000, signal);
|
|
3574
|
+
if (page) {
|
|
3575
|
+
await this._assert_page_url_allowed_or_rollback(page);
|
|
3576
|
+
await this._syncCurrentTabFromPage(page);
|
|
3577
|
+
}
|
|
3578
|
+
this.cachedBrowserState = null;
|
|
2993
3579
|
}
|
|
2994
3580
|
// ==================== Page Information ====================
|
|
2995
3581
|
/**
|
|
@@ -3000,49 +3586,55 @@ export class BrowserSession {
|
|
|
3000
3586
|
if (!targetPage) {
|
|
3001
3587
|
return null;
|
|
3002
3588
|
}
|
|
3003
|
-
|
|
3589
|
+
await this.validate_page_after_action(targetPage);
|
|
3590
|
+
try {
|
|
3591
|
+
const pageData = await targetPage.evaluate(() => {
|
|
3592
|
+
return {
|
|
3593
|
+
// Current viewport dimensions
|
|
3594
|
+
viewport_width: window.innerWidth,
|
|
3595
|
+
viewport_height: window.innerHeight,
|
|
3596
|
+
// Total page dimensions
|
|
3597
|
+
page_width: Math.max(document.documentElement.scrollWidth, document.body.scrollWidth || 0),
|
|
3598
|
+
page_height: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight || 0),
|
|
3599
|
+
// Current scroll position
|
|
3600
|
+
scroll_x: window.scrollX ||
|
|
3601
|
+
window.pageXOffset ||
|
|
3602
|
+
document.documentElement.scrollLeft ||
|
|
3603
|
+
0,
|
|
3604
|
+
scroll_y: window.scrollY ||
|
|
3605
|
+
window.pageYOffset ||
|
|
3606
|
+
document.documentElement.scrollTop ||
|
|
3607
|
+
0,
|
|
3608
|
+
};
|
|
3609
|
+
});
|
|
3610
|
+
// Calculate derived values
|
|
3611
|
+
const viewport_width = Math.floor(pageData.viewport_width);
|
|
3612
|
+
const viewport_height = Math.floor(pageData.viewport_height);
|
|
3613
|
+
const page_width = Math.floor(pageData.page_width);
|
|
3614
|
+
const page_height = Math.floor(pageData.page_height);
|
|
3615
|
+
const scroll_x = Math.floor(pageData.scroll_x);
|
|
3616
|
+
const scroll_y = Math.floor(pageData.scroll_y);
|
|
3617
|
+
// Calculate scroll information
|
|
3618
|
+
const pixels_above = scroll_y;
|
|
3619
|
+
const pixels_below = Math.max(0, page_height - (scroll_y + viewport_height));
|
|
3620
|
+
const pixels_left = scroll_x;
|
|
3621
|
+
const pixels_right = Math.max(0, page_width - (scroll_x + viewport_width));
|
|
3004
3622
|
return {
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
0,
|
|
3016
|
-
scroll_y: window.scrollY ||
|
|
3017
|
-
window.pageYOffset ||
|
|
3018
|
-
document.documentElement.scrollTop ||
|
|
3019
|
-
0,
|
|
3623
|
+
viewport_width,
|
|
3624
|
+
viewport_height,
|
|
3625
|
+
page_width,
|
|
3626
|
+
page_height,
|
|
3627
|
+
scroll_x,
|
|
3628
|
+
scroll_y,
|
|
3629
|
+
pixels_above,
|
|
3630
|
+
pixels_below,
|
|
3631
|
+
pixels_left,
|
|
3632
|
+
pixels_right,
|
|
3020
3633
|
};
|
|
3021
|
-
}
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
const page_width = Math.floor(pageData.page_width);
|
|
3026
|
-
const page_height = Math.floor(pageData.page_height);
|
|
3027
|
-
const scroll_x = Math.floor(pageData.scroll_x);
|
|
3028
|
-
const scroll_y = Math.floor(pageData.scroll_y);
|
|
3029
|
-
// Calculate scroll information
|
|
3030
|
-
const pixels_above = scroll_y;
|
|
3031
|
-
const pixels_below = Math.max(0, page_height - (scroll_y + viewport_height));
|
|
3032
|
-
const pixels_left = scroll_x;
|
|
3033
|
-
const pixels_right = Math.max(0, page_width - (scroll_x + viewport_width));
|
|
3034
|
-
return {
|
|
3035
|
-
viewport_width,
|
|
3036
|
-
viewport_height,
|
|
3037
|
-
page_width,
|
|
3038
|
-
page_height,
|
|
3039
|
-
scroll_x,
|
|
3040
|
-
scroll_y,
|
|
3041
|
-
pixels_above,
|
|
3042
|
-
pixels_below,
|
|
3043
|
-
pixels_left,
|
|
3044
|
-
pixels_right,
|
|
3045
|
-
};
|
|
3634
|
+
}
|
|
3635
|
+
finally {
|
|
3636
|
+
await this.validate_page_after_action(targetPage);
|
|
3637
|
+
}
|
|
3046
3638
|
}
|
|
3047
3639
|
/**
|
|
3048
3640
|
* Get the HTML content of the current page
|
|
@@ -3052,7 +3644,13 @@ export class BrowserSession {
|
|
|
3052
3644
|
if (!page) {
|
|
3053
3645
|
return '';
|
|
3054
3646
|
}
|
|
3055
|
-
|
|
3647
|
+
await this.validate_page_after_action(page);
|
|
3648
|
+
try {
|
|
3649
|
+
return await page.content();
|
|
3650
|
+
}
|
|
3651
|
+
finally {
|
|
3652
|
+
await this.validate_page_after_action(page);
|
|
3653
|
+
}
|
|
3056
3654
|
}
|
|
3057
3655
|
/**
|
|
3058
3656
|
* Get a debug view of the page structure including iframes
|
|
@@ -3062,6 +3660,7 @@ export class BrowserSession {
|
|
|
3062
3660
|
if (!page) {
|
|
3063
3661
|
return '';
|
|
3064
3662
|
}
|
|
3663
|
+
await this.validate_page_after_action(page);
|
|
3065
3664
|
const debug_script = `(() => {
|
|
3066
3665
|
function getPageStructure(element = document, depth = 0, maxDepth = 10) {
|
|
3067
3666
|
if (depth >= maxDepth) return '';
|
|
@@ -3125,20 +3724,31 @@ export class BrowserSession {
|
|
|
3125
3724
|
|
|
3126
3725
|
return getPageStructure();
|
|
3127
3726
|
})()`;
|
|
3128
|
-
|
|
3727
|
+
try {
|
|
3728
|
+
return await page.evaluate(debug_script);
|
|
3729
|
+
}
|
|
3730
|
+
finally {
|
|
3731
|
+
await this.validate_page_after_action(page);
|
|
3732
|
+
}
|
|
3129
3733
|
}
|
|
3130
3734
|
// ==================== Navigation & History ====================
|
|
3131
3735
|
/**
|
|
3132
3736
|
* Navigate forward in browser history
|
|
3133
3737
|
*/
|
|
3134
3738
|
async go_forward() {
|
|
3739
|
+
const page = await this.get_current_page();
|
|
3740
|
+
if (!page?.goForward) {
|
|
3741
|
+
return;
|
|
3742
|
+
}
|
|
3743
|
+
await this.validate_page_after_action(page);
|
|
3135
3744
|
try {
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
await page.goForward({ timeout: 10000, waitUntil: 'load' });
|
|
3139
|
-
}
|
|
3745
|
+
await page.goForward({ timeout: 10000, waitUntil: 'load' });
|
|
3746
|
+
await this._waitForStableNetwork(page);
|
|
3140
3747
|
}
|
|
3141
3748
|
catch (error) {
|
|
3749
|
+
if (error instanceof URLNotAllowedError) {
|
|
3750
|
+
throw error;
|
|
3751
|
+
}
|
|
3142
3752
|
this.logger.debug(`⏭️ Error during go_forward: ${error.message}`);
|
|
3143
3753
|
// Verify page is still usable after navigation error
|
|
3144
3754
|
if (error.message.toLowerCase().includes('timeout')) {
|
|
@@ -3151,22 +3761,33 @@ export class BrowserSession {
|
|
|
3151
3761
|
}
|
|
3152
3762
|
}
|
|
3153
3763
|
}
|
|
3764
|
+
finally {
|
|
3765
|
+
await this.validate_page_after_action(page);
|
|
3766
|
+
}
|
|
3154
3767
|
}
|
|
3155
3768
|
/**
|
|
3156
3769
|
* Refresh the current page
|
|
3157
3770
|
*/
|
|
3158
3771
|
async refresh() {
|
|
3772
|
+
const page = await this.get_current_page();
|
|
3773
|
+
if (!page?.reload) {
|
|
3774
|
+
return;
|
|
3775
|
+
}
|
|
3776
|
+
await this.validate_page_after_action(page);
|
|
3159
3777
|
try {
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
await page.reload({ waitUntil: 'domcontentloaded' });
|
|
3164
|
-
await this._waitForStableNetwork(page);
|
|
3165
|
-
}
|
|
3778
|
+
this.currentPageLoadingStatus = null;
|
|
3779
|
+
await page.reload({ waitUntil: 'domcontentloaded' });
|
|
3780
|
+
await this._waitForStableNetwork(page);
|
|
3166
3781
|
}
|
|
3167
3782
|
catch (error) {
|
|
3783
|
+
if (error instanceof URLNotAllowedError) {
|
|
3784
|
+
throw error;
|
|
3785
|
+
}
|
|
3168
3786
|
this.logger.debug(`🔄 Error during refresh: ${error.message}`);
|
|
3169
3787
|
}
|
|
3788
|
+
finally {
|
|
3789
|
+
await this.validate_page_after_action(page);
|
|
3790
|
+
}
|
|
3170
3791
|
}
|
|
3171
3792
|
// ==================== Element Waiting ====================
|
|
3172
3793
|
/**
|
|
@@ -3177,7 +3798,13 @@ export class BrowserSession {
|
|
|
3177
3798
|
if (!page) {
|
|
3178
3799
|
throw new Error('No page available');
|
|
3179
3800
|
}
|
|
3180
|
-
await
|
|
3801
|
+
await this.validate_page_after_action(page);
|
|
3802
|
+
try {
|
|
3803
|
+
await page.waitForSelector(selector, { state: 'visible', timeout });
|
|
3804
|
+
}
|
|
3805
|
+
finally {
|
|
3806
|
+
await this.validate_page_after_action(page);
|
|
3807
|
+
}
|
|
3181
3808
|
}
|
|
3182
3809
|
// ==================== Screenshots ====================
|
|
3183
3810
|
/**
|
|
@@ -3191,15 +3818,17 @@ export class BrowserSession {
|
|
|
3191
3818
|
if (!page) {
|
|
3192
3819
|
throw new Error('No page available for screenshot');
|
|
3193
3820
|
}
|
|
3821
|
+
await this.validate_page_after_action(page);
|
|
3194
3822
|
if (!this.browser_context) {
|
|
3195
3823
|
throw new Error('Browser context is not set');
|
|
3196
3824
|
}
|
|
3197
3825
|
// Check if it's a new tab page
|
|
3198
3826
|
const url = page.url();
|
|
3827
|
+
const logUrl = BrowserSession._redact_url_for_logging(url);
|
|
3199
3828
|
if (url === 'about:blank' ||
|
|
3200
3829
|
url === 'chrome://newtab/' ||
|
|
3201
3830
|
url === 'edge://newtab/') {
|
|
3202
|
-
this.logger.warning(`▫️ Skipping screenshot of empty page: ${
|
|
3831
|
+
this.logger.warning(`▫️ Skipping screenshot of empty page: ${logUrl}`);
|
|
3203
3832
|
// Return a 4px placeholder
|
|
3204
3833
|
return 'iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAD0lEQVQIHWP8//8/AxYMACgtBP9g8jqYAAAAAElFTkSuQmCC';
|
|
3205
3834
|
}
|
|
@@ -3213,7 +3842,7 @@ export class BrowserSession {
|
|
|
3213
3842
|
// Take screenshot using CDP for better performance
|
|
3214
3843
|
let cdp_session = null;
|
|
3215
3844
|
try {
|
|
3216
|
-
this.logger.debug(`📸 Taking ${full_page ? 'full-page' : 'viewport'} PNG screenshot via CDP: ${
|
|
3845
|
+
this.logger.debug(`📸 Taking ${full_page ? 'full-page' : 'viewport'} PNG screenshot via CDP: ${logUrl}`);
|
|
3217
3846
|
// Create CDP session for the screenshot
|
|
3218
3847
|
cdp_session = await this.get_or_create_cdp_session(page);
|
|
3219
3848
|
// Capture screenshot via CDP
|
|
@@ -3234,17 +3863,17 @@ export class BrowserSession {
|
|
|
3234
3863
|
const screenshot_response = await cdp_session.send('Page.captureScreenshot', screenshotParams);
|
|
3235
3864
|
const screenshot_b64 = screenshot_response.data;
|
|
3236
3865
|
if (!screenshot_b64) {
|
|
3237
|
-
throw new Error(`CDP returned empty screenshot data for page ${
|
|
3866
|
+
throw new Error(`CDP returned empty screenshot data for page ${logUrl}`);
|
|
3238
3867
|
}
|
|
3239
3868
|
return screenshot_b64;
|
|
3240
3869
|
}
|
|
3241
3870
|
catch (error) {
|
|
3242
3871
|
const error_str = error.message || String(error);
|
|
3243
3872
|
if (error_str.toLowerCase().includes('timeout')) {
|
|
3244
|
-
this.logger.warning(`⏱️ Screenshot timed out on page ${
|
|
3873
|
+
this.logger.warning(`⏱️ Screenshot timed out on page ${logUrl}: ${error_str}`);
|
|
3245
3874
|
}
|
|
3246
3875
|
else {
|
|
3247
|
-
this.logger.error(`❌ Screenshot failed on page ${
|
|
3876
|
+
this.logger.error(`❌ Screenshot failed on page ${logUrl}: ${error_str}`);
|
|
3248
3877
|
}
|
|
3249
3878
|
throw error;
|
|
3250
3879
|
}
|
|
@@ -3257,6 +3886,7 @@ export class BrowserSession {
|
|
|
3257
3886
|
// Ignore detach errors
|
|
3258
3887
|
}
|
|
3259
3888
|
}
|
|
3889
|
+
await this.validate_page_after_action(page);
|
|
3260
3890
|
}
|
|
3261
3891
|
}
|
|
3262
3892
|
// ==================== Event Listeners ====================
|
|
@@ -3324,6 +3954,16 @@ export class BrowserSession {
|
|
|
3324
3954
|
// Keep tab url fallback when page url is unavailable.
|
|
3325
3955
|
}
|
|
3326
3956
|
}
|
|
3957
|
+
const exposedTab = this._sanitize_tab_for_exposure({
|
|
3958
|
+
page_id,
|
|
3959
|
+
tab_id,
|
|
3960
|
+
url: currentUrl,
|
|
3961
|
+
title: tab.title || currentUrl,
|
|
3962
|
+
});
|
|
3963
|
+
if (exposedTab.url !== currentUrl) {
|
|
3964
|
+
tabs_info.push(exposedTab);
|
|
3965
|
+
continue;
|
|
3966
|
+
}
|
|
3327
3967
|
// Skip chrome:// pages and new tab pages
|
|
3328
3968
|
const isNewTab = currentUrl === 'about:blank' ||
|
|
3329
3969
|
currentUrl.startsWith('chrome://newtab');
|
|
@@ -3359,7 +3999,7 @@ export class BrowserSession {
|
|
|
3359
3999
|
tabs_info.push({ page_id, tab_id, url: currentUrl, title });
|
|
3360
4000
|
}
|
|
3361
4001
|
catch (error) {
|
|
3362
|
-
this.logger.debug(`⚠️ Failed to get tab info for tab #${page_id}: ${currentUrl} (using fallback title)`);
|
|
4002
|
+
this.logger.debug(`⚠️ Failed to get tab info for tab #${page_id}: ${BrowserSession._redact_url_for_logging(currentUrl)} (using fallback title)`);
|
|
3363
4003
|
if (isNewTab) {
|
|
3364
4004
|
tabs_info.push({
|
|
3365
4005
|
page_id,
|
|
@@ -3415,22 +4055,28 @@ export class BrowserSession {
|
|
|
3415
4055
|
viewport_height: 0,
|
|
3416
4056
|
};
|
|
3417
4057
|
}
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
4058
|
+
await this.validate_page_after_action(page);
|
|
4059
|
+
try {
|
|
4060
|
+
return await page.evaluate(() => {
|
|
4061
|
+
return {
|
|
4062
|
+
scroll_x: window.scrollX ||
|
|
4063
|
+
window.pageXOffset ||
|
|
4064
|
+
document.documentElement.scrollLeft ||
|
|
4065
|
+
0,
|
|
4066
|
+
scroll_y: window.scrollY ||
|
|
4067
|
+
window.pageYOffset ||
|
|
4068
|
+
document.documentElement.scrollTop ||
|
|
4069
|
+
0,
|
|
4070
|
+
page_width: Math.max(document.documentElement.scrollWidth, document.body.scrollWidth || 0),
|
|
4071
|
+
page_height: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight || 0),
|
|
4072
|
+
viewport_width: window.innerWidth,
|
|
4073
|
+
viewport_height: window.innerHeight,
|
|
4074
|
+
};
|
|
4075
|
+
});
|
|
4076
|
+
}
|
|
4077
|
+
finally {
|
|
4078
|
+
await this.validate_page_after_action(page);
|
|
4079
|
+
}
|
|
3434
4080
|
}
|
|
3435
4081
|
/**
|
|
3436
4082
|
* Get a summary of the current browser state
|
|
@@ -3469,6 +4115,10 @@ export class BrowserSession {
|
|
|
3469
4115
|
async get_minimal_state_summary(include_recent_events = false) {
|
|
3470
4116
|
try {
|
|
3471
4117
|
const page = await this.get_current_page();
|
|
4118
|
+
if (page) {
|
|
4119
|
+
await this._assert_page_url_allowed_or_rollback(page);
|
|
4120
|
+
await this._syncCurrentTabFromPage(page);
|
|
4121
|
+
}
|
|
3472
4122
|
const url = page ? page.url() : 'unknown';
|
|
3473
4123
|
// Try to get title safely
|
|
3474
4124
|
let title = 'Page Load Error';
|
|
@@ -3554,11 +4204,13 @@ export class BrowserSession {
|
|
|
3554
4204
|
if (!page) {
|
|
3555
4205
|
throw new Error('No current page available');
|
|
3556
4206
|
}
|
|
4207
|
+
await this.validate_page_after_action(page);
|
|
3557
4208
|
const page_url = page.url();
|
|
4209
|
+
const logPageUrl = BrowserSession._redact_url_for_logging(page_url);
|
|
3558
4210
|
// Check for new tab or chrome:// pages - fast path
|
|
3559
4211
|
const is_empty_page = this._is_new_tab_page(page_url) || page_url.startsWith('chrome://');
|
|
3560
4212
|
if (is_empty_page) {
|
|
3561
|
-
this.logger.debug(`⚡ Fast path for empty page: ${
|
|
4213
|
+
this.logger.debug(`⚡ Fast path for empty page: ${logPageUrl}`);
|
|
3562
4214
|
// Create minimal DOM state
|
|
3563
4215
|
const minimal_element_tree = new DOMElementNode(false, null, 'body', '', {}, []);
|
|
3564
4216
|
const tabs_info = await this.get_tabs_info();
|
|
@@ -3604,8 +4256,14 @@ export class BrowserSession {
|
|
|
3604
4256
|
await this.remove_highlights();
|
|
3605
4257
|
}
|
|
3606
4258
|
catch (error) {
|
|
4259
|
+
if (error instanceof URLNotAllowedError) {
|
|
4260
|
+
throw error;
|
|
4261
|
+
}
|
|
3607
4262
|
this.logger.debug('Timeout removing highlights');
|
|
3608
4263
|
}
|
|
4264
|
+
finally {
|
|
4265
|
+
await this.validate_page_after_action(page);
|
|
4266
|
+
}
|
|
3609
4267
|
// Check for PDF and auto-download if needed
|
|
3610
4268
|
try {
|
|
3611
4269
|
const pdf_path = await this._auto_download_pdf_if_needed(page);
|
|
@@ -3614,8 +4272,14 @@ export class BrowserSession {
|
|
|
3614
4272
|
}
|
|
3615
4273
|
}
|
|
3616
4274
|
catch (error) {
|
|
4275
|
+
if (error instanceof URLNotAllowedError) {
|
|
4276
|
+
throw error;
|
|
4277
|
+
}
|
|
3617
4278
|
this.logger.debug(`PDF auto-download check failed: ${error.message}`);
|
|
3618
4279
|
}
|
|
4280
|
+
finally {
|
|
4281
|
+
await this.validate_page_after_action(page);
|
|
4282
|
+
}
|
|
3619
4283
|
// DOM processing
|
|
3620
4284
|
this.logger.debug('🌳 Starting DOM processing...');
|
|
3621
4285
|
const dom_service = new DomService(page, this.logger);
|
|
@@ -3627,12 +4291,18 @@ export class BrowserSession {
|
|
|
3627
4291
|
this.logger.debug('✅ DOM processing completed');
|
|
3628
4292
|
}
|
|
3629
4293
|
catch (error) {
|
|
3630
|
-
|
|
4294
|
+
if (error instanceof URLNotAllowedError) {
|
|
4295
|
+
throw error;
|
|
4296
|
+
}
|
|
4297
|
+
this.logger.warning(`DOM processing timed out for ${logPageUrl}`);
|
|
3631
4298
|
this.logger.warning('🔄 Falling back to minimal DOM state...');
|
|
3632
4299
|
// Create minimal DOM state for fallback
|
|
3633
4300
|
const minimal_element_tree = new DOMElementNode(true, null, 'body', '/body', {}, []);
|
|
3634
4301
|
content = new DOMState(minimal_element_tree, {});
|
|
3635
4302
|
}
|
|
4303
|
+
finally {
|
|
4304
|
+
await this.validate_page_after_action(page);
|
|
4305
|
+
}
|
|
3636
4306
|
// Get tabs info
|
|
3637
4307
|
this.logger.debug('📋 Getting tabs info...');
|
|
3638
4308
|
const tabs_info = await this.get_tabs_info();
|
|
@@ -3645,7 +4315,13 @@ export class BrowserSession {
|
|
|
3645
4315
|
screenshot_b64 = await this.take_screenshot();
|
|
3646
4316
|
}
|
|
3647
4317
|
catch (error) {
|
|
3648
|
-
|
|
4318
|
+
if (error instanceof URLNotAllowedError) {
|
|
4319
|
+
throw error;
|
|
4320
|
+
}
|
|
4321
|
+
this.logger.warning(`❌ Screenshot failed for ${logPageUrl}: ${error.message}`);
|
|
4322
|
+
}
|
|
4323
|
+
finally {
|
|
4324
|
+
await this.validate_page_after_action(page);
|
|
3649
4325
|
}
|
|
3650
4326
|
}
|
|
3651
4327
|
// Get page info and scroll info
|
|
@@ -3673,26 +4349,42 @@ export class BrowserSession {
|
|
|
3673
4349
|
this.logger.debug('✅ Scroll info completed');
|
|
3674
4350
|
}
|
|
3675
4351
|
catch (error) {
|
|
4352
|
+
if (error instanceof URLNotAllowedError) {
|
|
4353
|
+
throw error;
|
|
4354
|
+
}
|
|
3676
4355
|
this.logger.warning(`Failed to get scroll info: ${error.message}`);
|
|
3677
4356
|
}
|
|
4357
|
+
finally {
|
|
4358
|
+
await this.validate_page_after_action(page);
|
|
4359
|
+
}
|
|
3678
4360
|
// Get title
|
|
3679
4361
|
let title = 'Title unavailable';
|
|
3680
4362
|
try {
|
|
4363
|
+
await this.validate_page_after_action(page);
|
|
3681
4364
|
const titlePromise = page.title();
|
|
3682
4365
|
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000));
|
|
3683
4366
|
title = await Promise.race([titlePromise, timeoutPromise]);
|
|
3684
4367
|
}
|
|
3685
4368
|
catch (error) {
|
|
4369
|
+
if (error instanceof URLNotAllowedError) {
|
|
4370
|
+
throw error;
|
|
4371
|
+
}
|
|
3686
4372
|
// Keep default title
|
|
3687
4373
|
}
|
|
4374
|
+
finally {
|
|
4375
|
+
await this.validate_page_after_action(page);
|
|
4376
|
+
}
|
|
3688
4377
|
// Check for errors
|
|
3689
4378
|
const browser_errors = [];
|
|
3690
4379
|
if (Object.keys(content.selector_map).length === 0) {
|
|
3691
|
-
browser_errors.push(`DOM processing timed out for ${
|
|
4380
|
+
browser_errors.push(`DOM processing timed out for ${logPageUrl} - using minimal state. Basic navigation still available.`);
|
|
3692
4381
|
}
|
|
3693
4382
|
// Check if PDF viewer
|
|
4383
|
+
await this.validate_page_after_action(page);
|
|
3694
4384
|
const is_pdf_viewer = await this._is_pdf_viewer(page);
|
|
4385
|
+
await this.validate_page_after_action(page);
|
|
3695
4386
|
const pendingNetworkRequests = await this._getPendingNetworkRequests(page);
|
|
4387
|
+
await this.validate_page_after_action(page);
|
|
3696
4388
|
const paginationButtons = DomService.detect_pagination_buttons(content.selector_map);
|
|
3697
4389
|
const browser_state = new BrowserStateSummary(content, {
|
|
3698
4390
|
url: page_url,
|
|
@@ -3713,6 +4405,7 @@ export class BrowserSession {
|
|
|
3713
4405
|
closed_popup_messages: this._getClosedPopupMessagesSnapshot(),
|
|
3714
4406
|
});
|
|
3715
4407
|
this.logger.debug('✅ get_state_summary completed successfully');
|
|
4408
|
+
await this.validate_page_after_action(page);
|
|
3716
4409
|
return browser_state;
|
|
3717
4410
|
}
|
|
3718
4411
|
/**
|
|
@@ -3722,6 +4415,7 @@ export class BrowserSession {
|
|
|
3722
4415
|
return (url === 'about:blank' ||
|
|
3723
4416
|
url === 'about:newtab' ||
|
|
3724
4417
|
url === 'chrome://newtab/' ||
|
|
4418
|
+
url === 'chrome://newtab' ||
|
|
3725
4419
|
url === 'chrome://new-tab-page/' ||
|
|
3726
4420
|
url === 'chrome://new-tab-page');
|
|
3727
4421
|
}
|
|
@@ -3747,10 +4441,16 @@ export class BrowserSession {
|
|
|
3747
4441
|
// so keep parity with pattern matching default: https-only.
|
|
3748
4442
|
return protocol.toLowerCase() === 'https:';
|
|
3749
4443
|
}
|
|
4444
|
+
_domainCollectionHasEntries(value) {
|
|
4445
|
+
return Array.isArray(value)
|
|
4446
|
+
? value.length > 0
|
|
4447
|
+
: value instanceof Set && value.size > 0;
|
|
4448
|
+
}
|
|
3750
4449
|
/**
|
|
3751
4450
|
* Check if page is displaying a PDF
|
|
3752
4451
|
*/
|
|
3753
4452
|
async _is_pdf_viewer(page) {
|
|
4453
|
+
await this.validate_page_after_action(page);
|
|
3754
4454
|
try {
|
|
3755
4455
|
const url = page.url();
|
|
3756
4456
|
if (url.endsWith('.pdf') || url.includes('.pdf?')) {
|
|
@@ -3764,8 +4464,14 @@ export class BrowserSession {
|
|
|
3764
4464
|
return is_pdf;
|
|
3765
4465
|
}
|
|
3766
4466
|
catch (error) {
|
|
4467
|
+
if (error instanceof URLNotAllowedError) {
|
|
4468
|
+
throw error;
|
|
4469
|
+
}
|
|
3767
4470
|
return false;
|
|
3768
4471
|
}
|
|
4472
|
+
finally {
|
|
4473
|
+
await this.validate_page_after_action(page);
|
|
4474
|
+
}
|
|
3769
4475
|
}
|
|
3770
4476
|
/**
|
|
3771
4477
|
* Auto-download PDF if detected and auto-download is enabled
|
|
@@ -3775,13 +4481,15 @@ export class BrowserSession {
|
|
|
3775
4481
|
if (!downloadsPath || !this._autoDownloadPdfs) {
|
|
3776
4482
|
return null;
|
|
3777
4483
|
}
|
|
4484
|
+
await this.validate_page_after_action(page);
|
|
3778
4485
|
try {
|
|
3779
4486
|
const is_pdf = await this._is_pdf_viewer(page);
|
|
3780
4487
|
if (!is_pdf) {
|
|
3781
4488
|
return null;
|
|
3782
4489
|
}
|
|
3783
4490
|
const url = page.url();
|
|
3784
|
-
|
|
4491
|
+
const logUrl = BrowserSession._redact_url_for_logging(url);
|
|
4492
|
+
this.logger.info(`📄 PDF detected: ${logUrl}`);
|
|
3785
4493
|
let pdfFilename = path.basename(url.split('?')[0]);
|
|
3786
4494
|
if (!pdfFilename || !pdfFilename.toLowerCase().endsWith('.pdf')) {
|
|
3787
4495
|
const parsed = new URL(url);
|
|
@@ -3794,50 +4502,58 @@ export class BrowserSession {
|
|
|
3794
4502
|
this.logger.debug(`📄 PDF already downloaded: ${pdfFilename}`);
|
|
3795
4503
|
return null;
|
|
3796
4504
|
}
|
|
3797
|
-
this.logger.info(`📄 Auto-downloading PDF from: ${
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
4505
|
+
this.logger.info(`📄 Auto-downloading PDF from: ${logUrl}`);
|
|
4506
|
+
await this.validate_page_after_action(page);
|
|
4507
|
+
let downloadResult = null;
|
|
4508
|
+
try {
|
|
4509
|
+
downloadResult = await page.evaluate(async (pdfUrl) => {
|
|
4510
|
+
try {
|
|
4511
|
+
const response = await fetch(pdfUrl, {
|
|
4512
|
+
cache: 'force-cache',
|
|
4513
|
+
});
|
|
4514
|
+
if (!response.ok) {
|
|
4515
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
4516
|
+
}
|
|
4517
|
+
const blob = await response.blob();
|
|
4518
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
4519
|
+
const uint8Array = new Uint8Array(arrayBuffer);
|
|
4520
|
+
const cacheHeader = response.headers.get('x-cache') || '';
|
|
4521
|
+
const fromCache = response.headers.has('age') ||
|
|
4522
|
+
cacheHeader.toLowerCase().includes('hit');
|
|
4523
|
+
return {
|
|
4524
|
+
data: Array.from(uint8Array),
|
|
4525
|
+
fromCache,
|
|
4526
|
+
responseSize: uint8Array.length,
|
|
4527
|
+
};
|
|
3805
4528
|
}
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
return {
|
|
3820
|
-
data: [],
|
|
3821
|
-
fromCache: false,
|
|
3822
|
-
responseSize: 0,
|
|
3823
|
-
error: error instanceof Error ? error.message : 'Unknown fetch error',
|
|
3824
|
-
};
|
|
3825
|
-
}
|
|
3826
|
-
}, url);
|
|
4529
|
+
catch (error) {
|
|
4530
|
+
return {
|
|
4531
|
+
data: [],
|
|
4532
|
+
fromCache: false,
|
|
4533
|
+
responseSize: 0,
|
|
4534
|
+
error: error instanceof Error ? error.message : 'Unknown fetch error',
|
|
4535
|
+
};
|
|
4536
|
+
}
|
|
4537
|
+
}, url);
|
|
4538
|
+
}
|
|
4539
|
+
finally {
|
|
4540
|
+
await this.validate_page_after_action(page);
|
|
4541
|
+
}
|
|
3827
4542
|
if (downloadResult?.error) {
|
|
3828
|
-
this.logger.warning(`⚠️ Failed to auto-download PDF from ${
|
|
4543
|
+
this.logger.warning(`⚠️ Failed to auto-download PDF from ${logUrl}: ${downloadResult.error}`);
|
|
3829
4544
|
return null;
|
|
3830
4545
|
}
|
|
3831
4546
|
if (!downloadResult ||
|
|
3832
4547
|
!Array.isArray(downloadResult.data) ||
|
|
3833
4548
|
downloadResult.data.length === 0) {
|
|
3834
|
-
this.logger.warning(`⚠️ No data received when downloading PDF from ${
|
|
4549
|
+
this.logger.warning(`⚠️ No data received when downloading PDF from ${logUrl}`);
|
|
3835
4550
|
return null;
|
|
3836
4551
|
}
|
|
3837
|
-
|
|
4552
|
+
ensurePrivateDirectoryIfCreated(downloadsPath);
|
|
3838
4553
|
const uniqueFilename = await BrowserSession.get_unique_filename(downloadsPath, pdfFilename);
|
|
3839
4554
|
const downloadPath = path.join(downloadsPath, uniqueFilename);
|
|
3840
|
-
await fs.promises.writeFile(downloadPath, Buffer.from(downloadResult.data));
|
|
4555
|
+
await fs.promises.writeFile(downloadPath, Buffer.from(downloadResult.data), { mode: 0o600 });
|
|
4556
|
+
chmodPrivateFileBestEffort(downloadPath);
|
|
3841
4557
|
this.add_downloaded_file(downloadPath);
|
|
3842
4558
|
const cacheStatus = downloadResult.fromCache
|
|
3843
4559
|
? 'from cache'
|
|
@@ -3847,6 +4563,9 @@ export class BrowserSession {
|
|
|
3847
4563
|
return downloadPath;
|
|
3848
4564
|
}
|
|
3849
4565
|
catch (error) {
|
|
4566
|
+
if (error instanceof URLNotAllowedError) {
|
|
4567
|
+
throw error;
|
|
4568
|
+
}
|
|
3850
4569
|
this.logger.debug(`PDF detection failed: ${error.message}`);
|
|
3851
4570
|
return null;
|
|
3852
4571
|
}
|
|
@@ -3872,6 +4591,7 @@ export class BrowserSession {
|
|
|
3872
4591
|
if (!page) {
|
|
3873
4592
|
return null;
|
|
3874
4593
|
}
|
|
4594
|
+
await this.validate_page_after_action(page);
|
|
3875
4595
|
try {
|
|
3876
4596
|
// Use XPath to locate the element
|
|
3877
4597
|
const element_handle = await page
|
|
@@ -3899,6 +4619,7 @@ export class BrowserSession {
|
|
|
3899
4619
|
if (!page) {
|
|
3900
4620
|
return null;
|
|
3901
4621
|
}
|
|
4622
|
+
await this.validate_page_after_action(page);
|
|
3902
4623
|
try {
|
|
3903
4624
|
// Use CSS selector to locate the element
|
|
3904
4625
|
const element_handle = await page.locator(css_selector).elementHandle();
|
|
@@ -3927,6 +4648,7 @@ export class BrowserSession {
|
|
|
3927
4648
|
if (!page) {
|
|
3928
4649
|
return null;
|
|
3929
4650
|
}
|
|
4651
|
+
await this.validate_page_after_action(page);
|
|
3930
4652
|
try {
|
|
3931
4653
|
// Build selector: filter by element type and text
|
|
3932
4654
|
const selector = element_type
|
|
@@ -4023,8 +4745,19 @@ export class BrowserSession {
|
|
|
4023
4745
|
catch {
|
|
4024
4746
|
return 'invalid_url';
|
|
4025
4747
|
}
|
|
4026
|
-
|
|
4027
|
-
|
|
4748
|
+
const allowedDomains = this.browser_profile.allowed_domains;
|
|
4749
|
+
const hasAllowedDomains = this._domainCollectionHasEntries(allowedDomains);
|
|
4750
|
+
const prohibitedDomains = this.browser_profile.prohibited_domains;
|
|
4751
|
+
const hasProhibitedDomains = this._domainCollectionHasEntries(prohibitedDomains);
|
|
4752
|
+
const hasDomainRestrictions = hasAllowedDomains || hasProhibitedDomains;
|
|
4753
|
+
if (parsed.protocol === 'data:') {
|
|
4754
|
+
return hasDomainRestrictions ? 'opaque_origin_blocked' : null;
|
|
4755
|
+
}
|
|
4756
|
+
if (parsed.protocol === 'blob:') {
|
|
4757
|
+
if (parsed.origin && parsed.origin !== 'null') {
|
|
4758
|
+
return this._get_url_access_denial_reason(parsed.origin);
|
|
4759
|
+
}
|
|
4760
|
+
return hasDomainRestrictions ? 'opaque_origin_blocked' : null;
|
|
4028
4761
|
}
|
|
4029
4762
|
if (!parsed.hostname) {
|
|
4030
4763
|
return 'missing_host';
|
|
@@ -4034,50 +4767,44 @@ export class BrowserSession {
|
|
|
4034
4767
|
this._is_ip_address_host(parsed.hostname)) {
|
|
4035
4768
|
return 'ip_address_blocked';
|
|
4036
4769
|
}
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
if (allowedDomains instanceof Set) {
|
|
4042
|
-
if (this._setEntryMatchesUrl(allowedDomains, hostVariant, hostAlt, parsed.protocol)) {
|
|
4043
|
-
return null;
|
|
4770
|
+
if (prohibitedDomains && hasProhibitedDomains) {
|
|
4771
|
+
if (prohibitedDomains instanceof Set) {
|
|
4772
|
+
if (this._setEntryMatchesUrl(prohibitedDomains, hostVariant, hostAlt, parsed.protocol)) {
|
|
4773
|
+
return 'in_prohibited_domains';
|
|
4044
4774
|
}
|
|
4045
4775
|
}
|
|
4046
4776
|
else {
|
|
4047
|
-
for (const
|
|
4777
|
+
for (const prohibitedDomain of prohibitedDomains) {
|
|
4048
4778
|
try {
|
|
4049
|
-
if (match_url_with_domain_pattern(url,
|
|
4050
|
-
return
|
|
4779
|
+
if (match_url_with_domain_pattern(url, prohibitedDomain, true)) {
|
|
4780
|
+
return 'in_prohibited_domains';
|
|
4051
4781
|
}
|
|
4052
4782
|
}
|
|
4053
4783
|
catch {
|
|
4054
|
-
this.logger.warning(`Invalid domain pattern: ${
|
|
4784
|
+
this.logger.warning(`Invalid domain pattern: ${prohibitedDomain}`);
|
|
4055
4785
|
}
|
|
4056
4786
|
}
|
|
4057
4787
|
}
|
|
4058
|
-
return 'not_in_allowed_domains';
|
|
4059
4788
|
}
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
if (prohibitedDomains instanceof Set) {
|
|
4065
|
-
if (this._setEntryMatchesUrl(prohibitedDomains, hostVariant, hostAlt, parsed.protocol)) {
|
|
4066
|
-
return 'in_prohibited_domains';
|
|
4789
|
+
if (allowedDomains && hasAllowedDomains) {
|
|
4790
|
+
if (allowedDomains instanceof Set) {
|
|
4791
|
+
if (this._setEntryMatchesUrl(allowedDomains, hostVariant, hostAlt, parsed.protocol)) {
|
|
4792
|
+
return null;
|
|
4067
4793
|
}
|
|
4068
4794
|
}
|
|
4069
4795
|
else {
|
|
4070
|
-
for (const
|
|
4796
|
+
for (const allowedDomain of allowedDomains) {
|
|
4071
4797
|
try {
|
|
4072
|
-
if (match_url_with_domain_pattern(url,
|
|
4073
|
-
return
|
|
4798
|
+
if (match_url_with_domain_pattern(url, allowedDomain, true)) {
|
|
4799
|
+
return null;
|
|
4074
4800
|
}
|
|
4075
4801
|
}
|
|
4076
4802
|
catch {
|
|
4077
|
-
this.logger.warning(`Invalid domain pattern: ${
|
|
4803
|
+
this.logger.warning(`Invalid domain pattern: ${allowedDomain}`);
|
|
4078
4804
|
}
|
|
4079
4805
|
}
|
|
4080
4806
|
}
|
|
4807
|
+
return 'not_in_allowed_domains';
|
|
4081
4808
|
}
|
|
4082
4809
|
return null;
|
|
4083
4810
|
}
|
|
@@ -4090,25 +4817,150 @@ export class BrowserSession {
|
|
|
4090
4817
|
}
|
|
4091
4818
|
return JSON.stringify(value ?? null);
|
|
4092
4819
|
}
|
|
4820
|
+
_has_url_access_restrictions() {
|
|
4821
|
+
return (this._domainCollectionHasEntries(this.browser_profile.allowed_domains) ||
|
|
4822
|
+
this._domainCollectionHasEntries(this.browser_profile.prohibited_domains) ||
|
|
4823
|
+
Boolean(this.browser_profile.block_ip_addresses));
|
|
4824
|
+
}
|
|
4825
|
+
_sanitize_tab_for_exposure(tab) {
|
|
4826
|
+
if (!this._has_url_access_restrictions()) {
|
|
4827
|
+
return { ...tab };
|
|
4828
|
+
}
|
|
4829
|
+
const denialReason = this._get_url_access_denial_reason(tab.url);
|
|
4830
|
+
if (!denialReason) {
|
|
4831
|
+
return { ...tab };
|
|
4832
|
+
}
|
|
4833
|
+
return {
|
|
4834
|
+
...tab,
|
|
4835
|
+
url: 'about:blank',
|
|
4836
|
+
title: 'blocked by domain policy',
|
|
4837
|
+
};
|
|
4838
|
+
}
|
|
4093
4839
|
_assert_url_allowed(url) {
|
|
4094
4840
|
const denialReason = this._get_url_access_denial_reason(url);
|
|
4095
4841
|
if (!denialReason) {
|
|
4096
4842
|
return;
|
|
4097
4843
|
}
|
|
4844
|
+
const safeUrl = BrowserSession._redact_url_for_logging(url);
|
|
4098
4845
|
this._recordRecentEvent('navigation_blocked', {
|
|
4099
|
-
url,
|
|
4846
|
+
url: safeUrl,
|
|
4100
4847
|
error_message: denialReason,
|
|
4101
4848
|
});
|
|
4102
4849
|
if (denialReason === 'not_in_allowed_domains') {
|
|
4103
|
-
throw new URLNotAllowedError(`URL ${
|
|
4850
|
+
throw new URLNotAllowedError(`URL ${safeUrl} is not in allowed_domains. Current allowed_domains: ${this._formatDomainCollection(this.browser_profile.allowed_domains)}`);
|
|
4104
4851
|
}
|
|
4105
4852
|
if (denialReason === 'in_prohibited_domains') {
|
|
4106
|
-
throw new URLNotAllowedError(`URL ${
|
|
4853
|
+
throw new URLNotAllowedError(`URL ${safeUrl} is blocked by prohibited_domains. Current prohibited_domains: ${this._formatDomainCollection(this.browser_profile.prohibited_domains)}`);
|
|
4107
4854
|
}
|
|
4108
4855
|
if (denialReason === 'ip_address_blocked') {
|
|
4109
|
-
throw new URLNotAllowedError(`URL ${
|
|
4856
|
+
throw new URLNotAllowedError(`URL ${safeUrl} is blocked because block_ip_addresses=true`);
|
|
4857
|
+
}
|
|
4858
|
+
if (denialReason === 'opaque_origin_blocked') {
|
|
4859
|
+
throw new URLNotAllowedError(`URL ${safeUrl} is blocked because its origin cannot be validated against domain restrictions`);
|
|
4860
|
+
}
|
|
4861
|
+
throw new URLNotAllowedError(`URL ${safeUrl} is not allowed (${denialReason})`);
|
|
4862
|
+
}
|
|
4863
|
+
async _rollback_disallowed_navigation(page, blockedUrl, options = {}) {
|
|
4864
|
+
this.logger.warning(`Blocked navigation reached disallowed URL ${BrowserSession._redact_url_for_logging(blockedUrl)}; resetting current tab to about:blank`);
|
|
4865
|
+
try {
|
|
4866
|
+
await page.goto('about:blank', { waitUntil: 'load', timeout: 5000 });
|
|
4867
|
+
}
|
|
4868
|
+
catch (rollbackError) {
|
|
4869
|
+
this.logger.debug(`Failed to reset disallowed navigation to about:blank: ${rollbackError.message}`);
|
|
4870
|
+
}
|
|
4871
|
+
const currentUrl = typeof page.url === 'function' ? page.url() : 'about:blank';
|
|
4872
|
+
if (this._is_new_tab_page(currentUrl)) {
|
|
4873
|
+
this.currentUrl = 'about:blank';
|
|
4874
|
+
this.currentTitle = 'about:blank';
|
|
4875
|
+
const tab = this._tabs[this.currentTabIndex];
|
|
4876
|
+
if (tab) {
|
|
4877
|
+
tab.url = 'about:blank';
|
|
4878
|
+
tab.title = 'about:blank';
|
|
4879
|
+
}
|
|
4880
|
+
this._syncSessionManagerFromTabs();
|
|
4881
|
+
this.cachedBrowserState = null;
|
|
4882
|
+
return;
|
|
4883
|
+
}
|
|
4884
|
+
if (options.replace_on_failure ?? true) {
|
|
4885
|
+
await this._replace_disallowed_current_page(page, blockedUrl);
|
|
4886
|
+
}
|
|
4887
|
+
}
|
|
4888
|
+
async _replace_disallowed_current_page(page, blockedUrl) {
|
|
4889
|
+
const safeUrl = BrowserSession._redact_url_for_logging(blockedUrl);
|
|
4890
|
+
let replacementPage = null;
|
|
4891
|
+
const canCreateReplacement = typeof this.browser_context?.newPage === 'function';
|
|
4892
|
+
if (canCreateReplacement) {
|
|
4893
|
+
try {
|
|
4894
|
+
replacementPage = await this.browser_context.newPage();
|
|
4895
|
+
try {
|
|
4896
|
+
await replacementPage.goto('about:blank', {
|
|
4897
|
+
waitUntil: 'load',
|
|
4898
|
+
timeout: 5000,
|
|
4899
|
+
});
|
|
4900
|
+
}
|
|
4901
|
+
catch (error) {
|
|
4902
|
+
this.logger.debug(`Failed to initialize replacement blank tab after blocked navigation to ${safeUrl}: ${error.message}`);
|
|
4903
|
+
}
|
|
4904
|
+
const replacementUrl = typeof replacementPage.url === 'function'
|
|
4905
|
+
? replacementPage.url()
|
|
4906
|
+
: 'about:blank';
|
|
4907
|
+
if (!this._is_new_tab_page(replacementUrl)) {
|
|
4908
|
+
await replacementPage.close?.();
|
|
4909
|
+
replacementPage = null;
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
catch (error) {
|
|
4913
|
+
this.logger.debug(`Failed to create replacement blank tab after blocked navigation to ${safeUrl}: ${error.message}`);
|
|
4914
|
+
replacementPage = null;
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4917
|
+
try {
|
|
4918
|
+
await page.close?.();
|
|
4919
|
+
}
|
|
4920
|
+
catch (error) {
|
|
4921
|
+
this.logger.debug(`Failed to close blocked page after rollback failure: ${error.message}`);
|
|
4922
|
+
}
|
|
4923
|
+
this.currentUrl = 'about:blank';
|
|
4924
|
+
this.currentTitle = 'about:blank';
|
|
4925
|
+
const tab = this._tabs[this.currentTabIndex];
|
|
4926
|
+
if (tab) {
|
|
4927
|
+
tab.url = 'about:blank';
|
|
4928
|
+
tab.title = 'about:blank';
|
|
4929
|
+
}
|
|
4930
|
+
if (this.human_current_page === page) {
|
|
4931
|
+
this.human_current_page = replacementPage;
|
|
4932
|
+
}
|
|
4933
|
+
this._setActivePage(replacementPage);
|
|
4934
|
+
this._syncSessionManagerFromTabs();
|
|
4935
|
+
this.cachedBrowserState = null;
|
|
4936
|
+
}
|
|
4937
|
+
async _assert_page_url_allowed_or_rollback(page, options = {}) {
|
|
4938
|
+
const currentUrl = typeof page.url === 'function' ? page.url() : 'about:blank';
|
|
4939
|
+
try {
|
|
4940
|
+
this._assert_url_allowed(currentUrl);
|
|
4941
|
+
}
|
|
4942
|
+
catch (error) {
|
|
4943
|
+
if (error instanceof URLNotAllowedError) {
|
|
4944
|
+
await this._rollback_disallowed_navigation(page, currentUrl, options);
|
|
4945
|
+
}
|
|
4946
|
+
throw error;
|
|
4947
|
+
}
|
|
4948
|
+
}
|
|
4949
|
+
async _get_disallowed_page_error_after_navigation_error(page, options = {}) {
|
|
4950
|
+
if (!page) {
|
|
4951
|
+
return null;
|
|
4952
|
+
}
|
|
4953
|
+
try {
|
|
4954
|
+
await this._assert_page_url_allowed_or_rollback(page, options);
|
|
4955
|
+
return null;
|
|
4956
|
+
}
|
|
4957
|
+
catch (error) {
|
|
4958
|
+
if (error instanceof URLNotAllowedError) {
|
|
4959
|
+
return error;
|
|
4960
|
+
}
|
|
4961
|
+
this.logger.debug(`Failed to inspect page URL after navigation error: ${error.message}`);
|
|
4962
|
+
return null;
|
|
4110
4963
|
}
|
|
4111
|
-
throw new URLNotAllowedError(`URL ${url} is not allowed (${denialReason})`);
|
|
4112
4964
|
}
|
|
4113
4965
|
/**
|
|
4114
4966
|
* Navigate helper with URL validation
|
|
@@ -4182,88 +5034,98 @@ export class BrowserSession {
|
|
|
4182
5034
|
if (!element_handle) {
|
|
4183
5035
|
throw new Error(`Element not found: ${JSON.stringify(element_node)}`);
|
|
4184
5036
|
}
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
timeout: 5000,
|
|
4192
|
-
});
|
|
4193
|
-
// Click failures should bubble to the caller.
|
|
4194
|
-
try {
|
|
4195
|
-
await element_handle.click();
|
|
4196
|
-
}
|
|
4197
|
-
catch (error) {
|
|
4198
|
-
void download_promise.catch(() => undefined);
|
|
4199
|
-
throw error;
|
|
4200
|
-
}
|
|
4201
|
-
let download;
|
|
4202
|
-
try {
|
|
4203
|
-
download = await download_promise;
|
|
5037
|
+
const validateClickNavigation = async () => {
|
|
5038
|
+
await this._waitForLoad(page, 5000);
|
|
5039
|
+
await this._assert_page_url_allowed_or_rollback(page);
|
|
5040
|
+
await this._syncCurrentTabFromPage(page);
|
|
5041
|
+
if (this.historyStack[this.historyStack.length - 1] !== this.currentUrl) {
|
|
5042
|
+
this.historyStack.push(this.currentUrl);
|
|
4204
5043
|
}
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
5044
|
+
this.cachedBrowserState = null;
|
|
5045
|
+
};
|
|
5046
|
+
try {
|
|
5047
|
+
// Check if downloads are enabled
|
|
5048
|
+
const downloads_path = this.browser_profile.downloads_path;
|
|
5049
|
+
if (downloads_path) {
|
|
5050
|
+
ensurePrivateDirectoryIfCreated(downloads_path);
|
|
5051
|
+
// Try to detect file download.
|
|
5052
|
+
const download_promise = page.waitForEvent('download', {
|
|
5053
|
+
timeout: 5000,
|
|
5054
|
+
});
|
|
5055
|
+
// Click failures should bubble to the caller.
|
|
5056
|
+
try {
|
|
5057
|
+
await element_handle.click();
|
|
5058
|
+
}
|
|
5059
|
+
catch (error) {
|
|
5060
|
+
void download_promise.catch(() => undefined);
|
|
4211
5061
|
throw error;
|
|
4212
5062
|
}
|
|
4213
|
-
|
|
5063
|
+
let download;
|
|
4214
5064
|
try {
|
|
4215
|
-
await
|
|
5065
|
+
download = await download_promise;
|
|
4216
5066
|
}
|
|
4217
|
-
catch (
|
|
4218
|
-
|
|
5067
|
+
catch (error) {
|
|
5068
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5069
|
+
const isDownloadTimeout = error instanceof Error &&
|
|
5070
|
+
(error.name === 'TimeoutError' ||
|
|
5071
|
+
message.toLowerCase().includes('timeout'));
|
|
5072
|
+
if (!isDownloadTimeout) {
|
|
5073
|
+
throw error;
|
|
5074
|
+
}
|
|
5075
|
+
this.logger.debug('No download triggered within timeout. Checking navigation...');
|
|
5076
|
+
return null;
|
|
4219
5077
|
}
|
|
4220
|
-
|
|
5078
|
+
// Save the downloaded file.
|
|
5079
|
+
const suggested_filename = download.suggestedFilename();
|
|
5080
|
+
const unique_filename = await BrowserSession.get_unique_filename(downloads_path, suggested_filename);
|
|
5081
|
+
const download_path = path.join(downloads_path, unique_filename);
|
|
5082
|
+
const download_guid = uuid7str();
|
|
5083
|
+
const download_url = typeof download.url === 'function'
|
|
5084
|
+
? download.url()
|
|
5085
|
+
: (this.currentUrl ?? '');
|
|
5086
|
+
await this._assert_download_url_allowed(download, download_url);
|
|
5087
|
+
await this.event_bus.dispatch(new DownloadStartedEvent({
|
|
5088
|
+
guid: download_guid,
|
|
5089
|
+
url: download_url,
|
|
5090
|
+
suggested_filename,
|
|
5091
|
+
auto_download: false,
|
|
5092
|
+
}));
|
|
5093
|
+
await download.saveAs(download_path);
|
|
5094
|
+
chmodPrivateFileBestEffort(download_path);
|
|
5095
|
+
this.logger.info(`⬇️ Downloaded file to: ${download_path}`);
|
|
5096
|
+
const stats = fs.existsSync(download_path)
|
|
5097
|
+
? fs.statSync(download_path)
|
|
5098
|
+
: null;
|
|
5099
|
+
await this.event_bus.dispatch(new DownloadProgressEvent({
|
|
5100
|
+
guid: download_guid,
|
|
5101
|
+
received_bytes: stats?.size ?? 0,
|
|
5102
|
+
total_bytes: stats?.size ?? 0,
|
|
5103
|
+
state: 'completed',
|
|
5104
|
+
}));
|
|
5105
|
+
const fileDownloadedResult = await this.event_bus.dispatch(new FileDownloadedEvent({
|
|
5106
|
+
guid: download_guid,
|
|
5107
|
+
url: download_url,
|
|
5108
|
+
path: download_path,
|
|
5109
|
+
file_name: unique_filename,
|
|
5110
|
+
file_size: stats?.size ?? 0,
|
|
5111
|
+
file_type: path.extname(unique_filename).replace('.', '') || null,
|
|
5112
|
+
mime_type: null,
|
|
5113
|
+
auto_download: false,
|
|
5114
|
+
}));
|
|
5115
|
+
if (fileDownloadedResult.handler_results.length === 0) {
|
|
5116
|
+
this.add_downloaded_file(download_path);
|
|
5117
|
+
}
|
|
5118
|
+
return download_path;
|
|
4221
5119
|
}
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
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);
|
|
5120
|
+
else {
|
|
5121
|
+
// No downloads path configured, just click
|
|
5122
|
+
await element_handle.click();
|
|
4259
5123
|
}
|
|
4260
|
-
return
|
|
5124
|
+
return null;
|
|
4261
5125
|
}
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
await element_handle.click();
|
|
5126
|
+
finally {
|
|
5127
|
+
await validateClickNavigation();
|
|
4265
5128
|
}
|
|
4266
|
-
return null;
|
|
4267
5129
|
}
|
|
4268
5130
|
/**
|
|
4269
5131
|
* Remove all highlights from the current page
|
|
@@ -4309,6 +5171,9 @@ export class BrowserSession {
|
|
|
4309
5171
|
catch (error) {
|
|
4310
5172
|
this.logger.debug(`Failed to remove highlights: ${error.message}`);
|
|
4311
5173
|
}
|
|
5174
|
+
finally {
|
|
5175
|
+
await this.validate_page_after_action(page);
|
|
5176
|
+
}
|
|
4312
5177
|
}
|
|
4313
5178
|
// region - Trace Recording
|
|
4314
5179
|
/**
|
|
@@ -4329,6 +5194,11 @@ export class BrowserSession {
|
|
|
4329
5194
|
* Note: Currently optional as it may cause performance issues in some cases
|
|
4330
5195
|
*/
|
|
4331
5196
|
async _startContextTracing() {
|
|
5197
|
+
if (this.browser_profile.traces_dir &&
|
|
5198
|
+
this._has_url_access_restrictions()) {
|
|
5199
|
+
this.logger.warning('Skipping trace recording because domain restrictions are active and trace artifacts cannot be URL-filtered.');
|
|
5200
|
+
return;
|
|
5201
|
+
}
|
|
4332
5202
|
if (this.browser_profile.traces_dir && this.browser_context) {
|
|
4333
5203
|
try {
|
|
4334
5204
|
this.logger.debug(`📽️ Starting tracing (will save to: ${this.browser_profile.traces_dir})`);
|
|
@@ -4348,6 +5218,10 @@ export class BrowserSession {
|
|
|
4348
5218
|
* Save browser trace recording
|
|
4349
5219
|
*/
|
|
4350
5220
|
async _saveTraceRecording() {
|
|
5221
|
+
if (this.browser_profile.traces_dir &&
|
|
5222
|
+
this._has_url_access_restrictions()) {
|
|
5223
|
+
return;
|
|
5224
|
+
}
|
|
4351
5225
|
if (this.browser_profile.traces_dir && this.browser_context) {
|
|
4352
5226
|
try {
|
|
4353
5227
|
const tracesPath = this.browser_profile.traces_dir;
|
|
@@ -4362,8 +5236,10 @@ export class BrowserSession {
|
|
|
4362
5236
|
const traceFilename = `BrowserSession_${this.id}.zip`;
|
|
4363
5237
|
finalTracePath = path.join(tracesPath, traceFilename);
|
|
4364
5238
|
}
|
|
5239
|
+
ensurePrivateDirectoryIfCreated(path.dirname(finalTracePath));
|
|
4365
5240
|
this.logger.info(`🎥 Saving browser_context trace to ${finalTracePath}...`);
|
|
4366
5241
|
await this.browser_context.tracing.stop({ path: finalTracePath });
|
|
5242
|
+
chmodPrivateFileBestEffort(finalTracePath);
|
|
4367
5243
|
}
|
|
4368
5244
|
catch (error) {
|
|
4369
5245
|
this.logger.warning(`Failed to save trace recording: ${error.message}`);
|
|
@@ -4608,6 +5484,7 @@ export class BrowserSession {
|
|
|
4608
5484
|
* @returns true if successful, false otherwise
|
|
4609
5485
|
*/
|
|
4610
5486
|
async _forceClosePageViaCdp(pageUrl) {
|
|
5487
|
+
const logPageUrl = BrowserSession._redact_url_for_logging(pageUrl);
|
|
4611
5488
|
try {
|
|
4612
5489
|
if (!this.browser_context) {
|
|
4613
5490
|
throw new Error('Browser context is not set up yet');
|
|
@@ -4644,7 +5521,7 @@ export class BrowserSession {
|
|
|
4644
5521
|
}
|
|
4645
5522
|
if (blockedTargetId) {
|
|
4646
5523
|
// Force close the target
|
|
4647
|
-
this.logger.warning(`🪓 Force-closing crashed page target_id=${blockedTargetId} via CDP: ${
|
|
5524
|
+
this.logger.warning(`🪓 Force-closing crashed page target_id=${blockedTargetId} via CDP: ${logPageUrl}`);
|
|
4648
5525
|
await Promise.race([
|
|
4649
5526
|
cdpSession.send('Target.closeTarget', {
|
|
4650
5527
|
targetId: blockedTargetId,
|
|
@@ -4654,7 +5531,7 @@ export class BrowserSession {
|
|
|
4654
5531
|
return true;
|
|
4655
5532
|
}
|
|
4656
5533
|
else {
|
|
4657
|
-
this.logger.debug(`❌ Could not find CDP page target_id to force-close: ${
|
|
5534
|
+
this.logger.debug(`❌ Could not find CDP page target_id to force-close: ${logPageUrl} (concurrency issues?)`);
|
|
4658
5535
|
return false;
|
|
4659
5536
|
}
|
|
4660
5537
|
}
|
|
@@ -4693,8 +5570,14 @@ export class BrowserSession {
|
|
|
4693
5570
|
return false;
|
|
4694
5571
|
}
|
|
4695
5572
|
const timeout = timeoutMs || this.browser_profile.default_navigation_timeout || 6000;
|
|
5573
|
+
const logUrl = BrowserSession._redact_url_for_logging(url);
|
|
5574
|
+
const denialReason = this._get_url_access_denial_reason(url);
|
|
5575
|
+
if (denialReason) {
|
|
5576
|
+
this.logger.warning(`Skipping recovery reopen for disallowed URL: ${denialReason}`);
|
|
5577
|
+
return false;
|
|
5578
|
+
}
|
|
4696
5579
|
try {
|
|
4697
|
-
this.logger.debug(`🔄 Attempting to reload URL that crashed: ${
|
|
5580
|
+
this.logger.debug(`🔄 Attempting to reload URL that crashed: ${logUrl}`);
|
|
4698
5581
|
if (!this.browser_context) {
|
|
4699
5582
|
throw new Error('Browser context is not set');
|
|
4700
5583
|
}
|
|
@@ -4717,31 +5600,55 @@ export class BrowserSession {
|
|
|
4717
5600
|
]);
|
|
4718
5601
|
}
|
|
4719
5602
|
catch (error) {
|
|
4720
|
-
this.logger.debug(`⚠️ Attempting to reload previously crashed URL ${
|
|
5603
|
+
this.logger.debug(`⚠️ Attempting to reload previously crashed URL ${logUrl} failed again: ${error.name}`);
|
|
5604
|
+
}
|
|
5605
|
+
try {
|
|
5606
|
+
await this._assert_page_url_allowed_or_rollback(newPage, {
|
|
5607
|
+
replace_on_failure: false,
|
|
5608
|
+
});
|
|
5609
|
+
}
|
|
5610
|
+
catch (error) {
|
|
5611
|
+
if (error instanceof URLNotAllowedError) {
|
|
5612
|
+
this.logger.warning(`Skipping recovery reopen after redirect to disallowed URL: ${error.message}`);
|
|
5613
|
+
try {
|
|
5614
|
+
await newPage.close?.();
|
|
5615
|
+
}
|
|
5616
|
+
catch {
|
|
5617
|
+
// Ignore cleanup errors; the page was already reset best effort.
|
|
5618
|
+
}
|
|
5619
|
+
if (this.agent_current_page === newPage) {
|
|
5620
|
+
this.agent_current_page = null;
|
|
5621
|
+
}
|
|
5622
|
+
if (this.human_current_page === newPage) {
|
|
5623
|
+
this.human_current_page = null;
|
|
5624
|
+
}
|
|
5625
|
+
return false;
|
|
5626
|
+
}
|
|
5627
|
+
throw error;
|
|
4721
5628
|
}
|
|
4722
5629
|
// Wait a bit for any transient blocking to resolve
|
|
4723
5630
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
4724
5631
|
// Check if the reopened page is responsive
|
|
4725
5632
|
const isResponsive = await this._isPageResponsive(newPage, 2.0);
|
|
4726
5633
|
if (isResponsive) {
|
|
4727
|
-
this.logger.info(`✅ Page recovered and is now responsive after reopening on: ${
|
|
5634
|
+
this.logger.info(`✅ Page recovered and is now responsive after reopening on: ${logUrl}`);
|
|
4728
5635
|
return true;
|
|
4729
5636
|
}
|
|
4730
5637
|
else {
|
|
4731
|
-
this.logger.warning(`⚠️ Reopened page ${
|
|
5638
|
+
this.logger.warning(`⚠️ Reopened page ${logUrl} is still unresponsive`);
|
|
4732
5639
|
// Close the unresponsive page before returning
|
|
4733
5640
|
try {
|
|
4734
5641
|
await this._forceClosePageViaCdp(newPage.url());
|
|
4735
5642
|
}
|
|
4736
5643
|
catch (error) {
|
|
4737
|
-
this.logger.error(`❌ Failed to close crashed page ${
|
|
5644
|
+
this.logger.error(`❌ Failed to close crashed page ${logUrl} via CDP: ${error.message} (something is very wrong or system is extremely overloaded)`);
|
|
4738
5645
|
}
|
|
4739
5646
|
this.agent_current_page = null; // Clear reference to closed page
|
|
4740
5647
|
return false;
|
|
4741
5648
|
}
|
|
4742
5649
|
}
|
|
4743
5650
|
catch (error) {
|
|
4744
|
-
this.logger.error(`❌ Retrying crashed page ${
|
|
5651
|
+
this.logger.error(`❌ Retrying crashed page ${logUrl} failed: ${error.message}`);
|
|
4745
5652
|
return false;
|
|
4746
5653
|
}
|
|
4747
5654
|
}
|
|
@@ -4750,7 +5657,7 @@ export class BrowserSession {
|
|
|
4750
5657
|
* @param url - The original URL that failed
|
|
4751
5658
|
*/
|
|
4752
5659
|
async _createBlankFallbackPage(url) {
|
|
4753
|
-
this.logger.warning(`⚠️ Resetting to about:blank as fallback because browser is unable to load the original URL without crashing: ${
|
|
5660
|
+
this.logger.warning(`⚠️ Resetting to about:blank as fallback because browser is unable to load the original URL without crashing: ${BrowserSession._redact_url_for_logging(url)}`);
|
|
4754
5661
|
// Close any existing broken page
|
|
4755
5662
|
if (this.agent_current_page && !this.agent_current_page.isClosed()) {
|
|
4756
5663
|
try {
|
|
@@ -4832,7 +5739,7 @@ export class BrowserSession {
|
|
|
4832
5739
|
!pageUrl.startsWith('edge://')) {
|
|
4833
5740
|
try {
|
|
4834
5741
|
await page.close();
|
|
4835
|
-
this.logger.debug(`🪓 Closed page because it has a known crash-causing URL: ${
|
|
5742
|
+
this.logger.debug(`🪓 Closed page because it has a known crash-causing URL: ${BrowserSession._redact_url_for_logging(pageUrl)}`);
|
|
4836
5743
|
}
|
|
4837
5744
|
catch {
|
|
4838
5745
|
// Page might already be closed via CDP
|
|
@@ -4974,7 +5881,7 @@ export class BrowserSession {
|
|
|
4974
5881
|
}
|
|
4975
5882
|
// Ensure directory exists
|
|
4976
5883
|
if (!fs.existsSync(userDataDir)) {
|
|
4977
|
-
|
|
5884
|
+
ensurePrivateDirectoryIfCreated(userDataDir);
|
|
4978
5885
|
this.logger.debug(`Created user data directory: ${userDataDir}`);
|
|
4979
5886
|
}
|
|
4980
5887
|
return userDataDir;
|
|
@@ -5038,9 +5945,10 @@ export class BrowserSession {
|
|
|
5038
5945
|
* Create a temporary user data directory
|
|
5039
5946
|
*/
|
|
5040
5947
|
async _createTempUserDataDir() {
|
|
5041
|
-
const
|
|
5042
|
-
|
|
5043
|
-
|
|
5948
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'browser-use-user-data-dir-'));
|
|
5949
|
+
if (process.platform !== 'win32') {
|
|
5950
|
+
fs.chmodSync(tempDir, 0o700);
|
|
5951
|
+
}
|
|
5044
5952
|
return tempDir;
|
|
5045
5953
|
}
|
|
5046
5954
|
// endregion
|
|
@@ -5055,7 +5963,7 @@ export class BrowserSession {
|
|
|
5055
5963
|
}
|
|
5056
5964
|
// Listen for page events to track which page the user is viewing
|
|
5057
5965
|
this.browser_context.on?.('page', (page) => {
|
|
5058
|
-
this.logger.debug(`New page created: ${page.url?.() || 'about:blank'}`);
|
|
5966
|
+
this.logger.debug(`New page created: ${BrowserSession._redact_url_for_logging(page.url?.() || 'about:blank')}`);
|
|
5059
5967
|
// Note: 'visibilitychange' is not a standard Playwright page event
|
|
5060
5968
|
// Visibility tracking would need to be implemented differently
|
|
5061
5969
|
// (e.g., through page.evaluate polling or browser context events)
|
|
@@ -5076,7 +5984,7 @@ export class BrowserSession {
|
|
|
5076
5984
|
.evaluate?.(() => document.visibilityState === 'visible')
|
|
5077
5985
|
.then((isVisible) => {
|
|
5078
5986
|
if (isVisible) {
|
|
5079
|
-
this.logger.debug(`Tab became visible: ${page.url?.() || 'unknown'}`);
|
|
5987
|
+
this.logger.debug(`Tab became visible: ${BrowserSession._redact_url_for_logging(page.url?.() || 'unknown')}`);
|
|
5080
5988
|
this.human_current_page = page;
|
|
5081
5989
|
}
|
|
5082
5990
|
})
|