aether-mcp-server 2.1.0 → 2.1.1
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/bridge/debugging.js +133 -0
- package/dist/bridge/inspection.js +302 -0
- package/dist/bridge/interaction.js +586 -0
- package/dist/bridge/navigation.js +146 -0
- package/dist/bridge/session.js +287 -0
- package/dist/cdp-bridge.js +652 -2306
- package/dist/cdp-client.js +226 -359
- package/dist/eval-scripts.js +1024 -0
- package/dist/index.js +16 -28
- package/dist/locator-engine.js +21 -187
- package/dist/logger.js +105 -0
- package/dist/page-snapshot-cache.js +17 -2
- package/dist/types.js +267 -0
- package/package.json +1 -1
package/dist/cdp-client.js
CHANGED
|
@@ -14,6 +14,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
14
14
|
const os_1 = __importDefault(require("os"));
|
|
15
15
|
const stealth_1 = require("./stealth");
|
|
16
16
|
const element_collector_1 = require("./element-collector");
|
|
17
|
+
const eval_scripts_1 = require("./eval-scripts");
|
|
17
18
|
class CdpClient {
|
|
18
19
|
ws = null;
|
|
19
20
|
messageId = 0;
|
|
@@ -32,7 +33,97 @@ class CdpClient {
|
|
|
32
33
|
mousePosition = null;
|
|
33
34
|
networkLoggingAttached = false;
|
|
34
35
|
diagnosticsLoggingAttached = false;
|
|
36
|
+
speedMultiplier = 1.0;
|
|
37
|
+
documentNodeId = null;
|
|
35
38
|
constructor() { }
|
|
39
|
+
/** Set speed multiplier. 0 = instant, 1 = normal, 2 = slow (2x delays). */
|
|
40
|
+
setSpeed(m) {
|
|
41
|
+
this.speedMultiplier = Math.max(0, m);
|
|
42
|
+
}
|
|
43
|
+
/** Get current speed multiplier. */
|
|
44
|
+
getSpeedMultiplier() {
|
|
45
|
+
return this.speedMultiplier;
|
|
46
|
+
}
|
|
47
|
+
// ─── CDP DOM-level utilities (3-5x faster than Runtime.evaluate) ───
|
|
48
|
+
/** Invalidate cached document node ID. Call after navigation or tab switch. */
|
|
49
|
+
invalidateDocumentNodeId() {
|
|
50
|
+
this.documentNodeId = null;
|
|
51
|
+
}
|
|
52
|
+
/** Get the document root nodeId (cached per page). */
|
|
53
|
+
async getDocumentNodeId() {
|
|
54
|
+
try {
|
|
55
|
+
if (this.documentNodeId !== null)
|
|
56
|
+
return this.documentNodeId;
|
|
57
|
+
const result = await this.sendCommand("DOM.getDocument", { depth: -1, pierce: true });
|
|
58
|
+
const nodeId = result.root.nodeId;
|
|
59
|
+
this.documentNodeId = nodeId;
|
|
60
|
+
return nodeId;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return 0; // sentinel — caller should handle
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Run DOM.querySelector via CDP (no JS injection). Returns nodeId or null. */
|
|
67
|
+
async querySelectorNodeId(selector) {
|
|
68
|
+
try {
|
|
69
|
+
const docId = await this.getDocumentNodeId();
|
|
70
|
+
if (!docId)
|
|
71
|
+
return null;
|
|
72
|
+
const result = await this.sendCommand("DOM.querySelector", { nodeId: docId, selector });
|
|
73
|
+
return result.nodeId ?? null;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Get bounding box from the layout engine (no JS injection). Returns null if hidden. */
|
|
80
|
+
async getBoxModel(nodeId) {
|
|
81
|
+
try {
|
|
82
|
+
const result = await this.sendCommand("DOM.getBoxModel", { nodeId });
|
|
83
|
+
if (!result || !result.model)
|
|
84
|
+
return null;
|
|
85
|
+
// quad[0..7] = x1,y1, x2,y2, x3,y3, x4,y4 (corners of content box)
|
|
86
|
+
// top-left = [quad[0], quad[1]], bottom-right = [quad[4], quad[5]]
|
|
87
|
+
const q = result.model.content;
|
|
88
|
+
return {
|
|
89
|
+
x: q[0],
|
|
90
|
+
y: q[1],
|
|
91
|
+
width: q[4] - q[0],
|
|
92
|
+
height: q[5] - q[1],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Focus an element via CDP DOM.focus. */
|
|
100
|
+
async focusNode(nodeId) {
|
|
101
|
+
try {
|
|
102
|
+
await this.sendCommand("DOM.focus", { nodeId });
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// ignore
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Get the center coordinates of an element matched by selector. */
|
|
109
|
+
async getElementCenter(selector) {
|
|
110
|
+
try {
|
|
111
|
+
const nodeId = await this.querySelectorNodeId(selector);
|
|
112
|
+
if (!nodeId)
|
|
113
|
+
return null;
|
|
114
|
+
const box = await this.getBoxModel(nodeId);
|
|
115
|
+
if (!box)
|
|
116
|
+
return null;
|
|
117
|
+
return {
|
|
118
|
+
x: box.x + box.width / 2,
|
|
119
|
+
y: box.y + box.height / 2,
|
|
120
|
+
width: box.width,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
36
127
|
/**
|
|
37
128
|
* Connect to existing Chrome instance on given port
|
|
38
129
|
*/
|
|
@@ -92,25 +183,22 @@ class CdpClient {
|
|
|
92
183
|
this.ws.on("open", async () => {
|
|
93
184
|
this.connected = true;
|
|
94
185
|
this.activeTarget = target;
|
|
186
|
+
this.documentNodeId = null;
|
|
95
187
|
console.error(`[CDP] Connected to target: ${target.title} (${target.url})`);
|
|
96
188
|
try {
|
|
97
|
-
// Enable
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
]);
|
|
109
|
-
// Keep animations running. Pausing them can freeze SPA loaders and leave pages looking blank.
|
|
110
|
-
this.sendCommand("Animation.setPlaybackRate", { playbackRate: 1 }).catch(() => { });
|
|
189
|
+
// Enable minimal CDP domains on connect. Everything else is
|
|
190
|
+
// lazily enabled by the caller when needed.
|
|
191
|
+
await this.sendCommand("Runtime.enable").catch(() => { });
|
|
192
|
+
// Inject locator bootstrap once so element resolution works.
|
|
193
|
+
await this.sendCommand("Page.addScriptToEvaluateOnNewDocument", {
|
|
194
|
+
source: eval_scripts_1.LOCATOR_BOOTSTRAP_SCRIPT
|
|
195
|
+
});
|
|
196
|
+
// Stealth script — makes the browser harder to detect as automated.
|
|
197
|
+
await this.sendCommand("Page.addScriptToEvaluateOnNewDocument", {
|
|
198
|
+
source: stealth_1.STEALTH_SCRIPT
|
|
199
|
+
}).catch(() => { });
|
|
111
200
|
this.attachNetworkLogging();
|
|
112
201
|
this.attachDiagnosticsLogging();
|
|
113
|
-
console.error("[CDP] Core CDP domains enabled");
|
|
114
202
|
}
|
|
115
203
|
catch (e) {
|
|
116
204
|
console.error("[CDP] Failed to enable core domains:", e);
|
|
@@ -176,6 +264,7 @@ class CdpClient {
|
|
|
176
264
|
* Navigate to URL
|
|
177
265
|
*/
|
|
178
266
|
async navigate(url) {
|
|
267
|
+
this.documentNodeId = null;
|
|
179
268
|
await this.sendCommand("Page.navigate", { url });
|
|
180
269
|
}
|
|
181
270
|
async navigateAndWait(url, timeout = 10000) {
|
|
@@ -312,50 +401,28 @@ class CdpClient {
|
|
|
312
401
|
});
|
|
313
402
|
}
|
|
314
403
|
/**
|
|
315
|
-
* Wait for a selector to appear in DOM
|
|
404
|
+
* Wait for a selector to appear in DOM — fast CDP polling.
|
|
316
405
|
*/
|
|
317
406
|
async waitForSelector(selector, timeout = 10000, options = {}) {
|
|
318
407
|
const startTime = Date.now();
|
|
319
408
|
let lastBox = null;
|
|
320
409
|
let stableSince = 0;
|
|
321
410
|
while (Date.now() - startTime < timeout) {
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
box: {
|
|
338
|
-
x: Math.round(rect.left),
|
|
339
|
-
y: Math.round(rect.top),
|
|
340
|
-
width: Math.round(rect.width),
|
|
341
|
-
height: Math.round(rect.height)
|
|
342
|
-
}
|
|
343
|
-
};
|
|
344
|
-
})()
|
|
345
|
-
`,
|
|
346
|
-
returnByValue: true,
|
|
347
|
-
});
|
|
348
|
-
const state = result.result?.value;
|
|
349
|
-
if (state?.found && (!options.visible || state.visible)) {
|
|
350
|
-
if (!options.stable)
|
|
351
|
-
return true;
|
|
352
|
-
const box = state.box;
|
|
353
|
-
const sameBox = lastBox &&
|
|
354
|
-
lastBox.x === box.x &&
|
|
355
|
-
lastBox.y === box.y &&
|
|
356
|
-
lastBox.width === box.width &&
|
|
357
|
-
lastBox.height === box.height;
|
|
358
|
-
if (sameBox) {
|
|
411
|
+
const nodeId = await this.querySelectorNodeId(selector).catch(() => null);
|
|
412
|
+
if (!nodeId) {
|
|
413
|
+
await new Promise(r => setTimeout(r, 30));
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (!options.visible && !options.stable)
|
|
417
|
+
return true;
|
|
418
|
+
const box = await this.getBoxModel(nodeId);
|
|
419
|
+
if (!box || box.width === 0 || box.height === 0) {
|
|
420
|
+
await new Promise(r => setTimeout(r, 30));
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (options.stable) {
|
|
424
|
+
const boxKey = `${Math.round(box.x)},${Math.round(box.y)},${Math.round(box.width)},${Math.round(box.height)}`;
|
|
425
|
+
if (lastBox === boxKey) {
|
|
359
426
|
if (!stableSince)
|
|
360
427
|
stableSince = Date.now();
|
|
361
428
|
if (Date.now() - stableSince >= 120)
|
|
@@ -363,10 +430,13 @@ class CdpClient {
|
|
|
363
430
|
}
|
|
364
431
|
else {
|
|
365
432
|
stableSince = 0;
|
|
366
|
-
lastBox =
|
|
433
|
+
lastBox = boxKey;
|
|
367
434
|
}
|
|
368
435
|
}
|
|
369
|
-
|
|
436
|
+
else {
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
await new Promise(r => setTimeout(r, 30));
|
|
370
440
|
}
|
|
371
441
|
return false;
|
|
372
442
|
}
|
|
@@ -415,7 +485,7 @@ class CdpClient {
|
|
|
415
485
|
this.removeEventListener("Network.responseReceived", resetIdle);
|
|
416
486
|
resolve(); // Resolve anyway after timeout
|
|
417
487
|
}, timeout);
|
|
418
|
-
idleCheckInterval = setInterval(checkIdle, 100);
|
|
488
|
+
idleCheckInterval = setInterval(checkIdle, Math.max(1, 100 * this.speedMultiplier));
|
|
419
489
|
this.on("Network.requestWillBeSent", resetIdle);
|
|
420
490
|
this.on("Network.responseReceived", resetIdle);
|
|
421
491
|
});
|
|
@@ -520,51 +590,84 @@ class CdpClient {
|
|
|
520
590
|
if (!target) {
|
|
521
591
|
throw new Error(`Target not found: ${targetId}`);
|
|
522
592
|
}
|
|
593
|
+
this.documentNodeId = null;
|
|
523
594
|
await this.attachToTarget(target);
|
|
524
595
|
}
|
|
525
596
|
/**
|
|
526
|
-
* Get a
|
|
597
|
+
* Get a rich Accessibility Tree preserving tree structure, node IDs, and selectors.
|
|
598
|
+
* AI agents can use this instead of screenshots for most decisions.
|
|
527
599
|
*/
|
|
528
|
-
async
|
|
600
|
+
async getRichAXTree() {
|
|
529
601
|
await this.sendCommand("Accessibility.enable");
|
|
530
|
-
const result = await this.sendCommand("Accessibility.getFullAXTree");
|
|
602
|
+
const result = await this.sendCommand("Accessibility.getFullAXTree", {});
|
|
531
603
|
if (!result || !result.nodes)
|
|
532
|
-
return
|
|
604
|
+
return { tree: null, nodeCount: 0 };
|
|
533
605
|
const nodes = result.nodes;
|
|
534
|
-
const interactiveNodes = [];
|
|
535
|
-
// Map of node IDs for fast lookup
|
|
536
606
|
const nodeMap = new Map();
|
|
537
|
-
|
|
538
|
-
// Helper to get name from node properties
|
|
539
|
-
const getNodeName = (node) => {
|
|
540
|
-
if (node.name?.value)
|
|
541
|
-
return node.name.value;
|
|
542
|
-
const nameProp = node.properties?.find((p) => p.name === "name");
|
|
543
|
-
return nameProp?.value?.value || "";
|
|
544
|
-
};
|
|
545
|
-
// Filter and simplify nodes
|
|
607
|
+
// First pass: build lookup
|
|
546
608
|
nodes.forEach((node) => {
|
|
547
|
-
const
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
].includes(role);
|
|
554
|
-
const hasAction = node.properties?.some((p) => ["pressed", "expanded", "selected", "focused"].includes(p.name));
|
|
555
|
-
if (isInteractive || (name && name.length > 0 && role !== "generic" && role !== "none")) {
|
|
556
|
-
interactiveNodes.push({
|
|
557
|
-
id: node.nodeId,
|
|
558
|
-
role: role,
|
|
559
|
-
name: name,
|
|
560
|
-
description: node.description?.value || "",
|
|
561
|
-
value: node.value?.value || "",
|
|
562
|
-
disabled: node.properties?.find((p) => p.name === "disabled")?.value?.value || false,
|
|
563
|
-
focused: node.properties?.find((p) => p.name === "focused")?.value?.value || false,
|
|
609
|
+
const name = node.name?.value || '';
|
|
610
|
+
const role = node.role?.value || 'unknown';
|
|
611
|
+
const properties = {};
|
|
612
|
+
if (node.properties) {
|
|
613
|
+
node.properties.forEach((p) => {
|
|
614
|
+
properties[p.name] = p.value?.value ?? p.value;
|
|
564
615
|
});
|
|
565
616
|
}
|
|
617
|
+
nodeMap.set(node.nodeId, {
|
|
618
|
+
axId: node.nodeId,
|
|
619
|
+
backendDOMNodeId: node.backendDOMNodeId,
|
|
620
|
+
role,
|
|
621
|
+
name,
|
|
622
|
+
description: node.description?.value || '',
|
|
623
|
+
value: node.value?.value || '',
|
|
624
|
+
disabled: !!properties.disabled,
|
|
625
|
+
focused: !!properties.focused,
|
|
626
|
+
checked: properties.checked,
|
|
627
|
+
selected: !!properties.selected,
|
|
628
|
+
expanded: !!properties.expanded,
|
|
629
|
+
pressed: !!properties.pressed,
|
|
630
|
+
children: [],
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
// Second pass: build tree by following parentIds
|
|
634
|
+
const roots = [];
|
|
635
|
+
nodes.forEach((node) => {
|
|
636
|
+
const enriched = nodeMap.get(node.nodeId);
|
|
637
|
+
if (!enriched)
|
|
638
|
+
return;
|
|
639
|
+
if (node.parentId && nodeMap.has(node.parentId)) {
|
|
640
|
+
nodeMap.get(node.parentId).children.push(enriched);
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
roots.push(enriched);
|
|
644
|
+
}
|
|
566
645
|
});
|
|
567
|
-
|
|
646
|
+
// Prune: remove nodes with no name AND no meaningful role AND no children
|
|
647
|
+
function prune(node) {
|
|
648
|
+
node.children = node.children.filter(prune);
|
|
649
|
+
const hasContent = node.name || node.value ||
|
|
650
|
+
['button', 'link', 'textbox', 'checkbox', 'radio', 'combobox', 'listbox', 'heading', 'alert', 'dialog', 'navigation', 'main', 'banner', 'contentinfo', 'complementary', 'form', 'search', 'table', 'list', 'listitem', 'menu', 'menuitem', 'tab', 'tablist', 'tabpanel', 'tree', 'treeitem', 'slider', 'switch', 'progressbar', 'img', 'image'].includes(node.role);
|
|
651
|
+
return hasContent || node.children.length > 0;
|
|
652
|
+
}
|
|
653
|
+
roots.forEach(prune);
|
|
654
|
+
// Count total nodes after pruning
|
|
655
|
+
let totalNodes = 0;
|
|
656
|
+
function count(n) { totalNodes++; n.children.forEach(count); }
|
|
657
|
+
roots.forEach(count);
|
|
658
|
+
return { tree: roots, nodeCount: totalNodes };
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Backward-compat: flat list of accessible nodes.
|
|
662
|
+
* Calls getRichAXTree() and flattens the tree structure.
|
|
663
|
+
*/
|
|
664
|
+
async getSimplifiedAccessibilityTree() {
|
|
665
|
+
const result = await this.getRichAXTree();
|
|
666
|
+
// Flatten for backward compat
|
|
667
|
+
const flat = [];
|
|
668
|
+
function flatten(n) { flat.push(n); n.children.forEach(flatten); }
|
|
669
|
+
(result.tree || []).forEach(flatten);
|
|
670
|
+
return flat;
|
|
568
671
|
}
|
|
569
672
|
async getNetworkTraffic() {
|
|
570
673
|
return this.networkTraffic;
|
|
@@ -901,297 +1004,61 @@ class CdpClient {
|
|
|
901
1004
|
async takeHeapSnapshot(reportProgress, treatGlobalObjectsAsRoots, captureNumericValue) {
|
|
902
1005
|
await this.sendCommand("HeapProfiler.takeHeapSnapshot", { reportProgress, treatGlobalObjectsAsRoots, captureNumericValue });
|
|
903
1006
|
}
|
|
904
|
-
// Simple multi-octave noise function (fractional Brownian motion approximation)
|
|
905
|
-
fBm(t, octaves = 3) {
|
|
906
|
-
let value = 0;
|
|
907
|
-
let amplitude = 1.0;
|
|
908
|
-
let frequency = 1.0;
|
|
909
|
-
let maxValue = 0;
|
|
910
|
-
for (let j = 0; j < octaves; j++) {
|
|
911
|
-
value += Math.sin(t * frequency * Math.PI * 2 + (j * 12.34)) * amplitude;
|
|
912
|
-
maxValue += amplitude;
|
|
913
|
-
amplitude *= 0.5;
|
|
914
|
-
frequency *= 2.0;
|
|
915
|
-
}
|
|
916
|
-
return value / maxValue;
|
|
917
|
-
}
|
|
918
1007
|
/**
|
|
919
|
-
* Click at coordinates
|
|
1008
|
+
* Click at coordinates — fast path. Direct CDP dispatch, no Bezier/tremor/jitter.
|
|
920
1009
|
*/
|
|
921
|
-
async click(x, y, button = "left",
|
|
922
|
-
const
|
|
923
|
-
const
|
|
924
|
-
|
|
925
|
-
// Pre-click hover pause — humans don't instantly press after arriving
|
|
926
|
-
await new Promise((r) => setTimeout(r, 80 + Math.random() * 140));
|
|
927
|
-
// Micro-jitter at the moment of click (hand tremor)
|
|
928
|
-
const cx = targetX + Math.round((Math.random() - 0.5) * 3);
|
|
929
|
-
const cy = targetY + Math.round((Math.random() - 0.5) * 3);
|
|
1010
|
+
async click(x, y, button = "left", _targetWidth) {
|
|
1011
|
+
const rx = Math.round(Number(x));
|
|
1012
|
+
const ry = Math.round(Number(y));
|
|
1013
|
+
this.mousePosition = { x: rx, y: ry };
|
|
930
1014
|
await this.sendCommand("Input.dispatchMouseEvent", {
|
|
931
|
-
type: "mousePressed", x:
|
|
1015
|
+
type: "mousePressed", x: rx, y: ry, button, clickCount: 1, pointerType: "mouse",
|
|
932
1016
|
});
|
|
933
|
-
// Natural hold duration before release
|
|
934
|
-
await new Promise((r) => setTimeout(r, 60 + Math.random() * 110));
|
|
935
1017
|
await this.sendCommand("Input.dispatchMouseEvent", {
|
|
936
|
-
type: "mouseReleased", x:
|
|
1018
|
+
type: "mouseReleased", x: rx, y: ry, button, clickCount: 1, pointerType: "mouse",
|
|
937
1019
|
});
|
|
938
1020
|
}
|
|
939
1021
|
/**
|
|
940
|
-
* Move mouse
|
|
1022
|
+
* Move mouse to coordinates — fast path. Single CDP dispatch, no Bezier/tremor interpolation.
|
|
941
1023
|
*/
|
|
942
|
-
async moveMouse(x, y,
|
|
943
|
-
const
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
951
|
-
// Fitts's Law: MT = a + b * log2(2D / W)
|
|
952
|
-
const w = targetWidth ?? 30; // default target width to 30px
|
|
953
|
-
const indexDifficulty = Math.log2(Math.max(1, (2 * dist) / w));
|
|
954
|
-
const movementTime = 150 + 95 * indexDifficulty; // Fitts's MT in ms
|
|
955
|
-
// Human updates motor position every 10-15ms. Compute dynamic step count.
|
|
956
|
-
const steps = Math.max(12, Math.min(60, Math.round(movementTime / 12)));
|
|
957
|
-
// Random cubic Bezier control points — creates an organic arc
|
|
958
|
-
const angle = Math.atan2(targetY - start.y, targetX - start.x) + Math.PI / 2;
|
|
959
|
-
const spread = dist * (0.25 + Math.random() * 0.35);
|
|
960
|
-
const sign = Math.random() < 0.5 ? 1 : -1;
|
|
961
|
-
const cp1 = {
|
|
962
|
-
x: start.x + (targetX - start.x) * (0.1 + Math.random() * 0.2) + Math.cos(angle) * spread * sign * (0.3 + Math.random() * 0.7),
|
|
963
|
-
y: start.y + (targetY - start.y) * (0.1 + Math.random() * 0.2) + Math.sin(angle) * spread * sign * (0.3 + Math.random() * 0.7),
|
|
964
|
-
};
|
|
965
|
-
const cp2 = {
|
|
966
|
-
x: start.x + (targetX - start.x) * (0.7 + Math.random() * 0.2) + Math.cos(angle) * spread * sign * (0.05 + Math.random() * 0.35),
|
|
967
|
-
y: start.y + (targetY - start.y) * (0.7 + Math.random() * 0.2) + Math.sin(angle) * spread * sign * (0.05 + Math.random() * 0.35),
|
|
968
|
-
};
|
|
969
|
-
await this.updateMouseOverlay(start.x, start.y).catch(() => { });
|
|
970
|
-
// Unique noise seed for this movement path
|
|
971
|
-
const seedX = Math.random() * 100;
|
|
972
|
-
const seedY = Math.random() * 100;
|
|
973
|
-
for (let i = 1; i <= steps; i++) {
|
|
974
|
-
const t = i / steps;
|
|
975
|
-
// Ease-in-out: slow start, fast middle, slow near target
|
|
976
|
-
const e = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
|
977
|
-
const u = 1 - e;
|
|
978
|
-
const px = u * u * u * start.x + 3 * u * u * e * cp1.x + 3 * u * e * e * cp2.x + e * e * e * targetX;
|
|
979
|
-
const py = u * u * u * start.y + 3 * u * u * e * cp1.y + 3 * u * e * e * cp2.y + e * e * e * targetY;
|
|
980
|
-
// fractional Brownian motion noise walk (muscle tremor)
|
|
981
|
-
const tremorAmplitude = 1.2;
|
|
982
|
-
const noiseX = this.fBm(t * 8 + seedX, 3) * tremorAmplitude;
|
|
983
|
-
const noiseY = this.fBm(t * 8 + seedY, 3) * tremorAmplitude;
|
|
984
|
-
const cx = Math.round(px + noiseX);
|
|
985
|
-
const cy = Math.round(py + noiseY);
|
|
986
|
-
await this.sendCommand("Input.dispatchMouseEvent", {
|
|
987
|
-
type: "mouseMoved", x: cx, y: cy, button: "none", pointerType: "mouse",
|
|
988
|
-
});
|
|
989
|
-
await this.updateMouseOverlay(cx, cy).catch(() => { });
|
|
990
|
-
// Velocity profile delay: faster in the middle, slower at start/end
|
|
991
|
-
const velocityWeight = Math.sin(t * Math.PI); // bell curve 0 -> 1 -> 0
|
|
992
|
-
const stepDelay = 2 + (1 - velocityWeight) * 12 + Math.random() * 4;
|
|
993
|
-
await new Promise((r) => setTimeout(r, stepDelay));
|
|
994
|
-
}
|
|
995
|
-
this.mousePosition = { x: targetX, y: targetY };
|
|
1024
|
+
async moveMouse(x, y, _targetWidth) {
|
|
1025
|
+
const rx = Math.round(Number(x));
|
|
1026
|
+
const ry = Math.round(Number(y));
|
|
1027
|
+
this.mousePosition = { x: rx, y: ry };
|
|
1028
|
+
await this.sendCommand("Input.dispatchMouseEvent", {
|
|
1029
|
+
type: "mouseMoved", x: rx, y: ry, button: "none", pointerType: "mouse",
|
|
1030
|
+
});
|
|
996
1031
|
}
|
|
997
1032
|
getMousePosition() {
|
|
998
1033
|
return this.mousePosition ?? { x: 300, y: 300 };
|
|
999
1034
|
}
|
|
1000
|
-
async updateMouseOverlay(x, y) {
|
|
1001
|
-
await this.sendCommand("Runtime.evaluate", {
|
|
1002
|
-
expression: `
|
|
1003
|
-
(function() {
|
|
1004
|
-
const x = ${JSON.stringify(Math.round(x))};
|
|
1005
|
-
const y = ${JSON.stringify(Math.round(y))};
|
|
1006
|
-
let cursor = document.getElementById('__aether_mouse_cursor');
|
|
1007
|
-
if (!cursor) {
|
|
1008
|
-
cursor = document.createElement('div');
|
|
1009
|
-
cursor.id = '__aether_mouse_cursor';
|
|
1010
|
-
cursor.setAttribute('aria-hidden', 'true');
|
|
1011
|
-
cursor.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" style="display:block"><path d="M20.5056 10.7754C21.1225 10.5355 21.431 10.4155 21.5176 10.2459C21.5926 10.099 21.5903 9.92446 21.5115 9.77954C21.4205 9.61226 21.109 9.50044 20.486 9.2768L4.59629 3.5728C4.0866 3.38983 3.83175 3.29835 3.66514 3.35605C3.52029 3.40621 3.40645 3.52004 3.35629 3.6649C3.29859 3.8315 3.39008 4.08635 3.57304 4.59605L9.277 20.4858C9.50064 21.1088 9.61246 21.4203 9.77973 21.5113C9.92465 21.5901 10.0991 21.5924 10.2461 21.5174C10.4157 21.4308 10.5356 21.1223 10.7756 20.5054L13.3724 13.8278C13.4194 13.707 13.4429 13.6466 13.4792 13.5957C13.5114 13.5506 13.5508 13.5112 13.5959 13.479C13.6468 13.4427 13.7072 13.4192 13.828 13.3722L20.5056 10.7754Z" stroke="#111111" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M20.5056 10.7754C21.1225 10.5355 21.431 10.4155 21.5176 10.2459C21.5926 10.099 21.5903 9.92446 21.5115 9.77954C21.4205 9.61226 21.109 9.50044 20.486 9.2768L4.59629 3.5728C4.0866 3.38983 3.83175 3.29835 3.66514 3.35605C3.52029 3.40621 3.40645 3.52004 3.35629 3.6649C3.29859 3.8315 3.39008 4.08635 3.57304 4.59605L9.277 20.4858C9.50064 21.1088 9.61246 21.4203 9.77973 21.5113C9.92465 21.5901 10.0991 21.5924 10.2461 21.5174C10.4157 21.4308 10.5356 21.1223 10.7756 20.5054L13.3724 13.8278C13.4194 13.707 13.4429 13.6466 13.4792 13.5957C13.5114 13.5506 13.5508 13.5112 13.5959 13.479C13.6468 13.4427 13.7072 13.4192 13.828 13.3722L20.5056 10.7754Z" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
1012
|
-
cursor.style.cssText = [
|
|
1013
|
-
'position: fixed',
|
|
1014
|
-
'left: 0',
|
|
1015
|
-
'top: 0',
|
|
1016
|
-
'transform: translate(-3px, -3px)',
|
|
1017
|
-
'transition: left 70ms linear, top 70ms linear, opacity 120ms ease',
|
|
1018
|
-
'z-index: 2147483647',
|
|
1019
|
-
'pointer-events: none',
|
|
1020
|
-
'opacity: 1'
|
|
1021
|
-
].join(';');
|
|
1022
|
-
document.documentElement.appendChild(cursor);
|
|
1023
|
-
}
|
|
1024
|
-
cursor.style.left = x + 'px';
|
|
1025
|
-
cursor.style.top = y + 'px';
|
|
1026
|
-
cursor.style.opacity = '1';
|
|
1027
|
-
clearTimeout(window.__aetherMouseCursorTimer);
|
|
1028
|
-
window.__aetherMouseCursorTimer = setTimeout(() => {
|
|
1029
|
-
const current = document.getElementById('__aether_mouse_cursor');
|
|
1030
|
-
if (current) current.style.opacity = '0.55';
|
|
1031
|
-
}, 900);
|
|
1032
|
-
return true;
|
|
1033
|
-
})()
|
|
1034
|
-
`,
|
|
1035
|
-
returnByValue: true,
|
|
1036
|
-
});
|
|
1037
|
-
}
|
|
1038
|
-
async showScrollIndicator(x, y, deltaY) {
|
|
1039
|
-
const isDown = deltaY > 0;
|
|
1040
|
-
const chevron = (dy, opacity) => {
|
|
1041
|
-
const d = isDown
|
|
1042
|
-
? `M10,${dy} L16,${dy + 7} L22,${dy}`
|
|
1043
|
-
: `M10,${dy + 7} L16,${dy} L22,${dy + 7}`;
|
|
1044
|
-
return `<path d="${d}" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none" opacity="${opacity}"/>`;
|
|
1045
|
-
};
|
|
1046
|
-
const chevrons = isDown
|
|
1047
|
-
? chevron(4, 0.3) + chevron(15, 0.65) + chevron(26, 1)
|
|
1048
|
-
: chevron(26, 0.3) + chevron(15, 0.65) + chevron(4, 1);
|
|
1049
|
-
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="36" style="display:block">${chevrons}</svg>`;
|
|
1050
|
-
await this.sendCommand("Runtime.evaluate", {
|
|
1051
|
-
expression: `
|
|
1052
|
-
(function() {
|
|
1053
|
-
var ind = document.getElementById('__aether_scroll_ind');
|
|
1054
|
-
if (ind) ind.remove();
|
|
1055
|
-
ind = document.createElement('div');
|
|
1056
|
-
ind.id = '__aether_scroll_ind';
|
|
1057
|
-
ind.innerHTML = ${JSON.stringify(svg)};
|
|
1058
|
-
ind.style.cssText = [
|
|
1059
|
-
'position: fixed',
|
|
1060
|
-
'left: ${Math.round(x)}px',
|
|
1061
|
-
'top: ${Math.round(y)}px',
|
|
1062
|
-
'transform: translate(-50%, -50%)',
|
|
1063
|
-
'background: rgba(0,0,0,0.52)',
|
|
1064
|
-
'border-radius: 20px',
|
|
1065
|
-
'padding: 6px 8px',
|
|
1066
|
-
'z-index: 2147483647',
|
|
1067
|
-
'pointer-events: none',
|
|
1068
|
-
'opacity: 1',
|
|
1069
|
-
'transition: opacity 300ms ease'
|
|
1070
|
-
].join(';');
|
|
1071
|
-
document.documentElement.appendChild(ind);
|
|
1072
|
-
clearTimeout(window.__aetherScrollTimer);
|
|
1073
|
-
window.__aetherScrollTimer = setTimeout(function() {
|
|
1074
|
-
var cur = document.getElementById('__aether_scroll_ind');
|
|
1075
|
-
if (cur) {
|
|
1076
|
-
cur.style.opacity = '0';
|
|
1077
|
-
setTimeout(function() { if (cur.parentNode) cur.parentNode.removeChild(cur); }, 320);
|
|
1078
|
-
}
|
|
1079
|
-
}, 500);
|
|
1080
|
-
return true;
|
|
1081
|
-
})()
|
|
1082
|
-
`,
|
|
1083
|
-
returnByValue: true,
|
|
1084
|
-
}).catch(() => { });
|
|
1085
|
-
}
|
|
1086
1035
|
async moveMouseToSelector(selector) {
|
|
1087
|
-
|
|
1088
|
-
(
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
})()
|
|
1096
|
-
`);
|
|
1097
|
-
if (!result)
|
|
1036
|
+
try {
|
|
1037
|
+
const center = await this.getElementCenter(selector);
|
|
1038
|
+
if (!center)
|
|
1039
|
+
return false;
|
|
1040
|
+
await this.moveMouse(center.x, center.y);
|
|
1041
|
+
return true;
|
|
1042
|
+
}
|
|
1043
|
+
catch {
|
|
1098
1044
|
return false;
|
|
1099
|
-
|
|
1100
|
-
return true;
|
|
1045
|
+
}
|
|
1101
1046
|
}
|
|
1102
1047
|
/**
|
|
1103
|
-
* Scroll from the current or supplied pointer location
|
|
1048
|
+
* Scroll from the current or supplied pointer location — fast path. Single wheel event.
|
|
1104
1049
|
*/
|
|
1105
1050
|
async wheel(deltaX, deltaY, x, y) {
|
|
1106
|
-
const
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
if (dominant !== 0)
|
|
1113
|
-
await this.showScrollIndicator(origin.x, origin.y, dominant);
|
|
1114
|
-
// Break scroll into irregular chunks — humans don't scroll at perfectly uniform speed
|
|
1115
|
-
const totalY = Number(deltaY);
|
|
1116
|
-
const totalX = Number(deltaX);
|
|
1117
|
-
const totalAbs = Math.max(Math.abs(totalX), Math.abs(totalY));
|
|
1118
|
-
const steps = Math.max(1, Math.ceil(totalAbs / (300 + Math.random() * 400)));
|
|
1119
|
-
let sentY = 0;
|
|
1120
|
-
let sentX = 0;
|
|
1121
|
-
for (let step = 0; step < steps; step++) {
|
|
1122
|
-
const last = step === steps - 1;
|
|
1123
|
-
// Random chunk size with slight ease-in (start slow, then momentum)
|
|
1124
|
-
const fraction = last ? 1 : (0.5 + Math.random() * 0.5) / (steps - step);
|
|
1125
|
-
const chunkY = last ? totalY - sentY : Math.round(totalY * fraction);
|
|
1126
|
-
const chunkX = last ? totalX - sentX : Math.round(totalX * fraction);
|
|
1127
|
-
sentY += chunkY;
|
|
1128
|
-
sentX += chunkX;
|
|
1129
|
-
await this.sendCommand("Input.dispatchMouseWheel", {
|
|
1130
|
-
x: origin.x, y: origin.y, deltaX: chunkX, deltaY: chunkY,
|
|
1131
|
-
});
|
|
1132
|
-
if (!last) {
|
|
1133
|
-
await new Promise((r) => setTimeout(r, 40 + Math.random() * 80));
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1051
|
+
const ox = Math.round(Number(x ?? this.mousePosition?.x ?? 0));
|
|
1052
|
+
const oy = Math.round(Number(y ?? this.mousePosition?.y ?? 0));
|
|
1053
|
+
this.mousePosition = { x: ox, y: oy };
|
|
1054
|
+
await this.sendCommand("Input.dispatchMouseWheel", {
|
|
1055
|
+
x: ox, y: oy, deltaX: Math.round(Number(deltaX)), deltaY: Math.round(Number(deltaY)),
|
|
1056
|
+
});
|
|
1136
1057
|
}
|
|
1137
1058
|
async typeText(text) {
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
'c': 'xdfv',
|
|
1142
|
-
'd': 'ersfxc',
|
|
1143
|
-
'e': 'wsdr',
|
|
1144
|
-
'f': 'rtgvcd',
|
|
1145
|
-
'g': 'tyhbvf',
|
|
1146
|
-
'h': 'yujnbg',
|
|
1147
|
-
'i': 'ujko',
|
|
1148
|
-
'j': 'uikmnh',
|
|
1149
|
-
'k': 'ijlm',
|
|
1150
|
-
'l': 'okp',
|
|
1151
|
-
'm': 'njk',
|
|
1152
|
-
'n': 'bhjm',
|
|
1153
|
-
'o': 'iklp',
|
|
1154
|
-
'p': 'ol',
|
|
1155
|
-
'q': 'wa',
|
|
1156
|
-
'r': 'edft',
|
|
1157
|
-
's': 'wedxza',
|
|
1158
|
-
't': 'rfgy',
|
|
1159
|
-
'u': 'yhji',
|
|
1160
|
-
'v': 'cfgb',
|
|
1161
|
-
'w': 'qase',
|
|
1162
|
-
'x': 'zsdc',
|
|
1163
|
-
'y': 'tghu',
|
|
1164
|
-
'z': 'asx',
|
|
1165
|
-
};
|
|
1166
|
-
for (let i = 0; i < text.length; i++) {
|
|
1167
|
-
const ch = text[i];
|
|
1168
|
-
const lowerCh = ch.toLowerCase();
|
|
1169
|
-
// ~1.5% chance of typo on lowercase QWERTY letters
|
|
1170
|
-
if (ADJACENT_KEYS[lowerCh] && Math.random() < 0.015) {
|
|
1171
|
-
const adjList = ADJACENT_KEYS[lowerCh];
|
|
1172
|
-
const typoCh = adjList[Math.floor(Math.random() * adjList.length)];
|
|
1173
|
-
const resolvedTypo = ch === ch.toUpperCase() ? typoCh.toUpperCase() : typoCh;
|
|
1174
|
-
// Type the typo first
|
|
1175
|
-
await this.sendCommand("Input.insertText", { text: resolvedTypo });
|
|
1176
|
-
// Natural pause for reaction time before correcting
|
|
1177
|
-
await new Promise((r) => setTimeout(r, 120 + Math.random() * 150));
|
|
1178
|
-
// Delete typo
|
|
1179
|
-
await this.pressKey("Backspace");
|
|
1180
|
-
// Short typing recovery pause
|
|
1181
|
-
await new Promise((r) => setTimeout(r, 80 + Math.random() * 100));
|
|
1182
|
-
}
|
|
1183
|
-
await this.sendCommand("Input.insertText", { text: ch });
|
|
1184
|
-
// Base inter-key delay (~55-90 WPM range)
|
|
1185
|
-
let delay = 35 + Math.random() * 75;
|
|
1186
|
-
// Longer pause after spaces (word boundary) and punctuation
|
|
1187
|
-
if (ch === " ")
|
|
1188
|
-
delay += 15 + Math.random() * 55;
|
|
1189
|
-
if (/[.,!?;:\n]/.test(ch))
|
|
1190
|
-
delay += 60 + Math.random() * 110;
|
|
1191
|
-
// ~3% chance of a "thinking" pause mid-sentence
|
|
1192
|
-
if (Math.random() < 0.03)
|
|
1193
|
-
delay += 250 + Math.random() * 600;
|
|
1194
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
1059
|
+
// Fast path — insert entire text in one CDP call, no per-character delays or typo simulation.
|
|
1060
|
+
if (text) {
|
|
1061
|
+
await this.sendCommand("Input.insertText", { text });
|
|
1195
1062
|
}
|
|
1196
1063
|
}
|
|
1197
1064
|
async pressKey(key, modifiers = []) {
|