@xbrowser/cli 1.10.0 → 1.12.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 (42) hide show
  1. package/dist/{anti-bot-DR56Y63V.js → anti-bot-GTTYNEFB.js} +1 -1
  2. package/dist/{browser-WQZ3D6AE.js → browser-3HQKDZQ6.js} +4 -2
  3. package/dist/{browser-T3V3JWVH.js → browser-ILXTEWH4.js} +1 -1
  4. package/dist/{browser-HBL72GPZ.js → browser-XPDMY2NX.js} +3 -3
  5. package/dist/{cdp-driver-J3YQ4LJV.js → cdp-driver-ESH3PDE6.js} +2 -1
  6. package/dist/cdp-driver-N2E2ZNSC.js +4644 -0
  7. package/dist/cdp-driver-YIKR4BUX.js +49 -0
  8. package/dist/chunk-3FWLW7FS.js +106 -0
  9. package/dist/chunk-A6LPGFAL.js +437 -0
  10. package/dist/{chunk-RRBXV7KE.js → chunk-DKM2JOZL.js} +633 -63
  11. package/dist/{chunk-ABXMBNQ6.js → chunk-H2A5JUK5.js} +37 -466
  12. package/dist/{chunk-5UGR6MUK.js → chunk-HBHOKZPN.js} +51 -16
  13. package/dist/{chunk-BNO7OKO4.js → chunk-HBMEFSTB.js} +39 -0
  14. package/dist/{chunk-INTQPBYF.js → chunk-JKVUFP3G.js} +6 -2
  15. package/dist/chunk-KJTABK3Z.js +1255 -0
  16. package/dist/{chunk-DWDXEGVK.js → chunk-NKW4A74J.js} +9 -3
  17. package/dist/{chunk-4452SPFI.js → chunk-NODRQGOK.js} +51 -16
  18. package/dist/{chunk-SQHSZENE.js → chunk-OMU63E6J.js} +3 -22
  19. package/dist/{chunk-NITFVWWS.js → chunk-OVW2UEWN.js} +981 -407
  20. package/dist/{chunk-WSCP7QCJ.js → chunk-R6IJ6PIV.js} +14 -11
  21. package/dist/{chunk-YMUSHPU4.js → chunk-SH6OTPXN.js} +46 -16
  22. package/dist/{chunk-MWFHZUIY.js → chunk-TEXCXIBW.js} +1 -1
  23. package/dist/{cdp-driver-2T4P4ZE2.js → chunk-Z6CUR3VG.js} +1542 -134
  24. package/dist/{chunk-IIM5GOD7.js → chunk-ZTHE5RBZ.js} +7 -1
  25. package/dist/cli.js +1136 -584
  26. package/dist/{daemon-client-GKEPT4NY.js → daemon-client-7D6EZNOE.js} +46 -16
  27. package/dist/{daemon-client-O6BYVRXV.js → daemon-client-HEGAXWGK.js} +1 -1
  28. package/dist/daemon-main.js +1031 -522
  29. package/dist/{human-interaction-5AO42MBA.js → human-interaction-4YR2N6R6.js} +2 -2
  30. package/dist/{human-interaction-C5OEGXGO.js → human-interaction-Y5IDH6LD.js} +1 -1
  31. package/dist/{human-interaction-ISXZTAYY.js → human-interaction-ZUTR5AC2.js} +2 -2
  32. package/dist/index.d.ts +10 -0
  33. package/dist/index.js +1147 -593
  34. package/dist/{proxy-C6CK3UH5.js → proxy-LUR4U5YF.js} +2 -1
  35. package/dist/{recovery-J2ISVGUL.js → recovery-FTZGV6VY.js} +2 -2
  36. package/dist/{recovery-ZJVVHP7N.js → recovery-L5GLDX4O.js} +1 -1
  37. package/dist/{recovery-EF33LKRJ.js → recovery-Z3RDZENA.js} +2 -2
  38. package/dist/{session-recorder-H3KEYU26.js → session-recorder-3BEVWHOK.js} +1 -1
  39. package/dist/{session-recorder-QRZMKFVL.js → session-recorder-SLDBENVF.js} +1 -1
  40. package/dist/{session-replayer-LJUC4TI7.js → session-replayer-K4A6OI4M.js} +90 -2
  41. package/package.json +1 -1
  42. package/dist/chunk-BAHSRZIX.js +0 -2191
@@ -5,6 +5,9 @@ import {
5
5
  import {
6
6
  errMsg
7
7
  } from "./chunk-GDKLH7ZY.js";
8
+ import {
9
+ createRuleEngine
10
+ } from "./chunk-H2A5JUK5.js";
8
11
  import {
9
12
  __require
10
13
  } from "./chunk-KFQGP6VL.js";
@@ -18,6 +21,285 @@ import { EventEmitter as EventEmitter2 } from "events";
18
21
  // src/cdp-driver/page.ts
19
22
  import { EventEmitter } from "events";
20
23
 
24
+ // src/cdp-driver/stealth.ts
25
+ var DEFAULT_STEALTH_CONFIG = {
26
+ bezierCurvature: [0.35, 0.6],
27
+ noiseAmplitude: 5.5,
28
+ overshootRange: [6, 14],
29
+ aimPause: [150, 400],
30
+ pressDuration: [60, 140],
31
+ releaseDrift: [0.8, 2.5],
32
+ landingOffsetSmall: [0.3, 2.5],
33
+ landingOffsetLarge: [1.5, 7],
34
+ smallElementThreshold: 30,
35
+ typingRhythm: {
36
+ fastProb: 0.22,
37
+ fastRange: [25, 60],
38
+ normalRange: [50, 350],
39
+ pauseProb: 0.18,
40
+ pauseRange: [400, 1200]
41
+ },
42
+ keyPressDuration: [50, 110],
43
+ typoProbability: 0.06,
44
+ wheelPeak: 180,
45
+ wheelDecayRate: 0.4
46
+ };
47
+ function rand(min, max) {
48
+ return min + Math.random() * (max - min);
49
+ }
50
+ function cosineEase(t) {
51
+ return 0.5 - 0.5 * Math.cos(Math.PI * t);
52
+ }
53
+ function bezierTrajectory(x0, y0, x1, y1, config = DEFAULT_STEALTH_CONFIG) {
54
+ const dist = Math.hypot(x1 - x0, y1 - y0);
55
+ const n = Math.max(10, Math.min(28, Math.round(dist / 15)));
56
+ const shortMove = dist < 120;
57
+ const curvature = shortMove ? rand(2, 6) : Math.max(dist * rand(...config.bezierCurvature), rand(18, 35));
58
+ const dir = Math.random() < 0.5 ? 1 : -1;
59
+ const d = dist || 1;
60
+ const dx = x1 - x0, dy = y1 - y0;
61
+ const c1x = x0 + dx * 0.3 - dy / d * curvature * 0.5 * dir;
62
+ const c1y = y0 + dy * 0.3 + dx / d * curvature * 0.5 * dir;
63
+ const c2x = x0 + dx * 0.7 - dy / d * curvature * 0.8 * dir;
64
+ const c2y = y0 + dy * 0.7 + dx / d * curvature * 0.8 * dir;
65
+ const points = [];
66
+ for (let i = 1; i <= n; i++) {
67
+ const t = cosineEase(i / n);
68
+ const mt = 1 - t;
69
+ let px = mt ** 3 * x0 + 3 * mt ** 2 * t * c1x + 3 * mt * t ** 2 * c2x + t ** 3 * x1;
70
+ let py = mt ** 3 * y0 + 3 * mt ** 2 * t * c1y + 3 * mt * t ** 2 * c2y + t ** 3 * y1;
71
+ const amp = shortMove ? Math.min(2, config.noiseAmplitude) : config.noiseAmplitude;
72
+ px += rand(-amp, amp);
73
+ py += rand(-amp, amp);
74
+ points.push({ x: px, y: py, delay: rand(9, 16) });
75
+ }
76
+ if (!shortMove) {
77
+ const over = rand(...config.overshootRange);
78
+ const ox = x1 + dx / d * over + rand(-2, 2);
79
+ const oy = y1 + dy / d * over + rand(-2, 2);
80
+ points.push({ x: ox, y: oy, delay: rand(14, 30) });
81
+ points.push({
82
+ x: x1 + dx / d * over * 0.4,
83
+ y: y1 + dy / d * over * 0.4,
84
+ delay: rand(14, 30)
85
+ });
86
+ }
87
+ points.push({ x: x1 + rand(-1, 1), y: y1 + rand(-1, 1), delay: rand(14, 30) });
88
+ return points;
89
+ }
90
+ function landingOffset(width, height, config = DEFAULT_STEALTH_CONFIG) {
91
+ const isSmall = Math.min(width, height) < config.smallElementThreshold;
92
+ const range = isSmall ? config.landingOffsetSmall : config.landingOffsetLarge;
93
+ const dx = rand(...range) * (Math.random() < 0.5 ? -1 : 1);
94
+ const dy = rand(...range) * (Math.random() < 0.5 ? -1 : 1);
95
+ return { dx, dy };
96
+ }
97
+ var KEY_MAP = {};
98
+ for (let i = 97; i <= 122; i++) {
99
+ const ch = String.fromCharCode(i);
100
+ KEY_MAP[ch] = { key: ch, code: "Key" + ch.toUpperCase(), vk: i - 32 };
101
+ }
102
+ for (let i = 65; i <= 90; i++) {
103
+ const ch = String.fromCharCode(i);
104
+ KEY_MAP[ch] = { key: ch, code: "Key" + ch, vk: i, shift: true };
105
+ }
106
+ for (let i = 48; i <= 57; i++) {
107
+ const ch = String.fromCharCode(i);
108
+ KEY_MAP[ch] = { key: ch, code: "Digit" + ch, vk: i };
109
+ }
110
+ Object.assign(KEY_MAP, {
111
+ " ": { key: " ", code: "Space", vk: 32 },
112
+ ".": { key: ".", code: "Period", vk: 190 },
113
+ "-": { key: "-", code: "Minus", vk: 189 },
114
+ "@": { key: "@", code: "Digit2", vk: 50, shift: true },
115
+ "_": { key: "_", code: "Minus", vk: 189, shift: true }
116
+ });
117
+ function buildStealthInitScript() {
118
+ return [
119
+ "(function(){",
120
+ // 1. AEL event proxy
121
+ " var o=EventTarget.prototype.addEventListener;",
122
+ " var fc=new InputDeviceCapabilities({firesTouchEvents:false});",
123
+ " var _ael=function(t,f){",
124
+ " var op=arguments[2];",
125
+ ' if(typeof f!=="function")return o.call(this,t,f,op);',
126
+ " var w=function(e){",
127
+ " if(!e||e.constructor===FocusEvent||e.constructor===KeyboardEvent)return f.call(this,e);",
128
+ " return f.call(this,new Proxy(e,{get:function(k,p){",
129
+ ' if(p==="sourceCapabilities")return fc;',
130
+ ' if(p==="isTrusted")return k.isTrusted;',
131
+ ' if((p==="clientX"||p==="clientY")&&k.type==="click"&&Number.isInteger(k[p])&&k.isTrusted===true){',
132
+ " var _f=((k.timeStamp||Date.now())%89)/89*0.7+0.15;",
133
+ " return k[p]+_f;",
134
+ " }",
135
+ ' var v=Reflect.get(k,p);return typeof v==="function"?v.bind(k):v;',
136
+ " }}));",
137
+ " };",
138
+ " return o.call(this,t,w,op);",
139
+ " };",
140
+ " EventTarget.prototype.addEventListener=_ael;",
141
+ // 2. Screen override (prototype-level, not instance-level)
142
+ " var _gw=function(){return 1728};",
143
+ " var _gh=function(){return 1117};",
144
+ " var _gah=function(){return 1092};",
145
+ ' Object.defineProperty(Screen.prototype,"width",{get:_gw,configurable:true});',
146
+ ' Object.defineProperty(Screen.prototype,"height",{get:_gh,configurable:true});',
147
+ ' Object.defineProperty(Screen.prototype,"availWidth",{get:_gw,configurable:true});',
148
+ ' Object.defineProperty(Screen.prototype,"availHeight",{get:_gah,configurable:true});',
149
+ " document.hasFocus=function(){return true};",
150
+ // 4. Canvas/WebGL fingerprint: per-session stable noise (d20).
151
+ // Headless software raster differs subtly from Chrome GPU raster —
152
+ // toDataURL hashes fingerprint the rasterizer. Inject a stable
153
+ // (per-page-load) subpixel shift into fillText so hashes look like a
154
+ // distinct-but-consistent real GPU, and hide the HEADLESS tell in
155
+ // WebGL renderer strings.
156
+ " var _seed=Math.floor(Math.random()*2147483647);",
157
+ " var _prng=function(){_seed=(_seed*48271)%2147483647;return _seed/2147483647;};",
158
+ " var _dx=(_prng()*0.4-0.2).toFixed(3)*1, _dy=(_prng()*0.4-0.2).toFixed(3)*1;",
159
+ " var _fillText=CanvasRenderingContext2D.prototype.fillText;",
160
+ " CanvasRenderingContext2D.prototype.fillText=function(t,x,y,m){",
161
+ " return _fillText.call(this,t,x+_dx,y+_dy,m);",
162
+ " };",
163
+ " var _toDataURL=HTMLCanvasElement.prototype.toDataURL;",
164
+ " HTMLCanvasElement.prototype.toDataURL=function(){",
165
+ ' var ctx=this.getContext("2d");',
166
+ " if(ctx){var d=ctx.getImageData(0,0,Math.min(this.width,2),Math.min(this.height,2));",
167
+ " for(var i=0;i<d.data.length;i+=4){if(d.data[i+3]>0){d.data[i]^=1;break;}}",
168
+ " ctx.putImageData(d,0,0);}",
169
+ " return _toDataURL.apply(this,arguments);",
170
+ " };",
171
+ " try{",
172
+ " var _gl=HTMLCanvasElement.prototype.getContext;",
173
+ " HTMLCanvasElement.prototype.getContext=function(t,o){",
174
+ " var c=_gl.call(this,t,o);",
175
+ ' if(c&&(t==="webgl"||t==="experimental-webgl")&&c.getParameter){',
176
+ " var _gp=c.getParameter.bind(c);",
177
+ " c.getParameter=function(p){",
178
+ " var v=_gp(p);",
179
+ ' if(typeof v==="string"&&/SwiftShader|Software|Rasterizer|Headless/i.test(v))',
180
+ ' return "ANGLE (Apple, ANGLE Metal Renderer: Apple M2 Max, Unspecified Version)";',
181
+ " return v;};}",
182
+ " return c;};",
183
+ " }catch(e){}",
184
+ // 5. AudioContext fingerprint: per-load stable micro-noise on channel
185
+ // data (d21). DSP sum differences between headless software audio and
186
+ // real hardware audio are a classic fingerprint — add ±1e-7 level
187
+ // noise (inaudible, changes the sum hash).
188
+ " try{",
189
+ " var _gcd=AudioBuffer.prototype.getChannelData;",
190
+ " AudioBuffer.prototype.getChannelData=function(ch){",
191
+ " var d=_gcd.call(this,ch);",
192
+ ' var key="__xb_audio_"+ch;',
193
+ " if(!this[key]){",
194
+ " this[key]=true;",
195
+ " for(var i=0;i<d.length;i+=997){d[i]=d[i]+(_prng()-0.5)*2e-7;}",
196
+ " }",
197
+ " return d;",
198
+ " };",
199
+ " var _gffd=AnalyserNode.prototype.getFloatFrequencyData;",
200
+ " AnalyserNode.prototype.getFloatFrequencyData=function(arr){",
201
+ " _gffd.call(this,arr);",
202
+ " for(var i=0;i<arr.length;i+=31){arr[i]=arr[i]+(_prng()-0.5)*0.01;}",
203
+ " };",
204
+ " var _gfbd=AnalyserNode.prototype.getByteFrequencyData;",
205
+ " AnalyserNode.prototype.getByteFrequencyData=function(arr){",
206
+ " _gfbd.call(this,arr);",
207
+ " for(var i=0;i<arr.length;i+=31){arr[i]=(arr[i]+((_prng()*3)|0))&255;}",
208
+ " };",
209
+ " }catch(e){}",
210
+ // 3b. Font metrics + speechSynthesis + battery (d22): headless tells
211
+ " try{",
212
+ " var _mt=CanvasRenderingContext2D.prototype.measureText;",
213
+ ' var _tmw=Object.getOwnPropertyDescriptor(TextMetrics.prototype,"width");',
214
+ " CanvasRenderingContext2D.prototype.measureText=function(t){",
215
+ " var m=_mt.call(this,t);",
216
+ ' try{Object.defineProperty(m,"width",{get:function(){return _tmw.get.call(m)+_dx*0.01;},configurable:true});}catch(e){}',
217
+ " return m;",
218
+ " };",
219
+ " }catch(e){}",
220
+ " try{",
221
+ ' var _fakeVoices=["Alex","Daniel","Karen","Moira","Ralph","Samantha","Ting-Ting","Mei-Jia","Sinji","Yunda"];',
222
+ " var _sv=speechSynthesis.getVoices.bind(speechSynthesis);",
223
+ " speechSynthesis.getVoices=function(){",
224
+ " var real=_sv();",
225
+ " if(real&&real.length)return real;",
226
+ ' return _fakeVoices.map(function(n,i){return {name:n,lang:i<2?"en-US":i<6?"en-GB":"zh-TW",localService:true,default:i===0,voiceURI:n};});',
227
+ " };",
228
+ " }catch(e){}",
229
+ " try{",
230
+ " var _gb=navigator.getBattery.bind(navigator);",
231
+ " navigator.getBattery=function(){",
232
+ " return _gb().then(function(b){",
233
+ ' Object.defineProperty(b,"charging",{get:function(){return true;},configurable:true});',
234
+ " return b;});",
235
+ " };",
236
+ " }catch(e){}",
237
+ // 3c. WebRTC local IP leak guard (d23): headless STUN candidates can
238
+ // expose host IPs; mDNS-only candidates are the modern Chrome default.
239
+ " try{",
240
+ " var _oc=RTCPeerConnection.prototype.createOffer;",
241
+ " RTCPeerConnection.prototype.createOffer=function(){",
242
+ " var p=_oc.apply(this,arguments);",
243
+ " var self=this;",
244
+ " return p.then(function(offer){",
245
+ ' offer.sdp=offer.sdp.split(String.fromCharCode(10)).filter(function(l){return l.indexOf("typ host")<0}).join(String.fromCharCode(10));',
246
+ " return offer;});",
247
+ " };",
248
+ " }catch(e){}",
249
+ // 3d. Chrome object depth (d24): automation fakes usually only set
250
+ // window.chrome = {}; deep checks hit app.run/runtime/csi/loadTimes.
251
+ " try{",
252
+ " if(window.chrome){",
253
+ ' if(!window.chrome.app||!window.chrome.app.run)window.chrome.app={run:function(){},load:function(){},getDetails:function(){return null},InstallState:{DISABLED:"disabled",INSTALLED:"installed",NOT_INSTALLED:"not_installed"},RunningState:{CANNOT_RUN:"cannot_run",READY_TO_RUN:"ready_to_run",RUNNING:"running"}};',
254
+ ' if(!window.chrome.runtime)window.chrome.runtime={OnInstalledReason:{CHROME_UPDATE:"chrome_update",INSTALL:"install",UPDATE:"update"},PlatformOs:{ANDROID:"android",CROS:"cros",LINUX:"linux",MAC:"mac",OPENBSD:"openbsd",WIN:"win"},connect:function(){},sendMessage:function(){},id:undefined};',
255
+ " if(!window.chrome.csi)window.chrome.csi=function(){return{};}",
256
+ " if(!window.chrome.loadTimes)window.chrome.loadTimes=function(){return{};}",
257
+ " }",
258
+ " }catch(e){}",
259
+ // 3e. Font availability patch (d25): headless Chromium misses some system
260
+ // fonts (e.g. Menlo on macOS) that real Chrome has. Register a FontFace
261
+ // aliasing the missing font to a local() equivalent so offsetWidth-based
262
+ // font probing sees the same availability as the faked environment.
263
+ " try{",
264
+ ' var _fontAliases=[["Menlo","Courier New"],["SF Mono","Menlo"],["Segoe UI","Helvetica"]];',
265
+ " _fontAliases.forEach(function(pa){",
266
+ ' try{var ff=new FontFace(pa[0],"local("+JSON.stringify(pa[1])+")");',
267
+ " document.fonts.add(ff);ff.load();}catch(e2){}",
268
+ " });",
269
+ " }catch(e){}",
270
+ // 3. toString disguise (name-list based)
271
+ " var _ts=Function.prototype.toString;",
272
+ " var _hf=document.hasFocus;",
273
+ " Function.prototype.toString=function(){",
274
+ ' if(this===_ael)return"function addEventListener(type, callback) { [native code] }";',
275
+ ' if(this===_hf)return"function hasFocus() { [native code] }";',
276
+ ' if(this===_gw)return"function get width() { [native code] }";',
277
+ ' if(this===_gh)return"function get height() { [native code] }";',
278
+ ' if(this===_gah)return"function get availHeight() { [native code] }";',
279
+ ' if(this===CanvasRenderingContext2D.prototype.fillText)return"function fillText() { [native code] }";',
280
+ ' if(this===HTMLCanvasElement.prototype.toDataURL)return"function toDataURL() { [native code] }";',
281
+ ' if(this===AnalyserNode.prototype.getFloatFrequencyData)return"function getFloatFrequencyData() { [native code] }";',
282
+ ' if(this===AudioBuffer.prototype.getChannelData)return"function getChannelData() { [native code] }";',
283
+ " return _ts.call(this);",
284
+ " };",
285
+ // 4. onclick prototype hijack (dual-stream consistency)
286
+ " var _ba=function(k,p){",
287
+ ' if(p==="isTrusted")return k.isTrusted;',
288
+ ' if((p==="clientX"||p==="clientY")&&k.type==="click"&&Number.isInteger(k[p])&&k.isTrusted===true){',
289
+ " var _f=((k.timeStamp||Date.now())%89)/89*0.7+0.15;return k[p]+_f;",
290
+ " }",
291
+ ' var v=Reflect.get(k,p);return typeof v==="function"?v.bind(k):v;',
292
+ " };",
293
+ ' Object.defineProperty(Document.prototype,"onclick",{',
294
+ " configurable:true,",
295
+ " get:function(){var raw=this.__ocRaw||null;if(!raw)return null;var self=this;",
296
+ " return function(e){return raw.call(self,new Proxy(e,{get:function(k,p){return _ba(k,p)}}))}},",
297
+ " set:function(fn){this.__ocRaw=fn}",
298
+ " });",
299
+ "})()"
300
+ ].join("\n");
301
+ }
302
+
21
303
  // src/cdp-driver/mouse.ts
22
304
  var XBMouseImpl = class {
23
305
  conn;
@@ -39,18 +321,46 @@ var XBMouseImpl = class {
39
321
  }
40
322
  async click(x, y, opts = {}) {
41
323
  const button = opts.button ?? "left";
42
- const clickCount = opts.clickCount ?? 1;
43
- const delay = opts.delay ?? 0;
44
- await this.move(x, y);
45
- await this.down({ button });
46
- if (delay > 0) {
47
- await sleep(delay);
324
+ const stealth = opts.stealth ?? process.env.XBROWSER_STEALTH !== "off";
325
+ let tx = x, ty = y;
326
+ if (stealth && opts.elementWidth !== void 0 && opts.elementHeight !== void 0) {
327
+ const off = landingOffset(opts.elementWidth, opts.elementHeight);
328
+ tx += off.dx;
329
+ ty += off.dy;
330
+ }
331
+ if (stealth) {
332
+ const traj = bezierTrajectory(this._x, this._y, tx, ty);
333
+ const _tb = Date.now();
334
+ let _truncated = false;
335
+ for (const p of traj) {
336
+ if (Date.now() - _tb > 5e3) {
337
+ _truncated = true;
338
+ break;
339
+ }
340
+ await this.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: p.x, y: p.y, button: this._button });
341
+ this._x = p.x;
342
+ this._y = p.y;
343
+ await sleep(p.delay);
344
+ }
345
+ this._x = tx;
346
+ this._y = ty;
347
+ if (_truncated) {
348
+ await this.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: tx, y: ty, button: this._button });
349
+ }
350
+ await sleep(rand(...DEFAULT_STEALTH_CONFIG.aimPause));
351
+ } else {
352
+ await this.move(tx, ty);
48
353
  }
49
- await this.up({ button });
50
- for (let i = 1; i < clickCount; i++) {
51
- if (delay > 0) await sleep(delay);
354
+ await this.down({ button });
355
+ await sleep(stealth ? rand(...DEFAULT_STEALTH_CONFIG.pressDuration) : opts.delay ?? 0);
356
+ const rx = stealth ? this._x + rand(...DEFAULT_STEALTH_CONFIG.releaseDrift) * (Math.random() < 0.5 ? -1 : 1) : this._x;
357
+ const ry = stealth ? this._y + rand(...DEFAULT_STEALTH_CONFIG.releaseDrift) * (Math.random() < 0.5 ? -1 : 1) : this._y;
358
+ this._x = rx;
359
+ this._y = ry;
360
+ await this.send("Input.dispatchMouseEvent", { type: "mouseReleased", x: rx, y: ry, button, clickCount: opts.clickCount ?? 1 });
361
+ for (let i = 1; i < (opts.clickCount ?? 1); i++) {
362
+ if (opts.delay) await sleep(opts.delay);
52
363
  await this.down({ button });
53
- if (delay > 0) await sleep(delay);
54
364
  await this.up({ button });
55
365
  }
56
366
  }
@@ -141,12 +451,20 @@ var XBKeyboardImpl = class {
141
451
  }
142
452
  await this.dispatchKeyEvent(downParams);
143
453
  if (mapping.text) {
454
+ if (process.env.XBROWSER_STEALTH !== "off") {
455
+ await sleep2(rand(...DEFAULT_STEALTH_CONFIG.keyPressDuration));
456
+ }
144
457
  await this.dispatchKeyEvent({
145
458
  type: "char",
146
459
  text: mapping.text
147
460
  });
148
461
  }
149
462
  if (delay > 0) await sleep2(delay);
463
+ else {
464
+ if (process.env.XBROWSER_STEALTH !== "off") {
465
+ await sleep2(rand(...DEFAULT_STEALTH_CONFIG.keyPressDuration));
466
+ }
467
+ }
150
468
  const upParams = {
151
469
  type: "keyUp",
152
470
  key: mapping.key,
@@ -229,7 +547,7 @@ var XBKeyboardImpl = class {
229
547
  }
230
548
  };
231
549
  function resolveKeyMapping(key) {
232
- if (KEY_MAP[key]) return KEY_MAP[key];
550
+ if (KEY_MAP2[key]) return KEY_MAP2[key];
233
551
  if (key.length === 1) {
234
552
  const lower = key.toLowerCase();
235
553
  if (lower >= "a" && lower <= "z") {
@@ -246,7 +564,7 @@ function resolveKeyMapping(key) {
246
564
  }
247
565
  return { key, code: key };
248
566
  }
249
- var KEY_MAP = {
567
+ var KEY_MAP2 = {
250
568
  Enter: { key: "Enter", code: "Enter", text: "\r", keyCode: 13 },
251
569
  Tab: { key: "Tab", code: "Tab", text: " ", keyCode: 9 },
252
570
  Escape: { key: "Escape", code: "Escape", keyCode: 27 },
@@ -284,6 +602,37 @@ function sleep2(ms) {
284
602
 
285
603
  // src/cdp-driver/selector-utils.ts
286
604
  function queryJS(selector) {
605
+ return `(${deepQueryIIFE})( ${JSON.stringify(queryMainJS(selector))} )`;
606
+ }
607
+ var deepQueryIIFE = `(function(mainExpr) {
608
+ const run = (root) => {
609
+ try { return new Function('document', 'return (' + mainExpr + ')')(root); }
610
+ catch (e) { return null; }
611
+ };
612
+ const scanRoot = (root) => {
613
+ const direct = run(root);
614
+ if (direct) return direct;
615
+ let all;
616
+ try { all = root.querySelectorAll('*'); } catch (e) { return null; }
617
+ for (const el of all) {
618
+ if (el.shadowRoot) {
619
+ const r = scanRoot(el.shadowRoot);
620
+ if (r) return r;
621
+ }
622
+ if (el.tagName === 'IFRAME') {
623
+ let inner = null;
624
+ try { inner = el.contentDocument; } catch (e) { /* cross-origin */ }
625
+ if (inner) {
626
+ const r = scanRoot(inner);
627
+ if (r) return r;
628
+ }
629
+ }
630
+ }
631
+ return null;
632
+ };
633
+ return scanRoot(document);
634
+ })`;
635
+ function queryMainJS(selector) {
287
636
  if (selector.startsWith("xpath=")) {
288
637
  const xpath = JSON.stringify(selector.slice(6));
289
638
  return `document.evaluate(${xpath}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue`;
@@ -295,13 +644,35 @@ function queryJS(selector) {
295
644
  return `(() => {
296
645
  const target = ${JSON.stringify(text)};
297
646
  const exact = ${exact};
647
+ // Match on OWN text nodes (not strict leaf elements): search-result
648
+ // titles mix text with inline highlight <em> marks \u2014 a strict leaf filter
649
+ // finds nothing there (real-world juejin). Own-text keeps the match
650
+ // precise (descendant-only text doesn't count) while tolerating markup.
651
+ const ownText = (e) => Array.prototype.filter.call(e.childNodes, (n) => n.nodeType === 3)
652
+ .map((n) => n.textContent).join('').trim();
298
653
  const els = [...document.querySelectorAll('*')].filter(e => {
299
- if (e.children.length > 0) return false;
300
- if (e.offsetParent === null) return false;
301
- const t = (e.textContent || '').trim();
654
+ if (e.offsetParent === null && e.tagName !== 'BODY') return false;
655
+ const t = ownText(e);
302
656
  if (!t) return false;
303
657
  return exact ? t === target : t.toLowerCase().includes(target.toLowerCase());
304
658
  });
659
+ // Rank instead of raw DOM order: exact text beats substring, interactive
660
+ // elements (button/a/[onclick]/inputs) beat prose. Prevents matching a
661
+ // description paragraph that merely MENTIONS the target label
662
+ // (rec-duel d06: header text "\u76EE\u6807\u9879\u300C\u7B2C 87 \u53F7\u300D" hijacked text=\u7B2C 87 \u53F7).
663
+ const isInteractive = (e) => {
664
+ const tag = e.tagName;
665
+ return tag === 'BUTTON' || tag === 'A' || tag === 'INPUT' || tag === 'SELECT'
666
+ || e.hasAttribute('onclick') || e.getAttribute('role') === 'button';
667
+ };
668
+ els.sort((a, b) => {
669
+ const ta = ownText(a), tb = ownText(b);
670
+ const ea = ta === target ? 0 : 1, eb = tb === target ? 0 : 1;
671
+ if (ea !== eb) return ea - eb;
672
+ const ia = isInteractive(a) ? 0 : 1, ib = isInteractive(b) ? 0 : 1;
673
+ if (ia !== ib) return ia - ib;
674
+ return 0; // stable \u2014 preserve DOM order
675
+ });
305
676
  return els[0] || null;
306
677
  })()`;
307
678
  }
@@ -335,43 +706,53 @@ function queryAllJS(selector) {
335
706
  async function waitForActionable(page, selector, opts = {}) {
336
707
  const timeout = opts.timeout ?? 3e4;
337
708
  if (opts.force) {
338
- if (selector.startsWith("xpath=")) {
339
- const rect2 = await page.evaluate(`
709
+ const deadline2 = Date.now() + timeout;
710
+ let lastError;
711
+ while (Date.now() < deadline2) {
712
+ const rect = await page.evaluate(`
340
713
  (function() {
341
714
  const el = ${queryJS(selector)};
342
715
  if (!el) return null;
343
716
  const r = el.getBoundingClientRect();
344
- return { x: r.x, y: r.y, width: r.width, height: r.height };
717
+ let x = r.x, y = r.y;
718
+ let doc = el.ownerDocument;
719
+ while (doc !== document) {
720
+ let host = null;
721
+ const scan = (d) => {
722
+ let frames;
723
+ try { frames = d.querySelectorAll('iframe'); } catch (e) { return null; }
724
+ for (const f of frames) {
725
+ let inner = null;
726
+ try { inner = f.contentDocument; } catch (e) { continue; }
727
+ if (!inner) continue;
728
+ if (inner === doc) return f;
729
+ const rr = scan(inner);
730
+ if (rr) return rr;
731
+ }
732
+ return null;
733
+ };
734
+ host = scan(document);
735
+ if (!host) break;
736
+ const hr = host.getBoundingClientRect();
737
+ x += hr.x; y += hr.y;
738
+ doc = host.ownerDocument;
739
+ }
740
+ return { x, y, width: r.width, height: r.height };
345
741
  })()
346
- `);
347
- if (!rect2) throw new Error(`Element not found: ${selector}`);
348
- return { nodeId: 0, rect: rect2 };
349
- }
350
- const deadline2 = Date.now() + timeout;
351
- let lastError;
352
- let nodeId = 0;
353
- let rect = null;
354
- while (Date.now() < deadline2) {
355
- nodeId = await page.querySelector(selector);
356
- if (!nodeId) {
357
- lastError = `Element not found: ${selector}`;
358
- await page.waitForTimeout(200);
359
- continue;
360
- }
361
- rect = await page.getBoxModel(nodeId);
362
- if (rect) break;
363
- lastError = `Element has no box: ${selector}`;
364
- await page.waitForTimeout(500);
742
+ `).catch(() => null);
743
+ if (rect && rect.width > 0 && rect.height > 0) return { nodeId: 0, rect };
744
+ lastError = `Element not visible (zero size): ${selector}`;
745
+ lastError = `Element not found: ${selector}`;
746
+ await page.waitForTimeout(200);
365
747
  }
366
- if (!rect) throw new Error(lastError || `Element has no box: ${selector}`);
367
- return { nodeId, rect };
748
+ throw new Error(lastError || `Element not found: ${selector}`);
368
749
  }
369
750
  const deadline = Date.now() + timeout;
370
751
  while (Date.now() < deadline) {
371
752
  const result = await checkActionable(page, selector);
372
753
  if (result.ok && result.rect) {
373
- const nodeId = await page.querySelector(selector);
374
- if (nodeId) return { nodeId, rect: result.rect };
754
+ const nodeId = await page.querySelector(selector).catch(() => 0) ?? 0;
755
+ return { nodeId, rect: result.rect };
375
756
  }
376
757
  await page.waitForTimeout(50);
377
758
  }
@@ -404,12 +785,25 @@ async function checkActionable(page, selector) {
404
785
  return { ok: false, reason: 'parent_disabled' };
405
786
  }
406
787
 
407
- // Check not covered by another element at center
788
+ // Check not covered by another element at center.
789
+ // elementFromPoint must run in the element's OWN document: for iframe-
790
+ // internal elements the main-document hit-test returns the <iframe>
791
+ // host itself, which falsely reports "covered" (rec-duel d01).
792
+ // For shadow-internal elements the hit-test retargets to the shadow
793
+ // HOST \u2014 walk the host chain before declaring coverage (rec-duel d04).
408
794
  const cx = rect.x + rect.width / 2;
409
795
  const cy = rect.y + rect.height / 2;
410
- const topEl = document.elementFromPoint(cx, cy);
796
+ const topEl = el.ownerDocument.elementFromPoint(cx, cy);
411
797
  if (topEl && topEl !== el && !el.contains(topEl) && !topEl.contains(el)) {
412
- return { ok: false, reason: 'covered', rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } };
798
+ let hostChain = [];
799
+ let rootNode = el.getRootNode();
800
+ while (rootNode && rootNode.host) {
801
+ hostChain.push(rootNode.host);
802
+ rootNode = rootNode.host.getRootNode();
803
+ }
804
+ if (!hostChain.includes(topEl)) {
805
+ return { ok: false, reason: 'covered', rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } };
806
+ }
413
807
  }
414
808
 
415
809
  return {
@@ -454,21 +848,48 @@ var XBLocatorImpl = class _XBLocatorImpl {
454
848
  const el = ${this._q(this.selector)};
455
849
  if (!el) return null;
456
850
  const rect = el.getBoundingClientRect();
457
- return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
851
+ let x = rect.x, y = rect.y;
852
+ let doc = el.ownerDocument;
853
+ while (doc !== document) {
854
+ let host = null;
855
+ const scan = (d) => {
856
+ let frames;
857
+ try { frames = d.querySelectorAll('iframe'); } catch (e) { return null; }
858
+ for (const f of frames) {
859
+ let inner = null;
860
+ try { inner = f.contentDocument; } catch (e) { continue; }
861
+ if (!inner) continue;
862
+ if (inner === doc) return f;
863
+ const r = scan(inner);
864
+ if (r) return r;
865
+ }
866
+ return null;
867
+ };
868
+ host = scan(document);
869
+ if (!host) break;
870
+ const hr = host.getBoundingClientRect();
871
+ x += hr.x; y += hr.y;
872
+ doc = host.ownerDocument;
873
+ }
874
+ return { x, y, width: rect.width, height: rect.height };
458
875
  })()
459
876
  `);
460
877
  const finalRect = updatedRect ?? rect;
461
878
  const cx = finalRect.x + finalRect.width / 2;
462
879
  const cy = finalRect.y + finalRect.height / 2;
463
880
  await this.page.mouse.click(cx, cy, {
464
- button: opts.button ?? "left",
465
- clickCount: opts.clickCount ?? 1,
466
- delay: opts.delay
881
+ stealth: true,
882
+ elementWidth: finalRect.width,
883
+ elementHeight: finalRect.height,
884
+ ...{ button: opts.button ?? "left", clickCount: opts.clickCount ?? 1, delay: opts.delay }
467
885
  });
468
886
  }
469
887
  async fill(value, opts = {}) {
470
888
  await waitForActionable(this.page, this.selector, opts);
471
889
  await scrollIntoView(this.page, this.selector);
890
+ await this.click({ ...opts });
891
+ await this.page.keyboard.type(value, { stealth: true });
892
+ return;
472
893
  await this.page.evaluate(`
473
894
  (function() {
474
895
  const el = ${this._q(this.selector)};
@@ -1178,6 +1599,8 @@ var XBPageImpl = class _XBPageImpl {
1178
1599
  await this.conn.send("Page.enable", void 0, this.sessionId);
1179
1600
  await this.conn.send("Runtime.enable", void 0, this.sessionId);
1180
1601
  await this.conn.send("Network.enable", void 0, this.sessionId);
1602
+ await this.conn.send("DOM.enable", void 0, this.sessionId).catch(() => {
1603
+ });
1181
1604
  this.setupPageEvents();
1182
1605
  this.setupNetworkEvents();
1183
1606
  this.setupConsoleEvents();
@@ -1190,6 +1613,9 @@ var XBPageImpl = class _XBPageImpl {
1190
1613
  );
1191
1614
  this._url = info.url;
1192
1615
  this._title = info.title;
1616
+ if (info.url && info.url !== "about:blank" && info.url !== "") {
1617
+ this._loadState = { loadFired: true, domContentFired: true, networkIdle: true };
1618
+ }
1193
1619
  } catch {
1194
1620
  }
1195
1621
  }
@@ -1202,6 +1628,16 @@ var XBPageImpl = class _XBPageImpl {
1202
1628
  const waitUntil = opts.waitUntil ?? "load";
1203
1629
  const timeout = opts.timeout ?? 3e4;
1204
1630
  this._loadState = { loadFired: false, domContentFired: false, networkIdle: false };
1631
+ if (process.env.XBROWSER_STEALTH !== "off") {
1632
+ try {
1633
+ await this.conn.send(
1634
+ "Page.addScriptToEvaluateOnNewDocument",
1635
+ { source: buildStealthInitScript() },
1636
+ this.sessionId
1637
+ );
1638
+ } catch {
1639
+ }
1640
+ }
1205
1641
  const result = await this.conn.send(
1206
1642
  "Page.navigate",
1207
1643
  { url, referrer: opts.referer },
@@ -1409,6 +1845,91 @@ Last error: ${lastError.message}` : "";
1409
1845
  }
1410
1846
  return result.result?.value;
1411
1847
  }
1848
+ /**
1849
+ * 在指定 iframe 上下文中执行表达式(攻防 D16 能力建设,2026-08-19)。
1850
+ *
1851
+ * 双路径:
1852
+ * 1. 同进程 iframe —— Runtime.enable 收集 executionContextCreated,
1853
+ * 找到目标 frameId 的 contextId,用 contextId 定向执行;
1854
+ * 2. 跨域 OOPIF(独立 target)—— Target.setAutoAttach(flatten) 监听
1855
+ * attachedToTarget 中 type==='iframe' 的会话,用其 sessionId 执行。
1856
+ *
1857
+ * 这绕过了页面同源策略(那是页面 JS 的约束,CDP 是调试通道)——
1858
+ * 支付窗/验证码/第三方嵌入内容的读写都靠它。
1859
+ */
1860
+ async evaluateInFrame(urlIncludes, expression) {
1861
+ if (this._closed) throw new Error("Page is closed");
1862
+ const evalIn = async (sessionId, contextId) => {
1863
+ const params = { expression, returnByValue: true, awaitPromise: true };
1864
+ if (contextId !== void 0) params.contextId = contextId;
1865
+ const result = await this.conn.send("Runtime.evaluate", params, sessionId);
1866
+ if (result.exceptionDetails) {
1867
+ const detail = result.exceptionDetails.exception?.description ?? result.exceptionDetails.exception?.value ?? result.exceptionDetails.text;
1868
+ throw new Error(`[frame ${urlIncludes}] ${detail}`);
1869
+ }
1870
+ return result.result?.value;
1871
+ };
1872
+ try {
1873
+ const tg = await this.conn.send("Target.getTargets", void 0);
1874
+ const hit2 = (tg.targetInfos || []).find((t) => t.type === "iframe" && (t.url || "").includes(urlIncludes));
1875
+ if (hit2) {
1876
+ const att = await this.conn.send("Target.attachToTarget", { targetId: hit2.targetId, flatten: true });
1877
+ return evalIn(att.sessionId);
1878
+ }
1879
+ } catch {
1880
+ }
1881
+ const tree = await this.conn.send("Page.getFrameTree", void 0, this.sessionId);
1882
+ const all = [];
1883
+ const walk = (node) => {
1884
+ all.push({ id: node.frame.id, url: node.frame.url });
1885
+ for (const child of node.childFrames || []) walk(child);
1886
+ };
1887
+ walk(tree.frameTree);
1888
+ const mainId = tree.frameTree?.frame?.id;
1889
+ const target = all.find((f) => f.id !== mainId && f.url.includes(urlIncludes));
1890
+ if (target) {
1891
+ const contexts = [];
1892
+ const onCtx = (raw) => {
1893
+ const c = raw?.context;
1894
+ if (c?.id && c?.auxData?.frameId) contexts.push({ id: c.id, frameId: c.auxData.frameId });
1895
+ };
1896
+ this.conn.on("Runtime.executionContextCreated", onCtx);
1897
+ try {
1898
+ await this.conn.send("Runtime.enable", void 0, this.sessionId).catch(() => {
1899
+ });
1900
+ await new Promise((r) => setTimeout(r, 400));
1901
+ } finally {
1902
+ this.conn.off("Runtime.executionContextCreated", onCtx);
1903
+ }
1904
+ const ctx = contexts.find((c) => c.frameId === target.id);
1905
+ if (ctx) return evalIn(this.sessionId, ctx.id);
1906
+ }
1907
+ const attached = [];
1908
+ const onAttach = (raw) => {
1909
+ const ev = raw;
1910
+ if (ev?.sessionId && ev.targetInfo?.type === "iframe") {
1911
+ attached.push({ sessionId: ev.sessionId, url: ev.targetInfo.url || "" });
1912
+ }
1913
+ };
1914
+ this.conn.on("Target.attachedToTarget", onAttach);
1915
+ try {
1916
+ await this.conn.send("Target.setAutoAttach", {
1917
+ autoAttach: true,
1918
+ waitForDebuggerOnStart: false,
1919
+ flatten: true
1920
+ }, this.sessionId);
1921
+ await new Promise((r) => setTimeout(r, 600));
1922
+ } finally {
1923
+ this.conn.off("Target.attachedToTarget", onAttach);
1924
+ this.conn.send("Target.setAutoAttach", { autoAttach: false, waitForDebuggerOnStart: false, flatten: true }, this.sessionId).catch(() => {
1925
+ });
1926
+ }
1927
+ const hit = attached.find((a) => a.url.includes(urlIncludes));
1928
+ if (!hit) {
1929
+ 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`);
1930
+ }
1931
+ return evalIn(hit.sessionId);
1932
+ }
1412
1933
  /** evaluateHandle — evaluates fn and returns a handle for element bounding box */
1413
1934
  async evaluateHandle(fn, ...args) {
1414
1935
  let expression;
@@ -1653,6 +2174,10 @@ Last error: ${lastError.message}` : "";
1653
2174
  this.sessionId
1654
2175
  );
1655
2176
  }
2177
+ /** 覆盖 User-Agent(headless 环境对抗 UA 检测的标配能力) */
2178
+ async setUserAgent(userAgent) {
2179
+ await this._setUserAgent(userAgent);
2180
+ }
1656
2181
  /** Internal: set extra HTTP headers */
1657
2182
  async _setExtraHTTPHeaders(headers) {
1658
2183
  await this.setExtraHTTPHeaders(headers);
@@ -1779,16 +2304,21 @@ Last error: ${lastError.message}` : "";
1779
2304
  }
1780
2305
  return 1;
1781
2306
  }
1782
- const doc = await this.conn.send(
1783
- "DOM.getDocument",
1784
- { depth: 0 },
1785
- this.sessionId
2307
+ const withTimeout = (p, ms) => Promise.race([p, new Promise((r) => setTimeout(() => r(null), ms))]);
2308
+ const doc = await withTimeout(
2309
+ this.conn.send("DOM.getDocument", { depth: 0 }, this.sessionId),
2310
+ 8e3
1786
2311
  );
1787
- const result = await this.conn.send(
1788
- "DOM.querySelector",
1789
- { nodeId: doc.root.nodeId, selector },
1790
- this.sessionId
2312
+ if (!doc) return 0;
2313
+ const result = await withTimeout(
2314
+ this.conn.send(
2315
+ "DOM.querySelector",
2316
+ { nodeId: doc.root.nodeId, selector },
2317
+ this.sessionId
2318
+ ),
2319
+ 8e3
1791
2320
  );
2321
+ if (!result) return 0;
1792
2322
  return result.nodeId;
1793
2323
  }
1794
2324
  /** Query all matching elements, returns array of CDP nodeIds */
@@ -2777,9 +3307,28 @@ var XBBrowserImpl = class {
2777
3307
  // src/cdp-driver/connection.ts
2778
3308
  import { EventEmitter as EventEmitter4 } from "events";
2779
3309
  import { WebSocket } from "ws";
2780
- var CDPConnection = class extends EventEmitter4 {
3310
+ var CDPConnection = class _CDPConnection extends EventEmitter4 {
2781
3311
  ws;
2782
3312
  nextId = 1;
3313
+ /** CDP Guard:全部出站命令过规则引擎(env XBROWSER_CDP_GUARD=off 关闭)。
3314
+ * 173 条规则 / 9 模块:合成事件 block、指纹暴露警告、自动化信号拦截。 */
3315
+ static __guard;
3316
+ get guard() {
3317
+ if (_CDPConnection.__guard === void 0) {
3318
+ if (process.env.XBROWSER_CDP_GUARD === "off") {
3319
+ _CDPConnection.__guard = null;
3320
+ } else {
3321
+ try {
3322
+ const g = createRuleEngine();
3323
+ g.start();
3324
+ _CDPConnection.__guard = g;
3325
+ } catch {
3326
+ _CDPConnection.__guard = null;
3327
+ }
3328
+ }
3329
+ }
3330
+ return _CDPConnection.__guard;
3331
+ }
2783
3332
  pending = /* @__PURE__ */ new Map();
2784
3333
  closed = false;
2785
3334
  closeReason = null;
@@ -2803,6 +3352,8 @@ var CDPConnection = class extends EventEmitter4 {
2803
3352
  * ping, the connection is considered dead and forcibly closed. */
2804
3353
  keepaliveTimer = null;
2805
3354
  pongTimer = null;
3355
+ /** 最近一次收到任何 WS 消息的时间——有数据流动即证明连接存活 */
3356
+ lastIncomingAt = Date.now();
2806
3357
  startKeepalive() {
2807
3358
  this.ws.on("pong", () => {
2808
3359
  if (this.pongTimer) {
@@ -2814,9 +3365,14 @@ var CDPConnection = class extends EventEmitter4 {
2814
3365
  if (this.ws.readyState === WebSocket.OPEN) {
2815
3366
  if (!this.pongTimer) {
2816
3367
  this.pongTimer = setTimeout(() => {
3368
+ const recentlyActive = Date.now() - this.lastIncomingAt < 3e4;
3369
+ if (recentlyActive) {
3370
+ this.pongTimer = null;
3371
+ return;
3372
+ }
2817
3373
  if (!this.closed) {
2818
3374
  this.closed = true;
2819
- this.closeReason = "keepalive timeout (no pong in 10s)";
3375
+ this.closeReason = "keepalive timeout (no pong and idle >30s)";
2820
3376
  try {
2821
3377
  this.ws.terminate();
2822
3378
  } catch {
@@ -2828,7 +3384,7 @@ var CDPConnection = class extends EventEmitter4 {
2828
3384
  this.pending.clear();
2829
3385
  this.emit("disconnect");
2830
3386
  }
2831
- }, 1e4);
3387
+ }, 6e4);
2832
3388
  }
2833
3389
  this.ws.ping?.();
2834
3390
  } else if (this.closed) {
@@ -2866,10 +3422,23 @@ var CDPConnection = class extends EventEmitter4 {
2866
3422
  * @param method — CDP domain.method (e.g. "Page.navigate")
2867
3423
  * @param params — method parameters
2868
3424
  * @param sessionId — optional flat session ID for sub-targets
2869
- * @param timeoutMs — response timeout (default: 30s)
3425
+ * @param timeoutMs — response timeout (default: 30s;重 SPA 加载期间主线程忙,
3426
+ * evaluate 可排队 >30s——Runtime.evaluate 单独放宽到 90s)
2870
3427
  * @returns the `result` field from the CDP response
2871
3428
  */
2872
- async send(method, params, sessionId, timeoutMs = 3e4) {
3429
+ async send(method, params, sessionId, timeoutMs) {
3430
+ const effectiveTimeout = timeoutMs ?? (method === "Runtime.evaluate" ? 9e4 : 3e4);
3431
+ if (this.guard) {
3432
+ const decision = this.guard.evaluate({
3433
+ method,
3434
+ params: params ?? {},
3435
+ sessionId: sessionId ?? this.defaultSessionId ?? "",
3436
+ direction: "client\u2192browser"
3437
+ });
3438
+ if (decision && decision.action === "block") {
3439
+ throw new Error(`[CDP-Guard] ${decision.reason}${decision.suggestion ? " | \u66FF\u4EE3: " + decision.suggestion : ""} [${decision.ruleId}]`);
3440
+ }
3441
+ }
2873
3442
  if (this.closed) {
2874
3443
  throw new Error(`CDP connection closed: ${this.closeReason ?? "unknown"}`);
2875
3444
  }
@@ -2884,8 +3453,8 @@ var CDPConnection = class extends EventEmitter4 {
2884
3453
  return new Promise((resolve, reject) => {
2885
3454
  const timeout = setTimeout(() => {
2886
3455
  this.pending.delete(id);
2887
- reject(new Error(`CDP timeout: ${method} (${timeoutMs}ms)`));
2888
- }, timeoutMs);
3456
+ reject(new Error(`CDP timeout: ${method} (${effectiveTimeout}ms)`));
3457
+ }, effectiveTimeout);
2889
3458
  this.pending.set(id, {
2890
3459
  resolve: (v) => {
2891
3460
  clearTimeout(timeout);
@@ -2968,6 +3537,7 @@ var CDPConnection = class extends EventEmitter4 {
2968
3537
  // ── Private ─────────────────────────────────────────────────
2969
3538
  bindWebSocket() {
2970
3539
  this.ws.on("message", (raw) => {
3540
+ this.lastIncomingAt = Date.now();
2971
3541
  let msg;
2972
3542
  try {
2973
3543
  msg = JSON.parse(raw.toString());