mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:50:45 +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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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
|
9a012a9c87b9010683289dc7d68ba5446a4b78beedf381e2c5f9d486f25a9213 config-baseline.json
|
||||||
f54f808dc85123a5ba788618a6dff7f2c869ced639dd0db34a86802985730dc6 config-baseline.core.json
|
6128d6eff8c28d17194d1ae9ee7f72abae48da1c6476ab16e6378f1898e4373a config-baseline.core.json
|
||||||
7cd9c908f066c143eab2a201efbc9640f483ab28bba92ddeca1d18cc2b528bc3 config-baseline.channel.json
|
7cd9c908f066c143eab2a201efbc9640f483ab28bba92ddeca1d18cc2b528bc3 config-baseline.channel.json
|
||||||
7825b56a5b3fcdbe2e09ef8fe5d9f12ac3598435afebe20413051e45b0d1968e config-baseline.plugin.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 () => {
|
it("parses new_page text responses and returns the created tab", async () => {
|
||||||
const factory: ChromeMcpSessionFactory = async () => createFakeSession();
|
const factory: ChromeMcpSessionFactory = async () => createFakeSession();
|
||||||
setChromeMcpSessionFactoryForTest(factory);
|
setChromeMcpSessionFactoryForTest(factory);
|
||||||
@@ -435,8 +497,8 @@ describe("chrome MCP page parsing", () => {
|
|||||||
const createdSessions: ChromeMcpSession[] = [];
|
const createdSessions: ChromeMcpSession[] = [];
|
||||||
const closeMocks: Array<ReturnType<typeof vi.fn>> = [];
|
const closeMocks: Array<ReturnType<typeof vi.fn>> = [];
|
||||||
const factoryCalls: Array<{ profileName: string; userDataDir?: string }> = [];
|
const factoryCalls: Array<{ profileName: string; userDataDir?: string }> = [];
|
||||||
const factory: ChromeMcpSessionFactory = async (profileName, userDataDir) => {
|
const factory: ChromeMcpSessionFactory = async (profileName, options) => {
|
||||||
factoryCalls.push({ profileName, userDataDir });
|
factoryCalls.push({ profileName, userDataDir: options?.userDataDir });
|
||||||
const session = createFakeSession();
|
const session = createFakeSession();
|
||||||
const closeMock = vi.fn().mockResolvedValue(undefined);
|
const closeMock = vi.fn().mockResolvedValue(undefined);
|
||||||
session.client.close = closeMock as typeof session.client.close;
|
session.client.close = closeMock as typeof session.client.close;
|
||||||
|
|||||||
@@ -37,6 +37,21 @@ type ChromeMcpCallOptions = {
|
|||||||
signal?: AbortSignal;
|
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 = {
|
type ChromeMcpSessionLease = {
|
||||||
session: ChromeMcpSession;
|
session: ChromeMcpSession;
|
||||||
cacheKey: string;
|
cacheKey: string;
|
||||||
@@ -45,18 +60,26 @@ type ChromeMcpSessionLease = {
|
|||||||
|
|
||||||
type ChromeMcpSessionFactory = (
|
type ChromeMcpSessionFactory = (
|
||||||
profileName: string,
|
profileName: string,
|
||||||
userDataDir?: string,
|
options?: NormalizedChromeMcpProfileOptions,
|
||||||
) => Promise<ChromeMcpSession>;
|
) => Promise<ChromeMcpSession>;
|
||||||
|
|
||||||
const DEFAULT_CHROME_MCP_COMMAND = "npx";
|
const DEFAULT_CHROME_MCP_COMMAND = "npx";
|
||||||
const DEFAULT_CHROME_MCP_ARGS = [
|
const DEFAULT_CHROME_MCP_PACKAGE_ARGS = ["-y", "chrome-devtools-mcp@latest"];
|
||||||
"-y",
|
const DEFAULT_CHROME_MCP_FEATURE_ARGS = [
|
||||||
"chrome-devtools-mcp@latest",
|
|
||||||
"--autoConnect",
|
|
||||||
// Direct chrome-devtools-mcp launches do not enable structuredContent by default.
|
// Direct chrome-devtools-mcp launches do not enable structuredContent by default.
|
||||||
"--experimentalStructuredContent",
|
"--experimentalStructuredContent",
|
||||||
"--experimental-page-id-routing",
|
"--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_NEW_PAGE_TIMEOUT_MS = 5_000;
|
||||||
const CHROME_MCP_NAVIGATE_TIMEOUT_MS = 20_000;
|
const CHROME_MCP_NAVIGATE_TIMEOUT_MS = 20_000;
|
||||||
const CHROME_MCP_HANDSHAKE_TIMEOUT_MS = 30_000;
|
const CHROME_MCP_HANDSHAKE_TIMEOUT_MS = 30_000;
|
||||||
@@ -197,8 +220,83 @@ function normalizeChromeMcpUserDataDir(userDataDir?: string): string | undefined
|
|||||||
return trimmed ? trimmed : undefined;
|
return trimmed ? trimmed : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildChromeMcpSessionCacheKey(profileName: string, userDataDir?: string): string {
|
function normalizeChromeMcpStringList(values?: string[]): string[] {
|
||||||
return JSON.stringify([profileName, normalizeChromeMcpUserDataDir(userDataDir) ?? ""]);
|
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 {
|
function cacheKeyMatchesProfileName(cacheKey: string, profileName: string): boolean {
|
||||||
@@ -234,11 +332,20 @@ async function closeChromeMcpSessionsForProfile(
|
|||||||
return closed;
|
return closed;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildChromeMcpArgs(userDataDir?: string): string[] {
|
function buildChromeMcpArgsFromOptions(options: NormalizedChromeMcpProfileOptions): string[] {
|
||||||
const normalizedUserDataDir = normalizeChromeMcpUserDataDir(userDataDir);
|
const commandPrefix =
|
||||||
return normalizedUserDataDir
|
options.command === DEFAULT_CHROME_MCP_COMMAND ? DEFAULT_CHROME_MCP_PACKAGE_ARGS : [];
|
||||||
? [...DEFAULT_CHROME_MCP_ARGS, "--userDataDir", normalizedUserDataDir]
|
return [
|
||||||
: [...DEFAULT_CHROME_MCP_ARGS];
|
...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 {
|
function drainStderr(transport: StdioClientTransport): () => string {
|
||||||
@@ -289,11 +396,11 @@ async function withChromeMcpHandshakeTimeout<T>(task: Promise<T>): Promise<T> {
|
|||||||
|
|
||||||
async function createRealSession(
|
async function createRealSession(
|
||||||
profileName: string,
|
profileName: string,
|
||||||
userDataDir?: string,
|
options: NormalizedChromeMcpProfileOptions = normalizeChromeMcpOptions(),
|
||||||
): Promise<ChromeMcpSession> {
|
): Promise<ChromeMcpSession> {
|
||||||
const transport = new StdioClientTransport({
|
const transport = new StdioClientTransport({
|
||||||
command: DEFAULT_CHROME_MCP_COMMAND,
|
command: options.command,
|
||||||
args: buildChromeMcpArgs(userDataDir),
|
args: buildChromeMcpArgsFromOptions(options),
|
||||||
stderr: "pipe",
|
stderr: "pipe",
|
||||||
});
|
});
|
||||||
const client = new Client(
|
const client = new Client(
|
||||||
@@ -325,9 +432,11 @@ async function createRealSession(
|
|||||||
`Chrome MCP attach failed for profile "${profileName}". Subprocess stderr:\n${stderr}`,
|
`Chrome MCP attach failed for profile "${profileName}". Subprocess stderr:\n${stderr}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const targetLabel = userDataDir
|
const targetLabel = options.browserUrl
|
||||||
? `the configured Chromium user data dir (${userDataDir})`
|
? `the configured Chrome endpoint (${options.browserUrl})`
|
||||||
: "Google Chrome's default profile";
|
: options.userDataDir
|
||||||
|
? `the configured Chromium user data dir (${options.userDataDir})`
|
||||||
|
: "Google Chrome's default profile";
|
||||||
throw new BrowserProfileUnavailableError(
|
throw new BrowserProfileUnavailableError(
|
||||||
`Chrome MCP existing-session attach failed for profile "${profileName}". ` +
|
`Chrome MCP existing-session attach failed for profile "${profileName}". ` +
|
||||||
`Make sure ${targetLabel} is running locally with remote debugging enabled. ` +
|
`Make sure ${targetLabel} is running locally with remote debugging enabled. ` +
|
||||||
@@ -377,10 +486,11 @@ async function waitForChromeMcpReady(
|
|||||||
|
|
||||||
async function getSession(
|
async function getSession(
|
||||||
profileName: string,
|
profileName: string,
|
||||||
userDataDir?: string,
|
profileOptions?: ChromeMcpOptionsInput,
|
||||||
timeoutMs?: number,
|
timeoutMs?: number,
|
||||||
): Promise<ChromeMcpSession> {
|
): Promise<ChromeMcpSession> {
|
||||||
const cacheKey = buildChromeMcpSessionCacheKey(profileName, userDataDir);
|
const options = normalizeChromeMcpOptions(profileOptions);
|
||||||
|
const cacheKey = buildChromeMcpSessionCacheKey(profileName, options);
|
||||||
await closeChromeMcpSessionsForProfile(profileName, cacheKey);
|
await closeChromeMcpSessionsForProfile(profileName, cacheKey);
|
||||||
|
|
||||||
let session = sessions.get(cacheKey);
|
let session = sessions.get(cacheKey);
|
||||||
@@ -392,7 +502,7 @@ async function getSession(
|
|||||||
let pending = pendingSessions.get(cacheKey);
|
let pending = pendingSessions.get(cacheKey);
|
||||||
if (!pending) {
|
if (!pending) {
|
||||||
pending = (async () => {
|
pending = (async () => {
|
||||||
const created = await (sessionFactory ?? createRealSession)(profileName, userDataDir);
|
const created = await (sessionFactory ?? createRealSession)(profileName, options);
|
||||||
if (pendingSessions.get(cacheKey) === pending) {
|
if (pendingSessions.get(cacheKey) === pending) {
|
||||||
sessions.set(cacheKey, created);
|
sessions.set(cacheKey, created);
|
||||||
} else {
|
} else {
|
||||||
@@ -465,10 +575,11 @@ async function getExistingSession(
|
|||||||
|
|
||||||
async function createEphemeralSession(
|
async function createEphemeralSession(
|
||||||
profileName: string,
|
profileName: string,
|
||||||
userDataDir?: string,
|
profileOptions?: ChromeMcpOptionsInput,
|
||||||
timeoutMs?: number,
|
timeoutMs?: number,
|
||||||
): Promise<ChromeMcpSession> {
|
): Promise<ChromeMcpSession> {
|
||||||
const session = await (sessionFactory ?? createRealSession)(profileName, userDataDir);
|
const options = normalizeChromeMcpOptions(profileOptions);
|
||||||
|
const session = await (sessionFactory ?? createRealSession)(profileName, options);
|
||||||
try {
|
try {
|
||||||
await waitForChromeMcpReady(session, profileName, timeoutMs);
|
await waitForChromeMcpReady(session, profileName, timeoutMs);
|
||||||
return session;
|
return session;
|
||||||
@@ -480,13 +591,14 @@ async function createEphemeralSession(
|
|||||||
|
|
||||||
async function leaseSession(
|
async function leaseSession(
|
||||||
profileName: string,
|
profileName: string,
|
||||||
userDataDir?: string,
|
profileOptions?: ChromeMcpOptionsInput,
|
||||||
options: ChromeMcpCallOptions = {},
|
options: ChromeMcpCallOptions = {},
|
||||||
): Promise<ChromeMcpSessionLease> {
|
): Promise<ChromeMcpSessionLease> {
|
||||||
const cacheKey = buildChromeMcpSessionCacheKey(profileName, userDataDir);
|
const normalizedProfileOptions = normalizeChromeMcpOptions(profileOptions);
|
||||||
|
const cacheKey = buildChromeMcpSessionCacheKey(profileName, normalizedProfileOptions);
|
||||||
if (!options.ephemeral) {
|
if (!options.ephemeral) {
|
||||||
return {
|
return {
|
||||||
session: await getSession(profileName, userDataDir, options.timeoutMs),
|
session: await getSession(profileName, normalizedProfileOptions, options.timeoutMs),
|
||||||
cacheKey,
|
cacheKey,
|
||||||
temporary: false,
|
temporary: false,
|
||||||
};
|
};
|
||||||
@@ -504,7 +616,7 @@ async function leaseSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
session: await createEphemeralSession(profileName, userDataDir, options.timeoutMs),
|
session: await createEphemeralSession(profileName, normalizedProfileOptions, options.timeoutMs),
|
||||||
cacheKey,
|
cacheKey,
|
||||||
temporary: true,
|
temporary: true,
|
||||||
};
|
};
|
||||||
@@ -512,7 +624,7 @@ async function leaseSession(
|
|||||||
|
|
||||||
async function callTool(
|
async function callTool(
|
||||||
profileName: string,
|
profileName: string,
|
||||||
userDataDir: string | undefined,
|
profileOptions: ChromeMcpOptionsInput | undefined,
|
||||||
name: string,
|
name: string,
|
||||||
args: Record<string, unknown> = {},
|
args: Record<string, unknown> = {},
|
||||||
options: ChromeMcpCallOptions = {},
|
options: ChromeMcpCallOptions = {},
|
||||||
@@ -524,7 +636,7 @@ async function callTool(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
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({
|
const rawCall = lease.session.client.callTool({
|
||||||
name,
|
name,
|
||||||
arguments: args,
|
arguments: args,
|
||||||
@@ -620,9 +732,9 @@ async function withTempFile<T>(fn: (filePath: string) => Promise<T>): Promise<T>
|
|||||||
async function findPageById(
|
async function findPageById(
|
||||||
profileName: string,
|
profileName: string,
|
||||||
pageId: number,
|
pageId: number,
|
||||||
userDataDir?: string,
|
profileOptions?: string | ChromeMcpProfileOptions,
|
||||||
): Promise<ChromeMcpStructuredPage> {
|
): Promise<ChromeMcpStructuredPage> {
|
||||||
const pages = await listChromeMcpPages(profileName, userDataDir);
|
const pages = await listChromeMcpPages(profileName, profileOptions);
|
||||||
const page = pages.find((entry) => entry.id === pageId);
|
const page = pages.find((entry) => entry.id === pageId);
|
||||||
if (!page) {
|
if (!page) {
|
||||||
throw new BrowserTabNotFoundError();
|
throw new BrowserTabNotFoundError();
|
||||||
@@ -632,10 +744,10 @@ async function findPageById(
|
|||||||
|
|
||||||
export async function ensureChromeMcpAvailable(
|
export async function ensureChromeMcpAvailable(
|
||||||
profileName: string,
|
profileName: string,
|
||||||
userDataDir?: string,
|
profileOptions?: string | ChromeMcpProfileOptions,
|
||||||
options: ChromeMcpCallOptions = {},
|
options: ChromeMcpCallOptions = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const lease = await leaseSession(profileName, userDataDir, options);
|
const lease = await leaseSession(profileName, profileOptions, options);
|
||||||
if (lease.temporary) {
|
if (lease.temporary) {
|
||||||
await lease.session.client.close().catch(() => {});
|
await lease.session.client.close().catch(() => {});
|
||||||
}
|
}
|
||||||
@@ -663,28 +775,28 @@ export async function stopAllChromeMcpSessions(): Promise<void> {
|
|||||||
|
|
||||||
export async function listChromeMcpPages(
|
export async function listChromeMcpPages(
|
||||||
profileName: string,
|
profileName: string,
|
||||||
userDataDir?: string,
|
profileOptions?: string | ChromeMcpProfileOptions,
|
||||||
options: ChromeMcpCallOptions = {},
|
options: ChromeMcpCallOptions = {},
|
||||||
): Promise<ChromeMcpStructuredPage[]> {
|
): Promise<ChromeMcpStructuredPage[]> {
|
||||||
const result = await callTool(profileName, userDataDir, "list_pages", {}, options);
|
const result = await callTool(profileName, profileOptions, "list_pages", {}, options);
|
||||||
return extractStructuredPages(result);
|
return extractStructuredPages(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listChromeMcpTabs(
|
export async function listChromeMcpTabs(
|
||||||
profileName: string,
|
profileName: string,
|
||||||
userDataDir?: string,
|
profileOptions?: string | ChromeMcpProfileOptions,
|
||||||
options: ChromeMcpCallOptions = {},
|
options: ChromeMcpCallOptions = {},
|
||||||
): Promise<BrowserTab[]> {
|
): Promise<BrowserTab[]> {
|
||||||
return toBrowserTabs(await listChromeMcpPages(profileName, userDataDir, options));
|
return toBrowserTabs(await listChromeMcpPages(profileName, profileOptions, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openChromeMcpTab(
|
export async function openChromeMcpTab(
|
||||||
profileName: string,
|
profileName: string,
|
||||||
url: string,
|
url: string,
|
||||||
userDataDir?: string,
|
profileOptions?: string | ChromeMcpProfileOptions,
|
||||||
): Promise<BrowserTab> {
|
): Promise<BrowserTab> {
|
||||||
const targetUrl = url.trim() || "about:blank";
|
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",
|
url: "about:blank",
|
||||||
timeout: CHROME_MCP_NEW_PAGE_TIMEOUT_MS,
|
timeout: CHROME_MCP_NEW_PAGE_TIMEOUT_MS,
|
||||||
});
|
});
|
||||||
@@ -700,7 +812,8 @@ export async function openChromeMcpTab(
|
|||||||
: (
|
: (
|
||||||
await navigateChromeMcpPage({
|
await navigateChromeMcpPage({
|
||||||
profileName,
|
profileName,
|
||||||
userDataDir,
|
profile: typeof profileOptions === "string" ? undefined : profileOptions,
|
||||||
|
userDataDir: typeof profileOptions === "string" ? profileOptions : undefined,
|
||||||
targetId,
|
targetId,
|
||||||
url: targetUrl,
|
url: targetUrl,
|
||||||
timeoutMs: CHROME_MCP_NAVIGATE_TIMEOUT_MS,
|
timeoutMs: CHROME_MCP_NAVIGATE_TIMEOUT_MS,
|
||||||
@@ -717,9 +830,9 @@ export async function openChromeMcpTab(
|
|||||||
export async function focusChromeMcpTab(
|
export async function focusChromeMcpTab(
|
||||||
profileName: string,
|
profileName: string,
|
||||||
targetId: string,
|
targetId: string,
|
||||||
userDataDir?: string,
|
profileOptions?: string | ChromeMcpProfileOptions,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await callTool(profileName, userDataDir, "select_page", {
|
await callTool(profileName, profileOptions, "select_page", {
|
||||||
pageId: parsePageId(targetId),
|
pageId: parsePageId(targetId),
|
||||||
bringToFront: true,
|
bringToFront: true,
|
||||||
});
|
});
|
||||||
@@ -728,13 +841,14 @@ export async function focusChromeMcpTab(
|
|||||||
export async function closeChromeMcpTab(
|
export async function closeChromeMcpTab(
|
||||||
profileName: string,
|
profileName: string,
|
||||||
targetId: string,
|
targetId: string,
|
||||||
userDataDir?: string,
|
profileOptions?: string | ChromeMcpProfileOptions,
|
||||||
): Promise<void> {
|
): 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: {
|
export async function navigateChromeMcpPage(params: {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profile?: ChromeMcpProfileOptions;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
url: string;
|
url: string;
|
||||||
@@ -743,7 +857,7 @@ export async function navigateChromeMcpPage(params: {
|
|||||||
const resolvedTimeoutMs = params.timeoutMs ?? CHROME_MCP_NAVIGATE_TIMEOUT_MS;
|
const resolvedTimeoutMs = params.timeoutMs ?? CHROME_MCP_NAVIGATE_TIMEOUT_MS;
|
||||||
await callTool(
|
await callTool(
|
||||||
params.profileName,
|
params.profileName,
|
||||||
params.userDataDir,
|
chromeMcpProfileOptionsFromParams(params),
|
||||||
"navigate_page",
|
"navigate_page",
|
||||||
{
|
{
|
||||||
pageId: parsePageId(params.targetId),
|
pageId: parsePageId(params.targetId),
|
||||||
@@ -756,24 +870,31 @@ export async function navigateChromeMcpPage(params: {
|
|||||||
const page = await findPageById(
|
const page = await findPageById(
|
||||||
params.profileName,
|
params.profileName,
|
||||||
parsePageId(params.targetId),
|
parsePageId(params.targetId),
|
||||||
params.userDataDir,
|
chromeMcpProfileOptionsFromParams(params),
|
||||||
);
|
);
|
||||||
return { url: page.url ?? params.url };
|
return { url: page.url ?? params.url };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function takeChromeMcpSnapshot(params: {
|
export async function takeChromeMcpSnapshot(params: {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profile?: ChromeMcpProfileOptions;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
}): Promise<ChromeMcpSnapshotNode> {
|
}): Promise<ChromeMcpSnapshotNode> {
|
||||||
const result = await callTool(params.profileName, params.userDataDir, "take_snapshot", {
|
const result = await callTool(
|
||||||
pageId: parsePageId(params.targetId),
|
params.profileName,
|
||||||
});
|
chromeMcpProfileOptionsFromParams(params),
|
||||||
|
"take_snapshot",
|
||||||
|
{
|
||||||
|
pageId: parsePageId(params.targetId),
|
||||||
|
},
|
||||||
|
);
|
||||||
return extractSnapshot(result);
|
return extractSnapshot(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function takeChromeMcpScreenshot(params: {
|
export async function takeChromeMcpScreenshot(params: {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profile?: ChromeMcpProfileOptions;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
uid?: string;
|
uid?: string;
|
||||||
@@ -784,7 +905,7 @@ export async function takeChromeMcpScreenshot(params: {
|
|||||||
return await withTempFile(async (filePath) => {
|
return await withTempFile(async (filePath) => {
|
||||||
await callTool(
|
await callTool(
|
||||||
params.profileName,
|
params.profileName,
|
||||||
params.userDataDir,
|
chromeMcpProfileOptionsFromParams(params),
|
||||||
"take_screenshot",
|
"take_screenshot",
|
||||||
{
|
{
|
||||||
pageId: parsePageId(params.targetId),
|
pageId: parsePageId(params.targetId),
|
||||||
@@ -801,6 +922,7 @@ export async function takeChromeMcpScreenshot(params: {
|
|||||||
|
|
||||||
export async function clickChromeMcpElement(params: {
|
export async function clickChromeMcpElement(params: {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profile?: ChromeMcpProfileOptions;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
uid: string;
|
uid: string;
|
||||||
@@ -810,7 +932,7 @@ export async function clickChromeMcpElement(params: {
|
|||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
await callTool(
|
await callTool(
|
||||||
params.profileName,
|
params.profileName,
|
||||||
params.userDataDir,
|
chromeMcpProfileOptionsFromParams(params),
|
||||||
"click",
|
"click",
|
||||||
{
|
{
|
||||||
pageId: parsePageId(params.targetId),
|
pageId: parsePageId(params.targetId),
|
||||||
@@ -826,6 +948,7 @@ export async function clickChromeMcpElement(params: {
|
|||||||
|
|
||||||
export async function clickChromeMcpCoords(params: {
|
export async function clickChromeMcpCoords(params: {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profile?: ChromeMcpProfileOptions;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
x: number;
|
x: number;
|
||||||
@@ -843,6 +966,7 @@ export async function clickChromeMcpCoords(params: {
|
|||||||
const doubleClick = params.doubleClick ? "true" : "false";
|
const doubleClick = params.doubleClick ? "true" : "false";
|
||||||
await evaluateChromeMcpScript({
|
await evaluateChromeMcpScript({
|
||||||
profileName: params.profileName,
|
profileName: params.profileName,
|
||||||
|
profile: params.profile,
|
||||||
userDataDir: params.userDataDir,
|
userDataDir: params.userDataDir,
|
||||||
targetId: params.targetId,
|
targetId: params.targetId,
|
||||||
fn: `async () => {
|
fn: `async () => {
|
||||||
@@ -885,12 +1009,13 @@ export async function clickChromeMcpCoords(params: {
|
|||||||
|
|
||||||
export async function fillChromeMcpElement(params: {
|
export async function fillChromeMcpElement(params: {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profile?: ChromeMcpProfileOptions;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
uid: string;
|
uid: string;
|
||||||
value: string;
|
value: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
await callTool(params.profileName, params.userDataDir, "fill", {
|
await callTool(params.profileName, chromeMcpProfileOptionsFromParams(params), "fill", {
|
||||||
pageId: parsePageId(params.targetId),
|
pageId: parsePageId(params.targetId),
|
||||||
uid: params.uid,
|
uid: params.uid,
|
||||||
value: params.value,
|
value: params.value,
|
||||||
@@ -899,11 +1024,12 @@ export async function fillChromeMcpElement(params: {
|
|||||||
|
|
||||||
export async function fillChromeMcpForm(params: {
|
export async function fillChromeMcpForm(params: {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profile?: ChromeMcpProfileOptions;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
elements: Array<{ uid: string; value: string }>;
|
elements: Array<{ uid: string; value: string }>;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
await callTool(params.profileName, params.userDataDir, "fill_form", {
|
await callTool(params.profileName, chromeMcpProfileOptionsFromParams(params), "fill_form", {
|
||||||
pageId: parsePageId(params.targetId),
|
pageId: parsePageId(params.targetId),
|
||||||
elements: params.elements,
|
elements: params.elements,
|
||||||
});
|
});
|
||||||
@@ -911,11 +1037,12 @@ export async function fillChromeMcpForm(params: {
|
|||||||
|
|
||||||
export async function hoverChromeMcpElement(params: {
|
export async function hoverChromeMcpElement(params: {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profile?: ChromeMcpProfileOptions;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
uid: string;
|
uid: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
await callTool(params.profileName, params.userDataDir, "hover", {
|
await callTool(params.profileName, chromeMcpProfileOptionsFromParams(params), "hover", {
|
||||||
pageId: parsePageId(params.targetId),
|
pageId: parsePageId(params.targetId),
|
||||||
uid: params.uid,
|
uid: params.uid,
|
||||||
});
|
});
|
||||||
@@ -923,12 +1050,13 @@ export async function hoverChromeMcpElement(params: {
|
|||||||
|
|
||||||
export async function dragChromeMcpElement(params: {
|
export async function dragChromeMcpElement(params: {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profile?: ChromeMcpProfileOptions;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
fromUid: string;
|
fromUid: string;
|
||||||
toUid: string;
|
toUid: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
await callTool(params.profileName, params.userDataDir, "drag", {
|
await callTool(params.profileName, chromeMcpProfileOptionsFromParams(params), "drag", {
|
||||||
pageId: parsePageId(params.targetId),
|
pageId: parsePageId(params.targetId),
|
||||||
from_uid: params.fromUid,
|
from_uid: params.fromUid,
|
||||||
to_uid: params.toUid,
|
to_uid: params.toUid,
|
||||||
@@ -937,12 +1065,13 @@ export async function dragChromeMcpElement(params: {
|
|||||||
|
|
||||||
export async function uploadChromeMcpFile(params: {
|
export async function uploadChromeMcpFile(params: {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profile?: ChromeMcpProfileOptions;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
uid: string;
|
uid: string;
|
||||||
filePath: string;
|
filePath: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
await callTool(params.profileName, params.userDataDir, "upload_file", {
|
await callTool(params.profileName, chromeMcpProfileOptionsFromParams(params), "upload_file", {
|
||||||
pageId: parsePageId(params.targetId),
|
pageId: parsePageId(params.targetId),
|
||||||
uid: params.uid,
|
uid: params.uid,
|
||||||
filePath: params.filePath,
|
filePath: params.filePath,
|
||||||
@@ -951,11 +1080,12 @@ export async function uploadChromeMcpFile(params: {
|
|||||||
|
|
||||||
export async function pressChromeMcpKey(params: {
|
export async function pressChromeMcpKey(params: {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profile?: ChromeMcpProfileOptions;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
key: string;
|
key: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
await callTool(params.profileName, params.userDataDir, "press_key", {
|
await callTool(params.profileName, chromeMcpProfileOptionsFromParams(params), "press_key", {
|
||||||
pageId: parsePageId(params.targetId),
|
pageId: parsePageId(params.targetId),
|
||||||
key: params.key,
|
key: params.key,
|
||||||
});
|
});
|
||||||
@@ -963,12 +1093,13 @@ export async function pressChromeMcpKey(params: {
|
|||||||
|
|
||||||
export async function resizeChromeMcpPage(params: {
|
export async function resizeChromeMcpPage(params: {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profile?: ChromeMcpProfileOptions;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
await callTool(params.profileName, params.userDataDir, "resize_page", {
|
await callTool(params.profileName, chromeMcpProfileOptionsFromParams(params), "resize_page", {
|
||||||
pageId: parsePageId(params.targetId),
|
pageId: parsePageId(params.targetId),
|
||||||
width: params.width,
|
width: params.width,
|
||||||
height: params.height,
|
height: params.height,
|
||||||
@@ -977,12 +1108,13 @@ export async function resizeChromeMcpPage(params: {
|
|||||||
|
|
||||||
export async function handleChromeMcpDialog(params: {
|
export async function handleChromeMcpDialog(params: {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profile?: ChromeMcpProfileOptions;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
action: "accept" | "dismiss";
|
action: "accept" | "dismiss";
|
||||||
promptText?: string;
|
promptText?: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
await callTool(params.profileName, params.userDataDir, "handle_dialog", {
|
await callTool(params.profileName, chromeMcpProfileOptionsFromParams(params), "handle_dialog", {
|
||||||
pageId: parsePageId(params.targetId),
|
pageId: parsePageId(params.targetId),
|
||||||
action: params.action,
|
action: params.action,
|
||||||
...(params.promptText ? { promptText: params.promptText } : {}),
|
...(params.promptText ? { promptText: params.promptText } : {}),
|
||||||
@@ -991,27 +1123,34 @@ export async function handleChromeMcpDialog(params: {
|
|||||||
|
|
||||||
export async function evaluateChromeMcpScript(params: {
|
export async function evaluateChromeMcpScript(params: {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profile?: ChromeMcpProfileOptions;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
fn: string;
|
fn: string;
|
||||||
args?: string[];
|
args?: string[];
|
||||||
}): Promise<unknown> {
|
}): Promise<unknown> {
|
||||||
const result = await callTool(params.profileName, params.userDataDir, "evaluate_script", {
|
const result = await callTool(
|
||||||
pageId: parsePageId(params.targetId),
|
params.profileName,
|
||||||
function: params.fn,
|
chromeMcpProfileOptionsFromParams(params),
|
||||||
...(params.args?.length ? { args: params.args } : {}),
|
"evaluate_script",
|
||||||
});
|
{
|
||||||
|
pageId: parsePageId(params.targetId),
|
||||||
|
function: params.fn,
|
||||||
|
...(params.args?.length ? { args: params.args } : {}),
|
||||||
|
},
|
||||||
|
);
|
||||||
return extractJsonMessage(result);
|
return extractJsonMessage(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function waitForChromeMcpText(params: {
|
export async function waitForChromeMcpText(params: {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profile?: ChromeMcpProfileOptions;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
text: string[];
|
text: string[];
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
await callTool(params.profileName, params.userDataDir, "wait_for", {
|
await callTool(params.profileName, chromeMcpProfileOptionsFromParams(params), "wait_for", {
|
||||||
pageId: parsePageId(params.targetId),
|
pageId: parsePageId(params.targetId),
|
||||||
text: params.text,
|
text: params.text,
|
||||||
...(typeof params.timeoutMs === "number" ? { timeout: params.timeoutMs } : {}),
|
...(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", () => {
|
it("sets usesChromeMcp only for existing-session profiles", () => {
|
||||||
const resolved = resolveBrowserConfig({
|
const resolved = resolveBrowserConfig({
|
||||||
profiles: {
|
profiles: {
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ export type ResolvedBrowserProfile = {
|
|||||||
cdpHost: string;
|
cdpHost: string;
|
||||||
cdpIsLoopback: boolean;
|
cdpIsLoopback: boolean;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
|
mcpCommand?: string;
|
||||||
|
mcpArgs?: string[];
|
||||||
color: string;
|
color: string;
|
||||||
driver: "openclaw" | "existing-session";
|
driver: "openclaw" | "existing-session";
|
||||||
executablePath?: string;
|
executablePath?: string;
|
||||||
@@ -180,6 +182,37 @@ function normalizeExecutablePath(raw: string | undefined): string | undefined {
|
|||||||
return path.resolve(value.replace(/^~(?=$|[\\/])/, os.homedir()));
|
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 {
|
function hasLinuxDisplay(env: NodeJS.ProcessEnv): boolean {
|
||||||
return Boolean(env.DISPLAY?.trim() || env.WAYLAND_DISPLAY?.trim());
|
return Boolean(env.DISPLAY?.trim() || env.WAYLAND_DISPLAY?.trim());
|
||||||
}
|
}
|
||||||
@@ -442,13 +475,16 @@ export function resolveProfile(
|
|||||||
const executablePath = normalizeExecutablePath(profile.executablePath) ?? resolved.executablePath;
|
const executablePath = normalizeExecutablePath(profile.executablePath) ?? resolved.executablePath;
|
||||||
|
|
||||||
if (driver === "existing-session") {
|
if (driver === "existing-session") {
|
||||||
|
const existingSessionCdp = normalizeExistingSessionCdpUrl(rawProfileUrl, profileName);
|
||||||
return {
|
return {
|
||||||
name: profileName,
|
name: profileName,
|
||||||
cdpPort: 0,
|
cdpPort: 0,
|
||||||
cdpUrl: "",
|
cdpUrl: existingSessionCdp?.cdpUrl ?? "",
|
||||||
cdpHost: "",
|
cdpHost: existingSessionCdp?.cdpHost ?? "",
|
||||||
cdpIsLoopback: true,
|
cdpIsLoopback: existingSessionCdp?.cdpIsLoopback ?? true,
|
||||||
userDataDir: resolveUserPath(profile.userDataDir?.trim() || "") || undefined,
|
userDataDir: resolveUserPath(profile.userDataDir?.trim() || "") || undefined,
|
||||||
|
mcpCommand: normalizeOptionalString(profile.mcpCommand),
|
||||||
|
mcpArgs: normalizeStringList(profile.mcpArgs) ?? undefined,
|
||||||
color: profile.color,
|
color: profile.color,
|
||||||
driver,
|
driver,
|
||||||
executablePath,
|
executablePath,
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function registerBrowserAgentActHookRoutes(
|
|||||||
}
|
}
|
||||||
await uploadChromeMcpFile({
|
await uploadChromeMcpFile({
|
||||||
profileName: profileCtx.profile.name,
|
profileName: profileCtx.profile.name,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
uid,
|
uid,
|
||||||
filePath: resolvedPaths[0] ?? "",
|
filePath: resolvedPaths[0] ?? "",
|
||||||
@@ -137,7 +137,7 @@ export function registerBrowserAgentActHookRoutes(
|
|||||||
}
|
}
|
||||||
await evaluateChromeMcpScript({
|
await evaluateChromeMcpScript({
|
||||||
profileName: profileCtx.profile.name,
|
profileName: profileCtx.profile.name,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
fn: `() => {
|
fn: `() => {
|
||||||
const state = (window.__openclawDialogHook ??= {});
|
const state = (window.__openclawDialogHook ??= {});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
hoverChromeMcpElement,
|
hoverChromeMcpElement,
|
||||||
pressChromeMcpKey,
|
pressChromeMcpKey,
|
||||||
resizeChromeMcpPage,
|
resizeChromeMcpPage,
|
||||||
|
type ChromeMcpProfileOptions,
|
||||||
} from "../chrome-mcp.js";
|
} from "../chrome-mcp.js";
|
||||||
import type { BrowserActRequest } from "../client-actions.types.js";
|
import type { BrowserActRequest } from "../client-actions.types.js";
|
||||||
import {
|
import {
|
||||||
@@ -48,11 +49,13 @@ const EXISTING_SESSION_INTERACTION_NAVIGATION_RECHECK_DELAYS_MS = [0, 250, 500]
|
|||||||
|
|
||||||
async function readExistingSessionLocationHref(params: {
|
async function readExistingSessionLocationHref(params: {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profile?: ChromeMcpProfileOptions;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
const currentUrl = await evaluateChromeMcpScript({
|
const currentUrl = await evaluateChromeMcpScript({
|
||||||
profileName: params.profileName,
|
profileName: params.profileName,
|
||||||
|
profile: params.profile,
|
||||||
userDataDir: params.userDataDir,
|
userDataDir: params.userDataDir,
|
||||||
targetId: params.targetId,
|
targetId: params.targetId,
|
||||||
fn: "() => window.location.href",
|
fn: "() => window.location.href",
|
||||||
@@ -69,6 +72,7 @@ async function readExistingSessionLocationHref(params: {
|
|||||||
|
|
||||||
async function assertExistingSessionPostInteractionNavigationAllowed(params: {
|
async function assertExistingSessionPostInteractionNavigationAllowed(params: {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profile?: ChromeMcpProfileOptions;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
ssrfPolicy?: BrowserNavigationPolicyOptions["ssrfPolicy"];
|
ssrfPolicy?: BrowserNavigationPolicyOptions["ssrfPolicy"];
|
||||||
@@ -208,6 +212,7 @@ function buildExistingSessionWaitPredicate(params: {
|
|||||||
|
|
||||||
async function waitForExistingSessionCondition(params: {
|
async function waitForExistingSessionCondition(params: {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profile?: ChromeMcpProfileOptions;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
timeMs?: number;
|
timeMs?: number;
|
||||||
@@ -234,6 +239,7 @@ async function waitForExistingSessionCondition(params: {
|
|||||||
ready = Boolean(
|
ready = Boolean(
|
||||||
await evaluateChromeMcpScript({
|
await evaluateChromeMcpScript({
|
||||||
profileName: params.profileName,
|
profileName: params.profileName,
|
||||||
|
profile: params.profile,
|
||||||
userDataDir: params.userDataDir,
|
userDataDir: params.userDataDir,
|
||||||
targetId: params.targetId,
|
targetId: params.targetId,
|
||||||
fn: `async () => ${predicate}`,
|
fn: `async () => ${predicate}`,
|
||||||
@@ -243,6 +249,7 @@ async function waitForExistingSessionCondition(params: {
|
|||||||
if (ready && params.url) {
|
if (ready && params.url) {
|
||||||
const currentUrl = await evaluateChromeMcpScript({
|
const currentUrl = await evaluateChromeMcpScript({
|
||||||
profileName: params.profileName,
|
profileName: params.profileName,
|
||||||
|
profile: params.profile,
|
||||||
userDataDir: params.userDataDir,
|
userDataDir: params.userDataDir,
|
||||||
targetId: params.targetId,
|
targetId: params.targetId,
|
||||||
fn: "() => window.location.href",
|
fn: "() => window.location.href",
|
||||||
@@ -406,7 +413,7 @@ export function registerBrowserAgentActRoutes(
|
|||||||
: new Set<string>();
|
: new Set<string>();
|
||||||
const existingSessionNavigationGuard = {
|
const existingSessionNavigationGuard = {
|
||||||
profileName,
|
profileName,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
ssrfPolicy,
|
ssrfPolicy,
|
||||||
listTabs: () => profileCtx.listTabs(),
|
listTabs: () => profileCtx.listTabs(),
|
||||||
@@ -427,7 +434,7 @@ export function registerBrowserAgentActRoutes(
|
|||||||
execute: () =>
|
execute: () =>
|
||||||
clickChromeMcpElement({
|
clickChromeMcpElement({
|
||||||
profileName,
|
profileName,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
uid: action.ref!,
|
uid: action.ref!,
|
||||||
doubleClick: action.doubleClick ?? false,
|
doubleClick: action.doubleClick ?? false,
|
||||||
@@ -442,7 +449,7 @@ export function registerBrowserAgentActRoutes(
|
|||||||
execute: () =>
|
execute: () =>
|
||||||
clickChromeMcpCoords({
|
clickChromeMcpCoords({
|
||||||
profileName,
|
profileName,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
x: action.x,
|
x: action.x,
|
||||||
y: action.y,
|
y: action.y,
|
||||||
@@ -458,7 +465,7 @@ export function registerBrowserAgentActRoutes(
|
|||||||
execute: async () => {
|
execute: async () => {
|
||||||
await fillChromeMcpElement({
|
await fillChromeMcpElement({
|
||||||
profileName,
|
profileName,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
uid: action.ref!,
|
uid: action.ref!,
|
||||||
value: action.text,
|
value: action.text,
|
||||||
@@ -466,7 +473,7 @@ export function registerBrowserAgentActRoutes(
|
|||||||
if (action.submit) {
|
if (action.submit) {
|
||||||
await pressChromeMcpKey({
|
await pressChromeMcpKey({
|
||||||
profileName,
|
profileName,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
key: "Enter",
|
key: "Enter",
|
||||||
});
|
});
|
||||||
@@ -480,7 +487,7 @@ export function registerBrowserAgentActRoutes(
|
|||||||
execute: () =>
|
execute: () =>
|
||||||
pressChromeMcpKey({
|
pressChromeMcpKey({
|
||||||
profileName,
|
profileName,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
key: action.key,
|
key: action.key,
|
||||||
}),
|
}),
|
||||||
@@ -492,7 +499,7 @@ export function registerBrowserAgentActRoutes(
|
|||||||
execute: () =>
|
execute: () =>
|
||||||
hoverChromeMcpElement({
|
hoverChromeMcpElement({
|
||||||
profileName,
|
profileName,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
uid: action.ref!,
|
uid: action.ref!,
|
||||||
}),
|
}),
|
||||||
@@ -504,7 +511,7 @@ export function registerBrowserAgentActRoutes(
|
|||||||
execute: () =>
|
execute: () =>
|
||||||
evaluateChromeMcpScript({
|
evaluateChromeMcpScript({
|
||||||
profileName,
|
profileName,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
fn: `(el) => { el.scrollIntoView({ block: "center", inline: "center" }); return true; }`,
|
fn: `(el) => { el.scrollIntoView({ block: "center", inline: "center" }); return true; }`,
|
||||||
args: [action.ref!],
|
args: [action.ref!],
|
||||||
@@ -517,7 +524,7 @@ export function registerBrowserAgentActRoutes(
|
|||||||
execute: () =>
|
execute: () =>
|
||||||
dragChromeMcpElement({
|
dragChromeMcpElement({
|
||||||
profileName,
|
profileName,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
fromUid: action.startRef!,
|
fromUid: action.startRef!,
|
||||||
toUid: action.endRef!,
|
toUid: action.endRef!,
|
||||||
@@ -530,7 +537,7 @@ export function registerBrowserAgentActRoutes(
|
|||||||
execute: () =>
|
execute: () =>
|
||||||
fillChromeMcpElement({
|
fillChromeMcpElement({
|
||||||
profileName,
|
profileName,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
uid: action.ref!,
|
uid: action.ref!,
|
||||||
value: action.values[0] ?? "",
|
value: action.values[0] ?? "",
|
||||||
@@ -543,7 +550,7 @@ export function registerBrowserAgentActRoutes(
|
|||||||
execute: () =>
|
execute: () =>
|
||||||
fillChromeMcpForm({
|
fillChromeMcpForm({
|
||||||
profileName,
|
profileName,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
elements: action.fields.map((field) => ({
|
elements: action.fields.map((field) => ({
|
||||||
uid: field.ref,
|
uid: field.ref,
|
||||||
@@ -556,7 +563,7 @@ export function registerBrowserAgentActRoutes(
|
|||||||
case "resize":
|
case "resize":
|
||||||
await resizeChromeMcpPage({
|
await resizeChromeMcpPage({
|
||||||
profileName,
|
profileName,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
width: action.width,
|
width: action.width,
|
||||||
height: action.height,
|
height: action.height,
|
||||||
@@ -565,7 +572,7 @@ export function registerBrowserAgentActRoutes(
|
|||||||
case "wait":
|
case "wait":
|
||||||
await waitForExistingSessionCondition({
|
await waitForExistingSessionCondition({
|
||||||
profileName,
|
profileName,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
timeMs: action.timeMs,
|
timeMs: action.timeMs,
|
||||||
text: action.text,
|
text: action.text,
|
||||||
@@ -582,7 +589,7 @@ export function registerBrowserAgentActRoutes(
|
|||||||
execute: () =>
|
execute: () =>
|
||||||
evaluateChromeMcpScript({
|
evaluateChromeMcpScript({
|
||||||
profileName,
|
profileName,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
fn: action.fn,
|
fn: action.fn,
|
||||||
args: action.ref ? [action.ref] : undefined,
|
args: action.ref ? [action.ref] : undefined,
|
||||||
@@ -592,7 +599,7 @@ export function registerBrowserAgentActRoutes(
|
|||||||
return await jsonOk({ result });
|
return await jsonOk({ result });
|
||||||
}
|
}
|
||||||
case "close":
|
case "close":
|
||||||
await closeChromeMcpTab(profileName, tab.targetId, profileCtx.profile.userDataDir);
|
await closeChromeMcpTab(profileName, tab.targetId, profileCtx.profile);
|
||||||
return await jsonOk();
|
return await jsonOk();
|
||||||
case "batch":
|
case "batch":
|
||||||
return jsonActError(
|
return jsonActError(
|
||||||
@@ -713,7 +720,7 @@ export function registerBrowserAgentActRoutes(
|
|||||||
if (getBrowserProfileCapabilities(profileCtx.profile).usesChromeMcp) {
|
if (getBrowserProfileCapabilities(profileCtx.profile).usesChromeMcp) {
|
||||||
await evaluateChromeMcpScript({
|
await evaluateChromeMcpScript({
|
||||||
profileName: profileCtx.profile.name,
|
profileName: profileCtx.profile.name,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
args: [ref],
|
args: [ref],
|
||||||
fn: `(el) => {
|
fn: `(el) => {
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ describe("existing-session browser routes", () => {
|
|||||||
});
|
});
|
||||||
expect(chromeMcpMocks.takeChromeMcpSnapshot).toHaveBeenCalledWith({
|
expect(chromeMcpMocks.takeChromeMcpSnapshot).toHaveBeenCalledWith({
|
||||||
profileName: "chrome-live",
|
profileName: "chrome-live",
|
||||||
|
profile: expect.objectContaining({ name: "chrome-live", driver: "existing-session" }),
|
||||||
targetId: "7",
|
targetId: "7",
|
||||||
});
|
});
|
||||||
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).not.toHaveBeenCalled();
|
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).not.toHaveBeenCalled();
|
||||||
@@ -166,6 +167,7 @@ describe("existing-session browser routes", () => {
|
|||||||
});
|
});
|
||||||
expect(chromeMcpMocks.takeChromeMcpScreenshot).toHaveBeenCalledWith({
|
expect(chromeMcpMocks.takeChromeMcpScreenshot).toHaveBeenCalledWith({
|
||||||
profileName: "chrome-live",
|
profileName: "chrome-live",
|
||||||
|
profile: expect.objectContaining({ name: "chrome-live", driver: "existing-session" }),
|
||||||
targetId: "7",
|
targetId: "7",
|
||||||
uid: "btn-1",
|
uid: "btn-1",
|
||||||
fullPage: false,
|
fullPage: false,
|
||||||
@@ -285,6 +287,8 @@ describe("existing-session browser routes", () => {
|
|||||||
expect(response.body).toMatchObject({ ok: true, targetId: "7" });
|
expect(response.body).toMatchObject({ ok: true, targetId: "7" });
|
||||||
expect(chromeMcpMocks.evaluateChromeMcpScript).toHaveBeenCalledWith({
|
expect(chromeMcpMocks.evaluateChromeMcpScript).toHaveBeenCalledWith({
|
||||||
profileName: "chrome-live",
|
profileName: "chrome-live",
|
||||||
|
profile: expect.objectContaining({ name: "chrome-live", driver: "existing-session" }),
|
||||||
|
userDataDir: undefined,
|
||||||
targetId: "7",
|
targetId: "7",
|
||||||
fn: "() => window.location.href",
|
fn: "() => window.location.href",
|
||||||
});
|
});
|
||||||
@@ -308,7 +312,7 @@ describe("existing-session browser routes", () => {
|
|||||||
expect(response.statusCode).toBe(200);
|
expect(response.statusCode).toBe(200);
|
||||||
expect(chromeMcpMocks.clickChromeMcpElement).toHaveBeenCalledWith({
|
expect(chromeMcpMocks.clickChromeMcpElement).toHaveBeenCalledWith({
|
||||||
profileName: "chrome-live",
|
profileName: "chrome-live",
|
||||||
userDataDir: undefined,
|
profile: expect.objectContaining({ name: "chrome-live", driver: "existing-session" }),
|
||||||
targetId: "7",
|
targetId: "7",
|
||||||
uid: "btn-1",
|
uid: "btn-1",
|
||||||
doubleClick: false,
|
doubleClick: false,
|
||||||
@@ -334,7 +338,7 @@ describe("existing-session browser routes", () => {
|
|||||||
expect(response.body).toMatchObject({ ok: true, targetId: "7", url: "https://example.com" });
|
expect(response.body).toMatchObject({ ok: true, targetId: "7", url: "https://example.com" });
|
||||||
expect(chromeMcpMocks.clickChromeMcpCoords).toHaveBeenCalledWith({
|
expect(chromeMcpMocks.clickChromeMcpCoords).toHaveBeenCalledWith({
|
||||||
profileName: "chrome-live",
|
profileName: "chrome-live",
|
||||||
userDataDir: undefined,
|
profile: expect.objectContaining({ name: "chrome-live", driver: "existing-session" }),
|
||||||
targetId: "7",
|
targetId: "7",
|
||||||
x: 25,
|
x: 25,
|
||||||
y: 32,
|
y: 32,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
navigateChromeMcpPage,
|
navigateChromeMcpPage,
|
||||||
takeChromeMcpScreenshot,
|
takeChromeMcpScreenshot,
|
||||||
takeChromeMcpSnapshot,
|
takeChromeMcpSnapshot,
|
||||||
|
type ChromeMcpProfileOptions,
|
||||||
} from "../chrome-mcp.js";
|
} from "../chrome-mcp.js";
|
||||||
import {
|
import {
|
||||||
buildAiSnapshotFromChromeMcpSnapshot,
|
buildAiSnapshotFromChromeMcpSnapshot,
|
||||||
@@ -57,11 +58,13 @@ function browserNavigationPolicyForProfile(ctx: BrowserRouteContext, profileCtx:
|
|||||||
|
|
||||||
async function collectChromeMcpSnapshotUrls(params: {
|
async function collectChromeMcpSnapshotUrls(params: {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profile?: ChromeMcpProfileOptions;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
}): Promise<Array<{ text: string; url: string }>> {
|
}): Promise<Array<{ text: string; url: string }>> {
|
||||||
const result = await evaluateChromeMcpScript({
|
const result = await evaluateChromeMcpScript({
|
||||||
profileName: params.profileName,
|
profileName: params.profileName,
|
||||||
|
profile: params.profile,
|
||||||
userDataDir: params.userDataDir,
|
userDataDir: params.userDataDir,
|
||||||
targetId: params.targetId,
|
targetId: params.targetId,
|
||||||
fn: `() => {
|
fn: `() => {
|
||||||
@@ -102,11 +105,13 @@ function appendSnapshotUrls(snapshot: string, urls: Array<{ text: string; url: s
|
|||||||
|
|
||||||
async function clearChromeMcpOverlay(params: {
|
async function clearChromeMcpOverlay(params: {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profile?: ChromeMcpProfileOptions;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
await evaluateChromeMcpScript({
|
await evaluateChromeMcpScript({
|
||||||
profileName: params.profileName,
|
profileName: params.profileName,
|
||||||
|
profile: params.profile,
|
||||||
userDataDir: params.userDataDir,
|
userDataDir: params.userDataDir,
|
||||||
targetId: params.targetId,
|
targetId: params.targetId,
|
||||||
fn: `() => {
|
fn: `() => {
|
||||||
@@ -118,6 +123,7 @@ async function clearChromeMcpOverlay(params: {
|
|||||||
|
|
||||||
async function renderChromeMcpLabels(params: {
|
async function renderChromeMcpLabels(params: {
|
||||||
profileName: string;
|
profileName: string;
|
||||||
|
profile?: ChromeMcpProfileOptions;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
refs: string[];
|
refs: string[];
|
||||||
@@ -125,6 +131,7 @@ async function renderChromeMcpLabels(params: {
|
|||||||
const refList = JSON.stringify(params.refs);
|
const refList = JSON.stringify(params.refs);
|
||||||
const result = await evaluateChromeMcpScript({
|
const result = await evaluateChromeMcpScript({
|
||||||
profileName: params.profileName,
|
profileName: params.profileName,
|
||||||
|
profile: params.profile,
|
||||||
userDataDir: params.userDataDir,
|
userDataDir: params.userDataDir,
|
||||||
targetId: params.targetId,
|
targetId: params.targetId,
|
||||||
args: params.refs,
|
args: params.refs,
|
||||||
@@ -265,7 +272,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
|||||||
await assertBrowserNavigationAllowed({ url, ...ssrfPolicyOpts });
|
await assertBrowserNavigationAllowed({ url, ...ssrfPolicyOpts });
|
||||||
const result = await navigateChromeMcpPage({
|
const result = await navigateChromeMcpPage({
|
||||||
profileName: profileCtx.profile.name,
|
profileName: profileCtx.profile.name,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
url,
|
url,
|
||||||
});
|
});
|
||||||
@@ -369,20 +376,20 @@ export function registerBrowserAgentSnapshotRoutes(
|
|||||||
if (labels) {
|
if (labels) {
|
||||||
const snapshot = await takeChromeMcpSnapshot({
|
const snapshot = await takeChromeMcpSnapshot({
|
||||||
profileName: profileCtx.profile.name,
|
profileName: profileCtx.profile.name,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
});
|
});
|
||||||
const built = buildAiSnapshotFromChromeMcpSnapshot({ root: snapshot });
|
const built = buildAiSnapshotFromChromeMcpSnapshot({ root: snapshot });
|
||||||
const labelResult = await renderChromeMcpLabels({
|
const labelResult = await renderChromeMcpLabels({
|
||||||
profileName: profileCtx.profile.name,
|
profileName: profileCtx.profile.name,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
refs: Object.keys(built.refs),
|
refs: Object.keys(built.refs),
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const buffer = await takeChromeMcpScreenshot({
|
const buffer = await takeChromeMcpScreenshot({
|
||||||
profileName: profileCtx.profile.name,
|
profileName: profileCtx.profile.name,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
fullPage,
|
fullPage,
|
||||||
format: type,
|
format: type,
|
||||||
@@ -401,7 +408,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
|||||||
} finally {
|
} finally {
|
||||||
await clearChromeMcpOverlay({
|
await clearChromeMcpOverlay({
|
||||||
profileName: profileCtx.profile.name,
|
profileName: profileCtx.profile.name,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -409,7 +416,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
|||||||
}
|
}
|
||||||
const buffer = await takeChromeMcpScreenshot({
|
const buffer = await takeChromeMcpScreenshot({
|
||||||
profileName: profileCtx.profile.name,
|
profileName: profileCtx.profile.name,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
uid: ref,
|
uid: ref,
|
||||||
fullPage,
|
fullPage,
|
||||||
@@ -531,7 +538,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
|||||||
}
|
}
|
||||||
const snapshot = await takeChromeMcpSnapshot({
|
const snapshot = await takeChromeMcpSnapshot({
|
||||||
profileName: profileCtx.profile.name,
|
profileName: profileCtx.profile.name,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
});
|
});
|
||||||
if (plan.format === "aria") {
|
if (plan.format === "aria") {
|
||||||
@@ -559,7 +566,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
|||||||
built.snapshot,
|
built.snapshot,
|
||||||
await collectChromeMcpSnapshotUrls({
|
await collectChromeMcpSnapshotUrls({
|
||||||
profileName: profileCtx.profile.name,
|
profileName: profileCtx.profile.name,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -569,14 +576,14 @@ export function registerBrowserAgentSnapshotRoutes(
|
|||||||
const refs = Object.keys(builtWithUrls.refs);
|
const refs = Object.keys(builtWithUrls.refs);
|
||||||
const labelResult = await renderChromeMcpLabels({
|
const labelResult = await renderChromeMcpLabels({
|
||||||
profileName: profileCtx.profile.name,
|
profileName: profileCtx.profile.name,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
refs,
|
refs,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const labeled = await takeChromeMcpScreenshot({
|
const labeled = await takeChromeMcpScreenshot({
|
||||||
profileName: profileCtx.profile.name,
|
profileName: profileCtx.profile.name,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
format: "png",
|
format: "png",
|
||||||
});
|
});
|
||||||
@@ -606,7 +613,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
|||||||
} finally {
|
} finally {
|
||||||
await clearChromeMcpOverlay({
|
await clearChromeMcpOverlay({
|
||||||
profileName: profileCtx.profile.name,
|
profileName: profileCtx.profile.name,
|
||||||
userDataDir: profileCtx.profile.userDataDir,
|
profile: profileCtx.profile,
|
||||||
targetId: tab.targetId,
|
targetId: tab.targetId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export function createProfileAvailability({
|
|||||||
if (capabilities.usesChromeMcp) {
|
if (capabilities.usesChromeMcp) {
|
||||||
// listChromeMcpTabs creates the session if needed — no separate ensureChromeMcpAvailable call required
|
// listChromeMcpTabs creates the session if needed — no separate ensureChromeMcpAvailable call required
|
||||||
const { listChromeMcpTabs } = await getChromeMcpModule();
|
const { listChromeMcpTabs } = await getChromeMcpModule();
|
||||||
await listChromeMcpTabs(profile.name, profile.userDataDir);
|
await listChromeMcpTabs(profile.name, profile);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const { httpTimeoutMs, wsTimeoutMs } = resolveTimeouts(timeoutMs);
|
const { httpTimeoutMs, wsTimeoutMs } = resolveTimeouts(timeoutMs);
|
||||||
@@ -105,7 +105,7 @@ export function createProfileAvailability({
|
|||||||
const isTransportAvailable = async (timeoutMs?: number) => {
|
const isTransportAvailable = async (timeoutMs?: number) => {
|
||||||
if (capabilities.usesChromeMcp) {
|
if (capabilities.usesChromeMcp) {
|
||||||
const { ensureChromeMcpAvailable } = await getChromeMcpModule();
|
const { ensureChromeMcpAvailable } = await getChromeMcpModule();
|
||||||
await ensureChromeMcpAvailable(profile.name, profile.userDataDir, {
|
await ensureChromeMcpAvailable(profile.name, profile, {
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
});
|
});
|
||||||
@@ -218,7 +218,7 @@ export function createProfileAvailability({
|
|||||||
while (Date.now() < deadlineMs) {
|
while (Date.now() < deadlineMs) {
|
||||||
try {
|
try {
|
||||||
const { listChromeMcpTabs } = await getChromeMcpModule();
|
const { listChromeMcpTabs } = await getChromeMcpModule();
|
||||||
await listChromeMcpTabs(profile.name, profile.userDataDir);
|
await listChromeMcpTabs(profile.name, profile);
|
||||||
return;
|
return;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
lastError = err;
|
lastError = err;
|
||||||
@@ -239,7 +239,7 @@ export function createProfileAvailability({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { ensureChromeMcpAvailable } = await getChromeMcpModule();
|
const { ensureChromeMcpAvailable } = await getChromeMcpModule();
|
||||||
await ensureChromeMcpAvailable(profile.name, profile.userDataDir);
|
await ensureChromeMcpAvailable(profile.name, profile);
|
||||||
await waitForChromeMcpReadyAfterAttach();
|
await waitForChromeMcpReadyAfterAttach();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,14 @@ function makeState(): BrowserServerState {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function expectChromeLiveProfile() {
|
||||||
|
return expect.objectContaining({
|
||||||
|
name: "chrome-live",
|
||||||
|
driver: "existing-session",
|
||||||
|
userDataDir: "/tmp/brave-profile",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
for (const key of [
|
for (const key of [
|
||||||
"ALL_PROXY",
|
"ALL_PROXY",
|
||||||
@@ -114,12 +122,16 @@ describe("browser server-context existing-session profile", () => {
|
|||||||
|
|
||||||
expect(chromeMcp.ensureChromeMcpAvailable).toHaveBeenCalledWith(
|
expect(chromeMcp.ensureChromeMcpAvailable).toHaveBeenCalledWith(
|
||||||
"chrome-live",
|
"chrome-live",
|
||||||
"/tmp/brave-profile",
|
expectChromeLiveProfile(),
|
||||||
{ ephemeral: true, timeoutMs: 300 },
|
{ ephemeral: true, timeoutMs: 300 },
|
||||||
);
|
);
|
||||||
expect(chromeMcp.listChromeMcpTabs).toHaveBeenCalledWith("chrome-live", "/tmp/brave-profile", {
|
expect(chromeMcp.listChromeMcpTabs).toHaveBeenCalledWith(
|
||||||
ephemeral: true,
|
"chrome-live",
|
||||||
});
|
expectChromeLiveProfile(),
|
||||||
|
{
|
||||||
|
ephemeral: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps the next real attach on the normal sticky session path after an idle status probe", async () => {
|
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(tabs.map((tab) => tab.targetId)).toEqual(["7"]);
|
||||||
expect(chromeMcp.ensureChromeMcpAvailable).toHaveBeenLastCalledWith(
|
expect(chromeMcp.ensureChromeMcpAvailable).toHaveBeenLastCalledWith(
|
||||||
"chrome-live",
|
"chrome-live",
|
||||||
"/tmp/brave-profile",
|
expectChromeLiveProfile(),
|
||||||
);
|
);
|
||||||
expect(chromeMcp.listChromeMcpTabs).toHaveBeenNthCalledWith(
|
expect(chromeMcp.listChromeMcpTabs).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
"chrome-live",
|
"chrome-live",
|
||||||
"/tmp/brave-profile",
|
expectChromeLiveProfile(),
|
||||||
);
|
);
|
||||||
expect(chromeMcp.listChromeMcpTabs).toHaveBeenNthCalledWith(
|
expect(chromeMcp.listChromeMcpTabs).toHaveBeenNthCalledWith(
|
||||||
2,
|
2,
|
||||||
"chrome-live",
|
"chrome-live",
|
||||||
"/tmp/brave-profile",
|
expectChromeLiveProfile(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -201,18 +213,21 @@ describe("browser server-context existing-session profile", () => {
|
|||||||
|
|
||||||
expect(chromeMcp.ensureChromeMcpAvailable).toHaveBeenCalledWith(
|
expect(chromeMcp.ensureChromeMcpAvailable).toHaveBeenCalledWith(
|
||||||
"chrome-live",
|
"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(
|
expect(chromeMcp.openChromeMcpTab).toHaveBeenCalledWith(
|
||||||
"chrome-live",
|
"chrome-live",
|
||||||
"about:blank",
|
"about:blank",
|
||||||
"/tmp/brave-profile",
|
expectChromeLiveProfile(),
|
||||||
);
|
);
|
||||||
expect(chromeMcp.focusChromeMcpTab).toHaveBeenCalledWith(
|
expect(chromeMcp.focusChromeMcpTab).toHaveBeenCalledWith(
|
||||||
"chrome-live",
|
"chrome-live",
|
||||||
"7",
|
"7",
|
||||||
"/tmp/brave-profile",
|
expectChromeLiveProfile(),
|
||||||
);
|
);
|
||||||
expect(chromeMcp.closeChromeMcpSession).toHaveBeenCalledWith("chrome-live");
|
expect(chromeMcp.closeChromeMcpSession).toHaveBeenCalledWith("chrome-live");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export function createProfileSelectionOps({
|
|||||||
|
|
||||||
if (capabilities.usesChromeMcp) {
|
if (capabilities.usesChromeMcp) {
|
||||||
const { focusChromeMcpTab } = await getChromeMcpModule();
|
const { focusChromeMcpTab } = await getChromeMcpModule();
|
||||||
await focusChromeMcpTab(profile.name, resolvedTargetId, profile.userDataDir);
|
await focusChromeMcpTab(profile.name, resolvedTargetId, profile);
|
||||||
const profileState = getProfileState();
|
const profileState = getProfileState();
|
||||||
profileState.lastTargetId = resolvedTargetId;
|
profileState.lastTargetId = resolvedTargetId;
|
||||||
return;
|
return;
|
||||||
@@ -136,7 +136,7 @@ export function createProfileSelectionOps({
|
|||||||
|
|
||||||
if (capabilities.usesChromeMcp) {
|
if (capabilities.usesChromeMcp) {
|
||||||
const { closeChromeMcpTab } = await getChromeMcpModule();
|
const { closeChromeMcpTab } = await getChromeMcpModule();
|
||||||
await closeChromeMcpTab(profile.name, resolvedTargetId, profile.userDataDir);
|
await closeChromeMcpTab(profile.name, resolvedTargetId, profile);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export function createProfileTabOps({
|
|||||||
const readTabs = async (): Promise<BrowserTab[]> => {
|
const readTabs = async (): Promise<BrowserTab[]> => {
|
||||||
if (capabilities.usesChromeMcp) {
|
if (capabilities.usesChromeMcp) {
|
||||||
const { listChromeMcpTabs } = await getChromeMcpModule();
|
const { listChromeMcpTabs } = await getChromeMcpModule();
|
||||||
return await listChromeMcpTabs(profile.name, profile.userDataDir);
|
return await listChromeMcpTabs(profile.name, profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (capabilities.usesPersistentPlaywright) {
|
if (capabilities.usesPersistentPlaywright) {
|
||||||
@@ -231,7 +231,7 @@ export function createProfileTabOps({
|
|||||||
if (capabilities.usesChromeMcp) {
|
if (capabilities.usesChromeMcp) {
|
||||||
await assertBrowserNavigationAllowed({ url, ...ssrfPolicyOpts });
|
await assertBrowserNavigationAllowed({ url, ...ssrfPolicyOpts });
|
||||||
const { openChromeMcpTab } = await getChromeMcpModule();
|
const { openChromeMcpTab } = await getChromeMcpModule();
|
||||||
const page = await openChromeMcpTab(profile.name, url, profile.userDataDir);
|
const page = await openChromeMcpTab(profile.name, url, profile);
|
||||||
const profileState = getProfileState();
|
const profileState = getProfileState();
|
||||||
profileState.lastTargetId = page.targetId;
|
profileState.lastTargetId = page.targetId;
|
||||||
await assertBrowserNavigationResultAllowed({ url: page.url, ...ssrfPolicyOpts });
|
await assertBrowserNavigationResultAllowed({ url: page.url, ...ssrfPolicyOpts });
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ export function createBrowserRouteContext(opts: ContextOptions): BrowserRouteCon
|
|||||||
try {
|
try {
|
||||||
running = await profileCtx.isTransportAvailable(300);
|
running = await profileCtx.isTransportAvailable(300);
|
||||||
if (running) {
|
if (running) {
|
||||||
const tabs = await listChromeMcpTabs(profile.name, profile.userDataDir, {
|
const tabs = await listChromeMcpTabs(profile.name, profile, {
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
}).catch(() => [] as BrowserTab[]);
|
}).catch(() => [] as BrowserTab[]);
|
||||||
tabCount = tabs.filter((t) => t.type === "page").length;
|
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
|
// 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
|
// 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.
|
// 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;
|
return null;
|
||||||
}
|
}
|
||||||
const npmSpec = record.packageInstall?.npm?.spec;
|
const npmSpec = record.packageInstall?.npm?.spec;
|
||||||
@@ -19,7 +19,7 @@ function buildBridgeFromPersistedBundledRecord(
|
|||||||
bundledPluginId: record.pluginId,
|
bundledPluginId: record.pluginId,
|
||||||
pluginId: record.pluginId,
|
pluginId: record.pluginId,
|
||||||
npmSpec,
|
npmSpec,
|
||||||
...(record.enabledByDefault === true ? { enabledByDefault: true } : {}),
|
...(record.enabledByDefault ? { enabledByDefault: true } : {}),
|
||||||
channelIds: record.contributions.channels,
|
channelIds: record.contributions.channels,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -780,6 +780,21 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
|||||||
description:
|
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.",
|
"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: {
|
driver: {
|
||||||
anyOf: [
|
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.",
|
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"],
|
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": {
|
"browser.profiles.*.driver": {
|
||||||
label: "Browser Profile 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.',
|
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.",
|
"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":
|
"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.",
|
"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":
|
"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.',
|
'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":
|
"browser.profiles.*.headless":
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ export const FIELD_LABELS: Record<string, string> = {
|
|||||||
"browser.profiles.*.cdpPort": "Browser Profile CDP Port",
|
"browser.profiles.*.cdpPort": "Browser Profile CDP Port",
|
||||||
"browser.profiles.*.cdpUrl": "Browser Profile CDP URL",
|
"browser.profiles.*.cdpUrl": "Browser Profile CDP URL",
|
||||||
"browser.profiles.*.userDataDir": "Browser Profile User Data Dir",
|
"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.*.driver": "Browser Profile Driver",
|
||||||
"browser.profiles.*.headless": "Browser Profile Headless Mode",
|
"browser.profiles.*.headless": "Browser Profile Headless Mode",
|
||||||
"browser.profiles.*.attachOnly": "Browser Profile Attach-only Mode",
|
"browser.profiles.*.attachOnly": "Browser Profile Attach-only Mode",
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ export type BrowserProfileConfig = {
|
|||||||
cdpUrl?: string;
|
cdpUrl?: string;
|
||||||
/** Explicit user data directory for existing-session Chrome MCP attachment. */
|
/** Explicit user data directory for existing-session Chrome MCP attachment. */
|
||||||
userDataDir?: string;
|
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). */
|
/** Profile driver (default: openclaw). */
|
||||||
driver?: "openclaw" | "clawd" | "existing-session";
|
driver?: "openclaw" | "clawd" | "existing-session";
|
||||||
/** If true, launch this profile in headless mode. Falls back to browser.headless. */
|
/** 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(),
|
cdpPort: z.number().int().min(1).max(65535).optional(),
|
||||||
cdpUrl: z.string().optional(),
|
cdpUrl: z.string().optional(),
|
||||||
userDataDir: z.string().optional(),
|
userDataDir: z.string().optional(),
|
||||||
|
mcpCommand: z.string().optional(),
|
||||||
|
mcpArgs: z.array(z.string()).optional(),
|
||||||
driver: z
|
driver: z
|
||||||
.union([z.literal("openclaw"), z.literal("clawd"), z.literal("existing-session")])
|
.union([z.literal("openclaw"), z.literal("clawd"), z.literal("existing-session")])
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|||||||
@@ -222,6 +222,10 @@ function pathEndsWithSegment(params: {
|
|||||||
return Boolean(value && segment && (value === segment || value.endsWith(`/${segment}`)));
|
return Boolean(value && segment && (value === segment || value.endsWith(`/${segment}`)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bundledExtensionPathSegment(bundledDirName: string): string {
|
||||||
|
return ["extensions", bundledDirName].join("/");
|
||||||
|
}
|
||||||
|
|
||||||
function isBridgeBundledPathRecord(params: {
|
function isBridgeBundledPathRecord(params: {
|
||||||
bridge: ExternalizedBundledPluginBridge;
|
bridge: ExternalizedBundledPluginBridge;
|
||||||
bundledLocalPath?: string;
|
bundledLocalPath?: string;
|
||||||
@@ -242,12 +246,12 @@ function isBridgeBundledPathRecord(params: {
|
|||||||
return (
|
return (
|
||||||
pathEndsWithSegment({
|
pathEndsWithSegment({
|
||||||
value: params.record.sourcePath,
|
value: params.record.sourcePath,
|
||||||
segment: `extensions/${bundledDirName}`,
|
segment: bundledExtensionPathSegment(bundledDirName),
|
||||||
env: params.env,
|
env: params.env,
|
||||||
}) ||
|
}) ||
|
||||||
pathEndsWithSegment({
|
pathEndsWithSegment({
|
||||||
value: params.record.installPath,
|
value: params.record.installPath,
|
||||||
segment: `extensions/${bundledDirName}`,
|
segment: bundledExtensionPathSegment(bundledDirName),
|
||||||
env: params.env,
|
env: params.env,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -262,7 +266,7 @@ function removeBridgeBundledLoadPaths(params: {
|
|||||||
params.loadPaths.removeMatching((entry) =>
|
params.loadPaths.removeMatching((entry) =>
|
||||||
pathEndsWithSegment({
|
pathEndsWithSegment({
|
||||||
value: entry,
|
value: entry,
|
||||||
segment: `extensions/${bundledDirName}`,
|
segment: bundledExtensionPathSegment(bundledDirName),
|
||||||
env: params.env,
|
env: params.env,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -896,9 +900,6 @@ export async function syncPluginsForUpdateChannel(params: {
|
|||||||
installs = next.plugins?.installs ?? {};
|
installs = next.plugins?.installs ?? {};
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
if (bundledInfo?.localPath) {
|
|
||||||
loadHelpers.removePath(bundledInfo.localPath);
|
|
||||||
}
|
|
||||||
removeBridgeBundledLoadPaths({ bridge, loadPaths: loadHelpers, env });
|
removeBridgeBundledLoadPaths({ bridge, loadPaths: loadHelpers, env });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -907,7 +908,7 @@ export async function syncPluginsForUpdateChannel(params: {
|
|||||||
existing &&
|
existing &&
|
||||||
!isBridgeBundledPathRecord({
|
!isBridgeBundledPathRecord({
|
||||||
bridge,
|
bridge,
|
||||||
bundledLocalPath: bundledInfo?.localPath,
|
bundledLocalPath: undefined,
|
||||||
record: existing.record,
|
record: existing.record,
|
||||||
env,
|
env,
|
||||||
})
|
})
|
||||||
@@ -947,9 +948,6 @@ export async function syncPluginsForUpdateChannel(params: {
|
|||||||
...buildNpmResolutionInstallFields(result.npmResolution),
|
...buildNpmResolutionInstallFields(result.npmResolution),
|
||||||
});
|
});
|
||||||
installs = next.plugins?.installs ?? {};
|
installs = next.plugins?.installs ?? {};
|
||||||
if (bundledInfo?.localPath) {
|
|
||||||
loadHelpers.removePath(bundledInfo.localPath);
|
|
||||||
}
|
|
||||||
if (existing?.record.sourcePath) {
|
if (existing?.record.sourcePath) {
|
||||||
loadHelpers.removePath(existing.record.sourcePath);
|
loadHelpers.removePath(existing.record.sourcePath);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user