@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.
Files changed (39) hide show
  1. package/dist/{anti-bot-MCVM6IFB.js → anti-bot-DR56Y63V.js} +1 -1
  2. package/dist/{browser-SJWPJFN7.js → browser-HBL72GPZ.js} +2 -2
  3. package/dist/{browser-GT4BMX6X.js → browser-T3V3JWVH.js} +1 -1
  4. package/dist/{browser-43YXUIML.js → browser-WQZ3D6AE.js} +2 -2
  5. package/dist/{cdp-driver-WAFYSIIW.js → cdp-driver-2T4P4ZE2.js} +87 -1
  6. package/dist/{cdp-driver-PUZLDSQU.js → cdp-driver-J3YQ4LJV.js} +1 -1
  7. package/dist/chunk-3OD76SUE.js +20 -0
  8. package/dist/chunk-43TEMG4U.js +9 -0
  9. package/dist/{chunk-C3LIGO4V.js → chunk-4452SPFI.js} +906 -16
  10. package/dist/{chunk-VEWTHYDG.js → chunk-5UGR6MUK.js} +906 -16
  11. package/dist/{chunk-ENCEMM7Z.js → chunk-BAHSRZIX.js} +82 -29
  12. package/dist/{chunk-7ZDWM6T2.js → chunk-BNO7OKO4.js} +5 -6
  13. package/dist/{chunk-24SY6PE5.js → chunk-CG3D3CHY.js} +1 -8
  14. package/dist/{chunk-ZJHQPMCY.js → chunk-DWDXEGVK.js} +5 -6
  15. package/dist/{chunk-DPJNCMLC.js → chunk-INTQPBYF.js} +7 -0
  16. package/dist/{human-interaction-OU7LZ6OZ.js → chunk-MWFHZUIY.js} +6 -7
  17. package/dist/{chunk-A4YHJW3Z.js → chunk-NITFVWWS.js} +168 -29
  18. package/dist/chunk-Q2DFJQGS.js +128 -0
  19. package/dist/{chunk-GM3HWQ5U.js → chunk-RRBXV7KE.js} +87 -1
  20. package/dist/{human-interaction-HJTXFVQS.js → chunk-SQHSZENE.js} +2 -2
  21. package/dist/chunk-TZPKFUBT.js +20 -0
  22. package/dist/{chunk-PBDID7SE.js → chunk-WSCP7QCJ.js} +82 -29
  23. package/dist/{chunk-YAUG3NSI.js → chunk-YMUSHPU4.js} +16 -128
  24. package/dist/cli.js +48 -31
  25. package/dist/{daemon-client-4HJENICO.js → daemon-client-GKEPT4NY.js} +15 -3
  26. package/dist/{daemon-client-CMRAWXTN.js → daemon-client-O6BYVRXV.js} +4 -1
  27. package/dist/daemon-main.js +649 -58
  28. package/dist/human-interaction-5AO42MBA.js +8 -0
  29. package/dist/{human-interaction-2DZK4MW7.js → human-interaction-C5OEGXGO.js} +1 -1
  30. package/dist/human-interaction-ISXZTAYY.js +8 -0
  31. package/dist/index.d.ts +167 -2
  32. package/dist/index.js +194 -67
  33. package/dist/recovery-EF33LKRJ.js +77 -0
  34. package/dist/recovery-J2ISVGUL.js +77 -0
  35. package/dist/recovery-ZJVVHP7N.js +76 -0
  36. package/dist/{session-recorder-K3K63Q33.js → session-recorder-H3KEYU26.js} +1 -1
  37. package/dist/{session-recorder-AG6CH6EI.js → session-recorder-QRZMKFVL.js} +1 -1
  38. package/dist/{session-replayer-V4MP6M6I.js → session-replayer-LJUC4TI7.js} +24 -7
  39. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  launch
3
- } from "./chunk-GM3HWQ5U.js";
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) return 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.url();
1925
- if (pUrl && pUrl !== "about:blank" && !pUrl.startsWith("chrome://") && !pUrl.startsWith("chrome-untrusted://") && !pUrl.startsWith("chrome-error://") && pUrl.includes(targetHostname)) {
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.url();
1938
- if (pUrl && pUrl !== "about:blank" && !pUrl.startsWith("chrome://") && !pUrl.startsWith("chrome-untrusted://") && !pUrl.startsWith("chrome-error://")) {
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-K3K63Q33.js");
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 (!session.isCDP) {
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
- if (session.browser) {
2047
- await session.browser.close().catch(() => {
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 browsers.`);
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
- if (!this.frameCallback) return;
40
- try {
41
- await cdp.send("Page.screencastFrameAck", { sessionId: params.sessionId });
42
- } catch {
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
- this.frameCallback({
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
- if (!this.frameCallback) return;
43
- try {
44
- await cdp.send("Page.screencastFrameAck", { sessionId: params.sessionId });
45
- } catch {
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
- this.frameCallback({
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
- if (!this.frameCallback) return;
48
- try {
49
- await cdp.send("Page.screencastFrameAck", { sessionId: params.sessionId });
50
- } catch {
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
- this.frameCallback({
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
- this.ws = new WebSocket(wsOrUrl);
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) return 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.url();
4884
- if (pUrl && pUrl !== "about:blank" && !pUrl.startsWith("chrome://") && !pUrl.startsWith("chrome-untrusted://") && !pUrl.startsWith("chrome-error://") && pUrl.includes(targetHostname)) {
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.url();
4897
- if (pUrl && pUrl !== "about:blank" && !pUrl.startsWith("chrome://") && !pUrl.startsWith("chrome-untrusted://") && !pUrl.startsWith("chrome-error://")) {
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-AG6CH6EI.js");
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 (!session.isCDP) {
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
- if (session.browser) {
5006
- await session.browser.close().catch(() => {
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 browsers.`);
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
  });