feat: add forceExtension to bypass CDP detection for anti-bot sites

- Added forceExtension flag to CliCommand, CliOptions, browserSession, and PlaywrightMCP.connect()
- BOSS Zhipin detects CDP mode and drops connections; marked boss/search with forceExtension: true
- Also supports OPENCLI_FORCE_EXTENSION=1 env var for global override
- Fixed async discoverChromeEndpoint() placement outside Promise constructor
- Enhanced boss search with city names, experience, degree, salary, etc.
This commit is contained in:
jackwener committed 2026-03-15 19:39:39 +08:00
1 parent c20eaee393
commit da155e96f0
8 files changed
+87 -48

No files matched your search

+18 -15
View File
@@ -57,22 +57,25 @@ npm install -g @jackwener/opencli@latest
Browser commands need:
1. **Chrome** running **and logged into the target site** (e.g. bilibili.com, zhihu.com, xiaohongshu.com)
2. **[Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm)** extension installed
3. Configure `PLAYWRIGHT_MCP_EXTENSION_TOKEN` (from the extension settings page) in your MCP config:
```json
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--extension"],
"env": {
"PLAYWRIGHT_MCP_EXTENSION_TOKEN": "<your-token>"
}
}
}
}
```
#### Option A: Chrome 144+ Auto-Discovery (Recommended)
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. That's it — opencli will auto-detect the running Chrome via `DevToolsActivePort`
You can also manually specify a CDP endpoint via `OPENCLI_CDP_ENDPOINT` env var.
#### Option B: Playwright MCP Bridge Extension (Legacy)
For older Chrome versions or if auto-discovery doesn't work:
1. Install **[Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm)** extension
2. Configure `PLAYWRIGHT_MCP_EXTENSION_TOKEN` (from the extension settings page)
> opencli will automatically fall back to extension mode if no running Chrome with remote debugging is detected.
Public API commands (`hackernews`, `github search`, `v2ex`) need no browser at all.
+18 -15
View File
@@ -58,22 +58,25 @@ npm install -g @jackwener/opencli@latest
浏览器命令需要:
1. **Chrome** 浏览器正在运行,且**已登录目标网站**(如 bilibili.com、zhihu.com、xiaohongshu.com)
2. 安装 **[Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm)** 扩展
3. 在 MCP 配置中设置 `PLAYWRIGHT_MCP_EXTENSION_TOKEN`(从扩展设置页获取):
```json
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--extension"],
"env": {
"PLAYWRIGHT_MCP_EXTENSION_TOKEN": "<your-token>"
}
}
}
}
```
#### 方式 A:Chrome 144+ 自动发现(推荐)
无需安装扩展,只需开启 Chrome 内置远程调试:
1. 在 Chrome 中打开 `chrome://inspect#remote-debugging`
2. 勾选 **"允许对此浏览器实例进行远程调试"**
3. 完成 — opencli 会通过 `DevToolsActivePort` 自动发现运行中的 Chrome
也可通过 `OPENCLI_CDP_ENDPOINT` 环境变量手动指定 CDP endpoint。
#### 方式 B:Playwright MCP Bridge 扩展(旧版)
适用于旧版 Chrome 或自动发现不可用的情况:
1. 安装 **[Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm)** 扩展
2. 配置 `PLAYWRIGHT_MCP_EXTENSION_TOKEN`(从扩展设置页获取)
> 如果未检测到开启远程调试的 Chrome,opencli 会自动回退到扩展模式。
公共 API 命令(`hackernews`、`github search`、`v2ex`)无需浏览器。
+5 -3
View File
@@ -34,8 +34,8 @@ 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
3. Configure `PLAYWRIGHT_MCP_EXTENSION_TOKEN` (from extension settings) in your MCP config
2. **Chrome 144+** (recommended): Open `chrome://inspect#remote-debugging` → enable remote debugging. opencli auto-discovers via `DevToolsActivePort`
3. **Fallback**: [Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm) extension (auto-used when CDP not available)
> **Note**: You must be logged into the target website in Chrome before running commands. Tabs opened during command execution are auto-closed afterwards.
@@ -332,6 +332,8 @@ ${{ index + 1 }}
| `OPENCLI_BROWSER_COMMAND_TIMEOUT` | 45 | Command execution timeout (sec) |
| `OPENCLI_BROWSER_EXPLORE_TIMEOUT` | 120 | Explore timeout (sec) |
| `OPENCLI_EXTENSION_LOCK_TIMEOUT` | 120 | Extension lock timeout (sec) |
| `OPENCLI_CDP_ENDPOINT` | — | Manual CDP WebSocket endpoint (overrides auto-discovery) |
| `OPENCLI_FORCE_EXTENSION` | — | Set to `1` to skip CDP and force extension mode |
| `PLAYWRIGHT_MCP_EXTENSION_TOKEN` | — | Auto-approve extension connection |
## Troubleshooting
@@ -339,7 +341,7 @@ ${{ index + 1 }}
| Issue | Solution |
|-------|----------|
| `npx not found` | Install Node.js: `brew install node` |
| `Timed out connecting to browser` | 1) Chrome must be open 2) Install MCP Bridge extension 3) Click to approve |
| `Timed out connecting to browser` | 1) Chrome must be open 2) Enable remote debugging at `chrome://inspect#remote-debugging` or install MCP Bridge extension |
| `Extension lock timed out` | Another opencli command is running; browser commands run serially |
| `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 |
+36 -13
View File
@@ -4,6 +4,7 @@
*/
import { spawn, execSync, type ChildProcess } from 'node:child_process';
import * as net from 'node:net';
import { fileURLToPath } from 'node:url';
import * as fs from 'node:fs';
import * as os from 'node:os';
@@ -20,7 +21,19 @@ import { formatSnapshot } from './snapshotFormatter.js';
*
* Priority: OPENCLI_CDP_ENDPOINT env > DevToolsActivePort auto-discovery > --extension fallback
*/
function discoverChromeEndpoint(): string | null {
/** 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); });
});
}
async function discoverChromeEndpoint(): Promise<string | null> {
const candidates: string[] = [];
// User-specified Chrome data dir takes highest priority
@@ -50,7 +63,11 @@ function discoverChromeEndpoint(): string | null {
const port = parseInt(lines[0], 10);
const browserPath = lines[1]; // e.g. /devtools/browser/<GUID>
if (port > 0 && browserPath.startsWith('/devtools/browser/')) {
return `ws://127.0.0.1:${port}${browserPath}`;
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)) {
return endpoint;
}
}
}
} catch {}
@@ -287,32 +304,38 @@ export class PlaywrightMCP {
private _page: Page | null = null;
async connect(opts: { timeout?: number } = {}): Promise<Page> {
async connect(opts: { timeout?: number; forceExtension?: boolean } = {}): Promise<Page> {
await this._acquireLock();
const timeout = opts.timeout ?? CONNECT_TIMEOUT;
const mcpPath = findMcpServerPath();
if (!mcpPath) throw new Error('Playwright MCP server not found. Install: npm install -D @playwright/mcp');
// Chrome 144+ auto-discovery via DevToolsActivePort file.
// Falls back to --extension mode if no running Chrome is detected.
// Some anti-bot sites (e.g. BOSS Zhipin) detect CDP — use forceExtension to bypass.
const forceExt = opts.forceExtension || process.env.OPENCLI_FORCE_EXTENSION === '1';
const cdpEndpoint = forceExt ? null : (process.env.OPENCLI_CDP_ENDPOINT ?? await discoverChromeEndpoint());
return new Promise<Page>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(`Timed out connecting to browser (${timeout}s)`)), timeout * 1000);
// Chrome 144+ auto-discovery via DevToolsActivePort file.
// Falls back to --extension mode if no running Chrome is detected.
const cdpEndpoint = process.env.OPENCLI_CDP_ENDPOINT ?? discoverChromeEndpoint();
const mcpArgs: string[] = [mcpPath];
if (cdpEndpoint) {
mcpArgs.push('--cdp-endpoint', cdpEndpoint);
} else {
mcpArgs.push('--extension');
}
if (process.env.OPENCLI_BROWSER_EXECUTABLE_PATH) {
mcpArgs.push('--executablePath', process.env.OPENCLI_BROWSER_EXECUTABLE_PATH);
}
if (process.env.OPENCLI_VERBOSE) {
console.error(`[opencli] CDP mode: ${cdpEndpoint ? `auto-discovered ${cdpEndpoint}` : 'fallback to --extension'}`);
}
if (process.env.OPENCLI_BROWSER_EXECUTABLE_PATH) {
mcpArgs.push('--executablePath', process.env.OPENCLI_BROWSER_EXECUTABLE_PATH);
}
this._proc = spawn('node', mcpArgs, {
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env },
});
this._proc = spawn('node', mcpArgs, {
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env },
});
// Increase max listeners to avoid warnings
this._proc.setMaxListeners(20);
+1
View File
@@ -67,6 +67,7 @@ cli({
description: 'BOSS直聘搜索职位',
domain: 'www.zhipin.com',
strategy: Strategy.COOKIE,
forceExtension: true, // BOSS Zhipin detects CDP mode — must use extension bridge
args: [
{ name: 'query', required: true, help: 'Search keyword (e.g. AI agent, 前端)' },
{ name: 'city', default: '北京', help: 'City name or code (e.g. 杭州, 上海, 101010100)' },
+2 -1
View File
@@ -99,9 +99,10 @@ for (const [, cmd] of registry) {
else if (arg.default != null) kwargs[arg.name] = arg.default;
}
try {
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) }));
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 });
} 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.`));
+5
View File
@@ -37,6 +37,8 @@ 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 {
@@ -51,6 +53,8 @@ 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>();
@@ -68,6 +72,7 @@ export function cli(opts: CliOptions): CliCommand {
func: opts.func,
pipeline: opts.pipeline,
timeoutSeconds: opts.timeoutSeconds,
forceExtension: opts.forceExtension,
};
const key = fullName(cmd);
+2 -1
View File
@@ -27,10 +27,11 @@ 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 });
const page = await mcp.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT, forceExtension: opts?.forceExtension });
return await fn(page);
} finally {
await mcp.close().catch(() => {});