feat(browser): add remote-capable profiles
Co-authored-by: James Groat <james@groat.com>
This commit is contained in:
@@ -1,24 +1,36 @@
|
||||
import type { BrowserConfig } from "../config/config.js";
|
||||
import type { BrowserConfig, BrowserProfileConfig } from "../config/config.js";
|
||||
import {
|
||||
DEFAULT_CLAWD_BROWSER_COLOR,
|
||||
DEFAULT_CLAWD_BROWSER_CONTROL_URL,
|
||||
DEFAULT_CLAWD_BROWSER_ENABLED,
|
||||
DEFAULT_CLAWD_BROWSER_PROFILE_NAME,
|
||||
} from "./constants.js";
|
||||
import { CDP_PORT_RANGE_START } from "./profiles.js";
|
||||
|
||||
export type ResolvedBrowserConfig = {
|
||||
enabled: boolean;
|
||||
controlUrl: string;
|
||||
controlHost: string;
|
||||
controlPort: number;
|
||||
cdpUrl: string;
|
||||
cdpProtocol: "http" | "https";
|
||||
cdpHost: string;
|
||||
cdpPort: number;
|
||||
cdpIsLoopback: boolean;
|
||||
color: string;
|
||||
executablePath?: string;
|
||||
headless: boolean;
|
||||
noSandbox: boolean;
|
||||
attachOnly: boolean;
|
||||
defaultProfile: string;
|
||||
profiles: Record<string, BrowserProfileConfig>;
|
||||
};
|
||||
|
||||
export type ResolvedBrowserProfile = {
|
||||
name: string;
|
||||
cdpPort: number;
|
||||
cdpUrl: string;
|
||||
cdpHost: string;
|
||||
cdpIsLoopback: boolean;
|
||||
color: string;
|
||||
};
|
||||
|
||||
function isLoopbackHost(host: string) {
|
||||
@@ -42,7 +54,7 @@ function normalizeHexColor(raw: string | undefined) {
|
||||
return normalized.toUpperCase();
|
||||
}
|
||||
|
||||
function parseHttpUrl(raw: string, label: string) {
|
||||
export function parseHttpUrl(raw: string, label: string) {
|
||||
const trimmed = raw.trim();
|
||||
const parsed = new URL(trimmed);
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
@@ -69,6 +81,24 @@ function parseHttpUrl(raw: string, label: string) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the default "clawd" profile exists in the profiles map.
|
||||
* Auto-creates it with the legacy CDP port (from browser.cdpUrl) or first port if missing.
|
||||
*/
|
||||
function ensureDefaultProfile(
|
||||
profiles: Record<string, BrowserProfileConfig> | undefined,
|
||||
defaultColor: string,
|
||||
legacyCdpPort?: number,
|
||||
): Record<string, BrowserProfileConfig> {
|
||||
const result = { ...profiles };
|
||||
if (!result[DEFAULT_CLAWD_BROWSER_PROFILE_NAME]) {
|
||||
result[DEFAULT_CLAWD_BROWSER_PROFILE_NAME] = {
|
||||
cdpPort: legacyCdpPort ?? CDP_PORT_RANGE_START,
|
||||
color: defaultColor,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
export function resolveBrowserConfig(
|
||||
cfg: BrowserConfig | undefined,
|
||||
): ResolvedBrowserConfig {
|
||||
@@ -78,6 +108,7 @@ export function resolveBrowserConfig(
|
||||
"browser.controlUrl",
|
||||
);
|
||||
const controlPort = controlInfo.port;
|
||||
const defaultColor = normalizeHexColor(cfg?.color);
|
||||
|
||||
const rawCdpUrl = (cfg?.cdpUrl ?? "").trim();
|
||||
let cdpInfo:
|
||||
@@ -105,26 +136,77 @@ export function resolveBrowserConfig(
|
||||
};
|
||||
}
|
||||
|
||||
const cdpPort = cdpInfo.port;
|
||||
const headless = cfg?.headless === true;
|
||||
const noSandbox = cfg?.noSandbox === true;
|
||||
const attachOnly = cfg?.attachOnly === true;
|
||||
const executablePath = cfg?.executablePath?.trim() || undefined;
|
||||
|
||||
const defaultProfile =
|
||||
cfg?.defaultProfile ?? DEFAULT_CLAWD_BROWSER_PROFILE_NAME;
|
||||
// Use legacy cdpUrl port for backward compatibility when no profiles configured
|
||||
const legacyCdpPort = rawCdpUrl ? cdpInfo.port : undefined;
|
||||
const profiles = ensureDefaultProfile(
|
||||
cfg?.profiles,
|
||||
defaultColor,
|
||||
legacyCdpPort,
|
||||
);
|
||||
const cdpProtocol = cdpInfo.parsed.protocol === "https:" ? "https" : "http";
|
||||
|
||||
return {
|
||||
enabled,
|
||||
controlUrl: controlInfo.normalized,
|
||||
controlHost: controlInfo.parsed.hostname,
|
||||
controlPort,
|
||||
cdpUrl: cdpInfo.normalized,
|
||||
cdpProtocol,
|
||||
cdpHost: cdpInfo.parsed.hostname,
|
||||
cdpPort,
|
||||
cdpIsLoopback: isLoopbackHost(cdpInfo.parsed.hostname),
|
||||
color: normalizeHexColor(cfg?.color),
|
||||
color: defaultColor,
|
||||
executablePath,
|
||||
headless,
|
||||
noSandbox,
|
||||
attachOnly,
|
||||
defaultProfile,
|
||||
profiles,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a profile by name from the config.
|
||||
* Returns null if the profile doesn't exist.
|
||||
*/
|
||||
export function resolveProfile(
|
||||
resolved: ResolvedBrowserConfig,
|
||||
profileName: string,
|
||||
): ResolvedBrowserProfile | null {
|
||||
const profile = resolved.profiles[profileName];
|
||||
if (!profile) return null;
|
||||
|
||||
const rawProfileUrl = profile.cdpUrl?.trim() ?? "";
|
||||
let cdpHost = resolved.cdpHost;
|
||||
let cdpPort = profile.cdpPort ?? 0;
|
||||
let cdpUrl = "";
|
||||
|
||||
if (rawProfileUrl) {
|
||||
const parsed = parseHttpUrl(
|
||||
rawProfileUrl,
|
||||
`browser.profiles.${profileName}.cdpUrl`,
|
||||
);
|
||||
cdpHost = parsed.parsed.hostname;
|
||||
cdpPort = parsed.port;
|
||||
cdpUrl = parsed.normalized;
|
||||
} else if (cdpPort) {
|
||||
cdpUrl = `${resolved.cdpProtocol}://${resolved.cdpHost}:${cdpPort}`;
|
||||
} else {
|
||||
throw new Error(`Profile "${profileName}" must define cdpPort or cdpUrl.`);
|
||||
}
|
||||
|
||||
return {
|
||||
name: profileName,
|
||||
cdpPort,
|
||||
cdpUrl,
|
||||
cdpHost,
|
||||
cdpIsLoopback: isLoopbackHost(cdpHost),
|
||||
color: profile.color,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user