agentic-browser 2.6.2 → 2.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 +15 -0
- package/dist/cli/index.mjs +263 -16
- package/dist/index.d.mts +73 -1
- package/dist/index.mjs +1 -1
- package/dist/mcp/index.mjs +1 -1
- package/dist/{runtime-Bo7FmRYk.mjs → runtime-CnexYwrf.mjs} +700 -94
- package/package.json +1 -1
|
@@ -393,6 +393,28 @@ async function initTarget(targetWsUrl, userAgent) {
|
|
|
393
393
|
conn.close();
|
|
394
394
|
}
|
|
395
395
|
}
|
|
396
|
+
/** Called on the iframe element: origin of its content box in top-level viewport coordinates. */
|
|
397
|
+
const FRAME_CONTENT_OFFSET_FUNCTION = `function () {
|
|
398
|
+
const rect = this.getBoundingClientRect();
|
|
399
|
+
const style = getComputedStyle(this);
|
|
400
|
+
let x = rect.left + this.clientLeft + (parseFloat(style.paddingLeft) || 0);
|
|
401
|
+
let y = rect.top + this.clientTop + (parseFloat(style.paddingTop) || 0);
|
|
402
|
+
let doc = this.ownerDocument;
|
|
403
|
+
while (doc) {
|
|
404
|
+
let frameElement = null;
|
|
405
|
+
try {
|
|
406
|
+
frameElement = doc.defaultView && doc.defaultView.frameElement;
|
|
407
|
+
} catch {
|
|
408
|
+
frameElement = null;
|
|
409
|
+
}
|
|
410
|
+
if (!frameElement) break;
|
|
411
|
+
const frameRect = frameElement.getBoundingClientRect();
|
|
412
|
+
x += frameRect.left + frameElement.clientLeft;
|
|
413
|
+
y += frameRect.top + frameElement.clientTop;
|
|
414
|
+
doc = frameElement.ownerDocument;
|
|
415
|
+
}
|
|
416
|
+
return { x, y };
|
|
417
|
+
}`;
|
|
396
418
|
/**
|
|
397
419
|
* Run Runtime.evaluate and surface in-page exceptions as errors.
|
|
398
420
|
* CDP reports page-side throws via `exceptionDetails` on an otherwise successful
|
|
@@ -493,6 +515,82 @@ const LOCATOR_RUNTIME_HELPERS = String.raw`
|
|
|
493
515
|
return false;
|
|
494
516
|
}
|
|
495
517
|
|
|
518
|
+
const TEXT_INTERACTIVE_SELECTOR = [
|
|
519
|
+
'a[href]', 'button', 'input[type="button"]', 'input[type="submit"]', 'input[type="reset"]',
|
|
520
|
+
'[role="button"]', '[role="link"]', '[role="menuitem"]', '[role="tab"]', '[role="option"]',
|
|
521
|
+
'[role="checkbox"]', '[role="radio"]', '[role="switch"]', 'summary', 'label', 'option',
|
|
522
|
+
'[onclick]', '[tabindex]'
|
|
523
|
+
].join(',');
|
|
524
|
+
|
|
525
|
+
function normalizeText(value) {
|
|
526
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function textLabels(el, useInnerText) {
|
|
530
|
+
const labels = [];
|
|
531
|
+
const body = useInnerText && typeof el.innerText === 'string' ? el.innerText : el.textContent;
|
|
532
|
+
if (body) labels.push(normalizeText(body));
|
|
533
|
+
for (const attr of ['aria-label', 'title', 'alt', 'placeholder']) {
|
|
534
|
+
const value = el.getAttribute && el.getAttribute(attr);
|
|
535
|
+
if (value) labels.push(normalizeText(value));
|
|
536
|
+
}
|
|
537
|
+
if (el.tagName && el.tagName.toLowerCase() === 'input' && typeof el.value === 'string' && el.value) {
|
|
538
|
+
labels.push(normalizeText(el.value));
|
|
539
|
+
}
|
|
540
|
+
return labels;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function hasBox(el) {
|
|
544
|
+
const rect = el.getBoundingClientRect();
|
|
545
|
+
return rect.width > 0 && rect.height > 0;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// text=Foo matches by visible text (exact first, then substring);
|
|
549
|
+
// text="Foo" matches exactly. Interactive and visible elements win.
|
|
550
|
+
function queryByText(root, query) {
|
|
551
|
+
let raw = String(query).trim();
|
|
552
|
+
let exactOnly = false;
|
|
553
|
+
if (raw.length >= 2 && (raw[0] === '"' || raw[0] === "'") && raw[raw.length - 1] === raw[0]) {
|
|
554
|
+
raw = raw.slice(1, -1);
|
|
555
|
+
exactOnly = true;
|
|
556
|
+
}
|
|
557
|
+
const needle = normalizeText(raw);
|
|
558
|
+
if (!needle) return null;
|
|
559
|
+
|
|
560
|
+
function pick(candidates, useInnerText, preferSmallest) {
|
|
561
|
+
let best = null;
|
|
562
|
+
let bestScore = -1;
|
|
563
|
+
let bestSize = Infinity;
|
|
564
|
+
for (const el of candidates) {
|
|
565
|
+
const labels = textLabels(el, useInnerText);
|
|
566
|
+
let score = -1;
|
|
567
|
+
if (labels.some((label) => label === needle)) score = 2;
|
|
568
|
+
else if (!exactOnly && labels.some((label) => label.includes(needle))) score = 1;
|
|
569
|
+
if (score < 0) continue;
|
|
570
|
+
if (hasBox(el)) score += 4;
|
|
571
|
+
const size = preferSmallest ? (el.textContent || '').length : 0;
|
|
572
|
+
if (score > bestScore || (score === bestScore && size < bestSize)) {
|
|
573
|
+
best = el;
|
|
574
|
+
bestScore = score;
|
|
575
|
+
bestSize = size;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return best;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// On equal score the element with the least text is the most specific match.
|
|
582
|
+
const interactive = pick(root.querySelectorAll(TEXT_INTERACTIVE_SELECTOR), true, true);
|
|
583
|
+
if (interactive) return interactive;
|
|
584
|
+
return pick(root.querySelectorAll(root.nodeType === 9 ? 'body *' : '*'), false, true);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function querySegment(root, part) {
|
|
588
|
+
if (/^text=/i.test(part)) {
|
|
589
|
+
return queryByText(root, part.slice(5));
|
|
590
|
+
}
|
|
591
|
+
return root.querySelector(part);
|
|
592
|
+
}
|
|
593
|
+
|
|
496
594
|
function resolveLocator(locator) {
|
|
497
595
|
const parts = splitLocator(locator);
|
|
498
596
|
if (parts.length === 0) return null;
|
|
@@ -500,7 +598,7 @@ const LOCATOR_RUNTIME_HELPERS = String.raw`
|
|
|
500
598
|
let root = document;
|
|
501
599
|
let element = null;
|
|
502
600
|
for (let index = 0; index < parts.length; index += 1) {
|
|
503
|
-
element = root
|
|
601
|
+
element = querySegment(root, parts[index]);
|
|
504
602
|
if (!element) return null;
|
|
505
603
|
|
|
506
604
|
if (index === parts.length - 1) {
|
|
@@ -556,7 +654,7 @@ const LOCATOR_RUNTIME_HELPERS = String.raw`
|
|
|
556
654
|
if (lastError) {
|
|
557
655
|
throw lastError;
|
|
558
656
|
}
|
|
559
|
-
throw new Error('
|
|
657
|
+
throw new Error('Element not found: ' + locators.join(' | '));
|
|
560
658
|
}
|
|
561
659
|
|
|
562
660
|
function ensureInView(element) {
|
|
@@ -1015,6 +1113,182 @@ var ChromeCdpBrowserController = class {
|
|
|
1015
1113
|
return await fn(conn);
|
|
1016
1114
|
}
|
|
1017
1115
|
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Split a composed locator at the first iframe whose document the page
|
|
1118
|
+
* cannot reach (cross-origin). Returns null when no such boundary exists.
|
|
1119
|
+
*/
|
|
1120
|
+
async splitAtCrossOriginFrame(conn, locator) {
|
|
1121
|
+
if (!locator.includes(">>>")) return null;
|
|
1122
|
+
return (await evaluateOnPage(conn, {
|
|
1123
|
+
expression: buildLocatorRuntimeExpression("locator", locator, `
|
|
1124
|
+
const parts = splitLocator(locator);
|
|
1125
|
+
let root = document;
|
|
1126
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
1127
|
+
const el = querySegment(root, parts[index]);
|
|
1128
|
+
if (!el) return null;
|
|
1129
|
+
const tag = el.tagName ? el.tagName.toLowerCase() : '';
|
|
1130
|
+
if (tag === 'iframe' || tag === 'frame') {
|
|
1131
|
+
let doc = null;
|
|
1132
|
+
try {
|
|
1133
|
+
doc = el.contentDocument;
|
|
1134
|
+
} catch {
|
|
1135
|
+
doc = null;
|
|
1136
|
+
}
|
|
1137
|
+
if (!doc) {
|
|
1138
|
+
return {
|
|
1139
|
+
frame: parts.slice(0, index + 1).join(LOCATOR_SEPARATOR),
|
|
1140
|
+
rest: parts.slice(index + 1).join(LOCATOR_SEPARATOR),
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
root = doc;
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
if (el.shadowRoot) {
|
|
1147
|
+
root = el.shadowRoot;
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
return null;
|
|
1151
|
+
}
|
|
1152
|
+
return null;
|
|
1153
|
+
`),
|
|
1154
|
+
returnByValue: true,
|
|
1155
|
+
awaitPromise: true
|
|
1156
|
+
})).result.value ?? null;
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Work out which frame a payload targets. An explicit `frame` wins;
|
|
1160
|
+
* otherwise a composed selector crossing a cross-origin iframe is split.
|
|
1161
|
+
*/
|
|
1162
|
+
async resolveFrameTarget(conn, payload) {
|
|
1163
|
+
if (payload.frame) return {
|
|
1164
|
+
frame: payload.frame,
|
|
1165
|
+
payload
|
|
1166
|
+
};
|
|
1167
|
+
if (!payload.selector) return { payload };
|
|
1168
|
+
const split = await this.splitAtCrossOriginFrame(conn, payload.selector);
|
|
1169
|
+
if (!split) return { payload };
|
|
1170
|
+
const prefix = split.frame + " >>> ";
|
|
1171
|
+
const fallbackSelectors = payload.fallbackSelectors?.map((fallback) => fallback.startsWith(prefix) ? fallback.slice(prefix.length) : fallback);
|
|
1172
|
+
return {
|
|
1173
|
+
frame: split.frame,
|
|
1174
|
+
payload: {
|
|
1175
|
+
...payload,
|
|
1176
|
+
selector: split.rest,
|
|
1177
|
+
fallbackSelectors
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Open an evaluation scope inside an iframe. Same-process frames get an
|
|
1183
|
+
* isolated world on the page connection; out-of-process (cross-site) frames
|
|
1184
|
+
* get their own CDP connection to the frame target. Input events still go
|
|
1185
|
+
* to the page connection, translated by `getOffset()`.
|
|
1186
|
+
*/
|
|
1187
|
+
async openFrameScope(conn, targetWsUrl, frameLocator) {
|
|
1188
|
+
const objectId = (await evaluateOnPage(conn, {
|
|
1189
|
+
expression: buildLocatorRuntimeExpression("frameLocator", frameLocator, `
|
|
1190
|
+
const el = resolveLocator(frameLocator);
|
|
1191
|
+
if (!el) throw new Error('Frame not found: ' + frameLocator);
|
|
1192
|
+
const tag = el.tagName ? el.tagName.toLowerCase() : '';
|
|
1193
|
+
if (tag !== 'iframe' && tag !== 'frame') {
|
|
1194
|
+
throw new Error('Frame locator does not match an iframe: ' + frameLocator);
|
|
1195
|
+
}
|
|
1196
|
+
ensureInView(el);
|
|
1197
|
+
return el;
|
|
1198
|
+
`),
|
|
1199
|
+
awaitPromise: true
|
|
1200
|
+
})).result.objectId;
|
|
1201
|
+
if (!objectId) throw new Error(`Frame not found: ${frameLocator}`);
|
|
1202
|
+
const releaseObject = () => {
|
|
1203
|
+
conn.send("Runtime.releaseObject", { objectId }).catch(() => {});
|
|
1204
|
+
};
|
|
1205
|
+
try {
|
|
1206
|
+
const frameId = (await conn.send("DOM.describeNode", { objectId })).node?.frameId;
|
|
1207
|
+
if (!frameId) throw new Error(`Frame has no document yet: ${frameLocator}`);
|
|
1208
|
+
const getOffset = async () => {
|
|
1209
|
+
return (await conn.send("Runtime.callFunctionOn", {
|
|
1210
|
+
objectId,
|
|
1211
|
+
functionDeclaration: FRAME_CONTENT_OFFSET_FUNCTION,
|
|
1212
|
+
returnByValue: true
|
|
1213
|
+
})).result.value ?? {
|
|
1214
|
+
x: 0,
|
|
1215
|
+
y: 0
|
|
1216
|
+
};
|
|
1217
|
+
};
|
|
1218
|
+
let world = null;
|
|
1219
|
+
try {
|
|
1220
|
+
world = await conn.send("Page.createIsolatedWorld", {
|
|
1221
|
+
frameId,
|
|
1222
|
+
worldName: "agentic-browser"
|
|
1223
|
+
});
|
|
1224
|
+
} catch {
|
|
1225
|
+
world = null;
|
|
1226
|
+
}
|
|
1227
|
+
if (world?.executionContextId !== void 0) {
|
|
1228
|
+
const contextId = world.executionContextId;
|
|
1229
|
+
return {
|
|
1230
|
+
client: {
|
|
1231
|
+
send: (method, params, timeoutMs) => conn.send(method, method === "Runtime.evaluate" ? {
|
|
1232
|
+
...params,
|
|
1233
|
+
contextId
|
|
1234
|
+
} : params, timeoutMs),
|
|
1235
|
+
waitForEvent: (method, timeoutMs) => conn.waitForEvent(method, timeoutMs),
|
|
1236
|
+
close: () => {}
|
|
1237
|
+
},
|
|
1238
|
+
getOffset,
|
|
1239
|
+
release: releaseObject
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
const frameWsUrl = targetWsUrl.replace(/\/devtools\/page\/[^/?#]+/, `/devtools/page/${frameId}`);
|
|
1243
|
+
if (frameWsUrl === targetWsUrl) throw new Error(`Cannot derive a frame target URL from ${targetWsUrl}`);
|
|
1244
|
+
let frameConn;
|
|
1245
|
+
let lastError;
|
|
1246
|
+
const listUrl = targetWsUrl.replace(/^ws(s?):\/\/([^/]+)\/.*$/, "http$1://$2/json/list");
|
|
1247
|
+
for (let attempt = 0; attempt < 4 && !frameConn; attempt++) {
|
|
1248
|
+
if (attempt > 0) {
|
|
1249
|
+
await getJson(listUrl).catch(() => {});
|
|
1250
|
+
if (attempt > 1) await new Promise((resolve) => setTimeout(resolve, 250 * attempt));
|
|
1251
|
+
}
|
|
1252
|
+
try {
|
|
1253
|
+
frameConn = await this.connectionFactory(frameWsUrl);
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
lastError = error;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
if (!frameConn) throw new Error(`Unable to attach to cross-origin frame ${frameLocator}: ${lastError?.message}`, { cause: lastError });
|
|
1259
|
+
const attached = frameConn;
|
|
1260
|
+
return {
|
|
1261
|
+
client: attached,
|
|
1262
|
+
getOffset,
|
|
1263
|
+
release: () => {
|
|
1264
|
+
releaseObject();
|
|
1265
|
+
try {
|
|
1266
|
+
attached.close();
|
|
1267
|
+
} catch {}
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
} catch (error) {
|
|
1271
|
+
releaseObject();
|
|
1272
|
+
throw error;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
/** Run `fn` against the frame a payload targets (the page itself when none). */
|
|
1276
|
+
async withFrameScope(conn, targetWsUrl, payload, fn) {
|
|
1277
|
+
const target = await this.resolveFrameTarget(conn, payload);
|
|
1278
|
+
if (!target.frame) return await fn(conn, payload, async () => ({
|
|
1279
|
+
x: 0,
|
|
1280
|
+
y: 0
|
|
1281
|
+
}));
|
|
1282
|
+
const scope = await this.openFrameScope(conn, targetWsUrl, target.frame);
|
|
1283
|
+
try {
|
|
1284
|
+
return await fn(scope.client, {
|
|
1285
|
+
...target.payload,
|
|
1286
|
+
frame: void 0
|
|
1287
|
+
}, scope.getOffset);
|
|
1288
|
+
} finally {
|
|
1289
|
+
scope.release();
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1018
1292
|
async navigate(targetWsUrl, url) {
|
|
1019
1293
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1020
1294
|
await this.ensureEnabled(targetWsUrl);
|
|
@@ -1276,32 +1550,36 @@ var ChromeCdpBrowserController = class {
|
|
|
1276
1550
|
if (payload.action === "dialog") return await this.handleDialogAction(targetWsUrl, payload);
|
|
1277
1551
|
if (payload.action === "click" || payload.action === "hover" || payload.action === "type" || payload.action === "press") return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1278
1552
|
await this.ensureEnabled(targetWsUrl);
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1553
|
+
return await this.withFrameScope(conn, targetWsUrl, payload, async (client, scoped, getOffset) => {
|
|
1554
|
+
if (scoped.action === "click") {
|
|
1555
|
+
const local = await this.preparePointerTarget(client, scoped);
|
|
1556
|
+
const offset = await getOffset();
|
|
1557
|
+
const nav = await this.armMainFrameNavigationWait(conn);
|
|
1558
|
+
await this.dispatchMouseClick(conn, local.x + offset.x, local.y + offset.y);
|
|
1559
|
+
await nav.result;
|
|
1560
|
+
return "clicked";
|
|
1561
|
+
}
|
|
1562
|
+
if (scoped.action === "hover") {
|
|
1563
|
+
const local = await this.preparePointerTarget(client, scoped);
|
|
1564
|
+
const offset = await getOffset();
|
|
1565
|
+
await conn.send("Input.dispatchMouseEvent", {
|
|
1566
|
+
type: "mouseMoved",
|
|
1567
|
+
x: local.x + offset.x,
|
|
1568
|
+
y: local.y + offset.y,
|
|
1569
|
+
button: "none"
|
|
1570
|
+
});
|
|
1571
|
+
return "hovered";
|
|
1572
|
+
}
|
|
1573
|
+
if (scoped.action === "press") {
|
|
1574
|
+
await this.focusTarget(client, scoped);
|
|
1575
|
+
await this.dispatchKeyPress(conn, scoped.key);
|
|
1576
|
+
return "pressed";
|
|
1577
|
+
}
|
|
1578
|
+
if (await this.prepareTypeTarget(client, scoped) === "ready") await conn.send("Input.insertText", { text: scoped.text ?? "" });
|
|
1579
|
+
return "typed";
|
|
1580
|
+
});
|
|
1303
1581
|
}, "connection");
|
|
1304
|
-
const
|
|
1582
|
+
const buildExpression = (scoped) => buildLocatorRuntimeExpression("payload", scoped, `
|
|
1305
1583
|
if (payload.action === 'waitFor') {
|
|
1306
1584
|
const timeout = payload.timeoutMs ?? 2000;
|
|
1307
1585
|
const started = Date.now();
|
|
@@ -1319,7 +1597,7 @@ var ChromeCdpBrowserController = class {
|
|
|
1319
1597
|
if (result && typeof result === 'object' && typeof result.then === 'function') {
|
|
1320
1598
|
result = await result;
|
|
1321
1599
|
}
|
|
1322
|
-
return
|
|
1600
|
+
return result === undefined ? 'undefined' : JSON.stringify(result);
|
|
1323
1601
|
}
|
|
1324
1602
|
if (payload.action === 'scroll') {
|
|
1325
1603
|
if (payload.selector) {
|
|
@@ -1348,11 +1626,18 @@ var ChromeCdpBrowserController = class {
|
|
|
1348
1626
|
`);
|
|
1349
1627
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1350
1628
|
await this.ensureEnabled(targetWsUrl);
|
|
1351
|
-
return
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1629
|
+
return await this.withFrameScope(conn, targetWsUrl, payload, async (client, scoped) => {
|
|
1630
|
+
return (await evaluateOnPage(client, {
|
|
1631
|
+
expression: scoped.action === "evaluate" ? `(async () => {
|
|
1632
|
+
const __agenticResult = await (${scoped.text ?? "undefined"}
|
|
1633
|
+
);
|
|
1634
|
+
// Always JSON-encode so strings like "42" or "" survive the round trip as strings.
|
|
1635
|
+
return __agenticResult === undefined ? 'undefined' : JSON.stringify(__agenticResult);
|
|
1636
|
+
})()` : buildExpression(scoped),
|
|
1637
|
+
returnByValue: true,
|
|
1638
|
+
awaitPromise: true
|
|
1639
|
+
})).result.value ?? "";
|
|
1640
|
+
});
|
|
1356
1641
|
}, "connection");
|
|
1357
1642
|
}
|
|
1358
1643
|
async screenshot(targetWsUrl, options) {
|
|
@@ -1388,6 +1673,66 @@ var ChromeCdpBrowserController = class {
|
|
|
1388
1673
|
})).result.value ?? "";
|
|
1389
1674
|
});
|
|
1390
1675
|
}
|
|
1676
|
+
async getPageState(targetWsUrl) {
|
|
1677
|
+
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1678
|
+
return (await evaluateOnPage(conn, {
|
|
1679
|
+
expression: "({ url: location.href, title: document.title, readyState: document.readyState })",
|
|
1680
|
+
returnByValue: true
|
|
1681
|
+
})).result.value ?? {
|
|
1682
|
+
url: "",
|
|
1683
|
+
title: "",
|
|
1684
|
+
readyState: "loading"
|
|
1685
|
+
};
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
async listTabs(cdpUrl) {
|
|
1689
|
+
return (await getJson(`${cdpUrl}/json/list`)).filter((target) => target.type === "page" && target.webSocketDebuggerUrl).map((target) => ({
|
|
1690
|
+
id: target.id,
|
|
1691
|
+
url: target.url ?? "",
|
|
1692
|
+
title: target.title ?? "",
|
|
1693
|
+
targetWsUrl: target.webSocketDebuggerUrl
|
|
1694
|
+
}));
|
|
1695
|
+
}
|
|
1696
|
+
async openTab(cdpUrl) {
|
|
1697
|
+
const endpoint = `${cdpUrl}/json/new?${encodeURIComponent("about:blank")}`;
|
|
1698
|
+
let created;
|
|
1699
|
+
for (const method of ["PUT", "GET"]) try {
|
|
1700
|
+
const response = await fetch(endpoint, {
|
|
1701
|
+
method,
|
|
1702
|
+
signal: AbortSignal.timeout(5e3)
|
|
1703
|
+
});
|
|
1704
|
+
if (!response.ok) continue;
|
|
1705
|
+
created = await response.json();
|
|
1706
|
+
if (created.webSocketDebuggerUrl) break;
|
|
1707
|
+
} catch {}
|
|
1708
|
+
if (!created?.webSocketDebuggerUrl) throw new Error(`Unable to open a new tab via ${cdpUrl}/json/new`);
|
|
1709
|
+
const targetWsUrl = created.webSocketDebuggerUrl;
|
|
1710
|
+
const host = new URL(cdpUrl).host;
|
|
1711
|
+
let userAgent;
|
|
1712
|
+
for (const [wsUrl, ua] of this.userAgentOverrides) if (new URL(wsUrl).host === host) {
|
|
1713
|
+
userAgent = ua;
|
|
1714
|
+
break;
|
|
1715
|
+
}
|
|
1716
|
+
const appliedUa = await initTarget(targetWsUrl, userAgent);
|
|
1717
|
+
if (appliedUa) this.userAgentOverrides.set(targetWsUrl, appliedUa);
|
|
1718
|
+
return {
|
|
1719
|
+
id: created.id,
|
|
1720
|
+
url: created.url ?? "about:blank",
|
|
1721
|
+
title: created.title ?? "",
|
|
1722
|
+
targetWsUrl
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
async activateTab(cdpUrl, tabId) {
|
|
1726
|
+
if (!(await fetch(`${cdpUrl}/json/activate/${encodeURIComponent(tabId)}`, { signal: AbortSignal.timeout(5e3) })).ok) throw new Error(`Tab not found: ${tabId}`);
|
|
1727
|
+
}
|
|
1728
|
+
async closeTab(cdpUrl, tabId) {
|
|
1729
|
+
const tab = (await this.listTabs(cdpUrl)).find((candidate) => candidate.id === tabId);
|
|
1730
|
+
if (!tab) throw new Error(`Tab not found: ${tabId}`);
|
|
1731
|
+
this.dropConnection(tab.targetWsUrl);
|
|
1732
|
+
this.userAgentOverrides.delete(tab.targetWsUrl);
|
|
1733
|
+
const response = await fetch(`${cdpUrl}/json/close/${encodeURIComponent(tabId)}`, { signal: AbortSignal.timeout(5e3) });
|
|
1734
|
+
if (!response.ok) throw new Error(`Unable to close tab ${tabId}: HTTP ${response.status}`);
|
|
1735
|
+
}
|
|
1391
1736
|
async getContent(targetWsUrl, options) {
|
|
1392
1737
|
if (options.mode === "summary") return await this.getSummaryContent(targetWsUrl, options);
|
|
1393
1738
|
if (options.mode === "a11y") {
|
|
@@ -1773,6 +2118,10 @@ var ChromeCdpBrowserController = class {
|
|
|
1773
2118
|
const limit = options.limit ?? 50;
|
|
1774
2119
|
const scopeSelector = options.selector;
|
|
1775
2120
|
const roleFilter = options.roles ? new Set(options.roles) : null;
|
|
2121
|
+
const hrefFilter = options.href ? String(options.href).toLowerCase() : '';
|
|
2122
|
+
const textFilter = options.text
|
|
2123
|
+
? String(options.text).replace(/\\s+/g, ' ').trim().toLowerCase()
|
|
2124
|
+
: '';
|
|
1776
2125
|
const candidateSelector = [
|
|
1777
2126
|
'a[href]',
|
|
1778
2127
|
'button',
|
|
@@ -2044,6 +2393,7 @@ var ChromeCdpBrowserController = class {
|
|
|
2044
2393
|
}
|
|
2045
2394
|
|
|
2046
2395
|
const results = [];
|
|
2396
|
+
const crossOriginFrames = [];
|
|
2047
2397
|
let totalFound = 0;
|
|
2048
2398
|
|
|
2049
2399
|
function maybeRecordElement(root, path, el) {
|
|
@@ -2061,6 +2411,25 @@ var ChromeCdpBrowserController = class {
|
|
|
2061
2411
|
const visible = isVisible(el);
|
|
2062
2412
|
if (visibleOnly && !visible) return;
|
|
2063
2413
|
|
|
2414
|
+
if (hrefFilter) {
|
|
2415
|
+
const href = typeof el.href === 'string' ? el.href : el.getAttribute('href');
|
|
2416
|
+
if (!href || !String(href).toLowerCase().includes(hrefFilter)) return;
|
|
2417
|
+
}
|
|
2418
|
+
if (textFilter) {
|
|
2419
|
+
const haystack = [
|
|
2420
|
+
el.innerText || el.textContent || '',
|
|
2421
|
+
el.getAttribute('aria-label'),
|
|
2422
|
+
el.getAttribute('placeholder'),
|
|
2423
|
+
el.getAttribute('title'),
|
|
2424
|
+
typeof el.value === 'string' ? el.value : '',
|
|
2425
|
+
]
|
|
2426
|
+
.filter(Boolean)
|
|
2427
|
+
.join(' ')
|
|
2428
|
+
.replace(/\\s+/g, ' ')
|
|
2429
|
+
.toLowerCase();
|
|
2430
|
+
if (!haystack.includes(textFilter)) return;
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2064
2433
|
totalFound += 1;
|
|
2065
2434
|
if (results.length >= limit) return;
|
|
2066
2435
|
|
|
@@ -2104,12 +2473,21 @@ var ChromeCdpBrowserController = class {
|
|
|
2104
2473
|
}
|
|
2105
2474
|
|
|
2106
2475
|
if (current.tagName && current.tagName.toLowerCase() === 'iframe') {
|
|
2476
|
+
let frameDocument = null;
|
|
2107
2477
|
try {
|
|
2108
|
-
|
|
2109
|
-
walkRoot(current.contentDocument, path.concat(buildSelector(root, current)));
|
|
2110
|
-
}
|
|
2478
|
+
frameDocument = current.contentDocument;
|
|
2111
2479
|
} catch {
|
|
2112
|
-
|
|
2480
|
+
frameDocument = null;
|
|
2481
|
+
}
|
|
2482
|
+
if (frameDocument) {
|
|
2483
|
+
walkRoot(frameDocument, path.concat(buildSelector(root, current)));
|
|
2484
|
+
} else if (crossOriginFrames.length < 20 && isVisible(current)) {
|
|
2485
|
+
// Cross-origin: not walkable from here, report it so callers can pass it as frame.
|
|
2486
|
+
const info = { selector: composeLocator(path, buildSelector(root, current)) };
|
|
2487
|
+
const title = current.getAttribute('title');
|
|
2488
|
+
if (title) info.title = title.slice(0, 80);
|
|
2489
|
+
if (current.src) info.src = String(current.src).slice(0, 200);
|
|
2490
|
+
crossOriginFrames.push(info);
|
|
2113
2491
|
}
|
|
2114
2492
|
}
|
|
2115
2493
|
|
|
@@ -2120,7 +2498,9 @@ var ChromeCdpBrowserController = class {
|
|
|
2120
2498
|
}
|
|
2121
2499
|
|
|
2122
2500
|
walkRoot(document, []);
|
|
2123
|
-
|
|
2501
|
+
const output = { elements: results, totalFound, truncated: totalFound > results.length };
|
|
2502
|
+
if (crossOriginFrames.length) output.crossOriginFrames = crossOriginFrames;
|
|
2503
|
+
return output;
|
|
2124
2504
|
`);
|
|
2125
2505
|
const emptyResult = {
|
|
2126
2506
|
elements: [],
|
|
@@ -2134,12 +2514,31 @@ var ChromeCdpBrowserController = class {
|
|
|
2134
2514
|
};
|
|
2135
2515
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
2136
2516
|
await this.ensureEnabled(targetWsUrl);
|
|
2137
|
-
const
|
|
2517
|
+
const evaluate = async (client) => extract(await evaluateOnPage(client, {
|
|
2138
2518
|
expression,
|
|
2139
2519
|
returnByValue: true,
|
|
2140
2520
|
awaitPromise: true
|
|
2141
|
-
});
|
|
2142
|
-
return
|
|
2521
|
+
}));
|
|
2522
|
+
if (!options.frame) return await evaluate(conn);
|
|
2523
|
+
const scope = await this.openFrameScope(conn, targetWsUrl, options.frame);
|
|
2524
|
+
try {
|
|
2525
|
+
const inner = await evaluate(scope.client);
|
|
2526
|
+
const withPrefix = (selector) => `${options.frame} >>> ${selector}`;
|
|
2527
|
+
return {
|
|
2528
|
+
...inner,
|
|
2529
|
+
elements: inner.elements.map((element) => ({
|
|
2530
|
+
...element,
|
|
2531
|
+
selector: withPrefix(element.selector),
|
|
2532
|
+
fallbackSelectors: element.fallbackSelectors?.map(withPrefix)
|
|
2533
|
+
})),
|
|
2534
|
+
crossOriginFrames: inner.crossOriginFrames?.map((frame) => ({
|
|
2535
|
+
...frame,
|
|
2536
|
+
selector: withPrefix(frame.selector)
|
|
2537
|
+
}))
|
|
2538
|
+
};
|
|
2539
|
+
} finally {
|
|
2540
|
+
scope.release();
|
|
2541
|
+
}
|
|
2143
2542
|
});
|
|
2144
2543
|
}
|
|
2145
2544
|
/**
|
|
@@ -2153,22 +2552,33 @@ var ChromeCdpBrowserController = class {
|
|
|
2153
2552
|
* Returns silently on failure — never blocks navigation.
|
|
2154
2553
|
*/
|
|
2155
2554
|
async dismissCookieBanner(targetWsUrl) {
|
|
2555
|
+
const notes = /* @__PURE__ */ new Set();
|
|
2156
2556
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2157
2557
|
try {
|
|
2158
2558
|
const domResult = await this.dismissViaDom(targetWsUrl);
|
|
2159
2559
|
if (domResult.dismissed) return domResult;
|
|
2160
|
-
} catch {
|
|
2560
|
+
} catch (error) {
|
|
2561
|
+
notes.add(`dom: ${error.message}`);
|
|
2562
|
+
}
|
|
2161
2563
|
try {
|
|
2162
2564
|
const iframeResult = await this.dismissViaIframe(targetWsUrl);
|
|
2163
2565
|
if (iframeResult.dismissed) return iframeResult;
|
|
2164
|
-
|
|
2566
|
+
if (iframeResult.detail) notes.add(iframeResult.detail);
|
|
2567
|
+
} catch (error) {
|
|
2568
|
+
notes.add(`iframe: ${error.message}`);
|
|
2569
|
+
}
|
|
2165
2570
|
try {
|
|
2166
2571
|
const a11yResult = await this.dismissViaA11y(targetWsUrl);
|
|
2167
2572
|
if (a11yResult.dismissed) return a11yResult;
|
|
2168
|
-
} catch {
|
|
2573
|
+
} catch (error) {
|
|
2574
|
+
notes.add(`a11y: ${error.message}`);
|
|
2575
|
+
}
|
|
2169
2576
|
if (attempt === 0) await new Promise((resolve) => setTimeout(resolve, 800));
|
|
2170
2577
|
}
|
|
2171
|
-
return {
|
|
2578
|
+
return {
|
|
2579
|
+
dismissed: false,
|
|
2580
|
+
detail: notes.size ? [...notes].join("; ") : "no consent banner or accept button found (page, consent iframes, a11y tree)"
|
|
2581
|
+
};
|
|
2172
2582
|
}
|
|
2173
2583
|
async dismissViaDom(targetWsUrl) {
|
|
2174
2584
|
const expression = `(() => {
|
|
@@ -2423,7 +2833,42 @@ var ChromeCdpBrowserController = class {
|
|
|
2423
2833
|
* These banners render inside an iframe that needs to be accessed separately.
|
|
2424
2834
|
*/
|
|
2425
2835
|
async dismissViaIframe(targetWsUrl) {
|
|
2836
|
+
const innerSearch = `function (doc) {
|
|
2837
|
+
const innerSelectors = [
|
|
2838
|
+
// Sourcepoint specific
|
|
2839
|
+
'button.sp_choice_type_11', // Accept all
|
|
2840
|
+
'button.sp_choice_type_ACCEPT_ALL', // Accept all variant
|
|
2841
|
+
'button.sp_choice_type_SAVE_AND_EXIT', // Save & exit
|
|
2842
|
+
'.priv-save-btn', // Sourcepoint frame
|
|
2843
|
+
'.message-button', // Generic Sourcepoint
|
|
2844
|
+
// Title-based
|
|
2845
|
+
'button[title*="Accept"]', 'button[title*="accept"]',
|
|
2846
|
+
'button[title*="Agree"]', 'button[title*="agree"]',
|
|
2847
|
+
'button[title*="Allow"]',
|
|
2848
|
+
'button[title*="Akzeptieren"]', 'button[title*="akzeptieren"]',
|
|
2849
|
+
'button[title*="Zustimmen"]',
|
|
2850
|
+
];
|
|
2851
|
+
for (const innerSel of innerSelectors) {
|
|
2852
|
+
const btn = doc.querySelector(innerSel);
|
|
2853
|
+
if (btn && btn.offsetWidth > 0) {
|
|
2854
|
+
btn.click();
|
|
2855
|
+
return { method: 'iframe', detail: innerSel };
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
// Text-based fallback inside the iframe
|
|
2859
|
+
const buttons = doc.querySelectorAll('button, [role="button"]');
|
|
2860
|
+
for (const btn of buttons) {
|
|
2861
|
+
const text = (btn.textContent || '').trim();
|
|
2862
|
+
if (text.length > 60) continue;
|
|
2863
|
+
if (/accept|agree|allow|akzeptieren|zustimmen|einwilligen|accepter|aceptar/i.test(text)) {
|
|
2864
|
+
btn.click();
|
|
2865
|
+
return { method: 'iframe-text', detail: text };
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
return null;
|
|
2869
|
+
}`;
|
|
2426
2870
|
const expression = `(() => {
|
|
2871
|
+
const clickAccept = ${innerSearch};
|
|
2427
2872
|
// Known consent iframe selectors
|
|
2428
2873
|
const iframeSelectors = [
|
|
2429
2874
|
'iframe[id^="sp_message_iframe"]', // Sourcepoint (dynamic ID suffix)
|
|
@@ -2444,62 +2889,69 @@ var ChromeCdpBrowserController = class {
|
|
|
2444
2889
|
'iframe[src*="consentmanager"]',
|
|
2445
2890
|
];
|
|
2446
2891
|
|
|
2892
|
+
// Cross-origin consent frames are handed back so the caller can attach to them.
|
|
2893
|
+
const crossOrigin = [];
|
|
2447
2894
|
for (const sel of iframeSelectors) {
|
|
2895
|
+
const iframe = document.querySelector(sel);
|
|
2896
|
+
if (!iframe) continue;
|
|
2897
|
+
let doc = null;
|
|
2448
2898
|
try {
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
'.priv-save-btn', // Sourcepoint frame
|
|
2461
|
-
'.message-button', // Generic Sourcepoint
|
|
2462
|
-
// Title-based
|
|
2463
|
-
'button[title*="Accept"]', 'button[title*="accept"]',
|
|
2464
|
-
'button[title*="Agree"]', 'button[title*="agree"]',
|
|
2465
|
-
'button[title*="Allow"]',
|
|
2466
|
-
'button[title*="Akzeptieren"]', 'button[title*="akzeptieren"]',
|
|
2467
|
-
'button[title*="Zustimmen"]',
|
|
2468
|
-
];
|
|
2469
|
-
for (const innerSel of innerSelectors) {
|
|
2470
|
-
const btn = doc.querySelector(innerSel);
|
|
2471
|
-
if (btn && btn.offsetWidth > 0) {
|
|
2472
|
-
btn.click();
|
|
2473
|
-
return JSON.stringify({ dismissed: true, method: 'iframe', detail: sel + ' > ' + innerSel });
|
|
2474
|
-
}
|
|
2475
|
-
}
|
|
2476
|
-
|
|
2477
|
-
// Text-based fallback inside iframe
|
|
2478
|
-
const buttons = doc.querySelectorAll('button, [role="button"]');
|
|
2479
|
-
for (const btn of buttons) {
|
|
2480
|
-
const text = (btn.textContent || '').trim();
|
|
2481
|
-
if (text.length > 60) continue;
|
|
2482
|
-
if (/accept|agree|allow|akzeptieren|zustimmen|einwilligen|accepter|aceptar/i.test(text)) {
|
|
2483
|
-
btn.click();
|
|
2484
|
-
return JSON.stringify({ dismissed: true, method: 'iframe-text', detail: text });
|
|
2485
|
-
}
|
|
2486
|
-
}
|
|
2487
|
-
} catch { /* cross-origin iframe — skip */ }
|
|
2899
|
+
doc = iframe.contentDocument || iframe.contentWindow?.document || null;
|
|
2900
|
+
} catch {
|
|
2901
|
+
doc = null;
|
|
2902
|
+
}
|
|
2903
|
+
if (!doc) {
|
|
2904
|
+
const locator = iframe.id ? '#' + CSS.escape(iframe.id) : sel;
|
|
2905
|
+
if (!crossOrigin.includes(locator)) crossOrigin.push(locator);
|
|
2906
|
+
continue;
|
|
2907
|
+
}
|
|
2908
|
+
const hit = clickAccept(doc);
|
|
2909
|
+
if (hit) return { dismissed: true, method: hit.method, detail: sel + ' > ' + hit.detail };
|
|
2488
2910
|
}
|
|
2489
|
-
|
|
2490
|
-
return JSON.stringify({ dismissed: false });
|
|
2911
|
+
return { dismissed: false, crossOrigin };
|
|
2491
2912
|
})()`;
|
|
2492
2913
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
2493
2914
|
await this.ensureEnabled(targetWsUrl);
|
|
2494
|
-
const
|
|
2915
|
+
const value = (await evaluateOnPage(conn, {
|
|
2495
2916
|
expression,
|
|
2496
2917
|
returnByValue: true
|
|
2497
|
-
})).result.value
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2918
|
+
})).result.value;
|
|
2919
|
+
if (!value) return { dismissed: false };
|
|
2920
|
+
if (value.dismissed) return {
|
|
2921
|
+
dismissed: true,
|
|
2922
|
+
method: value.method,
|
|
2923
|
+
detail: value.detail
|
|
2924
|
+
};
|
|
2925
|
+
const notes = [];
|
|
2926
|
+
for (const frame of value.crossOrigin ?? []) {
|
|
2927
|
+
let scope;
|
|
2928
|
+
try {
|
|
2929
|
+
scope = await this.openFrameScope(conn, targetWsUrl, frame);
|
|
2930
|
+
} catch (error) {
|
|
2931
|
+
notes.push(`consent iframe ${frame}: ${error.message}`);
|
|
2932
|
+
continue;
|
|
2933
|
+
}
|
|
2934
|
+
try {
|
|
2935
|
+
const hit = (await evaluateOnPage(scope.client, {
|
|
2936
|
+
expression: `(${innerSearch})(document)`,
|
|
2937
|
+
returnByValue: true
|
|
2938
|
+
})).result.value;
|
|
2939
|
+
if (hit) return {
|
|
2940
|
+
dismissed: true,
|
|
2941
|
+
method: hit.method,
|
|
2942
|
+
detail: `${frame} >>> ${hit.detail}`
|
|
2943
|
+
};
|
|
2944
|
+
notes.push(`consent iframe ${frame}: no accept button found`);
|
|
2945
|
+
} catch (error) {
|
|
2946
|
+
notes.push(`consent iframe ${frame}: ${error.message}`);
|
|
2947
|
+
} finally {
|
|
2948
|
+
scope.release();
|
|
2949
|
+
}
|
|
2502
2950
|
}
|
|
2951
|
+
return notes.length ? {
|
|
2952
|
+
dismissed: false,
|
|
2953
|
+
detail: notes.join("; ")
|
|
2954
|
+
} : { dismissed: false };
|
|
2503
2955
|
});
|
|
2504
2956
|
}
|
|
2505
2957
|
/**
|
|
@@ -2696,6 +3148,64 @@ var MockBrowserController = class {
|
|
|
2696
3148
|
async getCurrentUrl(cdpUrl) {
|
|
2697
3149
|
return this.pages.get(cdpUrl)?.url ?? "about:blank";
|
|
2698
3150
|
}
|
|
3151
|
+
tabs = /* @__PURE__ */ new Map();
|
|
3152
|
+
tabCounter = 0;
|
|
3153
|
+
/** Tab ids are the page keys (targetWsUrl); the initial page is the cdpUrl itself. */
|
|
3154
|
+
tabIds(cdpUrl) {
|
|
3155
|
+
let ids = this.tabs.get(cdpUrl);
|
|
3156
|
+
if (!ids) {
|
|
3157
|
+
ids = this.pages.has(cdpUrl) ? [cdpUrl] : [];
|
|
3158
|
+
this.tabs.set(cdpUrl, ids);
|
|
3159
|
+
}
|
|
3160
|
+
return ids;
|
|
3161
|
+
}
|
|
3162
|
+
async getPageState(targetWsUrl) {
|
|
3163
|
+
const page = this.pages.get(targetWsUrl);
|
|
3164
|
+
if (!page) throw new Error("mock page missing");
|
|
3165
|
+
return {
|
|
3166
|
+
url: page.url,
|
|
3167
|
+
title: page.title,
|
|
3168
|
+
readyState: "complete"
|
|
3169
|
+
};
|
|
3170
|
+
}
|
|
3171
|
+
async listTabs(cdpUrl) {
|
|
3172
|
+
return this.tabIds(cdpUrl).map((id) => {
|
|
3173
|
+
const page = this.pages.get(id);
|
|
3174
|
+
return {
|
|
3175
|
+
id,
|
|
3176
|
+
url: page?.url ?? "about:blank",
|
|
3177
|
+
title: page?.title ?? "",
|
|
3178
|
+
targetWsUrl: id
|
|
3179
|
+
};
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
async openTab(cdpUrl) {
|
|
3183
|
+
this.tabCounter += 1;
|
|
3184
|
+
const id = `${cdpUrl}/tab-${this.tabCounter}`;
|
|
3185
|
+
this.pages.set(id, {
|
|
3186
|
+
url: "about:blank",
|
|
3187
|
+
title: "about:blank",
|
|
3188
|
+
text: "",
|
|
3189
|
+
html: "<html><body></body></html>"
|
|
3190
|
+
});
|
|
3191
|
+
this.tabIds(cdpUrl).push(id);
|
|
3192
|
+
return {
|
|
3193
|
+
id,
|
|
3194
|
+
url: "about:blank",
|
|
3195
|
+
title: "about:blank",
|
|
3196
|
+
targetWsUrl: id
|
|
3197
|
+
};
|
|
3198
|
+
}
|
|
3199
|
+
async activateTab(cdpUrl, tabId) {
|
|
3200
|
+
if (!this.tabIds(cdpUrl).includes(tabId)) throw new Error(`Tab not found: ${tabId}`);
|
|
3201
|
+
}
|
|
3202
|
+
async closeTab(cdpUrl, tabId) {
|
|
3203
|
+
const ids = this.tabIds(cdpUrl);
|
|
3204
|
+
const index = ids.indexOf(tabId);
|
|
3205
|
+
if (index === -1) throw new Error(`Tab not found: ${tabId}`);
|
|
3206
|
+
ids.splice(index, 1);
|
|
3207
|
+
this.pages.delete(tabId);
|
|
3208
|
+
}
|
|
2699
3209
|
async getContent(cdpUrl, options) {
|
|
2700
3210
|
const page = this.pages.get(cdpUrl);
|
|
2701
3211
|
if (!page) throw new Error("mock page missing");
|
|
@@ -2762,7 +3272,8 @@ const InteractPayloadSchema = z.object({
|
|
|
2762
3272
|
value: z.string().optional(),
|
|
2763
3273
|
scrollX: z.number().optional(),
|
|
2764
3274
|
scrollY: z.number().optional(),
|
|
2765
|
-
timeoutMs: z.number().optional()
|
|
3275
|
+
timeoutMs: z.number().optional(),
|
|
3276
|
+
frame: z.string().optional()
|
|
2766
3277
|
});
|
|
2767
3278
|
const SessionStatusSchema = z.enum([
|
|
2768
3279
|
"starting",
|
|
@@ -3153,6 +3664,68 @@ var SessionManager = class {
|
|
|
3153
3664
|
const record = await this.ensureSession(sessionId);
|
|
3154
3665
|
return await this.browser.getCurrentUrl(record.targetWsUrl);
|
|
3155
3666
|
}
|
|
3667
|
+
async getPageState(sessionId) {
|
|
3668
|
+
const record = await this.ensureSession(sessionId);
|
|
3669
|
+
return await this.browser.getPageState(record.targetWsUrl);
|
|
3670
|
+
}
|
|
3671
|
+
async listTabs(sessionId) {
|
|
3672
|
+
const record = await this.ensureSession(sessionId);
|
|
3673
|
+
return (await this.browser.listTabs(record.cdpUrl)).map((tab) => ({
|
|
3674
|
+
...tab,
|
|
3675
|
+
active: tab.targetWsUrl === record.targetWsUrl
|
|
3676
|
+
}));
|
|
3677
|
+
}
|
|
3678
|
+
/** Points the session at another tab; later commands run there. */
|
|
3679
|
+
async switchTab(sessionId, tabId) {
|
|
3680
|
+
const record = await this.ensureSession(sessionId);
|
|
3681
|
+
const tab = (await this.browser.listTabs(record.cdpUrl)).find((t) => t.id === tabId);
|
|
3682
|
+
if (!tab) throw new Error(`Tab not found: ${tabId}`);
|
|
3683
|
+
await this.browser.activateTab(record.cdpUrl, tab.id);
|
|
3684
|
+
this.store.save({
|
|
3685
|
+
...record,
|
|
3686
|
+
targetWsUrl: tab.targetWsUrl,
|
|
3687
|
+
lastUrl: tab.url || record.lastUrl
|
|
3688
|
+
});
|
|
3689
|
+
this.recordEvent(sessionId, "lifecycle", "info", `Switched to tab ${tab.id}`);
|
|
3690
|
+
return tab;
|
|
3691
|
+
}
|
|
3692
|
+
async newTab(sessionId) {
|
|
3693
|
+
const record = await this.ensureSession(sessionId);
|
|
3694
|
+
const tab = await this.browser.openTab(record.cdpUrl);
|
|
3695
|
+
await this.browser.activateTab(record.cdpUrl, tab.id);
|
|
3696
|
+
this.store.save({
|
|
3697
|
+
...record,
|
|
3698
|
+
targetWsUrl: tab.targetWsUrl,
|
|
3699
|
+
lastUrl: tab.url
|
|
3700
|
+
});
|
|
3701
|
+
this.recordEvent(sessionId, "lifecycle", "info", `Opened tab ${tab.id}`);
|
|
3702
|
+
return tab;
|
|
3703
|
+
}
|
|
3704
|
+
/** Closes a tab; closing the active one moves the session to a remaining tab. */
|
|
3705
|
+
async closeTab(sessionId, tabId) {
|
|
3706
|
+
const record = await this.ensureSession(sessionId);
|
|
3707
|
+
const tabs = await this.browser.listTabs(record.cdpUrl);
|
|
3708
|
+
const target = tabs.find((t) => t.id === tabId);
|
|
3709
|
+
if (!target) throw new Error(`Tab not found: ${tabId}`);
|
|
3710
|
+
const remaining = tabs.filter((t) => t.id !== tabId);
|
|
3711
|
+
if (remaining.length === 0) throw new Error("Refusing to close the last tab; use agent stop to end the session");
|
|
3712
|
+
await this.browser.closeTab(record.cdpUrl, tabId);
|
|
3713
|
+
let active = tabs.find((t) => t.targetWsUrl === record.targetWsUrl) ?? remaining[0];
|
|
3714
|
+
if (target.targetWsUrl === record.targetWsUrl) {
|
|
3715
|
+
active = remaining[remaining.length - 1];
|
|
3716
|
+
await this.browser.activateTab(record.cdpUrl, active.id);
|
|
3717
|
+
this.store.save({
|
|
3718
|
+
...record,
|
|
3719
|
+
targetWsUrl: active.targetWsUrl,
|
|
3720
|
+
lastUrl: active.url
|
|
3721
|
+
});
|
|
3722
|
+
}
|
|
3723
|
+
this.recordEvent(sessionId, "lifecycle", "info", `Closed tab ${tabId}`);
|
|
3724
|
+
return {
|
|
3725
|
+
closed: tabId,
|
|
3726
|
+
active
|
|
3727
|
+
};
|
|
3728
|
+
}
|
|
3156
3729
|
async dismissCookieBanner(sessionId) {
|
|
3157
3730
|
const record = await this.ensureSession(sessionId);
|
|
3158
3731
|
const result = await this.browser.dismissCookieBanner(record.targetWsUrl);
|
|
@@ -3437,6 +4010,21 @@ var ControlApi = class {
|
|
|
3437
4010
|
async getCurrentUrl(sessionId) {
|
|
3438
4011
|
return await this.sessions.getCurrentUrl(sessionId);
|
|
3439
4012
|
}
|
|
4013
|
+
async getPageState(sessionId) {
|
|
4014
|
+
return await this.sessions.getPageState(sessionId);
|
|
4015
|
+
}
|
|
4016
|
+
async listTabs(sessionId) {
|
|
4017
|
+
return await this.sessions.listTabs(sessionId);
|
|
4018
|
+
}
|
|
4019
|
+
async switchTab(sessionId, tabId) {
|
|
4020
|
+
return await this.sessions.switchTab(sessionId, tabId);
|
|
4021
|
+
}
|
|
4022
|
+
async newTab(sessionId) {
|
|
4023
|
+
return await this.sessions.newTab(sessionId);
|
|
4024
|
+
}
|
|
4025
|
+
async closeTab(sessionId, tabId) {
|
|
4026
|
+
return await this.sessions.closeTab(sessionId, tabId);
|
|
4027
|
+
}
|
|
3440
4028
|
listEvents(sessionId, limit = 100) {
|
|
3441
4029
|
return { events: this.sessions.listEvents(sessionId, limit) };
|
|
3442
4030
|
}
|
|
@@ -4580,7 +5168,10 @@ var AgenticBrowserCore = class {
|
|
|
4580
5168
|
roles: input.roles,
|
|
4581
5169
|
visibleOnly: input.visibleOnly,
|
|
4582
5170
|
limit: input.limit,
|
|
4583
|
-
selector: input.selector
|
|
5171
|
+
selector: input.selector,
|
|
5172
|
+
href: input.href,
|
|
5173
|
+
text: input.text,
|
|
5174
|
+
frame: input.frame
|
|
4584
5175
|
});
|
|
4585
5176
|
}
|
|
4586
5177
|
async dismissCookieBanner(sessionId) {
|
|
@@ -4592,6 +5183,21 @@ var AgenticBrowserCore = class {
|
|
|
4592
5183
|
async getCurrentUrl(sessionId) {
|
|
4593
5184
|
return await this.api.getCurrentUrl(sessionId);
|
|
4594
5185
|
}
|
|
5186
|
+
async getPageState(sessionId) {
|
|
5187
|
+
return await this.api.getPageState(sessionId);
|
|
5188
|
+
}
|
|
5189
|
+
async listTabs(sessionId) {
|
|
5190
|
+
return await this.api.listTabs(sessionId);
|
|
5191
|
+
}
|
|
5192
|
+
async switchTab(sessionId, tabId) {
|
|
5193
|
+
return await this.api.switchTab(sessionId, tabId);
|
|
5194
|
+
}
|
|
5195
|
+
async newTab(sessionId) {
|
|
5196
|
+
return await this.api.newTab(sessionId);
|
|
5197
|
+
}
|
|
5198
|
+
async closeTab(sessionId, tabId) {
|
|
5199
|
+
return await this.api.closeTab(sessionId, tabId);
|
|
5200
|
+
}
|
|
4595
5201
|
async restartSession(sessionId) {
|
|
4596
5202
|
return await this.api.restartSession(sessionId);
|
|
4597
5203
|
}
|