@xbrowser/cli 1.9.9 → 1.11.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 (27) hide show
  1. package/dist/{anti-bot-DR56Y63V.js → anti-bot-GTTYNEFB.js} +1 -1
  2. package/dist/{browser-FFYPFTBV.js → browser-Q5APBNF6.js} +1 -1
  3. package/dist/{browser-YCO4GJMZ.js → browser-V3JIWSTR.js} +2 -2
  4. package/dist/{browser-RHWUAMTB.js → browser-XNU7JQYC.js} +3 -2
  5. package/dist/{cdp-driver-J3YQ4LJV.js → cdp-driver-GD6YBBGE.js} +1 -1
  6. package/dist/cdp-driver-VEK6BNN6.js +49 -0
  7. package/dist/{cdp-driver-2T4P4ZE2.js → cdp-driver-XFTTZI5O.js} +436 -56
  8. package/dist/chunk-3FWLW7FS.js +106 -0
  9. package/dist/chunk-7OFM755Z.js +3509 -0
  10. package/dist/{chunk-5UGR6MUK.js → chunk-HBHOKZPN.js} +51 -16
  11. package/dist/{chunk-6OZWKXSW.js → chunk-HVPUVVSA.js} +72 -15
  12. package/dist/{chunk-GUTBIYFW.js → chunk-JEDP4PJW.js} +507 -70
  13. package/dist/{chunk-INTQPBYF.js → chunk-JKVUFP3G.js} +6 -2
  14. package/dist/{chunk-4452SPFI.js → chunk-NODRQGOK.js} +51 -16
  15. package/dist/{chunk-YMUSHPU4.js → chunk-WHPBNUKB.js} +35 -1
  16. package/dist/{chunk-RZM5CNIY.js → chunk-XKQVUFUS.js} +72 -15
  17. package/dist/{chunk-RRBXV7KE.js → chunk-XQ4HRPDJ.js} +384 -102
  18. package/dist/cli.js +79 -29
  19. package/dist/{daemon-client-GKEPT4NY.js → daemon-client-MMDRYCF5.js} +35 -1
  20. package/dist/{daemon-client-O6BYVRXV.js → daemon-client-Y2YSZCGK.js} +1 -1
  21. package/dist/daemon-main.js +103 -32
  22. package/dist/index.d.ts +8 -0
  23. package/dist/index.js +94 -31
  24. package/dist/{session-recorder-H3KEYU26.js → session-recorder-3BEVWHOK.js} +1 -1
  25. package/dist/{session-recorder-QRZMKFVL.js → session-recorder-SLDBENVF.js} +1 -1
  26. package/dist/{session-replayer-LJUC4TI7.js → session-replayer-WIUYVN5J.js} +90 -2
  27. package/package.json +1 -1
@@ -13,7 +13,11 @@ var WARNING_TEXTS = [
13
13
  { text: "unusual traffic", severity: "high" },
14
14
  { text: "please verify you are human", severity: "medium" },
15
15
  { text: "access denied", severity: "high" },
16
- { text: "blocked", severity: "high" },
16
+ // 裸词 "blocked" 会误伤大量正常页面("unblocked"、adblock 检测脚本文本、
17
+ // CSS-in-JS 字符串等,实测 doubao.com),只匹配完整阻断短语
18
+ { text: "you have been blocked", severity: "high" },
19
+ { text: "your access has been blocked", severity: "high" },
20
+ { text: "access blocked", severity: "high" },
17
21
  { text: "rate limit", severity: "medium" },
18
22
  { text: "too many requests", severity: "medium" },
19
23
  { text: "\u9A8C\u8BC1", severity: "low" },
@@ -122,7 +126,7 @@ async function detectCaptcha(page) {
122
126
  }
123
127
  async function detectWarningText(page) {
124
128
  try {
125
- const pageText = await page.textContent("body").catch(() => "") || "";
129
+ const pageText = await page.evaluate(() => document.body?.innerText || "").catch(() => "") || "";
126
130
  const lowerText = pageText.toLowerCase();
127
131
  for (const { text, severity } of WARNING_TEXTS) {
128
132
  if (lowerText.includes(text.toLowerCase())) {
@@ -1093,6 +1093,33 @@ var ACTION_SIGNAL_SCRIPT = `
1093
1093
  }
1094
1094
  }, true);
1095
1095
 
1096
+ // \u2500\u2500 Pointer-based drag (modern sliders, sortable lists) \u2500\u2500
1097
+ // HTML5 dragstart/drop never fires for pointer-driven drags \u2014 capture via
1098
+ // pointerdown/pointerup with a movement threshold instead (rec-duel d08).
1099
+ var __xb_ptr_down = null;
1100
+ document.addEventListener('pointerdown', function(e) {
1101
+ var t = actualTarget(e);
1102
+ __xb_ptr_down = { x: e.clientX, y: e.clientY, element: describe(t), ts: Date.now() };
1103
+ }, true);
1104
+ document.addEventListener('pointerup', function(e) {
1105
+ if (!__xb_ptr_down) return;
1106
+ var start = __xb_ptr_down;
1107
+ __xb_ptr_down = null;
1108
+ var dx = e.clientX - start.x, dy = e.clientY - start.y;
1109
+ if (Math.abs(dx) < 15 && Math.abs(dy) < 15) return; // tap/click, not a drag
1110
+ pushAction('drag', {
1111
+ x: e.clientX,
1112
+ y: e.clientY,
1113
+ element: start.element,
1114
+ drag: {
1115
+ fromX: start.x,
1116
+ fromY: start.y,
1117
+ toX: e.clientX,
1118
+ toY: e.clientY,
1119
+ },
1120
+ });
1121
+ }, true);
1122
+
1096
1123
  // \u2500\u2500 Drag & drop \u2500\u2500
1097
1124
  var __xb_drag_source = null;
1098
1125
  var __xb_drag_start_pos = null;
@@ -1985,8 +2012,7 @@ var SessionRecorder = class _SessionRecorder {
1985
2012
  "resize",
1986
2013
  "clipboard",
1987
2014
  "touch",
1988
- "contextmenu",
1989
- "drag"
2015
+ "contextmenu"
1990
2016
  ]);
1991
2017
  constructor(context, page, sessionName) {
1992
2018
  this.context = context;
@@ -2003,8 +2029,8 @@ var SessionRecorder = class _SessionRecorder {
2003
2029
  async recordCommandAction(action) {
2004
2030
  const dedupFromAction = this.dedupKey(action.type, action.selector, action.element?.tag, action.value);
2005
2031
  if (dedupFromAction) {
2006
- const expires = this.dedupMap.get(dedupFromAction);
2007
- if (expires && Date.now() < expires) {
2032
+ const entry = this.dedupMap.get(dedupFromAction);
2033
+ if (entry && entry.origin === "signal" && Date.now() < entry.expires) {
2008
2034
  return;
2009
2035
  }
2010
2036
  }
@@ -2021,19 +2047,25 @@ var SessionRecorder = class _SessionRecorder {
2021
2047
  value: action.value
2022
2048
  };
2023
2049
  this.actions.push(actionToPush);
2024
- actionToPush.elementScreenshot = await this.captureElementScreenshot(this.page, {
2025
- type: action.type,
2026
- element: actionToPush.element
2027
- });
2028
- await this.captureAutoSnapshot(this.page, actionToPush, "after").catch(() => {
2029
- });
2050
+ actionToPush.elementScreenshot = await Promise.race([
2051
+ this.captureElementScreenshot(this.page, {
2052
+ type: action.type,
2053
+ element: actionToPush.element
2054
+ }).catch(() => void 0),
2055
+ new Promise((r) => setTimeout(r, 3e3))
2056
+ ]);
2057
+ await Promise.race([
2058
+ this.captureAutoSnapshot(this.page, actionToPush, "after").catch(() => {
2059
+ }),
2060
+ new Promise((r) => setTimeout(r, 6e3))
2061
+ ]);
2030
2062
  this.lastActionTs = ts;
2031
- this.dedupMap.set(dedupFromAction, Date.now() + 2e3);
2063
+ this.dedupMap.set(dedupFromAction, { expires: Date.now() + 4e3, origin: "command" });
2032
2064
  this.dedupActionCount++;
2033
2065
  if (this.dedupActionCount % 200 === 0) {
2034
2066
  const now = Date.now();
2035
2067
  for (const [k, v] of this.dedupMap) {
2036
- if (now >= v) this.dedupMap.delete(k);
2068
+ if (now >= v.expires) this.dedupMap.delete(k);
2037
2069
  }
2038
2070
  }
2039
2071
  if (action.url && action.url !== "about:blank") {
@@ -2746,8 +2778,8 @@ var SessionRecorder = class _SessionRecorder {
2746
2778
  }
2747
2779
  const dedupFromSignal = this.dedupKey(raw.type, raw.element?.selector, raw.element?.tag, raw.value);
2748
2780
  if (dedupFromSignal) {
2749
- const expires = this.dedupMap.get(dedupFromSignal);
2750
- if (expires && Date.now() < expires) {
2781
+ const entry = this.dedupMap.get(dedupFromSignal);
2782
+ if (entry && entry.origin === "command" && Date.now() < entry.expires) {
2751
2783
  continue;
2752
2784
  }
2753
2785
  }
@@ -2790,7 +2822,10 @@ var SessionRecorder = class _SessionRecorder {
2790
2822
  // run before the last sample lands — passthrough keeps whatever has
2791
2823
  // been collected so far.
2792
2824
  hoverContext: raw.hoverContext,
2793
- files: raw.files
2825
+ files: raw.files,
2826
+ // Pointer-drag payload (fromX/fromY/toX/toY) — replayer replays the
2827
+ // drag from these coordinates; dropping it made drags no-ops (d08)
2828
+ drag: raw.drag
2794
2829
  });
2795
2830
  const pushed = this.actions[this.actions.length - 1];
2796
2831
  pushed.elementScreenshot = await this.captureElementScreenshot(page, raw);
@@ -2799,7 +2834,7 @@ var SessionRecorder = class _SessionRecorder {
2799
2834
  this.lastActionTs = raw.ts;
2800
2835
  const dedupKey = this.dedupKey(raw.type, raw.element?.selector, raw.element?.tag, raw.value);
2801
2836
  if (dedupKey) {
2802
- this.dedupMap.set(dedupKey, Date.now() + 2e3);
2837
+ this.dedupMap.set(dedupKey, { expires: Date.now() + 4e3, origin: "signal" });
2803
2838
  this.dedupActionCount++;
2804
2839
  }
2805
2840
  const verifyTypes = /* @__PURE__ */ new Set(["click", "submit", "input", "change", "cdp-click", "cdp-fill"]);
@@ -13,6 +13,7 @@ var pkg = require2("../package.json");
13
13
  var version = pkg.version;
14
14
 
15
15
  // src/client/daemon-client.ts
16
+ import { request as httpRequest } from "http";
16
17
  var DAEMON_PORT = 9224;
17
18
  var DAEMON_BASE = `http://localhost:${DAEMON_PORT}`;
18
19
  var _ensurePromise = null;
@@ -207,7 +208,40 @@ async function forwardRecordSummary(session) {
207
208
  return rpcCall("record:summary", { session }, 5e3);
208
209
  }
209
210
  async function forwardReplay(file, session, slowMo) {
210
- return rpcCall("replay", { file, session, slowMo }, 12e4);
211
+ return longRpcCall("replay", { file, session, slowMo }, 20 * 6e4);
212
+ }
213
+ function longRpcCall(method, params, timeoutMs) {
214
+ return new Promise((resolve, reject) => {
215
+ const body = JSON.stringify({ method, params });
216
+ const req = httpRequest(
217
+ {
218
+ hostname: "127.0.0.1",
219
+ port: DAEMON_PORT,
220
+ path: "/rpc",
221
+ method: "POST",
222
+ headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) }
223
+ },
224
+ (res) => {
225
+ const chunks = [];
226
+ res.on("data", (c) => chunks.push(c));
227
+ res.on("end", () => {
228
+ try {
229
+ const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
230
+ if (parsed && typeof parsed.error === "string" && Object.keys(parsed).length === 1) {
231
+ reject(new Error(`Daemon error: ${parsed.error}`));
232
+ } else {
233
+ resolve(parsed);
234
+ }
235
+ } catch (e) {
236
+ reject(e instanceof Error ? e : new Error(String(e)));
237
+ }
238
+ });
239
+ }
240
+ );
241
+ req.setTimeout(timeoutMs, () => req.destroy(new Error(`replay RPC timeout after ${timeoutMs}ms`)));
242
+ req.on("error", reject);
243
+ req.end(body);
244
+ });
211
245
  }
212
246
  async function forwardRecordCheckpoint(session, type, hint, selector) {
213
247
  return rpcCall("record:checkpoint", { session, type, hint, selector }, 1e4);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  launch
3
- } from "./chunk-RRBXV7KE.js";
3
+ } from "./chunk-7OFM755Z.js";
4
4
  import {
5
5
  errMsg
6
6
  } from "./chunk-GDKLH7ZY.js";
@@ -272,9 +272,32 @@ function deleteSessionDiskMeta(name) {
272
272
  } catch {
273
273
  }
274
274
  }
275
+ async function isSessionPageAlive(session) {
276
+ const page = session.page;
277
+ if (!page || typeof page.evaluate !== "function") return false;
278
+ for (let attempt = 0; attempt < 3; attempt++) {
279
+ try {
280
+ await Promise.race([
281
+ page.evaluate("1"),
282
+ new Promise((_, reject) => setTimeout(() => reject(new Error("liveness probe timeout")), 1500))
283
+ ]);
284
+ return true;
285
+ } catch {
286
+ await new Promise((r) => setTimeout(r, 400));
287
+ }
288
+ }
289
+ return false;
290
+ }
275
291
  async function findOrRestoreSession(name, cdpEndpoint) {
276
292
  const inMem = findSession(name);
277
- if (inMem) return inMem;
293
+ if (inMem) {
294
+ if (await isSessionPageAlive(inMem)) return inMem;
295
+ 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`);
296
+ try {
297
+ await closeSessionByName(name);
298
+ } catch {
299
+ }
300
+ }
278
301
  const meta = readSessionDiskMeta(name);
279
302
  if (!meta) return void 0;
280
303
  const ep = cdpEndpoint || meta.cdpEndpoint;
@@ -537,14 +560,6 @@ async function createSession(name, url, options) {
537
560
  let context;
538
561
  let page;
539
562
  if (isCDP) {
540
- await new Promise((r) => setTimeout(r, 500));
541
- let contexts = b.contexts();
542
- if (contexts.length === 0) {
543
- await new Promise((r) => setTimeout(r, 500));
544
- contexts = b.contexts();
545
- }
546
- context = contexts[0] || await b.newContext();
547
- let targetPage = null;
548
563
  const targetHostname = url ? (() => {
549
564
  try {
550
565
  return new URL(url).hostname;
@@ -552,12 +567,54 @@ async function createSession(name, url, options) {
552
567
  return "";
553
568
  }
554
569
  })() : "";
570
+ const isRealPageUrl = (u) => !!u && u !== "about:blank" && !u.startsWith("chrome://") && !u.startsWith("chrome-untrusted://") && !u.startsWith("chrome-error://");
571
+ const resolvePageUrl = async (p) => {
572
+ const u = p.url();
573
+ if (u && u !== "about:blank") return u;
574
+ try {
575
+ return await p.evaluate("location.href");
576
+ } catch {
577
+ return u ?? "";
578
+ }
579
+ };
580
+ const pollStart = Date.now();
581
+ for (; ; ) {
582
+ const ctxs = b.contexts();
583
+ let hostHit = false;
584
+ let anyHit = false;
585
+ for (const ctx of ctxs) {
586
+ for (const p of ctx.pages()) {
587
+ const u = await resolvePageUrl(p);
588
+ if (!isRealPageUrl(u)) continue;
589
+ anyHit = true;
590
+ if (targetHostname && u.includes(targetHostname)) {
591
+ hostHit = true;
592
+ break;
593
+ }
594
+ }
595
+ if (hostHit) break;
596
+ }
597
+ if (process.env.XB_DEBUG_SESSION_POLL) {
598
+ console.error(`[poll] t=${Date.now() - pollStart}ms ctxs=${ctxs.length} hostHit=${hostHit} anyHit=${anyHit}`);
599
+ }
600
+ if (hostHit) break;
601
+ if (anyHit && Date.now() - pollStart >= 2e3) break;
602
+ if (Date.now() - pollStart >= 5e3) break;
603
+ await new Promise((r) => setTimeout(r, 200));
604
+ }
605
+ let contexts = b.contexts();
606
+ if (contexts.length === 0) {
607
+ await new Promise((r) => setTimeout(r, 500));
608
+ contexts = b.contexts();
609
+ }
610
+ context = contexts[0] || await b.newContext();
611
+ let targetPage = null;
555
612
  if (targetHostname) {
556
613
  for (const ctx of contexts) {
557
614
  const pages = ctx.pages();
558
615
  for (const p of pages) {
559
- const pUrl = p.url();
560
- if (pUrl && pUrl !== "about:blank" && !pUrl.startsWith("chrome://") && !pUrl.startsWith("chrome-untrusted://") && !pUrl.startsWith("chrome-error://") && pUrl.includes(targetHostname)) {
616
+ const pUrl = await resolvePageUrl(p);
617
+ if (isRealPageUrl(pUrl) && pUrl.includes(targetHostname)) {
561
618
  targetPage = p;
562
619
  break;
563
620
  }
@@ -569,8 +626,8 @@ async function createSession(name, url, options) {
569
626
  for (const ctx of contexts) {
570
627
  const pages = ctx.pages();
571
628
  for (const p of pages) {
572
- const pUrl = p.url();
573
- if (pUrl && pUrl !== "about:blank" && !pUrl.startsWith("chrome://") && !pUrl.startsWith("chrome-untrusted://") && !pUrl.startsWith("chrome-error://")) {
629
+ const pUrl = await resolvePageUrl(p);
630
+ if (isRealPageUrl(pUrl)) {
574
631
  targetPage = p;
575
632
  break;
576
633
  }
@@ -652,7 +709,7 @@ async function closeSessionByName(name) {
652
709
  } catch {
653
710
  }
654
711
  try {
655
- const { SessionRecorder } = await import("./session-recorder-QRZMKFVL.js");
712
+ const { SessionRecorder } = await import("./session-recorder-SLDBENVF.js");
656
713
  SessionRecorder.cleanup(session.name);
657
714
  } catch {
658
715
  }