mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-28 20:59:47 +08:00
feat(baoyu-fetch): add X session cookie sidecar and graceful Chrome lifecycle
Add cookie sidecar to export/restore X session cookies across runs. Implement graceful Browser.close via WebSocket before falling back to kill. Auto-detect and clean stale Chrome profile lock artifacts on launch failure. Wait for X session readiness (auth_token + ct0) before auto-continuing login flows.
This commit is contained in:
@@ -1,6 +1,16 @@
|
||||
import { launch, type LaunchedChrome } from "chrome-launcher";
|
||||
import WebSocket from "ws";
|
||||
import type { Logger } from "../utils/logger";
|
||||
import { ensureChromeProfileDir, findExistingChromeDebugPort, resolveChromeProfileDir } from "./profile";
|
||||
import {
|
||||
cleanChromeLockArtifacts,
|
||||
ensureChromeProfileDir,
|
||||
findChromeProcessUsingProfile,
|
||||
findExistingChromeDebugPort,
|
||||
hasChromeLockArtifacts,
|
||||
listChromeProfileEntries,
|
||||
resolveChromeProfileDir,
|
||||
shouldRetryChromeLaunchRecovery,
|
||||
} from "./profile";
|
||||
|
||||
interface ChromeVersionResponse {
|
||||
webSocketDebuggerUrl: string;
|
||||
@@ -65,6 +75,80 @@ async function tryReuseChrome(profileDir: string, logger?: Logger): Promise<Chro
|
||||
return null;
|
||||
}
|
||||
|
||||
async function launchFreshChrome(
|
||||
profileDir: string,
|
||||
options: Pick<ChromeConnectOptions, "browserPath" | "headless">,
|
||||
): Promise<ChromeConnection> {
|
||||
let launchedChrome: LaunchedChrome | null = null;
|
||||
try {
|
||||
launchedChrome = await launch({
|
||||
chromePath: options.browserPath,
|
||||
userDataDir: profileDir,
|
||||
chromeFlags: [
|
||||
"--disable-background-networking",
|
||||
"--disable-default-apps",
|
||||
"--disable-popup-blocking",
|
||||
"--disable-sync",
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check",
|
||||
"--remote-allow-origins=*",
|
||||
...(!options.headless ? ["--no-startup-window"] : []),
|
||||
...(options.headless ? ["--headless=new"] : []),
|
||||
],
|
||||
});
|
||||
|
||||
const origin = `http://127.0.0.1:${launchedChrome.port}`;
|
||||
const version = await fetchJson<ChromeVersionResponse>(`${origin}/json/version`);
|
||||
|
||||
const chrome = launchedChrome;
|
||||
return {
|
||||
browserWsUrl: version.webSocketDebuggerUrl,
|
||||
origin,
|
||||
port: launchedChrome.port,
|
||||
profileDir,
|
||||
launched: true,
|
||||
async close() {
|
||||
if (!chrome) return;
|
||||
await gracefulCloseChrome(chrome, origin);
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
launchedChrome?.kill();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function gracefulCloseChrome(chrome: LaunchedChrome, origin: string): Promise<void> {
|
||||
try {
|
||||
const resp = await fetch(`${origin}/json/version`);
|
||||
const { webSocketDebuggerUrl } = (await resp.json()) as ChromeVersionResponse;
|
||||
if (webSocketDebuggerUrl) {
|
||||
const ws = await new Promise<WebSocket>((resolve, reject) => {
|
||||
const socket = new WebSocket(webSocketDebuggerUrl);
|
||||
socket.once("open", () => resolve(socket));
|
||||
socket.once("error", reject);
|
||||
});
|
||||
const id = 1;
|
||||
ws.send(JSON.stringify({ id, method: "Browser.close" }));
|
||||
await new Promise<void>((resolve) => {
|
||||
const timer = setTimeout(() => { ws.close(); resolve(); }, 5_000);
|
||||
ws.once("close", () => { clearTimeout(timer); resolve(); });
|
||||
});
|
||||
const exited = await new Promise<boolean>((resolve) => {
|
||||
if (chrome.pid && !isProcessAlive(chrome.pid)) { resolve(true); return; }
|
||||
const timer = setTimeout(() => resolve(false), 3_000);
|
||||
chrome.process.once("exit", () => { clearTimeout(timer); resolve(true); });
|
||||
});
|
||||
if (exited) return;
|
||||
}
|
||||
} catch {}
|
||||
chrome.kill();
|
||||
}
|
||||
|
||||
function isProcessAlive(pid: number): boolean {
|
||||
try { process.kill(pid, 0); return true; } catch { return false; }
|
||||
}
|
||||
|
||||
export async function connectChrome(options: ChromeConnectOptions): Promise<ChromeConnection> {
|
||||
if (options.cdpUrl) {
|
||||
if (options.cdpUrl.startsWith("ws://") || options.cdpUrl.startsWith("wss://")) {
|
||||
@@ -84,34 +168,20 @@ export async function connectChrome(options: ChromeConnectOptions): Promise<Chro
|
||||
}
|
||||
|
||||
options.logger?.warn(`No running Chrome debugger found for profile ${profileDir}. Launching Chrome with that profile.`);
|
||||
try {
|
||||
return await launchFreshChrome(profileDir, options);
|
||||
} catch (error) {
|
||||
const entries = await listChromeProfileEntries(profileDir);
|
||||
const shouldRetry = shouldRetryChromeLaunchRecovery({
|
||||
hasLockArtifacts: hasChromeLockArtifacts(entries),
|
||||
hasLiveOwner: findChromeProcessUsingProfile(profileDir),
|
||||
});
|
||||
if (!shouldRetry) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const launchedChrome: LaunchedChrome = await launch({
|
||||
chromePath: options.browserPath,
|
||||
userDataDir: profileDir,
|
||||
chromeFlags: [
|
||||
"--disable-background-networking",
|
||||
"--disable-default-apps",
|
||||
"--disable-popup-blocking",
|
||||
"--disable-sync",
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check",
|
||||
"--remote-allow-origins=*",
|
||||
...(!options.headless ? ["--no-startup-window"] : []),
|
||||
...(options.headless ? ["--headless=new"] : []),
|
||||
],
|
||||
});
|
||||
|
||||
const origin = `http://127.0.0.1:${launchedChrome.port}`;
|
||||
const version = await fetchJson<ChromeVersionResponse>(`${origin}/json/version`);
|
||||
|
||||
return {
|
||||
browserWsUrl: version.webSocketDebuggerUrl,
|
||||
origin,
|
||||
port: launchedChrome.port,
|
||||
profileDir,
|
||||
launched: true,
|
||||
async close() {
|
||||
launchedChrome.kill();
|
||||
},
|
||||
};
|
||||
options.logger?.warn(`Chrome launch failed with stale profile locks. Cleaning ${profileDir} and retrying once.`);
|
||||
cleanChromeLockArtifacts(profileDir);
|
||||
return await launchFreshChrome(profileDir, options);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user