aether-mcp-server 2.0.2 → 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 +598 -1981
- package/dist/cdp-client.js +232 -366
- package/dist/element-collector.js +198 -0
- package/dist/eval-scripts.js +1024 -0
- package/dist/index.js +16 -28
- package/dist/locator-engine.js +21 -259
- package/dist/logger.js +105 -0
- package/dist/mcp-server.js +59 -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
|
@@ -13,6 +13,8 @@ const fs_1 = require("fs");
|
|
|
13
13
|
const path_1 = __importDefault(require("path"));
|
|
14
14
|
const os_1 = __importDefault(require("os"));
|
|
15
15
|
const stealth_1 = require("./stealth");
|
|
16
|
+
const element_collector_1 = require("./element-collector");
|
|
17
|
+
const eval_scripts_1 = require("./eval-scripts");
|
|
16
18
|
class CdpClient {
|
|
17
19
|
ws = null;
|
|
18
20
|
messageId = 0;
|
|
@@ -31,7 +33,97 @@ class CdpClient {
|
|
|
31
33
|
mousePosition = null;
|
|
32
34
|
networkLoggingAttached = false;
|
|
33
35
|
diagnosticsLoggingAttached = false;
|
|
36
|
+
speedMultiplier = 1.0;
|
|
37
|
+
documentNodeId = null;
|
|
34
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
|
+
}
|
|
35
127
|
/**
|
|
36
128
|
* Connect to existing Chrome instance on given port
|
|
37
129
|
*/
|
|
@@ -91,25 +183,22 @@ class CdpClient {
|
|
|
91
183
|
this.ws.on("open", async () => {
|
|
92
184
|
this.connected = true;
|
|
93
185
|
this.activeTarget = target;
|
|
186
|
+
this.documentNodeId = null;
|
|
94
187
|
console.error(`[CDP] Connected to target: ${target.title} (${target.url})`);
|
|
95
188
|
try {
|
|
96
|
-
// Enable
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
]);
|
|
108
|
-
// Keep animations running. Pausing them can freeze SPA loaders and leave pages looking blank.
|
|
109
|
-
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(() => { });
|
|
110
200
|
this.attachNetworkLogging();
|
|
111
201
|
this.attachDiagnosticsLogging();
|
|
112
|
-
console.error("[CDP] Core CDP domains enabled");
|
|
113
202
|
}
|
|
114
203
|
catch (e) {
|
|
115
204
|
console.error("[CDP] Failed to enable core domains:", e);
|
|
@@ -175,6 +264,7 @@ class CdpClient {
|
|
|
175
264
|
* Navigate to URL
|
|
176
265
|
*/
|
|
177
266
|
async navigate(url) {
|
|
267
|
+
this.documentNodeId = null;
|
|
178
268
|
await this.sendCommand("Page.navigate", { url });
|
|
179
269
|
}
|
|
180
270
|
async navigateAndWait(url, timeout = 10000) {
|
|
@@ -208,7 +298,8 @@ class CdpClient {
|
|
|
208
298
|
expression: `
|
|
209
299
|
(function() {
|
|
210
300
|
const withSoM = ${JSON.stringify(withSoM)};
|
|
211
|
-
|
|
301
|
+
${element_collector_1.SHARED_DOM_HELPERS}
|
|
302
|
+
|
|
212
303
|
// Remove existing overlays
|
|
213
304
|
const oldContainer = document.getElementById('aether-som-container');
|
|
214
305
|
if (oldContainer) oldContainer.remove();
|
|
@@ -245,11 +336,8 @@ class CdpClient {
|
|
|
245
336
|
let text = el.innerText || el.textContent || '';
|
|
246
337
|
text = text.trim().substring(0, 100);
|
|
247
338
|
|
|
248
|
-
// Get selector
|
|
249
|
-
|
|
250
|
-
if (el.id) selector = '#' + CSS.escape(el.id);
|
|
251
|
-
else if (el.className && typeof el.className === 'string') selector = '.' + el.className.split(' ')[0];
|
|
252
|
-
else selector = el.tagName.toLowerCase();
|
|
339
|
+
// Get a stable selector (shared with the locator engine)
|
|
340
|
+
const selector = aetherStableSelector(el);
|
|
253
341
|
|
|
254
342
|
if (withSoM && container) {
|
|
255
343
|
const id = String(validIndex);
|
|
@@ -285,7 +373,7 @@ class CdpClient {
|
|
|
285
373
|
attributes: {
|
|
286
374
|
type: el.getAttribute('type') || '',
|
|
287
375
|
href: el.getAttribute('href') || '',
|
|
288
|
-
role: el
|
|
376
|
+
role: aetherImplicitRole(el),
|
|
289
377
|
'aria-label': el.getAttribute('aria-label') || ''
|
|
290
378
|
}
|
|
291
379
|
};
|
|
@@ -313,50 +401,28 @@ class CdpClient {
|
|
|
313
401
|
});
|
|
314
402
|
}
|
|
315
403
|
/**
|
|
316
|
-
* Wait for a selector to appear in DOM
|
|
404
|
+
* Wait for a selector to appear in DOM — fast CDP polling.
|
|
317
405
|
*/
|
|
318
406
|
async waitForSelector(selector, timeout = 10000, options = {}) {
|
|
319
407
|
const startTime = Date.now();
|
|
320
408
|
let lastBox = null;
|
|
321
409
|
let stableSince = 0;
|
|
322
410
|
while (Date.now() - startTime < timeout) {
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
box: {
|
|
339
|
-
x: Math.round(rect.left),
|
|
340
|
-
y: Math.round(rect.top),
|
|
341
|
-
width: Math.round(rect.width),
|
|
342
|
-
height: Math.round(rect.height)
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
})()
|
|
346
|
-
`,
|
|
347
|
-
returnByValue: true,
|
|
348
|
-
});
|
|
349
|
-
const state = result.result?.value;
|
|
350
|
-
if (state?.found && (!options.visible || state.visible)) {
|
|
351
|
-
if (!options.stable)
|
|
352
|
-
return true;
|
|
353
|
-
const box = state.box;
|
|
354
|
-
const sameBox = lastBox &&
|
|
355
|
-
lastBox.x === box.x &&
|
|
356
|
-
lastBox.y === box.y &&
|
|
357
|
-
lastBox.width === box.width &&
|
|
358
|
-
lastBox.height === box.height;
|
|
359
|
-
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) {
|
|
360
426
|
if (!stableSince)
|
|
361
427
|
stableSince = Date.now();
|
|
362
428
|
if (Date.now() - stableSince >= 120)
|
|
@@ -364,10 +430,13 @@ class CdpClient {
|
|
|
364
430
|
}
|
|
365
431
|
else {
|
|
366
432
|
stableSince = 0;
|
|
367
|
-
lastBox =
|
|
433
|
+
lastBox = boxKey;
|
|
368
434
|
}
|
|
369
435
|
}
|
|
370
|
-
|
|
436
|
+
else {
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
await new Promise(r => setTimeout(r, 30));
|
|
371
440
|
}
|
|
372
441
|
return false;
|
|
373
442
|
}
|
|
@@ -416,7 +485,7 @@ class CdpClient {
|
|
|
416
485
|
this.removeEventListener("Network.responseReceived", resetIdle);
|
|
417
486
|
resolve(); // Resolve anyway after timeout
|
|
418
487
|
}, timeout);
|
|
419
|
-
idleCheckInterval = setInterval(checkIdle, 100);
|
|
488
|
+
idleCheckInterval = setInterval(checkIdle, Math.max(1, 100 * this.speedMultiplier));
|
|
420
489
|
this.on("Network.requestWillBeSent", resetIdle);
|
|
421
490
|
this.on("Network.responseReceived", resetIdle);
|
|
422
491
|
});
|
|
@@ -521,51 +590,84 @@ class CdpClient {
|
|
|
521
590
|
if (!target) {
|
|
522
591
|
throw new Error(`Target not found: ${targetId}`);
|
|
523
592
|
}
|
|
593
|
+
this.documentNodeId = null;
|
|
524
594
|
await this.attachToTarget(target);
|
|
525
595
|
}
|
|
526
596
|
/**
|
|
527
|
-
* 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.
|
|
528
599
|
*/
|
|
529
|
-
async
|
|
600
|
+
async getRichAXTree() {
|
|
530
601
|
await this.sendCommand("Accessibility.enable");
|
|
531
|
-
const result = await this.sendCommand("Accessibility.getFullAXTree");
|
|
602
|
+
const result = await this.sendCommand("Accessibility.getFullAXTree", {});
|
|
532
603
|
if (!result || !result.nodes)
|
|
533
|
-
return
|
|
604
|
+
return { tree: null, nodeCount: 0 };
|
|
534
605
|
const nodes = result.nodes;
|
|
535
|
-
const interactiveNodes = [];
|
|
536
|
-
// Map of node IDs for fast lookup
|
|
537
606
|
const nodeMap = new Map();
|
|
538
|
-
|
|
539
|
-
// Helper to get name from node properties
|
|
540
|
-
const getNodeName = (node) => {
|
|
541
|
-
if (node.name?.value)
|
|
542
|
-
return node.name.value;
|
|
543
|
-
const nameProp = node.properties?.find((p) => p.name === "name");
|
|
544
|
-
return nameProp?.value?.value || "";
|
|
545
|
-
};
|
|
546
|
-
// Filter and simplify nodes
|
|
607
|
+
// First pass: build lookup
|
|
547
608
|
nodes.forEach((node) => {
|
|
548
|
-
const
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
].includes(role);
|
|
555
|
-
const hasAction = node.properties?.some((p) => ["pressed", "expanded", "selected", "focused"].includes(p.name));
|
|
556
|
-
if (isInteractive || (name && name.length > 0 && role !== "generic" && role !== "none")) {
|
|
557
|
-
interactiveNodes.push({
|
|
558
|
-
id: node.nodeId,
|
|
559
|
-
role: role,
|
|
560
|
-
name: name,
|
|
561
|
-
description: node.description?.value || "",
|
|
562
|
-
value: node.value?.value || "",
|
|
563
|
-
disabled: node.properties?.find((p) => p.name === "disabled")?.value?.value || false,
|
|
564
|
-
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;
|
|
565
615
|
});
|
|
566
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
|
+
});
|
|
567
632
|
});
|
|
568
|
-
|
|
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
|
+
}
|
|
645
|
+
});
|
|
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;
|
|
569
671
|
}
|
|
570
672
|
async getNetworkTraffic() {
|
|
571
673
|
return this.networkTraffic;
|
|
@@ -902,297 +1004,61 @@ class CdpClient {
|
|
|
902
1004
|
async takeHeapSnapshot(reportProgress, treatGlobalObjectsAsRoots, captureNumericValue) {
|
|
903
1005
|
await this.sendCommand("HeapProfiler.takeHeapSnapshot", { reportProgress, treatGlobalObjectsAsRoots, captureNumericValue });
|
|
904
1006
|
}
|
|
905
|
-
// Simple multi-octave noise function (fractional Brownian motion approximation)
|
|
906
|
-
fBm(t, octaves = 3) {
|
|
907
|
-
let value = 0;
|
|
908
|
-
let amplitude = 1.0;
|
|
909
|
-
let frequency = 1.0;
|
|
910
|
-
let maxValue = 0;
|
|
911
|
-
for (let j = 0; j < octaves; j++) {
|
|
912
|
-
value += Math.sin(t * frequency * Math.PI * 2 + (j * 12.34)) * amplitude;
|
|
913
|
-
maxValue += amplitude;
|
|
914
|
-
amplitude *= 0.5;
|
|
915
|
-
frequency *= 2.0;
|
|
916
|
-
}
|
|
917
|
-
return value / maxValue;
|
|
918
|
-
}
|
|
919
1007
|
/**
|
|
920
|
-
* Click at coordinates
|
|
1008
|
+
* Click at coordinates — fast path. Direct CDP dispatch, no Bezier/tremor/jitter.
|
|
921
1009
|
*/
|
|
922
|
-
async click(x, y, button = "left",
|
|
923
|
-
const
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
// Pre-click hover pause — humans don't instantly press after arriving
|
|
927
|
-
await new Promise((r) => setTimeout(r, 80 + Math.random() * 140));
|
|
928
|
-
// Micro-jitter at the moment of click (hand tremor)
|
|
929
|
-
const cx = targetX + Math.round((Math.random() - 0.5) * 3);
|
|
930
|
-
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 };
|
|
931
1014
|
await this.sendCommand("Input.dispatchMouseEvent", {
|
|
932
|
-
type: "mousePressed", x:
|
|
1015
|
+
type: "mousePressed", x: rx, y: ry, button, clickCount: 1, pointerType: "mouse",
|
|
933
1016
|
});
|
|
934
|
-
// Natural hold duration before release
|
|
935
|
-
await new Promise((r) => setTimeout(r, 60 + Math.random() * 110));
|
|
936
1017
|
await this.sendCommand("Input.dispatchMouseEvent", {
|
|
937
|
-
type: "mouseReleased", x:
|
|
1018
|
+
type: "mouseReleased", x: rx, y: ry, button, clickCount: 1, pointerType: "mouse",
|
|
938
1019
|
});
|
|
939
1020
|
}
|
|
940
1021
|
/**
|
|
941
|
-
* Move mouse
|
|
1022
|
+
* Move mouse to coordinates — fast path. Single CDP dispatch, no Bezier/tremor interpolation.
|
|
942
1023
|
*/
|
|
943
|
-
async moveMouse(x, y,
|
|
944
|
-
const
|
|
945
|
-
const
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
return;
|
|
951
|
-
}
|
|
952
|
-
// Fitts's Law: MT = a + b * log2(2D / W)
|
|
953
|
-
const w = targetWidth ?? 30; // default target width to 30px
|
|
954
|
-
const indexDifficulty = Math.log2(Math.max(1, (2 * dist) / w));
|
|
955
|
-
const movementTime = 150 + 95 * indexDifficulty; // Fitts's MT in ms
|
|
956
|
-
// Human updates motor position every 10-15ms. Compute dynamic step count.
|
|
957
|
-
const steps = Math.max(12, Math.min(60, Math.round(movementTime / 12)));
|
|
958
|
-
// Random cubic Bezier control points — creates an organic arc
|
|
959
|
-
const angle = Math.atan2(targetY - start.y, targetX - start.x) + Math.PI / 2;
|
|
960
|
-
const spread = dist * (0.25 + Math.random() * 0.35);
|
|
961
|
-
const sign = Math.random() < 0.5 ? 1 : -1;
|
|
962
|
-
const cp1 = {
|
|
963
|
-
x: start.x + (targetX - start.x) * (0.1 + Math.random() * 0.2) + Math.cos(angle) * spread * sign * (0.3 + Math.random() * 0.7),
|
|
964
|
-
y: start.y + (targetY - start.y) * (0.1 + Math.random() * 0.2) + Math.sin(angle) * spread * sign * (0.3 + Math.random() * 0.7),
|
|
965
|
-
};
|
|
966
|
-
const cp2 = {
|
|
967
|
-
x: start.x + (targetX - start.x) * (0.7 + Math.random() * 0.2) + Math.cos(angle) * spread * sign * (0.05 + Math.random() * 0.35),
|
|
968
|
-
y: start.y + (targetY - start.y) * (0.7 + Math.random() * 0.2) + Math.sin(angle) * spread * sign * (0.05 + Math.random() * 0.35),
|
|
969
|
-
};
|
|
970
|
-
await this.updateMouseOverlay(start.x, start.y).catch(() => { });
|
|
971
|
-
// Unique noise seed for this movement path
|
|
972
|
-
const seedX = Math.random() * 100;
|
|
973
|
-
const seedY = Math.random() * 100;
|
|
974
|
-
for (let i = 1; i <= steps; i++) {
|
|
975
|
-
const t = i / steps;
|
|
976
|
-
// Ease-in-out: slow start, fast middle, slow near target
|
|
977
|
-
const e = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
|
978
|
-
const u = 1 - e;
|
|
979
|
-
const px = u * u * u * start.x + 3 * u * u * e * cp1.x + 3 * u * e * e * cp2.x + e * e * e * targetX;
|
|
980
|
-
const py = u * u * u * start.y + 3 * u * u * e * cp1.y + 3 * u * e * e * cp2.y + e * e * e * targetY;
|
|
981
|
-
// fractional Brownian motion noise walk (muscle tremor)
|
|
982
|
-
const tremorAmplitude = 1.2;
|
|
983
|
-
const noiseX = this.fBm(t * 8 + seedX, 3) * tremorAmplitude;
|
|
984
|
-
const noiseY = this.fBm(t * 8 + seedY, 3) * tremorAmplitude;
|
|
985
|
-
const cx = Math.round(px + noiseX);
|
|
986
|
-
const cy = Math.round(py + noiseY);
|
|
987
|
-
await this.sendCommand("Input.dispatchMouseEvent", {
|
|
988
|
-
type: "mouseMoved", x: cx, y: cy, button: "none", pointerType: "mouse",
|
|
989
|
-
});
|
|
990
|
-
await this.updateMouseOverlay(cx, cy).catch(() => { });
|
|
991
|
-
// Velocity profile delay: faster in the middle, slower at start/end
|
|
992
|
-
const velocityWeight = Math.sin(t * Math.PI); // bell curve 0 -> 1 -> 0
|
|
993
|
-
const stepDelay = 2 + (1 - velocityWeight) * 12 + Math.random() * 4;
|
|
994
|
-
await new Promise((r) => setTimeout(r, stepDelay));
|
|
995
|
-
}
|
|
996
|
-
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
|
+
});
|
|
997
1031
|
}
|
|
998
1032
|
getMousePosition() {
|
|
999
1033
|
return this.mousePosition ?? { x: 300, y: 300 };
|
|
1000
1034
|
}
|
|
1001
|
-
async updateMouseOverlay(x, y) {
|
|
1002
|
-
await this.sendCommand("Runtime.evaluate", {
|
|
1003
|
-
expression: `
|
|
1004
|
-
(function() {
|
|
1005
|
-
const x = ${JSON.stringify(Math.round(x))};
|
|
1006
|
-
const y = ${JSON.stringify(Math.round(y))};
|
|
1007
|
-
let cursor = document.getElementById('__aether_mouse_cursor');
|
|
1008
|
-
if (!cursor) {
|
|
1009
|
-
cursor = document.createElement('div');
|
|
1010
|
-
cursor.id = '__aether_mouse_cursor';
|
|
1011
|
-
cursor.setAttribute('aria-hidden', 'true');
|
|
1012
|
-
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>';
|
|
1013
|
-
cursor.style.cssText = [
|
|
1014
|
-
'position: fixed',
|
|
1015
|
-
'left: 0',
|
|
1016
|
-
'top: 0',
|
|
1017
|
-
'transform: translate(-3px, -3px)',
|
|
1018
|
-
'transition: left 70ms linear, top 70ms linear, opacity 120ms ease',
|
|
1019
|
-
'z-index: 2147483647',
|
|
1020
|
-
'pointer-events: none',
|
|
1021
|
-
'opacity: 1'
|
|
1022
|
-
].join(';');
|
|
1023
|
-
document.documentElement.appendChild(cursor);
|
|
1024
|
-
}
|
|
1025
|
-
cursor.style.left = x + 'px';
|
|
1026
|
-
cursor.style.top = y + 'px';
|
|
1027
|
-
cursor.style.opacity = '1';
|
|
1028
|
-
clearTimeout(window.__aetherMouseCursorTimer);
|
|
1029
|
-
window.__aetherMouseCursorTimer = setTimeout(() => {
|
|
1030
|
-
const current = document.getElementById('__aether_mouse_cursor');
|
|
1031
|
-
if (current) current.style.opacity = '0.55';
|
|
1032
|
-
}, 900);
|
|
1033
|
-
return true;
|
|
1034
|
-
})()
|
|
1035
|
-
`,
|
|
1036
|
-
returnByValue: true,
|
|
1037
|
-
});
|
|
1038
|
-
}
|
|
1039
|
-
async showScrollIndicator(x, y, deltaY) {
|
|
1040
|
-
const isDown = deltaY > 0;
|
|
1041
|
-
const chevron = (dy, opacity) => {
|
|
1042
|
-
const d = isDown
|
|
1043
|
-
? `M10,${dy} L16,${dy + 7} L22,${dy}`
|
|
1044
|
-
: `M10,${dy + 7} L16,${dy} L22,${dy + 7}`;
|
|
1045
|
-
return `<path d="${d}" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none" opacity="${opacity}"/>`;
|
|
1046
|
-
};
|
|
1047
|
-
const chevrons = isDown
|
|
1048
|
-
? chevron(4, 0.3) + chevron(15, 0.65) + chevron(26, 1)
|
|
1049
|
-
: chevron(26, 0.3) + chevron(15, 0.65) + chevron(4, 1);
|
|
1050
|
-
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="36" style="display:block">${chevrons}</svg>`;
|
|
1051
|
-
await this.sendCommand("Runtime.evaluate", {
|
|
1052
|
-
expression: `
|
|
1053
|
-
(function() {
|
|
1054
|
-
var ind = document.getElementById('__aether_scroll_ind');
|
|
1055
|
-
if (ind) ind.remove();
|
|
1056
|
-
ind = document.createElement('div');
|
|
1057
|
-
ind.id = '__aether_scroll_ind';
|
|
1058
|
-
ind.innerHTML = ${JSON.stringify(svg)};
|
|
1059
|
-
ind.style.cssText = [
|
|
1060
|
-
'position: fixed',
|
|
1061
|
-
'left: ${Math.round(x)}px',
|
|
1062
|
-
'top: ${Math.round(y)}px',
|
|
1063
|
-
'transform: translate(-50%, -50%)',
|
|
1064
|
-
'background: rgba(0,0,0,0.52)',
|
|
1065
|
-
'border-radius: 20px',
|
|
1066
|
-
'padding: 6px 8px',
|
|
1067
|
-
'z-index: 2147483647',
|
|
1068
|
-
'pointer-events: none',
|
|
1069
|
-
'opacity: 1',
|
|
1070
|
-
'transition: opacity 300ms ease'
|
|
1071
|
-
].join(';');
|
|
1072
|
-
document.documentElement.appendChild(ind);
|
|
1073
|
-
clearTimeout(window.__aetherScrollTimer);
|
|
1074
|
-
window.__aetherScrollTimer = setTimeout(function() {
|
|
1075
|
-
var cur = document.getElementById('__aether_scroll_ind');
|
|
1076
|
-
if (cur) {
|
|
1077
|
-
cur.style.opacity = '0';
|
|
1078
|
-
setTimeout(function() { if (cur.parentNode) cur.parentNode.removeChild(cur); }, 320);
|
|
1079
|
-
}
|
|
1080
|
-
}, 500);
|
|
1081
|
-
return true;
|
|
1082
|
-
})()
|
|
1083
|
-
`,
|
|
1084
|
-
returnByValue: true,
|
|
1085
|
-
}).catch(() => { });
|
|
1086
|
-
}
|
|
1087
1035
|
async moveMouseToSelector(selector) {
|
|
1088
|
-
|
|
1089
|
-
(
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
})()
|
|
1097
|
-
`);
|
|
1098
|
-
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 {
|
|
1099
1044
|
return false;
|
|
1100
|
-
|
|
1101
|
-
return true;
|
|
1045
|
+
}
|
|
1102
1046
|
}
|
|
1103
1047
|
/**
|
|
1104
|
-
* Scroll from the current or supplied pointer location
|
|
1048
|
+
* Scroll from the current or supplied pointer location — fast path. Single wheel event.
|
|
1105
1049
|
*/
|
|
1106
1050
|
async wheel(deltaX, deltaY, x, y) {
|
|
1107
|
-
const
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
if (dominant !== 0)
|
|
1114
|
-
await this.showScrollIndicator(origin.x, origin.y, dominant);
|
|
1115
|
-
// Break scroll into irregular chunks — humans don't scroll at perfectly uniform speed
|
|
1116
|
-
const totalY = Number(deltaY);
|
|
1117
|
-
const totalX = Number(deltaX);
|
|
1118
|
-
const totalAbs = Math.max(Math.abs(totalX), Math.abs(totalY));
|
|
1119
|
-
const steps = Math.max(1, Math.ceil(totalAbs / (300 + Math.random() * 400)));
|
|
1120
|
-
let sentY = 0;
|
|
1121
|
-
let sentX = 0;
|
|
1122
|
-
for (let step = 0; step < steps; step++) {
|
|
1123
|
-
const last = step === steps - 1;
|
|
1124
|
-
// Random chunk size with slight ease-in (start slow, then momentum)
|
|
1125
|
-
const fraction = last ? 1 : (0.5 + Math.random() * 0.5) / (steps - step);
|
|
1126
|
-
const chunkY = last ? totalY - sentY : Math.round(totalY * fraction);
|
|
1127
|
-
const chunkX = last ? totalX - sentX : Math.round(totalX * fraction);
|
|
1128
|
-
sentY += chunkY;
|
|
1129
|
-
sentX += chunkX;
|
|
1130
|
-
await this.sendCommand("Input.dispatchMouseWheel", {
|
|
1131
|
-
x: origin.x, y: origin.y, deltaX: chunkX, deltaY: chunkY,
|
|
1132
|
-
});
|
|
1133
|
-
if (!last) {
|
|
1134
|
-
await new Promise((r) => setTimeout(r, 40 + Math.random() * 80));
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
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
|
+
});
|
|
1137
1057
|
}
|
|
1138
1058
|
async typeText(text) {
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
'c': 'xdfv',
|
|
1143
|
-
'd': 'ersfxc',
|
|
1144
|
-
'e': 'wsdr',
|
|
1145
|
-
'f': 'rtgvcd',
|
|
1146
|
-
'g': 'tyhbvf',
|
|
1147
|
-
'h': 'yujnbg',
|
|
1148
|
-
'i': 'ujko',
|
|
1149
|
-
'j': 'uikmnh',
|
|
1150
|
-
'k': 'ijlm',
|
|
1151
|
-
'l': 'okp',
|
|
1152
|
-
'm': 'njk',
|
|
1153
|
-
'n': 'bhjm',
|
|
1154
|
-
'o': 'iklp',
|
|
1155
|
-
'p': 'ol',
|
|
1156
|
-
'q': 'wa',
|
|
1157
|
-
'r': 'edft',
|
|
1158
|
-
's': 'wedxza',
|
|
1159
|
-
't': 'rfgy',
|
|
1160
|
-
'u': 'yhji',
|
|
1161
|
-
'v': 'cfgb',
|
|
1162
|
-
'w': 'qase',
|
|
1163
|
-
'x': 'zsdc',
|
|
1164
|
-
'y': 'tghu',
|
|
1165
|
-
'z': 'asx',
|
|
1166
|
-
};
|
|
1167
|
-
for (let i = 0; i < text.length; i++) {
|
|
1168
|
-
const ch = text[i];
|
|
1169
|
-
const lowerCh = ch.toLowerCase();
|
|
1170
|
-
// ~1.5% chance of typo on lowercase QWERTY letters
|
|
1171
|
-
if (ADJACENT_KEYS[lowerCh] && Math.random() < 0.015) {
|
|
1172
|
-
const adjList = ADJACENT_KEYS[lowerCh];
|
|
1173
|
-
const typoCh = adjList[Math.floor(Math.random() * adjList.length)];
|
|
1174
|
-
const resolvedTypo = ch === ch.toUpperCase() ? typoCh.toUpperCase() : typoCh;
|
|
1175
|
-
// Type the typo first
|
|
1176
|
-
await this.sendCommand("Input.insertText", { text: resolvedTypo });
|
|
1177
|
-
// Natural pause for reaction time before correcting
|
|
1178
|
-
await new Promise((r) => setTimeout(r, 120 + Math.random() * 150));
|
|
1179
|
-
// Delete typo
|
|
1180
|
-
await this.pressKey("Backspace");
|
|
1181
|
-
// Short typing recovery pause
|
|
1182
|
-
await new Promise((r) => setTimeout(r, 80 + Math.random() * 100));
|
|
1183
|
-
}
|
|
1184
|
-
await this.sendCommand("Input.insertText", { text: ch });
|
|
1185
|
-
// Base inter-key delay (~55-90 WPM range)
|
|
1186
|
-
let delay = 35 + Math.random() * 75;
|
|
1187
|
-
// Longer pause after spaces (word boundary) and punctuation
|
|
1188
|
-
if (ch === " ")
|
|
1189
|
-
delay += 15 + Math.random() * 55;
|
|
1190
|
-
if (/[.,!?;:\n]/.test(ch))
|
|
1191
|
-
delay += 60 + Math.random() * 110;
|
|
1192
|
-
// ~3% chance of a "thinking" pause mid-sentence
|
|
1193
|
-
if (Math.random() < 0.03)
|
|
1194
|
-
delay += 250 + Math.random() * 600;
|
|
1195
|
-
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 });
|
|
1196
1062
|
}
|
|
1197
1063
|
}
|
|
1198
1064
|
async pressKey(key, modifiers = []) {
|