From 05d43e6773545e712726213a44f7078e34e44ed5 Mon Sep 17 00:00:00 2001 From: ByteYue Date: Mon, 16 Mar 2026 22:11:05 +0800 Subject: [PATCH 1/3] feat(browser): add CDP remote connection support for server environments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This feature enables OpenCLI to connect to a Chrome browser running on a different machine (e.g., your local computer) from a headless server environment via Chrome DevTools Protocol (CDP). Server environments (CI, cloud VMs, headless Linux) cannot run Chrome with a GUI or install the Playwright MCP Bridge extension. This makes it impossible to use OpenCLI commands that require browser authentication. Add support for the `OPENCLI_CDP_ENDPOINT` environment variable, which tells OpenCLI to connect to a remote Chrome instance via CDP instead of using the local extension mode. 1. Start Chrome with remote debugging on local machine: ``` chrome --remote-debugging-port=9222 --user-data-dir="$HOME/chrome-debug" ``` 2. Create SSH tunnel to forward port to server: ``` ssh -R 9222:localhost:9222 your-server ``` 3. Run OpenCLI on server: ``` export OPENCLI_CDP_ENDPOINT="http://localhost:9222" opencli bilibili hot --limit 5 ``` - src/browser.ts: Add CDP endpoint detection in buildMcpArgs() - src/doctor.ts: Show CDP mode status in doctor report - README.md: Add "Remote Chrome (Server/Headless)" section - README.zh-CN.md: Add corresponding Chinese documentation ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- README.md | 65 +++++++++++++++++++++++++++++++++ README.zh-CN.md | 65 +++++++++++++++++++++++++++++++++ src/browser/discover.ts | 80 +++++++++++++++++++++++++++++++++++++++-- src/browser/errors.ts | 18 +++++++++- src/browser/index.ts | 3 +- src/browser/mcp.ts | 11 ++++-- src/doctor.ts | 9 +++++ 7 files changed, 243 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index faa4f52..1edf124 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ A CLI tool that turns **any website** into a command-line interface โ€” Bilibili - [Built-in Commands](#built-in-commands) - [Output Formats](#output-formats) - [For AI Agents (Developer Guide)](#for-ai-agents-developer-guide) +- [Remote Chrome (Server/Headless)](#remote-chrome-serverheadless) - [Testing](#testing) - [Troubleshooting](#troubleshooting) - [Releasing New Versions](#releasing-new-versions) @@ -197,6 +198,70 @@ opencli cascade https://api.example.com/data Explore outputs to `.opencli/explore//` (manifest.json, endpoints.json, capabilities.json, auth.json). +## Remote Chrome (Server/Headless) + +For server environments without a display, you can connect OpenCLI to a Chrome browser running on your local machine via Chrome DevTools Protocol (CDP). + +### How It Works + +``` +Local Machine Server +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Chrome (logged into sites) โ”‚ โ”‚ OpenCLI โ”‚ +โ”‚ --remote-debugging-port=9222 โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ OPENCLI_CDP_ENDPOINT= โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ SSH โ”‚ http://localhost:9222 โ”‚ + tunnel โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Step 1: Start Chrome with Remote Debugging (Local Machine) + +**macOS:** +```bash +/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \ + --remote-debugging-port=9222 \ + --user-data-dir="$HOME/chrome-debug-profile" +``` + +**Linux:** +```bash +google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/chrome-debug-profile" +``` + +**Windows:** +```cmd +"C:\Program Files\Google\Chrome\Application\chrome.exe" ^ + --remote-debugging-port=9222 ^ + --user-data-dir="%USERPROFILE%\chrome-debug-profile" +``` + +### Step 2: Log Into Target Websites + +Open Chrome and log into the websites you want to use (e.g., bilibili.com, zhihu.com). + +### Step 3: Create SSH Tunnel (Local Machine) + +Forward the debugging port to your server: + +```bash +ssh -R 9222:localhost:9222 your-server +``` + +### Step 4: Run OpenCLI on Server + +```bash +export OPENCLI_CDP_ENDPOINT="http://localhost:9222" +opencli doctor # Verify connection +opencli bilibili hot --limit 5 # Test a command +``` + +### Persistent Configuration + +Add to your shell profile (`~/.bashrc` or `~/.zshrc`): + +```bash +export OPENCLI_CDP_ENDPOINT="http://localhost:9222" +``` + ## Testing See **[TESTING.md](./TESTING.md)** for the full testing guide, including: diff --git a/README.zh-CN.md b/README.zh-CN.md index c994183..8ca2845 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -21,6 +21,7 @@ OpenCLI ๅฐ†ไปปไฝ•็ฝ‘็ซ™ๅ˜ๆˆๅ‘ฝไปค่กŒๅทฅๅ…ท โ€” B็ซ™ใ€็ŸฅไนŽใ€ๅฐ็บขไนฆใ€Twi - [ๅ†…็ฝฎๅ‘ฝไปค](#ๅ†…็ฝฎๅ‘ฝไปค) - [่พ“ๅ‡บๆ ผๅผ](#่พ“ๅ‡บๆ ผๅผ) - [่‡ด AI Agent๏ผˆๅผ€ๅ‘่€…ๆŒ‡ๅ—๏ผ‰](#่‡ด-ai-agentๅผ€ๅ‘่€…ๆŒ‡ๅ—) +- [่ฟœ็จ‹ Chrome๏ผˆๆœๅŠกๅ™จ/ๆ— ๅคด็Žฏๅขƒ๏ผ‰](#่ฟœ็จ‹-chromeๆœๅŠกๅ™จๆ— ๅคด็Žฏๅขƒ) - [ๅธธ่ง้—ฎ้ข˜ๆŽ’ๆŸฅ](#ๅธธ่ง้—ฎ้ข˜ๆŽ’ๆŸฅ) - [็‰ˆๆœฌๅ‘ๅธƒ](#็‰ˆๆœฌๅ‘ๅธƒ) - [License](#license) @@ -196,6 +197,70 @@ opencli cascade https://api.example.com/data ๆŽข็ดข็ป“ๆžœ่พ“ๅ‡บๅˆฐ `.opencli/explore//`ใ€‚ +## ่ฟœ็จ‹ Chrome๏ผˆๆœๅŠกๅ™จ/ๆ— ๅคด็Žฏๅขƒ๏ผ‰ + +ๅœจๆฒกๆœ‰ๆ˜พ็คบๅ™จ็š„ๆœๅŠกๅ™จ็Žฏๅขƒไธญ๏ผŒๅฏไปฅ้€š่ฟ‡ Chrome DevTools Protocol (CDP) ่ฟžๆŽฅๅˆฐๆœฌๅœฐ็”ต่„‘ไธŠ่ฟ่กŒ็š„ Chrome ๆต่งˆๅ™จใ€‚ + +### ๅทฅไฝœๅŽŸ็† + +``` +ๆœฌๅœฐ็”ต่„‘ ๆœๅŠกๅ™จ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Chrome๏ผˆๅทฒ็™ปๅฝ•็›ฎๆ ‡็ฝ‘็ซ™๏ผ‰ โ”‚ โ”‚ OpenCLI โ”‚ +โ”‚ --remote-debugging-port=9222 โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ OPENCLI_CDP_ENDPOINT= โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ SSH โ”‚ http://localhost:9222 โ”‚ + ้šง้“ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### ็ฌฌไธ€ๆญฅ๏ผšๅฏๅŠจๅธฆ่ฟœ็จ‹่ฐƒ่ฏ•็š„ Chrome๏ผˆๆœฌๅœฐ็”ต่„‘๏ผ‰ + +**macOS:** +```bash +/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \ + --remote-debugging-port=9222 \ + --user-data-dir="$HOME/chrome-debug-profile" +``` + +**Linux:** +```bash +google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/chrome-debug-profile" +``` + +**Windows:** +```cmd +"C:\Program Files\Google\Chrome\Application\chrome.exe" ^ + --remote-debugging-port=9222 ^ + --user-data-dir="%USERPROFILE%\chrome-debug-profile" +``` + +### ็ฌฌไบŒๆญฅ๏ผš็™ปๅฝ•็›ฎๆ ‡็ฝ‘็ซ™ + +ๆ‰“ๅผ€ Chrome ๅนถ็™ปๅฝ•ไฝ ่ฆไฝฟ็”จ็š„็ฝ‘็ซ™๏ผˆๅฆ‚ bilibili.comใ€zhihu.com๏ผ‰ใ€‚ + +### ็ฌฌไธ‰ๆญฅ๏ผšๅปบ็ซ‹ SSH ้šง้“๏ผˆๆœฌๅœฐ็”ต่„‘๏ผ‰ + +ๅฐ†่ฐƒ่ฏ•็ซฏๅฃ่ฝฌๅ‘ๅˆฐๆœๅŠกๅ™จ๏ผš + +```bash +ssh -R 9222:localhost:9222 your-server +``` + +### ็ฌฌๅ››ๆญฅ๏ผšๅœจๆœๅŠกๅ™จไธŠ่ฟ่กŒ OpenCLI + +```bash +export OPENCLI_CDP_ENDPOINT="http://localhost:9222" +opencli doctor # ้ชŒ่ฏ่ฟžๆŽฅ +opencli bilibili hot --limit 5 # ๆต‹่ฏ•ๅ‘ฝไปค +``` + +### ๆŒไน…ๅŒ–้…็ฝฎ + +ๆทปๅŠ ๅˆฐ shell ้…็ฝฎๆ–‡ไปถ๏ผˆ`~/.bashrc` ๆˆ– `~/.zshrc`๏ผ‰๏ผš + +```bash +export OPENCLI_CDP_ENDPOINT="http://localhost:9222" +``` + ## ๅธธ่ง้—ฎ้ข˜ๆŽ’ๆŸฅ - **"Failed to connect to Playwright MCP Bridge"** ๆŠฅ้”™ diff --git a/src/browser/discover.ts b/src/browser/discover.ts index 56f8538..3ca2fcb 100644 --- a/src/browser/discover.ts +++ b/src/browser/discover.ts @@ -75,13 +75,87 @@ export function findMcpServerPath(): string | null { return _cachedMcpServerPath; } -export function buildMcpArgs(input: { mcpPath: string; executablePath?: string | null }): string[] { +/** + * Chrome 144+ auto-discovery: read DevToolsActivePort file to get CDP endpoint. + * + * Starting with Chrome 144, users can enable remote debugging from + * chrome://inspect#remote-debugging without any command-line flags. + * Chrome writes the active port and browser GUID to a DevToolsActivePort file + * in the user data directory, which we read to construct the WebSocket endpoint. + */ +export function discoverChromeEndpoint(): string | null { + const candidates: string[] = []; + + // User-specified Chrome data dir takes highest priority + if (process.env.CHROME_USER_DATA_DIR) { + candidates.push(path.join(process.env.CHROME_USER_DATA_DIR, 'DevToolsActivePort')); + } + + // Standard Chrome/Edge user data dirs per platform + if (process.platform === 'win32') { + const localAppData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local'); + candidates.push(path.join(localAppData, 'Google', 'Chrome', 'User Data', 'DevToolsActivePort')); + candidates.push(path.join(localAppData, 'Microsoft', 'Edge', 'User Data', 'DevToolsActivePort')); + } else if (process.platform === 'darwin') { + candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'DevToolsActivePort')); + candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Microsoft Edge', 'DevToolsActivePort')); + } else { + candidates.push(path.join(os.homedir(), '.config', 'google-chrome', 'DevToolsActivePort')); + candidates.push(path.join(os.homedir(), '.config', 'chromium', 'DevToolsActivePort')); + candidates.push(path.join(os.homedir(), '.config', 'microsoft-edge', 'DevToolsActivePort')); + } + + for (const filePath of candidates) { + try { + const content = fs.readFileSync(filePath, 'utf-8').trim(); + const lines = content.split('\n'); + if (lines.length >= 2) { + const port = parseInt(lines[0], 10); + const browserPath = lines[1]; // e.g. /devtools/browser/ + if (port > 0 && browserPath.startsWith('/devtools/browser/')) { + return `ws://127.0.0.1:${port}${browserPath}`; + } + } + } catch {} + } + return null; +} + +export function resolveCdpEndpoint(): { endpoint?: string; requestedCdp: boolean } { + const envVal = process.env.OPENCLI_CDP_ENDPOINT; + if (envVal === '1' || envVal?.toLowerCase() === 'true') { + const autoDiscovered = discoverChromeEndpoint(); + return { endpoint: autoDiscovered ?? envVal, requestedCdp: true }; + } + + if (envVal) { + return { endpoint: envVal, requestedCdp: true }; + } + + // Fallback to auto-discovery if not explicitly set + const autoDiscovered = discoverChromeEndpoint(); + if (autoDiscovered) { + return { endpoint: autoDiscovered, requestedCdp: true }; + } + + return { requestedCdp: false }; +} + +export function buildMcpArgs(input: { mcpPath: string; executablePath?: string | null; cdpEndpoint?: string }): string[] { const args = [input.mcpPath]; + + // Priority 1: CDP endpoint (remote Chrome debugging or local Auto-Discovery) + if (input.cdpEndpoint) { + args.push('--cdp-endpoint', input.cdpEndpoint); + return args; + } + + // Priority 2: Extension mode (local Chrome with MCP Bridge extension) if (!process.env.CI) { - // Local: always connect to user's running Chrome via MCP Bridge extension args.push('--extension'); } - // CI: standalone mode โ€” @playwright/mcp launches its own browser (headed by default). + + // CI/standalone mode: @playwright/mcp launches its own browser (headed by default). // xvfb provides a virtual display for headed mode in GitHub Actions. if (input.executablePath) { args.push('--executable-path', input.executablePath); diff --git a/src/browser/errors.ts b/src/browser/errors.ts index c03e743..db41fbb 100644 --- a/src/browser/errors.ts +++ b/src/browser/errors.ts @@ -4,7 +4,7 @@ import { createHash } from 'node:crypto'; -export type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'mcp-init' | 'process-exit' | 'unknown'; +export type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'mcp-init' | 'process-exit' | 'cdp-connection-failed' | 'unknown'; export type ConnectFailureInput = { kind: ConnectFailureKind; @@ -26,6 +26,15 @@ export function formatBrowserConnectError(input: ConnectFailureInput): Error { const suffix = stderr ? `\n\nMCP stderr:\n${stderr}` : ''; const tokenHint = input.tokenFingerprint ? ` Token fingerprint: ${input.tokenFingerprint}.` : ''; + if (input.kind === 'cdp-connection-failed') { + return new Error( + `Failed to connect to remote Chrome via CDP endpoint.\n\n` + + `Check if Chrome is running with remote debugging enabled (--remote-debugging-port=9222) or DevToolsActivePort is available under chrome://inspect#remote-debugging.\n` + + `If you specified OPENCLI_CDP_ENDPOINT=1, auto-discovery might have failed.` + + suffix, + ); + } + if (input.kind === 'missing-token') { return new Error( 'Failed to connect to Playwright MCP Bridge: PLAYWRIGHT_MCP_EXTENSION_TOKEN is not set.\n\n' + @@ -74,9 +83,16 @@ export function inferConnectFailureKind(args: { stderr: string; rawMessage?: string; exited?: boolean; + isCdpMode?: boolean; }): ConnectFailureKind { const haystack = `${args.rawMessage ?? ''}\n${args.stderr}`.toLowerCase(); + if (args.isCdpMode) { + if (args.rawMessage?.startsWith('MCP init failed:')) return 'mcp-init'; + if (args.exited) return 'cdp-connection-failed'; + return 'cdp-connection-failed'; + } + if (!args.hasExtensionToken) return 'missing-token'; if (haystack.includes('extension connection timeout') || haystack.includes('playwright mcp bridge')) diff --git a/src/browser/index.ts b/src/browser/index.ts index afe502d..7fd4c92 100644 --- a/src/browser/index.ts +++ b/src/browser/index.ts @@ -13,7 +13,8 @@ export type { ConnectFailureKind, ConnectFailureInput } from './errors.js'; // Test-only helpers โ€” exposed for unit tests import { createJsonRpcRequest } from './mcp.js'; import { extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js'; -import { buildMcpArgs } from './discover.js'; +import { buildMcpArgs, resolveCdpEndpoint } from './discover.js'; +export { resolveCdpEndpoint } from './discover.js'; import { withTimeoutMs } from '../runtime.js'; export const __test__ = { diff --git a/src/browser/mcp.ts b/src/browser/mcp.ts index c7ee855..c5ebd3a 100644 --- a/src/browser/mcp.ts +++ b/src/browser/mcp.ts @@ -9,7 +9,7 @@ import { withTimeoutMs, DEFAULT_BROWSER_CONNECT_TIMEOUT } from '../runtime.js'; import { PKG_VERSION } from '../version.js'; import { Page } from './page.js'; import { getTokenFingerprint, formatBrowserConnectError, inferConnectFailureKind } from './errors.js'; -import { findMcpServerPath, buildMcpArgs } from './discover.js'; +import { findMcpServerPath, buildMcpArgs, resolveCdpEndpoint } from './discover.js'; import { extractTabIdentities, extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js'; const STDERR_BUFFER_LIMIT = 16 * 1024; @@ -115,7 +115,8 @@ export class PlaywrightMCP { return new Promise((resolve, reject) => { const isDebug = process.env.DEBUG?.includes('opencli:mcp'); const debugLog = (msg: string) => isDebug && console.error(`[opencli:mcp] ${msg}`); - const useExtension = !!process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN; + const { endpoint: cdpEndpoint, requestedCdp } = resolveCdpEndpoint(); + const useExtension = !requestedCdp; const extensionToken = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN; const tokenFingerprint = getTokenFingerprint(extensionToken); let stderrBuffer = ''; @@ -151,15 +152,17 @@ export class PlaywrightMCP { settleError(inferConnectFailureKind({ hasExtensionToken: !!extensionToken, stderr: stderrBuffer, + isCdpMode: requestedCdp, })); }, timeout * 1000); const mcpArgs = buildMcpArgs({ mcpPath, executablePath: process.env.OPENCLI_BROWSER_EXECUTABLE_PATH, + cdpEndpoint, }); if (process.env.OPENCLI_VERBOSE) { - console.error(`[opencli] Mode: ${useExtension ? 'extension' : 'standalone'}`); + console.error(`[opencli] Mode: ${requestedCdp ? 'CDP' : useExtension ? 'extension' : 'standalone'}`); if (useExtension) console.error(`[opencli] Extension token: fingerprint ${tokenFingerprint}`); } debugLog(`Spawning node ${mcpArgs.join(' ')}`); @@ -216,6 +219,7 @@ export class PlaywrightMCP { hasExtensionToken: !!extensionToken, stderr: stderrBuffer, exited: true, + isCdpMode: requestedCdp, }), { exitCode: code }); } }); @@ -233,6 +237,7 @@ export class PlaywrightMCP { hasExtensionToken: !!extensionToken, stderr: stderrBuffer, rawMessage: `MCP init failed: ${resp.error.message}`, + isCdpMode: requestedCdp, }), { rawMessage: resp.error.message }); return; } diff --git a/src/doctor.ts b/src/doctor.ts index 98066a2..3e75210 100644 --- a/src/doctor.ts +++ b/src/doctor.ts @@ -591,6 +591,15 @@ export function renderBrowserDoctorReport(report: DoctorReport): string { const hasMismatch = uniqueFingerprints.length > 1; const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`), '']; + // CDP endpoint mode (for remote/server environments) + const cdpEndpoint = process.env.OPENCLI_CDP_ENDPOINT; + if (cdpEndpoint) { + lines.push(statusLine('OK', `CDP endpoint: ${chalk.cyan(cdpEndpoint)}`)); + lines.push(chalk.dim(' โ†’ Remote Chrome mode: extension token not required')); + lines.push(''); + return lines.join('\n'); + } + const installStatus: ReportStatus = report.extensionInstalled ? 'OK' : 'MISSING'; const installDetail = report.extensionInstalled ? `Extension installed (${report.extensionBrowsers.join(', ')})` From b8ca5f343793719a2de890dc0abd7d9ac6a068fb Mon Sep 17 00:00:00 2001 From: jackwener Date: Tue, 17 Mar 2026 00:09:17 +0800 Subject: [PATCH 2/3] feat(browser): add Chrome DevToolsActivePort auto-discovery for CDP mode This adds support for reading DevToolsActivePort (Chrome 144+) when OPENCLI_CDP_ENDPOINT=1, and fixes a bug where CDP connection failures were incorrectly reported as missing extension tokens. --- src/doctor.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/doctor.ts b/src/doctor.ts index 3e75210..a8cf1b6 100644 --- a/src/doctor.ts +++ b/src/doctor.ts @@ -6,7 +6,7 @@ import { createInterface } from 'node:readline/promises'; import { stdin as input, stdout as output } from 'node:process'; import chalk from 'chalk'; import type { IPage } from './types.js'; -import { PlaywrightMCP, getTokenFingerprint } from './browser/index.js'; +import { PlaywrightMCP, getTokenFingerprint, resolveCdpEndpoint } from './browser/index.js'; import { browserSession } from './runtime.js'; const PLAYWRIGHT_SERVER_NAME = 'playwright'; @@ -592,9 +592,13 @@ export function renderBrowserDoctorReport(report: DoctorReport): string { const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`), '']; // CDP endpoint mode (for remote/server environments) - const cdpEndpoint = process.env.OPENCLI_CDP_ENDPOINT; - if (cdpEndpoint) { - lines.push(statusLine('OK', `CDP endpoint: ${chalk.cyan(cdpEndpoint)}`)); + const { endpoint: cdpEndpoint, requestedCdp } = resolveCdpEndpoint(); + if (requestedCdp) { + if (cdpEndpoint) { + lines.push(statusLine('OK', `CDP endpoint: ${chalk.cyan(cdpEndpoint)}`)); + } else { + lines.push(statusLine('MISSING', 'CDP endpoint (auto-discovery failed)')); + } lines.push(chalk.dim(' โ†’ Remote Chrome mode: extension token not required')); lines.push(''); return lines.join('\n'); From da8ec1a78d4951df772bb20d7b3d619d71a22789 Mon Sep 17 00:00:00 2001 From: ByteYue Date: Tue, 17 Mar 2026 22:45:31 +0800 Subject: [PATCH 3/3] docs: rewrite Remote Chrome section with Chrome 144+ auto-discovery and classic CDP modes - Document two connection methods: Chrome 144+ (no restart, ws://) and classic --remote-debugging-port (any version, http://) - Add DevToolsActivePort file paths for all platforms (macOS, Linux, Windows) and browsers (Chrome, Chromium, Edge) - Add environment variable reference table (OPENCLI_CDP_ENDPOINT, CHROME_USER_DATA_DIR) - Add buildMcpArgs CDP unit test covering ws:// and http:// endpoints - Update both README.md and README.zh-CN.md --- README.md | 115 +++++++++++++++++++++++++++++++------------ README.zh-CN.md | 117 ++++++++++++++++++++++++++++++++------------ src/browser.test.ts | 33 +++++++++++++ 3 files changed, 204 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 1edf124..040acd7 100644 --- a/README.md +++ b/README.md @@ -200,66 +200,121 @@ Explore outputs to `.opencli/explore//` (manifest.json, endpoints.json, ca ## Remote Chrome (Server/Headless) -For server environments without a display, you can connect OpenCLI to a Chrome browser running on your local machine via Chrome DevTools Protocol (CDP). +For server environments without a display, connect OpenCLI to a Chrome browser running on your local machine via Chrome DevTools Protocol (CDP). Two methods are available: -### How It Works +| Method | Chrome restart? | Chrome version | Endpoint format | +|--------|:-:|:-:|:-:| +| **A. Chrome 144+ Auto-Discovery** | No | โ‰ฅ 144 | `ws://` | +| **B. Classic `--remote-debugging-port`** | Yes | Any | `http://` | -``` -Local Machine Server -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Chrome (logged into sites) โ”‚ โ”‚ OpenCLI โ”‚ -โ”‚ --remote-debugging-port=9222 โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ OPENCLI_CDP_ENDPOINT= โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ SSH โ”‚ http://localhost:9222 โ”‚ - tunnel โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` +--- -### Step 1: Start Chrome with Remote Debugging (Local Machine) +### Method A: Chrome 144+ (No Restart Required) -**macOS:** -```bash -/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \ - --remote-debugging-port=9222 \ - --user-data-dir="$HOME/chrome-debug-profile" -``` +Use your **already-running Chrome** โ€” no command-line flags needed. + +**Step 1 โ€” Enable remote debugging in Chrome** + +Navigate to `chrome://inspect#remote-debugging` and check "Allow remote debugging". + +**Step 2 โ€” Get the WebSocket URL** + +Read Chrome's `DevToolsActivePort` file to get the port and browser GUID: -**Linux:** ```bash -google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/chrome-debug-profile" +# macOS (Chrome) +cat ~/Library/Application\ Support/Google/Chrome/DevToolsActivePort + +# macOS (Edge) +cat ~/Library/Application\ Support/Microsoft\ Edge/DevToolsActivePort + +# Linux (Chrome) +cat ~/.config/google-chrome/DevToolsActivePort + +# Linux (Chromium) +cat ~/.config/chromium/DevToolsActivePort ``` -**Windows:** ```cmd -"C:\Program Files\Google\Chrome\Application\chrome.exe" ^ - --remote-debugging-port=9222 ^ - --user-data-dir="%USERPROFILE%\chrome-debug-profile" +:: Windows (Chrome) +type "%LOCALAPPDATA%\Google\Chrome\User Data\DevToolsActivePort" + +:: Windows (Edge) +type "%LOCALAPPDATA%\Microsoft\Edge\User Data\DevToolsActivePort" ``` -### Step 2: Log Into Target Websites +Output: +``` +61882 +/devtools/browser/9f395fbe-24cb-4075-b58f-dd1c4f6eb172 +``` -Open Chrome and log into the websites you want to use (e.g., bilibili.com, zhihu.com). +**Step 3 โ€” SSH tunnel + run OpenCLI** -### Step 3: Create SSH Tunnel (Local Machine) +```bash +# On your local machine โ€” forward the port to your server +ssh -R 61882:localhost:61882 your-server -Forward the debugging port to your server: +# On the server +export OPENCLI_CDP_ENDPOINT="ws://localhost:61882/devtools/browser/9f395fbe-..." +opencli doctor # Verify connection +opencli bilibili hot --limit 5 # Test a command +``` + +> **Same-machine shortcut**: If Chrome and OpenCLI run on the same machine, auto-discovery reads `DevToolsActivePort` automatically โ€” no env var needed, or set `OPENCLI_CDP_ENDPOINT=1` to force it. + +> **Note**: The port and GUID change every time Chrome restarts or re-enables remote debugging. You'll need to re-read `DevToolsActivePort` and update the env var. + +--- + +### Method B: Classic `--remote-debugging-port` (Any Chrome Version) + +Requires restarting Chrome with a flag, but works with any Chrome version and provides a stable HTTP endpoint. + +**Step 1 โ€” Start Chrome with remote debugging** ```bash -ssh -R 9222:localhost:9222 your-server +# macOS +/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \ + --remote-debugging-port=9222 + +# Linux +google-chrome --remote-debugging-port=9222 + +# Windows +"C:\Program Files\Google\Chrome\Application\chrome.exe" ^ + --remote-debugging-port=9222 ``` -### Step 4: Run OpenCLI on Server +**Step 2 โ€” Log into target websites** in that Chrome window. + +**Step 3 โ€” SSH tunnel + run OpenCLI** ```bash +# On your local machine +ssh -R 9222:localhost:9222 your-server + +# On the server export OPENCLI_CDP_ENDPOINT="http://localhost:9222" opencli doctor # Verify connection opencli bilibili hot --limit 5 # Test a command ``` +--- + +### Environment Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `OPENCLI_CDP_ENDPOINT` | CDP endpoint URL (`ws://` or `http://`), or `1` to force auto-discovery | `ws://localhost:61882/devtools/browser/...` | +| `CHROME_USER_DATA_DIR` | Override Chrome user data directory for `DevToolsActivePort` discovery | `/home/user/.config/google-chrome` | + ### Persistent Configuration Add to your shell profile (`~/.bashrc` or `~/.zshrc`): ```bash -export OPENCLI_CDP_ENDPOINT="http://localhost:9222" +export OPENCLI_CDP_ENDPOINT="ws://localhost:61882/devtools/browser/..." ``` ## Testing diff --git a/README.zh-CN.md b/README.zh-CN.md index 8ca2845..4c55501 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -199,66 +199,121 @@ opencli cascade https://api.example.com/data ## ่ฟœ็จ‹ Chrome๏ผˆๆœๅŠกๅ™จ/ๆ— ๅคด็Žฏๅขƒ๏ผ‰ -ๅœจๆฒกๆœ‰ๆ˜พ็คบๅ™จ็š„ๆœๅŠกๅ™จ็Žฏๅขƒไธญ๏ผŒๅฏไปฅ้€š่ฟ‡ Chrome DevTools Protocol (CDP) ่ฟžๆŽฅๅˆฐๆœฌๅœฐ็”ต่„‘ไธŠ่ฟ่กŒ็š„ Chrome ๆต่งˆๅ™จใ€‚ +ๅœจๆœๅŠกๅ™จ๏ผˆๆ— ๆ˜พ็คบๅ™จ๏ผ‰็Žฏๅขƒไธญ๏ผŒ้€š่ฟ‡ Chrome DevTools Protocol (CDP) ่ฟžๆŽฅๅˆฐๆœฌๅœฐ็”ต่„‘ไธŠ่ฟ่กŒ็š„ Chromeใ€‚ๆ”ฏๆŒไธค็งๆ–นๅผ๏ผš -### ๅทฅไฝœๅŽŸ็† +| ๆ–นๅผ | ้œ€้‡ๅฏ Chrome๏ผŸ | Chrome ็‰ˆๆœฌ | ็ซฏ็‚นๆ ผๅผ | +|------|:-:|:-:|:-:| +| **A. Chrome 144+ ่‡ชๅŠจๅ‘็Žฐ** | ๅฆ | โ‰ฅ 144 | `ws://` | +| **B. ็ปๅ…ธ `--remote-debugging-port`** | ๆ˜ฏ | ไปปๆ„ | `http://` | -``` -ๆœฌๅœฐ็”ต่„‘ ๆœๅŠกๅ™จ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Chrome๏ผˆๅทฒ็™ปๅฝ•็›ฎๆ ‡็ฝ‘็ซ™๏ผ‰ โ”‚ โ”‚ OpenCLI โ”‚ -โ”‚ --remote-debugging-port=9222 โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ OPENCLI_CDP_ENDPOINT= โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ SSH โ”‚ http://localhost:9222 โ”‚ - ้šง้“ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` +--- -### ็ฌฌไธ€ๆญฅ๏ผšๅฏๅŠจๅธฆ่ฟœ็จ‹่ฐƒ่ฏ•็š„ Chrome๏ผˆๆœฌๅœฐ็”ต่„‘๏ผ‰ +### ๆ–นๅผ A๏ผšChrome 144+๏ผˆๆ— ้œ€้‡ๅฏ๏ผ‰ -**macOS:** -```bash -/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \ - --remote-debugging-port=9222 \ - --user-data-dir="$HOME/chrome-debug-profile" -``` +็›ดๆŽฅๅค็”จ**ๅทฒ่ฟ่กŒ็š„ Chrome**๏ผŒไธ้œ€่ฆไปปไฝ•ๅ‘ฝไปค่กŒๅ‚ๆ•ฐใ€‚ + +**็ฌฌไธ€ๆญฅ โ€” ๅœจ Chrome ไธญๅผ€ๅฏ่ฟœ็จ‹่ฐƒ่ฏ•** + +ๆ‰“ๅผ€ `chrome://inspect#remote-debugging`๏ผŒๅ‹พ้€‰"ๅ…่ฎธ่ฟœ็จ‹่ฐƒ่ฏ•"ใ€‚ + +**็ฌฌไบŒๆญฅ โ€” ่Žทๅ– WebSocket URL** + +่ฏปๅ– Chrome ็š„ `DevToolsActivePort` ๆ–‡ไปถ่Žทๅ–็ซฏๅฃๅ’Œๆต่งˆๅ™จ GUID๏ผš -**Linux:** ```bash -google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/chrome-debug-profile" +# macOS (Chrome) +cat ~/Library/Application\ Support/Google/Chrome/DevToolsActivePort + +# macOS (Edge) +cat ~/Library/Application\ Support/Microsoft\ Edge/DevToolsActivePort + +# Linux (Chrome) +cat ~/.config/google-chrome/DevToolsActivePort + +# Linux (Chromium) +cat ~/.config/chromium/DevToolsActivePort ``` -**Windows:** ```cmd -"C:\Program Files\Google\Chrome\Application\chrome.exe" ^ - --remote-debugging-port=9222 ^ - --user-data-dir="%USERPROFILE%\chrome-debug-profile" +:: Windows (Chrome) +type "%LOCALAPPDATA%\Google\Chrome\User Data\DevToolsActivePort" + +:: Windows (Edge) +type "%LOCALAPPDATA%\Microsoft\Edge\User Data\DevToolsActivePort" ``` -### ็ฌฌไบŒๆญฅ๏ผš็™ปๅฝ•็›ฎๆ ‡็ฝ‘็ซ™ +่พ“ๅ‡บ็คบไพ‹๏ผš +``` +61882 +/devtools/browser/9f395fbe-24cb-4075-b58f-dd1c4f6eb172 +``` -ๆ‰“ๅผ€ Chrome ๅนถ็™ปๅฝ•ไฝ ่ฆไฝฟ็”จ็š„็ฝ‘็ซ™๏ผˆๅฆ‚ bilibili.comใ€zhihu.com๏ผ‰ใ€‚ +**็ฌฌไธ‰ๆญฅ โ€” SSH ้šง้“ + ่ฟ่กŒ OpenCLI** -### ็ฌฌไธ‰ๆญฅ๏ผšๅปบ็ซ‹ SSH ้šง้“๏ผˆๆœฌๅœฐ็”ต่„‘๏ผ‰ +```bash +# ๆœฌๅœฐ็”ต่„‘ โ€” ๅฐ†็ซฏๅฃ่ฝฌๅ‘ๅˆฐๆœๅŠกๅ™จ +ssh -R 61882:localhost:61882 your-server -ๅฐ†่ฐƒ่ฏ•็ซฏๅฃ่ฝฌๅ‘ๅˆฐๆœๅŠกๅ™จ๏ผš +# ๅœจๆœๅŠกๅ™จไธŠ +export OPENCLI_CDP_ENDPOINT="ws://localhost:61882/devtools/browser/9f395fbe-..." +opencli doctor # ้ชŒ่ฏ่ฟžๆŽฅ +opencli bilibili hot --limit 5 # ๆต‹่ฏ•ๅ‘ฝไปค +``` + +> **ๅŒๆœบๅฟซๆทๆ–นๅผ**๏ผšๅฆ‚ๆžœ Chrome ๅ’Œ OpenCLI ๅœจๅŒไธ€ๅฐๆœบๅ™จไธŠ่ฟ่กŒ๏ผŒauto-discovery ไผš่‡ชๅŠจ่ฏปๅ– `DevToolsActivePort`๏ผŒๆ— ้œ€่ฎพ็ฝฎ็Žฏๅขƒๅ˜้‡๏ผŒๆˆ–่ฎพ็ฝฎ `OPENCLI_CDP_ENDPOINT=1` ๅผบๅˆถๅฏ็”จใ€‚ + +> **ๆณจๆ„**๏ผšๆฏๆฌก Chrome ้‡ๅฏๆˆ–้‡ๆ–ฐๅฏ็”จ่ฟœ็จ‹่ฐƒ่ฏ•ๅŽ๏ผŒ็ซฏๅฃๅ’Œ GUID ไผšๆ”นๅ˜๏ผŒ้œ€่ฆ้‡ๆ–ฐ่ฏปๅ– `DevToolsActivePort` ๅนถๆ›ดๆ–ฐ็Žฏๅขƒๅ˜้‡ใ€‚ + +--- + +### ๆ–นๅผ B๏ผš็ปๅ…ธ `--remote-debugging-port`๏ผˆไปปๆ„ Chrome ็‰ˆๆœฌ๏ผ‰ + +้œ€่ฆ็”จๅ‘ฝไปค่กŒๅ‚ๆ•ฐ้‡ๅฏ Chrome๏ผŒไฝ†ๅ…ผๅฎนๆ‰€ๆœ‰ Chrome ็‰ˆๆœฌ๏ผŒไธ” HTTP ็ซฏ็‚น็จณๅฎšไธๅ˜ใ€‚ + +**็ฌฌไธ€ๆญฅ โ€” ๅฏๅŠจๅธฆ่ฟœ็จ‹่ฐƒ่ฏ•็š„ Chrome** ```bash -ssh -R 9222:localhost:9222 your-server +# macOS +/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \ + --remote-debugging-port=9222 + +# Linux +google-chrome --remote-debugging-port=9222 + +# Windows +"C:\Program Files\Google\Chrome\Application\chrome.exe" ^ + --remote-debugging-port=9222 ``` -### ็ฌฌๅ››ๆญฅ๏ผšๅœจๆœๅŠกๅ™จไธŠ่ฟ่กŒ OpenCLI +**็ฌฌไบŒๆญฅ โ€” ็™ปๅฝ•็›ฎๆ ‡็ฝ‘็ซ™** + +**็ฌฌไธ‰ๆญฅ โ€” SSH ้šง้“ + ่ฟ่กŒ OpenCLI** ```bash +# ๆœฌๅœฐ็”ต่„‘ +ssh -R 9222:localhost:9222 your-server + +# ๅœจๆœๅŠกๅ™จไธŠ export OPENCLI_CDP_ENDPOINT="http://localhost:9222" opencli doctor # ้ชŒ่ฏ่ฟžๆŽฅ opencli bilibili hot --limit 5 # ๆต‹่ฏ•ๅ‘ฝไปค ``` +--- + +### ็Žฏๅขƒๅ˜้‡ + +| ๅ˜้‡ | ่ฏดๆ˜Ž | ็คบไพ‹ | +|------|------|------| +| `OPENCLI_CDP_ENDPOINT` | CDP ็ซฏ็‚น URL๏ผˆ`ws://` ๆˆ– `http://`๏ผ‰๏ผŒๆˆ– `1` ๅผบๅˆถ่‡ชๅŠจๅ‘็Žฐ | `ws://localhost:61882/devtools/browser/...` | +| `CHROME_USER_DATA_DIR` | ่‡ชๅฎšไน‰ Chrome ็”จๆˆทๆ•ฐๆฎ็›ฎๅฝ•๏ผˆ็”จไบŽ `DevToolsActivePort` ๅ‘็Žฐ๏ผ‰ | `/home/user/.config/google-chrome` | + ### ๆŒไน…ๅŒ–้…็ฝฎ -ๆทปๅŠ ๅˆฐ shell ้…็ฝฎๆ–‡ไปถ๏ผˆ`~/.bashrc` ๆˆ– `~/.zshrc`๏ผ‰๏ผš +ๅ†™ๅ…ฅ shell ้…็ฝฎๆ–‡ไปถ๏ผˆ`~/.bashrc` ๆˆ– `~/.zshrc`๏ผ‰๏ผš ```bash -export OPENCLI_CDP_ENDPOINT="http://localhost:9222" +export OPENCLI_CDP_ENDPOINT="ws://localhost:61882/devtools/browser/..." ``` ## ๅธธ่ง้—ฎ้ข˜ๆŽ’ๆŸฅ diff --git a/src/browser.test.ts b/src/browser.test.ts index e3ad022..b6173ab 100644 --- a/src/browser.test.ts +++ b/src/browser.test.ts @@ -106,6 +106,39 @@ describe('browser helpers', () => { } }); + it('builds CDP MCP args when cdpEndpoint is provided', () => { + const savedCI = process.env.CI; + delete process.env.CI; + try { + // CDP mode: --cdp-endpoint takes priority, no --extension + expect(__test__.buildMcpArgs({ + mcpPath: '/tmp/cli.js', + cdpEndpoint: 'ws://localhost:9222/devtools/browser/abc-123', + })).toEqual([ + '/tmp/cli.js', + '--cdp-endpoint', + 'ws://localhost:9222/devtools/browser/abc-123', + ]); + + // CDP with executable path โ€” executable path is ignored in CDP mode + expect(__test__.buildMcpArgs({ + mcpPath: '/tmp/cli.js', + cdpEndpoint: 'http://localhost:9222', + executablePath: '/usr/bin/chromium', + })).toEqual([ + '/tmp/cli.js', + '--cdp-endpoint', + 'http://localhost:9222', + ]); + } finally { + if (savedCI !== undefined) { + process.env.CI = savedCI; + } else { + delete process.env.CI; + } + } + }); + it('times out slow promises', async () => { await expect(__test__.withTimeoutMs(new Promise(() => {}), 10, 'timeout')).rejects.toThrow('timeout'); });