import process from 'node:process'; import { CdpConnection, findChromeExecutable as findChromeExecutableBase, findExistingChromeDebugPort, getFreePort, killChrome, launchChrome as launchChromeBase, openPageSession, sleep, waitForChromeDebugPort, type PlatformCandidates, } from 'baoyu-chrome-cdp'; import { Endpoint, Headers } from '../constants.js'; import { logger } from './logger.js'; import { cookie_header, fetch_with_timeout } from './http.js'; import { read_cookie_file, type CookieMap, write_cookie_file } from './cookie-file.js'; import { resolveGeminiWebChromeProfileDir, resolveGeminiWebCookiePath } from './paths.js'; const GEMINI_APP_URL = 'https://gemini.google.com/app'; const CHROME_CANDIDATES_FULL: PlatformCandidates = { darwin: [ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', '/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta', '/Applications/Chromium.app/Contents/MacOS/Chromium', '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', ], win32: [ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe', 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', ], default: [ '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/usr/bin/chromium-browser', '/snap/bin/chromium', '/usr/bin/microsoft-edge', ], }; async function get_free_port(): Promise { return await getFreePort('GEMINI_WEB_DEBUG_PORT'); } function find_chrome_executable(): string | null { return findChromeExecutableBase({ candidates: CHROME_CANDIDATES_FULL, envNames: ['GEMINI_WEB_CHROME_PATH'], }) ?? null; } async function find_existing_chrome_debug_port(profileDir: string): Promise { return await findExistingChromeDebugPort({ profileDir }); } async function launch_chrome(profileDir: string, port: number) { const chromePath = find_chrome_executable(); if (!chromePath) throw new Error('Chrome executable not found.'); return await launchChromeBase({ chromePath, profileDir, port, url: GEMINI_APP_URL, extraArgs: ['--disable-popup-blocking'], }); } async function is_gemini_session_ready(cookies: CookieMap, verbose: boolean): Promise { if (!cookies['__Secure-1PSID']) return false; try { const res = await fetch_with_timeout(Endpoint.INIT, { method: 'GET', headers: { ...Headers.GEMINI, Cookie: cookie_header(cookies) }, redirect: 'follow', timeout_ms: 30_000, }); if (!res.ok) { if (verbose) logger.debug(`Gemini init check failed: ${res.status} ${res.statusText}`); return false; } const text = await res.text(); return /\"SNlM0e\":\"(.*?)\"/.test(text); } catch (e) { if (verbose) logger.debug(`Gemini init check error: ${e instanceof Error ? e.message : String(e)}`); return false; } } async function fetch_google_cookies_via_cdp( profileDir: string, timeoutMs: number, verbose: boolean, ): Promise { const existingPort = await find_existing_chrome_debug_port(profileDir); const reusing = existingPort !== null; const port = existingPort ?? await get_free_port(); const chrome = reusing ? null : await launch_chrome(profileDir, port); let cdp: CdpConnection | null = null; let targetId: string | null = null; try { const wsUrl = await waitForChromeDebugPort(port, 30_000, { includeLastError: true }); cdp = await CdpConnection.connect(wsUrl, 15_000); if (verbose) { logger.info(reusing ? `Reusing existing Chrome on port ${port}. Waiting for a valid Gemini session...` : 'Chrome opened. If needed, complete Google login in the window. Waiting for a valid Gemini session...'); } const page = await openPageSession({ cdp, reusing, url: GEMINI_APP_URL, matchTarget: (target) => target.type === 'page' && target.url.includes('gemini.google.com'), enableNetwork: true, }); 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); } throw new Error(`Timed out waiting for a valid Gemini session. Last keys: ${Object.keys(last).join(', ')}`); } finally { if (cdp) { if (reusing && targetId) { try { await cdp.send('Target.closeTarget', { targetId }, { timeoutMs: 5_000 }); } catch {} } else { try { await cdp.send('Browser.close', {}, { timeoutMs: 5_000 }); } catch {} } cdp.close(); } if (chrome) killChrome(chrome); } } export async function load_browser_cookies(domain_name: string = '', verbose: boolean = true): Promise> { const force = process.env.GEMINI_WEB_LOGIN?.trim() || process.env.GEMINI_WEB_FORCE_LOGIN?.trim(); if (!force) { const cached = await read_cookie_file(); if (cached) return { chrome: cached }; } const profileDir = process.env.GEMINI_WEB_CHROME_PROFILE_DIR?.trim() || resolveGeminiWebChromeProfileDir(); const cookies = await fetch_google_cookies_via_cdp(profileDir, 120_000, verbose); const filtered: CookieMap = {}; for (const [key, value] of Object.entries(cookies)) { if (typeof value === 'string' && value.length > 0) filtered[key] = value; } await write_cookie_file(filtered, resolveGeminiWebCookiePath(), 'cdp'); void domain_name; return { chrome: filtered }; } export const loadBrowserCookies = load_browser_cookies;