@midscene/computer 1.4.6 → 1.4.7
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 +145 -39
- package/dist/es/index.mjs +132 -32
- package/dist/es/mcp-server.mjs +145 -39
- package/dist/lib/cli.js +144 -38
- package/dist/lib/index.js +138 -32
- package/dist/lib/mcp-server.js +144 -38
- package/dist/types/index.d.ts +25 -0
- package/dist/types/mcp-server.d.ts +14 -1
- package/package.json +3 -3
package/dist/es/mcp-server.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BaseMCPServer, BaseMidsceneTools, createMCPServerLauncher } from "@midscene/shared/mcp";
|
|
2
2
|
import { Agent } from "@midscene/core/agent";
|
|
3
3
|
import node_assert from "node:assert";
|
|
4
|
-
import { execSync } from "node:child_process";
|
|
4
|
+
import { execSync, spawn } from "node:child_process";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { getMidsceneLocationSchema, z } from "@midscene/core";
|
|
7
7
|
import { actionHoverParamSchema, defineAction, defineActionClearInput, defineActionDoubleClick, defineActionDragAndDrop, defineActionKeyboardPress, defineActionRightClick, defineActionScroll, defineActionTap } from "@midscene/core/device";
|
|
@@ -9,6 +9,74 @@ import { sleep } from "@midscene/core/utils";
|
|
|
9
9
|
import { createImgBase64ByFormat } from "@midscene/shared/img";
|
|
10
10
|
import { getDebug } from "@midscene/shared/logger";
|
|
11
11
|
import screenshot_desktop from "screenshot-desktop";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
const debugXvfb = getDebug('computer:xvfb');
|
|
14
|
+
function checkXvfbInstalled() {
|
|
15
|
+
try {
|
|
16
|
+
execSync('which Xvfb', {
|
|
17
|
+
stdio: 'ignore'
|
|
18
|
+
});
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function findAvailableDisplay(startFrom = 99) {
|
|
25
|
+
for(let n = startFrom; n < startFrom + 100; n++)if (!existsSync(`/tmp/.X${n}-lock`)) return n;
|
|
26
|
+
throw new Error(`No available display number found (checked ${startFrom} to ${startFrom + 99})`);
|
|
27
|
+
}
|
|
28
|
+
function startXvfb(options) {
|
|
29
|
+
const resolution = options?.resolution || '1920x1080x24';
|
|
30
|
+
const displayNum = options?.displayNumber ?? findAvailableDisplay();
|
|
31
|
+
const display = `:${displayNum}`;
|
|
32
|
+
return new Promise((resolve, reject)=>{
|
|
33
|
+
debugXvfb(`Starting Xvfb on display ${display} with resolution ${resolution}`);
|
|
34
|
+
const xvfbProcess = spawn('Xvfb', [
|
|
35
|
+
display,
|
|
36
|
+
'-screen',
|
|
37
|
+
'0',
|
|
38
|
+
resolution,
|
|
39
|
+
'-ac',
|
|
40
|
+
'-nolisten',
|
|
41
|
+
'tcp'
|
|
42
|
+
], {
|
|
43
|
+
stdio: 'ignore'
|
|
44
|
+
});
|
|
45
|
+
let settled = false;
|
|
46
|
+
xvfbProcess.on('error', (err)=>{
|
|
47
|
+
if (!settled) {
|
|
48
|
+
settled = true;
|
|
49
|
+
reject(new Error(`Failed to start Xvfb: ${err.message}`));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
xvfbProcess.on('exit', (code)=>{
|
|
53
|
+
if (!settled) {
|
|
54
|
+
settled = true;
|
|
55
|
+
reject(new Error(`Xvfb exited unexpectedly with code ${code}`));
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
const instance = {
|
|
59
|
+
process: xvfbProcess,
|
|
60
|
+
display,
|
|
61
|
+
stop () {
|
|
62
|
+
try {
|
|
63
|
+
xvfbProcess.kill('SIGTERM');
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
setTimeout(()=>{
|
|
68
|
+
if (!settled) {
|
|
69
|
+
settled = true;
|
|
70
|
+
debugXvfb(`Xvfb started on display ${display}`);
|
|
71
|
+
resolve(instance);
|
|
72
|
+
}
|
|
73
|
+
}, 500);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function needsXvfb(explicitOpt) {
|
|
77
|
+
if ('linux' !== process.platform) return false;
|
|
78
|
+
return true === explicitOpt;
|
|
79
|
+
}
|
|
12
80
|
function _define_property(obj, key, value) {
|
|
13
81
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
14
82
|
value: value,
|
|
@@ -179,53 +247,74 @@ class ComputerDevice {
|
|
|
179
247
|
async connect() {
|
|
180
248
|
debugDevice('Connecting to computer device');
|
|
181
249
|
try {
|
|
250
|
+
const headless = this.options?.headless ?? 'true' === process.env.MIDSCENE_COMPUTER_HEADLESS_LINUX;
|
|
251
|
+
if (needsXvfb(headless)) {
|
|
252
|
+
if (!checkXvfbInstalled()) throw new Error('Xvfb is required for headless mode but not installed. Install: sudo apt-get install xvfb');
|
|
253
|
+
this.xvfbInstance = await startXvfb({
|
|
254
|
+
resolution: this.options?.xvfbResolution
|
|
255
|
+
});
|
|
256
|
+
process.env.DISPLAY = this.xvfbInstance.display;
|
|
257
|
+
debugDevice(`Xvfb started on display ${this.xvfbInstance.display}`);
|
|
258
|
+
this.xvfbCleanup = ()=>{
|
|
259
|
+
if (this.xvfbInstance) {
|
|
260
|
+
this.xvfbInstance.stop();
|
|
261
|
+
this.xvfbInstance = void 0;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
process.on('exit', this.xvfbCleanup);
|
|
265
|
+
process.on('SIGINT', this.xvfbCleanup);
|
|
266
|
+
process.on('SIGTERM', this.xvfbCleanup);
|
|
267
|
+
}
|
|
182
268
|
libnut = await getLibnut();
|
|
183
269
|
const size = await this.size();
|
|
184
270
|
const displays = await ComputerDevice.listDisplays();
|
|
271
|
+
const headlessInfo = this.xvfbInstance ? `\nHeadless: true (Xvfb on ${this.xvfbInstance.display})` : '';
|
|
185
272
|
this.description = `
|
|
186
273
|
Type: Computer
|
|
187
274
|
Platform: ${process.platform}
|
|
188
275
|
Display: ${this.displayId || 'Primary'}
|
|
189
276
|
Screen Size: ${size.width}x${size.height}
|
|
190
|
-
Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ') : 'Unknown'}
|
|
277
|
+
Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ') : 'Unknown'}${headlessInfo}
|
|
191
278
|
`;
|
|
192
279
|
debugDevice('Computer device connected', this.description);
|
|
280
|
+
await this.healthCheck();
|
|
193
281
|
} catch (error) {
|
|
282
|
+
if (this.xvfbInstance) {
|
|
283
|
+
this.xvfbInstance.stop();
|
|
284
|
+
this.xvfbInstance = void 0;
|
|
285
|
+
}
|
|
194
286
|
debugDevice(`Failed to connect: ${error}`);
|
|
195
287
|
throw new Error(`Unable to connect to computer device: ${error}`);
|
|
196
288
|
}
|
|
197
|
-
await this.healthCheck();
|
|
198
289
|
}
|
|
199
290
|
async healthCheck() {
|
|
200
291
|
console.log('[HealthCheck] Starting health check...');
|
|
201
292
|
console.log('[HealthCheck] Taking screenshot...');
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
293
|
+
const screenshotTimeout = 15000;
|
|
294
|
+
let timeoutId;
|
|
295
|
+
const timeoutPromise = new Promise((_, reject)=>{
|
|
296
|
+
timeoutId = setTimeout(()=>reject(new Error('Screenshot timed out')), screenshotTimeout);
|
|
297
|
+
});
|
|
298
|
+
const base64 = await Promise.race([
|
|
299
|
+
this.screenshotBase64().finally(()=>clearTimeout(timeoutId)),
|
|
300
|
+
timeoutPromise
|
|
301
|
+
]);
|
|
302
|
+
console.log(`[HealthCheck] Screenshot succeeded (length=${base64.length})`);
|
|
209
303
|
console.log('[HealthCheck] Moving mouse...');
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
console.log(`[HealthCheck] Mouse restored to (${startPos.x}, ${startPos.y})`);
|
|
225
|
-
} catch (error) {
|
|
226
|
-
console.error(`[HealthCheck] Mouse move failed: ${error}`);
|
|
227
|
-
process.exit(1);
|
|
228
|
-
}
|
|
304
|
+
node_assert(libnut, 'libnut not initialized');
|
|
305
|
+
const startPos = libnut.getMousePos();
|
|
306
|
+
console.log(`[HealthCheck] Current mouse position: (${startPos.x}, ${startPos.y})`);
|
|
307
|
+
const offsetX = Math.floor(40 * Math.random()) + 10;
|
|
308
|
+
const offsetY = Math.floor(40 * Math.random()) + 10;
|
|
309
|
+
const targetX = startPos.x + offsetX;
|
|
310
|
+
const targetY = startPos.y + offsetY;
|
|
311
|
+
console.log(`[HealthCheck] Moving mouse to (${targetX}, ${targetY})...`);
|
|
312
|
+
libnut.moveMouse(targetX, targetY);
|
|
313
|
+
await sleep(50);
|
|
314
|
+
const movedPos = libnut.getMousePos();
|
|
315
|
+
console.log(`[HealthCheck] Mouse position after move: (${movedPos.x}, ${movedPos.y})`);
|
|
316
|
+
libnut.moveMouse(startPos.x, startPos.y);
|
|
317
|
+
console.log(`[HealthCheck] Mouse restored to (${startPos.x}, ${startPos.y})`);
|
|
229
318
|
console.log('[HealthCheck] Health check passed');
|
|
230
319
|
}
|
|
231
320
|
async screenshotBase64() {
|
|
@@ -255,8 +344,7 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
|
|
|
255
344
|
const screenSize = libnut.getScreenSize();
|
|
256
345
|
return {
|
|
257
346
|
width: screenSize.width,
|
|
258
|
-
height: screenSize.height
|
|
259
|
-
dpr: 1
|
|
347
|
+
height: screenSize.height
|
|
260
348
|
};
|
|
261
349
|
} catch (error) {
|
|
262
350
|
debugDevice(`Failed to get screen size: ${error}`);
|
|
@@ -521,6 +609,16 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
|
|
|
521
609
|
}
|
|
522
610
|
async destroy() {
|
|
523
611
|
if (this.destroyed) return;
|
|
612
|
+
if (this.xvfbInstance) {
|
|
613
|
+
this.xvfbInstance.stop();
|
|
614
|
+
this.xvfbInstance = void 0;
|
|
615
|
+
}
|
|
616
|
+
if (this.xvfbCleanup) {
|
|
617
|
+
process.removeListener('exit', this.xvfbCleanup);
|
|
618
|
+
process.removeListener('SIGINT', this.xvfbCleanup);
|
|
619
|
+
process.removeListener('SIGTERM', this.xvfbCleanup);
|
|
620
|
+
this.xvfbCleanup = void 0;
|
|
621
|
+
}
|
|
524
622
|
this.destroyed = true;
|
|
525
623
|
debugDevice('Computer device destroyed');
|
|
526
624
|
}
|
|
@@ -533,6 +631,8 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
|
|
|
533
631
|
_define_property(this, "displayId", void 0);
|
|
534
632
|
_define_property(this, "description", void 0);
|
|
535
633
|
_define_property(this, "destroyed", false);
|
|
634
|
+
_define_property(this, "xvfbInstance", void 0);
|
|
635
|
+
_define_property(this, "xvfbCleanup", void 0);
|
|
536
636
|
_define_property(this, "useAppleScript", void 0);
|
|
537
637
|
_define_property(this, "uri", void 0);
|
|
538
638
|
this.options = options;
|
|
@@ -561,7 +661,7 @@ class ComputerMidsceneTools extends BaseMidsceneTools {
|
|
|
561
661
|
createTemporaryDevice() {
|
|
562
662
|
return new ComputerDevice({});
|
|
563
663
|
}
|
|
564
|
-
async ensureAgent(displayId) {
|
|
664
|
+
async ensureAgent(displayId, headless) {
|
|
565
665
|
if (this.agent && displayId) {
|
|
566
666
|
try {
|
|
567
667
|
await this.agent.destroy?.();
|
|
@@ -572,10 +672,15 @@ class ComputerMidsceneTools extends BaseMidsceneTools {
|
|
|
572
672
|
}
|
|
573
673
|
if (this.agent) return this.agent;
|
|
574
674
|
debug('Creating Computer agent with displayId:', displayId || 'primary');
|
|
575
|
-
const opts =
|
|
576
|
-
displayId
|
|
577
|
-
|
|
578
|
-
|
|
675
|
+
const opts = {
|
|
676
|
+
...displayId ? {
|
|
677
|
+
displayId
|
|
678
|
+
} : {},
|
|
679
|
+
...void 0 !== headless ? {
|
|
680
|
+
headless
|
|
681
|
+
} : {}
|
|
682
|
+
};
|
|
683
|
+
const agent = await agentFromComputer(Object.keys(opts).length > 0 ? opts : void 0);
|
|
579
684
|
this.agent = agent;
|
|
580
685
|
return agent;
|
|
581
686
|
}
|
|
@@ -585,10 +690,11 @@ class ComputerMidsceneTools extends BaseMidsceneTools {
|
|
|
585
690
|
name: 'computer_connect',
|
|
586
691
|
description: 'Connect to computer desktop. Provide displayId to connect to a specific display (use computer_list_displays to get available IDs). If not provided, uses the primary display.',
|
|
587
692
|
schema: {
|
|
588
|
-
displayId: z.string().optional().describe('Display ID (from computer_list_displays)')
|
|
693
|
+
displayId: z.string().optional().describe('Display ID (from computer_list_displays)'),
|
|
694
|
+
headless: z.boolean().optional().describe('Start virtual display via Xvfb (Linux only)')
|
|
589
695
|
},
|
|
590
|
-
handler: async ({ displayId })=>{
|
|
591
|
-
const agent = await this.ensureAgent(displayId);
|
|
696
|
+
handler: async ({ displayId, headless })=>{
|
|
697
|
+
const agent = await this.ensureAgent(displayId, headless);
|
|
592
698
|
const screenshot = await agent.interface.screenshotBase64();
|
|
593
699
|
return {
|
|
594
700
|
content: [
|
package/dist/lib/cli.js
CHANGED
|
@@ -38,6 +38,74 @@ const utils_namespaceObject = require("@midscene/core/utils");
|
|
|
38
38
|
const img_namespaceObject = require("@midscene/shared/img");
|
|
39
39
|
const external_screenshot_desktop_namespaceObject = require("screenshot-desktop");
|
|
40
40
|
var external_screenshot_desktop_default = /*#__PURE__*/ __webpack_require__.n(external_screenshot_desktop_namespaceObject);
|
|
41
|
+
const external_node_fs_namespaceObject = require("node:fs");
|
|
42
|
+
const debugXvfb = (0, logger_namespaceObject.getDebug)('computer:xvfb');
|
|
43
|
+
function checkXvfbInstalled() {
|
|
44
|
+
try {
|
|
45
|
+
(0, external_node_child_process_namespaceObject.execSync)('which Xvfb', {
|
|
46
|
+
stdio: 'ignore'
|
|
47
|
+
});
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function findAvailableDisplay(startFrom = 99) {
|
|
54
|
+
for(let n = startFrom; n < startFrom + 100; n++)if (!(0, external_node_fs_namespaceObject.existsSync)(`/tmp/.X${n}-lock`)) return n;
|
|
55
|
+
throw new Error(`No available display number found (checked ${startFrom} to ${startFrom + 99})`);
|
|
56
|
+
}
|
|
57
|
+
function startXvfb(options) {
|
|
58
|
+
const resolution = options?.resolution || '1920x1080x24';
|
|
59
|
+
const displayNum = options?.displayNumber ?? findAvailableDisplay();
|
|
60
|
+
const display = `:${displayNum}`;
|
|
61
|
+
return new Promise((resolve, reject)=>{
|
|
62
|
+
debugXvfb(`Starting Xvfb on display ${display} with resolution ${resolution}`);
|
|
63
|
+
const xvfbProcess = (0, external_node_child_process_namespaceObject.spawn)('Xvfb', [
|
|
64
|
+
display,
|
|
65
|
+
'-screen',
|
|
66
|
+
'0',
|
|
67
|
+
resolution,
|
|
68
|
+
'-ac',
|
|
69
|
+
'-nolisten',
|
|
70
|
+
'tcp'
|
|
71
|
+
], {
|
|
72
|
+
stdio: 'ignore'
|
|
73
|
+
});
|
|
74
|
+
let settled = false;
|
|
75
|
+
xvfbProcess.on('error', (err)=>{
|
|
76
|
+
if (!settled) {
|
|
77
|
+
settled = true;
|
|
78
|
+
reject(new Error(`Failed to start Xvfb: ${err.message}`));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
xvfbProcess.on('exit', (code)=>{
|
|
82
|
+
if (!settled) {
|
|
83
|
+
settled = true;
|
|
84
|
+
reject(new Error(`Xvfb exited unexpectedly with code ${code}`));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
const instance = {
|
|
88
|
+
process: xvfbProcess,
|
|
89
|
+
display,
|
|
90
|
+
stop () {
|
|
91
|
+
try {
|
|
92
|
+
xvfbProcess.kill('SIGTERM');
|
|
93
|
+
} catch {}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
setTimeout(()=>{
|
|
97
|
+
if (!settled) {
|
|
98
|
+
settled = true;
|
|
99
|
+
debugXvfb(`Xvfb started on display ${display}`);
|
|
100
|
+
resolve(instance);
|
|
101
|
+
}
|
|
102
|
+
}, 500);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function needsXvfb(explicitOpt) {
|
|
106
|
+
if ('linux' !== process.platform) return false;
|
|
107
|
+
return true === explicitOpt;
|
|
108
|
+
}
|
|
41
109
|
function _define_property(obj, key, value) {
|
|
42
110
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
43
111
|
value: value,
|
|
@@ -208,53 +276,74 @@ class ComputerDevice {
|
|
|
208
276
|
async connect() {
|
|
209
277
|
debugDevice('Connecting to computer device');
|
|
210
278
|
try {
|
|
279
|
+
const headless = this.options?.headless ?? 'true' === process.env.MIDSCENE_COMPUTER_HEADLESS_LINUX;
|
|
280
|
+
if (needsXvfb(headless)) {
|
|
281
|
+
if (!checkXvfbInstalled()) throw new Error('Xvfb is required for headless mode but not installed. Install: sudo apt-get install xvfb');
|
|
282
|
+
this.xvfbInstance = await startXvfb({
|
|
283
|
+
resolution: this.options?.xvfbResolution
|
|
284
|
+
});
|
|
285
|
+
process.env.DISPLAY = this.xvfbInstance.display;
|
|
286
|
+
debugDevice(`Xvfb started on display ${this.xvfbInstance.display}`);
|
|
287
|
+
this.xvfbCleanup = ()=>{
|
|
288
|
+
if (this.xvfbInstance) {
|
|
289
|
+
this.xvfbInstance.stop();
|
|
290
|
+
this.xvfbInstance = void 0;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
process.on('exit', this.xvfbCleanup);
|
|
294
|
+
process.on('SIGINT', this.xvfbCleanup);
|
|
295
|
+
process.on('SIGTERM', this.xvfbCleanup);
|
|
296
|
+
}
|
|
211
297
|
libnut = await getLibnut();
|
|
212
298
|
const size = await this.size();
|
|
213
299
|
const displays = await ComputerDevice.listDisplays();
|
|
300
|
+
const headlessInfo = this.xvfbInstance ? `\nHeadless: true (Xvfb on ${this.xvfbInstance.display})` : '';
|
|
214
301
|
this.description = `
|
|
215
302
|
Type: Computer
|
|
216
303
|
Platform: ${process.platform}
|
|
217
304
|
Display: ${this.displayId || 'Primary'}
|
|
218
305
|
Screen Size: ${size.width}x${size.height}
|
|
219
|
-
Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ') : 'Unknown'}
|
|
306
|
+
Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ') : 'Unknown'}${headlessInfo}
|
|
220
307
|
`;
|
|
221
308
|
debugDevice('Computer device connected', this.description);
|
|
309
|
+
await this.healthCheck();
|
|
222
310
|
} catch (error) {
|
|
311
|
+
if (this.xvfbInstance) {
|
|
312
|
+
this.xvfbInstance.stop();
|
|
313
|
+
this.xvfbInstance = void 0;
|
|
314
|
+
}
|
|
223
315
|
debugDevice(`Failed to connect: ${error}`);
|
|
224
316
|
throw new Error(`Unable to connect to computer device: ${error}`);
|
|
225
317
|
}
|
|
226
|
-
await this.healthCheck();
|
|
227
318
|
}
|
|
228
319
|
async healthCheck() {
|
|
229
320
|
console.log('[HealthCheck] Starting health check...');
|
|
230
321
|
console.log('[HealthCheck] Taking screenshot...');
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
322
|
+
const screenshotTimeout = 15000;
|
|
323
|
+
let timeoutId;
|
|
324
|
+
const timeoutPromise = new Promise((_, reject)=>{
|
|
325
|
+
timeoutId = setTimeout(()=>reject(new Error('Screenshot timed out')), screenshotTimeout);
|
|
326
|
+
});
|
|
327
|
+
const base64 = await Promise.race([
|
|
328
|
+
this.screenshotBase64().finally(()=>clearTimeout(timeoutId)),
|
|
329
|
+
timeoutPromise
|
|
330
|
+
]);
|
|
331
|
+
console.log(`[HealthCheck] Screenshot succeeded (length=${base64.length})`);
|
|
238
332
|
console.log('[HealthCheck] Moving mouse...');
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
console.log(`[HealthCheck] Mouse restored to (${startPos.x}, ${startPos.y})`);
|
|
254
|
-
} catch (error) {
|
|
255
|
-
console.error(`[HealthCheck] Mouse move failed: ${error}`);
|
|
256
|
-
process.exit(1);
|
|
257
|
-
}
|
|
333
|
+
external_node_assert_default()(libnut, 'libnut not initialized');
|
|
334
|
+
const startPos = libnut.getMousePos();
|
|
335
|
+
console.log(`[HealthCheck] Current mouse position: (${startPos.x}, ${startPos.y})`);
|
|
336
|
+
const offsetX = Math.floor(40 * Math.random()) + 10;
|
|
337
|
+
const offsetY = Math.floor(40 * Math.random()) + 10;
|
|
338
|
+
const targetX = startPos.x + offsetX;
|
|
339
|
+
const targetY = startPos.y + offsetY;
|
|
340
|
+
console.log(`[HealthCheck] Moving mouse to (${targetX}, ${targetY})...`);
|
|
341
|
+
libnut.moveMouse(targetX, targetY);
|
|
342
|
+
await (0, utils_namespaceObject.sleep)(50);
|
|
343
|
+
const movedPos = libnut.getMousePos();
|
|
344
|
+
console.log(`[HealthCheck] Mouse position after move: (${movedPos.x}, ${movedPos.y})`);
|
|
345
|
+
libnut.moveMouse(startPos.x, startPos.y);
|
|
346
|
+
console.log(`[HealthCheck] Mouse restored to (${startPos.x}, ${startPos.y})`);
|
|
258
347
|
console.log('[HealthCheck] Health check passed');
|
|
259
348
|
}
|
|
260
349
|
async screenshotBase64() {
|
|
@@ -284,8 +373,7 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
|
|
|
284
373
|
const screenSize = libnut.getScreenSize();
|
|
285
374
|
return {
|
|
286
375
|
width: screenSize.width,
|
|
287
|
-
height: screenSize.height
|
|
288
|
-
dpr: 1
|
|
376
|
+
height: screenSize.height
|
|
289
377
|
};
|
|
290
378
|
} catch (error) {
|
|
291
379
|
debugDevice(`Failed to get screen size: ${error}`);
|
|
@@ -550,6 +638,16 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
|
|
|
550
638
|
}
|
|
551
639
|
async destroy() {
|
|
552
640
|
if (this.destroyed) return;
|
|
641
|
+
if (this.xvfbInstance) {
|
|
642
|
+
this.xvfbInstance.stop();
|
|
643
|
+
this.xvfbInstance = void 0;
|
|
644
|
+
}
|
|
645
|
+
if (this.xvfbCleanup) {
|
|
646
|
+
process.removeListener('exit', this.xvfbCleanup);
|
|
647
|
+
process.removeListener('SIGINT', this.xvfbCleanup);
|
|
648
|
+
process.removeListener('SIGTERM', this.xvfbCleanup);
|
|
649
|
+
this.xvfbCleanup = void 0;
|
|
650
|
+
}
|
|
553
651
|
this.destroyed = true;
|
|
554
652
|
debugDevice('Computer device destroyed');
|
|
555
653
|
}
|
|
@@ -562,6 +660,8 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
|
|
|
562
660
|
_define_property(this, "displayId", void 0);
|
|
563
661
|
_define_property(this, "description", void 0);
|
|
564
662
|
_define_property(this, "destroyed", false);
|
|
663
|
+
_define_property(this, "xvfbInstance", void 0);
|
|
664
|
+
_define_property(this, "xvfbCleanup", void 0);
|
|
565
665
|
_define_property(this, "useAppleScript", void 0);
|
|
566
666
|
_define_property(this, "uri", void 0);
|
|
567
667
|
this.options = options;
|
|
@@ -590,7 +690,7 @@ class ComputerMidsceneTools extends mcp_namespaceObject.BaseMidsceneTools {
|
|
|
590
690
|
createTemporaryDevice() {
|
|
591
691
|
return new ComputerDevice({});
|
|
592
692
|
}
|
|
593
|
-
async ensureAgent(displayId) {
|
|
693
|
+
async ensureAgent(displayId, headless) {
|
|
594
694
|
if (this.agent && displayId) {
|
|
595
695
|
try {
|
|
596
696
|
await this.agent.destroy?.();
|
|
@@ -601,10 +701,15 @@ class ComputerMidsceneTools extends mcp_namespaceObject.BaseMidsceneTools {
|
|
|
601
701
|
}
|
|
602
702
|
if (this.agent) return this.agent;
|
|
603
703
|
debug('Creating Computer agent with displayId:', displayId || 'primary');
|
|
604
|
-
const opts =
|
|
605
|
-
displayId
|
|
606
|
-
|
|
607
|
-
|
|
704
|
+
const opts = {
|
|
705
|
+
...displayId ? {
|
|
706
|
+
displayId
|
|
707
|
+
} : {},
|
|
708
|
+
...void 0 !== headless ? {
|
|
709
|
+
headless
|
|
710
|
+
} : {}
|
|
711
|
+
};
|
|
712
|
+
const agent = await agentFromComputer(Object.keys(opts).length > 0 ? opts : void 0);
|
|
608
713
|
this.agent = agent;
|
|
609
714
|
return agent;
|
|
610
715
|
}
|
|
@@ -614,10 +719,11 @@ class ComputerMidsceneTools extends mcp_namespaceObject.BaseMidsceneTools {
|
|
|
614
719
|
name: 'computer_connect',
|
|
615
720
|
description: 'Connect to computer desktop. Provide displayId to connect to a specific display (use computer_list_displays to get available IDs). If not provided, uses the primary display.',
|
|
616
721
|
schema: {
|
|
617
|
-
displayId: core_namespaceObject.z.string().optional().describe('Display ID (from computer_list_displays)')
|
|
722
|
+
displayId: core_namespaceObject.z.string().optional().describe('Display ID (from computer_list_displays)'),
|
|
723
|
+
headless: core_namespaceObject.z.boolean().optional().describe('Start virtual display via Xvfb (Linux only)')
|
|
618
724
|
},
|
|
619
|
-
handler: async ({ displayId })=>{
|
|
620
|
-
const agent = await this.ensureAgent(displayId);
|
|
725
|
+
handler: async ({ displayId, headless })=>{
|
|
726
|
+
const agent = await this.ensureAgent(displayId, headless);
|
|
621
727
|
const screenshot = await agent.interface.screenshotBase64();
|
|
622
728
|
return {
|
|
623
729
|
content: [
|