agentic-browser 2.6.2 → 2.7.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/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-CfQ0xGAm.mjs} +698 -93
- 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();
|
|
@@ -1348,11 +1626,17 @@ 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
|
+
return typeof __agenticResult === 'string' ? __agenticResult : JSON.stringify(__agenticResult);
|
|
1635
|
+
})()` : buildExpression(scoped),
|
|
1636
|
+
returnByValue: true,
|
|
1637
|
+
awaitPromise: true
|
|
1638
|
+
})).result.value ?? "";
|
|
1639
|
+
});
|
|
1356
1640
|
}, "connection");
|
|
1357
1641
|
}
|
|
1358
1642
|
async screenshot(targetWsUrl, options) {
|
|
@@ -1388,6 +1672,66 @@ var ChromeCdpBrowserController = class {
|
|
|
1388
1672
|
})).result.value ?? "";
|
|
1389
1673
|
});
|
|
1390
1674
|
}
|
|
1675
|
+
async getPageState(targetWsUrl) {
|
|
1676
|
+
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1677
|
+
return (await evaluateOnPage(conn, {
|
|
1678
|
+
expression: "({ url: location.href, title: document.title, readyState: document.readyState })",
|
|
1679
|
+
returnByValue: true
|
|
1680
|
+
})).result.value ?? {
|
|
1681
|
+
url: "",
|
|
1682
|
+
title: "",
|
|
1683
|
+
readyState: "loading"
|
|
1684
|
+
};
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
async listTabs(cdpUrl) {
|
|
1688
|
+
return (await getJson(`${cdpUrl}/json/list`)).filter((target) => target.type === "page" && target.webSocketDebuggerUrl).map((target) => ({
|
|
1689
|
+
id: target.id,
|
|
1690
|
+
url: target.url ?? "",
|
|
1691
|
+
title: target.title ?? "",
|
|
1692
|
+
targetWsUrl: target.webSocketDebuggerUrl
|
|
1693
|
+
}));
|
|
1694
|
+
}
|
|
1695
|
+
async openTab(cdpUrl) {
|
|
1696
|
+
const endpoint = `${cdpUrl}/json/new?${encodeURIComponent("about:blank")}`;
|
|
1697
|
+
let created;
|
|
1698
|
+
for (const method of ["PUT", "GET"]) try {
|
|
1699
|
+
const response = await fetch(endpoint, {
|
|
1700
|
+
method,
|
|
1701
|
+
signal: AbortSignal.timeout(5e3)
|
|
1702
|
+
});
|
|
1703
|
+
if (!response.ok) continue;
|
|
1704
|
+
created = await response.json();
|
|
1705
|
+
if (created.webSocketDebuggerUrl) break;
|
|
1706
|
+
} catch {}
|
|
1707
|
+
if (!created?.webSocketDebuggerUrl) throw new Error(`Unable to open a new tab via ${cdpUrl}/json/new`);
|
|
1708
|
+
const targetWsUrl = created.webSocketDebuggerUrl;
|
|
1709
|
+
const host = new URL(cdpUrl).host;
|
|
1710
|
+
let userAgent;
|
|
1711
|
+
for (const [wsUrl, ua] of this.userAgentOverrides) if (new URL(wsUrl).host === host) {
|
|
1712
|
+
userAgent = ua;
|
|
1713
|
+
break;
|
|
1714
|
+
}
|
|
1715
|
+
const appliedUa = await initTarget(targetWsUrl, userAgent);
|
|
1716
|
+
if (appliedUa) this.userAgentOverrides.set(targetWsUrl, appliedUa);
|
|
1717
|
+
return {
|
|
1718
|
+
id: created.id,
|
|
1719
|
+
url: created.url ?? "about:blank",
|
|
1720
|
+
title: created.title ?? "",
|
|
1721
|
+
targetWsUrl
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
async activateTab(cdpUrl, tabId) {
|
|
1725
|
+
if (!(await fetch(`${cdpUrl}/json/activate/${encodeURIComponent(tabId)}`, { signal: AbortSignal.timeout(5e3) })).ok) throw new Error(`Tab not found: ${tabId}`);
|
|
1726
|
+
}
|
|
1727
|
+
async closeTab(cdpUrl, tabId) {
|
|
1728
|
+
const tab = (await this.listTabs(cdpUrl)).find((candidate) => candidate.id === tabId);
|
|
1729
|
+
if (!tab) throw new Error(`Tab not found: ${tabId}`);
|
|
1730
|
+
this.dropConnection(tab.targetWsUrl);
|
|
1731
|
+
this.userAgentOverrides.delete(tab.targetWsUrl);
|
|
1732
|
+
const response = await fetch(`${cdpUrl}/json/close/${encodeURIComponent(tabId)}`, { signal: AbortSignal.timeout(5e3) });
|
|
1733
|
+
if (!response.ok) throw new Error(`Unable to close tab ${tabId}: HTTP ${response.status}`);
|
|
1734
|
+
}
|
|
1391
1735
|
async getContent(targetWsUrl, options) {
|
|
1392
1736
|
if (options.mode === "summary") return await this.getSummaryContent(targetWsUrl, options);
|
|
1393
1737
|
if (options.mode === "a11y") {
|
|
@@ -1773,6 +2117,10 @@ var ChromeCdpBrowserController = class {
|
|
|
1773
2117
|
const limit = options.limit ?? 50;
|
|
1774
2118
|
const scopeSelector = options.selector;
|
|
1775
2119
|
const roleFilter = options.roles ? new Set(options.roles) : null;
|
|
2120
|
+
const hrefFilter = options.href ? String(options.href).toLowerCase() : '';
|
|
2121
|
+
const textFilter = options.text
|
|
2122
|
+
? String(options.text).replace(/\\s+/g, ' ').trim().toLowerCase()
|
|
2123
|
+
: '';
|
|
1776
2124
|
const candidateSelector = [
|
|
1777
2125
|
'a[href]',
|
|
1778
2126
|
'button',
|
|
@@ -2044,6 +2392,7 @@ var ChromeCdpBrowserController = class {
|
|
|
2044
2392
|
}
|
|
2045
2393
|
|
|
2046
2394
|
const results = [];
|
|
2395
|
+
const crossOriginFrames = [];
|
|
2047
2396
|
let totalFound = 0;
|
|
2048
2397
|
|
|
2049
2398
|
function maybeRecordElement(root, path, el) {
|
|
@@ -2061,6 +2410,25 @@ var ChromeCdpBrowserController = class {
|
|
|
2061
2410
|
const visible = isVisible(el);
|
|
2062
2411
|
if (visibleOnly && !visible) return;
|
|
2063
2412
|
|
|
2413
|
+
if (hrefFilter) {
|
|
2414
|
+
const href = typeof el.href === 'string' ? el.href : el.getAttribute('href');
|
|
2415
|
+
if (!href || !String(href).toLowerCase().includes(hrefFilter)) return;
|
|
2416
|
+
}
|
|
2417
|
+
if (textFilter) {
|
|
2418
|
+
const haystack = [
|
|
2419
|
+
el.innerText || el.textContent || '',
|
|
2420
|
+
el.getAttribute('aria-label'),
|
|
2421
|
+
el.getAttribute('placeholder'),
|
|
2422
|
+
el.getAttribute('title'),
|
|
2423
|
+
typeof el.value === 'string' ? el.value : '',
|
|
2424
|
+
]
|
|
2425
|
+
.filter(Boolean)
|
|
2426
|
+
.join(' ')
|
|
2427
|
+
.replace(/\\s+/g, ' ')
|
|
2428
|
+
.toLowerCase();
|
|
2429
|
+
if (!haystack.includes(textFilter)) return;
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2064
2432
|
totalFound += 1;
|
|
2065
2433
|
if (results.length >= limit) return;
|
|
2066
2434
|
|
|
@@ -2104,12 +2472,21 @@ var ChromeCdpBrowserController = class {
|
|
|
2104
2472
|
}
|
|
2105
2473
|
|
|
2106
2474
|
if (current.tagName && current.tagName.toLowerCase() === 'iframe') {
|
|
2475
|
+
let frameDocument = null;
|
|
2107
2476
|
try {
|
|
2108
|
-
|
|
2109
|
-
walkRoot(current.contentDocument, path.concat(buildSelector(root, current)));
|
|
2110
|
-
}
|
|
2477
|
+
frameDocument = current.contentDocument;
|
|
2111
2478
|
} catch {
|
|
2112
|
-
|
|
2479
|
+
frameDocument = null;
|
|
2480
|
+
}
|
|
2481
|
+
if (frameDocument) {
|
|
2482
|
+
walkRoot(frameDocument, path.concat(buildSelector(root, current)));
|
|
2483
|
+
} else if (crossOriginFrames.length < 20 && isVisible(current)) {
|
|
2484
|
+
// Cross-origin: not walkable from here, report it so callers can pass it as frame.
|
|
2485
|
+
const info = { selector: composeLocator(path, buildSelector(root, current)) };
|
|
2486
|
+
const title = current.getAttribute('title');
|
|
2487
|
+
if (title) info.title = title.slice(0, 80);
|
|
2488
|
+
if (current.src) info.src = String(current.src).slice(0, 200);
|
|
2489
|
+
crossOriginFrames.push(info);
|
|
2113
2490
|
}
|
|
2114
2491
|
}
|
|
2115
2492
|
|
|
@@ -2120,7 +2497,9 @@ var ChromeCdpBrowserController = class {
|
|
|
2120
2497
|
}
|
|
2121
2498
|
|
|
2122
2499
|
walkRoot(document, []);
|
|
2123
|
-
|
|
2500
|
+
const output = { elements: results, totalFound, truncated: totalFound > results.length };
|
|
2501
|
+
if (crossOriginFrames.length) output.crossOriginFrames = crossOriginFrames;
|
|
2502
|
+
return output;
|
|
2124
2503
|
`);
|
|
2125
2504
|
const emptyResult = {
|
|
2126
2505
|
elements: [],
|
|
@@ -2134,12 +2513,31 @@ var ChromeCdpBrowserController = class {
|
|
|
2134
2513
|
};
|
|
2135
2514
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
2136
2515
|
await this.ensureEnabled(targetWsUrl);
|
|
2137
|
-
const
|
|
2516
|
+
const evaluate = async (client) => extract(await evaluateOnPage(client, {
|
|
2138
2517
|
expression,
|
|
2139
2518
|
returnByValue: true,
|
|
2140
2519
|
awaitPromise: true
|
|
2141
|
-
});
|
|
2142
|
-
return
|
|
2520
|
+
}));
|
|
2521
|
+
if (!options.frame) return await evaluate(conn);
|
|
2522
|
+
const scope = await this.openFrameScope(conn, targetWsUrl, options.frame);
|
|
2523
|
+
try {
|
|
2524
|
+
const inner = await evaluate(scope.client);
|
|
2525
|
+
const withPrefix = (selector) => `${options.frame} >>> ${selector}`;
|
|
2526
|
+
return {
|
|
2527
|
+
...inner,
|
|
2528
|
+
elements: inner.elements.map((element) => ({
|
|
2529
|
+
...element,
|
|
2530
|
+
selector: withPrefix(element.selector),
|
|
2531
|
+
fallbackSelectors: element.fallbackSelectors?.map(withPrefix)
|
|
2532
|
+
})),
|
|
2533
|
+
crossOriginFrames: inner.crossOriginFrames?.map((frame) => ({
|
|
2534
|
+
...frame,
|
|
2535
|
+
selector: withPrefix(frame.selector)
|
|
2536
|
+
}))
|
|
2537
|
+
};
|
|
2538
|
+
} finally {
|
|
2539
|
+
scope.release();
|
|
2540
|
+
}
|
|
2143
2541
|
});
|
|
2144
2542
|
}
|
|
2145
2543
|
/**
|
|
@@ -2153,22 +2551,33 @@ var ChromeCdpBrowserController = class {
|
|
|
2153
2551
|
* Returns silently on failure — never blocks navigation.
|
|
2154
2552
|
*/
|
|
2155
2553
|
async dismissCookieBanner(targetWsUrl) {
|
|
2554
|
+
const notes = /* @__PURE__ */ new Set();
|
|
2156
2555
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2157
2556
|
try {
|
|
2158
2557
|
const domResult = await this.dismissViaDom(targetWsUrl);
|
|
2159
2558
|
if (domResult.dismissed) return domResult;
|
|
2160
|
-
} catch {
|
|
2559
|
+
} catch (error) {
|
|
2560
|
+
notes.add(`dom: ${error.message}`);
|
|
2561
|
+
}
|
|
2161
2562
|
try {
|
|
2162
2563
|
const iframeResult = await this.dismissViaIframe(targetWsUrl);
|
|
2163
2564
|
if (iframeResult.dismissed) return iframeResult;
|
|
2164
|
-
|
|
2565
|
+
if (iframeResult.detail) notes.add(iframeResult.detail);
|
|
2566
|
+
} catch (error) {
|
|
2567
|
+
notes.add(`iframe: ${error.message}`);
|
|
2568
|
+
}
|
|
2165
2569
|
try {
|
|
2166
2570
|
const a11yResult = await this.dismissViaA11y(targetWsUrl);
|
|
2167
2571
|
if (a11yResult.dismissed) return a11yResult;
|
|
2168
|
-
} catch {
|
|
2572
|
+
} catch (error) {
|
|
2573
|
+
notes.add(`a11y: ${error.message}`);
|
|
2574
|
+
}
|
|
2169
2575
|
if (attempt === 0) await new Promise((resolve) => setTimeout(resolve, 800));
|
|
2170
2576
|
}
|
|
2171
|
-
return {
|
|
2577
|
+
return {
|
|
2578
|
+
dismissed: false,
|
|
2579
|
+
detail: notes.size ? [...notes].join("; ") : "no consent banner or accept button found (page, consent iframes, a11y tree)"
|
|
2580
|
+
};
|
|
2172
2581
|
}
|
|
2173
2582
|
async dismissViaDom(targetWsUrl) {
|
|
2174
2583
|
const expression = `(() => {
|
|
@@ -2423,7 +2832,42 @@ var ChromeCdpBrowserController = class {
|
|
|
2423
2832
|
* These banners render inside an iframe that needs to be accessed separately.
|
|
2424
2833
|
*/
|
|
2425
2834
|
async dismissViaIframe(targetWsUrl) {
|
|
2835
|
+
const innerSearch = `function (doc) {
|
|
2836
|
+
const innerSelectors = [
|
|
2837
|
+
// Sourcepoint specific
|
|
2838
|
+
'button.sp_choice_type_11', // Accept all
|
|
2839
|
+
'button.sp_choice_type_ACCEPT_ALL', // Accept all variant
|
|
2840
|
+
'button.sp_choice_type_SAVE_AND_EXIT', // Save & exit
|
|
2841
|
+
'.priv-save-btn', // Sourcepoint frame
|
|
2842
|
+
'.message-button', // Generic Sourcepoint
|
|
2843
|
+
// Title-based
|
|
2844
|
+
'button[title*="Accept"]', 'button[title*="accept"]',
|
|
2845
|
+
'button[title*="Agree"]', 'button[title*="agree"]',
|
|
2846
|
+
'button[title*="Allow"]',
|
|
2847
|
+
'button[title*="Akzeptieren"]', 'button[title*="akzeptieren"]',
|
|
2848
|
+
'button[title*="Zustimmen"]',
|
|
2849
|
+
];
|
|
2850
|
+
for (const innerSel of innerSelectors) {
|
|
2851
|
+
const btn = doc.querySelector(innerSel);
|
|
2852
|
+
if (btn && btn.offsetWidth > 0) {
|
|
2853
|
+
btn.click();
|
|
2854
|
+
return { method: 'iframe', detail: innerSel };
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
// Text-based fallback inside the iframe
|
|
2858
|
+
const buttons = doc.querySelectorAll('button, [role="button"]');
|
|
2859
|
+
for (const btn of buttons) {
|
|
2860
|
+
const text = (btn.textContent || '').trim();
|
|
2861
|
+
if (text.length > 60) continue;
|
|
2862
|
+
if (/accept|agree|allow|akzeptieren|zustimmen|einwilligen|accepter|aceptar/i.test(text)) {
|
|
2863
|
+
btn.click();
|
|
2864
|
+
return { method: 'iframe-text', detail: text };
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
return null;
|
|
2868
|
+
}`;
|
|
2426
2869
|
const expression = `(() => {
|
|
2870
|
+
const clickAccept = ${innerSearch};
|
|
2427
2871
|
// Known consent iframe selectors
|
|
2428
2872
|
const iframeSelectors = [
|
|
2429
2873
|
'iframe[id^="sp_message_iframe"]', // Sourcepoint (dynamic ID suffix)
|
|
@@ -2444,62 +2888,69 @@ var ChromeCdpBrowserController = class {
|
|
|
2444
2888
|
'iframe[src*="consentmanager"]',
|
|
2445
2889
|
];
|
|
2446
2890
|
|
|
2891
|
+
// Cross-origin consent frames are handed back so the caller can attach to them.
|
|
2892
|
+
const crossOrigin = [];
|
|
2447
2893
|
for (const sel of iframeSelectors) {
|
|
2894
|
+
const iframe = document.querySelector(sel);
|
|
2895
|
+
if (!iframe) continue;
|
|
2896
|
+
let doc = null;
|
|
2448
2897
|
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 */ }
|
|
2898
|
+
doc = iframe.contentDocument || iframe.contentWindow?.document || null;
|
|
2899
|
+
} catch {
|
|
2900
|
+
doc = null;
|
|
2901
|
+
}
|
|
2902
|
+
if (!doc) {
|
|
2903
|
+
const locator = iframe.id ? '#' + CSS.escape(iframe.id) : sel;
|
|
2904
|
+
if (!crossOrigin.includes(locator)) crossOrigin.push(locator);
|
|
2905
|
+
continue;
|
|
2906
|
+
}
|
|
2907
|
+
const hit = clickAccept(doc);
|
|
2908
|
+
if (hit) return { dismissed: true, method: hit.method, detail: sel + ' > ' + hit.detail };
|
|
2488
2909
|
}
|
|
2489
|
-
|
|
2490
|
-
return JSON.stringify({ dismissed: false });
|
|
2910
|
+
return { dismissed: false, crossOrigin };
|
|
2491
2911
|
})()`;
|
|
2492
2912
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
2493
2913
|
await this.ensureEnabled(targetWsUrl);
|
|
2494
|
-
const
|
|
2914
|
+
const value = (await evaluateOnPage(conn, {
|
|
2495
2915
|
expression,
|
|
2496
2916
|
returnByValue: true
|
|
2497
|
-
})).result.value
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2917
|
+
})).result.value;
|
|
2918
|
+
if (!value) return { dismissed: false };
|
|
2919
|
+
if (value.dismissed) return {
|
|
2920
|
+
dismissed: true,
|
|
2921
|
+
method: value.method,
|
|
2922
|
+
detail: value.detail
|
|
2923
|
+
};
|
|
2924
|
+
const notes = [];
|
|
2925
|
+
for (const frame of value.crossOrigin ?? []) {
|
|
2926
|
+
let scope;
|
|
2927
|
+
try {
|
|
2928
|
+
scope = await this.openFrameScope(conn, targetWsUrl, frame);
|
|
2929
|
+
} catch (error) {
|
|
2930
|
+
notes.push(`consent iframe ${frame}: ${error.message}`);
|
|
2931
|
+
continue;
|
|
2932
|
+
}
|
|
2933
|
+
try {
|
|
2934
|
+
const hit = (await evaluateOnPage(scope.client, {
|
|
2935
|
+
expression: `(${innerSearch})(document)`,
|
|
2936
|
+
returnByValue: true
|
|
2937
|
+
})).result.value;
|
|
2938
|
+
if (hit) return {
|
|
2939
|
+
dismissed: true,
|
|
2940
|
+
method: hit.method,
|
|
2941
|
+
detail: `${frame} >>> ${hit.detail}`
|
|
2942
|
+
};
|
|
2943
|
+
notes.push(`consent iframe ${frame}: no accept button found`);
|
|
2944
|
+
} catch (error) {
|
|
2945
|
+
notes.push(`consent iframe ${frame}: ${error.message}`);
|
|
2946
|
+
} finally {
|
|
2947
|
+
scope.release();
|
|
2948
|
+
}
|
|
2502
2949
|
}
|
|
2950
|
+
return notes.length ? {
|
|
2951
|
+
dismissed: false,
|
|
2952
|
+
detail: notes.join("; ")
|
|
2953
|
+
} : { dismissed: false };
|
|
2503
2954
|
});
|
|
2504
2955
|
}
|
|
2505
2956
|
/**
|
|
@@ -2696,6 +3147,64 @@ var MockBrowserController = class {
|
|
|
2696
3147
|
async getCurrentUrl(cdpUrl) {
|
|
2697
3148
|
return this.pages.get(cdpUrl)?.url ?? "about:blank";
|
|
2698
3149
|
}
|
|
3150
|
+
tabs = /* @__PURE__ */ new Map();
|
|
3151
|
+
tabCounter = 0;
|
|
3152
|
+
/** Tab ids are the page keys (targetWsUrl); the initial page is the cdpUrl itself. */
|
|
3153
|
+
tabIds(cdpUrl) {
|
|
3154
|
+
let ids = this.tabs.get(cdpUrl);
|
|
3155
|
+
if (!ids) {
|
|
3156
|
+
ids = this.pages.has(cdpUrl) ? [cdpUrl] : [];
|
|
3157
|
+
this.tabs.set(cdpUrl, ids);
|
|
3158
|
+
}
|
|
3159
|
+
return ids;
|
|
3160
|
+
}
|
|
3161
|
+
async getPageState(targetWsUrl) {
|
|
3162
|
+
const page = this.pages.get(targetWsUrl);
|
|
3163
|
+
if (!page) throw new Error("mock page missing");
|
|
3164
|
+
return {
|
|
3165
|
+
url: page.url,
|
|
3166
|
+
title: page.title,
|
|
3167
|
+
readyState: "complete"
|
|
3168
|
+
};
|
|
3169
|
+
}
|
|
3170
|
+
async listTabs(cdpUrl) {
|
|
3171
|
+
return this.tabIds(cdpUrl).map((id) => {
|
|
3172
|
+
const page = this.pages.get(id);
|
|
3173
|
+
return {
|
|
3174
|
+
id,
|
|
3175
|
+
url: page?.url ?? "about:blank",
|
|
3176
|
+
title: page?.title ?? "",
|
|
3177
|
+
targetWsUrl: id
|
|
3178
|
+
};
|
|
3179
|
+
});
|
|
3180
|
+
}
|
|
3181
|
+
async openTab(cdpUrl) {
|
|
3182
|
+
this.tabCounter += 1;
|
|
3183
|
+
const id = `${cdpUrl}/tab-${this.tabCounter}`;
|
|
3184
|
+
this.pages.set(id, {
|
|
3185
|
+
url: "about:blank",
|
|
3186
|
+
title: "about:blank",
|
|
3187
|
+
text: "",
|
|
3188
|
+
html: "<html><body></body></html>"
|
|
3189
|
+
});
|
|
3190
|
+
this.tabIds(cdpUrl).push(id);
|
|
3191
|
+
return {
|
|
3192
|
+
id,
|
|
3193
|
+
url: "about:blank",
|
|
3194
|
+
title: "about:blank",
|
|
3195
|
+
targetWsUrl: id
|
|
3196
|
+
};
|
|
3197
|
+
}
|
|
3198
|
+
async activateTab(cdpUrl, tabId) {
|
|
3199
|
+
if (!this.tabIds(cdpUrl).includes(tabId)) throw new Error(`Tab not found: ${tabId}`);
|
|
3200
|
+
}
|
|
3201
|
+
async closeTab(cdpUrl, tabId) {
|
|
3202
|
+
const ids = this.tabIds(cdpUrl);
|
|
3203
|
+
const index = ids.indexOf(tabId);
|
|
3204
|
+
if (index === -1) throw new Error(`Tab not found: ${tabId}`);
|
|
3205
|
+
ids.splice(index, 1);
|
|
3206
|
+
this.pages.delete(tabId);
|
|
3207
|
+
}
|
|
2699
3208
|
async getContent(cdpUrl, options) {
|
|
2700
3209
|
const page = this.pages.get(cdpUrl);
|
|
2701
3210
|
if (!page) throw new Error("mock page missing");
|
|
@@ -2762,7 +3271,8 @@ const InteractPayloadSchema = z.object({
|
|
|
2762
3271
|
value: z.string().optional(),
|
|
2763
3272
|
scrollX: z.number().optional(),
|
|
2764
3273
|
scrollY: z.number().optional(),
|
|
2765
|
-
timeoutMs: z.number().optional()
|
|
3274
|
+
timeoutMs: z.number().optional(),
|
|
3275
|
+
frame: z.string().optional()
|
|
2766
3276
|
});
|
|
2767
3277
|
const SessionStatusSchema = z.enum([
|
|
2768
3278
|
"starting",
|
|
@@ -3153,6 +3663,68 @@ var SessionManager = class {
|
|
|
3153
3663
|
const record = await this.ensureSession(sessionId);
|
|
3154
3664
|
return await this.browser.getCurrentUrl(record.targetWsUrl);
|
|
3155
3665
|
}
|
|
3666
|
+
async getPageState(sessionId) {
|
|
3667
|
+
const record = await this.ensureSession(sessionId);
|
|
3668
|
+
return await this.browser.getPageState(record.targetWsUrl);
|
|
3669
|
+
}
|
|
3670
|
+
async listTabs(sessionId) {
|
|
3671
|
+
const record = await this.ensureSession(sessionId);
|
|
3672
|
+
return (await this.browser.listTabs(record.cdpUrl)).map((tab) => ({
|
|
3673
|
+
...tab,
|
|
3674
|
+
active: tab.targetWsUrl === record.targetWsUrl
|
|
3675
|
+
}));
|
|
3676
|
+
}
|
|
3677
|
+
/** Points the session at another tab; later commands run there. */
|
|
3678
|
+
async switchTab(sessionId, tabId) {
|
|
3679
|
+
const record = await this.ensureSession(sessionId);
|
|
3680
|
+
const tab = (await this.browser.listTabs(record.cdpUrl)).find((t) => t.id === tabId);
|
|
3681
|
+
if (!tab) throw new Error(`Tab not found: ${tabId}`);
|
|
3682
|
+
await this.browser.activateTab(record.cdpUrl, tab.id);
|
|
3683
|
+
this.store.save({
|
|
3684
|
+
...record,
|
|
3685
|
+
targetWsUrl: tab.targetWsUrl,
|
|
3686
|
+
lastUrl: tab.url || record.lastUrl
|
|
3687
|
+
});
|
|
3688
|
+
this.recordEvent(sessionId, "lifecycle", "info", `Switched to tab ${tab.id}`);
|
|
3689
|
+
return tab;
|
|
3690
|
+
}
|
|
3691
|
+
async newTab(sessionId) {
|
|
3692
|
+
const record = await this.ensureSession(sessionId);
|
|
3693
|
+
const tab = await this.browser.openTab(record.cdpUrl);
|
|
3694
|
+
await this.browser.activateTab(record.cdpUrl, tab.id);
|
|
3695
|
+
this.store.save({
|
|
3696
|
+
...record,
|
|
3697
|
+
targetWsUrl: tab.targetWsUrl,
|
|
3698
|
+
lastUrl: tab.url
|
|
3699
|
+
});
|
|
3700
|
+
this.recordEvent(sessionId, "lifecycle", "info", `Opened tab ${tab.id}`);
|
|
3701
|
+
return tab;
|
|
3702
|
+
}
|
|
3703
|
+
/** Closes a tab; closing the active one moves the session to a remaining tab. */
|
|
3704
|
+
async closeTab(sessionId, tabId) {
|
|
3705
|
+
const record = await this.ensureSession(sessionId);
|
|
3706
|
+
const tabs = await this.browser.listTabs(record.cdpUrl);
|
|
3707
|
+
const target = tabs.find((t) => t.id === tabId);
|
|
3708
|
+
if (!target) throw new Error(`Tab not found: ${tabId}`);
|
|
3709
|
+
const remaining = tabs.filter((t) => t.id !== tabId);
|
|
3710
|
+
if (remaining.length === 0) throw new Error("Refusing to close the last tab; use agent stop to end the session");
|
|
3711
|
+
await this.browser.closeTab(record.cdpUrl, tabId);
|
|
3712
|
+
let active = tabs.find((t) => t.targetWsUrl === record.targetWsUrl) ?? remaining[0];
|
|
3713
|
+
if (target.targetWsUrl === record.targetWsUrl) {
|
|
3714
|
+
active = remaining[remaining.length - 1];
|
|
3715
|
+
await this.browser.activateTab(record.cdpUrl, active.id);
|
|
3716
|
+
this.store.save({
|
|
3717
|
+
...record,
|
|
3718
|
+
targetWsUrl: active.targetWsUrl,
|
|
3719
|
+
lastUrl: active.url
|
|
3720
|
+
});
|
|
3721
|
+
}
|
|
3722
|
+
this.recordEvent(sessionId, "lifecycle", "info", `Closed tab ${tabId}`);
|
|
3723
|
+
return {
|
|
3724
|
+
closed: tabId,
|
|
3725
|
+
active
|
|
3726
|
+
};
|
|
3727
|
+
}
|
|
3156
3728
|
async dismissCookieBanner(sessionId) {
|
|
3157
3729
|
const record = await this.ensureSession(sessionId);
|
|
3158
3730
|
const result = await this.browser.dismissCookieBanner(record.targetWsUrl);
|
|
@@ -3437,6 +4009,21 @@ var ControlApi = class {
|
|
|
3437
4009
|
async getCurrentUrl(sessionId) {
|
|
3438
4010
|
return await this.sessions.getCurrentUrl(sessionId);
|
|
3439
4011
|
}
|
|
4012
|
+
async getPageState(sessionId) {
|
|
4013
|
+
return await this.sessions.getPageState(sessionId);
|
|
4014
|
+
}
|
|
4015
|
+
async listTabs(sessionId) {
|
|
4016
|
+
return await this.sessions.listTabs(sessionId);
|
|
4017
|
+
}
|
|
4018
|
+
async switchTab(sessionId, tabId) {
|
|
4019
|
+
return await this.sessions.switchTab(sessionId, tabId);
|
|
4020
|
+
}
|
|
4021
|
+
async newTab(sessionId) {
|
|
4022
|
+
return await this.sessions.newTab(sessionId);
|
|
4023
|
+
}
|
|
4024
|
+
async closeTab(sessionId, tabId) {
|
|
4025
|
+
return await this.sessions.closeTab(sessionId, tabId);
|
|
4026
|
+
}
|
|
3440
4027
|
listEvents(sessionId, limit = 100) {
|
|
3441
4028
|
return { events: this.sessions.listEvents(sessionId, limit) };
|
|
3442
4029
|
}
|
|
@@ -4580,7 +5167,10 @@ var AgenticBrowserCore = class {
|
|
|
4580
5167
|
roles: input.roles,
|
|
4581
5168
|
visibleOnly: input.visibleOnly,
|
|
4582
5169
|
limit: input.limit,
|
|
4583
|
-
selector: input.selector
|
|
5170
|
+
selector: input.selector,
|
|
5171
|
+
href: input.href,
|
|
5172
|
+
text: input.text,
|
|
5173
|
+
frame: input.frame
|
|
4584
5174
|
});
|
|
4585
5175
|
}
|
|
4586
5176
|
async dismissCookieBanner(sessionId) {
|
|
@@ -4592,6 +5182,21 @@ var AgenticBrowserCore = class {
|
|
|
4592
5182
|
async getCurrentUrl(sessionId) {
|
|
4593
5183
|
return await this.api.getCurrentUrl(sessionId);
|
|
4594
5184
|
}
|
|
5185
|
+
async getPageState(sessionId) {
|
|
5186
|
+
return await this.api.getPageState(sessionId);
|
|
5187
|
+
}
|
|
5188
|
+
async listTabs(sessionId) {
|
|
5189
|
+
return await this.api.listTabs(sessionId);
|
|
5190
|
+
}
|
|
5191
|
+
async switchTab(sessionId, tabId) {
|
|
5192
|
+
return await this.api.switchTab(sessionId, tabId);
|
|
5193
|
+
}
|
|
5194
|
+
async newTab(sessionId) {
|
|
5195
|
+
return await this.api.newTab(sessionId);
|
|
5196
|
+
}
|
|
5197
|
+
async closeTab(sessionId, tabId) {
|
|
5198
|
+
return await this.api.closeTab(sessionId, tabId);
|
|
5199
|
+
}
|
|
4595
5200
|
async restartSession(sessionId) {
|
|
4596
5201
|
return await this.api.restartSession(sessionId);
|
|
4597
5202
|
}
|