@elizaos/plugin-screenshare 2.0.3-beta.5 → 2.0.3-beta.7
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/dist/components/ScreenshareSpatialView.d.ts +61 -0
- package/dist/components/ScreenshareSpatialView.d.ts.map +1 -0
- package/dist/components/ScreenshareSpatialView.js +206 -0
- package/dist/components/ScreenshareSpatialView.js.map +1 -0
- package/dist/components/ScreenshareView.d.ts +13 -0
- package/dist/components/ScreenshareView.d.ts.map +1 -0
- package/dist/components/ScreenshareView.js +263 -0
- package/dist/components/ScreenshareView.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/register-terminal-view.d.ts +15 -0
- package/dist/register-terminal-view.d.ts.map +1 -0
- package/dist/register-terminal-view.js +27 -0
- package/dist/register-terminal-view.js.map +1 -0
- package/dist/routes.d.ts +8 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +639 -0
- package/dist/routes.js.map +1 -0
- package/dist/session-store.d.ts +44 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/session-store.js +200 -0
- package/dist/session-store.js.map +1 -0
- package/dist/ui/ScreenshareOperatorSurface.d.ts +5 -0
- package/dist/ui/ScreenshareOperatorSurface.d.ts.map +1 -0
- package/dist/ui/ScreenshareOperatorSurface.helpers.d.ts +40 -0
- package/dist/ui/ScreenshareOperatorSurface.helpers.d.ts.map +1 -0
- package/dist/ui/ScreenshareOperatorSurface.helpers.js +47 -0
- package/dist/ui/ScreenshareOperatorSurface.helpers.js.map +1 -0
- package/dist/ui/ScreenshareOperatorSurface.interact.d.ts +2 -0
- package/dist/ui/ScreenshareOperatorSurface.interact.d.ts.map +1 -0
- package/dist/ui/ScreenshareOperatorSurface.interact.js +100 -0
- package/dist/ui/ScreenshareOperatorSurface.interact.js.map +1 -0
- package/dist/ui/ScreenshareOperatorSurface.js +746 -0
- package/dist/ui/ScreenshareOperatorSurface.js.map +1 -0
- package/dist/ui/index.d.ts +3 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +10 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/screenshare-view-bundle.d.ts +3 -0
- package/dist/ui/screenshare-view-bundle.d.ts.map +1 -0
- package/dist/ui/screenshare-view-bundle.js +7 -0
- package/dist/ui/screenshare-view-bundle.js.map +1 -0
- package/dist/views/bundle.js +507 -0
- package/dist/views/bundle.js.map +1 -0
- package/package.json +8 -8
package/dist/routes.js
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
import {
|
|
2
|
+
captureDesktopScreenshot,
|
|
3
|
+
listDesktopWindows,
|
|
4
|
+
performDesktopClick,
|
|
5
|
+
performDesktopDoubleClick,
|
|
6
|
+
performDesktopKeypress,
|
|
7
|
+
performDesktopMouseMove,
|
|
8
|
+
performDesktopScroll,
|
|
9
|
+
performDesktopTextInput
|
|
10
|
+
} from "@elizaos/plugin-computeruse";
|
|
11
|
+
import {
|
|
12
|
+
buildScreenshareAppSession,
|
|
13
|
+
canAccessScreenshareSession,
|
|
14
|
+
createScreenshareSession,
|
|
15
|
+
getOrCreateLocalScreenshareSession,
|
|
16
|
+
getScreenshareCapabilities,
|
|
17
|
+
getScreenshareSession,
|
|
18
|
+
listScreenshareSessions,
|
|
19
|
+
recordScreenshareFrame,
|
|
20
|
+
recordScreenshareInput,
|
|
21
|
+
stopScreenshareSession,
|
|
22
|
+
toPublicSession
|
|
23
|
+
} from "./session-store.js";
|
|
24
|
+
const BASE_PATH = "/api/apps/screenshare";
|
|
25
|
+
const VIEWER_SANDBOX = "allow-scripts allow-same-origin allow-forms allow-pointer-lock";
|
|
26
|
+
const MAX_TEXT_INPUT_LENGTH = 4096;
|
|
27
|
+
const SESSION_CREATE_LIMIT = 10;
|
|
28
|
+
const SESSION_CREATE_WINDOW_MS = 6e4;
|
|
29
|
+
const sessionCreateCounts = /* @__PURE__ */ new Map();
|
|
30
|
+
function getRemoteIp(req) {
|
|
31
|
+
const addr = req?.socket?.remoteAddress ?? req?.headers?.["x-forwarded-for"];
|
|
32
|
+
return (Array.isArray(addr) ? addr[0] : addr ?? "unknown").split(",")[0].trim();
|
|
33
|
+
}
|
|
34
|
+
function sessionCreateRateLimitExceeded(req) {
|
|
35
|
+
const ip = getRemoteIp(req);
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
const entry = sessionCreateCounts.get(ip);
|
|
38
|
+
if (!entry || entry.resetAt <= now) {
|
|
39
|
+
sessionCreateCounts.set(ip, {
|
|
40
|
+
count: 1,
|
|
41
|
+
resetAt: now + SESSION_CREATE_WINDOW_MS
|
|
42
|
+
});
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
entry.count += 1;
|
|
46
|
+
return entry.count > SESSION_CREATE_LIMIT;
|
|
47
|
+
}
|
|
48
|
+
const MAX_KEYPRESS_LENGTH = 128;
|
|
49
|
+
const SAFE_KEYPRESS_PATTERN = /^[A-Za-z0-9+_.,: -]+$/;
|
|
50
|
+
async function prepareLaunch(_ctx) {
|
|
51
|
+
const session = getOrCreateLocalScreenshareSession();
|
|
52
|
+
const viewerUrl = buildViewerUrl(session);
|
|
53
|
+
return {
|
|
54
|
+
launchUrl: viewerUrl,
|
|
55
|
+
viewer: {
|
|
56
|
+
url: viewerUrl,
|
|
57
|
+
sandbox: VIEWER_SANDBOX
|
|
58
|
+
},
|
|
59
|
+
skipRuntimePluginRegistration: true,
|
|
60
|
+
diagnostics: collectCapabilityDiagnostics()
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async function resolveLaunchSession(_ctx) {
|
|
64
|
+
return buildScreenshareAppSession(getOrCreateLocalScreenshareSession());
|
|
65
|
+
}
|
|
66
|
+
async function refreshRunSession(ctx) {
|
|
67
|
+
const sessionId = ctx.session?.sessionId;
|
|
68
|
+
if (!sessionId) {
|
|
69
|
+
return buildScreenshareAppSession(getOrCreateLocalScreenshareSession());
|
|
70
|
+
}
|
|
71
|
+
const session = getScreenshareSession(sessionId);
|
|
72
|
+
if (!session || session.status !== "active") {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return buildScreenshareAppSession(session);
|
|
76
|
+
}
|
|
77
|
+
async function stopRun(ctx) {
|
|
78
|
+
const sessionId = ctx.session?.sessionId;
|
|
79
|
+
if (sessionId) {
|
|
80
|
+
stopScreenshareSession(sessionId);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function handleAppRoutes(ctx) {
|
|
84
|
+
if (!ctx.pathname.startsWith(BASE_PATH)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (ctx.method === "GET" && ctx.pathname === `${BASE_PATH}/viewer`) {
|
|
88
|
+
sendHtml(ctx.res, renderViewerHtml());
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
if (ctx.method === "GET" && ctx.pathname === `${BASE_PATH}/capabilities`) {
|
|
92
|
+
ctx.json(ctx.res, {
|
|
93
|
+
platform: process.platform,
|
|
94
|
+
capabilities: getScreenshareCapabilities()
|
|
95
|
+
});
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
if (ctx.method === "GET" && ctx.pathname === `${BASE_PATH}/windows`) {
|
|
99
|
+
try {
|
|
100
|
+
ctx.json(ctx.res, { windows: listDesktopWindows() });
|
|
101
|
+
} catch (error) {
|
|
102
|
+
ctx.error(
|
|
103
|
+
ctx.res,
|
|
104
|
+
error instanceof Error ? error.message : "Desktop window listing failed.",
|
|
105
|
+
500
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (ctx.method === "GET" && ctx.pathname === `${BASE_PATH}/sessions`) {
|
|
111
|
+
ctx.json(ctx.res, { sessions: listScreenshareSessions() });
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
if (ctx.method === "POST" && ctx.pathname === `${BASE_PATH}/session`) {
|
|
115
|
+
if (sessionCreateRateLimitExceeded(ctx.req)) {
|
|
116
|
+
ctx.error(
|
|
117
|
+
ctx.res,
|
|
118
|
+
"Too many session creation requests. Please wait before trying again.",
|
|
119
|
+
429
|
|
120
|
+
);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
const body = await ctx.readJsonBody();
|
|
124
|
+
if (body === null) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
const session2 = createScreenshareSession(readLabel(body.label));
|
|
128
|
+
ctx.json(ctx.res, {
|
|
129
|
+
session: toPublicSession(session2),
|
|
130
|
+
token: session2.token,
|
|
131
|
+
viewerUrl: buildViewerUrl(session2)
|
|
132
|
+
});
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
const match = ctx.pathname.match(
|
|
136
|
+
/^\/api\/apps\/screenshare\/session\/([^/]+)(?:\/([^/]+))?$/
|
|
137
|
+
);
|
|
138
|
+
if (!match?.[1]) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
const sessionId = decodeURIComponent(match[1]);
|
|
142
|
+
const subroute = match[2] ? decodeURIComponent(match[2]) : "";
|
|
143
|
+
const session = getScreenshareSession(sessionId);
|
|
144
|
+
if (!session) {
|
|
145
|
+
ctx.error(
|
|
146
|
+
ctx.res,
|
|
147
|
+
`Screen share session "${sessionId}" was not found.`,
|
|
148
|
+
404
|
|
149
|
+
);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
if (ctx.method === "GET" && !subroute) {
|
|
153
|
+
const token = readRequestToken(ctx);
|
|
154
|
+
if (!canAccessScreenshareSession(session, token)) {
|
|
155
|
+
ctx.error(ctx.res, "Invalid screen share token.", 403);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
ctx.json(ctx.res, { session: toPublicSession(session) });
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
if (ctx.method === "GET" && subroute === "frame") {
|
|
162
|
+
const token = readRequestToken(ctx);
|
|
163
|
+
if (!canAccessScreenshareSession(session, token)) {
|
|
164
|
+
ctx.error(ctx.res, "Invalid screen share token.", 403);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
if (session.status !== "active") {
|
|
168
|
+
ctx.error(ctx.res, "Screen share session is stopped.", 409);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
const region = readFrameRegion(ctx.url);
|
|
172
|
+
try {
|
|
173
|
+
const screenshot = captureDesktopScreenshot(region);
|
|
174
|
+
recordScreenshareFrame(session.id);
|
|
175
|
+
sendPng(ctx.res, screenshot);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
ctx.error(
|
|
178
|
+
ctx.res,
|
|
179
|
+
error instanceof Error ? error.message : "Screenshot failed.",
|
|
180
|
+
500
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
if (ctx.method === "POST" && subroute === "input") {
|
|
186
|
+
const body = await ctx.readJsonBody();
|
|
187
|
+
if (body === null) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
const token = readBodyToken(body) ?? readRequestToken(ctx);
|
|
191
|
+
if (!canAccessScreenshareSession(session, token)) {
|
|
192
|
+
ctx.error(ctx.res, "Invalid screen share token.", 403);
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
if (session.status !== "active") {
|
|
196
|
+
ctx.error(ctx.res, "Screen share session is stopped.", 409);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
let result;
|
|
200
|
+
try {
|
|
201
|
+
result = executeInput(body);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
ctx.error(
|
|
204
|
+
ctx.res,
|
|
205
|
+
error instanceof Error ? error.message : "Desktop input failed.",
|
|
206
|
+
500
|
|
207
|
+
);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
if (!result.success) {
|
|
211
|
+
ctx.error(ctx.res, result.message, 400);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
const updated = recordScreenshareInput(session.id) ?? session;
|
|
215
|
+
ctx.json(ctx.res, {
|
|
216
|
+
success: true,
|
|
217
|
+
message: result.message,
|
|
218
|
+
session: toPublicSession(updated)
|
|
219
|
+
});
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
if (ctx.method === "POST" && subroute === "stop") {
|
|
223
|
+
const body = await ctx.readJsonBody();
|
|
224
|
+
if (body === null) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
const token = readBodyToken(body) ?? readRequestToken(ctx);
|
|
228
|
+
if (!canAccessScreenshareSession(session, token)) {
|
|
229
|
+
ctx.error(ctx.res, "Invalid screen share token.", 403);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
const stopped = stopScreenshareSession(session.id) ?? session;
|
|
233
|
+
ctx.json(ctx.res, { session: toPublicSession(stopped) });
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
function collectCapabilityDiagnostics() {
|
|
239
|
+
const capabilities = getScreenshareCapabilities();
|
|
240
|
+
return Object.entries(capabilities).filter(([, capability]) => !capability.available).map(([code, capability]) => ({
|
|
241
|
+
code: `screenshare-${code}-unavailable`,
|
|
242
|
+
severity: "warning",
|
|
243
|
+
message: `${code} unavailable: ${capability.tool}`
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
function buildViewerUrl(session) {
|
|
247
|
+
const params = new URLSearchParams({
|
|
248
|
+
sessionId: session.id,
|
|
249
|
+
token: session.token,
|
|
250
|
+
mode: "host"
|
|
251
|
+
});
|
|
252
|
+
return `${BASE_PATH}/viewer?${params.toString()}`;
|
|
253
|
+
}
|
|
254
|
+
function readLabel(value) {
|
|
255
|
+
return typeof value === "string" && value.trim() ? value.trim().slice(0, 80) : "This machine";
|
|
256
|
+
}
|
|
257
|
+
function readRequestToken(ctx) {
|
|
258
|
+
const queryToken = ctx.url.searchParams.get("token");
|
|
259
|
+
if (queryToken?.trim()) {
|
|
260
|
+
return queryToken.trim();
|
|
261
|
+
}
|
|
262
|
+
const req = ctx.req;
|
|
263
|
+
const headerToken = req.headers["x-screenshare-token"];
|
|
264
|
+
if (typeof headerToken === "string" && headerToken.trim()) {
|
|
265
|
+
return headerToken.trim();
|
|
266
|
+
}
|
|
267
|
+
const authorization = req.headers.authorization;
|
|
268
|
+
if (typeof authorization === "string") {
|
|
269
|
+
const match = authorization.match(/^Bearer\s+(.+)$/i);
|
|
270
|
+
const token = match?.[1]?.trim();
|
|
271
|
+
if (token) {
|
|
272
|
+
return token;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
function readBodyToken(body) {
|
|
278
|
+
return typeof body.token === "string" && body.token.trim() ? body.token.trim() : null;
|
|
279
|
+
}
|
|
280
|
+
function readFrameRegion(url) {
|
|
281
|
+
const x = readIntegerParam(url, "x");
|
|
282
|
+
const y = readIntegerParam(url, "y");
|
|
283
|
+
const width = readIntegerParam(url, "width");
|
|
284
|
+
const height = readIntegerParam(url, "height");
|
|
285
|
+
if (x === null || y === null || width === null || height === null) {
|
|
286
|
+
return void 0;
|
|
287
|
+
}
|
|
288
|
+
if (width <= 0 || height <= 0) {
|
|
289
|
+
return void 0;
|
|
290
|
+
}
|
|
291
|
+
return { x, y, width, height };
|
|
292
|
+
}
|
|
293
|
+
function readIntegerParam(url, key) {
|
|
294
|
+
const raw = url.searchParams.get(key);
|
|
295
|
+
if (raw === null || raw.trim() === "") {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const parsed = Number(raw);
|
|
299
|
+
return Number.isInteger(parsed) ? parsed : null;
|
|
300
|
+
}
|
|
301
|
+
function executeInput(body) {
|
|
302
|
+
const type = typeof body.type === "string" ? body.type.trim() : "";
|
|
303
|
+
if (type === "click" || type === "double-click" || type === "move") {
|
|
304
|
+
const point = readPoint(body);
|
|
305
|
+
if (!point) {
|
|
306
|
+
return { success: false, message: "Input requires integer x and y." };
|
|
307
|
+
}
|
|
308
|
+
if (type === "move") {
|
|
309
|
+
performDesktopMouseMove(point.x, point.y);
|
|
310
|
+
return { success: true, message: "Pointer moved." };
|
|
311
|
+
}
|
|
312
|
+
const button = readButton(body.button);
|
|
313
|
+
if (!button) {
|
|
314
|
+
return { success: false, message: "button must be left or right." };
|
|
315
|
+
}
|
|
316
|
+
if (type === "double-click") {
|
|
317
|
+
performDesktopDoubleClick(point.x, point.y, button);
|
|
318
|
+
return { success: true, message: "Double-click sent." };
|
|
319
|
+
}
|
|
320
|
+
performDesktopClick(point.x, point.y, button);
|
|
321
|
+
return { success: true, message: "Click sent." };
|
|
322
|
+
}
|
|
323
|
+
if (type === "type") {
|
|
324
|
+
if (typeof body.text !== "string" || body.text.length === 0) {
|
|
325
|
+
return { success: false, message: "text is required." };
|
|
326
|
+
}
|
|
327
|
+
if (body.text.length > MAX_TEXT_INPUT_LENGTH) {
|
|
328
|
+
return {
|
|
329
|
+
success: false,
|
|
330
|
+
message: `text exceeds maximum length (${MAX_TEXT_INPUT_LENGTH}).`
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
performDesktopTextInput(body.text);
|
|
334
|
+
return { success: true, message: "Text sent." };
|
|
335
|
+
}
|
|
336
|
+
if (type === "keypress") {
|
|
337
|
+
if (typeof body.keys !== "string" || !body.keys.trim()) {
|
|
338
|
+
return { success: false, message: "keys is required." };
|
|
339
|
+
}
|
|
340
|
+
const keys = body.keys.trim();
|
|
341
|
+
if (keys.length > MAX_KEYPRESS_LENGTH) {
|
|
342
|
+
return {
|
|
343
|
+
success: false,
|
|
344
|
+
message: `keys exceeds maximum length (${MAX_KEYPRESS_LENGTH}).`
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
if (!SAFE_KEYPRESS_PATTERN.test(keys)) {
|
|
348
|
+
return {
|
|
349
|
+
success: false,
|
|
350
|
+
message: "keys contains unsupported characters; allowed: letters, numbers, space, +, _, ., ,, :, -"
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
performDesktopKeypress(keys);
|
|
354
|
+
return { success: true, message: "Keypress sent." };
|
|
355
|
+
}
|
|
356
|
+
if (type === "scroll") {
|
|
357
|
+
const deltaX = readInteger(body.deltaX) ?? 0;
|
|
358
|
+
const deltaY = readInteger(body.deltaY) ?? 0;
|
|
359
|
+
performDesktopScroll(deltaX, deltaY);
|
|
360
|
+
return { success: true, message: "Scroll sent." };
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
success: false,
|
|
364
|
+
message: "type must be one of: click, double-click, move, type, keypress, scroll."
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function readPoint(body) {
|
|
368
|
+
const x = readInteger(body.x);
|
|
369
|
+
const y = readInteger(body.y);
|
|
370
|
+
return x === null || y === null ? null : { x, y };
|
|
371
|
+
}
|
|
372
|
+
function readInteger(value) {
|
|
373
|
+
return typeof value === "number" && Number.isInteger(value) ? value : null;
|
|
374
|
+
}
|
|
375
|
+
function readButton(value) {
|
|
376
|
+
if (value === void 0 || value === null || value === "left") {
|
|
377
|
+
return "left";
|
|
378
|
+
}
|
|
379
|
+
return value === "right" ? "right" : null;
|
|
380
|
+
}
|
|
381
|
+
function sendPng(response, png) {
|
|
382
|
+
const res = response;
|
|
383
|
+
res.writeHead(200, {
|
|
384
|
+
"Content-Type": "image/png",
|
|
385
|
+
"Content-Length": png.byteLength,
|
|
386
|
+
"Cache-Control": "no-store"
|
|
387
|
+
});
|
|
388
|
+
res.end(png);
|
|
389
|
+
}
|
|
390
|
+
function sendHtml(response, html) {
|
|
391
|
+
const data = Buffer.from(html, "utf8");
|
|
392
|
+
const res = response;
|
|
393
|
+
res.writeHead(200, {
|
|
394
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
395
|
+
"Content-Length": data.byteLength,
|
|
396
|
+
"Cache-Control": "no-store"
|
|
397
|
+
});
|
|
398
|
+
res.end(data);
|
|
399
|
+
}
|
|
400
|
+
function renderViewerHtml() {
|
|
401
|
+
return `<!doctype html>
|
|
402
|
+
<html lang="en">
|
|
403
|
+
<head>
|
|
404
|
+
<meta charset="utf-8" />
|
|
405
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
406
|
+
<title>Screen Share</title>
|
|
407
|
+
<style>
|
|
408
|
+
:root{color-scheme:dark;--bg:#0b0d0f;--panel:#15181c;--line:#2a3036;--txt:#edf1f4;--muted:#9aa7ae;--accent:#d4b45e;--ok:#70d6a7;--danger:#f17a7a}
|
|
409
|
+
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--txt);font:13px/1.45 "Poppins",Arial,system-ui,sans-serif}
|
|
410
|
+
main{display:grid;grid-template-rows:auto 1fr auto;min-height:100vh}
|
|
411
|
+
.bar{display:flex;gap:10px;align-items:center;border-bottom:1px solid var(--line);background:var(--panel);padding:10px 12px}
|
|
412
|
+
.status{display:inline-flex;align-items:center;gap:8px;min-width:0;white-space:nowrap}.dot{width:8px;height:8px;border-radius:999px;background:var(--muted)}.dot.live{background:var(--ok)}.dot.err{background:var(--danger)}
|
|
413
|
+
.spacer{flex:1}.btn,.input{height:32px;border:1px solid var(--line);border-radius:8px;background:#0f1215;color:var(--txt);padding:0 10px}.btn{cursor:pointer}.btn:hover{border-color:var(--accent)}.btn:disabled{cursor:not-allowed;opacity:.55}.input{min-width:0}
|
|
414
|
+
.stage{display:grid;place-items:center;min-height:0;background:#050607;overflow:hidden;position:relative}.frame{max-width:100%;max-height:100%;object-fit:contain;user-select:none;outline:none}.empty{color:var(--muted);text-align:center;padding:24px}
|
|
415
|
+
.controls{display:grid;grid-template-columns:1fr auto auto auto;gap:8px;border-top:1px solid var(--line);background:var(--panel);padding:10px 12px}.keys{display:flex;gap:8px;min-width:0}
|
|
416
|
+
@media(max-width:720px){.bar,.controls{grid-template-columns:1fr;flex-wrap:wrap}.controls{display:flex}.keys{width:100%}.input{flex:1}}
|
|
417
|
+
</style>
|
|
418
|
+
</head>
|
|
419
|
+
<body>
|
|
420
|
+
<main>
|
|
421
|
+
<div class="bar">
|
|
422
|
+
<div class="status"><span id="dot" class="dot"></span><span id="status">Connecting</span></div>
|
|
423
|
+
<div class="spacer"></div>
|
|
424
|
+
<input id="base" class="input" placeholder="Server URL" />
|
|
425
|
+
<input id="session" class="input" placeholder="Session" />
|
|
426
|
+
<input id="token" class="input" placeholder="Token" />
|
|
427
|
+
<button id="connect" class="btn" type="button">Connect</button>
|
|
428
|
+
</div>
|
|
429
|
+
<div id="stage" class="stage" tabindex="0">
|
|
430
|
+
<img id="frame" class="frame" alt="Remote desktop stream" draggable="false" />
|
|
431
|
+
<div id="empty" class="empty">No stream selected.</div>
|
|
432
|
+
</div>
|
|
433
|
+
<div class="controls">
|
|
434
|
+
<div class="keys">
|
|
435
|
+
<input id="text" class="input" placeholder="Text" />
|
|
436
|
+
<button id="type" class="btn" type="button">Type</button>
|
|
437
|
+
</div>
|
|
438
|
+
<button class="btn" data-key="Enter" type="button">Enter</button>
|
|
439
|
+
<button class="btn" data-key="Escape" type="button">Esc</button>
|
|
440
|
+
<button id="stop" class="btn" type="button">Stop</button>
|
|
441
|
+
</div>
|
|
442
|
+
</main>
|
|
443
|
+
<script>
|
|
444
|
+
(() => {
|
|
445
|
+
const params = new URLSearchParams(location.search);
|
|
446
|
+
const state = {
|
|
447
|
+
base: params.get("remoteBase") || "",
|
|
448
|
+
sessionId: params.get("sessionId") || "",
|
|
449
|
+
token: params.get("token") || "",
|
|
450
|
+
running: false,
|
|
451
|
+
timer: 0,
|
|
452
|
+
frameObjectUrl: ""
|
|
453
|
+
};
|
|
454
|
+
const dot = document.getElementById("dot");
|
|
455
|
+
const status = document.getElementById("status");
|
|
456
|
+
const frame = document.getElementById("frame");
|
|
457
|
+
const empty = document.getElementById("empty");
|
|
458
|
+
const stage = document.getElementById("stage");
|
|
459
|
+
const base = document.getElementById("base");
|
|
460
|
+
const session = document.getElementById("session");
|
|
461
|
+
const token = document.getElementById("token");
|
|
462
|
+
const text = document.getElementById("text");
|
|
463
|
+
base.value = state.base;
|
|
464
|
+
session.value = state.sessionId;
|
|
465
|
+
token.value = state.token;
|
|
466
|
+
|
|
467
|
+
function endpoint(path) {
|
|
468
|
+
const root = state.base.replace(/\\/$/, "");
|
|
469
|
+
return root + path;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function setStatus(label, tone) {
|
|
473
|
+
status.textContent = label;
|
|
474
|
+
dot.className = "dot" + (tone === "live" ? " live" : tone === "err" ? " err" : "");
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function applyConnection() {
|
|
478
|
+
state.base = base.value.trim();
|
|
479
|
+
state.sessionId = session.value.trim();
|
|
480
|
+
state.token = token.value.trim();
|
|
481
|
+
state.running = Boolean(state.sessionId && state.token);
|
|
482
|
+
empty.style.display = state.running ? "none" : "block";
|
|
483
|
+
frame.style.display = state.running ? "block" : "none";
|
|
484
|
+
clearTimeout(state.timer);
|
|
485
|
+
if (state.running) {
|
|
486
|
+
setStatus("Streaming", "live");
|
|
487
|
+
loadFrame();
|
|
488
|
+
} else {
|
|
489
|
+
setStatus("Idle", "");
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function disconnect(label) {
|
|
494
|
+
state.running = false;
|
|
495
|
+
clearTimeout(state.timer);
|
|
496
|
+
if (state.frameObjectUrl) {
|
|
497
|
+
URL.revokeObjectURL(state.frameObjectUrl);
|
|
498
|
+
state.frameObjectUrl = "";
|
|
499
|
+
}
|
|
500
|
+
frame.removeAttribute("src");
|
|
501
|
+
frame.style.display = "none";
|
|
502
|
+
empty.style.display = "block";
|
|
503
|
+
setStatus(label, "");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function readErrorMessage(response) {
|
|
507
|
+
const body = await response.clone().json().catch(() => null);
|
|
508
|
+
if (body && typeof body.error === "string" && body.error.trim()) {
|
|
509
|
+
return body.error.trim();
|
|
510
|
+
}
|
|
511
|
+
const text = await response.text().catch(() => "");
|
|
512
|
+
return text.trim() || "Frame unavailable";
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function loadFrame() {
|
|
516
|
+
if (!state.running) return;
|
|
517
|
+
const src = endpoint("/api/apps/screenshare/session/" + encodeURIComponent(state.sessionId) + "/frame?token=" + encodeURIComponent(state.token) + "&t=" + Date.now());
|
|
518
|
+
try {
|
|
519
|
+
const response = await fetch(src, {
|
|
520
|
+
headers: { "X-Screenshare-Token": state.token }
|
|
521
|
+
});
|
|
522
|
+
if (!response.ok) {
|
|
523
|
+
throw new Error(await readErrorMessage(response));
|
|
524
|
+
}
|
|
525
|
+
const blob = await response.blob();
|
|
526
|
+
if (!state.running) return;
|
|
527
|
+
if (state.frameObjectUrl) {
|
|
528
|
+
URL.revokeObjectURL(state.frameObjectUrl);
|
|
529
|
+
}
|
|
530
|
+
state.frameObjectUrl = URL.createObjectURL(blob);
|
|
531
|
+
frame.src = state.frameObjectUrl;
|
|
532
|
+
setStatus("Streaming", "live");
|
|
533
|
+
state.timer = window.setTimeout(loadFrame, 500);
|
|
534
|
+
} catch (err) {
|
|
535
|
+
if (!state.running) return;
|
|
536
|
+
setStatus(err instanceof Error ? err.message : "Frame unavailable", "err");
|
|
537
|
+
state.timer = window.setTimeout(loadFrame, 1500);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function sendInput(payload) {
|
|
542
|
+
if (!state.sessionId || !state.token) return;
|
|
543
|
+
const response = await fetch(endpoint("/api/apps/screenshare/session/" + encodeURIComponent(state.sessionId) + "/input"), {
|
|
544
|
+
method: "POST",
|
|
545
|
+
headers: { "Content-Type": "application/json", "X-Screenshare-Token": state.token },
|
|
546
|
+
body: JSON.stringify({ ...payload, token: state.token })
|
|
547
|
+
});
|
|
548
|
+
if (!response.ok) {
|
|
549
|
+
const body = await response.json().catch(() => null);
|
|
550
|
+
throw new Error(body?.error || body?.message || "Input failed");
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function imagePoint(event) {
|
|
555
|
+
const rect = frame.getBoundingClientRect();
|
|
556
|
+
if (!frame.naturalWidth || !frame.naturalHeight || rect.width <= 0 || rect.height <= 0) return null;
|
|
557
|
+
return {
|
|
558
|
+
x: Math.round((event.clientX - rect.left) * frame.naturalWidth / rect.width),
|
|
559
|
+
y: Math.round((event.clientY - rect.top) * frame.naturalHeight / rect.height)
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
frame.addEventListener("click", (event) => {
|
|
564
|
+
const point = imagePoint(event);
|
|
565
|
+
if (!point) return;
|
|
566
|
+
stage.focus();
|
|
567
|
+
void sendInput({ type: "click", ...point, button: "left" }).catch((err) => setStatus(err.message, "err"));
|
|
568
|
+
});
|
|
569
|
+
frame.addEventListener("dblclick", (event) => {
|
|
570
|
+
const point = imagePoint(event);
|
|
571
|
+
if (!point) return;
|
|
572
|
+
stage.focus();
|
|
573
|
+
void sendInput({ type: "double-click", ...point, button: "left" }).catch((err) => setStatus(err.message, "err"));
|
|
574
|
+
});
|
|
575
|
+
frame.addEventListener("contextmenu", (event) => {
|
|
576
|
+
event.preventDefault();
|
|
577
|
+
const point = imagePoint(event);
|
|
578
|
+
if (!point) return;
|
|
579
|
+
stage.focus();
|
|
580
|
+
void sendInput({ type: "click", ...point, button: "right" }).catch((err) => setStatus(err.message, "err"));
|
|
581
|
+
});
|
|
582
|
+
frame.addEventListener("mousemove", (event) => {
|
|
583
|
+
if (event.buttons === 0) return;
|
|
584
|
+
const point = imagePoint(event);
|
|
585
|
+
if (!point) return;
|
|
586
|
+
void sendInput({ type: "move", ...point }).catch(() => {});
|
|
587
|
+
});
|
|
588
|
+
frame.addEventListener("wheel", (event) => {
|
|
589
|
+
event.preventDefault();
|
|
590
|
+
void sendInput({
|
|
591
|
+
type: "scroll",
|
|
592
|
+
deltaX: Math.max(-10, Math.min(10, Math.round(event.deltaX / 80))),
|
|
593
|
+
deltaY: Math.max(-10, Math.min(10, Math.round(event.deltaY / 80)))
|
|
594
|
+
}).catch((err) => setStatus(err.message, "err"));
|
|
595
|
+
}, { passive: false });
|
|
596
|
+
|
|
597
|
+
document.getElementById("connect").addEventListener("click", applyConnection);
|
|
598
|
+
document.getElementById("type").addEventListener("click", () => {
|
|
599
|
+
const value = text.value;
|
|
600
|
+
if (!value) return;
|
|
601
|
+
void sendInput({ type: "type", text: value }).then(() => { text.value = ""; }).catch((err) => setStatus(err.message, "err"));
|
|
602
|
+
});
|
|
603
|
+
for (const button of document.querySelectorAll("[data-key]")) {
|
|
604
|
+
button.addEventListener("click", () => {
|
|
605
|
+
void sendInput({ type: "keypress", keys: button.getAttribute("data-key") }).catch((err) => setStatus(err.message, "err"));
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
stage.addEventListener("keydown", (event) => {
|
|
609
|
+
const keyMap = { Enter: "Enter", Escape: "Escape", Tab: "Tab", ArrowUp: "Up", ArrowDown: "Down", ArrowLeft: "Left", ArrowRight: "Right", Backspace: "Backspace" };
|
|
610
|
+
const mapped = keyMap[event.key];
|
|
611
|
+
if (!mapped) return;
|
|
612
|
+
event.preventDefault();
|
|
613
|
+
void sendInput({ type: "keypress", keys: mapped }).catch((err) => setStatus(err.message, "err"));
|
|
614
|
+
});
|
|
615
|
+
document.getElementById("stop").addEventListener("click", async () => {
|
|
616
|
+
if (!state.sessionId || !state.token) return;
|
|
617
|
+
const response = await fetch(endpoint("/api/apps/screenshare/session/" + encodeURIComponent(state.sessionId) + "/stop"), {
|
|
618
|
+
method: "POST",
|
|
619
|
+
headers: { "Content-Type": "application/json", "X-Screenshare-Token": state.token },
|
|
620
|
+
body: JSON.stringify({ token: state.token })
|
|
621
|
+
});
|
|
622
|
+
if (response.ok) {
|
|
623
|
+
disconnect("Stopped");
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
applyConnection();
|
|
627
|
+
})();
|
|
628
|
+
</script>
|
|
629
|
+
</body>
|
|
630
|
+
</html>`;
|
|
631
|
+
}
|
|
632
|
+
export {
|
|
633
|
+
handleAppRoutes,
|
|
634
|
+
prepareLaunch,
|
|
635
|
+
refreshRunSession,
|
|
636
|
+
resolveLaunchSession,
|
|
637
|
+
stopRun
|
|
638
|
+
};
|
|
639
|
+
//# sourceMappingURL=routes.js.map
|