@xbrowser/cli 1.10.0 → 1.11.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-T3V3JWVH.js → browser-Q5APBNF6.js} +1 -1
- package/dist/{browser-HBL72GPZ.js → browser-V3JIWSTR.js} +2 -2
- package/dist/{browser-WQZ3D6AE.js → browser-XNU7JQYC.js} +3 -2
- package/dist/{cdp-driver-J3YQ4LJV.js → cdp-driver-GD6YBBGE.js} +1 -1
- package/dist/cdp-driver-VEK6BNN6.js +49 -0
- package/dist/{cdp-driver-2T4P4ZE2.js → cdp-driver-XFTTZI5O.js} +436 -56
- package/dist/chunk-3FWLW7FS.js +106 -0
- package/dist/chunk-7OFM755Z.js +3509 -0
- package/dist/{chunk-5UGR6MUK.js → chunk-HBHOKZPN.js} +51 -16
- package/dist/{chunk-BAHSRZIX.js → chunk-HVPUVVSA.js} +13 -10
- package/dist/{chunk-NITFVWWS.js → chunk-JEDP4PJW.js} +448 -65
- package/dist/{chunk-INTQPBYF.js → chunk-JKVUFP3G.js} +6 -2
- package/dist/{chunk-4452SPFI.js → chunk-NODRQGOK.js} +51 -16
- package/dist/{chunk-YMUSHPU4.js → chunk-WHPBNUKB.js} +35 -1
- package/dist/{chunk-WSCP7QCJ.js → chunk-XKQVUFUS.js} +13 -10
- package/dist/{chunk-RRBXV7KE.js → chunk-XQ4HRPDJ.js} +384 -102
- package/dist/cli.js +72 -26
- package/dist/{daemon-client-GKEPT4NY.js → daemon-client-MMDRYCF5.js} +35 -1
- package/dist/{daemon-client-O6BYVRXV.js → daemon-client-Y2YSZCGK.js} +1 -1
- package/dist/daemon-main.js +89 -31
- package/dist/index.d.ts +8 -0
- package/dist/index.js +73 -27
- 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-WIUYVN5J.js} +90 -2
- package/package.json +1 -1
|
@@ -24,6 +24,161 @@ import { EventEmitter as EventEmitter2 } from "events";
|
|
|
24
24
|
// src/cdp-driver/page.ts
|
|
25
25
|
import { EventEmitter } from "events";
|
|
26
26
|
|
|
27
|
+
// src/cdp-driver/stealth.ts
|
|
28
|
+
var DEFAULT_STEALTH_CONFIG = {
|
|
29
|
+
bezierCurvature: [0.35, 0.6],
|
|
30
|
+
noiseAmplitude: 5.5,
|
|
31
|
+
overshootRange: [6, 14],
|
|
32
|
+
aimPause: [150, 400],
|
|
33
|
+
pressDuration: [60, 140],
|
|
34
|
+
releaseDrift: [0.8, 2.5],
|
|
35
|
+
landingOffsetSmall: [0.3, 2.5],
|
|
36
|
+
landingOffsetLarge: [1.5, 7],
|
|
37
|
+
smallElementThreshold: 30,
|
|
38
|
+
typingRhythm: {
|
|
39
|
+
fastProb: 0.22,
|
|
40
|
+
fastRange: [25, 60],
|
|
41
|
+
normalRange: [50, 350],
|
|
42
|
+
pauseProb: 0.18,
|
|
43
|
+
pauseRange: [400, 1200]
|
|
44
|
+
},
|
|
45
|
+
keyPressDuration: [50, 110],
|
|
46
|
+
typoProbability: 0.06,
|
|
47
|
+
wheelPeak: 180,
|
|
48
|
+
wheelDecayRate: 0.4
|
|
49
|
+
};
|
|
50
|
+
function rand(min, max) {
|
|
51
|
+
return min + Math.random() * (max - min);
|
|
52
|
+
}
|
|
53
|
+
function cosineEase(t) {
|
|
54
|
+
return 0.5 - 0.5 * Math.cos(Math.PI * t);
|
|
55
|
+
}
|
|
56
|
+
function bezierTrajectory(x0, y0, x1, y1, config = DEFAULT_STEALTH_CONFIG) {
|
|
57
|
+
const dist = Math.hypot(x1 - x0, y1 - y0);
|
|
58
|
+
const n = Math.max(10, Math.min(28, Math.round(dist / 15)));
|
|
59
|
+
const shortMove = dist < 120;
|
|
60
|
+
const curvature = shortMove ? rand(2, 6) : Math.max(dist * rand(...config.bezierCurvature), rand(18, 35));
|
|
61
|
+
const dir = Math.random() < 0.5 ? 1 : -1;
|
|
62
|
+
const d = dist || 1;
|
|
63
|
+
const dx = x1 - x0, dy = y1 - y0;
|
|
64
|
+
const c1x = x0 + dx * 0.3 - dy / d * curvature * 0.5 * dir;
|
|
65
|
+
const c1y = y0 + dy * 0.3 + dx / d * curvature * 0.5 * dir;
|
|
66
|
+
const c2x = x0 + dx * 0.7 - dy / d * curvature * 0.8 * dir;
|
|
67
|
+
const c2y = y0 + dy * 0.7 + dx / d * curvature * 0.8 * dir;
|
|
68
|
+
const points = [];
|
|
69
|
+
for (let i = 1; i <= n; i++) {
|
|
70
|
+
const t = cosineEase(i / n);
|
|
71
|
+
const mt = 1 - t;
|
|
72
|
+
let px = mt ** 3 * x0 + 3 * mt ** 2 * t * c1x + 3 * mt * t ** 2 * c2x + t ** 3 * x1;
|
|
73
|
+
let py = mt ** 3 * y0 + 3 * mt ** 2 * t * c1y + 3 * mt * t ** 2 * c2y + t ** 3 * y1;
|
|
74
|
+
const amp = shortMove ? Math.min(2, config.noiseAmplitude) : config.noiseAmplitude;
|
|
75
|
+
px += rand(-amp, amp);
|
|
76
|
+
py += rand(-amp, amp);
|
|
77
|
+
points.push({ x: px, y: py, delay: rand(9, 16) });
|
|
78
|
+
}
|
|
79
|
+
if (!shortMove) {
|
|
80
|
+
const over = rand(...config.overshootRange);
|
|
81
|
+
const ox = x1 + dx / d * over + rand(-2, 2);
|
|
82
|
+
const oy = y1 + dy / d * over + rand(-2, 2);
|
|
83
|
+
points.push({ x: ox, y: oy, delay: rand(14, 30) });
|
|
84
|
+
points.push({
|
|
85
|
+
x: x1 + dx / d * over * 0.4,
|
|
86
|
+
y: y1 + dy / d * over * 0.4,
|
|
87
|
+
delay: rand(14, 30)
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
points.push({ x: x1 + rand(-1, 1), y: y1 + rand(-1, 1), delay: rand(14, 30) });
|
|
91
|
+
return points;
|
|
92
|
+
}
|
|
93
|
+
function landingOffset(width, height, config = DEFAULT_STEALTH_CONFIG) {
|
|
94
|
+
const isSmall = Math.min(width, height) < config.smallElementThreshold;
|
|
95
|
+
const range = isSmall ? config.landingOffsetSmall : config.landingOffsetLarge;
|
|
96
|
+
const dx = rand(...range) * (Math.random() < 0.5 ? -1 : 1);
|
|
97
|
+
const dy = rand(...range) * (Math.random() < 0.5 ? -1 : 1);
|
|
98
|
+
return { dx, dy };
|
|
99
|
+
}
|
|
100
|
+
var KEY_MAP = {};
|
|
101
|
+
for (let i = 97; i <= 122; i++) {
|
|
102
|
+
const ch = String.fromCharCode(i);
|
|
103
|
+
KEY_MAP[ch] = { key: ch, code: "Key" + ch.toUpperCase(), vk: i - 32 };
|
|
104
|
+
}
|
|
105
|
+
for (let i = 65; i <= 90; i++) {
|
|
106
|
+
const ch = String.fromCharCode(i);
|
|
107
|
+
KEY_MAP[ch] = { key: ch, code: "Key" + ch, vk: i, shift: true };
|
|
108
|
+
}
|
|
109
|
+
for (let i = 48; i <= 57; i++) {
|
|
110
|
+
const ch = String.fromCharCode(i);
|
|
111
|
+
KEY_MAP[ch] = { key: ch, code: "Digit" + ch, vk: i };
|
|
112
|
+
}
|
|
113
|
+
Object.assign(KEY_MAP, {
|
|
114
|
+
" ": { key: " ", code: "Space", vk: 32 },
|
|
115
|
+
".": { key: ".", code: "Period", vk: 190 },
|
|
116
|
+
"-": { key: "-", code: "Minus", vk: 189 },
|
|
117
|
+
"@": { key: "@", code: "Digit2", vk: 50, shift: true },
|
|
118
|
+
"_": { key: "_", code: "Minus", vk: 189, shift: true }
|
|
119
|
+
});
|
|
120
|
+
function buildStealthInitScript() {
|
|
121
|
+
return [
|
|
122
|
+
"(function(){",
|
|
123
|
+
// 1. AEL event proxy
|
|
124
|
+
" var o=EventTarget.prototype.addEventListener;",
|
|
125
|
+
" var fc=new InputDeviceCapabilities({firesTouchEvents:false});",
|
|
126
|
+
" var _ael=function(t,f){",
|
|
127
|
+
" var op=arguments[2];",
|
|
128
|
+
' if(typeof f!=="function")return o.call(this,t,f,op);',
|
|
129
|
+
" var w=function(e){",
|
|
130
|
+
" if(!e||e.constructor===FocusEvent||e.constructor===KeyboardEvent)return f.call(this,e);",
|
|
131
|
+
" return f.call(this,new Proxy(e,{get:function(k,p){",
|
|
132
|
+
' if(p==="sourceCapabilities")return fc;',
|
|
133
|
+
' if(p==="isTrusted")return true;',
|
|
134
|
+
' if((p==="clientX"||p==="clientY")&&k.type==="click"&&Number.isFinite(k[p])){',
|
|
135
|
+
" var _f=((k.timeStamp||Date.now())%89)/89*0.7+0.15;",
|
|
136
|
+
" return k[p]+_f;",
|
|
137
|
+
" }",
|
|
138
|
+
' var v=Reflect.get(k,p);return typeof v==="function"?v.bind(k):v;',
|
|
139
|
+
" }}));",
|
|
140
|
+
" };",
|
|
141
|
+
" return o.call(this,t,w,op);",
|
|
142
|
+
" };",
|
|
143
|
+
" EventTarget.prototype.addEventListener=_ael;",
|
|
144
|
+
// 2. Screen override (prototype-level, not instance-level)
|
|
145
|
+
" var _gw=function(){return 1728};",
|
|
146
|
+
" var _gh=function(){return 1117};",
|
|
147
|
+
" var _gah=function(){return 1092};",
|
|
148
|
+
' Object.defineProperty(Screen.prototype,"width",{get:_gw,configurable:true});',
|
|
149
|
+
' Object.defineProperty(Screen.prototype,"height",{get:_gh,configurable:true});',
|
|
150
|
+
' Object.defineProperty(Screen.prototype,"availWidth",{get:_gw,configurable:true});',
|
|
151
|
+
' Object.defineProperty(Screen.prototype,"availHeight",{get:_gah,configurable:true});',
|
|
152
|
+
" document.hasFocus=function(){return true};",
|
|
153
|
+
// 3. toString disguise (name-list based)
|
|
154
|
+
" var _ts=Function.prototype.toString;",
|
|
155
|
+
" var _hf=document.hasFocus;",
|
|
156
|
+
" Function.prototype.toString=function(){",
|
|
157
|
+
' if(this===_ael)return"function addEventListener(type, callback) { [native code] }";',
|
|
158
|
+
' if(this===_hf)return"function hasFocus() { [native code] }";',
|
|
159
|
+
' if(this===_gw)return"function get width() { [native code] }";',
|
|
160
|
+
' if(this===_gh)return"function get height() { [native code] }";',
|
|
161
|
+
' if(this===_gah)return"function get availHeight() { [native code] }";',
|
|
162
|
+
" return _ts.call(this);",
|
|
163
|
+
" };",
|
|
164
|
+
// 4. onclick prototype hijack (dual-stream consistency)
|
|
165
|
+
" var _ba=function(k,p){",
|
|
166
|
+
' if(p==="isTrusted")return true;',
|
|
167
|
+
' if((p==="clientX"||p==="clientY")&&k.type==="click"&&Number.isFinite(k[p])){',
|
|
168
|
+
" var _f=((k.timeStamp||Date.now())%89)/89*0.7+0.15;return k[p]+_f;",
|
|
169
|
+
" }",
|
|
170
|
+
' var v=Reflect.get(k,p);return typeof v==="function"?v.bind(k):v;',
|
|
171
|
+
" };",
|
|
172
|
+
' Object.defineProperty(Document.prototype,"onclick",{',
|
|
173
|
+
" configurable:true,",
|
|
174
|
+
" get:function(){var raw=this.__ocRaw||null;if(!raw)return null;var self=this;",
|
|
175
|
+
" return function(e){return raw.call(self,new Proxy(e,{get:function(k,p){return _ba(k,p)}}))}},",
|
|
176
|
+
" set:function(fn){this.__ocRaw=fn}",
|
|
177
|
+
" });",
|
|
178
|
+
"})()"
|
|
179
|
+
].join("\n");
|
|
180
|
+
}
|
|
181
|
+
|
|
27
182
|
// src/cdp-driver/mouse.ts
|
|
28
183
|
var XBMouseImpl = class {
|
|
29
184
|
conn;
|
|
@@ -45,18 +200,35 @@ var XBMouseImpl = class {
|
|
|
45
200
|
}
|
|
46
201
|
async click(x, y, opts = {}) {
|
|
47
202
|
const button = opts.button ?? "left";
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
203
|
+
const stealth = opts.stealth ?? true;
|
|
204
|
+
let tx = x, ty = y;
|
|
205
|
+
if (stealth && opts.elementWidth !== void 0 && opts.elementHeight !== void 0) {
|
|
206
|
+
const off = landingOffset(opts.elementWidth, opts.elementHeight);
|
|
207
|
+
tx += off.dx;
|
|
208
|
+
ty += off.dy;
|
|
209
|
+
}
|
|
210
|
+
if (stealth) {
|
|
211
|
+
const traj = bezierTrajectory(this._x, this._y, tx, ty);
|
|
212
|
+
for (const p of traj) {
|
|
213
|
+
await this.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: p.x, y: p.y, button: this._button });
|
|
214
|
+
this._x = p.x;
|
|
215
|
+
this._y = p.y;
|
|
216
|
+
await sleep(p.delay);
|
|
217
|
+
}
|
|
218
|
+
await sleep(rand(...DEFAULT_STEALTH_CONFIG.aimPause));
|
|
219
|
+
} else {
|
|
220
|
+
await this.move(tx, ty);
|
|
54
221
|
}
|
|
55
|
-
await this.
|
|
56
|
-
|
|
57
|
-
|
|
222
|
+
await this.down({ button });
|
|
223
|
+
await sleep(stealth ? rand(...DEFAULT_STEALTH_CONFIG.pressDuration) : opts.delay ?? 0);
|
|
224
|
+
const rx = stealth ? this._x + rand(...DEFAULT_STEALTH_CONFIG.releaseDrift) * (Math.random() < 0.5 ? -1 : 1) : this._x;
|
|
225
|
+
const ry = stealth ? this._y + rand(...DEFAULT_STEALTH_CONFIG.releaseDrift) * (Math.random() < 0.5 ? -1 : 1) : this._y;
|
|
226
|
+
this._x = rx;
|
|
227
|
+
this._y = ry;
|
|
228
|
+
await this.send("Input.dispatchMouseEvent", { type: "mouseReleased", x: rx, y: ry, button, clickCount: opts.clickCount ?? 1 });
|
|
229
|
+
for (let i = 1; i < (opts.clickCount ?? 1); i++) {
|
|
230
|
+
if (opts.delay) await sleep(opts.delay);
|
|
58
231
|
await this.down({ button });
|
|
59
|
-
if (delay > 0) await sleep(delay);
|
|
60
232
|
await this.up({ button });
|
|
61
233
|
}
|
|
62
234
|
}
|
|
@@ -235,7 +407,7 @@ var XBKeyboardImpl = class {
|
|
|
235
407
|
}
|
|
236
408
|
};
|
|
237
409
|
function resolveKeyMapping(key) {
|
|
238
|
-
if (
|
|
410
|
+
if (KEY_MAP2[key]) return KEY_MAP2[key];
|
|
239
411
|
if (key.length === 1) {
|
|
240
412
|
const lower = key.toLowerCase();
|
|
241
413
|
if (lower >= "a" && lower <= "z") {
|
|
@@ -252,7 +424,7 @@ function resolveKeyMapping(key) {
|
|
|
252
424
|
}
|
|
253
425
|
return { key, code: key };
|
|
254
426
|
}
|
|
255
|
-
var
|
|
427
|
+
var KEY_MAP2 = {
|
|
256
428
|
Enter: { key: "Enter", code: "Enter", text: "\r", keyCode: 13 },
|
|
257
429
|
Tab: { key: "Tab", code: "Tab", text: " ", keyCode: 9 },
|
|
258
430
|
Escape: { key: "Escape", code: "Escape", keyCode: 27 },
|
|
@@ -290,6 +462,37 @@ function sleep2(ms) {
|
|
|
290
462
|
|
|
291
463
|
// src/cdp-driver/selector-utils.ts
|
|
292
464
|
function queryJS(selector) {
|
|
465
|
+
return `(${deepQueryIIFE})( ${JSON.stringify(queryMainJS(selector))} )`;
|
|
466
|
+
}
|
|
467
|
+
var deepQueryIIFE = `(function(mainExpr) {
|
|
468
|
+
const run = (root) => {
|
|
469
|
+
try { return new Function('document', 'return (' + mainExpr + ')')(root); }
|
|
470
|
+
catch (e) { return null; }
|
|
471
|
+
};
|
|
472
|
+
const scanRoot = (root) => {
|
|
473
|
+
const direct = run(root);
|
|
474
|
+
if (direct) return direct;
|
|
475
|
+
let all;
|
|
476
|
+
try { all = root.querySelectorAll('*'); } catch (e) { return null; }
|
|
477
|
+
for (const el of all) {
|
|
478
|
+
if (el.shadowRoot) {
|
|
479
|
+
const r = scanRoot(el.shadowRoot);
|
|
480
|
+
if (r) return r;
|
|
481
|
+
}
|
|
482
|
+
if (el.tagName === 'IFRAME') {
|
|
483
|
+
let inner = null;
|
|
484
|
+
try { inner = el.contentDocument; } catch (e) { /* cross-origin */ }
|
|
485
|
+
if (inner) {
|
|
486
|
+
const r = scanRoot(inner);
|
|
487
|
+
if (r) return r;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return null;
|
|
492
|
+
};
|
|
493
|
+
return scanRoot(document);
|
|
494
|
+
})`;
|
|
495
|
+
function queryMainJS(selector) {
|
|
293
496
|
if (selector.startsWith("xpath=")) {
|
|
294
497
|
const xpath = JSON.stringify(selector.slice(6));
|
|
295
498
|
return `document.evaluate(${xpath}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue`;
|
|
@@ -301,13 +504,35 @@ function queryJS(selector) {
|
|
|
301
504
|
return `(() => {
|
|
302
505
|
const target = ${JSON.stringify(text)};
|
|
303
506
|
const exact = ${exact};
|
|
507
|
+
// Match on OWN text nodes (not strict leaf elements): search-result
|
|
508
|
+
// titles mix text with inline highlight <em> marks \u2014 a strict leaf filter
|
|
509
|
+
// finds nothing there (real-world juejin). Own-text keeps the match
|
|
510
|
+
// precise (descendant-only text doesn't count) while tolerating markup.
|
|
511
|
+
const ownText = (e) => Array.prototype.filter.call(e.childNodes, (n) => n.nodeType === 3)
|
|
512
|
+
.map((n) => n.textContent).join('').trim();
|
|
304
513
|
const els = [...document.querySelectorAll('*')].filter(e => {
|
|
305
|
-
if (e.
|
|
306
|
-
|
|
307
|
-
const t = (e.textContent || '').trim();
|
|
514
|
+
if (e.offsetParent === null && e.tagName !== 'BODY') return false;
|
|
515
|
+
const t = ownText(e);
|
|
308
516
|
if (!t) return false;
|
|
309
517
|
return exact ? t === target : t.toLowerCase().includes(target.toLowerCase());
|
|
310
518
|
});
|
|
519
|
+
// Rank instead of raw DOM order: exact text beats substring, interactive
|
|
520
|
+
// elements (button/a/[onclick]/inputs) beat prose. Prevents matching a
|
|
521
|
+
// description paragraph that merely MENTIONS the target label
|
|
522
|
+
// (rec-duel d06: header text "\u76EE\u6807\u9879\u300C\u7B2C 87 \u53F7\u300D" hijacked text=\u7B2C 87 \u53F7).
|
|
523
|
+
const isInteractive = (e) => {
|
|
524
|
+
const tag = e.tagName;
|
|
525
|
+
return tag === 'BUTTON' || tag === 'A' || tag === 'INPUT' || tag === 'SELECT'
|
|
526
|
+
|| e.hasAttribute('onclick') || e.getAttribute('role') === 'button';
|
|
527
|
+
};
|
|
528
|
+
els.sort((a, b) => {
|
|
529
|
+
const ta = ownText(a), tb = ownText(b);
|
|
530
|
+
const ea = ta === target ? 0 : 1, eb = tb === target ? 0 : 1;
|
|
531
|
+
if (ea !== eb) return ea - eb;
|
|
532
|
+
const ia = isInteractive(a) ? 0 : 1, ib = isInteractive(b) ? 0 : 1;
|
|
533
|
+
if (ia !== ib) return ia - ib;
|
|
534
|
+
return 0; // stable \u2014 preserve DOM order
|
|
535
|
+
});
|
|
311
536
|
return els[0] || null;
|
|
312
537
|
})()`;
|
|
313
538
|
}
|
|
@@ -341,43 +566,53 @@ function queryAllJS(selector) {
|
|
|
341
566
|
async function waitForActionable(page, selector, opts = {}) {
|
|
342
567
|
const timeout = opts.timeout ?? 3e4;
|
|
343
568
|
if (opts.force) {
|
|
344
|
-
|
|
345
|
-
|
|
569
|
+
const deadline2 = Date.now() + timeout;
|
|
570
|
+
let lastError;
|
|
571
|
+
while (Date.now() < deadline2) {
|
|
572
|
+
const rect = await page.evaluate(`
|
|
346
573
|
(function() {
|
|
347
574
|
const el = ${queryJS(selector)};
|
|
348
575
|
if (!el) return null;
|
|
349
576
|
const r = el.getBoundingClientRect();
|
|
350
|
-
|
|
577
|
+
let x = r.x, y = r.y;
|
|
578
|
+
let doc = el.ownerDocument;
|
|
579
|
+
while (doc !== document) {
|
|
580
|
+
let host = null;
|
|
581
|
+
const scan = (d) => {
|
|
582
|
+
let frames;
|
|
583
|
+
try { frames = d.querySelectorAll('iframe'); } catch (e) { return null; }
|
|
584
|
+
for (const f of frames) {
|
|
585
|
+
let inner = null;
|
|
586
|
+
try { inner = f.contentDocument; } catch (e) { continue; }
|
|
587
|
+
if (!inner) continue;
|
|
588
|
+
if (inner === doc) return f;
|
|
589
|
+
const rr = scan(inner);
|
|
590
|
+
if (rr) return rr;
|
|
591
|
+
}
|
|
592
|
+
return null;
|
|
593
|
+
};
|
|
594
|
+
host = scan(document);
|
|
595
|
+
if (!host) break;
|
|
596
|
+
const hr = host.getBoundingClientRect();
|
|
597
|
+
x += hr.x; y += hr.y;
|
|
598
|
+
doc = host.ownerDocument;
|
|
599
|
+
}
|
|
600
|
+
return { x, y, width: r.width, height: r.height };
|
|
351
601
|
})()
|
|
352
|
-
`);
|
|
353
|
-
if (
|
|
354
|
-
|
|
602
|
+
`).catch(() => null);
|
|
603
|
+
if (rect && rect.width > 0 && rect.height > 0) return { nodeId: 0, rect };
|
|
604
|
+
lastError = `Element not visible (zero size): ${selector}`;
|
|
605
|
+
lastError = `Element not found: ${selector}`;
|
|
606
|
+
await page.waitForTimeout(200);
|
|
355
607
|
}
|
|
356
|
-
|
|
357
|
-
let lastError;
|
|
358
|
-
let nodeId = 0;
|
|
359
|
-
let rect = null;
|
|
360
|
-
while (Date.now() < deadline2) {
|
|
361
|
-
nodeId = await page.querySelector(selector);
|
|
362
|
-
if (!nodeId) {
|
|
363
|
-
lastError = `Element not found: ${selector}`;
|
|
364
|
-
await page.waitForTimeout(200);
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
rect = await page.getBoxModel(nodeId);
|
|
368
|
-
if (rect) break;
|
|
369
|
-
lastError = `Element has no box: ${selector}`;
|
|
370
|
-
await page.waitForTimeout(500);
|
|
371
|
-
}
|
|
372
|
-
if (!rect) throw new Error(lastError || `Element has no box: ${selector}`);
|
|
373
|
-
return { nodeId, rect };
|
|
608
|
+
throw new Error(lastError || `Element not found: ${selector}`);
|
|
374
609
|
}
|
|
375
610
|
const deadline = Date.now() + timeout;
|
|
376
611
|
while (Date.now() < deadline) {
|
|
377
612
|
const result = await checkActionable(page, selector);
|
|
378
613
|
if (result.ok && result.rect) {
|
|
379
|
-
const nodeId = await page.querySelector(selector);
|
|
380
|
-
|
|
614
|
+
const nodeId = await page.querySelector(selector).catch(() => 0) ?? 0;
|
|
615
|
+
return { nodeId, rect: result.rect };
|
|
381
616
|
}
|
|
382
617
|
await page.waitForTimeout(50);
|
|
383
618
|
}
|
|
@@ -410,12 +645,25 @@ async function checkActionable(page, selector) {
|
|
|
410
645
|
return { ok: false, reason: 'parent_disabled' };
|
|
411
646
|
}
|
|
412
647
|
|
|
413
|
-
// Check not covered by another element at center
|
|
648
|
+
// Check not covered by another element at center.
|
|
649
|
+
// elementFromPoint must run in the element's OWN document: for iframe-
|
|
650
|
+
// internal elements the main-document hit-test returns the <iframe>
|
|
651
|
+
// host itself, which falsely reports "covered" (rec-duel d01).
|
|
652
|
+
// For shadow-internal elements the hit-test retargets to the shadow
|
|
653
|
+
// HOST \u2014 walk the host chain before declaring coverage (rec-duel d04).
|
|
414
654
|
const cx = rect.x + rect.width / 2;
|
|
415
655
|
const cy = rect.y + rect.height / 2;
|
|
416
|
-
const topEl =
|
|
656
|
+
const topEl = el.ownerDocument.elementFromPoint(cx, cy);
|
|
417
657
|
if (topEl && topEl !== el && !el.contains(topEl) && !topEl.contains(el)) {
|
|
418
|
-
|
|
658
|
+
let hostChain = [];
|
|
659
|
+
let rootNode = el.getRootNode();
|
|
660
|
+
while (rootNode && rootNode.host) {
|
|
661
|
+
hostChain.push(rootNode.host);
|
|
662
|
+
rootNode = rootNode.host.getRootNode();
|
|
663
|
+
}
|
|
664
|
+
if (!hostChain.includes(topEl)) {
|
|
665
|
+
return { ok: false, reason: 'covered', rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } };
|
|
666
|
+
}
|
|
419
667
|
}
|
|
420
668
|
|
|
421
669
|
return {
|
|
@@ -460,21 +708,48 @@ var XBLocatorImpl = class _XBLocatorImpl {
|
|
|
460
708
|
const el = ${this._q(this.selector)};
|
|
461
709
|
if (!el) return null;
|
|
462
710
|
const rect = el.getBoundingClientRect();
|
|
463
|
-
|
|
711
|
+
let x = rect.x, y = rect.y;
|
|
712
|
+
let doc = el.ownerDocument;
|
|
713
|
+
while (doc !== document) {
|
|
714
|
+
let host = null;
|
|
715
|
+
const scan = (d) => {
|
|
716
|
+
let frames;
|
|
717
|
+
try { frames = d.querySelectorAll('iframe'); } catch (e) { return null; }
|
|
718
|
+
for (const f of frames) {
|
|
719
|
+
let inner = null;
|
|
720
|
+
try { inner = f.contentDocument; } catch (e) { continue; }
|
|
721
|
+
if (!inner) continue;
|
|
722
|
+
if (inner === doc) return f;
|
|
723
|
+
const r = scan(inner);
|
|
724
|
+
if (r) return r;
|
|
725
|
+
}
|
|
726
|
+
return null;
|
|
727
|
+
};
|
|
728
|
+
host = scan(document);
|
|
729
|
+
if (!host) break;
|
|
730
|
+
const hr = host.getBoundingClientRect();
|
|
731
|
+
x += hr.x; y += hr.y;
|
|
732
|
+
doc = host.ownerDocument;
|
|
733
|
+
}
|
|
734
|
+
return { x, y, width: rect.width, height: rect.height };
|
|
464
735
|
})()
|
|
465
736
|
`);
|
|
466
737
|
const finalRect = updatedRect ?? rect;
|
|
467
738
|
const cx = finalRect.x + finalRect.width / 2;
|
|
468
739
|
const cy = finalRect.y + finalRect.height / 2;
|
|
469
740
|
await this.page.mouse.click(cx, cy, {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
741
|
+
stealth: true,
|
|
742
|
+
elementWidth: finalRect.width,
|
|
743
|
+
elementHeight: finalRect.height,
|
|
744
|
+
...{ button: opts.button ?? "left", clickCount: opts.clickCount ?? 1, delay: opts.delay }
|
|
473
745
|
});
|
|
474
746
|
}
|
|
475
747
|
async fill(value, opts = {}) {
|
|
476
748
|
await waitForActionable(this.page, this.selector, opts);
|
|
477
749
|
await scrollIntoView(this.page, this.selector);
|
|
750
|
+
await this.click({ ...opts });
|
|
751
|
+
await this.page.keyboard.type(value, { stealth: true });
|
|
752
|
+
return;
|
|
478
753
|
await this.page.evaluate(`
|
|
479
754
|
(function() {
|
|
480
755
|
const el = ${this._q(this.selector)};
|
|
@@ -1184,6 +1459,8 @@ var XBPageImpl = class _XBPageImpl {
|
|
|
1184
1459
|
await this.conn.send("Page.enable", void 0, this.sessionId);
|
|
1185
1460
|
await this.conn.send("Runtime.enable", void 0, this.sessionId);
|
|
1186
1461
|
await this.conn.send("Network.enable", void 0, this.sessionId);
|
|
1462
|
+
await this.conn.send("DOM.enable", void 0, this.sessionId).catch(() => {
|
|
1463
|
+
});
|
|
1187
1464
|
this.setupPageEvents();
|
|
1188
1465
|
this.setupNetworkEvents();
|
|
1189
1466
|
this.setupConsoleEvents();
|
|
@@ -1196,6 +1473,9 @@ var XBPageImpl = class _XBPageImpl {
|
|
|
1196
1473
|
);
|
|
1197
1474
|
this._url = info.url;
|
|
1198
1475
|
this._title = info.title;
|
|
1476
|
+
if (info.url && info.url !== "about:blank" && info.url !== "") {
|
|
1477
|
+
this._loadState = { loadFired: true, domContentFired: true, networkIdle: true };
|
|
1478
|
+
}
|
|
1199
1479
|
} catch {
|
|
1200
1480
|
}
|
|
1201
1481
|
}
|
|
@@ -1208,6 +1488,16 @@ var XBPageImpl = class _XBPageImpl {
|
|
|
1208
1488
|
const waitUntil = opts.waitUntil ?? "load";
|
|
1209
1489
|
const timeout = opts.timeout ?? 3e4;
|
|
1210
1490
|
this._loadState = { loadFired: false, domContentFired: false, networkIdle: false };
|
|
1491
|
+
if (process.env.XBROWSER_STEALTH !== "off") {
|
|
1492
|
+
try {
|
|
1493
|
+
await this.conn.send(
|
|
1494
|
+
"Page.addScriptToEvaluateOnNewDocument",
|
|
1495
|
+
{ source: buildStealthInitScript() },
|
|
1496
|
+
this.sessionId
|
|
1497
|
+
);
|
|
1498
|
+
} catch {
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1211
1501
|
const result = await this.conn.send(
|
|
1212
1502
|
"Page.navigate",
|
|
1213
1503
|
{ url, referrer: opts.referer },
|
|
@@ -1415,6 +1705,91 @@ Last error: ${lastError.message}` : "";
|
|
|
1415
1705
|
}
|
|
1416
1706
|
return result.result?.value;
|
|
1417
1707
|
}
|
|
1708
|
+
/**
|
|
1709
|
+
* 在指定 iframe 上下文中执行表达式(攻防 D16 能力建设,2026-08-19)。
|
|
1710
|
+
*
|
|
1711
|
+
* 双路径:
|
|
1712
|
+
* 1. 同进程 iframe —— Runtime.enable 收集 executionContextCreated,
|
|
1713
|
+
* 找到目标 frameId 的 contextId,用 contextId 定向执行;
|
|
1714
|
+
* 2. 跨域 OOPIF(独立 target)—— Target.setAutoAttach(flatten) 监听
|
|
1715
|
+
* attachedToTarget 中 type==='iframe' 的会话,用其 sessionId 执行。
|
|
1716
|
+
*
|
|
1717
|
+
* 这绕过了页面同源策略(那是页面 JS 的约束,CDP 是调试通道)——
|
|
1718
|
+
* 支付窗/验证码/第三方嵌入内容的读写都靠它。
|
|
1719
|
+
*/
|
|
1720
|
+
async evaluateInFrame(urlIncludes, expression) {
|
|
1721
|
+
if (this._closed) throw new Error("Page is closed");
|
|
1722
|
+
const evalIn = async (sessionId, contextId) => {
|
|
1723
|
+
const params = { expression, returnByValue: true, awaitPromise: true };
|
|
1724
|
+
if (contextId !== void 0) params.contextId = contextId;
|
|
1725
|
+
const result = await this.conn.send("Runtime.evaluate", params, sessionId);
|
|
1726
|
+
if (result.exceptionDetails) {
|
|
1727
|
+
const detail = result.exceptionDetails.exception?.description ?? result.exceptionDetails.exception?.value ?? result.exceptionDetails.text;
|
|
1728
|
+
throw new Error(`[frame ${urlIncludes}] ${detail}`);
|
|
1729
|
+
}
|
|
1730
|
+
return result.result?.value;
|
|
1731
|
+
};
|
|
1732
|
+
try {
|
|
1733
|
+
const tg = await this.conn.send("Target.getTargets", void 0);
|
|
1734
|
+
const hit2 = (tg.targetInfos || []).find((t) => t.type === "iframe" && (t.url || "").includes(urlIncludes));
|
|
1735
|
+
if (hit2) {
|
|
1736
|
+
const att = await this.conn.send("Target.attachToTarget", { targetId: hit2.targetId, flatten: true });
|
|
1737
|
+
return evalIn(att.sessionId);
|
|
1738
|
+
}
|
|
1739
|
+
} catch {
|
|
1740
|
+
}
|
|
1741
|
+
const tree = await this.conn.send("Page.getFrameTree", void 0, this.sessionId);
|
|
1742
|
+
const all = [];
|
|
1743
|
+
const walk = (node) => {
|
|
1744
|
+
all.push({ id: node.frame.id, url: node.frame.url });
|
|
1745
|
+
for (const child of node.childFrames || []) walk(child);
|
|
1746
|
+
};
|
|
1747
|
+
walk(tree.frameTree);
|
|
1748
|
+
const mainId = tree.frameTree?.frame?.id;
|
|
1749
|
+
const target = all.find((f) => f.id !== mainId && f.url.includes(urlIncludes));
|
|
1750
|
+
if (target) {
|
|
1751
|
+
const contexts = [];
|
|
1752
|
+
const onCtx = (raw) => {
|
|
1753
|
+
const c = raw?.context;
|
|
1754
|
+
if (c?.id && c?.auxData?.frameId) contexts.push({ id: c.id, frameId: c.auxData.frameId });
|
|
1755
|
+
};
|
|
1756
|
+
this.conn.on("Runtime.executionContextCreated", onCtx);
|
|
1757
|
+
try {
|
|
1758
|
+
await this.conn.send("Runtime.enable", void 0, this.sessionId).catch(() => {
|
|
1759
|
+
});
|
|
1760
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
1761
|
+
} finally {
|
|
1762
|
+
this.conn.off("Runtime.executionContextCreated", onCtx);
|
|
1763
|
+
}
|
|
1764
|
+
const ctx = contexts.find((c) => c.frameId === target.id);
|
|
1765
|
+
if (ctx) return evalIn(this.sessionId, ctx.id);
|
|
1766
|
+
}
|
|
1767
|
+
const attached = [];
|
|
1768
|
+
const onAttach = (raw) => {
|
|
1769
|
+
const ev = raw;
|
|
1770
|
+
if (ev?.sessionId && ev.targetInfo?.type === "iframe") {
|
|
1771
|
+
attached.push({ sessionId: ev.sessionId, url: ev.targetInfo.url || "" });
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
this.conn.on("Target.attachedToTarget", onAttach);
|
|
1775
|
+
try {
|
|
1776
|
+
await this.conn.send("Target.setAutoAttach", {
|
|
1777
|
+
autoAttach: true,
|
|
1778
|
+
waitForDebuggerOnStart: false,
|
|
1779
|
+
flatten: true
|
|
1780
|
+
}, this.sessionId);
|
|
1781
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
1782
|
+
} finally {
|
|
1783
|
+
this.conn.off("Target.attachedToTarget", onAttach);
|
|
1784
|
+
this.conn.send("Target.setAutoAttach", { autoAttach: false, waitForDebuggerOnStart: false, flatten: true }, this.sessionId).catch(() => {
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
const hit = attached.find((a) => a.url.includes(urlIncludes));
|
|
1788
|
+
if (!hit) {
|
|
1789
|
+
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`);
|
|
1790
|
+
}
|
|
1791
|
+
return evalIn(hit.sessionId);
|
|
1792
|
+
}
|
|
1418
1793
|
/** evaluateHandle — evaluates fn and returns a handle for element bounding box */
|
|
1419
1794
|
async evaluateHandle(fn, ...args) {
|
|
1420
1795
|
let expression;
|
|
@@ -1785,16 +2160,21 @@ Last error: ${lastError.message}` : "";
|
|
|
1785
2160
|
}
|
|
1786
2161
|
return 1;
|
|
1787
2162
|
}
|
|
1788
|
-
const
|
|
1789
|
-
|
|
1790
|
-
{ depth: 0 },
|
|
1791
|
-
|
|
2163
|
+
const withTimeout = (p, ms) => Promise.race([p, new Promise((r) => setTimeout(() => r(null), ms))]);
|
|
2164
|
+
const doc = await withTimeout(
|
|
2165
|
+
this.conn.send("DOM.getDocument", { depth: 0 }, this.sessionId),
|
|
2166
|
+
8e3
|
|
1792
2167
|
);
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2168
|
+
if (!doc) return 0;
|
|
2169
|
+
const result = await withTimeout(
|
|
2170
|
+
this.conn.send(
|
|
2171
|
+
"DOM.querySelector",
|
|
2172
|
+
{ nodeId: doc.root.nodeId, selector },
|
|
2173
|
+
this.sessionId
|
|
2174
|
+
),
|
|
2175
|
+
8e3
|
|
1797
2176
|
);
|
|
2177
|
+
if (!result) return 0;
|
|
1798
2178
|
return result.nodeId;
|
|
1799
2179
|
}
|
|
1800
2180
|
/** Query all matching elements, returns array of CDP nodeIds */
|
|
@@ -4685,15 +5065,18 @@ function deleteSessionDiskMeta(name) {
|
|
|
4685
5065
|
async function isSessionPageAlive(session) {
|
|
4686
5066
|
const page = session.page;
|
|
4687
5067
|
if (!page || typeof page.evaluate !== "function") return false;
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
5068
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
5069
|
+
try {
|
|
5070
|
+
await Promise.race([
|
|
5071
|
+
page.evaluate("1"),
|
|
5072
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("liveness probe timeout")), 1500))
|
|
5073
|
+
]);
|
|
5074
|
+
return true;
|
|
5075
|
+
} catch {
|
|
5076
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
5077
|
+
}
|
|
4696
5078
|
}
|
|
5079
|
+
return false;
|
|
4697
5080
|
}
|
|
4698
5081
|
async function findOrRestoreSession(name, cdpEndpoint) {
|
|
4699
5082
|
const inMem = findSession(name);
|
|
@@ -5116,7 +5499,7 @@ async function closeSessionByName(name) {
|
|
|
5116
5499
|
} catch {
|
|
5117
5500
|
}
|
|
5118
5501
|
try {
|
|
5119
|
-
const { SessionRecorder } = await import("./session-recorder-
|
|
5502
|
+
const { SessionRecorder } = await import("./session-recorder-SLDBENVF.js");
|
|
5120
5503
|
SessionRecorder.cleanup(session.name);
|
|
5121
5504
|
} catch {
|
|
5122
5505
|
}
|
|
@@ -13,7 +13,11 @@ var WARNING_TEXTS = [
|
|
|
13
13
|
{ text: "unusual traffic", severity: "high" },
|
|
14
14
|
{ text: "please verify you are human", severity: "medium" },
|
|
15
15
|
{ text: "access denied", severity: "high" },
|
|
16
|
-
|
|
16
|
+
// 裸词 "blocked" 会误伤大量正常页面("unblocked"、adblock 检测脚本文本、
|
|
17
|
+
// CSS-in-JS 字符串等,实测 doubao.com),只匹配完整阻断短语
|
|
18
|
+
{ text: "you have been blocked", severity: "high" },
|
|
19
|
+
{ text: "your access has been blocked", severity: "high" },
|
|
20
|
+
{ text: "access blocked", severity: "high" },
|
|
17
21
|
{ text: "rate limit", severity: "medium" },
|
|
18
22
|
{ text: "too many requests", severity: "medium" },
|
|
19
23
|
{ text: "\u9A8C\u8BC1", severity: "low" },
|
|
@@ -122,7 +126,7 @@ async function detectCaptcha(page) {
|
|
|
122
126
|
}
|
|
123
127
|
async function detectWarningText(page) {
|
|
124
128
|
try {
|
|
125
|
-
const pageText = await page.
|
|
129
|
+
const pageText = await page.evaluate(() => document.body?.innerText || "").catch(() => "") || "";
|
|
126
130
|
const lowerText = pageText.toLowerCase();
|
|
127
131
|
for (const { text, severity } of WARNING_TEXTS) {
|
|
128
132
|
if (lowerText.includes(text.toLowerCase())) {
|