@midscene/computer 1.8.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/cli.mjs +898 -274
- package/dist/es/index.mjs +450 -395
- package/dist/es/mcp-server.mjs +898 -274
- package/dist/lib/cli.js +879 -256
- package/dist/lib/index.js +452 -394
- package/dist/lib/mcp-server.js +880 -257
- package/dist/types/index.d.ts +34 -5
- package/dist/types/mcp-server.d.ts +25 -5
- package/package.json +3 -3
package/dist/es/cli.mjs
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { createReportCliCommands,
|
|
1
|
+
import { createReportCliCommands, z } from "@midscene/core";
|
|
2
2
|
import { reportCLIError, runToolsCLI } from "@midscene/shared/cli";
|
|
3
3
|
import { getDebug } from "@midscene/shared/logger";
|
|
4
4
|
import { BaseMidsceneTools } from "@midscene/shared/mcp/base-tools";
|
|
5
5
|
import { Agent } from "@midscene/core/agent";
|
|
6
6
|
import node_assert from "node:assert";
|
|
7
|
-
import { execFileSync, execSync, spawn
|
|
8
|
-
import { existsSync
|
|
7
|
+
import { execFileSync, execSync, spawn, spawnSync } from "node:child_process";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
9
|
import { createRequire } from "node:module";
|
|
10
|
-
import { dirname
|
|
11
|
-
import { fileURLToPath
|
|
12
|
-
import {
|
|
13
|
-
import { sleep
|
|
10
|
+
import { dirname, resolve as external_node_path_resolve } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { defineAction, defineActionsFromInputPrimitives } from "@midscene/core/device";
|
|
13
|
+
import { sleep } from "@midscene/core/utils";
|
|
14
14
|
import { createImgBase64ByFormat } from "@midscene/shared/img";
|
|
15
15
|
import screenshot_desktop from "screenshot-desktop";
|
|
16
|
-
import "node:events";
|
|
17
|
-
import "node:readline";
|
|
16
|
+
import { once } from "node:events";
|
|
17
|
+
import { createInterface } from "node:readline";
|
|
18
18
|
const debugXvfb = getDebug('computer:xvfb');
|
|
19
19
|
function checkXvfbInstalled() {
|
|
20
20
|
try {
|
|
@@ -27,7 +27,7 @@ function checkXvfbInstalled() {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
function findAvailableDisplay(startFrom = 99) {
|
|
30
|
-
for(let n = startFrom; n < startFrom + 100; n++)if (!
|
|
30
|
+
for(let n = startFrom; n < startFrom + 100; n++)if (!existsSync(`/tmp/.X${n}-lock`)) return n;
|
|
31
31
|
throw new Error(`No available display number found (checked ${startFrom} to ${startFrom + 99})`);
|
|
32
32
|
}
|
|
33
33
|
function startXvfb(options) {
|
|
@@ -36,7 +36,7 @@ function startXvfb(options) {
|
|
|
36
36
|
const display = `:${displayNum}`;
|
|
37
37
|
return new Promise((resolve, reject)=>{
|
|
38
38
|
debugXvfb(`Starting Xvfb on display ${display} with resolution ${resolution}`);
|
|
39
|
-
const xvfbProcess =
|
|
39
|
+
const xvfbProcess = spawn('Xvfb', [
|
|
40
40
|
display,
|
|
41
41
|
'-screen',
|
|
42
42
|
'0',
|
|
@@ -92,15 +92,6 @@ function _define_property(obj, key, value) {
|
|
|
92
92
|
else obj[key] = value;
|
|
93
93
|
return obj;
|
|
94
94
|
}
|
|
95
|
-
const computerInputParamSchema = z.object({
|
|
96
|
-
value: z.string().describe('The text to input'),
|
|
97
|
-
mode: z["enum"]([
|
|
98
|
-
'replace',
|
|
99
|
-
'clear',
|
|
100
|
-
'append'
|
|
101
|
-
]).default('replace').optional().describe('Input mode: replace, clear, or append'),
|
|
102
|
-
locate: getMidsceneLocationSchema().describe('The input field to be filled').optional()
|
|
103
|
-
});
|
|
104
95
|
const SMOOTH_MOVE_STEPS_TAP = 8;
|
|
105
96
|
const SMOOTH_MOVE_STEPS_MOUSE_MOVE = 10;
|
|
106
97
|
const SMOOTH_MOVE_DELAY_TAP = 8;
|
|
@@ -239,13 +230,13 @@ function getPhasedScrollBinary() {
|
|
|
239
230
|
const require = createRequire(import.meta.url);
|
|
240
231
|
let pkgRoot = null;
|
|
241
232
|
try {
|
|
242
|
-
pkgRoot =
|
|
233
|
+
pkgRoot = dirname(require.resolve('@midscene/computer/package.json'));
|
|
243
234
|
} catch {
|
|
244
|
-
const hereDir =
|
|
235
|
+
const hereDir = dirname(fileURLToPath(import.meta.url));
|
|
245
236
|
for (const candidate of [
|
|
246
237
|
external_node_path_resolve(hereDir, '..'),
|
|
247
238
|
external_node_path_resolve(hereDir, '../..')
|
|
248
|
-
])if (
|
|
239
|
+
])if (existsSync(external_node_path_resolve(candidate, 'package.json'))) {
|
|
249
240
|
pkgRoot = candidate;
|
|
250
241
|
break;
|
|
251
242
|
}
|
|
@@ -256,7 +247,7 @@ function getPhasedScrollBinary() {
|
|
|
256
247
|
return null;
|
|
257
248
|
}
|
|
258
249
|
const binPath = external_node_path_resolve(pkgRoot, 'bin/darwin/phased-scroll');
|
|
259
|
-
if (!
|
|
250
|
+
if (!existsSync(binPath)) {
|
|
260
251
|
debugDevice('phased-scroll binary not found at', binPath);
|
|
261
252
|
phasedScrollBinaryPath = null;
|
|
262
253
|
return null;
|
|
@@ -299,7 +290,7 @@ async function smoothMoveMouse(targetX, targetY, steps, stepDelay) {
|
|
|
299
290
|
const stepX = Math.round(currentPos.x + (targetX - currentPos.x) * i / steps);
|
|
300
291
|
const stepY = Math.round(currentPos.y + (targetY - currentPos.y) * i / steps);
|
|
301
292
|
libnut.moveMouse(stepX, stepY);
|
|
302
|
-
await
|
|
293
|
+
await sleep(stepDelay);
|
|
303
294
|
}
|
|
304
295
|
}
|
|
305
296
|
const KEY_NAME_MAP = {
|
|
@@ -411,7 +402,7 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
|
|
|
411
402
|
}
|
|
412
403
|
async healthCheck() {
|
|
413
404
|
console.log('[HealthCheck] Starting health check...');
|
|
414
|
-
console.log("[HealthCheck] @midscene/computer v1.8.
|
|
405
|
+
console.log("[HealthCheck] @midscene/computer v1.8.1");
|
|
415
406
|
console.log('[HealthCheck] Taking screenshot...');
|
|
416
407
|
const screenshotTimeout = 15000;
|
|
417
408
|
let timeoutId;
|
|
@@ -433,7 +424,7 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
|
|
|
433
424
|
const targetY = startPos.y + offsetY;
|
|
434
425
|
console.log(`[HealthCheck] Moving mouse to (${targetX}, ${targetY})...`);
|
|
435
426
|
libnut.moveMouse(targetX, targetY);
|
|
436
|
-
await
|
|
427
|
+
await sleep(50);
|
|
437
428
|
const movedPos = libnut.getMousePos();
|
|
438
429
|
console.log(`[HealthCheck] Mouse position after move: (${movedPos.x}, ${movedPos.y})`);
|
|
439
430
|
const deltaX = Math.abs(movedPos.x - targetX);
|
|
@@ -477,21 +468,38 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
|
|
|
477
468
|
debugDevice('Taking screenshot', {
|
|
478
469
|
displayId: this.displayId
|
|
479
470
|
});
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
471
|
+
const options = {
|
|
472
|
+
format: 'png'
|
|
473
|
+
};
|
|
474
|
+
if (void 0 !== this.displayId) if ('darwin' === process.platform) {
|
|
475
|
+
const screenIndex = Number(this.displayId);
|
|
476
|
+
if (!Number.isNaN(screenIndex)) options.screen = screenIndex;
|
|
477
|
+
} else options.screen = this.displayId;
|
|
478
|
+
debugDevice('Screenshot options', options);
|
|
479
|
+
const MAX_ATTEMPTS = 3;
|
|
480
|
+
const RETRY_DELAY_MS = 300;
|
|
481
|
+
let lastRawMessage = '';
|
|
482
|
+
for(let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++)try {
|
|
489
483
|
const buffer = await screenshot_desktop(options);
|
|
484
|
+
if (attempt > 1) debugDevice(`Screenshot succeeded on attempt ${attempt}`);
|
|
490
485
|
return createImgBase64ByFormat('png', buffer.toString('base64'));
|
|
491
486
|
} catch (error) {
|
|
492
|
-
|
|
493
|
-
|
|
487
|
+
lastRawMessage = error instanceof Error ? error.message : String(error);
|
|
488
|
+
const isMacTransient = 'darwin' === process.platform && /could not create image from display/i.test(lastRawMessage);
|
|
489
|
+
const willRetry = isMacTransient && attempt < MAX_ATTEMPTS;
|
|
490
|
+
debugDevice(`Screenshot attempt ${attempt} failed: ${lastRawMessage}${willRetry ? ' — retrying' : ''}`);
|
|
491
|
+
if (!willRetry) break;
|
|
492
|
+
await sleep(RETRY_DELAY_MS);
|
|
494
493
|
}
|
|
494
|
+
if ('darwin' === process.platform && /could not create image from display/i.test(lastRawMessage)) throw new Error(`Failed to take screenshot on macOS: the host process is missing Screen Recording permission, or the target display is locked/sleeping.
|
|
495
|
+
|
|
496
|
+
Please follow these steps:
|
|
497
|
+
1. Open System Settings > Privacy & Security > Screen Recording
|
|
498
|
+
2. Enable the application running this script (e.g., Terminal, iTerm2, VS Code, WebStorm, or Midscene Studio)
|
|
499
|
+
3. Fully quit and relaunch that application after granting permission — macOS only re-reads this permission on process launch.
|
|
500
|
+
|
|
501
|
+
Original error: ${lastRawMessage}`);
|
|
502
|
+
throw new Error(`Failed to take screenshot: ${lastRawMessage}`);
|
|
495
503
|
}
|
|
496
504
|
async size() {
|
|
497
505
|
node_assert(libnut, 'libnut not initialized');
|
|
@@ -516,7 +524,7 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
|
|
|
516
524
|
const oldClipboard = await clipboardy.default.read().catch(()=>'');
|
|
517
525
|
try {
|
|
518
526
|
await clipboardy.default.write(text);
|
|
519
|
-
await
|
|
527
|
+
await sleep(50);
|
|
520
528
|
if (this.useAppleScript) sendKeyViaAppleScript('v', [
|
|
521
529
|
'command'
|
|
522
530
|
]);
|
|
@@ -526,7 +534,7 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
|
|
|
526
534
|
modifier
|
|
527
535
|
]);
|
|
528
536
|
}
|
|
529
|
-
await
|
|
537
|
+
await sleep(100);
|
|
530
538
|
} finally{
|
|
531
539
|
if (oldClipboard) await clipboardy.default.write(oldClipboard).catch(()=>{
|
|
532
540
|
debugDevice('Failed to restore clipboard content');
|
|
@@ -537,228 +545,111 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
|
|
|
537
545
|
node_assert(libnut, 'libnut not initialized');
|
|
538
546
|
await this.typeViaClipboard(text);
|
|
539
547
|
}
|
|
548
|
+
async selectAllAndDelete() {
|
|
549
|
+
node_assert(libnut, 'libnut not initialized');
|
|
550
|
+
if (this.useAppleScript) {
|
|
551
|
+
sendKeyViaAppleScript('a', [
|
|
552
|
+
'command'
|
|
553
|
+
]);
|
|
554
|
+
await sleep(50);
|
|
555
|
+
sendKeyViaAppleScript('backspace', []);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const modifier = 'darwin' === process.platform ? 'command' : 'control';
|
|
559
|
+
libnut.keyTap('a', [
|
|
560
|
+
modifier
|
|
561
|
+
]);
|
|
562
|
+
await sleep(50);
|
|
563
|
+
libnut.keyTap('backspace');
|
|
564
|
+
}
|
|
565
|
+
async pressKeyboardShortcut(keyName) {
|
|
566
|
+
node_assert(libnut, 'libnut not initialized');
|
|
567
|
+
const keys = keyName.split('+');
|
|
568
|
+
const modifiers = keys.slice(0, -1).map(normalizeKeyName);
|
|
569
|
+
const key = normalizePrimaryKey(keys[keys.length - 1]);
|
|
570
|
+
debugDevice('KeyboardPress', {
|
|
571
|
+
original: keyName,
|
|
572
|
+
key,
|
|
573
|
+
modifiers,
|
|
574
|
+
driver: this.useAppleScript ? "applescript" : 'libnut'
|
|
575
|
+
});
|
|
576
|
+
if (this.useAppleScript) sendKeyViaAppleScript(key, modifiers);
|
|
577
|
+
else if (modifiers.length > 0) libnut.keyTap(key, modifiers);
|
|
578
|
+
else libnut.keyTap(key);
|
|
579
|
+
}
|
|
580
|
+
async performScroll(param) {
|
|
581
|
+
node_assert(libnut, 'libnut not initialized');
|
|
582
|
+
if (param.locate) {
|
|
583
|
+
const element = param.locate;
|
|
584
|
+
const [x, y] = element.center;
|
|
585
|
+
libnut.moveMouse(Math.round(x), Math.round(y));
|
|
586
|
+
}
|
|
587
|
+
const scrollType = param?.scrollType;
|
|
588
|
+
const edgeSpec = scrollType && scrollType in EDGE_SCROLL_SPEC ? EDGE_SCROLL_SPEC[scrollType] : null;
|
|
589
|
+
if (edgeSpec) {
|
|
590
|
+
if (runPhasedScroll(edgeSpec.direction, EDGE_SCROLL_TOTAL_PX, EDGE_SCROLL_STEPS)) return void await sleep(SCROLL_COMPLETE_DELAY);
|
|
591
|
+
if (this.useAppleScript) {
|
|
592
|
+
sendKeyViaAppleScript(edgeSpec.key);
|
|
593
|
+
await sleep(SCROLL_COMPLETE_DELAY);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const [dx, dy] = edgeSpec.libnut;
|
|
597
|
+
for(let i = 0; i < SCROLL_REPEAT_COUNT; i++){
|
|
598
|
+
libnut.scrollMouse(dx, dy);
|
|
599
|
+
await sleep(SCROLL_STEP_DELAY);
|
|
600
|
+
}
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if ('singleAction' === scrollType || !scrollType) {
|
|
604
|
+
const distance = param?.distance || 500;
|
|
605
|
+
const direction = param?.direction || 'down';
|
|
606
|
+
const isKnownDirection = 'up' === direction || 'down' === direction || 'left' === direction || 'right' === direction;
|
|
607
|
+
if (isKnownDirection) {
|
|
608
|
+
const steps = Math.max(PHASED_MIN_STEPS, Math.round(distance / PHASED_PIXELS_PER_STEP));
|
|
609
|
+
if (runPhasedScroll(direction, distance, steps)) return void await sleep(SCROLL_COMPLETE_DELAY);
|
|
610
|
+
}
|
|
611
|
+
if (this.useAppleScript && ('up' === direction || 'down' === direction)) {
|
|
612
|
+
const pages = Math.max(1, Math.round(distance / APPROX_VIEWPORT_HEIGHT_PX));
|
|
613
|
+
const key = 'up' === direction ? 'pageup' : 'pagedown';
|
|
614
|
+
for(let i = 0; i < pages; i++){
|
|
615
|
+
sendKeyViaAppleScript(key);
|
|
616
|
+
await sleep(SCROLL_STEP_DELAY);
|
|
617
|
+
}
|
|
618
|
+
await sleep(SCROLL_COMPLETE_DELAY);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const ticks = Math.ceil(distance / 100);
|
|
622
|
+
const directionMap = {
|
|
623
|
+
up: [
|
|
624
|
+
0,
|
|
625
|
+
ticks
|
|
626
|
+
],
|
|
627
|
+
down: [
|
|
628
|
+
0,
|
|
629
|
+
-ticks
|
|
630
|
+
],
|
|
631
|
+
left: [
|
|
632
|
+
-ticks,
|
|
633
|
+
0
|
|
634
|
+
],
|
|
635
|
+
right: [
|
|
636
|
+
ticks,
|
|
637
|
+
0
|
|
638
|
+
]
|
|
639
|
+
};
|
|
640
|
+
const [dx, dy] = directionMap[direction] || [
|
|
641
|
+
0,
|
|
642
|
+
-ticks
|
|
643
|
+
];
|
|
644
|
+
libnut.scrollMouse(dx, dy);
|
|
645
|
+
await sleep(SCROLL_COMPLETE_DELAY);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
throw new Error(`Unknown scroll type: ${scrollType}, param: ${JSON.stringify(param)}`);
|
|
649
|
+
}
|
|
540
650
|
actionSpace() {
|
|
541
651
|
const defaultActions = [
|
|
542
|
-
|
|
543
|
-
node_assert(libnut, 'libnut not initialized');
|
|
544
|
-
const element = param.locate;
|
|
545
|
-
node_assert(element, 'Element not found, cannot tap');
|
|
546
|
-
const [x, y] = element.center;
|
|
547
|
-
const targetX = Math.round(x);
|
|
548
|
-
const targetY = Math.round(y);
|
|
549
|
-
await smoothMoveMouse(targetX, targetY, SMOOTH_MOVE_STEPS_TAP, SMOOTH_MOVE_DELAY_TAP);
|
|
550
|
-
libnut.mouseToggle('down', 'left');
|
|
551
|
-
await utils_sleep(CLICK_HOLD_DURATION);
|
|
552
|
-
libnut.mouseToggle('up', 'left');
|
|
553
|
-
}),
|
|
554
|
-
device_defineActionDoubleClick(async (param)=>{
|
|
555
|
-
node_assert(libnut, 'libnut not initialized');
|
|
556
|
-
const element = param.locate;
|
|
557
|
-
node_assert(element, 'Element not found, cannot double click');
|
|
558
|
-
const [x, y] = element.center;
|
|
559
|
-
libnut.moveMouse(Math.round(x), Math.round(y));
|
|
560
|
-
libnut.mouseClick('left', true);
|
|
561
|
-
}),
|
|
562
|
-
device_defineActionRightClick(async (param)=>{
|
|
563
|
-
node_assert(libnut, 'libnut not initialized');
|
|
564
|
-
const element = param.locate;
|
|
565
|
-
node_assert(element, 'Element not found, cannot right click');
|
|
566
|
-
const [x, y] = element.center;
|
|
567
|
-
libnut.moveMouse(Math.round(x), Math.round(y));
|
|
568
|
-
libnut.mouseClick('right');
|
|
569
|
-
}),
|
|
570
|
-
device_defineAction({
|
|
571
|
-
name: 'MouseMove',
|
|
572
|
-
description: 'Move the mouse to the element',
|
|
573
|
-
interfaceAlias: 'aiHover',
|
|
574
|
-
paramSchema: actionHoverParamSchema,
|
|
575
|
-
sample: {
|
|
576
|
-
locate: {
|
|
577
|
-
prompt: 'the navigation menu item "Products"'
|
|
578
|
-
}
|
|
579
|
-
},
|
|
580
|
-
call: async (param)=>{
|
|
581
|
-
node_assert(libnut, 'libnut not initialized');
|
|
582
|
-
const element = param.locate;
|
|
583
|
-
node_assert(element, 'Element not found, cannot move mouse');
|
|
584
|
-
const [x, y] = element.center;
|
|
585
|
-
const targetX = Math.round(x);
|
|
586
|
-
const targetY = Math.round(y);
|
|
587
|
-
await smoothMoveMouse(targetX, targetY, SMOOTH_MOVE_STEPS_MOUSE_MOVE, SMOOTH_MOVE_DELAY_MOUSE_MOVE);
|
|
588
|
-
await utils_sleep(MOUSE_MOVE_EFFECT_WAIT);
|
|
589
|
-
}
|
|
590
|
-
}),
|
|
591
|
-
device_defineAction({
|
|
592
|
-
name: 'Input',
|
|
593
|
-
description: 'Input text into the input field',
|
|
594
|
-
interfaceAlias: 'aiInput',
|
|
595
|
-
paramSchema: computerInputParamSchema,
|
|
596
|
-
sample: {
|
|
597
|
-
value: 'test@example.com',
|
|
598
|
-
locate: {
|
|
599
|
-
prompt: 'the email input field'
|
|
600
|
-
}
|
|
601
|
-
},
|
|
602
|
-
call: async (param)=>{
|
|
603
|
-
node_assert(libnut, 'libnut not initialized');
|
|
604
|
-
const element = param.locate;
|
|
605
|
-
if (element) {
|
|
606
|
-
const [x, y] = element.center;
|
|
607
|
-
libnut.moveMouse(Math.round(x), Math.round(y));
|
|
608
|
-
libnut.mouseClick('left');
|
|
609
|
-
await utils_sleep(INPUT_FOCUS_DELAY);
|
|
610
|
-
if ('append' !== param.mode) {
|
|
611
|
-
if (this.useAppleScript) {
|
|
612
|
-
sendKeyViaAppleScript('a', [
|
|
613
|
-
'command'
|
|
614
|
-
]);
|
|
615
|
-
await utils_sleep(50);
|
|
616
|
-
sendKeyViaAppleScript('backspace', []);
|
|
617
|
-
} else {
|
|
618
|
-
const modifier = 'darwin' === process.platform ? 'command' : 'control';
|
|
619
|
-
libnut.keyTap('a', [
|
|
620
|
-
modifier
|
|
621
|
-
]);
|
|
622
|
-
await utils_sleep(50);
|
|
623
|
-
libnut.keyTap('backspace');
|
|
624
|
-
}
|
|
625
|
-
await utils_sleep(INPUT_CLEAR_DELAY);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
if ('clear' === param.mode) return;
|
|
629
|
-
if (!param.value) return;
|
|
630
|
-
await this.smartTypeString(param.value);
|
|
631
|
-
}
|
|
632
|
-
}),
|
|
633
|
-
device_defineActionScroll(async (param)=>{
|
|
634
|
-
node_assert(libnut, 'libnut not initialized');
|
|
635
|
-
if (param.locate) {
|
|
636
|
-
const element = param.locate;
|
|
637
|
-
const [x, y] = element.center;
|
|
638
|
-
libnut.moveMouse(Math.round(x), Math.round(y));
|
|
639
|
-
}
|
|
640
|
-
const scrollType = param?.scrollType;
|
|
641
|
-
const edgeSpec = scrollType && scrollType in EDGE_SCROLL_SPEC ? EDGE_SCROLL_SPEC[scrollType] : null;
|
|
642
|
-
if (edgeSpec) {
|
|
643
|
-
if (runPhasedScroll(edgeSpec.direction, EDGE_SCROLL_TOTAL_PX, EDGE_SCROLL_STEPS)) return void await utils_sleep(SCROLL_COMPLETE_DELAY);
|
|
644
|
-
if (this.useAppleScript) {
|
|
645
|
-
sendKeyViaAppleScript(edgeSpec.key);
|
|
646
|
-
await utils_sleep(SCROLL_COMPLETE_DELAY);
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
const [dx, dy] = edgeSpec.libnut;
|
|
650
|
-
for(let i = 0; i < SCROLL_REPEAT_COUNT; i++){
|
|
651
|
-
libnut.scrollMouse(dx, dy);
|
|
652
|
-
await utils_sleep(SCROLL_STEP_DELAY);
|
|
653
|
-
}
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
if ('singleAction' === scrollType || !scrollType) {
|
|
657
|
-
const distance = param?.distance || 500;
|
|
658
|
-
const direction = param?.direction || 'down';
|
|
659
|
-
const isKnownDirection = 'up' === direction || 'down' === direction || 'left' === direction || 'right' === direction;
|
|
660
|
-
if (isKnownDirection) {
|
|
661
|
-
const steps = Math.max(PHASED_MIN_STEPS, Math.round(distance / PHASED_PIXELS_PER_STEP));
|
|
662
|
-
if (runPhasedScroll(direction, distance, steps)) return void await utils_sleep(SCROLL_COMPLETE_DELAY);
|
|
663
|
-
}
|
|
664
|
-
if (this.useAppleScript && ('up' === direction || 'down' === direction)) {
|
|
665
|
-
const pages = Math.max(1, Math.round(distance / APPROX_VIEWPORT_HEIGHT_PX));
|
|
666
|
-
const key = 'up' === direction ? 'pageup' : 'pagedown';
|
|
667
|
-
for(let i = 0; i < pages; i++){
|
|
668
|
-
sendKeyViaAppleScript(key);
|
|
669
|
-
await utils_sleep(SCROLL_STEP_DELAY);
|
|
670
|
-
}
|
|
671
|
-
await utils_sleep(SCROLL_COMPLETE_DELAY);
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
const ticks = Math.ceil(distance / 100);
|
|
675
|
-
const directionMap = {
|
|
676
|
-
up: [
|
|
677
|
-
0,
|
|
678
|
-
ticks
|
|
679
|
-
],
|
|
680
|
-
down: [
|
|
681
|
-
0,
|
|
682
|
-
-ticks
|
|
683
|
-
],
|
|
684
|
-
left: [
|
|
685
|
-
-ticks,
|
|
686
|
-
0
|
|
687
|
-
],
|
|
688
|
-
right: [
|
|
689
|
-
ticks,
|
|
690
|
-
0
|
|
691
|
-
]
|
|
692
|
-
};
|
|
693
|
-
const [dx, dy] = directionMap[direction] || [
|
|
694
|
-
0,
|
|
695
|
-
-ticks
|
|
696
|
-
];
|
|
697
|
-
libnut.scrollMouse(dx, dy);
|
|
698
|
-
await utils_sleep(SCROLL_COMPLETE_DELAY);
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
throw new Error(`Unknown scroll type: ${scrollType}, param: ${JSON.stringify(param)}`);
|
|
702
|
-
}),
|
|
703
|
-
device_defineActionKeyboardPress(async (param)=>{
|
|
704
|
-
node_assert(libnut, 'libnut not initialized');
|
|
705
|
-
if (param.locate) {
|
|
706
|
-
const [x, y] = param.locate.center;
|
|
707
|
-
libnut.moveMouse(Math.round(x), Math.round(y));
|
|
708
|
-
libnut.mouseClick('left');
|
|
709
|
-
await utils_sleep(50);
|
|
710
|
-
}
|
|
711
|
-
const keys = param.keyName.split('+');
|
|
712
|
-
const modifiers = keys.slice(0, -1).map(normalizeKeyName);
|
|
713
|
-
const key = normalizePrimaryKey(keys[keys.length - 1]);
|
|
714
|
-
debugDevice('KeyboardPress', {
|
|
715
|
-
original: param.keyName,
|
|
716
|
-
key,
|
|
717
|
-
modifiers,
|
|
718
|
-
driver: this.useAppleScript ? "applescript" : 'libnut'
|
|
719
|
-
});
|
|
720
|
-
if (this.useAppleScript) sendKeyViaAppleScript(key, modifiers);
|
|
721
|
-
else if (modifiers.length > 0) libnut.keyTap(key, modifiers);
|
|
722
|
-
else libnut.keyTap(key);
|
|
723
|
-
}),
|
|
724
|
-
device_defineActionDragAndDrop(async (param)=>{
|
|
725
|
-
node_assert(libnut, 'libnut not initialized');
|
|
726
|
-
const from = param.from;
|
|
727
|
-
const to = param.to;
|
|
728
|
-
node_assert(from, 'missing "from" param for drag and drop');
|
|
729
|
-
node_assert(to, 'missing "to" param for drag and drop');
|
|
730
|
-
const [fromX, fromY] = from.center;
|
|
731
|
-
const [toX, toY] = to.center;
|
|
732
|
-
libnut.moveMouse(Math.round(fromX), Math.round(fromY));
|
|
733
|
-
libnut.mouseToggle('down', 'left');
|
|
734
|
-
await utils_sleep(100);
|
|
735
|
-
libnut.moveMouse(Math.round(toX), Math.round(toY));
|
|
736
|
-
await utils_sleep(100);
|
|
737
|
-
libnut.mouseToggle('up', 'left');
|
|
738
|
-
}),
|
|
739
|
-
device_defineActionClearInput(async (param)=>{
|
|
740
|
-
node_assert(libnut, 'libnut not initialized');
|
|
741
|
-
const element = param.locate;
|
|
742
|
-
node_assert(element, 'Element not found, cannot clear input');
|
|
743
|
-
const [x, y] = element.center;
|
|
744
|
-
libnut.moveMouse(Math.round(x), Math.round(y));
|
|
745
|
-
libnut.mouseClick('left');
|
|
746
|
-
await utils_sleep(100);
|
|
747
|
-
if (this.useAppleScript) {
|
|
748
|
-
sendKeyViaAppleScript('a', [
|
|
749
|
-
'command'
|
|
750
|
-
]);
|
|
751
|
-
await utils_sleep(50);
|
|
752
|
-
sendKeyViaAppleScript('backspace', []);
|
|
753
|
-
} else {
|
|
754
|
-
const modifier = 'darwin' === process.platform ? 'command' : 'control';
|
|
755
|
-
libnut.keyTap('a', [
|
|
756
|
-
modifier
|
|
757
|
-
]);
|
|
758
|
-
libnut.keyTap('backspace');
|
|
759
|
-
}
|
|
760
|
-
await utils_sleep(50);
|
|
761
|
-
})
|
|
652
|
+
...defineActionsFromInputPrimitives(this.inputPrimitives)
|
|
762
653
|
];
|
|
763
654
|
const platformActions = Object.values(createPlatformActions());
|
|
764
655
|
const customActions = this.options?.customActions || [];
|
|
@@ -796,6 +687,88 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
|
|
|
796
687
|
_define_property(this, "xvfbCleanup", void 0);
|
|
797
688
|
_define_property(this, "useAppleScript", void 0);
|
|
798
689
|
_define_property(this, "uri", void 0);
|
|
690
|
+
_define_property(this, "inputPrimitives", {
|
|
691
|
+
pointer: {
|
|
692
|
+
tap: async ({ x, y })=>{
|
|
693
|
+
node_assert(libnut, 'libnut not initialized');
|
|
694
|
+
const targetX = Math.round(x);
|
|
695
|
+
const targetY = Math.round(y);
|
|
696
|
+
await smoothMoveMouse(targetX, targetY, SMOOTH_MOVE_STEPS_TAP, SMOOTH_MOVE_DELAY_TAP);
|
|
697
|
+
libnut.mouseToggle('down', 'left');
|
|
698
|
+
await sleep(CLICK_HOLD_DURATION);
|
|
699
|
+
libnut.mouseToggle('up', 'left');
|
|
700
|
+
},
|
|
701
|
+
doubleClick: async ({ x, y })=>{
|
|
702
|
+
node_assert(libnut, 'libnut not initialized');
|
|
703
|
+
libnut.moveMouse(Math.round(x), Math.round(y));
|
|
704
|
+
libnut.mouseClick('left', true);
|
|
705
|
+
},
|
|
706
|
+
rightClick: async ({ x, y })=>{
|
|
707
|
+
node_assert(libnut, 'libnut not initialized');
|
|
708
|
+
libnut.moveMouse(Math.round(x), Math.round(y));
|
|
709
|
+
libnut.mouseClick('right');
|
|
710
|
+
},
|
|
711
|
+
hover: async ({ x, y })=>{
|
|
712
|
+
node_assert(libnut, 'libnut not initialized');
|
|
713
|
+
await smoothMoveMouse(Math.round(x), Math.round(y), SMOOTH_MOVE_STEPS_MOUSE_MOVE, SMOOTH_MOVE_DELAY_MOUSE_MOVE);
|
|
714
|
+
await sleep(MOUSE_MOVE_EFFECT_WAIT);
|
|
715
|
+
},
|
|
716
|
+
dragAndDrop: async (from, to)=>{
|
|
717
|
+
node_assert(libnut, 'libnut not initialized');
|
|
718
|
+
libnut.moveMouse(Math.round(from.x), Math.round(from.y));
|
|
719
|
+
libnut.mouseToggle('down', 'left');
|
|
720
|
+
await sleep(100);
|
|
721
|
+
libnut.moveMouse(Math.round(to.x), Math.round(to.y));
|
|
722
|
+
await sleep(100);
|
|
723
|
+
libnut.mouseToggle('up', 'left');
|
|
724
|
+
}
|
|
725
|
+
},
|
|
726
|
+
keyboard: {
|
|
727
|
+
typeText: async (value, opts)=>{
|
|
728
|
+
node_assert(libnut, 'libnut not initialized');
|
|
729
|
+
const element = opts?.target;
|
|
730
|
+
if (element) {
|
|
731
|
+
const [x, y] = element.center;
|
|
732
|
+
libnut.moveMouse(Math.round(x), Math.round(y));
|
|
733
|
+
libnut.mouseClick('left');
|
|
734
|
+
await sleep(INPUT_FOCUS_DELAY);
|
|
735
|
+
if (opts?.replace !== false) {
|
|
736
|
+
await this.selectAllAndDelete();
|
|
737
|
+
await sleep(INPUT_CLEAR_DELAY);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
await this.smartTypeString(value);
|
|
741
|
+
},
|
|
742
|
+
keyboardPress: async (keyName, opts)=>{
|
|
743
|
+
node_assert(libnut, 'libnut not initialized');
|
|
744
|
+
const target = opts?.target;
|
|
745
|
+
if (target) {
|
|
746
|
+
const [x, y] = target.center;
|
|
747
|
+
libnut.moveMouse(Math.round(x), Math.round(y));
|
|
748
|
+
libnut.mouseClick('left');
|
|
749
|
+
await sleep(50);
|
|
750
|
+
}
|
|
751
|
+
await this.pressKeyboardShortcut(keyName);
|
|
752
|
+
},
|
|
753
|
+
clearInput: async (target)=>{
|
|
754
|
+
node_assert(libnut, 'libnut not initialized');
|
|
755
|
+
if (target) {
|
|
756
|
+
const element = target;
|
|
757
|
+
const [x, y] = element.center;
|
|
758
|
+
libnut.moveMouse(Math.round(x), Math.round(y));
|
|
759
|
+
libnut.mouseClick('left');
|
|
760
|
+
await sleep(100);
|
|
761
|
+
}
|
|
762
|
+
await this.selectAllAndDelete();
|
|
763
|
+
await sleep(50);
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
scroll: {
|
|
767
|
+
scroll: async (param)=>{
|
|
768
|
+
await this.performScroll(param);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
});
|
|
799
772
|
this.options = options;
|
|
800
773
|
this.displayId = options?.displayId;
|
|
801
774
|
this.useAppleScript = 'darwin' === process.platform && options?.keyboardDriver !== 'libnut';
|
|
@@ -803,15 +776,584 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
|
|
|
803
776
|
}
|
|
804
777
|
function createPlatformActions() {
|
|
805
778
|
return {
|
|
806
|
-
ListDisplays:
|
|
779
|
+
ListDisplays: defineAction({
|
|
807
780
|
name: 'ListDisplays',
|
|
808
781
|
description: 'List all available displays/monitors',
|
|
809
782
|
call: async ()=>await ComputerDevice.listDisplays()
|
|
810
783
|
})
|
|
811
784
|
};
|
|
812
785
|
}
|
|
813
|
-
|
|
814
|
-
|
|
786
|
+
const platformBinaryMap = {
|
|
787
|
+
darwin: {
|
|
788
|
+
directory: 'darwin',
|
|
789
|
+
fileName: 'rdp-helper'
|
|
790
|
+
},
|
|
791
|
+
linux: {
|
|
792
|
+
directory: 'linux',
|
|
793
|
+
fileName: 'rdp-helper'
|
|
794
|
+
},
|
|
795
|
+
win32: {
|
|
796
|
+
directory: 'win32',
|
|
797
|
+
fileName: 'rdp-helper.exe'
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
function getPlatformBinary(platform) {
|
|
801
|
+
if (platform in platformBinaryMap) return platformBinaryMap[platform];
|
|
802
|
+
}
|
|
803
|
+
function currentDirname() {
|
|
804
|
+
if ('undefined' != typeof __dirname) return __dirname;
|
|
805
|
+
return dirname(fileURLToPath(import.meta.url));
|
|
806
|
+
}
|
|
807
|
+
function getRdpHelperBinaryPath() {
|
|
808
|
+
const platformBinary = getPlatformBinary(process.platform);
|
|
809
|
+
if (!platformBinary) throw new Error(`@midscene/computer RDP helper does not support platform ${process.platform}`);
|
|
810
|
+
const hereDir = currentDirname();
|
|
811
|
+
const candidateRoots = [
|
|
812
|
+
external_node_path_resolve(hereDir, '../..'),
|
|
813
|
+
external_node_path_resolve(hereDir, '../../..')
|
|
814
|
+
];
|
|
815
|
+
for (const root of candidateRoots){
|
|
816
|
+
const binaryPath = external_node_path_resolve(root, 'bin', platformBinary.directory, platformBinary.fileName);
|
|
817
|
+
if (existsSync(binaryPath)) return binaryPath;
|
|
818
|
+
}
|
|
819
|
+
throw new Error(`RDP helper binary not found for ${process.platform}. Run \`pnpm --filter @midscene/computer run build:native\` first.`);
|
|
820
|
+
}
|
|
821
|
+
function backend_client_define_property(obj, key, value) {
|
|
822
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
823
|
+
value: value,
|
|
824
|
+
enumerable: true,
|
|
825
|
+
configurable: true,
|
|
826
|
+
writable: true
|
|
827
|
+
});
|
|
828
|
+
else obj[key] = value;
|
|
829
|
+
return obj;
|
|
830
|
+
}
|
|
831
|
+
const debug = getDebug('rdp:backend');
|
|
832
|
+
const HELPER_SHUTDOWN_TIMEOUT_MS = 3000;
|
|
833
|
+
const MAX_STDERR_LINES = 40;
|
|
834
|
+
class HelperProcessRDPBackendClient {
|
|
835
|
+
async connect(config) {
|
|
836
|
+
this.fatalHelperError = void 0;
|
|
837
|
+
await this.ensureHelperStarted();
|
|
838
|
+
const response = await this.send({
|
|
839
|
+
type: 'connect',
|
|
840
|
+
config
|
|
841
|
+
});
|
|
842
|
+
if ('connected' !== response.type) throw new Error(`Expected connected response, got ${response.type}`);
|
|
843
|
+
this.connected = true;
|
|
844
|
+
this.fatalHelperError = void 0;
|
|
845
|
+
return response.info;
|
|
846
|
+
}
|
|
847
|
+
async disconnect() {
|
|
848
|
+
const child = this.child;
|
|
849
|
+
if (!child) return;
|
|
850
|
+
let disconnectError;
|
|
851
|
+
if (this.connected && null === child.exitCode) try {
|
|
852
|
+
const response = await this.send({
|
|
853
|
+
type: 'disconnect'
|
|
854
|
+
});
|
|
855
|
+
this.expectOk(response, 'disconnect');
|
|
856
|
+
} catch (error) {
|
|
857
|
+
disconnectError = error instanceof Error ? error : new Error(String(error));
|
|
858
|
+
}
|
|
859
|
+
this.connected = false;
|
|
860
|
+
this.fatalHelperError = void 0;
|
|
861
|
+
await this.shutdownHelper();
|
|
862
|
+
if (disconnectError && !/RDP helper exited unexpectedly|RDP helper is not running|RDP helper shut down/u.test(disconnectError.message)) throw disconnectError;
|
|
863
|
+
}
|
|
864
|
+
async screenshotBase64() {
|
|
865
|
+
const response = await this.send({
|
|
866
|
+
type: 'screenshot'
|
|
867
|
+
});
|
|
868
|
+
if ('screenshot' !== response.type) throw new Error(`Expected screenshot response, got ${response.type}`);
|
|
869
|
+
return response.base64;
|
|
870
|
+
}
|
|
871
|
+
async size() {
|
|
872
|
+
const response = await this.send({
|
|
873
|
+
type: 'size'
|
|
874
|
+
});
|
|
875
|
+
if ('size' !== response.type) throw new Error(`Expected size response, got ${response.type}`);
|
|
876
|
+
return response.size;
|
|
877
|
+
}
|
|
878
|
+
async mouseMove(x, y) {
|
|
879
|
+
const response = await this.send({
|
|
880
|
+
type: 'mouseMove',
|
|
881
|
+
x,
|
|
882
|
+
y
|
|
883
|
+
});
|
|
884
|
+
this.expectOk(response, 'mouseMove');
|
|
885
|
+
}
|
|
886
|
+
async mouseButton(button, action) {
|
|
887
|
+
const response = await this.send({
|
|
888
|
+
type: 'mouseButton',
|
|
889
|
+
button,
|
|
890
|
+
action
|
|
891
|
+
});
|
|
892
|
+
this.expectOk(response, 'mouseButton');
|
|
893
|
+
}
|
|
894
|
+
async wheel(direction, amount, x, y) {
|
|
895
|
+
const response = await this.send({
|
|
896
|
+
type: 'wheel',
|
|
897
|
+
direction,
|
|
898
|
+
amount,
|
|
899
|
+
x,
|
|
900
|
+
y
|
|
901
|
+
});
|
|
902
|
+
this.expectOk(response, 'wheel');
|
|
903
|
+
}
|
|
904
|
+
async keyPress(keyName) {
|
|
905
|
+
const response = await this.send({
|
|
906
|
+
type: 'keyPress',
|
|
907
|
+
keyName
|
|
908
|
+
});
|
|
909
|
+
this.expectOk(response, 'keyPress');
|
|
910
|
+
}
|
|
911
|
+
async typeText(text) {
|
|
912
|
+
const response = await this.send({
|
|
913
|
+
type: 'typeText',
|
|
914
|
+
text
|
|
915
|
+
});
|
|
916
|
+
this.expectOk(response, 'typeText');
|
|
917
|
+
}
|
|
918
|
+
async clearInput() {
|
|
919
|
+
const response = await this.send({
|
|
920
|
+
type: 'clearInput'
|
|
921
|
+
});
|
|
922
|
+
this.expectOk(response, 'clearInput');
|
|
923
|
+
}
|
|
924
|
+
async ensureHelperStarted() {
|
|
925
|
+
if (this.child && null === this.child.exitCode) return;
|
|
926
|
+
const helperPath = this.resolveHelperPath();
|
|
927
|
+
debug('starting rdp helper', {
|
|
928
|
+
helperPath
|
|
929
|
+
});
|
|
930
|
+
const child = this.spawnFn(helperPath, [], {
|
|
931
|
+
stdio: [
|
|
932
|
+
'pipe',
|
|
933
|
+
'pipe',
|
|
934
|
+
'pipe'
|
|
935
|
+
]
|
|
936
|
+
});
|
|
937
|
+
child.stdout.setEncoding('utf8');
|
|
938
|
+
child.stderr.setEncoding('utf8');
|
|
939
|
+
this.child = child;
|
|
940
|
+
this.stderrLines.length = 0;
|
|
941
|
+
this.stdoutReader = createInterface({
|
|
942
|
+
input: child.stdout,
|
|
943
|
+
crlfDelay: 1 / 0
|
|
944
|
+
});
|
|
945
|
+
this.stderrReader = createInterface({
|
|
946
|
+
input: child.stderr,
|
|
947
|
+
crlfDelay: 1 / 0
|
|
948
|
+
});
|
|
949
|
+
this.stdoutReader.on('line', (line)=>{
|
|
950
|
+
this.handleStdoutLine(line);
|
|
951
|
+
});
|
|
952
|
+
this.stderrReader.on('line', (line)=>{
|
|
953
|
+
this.captureStderrLine(line);
|
|
954
|
+
});
|
|
955
|
+
child.on('exit', (code, signal)=>{
|
|
956
|
+
this.connected = false;
|
|
957
|
+
const error = this.createHelperError(`RDP helper exited unexpectedly (code=${code}, signal=${signal})`);
|
|
958
|
+
this.fatalHelperError = error;
|
|
959
|
+
this.rejectPending(error);
|
|
960
|
+
this.disposeReaders();
|
|
961
|
+
this.child = void 0;
|
|
962
|
+
});
|
|
963
|
+
child.on('error', (error)=>{
|
|
964
|
+
this.connected = false;
|
|
965
|
+
const helperError = this.createHelperError(`Failed to start RDP helper: ${error.message}`);
|
|
966
|
+
this.fatalHelperError = helperError;
|
|
967
|
+
this.rejectPending(helperError);
|
|
968
|
+
this.disposeReaders();
|
|
969
|
+
this.child = void 0;
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
handleStdoutLine(line) {
|
|
973
|
+
if (!line.trim()) return;
|
|
974
|
+
let parsed;
|
|
975
|
+
try {
|
|
976
|
+
parsed = JSON.parse(line);
|
|
977
|
+
} catch (error) {
|
|
978
|
+
const protocolError = this.createHelperError(`RDP helper emitted malformed JSON: ${line}`);
|
|
979
|
+
this.rejectPending(protocolError);
|
|
980
|
+
this.shutdownHelper(protocolError);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const pending = this.pending.get(parsed.id);
|
|
984
|
+
if (!pending) return void debug('dropping response for unknown request id', parsed);
|
|
985
|
+
this.pending.delete(parsed.id);
|
|
986
|
+
if (parsed.ok) return void pending.resolve(parsed.payload);
|
|
987
|
+
pending.reject(this.createHelperError(parsed.error.message, parsed.error.code));
|
|
988
|
+
}
|
|
989
|
+
captureStderrLine(line) {
|
|
990
|
+
if (!line.trim()) return;
|
|
991
|
+
this.stderrLines.push(line);
|
|
992
|
+
if (this.stderrLines.length > MAX_STDERR_LINES) this.stderrLines.shift();
|
|
993
|
+
}
|
|
994
|
+
async send(payload) {
|
|
995
|
+
if ('connect' !== payload.type && this.fatalHelperError && (!this.child || null !== this.child.exitCode)) throw this.fatalHelperError;
|
|
996
|
+
await this.ensureHelperStarted();
|
|
997
|
+
const child = this.child;
|
|
998
|
+
if (!child || null !== child.exitCode) throw this.createHelperError('RDP helper is not running');
|
|
999
|
+
const id = `req-${++this.nextRequestId}`;
|
|
1000
|
+
const request = {
|
|
1001
|
+
id,
|
|
1002
|
+
payload
|
|
1003
|
+
};
|
|
1004
|
+
return new Promise((resolve, reject)=>{
|
|
1005
|
+
this.pending.set(id, {
|
|
1006
|
+
resolve,
|
|
1007
|
+
reject
|
|
1008
|
+
});
|
|
1009
|
+
child.stdin.write(`${JSON.stringify(request)}\n`, (error)=>{
|
|
1010
|
+
if (!error) return;
|
|
1011
|
+
this.pending.delete(id);
|
|
1012
|
+
reject(this.createHelperError(`Failed to send ${payload.type} request to RDP helper: ${error.message}`));
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
expectOk(response, actionName) {
|
|
1017
|
+
if ('ok' !== response.type) throw new Error(`Expected ok response for ${actionName}, got ${response.type}`);
|
|
1018
|
+
}
|
|
1019
|
+
rejectPending(error) {
|
|
1020
|
+
for (const { reject } of this.pending.values())reject(error);
|
|
1021
|
+
this.pending.clear();
|
|
1022
|
+
}
|
|
1023
|
+
createHelperError(message, code) {
|
|
1024
|
+
const stderrSummary = this.stderrLines.join('\n').trim();
|
|
1025
|
+
const suffix = stderrSummary ? `\nHelper stderr:\n${stderrSummary}` : '';
|
|
1026
|
+
const error = new Error(`${message}${suffix}`);
|
|
1027
|
+
if (code) error.name = code;
|
|
1028
|
+
return error;
|
|
1029
|
+
}
|
|
1030
|
+
disposeReaders() {
|
|
1031
|
+
this.stdoutReader?.close();
|
|
1032
|
+
this.stderrReader?.close();
|
|
1033
|
+
this.stdoutReader = void 0;
|
|
1034
|
+
this.stderrReader = void 0;
|
|
1035
|
+
}
|
|
1036
|
+
async shutdownHelper(rootError) {
|
|
1037
|
+
const child = this.child;
|
|
1038
|
+
this.child = void 0;
|
|
1039
|
+
this.disposeReaders();
|
|
1040
|
+
if (!child) return;
|
|
1041
|
+
this.rejectPending(rootError || this.createHelperError('RDP helper shut down'));
|
|
1042
|
+
if (null !== child.exitCode) return;
|
|
1043
|
+
child.stdin.end();
|
|
1044
|
+
const exited = Promise.race([
|
|
1045
|
+
once(child, 'exit'),
|
|
1046
|
+
new Promise((resolve)=>{
|
|
1047
|
+
setTimeout(()=>resolve('timeout'), HELPER_SHUTDOWN_TIMEOUT_MS);
|
|
1048
|
+
})
|
|
1049
|
+
]);
|
|
1050
|
+
const result = await exited;
|
|
1051
|
+
if ('timeout' !== result) return;
|
|
1052
|
+
child.kill('SIGTERM');
|
|
1053
|
+
const terminated = Promise.race([
|
|
1054
|
+
once(child, 'exit'),
|
|
1055
|
+
new Promise((resolve)=>{
|
|
1056
|
+
setTimeout(()=>resolve('timeout'), HELPER_SHUTDOWN_TIMEOUT_MS);
|
|
1057
|
+
})
|
|
1058
|
+
]);
|
|
1059
|
+
const terminateResult = await terminated;
|
|
1060
|
+
if ('timeout' !== terminateResult) return;
|
|
1061
|
+
child.kill('SIGKILL');
|
|
1062
|
+
await once(child, 'exit');
|
|
1063
|
+
}
|
|
1064
|
+
constructor(options){
|
|
1065
|
+
backend_client_define_property(this, "spawnFn", void 0);
|
|
1066
|
+
backend_client_define_property(this, "resolveHelperPath", void 0);
|
|
1067
|
+
backend_client_define_property(this, "child", void 0);
|
|
1068
|
+
backend_client_define_property(this, "stdoutReader", void 0);
|
|
1069
|
+
backend_client_define_property(this, "stderrReader", void 0);
|
|
1070
|
+
backend_client_define_property(this, "pending", new Map());
|
|
1071
|
+
backend_client_define_property(this, "stderrLines", []);
|
|
1072
|
+
backend_client_define_property(this, "nextRequestId", 0);
|
|
1073
|
+
backend_client_define_property(this, "connected", false);
|
|
1074
|
+
backend_client_define_property(this, "fatalHelperError", void 0);
|
|
1075
|
+
this.spawnFn = options?.spawnFn || spawn;
|
|
1076
|
+
const overridePath = options?.helperPath;
|
|
1077
|
+
this.resolveHelperPath = overridePath ? ()=>overridePath : getRdpHelperBinaryPath;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
function createDefaultRDPBackendClient() {
|
|
1081
|
+
return new HelperProcessRDPBackendClient();
|
|
1082
|
+
}
|
|
1083
|
+
function device_define_property(obj, key, value) {
|
|
1084
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
1085
|
+
value: value,
|
|
1086
|
+
enumerable: true,
|
|
1087
|
+
configurable: true,
|
|
1088
|
+
writable: true
|
|
1089
|
+
});
|
|
1090
|
+
else obj[key] = value;
|
|
1091
|
+
return obj;
|
|
1092
|
+
}
|
|
1093
|
+
const device_debug = getDebug('rdp:device');
|
|
1094
|
+
const device_SMOOTH_MOVE_STEPS_TAP = 8;
|
|
1095
|
+
const device_SMOOTH_MOVE_STEPS_MOUSE_MOVE = 10;
|
|
1096
|
+
const SMOOTH_MOVE_STEPS_DRAG = 12;
|
|
1097
|
+
const device_SMOOTH_MOVE_DELAY_TAP = 8;
|
|
1098
|
+
const device_SMOOTH_MOVE_DELAY_MOUSE_MOVE = 10;
|
|
1099
|
+
const SMOOTH_MOVE_DELAY_DRAG = 10;
|
|
1100
|
+
const device_MOUSE_MOVE_EFFECT_WAIT = 300;
|
|
1101
|
+
const device_CLICK_HOLD_DURATION = 50;
|
|
1102
|
+
const DRAG_HOLD_DURATION = 100;
|
|
1103
|
+
const device_INPUT_FOCUS_DELAY = 300;
|
|
1104
|
+
const device_INPUT_CLEAR_DELAY = 150;
|
|
1105
|
+
const device_SCROLL_STEP_DELAY = 100;
|
|
1106
|
+
const device_SCROLL_COMPLETE_DELAY = 500;
|
|
1107
|
+
const DEFAULT_SCROLL_DISTANCE = 480;
|
|
1108
|
+
const device_EDGE_SCROLL_STEPS = 10;
|
|
1109
|
+
const DEFAULT_SCROLL_STEP_AMOUNT = 120;
|
|
1110
|
+
class RDPDevice {
|
|
1111
|
+
describe() {
|
|
1112
|
+
const port = this.options.port || 3389;
|
|
1113
|
+
const username = this.options.username ? ` as ${this.options.username}` : '';
|
|
1114
|
+
const session = this.connectionInfo?.sessionId ? ` [session ${this.connectionInfo.sessionId}]` : '';
|
|
1115
|
+
return `RDP Device ${this.options.host}:${port}${username}${session}`;
|
|
1116
|
+
}
|
|
1117
|
+
async connect() {
|
|
1118
|
+
this.throwIfDestroyed();
|
|
1119
|
+
device_debug('connecting to rdp backend', {
|
|
1120
|
+
host: this.options.host,
|
|
1121
|
+
port: this.options.port,
|
|
1122
|
+
username: this.options.username
|
|
1123
|
+
});
|
|
1124
|
+
this.connectionInfo = await this.backend.connect(this.options);
|
|
1125
|
+
this.cursorPosition = [
|
|
1126
|
+
Math.round(this.connectionInfo.size.width / 2),
|
|
1127
|
+
Math.round(this.connectionInfo.size.height / 2)
|
|
1128
|
+
];
|
|
1129
|
+
}
|
|
1130
|
+
async screenshotBase64() {
|
|
1131
|
+
this.assertConnected();
|
|
1132
|
+
return this.backend.screenshotBase64();
|
|
1133
|
+
}
|
|
1134
|
+
async size() {
|
|
1135
|
+
this.assertConnected();
|
|
1136
|
+
return this.backend.size();
|
|
1137
|
+
}
|
|
1138
|
+
async destroy() {
|
|
1139
|
+
if (this.destroyed) return;
|
|
1140
|
+
this.destroyed = true;
|
|
1141
|
+
this.connectionInfo = void 0;
|
|
1142
|
+
this.cursorPosition = void 0;
|
|
1143
|
+
await this.backend.disconnect();
|
|
1144
|
+
}
|
|
1145
|
+
actionSpace() {
|
|
1146
|
+
const defaultActions = [
|
|
1147
|
+
...defineActionsFromInputPrimitives(this.inputPrimitives),
|
|
1148
|
+
defineAction({
|
|
1149
|
+
name: 'ListDisplays',
|
|
1150
|
+
description: 'List all available displays/monitors',
|
|
1151
|
+
call: async ()=>{
|
|
1152
|
+
this.assertConnected();
|
|
1153
|
+
const size = await this.size();
|
|
1154
|
+
return [
|
|
1155
|
+
{
|
|
1156
|
+
id: this.connectionInfo?.sessionId || this.options.host,
|
|
1157
|
+
name: `RDP ${this.connectionInfo?.server || this.options.host} (${size.width}x${size.height})`,
|
|
1158
|
+
primary: true
|
|
1159
|
+
}
|
|
1160
|
+
];
|
|
1161
|
+
}
|
|
1162
|
+
})
|
|
1163
|
+
];
|
|
1164
|
+
return [
|
|
1165
|
+
...defaultActions,
|
|
1166
|
+
...this.options.customActions || []
|
|
1167
|
+
];
|
|
1168
|
+
}
|
|
1169
|
+
assertConnected() {
|
|
1170
|
+
this.throwIfDestroyed();
|
|
1171
|
+
if (!this.connectionInfo) throw new Error('RDPDevice is not connected');
|
|
1172
|
+
}
|
|
1173
|
+
throwIfDestroyed() {
|
|
1174
|
+
if (this.destroyed) throw new Error('RDPDevice has been destroyed');
|
|
1175
|
+
}
|
|
1176
|
+
async moveToElement(element, options) {
|
|
1177
|
+
this.assertConnected();
|
|
1178
|
+
const targetX = Math.round(element.center[0]);
|
|
1179
|
+
const targetY = Math.round(element.center[1]);
|
|
1180
|
+
await this.movePointer(targetX, targetY, options);
|
|
1181
|
+
}
|
|
1182
|
+
async clearInput() {
|
|
1183
|
+
if (this.backend.clearInput) return void await this.backend.clearInput();
|
|
1184
|
+
await this.backend.keyPress('Control+A');
|
|
1185
|
+
await this.backend.keyPress('Backspace');
|
|
1186
|
+
}
|
|
1187
|
+
edgeScrollDirection(scrollType) {
|
|
1188
|
+
switch(scrollType){
|
|
1189
|
+
case 'scrollToTop':
|
|
1190
|
+
return 'up';
|
|
1191
|
+
case 'scrollToBottom':
|
|
1192
|
+
return 'down';
|
|
1193
|
+
case 'scrollToLeft':
|
|
1194
|
+
return 'left';
|
|
1195
|
+
case 'scrollToRight':
|
|
1196
|
+
return 'right';
|
|
1197
|
+
case 'singleAction':
|
|
1198
|
+
return 'down';
|
|
1199
|
+
default:
|
|
1200
|
+
throw new Error(`Unsupported scroll type: ${scrollType}`);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
async movePointer(targetX, targetY, options) {
|
|
1204
|
+
this.assertConnected();
|
|
1205
|
+
const start = this.cursorPosition || [
|
|
1206
|
+
targetX,
|
|
1207
|
+
targetY
|
|
1208
|
+
];
|
|
1209
|
+
const steps = Math.max(1, options?.steps || 1);
|
|
1210
|
+
const stepDelayMs = options?.stepDelayMs || 0;
|
|
1211
|
+
for(let step = 1; step <= steps; step++){
|
|
1212
|
+
const x = Math.round(start[0] + (targetX - start[0]) * step / steps);
|
|
1213
|
+
const y = Math.round(start[1] + (targetY - start[1]) * step / steps);
|
|
1214
|
+
await this.backend.mouseMove(x, y);
|
|
1215
|
+
this.cursorPosition = [
|
|
1216
|
+
x,
|
|
1217
|
+
y
|
|
1218
|
+
];
|
|
1219
|
+
if (stepDelayMs > 0 && step < steps) await sleep(stepDelayMs);
|
|
1220
|
+
}
|
|
1221
|
+
if (options?.settleDelayMs) await sleep(options.settleDelayMs);
|
|
1222
|
+
}
|
|
1223
|
+
async performWheel(direction, amount, x, y) {
|
|
1224
|
+
let remaining = Math.abs(amount);
|
|
1225
|
+
if (0 === remaining) remaining = DEFAULT_SCROLL_STEP_AMOUNT;
|
|
1226
|
+
while(remaining > 0){
|
|
1227
|
+
const chunk = Math.min(remaining, DEFAULT_SCROLL_STEP_AMOUNT);
|
|
1228
|
+
await this.backend.wheel(direction, chunk, x, y);
|
|
1229
|
+
remaining -= chunk;
|
|
1230
|
+
if (remaining > 0) await sleep(device_SCROLL_STEP_DELAY);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
constructor(options){
|
|
1234
|
+
device_define_property(this, "interfaceType", 'rdp');
|
|
1235
|
+
device_define_property(this, "options", void 0);
|
|
1236
|
+
device_define_property(this, "backend", void 0);
|
|
1237
|
+
device_define_property(this, "connectionInfo", void 0);
|
|
1238
|
+
device_define_property(this, "destroyed", false);
|
|
1239
|
+
device_define_property(this, "cursorPosition", void 0);
|
|
1240
|
+
device_define_property(this, "uri", void 0);
|
|
1241
|
+
device_define_property(this, "inputPrimitives", {
|
|
1242
|
+
pointer: {
|
|
1243
|
+
tap: async ({ x, y })=>{
|
|
1244
|
+
await this.movePointer(Math.round(x), Math.round(y), {
|
|
1245
|
+
steps: device_SMOOTH_MOVE_STEPS_TAP,
|
|
1246
|
+
stepDelayMs: device_SMOOTH_MOVE_DELAY_TAP
|
|
1247
|
+
});
|
|
1248
|
+
await this.backend.mouseButton('left', 'down');
|
|
1249
|
+
await sleep(device_CLICK_HOLD_DURATION);
|
|
1250
|
+
await this.backend.mouseButton('left', 'up');
|
|
1251
|
+
},
|
|
1252
|
+
doubleClick: async ({ x, y })=>{
|
|
1253
|
+
await this.movePointer(Math.round(x), Math.round(y), {
|
|
1254
|
+
steps: device_SMOOTH_MOVE_STEPS_TAP,
|
|
1255
|
+
stepDelayMs: device_SMOOTH_MOVE_DELAY_TAP
|
|
1256
|
+
});
|
|
1257
|
+
await this.backend.mouseButton('left', 'doubleClick');
|
|
1258
|
+
},
|
|
1259
|
+
rightClick: async ({ x, y })=>{
|
|
1260
|
+
await this.movePointer(Math.round(x), Math.round(y), {
|
|
1261
|
+
steps: device_SMOOTH_MOVE_STEPS_TAP,
|
|
1262
|
+
stepDelayMs: device_SMOOTH_MOVE_DELAY_TAP
|
|
1263
|
+
});
|
|
1264
|
+
await this.backend.mouseButton('right', 'click');
|
|
1265
|
+
},
|
|
1266
|
+
hover: async ({ x, y })=>{
|
|
1267
|
+
await this.movePointer(Math.round(x), Math.round(y), {
|
|
1268
|
+
steps: device_SMOOTH_MOVE_STEPS_MOUSE_MOVE,
|
|
1269
|
+
stepDelayMs: device_SMOOTH_MOVE_DELAY_MOUSE_MOVE,
|
|
1270
|
+
settleDelayMs: device_MOUSE_MOVE_EFFECT_WAIT
|
|
1271
|
+
});
|
|
1272
|
+
},
|
|
1273
|
+
dragAndDrop: async (from, to)=>{
|
|
1274
|
+
await this.movePointer(Math.round(from.x), Math.round(from.y), {
|
|
1275
|
+
steps: device_SMOOTH_MOVE_STEPS_TAP,
|
|
1276
|
+
stepDelayMs: device_SMOOTH_MOVE_DELAY_TAP
|
|
1277
|
+
});
|
|
1278
|
+
await this.backend.mouseButton('left', 'down');
|
|
1279
|
+
await sleep(DRAG_HOLD_DURATION);
|
|
1280
|
+
await this.movePointer(Math.round(to.x), Math.round(to.y), {
|
|
1281
|
+
steps: SMOOTH_MOVE_STEPS_DRAG,
|
|
1282
|
+
stepDelayMs: SMOOTH_MOVE_DELAY_DRAG
|
|
1283
|
+
});
|
|
1284
|
+
await sleep(DRAG_HOLD_DURATION);
|
|
1285
|
+
await this.backend.mouseButton('left', 'up');
|
|
1286
|
+
}
|
|
1287
|
+
},
|
|
1288
|
+
keyboard: {
|
|
1289
|
+
typeText: async (value, opts)=>{
|
|
1290
|
+
this.assertConnected();
|
|
1291
|
+
const target = opts?.target;
|
|
1292
|
+
if (target) {
|
|
1293
|
+
await this.inputPrimitives.pointer.tap({
|
|
1294
|
+
x: target.center[0],
|
|
1295
|
+
y: target.center[1]
|
|
1296
|
+
});
|
|
1297
|
+
await sleep(device_INPUT_FOCUS_DELAY);
|
|
1298
|
+
}
|
|
1299
|
+
if (opts?.replace !== false) {
|
|
1300
|
+
await this.clearInput();
|
|
1301
|
+
await sleep(device_INPUT_CLEAR_DELAY);
|
|
1302
|
+
}
|
|
1303
|
+
if (opts?.focusOnly || !value) return;
|
|
1304
|
+
await this.backend.typeText(value);
|
|
1305
|
+
},
|
|
1306
|
+
clearInput: async (target)=>{
|
|
1307
|
+
this.assertConnected();
|
|
1308
|
+
const element = target;
|
|
1309
|
+
if (element) {
|
|
1310
|
+
await this.inputPrimitives.pointer.tap({
|
|
1311
|
+
x: element.center[0],
|
|
1312
|
+
y: element.center[1]
|
|
1313
|
+
});
|
|
1314
|
+
await sleep(device_INPUT_FOCUS_DELAY);
|
|
1315
|
+
}
|
|
1316
|
+
await this.clearInput();
|
|
1317
|
+
await sleep(device_INPUT_CLEAR_DELAY);
|
|
1318
|
+
},
|
|
1319
|
+
keyboardPress: async (keyName, opts)=>{
|
|
1320
|
+
this.assertConnected();
|
|
1321
|
+
const target = opts?.target;
|
|
1322
|
+
if (target) await this.inputPrimitives.pointer.tap({
|
|
1323
|
+
x: target.center[0],
|
|
1324
|
+
y: target.center[1]
|
|
1325
|
+
});
|
|
1326
|
+
await this.backend.keyPress(keyName);
|
|
1327
|
+
}
|
|
1328
|
+
},
|
|
1329
|
+
scroll: {
|
|
1330
|
+
scroll: async (param)=>{
|
|
1331
|
+
this.assertConnected();
|
|
1332
|
+
const target = param.locate;
|
|
1333
|
+
if (target) await this.moveToElement(target, {
|
|
1334
|
+
steps: device_SMOOTH_MOVE_STEPS_MOUSE_MOVE,
|
|
1335
|
+
stepDelayMs: device_SMOOTH_MOVE_DELAY_MOUSE_MOVE
|
|
1336
|
+
});
|
|
1337
|
+
if (param.scrollType && 'singleAction' !== param.scrollType) {
|
|
1338
|
+
const direction = this.edgeScrollDirection(param.scrollType);
|
|
1339
|
+
for(let i = 0; i < device_EDGE_SCROLL_STEPS; i++)await this.performWheel(direction, DEFAULT_SCROLL_DISTANCE, target?.center[0], target?.center[1]);
|
|
1340
|
+
await sleep(device_SCROLL_COMPLETE_DELAY);
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
await this.performWheel(param.direction || 'down', param.distance || DEFAULT_SCROLL_DISTANCE, target?.center[0], target?.center[1]);
|
|
1344
|
+
await sleep(device_SCROLL_COMPLETE_DELAY);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
this.options = {
|
|
1349
|
+
port: 3389,
|
|
1350
|
+
securityProtocol: 'auto',
|
|
1351
|
+
ignoreCertificate: false,
|
|
1352
|
+
...options
|
|
1353
|
+
};
|
|
1354
|
+
this.backend = options.backend || createDefaultRDPBackendClient();
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
815
1357
|
class ComputerAgent extends Agent {
|
|
816
1358
|
}
|
|
817
1359
|
function createLocalComputerDevice(opts) {
|
|
@@ -823,12 +1365,33 @@ function createLocalComputerDevice(opts) {
|
|
|
823
1365
|
xvfbResolution: opts?.xvfbResolution
|
|
824
1366
|
});
|
|
825
1367
|
}
|
|
1368
|
+
function createRDPComputerDevice(opts) {
|
|
1369
|
+
return new RDPDevice({
|
|
1370
|
+
host: opts.host,
|
|
1371
|
+
port: opts.port,
|
|
1372
|
+
username: opts.username,
|
|
1373
|
+
password: opts.password,
|
|
1374
|
+
domain: opts.domain,
|
|
1375
|
+
adminSession: opts.adminSession,
|
|
1376
|
+
ignoreCertificate: opts.ignoreCertificate,
|
|
1377
|
+
securityProtocol: opts.securityProtocol,
|
|
1378
|
+
desktopWidth: opts.desktopWidth,
|
|
1379
|
+
desktopHeight: opts.desktopHeight,
|
|
1380
|
+
backend: opts.backend,
|
|
1381
|
+
customActions: opts.customActions
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
826
1384
|
async function agentForComputer(opts) {
|
|
827
1385
|
const device = createLocalComputerDevice(opts);
|
|
828
1386
|
await device.connect();
|
|
829
1387
|
return new ComputerAgent(device, opts);
|
|
830
1388
|
}
|
|
831
1389
|
const agentFromComputer = agentForComputer;
|
|
1390
|
+
async function agentForRDPComputer(opts) {
|
|
1391
|
+
const device = createRDPComputerDevice(opts);
|
|
1392
|
+
await device.connect();
|
|
1393
|
+
return new ComputerAgent(device, opts);
|
|
1394
|
+
}
|
|
832
1395
|
function mcp_tools_define_property(obj, key, value) {
|
|
833
1396
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
834
1397
|
value: value,
|
|
@@ -840,10 +1403,61 @@ function mcp_tools_define_property(obj, key, value) {
|
|
|
840
1403
|
return obj;
|
|
841
1404
|
}
|
|
842
1405
|
const mcp_tools_debug = getDebug('mcp:computer-tools');
|
|
1406
|
+
const RDP_SECURITY_PROTOCOLS = [
|
|
1407
|
+
'auto',
|
|
1408
|
+
'tls',
|
|
1409
|
+
'nla',
|
|
1410
|
+
'rdp'
|
|
1411
|
+
];
|
|
843
1412
|
const computerInitArgShape = {
|
|
844
|
-
displayId: z.string().optional().describe('Display ID (from computer_list_displays)'),
|
|
845
|
-
headless: z.boolean().optional().describe('Start virtual display via Xvfb (Linux only)')
|
|
1413
|
+
displayId: z.string().optional().describe('Display ID for local mode (from computer_list_displays). Ignored when host is set.'),
|
|
1414
|
+
headless: z.boolean().optional().describe('Start virtual display via Xvfb (Linux local mode only). Ignored when host is set.'),
|
|
1415
|
+
host: z.string().optional().describe('RDP host (FQDN or IP). Set this to switch into RDP mode.'),
|
|
1416
|
+
port: z.number().optional().describe('RDP port (default 3389). Requires host.'),
|
|
1417
|
+
username: z.string().optional().describe('RDP username. Requires host.'),
|
|
1418
|
+
password: z.string().optional().describe('RDP password. Requires host. Prefer setting via environment or a secrets manager.'),
|
|
1419
|
+
domain: z.string().optional().describe('RDP domain. Requires host.'),
|
|
1420
|
+
adminSession: z.boolean().optional().describe('Attach to the RDP admin/console session. Requires host.'),
|
|
1421
|
+
ignoreCertificate: z.boolean().optional().describe('Skip TLS certificate validation. Requires host.'),
|
|
1422
|
+
securityProtocol: z["enum"](RDP_SECURITY_PROTOCOLS).optional().describe('RDP security protocol negotiation (default auto). Requires host.'),
|
|
1423
|
+
desktopWidth: z.number().optional().describe('Remote desktop width in pixels. Requires host.'),
|
|
1424
|
+
desktopHeight: z.number().optional().describe('Remote desktop height in pixels. Requires host.')
|
|
846
1425
|
};
|
|
1426
|
+
function adaptComputerInitArgs(extracted) {
|
|
1427
|
+
if (!extracted || 0 === Object.keys(extracted).length) return;
|
|
1428
|
+
if (extracted.host) {
|
|
1429
|
+
const { displayId: _d, headless: _h, ...rdpFields } = extracted;
|
|
1430
|
+
return {
|
|
1431
|
+
mode: 'rdp',
|
|
1432
|
+
...rdpFields,
|
|
1433
|
+
host: extracted.host
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
return {
|
|
1437
|
+
mode: 'local',
|
|
1438
|
+
displayId: extracted.displayId,
|
|
1439
|
+
headless: extracted.headless
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
function shouldRetargetAgent(opts) {
|
|
1443
|
+
if (!opts) return false;
|
|
1444
|
+
if ('rdp' === opts.mode) return true;
|
|
1445
|
+
return void 0 !== opts.displayId || void 0 !== opts.headless;
|
|
1446
|
+
}
|
|
1447
|
+
function describeConnectTarget(opts) {
|
|
1448
|
+
if (opts?.mode === 'rdp') {
|
|
1449
|
+
const portSuffix = opts.port ? `:${opts.port}` : '';
|
|
1450
|
+
const userSuffix = opts.username ? ` as ${opts.username}` : '';
|
|
1451
|
+
return ` via RDP (${opts.host}${portSuffix}${userSuffix})`;
|
|
1452
|
+
}
|
|
1453
|
+
if (opts?.mode === 'local' && opts.displayId) return ` (Display: ${opts.displayId})`;
|
|
1454
|
+
return ' (Primary display)';
|
|
1455
|
+
}
|
|
1456
|
+
function getCliReportSessionTarget(opts) {
|
|
1457
|
+
if (opts?.mode === 'rdp') return `rdp:${opts.host}`;
|
|
1458
|
+
if (opts?.mode === 'local' && opts.displayId) return opts.displayId;
|
|
1459
|
+
return 'primary';
|
|
1460
|
+
}
|
|
847
1461
|
class ComputerMidsceneTools extends BaseMidsceneTools {
|
|
848
1462
|
getCliReportSessionName() {
|
|
849
1463
|
return 'midscene-computer';
|
|
@@ -852,9 +1466,7 @@ class ComputerMidsceneTools extends BaseMidsceneTools {
|
|
|
852
1466
|
return new ComputerDevice({});
|
|
853
1467
|
}
|
|
854
1468
|
async ensureAgent(opts) {
|
|
855
|
-
|
|
856
|
-
const headless = opts?.headless;
|
|
857
|
-
if (this.agent && (void 0 !== displayId || void 0 !== headless)) {
|
|
1469
|
+
if (this.agent && shouldRetargetAgent(opts)) {
|
|
858
1470
|
try {
|
|
859
1471
|
await this.agent.destroy?.();
|
|
860
1472
|
} catch (error) {
|
|
@@ -863,8 +1475,20 @@ class ComputerMidsceneTools extends BaseMidsceneTools {
|
|
|
863
1475
|
this.agent = void 0;
|
|
864
1476
|
}
|
|
865
1477
|
if (this.agent) return this.agent;
|
|
866
|
-
mcp_tools_debug('Creating Computer agent with displayId:', displayId || 'primary');
|
|
867
1478
|
const reportOptions = this.readCliReportAgentOptions();
|
|
1479
|
+
if (opts?.mode === 'rdp') {
|
|
1480
|
+
mcp_tools_debug('Creating RDP Computer agent for host:', opts.host);
|
|
1481
|
+
const { mode: _mode, ...rdpFields } = opts;
|
|
1482
|
+
const agent = await agentForRDPComputer({
|
|
1483
|
+
...rdpFields,
|
|
1484
|
+
...reportOptions ?? {}
|
|
1485
|
+
});
|
|
1486
|
+
this.agent = agent;
|
|
1487
|
+
return agent;
|
|
1488
|
+
}
|
|
1489
|
+
const displayId = opts?.mode === 'local' ? opts.displayId : void 0;
|
|
1490
|
+
const headless = opts?.mode === 'local' ? opts.headless : void 0;
|
|
1491
|
+
mcp_tools_debug('Creating Computer agent with displayId:', displayId || 'primary');
|
|
868
1492
|
const agentOpts = {
|
|
869
1493
|
...displayId ? {
|
|
870
1494
|
displayId
|
|
@@ -882,12 +1506,12 @@ class ComputerMidsceneTools extends BaseMidsceneTools {
|
|
|
882
1506
|
return [
|
|
883
1507
|
{
|
|
884
1508
|
name: 'computer_connect',
|
|
885
|
-
description:
|
|
1509
|
+
description: "Connect to a computer desktop. Default (local) mode controls the local machine; pass displayId to target a specific local display (see computer_list_displays). Pass host to switch to RDP mode and connect to a remote Windows desktop via the RDP helper binary. RDP-related options (port/username/password/domain/securityProtocol/ignoreCertificate/adminSession/desktopWidth/desktopHeight) only take effect when host is set.",
|
|
886
1510
|
schema: this.getAgentInitArgSchema(),
|
|
887
1511
|
cli: this.getAgentInitArgCliMetadata(),
|
|
888
1512
|
handler: async (args)=>{
|
|
889
1513
|
const initArgs = this.extractAgentInitParam(args);
|
|
890
|
-
const reportSession = this.createNewCliReportSession(initArgs
|
|
1514
|
+
const reportSession = this.createNewCliReportSession(getCliReportSessionTarget(initArgs));
|
|
891
1515
|
this.commitCliReportSession(reportSession);
|
|
892
1516
|
if (this.agent) {
|
|
893
1517
|
try {
|
|
@@ -903,7 +1527,7 @@ class ComputerMidsceneTools extends BaseMidsceneTools {
|
|
|
903
1527
|
content: [
|
|
904
1528
|
{
|
|
905
1529
|
type: 'text',
|
|
906
|
-
text: `Connected to computer${
|
|
1530
|
+
text: `Connected to computer${describeConnectTarget(initArgs)}`
|
|
907
1531
|
},
|
|
908
1532
|
...this.buildScreenshotContent(screenshot)
|
|
909
1533
|
]
|
|
@@ -941,14 +1565,14 @@ class ComputerMidsceneTools extends BaseMidsceneTools {
|
|
|
941
1565
|
cli: {
|
|
942
1566
|
preferBareKeys: true
|
|
943
1567
|
},
|
|
944
|
-
adapt: (extracted)=>extracted
|
|
1568
|
+
adapt: (extracted)=>adaptComputerInitArgs(extracted)
|
|
945
1569
|
});
|
|
946
1570
|
}
|
|
947
1571
|
}
|
|
948
1572
|
const tools = new ComputerMidsceneTools();
|
|
949
1573
|
runToolsCLI(tools, 'midscene-computer', {
|
|
950
1574
|
stripPrefix: 'computer_',
|
|
951
|
-
version: "1.8.
|
|
1575
|
+
version: "1.8.1",
|
|
952
1576
|
extraCommands: createReportCliCommands()
|
|
953
1577
|
}).catch((e)=>{
|
|
954
1578
|
process.exit(reportCLIError(e));
|