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:
1 parent
c20eaee393
commit
da155e96f0
8 files changed
+87
-48
No files matched your search
@@ -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
@@ -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`)无需浏览器。
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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.`));
|
||||
|
||||
@@ -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
@@ -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(() => {});
|
||||
|
||||
Reference in new issue
Block a user