diff --git a/README.md b/README.md index 077ca2b..f47fd3f 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ A CLI tool that turns **any website** into a command-line interface. **57 comman > **⚠️ Important**: Browser commands reuse your Chrome login session. You must be logged into the target website in Chrome before running commands. If you get empty data or errors, check your login status first. -OpenCLI needs a way to communicate with your browser. We highly recommend configuring **both** of the following methods for maximum reliability. +OpenCLI connects to your browser through the Playwright MCP Bridge extension. -### Connection Method A: Playwright MCP Bridge Extension (Primary) +### Playwright MCP Bridge Extension Setup 1. Install **[Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm)** extension in Chrome. 2. Obtain your token by clicking the extension icon in the browser toolbar or from the extension settings page. @@ -72,16 +72,6 @@ And, so that `opencli` commands can use it directly in the terminal, export it i export PLAYWRIGHT_MCP_EXTENSION_TOKEN="" ``` -### Connection Method B: Chrome 144+ Auto-Discovery (Fallback) - -No extensions needed. Just enable Chrome's built-in remote debugging: - -1. Open `chrome://inspect#remote-debugging` in Chrome -2. Check **"Allow remote debugging for this browser instance"** -3. Set `OPENCLI_USE_CDP=1` before running opencli - -*You can also manually specify an endpoint via `OPENCLI_CDP_ENDPOINT` env var.* - ## Quick Start ### Install via npm (recommended) @@ -184,8 +174,6 @@ Explore outputs to `.opencli/explore//` (manifest.json, endpoints.json, ca - **"Failed to connect to Playwright MCP Bridge"** - Ensure the Playwright MCP extension is installed and **enabled** in your running Chrome. - Restart the Chrome browser if you just installed the extension. -- **"CDP command failed" or "boss search blocked"** - - Some sites (like BOSS Zhipin) actively block Chrome DevTools Protocol connections. OpenCLI falls back to cookie extraction, but ensure you didn't force `--chrome-mode` unnecessarily. - **Empty data returns or 'Unauthorized' error** - Your login session in Chrome might have expired. Open a normal Chrome tab, navigate to the target site, and log in or refresh the page to prove you are human. - **Node API errors** diff --git a/README.zh-CN.md b/README.zh-CN.md index 4d2cdd7..9b9b2b3 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -41,9 +41,9 @@ OpenCLI 通过 Chrome 浏览器 + [Playwright MCP Bridge](https://github.com/nic > **⚠️ 重要**:大多数命令复用你的 Chrome 登录状态。运行命令前,你必须已在 Chrome 中打开目标网站并完成登录。如果获取到空数据或报错,请先检查你的浏览器登录状态。 -为了让 OpenCLI 能够联通你的浏览器,你需要配置连接方式。**强烈建议以下两种方式都配置上**,互为后备: +OpenCLI 通过 Playwright MCP Bridge 扩展与你的浏览器通信。 -### 连接方式 A:Playwright MCP Bridge 扩展(首选) +### Playwright MCP Bridge 扩展配置 1. 安装 **[Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm)** 扩展 2. 在浏览器插件栏点击该插件,或者在插件设置页获取你的 Extension Token。 @@ -72,16 +72,6 @@ OpenCLI 通过 Chrome 浏览器 + [Playwright MCP Bridge](https://github.com/nic export PLAYWRIGHT_MCP_EXTENSION_TOKEN="<你的-token>" ``` -### 连接方式 B:Chrome 144+ CDP 自动发现(备选) - -无需安装任何扩展。只需开启 Chrome 内置的远程调试: - -1. 在 Chrome 中打开 `chrome://inspect#remote-debugging` -2. 勾选 **"允许对此浏览器实例进行远程调试" (Allow remote debugging for this browser instance)** -3. 运行时设置环境变量 `OPENCLI_USE_CDP=1` - -*也可通过 `OPENCLI_CDP_ENDPOINT` 环境变量手动指定 CDP endpoint 地址。* - ## 快速开始 ### npm 全局安装(推荐) @@ -184,8 +174,6 @@ opencli cascade https://api.example.com/data - **"Failed to connect to Playwright MCP Bridge"** 报错 - 确保你当前的 Chrome 已安装且**开启了** Playwright MCP Bridge 浏览器插件。 - 如果是刚装完插件,需要重启 Chrome 浏览器。 -- **"CDP command failed" / "被风控拦截"** - - 有些网站(例如 BOSS 直聘)会因为开了 DevTools 或者 CDP 端口拦截验证。OpenCLI 有 cookie 降级机制,通常不需要干预,不用去强行加上 CDP 标识参数即可。 - **返回空数据,或者报错 "Unauthorized"** - Chrome 里的登录态可能已经过期(甚至被要求过滑动验证码)。请打开当前 Chrome 页面,在新标签页重新手工登录或刷新该页面。 - **Node API 错误 (如 parseArgs, fs 等)** diff --git a/SKILL.md b/SKILL.md index b493acb..0c202df 100644 --- a/SKILL.md +++ b/SKILL.md @@ -34,8 +34,7 @@ npm update -g @jackwener/opencli Browser commands require: 1. Chrome browser running **(logged into target sites)** -2. [Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm) extension (default connection mode) -3. **Alternative**: Chrome 144+ CDP auto-discovery — set `OPENCLI_USE_CDP=1` (no extension needed) +2. [Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm) extension installed and configured > **Note**: You must be logged into the target website in Chrome before running commands. Tabs opened during command execution are auto-closed afterwards. @@ -339,9 +338,6 @@ ${{ index + 1 }} | `OPENCLI_BROWSER_CONNECT_TIMEOUT` | 30 | Browser connection timeout (sec) | | `OPENCLI_BROWSER_COMMAND_TIMEOUT` | 45 | Command execution timeout (sec) | | `OPENCLI_BROWSER_EXPLORE_TIMEOUT` | 120 | Explore timeout (sec) | -| `OPENCLI_CDP_ENDPOINT` | — | Manual CDP WebSocket endpoint (overrides auto-discovery) | -| `OPENCLI_USE_CDP` | — | Set to `1` to use Chrome 144+ CDP auto-discovery instead of extension | -| `OPENCLI_FORCE_EXTENSION` | — | Set to `1` to skip CDP and force extension mode | | `PLAYWRIGHT_MCP_EXTENSION_TOKEN` | — | Auto-approve extension connection | ## Troubleshooting @@ -349,6 +345,6 @@ ${{ index + 1 }} | Issue | Solution | |-------|----------| | `npx not found` | Install Node.js: `brew install node` | -| `Timed out connecting to browser` | 1) Chrome must be open 2) Enable remote debugging at `chrome://inspect#remote-debugging` or install MCP Bridge extension | +| `Timed out connecting to browser` | 1) Chrome must be open 2) Install MCP Bridge extension and configure token | | `Target page context` error | Add `navigate:` step before `evaluate:` in YAML | | Empty table data | Check if evaluate returns JSON string (MCP parsing) or data path is wrong | diff --git a/src/browser.test.ts b/src/browser.test.ts index c80fc19..fbfe211 100644 --- a/src/browser.test.ts +++ b/src/browser.test.ts @@ -75,13 +75,5 @@ describe('PlaywrightMCP state', () => { await expect(mcp.connect()).rejects.toThrow('Playwright MCP is closing'); }); - it('tracks backend mode for lifecycle policy', async () => { - const mcp = new PlaywrightMCP(); - expect((mcp as any)._mode).toBeNull(); - - await mcp.close(); - - expect((mcp as any)._mode).toBeNull(); - }); }); diff --git a/src/browser.ts b/src/browser.ts index 5773627..8cd45a8 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -1,114 +1,16 @@ /** - * Browser interaction via Chrome DevTools Protocol. - * Connects to an existing Chrome browser through CDP auto-discovery or extension bridge. + * Browser interaction via Playwright MCP Bridge extension. + * Connects to an existing Chrome browser through the extension. */ import { spawn, execSync, type ChildProcess } from 'node:child_process'; import { createHash } from 'node:crypto'; -import * as http from 'node:http'; -import * as net from 'node:net'; import { fileURLToPath } from 'node:url'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { formatSnapshot } from './snapshotFormatter.js'; -/** - * 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. - * - * Priority: OPENCLI_CDP_ENDPOINT env > DevToolsActivePort auto-discovery > --extension fallback - */ - -/** Quick TCP port probe to verify Chrome is actually listening */ -function isPortReachable(port: number, host = '127.0.0.1', timeoutMs = 800): Promise { - return new Promise(resolve => { - const sock = net.createConnection({ port, host }); - sock.setTimeout(timeoutMs); - sock.on('connect', () => { sock.destroy(); resolve(true); }); - sock.on('error', () => resolve(false)); - sock.on('timeout', () => { sock.destroy(); resolve(false); }); - }); -} - -/** - * Verify the CDP HTTP JSON API is functional. - * Chrome's chrome://inspect#remote-debugging mode writes DevToolsActivePort - * but doesn't expose the full CDP HTTP API (/json/version), which means - * Playwright's connectOverCDP won't work properly (init succeeds but - * all tool calls hang silently). - */ -export function isCdpApiAvailable(port: number, host = '127.0.0.1', timeoutMs = 2000): Promise { - return new Promise(resolve => { - const req = http.get(`http://${host}:${port}/json/version`, { timeout: timeoutMs }, (res) => { - let body = ''; - res.on('data', (chunk: Buffer) => { body += chunk.toString(); }); - res.on('end', () => { - try { - const data = JSON.parse(body); - // A valid CDP endpoint returns { Browser, ... } with a webSocketDebuggerUrl - resolve(!!data && typeof data === 'object' && !!data.Browser); - } catch { - resolve(false); - } - }); - }); - req.on('error', () => resolve(false)); - req.on('timeout', () => { req.destroy(); resolve(false); }); - }); -} - -export async function discoverChromeEndpoint(): Promise { - 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/')) { - const endpoint = `ws://127.0.0.1:${port}${browserPath}`; - // Verify the port is actually reachable (Chrome may have closed, leaving a stale file) - if (await isPortReachable(port)) { - // Verify CDP HTTP API is functional — chrome://inspect#remote-debugging - // writes DevToolsActivePort but doesn't expose the full CDP API, - // causing Playwright connectOverCDP to hang on all tool calls. - if (await isCdpApiAvailable(port)) { - return endpoint; - } - } - } - } - } catch {} - } - return null; -} - // Read version from package.json (single source of truth) const __browser_dirname = path.dirname(fileURLToPath(import.meta.url)); const PKG_VERSION = (() => { try { return JSON.parse(fs.readFileSync(path.resolve(__browser_dirname, '..', 'package.json'), 'utf-8')).version; } catch { return '0.0.0'; } })(); @@ -116,17 +18,14 @@ const PKG_VERSION = (() => { try { return JSON.parse(fs.readFileSync(path.resolv const CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10); const STDERR_BUFFER_LIMIT = 16 * 1024; const INITIAL_TABS_TIMEOUT_MS = 1500; -const CDP_READINESS_PROBE_TIMEOUT_MS = 5000; const TAB_CLEANUP_TIMEOUT_MS = 2000; let _cachedMcpServerPath: string | null | undefined; -type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'cdp-timeout' | 'mcp-init' | 'process-exit' | 'unknown'; +type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'mcp-init' | 'process-exit' | 'unknown'; type PlaywrightMCPState = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed'; -type PlaywrightMCPMode = 'extension' | 'cdp' | null; type ConnectFailureInput = { kind: ConnectFailureKind; - mode: 'extension' | 'cdp'; timeout: number; hasExtensionToken: boolean; tokenFingerprint?: string | null; @@ -145,41 +44,31 @@ export function formatBrowserConnectError(input: ConnectFailureInput): Error { const suffix = stderr ? `\n\nMCP stderr:\n${stderr}` : ''; const tokenHint = input.tokenFingerprint ? ` Token fingerprint: ${input.tokenFingerprint}.` : ''; - if (input.mode === 'extension') { - if (input.kind === 'missing-token') { - return new Error( - 'Failed to connect to Playwright MCP Bridge: PLAYWRIGHT_MCP_EXTENSION_TOKEN is not set.\n\n' + - 'Without this token, Chrome will show a manual approval dialog for every new MCP connection. ' + - 'Copy the token from the Playwright MCP Bridge extension and set it in BOTH your shell environment and MCP client config.' + - suffix, - ); - } - - if (input.kind === 'extension-not-installed') { - return new Error( - 'Failed to connect to Playwright MCP Bridge: the browser extension did not attach.\n\n' + - 'Make sure Chrome is running and the "Playwright MCP Bridge" extension is installed and enabled. ' + - 'If Chrome shows an approval dialog, click Allow.' + - suffix, - ); - } - - if (input.kind === 'extension-timeout') { - const likelyCause = input.hasExtensionToken - ? `The most likely cause is that PLAYWRIGHT_MCP_EXTENSION_TOKEN does not match the token currently shown by the browser extension.${tokenHint} Re-copy the token from the extension and update BOTH your shell environment and MCP client config.` - : 'PLAYWRIGHT_MCP_EXTENSION_TOKEN is not configured, so the extension may be waiting for manual approval.'; - return new Error( - `Timed out connecting to Playwright MCP Bridge (${input.timeout}s).\n\n` + - `${likelyCause} If a browser prompt is visible, click Allow. You can also switch to Chrome remote debugging mode with OPENCLI_USE_CDP=1 as a fallback.` + - 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' + + 'Without this token, Chrome will show a manual approval dialog for every new MCP connection. ' + + 'Copy the token from the Playwright MCP Bridge extension and set it in BOTH your shell environment and MCP client config.' + + suffix, + ); } - if (input.mode === 'cdp' && input.kind === 'cdp-timeout') { + if (input.kind === 'extension-not-installed') { return new Error( - `Timed out connecting to browser via CDP (${input.timeout}s).\n\n` + - 'Make sure Chrome is running and remote debugging is enabled at chrome://inspect#remote-debugging, or set OPENCLI_CDP_ENDPOINT explicitly.' + + 'Failed to connect to Playwright MCP Bridge: the browser extension did not attach.\n\n' + + 'Make sure Chrome is running and the "Playwright MCP Bridge" extension is installed and enabled. ' + + 'If Chrome shows an approval dialog, click Allow.' + + suffix, + ); + } + + if (input.kind === 'extension-timeout') { + const likelyCause = input.hasExtensionToken + ? `The most likely cause is that PLAYWRIGHT_MCP_EXTENSION_TOKEN does not match the token currently shown by the browser extension.${tokenHint} Re-copy the token from the extension and update BOTH your shell environment and MCP client config.` + : 'PLAYWRIGHT_MCP_EXTENSION_TOKEN is not configured, so the extension may be waiting for manual approval.'; + return new Error( + `Timed out connecting to Playwright MCP Bridge (${input.timeout}s).\n\n` + + `${likelyCause} If a browser prompt is visible, click Allow.` + suffix, ); } @@ -199,7 +88,6 @@ export function formatBrowserConnectError(input: ConnectFailureInput): Error { } function inferConnectFailureKind(args: { - mode: 'extension' | 'cdp'; hasExtensionToken: boolean; stderr: string; rawMessage?: string; @@ -207,7 +95,7 @@ function inferConnectFailureKind(args: { }): ConnectFailureKind { const haystack = `${args.rawMessage ?? ''}\n${args.stderr}`.toLowerCase(); - if (args.mode === 'extension' && !args.hasExtensionToken) + if (!args.hasExtensionToken) return 'missing-token'; if (haystack.includes('extension connection timeout') || haystack.includes('playwright mcp bridge')) return 'extension-not-installed'; @@ -215,11 +103,7 @@ function inferConnectFailureKind(args: { return 'mcp-init'; if (args.exited) return 'process-exit'; - if (args.mode === 'extension') - return 'extension-timeout'; - if (args.mode === 'cdp') - return 'cdp-timeout'; - return 'unknown'; + return 'extension-timeout'; } // JSON-RPC helpers @@ -461,7 +345,6 @@ export class PlaywrightMCP { private _initialTabIdentities: string[] = []; private _closingPromise: Promise | null = null; private _state: PlaywrightMCPState = 'idle'; - private _mode: PlaywrightMCPMode = null; private _page: Page | null = null; @@ -496,7 +379,6 @@ export class PlaywrightMCP { this._page = null; this._proc = null; this._buffer = ''; - this._mode = null; this._initialTabIdentities = []; this._rejectPendingRequests(new Error('Playwright MCP connect failed')); PlaywrightMCP._activeInsts.delete(this); @@ -505,7 +387,7 @@ export class PlaywrightMCP { } } - async connect(opts: { timeout?: number; forceExtension?: boolean } = {}): Promise { + async connect(opts: { timeout?: number } = {}): Promise { if (this._state === 'connected' && this._page) return this._page; if (this._state === 'connecting') throw new Error('Playwright MCP is already connecting'); if (this._state === 'closing') throw new Error('Playwright MCP is closing'); @@ -519,26 +401,9 @@ export class PlaywrightMCP { this._state = 'connecting'; const timeout = opts.timeout ?? CONNECT_TIMEOUT; - // Connection priority: - // 1. OPENCLI_CDP_ENDPOINT env var → explicit CDP endpoint - // 2. OPENCLI_USE_CDP=1 → auto-discover via DevToolsActivePort - // 3. Default → --extension mode (Playwright MCP Bridge) - // Some anti-bot sites (e.g. BOSS Zhipin) detect CDP — use forceExtension to bypass. - const forceExt = opts.forceExtension || process.env.OPENCLI_FORCE_EXTENSION === '1'; - let cdpEndpoint: string | null = null; - if (!forceExt) { - if (process.env.OPENCLI_CDP_ENDPOINT) { - cdpEndpoint = process.env.OPENCLI_CDP_ENDPOINT; - } else if (process.env.OPENCLI_USE_CDP === '1') { - cdpEndpoint = await discoverChromeEndpoint(); - } - } - return new Promise((resolve, reject) => { const isDebug = process.env.DEBUG?.includes('opencli:mcp'); const debugLog = (msg: string) => isDebug && console.error(`[opencli:mcp] ${msg}`); - const mode: 'extension' | 'cdp' = cdpEndpoint ? 'cdp' : 'extension'; - this._mode = mode; const extensionToken = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN; const tokenFingerprint = getTokenFingerprint(extensionToken); let stderrBuffer = ''; @@ -552,7 +417,6 @@ export class PlaywrightMCP { this._resetAfterFailedConnect(); reject(formatBrowserConnectError({ kind, - mode, timeout, hasExtensionToken: !!extensionToken, tokenFingerprint, @@ -573,23 +437,14 @@ export class PlaywrightMCP { const timer = setTimeout(() => { debugLog('Connection timed out'); settleError(inferConnectFailureKind({ - mode, hasExtensionToken: !!extensionToken, stderr: stderrBuffer, })); }, timeout * 1000); - const mcpArgs: string[] = [mcpPath]; - if (cdpEndpoint) { - mcpArgs.push('--cdp-endpoint', cdpEndpoint); - } else { - mcpArgs.push('--extension'); - } + const mcpArgs: string[] = [mcpPath, '--extension']; if (process.env.OPENCLI_VERBOSE) { - console.error(`[opencli] CDP mode: ${cdpEndpoint ? `auto-discovered ${cdpEndpoint}` : 'fallback to --extension'}`); - if (mode === 'extension') { - console.error(`[opencli] Extension token: ${extensionToken ? `configured (fingerprint ${tokenFingerprint})` : 'missing'}`); - } + console.error(`[opencli] Extension token: ${extensionToken ? `configured (fingerprint ${tokenFingerprint})` : 'missing'}`); } if (process.env.OPENCLI_BROWSER_EXECUTABLE_PATH) { mcpArgs.push('--executablePath', process.env.OPENCLI_BROWSER_EXECUTABLE_PATH); @@ -645,7 +500,6 @@ export class PlaywrightMCP { this._rejectPendingRequests(new Error(`Playwright MCP process exited before response${code == null ? '' : ` (code ${code})`}`)); if (!settled) { settleError(inferConnectFailureKind({ - mode, hasExtensionToken: !!extensionToken, stderr: stderrBuffer, exited: true, @@ -663,7 +517,6 @@ export class PlaywrightMCP { debugLog('Got initialize response'); if (resp.error) { settleError(inferConnectFailureKind({ - mode, hasExtensionToken: !!extensionToken, stderr: stderrBuffer, rawMessage: `MCP init failed: ${resp.error.message}`, @@ -675,29 +528,7 @@ export class PlaywrightMCP { debugLog(`SEND: ${initializedMsg.trim()}`); this._proc?.stdin?.write(initializedMsg); - if (mode === 'cdp') { - // CDP readiness probe: verify tool calls actually work. - // Some CDP endpoints (e.g. chrome://inspect mode) accept WebSocket - // connections and respond to MCP init but silently drop tool calls. - debugLog('CDP readiness probe (tabs)...'); - withTimeout(page.tabs(), CDP_READINESS_PROBE_TIMEOUT_MS, 'CDP readiness probe timed out') - .then(() => { - debugLog('CDP readiness probe succeeded'); - settleSuccess(page); - }) - .catch((err) => { - debugLog(`CDP readiness probe failed: ${err.message}`); - settleError('cdp-timeout', { - rawMessage: 'CDP endpoint connected but tool calls are unresponsive. ' + - 'This usually means Chrome was opened with chrome://inspect#remote-debugging ' + - 'which is not fully compatible. Launch Chrome with --remote-debugging-port=9222 instead, ' + - 'or use the Playwright MCP Bridge extension (default mode).', - }); - }); - return; - } - - // Extension mode uses tabs as a readiness probe and for tab cleanup bookkeeping. + // Use tabs as a readiness probe and for tab cleanup bookkeeping. debugLog('Fetching initial tabs count...'); withTimeout(page.tabs(), INITIAL_TABS_TIMEOUT_MS, 'Timed out fetching initial tabs').then((tabs: any) => { debugLog(`Tabs response: ${typeof tabs === 'string' ? tabs : JSON.stringify(tabs)}`); @@ -714,6 +545,7 @@ export class PlaywrightMCP { }); } + async close(): Promise { if (this._closingPromise) return this._closingPromise; if (this._state === 'closed') return; @@ -721,7 +553,7 @@ export class PlaywrightMCP { this._closingPromise = (async () => { try { // Extension mode opens bridge/session tabs that we can clean up best-effort. - if (this._mode === 'extension' && this._page && this._proc && !this._proc.killed) { + if (this._page && this._proc && !this._proc.killed) { try { const tabs = await withTimeout(this._page.tabs(), TAB_CLEANUP_TIMEOUT_MS, 'Timed out fetching tabs during cleanup'); const tabEntries = extractTabEntries(tabs); @@ -751,7 +583,6 @@ export class PlaywrightMCP { this._rejectPendingRequests(new Error('Playwright MCP session closed')); this._page = null; this._proc = null; - this._mode = null; this._state = 'closed'; PlaywrightMCP._activeInsts.delete(this); } @@ -844,7 +675,6 @@ export const __test__ = { diffTabIndexes, appendLimited, withTimeout, - isCdpApiAvailable, }; function findMcpServerPath(): string | null { diff --git a/src/clis/boss/search.ts b/src/clis/boss/search.ts index 73f430a..984ee89 100644 --- a/src/clis/boss/search.ts +++ b/src/clis/boss/search.ts @@ -69,7 +69,7 @@ cli({ description: 'BOSS直聘搜索职位', domain: 'www.zhipin.com', strategy: Strategy.COOKIE, - forceExtension: true, // BOSS Zhipin detects CDP mode — must use extension bridge + browser: true, args: [ { name: 'query', required: true, help: 'Search keyword (e.g. AI agent, 前端)' }, diff --git a/src/clis/v2ex/daily.ts b/src/clis/v2ex/daily.ts index 5376adc..de0c234 100644 --- a/src/clis/v2ex/daily.ts +++ b/src/clis/v2ex/daily.ts @@ -11,7 +11,7 @@ cli({ domain: 'www.v2ex.com', strategy: Strategy.COOKIE, browser: true, - forceExtension: true, + args: [], columns: ['status', 'message'], func: async (page: IPage | null) => { diff --git a/src/clis/v2ex/me.ts b/src/clis/v2ex/me.ts index 0e24bb8..7983cbe 100644 --- a/src/clis/v2ex/me.ts +++ b/src/clis/v2ex/me.ts @@ -11,7 +11,7 @@ cli({ domain: 'www.v2ex.com', strategy: Strategy.COOKIE, browser: true, - forceExtension: true, + args: [], columns: ['username', 'balance', 'unread_notifications', 'daily_reward_ready'], func: async (page: IPage | null) => { diff --git a/src/clis/v2ex/notifications.ts b/src/clis/v2ex/notifications.ts index e28575f..3400bf2 100644 --- a/src/clis/v2ex/notifications.ts +++ b/src/clis/v2ex/notifications.ts @@ -11,7 +11,7 @@ cli({ domain: 'www.v2ex.com', strategy: Strategy.COOKIE, browser: true, - forceExtension: true, + args: [ { name: 'limit', type: 'int', default: 20, help: 'Number of notifications' } ], diff --git a/src/doctor.test.ts b/src/doctor.test.ts index 4ec5c60..b83bc8e 100644 --- a/src/doctor.test.ts +++ b/src/doctor.test.ts @@ -92,18 +92,12 @@ describe('doctor report rendering', () => { envFingerprint: 'fp1', shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'abc123', fingerprint: 'fp1' }], configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }], - remoteDebuggingEnabled: true, - remoteDebuggingEndpoint: 'ws://127.0.0.1:9222/devtools/browser/test', - cdpEnabled: false, - cdpToken: null, - cdpFingerprint: null, recommendedToken: 'abc123', recommendedFingerprint: 'fp1', warnings: [], issues: [], }); - expect(text).toContain('[OK] Chrome remote debugging: enabled'); expect(text).toContain('[OK] Environment token: configured (fp1)'); expect(text).toContain('[OK] MCP config /tmp/mcp.json: configured (fp1)'); }); @@ -114,18 +108,12 @@ describe('doctor report rendering', () => { envFingerprint: 'fp1', shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'def456', fingerprint: 'fp2' }], configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }], - remoteDebuggingEnabled: false, - remoteDebuggingEndpoint: null, - cdpEnabled: false, - cdpToken: null, - cdpFingerprint: null, recommendedToken: 'abc123', recommendedFingerprint: 'fp1', - warnings: ['Chrome remote debugging appears to be disabled or Chrome is not currently exposing a DevTools endpoint.'], + warnings: [], issues: ['Detected inconsistent Playwright MCP tokens across env/config files.'], }); - expect(text).toContain('[WARN] Chrome remote debugging: disabled'); expect(text).toContain('[MISMATCH] Environment token: configured (fp1)'); expect(text).toContain('[MISMATCH] Shell file /tmp/.zshrc: configured (fp2)'); expect(text).toContain('[MISMATCH] Recommended token fingerprint: fp1'); diff --git a/src/doctor.ts b/src/doctor.ts index 07d01da..6640f83 100644 --- a/src/doctor.ts +++ b/src/doctor.ts @@ -4,7 +4,7 @@ import * as path from 'node:path'; import { createInterface } from 'node:readline/promises'; import { stdin as input, stdout as output } from 'node:process'; import type { IPage } from './types.js'; -import { PlaywrightMCP, discoverChromeEndpoint, getTokenFingerprint } from './browser.js'; +import { PlaywrightMCP, getTokenFingerprint } from './browser.js'; import { browserSession } from './runtime.js'; const PLAYWRIGHT_SERVER_NAME = 'playwright'; @@ -45,11 +45,6 @@ export type DoctorReport = { envFingerprint: string | null; shellFiles: ShellFileStatus[]; configs: McpConfigStatus[]; - remoteDebuggingEnabled: boolean; - remoteDebuggingEndpoint: string | null; - cdpEnabled: boolean; - cdpToken: string | null; - cdpFingerprint: string | null; recommendedToken: string | null; recommendedFingerprint: string | null; warnings: string[]; @@ -225,45 +220,10 @@ function readConfigStatus(filePath: string): McpConfigStatus { } } -async function extractTokenViaCdp(): Promise { - if (!(process.env.OPENCLI_USE_CDP === '1' || process.env.OPENCLI_CDP_ENDPOINT)) - return null; - const candidates = [ - `chrome-extension://${PLAYWRIGHT_EXTENSION_ID}/options.html`, - `chrome-extension://${PLAYWRIGHT_EXTENSION_ID}/popup.html`, - `chrome-extension://${PLAYWRIGHT_EXTENSION_ID}/connect.html`, - `chrome-extension://${PLAYWRIGHT_EXTENSION_ID}/index.html`, - ]; - const result = await browserSession(PlaywrightMCP, async (page: IPage) => { - for (const url of candidates) { - try { - await page.goto(url); - await page.wait(1); - const token = await page.evaluate(`() => { - const values = new Set(); - const push = (value) => { - if (!value || typeof value !== 'string') return; - for (const match of value.matchAll(/[A-Za-z0-9_-]{24,}/g)) values.add(match[0]); - }; - document.querySelectorAll('input, textarea, code, pre, span, div').forEach((el) => { - push(el.value); - push(el.textContent || ''); - push(el.getAttribute && el.getAttribute('value')); - }); - return Array.from(values); - }`); - const matches = Array.isArray(token) ? token.filter((v: string) => v.length >= 24) : []; - if (matches.length > 0) return matches.sort((a: string, b: string) => b.length - a.length)[0]; - } catch {} - } - return null; - }); - return typeof result === 'string' && result ? result : null; -} + export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise { const envToken = process.env[PLAYWRIGHT_TOKEN_ENV] ?? null; - const remoteDebuggingEndpoint = await discoverChromeEndpoint().catch(() => null); const shellPath = opts.shellRc ?? getDefaultShellRcPath(); const shellFiles: ShellFileStatus[] = [shellPath].map((filePath) => { if (!fileExists(filePath)) return { path: filePath, exists: false, token: null, fingerprint: null }; @@ -273,17 +233,15 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise null) : null; const allTokens = [ opts.token ?? null, envToken, ...shellFiles.map(s => s.token), ...configs.map(c => c.token), - cdpToken, ].filter((v): v is string => !!v); const uniqueTokens = [...new Set(allTokens)]; - const recommendedToken = opts.token ?? envToken ?? (uniqueTokens.length === 1 ? uniqueTokens[0] : cdpToken) ?? null; + const recommendedToken = opts.token ?? envToken ?? (uniqueTokens.length === 1 ? uniqueTokens[0] : null) ?? null; const report: DoctorReport = { cliVersion: opts.cliVersion, @@ -291,11 +249,6 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise s.token)) report.issues.push('Shell startup file does not export PLAYWRIGHT_MCP_EXTENSION_TOKEN.'); if (!configs.some(c => c.token)) report.issues.push('No scanned MCP config currently contains a Playwright extension token.'); if (uniqueTokens.length > 1) report.issues.push('Detected inconsistent Playwright MCP tokens across env/config files.'); - if (!report.remoteDebuggingEnabled) report.warnings.push('Chrome remote debugging appears to be disabled or Chrome is not currently exposing a DevTools endpoint.'); for (const config of configs) { if (config.parseError) report.warnings.push(`Could not parse ${config.path}: ${config.parseError}`); } if (!recommendedToken) { - if (report.cdpEnabled) report.warnings.push('CDP is enabled, but no token could be extracted automatically from the extension UI.'); - else report.warnings.push('No token source found. Enable OPENCLI_USE_CDP=1 to allow a best-effort token read from the extension page.'); + report.warnings.push('No token source found.'); } return report; } @@ -326,8 +277,6 @@ export function renderBrowserDoctorReport(report: DoctorReport): string { const uniqueFingerprints = [...new Set(tokenFingerprints)]; const hasMismatch = uniqueFingerprints.length > 1; const lines = [`opencli v${report.cliVersion ?? 'unknown'} doctor`, '']; - lines.push(statusLine(report.remoteDebuggingEnabled ? 'OK' : 'WARN', `Chrome remote debugging: ${report.remoteDebuggingEnabled ? 'enabled' : 'disabled'}`)); - if (report.remoteDebuggingEndpoint) lines.push(` ${report.remoteDebuggingEndpoint}`); const envStatus: ReportStatus = !report.envToken ? 'MISSING' : hasMismatch ? 'MISMATCH' : 'OK'; lines.push(statusLine(envStatus, `Environment token: ${tokenSummary(report.envToken, report.envFingerprint)}`)); @@ -354,10 +303,6 @@ export function renderBrowserDoctorReport(report: DoctorReport): string { lines.push(statusLine('MISSING', 'MCP config: no existing config files found in scanned locations')); } if (missingConfigCount > 0) lines.push(` Other scanned config locations not present: ${missingConfigCount}`); - if (report.cdpEnabled) { - const cdpStatus: ReportStatus = report.cdpToken ? 'OK' : 'WARN'; - lines.push(statusLine(cdpStatus, `CDP token probe: ${tokenSummary(report.cdpToken, report.cdpFingerprint)}`)); - } lines.push(''); lines.push(statusLine( hasMismatch ? 'MISMATCH' : report.recommendedToken ? 'OK' : 'WARN', @@ -391,7 +336,7 @@ function writeFileWithMkdir(filePath: string, content: string): void { export async function applyBrowserDoctorFix(report: DoctorReport, opts: DoctorOptions = {}): Promise { const token = opts.token ?? report.recommendedToken; - if (!token) throw new Error('No Playwright MCP token is available to write. Provide --token or enable CDP token probing first.'); + if (!token) throw new Error('No Playwright MCP token is available to write. Provide --token first.'); const plannedWrites: string[] = []; const shellPath = opts.shellRc ?? report.shellFiles[0]?.path ?? getDefaultShellRcPath(); diff --git a/src/main.ts b/src/main.ts index e4826a2..0f1c7aa 100644 --- a/src/main.ts +++ b/src/main.ts @@ -144,7 +144,7 @@ for (const [, cmd] of registry) { if (actionOpts.verbose) process.env.OPENCLI_VERBOSE = '1'; let result: any; if (cmd.browser) { - result = await browserSession(PlaywrightMCP, async (page) => runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) }), { forceExtension: cmd.forceExtension }); + result = await browserSession(PlaywrightMCP, async (page) => runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) })); } else { result = await executeCommand(cmd, null, kwargs, actionOpts.verbose); } if (actionOpts.verbose && (!result || (Array.isArray(result) && result.length === 0))) { console.error(chalk.yellow(`[Verbose] Warning: Command returned an empty result. If the website structural API changed or requires authentication, check the network or update the adapter.`)); diff --git a/src/registry.ts b/src/registry.ts index d77bb46..3b4fc38 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -37,10 +37,7 @@ export interface CliCommand { /** Internal: lazy-loaded TS module support */ _lazy?: boolean; _modulePath?: string; - /** Force extension bridge mode (bypass CDP), for anti-bot sites */ - forceExtension?: boolean; } - export interface CliOptions { site: string; name: string; @@ -53,10 +50,7 @@ export interface CliOptions { func?: (page: IPage | null, kwargs: Record, debug?: boolean) => Promise; pipeline?: any[]; timeoutSeconds?: number; - /** Force extension bridge mode (bypass CDP), for anti-bot sites */ - forceExtension?: boolean; } - const _registry = new Map(); export function cli(opts: CliOptions): CliCommand { @@ -72,7 +66,6 @@ export function cli(opts: CliOptions): CliCommand { func: opts.func, pipeline: opts.pipeline, timeoutSeconds: opts.timeoutSeconds, - forceExtension: opts.forceExtension, }; const key = fullName(cmd); diff --git a/src/runtime.ts b/src/runtime.ts index 03408e5..ff6626f 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -27,11 +27,10 @@ export async function runWithTimeout( export async function browserSession( BrowserFactory: new () => any, fn: (page: IPage) => Promise, - opts?: { forceExtension?: boolean }, ): Promise { const mcp = new BrowserFactory(); try { - const page = await mcp.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT, forceExtension: opts?.forceExtension }); + const page = await mcp.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT }); return await fn(page); } finally { await mcp.close().catch(() => {});