@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.
- package/dist/{anti-bot-DR56Y63V.js → anti-bot-GTTYNEFB.js} +1 -1
- package/dist/{browser-WQZ3D6AE.js → browser-3HQKDZQ6.js} +4 -2
- package/dist/{browser-T3V3JWVH.js → browser-ILXTEWH4.js} +1 -1
- package/dist/{browser-HBL72GPZ.js → browser-XPDMY2NX.js} +3 -3
- package/dist/{cdp-driver-J3YQ4LJV.js → cdp-driver-ESH3PDE6.js} +2 -1
- package/dist/cdp-driver-N2E2ZNSC.js +4644 -0
- package/dist/cdp-driver-YIKR4BUX.js +49 -0
- package/dist/chunk-3FWLW7FS.js +106 -0
- package/dist/chunk-A6LPGFAL.js +437 -0
- package/dist/{chunk-RRBXV7KE.js → chunk-DKM2JOZL.js} +633 -63
- package/dist/{chunk-ABXMBNQ6.js → chunk-H2A5JUK5.js} +37 -466
- package/dist/{chunk-5UGR6MUK.js → chunk-HBHOKZPN.js} +51 -16
- package/dist/{chunk-BNO7OKO4.js → chunk-HBMEFSTB.js} +39 -0
- package/dist/{chunk-INTQPBYF.js → chunk-JKVUFP3G.js} +6 -2
- package/dist/chunk-KJTABK3Z.js +1255 -0
- package/dist/{chunk-DWDXEGVK.js → chunk-NKW4A74J.js} +9 -3
- package/dist/{chunk-4452SPFI.js → chunk-NODRQGOK.js} +51 -16
- package/dist/{chunk-SQHSZENE.js → chunk-OMU63E6J.js} +3 -22
- package/dist/{chunk-NITFVWWS.js → chunk-OVW2UEWN.js} +981 -407
- package/dist/{chunk-WSCP7QCJ.js → chunk-R6IJ6PIV.js} +14 -11
- package/dist/{chunk-YMUSHPU4.js → chunk-SH6OTPXN.js} +46 -16
- package/dist/{chunk-MWFHZUIY.js → chunk-TEXCXIBW.js} +1 -1
- package/dist/{cdp-driver-2T4P4ZE2.js → chunk-Z6CUR3VG.js} +1542 -134
- package/dist/{chunk-IIM5GOD7.js → chunk-ZTHE5RBZ.js} +7 -1
- package/dist/cli.js +1136 -584
- package/dist/{daemon-client-GKEPT4NY.js → daemon-client-7D6EZNOE.js} +46 -16
- package/dist/{daemon-client-O6BYVRXV.js → daemon-client-HEGAXWGK.js} +1 -1
- package/dist/daemon-main.js +1031 -522
- 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 +10 -0
- package/dist/index.js +1147 -593
- 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-recorder-H3KEYU26.js → session-recorder-3BEVWHOK.js} +1 -1
- package/dist/{session-recorder-QRZMKFVL.js → session-recorder-SLDBENVF.js} +1 -1
- package/dist/{session-replayer-LJUC4TI7.js → session-replayer-K4A6OI4M.js} +90 -2
- package/package.json +1 -1
- 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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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.
|
|
50
|
-
|
|
51
|
-
|
|
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 (
|
|
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
|
|
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.
|
|
300
|
-
|
|
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
|
-
|
|
339
|
-
|
|
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
|
-
|
|
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 (
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
796
|
+
const topEl = el.ownerDocument.elementFromPoint(cx, cy);
|
|
411
797
|
if (topEl && topEl !== el && !el.contains(topEl) && !topEl.contains(el)) {
|
|
412
|
-
|
|
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
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
|
1783
|
-
|
|
1784
|
-
{ depth: 0 },
|
|
1785
|
-
|
|
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
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
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
|
|
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
|
-
},
|
|
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
|
|
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} (${
|
|
2888
|
-
},
|
|
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());
|