@midscene/computer 1.12.2 → 1.12.3-beta-20260828110230.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lib/index.js CHANGED
@@ -80,6 +80,7 @@ function _define_property(obj, key, value) {
80
80
  else obj[key] = value;
81
81
  return obj;
82
82
  }
83
+ const MOUSE_COORDINATE_TOLERANCE_PX = 5;
83
84
  class ComputerInputDriver {
84
85
  destroy() {
85
86
  if (this.destroyed) return;
@@ -95,6 +96,15 @@ class ComputerInputDriver {
95
96
  moveMouse(x, y) {
96
97
  this.getLibnutOrThrow('moveMouse').moveMouse(x, y);
97
98
  }
99
+ assertMousePosition(targetX, targetY, context) {
100
+ const current = this.getMousePos();
101
+ const drift = {
102
+ x: current.x - targetX,
103
+ y: current.y - targetY
104
+ };
105
+ if (Math.abs(drift.x) > MOUSE_COORDINATE_TOLERANCE_PX || Math.abs(drift.y) > MOUSE_COORDINATE_TOLERANCE_PX) throw new Error(`${context}: expected (${targetX}, ${targetY}), got (${current.x}, ${current.y}), drift=(${drift.x}, ${drift.y})`);
106
+ return drift;
107
+ }
98
108
  focusActiveWindow() {
99
109
  const lib = this.getLibnutOrThrow('focusActiveWindow');
100
110
  if ('function' != typeof lib.getActiveWindow || 'function' != typeof lib.focusWindow) return false;
@@ -235,6 +245,122 @@ class ComputerInputDriver {
235
245
  this.pendingInputDelayWaits = new Set();
236
246
  }
237
247
  }
248
+ const WINDOWS_PHYSICAL_PIXEL_POWERSHELL_PREAMBLE = `
249
+ $midsceneAssemblyName = New-Object System.Reflection.AssemblyName('MidsceneDpiNative')
250
+ $midsceneAssembly = [System.AppDomain]::CurrentDomain.DefineDynamicAssembly(
251
+ $midsceneAssemblyName,
252
+ [System.Reflection.Emit.AssemblyBuilderAccess]::Run
253
+ )
254
+ $midsceneModule = $midsceneAssembly.DefineDynamicModule('MidsceneDpiNativeModule')
255
+ $midsceneType = $midsceneModule.DefineType(
256
+ 'MidsceneDpiNative.User32',
257
+ [System.Reflection.TypeAttributes]'Public, Class, Sealed, Abstract'
258
+ )
259
+ $midsceneMethodAttributes =
260
+ [System.Reflection.MethodAttributes]::Public -bor
261
+ [System.Reflection.MethodAttributes]::Static -bor
262
+ [System.Reflection.MethodAttributes]::PinvokeImpl
263
+ $midsceneSetDpiMethod = $midsceneType.DefinePInvokeMethod(
264
+ 'SetThreadDpiAwarenessContext',
265
+ 'user32.dll',
266
+ $midsceneMethodAttributes,
267
+ [System.Reflection.CallingConventions]::Standard,
268
+ [System.IntPtr],
269
+ [System.Type[]]@([System.IntPtr]),
270
+ [System.Runtime.InteropServices.CallingConvention]::Winapi,
271
+ [System.Runtime.InteropServices.CharSet]::None
272
+ )
273
+ $midsceneSetDpiMethod.SetImplementationFlags(
274
+ $midsceneSetDpiMethod.GetMethodImplementationFlags() -bor
275
+ [System.Reflection.MethodImplAttributes]::PreserveSig
276
+ )
277
+ $midsceneNativeMethods = $midsceneType.CreateType()
278
+ $midscenePerMonitorV2 = [System.IntPtr](-4)
279
+ $midscenePreviousDpiContext =
280
+ $midsceneNativeMethods::SetThreadDpiAwarenessContext($midscenePerMonitorV2)
281
+ if ($midscenePreviousDpiContext -eq [System.IntPtr]::Zero) {
282
+ throw 'Unable to enter the Per-Monitor V2 DPI awareness context.'
283
+ }
284
+ `.trim();
285
+ function windows_pointer_define_property(obj, key, value) {
286
+ if (key in obj) Object.defineProperty(obj, key, {
287
+ value: value,
288
+ enumerable: true,
289
+ configurable: true,
290
+ writable: true
291
+ });
292
+ else obj[key] = value;
293
+ return obj;
294
+ }
295
+ const WINDOWS_POINTER_TOLERANCE_PX = 5;
296
+ function assertFinitePoint(point, context) {
297
+ if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) throw new Error(`${context} must contain finite coordinates, got (${point.x}, ${point.y})`);
298
+ }
299
+ function parseWindowsPointerPosition(output, context) {
300
+ const match = output.trim().match(/^(-?\d+),(-?\d+)$/);
301
+ if (!match) throw new Error(`${context} returned an invalid cursor position: ${JSON.stringify(output.trim())}`);
302
+ return {
303
+ x: Number(match[1]),
304
+ y: Number(match[2])
305
+ };
306
+ }
307
+ function windowsPointerPositionScript() {
308
+ return `
309
+ ${WINDOWS_PHYSICAL_PIXEL_POWERSHELL_PREAMBLE}
310
+ Add-Type -AssemblyName System.Windows.Forms, System.Drawing
311
+ $position = [System.Windows.Forms.Cursor]::Position
312
+ [Console]::Out.Write(('{0},{1}' -f $position.X, $position.Y))
313
+ `.trim();
314
+ }
315
+ function windowsPointerMoveScript(point, options) {
316
+ assertFinitePoint(point, 'Windows pointer target');
317
+ if (options?.smoothSteps !== void 0 && !Number.isFinite(options.smoothSteps) || options?.smoothDelayMs !== void 0 && !Number.isFinite(options.smoothDelayMs)) throw new Error('Windows pointer smoothing options must be finite numbers');
318
+ const targetX = Math.round(point.x);
319
+ const targetY = Math.round(point.y);
320
+ const smoothSteps = Math.max(1, Math.round(options?.smoothSteps ?? 1));
321
+ const smoothDelayMs = Math.max(0, Math.round(options?.smoothDelayMs ?? 0));
322
+ return `
323
+ ${WINDOWS_PHYSICAL_PIXEL_POWERSHELL_PREAMBLE}
324
+ Add-Type -AssemblyName System.Windows.Forms, System.Drawing
325
+ $targetX = ${targetX}
326
+ $targetY = ${targetY}
327
+ $smoothSteps = ${smoothSteps}
328
+ $smoothDelayMs = ${smoothDelayMs}
329
+ $start = [System.Windows.Forms.Cursor]::Position
330
+ for ($step = 1; $step -le $smoothSteps; $step += 1) {
331
+ $x = [int][Math]::Round($start.X + (($targetX - $start.X) * $step / [double]$smoothSteps))
332
+ $y = [int][Math]::Round($start.Y + (($targetY - $start.Y) * $step / [double]$smoothSteps))
333
+ [System.Windows.Forms.Cursor]::Position = [System.Drawing.Point]::new($x, $y)
334
+ if ($smoothDelayMs -gt 0 -and $step -lt $smoothSteps) {
335
+ Start-Sleep -Milliseconds $smoothDelayMs
336
+ }
337
+ }
338
+ [System.Windows.Forms.Cursor]::Position = [System.Drawing.Point]::new($targetX, $targetY)
339
+ $position = [System.Windows.Forms.Cursor]::Position
340
+ [Console]::Out.Write(('{0},{1}' -f $position.X, $position.Y))
341
+ `.trim();
342
+ }
343
+ class WindowsPointerDriver {
344
+ getPosition() {
345
+ return parseWindowsPointerPosition(this.options.runPowershell(windowsPointerPositionScript()), 'Windows pointer query');
346
+ }
347
+ moveTo(point, options) {
348
+ return parseWindowsPointerPosition(this.options.runPowershell(windowsPointerMoveScript(point, options)), 'Windows pointer move');
349
+ }
350
+ constructor(options){
351
+ windows_pointer_define_property(this, "options", void 0);
352
+ this.options = options;
353
+ }
354
+ }
355
+ function windowsPointerDrift(expected, actual) {
356
+ return {
357
+ x: actual.x - expected.x,
358
+ y: actual.y - expected.y
359
+ };
360
+ }
361
+ function windowsPointerIsWithinTolerance(drift) {
362
+ return Math.abs(drift.x) <= WINDOWS_POINTER_TOLERANCE_PX && Math.abs(drift.y) <= WINDOWS_POINTER_TOLERANCE_PX;
363
+ }
238
364
  const interrupt_namespaceObject = require("@midscene/shared/cli/interrupt");
239
365
  const debugXvfb = (0, logger_namespaceObject.getDebug)('computer:xvfb');
240
366
  const xvfbCleanupMonitorScript = String.raw`
@@ -513,19 +639,34 @@ function runPowershell(script) {
513
639
  windowsHide: true
514
640
  });
515
641
  }
516
- function listWindowsDisplays() {
642
+ function readWindowsDisplayGeometries() {
517
643
  const script = `
644
+ ${WINDOWS_PHYSICAL_PIXEL_POWERSHELL_PREAMBLE}
518
645
  Add-Type -AssemblyName System.Windows.Forms
519
646
  $s = [System.Windows.Forms.Screen]::AllScreens | ForEach-Object {
520
- [PSCustomObject]@{ id = $_.DeviceName; name = $_.DeviceName; primary = $_.Primary }
647
+ $b = $_.Bounds
648
+ [PSCustomObject]@{
649
+ id = $_.DeviceName
650
+ name = $_.DeviceName
651
+ primary = $_.Primary
652
+ bounds = [PSCustomObject]@{ x = $b.X; y = $b.Y; width = $b.Width; height = $b.Height }
653
+ }
521
654
  }
522
655
  ConvertTo-Json @($s) -Compress
523
656
  `.trim();
524
- const parsed = JSON.parse(runPowershell(script).trim());
525
- return parsed.map((d)=>({
526
- id: String(d.id),
527
- name: d.name || String(d.id),
528
- primary: d.primary || false
657
+ const output = runPowershell(script).trim();
658
+ if (!output) throw new Error('Windows display enumeration returned no data');
659
+ const parsed = JSON.parse(output);
660
+ if (!Array.isArray(parsed)) throw new Error('Windows display enumeration returned invalid data');
661
+ const displays = parsed.filter(isWindowsDisplayGeometry);
662
+ if (displays.length !== parsed.length || 0 === displays.length) throw new Error('Windows display enumeration returned invalid geometry');
663
+ return displays;
664
+ }
665
+ function listWindowsDisplays(geometries = readWindowsDisplayGeometries()) {
666
+ return geometries.map((display)=>({
667
+ id: display.id,
668
+ name: display.name,
669
+ primary: display.primary
529
670
  }));
530
671
  }
531
672
  let device_libnut = null;
@@ -627,6 +768,16 @@ function getDisplayInfoBinary() {
627
768
  function isFiniteNumber(value) {
628
769
  return 'number' == typeof value && Number.isFinite(value);
629
770
  }
771
+ function isDisplayBounds(value) {
772
+ if (!value || 'object' != typeof value) return false;
773
+ const bounds = value;
774
+ return isFiniteNumber(bounds.x) && isFiniteNumber(bounds.y) && isFiniteNumber(bounds.width) && isFiniteNumber(bounds.height) && bounds.width > 0 && bounds.height > 0;
775
+ }
776
+ function isWindowsDisplayGeometry(value) {
777
+ if (!value || 'object' != typeof value) return false;
778
+ const candidate = value;
779
+ return 'string' == typeof candidate.id && candidate.id.length > 0 && 'string' == typeof candidate.name && 'boolean' == typeof candidate.primary && isDisplayBounds(candidate.bounds);
780
+ }
630
781
  function isDarwinDisplayGeometry(value) {
631
782
  if (!value || 'object' != typeof value) return false;
632
783
  const candidate = value;
@@ -669,9 +820,11 @@ function readDarwinFrontmostApplication() {
669
820
  return;
670
821
  }
671
822
  }
672
- async function pressMouseAtGlobalPoint(inputDriver, targetX, targetY, holdDuration, reason) {
673
- await inputDriver.delay(CLICK_SETTLE_DELAY);
674
- const current = inputDriver.getMousePos();
823
+ async function pressMouseAtGlobalPoint(inputDriver, targetX, targetY, current, holdDuration, reason) {
824
+ const drift = {
825
+ x: current.x - targetX,
826
+ y: current.y - targetY
827
+ };
675
828
  debugComputerInput('tap mouse moved %o', {
676
829
  reason,
677
830
  target: {
@@ -679,10 +832,7 @@ async function pressMouseAtGlobalPoint(inputDriver, targetX, targetY, holdDurati
679
832
  y: targetY
680
833
  },
681
834
  current,
682
- drift: {
683
- x: current.x - targetX,
684
- y: current.y - targetY
685
- }
835
+ drift
686
836
  });
687
837
  await inputDriver.withMouseButton('left', async ()=>{
688
838
  debugComputerInput('tap mouse down %o', {
@@ -701,9 +851,18 @@ function resolveDarwinDisplayGeometryFromList(displayId, displays) {
701
851
  if (void 0 === displayId || '' === displayId) return displays.find((display)=>display.primary) || displays.find((display)=>0 === display.screenIndex) || displays[0];
702
852
  return displays.find((display)=>display.screenIndex === screenIndex) || displays.find((display)=>display.cgDisplayId === screenIndex);
703
853
  }
704
- function resolveDisplayGeometry(displayId) {
705
- if ('darwin' !== process.platform) return;
706
- return resolveDarwinDisplayGeometryFromList(displayId, readDarwinDisplayGeometries());
854
+ function resolveWindowsDisplayGeometryFromList(displayId, displays) {
855
+ if (!displays.length) return;
856
+ if (void 0 === displayId || '' === displayId) return displays.find((display)=>display.primary) || displays[0];
857
+ return displays.find((display)=>display.id === displayId);
858
+ }
859
+ function resolveDisplayGeometry(displayId, windowsDisplays) {
860
+ if ('darwin' === process.platform) return resolveDarwinDisplayGeometryFromList(displayId, readDarwinDisplayGeometries());
861
+ if ('win32' === process.platform) {
862
+ const geometry = resolveWindowsDisplayGeometryFromList(displayId, windowsDisplays ?? readWindowsDisplayGeometries());
863
+ if (!geometry) throw new Error(displayId ? `Requested Windows display not found: ${displayId}` : 'No Windows displays were detected');
864
+ return geometry;
865
+ }
707
866
  }
708
867
  function mapDisplayLocalPointToGlobal(point, geometry) {
709
868
  if (!geometry) return point;
@@ -789,6 +948,30 @@ function normalizePrimaryKey(key) {
789
948
  return KEY_NAME_MAP[lowerKey] || lowerKey;
790
949
  }
791
950
  class ComputerDevice {
951
+ async moveGlobalPointer(point, context, smooth) {
952
+ if (this.destroyed) throw new Error('ComputerDevice has been destroyed');
953
+ const target = {
954
+ x: Math.round(point.x),
955
+ y: Math.round(point.y)
956
+ };
957
+ if ('win32' === process.platform) {
958
+ const actual = this.windowsPointerDriver.moveTo(target, {
959
+ smoothSteps: smooth?.smoothSteps,
960
+ smoothDelayMs: smooth?.smoothDelay
961
+ });
962
+ const drift = windowsPointerDrift(target, actual);
963
+ if (!windowsPointerIsWithinTolerance(drift)) throw new Error(`${context}: expected (${target.x}, ${target.y}), got (${actual.x}, ${actual.y}), drift=(${drift.x}, ${drift.y})`);
964
+ await this.inputDriver.delay(CLICK_SETTLE_DELAY);
965
+ return actual;
966
+ }
967
+ if (smooth) await this.inputDriver.smoothMoveMouse(target.x, target.y, smooth.smoothSteps, smooth.smoothDelay);
968
+ else this.inputDriver.moveMouse(target.x, target.y);
969
+ await this.inputDriver.delay(CLICK_SETTLE_DELAY);
970
+ return target;
971
+ }
972
+ moveDisplayPointer(point, context, smooth) {
973
+ return this.moveGlobalPointer(this.toGlobalPoint(point), context, smooth);
974
+ }
792
975
  async focusKeyboardTarget(element, delayMs) {
793
976
  const [x, y] = element.center;
794
977
  if ('darwin' === process.platform) await this.inputPrimitives.pointer.tap({
@@ -796,11 +979,10 @@ class ComputerDevice {
796
979
  y
797
980
  });
798
981
  else {
799
- const point = this.toGlobalPoint({
982
+ await this.moveDisplayPointer({
800
983
  x,
801
984
  y
802
- });
803
- this.inputDriver.moveMouse(Math.round(point.x), Math.round(point.y));
985
+ }, 'Mouse did not reach the keyboard focus target');
804
986
  this.inputDriver.mouseClick('left');
805
987
  }
806
988
  await this.inputDriver.delay(delayMs);
@@ -819,7 +1001,7 @@ class ComputerDevice {
819
1001
  }));
820
1002
  } catch (error) {
821
1003
  debugDevice(`Failed to list displays: ${error}`);
822
- return [];
1004
+ throw new Error(`Failed to list displays: ${error}`);
823
1005
  }
824
1006
  }
825
1007
  async connect() {
@@ -848,9 +1030,10 @@ class ComputerDevice {
848
1030
  }
849
1031
  }
850
1032
  device_libnut = await getLibnut();
851
- this.displayGeometry = resolveDisplayGeometry(this.displayId);
1033
+ const windowsDisplayGeometries = 'win32' === process.platform ? readWindowsDisplayGeometries() : void 0;
1034
+ this.displayGeometry = resolveDisplayGeometry(this.displayId, windowsDisplayGeometries);
852
1035
  const size = await this.size();
853
- const displays = await ComputerDevice.listDisplays();
1036
+ const displays = windowsDisplayGeometries ? listWindowsDisplays(windowsDisplayGeometries) : await ComputerDevice.listDisplays();
854
1037
  const headlessInfo = this.xvfbInstance ? `\nHeadless: true (Xvfb on ${this.xvfbInstance.display})` : '';
855
1038
  this.description = `
856
1039
  Type: Computer
@@ -860,7 +1043,7 @@ Screen Size: ${size.width}x${size.height}
860
1043
  Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ') : 'Unknown'}${headlessInfo}
861
1044
  `;
862
1045
  debugDevice('Computer device connected', this.description);
863
- await this.healthCheck();
1046
+ await this.healthCheck(displays);
864
1047
  } catch (error) {
865
1048
  if (this.xvfbInstance) {
866
1049
  if (!this.options?.keepXvfbAliveUntilProcessExit) this.xvfbInstance.stop();
@@ -879,9 +1062,9 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
879
1062
  throw new Error(`Unable to connect to computer device: ${error}`);
880
1063
  }
881
1064
  }
882
- async healthCheck() {
1065
+ async healthCheck(displays) {
883
1066
  console.log('[HealthCheck] Starting health check...');
884
- console.log("[HealthCheck] @midscene/computer v1.12.2");
1067
+ console.log("[HealthCheck] @midscene/computer v1.12.3-beta-20260828110230.0");
885
1068
  console.log('[HealthCheck] Taking screenshot...');
886
1069
  const screenshotTimeout = 15000;
887
1070
  let timeoutId;
@@ -893,23 +1076,38 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
893
1076
  timeoutPromise
894
1077
  ]);
895
1078
  console.log(`[HealthCheck] Screenshot succeeded (length=${base64.length})`);
896
- console.log('[HealthCheck] Moving mouse...');
897
- const startPos = this.inputDriver.getMousePos();
1079
+ console.log('[HealthCheck] Verifying mouse control...');
1080
+ const startPos = 'win32' === process.platform ? this.windowsPointerDriver.getPosition() : this.inputDriver.getMousePos();
898
1081
  console.log(`[HealthCheck] Current mouse position: (${startPos.x}, ${startPos.y})`);
899
- const offsetX = Math.floor(40 * Math.random()) + 10;
900
- const offsetY = Math.floor(40 * Math.random()) + 10;
901
- const targetX = startPos.x + offsetX;
902
- const targetY = startPos.y + offsetY;
903
- console.log(`[HealthCheck] Moving mouse to (${targetX}, ${targetY})...`);
904
- this.inputDriver.moveMouse(targetX, targetY);
905
- await (0, utils_namespaceObject.sleep)(50);
906
- const movedPos = this.inputDriver.getMousePos();
907
- console.log(`[HealthCheck] Mouse position after move: (${movedPos.x}, ${movedPos.y})`);
908
- const deltaX = Math.abs(movedPos.x - targetX);
909
- const deltaY = Math.abs(movedPos.y - targetY);
910
- if (deltaX > 5 || deltaY > 5) {
911
- const msg = `[HealthCheck] WARNING: Mouse control may not be working. Expected (${targetX}, ${targetY}), got (${movedPos.x}, ${movedPos.y}), delta=(${deltaX}, ${deltaY})`;
912
- warnDevice(msg);
1082
+ if ('win32' === process.platform) {
1083
+ if (!this.displayGeometry) throw new Error('Windows display geometry is unavailable');
1084
+ const bounds = this.displayGeometry.bounds;
1085
+ const target = {
1086
+ x: Math.round(bounds.x + bounds.width / 2),
1087
+ y: Math.round(bounds.y + bounds.height / 2)
1088
+ };
1089
+ try {
1090
+ const actual = this.windowsPointerDriver.moveTo(target);
1091
+ const drift = windowsPointerDrift(target, actual);
1092
+ if (!windowsPointerIsWithinTolerance(drift)) throw new Error(`Windows screenshot-space pointer verification: expected (${target.x}, ${target.y}), got (${actual.x}, ${actual.y}), drift=(${drift.x}, ${drift.y})`);
1093
+ } finally{
1094
+ this.windowsPointerDriver.moveTo(startPos);
1095
+ }
1096
+ console.log(`[HealthCheck] Mouse verified in screenshot-space display bounds (${bounds.x}, ${bounds.y}, ${bounds.width}, ${bounds.height})`);
1097
+ } else {
1098
+ const offsetX = Math.floor(40 * Math.random()) + 10;
1099
+ const offsetY = Math.floor(40 * Math.random()) + 10;
1100
+ const targetX = startPos.x + offsetX;
1101
+ const targetY = startPos.y + offsetY;
1102
+ console.log(`[HealthCheck] Moving mouse to (${targetX}, ${targetY})...`);
1103
+ try {
1104
+ this.inputDriver.moveMouse(targetX, targetY);
1105
+ await (0, utils_namespaceObject.sleep)(CLICK_SETTLE_DELAY);
1106
+ this.inputDriver.assertMousePosition(targetX, targetY, 'Mouse health check failed');
1107
+ } finally{
1108
+ this.inputDriver.moveMouse(startPos.x, startPos.y);
1109
+ console.log(`[HealthCheck] Mouse restored to (${startPos.x}, ${startPos.y})`);
1110
+ }
913
1111
  }
914
1112
  if ('win32' === process.platform && !this.isRunningAsAdmin()) {
915
1113
  const hint = [
@@ -919,10 +1117,7 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
919
1117
  ].join(' ');
920
1118
  warnDevice(`[HealthCheck] ${hint}`);
921
1119
  }
922
- this.inputDriver.moveMouse(startPos.x, startPos.y);
923
- console.log(`[HealthCheck] Mouse restored to (${startPos.x}, ${startPos.y})`);
924
1120
  console.log('[HealthCheck] Listing monitors...');
925
- const displays = await ComputerDevice.listDisplays();
926
1121
  if (displays.length > 0) {
927
1122
  console.log(`[HealthCheck] Found ${displays.length} monitor(s):`);
928
1123
  for (const display of displays){
@@ -991,6 +1186,7 @@ $screen = [System.Windows.Forms.Screen]::AllScreens | Where-Object { $_.DeviceNa
991
1186
  if (-not $screen) { throw "Requested display not found: $dn" }` : '$screen = [System.Windows.Forms.Screen]::PrimaryScreen';
992
1187
  const script = `
993
1188
  $ErrorActionPreference = 'Stop'
1189
+ ${WINDOWS_PHYSICAL_PIXEL_POWERSHELL_PREAMBLE}
994
1190
  Add-Type -AssemblyName System.Windows.Forms, System.Drawing
995
1191
  ${selectScreen}
996
1192
  $b = $screen.Bounds
@@ -1105,10 +1301,14 @@ $g.Dispose(); $bmp.Dispose(); $ms.Dispose()
1105
1301
  resolveUntargetedScrollPoint(screenSize) {
1106
1302
  if ('win32' === process.platform) {
1107
1303
  const activeWindowRect = this.inputDriver.getActiveWindowRect();
1108
- if (activeWindowRect) return {
1109
- x: activeWindowRect.x + activeWindowRect.width / 2,
1110
- y: activeWindowRect.y + activeWindowRect.height / 2
1111
- };
1304
+ if (activeWindowRect) {
1305
+ const activeWindowCenter = {
1306
+ x: activeWindowRect.x + activeWindowRect.width / 2,
1307
+ y: activeWindowRect.y + activeWindowRect.height / 2
1308
+ };
1309
+ const bounds = this.displayGeometry?.bounds;
1310
+ if (!bounds || activeWindowCenter.x >= bounds.x && activeWindowCenter.x < bounds.x + bounds.width && activeWindowCenter.y >= bounds.y && activeWindowCenter.y < bounds.y + bounds.height) return activeWindowCenter;
1311
+ }
1112
1312
  }
1113
1313
  return this.toGlobalPoint({
1114
1314
  x: screenSize.width / 2,
@@ -1119,17 +1319,16 @@ $g.Dispose(); $bmp.Dispose(); $ms.Dispose()
1119
1319
  if (param.locate) {
1120
1320
  const element = param.locate;
1121
1321
  const [x, y] = element.center;
1122
- const point = this.toGlobalPoint({
1322
+ await this.moveDisplayPointer({
1123
1323
  x,
1124
1324
  y
1125
- });
1126
- this.inputDriver.moveMouse(Math.round(point.x), Math.round(point.y));
1325
+ }, 'Mouse did not reach the scroll target');
1127
1326
  return;
1128
1327
  }
1129
1328
  const screenSize = await this.size();
1130
1329
  if ('win32' === process.platform && this.inputDriver.focusActiveWindow()) await this.inputDriver.delay(CLICK_FOCUS_SETTLE_DELAY);
1131
1330
  const point = this.resolveUntargetedScrollPoint(screenSize);
1132
- this.inputDriver.moveMouse(Math.round(point.x), Math.round(point.y));
1331
+ await this.moveGlobalPointer(point, 'Mouse did not reach the scroll viewport');
1133
1332
  await this.inputDriver.delay(MOUSE_MOVE_EFFECT_WAIT);
1134
1333
  return screenSize;
1135
1334
  }
@@ -1254,6 +1453,9 @@ $g.Dispose(); $bmp.Dispose(); $ms.Dispose()
1254
1453
  runPhasedScroll,
1255
1454
  debug: (message)=>debugDevice(message)
1256
1455
  }));
1456
+ device_define_property(this, "windowsPointerDriver", new WindowsPointerDriver({
1457
+ runPowershell
1458
+ }));
1257
1459
  device_define_property(this, "useAppleScript", void 0);
1258
1460
  device_define_property(this, "adminCheckCache", void 0);
1259
1461
  device_define_property(this, "uri", void 0);
@@ -1278,15 +1480,17 @@ $g.Dispose(); $bmp.Dispose(); $ms.Dispose()
1278
1480
  },
1279
1481
  holdDuration,
1280
1482
  displayId: this.displayId,
1281
- displayGeometry: this.displayGeometry ? {
1282
- screenIndex: this.displayGeometry.screenIndex,
1283
- cgDisplayId: this.displayGeometry.cgDisplayId,
1284
- bounds: this.displayGeometry.bounds
1285
- } : void 0
1483
+ displayGeometry: this.displayGeometry
1286
1484
  });
1287
1485
  const frontmostBefore = 'darwin' === process.platform ? readDarwinFrontmostApplication() : void 0;
1288
- await this.inputDriver.smoothMoveMouse(targetX, targetY, SMOOTH_MOVE_STEPS_TAP, SMOOTH_MOVE_DELAY_TAP);
1289
- await pressMouseAtGlobalPoint(this.inputDriver, targetX, targetY, holdDuration, 'primary');
1486
+ const current = await this.moveGlobalPointer({
1487
+ x: targetX,
1488
+ y: targetY
1489
+ }, 'Mouse did not reach the tap target', {
1490
+ smoothSteps: SMOOTH_MOVE_STEPS_TAP,
1491
+ smoothDelay: SMOOTH_MOVE_DELAY_TAP
1492
+ });
1493
+ await pressMouseAtGlobalPoint(this.inputDriver, targetX, targetY, current, holdDuration, 'primary');
1290
1494
  if (frontmostBefore && 'darwin' === process.platform) {
1291
1495
  await (0, utils_namespaceObject.sleep)(CLICK_FOCUS_SETTLE_DELAY);
1292
1496
  const frontmostAfter = readDarwinFrontmostApplication();
@@ -1297,42 +1501,43 @@ $g.Dispose(); $bmp.Dispose(); $ms.Dispose()
1297
1501
  focusChanged
1298
1502
  });
1299
1503
  if (focusChanged) {
1300
- this.inputDriver.moveMouse(targetX, targetY);
1301
- await pressMouseAtGlobalPoint(this.inputDriver, targetX, targetY, holdDuration, 'focus-follow-up');
1504
+ const followUpCurrent = await this.moveGlobalPointer({
1505
+ x: targetX,
1506
+ y: targetY
1507
+ }, 'Mouse did not reach the focus follow-up target');
1508
+ await pressMouseAtGlobalPoint(this.inputDriver, targetX, targetY, followUpCurrent, holdDuration, 'focus-follow-up');
1302
1509
  }
1303
1510
  }
1304
1511
  },
1305
1512
  doubleClick: async ({ x, y })=>{
1306
- const target = this.toGlobalPoint({
1513
+ await this.moveDisplayPointer({
1307
1514
  x,
1308
1515
  y
1309
- });
1310
- this.inputDriver.moveMouse(Math.round(target.x), Math.round(target.y));
1516
+ }, 'Mouse did not reach the double-click target');
1311
1517
  this.inputDriver.mouseClick('left', true);
1312
1518
  },
1313
1519
  rightClick: async ({ x, y })=>{
1314
- const target = this.toGlobalPoint({
1520
+ await this.moveDisplayPointer({
1315
1521
  x,
1316
1522
  y
1317
- });
1318
- this.inputDriver.moveMouse(Math.round(target.x), Math.round(target.y));
1523
+ }, 'Mouse did not reach the right-click target');
1319
1524
  this.inputDriver.mouseClick('right');
1320
1525
  },
1321
1526
  hover: async ({ x, y })=>{
1322
- const target = this.toGlobalPoint({
1527
+ await this.moveDisplayPointer({
1323
1528
  x,
1324
1529
  y
1530
+ }, 'Mouse did not reach the hover target', {
1531
+ smoothSteps: SMOOTH_MOVE_STEPS_MOUSE_MOVE,
1532
+ smoothDelay: SMOOTH_MOVE_DELAY_MOUSE_MOVE
1325
1533
  });
1326
- await this.inputDriver.smoothMoveMouse(Math.round(target.x), Math.round(target.y), SMOOTH_MOVE_STEPS_MOUSE_MOVE, SMOOTH_MOVE_DELAY_MOUSE_MOVE);
1327
1534
  await this.inputDriver.delay(MOUSE_MOVE_EFFECT_WAIT);
1328
1535
  },
1329
1536
  dragAndDrop: async (from, to)=>{
1330
- const globalFrom = this.toGlobalPoint(from);
1331
- const globalTo = this.toGlobalPoint(to);
1332
- this.inputDriver.moveMouse(Math.round(globalFrom.x), Math.round(globalFrom.y));
1537
+ await this.moveDisplayPointer(from, 'Mouse did not reach the drag start target');
1333
1538
  await this.inputDriver.withMouseButton('left', async ()=>{
1334
1539
  await this.inputDriver.delay(100);
1335
- this.inputDriver.moveMouse(Math.round(globalTo.x), Math.round(globalTo.y));
1540
+ await this.moveDisplayPointer(to, 'Mouse did not reach the drag end target');
1336
1541
  await this.inputDriver.delay(100);
1337
1542
  });
1338
1543
  }
@@ -2335,7 +2540,7 @@ class ComputerMidsceneTools extends base_tools_namespaceObject.BaseMidsceneTools
2335
2540
  }
2336
2541
  const env_namespaceObject = require("@midscene/shared/env");
2337
2542
  function version() {
2338
- const currentVersion = "1.12.2";
2543
+ const currentVersion = "1.12.3-beta-20260828110230.0";
2339
2544
  console.log(`@midscene/computer v${currentVersion}`);
2340
2545
  return currentVersion;
2341
2546
  }
@@ -75,6 +75,7 @@ export declare class ComputerDevice implements AbstractInterface {
75
75
  private xvfbCleanup?;
76
76
  private xvfbSignalCleanup?;
77
77
  private readonly inputDriver;
78
+ private readonly windowsPointerDriver;
78
79
  /**
79
80
  * On macOS, use AppleScript for keyboard operations by default
80
81
  * to avoid focus issues with system overlays (e.g. Spotlight).
@@ -85,6 +86,8 @@ export declare class ComputerDevice implements AbstractInterface {
85
86
  uri?: string;
86
87
  readonly inputPrimitives: ComputerInputPrimitives;
87
88
  constructor(options?: ComputerDeviceOpt);
89
+ private moveGlobalPointer;
90
+ private moveDisplayPointer;
88
91
  private focusKeyboardTarget;
89
92
  describe(): string;
90
93
  /**
@@ -110,15 +113,10 @@ export declare class ComputerDevice implements AbstractInterface {
110
113
  * DeviceName and captures in virtual-desktop coordinates, so secondary
111
114
  * displays — including those at negative offsets — are supported.
112
115
  *
113
- * Note: the process is left at its default (DPI-unaware) state on purpose.
114
- * Making it DPI-aware would require a runtime `Add-Type` C# compile (csc) —
115
- * the exact .NET-compiler dependency this PR removes by dropping
116
- * screenshot-desktop's polyglot .bat — so it is intentionally avoided here.
117
- * As a result, captures on a scaled display come back at logical (scaled)
118
- * resolution. That is sufficient for the #2150 fix (the health check only
119
- * needs a successful capture). Per-monitor DPI / coordinate accuracy is a
120
- * separate Windows concern to be addressed in a follow-up with real-device
121
- * verification.
116
+ * The PowerShell thread is switched to Per-Monitor V2 before WinForms is
117
+ * loaded. Screen.Bounds, CopyFromScreen, and pointer movement therefore use
118
+ * physical pixels even when Windows display scaling is enabled. The native
119
+ * declaration is emitted in memory, so this does not require csc.exe.
122
120
  */
123
121
  private screenshotViaPowershell;
124
122
  size(): Promise<Size>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@midscene/computer",
3
- "version": "1.12.2",
3
+ "version": "1.12.3-beta-20260828110230.0",
4
4
  "description": "Midscene.js Computer Desktop Automation",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -33,8 +33,8 @@
33
33
  "@computer-use/libnut": "^4.2.0",
34
34
  "clipboardy": "^4.0.0",
35
35
  "screenshot-desktop": "^1.15.3",
36
- "@midscene/core": "1.12.2",
37
- "@midscene/shared": "1.12.2"
36
+ "@midscene/core": "1.12.3-beta-20260828110230.0",
37
+ "@midscene/shared": "1.12.3-beta-20260828110230.0"
38
38
  },
39
39
  "optionalDependencies": {
40
40
  "node-mac-permissions": "2.5.0"