@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
package/dist/cli.js CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  forwardSessionList,
23
23
  forwardViewerCheckSelector,
24
24
  version
25
- } from "./chunk-YMUSHPU4.js";
25
+ } from "./chunk-WHPBNUKB.js";
26
26
  import {
27
27
  buildViewerUrl
28
28
  } from "./chunk-3OD76SUE.js";
@@ -35,7 +35,7 @@ import {
35
35
  } from "./chunk-Q2DFJQGS.js";
36
36
  import {
37
37
  SessionRecorder
38
- } from "./chunk-4452SPFI.js";
38
+ } from "./chunk-NODRQGOK.js";
39
39
  import {
40
40
  addKnownIssue,
41
41
  getKnowledgePath,
@@ -59,7 +59,7 @@ import {
59
59
  resolveLaunchOpts,
60
60
  saveSessionDiskMeta,
61
61
  setActivePage
62
- } from "./chunk-GUTBIYFW.js";
62
+ } from "./chunk-JEDP4PJW.js";
63
63
  import "./chunk-TNEN6VQ2.js";
64
64
  import {
65
65
  errMsg
@@ -67,7 +67,7 @@ import {
67
67
  import {
68
68
  detectAntiBot,
69
69
  formatDetectionMessage
70
- } from "./chunk-INTQPBYF.js";
70
+ } from "./chunk-JKVUFP3G.js";
71
71
  import {
72
72
  NPM_REGISTRY_URL,
73
73
  NPM_SCOPE,
@@ -460,14 +460,23 @@ var CAPTCHA_SELECTORS = {
460
460
  generic: '#captcha, .captcha-container, [class*="captcha"]'
461
461
  };
462
462
  async function detectCaptcha(page) {
463
- for (const [type, selector] of Object.entries(CAPTCHA_SELECTORS)) {
464
- const el = await page.$(selector);
465
- if (el) {
466
- const iframeUrl = await el.getAttribute("src").catch(() => void 0);
467
- return { type, selector, iframeUrl: iframeUrl || void 0 };
468
- }
469
- }
470
- return null;
463
+ const hit = await page.evaluate(`
464
+ (function() {
465
+ var S = ${JSON.stringify(CAPTCHA_SELECTORS)};
466
+ for (var type in S) {
467
+ var sel = S[type];
468
+ var el;
469
+ try { el = document.querySelector(sel); } catch (e) { continue; }
470
+ if (el) {
471
+ var url = el.getAttribute && el.getAttribute('src');
472
+ return { type: type, selector: sel, iframeUrl: url || undefined };
473
+ }
474
+ }
475
+ return null;
476
+ })()
477
+ `).catch(() => null);
478
+ if (!hit) return null;
479
+ return { type: hit.type, selector: hit.selector, iframeUrl: hit.iframeUrl || void 0 };
471
480
  }
472
481
  async function waitForCaptchaSolved(page, captchaInfo, timeoutMs = 18e4) {
473
482
  const startTime = Date.now();
@@ -518,12 +527,24 @@ var clickCommand = registerCommand({
518
527
  let detectedNewPage;
519
528
  let cleanup;
520
529
  if (ctx.browserContext?.on) {
530
+ const targetIdOf = (p2) => p2._targetId;
531
+ let targetIdsBefore;
532
+ try {
533
+ targetIdsBefore = new Set(ctx.browserContext.pages().map(targetIdOf));
534
+ } catch {
535
+ targetIdsBefore = /* @__PURE__ */ new Set();
536
+ }
521
537
  const pagePromise = new Promise((resolve10) => {
522
538
  const timer = setTimeout(() => {
523
539
  ctx.browserContext.off("page", handler);
524
540
  resolve10(void 0);
525
541
  }, 3e3);
526
542
  const handler = (page2) => {
543
+ if (targetIdsBefore.has(targetIdOf(page2))) return;
544
+ try {
545
+ if ((page2.url?.() ?? "") === "about:blank") return;
546
+ } catch {
547
+ }
527
548
  clearTimeout(timer);
528
549
  ctx.browserContext.off("page", handler);
529
550
  resolve10(page2);
@@ -924,9 +945,11 @@ var mouseCommand = registerCommand({
924
945
  await ctx.page.mouse.move(p.x, p.y, { steps: p.steps || 1 });
925
946
  break;
926
947
  case "down":
948
+ await ctx.page.mouse.move(p.x, p.y);
927
949
  await ctx.page.mouse.down({ button });
928
950
  break;
929
951
  case "up":
952
+ await ctx.page.mouse.move(p.x, p.y);
930
953
  await ctx.page.mouse.up({ button });
931
954
  break;
932
955
  case "click":
@@ -945,10 +968,11 @@ import { z as z7 } from "zod";
945
968
  import { ok as ok7, normalizeTips as normalizeTips2 } from "@dyyz1993/xcli-core";
946
969
  var evaluateCommand = registerCommand({
947
970
  name: "eval",
948
- description: "Evaluate JavaScript expression in the browser",
971
+ description: "Evaluate JavaScript expression in the browser (--frame <url-substring> \u53EF\u5B9A\u5411\u5230 iframe \u4E0A\u4E0B\u6587\uFF0C\u8DE8\u57DF iframe \u4EA6\u53EF)",
949
972
  scope: "page",
950
973
  parameters: z7.object({
951
- expression: z7.string()
974
+ expression: z7.string(),
975
+ frame: z7.string().optional().describe("\u5728 URL \u542B\u6B64\u5B50\u4E32\u7684 iframe \u91CC\u6267\u884C\uFF08\u540C\u8FDB\u7A0B\u7528 contextId\uFF0C\u8DE8\u57DF OOPIF \u7528 Target auto-attach\uFF09")
952
976
  }),
953
977
  handler: async (p, ctx) => {
954
978
  const engine = createRuleEngine();
@@ -960,7 +984,7 @@ var evaluateCommand = registerCommand({
960
984
  direction: "client\u2192browser"
961
985
  });
962
986
  engine.stop();
963
- const result = await ctx.page.evaluate(p.expression);
987
+ const result = p.frame ? await ctx.page.evaluateInFrame(p.frame, p.expression) : await ctx.page.evaluate(p.expression);
964
988
  const response = ok7({ result });
965
989
  if (decision && decision.severity === "danger") {
966
990
  response.tips = normalizeTips2([`${decision.suggestion}`]);
@@ -5448,6 +5472,11 @@ async function handleSwitch(p, pages, ctx) {
5448
5472
  const targetPage = pages[p.index];
5449
5473
  await targetPage.bringToFront().catch(() => {
5450
5474
  });
5475
+ for (let i = 0; i < 5; i++) {
5476
+ const okProbe = await targetPage.evaluate("1").then(() => true, () => false);
5477
+ if (okProbe) break;
5478
+ await new Promise((r) => setTimeout(r, 300));
5479
+ }
5451
5480
  const session = ctx.sessionId ? getSessionById(ctx.sessionId) : void 0;
5452
5481
  if (session) {
5453
5482
  setActivePage(session, targetPage);
@@ -5871,7 +5900,7 @@ function describeSelector(strategy, value, name) {
5871
5900
  // src/context.ts
5872
5901
  function attachDetectAntiBot(ctx) {
5873
5902
  ctx.detectAntiBot = async (page, config) => {
5874
- const { detectAntiBot: detectAntiBot2 } = await import("./anti-bot-DR56Y63V.js");
5903
+ const { detectAntiBot: detectAntiBot2 } = await import("./anti-bot-GTTYNEFB.js");
5875
5904
  return detectAntiBot2(page, config);
5876
5905
  };
5877
5906
  }
@@ -7051,7 +7080,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
7051
7080
  }
7052
7081
  let targetPageOverride = null;
7053
7082
  if (_target && extraOpts?.cdpEndpoint) {
7054
- const { findTargetPage } = await import("./browser-FFYPFTBV.js");
7083
+ const { findTargetPage } = await import("./browser-Q5APBNF6.js");
7055
7084
  targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
7056
7085
  if (!targetPageOverride) {
7057
7086
  return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
@@ -7068,7 +7097,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
7068
7097
  params = result.data;
7069
7098
  }
7070
7099
  if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
7071
- const { forwardExec } = await import("./daemon-client-O6BYVRXV.js");
7100
+ const { forwardExec } = await import("./daemon-client-Y2YSZCGK.js");
7072
7101
  const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
7073
7102
  if (result) return result;
7074
7103
  }
@@ -9679,10 +9708,12 @@ var HTML_TAGS = /* @__PURE__ */ new Set([
9679
9708
  "map",
9680
9709
  "area"
9681
9710
  ]);
9711
+ var SELECTOR_DSL_PREFIXES = /^(text=|popup-text=|xpath=|role=|css=|chain=)/;
9682
9712
  function normalizeSelector(input) {
9683
9713
  if (!input) return input;
9684
9714
  if (/^[#\.\[\:\/]/.test(input)) return input;
9685
9715
  if (input === "*") return input;
9716
+ if (SELECTOR_DSL_PREFIXES.test(input)) return input;
9686
9717
  if (/[>\s+,~]/.test(input)) return input;
9687
9718
  const tagName = input.match(/^[a-zA-Z][a-zA-Z0-9]*/)?.[0];
9688
9719
  if (tagName && HTML_TAGS.has(tagName.toLowerCase())) return input;
@@ -9857,11 +9888,23 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
9857
9888
  output: options.output || options.o || args[0]
9858
9889
  };
9859
9890
  break;
9860
- case "eval":
9891
+ case "eval": {
9861
9892
  cmdName = "eval";
9862
- params = { expression: args.join(" ") || options.expression || "" };
9863
- if (!params.expression) outputError("Usage: xbrowser eval <expression>");
9893
+ const frameVal = options.frame || void 0;
9894
+ const exprParts = [];
9895
+ for (let i = 0; i < args.length; i++) {
9896
+ if (args[i] === "--frame") {
9897
+ i++;
9898
+ continue;
9899
+ }
9900
+ if (args[i].startsWith("--frame=")) continue;
9901
+ exprParts.push(args[i]);
9902
+ }
9903
+ params = { expression: exprParts.join(" ") || options.expression || "" };
9904
+ if (frameVal) params.frame = frameVal;
9905
+ if (!params.expression) outputError("Usage: xbrowser eval <expression> [--frame <url-substring>]");
9864
9906
  break;
9907
+ }
9865
9908
  case "scroll": {
9866
9909
  let direction = "down";
9867
9910
  let distance;
@@ -10564,7 +10607,7 @@ async function handlePlugin(args, options, mode) {
10564
10607
  } catch {
10565
10608
  }
10566
10609
  try {
10567
- const { daemonPing } = await import("./daemon-client-O6BYVRXV.js");
10610
+ const { daemonPing } = await import("./daemon-client-Y2YSZCGK.js");
10568
10611
  if (await daemonPing()) {
10569
10612
  await fetch("http://localhost:9224/rpc", {
10570
10613
  method: "POST",
@@ -11130,7 +11173,7 @@ async function handleFilter(args, _mode, options) {
11130
11173
  }
11131
11174
  }
11132
11175
  async function handleGeneratePlugin(sessionName, pluginName, outputDir) {
11133
- const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-QRZMKFVL.js");
11176
+ const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-SLDBENVF.js");
11134
11177
  const { readSiteKnowledge: readSiteKnowledge2, toMarkdown } = await import("./site-knowledge-SYC6VCDB.js");
11135
11178
  const { mkdirSync: mkdirSync10, writeFileSync: writeFileSync12 } = await import("fs");
11136
11179
  const { join: join13 } = await import("path");
@@ -12436,6 +12479,9 @@ var HTTPServer = class {
12436
12479
  this.server = null;
12437
12480
  reject(err);
12438
12481
  });
12482
+ server.headersTimeout = 20 * 6e4;
12483
+ server.requestTimeout = 20 * 6e4;
12484
+ server.keepAliveTimeout = 20 * 6e4;
12439
12485
  server.listen(this.port, this.host, () => {
12440
12486
  const addr = server.address();
12441
12487
  if (addr && typeof addr === "object") {
@@ -12698,12 +12744,16 @@ async function routeCommand(argvIn, stdinCommands) {
12698
12744
  }
12699
12745
  const globalFlags = /* @__PURE__ */ new Set(["--session", "--cdp", "--json", "--yaml", "--output", "--timeout", "--help", "-h", "--version"]);
12700
12746
  let chainArgIdx = -1;
12747
+ let seenCommandWord = false;
12701
12748
  for (let i = 0; i < argv.length; i++) {
12702
12749
  if (globalFlags.has(argv[i])) continue;
12703
12750
  if (globalFlags.has(argv[i]) || i > 0 && globalFlags.has(argv[i - 1]) && !argv[i].startsWith("-")) continue;
12704
- if (argv[i].includes(" ") && /^[a-zA-Z]/.test(argv[i])) {
12705
- chainArgIdx = i;
12706
- break;
12751
+ if (!seenCommandWord) {
12752
+ if (argv[i].includes(" ") && /^[a-zA-Z]/.test(argv[i])) {
12753
+ chainArgIdx = i;
12754
+ break;
12755
+ }
12756
+ seenCommandWord = true;
12707
12757
  }
12708
12758
  }
12709
12759
  if (chainArgIdx >= 0) {
@@ -13067,7 +13117,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
13067
13117
  }
13068
13118
  const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
13069
13119
  if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
13070
- const { forwardPluginExec } = await import("./daemon-client-O6BYVRXV.js");
13120
+ const { forwardPluginExec } = await import("./daemon-client-Y2YSZCGK.js");
13071
13121
  const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
13072
13122
  const result = await forwardPluginExec(`${command}.${subCommand}`, params, sessionName, cdpEndpoint, userTimeout);
13073
13123
  const resultData = result && typeof result === "object" && "data" in result ? result.data : void 0;
@@ -13107,7 +13157,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
13107
13157
  const targetPage = pages[cmdTabIndex];
13108
13158
  await targetPage.bringToFront().catch(() => {
13109
13159
  });
13110
- const { setActivePage: setActivePage2 } = await import("./browser-FFYPFTBV.js");
13160
+ const { setActivePage: setActivePage2 } = await import("./browser-Q5APBNF6.js");
13111
13161
  setActivePage2(session, targetPage);
13112
13162
  }
13113
13163
  }
@@ -13383,7 +13433,7 @@ async function main() {
13383
13433
  const command = process.argv[2];
13384
13434
  const isLongRunning = command === "preview" || command === "serve";
13385
13435
  if (!isLongRunning) {
13386
- const { ensureProcessCanExit } = await import("./browser-FFYPFTBV.js");
13436
+ const { ensureProcessCanExit } = await import("./browser-Q5APBNF6.js");
13387
13437
  await ensureProcessCanExit().catch(() => {
13388
13438
  });
13389
13439
  process.exit(process.exitCode || exitCode);
@@ -11,6 +11,7 @@ import {
11
11
  import "./chunk-KFQGP6VL.js";
12
12
 
13
13
  // src/client/daemon-client.ts
14
+ import { request as httpRequest } from "http";
14
15
  var DAEMON_PORT = 9224;
15
16
  var DAEMON_BASE = `http://localhost:${DAEMON_PORT}`;
16
17
  var _ensurePromise = null;
@@ -205,7 +206,40 @@ async function forwardRecordSummary(session) {
205
206
  return rpcCall("record:summary", { session }, 5e3);
206
207
  }
207
208
  async function forwardReplay(file, session, slowMo) {
208
- return rpcCall("replay", { file, session, slowMo }, 12e4);
209
+ return longRpcCall("replay", { file, session, slowMo }, 20 * 6e4);
210
+ }
211
+ function longRpcCall(method, params, timeoutMs) {
212
+ return new Promise((resolve, reject) => {
213
+ const body = JSON.stringify({ method, params });
214
+ const req = httpRequest(
215
+ {
216
+ hostname: "127.0.0.1",
217
+ port: DAEMON_PORT,
218
+ path: "/rpc",
219
+ method: "POST",
220
+ headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) }
221
+ },
222
+ (res) => {
223
+ const chunks = [];
224
+ res.on("data", (c) => chunks.push(c));
225
+ res.on("end", () => {
226
+ try {
227
+ const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
228
+ if (parsed && typeof parsed.error === "string" && Object.keys(parsed).length === 1) {
229
+ reject(new Error(`Daemon error: ${parsed.error}`));
230
+ } else {
231
+ resolve(parsed);
232
+ }
233
+ } catch (e) {
234
+ reject(e instanceof Error ? e : new Error(String(e)));
235
+ }
236
+ });
237
+ }
238
+ );
239
+ req.setTimeout(timeoutMs, () => req.destroy(new Error(`replay RPC timeout after ${timeoutMs}ms`)));
240
+ req.on("error", reject);
241
+ req.end(body);
242
+ });
209
243
  }
210
244
  async function forwardRecordCheckpoint(session, type, hint, selector) {
211
245
  return rpcCall("record:checkpoint", { session, type, hint, selector }, 1e4);
@@ -30,7 +30,7 @@ import {
30
30
  forwardSessionList,
31
31
  forwardViewerCheckSelector,
32
32
  isDaemonRunning
33
- } from "./chunk-YMUSHPU4.js";
33
+ } from "./chunk-WHPBNUKB.js";
34
34
  import "./chunk-Q2DFJQGS.js";
35
35
  import "./chunk-GDKLH7ZY.js";
36
36
  import "./chunk-KFQGP6VL.js";
@@ -15,7 +15,7 @@ import {
15
15
  } from "./chunk-VEDJ5XSQ.js";
16
16
  import {
17
17
  SessionRecorder
18
- } from "./chunk-5UGR6MUK.js";
18
+ } from "./chunk-HBHOKZPN.js";
19
19
  import {
20
20
  closeEphemeralContext,
21
21
  closeSessionByName,
@@ -31,8 +31,11 @@ import {
31
31
  resolveLaunchOpts,
32
32
  saveSessionDiskMeta,
33
33
  setActivePage
34
- } from "./chunk-6OZWKXSW.js";
35
- import "./chunk-RRBXV7KE.js";
34
+ } from "./chunk-HVPUVVSA.js";
35
+ import "./chunk-XQ4HRPDJ.js";
36
+ import {
37
+ queryJS
38
+ } from "./chunk-3FWLW7FS.js";
36
39
  import "./chunk-TNEN6VQ2.js";
37
40
  import {
38
41
  errMsg
@@ -40,7 +43,7 @@ import {
40
43
  import {
41
44
  detectAntiBot,
42
45
  formatDetectionMessage
43
- } from "./chunk-INTQPBYF.js";
46
+ } from "./chunk-JKVUFP3G.js";
44
47
  import {
45
48
  ScreencastCapturer
46
49
  } from "./chunk-BNO7OKO4.js";
@@ -416,14 +419,23 @@ var CAPTCHA_SELECTORS = {
416
419
  generic: '#captcha, .captcha-container, [class*="captcha"]'
417
420
  };
418
421
  async function detectCaptcha(page) {
419
- for (const [type, selector] of Object.entries(CAPTCHA_SELECTORS)) {
420
- const el = await page.$(selector);
421
- if (el) {
422
- const iframeUrl = await el.getAttribute("src").catch(() => void 0);
423
- return { type, selector, iframeUrl: iframeUrl || void 0 };
424
- }
425
- }
426
- return null;
422
+ const hit = await page.evaluate(`
423
+ (function() {
424
+ var S = ${JSON.stringify(CAPTCHA_SELECTORS)};
425
+ for (var type in S) {
426
+ var sel = S[type];
427
+ var el;
428
+ try { el = document.querySelector(sel); } catch (e) { continue; }
429
+ if (el) {
430
+ var url = el.getAttribute && el.getAttribute('src');
431
+ return { type: type, selector: sel, iframeUrl: url || undefined };
432
+ }
433
+ }
434
+ return null;
435
+ })()
436
+ `).catch(() => null);
437
+ if (!hit) return null;
438
+ return { type: hit.type, selector: hit.selector, iframeUrl: hit.iframeUrl || void 0 };
427
439
  }
428
440
  async function waitForCaptchaSolved(page, captchaInfo, timeoutMs = 18e4) {
429
441
  const startTime = Date.now();
@@ -474,12 +486,24 @@ var clickCommand = registerCommand({
474
486
  let detectedNewPage;
475
487
  let cleanup;
476
488
  if (ctx.browserContext?.on) {
489
+ const targetIdOf = (p2) => p2._targetId;
490
+ let targetIdsBefore;
491
+ try {
492
+ targetIdsBefore = new Set(ctx.browserContext.pages().map(targetIdOf));
493
+ } catch {
494
+ targetIdsBefore = /* @__PURE__ */ new Set();
495
+ }
477
496
  const pagePromise = new Promise((resolve) => {
478
497
  const timer = setTimeout(() => {
479
498
  ctx.browserContext.off("page", handler);
480
499
  resolve(void 0);
481
500
  }, 3e3);
482
501
  const handler = (page2) => {
502
+ if (targetIdsBefore.has(targetIdOf(page2))) return;
503
+ try {
504
+ if ((page2.url?.() ?? "") === "about:blank") return;
505
+ } catch {
506
+ }
483
507
  clearTimeout(timer);
484
508
  ctx.browserContext.off("page", handler);
485
509
  resolve(page2);
@@ -880,9 +904,11 @@ var mouseCommand = registerCommand({
880
904
  await ctx.page.mouse.move(p.x, p.y, { steps: p.steps || 1 });
881
905
  break;
882
906
  case "down":
907
+ await ctx.page.mouse.move(p.x, p.y);
883
908
  await ctx.page.mouse.down({ button });
884
909
  break;
885
910
  case "up":
911
+ await ctx.page.mouse.move(p.x, p.y);
886
912
  await ctx.page.mouse.up({ button });
887
913
  break;
888
914
  case "click":
@@ -901,10 +927,11 @@ import { z as z7 } from "zod";
901
927
  import { ok as ok7, normalizeTips as normalizeTips2 } from "@dyyz1993/xcli-core";
902
928
  var evaluateCommand = registerCommand({
903
929
  name: "eval",
904
- description: "Evaluate JavaScript expression in the browser",
930
+ description: "Evaluate JavaScript expression in the browser (--frame <url-substring> \u53EF\u5B9A\u5411\u5230 iframe \u4E0A\u4E0B\u6587\uFF0C\u8DE8\u57DF iframe \u4EA6\u53EF)",
905
931
  scope: "page",
906
932
  parameters: z7.object({
907
- expression: z7.string()
933
+ expression: z7.string(),
934
+ frame: z7.string().optional().describe("\u5728 URL \u542B\u6B64\u5B50\u4E32\u7684 iframe \u91CC\u6267\u884C\uFF08\u540C\u8FDB\u7A0B\u7528 contextId\uFF0C\u8DE8\u57DF OOPIF \u7528 Target auto-attach\uFF09")
908
935
  }),
909
936
  handler: async (p, ctx) => {
910
937
  const engine = createRuleEngine();
@@ -916,7 +943,7 @@ var evaluateCommand = registerCommand({
916
943
  direction: "client\u2192browser"
917
944
  });
918
945
  engine.stop();
919
- const result = await ctx.page.evaluate(p.expression);
946
+ const result = p.frame ? await ctx.page.evaluateInFrame(p.frame, p.expression) : await ctx.page.evaluate(p.expression);
920
947
  const response = ok7({ result });
921
948
  if (decision && decision.severity === "danger") {
922
949
  response.tips = normalizeTips2([`${decision.suggestion}`]);
@@ -5404,6 +5431,11 @@ async function handleSwitch(p, pages, ctx) {
5404
5431
  const targetPage = pages[p.index];
5405
5432
  await targetPage.bringToFront().catch(() => {
5406
5433
  });
5434
+ for (let i = 0; i < 5; i++) {
5435
+ const okProbe = await targetPage.evaluate("1").then(() => true, () => false);
5436
+ if (okProbe) break;
5437
+ await new Promise((r) => setTimeout(r, 300));
5438
+ }
5407
5439
  const session = ctx.sessionId ? getSessionById(ctx.sessionId) : void 0;
5408
5440
  if (session) {
5409
5441
  setActivePage(session, targetPage);
@@ -5827,7 +5859,7 @@ function describeSelector(strategy, value, name) {
5827
5859
  // src/context.ts
5828
5860
  function attachDetectAntiBot(ctx) {
5829
5861
  ctx.detectAntiBot = async (page, config) => {
5830
- const { detectAntiBot: detectAntiBot2 } = await import("./anti-bot-DR56Y63V.js");
5862
+ const { detectAntiBot: detectAntiBot2 } = await import("./anti-bot-GTTYNEFB.js");
5831
5863
  return detectAntiBot2(page, config);
5832
5864
  };
5833
5865
  }
@@ -6565,7 +6597,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6565
6597
  }
6566
6598
  let targetPageOverride = null;
6567
6599
  if (_target && extraOpts?.cdpEndpoint) {
6568
- const { findTargetPage } = await import("./browser-RHWUAMTB.js");
6600
+ const { findTargetPage } = await import("./browser-XNU7JQYC.js");
6569
6601
  targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
6570
6602
  if (!targetPageOverride) {
6571
6603
  return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
@@ -6582,7 +6614,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6582
6614
  params = result.data;
6583
6615
  }
6584
6616
  if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
6585
- const { forwardExec } = await import("./daemon-client-GKEPT4NY.js");
6617
+ const { forwardExec } = await import("./daemon-client-MMDRYCF5.js");
6586
6618
  const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
6587
6619
  if (result) return result;
6588
6620
  }
@@ -7941,15 +7973,26 @@ function createRPCHandler() {
7941
7973
  const needsPause = INTERACTION_COMMANDS2.has(command) && !!previewWS;
7942
7974
  if (needsPause) await previewWS.pauseScreencast(sessionName);
7943
7975
  let urlBeforeCommand;
7976
+ let preElement;
7944
7977
  try {
7945
7978
  const session = findSession(sessionName);
7946
7979
  urlBeforeCommand = session?.page?.url();
7980
+ const sel = cmdParams.selector || cmdParams.css;
7981
+ if (session?.page && sel && INTERACTION_COMMANDS2.has(command)) {
7982
+ preElement = await session.page.evaluate(`
7983
+ (function() {
7984
+ const el = ${queryJS(sel)};
7985
+ if (!el || typeof window.__xb_describe !== 'function') return null;
7986
+ return window.__xb_describe(el);
7987
+ })()
7988
+ `);
7989
+ }
7947
7990
  } catch {
7948
7991
  }
7949
7992
  try {
7950
7993
  const result = await executeCommand(command, cmdParams, sessionName, { cdpEndpoint: endpoint });
7951
7994
  registerSessionIfNew(sessionName);
7952
- await injectCommandToRecorder(sessionName, command, cmdParams, urlBeforeCommand);
7995
+ await injectCommandToRecorder(sessionName, command, cmdParams, urlBeforeCommand, preElement);
7953
7996
  return result;
7954
7997
  } finally {
7955
7998
  if (needsPause) await previewWS.resumeScreencast(sessionName).catch(() => {
@@ -8104,7 +8147,7 @@ function createRPCHandler() {
8104
8147
  const limit = params.limit || 50;
8105
8148
  return { session: sessionName, commands: commandLogStore.list(sessionName, { limit }) };
8106
8149
  }
8107
- async function injectCommandToRecorder(sessionName, command, params, urlBeforeCommand) {
8150
+ async function injectCommandToRecorder(sessionName, command, params, urlBeforeCommand, preElement) {
8108
8151
  const recorder = activeRecorders.get(sessionName);
8109
8152
  if (!recorder) return;
8110
8153
  const COMMAND_ACTION_MAP = {
@@ -8112,33 +8155,48 @@ function createRPCHandler() {
8112
8155
  fill: "cdp-fill",
8113
8156
  click: "cdp-click",
8114
8157
  type: "input",
8115
- select: "change"
8158
+ select: "change",
8159
+ scroll: "scroll"
8116
8160
  };
8117
8161
  const actionType = COMMAND_ACTION_MAP[command];
8118
8162
  if (!actionType) return;
8119
8163
  const selector = params.selector || params.css;
8120
- const value = params.value || params.expression;
8164
+ let value = params.value || params.expression;
8165
+ if (value === void 0 || value === "") {
8166
+ value = params.text;
8167
+ }
8168
+ if (command === "scroll") {
8169
+ const dir = params.direction || "down";
8170
+ const dist = params.distance ?? 300;
8171
+ value = `${dir}:${dist}`;
8172
+ }
8121
8173
  let currentUrl = params.url || urlBeforeCommand;
8122
8174
  if (command !== "goto" && urlBeforeCommand && urlBeforeCommand !== "about:blank") {
8123
8175
  currentUrl = urlBeforeCommand;
8124
8176
  }
8125
- let element;
8126
- if (selector) {
8177
+ let element = preElement;
8178
+ if (!element && selector) {
8127
8179
  try {
8128
8180
  const session = findSession(sessionName);
8129
8181
  if (session?.page) {
8130
- element = await session.page.evaluate((sel) => {
8131
- const el = document.querySelector(sel);
8132
- if (!el || typeof window.__xb_describe !== "function") return null;
8133
- return window.__xb_describe(el);
8134
- }, selector);
8182
+ element = await session.page.evaluate(`
8183
+ (function() {
8184
+ const el = ${queryJS(selector)};
8185
+ if (!el || typeof window.__xb_describe !== 'function') return null;
8186
+ return window.__xb_describe(el);
8187
+ })()
8188
+ `);
8135
8189
  }
8136
8190
  } catch {
8137
8191
  }
8138
8192
  }
8139
8193
  await recorder.recordCommandAction({
8140
8194
  type: actionType,
8141
- selector,
8195
+ // Prefer the described unique selector so the dedup key matches the
8196
+ // real action signal's key (which uses element.selector). The raw
8197
+ // command selector (e.g. "button") never equals the generated unique
8198
+ // selector, so dedup silently failed before (rec-duel d02/d03).
8199
+ selector: element?.selector || selector,
8142
8200
  value,
8143
8201
  url: currentUrl,
8144
8202
  element
@@ -8348,7 +8406,7 @@ function createRPCHandler() {
8348
8406
  if (isNewFormat) {
8349
8407
  try {
8350
8408
  const replayErrors = [];
8351
- const { SessionReplayer } = await import("./session-replayer-LJUC4TI7.js");
8409
+ const { SessionReplayer } = await import("./session-replayer-WIUYVN5J.js");
8352
8410
  const replayer = new SessionReplayer({
8353
8411
  page: session.page,
8354
8412
  stepDelay: slowMo * 500,
@@ -8430,7 +8488,19 @@ function createRPCHandler() {
8430
8488
  import { EventEmitter as EventEmitter2 } from "events";
8431
8489
 
8432
8490
  // src/stream/frame-processor.ts
8433
- import sharp from "sharp";
8491
+ var _sharp = null;
8492
+ async function loadSharp() {
8493
+ if (!_sharp) {
8494
+ try {
8495
+ _sharp = (await import("sharp")).default;
8496
+ } catch (err) {
8497
+ throw new Error(
8498
+ "\u76F4\u64AD\u6D41\u5E27\u5904\u7406\u9700\u8981 sharp \u539F\u751F\u6A21\u5757\uFF0C\u5F53\u524D\u73AF\u5883\u52A0\u8F7D\u5931\u8D25\uFF1A" + (err instanceof Error ? err.message : String(err)) + "\u3002\u4E0D\u7528\u76F4\u64AD\u6D41\u529F\u80FD\u53EF\u5FFD\u7565\uFF1B\u9700\u8981\u76F4\u64AD\u8BF7\u5728\u517C\u5BB9\u73AF\u5883\u8FD0\u884C\u6216\u5B89\u88C5\u5BF9\u5E94\u5E73\u53F0\u4F9D\u8D56\u3002"
8499
+ );
8500
+ }
8501
+ }
8502
+ return _sharp;
8503
+ }
8434
8504
  var STATE_CONFIGS = {
8435
8505
  // Full-screen: aggressive compression for speed
8436
8506
  // user_interacting: low quality + downscale — prioritize FPS over clarity
@@ -8582,6 +8652,7 @@ var FrameProcessor = class {
8582
8652
  if (!needsResize && !needsCrop && !needsReencode) {
8583
8653
  return buffer;
8584
8654
  }
8655
+ const sharp = await loadSharp();
8585
8656
  let processed = sharp(buffer);
8586
8657
  if (cropConfig) {
8587
8658
  const meta = await processed.metadata();
package/dist/index.d.ts CHANGED
@@ -173,6 +173,12 @@ interface XBPage {
173
173
  }): Promise<void>;
174
174
  addInitScript(script: string): Promise<void>;
175
175
  discoverFrames(): Promise<XBFrame[]>;
176
+ /**
177
+ * 在 URL 含 urlIncludes 的 iframe 上下文中执行表达式。
178
+ * 同进程 iframe 走 contextId;跨域 OOPIF 走 Target auto-attach。
179
+ * 绕过页面同源策略(CDP 是调试通道)——跨域嵌入内容读写的唯一路径。
180
+ */
181
+ evaluateInFrame<R = unknown>(urlIncludes: string, expression: string): Promise<R>;
176
182
  bringToFront(): Promise<void>;
177
183
  setExtraHTTPHeaders(headers: Record<string, string>): Promise<void>;
178
184
  setOfflineMode(offline: boolean): Promise<void>;
@@ -288,6 +294,7 @@ interface XBMouse {
288
294
  button?: 'left' | 'right' | 'middle';
289
295
  clickCount?: number;
290
296
  delay?: number;
297
+ stealth?: boolean;
291
298
  }): Promise<void>;
292
299
  dblclick(x: number, y: number, opts?: {
293
300
  button?: 'left' | 'right' | 'middle';
@@ -311,6 +318,7 @@ interface XBKeyboard {
311
318
  up(key: string): Promise<void>;
312
319
  type(text: string, opts?: {
313
320
  delay?: number;
321
+ stealth?: boolean;
314
322
  }): Promise<void>;
315
323
  insertText(text: string): Promise<void>;
316
324
  }