browserclaw 0.5.7 → 0.5.8
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/index.cjs +532 -171
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +211 -2
- package/dist/index.d.ts +211 -2
- package/dist/index.js +520 -173
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1441,6 +1441,64 @@ async function stopChrome(running, timeoutMs = 2500) {
|
|
|
1441
1441
|
} catch {
|
|
1442
1442
|
}
|
|
1443
1443
|
}
|
|
1444
|
+
var BrowserTabNotFoundError = class extends Error {
|
|
1445
|
+
constructor(message = "Tab not found") {
|
|
1446
|
+
super(message);
|
|
1447
|
+
this.name = "BrowserTabNotFoundError";
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
var OPENCLAW_EXTENSION_RELAY_BROWSER = "OpenClaw/extension-relay";
|
|
1451
|
+
var extensionRelayByCdpUrl = /* @__PURE__ */ new Map();
|
|
1452
|
+
async function fetchJsonForCdp(url, timeoutMs) {
|
|
1453
|
+
const ctrl = new AbortController();
|
|
1454
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
1455
|
+
try {
|
|
1456
|
+
const res = await fetch(url, { signal: ctrl.signal });
|
|
1457
|
+
if (!res.ok) return null;
|
|
1458
|
+
return await res.json();
|
|
1459
|
+
} catch {
|
|
1460
|
+
return null;
|
|
1461
|
+
} finally {
|
|
1462
|
+
clearTimeout(t);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
function appendCdpPath2(cdpUrl, cdpPath) {
|
|
1466
|
+
try {
|
|
1467
|
+
const url = new URL(cdpUrl);
|
|
1468
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}${cdpPath.startsWith("/") ? cdpPath : `/${cdpPath}`}`;
|
|
1469
|
+
return url.toString();
|
|
1470
|
+
} catch {
|
|
1471
|
+
return `${cdpUrl.replace(/\/$/, "")}${cdpPath}`;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
async function isExtensionRelayCdpEndpoint(cdpUrl) {
|
|
1475
|
+
const normalized = normalizeCdpUrl(cdpUrl);
|
|
1476
|
+
const cached = extensionRelayByCdpUrl.get(normalized);
|
|
1477
|
+
if (cached !== void 0) return cached;
|
|
1478
|
+
try {
|
|
1479
|
+
const version = await fetchJsonForCdp(appendCdpPath2(normalizeCdpHttpBaseForJsonEndpoints(normalized), "/json/version"), 2e3);
|
|
1480
|
+
const isRelay = String(version?.Browser ?? "").trim() === OPENCLAW_EXTENSION_RELAY_BROWSER;
|
|
1481
|
+
extensionRelayByCdpUrl.set(normalized, isRelay);
|
|
1482
|
+
return isRelay;
|
|
1483
|
+
} catch {
|
|
1484
|
+
extensionRelayByCdpUrl.set(normalized, false);
|
|
1485
|
+
return false;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
async function withPlaywrightPageCdpSession(page, fn) {
|
|
1489
|
+
const session = await page.context().newCDPSession(page);
|
|
1490
|
+
try {
|
|
1491
|
+
return await fn(session);
|
|
1492
|
+
} finally {
|
|
1493
|
+
await session.detach().catch(() => {
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
async function withPageScopedCdpClient(opts) {
|
|
1498
|
+
return await withPlaywrightPageCdpSession(opts.page, async (session) => {
|
|
1499
|
+
return await opts.fn((method, params) => session.send(method, params));
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1444
1502
|
var LOOPBACK_ENTRIES = "localhost,127.0.0.1,[::1]";
|
|
1445
1503
|
function noProxyAlreadyCoversLocalhost() {
|
|
1446
1504
|
const current = process.env.NO_PROXY || process.env.no_proxy || "";
|
|
@@ -1713,9 +1771,11 @@ async function connectBrowser(cdpUrl, authToken) {
|
|
|
1713
1771
|
if (authToken && !headers["Authorization"]) headers["Authorization"] = `Bearer ${authToken}`;
|
|
1714
1772
|
const browser = await withNoProxyForCdpUrl(endpoint, () => playwrightCore.chromium.connectOverCDP(endpoint, { timeout, headers }));
|
|
1715
1773
|
const onDisconnected = () => {
|
|
1716
|
-
if (cachedByCdpUrl.get(normalized)?.browser === browser)
|
|
1717
|
-
|
|
1718
|
-
|
|
1774
|
+
if (cachedByCdpUrl.get(normalized)?.browser === browser) {
|
|
1775
|
+
cachedByCdpUrl.delete(normalized);
|
|
1776
|
+
for (const key of roleRefsByTarget.keys()) {
|
|
1777
|
+
if (key.startsWith(normalized + "::")) roleRefsByTarget.delete(key);
|
|
1778
|
+
}
|
|
1719
1779
|
}
|
|
1720
1780
|
};
|
|
1721
1781
|
const connected = { browser, cdpUrl: normalized, onDisconnected };
|
|
@@ -1859,8 +1919,36 @@ async function pageTargetId(page) {
|
|
|
1859
1919
|
});
|
|
1860
1920
|
}
|
|
1861
1921
|
}
|
|
1922
|
+
function matchPageByTargetList(pages, targets, targetId) {
|
|
1923
|
+
const target = targets.find((entry) => entry.id === targetId);
|
|
1924
|
+
if (!target) return null;
|
|
1925
|
+
const urlMatch = pages.filter((page) => page.url() === target.url);
|
|
1926
|
+
if (urlMatch.length === 1) return urlMatch[0] ?? null;
|
|
1927
|
+
if (urlMatch.length > 1) {
|
|
1928
|
+
const sameUrlTargets = targets.filter((entry) => entry.url === target.url);
|
|
1929
|
+
if (sameUrlTargets.length === urlMatch.length) {
|
|
1930
|
+
const idx = sameUrlTargets.findIndex((entry) => entry.id === targetId);
|
|
1931
|
+
if (idx >= 0 && idx < urlMatch.length) return urlMatch[idx] ?? null;
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
return null;
|
|
1935
|
+
}
|
|
1936
|
+
async function findPageByTargetIdViaTargetList(pages, targetId, cdpUrl) {
|
|
1937
|
+
const targets = await fetchJsonForCdp(appendCdpPath2(normalizeCdpHttpBaseForJsonEndpoints(cdpUrl), "/json/list"), 2e3);
|
|
1938
|
+
if (!Array.isArray(targets)) return null;
|
|
1939
|
+
return matchPageByTargetList(pages, targets, targetId);
|
|
1940
|
+
}
|
|
1862
1941
|
async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
1863
1942
|
const pages = await getAllPages(browser);
|
|
1943
|
+
const isExtensionRelay = cdpUrl ? await isExtensionRelayCdpEndpoint(cdpUrl).catch(() => false) : false;
|
|
1944
|
+
if (cdpUrl && isExtensionRelay) {
|
|
1945
|
+
try {
|
|
1946
|
+
const matched = await findPageByTargetIdViaTargetList(pages, targetId, cdpUrl);
|
|
1947
|
+
if (matched) return matched;
|
|
1948
|
+
} catch {
|
|
1949
|
+
}
|
|
1950
|
+
return pages.length === 1 ? pages[0] ?? null : null;
|
|
1951
|
+
}
|
|
1864
1952
|
let resolvedViaCdp = false;
|
|
1865
1953
|
for (const page of pages) {
|
|
1866
1954
|
let tid = null;
|
|
@@ -1874,34 +1962,11 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
|
1874
1962
|
}
|
|
1875
1963
|
if (cdpUrl) {
|
|
1876
1964
|
try {
|
|
1877
|
-
|
|
1878
|
-
const headers = {};
|
|
1879
|
-
const ctrl = new AbortController();
|
|
1880
|
-
const t = setTimeout(() => ctrl.abort(), 2e3);
|
|
1881
|
-
try {
|
|
1882
|
-
const response = await fetch(listUrl, { headers, signal: ctrl.signal });
|
|
1883
|
-
if (response.ok) {
|
|
1884
|
-
const targets = await response.json();
|
|
1885
|
-
const target = targets.find((entry) => entry.id === targetId);
|
|
1886
|
-
if (target) {
|
|
1887
|
-
const urlMatch = pages.filter((p) => p.url() === target.url);
|
|
1888
|
-
if (urlMatch.length === 1) return urlMatch[0];
|
|
1889
|
-
if (urlMatch.length > 1) {
|
|
1890
|
-
const sameUrlTargets = targets.filter((entry) => entry.url === target.url);
|
|
1891
|
-
if (sameUrlTargets.length === urlMatch.length) {
|
|
1892
|
-
const idx = sameUrlTargets.findIndex((entry) => entry.id === targetId);
|
|
1893
|
-
if (idx >= 0 && idx < urlMatch.length) return urlMatch[idx];
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
} finally {
|
|
1899
|
-
clearTimeout(t);
|
|
1900
|
-
}
|
|
1965
|
+
return await findPageByTargetIdViaTargetList(pages, targetId, cdpUrl);
|
|
1901
1966
|
} catch {
|
|
1902
1967
|
}
|
|
1903
1968
|
}
|
|
1904
|
-
if (!resolvedViaCdp && pages.length === 1) return pages[0];
|
|
1969
|
+
if (!resolvedViaCdp && pages.length === 1) return pages[0] ?? null;
|
|
1905
1970
|
return null;
|
|
1906
1971
|
}
|
|
1907
1972
|
async function getPageForTargetId(opts) {
|
|
@@ -1913,10 +1978,49 @@ async function getPageForTargetId(opts) {
|
|
|
1913
1978
|
const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
|
|
1914
1979
|
if (!found) {
|
|
1915
1980
|
if (pages.length === 1) return first;
|
|
1916
|
-
throw new
|
|
1981
|
+
throw new BrowserTabNotFoundError(`Tab not found (targetId: ${opts.targetId}). Call browser.tabs() to list open tabs.`);
|
|
1917
1982
|
}
|
|
1918
1983
|
return found;
|
|
1919
1984
|
}
|
|
1985
|
+
async function resolvePageByTargetIdOrThrow(opts) {
|
|
1986
|
+
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
1987
|
+
const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
|
|
1988
|
+
if (!page) throw new BrowserTabNotFoundError();
|
|
1989
|
+
return page;
|
|
1990
|
+
}
|
|
1991
|
+
function parseRoleRef(raw) {
|
|
1992
|
+
const trimmed = raw.trim();
|
|
1993
|
+
if (!trimmed) return null;
|
|
1994
|
+
const normalized = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed.startsWith("ref=") ? trimmed.slice(4) : trimmed;
|
|
1995
|
+
return /^e\d+$/.test(normalized) ? normalized : null;
|
|
1996
|
+
}
|
|
1997
|
+
function requireRef(value) {
|
|
1998
|
+
const raw = typeof value === "string" ? value.trim() : "";
|
|
1999
|
+
const ref = (raw ? parseRoleRef(raw) : null) ?? (raw.startsWith("@") ? raw.slice(1) : raw);
|
|
2000
|
+
if (!ref) throw new Error("ref is required");
|
|
2001
|
+
return ref;
|
|
2002
|
+
}
|
|
2003
|
+
function requireRefOrSelector(ref, selector) {
|
|
2004
|
+
const trimmedRef = typeof ref === "string" ? ref.trim() : "";
|
|
2005
|
+
const trimmedSelector = typeof selector === "string" ? selector.trim() : "";
|
|
2006
|
+
if (!trimmedRef && !trimmedSelector) throw new Error("ref or selector is required");
|
|
2007
|
+
return { ref: trimmedRef || void 0, selector: trimmedSelector || void 0 };
|
|
2008
|
+
}
|
|
2009
|
+
function resolveInteractionTimeoutMs(timeoutMs) {
|
|
2010
|
+
return Math.max(500, Math.min(6e4, Math.floor(timeoutMs ?? 8e3)));
|
|
2011
|
+
}
|
|
2012
|
+
function resolveBoundedDelayMs(value, label, maxMs) {
|
|
2013
|
+
const normalized = Math.floor(value ?? 0);
|
|
2014
|
+
if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
|
|
2015
|
+
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
|
|
2016
|
+
return normalized;
|
|
2017
|
+
}
|
|
2018
|
+
async function getRestoredPageForTarget(opts) {
|
|
2019
|
+
const page = await getPageForTargetId(opts);
|
|
2020
|
+
ensurePageState(page);
|
|
2021
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
2022
|
+
return page;
|
|
2023
|
+
}
|
|
1920
2024
|
function refLocator(page, ref) {
|
|
1921
2025
|
const normalized = ref.startsWith("@") ? ref.slice(1) : ref.startsWith("ref=") ? ref.slice(4) : ref;
|
|
1922
2026
|
if (!normalized.trim()) throw new Error("ref is required");
|
|
@@ -2232,12 +2336,14 @@ async function snapshotAi(opts) {
|
|
|
2232
2336
|
let snapshot = String(result?.full ?? "");
|
|
2233
2337
|
const maxChars = opts.maxChars;
|
|
2234
2338
|
const limit = typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : void 0;
|
|
2339
|
+
let truncated = false;
|
|
2235
2340
|
if (limit && snapshot.length > limit) {
|
|
2236
2341
|
const lastNewline = snapshot.lastIndexOf("\n", limit);
|
|
2237
2342
|
const cutoff = lastNewline > 0 ? lastNewline : limit;
|
|
2238
2343
|
snapshot = `${snapshot.slice(0, cutoff)}
|
|
2239
2344
|
|
|
2240
2345
|
[...TRUNCATED - page too large]`;
|
|
2346
|
+
truncated = true;
|
|
2241
2347
|
}
|
|
2242
2348
|
const built = buildRoleSnapshotFromAiSnapshot(snapshot, opts.options);
|
|
2243
2349
|
storeRoleRefsForTarget({
|
|
@@ -2251,6 +2357,7 @@ async function snapshotAi(opts) {
|
|
|
2251
2357
|
snapshot: built.snapshot,
|
|
2252
2358
|
refs: built.refs,
|
|
2253
2359
|
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
2360
|
+
...truncated ? { truncated } : {},
|
|
2254
2361
|
untrusted: true,
|
|
2255
2362
|
contentMeta: {
|
|
2256
2363
|
sourceUrl,
|
|
@@ -2324,24 +2431,20 @@ async function snapshotAria(opts) {
|
|
|
2324
2431
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2325
2432
|
ensurePageState(page);
|
|
2326
2433
|
const sourceUrl = page.url();
|
|
2327
|
-
const
|
|
2328
|
-
try {
|
|
2434
|
+
const res = await withPlaywrightPageCdpSession(page, async (session) => {
|
|
2329
2435
|
await session.send("Accessibility.enable").catch(() => {
|
|
2330
2436
|
});
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
}
|
|
2341
|
-
}
|
|
2342
|
-
await session.detach().catch(() => {
|
|
2343
|
-
});
|
|
2344
|
-
}
|
|
2437
|
+
return await session.send("Accessibility.getFullAXTree");
|
|
2438
|
+
});
|
|
2439
|
+
return {
|
|
2440
|
+
nodes: formatAriaNodes(Array.isArray(res?.nodes) ? res.nodes : [], limit),
|
|
2441
|
+
untrusted: true,
|
|
2442
|
+
contentMeta: {
|
|
2443
|
+
sourceUrl,
|
|
2444
|
+
contentType: "browser-aria-tree",
|
|
2445
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2446
|
+
}
|
|
2447
|
+
};
|
|
2345
2448
|
}
|
|
2346
2449
|
function axValue(v) {
|
|
2347
2450
|
if (!v || typeof v !== "object") return "";
|
|
@@ -2793,6 +2896,14 @@ async function assertSafeUploadPaths(paths) {
|
|
|
2793
2896
|
}
|
|
2794
2897
|
}
|
|
2795
2898
|
}
|
|
2899
|
+
async function resolveStrictExistingUploadPaths(params) {
|
|
2900
|
+
try {
|
|
2901
|
+
await assertSafeUploadPaths(params.requestedPaths);
|
|
2902
|
+
return { ok: true, paths: params.requestedPaths };
|
|
2903
|
+
} catch (err) {
|
|
2904
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2796
2907
|
function sanitizeUntrustedFileName(fileName, fallbackName) {
|
|
2797
2908
|
const trimmed = String(fileName ?? "").trim();
|
|
2798
2909
|
if (!trimmed) return fallbackName;
|
|
@@ -2866,47 +2977,57 @@ function requiresInspectableBrowserNavigationRedirects(ssrfPolicy) {
|
|
|
2866
2977
|
|
|
2867
2978
|
// src/actions/interaction.ts
|
|
2868
2979
|
var MAX_CLICK_DELAY_MS = 5e3;
|
|
2869
|
-
|
|
2870
|
-
const normalized = Math.floor(value ?? 0);
|
|
2871
|
-
if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
|
|
2872
|
-
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
|
|
2873
|
-
return normalized;
|
|
2874
|
-
}
|
|
2875
|
-
function resolveInteractionTimeoutMs(timeoutMs) {
|
|
2876
|
-
return Math.max(500, Math.min(6e4, Math.floor(timeoutMs ?? 8e3)));
|
|
2877
|
-
}
|
|
2878
|
-
function requireRefOrSelector(ref, selector) {
|
|
2879
|
-
const trimmedRef = typeof ref === "string" ? ref.trim() : "";
|
|
2880
|
-
const trimmedSelector = typeof selector === "string" ? selector.trim() : "";
|
|
2881
|
-
if (!trimmedRef && !trimmedSelector) throw new Error("ref or selector is required");
|
|
2882
|
-
return { ref: trimmedRef || void 0, selector: trimmedSelector || void 0 };
|
|
2883
|
-
}
|
|
2980
|
+
var CHECKABLE_ROLES = /* @__PURE__ */ new Set(["menuitemcheckbox", "menuitemradio", "checkbox", "switch"]);
|
|
2884
2981
|
function resolveLocator(page, resolved) {
|
|
2885
2982
|
return resolved.ref ? refLocator(page, resolved.ref) : page.locator(resolved.selector);
|
|
2886
2983
|
}
|
|
2887
|
-
async function getRestoredPageForTarget(opts) {
|
|
2888
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2889
|
-
ensurePageState(page);
|
|
2890
|
-
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
2891
|
-
return page;
|
|
2892
|
-
}
|
|
2893
2984
|
async function clickViaPlaywright(opts) {
|
|
2894
2985
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
2895
2986
|
const page = await getRestoredPageForTarget(opts);
|
|
2896
2987
|
const label = resolved.ref ?? resolved.selector;
|
|
2897
2988
|
const locator = resolveLocator(page, resolved);
|
|
2898
2989
|
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
2990
|
+
let checkableRole = false;
|
|
2991
|
+
if (resolved.ref) {
|
|
2992
|
+
const refId = parseRoleRef(resolved.ref);
|
|
2993
|
+
if (refId) {
|
|
2994
|
+
const state = ensurePageState(page);
|
|
2995
|
+
const info = state.roleRefs?.[refId];
|
|
2996
|
+
if (info && CHECKABLE_ROLES.has(info.role)) checkableRole = true;
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2899
2999
|
try {
|
|
2900
3000
|
const delayMs = resolveBoundedDelayMs(opts.delayMs, "click delayMs", MAX_CLICK_DELAY_MS);
|
|
2901
3001
|
if (delayMs > 0) {
|
|
2902
3002
|
await locator.hover({ timeout });
|
|
2903
3003
|
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
2904
3004
|
}
|
|
3005
|
+
let ariaCheckedBefore;
|
|
3006
|
+
if (checkableRole && !opts.doubleClick) {
|
|
3007
|
+
ariaCheckedBefore = await locator.getAttribute("aria-checked", { timeout }).catch(() => void 0);
|
|
3008
|
+
}
|
|
2905
3009
|
if (opts.doubleClick) {
|
|
2906
3010
|
await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
2907
3011
|
} else {
|
|
2908
3012
|
await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
2909
3013
|
}
|
|
3014
|
+
if (checkableRole && !opts.doubleClick && ariaCheckedBefore !== void 0) {
|
|
3015
|
+
const POLL_INTERVAL_MS = 50;
|
|
3016
|
+
const POLL_TIMEOUT_MS = 500;
|
|
3017
|
+
let changed = false;
|
|
3018
|
+
for (let elapsed = 0; elapsed < POLL_TIMEOUT_MS; elapsed += POLL_INTERVAL_MS) {
|
|
3019
|
+
const current = await locator.getAttribute("aria-checked", { timeout }).catch(() => void 0);
|
|
3020
|
+
if (current === void 0 || current !== ariaCheckedBefore) {
|
|
3021
|
+
changed = true;
|
|
3022
|
+
break;
|
|
3023
|
+
}
|
|
3024
|
+
await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
|
|
3025
|
+
}
|
|
3026
|
+
if (!changed) {
|
|
3027
|
+
await locator.evaluate((el) => el.click()).catch(() => {
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
2910
3031
|
} catch (err) {
|
|
2911
3032
|
throw toAIFriendlyError(err, label);
|
|
2912
3033
|
}
|
|
@@ -3006,10 +3127,11 @@ async function scrollIntoViewViaPlaywright(opts) {
|
|
|
3006
3127
|
}
|
|
3007
3128
|
async function highlightViaPlaywright(opts) {
|
|
3008
3129
|
const page = await getRestoredPageForTarget(opts);
|
|
3130
|
+
const ref = requireRef(opts.ref);
|
|
3009
3131
|
try {
|
|
3010
|
-
await refLocator(page,
|
|
3132
|
+
await refLocator(page, ref).highlight();
|
|
3011
3133
|
} catch (err) {
|
|
3012
|
-
throw toAIFriendlyError(err,
|
|
3134
|
+
throw toAIFriendlyError(err, ref);
|
|
3013
3135
|
}
|
|
3014
3136
|
}
|
|
3015
3137
|
async function setInputFilesViaPlaywright(opts) {
|
|
@@ -3020,9 +3142,14 @@ async function setInputFilesViaPlaywright(opts) {
|
|
|
3020
3142
|
if (inputRef && element) throw new Error("ref and element are mutually exclusive");
|
|
3021
3143
|
if (!inputRef && !element) throw new Error("Either ref or element is required for setInputFiles");
|
|
3022
3144
|
const locator = inputRef ? refLocator(page, inputRef) : page.locator(element).first();
|
|
3023
|
-
await
|
|
3145
|
+
const uploadPathsResult = await resolveStrictExistingUploadPaths({
|
|
3146
|
+
requestedPaths: opts.paths,
|
|
3147
|
+
scopeLabel: "uploads directory"
|
|
3148
|
+
});
|
|
3149
|
+
if (!uploadPathsResult.ok) throw new Error(uploadPathsResult.error);
|
|
3150
|
+
const resolvedPaths = uploadPathsResult.paths;
|
|
3024
3151
|
try {
|
|
3025
|
-
await locator.setInputFiles(
|
|
3152
|
+
await locator.setInputFiles(resolvedPaths);
|
|
3026
3153
|
} catch (err) {
|
|
3027
3154
|
throw toAIFriendlyError(err, inputRef || element);
|
|
3028
3155
|
}
|
|
@@ -3065,16 +3192,18 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
3065
3192
|
}
|
|
3066
3193
|
return;
|
|
3067
3194
|
}
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3195
|
+
const uploadPathsResult = await resolveStrictExistingUploadPaths({
|
|
3196
|
+
requestedPaths: opts.paths,
|
|
3197
|
+
scopeLabel: "uploads directory"
|
|
3198
|
+
});
|
|
3199
|
+
if (!uploadPathsResult.ok) {
|
|
3071
3200
|
try {
|
|
3072
3201
|
await page.keyboard.press("Escape");
|
|
3073
3202
|
} catch {
|
|
3074
3203
|
}
|
|
3075
3204
|
return;
|
|
3076
3205
|
}
|
|
3077
|
-
await fileChooser.setFiles(
|
|
3206
|
+
await fileChooser.setFiles(uploadPathsResult.paths);
|
|
3078
3207
|
try {
|
|
3079
3208
|
const input = typeof fileChooser.element === "function" ? await Promise.resolve(fileChooser.element()) : null;
|
|
3080
3209
|
if (input) {
|
|
@@ -3146,21 +3275,21 @@ async function listPagesViaPlaywright(opts) {
|
|
|
3146
3275
|
return results;
|
|
3147
3276
|
}
|
|
3148
3277
|
async function createPageViaPlaywright(opts) {
|
|
3149
|
-
const targetUrl = (opts.url ?? "").trim() || "about:blank";
|
|
3150
|
-
const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3151
|
-
if (targetUrl !== "about:blank") {
|
|
3152
|
-
await assertBrowserNavigationAllowed({ url: targetUrl, ssrfPolicy: policy });
|
|
3153
|
-
}
|
|
3154
3278
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
3155
3279
|
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
3156
3280
|
ensureContextState(context);
|
|
3157
3281
|
const page = await context.newPage();
|
|
3158
3282
|
ensurePageState(page);
|
|
3283
|
+
const targetUrl = (opts.url ?? "").trim() || "about:blank";
|
|
3284
|
+
const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3159
3285
|
if (targetUrl !== "about:blank") {
|
|
3160
3286
|
const navigationPolicy = withBrowserNavigationPolicy(policy);
|
|
3161
|
-
|
|
3162
|
-
await assertBrowserNavigationRedirectChainAllowed({
|
|
3163
|
-
|
|
3287
|
+
await assertBrowserNavigationAllowed({ url: targetUrl, ...navigationPolicy });
|
|
3288
|
+
await assertBrowserNavigationRedirectChainAllowed({
|
|
3289
|
+
request: (await page.goto(targetUrl, { timeout: 3e4 }).catch(() => null))?.request(),
|
|
3290
|
+
...navigationPolicy
|
|
3291
|
+
});
|
|
3292
|
+
await assertBrowserNavigationResultAllowed({ url: page.url(), ...navigationPolicy });
|
|
3164
3293
|
}
|
|
3165
3294
|
const tid = await pageTargetId(page).catch(() => null);
|
|
3166
3295
|
if (!tid) throw new Error("Failed to get targetId for new page");
|
|
@@ -3171,27 +3300,31 @@ async function createPageViaPlaywright(opts) {
|
|
|
3171
3300
|
type: "page"
|
|
3172
3301
|
};
|
|
3173
3302
|
}
|
|
3174
|
-
async function
|
|
3175
|
-
const
|
|
3176
|
-
|
|
3177
|
-
if (!page) throw new Error(`Tab not found (targetId: ${opts.targetId}). Use browser.tabs() to list open tabs.`);
|
|
3303
|
+
async function closePageViaPlaywright(opts) {
|
|
3304
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3305
|
+
ensurePageState(page);
|
|
3178
3306
|
await page.close();
|
|
3179
3307
|
}
|
|
3308
|
+
async function closePageByTargetIdViaPlaywright(opts) {
|
|
3309
|
+
await (await resolvePageByTargetIdOrThrow(opts)).close();
|
|
3310
|
+
}
|
|
3180
3311
|
async function focusPageByTargetIdViaPlaywright(opts) {
|
|
3181
|
-
const
|
|
3182
|
-
const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
|
|
3183
|
-
if (!page) throw new Error(`Tab not found (targetId: ${opts.targetId}). Use browser.tabs() to list open tabs.`);
|
|
3312
|
+
const page = await resolvePageByTargetIdOrThrow(opts);
|
|
3184
3313
|
try {
|
|
3185
3314
|
await page.bringToFront();
|
|
3186
3315
|
} catch (err) {
|
|
3187
|
-
const session = await page.context().newCDPSession(page);
|
|
3188
3316
|
try {
|
|
3189
|
-
await
|
|
3317
|
+
await withPageScopedCdpClient({
|
|
3318
|
+
cdpUrl: opts.cdpUrl,
|
|
3319
|
+
page,
|
|
3320
|
+
targetId: opts.targetId,
|
|
3321
|
+
fn: async (send) => {
|
|
3322
|
+
await send("Page.bringToFront");
|
|
3323
|
+
}
|
|
3324
|
+
});
|
|
3325
|
+
return;
|
|
3190
3326
|
} catch {
|
|
3191
3327
|
throw err;
|
|
3192
|
-
} finally {
|
|
3193
|
-
await session.detach().catch(() => {
|
|
3194
|
-
});
|
|
3195
3328
|
}
|
|
3196
3329
|
}
|
|
3197
3330
|
}
|
|
@@ -3379,6 +3512,165 @@ async function evaluateViaPlaywright(opts) {
|
|
|
3379
3512
|
if (signal && abortListener) signal.removeEventListener("abort", abortListener);
|
|
3380
3513
|
}
|
|
3381
3514
|
}
|
|
3515
|
+
|
|
3516
|
+
// src/actions/batch.ts
|
|
3517
|
+
var MAX_BATCH_DEPTH = 5;
|
|
3518
|
+
var MAX_BATCH_ACTIONS = 100;
|
|
3519
|
+
async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, depth = 0) {
|
|
3520
|
+
if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${MAX_BATCH_DEPTH}`);
|
|
3521
|
+
const effectiveTargetId = action.targetId ?? targetId;
|
|
3522
|
+
switch (action.kind) {
|
|
3523
|
+
case "click":
|
|
3524
|
+
await clickViaPlaywright({
|
|
3525
|
+
cdpUrl,
|
|
3526
|
+
targetId: effectiveTargetId,
|
|
3527
|
+
ref: action.ref,
|
|
3528
|
+
selector: action.selector,
|
|
3529
|
+
doubleClick: action.doubleClick,
|
|
3530
|
+
button: action.button,
|
|
3531
|
+
modifiers: action.modifiers,
|
|
3532
|
+
delayMs: action.delayMs,
|
|
3533
|
+
timeoutMs: action.timeoutMs
|
|
3534
|
+
});
|
|
3535
|
+
break;
|
|
3536
|
+
case "type":
|
|
3537
|
+
await typeViaPlaywright({
|
|
3538
|
+
cdpUrl,
|
|
3539
|
+
targetId: effectiveTargetId,
|
|
3540
|
+
ref: action.ref,
|
|
3541
|
+
selector: action.selector,
|
|
3542
|
+
text: action.text,
|
|
3543
|
+
submit: action.submit,
|
|
3544
|
+
slowly: action.slowly,
|
|
3545
|
+
timeoutMs: action.timeoutMs
|
|
3546
|
+
});
|
|
3547
|
+
break;
|
|
3548
|
+
case "press":
|
|
3549
|
+
await pressKeyViaPlaywright({
|
|
3550
|
+
cdpUrl,
|
|
3551
|
+
targetId: effectiveTargetId,
|
|
3552
|
+
key: action.key,
|
|
3553
|
+
delayMs: action.delayMs
|
|
3554
|
+
});
|
|
3555
|
+
break;
|
|
3556
|
+
case "hover":
|
|
3557
|
+
await hoverViaPlaywright({
|
|
3558
|
+
cdpUrl,
|
|
3559
|
+
targetId: effectiveTargetId,
|
|
3560
|
+
ref: action.ref,
|
|
3561
|
+
selector: action.selector,
|
|
3562
|
+
timeoutMs: action.timeoutMs
|
|
3563
|
+
});
|
|
3564
|
+
break;
|
|
3565
|
+
case "scrollIntoView":
|
|
3566
|
+
await scrollIntoViewViaPlaywright({
|
|
3567
|
+
cdpUrl,
|
|
3568
|
+
targetId: effectiveTargetId,
|
|
3569
|
+
ref: action.ref,
|
|
3570
|
+
selector: action.selector,
|
|
3571
|
+
timeoutMs: action.timeoutMs
|
|
3572
|
+
});
|
|
3573
|
+
break;
|
|
3574
|
+
case "drag":
|
|
3575
|
+
await dragViaPlaywright({
|
|
3576
|
+
cdpUrl,
|
|
3577
|
+
targetId: effectiveTargetId,
|
|
3578
|
+
startRef: action.startRef,
|
|
3579
|
+
startSelector: action.startSelector,
|
|
3580
|
+
endRef: action.endRef,
|
|
3581
|
+
endSelector: action.endSelector,
|
|
3582
|
+
timeoutMs: action.timeoutMs
|
|
3583
|
+
});
|
|
3584
|
+
break;
|
|
3585
|
+
case "select":
|
|
3586
|
+
await selectOptionViaPlaywright({
|
|
3587
|
+
cdpUrl,
|
|
3588
|
+
targetId: effectiveTargetId,
|
|
3589
|
+
ref: action.ref,
|
|
3590
|
+
selector: action.selector,
|
|
3591
|
+
values: action.values,
|
|
3592
|
+
timeoutMs: action.timeoutMs
|
|
3593
|
+
});
|
|
3594
|
+
break;
|
|
3595
|
+
case "fill":
|
|
3596
|
+
await fillFormViaPlaywright({
|
|
3597
|
+
cdpUrl,
|
|
3598
|
+
targetId: effectiveTargetId,
|
|
3599
|
+
fields: action.fields,
|
|
3600
|
+
timeoutMs: action.timeoutMs
|
|
3601
|
+
});
|
|
3602
|
+
break;
|
|
3603
|
+
case "resize":
|
|
3604
|
+
await resizeViewportViaPlaywright({
|
|
3605
|
+
cdpUrl,
|
|
3606
|
+
targetId: effectiveTargetId,
|
|
3607
|
+
width: action.width,
|
|
3608
|
+
height: action.height
|
|
3609
|
+
});
|
|
3610
|
+
break;
|
|
3611
|
+
case "wait":
|
|
3612
|
+
if (action.fn && !evaluateEnabled) throw new Error("wait --fn is disabled by config (browser.evaluateEnabled=false)");
|
|
3613
|
+
await waitForViaPlaywright({
|
|
3614
|
+
cdpUrl,
|
|
3615
|
+
targetId: effectiveTargetId,
|
|
3616
|
+
timeMs: action.timeMs,
|
|
3617
|
+
text: action.text,
|
|
3618
|
+
textGone: action.textGone,
|
|
3619
|
+
selector: action.selector,
|
|
3620
|
+
url: action.url,
|
|
3621
|
+
loadState: action.loadState,
|
|
3622
|
+
fn: action.fn,
|
|
3623
|
+
timeoutMs: action.timeoutMs
|
|
3624
|
+
});
|
|
3625
|
+
break;
|
|
3626
|
+
case "evaluate":
|
|
3627
|
+
if (!evaluateEnabled) throw new Error("act:evaluate is disabled by config (browser.evaluateEnabled=false)");
|
|
3628
|
+
await evaluateViaPlaywright({
|
|
3629
|
+
cdpUrl,
|
|
3630
|
+
targetId: effectiveTargetId,
|
|
3631
|
+
fn: action.fn,
|
|
3632
|
+
ref: action.ref,
|
|
3633
|
+
timeoutMs: action.timeoutMs
|
|
3634
|
+
});
|
|
3635
|
+
break;
|
|
3636
|
+
case "close":
|
|
3637
|
+
await closePageViaPlaywright({
|
|
3638
|
+
cdpUrl,
|
|
3639
|
+
targetId: effectiveTargetId
|
|
3640
|
+
});
|
|
3641
|
+
break;
|
|
3642
|
+
case "batch":
|
|
3643
|
+
await batchViaPlaywright({
|
|
3644
|
+
cdpUrl,
|
|
3645
|
+
targetId: effectiveTargetId,
|
|
3646
|
+
actions: action.actions,
|
|
3647
|
+
stopOnError: action.stopOnError,
|
|
3648
|
+
evaluateEnabled,
|
|
3649
|
+
depth: depth + 1
|
|
3650
|
+
});
|
|
3651
|
+
break;
|
|
3652
|
+
default:
|
|
3653
|
+
throw new Error(`Unsupported batch action kind: ${action.kind}`);
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
async function batchViaPlaywright(opts) {
|
|
3657
|
+
const depth = opts.depth ?? 0;
|
|
3658
|
+
if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${MAX_BATCH_DEPTH}`);
|
|
3659
|
+
if (opts.actions.length > MAX_BATCH_ACTIONS) throw new Error(`Batch exceeds maximum of ${MAX_BATCH_ACTIONS} actions`);
|
|
3660
|
+
const results = [];
|
|
3661
|
+
const evaluateEnabled = opts.evaluateEnabled !== false;
|
|
3662
|
+
for (const action of opts.actions) {
|
|
3663
|
+
try {
|
|
3664
|
+
await executeSingleAction(action, opts.cdpUrl, opts.targetId, evaluateEnabled, depth);
|
|
3665
|
+
results.push({ ok: true });
|
|
3666
|
+
} catch (err) {
|
|
3667
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3668
|
+
results.push({ ok: false, error: message });
|
|
3669
|
+
if (opts.stopOnError !== false) break;
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
return { results };
|
|
3673
|
+
}
|
|
3382
3674
|
function createPageDownloadWaiter(page, timeoutMs) {
|
|
3383
3675
|
let done = false;
|
|
3384
3676
|
let timer;
|
|
@@ -3500,32 +3792,33 @@ async function setDeviceViaPlaywright(opts) {
|
|
|
3500
3792
|
height: device.viewport.height
|
|
3501
3793
|
});
|
|
3502
3794
|
}
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3795
|
+
await withPageScopedCdpClient({
|
|
3796
|
+
cdpUrl: opts.cdpUrl,
|
|
3797
|
+
page,
|
|
3798
|
+
targetId: opts.targetId,
|
|
3799
|
+
fn: async (send) => {
|
|
3800
|
+
const locale = device.locale;
|
|
3801
|
+
if (device.userAgent || locale) {
|
|
3802
|
+
await send("Emulation.setUserAgentOverride", {
|
|
3803
|
+
userAgent: device.userAgent ?? "",
|
|
3804
|
+
acceptLanguage: locale ?? void 0
|
|
3805
|
+
});
|
|
3806
|
+
}
|
|
3807
|
+
if (device.viewport) {
|
|
3808
|
+
await send("Emulation.setDeviceMetricsOverride", {
|
|
3809
|
+
mobile: Boolean(device.isMobile),
|
|
3810
|
+
width: device.viewport.width,
|
|
3811
|
+
height: device.viewport.height,
|
|
3812
|
+
deviceScaleFactor: device.deviceScaleFactor ?? 1,
|
|
3813
|
+
screenWidth: device.viewport.width,
|
|
3814
|
+
screenHeight: device.viewport.height
|
|
3815
|
+
});
|
|
3816
|
+
}
|
|
3817
|
+
if (device.hasTouch) {
|
|
3818
|
+
await send("Emulation.setTouchEmulationEnabled", { enabled: true });
|
|
3819
|
+
}
|
|
3524
3820
|
}
|
|
3525
|
-
}
|
|
3526
|
-
await session.detach().catch(() => {
|
|
3527
|
-
});
|
|
3528
|
-
}
|
|
3821
|
+
});
|
|
3529
3822
|
}
|
|
3530
3823
|
async function setExtraHTTPHeadersViaPlaywright(opts) {
|
|
3531
3824
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
@@ -3577,18 +3870,19 @@ async function setLocaleViaPlaywright(opts) {
|
|
|
3577
3870
|
ensurePageState(page);
|
|
3578
3871
|
const locale = String(opts.locale ?? "").trim();
|
|
3579
3872
|
if (!locale) throw new Error("locale is required");
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3873
|
+
await withPageScopedCdpClient({
|
|
3874
|
+
cdpUrl: opts.cdpUrl,
|
|
3875
|
+
page,
|
|
3876
|
+
targetId: opts.targetId,
|
|
3877
|
+
fn: async (send) => {
|
|
3878
|
+
try {
|
|
3879
|
+
await send("Emulation.setLocaleOverride", { locale });
|
|
3880
|
+
} catch (err) {
|
|
3881
|
+
if (String(err).includes("Another locale override is already in effect")) return;
|
|
3882
|
+
throw err;
|
|
3883
|
+
}
|
|
3587
3884
|
}
|
|
3588
|
-
}
|
|
3589
|
-
await session.detach().catch(() => {
|
|
3590
|
-
});
|
|
3591
|
-
}
|
|
3885
|
+
});
|
|
3592
3886
|
}
|
|
3593
3887
|
async function setOfflineViaPlaywright(opts) {
|
|
3594
3888
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
@@ -3600,20 +3894,21 @@ async function setTimezoneViaPlaywright(opts) {
|
|
|
3600
3894
|
ensurePageState(page);
|
|
3601
3895
|
const timezoneId = String(opts.timezoneId ?? "").trim();
|
|
3602
3896
|
if (!timezoneId) throw new Error("timezoneId is required");
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3897
|
+
await withPageScopedCdpClient({
|
|
3898
|
+
cdpUrl: opts.cdpUrl,
|
|
3899
|
+
page,
|
|
3900
|
+
targetId: opts.targetId,
|
|
3901
|
+
fn: async (send) => {
|
|
3902
|
+
try {
|
|
3903
|
+
await send("Emulation.setTimezoneOverride", { timezoneId });
|
|
3904
|
+
} catch (err) {
|
|
3905
|
+
const msg = String(err);
|
|
3906
|
+
if (msg.includes("Timezone override is already in effect")) return;
|
|
3907
|
+
if (msg.includes("Invalid timezone")) throw new Error(`Invalid timezone ID: ${timezoneId}`, { cause: err });
|
|
3908
|
+
throw err;
|
|
3909
|
+
}
|
|
3612
3910
|
}
|
|
3613
|
-
}
|
|
3614
|
-
await session.detach().catch(() => {
|
|
3615
|
-
});
|
|
3616
|
-
}
|
|
3911
|
+
});
|
|
3617
3912
|
}
|
|
3618
3913
|
|
|
3619
3914
|
// src/capture/screenshot.ts
|
|
@@ -3636,47 +3931,65 @@ async function screenshotWithLabelsViaPlaywright(opts) {
|
|
|
3636
3931
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3637
3932
|
ensurePageState(page);
|
|
3638
3933
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
3639
|
-
const maxLabels = opts.maxLabels
|
|
3934
|
+
const maxLabels = typeof opts.maxLabels === "number" && Number.isFinite(opts.maxLabels) ? Math.max(1, Math.floor(opts.maxLabels)) : 150;
|
|
3640
3935
|
const type = opts.type ?? "png";
|
|
3641
3936
|
const refs = opts.refs.slice(0, maxLabels);
|
|
3642
3937
|
const skipped = opts.refs.slice(maxLabels);
|
|
3938
|
+
const viewport = await page.evaluate(() => ({
|
|
3939
|
+
width: window.innerWidth || 0,
|
|
3940
|
+
height: window.innerHeight || 0
|
|
3941
|
+
}));
|
|
3643
3942
|
const labels = [];
|
|
3644
3943
|
for (let i = 0; i < refs.length; i++) {
|
|
3645
3944
|
const ref = refs[i];
|
|
3646
3945
|
try {
|
|
3647
3946
|
const locator = refLocator(page, ref);
|
|
3648
3947
|
const box = await locator.boundingBox({ timeout: 2e3 });
|
|
3649
|
-
if (box) {
|
|
3650
|
-
labels.push({ ref, index: i + 1, box });
|
|
3651
|
-
} else {
|
|
3948
|
+
if (!box) {
|
|
3652
3949
|
skipped.push(ref);
|
|
3950
|
+
continue;
|
|
3653
3951
|
}
|
|
3952
|
+
const x1 = box.x + box.width;
|
|
3953
|
+
const y1 = box.y + box.height;
|
|
3954
|
+
if (x1 < 0 || box.x > viewport.width || y1 < 0 || box.y > viewport.height) {
|
|
3955
|
+
skipped.push(ref);
|
|
3956
|
+
continue;
|
|
3957
|
+
}
|
|
3958
|
+
labels.push({ ref, index: i + 1, box });
|
|
3654
3959
|
} catch {
|
|
3655
3960
|
skipped.push(ref);
|
|
3656
3961
|
}
|
|
3657
3962
|
}
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3963
|
+
try {
|
|
3964
|
+
if (labels.length > 0) {
|
|
3965
|
+
await page.evaluate((labelData) => {
|
|
3966
|
+
document.querySelectorAll("[data-browserclaw-labels]").forEach((el) => el.remove());
|
|
3967
|
+
const container = document.createElement("div");
|
|
3968
|
+
container.setAttribute("data-browserclaw-labels", "1");
|
|
3969
|
+
container.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:2147483647;";
|
|
3970
|
+
for (const { index, box } of labelData) {
|
|
3971
|
+
const border = document.createElement("div");
|
|
3972
|
+
border.style.cssText = `position:absolute;left:${box.x}px;top:${box.y}px;width:${box.width}px;height:${box.height}px;border:2px solid #FF4500;box-sizing:border-box;`;
|
|
3973
|
+
container.appendChild(border);
|
|
3974
|
+
const badge = document.createElement("div");
|
|
3975
|
+
badge.textContent = String(index);
|
|
3976
|
+
badge.style.cssText = `position:absolute;left:${box.x}px;top:${Math.max(0, box.y - 18)}px;background:#FF4500;color:#fff;font:bold 12px/16px monospace;padding:0 4px;border-radius:2px;`;
|
|
3977
|
+
container.appendChild(badge);
|
|
3978
|
+
}
|
|
3979
|
+
document.documentElement.appendChild(container);
|
|
3980
|
+
}, labels.map((l) => ({ index: l.index, box: l.box })));
|
|
3670
3981
|
}
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3982
|
+
return {
|
|
3983
|
+
buffer: await page.screenshot({ type }),
|
|
3984
|
+
labels,
|
|
3985
|
+
skipped
|
|
3986
|
+
};
|
|
3987
|
+
} finally {
|
|
3988
|
+
await page.evaluate(() => {
|
|
3989
|
+
document.querySelectorAll("[data-browserclaw-labels]").forEach((el) => el.remove());
|
|
3990
|
+
}).catch(() => {
|
|
3991
|
+
});
|
|
3992
|
+
}
|
|
3680
3993
|
}
|
|
3681
3994
|
|
|
3682
3995
|
// src/capture/pdf.ts
|
|
@@ -3720,15 +4033,33 @@ async function traceStopViaPlaywright(opts) {
|
|
|
3720
4033
|
}
|
|
3721
4034
|
|
|
3722
4035
|
// src/capture/response.ts
|
|
4036
|
+
function matchUrlPattern(pattern, url) {
|
|
4037
|
+
if (!pattern || !url) return false;
|
|
4038
|
+
if (pattern === url) return true;
|
|
4039
|
+
if (pattern.includes("*")) {
|
|
4040
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
4041
|
+
try {
|
|
4042
|
+
return new RegExp(`^${escaped}$`).test(url);
|
|
4043
|
+
} catch {
|
|
4044
|
+
return false;
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
return url.includes(pattern);
|
|
4048
|
+
}
|
|
3723
4049
|
async function responseBodyViaPlaywright(opts) {
|
|
3724
4050
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3725
4051
|
ensurePageState(page);
|
|
3726
4052
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
|
|
3727
|
-
const
|
|
4053
|
+
const pattern = String(opts.url ?? "").trim();
|
|
4054
|
+
if (!pattern) throw new Error("url is required");
|
|
4055
|
+
const response = await page.waitForResponse(
|
|
4056
|
+
(resp) => matchUrlPattern(pattern, resp.url()),
|
|
4057
|
+
{ timeout }
|
|
4058
|
+
);
|
|
3728
4059
|
let body = await response.text();
|
|
3729
4060
|
let truncated = false;
|
|
3730
|
-
const maxChars = typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars) ? Math.max(1, Math.min(5e6, Math.floor(opts.maxChars))) :
|
|
3731
|
-
if (
|
|
4061
|
+
const maxChars = typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars) ? Math.max(1, Math.min(5e6, Math.floor(opts.maxChars))) : 2e5;
|
|
4062
|
+
if (body.length > maxChars) {
|
|
3732
4063
|
body = body.slice(0, maxChars);
|
|
3733
4064
|
truncated = true;
|
|
3734
4065
|
}
|
|
@@ -4148,6 +4479,22 @@ var CrawlPage = class {
|
|
|
4148
4479
|
timeoutMs: opts?.timeoutMs
|
|
4149
4480
|
});
|
|
4150
4481
|
}
|
|
4482
|
+
/**
|
|
4483
|
+
* Execute multiple browser actions in sequence.
|
|
4484
|
+
*
|
|
4485
|
+
* @param actions - Array of actions to execute
|
|
4486
|
+
* @param opts - Options (stopOnError: stop on first failure, default true)
|
|
4487
|
+
* @returns Array of per-action results
|
|
4488
|
+
*/
|
|
4489
|
+
async batch(actions, opts) {
|
|
4490
|
+
return batchViaPlaywright({
|
|
4491
|
+
cdpUrl: this.cdpUrl,
|
|
4492
|
+
targetId: this.targetId,
|
|
4493
|
+
actions,
|
|
4494
|
+
stopOnError: opts?.stopOnError,
|
|
4495
|
+
evaluateEnabled: opts?.evaluateEnabled
|
|
4496
|
+
});
|
|
4497
|
+
}
|
|
4151
4498
|
// ── Keyboard ─────────────────────────────────────────────────
|
|
4152
4499
|
/**
|
|
4153
4500
|
* Press a keyboard key or key combination.
|
|
@@ -4875,22 +5222,36 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
4875
5222
|
};
|
|
4876
5223
|
|
|
4877
5224
|
exports.BrowserClaw = BrowserClaw;
|
|
5225
|
+
exports.BrowserTabNotFoundError = BrowserTabNotFoundError;
|
|
4878
5226
|
exports.CrawlPage = CrawlPage;
|
|
4879
5227
|
exports.InvalidBrowserNavigationUrlError = InvalidBrowserNavigationUrlError;
|
|
4880
5228
|
exports.assertBrowserNavigationAllowed = assertBrowserNavigationAllowed;
|
|
4881
5229
|
exports.assertBrowserNavigationRedirectChainAllowed = assertBrowserNavigationRedirectChainAllowed;
|
|
4882
5230
|
exports.assertBrowserNavigationResultAllowed = assertBrowserNavigationResultAllowed;
|
|
5231
|
+
exports.assertSafeUploadPaths = assertSafeUploadPaths;
|
|
5232
|
+
exports.batchViaPlaywright = batchViaPlaywright;
|
|
4883
5233
|
exports.createPinnedLookup = createPinnedLookup;
|
|
4884
5234
|
exports.ensureContextState = ensureContextState;
|
|
5235
|
+
exports.executeSingleAction = executeSingleAction;
|
|
4885
5236
|
exports.forceDisconnectPlaywrightForTarget = forceDisconnectPlaywrightForTarget;
|
|
4886
5237
|
exports.getChromeWebSocketUrl = getChromeWebSocketUrl;
|
|
5238
|
+
exports.getRestoredPageForTarget = getRestoredPageForTarget;
|
|
4887
5239
|
exports.isChromeCdpReady = isChromeCdpReady;
|
|
4888
5240
|
exports.isChromeReachable = isChromeReachable;
|
|
4889
5241
|
exports.normalizeCdpHttpBaseForJsonEndpoints = normalizeCdpHttpBaseForJsonEndpoints;
|
|
5242
|
+
exports.parseRoleRef = parseRoleRef;
|
|
5243
|
+
exports.requireRef = requireRef;
|
|
5244
|
+
exports.requireRefOrSelector = requireRefOrSelector;
|
|
4890
5245
|
exports.requiresInspectableBrowserNavigationRedirects = requiresInspectableBrowserNavigationRedirects;
|
|
5246
|
+
exports.resolveBoundedDelayMs = resolveBoundedDelayMs;
|
|
5247
|
+
exports.resolveInteractionTimeoutMs = resolveInteractionTimeoutMs;
|
|
5248
|
+
exports.resolvePageByTargetIdOrThrow = resolvePageByTargetIdOrThrow;
|
|
4891
5249
|
exports.resolvePinnedHostnameWithPolicy = resolvePinnedHostnameWithPolicy;
|
|
5250
|
+
exports.resolveStrictExistingUploadPaths = resolveStrictExistingUploadPaths;
|
|
4892
5251
|
exports.sanitizeUntrustedFileName = sanitizeUntrustedFileName;
|
|
4893
5252
|
exports.withBrowserNavigationPolicy = withBrowserNavigationPolicy;
|
|
5253
|
+
exports.withPageScopedCdpClient = withPageScopedCdpClient;
|
|
5254
|
+
exports.withPlaywrightPageCdpSession = withPlaywrightPageCdpSession;
|
|
4894
5255
|
exports.writeViaSiblingTempPath = writeViaSiblingTempPath;
|
|
4895
5256
|
//# sourceMappingURL=index.cjs.map
|
|
4896
5257
|
//# sourceMappingURL=index.cjs.map
|