@melaya/runner 1.1.3 → 1.1.4
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/browserBridge.d.ts +12 -0
- package/dist/browserBridge.js +42 -6
- package/dist/connection.js +33 -0
- package/package.json +1 -1
package/dist/browserBridge.d.ts
CHANGED
|
@@ -78,6 +78,18 @@ export interface BrowserBridge {
|
|
|
78
78
|
* (looked up by sessionId directly).
|
|
79
79
|
*/
|
|
80
80
|
setWatchLease(sessionId: string, active: boolean, framePostUrl: string, runAuthHeader: string): void;
|
|
81
|
+
/**
|
|
82
|
+
* Close one or all interactive sessions (emergency stop / per-session close).
|
|
83
|
+
*
|
|
84
|
+
* When sessionId is provided, only that interactive session is closed.
|
|
85
|
+
* When sessionId is omitted (or undefined), ALL active interactive sessions
|
|
86
|
+
* on this runner are closed.
|
|
87
|
+
*
|
|
88
|
+
* Returns the array of sessionIds that were actually closed. Idempotent;
|
|
89
|
+
* per-session errors are swallowed (best-effort). Does NOT touch
|
|
90
|
+
* grant-scoped run sessions (byRunId).
|
|
91
|
+
*/
|
|
92
|
+
killInteractive(sessionId?: string): Promise<string[]>;
|
|
81
93
|
shutdown(): Promise<void>;
|
|
82
94
|
}
|
|
83
95
|
export declare function startBrowserBridge(opts: {
|
package/dist/browserBridge.js
CHANGED
|
@@ -42,23 +42,26 @@ import { runSandboxedScript } from "./codeWorker.js";
|
|
|
42
42
|
// Limits and constants
|
|
43
43
|
// ---------------------------------------------------------------------
|
|
44
44
|
// Injected into every interactive-session page to display a non-interactive
|
|
45
|
-
// Melaya branding overlay (pill badge + pulsing
|
|
46
|
-
// here so it is shared between context.addInitScript (persistent
|
|
47
|
-
// navigations) and the one-shot page.evaluate on the already-open page.
|
|
45
|
+
// Melaya branding overlay (pill badge with logo + pulsing cyan border).
|
|
46
|
+
// Defined once here so it is shared between context.addInitScript (persistent
|
|
47
|
+
// across navigations) and the one-shot page.evaluate on the already-open page.
|
|
48
|
+
// Palette: cyan #22D3EE primary accent, indigo #7C6FF0 subtle secondary,
|
|
49
|
+
// slate/navy #0F172A base. Less purple than the prior version.
|
|
48
50
|
const MELAYA_BRAND_SCRIPT = `(() => {
|
|
49
51
|
if (window.__melayaBrand) return; window.__melayaBrand = true;
|
|
50
52
|
const ID = "__melaya_brand";
|
|
53
|
+
const LOGO = "data:image/png;base64,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";
|
|
51
54
|
const inject = () => {
|
|
52
55
|
const root = document.documentElement; if (!root) return;
|
|
53
56
|
if (document.getElementById(ID + "_border")) return;
|
|
54
57
|
const style = document.createElement("style");
|
|
55
58
|
style.id = ID + "_style";
|
|
56
|
-
style.textContent = "@keyframes melBrandPulse{0%,100%{box-shadow:inset 0 0
|
|
59
|
+
style.textContent = "@keyframes melBrandPulse{0%,100%{box-shadow:inset 0 0 9px 2px rgba(34,211,238,.5),inset 0 0 30px 7px rgba(124,111,240,.18)}50%{box-shadow:inset 0 0 16px 4px rgba(34,211,238,.8),inset 0 0 54px 13px rgba(124,111,240,.32)}}@keyframes melBrandGlow{0%,100%{opacity:.78}50%{opacity:1}}#" + ID + "_border{position:fixed;inset:0;pointer-events:none;z-index:2147483647;border-radius:7px;animation:melBrandPulse 2.6s ease-in-out infinite}#" + ID + "_badge{position:fixed;top:10px;left:50%;transform:translateX(-50%);pointer-events:none;z-index:2147483647;font:600 12px/1 -apple-system,Segoe UI,Roboto,sans-serif;color:#E2E8F0;background:linear-gradient(180deg,rgba(15,23,42,.94),rgba(15,23,42,.82));padding:5px 13px 5px 7px;border-radius:999px;letter-spacing:.2px;border:1px solid rgba(34,211,238,.45);box-shadow:0 0 16px 1px rgba(34,211,238,.4),0 4px 14px rgba(0,0,0,.45);display:flex;align-items:center;gap:8px;backdrop-filter:blur(6px);animation:melBrandGlow 2.6s ease-in-out infinite}#" + ID + "_logo{height:16px;width:auto;display:block;filter:drop-shadow(0 0 4px rgba(34,211,238,.55))}";
|
|
57
60
|
(document.head || root).appendChild(style);
|
|
58
61
|
const border = document.createElement("div"); border.id = ID + "_border";
|
|
59
62
|
const badge = document.createElement("div"); badge.id = ID + "_badge";
|
|
60
|
-
const
|
|
61
|
-
badge.appendChild(
|
|
63
|
+
const logo = document.createElement("img"); logo.id = ID + "_logo"; logo.src = LOGO; logo.alt = "Melaya";
|
|
64
|
+
badge.appendChild(logo); badge.appendChild(document.createTextNode("Controlled by Melaya"));
|
|
62
65
|
root.appendChild(border); root.appendChild(badge);
|
|
63
66
|
};
|
|
64
67
|
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", inject); else inject();
|
|
@@ -1206,6 +1209,38 @@ export async function startBrowserBridge(opts) {
|
|
|
1206
1209
|
}),
|
|
1207
1210
|
]);
|
|
1208
1211
|
};
|
|
1212
|
+
// killInteractive: close one or all interactive sessions (emergency stop).
|
|
1213
|
+
// Does NOT touch grant-scoped run sessions in byRunId.
|
|
1214
|
+
const killInteractive = async (sessionId) => {
|
|
1215
|
+
// Build the list of sessionIds to close.
|
|
1216
|
+
const targets = sessionId !== undefined
|
|
1217
|
+
? (interactiveSessions.has(sessionId) ? [sessionId] : [])
|
|
1218
|
+
: [...interactiveSessions.keys()];
|
|
1219
|
+
const closed = [];
|
|
1220
|
+
await Promise.all(targets.map(async (id) => {
|
|
1221
|
+
try {
|
|
1222
|
+
// Stop any active watch-lease frame producer for this session.
|
|
1223
|
+
for (const [sid, wl] of watchLeases) {
|
|
1224
|
+
if (wl.runId === id)
|
|
1225
|
+
stopWatchLease(sid);
|
|
1226
|
+
}
|
|
1227
|
+
// Remove from the interactiveSessions map first so the context
|
|
1228
|
+
// "close" handler does not re-emit a redundant "closed" state.
|
|
1229
|
+
interactiveSessions.delete(id);
|
|
1230
|
+
// Tear down through sessionManager (closes the Playwright context,
|
|
1231
|
+
// cleans up leases, removes the session record).
|
|
1232
|
+
await sessions.teardownRun(id, "kill_interactive").catch(() => { });
|
|
1233
|
+
log(`[browser-kill] interactive session killed: ${id.slice(0, 16)}`);
|
|
1234
|
+
closed.push(id);
|
|
1235
|
+
}
|
|
1236
|
+
catch {
|
|
1237
|
+
// Best-effort per-session: a session that already closed or crashed
|
|
1238
|
+
// should not prevent other sessions from being killed.
|
|
1239
|
+
closed.push(id);
|
|
1240
|
+
}
|
|
1241
|
+
}));
|
|
1242
|
+
return closed;
|
|
1243
|
+
};
|
|
1209
1244
|
return {
|
|
1210
1245
|
url: `http://127.0.0.1:${port}`,
|
|
1211
1246
|
port,
|
|
@@ -1218,6 +1253,7 @@ export async function startBrowserBridge(opts) {
|
|
|
1218
1253
|
launchInteractive,
|
|
1219
1254
|
getInteractiveSession,
|
|
1220
1255
|
setWatchLease,
|
|
1256
|
+
killInteractive,
|
|
1221
1257
|
async shutdown() {
|
|
1222
1258
|
// Stop all watch-lease producers before closing sessions.
|
|
1223
1259
|
for (const sid of [...watchLeases.keys()])
|
package/dist/connection.js
CHANGED
|
@@ -327,6 +327,39 @@ export async function connect(opts) {
|
|
|
327
327
|
}
|
|
328
328
|
});
|
|
329
329
|
});
|
|
330
|
+
// ── BrowserControlPage emergency stop / per-session close (browser:kill) ──
|
|
331
|
+
// The server emits browser:kill { sessionId? } when the user presses the
|
|
332
|
+
// emergency-stop button (no sessionId = kill all) or closes a specific
|
|
333
|
+
// session card (sessionId present). The bridge closes the matching
|
|
334
|
+
// Playwright context(s) and returns the sessionIds it actually closed.
|
|
335
|
+
// For each closed session a browser:session { state: "closed" } is emitted
|
|
336
|
+
// so the server marks the DB row closed.
|
|
337
|
+
socket.on("browser:kill", async (payload) => {
|
|
338
|
+
const targetId = payload?.sessionId ? String(payload.sessionId) : undefined;
|
|
339
|
+
if (opts.verbose) {
|
|
340
|
+
console.log(chalk.gray(` [browser-kill] received sessionId=${targetId ? targetId.slice(0, 16) : "(all)"}`));
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const bridge = await _ensureBrowserBridge();
|
|
344
|
+
if (!bridge) {
|
|
345
|
+
console.log(chalk.yellow(" ! browser:kill: bridge unavailable"));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const closedIds = await bridge.killInteractive(targetId);
|
|
349
|
+
for (const sid of closedIds) {
|
|
350
|
+
socket.emit("browser:session", { sessionId: sid, state: "closed", engine: "" });
|
|
351
|
+
if (opts.verbose) {
|
|
352
|
+
console.log(chalk.gray(` [browser-kill] browser:session sessionId=${sid.slice(0, 16)} state=closed`));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (closedIds.length > 0) {
|
|
356
|
+
console.log(chalk.gray(` [browser-kill] closed ${closedIds.length} interactive session(s)`));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
catch (e) {
|
|
360
|
+
console.log(chalk.yellow(` ! browser:kill error: ${e?.message || e}`));
|
|
361
|
+
}
|
|
362
|
+
});
|
|
330
363
|
// ── Heartbeat ──────────────────────────────────────────────────────
|
|
331
364
|
setInterval(() => {
|
|
332
365
|
if (socket.connected)
|