diff --git a/packages/baoyu-chrome-cdp/src/index.ts b/packages/baoyu-chrome-cdp/src/index.ts index 1fcd241..ee4fd71 100644 --- a/packages/baoyu-chrome-cdp/src/index.ts +++ b/packages/baoyu-chrome-cdp/src/index.ts @@ -43,6 +43,18 @@ type FindExistingChromeDebugPortOptions = { timeoutMs?: number; }; +export type ChromeChannel = "stable" | "beta" | "canary" | "dev"; + +export type DiscoveredChrome = { + port: number; + wsUrl: string; +}; + +type DiscoverRunningChromeOptions = { + channels?: ChromeChannel[]; + timeoutMs?: number; +}; + type LaunchChromeOptions = { chromePath: string; profileDir: string; @@ -173,16 +185,32 @@ async function isDebugPortReady(port: number, timeoutMs = 3_000): Promise { + return new Promise((resolve) => { + const socket = new net.Socket(); + const timer = setTimeout(() => { socket.destroy(); resolve(false); }, timeoutMs); + socket.once("connect", () => { clearTimeout(timer); socket.destroy(); resolve(true); }); + socket.once("error", () => { clearTimeout(timer); resolve(false); }); + socket.connect(port, "127.0.0.1"); + }); +} + +function parseDevToolsActivePort(filePath: string): { port: number; wsPath: string } | null { + try { + const content = fs.readFileSync(filePath, "utf-8"); + const lines = content.split(/\r?\n/); + const port = Number.parseInt(lines[0]?.trim() ?? "", 10); + const wsPath = lines[1]?.trim(); + if (port > 0 && wsPath) return { port, wsPath }; + } catch {} + return null; +} + export async function findExistingChromeDebugPort(options: FindExistingChromeDebugPortOptions): Promise { const timeoutMs = options.timeoutMs ?? 3_000; - const portFile = path.join(options.profileDir, "DevToolsActivePort"); + const parsed = parseDevToolsActivePort(path.join(options.profileDir, "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 (parsed && parsed.port > 0 && await isDebugPortReady(parsed.port, timeoutMs)) return parsed.port; if (process.platform === "win32") return null; @@ -204,6 +232,81 @@ 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 = { + 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 { + const channels = options.channels ?? ["stable", "beta", "canary", "dev"]; + const timeoutMs = options.timeoutMs ?? 3_000; + + const userDataDirs = getDefaultChromeUserDataDirs(channels); + for (const dir of userDataDirs) { + const parsed = parseDevToolsActivePort(path.join(dir, "DevToolsActivePort")); + if (!parsed) continue; + if (await isPortListening(parsed.port, timeoutMs)) { + return { port: parsed.port, wsUrl: `ws://127.0.0.1:${parsed.port}${parsed.wsPath}` }; + } + } + + 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)) { + try { + const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(`http://127.0.0.1:${port}/json/version`, { timeoutMs }); + if (version.webSocketDebuggerUrl) return { port, wsUrl: version.webSocketDebuggerUrl }; + } catch {} + } + } + } + } catch {} + } + + return null; +} + export async function waitForChromeDebugPort( port: number, timeoutMs: number, diff --git a/skills/baoyu-danger-gemini-web/scripts/gemini-webapi/utils/load-browser-cookies.ts b/skills/baoyu-danger-gemini-web/scripts/gemini-webapi/utils/load-browser-cookies.ts index 97fa369..6b9a51a 100644 --- a/skills/baoyu-danger-gemini-web/scripts/gemini-webapi/utils/load-browser-cookies.ts +++ b/skills/baoyu-danger-gemini-web/scripts/gemini-webapi/utils/load-browser-cookies.ts @@ -2,6 +2,7 @@ import process from 'node:process'; import { CdpConnection, + discoverRunningChromeDebugPort, findChromeExecutable as findChromeExecutableBase, findExistingChromeDebugPort, getFreePort, @@ -97,6 +98,92 @@ async function is_gemini_session_ready(cookies: CookieMap, verbose: boolean): Pr } } +async function fetch_cookies_from_existing_chrome( + timeoutMs: number, + verbose: boolean, +): Promise { + const discovered = await discoverRunningChromeDebugPort(); + if (discovered === null) return null; + + if (verbose) logger.info(`Found existing Chrome on port ${discovered.port}. Connecting via WebSocket...`); + + let cdp: CdpConnection | null = null; + let createdTab = false; + let targetId: string | null = null; + try { + const connectStart = Date.now(); + const connectTimeout = 30_000; + let lastConnErr: unknown = null; + while (Date.now() - connectStart < connectTimeout) { + try { + cdp = await CdpConnection.connect(discovered.wsUrl, 5_000); + break; + } catch (e) { + lastConnErr = e; + if (verbose) logger.debug(`WebSocket connect attempt failed: ${e instanceof Error ? e.message : String(e)}, retrying...`); + await sleep(1000); + } + } + if (!cdp) { + if (verbose) logger.debug(`Could not connect to Chrome after ${connectTimeout / 1000}s: ${lastConnErr instanceof Error ? lastConnErr.message : String(lastConnErr)}`); + return null; + } + + const targets = await cdp.send<{ targetInfos: Array<{ targetId: string; url: string; type: string }> }>('Target.getTargets'); + const hasGeminiTab = targets.targetInfos.some( + (t) => t.type === 'page' && t.url.includes('gemini.google.com'), + ); + createdTab = !hasGeminiTab; + + if (verbose) logger.debug(hasGeminiTab ? 'Found existing Gemini tab, attaching...' : 'No Gemini tab found, creating new tab...'); + + const page = await openPageSession({ + cdp, + reusing: false, + url: GEMINI_APP_URL, + matchTarget: (target) => target.type === 'page' && target.url.includes('gemini.google.com'), + enableNetwork: true, + activateTarget: false, + }); + const { sessionId } = page; + targetId = page.targetId; + + 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)) return cookieMap; + + await sleep(1000); + } + + 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) { + if (createdTab && targetId) { + try { await cdp.send('Target.closeTarget', { targetId }, { timeoutMs: 5_000 }); } catch {} + } + cdp.close(); + } + } +} + async function fetch_google_cookies_via_cdp( profileDir: string, timeoutMs: number, @@ -178,6 +265,19 @@ export async function load_browser_cookies(domain_name: string = '', verbose: bo if (cached) return { chrome: cached }; } + const hasExplicitProfile = !!(process.env.GEMINI_WEB_CHROME_PROFILE_DIR?.trim() || process.env.BAOYU_CHROME_PROFILE_DIR?.trim()); + const existingCookies = hasExplicitProfile ? null : 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); diff --git a/skills/baoyu-danger-gemini-web/scripts/vendor/baoyu-chrome-cdp/src/index.ts b/skills/baoyu-danger-gemini-web/scripts/vendor/baoyu-chrome-cdp/src/index.ts index 1fcd241..ee4fd71 100644 --- a/skills/baoyu-danger-gemini-web/scripts/vendor/baoyu-chrome-cdp/src/index.ts +++ b/skills/baoyu-danger-gemini-web/scripts/vendor/baoyu-chrome-cdp/src/index.ts @@ -43,6 +43,18 @@ type FindExistingChromeDebugPortOptions = { timeoutMs?: number; }; +export type ChromeChannel = "stable" | "beta" | "canary" | "dev"; + +export type DiscoveredChrome = { + port: number; + wsUrl: string; +}; + +type DiscoverRunningChromeOptions = { + channels?: ChromeChannel[]; + timeoutMs?: number; +}; + type LaunchChromeOptions = { chromePath: string; profileDir: string; @@ -173,16 +185,32 @@ async function isDebugPortReady(port: number, timeoutMs = 3_000): Promise { + return new Promise((resolve) => { + const socket = new net.Socket(); + const timer = setTimeout(() => { socket.destroy(); resolve(false); }, timeoutMs); + socket.once("connect", () => { clearTimeout(timer); socket.destroy(); resolve(true); }); + socket.once("error", () => { clearTimeout(timer); resolve(false); }); + socket.connect(port, "127.0.0.1"); + }); +} + +function parseDevToolsActivePort(filePath: string): { port: number; wsPath: string } | null { + try { + const content = fs.readFileSync(filePath, "utf-8"); + const lines = content.split(/\r?\n/); + const port = Number.parseInt(lines[0]?.trim() ?? "", 10); + const wsPath = lines[1]?.trim(); + if (port > 0 && wsPath) return { port, wsPath }; + } catch {} + return null; +} + export async function findExistingChromeDebugPort(options: FindExistingChromeDebugPortOptions): Promise { const timeoutMs = options.timeoutMs ?? 3_000; - const portFile = path.join(options.profileDir, "DevToolsActivePort"); + const parsed = parseDevToolsActivePort(path.join(options.profileDir, "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 (parsed && parsed.port > 0 && await isDebugPortReady(parsed.port, timeoutMs)) return parsed.port; if (process.platform === "win32") return null; @@ -204,6 +232,81 @@ 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 = { + 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 { + const channels = options.channels ?? ["stable", "beta", "canary", "dev"]; + const timeoutMs = options.timeoutMs ?? 3_000; + + const userDataDirs = getDefaultChromeUserDataDirs(channels); + for (const dir of userDataDirs) { + const parsed = parseDevToolsActivePort(path.join(dir, "DevToolsActivePort")); + if (!parsed) continue; + if (await isPortListening(parsed.port, timeoutMs)) { + return { port: parsed.port, wsUrl: `ws://127.0.0.1:${parsed.port}${parsed.wsPath}` }; + } + } + + 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)) { + try { + const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(`http://127.0.0.1:${port}/json/version`, { timeoutMs }); + if (version.webSocketDebuggerUrl) return { port, wsUrl: version.webSocketDebuggerUrl }; + } catch {} + } + } + } + } catch {} + } + + return null; +} + export async function waitForChromeDebugPort( port: number, timeoutMs: number,