@mindstudio-ai/local-model-tunnel 0.5.54 → 0.5.55
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/{chunk-B7BNG7SI.js → chunk-5A5ASXUO.js} +628 -102
- package/dist/chunk-5A5ASXUO.js.map +1 -0
- package/dist/{chunk-7XPEELY3.js → chunk-ERAJTIOM.js} +37 -17
- package/dist/chunk-ERAJTIOM.js.map +1 -0
- package/dist/{chunk-WXGONHM6.js → chunk-IB7HD3VY.js} +2 -2
- package/dist/cli.js +3 -2
- package/dist/cli.js.map +1 -1
- package/dist/headless.d.ts +2 -0
- package/dist/headless.js +2 -2
- package/dist/index.js +3 -3
- package/dist/{tui-3ZNLKZ3V.js → tui-T24TDGEV.js} +6 -6
- package/package.json +2 -1
- package/dist/chunk-7XPEELY3.js.map +0 -1
- package/dist/chunk-B7BNG7SI.js.map +0 -1
- /package/dist/{chunk-WXGONHM6.js.map → chunk-IB7HD3VY.js.map} +0 -0
- /package/dist/{tui-3ZNLKZ3V.js.map → tui-T24TDGEV.js.map} +0 -0
|
@@ -25,7 +25,118 @@ import {
|
|
|
25
25
|
syncSchema,
|
|
26
26
|
watchConfigFile,
|
|
27
27
|
watchTableFiles
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-ERAJTIOM.js";
|
|
29
|
+
|
|
30
|
+
// src/dev/browser/launcher.ts
|
|
31
|
+
import puppeteer from "puppeteer-core";
|
|
32
|
+
|
|
33
|
+
// src/dev/browser/chrome-path.ts
|
|
34
|
+
import { existsSync } from "fs";
|
|
35
|
+
import { execSync } from "child_process";
|
|
36
|
+
var CANDIDATES = [
|
|
37
|
+
"/usr/bin/google-chrome-stable",
|
|
38
|
+
"/usr/bin/google-chrome",
|
|
39
|
+
"/usr/bin/chromium",
|
|
40
|
+
"/usr/bin/chromium-browser",
|
|
41
|
+
"/opt/google/chrome/google-chrome",
|
|
42
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
43
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
44
|
+
];
|
|
45
|
+
var PATH_COMMANDS = ["google-chrome-stable", "google-chrome", "chromium"];
|
|
46
|
+
function resolveChromePath() {
|
|
47
|
+
for (const candidate of CANDIDATES) {
|
|
48
|
+
if (existsSync(candidate)) return candidate;
|
|
49
|
+
}
|
|
50
|
+
for (const cmd of PATH_COMMANDS) {
|
|
51
|
+
try {
|
|
52
|
+
const resolved = execSync(`command -v ${cmd}`, {
|
|
53
|
+
encoding: "utf-8",
|
|
54
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
55
|
+
}).trim();
|
|
56
|
+
if (resolved && existsSync(resolved)) return resolved;
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/dev/browser/launcher.ts
|
|
64
|
+
var LAUNCH_ARGS = [
|
|
65
|
+
"--no-sandbox",
|
|
66
|
+
"--disable-dev-shm-usage",
|
|
67
|
+
"--disable-gpu",
|
|
68
|
+
"--hide-scrollbars",
|
|
69
|
+
"--force-color-profile=srgb",
|
|
70
|
+
"--font-render-hinting=none",
|
|
71
|
+
"--disable-blink-features=AutomationControlled",
|
|
72
|
+
"--lang=en-US"
|
|
73
|
+
];
|
|
74
|
+
var DESKTOP_VIEWPORT = {
|
|
75
|
+
width: 1440,
|
|
76
|
+
height: 900,
|
|
77
|
+
deviceScaleFactor: 1,
|
|
78
|
+
isMobile: false,
|
|
79
|
+
hasTouch: false
|
|
80
|
+
};
|
|
81
|
+
var MOBILE_VIEWPORT = {
|
|
82
|
+
width: 390,
|
|
83
|
+
height: 844,
|
|
84
|
+
deviceScaleFactor: 2,
|
|
85
|
+
isMobile: true,
|
|
86
|
+
hasTouch: true
|
|
87
|
+
};
|
|
88
|
+
function viewportFor(mode) {
|
|
89
|
+
return mode === "mobile" ? MOBILE_VIEWPORT : DESKTOP_VIEWPORT;
|
|
90
|
+
}
|
|
91
|
+
async function launchSandboxBrowser(opts) {
|
|
92
|
+
const executablePath = resolveChromePath();
|
|
93
|
+
if (!executablePath) {
|
|
94
|
+
log.warn(
|
|
95
|
+
"browser",
|
|
96
|
+
"No Chrome executable found \u2014 sandbox-browser mode disabled for this session"
|
|
97
|
+
);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const previewMode = opts.previewMode === "mobile" ? "mobile" : "desktop";
|
|
101
|
+
const viewport = viewportFor(previewMode);
|
|
102
|
+
const browser = await puppeteer.launch({
|
|
103
|
+
executablePath,
|
|
104
|
+
headless: true,
|
|
105
|
+
args: LAUNCH_ARGS,
|
|
106
|
+
defaultViewport: viewport
|
|
107
|
+
});
|
|
108
|
+
const proc = browser.process();
|
|
109
|
+
proc?.stderr?.on("data", (buf) => {
|
|
110
|
+
const line = buf.toString().trim();
|
|
111
|
+
if (line) log.debug("browser-chrome", line);
|
|
112
|
+
});
|
|
113
|
+
const pages = await browser.pages();
|
|
114
|
+
const page = pages[0] ?? await browser.newPage();
|
|
115
|
+
const target = `http://127.0.0.1:${opts.proxyPort}/?ms_sandbox=1`;
|
|
116
|
+
try {
|
|
117
|
+
await page.goto(target, { waitUntil: "networkidle0", timeout: 15e3 });
|
|
118
|
+
} catch (err) {
|
|
119
|
+
await browser.close().catch(() => {
|
|
120
|
+
});
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
const viewportStr = `${viewport.width}x${viewport.height}@${viewport.deviceScaleFactor}x`;
|
|
124
|
+
log.info("browser", "Sandbox browser launched", {
|
|
125
|
+
executablePath,
|
|
126
|
+
target,
|
|
127
|
+
previewMode,
|
|
128
|
+
viewport: viewportStr,
|
|
129
|
+
pid: proc?.pid ?? null
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
browser,
|
|
133
|
+
page,
|
|
134
|
+
executablePath,
|
|
135
|
+
pid: proc?.pid ?? null,
|
|
136
|
+
previewMode,
|
|
137
|
+
viewport: viewportStr
|
|
138
|
+
};
|
|
139
|
+
}
|
|
29
140
|
|
|
30
141
|
// src/dev/ipc/ipc.ts
|
|
31
142
|
function emitEvent(event, data) {
|
|
@@ -37,6 +148,349 @@ function emitResponse(action, requestId, status, data) {
|
|
|
37
148
|
);
|
|
38
149
|
}
|
|
39
150
|
|
|
151
|
+
// src/dev/browser/supervisor.ts
|
|
152
|
+
var BACKOFF_MS = [1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
|
|
153
|
+
var MAX_FAILURES = 5;
|
|
154
|
+
var CLOSE_TIMEOUT_MS = 5e3;
|
|
155
|
+
var BrowserSupervisor = class {
|
|
156
|
+
constructor(proxyPort, previewMode = "desktop") {
|
|
157
|
+
this.proxyPort = proxyPort;
|
|
158
|
+
this.previewMode = previewMode;
|
|
159
|
+
}
|
|
160
|
+
proxyPort;
|
|
161
|
+
previewMode;
|
|
162
|
+
browser = null;
|
|
163
|
+
page = null;
|
|
164
|
+
stopping = false;
|
|
165
|
+
degraded = false;
|
|
166
|
+
consecutiveFailures = 0;
|
|
167
|
+
restartTimer = null;
|
|
168
|
+
runningSince = null;
|
|
169
|
+
lastExitInfo = null;
|
|
170
|
+
async start() {
|
|
171
|
+
if (this.browser) return;
|
|
172
|
+
await this.launchOnce();
|
|
173
|
+
}
|
|
174
|
+
async stop() {
|
|
175
|
+
if (this.stopping) return;
|
|
176
|
+
this.stopping = true;
|
|
177
|
+
if (this.restartTimer) {
|
|
178
|
+
clearTimeout(this.restartTimer);
|
|
179
|
+
this.restartTimer = null;
|
|
180
|
+
}
|
|
181
|
+
const browser = this.browser;
|
|
182
|
+
this.browser = null;
|
|
183
|
+
this.page = null;
|
|
184
|
+
if (browser) {
|
|
185
|
+
await this.closeBrowser(browser);
|
|
186
|
+
}
|
|
187
|
+
this.runningSince = null;
|
|
188
|
+
this.lastExitInfo = null;
|
|
189
|
+
emitEvent("sandbox-browser-state", { state: "stopped" });
|
|
190
|
+
}
|
|
191
|
+
isRunning() {
|
|
192
|
+
return !!this.browser && !this.degraded;
|
|
193
|
+
}
|
|
194
|
+
isDegraded() {
|
|
195
|
+
return this.degraded;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Returns the active puppeteer Page when the sandbox browser is running
|
|
199
|
+
* and not degraded; null otherwise. Callers use this to decide whether
|
|
200
|
+
* a CDP-side fast path is available for a given command.
|
|
201
|
+
*/
|
|
202
|
+
getActivePage() {
|
|
203
|
+
if (this.stopping || this.degraded) return null;
|
|
204
|
+
if (!this.browser || !this.page) return null;
|
|
205
|
+
return this.page;
|
|
206
|
+
}
|
|
207
|
+
async launchOnce() {
|
|
208
|
+
if (this.stopping) return;
|
|
209
|
+
const attempt = this.consecutiveFailures + 1;
|
|
210
|
+
log.info("browser", "Sandbox browser launch starting", {
|
|
211
|
+
proxyPort: this.proxyPort,
|
|
212
|
+
attempt
|
|
213
|
+
});
|
|
214
|
+
emitEvent("sandbox-browser-state", {
|
|
215
|
+
state: "starting",
|
|
216
|
+
attempt,
|
|
217
|
+
previewMode: this.previewMode
|
|
218
|
+
});
|
|
219
|
+
try {
|
|
220
|
+
const launched = await launchSandboxBrowser({
|
|
221
|
+
proxyPort: this.proxyPort,
|
|
222
|
+
previewMode: this.previewMode
|
|
223
|
+
});
|
|
224
|
+
if (!launched) {
|
|
225
|
+
this.degraded = true;
|
|
226
|
+
emitEvent("sandbox-browser-state", {
|
|
227
|
+
state: "degraded",
|
|
228
|
+
reason: "no-executable"
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (this.stopping) {
|
|
233
|
+
await this.closeBrowser(launched.browser).catch(() => {
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
this.browser = launched.browser;
|
|
238
|
+
this.page = launched.page;
|
|
239
|
+
this.consecutiveFailures = 0;
|
|
240
|
+
this.degraded = false;
|
|
241
|
+
this.runningSince = Date.now();
|
|
242
|
+
this.lastExitInfo = null;
|
|
243
|
+
const proc = launched.browser.process();
|
|
244
|
+
proc?.once("exit", (code, signal) => {
|
|
245
|
+
this.lastExitInfo = { exitCode: code, signal: signal ?? null };
|
|
246
|
+
});
|
|
247
|
+
launched.browser.on("disconnected", () => this.onDisconnect());
|
|
248
|
+
emitEvent("sandbox-browser-state", {
|
|
249
|
+
state: "running",
|
|
250
|
+
pid: launched.pid,
|
|
251
|
+
previewMode: launched.previewMode,
|
|
252
|
+
viewport: launched.viewport,
|
|
253
|
+
executablePath: launched.executablePath
|
|
254
|
+
});
|
|
255
|
+
} catch (err) {
|
|
256
|
+
if (this.stopping) return;
|
|
257
|
+
this.consecutiveFailures++;
|
|
258
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
259
|
+
log.warn("browser", "Failed to launch sandbox browser", {
|
|
260
|
+
attempt: this.consecutiveFailures,
|
|
261
|
+
error: message
|
|
262
|
+
});
|
|
263
|
+
emitEvent("sandbox-browser-state", {
|
|
264
|
+
state: "crashed",
|
|
265
|
+
exitCode: null,
|
|
266
|
+
signal: null,
|
|
267
|
+
durationMs: 0,
|
|
268
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
269
|
+
error: message
|
|
270
|
+
});
|
|
271
|
+
this.scheduleRestart();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async onDisconnect() {
|
|
275
|
+
if (this.stopping) return;
|
|
276
|
+
const hadBrowser = !!this.browser;
|
|
277
|
+
this.browser = null;
|
|
278
|
+
this.page = null;
|
|
279
|
+
if (!hadBrowser) return;
|
|
280
|
+
this.consecutiveFailures++;
|
|
281
|
+
const durationMs = this.runningSince ? Date.now() - this.runningSince : 0;
|
|
282
|
+
this.runningSince = null;
|
|
283
|
+
log.warn("browser", "Sandbox browser disconnected", {
|
|
284
|
+
attempt: this.consecutiveFailures
|
|
285
|
+
});
|
|
286
|
+
await this.waitForExitInfo();
|
|
287
|
+
emitEvent("sandbox-browser-state", {
|
|
288
|
+
state: "crashed",
|
|
289
|
+
exitCode: this.lastExitInfo?.exitCode ?? null,
|
|
290
|
+
signal: this.lastExitInfo?.signal ?? null,
|
|
291
|
+
durationMs,
|
|
292
|
+
consecutiveFailures: this.consecutiveFailures
|
|
293
|
+
});
|
|
294
|
+
this.lastExitInfo = null;
|
|
295
|
+
this.scheduleRestart();
|
|
296
|
+
}
|
|
297
|
+
async waitForExitInfo(timeoutMs = 200) {
|
|
298
|
+
if (this.lastExitInfo) return;
|
|
299
|
+
const deadline = Date.now() + timeoutMs;
|
|
300
|
+
while (!this.lastExitInfo && Date.now() < deadline) {
|
|
301
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
scheduleRestart() {
|
|
305
|
+
if (this.stopping) return;
|
|
306
|
+
if (this.consecutiveFailures >= MAX_FAILURES) {
|
|
307
|
+
this.degraded = true;
|
|
308
|
+
log.warn(
|
|
309
|
+
"browser",
|
|
310
|
+
"Sandbox browser entering degraded mode after repeated failures \u2014 automation will fall back to user browsers",
|
|
311
|
+
{ failures: this.consecutiveFailures }
|
|
312
|
+
);
|
|
313
|
+
emitEvent("sandbox-browser-state", {
|
|
314
|
+
state: "degraded",
|
|
315
|
+
reason: "repeated-crashes",
|
|
316
|
+
consecutiveFailures: this.consecutiveFailures
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const delay = BACKOFF_MS[Math.min(this.consecutiveFailures, BACKOFF_MS.length - 1)];
|
|
321
|
+
log.info("browser", "Scheduling sandbox browser restart", {
|
|
322
|
+
delayMs: delay,
|
|
323
|
+
attempt: this.consecutiveFailures
|
|
324
|
+
});
|
|
325
|
+
emitEvent("sandbox-browser-state", {
|
|
326
|
+
state: "restarting",
|
|
327
|
+
delayMs: delay,
|
|
328
|
+
nextAttempt: this.consecutiveFailures + 1
|
|
329
|
+
});
|
|
330
|
+
this.restartTimer = setTimeout(() => {
|
|
331
|
+
this.restartTimer = null;
|
|
332
|
+
void this.launchOnce();
|
|
333
|
+
}, delay);
|
|
334
|
+
}
|
|
335
|
+
async closeBrowser(browser) {
|
|
336
|
+
let resolved = false;
|
|
337
|
+
await new Promise((resolve) => {
|
|
338
|
+
const done = () => {
|
|
339
|
+
if (resolved) return;
|
|
340
|
+
resolved = true;
|
|
341
|
+
resolve();
|
|
342
|
+
};
|
|
343
|
+
const timeout = setTimeout(() => {
|
|
344
|
+
try {
|
|
345
|
+
browser.process()?.kill("SIGKILL");
|
|
346
|
+
} catch {
|
|
347
|
+
}
|
|
348
|
+
done();
|
|
349
|
+
}, CLOSE_TIMEOUT_MS);
|
|
350
|
+
browser.close().then(() => {
|
|
351
|
+
clearTimeout(timeout);
|
|
352
|
+
done();
|
|
353
|
+
}).catch(() => {
|
|
354
|
+
clearTimeout(timeout);
|
|
355
|
+
try {
|
|
356
|
+
browser.process()?.kill("SIGKILL");
|
|
357
|
+
} catch {
|
|
358
|
+
}
|
|
359
|
+
done();
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// src/dev/browser/screenshot.ts
|
|
366
|
+
var GOTO_TIMEOUT_MS = 15e3;
|
|
367
|
+
var SETTLE_TIMEOUT_MS = 3e3;
|
|
368
|
+
var SETTLE_IDLE_MS = 200;
|
|
369
|
+
var JPEG_QUALITY = 85;
|
|
370
|
+
var PREROLL_BOTTOM_DWELL_MS = 300;
|
|
371
|
+
var PREROLL_NETWORK_IDLE_MS = 1500;
|
|
372
|
+
var PREROLL_TOP_DWELL_MS = 100;
|
|
373
|
+
async function captureViaCdp(page, opts) {
|
|
374
|
+
if (opts.path) {
|
|
375
|
+
await page.goto(opts.path, {
|
|
376
|
+
waitUntil: "networkidle0",
|
|
377
|
+
timeout: GOTO_TIMEOUT_MS
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
await page.waitForNetworkIdle({ timeout: SETTLE_TIMEOUT_MS, idleTime: SETTLE_IDLE_MS }).catch(() => {
|
|
381
|
+
});
|
|
382
|
+
if (opts.fullPage) {
|
|
383
|
+
await preRollScroll(page);
|
|
384
|
+
}
|
|
385
|
+
let width;
|
|
386
|
+
let height;
|
|
387
|
+
if (opts.fullPage) {
|
|
388
|
+
const dims = await page.evaluate(() => ({
|
|
389
|
+
width: document.documentElement.scrollWidth,
|
|
390
|
+
height: document.documentElement.scrollHeight
|
|
391
|
+
}));
|
|
392
|
+
width = dims.width;
|
|
393
|
+
height = dims.height;
|
|
394
|
+
} else {
|
|
395
|
+
const vp = page.viewport();
|
|
396
|
+
width = vp?.width ?? 0;
|
|
397
|
+
height = vp?.height ?? 0;
|
|
398
|
+
}
|
|
399
|
+
let styleMap;
|
|
400
|
+
try {
|
|
401
|
+
const result = await page.evaluate(() => {
|
|
402
|
+
const api = window.__MINDSTUDIO_BROWSER_AGENT__;
|
|
403
|
+
return api?.computeStyleMap?.() ?? null;
|
|
404
|
+
});
|
|
405
|
+
if (typeof result === "string" && result.length > 0) styleMap = result;
|
|
406
|
+
} catch {
|
|
407
|
+
}
|
|
408
|
+
const buf = await page.screenshot({
|
|
409
|
+
type: "jpeg",
|
|
410
|
+
quality: JPEG_QUALITY,
|
|
411
|
+
fullPage: opts.fullPage
|
|
412
|
+
});
|
|
413
|
+
await uploadToPresigned(opts.uploadUrl, opts.uploadFields, buf);
|
|
414
|
+
return {
|
|
415
|
+
uploaded: true,
|
|
416
|
+
width,
|
|
417
|
+
height,
|
|
418
|
+
...styleMap ? { styleMap } : {}
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
async function preRollScroll(page) {
|
|
422
|
+
try {
|
|
423
|
+
const scrolled = await page.evaluate(() => {
|
|
424
|
+
const el = document.scrollingElement || document.documentElement;
|
|
425
|
+
const max = Math.max(
|
|
426
|
+
document.documentElement.scrollHeight,
|
|
427
|
+
document.body.scrollHeight
|
|
428
|
+
);
|
|
429
|
+
if (max <= window.innerHeight + 10) return false;
|
|
430
|
+
el.scrollTo({ top: max, left: 0, behavior: "instant" });
|
|
431
|
+
return true;
|
|
432
|
+
});
|
|
433
|
+
if (!scrolled) return;
|
|
434
|
+
await new Promise((r) => setTimeout(r, PREROLL_BOTTOM_DWELL_MS));
|
|
435
|
+
await page.waitForNetworkIdle({
|
|
436
|
+
timeout: PREROLL_NETWORK_IDLE_MS,
|
|
437
|
+
idleTime: SETTLE_IDLE_MS
|
|
438
|
+
}).catch(() => {
|
|
439
|
+
});
|
|
440
|
+
await page.evaluate(() => {
|
|
441
|
+
const el = document.scrollingElement || document.documentElement;
|
|
442
|
+
el.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
443
|
+
});
|
|
444
|
+
await new Promise((r) => setTimeout(r, PREROLL_TOP_DWELL_MS));
|
|
445
|
+
} catch {
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async function uploadToPresigned(uploadUrl, uploadFields, buf) {
|
|
449
|
+
const form = new FormData();
|
|
450
|
+
for (const [k, v] of Object.entries(uploadFields)) form.append(k, v);
|
|
451
|
+
form.append(
|
|
452
|
+
"file",
|
|
453
|
+
new Blob([buf], { type: "image/jpeg" }),
|
|
454
|
+
"screenshot.jpg"
|
|
455
|
+
);
|
|
456
|
+
const res = await fetch(uploadUrl, { method: "POST", body: form });
|
|
457
|
+
if (!res.ok) {
|
|
458
|
+
throw new Error(`Screenshot upload failed: ${res.status}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// src/dev/browser/cookies.ts
|
|
463
|
+
var AUTH_COOKIE_NAME = "__ms_auth";
|
|
464
|
+
function cookieHost(page) {
|
|
465
|
+
try {
|
|
466
|
+
return new URL(page.url()).hostname || "127.0.0.1";
|
|
467
|
+
} catch {
|
|
468
|
+
return "127.0.0.1";
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async function clearAuthCookies(page) {
|
|
472
|
+
const domain = cookieHost(page);
|
|
473
|
+
try {
|
|
474
|
+
await page.deleteCookie({ name: AUTH_COOKIE_NAME, domain });
|
|
475
|
+
} catch {
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
await page.deleteCookie({ name: AUTH_COOKIE_NAME });
|
|
479
|
+
} catch {
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async function setAuthCookie(page, value) {
|
|
483
|
+
const domain = cookieHost(page);
|
|
484
|
+
await page.setCookie({
|
|
485
|
+
name: AUTH_COOKIE_NAME,
|
|
486
|
+
value,
|
|
487
|
+
domain,
|
|
488
|
+
path: "/",
|
|
489
|
+
sameSite: "None",
|
|
490
|
+
secure: true
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
40
494
|
// src/dev/ipc/session-events.ts
|
|
41
495
|
function subscribeDevEvents(shutdown) {
|
|
42
496
|
const unsubs = [];
|
|
@@ -154,68 +608,159 @@ async function handleClearImpersonation(ctx) {
|
|
|
154
608
|
}
|
|
155
609
|
|
|
156
610
|
// src/dev/stdin-commands/browser.ts
|
|
611
|
+
var MIN_RECORDING_BYTES = 5e3;
|
|
157
612
|
async function handleBrowser(ctx, cmd) {
|
|
158
613
|
if (!ctx.state.proxy) throw new CommandError("No active proxy", "NO_BROWSER");
|
|
159
614
|
const steps = cmd.steps;
|
|
160
615
|
if (!Array.isArray(steps) || steps.length === 0) {
|
|
161
616
|
throw new CommandError('browser action requires a non-empty "steps" array', "INVALID_INPUT");
|
|
162
617
|
}
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
618
|
+
const page = ctx.state.browser?.getActivePage();
|
|
619
|
+
if (!page) {
|
|
620
|
+
throw new CommandError(
|
|
621
|
+
"Sandbox browser unavailable \u2014 headless Chrome is required for automation",
|
|
622
|
+
"NO_BROWSER"
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
const resultsByIndex = new Array(
|
|
626
|
+
steps.length
|
|
627
|
+
);
|
|
628
|
+
let lastSnapshot = "";
|
|
629
|
+
let lastLogs = [];
|
|
630
|
+
let totalDuration = 0;
|
|
631
|
+
const allEvents = [];
|
|
632
|
+
let buffer = [];
|
|
633
|
+
const flushBuffer = async () => {
|
|
634
|
+
if (buffer.length === 0) return;
|
|
635
|
+
const batch = buffer.map((b) => b.step);
|
|
636
|
+
const out = await ctx.state.proxy.dispatchBrowserCommand(batch);
|
|
637
|
+
const outSteps = out.steps ?? [];
|
|
638
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
639
|
+
const returned = outSteps[i] ?? {};
|
|
640
|
+
resultsByIndex[buffer[i].idx] = {
|
|
641
|
+
...returned,
|
|
642
|
+
index: buffer[i].idx,
|
|
643
|
+
command: buffer[i].step.command
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
if (typeof out.snapshot === "string" && out.snapshot.length > 0) {
|
|
647
|
+
lastSnapshot = out.snapshot;
|
|
648
|
+
}
|
|
649
|
+
if (Array.isArray(out.logs)) lastLogs = out.logs;
|
|
650
|
+
if (typeof out.duration === "number") totalDuration += out.duration;
|
|
651
|
+
if (Array.isArray(out.events)) allEvents.push(...out.events);
|
|
652
|
+
buffer = [];
|
|
653
|
+
};
|
|
654
|
+
for (let i = 0; i < steps.length; i++) {
|
|
655
|
+
const step = steps[i];
|
|
656
|
+
const command = step.command;
|
|
657
|
+
if (command === "screenshotFullPage" || command === "screenshotViewport") {
|
|
658
|
+
await flushBuffer();
|
|
659
|
+
const captured = await captureScreenshotStep(ctx, page, step, command);
|
|
660
|
+
resultsByIndex[i] = { index: i, command, result: captured };
|
|
661
|
+
totalDuration += captured._durationMs ?? 0;
|
|
662
|
+
delete captured._durationMs;
|
|
663
|
+
} else {
|
|
664
|
+
buffer.push({ idx: i, step });
|
|
173
665
|
}
|
|
174
666
|
}
|
|
175
|
-
|
|
667
|
+
await flushBuffer();
|
|
668
|
+
const densified = resultsByIndex.map(
|
|
669
|
+
(r, idx) => r ?? { index: idx, command: steps[idx].command, error: "no result" }
|
|
670
|
+
);
|
|
671
|
+
const hasStepError = densified.some((s) => s?.error);
|
|
672
|
+
const recordingUrl = await uploadRecording(ctx, allEvents);
|
|
176
673
|
return {
|
|
177
674
|
success: !hasStepError,
|
|
178
675
|
...hasStepError ? { errorCode: "BROWSER_ERROR" } : {},
|
|
179
|
-
steps:
|
|
180
|
-
snapshot:
|
|
181
|
-
logs:
|
|
182
|
-
duration:
|
|
676
|
+
steps: densified,
|
|
677
|
+
snapshot: lastSnapshot,
|
|
678
|
+
logs: lastLogs,
|
|
679
|
+
duration: totalDuration,
|
|
680
|
+
...recordingUrl ? { recordingUrl } : {}
|
|
183
681
|
};
|
|
184
682
|
}
|
|
185
|
-
async function
|
|
683
|
+
async function captureScreenshotStep(ctx, page, step, command) {
|
|
186
684
|
const session = ctx.state.runner?.getSession();
|
|
187
685
|
const appId = ctx.state.appConfig?.appId;
|
|
188
|
-
if (!session || !appId)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
686
|
+
if (!session || !appId) {
|
|
687
|
+
throw new CommandError("No active session", "NO_SESSION");
|
|
688
|
+
}
|
|
689
|
+
const { uploadUrl, uploadFields, publicUrl } = await getUploadUrl(
|
|
690
|
+
appId,
|
|
691
|
+
session.sessionId,
|
|
692
|
+
"jpg",
|
|
693
|
+
"image/jpeg"
|
|
694
|
+
);
|
|
695
|
+
const start = Date.now();
|
|
696
|
+
const r = await captureViaCdp(page, {
|
|
697
|
+
fullPage: command === "screenshotFullPage",
|
|
698
|
+
path: typeof step.path === "string" ? step.path : void 0,
|
|
699
|
+
uploadUrl,
|
|
700
|
+
uploadFields
|
|
701
|
+
});
|
|
702
|
+
return {
|
|
703
|
+
url: publicUrl,
|
|
704
|
+
width: r.width,
|
|
705
|
+
height: r.height,
|
|
706
|
+
...r.styleMap ? { styleMap: r.styleMap } : {},
|
|
707
|
+
_durationMs: Date.now() - start
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
async function uploadRecording(ctx, events) {
|
|
711
|
+
if (events.length === 0) return null;
|
|
712
|
+
const session = ctx.state.runner?.getSession();
|
|
713
|
+
const appId = ctx.state.appConfig?.appId;
|
|
714
|
+
if (!session || !appId) return null;
|
|
715
|
+
const body = JSON.stringify(events);
|
|
716
|
+
if (body.length < MIN_RECORDING_BYTES) return null;
|
|
717
|
+
try {
|
|
718
|
+
const { uploadUrl, uploadFields, publicUrl } = await getUploadUrl(
|
|
719
|
+
appId,
|
|
720
|
+
session.sessionId,
|
|
721
|
+
"json",
|
|
722
|
+
"application/json"
|
|
723
|
+
);
|
|
724
|
+
const form = new FormData();
|
|
725
|
+
for (const [k, v] of Object.entries(uploadFields)) form.append(k, v);
|
|
726
|
+
form.append(
|
|
727
|
+
"file",
|
|
728
|
+
new Blob([body], { type: "application/json" }),
|
|
729
|
+
"recording.json"
|
|
730
|
+
);
|
|
731
|
+
const res = await fetch(uploadUrl, { method: "POST", body: form });
|
|
732
|
+
if (!res.ok) {
|
|
733
|
+
log.warn("browser", "Recording upload failed", {
|
|
734
|
+
status: res.status,
|
|
735
|
+
bytes: body.length
|
|
736
|
+
});
|
|
737
|
+
return null;
|
|
205
738
|
}
|
|
739
|
+
log.info("browser", "Recording uploaded", {
|
|
740
|
+
bytes: body.length,
|
|
741
|
+
events: events.length
|
|
742
|
+
});
|
|
743
|
+
return publicUrl;
|
|
744
|
+
} catch (err) {
|
|
745
|
+
log.warn("browser", "Recording upload errored", {
|
|
746
|
+
error: err instanceof Error ? err.message : String(err)
|
|
747
|
+
});
|
|
748
|
+
return null;
|
|
206
749
|
}
|
|
207
|
-
return prepared;
|
|
208
750
|
}
|
|
209
751
|
|
|
210
752
|
// src/dev/stdin-commands/screenshot-full-page.ts
|
|
211
753
|
async function handleScreenshotFullPage(ctx, cmd) {
|
|
212
|
-
if (!ctx.state.proxy) throw new CommandError("No active proxy", "NO_BROWSER");
|
|
213
|
-
if (!ctx.state.proxy.isBrowserConnected()) {
|
|
214
|
-
throw new CommandError("No browser connected", "NO_BROWSER");
|
|
215
|
-
}
|
|
216
754
|
if (!ctx.state.runner?.getSession() || !ctx.state.appConfig?.appId) {
|
|
217
755
|
throw new CommandError("No active session", "NO_SESSION");
|
|
218
756
|
}
|
|
757
|
+
const page = ctx.state.browser?.getActivePage();
|
|
758
|
+
if (!page) {
|
|
759
|
+
throw new CommandError(
|
|
760
|
+
"Sandbox browser unavailable \u2014 headless Chrome is required for screenshots",
|
|
761
|
+
"NO_BROWSER"
|
|
762
|
+
);
|
|
763
|
+
}
|
|
219
764
|
const startTime = Date.now();
|
|
220
765
|
const session = ctx.state.runner.getSession();
|
|
221
766
|
const { uploadUrl, uploadFields, publicUrl } = await getUploadUrl(
|
|
@@ -224,23 +769,18 @@ async function handleScreenshotFullPage(ctx, cmd) {
|
|
|
224
769
|
"jpg",
|
|
225
770
|
"image/jpeg"
|
|
226
771
|
);
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const resultSteps = result.steps;
|
|
234
|
-
const stepResult = resultSteps?.[resultSteps.length - 1]?.result;
|
|
235
|
-
if (!stepResult?.uploaded) {
|
|
236
|
-
throw new CommandError("Screenshot capture or upload failed", "UPLOAD_FAILED");
|
|
237
|
-
}
|
|
772
|
+
const r = await captureViaCdp(page, {
|
|
773
|
+
fullPage: true,
|
|
774
|
+
path: typeof cmd.path === "string" ? cmd.path : void 0,
|
|
775
|
+
uploadUrl,
|
|
776
|
+
uploadFields
|
|
777
|
+
});
|
|
238
778
|
return {
|
|
239
779
|
success: true,
|
|
240
780
|
url: publicUrl,
|
|
241
|
-
width:
|
|
242
|
-
height:
|
|
243
|
-
...
|
|
781
|
+
width: r.width,
|
|
782
|
+
height: r.height,
|
|
783
|
+
...r.styleMap ? { styleMap: r.styleMap } : {},
|
|
244
784
|
duration: Date.now() - startTime
|
|
245
785
|
};
|
|
246
786
|
}
|
|
@@ -252,33 +792,6 @@ async function handleDevServerRestarting(ctx) {
|
|
|
252
792
|
return { success: true };
|
|
253
793
|
}
|
|
254
794
|
|
|
255
|
-
// src/dev/stdin-commands/browser-status.ts
|
|
256
|
-
async function handleBrowserStatus(ctx) {
|
|
257
|
-
return { connected: ctx.state.proxy?.isBrowserConnected() ?? false };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// src/dev/stdin-commands/reset-browser.ts
|
|
261
|
-
async function handleResetBrowser(ctx) {
|
|
262
|
-
if (!ctx.state.proxy) throw new CommandError("No active proxy", "NO_BROWSER");
|
|
263
|
-
if (!ctx.state.proxy.isBrowserConnected()) throw new CommandError("No browser connected", "NO_BROWSER");
|
|
264
|
-
const restoreResult = await ctx.state.proxy.dispatchBrowserCommand([
|
|
265
|
-
{ command: "restoreState" }
|
|
266
|
-
]);
|
|
267
|
-
const stepResult = restoreResult.steps?.[0];
|
|
268
|
-
const restored = stepResult?.result;
|
|
269
|
-
if (restored?.restored) {
|
|
270
|
-
const steps = [{ command: "reload" }];
|
|
271
|
-
if (restored.path && restored.path !== "/") {
|
|
272
|
-
steps.push({ command: "navigate", url: restored.path });
|
|
273
|
-
}
|
|
274
|
-
steps.push({ command: "snapshot" });
|
|
275
|
-
await ctx.state.proxy.dispatchBrowserCommand(steps);
|
|
276
|
-
return { success: true, restored: true, path: restored.path };
|
|
277
|
-
}
|
|
278
|
-
ctx.state.proxy.broadcastToClients("reload");
|
|
279
|
-
return { success: true, restored: false };
|
|
280
|
-
}
|
|
281
|
-
|
|
282
795
|
// src/dev/stdin-commands/db-query.ts
|
|
283
796
|
async function handleDbQuery(ctx, cmd) {
|
|
284
797
|
if (!ctx.state.runner) throw new CommandError("No active session", "NO_SESSION");
|
|
@@ -321,32 +834,30 @@ async function handleDbQuery(ctx, cmd) {
|
|
|
321
834
|
|
|
322
835
|
// src/dev/stdin-commands/setup-browser.ts
|
|
323
836
|
async function handleSetupBrowser(ctx, cmd) {
|
|
324
|
-
if (!ctx.state.proxy) throw new CommandError("No active proxy", "NO_BROWSER");
|
|
325
|
-
if (!ctx.state.proxy.isBrowserConnected()) {
|
|
326
|
-
throw new CommandError("No browser connected", "NO_BROWSER");
|
|
327
|
-
}
|
|
328
837
|
if (!ctx.state.appConfig?.appId) throw new CommandError("No active session", "NO_SESSION");
|
|
838
|
+
const page = ctx.state.browser?.getActivePage();
|
|
839
|
+
if (!page) {
|
|
840
|
+
throw new CommandError(
|
|
841
|
+
"Sandbox browser unavailable \u2014 headless Chrome is required for setup-browser",
|
|
842
|
+
"NO_BROWSER"
|
|
843
|
+
);
|
|
844
|
+
}
|
|
329
845
|
const auth = cmd.auth;
|
|
330
846
|
const path = cmd.path || "/";
|
|
331
|
-
|
|
332
|
-
steps.push({ command: "stashState" });
|
|
333
|
-
steps.push({
|
|
334
|
-
command: "evaluate",
|
|
335
|
-
script: `document.cookie = '__ms_auth=; Max-Age=0; Path=/; Secure; SameSite=None'`
|
|
336
|
-
});
|
|
847
|
+
await clearAuthCookies(page);
|
|
337
848
|
if (auth) {
|
|
338
849
|
const { cookie } = await createAuthSession(ctx.state.appConfig.appId, auth);
|
|
339
|
-
|
|
340
|
-
command: "evaluate",
|
|
341
|
-
script: `document.cookie = '__ms_auth=${cookie}; Path=/; Secure; SameSite=None'`
|
|
342
|
-
});
|
|
850
|
+
await setAuthCookie(page, cookie);
|
|
343
851
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
852
|
+
const absolute = new URL(path, page.url()).toString();
|
|
853
|
+
try {
|
|
854
|
+
await page.goto(absolute, { waitUntil: "networkidle0", timeout: 15e3 });
|
|
855
|
+
} catch (err) {
|
|
856
|
+
throw new CommandError(
|
|
857
|
+
`Navigation to ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
858
|
+
"BROWSER_ERROR"
|
|
859
|
+
);
|
|
347
860
|
}
|
|
348
|
-
steps.push({ command: "snapshot" });
|
|
349
|
-
await ctx.state.proxy.dispatchBrowserCommand(steps);
|
|
350
861
|
return { success: true, path, authenticated: !!auth };
|
|
351
862
|
}
|
|
352
863
|
|
|
@@ -358,8 +869,6 @@ var handlers = {
|
|
|
358
869
|
"clear-impersonation": handleClearImpersonation,
|
|
359
870
|
"browser": handleBrowser,
|
|
360
871
|
"screenshotFullPage": handleScreenshotFullPage,
|
|
361
|
-
"browser-status": handleBrowserStatus,
|
|
362
|
-
"reset-browser": handleResetBrowser,
|
|
363
872
|
"db-query": handleDbQuery,
|
|
364
873
|
"setup-browser": handleSetupBrowser,
|
|
365
874
|
"dev-server-restarting": handleDevServerRestarting
|
|
@@ -490,6 +999,17 @@ async function startSession(cwd, opts, state, shutdown) {
|
|
|
490
999
|
}
|
|
491
1000
|
runner.setProxyUrl(`http://${bindAddress === "0.0.0.0" ? "localhost" : bindAddress}:${state.proxyPort}`);
|
|
492
1001
|
runner.setProxy(state.proxy);
|
|
1002
|
+
if (opts.sandboxBrowser && state.proxyPort !== null && !state.browser) {
|
|
1003
|
+
const webConfig = getWebInterfaceConfig(appConfig, cwd);
|
|
1004
|
+
const previewMode = webConfig?.defaultPreviewMode ?? "desktop";
|
|
1005
|
+
const supervisor = new BrowserSupervisor(state.proxyPort, previewMode);
|
|
1006
|
+
state.browser = supervisor;
|
|
1007
|
+
supervisor.start().catch((err) => {
|
|
1008
|
+
log.warn("browser", "Sandbox browser failed to start", {
|
|
1009
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1010
|
+
});
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
493
1013
|
}
|
|
494
1014
|
emitEvent("session-started", {
|
|
495
1015
|
sessionId: session.sessionId,
|
|
@@ -563,6 +1083,11 @@ async function teardownRunner(state) {
|
|
|
563
1083
|
}
|
|
564
1084
|
async function teardownAll(state) {
|
|
565
1085
|
await teardownRunner(state);
|
|
1086
|
+
if (state.browser) {
|
|
1087
|
+
await state.browser.stop().catch(() => {
|
|
1088
|
+
});
|
|
1089
|
+
state.browser = null;
|
|
1090
|
+
}
|
|
566
1091
|
state.proxy?.stop();
|
|
567
1092
|
state.proxy = null;
|
|
568
1093
|
state.proxyPort = null;
|
|
@@ -585,6 +1110,7 @@ async function startHeadless(opts = {}) {
|
|
|
585
1110
|
const state = {
|
|
586
1111
|
runner: null,
|
|
587
1112
|
proxy: null,
|
|
1113
|
+
browser: null,
|
|
588
1114
|
appConfig: null,
|
|
589
1115
|
proxyPort: null,
|
|
590
1116
|
unsubscribers: []
|
|
@@ -698,4 +1224,4 @@ async function startHeadless(opts = {}) {
|
|
|
698
1224
|
export {
|
|
699
1225
|
startHeadless
|
|
700
1226
|
};
|
|
701
|
-
//# sourceMappingURL=chunk-
|
|
1227
|
+
//# sourceMappingURL=chunk-5A5ASXUO.js.map
|