ornold-mcp 1.0.0 → 1.0.2
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/cli.js +2788 -0
- package/package.json +18 -17
- package/README.md +0 -27
- package/dist/index.js +0 -2
- package/executor/concurrency.ts +0 -118
- package/executor/human-like.ts +0 -523
- package/executor/multi-browser.ts +0 -1960
- package/executor/snapshot-helpers.ts +0 -88
- package/executor/types.ts +0 -128
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2788 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/executor/concurrency.ts
|
|
13
|
+
import * as os from "os";
|
|
14
|
+
function detectOptimalConcurrency(browserCount = 0) {
|
|
15
|
+
const cpuCores = os.cpus().length;
|
|
16
|
+
const totalMemGB = os.totalmem() / 1024 ** 3;
|
|
17
|
+
const freeMemGB = os.freemem() / 1024 ** 3;
|
|
18
|
+
const memPerHeavyOp = 0.5;
|
|
19
|
+
const memPerLightOp = 0.2;
|
|
20
|
+
const ramBasedHeavy = Math.floor(freeMemGB / memPerHeavyOp);
|
|
21
|
+
const ramBasedLight = Math.floor(freeMemGB / memPerLightOp);
|
|
22
|
+
const cpuBasedDefault = Math.max(2, Math.floor(cpuCores / 2));
|
|
23
|
+
const cpuBasedLight = Math.max(3, cpuCores);
|
|
24
|
+
const heavyConcurrency = Math.max(2, Math.min(ramBasedHeavy, cpuBasedDefault));
|
|
25
|
+
const lightConcurrency = Math.max(3, Math.min(ramBasedLight, cpuBasedLight));
|
|
26
|
+
const defaultConcurrency = Math.max(2, Math.floor((heavyConcurrency + lightConcurrency) / 2));
|
|
27
|
+
const effectiveHeavy = browserCount > 0 ? Math.min(heavyConcurrency, browserCount) : heavyConcurrency;
|
|
28
|
+
const effectiveLight = browserCount > 0 ? Math.min(lightConcurrency, browserCount) : lightConcurrency;
|
|
29
|
+
const effectiveDefault = browserCount > 0 ? Math.min(defaultConcurrency, browserCount) : defaultConcurrency;
|
|
30
|
+
console.error(`[Concurrency] System: ${cpuCores} cores, ${totalMemGB.toFixed(1)}GB total RAM, ${freeMemGB.toFixed(1)}GB free`);
|
|
31
|
+
console.error(`[Concurrency] Detected limits: default=${effectiveDefault}, heavy=${effectiveHeavy}, light=${effectiveLight}`);
|
|
32
|
+
return {
|
|
33
|
+
default: effectiveDefault,
|
|
34
|
+
heavy: effectiveHeavy,
|
|
35
|
+
light: effectiveLight
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
var ConcurrencyLimiter;
|
|
39
|
+
var init_concurrency = __esm({
|
|
40
|
+
"src/executor/concurrency.ts"() {
|
|
41
|
+
"use strict";
|
|
42
|
+
ConcurrencyLimiter = class {
|
|
43
|
+
constructor(maxConcurrent) {
|
|
44
|
+
this.maxConcurrent = maxConcurrent;
|
|
45
|
+
if (maxConcurrent < 1) {
|
|
46
|
+
throw new Error("maxConcurrent must be at least 1");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
maxConcurrent;
|
|
50
|
+
running = 0;
|
|
51
|
+
queue = [];
|
|
52
|
+
get limit() {
|
|
53
|
+
return this.maxConcurrent;
|
|
54
|
+
}
|
|
55
|
+
get active() {
|
|
56
|
+
return this.running;
|
|
57
|
+
}
|
|
58
|
+
get pending() {
|
|
59
|
+
return this.queue.length;
|
|
60
|
+
}
|
|
61
|
+
async acquire() {
|
|
62
|
+
if (this.running < this.maxConcurrent) {
|
|
63
|
+
this.running++;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
this.queue.push(() => {
|
|
68
|
+
this.running++;
|
|
69
|
+
resolve();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
release() {
|
|
74
|
+
if (this.running <= 0) {
|
|
75
|
+
console.error("[ConcurrencyLimiter] Warning: release() called when running <= 0");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this.running--;
|
|
79
|
+
const next = this.queue.shift();
|
|
80
|
+
if (next) next();
|
|
81
|
+
}
|
|
82
|
+
async run(fn) {
|
|
83
|
+
await this.acquire();
|
|
84
|
+
try {
|
|
85
|
+
return await fn();
|
|
86
|
+
} finally {
|
|
87
|
+
this.release();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// src/executor/human-like.ts
|
|
95
|
+
import * as fs from "fs";
|
|
96
|
+
import * as path from "path";
|
|
97
|
+
function debugLog(category, message, data) {
|
|
98
|
+
try {
|
|
99
|
+
const dir = path.dirname(LOG_FILE);
|
|
100
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
101
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
102
|
+
let line = `[${ts}] [${category}] ${message}`;
|
|
103
|
+
if (data !== void 0) {
|
|
104
|
+
line += ` | ${JSON.stringify(data)}`;
|
|
105
|
+
}
|
|
106
|
+
fs.appendFileSync(LOG_FILE, line + "\n");
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function gaussianRandom(mean, stddev) {
|
|
111
|
+
const u1 = Math.random();
|
|
112
|
+
const u2 = Math.random();
|
|
113
|
+
const z2 = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
|
114
|
+
return mean + z2 * stddev;
|
|
115
|
+
}
|
|
116
|
+
function clampedRandom(min, max) {
|
|
117
|
+
return Math.min(max, Math.max(min, min + Math.random() * (max - min)));
|
|
118
|
+
}
|
|
119
|
+
function clampedGaussian(mean, stddev, min, max) {
|
|
120
|
+
return Math.min(max, Math.max(min, gaussianRandom(mean, stddev)));
|
|
121
|
+
}
|
|
122
|
+
function sleep(ms) {
|
|
123
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
124
|
+
}
|
|
125
|
+
function generateProfile(browserId) {
|
|
126
|
+
let hash = 0;
|
|
127
|
+
for (let i = 0; i < browserId.length; i++) {
|
|
128
|
+
hash = (hash << 5) - hash + browserId.charCodeAt(i) | 0;
|
|
129
|
+
}
|
|
130
|
+
const seed = Math.abs(hash);
|
|
131
|
+
return {
|
|
132
|
+
typingSpeed: 0.8 + seed % 100 / 100 * 0.4,
|
|
133
|
+
// 0.8 - 1.2
|
|
134
|
+
mouseSpeed: 0.85 + (seed >> 8) % 100 / 100 * 0.3,
|
|
135
|
+
// 0.85 - 1.15
|
|
136
|
+
actionDelay: 0.7 + (seed >> 16) % 100 / 100 * 0.6
|
|
137
|
+
// 0.7 - 1.3
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async function preActionDelay(profile) {
|
|
141
|
+
const base = clampedGaussian(250, 80, 100, 400);
|
|
142
|
+
const ms = profile ? base * profile.actionDelay : base;
|
|
143
|
+
await sleep(ms);
|
|
144
|
+
}
|
|
145
|
+
function typingDelay(profile) {
|
|
146
|
+
if (Math.random() < 0.05) {
|
|
147
|
+
const pause = clampedGaussian(350, 100, 200, 600);
|
|
148
|
+
return profile ? pause * profile.typingSpeed : pause;
|
|
149
|
+
}
|
|
150
|
+
const base = clampedGaussian(80, 30, 40, 180);
|
|
151
|
+
return profile ? base * profile.typingSpeed : base;
|
|
152
|
+
}
|
|
153
|
+
async function humanDelay(min, max) {
|
|
154
|
+
await sleep(clampedRandom(min, max));
|
|
155
|
+
}
|
|
156
|
+
function cubicBezier(t, p0, p1, p2, p3) {
|
|
157
|
+
const t2 = t * t;
|
|
158
|
+
const t3 = t2 * t;
|
|
159
|
+
const mt = 1 - t;
|
|
160
|
+
const mt2 = mt * mt;
|
|
161
|
+
const mt3 = mt2 * mt;
|
|
162
|
+
return mt3 * p0 + 3 * mt2 * t * p1 + 3 * mt * t2 * p2 + t3 * p3;
|
|
163
|
+
}
|
|
164
|
+
function generateBezierPath(from, to, steps = 20) {
|
|
165
|
+
const dx = to.x - from.x;
|
|
166
|
+
const dy = to.y - from.y;
|
|
167
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
168
|
+
const spreadX = distance * 0.15;
|
|
169
|
+
const spreadY = distance * 0.15;
|
|
170
|
+
const cp1 = {
|
|
171
|
+
x: from.x + dx * 0.25 + (Math.random() - 0.5) * spreadX,
|
|
172
|
+
y: from.y + dy * 0.25 + (Math.random() - 0.5) * spreadY
|
|
173
|
+
};
|
|
174
|
+
const cp2 = {
|
|
175
|
+
x: from.x + dx * 0.75 + (Math.random() - 0.5) * spreadX,
|
|
176
|
+
y: from.y + dy * 0.75 + (Math.random() - 0.5) * spreadY
|
|
177
|
+
};
|
|
178
|
+
const actualSteps = Math.max(10, Math.min(40, Math.round(steps * (distance / 500))));
|
|
179
|
+
const points = [];
|
|
180
|
+
for (let i = 0; i <= actualSteps; i++) {
|
|
181
|
+
const t = i / actualSteps;
|
|
182
|
+
const eased = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
183
|
+
points.push({
|
|
184
|
+
x: Math.round(cubicBezier(eased, from.x, cp1.x, cp2.x, to.x)),
|
|
185
|
+
y: Math.round(cubicBezier(eased, from.y, cp1.y, cp2.y, to.y))
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return points;
|
|
189
|
+
}
|
|
190
|
+
async function humanMouseMove(page, from, to, profile) {
|
|
191
|
+
debugLog("MOUSE_MOVE", `from=(${from.x},${from.y}) to=(${to.x},${to.y})`);
|
|
192
|
+
const path2 = generateBezierPath(from, to);
|
|
193
|
+
const speedMult = profile?.mouseSpeed ?? 1;
|
|
194
|
+
debugLog("MOUSE_MOVE", `bezier path generated: ${path2.length} steps, speedMult=${speedMult.toFixed(2)}`);
|
|
195
|
+
let cdpSession = null;
|
|
196
|
+
try {
|
|
197
|
+
cdpSession = await page.context().newCDPSession(page);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
debugLog("MOUSE_MOVE", `CDP session creation failed, cursor won't be visible in screencast`, String(err));
|
|
200
|
+
}
|
|
201
|
+
for (let i = 0; i < path2.length; i++) {
|
|
202
|
+
const point = path2[i];
|
|
203
|
+
const delay = clampedRandom(5, 15) * speedMult;
|
|
204
|
+
try {
|
|
205
|
+
await page.mouse.move(point.x, point.y);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
debugLog("MOUSE_MOVE", `ERROR at step ${i}/${path2.length}: page.mouse.move(${point.x},${point.y}) failed`, String(err));
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
if (cdpSession) {
|
|
211
|
+
try {
|
|
212
|
+
await cdpSession.send("Runtime.evaluate", {
|
|
213
|
+
expression: `window.__cpSrv = {x:${Math.round(point.x)},y:${Math.round(point.y)}}`,
|
|
214
|
+
returnByValue: false
|
|
215
|
+
// no need to read back on every step
|
|
216
|
+
});
|
|
217
|
+
} catch (err) {
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
await sleep(delay);
|
|
221
|
+
}
|
|
222
|
+
if (cdpSession) {
|
|
223
|
+
try {
|
|
224
|
+
await cdpSession.detach();
|
|
225
|
+
} catch (err) {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
debugLog("MOUSE_MOVE", `movement complete at (${to.x},${to.y})`);
|
|
229
|
+
console.log(`[HumanLike] Mouse moved smoothly to (${to.x},${to.y}), ${path2.length} steps`);
|
|
230
|
+
await showCursorOverlay(page, to.x, to.y);
|
|
231
|
+
}
|
|
232
|
+
function clickOffset(width, height) {
|
|
233
|
+
const maxOffX = Math.min(8, width * 0.15);
|
|
234
|
+
const maxOffY = Math.min(5, height * 0.15);
|
|
235
|
+
return {
|
|
236
|
+
x: clampedGaussian(0, maxOffX * 0.5, -maxOffX, maxOffX),
|
|
237
|
+
y: clampedGaussian(0, maxOffY * 0.5, -maxOffY, maxOffY)
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
async function humanClick(page, target, fromPos, profile) {
|
|
241
|
+
debugLog("CLICK", `target=(${target.x},${target.y}) from=(${fromPos.x},${fromPos.y})`);
|
|
242
|
+
await humanMouseMove(page, fromPos, target, profile);
|
|
243
|
+
debugLog("CLICK", `mouse.down()`);
|
|
244
|
+
await page.mouse.down();
|
|
245
|
+
const holdDelay = clampedRandom(50, 120);
|
|
246
|
+
debugLog("CLICK", `holding for ${holdDelay.toFixed(0)}ms`);
|
|
247
|
+
await sleep(holdDelay);
|
|
248
|
+
debugLog("CLICK", `mouse.up()`);
|
|
249
|
+
await page.mouse.up();
|
|
250
|
+
debugLog("CLICK", `click complete`);
|
|
251
|
+
}
|
|
252
|
+
async function getClickTarget(page, locator) {
|
|
253
|
+
debugLog("GET_TARGET", `getting bounding box for locator`);
|
|
254
|
+
const box = await locator.boundingBox();
|
|
255
|
+
if (!box) {
|
|
256
|
+
debugLog("GET_TARGET", `ERROR: no bounding box returned (element not visible)`);
|
|
257
|
+
throw new Error("Element is not visible \u2014 no bounding box");
|
|
258
|
+
}
|
|
259
|
+
debugLog("GET_TARGET", `boundingBox: x=${box.x} y=${box.y} w=${box.width} h=${box.height}`);
|
|
260
|
+
const offset = clickOffset(box.width, box.height);
|
|
261
|
+
const target = {
|
|
262
|
+
x: Math.round(box.x + box.width / 2 + offset.x),
|
|
263
|
+
y: Math.round(box.y + box.height / 2 + offset.y)
|
|
264
|
+
};
|
|
265
|
+
debugLog("GET_TARGET", `center=(${Math.round(box.x + box.width / 2)},${Math.round(box.y + box.height / 2)}) offset=(${offset.x.toFixed(1)},${offset.y.toFixed(1)}) final=(${target.x},${target.y})`);
|
|
266
|
+
return {
|
|
267
|
+
target,
|
|
268
|
+
box: { width: box.width, height: box.height }
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
async function cdpTypeChar(cdp, char) {
|
|
272
|
+
const info = KEY_CODES[char];
|
|
273
|
+
const code = info?.code ?? "";
|
|
274
|
+
const keyCode = info?.keyCode ?? char.charCodeAt(0);
|
|
275
|
+
debugLog("TYPE_CHAR", `char="${char}" code=${code} keyCode=${keyCode} (has mapping: ${!!info})`);
|
|
276
|
+
try {
|
|
277
|
+
await cdp.send("Input.dispatchKeyEvent", {
|
|
278
|
+
type: "keyDown",
|
|
279
|
+
key: char,
|
|
280
|
+
code,
|
|
281
|
+
windowsVirtualKeyCode: keyCode,
|
|
282
|
+
nativeVirtualKeyCode: keyCode
|
|
283
|
+
});
|
|
284
|
+
debugLog("TYPE_CHAR", `keyDown sent OK`);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
debugLog("TYPE_CHAR", `keyDown FAILED`, String(err));
|
|
287
|
+
throw err;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
await cdp.send("Input.dispatchKeyEvent", {
|
|
291
|
+
type: "char",
|
|
292
|
+
text: char,
|
|
293
|
+
unmodifiedText: char
|
|
294
|
+
});
|
|
295
|
+
debugLog("TYPE_CHAR", `char event sent OK`);
|
|
296
|
+
} catch (err) {
|
|
297
|
+
debugLog("TYPE_CHAR", `char event FAILED`, String(err));
|
|
298
|
+
throw err;
|
|
299
|
+
}
|
|
300
|
+
const pressDelay = clampedRandom(20, 60);
|
|
301
|
+
await sleep(pressDelay);
|
|
302
|
+
try {
|
|
303
|
+
await cdp.send("Input.dispatchKeyEvent", {
|
|
304
|
+
type: "keyUp",
|
|
305
|
+
key: char,
|
|
306
|
+
code,
|
|
307
|
+
windowsVirtualKeyCode: keyCode,
|
|
308
|
+
nativeVirtualKeyCode: keyCode
|
|
309
|
+
});
|
|
310
|
+
debugLog("TYPE_CHAR", `keyUp sent OK (pressDelay=${pressDelay.toFixed(0)}ms)`);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
debugLog("TYPE_CHAR", `keyUp FAILED`, String(err));
|
|
313
|
+
throw err;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async function humanTypeText(page, text, profile) {
|
|
317
|
+
debugLog("TYPE_TEXT", `typing "${text}" (${text.length} chars)`);
|
|
318
|
+
let cdp;
|
|
319
|
+
try {
|
|
320
|
+
cdp = await page.context().newCDPSession(page);
|
|
321
|
+
debugLog("TYPE_TEXT", `CDP session created for typing`);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
debugLog("TYPE_TEXT", `FAILED to create CDP session`, String(err));
|
|
324
|
+
throw err;
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
for (let i = 0; i < text.length; i++) {
|
|
328
|
+
const char = text[i];
|
|
329
|
+
await cdpTypeChar(cdp, char);
|
|
330
|
+
const delay = typingDelay(profile);
|
|
331
|
+
debugLog("TYPE_TEXT", `char ${i + 1}/${text.length} "${char}" typed, next delay=${delay.toFixed(0)}ms`);
|
|
332
|
+
await sleep(delay);
|
|
333
|
+
}
|
|
334
|
+
debugLog("TYPE_TEXT", `typing complete`);
|
|
335
|
+
} finally {
|
|
336
|
+
await cdp.detach().catch(() => {
|
|
337
|
+
});
|
|
338
|
+
debugLog("TYPE_TEXT", `CDP session detached`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async function getOverlaySession(page) {
|
|
342
|
+
let cdp = overlaySessions.get(page);
|
|
343
|
+
if (cdp) {
|
|
344
|
+
debugLog("OVERLAY", `reusing existing overlay session`);
|
|
345
|
+
return cdp;
|
|
346
|
+
}
|
|
347
|
+
debugLog("OVERLAY", `creating new overlay CDP session`);
|
|
348
|
+
cdp = await page.context().newCDPSession(page);
|
|
349
|
+
try {
|
|
350
|
+
await cdp.send("DOM.enable");
|
|
351
|
+
debugLog("OVERLAY", `DOM.enable SUCCESS`);
|
|
352
|
+
await cdp.send("Overlay.enable");
|
|
353
|
+
debugLog("OVERLAY", `Overlay.enable SUCCESS`);
|
|
354
|
+
} catch (err) {
|
|
355
|
+
debugLog("OVERLAY", `Overlay.enable FAILED`, String(err));
|
|
356
|
+
}
|
|
357
|
+
overlaySessions.set(page, cdp);
|
|
358
|
+
cdp.on("detached", () => {
|
|
359
|
+
debugLog("OVERLAY", `overlay session detached (cleanup)`);
|
|
360
|
+
overlaySessions.delete(page);
|
|
361
|
+
});
|
|
362
|
+
return cdp;
|
|
363
|
+
}
|
|
364
|
+
async function showCursorOverlay(page, x, y) {
|
|
365
|
+
debugLog("CURSOR", `showCursorOverlay called at (${x},${y})`);
|
|
366
|
+
try {
|
|
367
|
+
const cdp = await getOverlaySession(page);
|
|
368
|
+
const params = {
|
|
369
|
+
x: Math.round(x) - 4,
|
|
370
|
+
y: Math.round(y) - 4,
|
|
371
|
+
width: 8,
|
|
372
|
+
height: 8,
|
|
373
|
+
color: { r: 255, g: 69, b: 58, a: 0.7 },
|
|
374
|
+
outlineColor: { r: 255, g: 255, b: 255, a: 0.9 }
|
|
375
|
+
};
|
|
376
|
+
debugLog("CURSOR", `sending Overlay.highlightRect`, params);
|
|
377
|
+
await cdp.send("Overlay.highlightRect", params);
|
|
378
|
+
debugLog("CURSOR", `highlightRect SUCCESS`);
|
|
379
|
+
} catch (err) {
|
|
380
|
+
debugLog("CURSOR", `highlightRect FAILED (attempt 1), retrying...`, String(err));
|
|
381
|
+
overlaySessions.delete(page);
|
|
382
|
+
try {
|
|
383
|
+
const cdp = await getOverlaySession(page);
|
|
384
|
+
await cdp.send("Overlay.highlightRect", {
|
|
385
|
+
x: Math.round(x) - 4,
|
|
386
|
+
y: Math.round(y) - 4,
|
|
387
|
+
width: 8,
|
|
388
|
+
height: 8,
|
|
389
|
+
color: { r: 255, g: 69, b: 58, a: 0.7 },
|
|
390
|
+
outlineColor: { r: 255, g: 255, b: 255, a: 0.9 }
|
|
391
|
+
});
|
|
392
|
+
debugLog("CURSOR", `highlightRect SUCCESS (retry)`);
|
|
393
|
+
} catch (err2) {
|
|
394
|
+
debugLog("CURSOR", `highlightRect FAILED (retry), giving up`, String(err2));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async function ensureCursor(page) {
|
|
399
|
+
}
|
|
400
|
+
var LOG_FILE, KEY_CODES, SYMBOLS, overlaySessions;
|
|
401
|
+
var init_human_like = __esm({
|
|
402
|
+
"src/executor/human-like.ts"() {
|
|
403
|
+
"use strict";
|
|
404
|
+
LOG_FILE = path.join(process.cwd(), "logs", "human-like-debug.log");
|
|
405
|
+
KEY_CODES = {};
|
|
406
|
+
for (let i = 0; i < 26; i++) {
|
|
407
|
+
const lower = String.fromCharCode(97 + i);
|
|
408
|
+
const upper = String.fromCharCode(65 + i);
|
|
409
|
+
KEY_CODES[lower] = { keyCode: 65 + i, code: `Key${upper}`, key: lower };
|
|
410
|
+
KEY_CODES[upper] = { keyCode: 65 + i, code: `Key${upper}`, key: upper };
|
|
411
|
+
}
|
|
412
|
+
for (let i = 0; i < 10; i++) {
|
|
413
|
+
const ch = String(i);
|
|
414
|
+
KEY_CODES[ch] = { keyCode: 48 + i, code: `Digit${i}`, key: ch };
|
|
415
|
+
}
|
|
416
|
+
SYMBOLS = {
|
|
417
|
+
" ": { keyCode: 32, code: "Space" },
|
|
418
|
+
".": { keyCode: 190, code: "Period" },
|
|
419
|
+
",": { keyCode: 188, code: "Comma" },
|
|
420
|
+
"/": { keyCode: 191, code: "Slash" },
|
|
421
|
+
"\\": { keyCode: 220, code: "Backslash" },
|
|
422
|
+
"-": { keyCode: 189, code: "Minus" },
|
|
423
|
+
"=": { keyCode: 187, code: "Equal" },
|
|
424
|
+
"[": { keyCode: 219, code: "BracketLeft" },
|
|
425
|
+
"]": { keyCode: 221, code: "BracketRight" },
|
|
426
|
+
";": { keyCode: 186, code: "Semicolon" },
|
|
427
|
+
"'": { keyCode: 222, code: "Quote" },
|
|
428
|
+
"`": { keyCode: 192, code: "Backquote" },
|
|
429
|
+
"@": { keyCode: 50, code: "Digit2" },
|
|
430
|
+
"!": { keyCode: 49, code: "Digit1" },
|
|
431
|
+
"#": { keyCode: 51, code: "Digit3" },
|
|
432
|
+
"$": { keyCode: 52, code: "Digit4" },
|
|
433
|
+
"%": { keyCode: 53, code: "Digit5" },
|
|
434
|
+
"^": { keyCode: 54, code: "Digit6" },
|
|
435
|
+
"&": { keyCode: 55, code: "Digit7" },
|
|
436
|
+
"*": { keyCode: 56, code: "Digit8" },
|
|
437
|
+
"(": { keyCode: 57, code: "Digit9" },
|
|
438
|
+
")": { keyCode: 48, code: "Digit0" },
|
|
439
|
+
"_": { keyCode: 189, code: "Minus" },
|
|
440
|
+
"+": { keyCode: 187, code: "Equal" },
|
|
441
|
+
"{": { keyCode: 219, code: "BracketLeft" },
|
|
442
|
+
"}": { keyCode: 221, code: "BracketRight" },
|
|
443
|
+
":": { keyCode: 186, code: "Semicolon" },
|
|
444
|
+
'"': { keyCode: 222, code: "Quote" },
|
|
445
|
+
"~": { keyCode: 192, code: "Backquote" },
|
|
446
|
+
"<": { keyCode: 188, code: "Comma" },
|
|
447
|
+
">": { keyCode: 190, code: "Period" },
|
|
448
|
+
"?": { keyCode: 191, code: "Slash" },
|
|
449
|
+
"|": { keyCode: 220, code: "Backslash" },
|
|
450
|
+
"\n": { keyCode: 13, code: "Enter" },
|
|
451
|
+
" ": { keyCode: 9, code: "Tab" }
|
|
452
|
+
};
|
|
453
|
+
for (const [ch, info] of Object.entries(SYMBOLS)) {
|
|
454
|
+
KEY_CODES[ch] = { ...info, key: ch };
|
|
455
|
+
}
|
|
456
|
+
overlaySessions = /* @__PURE__ */ new WeakMap();
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// src/executor/multi-browser.ts
|
|
461
|
+
var multi_browser_exports = {};
|
|
462
|
+
__export(multi_browser_exports, {
|
|
463
|
+
MultiBrowserExecutor: () => MultiBrowserExecutor
|
|
464
|
+
});
|
|
465
|
+
import * as playwright from "patchright";
|
|
466
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
467
|
+
import { join as join2, extname } from "node:path";
|
|
468
|
+
var MultiBrowserExecutor;
|
|
469
|
+
var init_multi_browser = __esm({
|
|
470
|
+
"src/executor/multi-browser.ts"() {
|
|
471
|
+
"use strict";
|
|
472
|
+
init_concurrency();
|
|
473
|
+
init_human_like();
|
|
474
|
+
MultiBrowserExecutor = class _MultiBrowserExecutor {
|
|
475
|
+
browsers = /* @__PURE__ */ new Map();
|
|
476
|
+
defaultTimeout;
|
|
477
|
+
limiter;
|
|
478
|
+
heavyLimiter;
|
|
479
|
+
lightLimiter;
|
|
480
|
+
// Cache for browser sync - prevents excessive API calls
|
|
481
|
+
lastSyncTime = 0;
|
|
482
|
+
syncInProgress = null;
|
|
483
|
+
static SYNC_TTL_MS = 5e3;
|
|
484
|
+
// 5 seconds cache TTL
|
|
485
|
+
// Track last mouse position per browser for human-like movement
|
|
486
|
+
lastMousePosition = /* @__PURE__ */ new Map();
|
|
487
|
+
// Per-browser human profiles for varied timing
|
|
488
|
+
profiles = /* @__PURE__ */ new Map();
|
|
489
|
+
// Snapshot refs map back to selectors for flow-agent style browser tools
|
|
490
|
+
snapshotRefs = /* @__PURE__ */ new Map();
|
|
491
|
+
// Console messages collected per browser (since connect time)
|
|
492
|
+
consoleMessages = /* @__PURE__ */ new Map();
|
|
493
|
+
// Network requests collected per browser (since connect time)
|
|
494
|
+
networkRequests = /* @__PURE__ */ new Map();
|
|
495
|
+
constructor(options = {}) {
|
|
496
|
+
this.defaultTimeout = options.timeout ?? 5e3;
|
|
497
|
+
const detected = detectOptimalConcurrency();
|
|
498
|
+
const defaultConcurrency = options.maxConcurrency ?? detected.default;
|
|
499
|
+
const heavyConcurrency = options.maxConcurrency ? Math.max(2, Math.floor(options.maxConcurrency * 0.75)) : detected.heavy;
|
|
500
|
+
const lightConcurrency = options.maxConcurrency ? Math.min(options.maxConcurrency * 1.5, 10) : detected.light;
|
|
501
|
+
this.limiter = new ConcurrencyLimiter(defaultConcurrency);
|
|
502
|
+
this.heavyLimiter = new ConcurrencyLimiter(heavyConcurrency);
|
|
503
|
+
this.lightLimiter = new ConcurrencyLimiter(lightConcurrency);
|
|
504
|
+
console.error(`[Executor] Concurrency limiters initialized: default=${defaultConcurrency}, heavy=${heavyConcurrency}, light=${lightConcurrency}`);
|
|
505
|
+
}
|
|
506
|
+
// ==================== CONNECTION MANAGEMENT ====================
|
|
507
|
+
/**
|
|
508
|
+
* Connect to multiple browsers with controlled concurrency.
|
|
509
|
+
* OPTIMIZATION: Limits simultaneous connections to prevent overwhelming CDP endpoints.
|
|
510
|
+
*/
|
|
511
|
+
async connectAll(endpoints) {
|
|
512
|
+
const start = Date.now();
|
|
513
|
+
console.error(`[Executor] connectAll: starting connection to ${endpoints.length} browsers with concurrency limit`);
|
|
514
|
+
const connectionLimiter = new ConcurrencyLimiter(
|
|
515
|
+
this.heavyLimiter.limit
|
|
516
|
+
);
|
|
517
|
+
const tasks = endpoints.map(async (endpoint) => {
|
|
518
|
+
return connectionLimiter.run(async () => {
|
|
519
|
+
const taskStart = Date.now();
|
|
520
|
+
try {
|
|
521
|
+
console.error(`[Executor] Connecting to ${endpoint.id} at ${endpoint.cdpEndpoint}...`);
|
|
522
|
+
const result = await this.connect(endpoint);
|
|
523
|
+
console.error(`[Executor] \u2705 Connected to ${endpoint.id}: ${result.url}`);
|
|
524
|
+
return {
|
|
525
|
+
browserId: endpoint.id,
|
|
526
|
+
success: true,
|
|
527
|
+
result,
|
|
528
|
+
duration: Date.now() - taskStart
|
|
529
|
+
};
|
|
530
|
+
} catch (error) {
|
|
531
|
+
console.error(`[Executor] \u274C Failed to connect to ${endpoint.id}: ${error}`);
|
|
532
|
+
return {
|
|
533
|
+
browserId: endpoint.id,
|
|
534
|
+
success: false,
|
|
535
|
+
error: String(error),
|
|
536
|
+
duration: Date.now() - taskStart
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
const results = await Promise.all(tasks);
|
|
542
|
+
console.error(`[Executor] connectAll complete: ${results.filter((r) => r.success).length}/${results.length} connected in ${Date.now() - start}ms`);
|
|
543
|
+
return {
|
|
544
|
+
results,
|
|
545
|
+
totalDuration: Date.now() - start,
|
|
546
|
+
successCount: results.filter((r) => r.success).length,
|
|
547
|
+
failureCount: results.filter((r) => !r.success).length
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
async connect(endpoint, retries = 3) {
|
|
551
|
+
if (this.browsers.has(endpoint.id)) {
|
|
552
|
+
throw new Error(`Browser ${endpoint.id} is already connected`);
|
|
553
|
+
}
|
|
554
|
+
let lastError = null;
|
|
555
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
556
|
+
try {
|
|
557
|
+
console.error(`[Executor] Attempt ${attempt}/${retries} to connect to ${endpoint.id} at ${endpoint.cdpEndpoint}`);
|
|
558
|
+
const browser = await playwright.chromium.connectOverCDP(endpoint.cdpEndpoint, {
|
|
559
|
+
headers: endpoint.cdpHeaders
|
|
560
|
+
});
|
|
561
|
+
return await this._finishConnect(endpoint, browser);
|
|
562
|
+
} catch (error) {
|
|
563
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
564
|
+
console.error(`[Executor] Attempt ${attempt}/${retries} failed for ${endpoint.id}: ${lastError.message}`);
|
|
565
|
+
if (attempt < retries) {
|
|
566
|
+
const delay = Math.pow(2, attempt - 1) * 300;
|
|
567
|
+
console.error(`[Executor] Retrying in ${delay}ms...`);
|
|
568
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
throw lastError || new Error(`Failed to connect to ${endpoint.id} after ${retries} attempts`);
|
|
573
|
+
}
|
|
574
|
+
async _finishConnect(endpoint, browser) {
|
|
575
|
+
let context;
|
|
576
|
+
let page;
|
|
577
|
+
const contexts = browser.contexts();
|
|
578
|
+
console.error(`[Executor] Browser ${endpoint.id} has ${contexts.length} contexts`);
|
|
579
|
+
if (contexts.length > 0) {
|
|
580
|
+
context = contexts[0];
|
|
581
|
+
const pages = context.pages();
|
|
582
|
+
console.error(`[Executor] Context has ${pages.length} pages`);
|
|
583
|
+
if (pages.length > 0) {
|
|
584
|
+
page = pages.find((p) => !p.url().startsWith("about:")) || pages[0];
|
|
585
|
+
console.error(`[Executor] Using existing page: ${page.url()}`);
|
|
586
|
+
} else {
|
|
587
|
+
console.error(`[Executor] No pages in context, waiting for existing page...`);
|
|
588
|
+
const waitStart = Date.now();
|
|
589
|
+
while (Date.now() - waitStart < 2e3) {
|
|
590
|
+
const currentPages = context.pages();
|
|
591
|
+
if (currentPages.length > 0) {
|
|
592
|
+
page = currentPages.find((p) => !p.url().startsWith("about:")) || currentPages[0];
|
|
593
|
+
console.error(`[Executor] Found page after waiting: ${page.url()}`);
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
597
|
+
}
|
|
598
|
+
if (!page) {
|
|
599
|
+
console.error(`[Executor] No pages appeared after 2s, creating new page (fallback)`);
|
|
600
|
+
page = await context.newPage();
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
} else {
|
|
604
|
+
console.error(`[Executor] No contexts, waiting for context to appear...`);
|
|
605
|
+
const waitStart = Date.now();
|
|
606
|
+
while (Date.now() - waitStart < 2e3) {
|
|
607
|
+
const currentContexts = browser.contexts();
|
|
608
|
+
if (currentContexts.length > 0) {
|
|
609
|
+
context = currentContexts[0];
|
|
610
|
+
const pages = context.pages();
|
|
611
|
+
if (pages.length > 0) {
|
|
612
|
+
page = pages.find((p) => !p.url().startsWith("about:")) || pages[0];
|
|
613
|
+
console.error(`[Executor] Found context and page after waiting: ${page.url()}`);
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
618
|
+
}
|
|
619
|
+
if (!context) {
|
|
620
|
+
console.error(`[Executor] Creating new context and page for ${endpoint.id} (fallback)`);
|
|
621
|
+
context = await browser.newContext();
|
|
622
|
+
page = await context.newPage();
|
|
623
|
+
} else if (!page) {
|
|
624
|
+
console.error(`[Executor] Context found but no pages, creating page (fallback)`);
|
|
625
|
+
page = await context.newPage();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
const allPages = context.pages();
|
|
630
|
+
for (const p of allPages) {
|
|
631
|
+
if (p !== page && p.url() === "about:blank") {
|
|
632
|
+
await p.close().catch(() => {
|
|
633
|
+
});
|
|
634
|
+
console.error(`[Executor] Closed extra about:blank page for ${endpoint.id}`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
} catch {
|
|
638
|
+
}
|
|
639
|
+
page.setDefaultTimeout(this.defaultTimeout);
|
|
640
|
+
page.setDefaultNavigationTimeout(6e4);
|
|
641
|
+
const connected = {
|
|
642
|
+
id: endpoint.id,
|
|
643
|
+
browser,
|
|
644
|
+
context,
|
|
645
|
+
page,
|
|
646
|
+
endpoint,
|
|
647
|
+
connectedAt: /* @__PURE__ */ new Date()
|
|
648
|
+
};
|
|
649
|
+
this.browsers.set(endpoint.id, connected);
|
|
650
|
+
console.error(`[Executor] Successfully stored ${endpoint.id} in browsers map. Total browsers: ${this.browsers.size}`);
|
|
651
|
+
this.snapshotRefs.set(endpoint.id, /* @__PURE__ */ new Map());
|
|
652
|
+
const consoleLog = [];
|
|
653
|
+
this.consoleMessages.set(endpoint.id, consoleLog);
|
|
654
|
+
page.on("console", (msg) => {
|
|
655
|
+
consoleLog.push({ type: msg.type(), text: msg.text(), timestamp: Date.now() });
|
|
656
|
+
if (consoleLog.length > 1e3) consoleLog.shift();
|
|
657
|
+
});
|
|
658
|
+
const networkLog = [];
|
|
659
|
+
this.networkRequests.set(endpoint.id, networkLog);
|
|
660
|
+
page.on("response", (response) => {
|
|
661
|
+
const req = response.request();
|
|
662
|
+
networkLog.push({
|
|
663
|
+
url: req.url(),
|
|
664
|
+
method: req.method(),
|
|
665
|
+
status: response.status(),
|
|
666
|
+
resourceType: req.resourceType(),
|
|
667
|
+
timestamp: Date.now()
|
|
668
|
+
});
|
|
669
|
+
if (networkLog.length > 1e3) networkLog.shift();
|
|
670
|
+
});
|
|
671
|
+
browser.on("disconnected", () => {
|
|
672
|
+
console.error(`[Executor] Browser ${endpoint.id} disconnected`);
|
|
673
|
+
this.browsers.delete(endpoint.id);
|
|
674
|
+
this.consoleMessages.delete(endpoint.id);
|
|
675
|
+
this.networkRequests.delete(endpoint.id);
|
|
676
|
+
this.snapshotRefs.delete(endpoint.id);
|
|
677
|
+
setTimeout(async () => {
|
|
678
|
+
if (!this.browsers.has(endpoint.id)) {
|
|
679
|
+
console.error(`[Executor] Attempting auto-reconnect for ${endpoint.id}...`);
|
|
680
|
+
try {
|
|
681
|
+
await this.connect(endpoint, 2);
|
|
682
|
+
console.error(`[Executor] \u2705 Auto-reconnected ${endpoint.id}`);
|
|
683
|
+
} catch (e) {
|
|
684
|
+
console.error(`[Executor] \u274C Auto-reconnect failed for ${endpoint.id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}, 500);
|
|
688
|
+
});
|
|
689
|
+
const url = page.url();
|
|
690
|
+
const title = await page.title().catch(() => "");
|
|
691
|
+
console.error(`[Executor] Browser ${endpoint.id} connected. URL: ${url}, Title: ${title}`);
|
|
692
|
+
return {
|
|
693
|
+
id: endpoint.id,
|
|
694
|
+
connected: true,
|
|
695
|
+
url,
|
|
696
|
+
title,
|
|
697
|
+
endpoint: endpoint.cdpEndpoint,
|
|
698
|
+
tags: endpoint.tags,
|
|
699
|
+
connectedAt: connected.connectedAt
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
async disconnect(browserId) {
|
|
703
|
+
const browser = this.browsers.get(browserId);
|
|
704
|
+
if (!browser) throw new Error(`Browser ${browserId} not found`);
|
|
705
|
+
await browser.browser.close().catch(() => {
|
|
706
|
+
});
|
|
707
|
+
this.browsers.delete(browserId);
|
|
708
|
+
this.consoleMessages.delete(browserId);
|
|
709
|
+
this.networkRequests.delete(browserId);
|
|
710
|
+
this.snapshotRefs.delete(browserId);
|
|
711
|
+
}
|
|
712
|
+
async disconnectAll() {
|
|
713
|
+
const ids = Array.from(this.browsers.keys());
|
|
714
|
+
await Promise.all(
|
|
715
|
+
ids.map((id) => this.disconnect(id).catch(() => {
|
|
716
|
+
}))
|
|
717
|
+
);
|
|
718
|
+
this.browsers.clear();
|
|
719
|
+
}
|
|
720
|
+
async listBrowsers() {
|
|
721
|
+
const statuses = [];
|
|
722
|
+
for (const [id, browser] of this.browsers) {
|
|
723
|
+
try {
|
|
724
|
+
statuses.push({
|
|
725
|
+
id,
|
|
726
|
+
connected: browser.browser.isConnected(),
|
|
727
|
+
url: browser.page.url(),
|
|
728
|
+
title: await browser.page.title().catch(() => ""),
|
|
729
|
+
endpoint: browser.endpoint.cdpEndpoint,
|
|
730
|
+
tags: browser.endpoint.tags,
|
|
731
|
+
connectedAt: browser.connectedAt
|
|
732
|
+
});
|
|
733
|
+
} catch {
|
|
734
|
+
statuses.push({
|
|
735
|
+
id,
|
|
736
|
+
connected: false,
|
|
737
|
+
endpoint: browser.endpoint.cdpEndpoint,
|
|
738
|
+
tags: browser.endpoint.tags
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return statuses;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Detailed status check for all browsers
|
|
746
|
+
*/
|
|
747
|
+
async getDetailedStatus(options = {}) {
|
|
748
|
+
const results = [];
|
|
749
|
+
const urlCounts = /* @__PURE__ */ new Map();
|
|
750
|
+
for (const [id, browser] of this.browsers) {
|
|
751
|
+
try {
|
|
752
|
+
if (!browser.browser.isConnected()) {
|
|
753
|
+
results.push({
|
|
754
|
+
id,
|
|
755
|
+
status: "disconnected",
|
|
756
|
+
url: "",
|
|
757
|
+
title: "",
|
|
758
|
+
error: "Browser disconnected",
|
|
759
|
+
responsive: false
|
|
760
|
+
});
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
const url = browser.page.url();
|
|
764
|
+
const title = await browser.page.title().catch(() => "");
|
|
765
|
+
let responsive = true;
|
|
766
|
+
try {
|
|
767
|
+
let timeoutId;
|
|
768
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
769
|
+
timeoutId = setTimeout(() => reject(new Error("timeout")), 1e3);
|
|
770
|
+
});
|
|
771
|
+
try {
|
|
772
|
+
await Promise.race([
|
|
773
|
+
browser.page.evaluate(() => true),
|
|
774
|
+
timeoutPromise
|
|
775
|
+
]);
|
|
776
|
+
clearTimeout(timeoutId);
|
|
777
|
+
} catch {
|
|
778
|
+
clearTimeout(timeoutId);
|
|
779
|
+
responsive = false;
|
|
780
|
+
}
|
|
781
|
+
} catch {
|
|
782
|
+
responsive = false;
|
|
783
|
+
}
|
|
784
|
+
const baseUrl = url.split("?")[0];
|
|
785
|
+
urlCounts.set(baseUrl, (urlCounts.get(baseUrl) || 0) + 1);
|
|
786
|
+
results.push({
|
|
787
|
+
id,
|
|
788
|
+
status: responsive ? "ok" : "error",
|
|
789
|
+
url,
|
|
790
|
+
title,
|
|
791
|
+
error: responsive ? void 0 : "Page not responding",
|
|
792
|
+
responsive
|
|
793
|
+
});
|
|
794
|
+
} catch (error) {
|
|
795
|
+
results.push({
|
|
796
|
+
id,
|
|
797
|
+
status: "error",
|
|
798
|
+
url: "",
|
|
799
|
+
title: "",
|
|
800
|
+
error: String(error),
|
|
801
|
+
responsive: false
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
let majorityUrl;
|
|
806
|
+
let maxCount = 0;
|
|
807
|
+
for (const [url, count] of urlCounts) {
|
|
808
|
+
if (count > maxCount) {
|
|
809
|
+
maxCount = count;
|
|
810
|
+
majorityUrl = url;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (majorityUrl && maxCount > 1) {
|
|
814
|
+
for (const result of results) {
|
|
815
|
+
if (result.status === "ok" && result.url.split("?")[0] !== majorityUrl) {
|
|
816
|
+
result.status = "behind";
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const okCount = results.filter((r) => r.status === "ok").length;
|
|
821
|
+
return {
|
|
822
|
+
browsers: results,
|
|
823
|
+
summary: {
|
|
824
|
+
total: results.length,
|
|
825
|
+
ok: okCount,
|
|
826
|
+
problems: results.length - okCount
|
|
827
|
+
},
|
|
828
|
+
majorityUrl
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Sync browsers from API (on-demand) with retry logic.
|
|
833
|
+
* Called automatically when getBrowserIds finds no connected browsers.
|
|
834
|
+
*/
|
|
835
|
+
async syncFromAPI(retries = 2) {
|
|
836
|
+
const apiServerUrl = process.env.API_SERVER_URL;
|
|
837
|
+
const userId = process.env.USER_ID;
|
|
838
|
+
if (!apiServerUrl || !userId) {
|
|
839
|
+
console.error(`[Executor] syncFromAPI: missing API_SERVER_URL or USER_ID`);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
let lastError = null;
|
|
843
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
844
|
+
try {
|
|
845
|
+
const url = `${apiServerUrl}/api/browsers?userId=${userId}`;
|
|
846
|
+
console.error(`[Executor] syncFromAPI: fetching ${url} (attempt ${attempt}/${retries})`);
|
|
847
|
+
const controller = new AbortController();
|
|
848
|
+
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
849
|
+
try {
|
|
850
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
851
|
+
clearTimeout(timeoutId);
|
|
852
|
+
if (!response.ok) {
|
|
853
|
+
throw new Error(`API returned ${response.status}`);
|
|
854
|
+
}
|
|
855
|
+
const data = await response.json();
|
|
856
|
+
const apiBrowsers = data.browsers || [];
|
|
857
|
+
console.error(`[Executor] syncFromAPI: API returned ${apiBrowsers.length} browsers`);
|
|
858
|
+
const connectedIds = new Set(this.browsers.keys());
|
|
859
|
+
for (const browser of apiBrowsers) {
|
|
860
|
+
if (!connectedIds.has(browser.id)) {
|
|
861
|
+
console.error(`[Executor] syncFromAPI: connecting ${browser.id}`);
|
|
862
|
+
try {
|
|
863
|
+
await this.connect({
|
|
864
|
+
id: browser.id,
|
|
865
|
+
cdpEndpoint: browser.cdpEndpoint,
|
|
866
|
+
tags: browser.tags || []
|
|
867
|
+
});
|
|
868
|
+
console.error(`[Executor] syncFromAPI: \u2713 connected ${browser.id}`);
|
|
869
|
+
} catch (e) {
|
|
870
|
+
console.error(`[Executor] syncFromAPI: \u2717 failed ${browser.id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
const apiIds = new Set(apiBrowsers.map((b) => b.id));
|
|
875
|
+
for (const id of connectedIds) {
|
|
876
|
+
if (!apiIds.has(id)) {
|
|
877
|
+
console.error(`[Executor] syncFromAPI: disconnecting stale ${id}`);
|
|
878
|
+
try {
|
|
879
|
+
await this.disconnect(id);
|
|
880
|
+
} catch {
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
console.error(`[Executor] syncFromAPI: done, now have ${this.browsers.size} browsers`);
|
|
885
|
+
this.lastSyncTime = Date.now();
|
|
886
|
+
return;
|
|
887
|
+
} catch (e) {
|
|
888
|
+
clearTimeout(timeoutId);
|
|
889
|
+
throw e;
|
|
890
|
+
}
|
|
891
|
+
} catch (e) {
|
|
892
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
893
|
+
console.error(`[Executor] syncFromAPI: attempt ${attempt}/${retries} failed: ${lastError.message}`);
|
|
894
|
+
if (attempt < retries) {
|
|
895
|
+
const delay = Math.pow(2, attempt - 1) * 300;
|
|
896
|
+
console.error(`[Executor] syncFromAPI: retrying in ${delay}ms...`);
|
|
897
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
console.error(`[Executor] syncFromAPI: all ${retries} attempts failed`);
|
|
902
|
+
}
|
|
903
|
+
getBrowserIds(filter) {
|
|
904
|
+
let ids = Array.from(this.browsers.keys());
|
|
905
|
+
if (filter?.browserIds?.length) {
|
|
906
|
+
ids = ids.filter((id) => filter.browserIds.includes(id));
|
|
907
|
+
}
|
|
908
|
+
if (filter?.tags?.length) {
|
|
909
|
+
ids = ids.filter((id) => {
|
|
910
|
+
const browser = this.browsers.get(id);
|
|
911
|
+
return browser?.endpoint.tags?.some((t) => filter.tags.includes(t));
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
return ids;
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Ensure browsers are loaded before operations.
|
|
918
|
+
* Uses caching to prevent excessive API calls.
|
|
919
|
+
* Only syncs if: no browsers connected AND cache expired.
|
|
920
|
+
*/
|
|
921
|
+
async ensureBrowsersLoaded() {
|
|
922
|
+
if (this.browsers.size > 0) {
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
const now = Date.now();
|
|
926
|
+
if (now - this.lastSyncTime < _MultiBrowserExecutor.SYNC_TTL_MS) {
|
|
927
|
+
console.error(`[Executor] ensureBrowsersLoaded: skipping sync, cache valid (${now - this.lastSyncTime}ms old)`);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (this.syncInProgress) {
|
|
931
|
+
console.error(`[Executor] ensureBrowsersLoaded: sync already in progress, waiting...`);
|
|
932
|
+
await this.syncInProgress;
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
console.error(`[Executor] ensureBrowsersLoaded: no browsers, syncing from API...`);
|
|
936
|
+
this.syncInProgress = this.syncFromAPI().finally(() => {
|
|
937
|
+
this.syncInProgress = null;
|
|
938
|
+
});
|
|
939
|
+
await this.syncInProgress;
|
|
940
|
+
}
|
|
941
|
+
getBrowserEndpoint(browserId) {
|
|
942
|
+
const browser = this.browsers.get(browserId);
|
|
943
|
+
return browser?.endpoint;
|
|
944
|
+
}
|
|
945
|
+
getPage(browserId) {
|
|
946
|
+
const browser = this.browsers.get(browserId);
|
|
947
|
+
if (!browser) throw new Error(`Browser ${browserId} not found`);
|
|
948
|
+
return browser.page;
|
|
949
|
+
}
|
|
950
|
+
resolveSelector(target, browserId) {
|
|
951
|
+
if (typeof target === "string") return target;
|
|
952
|
+
if (target.selector) return target.selector;
|
|
953
|
+
if (target.ref) {
|
|
954
|
+
const selector = this.snapshotRefs.get(browserId)?.get(String(target.ref));
|
|
955
|
+
if (selector) return selector;
|
|
956
|
+
throw new Error(`Unknown ref "${target.ref}" for browser ${browserId}. Call browser_parallel_snapshot first.`);
|
|
957
|
+
}
|
|
958
|
+
throw new Error("Either selector or ref is required");
|
|
959
|
+
}
|
|
960
|
+
isTruthyFieldValue(value) {
|
|
961
|
+
return ["true", "1", "yes", "on", "checked"].includes(value.trim().toLowerCase());
|
|
962
|
+
}
|
|
963
|
+
async waitForDomStable(page, stableMs, timeoutMs) {
|
|
964
|
+
const startedAt = Date.now();
|
|
965
|
+
let lastSignature = "";
|
|
966
|
+
let stableSince = 0;
|
|
967
|
+
const pollInterval = Math.min(250, Math.max(75, Math.floor(stableMs / 3) || 100));
|
|
968
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
969
|
+
const signature = await page.evaluate(() => JSON.stringify({
|
|
970
|
+
readyState: document.readyState,
|
|
971
|
+
nodeCount: document.querySelectorAll("*").length,
|
|
972
|
+
textLength: document.body?.innerText.length ?? 0,
|
|
973
|
+
scrollHeight: document.documentElement?.scrollHeight ?? 0,
|
|
974
|
+
scrollWidth: document.documentElement?.scrollWidth ?? 0
|
|
975
|
+
}));
|
|
976
|
+
if (signature === lastSignature) {
|
|
977
|
+
if (!stableSince) stableSince = Date.now();
|
|
978
|
+
if (Date.now() - stableSince >= stableMs) return;
|
|
979
|
+
} else {
|
|
980
|
+
lastSignature = signature;
|
|
981
|
+
stableSince = 0;
|
|
982
|
+
}
|
|
983
|
+
await page.waitForTimeout(pollInterval);
|
|
984
|
+
}
|
|
985
|
+
throw new Error(`DOM did not become stable within ${timeoutMs}ms`);
|
|
986
|
+
}
|
|
987
|
+
getMimeType(fileName) {
|
|
988
|
+
const ext = extname(fileName).toLowerCase();
|
|
989
|
+
const types = {
|
|
990
|
+
".txt": "text/plain",
|
|
991
|
+
".json": "application/json",
|
|
992
|
+
".csv": "text/csv",
|
|
993
|
+
".pdf": "application/pdf",
|
|
994
|
+
".png": "image/png",
|
|
995
|
+
".jpg": "image/jpeg",
|
|
996
|
+
".jpeg": "image/jpeg",
|
|
997
|
+
".gif": "image/gif",
|
|
998
|
+
".webp": "image/webp",
|
|
999
|
+
".svg": "image/svg+xml",
|
|
1000
|
+
".zip": "application/zip"
|
|
1001
|
+
};
|
|
1002
|
+
return types[ext] || "application/octet-stream";
|
|
1003
|
+
}
|
|
1004
|
+
// ==================== PARALLEL EXECUTION ====================
|
|
1005
|
+
/**
|
|
1006
|
+
* Get the appropriate limiter based on operation type
|
|
1007
|
+
*/
|
|
1008
|
+
getLimiter(type) {
|
|
1009
|
+
if (typeof type === "number") {
|
|
1010
|
+
return new ConcurrencyLimiter(type);
|
|
1011
|
+
}
|
|
1012
|
+
switch (type) {
|
|
1013
|
+
case "heavy":
|
|
1014
|
+
return this.heavyLimiter;
|
|
1015
|
+
case "light":
|
|
1016
|
+
return this.lightLimiter;
|
|
1017
|
+
default:
|
|
1018
|
+
return this.limiter;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Execute action on multiple browsers with controlled concurrency.
|
|
1023
|
+
* This prevents system overload when operating on 9+ browsers.
|
|
1024
|
+
* Concurrency is auto-detected based on system resources (RAM, CPU cores).
|
|
1025
|
+
*
|
|
1026
|
+
* @param browserIds - specific browsers to target, or undefined for all
|
|
1027
|
+
* @param action - async function to execute on each browser
|
|
1028
|
+
* @param options.timeout - per-browser timeout
|
|
1029
|
+
* @param options.concurrency - 'heavy' | 'light' | number - type of operation or explicit limit
|
|
1030
|
+
*/
|
|
1031
|
+
async executeParallel(browserIds, action, options = {}) {
|
|
1032
|
+
const start = Date.now();
|
|
1033
|
+
await this.ensureBrowsersLoaded();
|
|
1034
|
+
const ids = browserIds ?? this.getBrowserIds();
|
|
1035
|
+
const timeout = options.timeout ?? this.defaultTimeout;
|
|
1036
|
+
const limiter = this.getLimiter(options.concurrency);
|
|
1037
|
+
console.error(`[Executor] executeParallel: running on ${ids.length} browsers with concurrency limit: [${ids.join(", ")}]`);
|
|
1038
|
+
const tasks = ids.map(async (id) => {
|
|
1039
|
+
return limiter.run(async () => {
|
|
1040
|
+
const taskStart = Date.now();
|
|
1041
|
+
const browser = this.browsers.get(id);
|
|
1042
|
+
if (!browser) {
|
|
1043
|
+
console.error(`[Executor] \u274C Browser ${id} not found in map`);
|
|
1044
|
+
return {
|
|
1045
|
+
browserId: id,
|
|
1046
|
+
success: false,
|
|
1047
|
+
error: `Browser ${id} not found`,
|
|
1048
|
+
duration: Date.now() - taskStart
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
if (!browser.browser.isConnected()) {
|
|
1052
|
+
console.error(`[Executor] \u274C Browser ${id} is disconnected`);
|
|
1053
|
+
this.browsers.delete(id);
|
|
1054
|
+
return {
|
|
1055
|
+
browserId: id,
|
|
1056
|
+
success: false,
|
|
1057
|
+
error: `Browser ${id} is disconnected`,
|
|
1058
|
+
duration: Date.now() - taskStart
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
try {
|
|
1062
|
+
console.error(`[Executor] \u{1F504} Executing action on ${id}...`);
|
|
1063
|
+
let timeoutId;
|
|
1064
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1065
|
+
timeoutId = setTimeout(() => reject(new Error("Timeout")), timeout);
|
|
1066
|
+
});
|
|
1067
|
+
try {
|
|
1068
|
+
const result = await Promise.race([
|
|
1069
|
+
action(browser.page, id),
|
|
1070
|
+
timeoutPromise
|
|
1071
|
+
]);
|
|
1072
|
+
clearTimeout(timeoutId);
|
|
1073
|
+
console.error(`[Executor] \u2705 Action completed on ${id}`);
|
|
1074
|
+
return {
|
|
1075
|
+
browserId: id,
|
|
1076
|
+
success: true,
|
|
1077
|
+
result,
|
|
1078
|
+
duration: Date.now() - taskStart
|
|
1079
|
+
};
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
clearTimeout(timeoutId);
|
|
1082
|
+
throw error;
|
|
1083
|
+
}
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
console.error(`[Executor] \u274C Action failed on ${id}: ${error}`);
|
|
1086
|
+
return {
|
|
1087
|
+
browserId: id,
|
|
1088
|
+
success: false,
|
|
1089
|
+
error: String(error),
|
|
1090
|
+
duration: Date.now() - taskStart
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
});
|
|
1095
|
+
const results = await Promise.all(tasks);
|
|
1096
|
+
console.error(`[Executor] executeParallel complete: ${results.filter((r) => r.success).length}/${results.length} succeeded in ${Date.now() - start}ms`);
|
|
1097
|
+
return {
|
|
1098
|
+
results,
|
|
1099
|
+
totalDuration: Date.now() - start,
|
|
1100
|
+
successCount: results.filter((r) => r.success).length,
|
|
1101
|
+
failureCount: results.filter((r) => !r.success).length
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
// ==================== NAVIGATION ====================
|
|
1105
|
+
/**
|
|
1106
|
+
* Navigate multiple browsers to the same URL.
|
|
1107
|
+
* OPTIMIZATION: Uses higher concurrency for navigation (it's mostly network-bound)
|
|
1108
|
+
*/
|
|
1109
|
+
async parallelNavigate(url, browserIds) {
|
|
1110
|
+
return this.executeParallel(
|
|
1111
|
+
browserIds,
|
|
1112
|
+
async (page) => {
|
|
1113
|
+
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
1114
|
+
await page.waitForLoadState("load", { timeout: 5e3 }).catch(() => {
|
|
1115
|
+
});
|
|
1116
|
+
return { url: page.url(), title: await page.title() };
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
timeout: 6e4,
|
|
1120
|
+
concurrency: "light"
|
|
1121
|
+
}
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
async parallelNavigateMulti(targets) {
|
|
1125
|
+
const start = Date.now();
|
|
1126
|
+
const tasks = targets.map(async (target) => {
|
|
1127
|
+
const taskStart = Date.now();
|
|
1128
|
+
const browser = this.browsers.get(target.browserId);
|
|
1129
|
+
if (!browser) {
|
|
1130
|
+
return {
|
|
1131
|
+
browserId: target.browserId,
|
|
1132
|
+
success: false,
|
|
1133
|
+
error: `Browser ${target.browserId} not found`,
|
|
1134
|
+
duration: Date.now() - taskStart
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
try {
|
|
1138
|
+
await browser.page.goto(target.url, { waitUntil: "domcontentloaded" });
|
|
1139
|
+
await browser.page.waitForLoadState("load", { timeout: 5e3 }).catch(() => {
|
|
1140
|
+
});
|
|
1141
|
+
return {
|
|
1142
|
+
browserId: target.browserId,
|
|
1143
|
+
success: true,
|
|
1144
|
+
result: { url: browser.page.url(), title: await browser.page.title() },
|
|
1145
|
+
duration: Date.now() - taskStart
|
|
1146
|
+
};
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
return {
|
|
1149
|
+
browserId: target.browserId,
|
|
1150
|
+
success: false,
|
|
1151
|
+
error: String(error),
|
|
1152
|
+
duration: Date.now() - taskStart
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
const results = await Promise.all(tasks);
|
|
1157
|
+
return {
|
|
1158
|
+
results,
|
|
1159
|
+
totalDuration: Date.now() - start,
|
|
1160
|
+
successCount: results.filter((r) => r.success).length,
|
|
1161
|
+
failureCount: results.filter((r) => !r.success).length
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
async parallelGoBack(browserIds) {
|
|
1165
|
+
return this.executeParallel(browserIds, async (page) => {
|
|
1166
|
+
await page.goBack();
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
async parallelGoForward(browserIds) {
|
|
1170
|
+
return this.executeParallel(browserIds, async (page) => {
|
|
1171
|
+
await page.goForward();
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
async parallelReload(browserIds) {
|
|
1175
|
+
return this.executeParallel(browserIds, async (page) => {
|
|
1176
|
+
await page.reload();
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
// ==================== SNAPSHOT ====================
|
|
1180
|
+
async parallelSnapshot(browserIds, options = {}) {
|
|
1181
|
+
const MAX_SNAPSHOT_SIZE = 3e4;
|
|
1182
|
+
return this.executeParallel(
|
|
1183
|
+
browserIds,
|
|
1184
|
+
async (page, browserId) => {
|
|
1185
|
+
const elements = await page.evaluate((compactMode) => {
|
|
1186
|
+
const interactiveTags = /* @__PURE__ */ new Set(["a", "button", "input", "select", "textarea", "summary"]);
|
|
1187
|
+
const interactiveRoles = /* @__PURE__ */ new Set([
|
|
1188
|
+
"button",
|
|
1189
|
+
"link",
|
|
1190
|
+
"textbox",
|
|
1191
|
+
"combobox",
|
|
1192
|
+
"searchbox",
|
|
1193
|
+
"checkbox",
|
|
1194
|
+
"radio",
|
|
1195
|
+
"switch",
|
|
1196
|
+
"slider",
|
|
1197
|
+
"spinbutton",
|
|
1198
|
+
"menuitem",
|
|
1199
|
+
"menuitemcheckbox",
|
|
1200
|
+
"menuitemradio",
|
|
1201
|
+
"option",
|
|
1202
|
+
"tab",
|
|
1203
|
+
"treeitem",
|
|
1204
|
+
"listbox"
|
|
1205
|
+
]);
|
|
1206
|
+
function buildSelector(el) {
|
|
1207
|
+
const tag = el.tagName.toLowerCase();
|
|
1208
|
+
if (el.id) return `#${CSS.escape(el.id)}`;
|
|
1209
|
+
const testId = el.getAttribute("data-testid");
|
|
1210
|
+
if (testId) return `[data-testid="${testId}"]`;
|
|
1211
|
+
const name = el.getAttribute("name");
|
|
1212
|
+
if (name) return `${tag}[name="${name}"]`;
|
|
1213
|
+
const placeholder = el.getAttribute("placeholder");
|
|
1214
|
+
if (placeholder) return `${tag}[placeholder="${placeholder}"]`;
|
|
1215
|
+
const ariaLabel = el.getAttribute("aria-label");
|
|
1216
|
+
if (ariaLabel) return `[aria-label="${ariaLabel}"]`;
|
|
1217
|
+
const role = el.getAttribute("role");
|
|
1218
|
+
if (role) {
|
|
1219
|
+
const text = el.textContent?.trim().slice(0, 40);
|
|
1220
|
+
if (text) return `role=${role}[name="${text}"]`;
|
|
1221
|
+
return `[role="${role}"]`;
|
|
1222
|
+
}
|
|
1223
|
+
if (tag === "button" || tag === "a") {
|
|
1224
|
+
const text = el.textContent?.trim().slice(0, 40);
|
|
1225
|
+
if (text) return `${tag}:has-text("${text}")`;
|
|
1226
|
+
}
|
|
1227
|
+
const type = el.getAttribute("type");
|
|
1228
|
+
if (tag === "input" && type) return `input[type="${type}"]`;
|
|
1229
|
+
if (el.className && typeof el.className === "string") {
|
|
1230
|
+
const cls = el.className.trim().split(/\s+/)[0];
|
|
1231
|
+
if (cls) return `${tag}.${CSS.escape(cls)}`;
|
|
1232
|
+
}
|
|
1233
|
+
return tag;
|
|
1234
|
+
}
|
|
1235
|
+
function findSection(el) {
|
|
1236
|
+
let node = el;
|
|
1237
|
+
while (node) {
|
|
1238
|
+
let sibling = node.previousElementSibling;
|
|
1239
|
+
while (sibling) {
|
|
1240
|
+
if (/^H[1-6]$/.test(sibling.tagName)) {
|
|
1241
|
+
return sibling.textContent?.trim().slice(0, 50) || "";
|
|
1242
|
+
}
|
|
1243
|
+
sibling = sibling.previousElementSibling;
|
|
1244
|
+
}
|
|
1245
|
+
const role = node.getAttribute("role");
|
|
1246
|
+
const ariaLabel = node.getAttribute("aria-label");
|
|
1247
|
+
if (role === "navigation" || role === "main" || role === "banner" || role === "dialog" || role === "form") {
|
|
1248
|
+
return `[${role}${ariaLabel ? ": " + ariaLabel : ""}]`;
|
|
1249
|
+
}
|
|
1250
|
+
if (node.tagName === "NAV" || node.tagName === "MAIN" || node.tagName === "HEADER" || node.tagName === "FOOTER" || node.tagName === "FORM") {
|
|
1251
|
+
const label = ariaLabel || node.getAttribute("name") || "";
|
|
1252
|
+
return `[${node.tagName.toLowerCase()}${label ? ": " + label : ""}]`;
|
|
1253
|
+
}
|
|
1254
|
+
node = node.parentElement;
|
|
1255
|
+
}
|
|
1256
|
+
return "";
|
|
1257
|
+
}
|
|
1258
|
+
const results = [];
|
|
1259
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1260
|
+
if (!compactMode) {
|
|
1261
|
+
const headings = Array.from(document.querySelectorAll("h1, h2, h3"));
|
|
1262
|
+
for (const h of headings) {
|
|
1263
|
+
const text = h.textContent?.trim().slice(0, 80);
|
|
1264
|
+
if (text) {
|
|
1265
|
+
results.push({
|
|
1266
|
+
tag: h.tagName.toLowerCase(),
|
|
1267
|
+
selector: "",
|
|
1268
|
+
text
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
const allElements = Array.from(document.querySelectorAll("*"));
|
|
1274
|
+
for (const el of allElements) {
|
|
1275
|
+
const tag = el.tagName.toLowerCase();
|
|
1276
|
+
const role = el.getAttribute("role");
|
|
1277
|
+
const isInteractive = interactiveTags.has(tag) || role !== null && interactiveRoles.has(role) || el.getAttribute("contenteditable") === "true" || el.getAttribute("tabindex") !== null && el.getAttribute("tabindex") !== "-1";
|
|
1278
|
+
if (!isInteractive) continue;
|
|
1279
|
+
const rect = el.getBoundingClientRect();
|
|
1280
|
+
if (rect.width === 0 && rect.height === 0) continue;
|
|
1281
|
+
const style = window.getComputedStyle(el);
|
|
1282
|
+
if (style.display === "none" || style.visibility === "hidden") continue;
|
|
1283
|
+
const selector = buildSelector(el);
|
|
1284
|
+
if (seen.has(selector)) continue;
|
|
1285
|
+
seen.add(selector);
|
|
1286
|
+
const text = el.textContent?.trim().slice(0, 60) || "";
|
|
1287
|
+
const info = { tag, selector, text };
|
|
1288
|
+
const type = el.getAttribute("type");
|
|
1289
|
+
if (type) info.type = type;
|
|
1290
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
1291
|
+
if (el.value) info.value = el.value.slice(0, 60);
|
|
1292
|
+
}
|
|
1293
|
+
if (el instanceof HTMLInputElement && (el.type === "checkbox" || el.type === "radio")) {
|
|
1294
|
+
info.checked = el.checked;
|
|
1295
|
+
}
|
|
1296
|
+
if (el.getAttribute("disabled") !== null) {
|
|
1297
|
+
info.disabled = true;
|
|
1298
|
+
}
|
|
1299
|
+
const href = el.getAttribute("href");
|
|
1300
|
+
if (href && href !== "#" && !href.startsWith("javascript:")) {
|
|
1301
|
+
info.href = href.slice(0, 100);
|
|
1302
|
+
}
|
|
1303
|
+
const placeholder = el.getAttribute("placeholder");
|
|
1304
|
+
if (placeholder) info.placeholder = placeholder;
|
|
1305
|
+
if (!compactMode) {
|
|
1306
|
+
const section = findSection(el);
|
|
1307
|
+
if (section) info.section = section;
|
|
1308
|
+
}
|
|
1309
|
+
results.push(info);
|
|
1310
|
+
}
|
|
1311
|
+
return results;
|
|
1312
|
+
}, !!options.compact);
|
|
1313
|
+
const refMap = /* @__PURE__ */ new Map();
|
|
1314
|
+
let nextRef = 1;
|
|
1315
|
+
const elementsWithRefs = elements.map((element) => {
|
|
1316
|
+
if (!element.selector) return element;
|
|
1317
|
+
const ref = String(nextRef++);
|
|
1318
|
+
refMap.set(ref, element.selector);
|
|
1319
|
+
return { ...element, ref };
|
|
1320
|
+
});
|
|
1321
|
+
this.snapshotRefs.set(browserId, refMap);
|
|
1322
|
+
let snapshot = this.formatSnapshot(elementsWithRefs, !!options.compact);
|
|
1323
|
+
if (snapshot.length > MAX_SNAPSHOT_SIZE) {
|
|
1324
|
+
snapshot = snapshot.substring(0, MAX_SNAPSHOT_SIZE) + `
|
|
1325
|
+
|
|
1326
|
+
... [TRUNCATED: ${elementsWithRefs.length} elements total, showing first ${MAX_SNAPSHOT_SIZE} chars]`;
|
|
1327
|
+
}
|
|
1328
|
+
return {
|
|
1329
|
+
browserId,
|
|
1330
|
+
snapshot,
|
|
1331
|
+
url: page.url(),
|
|
1332
|
+
title: await page.title()
|
|
1333
|
+
};
|
|
1334
|
+
},
|
|
1335
|
+
{ concurrency: "heavy" }
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Format extracted elements into readable snapshot text
|
|
1340
|
+
*/
|
|
1341
|
+
formatSnapshot(elements, compact) {
|
|
1342
|
+
const lines = [];
|
|
1343
|
+
let lastSection = "";
|
|
1344
|
+
for (const el of elements) {
|
|
1345
|
+
if (!el.selector && (el.tag === "h1" || el.tag === "h2" || el.tag === "h3")) {
|
|
1346
|
+
lines.push(`
|
|
1347
|
+
## ${el.text}`);
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
if (!compact && el.section && el.section !== lastSection) {
|
|
1351
|
+
lastSection = el.section;
|
|
1352
|
+
lines.push(`
|
|
1353
|
+
--- ${el.section} ---`);
|
|
1354
|
+
}
|
|
1355
|
+
let line = `- ${el.tag}`;
|
|
1356
|
+
if (el.type) line += `[type=${el.type}]`;
|
|
1357
|
+
if (el.disabled) line += " (disabled)";
|
|
1358
|
+
if (el.ref) line += ` [ref=${el.ref}]`;
|
|
1359
|
+
line += ` \u2192 ${el.selector}`;
|
|
1360
|
+
const details = [];
|
|
1361
|
+
if (el.placeholder) details.push(`placeholder="${el.placeholder}"`);
|
|
1362
|
+
if (el.value) details.push(`value="${el.value}"`);
|
|
1363
|
+
if (el.checked !== void 0) details.push(el.checked ? "checked" : "unchecked");
|
|
1364
|
+
if (el.text && el.text.length > 0) details.push(`"${el.text}"`);
|
|
1365
|
+
if (el.href) details.push(`\u2192 ${el.href}`);
|
|
1366
|
+
if (details.length > 0) {
|
|
1367
|
+
line += ` ${details.join(" | ")}`;
|
|
1368
|
+
}
|
|
1369
|
+
lines.push(line);
|
|
1370
|
+
}
|
|
1371
|
+
return lines.join("\n");
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Take screenshots from multiple browsers.
|
|
1375
|
+
* OPTIMIZATION: Uses JPEG format with quality 70% for ~5x smaller files.
|
|
1376
|
+
* For 9 browsers: ~4.5MB PNG → ~900KB JPEG
|
|
1377
|
+
*/
|
|
1378
|
+
async parallelScreenshot(browserIds, options = {}) {
|
|
1379
|
+
const format = options.format ?? "jpeg";
|
|
1380
|
+
const quality = format === "jpeg" ? options.quality ?? 70 : void 0;
|
|
1381
|
+
return this.executeParallel(
|
|
1382
|
+
browserIds,
|
|
1383
|
+
async (page) => {
|
|
1384
|
+
return await page.screenshot({
|
|
1385
|
+
fullPage: options.fullPage,
|
|
1386
|
+
type: format,
|
|
1387
|
+
quality
|
|
1388
|
+
});
|
|
1389
|
+
},
|
|
1390
|
+
{ concurrency: "heavy" }
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
// ==================== HUMAN-LIKE HELPERS ====================
|
|
1394
|
+
/** Get or create a human profile for a browser */
|
|
1395
|
+
getProfile(browserId) {
|
|
1396
|
+
let profile = this.profiles.get(browserId);
|
|
1397
|
+
if (!profile) {
|
|
1398
|
+
profile = generateProfile(browserId);
|
|
1399
|
+
this.profiles.set(browserId, profile);
|
|
1400
|
+
}
|
|
1401
|
+
return profile;
|
|
1402
|
+
}
|
|
1403
|
+
/** Get last known mouse position, or a random starting position */
|
|
1404
|
+
getMousePos(browserId) {
|
|
1405
|
+
return this.lastMousePosition.get(browserId) ?? {
|
|
1406
|
+
x: 200 + Math.random() * 400,
|
|
1407
|
+
y: 150 + Math.random() * 300
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
/** Update tracked mouse position */
|
|
1411
|
+
setMousePos(browserId, pos) {
|
|
1412
|
+
this.lastMousePosition.set(browserId, pos);
|
|
1413
|
+
}
|
|
1414
|
+
// ==================== REF-BASED INTERACTION (human-like) ====================
|
|
1415
|
+
async parallelClick(target, browserIds) {
|
|
1416
|
+
return this.executeParallel(browserIds, async (page, browserId) => {
|
|
1417
|
+
const profile = this.getProfile(browserId);
|
|
1418
|
+
const selector = this.resolveSelector(target, browserId);
|
|
1419
|
+
const locator = page.locator(selector);
|
|
1420
|
+
await locator.waitFor({ state: "visible", timeout: 5e3 });
|
|
1421
|
+
await ensureCursor(page);
|
|
1422
|
+
await preActionDelay(profile);
|
|
1423
|
+
const { target: clickTarget } = await getClickTarget(page, locator);
|
|
1424
|
+
const fromPos = this.getMousePos(browserId);
|
|
1425
|
+
await humanClick(page, clickTarget, fromPos, profile);
|
|
1426
|
+
this.setMousePos(browserId, clickTarget);
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
async parallelFill(target, text, browserIds) {
|
|
1430
|
+
return this.executeParallel(browserIds, async (page, browserId) => {
|
|
1431
|
+
const profile = this.getProfile(browserId);
|
|
1432
|
+
const selector = this.resolveSelector(target, browserId);
|
|
1433
|
+
const locator = page.locator(selector);
|
|
1434
|
+
await locator.waitFor({ state: "visible", timeout: 5e3 });
|
|
1435
|
+
await ensureCursor(page);
|
|
1436
|
+
await preActionDelay(profile);
|
|
1437
|
+
const { target: clickTarget } = await getClickTarget(page, locator);
|
|
1438
|
+
const fromPos = this.getMousePos(browserId);
|
|
1439
|
+
await humanClick(page, clickTarget, fromPos, profile);
|
|
1440
|
+
this.setMousePos(browserId, clickTarget);
|
|
1441
|
+
await humanDelay(80, 200);
|
|
1442
|
+
await page.keyboard.press("Meta+a");
|
|
1443
|
+
await humanDelay(30, 80);
|
|
1444
|
+
await page.keyboard.press("Backspace");
|
|
1445
|
+
await humanDelay(50, 150);
|
|
1446
|
+
await humanTypeText(page, text, profile);
|
|
1447
|
+
return `filled "${text}"`;
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
async parallelFillMulti(target, texts) {
|
|
1451
|
+
const browserIds = Object.keys(texts);
|
|
1452
|
+
return this.executeParallel(browserIds, async (page, browserId) => {
|
|
1453
|
+
const text = texts[browserId];
|
|
1454
|
+
if (!text) throw new Error(`No text provided for browser ${browserId}`);
|
|
1455
|
+
const profile = this.getProfile(browserId);
|
|
1456
|
+
const selector = this.resolveSelector(target, browserId);
|
|
1457
|
+
const locator = page.locator(selector);
|
|
1458
|
+
await locator.waitFor({ state: "visible", timeout: 5e3 });
|
|
1459
|
+
await ensureCursor(page);
|
|
1460
|
+
await preActionDelay(profile);
|
|
1461
|
+
const { target: clickTarget } = await getClickTarget(page, locator);
|
|
1462
|
+
const fromPos = this.getMousePos(browserId);
|
|
1463
|
+
await humanClick(page, clickTarget, fromPos, profile);
|
|
1464
|
+
this.setMousePos(browserId, clickTarget);
|
|
1465
|
+
await humanDelay(80, 200);
|
|
1466
|
+
await page.keyboard.press("Meta+a");
|
|
1467
|
+
await humanDelay(30, 80);
|
|
1468
|
+
await page.keyboard.press("Backspace");
|
|
1469
|
+
await humanDelay(50, 150);
|
|
1470
|
+
await humanTypeText(page, text, profile);
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
async parallelType(target, text, browserIds) {
|
|
1474
|
+
return this.executeParallel(browserIds, async (page, browserId) => {
|
|
1475
|
+
const profile = this.getProfile(browserId);
|
|
1476
|
+
const selector = this.resolveSelector(target, browserId);
|
|
1477
|
+
const locator = page.locator(selector);
|
|
1478
|
+
await locator.waitFor({ state: "visible", timeout: 5e3 });
|
|
1479
|
+
await ensureCursor(page);
|
|
1480
|
+
const { target: clickTarget } = await getClickTarget(page, locator);
|
|
1481
|
+
const fromPos = this.getMousePos(browserId);
|
|
1482
|
+
await humanClick(page, clickTarget, fromPos, profile);
|
|
1483
|
+
this.setMousePos(browserId, clickTarget);
|
|
1484
|
+
await humanDelay(50, 150);
|
|
1485
|
+
await humanTypeText(page, text, profile);
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
async parallelHover(target, browserIds) {
|
|
1489
|
+
return this.executeParallel(browserIds, async (page, browserId) => {
|
|
1490
|
+
const profile = this.getProfile(browserId);
|
|
1491
|
+
const selector = this.resolveSelector(target, browserId);
|
|
1492
|
+
const locator = page.locator(selector);
|
|
1493
|
+
await locator.waitFor({ state: "visible", timeout: 5e3 });
|
|
1494
|
+
await ensureCursor(page);
|
|
1495
|
+
await preActionDelay(profile);
|
|
1496
|
+
const { target: hoverTarget } = await getClickTarget(page, locator);
|
|
1497
|
+
const fromPos = this.getMousePos(browserId);
|
|
1498
|
+
await humanMouseMove(page, fromPos, hoverTarget, profile);
|
|
1499
|
+
this.setMousePos(browserId, hoverTarget);
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
async parallelSelectOption(target, values, browserIds) {
|
|
1503
|
+
return this.executeParallel(browserIds, async (page, browserId) => {
|
|
1504
|
+
const profile = this.getProfile(browserId);
|
|
1505
|
+
const selector = this.resolveSelector(target, browserId);
|
|
1506
|
+
const locator = page.locator(selector);
|
|
1507
|
+
await locator.waitFor({ state: "visible", timeout: 5e3 });
|
|
1508
|
+
await ensureCursor(page);
|
|
1509
|
+
await preActionDelay(profile);
|
|
1510
|
+
const { target: clickTarget } = await getClickTarget(page, locator);
|
|
1511
|
+
const fromPos = this.getMousePos(browserId);
|
|
1512
|
+
await humanClick(page, clickTarget, fromPos, profile);
|
|
1513
|
+
this.setMousePos(browserId, clickTarget);
|
|
1514
|
+
await humanDelay(200, 500);
|
|
1515
|
+
return await locator.selectOption(values);
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
async parallelPressKey(key, browserIds) {
|
|
1519
|
+
return this.executeParallel(browserIds, async (page, browserId) => {
|
|
1520
|
+
await preActionDelay(this.getProfile(browserId));
|
|
1521
|
+
await page.keyboard.press(key);
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
async parallelClickCoordinates(coordinates, options = {}) {
|
|
1525
|
+
const browserIds = Object.keys(coordinates);
|
|
1526
|
+
return this.executeParallel(
|
|
1527
|
+
browserIds,
|
|
1528
|
+
async (page, browserId) => {
|
|
1529
|
+
const profile = this.getProfile(browserId);
|
|
1530
|
+
const target = coordinates[browserId];
|
|
1531
|
+
if (!target) {
|
|
1532
|
+
throw new Error(`No coordinates provided for browser ${browserId}`);
|
|
1533
|
+
}
|
|
1534
|
+
const viewport = page.viewportSize();
|
|
1535
|
+
if (!viewport) {
|
|
1536
|
+
throw new Error("Viewport size is not available for the page");
|
|
1537
|
+
}
|
|
1538
|
+
await ensureCursor(page);
|
|
1539
|
+
await preActionDelay(profile);
|
|
1540
|
+
if (options.fromViewport !== false) {
|
|
1541
|
+
const clickX = Math.max(0, Math.min(Math.round(target.x), viewport.width - 1));
|
|
1542
|
+
const clickY = Math.max(0, Math.min(Math.round(target.y), viewport.height - 1));
|
|
1543
|
+
const fromPos = this.getMousePos(browserId);
|
|
1544
|
+
await humanClick(page, { x: clickX, y: clickY }, fromPos, profile);
|
|
1545
|
+
this.setMousePos(browserId, { x: clickX, y: clickY });
|
|
1546
|
+
const scrollY = await page.evaluate(() => globalThis.scrollY ?? 0);
|
|
1547
|
+
return {
|
|
1548
|
+
clickedAt: { x: clickX, y: clickY },
|
|
1549
|
+
documentY: scrollY + clickY,
|
|
1550
|
+
scrollY
|
|
1551
|
+
};
|
|
1552
|
+
} else {
|
|
1553
|
+
const desiredCenter = options.center ?? viewport.height / 2;
|
|
1554
|
+
const desiredScroll = Math.max(0, Math.floor(target.y - desiredCenter));
|
|
1555
|
+
await page.evaluate((scrollY) => {
|
|
1556
|
+
globalThis.scrollTo?.(0, scrollY);
|
|
1557
|
+
}, desiredScroll);
|
|
1558
|
+
const actualScroll = await page.evaluate(() => globalThis.scrollY ?? 0);
|
|
1559
|
+
const clickX = Math.max(0, Math.min(Math.round(target.x), viewport.width - 1));
|
|
1560
|
+
const clickY = Math.round(target.y - actualScroll);
|
|
1561
|
+
if (clickY < 0 || clickY > viewport.height) {
|
|
1562
|
+
throw new Error(
|
|
1563
|
+
`Target Y=${target.y} is outside viewport after scrolling (scrollY=${actualScroll}, viewportHeight=${viewport.height})`
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
const fromPos = this.getMousePos(browserId);
|
|
1567
|
+
await humanClick(page, { x: clickX, y: clickY }, fromPos, profile);
|
|
1568
|
+
this.setMousePos(browserId, { x: clickX, y: clickY });
|
|
1569
|
+
return {
|
|
1570
|
+
clickedAt: { x: clickX, y: clickY },
|
|
1571
|
+
documentY: target.y,
|
|
1572
|
+
scrollY: actualScroll
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
},
|
|
1576
|
+
{ timeout: options.timeout ?? this.defaultTimeout }
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Click at center of normalized bounding box (from OmniParser output)
|
|
1581
|
+
*/
|
|
1582
|
+
async parallelClickNormalizedBox(box, browserIds, options = {}) {
|
|
1583
|
+
const [x1, y1, x2, y2] = box;
|
|
1584
|
+
if (x1 < 0 || x1 > 1 || x2 < 0 || x2 > 1 || y1 < 0 || y1 > 1 || y2 < 0 || y2 > 1) {
|
|
1585
|
+
throw new Error("Coordinates must be normalized (0-1 range)");
|
|
1586
|
+
}
|
|
1587
|
+
const normalizedX = (x1 + x2) / 2;
|
|
1588
|
+
const normalizedY = (y1 + y2) / 2;
|
|
1589
|
+
return this.executeParallel(
|
|
1590
|
+
browserIds,
|
|
1591
|
+
async (page, browserId) => {
|
|
1592
|
+
const profile = this.getProfile(browserId);
|
|
1593
|
+
const viewport = page.viewportSize();
|
|
1594
|
+
if (!viewport) {
|
|
1595
|
+
throw new Error("Viewport size is not available");
|
|
1596
|
+
}
|
|
1597
|
+
const absoluteX = Math.round(normalizedX * viewport.width);
|
|
1598
|
+
const absoluteY = Math.round(normalizedY * viewport.height);
|
|
1599
|
+
await ensureCursor(page);
|
|
1600
|
+
await preActionDelay(profile);
|
|
1601
|
+
const fromPos = this.getMousePos(browserId);
|
|
1602
|
+
await humanMouseMove(page, fromPos, { x: absoluteX, y: absoluteY }, profile);
|
|
1603
|
+
await page.mouse.down({ button: options.button || "left" });
|
|
1604
|
+
await humanDelay(50, 120);
|
|
1605
|
+
await page.mouse.up({ button: options.button || "left" });
|
|
1606
|
+
if (options.clickCount && options.clickCount > 1) {
|
|
1607
|
+
for (let i = 1; i < options.clickCount; i++) {
|
|
1608
|
+
await humanDelay(80, 150);
|
|
1609
|
+
await page.mouse.down({ button: options.button || "left" });
|
|
1610
|
+
await humanDelay(30, 80);
|
|
1611
|
+
await page.mouse.up({ button: options.button || "left" });
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
this.setMousePos(browserId, { x: absoluteX, y: absoluteY });
|
|
1615
|
+
return { absoluteX, absoluteY, viewport };
|
|
1616
|
+
},
|
|
1617
|
+
{ timeout: options.timeout ?? this.defaultTimeout }
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
// ==================== CODE EXECUTION ====================
|
|
1621
|
+
async parallelRunCode(code, browserIds) {
|
|
1622
|
+
return this.executeParallel(browserIds, async (page) => {
|
|
1623
|
+
const AsyncFunction = Object.getPrototypeOf(async function() {
|
|
1624
|
+
}).constructor;
|
|
1625
|
+
const fn = new AsyncFunction("page", code);
|
|
1626
|
+
return await fn(page);
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
async parallelRunCodeWithVariables(codeTemplate, variables) {
|
|
1630
|
+
const browserIds = Object.keys(variables);
|
|
1631
|
+
return this.executeParallel(browserIds, async (page, browserId) => {
|
|
1632
|
+
const vars = variables[browserId] || {};
|
|
1633
|
+
let code = codeTemplate;
|
|
1634
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
1635
|
+
code = code.replace(new RegExp(`{{${key}}}`, "g"), value);
|
|
1636
|
+
}
|
|
1637
|
+
const AsyncFunction = Object.getPrototypeOf(async function() {
|
|
1638
|
+
}).constructor;
|
|
1639
|
+
const fn = new AsyncFunction("page", code);
|
|
1640
|
+
return await fn(page);
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
async parallelEvaluate(script, browserIds) {
|
|
1644
|
+
return this.executeParallel(browserIds, async (page) => {
|
|
1645
|
+
return await page.evaluate(script);
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
// ==================== WAITING ====================
|
|
1649
|
+
async parallelWaitFor(options, browserIds) {
|
|
1650
|
+
const waitTimeMs = options.time ? options.time * 1e3 : 0;
|
|
1651
|
+
const textTimeout = options.timeoutMs ?? 3e4;
|
|
1652
|
+
const domStableTimeout = options.timeoutMs ?? Math.max(options.domStableMs ?? 0, 5e3);
|
|
1653
|
+
const timeout = waitTimeMs + Math.max(textTimeout, domStableTimeout) + 5e3;
|
|
1654
|
+
return this.executeParallel(browserIds, async (page) => {
|
|
1655
|
+
if (options.time) {
|
|
1656
|
+
await page.waitForTimeout(options.time * 1e3);
|
|
1657
|
+
}
|
|
1658
|
+
if (options.text) {
|
|
1659
|
+
await page.getByText(options.text).waitFor({ timeout: textTimeout });
|
|
1660
|
+
}
|
|
1661
|
+
if (options.textGone) {
|
|
1662
|
+
await page.getByText(options.textGone).waitFor({ state: "hidden", timeout: textTimeout });
|
|
1663
|
+
}
|
|
1664
|
+
if (options.domStableMs) {
|
|
1665
|
+
await this.waitForDomStable(page, options.domStableMs, domStableTimeout);
|
|
1666
|
+
}
|
|
1667
|
+
}, { timeout });
|
|
1668
|
+
}
|
|
1669
|
+
// ==================== FILE OPERATIONS (downloads, uploads, drag&drop) ====================
|
|
1670
|
+
/** Cached project files directory path */
|
|
1671
|
+
projectFilesDir = void 0;
|
|
1672
|
+
/** Fetch project files directory from API (cached) */
|
|
1673
|
+
async getProjectFilesDir() {
|
|
1674
|
+
if (this.projectFilesDir !== void 0) return this.projectFilesDir;
|
|
1675
|
+
const apiUrl = process.env.API_SERVER_URL;
|
|
1676
|
+
const userId = process.env.USER_ID;
|
|
1677
|
+
const deviceId = process.env.DEVICE_ID;
|
|
1678
|
+
if (!apiUrl || !userId) {
|
|
1679
|
+
this.projectFilesDir = null;
|
|
1680
|
+
return null;
|
|
1681
|
+
}
|
|
1682
|
+
try {
|
|
1683
|
+
const params = new URLSearchParams({ userId });
|
|
1684
|
+
if (deviceId) params.set("deviceId", deviceId);
|
|
1685
|
+
const resp = await fetch(`${apiUrl}/api/project-path?${params}`);
|
|
1686
|
+
const data = await resp.json();
|
|
1687
|
+
this.projectFilesDir = data.localPath;
|
|
1688
|
+
console.error(`[Executor] Project files dir: ${this.projectFilesDir}`);
|
|
1689
|
+
return this.projectFilesDir;
|
|
1690
|
+
} catch (e) {
|
|
1691
|
+
console.error(`[Executor] Failed to fetch project path:`, e);
|
|
1692
|
+
this.projectFilesDir = null;
|
|
1693
|
+
return null;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
/** Configure browser to download files to project directory */
|
|
1697
|
+
async setupDownloads(browserIds) {
|
|
1698
|
+
const dir = await this.getProjectFilesDir();
|
|
1699
|
+
if (!dir) {
|
|
1700
|
+
throw new Error("Project files directory not available \u2014 file relay may not be connected");
|
|
1701
|
+
}
|
|
1702
|
+
return this.executeParallel(browserIds, async (page) => {
|
|
1703
|
+
const cdp = await page.context().newCDPSession(page);
|
|
1704
|
+
try {
|
|
1705
|
+
await cdp.send("Page.setDownloadBehavior", {
|
|
1706
|
+
behavior: "allow",
|
|
1707
|
+
downloadPath: dir
|
|
1708
|
+
});
|
|
1709
|
+
} finally {
|
|
1710
|
+
await cdp.detach().catch(() => {
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
return `Downloads \u2192 ${dir}`;
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
/** List files in the project directory */
|
|
1717
|
+
async listProjectFiles() {
|
|
1718
|
+
const dir = await this.getProjectFilesDir();
|
|
1719
|
+
if (!dir) return [];
|
|
1720
|
+
try {
|
|
1721
|
+
const files = await readdir(dir);
|
|
1722
|
+
const items = await Promise.all(
|
|
1723
|
+
files.filter((file) => !file.startsWith(".")).map(async (file) => {
|
|
1724
|
+
const fileStat = await stat(join2(dir, file));
|
|
1725
|
+
return { name: file, size: fileStat.size, modified: fileStat.mtimeMs };
|
|
1726
|
+
})
|
|
1727
|
+
);
|
|
1728
|
+
return items;
|
|
1729
|
+
} catch {
|
|
1730
|
+
}
|
|
1731
|
+
return [];
|
|
1732
|
+
}
|
|
1733
|
+
/** Upload a file from project directory via filechooser (selector-based) */
|
|
1734
|
+
async parallelUploadFile(target, fileName, browserIds) {
|
|
1735
|
+
const dir = await this.getProjectFilesDir();
|
|
1736
|
+
if (!dir) {
|
|
1737
|
+
throw new Error("Project files directory not available");
|
|
1738
|
+
}
|
|
1739
|
+
return this.executeParallel(browserIds, async (page, browserId) => {
|
|
1740
|
+
const profile = this.getProfile(browserId);
|
|
1741
|
+
const selector = this.resolveSelector(target, browserId);
|
|
1742
|
+
const locator = page.locator(selector);
|
|
1743
|
+
await locator.waitFor({ state: "visible", timeout: 5e3 });
|
|
1744
|
+
await ensureCursor(page);
|
|
1745
|
+
await preActionDelay(profile);
|
|
1746
|
+
const fileChooserPromise = page.waitForEvent("filechooser", { timeout: 1e4 });
|
|
1747
|
+
const { target: clickTarget } = await getClickTarget(page, locator);
|
|
1748
|
+
const fromPos = this.getMousePos(browserId);
|
|
1749
|
+
await humanClick(page, clickTarget, fromPos, profile);
|
|
1750
|
+
this.setMousePos(browserId, clickTarget);
|
|
1751
|
+
const fileChooser = await fileChooserPromise;
|
|
1752
|
+
const filePath = join2(dir, fileName);
|
|
1753
|
+
await fileChooser.setFiles(filePath);
|
|
1754
|
+
return `Uploaded "${fileName}" from ${dir}`;
|
|
1755
|
+
}, { timeout: 15e3 });
|
|
1756
|
+
}
|
|
1757
|
+
// ==================== BATCH FORM FILL ====================
|
|
1758
|
+
async parallelFillForm(fields, browserIds) {
|
|
1759
|
+
return this.executeParallel(browserIds, async (page, browserId) => {
|
|
1760
|
+
const profile = this.getProfile(browserId);
|
|
1761
|
+
const filled = [];
|
|
1762
|
+
for (const field of fields) {
|
|
1763
|
+
const selector = this.resolveSelector(field, browserId);
|
|
1764
|
+
const locator = page.locator(selector);
|
|
1765
|
+
await locator.waitFor({ state: "visible", timeout: 5e3 });
|
|
1766
|
+
await ensureCursor(page);
|
|
1767
|
+
await preActionDelay(profile);
|
|
1768
|
+
if (field.type === "checkbox") {
|
|
1769
|
+
if (this.isTruthyFieldValue(field.value)) {
|
|
1770
|
+
await locator.check();
|
|
1771
|
+
} else {
|
|
1772
|
+
await locator.uncheck();
|
|
1773
|
+
}
|
|
1774
|
+
filled.push(`${selector}=${field.value}`);
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1777
|
+
if (field.type === "radio") {
|
|
1778
|
+
await locator.check();
|
|
1779
|
+
filled.push(`${selector}=${field.value}`);
|
|
1780
|
+
continue;
|
|
1781
|
+
}
|
|
1782
|
+
if (field.type === "combobox") {
|
|
1783
|
+
const { target: clickTarget2 } = await getClickTarget(page, locator);
|
|
1784
|
+
const fromPos2 = this.getMousePos(browserId);
|
|
1785
|
+
await humanClick(page, clickTarget2, fromPos2, profile);
|
|
1786
|
+
this.setMousePos(browserId, clickTarget2);
|
|
1787
|
+
await humanDelay(120, 240);
|
|
1788
|
+
await locator.selectOption(field.value);
|
|
1789
|
+
filled.push(`${selector}="${field.value}"`);
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1792
|
+
const { target: clickTarget } = await getClickTarget(page, locator);
|
|
1793
|
+
const fromPos = this.getMousePos(browserId);
|
|
1794
|
+
await humanClick(page, clickTarget, fromPos, profile);
|
|
1795
|
+
this.setMousePos(browserId, clickTarget);
|
|
1796
|
+
await humanDelay(80, 200);
|
|
1797
|
+
await page.keyboard.press("Meta+a");
|
|
1798
|
+
await humanDelay(30, 80);
|
|
1799
|
+
await page.keyboard.press("Backspace");
|
|
1800
|
+
await humanDelay(50, 150);
|
|
1801
|
+
await humanTypeText(page, field.value, profile);
|
|
1802
|
+
filled.push(`${selector}="${field.value}"`);
|
|
1803
|
+
}
|
|
1804
|
+
return `Filled ${filled.length} fields: ${filled.join(", ")}`;
|
|
1805
|
+
}, { timeout: fields.length * 5e3 + 5e3 });
|
|
1806
|
+
}
|
|
1807
|
+
// ==================== ELEMENT DRAG & DROP ====================
|
|
1808
|
+
async parallelDrag(startTarget, endTarget, browserIds) {
|
|
1809
|
+
return this.executeParallel(browserIds, async (page, browserId) => {
|
|
1810
|
+
const profile = this.getProfile(browserId);
|
|
1811
|
+
const startSelector = this.resolveSelector(startTarget, browserId);
|
|
1812
|
+
const endSelector = this.resolveSelector(endTarget, browserId);
|
|
1813
|
+
const startLocator = page.locator(startSelector);
|
|
1814
|
+
const endLocator = page.locator(endSelector);
|
|
1815
|
+
await startLocator.waitFor({ state: "visible", timeout: 5e3 });
|
|
1816
|
+
await endLocator.waitFor({ state: "visible", timeout: 5e3 });
|
|
1817
|
+
await ensureCursor(page);
|
|
1818
|
+
await preActionDelay(profile);
|
|
1819
|
+
const { target: startPos } = await getClickTarget(page, startLocator);
|
|
1820
|
+
const { target: endPos } = await getClickTarget(page, endLocator);
|
|
1821
|
+
const fromPos = this.getMousePos(browserId);
|
|
1822
|
+
await humanMouseMove(page, fromPos, startPos, profile);
|
|
1823
|
+
await humanDelay(50, 150);
|
|
1824
|
+
await page.mouse.down();
|
|
1825
|
+
await humanDelay(100, 250);
|
|
1826
|
+
await humanMouseMove(page, startPos, endPos, profile);
|
|
1827
|
+
await humanDelay(50, 150);
|
|
1828
|
+
await page.mouse.up();
|
|
1829
|
+
this.setMousePos(browserId, endPos);
|
|
1830
|
+
return `Dragged "${startSelector}" \u2192 "${endSelector}"`;
|
|
1831
|
+
}, { timeout: 15e3 });
|
|
1832
|
+
}
|
|
1833
|
+
// ==================== TAB MANAGEMENT ====================
|
|
1834
|
+
async parallelTabs(action, options = {}, browserIds) {
|
|
1835
|
+
return this.executeParallel(browserIds, async (page, browserId) => {
|
|
1836
|
+
const browser = this.browsers.get(browserId);
|
|
1837
|
+
if (!browser) throw new Error(`Browser ${browserId} not found`);
|
|
1838
|
+
const context = browser.context;
|
|
1839
|
+
const pages = context.pages();
|
|
1840
|
+
switch (action) {
|
|
1841
|
+
case "list": {
|
|
1842
|
+
return pages.map((p, i) => ({
|
|
1843
|
+
index: i,
|
|
1844
|
+
url: p.url(),
|
|
1845
|
+
active: p === browser.page
|
|
1846
|
+
}));
|
|
1847
|
+
}
|
|
1848
|
+
case "new": {
|
|
1849
|
+
const newPage = await context.newPage();
|
|
1850
|
+
if (options.url) {
|
|
1851
|
+
await newPage.goto(options.url, { waitUntil: "domcontentloaded" });
|
|
1852
|
+
}
|
|
1853
|
+
browser.page = newPage;
|
|
1854
|
+
newPage.setDefaultTimeout(this.defaultTimeout);
|
|
1855
|
+
newPage.setDefaultNavigationTimeout(6e4);
|
|
1856
|
+
this._setupPageTracking(browserId, newPage);
|
|
1857
|
+
return { index: context.pages().length - 1, url: newPage.url() };
|
|
1858
|
+
}
|
|
1859
|
+
case "close": {
|
|
1860
|
+
const idx = options.index ?? pages.indexOf(browser.page);
|
|
1861
|
+
if (idx < 0 || idx >= pages.length) throw new Error(`Tab index ${idx} out of range (0-${pages.length - 1})`);
|
|
1862
|
+
const targetPage = pages[idx];
|
|
1863
|
+
await targetPage.close();
|
|
1864
|
+
if (targetPage === browser.page) {
|
|
1865
|
+
const remaining = context.pages();
|
|
1866
|
+
if (remaining.length > 0) {
|
|
1867
|
+
browser.page = remaining[Math.min(idx, remaining.length - 1)];
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
return { closed: idx, remaining: context.pages().length };
|
|
1871
|
+
}
|
|
1872
|
+
case "select": {
|
|
1873
|
+
if (options.index === void 0) throw new Error("index required for select action");
|
|
1874
|
+
const currentPages = context.pages();
|
|
1875
|
+
if (options.index < 0 || options.index >= currentPages.length) {
|
|
1876
|
+
throw new Error(`Tab index ${options.index} out of range (0-${currentPages.length - 1})`);
|
|
1877
|
+
}
|
|
1878
|
+
browser.page = currentPages[options.index];
|
|
1879
|
+
await browser.page.bringToFront();
|
|
1880
|
+
return { selected: options.index, url: browser.page.url() };
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
}, { timeout: 3e4 });
|
|
1884
|
+
}
|
|
1885
|
+
/** Set up console/network tracking on a new page */
|
|
1886
|
+
_setupPageTracking(browserId, page) {
|
|
1887
|
+
const consoleLog = this.consoleMessages.get(browserId);
|
|
1888
|
+
if (consoleLog) {
|
|
1889
|
+
page.on("console", (msg) => {
|
|
1890
|
+
consoleLog.push({ type: msg.type(), text: msg.text(), timestamp: Date.now() });
|
|
1891
|
+
if (consoleLog.length > 1e3) consoleLog.shift();
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
const networkLog = this.networkRequests.get(browserId);
|
|
1895
|
+
if (networkLog) {
|
|
1896
|
+
page.on("response", (response) => {
|
|
1897
|
+
const req = response.request();
|
|
1898
|
+
networkLog.push({
|
|
1899
|
+
url: req.url(),
|
|
1900
|
+
method: req.method(),
|
|
1901
|
+
status: response.status(),
|
|
1902
|
+
resourceType: req.resourceType(),
|
|
1903
|
+
timestamp: Date.now()
|
|
1904
|
+
});
|
|
1905
|
+
if (networkLog.length > 1e3) networkLog.shift();
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
// ==================== DIALOG HANDLING ====================
|
|
1910
|
+
async parallelHandleDialog(accept, promptText, browserIds) {
|
|
1911
|
+
return this.executeParallel(browserIds, async (page) => {
|
|
1912
|
+
return new Promise((resolve, reject) => {
|
|
1913
|
+
const timeout = setTimeout(() => {
|
|
1914
|
+
reject(new Error("No dialog appeared within 10 seconds"));
|
|
1915
|
+
}, 1e4);
|
|
1916
|
+
page.once("dialog", async (dialog) => {
|
|
1917
|
+
clearTimeout(timeout);
|
|
1918
|
+
const type = dialog.type();
|
|
1919
|
+
const message = dialog.message();
|
|
1920
|
+
try {
|
|
1921
|
+
if (accept) {
|
|
1922
|
+
await dialog.accept(promptText);
|
|
1923
|
+
} else {
|
|
1924
|
+
await dialog.dismiss();
|
|
1925
|
+
}
|
|
1926
|
+
resolve(`${accept ? "Accepted" : "Dismissed"} ${type} dialog: "${message}"`);
|
|
1927
|
+
} catch (e) {
|
|
1928
|
+
reject(e);
|
|
1929
|
+
}
|
|
1930
|
+
});
|
|
1931
|
+
});
|
|
1932
|
+
}, { timeout: 15e3 });
|
|
1933
|
+
}
|
|
1934
|
+
// ==================== CONSOLE MESSAGES ====================
|
|
1935
|
+
async parallelConsoleMessages(onlyErrors, browserIds) {
|
|
1936
|
+
await this.ensureBrowsersLoaded();
|
|
1937
|
+
const ids = browserIds ?? this.getBrowserIds();
|
|
1938
|
+
const results = ids.map((id) => {
|
|
1939
|
+
let messages = this.consoleMessages.get(id) || [];
|
|
1940
|
+
if (onlyErrors) {
|
|
1941
|
+
messages = messages.filter((m) => m.type === "error");
|
|
1942
|
+
}
|
|
1943
|
+
return {
|
|
1944
|
+
browserId: id,
|
|
1945
|
+
success: true,
|
|
1946
|
+
result: [...messages],
|
|
1947
|
+
duration: 0
|
|
1948
|
+
};
|
|
1949
|
+
});
|
|
1950
|
+
return {
|
|
1951
|
+
results,
|
|
1952
|
+
totalDuration: 0,
|
|
1953
|
+
successCount: results.length,
|
|
1954
|
+
failureCount: 0
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
// ==================== NETWORK REQUESTS ====================
|
|
1958
|
+
async parallelNetworkRequests(browserIds) {
|
|
1959
|
+
await this.ensureBrowsersLoaded();
|
|
1960
|
+
const ids = browserIds ?? this.getBrowserIds();
|
|
1961
|
+
const results = ids.map((id) => {
|
|
1962
|
+
const requests = this.networkRequests.get(id) || [];
|
|
1963
|
+
return {
|
|
1964
|
+
browserId: id,
|
|
1965
|
+
success: true,
|
|
1966
|
+
result: [...requests],
|
|
1967
|
+
duration: 0
|
|
1968
|
+
};
|
|
1969
|
+
});
|
|
1970
|
+
return {
|
|
1971
|
+
results,
|
|
1972
|
+
totalDuration: 0,
|
|
1973
|
+
successCount: results.length,
|
|
1974
|
+
failureCount: 0
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
/** Drag and drop a file onto a dropzone (selector-based) */
|
|
1978
|
+
async parallelDragDropFile(target, fileName, browserIds) {
|
|
1979
|
+
const dir = await this.getProjectFilesDir();
|
|
1980
|
+
if (!dir) {
|
|
1981
|
+
throw new Error("Project files directory not available");
|
|
1982
|
+
}
|
|
1983
|
+
return this.executeParallel(browserIds, async (page, browserId) => {
|
|
1984
|
+
const profile = this.getProfile(browserId);
|
|
1985
|
+
const selector = this.resolveSelector(target, browserId);
|
|
1986
|
+
const locator = page.locator(selector);
|
|
1987
|
+
await locator.waitFor({ state: "visible", timeout: 5e3 });
|
|
1988
|
+
await ensureCursor(page);
|
|
1989
|
+
await preActionDelay(profile);
|
|
1990
|
+
const { target: dropTarget } = await getClickTarget(page, locator);
|
|
1991
|
+
const fromPos = this.getMousePos(browserId);
|
|
1992
|
+
await humanMouseMove(page, fromPos, dropTarget, profile);
|
|
1993
|
+
this.setMousePos(browserId, dropTarget);
|
|
1994
|
+
const filePath = join2(dir, fileName);
|
|
1995
|
+
const fileBytes = Array.from(await readFile(filePath));
|
|
1996
|
+
const mimeType = this.getMimeType(fileName);
|
|
1997
|
+
await page.evaluate(async ({
|
|
1998
|
+
x,
|
|
1999
|
+
y,
|
|
2000
|
+
fileName: fileName2,
|
|
2001
|
+
mimeType: mimeType2,
|
|
2002
|
+
fileBytes: fileBytes2
|
|
2003
|
+
}) => {
|
|
2004
|
+
const dropzone = document.elementFromPoint(x, y);
|
|
2005
|
+
if (!dropzone) throw new Error("No element at drop coordinates");
|
|
2006
|
+
const dataTransfer = new DataTransfer();
|
|
2007
|
+
const file = new File([new Uint8Array(fileBytes2)], fileName2, { type: mimeType2 });
|
|
2008
|
+
dataTransfer.items.add(file);
|
|
2009
|
+
const events = ["dragenter", "dragover", "drop"];
|
|
2010
|
+
for (const eventType of events) {
|
|
2011
|
+
const event = new DragEvent(eventType, {
|
|
2012
|
+
bubbles: true,
|
|
2013
|
+
cancelable: true,
|
|
2014
|
+
dataTransfer
|
|
2015
|
+
});
|
|
2016
|
+
dropzone.dispatchEvent(event);
|
|
2017
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2018
|
+
}
|
|
2019
|
+
}, { x: dropTarget.x, y: dropTarget.y, fileName, mimeType, fileBytes });
|
|
2020
|
+
return `Dropped "${fileName}" onto element`;
|
|
2021
|
+
}, { timeout: 15e3 });
|
|
2022
|
+
}
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
// client/index.ts
|
|
2028
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2029
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2030
|
+
import { readFileSync } from "node:fs";
|
|
2031
|
+
import { z } from "zod";
|
|
2032
|
+
import WebSocket from "ws";
|
|
2033
|
+
var args = process.argv.slice(2);
|
|
2034
|
+
var getArg = (name) => {
|
|
2035
|
+
const i = args.indexOf(`--${name}`);
|
|
2036
|
+
return i >= 0 ? args[i + 1] : void 0;
|
|
2037
|
+
};
|
|
2038
|
+
function getOptionalPort(argName, envName) {
|
|
2039
|
+
const raw = getArg(argName) ?? process.env[envName];
|
|
2040
|
+
if (raw === void 0 || raw === "") return void 0;
|
|
2041
|
+
const port = Number.parseInt(raw, 10);
|
|
2042
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
2043
|
+
console.error(`[ornold] Invalid --${argName}: ${raw}`);
|
|
2044
|
+
process.exit(1);
|
|
2045
|
+
}
|
|
2046
|
+
return port;
|
|
2047
|
+
}
|
|
2048
|
+
var TOKEN = getArg("token") || process.env.ORNOLD_TOKEN || "";
|
|
2049
|
+
var SERVER_URL = getArg("server") || process.env.ORNOLD_SERVER || "wss://ornold-mcp.fly.dev/bridge";
|
|
2050
|
+
var LINKEN_PORT = getOptionalPort("linken-port", "LINKEN_PORT");
|
|
2051
|
+
var DOLPHIN_PORT = getOptionalPort("dolphin-port", "DOLPHIN_PORT");
|
|
2052
|
+
var ENABLED_VENDORS = [
|
|
2053
|
+
...LINKEN_PORT !== void 0 ? ["linken"] : [],
|
|
2054
|
+
...DOLPHIN_PORT !== void 0 ? ["dolphin"] : []
|
|
2055
|
+
];
|
|
2056
|
+
function getPackageVersion() {
|
|
2057
|
+
try {
|
|
2058
|
+
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
2059
|
+
return packageJson.version || "0.0.0";
|
|
2060
|
+
} catch {
|
|
2061
|
+
return "0.0.0";
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
var CLIENT_VERSION = getPackageVersion();
|
|
2065
|
+
if (!TOKEN) {
|
|
2066
|
+
console.error("Usage: npx ornold-mcp --token <your_token>");
|
|
2067
|
+
console.error("Get your token at https://ornold-mcp.fly.dev");
|
|
2068
|
+
process.exit(1);
|
|
2069
|
+
}
|
|
2070
|
+
var ws = null;
|
|
2071
|
+
var serverReady = false;
|
|
2072
|
+
var pendingCalls = /* @__PURE__ */ new Map();
|
|
2073
|
+
var callCounter = 0;
|
|
2074
|
+
function connectServer() {
|
|
2075
|
+
return new Promise((resolve, reject) => {
|
|
2076
|
+
console.error(`[ornold] Connecting to ${SERVER_URL}...`);
|
|
2077
|
+
ws = new WebSocket(`${SERVER_URL}?token=${TOKEN}`);
|
|
2078
|
+
ws.on("open", () => {
|
|
2079
|
+
console.error("[ornold] Connected to server");
|
|
2080
|
+
});
|
|
2081
|
+
ws.on("message", (data) => {
|
|
2082
|
+
try {
|
|
2083
|
+
const msg = JSON.parse(data.toString());
|
|
2084
|
+
if (msg.type === "connected") {
|
|
2085
|
+
serverReady = true;
|
|
2086
|
+
console.error(`[ornold] Authenticated. Plan: ${msg.plan}, Profiles: ${msg.limits?.profiles}, Captchas: ${msg.limits?.captchas}`);
|
|
2087
|
+
resolve();
|
|
2088
|
+
}
|
|
2089
|
+
if (msg.type === "error") {
|
|
2090
|
+
console.error(`[ornold] Server error: ${msg.message}`);
|
|
2091
|
+
if (!serverReady) reject(new Error(msg.message));
|
|
2092
|
+
}
|
|
2093
|
+
if (msg.type === "tool_result" && msg.requestId) {
|
|
2094
|
+
const p = pendingCalls.get(msg.requestId);
|
|
2095
|
+
if (p) {
|
|
2096
|
+
clearTimeout(p.timer);
|
|
2097
|
+
pendingCalls.delete(msg.requestId);
|
|
2098
|
+
p.resolve(msg.result);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
if (msg.type === "execute") {
|
|
2102
|
+
handleExecuteCommand(msg).then((result) => {
|
|
2103
|
+
ws?.send(JSON.stringify({
|
|
2104
|
+
type: "execute_result",
|
|
2105
|
+
requestId: msg.requestId,
|
|
2106
|
+
result
|
|
2107
|
+
}));
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
} catch {
|
|
2111
|
+
}
|
|
2112
|
+
});
|
|
2113
|
+
ws.on("close", (code) => {
|
|
2114
|
+
serverReady = false;
|
|
2115
|
+
if (code === 4001) {
|
|
2116
|
+
console.error("[ornold] Invalid token. Get your token at https://ornold-mcp.fly.dev");
|
|
2117
|
+
process.exit(1);
|
|
2118
|
+
}
|
|
2119
|
+
console.error(`[ornold] Disconnected (${code}). Reconnecting in 3s...`);
|
|
2120
|
+
setTimeout(connectServer, 3e3);
|
|
2121
|
+
});
|
|
2122
|
+
ws.on("error", (err) => {
|
|
2123
|
+
console.error(`[ornold] WS error: ${err.message}`);
|
|
2124
|
+
});
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
function callServer(tool, toolArgs) {
|
|
2128
|
+
if (!ws || ws.readyState !== WebSocket.OPEN || !serverReady) {
|
|
2129
|
+
return Promise.resolve({ content: "Not connected to Ornold server. Check your token and internet connection.", isError: true });
|
|
2130
|
+
}
|
|
2131
|
+
const requestId = `r_${++callCounter}`;
|
|
2132
|
+
return new Promise((resolve) => {
|
|
2133
|
+
const timer = setTimeout(() => {
|
|
2134
|
+
pendingCalls.delete(requestId);
|
|
2135
|
+
resolve({ content: "Server timeout", isError: true });
|
|
2136
|
+
}, 12e4);
|
|
2137
|
+
pendingCalls.set(requestId, { resolve, timer });
|
|
2138
|
+
ws.send(JSON.stringify({
|
|
2139
|
+
type: "tool_call",
|
|
2140
|
+
requestId,
|
|
2141
|
+
tool,
|
|
2142
|
+
args: toolArgs
|
|
2143
|
+
}));
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
var executor = null;
|
|
2147
|
+
async function getExecutor() {
|
|
2148
|
+
if (!executor) {
|
|
2149
|
+
const { MultiBrowserExecutor: MultiBrowserExecutor2 } = await Promise.resolve().then(() => (init_multi_browser(), multi_browser_exports));
|
|
2150
|
+
executor = new MultiBrowserExecutor2();
|
|
2151
|
+
}
|
|
2152
|
+
return executor;
|
|
2153
|
+
}
|
|
2154
|
+
function getSelectorTarget(params) {
|
|
2155
|
+
const selector = typeof params.selector === "string" ? params.selector : void 0;
|
|
2156
|
+
const ref = typeof params.ref === "string" || typeof params.ref === "number" ? String(params.ref) : void 0;
|
|
2157
|
+
if (!selector && !ref) {
|
|
2158
|
+
throw new Error("Either selector or ref is required");
|
|
2159
|
+
}
|
|
2160
|
+
return selector ? { selector } : { ref };
|
|
2161
|
+
}
|
|
2162
|
+
async function handleExecuteCommand(msg) {
|
|
2163
|
+
const { command, params } = msg;
|
|
2164
|
+
try {
|
|
2165
|
+
switch (command) {
|
|
2166
|
+
// CDP commands — executed via local Patchright
|
|
2167
|
+
case "cdp_status": {
|
|
2168
|
+
const exec = await getExecutor();
|
|
2169
|
+
await exec.ensureBrowsersLoaded();
|
|
2170
|
+
const status = await exec.getDetailedStatus({ checkContent: !!params.checkContent });
|
|
2171
|
+
return { content: JSON.stringify(status), isError: false };
|
|
2172
|
+
}
|
|
2173
|
+
case "cdp_tabs": {
|
|
2174
|
+
const exec = await getExecutor();
|
|
2175
|
+
const r = await exec.parallelTabs(params.action, { index: params.index, url: params.url }, params.browserIds);
|
|
2176
|
+
return { content: formatResult(r), isError: false };
|
|
2177
|
+
}
|
|
2178
|
+
case "cdp_navigate": {
|
|
2179
|
+
const exec = await getExecutor();
|
|
2180
|
+
const r = await exec.parallelNavigate(params.url, params.browserIds);
|
|
2181
|
+
return { content: formatResult(r), isError: false };
|
|
2182
|
+
}
|
|
2183
|
+
case "cdp_navigate_multi": {
|
|
2184
|
+
const exec = await getExecutor();
|
|
2185
|
+
const r = await exec.parallelNavigateMulti(params.targets);
|
|
2186
|
+
return { content: formatResult(r), isError: false };
|
|
2187
|
+
}
|
|
2188
|
+
case "cdp_click": {
|
|
2189
|
+
const exec = await getExecutor();
|
|
2190
|
+
const r = await exec.parallelClick(getSelectorTarget(params), params.browserIds);
|
|
2191
|
+
return { content: formatResult(r), isError: false };
|
|
2192
|
+
}
|
|
2193
|
+
case "cdp_type": {
|
|
2194
|
+
const exec = await getExecutor();
|
|
2195
|
+
const r = await exec.parallelType(getSelectorTarget(params), params.text, params.browserIds);
|
|
2196
|
+
return { content: formatResult(r), isError: false };
|
|
2197
|
+
}
|
|
2198
|
+
case "cdp_fill": {
|
|
2199
|
+
const exec = await getExecutor();
|
|
2200
|
+
const target = getSelectorTarget(params);
|
|
2201
|
+
const r = params.texts ? await exec.parallelFillMulti(target, params.texts) : await exec.parallelFill(target, params.text, params.browserIds);
|
|
2202
|
+
return { content: formatResult(r), isError: false };
|
|
2203
|
+
}
|
|
2204
|
+
case "cdp_fill_multi": {
|
|
2205
|
+
const exec = await getExecutor();
|
|
2206
|
+
const r = await exec.parallelFillMulti(getSelectorTarget(params), params.texts);
|
|
2207
|
+
return { content: formatResult(r), isError: false };
|
|
2208
|
+
}
|
|
2209
|
+
case "cdp_fill_form": {
|
|
2210
|
+
const exec = await getExecutor();
|
|
2211
|
+
const r = await exec.parallelFillForm(params.fields, params.browserIds);
|
|
2212
|
+
return { content: formatResult(r), isError: false };
|
|
2213
|
+
}
|
|
2214
|
+
case "cdp_drag": {
|
|
2215
|
+
const exec = await getExecutor();
|
|
2216
|
+
const r = await exec.parallelDrag(
|
|
2217
|
+
{
|
|
2218
|
+
selector: typeof params.startSelector === "string" ? params.startSelector : void 0,
|
|
2219
|
+
ref: typeof params.startRef === "string" || typeof params.startRef === "number" ? String(params.startRef) : void 0
|
|
2220
|
+
},
|
|
2221
|
+
{
|
|
2222
|
+
selector: typeof params.endSelector === "string" ? params.endSelector : void 0,
|
|
2223
|
+
ref: typeof params.endRef === "string" || typeof params.endRef === "number" ? String(params.endRef) : void 0
|
|
2224
|
+
},
|
|
2225
|
+
params.browserIds
|
|
2226
|
+
);
|
|
2227
|
+
return { content: formatResult(r), isError: false };
|
|
2228
|
+
}
|
|
2229
|
+
case "cdp_snapshot": {
|
|
2230
|
+
const exec = await getExecutor();
|
|
2231
|
+
const r = await exec.parallelSnapshot(params.browserIds, { compact: params.compact });
|
|
2232
|
+
const parts = (r.results || []).map((item) => {
|
|
2233
|
+
if (!item.success) return `[${item.browserId}] ERROR: ${item.error || "unknown"}`;
|
|
2234
|
+
const result = item.result || {};
|
|
2235
|
+
return `### ${item.browserId}
|
|
2236
|
+
URL: ${result.url || ""}
|
|
2237
|
+
Title: ${result.title || ""}
|
|
2238
|
+
${result.snapshot || ""}`;
|
|
2239
|
+
});
|
|
2240
|
+
return { content: parts.join("\n\n") || "OK", isError: false };
|
|
2241
|
+
}
|
|
2242
|
+
case "cdp_screenshot": {
|
|
2243
|
+
const exec = await getExecutor();
|
|
2244
|
+
const r = await exec.parallelScreenshot(params.browserIds, { fullPage: params.fullPage });
|
|
2245
|
+
const lines = (r.results || []).map((item) => {
|
|
2246
|
+
if (!item.success) return `[${item.browserId}] ERROR: ${item.error || "unknown"}`;
|
|
2247
|
+
const image = Buffer.isBuffer(item.result) ? item.result.toString("base64") : Buffer.from(item.result || []).toString("base64");
|
|
2248
|
+
return `[${item.browserId}] ${image}`;
|
|
2249
|
+
});
|
|
2250
|
+
return { content: lines.join("\n") || "OK", isError: false };
|
|
2251
|
+
}
|
|
2252
|
+
case "cdp_evaluate": {
|
|
2253
|
+
const exec = await getExecutor();
|
|
2254
|
+
const r = await exec.parallelEvaluate(params.script, params.browserIds);
|
|
2255
|
+
return { content: formatResult(r), isError: false };
|
|
2256
|
+
}
|
|
2257
|
+
case "cdp_run_code": {
|
|
2258
|
+
const exec = await getExecutor();
|
|
2259
|
+
const r = await exec.parallelRunCode(params.code, params.browserIds);
|
|
2260
|
+
return { content: formatResult(r), isError: false };
|
|
2261
|
+
}
|
|
2262
|
+
case "cdp_run_code_with_vars": {
|
|
2263
|
+
const exec = await getExecutor();
|
|
2264
|
+
const r = await exec.parallelRunCodeWithVariables(params.codeTemplate, params.variables);
|
|
2265
|
+
return { content: formatResult(r), isError: false };
|
|
2266
|
+
}
|
|
2267
|
+
case "cdp_press_key": {
|
|
2268
|
+
const exec = await getExecutor();
|
|
2269
|
+
const r = await exec.parallelPressKey(params.key, params.browserIds);
|
|
2270
|
+
return { content: formatResult(r), isError: false };
|
|
2271
|
+
}
|
|
2272
|
+
case "cdp_select_option": {
|
|
2273
|
+
const exec = await getExecutor();
|
|
2274
|
+
const r = await exec.parallelSelectOption(getSelectorTarget(params), params.values, params.browserIds);
|
|
2275
|
+
return { content: formatResult(r), isError: false };
|
|
2276
|
+
}
|
|
2277
|
+
case "cdp_hover": {
|
|
2278
|
+
const exec = await getExecutor();
|
|
2279
|
+
const r = await exec.parallelHover(getSelectorTarget(params), params.browserIds);
|
|
2280
|
+
return { content: formatResult(r), isError: false };
|
|
2281
|
+
}
|
|
2282
|
+
case "cdp_go_back": {
|
|
2283
|
+
const exec = await getExecutor();
|
|
2284
|
+
const r = await exec.parallelGoBack(params.browserIds);
|
|
2285
|
+
return { content: formatResult(r), isError: false };
|
|
2286
|
+
}
|
|
2287
|
+
case "cdp_go_forward": {
|
|
2288
|
+
const exec = await getExecutor();
|
|
2289
|
+
const r = await exec.parallelGoForward(params.browserIds);
|
|
2290
|
+
return { content: formatResult(r), isError: false };
|
|
2291
|
+
}
|
|
2292
|
+
case "cdp_reload": {
|
|
2293
|
+
const exec = await getExecutor();
|
|
2294
|
+
const r = await exec.parallelReload(params.browserIds);
|
|
2295
|
+
return { content: formatResult(r), isError: false };
|
|
2296
|
+
}
|
|
2297
|
+
case "cdp_wait_for": {
|
|
2298
|
+
const exec = await getExecutor();
|
|
2299
|
+
const opts = {};
|
|
2300
|
+
if (params.time) opts.time = params.time;
|
|
2301
|
+
if (params.text) opts.text = params.text;
|
|
2302
|
+
if (params.textGone) opts.textGone = params.textGone;
|
|
2303
|
+
if (params.domStableMs) opts.domStableMs = params.domStableMs;
|
|
2304
|
+
if (params.timeoutMs) opts.timeoutMs = params.timeoutMs;
|
|
2305
|
+
const r = await exec.parallelWaitFor(opts, params.browserIds);
|
|
2306
|
+
return { content: formatResult(r), isError: false };
|
|
2307
|
+
}
|
|
2308
|
+
case "cdp_handle_dialog": {
|
|
2309
|
+
const exec = await getExecutor();
|
|
2310
|
+
const r = await exec.parallelHandleDialog(!!params.accept, params.promptText, params.browserIds);
|
|
2311
|
+
return { content: formatResult(r), isError: false };
|
|
2312
|
+
}
|
|
2313
|
+
case "cdp_console_messages": {
|
|
2314
|
+
const exec = await getExecutor();
|
|
2315
|
+
const r = await exec.parallelConsoleMessages(!!params.onlyErrors, params.browserIds);
|
|
2316
|
+
return { content: formatResult(r), isError: false };
|
|
2317
|
+
}
|
|
2318
|
+
case "cdp_network_requests": {
|
|
2319
|
+
const exec = await getExecutor();
|
|
2320
|
+
const r = await exec.parallelNetworkRequests(params.browserIds);
|
|
2321
|
+
return { content: formatResult(r), isError: false };
|
|
2322
|
+
}
|
|
2323
|
+
case "cdp_click_normalized_box": {
|
|
2324
|
+
const exec = await getExecutor();
|
|
2325
|
+
const r = await exec.parallelClickNormalizedBox(params.box, params.browserIds);
|
|
2326
|
+
return { content: formatResult(r), isError: false };
|
|
2327
|
+
}
|
|
2328
|
+
case "cdp_setup_downloads": {
|
|
2329
|
+
const exec = await getExecutor();
|
|
2330
|
+
const r = await exec.setupDownloads(params.browserIds);
|
|
2331
|
+
return { content: formatResult(r), isError: false };
|
|
2332
|
+
}
|
|
2333
|
+
case "cdp_list_downloads": {
|
|
2334
|
+
const exec = await getExecutor();
|
|
2335
|
+
const files = await exec.listProjectFiles();
|
|
2336
|
+
return { content: JSON.stringify(files), isError: false };
|
|
2337
|
+
}
|
|
2338
|
+
case "cdp_upload_file": {
|
|
2339
|
+
const exec = await getExecutor();
|
|
2340
|
+
const r = await exec.parallelUploadFile(getSelectorTarget(params), params.fileName, params.browserIds);
|
|
2341
|
+
return { content: formatResult(r), isError: false };
|
|
2342
|
+
}
|
|
2343
|
+
case "cdp_drag_drop_file": {
|
|
2344
|
+
const exec = await getExecutor();
|
|
2345
|
+
const r = await exec.parallelDragDropFile(getSelectorTarget(params), params.fileName, params.browserIds);
|
|
2346
|
+
return { content: formatResult(r), isError: false };
|
|
2347
|
+
}
|
|
2348
|
+
case "cdp_vision_capture": {
|
|
2349
|
+
const exec = await getExecutor();
|
|
2350
|
+
await exec.ensureBrowsersLoaded();
|
|
2351
|
+
const statuses = await exec.listBrowsers();
|
|
2352
|
+
const statusMap = new Map(statuses.map((browser) => [browser.id, browser]));
|
|
2353
|
+
const screenshotResult = await exec.parallelScreenshot(params.browserIds, {
|
|
2354
|
+
format: "jpeg",
|
|
2355
|
+
quality: 70
|
|
2356
|
+
});
|
|
2357
|
+
const captures = (screenshotResult.results || []).filter((item) => item.success && item.result !== void 0).map((item) => ({
|
|
2358
|
+
browserId: item.browserId,
|
|
2359
|
+
url: statusMap.get(item.browserId)?.url || "",
|
|
2360
|
+
screenshot: Buffer.isBuffer(item.result) ? item.result.toString("base64") : Buffer.from(item.result || []).toString("base64")
|
|
2361
|
+
}));
|
|
2362
|
+
const errors = (screenshotResult.results || []).filter((item) => !item.success).map((item) => ({
|
|
2363
|
+
browserId: item.browserId,
|
|
2364
|
+
error: item.error || "unknown"
|
|
2365
|
+
}));
|
|
2366
|
+
return { content: JSON.stringify({ captures, errors }), isError: captures.length === 0 };
|
|
2367
|
+
}
|
|
2368
|
+
case "cdp_list_browsers": {
|
|
2369
|
+
const exec = await getExecutor();
|
|
2370
|
+
await exec.ensureBrowsersLoaded();
|
|
2371
|
+
const browsers = await exec.listBrowsers();
|
|
2372
|
+
return { content: browsers.length === 0 ? "No browsers connected" : browsers.map((b) => `${b.id} ${b.connected ? "OK" : "DISCONNECTED"} ${b.url || ""}`).join("\n"), isError: false };
|
|
2373
|
+
}
|
|
2374
|
+
case "cdp_connect": {
|
|
2375
|
+
const exec = await getExecutor();
|
|
2376
|
+
await exec.connect({ id: params.id, cdpEndpoint: params.cdpEndpoint });
|
|
2377
|
+
return { content: `Connected: ${params.id}`, isError: false };
|
|
2378
|
+
}
|
|
2379
|
+
case "cdp_disconnect": {
|
|
2380
|
+
const exec = await getExecutor();
|
|
2381
|
+
await exec.disconnect(params.id);
|
|
2382
|
+
return { content: `Disconnected: ${params.id}`, isError: false };
|
|
2383
|
+
}
|
|
2384
|
+
// Press and hold (for captcha solving)
|
|
2385
|
+
case "cdp_press_hold": {
|
|
2386
|
+
const exec = await getExecutor();
|
|
2387
|
+
const browsers = params.browserIds ? void 0 : await exec.listBrowsers();
|
|
2388
|
+
const targetId = params.browserIds?.[0] || browsers?.[0]?.id;
|
|
2389
|
+
if (!targetId) return { content: "No browser connected", isError: true };
|
|
2390
|
+
const page = exec.getPage(targetId);
|
|
2391
|
+
if (!page) return { content: "Browser page not found", isError: true };
|
|
2392
|
+
const x = params.x;
|
|
2393
|
+
const y = params.y;
|
|
2394
|
+
const duration = params.duration;
|
|
2395
|
+
await page.mouse.move(x, y);
|
|
2396
|
+
await page.mouse.down();
|
|
2397
|
+
const start = Date.now();
|
|
2398
|
+
while (Date.now() - start < duration) {
|
|
2399
|
+
await page.waitForTimeout(80 + Math.random() * 170);
|
|
2400
|
+
await page.mouse.move(x + (Math.random() - 0.5) * 6, y + (Math.random() - 0.5) * 6);
|
|
2401
|
+
}
|
|
2402
|
+
await page.mouse.up();
|
|
2403
|
+
return { content: JSON.stringify({ held: true, duration }), isError: false };
|
|
2404
|
+
}
|
|
2405
|
+
// Antidetect HTTP proxy — executed locally
|
|
2406
|
+
case "http_request": {
|
|
2407
|
+
const resp = await fetch(params.url, {
|
|
2408
|
+
method: params.method || "GET",
|
|
2409
|
+
headers: params.headers || { "Content-Type": "application/json" },
|
|
2410
|
+
body: params.body ? JSON.stringify(params.body) : void 0,
|
|
2411
|
+
signal: AbortSignal.timeout(3e4)
|
|
2412
|
+
});
|
|
2413
|
+
const data = await resp.text();
|
|
2414
|
+
try {
|
|
2415
|
+
return { content: JSON.stringify(JSON.parse(data)), isError: !resp.ok };
|
|
2416
|
+
} catch {
|
|
2417
|
+
return { content: data, isError: !resp.ok };
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
default:
|
|
2421
|
+
return { content: `Unknown command: ${command}`, isError: true };
|
|
2422
|
+
}
|
|
2423
|
+
} catch (e) {
|
|
2424
|
+
return { content: `Execution error: ${e.message}`, isError: true };
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
function formatResult(r) {
|
|
2428
|
+
const lines = [];
|
|
2429
|
+
for (const item of r.results || []) {
|
|
2430
|
+
if (item.success && item.result !== void 0) {
|
|
2431
|
+
const val = typeof item.result === "string" ? item.result : JSON.stringify(item.result);
|
|
2432
|
+
lines.push(`[${item.browserId}] ${val.slice(0, 4e3)}`);
|
|
2433
|
+
} else if (!item.success) {
|
|
2434
|
+
lines.push(`[${item.browserId}] ERROR: ${item.error || "unknown"}`);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
return lines.join("\n") || "OK";
|
|
2438
|
+
}
|
|
2439
|
+
function textResult(text, isError = false) {
|
|
2440
|
+
return { content: [{ type: "text", text }], isError };
|
|
2441
|
+
}
|
|
2442
|
+
async function toolHandler(tool, args2) {
|
|
2443
|
+
const result = await callServer(tool, args2);
|
|
2444
|
+
return textResult(result.content, result.isError);
|
|
2445
|
+
}
|
|
2446
|
+
function registerLinkenTools(server) {
|
|
2447
|
+
server.tool("linken_get_instances", "Get list of Linken Sphere sessions", {
|
|
2448
|
+
status: z.enum(["stopped", "running", "imported", "warmup", "automationRunning"]).optional(),
|
|
2449
|
+
proxy_info: z.boolean().optional()
|
|
2450
|
+
}, (args2) => toolHandler("linken_get_instances", args2));
|
|
2451
|
+
server.tool("linken_create_quick_sessions", "Create Linken Sphere sessions", {
|
|
2452
|
+
count: z.number().optional()
|
|
2453
|
+
}, (args2) => toolHandler("linken_create_quick_sessions", args2));
|
|
2454
|
+
server.tool("linken_start_instances", "Start Linken browser sessions", {
|
|
2455
|
+
uuids: z.array(z.string()),
|
|
2456
|
+
headless: z.boolean().optional(),
|
|
2457
|
+
debug_port: z.number().optional(),
|
|
2458
|
+
_scope: z.string().optional()
|
|
2459
|
+
}, (args2) => toolHandler("linken_start_instances", args2));
|
|
2460
|
+
server.tool("linken_stop_instances", "Stop Linken sessions", {
|
|
2461
|
+
uuids: z.array(z.string())
|
|
2462
|
+
}, (args2) => toolHandler("linken_stop_instances", args2));
|
|
2463
|
+
server.tool("linken_session_info", "Get session details", {
|
|
2464
|
+
uuid: z.string()
|
|
2465
|
+
}, (args2) => toolHandler("linken_session_info", args2));
|
|
2466
|
+
server.tool("linken_force_stop", "Force stop Linken session", {
|
|
2467
|
+
uuid: z.string()
|
|
2468
|
+
}, (args2) => toolHandler("linken_force_stop", args2));
|
|
2469
|
+
server.tool("linken_delete_sessions", "Delete Linken sessions", {
|
|
2470
|
+
uuid: z.string().optional(),
|
|
2471
|
+
uuids: z.array(z.string()).optional()
|
|
2472
|
+
}, (args2) => toolHandler("linken_delete_sessions", args2));
|
|
2473
|
+
server.tool("linken_unlock_stopped_sessions", "Unlock stopped Linken sessions", {}, () => toolHandler("linken_unlock_stopped_sessions", {}));
|
|
2474
|
+
server.tool("linken_set_session_name", "Rename Linken session", {
|
|
2475
|
+
uuid: z.string(),
|
|
2476
|
+
name: z.string()
|
|
2477
|
+
}, (args2) => toolHandler("linken_set_session_name", args2));
|
|
2478
|
+
server.tool("linken_set_connection", "Set proxy for Linken session", {
|
|
2479
|
+
uuid: z.string(),
|
|
2480
|
+
type: z.string(),
|
|
2481
|
+
ip: z.string().optional(),
|
|
2482
|
+
port: z.number().optional(),
|
|
2483
|
+
login: z.string().optional(),
|
|
2484
|
+
password: z.string().optional()
|
|
2485
|
+
}, (args2) => toolHandler("linken_set_connection", args2));
|
|
2486
|
+
server.tool("linken_check_connection", "Check proxy for Linken session", {
|
|
2487
|
+
uuid: z.string()
|
|
2488
|
+
}, (args2) => toolHandler("linken_check_connection", args2));
|
|
2489
|
+
server.tool("linken_set_geo", "Set Linken geolocation", {
|
|
2490
|
+
uuid: z.string(),
|
|
2491
|
+
timezone: z.string().optional(),
|
|
2492
|
+
language: z.string().optional(),
|
|
2493
|
+
latitude: z.number().optional(),
|
|
2494
|
+
longitude: z.number().optional()
|
|
2495
|
+
}, (args2) => toolHandler("linken_set_geo", args2));
|
|
2496
|
+
server.tool("linken_set_useragent", "Set Linken user agent", {
|
|
2497
|
+
uuid: z.string(),
|
|
2498
|
+
useragent: z.string()
|
|
2499
|
+
}, (args2) => toolHandler("linken_set_useragent", args2));
|
|
2500
|
+
server.tool("linken_import_cookies", "Import cookies into Linken session", {
|
|
2501
|
+
uuid: z.string(),
|
|
2502
|
+
file_path: z.string().optional(),
|
|
2503
|
+
json: z.string().optional()
|
|
2504
|
+
}, (args2) => toolHandler("linken_import_cookies", args2));
|
|
2505
|
+
server.tool("linken_export_cookies", "Export cookies from Linken sessions", {
|
|
2506
|
+
uuids: z.array(z.string()),
|
|
2507
|
+
folder_path: z.string()
|
|
2508
|
+
}, (args2) => toolHandler("linken_export_cookies", args2));
|
|
2509
|
+
server.tool("linken_start_warmup", "Start Linken session warmup", {
|
|
2510
|
+
uuid: z.string(),
|
|
2511
|
+
url_count: z.number().optional(),
|
|
2512
|
+
time_per_url: z.number().optional(),
|
|
2513
|
+
view_depth: z.number().optional(),
|
|
2514
|
+
urls: z.array(z.string()).optional()
|
|
2515
|
+
}, (args2) => toolHandler("linken_start_warmup", args2));
|
|
2516
|
+
server.tool("linken_get_providers", "List Linken providers", {}, () => toolHandler("linken_get_providers", {}));
|
|
2517
|
+
server.tool("linken_set_active_provider", "Set active Linken provider", {
|
|
2518
|
+
uuid: z.string()
|
|
2519
|
+
}, (args2) => toolHandler("linken_set_active_provider", args2));
|
|
2520
|
+
server.tool("linken_delete_provider", "Delete Linken provider", {
|
|
2521
|
+
uuid: z.string()
|
|
2522
|
+
}, (args2) => toolHandler("linken_delete_provider", args2));
|
|
2523
|
+
server.tool("linken_get_desktops", "List Linken desktops", {}, () => toolHandler("linken_get_desktops", {}));
|
|
2524
|
+
server.tool("linken_create_desktop", "Create Linken desktop", {
|
|
2525
|
+
name: z.string(),
|
|
2526
|
+
team_uuid: z.string().optional()
|
|
2527
|
+
}, (args2) => toolHandler("linken_create_desktop", args2));
|
|
2528
|
+
server.tool("linken_set_active_desktop", "Set active Linken desktop", {
|
|
2529
|
+
uuid: z.string()
|
|
2530
|
+
}, (args2) => toolHandler("linken_set_active_desktop", args2));
|
|
2531
|
+
server.tool("linken_delete_desktop", "Delete Linken desktop", {
|
|
2532
|
+
uuid: z.string()
|
|
2533
|
+
}, (args2) => toolHandler("linken_delete_desktop", args2));
|
|
2534
|
+
server.tool("linken_optimize_storage", "Optimize Linken storage", {}, () => toolHandler("linken_optimize_storage", {}));
|
|
2535
|
+
}
|
|
2536
|
+
var browserIdsArg = z.array(z.string()).optional();
|
|
2537
|
+
var browserScopeArg = z.string().optional();
|
|
2538
|
+
var browserRefArg = z.union([z.string(), z.number()]).optional();
|
|
2539
|
+
var browserTargetArgs = {
|
|
2540
|
+
element: z.string().optional(),
|
|
2541
|
+
ref: browserRefArg,
|
|
2542
|
+
selector: z.string().optional(),
|
|
2543
|
+
browserIds: browserIdsArg,
|
|
2544
|
+
_scope: browserScopeArg
|
|
2545
|
+
};
|
|
2546
|
+
function createServer() {
|
|
2547
|
+
const server = new McpServer({ name: "ornold-browser", version: CLIENT_VERSION });
|
|
2548
|
+
server.tool("browser_list", "List connected browsers", {}, () => toolHandler("browser_list", {}));
|
|
2549
|
+
server.tool("browser_status", "Check browser sync and responsiveness", {
|
|
2550
|
+
checkContent: z.boolean().optional(),
|
|
2551
|
+
_scope: browserScopeArg
|
|
2552
|
+
}, (args2) => toolHandler("browser_status", args2));
|
|
2553
|
+
server.tool("browser_parallel_tabs", "Manage browser tabs in parallel", {
|
|
2554
|
+
action: z.enum(["list", "new", "close", "select"]),
|
|
2555
|
+
index: z.number().optional(),
|
|
2556
|
+
url: z.string().optional(),
|
|
2557
|
+
browserIds: browserIdsArg,
|
|
2558
|
+
_scope: browserScopeArg
|
|
2559
|
+
}, (args2) => toolHandler("browser_parallel_tabs", args2));
|
|
2560
|
+
server.tool("browser_parallel_snapshot", "Get page snapshot with [ref=N] markers", {
|
|
2561
|
+
compact: z.boolean().optional(),
|
|
2562
|
+
browserIds: browserIdsArg,
|
|
2563
|
+
_scope: browserScopeArg
|
|
2564
|
+
}, (args2) => toolHandler("browser_parallel_snapshot", args2));
|
|
2565
|
+
server.tool("browser_parallel_navigate", "Navigate to URL", {
|
|
2566
|
+
url: z.string(),
|
|
2567
|
+
browserIds: browserIdsArg,
|
|
2568
|
+
_scope: browserScopeArg
|
|
2569
|
+
}, (args2) => toolHandler("browser_parallel_navigate", args2));
|
|
2570
|
+
server.tool("browser_parallel_navigate_multi", "Navigate each browser to a different URL", {
|
|
2571
|
+
targets: z.array(z.object({
|
|
2572
|
+
browserId: z.string(),
|
|
2573
|
+
url: z.string()
|
|
2574
|
+
})),
|
|
2575
|
+
_scope: browserScopeArg
|
|
2576
|
+
}, (args2) => toolHandler("browser_parallel_navigate_multi", args2));
|
|
2577
|
+
server.tool("browser_parallel_click", "Click element", {
|
|
2578
|
+
...browserTargetArgs
|
|
2579
|
+
}, (args2) => toolHandler("browser_parallel_click", args2));
|
|
2580
|
+
server.tool("browser_parallel_type", "Type text into element", {
|
|
2581
|
+
...browserTargetArgs,
|
|
2582
|
+
text: z.string()
|
|
2583
|
+
}, (args2) => toolHandler("browser_parallel_type", args2));
|
|
2584
|
+
server.tool("browser_parallel_fill", "Fill input (clear + type)", {
|
|
2585
|
+
...browserTargetArgs,
|
|
2586
|
+
text: z.string().optional(),
|
|
2587
|
+
texts: z.record(z.string()).optional()
|
|
2588
|
+
}, (args2) => toolHandler("browser_parallel_fill", args2));
|
|
2589
|
+
server.tool("browser_parallel_fill_multi", "Fill input with per-browser values", {
|
|
2590
|
+
...browserTargetArgs,
|
|
2591
|
+
texts: z.record(z.string())
|
|
2592
|
+
}, (args2) => toolHandler("browser_parallel_fill_multi", args2));
|
|
2593
|
+
server.tool("browser_parallel_fill_form", "Fill multiple form fields sequentially", {
|
|
2594
|
+
fields: z.array(z.object({
|
|
2595
|
+
element: z.string().optional(),
|
|
2596
|
+
ref: browserRefArg,
|
|
2597
|
+
selector: z.string().optional(),
|
|
2598
|
+
value: z.string(),
|
|
2599
|
+
type: z.enum(["textbox", "checkbox", "radio", "combobox"]).optional()
|
|
2600
|
+
})),
|
|
2601
|
+
browserIds: browserIdsArg,
|
|
2602
|
+
_scope: browserScopeArg
|
|
2603
|
+
}, (args2) => toolHandler("browser_parallel_fill_form", args2));
|
|
2604
|
+
server.tool("browser_parallel_drag", "Drag from one element to another", {
|
|
2605
|
+
startElement: z.string().optional(),
|
|
2606
|
+
startRef: browserRefArg,
|
|
2607
|
+
startSelector: z.string().optional(),
|
|
2608
|
+
endElement: z.string().optional(),
|
|
2609
|
+
endRef: browserRefArg,
|
|
2610
|
+
endSelector: z.string().optional(),
|
|
2611
|
+
browserIds: browserIdsArg,
|
|
2612
|
+
_scope: browserScopeArg
|
|
2613
|
+
}, (args2) => toolHandler("browser_parallel_drag", args2));
|
|
2614
|
+
server.tool("browser_parallel_press_key", "Press keyboard key", {
|
|
2615
|
+
key: z.string(),
|
|
2616
|
+
browserIds: browserIdsArg,
|
|
2617
|
+
_scope: browserScopeArg
|
|
2618
|
+
}, (args2) => toolHandler("browser_parallel_press_key", args2));
|
|
2619
|
+
server.tool("browser_parallel_select_option", "Select dropdown option", {
|
|
2620
|
+
...browserTargetArgs,
|
|
2621
|
+
values: z.array(z.string())
|
|
2622
|
+
}, (args2) => toolHandler("browser_parallel_select_option", args2));
|
|
2623
|
+
server.tool("browser_parallel_wait_for", "Wait for condition", {
|
|
2624
|
+
time: z.number().optional(),
|
|
2625
|
+
text: z.string().optional(),
|
|
2626
|
+
textGone: z.string().optional(),
|
|
2627
|
+
domStableMs: z.number().optional(),
|
|
2628
|
+
timeoutMs: z.number().optional(),
|
|
2629
|
+
browserIds: browserIdsArg,
|
|
2630
|
+
_scope: browserScopeArg
|
|
2631
|
+
}, (args2) => toolHandler("browser_parallel_wait_for", args2));
|
|
2632
|
+
server.tool("browser_parallel_screenshot", "Take screenshot", {
|
|
2633
|
+
fullPage: z.boolean().optional(),
|
|
2634
|
+
browserIds: browserIdsArg,
|
|
2635
|
+
_scope: browserScopeArg
|
|
2636
|
+
}, (args2) => toolHandler("browser_parallel_screenshot", args2));
|
|
2637
|
+
server.tool("browser_parallel_evaluate", "Run JavaScript in page", {
|
|
2638
|
+
script: z.string(),
|
|
2639
|
+
browserIds: browserIdsArg,
|
|
2640
|
+
_scope: browserScopeArg
|
|
2641
|
+
}, (args2) => toolHandler("browser_parallel_evaluate", args2));
|
|
2642
|
+
server.tool("browser_parallel_run_code", "Run JavaScript code in page context", {
|
|
2643
|
+
code: z.string(),
|
|
2644
|
+
browserIds: browserIdsArg,
|
|
2645
|
+
_scope: browserScopeArg
|
|
2646
|
+
}, (args2) => toolHandler("browser_parallel_run_code", args2));
|
|
2647
|
+
server.tool("browser_parallel_run_code_with_vars", "Run templated JavaScript code with per-browser variables", {
|
|
2648
|
+
codeTemplate: z.string(),
|
|
2649
|
+
variables: z.record(z.record(z.string())),
|
|
2650
|
+
_scope: browserScopeArg
|
|
2651
|
+
}, (args2) => toolHandler("browser_parallel_run_code_with_vars", args2));
|
|
2652
|
+
server.tool("browser_parallel_hover", "Hover over element", {
|
|
2653
|
+
...browserTargetArgs
|
|
2654
|
+
}, (args2) => toolHandler("browser_parallel_hover", args2));
|
|
2655
|
+
server.tool("browser_parallel_go_back", "Go back", {
|
|
2656
|
+
browserIds: browserIdsArg,
|
|
2657
|
+
_scope: browserScopeArg
|
|
2658
|
+
}, (args2) => toolHandler("browser_parallel_go_back", args2));
|
|
2659
|
+
server.tool("browser_parallel_go_forward", "Go forward", {
|
|
2660
|
+
browserIds: browserIdsArg,
|
|
2661
|
+
_scope: browserScopeArg
|
|
2662
|
+
}, (args2) => toolHandler("browser_parallel_go_forward", args2));
|
|
2663
|
+
server.tool("browser_parallel_reload", "Reload page", {
|
|
2664
|
+
browserIds: browserIdsArg,
|
|
2665
|
+
_scope: browserScopeArg
|
|
2666
|
+
}, (args2) => toolHandler("browser_parallel_reload", args2));
|
|
2667
|
+
server.tool("browser_parallel_handle_dialog", "Handle JavaScript dialogs", {
|
|
2668
|
+
accept: z.boolean(),
|
|
2669
|
+
promptText: z.string().optional(),
|
|
2670
|
+
browserIds: browserIdsArg,
|
|
2671
|
+
_scope: browserScopeArg
|
|
2672
|
+
}, (args2) => toolHandler("browser_parallel_handle_dialog", args2));
|
|
2673
|
+
server.tool("browser_parallel_console_messages", "Get console messages", {
|
|
2674
|
+
onlyErrors: z.boolean().optional(),
|
|
2675
|
+
browserIds: browserIdsArg,
|
|
2676
|
+
_scope: browserScopeArg
|
|
2677
|
+
}, (args2) => toolHandler("browser_parallel_console_messages", args2));
|
|
2678
|
+
server.tool("browser_parallel_network_requests", "Get network requests", {
|
|
2679
|
+
browserIds: browserIdsArg,
|
|
2680
|
+
_scope: browserScopeArg
|
|
2681
|
+
}, (args2) => toolHandler("browser_parallel_network_requests", args2));
|
|
2682
|
+
server.tool("browser_parallel_vision_analyze_grouped", "Analyze grouped browser screenshots with OmniParser via Ornold server", {
|
|
2683
|
+
similarityThreshold: z.number().optional(),
|
|
2684
|
+
browserIds: browserIdsArg,
|
|
2685
|
+
_scope: browserScopeArg
|
|
2686
|
+
}, (args2) => toolHandler("browser_parallel_vision_analyze_grouped", args2));
|
|
2687
|
+
server.tool("browser_parallel_click_normalized_box", "Click normalized viewport box center", {
|
|
2688
|
+
box: z.tuple([z.number(), z.number(), z.number(), z.number()]),
|
|
2689
|
+
browserIds: browserIdsArg,
|
|
2690
|
+
_scope: browserScopeArg
|
|
2691
|
+
}, (args2) => toolHandler("browser_parallel_click_normalized_box", args2));
|
|
2692
|
+
server.tool("browser_setup_downloads", "Enable browser downloads to project files directory", {
|
|
2693
|
+
browserIds: browserIdsArg,
|
|
2694
|
+
_scope: browserScopeArg
|
|
2695
|
+
}, (args2) => toolHandler("browser_setup_downloads", args2));
|
|
2696
|
+
server.tool("browser_list_downloads", "List files in project files directory", {}, () => toolHandler("browser_list_downloads", {}));
|
|
2697
|
+
server.tool("browser_parallel_upload_file", "Upload a file to an input element", {
|
|
2698
|
+
...browserTargetArgs,
|
|
2699
|
+
fileName: z.string()
|
|
2700
|
+
}, (args2) => toolHandler("browser_parallel_upload_file", args2));
|
|
2701
|
+
server.tool("browser_parallel_drag_drop_file", "Drag and drop a file onto an element", {
|
|
2702
|
+
...browserTargetArgs,
|
|
2703
|
+
fileName: z.string()
|
|
2704
|
+
}, (args2) => toolHandler("browser_parallel_drag_drop_file", args2));
|
|
2705
|
+
server.tool("browser_search_macros", "Search saved browser macros", {
|
|
2706
|
+
name: z.string(),
|
|
2707
|
+
_scope: browserScopeArg
|
|
2708
|
+
}, (args2) => toolHandler("browser_search_macros", args2));
|
|
2709
|
+
server.tool("browser_run_recorded_flow", "Run saved browser macro by ID", {
|
|
2710
|
+
macroId: z.string(),
|
|
2711
|
+
browserIds: browserIdsArg,
|
|
2712
|
+
_scope: browserScopeArg
|
|
2713
|
+
}, (args2) => toolHandler("browser_run_recorded_flow", args2));
|
|
2714
|
+
if (LINKEN_PORT !== void 0) {
|
|
2715
|
+
registerLinkenTools(server);
|
|
2716
|
+
}
|
|
2717
|
+
if (DOLPHIN_PORT !== void 0) {
|
|
2718
|
+
server.tool("dolphin_get_profiles", "List Dolphin profiles", {}, () => toolHandler("dolphin_get_profiles", {}));
|
|
2719
|
+
server.tool("dolphin_start_profile", "Start Dolphin profile", {
|
|
2720
|
+
profileId: z.string()
|
|
2721
|
+
}, (args2) => toolHandler("dolphin_start_profile", args2));
|
|
2722
|
+
server.tool("dolphin_stop_profile", "Stop Dolphin profile", {
|
|
2723
|
+
profileId: z.string()
|
|
2724
|
+
}, (args2) => toolHandler("dolphin_stop_profile", args2));
|
|
2725
|
+
}
|
|
2726
|
+
server.tool("browser_detect_captcha", "Detect captcha on page", {
|
|
2727
|
+
browserIds: browserIdsArg,
|
|
2728
|
+
_scope: browserScopeArg
|
|
2729
|
+
}, (args2) => toolHandler("browser_detect_captcha", args2));
|
|
2730
|
+
server.tool("browser_solve_press_hold", "Solve press-and-hold captcha", {
|
|
2731
|
+
browserIds: browserIdsArg,
|
|
2732
|
+
maxAttempts: z.number().optional(),
|
|
2733
|
+
holdMs: z.number().optional(),
|
|
2734
|
+
_scope: browserScopeArg
|
|
2735
|
+
}, (args2) => toolHandler("browser_solve_press_hold", args2));
|
|
2736
|
+
server.tool("browser_detect_press_hold", "Detect press-and-hold captcha", {
|
|
2737
|
+
browserIds: browserIdsArg,
|
|
2738
|
+
_scope: browserScopeArg
|
|
2739
|
+
}, (args2) => toolHandler("browser_detect_press_hold", args2));
|
|
2740
|
+
server.tool("browser_solve_captcha", "Solve captcha (reCAPTCHA/hCaptcha)", {
|
|
2741
|
+
browserIds: browserIdsArg,
|
|
2742
|
+
timeout: z.number().optional(),
|
|
2743
|
+
autoSubmit: z.boolean().optional(),
|
|
2744
|
+
_scope: browserScopeArg
|
|
2745
|
+
}, (args2) => toolHandler("browser_solve_captcha", args2));
|
|
2746
|
+
server.tool("browser_captcha_balance", "Check 2captcha balance", {}, () => toolHandler("browser_captcha_balance", {}));
|
|
2747
|
+
server.tool("captcha_detect", "Detect captcha on page", {
|
|
2748
|
+
browserIds: browserIdsArg
|
|
2749
|
+
}, (args2) => toolHandler("captcha_detect", args2));
|
|
2750
|
+
server.tool("captcha_solve_press_hold", "Solve press-and-hold captcha", {
|
|
2751
|
+
browserIds: browserIdsArg
|
|
2752
|
+
}, (args2) => toolHandler("captcha_solve_press_hold", args2));
|
|
2753
|
+
server.tool("captcha_solve", "Solve captcha (reCAPTCHA/hCaptcha)", {
|
|
2754
|
+
browserIds: browserIdsArg
|
|
2755
|
+
}, (args2) => toolHandler("captcha_solve", args2));
|
|
2756
|
+
return server;
|
|
2757
|
+
}
|
|
2758
|
+
async function main() {
|
|
2759
|
+
await connectServer();
|
|
2760
|
+
if (ENABLED_VENDORS.length === 0) {
|
|
2761
|
+
console.error("[ornold] No antidetect vendor ports configured. Only browser_* and captcha_* tools will be available.");
|
|
2762
|
+
}
|
|
2763
|
+
for (const [name, port] of [["linken", LINKEN_PORT], ["dolphin", DOLPHIN_PORT]]) {
|
|
2764
|
+
if (port === void 0) continue;
|
|
2765
|
+
try {
|
|
2766
|
+
const resp = await fetch(`http://127.0.0.1:${port}/sessions`, { signal: AbortSignal.timeout(2e3) });
|
|
2767
|
+
if (resp.ok) console.error(`[ornold] ${name}: detected on port ${port}`);
|
|
2768
|
+
} catch {
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
ws?.send(JSON.stringify({
|
|
2772
|
+
type: "bridge_hello",
|
|
2773
|
+
hostname: (await import("os")).hostname(),
|
|
2774
|
+
config: {
|
|
2775
|
+
enabledVendors: ENABLED_VENDORS,
|
|
2776
|
+
...LINKEN_PORT !== void 0 ? { linkenPort: LINKEN_PORT } : {},
|
|
2777
|
+
...DOLPHIN_PORT !== void 0 ? { dolphinPort: DOLPHIN_PORT } : {}
|
|
2778
|
+
}
|
|
2779
|
+
}));
|
|
2780
|
+
const server = createServer();
|
|
2781
|
+
const transport = new StdioServerTransport();
|
|
2782
|
+
await server.connect(transport);
|
|
2783
|
+
console.error("[ornold] MCP server ready (stdio)");
|
|
2784
|
+
}
|
|
2785
|
+
main().catch((e) => {
|
|
2786
|
+
console.error(`[ornold] Fatal: ${e.message}`);
|
|
2787
|
+
process.exit(1);
|
|
2788
|
+
});
|