@anusornneal/chat-relay 0.6.0 → 0.7.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 +311 -230
- package/agent/desktop-manager.mjs +175 -0
- package/agent/file-manager.mjs +369 -265
- package/agent/local-agent.mjs +212 -157
- package/agent/windows-desktop.ps1 +263 -0
- package/cli/config.mjs +114 -113
- package/cli/device-login.mjs +170 -168
- package/cli/index.mjs +201 -145
- package/cli/remote.mjs +94 -88
- package/package.json +75 -65
package/agent/local-agent.mjs
CHANGED
|
@@ -1,157 +1,212 @@
|
|
|
1
|
-
import WebSocket from "ws";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
case "
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
case "
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
case "
|
|
67
|
-
return
|
|
68
|
-
case "
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return
|
|
74
|
-
case "
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return
|
|
80
|
-
case "
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import { DesktopManager } from "./desktop-manager.mjs";
|
|
3
|
+
import { FileManager } from "./file-manager.mjs";
|
|
4
|
+
import { ProcessManager } from "./process-manager.mjs";
|
|
5
|
+
import { TerminalManager } from "./terminal-manager.mjs";
|
|
6
|
+
|
|
7
|
+
const relayUrl = process.env.RELAY_URL;
|
|
8
|
+
const agentToken = process.env.AGENT_TOKEN;
|
|
9
|
+
const agentId = process.env.AGENT_ID || "default";
|
|
10
|
+
const agentName = process.env.AGENT_NAME || agentId;
|
|
11
|
+
const reconnectMs = Number(process.env.RECONNECT_MS ?? 2000);
|
|
12
|
+
const terminalEnabled = process.env.TERMINAL_ENABLED === "1";
|
|
13
|
+
const desktopEnabled = process.env.DESKTOP_ENABLED === "1";
|
|
14
|
+
const MAX_RESPONSE_BYTES = 60 * 1024;
|
|
15
|
+
const terminals = new TerminalManager();
|
|
16
|
+
const files = new FileManager(process.env.ALLOWED_ROOTS);
|
|
17
|
+
const processes = new ProcessManager();
|
|
18
|
+
const desktop = new DesktopManager({ enabled: desktopEnabled });
|
|
19
|
+
const recentCalls = [];
|
|
20
|
+
let reauthorizationRequired = false;
|
|
21
|
+
|
|
22
|
+
if (!relayUrl || !agentToken) {
|
|
23
|
+
console.error("RELAY_URL and AGENT_TOKEN are required");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const wsUrl = relayUrl.replace(/^http/, "ws").replace(/\/$/, "") +
|
|
28
|
+
`/agent?agentId=${encodeURIComponent(agentId)}`;
|
|
29
|
+
|
|
30
|
+
function serializeResponse(requestId, payload) {
|
|
31
|
+
const response = JSON.stringify({ requestId, payload });
|
|
32
|
+
if (Buffer.byteLength(response, "utf8") <= MAX_RESPONSE_BYTES) return response;
|
|
33
|
+
return JSON.stringify({
|
|
34
|
+
requestId,
|
|
35
|
+
payload: {
|
|
36
|
+
ok: false,
|
|
37
|
+
error: "response_too_large",
|
|
38
|
+
maxBytes: MAX_RESPONSE_BYTES,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function handlePayload(payload) {
|
|
44
|
+
if (!payload || typeof payload !== "object") return payload;
|
|
45
|
+
|
|
46
|
+
switch (payload.action) {
|
|
47
|
+
case "ping":
|
|
48
|
+
return { ok: true, action: "pong", at: new Date().toISOString(), agentId, agentName };
|
|
49
|
+
|
|
50
|
+
case "agent.config":
|
|
51
|
+
return processes.info({
|
|
52
|
+
agentId,
|
|
53
|
+
agentName,
|
|
54
|
+
terminalEnabled,
|
|
55
|
+
desktop: desktop.getConfig(),
|
|
56
|
+
allowedRoots: files.getRoots(),
|
|
57
|
+
reconnectMs,
|
|
58
|
+
});
|
|
59
|
+
case "agent.recentCalls":
|
|
60
|
+
return recentCalls.slice(-(Math.min(Math.max(Number(payload.limit) || 50, 1), 100)));
|
|
61
|
+
|
|
62
|
+
case "fs.stat":
|
|
63
|
+
return files.stat(payload.path);
|
|
64
|
+
case "fs.list":
|
|
65
|
+
return files.list(payload.path, payload.depth);
|
|
66
|
+
case "fs.read":
|
|
67
|
+
return files.read(payload.path, payload.offset, payload.length, payload.maxBytes);
|
|
68
|
+
case "fs.readMany":
|
|
69
|
+
return files.readMany(payload.paths, payload.maxTotalBytes);
|
|
70
|
+
case "fs.write":
|
|
71
|
+
return files.write(payload.path, payload.content, payload.mode);
|
|
72
|
+
case "fs.edit":
|
|
73
|
+
return files.edit(payload.path, payload.oldText, payload.newText, payload.expectedReplacements);
|
|
74
|
+
case "fs.mkdir":
|
|
75
|
+
return files.mkdir(payload.path);
|
|
76
|
+
case "fs.move":
|
|
77
|
+
return files.move(payload.source, payload.destination);
|
|
78
|
+
case "fs.delete":
|
|
79
|
+
return files.remove(payload.path, payload.recursive);
|
|
80
|
+
case "fs.search.start":
|
|
81
|
+
return files.startSearch(payload.path, payload.pattern, payload.searchType, payload.maxResults);
|
|
82
|
+
case "fs.search.results":
|
|
83
|
+
return files.getSearchResults(payload.sessionId, payload.offset, payload.length);
|
|
84
|
+
|
|
85
|
+
case "process.list":
|
|
86
|
+
return processes.list(payload.filter);
|
|
87
|
+
case "process.kill":
|
|
88
|
+
return processes.kill(payload.pid);
|
|
89
|
+
|
|
90
|
+
case "desktop.screenshot":
|
|
91
|
+
return desktop.screenshot();
|
|
92
|
+
case "desktop.mouse.click":
|
|
93
|
+
return desktop.mouseClick(payload);
|
|
94
|
+
case "desktop.keyboard.input":
|
|
95
|
+
return desktop.keyboardInput(payload);
|
|
96
|
+
|
|
97
|
+
case "terminal.exec":
|
|
98
|
+
if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
|
|
99
|
+
return terminals.exec(payload.command, payload.cwd, payload.timeoutMs);
|
|
100
|
+
case "terminal.start":
|
|
101
|
+
if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
|
|
102
|
+
return terminals.start(payload.command, payload.cwd);
|
|
103
|
+
case "terminal.shell.start":
|
|
104
|
+
if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
|
|
105
|
+
return terminals.startShell(payload.cwd);
|
|
106
|
+
case "terminal.read":
|
|
107
|
+
if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
|
|
108
|
+
return terminals.read(payload.sessionId, payload.afterSeq, payload.maxChars);
|
|
109
|
+
case "terminal.write":
|
|
110
|
+
if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
|
|
111
|
+
return terminals.write(payload.sessionId, payload.input, payload.appendNewline !== false);
|
|
112
|
+
case "terminal.kill":
|
|
113
|
+
if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
|
|
114
|
+
return terminals.kill(payload.sessionId);
|
|
115
|
+
case "terminal.list":
|
|
116
|
+
if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
|
|
117
|
+
return terminals.list();
|
|
118
|
+
|
|
119
|
+
default:
|
|
120
|
+
return { ok: false, error: "unknown_action" };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function requireReauthorization() {
|
|
125
|
+
if (reauthorizationRequired) return;
|
|
126
|
+
reauthorizationRequired = true;
|
|
127
|
+
process.exitCode = 2;
|
|
128
|
+
console.error("Agent credential was revoked or rejected.");
|
|
129
|
+
console.error('Recovery: run "chat-relay login --force", then "chat-relay remote".');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function connect() {
|
|
133
|
+
if (reauthorizationRequired) return;
|
|
134
|
+
console.log(`Connecting to ${wsUrl}`);
|
|
135
|
+
console.log(`Terminal access: ${terminalEnabled ? "enabled" : "disabled"}`);
|
|
136
|
+
console.log("Desktop access: " + (desktopEnabled ? "enabled" : "disabled"));
|
|
137
|
+
const socket = new WebSocket(wsUrl, {
|
|
138
|
+
headers: { Authorization: `Bearer ${agentToken}` },
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
socket.on("open", () => console.log("Agent connected"));
|
|
142
|
+
|
|
143
|
+
socket.on("message", async (raw) => {
|
|
144
|
+
let message;
|
|
145
|
+
try {
|
|
146
|
+
message = JSON.parse(raw.toString());
|
|
147
|
+
} catch {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (message?.control === "credential_revoked") {
|
|
152
|
+
requireReauthorization();
|
|
153
|
+
try { socket.close(4001, "credential_revoked"); } catch {}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!message || typeof message.requestId !== "string") return;
|
|
158
|
+
|
|
159
|
+
const startedAt = Date.now();
|
|
160
|
+
const action = message.payload && typeof message.payload === "object"
|
|
161
|
+
? String(message.payload.action ?? "unknown")
|
|
162
|
+
: "unknown";
|
|
163
|
+
try {
|
|
164
|
+
const result = await handlePayload(message.payload);
|
|
165
|
+
recentCalls.push({
|
|
166
|
+
at: new Date().toISOString(),
|
|
167
|
+
action,
|
|
168
|
+
durationMs: Date.now() - startedAt,
|
|
169
|
+
ok: !(result && typeof result === "object" && result.ok === false),
|
|
170
|
+
});
|
|
171
|
+
if (recentCalls.length > 100) recentCalls.shift();
|
|
172
|
+
socket.send(serializeResponse(message.requestId, result));
|
|
173
|
+
} catch (error) {
|
|
174
|
+
recentCalls.push({
|
|
175
|
+
at: new Date().toISOString(),
|
|
176
|
+
action,
|
|
177
|
+
durationMs: Date.now() - startedAt,
|
|
178
|
+
ok: false,
|
|
179
|
+
error: error instanceof Error ? error.message : "agent_error",
|
|
180
|
+
});
|
|
181
|
+
if (recentCalls.length > 100) recentCalls.shift();
|
|
182
|
+
socket.send(serializeResponse(message.requestId, {
|
|
183
|
+
ok: false,
|
|
184
|
+
error: error instanceof Error ? error.message : "agent_error",
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
socket.on("unexpected-response", (_request, response) => {
|
|
190
|
+
if (response.statusCode === 401 || response.statusCode === 403) {
|
|
191
|
+
response.resume();
|
|
192
|
+
requireReauthorization();
|
|
193
|
+
socket.terminate();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
socket.on("close", (code, reason) => {
|
|
198
|
+
const reasonText = reason.toString();
|
|
199
|
+
console.log("Agent disconnected (" + code + ") " + reasonText);
|
|
200
|
+
if (code === 4001 || reasonText === "credential_revoked") {
|
|
201
|
+
requireReauthorization();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (!reauthorizationRequired) setTimeout(connect, reconnectMs);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
socket.on("error", (error) => {
|
|
208
|
+
if (!reauthorizationRequired) console.error("WebSocket error:", error.message);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
connect();
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[Parameter(Mandatory=$true)][string]$Operation,
|
|
3
|
+
[Parameter(Mandatory=$true)][string]$InputBase64
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
$ErrorActionPreference = "Stop"
|
|
7
|
+
$ProgressPreference = "SilentlyContinue"
|
|
8
|
+
|
|
9
|
+
function Write-Result($Value) {
|
|
10
|
+
$Value | ConvertTo-Json -Compress -Depth 8
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function Fail($Code) {
|
|
14
|
+
Write-Result ([pscustomobject]@{ ok = $false; error = $Code })
|
|
15
|
+
exit 0
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
$inputJson = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($InputBase64))
|
|
20
|
+
$payload = if ($inputJson) { $inputJson | ConvertFrom-Json } else { [pscustomobject]@{} }
|
|
21
|
+
|
|
22
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
23
|
+
Add-Type -AssemblyName System.Drawing
|
|
24
|
+
Add-Type @"
|
|
25
|
+
using System;
|
|
26
|
+
using System.Runtime.InteropServices;
|
|
27
|
+
|
|
28
|
+
public static class ChatRelayNative {
|
|
29
|
+
public const uint INPUT_MOUSE = 0;
|
|
30
|
+
public const uint INPUT_KEYBOARD = 1;
|
|
31
|
+
public const uint KEYEVENTF_KEYUP = 0x0002;
|
|
32
|
+
public const uint KEYEVENTF_UNICODE = 0x0004;
|
|
33
|
+
|
|
34
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
35
|
+
public struct MOUSEINPUT {
|
|
36
|
+
public int dx;
|
|
37
|
+
public int dy;
|
|
38
|
+
public uint mouseData;
|
|
39
|
+
public uint dwFlags;
|
|
40
|
+
public uint time;
|
|
41
|
+
public UIntPtr dwExtraInfo;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
45
|
+
public struct KEYBDINPUT {
|
|
46
|
+
public ushort wVk;
|
|
47
|
+
public ushort wScan;
|
|
48
|
+
public uint dwFlags;
|
|
49
|
+
public uint time;
|
|
50
|
+
public UIntPtr dwExtraInfo;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
[StructLayout(LayoutKind.Explicit)]
|
|
54
|
+
public struct INPUTUNION {
|
|
55
|
+
[FieldOffset(0)] public MOUSEINPUT mi;
|
|
56
|
+
[FieldOffset(0)] public KEYBDINPUT ki;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
60
|
+
public struct INPUT {
|
|
61
|
+
public uint type;
|
|
62
|
+
public INPUTUNION U;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
[DllImport("user32.dll")] public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
|
|
66
|
+
[DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
|
|
67
|
+
[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
|
|
68
|
+
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
|
69
|
+
|
|
70
|
+
public static bool MouseButton(uint downFlag, uint upFlag) {
|
|
71
|
+
var inputs = new INPUT[2];
|
|
72
|
+
inputs[0].type = INPUT_MOUSE;
|
|
73
|
+
inputs[0].U.mi.dwFlags = downFlag;
|
|
74
|
+
inputs[1].type = INPUT_MOUSE;
|
|
75
|
+
inputs[1].U.mi.dwFlags = upFlag;
|
|
76
|
+
return SendInput(2, inputs, Marshal.SizeOf(typeof(INPUT))) == 2;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public static bool Key(ushort virtualKey, bool down) {
|
|
80
|
+
var input = new INPUT[1];
|
|
81
|
+
input[0].type = INPUT_KEYBOARD;
|
|
82
|
+
input[0].U.ki.wVk = virtualKey;
|
|
83
|
+
input[0].U.ki.dwFlags = down ? 0u : KEYEVENTF_KEYUP;
|
|
84
|
+
return SendInput(1, input, Marshal.SizeOf(typeof(INPUT))) == 1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public static bool UnicodeText(string text) {
|
|
88
|
+
foreach (char c in text) {
|
|
89
|
+
var inputs = new INPUT[2];
|
|
90
|
+
inputs[0].type = INPUT_KEYBOARD;
|
|
91
|
+
inputs[0].U.ki.wScan = c;
|
|
92
|
+
inputs[0].U.ki.dwFlags = KEYEVENTF_UNICODE;
|
|
93
|
+
inputs[1].type = INPUT_KEYBOARD;
|
|
94
|
+
inputs[1].U.ki.wScan = c;
|
|
95
|
+
inputs[1].U.ki.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP;
|
|
96
|
+
if (SendInput(2, inputs, Marshal.SizeOf(typeof(INPUT))) != 2) return false;
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
"@
|
|
102
|
+
|
|
103
|
+
[ChatRelayNative]::SetProcessDPIAware() | Out-Null
|
|
104
|
+
if (-not [System.Windows.Forms.SystemInformation]::UserInteractive) {
|
|
105
|
+
Fail "session_unavailable"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if ($Operation -eq "screenshot") {
|
|
109
|
+
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
|
|
110
|
+
if ($null -eq $screen) { Fail "session_unavailable" }
|
|
111
|
+
$bounds = $screen.Bounds
|
|
112
|
+
if ($bounds.Width -le 0 -or $bounds.Height -le 0) { Fail "session_unavailable" }
|
|
113
|
+
|
|
114
|
+
$source = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
|
|
115
|
+
$graphics = [System.Drawing.Graphics]::FromImage($source)
|
|
116
|
+
try {
|
|
117
|
+
$graphics.CopyFromScreen($bounds.Left, $bounds.Top, 0, 0, $bounds.Size)
|
|
118
|
+
} finally {
|
|
119
|
+
$graphics.Dispose()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
$codec = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() |
|
|
123
|
+
Where-Object { $_.MimeType -eq "image/jpeg" } |
|
|
124
|
+
Select-Object -First 1
|
|
125
|
+
if ($null -eq $codec) {
|
|
126
|
+
$source.Dispose()
|
|
127
|
+
Fail "capture_failed"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
$maxBytes = 32768
|
|
131
|
+
$targetWidth = [Math]::Min($bounds.Width, 960)
|
|
132
|
+
$scale = $targetWidth / [double]$bounds.Width
|
|
133
|
+
$targetHeight = [Math]::Max(1, [int][Math]::Round($bounds.Height * $scale))
|
|
134
|
+
$qualities = @(70L, 55L, 42L, 32L, 24L)
|
|
135
|
+
$bytes = $null
|
|
136
|
+
$finalWidth = $targetWidth
|
|
137
|
+
$finalHeight = $targetHeight
|
|
138
|
+
|
|
139
|
+
for ($sizeAttempt = 0; $sizeAttempt -lt 4 -and $null -eq $bytes; $sizeAttempt++) {
|
|
140
|
+
$resized = New-Object System.Drawing.Bitmap $targetWidth, $targetHeight
|
|
141
|
+
$draw = [System.Drawing.Graphics]::FromImage($resized)
|
|
142
|
+
try {
|
|
143
|
+
$draw.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
|
144
|
+
$draw.DrawImage($source, 0, 0, $targetWidth, $targetHeight)
|
|
145
|
+
} finally {
|
|
146
|
+
$draw.Dispose()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
foreach ($quality in $qualities) {
|
|
150
|
+
$stream = New-Object System.IO.MemoryStream
|
|
151
|
+
$parameters = New-Object System.Drawing.Imaging.EncoderParameters 1
|
|
152
|
+
$parameters.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter(
|
|
153
|
+
[System.Drawing.Imaging.Encoder]::Quality,
|
|
154
|
+
$quality
|
|
155
|
+
)
|
|
156
|
+
try {
|
|
157
|
+
$resized.Save($stream, $codec, $parameters)
|
|
158
|
+
if ($stream.Length -le $maxBytes) {
|
|
159
|
+
$bytes = $stream.ToArray()
|
|
160
|
+
$finalWidth = $targetWidth
|
|
161
|
+
$finalHeight = $targetHeight
|
|
162
|
+
break
|
|
163
|
+
}
|
|
164
|
+
} finally {
|
|
165
|
+
$parameters.Dispose()
|
|
166
|
+
$stream.Dispose()
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
$resized.Dispose()
|
|
171
|
+
if ($null -eq $bytes) {
|
|
172
|
+
$targetWidth = [Math]::Max(320, [int][Math]::Floor($targetWidth * 0.78))
|
|
173
|
+
$targetHeight = [Math]::Max(180, [int][Math]::Round($bounds.Height * ($targetWidth / [double]$bounds.Width)))
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
$source.Dispose()
|
|
178
|
+
if ($null -eq $bytes) { Fail "screenshot_too_large" }
|
|
179
|
+
|
|
180
|
+
Write-Result ([pscustomobject]@{
|
|
181
|
+
ok = $true
|
|
182
|
+
mimeType = "image/jpeg"
|
|
183
|
+
data = [Convert]::ToBase64String($bytes)
|
|
184
|
+
width = $finalWidth
|
|
185
|
+
height = $finalHeight
|
|
186
|
+
desktopOriginX = $bounds.Left
|
|
187
|
+
desktopOriginY = $bounds.Top
|
|
188
|
+
desktopWidth = $bounds.Width
|
|
189
|
+
desktopHeight = $bounds.Height
|
|
190
|
+
scaleX = $bounds.Width / [double]$finalWidth
|
|
191
|
+
scaleY = $bounds.Height / [double]$finalHeight
|
|
192
|
+
byteLength = $bytes.Length
|
|
193
|
+
})
|
|
194
|
+
exit 0
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if ([ChatRelayNative]::GetForegroundWindow() -eq [IntPtr]::Zero) {
|
|
198
|
+
Fail "session_unavailable"
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if ($Operation -eq "mouse_click") {
|
|
202
|
+
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
|
|
203
|
+
if ($null -eq $screen) { Fail "session_unavailable" }
|
|
204
|
+
$bounds = $screen.Bounds
|
|
205
|
+
$x = [int]$payload.x
|
|
206
|
+
$y = [int]$payload.y
|
|
207
|
+
if ($x -lt $bounds.Left -or $x -ge $bounds.Right -or $y -lt $bounds.Top -or $y -ge $bounds.Bottom) {
|
|
208
|
+
Fail "invalid_coordinates"
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
switch ([string]$payload.button) {
|
|
212
|
+
"left" { $down = 0x0002; $up = 0x0004 }
|
|
213
|
+
"right" { $down = 0x0008; $up = 0x0010 }
|
|
214
|
+
"middle" { $down = 0x0020; $up = 0x0040 }
|
|
215
|
+
default { Fail "invalid_button" }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (-not [ChatRelayNative]::SetCursorPos($x, $y)) { Fail "input_failed" }
|
|
219
|
+
$clicks = [int]$payload.clicks
|
|
220
|
+
for ($i = 0; $i -lt $clicks; $i++) {
|
|
221
|
+
if (-not [ChatRelayNative]::MouseButton($down, $up)) { Fail "input_failed" }
|
|
222
|
+
if ($i + 1 -lt $clicks) { Start-Sleep -Milliseconds 100 }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
Write-Result ([pscustomobject]@{ ok = $true; action = "mouse_click" })
|
|
226
|
+
exit 0
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if ($Operation -eq "keyboard_input") {
|
|
230
|
+
if ($null -ne $payload.text) {
|
|
231
|
+
if (-not [ChatRelayNative]::UnicodeText([string]$payload.text)) { Fail "input_failed" }
|
|
232
|
+
} else {
|
|
233
|
+
$mods = New-Object System.Collections.Generic.List[uint16]
|
|
234
|
+
if ($payload.ctrl) { $mods.Add(0x11) }
|
|
235
|
+
if ($payload.alt) { $mods.Add(0x12) }
|
|
236
|
+
if ($payload.shift) { $mods.Add(0x10) }
|
|
237
|
+
if ($payload.win) { $mods.Add(0x5B) }
|
|
238
|
+
|
|
239
|
+
foreach ($vk in $mods) {
|
|
240
|
+
if (-not [ChatRelayNative]::Key($vk, $true)) { Fail "input_failed" }
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
$keyCode = [uint16]$payload.keyCode
|
|
244
|
+
if (-not [ChatRelayNative]::Key($keyCode, $true)) { Fail "input_failed" }
|
|
245
|
+
if (-not [ChatRelayNative]::Key($keyCode, $false)) { Fail "input_failed" }
|
|
246
|
+
} finally {
|
|
247
|
+
for ($i = $mods.Count - 1; $i -ge 0; $i--) {
|
|
248
|
+
[ChatRelayNative]::Key($mods[$i], $false) | Out-Null
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
Write-Result ([pscustomobject]@{ ok = $true; action = "keyboard_input" })
|
|
254
|
+
exit 0
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
Fail "unknown_desktop_operation"
|
|
258
|
+
} catch {
|
|
259
|
+
$message = [string]$_.Exception.Message
|
|
260
|
+
if ($message -match "session_unavailable") { Fail "session_unavailable" }
|
|
261
|
+
if ($Operation -eq "screenshot") { Fail "capture_failed" }
|
|
262
|
+
Fail "input_failed"
|
|
263
|
+
}
|