@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.
- package/dist/{browser-XNU7JQYC.js → browser-AQ2AT2YZ.js} +2 -2
- package/dist/{browser-Q5APBNF6.js → browser-OMGJMSBB.js} +1 -1
- package/dist/{browser-V3JIWSTR.js → browser-T7OVLAEC.js} +4 -3
- package/dist/{cdp-driver-XFTTZI5O.js → cdp-driver-ALDNOZYD.js} +1176 -13
- package/dist/{cdp-driver-GD6YBBGE.js → cdp-driver-HYCLE4YP.js} +2 -1
- package/dist/{cdp-driver-VEK6BNN6.js → cdp-driver-VS7Z7W7Y.js} +1 -1
- package/dist/chunk-A6LPGFAL.js +437 -0
- package/dist/{chunk-JEDP4PJW.js → chunk-BY5TT6WZ.js} +912 -688
- package/dist/{chunk-XKQVUFUS.js → chunk-CW7L53JE.js} +2 -2
- package/dist/{chunk-ABXMBNQ6.js → chunk-H2A5JUK5.js} +37 -466
- package/dist/{chunk-BNO7OKO4.js → chunk-HBMEFSTB.js} +39 -0
- package/dist/chunk-HD4FX2N5.js +1255 -0
- package/dist/{chunk-DWDXEGVK.js → chunk-NKW4A74J.js} +9 -3
- package/dist/{chunk-SQHSZENE.js → chunk-OMU63E6J.js} +3 -22
- package/dist/{chunk-7OFM755Z.js → chunk-R2POF2GP.js} +236 -13
- package/dist/{chunk-WHPBNUKB.js → chunk-SH6OTPXN.js} +42 -46
- package/dist/{chunk-XQ4HRPDJ.js → chunk-T2CNKWPV.js} +1177 -13
- package/dist/{chunk-MWFHZUIY.js → chunk-TEXCXIBW.js} +1 -1
- package/dist/{chunk-IIM5GOD7.js → chunk-ZTHE5RBZ.js} +7 -1
- package/dist/cli.js +1075 -569
- package/dist/{daemon-client-MMDRYCF5.js → daemon-client-7D6EZNOE.js} +42 -46
- package/dist/{daemon-client-Y2YSZCGK.js → daemon-client-HEGAXWGK.js} +1 -1
- package/dist/daemon-main.js +950 -499
- package/dist/{human-interaction-5AO42MBA.js → human-interaction-4YR2N6R6.js} +2 -2
- package/dist/{human-interaction-C5OEGXGO.js → human-interaction-Y5IDH6LD.js} +1 -1
- package/dist/{human-interaction-ISXZTAYY.js → human-interaction-ZUTR5AC2.js} +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1086 -578
- package/dist/{proxy-C6CK3UH5.js → proxy-LUR4U5YF.js} +2 -1
- package/dist/{recovery-J2ISVGUL.js → recovery-FTZGV6VY.js} +2 -2
- package/dist/{recovery-ZJVVHP7N.js → recovery-L5GLDX4O.js} +1 -1
- package/dist/{recovery-EF33LKRJ.js → recovery-Z3RDZENA.js} +2 -2
- package/dist/{session-replayer-WIUYVN5J.js → session-replayer-UVH5FGY2.js} +1 -1
- package/package.json +1 -1
- 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
|
-
|
|
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
|
|
128
|
-
' if((p==="clientX"||p==="clientY")&&k.type==="click"&&Number.
|
|
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
|
-
|
|
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
|
|
161
|
-
' if((p==="clientX"||p==="clientY")&&k.type==="click"&&Number.
|
|
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 ??
|
|
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
|
|
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
|
-
},
|
|
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
|
|
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} (${
|
|
3268
|
-
},
|
|
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
|
-
|
|
72
|
-
method
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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);
|