@melaya/runner 1.1.2 → 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 +81 -0
- 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
|
@@ -41,6 +41,32 @@ import { runSandboxedScript } from "./codeWorker.js";
|
|
|
41
41
|
// ---------------------------------------------------------------------
|
|
42
42
|
// Limits and constants
|
|
43
43
|
// ---------------------------------------------------------------------
|
|
44
|
+
// Injected into every interactive-session page to display a non-interactive
|
|
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.
|
|
50
|
+
const MELAYA_BRAND_SCRIPT = `(() => {
|
|
51
|
+
if (window.__melayaBrand) return; window.__melayaBrand = true;
|
|
52
|
+
const ID = "__melaya_brand";
|
|
53
|
+
const LOGO = "data:image/png;base64,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";
|
|
54
|
+
const inject = () => {
|
|
55
|
+
const root = document.documentElement; if (!root) return;
|
|
56
|
+
if (document.getElementById(ID + "_border")) return;
|
|
57
|
+
const style = document.createElement("style");
|
|
58
|
+
style.id = ID + "_style";
|
|
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))}";
|
|
60
|
+
(document.head || root).appendChild(style);
|
|
61
|
+
const border = document.createElement("div"); border.id = ID + "_border";
|
|
62
|
+
const badge = document.createElement("div"); badge.id = ID + "_badge";
|
|
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"));
|
|
65
|
+
root.appendChild(border); root.appendChild(badge);
|
|
66
|
+
};
|
|
67
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", inject); else inject();
|
|
68
|
+
try { new MutationObserver(() => { if (!document.getElementById(ID + "_border")) inject(); }).observe(document.documentElement, { childList: true }); } catch (e) {}
|
|
69
|
+
})();`;
|
|
44
70
|
const MAX_BODY_BYTES = 512 * 1024; // /browser/run scripts included
|
|
45
71
|
const NODE_CAP = 240; // snapshot node cap (Section 8)
|
|
46
72
|
const SCREENSHOT_MAX_B64 = 1_400_000; // ~1 MB binary; ladder recompresses
|
|
@@ -252,11 +278,15 @@ export async function startBrowserBridge(opts) {
|
|
|
252
278
|
headless: false,
|
|
253
279
|
viewport: { width: 1280, height: 800 },
|
|
254
280
|
acceptDownloads: false,
|
|
281
|
+
// Suppress the "browser is being controlled by automated test
|
|
282
|
+
// software" infobar that Chromium/Brave shows by default.
|
|
283
|
+
ignoreDefaultArgs: ["--enable-automation"],
|
|
255
284
|
args: [
|
|
256
285
|
"--no-first-run",
|
|
257
286
|
"--no-default-browser-check",
|
|
258
287
|
"--disable-background-networking",
|
|
259
288
|
"--disable-sync",
|
|
289
|
+
"--disable-blink-features=AutomationControlled",
|
|
260
290
|
],
|
|
261
291
|
});
|
|
262
292
|
}
|
|
@@ -272,6 +302,24 @@ export async function startBrowserBridge(opts) {
|
|
|
272
302
|
context,
|
|
273
303
|
ephemeralUserDataDir: ephemeral ? userDataDir : null,
|
|
274
304
|
});
|
|
305
|
+
// Install Melaya branding overlay. addInitScript persists it across
|
|
306
|
+
// all navigations and new pages. The one-shot evaluate covers the
|
|
307
|
+
// already-open page (typically about:blank) immediately.
|
|
308
|
+
// Both are wrapped so a branding failure cannot fail the launch.
|
|
309
|
+
try {
|
|
310
|
+
await context.addInitScript(MELAYA_BRAND_SCRIPT);
|
|
311
|
+
}
|
|
312
|
+
catch (brandErr) {
|
|
313
|
+
log(`[browser-launch] branding init-script install failed (non-fatal): ${brandErr?.message || brandErr}`);
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
const firstPage = context.pages()[0];
|
|
317
|
+
if (firstPage)
|
|
318
|
+
await firstPage.evaluate(MELAYA_BRAND_SCRIPT);
|
|
319
|
+
}
|
|
320
|
+
catch (brandErr) {
|
|
321
|
+
log(`[browser-launch] branding evaluate on first page failed (non-fatal): ${brandErr?.message || brandErr}`);
|
|
322
|
+
}
|
|
275
323
|
log(`[browser-launch] interactive session active sessionId=${sessionId.slice(0, 16)} engine=${engineId}`);
|
|
276
324
|
onState(sessionId, "active", engine);
|
|
277
325
|
// Detect browser close: context 'close' fires when the user shuts
|
|
@@ -1161,6 +1209,38 @@ export async function startBrowserBridge(opts) {
|
|
|
1161
1209
|
}),
|
|
1162
1210
|
]);
|
|
1163
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
|
+
};
|
|
1164
1244
|
return {
|
|
1165
1245
|
url: `http://127.0.0.1:${port}`,
|
|
1166
1246
|
port,
|
|
@@ -1173,6 +1253,7 @@ export async function startBrowserBridge(opts) {
|
|
|
1173
1253
|
launchInteractive,
|
|
1174
1254
|
getInteractiveSession,
|
|
1175
1255
|
setWatchLease,
|
|
1256
|
+
killInteractive,
|
|
1176
1257
|
async shutdown() {
|
|
1177
1258
|
// Stop all watch-lease producers before closing sessions.
|
|
1178
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)
|