@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/dist/registry.d.ts
CHANGED
|
@@ -33,8 +33,6 @@ export interface CliCommand {
|
|
|
33
33
|
/** Internal: lazy-loaded TS module support */
|
|
34
34
|
_lazy?: boolean;
|
|
35
35
|
_modulePath?: string;
|
|
36
|
-
/** Force extension bridge mode (bypass CDP), for anti-bot sites */
|
|
37
|
-
forceExtension?: boolean;
|
|
38
36
|
}
|
|
39
37
|
export interface CliOptions {
|
|
40
38
|
site: string;
|
|
@@ -48,8 +46,6 @@ export interface CliOptions {
|
|
|
48
46
|
func?: (page: IPage | null, kwargs: Record<string, any>, debug?: boolean) => Promise<any>;
|
|
49
47
|
pipeline?: any[];
|
|
50
48
|
timeoutSeconds?: number;
|
|
51
|
-
/** Force extension bridge mode (bypass CDP), for anti-bot sites */
|
|
52
|
-
forceExtension?: boolean;
|
|
53
49
|
}
|
|
54
50
|
export declare function cli(opts: CliOptions): CliCommand;
|
|
55
51
|
export declare function getRegistry(): Map<string, CliCommand>;
|
package/dist/registry.js
CHANGED
package/dist/runtime.d.ts
CHANGED
|
@@ -10,6 +10,4 @@ export declare function runWithTimeout<T>(promise: Promise<T>, opts: {
|
|
|
10
10
|
timeout: number;
|
|
11
11
|
label?: string;
|
|
12
12
|
}): Promise<T>;
|
|
13
|
-
export declare function browserSession<T>(BrowserFactory: new () => any, fn: (page: IPage) => Promise<T
|
|
14
|
-
forceExtension?: boolean;
|
|
15
|
-
}): Promise<T>;
|
|
13
|
+
export declare function browserSession<T>(BrowserFactory: new () => any, fn: (page: IPage) => Promise<T>): Promise<T>;
|
package/dist/runtime.js
CHANGED
|
@@ -15,10 +15,10 @@ export async function runWithTimeout(promise, opts) {
|
|
|
15
15
|
.catch((err) => { clearTimeout(timer); reject(err); });
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
|
-
export async function browserSession(BrowserFactory, fn
|
|
18
|
+
export async function browserSession(BrowserFactory, fn) {
|
|
19
19
|
const mcp = new BrowserFactory();
|
|
20
20
|
try {
|
|
21
|
-
const page = await mcp.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT
|
|
21
|
+
const page = await mcp.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT });
|
|
22
22
|
return await fn(page);
|
|
23
23
|
}
|
|
24
24
|
finally {
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jackwener/opencli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
7
|
"description": "Make any website your CLI. AI-powered.",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=18.0.0"
|
|
10
|
+
},
|
|
8
11
|
"type": "module",
|
|
9
12
|
"main": "dist/main.js",
|
|
10
13
|
"bin": {
|
package/src/browser.test.ts
CHANGED
package/src/browser.ts
CHANGED
|
@@ -1,96 +1,31 @@
|
|
|
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
|
|
|
6
6
|
import { spawn, execSync, type ChildProcess } from 'node:child_process';
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
8
|
-
import * as net from 'node:net';
|
|
9
8
|
import { fileURLToPath } from 'node:url';
|
|
10
9
|
import * as fs from 'node:fs';
|
|
11
10
|
import * as os from 'node:os';
|
|
12
11
|
import * as path from 'node:path';
|
|
13
12
|
import { formatSnapshot } from './snapshotFormatter.js';
|
|
14
13
|
|
|
15
|
-
/**
|
|
16
|
-
* Chrome 144+ auto-discovery: read DevToolsActivePort file to get CDP endpoint.
|
|
17
|
-
*
|
|
18
|
-
* Starting with Chrome 144, users can enable remote debugging from
|
|
19
|
-
* chrome://inspect#remote-debugging without any command-line flags.
|
|
20
|
-
* Chrome writes the active port and browser GUID to a DevToolsActivePort file
|
|
21
|
-
* in the user data directory, which we read to construct the WebSocket endpoint.
|
|
22
|
-
*
|
|
23
|
-
* Priority: OPENCLI_CDP_ENDPOINT env > DevToolsActivePort auto-discovery > --extension fallback
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
/** Quick TCP port probe to verify Chrome is actually listening */
|
|
27
|
-
function isPortReachable(port: number, host = '127.0.0.1', timeoutMs = 800): Promise<boolean> {
|
|
28
|
-
return new Promise(resolve => {
|
|
29
|
-
const sock = net.createConnection({ port, host });
|
|
30
|
-
sock.setTimeout(timeoutMs);
|
|
31
|
-
sock.on('connect', () => { sock.destroy(); resolve(true); });
|
|
32
|
-
sock.on('error', () => resolve(false));
|
|
33
|
-
sock.on('timeout', () => { sock.destroy(); resolve(false); });
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export async function discoverChromeEndpoint(): Promise<string | null> {
|
|
38
|
-
const candidates: string[] = [];
|
|
39
|
-
|
|
40
|
-
// User-specified Chrome data dir takes highest priority
|
|
41
|
-
if (process.env.CHROME_USER_DATA_DIR) {
|
|
42
|
-
candidates.push(path.join(process.env.CHROME_USER_DATA_DIR, 'DevToolsActivePort'));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Standard Chrome/Edge user data dirs per platform
|
|
46
|
-
if (process.platform === 'win32') {
|
|
47
|
-
const localAppData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local');
|
|
48
|
-
candidates.push(path.join(localAppData, 'Google', 'Chrome', 'User Data', 'DevToolsActivePort'));
|
|
49
|
-
candidates.push(path.join(localAppData, 'Microsoft', 'Edge', 'User Data', 'DevToolsActivePort'));
|
|
50
|
-
} else if (process.platform === 'darwin') {
|
|
51
|
-
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'DevToolsActivePort'));
|
|
52
|
-
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Microsoft Edge', 'DevToolsActivePort'));
|
|
53
|
-
} else {
|
|
54
|
-
candidates.push(path.join(os.homedir(), '.config', 'google-chrome', 'DevToolsActivePort'));
|
|
55
|
-
candidates.push(path.join(os.homedir(), '.config', 'chromium', 'DevToolsActivePort'));
|
|
56
|
-
candidates.push(path.join(os.homedir(), '.config', 'microsoft-edge', 'DevToolsActivePort'));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
for (const filePath of candidates) {
|
|
60
|
-
try {
|
|
61
|
-
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
|
62
|
-
const lines = content.split('\n');
|
|
63
|
-
if (lines.length >= 2) {
|
|
64
|
-
const port = parseInt(lines[0], 10);
|
|
65
|
-
const browserPath = lines[1]; // e.g. /devtools/browser/<GUID>
|
|
66
|
-
if (port > 0 && browserPath.startsWith('/devtools/browser/')) {
|
|
67
|
-
const endpoint = `ws://127.0.0.1:${port}${browserPath}`;
|
|
68
|
-
// Verify the port is actually reachable (Chrome may have closed, leaving a stale file)
|
|
69
|
-
if (await isPortReachable(port)) {
|
|
70
|
-
return endpoint;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
} catch {}
|
|
75
|
-
}
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
14
|
// Read version from package.json (single source of truth)
|
|
80
15
|
const __browser_dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
81
16
|
const PKG_VERSION = (() => { try { return JSON.parse(fs.readFileSync(path.resolve(__browser_dirname, '..', 'package.json'), 'utf-8')).version; } catch { return '0.0.0'; } })();
|
|
82
17
|
|
|
83
18
|
const CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
|
|
84
19
|
const STDERR_BUFFER_LIMIT = 16 * 1024;
|
|
20
|
+
const INITIAL_TABS_TIMEOUT_MS = 1500;
|
|
85
21
|
const TAB_CLEANUP_TIMEOUT_MS = 2000;
|
|
86
22
|
let _cachedMcpServerPath: string | null | undefined;
|
|
87
23
|
|
|
88
|
-
type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | '
|
|
24
|
+
type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'mcp-init' | 'process-exit' | 'unknown';
|
|
89
25
|
type PlaywrightMCPState = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed';
|
|
90
26
|
|
|
91
27
|
type ConnectFailureInput = {
|
|
92
28
|
kind: ConnectFailureKind;
|
|
93
|
-
mode: 'extension' | 'cdp';
|
|
94
29
|
timeout: number;
|
|
95
30
|
hasExtensionToken: boolean;
|
|
96
31
|
tokenFingerprint?: string | null;
|
|
@@ -109,41 +44,31 @@ export function formatBrowserConnectError(input: ConnectFailureInput): Error {
|
|
|
109
44
|
const suffix = stderr ? `\n\nMCP stderr:\n${stderr}` : '';
|
|
110
45
|
const tokenHint = input.tokenFingerprint ? ` Token fingerprint: ${input.tokenFingerprint}.` : '';
|
|
111
46
|
|
|
112
|
-
if (input.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (input.kind === 'extension-not-installed') {
|
|
123
|
-
return new Error(
|
|
124
|
-
'Failed to connect to Playwright MCP Bridge: the browser extension did not attach.\n\n' +
|
|
125
|
-
'Make sure Chrome is running and the "Playwright MCP Bridge" extension is installed and enabled. ' +
|
|
126
|
-
'If Chrome shows an approval dialog, click Allow.' +
|
|
127
|
-
suffix,
|
|
128
|
-
);
|
|
129
|
-
}
|
|
47
|
+
if (input.kind === 'missing-token') {
|
|
48
|
+
return new Error(
|
|
49
|
+
'Failed to connect to Playwright MCP Bridge: PLAYWRIGHT_MCP_EXTENSION_TOKEN is not set.\n\n' +
|
|
50
|
+
'Without this token, Chrome will show a manual approval dialog for every new MCP connection. ' +
|
|
51
|
+
'Copy the token from the Playwright MCP Bridge extension and set it in BOTH your shell environment and MCP client config.' +
|
|
52
|
+
suffix,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
130
55
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
suffix,
|
|
139
|
-
);
|
|
140
|
-
}
|
|
56
|
+
if (input.kind === 'extension-not-installed') {
|
|
57
|
+
return new Error(
|
|
58
|
+
'Failed to connect to Playwright MCP Bridge: the browser extension did not attach.\n\n' +
|
|
59
|
+
'Make sure Chrome is running and the "Playwright MCP Bridge" extension is installed and enabled. ' +
|
|
60
|
+
'If Chrome shows an approval dialog, click Allow.' +
|
|
61
|
+
suffix,
|
|
62
|
+
);
|
|
141
63
|
}
|
|
142
64
|
|
|
143
|
-
if (input.
|
|
65
|
+
if (input.kind === 'extension-timeout') {
|
|
66
|
+
const likelyCause = input.hasExtensionToken
|
|
67
|
+
? `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.`
|
|
68
|
+
: 'PLAYWRIGHT_MCP_EXTENSION_TOKEN is not configured, so the extension may be waiting for manual approval.';
|
|
144
69
|
return new Error(
|
|
145
|
-
`Timed out connecting to
|
|
146
|
-
|
|
70
|
+
`Timed out connecting to Playwright MCP Bridge (${input.timeout}s).\n\n` +
|
|
71
|
+
`${likelyCause} If a browser prompt is visible, click Allow.` +
|
|
147
72
|
suffix,
|
|
148
73
|
);
|
|
149
74
|
}
|
|
@@ -163,7 +88,6 @@ export function formatBrowserConnectError(input: ConnectFailureInput): Error {
|
|
|
163
88
|
}
|
|
164
89
|
|
|
165
90
|
function inferConnectFailureKind(args: {
|
|
166
|
-
mode: 'extension' | 'cdp';
|
|
167
91
|
hasExtensionToken: boolean;
|
|
168
92
|
stderr: string;
|
|
169
93
|
rawMessage?: string;
|
|
@@ -171,7 +95,7 @@ function inferConnectFailureKind(args: {
|
|
|
171
95
|
}): ConnectFailureKind {
|
|
172
96
|
const haystack = `${args.rawMessage ?? ''}\n${args.stderr}`.toLowerCase();
|
|
173
97
|
|
|
174
|
-
if (
|
|
98
|
+
if (!args.hasExtensionToken)
|
|
175
99
|
return 'missing-token';
|
|
176
100
|
if (haystack.includes('extension connection timeout') || haystack.includes('playwright mcp bridge'))
|
|
177
101
|
return 'extension-not-installed';
|
|
@@ -179,11 +103,7 @@ function inferConnectFailureKind(args: {
|
|
|
179
103
|
return 'mcp-init';
|
|
180
104
|
if (args.exited)
|
|
181
105
|
return 'process-exit';
|
|
182
|
-
|
|
183
|
-
return 'extension-timeout';
|
|
184
|
-
if (args.mode === 'cdp')
|
|
185
|
-
return 'cdp-timeout';
|
|
186
|
-
return 'unknown';
|
|
106
|
+
return 'extension-timeout';
|
|
187
107
|
}
|
|
188
108
|
|
|
189
109
|
// JSON-RPC helpers
|
|
@@ -467,7 +387,7 @@ export class PlaywrightMCP {
|
|
|
467
387
|
}
|
|
468
388
|
}
|
|
469
389
|
|
|
470
|
-
async connect(opts: { timeout?: number
|
|
390
|
+
async connect(opts: { timeout?: number } = {}): Promise<Page> {
|
|
471
391
|
if (this._state === 'connected' && this._page) return this._page;
|
|
472
392
|
if (this._state === 'connecting') throw new Error('Playwright MCP is already connecting');
|
|
473
393
|
if (this._state === 'closing') throw new Error('Playwright MCP is closing');
|
|
@@ -481,25 +401,9 @@ export class PlaywrightMCP {
|
|
|
481
401
|
this._state = 'connecting';
|
|
482
402
|
const timeout = opts.timeout ?? CONNECT_TIMEOUT;
|
|
483
403
|
|
|
484
|
-
// Connection priority:
|
|
485
|
-
// 1. OPENCLI_CDP_ENDPOINT env var → explicit CDP endpoint
|
|
486
|
-
// 2. OPENCLI_USE_CDP=1 → auto-discover via DevToolsActivePort
|
|
487
|
-
// 3. Default → --extension mode (Playwright MCP Bridge)
|
|
488
|
-
// Some anti-bot sites (e.g. BOSS Zhipin) detect CDP — use forceExtension to bypass.
|
|
489
|
-
const forceExt = opts.forceExtension || process.env.OPENCLI_FORCE_EXTENSION === '1';
|
|
490
|
-
let cdpEndpoint: string | null = null;
|
|
491
|
-
if (!forceExt) {
|
|
492
|
-
if (process.env.OPENCLI_CDP_ENDPOINT) {
|
|
493
|
-
cdpEndpoint = process.env.OPENCLI_CDP_ENDPOINT;
|
|
494
|
-
} else if (process.env.OPENCLI_USE_CDP === '1') {
|
|
495
|
-
cdpEndpoint = await discoverChromeEndpoint();
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
404
|
return new Promise<Page>((resolve, reject) => {
|
|
500
405
|
const isDebug = process.env.DEBUG?.includes('opencli:mcp');
|
|
501
406
|
const debugLog = (msg: string) => isDebug && console.error(`[opencli:mcp] ${msg}`);
|
|
502
|
-
const mode: 'extension' | 'cdp' = cdpEndpoint ? 'cdp' : 'extension';
|
|
503
407
|
const extensionToken = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
|
|
504
408
|
const tokenFingerprint = getTokenFingerprint(extensionToken);
|
|
505
409
|
let stderrBuffer = '';
|
|
@@ -513,7 +417,6 @@ export class PlaywrightMCP {
|
|
|
513
417
|
this._resetAfterFailedConnect();
|
|
514
418
|
reject(formatBrowserConnectError({
|
|
515
419
|
kind,
|
|
516
|
-
mode,
|
|
517
420
|
timeout,
|
|
518
421
|
hasExtensionToken: !!extensionToken,
|
|
519
422
|
tokenFingerprint,
|
|
@@ -534,23 +437,14 @@ export class PlaywrightMCP {
|
|
|
534
437
|
const timer = setTimeout(() => {
|
|
535
438
|
debugLog('Connection timed out');
|
|
536
439
|
settleError(inferConnectFailureKind({
|
|
537
|
-
mode,
|
|
538
440
|
hasExtensionToken: !!extensionToken,
|
|
539
441
|
stderr: stderrBuffer,
|
|
540
442
|
}));
|
|
541
443
|
}, timeout * 1000);
|
|
542
444
|
|
|
543
|
-
const mcpArgs: string[] = [mcpPath];
|
|
544
|
-
if (cdpEndpoint) {
|
|
545
|
-
mcpArgs.push('--cdp-endpoint', cdpEndpoint);
|
|
546
|
-
} else {
|
|
547
|
-
mcpArgs.push('--extension');
|
|
548
|
-
}
|
|
445
|
+
const mcpArgs: string[] = [mcpPath, '--extension'];
|
|
549
446
|
if (process.env.OPENCLI_VERBOSE) {
|
|
550
|
-
console.error(`[opencli]
|
|
551
|
-
if (mode === 'extension') {
|
|
552
|
-
console.error(`[opencli] Extension token: ${extensionToken ? `configured (fingerprint ${tokenFingerprint})` : 'missing'}`);
|
|
553
|
-
}
|
|
447
|
+
console.error(`[opencli] Extension token: ${extensionToken ? `configured (fingerprint ${tokenFingerprint})` : 'missing'}`);
|
|
554
448
|
}
|
|
555
449
|
if (process.env.OPENCLI_BROWSER_EXECUTABLE_PATH) {
|
|
556
450
|
mcpArgs.push('--executablePath', process.env.OPENCLI_BROWSER_EXECUTABLE_PATH);
|
|
@@ -606,7 +500,6 @@ export class PlaywrightMCP {
|
|
|
606
500
|
this._rejectPendingRequests(new Error(`Playwright MCP process exited before response${code == null ? '' : ` (code ${code})`}`));
|
|
607
501
|
if (!settled) {
|
|
608
502
|
settleError(inferConnectFailureKind({
|
|
609
|
-
mode,
|
|
610
503
|
hasExtensionToken: !!extensionToken,
|
|
611
504
|
stderr: stderrBuffer,
|
|
612
505
|
exited: true,
|
|
@@ -624,7 +517,6 @@ export class PlaywrightMCP {
|
|
|
624
517
|
debugLog('Got initialize response');
|
|
625
518
|
if (resp.error) {
|
|
626
519
|
settleError(inferConnectFailureKind({
|
|
627
|
-
mode,
|
|
628
520
|
hasExtensionToken: !!extensionToken,
|
|
629
521
|
stderr: stderrBuffer,
|
|
630
522
|
rawMessage: `MCP init failed: ${resp.error.message}`,
|
|
@@ -636,9 +528,9 @@ export class PlaywrightMCP {
|
|
|
636
528
|
debugLog(`SEND: ${initializedMsg.trim()}`);
|
|
637
529
|
this._proc?.stdin?.write(initializedMsg);
|
|
638
530
|
|
|
639
|
-
//
|
|
531
|
+
// Use tabs as a readiness probe and for tab cleanup bookkeeping.
|
|
640
532
|
debugLog('Fetching initial tabs count...');
|
|
641
|
-
withTimeout(page.tabs(),
|
|
533
|
+
withTimeout(page.tabs(), INITIAL_TABS_TIMEOUT_MS, 'Timed out fetching initial tabs').then((tabs: any) => {
|
|
642
534
|
debugLog(`Tabs response: ${typeof tabs === 'string' ? tabs : JSON.stringify(tabs)}`);
|
|
643
535
|
this._initialTabIdentities = extractTabIdentities(tabs);
|
|
644
536
|
settleSuccess(page);
|
|
@@ -653,13 +545,14 @@ export class PlaywrightMCP {
|
|
|
653
545
|
});
|
|
654
546
|
}
|
|
655
547
|
|
|
548
|
+
|
|
656
549
|
async close(): Promise<void> {
|
|
657
550
|
if (this._closingPromise) return this._closingPromise;
|
|
658
551
|
if (this._state === 'closed') return;
|
|
659
552
|
this._state = 'closing';
|
|
660
553
|
this._closingPromise = (async () => {
|
|
661
554
|
try {
|
|
662
|
-
//
|
|
555
|
+
// Extension mode opens bridge/session tabs that we can clean up best-effort.
|
|
663
556
|
if (this._page && this._proc && !this._proc.killed) {
|
|
664
557
|
try {
|
|
665
558
|
const tabs = await withTimeout(this._page.tabs(), TAB_CLEANUP_TIMEOUT_MS, 'Timed out fetching tabs during cleanup');
|
package/src/clis/boss/search.ts
CHANGED
|
@@ -69,7 +69,7 @@ cli({
|
|
|
69
69
|
description: 'BOSS直聘搜索职位',
|
|
70
70
|
domain: 'www.zhipin.com',
|
|
71
71
|
strategy: Strategy.COOKIE,
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
browser: true,
|
|
74
74
|
args: [
|
|
75
75
|
{ name: 'query', required: true, help: 'Search keyword (e.g. AI agent, 前端)' },
|
package/src/clis/v2ex/daily.ts
CHANGED
package/src/clis/v2ex/me.ts
CHANGED
package/src/doctor.test.ts
CHANGED
|
@@ -92,18 +92,12 @@ describe('doctor report rendering', () => {
|
|
|
92
92
|
envFingerprint: 'fp1',
|
|
93
93
|
shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'abc123', fingerprint: 'fp1' }],
|
|
94
94
|
configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }],
|
|
95
|
-
remoteDebuggingEnabled: true,
|
|
96
|
-
remoteDebuggingEndpoint: 'ws://127.0.0.1:9222/devtools/browser/test',
|
|
97
|
-
cdpEnabled: false,
|
|
98
|
-
cdpToken: null,
|
|
99
|
-
cdpFingerprint: null,
|
|
100
95
|
recommendedToken: 'abc123',
|
|
101
96
|
recommendedFingerprint: 'fp1',
|
|
102
97
|
warnings: [],
|
|
103
98
|
issues: [],
|
|
104
99
|
});
|
|
105
100
|
|
|
106
|
-
expect(text).toContain('[OK] Chrome remote debugging: enabled');
|
|
107
101
|
expect(text).toContain('[OK] Environment token: configured (fp1)');
|
|
108
102
|
expect(text).toContain('[OK] MCP config /tmp/mcp.json: configured (fp1)');
|
|
109
103
|
});
|
|
@@ -114,18 +108,12 @@ describe('doctor report rendering', () => {
|
|
|
114
108
|
envFingerprint: 'fp1',
|
|
115
109
|
shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'def456', fingerprint: 'fp2' }],
|
|
116
110
|
configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }],
|
|
117
|
-
remoteDebuggingEnabled: false,
|
|
118
|
-
remoteDebuggingEndpoint: null,
|
|
119
|
-
cdpEnabled: false,
|
|
120
|
-
cdpToken: null,
|
|
121
|
-
cdpFingerprint: null,
|
|
122
111
|
recommendedToken: 'abc123',
|
|
123
112
|
recommendedFingerprint: 'fp1',
|
|
124
|
-
warnings: [
|
|
113
|
+
warnings: [],
|
|
125
114
|
issues: ['Detected inconsistent Playwright MCP tokens across env/config files.'],
|
|
126
115
|
});
|
|
127
116
|
|
|
128
|
-
expect(text).toContain('[WARN] Chrome remote debugging: disabled');
|
|
129
117
|
expect(text).toContain('[MISMATCH] Environment token: configured (fp1)');
|
|
130
118
|
expect(text).toContain('[MISMATCH] Shell file /tmp/.zshrc: configured (fp2)');
|
|
131
119
|
expect(text).toContain('[MISMATCH] Recommended token fingerprint: fp1');
|
package/src/doctor.ts
CHANGED
|
@@ -4,7 +4,7 @@ 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
6
|
import type { IPage } from './types.js';
|
|
7
|
-
import { PlaywrightMCP,
|
|
7
|
+
import { PlaywrightMCP, getTokenFingerprint } from './browser.js';
|
|
8
8
|
import { browserSession } from './runtime.js';
|
|
9
9
|
|
|
10
10
|
const PLAYWRIGHT_SERVER_NAME = 'playwright';
|
|
@@ -45,11 +45,6 @@ export type DoctorReport = {
|
|
|
45
45
|
envFingerprint: string | null;
|
|
46
46
|
shellFiles: ShellFileStatus[];
|
|
47
47
|
configs: McpConfigStatus[];
|
|
48
|
-
remoteDebuggingEnabled: boolean;
|
|
49
|
-
remoteDebuggingEndpoint: string | null;
|
|
50
|
-
cdpEnabled: boolean;
|
|
51
|
-
cdpToken: string | null;
|
|
52
|
-
cdpFingerprint: string | null;
|
|
53
48
|
recommendedToken: string | null;
|
|
54
49
|
recommendedFingerprint: string | null;
|
|
55
50
|
warnings: string[];
|
|
@@ -225,45 +220,10 @@ function readConfigStatus(filePath: string): McpConfigStatus {
|
|
|
225
220
|
}
|
|
226
221
|
}
|
|
227
222
|
|
|
228
|
-
|
|
229
|
-
if (!(process.env.OPENCLI_USE_CDP === '1' || process.env.OPENCLI_CDP_ENDPOINT))
|
|
230
|
-
return null;
|
|
231
|
-
const candidates = [
|
|
232
|
-
`chrome-extension://${PLAYWRIGHT_EXTENSION_ID}/options.html`,
|
|
233
|
-
`chrome-extension://${PLAYWRIGHT_EXTENSION_ID}/popup.html`,
|
|
234
|
-
`chrome-extension://${PLAYWRIGHT_EXTENSION_ID}/connect.html`,
|
|
235
|
-
`chrome-extension://${PLAYWRIGHT_EXTENSION_ID}/index.html`,
|
|
236
|
-
];
|
|
237
|
-
const result = await browserSession(PlaywrightMCP, async (page: IPage) => {
|
|
238
|
-
for (const url of candidates) {
|
|
239
|
-
try {
|
|
240
|
-
await page.goto(url);
|
|
241
|
-
await page.wait(1);
|
|
242
|
-
const token = await page.evaluate(`() => {
|
|
243
|
-
const values = new Set();
|
|
244
|
-
const push = (value) => {
|
|
245
|
-
if (!value || typeof value !== 'string') return;
|
|
246
|
-
for (const match of value.matchAll(/[A-Za-z0-9_-]{24,}/g)) values.add(match[0]);
|
|
247
|
-
};
|
|
248
|
-
document.querySelectorAll('input, textarea, code, pre, span, div').forEach((el) => {
|
|
249
|
-
push(el.value);
|
|
250
|
-
push(el.textContent || '');
|
|
251
|
-
push(el.getAttribute && el.getAttribute('value'));
|
|
252
|
-
});
|
|
253
|
-
return Array.from(values);
|
|
254
|
-
}`);
|
|
255
|
-
const matches = Array.isArray(token) ? token.filter((v: string) => v.length >= 24) : [];
|
|
256
|
-
if (matches.length > 0) return matches.sort((a: string, b: string) => b.length - a.length)[0];
|
|
257
|
-
} catch {}
|
|
258
|
-
}
|
|
259
|
-
return null;
|
|
260
|
-
});
|
|
261
|
-
return typeof result === 'string' && result ? result : null;
|
|
262
|
-
}
|
|
223
|
+
|
|
263
224
|
|
|
264
225
|
export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<DoctorReport> {
|
|
265
226
|
const envToken = process.env[PLAYWRIGHT_TOKEN_ENV] ?? null;
|
|
266
|
-
const remoteDebuggingEndpoint = await discoverChromeEndpoint().catch(() => null);
|
|
267
227
|
const shellPath = opts.shellRc ?? getDefaultShellRcPath();
|
|
268
228
|
const shellFiles: ShellFileStatus[] = [shellPath].map((filePath) => {
|
|
269
229
|
if (!fileExists(filePath)) return { path: filePath, exists: false, token: null, fingerprint: null };
|
|
@@ -273,17 +233,15 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
|
|
|
273
233
|
});
|
|
274
234
|
const configPaths = opts.configPaths?.length ? opts.configPaths : getDefaultMcpConfigPaths();
|
|
275
235
|
const configs = configPaths.map(readConfigStatus);
|
|
276
|
-
const cdpToken = !opts.token && !envToken ? await extractTokenViaCdp().catch(() => null) : null;
|
|
277
236
|
|
|
278
237
|
const allTokens = [
|
|
279
238
|
opts.token ?? null,
|
|
280
239
|
envToken,
|
|
281
240
|
...shellFiles.map(s => s.token),
|
|
282
241
|
...configs.map(c => c.token),
|
|
283
|
-
cdpToken,
|
|
284
242
|
].filter((v): v is string => !!v);
|
|
285
243
|
const uniqueTokens = [...new Set(allTokens)];
|
|
286
|
-
const recommendedToken = opts.token ?? envToken ?? (uniqueTokens.length === 1 ? uniqueTokens[0] :
|
|
244
|
+
const recommendedToken = opts.token ?? envToken ?? (uniqueTokens.length === 1 ? uniqueTokens[0] : null) ?? null;
|
|
287
245
|
|
|
288
246
|
const report: DoctorReport = {
|
|
289
247
|
cliVersion: opts.cliVersion,
|
|
@@ -291,11 +249,6 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
|
|
|
291
249
|
envFingerprint: getTokenFingerprint(envToken ?? undefined),
|
|
292
250
|
shellFiles,
|
|
293
251
|
configs,
|
|
294
|
-
remoteDebuggingEnabled: !!remoteDebuggingEndpoint,
|
|
295
|
-
remoteDebuggingEndpoint,
|
|
296
|
-
cdpEnabled: process.env.OPENCLI_USE_CDP === '1' || !!process.env.OPENCLI_CDP_ENDPOINT,
|
|
297
|
-
cdpToken,
|
|
298
|
-
cdpFingerprint: getTokenFingerprint(cdpToken ?? undefined),
|
|
299
252
|
recommendedToken,
|
|
300
253
|
recommendedFingerprint: getTokenFingerprint(recommendedToken ?? undefined),
|
|
301
254
|
warnings: [],
|
|
@@ -306,13 +259,11 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
|
|
|
306
259
|
if (!shellFiles.some(s => s.token)) report.issues.push('Shell startup file does not export PLAYWRIGHT_MCP_EXTENSION_TOKEN.');
|
|
307
260
|
if (!configs.some(c => c.token)) report.issues.push('No scanned MCP config currently contains a Playwright extension token.');
|
|
308
261
|
if (uniqueTokens.length > 1) report.issues.push('Detected inconsistent Playwright MCP tokens across env/config files.');
|
|
309
|
-
if (!report.remoteDebuggingEnabled) report.warnings.push('Chrome remote debugging appears to be disabled or Chrome is not currently exposing a DevTools endpoint.');
|
|
310
262
|
for (const config of configs) {
|
|
311
263
|
if (config.parseError) report.warnings.push(`Could not parse ${config.path}: ${config.parseError}`);
|
|
312
264
|
}
|
|
313
265
|
if (!recommendedToken) {
|
|
314
|
-
|
|
315
|
-
else report.warnings.push('No token source found. Enable OPENCLI_USE_CDP=1 to allow a best-effort token read from the extension page.');
|
|
266
|
+
report.warnings.push('No token source found.');
|
|
316
267
|
}
|
|
317
268
|
return report;
|
|
318
269
|
}
|
|
@@ -326,8 +277,6 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
|
|
|
326
277
|
const uniqueFingerprints = [...new Set(tokenFingerprints)];
|
|
327
278
|
const hasMismatch = uniqueFingerprints.length > 1;
|
|
328
279
|
const lines = [`opencli v${report.cliVersion ?? 'unknown'} doctor`, ''];
|
|
329
|
-
lines.push(statusLine(report.remoteDebuggingEnabled ? 'OK' : 'WARN', `Chrome remote debugging: ${report.remoteDebuggingEnabled ? 'enabled' : 'disabled'}`));
|
|
330
|
-
if (report.remoteDebuggingEndpoint) lines.push(` ${report.remoteDebuggingEndpoint}`);
|
|
331
280
|
|
|
332
281
|
const envStatus: ReportStatus = !report.envToken ? 'MISSING' : hasMismatch ? 'MISMATCH' : 'OK';
|
|
333
282
|
lines.push(statusLine(envStatus, `Environment token: ${tokenSummary(report.envToken, report.envFingerprint)}`));
|
|
@@ -354,10 +303,6 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
|
|
|
354
303
|
lines.push(statusLine('MISSING', 'MCP config: no existing config files found in scanned locations'));
|
|
355
304
|
}
|
|
356
305
|
if (missingConfigCount > 0) lines.push(` Other scanned config locations not present: ${missingConfigCount}`);
|
|
357
|
-
if (report.cdpEnabled) {
|
|
358
|
-
const cdpStatus: ReportStatus = report.cdpToken ? 'OK' : 'WARN';
|
|
359
|
-
lines.push(statusLine(cdpStatus, `CDP token probe: ${tokenSummary(report.cdpToken, report.cdpFingerprint)}`));
|
|
360
|
-
}
|
|
361
306
|
lines.push('');
|
|
362
307
|
lines.push(statusLine(
|
|
363
308
|
hasMismatch ? 'MISMATCH' : report.recommendedToken ? 'OK' : 'WARN',
|
|
@@ -391,7 +336,7 @@ function writeFileWithMkdir(filePath: string, content: string): void {
|
|
|
391
336
|
|
|
392
337
|
export async function applyBrowserDoctorFix(report: DoctorReport, opts: DoctorOptions = {}): Promise<string[]> {
|
|
393
338
|
const token = opts.token ?? report.recommendedToken;
|
|
394
|
-
if (!token) throw new Error('No Playwright MCP token is available to write. Provide --token
|
|
339
|
+
if (!token) throw new Error('No Playwright MCP token is available to write. Provide --token first.');
|
|
395
340
|
|
|
396
341
|
const plannedWrites: string[] = [];
|
|
397
342
|
const shellPath = opts.shellRc ?? report.shellFiles[0]?.path ?? getDefaultShellRcPath();
|
package/src/main.ts
CHANGED
|
@@ -144,7 +144,7 @@ for (const [, cmd] of registry) {
|
|
|
144
144
|
if (actionOpts.verbose) process.env.OPENCLI_VERBOSE = '1';
|
|
145
145
|
let result: any;
|
|
146
146
|
if (cmd.browser) {
|
|
147
|
-
result = await browserSession(PlaywrightMCP, async (page) => runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) })
|
|
147
|
+
result = await browserSession(PlaywrightMCP, async (page) => runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) }));
|
|
148
148
|
} else { result = await executeCommand(cmd, null, kwargs, actionOpts.verbose); }
|
|
149
149
|
if (actionOpts.verbose && (!result || (Array.isArray(result) && result.length === 0))) {
|
|
150
150
|
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.`));
|
package/src/registry.ts
CHANGED
|
@@ -37,10 +37,7 @@ export interface CliCommand {
|
|
|
37
37
|
/** Internal: lazy-loaded TS module support */
|
|
38
38
|
_lazy?: boolean;
|
|
39
39
|
_modulePath?: string;
|
|
40
|
-
/** Force extension bridge mode (bypass CDP), for anti-bot sites */
|
|
41
|
-
forceExtension?: boolean;
|
|
42
40
|
}
|
|
43
|
-
|
|
44
41
|
export interface CliOptions {
|
|
45
42
|
site: string;
|
|
46
43
|
name: string;
|
|
@@ -53,10 +50,7 @@ export interface CliOptions {
|
|
|
53
50
|
func?: (page: IPage | null, kwargs: Record<string, any>, debug?: boolean) => Promise<any>;
|
|
54
51
|
pipeline?: any[];
|
|
55
52
|
timeoutSeconds?: number;
|
|
56
|
-
/** Force extension bridge mode (bypass CDP), for anti-bot sites */
|
|
57
|
-
forceExtension?: boolean;
|
|
58
53
|
}
|
|
59
|
-
|
|
60
54
|
const _registry = new Map<string, CliCommand>();
|
|
61
55
|
|
|
62
56
|
export function cli(opts: CliOptions): CliCommand {
|
|
@@ -72,7 +66,6 @@ export function cli(opts: CliOptions): CliCommand {
|
|
|
72
66
|
func: opts.func,
|
|
73
67
|
pipeline: opts.pipeline,
|
|
74
68
|
timeoutSeconds: opts.timeoutSeconds,
|
|
75
|
-
forceExtension: opts.forceExtension,
|
|
76
69
|
};
|
|
77
70
|
|
|
78
71
|
const key = fullName(cmd);
|
package/src/runtime.ts
CHANGED
|
@@ -27,11 +27,10 @@ export async function runWithTimeout<T>(
|
|
|
27
27
|
export async function browserSession<T>(
|
|
28
28
|
BrowserFactory: new () => any,
|
|
29
29
|
fn: (page: IPage) => Promise<T>,
|
|
30
|
-
opts?: { forceExtension?: boolean },
|
|
31
30
|
): Promise<T> {
|
|
32
31
|
const mcp = new BrowserFactory();
|
|
33
32
|
try {
|
|
34
|
-
const page = await mcp.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT
|
|
33
|
+
const page = await mcp.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT });
|
|
35
34
|
return await fn(page);
|
|
36
35
|
} finally {
|
|
37
36
|
await mcp.close().catch(() => {});
|