@xbrowser/cli 1.21.0 → 1.23.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/README.md +21 -3
- package/dist/{browser-CNBSH53B.js → browser-BVRFSUXU.js} +3 -2
- package/dist/{browser-C2VEOJC2.js → browser-DSYUAE7H.js} +5 -4
- package/dist/{browser-VZOOFLAH.js → browser-Q67BJTI2.js} +4 -3
- package/dist/{cdp-driver-KFX4A6WR.js → cdp-driver-DC7EIUG5.js} +4 -3
- package/dist/{cdp-driver-7Z6ZAT5E.js → cdp-driver-T7D6H2RW.js} +335 -546
- package/dist/{cdp-driver-26LGCHXV.js → cdp-driver-WWO2UZDR.js} +3 -2
- package/dist/{chunk-Z5RTK4GW.js → chunk-2Q3GQRUP.js} +2 -2
- package/dist/{chunk-CLYZPSDR.js → chunk-4OU37CXP.js} +354 -576
- package/dist/{chunk-HWWG2ISJ.js → chunk-7OP2ISEY.js} +353 -575
- package/dist/{chunk-PIWNMC36.js → chunk-A3MORKYR.js} +1 -1
- package/dist/{chunk-3FWLW7FS.js → chunk-A7NEA4PA.js} +39 -1
- package/dist/{chunk-OMU63E6J.js → chunk-FBQZ3PVD.js} +5 -8
- package/dist/chunk-GYB5BOSP.js +602 -0
- package/dist/{chunk-ESJYANQO.js → chunk-IFKJO2UR.js} +360 -582
- package/dist/{chunk-NODRQGOK.js → chunk-IR6C24P7.js} +97 -0
- package/dist/{chunk-TEXCXIBW.js → chunk-LPW6QS4M.js} +5 -8
- package/dist/{chunk-3KZ34AUC.js → chunk-MYH6FIBZ.js} +2 -1
- package/dist/{chunk-AVSTNODR.js → chunk-T4WTKBWI.js} +2 -2
- package/dist/{chunk-HBHOKZPN.js → chunk-V6KGBU5J.js} +97 -0
- package/dist/{chunk-IWVG4XYZ.js → chunk-VCVDILH6.js} +2 -1
- package/dist/{chunk-NKW4A74J.js → chunk-XTUAUFL3.js} +5 -8
- package/dist/cli.js +497 -252
- package/dist/daemon-main.js +25 -19
- package/dist/{human-interaction-Y5IDH6LD.js → human-interaction-57VYIR56.js} +1 -1
- package/dist/{human-interaction-ZUTR5AC2.js → human-interaction-67K43B4O.js} +1 -1
- package/dist/{human-interaction-4YR2N6R6.js → human-interaction-MXST5ZQM.js} +1 -1
- package/dist/index.d.ts +52 -4
- package/dist/index.js +500 -256
- package/dist/keep-awake-M4KJS4B4.js +29 -0
- package/dist/{launcher-44STYRJC.js → launcher-IK2FKW6T.js} +1 -1
- package/dist/{launcher-EXJHHAUL.js → launcher-LUOMMGZC.js} +1 -1
- package/dist/{plugin-singleton-FCNDN3FX.js → plugin-singleton-GJO7EZXB.js} +1 -1
- package/dist/{recovery-NXC35EQN.js → recovery-6RI2FE3E.js} +1 -1
- package/dist/{recovery-MDWAVXE4.js → recovery-LGY25LIM.js} +1 -1
- package/dist/{recovery-L5GLDX4O.js → recovery-LMJW537X.js} +1 -1
- package/dist/{session-recorder-SLDBENVF.js → session-recorder-563ISSJK.js} +1 -1
- package/dist/{session-recorder-3BEVWHOK.js → session-recorder-GEZVQBKW.js} +1 -1
- package/dist/session-replayer-N665T3G5.js +932 -0
- package/dist/stealth-2AA2FSNN.js +29 -0
- package/dist/stealth-JOXGV34D.js +29 -0
- package/package.json +11 -7
- package/dist/session-replayer-EVD4HZBB.js +0 -413
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_STEALTH_CONFIG,
|
|
3
|
+
bezierTrajectory,
|
|
4
|
+
buildStealthInitScript,
|
|
5
|
+
landingOffset,
|
|
6
|
+
rand,
|
|
7
|
+
typingDelay
|
|
8
|
+
} from "./chunk-GYB5BOSP.js";
|
|
1
9
|
import {
|
|
2
10
|
connectToCDP,
|
|
3
11
|
launchChrome
|
|
4
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-MYH6FIBZ.js";
|
|
5
13
|
import {
|
|
6
14
|
errMsg
|
|
7
15
|
} from "./chunk-GDKLH7ZY.js";
|
|
@@ -11,12 +19,15 @@ import {
|
|
|
11
19
|
|
|
12
20
|
// src/browser.ts
|
|
13
21
|
import { randomUUID } from "crypto";
|
|
14
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "fs";
|
|
15
|
-
import { join } from "path";
|
|
22
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync, readdirSync, unlinkSync, writeFileSync } from "fs";
|
|
23
|
+
import { join as join2 } from "path";
|
|
16
24
|
import { homedir } from "os";
|
|
17
25
|
|
|
18
26
|
// src/cdp-driver/browser.ts
|
|
19
27
|
import { EventEmitter as EventEmitter3 } from "events";
|
|
28
|
+
import { mkdirSync } from "fs";
|
|
29
|
+
import { join } from "path";
|
|
30
|
+
import { tmpdir } from "os";
|
|
20
31
|
|
|
21
32
|
// src/cdp-driver/context.ts
|
|
22
33
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
@@ -24,541 +35,6 @@ import { EventEmitter as EventEmitter2 } from "events";
|
|
|
24
35
|
// src/cdp-driver/page.ts
|
|
25
36
|
import { EventEmitter } from "events";
|
|
26
37
|
|
|
27
|
-
// src/cdp-driver/stealth.ts
|
|
28
|
-
var DEFAULT_STEALTH_CONFIG = {
|
|
29
|
-
// S191: macOS arm64 + Chrome 151 真实采样(来源:登录桥只读通道)
|
|
30
|
-
uaChProfile: {
|
|
31
|
-
brands: [
|
|
32
|
-
{ brand: "Not=A?Brand", version: "99" },
|
|
33
|
-
{ brand: "Google Chrome", version: "151" },
|
|
34
|
-
{ brand: "Chromium", version: "151" }
|
|
35
|
-
],
|
|
36
|
-
platform: "macOS",
|
|
37
|
-
platformVersion: "26.2.0",
|
|
38
|
-
architecture: "arm",
|
|
39
|
-
bitness: "64",
|
|
40
|
-
model: "",
|
|
41
|
-
uaFullVersion: "151.0.7922.175",
|
|
42
|
-
fullVersionList: [
|
|
43
|
-
{ brand: "Not=A?Brand", version: "99.0.0.0" },
|
|
44
|
-
{ brand: "Google Chrome", version: "151.0.7922.175" },
|
|
45
|
-
{ brand: "Chromium", version: "151.0.7922.175" }
|
|
46
|
-
]
|
|
47
|
-
},
|
|
48
|
-
bezierCurvature: [0.35, 0.6],
|
|
49
|
-
noiseAmplitude: 5.5,
|
|
50
|
-
overshootRange: [6, 14],
|
|
51
|
-
aimPause: [80, 280],
|
|
52
|
-
pressDuration: [60, 140],
|
|
53
|
-
releaseDrift: [0.8, 2.5],
|
|
54
|
-
landingOffsetSmall: [0.3, 2.5],
|
|
55
|
-
landingOffsetLarge: [1.5, 7],
|
|
56
|
-
smallElementThreshold: 30,
|
|
57
|
-
typingRhythm: {
|
|
58
|
-
fastProb: 0.22,
|
|
59
|
-
fastRange: [25, 60],
|
|
60
|
-
normalRange: [50, 350],
|
|
61
|
-
pauseProb: 0.18,
|
|
62
|
-
pauseRange: [400, 1200]
|
|
63
|
-
},
|
|
64
|
-
keyPressDuration: [50, 110],
|
|
65
|
-
typoProbability: 0.06,
|
|
66
|
-
wheelPeak: 180,
|
|
67
|
-
wheelDecayRate: 0.4
|
|
68
|
-
};
|
|
69
|
-
function rand(min, max) {
|
|
70
|
-
return min + Math.random() * (max - min);
|
|
71
|
-
}
|
|
72
|
-
function sleep(ms) {
|
|
73
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
74
|
-
}
|
|
75
|
-
function cosineEase(t) {
|
|
76
|
-
return 0.5 - 0.5 * Math.cos(Math.PI * t);
|
|
77
|
-
}
|
|
78
|
-
function bezierTrajectory(x0, y0, x1, y1, config = DEFAULT_STEALTH_CONFIG) {
|
|
79
|
-
const dist = Math.hypot(x1 - x0, y1 - y0);
|
|
80
|
-
const n = Math.max(10, Math.min(28, Math.round(dist / 15)));
|
|
81
|
-
const shortMove = dist < 120;
|
|
82
|
-
const curvature = shortMove ? rand(2, 6) : Math.max(dist * rand(...config.bezierCurvature), rand(18, 35));
|
|
83
|
-
const dir = Math.random() < 0.5 ? 1 : -1;
|
|
84
|
-
const d = dist || 1;
|
|
85
|
-
const dx = x1 - x0, dy = y1 - y0;
|
|
86
|
-
const c1x = x0 + dx * 0.3 - dy / d * curvature * 0.5 * dir;
|
|
87
|
-
const c1y = y0 + dy * 0.3 + dx / d * curvature * 0.5 * dir;
|
|
88
|
-
const c2x = x0 + dx * 0.7 - dy / d * curvature * 0.8 * dir;
|
|
89
|
-
const c2y = y0 + dy * 0.7 + dx / d * curvature * 0.8 * dir;
|
|
90
|
-
const points = [];
|
|
91
|
-
for (let i = 1; i <= n; i++) {
|
|
92
|
-
const t = cosineEase(i / n);
|
|
93
|
-
const mt = 1 - t;
|
|
94
|
-
let px = mt ** 3 * x0 + 3 * mt ** 2 * t * c1x + 3 * mt * t ** 2 * c2x + t ** 3 * x1;
|
|
95
|
-
let py = mt ** 3 * y0 + 3 * mt ** 2 * t * c1y + 3 * mt * t ** 2 * c2y + t ** 3 * y1;
|
|
96
|
-
const amp = shortMove ? Math.min(2, config.noiseAmplitude) : config.noiseAmplitude;
|
|
97
|
-
px += rand(-amp, amp);
|
|
98
|
-
py += rand(-amp, amp);
|
|
99
|
-
points.push({ x: px, y: py, delay: rand(9, 16) });
|
|
100
|
-
}
|
|
101
|
-
if (!shortMove) {
|
|
102
|
-
const over = rand(...config.overshootRange);
|
|
103
|
-
const ox = x1 + dx / d * over + rand(-2, 2);
|
|
104
|
-
const oy = y1 + dy / d * over + rand(-2, 2);
|
|
105
|
-
points.push({ x: ox, y: oy, delay: rand(14, 30) });
|
|
106
|
-
points.push({
|
|
107
|
-
x: x1 + dx / d * over * 0.4,
|
|
108
|
-
y: y1 + dy / d * over * 0.4,
|
|
109
|
-
delay: rand(14, 30)
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
points.push({ x: x1 + rand(-1, 1), y: y1 + rand(-1, 1), delay: rand(14, 30) });
|
|
113
|
-
return points;
|
|
114
|
-
}
|
|
115
|
-
function landingOffset(width, height, config = DEFAULT_STEALTH_CONFIG) {
|
|
116
|
-
const isSmall = Math.min(width, height) < config.smallElementThreshold;
|
|
117
|
-
const range = isSmall ? config.landingOffsetSmall : config.landingOffsetLarge;
|
|
118
|
-
const dx = rand(...range) * (Math.random() < 0.5 ? -1 : 1);
|
|
119
|
-
const dy = rand(...range) * (Math.random() < 0.5 ? -1 : 1);
|
|
120
|
-
return { dx, dy };
|
|
121
|
-
}
|
|
122
|
-
function typingDelay(config = DEFAULT_STEALTH_CONFIG) {
|
|
123
|
-
const roll = Math.random();
|
|
124
|
-
const r = config.typingRhythm;
|
|
125
|
-
if (roll < r.pauseProb) return rand(...r.pauseRange);
|
|
126
|
-
if (roll < r.pauseProb + r.fastProb) return rand(...r.fastRange);
|
|
127
|
-
return rand(...r.normalRange);
|
|
128
|
-
}
|
|
129
|
-
function wheelDelta(step, config = DEFAULT_STEALTH_CONFIG) {
|
|
130
|
-
return Math.round(
|
|
131
|
-
config.wheelPeak * Math.exp(-step * config.wheelDecayRate) * rand(0.85, 1.15)
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
var KEY_MAP = {};
|
|
135
|
-
for (let i = 97; i <= 122; i++) {
|
|
136
|
-
const ch = String.fromCharCode(i);
|
|
137
|
-
KEY_MAP[ch] = { key: ch, code: "Key" + ch.toUpperCase(), vk: i - 32 };
|
|
138
|
-
}
|
|
139
|
-
for (let i = 65; i <= 90; i++) {
|
|
140
|
-
const ch = String.fromCharCode(i);
|
|
141
|
-
KEY_MAP[ch] = { key: ch, code: "Key" + ch, vk: i, shift: true };
|
|
142
|
-
}
|
|
143
|
-
for (let i = 48; i <= 57; i++) {
|
|
144
|
-
const ch = String.fromCharCode(i);
|
|
145
|
-
KEY_MAP[ch] = { key: ch, code: "Digit" + ch, vk: i };
|
|
146
|
-
}
|
|
147
|
-
Object.assign(KEY_MAP, {
|
|
148
|
-
" ": { key: " ", code: "Space", vk: 32 },
|
|
149
|
-
".": { key: ".", code: "Period", vk: 190 },
|
|
150
|
-
"-": { key: "-", code: "Minus", vk: 189 },
|
|
151
|
-
"@": { key: "@", code: "Digit2", vk: 50, shift: true },
|
|
152
|
-
"_": { key: "_", code: "Minus", vk: 189, shift: true }
|
|
153
|
-
});
|
|
154
|
-
function buildStealthInitScript(config = DEFAULT_STEALTH_CONFIG) {
|
|
155
|
-
const prof = config.uaChProfile;
|
|
156
|
-
const profJson = prof ? JSON.stringify(prof) : "null";
|
|
157
|
-
return [
|
|
158
|
-
"(function(){",
|
|
159
|
-
' window.__xbStealthVer="57";',
|
|
160
|
-
" window.__xbUaChProf=" + profJson + ";",
|
|
161
|
-
// 1. AEL event proxy
|
|
162
|
-
" var o=EventTarget.prototype.addEventListener;",
|
|
163
|
-
" var fc=new InputDeviceCapabilities({firesTouchEvents:false});",
|
|
164
|
-
" var _ael=function(t,f){",
|
|
165
|
-
" var op=arguments[2];",
|
|
166
|
-
' if(typeof f!=="function")return o.call(this,t,f,op);',
|
|
167
|
-
" var w=function(e){",
|
|
168
|
-
" if(!e)return f.call(this,e);",
|
|
169
|
-
" return f.call(this,new Proxy(e,{get:function(k,p){",
|
|
170
|
-
' if(p==="sourceCapabilities")return fc;',
|
|
171
|
-
// S124 修正(d67 攻防):恒 true 是"过度伪装"——站点自己的合成事件
|
|
172
|
-
// 也变 true → 直接判定浏览器被篡改(比检测自动化更严重的指纹暴露)。
|
|
173
|
-
// 正确策略:透传原始值(CDP Input 事件本来就是 true,JS 合成本来是 false)
|
|
174
|
-
// + paste 定向伪装(合成的 paste 在真实场景都是 trusted 的键盘操作产物)
|
|
175
|
-
' if(p==="isTrusted"){',
|
|
176
|
-
' var orig=Reflect.get(k,"isTrusted");',
|
|
177
|
-
" if(orig===true)return true;",
|
|
178
|
-
' if(k.type==="paste")return true;',
|
|
179
|
-
" return orig;",
|
|
180
|
-
" }",
|
|
181
|
-
' if((p==="clientX"||p==="clientY")&&k.type==="click"&&Number.isInteger(k[p])){',
|
|
182
|
-
" var _f=((k.timeStamp||Date.now())%89)/89*0.7+0.15;",
|
|
183
|
-
" return k[p]+_f;",
|
|
184
|
-
" }",
|
|
185
|
-
' var v=Reflect.get(k,p);return typeof v==="function"?v.bind(k):v;',
|
|
186
|
-
" }}));",
|
|
187
|
-
" };",
|
|
188
|
-
" return o.call(this,t,w,op);",
|
|
189
|
-
" };",
|
|
190
|
-
" EventTarget.prototype.addEventListener=_ael;",
|
|
191
|
-
// window.event isTrusted 伪装(S123):AEL 参数包装收不到 window.event
|
|
192
|
-
// (它是原始 Event 对象的隐式引用)。定义 getter 拦截读取。
|
|
193
|
-
" try{",
|
|
194
|
-
' var _weDesc=Object.getOwnPropertyDescriptor(Window.prototype,"event");',
|
|
195
|
-
" if(_weDesc&&_weDesc.get){",
|
|
196
|
-
" var _origWeGet=_weDesc.get;",
|
|
197
|
-
' Object.defineProperty(Window.prototype,"event",{',
|
|
198
|
-
" configurable:true,",
|
|
199
|
-
" get:function(){",
|
|
200
|
-
" var ev=_origWeGet.call(this);",
|
|
201
|
-
" if(!ev)return ev;",
|
|
202
|
-
" return new Proxy(ev,{get:function(k,p){",
|
|
203
|
-
' if(p==="isTrusted"){',
|
|
204
|
-
' var orig=Reflect.get(k,"isTrusted");',
|
|
205
|
-
" if(orig===true)return true;",
|
|
206
|
-
' if(k.type==="paste")return true;',
|
|
207
|
-
" return orig;",
|
|
208
|
-
" }",
|
|
209
|
-
' var v=Reflect.get(k,p);return typeof v==="function"?v.bind(k):v;',
|
|
210
|
-
" }});",
|
|
211
|
-
" }",
|
|
212
|
-
" });",
|
|
213
|
-
" }",
|
|
214
|
-
" }catch(e){}",
|
|
215
|
-
// 2. Screen override (prototype-level, not instance-level)
|
|
216
|
-
" var _gw=function(){return 1728};",
|
|
217
|
-
" var _gh=function(){return 1117};",
|
|
218
|
-
" var _gah=function(){return 1092};",
|
|
219
|
-
' Object.defineProperty(Screen.prototype,"width",{get:_gw,configurable:true});',
|
|
220
|
-
' Object.defineProperty(Screen.prototype,"height",{get:_gh,configurable:true});',
|
|
221
|
-
' Object.defineProperty(Screen.prototype,"availWidth",{get:_gw,configurable:true});',
|
|
222
|
-
' Object.defineProperty(Screen.prototype,"availHeight",{get:_gah,configurable:true});',
|
|
223
|
-
// S174: UA Headless 标记清洗——headless launch 的 UA 含 HeadlessChrome/xxx
|
|
224
|
-
// (最高频检测面),同步清洗 userAgent/appVersion/userAgentData(brands+高熵值)。
|
|
225
|
-
// 原型+实例双层覆写(S172 模式)。
|
|
226
|
-
" try{",
|
|
227
|
-
' var fixUA=function(ua){return String(ua).replace("HeadlessChrome","Chrome");};',
|
|
228
|
-
' var fixBrands=function(list){return (list||[]).map(function(b){return (b.brand&&b.brand.indexOf("Headless")!==-1)?Object.assign({},b,{brand:b.brand.replace("HeadlessChrome","Chrome")}):b;});};',
|
|
229
|
-
' var _nuad=Object.getOwnPropertyDescriptor(Navigator.prototype,"userAgentData");',
|
|
230
|
-
" var wrapUAD=function(uad){",
|
|
231
|
-
" if(!uad||uad.__xbUA)return uad;",
|
|
232
|
-
" try{",
|
|
233
|
-
' Object.defineProperty(uad,"brands",{get:function(){return fixBrands(_nuad.get.call(navigator).brands);},configurable:true});',
|
|
234
|
-
" var _ghev=uad.getHighEntropyValues.bind(uad);",
|
|
235
|
-
" uad.getHighEntropyValues=function(hints){return _ghev(hints).then(function(v){",
|
|
236
|
-
' var o={};for(var k in v){o[k]=(typeof v[k]==="string")?fixUA(v[k]):(Array.isArray(v[k])?fixBrands(v[k]):v[k]);}',
|
|
237
|
-
" return o;});};",
|
|
238
|
-
" uad.__xbUA=true;",
|
|
239
|
-
" }catch(e){}",
|
|
240
|
-
" return uad;",
|
|
241
|
-
" };",
|
|
242
|
-
' var _nua=Object.getOwnPropertyDescriptor(Navigator.prototype,"userAgent");',
|
|
243
|
-
" if(_nua&&_nua.get){",
|
|
244
|
-
" var _nuag=function(){return fixUA(_nua.get.call(this));};",
|
|
245
|
-
' _nuag.toString=function(){return "function get userAgent() { [native code] }";};',
|
|
246
|
-
' Object.defineProperty(Navigator.prototype,"userAgent",{get:_nuag,configurable:true});',
|
|
247
|
-
" }",
|
|
248
|
-
' Object.defineProperty(navigator,"userAgent",{get:function(){return fixUA(_nua?_nua.get.call(this):"");},configurable:true});',
|
|
249
|
-
" if(_nuad&&_nuad.get){",
|
|
250
|
-
" var _nuadg=function(){return wrapUAD(_nuad.get.call(this));};",
|
|
251
|
-
' _nuadg.toString=function(){return "function get userAgentData() { [native code] }";};',
|
|
252
|
-
' Object.defineProperty(Navigator.prototype,"userAgentData",{get:_nuadg,configurable:true});',
|
|
253
|
-
" }",
|
|
254
|
-
// S190: userAgentData 垫片——headless Chrome 151 不暴露 UA-CH API,
|
|
255
|
-
// 缺失本身即 headless 指纹。从 UA 派生 brands/platform 垫片 + getHighEntropyValues。
|
|
256
|
-
" if(!navigator.userAgentData && window.__xbUaChProf){",
|
|
257
|
-
" var prof = window.__xbUaChProf;",
|
|
258
|
-
" var uad={",
|
|
259
|
-
' brands:prof.brands.map(function(b){return {brand:b.brand,version:b.version.split(".")[0]}}),',
|
|
260
|
-
" mobile:false,",
|
|
261
|
-
" platform:prof.platform,",
|
|
262
|
-
" getHighEntropyValues:function(hints){",
|
|
263
|
-
" return Promise.resolve().then(function(){",
|
|
264
|
-
" var all={brands:uad.brands,mobile:false,platform:prof.platform,",
|
|
265
|
-
" platformVersion:prof.platformVersion,architecture:prof.architecture,bitness:prof.bitness,model:prof.model,",
|
|
266
|
-
" uaFullVersion:prof.uaFullVersion,",
|
|
267
|
-
" fullVersionList:prof.fullVersionList};",
|
|
268
|
-
" var out={};",
|
|
269
|
-
" (hints||[]).forEach(function(h){if(h in all)out[h]=all[h]});",
|
|
270
|
-
" return out;",
|
|
271
|
-
" });",
|
|
272
|
-
" },",
|
|
273
|
-
" toJSON:function(){return{brands:uad.brands,mobile:false,platform:prof.platform}}",
|
|
274
|
-
" };",
|
|
275
|
-
' Object.defineProperty(navigator,"userAgentData",{get:function(){return uad},configurable:true});',
|
|
276
|
-
" }",
|
|
277
|
-
" }catch(e){}",
|
|
278
|
-
// hasFocus 与 visibilityState 联动(d29):恒 true 在 hidden 标签下暴露
|
|
279
|
-
// —— 真实浏览器失焦/后台时 hasFocus=false
|
|
280
|
-
// S172: 原型层覆写——实例赋值可被 Document.prototype.hasFocus.call(document) 逃逸
|
|
281
|
-
' Document.prototype.hasFocus=function(){return document.visibilityState==="visible";};',
|
|
282
|
-
// 4. Canvas/WebGL fingerprint: per-session stable noise (d20).
|
|
283
|
-
// Headless software raster differs subtly from Chrome GPU raster —
|
|
284
|
-
// toDataURL hashes fingerprint the rasterizer. Inject a stable
|
|
285
|
-
// (per-page-load) subpixel shift into fillText so hashes look like a
|
|
286
|
-
// distinct-but-consistent real GPU, and hide the HEADLESS tell in
|
|
287
|
-
// WebGL renderer strings.
|
|
288
|
-
" var _seed=Math.floor(Math.random()*2147483647);",
|
|
289
|
-
" var _prng=function(){_seed=(_seed*48271)%2147483647;return _seed/2147483647;};",
|
|
290
|
-
" var _dx=(_prng()*0.4-0.2).toFixed(3)*1, _dy=(_prng()*0.4-0.2).toFixed(3)*1;",
|
|
291
|
-
" var _fillText=CanvasRenderingContext2D.prototype.fillText;",
|
|
292
|
-
" CanvasRenderingContext2D.prototype.fillText=function(t,x,y,m){",
|
|
293
|
-
" return _fillText.call(this,t,x+_dx,y+_dy,m);",
|
|
294
|
-
" };",
|
|
295
|
-
" var _toDataURL=HTMLCanvasElement.prototype.toDataURL;",
|
|
296
|
-
" HTMLCanvasElement.prototype.toDataURL=function(){",
|
|
297
|
-
' var ctx=this.getContext("2d");',
|
|
298
|
-
" if(ctx){var d=ctx.getImageData(0,0,Math.min(this.width,2),Math.min(this.height,2));",
|
|
299
|
-
" for(var i=0;i<d.data.length;i+=4){if(d.data[i+3]>0){d.data[i]^=1;break;}}",
|
|
300
|
-
" ctx.putImageData(d,0,0);}",
|
|
301
|
-
" return _toDataURL.apply(this,arguments);",
|
|
302
|
-
" };",
|
|
303
|
-
" try{",
|
|
304
|
-
" var _gl=HTMLCanvasElement.prototype.getContext;",
|
|
305
|
-
" HTMLCanvasElement.prototype.getContext=function(t,o){",
|
|
306
|
-
" var c=_gl.call(this,t,o);",
|
|
307
|
-
' if(c&&(t==="webgl"||t==="experimental-webgl")&&c.getParameter){',
|
|
308
|
-
" var _gp=c.getParameter.bind(c);",
|
|
309
|
-
" c.getParameter=function(p){",
|
|
310
|
-
" var v=_gp(p);",
|
|
311
|
-
' if(typeof v==="string"&&/SwiftShader|Software|Rasterizer|Headless/i.test(v))',
|
|
312
|
-
' return "ANGLE (Apple, ANGLE Metal Renderer: Apple M2 Max, Unspecified Version)";',
|
|
313
|
-
" return v;};}",
|
|
314
|
-
" return c;};",
|
|
315
|
-
" }catch(e){}",
|
|
316
|
-
// 5. AudioContext fingerprint: per-load stable micro-noise on channel
|
|
317
|
-
// data (d21). DSP sum differences between headless software audio and
|
|
318
|
-
// real hardware audio are a classic fingerprint — add ±1e-7 level
|
|
319
|
-
// noise (inaudible, changes the sum hash).
|
|
320
|
-
" try{",
|
|
321
|
-
" var _gcd=AudioBuffer.prototype.getChannelData;",
|
|
322
|
-
" AudioBuffer.prototype.getChannelData=function(ch){",
|
|
323
|
-
" var d=_gcd.call(this,ch);",
|
|
324
|
-
' var key="__xb_audio_"+ch;',
|
|
325
|
-
" if(!this[key]){",
|
|
326
|
-
" this[key]=true;",
|
|
327
|
-
" for(var i=0;i<d.length;i+=997){d[i]=d[i]+(_prng()-0.5)*2e-7;}",
|
|
328
|
-
" }",
|
|
329
|
-
" return d;",
|
|
330
|
-
" };",
|
|
331
|
-
" var _gffd=AnalyserNode.prototype.getFloatFrequencyData;",
|
|
332
|
-
" AnalyserNode.prototype.getFloatFrequencyData=function(arr){",
|
|
333
|
-
" _gffd.call(this,arr);",
|
|
334
|
-
" for(var i=0;i<arr.length;i+=31){arr[i]=arr[i]+(_prng()-0.5)*0.01;}",
|
|
335
|
-
" };",
|
|
336
|
-
" var _gfbd=AnalyserNode.prototype.getByteFrequencyData;",
|
|
337
|
-
" AnalyserNode.prototype.getByteFrequencyData=function(arr){",
|
|
338
|
-
" _gfbd.call(this,arr);",
|
|
339
|
-
" for(var i=0;i<arr.length;i+=31){arr[i]=(arr[i]+((_prng()*3)|0))&255;}",
|
|
340
|
-
" };",
|
|
341
|
-
" }catch(e){}",
|
|
342
|
-
// 3b. Font metrics + speechSynthesis + battery (d22): headless tells
|
|
343
|
-
" try{",
|
|
344
|
-
' var _tmw=Object.getOwnPropertyDescriptor(TextMetrics.prototype,"width");',
|
|
345
|
-
" if(_tmw&&_tmw.get){",
|
|
346
|
-
" // S172: \u539F\u578B\u5C42\u8986\u5199\u2014\u2014\u5B9E\u4F8B(m)\u8986\u5199\u53EF\u88AB TextMetrics.prototype.width getter \u9003\u9038",
|
|
347
|
-
' Object.defineProperty(TextMetrics.prototype,"width",{get:function(){return _tmw.get.call(this)+_dx*0.01;},configurable:true});',
|
|
348
|
-
" }",
|
|
349
|
-
" }catch(e){}",
|
|
350
|
-
" try{",
|
|
351
|
-
' var _fakeVoices=["Alex","Daniel","Karen","Moira","Ralph","Samantha","Ting-Ting","Mei-Jia","Sinji","Yunda"];',
|
|
352
|
-
// S172: 原型层覆写——实例赋值可被 SpeechSynthesis.prototype.getVoices.call() 逃逸
|
|
353
|
-
" var _sv=SpeechSynthesis.prototype.getVoices;",
|
|
354
|
-
" SpeechSynthesis.prototype.getVoices=function(){",
|
|
355
|
-
" var real=_sv.call(this);",
|
|
356
|
-
" if(real&&real.length>50)return real;",
|
|
357
|
-
// 注意阈值从 0 改 50:iframe 原生约 10 个 —— 原阈值下 iframe 返回原生 10 个
|
|
358
|
-
// 而不是伪装 180(d33 暴露)。>50 = 主文档伪装列表已生效,其余返回伪装。
|
|
359
|
-
' 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};});',
|
|
360
|
-
" };",
|
|
361
|
-
// d33 修复:iframe 的 speechSynthesis 是独立实例(主文档 hook 不可达)——
|
|
362
|
-
// 轮询同源 iframe,对其 contentWindow 的原型同样 patch(S172: 原型层,防逃逸)
|
|
363
|
-
" try{",
|
|
364
|
-
" var _piv=function(){",
|
|
365
|
-
' var frs=document.querySelectorAll("iframe");',
|
|
366
|
-
" for(var i=0;i<frs.length;i++){try{",
|
|
367
|
-
" var cw=frs[i].contentWindow;",
|
|
368
|
-
" if(!cw||!cw.speechSynthesis||cw.speechSynthesis.__xbP)continue;",
|
|
369
|
-
" cw.speechSynthesis.__xbP=true;",
|
|
370
|
-
" var _sv2=cw.SpeechSynthesis.prototype.getVoices;",
|
|
371
|
-
" cw.SpeechSynthesis.prototype.getVoices=function(){var r=_sv2.call(this);if(r&&r.length>50)return r;",
|
|
372
|
-
' 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};});};',
|
|
373
|
-
" }catch(e2){}}",
|
|
374
|
-
" };",
|
|
375
|
-
" setInterval(_piv,2000);",
|
|
376
|
-
" var _mo=new MutationObserver(function(muts){",
|
|
377
|
-
" for(var k=0;k<muts.length;k++){var ns=muts[k].addedNodes;",
|
|
378
|
-
' for(var q=0;q<ns.length;q++){if(ns[q].tagName==="IFRAME")setTimeout(_piv,20);}}',
|
|
379
|
-
" });",
|
|
380
|
-
" _mo.observe(document.documentElement,{childList:true,subtree:true});",
|
|
381
|
-
" }catch(e){}",
|
|
382
|
-
" }catch(e){}",
|
|
383
|
-
" try{",
|
|
384
|
-
" var _gb=navigator.getBattery.bind(navigator);",
|
|
385
|
-
" navigator.getBattery=function(){",
|
|
386
|
-
" return _gb().then(function(b){",
|
|
387
|
-
' Object.defineProperty(b,"charging",{get:function(){return true;},configurable:true});',
|
|
388
|
-
" return b;});",
|
|
389
|
-
" };",
|
|
390
|
-
" }catch(e){}",
|
|
391
|
-
// 3c. WebRTC local IP leak guard (d23): headless STUN candidates can
|
|
392
|
-
// expose host IPs; mDNS-only candidates are the modern Chrome default.
|
|
393
|
-
" try{",
|
|
394
|
-
" var _oc=RTCPeerConnection.prototype.createOffer;",
|
|
395
|
-
" RTCPeerConnection.prototype.createOffer=function(){",
|
|
396
|
-
" var p=_oc.apply(this,arguments);",
|
|
397
|
-
" var self=this;",
|
|
398
|
-
" return p.then(function(offer){",
|
|
399
|
-
' offer.sdp=offer.sdp.split(String.fromCharCode(10)).filter(function(l){return l.indexOf("typ host")<0}).join(String.fromCharCode(10));',
|
|
400
|
-
" return offer;});",
|
|
401
|
-
" };",
|
|
402
|
-
" }catch(e){}",
|
|
403
|
-
// 3d-2. performance.now precision clamp (d34): full-precision timestamps
|
|
404
|
-
// (11 decimals) differ from site-isolation-clamped real Chrome (~100μs).
|
|
405
|
-
" try{",
|
|
406
|
-
" var _pn=Performance.prototype.now;",
|
|
407
|
-
" // S172: \u539F\u578B\u5C42\u8986\u5199\u2014\u2014\u5B9E\u4F8B\u8D4B\u503C\u53EF\u88AB Performance.prototype.now.call(performance) \u9003\u9038",
|
|
408
|
-
" Performance.prototype.now=function(){return Math.round(_pn.call(this)*10)/10;};",
|
|
409
|
-
' var _pto=Object.getOwnPropertyDescriptor(Performance.prototype,"timeOrigin");',
|
|
410
|
-
" if(_pto&&_pto.get){",
|
|
411
|
-
" // S173: timeOrigin \u771F\u4F2A\u88C5\u2014\u2014\u5360\u4F4D\u5C42\uFF08\u8FD4\u56DE\u771F\u503C\uFF09\u5347\u7EA7\u4E3A\u4F1A\u8BDD\u7EA7\u968F\u673A\u504F\u79FB\u3002",
|
|
412
|
-
" // \u9632 cross-page \u5BFC\u822A\u65F6\u523B\u5173\u8054\uFF1A\u4E0D\u540C\u7AD9\u70B9\u7528 timeOrigin \u7CBE\u786E\u5BF9\u9F50\u7528\u6237\u884C\u4E3A\u6863\u6848\u3002",
|
|
413
|
-
" // \u504F\u79FB\u7531\u672C\u9875 _seed \u6D3E\u751F\uFF08\u9875\u5185\u81EA\u6D3D\uFF0Cnow()+\u504F\u79FB\u4E00\u81F4\uFF09\uFF0C\u8DE8\u9875\u968F\u673A\uFF08\u4E0D\u53EF\u5173\u8054\uFF09\u3002",
|
|
414
|
-
" var _toff=Math.floor((_seed/2147483647)*600000-300000);",
|
|
415
|
-
" var _tog=function(){return _pto.get.call(performance)+_toff;};",
|
|
416
|
-
' _tog.toString=function(){return "function get timeOrigin() { [native code] }";};',
|
|
417
|
-
" // S172 \u5BA1\u8BA1\uFF1A\u5B9E\u4F8B\u8986\u5199\u53EF\u88AB\u539F\u578B getter \u9003\u9038\u2014\u2014\u539F\u578B\u5C42\u8986\u5199",
|
|
418
|
-
' Object.defineProperty(Performance.prototype,"timeOrigin",{get:_tog,configurable:true});',
|
|
419
|
-
' Object.defineProperty(performance,"timeOrigin",{get:_tog,configurable:true});',
|
|
420
|
-
" }",
|
|
421
|
-
" }catch(e){}",
|
|
422
|
-
// 3f. getCoalescedEvents 合成(d47):真实鼠标 125Hz 采样被浏览器按帧合并,
|
|
423
|
-
// 快速移动时 pointermove.getCoalescedEvents() 返回 2~6 个事件(群内
|
|
424
|
-
// ≈8ms);CDP Input 逐事件派发不走合并管线,coalesced>1 恒为 0(结构性
|
|
425
|
-
// 暴露,间隔模拟救不了——实测 Chrome 帧对齐时 CDP 连发事件被直接丢弃
|
|
426
|
-
// 而非合并)。按 125Hz 物理插值合成 coalesced 群:期望样本数 = dt/8ms-1,
|
|
427
|
-
// 合成事件用真 PointerEvent 构造 + 实例级 isTrusted/timeStamp 遮蔽。
|
|
428
|
-
" try{",
|
|
429
|
-
" if(window.PointerEvent&&PointerEvent.prototype.getCoalescedEvents){",
|
|
430
|
-
" var _gce=PointerEvent.prototype.getCoalescedEvents;",
|
|
431
|
-
" var _lm=null;",
|
|
432
|
-
" var _gceH=function(){",
|
|
433
|
-
" var list=_gce.call(this);",
|
|
434
|
-
' if(this.type!=="pointermove"||!this.isTrusted)return list;',
|
|
435
|
-
" var cur={x:this.clientX,y:this.clientY,ts:this.timeStamp};",
|
|
436
|
-
" var out=null;",
|
|
437
|
-
" if((!list||list.length<2)&&_lm){",
|
|
438
|
-
" var dt=cur.ts-_lm.ts,dx=cur.x-_lm.x,dy=cur.y-_lm.y;",
|
|
439
|
-
// dt 长(帧丢弃后)不代表群覆盖整个 dt —— 真实 coalesced 群只覆盖
|
|
440
|
-
// 最后一帧窗口(≤16.7ms,群内 ≈8ms=125Hz)。合成群从自身往回推 8ms
|
|
441
|
-
// 链,位移取末端占比(span/dt,dt 远大于 span 时位移趋零=丢弃后实况)。
|
|
442
|
-
" var expN=Math.floor(dt/8)-1;",
|
|
443
|
-
// dt<12ms(不足一帧+采样周期)时真实浏览器不会产生合并群 ——
|
|
444
|
-
// 单采样帧 coalesced=1,强行合成会出现 ~2ms 的超物理群内间隔。
|
|
445
|
-
" if(expN>0&&dt>=12&&Math.random()>0.15){",
|
|
446
|
-
" var n=Math.min(4,expN+(Math.random()<0.3?1:0));",
|
|
447
|
-
" var span=Math.min(dt,n*8.3);",
|
|
448
|
-
" var frac=Math.min(1,span/Math.max(dt,1));",
|
|
449
|
-
" out=[];",
|
|
450
|
-
" for(var i=1;i<=n;i++){",
|
|
451
|
-
// k=距自身的步数(含自身共 n+1 个事件,相邻恒 ≈8.3ms=125Hz 周期)
|
|
452
|
-
" var k=n+1-i;",
|
|
453
|
-
' var ev=new PointerEvent("pointermove",{',
|
|
454
|
-
' pointerId:this.pointerId,pointerType:"mouse",isPrimary:this.isPrimary,',
|
|
455
|
-
" clientX:cur.x-dx*frac*(k/(n+1))+(Math.random()-0.5)*1.5,",
|
|
456
|
-
" clientY:cur.y-dy*frac*(k/(n+1))+(Math.random()-0.5)*1.5,",
|
|
457
|
-
" screenX:this.screenX,screenY:this.screenY,",
|
|
458
|
-
" buttons:this.buttons,button:this.button,",
|
|
459
|
-
" bubbles:true,cancelable:true,composed:true,",
|
|
460
|
-
" width:this.width,height:this.height,pressure:this.pressure,",
|
|
461
|
-
" tiltX:this.tiltX,tiltY:this.tiltY,twist:this.twist});",
|
|
462
|
-
" var tsV=cur.ts-k*8.3-Math.random()*1.2;",
|
|
463
|
-
// isTrusted/timeStamp 是实例不可配置属性(defineProperty 抛
|
|
464
|
-
// "Cannot redefine"),改 Proxy 包装:getPrototypeOf/ownKeys 透传,
|
|
465
|
-
// instanceof 与属性枚举行为与真事件一致。IIFE 捕获本次循环的 tsV
|
|
466
|
-
// 快照 —— var 提升会让所有 Proxy 闭包共享最后一次赋值(实测四个
|
|
467
|
-
// 合成事件同时间戳、群内间隔塌到 ~2ms)。
|
|
468
|
-
" out.push((function(e2,t2){return new Proxy(e2,{get:function(t,p){",
|
|
469
|
-
' if(p==="isTrusted")return true;',
|
|
470
|
-
' if(p==="timeStamp")return t2;',
|
|
471
|
-
' var v=Reflect.get(t,p);return typeof v==="function"?v.bind(t):v;',
|
|
472
|
-
" }});})(ev,tsV));",
|
|
473
|
-
" }",
|
|
474
|
-
" out.push(this);",
|
|
475
|
-
" }",
|
|
476
|
-
" }",
|
|
477
|
-
" _lm=cur;",
|
|
478
|
-
" return out||list;",
|
|
479
|
-
" };",
|
|
480
|
-
" PointerEvent.prototype.getCoalescedEvents=_gceH;",
|
|
481
|
-
// name 反查伪装:var _gceH=fn 的具名推断会暴露 hook(原生 name 是
|
|
482
|
-
// "getCoalescedEvents")——toString 白名单管不到 name 属性。
|
|
483
|
-
' try{Object.defineProperty(_gceH,"name",{value:"getCoalescedEvents"});}catch(e){}',
|
|
484
|
-
" }",
|
|
485
|
-
" }catch(e){}",
|
|
486
|
-
// 3d. Chrome object depth (d24): automation fakes usually only set
|
|
487
|
-
// window.chrome = {}; deep checks hit app.run/runtime/csi/loadTimes.
|
|
488
|
-
" try{",
|
|
489
|
-
" if(window.chrome){",
|
|
490
|
-
' 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"}};',
|
|
491
|
-
' 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};',
|
|
492
|
-
" if(!window.chrome.csi)window.chrome.csi=function(){return{};}",
|
|
493
|
-
" if(!window.chrome.loadTimes)window.chrome.loadTimes=function(){return{};}",
|
|
494
|
-
" }",
|
|
495
|
-
" }catch(e){}",
|
|
496
|
-
// 3e. Font availability patch (d25): headless Chromium misses some system
|
|
497
|
-
// fonts (e.g. Menlo on macOS) that real Chrome has. Register a FontFace
|
|
498
|
-
// aliasing the missing font to a local() equivalent so offsetWidth-based
|
|
499
|
-
// font probing sees the same availability as the faked environment.
|
|
500
|
-
" try{",
|
|
501
|
-
' var _fontAliases=[["Menlo","Courier New"],["SF Mono","Menlo"],["Segoe UI","Helvetica"]];',
|
|
502
|
-
" _fontAliases.forEach(function(pa){",
|
|
503
|
-
' try{var ff=new FontFace(pa[0],"local("+JSON.stringify(pa[1])+")");',
|
|
504
|
-
" document.fonts.add(ff);ff.load();}catch(e2){}",
|
|
505
|
-
" });",
|
|
506
|
-
" }catch(e){}",
|
|
507
|
-
// 3g. Notification.requestPermission 延迟(d62):headless 无原生横幅,
|
|
508
|
-
// request 瞬回 denied —— 真机 request 会 pending 在横幅上数秒到分钟。
|
|
509
|
-
// 延迟 2-6s 再 resolve,模拟横幅期(用户"看了下然后拒绝")。
|
|
510
|
-
" try{",
|
|
511
|
-
" if(window.Notification&&Notification.requestPermission){",
|
|
512
|
-
" var _nrp=Notification.requestPermission.bind(Notification);",
|
|
513
|
-
" Notification.requestPermission=function(cb){",
|
|
514
|
-
" return new Promise(function(res){",
|
|
515
|
-
" setTimeout(function(){_nrp().then(function(r){if(cb)try{cb(r)}catch(e){}res(r);});},2000+Math.random()*4000);",
|
|
516
|
-
" });",
|
|
517
|
-
" };",
|
|
518
|
-
" }",
|
|
519
|
-
" }catch(e){}",
|
|
520
|
-
// 3. toString disguise (name-list based)
|
|
521
|
-
" var _ts=Function.prototype.toString;",
|
|
522
|
-
" var _hf=document.hasFocus;",
|
|
523
|
-
" Function.prototype.toString=function(){",
|
|
524
|
-
' if(this===_ael)return"function addEventListener(type, callback) { [native code] }";',
|
|
525
|
-
' if(this===_hf)return"function hasFocus() { [native code] }";',
|
|
526
|
-
' if(this===_gw)return"function get width() { [native code] }";',
|
|
527
|
-
' if(this===_gh)return"function get height() { [native code] }";',
|
|
528
|
-
' if(this===_gah)return"function get availHeight() { [native code] }";',
|
|
529
|
-
' if(this===CanvasRenderingContext2D.prototype.fillText)return"function fillText() { [native code] }";',
|
|
530
|
-
' if(this===HTMLCanvasElement.prototype.toDataURL)return"function toDataURL() { [native code] }";',
|
|
531
|
-
' if(this===AnalyserNode.prototype.getFloatFrequencyData)return"function getFloatFrequencyData() { [native code] }";',
|
|
532
|
-
' if(this===AudioBuffer.prototype.getChannelData)return"function getChannelData() { [native code] }";',
|
|
533
|
-
' if(this===_gceH)return"function getCoalescedEvents() { [native code] }";',
|
|
534
|
-
' if(this===Document.prototype.hasFocus)return"function hasFocus() { [native code] }";',
|
|
535
|
-
' if(this===Performance.prototype.now)return"function now() { [native code] }";',
|
|
536
|
-
' if(this===SpeechSynthesis.prototype.getVoices)return"function getVoices() { [native code] }";',
|
|
537
|
-
" return _ts.call(this);",
|
|
538
|
-
" };",
|
|
539
|
-
// 4. onclick prototype hijack (dual-stream consistency)
|
|
540
|
-
" var _ba=function(k,p){",
|
|
541
|
-
' if(p==="isTrusted"){',
|
|
542
|
-
' var orig=Reflect.get(k,"isTrusted");',
|
|
543
|
-
" if(orig===true)return true;",
|
|
544
|
-
' if(k.type==="paste")return true;',
|
|
545
|
-
" return orig;",
|
|
546
|
-
" }",
|
|
547
|
-
' if((p==="clientX"||p==="clientY")&&k.type==="click"&&Number.isInteger(k[p])&&k.isTrusted===true){',
|
|
548
|
-
" var _f=((k.timeStamp||Date.now())%89)/89*0.7+0.15;return k[p]+_f;",
|
|
549
|
-
" }",
|
|
550
|
-
' var v=Reflect.get(k,p);return typeof v==="function"?v.bind(k):v;',
|
|
551
|
-
" };",
|
|
552
|
-
' Object.defineProperty(Document.prototype,"onclick",{',
|
|
553
|
-
" configurable:true,",
|
|
554
|
-
" get:function(){var raw=this.__ocRaw||null;if(!raw)return null;var self=this;",
|
|
555
|
-
" return function(e){return raw.call(self,new Proxy(e,{get:function(k,p){return _ba(k,p)}}))}},",
|
|
556
|
-
" set:function(fn){this.__ocRaw=fn}",
|
|
557
|
-
" });",
|
|
558
|
-
"})()"
|
|
559
|
-
].join("\n");
|
|
560
|
-
}
|
|
561
|
-
|
|
562
38
|
// src/cdp-driver/mouse.ts
|
|
563
39
|
var XBMouseImpl = class {
|
|
564
40
|
conn;
|
|
@@ -599,26 +75,26 @@ var XBMouseImpl = class {
|
|
|
599
75
|
await this.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: p.x, y: p.y, button: this._button });
|
|
600
76
|
this._x = p.x;
|
|
601
77
|
this._y = p.y;
|
|
602
|
-
await
|
|
78
|
+
await sleep(p.delay);
|
|
603
79
|
}
|
|
604
80
|
this._x = tx;
|
|
605
81
|
this._y = ty;
|
|
606
82
|
if (_truncated) {
|
|
607
83
|
await this.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: tx, y: ty, button: this._button });
|
|
608
84
|
}
|
|
609
|
-
await
|
|
85
|
+
await sleep(rand(...DEFAULT_STEALTH_CONFIG.aimPause));
|
|
610
86
|
} else {
|
|
611
87
|
await this.move(tx, ty);
|
|
612
88
|
}
|
|
613
|
-
await this.down({ button });
|
|
614
|
-
await
|
|
89
|
+
await this.down({ button, clickCount: opts.clickCount ?? 1 });
|
|
90
|
+
await sleep(stealth ? rand(...DEFAULT_STEALTH_CONFIG.pressDuration) : opts.delay ?? 0);
|
|
615
91
|
const rx = stealth ? this._x + rand(...DEFAULT_STEALTH_CONFIG.releaseDrift) * (Math.random() < 0.5 ? -1 : 1) : this._x;
|
|
616
92
|
const ry = stealth ? this._y + rand(...DEFAULT_STEALTH_CONFIG.releaseDrift) * (Math.random() < 0.5 ? -1 : 1) : this._y;
|
|
617
93
|
this._x = rx;
|
|
618
94
|
this._y = ry;
|
|
619
95
|
await this.send("Input.dispatchMouseEvent", { type: "mouseReleased", x: rx, y: ry, button, clickCount: opts.clickCount ?? 1 });
|
|
620
96
|
for (let i = 1; i < (opts.clickCount ?? 1); i++) {
|
|
621
|
-
if (opts.delay) await
|
|
97
|
+
if (opts.delay) await sleep(opts.delay);
|
|
622
98
|
await this.down({ button });
|
|
623
99
|
await this.up({ button });
|
|
624
100
|
}
|
|
@@ -634,7 +110,9 @@ var XBMouseImpl = class {
|
|
|
634
110
|
x: this._x,
|
|
635
111
|
y: this._y,
|
|
636
112
|
button,
|
|
637
|
-
|
|
113
|
+
// r23: press 必须与 release 声明同一 clickCount——Chrome 按序列计数
|
|
114
|
+
// 合成 click/dblclick,press 缺声明会把计数器重置(dblclick 永不合成)
|
|
115
|
+
clickCount: opts.clickCount ?? 1
|
|
638
116
|
});
|
|
639
117
|
}
|
|
640
118
|
async up(opts = {}) {
|
|
@@ -645,7 +123,7 @@ var XBMouseImpl = class {
|
|
|
645
123
|
x: this._x,
|
|
646
124
|
y: this._y,
|
|
647
125
|
button,
|
|
648
|
-
clickCount: 1
|
|
126
|
+
clickCount: opts.clickCount ?? 1
|
|
649
127
|
});
|
|
650
128
|
}
|
|
651
129
|
async move(x, y, opts = {}) {
|
|
@@ -656,7 +134,7 @@ var XBMouseImpl = class {
|
|
|
656
134
|
const dx = x - fromX;
|
|
657
135
|
const dy = y - fromY;
|
|
658
136
|
for (let i = 1; i <= steps; i++) {
|
|
659
|
-
if (stealth) await
|
|
137
|
+
if (stealth) await sleep(rand(16, 28));
|
|
660
138
|
const t = i / steps;
|
|
661
139
|
this._x = fromX + dx * t;
|
|
662
140
|
this._y = fromY + dy * t;
|
|
@@ -690,7 +168,7 @@ var XBMouseImpl = class {
|
|
|
690
168
|
const steps = opts.steps ?? Math.max(10, Math.min(24, Math.round(Math.hypot(x - this._x, y - this._y) / 20)));
|
|
691
169
|
const fx = this._x, fy = this._y;
|
|
692
170
|
for (let i = 1; i <= steps; i++) {
|
|
693
|
-
if (stealth) await
|
|
171
|
+
if (stealth) await sleep(rand(16, 28));
|
|
694
172
|
const t = i / steps;
|
|
695
173
|
this._x = fx + (x - fx) * t;
|
|
696
174
|
this._y = fy + (y - fy) * t;
|
|
@@ -703,7 +181,7 @@ var XBMouseImpl = class {
|
|
|
703
181
|
}
|
|
704
182
|
this._x = x;
|
|
705
183
|
this._y = y;
|
|
706
|
-
await
|
|
184
|
+
await sleep(rand(60, 140));
|
|
707
185
|
await this.send("Input.dispatchMouseEvent", {
|
|
708
186
|
type: "mouseReleased",
|
|
709
187
|
x,
|
|
@@ -726,7 +204,7 @@ var XBMouseImpl = class {
|
|
|
726
204
|
await this.conn.send(method, params, this.sessionId);
|
|
727
205
|
}
|
|
728
206
|
};
|
|
729
|
-
function
|
|
207
|
+
function sleep(ms) {
|
|
730
208
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
731
209
|
}
|
|
732
210
|
|
|
@@ -756,17 +234,17 @@ var XBKeyboardImpl = class {
|
|
|
756
234
|
await this.dispatchKeyEvent(downParams);
|
|
757
235
|
if (mapping.text) {
|
|
758
236
|
if (process.env.XBROWSER_STEALTH !== "off") {
|
|
759
|
-
await
|
|
237
|
+
await sleep2(rand(...DEFAULT_STEALTH_CONFIG.keyPressDuration));
|
|
760
238
|
}
|
|
761
239
|
await this.dispatchKeyEvent({
|
|
762
240
|
type: "char",
|
|
763
241
|
text: mapping.text
|
|
764
242
|
});
|
|
765
243
|
}
|
|
766
|
-
if (delay > 0) await
|
|
244
|
+
if (delay > 0) await sleep2(delay);
|
|
767
245
|
else {
|
|
768
246
|
if (process.env.XBROWSER_STEALTH !== "off") {
|
|
769
|
-
await
|
|
247
|
+
await sleep2(rand(...DEFAULT_STEALTH_CONFIG.keyPressDuration));
|
|
770
248
|
}
|
|
771
249
|
}
|
|
772
250
|
const upParams = {
|
|
@@ -812,16 +290,16 @@ var XBKeyboardImpl = class {
|
|
|
812
290
|
const fixedDelay = opts.delay ?? 0;
|
|
813
291
|
for (const char of text) {
|
|
814
292
|
if (fixedDelay > 0) {
|
|
815
|
-
await
|
|
293
|
+
await sleep2(fixedDelay);
|
|
816
294
|
} else if (stealth) {
|
|
817
|
-
await
|
|
295
|
+
await sleep2(typingDelay());
|
|
818
296
|
}
|
|
819
297
|
if (stealth && char.length === 1 && Math.random() < DEFAULT_STEALTH_CONFIG.typoProbability) {
|
|
820
298
|
const wrong = NEIGHBOR_KEYS[char];
|
|
821
299
|
if (wrong && wrong !== char) {
|
|
822
300
|
await this.dispatchKeySequence(resolveKeyMapping(wrong), stealth);
|
|
823
|
-
await
|
|
824
|
-
await this.dispatchKeySequence(
|
|
301
|
+
await sleep2(rand(120, 420));
|
|
302
|
+
await this.dispatchKeySequence(KEY_MAP.Backspace, stealth);
|
|
825
303
|
}
|
|
826
304
|
}
|
|
827
305
|
await this.dispatchKeySequence(resolveKeyMapping(char), stealth);
|
|
@@ -851,7 +329,7 @@ var XBKeyboardImpl = class {
|
|
|
851
329
|
await this.dispatchKeyEvent({ type: "char", text: mapping.text });
|
|
852
330
|
}
|
|
853
331
|
if (stealth) {
|
|
854
|
-
await
|
|
332
|
+
await sleep2(rand(...DEFAULT_STEALTH_CONFIG.keyPressDuration));
|
|
855
333
|
}
|
|
856
334
|
const upParams = {
|
|
857
335
|
type: "keyUp",
|
|
@@ -890,7 +368,7 @@ var XBKeyboardImpl = class {
|
|
|
890
368
|
}
|
|
891
369
|
await this.dispatchKeyEvent(downParams);
|
|
892
370
|
if (process.env.XBROWSER_STEALTH !== "off") {
|
|
893
|
-
await
|
|
371
|
+
await sleep2(rand(...DEFAULT_STEALTH_CONFIG.keyPressDuration));
|
|
894
372
|
}
|
|
895
373
|
await this.dispatchKeyEvent({
|
|
896
374
|
type: "keyUp",
|
|
@@ -914,7 +392,7 @@ var XBKeyboardImpl = class {
|
|
|
914
392
|
await this.dispatchKeyEvent({ type: "rawKeyDown", key: m.key, code: m.code, ...m.keyCode ? { windowsVirtualKeyCode: m.keyCode } : {}, modifiers: bits });
|
|
915
393
|
await this.dispatchKeyEvent({ type: "keyDown", key: k.key, code: k.code, ...k.keyCode ? { windowsVirtualKeyCode: k.keyCode } : {}, modifiers: bits });
|
|
916
394
|
if (process.env.XBROWSER_STEALTH !== "off") {
|
|
917
|
-
await
|
|
395
|
+
await sleep2(rand(...DEFAULT_STEALTH_CONFIG.keyPressDuration));
|
|
918
396
|
}
|
|
919
397
|
await this.dispatchKeyEvent({ type: "keyUp", key: k.key, code: k.code, ...k.keyCode ? { windowsVirtualKeyCode: k.keyCode } : {}, modifiers: bits });
|
|
920
398
|
await this.dispatchKeyEvent({ type: "keyUp", key: m.key, code: m.code, ...m.keyCode ? { windowsVirtualKeyCode: m.keyCode } : {}, modifiers: 0 });
|
|
@@ -924,7 +402,7 @@ var XBKeyboardImpl = class {
|
|
|
924
402
|
}
|
|
925
403
|
};
|
|
926
404
|
function resolveKeyMapping(key) {
|
|
927
|
-
if (
|
|
405
|
+
if (KEY_MAP[key]) return KEY_MAP[key];
|
|
928
406
|
if (key.length === 1) {
|
|
929
407
|
const lower = key.toLowerCase();
|
|
930
408
|
if (lower >= "a" && lower <= "z") {
|
|
@@ -941,7 +419,7 @@ function resolveKeyMapping(key) {
|
|
|
941
419
|
}
|
|
942
420
|
return { key, code: key };
|
|
943
421
|
}
|
|
944
|
-
var
|
|
422
|
+
var KEY_MAP = {
|
|
945
423
|
Enter: { key: "Enter", code: "Enter", text: "\r", keyCode: 13 },
|
|
946
424
|
Tab: { key: "Tab", code: "Tab", text: " ", keyCode: 9 },
|
|
947
425
|
Escape: { key: "Escape", code: "Escape", keyCode: 27 },
|
|
@@ -1010,7 +488,7 @@ var NEIGHBOR_KEYS = {
|
|
|
1010
488
|
"8": "9",
|
|
1011
489
|
"9": "0"
|
|
1012
490
|
};
|
|
1013
|
-
function
|
|
491
|
+
function sleep2(ms) {
|
|
1014
492
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1015
493
|
}
|
|
1016
494
|
|
|
@@ -1302,6 +780,14 @@ var XBLocatorImpl = class _XBLocatorImpl {
|
|
|
1302
780
|
await waitForActionable(this.page, this.selector, opts);
|
|
1303
781
|
await scrollIntoView(this.page, this.selector);
|
|
1304
782
|
await this.click({ ...opts });
|
|
783
|
+
await this.page.evaluate(`
|
|
784
|
+
(function() {
|
|
785
|
+
const el = ${this._q(this.selector)};
|
|
786
|
+
if (!el) return;
|
|
787
|
+
if (document.activeElement !== el) el.focus();
|
|
788
|
+
if ((el.value || el.textContent || '') !== '') document.execCommand('selectAll');
|
|
789
|
+
})()
|
|
790
|
+
`);
|
|
1305
791
|
if (process.env.XBROWSER_STEALTH !== "off" && value.length >= 40 && process.env.XBROWSER_FILL_TYPE !== "type") {
|
|
1306
792
|
try {
|
|
1307
793
|
const { pasteViaClipboard, syntheticPaste } = await import("./clipboard-BOL2GP2E.js");
|
|
@@ -2057,6 +1543,189 @@ var XBPageImpl = class _XBPageImpl {
|
|
|
2057
1543
|
await this.conn.send("Network.enable", void 0, this.sessionId);
|
|
2058
1544
|
await this.conn.send("DOM.enable", void 0, this.sessionId).catch(() => {
|
|
2059
1545
|
});
|
|
1546
|
+
if (this._contextImpl.ignoreHTTPSErrors) {
|
|
1547
|
+
await this.conn.send("Security.enable", void 0, this.sessionId).catch(() => {
|
|
1548
|
+
});
|
|
1549
|
+
await this.conn.send("Security.setIgnoreCertificateErrors", { ignore: true }, this.sessionId).catch(() => {
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
if (this._contextImpl.httpCredentials) {
|
|
1553
|
+
await this.conn.send("Fetch.enable", {
|
|
1554
|
+
patterns: [{ urlPattern: "*", requestStage: "Request" }],
|
|
1555
|
+
handleAuthRequests: true
|
|
1556
|
+
}, this.sessionId).catch(() => {
|
|
1557
|
+
});
|
|
1558
|
+
this._subscriptions.push(
|
|
1559
|
+
this.conn.subscribe("Fetch.requestPaused", this.sessionId, (params) => {
|
|
1560
|
+
if (this._interceptionEnabled) return;
|
|
1561
|
+
const p = params;
|
|
1562
|
+
this.conn.send("Fetch.continueRequest", { requestId: p.requestId }, this.sessionId).catch(() => {
|
|
1563
|
+
});
|
|
1564
|
+
})
|
|
1565
|
+
);
|
|
1566
|
+
this._subscriptions.push(
|
|
1567
|
+
this.conn.subscribe("Fetch.authRequired", this.sessionId, (params) => {
|
|
1568
|
+
const p = params;
|
|
1569
|
+
const creds = this._contextImpl.httpCredentials;
|
|
1570
|
+
this.conn.send("Fetch.continueWithAuth", {
|
|
1571
|
+
requestId: p.requestId,
|
|
1572
|
+
authChallengeResponse: creds ? { response: "ProvideCredentials", username: creds.username, password: creds.password } : { response: "CancelAuth" }
|
|
1573
|
+
}, this.sessionId).catch(() => {
|
|
1574
|
+
});
|
|
1575
|
+
})
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
await this.conn.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
1579
|
+
source: [
|
|
1580
|
+
"try { window.print = function() {}; window.__xb_print_suppressed = true; } catch (e) {}",
|
|
1581
|
+
"try {",
|
|
1582
|
+
" if (window.showOpenFilePicker) {",
|
|
1583
|
+
" var __xb_mkHandle = function(name, content) {",
|
|
1584
|
+
' var file = new File([content], name, { type: "text/plain" });',
|
|
1585
|
+
" return {",
|
|
1586
|
+
' kind: "file", name: name,',
|
|
1587
|
+
" getFile: function() { return Promise.resolve(file); },",
|
|
1588
|
+
" createWritable: function() { return Promise.resolve({ write: function() { return Promise.resolve(); }, close: function() { return Promise.resolve(); } }); },",
|
|
1589
|
+
" isSameEntry: function() { return Promise.resolve(false); },",
|
|
1590
|
+
' queryPermission: function() { return Promise.resolve("granted"); },',
|
|
1591
|
+
' requestPermission: function() { return Promise.resolve("granted"); },',
|
|
1592
|
+
" };",
|
|
1593
|
+
" };",
|
|
1594
|
+
" window.showOpenFilePicker = function() {",
|
|
1595
|
+
' return Promise.resolve([__xb_mkHandle("xbrowser-stub.txt", "xbrowser fs access stub")]);',
|
|
1596
|
+
" };",
|
|
1597
|
+
" window.showSaveFilePicker = function() {",
|
|
1598
|
+
' return Promise.resolve(__xb_mkHandle("xbrowser-stub-save.txt", ""));',
|
|
1599
|
+
" };",
|
|
1600
|
+
" window.showDirectoryPicker = function() {",
|
|
1601
|
+
" var emptyIter = function() { return [][Symbol.iterator](); };",
|
|
1602
|
+
" return Promise.resolve({",
|
|
1603
|
+
' kind: "directory", name: "xbrowser-stub-dir",',
|
|
1604
|
+
" values: emptyIter, entries: emptyIter, keys: emptyIter,",
|
|
1605
|
+
' getDirectoryHandle: function() { return Promise.reject(new Error("stub empty dir")); },',
|
|
1606
|
+
' getFileHandle: function() { return Promise.reject(new Error("stub empty dir")); },',
|
|
1607
|
+
" isSameEntry: function() { return Promise.resolve(false); },",
|
|
1608
|
+
' queryPermission: function() { return Promise.resolve("granted"); },',
|
|
1609
|
+
' requestPermission: function() { return Promise.resolve("granted"); },',
|
|
1610
|
+
" });",
|
|
1611
|
+
" };",
|
|
1612
|
+
" window.__xb_fs_stub_installed = true;",
|
|
1613
|
+
" }",
|
|
1614
|
+
"} catch (e) {}",
|
|
1615
|
+
// sup-s7: 右键菜单压制——headful 下 CDP 右键弹浏览器原生菜单(CDP
|
|
1616
|
+
// 无法关闭浏览器 UI)。document 捕获阶段 preventDefault 抑制原生
|
|
1617
|
+
// 菜单;不阻断传播,录制 contextmenu 事件与页面自定义菜单不受影响。
|
|
1618
|
+
"try {",
|
|
1619
|
+
' document.addEventListener("contextmenu", function(e) { e.preventDefault(); }, true);',
|
|
1620
|
+
" window.__xb_ctxmenu_suppressed = true;",
|
|
1621
|
+
"} catch (e) {}",
|
|
1622
|
+
// sup-s10: Notification 守卫——new Notification() 弹 OS 级系统横幅
|
|
1623
|
+
// (headful 污染屏幕,等待通知交互的流程挂起)。no-op 包装但保
|
|
1624
|
+
// API 形状:permission/granted、requestPermission→granted、实例
|
|
1625
|
+
// title/close 可用——页面通知流程无感继续。
|
|
1626
|
+
"try {",
|
|
1627
|
+
" var XBN = function Notification(title, options) {",
|
|
1628
|
+
" this.title = title;",
|
|
1629
|
+
' this.body = (options && options.body) || "";',
|
|
1630
|
+
' this.tag = (options && options.tag) || "";',
|
|
1631
|
+
" this.onclick = null; this.onshow = null; this.onerror = null; this.onclose = null;",
|
|
1632
|
+
" };",
|
|
1633
|
+
" XBN.prototype.close = function() {};",
|
|
1634
|
+
' XBN.permission = "granted";',
|
|
1635
|
+
' XBN.requestPermission = function() { return Promise.resolve("granted"); };',
|
|
1636
|
+
" XBN.maxActions = (window.Notification && window.Notification.maxActions) || 2;",
|
|
1637
|
+
" window.Notification = XBN;",
|
|
1638
|
+
" window.__xb_notification_suppressed = true;",
|
|
1639
|
+
"} catch (e) {}",
|
|
1640
|
+
// sup-W1: WebAPI 选择器/配对框家族 stub(1/7 Serial)——
|
|
1641
|
+
// navigator.serial.requestPort() 弹浏览器原生串口选择器(CDP 无法
|
|
1642
|
+
// 拦截),headless 下 promise 挂起。stub 返回 rejected Promise
|
|
1643
|
+
// (NotFoundError),页面 catch 分支接管、流程继续。
|
|
1644
|
+
"try {",
|
|
1645
|
+
" if (navigator.serial) {",
|
|
1646
|
+
" navigator.serial.requestPort = function() {",
|
|
1647
|
+
' return Promise.reject(new DOMException("xbrowser serial stub", "NotFoundError"));',
|
|
1648
|
+
" };",
|
|
1649
|
+
" window.__xb_serial_stubbed = true;",
|
|
1650
|
+
" }",
|
|
1651
|
+
"} catch (e) {}",
|
|
1652
|
+
// sup-W2: WebAPI stub(2/7 USB)——requestDevice 弹原生设备配对框,
|
|
1653
|
+
// 同 Serial pattern:rejected Promise(NotFoundError),catch 接管。
|
|
1654
|
+
"try {",
|
|
1655
|
+
" if (navigator.usb) {",
|
|
1656
|
+
" navigator.usb.requestDevice = function() {",
|
|
1657
|
+
' return Promise.reject(new DOMException("xbrowser usb stub", "NotFoundError"));',
|
|
1658
|
+
" };",
|
|
1659
|
+
" window.__xb_usb_stubbed = true;",
|
|
1660
|
+
" }",
|
|
1661
|
+
"} catch (e) {}",
|
|
1662
|
+
// sup-W3: WebAPI stub(3/7 Bluetooth)——requestDevice 弹原生蓝牙
|
|
1663
|
+
// 配对框,同 Serial/USB pattern:rejected Promise(NotFoundError)。
|
|
1664
|
+
// CI linux headless 下 navigator.bluetooth 原生不存在——缺失时合成
|
|
1665
|
+
// 对象再包装,保证跨平台语义一致(requestDevice 恒 rejected)。
|
|
1666
|
+
"try {",
|
|
1667
|
+
" if (!navigator.bluetooth) {",
|
|
1668
|
+
' try { Object.defineProperty(navigator, "bluetooth", { value: {}, configurable: true }); } catch (e) {}',
|
|
1669
|
+
" }",
|
|
1670
|
+
" if (navigator.bluetooth) {",
|
|
1671
|
+
" navigator.bluetooth.requestDevice = function() {",
|
|
1672
|
+
' return Promise.reject(new DOMException("xbrowser ble stub", "NotFoundError"));',
|
|
1673
|
+
" };",
|
|
1674
|
+
" window.__xb_ble_stubbed = true;",
|
|
1675
|
+
" }",
|
|
1676
|
+
"} catch (e) {}",
|
|
1677
|
+
// sup-W4: WebAPI stub(4/7 HID)——requestDevices 弹原生 HID 设备
|
|
1678
|
+
// 选择器,同家族 pattern:rejected Promise(NotFoundError)。
|
|
1679
|
+
"try {",
|
|
1680
|
+
" if (navigator.hid) {",
|
|
1681
|
+
" navigator.hid.requestDevices = function() {",
|
|
1682
|
+
' return Promise.reject(new DOMException("xbrowser hid stub", "NotFoundError"));',
|
|
1683
|
+
" };",
|
|
1684
|
+
" window.__xb_hid_stubbed = true;",
|
|
1685
|
+
" }",
|
|
1686
|
+
"} catch (e) {}",
|
|
1687
|
+
// sup-W5: WebAPI stub(5/7 WakeLock)——request('screen') 在无用户
|
|
1688
|
+
// 激活/权限时挂起或拒绝,等待 wake 事件的流程死锁。stub 返回合成
|
|
1689
|
+
// sentinel(release 可用,不真实持锁)。
|
|
1690
|
+
"try {",
|
|
1691
|
+
" if (navigator.wakeLock) {",
|
|
1692
|
+
" navigator.wakeLock.request = function() {",
|
|
1693
|
+
" return Promise.resolve({",
|
|
1694
|
+
" released: false,",
|
|
1695
|
+
' type: "screen",',
|
|
1696
|
+
" release: function() { this.released = true; return Promise.resolve(); },",
|
|
1697
|
+
" addEventListener: function() {},",
|
|
1698
|
+
" removeEventListener: function() {},",
|
|
1699
|
+
" });",
|
|
1700
|
+
" };",
|
|
1701
|
+
" window.__xb_wakelock_stubbed = true;",
|
|
1702
|
+
" }",
|
|
1703
|
+
"} catch (e) {}",
|
|
1704
|
+
// sup-W6: WebAPI stub(6/7 IdleDetector)——start() 需权限气泡授权
|
|
1705
|
+
// 才能继续,headless 下挂起。stub 返回 rejected(NotAllowedError),
|
|
1706
|
+
// 页面 catch 分支接管。
|
|
1707
|
+
"try {",
|
|
1708
|
+
" if (window.IdleDetector) {",
|
|
1709
|
+
" IdleDetector.start = function() {",
|
|
1710
|
+
' return Promise.reject(new DOMException("xbrowser idle stub", "NotAllowedError"));',
|
|
1711
|
+
" };",
|
|
1712
|
+
" window.__xb_idle_stubbed = true;",
|
|
1713
|
+
" }",
|
|
1714
|
+
"} catch (e) {}",
|
|
1715
|
+
// sup-W7: WebAPI stub(7/7 PaymentRequest)——show() 弹浏览器支付
|
|
1716
|
+
// 面板(无用户交互不关闭),headless 下 promise 挂起。stub 返回
|
|
1717
|
+
// rejected(NotAllowedError),页面 catch 分支接管。
|
|
1718
|
+
"try {",
|
|
1719
|
+
" if (window.PaymentRequest) {",
|
|
1720
|
+
" PaymentRequest.prototype.show = function() {",
|
|
1721
|
+
' return Promise.reject(new DOMException("xbrowser pay stub", "NotAllowedError"));',
|
|
1722
|
+
" };",
|
|
1723
|
+
" window.__xb_pay_stubbed = true;",
|
|
1724
|
+
" }",
|
|
1725
|
+
"} catch (e) {}"
|
|
1726
|
+
].join("\n")
|
|
1727
|
+
}, this.sessionId).catch(() => {
|
|
1728
|
+
});
|
|
2060
1729
|
this.setupPageEvents();
|
|
2061
1730
|
this.setupNetworkEvents();
|
|
2062
1731
|
this.setupConsoleEvents();
|
|
@@ -2497,7 +2166,9 @@ Last error: ${lastError.message}` : "";
|
|
|
2497
2166
|
await this.locator(selector).click(opts);
|
|
2498
2167
|
}
|
|
2499
2168
|
async dblclick(selector, opts = {}) {
|
|
2500
|
-
|
|
2169
|
+
const locator = this.locator(selector);
|
|
2170
|
+
await locator.click({ ...opts, clickCount: 1 });
|
|
2171
|
+
await locator.click({ ...opts, clickCount: 2 });
|
|
2501
2172
|
}
|
|
2502
2173
|
async fill(selector, value, opts = {}) {
|
|
2503
2174
|
await this.locator(selector).fill(value, opts);
|
|
@@ -2614,6 +2285,26 @@ Last error: ${lastError.message}` : "";
|
|
|
2614
2285
|
);
|
|
2615
2286
|
this._viewportSize = size;
|
|
2616
2287
|
}
|
|
2288
|
+
/**
|
|
2289
|
+
* env-E2: 缩放变异——deviceScaleFactor 切换(CSS 像素坐标空间不变,
|
|
2290
|
+
* 物理像素翻倍)。保留当前 CSS 视口尺寸,只换 DSF。
|
|
2291
|
+
*/
|
|
2292
|
+
async setDeviceScaleFactor(dsf) {
|
|
2293
|
+
const size = this._viewportSize ?? await this.evaluate(
|
|
2294
|
+
"({ width: window.innerWidth, height: window.innerHeight })"
|
|
2295
|
+
);
|
|
2296
|
+
await this.conn.send(
|
|
2297
|
+
"Emulation.setDeviceMetricsOverride",
|
|
2298
|
+
{
|
|
2299
|
+
width: size.width,
|
|
2300
|
+
height: size.height,
|
|
2301
|
+
deviceScaleFactor: dsf,
|
|
2302
|
+
mobile: false
|
|
2303
|
+
},
|
|
2304
|
+
this.sessionId
|
|
2305
|
+
);
|
|
2306
|
+
this._viewportSize = size;
|
|
2307
|
+
}
|
|
2617
2308
|
// ── Scripts ─────────────────────────────────────────────────
|
|
2618
2309
|
async addInitScript(script) {
|
|
2619
2310
|
await this.conn.send(
|
|
@@ -2837,6 +2528,12 @@ Last error: ${lastError.message}` : "";
|
|
|
2837
2528
|
async _cdpSend(method, params) {
|
|
2838
2529
|
return this.conn.send(method, params, this.sessionId);
|
|
2839
2530
|
}
|
|
2531
|
+
/** sup-s11: beforeunload 自动应答语义——'accept'(默认,离开页面,与录制
|
|
2532
|
+
* 意图一致)或 'dismiss'(取消导航留守原页,尽力而为:此构建上 CDP
|
|
2533
|
+
* accept:false 对浏览器侧导航的取消语义不稳定,不保证)。 */
|
|
2534
|
+
setBeforeUnloadBehavior(mode) {
|
|
2535
|
+
this._beforeUnloadAccept = mode === "accept";
|
|
2536
|
+
}
|
|
2840
2537
|
/**
|
|
2841
2538
|
* 开启/关闭原生文件选择框拦截(CDP stateful 开关)。
|
|
2842
2539
|
*
|
|
@@ -2895,8 +2592,9 @@ Last error: ${lastError.message}` : "";
|
|
|
2895
2592
|
}
|
|
2896
2593
|
};
|
|
2897
2594
|
this._emit("dialog", dialog);
|
|
2595
|
+
const autoAccept = p.type === "beforeunload" ? this._beforeUnloadAccept : false;
|
|
2898
2596
|
setTimeout(() => {
|
|
2899
|
-
this.conn.send("Page.handleJavaScriptDialog", { accept:
|
|
2597
|
+
this.conn.send("Page.handleJavaScriptDialog", { accept: autoAccept }, this.sessionId).catch(() => {
|
|
2900
2598
|
});
|
|
2901
2599
|
}, 0);
|
|
2902
2600
|
})
|
|
@@ -3039,6 +2737,7 @@ Last error: ${lastError.message}` : "";
|
|
|
3039
2737
|
_networkRequests = /* @__PURE__ */ new Map();
|
|
3040
2738
|
_routeHandlers = [];
|
|
3041
2739
|
_interceptionEnabled = false;
|
|
2740
|
+
_beforeUnloadAccept = true;
|
|
3042
2741
|
/** Store network data — called by browser.ts installNetworkCapture or internal event handlers */
|
|
3043
2742
|
_storeNetworkRequest(requestId, data) {
|
|
3044
2743
|
this._networkRequests.set(requestId, { requestId, ...data });
|
|
@@ -3150,7 +2849,10 @@ Last error: ${lastError.message}` : "";
|
|
|
3150
2849
|
if (!this._interceptionEnabled) {
|
|
3151
2850
|
this._interceptionEnabled = true;
|
|
3152
2851
|
await this.conn.send("Fetch.enable", {
|
|
3153
|
-
patterns: [{ urlPattern: "*", requestStage: "Request" }]
|
|
2852
|
+
patterns: [{ urlPattern: "*", requestStage: "Request" }],
|
|
2853
|
+
// sup-s9: 保留认证自动响应(Fetch.enable 为替换语义,不带此参数
|
|
2854
|
+
// 会顶掉 _init 的 auth 配置)
|
|
2855
|
+
handleAuthRequests: true
|
|
3154
2856
|
}, this.sessionId);
|
|
3155
2857
|
this._subscriptions.push(
|
|
3156
2858
|
this.conn.subscribe("Fetch.requestPaused", this.sessionId, (params) => {
|
|
@@ -3203,6 +2905,36 @@ Last error: ${lastError.message}` : "";
|
|
|
3203
2905
|
});
|
|
3204
2906
|
}
|
|
3205
2907
|
// ── setInputFiles ───────────────────────────────────────────
|
|
2908
|
+
/**
|
|
2909
|
+
* sup-s4: Dropzone 拖拽上传模拟——Dropzone/Uppy/自绘上传区只监听
|
|
2910
|
+
* dragover/drop(无 input[type=file] 可点),真实用户靠 OS 文件拖入
|
|
2911
|
+
* 触发,自动化无法产生 OS 拖拽。页内构造 DataTransfer+File,按拖放
|
|
2912
|
+
* 协议派发 dragenter/dragover/drop(bubbles+cancelable,坐标取目标
|
|
2913
|
+
* 中心)。
|
|
2914
|
+
*/
|
|
2915
|
+
async dropFiles(selector, payload) {
|
|
2916
|
+
await this.evaluate(`
|
|
2917
|
+
(function() {
|
|
2918
|
+
var el = ${queryJS(selector)};
|
|
2919
|
+
if (!el) throw new Error('drop target not found: ' + ${JSON.stringify(selector)});
|
|
2920
|
+
var b64 = ${JSON.stringify(payload.buffer.toString("base64"))};
|
|
2921
|
+
var bin = atob(b64);
|
|
2922
|
+
var buf = new Uint8Array(bin.length);
|
|
2923
|
+
for (var i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
2924
|
+
var file = new File([buf], ${JSON.stringify(payload.name)}, { type: ${JSON.stringify(payload.mimeType)} });
|
|
2925
|
+
var dt = new DataTransfer();
|
|
2926
|
+
dt.items.add(file);
|
|
2927
|
+
var rect = el.getBoundingClientRect();
|
|
2928
|
+
var opts = {
|
|
2929
|
+
bubbles: true, cancelable: true,
|
|
2930
|
+
clientX: rect.x + rect.width / 2, clientY: rect.y + rect.height / 2,
|
|
2931
|
+
};
|
|
2932
|
+
el.dispatchEvent(new DragEvent('dragenter', Object.assign({ dataTransfer: dt }, opts)));
|
|
2933
|
+
el.dispatchEvent(new DragEvent('dragover', Object.assign({ dataTransfer: dt }, opts)));
|
|
2934
|
+
el.dispatchEvent(new DragEvent('drop', Object.assign({ dataTransfer: dt }, opts)));
|
|
2935
|
+
})()
|
|
2936
|
+
`);
|
|
2937
|
+
}
|
|
3206
2938
|
async setInputFiles(selector, files) {
|
|
3207
2939
|
const fileArr = Array.isArray(files) ? files : [files];
|
|
3208
2940
|
const fileList = fileArr.map((f) => ({
|
|
@@ -3349,6 +3081,14 @@ var XBContextImpl = class {
|
|
|
3349
3081
|
get stealthConfig() {
|
|
3350
3082
|
return this.options.stealthConfig;
|
|
3351
3083
|
}
|
|
3084
|
+
/** sup-s9: HTTP 认证凭证经 context 传给 page 层(authRequired 自动响应) */
|
|
3085
|
+
get httpCredentials() {
|
|
3086
|
+
return this.options.httpCredentials;
|
|
3087
|
+
}
|
|
3088
|
+
/** sup-s12: 自签/无效证书放行——经 context 传给 page 层(原为死字段) */
|
|
3089
|
+
get ignoreHTTPSErrors() {
|
|
3090
|
+
return this.options.ignoreHTTPSErrors;
|
|
3091
|
+
}
|
|
3352
3092
|
async newPage() {
|
|
3353
3093
|
if (this.closed) throw new Error("Context is closed");
|
|
3354
3094
|
const { targetId } = await this._browser._createTarget(this.contextId);
|
|
@@ -3578,7 +3318,7 @@ var XBBrowserImpl = class {
|
|
|
3578
3318
|
this._exitHandler = null;
|
|
3579
3319
|
}
|
|
3580
3320
|
if (this.childProcess) {
|
|
3581
|
-
const { killChrome: killChrome2 } = await import("./launcher-
|
|
3321
|
+
const { killChrome: killChrome2 } = await import("./launcher-IK2FKW6T.js");
|
|
3582
3322
|
await killChrome2(this.childProcess, this.tmpDir);
|
|
3583
3323
|
}
|
|
3584
3324
|
await this.conn.close();
|
|
@@ -3604,6 +3344,38 @@ var XBBrowserImpl = class {
|
|
|
3604
3344
|
} catch {
|
|
3605
3345
|
}
|
|
3606
3346
|
const context = new XBContextImpl(this.conn, contextId, this, opts);
|
|
3347
|
+
if (!this.cdpEndpoint && contextId !== "default") {
|
|
3348
|
+
const downloadDir = join(tmpdir(), "xbrowser-downloads");
|
|
3349
|
+
try {
|
|
3350
|
+
mkdirSync(downloadDir, { recursive: true });
|
|
3351
|
+
} catch {
|
|
3352
|
+
}
|
|
3353
|
+
await this.conn.send("Browser.setDownloadBehavior", {
|
|
3354
|
+
behavior: "allowAndName",
|
|
3355
|
+
downloadPath: downloadDir,
|
|
3356
|
+
eventsEnabled: false,
|
|
3357
|
+
browserContextId: contextId
|
|
3358
|
+
}, void 0, 1e4).catch(() => {
|
|
3359
|
+
});
|
|
3360
|
+
await this.conn.send("Browser.grantPermissions", {
|
|
3361
|
+
permissions: [
|
|
3362
|
+
"notifications",
|
|
3363
|
+
"geolocation",
|
|
3364
|
+
"clipboardReadWrite",
|
|
3365
|
+
"clipboardSanitizedWrite",
|
|
3366
|
+
"videoCapture",
|
|
3367
|
+
"audioCapture",
|
|
3368
|
+
"midi"
|
|
3369
|
+
],
|
|
3370
|
+
browserContextId: contextId
|
|
3371
|
+
}, void 0, 1e4).catch(() => {
|
|
3372
|
+
this.conn.send("Browser.grantPermissions", {
|
|
3373
|
+
permissions: ["notifications", "geolocation"],
|
|
3374
|
+
browserContextId: contextId
|
|
3375
|
+
}, void 0, 1e4).catch(() => {
|
|
3376
|
+
});
|
|
3377
|
+
});
|
|
3378
|
+
}
|
|
3607
3379
|
context.on("page", (page) => {
|
|
3608
3380
|
this._emitter.emit("page", page);
|
|
3609
3381
|
});
|
|
@@ -3716,7 +3488,7 @@ var XBBrowserImpl = class {
|
|
|
3716
3488
|
if (pageTargets.length === 0 && httpFallbackUrl) {
|
|
3717
3489
|
console.log(`[discoverContexts] Target.getTargets returned ${targetInfos.length} targets (0 page type). Falling back to HTTP /json/list at ${httpFallbackUrl}`);
|
|
3718
3490
|
try {
|
|
3719
|
-
const { getCDPTargets: getCDPTargets3 } = await import("./launcher-
|
|
3491
|
+
const { getCDPTargets: getCDPTargets3 } = await import("./launcher-IK2FKW6T.js");
|
|
3720
3492
|
const httpPages = await getCDPTargets3(httpFallbackUrl);
|
|
3721
3493
|
console.log(`[discoverContexts] HTTP /json/list returned ${httpPages.length} pages`);
|
|
3722
3494
|
for (const p of httpPages) {
|
|
@@ -5032,14 +4804,23 @@ async function launch(options = {}) {
|
|
|
5032
4804
|
}
|
|
5033
4805
|
const conn = new CDPConnection(wsEndpoint);
|
|
5034
4806
|
await conn.ready();
|
|
4807
|
+
let stealthOpts;
|
|
4808
|
+
try {
|
|
4809
|
+
const bv = await conn.send("Browser.getVersion");
|
|
4810
|
+
const m = bv.product.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
|
|
4811
|
+
if (m && !options.stealthConfig?.uaChProfile) {
|
|
4812
|
+
const { deriveUaChProfileForBinary } = await import("./stealth-JOXGV34D.js");
|
|
4813
|
+
stealthOpts = { stealthConfig: { uaChProfile: deriveUaChProfileForBinary(m[0], m[1]) } };
|
|
4814
|
+
}
|
|
4815
|
+
} catch {
|
|
4816
|
+
}
|
|
4817
|
+
if (options.stealthConfig) {
|
|
4818
|
+
stealthOpts = {
|
|
4819
|
+
stealthConfig: { ...stealthOpts?.stealthConfig ?? {}, ...options.stealthConfig }
|
|
4820
|
+
};
|
|
4821
|
+
}
|
|
5035
4822
|
const httpEndpoint = options.cdpEndpoint && !options.cdpEndpoint.startsWith("ws") ? options.cdpEndpoint : void 0;
|
|
5036
|
-
const browser = new XBBrowserImpl(
|
|
5037
|
-
conn,
|
|
5038
|
-
childProcess,
|
|
5039
|
-
tmpDir,
|
|
5040
|
-
httpEndpoint,
|
|
5041
|
-
options.stealthConfig ? { stealthConfig: options.stealthConfig } : void 0
|
|
5042
|
-
);
|
|
4823
|
+
const browser = new XBBrowserImpl(conn, childProcess, tmpDir, httpEndpoint, stealthOpts);
|
|
5043
4824
|
return { browser, wsEndpoint };
|
|
5044
4825
|
}
|
|
5045
4826
|
|
|
@@ -5549,12 +5330,12 @@ function logSessionEvent(event, details) {
|
|
|
5549
5330
|
const pid = process.pid;
|
|
5550
5331
|
console.error(`[SESSION] ${ts} [PID:${pid}] ${event} | ${details}`);
|
|
5551
5332
|
}
|
|
5552
|
-
var SESSION_DIR =
|
|
5333
|
+
var SESSION_DIR = join2(homedir(), ".xbrowser", "sessions");
|
|
5553
5334
|
function sessionFile(name) {
|
|
5554
|
-
return
|
|
5335
|
+
return join2(SESSION_DIR, `${name}.json`);
|
|
5555
5336
|
}
|
|
5556
5337
|
function ensureSessionDir() {
|
|
5557
|
-
|
|
5338
|
+
mkdirSync2(SESSION_DIR, { recursive: true });
|
|
5558
5339
|
}
|
|
5559
5340
|
var sessions = new SessionStore();
|
|
5560
5341
|
var _sharedBrowser = null;
|
|
@@ -6258,7 +6039,7 @@ async function closeSessionByName(name) {
|
|
|
6258
6039
|
} catch {
|
|
6259
6040
|
}
|
|
6260
6041
|
try {
|
|
6261
|
-
const { SessionRecorder } = await import("./session-recorder-
|
|
6042
|
+
const { SessionRecorder } = await import("./session-recorder-563ISSJK.js");
|
|
6262
6043
|
SessionRecorder.cleanup(session.name);
|
|
6263
6044
|
} catch {
|
|
6264
6045
|
}
|
|
@@ -6319,7 +6100,7 @@ function resetForTesting() {
|
|
|
6319
6100
|
_sharedCdpProxy = null;
|
|
6320
6101
|
try {
|
|
6321
6102
|
for (const f of readdirSync(SESSION_DIR)) {
|
|
6322
|
-
unlinkSync(
|
|
6103
|
+
unlinkSync(join2(SESSION_DIR, f));
|
|
6323
6104
|
}
|
|
6324
6105
|
} catch {
|
|
6325
6106
|
}
|
|
@@ -6352,9 +6133,6 @@ async function ensureProcessCanExit() {
|
|
|
6352
6133
|
}
|
|
6353
6134
|
|
|
6354
6135
|
export {
|
|
6355
|
-
rand,
|
|
6356
|
-
sleep,
|
|
6357
|
-
wheelDelta,
|
|
6358
6136
|
createRuleEngine,
|
|
6359
6137
|
touchSession,
|
|
6360
6138
|
findTargetPage,
|