mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-12 05:51:44 +08:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4be6f3682a | |||
| b89ef02221 | |||
| 55356c8820 | |||
| f23d4eebc3 | |||
| 3b795b6ef3 | |||
| c62d9d5a1b | |||
| 84c56b0da3 | |||
| 462d080a0e | |||
| bd01c370c9 | |||
| b7bcc8c094 | |||
| d9f9da639d | |||
| 126b3040e6 | |||
| 682888cc95 |
@@ -6,7 +6,7 @@
|
|||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||||
"version": "1.68.0"
|
"version": "1.69.0"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,22 @@
|
|||||||
|
|
||||||
English | [中文](./CHANGELOG.zh.md)
|
English | [中文](./CHANGELOG.zh.md)
|
||||||
|
|
||||||
|
## 1.69.0 - 2026-03-16
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-chrome-cdp`: support connecting to existing Chrome session (by @bviews)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-chrome-cdp`: support Chrome 146 native remote debugging in approval mode (by @bviews)
|
||||||
|
- `baoyu-chrome-cdp`: keep HTTP validation in findExistingChromeDebugPort (by @bviews)
|
||||||
|
- `baoyu-danger-gemini-web`: reuse openPageSession and fix orphaned tab leak (by @bviews)
|
||||||
|
- `baoyu-danger-gemini-web`: respect explicit profile config over auto-discovery (by @bviews)
|
||||||
|
- `baoyu-danger-gemini-web`: respect BAOYU_CHROME_PROFILE_DIR in auto-discovery skip (by @bviews)
|
||||||
|
- `baoyu-post-to-wechat`: improve browser publishing reliability (by @cfh-7598)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- `baoyu-cover-image`: clarify people reference image workflow and interactive confirmation
|
||||||
|
|
||||||
## 1.68.0 - 2026-03-14
|
## 1.68.0 - 2026-03-14
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -2,6 +2,22 @@
|
|||||||
|
|
||||||
[English](./CHANGELOG.md) | 中文
|
[English](./CHANGELOG.md) | 中文
|
||||||
|
|
||||||
|
## 1.69.0 - 2026-03-16
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-chrome-cdp`:支持连接到已有的 Chrome 会话 (by @bviews)
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-chrome-cdp`:支持 Chrome 146 原生远程调试(审批模式)(by @bviews)
|
||||||
|
- `baoyu-chrome-cdp`:保留 findExistingChromeDebugPort 中的 HTTP 验证 (by @bviews)
|
||||||
|
- `baoyu-danger-gemini-web`:复用 openPageSession 并修复孤立标签页泄漏 (by @bviews)
|
||||||
|
- `baoyu-danger-gemini-web`:显式配置优先于自动发现 (by @bviews)
|
||||||
|
- `baoyu-danger-gemini-web`:自动发现跳过时也遵循 BAOYU_CHROME_PROFILE_DIR (by @bviews)
|
||||||
|
- `baoyu-post-to-wechat`:提升浏览器发布可靠性 (by @cfh-7598)
|
||||||
|
|
||||||
|
### 文档
|
||||||
|
- `baoyu-cover-image`:完善人物参考图片工作流和交互式确认说明
|
||||||
|
|
||||||
## 1.68.0 - 2026-03-14
|
## 1.68.0 - 2026-03-14
|
||||||
|
|
||||||
### 新功能
|
### 新功能
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.68.0**.
|
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.69.0**.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -159,13 +159,24 @@ if (Test-Path "$HOME/.baoyu-skills/baoyu-cover-image/EXTEND.md") { "user" }
|
|||||||
2. **Save source content** (if pasted, save to `source.md`)
|
2. **Save source content** (if pasted, save to `source.md`)
|
||||||
3. **Analyze content**: topic, tone, keywords, visual metaphors
|
3. **Analyze content**: topic, tone, keywords, visual metaphors
|
||||||
4. **Deep analyze references** ⚠️: Extract specific, concrete elements (see reference-images.md)
|
4. **Deep analyze references** ⚠️: Extract specific, concrete elements (see reference-images.md)
|
||||||
- If references contain **people** → set `usage: direct` so model sees reference image, describe character features for stylized preservation (see reference-images.md § Character Analysis)
|
|
||||||
5. **Detect language**: Compare source, user input, EXTEND.md preference
|
5. **Detect language**: Compare source, user input, EXTEND.md preference
|
||||||
6. **Determine output directory**: Per File Structure rules
|
6. **Determine output directory**: Per File Structure rules
|
||||||
|
|
||||||
|
**⚠️ People in Reference Images — MUST follow all 3 rules:**
|
||||||
|
|
||||||
|
If reference images contain **people** who should appear in the cover:
|
||||||
|
|
||||||
|
1. **`usage: direct`** — MUST set in refs description file. NEVER use `style` or `palette` when people need to appear
|
||||||
|
2. **Per-character description** — MUST describe each person's distinctive features (hair, glasses, skin tone, clothing) in `refs/ref-NN-{slug}.md`. Vague descriptions like "a man" will fail
|
||||||
|
3. **`--ref` flag** — MUST pass reference image via `--ref` in Step 4 so the model sees actual faces
|
||||||
|
|
||||||
|
See [reference-images.md § Character Analysis](references/workflow/reference-images.md) for description format.
|
||||||
|
|
||||||
### Step 2: Confirm Options ⚠️
|
### Step 2: Confirm Options ⚠️
|
||||||
|
|
||||||
Full confirmation flow: [references/workflow/confirm-options.md](references/workflow/confirm-options.md)
|
**MUST use `AskUserQuestion` tool** to present options as interactive selection — NOT plain text tables. Present up to 4 questions in a single `AskUserQuestion` call (Type, Palette, Rendering, Font + Settings). Each question shows the recommended option first with reason, followed by alternatives.
|
||||||
|
|
||||||
|
Full confirmation flow and question format: [references/workflow/confirm-options.md](references/workflow/confirm-options.md)
|
||||||
|
|
||||||
| Condition | Skipped | Still Asked |
|
| Condition | Skipped | Still Asked |
|
||||||
|-----------|---------|-------------|
|
|-----------|---------|-------------|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
+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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -51,8 +51,9 @@ async function waitForElement(session: ChromeSession, selector: string, timeoutM
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clickMenuByText(session: ChromeSession, text: string): Promise<void> {
|
async function clickMenuByText(session: ChromeSession, text: string, maxRetries = 5): Promise<void> {
|
||||||
console.log(`[wechat] Clicking "${text}" menu...`);
|
console.log(`[wechat] Clicking "${text}" menu...`);
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
const posResult = await session.cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
|
const posResult = await session.cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
|
||||||
expression: `
|
expression: `
|
||||||
(function() {
|
(function() {
|
||||||
@@ -71,12 +72,21 @@ async function clickMenuByText(session: ChromeSession, text: string): Promise<vo
|
|||||||
returnByValue: true,
|
returnByValue: true,
|
||||||
}, { sessionId: session.sessionId });
|
}, { sessionId: session.sessionId });
|
||||||
|
|
||||||
if (posResult.result.value === 'null') throw new Error(`Menu "${text}" not found`);
|
if (posResult.result.value !== 'null') {
|
||||||
const pos = JSON.parse(posResult.result.value);
|
const pos = JSON.parse(posResult.result.value);
|
||||||
|
|
||||||
await session.cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: pos.x, y: pos.y, button: 'left', clickCount: 1 }, { sessionId: session.sessionId });
|
await session.cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: pos.x, y: pos.y, button: 'left', clickCount: 1 }, { sessionId: session.sessionId });
|
||||||
await sleep(100);
|
await sleep(100);
|
||||||
await session.cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: pos.x, y: pos.y, button: 'left', clickCount: 1 }, { sessionId: session.sessionId });
|
await session.cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: pos.x, y: pos.y, button: 'left', clickCount: 1 }, { sessionId: session.sessionId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
const delay = Math.min(1000 * attempt, 3000);
|
||||||
|
console.log(`[wechat] Menu "${text}" not found, retrying in ${delay}ms (${attempt}/${maxRetries})...`);
|
||||||
|
await sleep(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`Menu "${text}" not found after ${maxRetries} attempts`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyImageToClipboard(imagePath: string): Promise<void> {
|
async function copyImageToClipboard(imagePath: string): Promise<void> {
|
||||||
@@ -495,10 +505,10 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
|||||||
if (!loggedIn) throw new Error('Login timeout');
|
if (!loggedIn) throw new Error('Login timeout');
|
||||||
}
|
}
|
||||||
console.log('[wechat] Logged in.');
|
console.log('[wechat] Logged in.');
|
||||||
await sleep(2000);
|
await sleep(5000);
|
||||||
|
|
||||||
// Wait for menu to be ready
|
// Wait for menu to be ready
|
||||||
const menuReady = await waitForElement(session, '.new-creation__menu', 20_000);
|
const menuReady = await waitForElement(session, '.new-creation__menu', 40_000);
|
||||||
if (!menuReady) throw new Error('Home page menu did not load');
|
if (!menuReady) throw new Error('Home page menu did not load');
|
||||||
|
|
||||||
const targets = await cdp.send<{ targetInfos: Array<{ targetId: string; url: string; type: string }> }>('Target.getTargets');
|
const targets = await cdp.send<{ targetInfos: Array<{ targetId: string; url: string; type: string }> }>('Target.getTargets');
|
||||||
@@ -517,16 +527,21 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
|||||||
await cdp.send('Runtime.enable', {}, { sessionId });
|
await cdp.send('Runtime.enable', {}, { sessionId });
|
||||||
await cdp.send('DOM.enable', {}, { sessionId });
|
await cdp.send('DOM.enable', {}, { sessionId });
|
||||||
|
|
||||||
await sleep(3000);
|
// Wait for editor elements to fully load
|
||||||
|
console.log('[wechat] Waiting for editor to load...');
|
||||||
|
const editorLoaded = await waitForElement(session, '#title', 30_000);
|
||||||
|
if (!editorLoaded) throw new Error('Editor did not load (#title not found)');
|
||||||
|
await waitForElement(session, '.ProseMirror', 15_000);
|
||||||
|
await sleep(2000);
|
||||||
|
|
||||||
if (effectiveTitle) {
|
if (effectiveTitle) {
|
||||||
console.log('[wechat] Filling title...');
|
console.log('[wechat] Filling title...');
|
||||||
await evaluate(session, `document.querySelector('#title').value = ${JSON.stringify(effectiveTitle)}; document.querySelector('#title').dispatchEvent(new Event('input', { bubbles: true }));`);
|
await evaluate(session, `(function() { const el = document.querySelector('#title'); el.focus(); el.value = ${JSON.stringify(effectiveTitle)}; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); })()`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (effectiveAuthor) {
|
if (effectiveAuthor) {
|
||||||
console.log('[wechat] Filling author...');
|
console.log('[wechat] Filling author...');
|
||||||
await evaluate(session, `document.querySelector('#author').value = ${JSON.stringify(effectiveAuthor)}; document.querySelector('#author').dispatchEvent(new Event('input', { bubbles: true }));`);
|
await evaluate(session, `(function() { const el = document.querySelector('#author'); el.focus(); el.value = ${JSON.stringify(effectiveAuthor)}; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); })()`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
+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