@anusornneal/chat-relay 0.7.2 → 0.8.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/README.md +30 -0
- package/agent/capability-scheduler.mjs +121 -0
- package/agent/connection-state.mjs +126 -0
- package/agent/desktop-manager.mjs +440 -319
- package/agent/file-manager.mjs +150 -5
- package/agent/lifecycle.mjs +101 -0
- package/agent/local-agent.mjs +259 -26
- package/agent/protocol.mjs +67 -0
- package/agent/windows-desktop-worker.ps1 +508 -380
- package/cli/index.mjs +41 -1
- package/cli/remote.mjs +51 -6
- package/package.json +1 -1
|
@@ -1,380 +1,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.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
public const uint
|
|
17
|
-
public const uint
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
public
|
|
24
|
-
public
|
|
25
|
-
public uint
|
|
26
|
-
public
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
public
|
|
34
|
-
public
|
|
35
|
-
public
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
[
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return Result-Error "
|
|
229
|
-
|
|
230
|
-
if ($
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
return $
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function Invoke-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
[
|
|
380
|
-
}
|
|
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
|
+
}
|