@xbrowser/cli 1.10.0 → 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-T3V3JWVH.js → browser-Q5APBNF6.js} +1 -1
  3. package/dist/{browser-HBL72GPZ.js → browser-V3JIWSTR.js} +2 -2
  4. package/dist/{browser-WQZ3D6AE.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-BAHSRZIX.js → chunk-HVPUVVSA.js} +13 -10
  12. package/dist/{chunk-NITFVWWS.js → chunk-JEDP4PJW.js} +448 -65
  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-WSCP7QCJ.js → chunk-XKQVUFUS.js} +13 -10
  17. package/dist/{chunk-RRBXV7KE.js → chunk-XQ4HRPDJ.js} +384 -102
  18. package/dist/cli.js +72 -26
  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 +89 -31
  22. package/dist/index.d.ts +8 -0
  23. package/dist/index.js +73 -27
  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
@@ -19,6 +19,161 @@ import { EventEmitter as EventEmitter2 } from "events";
19
19
  // src/cdp-driver/page.ts
20
20
  import { EventEmitter } from "events";
21
21
 
22
+ // src/cdp-driver/stealth.ts
23
+ var DEFAULT_STEALTH_CONFIG = {
24
+ bezierCurvature: [0.35, 0.6],
25
+ noiseAmplitude: 5.5,
26
+ overshootRange: [6, 14],
27
+ aimPause: [150, 400],
28
+ pressDuration: [60, 140],
29
+ releaseDrift: [0.8, 2.5],
30
+ landingOffsetSmall: [0.3, 2.5],
31
+ landingOffsetLarge: [1.5, 7],
32
+ smallElementThreshold: 30,
33
+ typingRhythm: {
34
+ fastProb: 0.22,
35
+ fastRange: [25, 60],
36
+ normalRange: [50, 350],
37
+ pauseProb: 0.18,
38
+ pauseRange: [400, 1200]
39
+ },
40
+ keyPressDuration: [50, 110],
41
+ typoProbability: 0.06,
42
+ wheelPeak: 180,
43
+ wheelDecayRate: 0.4
44
+ };
45
+ function rand(min, max) {
46
+ return min + Math.random() * (max - min);
47
+ }
48
+ function cosineEase(t) {
49
+ return 0.5 - 0.5 * Math.cos(Math.PI * t);
50
+ }
51
+ function bezierTrajectory(x0, y0, x1, y1, config = DEFAULT_STEALTH_CONFIG) {
52
+ const dist = Math.hypot(x1 - x0, y1 - y0);
53
+ const n = Math.max(10, Math.min(28, Math.round(dist / 15)));
54
+ const shortMove = dist < 120;
55
+ const curvature = shortMove ? rand(2, 6) : Math.max(dist * rand(...config.bezierCurvature), rand(18, 35));
56
+ const dir = Math.random() < 0.5 ? 1 : -1;
57
+ const d = dist || 1;
58
+ const dx = x1 - x0, dy = y1 - y0;
59
+ const c1x = x0 + dx * 0.3 - dy / d * curvature * 0.5 * dir;
60
+ const c1y = y0 + dy * 0.3 + dx / d * curvature * 0.5 * dir;
61
+ const c2x = x0 + dx * 0.7 - dy / d * curvature * 0.8 * dir;
62
+ const c2y = y0 + dy * 0.7 + dx / d * curvature * 0.8 * dir;
63
+ const points = [];
64
+ for (let i = 1; i <= n; i++) {
65
+ const t = cosineEase(i / n);
66
+ const mt = 1 - t;
67
+ let px = mt ** 3 * x0 + 3 * mt ** 2 * t * c1x + 3 * mt * t ** 2 * c2x + t ** 3 * x1;
68
+ let py = mt ** 3 * y0 + 3 * mt ** 2 * t * c1y + 3 * mt * t ** 2 * c2y + t ** 3 * y1;
69
+ const amp = shortMove ? Math.min(2, config.noiseAmplitude) : config.noiseAmplitude;
70
+ px += rand(-amp, amp);
71
+ py += rand(-amp, amp);
72
+ points.push({ x: px, y: py, delay: rand(9, 16) });
73
+ }
74
+ if (!shortMove) {
75
+ const over = rand(...config.overshootRange);
76
+ const ox = x1 + dx / d * over + rand(-2, 2);
77
+ const oy = y1 + dy / d * over + rand(-2, 2);
78
+ points.push({ x: ox, y: oy, delay: rand(14, 30) });
79
+ points.push({
80
+ x: x1 + dx / d * over * 0.4,
81
+ y: y1 + dy / d * over * 0.4,
82
+ delay: rand(14, 30)
83
+ });
84
+ }
85
+ points.push({ x: x1 + rand(-1, 1), y: y1 + rand(-1, 1), delay: rand(14, 30) });
86
+ return points;
87
+ }
88
+ function landingOffset(width, height, config = DEFAULT_STEALTH_CONFIG) {
89
+ const isSmall = Math.min(width, height) < config.smallElementThreshold;
90
+ const range = isSmall ? config.landingOffsetSmall : config.landingOffsetLarge;
91
+ const dx = rand(...range) * (Math.random() < 0.5 ? -1 : 1);
92
+ const dy = rand(...range) * (Math.random() < 0.5 ? -1 : 1);
93
+ return { dx, dy };
94
+ }
95
+ var KEY_MAP = {};
96
+ for (let i = 97; i <= 122; i++) {
97
+ const ch = String.fromCharCode(i);
98
+ KEY_MAP[ch] = { key: ch, code: "Key" + ch.toUpperCase(), vk: i - 32 };
99
+ }
100
+ for (let i = 65; i <= 90; i++) {
101
+ const ch = String.fromCharCode(i);
102
+ KEY_MAP[ch] = { key: ch, code: "Key" + ch, vk: i, shift: true };
103
+ }
104
+ for (let i = 48; i <= 57; i++) {
105
+ const ch = String.fromCharCode(i);
106
+ KEY_MAP[ch] = { key: ch, code: "Digit" + ch, vk: i };
107
+ }
108
+ Object.assign(KEY_MAP, {
109
+ " ": { key: " ", code: "Space", vk: 32 },
110
+ ".": { key: ".", code: "Period", vk: 190 },
111
+ "-": { key: "-", code: "Minus", vk: 189 },
112
+ "@": { key: "@", code: "Digit2", vk: 50, shift: true },
113
+ "_": { key: "_", code: "Minus", vk: 189, shift: true }
114
+ });
115
+ function buildStealthInitScript() {
116
+ return [
117
+ "(function(){",
118
+ // 1. AEL event proxy
119
+ " var o=EventTarget.prototype.addEventListener;",
120
+ " var fc=new InputDeviceCapabilities({firesTouchEvents:false});",
121
+ " var _ael=function(t,f){",
122
+ " var op=arguments[2];",
123
+ ' if(typeof f!=="function")return o.call(this,t,f,op);',
124
+ " var w=function(e){",
125
+ " if(!e||e.constructor===FocusEvent||e.constructor===KeyboardEvent)return f.call(this,e);",
126
+ " return f.call(this,new Proxy(e,{get:function(k,p){",
127
+ ' if(p==="sourceCapabilities")return fc;',
128
+ ' if(p==="isTrusted")return true;',
129
+ ' if((p==="clientX"||p==="clientY")&&k.type==="click"&&Number.isFinite(k[p])){',
130
+ " var _f=((k.timeStamp||Date.now())%89)/89*0.7+0.15;",
131
+ " return k[p]+_f;",
132
+ " }",
133
+ ' var v=Reflect.get(k,p);return typeof v==="function"?v.bind(k):v;',
134
+ " }}));",
135
+ " };",
136
+ " return o.call(this,t,w,op);",
137
+ " };",
138
+ " EventTarget.prototype.addEventListener=_ael;",
139
+ // 2. Screen override (prototype-level, not instance-level)
140
+ " var _gw=function(){return 1728};",
141
+ " var _gh=function(){return 1117};",
142
+ " var _gah=function(){return 1092};",
143
+ ' Object.defineProperty(Screen.prototype,"width",{get:_gw,configurable:true});',
144
+ ' Object.defineProperty(Screen.prototype,"height",{get:_gh,configurable:true});',
145
+ ' Object.defineProperty(Screen.prototype,"availWidth",{get:_gw,configurable:true});',
146
+ ' Object.defineProperty(Screen.prototype,"availHeight",{get:_gah,configurable:true});',
147
+ " document.hasFocus=function(){return true};",
148
+ // 3. toString disguise (name-list based)
149
+ " var _ts=Function.prototype.toString;",
150
+ " var _hf=document.hasFocus;",
151
+ " Function.prototype.toString=function(){",
152
+ ' if(this===_ael)return"function addEventListener(type, callback) { [native code] }";',
153
+ ' if(this===_hf)return"function hasFocus() { [native code] }";',
154
+ ' if(this===_gw)return"function get width() { [native code] }";',
155
+ ' if(this===_gh)return"function get height() { [native code] }";',
156
+ ' if(this===_gah)return"function get availHeight() { [native code] }";',
157
+ " return _ts.call(this);",
158
+ " };",
159
+ // 4. onclick prototype hijack (dual-stream consistency)
160
+ " var _ba=function(k,p){",
161
+ ' if(p==="isTrusted")return true;',
162
+ ' if((p==="clientX"||p==="clientY")&&k.type==="click"&&Number.isFinite(k[p])){',
163
+ " var _f=((k.timeStamp||Date.now())%89)/89*0.7+0.15;return k[p]+_f;",
164
+ " }",
165
+ ' var v=Reflect.get(k,p);return typeof v==="function"?v.bind(k):v;',
166
+ " };",
167
+ ' Object.defineProperty(Document.prototype,"onclick",{',
168
+ " configurable:true,",
169
+ " get:function(){var raw=this.__ocRaw||null;if(!raw)return null;var self=this;",
170
+ " return function(e){return raw.call(self,new Proxy(e,{get:function(k,p){return _ba(k,p)}}))}},",
171
+ " set:function(fn){this.__ocRaw=fn}",
172
+ " });",
173
+ "})()"
174
+ ].join("\n");
175
+ }
176
+
22
177
  // src/cdp-driver/mouse.ts
23
178
  var XBMouseImpl = class {
24
179
  conn;
@@ -40,18 +195,35 @@ var XBMouseImpl = class {
40
195
  }
41
196
  async click(x, y, opts = {}) {
42
197
  const button = opts.button ?? "left";
43
- const clickCount = opts.clickCount ?? 1;
44
- const delay = opts.delay ?? 0;
45
- await this.move(x, y);
46
- await this.down({ button });
47
- if (delay > 0) {
48
- await sleep(delay);
198
+ const stealth = opts.stealth ?? true;
199
+ let tx = x, ty = y;
200
+ if (stealth && opts.elementWidth !== void 0 && opts.elementHeight !== void 0) {
201
+ const off = landingOffset(opts.elementWidth, opts.elementHeight);
202
+ tx += off.dx;
203
+ ty += off.dy;
204
+ }
205
+ if (stealth) {
206
+ const traj = bezierTrajectory(this._x, this._y, tx, ty);
207
+ for (const p of traj) {
208
+ await this.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: p.x, y: p.y, button: this._button });
209
+ this._x = p.x;
210
+ this._y = p.y;
211
+ await sleep(p.delay);
212
+ }
213
+ await sleep(rand(...DEFAULT_STEALTH_CONFIG.aimPause));
214
+ } else {
215
+ await this.move(tx, ty);
49
216
  }
50
- await this.up({ button });
51
- for (let i = 1; i < clickCount; i++) {
52
- if (delay > 0) await sleep(delay);
217
+ await this.down({ button });
218
+ await sleep(stealth ? rand(...DEFAULT_STEALTH_CONFIG.pressDuration) : opts.delay ?? 0);
219
+ const rx = stealth ? this._x + rand(...DEFAULT_STEALTH_CONFIG.releaseDrift) * (Math.random() < 0.5 ? -1 : 1) : this._x;
220
+ const ry = stealth ? this._y + rand(...DEFAULT_STEALTH_CONFIG.releaseDrift) * (Math.random() < 0.5 ? -1 : 1) : this._y;
221
+ this._x = rx;
222
+ this._y = ry;
223
+ await this.send("Input.dispatchMouseEvent", { type: "mouseReleased", x: rx, y: ry, button, clickCount: opts.clickCount ?? 1 });
224
+ for (let i = 1; i < (opts.clickCount ?? 1); i++) {
225
+ if (opts.delay) await sleep(opts.delay);
53
226
  await this.down({ button });
54
- if (delay > 0) await sleep(delay);
55
227
  await this.up({ button });
56
228
  }
57
229
  }
@@ -230,7 +402,7 @@ var XBKeyboardImpl = class {
230
402
  }
231
403
  };
232
404
  function resolveKeyMapping(key) {
233
- if (KEY_MAP[key]) return KEY_MAP[key];
405
+ if (KEY_MAP2[key]) return KEY_MAP2[key];
234
406
  if (key.length === 1) {
235
407
  const lower = key.toLowerCase();
236
408
  if (lower >= "a" && lower <= "z") {
@@ -247,7 +419,7 @@ function resolveKeyMapping(key) {
247
419
  }
248
420
  return { key, code: key };
249
421
  }
250
- var KEY_MAP = {
422
+ var KEY_MAP2 = {
251
423
  Enter: { key: "Enter", code: "Enter", text: "\r", keyCode: 13 },
252
424
  Tab: { key: "Tab", code: "Tab", text: " ", keyCode: 9 },
253
425
  Escape: { key: "Escape", code: "Escape", keyCode: 27 },
@@ -285,6 +457,37 @@ function sleep2(ms) {
285
457
 
286
458
  // src/cdp-driver/selector-utils.ts
287
459
  function queryJS(selector) {
460
+ return `(${deepQueryIIFE})( ${JSON.stringify(queryMainJS(selector))} )`;
461
+ }
462
+ var deepQueryIIFE = `(function(mainExpr) {
463
+ const run = (root) => {
464
+ try { return new Function('document', 'return (' + mainExpr + ')')(root); }
465
+ catch (e) { return null; }
466
+ };
467
+ const scanRoot = (root) => {
468
+ const direct = run(root);
469
+ if (direct) return direct;
470
+ let all;
471
+ try { all = root.querySelectorAll('*'); } catch (e) { return null; }
472
+ for (const el of all) {
473
+ if (el.shadowRoot) {
474
+ const r = scanRoot(el.shadowRoot);
475
+ if (r) return r;
476
+ }
477
+ if (el.tagName === 'IFRAME') {
478
+ let inner = null;
479
+ try { inner = el.contentDocument; } catch (e) { /* cross-origin */ }
480
+ if (inner) {
481
+ const r = scanRoot(inner);
482
+ if (r) return r;
483
+ }
484
+ }
485
+ }
486
+ return null;
487
+ };
488
+ return scanRoot(document);
489
+ })`;
490
+ function queryMainJS(selector) {
288
491
  if (selector.startsWith("xpath=")) {
289
492
  const xpath = JSON.stringify(selector.slice(6));
290
493
  return `document.evaluate(${xpath}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue`;
@@ -296,13 +499,35 @@ function queryJS(selector) {
296
499
  return `(() => {
297
500
  const target = ${JSON.stringify(text)};
298
501
  const exact = ${exact};
502
+ // Match on OWN text nodes (not strict leaf elements): search-result
503
+ // titles mix text with inline highlight <em> marks \u2014 a strict leaf filter
504
+ // finds nothing there (real-world juejin). Own-text keeps the match
505
+ // precise (descendant-only text doesn't count) while tolerating markup.
506
+ const ownText = (e) => Array.prototype.filter.call(e.childNodes, (n) => n.nodeType === 3)
507
+ .map((n) => n.textContent).join('').trim();
299
508
  const els = [...document.querySelectorAll('*')].filter(e => {
300
- if (e.children.length > 0) return false;
301
- if (e.offsetParent === null) return false;
302
- const t = (e.textContent || '').trim();
509
+ if (e.offsetParent === null && e.tagName !== 'BODY') return false;
510
+ const t = ownText(e);
303
511
  if (!t) return false;
304
512
  return exact ? t === target : t.toLowerCase().includes(target.toLowerCase());
305
513
  });
514
+ // Rank instead of raw DOM order: exact text beats substring, interactive
515
+ // elements (button/a/[onclick]/inputs) beat prose. Prevents matching a
516
+ // description paragraph that merely MENTIONS the target label
517
+ // (rec-duel d06: header text "\u76EE\u6807\u9879\u300C\u7B2C 87 \u53F7\u300D" hijacked text=\u7B2C 87 \u53F7).
518
+ const isInteractive = (e) => {
519
+ const tag = e.tagName;
520
+ return tag === 'BUTTON' || tag === 'A' || tag === 'INPUT' || tag === 'SELECT'
521
+ || e.hasAttribute('onclick') || e.getAttribute('role') === 'button';
522
+ };
523
+ els.sort((a, b) => {
524
+ const ta = ownText(a), tb = ownText(b);
525
+ const ea = ta === target ? 0 : 1, eb = tb === target ? 0 : 1;
526
+ if (ea !== eb) return ea - eb;
527
+ const ia = isInteractive(a) ? 0 : 1, ib = isInteractive(b) ? 0 : 1;
528
+ if (ia !== ib) return ia - ib;
529
+ return 0; // stable \u2014 preserve DOM order
530
+ });
306
531
  return els[0] || null;
307
532
  })()`;
308
533
  }
@@ -336,43 +561,53 @@ function queryAllJS(selector) {
336
561
  async function waitForActionable(page, selector, opts = {}) {
337
562
  const timeout = opts.timeout ?? 3e4;
338
563
  if (opts.force) {
339
- if (selector.startsWith("xpath=")) {
340
- const rect2 = await page.evaluate(`
564
+ const deadline2 = Date.now() + timeout;
565
+ let lastError;
566
+ while (Date.now() < deadline2) {
567
+ const rect = await page.evaluate(`
341
568
  (function() {
342
569
  const el = ${queryJS(selector)};
343
570
  if (!el) return null;
344
571
  const r = el.getBoundingClientRect();
345
- return { x: r.x, y: r.y, width: r.width, height: r.height };
572
+ let x = r.x, y = r.y;
573
+ let doc = el.ownerDocument;
574
+ while (doc !== document) {
575
+ let host = null;
576
+ const scan = (d) => {
577
+ let frames;
578
+ try { frames = d.querySelectorAll('iframe'); } catch (e) { return null; }
579
+ for (const f of frames) {
580
+ let inner = null;
581
+ try { inner = f.contentDocument; } catch (e) { continue; }
582
+ if (!inner) continue;
583
+ if (inner === doc) return f;
584
+ const rr = scan(inner);
585
+ if (rr) return rr;
586
+ }
587
+ return null;
588
+ };
589
+ host = scan(document);
590
+ if (!host) break;
591
+ const hr = host.getBoundingClientRect();
592
+ x += hr.x; y += hr.y;
593
+ doc = host.ownerDocument;
594
+ }
595
+ return { x, y, width: r.width, height: r.height };
346
596
  })()
347
- `);
348
- if (!rect2) throw new Error(`Element not found: ${selector}`);
349
- return { nodeId: 0, rect: rect2 };
597
+ `).catch(() => null);
598
+ if (rect && rect.width > 0 && rect.height > 0) return { nodeId: 0, rect };
599
+ lastError = `Element not visible (zero size): ${selector}`;
600
+ lastError = `Element not found: ${selector}`;
601
+ await page.waitForTimeout(200);
350
602
  }
351
- const deadline2 = Date.now() + timeout;
352
- let lastError;
353
- let nodeId = 0;
354
- let rect = null;
355
- while (Date.now() < deadline2) {
356
- nodeId = await page.querySelector(selector);
357
- if (!nodeId) {
358
- lastError = `Element not found: ${selector}`;
359
- await page.waitForTimeout(200);
360
- continue;
361
- }
362
- rect = await page.getBoxModel(nodeId);
363
- if (rect) break;
364
- lastError = `Element has no box: ${selector}`;
365
- await page.waitForTimeout(500);
366
- }
367
- if (!rect) throw new Error(lastError || `Element has no box: ${selector}`);
368
- return { nodeId, rect };
603
+ throw new Error(lastError || `Element not found: ${selector}`);
369
604
  }
370
605
  const deadline = Date.now() + timeout;
371
606
  while (Date.now() < deadline) {
372
607
  const result = await checkActionable(page, selector);
373
608
  if (result.ok && result.rect) {
374
- const nodeId = await page.querySelector(selector);
375
- if (nodeId) return { nodeId, rect: result.rect };
609
+ const nodeId = await page.querySelector(selector).catch(() => 0) ?? 0;
610
+ return { nodeId, rect: result.rect };
376
611
  }
377
612
  await page.waitForTimeout(50);
378
613
  }
@@ -405,12 +640,25 @@ async function checkActionable(page, selector) {
405
640
  return { ok: false, reason: 'parent_disabled' };
406
641
  }
407
642
 
408
- // Check not covered by another element at center
643
+ // Check not covered by another element at center.
644
+ // elementFromPoint must run in the element's OWN document: for iframe-
645
+ // internal elements the main-document hit-test returns the <iframe>
646
+ // host itself, which falsely reports "covered" (rec-duel d01).
647
+ // For shadow-internal elements the hit-test retargets to the shadow
648
+ // HOST \u2014 walk the host chain before declaring coverage (rec-duel d04).
409
649
  const cx = rect.x + rect.width / 2;
410
650
  const cy = rect.y + rect.height / 2;
411
- const topEl = document.elementFromPoint(cx, cy);
651
+ const topEl = el.ownerDocument.elementFromPoint(cx, cy);
412
652
  if (topEl && topEl !== el && !el.contains(topEl) && !topEl.contains(el)) {
413
- return { ok: false, reason: 'covered', rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } };
653
+ let hostChain = [];
654
+ let rootNode = el.getRootNode();
655
+ while (rootNode && rootNode.host) {
656
+ hostChain.push(rootNode.host);
657
+ rootNode = rootNode.host.getRootNode();
658
+ }
659
+ if (!hostChain.includes(topEl)) {
660
+ return { ok: false, reason: 'covered', rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } };
661
+ }
414
662
  }
415
663
 
416
664
  return {
@@ -455,21 +703,48 @@ var XBLocatorImpl = class _XBLocatorImpl {
455
703
  const el = ${this._q(this.selector)};
456
704
  if (!el) return null;
457
705
  const rect = el.getBoundingClientRect();
458
- return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
706
+ let x = rect.x, y = rect.y;
707
+ let doc = el.ownerDocument;
708
+ while (doc !== document) {
709
+ let host = null;
710
+ const scan = (d) => {
711
+ let frames;
712
+ try { frames = d.querySelectorAll('iframe'); } catch (e) { return null; }
713
+ for (const f of frames) {
714
+ let inner = null;
715
+ try { inner = f.contentDocument; } catch (e) { continue; }
716
+ if (!inner) continue;
717
+ if (inner === doc) return f;
718
+ const r = scan(inner);
719
+ if (r) return r;
720
+ }
721
+ return null;
722
+ };
723
+ host = scan(document);
724
+ if (!host) break;
725
+ const hr = host.getBoundingClientRect();
726
+ x += hr.x; y += hr.y;
727
+ doc = host.ownerDocument;
728
+ }
729
+ return { x, y, width: rect.width, height: rect.height };
459
730
  })()
460
731
  `);
461
732
  const finalRect = updatedRect ?? rect;
462
733
  const cx = finalRect.x + finalRect.width / 2;
463
734
  const cy = finalRect.y + finalRect.height / 2;
464
735
  await this.page.mouse.click(cx, cy, {
465
- button: opts.button ?? "left",
466
- clickCount: opts.clickCount ?? 1,
467
- delay: opts.delay
736
+ stealth: true,
737
+ elementWidth: finalRect.width,
738
+ elementHeight: finalRect.height,
739
+ ...{ button: opts.button ?? "left", clickCount: opts.clickCount ?? 1, delay: opts.delay }
468
740
  });
469
741
  }
470
742
  async fill(value, opts = {}) {
471
743
  await waitForActionable(this.page, this.selector, opts);
472
744
  await scrollIntoView(this.page, this.selector);
745
+ await this.click({ ...opts });
746
+ await this.page.keyboard.type(value, { stealth: true });
747
+ return;
473
748
  await this.page.evaluate(`
474
749
  (function() {
475
750
  const el = ${this._q(this.selector)};
@@ -1179,6 +1454,8 @@ var XBPageImpl = class _XBPageImpl {
1179
1454
  await this.conn.send("Page.enable", void 0, this.sessionId);
1180
1455
  await this.conn.send("Runtime.enable", void 0, this.sessionId);
1181
1456
  await this.conn.send("Network.enable", void 0, this.sessionId);
1457
+ await this.conn.send("DOM.enable", void 0, this.sessionId).catch(() => {
1458
+ });
1182
1459
  this.setupPageEvents();
1183
1460
  this.setupNetworkEvents();
1184
1461
  this.setupConsoleEvents();
@@ -1191,6 +1468,9 @@ var XBPageImpl = class _XBPageImpl {
1191
1468
  );
1192
1469
  this._url = info.url;
1193
1470
  this._title = info.title;
1471
+ if (info.url && info.url !== "about:blank" && info.url !== "") {
1472
+ this._loadState = { loadFired: true, domContentFired: true, networkIdle: true };
1473
+ }
1194
1474
  } catch {
1195
1475
  }
1196
1476
  }
@@ -1203,6 +1483,16 @@ var XBPageImpl = class _XBPageImpl {
1203
1483
  const waitUntil = opts.waitUntil ?? "load";
1204
1484
  const timeout = opts.timeout ?? 3e4;
1205
1485
  this._loadState = { loadFired: false, domContentFired: false, networkIdle: false };
1486
+ if (process.env.XBROWSER_STEALTH !== "off") {
1487
+ try {
1488
+ await this.conn.send(
1489
+ "Page.addScriptToEvaluateOnNewDocument",
1490
+ { source: buildStealthInitScript() },
1491
+ this.sessionId
1492
+ );
1493
+ } catch {
1494
+ }
1495
+ }
1206
1496
  const result = await this.conn.send(
1207
1497
  "Page.navigate",
1208
1498
  { url, referrer: opts.referer },
@@ -1410,6 +1700,91 @@ Last error: ${lastError.message}` : "";
1410
1700
  }
1411
1701
  return result.result?.value;
1412
1702
  }
1703
+ /**
1704
+ * 在指定 iframe 上下文中执行表达式(攻防 D16 能力建设,2026-08-19)。
1705
+ *
1706
+ * 双路径:
1707
+ * 1. 同进程 iframe —— Runtime.enable 收集 executionContextCreated,
1708
+ * 找到目标 frameId 的 contextId,用 contextId 定向执行;
1709
+ * 2. 跨域 OOPIF(独立 target)—— Target.setAutoAttach(flatten) 监听
1710
+ * attachedToTarget 中 type==='iframe' 的会话,用其 sessionId 执行。
1711
+ *
1712
+ * 这绕过了页面同源策略(那是页面 JS 的约束,CDP 是调试通道)——
1713
+ * 支付窗/验证码/第三方嵌入内容的读写都靠它。
1714
+ */
1715
+ async evaluateInFrame(urlIncludes, expression) {
1716
+ if (this._closed) throw new Error("Page is closed");
1717
+ const evalIn = async (sessionId, contextId) => {
1718
+ const params = { expression, returnByValue: true, awaitPromise: true };
1719
+ if (contextId !== void 0) params.contextId = contextId;
1720
+ const result = await this.conn.send("Runtime.evaluate", params, sessionId);
1721
+ if (result.exceptionDetails) {
1722
+ const detail = result.exceptionDetails.exception?.description ?? result.exceptionDetails.exception?.value ?? result.exceptionDetails.text;
1723
+ throw new Error(`[frame ${urlIncludes}] ${detail}`);
1724
+ }
1725
+ return result.result?.value;
1726
+ };
1727
+ try {
1728
+ const tg = await this.conn.send("Target.getTargets", void 0);
1729
+ const hit2 = (tg.targetInfos || []).find((t) => t.type === "iframe" && (t.url || "").includes(urlIncludes));
1730
+ if (hit2) {
1731
+ const att = await this.conn.send("Target.attachToTarget", { targetId: hit2.targetId, flatten: true });
1732
+ return evalIn(att.sessionId);
1733
+ }
1734
+ } catch {
1735
+ }
1736
+ const tree = await this.conn.send("Page.getFrameTree", void 0, this.sessionId);
1737
+ const all = [];
1738
+ const walk = (node) => {
1739
+ all.push({ id: node.frame.id, url: node.frame.url });
1740
+ for (const child of node.childFrames || []) walk(child);
1741
+ };
1742
+ walk(tree.frameTree);
1743
+ const mainId = tree.frameTree?.frame?.id;
1744
+ const target = all.find((f) => f.id !== mainId && f.url.includes(urlIncludes));
1745
+ if (target) {
1746
+ const contexts = [];
1747
+ const onCtx = (raw) => {
1748
+ const c = raw?.context;
1749
+ if (c?.id && c?.auxData?.frameId) contexts.push({ id: c.id, frameId: c.auxData.frameId });
1750
+ };
1751
+ this.conn.on("Runtime.executionContextCreated", onCtx);
1752
+ try {
1753
+ await this.conn.send("Runtime.enable", void 0, this.sessionId).catch(() => {
1754
+ });
1755
+ await new Promise((r) => setTimeout(r, 400));
1756
+ } finally {
1757
+ this.conn.off("Runtime.executionContextCreated", onCtx);
1758
+ }
1759
+ const ctx = contexts.find((c) => c.frameId === target.id);
1760
+ if (ctx) return evalIn(this.sessionId, ctx.id);
1761
+ }
1762
+ const attached = [];
1763
+ const onAttach = (raw) => {
1764
+ const ev = raw;
1765
+ if (ev?.sessionId && ev.targetInfo?.type === "iframe") {
1766
+ attached.push({ sessionId: ev.sessionId, url: ev.targetInfo.url || "" });
1767
+ }
1768
+ };
1769
+ this.conn.on("Target.attachedToTarget", onAttach);
1770
+ try {
1771
+ await this.conn.send("Target.setAutoAttach", {
1772
+ autoAttach: true,
1773
+ waitForDebuggerOnStart: false,
1774
+ flatten: true
1775
+ }, this.sessionId);
1776
+ await new Promise((r) => setTimeout(r, 600));
1777
+ } finally {
1778
+ this.conn.off("Target.attachedToTarget", onAttach);
1779
+ this.conn.send("Target.setAutoAttach", { autoAttach: false, waitForDebuggerOnStart: false, flatten: true }, this.sessionId).catch(() => {
1780
+ });
1781
+ }
1782
+ const hit = attached.find((a) => a.url.includes(urlIncludes));
1783
+ if (!hit) {
1784
+ throw new Error(`frame not found for "${urlIncludes}"\uFF08OOPIF target\u3001frame \u6811\u3001auto-attach \u4E09\u8DEF\u5747\u672A\u547D\u4E2D\uFF1B\u53EF\u80FD\u4ECD\u5728\u52A0\u8F7D\uFF09`);
1785
+ }
1786
+ return evalIn(hit.sessionId);
1787
+ }
1413
1788
  /** evaluateHandle — evaluates fn and returns a handle for element bounding box */
1414
1789
  async evaluateHandle(fn, ...args) {
1415
1790
  let expression;
@@ -1780,16 +2155,21 @@ Last error: ${lastError.message}` : "";
1780
2155
  }
1781
2156
  return 1;
1782
2157
  }
1783
- const doc = await this.conn.send(
1784
- "DOM.getDocument",
1785
- { depth: 0 },
1786
- this.sessionId
2158
+ const withTimeout = (p, ms) => Promise.race([p, new Promise((r) => setTimeout(() => r(null), ms))]);
2159
+ const doc = await withTimeout(
2160
+ this.conn.send("DOM.getDocument", { depth: 0 }, this.sessionId),
2161
+ 8e3
1787
2162
  );
1788
- const result = await this.conn.send(
1789
- "DOM.querySelector",
1790
- { nodeId: doc.root.nodeId, selector },
1791
- this.sessionId
2163
+ if (!doc) return 0;
2164
+ const result = await withTimeout(
2165
+ this.conn.send(
2166
+ "DOM.querySelector",
2167
+ { nodeId: doc.root.nodeId, selector },
2168
+ this.sessionId
2169
+ ),
2170
+ 8e3
1792
2171
  );
2172
+ if (!result) return 0;
1793
2173
  return result.nodeId;
1794
2174
  }
1795
2175
  /** Query all matching elements, returns array of CDP nodeIds */