@mindstudio-ai/local-model-tunnel 0.5.54 → 0.5.56
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-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/{chunk-B7BNG7SI.js → chunk-M37JM7QT.js} +629 -102
- package/dist/chunk-M37JM7QT.js.map +1 -0
- 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-KTSJTXQG.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-KTSJTXQG.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,350 @@ 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
|
+
const absolute = new URL(opts.path, page.url()).toString();
|
|
376
|
+
await page.goto(absolute, {
|
|
377
|
+
waitUntil: "networkidle0",
|
|
378
|
+
timeout: GOTO_TIMEOUT_MS
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
await page.waitForNetworkIdle({ timeout: SETTLE_TIMEOUT_MS, idleTime: SETTLE_IDLE_MS }).catch(() => {
|
|
382
|
+
});
|
|
383
|
+
if (opts.fullPage) {
|
|
384
|
+
await preRollScroll(page);
|
|
385
|
+
}
|
|
386
|
+
let width;
|
|
387
|
+
let height;
|
|
388
|
+
if (opts.fullPage) {
|
|
389
|
+
const dims = await page.evaluate(() => ({
|
|
390
|
+
width: document.documentElement.scrollWidth,
|
|
391
|
+
height: document.documentElement.scrollHeight
|
|
392
|
+
}));
|
|
393
|
+
width = dims.width;
|
|
394
|
+
height = dims.height;
|
|
395
|
+
} else {
|
|
396
|
+
const vp = page.viewport();
|
|
397
|
+
width = vp?.width ?? 0;
|
|
398
|
+
height = vp?.height ?? 0;
|
|
399
|
+
}
|
|
400
|
+
let styleMap;
|
|
401
|
+
try {
|
|
402
|
+
const result = await page.evaluate(() => {
|
|
403
|
+
const api = window.__MINDSTUDIO_BROWSER_AGENT__;
|
|
404
|
+
return api?.computeStyleMap?.() ?? null;
|
|
405
|
+
});
|
|
406
|
+
if (typeof result === "string" && result.length > 0) styleMap = result;
|
|
407
|
+
} catch {
|
|
408
|
+
}
|
|
409
|
+
const buf = await page.screenshot({
|
|
410
|
+
type: "jpeg",
|
|
411
|
+
quality: JPEG_QUALITY,
|
|
412
|
+
fullPage: opts.fullPage
|
|
413
|
+
});
|
|
414
|
+
await uploadToPresigned(opts.uploadUrl, opts.uploadFields, buf);
|
|
415
|
+
return {
|
|
416
|
+
uploaded: true,
|
|
417
|
+
width,
|
|
418
|
+
height,
|
|
419
|
+
...styleMap ? { styleMap } : {}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
async function preRollScroll(page) {
|
|
423
|
+
try {
|
|
424
|
+
const scrolled = await page.evaluate(() => {
|
|
425
|
+
const el = document.scrollingElement || document.documentElement;
|
|
426
|
+
const max = Math.max(
|
|
427
|
+
document.documentElement.scrollHeight,
|
|
428
|
+
document.body.scrollHeight
|
|
429
|
+
);
|
|
430
|
+
if (max <= window.innerHeight + 10) return false;
|
|
431
|
+
el.scrollTo({ top: max, left: 0, behavior: "instant" });
|
|
432
|
+
return true;
|
|
433
|
+
});
|
|
434
|
+
if (!scrolled) return;
|
|
435
|
+
await new Promise((r) => setTimeout(r, PREROLL_BOTTOM_DWELL_MS));
|
|
436
|
+
await page.waitForNetworkIdle({
|
|
437
|
+
timeout: PREROLL_NETWORK_IDLE_MS,
|
|
438
|
+
idleTime: SETTLE_IDLE_MS
|
|
439
|
+
}).catch(() => {
|
|
440
|
+
});
|
|
441
|
+
await page.evaluate(() => {
|
|
442
|
+
const el = document.scrollingElement || document.documentElement;
|
|
443
|
+
el.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
444
|
+
});
|
|
445
|
+
await new Promise((r) => setTimeout(r, PREROLL_TOP_DWELL_MS));
|
|
446
|
+
} catch {
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async function uploadToPresigned(uploadUrl, uploadFields, buf) {
|
|
450
|
+
const form = new FormData();
|
|
451
|
+
for (const [k, v] of Object.entries(uploadFields)) form.append(k, v);
|
|
452
|
+
form.append(
|
|
453
|
+
"file",
|
|
454
|
+
new Blob([buf], { type: "image/jpeg" }),
|
|
455
|
+
"screenshot.jpg"
|
|
456
|
+
);
|
|
457
|
+
const res = await fetch(uploadUrl, { method: "POST", body: form });
|
|
458
|
+
if (!res.ok) {
|
|
459
|
+
throw new Error(`Screenshot upload failed: ${res.status}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/dev/browser/cookies.ts
|
|
464
|
+
var AUTH_COOKIE_NAME = "__ms_auth";
|
|
465
|
+
function cookieHost(page) {
|
|
466
|
+
try {
|
|
467
|
+
return new URL(page.url()).hostname || "127.0.0.1";
|
|
468
|
+
} catch {
|
|
469
|
+
return "127.0.0.1";
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async function clearAuthCookies(page) {
|
|
473
|
+
const domain = cookieHost(page);
|
|
474
|
+
try {
|
|
475
|
+
await page.deleteCookie({ name: AUTH_COOKIE_NAME, domain });
|
|
476
|
+
} catch {
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
await page.deleteCookie({ name: AUTH_COOKIE_NAME });
|
|
480
|
+
} catch {
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
async function setAuthCookie(page, value) {
|
|
484
|
+
const domain = cookieHost(page);
|
|
485
|
+
await page.setCookie({
|
|
486
|
+
name: AUTH_COOKIE_NAME,
|
|
487
|
+
value,
|
|
488
|
+
domain,
|
|
489
|
+
path: "/",
|
|
490
|
+
sameSite: "None",
|
|
491
|
+
secure: true
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
40
495
|
// src/dev/ipc/session-events.ts
|
|
41
496
|
function subscribeDevEvents(shutdown) {
|
|
42
497
|
const unsubs = [];
|
|
@@ -154,68 +609,159 @@ async function handleClearImpersonation(ctx) {
|
|
|
154
609
|
}
|
|
155
610
|
|
|
156
611
|
// src/dev/stdin-commands/browser.ts
|
|
612
|
+
var MIN_RECORDING_BYTES = 5e3;
|
|
157
613
|
async function handleBrowser(ctx, cmd) {
|
|
158
614
|
if (!ctx.state.proxy) throw new CommandError("No active proxy", "NO_BROWSER");
|
|
159
615
|
const steps = cmd.steps;
|
|
160
616
|
if (!Array.isArray(steps) || steps.length === 0) {
|
|
161
617
|
throw new CommandError('browser action requires a non-empty "steps" array', "INVALID_INPUT");
|
|
162
618
|
}
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
619
|
+
const page = ctx.state.browser?.getActivePage();
|
|
620
|
+
if (!page) {
|
|
621
|
+
throw new CommandError(
|
|
622
|
+
"Sandbox browser unavailable \u2014 headless Chrome is required for automation",
|
|
623
|
+
"NO_BROWSER"
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
const resultsByIndex = new Array(
|
|
627
|
+
steps.length
|
|
628
|
+
);
|
|
629
|
+
let lastSnapshot = "";
|
|
630
|
+
let lastLogs = [];
|
|
631
|
+
let totalDuration = 0;
|
|
632
|
+
const allEvents = [];
|
|
633
|
+
let buffer = [];
|
|
634
|
+
const flushBuffer = async () => {
|
|
635
|
+
if (buffer.length === 0) return;
|
|
636
|
+
const batch = buffer.map((b) => b.step);
|
|
637
|
+
const out = await ctx.state.proxy.dispatchBrowserCommand(batch);
|
|
638
|
+
const outSteps = out.steps ?? [];
|
|
639
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
640
|
+
const returned = outSteps[i] ?? {};
|
|
641
|
+
resultsByIndex[buffer[i].idx] = {
|
|
642
|
+
...returned,
|
|
643
|
+
index: buffer[i].idx,
|
|
644
|
+
command: buffer[i].step.command
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
if (typeof out.snapshot === "string" && out.snapshot.length > 0) {
|
|
648
|
+
lastSnapshot = out.snapshot;
|
|
649
|
+
}
|
|
650
|
+
if (Array.isArray(out.logs)) lastLogs = out.logs;
|
|
651
|
+
if (typeof out.duration === "number") totalDuration += out.duration;
|
|
652
|
+
if (Array.isArray(out.events)) allEvents.push(...out.events);
|
|
653
|
+
buffer = [];
|
|
654
|
+
};
|
|
655
|
+
for (let i = 0; i < steps.length; i++) {
|
|
656
|
+
const step = steps[i];
|
|
657
|
+
const command = step.command;
|
|
658
|
+
if (command === "screenshotFullPage" || command === "screenshotViewport") {
|
|
659
|
+
await flushBuffer();
|
|
660
|
+
const captured = await captureScreenshotStep(ctx, page, step, command);
|
|
661
|
+
resultsByIndex[i] = { index: i, command, result: captured };
|
|
662
|
+
totalDuration += captured._durationMs ?? 0;
|
|
663
|
+
delete captured._durationMs;
|
|
664
|
+
} else {
|
|
665
|
+
buffer.push({ idx: i, step });
|
|
173
666
|
}
|
|
174
667
|
}
|
|
175
|
-
|
|
668
|
+
await flushBuffer();
|
|
669
|
+
const densified = resultsByIndex.map(
|
|
670
|
+
(r, idx) => r ?? { index: idx, command: steps[idx].command, error: "no result" }
|
|
671
|
+
);
|
|
672
|
+
const hasStepError = densified.some((s) => s?.error);
|
|
673
|
+
const recordingUrl = await uploadRecording(ctx, allEvents);
|
|
176
674
|
return {
|
|
177
675
|
success: !hasStepError,
|
|
178
676
|
...hasStepError ? { errorCode: "BROWSER_ERROR" } : {},
|
|
179
|
-
steps:
|
|
180
|
-
snapshot:
|
|
181
|
-
logs:
|
|
182
|
-
duration:
|
|
677
|
+
steps: densified,
|
|
678
|
+
snapshot: lastSnapshot,
|
|
679
|
+
logs: lastLogs,
|
|
680
|
+
duration: totalDuration,
|
|
681
|
+
...recordingUrl ? { recordingUrl } : {}
|
|
183
682
|
};
|
|
184
683
|
}
|
|
185
|
-
async function
|
|
684
|
+
async function captureScreenshotStep(ctx, page, step, command) {
|
|
186
685
|
const session = ctx.state.runner?.getSession();
|
|
187
686
|
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
|
-
|
|
687
|
+
if (!session || !appId) {
|
|
688
|
+
throw new CommandError("No active session", "NO_SESSION");
|
|
689
|
+
}
|
|
690
|
+
const { uploadUrl, uploadFields, publicUrl } = await getUploadUrl(
|
|
691
|
+
appId,
|
|
692
|
+
session.sessionId,
|
|
693
|
+
"jpg",
|
|
694
|
+
"image/jpeg"
|
|
695
|
+
);
|
|
696
|
+
const start = Date.now();
|
|
697
|
+
const r = await captureViaCdp(page, {
|
|
698
|
+
fullPage: command === "screenshotFullPage",
|
|
699
|
+
path: typeof step.path === "string" ? step.path : void 0,
|
|
700
|
+
uploadUrl,
|
|
701
|
+
uploadFields
|
|
702
|
+
});
|
|
703
|
+
return {
|
|
704
|
+
url: publicUrl,
|
|
705
|
+
width: r.width,
|
|
706
|
+
height: r.height,
|
|
707
|
+
...r.styleMap ? { styleMap: r.styleMap } : {},
|
|
708
|
+
_durationMs: Date.now() - start
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
async function uploadRecording(ctx, events) {
|
|
712
|
+
if (events.length === 0) return null;
|
|
713
|
+
const session = ctx.state.runner?.getSession();
|
|
714
|
+
const appId = ctx.state.appConfig?.appId;
|
|
715
|
+
if (!session || !appId) return null;
|
|
716
|
+
const body = JSON.stringify(events);
|
|
717
|
+
if (body.length < MIN_RECORDING_BYTES) return null;
|
|
718
|
+
try {
|
|
719
|
+
const { uploadUrl, uploadFields, publicUrl } = await getUploadUrl(
|
|
720
|
+
appId,
|
|
721
|
+
session.sessionId,
|
|
722
|
+
"json",
|
|
723
|
+
"application/json"
|
|
724
|
+
);
|
|
725
|
+
const form = new FormData();
|
|
726
|
+
for (const [k, v] of Object.entries(uploadFields)) form.append(k, v);
|
|
727
|
+
form.append(
|
|
728
|
+
"file",
|
|
729
|
+
new Blob([body], { type: "application/json" }),
|
|
730
|
+
"recording.json"
|
|
731
|
+
);
|
|
732
|
+
const res = await fetch(uploadUrl, { method: "POST", body: form });
|
|
733
|
+
if (!res.ok) {
|
|
734
|
+
log.warn("browser", "Recording upload failed", {
|
|
735
|
+
status: res.status,
|
|
736
|
+
bytes: body.length
|
|
737
|
+
});
|
|
738
|
+
return null;
|
|
205
739
|
}
|
|
740
|
+
log.info("browser", "Recording uploaded", {
|
|
741
|
+
bytes: body.length,
|
|
742
|
+
events: events.length
|
|
743
|
+
});
|
|
744
|
+
return publicUrl;
|
|
745
|
+
} catch (err) {
|
|
746
|
+
log.warn("browser", "Recording upload errored", {
|
|
747
|
+
error: err instanceof Error ? err.message : String(err)
|
|
748
|
+
});
|
|
749
|
+
return null;
|
|
206
750
|
}
|
|
207
|
-
return prepared;
|
|
208
751
|
}
|
|
209
752
|
|
|
210
753
|
// src/dev/stdin-commands/screenshot-full-page.ts
|
|
211
754
|
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
755
|
if (!ctx.state.runner?.getSession() || !ctx.state.appConfig?.appId) {
|
|
217
756
|
throw new CommandError("No active session", "NO_SESSION");
|
|
218
757
|
}
|
|
758
|
+
const page = ctx.state.browser?.getActivePage();
|
|
759
|
+
if (!page) {
|
|
760
|
+
throw new CommandError(
|
|
761
|
+
"Sandbox browser unavailable \u2014 headless Chrome is required for screenshots",
|
|
762
|
+
"NO_BROWSER"
|
|
763
|
+
);
|
|
764
|
+
}
|
|
219
765
|
const startTime = Date.now();
|
|
220
766
|
const session = ctx.state.runner.getSession();
|
|
221
767
|
const { uploadUrl, uploadFields, publicUrl } = await getUploadUrl(
|
|
@@ -224,23 +770,18 @@ async function handleScreenshotFullPage(ctx, cmd) {
|
|
|
224
770
|
"jpg",
|
|
225
771
|
"image/jpeg"
|
|
226
772
|
);
|
|
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
|
-
}
|
|
773
|
+
const r = await captureViaCdp(page, {
|
|
774
|
+
fullPage: true,
|
|
775
|
+
path: typeof cmd.path === "string" ? cmd.path : void 0,
|
|
776
|
+
uploadUrl,
|
|
777
|
+
uploadFields
|
|
778
|
+
});
|
|
238
779
|
return {
|
|
239
780
|
success: true,
|
|
240
781
|
url: publicUrl,
|
|
241
|
-
width:
|
|
242
|
-
height:
|
|
243
|
-
...
|
|
782
|
+
width: r.width,
|
|
783
|
+
height: r.height,
|
|
784
|
+
...r.styleMap ? { styleMap: r.styleMap } : {},
|
|
244
785
|
duration: Date.now() - startTime
|
|
245
786
|
};
|
|
246
787
|
}
|
|
@@ -252,33 +793,6 @@ async function handleDevServerRestarting(ctx) {
|
|
|
252
793
|
return { success: true };
|
|
253
794
|
}
|
|
254
795
|
|
|
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
796
|
// src/dev/stdin-commands/db-query.ts
|
|
283
797
|
async function handleDbQuery(ctx, cmd) {
|
|
284
798
|
if (!ctx.state.runner) throw new CommandError("No active session", "NO_SESSION");
|
|
@@ -321,32 +835,30 @@ async function handleDbQuery(ctx, cmd) {
|
|
|
321
835
|
|
|
322
836
|
// src/dev/stdin-commands/setup-browser.ts
|
|
323
837
|
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
838
|
if (!ctx.state.appConfig?.appId) throw new CommandError("No active session", "NO_SESSION");
|
|
839
|
+
const page = ctx.state.browser?.getActivePage();
|
|
840
|
+
if (!page) {
|
|
841
|
+
throw new CommandError(
|
|
842
|
+
"Sandbox browser unavailable \u2014 headless Chrome is required for setup-browser",
|
|
843
|
+
"NO_BROWSER"
|
|
844
|
+
);
|
|
845
|
+
}
|
|
329
846
|
const auth = cmd.auth;
|
|
330
847
|
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
|
-
});
|
|
848
|
+
await clearAuthCookies(page);
|
|
337
849
|
if (auth) {
|
|
338
850
|
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
|
-
});
|
|
851
|
+
await setAuthCookie(page, cookie);
|
|
343
852
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
853
|
+
const absolute = new URL(path, page.url()).toString();
|
|
854
|
+
try {
|
|
855
|
+
await page.goto(absolute, { waitUntil: "networkidle0", timeout: 15e3 });
|
|
856
|
+
} catch (err) {
|
|
857
|
+
throw new CommandError(
|
|
858
|
+
`Navigation to ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
859
|
+
"BROWSER_ERROR"
|
|
860
|
+
);
|
|
347
861
|
}
|
|
348
|
-
steps.push({ command: "snapshot" });
|
|
349
|
-
await ctx.state.proxy.dispatchBrowserCommand(steps);
|
|
350
862
|
return { success: true, path, authenticated: !!auth };
|
|
351
863
|
}
|
|
352
864
|
|
|
@@ -358,8 +870,6 @@ var handlers = {
|
|
|
358
870
|
"clear-impersonation": handleClearImpersonation,
|
|
359
871
|
"browser": handleBrowser,
|
|
360
872
|
"screenshotFullPage": handleScreenshotFullPage,
|
|
361
|
-
"browser-status": handleBrowserStatus,
|
|
362
|
-
"reset-browser": handleResetBrowser,
|
|
363
873
|
"db-query": handleDbQuery,
|
|
364
874
|
"setup-browser": handleSetupBrowser,
|
|
365
875
|
"dev-server-restarting": handleDevServerRestarting
|
|
@@ -490,6 +1000,17 @@ async function startSession(cwd, opts, state, shutdown) {
|
|
|
490
1000
|
}
|
|
491
1001
|
runner.setProxyUrl(`http://${bindAddress === "0.0.0.0" ? "localhost" : bindAddress}:${state.proxyPort}`);
|
|
492
1002
|
runner.setProxy(state.proxy);
|
|
1003
|
+
if (opts.sandboxBrowser && state.proxyPort !== null && !state.browser) {
|
|
1004
|
+
const webConfig = getWebInterfaceConfig(appConfig, cwd);
|
|
1005
|
+
const previewMode = webConfig?.defaultPreviewMode ?? "desktop";
|
|
1006
|
+
const supervisor = new BrowserSupervisor(state.proxyPort, previewMode);
|
|
1007
|
+
state.browser = supervisor;
|
|
1008
|
+
supervisor.start().catch((err) => {
|
|
1009
|
+
log.warn("browser", "Sandbox browser failed to start", {
|
|
1010
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1011
|
+
});
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
493
1014
|
}
|
|
494
1015
|
emitEvent("session-started", {
|
|
495
1016
|
sessionId: session.sessionId,
|
|
@@ -563,6 +1084,11 @@ async function teardownRunner(state) {
|
|
|
563
1084
|
}
|
|
564
1085
|
async function teardownAll(state) {
|
|
565
1086
|
await teardownRunner(state);
|
|
1087
|
+
if (state.browser) {
|
|
1088
|
+
await state.browser.stop().catch(() => {
|
|
1089
|
+
});
|
|
1090
|
+
state.browser = null;
|
|
1091
|
+
}
|
|
566
1092
|
state.proxy?.stop();
|
|
567
1093
|
state.proxy = null;
|
|
568
1094
|
state.proxyPort = null;
|
|
@@ -585,6 +1111,7 @@ async function startHeadless(opts = {}) {
|
|
|
585
1111
|
const state = {
|
|
586
1112
|
runner: null,
|
|
587
1113
|
proxy: null,
|
|
1114
|
+
browser: null,
|
|
588
1115
|
appConfig: null,
|
|
589
1116
|
proxyPort: null,
|
|
590
1117
|
unsubscribers: []
|
|
@@ -698,4 +1225,4 @@ async function startHeadless(opts = {}) {
|
|
|
698
1225
|
export {
|
|
699
1226
|
startHeadless
|
|
700
1227
|
};
|
|
701
|
-
//# sourceMappingURL=chunk-
|
|
1228
|
+
//# sourceMappingURL=chunk-M37JM7QT.js.map
|