browser-use 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/profile.js +24 -8
- package/dist/browser/session.d.ts +3 -0
- package/dist/browser/session.js +352 -112
- package/dist/dom/dom_tree/index.js +24 -11
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +2 -1
- package/package.json +1 -1
package/dist/browser/profile.js
CHANGED
|
@@ -360,10 +360,17 @@ const DEFAULT_BROWSER_PROFILE_OPTIONS = {
|
|
|
360
360
|
profile_directory: 'Default',
|
|
361
361
|
cookies_file: null,
|
|
362
362
|
};
|
|
363
|
+
const splitArgOnce = (arg) => {
|
|
364
|
+
const separatorIndex = arg.indexOf('=');
|
|
365
|
+
if (separatorIndex === -1) {
|
|
366
|
+
return [arg, ''];
|
|
367
|
+
}
|
|
368
|
+
return [arg.slice(0, separatorIndex), arg.slice(separatorIndex + 1)];
|
|
369
|
+
};
|
|
363
370
|
const argsAsDict = (args) => {
|
|
364
371
|
const result = {};
|
|
365
372
|
for (const arg of args) {
|
|
366
|
-
const [keyPart, valuePart = ''] = arg
|
|
373
|
+
const [keyPart, valuePart = ''] = splitArgOnce(arg);
|
|
367
374
|
const key = keyPart.trim().replace(/^-+/, '');
|
|
368
375
|
result[key] = valuePart.trim();
|
|
369
376
|
}
|
|
@@ -376,9 +383,22 @@ const cloneDefaultOptions = () => JSON.parse(JSON.stringify(DEFAULT_BROWSER_PROF
|
|
|
376
383
|
const normalizeDomainEntry = (entry) => String(entry ?? '')
|
|
377
384
|
.trim()
|
|
378
385
|
.toLowerCase();
|
|
386
|
+
const isExactHostDomainEntry = (entry) => {
|
|
387
|
+
if (!entry) {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
if (entry.includes('*') || entry.includes('://') || entry.includes('/')) {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
// Keep set optimization for plain hostnames only. Entries with ports/pattern-like
|
|
394
|
+
// delimiters must stay as arrays to preserve wildcard/scheme matching semantics.
|
|
395
|
+
return !entry.includes(':');
|
|
396
|
+
};
|
|
379
397
|
const optimizeDomainList = (value) => {
|
|
380
398
|
const cleaned = value.map(normalizeDomainEntry).filter(Boolean);
|
|
381
|
-
|
|
399
|
+
const canOptimizeToSet = cleaned.length >= DOMAIN_OPTIMIZATION_THRESHOLD &&
|
|
400
|
+
cleaned.every(isExactHostDomainEntry);
|
|
401
|
+
if (canOptimizeToSet) {
|
|
382
402
|
logger.warning(`Optimizing domain list with ${cleaned.length} entries to a Set for O(1) matching`);
|
|
383
403
|
return new Set(cleaned);
|
|
384
404
|
}
|
|
@@ -407,16 +427,12 @@ export class BrowserProfile {
|
|
|
407
427
|
allowed_domains: Array.isArray(init.allowed_domains)
|
|
408
428
|
? optimizeDomainList(init.allowed_domains)
|
|
409
429
|
: init.allowed_domains instanceof Set
|
|
410
|
-
?
|
|
411
|
-
.map(normalizeDomainEntry)
|
|
412
|
-
.filter(Boolean))
|
|
430
|
+
? optimizeDomainList(Array.from(init.allowed_domains))
|
|
413
431
|
: defaults.allowed_domains,
|
|
414
432
|
prohibited_domains: Array.isArray(init.prohibited_domains)
|
|
415
433
|
? optimizeDomainList(init.prohibited_domains)
|
|
416
434
|
: init.prohibited_domains instanceof Set
|
|
417
|
-
?
|
|
418
|
-
.map(normalizeDomainEntry)
|
|
419
|
-
.filter(Boolean))
|
|
435
|
+
? optimizeDomainList(Array.from(init.prohibited_domains))
|
|
420
436
|
: defaults.prohibited_domains,
|
|
421
437
|
window_position: init.window_position ?? defaults.window_position,
|
|
422
438
|
};
|
|
@@ -96,6 +96,8 @@ export declare class BrowserSession {
|
|
|
96
96
|
get_or_create_cdp_session(page?: Page | null): Promise<any>;
|
|
97
97
|
private _waitForStableNetwork;
|
|
98
98
|
private _setActivePage;
|
|
99
|
+
private _syncCurrentTabFromPage;
|
|
100
|
+
private _syncTabsWithBrowserPages;
|
|
99
101
|
private _captureClosedPopupMessage;
|
|
100
102
|
private _getClosedPopupMessagesSnapshot;
|
|
101
103
|
private _recordRecentEvent;
|
|
@@ -327,6 +329,7 @@ export declare class BrowserSession {
|
|
|
327
329
|
private _is_new_tab_page;
|
|
328
330
|
private _is_ip_address_host;
|
|
329
331
|
private _get_domain_variants;
|
|
332
|
+
private _setEntryMatchesUrl;
|
|
330
333
|
/**
|
|
331
334
|
* Check if page is displaying a PDF
|
|
332
335
|
*/
|
package/dist/browser/session.js
CHANGED
|
@@ -400,6 +400,117 @@ export class BrowserSession {
|
|
|
400
400
|
this._attachDialogHandler(page);
|
|
401
401
|
this.agent_current_page = page ?? null;
|
|
402
402
|
}
|
|
403
|
+
async _syncCurrentTabFromPage(page) {
|
|
404
|
+
if (!page) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
let resolvedUrl = null;
|
|
408
|
+
try {
|
|
409
|
+
const rawUrl = page.url();
|
|
410
|
+
if (typeof rawUrl === 'string' && rawUrl.trim()) {
|
|
411
|
+
resolvedUrl = normalize_url(rawUrl);
|
|
412
|
+
this.currentUrl = resolvedUrl;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
// Ignore transient URL read failures.
|
|
417
|
+
}
|
|
418
|
+
let resolvedTitle = null;
|
|
419
|
+
if (typeof page.title === 'function') {
|
|
420
|
+
try {
|
|
421
|
+
const title = await page.title();
|
|
422
|
+
if (typeof title === 'string' && title.trim()) {
|
|
423
|
+
resolvedTitle = title;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// Ignore transient title read failures.
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (!resolvedTitle) {
|
|
431
|
+
resolvedTitle = resolvedUrl ?? this.currentTitle ?? this.currentUrl;
|
|
432
|
+
}
|
|
433
|
+
this.currentTitle = resolvedTitle;
|
|
434
|
+
const currentTab = this._tabs[this.currentTabIndex];
|
|
435
|
+
if (currentTab) {
|
|
436
|
+
if (resolvedUrl) {
|
|
437
|
+
currentTab.url = resolvedUrl;
|
|
438
|
+
}
|
|
439
|
+
currentTab.title = resolvedTitle;
|
|
440
|
+
this._syncSessionManagerFromTabs();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
_syncTabsWithBrowserPages() {
|
|
444
|
+
const pages = this.browser_context?.pages?.() ?? [];
|
|
445
|
+
if (!pages.length) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const nextTabs = [];
|
|
449
|
+
const nextTabPages = new Map();
|
|
450
|
+
const usedPageIds = new Set();
|
|
451
|
+
const knownPageMappings = Array.from(this.tabPages.entries());
|
|
452
|
+
for (const page of pages) {
|
|
453
|
+
this._attachDialogHandler(page ?? null);
|
|
454
|
+
let pageId = null;
|
|
455
|
+
for (const [candidateId, candidatePage] of knownPageMappings) {
|
|
456
|
+
if (candidatePage === page && !usedPageIds.has(candidateId)) {
|
|
457
|
+
pageId = candidateId;
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (pageId === null) {
|
|
462
|
+
pageId = this._tabCounter++;
|
|
463
|
+
}
|
|
464
|
+
usedPageIds.add(pageId);
|
|
465
|
+
const existingTab = this._tabs.find((tab) => tab.page_id === pageId);
|
|
466
|
+
const tab = existingTab
|
|
467
|
+
? { ...existingTab }
|
|
468
|
+
: this._createTabInfo({
|
|
469
|
+
page_id: pageId,
|
|
470
|
+
url: 'about:blank',
|
|
471
|
+
title: 'about:blank',
|
|
472
|
+
});
|
|
473
|
+
try {
|
|
474
|
+
const rawUrl = page.url();
|
|
475
|
+
if (typeof rawUrl === 'string' && rawUrl.trim()) {
|
|
476
|
+
tab.url = normalize_url(rawUrl);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
// Keep existing tab url when page url is not readable.
|
|
481
|
+
}
|
|
482
|
+
if (!existingTab || !tab.title || tab.title === 'about:blank') {
|
|
483
|
+
tab.title = tab.url;
|
|
484
|
+
}
|
|
485
|
+
nextTabs.push(tab);
|
|
486
|
+
nextTabPages.set(pageId, page);
|
|
487
|
+
}
|
|
488
|
+
if (!nextTabs.length) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
this._tabs = nextTabs;
|
|
492
|
+
this.tabPages = nextTabPages;
|
|
493
|
+
const activePage = this.agent_current_page && pages.includes(this.agent_current_page)
|
|
494
|
+
? this.agent_current_page
|
|
495
|
+
: pages[0] ?? null;
|
|
496
|
+
if (activePage) {
|
|
497
|
+
const activeIndex = this._tabs.findIndex((tab) => this.tabPages.get(tab.page_id) === activePage);
|
|
498
|
+
if (activeIndex !== -1) {
|
|
499
|
+
this.currentTabIndex = activeIndex;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (this.currentTabIndex < 0 || this.currentTabIndex >= this._tabs.length) {
|
|
503
|
+
this.currentTabIndex = Math.max(0, this._tabs.length - 1);
|
|
504
|
+
}
|
|
505
|
+
const activeTab = this._tabs[this.currentTabIndex] ?? null;
|
|
506
|
+
if (activeTab) {
|
|
507
|
+
this.currentUrl = activeTab.url;
|
|
508
|
+
this.currentTitle = activeTab.title || activeTab.url;
|
|
509
|
+
this.agent_current_page = this.tabPages.get(activeTab.page_id) ?? null;
|
|
510
|
+
this.human_current_page = this.human_current_page ?? this.agent_current_page;
|
|
511
|
+
}
|
|
512
|
+
this._syncSessionManagerFromTabs();
|
|
513
|
+
}
|
|
403
514
|
_captureClosedPopupMessage(dialogType, message) {
|
|
404
515
|
const normalizedType = String(dialogType || 'alert').trim() || 'alert';
|
|
405
516
|
const normalizedMessage = String(message || '').trim();
|
|
@@ -1101,7 +1212,7 @@ export class BrowserSession {
|
|
|
1101
1212
|
else {
|
|
1102
1213
|
try {
|
|
1103
1214
|
const domService = new DomService(page, this.logger);
|
|
1104
|
-
domState = await this._withAbort(domService.get_clickable_elements(), signal);
|
|
1215
|
+
domState = await this._withAbort(domService.get_clickable_elements(this.browser_profile.highlight_elements, -1, this.browser_profile.viewport_expansion), signal);
|
|
1105
1216
|
}
|
|
1106
1217
|
catch (error) {
|
|
1107
1218
|
if (this._isAbortError(error)) {
|
|
@@ -1176,6 +1287,9 @@ export class BrowserSession {
|
|
|
1176
1287
|
}
|
|
1177
1288
|
}
|
|
1178
1289
|
const pendingNetworkRequests = await this._getPendingNetworkRequests(page);
|
|
1290
|
+
if (page) {
|
|
1291
|
+
await this._syncCurrentTabFromPage(page);
|
|
1292
|
+
}
|
|
1179
1293
|
if (pageInfo &&
|
|
1180
1294
|
Number.isFinite(pageInfo.viewport_width) &&
|
|
1181
1295
|
Number.isFinite(pageInfo.viewport_height)) {
|
|
@@ -1225,6 +1339,7 @@ export class BrowserSession {
|
|
|
1225
1339
|
return summary;
|
|
1226
1340
|
}
|
|
1227
1341
|
async get_current_page() {
|
|
1342
|
+
this._syncTabsWithBrowserPages();
|
|
1228
1343
|
if (this.agent_current_page) {
|
|
1229
1344
|
return this.agent_current_page;
|
|
1230
1345
|
}
|
|
@@ -1275,6 +1390,7 @@ export class BrowserSession {
|
|
|
1275
1390
|
this._throwIfAborted(signal);
|
|
1276
1391
|
this._assert_url_allowed(url);
|
|
1277
1392
|
const normalized = normalize_url(url);
|
|
1393
|
+
let completedUrl = normalized;
|
|
1278
1394
|
const waitUntil = options.wait_until ?? 'domcontentloaded';
|
|
1279
1395
|
const timeoutMs = typeof options.timeout_ms === 'number' &&
|
|
1280
1396
|
Number.isFinite(options.timeout_ms)
|
|
@@ -1294,6 +1410,7 @@ export class BrowserSession {
|
|
|
1294
1410
|
await this._withAbort(page.goto(normalized, gotoOptions), signal);
|
|
1295
1411
|
const finalUrl = page.url();
|
|
1296
1412
|
this._assert_url_allowed(finalUrl);
|
|
1413
|
+
completedUrl = normalize_url(finalUrl);
|
|
1297
1414
|
await this._waitForStableNetwork(page, signal);
|
|
1298
1415
|
}
|
|
1299
1416
|
catch (error) {
|
|
@@ -1309,16 +1426,24 @@ export class BrowserSession {
|
|
|
1309
1426
|
}
|
|
1310
1427
|
}
|
|
1311
1428
|
this._throwIfAborted(signal);
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
this.
|
|
1429
|
+
if (page) {
|
|
1430
|
+
await this._syncCurrentTabFromPage(page);
|
|
1431
|
+
completedUrl = this.currentUrl || completedUrl;
|
|
1432
|
+
}
|
|
1433
|
+
else {
|
|
1434
|
+
this.currentUrl = normalized;
|
|
1435
|
+
this.currentTitle = normalized;
|
|
1436
|
+
if (this._tabs[this.currentTabIndex]) {
|
|
1437
|
+
this._tabs[this.currentTabIndex].url = normalized;
|
|
1438
|
+
this._tabs[this.currentTabIndex].title = normalized;
|
|
1439
|
+
}
|
|
1440
|
+
this._syncSessionManagerFromTabs();
|
|
1441
|
+
}
|
|
1442
|
+
if (this.historyStack[this.historyStack.length - 1] !== completedUrl) {
|
|
1443
|
+
this.historyStack.push(completedUrl);
|
|
1318
1444
|
}
|
|
1319
|
-
this._syncSessionManagerFromTabs();
|
|
1320
1445
|
this._setActivePage(page ?? null);
|
|
1321
|
-
this._recordRecentEvent('navigation_completed', { url:
|
|
1446
|
+
this._recordRecentEvent('navigation_completed', { url: completedUrl });
|
|
1322
1447
|
this.cachedBrowserState = null;
|
|
1323
1448
|
return this.agent_current_page;
|
|
1324
1449
|
}
|
|
@@ -1327,11 +1452,14 @@ export class BrowserSession {
|
|
|
1327
1452
|
this._throwIfAborted(signal);
|
|
1328
1453
|
this._assert_url_allowed(url);
|
|
1329
1454
|
const normalized = normalize_url(url);
|
|
1455
|
+
let completedUrl = normalized;
|
|
1330
1456
|
const waitUntil = options.wait_until ?? 'domcontentloaded';
|
|
1331
1457
|
const timeoutMs = typeof options.timeout_ms === 'number' &&
|
|
1332
1458
|
Number.isFinite(options.timeout_ms)
|
|
1333
1459
|
? Math.max(0, options.timeout_ms)
|
|
1334
1460
|
: null;
|
|
1461
|
+
const previousTabIndex = this.currentTabIndex;
|
|
1462
|
+
const previousTab = this._tabs[this.currentTabIndex] ?? null;
|
|
1335
1463
|
const newTab = this._createTabInfo({
|
|
1336
1464
|
page_id: this._tabCounter++,
|
|
1337
1465
|
url: normalized,
|
|
@@ -1342,11 +1470,6 @@ export class BrowserSession {
|
|
|
1342
1470
|
this.currentUrl = normalized;
|
|
1343
1471
|
this.currentTitle = normalized;
|
|
1344
1472
|
this.historyStack.push(normalized);
|
|
1345
|
-
this._recordRecentEvent('tab_created', {
|
|
1346
|
-
url: normalized,
|
|
1347
|
-
page_id: newTab.page_id,
|
|
1348
|
-
tab_id: newTab.tab_id,
|
|
1349
|
-
});
|
|
1350
1473
|
let page = null;
|
|
1351
1474
|
try {
|
|
1352
1475
|
page =
|
|
@@ -1362,6 +1485,7 @@ export class BrowserSession {
|
|
|
1362
1485
|
await this._withAbort(page.goto(normalized, gotoOptions), signal);
|
|
1363
1486
|
const finalUrl = page.url();
|
|
1364
1487
|
this._assert_url_allowed(finalUrl);
|
|
1488
|
+
completedUrl = normalize_url(finalUrl);
|
|
1365
1489
|
await this._waitForStableNetwork(page, signal);
|
|
1366
1490
|
}
|
|
1367
1491
|
}
|
|
@@ -1369,29 +1493,82 @@ export class BrowserSession {
|
|
|
1369
1493
|
if (this._isAbortError(error)) {
|
|
1370
1494
|
throw error;
|
|
1371
1495
|
}
|
|
1496
|
+
const message = error.message ?? 'Failed to open new tab';
|
|
1372
1497
|
this._recordRecentEvent('tab_navigation_failed', {
|
|
1373
1498
|
url: normalized,
|
|
1374
1499
|
page_id: newTab.page_id,
|
|
1375
1500
|
tab_id: newTab.tab_id,
|
|
1376
|
-
error_message:
|
|
1501
|
+
error_message: message,
|
|
1377
1502
|
});
|
|
1378
|
-
this.logger.debug(`Failed to open new tab via Playwright: ${
|
|
1503
|
+
this.logger.debug(`Failed to open new tab via Playwright: ${message}`);
|
|
1504
|
+
if (page?.close) {
|
|
1505
|
+
try {
|
|
1506
|
+
await page.close();
|
|
1507
|
+
}
|
|
1508
|
+
catch {
|
|
1509
|
+
// Ignore best-effort tab close failures during rollback.
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
this._tabs = this._tabs.filter((tab) => tab.page_id !== newTab.page_id);
|
|
1513
|
+
this.tabPages.delete(newTab.page_id);
|
|
1514
|
+
if (this.historyStack[this.historyStack.length - 1] === normalized) {
|
|
1515
|
+
this.historyStack.pop();
|
|
1516
|
+
}
|
|
1517
|
+
if (this._tabs.length > 0) {
|
|
1518
|
+
let restoredIndex = previousTab
|
|
1519
|
+
? this._tabs.findIndex((tab) => tab.page_id === previousTab.page_id)
|
|
1520
|
+
: -1;
|
|
1521
|
+
if (restoredIndex === -1) {
|
|
1522
|
+
restoredIndex = Math.min(previousTabIndex, this._tabs.length - 1);
|
|
1523
|
+
}
|
|
1524
|
+
this.currentTabIndex = Math.max(0, restoredIndex);
|
|
1525
|
+
const restoredTab = this._tabs[this.currentTabIndex];
|
|
1526
|
+
this.currentUrl = restoredTab.url;
|
|
1527
|
+
this.currentTitle = restoredTab.title;
|
|
1528
|
+
const restoredPage = this.tabPages.get(restoredTab.page_id) ?? null;
|
|
1529
|
+
this._setActivePage(restoredPage);
|
|
1530
|
+
await this._syncCurrentTabFromPage(restoredPage);
|
|
1531
|
+
}
|
|
1532
|
+
else {
|
|
1533
|
+
this.currentTabIndex = 0;
|
|
1534
|
+
this.currentUrl = 'about:blank';
|
|
1535
|
+
this.currentTitle = 'about:blank';
|
|
1536
|
+
this._setActivePage(null);
|
|
1537
|
+
}
|
|
1538
|
+
this._syncSessionManagerFromTabs();
|
|
1539
|
+
this.cachedBrowserState = null;
|
|
1540
|
+
throw new BrowserError(message);
|
|
1379
1541
|
}
|
|
1380
1542
|
this.tabPages.set(newTab.page_id, page);
|
|
1381
1543
|
this._syncSessionManagerFromTabs();
|
|
1382
1544
|
this._setActivePage(page);
|
|
1545
|
+
if (page) {
|
|
1546
|
+
await this._syncCurrentTabFromPage(page);
|
|
1547
|
+
completedUrl = this.currentUrl || completedUrl;
|
|
1548
|
+
}
|
|
1549
|
+
if (this.historyStack[this.historyStack.length - 1] === normalized) {
|
|
1550
|
+
this.historyStack[this.historyStack.length - 1] = completedUrl;
|
|
1551
|
+
}
|
|
1552
|
+
else if (this.historyStack[this.historyStack.length - 1] !== completedUrl) {
|
|
1553
|
+
this.historyStack.push(completedUrl);
|
|
1554
|
+
}
|
|
1383
1555
|
this.currentPageLoadingStatus = null;
|
|
1384
1556
|
if (!this.human_current_page) {
|
|
1385
1557
|
this.human_current_page = page;
|
|
1386
1558
|
}
|
|
1559
|
+
this._recordRecentEvent('tab_created', {
|
|
1560
|
+
url: completedUrl,
|
|
1561
|
+
page_id: newTab.page_id,
|
|
1562
|
+
tab_id: newTab.tab_id,
|
|
1563
|
+
});
|
|
1387
1564
|
this._recordRecentEvent('tab_ready', {
|
|
1388
|
-
url:
|
|
1565
|
+
url: completedUrl,
|
|
1389
1566
|
page_id: newTab.page_id,
|
|
1390
1567
|
tab_id: newTab.tab_id,
|
|
1391
1568
|
});
|
|
1392
1569
|
await this.event_bus.dispatch(new TabCreatedEvent({
|
|
1393
1570
|
target_id: newTab.target_id ?? newTab.tab_id ?? 'unknown_target',
|
|
1394
|
-
url:
|
|
1571
|
+
url: completedUrl,
|
|
1395
1572
|
}));
|
|
1396
1573
|
this.cachedBrowserState = null;
|
|
1397
1574
|
return this.agent_current_page;
|
|
@@ -1434,6 +1611,7 @@ export class BrowserSession {
|
|
|
1434
1611
|
async switch_to_tab(identifier, options = {}) {
|
|
1435
1612
|
const signal = options.signal ?? null;
|
|
1436
1613
|
this._throwIfAborted(signal);
|
|
1614
|
+
this._syncTabsWithBrowserPages();
|
|
1437
1615
|
const index = this._resolveTabIndex(identifier);
|
|
1438
1616
|
const tab = index >= 0 ? (this._tabs[index] ?? null) : null;
|
|
1439
1617
|
if (!tab) {
|
|
@@ -1473,6 +1651,7 @@ export class BrowserSession {
|
|
|
1473
1651
|
return page;
|
|
1474
1652
|
}
|
|
1475
1653
|
async close_tab(identifier) {
|
|
1654
|
+
this._syncTabsWithBrowserPages();
|
|
1476
1655
|
const index = this._resolveTabIndex(identifier);
|
|
1477
1656
|
if (index < 0 || index >= this._tabs.length) {
|
|
1478
1657
|
throw new Error(`Tab '${identifier}' does not exist`);
|
|
@@ -1887,31 +2066,34 @@ export class BrowserSession {
|
|
|
1887
2066
|
async go_back(options = {}) {
|
|
1888
2067
|
const signal = options.signal ?? null;
|
|
1889
2068
|
this._throwIfAborted(signal);
|
|
1890
|
-
|
|
2069
|
+
const page = await this._withAbort(this.get_current_page(), signal);
|
|
2070
|
+
if (!page?.goBack) {
|
|
1891
2071
|
return;
|
|
1892
2072
|
}
|
|
1893
|
-
const
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
throw error;
|
|
1901
|
-
}
|
|
1902
|
-
this.logger.debug(`Failed to navigate back: ${error.message}`);
|
|
2073
|
+
const previousUrl = this.currentUrl;
|
|
2074
|
+
try {
|
|
2075
|
+
await this._withAbort(page.goBack(), signal);
|
|
2076
|
+
}
|
|
2077
|
+
catch (error) {
|
|
2078
|
+
if (this._isAbortError(error)) {
|
|
2079
|
+
throw error;
|
|
1903
2080
|
}
|
|
2081
|
+
this.logger.debug(`Failed to navigate back: ${error.message}`);
|
|
1904
2082
|
}
|
|
1905
2083
|
this._throwIfAborted(signal);
|
|
1906
|
-
this.
|
|
1907
|
-
const
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
2084
|
+
await this._syncCurrentTabFromPage(page);
|
|
2085
|
+
const currentUrl = this.currentUrl;
|
|
2086
|
+
if (currentUrl && currentUrl !== previousUrl) {
|
|
2087
|
+
const existingIndex = this.historyStack.lastIndexOf(currentUrl);
|
|
2088
|
+
if (existingIndex !== -1) {
|
|
2089
|
+
this.historyStack = this.historyStack.slice(0, existingIndex + 1);
|
|
2090
|
+
}
|
|
2091
|
+
else if (this.historyStack[this.historyStack.length - 1] !== currentUrl) {
|
|
2092
|
+
this.historyStack.push(currentUrl);
|
|
2093
|
+
}
|
|
1913
2094
|
}
|
|
1914
|
-
this.
|
|
2095
|
+
this.cachedBrowserState = null;
|
|
2096
|
+
this._recordRecentEvent('navigation_back', { url: currentUrl });
|
|
1915
2097
|
}
|
|
1916
2098
|
async get_dom_element_by_index(_index, options = {}) {
|
|
1917
2099
|
const selectorMap = await this.get_selector_map(options);
|
|
@@ -2123,6 +2305,13 @@ export class BrowserSession {
|
|
|
2123
2305
|
await performClick();
|
|
2124
2306
|
}
|
|
2125
2307
|
await this._waitForLoad(page, 5000, signal);
|
|
2308
|
+
if (page) {
|
|
2309
|
+
await this._syncCurrentTabFromPage(page);
|
|
2310
|
+
if (this.historyStack[this.historyStack.length - 1] !== this.currentUrl) {
|
|
2311
|
+
this.historyStack.push(this.currentUrl);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
this.cachedBrowserState = null;
|
|
2126
2315
|
return null;
|
|
2127
2316
|
}
|
|
2128
2317
|
async _waitForLoad(page, timeout = 5000, signal = null) {
|
|
@@ -2471,7 +2660,7 @@ export class BrowserSession {
|
|
|
2471
2660
|
cdp_session = await this.get_or_create_cdp_session(page);
|
|
2472
2661
|
// Capture screenshot via CDP
|
|
2473
2662
|
const screenshot_response = await cdp_session.send('Page.captureScreenshot', {
|
|
2474
|
-
captureBeyondViewport:
|
|
2663
|
+
captureBeyondViewport: full_page,
|
|
2475
2664
|
fromSurface: true,
|
|
2476
2665
|
format: 'png',
|
|
2477
2666
|
});
|
|
@@ -2548,22 +2737,33 @@ export class BrowserSession {
|
|
|
2548
2737
|
if (!this.browser_context) {
|
|
2549
2738
|
return [];
|
|
2550
2739
|
}
|
|
2740
|
+
this._syncTabsWithBrowserPages();
|
|
2551
2741
|
const tabs_info = [];
|
|
2552
|
-
const
|
|
2553
|
-
|
|
2554
|
-
const page =
|
|
2555
|
-
const tab_id =
|
|
2556
|
-
|
|
2557
|
-
|
|
2742
|
+
for (const tab of this._tabs) {
|
|
2743
|
+
const page_id = tab.page_id;
|
|
2744
|
+
const page = this.tabPages.get(page_id) ?? null;
|
|
2745
|
+
const tab_id = tab.tab_id || this._formatTabId(page_id);
|
|
2746
|
+
if (!tab.tab_id) {
|
|
2747
|
+
tab.tab_id = tab_id;
|
|
2748
|
+
}
|
|
2749
|
+
this._attachDialogHandler(page);
|
|
2750
|
+
let currentUrl = tab.url;
|
|
2751
|
+
if (page?.url) {
|
|
2752
|
+
try {
|
|
2753
|
+
currentUrl = normalize_url(page.url());
|
|
2754
|
+
}
|
|
2755
|
+
catch {
|
|
2756
|
+
// Keep tab url fallback when page url is unavailable.
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2558
2759
|
// Skip chrome:// pages and new tab pages
|
|
2559
|
-
const isNewTab =
|
|
2560
|
-
|
|
2561
|
-
if (isNewTab || page.url().startsWith('chrome://')) {
|
|
2760
|
+
const isNewTab = currentUrl === 'about:blank' || currentUrl.startsWith('chrome://newtab');
|
|
2761
|
+
if (isNewTab || currentUrl.startsWith('chrome://')) {
|
|
2562
2762
|
if (isNewTab) {
|
|
2563
2763
|
tabs_info.push({
|
|
2564
2764
|
page_id,
|
|
2565
2765
|
tab_id,
|
|
2566
|
-
url:
|
|
2766
|
+
url: currentUrl,
|
|
2567
2767
|
title: 'ignore this tab and do not use it',
|
|
2568
2768
|
});
|
|
2569
2769
|
}
|
|
@@ -2571,28 +2771,31 @@ export class BrowserSession {
|
|
|
2571
2771
|
tabs_info.push({
|
|
2572
2772
|
page_id,
|
|
2573
2773
|
tab_id,
|
|
2574
|
-
url:
|
|
2575
|
-
title:
|
|
2774
|
+
url: currentUrl,
|
|
2775
|
+
title: currentUrl,
|
|
2576
2776
|
});
|
|
2577
2777
|
}
|
|
2578
2778
|
continue;
|
|
2579
2779
|
}
|
|
2580
2780
|
// Normal pages - try to get title with timeout
|
|
2581
2781
|
try {
|
|
2782
|
+
if (!page?.title) {
|
|
2783
|
+
throw new Error('page_title_unavailable');
|
|
2784
|
+
}
|
|
2582
2785
|
const titlePromise = page.title();
|
|
2583
2786
|
const timeoutPromise = new Promise((_, reject) => {
|
|
2584
2787
|
setTimeout(() => reject(new Error('timeout')), 2000);
|
|
2585
2788
|
});
|
|
2586
2789
|
const title = await Promise.race([titlePromise, timeoutPromise]);
|
|
2587
|
-
tabs_info.push({ page_id, tab_id, url:
|
|
2790
|
+
tabs_info.push({ page_id, tab_id, url: currentUrl, title });
|
|
2588
2791
|
}
|
|
2589
2792
|
catch (error) {
|
|
2590
|
-
this.logger.debug(`⚠️ Failed to get tab info for tab #${page_id}: ${
|
|
2793
|
+
this.logger.debug(`⚠️ Failed to get tab info for tab #${page_id}: ${currentUrl} (using fallback title)`);
|
|
2591
2794
|
if (isNewTab) {
|
|
2592
2795
|
tabs_info.push({
|
|
2593
2796
|
page_id,
|
|
2594
2797
|
tab_id,
|
|
2595
|
-
url:
|
|
2798
|
+
url: currentUrl,
|
|
2596
2799
|
title: 'ignore this tab and do not use it',
|
|
2597
2800
|
});
|
|
2598
2801
|
}
|
|
@@ -2600,8 +2803,8 @@ export class BrowserSession {
|
|
|
2600
2803
|
tabs_info.push({
|
|
2601
2804
|
page_id,
|
|
2602
2805
|
tab_id,
|
|
2603
|
-
url:
|
|
2604
|
-
title:
|
|
2806
|
+
url: currentUrl,
|
|
2807
|
+
title: tab.title || currentUrl,
|
|
2605
2808
|
});
|
|
2606
2809
|
}
|
|
2607
2810
|
}
|
|
@@ -2966,6 +3169,15 @@ export class BrowserSession {
|
|
|
2966
3169
|
}
|
|
2967
3170
|
return [host, `www.${host}`];
|
|
2968
3171
|
}
|
|
3172
|
+
_setEntryMatchesUrl(domains, hostVariant, hostAlt, protocol) {
|
|
3173
|
+
const matchedHost = domains.has(hostVariant) || domains.has(hostAlt);
|
|
3174
|
+
if (!matchedHost) {
|
|
3175
|
+
return false;
|
|
3176
|
+
}
|
|
3177
|
+
// Set-optimized entries are exact hostnames without explicit schemes,
|
|
3178
|
+
// so keep parity with pattern matching default: https-only.
|
|
3179
|
+
return protocol.toLowerCase() === 'https:';
|
|
3180
|
+
}
|
|
2969
3181
|
/**
|
|
2970
3182
|
* Check if page is displaying a PDF
|
|
2971
3183
|
*/
|
|
@@ -3258,7 +3470,7 @@ export class BrowserSession {
|
|
|
3258
3470
|
((Array.isArray(allowedDomains) && allowedDomains.length > 0) ||
|
|
3259
3471
|
(allowedDomains instanceof Set && allowedDomains.size > 0))) {
|
|
3260
3472
|
if (allowedDomains instanceof Set) {
|
|
3261
|
-
if (
|
|
3473
|
+
if (this._setEntryMatchesUrl(allowedDomains, hostVariant, hostAlt, parsed.protocol)) {
|
|
3262
3474
|
return null;
|
|
3263
3475
|
}
|
|
3264
3476
|
}
|
|
@@ -3281,8 +3493,7 @@ export class BrowserSession {
|
|
|
3281
3493
|
((Array.isArray(prohibitedDomains) && prohibitedDomains.length > 0) ||
|
|
3282
3494
|
(prohibitedDomains instanceof Set && prohibitedDomains.size > 0))) {
|
|
3283
3495
|
if (prohibitedDomains instanceof Set) {
|
|
3284
|
-
if (
|
|
3285
|
-
prohibitedDomains.has(hostAlt)) {
|
|
3496
|
+
if (this._setEntryMatchesUrl(prohibitedDomains, hostVariant, hostAlt, parsed.protocol)) {
|
|
3286
3497
|
return 'in_prohibited_domains';
|
|
3287
3498
|
}
|
|
3288
3499
|
}
|
|
@@ -3405,57 +3616,30 @@ export class BrowserSession {
|
|
|
3405
3616
|
// Check if downloads are enabled
|
|
3406
3617
|
const downloads_path = this.browser_profile.downloads_path;
|
|
3407
3618
|
if (downloads_path) {
|
|
3619
|
+
fs.mkdirSync(downloads_path, { recursive: true });
|
|
3620
|
+
// Try to detect file download.
|
|
3621
|
+
const download_promise = page.waitForEvent('download', {
|
|
3622
|
+
timeout: 5000,
|
|
3623
|
+
});
|
|
3624
|
+
// Click failures should bubble to the caller.
|
|
3408
3625
|
try {
|
|
3409
|
-
// Try to detect file download
|
|
3410
|
-
const download_promise = page.waitForEvent('download', {
|
|
3411
|
-
timeout: 5000,
|
|
3412
|
-
});
|
|
3413
|
-
// Perform the click
|
|
3414
3626
|
await element_handle.click();
|
|
3415
|
-
// Wait for download or timeout
|
|
3416
|
-
const download = await download_promise;
|
|
3417
|
-
// Save the downloaded file
|
|
3418
|
-
const suggested_filename = download.suggestedFilename();
|
|
3419
|
-
const unique_filename = await BrowserSession.get_unique_filename(downloads_path, suggested_filename);
|
|
3420
|
-
const download_path = path.join(downloads_path, unique_filename);
|
|
3421
|
-
const download_guid = uuid7str();
|
|
3422
|
-
const download_url = typeof download.url === 'function'
|
|
3423
|
-
? download.url()
|
|
3424
|
-
: (this.currentUrl ?? '');
|
|
3425
|
-
await this.event_bus.dispatch(new DownloadStartedEvent({
|
|
3426
|
-
guid: download_guid,
|
|
3427
|
-
url: download_url,
|
|
3428
|
-
suggested_filename,
|
|
3429
|
-
auto_download: false,
|
|
3430
|
-
}));
|
|
3431
|
-
await download.saveAs(download_path);
|
|
3432
|
-
this.logger.info(`⬇️ Downloaded file to: ${download_path}`);
|
|
3433
|
-
const stats = fs.existsSync(download_path)
|
|
3434
|
-
? fs.statSync(download_path)
|
|
3435
|
-
: null;
|
|
3436
|
-
await this.event_bus.dispatch(new DownloadProgressEvent({
|
|
3437
|
-
guid: download_guid,
|
|
3438
|
-
received_bytes: stats?.size ?? 0,
|
|
3439
|
-
total_bytes: stats?.size ?? 0,
|
|
3440
|
-
state: 'completed',
|
|
3441
|
-
}));
|
|
3442
|
-
const fileDownloadedResult = await this.event_bus.dispatch(new FileDownloadedEvent({
|
|
3443
|
-
guid: download_guid,
|
|
3444
|
-
url: download_url,
|
|
3445
|
-
path: download_path,
|
|
3446
|
-
file_name: unique_filename,
|
|
3447
|
-
file_size: stats?.size ?? 0,
|
|
3448
|
-
file_type: path.extname(unique_filename).replace('.', '') || null,
|
|
3449
|
-
mime_type: null,
|
|
3450
|
-
auto_download: false,
|
|
3451
|
-
}));
|
|
3452
|
-
if (fileDownloadedResult.handler_results.length === 0) {
|
|
3453
|
-
this.add_downloaded_file(download_path);
|
|
3454
|
-
}
|
|
3455
|
-
return download_path;
|
|
3456
3627
|
}
|
|
3457
3628
|
catch (error) {
|
|
3458
|
-
|
|
3629
|
+
void download_promise.catch(() => undefined);
|
|
3630
|
+
throw error;
|
|
3631
|
+
}
|
|
3632
|
+
let download;
|
|
3633
|
+
try {
|
|
3634
|
+
download = await download_promise;
|
|
3635
|
+
}
|
|
3636
|
+
catch (error) {
|
|
3637
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3638
|
+
const isDownloadTimeout = error instanceof Error &&
|
|
3639
|
+
(error.name === 'TimeoutError' || message.toLowerCase().includes('timeout'));
|
|
3640
|
+
if (!isDownloadTimeout) {
|
|
3641
|
+
throw error;
|
|
3642
|
+
}
|
|
3459
3643
|
this.logger.debug('No download triggered within timeout. Checking navigation...');
|
|
3460
3644
|
try {
|
|
3461
3645
|
await page.waitForLoadState();
|
|
@@ -3463,7 +3647,45 @@ export class BrowserSession {
|
|
|
3463
3647
|
catch (e) {
|
|
3464
3648
|
this.logger.warning(`Navigation check failed: ${e.message}`);
|
|
3465
3649
|
}
|
|
3650
|
+
return null;
|
|
3651
|
+
}
|
|
3652
|
+
// Save the downloaded file.
|
|
3653
|
+
const suggested_filename = download.suggestedFilename();
|
|
3654
|
+
const unique_filename = await BrowserSession.get_unique_filename(downloads_path, suggested_filename);
|
|
3655
|
+
const download_path = path.join(downloads_path, unique_filename);
|
|
3656
|
+
const download_guid = uuid7str();
|
|
3657
|
+
const download_url = typeof download.url === 'function' ? download.url() : (this.currentUrl ?? '');
|
|
3658
|
+
await this.event_bus.dispatch(new DownloadStartedEvent({
|
|
3659
|
+
guid: download_guid,
|
|
3660
|
+
url: download_url,
|
|
3661
|
+
suggested_filename,
|
|
3662
|
+
auto_download: false,
|
|
3663
|
+
}));
|
|
3664
|
+
await download.saveAs(download_path);
|
|
3665
|
+
this.logger.info(`⬇️ Downloaded file to: ${download_path}`);
|
|
3666
|
+
const stats = fs.existsSync(download_path)
|
|
3667
|
+
? fs.statSync(download_path)
|
|
3668
|
+
: null;
|
|
3669
|
+
await this.event_bus.dispatch(new DownloadProgressEvent({
|
|
3670
|
+
guid: download_guid,
|
|
3671
|
+
received_bytes: stats?.size ?? 0,
|
|
3672
|
+
total_bytes: stats?.size ?? 0,
|
|
3673
|
+
state: 'completed',
|
|
3674
|
+
}));
|
|
3675
|
+
const fileDownloadedResult = await this.event_bus.dispatch(new FileDownloadedEvent({
|
|
3676
|
+
guid: download_guid,
|
|
3677
|
+
url: download_url,
|
|
3678
|
+
path: download_path,
|
|
3679
|
+
file_name: unique_filename,
|
|
3680
|
+
file_size: stats?.size ?? 0,
|
|
3681
|
+
file_type: path.extname(unique_filename).replace('.', '') || null,
|
|
3682
|
+
mime_type: null,
|
|
3683
|
+
auto_download: false,
|
|
3684
|
+
}));
|
|
3685
|
+
if (fileDownloadedResult.handler_results.length === 0) {
|
|
3686
|
+
this.add_downloaded_file(download_path);
|
|
3466
3687
|
}
|
|
3688
|
+
return download_path;
|
|
3467
3689
|
}
|
|
3468
3690
|
else {
|
|
3469
3691
|
// No downloads path configured, just click
|
|
@@ -3481,15 +3703,33 @@ export class BrowserSession {
|
|
|
3481
3703
|
}
|
|
3482
3704
|
try {
|
|
3483
3705
|
await page.evaluate(() => {
|
|
3484
|
-
|
|
3706
|
+
const pageWindow = window;
|
|
3707
|
+
const cleanupFunctions = Array.isArray(pageWindow._highlightCleanupFunctions)
|
|
3708
|
+
? pageWindow._highlightCleanupFunctions
|
|
3709
|
+
: [];
|
|
3710
|
+
for (const cleanupFn of cleanupFunctions) {
|
|
3711
|
+
try {
|
|
3712
|
+
if (typeof cleanupFn === 'function') {
|
|
3713
|
+
cleanupFn();
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
catch {
|
|
3717
|
+
// Ignore callback cleanup failures.
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
pageWindow._highlightCleanupFunctions = [];
|
|
3721
|
+
const containers = document.querySelectorAll('#playwright-highlight-container');
|
|
3722
|
+
containers.forEach((element) => element.remove());
|
|
3723
|
+
const labels = document.querySelectorAll('.playwright-highlight-label');
|
|
3724
|
+
labels.forEach((element) => element.remove());
|
|
3725
|
+
// Backward compatibility with legacy selectors.
|
|
3485
3726
|
const highlights = document.querySelectorAll('.browser-use-highlight');
|
|
3486
|
-
highlights.forEach((
|
|
3487
|
-
// Remove inline highlight styles
|
|
3727
|
+
highlights.forEach((element) => element.remove());
|
|
3488
3728
|
const styled = document.querySelectorAll('[style*="browser-use"]');
|
|
3489
|
-
styled.forEach((
|
|
3490
|
-
if (
|
|
3491
|
-
|
|
3492
|
-
|
|
3729
|
+
styled.forEach((element) => {
|
|
3730
|
+
if (element.style) {
|
|
3731
|
+
element.style.outline = '';
|
|
3732
|
+
element.style.border = '';
|
|
3493
3733
|
}
|
|
3494
3734
|
});
|
|
3495
3735
|
});
|
|
@@ -343,17 +343,29 @@
|
|
|
343
343
|
}
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
346
|
+
function cleanupHighlights() {
|
|
347
|
+
try {
|
|
348
|
+
const cleanupFns = Array.isArray(window._highlightCleanupFunctions)
|
|
349
|
+
? window._highlightCleanupFunctions
|
|
350
|
+
: [];
|
|
351
|
+
for (const fn of cleanupFns) {
|
|
352
|
+
try {
|
|
353
|
+
if (typeof fn === 'function') {
|
|
354
|
+
fn();
|
|
355
|
+
}
|
|
356
|
+
} catch (error) {
|
|
357
|
+
// Ignore cleanup callback failures to keep extraction resilient.
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
window._highlightCleanupFunctions = [];
|
|
361
|
+
const container = document.getElementById(HIGHLIGHT_CONTAINER_ID);
|
|
362
|
+
if (container) {
|
|
363
|
+
container.remove();
|
|
364
|
+
}
|
|
365
|
+
} catch (error) {
|
|
366
|
+
// Ignore cleanup failures and continue with DOM extraction.
|
|
367
|
+
}
|
|
368
|
+
}
|
|
357
369
|
|
|
358
370
|
/**
|
|
359
371
|
* Gets the position of an element in its parent.
|
|
@@ -1391,6 +1403,7 @@
|
|
|
1391
1403
|
return id;
|
|
1392
1404
|
}
|
|
1393
1405
|
|
|
1406
|
+
cleanupHighlights();
|
|
1394
1407
|
const rootId = buildDomTree(document.body);
|
|
1395
1408
|
|
|
1396
1409
|
// Clear the cache before starting
|
package/dist/utils.d.ts
CHANGED
|
@@ -116,7 +116,7 @@ export declare function createSemaphore(maxConcurrent: number): {
|
|
|
116
116
|
getQueueLength(): number;
|
|
117
117
|
};
|
|
118
118
|
/**
|
|
119
|
-
* Check if a URL is a new tab page (about:blank
|
|
119
|
+
* Check if a URL is a new tab page (about:blank/about:newtab/chrome://new-tab-page/chrome://newtab).
|
|
120
120
|
*/
|
|
121
121
|
export declare function is_new_tab_page(url: string): boolean;
|
|
122
122
|
/**
|
package/dist/utils.js
CHANGED
|
@@ -488,10 +488,11 @@ export function createSemaphore(maxConcurrent) {
|
|
|
488
488
|
};
|
|
489
489
|
}
|
|
490
490
|
/**
|
|
491
|
-
* Check if a URL is a new tab page (about:blank
|
|
491
|
+
* Check if a URL is a new tab page (about:blank/about:newtab/chrome://new-tab-page/chrome://newtab).
|
|
492
492
|
*/
|
|
493
493
|
export function is_new_tab_page(url) {
|
|
494
494
|
return (url === 'about:blank' ||
|
|
495
|
+
url === 'about:newtab' ||
|
|
495
496
|
url === 'chrome://new-tab-page/' ||
|
|
496
497
|
url === 'chrome://new-tab-page' ||
|
|
497
498
|
url === 'chrome://newtab/' ||
|
package/package.json
CHANGED