mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:20:43 +00:00
fix(browser): configure Chrome MCP existing-session launch (#71560)
This commit is contained in:
@@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Browser/CDP: make readiness diagnostics use the same discovery-first fallback as reachability for bare `ws://` Browserless and Browserbase CDP URLs. Fixes #69532.
|
||||
- ACP/OpenCode: update the bundled acpx runtime to 0.6.0 and cover the OpenCode ACP bind path in Docker live tests.
|
||||
- Browser/existing-session: support per-profile Chrome MCP command/args, map `cdpUrl` to `--browserUrl` or `--wsEndpoint`, and avoid combining endpoint flags with `--userDataDir`. Fixes #47879, #48037, and #62706. Thanks @puneet1409, @zhehao, and @madkow1001.
|
||||
- Memory-host SDK: use trusted env-proxy mode for remote embedding and batch HTTP calls only when Undici will proxy that target, preserving SSRF DNS pinning for `ALL_PROXY`-only and `NO_PROXY` bypass cases. Fixes #52162. (#71506) Thanks @DhtIsCoding.
|
||||
- Gateway/dashboard: render Control UI and WebSocket links with `https://`/`wss://` when `gateway.tls.enabled=true`, including `openclaw gateway status`. Fixes #71494. (#71499) Thanks @deepkilo.
|
||||
- Agents/OpenAI-compatible: default proxy/local completions tool requests to `tool_choice: "auto"` when tools are present, so providers enter native tool-calling mode instead of replying with plain-text tool directives. (#71472) Thanks @Speed-maker.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
445663bd6907368befbfd76f6fcc58f9dc282244697f44e9860391e51e6f2f83 config-baseline.json
|
||||
f54f808dc85123a5ba788618a6dff7f2c869ced639dd0db34a86802985730dc6 config-baseline.core.json
|
||||
9a012a9c87b9010683289dc7d68ba5446a4b78beedf381e2c5f9d486f25a9213 config-baseline.json
|
||||
6128d6eff8c28d17194d1ae9ee7f72abae48da1c6476ab16e6378f1898e4373a config-baseline.core.json
|
||||
7cd9c908f066c143eab2a201efbc9640f483ab28bba92ddeca1d18cc2b528bc3 config-baseline.channel.json
|
||||
7825b56a5b3fcdbe2e09ef8fe5d9f12ac3598435afebe20413051e45b0d1968e config-baseline.plugin.json
|
||||
|
||||
@@ -139,6 +139,68 @@ describe("chrome MCP page parsing", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses browserUrl for existing-session cdpUrl without also passing userDataDir", () => {
|
||||
expect(
|
||||
buildChromeMcpArgs({
|
||||
cdpUrl: "http://127.0.0.1:9222",
|
||||
userDataDir: "/tmp/brave-profile",
|
||||
}),
|
||||
).toEqual([
|
||||
"-y",
|
||||
"chrome-devtools-mcp@latest",
|
||||
"--browserUrl",
|
||||
"http://127.0.0.1:9222",
|
||||
"--experimentalStructuredContent",
|
||||
"--experimental-page-id-routing",
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses wsEndpoint for direct existing-session websocket cdpUrl", () => {
|
||||
expect(
|
||||
buildChromeMcpArgs({
|
||||
cdpUrl: "ws://127.0.0.1:9222/devtools/browser/abc",
|
||||
}),
|
||||
).toEqual([
|
||||
"-y",
|
||||
"chrome-devtools-mcp@latest",
|
||||
"--wsEndpoint",
|
||||
"ws://127.0.0.1:9222/devtools/browser/abc",
|
||||
"--experimentalStructuredContent",
|
||||
"--experimental-page-id-routing",
|
||||
]);
|
||||
});
|
||||
|
||||
it("appends custom Chrome MCP args and lets explicit endpoint args override auto-connect", () => {
|
||||
expect(
|
||||
buildChromeMcpArgs({
|
||||
userDataDir: "/tmp/brave-profile",
|
||||
mcpArgs: ["--browserUrl", "http://127.0.0.1:9222", "--no-usage-statistics"],
|
||||
}),
|
||||
).toEqual([
|
||||
"-y",
|
||||
"chrome-devtools-mcp@latest",
|
||||
"--experimentalStructuredContent",
|
||||
"--experimental-page-id-routing",
|
||||
"--browserUrl",
|
||||
"http://127.0.0.1:9222",
|
||||
"--no-usage-statistics",
|
||||
]);
|
||||
});
|
||||
|
||||
it("omits the npx package prefix for a custom Chrome MCP command", () => {
|
||||
expect(
|
||||
buildChromeMcpArgs({
|
||||
mcpCommand: "/usr/local/bin/chrome-devtools-mcp",
|
||||
cdpUrl: "http://127.0.0.1:9222",
|
||||
}),
|
||||
).toEqual([
|
||||
"--browserUrl",
|
||||
"http://127.0.0.1:9222",
|
||||
"--experimentalStructuredContent",
|
||||
"--experimental-page-id-routing",
|
||||
]);
|
||||
});
|
||||
|
||||
it("parses new_page text responses and returns the created tab", async () => {
|
||||
const factory: ChromeMcpSessionFactory = async () => createFakeSession();
|
||||
setChromeMcpSessionFactoryForTest(factory);
|
||||
@@ -435,8 +497,8 @@ describe("chrome MCP page parsing", () => {
|
||||
const createdSessions: ChromeMcpSession[] = [];
|
||||
const closeMocks: Array<ReturnType<typeof vi.fn>> = [];
|
||||
const factoryCalls: Array<{ profileName: string; userDataDir?: string }> = [];
|
||||
const factory: ChromeMcpSessionFactory = async (profileName, userDataDir) => {
|
||||
factoryCalls.push({ profileName, userDataDir });
|
||||
const factory: ChromeMcpSessionFactory = async (profileName, options) => {
|
||||
factoryCalls.push({ profileName, userDataDir: options?.userDataDir });
|
||||
const session = createFakeSession();
|
||||
const closeMock = vi.fn().mockResolvedValue(undefined);
|
||||
session.client.close = closeMock as typeof session.client.close;
|
||||
|
||||
@@ -37,6 +37,21 @@ type ChromeMcpCallOptions = {
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
||||
export type ChromeMcpProfileOptions = {
|
||||
userDataDir?: string;
|
||||
cdpUrl?: string;
|
||||
mcpCommand?: string;
|
||||
mcpArgs?: string[];
|
||||
};
|
||||
|
||||
type NormalizedChromeMcpProfileOptions = {
|
||||
userDataDir?: string;
|
||||
browserUrl?: string;
|
||||
command: string;
|
||||
extraArgs: string[];
|
||||
};
|
||||
type ChromeMcpOptionsInput = string | ChromeMcpProfileOptions | NormalizedChromeMcpProfileOptions;
|
||||
|
||||
type ChromeMcpSessionLease = {
|
||||
session: ChromeMcpSession;
|
||||
cacheKey: string;
|
||||
@@ -45,18 +60,26 @@ type ChromeMcpSessionLease = {
|
||||
|
||||
type ChromeMcpSessionFactory = (
|
||||
profileName: string,
|
||||
userDataDir?: string,
|
||||
options?: NormalizedChromeMcpProfileOptions,
|
||||
) => Promise<ChromeMcpSession>;
|
||||
|
||||
const DEFAULT_CHROME_MCP_COMMAND = "npx";
|
||||
const DEFAULT_CHROME_MCP_ARGS = [
|
||||
"-y",
|
||||
"chrome-devtools-mcp@latest",
|
||||
"--autoConnect",
|
||||
const DEFAULT_CHROME_MCP_PACKAGE_ARGS = ["-y", "chrome-devtools-mcp@latest"];
|
||||
const DEFAULT_CHROME_MCP_FEATURE_ARGS = [
|
||||
// Direct chrome-devtools-mcp launches do not enable structuredContent by default.
|
||||
"--experimentalStructuredContent",
|
||||
"--experimental-page-id-routing",
|
||||
];
|
||||
const CHROME_MCP_CONNECTION_FLAGS = new Set([
|
||||
"--autoConnect",
|
||||
"--auto-connect",
|
||||
"--browserUrl",
|
||||
"--browser-url",
|
||||
"--wsEndpoint",
|
||||
"--ws-endpoint",
|
||||
"-w",
|
||||
]);
|
||||
const CHROME_MCP_USER_DATA_DIR_FLAGS = new Set(["--userDataDir", "--user-data-dir"]);
|
||||
const CHROME_MCP_NEW_PAGE_TIMEOUT_MS = 5_000;
|
||||
const CHROME_MCP_NAVIGATE_TIMEOUT_MS = 20_000;
|
||||
const CHROME_MCP_HANDSHAKE_TIMEOUT_MS = 30_000;
|
||||
@@ -197,8 +220,83 @@ function normalizeChromeMcpUserDataDir(userDataDir?: string): string | undefined
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function buildChromeMcpSessionCacheKey(profileName: string, userDataDir?: string): string {
|
||||
return JSON.stringify([profileName, normalizeChromeMcpUserDataDir(userDataDir) ?? ""]);
|
||||
function normalizeChromeMcpStringList(values?: string[]): string[] {
|
||||
return Array.isArray(values)
|
||||
? values.filter(
|
||||
(value): value is string => typeof value === "string" && value.trim().length > 0,
|
||||
)
|
||||
: [];
|
||||
}
|
||||
|
||||
function normalizeChromeMcpOptions(
|
||||
input?: ChromeMcpOptionsInput,
|
||||
): NormalizedChromeMcpProfileOptions {
|
||||
if (typeof input === "object" && input && "command" in input && "extraArgs" in input) {
|
||||
return input;
|
||||
}
|
||||
const options = typeof input === "string" ? { userDataDir: input } : (input ?? {});
|
||||
const command = normalizeOptionalString(options.mcpCommand) ?? DEFAULT_CHROME_MCP_COMMAND;
|
||||
return {
|
||||
command,
|
||||
userDataDir: normalizeChromeMcpUserDataDir(options.userDataDir),
|
||||
browserUrl: normalizeOptionalString(options.cdpUrl),
|
||||
extraArgs: normalizeChromeMcpStringList(options.mcpArgs),
|
||||
};
|
||||
}
|
||||
|
||||
function hasFlag(args: string[], flags: Set<string>): boolean {
|
||||
return args.some((arg) => {
|
||||
const [name] = arg.split("=", 1);
|
||||
return flags.has(name ?? arg);
|
||||
});
|
||||
}
|
||||
|
||||
function isChromeMcpWebSocketEndpoint(url: string): boolean {
|
||||
return /^wss?:\/\//i.test(url);
|
||||
}
|
||||
|
||||
function buildChromeMcpConnectionArgs(options: NormalizedChromeMcpProfileOptions): string[] {
|
||||
if (hasFlag(options.extraArgs, CHROME_MCP_CONNECTION_FLAGS)) {
|
||||
return [];
|
||||
}
|
||||
if (options.browserUrl) {
|
||||
return isChromeMcpWebSocketEndpoint(options.browserUrl)
|
||||
? ["--wsEndpoint", options.browserUrl]
|
||||
: ["--browserUrl", options.browserUrl];
|
||||
}
|
||||
return ["--autoConnect"];
|
||||
}
|
||||
|
||||
function buildChromeMcpUserDataDirArgs(options: NormalizedChromeMcpProfileOptions): string[] {
|
||||
if (
|
||||
!options.userDataDir ||
|
||||
options.browserUrl ||
|
||||
hasFlag(options.extraArgs, CHROME_MCP_CONNECTION_FLAGS) ||
|
||||
hasFlag(options.extraArgs, CHROME_MCP_USER_DATA_DIR_FLAGS)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
return ["--userDataDir", options.userDataDir];
|
||||
}
|
||||
|
||||
function buildChromeMcpSessionCacheKey(
|
||||
profileName: string,
|
||||
options: NormalizedChromeMcpProfileOptions,
|
||||
): string {
|
||||
return JSON.stringify([
|
||||
profileName,
|
||||
options.userDataDir ?? "",
|
||||
options.browserUrl ?? "",
|
||||
options.command,
|
||||
options.extraArgs,
|
||||
]);
|
||||
}
|
||||
|
||||
function chromeMcpProfileOptionsFromParams(params: {
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
}): string | ChromeMcpProfileOptions | undefined {
|
||||
return params.profile ?? params.userDataDir;
|
||||
}
|
||||
|
||||
function cacheKeyMatchesProfileName(cacheKey: string, profileName: string): boolean {
|
||||
@@ -234,11 +332,20 @@ async function closeChromeMcpSessionsForProfile(
|
||||
return closed;
|
||||
}
|
||||
|
||||
export function buildChromeMcpArgs(userDataDir?: string): string[] {
|
||||
const normalizedUserDataDir = normalizeChromeMcpUserDataDir(userDataDir);
|
||||
return normalizedUserDataDir
|
||||
? [...DEFAULT_CHROME_MCP_ARGS, "--userDataDir", normalizedUserDataDir]
|
||||
: [...DEFAULT_CHROME_MCP_ARGS];
|
||||
function buildChromeMcpArgsFromOptions(options: NormalizedChromeMcpProfileOptions): string[] {
|
||||
const commandPrefix =
|
||||
options.command === DEFAULT_CHROME_MCP_COMMAND ? DEFAULT_CHROME_MCP_PACKAGE_ARGS : [];
|
||||
return [
|
||||
...commandPrefix,
|
||||
...buildChromeMcpConnectionArgs(options),
|
||||
...DEFAULT_CHROME_MCP_FEATURE_ARGS,
|
||||
...buildChromeMcpUserDataDirArgs(options),
|
||||
...options.extraArgs,
|
||||
];
|
||||
}
|
||||
|
||||
export function buildChromeMcpArgs(input?: string | ChromeMcpProfileOptions): string[] {
|
||||
return buildChromeMcpArgsFromOptions(normalizeChromeMcpOptions(input));
|
||||
}
|
||||
|
||||
function drainStderr(transport: StdioClientTransport): () => string {
|
||||
@@ -289,11 +396,11 @@ async function withChromeMcpHandshakeTimeout<T>(task: Promise<T>): Promise<T> {
|
||||
|
||||
async function createRealSession(
|
||||
profileName: string,
|
||||
userDataDir?: string,
|
||||
options: NormalizedChromeMcpProfileOptions = normalizeChromeMcpOptions(),
|
||||
): Promise<ChromeMcpSession> {
|
||||
const transport = new StdioClientTransport({
|
||||
command: DEFAULT_CHROME_MCP_COMMAND,
|
||||
args: buildChromeMcpArgs(userDataDir),
|
||||
command: options.command,
|
||||
args: buildChromeMcpArgsFromOptions(options),
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client(
|
||||
@@ -325,9 +432,11 @@ async function createRealSession(
|
||||
`Chrome MCP attach failed for profile "${profileName}". Subprocess stderr:\n${stderr}`,
|
||||
);
|
||||
}
|
||||
const targetLabel = userDataDir
|
||||
? `the configured Chromium user data dir (${userDataDir})`
|
||||
: "Google Chrome's default profile";
|
||||
const targetLabel = options.browserUrl
|
||||
? `the configured Chrome endpoint (${options.browserUrl})`
|
||||
: options.userDataDir
|
||||
? `the configured Chromium user data dir (${options.userDataDir})`
|
||||
: "Google Chrome's default profile";
|
||||
throw new BrowserProfileUnavailableError(
|
||||
`Chrome MCP existing-session attach failed for profile "${profileName}". ` +
|
||||
`Make sure ${targetLabel} is running locally with remote debugging enabled. ` +
|
||||
@@ -377,10 +486,11 @@ async function waitForChromeMcpReady(
|
||||
|
||||
async function getSession(
|
||||
profileName: string,
|
||||
userDataDir?: string,
|
||||
profileOptions?: ChromeMcpOptionsInput,
|
||||
timeoutMs?: number,
|
||||
): Promise<ChromeMcpSession> {
|
||||
const cacheKey = buildChromeMcpSessionCacheKey(profileName, userDataDir);
|
||||
const options = normalizeChromeMcpOptions(profileOptions);
|
||||
const cacheKey = buildChromeMcpSessionCacheKey(profileName, options);
|
||||
await closeChromeMcpSessionsForProfile(profileName, cacheKey);
|
||||
|
||||
let session = sessions.get(cacheKey);
|
||||
@@ -392,7 +502,7 @@ async function getSession(
|
||||
let pending = pendingSessions.get(cacheKey);
|
||||
if (!pending) {
|
||||
pending = (async () => {
|
||||
const created = await (sessionFactory ?? createRealSession)(profileName, userDataDir);
|
||||
const created = await (sessionFactory ?? createRealSession)(profileName, options);
|
||||
if (pendingSessions.get(cacheKey) === pending) {
|
||||
sessions.set(cacheKey, created);
|
||||
} else {
|
||||
@@ -465,10 +575,11 @@ async function getExistingSession(
|
||||
|
||||
async function createEphemeralSession(
|
||||
profileName: string,
|
||||
userDataDir?: string,
|
||||
profileOptions?: ChromeMcpOptionsInput,
|
||||
timeoutMs?: number,
|
||||
): Promise<ChromeMcpSession> {
|
||||
const session = await (sessionFactory ?? createRealSession)(profileName, userDataDir);
|
||||
const options = normalizeChromeMcpOptions(profileOptions);
|
||||
const session = await (sessionFactory ?? createRealSession)(profileName, options);
|
||||
try {
|
||||
await waitForChromeMcpReady(session, profileName, timeoutMs);
|
||||
return session;
|
||||
@@ -480,13 +591,14 @@ async function createEphemeralSession(
|
||||
|
||||
async function leaseSession(
|
||||
profileName: string,
|
||||
userDataDir?: string,
|
||||
profileOptions?: ChromeMcpOptionsInput,
|
||||
options: ChromeMcpCallOptions = {},
|
||||
): Promise<ChromeMcpSessionLease> {
|
||||
const cacheKey = buildChromeMcpSessionCacheKey(profileName, userDataDir);
|
||||
const normalizedProfileOptions = normalizeChromeMcpOptions(profileOptions);
|
||||
const cacheKey = buildChromeMcpSessionCacheKey(profileName, normalizedProfileOptions);
|
||||
if (!options.ephemeral) {
|
||||
return {
|
||||
session: await getSession(profileName, userDataDir, options.timeoutMs),
|
||||
session: await getSession(profileName, normalizedProfileOptions, options.timeoutMs),
|
||||
cacheKey,
|
||||
temporary: false,
|
||||
};
|
||||
@@ -504,7 +616,7 @@ async function leaseSession(
|
||||
}
|
||||
|
||||
return {
|
||||
session: await createEphemeralSession(profileName, userDataDir, options.timeoutMs),
|
||||
session: await createEphemeralSession(profileName, normalizedProfileOptions, options.timeoutMs),
|
||||
cacheKey,
|
||||
temporary: true,
|
||||
};
|
||||
@@ -512,7 +624,7 @@ async function leaseSession(
|
||||
|
||||
async function callTool(
|
||||
profileName: string,
|
||||
userDataDir: string | undefined,
|
||||
profileOptions: ChromeMcpOptionsInput | undefined,
|
||||
name: string,
|
||||
args: Record<string, unknown> = {},
|
||||
options: ChromeMcpCallOptions = {},
|
||||
@@ -524,7 +636,7 @@ async function callTool(
|
||||
}
|
||||
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
const lease = await leaseSession(profileName, userDataDir, options);
|
||||
const lease = await leaseSession(profileName, profileOptions, options);
|
||||
const rawCall = lease.session.client.callTool({
|
||||
name,
|
||||
arguments: args,
|
||||
@@ -620,9 +732,9 @@ async function withTempFile<T>(fn: (filePath: string) => Promise<T>): Promise<T>
|
||||
async function findPageById(
|
||||
profileName: string,
|
||||
pageId: number,
|
||||
userDataDir?: string,
|
||||
profileOptions?: string | ChromeMcpProfileOptions,
|
||||
): Promise<ChromeMcpStructuredPage> {
|
||||
const pages = await listChromeMcpPages(profileName, userDataDir);
|
||||
const pages = await listChromeMcpPages(profileName, profileOptions);
|
||||
const page = pages.find((entry) => entry.id === pageId);
|
||||
if (!page) {
|
||||
throw new BrowserTabNotFoundError();
|
||||
@@ -632,10 +744,10 @@ async function findPageById(
|
||||
|
||||
export async function ensureChromeMcpAvailable(
|
||||
profileName: string,
|
||||
userDataDir?: string,
|
||||
profileOptions?: string | ChromeMcpProfileOptions,
|
||||
options: ChromeMcpCallOptions = {},
|
||||
): Promise<void> {
|
||||
const lease = await leaseSession(profileName, userDataDir, options);
|
||||
const lease = await leaseSession(profileName, profileOptions, options);
|
||||
if (lease.temporary) {
|
||||
await lease.session.client.close().catch(() => {});
|
||||
}
|
||||
@@ -663,28 +775,28 @@ export async function stopAllChromeMcpSessions(): Promise<void> {
|
||||
|
||||
export async function listChromeMcpPages(
|
||||
profileName: string,
|
||||
userDataDir?: string,
|
||||
profileOptions?: string | ChromeMcpProfileOptions,
|
||||
options: ChromeMcpCallOptions = {},
|
||||
): Promise<ChromeMcpStructuredPage[]> {
|
||||
const result = await callTool(profileName, userDataDir, "list_pages", {}, options);
|
||||
const result = await callTool(profileName, profileOptions, "list_pages", {}, options);
|
||||
return extractStructuredPages(result);
|
||||
}
|
||||
|
||||
export async function listChromeMcpTabs(
|
||||
profileName: string,
|
||||
userDataDir?: string,
|
||||
profileOptions?: string | ChromeMcpProfileOptions,
|
||||
options: ChromeMcpCallOptions = {},
|
||||
): Promise<BrowserTab[]> {
|
||||
return toBrowserTabs(await listChromeMcpPages(profileName, userDataDir, options));
|
||||
return toBrowserTabs(await listChromeMcpPages(profileName, profileOptions, options));
|
||||
}
|
||||
|
||||
export async function openChromeMcpTab(
|
||||
profileName: string,
|
||||
url: string,
|
||||
userDataDir?: string,
|
||||
profileOptions?: string | ChromeMcpProfileOptions,
|
||||
): Promise<BrowserTab> {
|
||||
const targetUrl = url.trim() || "about:blank";
|
||||
const result = await callTool(profileName, userDataDir, "new_page", {
|
||||
const result = await callTool(profileName, profileOptions, "new_page", {
|
||||
url: "about:blank",
|
||||
timeout: CHROME_MCP_NEW_PAGE_TIMEOUT_MS,
|
||||
});
|
||||
@@ -700,7 +812,8 @@ export async function openChromeMcpTab(
|
||||
: (
|
||||
await navigateChromeMcpPage({
|
||||
profileName,
|
||||
userDataDir,
|
||||
profile: typeof profileOptions === "string" ? undefined : profileOptions,
|
||||
userDataDir: typeof profileOptions === "string" ? profileOptions : undefined,
|
||||
targetId,
|
||||
url: targetUrl,
|
||||
timeoutMs: CHROME_MCP_NAVIGATE_TIMEOUT_MS,
|
||||
@@ -717,9 +830,9 @@ export async function openChromeMcpTab(
|
||||
export async function focusChromeMcpTab(
|
||||
profileName: string,
|
||||
targetId: string,
|
||||
userDataDir?: string,
|
||||
profileOptions?: string | ChromeMcpProfileOptions,
|
||||
): Promise<void> {
|
||||
await callTool(profileName, userDataDir, "select_page", {
|
||||
await callTool(profileName, profileOptions, "select_page", {
|
||||
pageId: parsePageId(targetId),
|
||||
bringToFront: true,
|
||||
});
|
||||
@@ -728,13 +841,14 @@ export async function focusChromeMcpTab(
|
||||
export async function closeChromeMcpTab(
|
||||
profileName: string,
|
||||
targetId: string,
|
||||
userDataDir?: string,
|
||||
profileOptions?: string | ChromeMcpProfileOptions,
|
||||
): Promise<void> {
|
||||
await callTool(profileName, userDataDir, "close_page", { pageId: parsePageId(targetId) });
|
||||
await callTool(profileName, profileOptions, "close_page", { pageId: parsePageId(targetId) });
|
||||
}
|
||||
|
||||
export async function navigateChromeMcpPage(params: {
|
||||
profileName: string;
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
url: string;
|
||||
@@ -743,7 +857,7 @@ export async function navigateChromeMcpPage(params: {
|
||||
const resolvedTimeoutMs = params.timeoutMs ?? CHROME_MCP_NAVIGATE_TIMEOUT_MS;
|
||||
await callTool(
|
||||
params.profileName,
|
||||
params.userDataDir,
|
||||
chromeMcpProfileOptionsFromParams(params),
|
||||
"navigate_page",
|
||||
{
|
||||
pageId: parsePageId(params.targetId),
|
||||
@@ -756,24 +870,31 @@ export async function navigateChromeMcpPage(params: {
|
||||
const page = await findPageById(
|
||||
params.profileName,
|
||||
parsePageId(params.targetId),
|
||||
params.userDataDir,
|
||||
chromeMcpProfileOptionsFromParams(params),
|
||||
);
|
||||
return { url: page.url ?? params.url };
|
||||
}
|
||||
|
||||
export async function takeChromeMcpSnapshot(params: {
|
||||
profileName: string;
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
}): Promise<ChromeMcpSnapshotNode> {
|
||||
const result = await callTool(params.profileName, params.userDataDir, "take_snapshot", {
|
||||
pageId: parsePageId(params.targetId),
|
||||
});
|
||||
const result = await callTool(
|
||||
params.profileName,
|
||||
chromeMcpProfileOptionsFromParams(params),
|
||||
"take_snapshot",
|
||||
{
|
||||
pageId: parsePageId(params.targetId),
|
||||
},
|
||||
);
|
||||
return extractSnapshot(result);
|
||||
}
|
||||
|
||||
export async function takeChromeMcpScreenshot(params: {
|
||||
profileName: string;
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
uid?: string;
|
||||
@@ -784,7 +905,7 @@ export async function takeChromeMcpScreenshot(params: {
|
||||
return await withTempFile(async (filePath) => {
|
||||
await callTool(
|
||||
params.profileName,
|
||||
params.userDataDir,
|
||||
chromeMcpProfileOptionsFromParams(params),
|
||||
"take_screenshot",
|
||||
{
|
||||
pageId: parsePageId(params.targetId),
|
||||
@@ -801,6 +922,7 @@ export async function takeChromeMcpScreenshot(params: {
|
||||
|
||||
export async function clickChromeMcpElement(params: {
|
||||
profileName: string;
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
uid: string;
|
||||
@@ -810,7 +932,7 @@ export async function clickChromeMcpElement(params: {
|
||||
}): Promise<void> {
|
||||
await callTool(
|
||||
params.profileName,
|
||||
params.userDataDir,
|
||||
chromeMcpProfileOptionsFromParams(params),
|
||||
"click",
|
||||
{
|
||||
pageId: parsePageId(params.targetId),
|
||||
@@ -826,6 +948,7 @@ export async function clickChromeMcpElement(params: {
|
||||
|
||||
export async function clickChromeMcpCoords(params: {
|
||||
profileName: string;
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
x: number;
|
||||
@@ -843,6 +966,7 @@ export async function clickChromeMcpCoords(params: {
|
||||
const doubleClick = params.doubleClick ? "true" : "false";
|
||||
await evaluateChromeMcpScript({
|
||||
profileName: params.profileName,
|
||||
profile: params.profile,
|
||||
userDataDir: params.userDataDir,
|
||||
targetId: params.targetId,
|
||||
fn: `async () => {
|
||||
@@ -885,12 +1009,13 @@ export async function clickChromeMcpCoords(params: {
|
||||
|
||||
export async function fillChromeMcpElement(params: {
|
||||
profileName: string;
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
uid: string;
|
||||
value: string;
|
||||
}): Promise<void> {
|
||||
await callTool(params.profileName, params.userDataDir, "fill", {
|
||||
await callTool(params.profileName, chromeMcpProfileOptionsFromParams(params), "fill", {
|
||||
pageId: parsePageId(params.targetId),
|
||||
uid: params.uid,
|
||||
value: params.value,
|
||||
@@ -899,11 +1024,12 @@ export async function fillChromeMcpElement(params: {
|
||||
|
||||
export async function fillChromeMcpForm(params: {
|
||||
profileName: string;
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
elements: Array<{ uid: string; value: string }>;
|
||||
}): Promise<void> {
|
||||
await callTool(params.profileName, params.userDataDir, "fill_form", {
|
||||
await callTool(params.profileName, chromeMcpProfileOptionsFromParams(params), "fill_form", {
|
||||
pageId: parsePageId(params.targetId),
|
||||
elements: params.elements,
|
||||
});
|
||||
@@ -911,11 +1037,12 @@ export async function fillChromeMcpForm(params: {
|
||||
|
||||
export async function hoverChromeMcpElement(params: {
|
||||
profileName: string;
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
uid: string;
|
||||
}): Promise<void> {
|
||||
await callTool(params.profileName, params.userDataDir, "hover", {
|
||||
await callTool(params.profileName, chromeMcpProfileOptionsFromParams(params), "hover", {
|
||||
pageId: parsePageId(params.targetId),
|
||||
uid: params.uid,
|
||||
});
|
||||
@@ -923,12 +1050,13 @@ export async function hoverChromeMcpElement(params: {
|
||||
|
||||
export async function dragChromeMcpElement(params: {
|
||||
profileName: string;
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
fromUid: string;
|
||||
toUid: string;
|
||||
}): Promise<void> {
|
||||
await callTool(params.profileName, params.userDataDir, "drag", {
|
||||
await callTool(params.profileName, chromeMcpProfileOptionsFromParams(params), "drag", {
|
||||
pageId: parsePageId(params.targetId),
|
||||
from_uid: params.fromUid,
|
||||
to_uid: params.toUid,
|
||||
@@ -937,12 +1065,13 @@ export async function dragChromeMcpElement(params: {
|
||||
|
||||
export async function uploadChromeMcpFile(params: {
|
||||
profileName: string;
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
uid: string;
|
||||
filePath: string;
|
||||
}): Promise<void> {
|
||||
await callTool(params.profileName, params.userDataDir, "upload_file", {
|
||||
await callTool(params.profileName, chromeMcpProfileOptionsFromParams(params), "upload_file", {
|
||||
pageId: parsePageId(params.targetId),
|
||||
uid: params.uid,
|
||||
filePath: params.filePath,
|
||||
@@ -951,11 +1080,12 @@ export async function uploadChromeMcpFile(params: {
|
||||
|
||||
export async function pressChromeMcpKey(params: {
|
||||
profileName: string;
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
key: string;
|
||||
}): Promise<void> {
|
||||
await callTool(params.profileName, params.userDataDir, "press_key", {
|
||||
await callTool(params.profileName, chromeMcpProfileOptionsFromParams(params), "press_key", {
|
||||
pageId: parsePageId(params.targetId),
|
||||
key: params.key,
|
||||
});
|
||||
@@ -963,12 +1093,13 @@ export async function pressChromeMcpKey(params: {
|
||||
|
||||
export async function resizeChromeMcpPage(params: {
|
||||
profileName: string;
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}): Promise<void> {
|
||||
await callTool(params.profileName, params.userDataDir, "resize_page", {
|
||||
await callTool(params.profileName, chromeMcpProfileOptionsFromParams(params), "resize_page", {
|
||||
pageId: parsePageId(params.targetId),
|
||||
width: params.width,
|
||||
height: params.height,
|
||||
@@ -977,12 +1108,13 @@ export async function resizeChromeMcpPage(params: {
|
||||
|
||||
export async function handleChromeMcpDialog(params: {
|
||||
profileName: string;
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
action: "accept" | "dismiss";
|
||||
promptText?: string;
|
||||
}): Promise<void> {
|
||||
await callTool(params.profileName, params.userDataDir, "handle_dialog", {
|
||||
await callTool(params.profileName, chromeMcpProfileOptionsFromParams(params), "handle_dialog", {
|
||||
pageId: parsePageId(params.targetId),
|
||||
action: params.action,
|
||||
...(params.promptText ? { promptText: params.promptText } : {}),
|
||||
@@ -991,27 +1123,34 @@ export async function handleChromeMcpDialog(params: {
|
||||
|
||||
export async function evaluateChromeMcpScript(params: {
|
||||
profileName: string;
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
fn: string;
|
||||
args?: string[];
|
||||
}): Promise<unknown> {
|
||||
const result = await callTool(params.profileName, params.userDataDir, "evaluate_script", {
|
||||
pageId: parsePageId(params.targetId),
|
||||
function: params.fn,
|
||||
...(params.args?.length ? { args: params.args } : {}),
|
||||
});
|
||||
const result = await callTool(
|
||||
params.profileName,
|
||||
chromeMcpProfileOptionsFromParams(params),
|
||||
"evaluate_script",
|
||||
{
|
||||
pageId: parsePageId(params.targetId),
|
||||
function: params.fn,
|
||||
...(params.args?.length ? { args: params.args } : {}),
|
||||
},
|
||||
);
|
||||
return extractJsonMessage(result);
|
||||
}
|
||||
|
||||
export async function waitForChromeMcpText(params: {
|
||||
profileName: string;
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
text: string[];
|
||||
timeoutMs?: number;
|
||||
}): Promise<void> {
|
||||
await callTool(params.profileName, params.userDataDir, "wait_for", {
|
||||
await callTool(params.profileName, chromeMcpProfileOptionsFromParams(params), "wait_for", {
|
||||
pageId: parsePageId(params.targetId),
|
||||
text: params.text,
|
||||
...(typeof params.timeoutMs === "number" ? { timeout: params.timeoutMs } : {}),
|
||||
|
||||
@@ -735,6 +735,47 @@ describe("browser config", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves Chrome MCP command, args, and endpoint URL for existing-session profiles", () => {
|
||||
const resolved = resolveBrowserConfig({
|
||||
profiles: {
|
||||
"chrome-live": {
|
||||
driver: "existing-session",
|
||||
attachOnly: true,
|
||||
cdpUrl: "http://127.0.0.1:9222/",
|
||||
mcpCommand: " /usr/local/bin/chrome-devtools-mcp ",
|
||||
mcpArgs: ["--no-usage-statistics", " ", "--performanceCrux", "false"],
|
||||
color: "#00AA00",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const profile = resolveProfile(resolved, "chrome-live");
|
||||
expect(profile?.driver).toBe("existing-session");
|
||||
expect(profile?.cdpUrl).toBe("http://127.0.0.1:9222");
|
||||
expect(profile?.cdpHost).toBe("127.0.0.1");
|
||||
expect(profile?.cdpIsLoopback).toBe(true);
|
||||
expect(profile?.mcpCommand).toBe("/usr/local/bin/chrome-devtools-mcp");
|
||||
expect(profile?.mcpArgs).toEqual(["--no-usage-statistics", "--performanceCrux", "false"]);
|
||||
});
|
||||
|
||||
it("preserves direct websocket cdpUrl for existing-session profiles", () => {
|
||||
const resolved = resolveBrowserConfig({
|
||||
profiles: {
|
||||
"chrome-live": {
|
||||
driver: "existing-session",
|
||||
attachOnly: true,
|
||||
cdpUrl: "ws://127.0.0.1:9222/devtools/browser/ABC?token=test-key",
|
||||
color: "#00AA00",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const profile = resolveProfile(resolved, "chrome-live");
|
||||
expect(profile?.cdpUrl).toBe("ws://127.0.0.1:9222/devtools/browser/ABC?token=test-key");
|
||||
expect(profile?.cdpHost).toBe("127.0.0.1");
|
||||
expect(profile?.cdpIsLoopback).toBe(true);
|
||||
});
|
||||
|
||||
it("sets usesChromeMcp only for existing-session profiles", () => {
|
||||
const resolved = resolveBrowserConfig({
|
||||
profiles: {
|
||||
|
||||
@@ -103,6 +103,8 @@ export type ResolvedBrowserProfile = {
|
||||
cdpHost: string;
|
||||
cdpIsLoopback: boolean;
|
||||
userDataDir?: string;
|
||||
mcpCommand?: string;
|
||||
mcpArgs?: string[];
|
||||
color: string;
|
||||
driver: "openclaw" | "existing-session";
|
||||
executablePath?: string;
|
||||
@@ -180,6 +182,37 @@ function normalizeExecutablePath(raw: string | undefined): string | undefined {
|
||||
return path.resolve(value.replace(/^~(?=$|[\\/])/, os.homedir()));
|
||||
}
|
||||
|
||||
function normalizeExistingSessionCdpUrl(
|
||||
raw: string | undefined,
|
||||
profileName: string,
|
||||
): { cdpUrl: string; cdpHost: string; cdpIsLoopback: boolean } | undefined {
|
||||
const value = normalizeOptionalString(raw);
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(value);
|
||||
} catch {
|
||||
throw new Error(`browser.profiles.${profileName}.cdpUrl must be a valid URL.`);
|
||||
}
|
||||
|
||||
if (!["http:", "https:", "ws:", "wss:"].includes(parsed.protocol)) {
|
||||
throw new Error(`browser.profiles.${profileName}.cdpUrl must use http, https, ws, or wss.`);
|
||||
}
|
||||
|
||||
const normalized =
|
||||
parsed.protocol === "http:" || parsed.protocol === "https:"
|
||||
? parsed.toString().replace(/\/$/, "")
|
||||
: parsed.toString();
|
||||
return {
|
||||
cdpUrl: normalized,
|
||||
cdpHost: parsed.hostname,
|
||||
cdpIsLoopback: isLoopbackHost(parsed.hostname),
|
||||
};
|
||||
}
|
||||
|
||||
function hasLinuxDisplay(env: NodeJS.ProcessEnv): boolean {
|
||||
return Boolean(env.DISPLAY?.trim() || env.WAYLAND_DISPLAY?.trim());
|
||||
}
|
||||
@@ -442,13 +475,16 @@ export function resolveProfile(
|
||||
const executablePath = normalizeExecutablePath(profile.executablePath) ?? resolved.executablePath;
|
||||
|
||||
if (driver === "existing-session") {
|
||||
const existingSessionCdp = normalizeExistingSessionCdpUrl(rawProfileUrl, profileName);
|
||||
return {
|
||||
name: profileName,
|
||||
cdpPort: 0,
|
||||
cdpUrl: "",
|
||||
cdpHost: "",
|
||||
cdpIsLoopback: true,
|
||||
cdpUrl: existingSessionCdp?.cdpUrl ?? "",
|
||||
cdpHost: existingSessionCdp?.cdpHost ?? "",
|
||||
cdpIsLoopback: existingSessionCdp?.cdpIsLoopback ?? true,
|
||||
userDataDir: resolveUserPath(profile.userDataDir?.trim() || "") || undefined,
|
||||
mcpCommand: normalizeOptionalString(profile.mcpCommand),
|
||||
mcpArgs: normalizeStringList(profile.mcpArgs) ?? undefined,
|
||||
color: profile.color,
|
||||
driver,
|
||||
executablePath,
|
||||
|
||||
@@ -67,7 +67,7 @@ export function registerBrowserAgentActHookRoutes(
|
||||
}
|
||||
await uploadChromeMcpFile({
|
||||
profileName: profileCtx.profile.name,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
uid,
|
||||
filePath: resolvedPaths[0] ?? "",
|
||||
@@ -137,7 +137,7 @@ export function registerBrowserAgentActHookRoutes(
|
||||
}
|
||||
await evaluateChromeMcpScript({
|
||||
profileName: profileCtx.profile.name,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
fn: `() => {
|
||||
const state = (window.__openclawDialogHook ??= {});
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
hoverChromeMcpElement,
|
||||
pressChromeMcpKey,
|
||||
resizeChromeMcpPage,
|
||||
type ChromeMcpProfileOptions,
|
||||
} from "../chrome-mcp.js";
|
||||
import type { BrowserActRequest } from "../client-actions.types.js";
|
||||
import {
|
||||
@@ -48,11 +49,13 @@ const EXISTING_SESSION_INTERACTION_NAVIGATION_RECHECK_DELAYS_MS = [0, 250, 500]
|
||||
|
||||
async function readExistingSessionLocationHref(params: {
|
||||
profileName: string;
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
}): Promise<string> {
|
||||
const currentUrl = await evaluateChromeMcpScript({
|
||||
profileName: params.profileName,
|
||||
profile: params.profile,
|
||||
userDataDir: params.userDataDir,
|
||||
targetId: params.targetId,
|
||||
fn: "() => window.location.href",
|
||||
@@ -69,6 +72,7 @@ async function readExistingSessionLocationHref(params: {
|
||||
|
||||
async function assertExistingSessionPostInteractionNavigationAllowed(params: {
|
||||
profileName: string;
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
ssrfPolicy?: BrowserNavigationPolicyOptions["ssrfPolicy"];
|
||||
@@ -208,6 +212,7 @@ function buildExistingSessionWaitPredicate(params: {
|
||||
|
||||
async function waitForExistingSessionCondition(params: {
|
||||
profileName: string;
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
timeMs?: number;
|
||||
@@ -234,6 +239,7 @@ async function waitForExistingSessionCondition(params: {
|
||||
ready = Boolean(
|
||||
await evaluateChromeMcpScript({
|
||||
profileName: params.profileName,
|
||||
profile: params.profile,
|
||||
userDataDir: params.userDataDir,
|
||||
targetId: params.targetId,
|
||||
fn: `async () => ${predicate}`,
|
||||
@@ -243,6 +249,7 @@ async function waitForExistingSessionCondition(params: {
|
||||
if (ready && params.url) {
|
||||
const currentUrl = await evaluateChromeMcpScript({
|
||||
profileName: params.profileName,
|
||||
profile: params.profile,
|
||||
userDataDir: params.userDataDir,
|
||||
targetId: params.targetId,
|
||||
fn: "() => window.location.href",
|
||||
@@ -406,7 +413,7 @@ export function registerBrowserAgentActRoutes(
|
||||
: new Set<string>();
|
||||
const existingSessionNavigationGuard = {
|
||||
profileName,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
ssrfPolicy,
|
||||
listTabs: () => profileCtx.listTabs(),
|
||||
@@ -427,7 +434,7 @@ export function registerBrowserAgentActRoutes(
|
||||
execute: () =>
|
||||
clickChromeMcpElement({
|
||||
profileName,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
uid: action.ref!,
|
||||
doubleClick: action.doubleClick ?? false,
|
||||
@@ -442,7 +449,7 @@ export function registerBrowserAgentActRoutes(
|
||||
execute: () =>
|
||||
clickChromeMcpCoords({
|
||||
profileName,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
x: action.x,
|
||||
y: action.y,
|
||||
@@ -458,7 +465,7 @@ export function registerBrowserAgentActRoutes(
|
||||
execute: async () => {
|
||||
await fillChromeMcpElement({
|
||||
profileName,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
uid: action.ref!,
|
||||
value: action.text,
|
||||
@@ -466,7 +473,7 @@ export function registerBrowserAgentActRoutes(
|
||||
if (action.submit) {
|
||||
await pressChromeMcpKey({
|
||||
profileName,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
key: "Enter",
|
||||
});
|
||||
@@ -480,7 +487,7 @@ export function registerBrowserAgentActRoutes(
|
||||
execute: () =>
|
||||
pressChromeMcpKey({
|
||||
profileName,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
key: action.key,
|
||||
}),
|
||||
@@ -492,7 +499,7 @@ export function registerBrowserAgentActRoutes(
|
||||
execute: () =>
|
||||
hoverChromeMcpElement({
|
||||
profileName,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
uid: action.ref!,
|
||||
}),
|
||||
@@ -504,7 +511,7 @@ export function registerBrowserAgentActRoutes(
|
||||
execute: () =>
|
||||
evaluateChromeMcpScript({
|
||||
profileName,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
fn: `(el) => { el.scrollIntoView({ block: "center", inline: "center" }); return true; }`,
|
||||
args: [action.ref!],
|
||||
@@ -517,7 +524,7 @@ export function registerBrowserAgentActRoutes(
|
||||
execute: () =>
|
||||
dragChromeMcpElement({
|
||||
profileName,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
fromUid: action.startRef!,
|
||||
toUid: action.endRef!,
|
||||
@@ -530,7 +537,7 @@ export function registerBrowserAgentActRoutes(
|
||||
execute: () =>
|
||||
fillChromeMcpElement({
|
||||
profileName,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
uid: action.ref!,
|
||||
value: action.values[0] ?? "",
|
||||
@@ -543,7 +550,7 @@ export function registerBrowserAgentActRoutes(
|
||||
execute: () =>
|
||||
fillChromeMcpForm({
|
||||
profileName,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
elements: action.fields.map((field) => ({
|
||||
uid: field.ref,
|
||||
@@ -556,7 +563,7 @@ export function registerBrowserAgentActRoutes(
|
||||
case "resize":
|
||||
await resizeChromeMcpPage({
|
||||
profileName,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
width: action.width,
|
||||
height: action.height,
|
||||
@@ -565,7 +572,7 @@ export function registerBrowserAgentActRoutes(
|
||||
case "wait":
|
||||
await waitForExistingSessionCondition({
|
||||
profileName,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
timeMs: action.timeMs,
|
||||
text: action.text,
|
||||
@@ -582,7 +589,7 @@ export function registerBrowserAgentActRoutes(
|
||||
execute: () =>
|
||||
evaluateChromeMcpScript({
|
||||
profileName,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
fn: action.fn,
|
||||
args: action.ref ? [action.ref] : undefined,
|
||||
@@ -592,7 +599,7 @@ export function registerBrowserAgentActRoutes(
|
||||
return await jsonOk({ result });
|
||||
}
|
||||
case "close":
|
||||
await closeChromeMcpTab(profileName, tab.targetId, profileCtx.profile.userDataDir);
|
||||
await closeChromeMcpTab(profileName, tab.targetId, profileCtx.profile);
|
||||
return await jsonOk();
|
||||
case "batch":
|
||||
return jsonActError(
|
||||
@@ -713,7 +720,7 @@ export function registerBrowserAgentActRoutes(
|
||||
if (getBrowserProfileCapabilities(profileCtx.profile).usesChromeMcp) {
|
||||
await evaluateChromeMcpScript({
|
||||
profileName: profileCtx.profile.name,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
args: [ref],
|
||||
fn: `(el) => {
|
||||
|
||||
@@ -140,6 +140,7 @@ describe("existing-session browser routes", () => {
|
||||
});
|
||||
expect(chromeMcpMocks.takeChromeMcpSnapshot).toHaveBeenCalledWith({
|
||||
profileName: "chrome-live",
|
||||
profile: expect.objectContaining({ name: "chrome-live", driver: "existing-session" }),
|
||||
targetId: "7",
|
||||
});
|
||||
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).not.toHaveBeenCalled();
|
||||
@@ -166,6 +167,7 @@ describe("existing-session browser routes", () => {
|
||||
});
|
||||
expect(chromeMcpMocks.takeChromeMcpScreenshot).toHaveBeenCalledWith({
|
||||
profileName: "chrome-live",
|
||||
profile: expect.objectContaining({ name: "chrome-live", driver: "existing-session" }),
|
||||
targetId: "7",
|
||||
uid: "btn-1",
|
||||
fullPage: false,
|
||||
@@ -285,6 +287,8 @@ describe("existing-session browser routes", () => {
|
||||
expect(response.body).toMatchObject({ ok: true, targetId: "7" });
|
||||
expect(chromeMcpMocks.evaluateChromeMcpScript).toHaveBeenCalledWith({
|
||||
profileName: "chrome-live",
|
||||
profile: expect.objectContaining({ name: "chrome-live", driver: "existing-session" }),
|
||||
userDataDir: undefined,
|
||||
targetId: "7",
|
||||
fn: "() => window.location.href",
|
||||
});
|
||||
@@ -308,7 +312,7 @@ describe("existing-session browser routes", () => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(chromeMcpMocks.clickChromeMcpElement).toHaveBeenCalledWith({
|
||||
profileName: "chrome-live",
|
||||
userDataDir: undefined,
|
||||
profile: expect.objectContaining({ name: "chrome-live", driver: "existing-session" }),
|
||||
targetId: "7",
|
||||
uid: "btn-1",
|
||||
doubleClick: false,
|
||||
@@ -334,7 +338,7 @@ describe("existing-session browser routes", () => {
|
||||
expect(response.body).toMatchObject({ ok: true, targetId: "7", url: "https://example.com" });
|
||||
expect(chromeMcpMocks.clickChromeMcpCoords).toHaveBeenCalledWith({
|
||||
profileName: "chrome-live",
|
||||
userDataDir: undefined,
|
||||
profile: expect.objectContaining({ name: "chrome-live", driver: "existing-session" }),
|
||||
targetId: "7",
|
||||
x: 25,
|
||||
y: 32,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
navigateChromeMcpPage,
|
||||
takeChromeMcpScreenshot,
|
||||
takeChromeMcpSnapshot,
|
||||
type ChromeMcpProfileOptions,
|
||||
} from "../chrome-mcp.js";
|
||||
import {
|
||||
buildAiSnapshotFromChromeMcpSnapshot,
|
||||
@@ -57,11 +58,13 @@ function browserNavigationPolicyForProfile(ctx: BrowserRouteContext, profileCtx:
|
||||
|
||||
async function collectChromeMcpSnapshotUrls(params: {
|
||||
profileName: string;
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
}): Promise<Array<{ text: string; url: string }>> {
|
||||
const result = await evaluateChromeMcpScript({
|
||||
profileName: params.profileName,
|
||||
profile: params.profile,
|
||||
userDataDir: params.userDataDir,
|
||||
targetId: params.targetId,
|
||||
fn: `() => {
|
||||
@@ -102,11 +105,13 @@ function appendSnapshotUrls(snapshot: string, urls: Array<{ text: string; url: s
|
||||
|
||||
async function clearChromeMcpOverlay(params: {
|
||||
profileName: string;
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
}): Promise<void> {
|
||||
await evaluateChromeMcpScript({
|
||||
profileName: params.profileName,
|
||||
profile: params.profile,
|
||||
userDataDir: params.userDataDir,
|
||||
targetId: params.targetId,
|
||||
fn: `() => {
|
||||
@@ -118,6 +123,7 @@ async function clearChromeMcpOverlay(params: {
|
||||
|
||||
async function renderChromeMcpLabels(params: {
|
||||
profileName: string;
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
refs: string[];
|
||||
@@ -125,6 +131,7 @@ async function renderChromeMcpLabels(params: {
|
||||
const refList = JSON.stringify(params.refs);
|
||||
const result = await evaluateChromeMcpScript({
|
||||
profileName: params.profileName,
|
||||
profile: params.profile,
|
||||
userDataDir: params.userDataDir,
|
||||
targetId: params.targetId,
|
||||
args: params.refs,
|
||||
@@ -265,7 +272,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
||||
await assertBrowserNavigationAllowed({ url, ...ssrfPolicyOpts });
|
||||
const result = await navigateChromeMcpPage({
|
||||
profileName: profileCtx.profile.name,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
url,
|
||||
});
|
||||
@@ -369,20 +376,20 @@ export function registerBrowserAgentSnapshotRoutes(
|
||||
if (labels) {
|
||||
const snapshot = await takeChromeMcpSnapshot({
|
||||
profileName: profileCtx.profile.name,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
});
|
||||
const built = buildAiSnapshotFromChromeMcpSnapshot({ root: snapshot });
|
||||
const labelResult = await renderChromeMcpLabels({
|
||||
profileName: profileCtx.profile.name,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
refs: Object.keys(built.refs),
|
||||
});
|
||||
try {
|
||||
const buffer = await takeChromeMcpScreenshot({
|
||||
profileName: profileCtx.profile.name,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
fullPage,
|
||||
format: type,
|
||||
@@ -401,7 +408,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
||||
} finally {
|
||||
await clearChromeMcpOverlay({
|
||||
profileName: profileCtx.profile.name,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
});
|
||||
}
|
||||
@@ -409,7 +416,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
||||
}
|
||||
const buffer = await takeChromeMcpScreenshot({
|
||||
profileName: profileCtx.profile.name,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
uid: ref,
|
||||
fullPage,
|
||||
@@ -531,7 +538,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
||||
}
|
||||
const snapshot = await takeChromeMcpSnapshot({
|
||||
profileName: profileCtx.profile.name,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
});
|
||||
if (plan.format === "aria") {
|
||||
@@ -559,7 +566,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
||||
built.snapshot,
|
||||
await collectChromeMcpSnapshotUrls({
|
||||
profileName: profileCtx.profile.name,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
}),
|
||||
),
|
||||
@@ -569,14 +576,14 @@ export function registerBrowserAgentSnapshotRoutes(
|
||||
const refs = Object.keys(builtWithUrls.refs);
|
||||
const labelResult = await renderChromeMcpLabels({
|
||||
profileName: profileCtx.profile.name,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
refs,
|
||||
});
|
||||
try {
|
||||
const labeled = await takeChromeMcpScreenshot({
|
||||
profileName: profileCtx.profile.name,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
format: "png",
|
||||
});
|
||||
@@ -606,7 +613,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
||||
} finally {
|
||||
await clearChromeMcpOverlay({
|
||||
profileName: profileCtx.profile.name,
|
||||
userDataDir: profileCtx.profile.userDataDir,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ export function createProfileAvailability({
|
||||
if (capabilities.usesChromeMcp) {
|
||||
// listChromeMcpTabs creates the session if needed — no separate ensureChromeMcpAvailable call required
|
||||
const { listChromeMcpTabs } = await getChromeMcpModule();
|
||||
await listChromeMcpTabs(profile.name, profile.userDataDir);
|
||||
await listChromeMcpTabs(profile.name, profile);
|
||||
return true;
|
||||
}
|
||||
const { httpTimeoutMs, wsTimeoutMs } = resolveTimeouts(timeoutMs);
|
||||
@@ -105,7 +105,7 @@ export function createProfileAvailability({
|
||||
const isTransportAvailable = async (timeoutMs?: number) => {
|
||||
if (capabilities.usesChromeMcp) {
|
||||
const { ensureChromeMcpAvailable } = await getChromeMcpModule();
|
||||
await ensureChromeMcpAvailable(profile.name, profile.userDataDir, {
|
||||
await ensureChromeMcpAvailable(profile.name, profile, {
|
||||
ephemeral: true,
|
||||
timeoutMs,
|
||||
});
|
||||
@@ -218,7 +218,7 @@ export function createProfileAvailability({
|
||||
while (Date.now() < deadlineMs) {
|
||||
try {
|
||||
const { listChromeMcpTabs } = await getChromeMcpModule();
|
||||
await listChromeMcpTabs(profile.name, profile.userDataDir);
|
||||
await listChromeMcpTabs(profile.name, profile);
|
||||
return;
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
@@ -239,7 +239,7 @@ export function createProfileAvailability({
|
||||
);
|
||||
}
|
||||
const { ensureChromeMcpAvailable } = await getChromeMcpModule();
|
||||
await ensureChromeMcpAvailable(profile.name, profile.userDataDir);
|
||||
await ensureChromeMcpAvailable(profile.name, profile);
|
||||
await waitForChromeMcpReadyAfterAttach();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -74,6 +74,14 @@ function makeState(): BrowserServerState {
|
||||
};
|
||||
}
|
||||
|
||||
function expectChromeLiveProfile() {
|
||||
return expect.objectContaining({
|
||||
name: "chrome-live",
|
||||
driver: "existing-session",
|
||||
userDataDir: "/tmp/brave-profile",
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
for (const key of [
|
||||
"ALL_PROXY",
|
||||
@@ -114,12 +122,16 @@ describe("browser server-context existing-session profile", () => {
|
||||
|
||||
expect(chromeMcp.ensureChromeMcpAvailable).toHaveBeenCalledWith(
|
||||
"chrome-live",
|
||||
"/tmp/brave-profile",
|
||||
expectChromeLiveProfile(),
|
||||
{ ephemeral: true, timeoutMs: 300 },
|
||||
);
|
||||
expect(chromeMcp.listChromeMcpTabs).toHaveBeenCalledWith("chrome-live", "/tmp/brave-profile", {
|
||||
ephemeral: true,
|
||||
});
|
||||
expect(chromeMcp.listChromeMcpTabs).toHaveBeenCalledWith(
|
||||
"chrome-live",
|
||||
expectChromeLiveProfile(),
|
||||
{
|
||||
ephemeral: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the next real attach on the normal sticky session path after an idle status probe", async () => {
|
||||
@@ -146,17 +158,17 @@ describe("browser server-context existing-session profile", () => {
|
||||
expect(tabs.map((tab) => tab.targetId)).toEqual(["7"]);
|
||||
expect(chromeMcp.ensureChromeMcpAvailable).toHaveBeenLastCalledWith(
|
||||
"chrome-live",
|
||||
"/tmp/brave-profile",
|
||||
expectChromeLiveProfile(),
|
||||
);
|
||||
expect(chromeMcp.listChromeMcpTabs).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"chrome-live",
|
||||
"/tmp/brave-profile",
|
||||
expectChromeLiveProfile(),
|
||||
);
|
||||
expect(chromeMcp.listChromeMcpTabs).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"chrome-live",
|
||||
"/tmp/brave-profile",
|
||||
expectChromeLiveProfile(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -201,18 +213,21 @@ describe("browser server-context existing-session profile", () => {
|
||||
|
||||
expect(chromeMcp.ensureChromeMcpAvailable).toHaveBeenCalledWith(
|
||||
"chrome-live",
|
||||
"/tmp/brave-profile",
|
||||
expectChromeLiveProfile(),
|
||||
);
|
||||
expect(chromeMcp.listChromeMcpTabs).toHaveBeenCalledWith(
|
||||
"chrome-live",
|
||||
expectChromeLiveProfile(),
|
||||
);
|
||||
expect(chromeMcp.listChromeMcpTabs).toHaveBeenCalledWith("chrome-live", "/tmp/brave-profile");
|
||||
expect(chromeMcp.openChromeMcpTab).toHaveBeenCalledWith(
|
||||
"chrome-live",
|
||||
"about:blank",
|
||||
"/tmp/brave-profile",
|
||||
expectChromeLiveProfile(),
|
||||
);
|
||||
expect(chromeMcp.focusChromeMcpTab).toHaveBeenCalledWith(
|
||||
"chrome-live",
|
||||
"7",
|
||||
"/tmp/brave-profile",
|
||||
expectChromeLiveProfile(),
|
||||
);
|
||||
expect(chromeMcp.closeChromeMcpSession).toHaveBeenCalledWith("chrome-live");
|
||||
});
|
||||
|
||||
@@ -99,7 +99,7 @@ export function createProfileSelectionOps({
|
||||
|
||||
if (capabilities.usesChromeMcp) {
|
||||
const { focusChromeMcpTab } = await getChromeMcpModule();
|
||||
await focusChromeMcpTab(profile.name, resolvedTargetId, profile.userDataDir);
|
||||
await focusChromeMcpTab(profile.name, resolvedTargetId, profile);
|
||||
const profileState = getProfileState();
|
||||
profileState.lastTargetId = resolvedTargetId;
|
||||
return;
|
||||
@@ -136,7 +136,7 @@ export function createProfileSelectionOps({
|
||||
|
||||
if (capabilities.usesChromeMcp) {
|
||||
const { closeChromeMcpTab } = await getChromeMcpModule();
|
||||
await closeChromeMcpTab(profile.name, resolvedTargetId, profile.userDataDir);
|
||||
await closeChromeMcpTab(profile.name, resolvedTargetId, profile);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ export function createProfileTabOps({
|
||||
const readTabs = async (): Promise<BrowserTab[]> => {
|
||||
if (capabilities.usesChromeMcp) {
|
||||
const { listChromeMcpTabs } = await getChromeMcpModule();
|
||||
return await listChromeMcpTabs(profile.name, profile.userDataDir);
|
||||
return await listChromeMcpTabs(profile.name, profile);
|
||||
}
|
||||
|
||||
if (capabilities.usesPersistentPlaywright) {
|
||||
@@ -231,7 +231,7 @@ export function createProfileTabOps({
|
||||
if (capabilities.usesChromeMcp) {
|
||||
await assertBrowserNavigationAllowed({ url, ...ssrfPolicyOpts });
|
||||
const { openChromeMcpTab } = await getChromeMcpModule();
|
||||
const page = await openChromeMcpTab(profile.name, url, profile.userDataDir);
|
||||
const page = await openChromeMcpTab(profile.name, url, profile);
|
||||
const profileState = getProfileState();
|
||||
profileState.lastTargetId = page.targetId;
|
||||
await assertBrowserNavigationResultAllowed({ url: page.url, ...ssrfPolicyOpts });
|
||||
|
||||
@@ -182,7 +182,7 @@ export function createBrowserRouteContext(opts: ContextOptions): BrowserRouteCon
|
||||
try {
|
||||
running = await profileCtx.isTransportAvailable(300);
|
||||
if (running) {
|
||||
const tabs = await listChromeMcpTabs(profile.name, profile.userDataDir, {
|
||||
const tabs = await listChromeMcpTabs(profile.name, profile, {
|
||||
ephemeral: true,
|
||||
}).catch(() => [] as BrowserTab[]);
|
||||
tabCount = tabs.filter((t) => t.type === "page").length;
|
||||
|
||||
@@ -8,7 +8,7 @@ function buildBridgeFromPersistedBundledRecord(
|
||||
// Relocation is derived from the previous persisted registry, not a hardcoded
|
||||
// table. A plugin moving from bundled to npm keeps the same plugin id; the old
|
||||
// registry row is the proof that this user actually had it bundled/enabled.
|
||||
if (record.origin !== "bundled" || record.enabled === false) {
|
||||
if (record.origin !== "bundled" || !record.enabled) {
|
||||
return null;
|
||||
}
|
||||
const npmSpec = record.packageInstall?.npm?.spec;
|
||||
@@ -19,7 +19,7 @@ function buildBridgeFromPersistedBundledRecord(
|
||||
bundledPluginId: record.pluginId,
|
||||
pluginId: record.pluginId,
|
||||
npmSpec,
|
||||
...(record.enabledByDefault === true ? { enabledByDefault: true } : {}),
|
||||
...(record.enabledByDefault ? { enabledByDefault: true } : {}),
|
||||
channelIds: record.contributions.channels,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -780,6 +780,21 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
description:
|
||||
"Per-profile Chromium user data directory for existing-session attachment through Chrome DevTools MCP. Use this for Brave, Edge, Chromium, or non-default Chrome profiles when the built-in auto-connect path would pick the wrong browser data directory on the selected host or browser node.",
|
||||
},
|
||||
mcpCommand: {
|
||||
type: "string",
|
||||
title: "Browser Profile Chrome MCP Command",
|
||||
description:
|
||||
"Per-profile Chrome DevTools MCP command for existing-session attachment. Defaults to npx.",
|
||||
},
|
||||
mcpArgs: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
title: "Browser Profile Chrome MCP Args",
|
||||
description:
|
||||
"Extra per-profile Chrome DevTools MCP arguments for existing-session attachment, such as --no-usage-statistics. Endpoint arguments here override the built-in auto-connect or browser URL selection.",
|
||||
},
|
||||
driver: {
|
||||
anyOf: [
|
||||
{
|
||||
@@ -24061,6 +24076,16 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
help: "Per-profile Chromium user data directory for existing-session attachment through Chrome DevTools MCP. Use this for Brave, Edge, Chromium, or non-default Chrome profiles when the built-in auto-connect path would pick the wrong browser data directory on the selected host or browser node.",
|
||||
tags: ["storage"],
|
||||
},
|
||||
"browser.profiles.*.mcpCommand": {
|
||||
label: "Browser Profile Chrome MCP Command",
|
||||
help: "Per-profile Chrome DevTools MCP command for existing-session attachment. Defaults to npx.",
|
||||
tags: ["storage"],
|
||||
},
|
||||
"browser.profiles.*.mcpArgs": {
|
||||
label: "Browser Profile Chrome MCP Args",
|
||||
help: "Extra per-profile Chrome DevTools MCP arguments for existing-session attachment, such as --no-usage-statistics. Endpoint arguments here override the built-in auto-connect or browser URL selection.",
|
||||
tags: ["storage"],
|
||||
},
|
||||
"browser.profiles.*.driver": {
|
||||
label: "Browser Profile Driver",
|
||||
help: 'Per-profile browser driver mode. Use "openclaw" (or legacy "clawd") for CDP-based profiles, or use "existing-session" for Chrome DevTools MCP attachment on the selected host or browser node.',
|
||||
|
||||
@@ -286,6 +286,10 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"Per-profile CDP websocket URL used for explicit remote browser routing by profile name. Use this when profile connections terminate on remote hosts or tunnels.",
|
||||
"browser.profiles.*.userDataDir":
|
||||
"Per-profile Chromium user data directory for existing-session attachment through Chrome DevTools MCP. Use this for Brave, Edge, Chromium, or non-default Chrome profiles when the built-in auto-connect path would pick the wrong browser data directory on the selected host or browser node.",
|
||||
"browser.profiles.*.mcpCommand":
|
||||
"Per-profile Chrome DevTools MCP command for existing-session attachment. Defaults to npx.",
|
||||
"browser.profiles.*.mcpArgs":
|
||||
"Extra per-profile Chrome DevTools MCP arguments for existing-session attachment, such as --no-usage-statistics. Endpoint arguments here override the built-in auto-connect or browser URL selection.",
|
||||
"browser.profiles.*.driver":
|
||||
'Per-profile browser driver mode. Use "openclaw" (or legacy "clawd") for CDP-based profiles, or use "existing-session" for Chrome DevTools MCP attachment on the selected host or browser node.',
|
||||
"browser.profiles.*.headless":
|
||||
|
||||
@@ -154,6 +154,8 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"browser.profiles.*.cdpPort": "Browser Profile CDP Port",
|
||||
"browser.profiles.*.cdpUrl": "Browser Profile CDP URL",
|
||||
"browser.profiles.*.userDataDir": "Browser Profile User Data Dir",
|
||||
"browser.profiles.*.mcpCommand": "Browser Profile Chrome MCP Command",
|
||||
"browser.profiles.*.mcpArgs": "Browser Profile Chrome MCP Args",
|
||||
"browser.profiles.*.driver": "Browser Profile Driver",
|
||||
"browser.profiles.*.headless": "Browser Profile Headless Mode",
|
||||
"browser.profiles.*.attachOnly": "Browser Profile Attach-only Mode",
|
||||
|
||||
@@ -5,6 +5,10 @@ export type BrowserProfileConfig = {
|
||||
cdpUrl?: string;
|
||||
/** Explicit user data directory for existing-session Chrome MCP attachment. */
|
||||
userDataDir?: string;
|
||||
/** Override the Chrome MCP command for existing-session profiles. */
|
||||
mcpCommand?: string;
|
||||
/** Extra Chrome MCP arguments for existing-session profiles. */
|
||||
mcpArgs?: string[];
|
||||
/** Profile driver (default: openclaw). */
|
||||
driver?: "openclaw" | "clawd" | "existing-session";
|
||||
/** If true, launch this profile in headless mode. Falls back to browser.headless. */
|
||||
|
||||
@@ -425,6 +425,8 @@ export const OpenClawSchema = z
|
||||
cdpPort: z.number().int().min(1).max(65535).optional(),
|
||||
cdpUrl: z.string().optional(),
|
||||
userDataDir: z.string().optional(),
|
||||
mcpCommand: z.string().optional(),
|
||||
mcpArgs: z.array(z.string()).optional(),
|
||||
driver: z
|
||||
.union([z.literal("openclaw"), z.literal("clawd"), z.literal("existing-session")])
|
||||
.optional(),
|
||||
|
||||
@@ -222,6 +222,10 @@ function pathEndsWithSegment(params: {
|
||||
return Boolean(value && segment && (value === segment || value.endsWith(`/${segment}`)));
|
||||
}
|
||||
|
||||
function bundledExtensionPathSegment(bundledDirName: string): string {
|
||||
return ["extensions", bundledDirName].join("/");
|
||||
}
|
||||
|
||||
function isBridgeBundledPathRecord(params: {
|
||||
bridge: ExternalizedBundledPluginBridge;
|
||||
bundledLocalPath?: string;
|
||||
@@ -242,12 +246,12 @@ function isBridgeBundledPathRecord(params: {
|
||||
return (
|
||||
pathEndsWithSegment({
|
||||
value: params.record.sourcePath,
|
||||
segment: `extensions/${bundledDirName}`,
|
||||
segment: bundledExtensionPathSegment(bundledDirName),
|
||||
env: params.env,
|
||||
}) ||
|
||||
pathEndsWithSegment({
|
||||
value: params.record.installPath,
|
||||
segment: `extensions/${bundledDirName}`,
|
||||
segment: bundledExtensionPathSegment(bundledDirName),
|
||||
env: params.env,
|
||||
})
|
||||
);
|
||||
@@ -262,7 +266,7 @@ function removeBridgeBundledLoadPaths(params: {
|
||||
params.loadPaths.removeMatching((entry) =>
|
||||
pathEndsWithSegment({
|
||||
value: entry,
|
||||
segment: `extensions/${bundledDirName}`,
|
||||
segment: bundledExtensionPathSegment(bundledDirName),
|
||||
env: params.env,
|
||||
}),
|
||||
);
|
||||
@@ -896,9 +900,6 @@ export async function syncPluginsForUpdateChannel(params: {
|
||||
installs = next.plugins?.installs ?? {};
|
||||
changed = true;
|
||||
}
|
||||
if (bundledInfo?.localPath) {
|
||||
loadHelpers.removePath(bundledInfo.localPath);
|
||||
}
|
||||
removeBridgeBundledLoadPaths({ bridge, loadPaths: loadHelpers, env });
|
||||
continue;
|
||||
}
|
||||
@@ -907,7 +908,7 @@ export async function syncPluginsForUpdateChannel(params: {
|
||||
existing &&
|
||||
!isBridgeBundledPathRecord({
|
||||
bridge,
|
||||
bundledLocalPath: bundledInfo?.localPath,
|
||||
bundledLocalPath: undefined,
|
||||
record: existing.record,
|
||||
env,
|
||||
})
|
||||
@@ -947,9 +948,6 @@ export async function syncPluginsForUpdateChannel(params: {
|
||||
...buildNpmResolutionInstallFields(result.npmResolution),
|
||||
});
|
||||
installs = next.plugins?.installs ?? {};
|
||||
if (bundledInfo?.localPath) {
|
||||
loadHelpers.removePath(bundledInfo.localPath);
|
||||
}
|
||||
if (existing?.record.sourcePath) {
|
||||
loadHelpers.removePath(existing.record.sourcePath);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user