@xbrowser/cli 1.11.0 → 1.13.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 (35) hide show
  1. package/dist/{browser-XNU7JQYC.js → browser-AQ2AT2YZ.js} +2 -2
  2. package/dist/{browser-Q5APBNF6.js → browser-OMGJMSBB.js} +1 -1
  3. package/dist/{browser-V3JIWSTR.js → browser-T7OVLAEC.js} +4 -3
  4. package/dist/{cdp-driver-XFTTZI5O.js → cdp-driver-ALDNOZYD.js} +1176 -13
  5. package/dist/{cdp-driver-GD6YBBGE.js → cdp-driver-HYCLE4YP.js} +2 -1
  6. package/dist/{cdp-driver-VEK6BNN6.js → cdp-driver-VS7Z7W7Y.js} +1 -1
  7. package/dist/chunk-A6LPGFAL.js +437 -0
  8. package/dist/{chunk-JEDP4PJW.js → chunk-BY5TT6WZ.js} +912 -688
  9. package/dist/{chunk-XKQVUFUS.js → chunk-CW7L53JE.js} +2 -2
  10. package/dist/{chunk-ABXMBNQ6.js → chunk-H2A5JUK5.js} +37 -466
  11. package/dist/{chunk-BNO7OKO4.js → chunk-HBMEFSTB.js} +39 -0
  12. package/dist/chunk-HD4FX2N5.js +1255 -0
  13. package/dist/{chunk-DWDXEGVK.js → chunk-NKW4A74J.js} +9 -3
  14. package/dist/{chunk-SQHSZENE.js → chunk-OMU63E6J.js} +3 -22
  15. package/dist/{chunk-7OFM755Z.js → chunk-R2POF2GP.js} +236 -13
  16. package/dist/{chunk-WHPBNUKB.js → chunk-SH6OTPXN.js} +42 -46
  17. package/dist/{chunk-XQ4HRPDJ.js → chunk-T2CNKWPV.js} +1177 -13
  18. package/dist/{chunk-MWFHZUIY.js → chunk-TEXCXIBW.js} +1 -1
  19. package/dist/{chunk-IIM5GOD7.js → chunk-ZTHE5RBZ.js} +7 -1
  20. package/dist/cli.js +1075 -569
  21. package/dist/{daemon-client-MMDRYCF5.js → daemon-client-7D6EZNOE.js} +42 -46
  22. package/dist/{daemon-client-Y2YSZCGK.js → daemon-client-HEGAXWGK.js} +1 -1
  23. package/dist/daemon-main.js +950 -499
  24. package/dist/{human-interaction-5AO42MBA.js → human-interaction-4YR2N6R6.js} +2 -2
  25. package/dist/{human-interaction-C5OEGXGO.js → human-interaction-Y5IDH6LD.js} +1 -1
  26. package/dist/{human-interaction-ISXZTAYY.js → human-interaction-ZUTR5AC2.js} +2 -2
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +1086 -578
  29. package/dist/{proxy-C6CK3UH5.js → proxy-LUR4U5YF.js} +2 -1
  30. package/dist/{recovery-J2ISVGUL.js → recovery-FTZGV6VY.js} +2 -2
  31. package/dist/{recovery-ZJVVHP7N.js → recovery-L5GLDX4O.js} +1 -1
  32. package/dist/{recovery-EF33LKRJ.js → recovery-Z3RDZENA.js} +2 -2
  33. package/dist/{session-replayer-WIUYVN5J.js → session-replayer-UVH5FGY2.js} +1 -1
  34. package/package.json +1 -1
  35. package/dist/chunk-HVPUVVSA.js +0 -2194
@@ -356,6 +356,11 @@ function getCaptchaConfig() {
356
356
  previewPort: parseInt(process.env.XBROWSER_PREVIEW_PORT || "") || config.preview?.port || 9223
357
357
  };
358
358
  }
359
+ function resolveScreenshotsDir() {
360
+ const configured = getConfigValue("screenshots.dir");
361
+ if (typeof configured === "string" && configured.trim()) return configured.trim();
362
+ return join(tmpdir(), "xbrowser", "screenshots");
363
+ }
359
364
 
360
365
  // src/utils/shell-escape.ts
361
366
  function shellEscape(value) {
@@ -508,9 +513,6 @@ var HumanInteractionManager = class {
508
513
  };
509
514
 
510
515
  export {
511
- ScreencastCapturer,
512
- CaptchaDetector,
513
- WebhookNotifier,
514
516
  loadConfig,
515
517
  getConfigValue,
516
518
  setConfigValue,
@@ -519,5 +521,9 @@ export {
519
521
  getMarketplaceUrl,
520
522
  resolveNpmPackageWithFallback,
521
523
  getCaptchaConfig,
524
+ resolveScreenshotsDir,
525
+ ScreencastCapturer,
526
+ CaptchaDetector,
527
+ WebhookNotifier,
522
528
  HumanInteractionManager
523
529
  };
@@ -1,6 +1,7 @@
1
1
  import {
2
- ScreencastCapturer
3
- } from "./chunk-BNO7OKO4.js";
2
+ ScreencastCapturer,
3
+ getCaptchaConfig
4
+ } from "./chunk-HBMEFSTB.js";
4
5
 
5
6
  // src/human-interaction.ts
6
7
  import { execSync } from "child_process";
@@ -137,26 +138,6 @@ var WebhookNotifier = class {
137
138
  }
138
139
  };
139
140
 
140
- // src/config.ts
141
- import { homedir, tmpdir } from "os";
142
- import { join } from "path";
143
- import { loadConfig as coreLoadConfig, saveConfig as coreSaveConfig } from "@dyyz1993/xcli-core";
144
- function getConfigSource() {
145
- return { configDir: join(homedir() || tmpdir(), ".xbrowser") };
146
- }
147
- function loadConfig() {
148
- return coreLoadConfig(getConfigSource());
149
- }
150
- function getCaptchaConfig() {
151
- const config = loadConfig();
152
- return {
153
- notifyUrl: process.env.XBROWSER_NOTIFY_URL || config.captcha?.notifyUrl,
154
- autoOpen: process.env.XBROWSER_AUTO_OPEN === "true" || config.captcha?.autoOpen === true,
155
- timeout: parseInt(process.env.XBROWSER_CAPTCHA_TIMEOUT || "") || config.captcha?.timeout || 120,
156
- previewPort: parseInt(process.env.XBROWSER_PREVIEW_PORT || "") || config.preview?.port || 9223
157
- };
158
- }
159
-
160
141
  // src/utils/shell-escape.ts
161
142
  function shellEscape(value) {
162
143
  return `'${value.replace(/'/g, "'\\''")}'`;
@@ -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";
@@ -124,8 +127,8 @@ function buildStealthInitScript() {
124
127
  " if(!e||e.constructor===FocusEvent||e.constructor===KeyboardEvent)return f.call(this,e);",
125
128
  " return f.call(this,new Proxy(e,{get:function(k,p){",
126
129
  ' if(p==="sourceCapabilities")return fc;',
127
- ' if(p==="isTrusted")return true;',
128
- ' if((p==="clientX"||p==="clientY")&&k.type==="click"&&Number.isFinite(k[p])){',
130
+ ' if(p==="isTrusted")return k.isTrusted;',
131
+ ' if((p==="clientX"||p==="clientY")&&k.type==="click"&&Number.isInteger(k[p])&&k.isTrusted===true){',
129
132
  " var _f=((k.timeStamp||Date.now())%89)/89*0.7+0.15;",
130
133
  " return k[p]+_f;",
131
134
  " }",
@@ -143,7 +146,160 @@ function buildStealthInitScript() {
143
146
  ' Object.defineProperty(Screen.prototype,"height",{get:_gh,configurable:true});',
144
147
  ' Object.defineProperty(Screen.prototype,"availWidth",{get:_gw,configurable:true});',
145
148
  ' Object.defineProperty(Screen.prototype,"availHeight",{get:_gah,configurable:true});',
146
- " document.hasFocus=function(){return true};",
149
+ // hasFocus 与 visibilityState 联动(d29):恒 true 在 hidden 标签下暴露
150
+ // —— 真实浏览器失焦/后台时 hasFocus=false
151
+ ' document.hasFocus=function(){return document.visibilityState==="visible";};',
152
+ // 4. Canvas/WebGL fingerprint: per-session stable noise (d20).
153
+ // Headless software raster differs subtly from Chrome GPU raster —
154
+ // toDataURL hashes fingerprint the rasterizer. Inject a stable
155
+ // (per-page-load) subpixel shift into fillText so hashes look like a
156
+ // distinct-but-consistent real GPU, and hide the HEADLESS tell in
157
+ // WebGL renderer strings.
158
+ " var _seed=Math.floor(Math.random()*2147483647);",
159
+ " var _prng=function(){_seed=(_seed*48271)%2147483647;return _seed/2147483647;};",
160
+ " var _dx=(_prng()*0.4-0.2).toFixed(3)*1, _dy=(_prng()*0.4-0.2).toFixed(3)*1;",
161
+ " var _fillText=CanvasRenderingContext2D.prototype.fillText;",
162
+ " CanvasRenderingContext2D.prototype.fillText=function(t,x,y,m){",
163
+ " return _fillText.call(this,t,x+_dx,y+_dy,m);",
164
+ " };",
165
+ " var _toDataURL=HTMLCanvasElement.prototype.toDataURL;",
166
+ " HTMLCanvasElement.prototype.toDataURL=function(){",
167
+ ' var ctx=this.getContext("2d");',
168
+ " if(ctx){var d=ctx.getImageData(0,0,Math.min(this.width,2),Math.min(this.height,2));",
169
+ " for(var i=0;i<d.data.length;i+=4){if(d.data[i+3]>0){d.data[i]^=1;break;}}",
170
+ " ctx.putImageData(d,0,0);}",
171
+ " return _toDataURL.apply(this,arguments);",
172
+ " };",
173
+ " try{",
174
+ " var _gl=HTMLCanvasElement.prototype.getContext;",
175
+ " HTMLCanvasElement.prototype.getContext=function(t,o){",
176
+ " var c=_gl.call(this,t,o);",
177
+ ' if(c&&(t==="webgl"||t==="experimental-webgl")&&c.getParameter){',
178
+ " var _gp=c.getParameter.bind(c);",
179
+ " c.getParameter=function(p){",
180
+ " var v=_gp(p);",
181
+ ' if(typeof v==="string"&&/SwiftShader|Software|Rasterizer|Headless/i.test(v))',
182
+ ' return "ANGLE (Apple, ANGLE Metal Renderer: Apple M2 Max, Unspecified Version)";',
183
+ " return v;};}",
184
+ " return c;};",
185
+ " }catch(e){}",
186
+ // 5. AudioContext fingerprint: per-load stable micro-noise on channel
187
+ // data (d21). DSP sum differences between headless software audio and
188
+ // real hardware audio are a classic fingerprint — add ±1e-7 level
189
+ // noise (inaudible, changes the sum hash).
190
+ " try{",
191
+ " var _gcd=AudioBuffer.prototype.getChannelData;",
192
+ " AudioBuffer.prototype.getChannelData=function(ch){",
193
+ " var d=_gcd.call(this,ch);",
194
+ ' var key="__xb_audio_"+ch;',
195
+ " if(!this[key]){",
196
+ " this[key]=true;",
197
+ " for(var i=0;i<d.length;i+=997){d[i]=d[i]+(_prng()-0.5)*2e-7;}",
198
+ " }",
199
+ " return d;",
200
+ " };",
201
+ " var _gffd=AnalyserNode.prototype.getFloatFrequencyData;",
202
+ " AnalyserNode.prototype.getFloatFrequencyData=function(arr){",
203
+ " _gffd.call(this,arr);",
204
+ " for(var i=0;i<arr.length;i+=31){arr[i]=arr[i]+(_prng()-0.5)*0.01;}",
205
+ " };",
206
+ " var _gfbd=AnalyserNode.prototype.getByteFrequencyData;",
207
+ " AnalyserNode.prototype.getByteFrequencyData=function(arr){",
208
+ " _gfbd.call(this,arr);",
209
+ " for(var i=0;i<arr.length;i+=31){arr[i]=(arr[i]+((_prng()*3)|0))&255;}",
210
+ " };",
211
+ " }catch(e){}",
212
+ // 3b. Font metrics + speechSynthesis + battery (d22): headless tells
213
+ " try{",
214
+ " var _mt=CanvasRenderingContext2D.prototype.measureText;",
215
+ ' var _tmw=Object.getOwnPropertyDescriptor(TextMetrics.prototype,"width");',
216
+ " CanvasRenderingContext2D.prototype.measureText=function(t){",
217
+ " var m=_mt.call(this,t);",
218
+ ' try{Object.defineProperty(m,"width",{get:function(){return _tmw.get.call(m)+_dx*0.01;},configurable:true});}catch(e){}',
219
+ " return m;",
220
+ " };",
221
+ " }catch(e){}",
222
+ " try{",
223
+ ' var _fakeVoices=["Alex","Daniel","Karen","Moira","Ralph","Samantha","Ting-Ting","Mei-Jia","Sinji","Yunda"];',
224
+ " var _sv=speechSynthesis.getVoices.bind(speechSynthesis);",
225
+ " speechSynthesis.getVoices=function(){",
226
+ " var real=_sv();",
227
+ " if(real&&real.length>50)return real;",
228
+ // 注意阈值从 0 改 50:iframe 原生约 10 个 —— 原阈值下 iframe 返回原生 10 个
229
+ // 而不是伪装 180(d33 暴露)。>50 = 主文档伪装列表已生效,其余返回伪装。
230
+ ' 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};});',
231
+ " };",
232
+ // d33 修复:iframe 的 speechSynthesis 是独立实例(主文档 hook 不可达)——
233
+ // 轮询同源 iframe,对其 contentWindow.speechSynthesis 同样 patch
234
+ " try{",
235
+ " var _piv=function(){",
236
+ ' var frs=document.querySelectorAll("iframe");',
237
+ " for(var i=0;i<frs.length;i++){try{",
238
+ " var cw=frs[i].contentWindow;",
239
+ " if(!cw||!cw.speechSynthesis||cw.speechSynthesis.__xbP)continue;",
240
+ " cw.speechSynthesis.__xbP=true;",
241
+ " var _sv2=cw.speechSynthesis.getVoices.bind(cw.speechSynthesis);",
242
+ " cw.speechSynthesis.getVoices=function(){var r=_sv2();if(r&&r.length>50)return r;",
243
+ ' return _fakeVoices.map(function(n,j){return{name:n,lang:j<2?"en-US":j<6?"en-GB":"zh-TW",localService:true,default:j===0,voiceURI:n};});};',
244
+ " }catch(e2){}}",
245
+ " };",
246
+ " setInterval(_piv,2000);",
247
+ " var _mo=new MutationObserver(function(muts){",
248
+ " for(var k=0;k<muts.length;k++){var ns=muts[k].addedNodes;",
249
+ ' for(var q=0;q<ns.length;q++){if(ns[q].tagName==="IFRAME")setTimeout(_piv,20);}}',
250
+ " });",
251
+ " _mo.observe(document.documentElement,{childList:true,subtree:true});",
252
+ " }catch(e){}",
253
+ " }catch(e){}",
254
+ " try{",
255
+ " var _gb=navigator.getBattery.bind(navigator);",
256
+ " navigator.getBattery=function(){",
257
+ " return _gb().then(function(b){",
258
+ ' Object.defineProperty(b,"charging",{get:function(){return true;},configurable:true});',
259
+ " return b;});",
260
+ " };",
261
+ " }catch(e){}",
262
+ // 3c. WebRTC local IP leak guard (d23): headless STUN candidates can
263
+ // expose host IPs; mDNS-only candidates are the modern Chrome default.
264
+ " try{",
265
+ " var _oc=RTCPeerConnection.prototype.createOffer;",
266
+ " RTCPeerConnection.prototype.createOffer=function(){",
267
+ " var p=_oc.apply(this,arguments);",
268
+ " var self=this;",
269
+ " return p.then(function(offer){",
270
+ ' offer.sdp=offer.sdp.split(String.fromCharCode(10)).filter(function(l){return l.indexOf("typ host")<0}).join(String.fromCharCode(10));',
271
+ " return offer;});",
272
+ " };",
273
+ " }catch(e){}",
274
+ // 3d-2. performance.now precision clamp (d34): full-precision timestamps
275
+ // (11 decimals) differ from site-isolation-clamped real Chrome (~100μs).
276
+ " try{",
277
+ " var _pn=performance.now.bind(performance);",
278
+ " performance.now=function(){return Math.round(_pn()*10)/10;};",
279
+ ' var _pto=Object.getOwnPropertyDescriptor(Performance.prototype,"timeOrigin");',
280
+ ' if(_pto&&_pto.get){Object.defineProperty(performance,"timeOrigin",{get:function(){return _pto.get.call(performance);},configurable:true});}',
281
+ " }catch(e){}",
282
+ // 3d. Chrome object depth (d24): automation fakes usually only set
283
+ // window.chrome = {}; deep checks hit app.run/runtime/csi/loadTimes.
284
+ " try{",
285
+ " if(window.chrome){",
286
+ ' 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"}};',
287
+ ' 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};',
288
+ " if(!window.chrome.csi)window.chrome.csi=function(){return{};}",
289
+ " if(!window.chrome.loadTimes)window.chrome.loadTimes=function(){return{};}",
290
+ " }",
291
+ " }catch(e){}",
292
+ // 3e. Font availability patch (d25): headless Chromium misses some system
293
+ // fonts (e.g. Menlo on macOS) that real Chrome has. Register a FontFace
294
+ // aliasing the missing font to a local() equivalent so offsetWidth-based
295
+ // font probing sees the same availability as the faked environment.
296
+ " try{",
297
+ ' var _fontAliases=[["Menlo","Courier New"],["SF Mono","Menlo"],["Segoe UI","Helvetica"]];',
298
+ " _fontAliases.forEach(function(pa){",
299
+ ' try{var ff=new FontFace(pa[0],"local("+JSON.stringify(pa[1])+")");',
300
+ " document.fonts.add(ff);ff.load();}catch(e2){}",
301
+ " });",
302
+ " }catch(e){}",
147
303
  // 3. toString disguise (name-list based)
148
304
  " var _ts=Function.prototype.toString;",
149
305
  " var _hf=document.hasFocus;",
@@ -153,12 +309,16 @@ function buildStealthInitScript() {
153
309
  ' if(this===_gw)return"function get width() { [native code] }";',
154
310
  ' if(this===_gh)return"function get height() { [native code] }";',
155
311
  ' if(this===_gah)return"function get availHeight() { [native code] }";',
312
+ ' if(this===CanvasRenderingContext2D.prototype.fillText)return"function fillText() { [native code] }";',
313
+ ' if(this===HTMLCanvasElement.prototype.toDataURL)return"function toDataURL() { [native code] }";',
314
+ ' if(this===AnalyserNode.prototype.getFloatFrequencyData)return"function getFloatFrequencyData() { [native code] }";',
315
+ ' if(this===AudioBuffer.prototype.getChannelData)return"function getChannelData() { [native code] }";',
156
316
  " return _ts.call(this);",
157
317
  " };",
158
318
  // 4. onclick prototype hijack (dual-stream consistency)
159
319
  " var _ba=function(k,p){",
160
- ' if(p==="isTrusted")return true;',
161
- ' if((p==="clientX"||p==="clientY")&&k.type==="click"&&Number.isFinite(k[p])){',
320
+ ' if(p==="isTrusted")return k.isTrusted;',
321
+ ' if((p==="clientX"||p==="clientY")&&k.type==="click"&&Number.isInteger(k[p])&&k.isTrusted===true){',
162
322
  " var _f=((k.timeStamp||Date.now())%89)/89*0.7+0.15;return k[p]+_f;",
163
323
  " }",
164
324
  ' var v=Reflect.get(k,p);return typeof v==="function"?v.bind(k):v;',
@@ -194,7 +354,7 @@ var XBMouseImpl = class {
194
354
  }
195
355
  async click(x, y, opts = {}) {
196
356
  const button = opts.button ?? "left";
197
- const stealth = opts.stealth ?? true;
357
+ const stealth = opts.stealth ?? process.env.XBROWSER_STEALTH !== "off";
198
358
  let tx = x, ty = y;
199
359
  if (stealth && opts.elementWidth !== void 0 && opts.elementHeight !== void 0) {
200
360
  const off = landingOffset(opts.elementWidth, opts.elementHeight);
@@ -203,12 +363,23 @@ var XBMouseImpl = class {
203
363
  }
204
364
  if (stealth) {
205
365
  const traj = bezierTrajectory(this._x, this._y, tx, ty);
366
+ const _tb = Date.now();
367
+ let _truncated = false;
206
368
  for (const p of traj) {
369
+ if (Date.now() - _tb > 5e3) {
370
+ _truncated = true;
371
+ break;
372
+ }
207
373
  await this.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: p.x, y: p.y, button: this._button });
208
374
  this._x = p.x;
209
375
  this._y = p.y;
210
376
  await sleep(p.delay);
211
377
  }
378
+ this._x = tx;
379
+ this._y = ty;
380
+ if (_truncated) {
381
+ await this.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: tx, y: ty, button: this._button });
382
+ }
212
383
  await sleep(rand(...DEFAULT_STEALTH_CONFIG.aimPause));
213
384
  } else {
214
385
  await this.move(tx, ty);
@@ -313,12 +484,20 @@ var XBKeyboardImpl = class {
313
484
  }
314
485
  await this.dispatchKeyEvent(downParams);
315
486
  if (mapping.text) {
487
+ if (process.env.XBROWSER_STEALTH !== "off") {
488
+ await sleep2(rand(...DEFAULT_STEALTH_CONFIG.keyPressDuration));
489
+ }
316
490
  await this.dispatchKeyEvent({
317
491
  type: "char",
318
492
  text: mapping.text
319
493
  });
320
494
  }
321
495
  if (delay > 0) await sleep2(delay);
496
+ else {
497
+ if (process.env.XBROWSER_STEALTH !== "off") {
498
+ await sleep2(rand(...DEFAULT_STEALTH_CONFIG.keyPressDuration));
499
+ }
500
+ }
322
501
  const upParams = {
323
502
  type: "keyUp",
324
503
  key: mapping.key,
@@ -2028,6 +2207,10 @@ Last error: ${lastError.message}` : "";
2028
2207
  this.sessionId
2029
2208
  );
2030
2209
  }
2210
+ /** 覆盖 User-Agent(headless 环境对抗 UA 检测的标配能力) */
2211
+ async setUserAgent(userAgent) {
2212
+ await this._setUserAgent(userAgent);
2213
+ }
2031
2214
  /** Internal: set extra HTTP headers */
2032
2215
  async _setExtraHTTPHeaders(headers) {
2033
2216
  await this.setExtraHTTPHeaders(headers);
@@ -3157,9 +3340,28 @@ var XBBrowserImpl = class {
3157
3340
  // src/cdp-driver/connection.ts
3158
3341
  import { EventEmitter as EventEmitter4 } from "events";
3159
3342
  import { WebSocket } from "ws";
3160
- var CDPConnection = class extends EventEmitter4 {
3343
+ var CDPConnection = class _CDPConnection extends EventEmitter4 {
3161
3344
  ws;
3162
3345
  nextId = 1;
3346
+ /** CDP Guard:全部出站命令过规则引擎(env XBROWSER_CDP_GUARD=off 关闭)。
3347
+ * 173 条规则 / 9 模块:合成事件 block、指纹暴露警告、自动化信号拦截。 */
3348
+ static __guard;
3349
+ get guard() {
3350
+ if (_CDPConnection.__guard === void 0) {
3351
+ if (process.env.XBROWSER_CDP_GUARD === "off") {
3352
+ _CDPConnection.__guard = null;
3353
+ } else {
3354
+ try {
3355
+ const g = createRuleEngine();
3356
+ g.start();
3357
+ _CDPConnection.__guard = g;
3358
+ } catch {
3359
+ _CDPConnection.__guard = null;
3360
+ }
3361
+ }
3362
+ }
3363
+ return _CDPConnection.__guard;
3364
+ }
3163
3365
  pending = /* @__PURE__ */ new Map();
3164
3366
  closed = false;
3165
3367
  closeReason = null;
@@ -3183,6 +3385,8 @@ var CDPConnection = class extends EventEmitter4 {
3183
3385
  * ping, the connection is considered dead and forcibly closed. */
3184
3386
  keepaliveTimer = null;
3185
3387
  pongTimer = null;
3388
+ /** 最近一次收到任何 WS 消息的时间——有数据流动即证明连接存活 */
3389
+ lastIncomingAt = Date.now();
3186
3390
  startKeepalive() {
3187
3391
  this.ws.on("pong", () => {
3188
3392
  if (this.pongTimer) {
@@ -3194,9 +3398,14 @@ var CDPConnection = class extends EventEmitter4 {
3194
3398
  if (this.ws.readyState === WebSocket.OPEN) {
3195
3399
  if (!this.pongTimer) {
3196
3400
  this.pongTimer = setTimeout(() => {
3401
+ const recentlyActive = Date.now() - this.lastIncomingAt < 3e4;
3402
+ if (recentlyActive) {
3403
+ this.pongTimer = null;
3404
+ return;
3405
+ }
3197
3406
  if (!this.closed) {
3198
3407
  this.closed = true;
3199
- this.closeReason = "keepalive timeout (no pong in 10s)";
3408
+ this.closeReason = "keepalive timeout (no pong and idle >30s)";
3200
3409
  try {
3201
3410
  this.ws.terminate();
3202
3411
  } catch {
@@ -3208,7 +3417,7 @@ var CDPConnection = class extends EventEmitter4 {
3208
3417
  this.pending.clear();
3209
3418
  this.emit("disconnect");
3210
3419
  }
3211
- }, 1e4);
3420
+ }, 6e4);
3212
3421
  }
3213
3422
  this.ws.ping?.();
3214
3423
  } else if (this.closed) {
@@ -3246,10 +3455,23 @@ var CDPConnection = class extends EventEmitter4 {
3246
3455
  * @param method — CDP domain.method (e.g. "Page.navigate")
3247
3456
  * @param params — method parameters
3248
3457
  * @param sessionId — optional flat session ID for sub-targets
3249
- * @param timeoutMs — response timeout (default: 30s)
3458
+ * @param timeoutMs — response timeout (default: 30s;重 SPA 加载期间主线程忙,
3459
+ * evaluate 可排队 >30s——Runtime.evaluate 单独放宽到 90s)
3250
3460
  * @returns the `result` field from the CDP response
3251
3461
  */
3252
- async send(method, params, sessionId, timeoutMs = 3e4) {
3462
+ async send(method, params, sessionId, timeoutMs) {
3463
+ const effectiveTimeout = timeoutMs ?? (method === "Runtime.evaluate" ? 9e4 : 3e4);
3464
+ if (this.guard) {
3465
+ const decision = this.guard.evaluate({
3466
+ method,
3467
+ params: params ?? {},
3468
+ sessionId: sessionId ?? this.defaultSessionId ?? "",
3469
+ direction: "client\u2192browser"
3470
+ });
3471
+ if (decision && decision.action === "block") {
3472
+ throw new Error(`[CDP-Guard] ${decision.reason}${decision.suggestion ? " | \u66FF\u4EE3: " + decision.suggestion : ""} [${decision.ruleId}]`);
3473
+ }
3474
+ }
3253
3475
  if (this.closed) {
3254
3476
  throw new Error(`CDP connection closed: ${this.closeReason ?? "unknown"}`);
3255
3477
  }
@@ -3264,8 +3486,8 @@ var CDPConnection = class extends EventEmitter4 {
3264
3486
  return new Promise((resolve, reject) => {
3265
3487
  const timeout = setTimeout(() => {
3266
3488
  this.pending.delete(id);
3267
- reject(new Error(`CDP timeout: ${method} (${timeoutMs}ms)`));
3268
- }, timeoutMs);
3489
+ reject(new Error(`CDP timeout: ${method} (${effectiveTimeout}ms)`));
3490
+ }, effectiveTimeout);
3269
3491
  this.pending.set(id, {
3270
3492
  resolve: (v) => {
3271
3493
  clearTimeout(timeout);
@@ -3348,6 +3570,7 @@ var CDPConnection = class extends EventEmitter4 {
3348
3570
  // ── Private ─────────────────────────────────────────────────
3349
3571
  bindWebSocket() {
3350
3572
  this.ws.on("message", (raw) => {
3573
+ this.lastIncomingAt = Date.now();
3351
3574
  let msg;
3352
3575
  try {
3353
3576
  msg = JSON.parse(raw.toString());
@@ -68,22 +68,48 @@ async function ensureDaemonRunning() {
68
68
  }
69
69
  async function rpcCall(method, params = {}, timeoutMs = 1e4) {
70
70
  await ensureDaemonRunning();
71
- const resp = await fetch(`${DAEMON_BASE}/rpc`, {
72
- method: "POST",
73
- headers: { "Content-Type": "application/json" },
74
- body: JSON.stringify({ method, params }),
75
- signal: AbortSignal.timeout(timeoutMs)
71
+ return new Promise((resolve, reject) => {
72
+ const body = JSON.stringify({ method, params });
73
+ const req = httpRequest(
74
+ {
75
+ hostname: "127.0.0.1",
76
+ port: DAEMON_PORT,
77
+ path: "/rpc",
78
+ method: "POST",
79
+ headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) }
80
+ },
81
+ (res) => {
82
+ const chunks = [];
83
+ res.on("data", (c) => chunks.push(c));
84
+ res.on("end", () => {
85
+ try {
86
+ const code = res.statusCode ?? 200;
87
+ if (code >= 400) {
88
+ let detail = `HTTP ${code}`;
89
+ try {
90
+ const body2 = JSON.parse(Buffer.concat(chunks).toString("utf8"));
91
+ if (body2?.error) detail = body2.error;
92
+ } catch {
93
+ }
94
+ reject(new Error(`Daemon error: ${detail}`));
95
+ return;
96
+ }
97
+ const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
98
+ if (parsed && typeof parsed.error === "string" && Object.keys(parsed).length === 1) {
99
+ reject(new Error(`Daemon error: ${parsed.error}`));
100
+ } else {
101
+ resolve(parsed);
102
+ }
103
+ } catch (e) {
104
+ reject(e instanceof Error ? e : new Error(String(e)));
105
+ }
106
+ });
107
+ }
108
+ );
109
+ req.setTimeout(timeoutMs, () => req.destroy(new Error(`RPC ${method} timeout after ${timeoutMs}ms`)));
110
+ req.on("error", reject);
111
+ req.end(body);
76
112
  });
77
- if (!resp.ok) {
78
- let detail = resp.statusText;
79
- try {
80
- const body = await resp.json();
81
- if (body?.error) detail = body.error;
82
- } catch {
83
- }
84
- throw new Error(`Daemon error: ${detail}`);
85
- }
86
- return resp.json();
87
113
  }
88
114
  async function isDaemonRunning() {
89
115
  try {
@@ -211,37 +237,7 @@ async function forwardReplay(file, session, slowMo) {
211
237
  return longRpcCall("replay", { file, session, slowMo }, 20 * 6e4);
212
238
  }
213
239
  function longRpcCall(method, params, timeoutMs) {
214
- return new Promise((resolve, reject) => {
215
- const body = JSON.stringify({ method, params });
216
- const req = httpRequest(
217
- {
218
- hostname: "127.0.0.1",
219
- port: DAEMON_PORT,
220
- path: "/rpc",
221
- method: "POST",
222
- headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) }
223
- },
224
- (res) => {
225
- const chunks = [];
226
- res.on("data", (c) => chunks.push(c));
227
- res.on("end", () => {
228
- try {
229
- const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
230
- if (parsed && typeof parsed.error === "string" && Object.keys(parsed).length === 1) {
231
- reject(new Error(`Daemon error: ${parsed.error}`));
232
- } else {
233
- resolve(parsed);
234
- }
235
- } catch (e) {
236
- reject(e instanceof Error ? e : new Error(String(e)));
237
- }
238
- });
239
- }
240
- );
241
- req.setTimeout(timeoutMs, () => req.destroy(new Error(`replay RPC timeout after ${timeoutMs}ms`)));
242
- req.on("error", reject);
243
- req.end(body);
244
- });
240
+ return rpcCall(method, params, timeoutMs);
245
241
  }
246
242
  async function forwardRecordCheckpoint(session, type, hint, selector) {
247
243
  return rpcCall("record:checkpoint", { session, type, hint, selector }, 1e4);