@midscene/computer 1.12.7-beta-20260911063016.0 → 1.12.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/README.md +1 -49
- package/bin/darwin/rdp-helper +0 -0
- package/bin/linux/rdp-helper +0 -0
- package/dist/es/cli.mjs +239 -157
- package/dist/es/index.mjs +239 -157
- package/dist/lib/cli.js +239 -157
- package/dist/lib/index.js +239 -157
- package/dist/types/index.d.ts +20 -1
- package/native/rdp/CMakeLists.txt +22 -0
- package/native/rdp/include/rdp_helper_framebuffer.hpp +24 -0
- package/native/rdp/include/rdp_helper_session.hpp +7 -3
- package/native/rdp/src/png.cpp +5 -2
- package/native/rdp/src/session.cpp +60 -55
- package/native/rdp/tests/screenshot_test.cpp +365 -0
- package/native/rdp/tests/xrdp_desktop.py +135 -0
- package/package.json +3 -3
package/dist/lib/index.js
CHANGED
|
@@ -68,6 +68,73 @@ const img_namespaceObject = require("@midscene/shared/img");
|
|
|
68
68
|
const logger_namespaceObject = require("@midscene/shared/logger");
|
|
69
69
|
const external_screenshot_desktop_namespaceObject = require("screenshot-desktop");
|
|
70
70
|
var external_screenshot_desktop_default = /*#__PURE__*/ __webpack_require__.n(external_screenshot_desktop_namespaceObject);
|
|
71
|
+
const debugKeyboard = (0, logger_namespaceObject.getDebug)('computer:keyboard');
|
|
72
|
+
const APPLE_SCRIPT_KEY_CODES = {
|
|
73
|
+
return: 36,
|
|
74
|
+
enter: 36,
|
|
75
|
+
tab: 48,
|
|
76
|
+
space: 49,
|
|
77
|
+
backspace: 51,
|
|
78
|
+
delete: 51,
|
|
79
|
+
escape: 53,
|
|
80
|
+
forwarddelete: 117,
|
|
81
|
+
left: 123,
|
|
82
|
+
right: 124,
|
|
83
|
+
down: 125,
|
|
84
|
+
up: 126,
|
|
85
|
+
home: 115,
|
|
86
|
+
end: 119,
|
|
87
|
+
pageup: 116,
|
|
88
|
+
pagedown: 121,
|
|
89
|
+
f1: 122,
|
|
90
|
+
f2: 120,
|
|
91
|
+
f3: 99,
|
|
92
|
+
f4: 118,
|
|
93
|
+
f5: 96,
|
|
94
|
+
f6: 97,
|
|
95
|
+
f7: 98,
|
|
96
|
+
f8: 100,
|
|
97
|
+
f9: 101,
|
|
98
|
+
f10: 109,
|
|
99
|
+
f11: 103,
|
|
100
|
+
f12: 111
|
|
101
|
+
};
|
|
102
|
+
const APPLE_SCRIPT_MODIFIER_KEYS = {
|
|
103
|
+
command: 'command',
|
|
104
|
+
cmd: 'command',
|
|
105
|
+
control: 'control',
|
|
106
|
+
ctrl: 'control',
|
|
107
|
+
shift: 'shift',
|
|
108
|
+
alt: 'option',
|
|
109
|
+
option: 'option',
|
|
110
|
+
meta: 'command'
|
|
111
|
+
};
|
|
112
|
+
function buildKeyCommand(key) {
|
|
113
|
+
const keyCode = APPLE_SCRIPT_KEY_CODES[key.toLowerCase()];
|
|
114
|
+
if (void 0 !== keyCode) return `key code ${keyCode}`;
|
|
115
|
+
const escapedKey = key.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
116
|
+
return `keystroke "${escapedKey}"`;
|
|
117
|
+
}
|
|
118
|
+
function resolveModifierKeys(modifiers) {
|
|
119
|
+
return modifiers.map((modifier)=>APPLE_SCRIPT_MODIFIER_KEYS[modifier.toLowerCase()]).filter((modifier)=>void 0 !== modifier);
|
|
120
|
+
}
|
|
121
|
+
function buildAppleScriptKeyPress(key, modifiers = []) {
|
|
122
|
+
const modifierKeys = resolveModifierKeys(modifiers);
|
|
123
|
+
const modifierClause = modifierKeys.length ? ` using {${modifierKeys.map((modifier)=>`${modifier} down`).join(', ')}}` : '';
|
|
124
|
+
return `tell application "System Events" to ${buildKeyCommand(key)}${modifierClause}`;
|
|
125
|
+
}
|
|
126
|
+
function sendKeyViaAppleScript(key, modifiers = []) {
|
|
127
|
+
const script = buildAppleScriptKeyPress(key, modifiers);
|
|
128
|
+
debugKeyboard('sendKeyViaAppleScript', {
|
|
129
|
+
key,
|
|
130
|
+
modifiers,
|
|
131
|
+
script
|
|
132
|
+
});
|
|
133
|
+
(0, external_node_child_process_namespaceObject.execFileSync)("osascript", [
|
|
134
|
+
'-e',
|
|
135
|
+
script
|
|
136
|
+
]);
|
|
137
|
+
}
|
|
71
138
|
const external_node_assert_namespaceObject = require("node:assert");
|
|
72
139
|
var external_node_assert_default = /*#__PURE__*/ __webpack_require__.n(external_node_assert_namespaceObject);
|
|
73
140
|
function _define_property(obj, key, value) {
|
|
@@ -142,6 +209,41 @@ class ComputerInputDriver {
|
|
|
142
209
|
if (void 0 !== modifiers) lib.keyTap(key, modifiers);
|
|
143
210
|
else lib.keyTap(key);
|
|
144
211
|
}
|
|
212
|
+
keyToggle(key, state, modifiers) {
|
|
213
|
+
const lib = this.getLibnutOrThrow('keyToggle');
|
|
214
|
+
if (void 0 !== modifiers) lib.keyToggle(key, state, modifiers);
|
|
215
|
+
else lib.keyToggle(key, state);
|
|
216
|
+
}
|
|
217
|
+
async keyTapWithModifierDelay(key, modifiers, delayMs) {
|
|
218
|
+
const uniqueModifiers = [
|
|
219
|
+
...new Set(modifiers)
|
|
220
|
+
];
|
|
221
|
+
if (0 === uniqueModifiers.length) return void this.keyTap(key);
|
|
222
|
+
if (0 === delayMs) return void this.keyTap(key, uniqueModifiers);
|
|
223
|
+
const pressedModifiers = [];
|
|
224
|
+
let failure;
|
|
225
|
+
try {
|
|
226
|
+
for (const modifier of uniqueModifiers){
|
|
227
|
+
this.keyToggle(modifier, 'down');
|
|
228
|
+
pressedModifiers.push(modifier);
|
|
229
|
+
await this.delay(delayMs);
|
|
230
|
+
}
|
|
231
|
+
this.keyTap(key);
|
|
232
|
+
await this.delay(delayMs);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
failure = error;
|
|
235
|
+
}
|
|
236
|
+
for (const modifier of pressedModifiers.reverse()){
|
|
237
|
+
const releaseFailure = this.tryReleaseKey(modifier);
|
|
238
|
+
if (void 0 === failure && void 0 !== releaseFailure) failure = releaseFailure;
|
|
239
|
+
if (void 0 === failure) try {
|
|
240
|
+
await this.delay(delayMs);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
failure = error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (void 0 !== failure) throw failure;
|
|
246
|
+
}
|
|
145
247
|
typeString(text) {
|
|
146
248
|
this.getLibnutOrThrow('typeString').typeString(text);
|
|
147
249
|
}
|
|
@@ -214,6 +316,20 @@ class ComputerInputDriver {
|
|
|
214
316
|
this.options.debug(`Failed to release mouse button ${button}: ${error}`);
|
|
215
317
|
}
|
|
216
318
|
}
|
|
319
|
+
tryReleaseKey(key) {
|
|
320
|
+
try {
|
|
321
|
+
const libnut = this.options.getLibnut();
|
|
322
|
+
external_node_assert_default()(libnut, 'libnut not initialized');
|
|
323
|
+
libnut.keyToggle(key, 'up');
|
|
324
|
+
return;
|
|
325
|
+
} catch (error) {
|
|
326
|
+
const releaseError = new Error(`Failed to release modifier key "${key}"`, {
|
|
327
|
+
cause: error
|
|
328
|
+
});
|
|
329
|
+
this.options.debug(`${releaseError.message}: ${error}`);
|
|
330
|
+
return releaseError;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
217
333
|
rejectPendingInputDelays() {
|
|
218
334
|
const error = this.createDestroyedError('in-flight input');
|
|
219
335
|
for (const waitRef of this.pendingInputDelayWaits){
|
|
@@ -231,6 +347,96 @@ class ComputerInputDriver {
|
|
|
231
347
|
this.pendingInputDelayWaits = new Set();
|
|
232
348
|
}
|
|
233
349
|
}
|
|
350
|
+
const US_SHIFTED_CHARACTER_KEYS = new Map([
|
|
351
|
+
[
|
|
352
|
+
'~',
|
|
353
|
+
'`'
|
|
354
|
+
],
|
|
355
|
+
[
|
|
356
|
+
'!',
|
|
357
|
+
'1'
|
|
358
|
+
],
|
|
359
|
+
[
|
|
360
|
+
'@',
|
|
361
|
+
'2'
|
|
362
|
+
],
|
|
363
|
+
[
|
|
364
|
+
'#',
|
|
365
|
+
'3'
|
|
366
|
+
],
|
|
367
|
+
[
|
|
368
|
+
'$',
|
|
369
|
+
'4'
|
|
370
|
+
],
|
|
371
|
+
[
|
|
372
|
+
'%',
|
|
373
|
+
'5'
|
|
374
|
+
],
|
|
375
|
+
[
|
|
376
|
+
'^',
|
|
377
|
+
'6'
|
|
378
|
+
],
|
|
379
|
+
[
|
|
380
|
+
'&',
|
|
381
|
+
'7'
|
|
382
|
+
],
|
|
383
|
+
[
|
|
384
|
+
'*',
|
|
385
|
+
'8'
|
|
386
|
+
],
|
|
387
|
+
[
|
|
388
|
+
'(',
|
|
389
|
+
'9'
|
|
390
|
+
],
|
|
391
|
+
[
|
|
392
|
+
')',
|
|
393
|
+
'0'
|
|
394
|
+
],
|
|
395
|
+
[
|
|
396
|
+
'_',
|
|
397
|
+
'-'
|
|
398
|
+
],
|
|
399
|
+
[
|
|
400
|
+
'+',
|
|
401
|
+
'='
|
|
402
|
+
],
|
|
403
|
+
[
|
|
404
|
+
'{',
|
|
405
|
+
'['
|
|
406
|
+
],
|
|
407
|
+
[
|
|
408
|
+
'}',
|
|
409
|
+
']'
|
|
410
|
+
],
|
|
411
|
+
[
|
|
412
|
+
'|',
|
|
413
|
+
'\\'
|
|
414
|
+
],
|
|
415
|
+
[
|
|
416
|
+
':',
|
|
417
|
+
';'
|
|
418
|
+
],
|
|
419
|
+
[
|
|
420
|
+
'"',
|
|
421
|
+
"'"
|
|
422
|
+
],
|
|
423
|
+
[
|
|
424
|
+
'<',
|
|
425
|
+
','
|
|
426
|
+
],
|
|
427
|
+
[
|
|
428
|
+
'>',
|
|
429
|
+
'.'
|
|
430
|
+
],
|
|
431
|
+
[
|
|
432
|
+
'?',
|
|
433
|
+
'/'
|
|
434
|
+
]
|
|
435
|
+
]);
|
|
436
|
+
function resolveShiftedKey(character, keyboardLayout) {
|
|
437
|
+
if (/^[A-Z]$/.test(character)) return character.toLowerCase();
|
|
438
|
+
return 'en-US' === keyboardLayout ? US_SHIFTED_CHARACTER_KEYS.get(character) : void 0;
|
|
439
|
+
}
|
|
234
440
|
const POWERSHELL_TIMEOUT_MS = 15000;
|
|
235
441
|
const POWERSHELL_MAX_BUFFER = 67108864;
|
|
236
442
|
const WINDOWS_PHYSICAL_PIXEL_POWERSHELL_PREAMBLE = `
|
|
@@ -595,92 +801,6 @@ const LIBNUT_FALLBACK_PIXELS_PER_DETENT = 100;
|
|
|
595
801
|
const LIBNUT_FALLBACK_TICK_DELAY_MS = 30;
|
|
596
802
|
const LIBNUT_FALLBACK_MAX_DETENTS = 200;
|
|
597
803
|
const LIBNUT_FALLBACK_DETENT_AMOUNT = 'win32' === process.platform ? 120 : 1;
|
|
598
|
-
const LINUX_SHIFTED_CHARACTER_KEYS = new Map([
|
|
599
|
-
[
|
|
600
|
-
'~',
|
|
601
|
-
'`'
|
|
602
|
-
],
|
|
603
|
-
[
|
|
604
|
-
'!',
|
|
605
|
-
'1'
|
|
606
|
-
],
|
|
607
|
-
[
|
|
608
|
-
'@',
|
|
609
|
-
'2'
|
|
610
|
-
],
|
|
611
|
-
[
|
|
612
|
-
'#',
|
|
613
|
-
'3'
|
|
614
|
-
],
|
|
615
|
-
[
|
|
616
|
-
'$',
|
|
617
|
-
'4'
|
|
618
|
-
],
|
|
619
|
-
[
|
|
620
|
-
'%',
|
|
621
|
-
'5'
|
|
622
|
-
],
|
|
623
|
-
[
|
|
624
|
-
'^',
|
|
625
|
-
'6'
|
|
626
|
-
],
|
|
627
|
-
[
|
|
628
|
-
'&',
|
|
629
|
-
'7'
|
|
630
|
-
],
|
|
631
|
-
[
|
|
632
|
-
'*',
|
|
633
|
-
'8'
|
|
634
|
-
],
|
|
635
|
-
[
|
|
636
|
-
'(',
|
|
637
|
-
'9'
|
|
638
|
-
],
|
|
639
|
-
[
|
|
640
|
-
')',
|
|
641
|
-
'0'
|
|
642
|
-
],
|
|
643
|
-
[
|
|
644
|
-
'_',
|
|
645
|
-
'-'
|
|
646
|
-
],
|
|
647
|
-
[
|
|
648
|
-
'+',
|
|
649
|
-
'='
|
|
650
|
-
],
|
|
651
|
-
[
|
|
652
|
-
'{',
|
|
653
|
-
'['
|
|
654
|
-
],
|
|
655
|
-
[
|
|
656
|
-
'}',
|
|
657
|
-
']'
|
|
658
|
-
],
|
|
659
|
-
[
|
|
660
|
-
'|',
|
|
661
|
-
'\\'
|
|
662
|
-
],
|
|
663
|
-
[
|
|
664
|
-
':',
|
|
665
|
-
';'
|
|
666
|
-
],
|
|
667
|
-
[
|
|
668
|
-
'"',
|
|
669
|
-
"'"
|
|
670
|
-
],
|
|
671
|
-
[
|
|
672
|
-
'<',
|
|
673
|
-
','
|
|
674
|
-
],
|
|
675
|
-
[
|
|
676
|
-
'>',
|
|
677
|
-
'.'
|
|
678
|
-
],
|
|
679
|
-
[
|
|
680
|
-
'?',
|
|
681
|
-
'/'
|
|
682
|
-
]
|
|
683
|
-
]);
|
|
684
804
|
const LIBNUT_FALLBACK_EDGE_DETENTS = Math.min(LIBNUT_FALLBACK_MAX_DETENTS, Math.max(1, Math.ceil(EDGE_SCROLL_TOTAL_PX / LIBNUT_FALLBACK_PIXELS_PER_DETENT)));
|
|
685
805
|
const DEFAULT_SCROLL_VIEWPORT_RATIO = 0.7;
|
|
686
806
|
const EDGE_SCROLL_SPEC = {
|
|
@@ -717,67 +837,6 @@ const EDGE_SCROLL_SPEC = {
|
|
|
717
837
|
]
|
|
718
838
|
}
|
|
719
839
|
};
|
|
720
|
-
const APPLESCRIPT_KEY_CODE_MAP = {
|
|
721
|
-
return: 36,
|
|
722
|
-
enter: 36,
|
|
723
|
-
tab: 48,
|
|
724
|
-
space: 49,
|
|
725
|
-
backspace: 51,
|
|
726
|
-
delete: 51,
|
|
727
|
-
escape: 53,
|
|
728
|
-
forwarddelete: 117,
|
|
729
|
-
left: 123,
|
|
730
|
-
right: 124,
|
|
731
|
-
down: 125,
|
|
732
|
-
up: 126,
|
|
733
|
-
home: 115,
|
|
734
|
-
end: 119,
|
|
735
|
-
pageup: 116,
|
|
736
|
-
pagedown: 121,
|
|
737
|
-
f1: 122,
|
|
738
|
-
f2: 120,
|
|
739
|
-
f3: 99,
|
|
740
|
-
f4: 118,
|
|
741
|
-
f5: 96,
|
|
742
|
-
f6: 97,
|
|
743
|
-
f7: 98,
|
|
744
|
-
f8: 100,
|
|
745
|
-
f9: 101,
|
|
746
|
-
f10: 109,
|
|
747
|
-
f11: 103,
|
|
748
|
-
f12: 111
|
|
749
|
-
};
|
|
750
|
-
const APPLESCRIPT_MODIFIER_MAP = {
|
|
751
|
-
command: 'command down',
|
|
752
|
-
cmd: 'command down',
|
|
753
|
-
control: 'control down',
|
|
754
|
-
ctrl: 'control down',
|
|
755
|
-
shift: 'shift down',
|
|
756
|
-
alt: 'option down',
|
|
757
|
-
option: 'option down',
|
|
758
|
-
meta: 'command down'
|
|
759
|
-
};
|
|
760
|
-
function sendKeyViaAppleScript(key, modifiers = []) {
|
|
761
|
-
const lowerKey = key.toLowerCase();
|
|
762
|
-
const keyCode = APPLESCRIPT_KEY_CODE_MAP[lowerKey];
|
|
763
|
-
const modifierParts = modifiers.map((m)=>APPLESCRIPT_MODIFIER_MAP[m.toLowerCase()]).filter(Boolean);
|
|
764
|
-
const modifierStr = modifierParts.length > 0 ? ` using {${modifierParts.join(', ')}}` : '';
|
|
765
|
-
let script;
|
|
766
|
-
if (void 0 !== keyCode) script = `tell application "System Events" to key code ${keyCode}${modifierStr}`;
|
|
767
|
-
else {
|
|
768
|
-
const escapedKey = key.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
769
|
-
script = `tell application "System Events" to keystroke "${escapedKey}"${modifierStr}`;
|
|
770
|
-
}
|
|
771
|
-
debugDevice('sendKeyViaAppleScript', {
|
|
772
|
-
key,
|
|
773
|
-
modifiers,
|
|
774
|
-
script
|
|
775
|
-
});
|
|
776
|
-
(0, external_node_child_process_namespaceObject.execFileSync)("osascript", [
|
|
777
|
-
'-e',
|
|
778
|
-
script
|
|
779
|
-
]);
|
|
780
|
-
}
|
|
781
840
|
function escapePowershellSingleQuoted(value) {
|
|
782
841
|
return value.replace(/'/g, "''");
|
|
783
842
|
}
|
|
@@ -1205,7 +1264,7 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
|
|
|
1205
1264
|
}
|
|
1206
1265
|
async healthCheck(displays) {
|
|
1207
1266
|
console.log('[HealthCheck] Starting health check...');
|
|
1208
|
-
console.log("[HealthCheck] @midscene/computer v1.12.7
|
|
1267
|
+
console.log("[HealthCheck] @midscene/computer v1.12.7");
|
|
1209
1268
|
console.log('[HealthCheck] Taking screenshot...');
|
|
1210
1269
|
const screenshotTimeout = 15000;
|
|
1211
1270
|
let timeoutId;
|
|
@@ -1382,9 +1441,9 @@ $g.Dispose(); $bmp.Dispose(); $ms.Dispose()
|
|
|
1382
1441
|
]);
|
|
1383
1442
|
else {
|
|
1384
1443
|
const modifier = 'darwin' === process.platform ? 'command' : 'control';
|
|
1385
|
-
this.inputDriver.
|
|
1444
|
+
await this.inputDriver.keyTapWithModifierDelay('v', [
|
|
1386
1445
|
modifier
|
|
1387
|
-
]);
|
|
1446
|
+
], this.options?.keyboardModifierDelay ?? 0);
|
|
1388
1447
|
}
|
|
1389
1448
|
await this.inputDriver.delay(100);
|
|
1390
1449
|
} finally{
|
|
@@ -1399,12 +1458,16 @@ $g.Dispose(); $bmp.Dispose(); $ms.Dispose()
|
|
|
1399
1458
|
}
|
|
1400
1459
|
async typeStringWithDelay(text, keyboardTypeDelay) {
|
|
1401
1460
|
await (0, device_namespaceObject.sendTextSequentially)(text.replace(/\r\n?/g, '\n'), {
|
|
1402
|
-
sendCharacter: (character)=>{
|
|
1403
|
-
const linuxShiftedKey = 'linux' === process.platform ?
|
|
1461
|
+
sendCharacter: async (character)=>{
|
|
1462
|
+
const linuxShiftedKey = 'linux' === process.platform ? US_SHIFTED_CHARACTER_KEYS.get(character) : void 0;
|
|
1463
|
+
const pacedShiftedKey = !this.useAppleScript && (this.options?.keyboardModifierDelay ?? 0) > 0 ? resolveShiftedKey(character, this.options?.keyboardLayout) : void 0;
|
|
1404
1464
|
if ('\n' === character) this.inputDriver.sendKey('return');
|
|
1405
1465
|
else if ('\t' === character) this.inputDriver.sendKey('tab');
|
|
1406
1466
|
else if (' ' === character) this.inputDriver.sendKey('space');
|
|
1407
1467
|
else if (this.useAppleScript) this.inputDriver.sendKeyViaAppleScript(character);
|
|
1468
|
+
else if (void 0 !== pacedShiftedKey) await this.inputDriver.keyTapWithModifierDelay(pacedShiftedKey, [
|
|
1469
|
+
'shift'
|
|
1470
|
+
], this.options?.keyboardModifierDelay ?? 0);
|
|
1408
1471
|
else if (void 0 !== linuxShiftedKey) this.inputDriver.keyTap(linuxShiftedKey, [
|
|
1409
1472
|
'shift'
|
|
1410
1473
|
]);
|
|
@@ -1425,9 +1488,9 @@ $g.Dispose(); $bmp.Dispose(); $ms.Dispose()
|
|
|
1425
1488
|
return;
|
|
1426
1489
|
}
|
|
1427
1490
|
const modifier = 'darwin' === process.platform ? 'command' : 'control';
|
|
1428
|
-
this.inputDriver.
|
|
1491
|
+
await this.inputDriver.keyTapWithModifierDelay('a', [
|
|
1429
1492
|
modifier
|
|
1430
|
-
]);
|
|
1493
|
+
], this.options?.keyboardModifierDelay ?? 0);
|
|
1431
1494
|
await this.inputDriver.delay(50);
|
|
1432
1495
|
this.inputDriver.keyTap('backspace');
|
|
1433
1496
|
}
|
|
@@ -1441,6 +1504,7 @@ $g.Dispose(); $bmp.Dispose(); $ms.Dispose()
|
|
|
1441
1504
|
modifiers,
|
|
1442
1505
|
driver: this.useAppleScript ? "applescript" : 'libnut'
|
|
1443
1506
|
});
|
|
1507
|
+
if (!this.useAppleScript && modifiers.length > 0 && (this.options?.keyboardModifierDelay ?? 0) > 0) return void await this.inputDriver.keyTapWithModifierDelay(key, modifiers, this.options?.keyboardModifierDelay ?? 0);
|
|
1444
1508
|
this.inputDriver.sendKey(key, modifiers);
|
|
1445
1509
|
}
|
|
1446
1510
|
resolveUntargetedScrollPoint(screenSize) {
|
|
@@ -1594,7 +1658,7 @@ $g.Dispose(); $bmp.Dispose(); $ms.Dispose()
|
|
|
1594
1658
|
device_define_property(this, "inputDriver", new ComputerInputDriver({
|
|
1595
1659
|
getLibnut: ()=>device_libnut,
|
|
1596
1660
|
useAppleScript: ()=>this.useAppleScript,
|
|
1597
|
-
sendKeyViaAppleScript,
|
|
1661
|
+
sendKeyViaAppleScript: sendKeyViaAppleScript,
|
|
1598
1662
|
runPhasedScroll,
|
|
1599
1663
|
debug: (message)=>debugDevice(message)
|
|
1600
1664
|
}));
|
|
@@ -1720,6 +1784,8 @@ $g.Dispose(); $bmp.Dispose(); $ms.Dispose()
|
|
|
1720
1784
|
}
|
|
1721
1785
|
}
|
|
1722
1786
|
});
|
|
1787
|
+
if (options?.keyboardModifierDelay !== void 0 && (!Number.isFinite(options.keyboardModifierDelay) || options.keyboardModifierDelay < 0)) throw new Error('keyboardModifierDelay must be a finite non-negative number');
|
|
1788
|
+
if (options?.keyboardLayout !== void 0 && 'en-US' !== options.keyboardLayout) throw new Error('keyboardLayout must be "en-US" when specified');
|
|
1723
1789
|
this.options = options;
|
|
1724
1790
|
this.displayId = options?.displayId;
|
|
1725
1791
|
this.useAppleScript = 'darwin' === process.platform && options?.keyboardDriver !== 'libnut';
|
|
@@ -2467,6 +2533,8 @@ function createLocalComputerDevice(opts) {
|
|
|
2467
2533
|
keyboardTypeDelay: opts?.keyboardTypeDelay,
|
|
2468
2534
|
inputStrategy: opts?.inputStrategy,
|
|
2469
2535
|
keyboardDriver: opts?.keyboardDriver,
|
|
2536
|
+
keyboardModifierDelay: opts?.keyboardModifierDelay,
|
|
2537
|
+
keyboardLayout: opts?.keyboardLayout,
|
|
2470
2538
|
headless: opts?.headless,
|
|
2471
2539
|
xvfbResolution: opts?.xvfbResolution,
|
|
2472
2540
|
keepXvfbAliveUntilProcessExit: opts?.keepXvfbAliveUntilProcessExit
|
|
@@ -2527,6 +2595,10 @@ const computerInitArgShape = {
|
|
|
2527
2595
|
headless: core_namespaceObject.z.boolean().optional().describe('Start virtual display via Xvfb (Linux local mode only). Ignored when host is set.'),
|
|
2528
2596
|
keyboardTypeDelay: core_namespaceObject.z.number().finite().nonnegative().optional().describe('Finite non-negative delay in milliseconds between keystrokes. In "legacy" mode, positive values enable key-by-key input; zero or omitted uses clipboard input.'),
|
|
2529
2597
|
inputStrategy: core_namespaceObject.z["enum"](device_namespaceObject.inputStrategies).optional().describe('Text input strategy. "legacy" (default) preserves current Computer behavior, "sequential" sends one Unicode code point at a time, and "bulk" uses one backend input operation. "bulk" requires keyboardTypeDelay to be omitted or set to 0.'),
|
|
2598
|
+
keyboardModifierDelay: core_namespaceObject.z.number().finite().nonnegative().optional().describe('Finite non-negative delay in milliseconds after each modifier transition and the main key for local libnut keyboard events. Positive values pace shortcuts and the implicit Shift used by uppercase letters during sequential input. Layout-dependent punctuation also requires keyboardLayout. Ignored in RDP mode and by the macOS AppleScript driver.'),
|
|
2599
|
+
keyboardLayout: core_namespaceObject.z["enum"]([
|
|
2600
|
+
'en-US'
|
|
2601
|
+
]).optional().describe('Keyboard layout used to resolve layout-dependent shifted punctuation to base keys during sequential local libnut input. Currently only "en-US" is supported. Ignored in RDP mode and by the macOS AppleScript driver.'),
|
|
2530
2602
|
host: core_namespaceObject.z.string().optional().describe('RDP host (FQDN or IP). Set this to switch into RDP mode.'),
|
|
2531
2603
|
port: core_namespaceObject.z.number().optional().describe('RDP port (default 3389). Requires host.'),
|
|
2532
2604
|
username: core_namespaceObject.z.string().optional().describe('RDP username. Requires host.'),
|
|
@@ -2543,7 +2615,7 @@ const computerInitArgShape = {
|
|
|
2543
2615
|
function adaptComputerInitArgs(extracted) {
|
|
2544
2616
|
if (!extracted || 0 === Object.keys(extracted).length) return;
|
|
2545
2617
|
if (extracted.host) {
|
|
2546
|
-
const { displayId: _d, headless: _h, ...rdpFields } = extracted;
|
|
2618
|
+
const { displayId: _d, headless: _h, keyboardModifierDelay: _s, keyboardLayout: _l, ...rdpFields } = extracted;
|
|
2547
2619
|
const host = normalizeRdpHost(extracted.host);
|
|
2548
2620
|
return {
|
|
2549
2621
|
mode: 'rdp',
|
|
@@ -2557,6 +2629,8 @@ function adaptComputerInitArgs(extracted) {
|
|
|
2557
2629
|
headless: extracted.headless,
|
|
2558
2630
|
keyboardTypeDelay: extracted.keyboardTypeDelay,
|
|
2559
2631
|
inputStrategy: extracted.inputStrategy,
|
|
2632
|
+
keyboardModifierDelay: extracted.keyboardModifierDelay,
|
|
2633
|
+
keyboardLayout: extracted.keyboardLayout,
|
|
2560
2634
|
...(0, agent_behavior_init_args_namespaceObject.extractAgentBehaviorInitArgs)(extracted) ?? {}
|
|
2561
2635
|
};
|
|
2562
2636
|
}
|
|
@@ -2608,6 +2682,8 @@ class ComputerMidsceneTools extends base_tools_namespaceObject.BaseMidsceneTools
|
|
|
2608
2682
|
const headless = opts?.mode === 'local' ? opts.headless : void 0;
|
|
2609
2683
|
const keyboardTypeDelay = opts?.keyboardTypeDelay;
|
|
2610
2684
|
const inputStrategy = opts?.inputStrategy;
|
|
2685
|
+
const keyboardModifierDelay = opts?.mode === 'local' ? opts.keyboardModifierDelay : void 0;
|
|
2686
|
+
const keyboardLayout = opts?.mode === 'local' ? opts.keyboardLayout : void 0;
|
|
2611
2687
|
agent_tools_debug('Creating Computer agent with displayId:', displayId || 'primary');
|
|
2612
2688
|
const agentOpts = {
|
|
2613
2689
|
...displayId ? {
|
|
@@ -2622,6 +2698,12 @@ class ComputerMidsceneTools extends base_tools_namespaceObject.BaseMidsceneTools
|
|
|
2622
2698
|
...void 0 !== inputStrategy ? {
|
|
2623
2699
|
inputStrategy
|
|
2624
2700
|
} : {},
|
|
2701
|
+
...void 0 !== keyboardModifierDelay ? {
|
|
2702
|
+
keyboardModifierDelay
|
|
2703
|
+
} : {},
|
|
2704
|
+
...void 0 !== keyboardLayout ? {
|
|
2705
|
+
keyboardLayout
|
|
2706
|
+
} : {},
|
|
2625
2707
|
...this.options.keepXvfbAliveUntilProcessExit ? {
|
|
2626
2708
|
keepXvfbAliveUntilProcessExit: true
|
|
2627
2709
|
} : {},
|
|
@@ -2703,7 +2785,7 @@ class ComputerMidsceneTools extends base_tools_namespaceObject.BaseMidsceneTools
|
|
|
2703
2785
|
}
|
|
2704
2786
|
const env_namespaceObject = require("@midscene/shared/env");
|
|
2705
2787
|
function version() {
|
|
2706
|
-
const currentVersion = "1.12.7
|
|
2788
|
+
const currentVersion = "1.12.7";
|
|
2707
2789
|
console.log(`@midscene/computer v${currentVersion}`);
|
|
2708
2790
|
return currentVersion;
|
|
2709
2791
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -177,6 +177,25 @@ export declare interface ComputerDeviceOpt extends ComputerDeviceInputOpt {
|
|
|
177
177
|
* - 'libnut': Use libnut's keyTap (faster but may not work with some TUI apps)
|
|
178
178
|
*/
|
|
179
179
|
keyboardDriver?: 'applescript' | 'libnut';
|
|
180
|
+
/**
|
|
181
|
+
* Delay in milliseconds after each explicit modifier transition and the
|
|
182
|
+
* main key for local libnut keyboard events. A positive value changes
|
|
183
|
+
* modified shortcuts and shifted en-US text characters into separately
|
|
184
|
+
* observable modifier-down, main-key, and modifier-up phases. This can help
|
|
185
|
+
* foreground clients whose full-screen keyboard capture misses rapidly
|
|
186
|
+
* synthesized modifier state changes.
|
|
187
|
+
*
|
|
188
|
+
* Ignored by the macOS AppleScript driver and RDP mode.
|
|
189
|
+
* @default 0
|
|
190
|
+
*/
|
|
191
|
+
keyboardModifierDelay?: number;
|
|
192
|
+
/**
|
|
193
|
+
* Keyboard layout used to resolve layout-dependent shifted characters
|
|
194
|
+
* during sequential local libnut input. Uppercase Latin letters do not
|
|
195
|
+
* require this option. Other characters keep using the backend's existing
|
|
196
|
+
* input behavior unless a supported layout is declared.
|
|
197
|
+
*/
|
|
198
|
+
keyboardLayout?: 'en-US';
|
|
180
199
|
/**
|
|
181
200
|
* Headless mode via Xvfb (Linux only).
|
|
182
201
|
* - true: start Xvfb virtual display
|
|
@@ -210,7 +229,7 @@ export declare type ComputerInterface = ComputerDevice | RDPDevice;
|
|
|
210
229
|
/** Init args for the local desktop agent (macOS/Windows/Linux). */
|
|
211
230
|
declare type ComputerLocalInitArgs = {
|
|
212
231
|
mode: 'local';
|
|
213
|
-
} & Pick<ComputerDeviceOpt, 'displayId' | 'headless'> & Pick<ComputerDeviceOpt, 'inputStrategy' | 'keyboardTypeDelay'> & AgentBehaviorInitArgs;
|
|
232
|
+
} & Pick<ComputerDeviceOpt, 'displayId' | 'headless'> & Pick<ComputerDeviceOpt, 'inputStrategy' | 'keyboardTypeDelay' | 'keyboardModifierDelay' | 'keyboardLayout'> & AgentBehaviorInitArgs;
|
|
214
233
|
|
|
215
234
|
/**
|
|
216
235
|
* Computer-specific tools manager
|
|
@@ -92,6 +92,28 @@ if(BUILD_TESTING)
|
|
|
92
92
|
NAME rdp-session-policy-test
|
|
93
93
|
COMMAND rdp-session-policy-test
|
|
94
94
|
)
|
|
95
|
+
|
|
96
|
+
add_executable(
|
|
97
|
+
rdp-screenshot-test
|
|
98
|
+
tests/screenshot_test.cpp
|
|
99
|
+
src/png.cpp
|
|
100
|
+
src/session.cpp
|
|
101
|
+
)
|
|
102
|
+
target_include_directories(
|
|
103
|
+
rdp-screenshot-test PRIVATE
|
|
104
|
+
${CMAKE_CURRENT_SOURCE_DIR}/include
|
|
105
|
+
${FREERDP_INCLUDE_DIRS}
|
|
106
|
+
)
|
|
107
|
+
target_link_libraries(
|
|
108
|
+
rdp-screenshot-test PRIVATE
|
|
109
|
+
${FREERDP_LIBRARIES}
|
|
110
|
+
Threads::Threads
|
|
111
|
+
)
|
|
112
|
+
if(APPLE)
|
|
113
|
+
target_compile_definitions(rdp-screenshot-test PRIVATE _DARWIN_C_SOURCE)
|
|
114
|
+
endif()
|
|
115
|
+
add_test(NAME rdp-screenshot-test COMMAND rdp-screenshot-test)
|
|
116
|
+
set_tests_properties(rdp-screenshot-test PROPERTIES TIMEOUT 30)
|
|
95
117
|
endif()
|
|
96
118
|
|
|
97
119
|
if(APPLE)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <algorithm>
|
|
4
|
+
#include <chrono>
|
|
5
|
+
|
|
6
|
+
#include "rdp_helper_session.hpp"
|
|
7
|
+
|
|
8
|
+
namespace midscene::rdp {
|
|
9
|
+
|
|
10
|
+
// FreeRDP embeds its context first; shared by the callback and native tests.
|
|
11
|
+
struct MidsceneRdpContext {
|
|
12
|
+
rdpContext context;
|
|
13
|
+
FreeRdpSessionTransport* owner = nullptr;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Compute the next initial-screenshot wakeup without consulting wall time.
|
|
17
|
+
inline std::chrono::steady_clock::time_point ScreenshotWakeAt(
|
|
18
|
+
std::chrono::steady_clock::time_point started,
|
|
19
|
+
std::chrono::steady_clock::time_point last_update) {
|
|
20
|
+
return std::min(std::max(started, last_update) + std::chrono::milliseconds(300),
|
|
21
|
+
started + std::chrono::seconds(3));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
} // namespace midscene::rdp
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
3
|
#include <atomic>
|
|
4
|
+
#include <chrono>
|
|
4
5
|
#include <condition_variable>
|
|
5
6
|
#include <cstdint>
|
|
6
7
|
#include <mutex>
|
|
@@ -76,7 +77,7 @@ class FreeRdpSessionTransport final : public SessionTransport {
|
|
|
76
77
|
void HookEndPaint(rdpUpdate* update);
|
|
77
78
|
// Records that at least one paint has reached the local framebuffer and
|
|
78
79
|
// wakes Connect().
|
|
79
|
-
void MarkFramePainted(
|
|
80
|
+
void MarkFramePainted();
|
|
80
81
|
// Records a framebuffer update even when it is too blank or uniform to count
|
|
81
82
|
// as the first informative frame.
|
|
82
83
|
void MarkFramebufferUpdated();
|
|
@@ -90,6 +91,7 @@ class FreeRdpSessionTransport final : public SessionTransport {
|
|
|
90
91
|
|
|
91
92
|
private:
|
|
92
93
|
friend struct MidsceneRdpContext;
|
|
94
|
+
friend struct RdpScreenshotTestPeer;
|
|
93
95
|
|
|
94
96
|
freerdp* instance_ = nullptr;
|
|
95
97
|
std::thread event_thread_;
|
|
@@ -110,8 +112,10 @@ class FreeRdpSessionTransport final : public SessionTransport {
|
|
|
110
112
|
std::atomic<bool> session_active_{false};
|
|
111
113
|
std::condition_variable frame_cv_;
|
|
112
114
|
std::mutex frame_mutex_;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
// Protected by frame_mutex_; use a monotonic clock for screenshot settling.
|
|
116
|
+
std::chrono::steady_clock::time_point last_frame_update_{};
|
|
117
|
+
// Protected by mutex_; reset for each new connection.
|
|
118
|
+
bool first_screenshot_pending_ = true;
|
|
115
119
|
pEndPaint original_end_paint_ = nullptr;
|
|
116
120
|
uint16_t mouse_x_ = 0;
|
|
117
121
|
uint16_t mouse_y_ = 0;
|
package/native/rdp/src/png.cpp
CHANGED
|
@@ -81,12 +81,15 @@ std::vector<uint8_t> BuildImageData(const RawFrame& frame) {
|
|
|
81
81
|
const uint8_t blue = frame.bgra[pixel_offset];
|
|
82
82
|
const uint8_t green = frame.bgra[pixel_offset + 1];
|
|
83
83
|
const uint8_t red = frame.bgra[pixel_offset + 2];
|
|
84
|
-
const uint8_t alpha = frame.bgra[pixel_offset + 3];
|
|
85
84
|
|
|
86
85
|
image_data.push_back(red);
|
|
87
86
|
image_data.push_back(green);
|
|
88
87
|
image_data.push_back(blue);
|
|
89
|
-
|
|
88
|
+
// FreeRDP's PIXEL_FORMAT_BGRA32 is an opaque BGRX desktop surface. The
|
|
89
|
+
// fourth byte is padding and may be zero, so treating it as alpha makes
|
|
90
|
+
// otherwise valid screenshots transparent and turns them black during
|
|
91
|
+
// PNG-to-JPEG conversion.
|
|
92
|
+
image_data.push_back(0xFF);
|
|
90
93
|
}
|
|
91
94
|
}
|
|
92
95
|
|