browserclaw 0.5.8 → 0.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/dist/index.cjs +1566 -1144
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +111 -21
- package/dist/index.d.ts +111 -21
- package/dist/index.js +1556 -1137
- package/dist/index.js.map +1 -1
- package/package.json +14 -2
package/dist/index.cjs
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var os = require('os');
|
|
4
|
-
var path = require('path');
|
|
5
|
-
var fs = require('fs');
|
|
6
|
-
var net = require('net');
|
|
7
|
-
var child_process = require('child_process');
|
|
8
|
-
var playwrightCore = require('playwright-core');
|
|
9
3
|
var http = require('http');
|
|
10
4
|
var https = require('https');
|
|
11
|
-
var
|
|
5
|
+
var playwrightCore = require('playwright-core');
|
|
6
|
+
var child_process = require('child_process');
|
|
7
|
+
var fs = require('fs');
|
|
8
|
+
var net = require('net');
|
|
9
|
+
var os = require('os');
|
|
10
|
+
var path = require('path');
|
|
11
|
+
var crypto = require('crypto');
|
|
12
12
|
var dns = require('dns');
|
|
13
|
+
var promises = require('dns/promises');
|
|
13
14
|
var promises$1 = require('fs/promises');
|
|
14
|
-
var crypto = require('crypto');
|
|
15
15
|
|
|
16
16
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
17
17
|
|
|
18
|
-
var os__default = /*#__PURE__*/_interopDefault(os);
|
|
19
|
-
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
20
|
-
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
21
|
-
var net__default = /*#__PURE__*/_interopDefault(net);
|
|
22
18
|
var http__default = /*#__PURE__*/_interopDefault(http);
|
|
23
19
|
var https__default = /*#__PURE__*/_interopDefault(https);
|
|
20
|
+
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
21
|
+
var net__default = /*#__PURE__*/_interopDefault(net);
|
|
22
|
+
var os__default = /*#__PURE__*/_interopDefault(os);
|
|
23
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
24
24
|
|
|
25
25
|
var __create = Object.create;
|
|
26
26
|
var __defProp = Object.defineProperty;
|
|
@@ -921,7 +921,7 @@ function execText(command, args, timeoutMs = 1200) {
|
|
|
921
921
|
encoding: "utf8",
|
|
922
922
|
maxBuffer: 1024 * 1024
|
|
923
923
|
});
|
|
924
|
-
return
|
|
924
|
+
return output.trim() || null;
|
|
925
925
|
} catch {
|
|
926
926
|
return null;
|
|
927
927
|
}
|
|
@@ -932,7 +932,8 @@ function inferKindFromIdentifier(identifier) {
|
|
|
932
932
|
if (id.includes("edge")) return "edge";
|
|
933
933
|
if (id.includes("chromium")) return "chromium";
|
|
934
934
|
if (id.includes("canary")) return "canary";
|
|
935
|
-
if (id.includes("opera") || id.includes("vivaldi") || id.includes("yandex") || id.includes("thebrowser"))
|
|
935
|
+
if (id.includes("opera") || id.includes("vivaldi") || id.includes("yandex") || id.includes("thebrowser"))
|
|
936
|
+
return "chromium";
|
|
936
937
|
return "chrome";
|
|
937
938
|
}
|
|
938
939
|
function inferKindFromExeName(name) {
|
|
@@ -949,24 +950,29 @@ function findFirstExe(candidates) {
|
|
|
949
950
|
return null;
|
|
950
951
|
}
|
|
951
952
|
function detectDefaultBrowserBundleIdMac() {
|
|
952
|
-
const plistPath = path__default.default.join(
|
|
953
|
+
const plistPath = path__default.default.join(
|
|
954
|
+
os__default.default.homedir(),
|
|
955
|
+
"Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist"
|
|
956
|
+
);
|
|
953
957
|
if (!fileExists(plistPath)) return null;
|
|
954
958
|
const handlersRaw = execText("/usr/bin/plutil", ["-extract", "LSHandlers", "json", "-o", "-", "--", plistPath], 2e3);
|
|
955
|
-
if (
|
|
959
|
+
if (handlersRaw === null) return null;
|
|
956
960
|
let handlers;
|
|
957
961
|
try {
|
|
958
|
-
|
|
962
|
+
const parsed = JSON.parse(handlersRaw);
|
|
963
|
+
if (!Array.isArray(parsed)) return null;
|
|
964
|
+
handlers = parsed;
|
|
959
965
|
} catch {
|
|
960
966
|
return null;
|
|
961
967
|
}
|
|
962
|
-
if (!Array.isArray(handlers)) return null;
|
|
963
968
|
const resolveScheme = (scheme) => {
|
|
964
969
|
let candidate = null;
|
|
965
970
|
for (const entry of handlers) {
|
|
966
|
-
if (
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
971
|
+
if (entry === null || entry === void 0 || typeof entry !== "object") continue;
|
|
972
|
+
const rec = entry;
|
|
973
|
+
if (rec.LSHandlerURLScheme !== scheme) continue;
|
|
974
|
+
const role = (typeof rec.LSHandlerRoleAll === "string" ? rec.LSHandlerRoleAll : null) ?? (typeof rec.LSHandlerRoleViewer === "string" ? rec.LSHandlerRoleViewer : null) ?? null;
|
|
975
|
+
if (role !== null) candidate = role;
|
|
970
976
|
}
|
|
971
977
|
return candidate;
|
|
972
978
|
};
|
|
@@ -974,12 +980,12 @@ function detectDefaultBrowserBundleIdMac() {
|
|
|
974
980
|
}
|
|
975
981
|
function detectDefaultChromiumMac() {
|
|
976
982
|
const bundleId = detectDefaultBrowserBundleIdMac();
|
|
977
|
-
if (
|
|
983
|
+
if (bundleId === null || !CHROMIUM_BUNDLE_IDS.has(bundleId)) return null;
|
|
978
984
|
const appPathRaw = execText("/usr/bin/osascript", ["-e", `POSIX path of (path to application id "${bundleId}")`]);
|
|
979
|
-
if (
|
|
985
|
+
if (appPathRaw === null) return null;
|
|
980
986
|
const appPath = appPathRaw.trim().replace(/\/$/, "");
|
|
981
987
|
const exeName = execText("/usr/bin/defaults", ["read", path__default.default.join(appPath, "Contents", "Info"), "CFBundleExecutable"]);
|
|
982
|
-
if (
|
|
988
|
+
if (exeName === null) return null;
|
|
983
989
|
const exePath = path__default.default.join(appPath, "Contents", "MacOS", exeName.trim());
|
|
984
990
|
if (!fileExists(exePath)) return null;
|
|
985
991
|
return { kind: inferKindFromIdentifier(bundleId), path: exePath };
|
|
@@ -995,12 +1001,15 @@ function findChromeMac() {
|
|
|
995
1001
|
{ kind: "chromium", path: "/Applications/Chromium.app/Contents/MacOS/Chromium" },
|
|
996
1002
|
{ kind: "chromium", path: path__default.default.join(os__default.default.homedir(), "Applications/Chromium.app/Contents/MacOS/Chromium") },
|
|
997
1003
|
{ kind: "canary", path: "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary" },
|
|
998
|
-
{
|
|
1004
|
+
{
|
|
1005
|
+
kind: "canary",
|
|
1006
|
+
path: path__default.default.join(os__default.default.homedir(), "Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary")
|
|
1007
|
+
}
|
|
999
1008
|
]);
|
|
1000
1009
|
}
|
|
1001
1010
|
function detectDefaultChromiumLinux() {
|
|
1002
|
-
const desktopId = execText("xdg-settings", ["get", "default-web-browser"])
|
|
1003
|
-
if (
|
|
1011
|
+
const desktopId = execText("xdg-settings", ["get", "default-web-browser"]) ?? execText("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
|
|
1012
|
+
if (desktopId === null) return null;
|
|
1004
1013
|
const trimmed = desktopId.trim();
|
|
1005
1014
|
if (!CHROMIUM_DESKTOP_IDS.has(trimmed)) return null;
|
|
1006
1015
|
const searchDirs = [
|
|
@@ -1017,17 +1026,18 @@ function detectDefaultChromiumLinux() {
|
|
|
1017
1026
|
break;
|
|
1018
1027
|
}
|
|
1019
1028
|
}
|
|
1020
|
-
if (
|
|
1029
|
+
if (desktopPath === null) return null;
|
|
1021
1030
|
let execLine = null;
|
|
1022
1031
|
try {
|
|
1023
1032
|
const lines = fs__default.default.readFileSync(desktopPath, "utf8").split(/\r?\n/);
|
|
1024
|
-
for (const line of lines)
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1033
|
+
for (const line of lines)
|
|
1034
|
+
if (line.startsWith("Exec=")) {
|
|
1035
|
+
execLine = line.slice(5).trim();
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
1028
1038
|
} catch {
|
|
1029
1039
|
}
|
|
1030
|
-
if (
|
|
1040
|
+
if (execLine === null) return null;
|
|
1031
1041
|
const tokens = execLine.split(/\s+/);
|
|
1032
1042
|
let command = null;
|
|
1033
1043
|
for (const token of tokens) {
|
|
@@ -1035,9 +1045,9 @@ function detectDefaultChromiumLinux() {
|
|
|
1035
1045
|
command = token.replace(/^["']|["']$/g, "");
|
|
1036
1046
|
break;
|
|
1037
1047
|
}
|
|
1038
|
-
if (
|
|
1048
|
+
if (command === null) return null;
|
|
1039
1049
|
const resolved = command.startsWith("/") ? command : execText("which", [command], 800)?.trim() ?? null;
|
|
1040
|
-
if (
|
|
1050
|
+
if (resolved === null || resolved === "") return null;
|
|
1041
1051
|
const exeName = path__default.default.posix.basename(resolved).toLowerCase();
|
|
1042
1052
|
if (!CHROMIUM_EXE_NAMES.has(exeName)) return null;
|
|
1043
1053
|
return { kind: inferKindFromExeName(exeName), path: resolved };
|
|
@@ -1066,21 +1076,30 @@ function findChromeWindows() {
|
|
|
1066
1076
|
const candidates = [];
|
|
1067
1077
|
if (localAppData) {
|
|
1068
1078
|
candidates.push({ kind: "chrome", path: j(localAppData, "Google", "Chrome", "Application", "chrome.exe") });
|
|
1069
|
-
candidates.push({
|
|
1079
|
+
candidates.push({
|
|
1080
|
+
kind: "brave",
|
|
1081
|
+
path: j(localAppData, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
|
|
1082
|
+
});
|
|
1070
1083
|
candidates.push({ kind: "edge", path: j(localAppData, "Microsoft", "Edge", "Application", "msedge.exe") });
|
|
1071
1084
|
candidates.push({ kind: "chromium", path: j(localAppData, "Chromium", "Application", "chrome.exe") });
|
|
1072
1085
|
candidates.push({ kind: "canary", path: j(localAppData, "Google", "Chrome SxS", "Application", "chrome.exe") });
|
|
1073
1086
|
}
|
|
1074
1087
|
candidates.push({ kind: "chrome", path: j(programFiles, "Google", "Chrome", "Application", "chrome.exe") });
|
|
1075
1088
|
candidates.push({ kind: "chrome", path: j(programFilesX86, "Google", "Chrome", "Application", "chrome.exe") });
|
|
1076
|
-
candidates.push({
|
|
1077
|
-
|
|
1089
|
+
candidates.push({
|
|
1090
|
+
kind: "brave",
|
|
1091
|
+
path: j(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
|
|
1092
|
+
});
|
|
1093
|
+
candidates.push({
|
|
1094
|
+
kind: "brave",
|
|
1095
|
+
path: j(programFilesX86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
|
|
1096
|
+
});
|
|
1078
1097
|
candidates.push({ kind: "edge", path: j(programFiles, "Microsoft", "Edge", "Application", "msedge.exe") });
|
|
1079
1098
|
candidates.push({ kind: "edge", path: j(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe") });
|
|
1080
1099
|
return findFirstExe(candidates);
|
|
1081
1100
|
}
|
|
1082
1101
|
function resolveBrowserExecutable(opts) {
|
|
1083
|
-
if (opts?.executablePath) {
|
|
1102
|
+
if (opts?.executablePath !== void 0 && opts.executablePath !== "") {
|
|
1084
1103
|
if (!fileExists(opts.executablePath)) throw new Error(`executablePath not found: ${opts.executablePath}`);
|
|
1085
1104
|
return { kind: "custom", path: opts.executablePath };
|
|
1086
1105
|
}
|
|
@@ -1093,10 +1112,12 @@ function resolveBrowserExecutable(opts) {
|
|
|
1093
1112
|
async function ensurePortAvailable(port) {
|
|
1094
1113
|
await new Promise((resolve2, reject) => {
|
|
1095
1114
|
const tester = net__default.default.createServer().once("error", (err) => {
|
|
1096
|
-
if (err.code === "EADDRINUSE") reject(new Error(`Port ${port} is already in use`));
|
|
1115
|
+
if (err.code === "EADDRINUSE") reject(new Error(`Port ${String(port)} is already in use`));
|
|
1097
1116
|
else reject(err);
|
|
1098
1117
|
}).once("listening", () => {
|
|
1099
|
-
tester.close(() =>
|
|
1118
|
+
tester.close(() => {
|
|
1119
|
+
resolve2();
|
|
1120
|
+
});
|
|
1100
1121
|
}).listen(port);
|
|
1101
1122
|
});
|
|
1102
1123
|
}
|
|
@@ -1178,7 +1199,7 @@ function isLoopbackHost(hostname) {
|
|
|
1178
1199
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
1179
1200
|
}
|
|
1180
1201
|
function hasProxyEnvConfigured() {
|
|
1181
|
-
return
|
|
1202
|
+
return (process.env.HTTP_PROXY ?? process.env.HTTPS_PROXY ?? process.env.http_proxy ?? process.env.https_proxy ?? "") !== "";
|
|
1182
1203
|
}
|
|
1183
1204
|
function normalizeCdpWsUrl(wsUrl, cdpUrl) {
|
|
1184
1205
|
const ws = new URL(wsUrl);
|
|
@@ -1230,7 +1251,12 @@ async function canOpenWebSocket(url, timeoutMs) {
|
|
|
1230
1251
|
}
|
|
1231
1252
|
resolve2(value);
|
|
1232
1253
|
};
|
|
1233
|
-
const timer = setTimeout(
|
|
1254
|
+
const timer = setTimeout(
|
|
1255
|
+
() => {
|
|
1256
|
+
finish(false);
|
|
1257
|
+
},
|
|
1258
|
+
Math.max(50, timeoutMs + 25)
|
|
1259
|
+
);
|
|
1234
1260
|
let ws;
|
|
1235
1261
|
try {
|
|
1236
1262
|
ws = new WebSocket(url);
|
|
@@ -1238,21 +1264,27 @@ async function canOpenWebSocket(url, timeoutMs) {
|
|
|
1238
1264
|
finish(false);
|
|
1239
1265
|
return;
|
|
1240
1266
|
}
|
|
1241
|
-
ws.onopen = () =>
|
|
1242
|
-
|
|
1267
|
+
ws.onopen = () => {
|
|
1268
|
+
finish(true);
|
|
1269
|
+
};
|
|
1270
|
+
ws.onerror = () => {
|
|
1271
|
+
finish(false);
|
|
1272
|
+
};
|
|
1243
1273
|
});
|
|
1244
1274
|
}
|
|
1245
1275
|
async function fetchChromeVersion(cdpUrl, timeoutMs = 500, authToken) {
|
|
1246
1276
|
const ctrl = new AbortController();
|
|
1247
|
-
const t = setTimeout(() =>
|
|
1277
|
+
const t = setTimeout(() => {
|
|
1278
|
+
ctrl.abort();
|
|
1279
|
+
}, timeoutMs);
|
|
1248
1280
|
try {
|
|
1249
1281
|
const httpBase = isWebSocketUrl(cdpUrl) ? normalizeCdpHttpBaseForJsonEndpoints(cdpUrl) : cdpUrl;
|
|
1250
1282
|
const headers = {};
|
|
1251
|
-
if (authToken) headers
|
|
1283
|
+
if (authToken !== void 0 && authToken !== "") headers.Authorization = `Bearer ${authToken}`;
|
|
1252
1284
|
const res = await fetch(appendCdpPath(httpBase, "/json/version"), { signal: ctrl.signal, headers });
|
|
1253
1285
|
if (!res.ok) return null;
|
|
1254
1286
|
const data = await res.json();
|
|
1255
|
-
if (
|
|
1287
|
+
if (data === null || data === void 0 || typeof data !== "object") return null;
|
|
1256
1288
|
return data;
|
|
1257
1289
|
} catch {
|
|
1258
1290
|
return null;
|
|
@@ -1263,18 +1295,32 @@ async function fetchChromeVersion(cdpUrl, timeoutMs = 500, authToken) {
|
|
|
1263
1295
|
async function isChromeReachable(cdpUrl, timeoutMs = 500, authToken) {
|
|
1264
1296
|
if (isWebSocketUrl(cdpUrl)) return await canOpenWebSocket(cdpUrl, timeoutMs);
|
|
1265
1297
|
const version = await fetchChromeVersion(cdpUrl, timeoutMs, authToken);
|
|
1266
|
-
|
|
1298
|
+
if (version !== null) return true;
|
|
1299
|
+
let isLoopback = false;
|
|
1300
|
+
try {
|
|
1301
|
+
const u = new URL(cdpUrl.startsWith("http") ? cdpUrl : `http://${cdpUrl}`);
|
|
1302
|
+
isLoopback = isLoopbackHost(u.hostname);
|
|
1303
|
+
} catch {
|
|
1304
|
+
}
|
|
1305
|
+
if (!isLoopback) return false;
|
|
1306
|
+
for (let i = 0; i < 2; i++) {
|
|
1307
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
1308
|
+
const retry = await fetchChromeVersion(cdpUrl, timeoutMs, authToken);
|
|
1309
|
+
if (retry !== null) return true;
|
|
1310
|
+
}
|
|
1311
|
+
return false;
|
|
1267
1312
|
}
|
|
1268
1313
|
async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500, authToken) {
|
|
1269
1314
|
if (isWebSocketUrl(cdpUrl)) return cdpUrl;
|
|
1270
1315
|
const version = await fetchChromeVersion(cdpUrl, timeoutMs, authToken);
|
|
1271
|
-
const
|
|
1272
|
-
|
|
1316
|
+
const rawWsUrl = version?.webSocketDebuggerUrl;
|
|
1317
|
+
const wsUrl = typeof rawWsUrl === "string" ? rawWsUrl.trim() : "";
|
|
1318
|
+
if (wsUrl === "") return null;
|
|
1273
1319
|
return normalizeCdpWsUrl(wsUrl, cdpUrl);
|
|
1274
1320
|
}
|
|
1275
1321
|
async function isChromeCdpReady(cdpUrl, timeoutMs = 500, handshakeTimeoutMs = 800) {
|
|
1276
1322
|
const wsUrl = await getChromeWebSocketUrl(cdpUrl, timeoutMs);
|
|
1277
|
-
if (
|
|
1323
|
+
if (wsUrl === null) return false;
|
|
1278
1324
|
return await canRunCdpHealthCommand(wsUrl, handshakeTimeoutMs);
|
|
1279
1325
|
}
|
|
1280
1326
|
async function canRunCdpHealthCommand(wsUrl, timeoutMs = 800) {
|
|
@@ -1290,7 +1336,12 @@ async function canRunCdpHealthCommand(wsUrl, timeoutMs = 800) {
|
|
|
1290
1336
|
}
|
|
1291
1337
|
resolve2(value);
|
|
1292
1338
|
};
|
|
1293
|
-
const timer = setTimeout(
|
|
1339
|
+
const timer = setTimeout(
|
|
1340
|
+
() => {
|
|
1341
|
+
finish(false);
|
|
1342
|
+
},
|
|
1343
|
+
Math.max(50, timeoutMs + 25)
|
|
1344
|
+
);
|
|
1294
1345
|
let ws;
|
|
1295
1346
|
try {
|
|
1296
1347
|
ws = new WebSocket(wsUrl);
|
|
@@ -1308,26 +1359,33 @@ async function canRunCdpHealthCommand(wsUrl, timeoutMs = 800) {
|
|
|
1308
1359
|
ws.onmessage = (event) => {
|
|
1309
1360
|
try {
|
|
1310
1361
|
const parsed = JSON.parse(String(event.data));
|
|
1311
|
-
if (parsed
|
|
1312
|
-
|
|
1362
|
+
if (typeof parsed !== "object" || parsed === null) return;
|
|
1363
|
+
const msg = parsed;
|
|
1364
|
+
if (msg.id !== 1) return;
|
|
1365
|
+
finish(typeof msg.result === "object" && msg.result !== null);
|
|
1313
1366
|
} catch {
|
|
1314
1367
|
}
|
|
1315
1368
|
};
|
|
1316
|
-
ws.onerror = () =>
|
|
1317
|
-
|
|
1369
|
+
ws.onerror = () => {
|
|
1370
|
+
finish(false);
|
|
1371
|
+
};
|
|
1372
|
+
ws.onclose = () => {
|
|
1373
|
+
finish(false);
|
|
1374
|
+
};
|
|
1318
1375
|
});
|
|
1319
1376
|
}
|
|
1320
1377
|
async function launchChrome(opts = {}) {
|
|
1321
1378
|
const cdpPort = opts.cdpPort ?? DEFAULT_CDP_PORT;
|
|
1322
1379
|
await ensurePortAvailable(cdpPort);
|
|
1323
1380
|
const exe = resolveBrowserExecutable({ executablePath: opts.executablePath });
|
|
1324
|
-
if (!exe)
|
|
1381
|
+
if (!exe)
|
|
1382
|
+
throw new Error("No supported browser found (Chrome/Brave/Edge/Chromium). Install one or provide executablePath.");
|
|
1325
1383
|
const profileName = opts.profileName ?? DEFAULT_PROFILE_NAME;
|
|
1326
1384
|
const userDataDir = opts.userDataDir ?? resolveUserDataDir(profileName);
|
|
1327
1385
|
fs__default.default.mkdirSync(userDataDir, { recursive: true });
|
|
1328
1386
|
const spawnChrome = () => {
|
|
1329
1387
|
const args = [
|
|
1330
|
-
`--remote-debugging-port=${cdpPort}`,
|
|
1388
|
+
`--remote-debugging-port=${String(cdpPort)}`,
|
|
1331
1389
|
`--user-data-dir=${userDataDir}`,
|
|
1332
1390
|
"--no-first-run",
|
|
1333
1391
|
"--no-default-browser-check",
|
|
@@ -1340,10 +1398,10 @@ async function launchChrome(opts = {}) {
|
|
|
1340
1398
|
"--hide-crash-restore-bubble",
|
|
1341
1399
|
"--password-store=basic"
|
|
1342
1400
|
];
|
|
1343
|
-
if (opts.headless) {
|
|
1401
|
+
if (opts.headless === true) {
|
|
1344
1402
|
args.push("--headless=new", "--disable-gpu");
|
|
1345
1403
|
}
|
|
1346
|
-
if (opts.noSandbox) {
|
|
1404
|
+
if (opts.noSandbox === true) {
|
|
1347
1405
|
args.push("--no-sandbox", "--disable-setuid-sandbox");
|
|
1348
1406
|
}
|
|
1349
1407
|
if (process.platform === "linux") args.push("--disable-dev-shm-usage");
|
|
@@ -1390,12 +1448,12 @@ async function launchChrome(opts = {}) {
|
|
|
1390
1448
|
} catch {
|
|
1391
1449
|
}
|
|
1392
1450
|
const proc = spawnChrome();
|
|
1393
|
-
const cdpUrl = `http://127.0.0.1:${cdpPort}`;
|
|
1451
|
+
const cdpUrl = `http://127.0.0.1:${String(cdpPort)}`;
|
|
1394
1452
|
const stderrChunks = [];
|
|
1395
1453
|
const onStderr = (chunk) => {
|
|
1396
1454
|
stderrChunks.push(chunk);
|
|
1397
1455
|
};
|
|
1398
|
-
proc.stderr
|
|
1456
|
+
proc.stderr.on("data", onStderr);
|
|
1399
1457
|
const readyDeadline = Date.now() + 15e3;
|
|
1400
1458
|
while (Date.now() < readyDeadline) {
|
|
1401
1459
|
if (await isChromeReachable(cdpUrl, 500)) break;
|
|
@@ -1406,14 +1464,14 @@ async function launchChrome(opts = {}) {
|
|
|
1406
1464
|
const stderrHint = stderrOutput ? `
|
|
1407
1465
|
Chrome stderr:
|
|
1408
1466
|
${stderrOutput.slice(0, 2e3)}` : "";
|
|
1409
|
-
const sandboxHint = process.platform === "linux" &&
|
|
1467
|
+
const sandboxHint = process.platform === "linux" && opts.noSandbox !== true ? "\nHint: If running in a container or as root, try setting noSandbox: true." : "";
|
|
1410
1468
|
try {
|
|
1411
1469
|
proc.kill("SIGKILL");
|
|
1412
1470
|
} catch {
|
|
1413
1471
|
}
|
|
1414
|
-
throw new Error(`Failed to start Chrome CDP on port ${cdpPort}.${sandboxHint}${stderrHint}`);
|
|
1472
|
+
throw new Error(`Failed to start Chrome CDP on port ${String(cdpPort)}.${sandboxHint}${stderrHint}`);
|
|
1415
1473
|
}
|
|
1416
|
-
proc.stderr
|
|
1474
|
+
proc.stderr.off("data", onStderr);
|
|
1417
1475
|
stderrChunks.length = 0;
|
|
1418
1476
|
return {
|
|
1419
1477
|
pid: proc.pid ?? -1,
|
|
@@ -1426,14 +1484,14 @@ ${stderrOutput.slice(0, 2e3)}` : "";
|
|
|
1426
1484
|
}
|
|
1427
1485
|
async function stopChrome(running, timeoutMs = 2500) {
|
|
1428
1486
|
const proc = running.proc;
|
|
1429
|
-
if (proc.exitCode
|
|
1487
|
+
if (proc.exitCode !== null) return;
|
|
1430
1488
|
try {
|
|
1431
1489
|
proc.kill("SIGTERM");
|
|
1432
1490
|
} catch {
|
|
1433
1491
|
}
|
|
1434
1492
|
const start = Date.now();
|
|
1435
1493
|
while (Date.now() - start < timeoutMs) {
|
|
1436
|
-
if (proc.exitCode
|
|
1494
|
+
if (proc.exitCode !== null) return;
|
|
1437
1495
|
await new Promise((r) => setTimeout(r, 100));
|
|
1438
1496
|
}
|
|
1439
1497
|
try {
|
|
@@ -1441,17 +1499,211 @@ async function stopChrome(running, timeoutMs = 2500) {
|
|
|
1441
1499
|
} catch {
|
|
1442
1500
|
}
|
|
1443
1501
|
}
|
|
1502
|
+
|
|
1503
|
+
// src/stealth.ts
|
|
1504
|
+
var STEALTH_SCRIPT = `(function() {
|
|
1505
|
+
'use strict';
|
|
1506
|
+
function p(fn) { try { fn(); } catch(_) {} }
|
|
1507
|
+
|
|
1508
|
+
// \u2500\u2500 1. navigator.webdriver \u2192 undefined \u2500\u2500
|
|
1509
|
+
p(function() {
|
|
1510
|
+
Object.defineProperty(navigator, 'webdriver', { get: function() { return undefined; }, configurable: true });
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
// \u2500\u2500 2. navigator.plugins + mimeTypes (only if empty \u2014 Chrome 92+ populates them natively) \u2500\u2500
|
|
1514
|
+
p(function() {
|
|
1515
|
+
if (navigator.plugins && navigator.plugins.length > 0) return;
|
|
1516
|
+
|
|
1517
|
+
function FakePlugin(name, fn, desc, mimes) {
|
|
1518
|
+
this.name = name; this.filename = fn; this.description = desc; this.length = mimes.length;
|
|
1519
|
+
for (var i = 0; i < mimes.length; i++) { this[i] = mimes[i]; mimes[i].enabledPlugin = this; }
|
|
1520
|
+
}
|
|
1521
|
+
FakePlugin.prototype.item = function(i) { return this[i] || null; };
|
|
1522
|
+
FakePlugin.prototype.namedItem = function(n) {
|
|
1523
|
+
for (var i = 0; i < this.length; i++) if (this[i].type === n) return this[i];
|
|
1524
|
+
return null;
|
|
1525
|
+
};
|
|
1526
|
+
|
|
1527
|
+
function M(type, suf, desc) { this.type = type; this.suffixes = suf; this.description = desc; }
|
|
1528
|
+
|
|
1529
|
+
var m1 = new M('application/pdf', 'pdf', 'Portable Document Format');
|
|
1530
|
+
var m2 = new M('application/x-google-chrome-pdf', 'pdf', 'Portable Document Format');
|
|
1531
|
+
var m3 = new M('application/x-nacl', '', 'Native Client Executable');
|
|
1532
|
+
var m4 = new M('application/x-pnacl', '', 'Portable Native Client Executable');
|
|
1533
|
+
|
|
1534
|
+
var plugins = [
|
|
1535
|
+
new FakePlugin('Chrome PDF Plugin', 'internal-pdf-viewer', 'Portable Document Format', [m1]),
|
|
1536
|
+
new FakePlugin('Chrome PDF Viewer', 'mhjfbmdgcfjbbpaeojofohoefgiehjai', '', [m2]),
|
|
1537
|
+
new FakePlugin('Native Client', 'internal-nacl-plugin', '', [m3, m4]),
|
|
1538
|
+
];
|
|
1539
|
+
|
|
1540
|
+
function makeIterable(arr, items) {
|
|
1541
|
+
arr.length = items.length;
|
|
1542
|
+
for (var i = 0; i < items.length; i++) arr[i] = items[i];
|
|
1543
|
+
arr[Symbol.iterator] = function() {
|
|
1544
|
+
var idx = 0;
|
|
1545
|
+
return { next: function() {
|
|
1546
|
+
return idx < items.length ? { value: items[idx++], done: false } : { done: true };
|
|
1547
|
+
}};
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
var pa = { item: function(i) { return plugins[i] || null; },
|
|
1552
|
+
namedItem: function(n) { for (var i = 0; i < plugins.length; i++) if (plugins[i].name === n) return plugins[i]; return null; },
|
|
1553
|
+
refresh: function() {} };
|
|
1554
|
+
makeIterable(pa, plugins);
|
|
1555
|
+
Object.defineProperty(navigator, 'plugins', { get: function() { return pa; } });
|
|
1556
|
+
|
|
1557
|
+
var allMimes = [m1, m2, m3, m4];
|
|
1558
|
+
var ma = { item: function(i) { return allMimes[i] || null; },
|
|
1559
|
+
namedItem: function(n) { for (var i = 0; i < allMimes.length; i++) if (allMimes[i].type === n) return allMimes[i]; return null; } };
|
|
1560
|
+
makeIterable(ma, allMimes);
|
|
1561
|
+
Object.defineProperty(navigator, 'mimeTypes', { get: function() { return ma; } });
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
// \u2500\u2500 3. navigator.languages (cached + frozen so identity check passes) \u2500\u2500
|
|
1565
|
+
p(function() {
|
|
1566
|
+
if (!navigator.languages || navigator.languages.length === 0) {
|
|
1567
|
+
var langs = Object.freeze(['en-US', 'en']);
|
|
1568
|
+
Object.defineProperty(navigator, 'languages', { get: function() { return langs; } });
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
// \u2500\u2500 4. window.chrome \u2500\u2500
|
|
1573
|
+
p(function() {
|
|
1574
|
+
if (window.chrome && window.chrome.runtime && window.chrome.runtime.connect) return;
|
|
1575
|
+
|
|
1576
|
+
var chrome = window.chrome || {};
|
|
1577
|
+
var noop = function() {};
|
|
1578
|
+
var evtStub = { addListener: noop, removeListener: noop, hasListeners: function() { return false; } };
|
|
1579
|
+
chrome.runtime = chrome.runtime || {};
|
|
1580
|
+
chrome.runtime.onMessage = chrome.runtime.onMessage || evtStub;
|
|
1581
|
+
chrome.runtime.onConnect = chrome.runtime.onConnect || evtStub;
|
|
1582
|
+
chrome.runtime.sendMessage = chrome.runtime.sendMessage || noop;
|
|
1583
|
+
chrome.runtime.connect = chrome.runtime.connect || function() {
|
|
1584
|
+
return { onMessage: { addListener: noop }, postMessage: noop, disconnect: noop };
|
|
1585
|
+
};
|
|
1586
|
+
if (chrome.runtime.id === undefined) chrome.runtime.id = undefined;
|
|
1587
|
+
if (!chrome.loadTimes) chrome.loadTimes = function() { return {}; };
|
|
1588
|
+
if (!chrome.csi) chrome.csi = function() { return {}; };
|
|
1589
|
+
if (!chrome.app) {
|
|
1590
|
+
chrome.app = {
|
|
1591
|
+
isInstalled: false,
|
|
1592
|
+
InstallState: { INSTALLED: 'installed', NOT_INSTALLED: 'not_installed', DISABLED: 'disabled' },
|
|
1593
|
+
RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' },
|
|
1594
|
+
getDetails: function() { return null; },
|
|
1595
|
+
getIsInstalled: function() { return false; },
|
|
1596
|
+
runningState: function() { return 'cannot_run'; },
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
if (!window.chrome) {
|
|
1601
|
+
Object.defineProperty(window, 'chrome', { value: chrome, writable: false, enumerable: true, configurable: false });
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
// \u2500\u2500 5. Permissions API consistency \u2500\u2500
|
|
1606
|
+
p(function() {
|
|
1607
|
+
var orig = navigator.permissions.query.bind(navigator.permissions);
|
|
1608
|
+
function q(params) {
|
|
1609
|
+
if (params.name === 'notifications') {
|
|
1610
|
+
return Promise.resolve({
|
|
1611
|
+
state: typeof Notification !== 'undefined' ? Notification.permission : 'prompt',
|
|
1612
|
+
name: 'notifications', onchange: null,
|
|
1613
|
+
addEventListener: function(){}, removeEventListener: function(){}, dispatchEvent: function(){ return true; },
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
return orig(params);
|
|
1617
|
+
}
|
|
1618
|
+
q.toString = function() { return 'function query() { [native code] }'; };
|
|
1619
|
+
navigator.permissions.query = q;
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
// \u2500\u2500 6. WebGL vendor / renderer \u2500\u2500
|
|
1623
|
+
p(function() {
|
|
1624
|
+
var h = {
|
|
1625
|
+
apply: function(target, self, args) {
|
|
1626
|
+
var param = args[0];
|
|
1627
|
+
if (param === 0x9245) return 'Intel Inc.';
|
|
1628
|
+
if (param === 0x9246) return 'Intel Iris OpenGL Engine';
|
|
1629
|
+
return Reflect.apply(target, self, args);
|
|
1630
|
+
}
|
|
1631
|
+
};
|
|
1632
|
+
if (typeof WebGLRenderingContext !== 'undefined')
|
|
1633
|
+
WebGLRenderingContext.prototype.getParameter = new Proxy(WebGLRenderingContext.prototype.getParameter, h);
|
|
1634
|
+
if (typeof WebGL2RenderingContext !== 'undefined')
|
|
1635
|
+
WebGL2RenderingContext.prototype.getParameter = new Proxy(WebGL2RenderingContext.prototype.getParameter, h);
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
// \u2500\u2500 7. Notification.permission \u2500\u2500
|
|
1639
|
+
p(function() {
|
|
1640
|
+
if (typeof Notification !== 'undefined' && Notification.permission === 'denied') {
|
|
1641
|
+
Object.defineProperty(Notification, 'permission', { get: function() { return 'default'; }, configurable: true });
|
|
1642
|
+
}
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
// \u2500\u2500 8. navigator.connection (cached so identity check passes) \u2500\u2500
|
|
1646
|
+
p(function() {
|
|
1647
|
+
if (navigator.connection) return;
|
|
1648
|
+
var conn = {
|
|
1649
|
+
effectiveType: '4g', rtt: 50, downlink: 10, saveData: false, onchange: null,
|
|
1650
|
+
addEventListener: function(){}, removeEventListener: function(){}, dispatchEvent: function(){ return true; },
|
|
1651
|
+
};
|
|
1652
|
+
Object.defineProperty(navigator, 'connection', { get: function() { return conn; } });
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
// \u2500\u2500 9. Iframe contentWindow.chrome \u2500\u2500
|
|
1656
|
+
// Handled by patch 4 \u2014 chrome object is now on window, propagates to iframes on same origin.
|
|
1657
|
+
|
|
1658
|
+
// \u2500\u2500 10. console method toString \u2500\u2500
|
|
1659
|
+
p(function() {
|
|
1660
|
+
['log','info','warn','error','debug','table','trace'].forEach(function(n) {
|
|
1661
|
+
if (console[n]) {
|
|
1662
|
+
console[n].toString = function() { return 'function ' + n + '() { [native code] }'; };
|
|
1663
|
+
}
|
|
1664
|
+
});
|
|
1665
|
+
});
|
|
1666
|
+
|
|
1667
|
+
// \u2500\u2500 11. Headless-mode window / screen fixes \u2500\u2500
|
|
1668
|
+
p(function() {
|
|
1669
|
+
if (window.outerWidth === 0)
|
|
1670
|
+
Object.defineProperty(window, 'outerWidth', { get: function() { return window.innerWidth || 1920; } });
|
|
1671
|
+
if (window.outerHeight === 0)
|
|
1672
|
+
Object.defineProperty(window, 'outerHeight', { get: function() { return (window.innerHeight || 1080) + 85; } });
|
|
1673
|
+
});
|
|
1674
|
+
|
|
1675
|
+
p(function() {
|
|
1676
|
+
if (screen.colorDepth === 0) {
|
|
1677
|
+
Object.defineProperty(screen, 'colorDepth', { get: function() { return 24; } });
|
|
1678
|
+
Object.defineProperty(screen, 'pixelDepth', { get: function() { return 24; } });
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
// \u2500\u2500 12. navigator.hardwareConcurrency \u2500\u2500
|
|
1683
|
+
p(function() {
|
|
1684
|
+
if (!navigator.hardwareConcurrency)
|
|
1685
|
+
Object.defineProperty(navigator, 'hardwareConcurrency', { get: function() { return 4; } });
|
|
1686
|
+
});
|
|
1687
|
+
|
|
1688
|
+
// \u2500\u2500 13. navigator.deviceMemory \u2500\u2500
|
|
1689
|
+
p(function() {
|
|
1690
|
+
if (!navigator.deviceMemory)
|
|
1691
|
+
Object.defineProperty(navigator, 'deviceMemory', { get: function() { return 8; } });
|
|
1692
|
+
});
|
|
1693
|
+
})()`;
|
|
1694
|
+
|
|
1695
|
+
// src/connection.ts
|
|
1444
1696
|
var BrowserTabNotFoundError = class extends Error {
|
|
1445
1697
|
constructor(message = "Tab not found") {
|
|
1446
1698
|
super(message);
|
|
1447
1699
|
this.name = "BrowserTabNotFoundError";
|
|
1448
1700
|
}
|
|
1449
1701
|
};
|
|
1450
|
-
var OPENCLAW_EXTENSION_RELAY_BROWSER = "OpenClaw/extension-relay";
|
|
1451
|
-
var extensionRelayByCdpUrl = /* @__PURE__ */ new Map();
|
|
1452
1702
|
async function fetchJsonForCdp(url, timeoutMs) {
|
|
1453
1703
|
const ctrl = new AbortController();
|
|
1454
|
-
const t = setTimeout(() =>
|
|
1704
|
+
const t = setTimeout(() => {
|
|
1705
|
+
ctrl.abort();
|
|
1706
|
+
}, timeoutMs);
|
|
1455
1707
|
try {
|
|
1456
1708
|
const res = await fetch(url, { signal: ctrl.signal });
|
|
1457
1709
|
if (!res.ok) return null;
|
|
@@ -1471,20 +1723,6 @@ function appendCdpPath2(cdpUrl, cdpPath) {
|
|
|
1471
1723
|
return `${cdpUrl.replace(/\/$/, "")}${cdpPath}`;
|
|
1472
1724
|
}
|
|
1473
1725
|
}
|
|
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
1726
|
async function withPlaywrightPageCdpSession(page, fn) {
|
|
1489
1727
|
const session = await page.context().newCDPSession(page);
|
|
1490
1728
|
try {
|
|
@@ -1501,7 +1739,7 @@ async function withPageScopedCdpClient(opts) {
|
|
|
1501
1739
|
}
|
|
1502
1740
|
var LOOPBACK_ENTRIES = "localhost,127.0.0.1,[::1]";
|
|
1503
1741
|
function noProxyAlreadyCoversLocalhost() {
|
|
1504
|
-
const current = process.env.NO_PROXY
|
|
1742
|
+
const current = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
|
|
1505
1743
|
return current.includes("localhost") && current.includes("127.0.0.1") && current.includes("[::1]");
|
|
1506
1744
|
}
|
|
1507
1745
|
function isLoopbackCdpUrl(url) {
|
|
@@ -1519,7 +1757,7 @@ var NoProxyLeaseManager = class {
|
|
|
1519
1757
|
if (this.leaseCount === 0 && !noProxyAlreadyCoversLocalhost()) {
|
|
1520
1758
|
const noProxy = process.env.NO_PROXY;
|
|
1521
1759
|
const noProxyLower = process.env.no_proxy;
|
|
1522
|
-
const current = noProxy
|
|
1760
|
+
const current = noProxy ?? noProxyLower ?? "";
|
|
1523
1761
|
const applied = current ? `${current},${LOOPBACK_ENTRIES}` : LOOPBACK_ENTRIES;
|
|
1524
1762
|
process.env.NO_PROXY = applied;
|
|
1525
1763
|
process.env.no_proxy = applied;
|
|
@@ -1564,9 +1802,12 @@ function getHeadersWithAuth(endpoint, baseHeaders = {}) {
|
|
|
1564
1802
|
const headers = { ...baseHeaders };
|
|
1565
1803
|
try {
|
|
1566
1804
|
const parsed = new URL(endpoint);
|
|
1567
|
-
if (
|
|
1568
|
-
|
|
1569
|
-
|
|
1805
|
+
if (Object.keys(headers).some((k) => k.toLowerCase() === "authorization")) return headers;
|
|
1806
|
+
if (parsed.username || parsed.password) {
|
|
1807
|
+
const credentials = Buffer.from(
|
|
1808
|
+
`${decodeURIComponent(parsed.username)}:${decodeURIComponent(parsed.password)}`
|
|
1809
|
+
).toString("base64");
|
|
1810
|
+
headers.Authorization = `Basic ${credentials}`;
|
|
1570
1811
|
}
|
|
1571
1812
|
} catch {
|
|
1572
1813
|
}
|
|
@@ -1614,7 +1855,7 @@ function roleRefsKey(cdpUrl, targetId) {
|
|
|
1614
1855
|
function findNetworkRequestById(state, id) {
|
|
1615
1856
|
for (let i = state.requests.length - 1; i >= 0; i--) {
|
|
1616
1857
|
const candidate = state.requests[i];
|
|
1617
|
-
if (candidate
|
|
1858
|
+
if (candidate.id === id) return candidate;
|
|
1618
1859
|
}
|
|
1619
1860
|
return void 0;
|
|
1620
1861
|
}
|
|
@@ -1645,16 +1886,16 @@ function ensurePageState(page) {
|
|
|
1645
1886
|
});
|
|
1646
1887
|
page.on("pageerror", (err) => {
|
|
1647
1888
|
state.errors.push({
|
|
1648
|
-
message: err
|
|
1649
|
-
name: err
|
|
1650
|
-
stack: err
|
|
1889
|
+
message: err.message !== "" ? err.message : String(err),
|
|
1890
|
+
name: err.name !== "" ? err.name : void 0,
|
|
1891
|
+
stack: err.stack !== void 0 && err.stack !== "" ? err.stack : void 0,
|
|
1651
1892
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1652
1893
|
});
|
|
1653
1894
|
if (state.errors.length > MAX_PAGE_ERRORS) state.errors.shift();
|
|
1654
1895
|
});
|
|
1655
1896
|
page.on("request", (req) => {
|
|
1656
1897
|
state.nextRequestId += 1;
|
|
1657
|
-
const id = `r${state.nextRequestId}`;
|
|
1898
|
+
const id = `r${String(state.nextRequestId)}`;
|
|
1658
1899
|
state.requestIds.set(req, id);
|
|
1659
1900
|
state.requests.push({
|
|
1660
1901
|
id,
|
|
@@ -1668,7 +1909,7 @@ function ensurePageState(page) {
|
|
|
1668
1909
|
page.on("response", (resp) => {
|
|
1669
1910
|
const req = resp.request();
|
|
1670
1911
|
const id = state.requestIds.get(req);
|
|
1671
|
-
if (
|
|
1912
|
+
if (id === void 0) return;
|
|
1672
1913
|
const rec = findNetworkRequestById(state, id);
|
|
1673
1914
|
if (rec) {
|
|
1674
1915
|
rec.status = resp.status();
|
|
@@ -1677,7 +1918,7 @@ function ensurePageState(page) {
|
|
|
1677
1918
|
});
|
|
1678
1919
|
page.on("requestfailed", (req) => {
|
|
1679
1920
|
const id = state.requestIds.get(req);
|
|
1680
|
-
if (
|
|
1921
|
+
if (id === void 0) return;
|
|
1681
1922
|
const rec = findNetworkRequestById(state, id);
|
|
1682
1923
|
if (rec) {
|
|
1683
1924
|
rec.failureText = req.failure()?.errorText;
|
|
@@ -1691,10 +1932,10 @@ function ensurePageState(page) {
|
|
|
1691
1932
|
}
|
|
1692
1933
|
return state;
|
|
1693
1934
|
}
|
|
1694
|
-
var STEALTH_SCRIPT = `Object.defineProperty(navigator, 'webdriver', { get: () => undefined })`;
|
|
1695
1935
|
function applyStealthToPage(page) {
|
|
1696
1936
|
page.evaluate(STEALTH_SCRIPT).catch((e) => {
|
|
1697
|
-
if (process.env.DEBUG
|
|
1937
|
+
if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
|
|
1938
|
+
console.warn("[browserclaw] stealth evaluate failed:", e instanceof Error ? e.message : String(e));
|
|
1698
1939
|
});
|
|
1699
1940
|
}
|
|
1700
1941
|
function observeContext(context) {
|
|
@@ -1702,7 +1943,8 @@ function observeContext(context) {
|
|
|
1702
1943
|
observedContexts.add(context);
|
|
1703
1944
|
ensureContextState(context);
|
|
1704
1945
|
context.addInitScript(STEALTH_SCRIPT).catch((e) => {
|
|
1705
|
-
if (process.env.DEBUG
|
|
1946
|
+
if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
|
|
1947
|
+
console.warn("[browserclaw] stealth initScript failed:", e instanceof Error ? e.message : String(e));
|
|
1706
1948
|
});
|
|
1707
1949
|
for (const page of context.pages()) {
|
|
1708
1950
|
ensurePageState(page);
|
|
@@ -1718,15 +1960,15 @@ function observeBrowser(browser) {
|
|
|
1718
1960
|
}
|
|
1719
1961
|
function rememberRoleRefsForTarget(opts) {
|
|
1720
1962
|
const targetId = opts.targetId.trim();
|
|
1721
|
-
if (
|
|
1963
|
+
if (targetId === "") return;
|
|
1722
1964
|
roleRefsByTarget.set(roleRefsKey(opts.cdpUrl, targetId), {
|
|
1723
1965
|
refs: opts.refs,
|
|
1724
|
-
...opts.frameSelector ? { frameSelector: opts.frameSelector } : {},
|
|
1725
|
-
...opts.mode ? { mode: opts.mode } : {}
|
|
1966
|
+
...opts.frameSelector !== void 0 && opts.frameSelector !== "" ? { frameSelector: opts.frameSelector } : {},
|
|
1967
|
+
...opts.mode !== void 0 ? { mode: opts.mode } : {}
|
|
1726
1968
|
});
|
|
1727
1969
|
while (roleRefsByTarget.size > MAX_ROLE_REFS_CACHE) {
|
|
1728
1970
|
const first = roleRefsByTarget.keys().next();
|
|
1729
|
-
if (first.done) break;
|
|
1971
|
+
if (first.done === true) break;
|
|
1730
1972
|
roleRefsByTarget.delete(first.value);
|
|
1731
1973
|
}
|
|
1732
1974
|
}
|
|
@@ -1735,7 +1977,7 @@ function storeRoleRefsForTarget(opts) {
|
|
|
1735
1977
|
state.roleRefs = opts.refs;
|
|
1736
1978
|
state.roleRefsFrameSelector = opts.frameSelector;
|
|
1737
1979
|
state.roleRefsMode = opts.mode;
|
|
1738
|
-
if (
|
|
1980
|
+
if (opts.targetId === void 0 || opts.targetId.trim() === "") return;
|
|
1739
1981
|
rememberRoleRefsForTarget({
|
|
1740
1982
|
cdpUrl: opts.cdpUrl,
|
|
1741
1983
|
targetId: opts.targetId,
|
|
@@ -1745,8 +1987,8 @@ function storeRoleRefsForTarget(opts) {
|
|
|
1745
1987
|
});
|
|
1746
1988
|
}
|
|
1747
1989
|
function restoreRoleRefsForTarget(opts) {
|
|
1748
|
-
const targetId = opts.targetId?.trim()
|
|
1749
|
-
if (
|
|
1990
|
+
const targetId = opts.targetId?.trim() ?? "";
|
|
1991
|
+
if (targetId === "") return;
|
|
1750
1992
|
const entry = roleRefsByTarget.get(roleRefsKey(opts.cdpUrl, targetId));
|
|
1751
1993
|
if (!entry) return;
|
|
1752
1994
|
const state = ensurePageState(opts.page);
|
|
@@ -1768,8 +2010,12 @@ async function connectBrowser(cdpUrl, authToken) {
|
|
|
1768
2010
|
const timeout = 5e3 + attempt * 2e3;
|
|
1769
2011
|
const endpoint = await getChromeWebSocketUrl(normalized, timeout, authToken).catch(() => null) ?? normalized;
|
|
1770
2012
|
const headers = getHeadersWithAuth(endpoint);
|
|
1771
|
-
if (authToken &&
|
|
1772
|
-
|
|
2013
|
+
if (authToken !== void 0 && authToken !== "" && !headers.Authorization)
|
|
2014
|
+
headers.Authorization = `Bearer ${authToken}`;
|
|
2015
|
+
const browser = await withNoProxyForCdpUrl(
|
|
2016
|
+
endpoint,
|
|
2017
|
+
() => playwrightCore.chromium.connectOverCDP(endpoint, { timeout, headers })
|
|
2018
|
+
);
|
|
1773
2019
|
const onDisconnected = () => {
|
|
1774
2020
|
if (cachedByCdpUrl.get(normalized)?.browser === browser) {
|
|
1775
2021
|
cachedByCdpUrl.delete(normalized);
|
|
@@ -1823,7 +2069,9 @@ function cdpSocketNeedsAttach(wsUrl) {
|
|
|
1823
2069
|
async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
1824
2070
|
const httpBase = normalizeCdpHttpBaseForJsonEndpoints(cdpUrl);
|
|
1825
2071
|
const ctrl = new AbortController();
|
|
1826
|
-
const t = setTimeout(() =>
|
|
2072
|
+
const t = setTimeout(() => {
|
|
2073
|
+
ctrl.abort();
|
|
2074
|
+
}, 2e3);
|
|
1827
2075
|
let targets;
|
|
1828
2076
|
try {
|
|
1829
2077
|
const res = await fetch(`${httpBase}/json/list`, { signal: ctrl.signal });
|
|
@@ -1835,9 +2083,12 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
1835
2083
|
clearTimeout(t);
|
|
1836
2084
|
}
|
|
1837
2085
|
if (!Array.isArray(targets)) return;
|
|
1838
|
-
const target = targets.find((entry) =>
|
|
1839
|
-
|
|
1840
|
-
|
|
2086
|
+
const target = targets.find((entry) => {
|
|
2087
|
+
const e = entry;
|
|
2088
|
+
return (e.id ?? "").trim() === targetId;
|
|
2089
|
+
});
|
|
2090
|
+
const wsUrlRaw = (target?.webSocketDebuggerUrl ?? "").trim();
|
|
2091
|
+
if (wsUrlRaw === "") return;
|
|
1841
2092
|
const wsUrl = normalizeCdpWsUrl(wsUrlRaw, httpBase);
|
|
1842
2093
|
const needsAttach = cdpSocketNeedsAttach(wsUrl);
|
|
1843
2094
|
await new Promise((resolve2) => {
|
|
@@ -1873,10 +2124,18 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
1873
2124
|
if (!needsAttach) return;
|
|
1874
2125
|
try {
|
|
1875
2126
|
const msg = JSON.parse(String(event.data));
|
|
1876
|
-
|
|
1877
|
-
|
|
2127
|
+
const result = msg.result;
|
|
2128
|
+
if (msg.id !== void 0 && result?.sessionId !== void 0) {
|
|
2129
|
+
const sessionId = result.sessionId;
|
|
2130
|
+
ws.send(JSON.stringify({ id: nextId++, sessionId, method: "Runtime.terminateExecution" }));
|
|
1878
2131
|
try {
|
|
1879
|
-
ws.send(
|
|
2132
|
+
ws.send(
|
|
2133
|
+
JSON.stringify({
|
|
2134
|
+
id: nextId++,
|
|
2135
|
+
method: "Target.detachFromTarget",
|
|
2136
|
+
params: { sessionId }
|
|
2137
|
+
})
|
|
2138
|
+
);
|
|
1880
2139
|
} catch {
|
|
1881
2140
|
}
|
|
1882
2141
|
setTimeout(finish, 300);
|
|
@@ -1884,8 +2143,12 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
1884
2143
|
} catch {
|
|
1885
2144
|
}
|
|
1886
2145
|
};
|
|
1887
|
-
ws.onerror = () =>
|
|
1888
|
-
|
|
2146
|
+
ws.onerror = () => {
|
|
2147
|
+
finish();
|
|
2148
|
+
};
|
|
2149
|
+
ws.onclose = () => {
|
|
2150
|
+
finish();
|
|
2151
|
+
};
|
|
1889
2152
|
});
|
|
1890
2153
|
}
|
|
1891
2154
|
async function forceDisconnectPlaywrightForTarget(opts) {
|
|
@@ -1897,15 +2160,15 @@ async function forceDisconnectPlaywrightForTarget(opts) {
|
|
|
1897
2160
|
if (cur.onDisconnected && typeof cur.browser.off === "function") {
|
|
1898
2161
|
cur.browser.off("disconnected", cur.onDisconnected);
|
|
1899
2162
|
}
|
|
1900
|
-
const targetId = opts.targetId?.trim()
|
|
1901
|
-
if (targetId) {
|
|
2163
|
+
const targetId = opts.targetId?.trim() ?? "";
|
|
2164
|
+
if (targetId !== "") {
|
|
1902
2165
|
await tryTerminateExecutionViaCdp(normalized, targetId).catch(() => {
|
|
1903
2166
|
});
|
|
1904
2167
|
}
|
|
1905
2168
|
cur.browser.close().catch(() => {
|
|
1906
2169
|
});
|
|
1907
2170
|
}
|
|
1908
|
-
|
|
2171
|
+
function getAllPages(browser) {
|
|
1909
2172
|
return browser.contexts().flatMap((c) => c.pages());
|
|
1910
2173
|
}
|
|
1911
2174
|
async function pageTargetId(page) {
|
|
@@ -1913,7 +2176,7 @@ async function pageTargetId(page) {
|
|
|
1913
2176
|
try {
|
|
1914
2177
|
const info = await session.send("Target.getTargetInfo");
|
|
1915
2178
|
const targetInfo = info.targetInfo;
|
|
1916
|
-
return
|
|
2179
|
+
return (targetInfo?.targetId ?? "").trim() || null;
|
|
1917
2180
|
} finally {
|
|
1918
2181
|
await session.detach().catch(() => {
|
|
1919
2182
|
});
|
|
@@ -1934,21 +2197,15 @@ function matchPageByTargetList(pages, targets, targetId) {
|
|
|
1934
2197
|
return null;
|
|
1935
2198
|
}
|
|
1936
2199
|
async function findPageByTargetIdViaTargetList(pages, targetId, cdpUrl) {
|
|
1937
|
-
const targets = await fetchJsonForCdp(
|
|
2200
|
+
const targets = await fetchJsonForCdp(
|
|
2201
|
+
appendCdpPath2(normalizeCdpHttpBaseForJsonEndpoints(cdpUrl), "/json/list"),
|
|
2202
|
+
2e3
|
|
2203
|
+
);
|
|
1938
2204
|
if (!Array.isArray(targets)) return null;
|
|
1939
2205
|
return matchPageByTargetList(pages, targets, targetId);
|
|
1940
2206
|
}
|
|
1941
2207
|
async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
1942
|
-
const pages =
|
|
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
|
-
}
|
|
2208
|
+
const pages = getAllPages(browser);
|
|
1952
2209
|
let resolvedViaCdp = false;
|
|
1953
2210
|
for (const page of pages) {
|
|
1954
2211
|
let tid = null;
|
|
@@ -1958,9 +2215,9 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
|
1958
2215
|
} catch {
|
|
1959
2216
|
tid = null;
|
|
1960
2217
|
}
|
|
1961
|
-
if (tid && tid === targetId) return page;
|
|
2218
|
+
if (tid !== null && tid !== "" && tid === targetId) return page;
|
|
1962
2219
|
}
|
|
1963
|
-
if (cdpUrl) {
|
|
2220
|
+
if (cdpUrl !== void 0 && cdpUrl !== "") {
|
|
1964
2221
|
try {
|
|
1965
2222
|
return await findPageByTargetIdViaTargetList(pages, targetId, cdpUrl);
|
|
1966
2223
|
} catch {
|
|
@@ -1971,14 +2228,16 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
|
1971
2228
|
}
|
|
1972
2229
|
async function getPageForTargetId(opts) {
|
|
1973
2230
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
1974
|
-
const pages =
|
|
2231
|
+
const pages = getAllPages(browser);
|
|
1975
2232
|
if (!pages.length) throw new Error("No pages available in the connected browser.");
|
|
1976
2233
|
const first = pages[0];
|
|
1977
|
-
if (
|
|
2234
|
+
if (opts.targetId === void 0 || opts.targetId === "") return first;
|
|
1978
2235
|
const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
|
|
1979
2236
|
if (!found) {
|
|
1980
2237
|
if (pages.length === 1) return first;
|
|
1981
|
-
throw new BrowserTabNotFoundError(
|
|
2238
|
+
throw new BrowserTabNotFoundError(
|
|
2239
|
+
`Tab not found (targetId: ${opts.targetId}). Call browser.tabs() to list open tabs.`
|
|
2240
|
+
);
|
|
1982
2241
|
}
|
|
1983
2242
|
return found;
|
|
1984
2243
|
}
|
|
@@ -2012,7 +2271,7 @@ function resolveInteractionTimeoutMs(timeoutMs) {
|
|
|
2012
2271
|
function resolveBoundedDelayMs(value, label, maxMs) {
|
|
2013
2272
|
const normalized = Math.floor(value ?? 0);
|
|
2014
2273
|
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`);
|
|
2274
|
+
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${String(maxMs)}ms`);
|
|
2016
2275
|
return normalized;
|
|
2017
2276
|
}
|
|
2018
2277
|
async function getRestoredPageForTarget(opts) {
|
|
@@ -2023,17 +2282,17 @@ async function getRestoredPageForTarget(opts) {
|
|
|
2023
2282
|
}
|
|
2024
2283
|
function refLocator(page, ref) {
|
|
2025
2284
|
const normalized = ref.startsWith("@") ? ref.slice(1) : ref.startsWith("ref=") ? ref.slice(4) : ref;
|
|
2026
|
-
if (
|
|
2285
|
+
if (normalized.trim() === "") throw new Error("ref is required");
|
|
2027
2286
|
if (/^e\d+$/.test(normalized)) {
|
|
2028
2287
|
const state = pageStates.get(page);
|
|
2029
2288
|
if (state?.roleRefsMode === "aria") {
|
|
2030
|
-
return (state.roleRefsFrameSelector ? page.frameLocator(state.roleRefsFrameSelector) : page).locator(`aria-ref=${normalized}`);
|
|
2289
|
+
return (state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page).locator(`aria-ref=${normalized}`);
|
|
2031
2290
|
}
|
|
2032
2291
|
const info = state?.roleRefs?.[normalized];
|
|
2033
2292
|
if (!info) throw new Error(`Unknown ref "${normalized}". Run a new snapshot and use a ref from that snapshot.`);
|
|
2034
|
-
const locAny = state
|
|
2293
|
+
const locAny = state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page;
|
|
2035
2294
|
const role = info.role;
|
|
2036
|
-
const locator = info.name ? locAny.getByRole(role, { name: info.name, exact: true }) : locAny.getByRole(role);
|
|
2295
|
+
const locator = info.name !== void 0 && info.name !== "" ? locAny.getByRole(role, { name: info.name, exact: true }) : locAny.getByRole(role);
|
|
2037
2296
|
return info.nth !== void 0 ? locator.nth(info.nth) : locator;
|
|
2038
2297
|
}
|
|
2039
2298
|
return page.locator(`aria-ref=${normalized}`);
|
|
@@ -2041,15 +2300,21 @@ function refLocator(page, ref) {
|
|
|
2041
2300
|
function toAIFriendlyError(error, selector) {
|
|
2042
2301
|
const message = error instanceof Error ? error.message : String(error);
|
|
2043
2302
|
if (message.includes("strict mode violation")) {
|
|
2044
|
-
const countMatch =
|
|
2303
|
+
const countMatch = /resolved to (\d+) elements/.exec(message);
|
|
2045
2304
|
const count = countMatch ? countMatch[1] : "multiple";
|
|
2046
|
-
return new Error(
|
|
2305
|
+
return new Error(
|
|
2306
|
+
`Selector "${selector}" matched ${count} elements. Run a new snapshot to get updated refs, or use a different ref.`
|
|
2307
|
+
);
|
|
2047
2308
|
}
|
|
2048
2309
|
if ((message.includes("Timeout") || message.includes("waiting for")) && (message.includes("to be visible") || message.includes("not visible"))) {
|
|
2049
|
-
return new Error(
|
|
2310
|
+
return new Error(
|
|
2311
|
+
`Element "${selector}" not found or not visible. Run a new snapshot to see current page elements.`
|
|
2312
|
+
);
|
|
2050
2313
|
}
|
|
2051
2314
|
if (message.includes("intercepts pointer events") || message.includes("not visible") || message.includes("not receive pointer events")) {
|
|
2052
|
-
return new Error(
|
|
2315
|
+
return new Error(
|
|
2316
|
+
`Element "${selector}" is not interactable (hidden or covered). Try scrolling it into view, closing overlays, or re-snapshotting.`
|
|
2317
|
+
);
|
|
2053
2318
|
}
|
|
2054
2319
|
return error instanceof Error ? error : new Error(message);
|
|
2055
2320
|
}
|
|
@@ -2057,437 +2322,151 @@ function normalizeTimeoutMs(timeoutMs, fallback, maxMs = 12e4) {
|
|
|
2057
2322
|
return Math.max(500, Math.min(maxMs, timeoutMs ?? fallback));
|
|
2058
2323
|
}
|
|
2059
2324
|
|
|
2060
|
-
// src/
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
"
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
"article",
|
|
2088
|
-
"region",
|
|
2089
|
-
"main",
|
|
2090
|
-
"navigation"
|
|
2091
|
-
]);
|
|
2092
|
-
var STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
|
|
2093
|
-
"generic",
|
|
2094
|
-
"group",
|
|
2095
|
-
"list",
|
|
2096
|
-
"table",
|
|
2097
|
-
"row",
|
|
2098
|
-
"rowgroup",
|
|
2099
|
-
"grid",
|
|
2100
|
-
"treegrid",
|
|
2101
|
-
"menu",
|
|
2102
|
-
"menubar",
|
|
2103
|
-
"toolbar",
|
|
2104
|
-
"tablist",
|
|
2105
|
-
"tree",
|
|
2106
|
-
"directory",
|
|
2107
|
-
"document",
|
|
2108
|
-
"application",
|
|
2109
|
-
"presentation",
|
|
2110
|
-
"none"
|
|
2111
|
-
]);
|
|
2112
|
-
function getIndentLevel(line) {
|
|
2113
|
-
const match = line.match(/^(\s*)/);
|
|
2114
|
-
return match ? Math.floor(match[1].length / 2) : 0;
|
|
2325
|
+
// src/actions/evaluate.ts
|
|
2326
|
+
async function evaluateInAllFramesViaPlaywright(opts) {
|
|
2327
|
+
const fnText = opts.fn.trim();
|
|
2328
|
+
if (!fnText) throw new Error("function is required");
|
|
2329
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2330
|
+
const frames = page.frames();
|
|
2331
|
+
const results = [];
|
|
2332
|
+
for (const frame of frames) {
|
|
2333
|
+
try {
|
|
2334
|
+
const result = await frame.evaluate((fnBody) => {
|
|
2335
|
+
"use strict";
|
|
2336
|
+
try {
|
|
2337
|
+
const candidate = (0, eval)("(" + fnBody + ")");
|
|
2338
|
+
return typeof candidate === "function" ? candidate() : candidate;
|
|
2339
|
+
} catch (err) {
|
|
2340
|
+
throw new Error("Invalid evaluate function: " + (err instanceof Error ? err.message : String(err)));
|
|
2341
|
+
}
|
|
2342
|
+
}, fnText);
|
|
2343
|
+
results.push({
|
|
2344
|
+
frameUrl: frame.url(),
|
|
2345
|
+
frameName: frame.name(),
|
|
2346
|
+
result
|
|
2347
|
+
});
|
|
2348
|
+
} catch {
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
return results;
|
|
2115
2352
|
}
|
|
2116
|
-
function
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
return
|
|
2353
|
+
async function awaitEvalWithAbort(evalPromise, abortPromise) {
|
|
2354
|
+
if (!abortPromise) return await evalPromise;
|
|
2355
|
+
try {
|
|
2356
|
+
return await Promise.race([evalPromise, abortPromise]);
|
|
2357
|
+
} catch (err) {
|
|
2358
|
+
evalPromise.catch(() => {
|
|
2359
|
+
});
|
|
2360
|
+
throw err;
|
|
2120
2361
|
}
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2362
|
+
}
|
|
2363
|
+
var BROWSER_EVALUATOR = new Function(
|
|
2364
|
+
"args",
|
|
2365
|
+
`
|
|
2366
|
+
"use strict";
|
|
2367
|
+
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
2368
|
+
try {
|
|
2369
|
+
var candidate = eval("(" + fnBody + ")");
|
|
2370
|
+
var result = typeof candidate === "function" ? candidate() : candidate;
|
|
2371
|
+
if (result && typeof result.then === "function") {
|
|
2372
|
+
return Promise.race([
|
|
2373
|
+
result,
|
|
2374
|
+
new Promise(function(_, reject) {
|
|
2375
|
+
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
2376
|
+
})
|
|
2377
|
+
]);
|
|
2378
|
+
}
|
|
2379
|
+
return result;
|
|
2380
|
+
} catch (err) {
|
|
2381
|
+
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
2124
2382
|
}
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2383
|
+
`
|
|
2384
|
+
);
|
|
2385
|
+
var ELEMENT_EVALUATOR = new Function(
|
|
2386
|
+
"el",
|
|
2387
|
+
"args",
|
|
2388
|
+
`
|
|
2389
|
+
"use strict";
|
|
2390
|
+
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
2391
|
+
try {
|
|
2392
|
+
var candidate = eval("(" + fnBody + ")");
|
|
2393
|
+
var result = typeof candidate === "function" ? candidate(el) : candidate;
|
|
2394
|
+
if (result && typeof result.then === "function") {
|
|
2395
|
+
return Promise.race([
|
|
2396
|
+
result,
|
|
2397
|
+
new Promise(function(_, reject) {
|
|
2398
|
+
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
2399
|
+
})
|
|
2400
|
+
]);
|
|
2401
|
+
}
|
|
2402
|
+
return result;
|
|
2403
|
+
} catch (err) {
|
|
2404
|
+
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
2128
2405
|
}
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
suffix
|
|
2135
|
-
};
|
|
2136
|
-
}
|
|
2137
|
-
function createRoleNameTracker() {
|
|
2138
|
-
const counts = /* @__PURE__ */ new Map();
|
|
2139
|
-
const refsByKey = /* @__PURE__ */ new Map();
|
|
2140
|
-
return {
|
|
2141
|
-
counts,
|
|
2142
|
-
refsByKey,
|
|
2143
|
-
getKey(role, name) {
|
|
2144
|
-
return `${role}:${name ?? ""}`;
|
|
2145
|
-
},
|
|
2146
|
-
getNextIndex(role, name) {
|
|
2147
|
-
const key = this.getKey(role, name);
|
|
2148
|
-
const current = counts.get(key) ?? 0;
|
|
2149
|
-
counts.set(key, current + 1);
|
|
2150
|
-
return current;
|
|
2151
|
-
},
|
|
2152
|
-
trackRef(role, name, ref) {
|
|
2153
|
-
const key = this.getKey(role, name);
|
|
2154
|
-
const list = refsByKey.get(key) ?? [];
|
|
2155
|
-
list.push(ref);
|
|
2156
|
-
refsByKey.set(key, list);
|
|
2157
|
-
},
|
|
2158
|
-
getDuplicateKeys() {
|
|
2159
|
-
const out = /* @__PURE__ */ new Set();
|
|
2160
|
-
for (const [key, refs] of refsByKey) if (refs.length > 1) out.add(key);
|
|
2161
|
-
return out;
|
|
2162
|
-
}
|
|
2163
|
-
};
|
|
2164
|
-
}
|
|
2165
|
-
function removeNthFromNonDuplicates(refs, tracker) {
|
|
2166
|
-
const duplicates = tracker.getDuplicateKeys();
|
|
2167
|
-
for (const [ref, data] of Object.entries(refs)) {
|
|
2168
|
-
const key = tracker.getKey(data.role, data.name);
|
|
2169
|
-
if (!duplicates.has(key)) delete refs[ref]?.nth;
|
|
2170
|
-
}
|
|
2171
|
-
}
|
|
2172
|
-
function compactTree(tree) {
|
|
2173
|
-
const lines = tree.split("\n");
|
|
2174
|
-
const result = [];
|
|
2175
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2176
|
-
const line = lines[i];
|
|
2177
|
-
if (line.includes("[ref=")) {
|
|
2178
|
-
result.push(line);
|
|
2179
|
-
continue;
|
|
2180
|
-
}
|
|
2181
|
-
if (line.includes(":") && !line.trimEnd().endsWith(":")) {
|
|
2182
|
-
result.push(line);
|
|
2183
|
-
continue;
|
|
2184
|
-
}
|
|
2185
|
-
const currentIndent = getIndentLevel(line);
|
|
2186
|
-
let hasRelevantChildren = false;
|
|
2187
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
2188
|
-
if (getIndentLevel(lines[j]) <= currentIndent) break;
|
|
2189
|
-
if (lines[j]?.includes("[ref=")) {
|
|
2190
|
-
hasRelevantChildren = true;
|
|
2191
|
-
break;
|
|
2192
|
-
}
|
|
2193
|
-
}
|
|
2194
|
-
if (hasRelevantChildren) result.push(line);
|
|
2195
|
-
}
|
|
2196
|
-
return result.join("\n");
|
|
2197
|
-
}
|
|
2198
|
-
function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
|
|
2199
|
-
const lines = ariaSnapshot.split("\n");
|
|
2200
|
-
const refs = {};
|
|
2201
|
-
const tracker = createRoleNameTracker();
|
|
2202
|
-
let counter = 0;
|
|
2203
|
-
const nextRef = () => {
|
|
2204
|
-
counter++;
|
|
2205
|
-
return `e${counter}`;
|
|
2206
|
-
};
|
|
2207
|
-
if (options.interactive) {
|
|
2208
|
-
const result2 = [];
|
|
2209
|
-
for (const line of lines) {
|
|
2210
|
-
const parsed = matchInteractiveSnapshotLine(line, options);
|
|
2211
|
-
if (!parsed) continue;
|
|
2212
|
-
const { roleRaw, role, name, suffix } = parsed;
|
|
2213
|
-
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
2214
|
-
const prefix = line.match(/^(\s*-\s*)/)?.[1] ?? "";
|
|
2215
|
-
const ref = nextRef();
|
|
2216
|
-
const nth = tracker.getNextIndex(role, name);
|
|
2217
|
-
tracker.trackRef(role, name, ref);
|
|
2218
|
-
refs[ref] = { role, name, nth };
|
|
2219
|
-
let enhanced = `${prefix}${roleRaw}`;
|
|
2220
|
-
if (name) enhanced += ` "${name}"`;
|
|
2221
|
-
enhanced += ` [ref=${ref}]`;
|
|
2222
|
-
if (nth > 0) enhanced += ` [nth=${nth}]`;
|
|
2223
|
-
if (suffix.includes("[")) enhanced += suffix;
|
|
2224
|
-
result2.push(enhanced);
|
|
2225
|
-
}
|
|
2226
|
-
removeNthFromNonDuplicates(refs, tracker);
|
|
2227
|
-
return { snapshot: result2.join("\n") || "(no interactive elements)", refs };
|
|
2228
|
-
}
|
|
2229
|
-
const result = [];
|
|
2230
|
-
for (const line of lines) {
|
|
2231
|
-
const depth = getIndentLevel(line);
|
|
2232
|
-
if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
|
|
2233
|
-
const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
|
|
2234
|
-
if (!match) {
|
|
2235
|
-
result.push(line);
|
|
2236
|
-
continue;
|
|
2237
|
-
}
|
|
2238
|
-
const [, prefix, roleRaw, name, suffix] = match;
|
|
2239
|
-
if (roleRaw.startsWith("/")) {
|
|
2240
|
-
result.push(line);
|
|
2241
|
-
continue;
|
|
2242
|
-
}
|
|
2243
|
-
const role = roleRaw.toLowerCase();
|
|
2244
|
-
const isInteractive = INTERACTIVE_ROLES.has(role);
|
|
2245
|
-
const isContent = CONTENT_ROLES.has(role);
|
|
2246
|
-
const isStructural = STRUCTURAL_ROLES.has(role);
|
|
2247
|
-
if (options.compact && isStructural && !name) continue;
|
|
2248
|
-
if (!(isInteractive || isContent && name)) {
|
|
2249
|
-
result.push(line);
|
|
2250
|
-
continue;
|
|
2251
|
-
}
|
|
2252
|
-
const ref = nextRef();
|
|
2253
|
-
const nth = tracker.getNextIndex(role, name);
|
|
2254
|
-
tracker.trackRef(role, name, ref);
|
|
2255
|
-
refs[ref] = { role, name, nth };
|
|
2256
|
-
let enhanced = `${prefix}${roleRaw}`;
|
|
2257
|
-
if (name) enhanced += ` "${name}"`;
|
|
2258
|
-
enhanced += ` [ref=${ref}]`;
|
|
2259
|
-
if (nth > 0) enhanced += ` [nth=${nth}]`;
|
|
2260
|
-
if (suffix) enhanced += suffix;
|
|
2261
|
-
result.push(enhanced);
|
|
2262
|
-
}
|
|
2263
|
-
removeNthFromNonDuplicates(refs, tracker);
|
|
2264
|
-
const tree = result.join("\n") || "(empty)";
|
|
2265
|
-
return { snapshot: options.compact ? compactTree(tree) : tree, refs };
|
|
2266
|
-
}
|
|
2267
|
-
function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
|
|
2268
|
-
const lines = String(aiSnapshot ?? "").split("\n");
|
|
2269
|
-
const refs = {};
|
|
2270
|
-
function parseAiSnapshotRef(suffix) {
|
|
2271
|
-
const match = suffix.match(/\[ref=(e\d+)\]/i);
|
|
2272
|
-
return match ? match[1] : null;
|
|
2273
|
-
}
|
|
2274
|
-
if (options.interactive) {
|
|
2275
|
-
const out2 = [];
|
|
2276
|
-
for (const line of lines) {
|
|
2277
|
-
const parsed = matchInteractiveSnapshotLine(line, options);
|
|
2278
|
-
if (!parsed) continue;
|
|
2279
|
-
const { roleRaw, role, name, suffix } = parsed;
|
|
2280
|
-
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
2281
|
-
const ref = parseAiSnapshotRef(suffix);
|
|
2282
|
-
if (!ref) continue;
|
|
2283
|
-
const prefix = line.match(/^(\s*-\s*)/)?.[1] ?? "";
|
|
2284
|
-
refs[ref] = { role, ...name ? { name } : {} };
|
|
2285
|
-
out2.push(`${prefix}${roleRaw}${name ? ` "${name}"` : ""}${suffix}`);
|
|
2286
|
-
}
|
|
2287
|
-
return { snapshot: out2.join("\n") || "(no interactive elements)", refs };
|
|
2288
|
-
}
|
|
2289
|
-
const out = [];
|
|
2290
|
-
for (const line of lines) {
|
|
2291
|
-
const depth = getIndentLevel(line);
|
|
2292
|
-
if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
|
|
2293
|
-
const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
|
|
2294
|
-
if (!match) {
|
|
2295
|
-
out.push(line);
|
|
2296
|
-
continue;
|
|
2297
|
-
}
|
|
2298
|
-
const [, , roleRaw, name, suffix] = match;
|
|
2299
|
-
if (roleRaw.startsWith("/")) {
|
|
2300
|
-
out.push(line);
|
|
2301
|
-
continue;
|
|
2302
|
-
}
|
|
2303
|
-
const role = roleRaw.toLowerCase();
|
|
2304
|
-
const isStructural = STRUCTURAL_ROLES.has(role);
|
|
2305
|
-
if (options.compact && isStructural && !name) continue;
|
|
2306
|
-
const ref = parseAiSnapshotRef(suffix);
|
|
2307
|
-
if (ref) refs[ref] = { role, ...name ? { name } : {} };
|
|
2308
|
-
out.push(line);
|
|
2309
|
-
}
|
|
2310
|
-
const tree = out.join("\n") || "(empty)";
|
|
2311
|
-
return { snapshot: options.compact ? compactTree(tree) : tree, refs };
|
|
2312
|
-
}
|
|
2313
|
-
function getRoleSnapshotStats(snapshot, refs) {
|
|
2314
|
-
const interactive = Object.values(refs).filter((r) => INTERACTIVE_ROLES.has(r.role)).length;
|
|
2315
|
-
return {
|
|
2316
|
-
lines: snapshot.split("\n").length,
|
|
2317
|
-
chars: snapshot.length,
|
|
2318
|
-
refs: Object.keys(refs).length,
|
|
2319
|
-
interactive
|
|
2320
|
-
};
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
// src/snapshot/ai-snapshot.ts
|
|
2324
|
-
async function snapshotAi(opts) {
|
|
2325
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2326
|
-
ensurePageState(page);
|
|
2327
|
-
const maybe = page;
|
|
2328
|
-
if (!maybe._snapshotForAI) {
|
|
2329
|
-
throw new Error("Playwright _snapshotForAI is not available. Upgrade playwright-core to >= 1.50.");
|
|
2330
|
-
}
|
|
2331
|
-
const sourceUrl = page.url();
|
|
2332
|
-
const result = await maybe._snapshotForAI({
|
|
2333
|
-
timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3, 6e4),
|
|
2334
|
-
track: "response"
|
|
2335
|
-
});
|
|
2336
|
-
let snapshot = String(result?.full ?? "");
|
|
2337
|
-
const maxChars = opts.maxChars;
|
|
2338
|
-
const limit = typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : void 0;
|
|
2339
|
-
let truncated = false;
|
|
2340
|
-
if (limit && snapshot.length > limit) {
|
|
2341
|
-
const lastNewline = snapshot.lastIndexOf("\n", limit);
|
|
2342
|
-
const cutoff = lastNewline > 0 ? lastNewline : limit;
|
|
2343
|
-
snapshot = `${snapshot.slice(0, cutoff)}
|
|
2344
|
-
|
|
2345
|
-
[...TRUNCATED - page too large]`;
|
|
2346
|
-
truncated = true;
|
|
2347
|
-
}
|
|
2348
|
-
const built = buildRoleSnapshotFromAiSnapshot(snapshot, opts.options);
|
|
2349
|
-
storeRoleRefsForTarget({
|
|
2350
|
-
page,
|
|
2351
|
-
cdpUrl: opts.cdpUrl,
|
|
2352
|
-
targetId: opts.targetId,
|
|
2353
|
-
refs: built.refs,
|
|
2354
|
-
mode: "aria"
|
|
2355
|
-
});
|
|
2356
|
-
return {
|
|
2357
|
-
snapshot: built.snapshot,
|
|
2358
|
-
refs: built.refs,
|
|
2359
|
-
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
2360
|
-
...truncated ? { truncated } : {},
|
|
2361
|
-
untrusted: true,
|
|
2362
|
-
contentMeta: {
|
|
2363
|
-
sourceUrl,
|
|
2364
|
-
contentType: "browser-snapshot",
|
|
2365
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2366
|
-
}
|
|
2367
|
-
};
|
|
2368
|
-
}
|
|
2369
|
-
|
|
2370
|
-
// src/snapshot/aria-snapshot.ts
|
|
2371
|
-
async function snapshotRole(opts) {
|
|
2406
|
+
`
|
|
2407
|
+
);
|
|
2408
|
+
async function evaluateViaPlaywright(opts) {
|
|
2409
|
+
const fnText = opts.fn.trim();
|
|
2410
|
+
if (!fnText) throw new Error("function is required");
|
|
2372
2411
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2373
2412
|
ensurePageState(page);
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
cdpUrl: opts.cdpUrl,
|
|
2388
|
-
targetId: opts.targetId,
|
|
2389
|
-
refs: built2.refs,
|
|
2390
|
-
mode: "aria"
|
|
2413
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
2414
|
+
const outerTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
|
|
2415
|
+
let evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 500));
|
|
2416
|
+
evaluateTimeout = Math.min(evaluateTimeout, outerTimeout);
|
|
2417
|
+
const signal = opts.signal;
|
|
2418
|
+
let abortListener;
|
|
2419
|
+
let abortReject;
|
|
2420
|
+
let abortPromise;
|
|
2421
|
+
if (signal !== void 0) {
|
|
2422
|
+
abortPromise = new Promise((_, reject) => {
|
|
2423
|
+
abortReject = reject;
|
|
2424
|
+
});
|
|
2425
|
+
abortPromise.catch(() => {
|
|
2391
2426
|
});
|
|
2392
|
-
return {
|
|
2393
|
-
snapshot: built2.snapshot,
|
|
2394
|
-
refs: built2.refs,
|
|
2395
|
-
stats: getRoleSnapshotStats(built2.snapshot, built2.refs),
|
|
2396
|
-
untrusted: true,
|
|
2397
|
-
contentMeta: {
|
|
2398
|
-
sourceUrl,
|
|
2399
|
-
contentType: "browser-snapshot",
|
|
2400
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2401
|
-
}
|
|
2402
|
-
};
|
|
2403
2427
|
}
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
frameSelector: frameSelector || void 0,
|
|
2415
|
-
mode: "role"
|
|
2416
|
-
});
|
|
2417
|
-
return {
|
|
2418
|
-
snapshot: built.snapshot,
|
|
2419
|
-
refs: built.refs,
|
|
2420
|
-
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
2421
|
-
untrusted: true,
|
|
2422
|
-
contentMeta: {
|
|
2423
|
-
sourceUrl,
|
|
2424
|
-
contentType: "browser-snapshot",
|
|
2425
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2428
|
+
if (signal !== void 0) {
|
|
2429
|
+
const disconnect = () => {
|
|
2430
|
+
forceDisconnectPlaywrightForTarget({
|
|
2431
|
+
cdpUrl: opts.cdpUrl,
|
|
2432
|
+
targetId: opts.targetId}).catch(() => {
|
|
2433
|
+
});
|
|
2434
|
+
};
|
|
2435
|
+
if (signal.aborted) {
|
|
2436
|
+
disconnect();
|
|
2437
|
+
throw signal.reason ?? new Error("aborted");
|
|
2426
2438
|
}
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
await session.send("Accessibility.enable").catch(() => {
|
|
2436
|
-
});
|
|
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()
|
|
2439
|
+
abortListener = () => {
|
|
2440
|
+
disconnect();
|
|
2441
|
+
abortReject?.(signal.reason ?? new Error("aborted"));
|
|
2442
|
+
};
|
|
2443
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
2444
|
+
if (signal.aborted) {
|
|
2445
|
+
abortListener();
|
|
2446
|
+
throw signal.reason ?? new Error("aborted");
|
|
2446
2447
|
}
|
|
2447
|
-
}
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
}
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
for (const n of nodes) if (n.nodeId) byId.set(n.nodeId, n);
|
|
2459
|
-
const referenced = /* @__PURE__ */ new Set();
|
|
2460
|
-
for (const n of nodes) for (const c of n.childIds ?? []) referenced.add(c);
|
|
2461
|
-
const root = nodes.find((n) => n.nodeId && !referenced.has(n.nodeId)) ?? nodes[0];
|
|
2462
|
-
if (!root?.nodeId) return [];
|
|
2463
|
-
const out = [];
|
|
2464
|
-
const stack = [{ id: root.nodeId, depth: 0 }];
|
|
2465
|
-
while (stack.length && out.length < limit) {
|
|
2466
|
-
const popped = stack.pop();
|
|
2467
|
-
if (!popped) break;
|
|
2468
|
-
const { id, depth } = popped;
|
|
2469
|
-
const n = byId.get(id);
|
|
2470
|
-
if (!n) continue;
|
|
2471
|
-
const role = axValue(n.role);
|
|
2472
|
-
const name = axValue(n.name);
|
|
2473
|
-
const value = axValue(n.value);
|
|
2474
|
-
const description = axValue(n.description);
|
|
2475
|
-
const ref = `ax${out.length + 1}`;
|
|
2476
|
-
out.push({
|
|
2477
|
-
ref,
|
|
2478
|
-
role: role || "unknown",
|
|
2479
|
-
name: name || "",
|
|
2480
|
-
...value ? { value } : {},
|
|
2481
|
-
...description ? { description } : {},
|
|
2482
|
-
...typeof n.backendDOMNodeId === "number" ? { backendDOMNodeId: n.backendDOMNodeId } : {},
|
|
2483
|
-
depth
|
|
2484
|
-
});
|
|
2485
|
-
const children = (n.childIds ?? []).filter((c) => byId.has(c));
|
|
2486
|
-
for (let i = children.length - 1; i >= 0; i--) {
|
|
2487
|
-
if (children[i]) stack.push({ id: children[i], depth: depth + 1 });
|
|
2448
|
+
}
|
|
2449
|
+
try {
|
|
2450
|
+
if (opts.ref !== void 0 && opts.ref !== "") {
|
|
2451
|
+
const locator = refLocator(page, opts.ref);
|
|
2452
|
+
return await awaitEvalWithAbort(
|
|
2453
|
+
locator.evaluate(ELEMENT_EVALUATOR, {
|
|
2454
|
+
fnBody: fnText,
|
|
2455
|
+
timeoutMs: evaluateTimeout
|
|
2456
|
+
}),
|
|
2457
|
+
abortPromise
|
|
2458
|
+
);
|
|
2488
2459
|
}
|
|
2460
|
+
return await awaitEvalWithAbort(
|
|
2461
|
+
page.evaluate(BROWSER_EVALUATOR, {
|
|
2462
|
+
fnBody: fnText,
|
|
2463
|
+
timeoutMs: evaluateTimeout
|
|
2464
|
+
}),
|
|
2465
|
+
abortPromise
|
|
2466
|
+
);
|
|
2467
|
+
} finally {
|
|
2468
|
+
if (signal && abortListener) signal.removeEventListener("abort", abortListener);
|
|
2489
2469
|
}
|
|
2490
|
-
return out;
|
|
2491
2470
|
}
|
|
2492
2471
|
|
|
2493
2472
|
// src/security.ts
|
|
@@ -2504,11 +2483,7 @@ function withBrowserNavigationPolicy(ssrfPolicy) {
|
|
|
2504
2483
|
var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
2505
2484
|
var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
|
|
2506
2485
|
var PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"];
|
|
2507
|
-
var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
2508
|
-
"localhost",
|
|
2509
|
-
"localhost.localdomain",
|
|
2510
|
-
"metadata.google.internal"
|
|
2511
|
-
]);
|
|
2486
|
+
var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "localhost.localdomain", "metadata.google.internal"]);
|
|
2512
2487
|
function isAllowedNonNetworkNavigationUrl(parsed) {
|
|
2513
2488
|
return SAFE_NON_NETWORK_URLS.has(parsed.href);
|
|
2514
2489
|
}
|
|
@@ -2523,7 +2498,7 @@ function hasProxyEnvConfigured2(env = process.env) {
|
|
|
2523
2498
|
return false;
|
|
2524
2499
|
}
|
|
2525
2500
|
function normalizeHostname(hostname) {
|
|
2526
|
-
let h =
|
|
2501
|
+
let h = hostname.trim().toLowerCase();
|
|
2527
2502
|
if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
|
|
2528
2503
|
if (h.endsWith(".")) h = h.slice(0, -1);
|
|
2529
2504
|
return h;
|
|
@@ -2542,13 +2517,7 @@ var BLOCKED_IPV4_RANGES = /* @__PURE__ */ new Set([
|
|
|
2542
2517
|
"private",
|
|
2543
2518
|
"reserved"
|
|
2544
2519
|
]);
|
|
2545
|
-
var BLOCKED_IPV6_RANGES = /* @__PURE__ */ new Set([
|
|
2546
|
-
"unspecified",
|
|
2547
|
-
"loopback",
|
|
2548
|
-
"linkLocal",
|
|
2549
|
-
"uniqueLocal",
|
|
2550
|
-
"multicast"
|
|
2551
|
-
]);
|
|
2520
|
+
var BLOCKED_IPV6_RANGES = /* @__PURE__ */ new Set(["unspecified", "loopback", "linkLocal", "uniqueLocal", "multicast"]);
|
|
2552
2521
|
var RFC2544_BENCHMARK_PREFIX = [ipaddr.IPv4.parse("198.18.0.0"), 15];
|
|
2553
2522
|
var EMBEDDED_IPV4_SENTINEL_RULES = [
|
|
2554
2523
|
// IPv4-compatible (::a.b.c.d)
|
|
@@ -2597,12 +2566,12 @@ function parseIpv6WithEmbeddedIpv4(raw) {
|
|
|
2597
2566
|
}
|
|
2598
2567
|
function normalizeIpParseInput(raw) {
|
|
2599
2568
|
const trimmed = raw?.trim();
|
|
2600
|
-
if (
|
|
2569
|
+
if (trimmed === void 0 || trimmed === "") return;
|
|
2601
2570
|
return stripIpv6Brackets(trimmed);
|
|
2602
2571
|
}
|
|
2603
2572
|
function parseCanonicalIpAddress(raw) {
|
|
2604
2573
|
const normalized = normalizeIpParseInput(raw);
|
|
2605
|
-
if (
|
|
2574
|
+
if (normalized === void 0) return;
|
|
2606
2575
|
if (ipaddr.IPv4.isValid(normalized)) {
|
|
2607
2576
|
if (!ipaddr.IPv4.isValidFourPartDecimal(normalized)) return;
|
|
2608
2577
|
return ipaddr.IPv4.parse(normalized);
|
|
@@ -2612,20 +2581,20 @@ function parseCanonicalIpAddress(raw) {
|
|
|
2612
2581
|
}
|
|
2613
2582
|
function parseLooseIpAddress(raw) {
|
|
2614
2583
|
const normalized = normalizeIpParseInput(raw);
|
|
2615
|
-
if (
|
|
2584
|
+
if (normalized === void 0) return;
|
|
2616
2585
|
if (ipaddr.isValid(normalized)) return ipaddr.parse(normalized);
|
|
2617
2586
|
return parseIpv6WithEmbeddedIpv4(normalized);
|
|
2618
2587
|
}
|
|
2619
2588
|
function isCanonicalDottedDecimalIPv4(raw) {
|
|
2620
|
-
const trimmed = raw
|
|
2621
|
-
if (
|
|
2589
|
+
const trimmed = raw.trim();
|
|
2590
|
+
if (trimmed === "") return false;
|
|
2622
2591
|
const normalized = stripIpv6Brackets(trimmed);
|
|
2623
2592
|
if (!normalized) return false;
|
|
2624
2593
|
return ipaddr.IPv4.isValidFourPartDecimal(normalized);
|
|
2625
2594
|
}
|
|
2626
2595
|
function isLegacyIpv4Literal(raw) {
|
|
2627
|
-
const trimmed = raw
|
|
2628
|
-
if (
|
|
2596
|
+
const trimmed = raw.trim();
|
|
2597
|
+
if (trimmed === "") return false;
|
|
2629
2598
|
const normalized = stripIpv6Brackets(trimmed);
|
|
2630
2599
|
if (!normalized || normalized.includes(":")) return false;
|
|
2631
2600
|
if (isCanonicalDottedDecimalIPv4(normalized)) return false;
|
|
@@ -2651,12 +2620,7 @@ function isBlockedSpecialUseIpv6Address(address) {
|
|
|
2651
2620
|
return (address.parts[0] & 65472) === 65216;
|
|
2652
2621
|
}
|
|
2653
2622
|
function decodeIpv4FromHextets(high, low) {
|
|
2654
|
-
const octets = [
|
|
2655
|
-
high >>> 8 & 255,
|
|
2656
|
-
high & 255,
|
|
2657
|
-
low >>> 8 & 255,
|
|
2658
|
-
low & 255
|
|
2659
|
-
];
|
|
2623
|
+
const octets = [high >>> 8 & 255, high & 255, low >>> 8 & 255, low & 255];
|
|
2660
2624
|
return ipaddr.IPv4.parse(octets.join("."));
|
|
2661
2625
|
}
|
|
2662
2626
|
function extractEmbeddedIpv4FromIpv6(address) {
|
|
@@ -2703,9 +2667,7 @@ function normalizeHostnameSet(values) {
|
|
|
2703
2667
|
function normalizeHostnameAllowlist(values) {
|
|
2704
2668
|
if (!values || values.length === 0) return [];
|
|
2705
2669
|
return Array.from(
|
|
2706
|
-
new Set(
|
|
2707
|
-
values.map((v) => normalizeHostname(v)).filter((v) => v !== "*" && v !== "*." && v.length > 0)
|
|
2708
|
-
)
|
|
2670
|
+
new Set(values.map((v) => normalizeHostname(v)).filter((v) => v !== "*" && v !== "*." && v.length > 0))
|
|
2709
2671
|
);
|
|
2710
2672
|
}
|
|
2711
2673
|
function isHostnameAllowedByPattern(hostname, pattern) {
|
|
@@ -2740,19 +2702,25 @@ function createPinnedLookup(params) {
|
|
|
2740
2702
|
family: address.includes(":") ? 6 : 4
|
|
2741
2703
|
}));
|
|
2742
2704
|
let index = 0;
|
|
2743
|
-
return ((
|
|
2744
|
-
const
|
|
2745
|
-
|
|
2746
|
-
const
|
|
2747
|
-
if (
|
|
2748
|
-
|
|
2749
|
-
|
|
2705
|
+
return ((_host, ...rest) => {
|
|
2706
|
+
const second = rest[0];
|
|
2707
|
+
const third = rest[1];
|
|
2708
|
+
const cb = typeof second === "function" ? second : typeof third === "function" ? third : void 0;
|
|
2709
|
+
if (cb === void 0) return;
|
|
2710
|
+
const normalized = normalizeHostname(_host);
|
|
2711
|
+
if (normalized === "" || normalized !== normalizedHost) {
|
|
2712
|
+
if (typeof second === "function" || second === void 0) {
|
|
2713
|
+
fallback(_host, cb);
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
fallback(_host, second, cb);
|
|
2717
|
+
return;
|
|
2750
2718
|
}
|
|
2751
|
-
const opts = typeof
|
|
2752
|
-
const requestedFamily = typeof
|
|
2719
|
+
const opts = typeof second === "object" ? second : {};
|
|
2720
|
+
const requestedFamily = typeof second === "number" ? second : typeof opts.family === "number" ? opts.family : 0;
|
|
2753
2721
|
const candidates = requestedFamily === 4 || requestedFamily === 6 ? records.filter((entry) => entry.family === requestedFamily) : records;
|
|
2754
2722
|
const usable = candidates.length > 0 ? candidates : records;
|
|
2755
|
-
if (opts.all) {
|
|
2723
|
+
if (opts.all === true) {
|
|
2756
2724
|
cb(null, usable);
|
|
2757
2725
|
return;
|
|
2758
2726
|
}
|
|
@@ -2770,9 +2738,7 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
2770
2738
|
const isExplicitlyAllowed = allowedHostnames.has(normalized);
|
|
2771
2739
|
const skipPrivateNetworkChecks = allowPrivateNetwork || isExplicitlyAllowed;
|
|
2772
2740
|
if (!matchesHostnameAllowlist(normalized, hostnameAllowlist)) {
|
|
2773
|
-
throw new InvalidBrowserNavigationUrlError(
|
|
2774
|
-
`Navigation blocked: hostname "${hostname}" is not in the allowlist.`
|
|
2775
|
-
);
|
|
2741
|
+
throw new InvalidBrowserNavigationUrlError(`Navigation blocked: hostname "${hostname}" is not in the allowlist.`);
|
|
2776
2742
|
}
|
|
2777
2743
|
if (!skipPrivateNetworkChecks) {
|
|
2778
2744
|
if (isBlockedHostnameOrIp(normalized, params.policy)) {
|
|
@@ -2790,7 +2756,7 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
2790
2756
|
`Navigation to internal/loopback address blocked: unable to resolve "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
2791
2757
|
);
|
|
2792
2758
|
}
|
|
2793
|
-
if (
|
|
2759
|
+
if (results.length === 0) {
|
|
2794
2760
|
throw new InvalidBrowserNavigationUrlError(
|
|
2795
2761
|
`Navigation to internal/loopback address blocked: unable to resolve "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
2796
2762
|
);
|
|
@@ -2817,8 +2783,8 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
2817
2783
|
};
|
|
2818
2784
|
}
|
|
2819
2785
|
async function assertBrowserNavigationAllowed(opts) {
|
|
2820
|
-
const rawUrl =
|
|
2821
|
-
if (
|
|
2786
|
+
const rawUrl = opts.url.trim();
|
|
2787
|
+
if (rawUrl === "") throw new InvalidBrowserNavigationUrlError("url is required");
|
|
2822
2788
|
let parsed;
|
|
2823
2789
|
try {
|
|
2824
2790
|
parsed = new URL(rawUrl);
|
|
@@ -2847,7 +2813,7 @@ async function assertSafeOutputPath(path2, allowedRoots) {
|
|
|
2847
2813
|
if (normalized.includes("..")) {
|
|
2848
2814
|
throw new Error(`Unsafe output path: directory traversal detected in "${path2}".`);
|
|
2849
2815
|
}
|
|
2850
|
-
if (allowedRoots
|
|
2816
|
+
if (allowedRoots !== void 0 && allowedRoots.length > 0) {
|
|
2851
2817
|
const resolved = path.resolve(normalized);
|
|
2852
2818
|
let parentReal;
|
|
2853
2819
|
try {
|
|
@@ -2905,8 +2871,8 @@ async function resolveStrictExistingUploadPaths(params) {
|
|
|
2905
2871
|
}
|
|
2906
2872
|
}
|
|
2907
2873
|
function sanitizeUntrustedFileName(fileName, fallbackName) {
|
|
2908
|
-
const trimmed =
|
|
2909
|
-
if (
|
|
2874
|
+
const trimmed = fileName.trim();
|
|
2875
|
+
if (trimmed === "") return fallbackName;
|
|
2910
2876
|
let base = path.posix.basename(trimmed);
|
|
2911
2877
|
base = path.win32.basename(base);
|
|
2912
2878
|
let cleaned = "";
|
|
@@ -2940,16 +2906,17 @@ async function writeViaSiblingTempPath(params) {
|
|
|
2940
2906
|
await promises$1.rename(tempPath, targetPath);
|
|
2941
2907
|
renameSucceeded = true;
|
|
2942
2908
|
} finally {
|
|
2943
|
-
if (!renameSucceeded)
|
|
2944
|
-
|
|
2909
|
+
if (!renameSucceeded)
|
|
2910
|
+
await promises$1.rm(tempPath, { force: true }).catch(() => {
|
|
2911
|
+
});
|
|
2945
2912
|
}
|
|
2946
2913
|
}
|
|
2947
2914
|
function isAbsolute(p) {
|
|
2948
2915
|
return p.startsWith("/") || /^[a-zA-Z]:/.test(p);
|
|
2949
2916
|
}
|
|
2950
2917
|
async function assertBrowserNavigationResultAllowed(opts) {
|
|
2951
|
-
const rawUrl =
|
|
2952
|
-
if (
|
|
2918
|
+
const rawUrl = opts.url.trim();
|
|
2919
|
+
if (rawUrl === "") return;
|
|
2953
2920
|
let parsed;
|
|
2954
2921
|
try {
|
|
2955
2922
|
parsed = new URL(rawUrl);
|
|
@@ -2979,18 +2946,20 @@ function requiresInspectableBrowserNavigationRedirects(ssrfPolicy) {
|
|
|
2979
2946
|
var MAX_CLICK_DELAY_MS = 5e3;
|
|
2980
2947
|
var CHECKABLE_ROLES = /* @__PURE__ */ new Set(["menuitemcheckbox", "menuitemradio", "checkbox", "switch"]);
|
|
2981
2948
|
function resolveLocator(page, resolved) {
|
|
2982
|
-
|
|
2949
|
+
if (resolved.ref !== void 0 && resolved.ref !== "") return refLocator(page, resolved.ref);
|
|
2950
|
+
const sel = resolved.selector ?? "";
|
|
2951
|
+
return page.locator(sel);
|
|
2983
2952
|
}
|
|
2984
2953
|
async function clickViaPlaywright(opts) {
|
|
2985
2954
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
2986
2955
|
const page = await getRestoredPageForTarget(opts);
|
|
2987
|
-
const label = resolved.ref ?? resolved.selector;
|
|
2956
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
2988
2957
|
const locator = resolveLocator(page, resolved);
|
|
2989
2958
|
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
2990
2959
|
let checkableRole = false;
|
|
2991
|
-
if (resolved.ref) {
|
|
2960
|
+
if (resolved.ref !== void 0 && resolved.ref !== "") {
|
|
2992
2961
|
const refId = parseRoleRef(resolved.ref);
|
|
2993
|
-
if (refId) {
|
|
2962
|
+
if (refId !== null) {
|
|
2994
2963
|
const state = ensurePageState(page);
|
|
2995
2964
|
const info = state.roleRefs?.[refId];
|
|
2996
2965
|
if (info && CHECKABLE_ROLES.has(info.role)) checkableRole = true;
|
|
@@ -3003,15 +2972,15 @@ async function clickViaPlaywright(opts) {
|
|
|
3003
2972
|
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
3004
2973
|
}
|
|
3005
2974
|
let ariaCheckedBefore;
|
|
3006
|
-
if (checkableRole &&
|
|
2975
|
+
if (checkableRole && opts.doubleClick !== true) {
|
|
3007
2976
|
ariaCheckedBefore = await locator.getAttribute("aria-checked", { timeout }).catch(() => void 0);
|
|
3008
2977
|
}
|
|
3009
|
-
if (opts.doubleClick) {
|
|
2978
|
+
if (opts.doubleClick === true) {
|
|
3010
2979
|
await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
3011
2980
|
} else {
|
|
3012
2981
|
await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
3013
2982
|
}
|
|
3014
|
-
if (checkableRole &&
|
|
2983
|
+
if (checkableRole && opts.doubleClick !== true && ariaCheckedBefore !== void 0) {
|
|
3015
2984
|
const POLL_INTERVAL_MS = 50;
|
|
3016
2985
|
const POLL_TIMEOUT_MS = 500;
|
|
3017
2986
|
let changed = false;
|
|
@@ -3024,7 +2993,9 @@ async function clickViaPlaywright(opts) {
|
|
|
3024
2993
|
await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
|
|
3025
2994
|
}
|
|
3026
2995
|
if (!changed) {
|
|
3027
|
-
await locator.evaluate((el) =>
|
|
2996
|
+
await locator.evaluate((el) => {
|
|
2997
|
+
el.click();
|
|
2998
|
+
}).catch(() => {
|
|
3028
2999
|
});
|
|
3029
3000
|
}
|
|
3030
3001
|
}
|
|
@@ -3035,7 +3006,7 @@ async function clickViaPlaywright(opts) {
|
|
|
3035
3006
|
async function hoverViaPlaywright(opts) {
|
|
3036
3007
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
3037
3008
|
const page = await getRestoredPageForTarget(opts);
|
|
3038
|
-
const label = resolved.ref ?? resolved.selector;
|
|
3009
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
3039
3010
|
const locator = resolveLocator(page, resolved);
|
|
3040
3011
|
try {
|
|
3041
3012
|
await locator.hover({ timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
|
|
@@ -3045,28 +3016,28 @@ async function hoverViaPlaywright(opts) {
|
|
|
3045
3016
|
}
|
|
3046
3017
|
async function typeViaPlaywright(opts) {
|
|
3047
3018
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
3048
|
-
const text =
|
|
3019
|
+
const text = opts.text;
|
|
3049
3020
|
const page = await getRestoredPageForTarget(opts);
|
|
3050
|
-
const label = resolved.ref ?? resolved.selector;
|
|
3021
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
3051
3022
|
const locator = resolveLocator(page, resolved);
|
|
3052
3023
|
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
3053
3024
|
try {
|
|
3054
|
-
if (opts.slowly) {
|
|
3025
|
+
if (opts.slowly === true) {
|
|
3055
3026
|
await locator.click({ timeout });
|
|
3056
3027
|
await locator.pressSequentially(text, { timeout, delay: 75 });
|
|
3057
3028
|
} else {
|
|
3058
3029
|
await locator.fill(text, { timeout });
|
|
3059
3030
|
}
|
|
3060
|
-
if (opts.submit) await locator.press("Enter", { timeout });
|
|
3031
|
+
if (opts.submit === true) await locator.press("Enter", { timeout });
|
|
3061
3032
|
} catch (err) {
|
|
3062
3033
|
throw toAIFriendlyError(err, label);
|
|
3063
3034
|
}
|
|
3064
3035
|
}
|
|
3065
3036
|
async function selectOptionViaPlaywright(opts) {
|
|
3066
3037
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
3067
|
-
if (
|
|
3038
|
+
if (opts.values.length === 0) throw new Error("values are required");
|
|
3068
3039
|
const page = await getRestoredPageForTarget(opts);
|
|
3069
|
-
const label = resolved.ref ?? resolved.selector;
|
|
3040
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
3070
3041
|
const locator = resolveLocator(page, resolved);
|
|
3071
3042
|
try {
|
|
3072
3043
|
await locator.selectOption(opts.values, { timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
|
|
@@ -3080,8 +3051,8 @@ async function dragViaPlaywright(opts) {
|
|
|
3080
3051
|
const page = await getRestoredPageForTarget(opts);
|
|
3081
3052
|
const startLocator = resolveLocator(page, resolvedStart);
|
|
3082
3053
|
const endLocator = resolveLocator(page, resolvedEnd);
|
|
3083
|
-
const startLabel = resolvedStart.ref ?? resolvedStart.selector;
|
|
3084
|
-
const endLabel = resolvedEnd.ref ?? resolvedEnd.selector;
|
|
3054
|
+
const startLabel = resolvedStart.ref ?? resolvedStart.selector ?? "";
|
|
3055
|
+
const endLabel = resolvedEnd.ref ?? resolvedEnd.selector ?? "";
|
|
3085
3056
|
try {
|
|
3086
3057
|
await startLocator.dragTo(endLocator, { timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
|
|
3087
3058
|
} catch (err) {
|
|
@@ -3117,7 +3088,7 @@ async function fillFormViaPlaywright(opts) {
|
|
|
3117
3088
|
async function scrollIntoViewViaPlaywright(opts) {
|
|
3118
3089
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
3119
3090
|
const page = await getRestoredPageForTarget(opts);
|
|
3120
|
-
const label = resolved.ref ?? resolved.selector;
|
|
3091
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
3121
3092
|
const locator = resolveLocator(page, resolved);
|
|
3122
3093
|
try {
|
|
3123
3094
|
await locator.scrollIntoViewIfNeeded({ timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
|
|
@@ -3185,7 +3156,7 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
3185
3156
|
const armId = state.armIdUpload;
|
|
3186
3157
|
page.waitForEvent("filechooser", { timeout }).then(async (fileChooser) => {
|
|
3187
3158
|
if (state.armIdUpload !== armId) return;
|
|
3188
|
-
if (
|
|
3159
|
+
if (opts.paths === void 0 || opts.paths.length === 0) {
|
|
3189
3160
|
try {
|
|
3190
3161
|
await page.keyboard.press("Escape");
|
|
3191
3162
|
} catch {
|
|
@@ -3206,7 +3177,7 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
3206
3177
|
await fileChooser.setFiles(uploadPathsResult.paths);
|
|
3207
3178
|
try {
|
|
3208
3179
|
const input = typeof fileChooser.element === "function" ? await Promise.resolve(fileChooser.element()) : null;
|
|
3209
|
-
if (input) {
|
|
3180
|
+
if (input !== null) {
|
|
3210
3181
|
await input.evaluate((el) => {
|
|
3211
3182
|
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
3212
3183
|
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
@@ -3220,7 +3191,7 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
3220
3191
|
|
|
3221
3192
|
// src/actions/keyboard.ts
|
|
3222
3193
|
async function pressKeyViaPlaywright(opts) {
|
|
3223
|
-
const key =
|
|
3194
|
+
const key = opts.key.trim();
|
|
3224
3195
|
if (!key) throw new Error("key is required");
|
|
3225
3196
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3226
3197
|
ensurePageState(page);
|
|
@@ -3233,9 +3204,9 @@ function isRetryableNavigateError(err) {
|
|
|
3233
3204
|
return msg.includes("frame has been detached") || msg.includes("target page, context or browser has been closed");
|
|
3234
3205
|
}
|
|
3235
3206
|
async function navigateViaPlaywright(opts) {
|
|
3236
|
-
const url =
|
|
3207
|
+
const url = opts.url.trim();
|
|
3237
3208
|
if (!url) throw new Error("url is required");
|
|
3238
|
-
const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3209
|
+
const policy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3239
3210
|
await assertBrowserNavigationAllowed({ url, ...withBrowserNavigationPolicy(policy) });
|
|
3240
3211
|
const timeout = Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 2e4));
|
|
3241
3212
|
let page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
@@ -3254,23 +3225,27 @@ async function navigateViaPlaywright(opts) {
|
|
|
3254
3225
|
ensurePageState(page);
|
|
3255
3226
|
response = await navigate();
|
|
3256
3227
|
}
|
|
3257
|
-
await assertBrowserNavigationRedirectChainAllowed({
|
|
3228
|
+
await assertBrowserNavigationRedirectChainAllowed({
|
|
3229
|
+
request: response?.request(),
|
|
3230
|
+
...withBrowserNavigationPolicy(policy)
|
|
3231
|
+
});
|
|
3258
3232
|
const finalUrl = page.url();
|
|
3259
3233
|
await assertBrowserNavigationResultAllowed({ url: finalUrl, ...withBrowserNavigationPolicy(policy) });
|
|
3260
3234
|
return { url: finalUrl };
|
|
3261
3235
|
}
|
|
3262
3236
|
async function listPagesViaPlaywright(opts) {
|
|
3263
3237
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
3264
|
-
const pages =
|
|
3238
|
+
const pages = getAllPages(browser);
|
|
3265
3239
|
const results = [];
|
|
3266
3240
|
for (const page of pages) {
|
|
3267
3241
|
const tid = await pageTargetId(page).catch(() => null);
|
|
3268
|
-
if (tid)
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3242
|
+
if (tid !== null && tid !== "")
|
|
3243
|
+
results.push({
|
|
3244
|
+
targetId: tid,
|
|
3245
|
+
title: await page.title().catch(() => ""),
|
|
3246
|
+
url: page.url(),
|
|
3247
|
+
type: "page"
|
|
3248
|
+
});
|
|
3274
3249
|
}
|
|
3275
3250
|
return results;
|
|
3276
3251
|
}
|
|
@@ -3281,7 +3256,7 @@ async function createPageViaPlaywright(opts) {
|
|
|
3281
3256
|
const page = await context.newPage();
|
|
3282
3257
|
ensurePageState(page);
|
|
3283
3258
|
const targetUrl = (opts.url ?? "").trim() || "about:blank";
|
|
3284
|
-
const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3259
|
+
const policy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3285
3260
|
if (targetUrl !== "about:blank") {
|
|
3286
3261
|
const navigationPolicy = withBrowserNavigationPolicy(policy);
|
|
3287
3262
|
await assertBrowserNavigationAllowed({ url: targetUrl, ...navigationPolicy });
|
|
@@ -3292,7 +3267,7 @@ async function createPageViaPlaywright(opts) {
|
|
|
3292
3267
|
await assertBrowserNavigationResultAllowed({ url: page.url(), ...navigationPolicy });
|
|
3293
3268
|
}
|
|
3294
3269
|
const tid = await pageTargetId(page).catch(() => null);
|
|
3295
|
-
if (
|
|
3270
|
+
if (tid === null || tid === "") throw new Error("Failed to get targetId for new page");
|
|
3296
3271
|
return {
|
|
3297
3272
|
targetId: tid,
|
|
3298
3273
|
title: await page.title().catch(() => ""),
|
|
@@ -3342,7 +3317,7 @@ var MAX_WAIT_TIME_MS = 3e4;
|
|
|
3342
3317
|
function resolveBoundedDelayMs2(value, label, maxMs) {
|
|
3343
3318
|
const normalized = Math.floor(value ?? 0);
|
|
3344
3319
|
if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
|
|
3345
|
-
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
|
|
3320
|
+
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${String(maxMs)}ms`);
|
|
3346
3321
|
return normalized;
|
|
3347
3322
|
}
|
|
3348
3323
|
async function waitForViaPlaywright(opts) {
|
|
@@ -3352,164 +3327,26 @@ async function waitForViaPlaywright(opts) {
|
|
|
3352
3327
|
if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
|
|
3353
3328
|
await page.waitForTimeout(resolveBoundedDelayMs2(opts.timeMs, "wait timeMs", MAX_WAIT_TIME_MS));
|
|
3354
3329
|
}
|
|
3355
|
-
if (opts.text) {
|
|
3356
|
-
await page.getByText(opts.text).first().waitFor({ state: "visible", timeout });
|
|
3357
|
-
}
|
|
3358
|
-
if (opts.textGone) {
|
|
3359
|
-
await page.getByText(opts.textGone).first().waitFor({ state: "hidden", timeout });
|
|
3360
|
-
}
|
|
3361
|
-
if (opts.selector) {
|
|
3362
|
-
const selector = String(opts.selector).trim();
|
|
3363
|
-
if (selector) await page.locator(selector).first().waitFor({ state: "visible", timeout });
|
|
3364
|
-
}
|
|
3365
|
-
if (opts.url) {
|
|
3366
|
-
const url = String(opts.url).trim();
|
|
3367
|
-
if (url) await page.waitForURL(url, { timeout });
|
|
3368
|
-
}
|
|
3369
|
-
if (opts.loadState) {
|
|
3370
|
-
await page.waitForLoadState(opts.loadState, { timeout });
|
|
3371
|
-
}
|
|
3372
|
-
if (opts.fn) {
|
|
3373
|
-
const fn = String(opts.fn).trim();
|
|
3374
|
-
if (fn) await page.waitForFunction(fn, void 0, { timeout });
|
|
3375
|
-
}
|
|
3376
|
-
}
|
|
3377
|
-
|
|
3378
|
-
// src/actions/evaluate.ts
|
|
3379
|
-
async function evaluateInAllFramesViaPlaywright(opts) {
|
|
3380
|
-
const fnText = String(opts.fn ?? "").trim();
|
|
3381
|
-
if (!fnText) throw new Error("function is required");
|
|
3382
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3383
|
-
const frames = page.frames();
|
|
3384
|
-
const results = [];
|
|
3385
|
-
for (const frame of frames) {
|
|
3386
|
-
try {
|
|
3387
|
-
const result = await frame.evaluate(
|
|
3388
|
-
// eslint-disable-next-line no-eval
|
|
3389
|
-
(fnBody) => {
|
|
3390
|
-
"use strict";
|
|
3391
|
-
try {
|
|
3392
|
-
const candidate = (0, eval)("(" + fnBody + ")");
|
|
3393
|
-
return typeof candidate === "function" ? candidate() : candidate;
|
|
3394
|
-
} catch (err) {
|
|
3395
|
-
throw new Error("Invalid evaluate function: " + (err instanceof Error ? err.message : String(err)));
|
|
3396
|
-
}
|
|
3397
|
-
},
|
|
3398
|
-
fnText
|
|
3399
|
-
);
|
|
3400
|
-
results.push({
|
|
3401
|
-
frameUrl: frame.url(),
|
|
3402
|
-
frameName: frame.name(),
|
|
3403
|
-
result
|
|
3404
|
-
});
|
|
3405
|
-
} catch {
|
|
3406
|
-
}
|
|
3407
|
-
}
|
|
3408
|
-
return results;
|
|
3409
|
-
}
|
|
3410
|
-
async function awaitEvalWithAbort(evalPromise, abortPromise) {
|
|
3411
|
-
if (!abortPromise) return await evalPromise;
|
|
3412
|
-
try {
|
|
3413
|
-
return await Promise.race([evalPromise, abortPromise]);
|
|
3414
|
-
} catch (err) {
|
|
3415
|
-
evalPromise.catch(() => {
|
|
3416
|
-
});
|
|
3417
|
-
throw err;
|
|
3418
|
-
}
|
|
3419
|
-
}
|
|
3420
|
-
var BROWSER_EVALUATOR = new Function("args", `
|
|
3421
|
-
"use strict";
|
|
3422
|
-
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
3423
|
-
try {
|
|
3424
|
-
var candidate = eval("(" + fnBody + ")");
|
|
3425
|
-
var result = typeof candidate === "function" ? candidate() : candidate;
|
|
3426
|
-
if (result && typeof result.then === "function") {
|
|
3427
|
-
return Promise.race([
|
|
3428
|
-
result,
|
|
3429
|
-
new Promise(function(_, reject) {
|
|
3430
|
-
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
3431
|
-
})
|
|
3432
|
-
]);
|
|
3433
|
-
}
|
|
3434
|
-
return result;
|
|
3435
|
-
} catch (err) {
|
|
3436
|
-
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
3437
|
-
}
|
|
3438
|
-
`);
|
|
3439
|
-
var ELEMENT_EVALUATOR = new Function("el", "args", `
|
|
3440
|
-
"use strict";
|
|
3441
|
-
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
3442
|
-
try {
|
|
3443
|
-
var candidate = eval("(" + fnBody + ")");
|
|
3444
|
-
var result = typeof candidate === "function" ? candidate(el) : candidate;
|
|
3445
|
-
if (result && typeof result.then === "function") {
|
|
3446
|
-
return Promise.race([
|
|
3447
|
-
result,
|
|
3448
|
-
new Promise(function(_, reject) {
|
|
3449
|
-
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
3450
|
-
})
|
|
3451
|
-
]);
|
|
3452
|
-
}
|
|
3453
|
-
return result;
|
|
3454
|
-
} catch (err) {
|
|
3455
|
-
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
3456
|
-
}
|
|
3457
|
-
`);
|
|
3458
|
-
async function evaluateViaPlaywright(opts) {
|
|
3459
|
-
const fnText = String(opts.fn ?? "").trim();
|
|
3460
|
-
if (!fnText) throw new Error("function is required");
|
|
3461
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3462
|
-
ensurePageState(page);
|
|
3463
|
-
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
3464
|
-
const outerTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
|
|
3465
|
-
let evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 500));
|
|
3466
|
-
evaluateTimeout = Math.min(evaluateTimeout, outerTimeout);
|
|
3467
|
-
const signal = opts.signal;
|
|
3468
|
-
let abortListener;
|
|
3469
|
-
let abortReject;
|
|
3470
|
-
let abortPromise;
|
|
3471
|
-
if (signal) {
|
|
3472
|
-
abortPromise = new Promise((_, reject) => {
|
|
3473
|
-
abortReject = reject;
|
|
3474
|
-
});
|
|
3475
|
-
abortPromise.catch(() => {
|
|
3476
|
-
});
|
|
3477
|
-
}
|
|
3478
|
-
if (signal) {
|
|
3479
|
-
const disconnect = () => {
|
|
3480
|
-
forceDisconnectPlaywrightForTarget({
|
|
3481
|
-
cdpUrl: opts.cdpUrl,
|
|
3482
|
-
targetId: opts.targetId}).catch(() => {
|
|
3483
|
-
});
|
|
3484
|
-
};
|
|
3485
|
-
if (signal.aborted) {
|
|
3486
|
-
disconnect();
|
|
3487
|
-
throw signal.reason ?? new Error("aborted");
|
|
3488
|
-
}
|
|
3489
|
-
abortListener = () => {
|
|
3490
|
-
disconnect();
|
|
3491
|
-
abortReject?.(signal.reason ?? new Error("aborted"));
|
|
3492
|
-
};
|
|
3493
|
-
signal.addEventListener("abort", abortListener, { once: true });
|
|
3494
|
-
if (signal.aborted) {
|
|
3495
|
-
abortListener();
|
|
3496
|
-
throw signal.reason ?? new Error("aborted");
|
|
3497
|
-
}
|
|
3330
|
+
if (opts.text !== void 0 && opts.text !== "") {
|
|
3331
|
+
await page.getByText(opts.text).first().waitFor({ state: "visible", timeout });
|
|
3498
3332
|
}
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3333
|
+
if (opts.textGone !== void 0 && opts.textGone !== "") {
|
|
3334
|
+
await page.getByText(opts.textGone).first().waitFor({ state: "hidden", timeout });
|
|
3335
|
+
}
|
|
3336
|
+
if (opts.selector !== void 0 && opts.selector !== "") {
|
|
3337
|
+
const selector = opts.selector.trim();
|
|
3338
|
+
if (selector !== "") await page.locator(selector).first().waitFor({ state: "visible", timeout });
|
|
3339
|
+
}
|
|
3340
|
+
if (opts.url !== void 0 && opts.url !== "") {
|
|
3341
|
+
const url = opts.url.trim();
|
|
3342
|
+
if (url !== "") await page.waitForURL(url, { timeout });
|
|
3343
|
+
}
|
|
3344
|
+
if (opts.loadState !== void 0) {
|
|
3345
|
+
await page.waitForLoadState(opts.loadState, { timeout });
|
|
3346
|
+
}
|
|
3347
|
+
if (opts.fn !== void 0 && opts.fn !== "") {
|
|
3348
|
+
const fn = opts.fn.trim();
|
|
3349
|
+
if (fn !== "") await page.waitForFunction(fn, void 0, { timeout });
|
|
3513
3350
|
}
|
|
3514
3351
|
}
|
|
3515
3352
|
|
|
@@ -3517,7 +3354,7 @@ async function evaluateViaPlaywright(opts) {
|
|
|
3517
3354
|
var MAX_BATCH_DEPTH = 5;
|
|
3518
3355
|
var MAX_BATCH_ACTIONS = 100;
|
|
3519
3356
|
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}`);
|
|
3357
|
+
if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${String(MAX_BATCH_DEPTH)}`);
|
|
3521
3358
|
const effectiveTargetId = action.targetId ?? targetId;
|
|
3522
3359
|
switch (action.kind) {
|
|
3523
3360
|
case "click":
|
|
@@ -3609,7 +3446,8 @@ async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, de
|
|
|
3609
3446
|
});
|
|
3610
3447
|
break;
|
|
3611
3448
|
case "wait":
|
|
3612
|
-
if (action.fn
|
|
3449
|
+
if (action.fn !== void 0 && action.fn !== "" && !evaluateEnabled)
|
|
3450
|
+
throw new Error("wait --fn is disabled by config (browser.evaluateEnabled=false)");
|
|
3613
3451
|
await waitForViaPlaywright({
|
|
3614
3452
|
cdpUrl,
|
|
3615
3453
|
targetId: effectiveTargetId,
|
|
@@ -3650,13 +3488,14 @@ async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, de
|
|
|
3650
3488
|
});
|
|
3651
3489
|
break;
|
|
3652
3490
|
default:
|
|
3653
|
-
throw new Error(`Unsupported batch action kind: ${action.kind}`);
|
|
3491
|
+
throw new Error(`Unsupported batch action kind: ${String(action.kind)}`);
|
|
3654
3492
|
}
|
|
3655
3493
|
}
|
|
3656
3494
|
async function batchViaPlaywright(opts) {
|
|
3657
3495
|
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)
|
|
3496
|
+
if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${String(MAX_BATCH_DEPTH)}`);
|
|
3497
|
+
if (opts.actions.length > MAX_BATCH_ACTIONS)
|
|
3498
|
+
throw new Error(`Batch exceeds maximum of ${String(MAX_BATCH_ACTIONS)} actions`);
|
|
3660
3499
|
const results = [];
|
|
3661
3500
|
const evaluateEnabled = opts.evaluateEnabled !== false;
|
|
3662
3501
|
for (const action of opts.actions) {
|
|
@@ -3736,7 +3575,7 @@ async function downloadViaPlaywright(opts) {
|
|
|
3736
3575
|
const state = ensurePageState(page);
|
|
3737
3576
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
3738
3577
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
3739
|
-
const outPath =
|
|
3578
|
+
const outPath = opts.path.trim();
|
|
3740
3579
|
if (!outPath) throw new Error("path is required");
|
|
3741
3580
|
state.armIdDownload = bumpDownloadArmId();
|
|
3742
3581
|
const armId = state.armIdDownload;
|
|
@@ -3744,378 +3583,909 @@ async function downloadViaPlaywright(opts) {
|
|
|
3744
3583
|
try {
|
|
3745
3584
|
const locator = refLocator(page, opts.ref);
|
|
3746
3585
|
try {
|
|
3747
|
-
await locator.click({ timeout });
|
|
3748
|
-
} catch (err) {
|
|
3749
|
-
throw toAIFriendlyError(err, opts.ref);
|
|
3586
|
+
await locator.click({ timeout });
|
|
3587
|
+
} catch (err) {
|
|
3588
|
+
throw toAIFriendlyError(err, opts.ref);
|
|
3589
|
+
}
|
|
3590
|
+
return await awaitDownloadPayload({ waiter, state, armId, outPath });
|
|
3591
|
+
} catch (err) {
|
|
3592
|
+
waiter.cancel();
|
|
3593
|
+
throw err;
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
async function waitForDownloadViaPlaywright(opts) {
|
|
3597
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3598
|
+
const state = ensurePageState(page);
|
|
3599
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
3600
|
+
state.armIdDownload = bumpDownloadArmId();
|
|
3601
|
+
const armId = state.armIdDownload;
|
|
3602
|
+
const waiter = createPageDownloadWaiter(page, timeout);
|
|
3603
|
+
try {
|
|
3604
|
+
const download = await waiter.promise;
|
|
3605
|
+
if (state.armIdDownload !== armId) throw new Error("Download was superseded by another waiter");
|
|
3606
|
+
const savePath = opts.path ?? download.suggestedFilename();
|
|
3607
|
+
await assertSafeOutputPath(savePath, opts.allowedOutputRoots);
|
|
3608
|
+
return await saveDownloadPayload(download, savePath);
|
|
3609
|
+
} catch (err) {
|
|
3610
|
+
waiter.cancel();
|
|
3611
|
+
throw err;
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
async function emulateMediaViaPlaywright(opts) {
|
|
3615
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3616
|
+
ensurePageState(page);
|
|
3617
|
+
await page.emulateMedia({ colorScheme: opts.colorScheme });
|
|
3618
|
+
}
|
|
3619
|
+
async function setDeviceViaPlaywright(opts) {
|
|
3620
|
+
const name = opts.name.trim();
|
|
3621
|
+
if (!name) throw new Error("device name is required");
|
|
3622
|
+
const device = playwrightCore.devices[name];
|
|
3623
|
+
if (device === void 0) {
|
|
3624
|
+
throw new Error(`Unknown device "${name}".`);
|
|
3625
|
+
}
|
|
3626
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3627
|
+
ensurePageState(page);
|
|
3628
|
+
if (device.viewport !== null) {
|
|
3629
|
+
await page.setViewportSize({
|
|
3630
|
+
width: device.viewport.width,
|
|
3631
|
+
height: device.viewport.height
|
|
3632
|
+
});
|
|
3633
|
+
}
|
|
3634
|
+
await withPageScopedCdpClient({
|
|
3635
|
+
cdpUrl: opts.cdpUrl,
|
|
3636
|
+
page,
|
|
3637
|
+
targetId: opts.targetId,
|
|
3638
|
+
fn: async (send) => {
|
|
3639
|
+
const locale = device.locale;
|
|
3640
|
+
if (device.userAgent !== "" || locale !== void 0 && locale !== "") {
|
|
3641
|
+
await send("Emulation.setUserAgentOverride", {
|
|
3642
|
+
userAgent: device.userAgent,
|
|
3643
|
+
acceptLanguage: locale
|
|
3644
|
+
});
|
|
3645
|
+
}
|
|
3646
|
+
if (device.viewport !== null) {
|
|
3647
|
+
await send("Emulation.setDeviceMetricsOverride", {
|
|
3648
|
+
mobile: device.isMobile,
|
|
3649
|
+
width: device.viewport.width,
|
|
3650
|
+
height: device.viewport.height,
|
|
3651
|
+
deviceScaleFactor: device.deviceScaleFactor,
|
|
3652
|
+
screenWidth: device.viewport.width,
|
|
3653
|
+
screenHeight: device.viewport.height
|
|
3654
|
+
});
|
|
3655
|
+
}
|
|
3656
|
+
if (device.hasTouch) {
|
|
3657
|
+
await send("Emulation.setTouchEmulationEnabled", { enabled: true });
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
});
|
|
3661
|
+
}
|
|
3662
|
+
async function setExtraHTTPHeadersViaPlaywright(opts) {
|
|
3663
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3664
|
+
ensurePageState(page);
|
|
3665
|
+
await page.context().setExtraHTTPHeaders(opts.headers);
|
|
3666
|
+
}
|
|
3667
|
+
async function setGeolocationViaPlaywright(opts) {
|
|
3668
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3669
|
+
ensurePageState(page);
|
|
3670
|
+
const context = page.context();
|
|
3671
|
+
if (opts.clear === true) {
|
|
3672
|
+
await context.setGeolocation(null);
|
|
3673
|
+
await context.clearPermissions().catch(() => {
|
|
3674
|
+
});
|
|
3675
|
+
return;
|
|
3676
|
+
}
|
|
3677
|
+
if (typeof opts.latitude !== "number" || typeof opts.longitude !== "number") {
|
|
3678
|
+
throw new Error("latitude and longitude are required (or set clear=true)");
|
|
3679
|
+
}
|
|
3680
|
+
await context.setGeolocation({
|
|
3681
|
+
latitude: opts.latitude,
|
|
3682
|
+
longitude: opts.longitude,
|
|
3683
|
+
accuracy: typeof opts.accuracy === "number" ? opts.accuracy : void 0
|
|
3684
|
+
});
|
|
3685
|
+
const origin = (opts.origin !== void 0 && opts.origin !== "" ? opts.origin.trim() : "") || (() => {
|
|
3686
|
+
try {
|
|
3687
|
+
return new URL(page.url()).origin;
|
|
3688
|
+
} catch {
|
|
3689
|
+
return "";
|
|
3690
|
+
}
|
|
3691
|
+
})();
|
|
3692
|
+
if (origin !== "")
|
|
3693
|
+
await context.grantPermissions(["geolocation"], { origin }).catch(() => {
|
|
3694
|
+
});
|
|
3695
|
+
}
|
|
3696
|
+
async function setHttpCredentialsViaPlaywright(opts) {
|
|
3697
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3698
|
+
ensurePageState(page);
|
|
3699
|
+
if (opts.clear === true) {
|
|
3700
|
+
await page.context().setHTTPCredentials(null);
|
|
3701
|
+
return;
|
|
3702
|
+
}
|
|
3703
|
+
const username = opts.username ?? "";
|
|
3704
|
+
const password = opts.password ?? "";
|
|
3705
|
+
if (!username) throw new Error("username is required (or set clear=true)");
|
|
3706
|
+
await page.context().setHTTPCredentials({ username, password });
|
|
3707
|
+
}
|
|
3708
|
+
async function setLocaleViaPlaywright(opts) {
|
|
3709
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3710
|
+
ensurePageState(page);
|
|
3711
|
+
const locale = opts.locale.trim();
|
|
3712
|
+
if (!locale) throw new Error("locale is required");
|
|
3713
|
+
await withPageScopedCdpClient({
|
|
3714
|
+
cdpUrl: opts.cdpUrl,
|
|
3715
|
+
page,
|
|
3716
|
+
targetId: opts.targetId,
|
|
3717
|
+
fn: async (send) => {
|
|
3718
|
+
try {
|
|
3719
|
+
await send("Emulation.setLocaleOverride", { locale });
|
|
3720
|
+
} catch (err) {
|
|
3721
|
+
if (String(err).includes("Another locale override is already in effect")) return;
|
|
3722
|
+
throw err;
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
});
|
|
3726
|
+
}
|
|
3727
|
+
async function setOfflineViaPlaywright(opts) {
|
|
3728
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3729
|
+
ensurePageState(page);
|
|
3730
|
+
await page.context().setOffline(opts.offline);
|
|
3731
|
+
}
|
|
3732
|
+
async function setTimezoneViaPlaywright(opts) {
|
|
3733
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3734
|
+
ensurePageState(page);
|
|
3735
|
+
const timezoneId = opts.timezoneId.trim();
|
|
3736
|
+
if (!timezoneId) throw new Error("timezoneId is required");
|
|
3737
|
+
await withPageScopedCdpClient({
|
|
3738
|
+
cdpUrl: opts.cdpUrl,
|
|
3739
|
+
page,
|
|
3740
|
+
targetId: opts.targetId,
|
|
3741
|
+
fn: async (send) => {
|
|
3742
|
+
try {
|
|
3743
|
+
await send("Emulation.setTimezoneOverride", { timezoneId });
|
|
3744
|
+
} catch (err) {
|
|
3745
|
+
const msg = String(err);
|
|
3746
|
+
if (msg.includes("Timezone override is already in effect")) return;
|
|
3747
|
+
if (msg.includes("Invalid timezone")) throw new Error(`Invalid timezone ID: ${timezoneId}`, { cause: err });
|
|
3748
|
+
throw err;
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
});
|
|
3752
|
+
}
|
|
3753
|
+
|
|
3754
|
+
// src/anti-bot.ts
|
|
3755
|
+
var DETECT_CHALLENGE_SCRIPT = `(function() {
|
|
3756
|
+
var title = (document.title || '').toLowerCase();
|
|
3757
|
+
|
|
3758
|
+
// Cloudflare JS challenge
|
|
3759
|
+
if (title === 'just a moment...'
|
|
3760
|
+
|| document.querySelector('#challenge-running, #cf-please-wait, #challenge-form')
|
|
3761
|
+
|| title.indexOf('checking your browser') !== -1) {
|
|
3762
|
+
return { kind: 'cloudflare-js', message: 'Cloudflare JS challenge' };
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
// Cloudflare block page (needs body text \u2014 read lazily)
|
|
3766
|
+
var body = null;
|
|
3767
|
+
function getBody() { if (body === null) body = (document.body && document.body.textContent) || ''; return body; }
|
|
3768
|
+
|
|
3769
|
+
if (title.indexOf('attention required') !== -1
|
|
3770
|
+
|| (document.querySelector('.cf-error-details') && getBody().indexOf('blocked') !== -1)) {
|
|
3771
|
+
return { kind: 'cloudflare-block', message: 'Cloudflare block page' };
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
// Cloudflare Turnstile
|
|
3775
|
+
if (document.querySelector('.cf-turnstile, iframe[src*="challenges.cloudflare.com"]')) {
|
|
3776
|
+
return { kind: 'cloudflare-turnstile', message: 'Cloudflare Turnstile challenge' };
|
|
3777
|
+
}
|
|
3778
|
+
|
|
3779
|
+
// hCaptcha
|
|
3780
|
+
if (document.querySelector('.h-captcha, iframe[src*="hcaptcha.com"]')) {
|
|
3781
|
+
return { kind: 'hcaptcha', message: 'hCaptcha challenge' };
|
|
3782
|
+
}
|
|
3783
|
+
|
|
3784
|
+
// reCAPTCHA
|
|
3785
|
+
if (document.querySelector('.g-recaptcha, iframe[src*="google.com/recaptcha"]')) {
|
|
3786
|
+
return { kind: 'recaptcha', message: 'reCAPTCHA challenge' };
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
// Generic access-denied / rate-limit pages (only read body for short pages)
|
|
3790
|
+
var b = getBody();
|
|
3791
|
+
if (b.length < 5000) {
|
|
3792
|
+
if (/access denied|403 forbidden/i.test(title) || /access denied/i.test(b)) {
|
|
3793
|
+
return { kind: 'blocked', message: 'Access denied' };
|
|
3794
|
+
}
|
|
3795
|
+
if (/\\b429\\b/i.test(title) || /too many requests|rate limit/i.test(b)) {
|
|
3796
|
+
return { kind: 'rate-limited', message: 'Rate limited' };
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
|
|
3800
|
+
return null;
|
|
3801
|
+
})()`;
|
|
3802
|
+
function parseChallengeResult(raw) {
|
|
3803
|
+
if (raw !== null && typeof raw === "object" && "kind" in raw) {
|
|
3804
|
+
return raw;
|
|
3805
|
+
}
|
|
3806
|
+
return null;
|
|
3807
|
+
}
|
|
3808
|
+
async function detectChallengeViaPlaywright(opts) {
|
|
3809
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3810
|
+
ensurePageState(page);
|
|
3811
|
+
return parseChallengeResult(await page.evaluate(DETECT_CHALLENGE_SCRIPT));
|
|
3812
|
+
}
|
|
3813
|
+
async function waitForChallengeViaPlaywright(opts) {
|
|
3814
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3815
|
+
ensurePageState(page);
|
|
3816
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 15e3);
|
|
3817
|
+
const poll = Math.max(250, Math.min(5e3, opts.pollMs ?? 500));
|
|
3818
|
+
const detect = async () => parseChallengeResult(await page.evaluate(DETECT_CHALLENGE_SCRIPT));
|
|
3819
|
+
const initial = await detect();
|
|
3820
|
+
if (initial === null) return { resolved: true, challenge: null };
|
|
3821
|
+
if (initial.kind === "cloudflare-js") {
|
|
3822
|
+
try {
|
|
3823
|
+
await page.waitForFunction(
|
|
3824
|
+
"document.title.toLowerCase() !== 'just a moment...' && !document.querySelector('#challenge-running')",
|
|
3825
|
+
void 0,
|
|
3826
|
+
{ timeout }
|
|
3827
|
+
);
|
|
3828
|
+
await page.waitForLoadState("domcontentloaded", { timeout: 5e3 }).catch(() => {
|
|
3829
|
+
});
|
|
3830
|
+
const after = await detect();
|
|
3831
|
+
return { resolved: after === null, challenge: after };
|
|
3832
|
+
} catch {
|
|
3833
|
+
const after = await detect();
|
|
3834
|
+
return { resolved: after === null, challenge: after };
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
const deadline = Date.now() + timeout;
|
|
3838
|
+
while (Date.now() < deadline) {
|
|
3839
|
+
await page.waitForTimeout(poll);
|
|
3840
|
+
const current = await detect();
|
|
3841
|
+
if (current === null) return { resolved: true, challenge: null };
|
|
3842
|
+
}
|
|
3843
|
+
const final = await detect();
|
|
3844
|
+
return { resolved: final === null, challenge: final };
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
// src/capture/activity.ts
|
|
3848
|
+
function consolePriority(level) {
|
|
3849
|
+
switch (level) {
|
|
3850
|
+
case "error":
|
|
3851
|
+
return 3;
|
|
3852
|
+
case "warning":
|
|
3853
|
+
case "warn":
|
|
3854
|
+
return 2;
|
|
3855
|
+
case "info":
|
|
3856
|
+
case "log":
|
|
3857
|
+
return 1;
|
|
3858
|
+
case "debug":
|
|
3859
|
+
return 0;
|
|
3860
|
+
default:
|
|
3861
|
+
return 1;
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
async function getConsoleMessagesViaPlaywright(opts) {
|
|
3865
|
+
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
3866
|
+
const messages = opts.level !== void 0 && opts.level !== "" ? state.console.filter((msg) => consolePriority(msg.type) >= consolePriority(opts.level ?? "")) : [...state.console];
|
|
3867
|
+
if (opts.clear === true) state.console = [];
|
|
3868
|
+
return messages;
|
|
3869
|
+
}
|
|
3870
|
+
async function getPageErrorsViaPlaywright(opts) {
|
|
3871
|
+
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
3872
|
+
const errors = [...state.errors];
|
|
3873
|
+
if (opts.clear === true) state.errors = [];
|
|
3874
|
+
return { errors };
|
|
3875
|
+
}
|
|
3876
|
+
async function getNetworkRequestsViaPlaywright(opts) {
|
|
3877
|
+
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
3878
|
+
const raw = [...state.requests];
|
|
3879
|
+
const filter = typeof opts.filter === "string" ? opts.filter.trim() : "";
|
|
3880
|
+
const requests = filter ? raw.filter((r) => r.url.includes(filter)) : raw;
|
|
3881
|
+
if (opts.clear === true) {
|
|
3882
|
+
state.requests = [];
|
|
3883
|
+
state.requestIds = /* @__PURE__ */ new WeakMap();
|
|
3884
|
+
}
|
|
3885
|
+
return { requests };
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
// src/capture/pdf.ts
|
|
3889
|
+
async function pdfViaPlaywright(opts) {
|
|
3890
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3891
|
+
ensurePageState(page);
|
|
3892
|
+
return { buffer: await page.pdf({ printBackground: true }) };
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
// src/capture/response.ts
|
|
3896
|
+
function matchUrlPattern(pattern, url) {
|
|
3897
|
+
if (!pattern || !url) return false;
|
|
3898
|
+
if (pattern === url) return true;
|
|
3899
|
+
if (pattern.includes("*")) {
|
|
3900
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
3901
|
+
try {
|
|
3902
|
+
return new RegExp(`^${escaped}$`).test(url);
|
|
3903
|
+
} catch {
|
|
3904
|
+
return false;
|
|
3750
3905
|
}
|
|
3751
|
-
return await awaitDownloadPayload({ waiter, state, armId, outPath });
|
|
3752
|
-
} catch (err) {
|
|
3753
|
-
waiter.cancel();
|
|
3754
|
-
throw err;
|
|
3755
3906
|
}
|
|
3907
|
+
return url.includes(pattern);
|
|
3756
3908
|
}
|
|
3757
|
-
async function
|
|
3909
|
+
async function responseBodyViaPlaywright(opts) {
|
|
3758
3910
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3759
|
-
|
|
3760
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
const
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
}
|
|
3771
|
-
|
|
3772
|
-
|
|
3911
|
+
ensurePageState(page);
|
|
3912
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
|
|
3913
|
+
const pattern = opts.url.trim();
|
|
3914
|
+
if (!pattern) throw new Error("url is required");
|
|
3915
|
+
const response = await page.waitForResponse((resp) => matchUrlPattern(pattern, resp.url()), { timeout });
|
|
3916
|
+
let body = await response.text();
|
|
3917
|
+
let truncated = false;
|
|
3918
|
+
const maxChars = typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars) ? Math.max(1, Math.min(5e6, Math.floor(opts.maxChars))) : 2e5;
|
|
3919
|
+
if (body.length > maxChars) {
|
|
3920
|
+
body = body.slice(0, maxChars);
|
|
3921
|
+
truncated = true;
|
|
3922
|
+
}
|
|
3923
|
+
const headers = {};
|
|
3924
|
+
const allHeaders = response.headers();
|
|
3925
|
+
for (const [key, value] of Object.entries(allHeaders)) {
|
|
3926
|
+
headers[key] = value;
|
|
3773
3927
|
}
|
|
3928
|
+
return {
|
|
3929
|
+
url: response.url(),
|
|
3930
|
+
status: response.status(),
|
|
3931
|
+
headers,
|
|
3932
|
+
body,
|
|
3933
|
+
truncated
|
|
3934
|
+
};
|
|
3774
3935
|
}
|
|
3775
|
-
|
|
3936
|
+
|
|
3937
|
+
// src/capture/screenshot.ts
|
|
3938
|
+
async function takeScreenshotViaPlaywright(opts) {
|
|
3776
3939
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3777
3940
|
ensurePageState(page);
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
const device = playwrightCore.devices[name];
|
|
3784
|
-
if (!device) {
|
|
3785
|
-
throw new Error(`Unknown device "${name}".`);
|
|
3941
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
3942
|
+
const type = opts.type ?? "png";
|
|
3943
|
+
if (opts.ref !== void 0 && opts.ref !== "") {
|
|
3944
|
+
if (opts.fullPage === true) throw new Error("fullPage is not supported for element screenshots");
|
|
3945
|
+
return { buffer: await refLocator(page, opts.ref).screenshot({ type }) };
|
|
3786
3946
|
}
|
|
3947
|
+
if (opts.element !== void 0 && opts.element !== "") {
|
|
3948
|
+
if (opts.fullPage === true) throw new Error("fullPage is not supported for element screenshots");
|
|
3949
|
+
return { buffer: await page.locator(opts.element).first().screenshot({ type }) };
|
|
3950
|
+
}
|
|
3951
|
+
return { buffer: await page.screenshot({ type, fullPage: Boolean(opts.fullPage) }) };
|
|
3952
|
+
}
|
|
3953
|
+
async function screenshotWithLabelsViaPlaywright(opts) {
|
|
3787
3954
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3788
3955
|
ensurePageState(page);
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
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
|
-
});
|
|
3956
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
3957
|
+
const maxLabels = typeof opts.maxLabels === "number" && Number.isFinite(opts.maxLabels) ? Math.max(1, Math.floor(opts.maxLabels)) : 150;
|
|
3958
|
+
const type = opts.type ?? "png";
|
|
3959
|
+
const refs = opts.refs.slice(0, maxLabels);
|
|
3960
|
+
const skipped = opts.refs.slice(maxLabels);
|
|
3961
|
+
const viewport = await page.evaluate(() => ({
|
|
3962
|
+
width: window.innerWidth || 0,
|
|
3963
|
+
height: window.innerHeight || 0
|
|
3964
|
+
}));
|
|
3965
|
+
const labels = [];
|
|
3966
|
+
for (let i = 0; i < refs.length; i++) {
|
|
3967
|
+
const ref = refs[i];
|
|
3968
|
+
try {
|
|
3969
|
+
const locator = refLocator(page, ref);
|
|
3970
|
+
const box = await locator.boundingBox({ timeout: 2e3 });
|
|
3971
|
+
if (!box) {
|
|
3972
|
+
skipped.push(ref);
|
|
3973
|
+
continue;
|
|
3816
3974
|
}
|
|
3817
|
-
|
|
3818
|
-
|
|
3975
|
+
const x1 = box.x + box.width;
|
|
3976
|
+
const y1 = box.y + box.height;
|
|
3977
|
+
if (x1 < 0 || box.x > viewport.width || y1 < 0 || box.y > viewport.height) {
|
|
3978
|
+
skipped.push(ref);
|
|
3979
|
+
continue;
|
|
3819
3980
|
}
|
|
3981
|
+
labels.push({ ref, index: i + 1, box });
|
|
3982
|
+
} catch {
|
|
3983
|
+
skipped.push(ref);
|
|
3820
3984
|
}
|
|
3821
|
-
}
|
|
3985
|
+
}
|
|
3986
|
+
try {
|
|
3987
|
+
if (labels.length > 0) {
|
|
3988
|
+
await page.evaluate(
|
|
3989
|
+
(labelData) => {
|
|
3990
|
+
document.querySelectorAll("[data-browserclaw-labels]").forEach((el) => {
|
|
3991
|
+
el.remove();
|
|
3992
|
+
});
|
|
3993
|
+
const container = document.createElement("div");
|
|
3994
|
+
container.setAttribute("data-browserclaw-labels", "1");
|
|
3995
|
+
container.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:2147483647;";
|
|
3996
|
+
for (const { index, box } of labelData) {
|
|
3997
|
+
const border = document.createElement("div");
|
|
3998
|
+
border.style.cssText = `position:absolute;left:${String(box.x)}px;top:${String(box.y)}px;width:${String(box.width)}px;height:${String(box.height)}px;border:2px solid #FF4500;box-sizing:border-box;`;
|
|
3999
|
+
container.appendChild(border);
|
|
4000
|
+
const badge = document.createElement("div");
|
|
4001
|
+
badge.textContent = String(index);
|
|
4002
|
+
badge.style.cssText = `position:absolute;left:${String(box.x)}px;top:${String(Math.max(0, box.y - 18))}px;background:#FF4500;color:#fff;font:bold 12px/16px monospace;padding:0 4px;border-radius:2px;`;
|
|
4003
|
+
container.appendChild(badge);
|
|
4004
|
+
}
|
|
4005
|
+
document.documentElement.appendChild(container);
|
|
4006
|
+
},
|
|
4007
|
+
labels.map((l) => ({ index: l.index, box: l.box }))
|
|
4008
|
+
);
|
|
4009
|
+
}
|
|
4010
|
+
return {
|
|
4011
|
+
buffer: await page.screenshot({ type }),
|
|
4012
|
+
labels,
|
|
4013
|
+
skipped
|
|
4014
|
+
};
|
|
4015
|
+
} finally {
|
|
4016
|
+
await page.evaluate(() => {
|
|
4017
|
+
document.querySelectorAll("[data-browserclaw-labels]").forEach((el) => {
|
|
4018
|
+
el.remove();
|
|
4019
|
+
});
|
|
4020
|
+
}).catch(() => {
|
|
4021
|
+
});
|
|
4022
|
+
}
|
|
3822
4023
|
}
|
|
3823
|
-
async function
|
|
4024
|
+
async function traceStartViaPlaywright(opts) {
|
|
3824
4025
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3825
4026
|
ensurePageState(page);
|
|
3826
|
-
|
|
4027
|
+
const context = page.context();
|
|
4028
|
+
const ctxState = ensureContextState(context);
|
|
4029
|
+
if (ctxState.traceActive) {
|
|
4030
|
+
throw new Error("Trace already running. Stop the current trace before starting a new one.");
|
|
4031
|
+
}
|
|
4032
|
+
await context.tracing.start({
|
|
4033
|
+
screenshots: opts.screenshots ?? true,
|
|
4034
|
+
snapshots: opts.snapshots ?? true,
|
|
4035
|
+
sources: opts.sources ?? false
|
|
4036
|
+
});
|
|
4037
|
+
ctxState.traceActive = true;
|
|
3827
4038
|
}
|
|
3828
|
-
async function
|
|
4039
|
+
async function traceStopViaPlaywright(opts) {
|
|
4040
|
+
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
3829
4041
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3830
4042
|
ensurePageState(page);
|
|
3831
4043
|
const context = page.context();
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
});
|
|
3836
|
-
return;
|
|
3837
|
-
}
|
|
3838
|
-
if (typeof opts.latitude !== "number" || typeof opts.longitude !== "number") {
|
|
3839
|
-
throw new Error("latitude and longitude are required (or set clear=true)");
|
|
4044
|
+
const ctxState = ensureContextState(context);
|
|
4045
|
+
if (!ctxState.traceActive) {
|
|
4046
|
+
throw new Error("No active trace. Start a trace before stopping it.");
|
|
3840
4047
|
}
|
|
3841
|
-
await
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
const origin = opts.origin?.trim() || (() => {
|
|
3847
|
-
try {
|
|
3848
|
-
return new URL(page.url()).origin;
|
|
3849
|
-
} catch {
|
|
3850
|
-
return "";
|
|
4048
|
+
await writeViaSiblingTempPath({
|
|
4049
|
+
rootDir: path.dirname(opts.path),
|
|
4050
|
+
targetPath: opts.path,
|
|
4051
|
+
writeTemp: async (tempPath) => {
|
|
4052
|
+
await context.tracing.stop({ path: tempPath });
|
|
3851
4053
|
}
|
|
3852
|
-
})();
|
|
3853
|
-
if (origin) await context.grantPermissions(["geolocation"], { origin }).catch(() => {
|
|
3854
4054
|
});
|
|
4055
|
+
ctxState.traceActive = false;
|
|
3855
4056
|
}
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
4057
|
+
|
|
4058
|
+
// src/snapshot/ref-map.ts
|
|
4059
|
+
var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
4060
|
+
"button",
|
|
4061
|
+
"link",
|
|
4062
|
+
"textbox",
|
|
4063
|
+
"checkbox",
|
|
4064
|
+
"radio",
|
|
4065
|
+
"combobox",
|
|
4066
|
+
"listbox",
|
|
4067
|
+
"menuitem",
|
|
4068
|
+
"menuitemcheckbox",
|
|
4069
|
+
"menuitemradio",
|
|
4070
|
+
"option",
|
|
4071
|
+
"searchbox",
|
|
4072
|
+
"slider",
|
|
4073
|
+
"spinbutton",
|
|
4074
|
+
"switch",
|
|
4075
|
+
"tab",
|
|
4076
|
+
"treeitem"
|
|
4077
|
+
]);
|
|
4078
|
+
var CONTENT_ROLES = /* @__PURE__ */ new Set([
|
|
4079
|
+
"heading",
|
|
4080
|
+
"cell",
|
|
4081
|
+
"gridcell",
|
|
4082
|
+
"columnheader",
|
|
4083
|
+
"rowheader",
|
|
4084
|
+
"listitem",
|
|
4085
|
+
"article",
|
|
4086
|
+
"region",
|
|
4087
|
+
"main",
|
|
4088
|
+
"navigation"
|
|
4089
|
+
]);
|
|
4090
|
+
var STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
|
|
4091
|
+
"generic",
|
|
4092
|
+
"group",
|
|
4093
|
+
"list",
|
|
4094
|
+
"table",
|
|
4095
|
+
"row",
|
|
4096
|
+
"rowgroup",
|
|
4097
|
+
"grid",
|
|
4098
|
+
"treegrid",
|
|
4099
|
+
"menu",
|
|
4100
|
+
"menubar",
|
|
4101
|
+
"toolbar",
|
|
4102
|
+
"tablist",
|
|
4103
|
+
"tree",
|
|
4104
|
+
"directory",
|
|
4105
|
+
"document",
|
|
4106
|
+
"application",
|
|
4107
|
+
"presentation",
|
|
4108
|
+
"none"
|
|
4109
|
+
]);
|
|
4110
|
+
function getIndentLevel(line) {
|
|
4111
|
+
const match = /^(\s*)/.exec(line);
|
|
4112
|
+
return match ? Math.floor(match[1].length / 2) : 0;
|
|
4113
|
+
}
|
|
4114
|
+
function matchInteractiveSnapshotLine(line, options) {
|
|
4115
|
+
const depth = getIndentLevel(line);
|
|
4116
|
+
if (options.maxDepth !== void 0 && depth > options.maxDepth) {
|
|
4117
|
+
return null;
|
|
3862
4118
|
}
|
|
3863
|
-
const
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
4119
|
+
const match = /^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/.exec(line);
|
|
4120
|
+
if (!match) {
|
|
4121
|
+
return null;
|
|
4122
|
+
}
|
|
4123
|
+
const [, , roleRaw, name, suffix] = match;
|
|
4124
|
+
if (roleRaw.startsWith("/")) {
|
|
4125
|
+
return null;
|
|
4126
|
+
}
|
|
4127
|
+
const role = roleRaw.toLowerCase();
|
|
4128
|
+
return {
|
|
4129
|
+
roleRaw,
|
|
4130
|
+
role,
|
|
4131
|
+
...name ? { name } : {},
|
|
4132
|
+
suffix
|
|
4133
|
+
};
|
|
3867
4134
|
}
|
|
3868
|
-
|
|
3869
|
-
const
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
4135
|
+
function createRoleNameTracker() {
|
|
4136
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4137
|
+
const refsByKey = /* @__PURE__ */ new Map();
|
|
4138
|
+
return {
|
|
4139
|
+
counts,
|
|
4140
|
+
refsByKey,
|
|
4141
|
+
getKey(role, name) {
|
|
4142
|
+
return `${role}:${name ?? ""}`;
|
|
4143
|
+
},
|
|
4144
|
+
getNextIndex(role, name) {
|
|
4145
|
+
const key = this.getKey(role, name);
|
|
4146
|
+
const current = counts.get(key) ?? 0;
|
|
4147
|
+
counts.set(key, current + 1);
|
|
4148
|
+
return current;
|
|
4149
|
+
},
|
|
4150
|
+
trackRef(role, name, ref) {
|
|
4151
|
+
const key = this.getKey(role, name);
|
|
4152
|
+
const list = refsByKey.get(key) ?? [];
|
|
4153
|
+
list.push(ref);
|
|
4154
|
+
refsByKey.set(key, list);
|
|
4155
|
+
},
|
|
4156
|
+
getDuplicateKeys() {
|
|
4157
|
+
const out = /* @__PURE__ */ new Set();
|
|
4158
|
+
for (const [key, refs] of refsByKey) if (refs.length > 1) out.add(key);
|
|
4159
|
+
return out;
|
|
3884
4160
|
}
|
|
3885
|
-
}
|
|
4161
|
+
};
|
|
3886
4162
|
}
|
|
3887
|
-
|
|
3888
|
-
const
|
|
3889
|
-
|
|
3890
|
-
|
|
4163
|
+
function removeNthFromNonDuplicates(refs, tracker) {
|
|
4164
|
+
const duplicates = tracker.getDuplicateKeys();
|
|
4165
|
+
for (const [ref, data] of Object.entries(refs)) {
|
|
4166
|
+
const key = tracker.getKey(data.role, data.name);
|
|
4167
|
+
if (!duplicates.has(key)) delete refs[ref].nth;
|
|
4168
|
+
}
|
|
3891
4169
|
}
|
|
3892
|
-
|
|
3893
|
-
const
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
4170
|
+
function compactTree(tree) {
|
|
4171
|
+
const lines = tree.split("\n");
|
|
4172
|
+
const result = [];
|
|
4173
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4174
|
+
const line = lines[i];
|
|
4175
|
+
if (line.includes("[ref=")) {
|
|
4176
|
+
result.push(line);
|
|
4177
|
+
continue;
|
|
4178
|
+
}
|
|
4179
|
+
if (line.includes(":") && !line.trimEnd().endsWith(":")) {
|
|
4180
|
+
result.push(line);
|
|
4181
|
+
continue;
|
|
4182
|
+
}
|
|
4183
|
+
const currentIndent = getIndentLevel(line);
|
|
4184
|
+
let hasRelevantChildren = false;
|
|
4185
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
4186
|
+
if (getIndentLevel(lines[j]) <= currentIndent) break;
|
|
4187
|
+
if (lines[j]?.includes("[ref=")) {
|
|
4188
|
+
hasRelevantChildren = true;
|
|
4189
|
+
break;
|
|
3909
4190
|
}
|
|
3910
4191
|
}
|
|
3911
|
-
|
|
4192
|
+
if (hasRelevantChildren) result.push(line);
|
|
4193
|
+
}
|
|
4194
|
+
return result.join("\n");
|
|
3912
4195
|
}
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
const
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
4196
|
+
function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
|
|
4197
|
+
const lines = ariaSnapshot.split("\n");
|
|
4198
|
+
const refs = {};
|
|
4199
|
+
const tracker = createRoleNameTracker();
|
|
4200
|
+
let counter = 0;
|
|
4201
|
+
const nextRef = () => {
|
|
4202
|
+
counter++;
|
|
4203
|
+
return `e${String(counter)}`;
|
|
4204
|
+
};
|
|
4205
|
+
if (options.interactive === true) {
|
|
4206
|
+
const result2 = [];
|
|
4207
|
+
for (const line of lines) {
|
|
4208
|
+
const parsed = matchInteractiveSnapshotLine(line, options);
|
|
4209
|
+
if (!parsed) continue;
|
|
4210
|
+
const { roleRaw, role, name, suffix } = parsed;
|
|
4211
|
+
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
4212
|
+
const prefix = /^(\s*-\s*)/.exec(line)?.[1] ?? "";
|
|
4213
|
+
const ref = nextRef();
|
|
4214
|
+
const nth = tracker.getNextIndex(role, name);
|
|
4215
|
+
tracker.trackRef(role, name, ref);
|
|
4216
|
+
refs[ref] = { role, name, nth };
|
|
4217
|
+
let enhanced = `${prefix}${roleRaw}`;
|
|
4218
|
+
if (name !== void 0 && name !== "") enhanced += ` "${name}"`;
|
|
4219
|
+
enhanced += ` [ref=${ref}]`;
|
|
4220
|
+
if (nth > 0) enhanced += ` [nth=${String(nth)}]`;
|
|
4221
|
+
if (suffix.includes("[")) enhanced += suffix;
|
|
4222
|
+
result2.push(enhanced);
|
|
4223
|
+
}
|
|
4224
|
+
removeNthFromNonDuplicates(refs, tracker);
|
|
4225
|
+
return { snapshot: result2.join("\n") || "(no interactive elements)", refs };
|
|
3923
4226
|
}
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
4227
|
+
const result = [];
|
|
4228
|
+
for (const line of lines) {
|
|
4229
|
+
const depth = getIndentLevel(line);
|
|
4230
|
+
if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
|
|
4231
|
+
const match = /^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/.exec(line);
|
|
4232
|
+
if (!match) {
|
|
4233
|
+
result.push(line);
|
|
4234
|
+
continue;
|
|
4235
|
+
}
|
|
4236
|
+
const [, prefix, roleRaw, name, suffix] = match;
|
|
4237
|
+
if (roleRaw.startsWith("/")) {
|
|
4238
|
+
result.push(line);
|
|
4239
|
+
continue;
|
|
4240
|
+
}
|
|
4241
|
+
const role = roleRaw.toLowerCase();
|
|
4242
|
+
const isInteractive = INTERACTIVE_ROLES.has(role);
|
|
4243
|
+
const isContent = CONTENT_ROLES.has(role);
|
|
4244
|
+
const isStructural = STRUCTURAL_ROLES.has(role);
|
|
4245
|
+
if (options.compact === true && isStructural && name === "") continue;
|
|
4246
|
+
if (!(isInteractive || isContent && name !== "")) {
|
|
4247
|
+
result.push(line);
|
|
4248
|
+
continue;
|
|
4249
|
+
}
|
|
4250
|
+
const ref = nextRef();
|
|
4251
|
+
const nth = tracker.getNextIndex(role, name);
|
|
4252
|
+
tracker.trackRef(role, name, ref);
|
|
4253
|
+
refs[ref] = { role, name, nth };
|
|
4254
|
+
let enhanced = `${prefix}${roleRaw}`;
|
|
4255
|
+
if (name !== "") enhanced += ` "${name}"`;
|
|
4256
|
+
enhanced += ` [ref=${ref}]`;
|
|
4257
|
+
if (nth > 0) enhanced += ` [nth=${String(nth)}]`;
|
|
4258
|
+
if (suffix !== "") enhanced += suffix;
|
|
4259
|
+
result.push(enhanced);
|
|
3927
4260
|
}
|
|
3928
|
-
|
|
4261
|
+
removeNthFromNonDuplicates(refs, tracker);
|
|
4262
|
+
const tree = result.join("\n") || "(empty)";
|
|
4263
|
+
return { snapshot: options.compact === true ? compactTree(tree) : tree, refs };
|
|
3929
4264
|
}
|
|
3930
|
-
|
|
3931
|
-
const
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
const
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
skipped.push(ref);
|
|
3950
|
-
continue;
|
|
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 });
|
|
3959
|
-
} catch {
|
|
3960
|
-
skipped.push(ref);
|
|
4265
|
+
function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
|
|
4266
|
+
const lines = aiSnapshot.split("\n");
|
|
4267
|
+
const refs = {};
|
|
4268
|
+
function parseAiSnapshotRef(suffix) {
|
|
4269
|
+
const match = /\[ref=(e\d+)\]/i.exec(suffix);
|
|
4270
|
+
return match ? match[1] : null;
|
|
4271
|
+
}
|
|
4272
|
+
if (options.interactive === true) {
|
|
4273
|
+
const out2 = [];
|
|
4274
|
+
for (const line of lines) {
|
|
4275
|
+
const parsed = matchInteractiveSnapshotLine(line, options);
|
|
4276
|
+
if (!parsed) continue;
|
|
4277
|
+
const { roleRaw, role, name, suffix } = parsed;
|
|
4278
|
+
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
4279
|
+
const ref = parseAiSnapshotRef(suffix);
|
|
4280
|
+
if (ref === null) continue;
|
|
4281
|
+
const prefix = /^(\s*-\s*)/.exec(line)?.[1] ?? "";
|
|
4282
|
+
refs[ref] = { role, ...name !== void 0 && name !== "" ? { name } : {} };
|
|
4283
|
+
out2.push(`${prefix}${roleRaw}${name !== void 0 && name !== "" ? ` "${name}"` : ""}${suffix}`);
|
|
3961
4284
|
}
|
|
4285
|
+
return { snapshot: out2.join("\n") || "(no interactive elements)", refs };
|
|
3962
4286
|
}
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
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 })));
|
|
4287
|
+
const out = [];
|
|
4288
|
+
for (const line of lines) {
|
|
4289
|
+
const depth = getIndentLevel(line);
|
|
4290
|
+
if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
|
|
4291
|
+
const match = /^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/.exec(line);
|
|
4292
|
+
if (!match) {
|
|
4293
|
+
out.push(line);
|
|
4294
|
+
continue;
|
|
3981
4295
|
}
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
}
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
}
|
|
4296
|
+
const [, , roleRaw, name, suffix] = match;
|
|
4297
|
+
if (roleRaw.startsWith("/")) {
|
|
4298
|
+
out.push(line);
|
|
4299
|
+
continue;
|
|
4300
|
+
}
|
|
4301
|
+
const role = roleRaw.toLowerCase();
|
|
4302
|
+
const isStructural = STRUCTURAL_ROLES.has(role);
|
|
4303
|
+
if (options.compact === true && isStructural && name === "") continue;
|
|
4304
|
+
const ref = parseAiSnapshotRef(suffix);
|
|
4305
|
+
if (ref !== null) refs[ref] = { role, ...name !== "" ? { name } : {} };
|
|
4306
|
+
out.push(line);
|
|
3992
4307
|
}
|
|
4308
|
+
const tree = out.join("\n") || "(empty)";
|
|
4309
|
+
return { snapshot: options.compact === true ? compactTree(tree) : tree, refs };
|
|
3993
4310
|
}
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4311
|
+
function getRoleSnapshotStats(snapshot, refs) {
|
|
4312
|
+
const interactive = Object.values(refs).filter((r) => INTERACTIVE_ROLES.has(r.role)).length;
|
|
4313
|
+
return {
|
|
4314
|
+
lines: snapshot.split("\n").length,
|
|
4315
|
+
chars: snapshot.length,
|
|
4316
|
+
refs: Object.keys(refs).length,
|
|
4317
|
+
interactive
|
|
4318
|
+
};
|
|
4000
4319
|
}
|
|
4001
|
-
|
|
4320
|
+
|
|
4321
|
+
// src/snapshot/ai-snapshot.ts
|
|
4322
|
+
async function snapshotAi(opts) {
|
|
4002
4323
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4003
4324
|
ensurePageState(page);
|
|
4004
|
-
const
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
throw new Error("Trace already running. Stop the current trace before starting a new one.");
|
|
4325
|
+
const maybe = page;
|
|
4326
|
+
if (!maybe._snapshotForAI) {
|
|
4327
|
+
throw new Error("Playwright _snapshotForAI is not available. Upgrade playwright-core to >= 1.50.");
|
|
4008
4328
|
}
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4329
|
+
const sourceUrl = page.url();
|
|
4330
|
+
const result = await maybe._snapshotForAI({
|
|
4331
|
+
timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3, 6e4),
|
|
4332
|
+
track: "response"
|
|
4013
4333
|
});
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4334
|
+
let snapshot = String(result.full);
|
|
4335
|
+
const maxChars = opts.maxChars;
|
|
4336
|
+
const limit = typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : void 0;
|
|
4337
|
+
let truncated = false;
|
|
4338
|
+
if (limit !== void 0 && snapshot.length > limit) {
|
|
4339
|
+
const lastNewline = snapshot.lastIndexOf("\n", limit);
|
|
4340
|
+
const cutoff = lastNewline > 0 ? lastNewline : limit;
|
|
4341
|
+
snapshot = `${snapshot.slice(0, cutoff)}
|
|
4342
|
+
|
|
4343
|
+
[...TRUNCATED - page too large]`;
|
|
4344
|
+
truncated = true;
|
|
4024
4345
|
}
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4346
|
+
const built = buildRoleSnapshotFromAiSnapshot(snapshot, opts.options);
|
|
4347
|
+
storeRoleRefsForTarget({
|
|
4348
|
+
page,
|
|
4349
|
+
cdpUrl: opts.cdpUrl,
|
|
4350
|
+
targetId: opts.targetId,
|
|
4351
|
+
refs: built.refs,
|
|
4352
|
+
mode: "aria"
|
|
4031
4353
|
});
|
|
4032
|
-
|
|
4354
|
+
return {
|
|
4355
|
+
snapshot: built.snapshot,
|
|
4356
|
+
refs: built.refs,
|
|
4357
|
+
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
4358
|
+
...truncated ? { truncated } : {},
|
|
4359
|
+
untrusted: true,
|
|
4360
|
+
contentMeta: {
|
|
4361
|
+
sourceUrl,
|
|
4362
|
+
contentType: "browser-snapshot",
|
|
4363
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4364
|
+
}
|
|
4365
|
+
};
|
|
4033
4366
|
}
|
|
4034
4367
|
|
|
4035
|
-
// src/
|
|
4036
|
-
function
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
}
|
|
4044
|
-
|
|
4368
|
+
// src/snapshot/aria-snapshot.ts
|
|
4369
|
+
async function snapshotRole(opts) {
|
|
4370
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4371
|
+
ensurePageState(page);
|
|
4372
|
+
const sourceUrl = page.url();
|
|
4373
|
+
if (opts.refsMode === "aria") {
|
|
4374
|
+
if (opts.selector !== void 0 && opts.selector.trim() !== "" || opts.frameSelector !== void 0 && opts.frameSelector.trim() !== "") {
|
|
4375
|
+
throw new Error("refs=aria does not support selector/frame snapshots yet.");
|
|
4376
|
+
}
|
|
4377
|
+
const maybe = page;
|
|
4378
|
+
if (!maybe._snapshotForAI) {
|
|
4379
|
+
throw new Error("refs=aria requires Playwright _snapshotForAI support.");
|
|
4045
4380
|
}
|
|
4381
|
+
const result = await maybe._snapshotForAI({ timeout: 5e3, track: "response" });
|
|
4382
|
+
const built2 = buildRoleSnapshotFromAiSnapshot(String(result.full), opts.options);
|
|
4383
|
+
storeRoleRefsForTarget({
|
|
4384
|
+
page,
|
|
4385
|
+
cdpUrl: opts.cdpUrl,
|
|
4386
|
+
targetId: opts.targetId,
|
|
4387
|
+
refs: built2.refs,
|
|
4388
|
+
mode: "aria"
|
|
4389
|
+
});
|
|
4390
|
+
return {
|
|
4391
|
+
snapshot: built2.snapshot,
|
|
4392
|
+
refs: built2.refs,
|
|
4393
|
+
stats: getRoleSnapshotStats(built2.snapshot, built2.refs),
|
|
4394
|
+
untrusted: true,
|
|
4395
|
+
contentMeta: {
|
|
4396
|
+
sourceUrl,
|
|
4397
|
+
contentType: "browser-snapshot",
|
|
4398
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4399
|
+
}
|
|
4400
|
+
};
|
|
4046
4401
|
}
|
|
4047
|
-
|
|
4402
|
+
const frameSelector = opts.frameSelector?.trim() ?? "";
|
|
4403
|
+
const selector = opts.selector?.trim() ?? "";
|
|
4404
|
+
const locator = frameSelector ? selector ? page.frameLocator(frameSelector).locator(selector) : page.frameLocator(frameSelector).locator(":root") : selector ? page.locator(selector) : page.locator(":root");
|
|
4405
|
+
const ariaSnapshot = await locator.ariaSnapshot({ timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3) });
|
|
4406
|
+
const built = buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, opts.options);
|
|
4407
|
+
storeRoleRefsForTarget({
|
|
4408
|
+
page,
|
|
4409
|
+
cdpUrl: opts.cdpUrl,
|
|
4410
|
+
targetId: opts.targetId,
|
|
4411
|
+
refs: built.refs,
|
|
4412
|
+
frameSelector: frameSelector !== "" ? frameSelector : void 0,
|
|
4413
|
+
mode: "role"
|
|
4414
|
+
});
|
|
4415
|
+
return {
|
|
4416
|
+
snapshot: built.snapshot,
|
|
4417
|
+
refs: built.refs,
|
|
4418
|
+
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
4419
|
+
untrusted: true,
|
|
4420
|
+
contentMeta: {
|
|
4421
|
+
sourceUrl,
|
|
4422
|
+
contentType: "browser-snapshot",
|
|
4423
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4424
|
+
}
|
|
4425
|
+
};
|
|
4048
4426
|
}
|
|
4049
|
-
async function
|
|
4427
|
+
async function snapshotAria(opts) {
|
|
4428
|
+
const limit = Math.max(1, Math.min(2e3, Math.floor(opts.limit ?? 500)));
|
|
4050
4429
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4051
4430
|
ensurePageState(page);
|
|
4052
|
-
const
|
|
4053
|
-
const
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
);
|
|
4059
|
-
let body = await response.text();
|
|
4060
|
-
let truncated = false;
|
|
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) {
|
|
4063
|
-
body = body.slice(0, maxChars);
|
|
4064
|
-
truncated = true;
|
|
4065
|
-
}
|
|
4066
|
-
const headers = {};
|
|
4067
|
-
const allHeaders = response.headers();
|
|
4068
|
-
for (const [key, value] of Object.entries(allHeaders)) {
|
|
4069
|
-
headers[key] = value;
|
|
4070
|
-
}
|
|
4431
|
+
const sourceUrl = page.url();
|
|
4432
|
+
const res = await withPlaywrightPageCdpSession(page, async (session) => {
|
|
4433
|
+
await session.send("Accessibility.enable").catch(() => {
|
|
4434
|
+
});
|
|
4435
|
+
return await session.send("Accessibility.getFullAXTree");
|
|
4436
|
+
});
|
|
4071
4437
|
return {
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4438
|
+
nodes: formatAriaNodes(Array.isArray(res.nodes) ? res.nodes : [], limit),
|
|
4439
|
+
untrusted: true,
|
|
4440
|
+
contentMeta: {
|
|
4441
|
+
sourceUrl,
|
|
4442
|
+
contentType: "browser-aria-tree",
|
|
4443
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4444
|
+
}
|
|
4077
4445
|
};
|
|
4078
4446
|
}
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
case "warning":
|
|
4086
|
-
case "warn":
|
|
4087
|
-
return 2;
|
|
4088
|
-
case "info":
|
|
4089
|
-
case "log":
|
|
4090
|
-
return 1;
|
|
4091
|
-
case "debug":
|
|
4092
|
-
return 0;
|
|
4093
|
-
default:
|
|
4094
|
-
return 1;
|
|
4095
|
-
}
|
|
4096
|
-
}
|
|
4097
|
-
async function getConsoleMessagesViaPlaywright(opts) {
|
|
4098
|
-
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
4099
|
-
const messages = opts.level ? state.console.filter((msg) => consolePriority(msg.type) >= consolePriority(opts.level)) : [...state.console];
|
|
4100
|
-
if (opts.clear) state.console = [];
|
|
4101
|
-
return messages;
|
|
4102
|
-
}
|
|
4103
|
-
async function getPageErrorsViaPlaywright(opts) {
|
|
4104
|
-
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
4105
|
-
const errors = [...state.errors];
|
|
4106
|
-
if (opts.clear) state.errors = [];
|
|
4107
|
-
return { errors };
|
|
4447
|
+
function axValue(v) {
|
|
4448
|
+
if (!v || typeof v !== "object") return "";
|
|
4449
|
+
const value = v.value;
|
|
4450
|
+
if (typeof value === "string") return value;
|
|
4451
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
4452
|
+
return "";
|
|
4108
4453
|
}
|
|
4109
|
-
|
|
4110
|
-
const
|
|
4111
|
-
const
|
|
4112
|
-
const
|
|
4113
|
-
const
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4454
|
+
function formatAriaNodes(nodes, limit) {
|
|
4455
|
+
const byId = /* @__PURE__ */ new Map();
|
|
4456
|
+
for (const n of nodes) if (n.nodeId) byId.set(n.nodeId, n);
|
|
4457
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
4458
|
+
for (const n of nodes) for (const c of n.childIds ?? []) referenced.add(c);
|
|
4459
|
+
const root = nodes.find((n) => n.nodeId !== "" && !referenced.has(n.nodeId)) ?? nodes[0];
|
|
4460
|
+
if (root.nodeId === "") return [];
|
|
4461
|
+
const out = [];
|
|
4462
|
+
const stack = [{ id: root.nodeId, depth: 0 }];
|
|
4463
|
+
while (stack.length && out.length < limit) {
|
|
4464
|
+
const popped = stack.pop();
|
|
4465
|
+
if (!popped) break;
|
|
4466
|
+
const { id, depth } = popped;
|
|
4467
|
+
const n = byId.get(id);
|
|
4468
|
+
if (!n) continue;
|
|
4469
|
+
const role = axValue(n.role);
|
|
4470
|
+
const name = axValue(n.name);
|
|
4471
|
+
const value = axValue(n.value);
|
|
4472
|
+
const description = axValue(n.description);
|
|
4473
|
+
const ref = `ax${String(out.length + 1)}`;
|
|
4474
|
+
out.push({
|
|
4475
|
+
ref,
|
|
4476
|
+
role: role || "unknown",
|
|
4477
|
+
name: name || "",
|
|
4478
|
+
...value ? { value } : {},
|
|
4479
|
+
...description ? { description } : {},
|
|
4480
|
+
...typeof n.backendDOMNodeId === "number" ? { backendDOMNodeId: n.backendDOMNodeId } : {},
|
|
4481
|
+
depth
|
|
4482
|
+
});
|
|
4483
|
+
const children = (n.childIds ?? []).filter((c) => byId.has(c));
|
|
4484
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
4485
|
+
if (children[i]) stack.push({ id: children[i], depth: depth + 1 });
|
|
4486
|
+
}
|
|
4117
4487
|
}
|
|
4118
|
-
return
|
|
4488
|
+
return out;
|
|
4119
4489
|
}
|
|
4120
4490
|
|
|
4121
4491
|
// src/storage/index.ts
|
|
@@ -4128,9 +4498,9 @@ async function cookiesSetViaPlaywright(opts) {
|
|
|
4128
4498
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4129
4499
|
ensurePageState(page);
|
|
4130
4500
|
const cookie = opts.cookie;
|
|
4131
|
-
if (
|
|
4132
|
-
const hasUrl = typeof cookie.url === "string" && cookie.url.trim();
|
|
4133
|
-
const hasDomainPath = typeof cookie.domain === "string" && cookie.domain.trim() && typeof cookie.path === "string" && cookie.path.trim();
|
|
4501
|
+
if (cookie.name === "") throw new Error("cookie name and value are required");
|
|
4502
|
+
const hasUrl = typeof cookie.url === "string" && cookie.url.trim() !== "";
|
|
4503
|
+
const hasDomainPath = typeof cookie.domain === "string" && cookie.domain.trim() !== "" && typeof cookie.path === "string" && cookie.path.trim() !== "";
|
|
4134
4504
|
if (!hasUrl && !hasDomainPath) throw new Error("cookie requires url, or domain+path");
|
|
4135
4505
|
await page.context().addCookies([cookie]);
|
|
4136
4506
|
}
|
|
@@ -4146,33 +4516,33 @@ async function storageGetViaPlaywright(opts) {
|
|
|
4146
4516
|
values: await page.evaluate(
|
|
4147
4517
|
({ kind, key }) => {
|
|
4148
4518
|
const store = kind === "session" ? window.sessionStorage : window.localStorage;
|
|
4149
|
-
if (key) {
|
|
4519
|
+
if (key !== void 0 && key !== "") {
|
|
4150
4520
|
const value = store.getItem(key);
|
|
4151
4521
|
return value === null ? {} : { [key]: value };
|
|
4152
4522
|
}
|
|
4153
4523
|
const out = {};
|
|
4154
4524
|
for (let i = 0; i < store.length; i++) {
|
|
4155
4525
|
const k = store.key(i);
|
|
4156
|
-
if (
|
|
4526
|
+
if (k === null || k === "") continue;
|
|
4157
4527
|
const v = store.getItem(k);
|
|
4158
4528
|
if (v !== null) out[k] = v;
|
|
4159
4529
|
}
|
|
4160
4530
|
return out;
|
|
4161
4531
|
},
|
|
4162
4532
|
{ kind: opts.kind, key: opts.key }
|
|
4163
|
-
)
|
|
4533
|
+
)
|
|
4164
4534
|
};
|
|
4165
4535
|
}
|
|
4166
4536
|
async function storageSetViaPlaywright(opts) {
|
|
4167
|
-
const key =
|
|
4168
|
-
if (
|
|
4537
|
+
const key = opts.key;
|
|
4538
|
+
if (key === "") throw new Error("key is required");
|
|
4169
4539
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4170
4540
|
ensurePageState(page);
|
|
4171
4541
|
await page.evaluate(
|
|
4172
4542
|
({ kind, key: k, value }) => {
|
|
4173
4543
|
(kind === "session" ? window.sessionStorage : window.localStorage).setItem(k, value);
|
|
4174
4544
|
},
|
|
4175
|
-
{ kind: opts.kind, key, value:
|
|
4545
|
+
{ kind: opts.kind, key, value: opts.value }
|
|
4176
4546
|
);
|
|
4177
4547
|
}
|
|
4178
4548
|
async function storageClearViaPlaywright(opts) {
|
|
@@ -4239,8 +4609,10 @@ var CrawlPage = class {
|
|
|
4239
4609
|
}
|
|
4240
4610
|
});
|
|
4241
4611
|
}
|
|
4242
|
-
if (opts?.selector || opts?.frameSelector) {
|
|
4243
|
-
throw new Error(
|
|
4612
|
+
if (opts?.selector !== void 0 && opts.selector !== "" || opts?.frameSelector !== void 0 && opts.frameSelector !== "") {
|
|
4613
|
+
throw new Error(
|
|
4614
|
+
'selector and frameSelector are only supported in role mode. Use { mode: "role" } or omit these options.'
|
|
4615
|
+
);
|
|
4244
4616
|
}
|
|
4245
4617
|
return snapshotAi({
|
|
4246
4618
|
cdpUrl: this.cdpUrl,
|
|
@@ -5076,6 +5448,53 @@ var CrawlPage = class {
|
|
|
5076
5448
|
name
|
|
5077
5449
|
});
|
|
5078
5450
|
}
|
|
5451
|
+
// ── Anti-Bot ──────────────────────────────────────────────────
|
|
5452
|
+
/**
|
|
5453
|
+
* Detect whether the page is showing an anti-bot challenge
|
|
5454
|
+
* (Cloudflare, hCaptcha, reCAPTCHA, access-denied, rate-limit, etc.).
|
|
5455
|
+
*
|
|
5456
|
+
* Returns `null` if no challenge is detected.
|
|
5457
|
+
*
|
|
5458
|
+
* @example
|
|
5459
|
+
* ```ts
|
|
5460
|
+
* const challenge = await page.detectChallenge();
|
|
5461
|
+
* if (challenge) {
|
|
5462
|
+
* console.log(challenge.kind); // 'cloudflare-js'
|
|
5463
|
+
* console.log(challenge.message); // 'Cloudflare JS challenge'
|
|
5464
|
+
* }
|
|
5465
|
+
* ```
|
|
5466
|
+
*/
|
|
5467
|
+
async detectChallenge() {
|
|
5468
|
+
return detectChallengeViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
|
|
5469
|
+
}
|
|
5470
|
+
/**
|
|
5471
|
+
* Wait for an anti-bot challenge to resolve on its own.
|
|
5472
|
+
*
|
|
5473
|
+
* Cloudflare JS challenges typically auto-resolve in ~5 seconds.
|
|
5474
|
+
* CAPTCHA challenges will only resolve if solved in a visible browser window.
|
|
5475
|
+
*
|
|
5476
|
+
* @param opts.timeoutMs - Maximum wait time (default: `15000`)
|
|
5477
|
+
* @param opts.pollMs - Poll interval (default: `500`)
|
|
5478
|
+
* @returns Whether the challenge resolved, and the remaining challenge info if not
|
|
5479
|
+
*
|
|
5480
|
+
* @example
|
|
5481
|
+
* ```ts
|
|
5482
|
+
* await page.goto('https://example.com');
|
|
5483
|
+
* const challenge = await page.detectChallenge();
|
|
5484
|
+
* if (challenge?.kind === 'cloudflare-js') {
|
|
5485
|
+
* const { resolved } = await page.waitForChallenge({ timeoutMs: 20000 });
|
|
5486
|
+
* if (!resolved) throw new Error('Challenge did not resolve');
|
|
5487
|
+
* }
|
|
5488
|
+
* ```
|
|
5489
|
+
*/
|
|
5490
|
+
async waitForChallenge(opts) {
|
|
5491
|
+
return waitForChallengeViaPlaywright({
|
|
5492
|
+
cdpUrl: this.cdpUrl,
|
|
5493
|
+
targetId: this.targetId,
|
|
5494
|
+
timeoutMs: opts?.timeoutMs,
|
|
5495
|
+
pollMs: opts?.pollMs
|
|
5496
|
+
});
|
|
5497
|
+
}
|
|
5079
5498
|
};
|
|
5080
5499
|
var BrowserClaw = class _BrowserClaw {
|
|
5081
5500
|
cdpUrl;
|
|
@@ -5111,8 +5530,8 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
5111
5530
|
*/
|
|
5112
5531
|
static async launch(opts = {}) {
|
|
5113
5532
|
const chrome = await launchChrome(opts);
|
|
5114
|
-
const cdpUrl = `http://127.0.0.1:${chrome.cdpPort}`;
|
|
5115
|
-
const ssrfPolicy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
5533
|
+
const cdpUrl = `http://127.0.0.1:${String(chrome.cdpPort)}`;
|
|
5534
|
+
const ssrfPolicy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
5116
5535
|
return new _BrowserClaw(cdpUrl, chrome, ssrfPolicy);
|
|
5117
5536
|
}
|
|
5118
5537
|
/**
|
|
@@ -5134,7 +5553,7 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
5134
5553
|
throw new Error(`Cannot connect to Chrome at ${cdpUrl}. Is Chrome running with --remote-debugging-port?`);
|
|
5135
5554
|
}
|
|
5136
5555
|
await connectBrowser(cdpUrl, opts?.authToken);
|
|
5137
|
-
const ssrfPolicy = opts?.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts?.ssrfPolicy;
|
|
5556
|
+
const ssrfPolicy = opts?.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts?.ssrfPolicy;
|
|
5138
5557
|
return new _BrowserClaw(cdpUrl, null, ssrfPolicy);
|
|
5139
5558
|
}
|
|
5140
5559
|
/**
|
|
@@ -5160,10 +5579,10 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
5160
5579
|
*/
|
|
5161
5580
|
async currentPage() {
|
|
5162
5581
|
const { browser } = await connectBrowser(this.cdpUrl);
|
|
5163
|
-
const pages =
|
|
5582
|
+
const pages = getAllPages(browser);
|
|
5164
5583
|
if (!pages.length) throw new Error("No pages available. Use browser.open(url) to create a tab.");
|
|
5165
5584
|
const tid = await pageTargetId(pages[0]).catch(() => null);
|
|
5166
|
-
if (
|
|
5585
|
+
if (tid === null || tid === "") throw new Error("Failed to get targetId for the current page.");
|
|
5167
5586
|
return new CrawlPage(this.cdpUrl, tid, this.ssrfPolicy);
|
|
5168
5587
|
}
|
|
5169
5588
|
/**
|
|
@@ -5225,12 +5644,14 @@ exports.BrowserClaw = BrowserClaw;
|
|
|
5225
5644
|
exports.BrowserTabNotFoundError = BrowserTabNotFoundError;
|
|
5226
5645
|
exports.CrawlPage = CrawlPage;
|
|
5227
5646
|
exports.InvalidBrowserNavigationUrlError = InvalidBrowserNavigationUrlError;
|
|
5647
|
+
exports.STEALTH_SCRIPT = STEALTH_SCRIPT;
|
|
5228
5648
|
exports.assertBrowserNavigationAllowed = assertBrowserNavigationAllowed;
|
|
5229
5649
|
exports.assertBrowserNavigationRedirectChainAllowed = assertBrowserNavigationRedirectChainAllowed;
|
|
5230
5650
|
exports.assertBrowserNavigationResultAllowed = assertBrowserNavigationResultAllowed;
|
|
5231
5651
|
exports.assertSafeUploadPaths = assertSafeUploadPaths;
|
|
5232
5652
|
exports.batchViaPlaywright = batchViaPlaywright;
|
|
5233
5653
|
exports.createPinnedLookup = createPinnedLookup;
|
|
5654
|
+
exports.detectChallengeViaPlaywright = detectChallengeViaPlaywright;
|
|
5234
5655
|
exports.ensureContextState = ensureContextState;
|
|
5235
5656
|
exports.executeSingleAction = executeSingleAction;
|
|
5236
5657
|
exports.forceDisconnectPlaywrightForTarget = forceDisconnectPlaywrightForTarget;
|
|
@@ -5249,6 +5670,7 @@ exports.resolvePageByTargetIdOrThrow = resolvePageByTargetIdOrThrow;
|
|
|
5249
5670
|
exports.resolvePinnedHostnameWithPolicy = resolvePinnedHostnameWithPolicy;
|
|
5250
5671
|
exports.resolveStrictExistingUploadPaths = resolveStrictExistingUploadPaths;
|
|
5251
5672
|
exports.sanitizeUntrustedFileName = sanitizeUntrustedFileName;
|
|
5673
|
+
exports.waitForChallengeViaPlaywright = waitForChallengeViaPlaywright;
|
|
5252
5674
|
exports.withBrowserNavigationPolicy = withBrowserNavigationPolicy;
|
|
5253
5675
|
exports.withPageScopedCdpClient = withPageScopedCdpClient;
|
|
5254
5676
|
exports.withPlaywrightPageCdpSession = withPlaywrightPageCdpSession;
|