iframer-toolkit 0.10.6 → 0.10.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +642 -71
- package/dist/local-server.cjs +825 -296
- package/dist/mcp-server.cjs +33 -5
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
- package/skills/iframer.md +61 -7
package/dist/cli.js
CHANGED
|
@@ -902,10 +902,54 @@ function findChromeExecutable() {
|
|
|
902
902
|
if (import_fs3.default.existsSync("/usr/bin/google-chrome-stable")) return "/usr/bin/google-chrome-stable";
|
|
903
903
|
return void 0;
|
|
904
904
|
}
|
|
905
|
+
function armHeadedIdleTimer() {
|
|
906
|
+
if (headedIdleTimer) clearTimeout(headedIdleTimer);
|
|
907
|
+
headedIdleTimer = setTimeout(() => {
|
|
908
|
+
if (!headedBrowser) return;
|
|
909
|
+
const pages = headedBrowser.contexts().reduce((n, c) => n + c.pages().length, 0);
|
|
910
|
+
if (pages > 0) {
|
|
911
|
+
armHeadedIdleTimer();
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
log2.info("idle timeout for the pivot headed browser, closing");
|
|
915
|
+
void closeHeadedBrowser();
|
|
916
|
+
}, HEADED_IDLE_MS);
|
|
917
|
+
}
|
|
905
918
|
async function getHeadedBrowser() {
|
|
906
|
-
if (!headedBrowser?.isConnected())
|
|
919
|
+
if (!headedBrowser?.isConnected()) {
|
|
920
|
+
const marker = `--iframer-key=pivot-headed-${process.pid}-${Date.now()}`;
|
|
921
|
+
headedBrowser = await import_patchright.chromium.launch({ headless: false, args: [...STEALTH_ARGS, marker] });
|
|
922
|
+
headedPid = findChromePidByMarker(marker);
|
|
923
|
+
if (headedPid) {
|
|
924
|
+
registerBrowser({
|
|
925
|
+
key: "pivot::headed",
|
|
926
|
+
chromePid: headedPid,
|
|
927
|
+
ownerPid: process.pid,
|
|
928
|
+
marker,
|
|
929
|
+
launchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
930
|
+
});
|
|
931
|
+
} else {
|
|
932
|
+
log2.warn("could not resolve Chrome PID for the pivot headed browser \u2014 force-kill unavailable");
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
armHeadedIdleTimer();
|
|
907
936
|
return headedBrowser;
|
|
908
937
|
}
|
|
938
|
+
async function closeHeadedBrowser() {
|
|
939
|
+
if (headedIdleTimer) {
|
|
940
|
+
clearTimeout(headedIdleTimer);
|
|
941
|
+
headedIdleTimer = null;
|
|
942
|
+
}
|
|
943
|
+
if (headedBrowser) {
|
|
944
|
+
await headedBrowser.close().catch(() => {
|
|
945
|
+
});
|
|
946
|
+
headedBrowser = null;
|
|
947
|
+
}
|
|
948
|
+
if (headedPid !== null) {
|
|
949
|
+
unregisterBrowser(headedPid);
|
|
950
|
+
headedPid = null;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
909
953
|
async function getBrowser(_name = "chromium") {
|
|
910
954
|
if (cachedBrowser) {
|
|
911
955
|
if (cachedBrowser.isConnected()) return cachedBrowser;
|
|
@@ -923,11 +967,7 @@ async function getBrowser(_name = "chromium") {
|
|
|
923
967
|
return cachedBrowser;
|
|
924
968
|
}
|
|
925
969
|
async function closeBrowser() {
|
|
926
|
-
|
|
927
|
-
await headedBrowser.close().catch(() => {
|
|
928
|
-
});
|
|
929
|
-
headedBrowser = null;
|
|
930
|
-
}
|
|
970
|
+
await closeHeadedBrowser();
|
|
931
971
|
if (!cachedBrowser) return;
|
|
932
972
|
try {
|
|
933
973
|
await cachedBrowser.close();
|
|
@@ -964,7 +1004,7 @@ async function launchHeadful(displayNum) {
|
|
|
964
1004
|
log2.debug(`headful: ${executablePath || "patchright chromium"}, extensions: ${hasExtensions}`);
|
|
965
1005
|
return import_patchright.chromium.launch(launchOpts);
|
|
966
1006
|
}
|
|
967
|
-
var import_fs3, import_patchright, log2, UBLOCK_PATH, cachedBrowser, headedBrowser;
|
|
1007
|
+
var import_fs3, import_patchright, log2, UBLOCK_PATH, cachedBrowser, headedBrowser, headedPid, headedIdleTimer, HEADED_IDLE_MS;
|
|
968
1008
|
var init_launcher = __esm({
|
|
969
1009
|
"src/lib/browser/launcher.ts"() {
|
|
970
1010
|
"use strict";
|
|
@@ -972,10 +1012,14 @@ var init_launcher = __esm({
|
|
|
972
1012
|
import_patchright = require("patchright");
|
|
973
1013
|
init_stealth();
|
|
974
1014
|
init_logger();
|
|
1015
|
+
init_registry();
|
|
975
1016
|
log2 = createLogger("launcher");
|
|
976
1017
|
UBLOCK_PATH = "/extensions/uBlock0.chromium";
|
|
977
1018
|
cachedBrowser = null;
|
|
978
1019
|
headedBrowser = null;
|
|
1020
|
+
headedPid = null;
|
|
1021
|
+
headedIdleTimer = null;
|
|
1022
|
+
HEADED_IDLE_MS = 5 * 60 * 1e3;
|
|
979
1023
|
}
|
|
980
1024
|
});
|
|
981
1025
|
|
|
@@ -1033,7 +1077,13 @@ var init_constants = __esm({
|
|
|
1033
1077
|
/** Scroll step delay */
|
|
1034
1078
|
SCROLL_DELAY: 150,
|
|
1035
1079
|
/** Stale state check interval */
|
|
1036
|
-
STALE_CHECK_INTERVAL: 2e3
|
|
1080
|
+
STALE_CHECK_INTERVAL: 2e3,
|
|
1081
|
+
/** captureApi: grace period after the last step for a submit's XHR to fire */
|
|
1082
|
+
CAPTURE_SETTLE: 300,
|
|
1083
|
+
/** captureApi: max wait for in-flight requests to answer before stopping */
|
|
1084
|
+
CAPTURE_PENDING_MAX: 3e3,
|
|
1085
|
+
/** Post-failure probe ("does the selector exist right now?") budget */
|
|
1086
|
+
FAILURE_PROBE: 3e3
|
|
1037
1087
|
};
|
|
1038
1088
|
CAPTCHA_GRID = {
|
|
1039
1089
|
/** Pixels from bframe top to grid start (reCAPTCHA) */
|
|
@@ -1071,6 +1121,10 @@ var init_constants = __esm({
|
|
|
1071
1121
|
DEFAULT_STALE: 2e4,
|
|
1072
1122
|
NAVIGATION: 6e4,
|
|
1073
1123
|
SELECTOR_WAIT: 1e4,
|
|
1124
|
+
// Actionability wait for click. Under the 20s stale timer on purpose, so
|
|
1125
|
+
// Playwright's own reason (not visible, covered by another element…) reaches
|
|
1126
|
+
// the agent instead of a generic "no state change".
|
|
1127
|
+
CLICK: 8e3,
|
|
1074
1128
|
TOTP_INPUT: 5e3,
|
|
1075
1129
|
API_REQUEST: 18e4,
|
|
1076
1130
|
HEALTH_CHECK: 3e3,
|
|
@@ -1827,7 +1881,7 @@ var init_cloak_browser = __esm({
|
|
|
1827
1881
|
function keyOf(mode, instanceId) {
|
|
1828
1882
|
return `${mode}::${instanceId}`;
|
|
1829
1883
|
}
|
|
1830
|
-
var import_patchright2, import_crypto2, log7, DEFAULT_IDLE_TIMEOUT, CLOSE_GRACE_MS, DEFAULT_INSTANCE, sleep3, BrowserDaemon;
|
|
1884
|
+
var import_patchright2, import_crypto2, log7, DEFAULT_IDLE_TIMEOUT, CLOSE_GRACE_MS, BUSY_MAX_MS, BLANK_IDLE_MS, DEFAULT_INSTANCE, sleep3, BrowserDaemon;
|
|
1831
1885
|
var init_daemon = __esm({
|
|
1832
1886
|
"src/lib/browser/daemon.ts"() {
|
|
1833
1887
|
"use strict";
|
|
@@ -1840,6 +1894,8 @@ var init_daemon = __esm({
|
|
|
1840
1894
|
log7 = createLogger("daemon");
|
|
1841
1895
|
DEFAULT_IDLE_TIMEOUT = 5 * 60 * 1e3;
|
|
1842
1896
|
CLOSE_GRACE_MS = 5e3;
|
|
1897
|
+
BUSY_MAX_MS = 30 * 60 * 1e3;
|
|
1898
|
+
BLANK_IDLE_MS = 2 * 60 * 1e3;
|
|
1843
1899
|
DEFAULT_INSTANCE = "default";
|
|
1844
1900
|
sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1845
1901
|
BrowserDaemon = class {
|
|
@@ -1928,7 +1984,8 @@ var init_daemon = __esm({
|
|
|
1928
1984
|
createdAt: /* @__PURE__ */ new Date(),
|
|
1929
1985
|
chromePid,
|
|
1930
1986
|
marker,
|
|
1931
|
-
active: 0
|
|
1987
|
+
active: 0,
|
|
1988
|
+
busySince: null
|
|
1932
1989
|
};
|
|
1933
1990
|
this.instances.set(key, instance);
|
|
1934
1991
|
this.resetIdleTimer(key);
|
|
@@ -1940,14 +1997,19 @@ var init_daemon = __esm({
|
|
|
1940
1997
|
* Always pair with release() in a finally block. */
|
|
1941
1998
|
acquire(mode, instanceId = DEFAULT_INSTANCE) {
|
|
1942
1999
|
const instance = this.instances.get(keyOf(mode, instanceId));
|
|
1943
|
-
if (instance)
|
|
2000
|
+
if (!instance) return;
|
|
2001
|
+
if (instance.active === 0) instance.busySince = Date.now();
|
|
2002
|
+
instance.active++;
|
|
1944
2003
|
}
|
|
1945
2004
|
release(mode, instanceId = DEFAULT_INSTANCE) {
|
|
1946
2005
|
const key = keyOf(mode, instanceId);
|
|
1947
2006
|
const instance = this.instances.get(key);
|
|
1948
2007
|
if (!instance) return;
|
|
1949
2008
|
instance.active = Math.max(0, instance.active - 1);
|
|
1950
|
-
if (instance.active === 0)
|
|
2009
|
+
if (instance.active === 0) {
|
|
2010
|
+
instance.busySince = null;
|
|
2011
|
+
this.resetIdleTimer(key);
|
|
2012
|
+
}
|
|
1951
2013
|
}
|
|
1952
2014
|
isRunning(mode, instanceId = DEFAULT_INSTANCE) {
|
|
1953
2015
|
const instance = this.instances.get(keyOf(mode, instanceId));
|
|
@@ -1958,6 +2020,35 @@ var init_daemon = __esm({
|
|
|
1958
2020
|
return false;
|
|
1959
2021
|
}
|
|
1960
2022
|
}
|
|
2023
|
+
/** Close instances that never left about:blank and have been idle a while.
|
|
2024
|
+
* Those are accidents — a pipeline that failed before its first navigate, or
|
|
2025
|
+
* a mode picked for a call that never needed a browser — and in headful mode
|
|
2026
|
+
* each one is an empty window sitting on the user's desktop. Called on the
|
|
2027
|
+
* server's reap tick, so it also catches instances whose idle timer was lost
|
|
2028
|
+
* (e.g. the daemon was restarted under them). */
|
|
2029
|
+
sweepBlankInstances() {
|
|
2030
|
+
const stopped = [];
|
|
2031
|
+
const now = Date.now();
|
|
2032
|
+
for (const [key, inst] of this.instances.entries()) {
|
|
2033
|
+
if (inst.active > 0) continue;
|
|
2034
|
+
if (now - inst.createdAt.getTime() < BLANK_IDLE_MS) continue;
|
|
2035
|
+
let blank = false;
|
|
2036
|
+
try {
|
|
2037
|
+
const pages = inst.context.pages();
|
|
2038
|
+
blank = pages.length === 0 || pages.every((p) => {
|
|
2039
|
+
const u = p.url();
|
|
2040
|
+
return u === "about:blank" || u === "" || u === "chrome://newtab/";
|
|
2041
|
+
});
|
|
2042
|
+
} catch {
|
|
2043
|
+
continue;
|
|
2044
|
+
}
|
|
2045
|
+
if (!blank) continue;
|
|
2046
|
+
log7.info(`blank-orphan sweep: ${key} never left about:blank, stopping`);
|
|
2047
|
+
stopped.push(key);
|
|
2048
|
+
this.stopKey(key).catch((err) => log7.warn(`blank sweep stop failed for ${key}: ${err}`));
|
|
2049
|
+
}
|
|
2050
|
+
return stopped;
|
|
2051
|
+
}
|
|
1961
2052
|
/** Distinct modes that currently have at least one live instance. */
|
|
1962
2053
|
runningModes() {
|
|
1963
2054
|
return [...new Set(this.liveInstances().map((i) => i.mode))];
|
|
@@ -2076,8 +2167,14 @@ var init_daemon = __esm({
|
|
|
2076
2167
|
setTimeout(() => {
|
|
2077
2168
|
const instance = this.instances.get(key);
|
|
2078
2169
|
if (instance && instance.active > 0) {
|
|
2079
|
-
|
|
2080
|
-
|
|
2170
|
+
const busyMs = instance.busySince ? Date.now() - instance.busySince : 0;
|
|
2171
|
+
if (busyMs < BUSY_MAX_MS) {
|
|
2172
|
+
this.resetIdleTimer(key);
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
log7.warn(`${key} reported busy for ${Math.round(busyMs / 6e4)}m \u2014 treating active=${instance.active} as leaked, stopping`);
|
|
2176
|
+
instance.active = 0;
|
|
2177
|
+
instance.busySince = null;
|
|
2081
2178
|
}
|
|
2082
2179
|
log7.info(`Idle timeout for ${key}, stopping...`);
|
|
2083
2180
|
this.stopKey(key).catch((err) => log7.warn(`idle stop failed for ${key}: ${err}`));
|
|
@@ -2479,6 +2576,24 @@ async function humanType(page, selector, text, opts = {}) {
|
|
|
2479
2576
|
await sleep4(randRange(TIMING.POST_CLICK));
|
|
2480
2577
|
}
|
|
2481
2578
|
await assertFocused(page, selector, !opts.skipClick);
|
|
2579
|
+
if (opts.clear) {
|
|
2580
|
+
const hadText = await page.evaluate(() => {
|
|
2581
|
+
const el = document.activeElement;
|
|
2582
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
2583
|
+
el.select();
|
|
2584
|
+
return el.value.length > 0;
|
|
2585
|
+
}
|
|
2586
|
+
if (el?.isContentEditable) {
|
|
2587
|
+
window.getSelection()?.selectAllChildren(el);
|
|
2588
|
+
return (el.textContent || "").length > 0;
|
|
2589
|
+
}
|
|
2590
|
+
return false;
|
|
2591
|
+
});
|
|
2592
|
+
if (hadText) {
|
|
2593
|
+
await page.keyboard.press("Backspace");
|
|
2594
|
+
await sleep4(randRange(TIMING.CHAR_DELAY));
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2482
2597
|
const factor = SPEED_FACTOR[opts.speed || "normal"] ?? 1;
|
|
2483
2598
|
for (const char of text) {
|
|
2484
2599
|
await page.keyboard.type(char);
|
|
@@ -2640,7 +2755,41 @@ var init_humanize = __esm({
|
|
|
2640
2755
|
}
|
|
2641
2756
|
});
|
|
2642
2757
|
|
|
2758
|
+
// src/lib/budget.ts
|
|
2759
|
+
function withBudget(p, ms, label = "page call") {
|
|
2760
|
+
p.catch(() => void 0);
|
|
2761
|
+
let timer;
|
|
2762
|
+
return Promise.race([
|
|
2763
|
+
p,
|
|
2764
|
+
new Promise((_, reject) => {
|
|
2765
|
+
timer = setTimeout(() => reject(new BudgetExceededError(label, ms)), ms);
|
|
2766
|
+
timer.unref?.();
|
|
2767
|
+
})
|
|
2768
|
+
]).finally(() => {
|
|
2769
|
+
if (timer) clearTimeout(timer);
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
var BudgetExceededError;
|
|
2773
|
+
var init_budget = __esm({
|
|
2774
|
+
"src/lib/budget.ts"() {
|
|
2775
|
+
"use strict";
|
|
2776
|
+
BudgetExceededError = class extends Error {
|
|
2777
|
+
constructor(label, ms) {
|
|
2778
|
+
super(`${label} did not answer within ${ms}ms`);
|
|
2779
|
+
this.name = "BudgetExceededError";
|
|
2780
|
+
}
|
|
2781
|
+
};
|
|
2782
|
+
}
|
|
2783
|
+
});
|
|
2784
|
+
|
|
2643
2785
|
// src/lib/actions/resolve-selector.ts
|
|
2786
|
+
function assertCssSelector(selector) {
|
|
2787
|
+
if (PLAYWRIGHT_ONLY.test(selector)) {
|
|
2788
|
+
throw new Error(
|
|
2789
|
+
`Unsupported selector "${selector}": Playwright text/role selectors (text=, :has-text(), :text-is(), >>) aren't supported. Locate the element by its text with a \`find\` step (e.g. { type: "find", text: "Texas" } or { role: "option", name: "Texas" }) and act on the @e ref it returns, in the same execute call.`
|
|
2790
|
+
);
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2644
2793
|
function resolveSelector(selector, ctx) {
|
|
2645
2794
|
if (selector.startsWith("@a:")) {
|
|
2646
2795
|
const name = selector.slice(3);
|
|
@@ -2661,23 +2810,160 @@ function resolveSelector(selector, ctx) {
|
|
|
2661
2810
|
}
|
|
2662
2811
|
return ref.selector;
|
|
2663
2812
|
}
|
|
2813
|
+
assertCssSelector(selector);
|
|
2664
2814
|
return selector;
|
|
2665
2815
|
}
|
|
2666
2816
|
function anchorNameOf(selector) {
|
|
2667
2817
|
return typeof selector === "string" && selector.startsWith("@a:") ? selector.slice(3) : null;
|
|
2668
2818
|
}
|
|
2819
|
+
var PLAYWRIGHT_ONLY;
|
|
2669
2820
|
var init_resolve_selector = __esm({
|
|
2670
2821
|
"src/lib/actions/resolve-selector.ts"() {
|
|
2671
2822
|
"use strict";
|
|
2823
|
+
PLAYWRIGHT_ONLY = /^\s*(text|xpath|role|id|data-testid|internal:[\w-]+)\s*=|:(has-text|text|text-is|text-matches|visible|nth-match|right-of|left-of|above|below|near)\b|\s>>\s/;
|
|
2824
|
+
}
|
|
2825
|
+
});
|
|
2826
|
+
|
|
2827
|
+
// src/lib/reachability.ts
|
|
2828
|
+
function isHoneypotReason(reason) {
|
|
2829
|
+
return !!reason && HONEYPOT_REASONS.has(reason);
|
|
2830
|
+
}
|
|
2831
|
+
function looksLikeBaitField(attrs) {
|
|
2832
|
+
const name = attrs.name || "";
|
|
2833
|
+
const id = attrs.id || "";
|
|
2834
|
+
if (HONEYPOT_NAME_RE.test(name) || HONEYPOT_NAME_RE.test(id)) return true;
|
|
2835
|
+
const textual = !attrs.type || /^(text|email|url|tel|search|number)$/i.test(attrs.type);
|
|
2836
|
+
if (textual && attrs.tabindex === "-1") return true;
|
|
2837
|
+
return false;
|
|
2838
|
+
}
|
|
2839
|
+
function honeypotRefusalMessage(selector, reason, name) {
|
|
2840
|
+
const what = name ? `"${name}" (${selector})` : selector;
|
|
2841
|
+
return `fill: refusing to fill ${what} \u2014 no human can reach this field (${reason}). It is almost certainly a honeypot: filling it marks the submission as a bot and the form fails with a generic error. Skip it. If you are certain it is a real field, retry with force: true.`;
|
|
2842
|
+
}
|
|
2843
|
+
async function probeField(page, selector) {
|
|
2844
|
+
const r = await page.evaluate((sel) => {
|
|
2845
|
+
const el = document.querySelector(sel);
|
|
2846
|
+
if (!el) return { found: false };
|
|
2847
|
+
const attrs = {
|
|
2848
|
+
name: el.getAttribute("name"),
|
|
2849
|
+
id: el.id || null,
|
|
2850
|
+
tabindex: el.getAttribute("tabindex"),
|
|
2851
|
+
type: el.getAttribute("type")
|
|
2852
|
+
};
|
|
2853
|
+
const tag = el.tagName.toLowerCase();
|
|
2854
|
+
{
|
|
2855
|
+
const r0 = el.getBoundingClientRect();
|
|
2856
|
+
const cx0 = r0.left + r0.width / 2, cy0 = r0.top + r0.height / 2;
|
|
2857
|
+
const dx = cx0 < 0 || cx0 >= window.innerWidth ? cx0 - window.innerWidth / 2 : 0;
|
|
2858
|
+
const dy = cy0 < 0 || cy0 >= window.innerHeight ? cy0 - window.innerHeight / 2 : 0;
|
|
2859
|
+
if (dx || dy) try {
|
|
2860
|
+
window.scrollBy(dx, dy);
|
|
2861
|
+
} catch {
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
const rect = el.getBoundingClientRect();
|
|
2865
|
+
let reason = null;
|
|
2866
|
+
if (rect.width === 0 || rect.height === 0) reason = "zero-size";
|
|
2867
|
+
const style = getComputedStyle(el);
|
|
2868
|
+
if (!reason && (style.display === "none" || style.visibility === "hidden")) reason = "hidden";
|
|
2869
|
+
if (!reason) {
|
|
2870
|
+
let opacity = 1;
|
|
2871
|
+
for (let n = el; n && n !== document.documentElement; n = n.parentElement) {
|
|
2872
|
+
const o = parseFloat(getComputedStyle(n).opacity);
|
|
2873
|
+
const animating = o < 1 && typeof n.getAnimations === "function" && n.getAnimations().length > 0;
|
|
2874
|
+
if (!isNaN(o) && !animating) opacity *= o;
|
|
2875
|
+
}
|
|
2876
|
+
if (opacity < 0.05) reason = "transparent";
|
|
2877
|
+
}
|
|
2878
|
+
if (!reason) {
|
|
2879
|
+
const docW = Math.max(document.documentElement.scrollWidth, window.innerWidth);
|
|
2880
|
+
const docH = Math.max(document.documentElement.scrollHeight, window.innerHeight);
|
|
2881
|
+
const left = rect.left + window.scrollX, right = rect.right + window.scrollX;
|
|
2882
|
+
const top = rect.top + window.scrollY, bottom = rect.bottom + window.scrollY;
|
|
2883
|
+
if (right <= 0 || left >= docW || bottom <= 0 || top >= docH) reason = "offscreen";
|
|
2884
|
+
}
|
|
2885
|
+
if (!reason) {
|
|
2886
|
+
const cx = rect.left + rect.width / 2, cy = rect.top + rect.height / 2;
|
|
2887
|
+
const inViewport = cx >= 0 && cy >= 0 && cx < window.innerWidth && cy < window.innerHeight;
|
|
2888
|
+
let clipped = false;
|
|
2889
|
+
for (let a = el.parentElement; a && a !== document.body; a = a.parentElement) {
|
|
2890
|
+
const s = getComputedStyle(a);
|
|
2891
|
+
const clipsX = /hidden|clip/.test(s.overflowX), clipsY = /hidden|clip/.test(s.overflowY);
|
|
2892
|
+
if (!clipsX && !clipsY) continue;
|
|
2893
|
+
const ar = a.getBoundingClientRect();
|
|
2894
|
+
const outside = clipsX && (cx < ar.left || cx > ar.right) || clipsY && (cy < ar.top || cy > ar.bottom);
|
|
2895
|
+
const tiny = clipsY && ar.height <= 2 || clipsX && ar.width <= 2;
|
|
2896
|
+
if (outside || tiny) {
|
|
2897
|
+
clipped = true;
|
|
2898
|
+
break;
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
if (inViewport) {
|
|
2902
|
+
const hit = document.elementFromPoint(cx, cy);
|
|
2903
|
+
if (!(hit && (hit === el || el.contains(hit)))) reason = clipped ? "clipped" : "covered";
|
|
2904
|
+
} else if (clipped) {
|
|
2905
|
+
reason = "clipped";
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
return { found: true, reason, attrs, tag };
|
|
2909
|
+
}, selector);
|
|
2910
|
+
if (!r.found) return { found: false, hiddenReason: null, suspiciousName: false };
|
|
2911
|
+
return {
|
|
2912
|
+
found: true,
|
|
2913
|
+
hiddenReason: r.reason ?? null,
|
|
2914
|
+
suspiciousName: looksLikeBaitField(r.attrs),
|
|
2915
|
+
tag: r.tag,
|
|
2916
|
+
name: r.attrs.name ?? void 0
|
|
2917
|
+
};
|
|
2918
|
+
}
|
|
2919
|
+
var HONEYPOT_REASONS, HONEYPOT_NAME_RE;
|
|
2920
|
+
var init_reachability = __esm({
|
|
2921
|
+
"src/lib/reachability.ts"() {
|
|
2922
|
+
"use strict";
|
|
2923
|
+
HONEYPOT_REASONS = /* @__PURE__ */ new Set(["zero-size", "hidden", "transparent", "offscreen", "clipped"]);
|
|
2924
|
+
HONEYPOT_NAME_RE = /^(website|url|homepage|fax|honeypot|honey|hp|hp_[\w-]*|bot|bot_[\w-]*|trap|_gotcha|leave_?blank|do_?not_?fill)$/i;
|
|
2672
2925
|
}
|
|
2673
2926
|
});
|
|
2674
2927
|
|
|
2675
2928
|
// src/lib/actions/handlers/navigation.ts
|
|
2676
|
-
async function navigate(page, step, ctx) {
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2929
|
+
async function navigate(page, step, ctx, monitor) {
|
|
2930
|
+
const waitUntil = step.waitUntil || "domcontentloaded";
|
|
2931
|
+
const urlBefore = page.url();
|
|
2932
|
+
const onDialog = (d) => {
|
|
2933
|
+
(d.type() === "beforeunload" ? d.accept() : d.dismiss()).catch(() => void 0);
|
|
2934
|
+
};
|
|
2935
|
+
page.on("dialog", onDialog);
|
|
2936
|
+
try {
|
|
2937
|
+
const nav = page.goto(step.url, { waitUntil, timeout: TIMEOUTS.NAVIGATION });
|
|
2938
|
+
nav.catch(() => void 0);
|
|
2939
|
+
let suspectTimer;
|
|
2940
|
+
const outcome = await Promise.race([
|
|
2941
|
+
nav.then(() => "done"),
|
|
2942
|
+
new Promise((r) => {
|
|
2943
|
+
suspectTimer = setTimeout(() => r("slow"), NAV_COMMIT_SUSPECT_MS);
|
|
2944
|
+
})
|
|
2945
|
+
]).finally(() => clearTimeout(suspectTimer));
|
|
2946
|
+
if (outcome === "slow") {
|
|
2947
|
+
if (page.url() !== urlBefore) {
|
|
2948
|
+
await nav;
|
|
2949
|
+
} else {
|
|
2950
|
+
log9.warn(`navigate: no commit after ${NAV_COMMIT_SUSPECT_MS}ms, falling back to location.assign`);
|
|
2951
|
+
monitor?.reportActivity();
|
|
2952
|
+
await withBudget(page.evaluate((u) => {
|
|
2953
|
+
location.assign(u);
|
|
2954
|
+
}, step.url), 3e3, "location.assign").catch(() => void 0);
|
|
2955
|
+
try {
|
|
2956
|
+
await page.waitForURL((u) => u.toString() !== urlBefore, { timeout: 15e3, waitUntil });
|
|
2957
|
+
} catch {
|
|
2958
|
+
throw new Error(
|
|
2959
|
+
`navigate: the page never left ${urlBefore} for ${step.url} (tried a normal navigation, then location.assign). The site is blocking the route change. Use a click on the site's own link to get there.`
|
|
2960
|
+
);
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
} finally {
|
|
2965
|
+
page.off("dialog", onDialog);
|
|
2966
|
+
}
|
|
2681
2967
|
const stealthScript = contextStealthScripts.get(page.context()) ?? STEALTH_SCRIPT;
|
|
2682
2968
|
try {
|
|
2683
2969
|
await page.evaluate(stealthScript);
|
|
@@ -2693,14 +2979,37 @@ async function navigate(page, step, ctx) {
|
|
|
2693
2979
|
}
|
|
2694
2980
|
}
|
|
2695
2981
|
async function click(page, step, ctx) {
|
|
2696
|
-
await page.click(resolveSelector(step.selector, ctx));
|
|
2982
|
+
await page.click(resolveSelector(step.selector, ctx), { timeout: TIMEOUTS.CLICK });
|
|
2983
|
+
}
|
|
2984
|
+
async function isCombobox(page, selector) {
|
|
2985
|
+
return withBudget(
|
|
2986
|
+
page.evaluate((sel) => {
|
|
2987
|
+
const el = document.querySelector(sel);
|
|
2988
|
+
if (!el) return false;
|
|
2989
|
+
const role = el.getAttribute("role");
|
|
2990
|
+
return role === "combobox" || el.hasAttribute("aria-autocomplete") || el.getAttribute("aria-haspopup") === "listbox" || el.hasAttribute("list") || !!el.closest('[role="combobox"]');
|
|
2991
|
+
}, selector),
|
|
2992
|
+
3e3
|
|
2993
|
+
).catch(() => false);
|
|
2697
2994
|
}
|
|
2698
2995
|
async function fill(page, step, ctx) {
|
|
2699
2996
|
const selector = resolveSelector(step.selector, ctx);
|
|
2700
2997
|
const value = step.value;
|
|
2998
|
+
if (!step.force) {
|
|
2999
|
+
let probe = null;
|
|
3000
|
+
try {
|
|
3001
|
+
probe = await probeField(page, selector);
|
|
3002
|
+
} catch {
|
|
3003
|
+
probe = null;
|
|
3004
|
+
}
|
|
3005
|
+
if (probe?.found && isHoneypotReason(probe.hiddenReason)) {
|
|
3006
|
+
throw new Error(honeypotRefusalMessage(step.selector, probe.hiddenReason, probe.name));
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
const combobox = await isCombobox(page, selector);
|
|
2701
3010
|
await page.fill(selector, value);
|
|
2702
3011
|
const stuck = await page.evaluate(
|
|
2703
|
-
({ sel, val }) => {
|
|
3012
|
+
({ sel, val, keepFocus }) => {
|
|
2704
3013
|
const el = document.querySelector(sel);
|
|
2705
3014
|
if (!el) return false;
|
|
2706
3015
|
const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
@@ -2716,13 +3025,15 @@ async function fill(page, step, ctx) {
|
|
|
2716
3025
|
}
|
|
2717
3026
|
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2718
3027
|
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
3028
|
+
if (!keepFocus) {
|
|
3029
|
+
try {
|
|
3030
|
+
el.blur();
|
|
3031
|
+
} catch {
|
|
3032
|
+
}
|
|
2722
3033
|
}
|
|
2723
3034
|
return el.value === val;
|
|
2724
3035
|
},
|
|
2725
|
-
{ sel: selector, val: value }
|
|
3036
|
+
{ sel: selector, val: value, keepFocus: combobox }
|
|
2726
3037
|
);
|
|
2727
3038
|
if (!stuck) {
|
|
2728
3039
|
const loc = page.locator(selector);
|
|
@@ -2730,10 +3041,12 @@ async function fill(page, step, ctx) {
|
|
|
2730
3041
|
await loc.click();
|
|
2731
3042
|
await loc.fill("");
|
|
2732
3043
|
await loc.pressSequentially(value, { delay: 15 });
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
3044
|
+
if (!combobox) {
|
|
3045
|
+
await page.evaluate((sel) => {
|
|
3046
|
+
const el = document.querySelector(sel);
|
|
3047
|
+
el?.blur?.();
|
|
3048
|
+
}, selector);
|
|
3049
|
+
}
|
|
2737
3050
|
} catch {
|
|
2738
3051
|
}
|
|
2739
3052
|
}
|
|
@@ -2757,7 +3070,7 @@ async function rightClick(page, step, ctx) {
|
|
|
2757
3070
|
}
|
|
2758
3071
|
}
|
|
2759
3072
|
async function humanTypeStep(page, step, ctx) {
|
|
2760
|
-
await humanType(page, resolveSelector(step.selector, ctx), step.value, { skipClick: step.skipClick, speed: step.speed });
|
|
3073
|
+
await humanType(page, resolveSelector(step.selector, ctx), step.value, { skipClick: step.skipClick, speed: step.speed, clear: step.clear });
|
|
2761
3074
|
}
|
|
2762
3075
|
async function selectOption(page, step, ctx) {
|
|
2763
3076
|
const selector = resolveSelector(step.selector, ctx);
|
|
@@ -2800,7 +3113,29 @@ async function scroll(page, step, ctx) {
|
|
|
2800
3113
|
{ dy: step.deltaY ?? 0, sel: selector }
|
|
2801
3114
|
);
|
|
2802
3115
|
}
|
|
2803
|
-
async function
|
|
3116
|
+
async function selectAllInFocused(page) {
|
|
3117
|
+
await page.evaluate(() => {
|
|
3118
|
+
const el = document.activeElement;
|
|
3119
|
+
if (el && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
|
|
3120
|
+
el.select();
|
|
3121
|
+
} else if (el && el.isContentEditable) {
|
|
3122
|
+
const sel = window.getSelection();
|
|
3123
|
+
sel?.selectAllChildren(el);
|
|
3124
|
+
} else {
|
|
3125
|
+
window.getSelection()?.selectAllChildren(document.body);
|
|
3126
|
+
}
|
|
3127
|
+
});
|
|
3128
|
+
}
|
|
3129
|
+
async function keyboard(page, step, ctx) {
|
|
3130
|
+
if ((step.meta || step.ctrl) && step.key.toLowerCase() === "a" && !step.shift && !step.alt) {
|
|
3131
|
+
await selectAllInFocused(page);
|
|
3132
|
+
return;
|
|
3133
|
+
}
|
|
3134
|
+
if ((step.meta || step.ctrl) && ctx.driver === "extension") {
|
|
3135
|
+
throw new Error(
|
|
3136
|
+
`keyboard: ${step.meta ? "Cmd" : "Ctrl"}+${step.key} can't be sent in extension mode (the relay drops Cmd/Ctrl, so it would arrive as a plain "${step.key}"). Nothing was pressed. Instead: select-all \u2192 keyboard {key:"a", meta:true} (handled in-page); replace a field's text \u2192 fill, or human-type with clear:true; paste \u2192 the paste step.`
|
|
3137
|
+
);
|
|
3138
|
+
}
|
|
2804
3139
|
const mods = [
|
|
2805
3140
|
step.meta ? "Meta" : null,
|
|
2806
3141
|
step.ctrl ? "Control" : null,
|
|
@@ -2871,7 +3206,7 @@ async function typeCode(page, step, ctx) {
|
|
|
2871
3206
|
}
|
|
2872
3207
|
return { typed: code.length };
|
|
2873
3208
|
}
|
|
2874
|
-
var log9;
|
|
3209
|
+
var log9, NAV_COMMIT_SUSPECT_MS;
|
|
2875
3210
|
var init_navigation = __esm({
|
|
2876
3211
|
"src/lib/actions/handlers/navigation.ts"() {
|
|
2877
3212
|
"use strict";
|
|
@@ -2881,8 +3216,11 @@ var init_navigation = __esm({
|
|
|
2881
3216
|
init_persistence();
|
|
2882
3217
|
init_logger();
|
|
2883
3218
|
init_constants();
|
|
3219
|
+
init_budget();
|
|
2884
3220
|
init_resolve_selector();
|
|
3221
|
+
init_reachability();
|
|
2885
3222
|
log9 = createLogger("actions");
|
|
3223
|
+
NAV_COMMIT_SUSPECT_MS = 12e3;
|
|
2886
3224
|
}
|
|
2887
3225
|
});
|
|
2888
3226
|
|
|
@@ -3004,6 +3342,56 @@ async function takeSnapshot(page, ctx, options) {
|
|
|
3004
3342
|
if (rect.bottom < 0 || rect.top > window.innerHeight + 200) return false;
|
|
3005
3343
|
return true;
|
|
3006
3344
|
}
|
|
3345
|
+
function isFillable(el) {
|
|
3346
|
+
if (el.tagName === "TEXTAREA" || el.tagName === "SELECT") return true;
|
|
3347
|
+
if (el.hasAttribute("contenteditable") && el.getAttribute("contenteditable") !== "false") return true;
|
|
3348
|
+
if (el.tagName !== "INPUT") return false;
|
|
3349
|
+
const t = (el.type || "text").toLowerCase();
|
|
3350
|
+
return !["button", "submit", "reset", "image", "checkbox", "radio", "file", "hidden", "range", "color"].includes(t);
|
|
3351
|
+
}
|
|
3352
|
+
function unreachableReason(el) {
|
|
3353
|
+
const rect = el.getBoundingClientRect();
|
|
3354
|
+
const style = getComputedStyle(el);
|
|
3355
|
+
let opacity = 1;
|
|
3356
|
+
for (let n = el; n && n !== document.documentElement; n = n.parentElement) {
|
|
3357
|
+
const o = parseFloat(n === el ? style.opacity : getComputedStyle(n).opacity);
|
|
3358
|
+
const animating = o < 1 && typeof n.getAnimations === "function" && n.getAnimations().length > 0;
|
|
3359
|
+
if (!isNaN(o) && !animating) opacity *= o;
|
|
3360
|
+
}
|
|
3361
|
+
if (opacity < 0.05) return "transparent";
|
|
3362
|
+
const docW = Math.max(document.documentElement.scrollWidth, window.innerWidth);
|
|
3363
|
+
const docH = Math.max(document.documentElement.scrollHeight, window.innerHeight);
|
|
3364
|
+
const left = rect.left + window.scrollX, right = rect.right + window.scrollX;
|
|
3365
|
+
const top = rect.top + window.scrollY, bottom = rect.bottom + window.scrollY;
|
|
3366
|
+
if (right <= 0 || left >= docW || bottom <= 0 || top >= docH) return "offscreen";
|
|
3367
|
+
const cx = rect.left + rect.width / 2, cy = rect.top + rect.height / 2;
|
|
3368
|
+
let clipped = false;
|
|
3369
|
+
for (let a = el.parentElement; a && a !== document.body; a = a.parentElement) {
|
|
3370
|
+
const s = getComputedStyle(a);
|
|
3371
|
+
const clipsX = /hidden|clip/.test(s.overflowX), clipsY = /hidden|clip/.test(s.overflowY);
|
|
3372
|
+
if (!clipsX && !clipsY) continue;
|
|
3373
|
+
const ar = a.getBoundingClientRect();
|
|
3374
|
+
const outside = clipsX && (cx < ar.left || cx > ar.right) || clipsY && (cy < ar.top || cy > ar.bottom);
|
|
3375
|
+
const tiny = clipsY && ar.height <= 2 || clipsX && ar.width <= 2;
|
|
3376
|
+
if (tiny) return "clipped";
|
|
3377
|
+
if (outside) clipped = true;
|
|
3378
|
+
}
|
|
3379
|
+
const inViewport = cx >= 0 && cy >= 0 && cx < window.innerWidth && cy < window.innerHeight;
|
|
3380
|
+
if (inViewport) {
|
|
3381
|
+
const hit = document.elementFromPoint(cx, cy);
|
|
3382
|
+
if (hit && (hit === el || el.contains(hit))) return null;
|
|
3383
|
+
return clipped ? "clipped" : null;
|
|
3384
|
+
}
|
|
3385
|
+
return null;
|
|
3386
|
+
}
|
|
3387
|
+
const baitName = /^(website|url|homepage|fax|honeypot|honey|hp|hp_[\w-]*|bot|bot_[\w-]*|trap|_gotcha|leave_?blank|do_?not_?fill)$/i;
|
|
3388
|
+
function looksLikeBait(el) {
|
|
3389
|
+
const name = el.getAttribute("name") || "", id = el.id || "";
|
|
3390
|
+
if (baitName.test(name) || baitName.test(id)) return true;
|
|
3391
|
+
const type = (el.getAttribute("type") || "text").toLowerCase();
|
|
3392
|
+
const textual = /^(text|email|url|tel|search|number)$/.test(type);
|
|
3393
|
+
return textual && el.getAttribute("tabindex") === "-1";
|
|
3394
|
+
}
|
|
3007
3395
|
function buildSelector(el) {
|
|
3008
3396
|
const path14 = [];
|
|
3009
3397
|
let current = el;
|
|
@@ -3046,6 +3434,11 @@ async function takeSnapshot(page, ctx, options) {
|
|
|
3046
3434
|
if (results.length >= maxElements2) break;
|
|
3047
3435
|
if (interactiveOnly2 && !isInteractive(el)) continue;
|
|
3048
3436
|
if (!isVisible(el)) continue;
|
|
3437
|
+
let suspect;
|
|
3438
|
+
if (isFillable(el)) {
|
|
3439
|
+
if (unreachableReason(el)) continue;
|
|
3440
|
+
if (looksLikeBait(el)) suspect = "honeypot?";
|
|
3441
|
+
}
|
|
3049
3442
|
const tag = el.tagName.toLowerCase();
|
|
3050
3443
|
const role = el.getAttribute("role") || "";
|
|
3051
3444
|
const type = el.type || "";
|
|
@@ -3058,7 +3451,8 @@ async function takeSnapshot(page, ctx, options) {
|
|
|
3058
3451
|
checked: el.checked || false,
|
|
3059
3452
|
disabled: el.disabled || false,
|
|
3060
3453
|
selector: buildSelector(el),
|
|
3061
|
-
isVisible: true
|
|
3454
|
+
isVisible: true,
|
|
3455
|
+
suspect
|
|
3062
3456
|
});
|
|
3063
3457
|
}
|
|
3064
3458
|
return results;
|
|
@@ -3079,6 +3473,7 @@ async function takeSnapshot(page, ctx, options) {
|
|
|
3079
3473
|
const state = [];
|
|
3080
3474
|
if (el.disabled) state.push("disabled");
|
|
3081
3475
|
if (el.checked) state.push("checked");
|
|
3476
|
+
if (el.suspect) state.push(el.suspect);
|
|
3082
3477
|
let description = "";
|
|
3083
3478
|
if (el.placeholder && el.name !== el.placeholder) description = `placeholder="${el.placeholder}"`;
|
|
3084
3479
|
if (el.type && !["text", "submit", "button", ""].includes(el.type)) {
|
|
@@ -4596,12 +4991,15 @@ var init_registry2 = __esm({
|
|
|
4596
4991
|
});
|
|
4597
4992
|
|
|
4598
4993
|
// src/lib/stale-monitor.ts
|
|
4599
|
-
var DEFAULT_STALE_TIMEOUT_MS, StaleStateMonitor, StaleStateError;
|
|
4994
|
+
var DEFAULT_STALE_TIMEOUT_MS, PROBE_BUDGET_MS, UNRESPONSIVE_GRACE_MS, StaleStateMonitor, StaleStateError;
|
|
4600
4995
|
var init_stale_monitor = __esm({
|
|
4601
4996
|
"src/lib/stale-monitor.ts"() {
|
|
4602
4997
|
"use strict";
|
|
4603
4998
|
init_constants();
|
|
4999
|
+
init_budget();
|
|
4604
5000
|
DEFAULT_STALE_TIMEOUT_MS = 2e4;
|
|
5001
|
+
PROBE_BUDGET_MS = 3e3;
|
|
5002
|
+
UNRESPONSIVE_GRACE_MS = 8e3;
|
|
4605
5003
|
StaleStateMonitor = class _StaleStateMonitor {
|
|
4606
5004
|
page;
|
|
4607
5005
|
timeoutMs;
|
|
@@ -4613,21 +5011,24 @@ var init_stale_monitor = __esm({
|
|
|
4613
5011
|
}
|
|
4614
5012
|
async snapshot() {
|
|
4615
5013
|
try {
|
|
4616
|
-
const
|
|
4617
|
-
|
|
5014
|
+
const url = this.page.url();
|
|
5015
|
+
let unresponsive = false;
|
|
5016
|
+
const state = await withBudget(
|
|
4618
5017
|
this.page.evaluate(() => ({
|
|
4619
5018
|
documentReadyState: document.readyState,
|
|
4620
5019
|
bodyTextLength: document.body?.innerText?.length || 0,
|
|
4621
5020
|
elementCount: document.querySelectorAll("*").length
|
|
4622
|
-
}))
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
5021
|
+
})),
|
|
5022
|
+
PROBE_BUDGET_MS,
|
|
5023
|
+
"state probe"
|
|
5024
|
+
).catch((e) => {
|
|
5025
|
+
unresponsive = e instanceof BudgetExceededError;
|
|
5026
|
+
return { documentReadyState: "unknown", bodyTextLength: 0, elementCount: 0 };
|
|
5027
|
+
});
|
|
4628
5028
|
return {
|
|
4629
5029
|
url,
|
|
4630
5030
|
...state,
|
|
5031
|
+
unresponsive,
|
|
4631
5032
|
timestamp: Date.now()
|
|
4632
5033
|
};
|
|
4633
5034
|
} catch {
|
|
@@ -4671,6 +5072,8 @@ var init_stale_monitor = __esm({
|
|
|
4671
5072
|
return new Promise((resolve, reject) => {
|
|
4672
5073
|
let resolved = false;
|
|
4673
5074
|
let checkInterval = null;
|
|
5075
|
+
let probing = false;
|
|
5076
|
+
let unresponsiveSince = null;
|
|
4674
5077
|
const cleanup = () => {
|
|
4675
5078
|
resolved = true;
|
|
4676
5079
|
if (checkInterval) clearInterval(checkInterval);
|
|
@@ -4678,11 +5081,25 @@ var init_stale_monitor = __esm({
|
|
|
4678
5081
|
this.timer = null;
|
|
4679
5082
|
};
|
|
4680
5083
|
checkInterval = setInterval(async () => {
|
|
4681
|
-
if (resolved) return;
|
|
5084
|
+
if (resolved || probing) return;
|
|
4682
5085
|
const elapsed = Date.now() - this.lastActivity;
|
|
4683
5086
|
if (elapsed < this.timeoutMs) return;
|
|
5087
|
+
probing = true;
|
|
4684
5088
|
try {
|
|
4685
5089
|
const currentSnapshot = await this.snapshot();
|
|
5090
|
+
if (resolved) return;
|
|
5091
|
+
if (currentSnapshot.unresponsive) {
|
|
5092
|
+
unresponsiveSince ??= Date.now();
|
|
5093
|
+
if (Date.now() - unresponsiveSince < UNRESPONSIVE_GRACE_MS) return;
|
|
5094
|
+
cleanup();
|
|
5095
|
+
reject(new StaleStateError(
|
|
5096
|
+
`The tab stopped responding: its JavaScript did not answer for ${Math.round((Date.now() - unresponsiveSince) / 1e3)}s (a blocking dialog, a frozen re-render, or a navigation that never finished)`,
|
|
5097
|
+
this.timeoutMs,
|
|
5098
|
+
true
|
|
5099
|
+
));
|
|
5100
|
+
return;
|
|
5101
|
+
}
|
|
5102
|
+
unresponsiveSince = null;
|
|
4686
5103
|
if (_StaleStateMonitor.hasChanged(beforeSnapshot, currentSnapshot)) {
|
|
4687
5104
|
this.lastActivity = Date.now();
|
|
4688
5105
|
return;
|
|
@@ -4690,6 +5107,8 @@ var init_stale_monitor = __esm({
|
|
|
4690
5107
|
} catch {
|
|
4691
5108
|
this.lastActivity = Date.now();
|
|
4692
5109
|
return;
|
|
5110
|
+
} finally {
|
|
5111
|
+
probing = false;
|
|
4693
5112
|
}
|
|
4694
5113
|
cleanup();
|
|
4695
5114
|
reject(new StaleStateError(
|
|
@@ -4713,10 +5132,13 @@ var init_stale_monitor = __esm({
|
|
|
4713
5132
|
};
|
|
4714
5133
|
StaleStateError = class extends Error {
|
|
4715
5134
|
timeoutMs;
|
|
4716
|
-
|
|
5135
|
+
/** True when the page stopped answering at all (vs. answering but not changing). */
|
|
5136
|
+
unresponsive;
|
|
5137
|
+
constructor(message, timeoutMs, unresponsive = false) {
|
|
4717
5138
|
super(message);
|
|
4718
5139
|
this.name = "StaleStateError";
|
|
4719
5140
|
this.timeoutMs = timeoutMs;
|
|
5141
|
+
this.unresponsive = unresponsive;
|
|
4720
5142
|
}
|
|
4721
5143
|
};
|
|
4722
5144
|
}
|
|
@@ -4945,10 +5367,14 @@ async function capturePageState(page, ctx, opts) {
|
|
|
4945
5367
|
url = page.url();
|
|
4946
5368
|
} catch {
|
|
4947
5369
|
}
|
|
4948
|
-
const title = await page.title().catch(() => "");
|
|
5370
|
+
const title = await withBudget(page.title(), 2e3, "title").catch(() => "");
|
|
4949
5371
|
if (!screenshot2) return { url, title };
|
|
4950
5372
|
try {
|
|
4951
|
-
const buf = await
|
|
5373
|
+
const buf = await withBudget(
|
|
5374
|
+
page.screenshot({ type: "jpeg", quality: 50, fullPage: false, timeout: 12e3 }),
|
|
5375
|
+
13e3,
|
|
5376
|
+
"screenshot"
|
|
5377
|
+
);
|
|
4952
5378
|
const screenshotUrl = saveScreenshot(buf, `${namePrefix}-${Date.now()}.jpg`, ctx.screenshotDir, ctx.publicUrl);
|
|
4953
5379
|
return { url, title, screenshotUrl };
|
|
4954
5380
|
} catch {
|
|
@@ -4959,6 +5385,7 @@ var init_page_state = __esm({
|
|
|
4959
5385
|
"src/lib/page-state.ts"() {
|
|
4960
5386
|
"use strict";
|
|
4961
5387
|
init_screenshot();
|
|
5388
|
+
init_budget();
|
|
4962
5389
|
}
|
|
4963
5390
|
});
|
|
4964
5391
|
|
|
@@ -5160,6 +5587,24 @@ function buildCurl(method, url, headers, auth, body) {
|
|
|
5160
5587
|
parts.push(` '${url}'`);
|
|
5161
5588
|
return parts.join(" \\\n");
|
|
5162
5589
|
}
|
|
5590
|
+
function serverErrorsFrom(requests) {
|
|
5591
|
+
const out = [];
|
|
5592
|
+
for (const r of requests) {
|
|
5593
|
+
if (r.responseStatus < 400) continue;
|
|
5594
|
+
let body;
|
|
5595
|
+
if (r.responseBody !== void 0 && r.responseBody !== null) {
|
|
5596
|
+
const raw = typeof r.responseBody === "string" ? r.responseBody : JSON.stringify(r.responseBody);
|
|
5597
|
+
const flat = raw.replace(/\s+/g, " ").trim();
|
|
5598
|
+
body = flat.length > SERVER_ERROR_BODY_MAX ? flat.slice(0, SERVER_ERROR_BODY_MAX) + "\u2026" : flat;
|
|
5599
|
+
if (/^\s*<(!doctype|html)/i.test(raw)) {
|
|
5600
|
+
const m = raw.match(/<title[^>]*>([^<]*)<\/title>|<h1[^>]*>([^<]*)<\/h1>/i);
|
|
5601
|
+
body = m ? `[html] ${(m[1] || m[2] || "").trim()}` : "[html page]";
|
|
5602
|
+
}
|
|
5603
|
+
}
|
|
5604
|
+
out.push({ stepIndex: r.triggeredAtStep, method: r.method, url: r.url, status: r.responseStatus, body });
|
|
5605
|
+
}
|
|
5606
|
+
return out;
|
|
5607
|
+
}
|
|
5163
5608
|
function extractAuth(requests) {
|
|
5164
5609
|
const auth = { cookies: {}, tokens: {} };
|
|
5165
5610
|
for (const req of requests) {
|
|
@@ -5245,7 +5690,7 @@ function buildCapturedApi(requests) {
|
|
|
5245
5690
|
}
|
|
5246
5691
|
return apis.sort((a, b) => b.endpoints.length - a.endpoints.length);
|
|
5247
5692
|
}
|
|
5248
|
-
var SKIP_RESOURCE_TYPES, SKIP_EXTENSIONS, BROWSER_NOISE_HEADERS, AUTH_HEADER_PATTERNS, ID_PATTERNS, ApiCapture;
|
|
5693
|
+
var SKIP_RESOURCE_TYPES, SKIP_EXTENSIONS, BROWSER_NOISE_HEADERS, AUTH_HEADER_PATTERNS, ID_PATTERNS, ApiCapture, SERVER_ERROR_BODY_MAX;
|
|
5249
5694
|
var init_api_capture = __esm({
|
|
5250
5695
|
"src/lib/api-capture.ts"() {
|
|
5251
5696
|
"use strict";
|
|
@@ -5415,10 +5860,19 @@ var init_api_capture = __esm({
|
|
|
5415
5860
|
await new Promise((r) => setTimeout(r, 100));
|
|
5416
5861
|
}
|
|
5417
5862
|
}
|
|
5863
|
+
/** Requests fired but not yet answered. */
|
|
5864
|
+
hasPending() {
|
|
5865
|
+
return this.pendingRequests.size > 0;
|
|
5866
|
+
}
|
|
5418
5867
|
getResults() {
|
|
5419
5868
|
return buildCapturedApi(this.requests);
|
|
5420
5869
|
}
|
|
5870
|
+
/** Every HTTP >= 400 response, compact, in capture order. */
|
|
5871
|
+
getServerErrors() {
|
|
5872
|
+
return serverErrorsFrom(this.requests);
|
|
5873
|
+
}
|
|
5421
5874
|
};
|
|
5875
|
+
SERVER_ERROR_BODY_MAX = 600;
|
|
5422
5876
|
}
|
|
5423
5877
|
});
|
|
5424
5878
|
|
|
@@ -5597,10 +6051,35 @@ function classifyError(err, step) {
|
|
|
5597
6051
|
if (step.type === "solve-captcha") return "captcha-unsolvable";
|
|
5598
6052
|
return "action-failed";
|
|
5599
6053
|
}
|
|
6054
|
+
async function refineError(page, step, ctx, errorType, message) {
|
|
6055
|
+
if (errorType !== "stale-state" && errorType !== "timeout") return { errorType, message };
|
|
6056
|
+
const raw = step.selector;
|
|
6057
|
+
if (typeof raw !== "string") return { errorType, message };
|
|
6058
|
+
let resolved;
|
|
6059
|
+
try {
|
|
6060
|
+
resolved = resolveSelector(raw, ctx);
|
|
6061
|
+
} catch {
|
|
6062
|
+
return { errorType, message };
|
|
6063
|
+
}
|
|
6064
|
+
try {
|
|
6065
|
+
const probe = page.evaluate((sel) => !!document.querySelector(sel), resolved);
|
|
6066
|
+
const budget = new Promise((r) => setTimeout(() => r(null), TIMING.FAILURE_PROBE));
|
|
6067
|
+
const found = await Promise.race([probe, budget]);
|
|
6068
|
+
if (found !== false) return { errorType, message };
|
|
6069
|
+
} catch {
|
|
6070
|
+
return { errorType, message };
|
|
6071
|
+
}
|
|
6072
|
+
const shown = resolved === raw ? `"${raw}"` : `"${raw}" (\u2192 ${resolved})`;
|
|
6073
|
+
return {
|
|
6074
|
+
errorType: "element-not-found",
|
|
6075
|
+
message: `No element matches ${shown} on the current page \u2014 the step waited for it to appear and it never did. It most likely existed when you looked and was re-rendered away before this step ran. (${message})`
|
|
6076
|
+
};
|
|
6077
|
+
}
|
|
5600
6078
|
function isRetryable(errorType) {
|
|
5601
6079
|
return errorType === "stale-state" || errorType === "timeout" || errorType === "element-not-found";
|
|
5602
6080
|
}
|
|
5603
|
-
function getSuggestion(errorType, step) {
|
|
6081
|
+
function getSuggestion(errorType, step, message = "") {
|
|
6082
|
+
if (message.includes("tab stopped responding")) return UNRESPONSIVE_SUGGESTION;
|
|
5604
6083
|
const usedEphemeralRef = typeof step.selector === "string" && step.selector.startsWith("@e");
|
|
5605
6084
|
if (usedEphemeralRef && (errorType === "element-not-found" || errorType === "timeout" || errorType === "stale-state")) {
|
|
5606
6085
|
return "The @e ref went stale \u2014 the page (likely a React/SPA) re-rendered and dropped it since the snapshot. Take a fresh `snapshot`/`find` right before acting, or save the element as a durable `@a:` anchor with the `remember` tool.";
|
|
@@ -5609,7 +6088,7 @@ function getSuggestion(errorType, step) {
|
|
|
5609
6088
|
case "stale-state":
|
|
5610
6089
|
return "The page stopped responding. The step may have triggered a very slow load or the server may be unreachable.";
|
|
5611
6090
|
case "element-not-found":
|
|
5612
|
-
return
|
|
6091
|
+
return "Selector not found. Either it is wrong, or the page re-rendered the element away (SPAs swap DOM nodes on every state change, so a selector that worked a second ago can return nothing now). Take a fresh `snapshot`/`find` immediately before acting and retry; if it keeps vanishing, `wait-for` it first.";
|
|
5613
6092
|
case "navigation-failed":
|
|
5614
6093
|
return "Navigation failed. The URL may be unreachable, blocked, or require authentication.";
|
|
5615
6094
|
case "captcha-unsolvable":
|
|
@@ -5620,7 +6099,7 @@ function getSuggestion(errorType, step) {
|
|
|
5620
6099
|
return void 0;
|
|
5621
6100
|
}
|
|
5622
6101
|
}
|
|
5623
|
-
var DEFAULT_STALE_TIMEOUT_MS2, PipelineRunner;
|
|
6102
|
+
var DEFAULT_STALE_TIMEOUT_MS2, UNRESPONSIVE_SUGGESTION, PipelineRunner;
|
|
5624
6103
|
var init_pipeline = __esm({
|
|
5625
6104
|
"src/lib/pipeline.ts"() {
|
|
5626
6105
|
"use strict";
|
|
@@ -5636,7 +6115,10 @@ var init_pipeline = __esm({
|
|
|
5636
6115
|
init_constants();
|
|
5637
6116
|
init_component_map();
|
|
5638
6117
|
init_resolve_selector();
|
|
6118
|
+
init_constants();
|
|
6119
|
+
init_budget();
|
|
5639
6120
|
DEFAULT_STALE_TIMEOUT_MS2 = 2e4;
|
|
6121
|
+
UNRESPONSIVE_SUGGESTION = "The tab's JavaScript stopped answering during this step. Run a single `read` or `snapshot` on its own to see if it recovered (it usually does). If this step froze it, don't repeat it as-is: for a route change in a single-page app use `evaluate` with `location.assign(url)`; for a click, use a fresh `@e` ref from `find`; avoid `evaluate` calls that scroll or re-render.";
|
|
5640
6122
|
PipelineRunner = class {
|
|
5641
6123
|
constructor(ctx, hooks) {
|
|
5642
6124
|
this.ctx = ctx;
|
|
@@ -5644,6 +6126,9 @@ var init_pipeline = __esm({
|
|
|
5644
6126
|
}
|
|
5645
6127
|
ctx;
|
|
5646
6128
|
hooks;
|
|
6129
|
+
/** Live progress, readable while run() is still pending — lets an outer
|
|
6130
|
+
* watchdog report WHICH step hung and what already finished. */
|
|
6131
|
+
progress = { stepIndex: -1, results: [] };
|
|
5647
6132
|
async run(initialPage, pipeline) {
|
|
5648
6133
|
const tracker = new TabTracker(initialPage.context(), initialPage);
|
|
5649
6134
|
try {
|
|
@@ -5661,6 +6146,7 @@ var init_pipeline = __esm({
|
|
|
5661
6146
|
const continueOnError = opts.continueOnError ?? false;
|
|
5662
6147
|
const results = [];
|
|
5663
6148
|
const obstacles = [];
|
|
6149
|
+
this.progress.results = results;
|
|
5664
6150
|
const navStep = pipeline.steps.find((s) => s.type === "navigate");
|
|
5665
6151
|
try {
|
|
5666
6152
|
const host = new URL(navStep?.url || safePageUrl(initialPage) || "http://x").hostname;
|
|
@@ -5678,12 +6164,18 @@ var init_pipeline = __esm({
|
|
|
5678
6164
|
if (capture) capture.start();
|
|
5679
6165
|
const finishCapture = async () => {
|
|
5680
6166
|
if (!capture) return void 0;
|
|
6167
|
+
await capture.drain(TIMING.CAPTURE_SETTLE, TIMING.CAPTURE_PENDING_MAX);
|
|
5681
6168
|
capture.stop();
|
|
6169
|
+
for (const se of capture.getServerErrors()) {
|
|
6170
|
+
const r = results.find((x) => x.stepIndex === se.stepIndex);
|
|
6171
|
+
if (r) (r.serverErrors ??= []).push(se);
|
|
6172
|
+
}
|
|
5682
6173
|
return capture.getResults();
|
|
5683
6174
|
};
|
|
5684
6175
|
for (let i = 0; i < pipeline.steps.length; i++) {
|
|
5685
6176
|
if (capture) capture.setStep(i);
|
|
5686
6177
|
const step = pipeline.steps[i];
|
|
6178
|
+
this.progress.stepIndex = i;
|
|
5687
6179
|
const page = tracker.active();
|
|
5688
6180
|
const urlBefore = safePageUrl(page);
|
|
5689
6181
|
const monitor = new StaleStateMonitor(page, staleTimeoutMs);
|
|
@@ -5703,9 +6195,10 @@ var init_pipeline = __esm({
|
|
|
5703
6195
|
throw err;
|
|
5704
6196
|
}
|
|
5705
6197
|
const asError = err instanceof Error ? err : new Error(String(err));
|
|
5706
|
-
const
|
|
6198
|
+
const refined = await refineError(tracker.active(), step, this.ctx, classifyError(asError, step), asError.message);
|
|
6199
|
+
const errorType = refined.errorType;
|
|
5707
6200
|
const pageState = await capturePageState(tracker.active(), this.ctx, { screenshot: true });
|
|
5708
|
-
stepResult = failedStepResult(step,
|
|
6201
|
+
stepResult = failedStepResult(step, refined.message, Date.now() - startTime, i);
|
|
5709
6202
|
results.push(stepResult);
|
|
5710
6203
|
recordAnchor(step, false);
|
|
5711
6204
|
return {
|
|
@@ -5719,9 +6212,9 @@ var init_pipeline = __esm({
|
|
|
5719
6212
|
failedAtStep: i,
|
|
5720
6213
|
failedStep: step,
|
|
5721
6214
|
errorType,
|
|
5722
|
-
message:
|
|
6215
|
+
message: refined.message,
|
|
5723
6216
|
pageState,
|
|
5724
|
-
suggestion: getSuggestion(errorType, step),
|
|
6217
|
+
suggestion: getSuggestion(errorType, step, refined.message),
|
|
5725
6218
|
retryable: isRetryable(errorType)
|
|
5726
6219
|
},
|
|
5727
6220
|
durationMs: Date.now() - startTime,
|
|
@@ -5755,8 +6248,16 @@ var init_pipeline = __esm({
|
|
|
5755
6248
|
}
|
|
5756
6249
|
results.push(stepResult);
|
|
5757
6250
|
if (!stepResult.ok && !continueOnError) {
|
|
6251
|
+
const refined = await refineError(
|
|
6252
|
+
tracker.active(),
|
|
6253
|
+
step,
|
|
6254
|
+
this.ctx,
|
|
6255
|
+
classifyError(new Error(stepResult.error || ""), step),
|
|
6256
|
+
stepResult.error || "Step failed"
|
|
6257
|
+
);
|
|
6258
|
+
stepResult.error = refined.message;
|
|
6259
|
+
const errorType = refined.errorType;
|
|
5758
6260
|
const pageState = await capturePageState(tracker.active(), this.ctx, { screenshot: true });
|
|
5759
|
-
const errorType = classifyError(new Error(stepResult.error || ""), step);
|
|
5760
6261
|
return {
|
|
5761
6262
|
ok: false,
|
|
5762
6263
|
completedSteps: i,
|
|
@@ -5770,7 +6271,7 @@ var init_pipeline = __esm({
|
|
|
5770
6271
|
errorType,
|
|
5771
6272
|
message: stepResult.error || "Step failed",
|
|
5772
6273
|
pageState,
|
|
5773
|
-
suggestion: getSuggestion(errorType, step),
|
|
6274
|
+
suggestion: getSuggestion(errorType, step, refined.message),
|
|
5774
6275
|
retryable: isRetryable(errorType)
|
|
5775
6276
|
},
|
|
5776
6277
|
durationMs: Date.now() - startTime,
|
|
@@ -5780,7 +6281,7 @@ var init_pipeline = __esm({
|
|
|
5780
6281
|
if (step.type === "navigate" && continueOnObstacle) {
|
|
5781
6282
|
const obstacleStart = Date.now();
|
|
5782
6283
|
const obstaclePage = tracker.active();
|
|
5783
|
-
const obstacle = await detectObstacles(obstaclePage);
|
|
6284
|
+
const obstacle = await withBudget(detectObstacles(obstaclePage), 1e4, "obstacle check").catch(() => null);
|
|
5784
6285
|
if (obstacle) {
|
|
5785
6286
|
const resolution = await resolveObstacle(obstaclePage, obstacle, this.ctx, monitor);
|
|
5786
6287
|
obstacles.push({
|
|
@@ -10235,7 +10736,7 @@ var init_cdp_relay = __esm({
|
|
|
10235
10736
|
return await Promise.race([
|
|
10236
10737
|
first,
|
|
10237
10738
|
new Promise((_, reject) => {
|
|
10238
|
-
const t = setTimeout(() => reject(new Error("screenshot timed out (no compositor frame)")),
|
|
10739
|
+
const t = setTimeout(() => reject(new Error("screenshot timed out (no compositor frame)")), 5e3);
|
|
10239
10740
|
t.unref?.();
|
|
10240
10741
|
})
|
|
10241
10742
|
]);
|
|
@@ -10295,6 +10796,24 @@ var init_cdp_relay = __esm({
|
|
|
10295
10796
|
});
|
|
10296
10797
|
|
|
10297
10798
|
// src/lib/execution/pipeline-executor.ts
|
|
10799
|
+
function blankPipelineResult(pipeline, modeUsed, error) {
|
|
10800
|
+
return {
|
|
10801
|
+
ok: false,
|
|
10802
|
+
completedSteps: 0,
|
|
10803
|
+
totalSteps: pipeline.steps.length,
|
|
10804
|
+
results: [],
|
|
10805
|
+
obstacles: [],
|
|
10806
|
+
error: {
|
|
10807
|
+
failedAtStep: 0,
|
|
10808
|
+
failedStep: pipeline.steps[0],
|
|
10809
|
+
pageState: { url: "", title: "" },
|
|
10810
|
+
...error
|
|
10811
|
+
},
|
|
10812
|
+
durationMs: 0,
|
|
10813
|
+
modeUsed,
|
|
10814
|
+
finalState: { url: "", title: "" }
|
|
10815
|
+
};
|
|
10816
|
+
}
|
|
10298
10817
|
var import_playwright_core, log18, PipelineExecutor;
|
|
10299
10818
|
var init_pipeline_executor = __esm({
|
|
10300
10819
|
"src/lib/execution/pipeline-executor.ts"() {
|
|
@@ -10351,8 +10870,24 @@ var init_pipeline_executor = __esm({
|
|
|
10351
10870
|
const forcedMode = opts.mode;
|
|
10352
10871
|
const autoEscalate = opts.autoEscalate !== false;
|
|
10353
10872
|
const instanceId = opts.instanceId || DEFAULT_INSTANCE;
|
|
10873
|
+
if (forcedMode === "extension") {
|
|
10874
|
+
return blankPipelineResult(pipeline, "extension", {
|
|
10875
|
+
errorType: "action-failed",
|
|
10876
|
+
message: 'mode="extension" requires options.tabId (the real-Chrome tab to drive).',
|
|
10877
|
+
suggestion: "Call the `tabs` tool (or GET /extension/tabs) to list open tabs, then pass that tab id. Nothing was launched.",
|
|
10878
|
+
retryable: false
|
|
10879
|
+
});
|
|
10880
|
+
}
|
|
10354
10881
|
const firstNav = pipeline.steps.find((s) => s.type === "navigate");
|
|
10355
10882
|
const domain = firstNav ? new URL(firstNav.url).hostname : null;
|
|
10883
|
+
if (!firstNav && !this.deps.daemon.findLiveMode(instanceId)) {
|
|
10884
|
+
return blankPipelineResult(pipeline, forcedMode || "headless", {
|
|
10885
|
+
errorType: "action-failed",
|
|
10886
|
+
message: `No navigate step and no live browser for instanceId "${instanceId}" to act on.`,
|
|
10887
|
+
suggestion: "Add a navigate step, or reattach to a live window (check `status` / `iframer instances` for instanceId). To drive a real Chrome tab, pass options.tabId from the `tabs` tool. Nothing was launched.",
|
|
10888
|
+
retryable: false
|
|
10889
|
+
});
|
|
10890
|
+
}
|
|
10356
10891
|
const availableModes = this.deps.availableModes();
|
|
10357
10892
|
const liveMode = forcedMode ? null : this.deps.daemon.findLiveMode(instanceId);
|
|
10358
10893
|
let mode;
|
|
@@ -10416,6 +10951,7 @@ var init_pipeline_executor = __esm({
|
|
|
10416
10951
|
const startTime = Date.now();
|
|
10417
10952
|
const relay = new CdpRelay(tabId, clientId, pipeline.options?.focus);
|
|
10418
10953
|
let browser;
|
|
10954
|
+
let runner;
|
|
10419
10955
|
try {
|
|
10420
10956
|
await relay.start();
|
|
10421
10957
|
browser = await import_playwright_core.chromium.connectOverCDP(relay.httpEndpoint(), { timeout: 3e4 });
|
|
@@ -10428,7 +10964,8 @@ var init_pipeline_executor = __esm({
|
|
|
10428
10964
|
if (!page) throw new Error("no page available for the tab (is it still open?)");
|
|
10429
10965
|
const ctx = this.deps.refStore.makeContext(userId, token);
|
|
10430
10966
|
if (this.pendingElicitOtp) ctx.elicitOtp = this.pendingElicitOtp;
|
|
10431
|
-
|
|
10967
|
+
ctx.driver = "extension";
|
|
10968
|
+
runner = new PipelineRunner(ctx);
|
|
10432
10969
|
const typeChars = pipeline.steps.reduce((n, s) => {
|
|
10433
10970
|
const v = s.value;
|
|
10434
10971
|
return (s.type === "human-type" || s.type === "type-code") && typeof v === "string" ? n + v.length : n;
|
|
@@ -10445,7 +10982,7 @@ var init_pipeline_executor = __esm({
|
|
|
10445
10982
|
watchdog = setTimeout(
|
|
10446
10983
|
() => reject(
|
|
10447
10984
|
new Error(
|
|
10448
|
-
`pipeline exceeded ${Math.round(capMs / 1e3)}s \u2014
|
|
10985
|
+
`pipeline exceeded ${Math.round(capMs / 1e3)}s \u2014 a call into the tab never returned.`
|
|
10449
10986
|
)
|
|
10450
10987
|
),
|
|
10451
10988
|
capMs
|
|
@@ -10469,23 +11006,26 @@ var init_pipeline_executor = __esm({
|
|
|
10469
11006
|
} catch (err) {
|
|
10470
11007
|
const msg = getErrorMessage(err);
|
|
10471
11008
|
const stalled = msg.includes("pipeline exceeded");
|
|
11009
|
+
const done = runner ? runner.progress.results.filter((r) => r.ok) : [];
|
|
11010
|
+
const hungAt = stalled && runner && runner.progress.stepIndex >= 0 ? runner.progress.stepIndex : 0;
|
|
11011
|
+
const hungStep = pipeline.steps[hungAt];
|
|
10472
11012
|
return {
|
|
10473
11013
|
ok: false,
|
|
10474
|
-
completedSteps: 0,
|
|
11014
|
+
completedSteps: stalled ? done.length : 0,
|
|
10475
11015
|
totalSteps: pipeline.steps.length,
|
|
10476
|
-
results: [],
|
|
11016
|
+
results: stalled ? done : [],
|
|
10477
11017
|
finalState: { url: "", title: "" },
|
|
10478
11018
|
obstacles: [],
|
|
10479
11019
|
durationMs: Date.now() - startTime,
|
|
10480
11020
|
modeUsed: "extension",
|
|
10481
11021
|
error: {
|
|
10482
|
-
failedAtStep:
|
|
10483
|
-
failedStep:
|
|
10484
|
-
errorType: "action-failed",
|
|
10485
|
-
message: `Extension mode failed: ${msg}`,
|
|
11022
|
+
failedAtStep: hungAt,
|
|
11023
|
+
failedStep: hungStep,
|
|
11024
|
+
errorType: stalled ? "stale-state" : "action-failed",
|
|
11025
|
+
message: stalled ? `Extension mode: step ${hungAt} (${hungStep?.type}) never returned; ${msg}` : `Extension mode failed: ${msg}`,
|
|
10486
11026
|
pageState: { url: "", title: "" },
|
|
10487
|
-
suggestion: stalled ?
|
|
10488
|
-
retryable:
|
|
11027
|
+
suggestion: stalled ? `Step ${hungAt} (${hungStep?.type}) hung inside the tab. Steps before it completed. Run a single \`read\` or \`snapshot\` on its own to check the tab recovered (it usually does), then continue from that step in its own execute call. Don't repeat the same step unchanged: for a route change use \`evaluate\` with \`location.assign(url)\`, for a click use a fresh \`@e\` ref from \`find\`. If the tab stays unresponsive, tell the user.` : "Ensure the iframer extension is connected (green dot) and the tab is still open. See chrome://extensions.",
|
|
11028
|
+
retryable: stalled
|
|
10489
11029
|
}
|
|
10490
11030
|
};
|
|
10491
11031
|
} finally {
|
|
@@ -11105,6 +11645,11 @@ var init_iframer = __esm({
|
|
|
11105
11645
|
listInstances() {
|
|
11106
11646
|
return this.daemon.instancesInfo();
|
|
11107
11647
|
}
|
|
11648
|
+
/** Close browsers that never navigated anywhere. See
|
|
11649
|
+
* BrowserDaemon.sweepBlankInstances — called from the server's reap tick. */
|
|
11650
|
+
sweepBlankBrowsers() {
|
|
11651
|
+
return this.daemon.sweepBlankInstances();
|
|
11652
|
+
}
|
|
11108
11653
|
/** Kill all browser instances and reset state. Next execute call will
|
|
11109
11654
|
* launch a fresh browser automatically — no manual restart needed. */
|
|
11110
11655
|
async restartBrowser() {
|
|
@@ -11244,6 +11789,13 @@ ${api.domain} (${api.baseUrl})`);
|
|
|
11244
11789
|
}
|
|
11245
11790
|
}
|
|
11246
11791
|
}
|
|
11792
|
+
const serverErrors = (data.results || []).flatMap((r) => r.serverErrors || []);
|
|
11793
|
+
if (serverErrors.length > 0) {
|
|
11794
|
+
lines.push("\n--- Server errors (HTTP >= 400) ---");
|
|
11795
|
+
for (const se of serverErrors) {
|
|
11796
|
+
lines.push(` [step ${se.stepIndex}] ${se.method} ${se.url} \u2192 ${se.status}${se.body ? `: ${se.body}` : ""}`);
|
|
11797
|
+
}
|
|
11798
|
+
}
|
|
11247
11799
|
if (data.error) {
|
|
11248
11800
|
lines.push("\n--- Failure ---");
|
|
11249
11801
|
if (typeof data.error === "string") {
|
|
@@ -11254,6 +11806,10 @@ ${api.domain} (${api.baseUrl})`);
|
|
|
11254
11806
|
lines.push(`Message: ${data.error.message}`);
|
|
11255
11807
|
lines.push(`Retryable: ${data.error.retryable}`);
|
|
11256
11808
|
if (data.error.suggestion) lines.push(`Suggestion: ${data.error.suggestion}`);
|
|
11809
|
+
const atFailure = serverErrors.filter((se) => se.stepIndex === data.error?.failedAtStep);
|
|
11810
|
+
for (const se of atFailure) {
|
|
11811
|
+
lines.push(`Server responded: ${se.method} ${se.url} \u2192 ${se.status}${se.body ? `: ${se.body}` : ""}`);
|
|
11812
|
+
}
|
|
11257
11813
|
if (data.error.pageState?.url) lines.push(`URL at failure: ${data.error.pageState.url}`);
|
|
11258
11814
|
}
|
|
11259
11815
|
}
|
|
@@ -11670,6 +12226,21 @@ function parseFlag(args2, flag, hasValue = true) {
|
|
|
11670
12226
|
function hasFlag(args2, flag) {
|
|
11671
12227
|
return args2.includes(flag);
|
|
11672
12228
|
}
|
|
12229
|
+
var LAUNCH_MODES = ["headless", "binary-headful", "docker-headful"];
|
|
12230
|
+
function parseModeFlag(args2) {
|
|
12231
|
+
const mode = parseFlag(args2, "--mode");
|
|
12232
|
+
if (!mode) return void 0;
|
|
12233
|
+
if (LAUNCH_MODES.includes(mode)) return mode;
|
|
12234
|
+
if (mode === "extension") {
|
|
12235
|
+
console.error("\n --mode extension is not available from the CLI: it needs a tab id.");
|
|
12236
|
+
console.error(" Use the `tabs` tool from your agent, or POST /extension/execute with a tabId.\n");
|
|
12237
|
+
process.exit(1);
|
|
12238
|
+
}
|
|
12239
|
+
console.error(`
|
|
12240
|
+
Unknown --mode "${mode}". Use one of: ${LAUNCH_MODES.join(", ")}.
|
|
12241
|
+
`);
|
|
12242
|
+
process.exit(1);
|
|
12243
|
+
}
|
|
11673
12244
|
function handleResponse(data, screenshotPath) {
|
|
11674
12245
|
const { screenshot: screenshot2, tileScreenshots, ...rest } = data;
|
|
11675
12246
|
if (screenshot2 && screenshotPath) {
|
|
@@ -12144,7 +12715,7 @@ async function main() {
|
|
|
12144
12715
|
process.exit(1);
|
|
12145
12716
|
}
|
|
12146
12717
|
const options = { ...inputOptions };
|
|
12147
|
-
const mode =
|
|
12718
|
+
const mode = parseModeFlag(args);
|
|
12148
12719
|
if (mode) options.mode = mode;
|
|
12149
12720
|
if (hasFlag(args, "--capture-api")) options.captureApi = true;
|
|
12150
12721
|
if (hasFlag(args, "--continue-on-error")) options.continueOnError = true;
|
|
@@ -12229,7 +12800,7 @@ Screenshot: ${shot}`);
|
|
|
12229
12800
|
const url = args[0];
|
|
12230
12801
|
const outPath = parseFlag(args, "--output") || parseFlag(args, "-o") || "/tmp/iframer-screenshot.jpg";
|
|
12231
12802
|
if (url && url.startsWith("http")) {
|
|
12232
|
-
const mode =
|
|
12803
|
+
const mode = parseModeFlag(args) || "headless";
|
|
12233
12804
|
const annotate = hasFlag(args, "--annotate");
|
|
12234
12805
|
const docker = await isDockerRunning();
|
|
12235
12806
|
const steps = [
|
|
@@ -12460,7 +13031,7 @@ Screenshot: ${shot}`);
|
|
|
12460
13031
|
process.exit(1);
|
|
12461
13032
|
}
|
|
12462
13033
|
const options = { captureApi: true };
|
|
12463
|
-
const mode =
|
|
13034
|
+
const mode = parseModeFlag(args);
|
|
12464
13035
|
if (mode) options.mode = mode;
|
|
12465
13036
|
const docker = await isDockerRunning();
|
|
12466
13037
|
let result;
|