@jackwener/opencli 0.4.5 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -14
- package/README.zh-CN.md +3 -15
- package/SKILL.md +3 -7
- package/dist/browser.d.ts +3 -6
- package/dist/browser.js +27 -128
- package/dist/clis/boss/search.js +0 -1
- package/dist/clis/v2ex/daily.js +0 -1
- package/dist/clis/v2ex/me.js +0 -1
- package/dist/clis/v2ex/notifications.js +0 -1
- package/dist/doctor.d.ts +0 -5
- package/dist/doctor.js +4 -62
- package/dist/doctor.test.js +1 -13
- package/dist/main.js +1 -1
- package/dist/registry.d.ts +0 -4
- package/dist/registry.js +0 -1
- package/dist/runtime.d.ts +1 -3
- package/dist/runtime.js +2 -2
- package/package.json +4 -1
- package/src/browser.test.ts +2 -0
- package/src/browser.ts +34 -141
- package/src/clis/boss/search.ts +1 -1
- package/src/clis/v2ex/daily.ts +1 -1
- package/src/clis/v2ex/me.ts +1 -1
- package/src/clis/v2ex/notifications.ts +1 -1
- package/src/doctor.test.ts +1 -13
- package/src/doctor.ts +5 -60
- package/src/main.ts +1 -1
- package/src/registry.ts +0 -7
- package/src/runtime.ts +1 -2
package/README.md
CHANGED
|
@@ -41,9 +41,9 @@ A CLI tool that turns **any website** into a command-line interface. **57 comman
|
|
|
41
41
|
|
|
42
42
|
> **⚠️ 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.
|
|
43
43
|
|
|
44
|
-
OpenCLI
|
|
44
|
+
OpenCLI connects to your browser through the Playwright MCP Bridge extension.
|
|
45
45
|
|
|
46
|
-
###
|
|
46
|
+
### Playwright MCP Bridge Extension Setup
|
|
47
47
|
|
|
48
48
|
1. Install **[Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm)** extension in Chrome.
|
|
49
49
|
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
|
|
|
72
72
|
export PLAYWRIGHT_MCP_EXTENSION_TOKEN="<your-token-here>"
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
### Connection Method B: Chrome 144+ Auto-Discovery (Fallback)
|
|
76
|
-
|
|
77
|
-
No extensions needed. Just enable Chrome's built-in remote debugging:
|
|
78
|
-
|
|
79
|
-
1. Open `chrome://inspect#remote-debugging` in Chrome
|
|
80
|
-
2. Check **"Allow remote debugging for this browser instance"**
|
|
81
|
-
3. Set `OPENCLI_USE_CDP=1` before running opencli
|
|
82
|
-
|
|
83
|
-
*You can also manually specify an endpoint via `OPENCLI_CDP_ENDPOINT` env var.*
|
|
84
|
-
|
|
85
75
|
## Quick Start
|
|
86
76
|
|
|
87
77
|
### Install via npm (recommended)
|
|
@@ -184,8 +174,6 @@ Explore outputs to `.opencli/explore/<site>/` (manifest.json, endpoints.json, ca
|
|
|
184
174
|
- **"Failed to connect to Playwright MCP Bridge"**
|
|
185
175
|
- Ensure the Playwright MCP extension is installed and **enabled** in your running Chrome.
|
|
186
176
|
- Restart the Chrome browser if you just installed the extension.
|
|
187
|
-
- **"CDP command failed" or "boss search blocked"**
|
|
188
|
-
- 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.
|
|
189
177
|
- **Empty data returns or 'Unauthorized' error**
|
|
190
178
|
- 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.
|
|
191
179
|
- **Node API errors**
|
package/README.zh-CN.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://nodejs.org)
|
|
10
10
|
[](./LICENSE)
|
|
11
11
|
|
|
12
|
-
OpenCLI
|
|
12
|
+
OpenCLI 将任何网站变成命令行工具。**57 个命令**覆盖 **17 个站点** — B站、知乎、小红书、Twitter、Reddit、雪球、GitHub、V2EX、Hacker News、BBC、微博、BOSS直聘、Yahoo Finance、路透社、什么值得买、携程、YouTube — 复用浏览器登录态,AI 驱动探索。
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -41,9 +41,9 @@ OpenCLI 通过 Chrome 浏览器 + [Playwright MCP Bridge](https://github.com/nic
|
|
|
41
41
|
|
|
42
42
|
> **⚠️ 重要**:大多数命令复用你的 Chrome 登录状态。运行命令前,你必须已在 Chrome 中打开目标网站并完成登录。如果获取到空数据或报错,请先检查你的浏览器登录状态。
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
OpenCLI 通过 Playwright MCP Bridge 扩展与你的浏览器通信。
|
|
45
45
|
|
|
46
|
-
###
|
|
46
|
+
### Playwright MCP Bridge 扩展配置
|
|
47
47
|
|
|
48
48
|
1. 安装 **[Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm)** 扩展
|
|
49
49
|
2. 在浏览器插件栏点击该插件,或者在插件设置页获取你的 Extension Token。
|
|
@@ -72,16 +72,6 @@ OpenCLI 通过 Chrome 浏览器 + [Playwright MCP Bridge](https://github.com/nic
|
|
|
72
72
|
export PLAYWRIGHT_MCP_EXTENSION_TOKEN="<你的-token>"
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
### 连接方式 B:Chrome 144+ CDP 自动发现(备选)
|
|
76
|
-
|
|
77
|
-
无需安装任何扩展。只需开启 Chrome 内置的远程调试:
|
|
78
|
-
|
|
79
|
-
1. 在 Chrome 中打开 `chrome://inspect#remote-debugging`
|
|
80
|
-
2. 勾选 **"允许对此浏览器实例进行远程调试" (Allow remote debugging for this browser instance)**
|
|
81
|
-
3. 运行时设置环境变量 `OPENCLI_USE_CDP=1`
|
|
82
|
-
|
|
83
|
-
*也可通过 `OPENCLI_CDP_ENDPOINT` 环境变量手动指定 CDP endpoint 地址。*
|
|
84
|
-
|
|
85
75
|
## 快速开始
|
|
86
76
|
|
|
87
77
|
### npm 全局安装(推荐)
|
|
@@ -184,8 +174,6 @@ opencli cascade https://api.example.com/data
|
|
|
184
174
|
- **"Failed to connect to Playwright MCP Bridge"** 报错
|
|
185
175
|
- 确保你当前的 Chrome 已安装且**开启了** Playwright MCP Bridge 浏览器插件。
|
|
186
176
|
- 如果是刚装完插件,需要重启 Chrome 浏览器。
|
|
187
|
-
- **"CDP command failed" / "被风控拦截"**
|
|
188
|
-
- 有些网站(例如 BOSS 直聘)会因为开了 DevTools 或者 CDP 端口拦截验证。OpenCLI 有 cookie 降级机制,通常不需要干预,不用去强行加上 CDP 标识参数即可。
|
|
189
177
|
- **返回空数据,或者报错 "Unauthorized"**
|
|
190
178
|
- Chrome 里的登录态可能已经过期(甚至被要求过滑动验证码)。请打开当前 Chrome 页面,在新标签页重新手工登录或刷新该页面。
|
|
191
179
|
- **Node API 错误 (如 parseArgs, fs 等)**
|
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: opencli
|
|
3
3
|
description: "OpenCLI — Make any website your CLI. Zero risk, AI-powered, reuse Chrome login."
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.6
|
|
5
5
|
author: jackwener
|
|
6
6
|
tags: [cli, browser, web, mcp, playwright, bilibili, zhihu, twitter, github, v2ex, hackernews, reddit, xiaohongshu, xueqiu, AI, agent]
|
|
7
7
|
---
|
|
@@ -34,8 +34,7 @@ npm update -g @jackwener/opencli
|
|
|
34
34
|
|
|
35
35
|
Browser commands require:
|
|
36
36
|
1. Chrome browser running **(logged into target sites)**
|
|
37
|
-
2. [Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm) extension
|
|
38
|
-
3. **Alternative**: Chrome 144+ CDP auto-discovery — set `OPENCLI_USE_CDP=1` (no extension needed)
|
|
37
|
+
2. [Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm) extension installed and configured
|
|
39
38
|
|
|
40
39
|
> **Note**: You must be logged into the target website in Chrome before running commands. Tabs opened during command execution are auto-closed afterwards.
|
|
41
40
|
|
|
@@ -339,9 +338,6 @@ ${{ index + 1 }}
|
|
|
339
338
|
| `OPENCLI_BROWSER_CONNECT_TIMEOUT` | 30 | Browser connection timeout (sec) |
|
|
340
339
|
| `OPENCLI_BROWSER_COMMAND_TIMEOUT` | 45 | Command execution timeout (sec) |
|
|
341
340
|
| `OPENCLI_BROWSER_EXPLORE_TIMEOUT` | 120 | Explore timeout (sec) |
|
|
342
|
-
| `OPENCLI_CDP_ENDPOINT` | — | Manual CDP WebSocket endpoint (overrides auto-discovery) |
|
|
343
|
-
| `OPENCLI_USE_CDP` | — | Set to `1` to use Chrome 144+ CDP auto-discovery instead of extension |
|
|
344
|
-
| `OPENCLI_FORCE_EXTENSION` | — | Set to `1` to skip CDP and force extension mode |
|
|
345
341
|
| `PLAYWRIGHT_MCP_EXTENSION_TOKEN` | — | Auto-approve extension connection |
|
|
346
342
|
|
|
347
343
|
## Troubleshooting
|
|
@@ -349,6 +345,6 @@ ${{ index + 1 }}
|
|
|
349
345
|
| Issue | Solution |
|
|
350
346
|
|-------|----------|
|
|
351
347
|
| `npx not found` | Install Node.js: `brew install node` |
|
|
352
|
-
| `Timed out connecting to browser` | 1) Chrome must be open 2)
|
|
348
|
+
| `Timed out connecting to browser` | 1) Chrome must be open 2) Install MCP Bridge extension and configure token |
|
|
353
349
|
| `Target page context` error | Add `navigate:` step before `evaluate:` in YAML |
|
|
354
350
|
| Empty table data | Check if evaluate returns JSON string (MCP parsing) or data path is wrong |
|
package/dist/browser.d.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Browser interaction via
|
|
3
|
-
* Connects to an existing Chrome browser through
|
|
2
|
+
* Browser interaction via Playwright MCP Bridge extension.
|
|
3
|
+
* Connects to an existing Chrome browser through the extension.
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'cdp-timeout' | 'mcp-init' | 'process-exit' | 'unknown';
|
|
5
|
+
type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'mcp-init' | 'process-exit' | 'unknown';
|
|
7
6
|
type PlaywrightMCPState = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed';
|
|
8
7
|
type ConnectFailureInput = {
|
|
9
8
|
kind: ConnectFailureKind;
|
|
10
|
-
mode: 'extension' | 'cdp';
|
|
11
9
|
timeout: number;
|
|
12
10
|
hasExtensionToken: boolean;
|
|
13
11
|
tokenFingerprint?: string | null;
|
|
@@ -80,7 +78,6 @@ export declare class PlaywrightMCP {
|
|
|
80
78
|
private _resetAfterFailedConnect;
|
|
81
79
|
connect(opts?: {
|
|
82
80
|
timeout?: number;
|
|
83
|
-
forceExtension?: boolean;
|
|
84
81
|
}): Promise<Page>;
|
|
85
82
|
close(): Promise<void>;
|
|
86
83
|
}
|
package/dist/browser.js
CHANGED
|
@@ -1,76 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Browser interaction via
|
|
3
|
-
* Connects to an existing Chrome browser through
|
|
2
|
+
* Browser interaction via Playwright MCP Bridge extension.
|
|
3
|
+
* Connects to an existing Chrome browser through the extension.
|
|
4
4
|
*/
|
|
5
5
|
import { spawn, execSync } from 'node:child_process';
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
|
-
import * as net from 'node:net';
|
|
8
7
|
import { fileURLToPath } from 'node:url';
|
|
9
8
|
import * as fs from 'node:fs';
|
|
10
9
|
import * as os from 'node:os';
|
|
11
10
|
import * as path from 'node:path';
|
|
12
11
|
import { formatSnapshot } from './snapshotFormatter.js';
|
|
13
|
-
/**
|
|
14
|
-
* Chrome 144+ auto-discovery: read DevToolsActivePort file to get CDP endpoint.
|
|
15
|
-
*
|
|
16
|
-
* Starting with Chrome 144, users can enable remote debugging from
|
|
17
|
-
* chrome://inspect#remote-debugging without any command-line flags.
|
|
18
|
-
* Chrome writes the active port and browser GUID to a DevToolsActivePort file
|
|
19
|
-
* in the user data directory, which we read to construct the WebSocket endpoint.
|
|
20
|
-
*
|
|
21
|
-
* Priority: OPENCLI_CDP_ENDPOINT env > DevToolsActivePort auto-discovery > --extension fallback
|
|
22
|
-
*/
|
|
23
|
-
/** Quick TCP port probe to verify Chrome is actually listening */
|
|
24
|
-
function isPortReachable(port, host = '127.0.0.1', timeoutMs = 800) {
|
|
25
|
-
return new Promise(resolve => {
|
|
26
|
-
const sock = net.createConnection({ port, host });
|
|
27
|
-
sock.setTimeout(timeoutMs);
|
|
28
|
-
sock.on('connect', () => { sock.destroy(); resolve(true); });
|
|
29
|
-
sock.on('error', () => resolve(false));
|
|
30
|
-
sock.on('timeout', () => { sock.destroy(); resolve(false); });
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
export async function discoverChromeEndpoint() {
|
|
34
|
-
const candidates = [];
|
|
35
|
-
// User-specified Chrome data dir takes highest priority
|
|
36
|
-
if (process.env.CHROME_USER_DATA_DIR) {
|
|
37
|
-
candidates.push(path.join(process.env.CHROME_USER_DATA_DIR, 'DevToolsActivePort'));
|
|
38
|
-
}
|
|
39
|
-
// Standard Chrome/Edge user data dirs per platform
|
|
40
|
-
if (process.platform === 'win32') {
|
|
41
|
-
const localAppData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local');
|
|
42
|
-
candidates.push(path.join(localAppData, 'Google', 'Chrome', 'User Data', 'DevToolsActivePort'));
|
|
43
|
-
candidates.push(path.join(localAppData, 'Microsoft', 'Edge', 'User Data', 'DevToolsActivePort'));
|
|
44
|
-
}
|
|
45
|
-
else if (process.platform === 'darwin') {
|
|
46
|
-
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'DevToolsActivePort'));
|
|
47
|
-
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Microsoft Edge', 'DevToolsActivePort'));
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
candidates.push(path.join(os.homedir(), '.config', 'google-chrome', 'DevToolsActivePort'));
|
|
51
|
-
candidates.push(path.join(os.homedir(), '.config', 'chromium', 'DevToolsActivePort'));
|
|
52
|
-
candidates.push(path.join(os.homedir(), '.config', 'microsoft-edge', 'DevToolsActivePort'));
|
|
53
|
-
}
|
|
54
|
-
for (const filePath of candidates) {
|
|
55
|
-
try {
|
|
56
|
-
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
|
57
|
-
const lines = content.split('\n');
|
|
58
|
-
if (lines.length >= 2) {
|
|
59
|
-
const port = parseInt(lines[0], 10);
|
|
60
|
-
const browserPath = lines[1]; // e.g. /devtools/browser/<GUID>
|
|
61
|
-
if (port > 0 && browserPath.startsWith('/devtools/browser/')) {
|
|
62
|
-
const endpoint = `ws://127.0.0.1:${port}${browserPath}`;
|
|
63
|
-
// Verify the port is actually reachable (Chrome may have closed, leaving a stale file)
|
|
64
|
-
if (await isPortReachable(port)) {
|
|
65
|
-
return endpoint;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
catch { }
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
12
|
// Read version from package.json (single source of truth)
|
|
75
13
|
const __browser_dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
76
14
|
const PKG_VERSION = (() => { try {
|
|
@@ -81,6 +19,7 @@ catch {
|
|
|
81
19
|
} })();
|
|
82
20
|
const CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
|
|
83
21
|
const STDERR_BUFFER_LIMIT = 16 * 1024;
|
|
22
|
+
const INITIAL_TABS_TIMEOUT_MS = 1500;
|
|
84
23
|
const TAB_CLEANUP_TIMEOUT_MS = 2000;
|
|
85
24
|
let _cachedMcpServerPath;
|
|
86
25
|
export function getTokenFingerprint(token) {
|
|
@@ -92,31 +31,24 @@ export function formatBrowserConnectError(input) {
|
|
|
92
31
|
const stderr = input.stderr?.trim();
|
|
93
32
|
const suffix = stderr ? `\n\nMCP stderr:\n${stderr}` : '';
|
|
94
33
|
const tokenHint = input.tokenFingerprint ? ` Token fingerprint: ${input.tokenFingerprint}.` : '';
|
|
95
|
-
if (input.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
suffix);
|
|
107
|
-
}
|
|
108
|
-
if (input.kind === 'extension-timeout') {
|
|
109
|
-
const likelyCause = input.hasExtensionToken
|
|
110
|
-
? `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.`
|
|
111
|
-
: 'PLAYWRIGHT_MCP_EXTENSION_TOKEN is not configured, so the extension may be waiting for manual approval.';
|
|
112
|
-
return new Error(`Timed out connecting to Playwright MCP Bridge (${input.timeout}s).\n\n` +
|
|
113
|
-
`${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.` +
|
|
114
|
-
suffix);
|
|
115
|
-
}
|
|
34
|
+
if (input.kind === 'missing-token') {
|
|
35
|
+
return new Error('Failed to connect to Playwright MCP Bridge: PLAYWRIGHT_MCP_EXTENSION_TOKEN is not set.\n\n' +
|
|
36
|
+
'Without this token, Chrome will show a manual approval dialog for every new MCP connection. ' +
|
|
37
|
+
'Copy the token from the Playwright MCP Bridge extension and set it in BOTH your shell environment and MCP client config.' +
|
|
38
|
+
suffix);
|
|
39
|
+
}
|
|
40
|
+
if (input.kind === 'extension-not-installed') {
|
|
41
|
+
return new Error('Failed to connect to Playwright MCP Bridge: the browser extension did not attach.\n\n' +
|
|
42
|
+
'Make sure Chrome is running and the "Playwright MCP Bridge" extension is installed and enabled. ' +
|
|
43
|
+
'If Chrome shows an approval dialog, click Allow.' +
|
|
44
|
+
suffix);
|
|
116
45
|
}
|
|
117
|
-
if (input.
|
|
118
|
-
|
|
119
|
-
|
|
46
|
+
if (input.kind === 'extension-timeout') {
|
|
47
|
+
const likelyCause = input.hasExtensionToken
|
|
48
|
+
? `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.`
|
|
49
|
+
: 'PLAYWRIGHT_MCP_EXTENSION_TOKEN is not configured, so the extension may be waiting for manual approval.';
|
|
50
|
+
return new Error(`Timed out connecting to Playwright MCP Bridge (${input.timeout}s).\n\n` +
|
|
51
|
+
`${likelyCause} If a browser prompt is visible, click Allow.` +
|
|
120
52
|
suffix);
|
|
121
53
|
}
|
|
122
54
|
if (input.kind === 'mcp-init') {
|
|
@@ -130,7 +62,7 @@ export function formatBrowserConnectError(input) {
|
|
|
130
62
|
}
|
|
131
63
|
function inferConnectFailureKind(args) {
|
|
132
64
|
const haystack = `${args.rawMessage ?? ''}\n${args.stderr}`.toLowerCase();
|
|
133
|
-
if (
|
|
65
|
+
if (!args.hasExtensionToken)
|
|
134
66
|
return 'missing-token';
|
|
135
67
|
if (haystack.includes('extension connection timeout') || haystack.includes('playwright mcp bridge'))
|
|
136
68
|
return 'extension-not-installed';
|
|
@@ -138,11 +70,7 @@ function inferConnectFailureKind(args) {
|
|
|
138
70
|
return 'mcp-init';
|
|
139
71
|
if (args.exited)
|
|
140
72
|
return 'process-exit';
|
|
141
|
-
|
|
142
|
-
return 'extension-timeout';
|
|
143
|
-
if (args.mode === 'cdp')
|
|
144
|
-
return 'cdp-timeout';
|
|
145
|
-
return 'unknown';
|
|
73
|
+
return 'extension-timeout';
|
|
146
74
|
}
|
|
147
75
|
// JSON-RPC helpers
|
|
148
76
|
let _nextId = 1;
|
|
@@ -435,25 +363,9 @@ export class PlaywrightMCP {
|
|
|
435
363
|
PlaywrightMCP._activeInsts.add(this);
|
|
436
364
|
this._state = 'connecting';
|
|
437
365
|
const timeout = opts.timeout ?? CONNECT_TIMEOUT;
|
|
438
|
-
// Connection priority:
|
|
439
|
-
// 1. OPENCLI_CDP_ENDPOINT env var → explicit CDP endpoint
|
|
440
|
-
// 2. OPENCLI_USE_CDP=1 → auto-discover via DevToolsActivePort
|
|
441
|
-
// 3. Default → --extension mode (Playwright MCP Bridge)
|
|
442
|
-
// Some anti-bot sites (e.g. BOSS Zhipin) detect CDP — use forceExtension to bypass.
|
|
443
|
-
const forceExt = opts.forceExtension || process.env.OPENCLI_FORCE_EXTENSION === '1';
|
|
444
|
-
let cdpEndpoint = null;
|
|
445
|
-
if (!forceExt) {
|
|
446
|
-
if (process.env.OPENCLI_CDP_ENDPOINT) {
|
|
447
|
-
cdpEndpoint = process.env.OPENCLI_CDP_ENDPOINT;
|
|
448
|
-
}
|
|
449
|
-
else if (process.env.OPENCLI_USE_CDP === '1') {
|
|
450
|
-
cdpEndpoint = await discoverChromeEndpoint();
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
366
|
return new Promise((resolve, reject) => {
|
|
454
367
|
const isDebug = process.env.DEBUG?.includes('opencli:mcp');
|
|
455
368
|
const debugLog = (msg) => isDebug && console.error(`[opencli:mcp] ${msg}`);
|
|
456
|
-
const mode = cdpEndpoint ? 'cdp' : 'extension';
|
|
457
369
|
const extensionToken = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
|
|
458
370
|
const tokenFingerprint = getTokenFingerprint(extensionToken);
|
|
459
371
|
let stderrBuffer = '';
|
|
@@ -467,7 +379,6 @@ export class PlaywrightMCP {
|
|
|
467
379
|
this._resetAfterFailedConnect();
|
|
468
380
|
reject(formatBrowserConnectError({
|
|
469
381
|
kind,
|
|
470
|
-
mode,
|
|
471
382
|
timeout,
|
|
472
383
|
hasExtensionToken: !!extensionToken,
|
|
473
384
|
tokenFingerprint,
|
|
@@ -487,23 +398,13 @@ export class PlaywrightMCP {
|
|
|
487
398
|
const timer = setTimeout(() => {
|
|
488
399
|
debugLog('Connection timed out');
|
|
489
400
|
settleError(inferConnectFailureKind({
|
|
490
|
-
mode,
|
|
491
401
|
hasExtensionToken: !!extensionToken,
|
|
492
402
|
stderr: stderrBuffer,
|
|
493
403
|
}));
|
|
494
404
|
}, timeout * 1000);
|
|
495
|
-
const mcpArgs = [mcpPath];
|
|
496
|
-
if (cdpEndpoint) {
|
|
497
|
-
mcpArgs.push('--cdp-endpoint', cdpEndpoint);
|
|
498
|
-
}
|
|
499
|
-
else {
|
|
500
|
-
mcpArgs.push('--extension');
|
|
501
|
-
}
|
|
405
|
+
const mcpArgs = [mcpPath, '--extension'];
|
|
502
406
|
if (process.env.OPENCLI_VERBOSE) {
|
|
503
|
-
console.error(`[opencli]
|
|
504
|
-
if (mode === 'extension') {
|
|
505
|
-
console.error(`[opencli] Extension token: ${extensionToken ? `configured (fingerprint ${tokenFingerprint})` : 'missing'}`);
|
|
506
|
-
}
|
|
407
|
+
console.error(`[opencli] Extension token: ${extensionToken ? `configured (fingerprint ${tokenFingerprint})` : 'missing'}`);
|
|
507
408
|
}
|
|
508
409
|
if (process.env.OPENCLI_BROWSER_EXECUTABLE_PATH) {
|
|
509
410
|
mcpArgs.push('--executablePath', process.env.OPENCLI_BROWSER_EXECUTABLE_PATH);
|
|
@@ -557,7 +458,6 @@ export class PlaywrightMCP {
|
|
|
557
458
|
this._rejectPendingRequests(new Error(`Playwright MCP process exited before response${code == null ? '' : ` (code ${code})`}`));
|
|
558
459
|
if (!settled) {
|
|
559
460
|
settleError(inferConnectFailureKind({
|
|
560
|
-
mode,
|
|
561
461
|
hasExtensionToken: !!extensionToken,
|
|
562
462
|
stderr: stderrBuffer,
|
|
563
463
|
exited: true,
|
|
@@ -574,7 +474,6 @@ export class PlaywrightMCP {
|
|
|
574
474
|
debugLog('Got initialize response');
|
|
575
475
|
if (resp.error) {
|
|
576
476
|
settleError(inferConnectFailureKind({
|
|
577
|
-
mode,
|
|
578
477
|
hasExtensionToken: !!extensionToken,
|
|
579
478
|
stderr: stderrBuffer,
|
|
580
479
|
rawMessage: `MCP init failed: ${resp.error.message}`,
|
|
@@ -584,9 +483,9 @@ export class PlaywrightMCP {
|
|
|
584
483
|
const initializedMsg = JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n';
|
|
585
484
|
debugLog(`SEND: ${initializedMsg.trim()}`);
|
|
586
485
|
this._proc?.stdin?.write(initializedMsg);
|
|
587
|
-
//
|
|
486
|
+
// Use tabs as a readiness probe and for tab cleanup bookkeeping.
|
|
588
487
|
debugLog('Fetching initial tabs count...');
|
|
589
|
-
withTimeout(page.tabs(),
|
|
488
|
+
withTimeout(page.tabs(), INITIAL_TABS_TIMEOUT_MS, 'Timed out fetching initial tabs').then((tabs) => {
|
|
590
489
|
debugLog(`Tabs response: ${typeof tabs === 'string' ? tabs : JSON.stringify(tabs)}`);
|
|
591
490
|
this._initialTabIdentities = extractTabIdentities(tabs);
|
|
592
491
|
settleSuccess(page);
|
|
@@ -608,7 +507,7 @@ export class PlaywrightMCP {
|
|
|
608
507
|
this._state = 'closing';
|
|
609
508
|
this._closingPromise = (async () => {
|
|
610
509
|
try {
|
|
611
|
-
//
|
|
510
|
+
// Extension mode opens bridge/session tabs that we can clean up best-effort.
|
|
612
511
|
if (this._page && this._proc && !this._proc.killed) {
|
|
613
512
|
try {
|
|
614
513
|
const tabs = await withTimeout(this._page.tabs(), TAB_CLEANUP_TIMEOUT_MS, 'Timed out fetching tabs during cleanup');
|
package/dist/clis/boss/search.js
CHANGED
|
@@ -67,7 +67,6 @@ cli({
|
|
|
67
67
|
description: 'BOSS直聘搜索职位',
|
|
68
68
|
domain: 'www.zhipin.com',
|
|
69
69
|
strategy: Strategy.COOKIE,
|
|
70
|
-
forceExtension: true, // BOSS Zhipin detects CDP mode — must use extension bridge
|
|
71
70
|
browser: true,
|
|
72
71
|
args: [
|
|
73
72
|
{ name: 'query', required: true, help: 'Search keyword (e.g. AI agent, 前端)' },
|
package/dist/clis/v2ex/daily.js
CHANGED
package/dist/clis/v2ex/me.js
CHANGED
package/dist/doctor.d.ts
CHANGED
|
@@ -28,11 +28,6 @@ export type DoctorReport = {
|
|
|
28
28
|
envFingerprint: string | null;
|
|
29
29
|
shellFiles: ShellFileStatus[];
|
|
30
30
|
configs: McpConfigStatus[];
|
|
31
|
-
remoteDebuggingEnabled: boolean;
|
|
32
|
-
remoteDebuggingEndpoint: string | null;
|
|
33
|
-
cdpEnabled: boolean;
|
|
34
|
-
cdpToken: string | null;
|
|
35
|
-
cdpFingerprint: string | null;
|
|
36
31
|
recommendedToken: string | null;
|
|
37
32
|
recommendedFingerprint: string | null;
|
|
38
33
|
warnings: string[];
|
package/dist/doctor.js
CHANGED
|
@@ -3,8 +3,7 @@ import * as os from 'node:os';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { createInterface } from 'node:readline/promises';
|
|
5
5
|
import { stdin as input, stdout as output } from 'node:process';
|
|
6
|
-
import {
|
|
7
|
-
import { browserSession } from './runtime.js';
|
|
6
|
+
import { getTokenFingerprint } from './browser.js';
|
|
8
7
|
const PLAYWRIGHT_SERVER_NAME = 'playwright';
|
|
9
8
|
const PLAYWRIGHT_TOKEN_ENV = 'PLAYWRIGHT_MCP_EXTENSION_TOKEN';
|
|
10
9
|
const PLAYWRIGHT_EXTENSION_ID = 'mmlmfjhmonkocbjadbfplnigmagldckm';
|
|
@@ -170,46 +169,8 @@ function readConfigStatus(filePath) {
|
|
|
170
169
|
};
|
|
171
170
|
}
|
|
172
171
|
}
|
|
173
|
-
async function extractTokenViaCdp() {
|
|
174
|
-
if (!(process.env.OPENCLI_USE_CDP === '1' || process.env.OPENCLI_CDP_ENDPOINT))
|
|
175
|
-
return null;
|
|
176
|
-
const candidates = [
|
|
177
|
-
`chrome-extension://${PLAYWRIGHT_EXTENSION_ID}/options.html`,
|
|
178
|
-
`chrome-extension://${PLAYWRIGHT_EXTENSION_ID}/popup.html`,
|
|
179
|
-
`chrome-extension://${PLAYWRIGHT_EXTENSION_ID}/connect.html`,
|
|
180
|
-
`chrome-extension://${PLAYWRIGHT_EXTENSION_ID}/index.html`,
|
|
181
|
-
];
|
|
182
|
-
const result = await browserSession(PlaywrightMCP, async (page) => {
|
|
183
|
-
for (const url of candidates) {
|
|
184
|
-
try {
|
|
185
|
-
await page.goto(url);
|
|
186
|
-
await page.wait(1);
|
|
187
|
-
const token = await page.evaluate(`() => {
|
|
188
|
-
const values = new Set();
|
|
189
|
-
const push = (value) => {
|
|
190
|
-
if (!value || typeof value !== 'string') return;
|
|
191
|
-
for (const match of value.matchAll(/[A-Za-z0-9_-]{24,}/g)) values.add(match[0]);
|
|
192
|
-
};
|
|
193
|
-
document.querySelectorAll('input, textarea, code, pre, span, div').forEach((el) => {
|
|
194
|
-
push(el.value);
|
|
195
|
-
push(el.textContent || '');
|
|
196
|
-
push(el.getAttribute && el.getAttribute('value'));
|
|
197
|
-
});
|
|
198
|
-
return Array.from(values);
|
|
199
|
-
}`);
|
|
200
|
-
const matches = Array.isArray(token) ? token.filter((v) => v.length >= 24) : [];
|
|
201
|
-
if (matches.length > 0)
|
|
202
|
-
return matches.sort((a, b) => b.length - a.length)[0];
|
|
203
|
-
}
|
|
204
|
-
catch { }
|
|
205
|
-
}
|
|
206
|
-
return null;
|
|
207
|
-
});
|
|
208
|
-
return typeof result === 'string' && result ? result : null;
|
|
209
|
-
}
|
|
210
172
|
export async function runBrowserDoctor(opts = {}) {
|
|
211
173
|
const envToken = process.env[PLAYWRIGHT_TOKEN_ENV] ?? null;
|
|
212
|
-
const remoteDebuggingEndpoint = await discoverChromeEndpoint().catch(() => null);
|
|
213
174
|
const shellPath = opts.shellRc ?? getDefaultShellRcPath();
|
|
214
175
|
const shellFiles = [shellPath].map((filePath) => {
|
|
215
176
|
if (!fileExists(filePath))
|
|
@@ -220,27 +181,20 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
220
181
|
});
|
|
221
182
|
const configPaths = opts.configPaths?.length ? opts.configPaths : getDefaultMcpConfigPaths();
|
|
222
183
|
const configs = configPaths.map(readConfigStatus);
|
|
223
|
-
const cdpToken = !opts.token && !envToken ? await extractTokenViaCdp().catch(() => null) : null;
|
|
224
184
|
const allTokens = [
|
|
225
185
|
opts.token ?? null,
|
|
226
186
|
envToken,
|
|
227
187
|
...shellFiles.map(s => s.token),
|
|
228
188
|
...configs.map(c => c.token),
|
|
229
|
-
cdpToken,
|
|
230
189
|
].filter((v) => !!v);
|
|
231
190
|
const uniqueTokens = [...new Set(allTokens)];
|
|
232
|
-
const recommendedToken = opts.token ?? envToken ?? (uniqueTokens.length === 1 ? uniqueTokens[0] :
|
|
191
|
+
const recommendedToken = opts.token ?? envToken ?? (uniqueTokens.length === 1 ? uniqueTokens[0] : null) ?? null;
|
|
233
192
|
const report = {
|
|
234
193
|
cliVersion: opts.cliVersion,
|
|
235
194
|
envToken,
|
|
236
195
|
envFingerprint: getTokenFingerprint(envToken ?? undefined),
|
|
237
196
|
shellFiles,
|
|
238
197
|
configs,
|
|
239
|
-
remoteDebuggingEnabled: !!remoteDebuggingEndpoint,
|
|
240
|
-
remoteDebuggingEndpoint,
|
|
241
|
-
cdpEnabled: process.env.OPENCLI_USE_CDP === '1' || !!process.env.OPENCLI_CDP_ENDPOINT,
|
|
242
|
-
cdpToken,
|
|
243
|
-
cdpFingerprint: getTokenFingerprint(cdpToken ?? undefined),
|
|
244
198
|
recommendedToken,
|
|
245
199
|
recommendedFingerprint: getTokenFingerprint(recommendedToken ?? undefined),
|
|
246
200
|
warnings: [],
|
|
@@ -254,17 +208,12 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
254
208
|
report.issues.push('No scanned MCP config currently contains a Playwright extension token.');
|
|
255
209
|
if (uniqueTokens.length > 1)
|
|
256
210
|
report.issues.push('Detected inconsistent Playwright MCP tokens across env/config files.');
|
|
257
|
-
if (!report.remoteDebuggingEnabled)
|
|
258
|
-
report.warnings.push('Chrome remote debugging appears to be disabled or Chrome is not currently exposing a DevTools endpoint.');
|
|
259
211
|
for (const config of configs) {
|
|
260
212
|
if (config.parseError)
|
|
261
213
|
report.warnings.push(`Could not parse ${config.path}: ${config.parseError}`);
|
|
262
214
|
}
|
|
263
215
|
if (!recommendedToken) {
|
|
264
|
-
|
|
265
|
-
report.warnings.push('CDP is enabled, but no token could be extracted automatically from the extension UI.');
|
|
266
|
-
else
|
|
267
|
-
report.warnings.push('No token source found. Enable OPENCLI_USE_CDP=1 to allow a best-effort token read from the extension page.');
|
|
216
|
+
report.warnings.push('No token source found.');
|
|
268
217
|
}
|
|
269
218
|
return report;
|
|
270
219
|
}
|
|
@@ -277,9 +226,6 @@ export function renderBrowserDoctorReport(report) {
|
|
|
277
226
|
const uniqueFingerprints = [...new Set(tokenFingerprints)];
|
|
278
227
|
const hasMismatch = uniqueFingerprints.length > 1;
|
|
279
228
|
const lines = [`opencli v${report.cliVersion ?? 'unknown'} doctor`, ''];
|
|
280
|
-
lines.push(statusLine(report.remoteDebuggingEnabled ? 'OK' : 'WARN', `Chrome remote debugging: ${report.remoteDebuggingEnabled ? 'enabled' : 'disabled'}`));
|
|
281
|
-
if (report.remoteDebuggingEndpoint)
|
|
282
|
-
lines.push(` ${report.remoteDebuggingEndpoint}`);
|
|
283
229
|
const envStatus = !report.envToken ? 'MISSING' : hasMismatch ? 'MISMATCH' : 'OK';
|
|
284
230
|
lines.push(statusLine(envStatus, `Environment token: ${tokenSummary(report.envToken, report.envFingerprint)}`));
|
|
285
231
|
for (const shell of report.shellFiles) {
|
|
@@ -306,10 +252,6 @@ export function renderBrowserDoctorReport(report) {
|
|
|
306
252
|
}
|
|
307
253
|
if (missingConfigCount > 0)
|
|
308
254
|
lines.push(` Other scanned config locations not present: ${missingConfigCount}`);
|
|
309
|
-
if (report.cdpEnabled) {
|
|
310
|
-
const cdpStatus = report.cdpToken ? 'OK' : 'WARN';
|
|
311
|
-
lines.push(statusLine(cdpStatus, `CDP token probe: ${tokenSummary(report.cdpToken, report.cdpFingerprint)}`));
|
|
312
|
-
}
|
|
313
255
|
lines.push('');
|
|
314
256
|
lines.push(statusLine(hasMismatch ? 'MISMATCH' : report.recommendedToken ? 'OK' : 'WARN', `Recommended token fingerprint: ${report.recommendedFingerprint ?? 'unavailable'}`));
|
|
315
257
|
if (report.issues.length) {
|
|
@@ -341,7 +283,7 @@ function writeFileWithMkdir(filePath, content) {
|
|
|
341
283
|
export async function applyBrowserDoctorFix(report, opts = {}) {
|
|
342
284
|
const token = opts.token ?? report.recommendedToken;
|
|
343
285
|
if (!token)
|
|
344
|
-
throw new Error('No Playwright MCP token is available to write. Provide --token
|
|
286
|
+
throw new Error('No Playwright MCP token is available to write. Provide --token first.');
|
|
345
287
|
const plannedWrites = [];
|
|
346
288
|
const shellPath = opts.shellRc ?? report.shellFiles[0]?.path ?? getDefaultShellRcPath();
|
|
347
289
|
plannedWrites.push(shellPath);
|
package/dist/doctor.test.js
CHANGED
|
@@ -76,17 +76,11 @@ describe('doctor report rendering', () => {
|
|
|
76
76
|
envFingerprint: 'fp1',
|
|
77
77
|
shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'abc123', fingerprint: 'fp1' }],
|
|
78
78
|
configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }],
|
|
79
|
-
remoteDebuggingEnabled: true,
|
|
80
|
-
remoteDebuggingEndpoint: 'ws://127.0.0.1:9222/devtools/browser/test',
|
|
81
|
-
cdpEnabled: false,
|
|
82
|
-
cdpToken: null,
|
|
83
|
-
cdpFingerprint: null,
|
|
84
79
|
recommendedToken: 'abc123',
|
|
85
80
|
recommendedFingerprint: 'fp1',
|
|
86
81
|
warnings: [],
|
|
87
82
|
issues: [],
|
|
88
83
|
});
|
|
89
|
-
expect(text).toContain('[OK] Chrome remote debugging: enabled');
|
|
90
84
|
expect(text).toContain('[OK] Environment token: configured (fp1)');
|
|
91
85
|
expect(text).toContain('[OK] MCP config /tmp/mcp.json: configured (fp1)');
|
|
92
86
|
});
|
|
@@ -96,17 +90,11 @@ describe('doctor report rendering', () => {
|
|
|
96
90
|
envFingerprint: 'fp1',
|
|
97
91
|
shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'def456', fingerprint: 'fp2' }],
|
|
98
92
|
configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }],
|
|
99
|
-
remoteDebuggingEnabled: false,
|
|
100
|
-
remoteDebuggingEndpoint: null,
|
|
101
|
-
cdpEnabled: false,
|
|
102
|
-
cdpToken: null,
|
|
103
|
-
cdpFingerprint: null,
|
|
104
93
|
recommendedToken: 'abc123',
|
|
105
94
|
recommendedFingerprint: 'fp1',
|
|
106
|
-
warnings: [
|
|
95
|
+
warnings: [],
|
|
107
96
|
issues: ['Detected inconsistent Playwright MCP tokens across env/config files.'],
|
|
108
97
|
});
|
|
109
|
-
expect(text).toContain('[WARN] Chrome remote debugging: disabled');
|
|
110
98
|
expect(text).toContain('[MISMATCH] Environment token: configured (fp1)');
|
|
111
99
|
expect(text).toContain('[MISMATCH] Shell file /tmp/.zshrc: configured (fp2)');
|
|
112
100
|
expect(text).toContain('[MISMATCH] Recommended token fingerprint: fp1');
|
package/dist/main.js
CHANGED
|
@@ -152,7 +152,7 @@ for (const [, cmd] of registry) {
|
|
|
152
152
|
process.env.OPENCLI_VERBOSE = '1';
|
|
153
153
|
let result;
|
|
154
154
|
if (cmd.browser) {
|
|
155
|
-
result = await browserSession(PlaywrightMCP, async (page) => runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) })
|
|
155
|
+
result = await browserSession(PlaywrightMCP, async (page) => runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) }));
|
|
156
156
|
}
|
|
157
157
|
else {
|
|
158
158
|
result = await executeCommand(cmd, null, kwargs, actionOpts.verbose);
|