mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-24 02:59:47 +08:00
Merge pull request #92 from bviews/main
feat(baoyu-chrome-cdp): support Chrome 146 auto-connect
This commit is contained in:
@@ -43,6 +43,18 @@ type FindExistingChromeDebugPortOptions = {
|
|||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ChromeChannel = "stable" | "beta" | "canary" | "dev";
|
||||||
|
|
||||||
|
export type DiscoveredChrome = {
|
||||||
|
port: number;
|
||||||
|
wsUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DiscoverRunningChromeOptions = {
|
||||||
|
channels?: ChromeChannel[];
|
||||||
|
timeoutMs?: number;
|
||||||
|
};
|
||||||
|
|
||||||
type LaunchChromeOptions = {
|
type LaunchChromeOptions = {
|
||||||
chromePath: string;
|
chromePath: string;
|
||||||
profileDir: string;
|
profileDir: string;
|
||||||
@@ -173,16 +185,32 @@ async function isDebugPortReady(port: number, timeoutMs = 3_000): Promise<boolea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPortListening(port: number, timeoutMs = 3_000): Promise<boolean> {
|
||||||
|
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<number | null> {
|
export async function findExistingChromeDebugPort(options: FindExistingChromeDebugPortOptions): Promise<number | null> {
|
||||||
const timeoutMs = options.timeoutMs ?? 3_000;
|
const timeoutMs = options.timeoutMs ?? 3_000;
|
||||||
const portFile = path.join(options.profileDir, "DevToolsActivePort");
|
const parsed = parseDevToolsActivePort(path.join(options.profileDir, "DevToolsActivePort"));
|
||||||
|
|
||||||
try {
|
if (parsed && parsed.port > 0 && await isDebugPortReady(parsed.port, timeoutMs)) return parsed.port;
|
||||||
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") return null;
|
if (process.platform === "win32") return null;
|
||||||
|
|
||||||
@@ -204,6 +232,81 @@ export async function findExistingChromeDebugPort(options: FindExistingChromeDeb
|
|||||||
return null;
|
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<DiscoveredChrome | 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 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(
|
export async function waitForChromeDebugPort(
|
||||||
port: number,
|
port: number,
|
||||||
timeoutMs: number,
|
timeoutMs: number,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import process from 'node:process';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CdpConnection,
|
CdpConnection,
|
||||||
|
discoverRunningChromeDebugPort,
|
||||||
findChromeExecutable as findChromeExecutableBase,
|
findChromeExecutable as findChromeExecutableBase,
|
||||||
findExistingChromeDebugPort,
|
findExistingChromeDebugPort,
|
||||||
getFreePort,
|
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<CookieMap | null> {
|
||||||
|
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(
|
async function fetch_google_cookies_via_cdp(
|
||||||
profileDir: string,
|
profileDir: string,
|
||||||
timeoutMs: number,
|
timeoutMs: number,
|
||||||
@@ -178,6 +265,19 @@ export async function load_browser_cookies(domain_name: string = '', verbose: bo
|
|||||||
if (cached) return { chrome: cached };
|
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 profileDir = process.env.GEMINI_WEB_CHROME_PROFILE_DIR?.trim() || resolveGeminiWebChromeProfileDir();
|
||||||
const cookies = await fetch_google_cookies_via_cdp(profileDir, 120_000, verbose);
|
const cookies = await fetch_google_cookies_via_cdp(profileDir, 120_000, verbose);
|
||||||
|
|
||||||
|
|||||||
+110
-7
@@ -43,6 +43,18 @@ type FindExistingChromeDebugPortOptions = {
|
|||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ChromeChannel = "stable" | "beta" | "canary" | "dev";
|
||||||
|
|
||||||
|
export type DiscoveredChrome = {
|
||||||
|
port: number;
|
||||||
|
wsUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DiscoverRunningChromeOptions = {
|
||||||
|
channels?: ChromeChannel[];
|
||||||
|
timeoutMs?: number;
|
||||||
|
};
|
||||||
|
|
||||||
type LaunchChromeOptions = {
|
type LaunchChromeOptions = {
|
||||||
chromePath: string;
|
chromePath: string;
|
||||||
profileDir: string;
|
profileDir: string;
|
||||||
@@ -173,16 +185,32 @@ async function isDebugPortReady(port: number, timeoutMs = 3_000): Promise<boolea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPortListening(port: number, timeoutMs = 3_000): Promise<boolean> {
|
||||||
|
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<number | null> {
|
export async function findExistingChromeDebugPort(options: FindExistingChromeDebugPortOptions): Promise<number | null> {
|
||||||
const timeoutMs = options.timeoutMs ?? 3_000;
|
const timeoutMs = options.timeoutMs ?? 3_000;
|
||||||
const portFile = path.join(options.profileDir, "DevToolsActivePort");
|
const parsed = parseDevToolsActivePort(path.join(options.profileDir, "DevToolsActivePort"));
|
||||||
|
|
||||||
try {
|
if (parsed && parsed.port > 0 && await isDebugPortReady(parsed.port, timeoutMs)) return parsed.port;
|
||||||
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") return null;
|
if (process.platform === "win32") return null;
|
||||||
|
|
||||||
@@ -204,6 +232,81 @@ export async function findExistingChromeDebugPort(options: FindExistingChromeDeb
|
|||||||
return null;
|
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<DiscoveredChrome | 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 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(
|
export async function waitForChromeDebugPort(
|
||||||
port: number,
|
port: number,
|
||||||
timeoutMs: number,
|
timeoutMs: number,
|
||||||
|
|||||||
Reference in New Issue
Block a user