aether-mcp-server 2.0.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/aether-memory-store.js +431 -0
- package/dist/agent.js +1046 -0
- package/dist/captcha-solver.js +303 -0
- package/dist/cdp-bridge.js +2317 -0
- package/dist/cdp-client.js +1602 -0
- package/dist/index.js +71 -0
- package/dist/locator-engine.js +326 -0
- package/dist/mcp-responses.js +20 -0
- package/dist/mcp-server.js +1236 -0
- package/dist/mcp-task-memory.js +36 -0
- package/dist/page-snapshot-cache.js +75 -0
- package/dist/policy-client.js +104 -0
- package/dist/stealth.js +137 -0
- package/dist/trace-recorder.js +46 -0
- package/dist/ws-server.js +134 -0
- package/package.json +35 -0
|
@@ -0,0 +1,1602 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CdpClient = void 0;
|
|
7
|
+
exports.getCdpClient = getCdpClient;
|
|
8
|
+
exports.ensureCdpConnected = ensureCdpConnected;
|
|
9
|
+
const ws_1 = __importDefault(require("ws"));
|
|
10
|
+
const http_1 = __importDefault(require("http"));
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const fs_1 = require("fs");
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const os_1 = __importDefault(require("os"));
|
|
15
|
+
const stealth_1 = require("./stealth");
|
|
16
|
+
class CdpClient {
|
|
17
|
+
ws = null;
|
|
18
|
+
messageId = 0;
|
|
19
|
+
pending = new Map();
|
|
20
|
+
chromeProcess = null;
|
|
21
|
+
targets = [];
|
|
22
|
+
activeTarget = null;
|
|
23
|
+
eventListeners = new Map();
|
|
24
|
+
connected = false;
|
|
25
|
+
reconnectTimer = null;
|
|
26
|
+
intentionalClose = false;
|
|
27
|
+
networkTraffic = [];
|
|
28
|
+
consoleLogs = [];
|
|
29
|
+
MAX_TRAFFIC_LOGS = 100;
|
|
30
|
+
MAX_CONSOLE_LOGS = 100;
|
|
31
|
+
mousePosition = null;
|
|
32
|
+
networkLoggingAttached = false;
|
|
33
|
+
diagnosticsLoggingAttached = false;
|
|
34
|
+
constructor() { }
|
|
35
|
+
/**
|
|
36
|
+
* Connect to existing Chrome instance on given port
|
|
37
|
+
*/
|
|
38
|
+
async connect(port = 9222) {
|
|
39
|
+
const targets = await this.listTargets(port);
|
|
40
|
+
if (targets.length === 0) {
|
|
41
|
+
throw new Error(`No targets found on port ${port}. Is Chrome running with --remote-debugging-port=${port}?`);
|
|
42
|
+
}
|
|
43
|
+
// Prefer first page target
|
|
44
|
+
const page = targets.find(t => t.type === "page") || targets[0];
|
|
45
|
+
await this.attachToTarget(page);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Launch a new Chrome instance with remote debugging
|
|
49
|
+
*/
|
|
50
|
+
async launch(options) {
|
|
51
|
+
// Delegate to launchAuto without specifying browser (will auto-detect)
|
|
52
|
+
await this.launchAuto(options);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* List available CDP targets (tabs/pages)
|
|
56
|
+
*/
|
|
57
|
+
async listTargets(port = 9222) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const req = http_1.default.get(`http://localhost:${port}/json`, (res) => {
|
|
60
|
+
let data = "";
|
|
61
|
+
res.on("data", (chunk) => (data += chunk));
|
|
62
|
+
res.on("end", () => {
|
|
63
|
+
try {
|
|
64
|
+
const targets = JSON.parse(data);
|
|
65
|
+
this.targets = targets;
|
|
66
|
+
resolve(targets);
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
reject(e);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
req.on("error", reject);
|
|
74
|
+
req.setTimeout(3000, () => {
|
|
75
|
+
req.destroy();
|
|
76
|
+
reject(new Error(`Cannot connect to Chrome on port ${port}`));
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Attach to a specific target/tab
|
|
82
|
+
*/
|
|
83
|
+
async attachToTarget(target) {
|
|
84
|
+
if (this.ws) {
|
|
85
|
+
this.intentionalClose = true;
|
|
86
|
+
this.ws.close();
|
|
87
|
+
this.ws = null;
|
|
88
|
+
}
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
this.ws = new ws_1.default(target.webSocketDebuggerUrl);
|
|
91
|
+
this.ws.on("open", async () => {
|
|
92
|
+
this.connected = true;
|
|
93
|
+
this.activeTarget = target;
|
|
94
|
+
console.error(`[CDP] Connected to target: ${target.title} (${target.url})`);
|
|
95
|
+
try {
|
|
96
|
+
// Enable core CDP domains
|
|
97
|
+
await Promise.all([
|
|
98
|
+
this.sendCommand("Page.enable"),
|
|
99
|
+
this.sendCommand("Network.enable"),
|
|
100
|
+
this.sendCommand("Runtime.enable"),
|
|
101
|
+
this.sendCommand("DOM.enable"),
|
|
102
|
+
this.sendCommand("Log.enable").catch(() => { }),
|
|
103
|
+
this.sendCommand("Animation.enable").catch(() => { }),
|
|
104
|
+
this.sendCommand("Page.addScriptToEvaluateOnNewDocument", {
|
|
105
|
+
source: stealth_1.STEALTH_SCRIPT
|
|
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(() => { });
|
|
110
|
+
this.attachNetworkLogging();
|
|
111
|
+
this.attachDiagnosticsLogging();
|
|
112
|
+
console.error("[CDP] Core CDP domains enabled");
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
console.error("[CDP] Failed to enable core domains:", e);
|
|
116
|
+
}
|
|
117
|
+
resolve();
|
|
118
|
+
});
|
|
119
|
+
this.ws.on("message", (data) => {
|
|
120
|
+
try {
|
|
121
|
+
const message = JSON.parse(data.toString());
|
|
122
|
+
if (message.id !== undefined && this.pending.has(message.id)) {
|
|
123
|
+
const { resolve, reject, timeout } = this.pending.get(message.id);
|
|
124
|
+
clearTimeout(timeout);
|
|
125
|
+
this.pending.delete(message.id);
|
|
126
|
+
if (message.error) {
|
|
127
|
+
reject(new Error(message.error.message || JSON.stringify(message.error)));
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
resolve(message.result);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else if (message.method) {
|
|
134
|
+
this.emitEvent(message.method, message.params);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
console.error("[CDP] Parse error:", e);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
this.ws.on("close", () => {
|
|
142
|
+
this.connected = false;
|
|
143
|
+
console.error("[CDP] Connection closed");
|
|
144
|
+
if (this.intentionalClose) {
|
|
145
|
+
this.intentionalClose = false;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
this.scheduleReconnect();
|
|
149
|
+
});
|
|
150
|
+
this.ws.on("error", (err) => {
|
|
151
|
+
console.error("[CDP] Error:", err.message);
|
|
152
|
+
if (!this.connected)
|
|
153
|
+
reject(err);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Send a CDP command
|
|
159
|
+
*/
|
|
160
|
+
async sendCommand(method, params = {}) {
|
|
161
|
+
if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
|
|
162
|
+
throw new Error("CDP not connected. Call connect() or launch() first.");
|
|
163
|
+
}
|
|
164
|
+
const id = ++this.messageId;
|
|
165
|
+
return new Promise((resolve, reject) => {
|
|
166
|
+
const timeout = setTimeout(() => {
|
|
167
|
+
this.pending.delete(id);
|
|
168
|
+
reject(new Error(`CDP command '${method}' timed out after 30s`));
|
|
169
|
+
}, 30000);
|
|
170
|
+
this.pending.set(id, { resolve, reject, timeout });
|
|
171
|
+
this.ws.send(JSON.stringify({ id, method, params }));
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Navigate to URL
|
|
176
|
+
*/
|
|
177
|
+
async navigate(url) {
|
|
178
|
+
await this.sendCommand("Page.navigate", { url });
|
|
179
|
+
}
|
|
180
|
+
async navigateAndWait(url, timeout = 10000) {
|
|
181
|
+
const navPromise = this.waitForNavigation(Math.min(timeout, 10000)).catch(() => undefined);
|
|
182
|
+
await this.navigate(url);
|
|
183
|
+
const completed = await Promise.race([
|
|
184
|
+
navPromise.then(() => true),
|
|
185
|
+
new Promise((resolve) => setTimeout(() => resolve(false), Math.min(timeout, 1500))),
|
|
186
|
+
]);
|
|
187
|
+
if (!completed) {
|
|
188
|
+
await this.waitForNetworkIdle(300, Math.min(timeout, 2500)).catch(() => { });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Take screenshot
|
|
193
|
+
*/
|
|
194
|
+
async screenshot(format = "jpeg", quality = 80) {
|
|
195
|
+
const result = await this.sendCommand("Page.captureScreenshot", {
|
|
196
|
+
format,
|
|
197
|
+
quality,
|
|
198
|
+
captureBeyondViewport: false,
|
|
199
|
+
});
|
|
200
|
+
return result.data; // base64 encoded
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Get interactive elements with Set-of-Marks (SoM) overlay
|
|
204
|
+
* Returns element map with IDs and injects visual markers
|
|
205
|
+
*/
|
|
206
|
+
async getInteractiveElements(withSoM = true) {
|
|
207
|
+
const result = await this.sendCommand("Runtime.evaluate", {
|
|
208
|
+
expression: `
|
|
209
|
+
(function() {
|
|
210
|
+
const withSoM = ${JSON.stringify(withSoM)};
|
|
211
|
+
|
|
212
|
+
// Remove existing overlays
|
|
213
|
+
const oldContainer = document.getElementById('aether-som-container');
|
|
214
|
+
if (oldContainer) oldContainer.remove();
|
|
215
|
+
document.querySelectorAll('.aether-som-marker').forEach(el => el.remove());
|
|
216
|
+
|
|
217
|
+
let container = null;
|
|
218
|
+
if (withSoM) {
|
|
219
|
+
container = document.createElement('div');
|
|
220
|
+
container.id = 'aether-som-container';
|
|
221
|
+
container.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 2147483647; pointer-events: none;';
|
|
222
|
+
document.documentElement.appendChild(container);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const selectors = [
|
|
226
|
+
'a[href]', 'button', 'input:not([type="hidden"])', 'select', 'textarea',
|
|
227
|
+
'[onclick]', '[role="button"]', '[role="link"]', '[role="checkbox"]',
|
|
228
|
+
'[tabindex]:not([tabindex="-1"])', 'label', 'summary'
|
|
229
|
+
].join(', ');
|
|
230
|
+
|
|
231
|
+
const elements = Array.from(document.querySelectorAll(selectors));
|
|
232
|
+
const docRect = document.documentElement.getBoundingClientRect();
|
|
233
|
+
|
|
234
|
+
let validIndex = 0;
|
|
235
|
+
const items = elements.map((el) => {
|
|
236
|
+
const r = el.getBoundingClientRect();
|
|
237
|
+
const computed = window.getComputedStyle(el);
|
|
238
|
+
if (computed.display === 'none' || computed.visibility === 'hidden' || r.width === 0 || r.height === 0) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
validIndex++;
|
|
243
|
+
|
|
244
|
+
// Get text content
|
|
245
|
+
let text = el.innerText || el.textContent || '';
|
|
246
|
+
text = text.trim().substring(0, 100);
|
|
247
|
+
|
|
248
|
+
// Get selector
|
|
249
|
+
let selector = '';
|
|
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();
|
|
253
|
+
|
|
254
|
+
if (withSoM && container) {
|
|
255
|
+
const id = String(validIndex);
|
|
256
|
+
const w = Math.max(20, id.length * 8 + 14);
|
|
257
|
+
const marker = document.createElement('div');
|
|
258
|
+
marker.className = 'aether-som-marker';
|
|
259
|
+
marker.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="20" style="display:block">'
|
|
260
|
+
+ '<rect width="' + w + '" height="20" rx="10" fill="#1e40af"/>'
|
|
261
|
+
+ '<text x="' + (w / 2) + '" y="10" dominant-baseline="central" text-anchor="middle" font-family="ui-monospace,monospace" font-size="11" font-weight="700" fill="white">' + id + '</text>'
|
|
262
|
+
+ '</svg>';
|
|
263
|
+
marker.style.cssText = \`
|
|
264
|
+
position: absolute;
|
|
265
|
+
left: \${r.left}px;
|
|
266
|
+
top: \${r.top}px;
|
|
267
|
+
pointer-events: none;
|
|
268
|
+
filter: drop-shadow(0 1px 4px rgba(0,0,0,0.35));
|
|
269
|
+
transform: translate(-4px, -4px);
|
|
270
|
+
\`;
|
|
271
|
+
container.appendChild(marker);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
id: validIndex,
|
|
276
|
+
tag: el.tagName.toLowerCase(),
|
|
277
|
+
text: text,
|
|
278
|
+
selector: selector,
|
|
279
|
+
bounds: {
|
|
280
|
+
x: Math.max(0, r.left - docRect.left),
|
|
281
|
+
y: Math.max(0, r.top - docRect.top),
|
|
282
|
+
width: r.width,
|
|
283
|
+
height: r.height
|
|
284
|
+
},
|
|
285
|
+
attributes: {
|
|
286
|
+
type: el.getAttribute('type') || '',
|
|
287
|
+
href: el.getAttribute('href') || '',
|
|
288
|
+
role: el.getAttribute('role') || '',
|
|
289
|
+
'aria-label': el.getAttribute('aria-label') || ''
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}).filter(x => x !== null);
|
|
293
|
+
|
|
294
|
+
return { items, somInjected: !!(withSoM && container) };
|
|
295
|
+
})()
|
|
296
|
+
`,
|
|
297
|
+
returnByValue: true,
|
|
298
|
+
awaitPromise: true,
|
|
299
|
+
});
|
|
300
|
+
const val = result.result?.value || { items: [], somInjected: false };
|
|
301
|
+
return { elements: val.items, somInjected: val.somInjected };
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Remove Set-of-Marks overlay
|
|
305
|
+
*/
|
|
306
|
+
async removeSoMOverlay() {
|
|
307
|
+
await this.sendCommand("Runtime.evaluate", {
|
|
308
|
+
expression: `
|
|
309
|
+
const container = document.getElementById('aether-som-container');
|
|
310
|
+
if (container) container.remove();
|
|
311
|
+
document.querySelectorAll('.aether-som-marker').forEach(el => el.remove());
|
|
312
|
+
`,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Wait for a selector to appear in DOM
|
|
317
|
+
*/
|
|
318
|
+
async waitForSelector(selector, timeout = 10000, options = {}) {
|
|
319
|
+
const startTime = Date.now();
|
|
320
|
+
let lastBox = null;
|
|
321
|
+
let stableSince = 0;
|
|
322
|
+
while (Date.now() - startTime < timeout) {
|
|
323
|
+
const result = await this.sendCommand("Runtime.evaluate", {
|
|
324
|
+
expression: `
|
|
325
|
+
(function() {
|
|
326
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
327
|
+
if (!el) return { found: false };
|
|
328
|
+
const rect = el.getBoundingClientRect();
|
|
329
|
+
const computed = window.getComputedStyle(el);
|
|
330
|
+
const visible = computed.display !== 'none' &&
|
|
331
|
+
computed.visibility !== 'hidden' &&
|
|
332
|
+
computed.opacity !== '0' &&
|
|
333
|
+
rect.width > 0 &&
|
|
334
|
+
rect.height > 0;
|
|
335
|
+
return {
|
|
336
|
+
found: true,
|
|
337
|
+
visible,
|
|
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) {
|
|
360
|
+
if (!stableSince)
|
|
361
|
+
stableSince = Date.now();
|
|
362
|
+
if (Date.now() - stableSince >= 120)
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
stableSince = 0;
|
|
367
|
+
lastBox = box;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
await new Promise(r => setTimeout(r, 75));
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Wait for navigation to complete
|
|
376
|
+
*/
|
|
377
|
+
async waitForNavigation(timeout = 10000) {
|
|
378
|
+
await this.sendCommand("Page.enable", {});
|
|
379
|
+
return new Promise((resolve, reject) => {
|
|
380
|
+
const timeoutId = setTimeout(() => {
|
|
381
|
+
this.removeEventListener("Page.loadEventFired", listener);
|
|
382
|
+
reject(new Error("Navigation timeout"));
|
|
383
|
+
}, timeout);
|
|
384
|
+
const listener = () => {
|
|
385
|
+
clearTimeout(timeoutId);
|
|
386
|
+
this.removeEventListener("Page.loadEventFired", listener);
|
|
387
|
+
resolve();
|
|
388
|
+
};
|
|
389
|
+
this.on("Page.loadEventFired", listener);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Wait for network idle (no requests for specified duration)
|
|
394
|
+
*/
|
|
395
|
+
async waitForNetworkIdle(idleTimeout = 500, timeout = 10000) {
|
|
396
|
+
await this.sendCommand("Network.enable", {});
|
|
397
|
+
return new Promise((resolve, reject) => {
|
|
398
|
+
let lastRequestTime = Date.now();
|
|
399
|
+
let idleCheckInterval;
|
|
400
|
+
let timeoutId;
|
|
401
|
+
const resetIdle = () => {
|
|
402
|
+
lastRequestTime = Date.now();
|
|
403
|
+
};
|
|
404
|
+
const checkIdle = () => {
|
|
405
|
+
if (Date.now() - lastRequestTime >= idleTimeout) {
|
|
406
|
+
clearInterval(idleCheckInterval);
|
|
407
|
+
clearTimeout(timeoutId);
|
|
408
|
+
this.removeEventListener("Network.requestWillBeSent", resetIdle);
|
|
409
|
+
this.removeEventListener("Network.responseReceived", resetIdle);
|
|
410
|
+
resolve();
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
timeoutId = setTimeout(() => {
|
|
414
|
+
clearInterval(idleCheckInterval);
|
|
415
|
+
this.removeEventListener("Network.requestWillBeSent", resetIdle);
|
|
416
|
+
this.removeEventListener("Network.responseReceived", resetIdle);
|
|
417
|
+
resolve(); // Resolve anyway after timeout
|
|
418
|
+
}, timeout);
|
|
419
|
+
idleCheckInterval = setInterval(checkIdle, 100);
|
|
420
|
+
this.on("Network.requestWillBeSent", resetIdle);
|
|
421
|
+
this.on("Network.responseReceived", resetIdle);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
attachNetworkLogging() {
|
|
425
|
+
if (this.networkLoggingAttached)
|
|
426
|
+
return;
|
|
427
|
+
this.networkLoggingAttached = true;
|
|
428
|
+
this.on("Network.requestWillBeSent", (params) => {
|
|
429
|
+
this.networkTraffic.push({
|
|
430
|
+
type: "request",
|
|
431
|
+
requestId: params.requestId,
|
|
432
|
+
url: params.request.url,
|
|
433
|
+
method: params.request.method,
|
|
434
|
+
timestamp: new Date().toISOString(),
|
|
435
|
+
});
|
|
436
|
+
if (this.networkTraffic.length > this.MAX_TRAFFIC_LOGS)
|
|
437
|
+
this.networkTraffic.shift();
|
|
438
|
+
});
|
|
439
|
+
this.on("Network.responseReceived", (params) => {
|
|
440
|
+
this.networkTraffic.push({
|
|
441
|
+
type: "response",
|
|
442
|
+
requestId: params.requestId,
|
|
443
|
+
url: params.response.url,
|
|
444
|
+
status: params.response.status,
|
|
445
|
+
mimeType: params.response.mimeType,
|
|
446
|
+
timestamp: new Date().toISOString(),
|
|
447
|
+
});
|
|
448
|
+
if (this.networkTraffic.length > this.MAX_TRAFFIC_LOGS)
|
|
449
|
+
this.networkTraffic.shift();
|
|
450
|
+
});
|
|
451
|
+
this.on("Network.loadingFailed", (params) => {
|
|
452
|
+
this.networkTraffic.push({
|
|
453
|
+
type: "error",
|
|
454
|
+
requestId: params.requestId,
|
|
455
|
+
errorText: params.errorText,
|
|
456
|
+
timestamp: new Date().toISOString(),
|
|
457
|
+
});
|
|
458
|
+
if (this.networkTraffic.length > this.MAX_TRAFFIC_LOGS)
|
|
459
|
+
this.networkTraffic.shift();
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
attachDiagnosticsLogging() {
|
|
463
|
+
if (this.diagnosticsLoggingAttached)
|
|
464
|
+
return;
|
|
465
|
+
this.diagnosticsLoggingAttached = true;
|
|
466
|
+
const pushLog = (entry) => {
|
|
467
|
+
this.consoleLogs.push({ timestamp: new Date().toISOString(), ...entry });
|
|
468
|
+
if (this.consoleLogs.length > this.MAX_CONSOLE_LOGS)
|
|
469
|
+
this.consoleLogs.shift();
|
|
470
|
+
};
|
|
471
|
+
this.on("Runtime.consoleAPICalled", (params) => {
|
|
472
|
+
pushLog({
|
|
473
|
+
source: "console",
|
|
474
|
+
level: params.type,
|
|
475
|
+
text: (params.args || []).map((arg) => arg.value ?? arg.description ?? arg.type).join(" "),
|
|
476
|
+
url: params.stackTrace?.callFrames?.[0]?.url,
|
|
477
|
+
lineNumber: params.stackTrace?.callFrames?.[0]?.lineNumber,
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
this.on("Runtime.exceptionThrown", (params) => {
|
|
481
|
+
pushLog({
|
|
482
|
+
source: "exception",
|
|
483
|
+
level: "error",
|
|
484
|
+
text: params.exceptionDetails?.text || params.exceptionDetails?.exception?.description || "Runtime exception",
|
|
485
|
+
url: params.exceptionDetails?.url,
|
|
486
|
+
lineNumber: params.exceptionDetails?.lineNumber,
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
this.on("Log.entryAdded", (params) => {
|
|
490
|
+
const entry = params.entry || {};
|
|
491
|
+
pushLog({
|
|
492
|
+
source: entry.source || "log",
|
|
493
|
+
level: entry.level,
|
|
494
|
+
text: entry.text,
|
|
495
|
+
url: entry.url,
|
|
496
|
+
lineNumber: entry.lineNumber,
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
this.on("Page.javascriptDialogOpening", (params) => {
|
|
500
|
+
pushLog({
|
|
501
|
+
source: "dialog",
|
|
502
|
+
level: "warning",
|
|
503
|
+
text: `${params.type}: ${params.message}`,
|
|
504
|
+
url: params.url,
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
removeEventListener(event, listener) {
|
|
509
|
+
const listeners = this.eventListeners.get(event) || [];
|
|
510
|
+
const idx = listeners.indexOf(listener);
|
|
511
|
+
if (idx !== -1) {
|
|
512
|
+
listeners.splice(idx, 1);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
async getTabs() {
|
|
516
|
+
return await this.listTargets();
|
|
517
|
+
}
|
|
518
|
+
async switchToTarget(targetId, port = 9222) {
|
|
519
|
+
const targets = await this.listTargets(port);
|
|
520
|
+
const target = targets.find(t => t.id === targetId);
|
|
521
|
+
if (!target) {
|
|
522
|
+
throw new Error(`Target not found: ${targetId}`);
|
|
523
|
+
}
|
|
524
|
+
await this.attachToTarget(target);
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Get a simplified version of the Accessibility Tree for AI agents.
|
|
528
|
+
*/
|
|
529
|
+
async getSimplifiedAccessibilityTree() {
|
|
530
|
+
await this.sendCommand("Accessibility.enable");
|
|
531
|
+
const result = await this.sendCommand("Accessibility.getFullAXTree");
|
|
532
|
+
if (!result || !result.nodes)
|
|
533
|
+
return [];
|
|
534
|
+
const nodes = result.nodes;
|
|
535
|
+
const interactiveNodes = [];
|
|
536
|
+
// Map of node IDs for fast lookup
|
|
537
|
+
const nodeMap = new Map();
|
|
538
|
+
nodes.forEach((node) => nodeMap.set(node.nodeId, node));
|
|
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
|
|
547
|
+
nodes.forEach((node) => {
|
|
548
|
+
const role = node.role?.value;
|
|
549
|
+
const name = getNodeName(node);
|
|
550
|
+
// Only include interactive or meaningful nodes
|
|
551
|
+
const isInteractive = [
|
|
552
|
+
"button", "link", "checkbox", "radio", "textbox", "searchbox",
|
|
553
|
+
"combobox", "listbox", "menuitem", "slider", "switch", "tab"
|
|
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,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
return interactiveNodes;
|
|
569
|
+
}
|
|
570
|
+
async getNetworkTraffic() {
|
|
571
|
+
return this.networkTraffic;
|
|
572
|
+
}
|
|
573
|
+
async getConsoleLogs(limit = 50) {
|
|
574
|
+
return this.consoleLogs.slice(-limit);
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Get page DOM snapshot
|
|
578
|
+
*/
|
|
579
|
+
async getDOMSnapshot() {
|
|
580
|
+
const result = await this.sendCommand("DOMSnapshot.captureSnapshot", {
|
|
581
|
+
computedStyles: [],
|
|
582
|
+
});
|
|
583
|
+
return result;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Evaluate JavaScript in page context
|
|
587
|
+
*/
|
|
588
|
+
async evaluate(expression) {
|
|
589
|
+
const result = await this.sendCommand("Runtime.evaluate", {
|
|
590
|
+
expression,
|
|
591
|
+
returnByValue: true,
|
|
592
|
+
awaitPromise: true,
|
|
593
|
+
});
|
|
594
|
+
return result.result?.value !== undefined ? result.result.value : result.result;
|
|
595
|
+
}
|
|
596
|
+
// ==================== Runtime Methods ====================
|
|
597
|
+
async callFunctionOn(functionDeclaration, objectId, returnByValue = true, awaitPromise = false) {
|
|
598
|
+
const result = await this.sendCommand("Runtime.callFunctionOn", {
|
|
599
|
+
functionDeclaration, objectId, returnByValue, awaitPromise
|
|
600
|
+
});
|
|
601
|
+
return result.result;
|
|
602
|
+
}
|
|
603
|
+
async awaitPromise(promiseObjectId) {
|
|
604
|
+
const result = await this.sendCommand("Runtime.awaitPromise", { promiseObjectId, returnByValue: true });
|
|
605
|
+
return result.result;
|
|
606
|
+
}
|
|
607
|
+
async getProperties(objectId, ownProperties, accessorPropertiesOnly, generatePreview) {
|
|
608
|
+
return await this.sendCommand("Runtime.getProperties", { objectId, ownProperties, accessorPropertiesOnly, generatePreview });
|
|
609
|
+
}
|
|
610
|
+
async releaseObject(objectId) {
|
|
611
|
+
await this.sendCommand("Runtime.releaseObject", { objectId });
|
|
612
|
+
}
|
|
613
|
+
// ==================== DOM Methods ====================
|
|
614
|
+
async getOuterHTML(nodeId) {
|
|
615
|
+
const result = await this.sendCommand("DOM.getOuterHTML", { nodeId });
|
|
616
|
+
return result.outerHTML;
|
|
617
|
+
}
|
|
618
|
+
async setOuterHTML(nodeId, outerHTML) {
|
|
619
|
+
await this.sendCommand("DOM.setOuterHTML", { nodeId, outerHTML });
|
|
620
|
+
}
|
|
621
|
+
async performSearch(query, includeUserAgentShadowDOM) {
|
|
622
|
+
return await this.sendCommand("DOM.performSearch", { query, includeUserAgentShadowDOM });
|
|
623
|
+
}
|
|
624
|
+
async getSearchResults(searchId, fromIndex, toIndex) {
|
|
625
|
+
const result = await this.sendCommand("DOM.getSearchResults", { searchId, fromIndex, toIndex });
|
|
626
|
+
return result.nodeIds;
|
|
627
|
+
}
|
|
628
|
+
async setAttributeValue(nodeId, name, value) {
|
|
629
|
+
await this.sendCommand("DOM.setAttributeValue", { nodeId, name, value });
|
|
630
|
+
}
|
|
631
|
+
async removeAttribute(nodeId, name) {
|
|
632
|
+
await this.sendCommand("DOM.removeAttribute", { nodeId, name });
|
|
633
|
+
}
|
|
634
|
+
async resolveNode(nodeId, objectGroup) {
|
|
635
|
+
const result = await this.sendCommand("DOM.resolveNode", { nodeId, objectGroup });
|
|
636
|
+
return result.object;
|
|
637
|
+
}
|
|
638
|
+
async requestChildNodes(nodeId, depth, pierce) {
|
|
639
|
+
await this.sendCommand("DOM.requestChildNodes", { nodeId, depth, pierce });
|
|
640
|
+
}
|
|
641
|
+
// ==================== Network Methods ====================
|
|
642
|
+
async getResponseBody(requestId) {
|
|
643
|
+
return await this.sendCommand("Network.getResponseBody", { requestId });
|
|
644
|
+
}
|
|
645
|
+
async setExtraHTTPHeaders(headers) {
|
|
646
|
+
await this.sendCommand("Network.setExtraHTTPHeaders", { headers });
|
|
647
|
+
}
|
|
648
|
+
async setUserAgentOverride(userAgent, acceptLanguage, platform, userAgentMetadata) {
|
|
649
|
+
await this.sendCommand("Network.setUserAgentOverride", { userAgent, acceptLanguage, platform, userAgentMetadata });
|
|
650
|
+
}
|
|
651
|
+
async setCacheDisabled(cacheDisabled) {
|
|
652
|
+
await this.sendCommand("Network.setCacheDisabled", { cacheDisabled });
|
|
653
|
+
}
|
|
654
|
+
async getRequestPostData(requestId) {
|
|
655
|
+
return await this.sendCommand("Network.getRequestPostData", { requestId });
|
|
656
|
+
}
|
|
657
|
+
async searchInResponseBody(requestId, query, caseSensitive, isRegex) {
|
|
658
|
+
return await this.sendCommand("Network.searchInResponseBody", { requestId, query, caseSensitive, isRegex });
|
|
659
|
+
}
|
|
660
|
+
async deleteCookies(name, url, domain, path, partitionKey) {
|
|
661
|
+
await this.sendCommand("Network.deleteCookies", { name, url, domain, path, partitionKey });
|
|
662
|
+
}
|
|
663
|
+
async setCookies(cookies) {
|
|
664
|
+
await this.sendCommand("Network.setCookies", { cookies });
|
|
665
|
+
}
|
|
666
|
+
// ==================== Page Methods ====================
|
|
667
|
+
async getFrameTree() {
|
|
668
|
+
const result = await this.sendCommand("Page.getFrameTree", {});
|
|
669
|
+
return result.frameTree;
|
|
670
|
+
}
|
|
671
|
+
async printToPDF(options) {
|
|
672
|
+
const result = await this.sendCommand("Page.printToPDF", options || {});
|
|
673
|
+
return result.data;
|
|
674
|
+
}
|
|
675
|
+
async reload(ignoreCache, scriptToEvaluateOnLoad, loaderId) {
|
|
676
|
+
await this.sendCommand("Page.reload", { ignoreCache, scriptToEvaluateOnLoad, loaderId });
|
|
677
|
+
}
|
|
678
|
+
async bringToFront() {
|
|
679
|
+
await this.sendCommand("Page.bringToFront", {});
|
|
680
|
+
}
|
|
681
|
+
async closePage() {
|
|
682
|
+
await this.sendCommand("Page.close", {});
|
|
683
|
+
}
|
|
684
|
+
// ==================== DOMStorage Methods ====================
|
|
685
|
+
async enableDOMStorage() {
|
|
686
|
+
await this.sendCommand("DOMStorage.enable", {});
|
|
687
|
+
}
|
|
688
|
+
async getDOMStorageItems(storageId) {
|
|
689
|
+
const result = await this.sendCommand("DOMStorage.getDOMStorageItems", { storageId });
|
|
690
|
+
return result.entries;
|
|
691
|
+
}
|
|
692
|
+
async setDOMStorageItem(storageId, key, value) {
|
|
693
|
+
await this.sendCommand("DOMStorage.setDOMStorageItem", { storageId, key, value });
|
|
694
|
+
}
|
|
695
|
+
async removeDOMStorageItem(storageId, key) {
|
|
696
|
+
await this.sendCommand("DOMStorage.removeDOMStorageItem", { storageId, key });
|
|
697
|
+
}
|
|
698
|
+
async clearDOMStorage(storageId) {
|
|
699
|
+
await this.sendCommand("DOMStorage.clear", { storageId });
|
|
700
|
+
}
|
|
701
|
+
// ==================== CacheStorage Methods ====================
|
|
702
|
+
async enableCacheStorage() {
|
|
703
|
+
await this.sendCommand("CacheStorage.enable", {});
|
|
704
|
+
}
|
|
705
|
+
async requestCacheNames(securityOrigin) {
|
|
706
|
+
return await this.sendCommand("CacheStorage.requestCacheNames", { securityOrigin });
|
|
707
|
+
}
|
|
708
|
+
async deleteCache(cacheId) {
|
|
709
|
+
await this.sendCommand("CacheStorage.deleteCache", { cacheId });
|
|
710
|
+
}
|
|
711
|
+
async deleteEntry(cacheId, request, method) {
|
|
712
|
+
await this.sendCommand("CacheStorage.deleteEntry", { cacheId, request, method });
|
|
713
|
+
}
|
|
714
|
+
async requestEntries(cacheId, skipCount, pageSize) {
|
|
715
|
+
return await this.sendCommand("CacheStorage.requestEntries", { cacheId, skipCount, pageSize });
|
|
716
|
+
}
|
|
717
|
+
// ==================== Browser Methods ====================
|
|
718
|
+
async getBrowserVersion() {
|
|
719
|
+
return await this.sendCommand("Browser.getVersion", {});
|
|
720
|
+
}
|
|
721
|
+
async setPermission(permission, setting, origin) {
|
|
722
|
+
await this.sendCommand("Browser.setPermission", { permission: { name: permission }, setting, origin });
|
|
723
|
+
}
|
|
724
|
+
async grantPermissions(permissions, origin) {
|
|
725
|
+
await this.sendCommand("Browser.grantPermissions", { permissions: permissions.map(p => ({ name: p })), origin });
|
|
726
|
+
}
|
|
727
|
+
async resetPermissions() {
|
|
728
|
+
await this.sendCommand("Browser.resetPermissions", {});
|
|
729
|
+
}
|
|
730
|
+
async setDownloadBehavior(behavior, downloadPath) {
|
|
731
|
+
await this.sendCommand("Browser.setDownloadBehavior", { behavior, downloadPath });
|
|
732
|
+
}
|
|
733
|
+
// ==================== Emulation Methods ====================
|
|
734
|
+
async setDeviceMetricsOverride(options) {
|
|
735
|
+
await this.sendCommand("Emulation.setDeviceMetricsOverride", options);
|
|
736
|
+
}
|
|
737
|
+
async emulateUserAgent(userAgent, acceptLanguage, platform, userAgentMetadata) {
|
|
738
|
+
await this.sendCommand("Emulation.setUserAgentOverride", { userAgent, acceptLanguage, platform, userAgentMetadata });
|
|
739
|
+
}
|
|
740
|
+
// ==================== IO Methods ====================
|
|
741
|
+
async ioRead(stream, offset, size) {
|
|
742
|
+
return await this.sendCommand("IO.read", { handle: stream, offset, size });
|
|
743
|
+
}
|
|
744
|
+
async ioClose(stream) {
|
|
745
|
+
await this.sendCommand("IO.close", { handle: stream });
|
|
746
|
+
}
|
|
747
|
+
// ==================== Security Methods ====================
|
|
748
|
+
async enableSecurity() {
|
|
749
|
+
await this.sendCommand("Security.enable", {});
|
|
750
|
+
}
|
|
751
|
+
async getSecurityState() {
|
|
752
|
+
return await this.sendCommand("Security.getSecurityState", {});
|
|
753
|
+
}
|
|
754
|
+
// ==================== SystemInfo Methods ====================
|
|
755
|
+
async getSystemInfo() {
|
|
756
|
+
return await this.sendCommand("SystemInfo.getInfo", {});
|
|
757
|
+
}
|
|
758
|
+
async getProcessInfo() {
|
|
759
|
+
return await this.sendCommand("SystemInfo.getProcessInfo", {});
|
|
760
|
+
}
|
|
761
|
+
// ==================== Debugger Methods ====================
|
|
762
|
+
async enableDebugger() {
|
|
763
|
+
await this.sendCommand("Debugger.enable", {});
|
|
764
|
+
}
|
|
765
|
+
async disableDebugger() {
|
|
766
|
+
await this.sendCommand("Debugger.disable", {});
|
|
767
|
+
}
|
|
768
|
+
async pauseDebugger() {
|
|
769
|
+
await this.sendCommand("Debugger.pause", {});
|
|
770
|
+
}
|
|
771
|
+
async resumeDebugger() {
|
|
772
|
+
await this.sendCommand("Debugger.resume", {});
|
|
773
|
+
}
|
|
774
|
+
async stepOver() {
|
|
775
|
+
await this.sendCommand("Debugger.stepOver", {});
|
|
776
|
+
}
|
|
777
|
+
async stepInto() {
|
|
778
|
+
await this.sendCommand("Debugger.stepInto", {});
|
|
779
|
+
}
|
|
780
|
+
async stepOut() {
|
|
781
|
+
await this.sendCommand("Debugger.stepOut", {});
|
|
782
|
+
}
|
|
783
|
+
async setBreakpointByUrl(url, lineNumber, columnNumber, condition) {
|
|
784
|
+
return await this.sendCommand("Debugger.setBreakpointByUrl", { url, lineNumber, columnNumber, condition });
|
|
785
|
+
}
|
|
786
|
+
async removeBreakpoint(breakpointId) {
|
|
787
|
+
await this.sendCommand("Debugger.removeBreakpoint", { breakpointId });
|
|
788
|
+
}
|
|
789
|
+
async getScriptSource(scriptId) {
|
|
790
|
+
return await this.sendCommand("Debugger.getScriptSource", { scriptId });
|
|
791
|
+
}
|
|
792
|
+
// ==================== IndexedDB Methods ====================
|
|
793
|
+
async enableIndexedDB() {
|
|
794
|
+
await this.sendCommand("IndexedDB.enable", {});
|
|
795
|
+
}
|
|
796
|
+
async disableIndexedDB() {
|
|
797
|
+
await this.sendCommand("IndexedDB.disable", {});
|
|
798
|
+
}
|
|
799
|
+
async requestDatabaseNames(securityOrigin) {
|
|
800
|
+
return await this.sendCommand("IndexedDB.requestDatabaseNames", { securityOrigin });
|
|
801
|
+
}
|
|
802
|
+
async requestDatabase(securityOrigin, databaseName) {
|
|
803
|
+
return await this.sendCommand("IndexedDB.requestDatabase", { securityOrigin, databaseName });
|
|
804
|
+
}
|
|
805
|
+
async requestData(securityOrigin, databaseName, objectStoreName, indexName, idbKeyRange, skipCount, pageSize) {
|
|
806
|
+
return await this.sendCommand("IndexedDB.requestData", {
|
|
807
|
+
securityOrigin, databaseName, objectStoreName, indexName, idbKeyRange, skipCount, pageSize
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
async clearObjectStore(securityOrigin, databaseName, objectStoreName) {
|
|
811
|
+
await this.sendCommand("IndexedDB.clearObjectStore", { securityOrigin, databaseName, objectStoreName });
|
|
812
|
+
}
|
|
813
|
+
async deleteDatabase(securityOrigin, databaseName) {
|
|
814
|
+
await this.sendCommand("IndexedDB.deleteDatabase", { securityOrigin, databaseName });
|
|
815
|
+
}
|
|
816
|
+
// ==================== Memory Methods ====================
|
|
817
|
+
async enableMemory() {
|
|
818
|
+
await this.sendCommand("Memory.enable", {});
|
|
819
|
+
}
|
|
820
|
+
async getDOMCounters() {
|
|
821
|
+
return await this.sendCommand("Memory.getDOMCounters", {});
|
|
822
|
+
}
|
|
823
|
+
async forceGarbageCollection() {
|
|
824
|
+
await this.sendCommand("Memory.forciblyPurgeJavaScriptMemory", {});
|
|
825
|
+
}
|
|
826
|
+
async startMemorySampling(samplingInterval, suppressRandomness) {
|
|
827
|
+
await this.sendCommand("Memory.startSampling", { samplingInterval, suppressRandomness });
|
|
828
|
+
}
|
|
829
|
+
async stopMemorySampling() {
|
|
830
|
+
return await this.sendCommand("Memory.stopSampling", {});
|
|
831
|
+
}
|
|
832
|
+
// ==================== ServiceWorker Methods ====================
|
|
833
|
+
async enableServiceWorker() {
|
|
834
|
+
await this.sendCommand("ServiceWorker.enable", {});
|
|
835
|
+
}
|
|
836
|
+
async setForceUpdateOnPageLoad(forceUpdateOnPageLoad) {
|
|
837
|
+
await this.sendCommand("ServiceWorker.setForceUpdateOnPageLoad", { forceUpdateOnPageLoad });
|
|
838
|
+
}
|
|
839
|
+
async skipWaiting(activationId) {
|
|
840
|
+
await this.sendCommand("ServiceWorker.skipWaiting", { activationId });
|
|
841
|
+
}
|
|
842
|
+
// ==================== WebAuthn Methods ====================
|
|
843
|
+
async enableWebAuthn() {
|
|
844
|
+
await this.sendCommand("WebAuthn.enable", {});
|
|
845
|
+
}
|
|
846
|
+
async addVirtualAuthenticator(options) {
|
|
847
|
+
return await this.sendCommand("WebAuthn.addVirtualAuthenticator", options);
|
|
848
|
+
}
|
|
849
|
+
async removeVirtualAuthenticator(authenticatorId) {
|
|
850
|
+
await this.sendCommand("WebAuthn.removeVirtualAuthenticator", { authenticatorId });
|
|
851
|
+
}
|
|
852
|
+
// ==================== Profiler Methods ====================
|
|
853
|
+
async enableProfiler() {
|
|
854
|
+
await this.sendCommand("Profiler.enable", {});
|
|
855
|
+
}
|
|
856
|
+
async disableProfiler() {
|
|
857
|
+
await this.sendCommand("Profiler.disable", {});
|
|
858
|
+
}
|
|
859
|
+
async startProfiler() {
|
|
860
|
+
await this.sendCommand("Profiler.start", {});
|
|
861
|
+
}
|
|
862
|
+
async stopProfiler() {
|
|
863
|
+
return await this.sendCommand("Profiler.stop", {});
|
|
864
|
+
}
|
|
865
|
+
async getBestEffortCoverage() {
|
|
866
|
+
return await this.sendCommand("Profiler.getBestEffortCoverage", {});
|
|
867
|
+
}
|
|
868
|
+
async startPreciseCoverage(callCount, detailed, allowFuntionLocations) {
|
|
869
|
+
await this.sendCommand("Profiler.startPreciseCoverage", { callCount, detailed, allowFuntionLocations });
|
|
870
|
+
}
|
|
871
|
+
async stopPreciseCoverage() {
|
|
872
|
+
await this.sendCommand("Profiler.stopPreciseCoverage", {});
|
|
873
|
+
}
|
|
874
|
+
async takePreciseCoverage() {
|
|
875
|
+
return await this.sendCommand("Profiler.takePreciseCoverage", {});
|
|
876
|
+
}
|
|
877
|
+
// ==================== HeapProfiler Methods ====================
|
|
878
|
+
async enableHeapProfiler() {
|
|
879
|
+
await this.sendCommand("HeapProfiler.enable", {});
|
|
880
|
+
}
|
|
881
|
+
async disableHeapProfiler() {
|
|
882
|
+
await this.sendCommand("HeapProfiler.disable", {});
|
|
883
|
+
}
|
|
884
|
+
async heapProfilerCollectGarbage() {
|
|
885
|
+
await this.sendCommand("HeapProfiler.collectGarbage", {});
|
|
886
|
+
}
|
|
887
|
+
async getHeapObjectId(objectId) {
|
|
888
|
+
return await this.sendCommand("HeapProfiler.getHeapObjectId", { objectId });
|
|
889
|
+
}
|
|
890
|
+
async getObjectByHeapObjectId(heapSnapshotObjectId, objectGroup) {
|
|
891
|
+
return await this.sendCommand("HeapProfiler.getObjectByHeapObjectId", { heapSnapshotObjectId, objectGroup });
|
|
892
|
+
}
|
|
893
|
+
async getHeapSamplingProfile() {
|
|
894
|
+
return await this.sendCommand("HeapProfiler.getSamplingProfile", {});
|
|
895
|
+
}
|
|
896
|
+
async startHeapSampling(samplingInterval, suppressRandomness) {
|
|
897
|
+
await this.sendCommand("HeapProfiler.startSampling", { samplingInterval, suppressRandomness });
|
|
898
|
+
}
|
|
899
|
+
async stopHeapSampling() {
|
|
900
|
+
return await this.sendCommand("HeapProfiler.stopSampling", {});
|
|
901
|
+
}
|
|
902
|
+
async takeHeapSnapshot(reportProgress, treatGlobalObjectsAsRoots, captureNumericValue) {
|
|
903
|
+
await this.sendCommand("HeapProfiler.takeHeapSnapshot", { reportProgress, treatGlobalObjectsAsRoots, captureNumericValue });
|
|
904
|
+
}
|
|
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
|
+
/**
|
|
920
|
+
* Click at coordinates with human-like timing, micro-jitter, and optional target width.
|
|
921
|
+
*/
|
|
922
|
+
async click(x, y, button = "left", targetWidth) {
|
|
923
|
+
const targetX = Math.round(Number(x));
|
|
924
|
+
const targetY = Math.round(Number(y));
|
|
925
|
+
await this.moveMouse(targetX, targetY, targetWidth);
|
|
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);
|
|
931
|
+
await this.sendCommand("Input.dispatchMouseEvent", {
|
|
932
|
+
type: "mousePressed", x: cx, y: cy, button, clickCount: 1, pointerType: "mouse",
|
|
933
|
+
});
|
|
934
|
+
// Natural hold duration before release
|
|
935
|
+
await new Promise((r) => setTimeout(r, 60 + Math.random() * 110));
|
|
936
|
+
await this.sendCommand("Input.dispatchMouseEvent", {
|
|
937
|
+
type: "mouseReleased", x: cx, y: cy, button, clickCount: 1, pointerType: "mouse",
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Move mouse along a cubic Bezier arc with Fitts's Law duration and fractional Brownian motion tremors.
|
|
942
|
+
*/
|
|
943
|
+
async moveMouse(x, y, targetWidth) {
|
|
944
|
+
const targetX = Math.round(Number(x));
|
|
945
|
+
const targetY = Math.round(Number(y));
|
|
946
|
+
const start = this.mousePosition ?? { x: targetX, y: targetY };
|
|
947
|
+
const dist = Math.hypot(targetX - start.x, targetY - start.y);
|
|
948
|
+
if (dist < 2) {
|
|
949
|
+
this.mousePosition = { x: targetX, y: targetY };
|
|
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 };
|
|
997
|
+
}
|
|
998
|
+
getMousePosition() {
|
|
999
|
+
return this.mousePosition ?? { x: 300, y: 300 };
|
|
1000
|
+
}
|
|
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
|
+
async moveMouseToSelector(selector) {
|
|
1088
|
+
const result = await this.evaluate(`
|
|
1089
|
+
(function() {
|
|
1090
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
1091
|
+
if (!el) return null;
|
|
1092
|
+
el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
|
|
1093
|
+
const r = el.getBoundingClientRect();
|
|
1094
|
+
if (r.width === 0 || r.height === 0) return null;
|
|
1095
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
|
1096
|
+
})()
|
|
1097
|
+
`);
|
|
1098
|
+
if (!result)
|
|
1099
|
+
return false;
|
|
1100
|
+
await this.moveMouse(result.x, result.y);
|
|
1101
|
+
return true;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Scroll from the current or supplied pointer location using wheel events.
|
|
1105
|
+
*/
|
|
1106
|
+
async wheel(deltaX, deltaY, x, y) {
|
|
1107
|
+
const origin = {
|
|
1108
|
+
x: Number(x ?? this.mousePosition?.x ?? 0),
|
|
1109
|
+
y: Number(y ?? this.mousePosition?.y ?? 0),
|
|
1110
|
+
};
|
|
1111
|
+
await this.moveMouse(origin.x, origin.y);
|
|
1112
|
+
const dominant = Math.abs(Number(deltaY)) >= Math.abs(Number(deltaX)) ? Number(deltaY) : Number(deltaX);
|
|
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
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
async typeText(text) {
|
|
1139
|
+
const ADJACENT_KEYS = {
|
|
1140
|
+
'a': 'qwsz',
|
|
1141
|
+
'b': 'vghn',
|
|
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));
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
async pressKey(key, modifiers = []) {
|
|
1199
|
+
const modifierMask = this.modifierMask(modifiers);
|
|
1200
|
+
const keyDef = this.keyDefinition(key);
|
|
1201
|
+
await this.sendCommand("Input.dispatchKeyEvent", {
|
|
1202
|
+
type: "keyDown",
|
|
1203
|
+
key: keyDef.key,
|
|
1204
|
+
code: keyDef.code,
|
|
1205
|
+
windowsVirtualKeyCode: keyDef.windowsVirtualKeyCode,
|
|
1206
|
+
nativeVirtualKeyCode: keyDef.windowsVirtualKeyCode,
|
|
1207
|
+
modifiers: modifierMask,
|
|
1208
|
+
});
|
|
1209
|
+
await this.sendCommand("Input.dispatchKeyEvent", {
|
|
1210
|
+
type: "keyUp",
|
|
1211
|
+
key: keyDef.key,
|
|
1212
|
+
code: keyDef.code,
|
|
1213
|
+
windowsVirtualKeyCode: keyDef.windowsVirtualKeyCode,
|
|
1214
|
+
nativeVirtualKeyCode: keyDef.windowsVirtualKeyCode,
|
|
1215
|
+
modifiers: modifierMask,
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
modifierMask(modifiers) {
|
|
1219
|
+
return modifiers.reduce((mask, modifier) => {
|
|
1220
|
+
const key = modifier.toLowerCase();
|
|
1221
|
+
if (key === "alt")
|
|
1222
|
+
return mask | 1;
|
|
1223
|
+
if (key === "ctrl" || key === "control")
|
|
1224
|
+
return mask | 2;
|
|
1225
|
+
if (key === "meta" || key === "cmd" || key === "command")
|
|
1226
|
+
return mask | 4;
|
|
1227
|
+
if (key === "shift")
|
|
1228
|
+
return mask | 8;
|
|
1229
|
+
return mask;
|
|
1230
|
+
}, 0);
|
|
1231
|
+
}
|
|
1232
|
+
keyDefinition(key) {
|
|
1233
|
+
const normalized = key.length === 1 ? key : key.toLowerCase();
|
|
1234
|
+
const special = {
|
|
1235
|
+
enter: { key: "Enter", code: "Enter", windowsVirtualKeyCode: 13 },
|
|
1236
|
+
tab: { key: "Tab", code: "Tab", windowsVirtualKeyCode: 9 },
|
|
1237
|
+
escape: { key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 },
|
|
1238
|
+
esc: { key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 },
|
|
1239
|
+
backspace: { key: "Backspace", code: "Backspace", windowsVirtualKeyCode: 8 },
|
|
1240
|
+
delete: { key: "Delete", code: "Delete", windowsVirtualKeyCode: 46 },
|
|
1241
|
+
arrowup: { key: "ArrowUp", code: "ArrowUp", windowsVirtualKeyCode: 38 },
|
|
1242
|
+
arrowdown: { key: "ArrowDown", code: "ArrowDown", windowsVirtualKeyCode: 40 },
|
|
1243
|
+
arrowleft: { key: "ArrowLeft", code: "ArrowLeft", windowsVirtualKeyCode: 37 },
|
|
1244
|
+
arrowright: { key: "ArrowRight", code: "ArrowRight", windowsVirtualKeyCode: 39 },
|
|
1245
|
+
home: { key: "Home", code: "Home", windowsVirtualKeyCode: 36 },
|
|
1246
|
+
end: { key: "End", code: "End", windowsVirtualKeyCode: 35 },
|
|
1247
|
+
pageup: { key: "PageUp", code: "PageUp", windowsVirtualKeyCode: 33 },
|
|
1248
|
+
pagedown: { key: "PageDown", code: "PageDown", windowsVirtualKeyCode: 34 },
|
|
1249
|
+
};
|
|
1250
|
+
if (special[normalized])
|
|
1251
|
+
return special[normalized];
|
|
1252
|
+
const upper = key.toUpperCase();
|
|
1253
|
+
const code = /^[A-Z]$/.test(upper) ? `Key${upper}` : /^[0-9]$/.test(key) ? `Digit${key}` : key;
|
|
1254
|
+
return { key, code, windowsVirtualKeyCode: upper.charCodeAt(0) };
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Enable events
|
|
1258
|
+
*/
|
|
1259
|
+
async enableEvents(events) {
|
|
1260
|
+
for (const event of events) {
|
|
1261
|
+
try {
|
|
1262
|
+
await this.sendCommand(event, {});
|
|
1263
|
+
}
|
|
1264
|
+
catch {
|
|
1265
|
+
// Some enable commands have different params
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
on(event, listener) {
|
|
1270
|
+
if (!this.eventListeners.has(event)) {
|
|
1271
|
+
this.eventListeners.set(event, []);
|
|
1272
|
+
}
|
|
1273
|
+
this.eventListeners.get(event).push(listener);
|
|
1274
|
+
}
|
|
1275
|
+
emitEvent(method, params) {
|
|
1276
|
+
const listeners = this.eventListeners.get(method) || [];
|
|
1277
|
+
for (const listener of listeners) {
|
|
1278
|
+
try {
|
|
1279
|
+
listener(params);
|
|
1280
|
+
}
|
|
1281
|
+
catch (e) {
|
|
1282
|
+
console.error(`[CDP] Event listener error for ${method}:`, e);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
scheduleReconnect() {
|
|
1287
|
+
if (this.reconnectTimer)
|
|
1288
|
+
return;
|
|
1289
|
+
this.reconnectTimer = setTimeout(() => {
|
|
1290
|
+
this.reconnectTimer = null;
|
|
1291
|
+
if (this.activeTarget) {
|
|
1292
|
+
console.error("[CDP] Attempting to reconnect...");
|
|
1293
|
+
this.attachToTarget(this.activeTarget).catch(() => { });
|
|
1294
|
+
}
|
|
1295
|
+
}, 2000);
|
|
1296
|
+
}
|
|
1297
|
+
async waitForChrome(port, timeoutMs = 10000) {
|
|
1298
|
+
const start = Date.now();
|
|
1299
|
+
while (Date.now() - start < timeoutMs) {
|
|
1300
|
+
try {
|
|
1301
|
+
await this.listTargets(port);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
catch {
|
|
1305
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
throw new Error(`Chrome did not start on port ${port} within ${timeoutMs}ms`);
|
|
1309
|
+
}
|
|
1310
|
+
getBrowserPaths() {
|
|
1311
|
+
const platform = os_1.default.platform();
|
|
1312
|
+
const chromePaths = [];
|
|
1313
|
+
const edgePaths = [];
|
|
1314
|
+
const bravePaths = [];
|
|
1315
|
+
const firefoxPaths = [];
|
|
1316
|
+
if (platform === "win32") {
|
|
1317
|
+
chromePaths.push("C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe");
|
|
1318
|
+
edgePaths.push("C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe");
|
|
1319
|
+
bravePaths.push("C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe", "C:\\Program Files (x86)\\BraveSoftware\\Brave-Browser\\Application\\brave.exe");
|
|
1320
|
+
firefoxPaths.push("C:\\Program Files\\Mozilla Firefox\\firefox.exe", "C:\\Program Files (x86)\\Mozilla Firefox\\firefox.exe");
|
|
1321
|
+
}
|
|
1322
|
+
else if (platform === "darwin") {
|
|
1323
|
+
chromePaths.push("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Chromium.app/Contents/MacOS/Chromium");
|
|
1324
|
+
edgePaths.push("/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge");
|
|
1325
|
+
bravePaths.push("/Applications/Brave Browser.app/Contents/MacOS/Brave Browser");
|
|
1326
|
+
firefoxPaths.push("/Applications/Firefox.app/Contents/MacOS/firefox");
|
|
1327
|
+
}
|
|
1328
|
+
else {
|
|
1329
|
+
chromePaths.push("/usr/bin/google-chrome", "/usr/bin/chromium", "/usr/bin/chromium-browser", "/snap/bin/chromium");
|
|
1330
|
+
edgePaths.push("/usr/bin/microsoft-edge", "/snap/bin/microsoft-edge");
|
|
1331
|
+
bravePaths.push("/usr/bin/brave-browser", "/snap/bin/brave");
|
|
1332
|
+
firefoxPaths.push("/usr/bin/firefox", "/snap/bin/firefox");
|
|
1333
|
+
}
|
|
1334
|
+
return [
|
|
1335
|
+
{ name: "chrome", paths: chromePaths },
|
|
1336
|
+
{ name: "edge", paths: edgePaths },
|
|
1337
|
+
{ name: "brave", paths: bravePaths },
|
|
1338
|
+
{ name: "firefox", paths: firefoxPaths },
|
|
1339
|
+
];
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Find first available browser from installed browsers
|
|
1343
|
+
*/
|
|
1344
|
+
async findAvailableBrowser() {
|
|
1345
|
+
const browsers = this.getBrowserPaths();
|
|
1346
|
+
for (const browser of browsers) {
|
|
1347
|
+
for (const p of browser.paths) {
|
|
1348
|
+
try {
|
|
1349
|
+
await fs_1.promises.access(p);
|
|
1350
|
+
return { name: browser.name, path: p };
|
|
1351
|
+
}
|
|
1352
|
+
catch { }
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return null;
|
|
1356
|
+
}
|
|
1357
|
+
getDefaultUserDataDir(browser) {
|
|
1358
|
+
const platform = os_1.default.platform();
|
|
1359
|
+
if (platform === "win32") {
|
|
1360
|
+
const local = process.env.LOCALAPPDATA;
|
|
1361
|
+
if (!local)
|
|
1362
|
+
return null;
|
|
1363
|
+
if (browser === "brave")
|
|
1364
|
+
return path_1.default.join(local, "BraveSoftware", "Brave-Browser", "User Data");
|
|
1365
|
+
if (browser === "chrome")
|
|
1366
|
+
return path_1.default.join(local, "Google", "Chrome", "User Data");
|
|
1367
|
+
if (browser === "edge")
|
|
1368
|
+
return path_1.default.join(local, "Microsoft", "Edge", "User Data");
|
|
1369
|
+
return null;
|
|
1370
|
+
}
|
|
1371
|
+
if (platform === "darwin") {
|
|
1372
|
+
const home = os_1.default.homedir();
|
|
1373
|
+
if (browser === "brave")
|
|
1374
|
+
return path_1.default.join(home, "Library", "Application Support", "BraveSoftware", "Brave-Browser");
|
|
1375
|
+
if (browser === "chrome")
|
|
1376
|
+
return path_1.default.join(home, "Library", "Application Support", "Google", "Chrome");
|
|
1377
|
+
if (browser === "edge")
|
|
1378
|
+
return path_1.default.join(home, "Library", "Application Support", "Microsoft Edge");
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
1381
|
+
const config = process.env.XDG_CONFIG_HOME || path_1.default.join(os_1.default.homedir(), ".config");
|
|
1382
|
+
if (browser === "brave")
|
|
1383
|
+
return path_1.default.join(config, "BraveSoftware", "Brave-Browser");
|
|
1384
|
+
if (browser === "chrome")
|
|
1385
|
+
return path_1.default.join(config, "google-chrome");
|
|
1386
|
+
if (browser === "edge")
|
|
1387
|
+
return path_1.default.join(config, "microsoft-edge");
|
|
1388
|
+
return null;
|
|
1389
|
+
}
|
|
1390
|
+
async listBrowserProfiles(browser = "brave") {
|
|
1391
|
+
const userDataDir = this.getDefaultUserDataDir(browser);
|
|
1392
|
+
if (!userDataDir)
|
|
1393
|
+
return [];
|
|
1394
|
+
const localStatePath = path_1.default.join(userDataDir, "Local State");
|
|
1395
|
+
let localState;
|
|
1396
|
+
try {
|
|
1397
|
+
localState = JSON.parse(await fs_1.promises.readFile(localStatePath, "utf8"));
|
|
1398
|
+
}
|
|
1399
|
+
catch {
|
|
1400
|
+
return [];
|
|
1401
|
+
}
|
|
1402
|
+
const infoCache = localState?.profile?.info_cache || {};
|
|
1403
|
+
const ordered = localState?.profile?.profiles_order || Object.keys(infoCache);
|
|
1404
|
+
const profiles = [];
|
|
1405
|
+
for (const directory of ordered) {
|
|
1406
|
+
const cache = infoCache[directory] || {};
|
|
1407
|
+
const profilePath = path_1.default.join(userDataDir, directory);
|
|
1408
|
+
try {
|
|
1409
|
+
const stat = await fs_1.promises.stat(profilePath);
|
|
1410
|
+
if (!stat.isDirectory())
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1413
|
+
catch {
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
const name = cache.name || cache.shortcut_name || directory;
|
|
1417
|
+
profiles.push({
|
|
1418
|
+
browser,
|
|
1419
|
+
id: `${browser}:${directory}`,
|
|
1420
|
+
name,
|
|
1421
|
+
directory,
|
|
1422
|
+
userDataDir,
|
|
1423
|
+
lastActive: typeof cache.active_time === "number" ? cache.active_time : undefined,
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
return profiles;
|
|
1427
|
+
}
|
|
1428
|
+
async resolveBrowserProfile(options) {
|
|
1429
|
+
if (options?.profileDirectory) {
|
|
1430
|
+
return {
|
|
1431
|
+
userDataDir: options.userDataDir,
|
|
1432
|
+
profileDirectory: options.profileDirectory,
|
|
1433
|
+
profileName: options.profileDirectory,
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
const profile = options?.profile?.trim();
|
|
1437
|
+
if (!profile) {
|
|
1438
|
+
return { userDataDir: options?.userDataDir };
|
|
1439
|
+
}
|
|
1440
|
+
const browser = options?.browser || "brave";
|
|
1441
|
+
const profiles = await this.listBrowserProfiles(browser);
|
|
1442
|
+
const normalized = profile.toLowerCase();
|
|
1443
|
+
const match = profiles.find((p) => p.directory.toLowerCase() === normalized ||
|
|
1444
|
+
p.name.toLowerCase() === normalized ||
|
|
1445
|
+
p.id.toLowerCase() === `${browser}:${normalized}`);
|
|
1446
|
+
if (!match) {
|
|
1447
|
+
const available = profiles.map((p) => `${p.name} (${p.directory})`).join(", ") || "none found";
|
|
1448
|
+
throw new Error(`Profile "${profile}" not found for ${browser}. Available profiles: ${available}`);
|
|
1449
|
+
}
|
|
1450
|
+
return {
|
|
1451
|
+
userDataDir: options?.userDataDir || match.userDataDir,
|
|
1452
|
+
profileDirectory: match.directory,
|
|
1453
|
+
profileName: match.name,
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Launch browser with auto-detection
|
|
1458
|
+
*/
|
|
1459
|
+
async launchAuto(options) {
|
|
1460
|
+
const port = options?.port ?? 9222;
|
|
1461
|
+
let browserPath = null;
|
|
1462
|
+
let browserName = options?.browser;
|
|
1463
|
+
if (browserName) {
|
|
1464
|
+
// User specified a browser - find its path
|
|
1465
|
+
const browsers = this.getBrowserPaths();
|
|
1466
|
+
const browser = browsers.find(b => b.name === browserName);
|
|
1467
|
+
if (browser) {
|
|
1468
|
+
for (const p of browser.paths) {
|
|
1469
|
+
try {
|
|
1470
|
+
await fs_1.promises.access(p);
|
|
1471
|
+
browserPath = p;
|
|
1472
|
+
break;
|
|
1473
|
+
}
|
|
1474
|
+
catch { }
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
if (!browserPath) {
|
|
1478
|
+
throw new Error(`${browserName} not found. Please install it or choose a different browser.`);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
else {
|
|
1482
|
+
// Auto-detect first available browser
|
|
1483
|
+
const found = await this.findAvailableBrowser();
|
|
1484
|
+
if (!found) {
|
|
1485
|
+
throw new Error("No supported browser found. Please install Chrome, Edge, Brave, or Firefox.\n" +
|
|
1486
|
+
"Or specify a browser path manually.");
|
|
1487
|
+
}
|
|
1488
|
+
browserPath = found.path;
|
|
1489
|
+
browserName = found.name;
|
|
1490
|
+
console.error(`[CDP] Auto-detected browser: ${browserName}`);
|
|
1491
|
+
}
|
|
1492
|
+
const profile = await this.resolveBrowserProfile({
|
|
1493
|
+
browser: browserName,
|
|
1494
|
+
profile: options?.profile,
|
|
1495
|
+
profileDirectory: options?.profileDirectory,
|
|
1496
|
+
userDataDir: options?.userDataDir,
|
|
1497
|
+
});
|
|
1498
|
+
const userDataDir = profile.userDataDir ?? path_1.default.join(os_1.default.tmpdir(), `aether-${browserName}-profile`);
|
|
1499
|
+
const headless = options?.headless ?? false;
|
|
1500
|
+
const args = [];
|
|
1501
|
+
// Firefox doesn't support CDP the same way - use remote debugging
|
|
1502
|
+
if (browserName === "firefox") {
|
|
1503
|
+
args.push("--remote-debugging-port", port.toString(), "--profile", userDataDir, ...(headless ? ["--headless"] : []), ...(options?.extraArgs || []), "about:blank");
|
|
1504
|
+
}
|
|
1505
|
+
else {
|
|
1506
|
+
// Chromium-based browsers (Chrome, Edge, Brave)
|
|
1507
|
+
args.push(`--remote-debugging-port=${port}`, `--user-data-dir=${userDataDir}`, ...(profile.profileDirectory ? [`--profile-directory=${profile.profileDirectory}`] : []), "--disable-infobars", ...(headless ? ["--headless", "--disable-gpu"] : []), ...(options?.extraArgs || []), "about:blank");
|
|
1508
|
+
}
|
|
1509
|
+
this.chromeProcess = (0, child_process_1.spawn)(browserPath, args, {
|
|
1510
|
+
detached: false,
|
|
1511
|
+
stdio: "ignore",
|
|
1512
|
+
});
|
|
1513
|
+
this.chromeProcess.on("exit", () => {
|
|
1514
|
+
this.chromeProcess = null;
|
|
1515
|
+
this.connected = false;
|
|
1516
|
+
});
|
|
1517
|
+
// Wait for browser to start
|
|
1518
|
+
await this.waitForChrome(port);
|
|
1519
|
+
await this.connect(port);
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Kill the browser process
|
|
1523
|
+
*/
|
|
1524
|
+
async killBrowser() {
|
|
1525
|
+
if (this.chromeProcess) {
|
|
1526
|
+
console.error("[CDP] Killing browser process...");
|
|
1527
|
+
try {
|
|
1528
|
+
if (os_1.default.platform() === "win32") {
|
|
1529
|
+
(0, child_process_1.spawn)("taskkill", ["/pid", this.chromeProcess.pid.toString(), "/f", "/t"]);
|
|
1530
|
+
}
|
|
1531
|
+
else {
|
|
1532
|
+
this.chromeProcess.kill("SIGKILL");
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
catch (e) {
|
|
1536
|
+
console.error("[CDP] Error killing browser:", e);
|
|
1537
|
+
}
|
|
1538
|
+
this.chromeProcess = null;
|
|
1539
|
+
}
|
|
1540
|
+
await this.disconnect();
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* List all available browsers on the system
|
|
1544
|
+
*/
|
|
1545
|
+
async listAvailableBrowsers() {
|
|
1546
|
+
const browsers = this.getBrowserPaths();
|
|
1547
|
+
const available = [];
|
|
1548
|
+
for (const browser of browsers) {
|
|
1549
|
+
for (const p of browser.paths) {
|
|
1550
|
+
try {
|
|
1551
|
+
await fs_1.promises.access(p);
|
|
1552
|
+
available.push({ name: browser.name, path: p });
|
|
1553
|
+
break; // Found one path for this browser
|
|
1554
|
+
}
|
|
1555
|
+
catch { }
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
return available;
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Disconnect and cleanup
|
|
1562
|
+
*/
|
|
1563
|
+
async disconnect() {
|
|
1564
|
+
if (this.reconnectTimer) {
|
|
1565
|
+
clearTimeout(this.reconnectTimer);
|
|
1566
|
+
this.reconnectTimer = null;
|
|
1567
|
+
}
|
|
1568
|
+
if (this.ws) {
|
|
1569
|
+
this.intentionalClose = true;
|
|
1570
|
+
this.ws.close();
|
|
1571
|
+
this.ws = null;
|
|
1572
|
+
}
|
|
1573
|
+
if (this.chromeProcess) {
|
|
1574
|
+
this.chromeProcess.kill();
|
|
1575
|
+
this.chromeProcess = null;
|
|
1576
|
+
}
|
|
1577
|
+
this.connected = false;
|
|
1578
|
+
this.activeTarget = null;
|
|
1579
|
+
}
|
|
1580
|
+
isConnected() {
|
|
1581
|
+
return this.connected && this.ws?.readyState === ws_1.default.OPEN;
|
|
1582
|
+
}
|
|
1583
|
+
getActiveTarget() {
|
|
1584
|
+
return this.activeTarget;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
exports.CdpClient = CdpClient;
|
|
1588
|
+
// Singleton instance
|
|
1589
|
+
let cdpClient = null;
|
|
1590
|
+
function getCdpClient() {
|
|
1591
|
+
if (!cdpClient) {
|
|
1592
|
+
cdpClient = new CdpClient();
|
|
1593
|
+
}
|
|
1594
|
+
return cdpClient;
|
|
1595
|
+
}
|
|
1596
|
+
async function ensureCdpConnected(port) {
|
|
1597
|
+
const client = getCdpClient();
|
|
1598
|
+
if (!client.isConnected()) {
|
|
1599
|
+
await client.connect(port);
|
|
1600
|
+
}
|
|
1601
|
+
return client;
|
|
1602
|
+
}
|