@anusornneal/chat-relay 0.8.0 → 0.9.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.
@@ -1,508 +1,582 @@
1
- param()
2
-
3
- $ErrorActionPreference = "Stop"
4
- $ProgressPreference = "SilentlyContinue"
5
-
6
- Add-Type -AssemblyName System.Windows.Forms
7
- Add-Type -AssemblyName System.Drawing
8
-
9
- Add-Type @"
10
- using System;
11
- using System.Collections.Generic;
12
- using System.Runtime.InteropServices;
13
- using System.Text;
14
-
15
- public static class ChatRelayNative {
16
- public const uint INPUT_MOUSE = 0;
17
- public const uint INPUT_KEYBOARD = 1;
18
- public const uint KEYEVENTF_KEYUP = 0x0002;
19
- public const uint KEYEVENTF_UNICODE = 0x0004;
20
-
21
- [StructLayout(LayoutKind.Sequential)]
22
- public struct MOUSEINPUT {
23
- public int dx;
24
- public int dy;
25
- public uint mouseData;
26
- public uint dwFlags;
27
- public uint time;
28
- public UIntPtr dwExtraInfo;
29
- }
30
-
31
- [StructLayout(LayoutKind.Sequential)]
32
- public struct KEYBDINPUT {
33
- public ushort wVk;
34
- public ushort wScan;
35
- public uint dwFlags;
36
- public uint time;
37
- public UIntPtr dwExtraInfo;
38
- }
39
-
40
- [StructLayout(LayoutKind.Explicit)]
41
- public struct INPUTUNION {
42
- [FieldOffset(0)] public MOUSEINPUT mi;
43
- [FieldOffset(0)] public KEYBDINPUT ki;
44
- }
45
-
46
- [StructLayout(LayoutKind.Sequential)]
47
- public struct INPUT {
48
- public uint type;
49
- public INPUTUNION U;
50
- }
51
-
52
- [StructLayout(LayoutKind.Sequential)]
53
- public struct RECT {
54
- public int Left;
55
- public int Top;
56
- public int Right;
57
- public int Bottom;
58
- }
59
-
60
- public sealed class WindowInfo {
61
- public string WindowId;
62
- public uint ProcessId;
63
- public string Title;
64
- public int X;
65
- public int Y;
66
- public int Width;
67
- public int Height;
68
- public bool IsForeground;
69
- }
70
-
71
- public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
72
-
73
- [DllImport("user32.dll")] public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
74
- [DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
75
- [DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
76
- [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
77
- [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc callback, IntPtr lParam);
78
- [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
79
- [DllImport("user32.dll")] public static extern bool IsWindow(IntPtr hWnd);
80
- [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowTextLength(IntPtr hWnd);
81
- [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int maxCount);
82
- [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
83
- [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
84
- [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
85
- [DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int command);
86
-
87
- public static WindowInfo[] ListWindows(int limit) {
88
- var items = new List<WindowInfo>();
89
- var foreground = GetForegroundWindow();
90
- EnumWindows(delegate(IntPtr hWnd, IntPtr lParam) {
91
- if (items.Count >= limit) return false;
92
- if (!IsWindowVisible(hWnd)) return true;
93
- int titleLength = GetWindowTextLength(hWnd);
94
- if (titleLength <= 0) return true;
95
- var title = new StringBuilder(titleLength + 1);
96
- if (GetWindowText(hWnd, title, title.Capacity) <= 0) return true;
97
- uint pid;
98
- GetWindowThreadProcessId(hWnd, out pid);
99
- RECT rect;
100
- if (!GetWindowRect(hWnd, out rect)) return true;
101
- items.Add(new WindowInfo {
102
- WindowId = unchecked((ulong)hWnd.ToInt64()).ToString(),
103
- ProcessId = pid,
104
- Title = title.ToString(),
105
- X = rect.Left,
106
- Y = rect.Top,
107
- Width = Math.Max(0, rect.Right - rect.Left),
108
- Height = Math.Max(0, rect.Bottom - rect.Top),
109
- IsForeground = hWnd == foreground,
110
- });
111
- return true;
112
- }, IntPtr.Zero);
113
- return items.ToArray();
114
- }
115
-
116
- public static bool FocusWindow(string windowId) {
117
- ulong raw;
118
- if (!UInt64.TryParse(windowId, out raw) || raw == 0) return false;
119
- var hWnd = new IntPtr(unchecked((long)raw));
120
- if (!IsWindow(hWnd)) return false;
121
- ShowWindowAsync(hWnd, 9);
122
- return SetForegroundWindow(hWnd);
123
- }
124
-
125
- public static bool MouseButton(uint downFlag, uint upFlag) {
126
- var inputs = new INPUT[2];
127
- inputs[0].type = INPUT_MOUSE;
128
- inputs[0].U.mi.dwFlags = downFlag;
129
- inputs[1].type = INPUT_MOUSE;
130
- inputs[1].U.mi.dwFlags = upFlag;
131
- return SendInput(2, inputs, Marshal.SizeOf(typeof(INPUT))) == 2;
132
- }
133
-
134
- public static bool Key(ushort virtualKey, bool down) {
135
- var input = new INPUT[1];
136
- input[0].type = INPUT_KEYBOARD;
137
- input[0].U.ki.wVk = virtualKey;
138
- input[0].U.ki.dwFlags = down ? 0u : KEYEVENTF_KEYUP;
139
- return SendInput(1, input, Marshal.SizeOf(typeof(INPUT))) == 1;
140
- }
141
-
142
- public static bool UnicodeText(string text) {
143
- foreach (char c in text) {
144
- var inputs = new INPUT[2];
145
- inputs[0].type = INPUT_KEYBOARD;
146
- inputs[0].U.ki.wScan = c;
147
- inputs[0].U.ki.dwFlags = KEYEVENTF_UNICODE;
148
- inputs[1].type = INPUT_KEYBOARD;
149
- inputs[1].U.ki.wScan = c;
150
- inputs[1].U.ki.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP;
151
- if (SendInput(2, inputs, Marshal.SizeOf(typeof(INPUT))) != 2) return false;
152
- }
153
- return true;
154
- }
155
- }
156
- "@
157
-
158
- [ChatRelayNative]::SetProcessDPIAware() | Out-Null
159
-
160
- function Result-Error([string]$Code) {
161
- return [pscustomobject]@{ ok = $false; error = $Code }
162
- }
163
-
164
- function Test-InteractiveSession {
165
- return [System.Windows.Forms.SystemInformation]::UserInteractive -and
166
- ([ChatRelayNative]::GetForegroundWindow() -ne [IntPtr]::Zero)
167
- }
168
-
169
- function Resolve-Screen($Monitor) {
170
- $screens = @([System.Windows.Forms.Screen]::AllScreens)
171
- if ($screens.Count -eq 0) { return $null }
172
-
173
- if ($null -eq $Monitor -or [string]$Monitor -eq "primary") {
174
- for ($i = 0; $i -lt $screens.Count; $i++) {
175
- if ($screens[$i].Primary) {
176
- return [pscustomobject]@{ screen = $screens[$i]; index = $i }
177
- }
178
- }
179
- return $null
180
- }
181
-
182
- if ([string]$Monitor -eq "secondary") {
183
- for ($i = 0; $i -lt $screens.Count; $i++) {
184
- if (-not $screens[$i].Primary) {
185
- return [pscustomobject]@{ screen = $screens[$i]; index = $i }
186
- }
187
- }
188
- return $null
189
- }
190
-
191
- $parsedIndex = 0
192
- if (-not [int]::TryParse([string]$Monitor, [ref]$parsedIndex)) { return $null }
193
- if ($parsedIndex -lt 0 -or $parsedIndex -ge $screens.Count) { return $null }
194
- return [pscustomobject]@{ screen = $screens[$parsedIndex]; index = $parsedIndex }
195
- }
196
-
197
- function Invoke-Screenshot($Payload) {
198
- if (-not [System.Windows.Forms.SystemInformation]::UserInteractive) {
199
- return Result-Error "session_unavailable"
200
- }
201
-
202
- $resolved = Resolve-Screen $Payload.monitor
203
- if ($null -eq $resolved) { return Result-Error "monitor_not_found" }
204
-
205
- $screen = $resolved.screen
206
- $bounds = $screen.Bounds
207
- if ($bounds.Width -le 0 -or $bounds.Height -le 0) {
208
- return Result-Error "session_unavailable"
209
- }
210
-
211
- $source = $null
212
- try {
213
- $source = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
214
- $graphics = [System.Drawing.Graphics]::FromImage($source)
215
- try {
216
- $graphics.CopyFromScreen($bounds.Left, $bounds.Top, 0, 0, $bounds.Size)
217
- } finally {
218
- $graphics.Dispose()
219
- }
220
-
221
- $codec = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() |
222
- Where-Object { $_.MimeType -eq "image/jpeg" } |
223
- Select-Object -First 1
224
- if ($null -eq $codec) { return Result-Error "capture_failed" }
225
-
226
- $maxBytes = 32768
227
- $requestedMaxWidth = if ($null -eq $Payload.maxWidth) { 960 } else { [int]$Payload.maxWidth }
228
- if ($requestedMaxWidth -lt 320 -or $requestedMaxWidth -gt 1920) { return Result-Error "invalid_max_width" }
229
- $requestedQuality = if ($null -eq $Payload.quality) { 58 } else { [int]$Payload.quality }
230
- if ($requestedQuality -lt 20 -or $requestedQuality -gt 85) { return Result-Error "invalid_quality" }
231
- $targetWidth = [Math]::Min($bounds.Width, $requestedMaxWidth)
232
- $targetHeight = [Math]::Max(1, [int][Math]::Round($bounds.Height * ($targetWidth / [double]$bounds.Width)))
233
- $qualities = @([long]$requestedQuality, [long][Math]::Min($requestedQuality, 42), [long][Math]::Min($requestedQuality, 30), 20L) | Select-Object -Unique
234
- $bytes = $null
235
- $finalWidth = $targetWidth
236
- $finalHeight = $targetHeight
237
- $finalQuality = $requestedQuality
238
-
239
- for ($sizeAttempt = 0; $sizeAttempt -lt 3 -and $null -eq $bytes; $sizeAttempt++) {
240
- $resized = New-Object System.Drawing.Bitmap $targetWidth, $targetHeight
241
- $draw = [System.Drawing.Graphics]::FromImage($resized)
242
- try {
243
- $draw.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighSpeed
244
- $draw.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::Bilinear
245
- $draw.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighSpeed
246
- $draw.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighSpeed
247
- $draw.DrawImage($source, 0, 0, $targetWidth, $targetHeight)
248
- } finally {
249
- $draw.Dispose()
250
- }
251
-
252
- try {
253
- foreach ($quality in $qualities) {
254
- $stream = New-Object System.IO.MemoryStream
255
- $parameters = New-Object System.Drawing.Imaging.EncoderParameters 1
256
- $parameters.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter(
257
- [System.Drawing.Imaging.Encoder]::Quality,
258
- $quality
259
- )
260
- try {
261
- $resized.Save($stream, $codec, $parameters)
262
- if ($stream.Length -le $maxBytes) {
263
- $bytes = $stream.ToArray()
264
- $finalWidth = $targetWidth
265
- $finalHeight = $targetHeight
266
- $finalQuality = [int]$quality
267
- break
268
- }
269
- } finally {
270
- $parameters.Dispose()
271
- $stream.Dispose()
272
- }
273
- }
274
- } finally {
275
- $resized.Dispose()
276
- }
277
-
278
- if ($null -eq $bytes) {
279
- $targetWidth = [Math]::Max(320, [int][Math]::Floor($targetWidth * 0.78))
280
- $targetHeight = [Math]::Max(180, [int][Math]::Round($bounds.Height * ($targetWidth / [double]$bounds.Width)))
281
- }
282
- }
283
-
284
- if ($null -eq $bytes) { return Result-Error "screenshot_too_large" }
285
- $virtualBounds = [System.Windows.Forms.SystemInformation]::VirtualScreen
286
- $monitorCount = @([System.Windows.Forms.Screen]::AllScreens).Count
287
-
288
- return [pscustomobject]@{
289
- ok = $true
290
- mimeType = "image/jpeg"
291
- data = [Convert]::ToBase64String($bytes)
292
- width = $finalWidth
293
- height = $finalHeight
294
- virtualDesktopOriginX = $virtualBounds.Left
295
- virtualDesktopOriginY = $virtualBounds.Top
296
- virtualDesktopWidth = $virtualBounds.Width
297
- virtualDesktopHeight = $virtualBounds.Height
298
- monitorCount = $monitorCount
299
- jpegQuality = $finalQuality
300
- desktopOriginX = $bounds.Left
301
- desktopOriginY = $bounds.Top
302
- desktopWidth = $bounds.Width
303
- desktopHeight = $bounds.Height
304
- scaleX = $bounds.Width / [double]$finalWidth
305
- scaleY = $bounds.Height / [double]$finalHeight
306
- monitorIndex = $resolved.index
307
- isPrimary = [bool]$screen.Primary
308
- deviceName = [string]$screen.DeviceName
309
- byteLength = $bytes.Length
310
- }
311
- } catch {
312
- return Result-Error "capture_failed"
313
- } finally {
314
- if ($null -ne $source) { $source.Dispose() }
315
- }
316
- }
317
-
318
- function Invoke-MouseClick($Payload) {
319
- if (-not (Test-InteractiveSession)) { return Result-Error "session_unavailable" }
320
-
321
- $x = [int]$Payload.x
322
- $y = [int]$Payload.y
323
- $containsPoint = $false
324
- foreach ($screen in @([System.Windows.Forms.Screen]::AllScreens)) {
325
- if ($screen.Bounds.Contains($x, $y)) {
326
- $containsPoint = $true
327
- break
328
- }
329
- }
330
- if (-not $containsPoint) { return Result-Error "invalid_coordinates" }
331
-
332
- switch ([string]$Payload.button) {
333
- "left" { $down = 0x0002; $up = 0x0004 }
334
- "right" { $down = 0x0008; $up = 0x0010 }
335
- "middle" { $down = 0x0020; $up = 0x0040 }
336
- default { return Result-Error "invalid_button" }
337
- }
338
-
339
- if (-not [ChatRelayNative]::SetCursorPos($x, $y)) { return Result-Error "input_failed" }
340
-
341
- $clicks = [int]$Payload.clicks
342
- for ($i = 0; $i -lt $clicks; $i++) {
343
- if (-not [ChatRelayNative]::MouseButton($down, $up)) { return Result-Error "input_failed" }
344
- if ($i + 1 -lt $clicks) { Start-Sleep -Milliseconds 100 }
345
- }
346
-
347
- return [pscustomobject]@{ ok = $true; action = "mouse_click" }
348
- }
349
-
350
- function Invoke-KeyboardInput($Payload) {
351
- if (-not (Test-InteractiveSession)) { return Result-Error "session_unavailable" }
352
-
353
- if ($null -ne $Payload.text) {
354
- if (-not [ChatRelayNative]::UnicodeText([string]$Payload.text)) {
355
- return Result-Error "input_failed"
356
- }
357
- } else {
358
- $mods = New-Object System.Collections.Generic.List[uint16]
359
- if ($Payload.ctrl) { $mods.Add(0x11) }
360
- if ($Payload.alt) { $mods.Add(0x12) }
361
- if ($Payload.shift) { $mods.Add(0x10) }
362
- if ($Payload.win) { $mods.Add(0x5B) }
363
-
364
- foreach ($vk in $mods) {
365
- if (-not [ChatRelayNative]::Key($vk, $true)) { return Result-Error "input_failed" }
366
- }
367
-
368
- try {
369
- $keyCode = [uint16]$Payload.keyCode
370
- if (-not [ChatRelayNative]::Key($keyCode, $true)) { return Result-Error "input_failed" }
371
- if (-not [ChatRelayNative]::Key($keyCode, $false)) { return Result-Error "input_failed" }
372
- } finally {
373
- for ($i = $mods.Count - 1; $i -ge 0; $i--) {
374
- [ChatRelayNative]::Key($mods[$i], $false) | Out-Null
375
- }
376
- }
377
- }
378
-
379
- return [pscustomobject]@{ ok = $true; action = "keyboard_input" }
380
- }
381
-
382
- function Invoke-ClipboardRead($Payload) {
383
- if (-not [System.Windows.Forms.SystemInformation]::UserInteractive) { return Result-Error "session_unavailable" }
384
- try {
385
- $text = if ([System.Windows.Forms.Clipboard]::ContainsText()) { [System.Windows.Forms.Clipboard]::GetText() } else { "" }
386
- $truncated = $text.Length -gt 8192
387
- if ($truncated) { $text = $text.Substring(0, 8192) }
388
- return [pscustomobject]@{ ok = $true; text = $text; truncated = $truncated }
389
- } catch { return Result-Error "clipboard_failed" }
390
- }
391
-
392
- function Invoke-ClipboardWrite($Payload) {
393
- if (-not (Test-InteractiveSession)) { return Result-Error "session_unavailable" }
394
- try {
395
- $text = [string]$Payload.text
396
- if ($text.Length -gt 8192) { return Result-Error "text_too_large" }
397
- if ($text.Length -eq 0) { [System.Windows.Forms.Clipboard]::Clear() } else { [System.Windows.Forms.Clipboard]::SetText($text) }
398
- return [pscustomobject]@{ ok = $true; action = "clipboard_write"; length = $text.Length }
399
- } catch { return Result-Error "clipboard_failed" }
400
- }
401
-
402
- function Invoke-WindowList($Payload) {
403
- if (-not [System.Windows.Forms.SystemInformation]::UserInteractive) { return Result-Error "session_unavailable" }
404
- $limit = if ($null -eq $Payload.limit) { 50 } else { [int]$Payload.limit }
405
- if ($limit -lt 1 -or $limit -gt 100) { return Result-Error "invalid_window_limit" }
406
- try {
407
- $windows = @([ChatRelayNative]::ListWindows($limit))
408
- return [pscustomobject]@{ ok = $true; windows = $windows; count = $windows.Count }
409
- } catch { return Result-Error "window_list_failed" }
410
- }
411
-
412
- function Invoke-WindowFocus($Payload) {
413
- if (-not (Test-InteractiveSession)) { return Result-Error "session_unavailable" }
414
- $windowId = [string]$Payload.windowId
415
- if ($windowId -notmatch '^[1-9][0-9]{0,19}$') { return Result-Error "invalid_window_id" }
416
- try {
417
- if (-not [ChatRelayNative]::FocusWindow($windowId)) { return Result-Error "focus_failed" }
418
- return [pscustomobject]@{ ok = $true; action = "window_focus"; windowId = $windowId }
419
- } catch { return Result-Error "focus_failed" }
420
- }
421
-
422
- function Invoke-Step($Payload) {
423
- $completed = 0
424
- foreach ($action in @($Payload.actions)) {
425
- $result = $null
426
- switch ([string]$action.type) {
427
- "click" { $result = Invoke-MouseClick $action }
428
- "text" { $result = Invoke-KeyboardInput $action }
429
- "key" { $result = Invoke-KeyboardInput $action }
430
- "wait" {
431
- Start-Sleep -Milliseconds ([int]$action.ms)
432
- $result = [pscustomobject]@{ ok = $true; action = "wait" }
433
- }
434
- default { $result = Result-Error "invalid_step_action" }
435
- }
436
-
437
- if (-not $result.ok) {
438
- return [pscustomobject]@{
439
- ok = $false
440
- error = $result.error
441
- actionsCompleted = $completed
442
- }
443
- }
444
- $completed++
445
- }
446
-
447
- if ([int]$Payload.settleMs -gt 0) {
448
- Start-Sleep -Milliseconds ([int]$Payload.settleMs)
449
- }
450
-
451
- if ($Payload.captureAfter -eq $false) {
452
- return [pscustomobject]@{
453
- ok = $true
454
- action = "step"
455
- actionsCompleted = $completed
456
- }
457
- }
458
-
459
- $shotPayload = [pscustomobject]@{ monitor = $Payload.monitor; maxWidth = $Payload.maxWidth; quality = $Payload.quality }
460
- $shot = Invoke-Screenshot $shotPayload
461
- if (-not $shot.ok) {
462
- return [pscustomobject]@{
463
- ok = $false
464
- error = $shot.error
465
- actionsCompleted = $completed
466
- }
467
- }
468
-
469
- $shot | Add-Member -NotePropertyName action -NotePropertyValue "step" -Force
470
- $shot | Add-Member -NotePropertyName actionsCompleted -NotePropertyValue $completed -Force
471
- return $shot
472
- }
473
-
474
- function Invoke-Operation([string]$Operation, $Payload) {
475
- switch ($Operation) {
476
- "screenshot" { return Invoke-Screenshot $Payload }
477
- "clipboard_read" { return Invoke-ClipboardRead $Payload }
478
- "clipboard_write" { return Invoke-ClipboardWrite $Payload }
479
- "window_list" { return Invoke-WindowList $Payload }
480
- "window_focus" { return Invoke-WindowFocus $Payload }
481
- "mouse_click" { return Invoke-MouseClick $Payload }
482
- "keyboard_input" { return Invoke-KeyboardInput $Payload }
483
- "step" { return Invoke-Step $Payload }
484
- default { return Result-Error "unknown_desktop_operation" }
485
- }
486
- }
487
-
488
- while (($line = [Console]::In.ReadLine()) -ne $null) {
489
- if ([string]::IsNullOrWhiteSpace($line)) { continue }
490
-
491
- $id = $null
492
- $result = $null
493
- try {
494
- $request = $line | ConvertFrom-Json
495
- $id = [string]$request.id
496
- $result = Invoke-Operation ([string]$request.operation) $request.args
497
- } catch {
498
- $result = Result-Error "desktop_worker_error"
499
- }
500
-
501
- $response = [pscustomobject]@{
502
- id = $id
503
- result = $result
504
- }
505
-
506
- [Console]::Out.WriteLine(($response | ConvertTo-Json -Compress -Depth 10))
507
- [Console]::Out.Flush()
508
- }
1
+ param()
2
+
3
+ $ErrorActionPreference = "Stop"
4
+ $ProgressPreference = "SilentlyContinue"
5
+
6
+ Add-Type -AssemblyName System.Windows.Forms
7
+ Add-Type -AssemblyName System.Drawing
8
+
9
+ Add-Type @"
10
+ using System;
11
+ using System.Collections.Generic;
12
+ using System.Runtime.InteropServices;
13
+ using System.Text;
14
+
15
+ public static class ChatRelayNative {
16
+ public const uint INPUT_MOUSE = 0;
17
+ public const uint INPUT_KEYBOARD = 1;
18
+ public const uint KEYEVENTF_KEYUP = 0x0002;
19
+ public const uint KEYEVENTF_UNICODE = 0x0004;
20
+
21
+ [StructLayout(LayoutKind.Sequential)]
22
+ public struct MOUSEINPUT {
23
+ public int dx;
24
+ public int dy;
25
+ public uint mouseData;
26
+ public uint dwFlags;
27
+ public uint time;
28
+ public UIntPtr dwExtraInfo;
29
+ }
30
+
31
+ [StructLayout(LayoutKind.Sequential)]
32
+ public struct KEYBDINPUT {
33
+ public ushort wVk;
34
+ public ushort wScan;
35
+ public uint dwFlags;
36
+ public uint time;
37
+ public UIntPtr dwExtraInfo;
38
+ }
39
+
40
+ [StructLayout(LayoutKind.Explicit)]
41
+ public struct INPUTUNION {
42
+ [FieldOffset(0)] public MOUSEINPUT mi;
43
+ [FieldOffset(0)] public KEYBDINPUT ki;
44
+ }
45
+
46
+ [StructLayout(LayoutKind.Sequential)]
47
+ public struct INPUT {
48
+ public uint type;
49
+ public INPUTUNION U;
50
+ }
51
+
52
+ [StructLayout(LayoutKind.Sequential)]
53
+ public struct RECT {
54
+ public int Left;
55
+ public int Top;
56
+ public int Right;
57
+ public int Bottom;
58
+ }
59
+
60
+ public sealed class WindowInfo {
61
+ public string WindowId;
62
+ public uint ProcessId;
63
+ public string Title;
64
+ public int X;
65
+ public int Y;
66
+ public int Width;
67
+ public int Height;
68
+ public bool IsForeground;
69
+ }
70
+
71
+ public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
72
+
73
+ [DllImport("user32.dll")] public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
74
+ [DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
75
+ [DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
76
+ [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
77
+ [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc callback, IntPtr lParam);
78
+ [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
79
+ [DllImport("user32.dll")] public static extern bool IsWindow(IntPtr hWnd);
80
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowTextLength(IntPtr hWnd);
81
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int maxCount);
82
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
83
+ [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
84
+ [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
85
+ [DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int command);
86
+
87
+ public static WindowInfo[] ListWindows(int limit) {
88
+ var items = new List<WindowInfo>();
89
+ var foreground = GetForegroundWindow();
90
+ EnumWindows(delegate(IntPtr hWnd, IntPtr lParam) {
91
+ if (items.Count >= limit) return false;
92
+ if (!IsWindowVisible(hWnd)) return true;
93
+ int titleLength = GetWindowTextLength(hWnd);
94
+ if (titleLength <= 0) return true;
95
+ var title = new StringBuilder(titleLength + 1);
96
+ if (GetWindowText(hWnd, title, title.Capacity) <= 0) return true;
97
+ uint pid;
98
+ GetWindowThreadProcessId(hWnd, out pid);
99
+ RECT rect;
100
+ if (!GetWindowRect(hWnd, out rect)) return true;
101
+ items.Add(new WindowInfo {
102
+ WindowId = unchecked((ulong)hWnd.ToInt64()).ToString(),
103
+ ProcessId = pid,
104
+ Title = title.ToString(),
105
+ X = rect.Left,
106
+ Y = rect.Top,
107
+ Width = Math.Max(0, rect.Right - rect.Left),
108
+ Height = Math.Max(0, rect.Bottom - rect.Top),
109
+ IsForeground = hWnd == foreground,
110
+ });
111
+ return true;
112
+ }, IntPtr.Zero);
113
+ return items.ToArray();
114
+ }
115
+
116
+ public static bool FocusWindow(string windowId) {
117
+ ulong raw;
118
+ if (!UInt64.TryParse(windowId, out raw) || raw == 0) return false;
119
+ var hWnd = new IntPtr(unchecked((long)raw));
120
+ if (!IsWindow(hWnd)) return false;
121
+ ShowWindowAsync(hWnd, 9);
122
+ return SetForegroundWindow(hWnd);
123
+ }
124
+
125
+ public static bool MouseButton(uint downFlag, uint upFlag) {
126
+ var inputs = new INPUT[2];
127
+ inputs[0].type = INPUT_MOUSE;
128
+ inputs[0].U.mi.dwFlags = downFlag;
129
+ inputs[1].type = INPUT_MOUSE;
130
+ inputs[1].U.mi.dwFlags = upFlag;
131
+ return SendInput(2, inputs, Marshal.SizeOf(typeof(INPUT))) == 2;
132
+ }
133
+
134
+ public static bool Key(ushort virtualKey, bool down) {
135
+ var input = new INPUT[1];
136
+ input[0].type = INPUT_KEYBOARD;
137
+ input[0].U.ki.wVk = virtualKey;
138
+ input[0].U.ki.dwFlags = down ? 0u : KEYEVENTF_KEYUP;
139
+ return SendInput(1, input, Marshal.SizeOf(typeof(INPUT))) == 1;
140
+ }
141
+
142
+ public static bool UnicodeText(string text) {
143
+ foreach (char c in text) {
144
+ var inputs = new INPUT[2];
145
+ inputs[0].type = INPUT_KEYBOARD;
146
+ inputs[0].U.ki.wScan = c;
147
+ inputs[0].U.ki.dwFlags = KEYEVENTF_UNICODE;
148
+ inputs[1].type = INPUT_KEYBOARD;
149
+ inputs[1].U.ki.wScan = c;
150
+ inputs[1].U.ki.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP;
151
+ if (SendInput(2, inputs, Marshal.SizeOf(typeof(INPUT))) != 2) return false;
152
+ }
153
+ return true;
154
+ }
155
+ }
156
+ "@
157
+
158
+ [ChatRelayNative]::SetProcessDPIAware() | Out-Null
159
+
160
+ function Result-Error([string]$Code) {
161
+ return [pscustomobject]@{ ok = $false; error = $Code }
162
+ }
163
+
164
+ function Test-InteractiveSession {
165
+ return [System.Windows.Forms.SystemInformation]::UserInteractive -and
166
+ ([ChatRelayNative]::GetForegroundWindow() -ne [IntPtr]::Zero)
167
+ }
168
+
169
+ function Resolve-Screen($Monitor) {
170
+ $screens = @([System.Windows.Forms.Screen]::AllScreens)
171
+ if ($screens.Count -eq 0) { return $null }
172
+
173
+ if ($null -eq $Monitor -or [string]$Monitor -eq "primary") {
174
+ for ($i = 0; $i -lt $screens.Count; $i++) {
175
+ if ($screens[$i].Primary) {
176
+ return [pscustomobject]@{ screen = $screens[$i]; index = $i }
177
+ }
178
+ }
179
+ return $null
180
+ }
181
+
182
+ if ([string]$Monitor -eq "secondary") {
183
+ for ($i = 0; $i -lt $screens.Count; $i++) {
184
+ if (-not $screens[$i].Primary) {
185
+ return [pscustomobject]@{ screen = $screens[$i]; index = $i }
186
+ }
187
+ }
188
+ return $null
189
+ }
190
+
191
+ $parsedIndex = 0
192
+ if (-not [int]::TryParse([string]$Monitor, [ref]$parsedIndex)) { return $null }
193
+ if ($parsedIndex -lt 0 -or $parsedIndex -ge $screens.Count) { return $null }
194
+ return [pscustomobject]@{ screen = $screens[$parsedIndex]; index = $parsedIndex }
195
+ }
196
+
197
+ function Invoke-Screenshot($Payload) {
198
+ $totalTimer = [System.Diagnostics.Stopwatch]::StartNew()
199
+ if (-not [System.Windows.Forms.SystemInformation]::UserInteractive) {
200
+ return Result-Error "session_unavailable"
201
+ }
202
+
203
+ $resolved = Resolve-Screen $Payload.monitor
204
+ if ($null -eq $resolved) { return Result-Error "monitor_not_found" }
205
+
206
+ $screen = $resolved.screen
207
+ $bounds = $screen.Bounds
208
+ if ($bounds.Width -le 0 -or $bounds.Height -le 0) {
209
+ return Result-Error "session_unavailable"
210
+ }
211
+
212
+ $source = $null
213
+ try {
214
+ $source = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
215
+ $graphics = [System.Drawing.Graphics]::FromImage($source)
216
+ $captureTimer = [System.Diagnostics.Stopwatch]::StartNew()
217
+ try {
218
+ $graphics.CopyFromScreen($bounds.Left, $bounds.Top, 0, 0, $bounds.Size)
219
+ } finally {
220
+ $captureTimer.Stop()
221
+ $graphics.Dispose()
222
+ }
223
+ $captureMs = [int][Math]::Round($captureTimer.Elapsed.TotalMilliseconds)
224
+ $encodeTimer = [System.Diagnostics.Stopwatch]::StartNew()
225
+
226
+ $codec = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() |
227
+ Where-Object { $_.MimeType -eq "image/jpeg" } |
228
+ Select-Object -First 1
229
+ if ($null -eq $codec) { return Result-Error "capture_failed" }
230
+
231
+ $maxBytes = 32768
232
+ $requestedMaxWidth = if ($null -eq $Payload.maxWidth) { 960 } else { [int]$Payload.maxWidth }
233
+ if ($requestedMaxWidth -lt 320 -or $requestedMaxWidth -gt 1920) { return Result-Error "invalid_max_width" }
234
+ $requestedQuality = if ($null -eq $Payload.quality) { 58 } else { [int]$Payload.quality }
235
+ if ($requestedQuality -lt 20 -or $requestedQuality -gt 85) { return Result-Error "invalid_quality" }
236
+ $targetWidth = [Math]::Min($bounds.Width, $requestedMaxWidth)
237
+ $targetHeight = [Math]::Max(1, [int][Math]::Round($bounds.Height * ($targetWidth / [double]$bounds.Width)))
238
+ $qualities = @([long]$requestedQuality, [long][Math]::Min($requestedQuality, 42), [long][Math]::Min($requestedQuality, 30), 20L) | Select-Object -Unique
239
+ $bytes = $null
240
+ $finalWidth = $targetWidth
241
+ $finalHeight = $targetHeight
242
+ $finalQuality = $requestedQuality
243
+
244
+ for ($sizeAttempt = 0; $sizeAttempt -lt 3 -and $null -eq $bytes; $sizeAttempt++) {
245
+ $resized = New-Object System.Drawing.Bitmap $targetWidth, $targetHeight
246
+ $draw = [System.Drawing.Graphics]::FromImage($resized)
247
+ try {
248
+ $draw.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighSpeed
249
+ $draw.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::Bilinear
250
+ $draw.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighSpeed
251
+ $draw.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighSpeed
252
+ $draw.DrawImage($source, 0, 0, $targetWidth, $targetHeight)
253
+ } finally {
254
+ $draw.Dispose()
255
+ }
256
+
257
+ try {
258
+ foreach ($quality in $qualities) {
259
+ $stream = New-Object System.IO.MemoryStream
260
+ $parameters = New-Object System.Drawing.Imaging.EncoderParameters 1
261
+ $parameters.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter(
262
+ [System.Drawing.Imaging.Encoder]::Quality,
263
+ $quality
264
+ )
265
+ try {
266
+ $resized.Save($stream, $codec, $parameters)
267
+ if ($stream.Length -le $maxBytes) {
268
+ $bytes = $stream.ToArray()
269
+ $finalWidth = $targetWidth
270
+ $finalHeight = $targetHeight
271
+ $finalQuality = [int]$quality
272
+ break
273
+ }
274
+ } finally {
275
+ $parameters.Dispose()
276
+ $stream.Dispose()
277
+ }
278
+ }
279
+ } finally {
280
+ $resized.Dispose()
281
+ }
282
+
283
+ if ($null -eq $bytes) {
284
+ $targetWidth = [Math]::Max(320, [int][Math]::Floor($targetWidth * 0.78))
285
+ $targetHeight = [Math]::Max(180, [int][Math]::Round($bounds.Height * ($targetWidth / [double]$bounds.Width)))
286
+ }
287
+ }
288
+
289
+ if ($null -eq $bytes) { return Result-Error "screenshot_too_large" }
290
+ $base64 = [Convert]::ToBase64String($bytes)
291
+ $encodeTimer.Stop()
292
+ $totalTimer.Stop()
293
+ $virtualBounds = [System.Windows.Forms.SystemInformation]::VirtualScreen
294
+ $monitorCount = @([System.Windows.Forms.Screen]::AllScreens).Count
295
+
296
+ return [pscustomobject]@{
297
+ ok = $true
298
+ mimeType = "image/jpeg"
299
+ data = $base64
300
+ width = $finalWidth
301
+ height = $finalHeight
302
+ virtualDesktopOriginX = $virtualBounds.Left
303
+ virtualDesktopOriginY = $virtualBounds.Top
304
+ virtualDesktopWidth = $virtualBounds.Width
305
+ virtualDesktopHeight = $virtualBounds.Height
306
+ monitorCount = $monitorCount
307
+ jpegQuality = $finalQuality
308
+ desktopOriginX = $bounds.Left
309
+ desktopOriginY = $bounds.Top
310
+ desktopWidth = $bounds.Width
311
+ desktopHeight = $bounds.Height
312
+ scaleX = $bounds.Width / [double]$finalWidth
313
+ scaleY = $bounds.Height / [double]$finalHeight
314
+ monitorIndex = $resolved.index
315
+ isPrimary = [bool]$screen.Primary
316
+ deviceName = [string]$screen.DeviceName
317
+ byteLength = $bytes.Length
318
+ timing = [pscustomobject]@{
319
+ captureMs = $captureMs
320
+ encodeMs = [int][Math]::Round($encodeTimer.Elapsed.TotalMilliseconds)
321
+ screenshotTotalMs = [int][Math]::Round($totalTimer.Elapsed.TotalMilliseconds)
322
+ }
323
+ }
324
+ } catch {
325
+ return Result-Error "capture_failed"
326
+ } finally {
327
+ if ($null -ne $source) { $source.Dispose() }
328
+ }
329
+ }
330
+
331
+ function Invoke-MouseClick($Payload) {
332
+ if (-not (Test-InteractiveSession)) { return Result-Error "session_unavailable" }
333
+
334
+ $x = [int]$Payload.x
335
+ $y = [int]$Payload.y
336
+ $containsPoint = $false
337
+ foreach ($screen in @([System.Windows.Forms.Screen]::AllScreens)) {
338
+ if ($screen.Bounds.Contains($x, $y)) {
339
+ $containsPoint = $true
340
+ break
341
+ }
342
+ }
343
+ if (-not $containsPoint) { return Result-Error "invalid_coordinates" }
344
+
345
+ switch ([string]$Payload.button) {
346
+ "left" { $down = 0x0002; $up = 0x0004 }
347
+ "right" { $down = 0x0008; $up = 0x0010 }
348
+ "middle" { $down = 0x0020; $up = 0x0040 }
349
+ default { return Result-Error "invalid_button" }
350
+ }
351
+
352
+ if (-not [ChatRelayNative]::SetCursorPos($x, $y)) { return Result-Error "input_failed" }
353
+
354
+ $clicks = [int]$Payload.clicks
355
+ for ($i = 0; $i -lt $clicks; $i++) {
356
+ if (-not [ChatRelayNative]::MouseButton($down, $up)) { return Result-Error "input_failed" }
357
+ if ($i + 1 -lt $clicks) { Start-Sleep -Milliseconds 100 }
358
+ }
359
+
360
+ return [pscustomobject]@{ ok = $true; action = "mouse_click" }
361
+ }
362
+
363
+ function Invoke-KeyboardInput($Payload) {
364
+ if (-not (Test-InteractiveSession)) { return Result-Error "session_unavailable" }
365
+
366
+ if ($null -ne $Payload.text) {
367
+ if (-not [ChatRelayNative]::UnicodeText([string]$Payload.text)) {
368
+ return Result-Error "input_failed"
369
+ }
370
+ } else {
371
+ $mods = New-Object System.Collections.Generic.List[uint16]
372
+ if ($Payload.ctrl) { $mods.Add(0x11) }
373
+ if ($Payload.alt) { $mods.Add(0x12) }
374
+ if ($Payload.shift) { $mods.Add(0x10) }
375
+ if ($Payload.win) { $mods.Add(0x5B) }
376
+
377
+ foreach ($vk in $mods) {
378
+ if (-not [ChatRelayNative]::Key($vk, $true)) { return Result-Error "input_failed" }
379
+ }
380
+
381
+ try {
382
+ $keyCode = [uint16]$Payload.keyCode
383
+ if (-not [ChatRelayNative]::Key($keyCode, $true)) { return Result-Error "input_failed" }
384
+ if (-not [ChatRelayNative]::Key($keyCode, $false)) { return Result-Error "input_failed" }
385
+ } finally {
386
+ for ($i = $mods.Count - 1; $i -ge 0; $i--) {
387
+ [ChatRelayNative]::Key($mods[$i], $false) | Out-Null
388
+ }
389
+ }
390
+ }
391
+
392
+ return [pscustomobject]@{ ok = $true; action = "keyboard_input" }
393
+ }
394
+
395
+ function Invoke-ClipboardRead($Payload) {
396
+ if (-not [System.Windows.Forms.SystemInformation]::UserInteractive) { return Result-Error "session_unavailable" }
397
+ try {
398
+ $text = if ([System.Windows.Forms.Clipboard]::ContainsText()) { [System.Windows.Forms.Clipboard]::GetText() } else { "" }
399
+ $truncated = $text.Length -gt 8192
400
+ if ($truncated) { $text = $text.Substring(0, 8192) }
401
+ return [pscustomobject]@{ ok = $true; text = $text; truncated = $truncated }
402
+ } catch { return Result-Error "clipboard_failed" }
403
+ }
404
+
405
+ function Invoke-ClipboardWrite($Payload) {
406
+ if (-not (Test-InteractiveSession)) { return Result-Error "session_unavailable" }
407
+ try {
408
+ $text = [string]$Payload.text
409
+ if ($text.Length -gt 8192) { return Result-Error "text_too_large" }
410
+ if ($text.Length -eq 0) { [System.Windows.Forms.Clipboard]::Clear() } else { [System.Windows.Forms.Clipboard]::SetText($text) }
411
+ return [pscustomobject]@{ ok = $true; action = "clipboard_write"; length = $text.Length }
412
+ } catch { return Result-Error "clipboard_failed" }
413
+ }
414
+
415
+ function Invoke-WindowList($Payload) {
416
+ if (-not [System.Windows.Forms.SystemInformation]::UserInteractive) { return Result-Error "session_unavailable" }
417
+ $limit = if ($null -eq $Payload.limit) { 50 } else { [int]$Payload.limit }
418
+ if ($limit -lt 1 -or $limit -gt 100) { return Result-Error "invalid_window_limit" }
419
+ try {
420
+ $windows = @([ChatRelayNative]::ListWindows($limit))
421
+ return [pscustomobject]@{ ok = $true; windows = $windows; count = $windows.Count }
422
+ } catch { return Result-Error "window_list_failed" }
423
+ }
424
+
425
+ function Invoke-WindowFocus($Payload) {
426
+ if (-not (Test-InteractiveSession)) { return Result-Error "session_unavailable" }
427
+ $windowId = [string]$Payload.windowId
428
+ if ($windowId -notmatch '^[1-9][0-9]{0,19}$') { return Result-Error "invalid_window_id" }
429
+ try {
430
+ if (-not [ChatRelayNative]::FocusWindow($windowId)) { return Result-Error "focus_failed" }
431
+ return [pscustomobject]@{ ok = $true; action = "window_focus"; windowId = $windowId }
432
+ } catch { return Result-Error "focus_failed" }
433
+ }
434
+
435
+ function Invoke-Step($Payload) {
436
+ $totalTimer = [System.Diagnostics.Stopwatch]::StartNew()
437
+ $completed = 0
438
+ $inputMs = 0
439
+ $explicitWaitMs = 0
440
+ $settleMs = 0
441
+
442
+ foreach ($action in @($Payload.actions)) {
443
+ $result = $null
444
+ if ([string]$action.type -eq "wait") {
445
+ $waitTimer = [System.Diagnostics.Stopwatch]::StartNew()
446
+ Start-Sleep -Milliseconds ([int]$action.ms)
447
+ $waitTimer.Stop()
448
+ $explicitWaitMs += [int][Math]::Round($waitTimer.Elapsed.TotalMilliseconds)
449
+ $result = [pscustomobject]@{ ok = $true; action = "wait" }
450
+ } else {
451
+ $inputTimer = [System.Diagnostics.Stopwatch]::StartNew()
452
+ switch ([string]$action.type) {
453
+ "click" { $result = Invoke-MouseClick $action }
454
+ "text" { $result = Invoke-KeyboardInput $action }
455
+ "key" { $result = Invoke-KeyboardInput $action }
456
+ default { $result = Result-Error "invalid_step_action" }
457
+ }
458
+ $inputTimer.Stop()
459
+ $inputMs += [int][Math]::Round($inputTimer.Elapsed.TotalMilliseconds)
460
+ }
461
+
462
+ if (-not $result.ok) {
463
+ $totalTimer.Stop()
464
+ return [pscustomobject]@{
465
+ ok = $false
466
+ error = $result.error
467
+ actionsCompleted = $completed
468
+ timing = [pscustomobject]@{
469
+ inputMs = $inputMs
470
+ explicitWaitMs = $explicitWaitMs
471
+ settleMs = $settleMs
472
+ captureMs = 0
473
+ encodeMs = 0
474
+ workerMs = [int][Math]::Round($totalTimer.Elapsed.TotalMilliseconds)
475
+ }
476
+ }
477
+ }
478
+
479
+ $completed++
480
+ $actionSettleMs = [int]$action.settleAfterMs
481
+ if ($actionSettleMs -gt 0) {
482
+ $settleTimer = [System.Diagnostics.Stopwatch]::StartNew()
483
+ Start-Sleep -Milliseconds $actionSettleMs
484
+ $settleTimer.Stop()
485
+ $settleMs += [int][Math]::Round($settleTimer.Elapsed.TotalMilliseconds)
486
+ }
487
+ }
488
+
489
+ if ([int]$Payload.settleMs -gt 0) {
490
+ $finalSettleTimer = [System.Diagnostics.Stopwatch]::StartNew()
491
+ Start-Sleep -Milliseconds ([int]$Payload.settleMs)
492
+ $finalSettleTimer.Stop()
493
+ $settleMs += [int][Math]::Round($finalSettleTimer.Elapsed.TotalMilliseconds)
494
+ }
495
+
496
+ if ($Payload.captureAfter -ne $true) {
497
+ $totalTimer.Stop()
498
+ return [pscustomobject]@{
499
+ ok = $true
500
+ action = "step"
501
+ actionsCompleted = $completed
502
+ timing = [pscustomobject]@{
503
+ inputMs = $inputMs
504
+ explicitWaitMs = $explicitWaitMs
505
+ settleMs = $settleMs
506
+ captureMs = 0
507
+ encodeMs = 0
508
+ workerMs = [int][Math]::Round($totalTimer.Elapsed.TotalMilliseconds)
509
+ }
510
+ }
511
+ }
512
+
513
+ $shotPayload = [pscustomobject]@{ monitor = $Payload.monitor; maxWidth = $Payload.maxWidth; quality = $Payload.quality }
514
+ $shot = Invoke-Screenshot $shotPayload
515
+ if (-not $shot.ok) {
516
+ $totalTimer.Stop()
517
+ return [pscustomobject]@{
518
+ ok = $false
519
+ error = $shot.error
520
+ actionsCompleted = $completed
521
+ timing = [pscustomobject]@{
522
+ inputMs = $inputMs
523
+ explicitWaitMs = $explicitWaitMs
524
+ settleMs = $settleMs
525
+ captureMs = 0
526
+ encodeMs = 0
527
+ workerMs = [int][Math]::Round($totalTimer.Elapsed.TotalMilliseconds)
528
+ }
529
+ }
530
+ }
531
+
532
+ $totalTimer.Stop()
533
+ $captureMs = if ($null -ne $shot.timing) { [int]$shot.timing.captureMs } else { 0 }
534
+ $encodeMs = if ($null -ne $shot.timing) { [int]$shot.timing.encodeMs } else { 0 }
535
+ $shot | Add-Member -NotePropertyName action -NotePropertyValue "step" -Force
536
+ $shot | Add-Member -NotePropertyName actionsCompleted -NotePropertyValue $completed -Force
537
+ $shot | Add-Member -NotePropertyName timing -NotePropertyValue ([pscustomobject]@{
538
+ inputMs = $inputMs
539
+ explicitWaitMs = $explicitWaitMs
540
+ settleMs = $settleMs
541
+ captureMs = $captureMs
542
+ encodeMs = $encodeMs
543
+ workerMs = [int][Math]::Round($totalTimer.Elapsed.TotalMilliseconds)
544
+ }) -Force
545
+ return $shot
546
+ }
547
+
548
+ function Invoke-Operation([string]$Operation, $Payload) {
549
+ switch ($Operation) {
550
+ "screenshot" { return Invoke-Screenshot $Payload }
551
+ "clipboard_read" { return Invoke-ClipboardRead $Payload }
552
+ "clipboard_write" { return Invoke-ClipboardWrite $Payload }
553
+ "window_list" { return Invoke-WindowList $Payload }
554
+ "window_focus" { return Invoke-WindowFocus $Payload }
555
+ "mouse_click" { return Invoke-MouseClick $Payload }
556
+ "keyboard_input" { return Invoke-KeyboardInput $Payload }
557
+ "step" { return Invoke-Step $Payload }
558
+ default { return Result-Error "unknown_desktop_operation" }
559
+ }
560
+ }
561
+
562
+ while (($line = [Console]::In.ReadLine()) -ne $null) {
563
+ if ([string]::IsNullOrWhiteSpace($line)) { continue }
564
+
565
+ $id = $null
566
+ $result = $null
567
+ try {
568
+ $request = $line | ConvertFrom-Json
569
+ $id = [string]$request.id
570
+ $result = Invoke-Operation ([string]$request.operation) $request.args
571
+ } catch {
572
+ $result = Result-Error "desktop_worker_error"
573
+ }
574
+
575
+ $response = [pscustomobject]@{
576
+ id = $id
577
+ result = $result
578
+ }
579
+
580
+ [Console]::Out.WriteLine(($response | ConvertTo-Json -Compress -Depth 10))
581
+ [Console]::Out.Flush()
582
+ }