feat(baoyu-chrome-cdp): support connecting to existing Chrome session
Add ability to discover and connect to an already-running Chrome browser (v144+) for cookie extraction, avoiding the need to launch a new window and re-login. Uses Chrome's DevToolsActivePort from default user data directories and process scanning as discovery mechanisms. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e31294415d
commit
682888cc95
|
|
@ -43,6 +43,13 @@ type FindExistingChromeDebugPortOptions = {
|
|||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
export type ChromeChannel = "stable" | "beta" | "canary" | "dev";
|
||||
|
||||
type DiscoverRunningChromeOptions = {
|
||||
channels?: ChromeChannel[];
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
type LaunchChromeOptions = {
|
||||
chromePath: string;
|
||||
profileDir: string;
|
||||
|
|
@ -204,6 +211,78 @@ export async function findExistingChromeDebugPort(options: FindExistingChromeDeb
|
|||
return null;
|
||||
}
|
||||
|
||||
export function getDefaultChromeUserDataDirs(channels: ChromeChannel[] = ["stable"]): string[] {
|
||||
const home = os.homedir();
|
||||
const dirs: string[] = [];
|
||||
|
||||
const channelDirs: Record<string, { darwin: string; linux: string; win32: string }> = {
|
||||
stable: {
|
||||
darwin: path.join(home, "Library", "Application Support", "Google", "Chrome"),
|
||||
linux: path.join(home, ".config", "google-chrome"),
|
||||
win32: path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome", "User Data"),
|
||||
},
|
||||
beta: {
|
||||
darwin: path.join(home, "Library", "Application Support", "Google", "Chrome Beta"),
|
||||
linux: path.join(home, ".config", "google-chrome-beta"),
|
||||
win32: path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome Beta", "User Data"),
|
||||
},
|
||||
canary: {
|
||||
darwin: path.join(home, "Library", "Application Support", "Google", "Chrome Canary"),
|
||||
linux: path.join(home, ".config", "google-chrome-canary"),
|
||||
win32: path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome SxS", "User Data"),
|
||||
},
|
||||
dev: {
|
||||
darwin: path.join(home, "Library", "Application Support", "Google", "Chrome Dev"),
|
||||
linux: path.join(home, ".config", "google-chrome-dev"),
|
||||
win32: path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome Dev", "User Data"),
|
||||
},
|
||||
};
|
||||
|
||||
const platform = process.platform === "darwin" ? "darwin" : process.platform === "win32" ? "win32" : "linux";
|
||||
|
||||
for (const ch of channels) {
|
||||
const entry = channelDirs[ch];
|
||||
if (entry) dirs.push(entry[platform]);
|
||||
}
|
||||
|
||||
return dirs;
|
||||
}
|
||||
|
||||
export async function discoverRunningChromeDebugPort(options: DiscoverRunningChromeOptions = {}): Promise<number | null> {
|
||||
const channels = options.channels ?? ["stable", "beta", "canary", "dev"];
|
||||
const timeoutMs = options.timeoutMs ?? 3_000;
|
||||
|
||||
const userDataDirs = getDefaultChromeUserDataDirs(channels);
|
||||
for (const dir of userDataDirs) {
|
||||
const portFile = path.join(dir, "DevToolsActivePort");
|
||||
try {
|
||||
const content = fs.readFileSync(portFile, "utf-8");
|
||||
const [portLine] = content.split(/\r?\n/);
|
||||
const port = Number.parseInt(portLine?.trim() ?? "", 10);
|
||||
if (port > 0 && await isDebugPortReady(port, timeoutMs)) return port;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
try {
|
||||
const result = spawnSync("ps", ["aux"], { encoding: "utf-8", timeout: 5_000 });
|
||||
if (result.status === 0 && result.stdout) {
|
||||
const lines = result.stdout
|
||||
.split("\n")
|
||||
.filter((line) => line.includes("--remote-debugging-port=") && /chrome|chromium/i.test(line));
|
||||
|
||||
for (const line of lines) {
|
||||
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
||||
const port = Number.parseInt(portMatch?.[1] ?? "", 10);
|
||||
if (port > 0 && await isDebugPortReady(port, timeoutMs)) return port;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function waitForChromeDebugPort(
|
||||
port: number,
|
||||
timeoutMs: number,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import process from 'node:process';
|
|||
|
||||
import {
|
||||
CdpConnection,
|
||||
discoverRunningChromeDebugPort,
|
||||
findChromeExecutable as findChromeExecutableBase,
|
||||
findExistingChromeDebugPort,
|
||||
getFreePort,
|
||||
|
|
@ -97,6 +98,84 @@ async function is_gemini_session_ready(cookies: CookieMap, verbose: boolean): Pr
|
|||
}
|
||||
}
|
||||
|
||||
async function fetch_cookies_from_existing_chrome(
|
||||
timeoutMs: number,
|
||||
verbose: boolean,
|
||||
): Promise<CookieMap | null> {
|
||||
const port = await discoverRunningChromeDebugPort();
|
||||
if (port === null) return null;
|
||||
|
||||
if (verbose) logger.info(`Found existing Chrome on port ${port}. Extracting cookies...`);
|
||||
|
||||
let cdp: CdpConnection | null = null;
|
||||
try {
|
||||
const wsUrl = await waitForChromeDebugPort(port, 10_000, { includeLastError: true });
|
||||
cdp = await CdpConnection.connect(wsUrl, 15_000);
|
||||
|
||||
const targets = await cdp.send<{ targetInfos: Array<{ targetId: string; url: string; type: string }> }>('Target.getTargets');
|
||||
const geminiTarget = targets.targetInfos.find(
|
||||
(t) => t.type === 'page' && t.url.includes('gemini.google.com'),
|
||||
);
|
||||
|
||||
let targetId: string;
|
||||
let createdTab = false;
|
||||
|
||||
if (geminiTarget) {
|
||||
targetId = geminiTarget.targetId;
|
||||
if (verbose) logger.debug('Found existing Gemini tab, attaching...');
|
||||
} else {
|
||||
const created = await cdp.send<{ targetId: string }>('Target.createTarget', { url: GEMINI_APP_URL });
|
||||
targetId = created.targetId;
|
||||
createdTab = true;
|
||||
if (verbose) logger.debug('No Gemini tab found, created new tab...');
|
||||
}
|
||||
|
||||
const { sessionId } = await cdp.send<{ sessionId: string }>(
|
||||
'Target.attachToTarget',
|
||||
{ targetId, flatten: true },
|
||||
);
|
||||
await cdp.send('Network.enable', {}, { sessionId });
|
||||
|
||||
const start = Date.now();
|
||||
let last: CookieMap = {};
|
||||
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
const { cookies } = await cdp.send<{ cookies: Array<{ name: string; value: string }> }>(
|
||||
'Network.getCookies',
|
||||
{ urls: ['https://gemini.google.com/', 'https://accounts.google.com/', 'https://www.google.com/'] },
|
||||
{ sessionId, timeoutMs: 10_000 },
|
||||
);
|
||||
|
||||
const cookieMap: CookieMap = {};
|
||||
for (const cookie of cookies) {
|
||||
if (cookie?.name && typeof cookie.value === 'string') cookieMap[cookie.name] = cookie.value;
|
||||
}
|
||||
|
||||
last = cookieMap;
|
||||
if (await is_gemini_session_ready(cookieMap, verbose)) {
|
||||
if (createdTab) {
|
||||
try { await cdp.send('Target.closeTarget', { targetId }, { timeoutMs: 5_000 }); } catch {}
|
||||
}
|
||||
return cookieMap;
|
||||
}
|
||||
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
if (createdTab) {
|
||||
try { await cdp.send('Target.closeTarget', { targetId }, { timeoutMs: 5_000 }); } catch {}
|
||||
}
|
||||
|
||||
if (verbose) logger.debug(`Existing Chrome did not yield valid cookies. Last keys: ${Object.keys(last).join(', ')}`);
|
||||
return null;
|
||||
} catch (e) {
|
||||
if (verbose) logger.debug(`Failed to connect to existing Chrome: ${e instanceof Error ? e.message : String(e)}`);
|
||||
return null;
|
||||
} finally {
|
||||
if (cdp) cdp.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function fetch_google_cookies_via_cdp(
|
||||
profileDir: string,
|
||||
timeoutMs: number,
|
||||
|
|
@ -178,6 +257,18 @@ export async function load_browser_cookies(domain_name: string = '', verbose: bo
|
|||
if (cached) return { chrome: cached };
|
||||
}
|
||||
|
||||
const existingCookies = await fetch_cookies_from_existing_chrome(30_000, verbose);
|
||||
if (existingCookies) {
|
||||
const filtered: CookieMap = {};
|
||||
for (const [key, value] of Object.entries(existingCookies)) {
|
||||
if (typeof value === 'string' && value.length > 0) filtered[key] = value;
|
||||
}
|
||||
|
||||
await write_cookie_file(filtered, resolveGeminiWebCookiePath(), 'cdp-existing');
|
||||
void domain_name;
|
||||
return { chrome: filtered };
|
||||
}
|
||||
|
||||
const profileDir = process.env.GEMINI_WEB_CHROME_PROFILE_DIR?.trim() || resolveGeminiWebChromeProfileDir();
|
||||
const cookies = await fetch_google_cookies_via_cdp(profileDir, 120_000, verbose);
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,13 @@ type FindExistingChromeDebugPortOptions = {
|
|||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
export type ChromeChannel = "stable" | "beta" | "canary" | "dev";
|
||||
|
||||
type DiscoverRunningChromeOptions = {
|
||||
channels?: ChromeChannel[];
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
type LaunchChromeOptions = {
|
||||
chromePath: string;
|
||||
profileDir: string;
|
||||
|
|
@ -204,6 +211,78 @@ export async function findExistingChromeDebugPort(options: FindExistingChromeDeb
|
|||
return null;
|
||||
}
|
||||
|
||||
export function getDefaultChromeUserDataDirs(channels: ChromeChannel[] = ["stable"]): string[] {
|
||||
const home = os.homedir();
|
||||
const dirs: string[] = [];
|
||||
|
||||
const channelDirs: Record<string, { darwin: string; linux: string; win32: string }> = {
|
||||
stable: {
|
||||
darwin: path.join(home, "Library", "Application Support", "Google", "Chrome"),
|
||||
linux: path.join(home, ".config", "google-chrome"),
|
||||
win32: path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome", "User Data"),
|
||||
},
|
||||
beta: {
|
||||
darwin: path.join(home, "Library", "Application Support", "Google", "Chrome Beta"),
|
||||
linux: path.join(home, ".config", "google-chrome-beta"),
|
||||
win32: path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome Beta", "User Data"),
|
||||
},
|
||||
canary: {
|
||||
darwin: path.join(home, "Library", "Application Support", "Google", "Chrome Canary"),
|
||||
linux: path.join(home, ".config", "google-chrome-canary"),
|
||||
win32: path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome SxS", "User Data"),
|
||||
},
|
||||
dev: {
|
||||
darwin: path.join(home, "Library", "Application Support", "Google", "Chrome Dev"),
|
||||
linux: path.join(home, ".config", "google-chrome-dev"),
|
||||
win32: path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome Dev", "User Data"),
|
||||
},
|
||||
};
|
||||
|
||||
const platform = process.platform === "darwin" ? "darwin" : process.platform === "win32" ? "win32" : "linux";
|
||||
|
||||
for (const ch of channels) {
|
||||
const entry = channelDirs[ch];
|
||||
if (entry) dirs.push(entry[platform]);
|
||||
}
|
||||
|
||||
return dirs;
|
||||
}
|
||||
|
||||
export async function discoverRunningChromeDebugPort(options: DiscoverRunningChromeOptions = {}): Promise<number | null> {
|
||||
const channels = options.channels ?? ["stable", "beta", "canary", "dev"];
|
||||
const timeoutMs = options.timeoutMs ?? 3_000;
|
||||
|
||||
const userDataDirs = getDefaultChromeUserDataDirs(channels);
|
||||
for (const dir of userDataDirs) {
|
||||
const portFile = path.join(dir, "DevToolsActivePort");
|
||||
try {
|
||||
const content = fs.readFileSync(portFile, "utf-8");
|
||||
const [portLine] = content.split(/\r?\n/);
|
||||
const port = Number.parseInt(portLine?.trim() ?? "", 10);
|
||||
if (port > 0 && await isDebugPortReady(port, timeoutMs)) return port;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
try {
|
||||
const result = spawnSync("ps", ["aux"], { encoding: "utf-8", timeout: 5_000 });
|
||||
if (result.status === 0 && result.stdout) {
|
||||
const lines = result.stdout
|
||||
.split("\n")
|
||||
.filter((line) => line.includes("--remote-debugging-port=") && /chrome|chromium/i.test(line));
|
||||
|
||||
for (const line of lines) {
|
||||
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
||||
const port = Number.parseInt(portMatch?.[1] ?? "", 10);
|
||||
if (port > 0 && await isDebugPortReady(port, timeoutMs)) return port;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function waitForChromeDebugPort(
|
||||
port: number,
|
||||
timeoutMs: number,
|
||||
|
|
|
|||
Loading…
Reference in New Issue