refactor: remove all CDP code, keep extension-only mode

- Remove CDP auto-discovery (discoverChromeEndpoint, isCdpApiAvailable, isPortReachable)
- Remove CDP readiness probe and autoAllowCdpDialog
- Remove forceExtension plumbing from registry, runtime, main, and adapters
- Remove CDP diagnostics from doctor (extractTokenViaCdp, remoteDebugging fields)
- Simplify connect() to always use --extension mode
- Remove CDP env vars (OPENCLI_USE_CDP, OPENCLI_CDP_ENDPOINT, OPENCLI_FORCE_EXTENSION)
- Update README.md, README.zh-CN.md, SKILL.md to remove CDP sections
- Update tests to remove CDP-related assertions

Current state preserved in extension-and-cdp branch.
This commit is contained in:
jackwener committed 2026-03-16 12:44:09 +08:00
1 parent 67d50191af
commit 95fb841fc7
14 files changed
+51 -332

No files matched your search

-8
View File
@@ -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();
});
});
+33 -203
View File
@@ -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<boolean> {
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<boolean> {
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<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/<GUID>
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<void> | 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<Page> {
async connect(opts: { timeout?: number } = {}): Promise<Page> {
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<Page>((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<void> {
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 {
+1 -1
View File
@@ -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, 前端)' },
+1 -1
View File
@@ -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) => {
+1 -1
View File
@@ -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) => {
+1 -1
View File
@@ -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' }
],
+1 -13
View File
@@ -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');
+5 -60
View File
@@ -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<string | null> {
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<DoctorReport> {
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<Doctor
});
const configPaths = opts.configPaths?.length ? opts.configPaths : getDefaultMcpConfigPaths();
const configs = configPaths.map(readConfigStatus);
const cdpToken = !opts.token && !envToken ? await extractTokenViaCdp().catch(() => 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<Doctor
envFingerprint: getTokenFingerprint(envToken ?? undefined),
shellFiles,
configs,
remoteDebuggingEnabled: !!remoteDebuggingEndpoint,
remoteDebuggingEndpoint,
cdpEnabled: process.env.OPENCLI_USE_CDP === '1' || !!process.env.OPENCLI_CDP_ENDPOINT,
cdpToken,
cdpFingerprint: getTokenFingerprint(cdpToken ?? undefined),
recommendedToken,
recommendedFingerprint: getTokenFingerprint(recommendedToken ?? undefined),
warnings: [],
@@ -306,13 +259,11 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
if (!shellFiles.some(s => 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<string[]> {
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();
+1 -1
View File
@@ -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.`));
-7
View File
@@ -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<string, any>, debug?: boolean) => Promise<any>;
pipeline?: any[];
timeoutSeconds?: number;
/** Force extension bridge mode (bypass CDP), for anti-bot sites */
forceExtension?: boolean;
}
const _registry = new Map<string, CliCommand>();
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);
+1 -2
View File
@@ -27,11 +27,10 @@ export async function runWithTimeout<T>(
export async function browserSession<T>(
BrowserFactory: new () => any,
fn: (page: IPage) => Promise<T>,
opts?: { forceExtension?: boolean },
): Promise<T> {
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(() => {});