@agenticmail/enterprise 0.5.77 → 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-CYABMD5B.js +2191 -0
- package/dist/chunk-F4GSFCM3.js +898 -0
- package/dist/chunk-GINZ56GG.js +15035 -0
- package/dist/chunk-NRKB2KGD.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 +491 -2
- package/dist/index.js +4 -3
- package/dist/pw-ai-KPETTB25.js +2212 -0
- package/dist/routes-2T2ZNH3D.js +6642 -0
- package/dist/routes-PDHMCIXU.js +6676 -0
- package/dist/runtime-5ZJYB5PY.js +47 -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/server-QPIMKFK4.js +12 -0
- package/dist/setup-NPFIX7LF.js +20 -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 +491 -2
- package/src/engine/agent-routes.ts +246 -0
- package/src/security/external-content.ts +299 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { IncomingMessage } from "node:http";
|
|
2
|
+
import { safeEqualSecret } from "./enterprise-compat.js";
|
|
3
|
+
|
|
4
|
+
function firstHeaderValue(value: string | string[] | undefined): string {
|
|
5
|
+
return Array.isArray(value) ? (value[0] ?? "") : (value ?? "");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function parseBearerToken(authorization: string): string | undefined {
|
|
9
|
+
if (!authorization || !authorization.toLowerCase().startsWith("bearer ")) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const token = authorization.slice(7).trim();
|
|
13
|
+
return token || undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseBasicPassword(authorization: string): string | undefined {
|
|
17
|
+
if (!authorization || !authorization.toLowerCase().startsWith("basic ")) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const encoded = authorization.slice(6).trim();
|
|
21
|
+
if (!encoded) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const decoded = Buffer.from(encoded, "base64").toString("utf8");
|
|
26
|
+
const sep = decoded.indexOf(":");
|
|
27
|
+
if (sep < 0) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const password = decoded.slice(sep + 1).trim();
|
|
31
|
+
return password || undefined;
|
|
32
|
+
} catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isAuthorizedBrowserRequest(
|
|
38
|
+
req: IncomingMessage,
|
|
39
|
+
auth: { token?: string; password?: string },
|
|
40
|
+
): boolean {
|
|
41
|
+
const authorization = firstHeaderValue(req.headers.authorization).trim();
|
|
42
|
+
|
|
43
|
+
if (auth.token) {
|
|
44
|
+
const bearer = parseBearerToken(authorization);
|
|
45
|
+
if (bearer && safeEqualSecret(bearer, auth.token)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (auth.password) {
|
|
51
|
+
const passwordHeader = firstHeaderValue(req.headers["x-openclaw-password"]).trim();
|
|
52
|
+
if (passwordHeader && safeEqualSecret(passwordHeader, auth.password)) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const basicPassword = parseBasicPassword(authorization);
|
|
57
|
+
if (basicPassword && safeEqualSecret(basicPassword, auth.password)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { resolvePinnedHostnameWithPolicy } from "./enterprise-compat.js";
|
|
2
|
+
import type { LookupFn, SsrFPolicy } from "./enterprise-compat.js";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
const NETWORK_NAVIGATION_PROTOCOLS = new Set(["http:", "https:"]);
|
|
6
|
+
|
|
7
|
+
export class InvalidBrowserNavigationUrlError extends Error {
|
|
8
|
+
constructor(message: string) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "InvalidBrowserNavigationUrlError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type BrowserNavigationPolicyOptions = {
|
|
15
|
+
ssrfPolicy?: SsrFPolicy;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function withBrowserNavigationPolicy(
|
|
19
|
+
ssrfPolicy?: SsrFPolicy,
|
|
20
|
+
): BrowserNavigationPolicyOptions {
|
|
21
|
+
return ssrfPolicy ? { ssrfPolicy } : {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function assertBrowserNavigationAllowed(
|
|
25
|
+
opts: {
|
|
26
|
+
url: string;
|
|
27
|
+
lookupFn?: LookupFn;
|
|
28
|
+
} & BrowserNavigationPolicyOptions,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
const rawUrl = String(opts.url ?? "").trim();
|
|
31
|
+
if (!rawUrl) {
|
|
32
|
+
throw new InvalidBrowserNavigationUrlError("url is required");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let parsed: URL;
|
|
36
|
+
try {
|
|
37
|
+
parsed = new URL(rawUrl);
|
|
38
|
+
} catch {
|
|
39
|
+
throw new InvalidBrowserNavigationUrlError(`Invalid URL: ${rawUrl}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await resolvePinnedHostnameWithPolicy(parsed.hostname, {
|
|
47
|
+
lookupFn: opts.lookupFn,
|
|
48
|
+
policy: opts.ssrfPolicy,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { resolvePreferredOpenClawTmpDir } from "./enterprise-compat.js";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_BROWSER_TMP_DIR = resolvePreferredOpenClawTmpDir();
|
|
5
|
+
export const DEFAULT_TRACE_DIR = DEFAULT_BROWSER_TMP_DIR;
|
|
6
|
+
export const DEFAULT_DOWNLOAD_DIR = path.join(DEFAULT_BROWSER_TMP_DIR, "downloads");
|
|
7
|
+
export const DEFAULT_UPLOAD_DIR = path.join(DEFAULT_BROWSER_TMP_DIR, "uploads");
|
|
8
|
+
|
|
9
|
+
export function resolvePathWithinRoot(params: {
|
|
10
|
+
rootDir: string;
|
|
11
|
+
requestedPath: string;
|
|
12
|
+
scopeLabel: string;
|
|
13
|
+
defaultFileName?: string;
|
|
14
|
+
}): { ok: true; path: string } | { ok: false; error: string } {
|
|
15
|
+
const root = path.resolve(params.rootDir);
|
|
16
|
+
const raw = params.requestedPath.trim();
|
|
17
|
+
if (!raw) {
|
|
18
|
+
if (!params.defaultFileName) {
|
|
19
|
+
return { ok: false, error: "path is required" };
|
|
20
|
+
}
|
|
21
|
+
return { ok: true, path: path.join(root, params.defaultFileName) };
|
|
22
|
+
}
|
|
23
|
+
const resolved = path.resolve(root, raw);
|
|
24
|
+
const rel = path.relative(root, resolved);
|
|
25
|
+
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
26
|
+
return { ok: false, error: `Invalid path: must stay within ${params.scopeLabel}` };
|
|
27
|
+
}
|
|
28
|
+
return { ok: true, path: resolved };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolvePathsWithinRoot(params: {
|
|
32
|
+
rootDir: string;
|
|
33
|
+
requestedPaths: string[];
|
|
34
|
+
scopeLabel: string;
|
|
35
|
+
}): { ok: true; paths: string[] } | { ok: false; error: string } {
|
|
36
|
+
const resolvedPaths: string[] = [];
|
|
37
|
+
for (const raw of params.requestedPaths) {
|
|
38
|
+
const pathResult = resolvePathWithinRoot({
|
|
39
|
+
rootDir: params.rootDir,
|
|
40
|
+
requestedPath: raw,
|
|
41
|
+
scopeLabel: params.scopeLabel,
|
|
42
|
+
});
|
|
43
|
+
if (!pathResult.ok) {
|
|
44
|
+
return { ok: false, error: pathResult.error };
|
|
45
|
+
}
|
|
46
|
+
resolvedPaths.push(pathResult.path);
|
|
47
|
+
}
|
|
48
|
+
return { ok: true, paths: resolvedPaths };
|
|
49
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { resolveOpenClawUserDataDir } from "./chrome.js";
|
|
5
|
+
import { parseHttpUrl, resolveProfile } from "./config.js";
|
|
6
|
+
import { DEFAULT_BROWSER_DEFAULT_PROFILE_NAME } from "./constants.js";
|
|
7
|
+
import {
|
|
8
|
+
allocateCdpPort,
|
|
9
|
+
allocateColor,
|
|
10
|
+
getUsedColors,
|
|
11
|
+
getUsedPorts,
|
|
12
|
+
isValidProfileName,
|
|
13
|
+
} from "./profiles.js";
|
|
14
|
+
import type { BrowserRouteContext, ProfileStatus } from "./server-context.js";
|
|
15
|
+
import { movePathToTrash } from "./trash.js";
|
|
16
|
+
import { deriveDefaultBrowserCdpPortRange, loadConfig, writeConfigFile } from "./enterprise-compat.js";
|
|
17
|
+
import type { BrowserProfileConfig, OpenClawConfig } from "./enterprise-compat.js";
|
|
18
|
+
|
|
19
|
+
export type CreateProfileParams = {
|
|
20
|
+
name: string;
|
|
21
|
+
color?: string;
|
|
22
|
+
cdpUrl?: string;
|
|
23
|
+
driver?: "openclaw" | "extension";
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type CreateProfileResult = {
|
|
27
|
+
ok: true;
|
|
28
|
+
profile: string;
|
|
29
|
+
cdpPort: number;
|
|
30
|
+
cdpUrl: string;
|
|
31
|
+
color: string;
|
|
32
|
+
isRemote: boolean;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type DeleteProfileResult = {
|
|
36
|
+
ok: true;
|
|
37
|
+
profile: string;
|
|
38
|
+
deleted: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const HEX_COLOR_RE = /^#[0-9A-Fa-f]{6}$/;
|
|
42
|
+
|
|
43
|
+
export function createBrowserProfilesService(ctx: BrowserRouteContext) {
|
|
44
|
+
const listProfiles = async (): Promise<ProfileStatus[]> => {
|
|
45
|
+
return await ctx.listProfiles();
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const createProfile = async (params: CreateProfileParams): Promise<CreateProfileResult> => {
|
|
49
|
+
const name = params.name.trim();
|
|
50
|
+
const rawCdpUrl = params.cdpUrl?.trim() || undefined;
|
|
51
|
+
const driver = params.driver === "extension" ? "extension" : undefined;
|
|
52
|
+
|
|
53
|
+
if (!isValidProfileName(name)) {
|
|
54
|
+
throw new Error("invalid profile name: use lowercase letters, numbers, and hyphens only");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const state = ctx.state();
|
|
58
|
+
const resolvedProfiles = state.resolved.profiles;
|
|
59
|
+
if (name in resolvedProfiles) {
|
|
60
|
+
throw new Error(`profile "${name}" already exists`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const cfg = loadConfig();
|
|
64
|
+
const rawProfiles = cfg.browser?.profiles ?? {};
|
|
65
|
+
if (name in rawProfiles) {
|
|
66
|
+
throw new Error(`profile "${name}" already exists`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const usedColors = getUsedColors(resolvedProfiles);
|
|
70
|
+
const profileColor =
|
|
71
|
+
params.color && HEX_COLOR_RE.test(params.color) ? params.color : allocateColor(usedColors);
|
|
72
|
+
|
|
73
|
+
let profileConfig: BrowserProfileConfig;
|
|
74
|
+
if (rawCdpUrl) {
|
|
75
|
+
const parsed = parseHttpUrl(rawCdpUrl, "browser.profiles.cdpUrl");
|
|
76
|
+
profileConfig = {
|
|
77
|
+
cdpUrl: parsed.normalized,
|
|
78
|
+
...(driver ? { driver } : {}),
|
|
79
|
+
color: profileColor,
|
|
80
|
+
};
|
|
81
|
+
} else {
|
|
82
|
+
const usedPorts = getUsedPorts(resolvedProfiles);
|
|
83
|
+
const range = deriveDefaultBrowserCdpPortRange(state.resolved.controlPort);
|
|
84
|
+
const cdpPort = allocateCdpPort(usedPorts, range);
|
|
85
|
+
if (cdpPort === null) {
|
|
86
|
+
throw new Error("no available CDP ports in range");
|
|
87
|
+
}
|
|
88
|
+
profileConfig = {
|
|
89
|
+
cdpPort,
|
|
90
|
+
...(driver ? { driver } : {}),
|
|
91
|
+
color: profileColor,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const nextConfig: OpenClawConfig = {
|
|
96
|
+
...cfg,
|
|
97
|
+
browser: {
|
|
98
|
+
...cfg.browser,
|
|
99
|
+
profiles: {
|
|
100
|
+
...rawProfiles,
|
|
101
|
+
[name]: profileConfig,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
await writeConfigFile(nextConfig);
|
|
107
|
+
|
|
108
|
+
state.resolved.profiles[name] = profileConfig;
|
|
109
|
+
const resolved = resolveProfile(state.resolved, name);
|
|
110
|
+
if (!resolved) {
|
|
111
|
+
throw new Error(`profile "${name}" not found after creation`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
profile: name,
|
|
117
|
+
cdpPort: resolved.cdpPort,
|
|
118
|
+
cdpUrl: resolved.cdpUrl,
|
|
119
|
+
color: resolved.color,
|
|
120
|
+
isRemote: !resolved.cdpIsLoopback,
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const deleteProfile = async (nameRaw: string): Promise<DeleteProfileResult> => {
|
|
125
|
+
const name = nameRaw.trim();
|
|
126
|
+
if (!name) {
|
|
127
|
+
throw new Error("profile name is required");
|
|
128
|
+
}
|
|
129
|
+
if (!isValidProfileName(name)) {
|
|
130
|
+
throw new Error("invalid profile name");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const cfg = loadConfig();
|
|
134
|
+
const profiles = cfg.browser?.profiles ?? {};
|
|
135
|
+
if (!(name in profiles)) {
|
|
136
|
+
throw new Error(`profile "${name}" not found`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const defaultProfile = cfg.browser?.defaultProfile ?? DEFAULT_BROWSER_DEFAULT_PROFILE_NAME;
|
|
140
|
+
if (name === defaultProfile) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`cannot delete the default profile "${name}"; change browser.defaultProfile first`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let deleted = false;
|
|
147
|
+
const state = ctx.state();
|
|
148
|
+
const resolved = resolveProfile(state.resolved, name);
|
|
149
|
+
|
|
150
|
+
if (resolved?.cdpIsLoopback) {
|
|
151
|
+
try {
|
|
152
|
+
await ctx.forProfile(name).stopRunningBrowser();
|
|
153
|
+
} catch {
|
|
154
|
+
// ignore
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const userDataDir = resolveOpenClawUserDataDir(name);
|
|
158
|
+
const profileDir = path.dirname(userDataDir);
|
|
159
|
+
if (fs.existsSync(profileDir)) {
|
|
160
|
+
await movePathToTrash(profileDir);
|
|
161
|
+
deleted = true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { [name]: _removed, ...remainingProfiles } = profiles;
|
|
166
|
+
const nextConfig: OpenClawConfig = {
|
|
167
|
+
...cfg,
|
|
168
|
+
browser: {
|
|
169
|
+
...cfg.browser,
|
|
170
|
+
profiles: remainingProfiles,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
await writeConfigFile(nextConfig);
|
|
175
|
+
|
|
176
|
+
delete state.resolved.profiles[name];
|
|
177
|
+
state.profiles.delete(name);
|
|
178
|
+
|
|
179
|
+
return { ok: true, profile: name, deleted };
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
listProfiles,
|
|
184
|
+
createProfile,
|
|
185
|
+
deleteProfile,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP port allocation for browser profiles.
|
|
3
|
+
*
|
|
4
|
+
* Default port range: 18800-18899 (100 profiles max)
|
|
5
|
+
* Ports are allocated once at profile creation and persisted in config.
|
|
6
|
+
* Multi-instance: callers may pass an explicit range to avoid collisions.
|
|
7
|
+
*
|
|
8
|
+
* Reserved ports (do not use for CDP):
|
|
9
|
+
* 18789 - Gateway WebSocket
|
|
10
|
+
* 18790 - Bridge
|
|
11
|
+
* 18791 - Browser control server
|
|
12
|
+
* 18792-18799 - Reserved for future one-off services (canvas at 18793)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const CDP_PORT_RANGE_START = 18800;
|
|
16
|
+
export const CDP_PORT_RANGE_END = 18899;
|
|
17
|
+
|
|
18
|
+
export const PROFILE_NAME_REGEX = /^[a-z0-9][a-z0-9-]*$/;
|
|
19
|
+
|
|
20
|
+
export function isValidProfileName(name: string): boolean {
|
|
21
|
+
if (!name || name.length > 64) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return PROFILE_NAME_REGEX.test(name);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function allocateCdpPort(
|
|
28
|
+
usedPorts: Set<number>,
|
|
29
|
+
range?: { start: number; end: number },
|
|
30
|
+
): number | null {
|
|
31
|
+
const start = range?.start ?? CDP_PORT_RANGE_START;
|
|
32
|
+
const end = range?.end ?? CDP_PORT_RANGE_END;
|
|
33
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || start <= 0 || end <= 0) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
if (start > end) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
for (let port = start; port <= end; port++) {
|
|
40
|
+
if (!usedPorts.has(port)) {
|
|
41
|
+
return port;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getUsedPorts(
|
|
48
|
+
profiles: Record<string, { cdpPort?: number; cdpUrl?: string }> | undefined,
|
|
49
|
+
): Set<number> {
|
|
50
|
+
if (!profiles) {
|
|
51
|
+
return new Set();
|
|
52
|
+
}
|
|
53
|
+
const used = new Set<number>();
|
|
54
|
+
for (const profile of Object.values(profiles)) {
|
|
55
|
+
if (typeof profile.cdpPort === "number") {
|
|
56
|
+
used.add(profile.cdpPort);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const rawUrl = profile.cdpUrl?.trim();
|
|
60
|
+
if (!rawUrl) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const parsed = new URL(rawUrl);
|
|
65
|
+
const port =
|
|
66
|
+
parsed.port && Number.parseInt(parsed.port, 10) > 0
|
|
67
|
+
? Number.parseInt(parsed.port, 10)
|
|
68
|
+
: parsed.protocol === "https:"
|
|
69
|
+
? 443
|
|
70
|
+
: 80;
|
|
71
|
+
if (!Number.isNaN(port) && port > 0 && port <= 65535) {
|
|
72
|
+
used.add(port);
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// ignore invalid URLs
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return used;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const PROFILE_COLORS = [
|
|
82
|
+
"#FF4500", // Orange-red (openclaw default)
|
|
83
|
+
"#0066CC", // Blue
|
|
84
|
+
"#00AA00", // Green
|
|
85
|
+
"#9933FF", // Purple
|
|
86
|
+
"#FF6699", // Pink
|
|
87
|
+
"#00CCCC", // Cyan
|
|
88
|
+
"#FF9900", // Orange
|
|
89
|
+
"#6666FF", // Indigo
|
|
90
|
+
"#CC3366", // Magenta
|
|
91
|
+
"#339966", // Teal
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
export function allocateColor(usedColors: Set<string>): string {
|
|
95
|
+
// Find first unused color from palette
|
|
96
|
+
for (const color of PROFILE_COLORS) {
|
|
97
|
+
if (!usedColors.has(color.toUpperCase())) {
|
|
98
|
+
return color;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// All colors used, cycle based on count
|
|
102
|
+
const index = usedColors.size % PROFILE_COLORS.length;
|
|
103
|
+
return PROFILE_COLORS[index] ?? PROFILE_COLORS[0];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function getUsedColors(
|
|
107
|
+
profiles: Record<string, { color: string }> | undefined,
|
|
108
|
+
): Set<string> {
|
|
109
|
+
if (!profiles) {
|
|
110
|
+
return new Set();
|
|
111
|
+
}
|
|
112
|
+
return new Set(Object.values(profiles).map((p) => p.color.toUpperCase()));
|
|
113
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { saveMediaBuffer } from "./enterprise-compat.js";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export type BrowserProxyFile = {
|
|
5
|
+
path: string;
|
|
6
|
+
base64: string;
|
|
7
|
+
mimeType?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export async function persistBrowserProxyFiles(files: BrowserProxyFile[] | undefined) {
|
|
11
|
+
if (!files || files.length === 0) {
|
|
12
|
+
return new Map<string, string>();
|
|
13
|
+
}
|
|
14
|
+
const mapping = new Map<string, string>();
|
|
15
|
+
for (const file of files) {
|
|
16
|
+
const buffer = Buffer.from(file.base64, "base64");
|
|
17
|
+
const saved = await saveMediaBuffer(buffer, file.mimeType, "browser", buffer.byteLength);
|
|
18
|
+
mapping.set(file.path, saved.path);
|
|
19
|
+
}
|
|
20
|
+
return mapping;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function applyBrowserProxyPaths(result: unknown, mapping: Map<string, string>) {
|
|
24
|
+
if (!result || typeof result !== "object") {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const obj = result as Record<string, unknown>;
|
|
28
|
+
if (typeof obj.path === "string" && mapping.has(obj.path)) {
|
|
29
|
+
obj.path = mapping.get(obj.path);
|
|
30
|
+
}
|
|
31
|
+
if (typeof obj.imagePath === "string" && mapping.has(obj.imagePath)) {
|
|
32
|
+
obj.imagePath = mapping.get(obj.imagePath);
|
|
33
|
+
}
|
|
34
|
+
const download = obj.download;
|
|
35
|
+
if (download && typeof download === "object") {
|
|
36
|
+
const d = download as Record<string, unknown>;
|
|
37
|
+
if (typeof d.path === "string" && mapping.has(d.path)) {
|
|
38
|
+
d.path = mapping.get(d.path);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { extractErrorCode, formatErrorMessage } from "./enterprise-compat.js";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export type PwAiModule = typeof import("./pw-ai.js");
|
|
5
|
+
|
|
6
|
+
type PwAiLoadMode = "soft" | "strict";
|
|
7
|
+
|
|
8
|
+
let pwAiModuleSoft: Promise<PwAiModule | null> | null = null;
|
|
9
|
+
let pwAiModuleStrict: Promise<PwAiModule | null> | null = null;
|
|
10
|
+
|
|
11
|
+
function isModuleNotFoundError(err: unknown): boolean {
|
|
12
|
+
const code = extractErrorCode(err);
|
|
13
|
+
if (code === "ERR_MODULE_NOT_FOUND") {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
const msg = formatErrorMessage(err);
|
|
17
|
+
return (
|
|
18
|
+
msg.includes("Cannot find module") ||
|
|
19
|
+
msg.includes("Cannot find package") ||
|
|
20
|
+
msg.includes("Failed to resolve import") ||
|
|
21
|
+
msg.includes("Failed to resolve entry for package") ||
|
|
22
|
+
msg.includes("Failed to load url")
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function loadPwAiModule(mode: PwAiLoadMode): Promise<PwAiModule | null> {
|
|
27
|
+
try {
|
|
28
|
+
return await import("./pw-ai.js");
|
|
29
|
+
} catch (err) {
|
|
30
|
+
if (mode === "soft") {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (isModuleNotFoundError(err)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function getPwAiModule(opts?: { mode?: PwAiLoadMode }): Promise<PwAiModule | null> {
|
|
41
|
+
const mode: PwAiLoadMode = opts?.mode ?? "soft";
|
|
42
|
+
if (mode === "soft") {
|
|
43
|
+
if (!pwAiModuleSoft) {
|
|
44
|
+
pwAiModuleSoft = loadPwAiModule("soft");
|
|
45
|
+
}
|
|
46
|
+
return await pwAiModuleSoft;
|
|
47
|
+
}
|
|
48
|
+
if (!pwAiModuleStrict) {
|
|
49
|
+
pwAiModuleStrict = loadPwAiModule("strict");
|
|
50
|
+
}
|
|
51
|
+
return await pwAiModuleStrict;
|
|
52
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { markPwAiLoaded } from "./pw-ai-state.js";
|
|
2
|
+
|
|
3
|
+
markPwAiLoaded();
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
type BrowserConsoleMessage,
|
|
7
|
+
closePageByTargetIdViaPlaywright,
|
|
8
|
+
closePlaywrightBrowserConnection,
|
|
9
|
+
createPageViaPlaywright,
|
|
10
|
+
ensurePageState,
|
|
11
|
+
forceDisconnectPlaywrightForTarget,
|
|
12
|
+
focusPageByTargetIdViaPlaywright,
|
|
13
|
+
getPageForTargetId,
|
|
14
|
+
listPagesViaPlaywright,
|
|
15
|
+
refLocator,
|
|
16
|
+
type WithSnapshotForAI,
|
|
17
|
+
} from "./pw-session.js";
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
armDialogViaPlaywright,
|
|
21
|
+
armFileUploadViaPlaywright,
|
|
22
|
+
clickViaPlaywright,
|
|
23
|
+
closePageViaPlaywright,
|
|
24
|
+
cookiesClearViaPlaywright,
|
|
25
|
+
cookiesGetViaPlaywright,
|
|
26
|
+
cookiesSetViaPlaywright,
|
|
27
|
+
downloadViaPlaywright,
|
|
28
|
+
dragViaPlaywright,
|
|
29
|
+
emulateMediaViaPlaywright,
|
|
30
|
+
evaluateViaPlaywright,
|
|
31
|
+
fillFormViaPlaywright,
|
|
32
|
+
getConsoleMessagesViaPlaywright,
|
|
33
|
+
getNetworkRequestsViaPlaywright,
|
|
34
|
+
getPageErrorsViaPlaywright,
|
|
35
|
+
highlightViaPlaywright,
|
|
36
|
+
hoverViaPlaywright,
|
|
37
|
+
navigateViaPlaywright,
|
|
38
|
+
pdfViaPlaywright,
|
|
39
|
+
pressKeyViaPlaywright,
|
|
40
|
+
resizeViewportViaPlaywright,
|
|
41
|
+
responseBodyViaPlaywright,
|
|
42
|
+
scrollIntoViewViaPlaywright,
|
|
43
|
+
selectOptionViaPlaywright,
|
|
44
|
+
setDeviceViaPlaywright,
|
|
45
|
+
setExtraHTTPHeadersViaPlaywright,
|
|
46
|
+
setGeolocationViaPlaywright,
|
|
47
|
+
setHttpCredentialsViaPlaywright,
|
|
48
|
+
setInputFilesViaPlaywright,
|
|
49
|
+
setLocaleViaPlaywright,
|
|
50
|
+
setOfflineViaPlaywright,
|
|
51
|
+
setTimezoneViaPlaywright,
|
|
52
|
+
snapshotAiViaPlaywright,
|
|
53
|
+
snapshotAriaViaPlaywright,
|
|
54
|
+
snapshotRoleViaPlaywright,
|
|
55
|
+
screenshotWithLabelsViaPlaywright,
|
|
56
|
+
storageClearViaPlaywright,
|
|
57
|
+
storageGetViaPlaywright,
|
|
58
|
+
storageSetViaPlaywright,
|
|
59
|
+
takeScreenshotViaPlaywright,
|
|
60
|
+
traceStartViaPlaywright,
|
|
61
|
+
traceStopViaPlaywright,
|
|
62
|
+
typeViaPlaywright,
|
|
63
|
+
waitForDownloadViaPlaywright,
|
|
64
|
+
waitForViaPlaywright,
|
|
65
|
+
} from "./pw-tools-core.js";
|