@xbrowser/cli 1.9.8 → 1.10.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/{anti-bot-MCVM6IFB.js → anti-bot-DR56Y63V.js} +1 -1
- package/dist/{browser-SJWPJFN7.js → browser-HBL72GPZ.js} +2 -2
- package/dist/{browser-GT4BMX6X.js → browser-T3V3JWVH.js} +1 -1
- package/dist/{browser-43YXUIML.js → browser-WQZ3D6AE.js} +2 -2
- package/dist/{cdp-driver-WAFYSIIW.js → cdp-driver-2T4P4ZE2.js} +87 -1
- package/dist/{cdp-driver-PUZLDSQU.js → cdp-driver-J3YQ4LJV.js} +1 -1
- package/dist/chunk-3OD76SUE.js +20 -0
- package/dist/chunk-43TEMG4U.js +9 -0
- package/dist/{chunk-C3LIGO4V.js → chunk-4452SPFI.js} +906 -16
- package/dist/{chunk-VEWTHYDG.js → chunk-5UGR6MUK.js} +906 -16
- package/dist/{chunk-ENCEMM7Z.js → chunk-BAHSRZIX.js} +82 -29
- package/dist/{chunk-7ZDWM6T2.js → chunk-BNO7OKO4.js} +5 -6
- package/dist/{chunk-24SY6PE5.js → chunk-CG3D3CHY.js} +1 -8
- package/dist/{chunk-ZJHQPMCY.js → chunk-DWDXEGVK.js} +5 -6
- package/dist/{chunk-DPJNCMLC.js → chunk-INTQPBYF.js} +7 -0
- package/dist/{human-interaction-OU7LZ6OZ.js → chunk-MWFHZUIY.js} +6 -7
- package/dist/{chunk-A4YHJW3Z.js → chunk-NITFVWWS.js} +168 -29
- package/dist/chunk-Q2DFJQGS.js +128 -0
- package/dist/{chunk-GM3HWQ5U.js → chunk-RRBXV7KE.js} +87 -1
- package/dist/{human-interaction-HJTXFVQS.js → chunk-SQHSZENE.js} +2 -2
- package/dist/chunk-TZPKFUBT.js +20 -0
- package/dist/{chunk-PBDID7SE.js → chunk-WSCP7QCJ.js} +82 -29
- package/dist/{chunk-YAUG3NSI.js → chunk-YMUSHPU4.js} +16 -128
- package/dist/cli.js +48 -31
- package/dist/{daemon-client-4HJENICO.js → daemon-client-GKEPT4NY.js} +15 -3
- package/dist/{daemon-client-CMRAWXTN.js → daemon-client-O6BYVRXV.js} +4 -1
- package/dist/daemon-main.js +649 -58
- package/dist/human-interaction-5AO42MBA.js +8 -0
- package/dist/{human-interaction-2DZK4MW7.js → human-interaction-C5OEGXGO.js} +1 -1
- package/dist/human-interaction-ISXZTAYY.js +8 -0
- package/dist/index.d.ts +167 -2
- package/dist/index.js +194 -67
- package/dist/recovery-EF33LKRJ.js +77 -0
- package/dist/recovery-J2ISVGUL.js +77 -0
- package/dist/recovery-ZJVVHP7N.js +76 -0
- package/dist/{session-recorder-K3K63Q33.js → session-recorder-H3KEYU26.js} +1 -1
- package/dist/{session-recorder-AG6CH6EI.js → session-recorder-QRZMKFVL.js} +1 -1
- package/dist/{session-replayer-V4MP6M6I.js → session-replayer-LJUC4TI7.js} +24 -7
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
launch
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-RRBXV7KE.js";
|
|
4
4
|
import {
|
|
5
5
|
errMsg
|
|
6
6
|
} from "./chunk-GDKLH7ZY.js";
|
|
@@ -1499,7 +1499,7 @@ process.on("exit", () => {
|
|
|
1499
1499
|
if (session.isCDP) {
|
|
1500
1500
|
logSessionEvent("process_exit", `Session "${session.name}": CDP connection (not closing external browser).`);
|
|
1501
1501
|
} else {
|
|
1502
|
-
logSessionEvent("process_exit", `Session "${session.name}": Closing browser.`);
|
|
1502
|
+
logSessionEvent("process_exit", `Session "${session.name}": Closing self-launched browser.`);
|
|
1503
1503
|
try {
|
|
1504
1504
|
session.browser?.close();
|
|
1505
1505
|
} catch {
|
|
@@ -1507,7 +1507,7 @@ process.on("exit", () => {
|
|
|
1507
1507
|
}
|
|
1508
1508
|
}
|
|
1509
1509
|
if (_sharedBrowser) {
|
|
1510
|
-
logSessionEvent("process_exit", "Closing shared browser.");
|
|
1510
|
+
logSessionEvent("process_exit", "Closing shared browser (self-launched only, external CDP is safe).");
|
|
1511
1511
|
try {
|
|
1512
1512
|
_sharedBrowser.close();
|
|
1513
1513
|
} catch {
|
|
@@ -1637,9 +1637,29 @@ function deleteSessionDiskMeta(name) {
|
|
|
1637
1637
|
} catch {
|
|
1638
1638
|
}
|
|
1639
1639
|
}
|
|
1640
|
+
async function isSessionPageAlive(session) {
|
|
1641
|
+
const page = session.page;
|
|
1642
|
+
if (!page || typeof page.evaluate !== "function") return false;
|
|
1643
|
+
try {
|
|
1644
|
+
await Promise.race([
|
|
1645
|
+
page.evaluate("1"),
|
|
1646
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("liveness probe timeout")), 1500))
|
|
1647
|
+
]);
|
|
1648
|
+
return true;
|
|
1649
|
+
} catch {
|
|
1650
|
+
return false;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1640
1653
|
async function findOrRestoreSession(name, cdpEndpoint) {
|
|
1641
1654
|
const inMem = findSession(name);
|
|
1642
|
-
if (inMem)
|
|
1655
|
+
if (inMem) {
|
|
1656
|
+
if (await isSessionPageAlive(inMem)) return inMem;
|
|
1657
|
+
logSessionEvent("stale_session", `name="${name}" \u2014 page \u63A2\u9488\u5931\u8D25\uFF08\u6D4F\u89C8\u5668\u53EF\u80FD\u5DF2\u91CD\u542F\uFF09\uFF0C\u5C31\u5730\u91CD\u5EFA`);
|
|
1658
|
+
try {
|
|
1659
|
+
await closeSessionByName(name);
|
|
1660
|
+
} catch {
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1643
1663
|
const meta = readSessionDiskMeta(name);
|
|
1644
1664
|
if (!meta) return void 0;
|
|
1645
1665
|
const ep = cdpEndpoint || meta.cdpEndpoint;
|
|
@@ -1902,14 +1922,6 @@ async function createSession(name, url, options) {
|
|
|
1902
1922
|
let context;
|
|
1903
1923
|
let page;
|
|
1904
1924
|
if (isCDP) {
|
|
1905
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
1906
|
-
let contexts = b.contexts();
|
|
1907
|
-
if (contexts.length === 0) {
|
|
1908
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
1909
|
-
contexts = b.contexts();
|
|
1910
|
-
}
|
|
1911
|
-
context = contexts[0] || await b.newContext();
|
|
1912
|
-
let targetPage = null;
|
|
1913
1925
|
const targetHostname = url ? (() => {
|
|
1914
1926
|
try {
|
|
1915
1927
|
return new URL(url).hostname;
|
|
@@ -1917,12 +1929,54 @@ async function createSession(name, url, options) {
|
|
|
1917
1929
|
return "";
|
|
1918
1930
|
}
|
|
1919
1931
|
})() : "";
|
|
1932
|
+
const isRealPageUrl = (u) => !!u && u !== "about:blank" && !u.startsWith("chrome://") && !u.startsWith("chrome-untrusted://") && !u.startsWith("chrome-error://");
|
|
1933
|
+
const resolvePageUrl = async (p) => {
|
|
1934
|
+
const u = p.url();
|
|
1935
|
+
if (u && u !== "about:blank") return u;
|
|
1936
|
+
try {
|
|
1937
|
+
return await p.evaluate("location.href");
|
|
1938
|
+
} catch {
|
|
1939
|
+
return u ?? "";
|
|
1940
|
+
}
|
|
1941
|
+
};
|
|
1942
|
+
const pollStart = Date.now();
|
|
1943
|
+
for (; ; ) {
|
|
1944
|
+
const ctxs = b.contexts();
|
|
1945
|
+
let hostHit = false;
|
|
1946
|
+
let anyHit = false;
|
|
1947
|
+
for (const ctx of ctxs) {
|
|
1948
|
+
for (const p of ctx.pages()) {
|
|
1949
|
+
const u = await resolvePageUrl(p);
|
|
1950
|
+
if (!isRealPageUrl(u)) continue;
|
|
1951
|
+
anyHit = true;
|
|
1952
|
+
if (targetHostname && u.includes(targetHostname)) {
|
|
1953
|
+
hostHit = true;
|
|
1954
|
+
break;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
if (hostHit) break;
|
|
1958
|
+
}
|
|
1959
|
+
if (process.env.XB_DEBUG_SESSION_POLL) {
|
|
1960
|
+
console.error(`[poll] t=${Date.now() - pollStart}ms ctxs=${ctxs.length} hostHit=${hostHit} anyHit=${anyHit}`);
|
|
1961
|
+
}
|
|
1962
|
+
if (hostHit) break;
|
|
1963
|
+
if (anyHit && Date.now() - pollStart >= 2e3) break;
|
|
1964
|
+
if (Date.now() - pollStart >= 5e3) break;
|
|
1965
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
1966
|
+
}
|
|
1967
|
+
let contexts = b.contexts();
|
|
1968
|
+
if (contexts.length === 0) {
|
|
1969
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1970
|
+
contexts = b.contexts();
|
|
1971
|
+
}
|
|
1972
|
+
context = contexts[0] || await b.newContext();
|
|
1973
|
+
let targetPage = null;
|
|
1920
1974
|
if (targetHostname) {
|
|
1921
1975
|
for (const ctx of contexts) {
|
|
1922
1976
|
const pages = ctx.pages();
|
|
1923
1977
|
for (const p of pages) {
|
|
1924
|
-
const pUrl = p
|
|
1925
|
-
if (
|
|
1978
|
+
const pUrl = await resolvePageUrl(p);
|
|
1979
|
+
if (isRealPageUrl(pUrl) && pUrl.includes(targetHostname)) {
|
|
1926
1980
|
targetPage = p;
|
|
1927
1981
|
break;
|
|
1928
1982
|
}
|
|
@@ -1934,8 +1988,8 @@ async function createSession(name, url, options) {
|
|
|
1934
1988
|
for (const ctx of contexts) {
|
|
1935
1989
|
const pages = ctx.pages();
|
|
1936
1990
|
for (const p of pages) {
|
|
1937
|
-
const pUrl = p
|
|
1938
|
-
if (
|
|
1991
|
+
const pUrl = await resolvePageUrl(p);
|
|
1992
|
+
if (isRealPageUrl(pUrl)) {
|
|
1939
1993
|
targetPage = p;
|
|
1940
1994
|
break;
|
|
1941
1995
|
}
|
|
@@ -1993,10 +2047,6 @@ async function closeSessionByName(name) {
|
|
|
1993
2047
|
if (session.name === name || session.id === name) {
|
|
1994
2048
|
logSessionEvent("close_session", `name="${session.name}" id="${session.id}" url="${session.page.url()}"`);
|
|
1995
2049
|
if (session.isCDP) {
|
|
1996
|
-
try {
|
|
1997
|
-
await session.page.close();
|
|
1998
|
-
} catch {
|
|
1999
|
-
}
|
|
2000
2050
|
if (session.browser) {
|
|
2001
2051
|
await session.browser.close().catch(() => {
|
|
2002
2052
|
});
|
|
@@ -2021,7 +2071,7 @@ async function closeSessionByName(name) {
|
|
|
2021
2071
|
} catch {
|
|
2022
2072
|
}
|
|
2023
2073
|
try {
|
|
2024
|
-
const { SessionRecorder } = await import("./session-recorder-
|
|
2074
|
+
const { SessionRecorder } = await import("./session-recorder-H3KEYU26.js");
|
|
2025
2075
|
SessionRecorder.cleanup(session.name);
|
|
2026
2076
|
} catch {
|
|
2027
2077
|
}
|
|
@@ -2040,12 +2090,17 @@ async function closeAllSessions() {
|
|
|
2040
2090
|
if (names) logSessionEvent("close_all_sessions", `Closing ${sessions.size} sessions: ${names}`);
|
|
2041
2091
|
for (const session of sessions.list()) {
|
|
2042
2092
|
try {
|
|
2043
|
-
if (
|
|
2093
|
+
if (session.isCDP) {
|
|
2094
|
+
if (session.browser) {
|
|
2095
|
+
await session.browser.close().catch(() => {
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
} else {
|
|
2044
2099
|
await session.context.close();
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
}
|
|
2100
|
+
if (session.browser) {
|
|
2101
|
+
await session.browser.close().catch(() => {
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2049
2104
|
}
|
|
2050
2105
|
sessions.removeById(session.id);
|
|
2051
2106
|
} catch {
|
|
@@ -2054,7 +2109,7 @@ async function closeAllSessions() {
|
|
|
2054
2109
|
}
|
|
2055
2110
|
}
|
|
2056
2111
|
async function destroyBrowser() {
|
|
2057
|
-
logSessionEvent("destroy_browser", `Sessions count: ${sessions.size}. Clearing idle timer and closing all
|
|
2112
|
+
logSessionEvent("destroy_browser", `Sessions count: ${sessions.size}. Clearing idle timer and closing all sessions.`);
|
|
2058
2113
|
if (idleTimer) {
|
|
2059
2114
|
clearTimeout(idleTimer);
|
|
2060
2115
|
idleTimer = null;
|
|
@@ -2090,8 +2145,6 @@ async function ensureProcessCanExit() {
|
|
|
2090
2145
|
for (const session of sessions.list()) {
|
|
2091
2146
|
if (session.browser) {
|
|
2092
2147
|
if (session.isCDP) {
|
|
2093
|
-
await session.browser.close().catch(() => {
|
|
2094
|
-
});
|
|
2095
2148
|
} else {
|
|
2096
2149
|
await session.browser.close().catch(() => {
|
|
2097
2150
|
});
|
|
@@ -36,16 +36,15 @@ var ScreencastCapturer = class {
|
|
|
36
36
|
const cdp = await page.context().newCDPSession(page);
|
|
37
37
|
this.cdpSession = cdp;
|
|
38
38
|
cdp.on("Page.screencastFrame", async (params) => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
}
|
|
39
|
+
const cb = this.frameCallback;
|
|
40
|
+
if (!cb) return;
|
|
41
|
+
cdp.send("Page.screencastFrameAck", { sessionId: params.sessionId }).catch(() => {
|
|
42
|
+
});
|
|
44
43
|
const viewport = {
|
|
45
44
|
width: params.metadata?.deviceWidth || this.maxWidth,
|
|
46
45
|
height: params.metadata?.deviceHeight || this.maxHeight
|
|
47
46
|
};
|
|
48
|
-
|
|
47
|
+
cb({
|
|
49
48
|
id: crypto.randomUUID(),
|
|
50
49
|
sessionId: this.sessionId,
|
|
51
50
|
timestamp: Date.now(),
|
|
@@ -116,16 +116,9 @@ function getDaemonProcessStatus() {
|
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
// src/version.ts
|
|
120
|
-
import { createRequire } from "module";
|
|
121
|
-
var require2 = createRequire(import.meta.url);
|
|
122
|
-
var pkg = require2("../package.json");
|
|
123
|
-
var version = pkg.version;
|
|
124
|
-
|
|
125
119
|
export {
|
|
126
120
|
getDaemonConfig,
|
|
127
121
|
startDaemonProcess,
|
|
128
122
|
stopDaemonProcess,
|
|
129
|
-
getDaemonProcessStatus
|
|
130
|
-
version
|
|
123
|
+
getDaemonProcessStatus
|
|
131
124
|
};
|
|
@@ -39,16 +39,15 @@ var ScreencastCapturer = class {
|
|
|
39
39
|
const cdp = await page.context().newCDPSession(page);
|
|
40
40
|
this.cdpSession = cdp;
|
|
41
41
|
cdp.on("Page.screencastFrame", async (params) => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
}
|
|
42
|
+
const cb = this.frameCallback;
|
|
43
|
+
if (!cb) return;
|
|
44
|
+
cdp.send("Page.screencastFrameAck", { sessionId: params.sessionId }).catch(() => {
|
|
45
|
+
});
|
|
47
46
|
const viewport = {
|
|
48
47
|
width: params.metadata?.deviceWidth || this.maxWidth,
|
|
49
48
|
height: params.metadata?.deviceHeight || this.maxHeight
|
|
50
49
|
};
|
|
51
|
-
|
|
50
|
+
cb({
|
|
52
51
|
id: crypto.randomUUID(),
|
|
53
52
|
sessionId: this.sessionId,
|
|
54
53
|
timestamp: Date.now(),
|
|
@@ -98,6 +98,13 @@ async function detectCaptcha(page) {
|
|
|
98
98
|
for (const selector of captchaSelectors) {
|
|
99
99
|
const element = await page.$(selector).catch(() => null);
|
|
100
100
|
if (element) {
|
|
101
|
+
const visible = await element.isVisible().catch(() => false);
|
|
102
|
+
if (!visible) continue;
|
|
103
|
+
try {
|
|
104
|
+
const box = await element.boundingBox();
|
|
105
|
+
if (box && (box.y < -1e3 || box.x < -1e3)) continue;
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
101
108
|
return {
|
|
102
109
|
detected: true,
|
|
103
110
|
type: "captcha",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getCaptchaConfig
|
|
3
3
|
} from "./chunk-IIM5GOD7.js";
|
|
4
|
-
import "./chunk-KFQGP6VL.js";
|
|
5
4
|
|
|
6
5
|
// src/human-interaction.ts
|
|
7
6
|
import { execSync } from "child_process";
|
|
@@ -44,16 +43,15 @@ var ScreencastCapturer = class {
|
|
|
44
43
|
const cdp = await page.context().newCDPSession(page);
|
|
45
44
|
this.cdpSession = cdp;
|
|
46
45
|
cdp.on("Page.screencastFrame", async (params) => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
}
|
|
46
|
+
const cb = this.frameCallback;
|
|
47
|
+
if (!cb) return;
|
|
48
|
+
cdp.send("Page.screencastFrameAck", { sessionId: params.sessionId }).catch(() => {
|
|
49
|
+
});
|
|
52
50
|
const viewport = {
|
|
53
51
|
width: params.metadata?.deviceWidth || this.maxWidth,
|
|
54
52
|
height: params.metadata?.deviceHeight || this.maxHeight
|
|
55
53
|
};
|
|
56
|
-
|
|
54
|
+
cb({
|
|
57
55
|
id: crypto.randomUUID(),
|
|
58
56
|
sessionId: this.sessionId,
|
|
59
57
|
timestamp: Date.now(),
|
|
@@ -429,6 +427,7 @@ var HumanInteractionManager = class {
|
|
|
429
427
|
});
|
|
430
428
|
}
|
|
431
429
|
};
|
|
430
|
+
|
|
432
431
|
export {
|
|
433
432
|
HumanInteractionManager
|
|
434
433
|
};
|
|
@@ -294,6 +294,36 @@ function queryJS(selector) {
|
|
|
294
294
|
const xpath = JSON.stringify(selector.slice(6));
|
|
295
295
|
return `document.evaluate(${xpath}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue`;
|
|
296
296
|
}
|
|
297
|
+
if (selector.startsWith("text=")) {
|
|
298
|
+
const raw = selector.slice(5);
|
|
299
|
+
const exact = raw.startsWith('"') && raw.endsWith('"');
|
|
300
|
+
const text = exact ? raw.slice(1, -1) : raw;
|
|
301
|
+
return `(() => {
|
|
302
|
+
const target = ${JSON.stringify(text)};
|
|
303
|
+
const exact = ${exact};
|
|
304
|
+
const els = [...document.querySelectorAll('*')].filter(e => {
|
|
305
|
+
if (e.children.length > 0) return false;
|
|
306
|
+
if (e.offsetParent === null) return false;
|
|
307
|
+
const t = (e.textContent || '').trim();
|
|
308
|
+
if (!t) return false;
|
|
309
|
+
return exact ? t === target : t.toLowerCase().includes(target.toLowerCase());
|
|
310
|
+
});
|
|
311
|
+
return els[0] || null;
|
|
312
|
+
})()`;
|
|
313
|
+
}
|
|
314
|
+
if (selector.startsWith("popup-text=")) {
|
|
315
|
+
const text = selector.slice("popup-text=".length);
|
|
316
|
+
return `(() => {
|
|
317
|
+
const target = ${JSON.stringify(text)};
|
|
318
|
+
const els = [...document.querySelectorAll('*')].filter(e => {
|
|
319
|
+
if (e.children.length > 0) return false;
|
|
320
|
+
if (e.offsetParent === null) return false;
|
|
321
|
+
if ((e.textContent || '').trim() !== target) return false;
|
|
322
|
+
return true;
|
|
323
|
+
});
|
|
324
|
+
return els[0] || null;
|
|
325
|
+
})()`;
|
|
326
|
+
}
|
|
297
327
|
return `document.querySelector(${JSON.stringify(selector)})`;
|
|
298
328
|
}
|
|
299
329
|
function queryAllJS(selector) {
|
|
@@ -301,6 +331,9 @@ function queryAllJS(selector) {
|
|
|
301
331
|
const xpath = JSON.stringify(selector.slice(6));
|
|
302
332
|
return `(() => { const it = document.evaluate(${xpath}, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); const r=[]; for(let i=0;i<it.snapshotLength;i++) r.push(it.snapshotItem(i)); return r; })()`;
|
|
303
333
|
}
|
|
334
|
+
if (selector.startsWith("text=") || selector.startsWith("popup-text=")) {
|
|
335
|
+
return `(() => { const el = ${queryJS(selector)}; return el ? [el] : []; })()`;
|
|
336
|
+
}
|
|
304
337
|
return `document.querySelectorAll(${JSON.stringify(selector)})`;
|
|
305
338
|
}
|
|
306
339
|
|
|
@@ -2763,11 +2796,52 @@ var CDPConnection = class extends EventEmitter4 {
|
|
|
2763
2796
|
this.setMaxListeners(0);
|
|
2764
2797
|
this.defaultSessionId = sessionId;
|
|
2765
2798
|
if (typeof wsOrUrl === "string") {
|
|
2766
|
-
|
|
2799
|
+
const wsOptions = /^wss:\/\/\d+\.\d+\.\d+\.\d+/.test(wsOrUrl) ? { rejectUnauthorized: false } : void 0;
|
|
2800
|
+
this.ws = new WebSocket(wsOrUrl, wsOptions);
|
|
2767
2801
|
} else {
|
|
2768
2802
|
this.ws = wsOrUrl;
|
|
2769
2803
|
}
|
|
2770
2804
|
this.bindWebSocket();
|
|
2805
|
+
this.startKeepalive();
|
|
2806
|
+
}
|
|
2807
|
+
/** Send periodic WS pings to prevent idle-timeout disconnects (e.g. CF's 100s).
|
|
2808
|
+
* Also detects dead connections: if a pong isn't received within 10s of a
|
|
2809
|
+
* ping, the connection is considered dead and forcibly closed. */
|
|
2810
|
+
keepaliveTimer = null;
|
|
2811
|
+
pongTimer = null;
|
|
2812
|
+
startKeepalive() {
|
|
2813
|
+
this.ws.on("pong", () => {
|
|
2814
|
+
if (this.pongTimer) {
|
|
2815
|
+
clearTimeout(this.pongTimer);
|
|
2816
|
+
this.pongTimer = null;
|
|
2817
|
+
}
|
|
2818
|
+
});
|
|
2819
|
+
this.keepaliveTimer = setInterval(() => {
|
|
2820
|
+
if (this.ws.readyState === WebSocket.OPEN) {
|
|
2821
|
+
if (!this.pongTimer) {
|
|
2822
|
+
this.pongTimer = setTimeout(() => {
|
|
2823
|
+
if (!this.closed) {
|
|
2824
|
+
this.closed = true;
|
|
2825
|
+
this.closeReason = "keepalive timeout (no pong in 10s)";
|
|
2826
|
+
try {
|
|
2827
|
+
this.ws.terminate();
|
|
2828
|
+
} catch {
|
|
2829
|
+
}
|
|
2830
|
+
for (const [, pending] of this.pending) {
|
|
2831
|
+
clearTimeout(pending.timeout);
|
|
2832
|
+
pending.reject(new Error("Connection dead: keepalive timeout"));
|
|
2833
|
+
}
|
|
2834
|
+
this.pending.clear();
|
|
2835
|
+
this.emit("disconnect");
|
|
2836
|
+
}
|
|
2837
|
+
}, 1e4);
|
|
2838
|
+
}
|
|
2839
|
+
this.ws.ping?.();
|
|
2840
|
+
} else if (this.closed) {
|
|
2841
|
+
if (this.keepaliveTimer) clearInterval(this.keepaliveTimer);
|
|
2842
|
+
this.keepaliveTimer = null;
|
|
2843
|
+
}
|
|
2844
|
+
}, 3e4);
|
|
2771
2845
|
}
|
|
2772
2846
|
/** Wait for the connection to be fully open */
|
|
2773
2847
|
async ready() {
|
|
@@ -2876,6 +2950,14 @@ var CDPConnection = class extends EventEmitter4 {
|
|
|
2876
2950
|
if (this.closed) return;
|
|
2877
2951
|
this.closed = true;
|
|
2878
2952
|
this.closeReason = "closed by caller";
|
|
2953
|
+
if (this.keepaliveTimer) {
|
|
2954
|
+
clearInterval(this.keepaliveTimer);
|
|
2955
|
+
this.keepaliveTimer = null;
|
|
2956
|
+
}
|
|
2957
|
+
if (this.pongTimer) {
|
|
2958
|
+
clearTimeout(this.pongTimer);
|
|
2959
|
+
this.pongTimer = null;
|
|
2960
|
+
}
|
|
2879
2961
|
for (const [id, pending] of this.pending) {
|
|
2880
2962
|
clearTimeout(pending.timeout);
|
|
2881
2963
|
pending.reject(new Error(`Connection closed: ${pending.method}`));
|
|
@@ -2917,6 +2999,10 @@ var CDPConnection = class extends EventEmitter4 {
|
|
|
2917
2999
|
if (this.closed) return;
|
|
2918
3000
|
this.closed = true;
|
|
2919
3001
|
this.closeReason = `WebSocket closed: ${code} ${reason?.toString() ?? ""}`.trim();
|
|
3002
|
+
if (this.keepaliveTimer) {
|
|
3003
|
+
clearInterval(this.keepaliveTimer);
|
|
3004
|
+
this.keepaliveTimer = null;
|
|
3005
|
+
}
|
|
2920
3006
|
for (const [id, pending] of this.pending) {
|
|
2921
3007
|
clearTimeout(pending.timeout);
|
|
2922
3008
|
pending.reject(new Error(`Connection closed: ${pending.method}`));
|
|
@@ -4458,7 +4544,7 @@ process.on("exit", () => {
|
|
|
4458
4544
|
if (session.isCDP) {
|
|
4459
4545
|
logSessionEvent("process_exit", `Session "${session.name}": CDP connection (not closing external browser).`);
|
|
4460
4546
|
} else {
|
|
4461
|
-
logSessionEvent("process_exit", `Session "${session.name}": Closing browser.`);
|
|
4547
|
+
logSessionEvent("process_exit", `Session "${session.name}": Closing self-launched browser.`);
|
|
4462
4548
|
try {
|
|
4463
4549
|
session.browser?.close();
|
|
4464
4550
|
} catch {
|
|
@@ -4466,7 +4552,7 @@ process.on("exit", () => {
|
|
|
4466
4552
|
}
|
|
4467
4553
|
}
|
|
4468
4554
|
if (_sharedBrowser) {
|
|
4469
|
-
logSessionEvent("process_exit", "Closing shared browser.");
|
|
4555
|
+
logSessionEvent("process_exit", "Closing shared browser (self-launched only, external CDP is safe).");
|
|
4470
4556
|
try {
|
|
4471
4557
|
_sharedBrowser.close();
|
|
4472
4558
|
} catch {
|
|
@@ -4596,9 +4682,29 @@ function deleteSessionDiskMeta(name) {
|
|
|
4596
4682
|
} catch {
|
|
4597
4683
|
}
|
|
4598
4684
|
}
|
|
4685
|
+
async function isSessionPageAlive(session) {
|
|
4686
|
+
const page = session.page;
|
|
4687
|
+
if (!page || typeof page.evaluate !== "function") return false;
|
|
4688
|
+
try {
|
|
4689
|
+
await Promise.race([
|
|
4690
|
+
page.evaluate("1"),
|
|
4691
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("liveness probe timeout")), 1500))
|
|
4692
|
+
]);
|
|
4693
|
+
return true;
|
|
4694
|
+
} catch {
|
|
4695
|
+
return false;
|
|
4696
|
+
}
|
|
4697
|
+
}
|
|
4599
4698
|
async function findOrRestoreSession(name, cdpEndpoint) {
|
|
4600
4699
|
const inMem = findSession(name);
|
|
4601
|
-
if (inMem)
|
|
4700
|
+
if (inMem) {
|
|
4701
|
+
if (await isSessionPageAlive(inMem)) return inMem;
|
|
4702
|
+
logSessionEvent("stale_session", `name="${name}" \u2014 page \u63A2\u9488\u5931\u8D25\uFF08\u6D4F\u89C8\u5668\u53EF\u80FD\u5DF2\u91CD\u542F\uFF09\uFF0C\u5C31\u5730\u91CD\u5EFA`);
|
|
4703
|
+
try {
|
|
4704
|
+
await closeSessionByName(name);
|
|
4705
|
+
} catch {
|
|
4706
|
+
}
|
|
4707
|
+
}
|
|
4602
4708
|
const meta = readSessionDiskMeta(name);
|
|
4603
4709
|
if (!meta) return void 0;
|
|
4604
4710
|
const ep = cdpEndpoint || meta.cdpEndpoint;
|
|
@@ -4861,14 +4967,6 @@ async function createSession(name, url, options) {
|
|
|
4861
4967
|
let context;
|
|
4862
4968
|
let page;
|
|
4863
4969
|
if (isCDP) {
|
|
4864
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
4865
|
-
let contexts = b.contexts();
|
|
4866
|
-
if (contexts.length === 0) {
|
|
4867
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
4868
|
-
contexts = b.contexts();
|
|
4869
|
-
}
|
|
4870
|
-
context = contexts[0] || await b.newContext();
|
|
4871
|
-
let targetPage = null;
|
|
4872
4970
|
const targetHostname = url ? (() => {
|
|
4873
4971
|
try {
|
|
4874
4972
|
return new URL(url).hostname;
|
|
@@ -4876,12 +4974,54 @@ async function createSession(name, url, options) {
|
|
|
4876
4974
|
return "";
|
|
4877
4975
|
}
|
|
4878
4976
|
})() : "";
|
|
4977
|
+
const isRealPageUrl = (u) => !!u && u !== "about:blank" && !u.startsWith("chrome://") && !u.startsWith("chrome-untrusted://") && !u.startsWith("chrome-error://");
|
|
4978
|
+
const resolvePageUrl = async (p) => {
|
|
4979
|
+
const u = p.url();
|
|
4980
|
+
if (u && u !== "about:blank") return u;
|
|
4981
|
+
try {
|
|
4982
|
+
return await p.evaluate("location.href");
|
|
4983
|
+
} catch {
|
|
4984
|
+
return u ?? "";
|
|
4985
|
+
}
|
|
4986
|
+
};
|
|
4987
|
+
const pollStart = Date.now();
|
|
4988
|
+
for (; ; ) {
|
|
4989
|
+
const ctxs = b.contexts();
|
|
4990
|
+
let hostHit = false;
|
|
4991
|
+
let anyHit = false;
|
|
4992
|
+
for (const ctx of ctxs) {
|
|
4993
|
+
for (const p of ctx.pages()) {
|
|
4994
|
+
const u = await resolvePageUrl(p);
|
|
4995
|
+
if (!isRealPageUrl(u)) continue;
|
|
4996
|
+
anyHit = true;
|
|
4997
|
+
if (targetHostname && u.includes(targetHostname)) {
|
|
4998
|
+
hostHit = true;
|
|
4999
|
+
break;
|
|
5000
|
+
}
|
|
5001
|
+
}
|
|
5002
|
+
if (hostHit) break;
|
|
5003
|
+
}
|
|
5004
|
+
if (process.env.XB_DEBUG_SESSION_POLL) {
|
|
5005
|
+
console.error(`[poll] t=${Date.now() - pollStart}ms ctxs=${ctxs.length} hostHit=${hostHit} anyHit=${anyHit}`);
|
|
5006
|
+
}
|
|
5007
|
+
if (hostHit) break;
|
|
5008
|
+
if (anyHit && Date.now() - pollStart >= 2e3) break;
|
|
5009
|
+
if (Date.now() - pollStart >= 5e3) break;
|
|
5010
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
5011
|
+
}
|
|
5012
|
+
let contexts = b.contexts();
|
|
5013
|
+
if (contexts.length === 0) {
|
|
5014
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
5015
|
+
contexts = b.contexts();
|
|
5016
|
+
}
|
|
5017
|
+
context = contexts[0] || await b.newContext();
|
|
5018
|
+
let targetPage = null;
|
|
4879
5019
|
if (targetHostname) {
|
|
4880
5020
|
for (const ctx of contexts) {
|
|
4881
5021
|
const pages = ctx.pages();
|
|
4882
5022
|
for (const p of pages) {
|
|
4883
|
-
const pUrl = p
|
|
4884
|
-
if (
|
|
5023
|
+
const pUrl = await resolvePageUrl(p);
|
|
5024
|
+
if (isRealPageUrl(pUrl) && pUrl.includes(targetHostname)) {
|
|
4885
5025
|
targetPage = p;
|
|
4886
5026
|
break;
|
|
4887
5027
|
}
|
|
@@ -4893,8 +5033,8 @@ async function createSession(name, url, options) {
|
|
|
4893
5033
|
for (const ctx of contexts) {
|
|
4894
5034
|
const pages = ctx.pages();
|
|
4895
5035
|
for (const p of pages) {
|
|
4896
|
-
const pUrl = p
|
|
4897
|
-
if (
|
|
5036
|
+
const pUrl = await resolvePageUrl(p);
|
|
5037
|
+
if (isRealPageUrl(pUrl)) {
|
|
4898
5038
|
targetPage = p;
|
|
4899
5039
|
break;
|
|
4900
5040
|
}
|
|
@@ -4952,10 +5092,6 @@ async function closeSessionByName(name) {
|
|
|
4952
5092
|
if (session.name === name || session.id === name) {
|
|
4953
5093
|
logSessionEvent("close_session", `name="${session.name}" id="${session.id}" url="${session.page.url()}"`);
|
|
4954
5094
|
if (session.isCDP) {
|
|
4955
|
-
try {
|
|
4956
|
-
await session.page.close();
|
|
4957
|
-
} catch {
|
|
4958
|
-
}
|
|
4959
5095
|
if (session.browser) {
|
|
4960
5096
|
await session.browser.close().catch(() => {
|
|
4961
5097
|
});
|
|
@@ -4980,7 +5116,7 @@ async function closeSessionByName(name) {
|
|
|
4980
5116
|
} catch {
|
|
4981
5117
|
}
|
|
4982
5118
|
try {
|
|
4983
|
-
const { SessionRecorder } = await import("./session-recorder-
|
|
5119
|
+
const { SessionRecorder } = await import("./session-recorder-QRZMKFVL.js");
|
|
4984
5120
|
SessionRecorder.cleanup(session.name);
|
|
4985
5121
|
} catch {
|
|
4986
5122
|
}
|
|
@@ -4999,12 +5135,17 @@ async function closeAllSessions() {
|
|
|
4999
5135
|
if (names) logSessionEvent("close_all_sessions", `Closing ${sessions.size} sessions: ${names}`);
|
|
5000
5136
|
for (const session of sessions.list()) {
|
|
5001
5137
|
try {
|
|
5002
|
-
if (
|
|
5138
|
+
if (session.isCDP) {
|
|
5139
|
+
if (session.browser) {
|
|
5140
|
+
await session.browser.close().catch(() => {
|
|
5141
|
+
});
|
|
5142
|
+
}
|
|
5143
|
+
} else {
|
|
5003
5144
|
await session.context.close();
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
}
|
|
5145
|
+
if (session.browser) {
|
|
5146
|
+
await session.browser.close().catch(() => {
|
|
5147
|
+
});
|
|
5148
|
+
}
|
|
5008
5149
|
}
|
|
5009
5150
|
sessions.removeById(session.id);
|
|
5010
5151
|
} catch {
|
|
@@ -5013,7 +5154,7 @@ async function closeAllSessions() {
|
|
|
5013
5154
|
}
|
|
5014
5155
|
}
|
|
5015
5156
|
async function destroyBrowser() {
|
|
5016
|
-
logSessionEvent("destroy_browser", `Sessions count: ${sessions.size}. Clearing idle timer and closing all
|
|
5157
|
+
logSessionEvent("destroy_browser", `Sessions count: ${sessions.size}. Clearing idle timer and closing all sessions.`);
|
|
5017
5158
|
if (idleTimer) {
|
|
5018
5159
|
clearTimeout(idleTimer);
|
|
5019
5160
|
idleTimer = null;
|
|
@@ -5049,8 +5190,6 @@ async function ensureProcessCanExit() {
|
|
|
5049
5190
|
for (const session of sessions.list()) {
|
|
5050
5191
|
if (session.browser) {
|
|
5051
5192
|
if (session.isCDP) {
|
|
5052
|
-
await session.browser.close().catch(() => {
|
|
5053
|
-
});
|
|
5054
5193
|
} else {
|
|
5055
5194
|
await session.browser.close().catch(() => {
|
|
5056
5195
|
});
|