@xbrowser/cli 1.9.9 → 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-FFYPFTBV.js → browser-Q5APBNF6.js} +1 -1
- package/dist/{browser-YCO4GJMZ.js → browser-V3JIWSTR.js} +2 -2
- package/dist/{browser-RHWUAMTB.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-6OZWKXSW.js → chunk-HVPUVVSA.js} +72 -15
- package/dist/{chunk-GUTBIYFW.js → chunk-JEDP4PJW.js} +507 -70
- 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-RZM5CNIY.js → chunk-XKQVUFUS.js} +72 -15
- package/dist/{chunk-RRBXV7KE.js → chunk-XQ4HRPDJ.js} +384 -102
- package/dist/cli.js +79 -29
- 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 +103 -32
- package/dist/index.d.ts +8 -0
- package/dist/index.js +94 -31
- 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
|
@@ -19,6 +19,161 @@ import { EventEmitter as EventEmitter2 } from "events";
|
|
|
19
19
|
// src/cdp-driver/page.ts
|
|
20
20
|
import { EventEmitter } from "events";
|
|
21
21
|
|
|
22
|
+
// src/cdp-driver/stealth.ts
|
|
23
|
+
var DEFAULT_STEALTH_CONFIG = {
|
|
24
|
+
bezierCurvature: [0.35, 0.6],
|
|
25
|
+
noiseAmplitude: 5.5,
|
|
26
|
+
overshootRange: [6, 14],
|
|
27
|
+
aimPause: [150, 400],
|
|
28
|
+
pressDuration: [60, 140],
|
|
29
|
+
releaseDrift: [0.8, 2.5],
|
|
30
|
+
landingOffsetSmall: [0.3, 2.5],
|
|
31
|
+
landingOffsetLarge: [1.5, 7],
|
|
32
|
+
smallElementThreshold: 30,
|
|
33
|
+
typingRhythm: {
|
|
34
|
+
fastProb: 0.22,
|
|
35
|
+
fastRange: [25, 60],
|
|
36
|
+
normalRange: [50, 350],
|
|
37
|
+
pauseProb: 0.18,
|
|
38
|
+
pauseRange: [400, 1200]
|
|
39
|
+
},
|
|
40
|
+
keyPressDuration: [50, 110],
|
|
41
|
+
typoProbability: 0.06,
|
|
42
|
+
wheelPeak: 180,
|
|
43
|
+
wheelDecayRate: 0.4
|
|
44
|
+
};
|
|
45
|
+
function rand(min, max) {
|
|
46
|
+
return min + Math.random() * (max - min);
|
|
47
|
+
}
|
|
48
|
+
function cosineEase(t) {
|
|
49
|
+
return 0.5 - 0.5 * Math.cos(Math.PI * t);
|
|
50
|
+
}
|
|
51
|
+
function bezierTrajectory(x0, y0, x1, y1, config = DEFAULT_STEALTH_CONFIG) {
|
|
52
|
+
const dist = Math.hypot(x1 - x0, y1 - y0);
|
|
53
|
+
const n = Math.max(10, Math.min(28, Math.round(dist / 15)));
|
|
54
|
+
const shortMove = dist < 120;
|
|
55
|
+
const curvature = shortMove ? rand(2, 6) : Math.max(dist * rand(...config.bezierCurvature), rand(18, 35));
|
|
56
|
+
const dir = Math.random() < 0.5 ? 1 : -1;
|
|
57
|
+
const d = dist || 1;
|
|
58
|
+
const dx = x1 - x0, dy = y1 - y0;
|
|
59
|
+
const c1x = x0 + dx * 0.3 - dy / d * curvature * 0.5 * dir;
|
|
60
|
+
const c1y = y0 + dy * 0.3 + dx / d * curvature * 0.5 * dir;
|
|
61
|
+
const c2x = x0 + dx * 0.7 - dy / d * curvature * 0.8 * dir;
|
|
62
|
+
const c2y = y0 + dy * 0.7 + dx / d * curvature * 0.8 * dir;
|
|
63
|
+
const points = [];
|
|
64
|
+
for (let i = 1; i <= n; i++) {
|
|
65
|
+
const t = cosineEase(i / n);
|
|
66
|
+
const mt = 1 - t;
|
|
67
|
+
let px = mt ** 3 * x0 + 3 * mt ** 2 * t * c1x + 3 * mt * t ** 2 * c2x + t ** 3 * x1;
|
|
68
|
+
let py = mt ** 3 * y0 + 3 * mt ** 2 * t * c1y + 3 * mt * t ** 2 * c2y + t ** 3 * y1;
|
|
69
|
+
const amp = shortMove ? Math.min(2, config.noiseAmplitude) : config.noiseAmplitude;
|
|
70
|
+
px += rand(-amp, amp);
|
|
71
|
+
py += rand(-amp, amp);
|
|
72
|
+
points.push({ x: px, y: py, delay: rand(9, 16) });
|
|
73
|
+
}
|
|
74
|
+
if (!shortMove) {
|
|
75
|
+
const over = rand(...config.overshootRange);
|
|
76
|
+
const ox = x1 + dx / d * over + rand(-2, 2);
|
|
77
|
+
const oy = y1 + dy / d * over + rand(-2, 2);
|
|
78
|
+
points.push({ x: ox, y: oy, delay: rand(14, 30) });
|
|
79
|
+
points.push({
|
|
80
|
+
x: x1 + dx / d * over * 0.4,
|
|
81
|
+
y: y1 + dy / d * over * 0.4,
|
|
82
|
+
delay: rand(14, 30)
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
points.push({ x: x1 + rand(-1, 1), y: y1 + rand(-1, 1), delay: rand(14, 30) });
|
|
86
|
+
return points;
|
|
87
|
+
}
|
|
88
|
+
function landingOffset(width, height, config = DEFAULT_STEALTH_CONFIG) {
|
|
89
|
+
const isSmall = Math.min(width, height) < config.smallElementThreshold;
|
|
90
|
+
const range = isSmall ? config.landingOffsetSmall : config.landingOffsetLarge;
|
|
91
|
+
const dx = rand(...range) * (Math.random() < 0.5 ? -1 : 1);
|
|
92
|
+
const dy = rand(...range) * (Math.random() < 0.5 ? -1 : 1);
|
|
93
|
+
return { dx, dy };
|
|
94
|
+
}
|
|
95
|
+
var KEY_MAP = {};
|
|
96
|
+
for (let i = 97; i <= 122; i++) {
|
|
97
|
+
const ch = String.fromCharCode(i);
|
|
98
|
+
KEY_MAP[ch] = { key: ch, code: "Key" + ch.toUpperCase(), vk: i - 32 };
|
|
99
|
+
}
|
|
100
|
+
for (let i = 65; i <= 90; i++) {
|
|
101
|
+
const ch = String.fromCharCode(i);
|
|
102
|
+
KEY_MAP[ch] = { key: ch, code: "Key" + ch, vk: i, shift: true };
|
|
103
|
+
}
|
|
104
|
+
for (let i = 48; i <= 57; i++) {
|
|
105
|
+
const ch = String.fromCharCode(i);
|
|
106
|
+
KEY_MAP[ch] = { key: ch, code: "Digit" + ch, vk: i };
|
|
107
|
+
}
|
|
108
|
+
Object.assign(KEY_MAP, {
|
|
109
|
+
" ": { key: " ", code: "Space", vk: 32 },
|
|
110
|
+
".": { key: ".", code: "Period", vk: 190 },
|
|
111
|
+
"-": { key: "-", code: "Minus", vk: 189 },
|
|
112
|
+
"@": { key: "@", code: "Digit2", vk: 50, shift: true },
|
|
113
|
+
"_": { key: "_", code: "Minus", vk: 189, shift: true }
|
|
114
|
+
});
|
|
115
|
+
function buildStealthInitScript() {
|
|
116
|
+
return [
|
|
117
|
+
"(function(){",
|
|
118
|
+
// 1. AEL event proxy
|
|
119
|
+
" var o=EventTarget.prototype.addEventListener;",
|
|
120
|
+
" var fc=new InputDeviceCapabilities({firesTouchEvents:false});",
|
|
121
|
+
" var _ael=function(t,f){",
|
|
122
|
+
" var op=arguments[2];",
|
|
123
|
+
' if(typeof f!=="function")return o.call(this,t,f,op);',
|
|
124
|
+
" var w=function(e){",
|
|
125
|
+
" if(!e||e.constructor===FocusEvent||e.constructor===KeyboardEvent)return f.call(this,e);",
|
|
126
|
+
" return f.call(this,new Proxy(e,{get:function(k,p){",
|
|
127
|
+
' if(p==="sourceCapabilities")return fc;',
|
|
128
|
+
' if(p==="isTrusted")return true;',
|
|
129
|
+
' if((p==="clientX"||p==="clientY")&&k.type==="click"&&Number.isFinite(k[p])){',
|
|
130
|
+
" var _f=((k.timeStamp||Date.now())%89)/89*0.7+0.15;",
|
|
131
|
+
" return k[p]+_f;",
|
|
132
|
+
" }",
|
|
133
|
+
' var v=Reflect.get(k,p);return typeof v==="function"?v.bind(k):v;',
|
|
134
|
+
" }}));",
|
|
135
|
+
" };",
|
|
136
|
+
" return o.call(this,t,w,op);",
|
|
137
|
+
" };",
|
|
138
|
+
" EventTarget.prototype.addEventListener=_ael;",
|
|
139
|
+
// 2. Screen override (prototype-level, not instance-level)
|
|
140
|
+
" var _gw=function(){return 1728};",
|
|
141
|
+
" var _gh=function(){return 1117};",
|
|
142
|
+
" var _gah=function(){return 1092};",
|
|
143
|
+
' Object.defineProperty(Screen.prototype,"width",{get:_gw,configurable:true});',
|
|
144
|
+
' Object.defineProperty(Screen.prototype,"height",{get:_gh,configurable:true});',
|
|
145
|
+
' Object.defineProperty(Screen.prototype,"availWidth",{get:_gw,configurable:true});',
|
|
146
|
+
' Object.defineProperty(Screen.prototype,"availHeight",{get:_gah,configurable:true});',
|
|
147
|
+
" document.hasFocus=function(){return true};",
|
|
148
|
+
// 3. toString disguise (name-list based)
|
|
149
|
+
" var _ts=Function.prototype.toString;",
|
|
150
|
+
" var _hf=document.hasFocus;",
|
|
151
|
+
" Function.prototype.toString=function(){",
|
|
152
|
+
' if(this===_ael)return"function addEventListener(type, callback) { [native code] }";',
|
|
153
|
+
' if(this===_hf)return"function hasFocus() { [native code] }";',
|
|
154
|
+
' if(this===_gw)return"function get width() { [native code] }";',
|
|
155
|
+
' if(this===_gh)return"function get height() { [native code] }";',
|
|
156
|
+
' if(this===_gah)return"function get availHeight() { [native code] }";',
|
|
157
|
+
" return _ts.call(this);",
|
|
158
|
+
" };",
|
|
159
|
+
// 4. onclick prototype hijack (dual-stream consistency)
|
|
160
|
+
" var _ba=function(k,p){",
|
|
161
|
+
' if(p==="isTrusted")return true;',
|
|
162
|
+
' if((p==="clientX"||p==="clientY")&&k.type==="click"&&Number.isFinite(k[p])){',
|
|
163
|
+
" var _f=((k.timeStamp||Date.now())%89)/89*0.7+0.15;return k[p]+_f;",
|
|
164
|
+
" }",
|
|
165
|
+
' var v=Reflect.get(k,p);return typeof v==="function"?v.bind(k):v;',
|
|
166
|
+
" };",
|
|
167
|
+
' Object.defineProperty(Document.prototype,"onclick",{',
|
|
168
|
+
" configurable:true,",
|
|
169
|
+
" get:function(){var raw=this.__ocRaw||null;if(!raw)return null;var self=this;",
|
|
170
|
+
" return function(e){return raw.call(self,new Proxy(e,{get:function(k,p){return _ba(k,p)}}))}},",
|
|
171
|
+
" set:function(fn){this.__ocRaw=fn}",
|
|
172
|
+
" });",
|
|
173
|
+
"})()"
|
|
174
|
+
].join("\n");
|
|
175
|
+
}
|
|
176
|
+
|
|
22
177
|
// src/cdp-driver/mouse.ts
|
|
23
178
|
var XBMouseImpl = class {
|
|
24
179
|
conn;
|
|
@@ -40,18 +195,35 @@ var XBMouseImpl = class {
|
|
|
40
195
|
}
|
|
41
196
|
async click(x, y, opts = {}) {
|
|
42
197
|
const button = opts.button ?? "left";
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
198
|
+
const stealth = opts.stealth ?? true;
|
|
199
|
+
let tx = x, ty = y;
|
|
200
|
+
if (stealth && opts.elementWidth !== void 0 && opts.elementHeight !== void 0) {
|
|
201
|
+
const off = landingOffset(opts.elementWidth, opts.elementHeight);
|
|
202
|
+
tx += off.dx;
|
|
203
|
+
ty += off.dy;
|
|
204
|
+
}
|
|
205
|
+
if (stealth) {
|
|
206
|
+
const traj = bezierTrajectory(this._x, this._y, tx, ty);
|
|
207
|
+
for (const p of traj) {
|
|
208
|
+
await this.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: p.x, y: p.y, button: this._button });
|
|
209
|
+
this._x = p.x;
|
|
210
|
+
this._y = p.y;
|
|
211
|
+
await sleep(p.delay);
|
|
212
|
+
}
|
|
213
|
+
await sleep(rand(...DEFAULT_STEALTH_CONFIG.aimPause));
|
|
214
|
+
} else {
|
|
215
|
+
await this.move(tx, ty);
|
|
49
216
|
}
|
|
50
|
-
await this.
|
|
51
|
-
|
|
52
|
-
|
|
217
|
+
await this.down({ button });
|
|
218
|
+
await sleep(stealth ? rand(...DEFAULT_STEALTH_CONFIG.pressDuration) : opts.delay ?? 0);
|
|
219
|
+
const rx = stealth ? this._x + rand(...DEFAULT_STEALTH_CONFIG.releaseDrift) * (Math.random() < 0.5 ? -1 : 1) : this._x;
|
|
220
|
+
const ry = stealth ? this._y + rand(...DEFAULT_STEALTH_CONFIG.releaseDrift) * (Math.random() < 0.5 ? -1 : 1) : this._y;
|
|
221
|
+
this._x = rx;
|
|
222
|
+
this._y = ry;
|
|
223
|
+
await this.send("Input.dispatchMouseEvent", { type: "mouseReleased", x: rx, y: ry, button, clickCount: opts.clickCount ?? 1 });
|
|
224
|
+
for (let i = 1; i < (opts.clickCount ?? 1); i++) {
|
|
225
|
+
if (opts.delay) await sleep(opts.delay);
|
|
53
226
|
await this.down({ button });
|
|
54
|
-
if (delay > 0) await sleep(delay);
|
|
55
227
|
await this.up({ button });
|
|
56
228
|
}
|
|
57
229
|
}
|
|
@@ -230,7 +402,7 @@ var XBKeyboardImpl = class {
|
|
|
230
402
|
}
|
|
231
403
|
};
|
|
232
404
|
function resolveKeyMapping(key) {
|
|
233
|
-
if (
|
|
405
|
+
if (KEY_MAP2[key]) return KEY_MAP2[key];
|
|
234
406
|
if (key.length === 1) {
|
|
235
407
|
const lower = key.toLowerCase();
|
|
236
408
|
if (lower >= "a" && lower <= "z") {
|
|
@@ -247,7 +419,7 @@ function resolveKeyMapping(key) {
|
|
|
247
419
|
}
|
|
248
420
|
return { key, code: key };
|
|
249
421
|
}
|
|
250
|
-
var
|
|
422
|
+
var KEY_MAP2 = {
|
|
251
423
|
Enter: { key: "Enter", code: "Enter", text: "\r", keyCode: 13 },
|
|
252
424
|
Tab: { key: "Tab", code: "Tab", text: " ", keyCode: 9 },
|
|
253
425
|
Escape: { key: "Escape", code: "Escape", keyCode: 27 },
|
|
@@ -285,6 +457,37 @@ function sleep2(ms) {
|
|
|
285
457
|
|
|
286
458
|
// src/cdp-driver/selector-utils.ts
|
|
287
459
|
function queryJS(selector) {
|
|
460
|
+
return `(${deepQueryIIFE})( ${JSON.stringify(queryMainJS(selector))} )`;
|
|
461
|
+
}
|
|
462
|
+
var deepQueryIIFE = `(function(mainExpr) {
|
|
463
|
+
const run = (root) => {
|
|
464
|
+
try { return new Function('document', 'return (' + mainExpr + ')')(root); }
|
|
465
|
+
catch (e) { return null; }
|
|
466
|
+
};
|
|
467
|
+
const scanRoot = (root) => {
|
|
468
|
+
const direct = run(root);
|
|
469
|
+
if (direct) return direct;
|
|
470
|
+
let all;
|
|
471
|
+
try { all = root.querySelectorAll('*'); } catch (e) { return null; }
|
|
472
|
+
for (const el of all) {
|
|
473
|
+
if (el.shadowRoot) {
|
|
474
|
+
const r = scanRoot(el.shadowRoot);
|
|
475
|
+
if (r) return r;
|
|
476
|
+
}
|
|
477
|
+
if (el.tagName === 'IFRAME') {
|
|
478
|
+
let inner = null;
|
|
479
|
+
try { inner = el.contentDocument; } catch (e) { /* cross-origin */ }
|
|
480
|
+
if (inner) {
|
|
481
|
+
const r = scanRoot(inner);
|
|
482
|
+
if (r) return r;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return null;
|
|
487
|
+
};
|
|
488
|
+
return scanRoot(document);
|
|
489
|
+
})`;
|
|
490
|
+
function queryMainJS(selector) {
|
|
288
491
|
if (selector.startsWith("xpath=")) {
|
|
289
492
|
const xpath = JSON.stringify(selector.slice(6));
|
|
290
493
|
return `document.evaluate(${xpath}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue`;
|
|
@@ -296,13 +499,35 @@ function queryJS(selector) {
|
|
|
296
499
|
return `(() => {
|
|
297
500
|
const target = ${JSON.stringify(text)};
|
|
298
501
|
const exact = ${exact};
|
|
502
|
+
// Match on OWN text nodes (not strict leaf elements): search-result
|
|
503
|
+
// titles mix text with inline highlight <em> marks \u2014 a strict leaf filter
|
|
504
|
+
// finds nothing there (real-world juejin). Own-text keeps the match
|
|
505
|
+
// precise (descendant-only text doesn't count) while tolerating markup.
|
|
506
|
+
const ownText = (e) => Array.prototype.filter.call(e.childNodes, (n) => n.nodeType === 3)
|
|
507
|
+
.map((n) => n.textContent).join('').trim();
|
|
299
508
|
const els = [...document.querySelectorAll('*')].filter(e => {
|
|
300
|
-
if (e.
|
|
301
|
-
|
|
302
|
-
const t = (e.textContent || '').trim();
|
|
509
|
+
if (e.offsetParent === null && e.tagName !== 'BODY') return false;
|
|
510
|
+
const t = ownText(e);
|
|
303
511
|
if (!t) return false;
|
|
304
512
|
return exact ? t === target : t.toLowerCase().includes(target.toLowerCase());
|
|
305
513
|
});
|
|
514
|
+
// Rank instead of raw DOM order: exact text beats substring, interactive
|
|
515
|
+
// elements (button/a/[onclick]/inputs) beat prose. Prevents matching a
|
|
516
|
+
// description paragraph that merely MENTIONS the target label
|
|
517
|
+
// (rec-duel d06: header text "\u76EE\u6807\u9879\u300C\u7B2C 87 \u53F7\u300D" hijacked text=\u7B2C 87 \u53F7).
|
|
518
|
+
const isInteractive = (e) => {
|
|
519
|
+
const tag = e.tagName;
|
|
520
|
+
return tag === 'BUTTON' || tag === 'A' || tag === 'INPUT' || tag === 'SELECT'
|
|
521
|
+
|| e.hasAttribute('onclick') || e.getAttribute('role') === 'button';
|
|
522
|
+
};
|
|
523
|
+
els.sort((a, b) => {
|
|
524
|
+
const ta = ownText(a), tb = ownText(b);
|
|
525
|
+
const ea = ta === target ? 0 : 1, eb = tb === target ? 0 : 1;
|
|
526
|
+
if (ea !== eb) return ea - eb;
|
|
527
|
+
const ia = isInteractive(a) ? 0 : 1, ib = isInteractive(b) ? 0 : 1;
|
|
528
|
+
if (ia !== ib) return ia - ib;
|
|
529
|
+
return 0; // stable \u2014 preserve DOM order
|
|
530
|
+
});
|
|
306
531
|
return els[0] || null;
|
|
307
532
|
})()`;
|
|
308
533
|
}
|
|
@@ -336,43 +561,53 @@ function queryAllJS(selector) {
|
|
|
336
561
|
async function waitForActionable(page, selector, opts = {}) {
|
|
337
562
|
const timeout = opts.timeout ?? 3e4;
|
|
338
563
|
if (opts.force) {
|
|
339
|
-
|
|
340
|
-
|
|
564
|
+
const deadline2 = Date.now() + timeout;
|
|
565
|
+
let lastError;
|
|
566
|
+
while (Date.now() < deadline2) {
|
|
567
|
+
const rect = await page.evaluate(`
|
|
341
568
|
(function() {
|
|
342
569
|
const el = ${queryJS(selector)};
|
|
343
570
|
if (!el) return null;
|
|
344
571
|
const r = el.getBoundingClientRect();
|
|
345
|
-
|
|
572
|
+
let x = r.x, y = r.y;
|
|
573
|
+
let doc = el.ownerDocument;
|
|
574
|
+
while (doc !== document) {
|
|
575
|
+
let host = null;
|
|
576
|
+
const scan = (d) => {
|
|
577
|
+
let frames;
|
|
578
|
+
try { frames = d.querySelectorAll('iframe'); } catch (e) { return null; }
|
|
579
|
+
for (const f of frames) {
|
|
580
|
+
let inner = null;
|
|
581
|
+
try { inner = f.contentDocument; } catch (e) { continue; }
|
|
582
|
+
if (!inner) continue;
|
|
583
|
+
if (inner === doc) return f;
|
|
584
|
+
const rr = scan(inner);
|
|
585
|
+
if (rr) return rr;
|
|
586
|
+
}
|
|
587
|
+
return null;
|
|
588
|
+
};
|
|
589
|
+
host = scan(document);
|
|
590
|
+
if (!host) break;
|
|
591
|
+
const hr = host.getBoundingClientRect();
|
|
592
|
+
x += hr.x; y += hr.y;
|
|
593
|
+
doc = host.ownerDocument;
|
|
594
|
+
}
|
|
595
|
+
return { x, y, width: r.width, height: r.height };
|
|
346
596
|
})()
|
|
347
|
-
`);
|
|
348
|
-
if (
|
|
349
|
-
|
|
597
|
+
`).catch(() => null);
|
|
598
|
+
if (rect && rect.width > 0 && rect.height > 0) return { nodeId: 0, rect };
|
|
599
|
+
lastError = `Element not visible (zero size): ${selector}`;
|
|
600
|
+
lastError = `Element not found: ${selector}`;
|
|
601
|
+
await page.waitForTimeout(200);
|
|
350
602
|
}
|
|
351
|
-
|
|
352
|
-
let lastError;
|
|
353
|
-
let nodeId = 0;
|
|
354
|
-
let rect = null;
|
|
355
|
-
while (Date.now() < deadline2) {
|
|
356
|
-
nodeId = await page.querySelector(selector);
|
|
357
|
-
if (!nodeId) {
|
|
358
|
-
lastError = `Element not found: ${selector}`;
|
|
359
|
-
await page.waitForTimeout(200);
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
rect = await page.getBoxModel(nodeId);
|
|
363
|
-
if (rect) break;
|
|
364
|
-
lastError = `Element has no box: ${selector}`;
|
|
365
|
-
await page.waitForTimeout(500);
|
|
366
|
-
}
|
|
367
|
-
if (!rect) throw new Error(lastError || `Element has no box: ${selector}`);
|
|
368
|
-
return { nodeId, rect };
|
|
603
|
+
throw new Error(lastError || `Element not found: ${selector}`);
|
|
369
604
|
}
|
|
370
605
|
const deadline = Date.now() + timeout;
|
|
371
606
|
while (Date.now() < deadline) {
|
|
372
607
|
const result = await checkActionable(page, selector);
|
|
373
608
|
if (result.ok && result.rect) {
|
|
374
|
-
const nodeId = await page.querySelector(selector);
|
|
375
|
-
|
|
609
|
+
const nodeId = await page.querySelector(selector).catch(() => 0) ?? 0;
|
|
610
|
+
return { nodeId, rect: result.rect };
|
|
376
611
|
}
|
|
377
612
|
await page.waitForTimeout(50);
|
|
378
613
|
}
|
|
@@ -405,12 +640,25 @@ async function checkActionable(page, selector) {
|
|
|
405
640
|
return { ok: false, reason: 'parent_disabled' };
|
|
406
641
|
}
|
|
407
642
|
|
|
408
|
-
// Check not covered by another element at center
|
|
643
|
+
// Check not covered by another element at center.
|
|
644
|
+
// elementFromPoint must run in the element's OWN document: for iframe-
|
|
645
|
+
// internal elements the main-document hit-test returns the <iframe>
|
|
646
|
+
// host itself, which falsely reports "covered" (rec-duel d01).
|
|
647
|
+
// For shadow-internal elements the hit-test retargets to the shadow
|
|
648
|
+
// HOST \u2014 walk the host chain before declaring coverage (rec-duel d04).
|
|
409
649
|
const cx = rect.x + rect.width / 2;
|
|
410
650
|
const cy = rect.y + rect.height / 2;
|
|
411
|
-
const topEl =
|
|
651
|
+
const topEl = el.ownerDocument.elementFromPoint(cx, cy);
|
|
412
652
|
if (topEl && topEl !== el && !el.contains(topEl) && !topEl.contains(el)) {
|
|
413
|
-
|
|
653
|
+
let hostChain = [];
|
|
654
|
+
let rootNode = el.getRootNode();
|
|
655
|
+
while (rootNode && rootNode.host) {
|
|
656
|
+
hostChain.push(rootNode.host);
|
|
657
|
+
rootNode = rootNode.host.getRootNode();
|
|
658
|
+
}
|
|
659
|
+
if (!hostChain.includes(topEl)) {
|
|
660
|
+
return { ok: false, reason: 'covered', rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } };
|
|
661
|
+
}
|
|
414
662
|
}
|
|
415
663
|
|
|
416
664
|
return {
|
|
@@ -455,21 +703,48 @@ var XBLocatorImpl = class _XBLocatorImpl {
|
|
|
455
703
|
const el = ${this._q(this.selector)};
|
|
456
704
|
if (!el) return null;
|
|
457
705
|
const rect = el.getBoundingClientRect();
|
|
458
|
-
|
|
706
|
+
let x = rect.x, y = rect.y;
|
|
707
|
+
let doc = el.ownerDocument;
|
|
708
|
+
while (doc !== document) {
|
|
709
|
+
let host = null;
|
|
710
|
+
const scan = (d) => {
|
|
711
|
+
let frames;
|
|
712
|
+
try { frames = d.querySelectorAll('iframe'); } catch (e) { return null; }
|
|
713
|
+
for (const f of frames) {
|
|
714
|
+
let inner = null;
|
|
715
|
+
try { inner = f.contentDocument; } catch (e) { continue; }
|
|
716
|
+
if (!inner) continue;
|
|
717
|
+
if (inner === doc) return f;
|
|
718
|
+
const r = scan(inner);
|
|
719
|
+
if (r) return r;
|
|
720
|
+
}
|
|
721
|
+
return null;
|
|
722
|
+
};
|
|
723
|
+
host = scan(document);
|
|
724
|
+
if (!host) break;
|
|
725
|
+
const hr = host.getBoundingClientRect();
|
|
726
|
+
x += hr.x; y += hr.y;
|
|
727
|
+
doc = host.ownerDocument;
|
|
728
|
+
}
|
|
729
|
+
return { x, y, width: rect.width, height: rect.height };
|
|
459
730
|
})()
|
|
460
731
|
`);
|
|
461
732
|
const finalRect = updatedRect ?? rect;
|
|
462
733
|
const cx = finalRect.x + finalRect.width / 2;
|
|
463
734
|
const cy = finalRect.y + finalRect.height / 2;
|
|
464
735
|
await this.page.mouse.click(cx, cy, {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
736
|
+
stealth: true,
|
|
737
|
+
elementWidth: finalRect.width,
|
|
738
|
+
elementHeight: finalRect.height,
|
|
739
|
+
...{ button: opts.button ?? "left", clickCount: opts.clickCount ?? 1, delay: opts.delay }
|
|
468
740
|
});
|
|
469
741
|
}
|
|
470
742
|
async fill(value, opts = {}) {
|
|
471
743
|
await waitForActionable(this.page, this.selector, opts);
|
|
472
744
|
await scrollIntoView(this.page, this.selector);
|
|
745
|
+
await this.click({ ...opts });
|
|
746
|
+
await this.page.keyboard.type(value, { stealth: true });
|
|
747
|
+
return;
|
|
473
748
|
await this.page.evaluate(`
|
|
474
749
|
(function() {
|
|
475
750
|
const el = ${this._q(this.selector)};
|
|
@@ -1179,6 +1454,8 @@ var XBPageImpl = class _XBPageImpl {
|
|
|
1179
1454
|
await this.conn.send("Page.enable", void 0, this.sessionId);
|
|
1180
1455
|
await this.conn.send("Runtime.enable", void 0, this.sessionId);
|
|
1181
1456
|
await this.conn.send("Network.enable", void 0, this.sessionId);
|
|
1457
|
+
await this.conn.send("DOM.enable", void 0, this.sessionId).catch(() => {
|
|
1458
|
+
});
|
|
1182
1459
|
this.setupPageEvents();
|
|
1183
1460
|
this.setupNetworkEvents();
|
|
1184
1461
|
this.setupConsoleEvents();
|
|
@@ -1191,6 +1468,9 @@ var XBPageImpl = class _XBPageImpl {
|
|
|
1191
1468
|
);
|
|
1192
1469
|
this._url = info.url;
|
|
1193
1470
|
this._title = info.title;
|
|
1471
|
+
if (info.url && info.url !== "about:blank" && info.url !== "") {
|
|
1472
|
+
this._loadState = { loadFired: true, domContentFired: true, networkIdle: true };
|
|
1473
|
+
}
|
|
1194
1474
|
} catch {
|
|
1195
1475
|
}
|
|
1196
1476
|
}
|
|
@@ -1203,6 +1483,16 @@ var XBPageImpl = class _XBPageImpl {
|
|
|
1203
1483
|
const waitUntil = opts.waitUntil ?? "load";
|
|
1204
1484
|
const timeout = opts.timeout ?? 3e4;
|
|
1205
1485
|
this._loadState = { loadFired: false, domContentFired: false, networkIdle: false };
|
|
1486
|
+
if (process.env.XBROWSER_STEALTH !== "off") {
|
|
1487
|
+
try {
|
|
1488
|
+
await this.conn.send(
|
|
1489
|
+
"Page.addScriptToEvaluateOnNewDocument",
|
|
1490
|
+
{ source: buildStealthInitScript() },
|
|
1491
|
+
this.sessionId
|
|
1492
|
+
);
|
|
1493
|
+
} catch {
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1206
1496
|
const result = await this.conn.send(
|
|
1207
1497
|
"Page.navigate",
|
|
1208
1498
|
{ url, referrer: opts.referer },
|
|
@@ -1410,6 +1700,91 @@ Last error: ${lastError.message}` : "";
|
|
|
1410
1700
|
}
|
|
1411
1701
|
return result.result?.value;
|
|
1412
1702
|
}
|
|
1703
|
+
/**
|
|
1704
|
+
* 在指定 iframe 上下文中执行表达式(攻防 D16 能力建设,2026-08-19)。
|
|
1705
|
+
*
|
|
1706
|
+
* 双路径:
|
|
1707
|
+
* 1. 同进程 iframe —— Runtime.enable 收集 executionContextCreated,
|
|
1708
|
+
* 找到目标 frameId 的 contextId,用 contextId 定向执行;
|
|
1709
|
+
* 2. 跨域 OOPIF(独立 target)—— Target.setAutoAttach(flatten) 监听
|
|
1710
|
+
* attachedToTarget 中 type==='iframe' 的会话,用其 sessionId 执行。
|
|
1711
|
+
*
|
|
1712
|
+
* 这绕过了页面同源策略(那是页面 JS 的约束,CDP 是调试通道)——
|
|
1713
|
+
* 支付窗/验证码/第三方嵌入内容的读写都靠它。
|
|
1714
|
+
*/
|
|
1715
|
+
async evaluateInFrame(urlIncludes, expression) {
|
|
1716
|
+
if (this._closed) throw new Error("Page is closed");
|
|
1717
|
+
const evalIn = async (sessionId, contextId) => {
|
|
1718
|
+
const params = { expression, returnByValue: true, awaitPromise: true };
|
|
1719
|
+
if (contextId !== void 0) params.contextId = contextId;
|
|
1720
|
+
const result = await this.conn.send("Runtime.evaluate", params, sessionId);
|
|
1721
|
+
if (result.exceptionDetails) {
|
|
1722
|
+
const detail = result.exceptionDetails.exception?.description ?? result.exceptionDetails.exception?.value ?? result.exceptionDetails.text;
|
|
1723
|
+
throw new Error(`[frame ${urlIncludes}] ${detail}`);
|
|
1724
|
+
}
|
|
1725
|
+
return result.result?.value;
|
|
1726
|
+
};
|
|
1727
|
+
try {
|
|
1728
|
+
const tg = await this.conn.send("Target.getTargets", void 0);
|
|
1729
|
+
const hit2 = (tg.targetInfos || []).find((t) => t.type === "iframe" && (t.url || "").includes(urlIncludes));
|
|
1730
|
+
if (hit2) {
|
|
1731
|
+
const att = await this.conn.send("Target.attachToTarget", { targetId: hit2.targetId, flatten: true });
|
|
1732
|
+
return evalIn(att.sessionId);
|
|
1733
|
+
}
|
|
1734
|
+
} catch {
|
|
1735
|
+
}
|
|
1736
|
+
const tree = await this.conn.send("Page.getFrameTree", void 0, this.sessionId);
|
|
1737
|
+
const all = [];
|
|
1738
|
+
const walk = (node) => {
|
|
1739
|
+
all.push({ id: node.frame.id, url: node.frame.url });
|
|
1740
|
+
for (const child of node.childFrames || []) walk(child);
|
|
1741
|
+
};
|
|
1742
|
+
walk(tree.frameTree);
|
|
1743
|
+
const mainId = tree.frameTree?.frame?.id;
|
|
1744
|
+
const target = all.find((f) => f.id !== mainId && f.url.includes(urlIncludes));
|
|
1745
|
+
if (target) {
|
|
1746
|
+
const contexts = [];
|
|
1747
|
+
const onCtx = (raw) => {
|
|
1748
|
+
const c = raw?.context;
|
|
1749
|
+
if (c?.id && c?.auxData?.frameId) contexts.push({ id: c.id, frameId: c.auxData.frameId });
|
|
1750
|
+
};
|
|
1751
|
+
this.conn.on("Runtime.executionContextCreated", onCtx);
|
|
1752
|
+
try {
|
|
1753
|
+
await this.conn.send("Runtime.enable", void 0, this.sessionId).catch(() => {
|
|
1754
|
+
});
|
|
1755
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
1756
|
+
} finally {
|
|
1757
|
+
this.conn.off("Runtime.executionContextCreated", onCtx);
|
|
1758
|
+
}
|
|
1759
|
+
const ctx = contexts.find((c) => c.frameId === target.id);
|
|
1760
|
+
if (ctx) return evalIn(this.sessionId, ctx.id);
|
|
1761
|
+
}
|
|
1762
|
+
const attached = [];
|
|
1763
|
+
const onAttach = (raw) => {
|
|
1764
|
+
const ev = raw;
|
|
1765
|
+
if (ev?.sessionId && ev.targetInfo?.type === "iframe") {
|
|
1766
|
+
attached.push({ sessionId: ev.sessionId, url: ev.targetInfo.url || "" });
|
|
1767
|
+
}
|
|
1768
|
+
};
|
|
1769
|
+
this.conn.on("Target.attachedToTarget", onAttach);
|
|
1770
|
+
try {
|
|
1771
|
+
await this.conn.send("Target.setAutoAttach", {
|
|
1772
|
+
autoAttach: true,
|
|
1773
|
+
waitForDebuggerOnStart: false,
|
|
1774
|
+
flatten: true
|
|
1775
|
+
}, this.sessionId);
|
|
1776
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
1777
|
+
} finally {
|
|
1778
|
+
this.conn.off("Target.attachedToTarget", onAttach);
|
|
1779
|
+
this.conn.send("Target.setAutoAttach", { autoAttach: false, waitForDebuggerOnStart: false, flatten: true }, this.sessionId).catch(() => {
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
const hit = attached.find((a) => a.url.includes(urlIncludes));
|
|
1783
|
+
if (!hit) {
|
|
1784
|
+
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`);
|
|
1785
|
+
}
|
|
1786
|
+
return evalIn(hit.sessionId);
|
|
1787
|
+
}
|
|
1413
1788
|
/** evaluateHandle — evaluates fn and returns a handle for element bounding box */
|
|
1414
1789
|
async evaluateHandle(fn, ...args) {
|
|
1415
1790
|
let expression;
|
|
@@ -1780,16 +2155,21 @@ Last error: ${lastError.message}` : "";
|
|
|
1780
2155
|
}
|
|
1781
2156
|
return 1;
|
|
1782
2157
|
}
|
|
1783
|
-
const
|
|
1784
|
-
|
|
1785
|
-
{ depth: 0 },
|
|
1786
|
-
|
|
2158
|
+
const withTimeout = (p, ms) => Promise.race([p, new Promise((r) => setTimeout(() => r(null), ms))]);
|
|
2159
|
+
const doc = await withTimeout(
|
|
2160
|
+
this.conn.send("DOM.getDocument", { depth: 0 }, this.sessionId),
|
|
2161
|
+
8e3
|
|
1787
2162
|
);
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
2163
|
+
if (!doc) return 0;
|
|
2164
|
+
const result = await withTimeout(
|
|
2165
|
+
this.conn.send(
|
|
2166
|
+
"DOM.querySelector",
|
|
2167
|
+
{ nodeId: doc.root.nodeId, selector },
|
|
2168
|
+
this.sessionId
|
|
2169
|
+
),
|
|
2170
|
+
8e3
|
|
1792
2171
|
);
|
|
2172
|
+
if (!result) return 0;
|
|
1793
2173
|
return result.nodeId;
|
|
1794
2174
|
}
|
|
1795
2175
|
/** Query all matching elements, returns array of CDP nodeIds */
|