@anusornneal/chat-relay 0.7.1 → 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 +325 -68
- package/agent/file-manager.mjs +181 -23
- package/agent/lifecycle.mjs +101 -0
- package/agent/local-agent.mjs +273 -26
- package/agent/protocol.mjs +67 -0
- package/agent/terminal-manager.mjs +46 -3
- package/agent/windows-desktop-worker.ps1 +508 -0
- package/cli/index.mjs +41 -1
- package/cli/remote.mjs +51 -6
- package/package.json +73 -73
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import readline from "node:readline";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { BoundedLane } from "./capability-scheduler.mjs";
|
|
4
7
|
|
|
5
8
|
const MAX_SCREENSHOT_BINARY_BYTES = 32 * 1024;
|
|
6
9
|
const POWERSHELL_TIMEOUT_MS = 15_000;
|
|
7
10
|
const MAX_TEXT_LENGTH = 8192;
|
|
8
|
-
const
|
|
11
|
+
const MAX_CLIPBOARD_TEXT_LENGTH = 8192;
|
|
12
|
+
const MIN_SCREENSHOT_WIDTH = 320;
|
|
13
|
+
const MAX_SCREENSHOT_WIDTH = 1920;
|
|
14
|
+
const MIN_SCREENSHOT_QUALITY = 20;
|
|
15
|
+
const MAX_SCREENSHOT_QUALITY = 85;
|
|
16
|
+
const MAX_WINDOW_LIST_LIMIT = 100;
|
|
17
|
+
const helperPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "windows-desktop-worker.ps1");
|
|
9
18
|
|
|
10
19
|
const KEY_CODES = new Map([
|
|
11
20
|
["enter", 0x0D],
|
|
@@ -31,69 +40,203 @@ function normalizedKey(value) {
|
|
|
31
40
|
return String(value || "").trim().toLowerCase().replace(/[\s_-]+/g, "");
|
|
32
41
|
}
|
|
33
42
|
|
|
34
|
-
function
|
|
35
|
-
|
|
43
|
+
function normalizeKeyboardInput(input = {}) {
|
|
44
|
+
const hasText = typeof input.text === "string" && input.text.length > 0;
|
|
45
|
+
const hasKey = typeof input.key === "string" && input.key.trim().length > 0;
|
|
46
|
+
|
|
47
|
+
if (hasText === hasKey) return { ok: false, error: "invalid_keyboard_input" };
|
|
48
|
+
|
|
49
|
+
if (hasText) {
|
|
50
|
+
if (input.text.length > MAX_TEXT_LENGTH) return { ok: false, error: "text_too_large" };
|
|
51
|
+
if (input.ctrl || input.alt || input.shift || input.win) {
|
|
52
|
+
return { ok: false, error: "invalid_keyboard_input" };
|
|
53
|
+
}
|
|
54
|
+
return { ok: true, payload: { text: input.text } };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const key = normalizedKey(input.key);
|
|
58
|
+
const keyCode = KEY_CODES.get(key);
|
|
59
|
+
if (!keyCode) return { ok: false, error: "unsupported_key" };
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
payload: {
|
|
64
|
+
keyCode,
|
|
65
|
+
ctrl: input.ctrl === true,
|
|
66
|
+
alt: input.alt === true,
|
|
67
|
+
shift: input.shift === true,
|
|
68
|
+
win: input.win === true,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class PersistentDesktopRunner {
|
|
74
|
+
constructor(options = {}) {
|
|
75
|
+
this.platform = options.platform ?? process.platform;
|
|
76
|
+
this.timeoutMs = Number(options.timeoutMs ?? POWERSHELL_TIMEOUT_MS);
|
|
77
|
+
this.process = null;
|
|
78
|
+
this.stdout = null;
|
|
79
|
+
this.pending = new Map();
|
|
80
|
+
this.stderr = "";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
ensureStarted() {
|
|
84
|
+
if (this.platform !== "win32") return false;
|
|
85
|
+
if (this.process && !this.process.killed) return true;
|
|
86
|
+
|
|
87
|
+
const child = spawn("powershell.exe", [
|
|
88
|
+
"-NoLogo",
|
|
89
|
+
"-NoProfile",
|
|
90
|
+
"-NonInteractive",
|
|
91
|
+
"-Sta",
|
|
92
|
+
"-ExecutionPolicy",
|
|
93
|
+
"Bypass",
|
|
94
|
+
"-File",
|
|
95
|
+
helperPath,
|
|
96
|
+
], {
|
|
97
|
+
windowsHide: true,
|
|
98
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
this.process = child;
|
|
102
|
+
this.stderr = "";
|
|
103
|
+
this.stdout = readline.createInterface({ input: child.stdout });
|
|
104
|
+
|
|
105
|
+
this.stdout.on("line", (line) => {
|
|
106
|
+
let message;
|
|
107
|
+
try {
|
|
108
|
+
message = JSON.parse(line);
|
|
109
|
+
} catch {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const pending = this.pending.get(message?.id);
|
|
113
|
+
if (!pending) return;
|
|
114
|
+
clearTimeout(pending.timer);
|
|
115
|
+
this.pending.delete(message.id);
|
|
116
|
+
pending.resolve(message.result ?? { ok: false, error: "desktop_worker_error" });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
child.stderr.on("data", (chunk) => {
|
|
120
|
+
this.stderr = (this.stderr + String(chunk)).slice(-8192);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
child.on("exit", () => {
|
|
124
|
+
const error = { ok: false, error: "desktop_worker_exited" };
|
|
125
|
+
for (const pending of this.pending.values()) {
|
|
126
|
+
clearTimeout(pending.timer);
|
|
127
|
+
pending.resolve(error);
|
|
128
|
+
}
|
|
129
|
+
this.pending.clear();
|
|
130
|
+
this.stdout?.close();
|
|
131
|
+
this.stdout = null;
|
|
132
|
+
this.process = null;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
child.on("error", () => {
|
|
136
|
+
// exit handler settles pending work.
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async run(operation, args) {
|
|
143
|
+
if (!this.ensureStarted() || !this.process?.stdin?.writable) {
|
|
144
|
+
return { ok: false, error: operation === "screenshot" ? "capture_failed" : "input_failed" };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const id = randomUUID();
|
|
148
|
+
const request = JSON.stringify({ id, operation, args: args || {} }) + "\n";
|
|
149
|
+
|
|
150
|
+
return new Promise((resolve) => {
|
|
151
|
+
const timer = setTimeout(() => {
|
|
152
|
+
this.pending.delete(id);
|
|
153
|
+
resolve({ ok: false, error: operation === "screenshot" ? "capture_timeout" : "input_timeout" });
|
|
154
|
+
}, this.timeoutMs);
|
|
155
|
+
|
|
156
|
+
this.pending.set(id, { resolve, timer });
|
|
157
|
+
this.process.stdin.write(request, (error) => {
|
|
158
|
+
if (!error) return;
|
|
159
|
+
clearTimeout(timer);
|
|
160
|
+
this.pending.delete(id);
|
|
161
|
+
resolve({ ok: false, error: operation === "screenshot" ? "capture_failed" : "input_failed" });
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
close() {
|
|
167
|
+
try { this.stdout?.close(); } catch {}
|
|
168
|
+
try { this.process?.kill(); } catch {}
|
|
169
|
+
this.stdout = null;
|
|
170
|
+
this.process = null;
|
|
171
|
+
}
|
|
36
172
|
}
|
|
37
173
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
(error, stdout) => {
|
|
61
|
-
const line = String(stdout || "").trim().split(/\r?\n/).filter(Boolean).at(-1);
|
|
62
|
-
if (!line) {
|
|
63
|
-
resolve({ ok: false, error: operation === "screenshot" ? "capture_failed" : "input_failed" });
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
try {
|
|
67
|
-
resolve(JSON.parse(line));
|
|
68
|
-
} catch {
|
|
69
|
-
resolve({ ok: false, error: operation === "screenshot" ? "capture_failed" : "input_failed" });
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
);
|
|
73
|
-
});
|
|
174
|
+
export function createDesktopPlatformAdapter(options = {}) {
|
|
175
|
+
const platform = options.platform ?? process.platform;
|
|
176
|
+
const customRunner = typeof options.runner === "function" ? options.runner : null;
|
|
177
|
+
|
|
178
|
+
if (platform === "win32") {
|
|
179
|
+
const persistentRunner = customRunner ? null : (options.persistentRunner ?? new PersistentDesktopRunner({ platform }));
|
|
180
|
+
return {
|
|
181
|
+
platform,
|
|
182
|
+
supported: true,
|
|
183
|
+
transport: "persistent-worker",
|
|
184
|
+
run: customRunner ?? ((operation, args) => persistentRunner.run(operation, args)),
|
|
185
|
+
close: () => persistentRunner?.close?.(),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
platform,
|
|
191
|
+
supported: false,
|
|
192
|
+
transport: "unsupported",
|
|
193
|
+
run: customRunner ?? (async () => ({ ok: false, error: "unsupported_platform" })),
|
|
194
|
+
close: () => {},
|
|
195
|
+
};
|
|
74
196
|
}
|
|
75
197
|
|
|
76
198
|
export class DesktopManager {
|
|
77
199
|
constructor(options = {}) {
|
|
78
200
|
this.enabled = options.enabled ?? process.env.DESKTOP_ENABLED === "1";
|
|
79
|
-
this.
|
|
80
|
-
|
|
201
|
+
this.adapter = options.adapter ?? createDesktopPlatformAdapter({
|
|
202
|
+
platform: options.platform,
|
|
203
|
+
persistentRunner: options.persistentRunner,
|
|
204
|
+
runner: options.runner,
|
|
205
|
+
});
|
|
206
|
+
this.platform = this.adapter.platform;
|
|
207
|
+
this.runner = (operation, args) => this.adapter.run(operation, args);
|
|
208
|
+
this.controlLane = options.controlLane ?? new BoundedLane({
|
|
209
|
+
name: "desktop_control",
|
|
210
|
+
concurrency: 1,
|
|
211
|
+
maxQueued: options.controlMaxQueued ?? process.env.DESKTOP_CONTROL_MAX_QUEUED,
|
|
212
|
+
queueTimeoutMs: options.controlQueueTimeoutMs ?? process.env.AGENT_QUEUE_TIMEOUT_MS,
|
|
213
|
+
});
|
|
81
214
|
}
|
|
82
215
|
|
|
83
216
|
getConfig() {
|
|
84
217
|
return {
|
|
85
218
|
enabled: this.enabled,
|
|
86
|
-
supported: this.
|
|
87
|
-
platform: this.platform,
|
|
219
|
+
supported: this.adapter.supported,
|
|
220
|
+
platform: this.adapter.platform,
|
|
221
|
+
transport: this.adapter.transport,
|
|
222
|
+
controlQueue: this.controlLane.snapshot(),
|
|
88
223
|
};
|
|
89
224
|
}
|
|
90
225
|
|
|
91
226
|
gate() {
|
|
92
227
|
if (!this.enabled) return { ok: false, error: "desktop_disabled" };
|
|
93
|
-
if (this.
|
|
228
|
+
if (!this.adapter.supported) return { ok: false, error: "unsupported_platform" };
|
|
94
229
|
return null;
|
|
95
230
|
}
|
|
96
231
|
|
|
232
|
+
async runControl(work) {
|
|
233
|
+
try {
|
|
234
|
+
return await this.controlLane.run(work);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
return { ok: false, error: error instanceof Error ? error.message : "desktop_queue_error" };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
97
240
|
async screenshot(input = {}) {
|
|
98
241
|
const gate = this.gate();
|
|
99
242
|
if (gate) return gate;
|
|
@@ -103,7 +246,20 @@ export class DesktopManager {
|
|
|
103
246
|
return { ok: false, error: "invalid_monitor" };
|
|
104
247
|
}
|
|
105
248
|
|
|
106
|
-
const
|
|
249
|
+
const maxWidth = input.maxWidth === undefined ? 960 : Number(input.maxWidth);
|
|
250
|
+
if (!Number.isInteger(maxWidth) || maxWidth < MIN_SCREENSHOT_WIDTH || maxWidth > MAX_SCREENSHOT_WIDTH) {
|
|
251
|
+
return { ok: false, error: "invalid_max_width" };
|
|
252
|
+
}
|
|
253
|
+
const quality = input.quality === undefined ? 58 : Number(input.quality);
|
|
254
|
+
if (!Number.isInteger(quality) || quality < MIN_SCREENSHOT_QUALITY || quality > MAX_SCREENSHOT_QUALITY) {
|
|
255
|
+
return { ok: false, error: "invalid_quality" };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const result = await this.runner("screenshot", { monitor, maxWidth, quality });
|
|
259
|
+
return this.normalizeScreenshotResult(result);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
normalizeScreenshotResult(result) {
|
|
107
263
|
if (!result?.ok) return result || { ok: false, error: "capture_failed" };
|
|
108
264
|
if (result.mimeType !== "image/jpeg" || typeof result.data !== "string") {
|
|
109
265
|
return { ok: false, error: "capture_failed" };
|
|
@@ -115,9 +271,8 @@ export class DesktopManager {
|
|
|
115
271
|
}
|
|
116
272
|
|
|
117
273
|
return {
|
|
274
|
+
...result,
|
|
118
275
|
ok: true,
|
|
119
|
-
mimeType: result.mimeType,
|
|
120
|
-
data: result.data,
|
|
121
276
|
width: Number(result.width),
|
|
122
277
|
height: Number(result.height),
|
|
123
278
|
desktopOriginX: Number(result.desktopOriginX),
|
|
@@ -129,10 +284,60 @@ export class DesktopManager {
|
|
|
129
284
|
monitorIndex: Number(result.monitorIndex),
|
|
130
285
|
isPrimary: result.isPrimary === true,
|
|
131
286
|
deviceName: typeof result.deviceName === "string" ? result.deviceName : undefined,
|
|
287
|
+
virtualDesktopOriginX: Number(result.virtualDesktopOriginX),
|
|
288
|
+
virtualDesktopOriginY: Number(result.virtualDesktopOriginY),
|
|
289
|
+
virtualDesktopWidth: Number(result.virtualDesktopWidth),
|
|
290
|
+
virtualDesktopHeight: Number(result.virtualDesktopHeight),
|
|
291
|
+
monitorCount: Number(result.monitorCount),
|
|
292
|
+
jpegQuality: Number(result.jpegQuality),
|
|
132
293
|
byteLength: binaryBytes,
|
|
133
294
|
};
|
|
134
295
|
}
|
|
135
296
|
|
|
297
|
+
async clipboardRead() {
|
|
298
|
+
const gate = this.gate();
|
|
299
|
+
if (gate) return gate;
|
|
300
|
+
const result = await this.runner("clipboard_read", {});
|
|
301
|
+
if (!result?.ok || typeof result.text !== "string") return result || { ok: false, error: "clipboard_failed" };
|
|
302
|
+
return { ok: true, text: result.text.slice(0, MAX_CLIPBOARD_TEXT_LENGTH), truncated: result.truncated === true };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async clipboardWrite(input = {}) {
|
|
306
|
+
const gate = this.gate();
|
|
307
|
+
if (gate) return gate;
|
|
308
|
+
if (typeof input.text !== "string") return { ok: false, error: "invalid_clipboard_text" };
|
|
309
|
+
if (input.text.length > MAX_CLIPBOARD_TEXT_LENGTH) return { ok: false, error: "text_too_large" };
|
|
310
|
+
return this.runControl(() => this.runner("clipboard_write", { text: input.text }));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async listWindows(input = {}) {
|
|
314
|
+
const gate = this.gate();
|
|
315
|
+
if (gate) return gate;
|
|
316
|
+
const limit = input.limit === undefined ? 50 : Number(input.limit);
|
|
317
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > MAX_WINDOW_LIST_LIMIT) return { ok: false, error: "invalid_window_limit" };
|
|
318
|
+
const result = await this.runner("window_list", { limit });
|
|
319
|
+
if (!result?.ok || !Array.isArray(result.windows)) return result || { ok: false, error: "window_list_failed" };
|
|
320
|
+
const windows = result.windows.slice(0, limit).map((window) => ({
|
|
321
|
+
windowId: String(window.windowId ?? window.WindowId ?? ""),
|
|
322
|
+
processId: Number(window.processId ?? window.ProcessId),
|
|
323
|
+
title: String(window.title ?? window.Title ?? ""),
|
|
324
|
+
x: Number(window.x ?? window.X),
|
|
325
|
+
y: Number(window.y ?? window.Y),
|
|
326
|
+
width: Number(window.width ?? window.Width),
|
|
327
|
+
height: Number(window.height ?? window.Height),
|
|
328
|
+
isForeground: (window.isForeground ?? window.IsForeground) === true,
|
|
329
|
+
}));
|
|
330
|
+
return { ok: true, windows, count: Math.min(Number(result.count ?? result.Count) || windows.length, limit) };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async focusWindow(input = {}) {
|
|
334
|
+
const gate = this.gate();
|
|
335
|
+
if (gate) return gate;
|
|
336
|
+
const windowId = String(input.windowId ?? "").trim();
|
|
337
|
+
if (!/^[1-9][0-9]{0,19}$/.test(windowId)) return { ok: false, error: "invalid_window_id" };
|
|
338
|
+
return this.runControl(() => this.runner("window_focus", { windowId }));
|
|
339
|
+
}
|
|
340
|
+
|
|
136
341
|
async mouseClick(input = {}) {
|
|
137
342
|
const gate = this.gate();
|
|
138
343
|
if (gate) return gate;
|
|
@@ -146,38 +351,90 @@ export class DesktopManager {
|
|
|
146
351
|
if (!["left", "right", "middle"].includes(button)) return { ok: false, error: "invalid_button" };
|
|
147
352
|
if (clicks !== 1 && clicks !== 2) return { ok: false, error: "invalid_click_count" };
|
|
148
353
|
|
|
149
|
-
return this.runner("mouse_click", { x, y, button, clicks });
|
|
354
|
+
return this.runControl(() => this.runner("mouse_click", { x, y, button, clicks }));
|
|
150
355
|
}
|
|
151
356
|
|
|
152
357
|
async keyboardInput(input = {}) {
|
|
153
358
|
const gate = this.gate();
|
|
154
359
|
if (gate) return gate;
|
|
155
360
|
|
|
156
|
-
const
|
|
157
|
-
|
|
361
|
+
const normalized = normalizeKeyboardInput(input);
|
|
362
|
+
if (!normalized.ok) return normalized;
|
|
363
|
+
return this.runControl(() => this.runner("keyboard_input", normalized.payload));
|
|
364
|
+
}
|
|
158
365
|
|
|
159
|
-
|
|
366
|
+
async step(input = {}) {
|
|
367
|
+
const gate = this.gate();
|
|
368
|
+
if (gate) return gate;
|
|
369
|
+
|
|
370
|
+
const actions = Array.isArray(input.actions) ? input.actions : [];
|
|
371
|
+
if (actions.length < 1 || actions.length > 20) return { ok: false, error: "invalid_step_actions" };
|
|
372
|
+
|
|
373
|
+
const normalizedActions = [];
|
|
374
|
+
for (const action of actions) {
|
|
375
|
+
if (!action || typeof action !== "object") return { ok: false, error: "invalid_step_action" };
|
|
376
|
+
if (action.type === "click") {
|
|
377
|
+
const x = Number(action.x);
|
|
378
|
+
const y = Number(action.y);
|
|
379
|
+
const button = String(action.button || "left").toLowerCase();
|
|
380
|
+
const clicks = Number(action.clicks ?? 1);
|
|
381
|
+
if (!Number.isInteger(x) || !Number.isInteger(y)) return { ok: false, error: "invalid_coordinates" };
|
|
382
|
+
if (!["left", "right", "middle"].includes(button)) return { ok: false, error: "invalid_button" };
|
|
383
|
+
if (clicks !== 1 && clicks !== 2) return { ok: false, error: "invalid_click_count" };
|
|
384
|
+
normalizedActions.push({ type: "click", x, y, button, clicks });
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (action.type === "text" || action.type === "key") {
|
|
389
|
+
const normalized = normalizeKeyboardInput(action);
|
|
390
|
+
if (!normalized.ok) return normalized;
|
|
391
|
+
normalizedActions.push({ type: action.type, ...normalized.payload });
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
160
394
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
395
|
+
if (action.type === "wait") {
|
|
396
|
+
const ms = Number(action.ms);
|
|
397
|
+
if (!Number.isInteger(ms) || ms < 0 || ms > 5000) return { ok: false, error: "invalid_wait" };
|
|
398
|
+
normalizedActions.push({ type: "wait", ms });
|
|
399
|
+
continue;
|
|
165
400
|
}
|
|
166
|
-
|
|
401
|
+
|
|
402
|
+
return { ok: false, error: "invalid_step_action" };
|
|
167
403
|
}
|
|
168
404
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
405
|
+
const monitor = input.monitor ?? "primary";
|
|
406
|
+
if (!(monitor === "primary" || monitor === "secondary" || (Number.isInteger(monitor) && monitor >= 0 && monitor <= 15))) {
|
|
407
|
+
return { ok: false, error: "invalid_monitor" };
|
|
408
|
+
}
|
|
172
409
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
410
|
+
const settleMs = input.settleMs === undefined ? 120 : Number(input.settleMs);
|
|
411
|
+
if (!Number.isInteger(settleMs) || settleMs < 0 || settleMs > 5000) return { ok: false, error: "invalid_settle_ms" };
|
|
412
|
+
|
|
413
|
+
const wantsCapture = input.captureAfter !== false;
|
|
414
|
+
const maxWidth = input.maxWidth === undefined ? 960 : Number(input.maxWidth);
|
|
415
|
+
const quality = input.quality === undefined ? 58 : Number(input.quality);
|
|
416
|
+
if (wantsCapture && (!Number.isInteger(maxWidth) || maxWidth < MIN_SCREENSHOT_WIDTH || maxWidth > MAX_SCREENSHOT_WIDTH)) {
|
|
417
|
+
return { ok: false, error: "invalid_max_width" };
|
|
418
|
+
}
|
|
419
|
+
if (wantsCapture && (!Number.isInteger(quality) || quality < MIN_SCREENSHOT_QUALITY || quality > MAX_SCREENSHOT_QUALITY)) {
|
|
420
|
+
return { ok: false, error: "invalid_quality" };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const result = await this.runControl(() => this.runner("step", {
|
|
424
|
+
actions: normalizedActions,
|
|
425
|
+
captureAfter: wantsCapture,
|
|
426
|
+
settleMs,
|
|
427
|
+
monitor,
|
|
428
|
+
...(wantsCapture ? { maxWidth, quality } : {}),
|
|
429
|
+
}));
|
|
430
|
+
|
|
431
|
+
if (input.captureAfter === false) return result;
|
|
432
|
+
return this.normalizeScreenshotResult(result);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
close() {
|
|
436
|
+
this.adapter.close();
|
|
180
437
|
}
|
|
181
438
|
}
|
|
182
439
|
|
|
183
|
-
export { KEY_CODES, MAX_SCREENSHOT_BINARY_BYTES };
|
|
440
|
+
export { KEY_CODES, MAX_SCREENSHOT_BINARY_BYTES, PersistentDesktopRunner };
|