@midscene/computer 1.13.0 → 1.13.1-beta-20260920094844.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/cli.js CHANGED
@@ -450,6 +450,20 @@ $midsceneSetDpiMethod.SetImplementationFlags(
450
450
  $midsceneSetDpiMethod.GetMethodImplementationFlags() -bor
451
451
  [System.Reflection.MethodImplAttributes]::PreserveSig
452
452
  )
453
+ $midsceneGetDpiForSystemMethod = $midsceneType.DefinePInvokeMethod(
454
+ 'GetDpiForSystem',
455
+ 'user32.dll',
456
+ $midsceneMethodAttributes,
457
+ [System.Reflection.CallingConventions]::Standard,
458
+ [uint32],
459
+ [System.Type[]]@(),
460
+ [System.Runtime.InteropServices.CallingConvention]::Winapi,
461
+ [System.Runtime.InteropServices.CharSet]::None
462
+ )
463
+ $midsceneGetDpiForSystemMethod.SetImplementationFlags(
464
+ $midsceneGetDpiForSystemMethod.GetMethodImplementationFlags() -bor
465
+ [System.Reflection.MethodImplAttributes]::PreserveSig
466
+ )
453
467
  $midsceneGetForegroundWindowMethod = $midsceneType.DefinePInvokeMethod(
454
468
  'GetForegroundWindow',
455
469
  'user32.dll',
@@ -486,12 +500,8 @@ if ($midscenePreviousDpiContext -eq [System.IntPtr]::Zero) {
486
500
  throw 'Unable to enter the Per-Monitor V2 DPI awareness context.'
487
501
  }
488
502
  `.trim();
489
- function runWindowsPhysicalPixelPowershell(script) {
490
- const physicalPixelScript = `$ProgressPreference = 'SilentlyContinue'
491
- $ErrorActionPreference = 'Stop'
492
- ${WINDOWS_PHYSICAL_PIXEL_POWERSHELL_PREAMBLE}
493
- ${script}`;
494
- const encoded = Buffer.from(physicalPixelScript, 'utf16le').toString('base64');
503
+ function runWindowsPowershellScript(script) {
504
+ const encoded = Buffer.from(script, 'utf16le').toString('base64');
495
505
  return (0, external_node_child_process_namespaceObject.execFileSync)('powershell.exe', [
496
506
  '-NoProfile',
497
507
  '-NonInteractive',
@@ -504,6 +514,137 @@ ${script}`;
504
514
  windowsHide: true
505
515
  });
506
516
  }
517
+ function runWindowsPowershellCommand(script) {
518
+ return (0, external_node_child_process_namespaceObject.execFileSync)('powershell.exe', [
519
+ '-NoProfile',
520
+ '-Command',
521
+ script
522
+ ], {
523
+ encoding: 'utf8',
524
+ timeout: POWERSHELL_TIMEOUT_MS,
525
+ maxBuffer: POWERSHELL_MAX_BUFFER,
526
+ windowsHide: true
527
+ });
528
+ }
529
+ function runWindowsPowershell(script) {
530
+ return runWindowsPowershellCommand(`$ProgressPreference = 'SilentlyContinue'
531
+ $ErrorActionPreference = 'Stop'
532
+ ${script}`);
533
+ }
534
+ function runWindowsPhysicalPixelPowershell(script) {
535
+ const physicalPixelScript = `$ProgressPreference = 'SilentlyContinue'
536
+ $ErrorActionPreference = 'Stop'
537
+ ${WINDOWS_PHYSICAL_PIXEL_POWERSHELL_PREAMBLE}
538
+ ${script}`;
539
+ return runWindowsPowershellScript(physicalPixelScript);
540
+ }
541
+ class WindowsDisplayEnumerationEmptyError extends Error {
542
+ }
543
+ function isFiniteNumber(value) {
544
+ return 'number' == typeof value && Number.isFinite(value);
545
+ }
546
+ function isWindowsDisplayGeometry(value) {
547
+ if (!value || 'object' != typeof value) return false;
548
+ const candidate = value;
549
+ const bounds = candidate.bounds;
550
+ return 'string' == typeof candidate.id && candidate.id.length > 0 && 'string' == typeof candidate.name && 'boolean' == typeof candidate.primary && !!bounds && isFiniteNumber(bounds.x) && isFiniteNumber(bounds.y) && isFiniteNumber(bounds.width) && isFiniteNumber(bounds.height) && bounds.width > 0 && bounds.height > 0;
551
+ }
552
+ function readWindowsDisplayGeometriesWith(runPowershell, context) {
553
+ const serializeScreens = 'legacy' === context ? 'ConvertTo-Json -InputObject $screens -Compress' : '[Console]::Out.Write((ConvertTo-Json -InputObject $screens -Compress))';
554
+ const script = `
555
+ Add-Type -AssemblyName System.Windows.Forms
556
+ $screens = @([System.Windows.Forms.Screen]::AllScreens | ForEach-Object {
557
+ $bounds = $_.Bounds
558
+ [PSCustomObject]@{
559
+ id = $_.DeviceName
560
+ name = $_.DeviceName
561
+ primary = $_.Primary
562
+ bounds = [PSCustomObject]@{
563
+ x = $bounds.X
564
+ y = $bounds.Y
565
+ width = $bounds.Width
566
+ height = $bounds.Height
567
+ }
568
+ }
569
+ })
570
+ ${serializeScreens}
571
+ `.trim();
572
+ const output = runPowershell(script).trim();
573
+ if (!output) throw new WindowsDisplayEnumerationEmptyError(`Windows ${context} display enumeration returned no data`);
574
+ const parsed = JSON.parse(output);
575
+ if (!Array.isArray(parsed)) throw new Error('Windows display enumeration returned invalid data');
576
+ const displays = parsed.filter(isWindowsDisplayGeometry);
577
+ if (displays.length !== parsed.length) throw new Error('Windows display enumeration returned invalid geometry');
578
+ if (0 === displays.length) throw new WindowsDisplayEnumerationEmptyError(`Windows ${context} display enumeration returned no displays`);
579
+ return displays;
580
+ }
581
+ function readWindowsDisplayGeometries() {
582
+ return readWindowsDisplayGeometriesWith(runWindowsPhysicalPixelPowershell, 'physical');
583
+ }
584
+ function discoverWindowsDisplays(runners = {
585
+ physical: runWindowsPhysicalPixelPowershell,
586
+ legacy: runWindowsPowershell
587
+ }) {
588
+ try {
589
+ return {
590
+ geometries: readWindowsDisplayGeometriesWith(runners.physical, 'physical'),
591
+ coordinateMode: 'physical'
592
+ };
593
+ } catch (error) {
594
+ if (!(error instanceof WindowsDisplayEnumerationEmptyError)) throw error;
595
+ const geometries = readWindowsDisplayGeometriesWith(runners.legacy, 'legacy');
596
+ return {
597
+ geometries,
598
+ coordinateMode: 'legacy'
599
+ };
600
+ }
601
+ }
602
+ function readPhysicalWindowsSystemDpi() {
603
+ const output = runWindowsPhysicalPixelPowershell('[Console]::Out.Write($midsceneNativeMethods::GetDpiForSystem())').trim();
604
+ const dpi = Number(output);
605
+ if (!Number.isFinite(dpi) || dpi <= 0) throw new Error(`Windows returned an invalid system DPI value: ${output}`);
606
+ return dpi;
607
+ }
608
+ function readPngDimensions(dataUri) {
609
+ const prefix = 'data:image/png;base64,';
610
+ if (!dataUri.startsWith(prefix)) throw new Error('Screenshot is not a PNG data URI');
611
+ const buffer = Buffer.from(dataUri.slice(prefix.length), 'base64');
612
+ const pngSignature = Buffer.from([
613
+ 137,
614
+ 80,
615
+ 78,
616
+ 71,
617
+ 13,
618
+ 10,
619
+ 26,
620
+ 10
621
+ ]);
622
+ if (buffer.length < 24 || !buffer.subarray(0, pngSignature.length).equals(pngSignature)) throw new Error('Screenshot returned invalid PNG data');
623
+ const width = buffer.readUInt32BE(16);
624
+ const height = buffer.readUInt32BE(20);
625
+ if (width <= 0 || height <= 0) throw new Error('Screenshot returned invalid PNG dimensions');
626
+ return {
627
+ width,
628
+ height
629
+ };
630
+ }
631
+ function assertLegacyWindowsCoordinateCompatibility({ geometry, systemDpi, screenshotBase64, inputSize }) {
632
+ if (!geometry?.primary) throw new Error('Windows legacy coordinate compatibility mode only supports the primary display');
633
+ if (0 !== geometry.bounds.x || 0 !== geometry.bounds.y) throw new Error(`Windows legacy coordinate compatibility mode requires a primary display origin of (0, 0), got (${geometry.bounds.x}, ${geometry.bounds.y})`);
634
+ if (96 !== systemDpi) throw new Error(`Windows legacy coordinate compatibility mode requires 100% display scaling (96 DPI), got ${systemDpi} DPI`);
635
+ const screenshotSize = readPngDimensions(screenshotBase64);
636
+ const displaySize = {
637
+ width: Math.round(geometry.bounds.width),
638
+ height: Math.round(geometry.bounds.height)
639
+ };
640
+ const sizesMatch = screenshotSize.width === displaySize.width && screenshotSize.height === displaySize.height && inputSize.width === displaySize.width && inputSize.height === displaySize.height;
641
+ if (!sizesMatch) throw new Error(`Windows legacy coordinate compatibility check failed: display=${displaySize.width}x${displaySize.height}, screenshot=${screenshotSize.width}x${screenshotSize.height}, input=${inputSize.width}x${inputSize.height}. Refusing to continue because clicks may use incorrect coordinates.`);
642
+ }
643
+ function resolveWindowsDisplayGeometryFromList(displayId, displays) {
644
+ if (!displays.length) return;
645
+ if (void 0 === displayId || '' === displayId) return displays.find((display)=>display.primary) || displays[0];
646
+ return displays.find((display)=>display.id === displayId);
647
+ }
507
648
  function windows_pointer_define_property(obj, key, value) {
508
649
  if (key in obj) Object.defineProperty(obj, key, {
509
650
  value: value,
@@ -822,28 +963,6 @@ const EDGE_SCROLL_SPEC = {
822
963
  function escapePowershellSingleQuoted(value) {
823
964
  return value.replace(/'/g, "''");
824
965
  }
825
- function readWindowsDisplayGeometries() {
826
- const script = `
827
- Add-Type -AssemblyName System.Windows.Forms
828
- $s = [System.Windows.Forms.Screen]::AllScreens | ForEach-Object {
829
- $b = $_.Bounds
830
- [PSCustomObject]@{
831
- id = $_.DeviceName
832
- name = $_.DeviceName
833
- primary = $_.Primary
834
- bounds = [PSCustomObject]@{ x = $b.X; y = $b.Y; width = $b.Width; height = $b.Height }
835
- }
836
- }
837
- ConvertTo-Json @($s) -Compress
838
- `.trim();
839
- const output = runWindowsPhysicalPixelPowershell(script).trim();
840
- if (!output) throw new Error('Windows display enumeration returned no data');
841
- const parsed = JSON.parse(output);
842
- if (!Array.isArray(parsed)) throw new Error('Windows display enumeration returned invalid data');
843
- const displays = parsed.filter(isWindowsDisplayGeometry);
844
- if (displays.length !== parsed.length || 0 === displays.length) throw new Error('Windows display enumeration returned invalid geometry');
845
- return displays;
846
- }
847
966
  function listWindowsDisplays(geometries = readWindowsDisplayGeometries()) {
848
967
  return geometries.map((display)=>({
849
968
  id: display.id,
@@ -947,23 +1066,13 @@ function getDisplayInfoBinary() {
947
1066
  displayInfoBinaryPath = binPath;
948
1067
  return binPath;
949
1068
  }
950
- function isFiniteNumber(value) {
1069
+ function device_isFiniteNumber(value) {
951
1070
  return 'number' == typeof value && Number.isFinite(value);
952
1071
  }
953
- function isDisplayBounds(value) {
954
- if (!value || 'object' != typeof value) return false;
955
- const bounds = value;
956
- return isFiniteNumber(bounds.x) && isFiniteNumber(bounds.y) && isFiniteNumber(bounds.width) && isFiniteNumber(bounds.height) && bounds.width > 0 && bounds.height > 0;
957
- }
958
- function isWindowsDisplayGeometry(value) {
959
- if (!value || 'object' != typeof value) return false;
960
- const candidate = value;
961
- return 'string' == typeof candidate.id && candidate.id.length > 0 && 'string' == typeof candidate.name && 'boolean' == typeof candidate.primary && isDisplayBounds(candidate.bounds);
962
- }
963
1072
  function isDarwinDisplayGeometry(value) {
964
1073
  if (!value || 'object' != typeof value) return false;
965
1074
  const candidate = value;
966
- return Number.isInteger(candidate.screenIndex) && Number.isInteger(candidate.cgDisplayId) && 'boolean' == typeof candidate.primary && !!candidate.bounds && isFiniteNumber(candidate.bounds.x) && isFiniteNumber(candidate.bounds.y) && isFiniteNumber(candidate.bounds.width) && isFiniteNumber(candidate.bounds.height);
1075
+ return Number.isInteger(candidate.screenIndex) && Number.isInteger(candidate.cgDisplayId) && 'boolean' == typeof candidate.primary && !!candidate.bounds && device_isFiniteNumber(candidate.bounds.x) && device_isFiniteNumber(candidate.bounds.y) && device_isFiniteNumber(candidate.bounds.width) && device_isFiniteNumber(candidate.bounds.height);
967
1076
  }
968
1077
  function readDarwinDisplayGeometries() {
969
1078
  const bin = getDisplayInfoBinary();
@@ -1033,11 +1142,6 @@ function resolveDarwinDisplayGeometryFromList(displayId, displays) {
1033
1142
  if (void 0 === displayId || '' === displayId) return displays.find((display)=>display.primary) || displays.find((display)=>0 === display.screenIndex) || displays[0];
1034
1143
  return displays.find((display)=>display.screenIndex === screenIndex) || displays.find((display)=>display.cgDisplayId === screenIndex);
1035
1144
  }
1036
- function resolveWindowsDisplayGeometryFromList(displayId, displays) {
1037
- if (!displays.length) return;
1038
- if (void 0 === displayId || '' === displayId) return displays.find((display)=>display.primary) || displays[0];
1039
- return displays.find((display)=>display.id === displayId);
1040
- }
1041
1145
  function resolveDisplayGeometry(displayId, windowsDisplays) {
1042
1146
  if ('darwin' === process.platform) return resolveDarwinDisplayGeometryFromList(displayId, readDarwinDisplayGeometries());
1043
1147
  if ('win32' === process.platform) {
@@ -1130,13 +1234,25 @@ function normalizePrimaryKey(key) {
1130
1234
  return KEY_NAME_MAP[lowerKey] || lowerKey;
1131
1235
  }
1132
1236
  class ComputerDevice {
1237
+ usesPhysicalWindowsCoordinates() {
1238
+ return 'win32' === process.platform && 'physical' === this.windowsCoordinateContext.mode;
1239
+ }
1240
+ assertLegacyWindowsCoordinateCompatibility(screenshotBase64) {
1241
+ if ('win32' !== process.platform || 'legacy' !== this.windowsCoordinateContext.mode) return;
1242
+ assertLegacyWindowsCoordinateCompatibility({
1243
+ geometry: this.displayGeometry,
1244
+ systemDpi: this.windowsCoordinateContext.systemDpi,
1245
+ screenshotBase64,
1246
+ inputSize: this.inputDriver.getScreenSize()
1247
+ });
1248
+ }
1133
1249
  async moveGlobalPointer(point, context, smooth) {
1134
1250
  if (this.destroyed) throw new Error('ComputerDevice has been destroyed');
1135
1251
  const target = {
1136
1252
  x: Math.round(point.x),
1137
1253
  y: Math.round(point.y)
1138
1254
  };
1139
- if ('win32' === process.platform) {
1255
+ if (this.usesPhysicalWindowsCoordinates()) {
1140
1256
  const actual = this.windowsPointerDriver.moveTo(target, {
1141
1257
  smoothSteps: smooth?.smoothSteps,
1142
1258
  smoothDelayMs: smooth?.smoothDelay
@@ -1188,7 +1304,7 @@ class ComputerDevice {
1188
1304
  }
1189
1305
  static async listDisplays() {
1190
1306
  try {
1191
- if ('win32' === process.platform) return listWindowsDisplays();
1307
+ if ('win32' === process.platform) return listWindowsDisplays(discoverWindowsDisplays().geometries);
1192
1308
  const displays = await external_screenshot_desktop_default().listDisplays();
1193
1309
  return displays.map((d)=>({
1194
1310
  id: String(d.id),
@@ -1226,7 +1342,15 @@ class ComputerDevice {
1226
1342
  }
1227
1343
  }
1228
1344
  device_libnut = await getLibnut();
1229
- const windowsDisplayGeometries = 'win32' === process.platform ? readWindowsDisplayGeometries() : void 0;
1345
+ const windowsDisplayDiscovery = 'win32' === process.platform ? discoverWindowsDisplays() : void 0;
1346
+ const windowsDisplayGeometries = windowsDisplayDiscovery?.geometries;
1347
+ if (windowsDisplayDiscovery?.coordinateMode === 'legacy') warnDevice('Windows display enumeration returned no data in the Per-Monitor V2 DPI context; using the legacy Windows coordinate path for this device connection.');
1348
+ this.windowsCoordinateContext = windowsDisplayDiscovery?.coordinateMode === 'legacy' ? {
1349
+ mode: 'legacy',
1350
+ systemDpi: readPhysicalWindowsSystemDpi()
1351
+ } : {
1352
+ mode: 'physical'
1353
+ };
1230
1354
  this.displayGeometry = resolveDisplayGeometry(this.displayId, windowsDisplayGeometries);
1231
1355
  const size = await this.size();
1232
1356
  const displays = windowsDisplayGeometries ? listWindowsDisplays(windowsDisplayGeometries) : await ComputerDevice.listDisplays();
@@ -1237,6 +1361,7 @@ Platform: ${process.platform}
1237
1361
  Display: ${this.displayId || 'Primary'}
1238
1362
  Screen Size: ${size.width}x${size.height}
1239
1363
  Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ') : 'Unknown'}${headlessInfo}
1364
+ ${'win32' === process.platform ? `Windows Coordinate Mode: ${this.windowsCoordinateContext.mode}` : ''}
1240
1365
  `;
1241
1366
  debugDevice('Computer device connected', this.description);
1242
1367
  await this.healthCheck(displays);
@@ -1260,7 +1385,7 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
1260
1385
  }
1261
1386
  async healthCheck(displays) {
1262
1387
  console.log('[HealthCheck] Starting health check...');
1263
- console.log("[HealthCheck] @midscene/computer v1.13.0");
1388
+ console.log("[HealthCheck] @midscene/computer v1.13.1-beta-20260920094844.0");
1264
1389
  console.log('[HealthCheck] Taking screenshot...');
1265
1390
  const screenshotTimeout = 15000;
1266
1391
  let timeoutId;
@@ -1272,10 +1397,12 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
1272
1397
  timeoutPromise
1273
1398
  ]);
1274
1399
  console.log(`[HealthCheck] Screenshot succeeded (length=${base64.length})`);
1400
+ this.assertLegacyWindowsCoordinateCompatibility(base64);
1275
1401
  console.log('[HealthCheck] Verifying mouse control...');
1276
- const startPos = 'win32' === process.platform ? this.windowsPointerDriver.getPosition() : this.inputDriver.getMousePos();
1402
+ const usePhysicalWindowsCoordinates = this.usesPhysicalWindowsCoordinates();
1403
+ const startPos = usePhysicalWindowsCoordinates ? this.windowsPointerDriver.getPosition() : this.inputDriver.getMousePos();
1277
1404
  console.log(`[HealthCheck] Current mouse position: (${startPos.x}, ${startPos.y})`);
1278
- if ('win32' === process.platform) {
1405
+ if (usePhysicalWindowsCoordinates) {
1279
1406
  if (!this.displayGeometry) throw new Error('Windows display geometry is unavailable');
1280
1407
  const bounds = this.displayGeometry.bounds;
1281
1408
  const target = {
@@ -1341,7 +1468,7 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
1341
1468
  debugDevice('Taking screenshot', {
1342
1469
  displayId: this.displayId
1343
1470
  });
1344
- if ('win32' === process.platform) return this.screenshotViaPowershell();
1471
+ if (this.usesPhysicalWindowsCoordinates()) return this.screenshotViaPowershell();
1345
1472
  const options = {
1346
1473
  format: 'png'
1347
1474
  };
@@ -1504,7 +1631,7 @@ $g.Dispose(); $bmp.Dispose(); $ms.Dispose()
1504
1631
  this.inputDriver.sendKey(key, modifiers);
1505
1632
  }
1506
1633
  resolveUntargetedScrollPoint(screenSize) {
1507
- if ('win32' === process.platform) {
1634
+ if (this.usesPhysicalWindowsCoordinates()) {
1508
1635
  const activeWindowRect = this.windowsPointerDriver.getActiveWindowRect();
1509
1636
  if (activeWindowRect) {
1510
1637
  const activeWindowCenter = {
@@ -1648,6 +1775,9 @@ $g.Dispose(); $bmp.Dispose(); $ms.Dispose()
1648
1775
  device_define_property(this, "options", void 0);
1649
1776
  device_define_property(this, "displayId", void 0);
1650
1777
  device_define_property(this, "displayGeometry", void 0);
1778
+ device_define_property(this, "windowsCoordinateContext", {
1779
+ mode: 'physical'
1780
+ });
1651
1781
  device_define_property(this, "description", void 0);
1652
1782
  device_define_property(this, "destroyed", false);
1653
1783
  device_define_property(this, "xvfbInstance", void 0);
@@ -2750,7 +2880,7 @@ const tools = new ComputerMidsceneTools({
2750
2880
  });
2751
2881
  (0, cli_namespaceObject.runToolsCLI)(tools, 'midscene-computer', {
2752
2882
  stripPrefix: 'computer_',
2753
- version: "1.13.0",
2883
+ version: "1.13.1-beta-20260920094844.0",
2754
2884
  extraCommands: (0, core_namespaceObject.createReportCliCommands)()
2755
2885
  }).catch((e)=>{
2756
2886
  process.exit((0, cli_namespaceObject.reportCLIError)(e));