@ai-agent-forge/plugin-desktop-windows-input 0.88.1 → 0.88.3
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/MANUAL.md +25 -0
- package/agent-forge.json +1 -1
- package/dist/capability.d.ts.map +1 -1
- package/dist/capability.js +27 -4
- package/dist/capability.js.map +1 -1
- package/dist/types.d.ts +39 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/windows-input-adapter.d.ts +3 -1
- package/dist/windows-input-adapter.d.ts.map +1 -1
- package/dist/windows-input-adapter.js +17 -0
- package/dist/windows-input-adapter.js.map +1 -1
- package/dist/windows-uia-adapter.d.ts +9 -2
- package/dist/windows-uia-adapter.d.ts.map +1 -1
- package/dist/windows-uia-adapter.js +276 -24
- package/dist/windows-uia-adapter.js.map +1 -1
- package/package.json +3 -2
- package/plugin.json +4 -11
|
@@ -19,6 +19,12 @@ const SUPPORTED_ACTIONS = [
|
|
|
19
19
|
"key",
|
|
20
20
|
"scroll",
|
|
21
21
|
];
|
|
22
|
+
/** Contract limit: a delivered screenshot must stay at or below 4 MiB decoded. */
|
|
23
|
+
const DESKTOP_SCREENSHOT_MAX_DECODED_BYTES = 4 * 1024 * 1024;
|
|
24
|
+
/** Spawn buffer headroom above the contract limit so the limit is enforced here, not by maxBuffer. */
|
|
25
|
+
const SCREENSHOT_SPAWN_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
26
|
+
/** Screens wider than this are scaled down proportionally before PNG encoding. */
|
|
27
|
+
const DESKTOP_SCREENSHOT_MAX_WIDTH_PX = 1280;
|
|
22
28
|
const error = (code, message, causeCode) => ({
|
|
23
29
|
version: 1,
|
|
24
30
|
code,
|
|
@@ -38,6 +44,7 @@ const rectangle = (raw) => ({
|
|
|
38
44
|
width: Number.isFinite(raw.width) ? Number(raw.width) : 0,
|
|
39
45
|
height: Number.isFinite(raw.height) ? Number(raw.height) : 0,
|
|
40
46
|
});
|
|
47
|
+
const screenshotDimension = (value) => typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value > 0 ? value : 0;
|
|
41
48
|
const QUERY_SCRIPT = `
|
|
42
49
|
try {
|
|
43
50
|
Add-Type -AssemblyName UIAutomationClient -ErrorAction Stop
|
|
@@ -226,29 +233,48 @@ public static class AgentForgeNativeText {
|
|
|
226
233
|
} catch { throw 'Target text update failed' }
|
|
227
234
|
}
|
|
228
235
|
} elseif ($payload.action.kind -eq 'key') {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
# click (BM_CLICK) already use. Keyboard focus is not required.
|
|
236
|
+
# Real keyboard-queue input: keybd_event posts to the same queue as the
|
|
237
|
+
# physical keyboard. The native-file-picker flow depends on this — the
|
|
238
|
+
# dialog is focused but invisible to UIA and not addressable by window
|
|
239
|
+
# messages sent to the owner window. Named keys and Ctrl/Alt/Shift
|
|
240
|
+
# combinations go through virtual-key codes; anything else must be a
|
|
241
|
+
# single character.
|
|
242
|
+
try {
|
|
237
243
|
Add-Type @'
|
|
238
244
|
using System;
|
|
239
245
|
using System.Runtime.InteropServices;
|
|
240
|
-
public static class
|
|
241
|
-
[DllImport("user32.dll"
|
|
246
|
+
public static class AgentForgeNativeKeyQueue {
|
|
247
|
+
[DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
|
|
242
248
|
}
|
|
243
249
|
'@
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
250
|
+
} catch {}
|
|
251
|
+
$vkMap = @{
|
|
252
|
+
'enter'=0x0D; 'return'=0x0D; 'tab'=0x09; 'escape'=0x1B; 'esc'=0x1B;
|
|
253
|
+
'backspace'=0x08; 'delete'=0x2E; 'space'=0x20;
|
|
254
|
+
'up'=0x26; 'down'=0x28; 'left'=0x25; 'right'=0x27;
|
|
255
|
+
'arrowup'=0x26; 'arrowdown'=0x28; 'arrowleft'=0x25; 'arrowright'=0x27;
|
|
256
|
+
'home'=0x24; 'end'=0x23; 'pageup'=0x21; 'pagedown'=0x22; 'insert'=0x2D
|
|
257
|
+
}
|
|
258
|
+
$modMap = @{ 'ctrl'=0x11; 'control'=0x11; 'alt'=0x12; 'shift'=0x10; 'meta'=0x5B; 'win'=0x5B; 'cmd'=0x5B }
|
|
259
|
+
$keyText = ([string]$payload.action.key).Trim()
|
|
260
|
+
$mods = @()
|
|
261
|
+
$base = $null
|
|
262
|
+
foreach ($p in ($keyText -split '+')) {
|
|
263
|
+
$lp = $p.ToLower()
|
|
264
|
+
if ($modMap.ContainsKey($lp)) { $mods += $modMap[$lp] } else { $base = $lp }
|
|
265
|
+
}
|
|
266
|
+
$vk = $null
|
|
267
|
+
if ($null -ne $base -and $vkMap.ContainsKey($base)) { $vk = $vkMap[$base] }
|
|
268
|
+
elseif ($null -ne $base -and $base.Length -eq 1) {
|
|
269
|
+
$upper = [char]::ToUpper($base[0])
|
|
270
|
+
$code = [int]$upper
|
|
271
|
+
if (($code -ge 0x20 -and $code -le 0x5A) -or ($code -ge 0x30 -and $code -le 0x39)) { $vk = $code }
|
|
272
|
+
}
|
|
273
|
+
if ($null -eq $vk) { throw ('Unsupported key: ' + $keyText) }
|
|
274
|
+
foreach ($m in $mods) { [AgentForgeNativeKeyQueue]::keybd_event([byte]$m, 0, 0, [UIntPtr]::Zero) }
|
|
275
|
+
[AgentForgeNativeKeyQueue]::keybd_event([byte]$vk, 0, 0, [UIntPtr]::Zero)
|
|
276
|
+
[AgentForgeNativeKeyQueue]::keybd_event([byte]$vk, 0, 2, [UIntPtr]::Zero)
|
|
277
|
+
for ($i = $mods.Count - 1; $i -ge 0; $i--) { [AgentForgeNativeKeyQueue]::keybd_event([byte]$mods[$i], 0, 2, [UIntPtr]::Zero) }
|
|
252
278
|
} elseif ($payload.action.kind -eq 'scroll') {
|
|
253
279
|
Add-Type @'
|
|
254
280
|
using System;
|
|
@@ -282,6 +308,128 @@ public static class AgentForgeNativeScroll {
|
|
|
282
308
|
$reason = if ($message -match 'ValuePattern|LegacyIAccessiblePattern') { 'value_pattern_unavailable' } elseif ($message -match 'InvokePattern') { 'invoke_pattern_unavailable' } elseif ($message -match 'SetFocus') { 'focus_failed' } else { 'uia_action_exception' }
|
|
283
309
|
[pscustomobject]@{ ok=$false; code=$code; reason=$reason } | ConvertTo-Json -Compress
|
|
284
310
|
}`;
|
|
311
|
+
/**
|
|
312
|
+
* Foreground (no-target) type/key: real keyboard-queue input for surfaces UIA
|
|
313
|
+
* cannot see — the native file dialog's focused name box being the canonical
|
|
314
|
+
* case. Named keys go through virtual-key codes with modifier support; other
|
|
315
|
+
* text is typed as VK_PACKET unicode events.
|
|
316
|
+
*/
|
|
317
|
+
const FOREGROUND_INPUT_SCRIPT = `
|
|
318
|
+
try {
|
|
319
|
+
$payload = $env:AGENT_FORGE_UIA_INPUT | ConvertFrom-Json
|
|
320
|
+
try {
|
|
321
|
+
Add-Type @'
|
|
322
|
+
using System;
|
|
323
|
+
using System.Runtime.InteropServices;
|
|
324
|
+
public static class AgentForgeForegroundKeyQueue {
|
|
325
|
+
[DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
|
|
326
|
+
}
|
|
327
|
+
'@
|
|
328
|
+
} catch {}
|
|
329
|
+
$vkMap = @{
|
|
330
|
+
'enter'=0x0D; 'return'=0x0D; 'tab'=0x09; 'escape'=0x1B; 'esc'=0x1B;
|
|
331
|
+
'backspace'=0x08; 'delete'=0x2E; 'space'=0x20;
|
|
332
|
+
'up'=0x26; 'down'=0x28; 'left'=0x25; 'right'=0x27;
|
|
333
|
+
'arrowup'=0x26; 'arrowdown'=0x28; 'arrowleft'=0x25; 'arrowright'=0x27;
|
|
334
|
+
'home'=0x24; 'end'=0x23; 'pageup'=0x21; 'pagedown'=0x22; 'insert'=0x2D
|
|
335
|
+
}
|
|
336
|
+
$modMap = @{ 'ctrl'=0x11; 'control'=0x11; 'alt'=0x12; 'shift'=0x10; 'meta'=0x5B; 'win'=0x5B; 'cmd'=0x5B }
|
|
337
|
+
if ($payload.action.kind -eq 'type') {
|
|
338
|
+
foreach ($ch in ([string]$payload.action.text).ToCharArray()) {
|
|
339
|
+
[AgentForgeForegroundKeyQueue]::keybd_event(0xE7, [byte][int]$ch, 0, [UIntPtr]::Zero)
|
|
340
|
+
[AgentForgeForegroundKeyQueue]::keybd_event(0xE7, [byte][int]$ch, 2, [UIntPtr]::Zero)
|
|
341
|
+
}
|
|
342
|
+
} elseif ($payload.action.kind -eq 'key') {
|
|
343
|
+
$keyText = ([string]$payload.action.key).Trim()
|
|
344
|
+
$mods = @()
|
|
345
|
+
$base = $null
|
|
346
|
+
foreach ($p in ($keyText -split '\\+')) {
|
|
347
|
+
$lp = $p.ToLower()
|
|
348
|
+
if ($modMap.ContainsKey($lp)) { $mods += $modMap[$lp] } else { $base = $lp }
|
|
349
|
+
}
|
|
350
|
+
$vk = $null
|
|
351
|
+
if ($null -ne $base -and $vkMap.ContainsKey($base)) { $vk = $vkMap[$base] }
|
|
352
|
+
elseif ($null -ne $base -and $base.Length -eq 1) {
|
|
353
|
+
$upper = [char]::ToUpper($base[0])
|
|
354
|
+
$code = [int]$upper
|
|
355
|
+
if (($code -ge 0x20 -and $code -le 0x5A) -or ($code -ge 0x30 -and $code -le 0x39)) { $vk = $code }
|
|
356
|
+
}
|
|
357
|
+
if ($null -eq $vk) { throw ('Unsupported key: ' + $keyText) }
|
|
358
|
+
foreach ($m in $mods) { [AgentForgeForegroundKeyQueue]::keybd_event([byte]$m, 0, 0, [UIntPtr]::Zero) }
|
|
359
|
+
[AgentForgeForegroundKeyQueue]::keybd_event([byte]$vk, 0, 0, [UIntPtr]::Zero)
|
|
360
|
+
[AgentForgeForegroundKeyQueue]::keybd_event([byte]$vk, 0, 2, [UIntPtr]::Zero)
|
|
361
|
+
for ($i = $mods.Count - 1; $i -ge 0; $i--) { [AgentForgeForegroundKeyQueue]::keybd_event([byte]$mods[$i], 0, 2, [UIntPtr]::Zero) }
|
|
362
|
+
} else { throw 'Foreground input supports only type and key' }
|
|
363
|
+
[pscustomobject]@{ ok=$true } | ConvertTo-Json -Compress
|
|
364
|
+
} catch {
|
|
365
|
+
[pscustomobject]@{ ok=$false; code='action_failed'; reason=[string]$_.Exception.Message } | ConvertTo-Json -Compress
|
|
366
|
+
}`;
|
|
367
|
+
const COORDINATE_CLICK_SCRIPT = `
|
|
368
|
+
try {
|
|
369
|
+
$payload = $env:AGENT_FORGE_UIA_INPUT | ConvertFrom-Json
|
|
370
|
+
$x = [int]$payload.target.x
|
|
371
|
+
$y = [int]$payload.target.y
|
|
372
|
+
Add-Type @'
|
|
373
|
+
using System;
|
|
374
|
+
using System.Runtime.InteropServices;
|
|
375
|
+
public static class AgentForgeCoordinateClick {
|
|
376
|
+
[DllImport("user32.dll", SetLastError=true)]
|
|
377
|
+
public static extern bool SetCursorPos(int X, int Y);
|
|
378
|
+
[DllImport("user32.dll")]
|
|
379
|
+
public static extern void mouse_event(uint dwFlags, int dx, int dy, int dwData, UIntPtr dwExtraInfo);
|
|
380
|
+
}
|
|
381
|
+
'@
|
|
382
|
+
if (-not [AgentForgeCoordinateClick]::SetCursorPos($x, $y)) { throw 'Coordinate click could not move the cursor' }
|
|
383
|
+
[AgentForgeCoordinateClick]::mouse_event(0x0002, 0, 0, 0, [UIntPtr]::Zero)
|
|
384
|
+
[AgentForgeCoordinateClick]::mouse_event(0x0004, 0, 0, 0, [UIntPtr]::Zero)
|
|
385
|
+
[pscustomobject]@{ ok=$true } | ConvertTo-Json -Compress
|
|
386
|
+
} catch {
|
|
387
|
+
[pscustomobject]@{ ok=$false; reason='uia_action_exception' } | ConvertTo-Json -Compress
|
|
388
|
+
}`;
|
|
389
|
+
const SCREENSHOT_SCRIPT = `
|
|
390
|
+
try {
|
|
391
|
+
Add-Type @'
|
|
392
|
+
using System;
|
|
393
|
+
using System.Runtime.InteropServices;
|
|
394
|
+
public static class AgentForgeScreenDpi {
|
|
395
|
+
[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
|
|
396
|
+
}
|
|
397
|
+
'@
|
|
398
|
+
[void][AgentForgeScreenDpi]::SetProcessDPIAware()
|
|
399
|
+
Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop
|
|
400
|
+
Add-Type -AssemblyName System.Drawing -ErrorAction Stop
|
|
401
|
+
$bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
|
|
402
|
+
$originalWidth = [int]$bounds.Width
|
|
403
|
+
$originalHeight = [int]$bounds.Height
|
|
404
|
+
if ($originalWidth -le 0 -or $originalHeight -le 0) { throw 'Primary display bounds are unavailable' }
|
|
405
|
+
$width = if ($originalWidth -gt ${DESKTOP_SCREENSHOT_MAX_WIDTH_PX}) { ${DESKTOP_SCREENSHOT_MAX_WIDTH_PX} } else { $originalWidth }
|
|
406
|
+
$height = [int][math]::Round([double]$originalHeight * $width / $originalWidth)
|
|
407
|
+
if ($height -lt 1) { $height = 1 }
|
|
408
|
+
$source = New-Object System.Drawing.Bitmap($originalWidth, $originalHeight)
|
|
409
|
+
$target = $source
|
|
410
|
+
try {
|
|
411
|
+
$graphics = [System.Drawing.Graphics]::FromImage($source)
|
|
412
|
+
try { $graphics.CopyFromScreen($bounds.X, $bounds.Y, 0, 0, [System.Drawing.Size]::new($originalWidth, $originalHeight)) } finally { $graphics.Dispose() }
|
|
413
|
+
if ($width -ne $originalWidth -or $height -ne $originalHeight) {
|
|
414
|
+
$target = New-Object System.Drawing.Bitmap($width, $height)
|
|
415
|
+
$resized = [System.Drawing.Graphics]::FromImage($target)
|
|
416
|
+
try {
|
|
417
|
+
$resized.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
|
418
|
+
$resized.DrawImage($source, 0, 0, $width, $height)
|
|
419
|
+
} finally { $resized.Dispose() }
|
|
420
|
+
}
|
|
421
|
+
$stream = New-Object System.IO.MemoryStream
|
|
422
|
+
try {
|
|
423
|
+
$target.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
424
|
+
[pscustomobject]@{ ok=$true; data=[Convert]::ToBase64String($stream.ToArray()); width=$width; height=$height; originalWidth=$originalWidth; originalHeight=$originalHeight } | ConvertTo-Json -Compress -Depth 4
|
|
425
|
+
} finally { $stream.Dispose() }
|
|
426
|
+
} finally {
|
|
427
|
+
if (-not [object]::ReferenceEquals($target, $source)) { $target.Dispose() }
|
|
428
|
+
$source.Dispose()
|
|
429
|
+
}
|
|
430
|
+
} catch {
|
|
431
|
+
[pscustomobject]@{ ok=$false; reason='screenshot_exception' } | ConvertTo-Json -Compress -Depth 4
|
|
432
|
+
}`;
|
|
285
433
|
const parseJson = (stdout) => {
|
|
286
434
|
try {
|
|
287
435
|
const parsed = JSON.parse(stdout.trim());
|
|
@@ -390,23 +538,49 @@ export class WindowsUiAutomationDriver {
|
|
|
390
538
|
};
|
|
391
539
|
if (!SUPPORTED_ACTIONS.includes(request.action.kind))
|
|
392
540
|
return unsupported(request.operationId, "The selected Windows UI Automation adapter does not support this action", "action-unsupported");
|
|
393
|
-
|
|
541
|
+
const postconditions = request.postconditions ?? [];
|
|
542
|
+
if (postconditions.length > 0 && request.reobserveAfterAction === false)
|
|
543
|
+
return this.failed(request.operationId, "reobserve_required", "Postconditions require an action re-observation");
|
|
544
|
+
// Coordinate clicks address raw screen pixels; they bypass observed target
|
|
545
|
+
// identity entirely and are click-only by contract.
|
|
546
|
+
if (request.action.target !== undefined && request.action.target.kind === "coordinates") {
|
|
547
|
+
if (request.action.kind !== "click")
|
|
548
|
+
return unsupported(request.operationId, "The selected Windows UI Automation adapter does not support this action", "action-unsupported");
|
|
549
|
+
const raw = this.run(COORDINATE_CLICK_SCRIPT, {
|
|
550
|
+
action: request.action,
|
|
551
|
+
target: request.action.target,
|
|
552
|
+
});
|
|
553
|
+
return this.completeAction(request, postconditions, raw);
|
|
554
|
+
}
|
|
555
|
+
// No observed target: type/key go to the real keyboard queue addressed at
|
|
556
|
+
// the current focus — the only path that reaches UIA-invisible surfaces
|
|
557
|
+
// like the native file dialog.
|
|
558
|
+
if (request.action.target === undefined) {
|
|
559
|
+
if (request.action.kind === "type" && typeof request.action.text === "string") {
|
|
560
|
+
const raw = this.run(FOREGROUND_INPUT_SCRIPT, { action: request.action });
|
|
561
|
+
return this.completeAction(request, postconditions, raw);
|
|
562
|
+
}
|
|
563
|
+
if (request.action.kind === "key" && typeof request.action.key === "string") {
|
|
564
|
+
const raw = this.run(FOREGROUND_INPUT_SCRIPT, { action: request.action });
|
|
565
|
+
return this.completeAction(request, postconditions, raw);
|
|
566
|
+
}
|
|
394
567
|
return this.failed(request.operationId, "target_required", "This desktop action requires an observed target");
|
|
568
|
+
}
|
|
395
569
|
if (request.action.kind === "type" && typeof request.action.text !== "string")
|
|
396
570
|
return this.failed(request.operationId, "text_required", "Type action requires text");
|
|
397
571
|
if (request.action.kind === "key" && typeof request.action.key !== "string")
|
|
398
572
|
return this.failed(request.operationId, "key_required", "Key action requires a key sequence");
|
|
399
573
|
if (request.action.kind === "scroll" && typeof request.action.text !== "string")
|
|
400
574
|
return this.failed(request.operationId, "direction_required", "Scroll action requires an up/down direction");
|
|
401
|
-
const postconditions = request.postconditions ?? [];
|
|
402
|
-
if (postconditions.length > 0 && request.reobserveAfterAction === false)
|
|
403
|
-
return this.failed(request.operationId, "reobserve_required", "Postconditions require an action re-observation");
|
|
404
575
|
const target = this.resolveTarget(request.sessionId, request.action.target);
|
|
405
576
|
if (target === undefined)
|
|
406
577
|
return this.failed(request.operationId, this.observedSessions.has(request.sessionId) ? "stale_target" : "target_not_found", this.observedSessions.has(request.sessionId)
|
|
407
578
|
? "Target identity is stale; reobserve the desktop before retrying"
|
|
408
579
|
: "Target identity was not observed by UI Automation");
|
|
409
580
|
const raw = this.run(ACTION_SCRIPT, { action: request.action, target: target ?? {} });
|
|
581
|
+
return this.completeAction(request, postconditions, raw);
|
|
582
|
+
}
|
|
583
|
+
completeAction(request, postconditions, raw) {
|
|
410
584
|
if (raw === undefined)
|
|
411
585
|
return unsupported(request.operationId, "Windows UI Automation action failed");
|
|
412
586
|
if (raw.ok !== true) {
|
|
@@ -464,7 +638,63 @@ export class WindowsUiAutomationDriver {
|
|
|
464
638
|
this.invalidateObservation(sessionId);
|
|
465
639
|
return stopped;
|
|
466
640
|
}
|
|
467
|
-
|
|
641
|
+
/** Capture the primary display through GDI; the PNG is scaled to a 1280px width cap. */
|
|
642
|
+
screenshot() {
|
|
643
|
+
if (this.platform !== "win32")
|
|
644
|
+
return {
|
|
645
|
+
version: 1,
|
|
646
|
+
source: "windows-gdi",
|
|
647
|
+
status: "unsupported",
|
|
648
|
+
error: error("platform_unavailable", "Windows GDI screen capture requires win32", "platform-unavailable"),
|
|
649
|
+
};
|
|
650
|
+
const raw = this.run(SCREENSHOT_SCRIPT, undefined, SCREENSHOT_SPAWN_MAX_BUFFER_BYTES);
|
|
651
|
+
if (raw === undefined)
|
|
652
|
+
return {
|
|
653
|
+
version: 1,
|
|
654
|
+
source: "windows-gdi",
|
|
655
|
+
status: "failed",
|
|
656
|
+
error: error("screenshot_failed", "Windows GDI screen capture failed", "screenshot-capture-unavailable"),
|
|
657
|
+
};
|
|
658
|
+
if (raw.ok !== true)
|
|
659
|
+
return {
|
|
660
|
+
version: 1,
|
|
661
|
+
source: "windows-gdi",
|
|
662
|
+
status: "failed",
|
|
663
|
+
error: error("screenshot_failed", "Windows GDI screen capture failed", "screenshot-exception"),
|
|
664
|
+
};
|
|
665
|
+
const data = typeof raw.data === "string" ? raw.data : "";
|
|
666
|
+
const dimensions = {
|
|
667
|
+
width: screenshotDimension(raw.width),
|
|
668
|
+
height: screenshotDimension(raw.height),
|
|
669
|
+
originalWidth: screenshotDimension(raw.originalWidth),
|
|
670
|
+
originalHeight: screenshotDimension(raw.originalHeight),
|
|
671
|
+
};
|
|
672
|
+
if (Math.floor((data.length * 3) / 4) > DESKTOP_SCREENSHOT_MAX_DECODED_BYTES)
|
|
673
|
+
return {
|
|
674
|
+
version: 1,
|
|
675
|
+
source: "windows-gdi",
|
|
676
|
+
status: "failed",
|
|
677
|
+
error: error("screenshot-too-large", "The captured screenshot exceeds the 4 MiB decoded-size limit", "screenshot-too-large"),
|
|
678
|
+
};
|
|
679
|
+
if (data.length === 0 || Object.values(dimensions).some((value) => value <= 0))
|
|
680
|
+
return {
|
|
681
|
+
version: 1,
|
|
682
|
+
source: "windows-gdi",
|
|
683
|
+
status: "failed",
|
|
684
|
+
error: error("screenshot_failed", "Windows GDI screen capture returned an invalid payload", "screenshot-invalid-payload"),
|
|
685
|
+
};
|
|
686
|
+
return {
|
|
687
|
+
version: 1,
|
|
688
|
+
source: "windows-gdi",
|
|
689
|
+
status: "succeeded",
|
|
690
|
+
mimeType: "image/png",
|
|
691
|
+
data,
|
|
692
|
+
...dimensions,
|
|
693
|
+
scale: dimensions.originalWidth / dimensions.width,
|
|
694
|
+
capturedAt: new Date().toISOString(),
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
run(script, payload, maxBuffer) {
|
|
468
698
|
if (this.platform !== "win32")
|
|
469
699
|
return undefined;
|
|
470
700
|
const env = {};
|
|
@@ -475,7 +705,13 @@ export class WindowsUiAutomationDriver {
|
|
|
475
705
|
}
|
|
476
706
|
if (payload !== undefined)
|
|
477
707
|
env.AGENT_FORGE_UIA_INPUT = JSON.stringify(payload);
|
|
478
|
-
const result = this.spawn(this.command, ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], {
|
|
708
|
+
const result = this.spawn(this.command, ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], {
|
|
709
|
+
encoding: "utf8",
|
|
710
|
+
timeout: this.timeoutMs,
|
|
711
|
+
windowsHide: true,
|
|
712
|
+
env,
|
|
713
|
+
...(maxBuffer === undefined ? {} : { maxBuffer }),
|
|
714
|
+
});
|
|
479
715
|
if (result.error !== undefined || result.status !== 0)
|
|
480
716
|
return undefined;
|
|
481
717
|
return parseJson(result.stdout);
|
|
@@ -563,6 +799,8 @@ export class WindowsUiAutomationDriver {
|
|
|
563
799
|
return observation;
|
|
564
800
|
}
|
|
565
801
|
resolveTarget(sessionId, target) {
|
|
802
|
+
if (target.kind === "coordinates")
|
|
803
|
+
return undefined;
|
|
566
804
|
const identity = target.kind === "control" ? target.controlId : target.windowId;
|
|
567
805
|
const record = this.targets.get(this.targetKey(sessionId, target.kind, identity));
|
|
568
806
|
if (record === undefined ||
|
|
@@ -612,6 +850,8 @@ export class WindowsUiAutomationDriver {
|
|
|
612
850
|
return false;
|
|
613
851
|
return window.title === condition.expected;
|
|
614
852
|
}
|
|
853
|
+
if (target.kind !== "control")
|
|
854
|
+
return false;
|
|
615
855
|
const control = observation.controls.find((item) => {
|
|
616
856
|
if (item.controlId !== target.controlId)
|
|
617
857
|
return false;
|
|
@@ -674,6 +914,18 @@ export class DesktopWindowsAdapter {
|
|
|
674
914
|
stop(sessionId) {
|
|
675
915
|
return this.driver.stop(sessionId);
|
|
676
916
|
}
|
|
917
|
+
/** Delegates to the driver when it implements DesktopScreenshotDriverV1; structured unsupported otherwise. */
|
|
918
|
+
screenshot() {
|
|
919
|
+
const driver = this.driver;
|
|
920
|
+
if (typeof driver.screenshot !== "function")
|
|
921
|
+
return {
|
|
922
|
+
version: 1,
|
|
923
|
+
source: "windows-gdi",
|
|
924
|
+
status: "unsupported",
|
|
925
|
+
error: error("driver_unavailable", "The selected desktop driver does not support screenshots", "screenshot-driver-unavailable"),
|
|
926
|
+
};
|
|
927
|
+
return driver.screenshot();
|
|
928
|
+
}
|
|
677
929
|
replay(request) {
|
|
678
930
|
const mismatchedOperationId = findDesktopReplaySessionMismatch(request);
|
|
679
931
|
if (mismatchedOperationId !== undefined)
|