refactor(browser): reuse shared tab context in snapshot routes

This commit is contained in:
Peter Steinberger
2026-02-18 22:18:33 +00:00
parent 42f34af776
commit bb00eb2031

View File

@@ -18,82 +18,90 @@ import {
readBody, readBody,
requirePwAi, requirePwAi,
resolveProfileContext, resolveProfileContext,
withPlaywrightRouteContext,
withRouteTabContext,
} from "./agent.shared.js"; } from "./agent.shared.js";
import type { BrowserRouteRegistrar } from "./types.js"; import type { BrowserResponse, BrowserRouteRegistrar } from "./types.js";
import { jsonError, toBoolean, toNumber, toStringOrEmpty } from "./utils.js"; import { jsonError, toBoolean, toNumber, toStringOrEmpty } from "./utils.js";
async function saveBrowserMediaResponse(params: {
res: BrowserResponse;
buffer: Buffer;
contentType: string;
maxBytes: number;
targetId: string;
url: string;
}) {
await ensureMediaDir();
const saved = await saveMediaBuffer(
params.buffer,
params.contentType,
"browser",
params.maxBytes,
);
params.res.json({
ok: true,
path: path.resolve(saved.path),
targetId: params.targetId,
url: params.url,
});
}
export function registerBrowserAgentSnapshotRoutes( export function registerBrowserAgentSnapshotRoutes(
app: BrowserRouteRegistrar, app: BrowserRouteRegistrar,
ctx: BrowserRouteContext, ctx: BrowserRouteContext,
) { ) {
app.post("/navigate", async (req, res) => { app.post("/navigate", async (req, res) => {
const profileCtx = resolveProfileContext(req, res, ctx);
if (!profileCtx) {
return;
}
const body = readBody(req); const body = readBody(req);
const url = toStringOrEmpty(body.url); const url = toStringOrEmpty(body.url);
const targetId = toStringOrEmpty(body.targetId) || undefined; const targetId = toStringOrEmpty(body.targetId) || undefined;
if (!url) { if (!url) {
return jsonError(res, 400, "url is required"); return jsonError(res, 400, "url is required");
} }
try { await withPlaywrightRouteContext({
const tab = await profileCtx.ensureTabAvailable(targetId); req,
const pw = await requirePwAi(res, "navigate"); res,
if (!pw) { ctx,
return; targetId,
} feature: "navigate",
const result = await pw.navigateViaPlaywright({ run: async ({ cdpUrl, tab, pw }) => {
cdpUrl: profileCtx.profile.cdpUrl, const result = await pw.navigateViaPlaywright({
targetId: tab.targetId, cdpUrl,
url, targetId: tab.targetId,
}); url,
res.json({ ok: true, targetId: tab.targetId, ...result }); });
} catch (err) { res.json({ ok: true, targetId: tab.targetId, ...result });
handleRouteError(ctx, res, err); },
} });
}); });
app.post("/pdf", async (req, res) => { app.post("/pdf", async (req, res) => {
const profileCtx = resolveProfileContext(req, res, ctx);
if (!profileCtx) {
return;
}
const body = readBody(req); const body = readBody(req);
const targetId = toStringOrEmpty(body.targetId) || undefined; const targetId = toStringOrEmpty(body.targetId) || undefined;
try { await withPlaywrightRouteContext({
const tab = await profileCtx.ensureTabAvailable(targetId); req,
const pw = await requirePwAi(res, "pdf"); res,
if (!pw) { ctx,
return; targetId,
} feature: "pdf",
const pdf = await pw.pdfViaPlaywright({ run: async ({ cdpUrl, tab, pw }) => {
cdpUrl: profileCtx.profile.cdpUrl, const pdf = await pw.pdfViaPlaywright({
targetId: tab.targetId, cdpUrl,
}); targetId: tab.targetId,
await ensureMediaDir(); });
const saved = await saveMediaBuffer( await saveBrowserMediaResponse({
pdf.buffer, res,
"application/pdf", buffer: pdf.buffer,
"browser", contentType: "application/pdf",
pdf.buffer.byteLength, maxBytes: pdf.buffer.byteLength,
); targetId: tab.targetId,
res.json({ url: tab.url,
ok: true, });
path: path.resolve(saved.path), },
targetId: tab.targetId, });
url: tab.url,
});
} catch (err) {
handleRouteError(ctx, res, err);
}
}); });
app.post("/screenshot", async (req, res) => { app.post("/screenshot", async (req, res) => {
const profileCtx = resolveProfileContext(req, res, ctx);
if (!profileCtx) {
return;
}
const body = readBody(req); const body = readBody(req);
const targetId = toStringOrEmpty(body.targetId) || undefined; const targetId = toStringOrEmpty(body.targetId) || undefined;
const fullPage = toBoolean(body.fullPage) ?? false; const fullPage = toBoolean(body.fullPage) ?? false;
@@ -105,54 +113,55 @@ export function registerBrowserAgentSnapshotRoutes(
return jsonError(res, 400, "fullPage is not supported for element screenshots"); return jsonError(res, 400, "fullPage is not supported for element screenshots");
} }
try { await withRouteTabContext({
const tab = await profileCtx.ensureTabAvailable(targetId); req,
let buffer: Buffer; res,
const shouldUsePlaywright = ctx,
profileCtx.profile.driver === "extension" || !tab.wsUrl || Boolean(ref) || Boolean(element); targetId,
if (shouldUsePlaywright) { run: async ({ profileCtx, tab, cdpUrl }) => {
const pw = await requirePwAi(res, "screenshot"); let buffer: Buffer;
if (!pw) { const shouldUsePlaywright =
return; profileCtx.profile.driver === "extension" ||
!tab.wsUrl ||
Boolean(ref) ||
Boolean(element);
if (shouldUsePlaywright) {
const pw = await requirePwAi(res, "screenshot");
if (!pw) {
return;
}
const snap = await pw.takeScreenshotViaPlaywright({
cdpUrl,
targetId: tab.targetId,
ref,
element,
fullPage,
type,
});
buffer = snap.buffer;
} else {
buffer = await captureScreenshot({
wsUrl: tab.wsUrl ?? "",
fullPage,
format: type,
quality: type === "jpeg" ? 85 : undefined,
});
} }
const snap = await pw.takeScreenshotViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl,
targetId: tab.targetId,
ref,
element,
fullPage,
type,
});
buffer = snap.buffer;
} else {
buffer = await captureScreenshot({
wsUrl: tab.wsUrl ?? "",
fullPage,
format: type,
quality: type === "jpeg" ? 85 : undefined,
});
}
const normalized = await normalizeBrowserScreenshot(buffer, { const normalized = await normalizeBrowserScreenshot(buffer, {
maxSide: DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE, maxSide: DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE,
maxBytes: DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES, maxBytes: DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES,
}); });
await ensureMediaDir(); await saveBrowserMediaResponse({
const saved = await saveMediaBuffer( res,
normalized.buffer, buffer: normalized.buffer,
normalized.contentType ?? `image/${type}`, contentType: normalized.contentType ?? `image/${type}`,
"browser", maxBytes: DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES,
DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES, targetId: tab.targetId,
); url: tab.url,
res.json({ });
ok: true, },
path: path.resolve(saved.path), });
targetId: tab.targetId,
url: tab.url,
});
} catch (err) {
handleRouteError(ctx, res, err);
}
}); });
app.get("/snapshot", async (req, res) => { app.get("/snapshot", async (req, res) => {