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