@agenticmail/enterprise 0.5.78 → 0.5.79
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-7RNT4O5T.js +15198 -0
- package/dist/chunk-AGFOJCSB.js +2191 -0
- package/dist/chunk-F4GSFCM3.js +898 -0
- package/dist/chunk-PZA7YOJE.js +898 -0
- package/dist/chunk-Q3V7VZFQ.js +2191 -0
- package/dist/chunk-RRFB6G6M.js +15198 -0
- package/dist/chunk-VX3VFMVB.js +409 -0
- package/dist/cli.js +1 -1
- package/dist/dashboard/pages/agent-detail.js +313 -1
- package/dist/index.js +4 -3
- package/dist/pw-ai-KPETTB25.js +2212 -0
- package/dist/routes-PDHMCIXU.js +6676 -0
- package/dist/runtime-7HW4GX5L.js +48 -0
- package/dist/runtime-XXDCZZIK.js +48 -0
- package/dist/server-FMP4BFGW.js +12 -0
- package/dist/server-JRHDUNII.js +12 -0
- package/dist/setup-O5FPRLK4.js +20 -0
- package/dist/setup-S4Z4PPIJ.js +20 -0
- package/package.json +15 -2
- package/src/agent-tools/common.ts +25 -0
- package/src/agent-tools/index.ts +3 -0
- package/src/agent-tools/schema/typebox.ts +25 -0
- package/src/agent-tools/tools/browser-tool.schema.ts +112 -0
- package/src/agent-tools/tools/browser-tool.ts +388 -0
- package/src/agent-tools/tools/gateway.ts +126 -0
- package/src/agent-tools/tools/nodes-utils.ts +80 -0
- package/src/browser/bridge-auth-registry.ts +34 -0
- package/src/browser/bridge-server.ts +93 -0
- package/src/browser/cdp.helpers.ts +180 -0
- package/src/browser/cdp.ts +466 -0
- package/src/browser/chrome.executables.ts +625 -0
- package/src/browser/chrome.profile-decoration.ts +198 -0
- package/src/browser/chrome.ts +349 -0
- package/src/browser/client-actions-core.ts +259 -0
- package/src/browser/client-actions-observe.ts +184 -0
- package/src/browser/client-actions-state.ts +284 -0
- package/src/browser/client-actions-types.ts +16 -0
- package/src/browser/client-actions-url.ts +11 -0
- package/src/browser/client-actions.ts +4 -0
- package/src/browser/client-fetch.ts +253 -0
- package/src/browser/client.ts +337 -0
- package/src/browser/config.ts +296 -0
- package/src/browser/constants.ts +8 -0
- package/src/browser/control-auth.ts +94 -0
- package/src/browser/control-service.ts +81 -0
- package/src/browser/csrf.ts +87 -0
- package/src/browser/enterprise-compat.ts +518 -0
- package/src/browser/extension-relay.ts +834 -0
- package/src/browser/http-auth.ts +63 -0
- package/src/browser/navigation-guard.ts +50 -0
- package/src/browser/paths.ts +49 -0
- package/src/browser/profiles-service.ts +187 -0
- package/src/browser/profiles.ts +113 -0
- package/src/browser/proxy-files.ts +41 -0
- package/src/browser/pw-ai-module.ts +52 -0
- package/src/browser/pw-ai-state.ts +9 -0
- package/src/browser/pw-ai.ts +65 -0
- package/src/browser/pw-role-snapshot.ts +434 -0
- package/src/browser/pw-session.ts +810 -0
- package/src/browser/pw-tools-core.activity.ts +68 -0
- package/src/browser/pw-tools-core.downloads.ts +281 -0
- package/src/browser/pw-tools-core.interactions.ts +646 -0
- package/src/browser/pw-tools-core.responses.ts +124 -0
- package/src/browser/pw-tools-core.shared.ts +70 -0
- package/src/browser/pw-tools-core.snapshot.ts +213 -0
- package/src/browser/pw-tools-core.state.ts +209 -0
- package/src/browser/pw-tools-core.storage.ts +128 -0
- package/src/browser/pw-tools-core.trace.ts +37 -0
- package/src/browser/pw-tools-core.ts +8 -0
- package/src/browser/resolved-config-refresh.ts +59 -0
- package/src/browser/routes/agent.act.shared.ts +52 -0
- package/src/browser/routes/agent.act.ts +575 -0
- package/src/browser/routes/agent.debug.ts +149 -0
- package/src/browser/routes/agent.shared.ts +143 -0
- package/src/browser/routes/agent.snapshot.ts +333 -0
- package/src/browser/routes/agent.storage.ts +451 -0
- package/src/browser/routes/agent.ts +13 -0
- package/src/browser/routes/basic.ts +202 -0
- package/src/browser/routes/dispatcher.ts +126 -0
- package/src/browser/routes/index.ts +11 -0
- package/src/browser/routes/path-output.ts +1 -0
- package/src/browser/routes/tabs.ts +217 -0
- package/src/browser/routes/types.ts +26 -0
- package/src/browser/routes/utils.ts +73 -0
- package/src/browser/screenshot.ts +54 -0
- package/src/browser/server-context.ts +688 -0
- package/src/browser/server-context.types.ts +65 -0
- package/src/browser/server-lifecycle.ts +48 -0
- package/src/browser/server-middleware.ts +37 -0
- package/src/browser/server.ts +110 -0
- package/src/browser/target-id.ts +30 -0
- package/src/browser/trash.ts +21 -0
- package/src/dashboard/pages/agent-detail.js +313 -1
- package/src/engine/agent-routes.ts +46 -0
- package/src/security/external-content.ts +299 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import type { BrowserRouteContext } from "../server-context.js";
|
|
5
|
+
import {
|
|
6
|
+
readBody,
|
|
7
|
+
resolveTargetIdFromBody,
|
|
8
|
+
resolveTargetIdFromQuery,
|
|
9
|
+
withPlaywrightRouteContext,
|
|
10
|
+
} from "./agent.shared.js";
|
|
11
|
+
import { DEFAULT_TRACE_DIR, resolvePathWithinRoot } from "./path-output.js";
|
|
12
|
+
import type { BrowserRouteRegistrar } from "./types.js";
|
|
13
|
+
import { toBoolean, toStringOrEmpty } from "./utils.js";
|
|
14
|
+
|
|
15
|
+
export function registerBrowserAgentDebugRoutes(
|
|
16
|
+
app: BrowserRouteRegistrar,
|
|
17
|
+
ctx: BrowserRouteContext,
|
|
18
|
+
) {
|
|
19
|
+
app.get("/console", async (req, res) => {
|
|
20
|
+
const targetId = resolveTargetIdFromQuery(req.query);
|
|
21
|
+
const level = typeof req.query.level === "string" ? req.query.level : "";
|
|
22
|
+
|
|
23
|
+
await withPlaywrightRouteContext({
|
|
24
|
+
req,
|
|
25
|
+
res,
|
|
26
|
+
ctx,
|
|
27
|
+
targetId,
|
|
28
|
+
feature: "console messages",
|
|
29
|
+
run: async ({ cdpUrl, tab, pw }) => {
|
|
30
|
+
const messages = await pw.getConsoleMessagesViaPlaywright({
|
|
31
|
+
cdpUrl,
|
|
32
|
+
targetId: tab.targetId,
|
|
33
|
+
level: level.trim() || undefined,
|
|
34
|
+
});
|
|
35
|
+
res.json({ ok: true, messages, targetId: tab.targetId });
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
app.get("/errors", async (req, res) => {
|
|
41
|
+
const targetId = resolveTargetIdFromQuery(req.query);
|
|
42
|
+
const clear = toBoolean(req.query.clear) ?? false;
|
|
43
|
+
|
|
44
|
+
await withPlaywrightRouteContext({
|
|
45
|
+
req,
|
|
46
|
+
res,
|
|
47
|
+
ctx,
|
|
48
|
+
targetId,
|
|
49
|
+
feature: "page errors",
|
|
50
|
+
run: async ({ cdpUrl, tab, pw }) => {
|
|
51
|
+
const result = await pw.getPageErrorsViaPlaywright({
|
|
52
|
+
cdpUrl,
|
|
53
|
+
targetId: tab.targetId,
|
|
54
|
+
clear,
|
|
55
|
+
});
|
|
56
|
+
res.json({ ok: true, targetId: tab.targetId, ...result });
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
app.get("/requests", async (req, res) => {
|
|
62
|
+
const targetId = resolveTargetIdFromQuery(req.query);
|
|
63
|
+
const filter = typeof req.query.filter === "string" ? req.query.filter : "";
|
|
64
|
+
const clear = toBoolean(req.query.clear) ?? false;
|
|
65
|
+
|
|
66
|
+
await withPlaywrightRouteContext({
|
|
67
|
+
req,
|
|
68
|
+
res,
|
|
69
|
+
ctx,
|
|
70
|
+
targetId,
|
|
71
|
+
feature: "network requests",
|
|
72
|
+
run: async ({ cdpUrl, tab, pw }) => {
|
|
73
|
+
const result = await pw.getNetworkRequestsViaPlaywright({
|
|
74
|
+
cdpUrl,
|
|
75
|
+
targetId: tab.targetId,
|
|
76
|
+
filter: filter.trim() || undefined,
|
|
77
|
+
clear,
|
|
78
|
+
});
|
|
79
|
+
res.json({ ok: true, targetId: tab.targetId, ...result });
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
app.post("/trace/start", async (req, res) => {
|
|
85
|
+
const body = readBody(req);
|
|
86
|
+
const targetId = resolveTargetIdFromBody(body);
|
|
87
|
+
const screenshots = toBoolean(body.screenshots) ?? undefined;
|
|
88
|
+
const snapshots = toBoolean(body.snapshots) ?? undefined;
|
|
89
|
+
const sources = toBoolean(body.sources) ?? undefined;
|
|
90
|
+
|
|
91
|
+
await withPlaywrightRouteContext({
|
|
92
|
+
req,
|
|
93
|
+
res,
|
|
94
|
+
ctx,
|
|
95
|
+
targetId,
|
|
96
|
+
feature: "trace start",
|
|
97
|
+
run: async ({ cdpUrl, tab, pw }) => {
|
|
98
|
+
await pw.traceStartViaPlaywright({
|
|
99
|
+
cdpUrl,
|
|
100
|
+
targetId: tab.targetId,
|
|
101
|
+
screenshots,
|
|
102
|
+
snapshots,
|
|
103
|
+
sources,
|
|
104
|
+
});
|
|
105
|
+
res.json({ ok: true, targetId: tab.targetId });
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
app.post("/trace/stop", async (req, res) => {
|
|
111
|
+
const body = readBody(req);
|
|
112
|
+
const targetId = resolveTargetIdFromBody(body);
|
|
113
|
+
const out = toStringOrEmpty(body.path) || "";
|
|
114
|
+
|
|
115
|
+
await withPlaywrightRouteContext({
|
|
116
|
+
req,
|
|
117
|
+
res,
|
|
118
|
+
ctx,
|
|
119
|
+
targetId,
|
|
120
|
+
feature: "trace stop",
|
|
121
|
+
run: async ({ cdpUrl, tab, pw }) => {
|
|
122
|
+
const id = crypto.randomUUID();
|
|
123
|
+
const dir = DEFAULT_TRACE_DIR;
|
|
124
|
+
await fs.mkdir(dir, { recursive: true });
|
|
125
|
+
const tracePathResult = resolvePathWithinRoot({
|
|
126
|
+
rootDir: dir,
|
|
127
|
+
requestedPath: out,
|
|
128
|
+
scopeLabel: "trace directory",
|
|
129
|
+
defaultFileName: `browser-trace-${id}.zip`,
|
|
130
|
+
});
|
|
131
|
+
if (!tracePathResult.ok) {
|
|
132
|
+
res.status(400).json({ error: tracePathResult.error });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const tracePath = tracePathResult.path;
|
|
136
|
+
await pw.traceStopViaPlaywright({
|
|
137
|
+
cdpUrl,
|
|
138
|
+
targetId: tab.targetId,
|
|
139
|
+
path: tracePath,
|
|
140
|
+
});
|
|
141
|
+
res.json({
|
|
142
|
+
ok: true,
|
|
143
|
+
targetId: tab.targetId,
|
|
144
|
+
path: path.resolve(tracePath),
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { PwAiModule } from "../pw-ai-module.js";
|
|
2
|
+
import { getPwAiModule as getPwAiModuleBase } from "../pw-ai-module.js";
|
|
3
|
+
import type { BrowserRouteContext, ProfileContext } from "../server-context.js";
|
|
4
|
+
import type { BrowserRequest, BrowserResponse } from "./types.js";
|
|
5
|
+
import { getProfileContext, jsonError } from "./utils.js";
|
|
6
|
+
|
|
7
|
+
export const SELECTOR_UNSUPPORTED_MESSAGE = [
|
|
8
|
+
"Error: 'selector' is not supported. Use 'ref' from snapshot instead.",
|
|
9
|
+
"",
|
|
10
|
+
"Example workflow:",
|
|
11
|
+
"1. snapshot action to get page state with refs",
|
|
12
|
+
'2. act with ref: "e123" to interact with element',
|
|
13
|
+
"",
|
|
14
|
+
"This is more reliable for modern SPAs.",
|
|
15
|
+
].join("\n");
|
|
16
|
+
|
|
17
|
+
export function readBody(req: BrowserRequest): Record<string, unknown> {
|
|
18
|
+
const body = req.body as Record<string, unknown> | undefined;
|
|
19
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
return body;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveTargetIdFromBody(body: Record<string, unknown>): string | undefined {
|
|
26
|
+
const targetId = typeof body.targetId === "string" ? body.targetId.trim() : "";
|
|
27
|
+
return targetId || undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveTargetIdFromQuery(query: Record<string, unknown>): string | undefined {
|
|
31
|
+
const targetId = typeof query.targetId === "string" ? query.targetId.trim() : "";
|
|
32
|
+
return targetId || undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function handleRouteError(ctx: BrowserRouteContext, res: BrowserResponse, err: unknown) {
|
|
36
|
+
const mapped = ctx.mapTabError(err);
|
|
37
|
+
if (mapped) {
|
|
38
|
+
return jsonError(res, mapped.status, mapped.message);
|
|
39
|
+
}
|
|
40
|
+
jsonError(res, 500, String(err));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function resolveProfileContext(
|
|
44
|
+
req: BrowserRequest,
|
|
45
|
+
res: BrowserResponse,
|
|
46
|
+
ctx: BrowserRouteContext,
|
|
47
|
+
): ProfileContext | null {
|
|
48
|
+
const profileCtx = getProfileContext(req, ctx);
|
|
49
|
+
if ("error" in profileCtx) {
|
|
50
|
+
jsonError(res, profileCtx.status, profileCtx.error);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return profileCtx;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function getPwAiModule(): Promise<PwAiModule | null> {
|
|
57
|
+
return await getPwAiModuleBase({ mode: "soft" });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function requirePwAi(
|
|
61
|
+
res: BrowserResponse,
|
|
62
|
+
feature: string,
|
|
63
|
+
): Promise<PwAiModule | null> {
|
|
64
|
+
const mod = await getPwAiModule();
|
|
65
|
+
if (mod) {
|
|
66
|
+
return mod;
|
|
67
|
+
}
|
|
68
|
+
jsonError(
|
|
69
|
+
res,
|
|
70
|
+
501,
|
|
71
|
+
[
|
|
72
|
+
`Playwright is not available in this gateway build; '${feature}' is unsupported.`,
|
|
73
|
+
"Install the full Playwright package (not playwright-core) and restart the gateway, or reinstall with browser support.",
|
|
74
|
+
"Docs: /tools/browser#playwright-requirement",
|
|
75
|
+
].join("\n"),
|
|
76
|
+
);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type RouteTabContext = {
|
|
81
|
+
profileCtx: ProfileContext;
|
|
82
|
+
tab: Awaited<ReturnType<ProfileContext["ensureTabAvailable"]>>;
|
|
83
|
+
cdpUrl: string;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
type RouteTabPwContext = RouteTabContext & {
|
|
87
|
+
pw: PwAiModule;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
type RouteWithTabParams<T> = {
|
|
91
|
+
req: BrowserRequest;
|
|
92
|
+
res: BrowserResponse;
|
|
93
|
+
ctx: BrowserRouteContext;
|
|
94
|
+
targetId?: string;
|
|
95
|
+
run: (ctx: RouteTabContext) => Promise<T>;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export async function withRouteTabContext<T>(
|
|
99
|
+
params: RouteWithTabParams<T>,
|
|
100
|
+
): Promise<T | undefined> {
|
|
101
|
+
const profileCtx = resolveProfileContext(params.req, params.res, params.ctx);
|
|
102
|
+
if (!profileCtx) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const tab = await profileCtx.ensureTabAvailable(params.targetId);
|
|
107
|
+
return await params.run({
|
|
108
|
+
profileCtx,
|
|
109
|
+
tab,
|
|
110
|
+
cdpUrl: profileCtx.profile.cdpUrl,
|
|
111
|
+
});
|
|
112
|
+
} catch (err) {
|
|
113
|
+
handleRouteError(params.ctx, params.res, err);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type RouteWithPwParams<T> = {
|
|
119
|
+
req: BrowserRequest;
|
|
120
|
+
res: BrowserResponse;
|
|
121
|
+
ctx: BrowserRouteContext;
|
|
122
|
+
targetId?: string;
|
|
123
|
+
feature: string;
|
|
124
|
+
run: (ctx: RouteTabPwContext) => Promise<T>;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export async function withPlaywrightRouteContext<T>(
|
|
128
|
+
params: RouteWithPwParams<T>,
|
|
129
|
+
): Promise<T | undefined> {
|
|
130
|
+
return await withRouteTabContext({
|
|
131
|
+
req: params.req,
|
|
132
|
+
res: params.res,
|
|
133
|
+
ctx: params.ctx,
|
|
134
|
+
targetId: params.targetId,
|
|
135
|
+
run: async ({ profileCtx, tab, cdpUrl }) => {
|
|
136
|
+
const pw = await requirePwAi(params.res, params.feature);
|
|
137
|
+
if (!pw) {
|
|
138
|
+
return undefined as T | undefined;
|
|
139
|
+
}
|
|
140
|
+
return await params.run({ profileCtx, tab, cdpUrl, pw });
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { ensureMediaDir, saveMediaBuffer } from "../enterprise-compat.js";
|
|
3
|
+
import { captureScreenshot, snapshotAria } from "../cdp.js";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH,
|
|
6
|
+
DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS,
|
|
7
|
+
DEFAULT_AI_SNAPSHOT_MAX_CHARS,
|
|
8
|
+
} from "../constants.js";
|
|
9
|
+
import { withBrowserNavigationPolicy } from "../navigation-guard.js";
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES,
|
|
12
|
+
DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE,
|
|
13
|
+
normalizeBrowserScreenshot,
|
|
14
|
+
} from "../screenshot.js";
|
|
15
|
+
import type { BrowserRouteContext } from "../server-context.js";
|
|
16
|
+
import {
|
|
17
|
+
getPwAiModule,
|
|
18
|
+
handleRouteError,
|
|
19
|
+
readBody,
|
|
20
|
+
requirePwAi,
|
|
21
|
+
resolveProfileContext,
|
|
22
|
+
withPlaywrightRouteContext,
|
|
23
|
+
withRouteTabContext,
|
|
24
|
+
} from "./agent.shared.js";
|
|
25
|
+
import type { BrowserResponse, BrowserRouteRegistrar } from "./types.js";
|
|
26
|
+
import { jsonError, toBoolean, toNumber, toStringOrEmpty } from "./utils.js";
|
|
27
|
+
|
|
28
|
+
async function saveBrowserMediaResponse(params: {
|
|
29
|
+
res: BrowserResponse;
|
|
30
|
+
buffer: Buffer;
|
|
31
|
+
contentType: string;
|
|
32
|
+
maxBytes: number;
|
|
33
|
+
targetId: string;
|
|
34
|
+
url: string;
|
|
35
|
+
}) {
|
|
36
|
+
await ensureMediaDir();
|
|
37
|
+
const saved = await saveMediaBuffer(
|
|
38
|
+
params.buffer,
|
|
39
|
+
params.contentType,
|
|
40
|
+
"browser",
|
|
41
|
+
params.maxBytes,
|
|
42
|
+
);
|
|
43
|
+
params.res.json({
|
|
44
|
+
ok: true,
|
|
45
|
+
path: path.resolve(saved.path),
|
|
46
|
+
targetId: params.targetId,
|
|
47
|
+
url: params.url,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function registerBrowserAgentSnapshotRoutes(
|
|
52
|
+
app: BrowserRouteRegistrar,
|
|
53
|
+
ctx: BrowserRouteContext,
|
|
54
|
+
) {
|
|
55
|
+
app.post("/navigate", async (req, res) => {
|
|
56
|
+
const body = readBody(req);
|
|
57
|
+
const url = toStringOrEmpty(body.url);
|
|
58
|
+
const targetId = toStringOrEmpty(body.targetId) || undefined;
|
|
59
|
+
if (!url) {
|
|
60
|
+
return jsonError(res, 400, "url is required");
|
|
61
|
+
}
|
|
62
|
+
await withPlaywrightRouteContext({
|
|
63
|
+
req,
|
|
64
|
+
res,
|
|
65
|
+
ctx,
|
|
66
|
+
targetId,
|
|
67
|
+
feature: "navigate",
|
|
68
|
+
run: async ({ cdpUrl, tab, pw }) => {
|
|
69
|
+
const result = await pw.navigateViaPlaywright({
|
|
70
|
+
cdpUrl,
|
|
71
|
+
targetId: tab.targetId,
|
|
72
|
+
url,
|
|
73
|
+
...withBrowserNavigationPolicy(ctx.state().resolved.ssrfPolicy),
|
|
74
|
+
});
|
|
75
|
+
res.json({ ok: true, targetId: tab.targetId, ...result });
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
app.post("/pdf", async (req, res) => {
|
|
81
|
+
const body = readBody(req);
|
|
82
|
+
const targetId = toStringOrEmpty(body.targetId) || undefined;
|
|
83
|
+
await withPlaywrightRouteContext({
|
|
84
|
+
req,
|
|
85
|
+
res,
|
|
86
|
+
ctx,
|
|
87
|
+
targetId,
|
|
88
|
+
feature: "pdf",
|
|
89
|
+
run: async ({ cdpUrl, tab, pw }) => {
|
|
90
|
+
const pdf = await pw.pdfViaPlaywright({
|
|
91
|
+
cdpUrl,
|
|
92
|
+
targetId: tab.targetId,
|
|
93
|
+
});
|
|
94
|
+
await saveBrowserMediaResponse({
|
|
95
|
+
res,
|
|
96
|
+
buffer: pdf.buffer,
|
|
97
|
+
contentType: "application/pdf",
|
|
98
|
+
maxBytes: pdf.buffer.byteLength,
|
|
99
|
+
targetId: tab.targetId,
|
|
100
|
+
url: tab.url,
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
app.post("/screenshot", async (req, res) => {
|
|
107
|
+
const body = readBody(req);
|
|
108
|
+
const targetId = toStringOrEmpty(body.targetId) || undefined;
|
|
109
|
+
const fullPage = toBoolean(body.fullPage) ?? false;
|
|
110
|
+
const ref = toStringOrEmpty(body.ref) || undefined;
|
|
111
|
+
const element = toStringOrEmpty(body.element) || undefined;
|
|
112
|
+
const type = body.type === "jpeg" ? "jpeg" : "png";
|
|
113
|
+
|
|
114
|
+
if (fullPage && (ref || element)) {
|
|
115
|
+
return jsonError(res, 400, "fullPage is not supported for element screenshots");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await withRouteTabContext({
|
|
119
|
+
req,
|
|
120
|
+
res,
|
|
121
|
+
ctx,
|
|
122
|
+
targetId,
|
|
123
|
+
run: async ({ profileCtx, tab, cdpUrl }) => {
|
|
124
|
+
let buffer: Buffer;
|
|
125
|
+
const shouldUsePlaywright =
|
|
126
|
+
profileCtx.profile.driver === "extension" ||
|
|
127
|
+
!tab.wsUrl ||
|
|
128
|
+
Boolean(ref) ||
|
|
129
|
+
Boolean(element);
|
|
130
|
+
if (shouldUsePlaywright) {
|
|
131
|
+
const pw = await requirePwAi(res, "screenshot");
|
|
132
|
+
if (!pw) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const snap = await pw.takeScreenshotViaPlaywright({
|
|
136
|
+
cdpUrl,
|
|
137
|
+
targetId: tab.targetId,
|
|
138
|
+
ref,
|
|
139
|
+
element,
|
|
140
|
+
fullPage,
|
|
141
|
+
type,
|
|
142
|
+
});
|
|
143
|
+
buffer = snap.buffer;
|
|
144
|
+
} else {
|
|
145
|
+
buffer = await captureScreenshot({
|
|
146
|
+
wsUrl: tab.wsUrl ?? "",
|
|
147
|
+
fullPage,
|
|
148
|
+
format: type,
|
|
149
|
+
quality: type === "jpeg" ? 85 : undefined,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const normalized = await normalizeBrowserScreenshot(buffer, {
|
|
154
|
+
maxSide: DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE,
|
|
155
|
+
maxBytes: DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES,
|
|
156
|
+
});
|
|
157
|
+
await saveBrowserMediaResponse({
|
|
158
|
+
res,
|
|
159
|
+
buffer: normalized.buffer,
|
|
160
|
+
contentType: normalized.contentType ?? `image/${type}`,
|
|
161
|
+
maxBytes: DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES,
|
|
162
|
+
targetId: tab.targetId,
|
|
163
|
+
url: tab.url,
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
app.get("/snapshot", async (req, res) => {
|
|
170
|
+
const profileCtx = resolveProfileContext(req, res, ctx);
|
|
171
|
+
if (!profileCtx) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const targetId = typeof req.query.targetId === "string" ? req.query.targetId.trim() : "";
|
|
175
|
+
const mode = req.query.mode === "efficient" ? "efficient" : undefined;
|
|
176
|
+
const labels = toBoolean(req.query.labels) ?? undefined;
|
|
177
|
+
const explicitFormat =
|
|
178
|
+
req.query.format === "aria" ? "aria" : req.query.format === "ai" ? "ai" : undefined;
|
|
179
|
+
const format = explicitFormat ?? (mode ? "ai" : (await getPwAiModule()) ? "ai" : "aria");
|
|
180
|
+
const limitRaw = typeof req.query.limit === "string" ? Number(req.query.limit) : undefined;
|
|
181
|
+
const hasMaxChars = Object.hasOwn(req.query, "maxChars");
|
|
182
|
+
const maxCharsRaw =
|
|
183
|
+
typeof req.query.maxChars === "string" ? Number(req.query.maxChars) : undefined;
|
|
184
|
+
const limit = Number.isFinite(limitRaw) ? limitRaw : undefined;
|
|
185
|
+
const maxChars =
|
|
186
|
+
typeof maxCharsRaw === "number" && Number.isFinite(maxCharsRaw) && maxCharsRaw > 0
|
|
187
|
+
? Math.floor(maxCharsRaw)
|
|
188
|
+
: undefined;
|
|
189
|
+
const resolvedMaxChars =
|
|
190
|
+
format === "ai"
|
|
191
|
+
? hasMaxChars
|
|
192
|
+
? maxChars
|
|
193
|
+
: mode === "efficient"
|
|
194
|
+
? DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS
|
|
195
|
+
: DEFAULT_AI_SNAPSHOT_MAX_CHARS
|
|
196
|
+
: undefined;
|
|
197
|
+
const interactiveRaw = toBoolean(req.query.interactive);
|
|
198
|
+
const compactRaw = toBoolean(req.query.compact);
|
|
199
|
+
const depthRaw = toNumber(req.query.depth);
|
|
200
|
+
const refsModeRaw = toStringOrEmpty(req.query.refs).trim();
|
|
201
|
+
const refsMode: "aria" | "role" | undefined =
|
|
202
|
+
refsModeRaw === "aria" ? "aria" : refsModeRaw === "role" ? "role" : undefined;
|
|
203
|
+
const interactive = interactiveRaw ?? (mode === "efficient" ? true : undefined);
|
|
204
|
+
const compact = compactRaw ?? (mode === "efficient" ? true : undefined);
|
|
205
|
+
const depth =
|
|
206
|
+
depthRaw ?? (mode === "efficient" ? DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH : undefined);
|
|
207
|
+
const selector = toStringOrEmpty(req.query.selector);
|
|
208
|
+
const frameSelector = toStringOrEmpty(req.query.frame);
|
|
209
|
+
const selectorValue = selector.trim() || undefined;
|
|
210
|
+
const frameSelectorValue = frameSelector.trim() || undefined;
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const tab = await profileCtx.ensureTabAvailable(targetId || undefined);
|
|
214
|
+
if ((labels || mode === "efficient") && format === "aria") {
|
|
215
|
+
return jsonError(res, 400, "labels/mode=efficient require format=ai");
|
|
216
|
+
}
|
|
217
|
+
if (format === "ai") {
|
|
218
|
+
const pw = await requirePwAi(res, "ai snapshot");
|
|
219
|
+
if (!pw) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const wantsRoleSnapshot =
|
|
223
|
+
labels === true ||
|
|
224
|
+
mode === "efficient" ||
|
|
225
|
+
interactive === true ||
|
|
226
|
+
compact === true ||
|
|
227
|
+
depth !== undefined ||
|
|
228
|
+
Boolean(selectorValue) ||
|
|
229
|
+
Boolean(frameSelectorValue);
|
|
230
|
+
const roleSnapshotArgs = {
|
|
231
|
+
cdpUrl: profileCtx.profile.cdpUrl,
|
|
232
|
+
targetId: tab.targetId,
|
|
233
|
+
selector: selectorValue,
|
|
234
|
+
frameSelector: frameSelectorValue,
|
|
235
|
+
refsMode,
|
|
236
|
+
options: {
|
|
237
|
+
interactive: interactive ?? undefined,
|
|
238
|
+
compact: compact ?? undefined,
|
|
239
|
+
maxDepth: depth ?? undefined,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const snap = wantsRoleSnapshot
|
|
244
|
+
? await pw.snapshotRoleViaPlaywright(roleSnapshotArgs)
|
|
245
|
+
: await pw
|
|
246
|
+
.snapshotAiViaPlaywright({
|
|
247
|
+
cdpUrl: profileCtx.profile.cdpUrl,
|
|
248
|
+
targetId: tab.targetId,
|
|
249
|
+
...(typeof resolvedMaxChars === "number" ? { maxChars: resolvedMaxChars } : {}),
|
|
250
|
+
})
|
|
251
|
+
.catch(async (err) => {
|
|
252
|
+
// Public-API fallback when Playwright's private _snapshotForAI is missing.
|
|
253
|
+
if (String(err).toLowerCase().includes("_snapshotforai")) {
|
|
254
|
+
return await pw.snapshotRoleViaPlaywright(roleSnapshotArgs);
|
|
255
|
+
}
|
|
256
|
+
throw err;
|
|
257
|
+
});
|
|
258
|
+
if (labels) {
|
|
259
|
+
const labeled = await pw.screenshotWithLabelsViaPlaywright({
|
|
260
|
+
cdpUrl: profileCtx.profile.cdpUrl,
|
|
261
|
+
targetId: tab.targetId,
|
|
262
|
+
refs: "refs" in snap ? snap.refs : {},
|
|
263
|
+
type: "png",
|
|
264
|
+
});
|
|
265
|
+
const normalized = await normalizeBrowserScreenshot(labeled.buffer, {
|
|
266
|
+
maxSide: DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE,
|
|
267
|
+
maxBytes: DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES,
|
|
268
|
+
});
|
|
269
|
+
await ensureMediaDir();
|
|
270
|
+
const saved = await saveMediaBuffer(
|
|
271
|
+
normalized.buffer,
|
|
272
|
+
normalized.contentType ?? "image/png",
|
|
273
|
+
"browser",
|
|
274
|
+
DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES,
|
|
275
|
+
);
|
|
276
|
+
const imageType = normalized.contentType?.includes("jpeg") ? "jpeg" : "png";
|
|
277
|
+
return res.json({
|
|
278
|
+
ok: true,
|
|
279
|
+
format,
|
|
280
|
+
targetId: tab.targetId,
|
|
281
|
+
url: tab.url,
|
|
282
|
+
labels: true,
|
|
283
|
+
labelsCount: labeled.labels,
|
|
284
|
+
labelsSkipped: labeled.skipped,
|
|
285
|
+
imagePath: path.resolve(saved.path),
|
|
286
|
+
imageType,
|
|
287
|
+
...snap,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return res.json({
|
|
292
|
+
ok: true,
|
|
293
|
+
format,
|
|
294
|
+
targetId: tab.targetId,
|
|
295
|
+
url: tab.url,
|
|
296
|
+
...snap,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const snap =
|
|
301
|
+
profileCtx.profile.driver === "extension" || !tab.wsUrl
|
|
302
|
+
? (() => {
|
|
303
|
+
// Extension relay doesn't expose per-page WS URLs; run AX snapshot via Playwright CDP session.
|
|
304
|
+
// Also covers cases where wsUrl is missing/unusable.
|
|
305
|
+
return requirePwAi(res, "aria snapshot").then(async (pw) => {
|
|
306
|
+
if (!pw) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
return await pw.snapshotAriaViaPlaywright({
|
|
310
|
+
cdpUrl: profileCtx.profile.cdpUrl,
|
|
311
|
+
targetId: tab.targetId,
|
|
312
|
+
limit,
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
})()
|
|
316
|
+
: snapshotAria({ wsUrl: tab.wsUrl ?? "", limit });
|
|
317
|
+
|
|
318
|
+
const resolved = await Promise.resolve(snap);
|
|
319
|
+
if (!resolved) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
return res.json({
|
|
323
|
+
ok: true,
|
|
324
|
+
format,
|
|
325
|
+
targetId: tab.targetId,
|
|
326
|
+
url: tab.url,
|
|
327
|
+
...resolved,
|
|
328
|
+
});
|
|
329
|
+
} catch (err) {
|
|
330
|
+
handleRouteError(ctx, res, err);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|