@jackwener/opencli 0.4.3 → 0.4.5
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/{CLI-CREATOR.md → CLI-EXPLORER.md} +5 -1
- package/CLI-ONESHOT.md +216 -0
- package/README.md +4 -3
- package/README.zh-CN.md +4 -3
- package/SKILL.md +6 -4
- package/dist/browser.d.ts +32 -8
- package/dist/browser.js +235 -109
- package/dist/browser.test.js +51 -38
- package/package.json +1 -1
- package/src/browser.test.ts +69 -43
- package/src/browser.ts +234 -88
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
# CLI-
|
|
1
|
+
# CLI-EXPLORER — 适配器探索式开发完全指南
|
|
2
2
|
|
|
3
3
|
> 本文档教你(或 AI Agent)如何为 OpenCLI 添加一个新网站的命令。
|
|
4
4
|
> 从零到发布,覆盖 API 发现、方案选择、适配器编写、测试验证全流程。
|
|
5
5
|
|
|
6
|
+
> [!TIP]
|
|
7
|
+
> **只想为一个具体页面快速生成一个命令?** 看 [CLI-ONESHOT.md](./CLI-ONESHOT.md)(~150 行,4 步搞定)。
|
|
8
|
+
> 本文档适合从零探索一个新站点的完整流程。
|
|
9
|
+
|
|
6
10
|
---
|
|
7
11
|
|
|
8
12
|
## AI Agent 开发者必读:用 Playwright MCP Bridge 探索
|
package/CLI-ONESHOT.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# CLI-ONESHOT — 单点快速 CLI 生成
|
|
2
|
+
|
|
3
|
+
> 给一个 URL + 一句话描述,4 步生成一个 CLI 命令。
|
|
4
|
+
> 完整探索式开发请看 [CLI-EXPLORER.md](./CLI-EXPLORER.md)。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 输入
|
|
9
|
+
|
|
10
|
+
| 项目 | 示例 |
|
|
11
|
+
|------|------|
|
|
12
|
+
| **URL** | `https://x.com/jakevin7/lists` |
|
|
13
|
+
| **Goal** | 获取我的 Twitter Lists |
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 流程
|
|
18
|
+
|
|
19
|
+
### Step 1: 打开页面 + 抓包
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
1. browser_navigate → 打开目标 URL
|
|
23
|
+
2. 等待 3-5 秒(让页面加载完、API 请求触发)
|
|
24
|
+
3. browser_network_requests → 筛选 JSON API
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**关键**:只关注返回 `application/json` 的请求,忽略静态资源。
|
|
28
|
+
如果没有自动触发 API,手动点击目标按钮/标签再抓一次。
|
|
29
|
+
|
|
30
|
+
### Step 2: 锁定一个接口
|
|
31
|
+
|
|
32
|
+
从抓包结果中找到**那个**目标 API。看这几个字段:
|
|
33
|
+
|
|
34
|
+
| 字段 | 关注什么 |
|
|
35
|
+
|------|----------|
|
|
36
|
+
| URL | API 路径 pattern(如 `/i/api/graphql/xxx/ListsManagePinTimeline`) |
|
|
37
|
+
| Method | GET / POST |
|
|
38
|
+
| Headers | 有 Cookie? Bearer? CSRF? 自定义签名? |
|
|
39
|
+
| Response | 数据在哪个路径(如 `data.list.lists`) |
|
|
40
|
+
|
|
41
|
+
### Step 3: 验证接口能复现
|
|
42
|
+
|
|
43
|
+
在 `browser_evaluate` 中用 `fetch` 复现请求:
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
// Tier 2 (Cookie): 大多数情况
|
|
47
|
+
fetch('/api/endpoint', { credentials: 'include' }).then(r => r.json())
|
|
48
|
+
|
|
49
|
+
// Tier 3 (Header): 如 Twitter 需要额外 header
|
|
50
|
+
const ct0 = document.cookie.match(/ct0=([^;]+)/)?.[1];
|
|
51
|
+
fetch('/api/endpoint', {
|
|
52
|
+
headers: { 'Authorization': 'Bearer ...', 'X-Csrf-Token': ct0 },
|
|
53
|
+
credentials: 'include'
|
|
54
|
+
}).then(r => r.json())
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
如果 fetch 能拿到数据 → 用 YAML 或简单 TS adapter。
|
|
58
|
+
如果 fetch 拿不到(签名/风控)→ 用 intercept 策略。
|
|
59
|
+
|
|
60
|
+
### Step 4: 套模板,生成 adapter
|
|
61
|
+
|
|
62
|
+
根据 Step 3 判定的策略,选一个模板生成文件。
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 认证速查
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
fetch(url) 直接能拿到? → Tier 1: public (YAML, browser: false)
|
|
70
|
+
fetch(url, {credentials:'include'})? → Tier 2: cookie (YAML)
|
|
71
|
+
加 Bearer/CSRF header 后拿到? → Tier 3: header (TS)
|
|
72
|
+
都不行,但页面自己能请求成功? → Tier 4: intercept (TS, installInterceptor)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 模板
|
|
78
|
+
|
|
79
|
+
### YAML — Cookie/Public(最简)
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
# src/clis/<site>/<name>.yaml
|
|
83
|
+
site: mysite
|
|
84
|
+
name: mycommand
|
|
85
|
+
description: "一句话描述"
|
|
86
|
+
domain: www.example.com
|
|
87
|
+
strategy: cookie # 或 public (加 browser: false)
|
|
88
|
+
|
|
89
|
+
args:
|
|
90
|
+
limit:
|
|
91
|
+
type: int
|
|
92
|
+
default: 20
|
|
93
|
+
|
|
94
|
+
pipeline:
|
|
95
|
+
- navigate: https://www.example.com/target-page
|
|
96
|
+
|
|
97
|
+
- evaluate: |
|
|
98
|
+
(async () => {
|
|
99
|
+
const res = await fetch('/api/target', { credentials: 'include' });
|
|
100
|
+
const d = await res.json();
|
|
101
|
+
return (d.data?.items || []).map(item => ({
|
|
102
|
+
title: item.title,
|
|
103
|
+
value: item.value,
|
|
104
|
+
}));
|
|
105
|
+
})()
|
|
106
|
+
|
|
107
|
+
- map:
|
|
108
|
+
rank: ${{ index + 1 }}
|
|
109
|
+
title: ${{ item.title }}
|
|
110
|
+
value: ${{ item.value }}
|
|
111
|
+
|
|
112
|
+
- limit: ${{ args.limit }}
|
|
113
|
+
|
|
114
|
+
columns: [rank, title, value]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### TS — Intercept(抓包模式)
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// src/clis/<site>/<name>.ts
|
|
121
|
+
import { cli, Strategy } from '../../registry.js';
|
|
122
|
+
|
|
123
|
+
cli({
|
|
124
|
+
site: 'mysite',
|
|
125
|
+
name: 'mycommand',
|
|
126
|
+
description: '一句话描述',
|
|
127
|
+
domain: 'www.example.com',
|
|
128
|
+
strategy: Strategy.INTERCEPT,
|
|
129
|
+
browser: true,
|
|
130
|
+
args: [
|
|
131
|
+
{ name: 'limit', type: 'int', default: 20 },
|
|
132
|
+
],
|
|
133
|
+
columns: ['rank', 'title', 'value'],
|
|
134
|
+
func: async (page, kwargs) => {
|
|
135
|
+
// 1. 导航
|
|
136
|
+
await page.goto('https://www.example.com/target-page');
|
|
137
|
+
await page.wait(3);
|
|
138
|
+
|
|
139
|
+
// 2. 注入拦截器(URL 子串匹配)
|
|
140
|
+
await page.installInterceptor('target-api-keyword');
|
|
141
|
+
|
|
142
|
+
// 3. 触发 API(滚动/点击)
|
|
143
|
+
await page.autoScroll({ times: 2, delayMs: 2000 });
|
|
144
|
+
|
|
145
|
+
// 4. 读取拦截的响应
|
|
146
|
+
const requests = await page.getInterceptedRequests();
|
|
147
|
+
if (!requests?.length) return [];
|
|
148
|
+
|
|
149
|
+
let results: any[] = [];
|
|
150
|
+
for (const req of requests) {
|
|
151
|
+
const items = req.data?.data?.items || [];
|
|
152
|
+
results.push(...items);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return results.slice(0, kwargs.limit).map((item, i) => ({
|
|
156
|
+
rank: i + 1,
|
|
157
|
+
title: item.title || '',
|
|
158
|
+
value: item.value || '',
|
|
159
|
+
}));
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### TS — Header(如 Twitter GraphQL)
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { cli, Strategy } from '../../registry.js';
|
|
168
|
+
|
|
169
|
+
cli({
|
|
170
|
+
site: 'twitter',
|
|
171
|
+
name: 'mycommand',
|
|
172
|
+
description: '一句话描述',
|
|
173
|
+
domain: 'x.com',
|
|
174
|
+
strategy: Strategy.HEADER,
|
|
175
|
+
browser: true,
|
|
176
|
+
args: [
|
|
177
|
+
{ name: 'limit', type: 'int', default: 20 },
|
|
178
|
+
],
|
|
179
|
+
columns: ['rank', 'name', 'value'],
|
|
180
|
+
func: async (page, kwargs) => {
|
|
181
|
+
await page.goto('https://x.com');
|
|
182
|
+
const data = await page.evaluate(`(async () => {
|
|
183
|
+
const ct0 = document.cookie.match(/ct0=([^;]+)/)?.[1];
|
|
184
|
+
if (!ct0) return { error: 'Not logged in' };
|
|
185
|
+
const bearer = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D...';
|
|
186
|
+
const res = await fetch('/i/api/graphql/QUERY_ID/Endpoint', {
|
|
187
|
+
headers: {
|
|
188
|
+
'Authorization': 'Bearer ' + decodeURIComponent(bearer),
|
|
189
|
+
'X-Csrf-Token': ct0,
|
|
190
|
+
'X-Twitter-Auth-Type': 'OAuth2Session',
|
|
191
|
+
},
|
|
192
|
+
credentials: 'include',
|
|
193
|
+
});
|
|
194
|
+
return res.json();
|
|
195
|
+
})()`);
|
|
196
|
+
// 解析 data...
|
|
197
|
+
return [];
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## 测试(必做)
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
npm run build # 语法检查
|
|
208
|
+
opencli list | grep mysite # 确认注册
|
|
209
|
+
opencli mysite mycommand --limit 3 -v # 实际运行
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 就这样,没了
|
|
215
|
+
|
|
216
|
+
写完文件 → build → run → 提交。有问题再看 [CLI-EXPLORER.md](./CLI-EXPLORER.md)。
|
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ OpenCLI needs a way to communicate with your browser. We highly recommend config
|
|
|
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.
|
|
50
50
|
|
|
51
|
-
**You must configure this token in BOTH your MCP configuration
|
|
51
|
+
**You must configure this token in BOTH your MCP configuration AND system environment variables.**
|
|
52
52
|
|
|
53
53
|
First, add it to your MCP client config (e.g. Claude/Cursor):
|
|
54
54
|
|
|
@@ -159,8 +159,9 @@ opencli bilibili hot -v # Verbose: show pipeline debug steps
|
|
|
159
159
|
|
|
160
160
|
If you are an AI assistant tasked with creating a new command adapter for `opencli`, please follow the AI Agent workflow below:
|
|
161
161
|
|
|
162
|
-
> **
|
|
163
|
-
|
|
162
|
+
> **Quick mode**: To generate a single command for a specific page URL, see [CLI-ONESHOT.md](./CLI-ONESHOT.md) — just a URL + one-line goal, 4 steps done.
|
|
163
|
+
|
|
164
|
+
> **Full mode**: Before writing any adapter code, read [CLI-EXPLORER.md](./CLI-EXPLORER.md). It contains the complete browser exploration workflow, the 5-tier authentication strategy decision tree, and debugging guide.
|
|
164
165
|
|
|
165
166
|
```bash
|
|
166
167
|
# 1. Deep Explore — discover APIs, infer capabilities, detect framework
|
package/README.zh-CN.md
CHANGED
|
@@ -48,7 +48,7 @@ OpenCLI 通过 Chrome 浏览器 + [Playwright MCP Bridge](https://github.com/nic
|
|
|
48
48
|
1. 安装 **[Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm)** 扩展
|
|
49
49
|
2. 在浏览器插件栏点击该插件,或者在插件设置页获取你的 Extension Token。
|
|
50
50
|
|
|
51
|
-
**你必须将这个 Token 同时配置到你的 MCP
|
|
51
|
+
**你必须将这个 Token 同时配置到你的 MCP 配置文件 AND 环境变量中。**
|
|
52
52
|
|
|
53
53
|
首先,配置你的 MCP 客户端(如 Claude/Cursor 等):
|
|
54
54
|
|
|
@@ -159,8 +159,9 @@ opencli bilibili hot -v # 详细模式:展示管线执行步骤调试
|
|
|
159
159
|
|
|
160
160
|
如果你是一个被要求查阅代码并编写新 `opencli` 适配器的 AI,请遵守以下工作流。
|
|
161
161
|
|
|
162
|
-
>
|
|
163
|
-
|
|
162
|
+
> **快速模式**:只想为某个页面快速生成一个命令?看 [CLI-ONESHOT.md](./CLI-ONESHOT.md) — 给一个 URL + 一句话描述,4 步搞定。
|
|
163
|
+
|
|
164
|
+
> **完整模式**:在编写任何新代码前,先阅读 [CLI-EXPLORER.md](./CLI-EXPLORER.md)。它包含完整的适配器探索开发指南、API 探测流程、5级认证策略以及常见陷阱。
|
|
164
165
|
|
|
165
166
|
```bash
|
|
166
167
|
# 1. Deep Explore — 网络拦截 → 响应分析 → 能力推理 → 框架检测
|
package/SKILL.md
CHANGED
|
@@ -11,7 +11,7 @@ tags: [cli, browser, web, mcp, playwright, bilibili, zhihu, twitter, github, v2e
|
|
|
11
11
|
> Make any website your CLI. Reuse Chrome login, zero risk, AI-powered discovery.
|
|
12
12
|
|
|
13
13
|
> [!CAUTION]
|
|
14
|
-
> **AI Agent 必读:创建或修改任何适配器之前,你必须先阅读 [CLI-
|
|
14
|
+
> **AI Agent 必读:创建或修改任何适配器之前,你必须先阅读 [CLI-EXPLORER.md](./CLI-EXPLORER.md)!**
|
|
15
15
|
> 该文档包含完整的 API 发现工作流(必须使用 Playwright MCP Bridge 浏览器探索)、5 级认证策略决策树、平台 SDK 速查表、`tap` 步骤调试流程、分页 API 模板、级联请求模式、以及常见陷阱。
|
|
16
16
|
> **本文件(SKILL.md)仅提供命令参考和简化模板,不足以正确开发适配器。**
|
|
17
17
|
|
|
@@ -183,8 +183,12 @@ opencli bilibili hot -v # Show each pipeline step and data flow
|
|
|
183
183
|
|
|
184
184
|
## Creating Adapters
|
|
185
185
|
|
|
186
|
+
> [!TIP]
|
|
187
|
+
> **快速模式**:如果你只想为一个具体页面生成一个命令,直接看 [CLI-ONESHOT.md](./CLI-ONESHOT.md)。
|
|
188
|
+
> 只需要一个 URL + 一句话描述,4 步搞定。
|
|
189
|
+
|
|
186
190
|
> [!IMPORTANT]
|
|
187
|
-
>
|
|
191
|
+
> **完整模式 — 在写任何代码之前,先阅读 [CLI-EXPLORER.md](./CLI-EXPLORER.md)。**
|
|
188
192
|
> 它包含:① AI Agent 浏览器探索工作流(必须用 Playwright MCP 抓包验证 API)② 认证策略决策树 ③ 平台 SDK(如 Bilibili 的 `apiGet`/`fetchJson`)④ YAML vs TS 选择指南 ⑤ `tap` 步骤调试方法 ⑥ 级联请求模板 ⑦ 常见陷阱表。
|
|
189
193
|
> **下方仅为简化模板参考,直接使用极易踩坑。**
|
|
190
194
|
|
|
@@ -335,7 +339,6 @@ ${{ index + 1 }}
|
|
|
335
339
|
| `OPENCLI_BROWSER_CONNECT_TIMEOUT` | 30 | Browser connection timeout (sec) |
|
|
336
340
|
| `OPENCLI_BROWSER_COMMAND_TIMEOUT` | 45 | Command execution timeout (sec) |
|
|
337
341
|
| `OPENCLI_BROWSER_EXPLORE_TIMEOUT` | 120 | Explore timeout (sec) |
|
|
338
|
-
| `OPENCLI_EXTENSION_LOCK_TIMEOUT` | 120 | Extension lock timeout (sec) |
|
|
339
342
|
| `OPENCLI_CDP_ENDPOINT` | — | Manual CDP WebSocket endpoint (overrides auto-discovery) |
|
|
340
343
|
| `OPENCLI_USE_CDP` | — | Set to `1` to use Chrome 144+ CDP auto-discovery instead of extension |
|
|
341
344
|
| `OPENCLI_FORCE_EXTENSION` | — | Set to `1` to skip CDP and force extension mode |
|
|
@@ -347,6 +350,5 @@ ${{ index + 1 }}
|
|
|
347
350
|
|-------|----------|
|
|
348
351
|
| `npx not found` | Install Node.js: `brew install node` |
|
|
349
352
|
| `Timed out connecting to browser` | 1) Chrome must be open 2) Enable remote debugging at `chrome://inspect#remote-debugging` or install MCP Bridge extension |
|
|
350
|
-
| `Extension lock timed out` | Another opencli command is running; browser commands run serially |
|
|
351
353
|
| `Target page context` error | Add `navigate:` step before `evaluate:` in YAML |
|
|
352
354
|
| Empty table data | Check if evaluate returns JSON string (MCP parsing) or data path is wrong |
|
package/dist/browser.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export declare function discoverChromeEndpoint(): Promise<string | null>;
|
|
6
6
|
type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'cdp-timeout' | 'mcp-init' | 'process-exit' | 'unknown';
|
|
7
|
+
type PlaywrightMCPState = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed';
|
|
7
8
|
type ConnectFailureInput = {
|
|
8
9
|
kind: ConnectFailureKind;
|
|
9
10
|
mode: 'extension' | 'cdp';
|
|
@@ -16,14 +17,17 @@ type ConnectFailureInput = {
|
|
|
16
17
|
};
|
|
17
18
|
export declare function getTokenFingerprint(token: string | undefined): string | null;
|
|
18
19
|
export declare function formatBrowserConnectError(input: ConnectFailureInput): Error;
|
|
20
|
+
declare function createJsonRpcRequest(method: string, params?: Record<string, any>): {
|
|
21
|
+
id: number;
|
|
22
|
+
message: string;
|
|
23
|
+
};
|
|
19
24
|
import type { IPage } from './types.js';
|
|
20
25
|
/**
|
|
21
26
|
* Page abstraction wrapping JSON-RPC calls to Playwright MCP.
|
|
22
27
|
*/
|
|
23
28
|
export declare class Page implements IPage {
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
constructor(_send: (msg: string) => void, _recv: () => Promise<any>);
|
|
29
|
+
private _request;
|
|
30
|
+
constructor(_request: (method: string, params?: Record<string, any>) => Promise<any>);
|
|
27
31
|
call(method: string, params?: Record<string, any>): Promise<any>;
|
|
28
32
|
goto(url: string): Promise<void>;
|
|
29
33
|
evaluate(js: string): Promise<any>;
|
|
@@ -65,16 +69,36 @@ export declare class PlaywrightMCP {
|
|
|
65
69
|
private static _registerGlobalCleanup;
|
|
66
70
|
private _proc;
|
|
67
71
|
private _buffer;
|
|
68
|
-
private
|
|
69
|
-
private
|
|
70
|
-
private
|
|
72
|
+
private _pending;
|
|
73
|
+
private _initialTabIdentities;
|
|
74
|
+
private _closingPromise;
|
|
75
|
+
private _state;
|
|
71
76
|
private _page;
|
|
77
|
+
get state(): PlaywrightMCPState;
|
|
78
|
+
private _sendRequest;
|
|
79
|
+
private _rejectPendingRequests;
|
|
80
|
+
private _resetAfterFailedConnect;
|
|
72
81
|
connect(opts?: {
|
|
73
82
|
timeout?: number;
|
|
74
83
|
forceExtension?: boolean;
|
|
75
84
|
}): Promise<Page>;
|
|
76
85
|
close(): Promise<void>;
|
|
77
|
-
private _acquireLock;
|
|
78
|
-
private _releaseLock;
|
|
79
86
|
}
|
|
87
|
+
declare function extractTabEntries(raw: any): Array<{
|
|
88
|
+
index: number;
|
|
89
|
+
identity: string;
|
|
90
|
+
}>;
|
|
91
|
+
declare function diffTabIndexes(initialIdentities: string[], currentTabs: Array<{
|
|
92
|
+
index: number;
|
|
93
|
+
identity: string;
|
|
94
|
+
}>): number[];
|
|
95
|
+
declare function appendLimited(current: string, chunk: string, limit: number): string;
|
|
96
|
+
declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T>;
|
|
97
|
+
export declare const __test__: {
|
|
98
|
+
createJsonRpcRequest: typeof createJsonRpcRequest;
|
|
99
|
+
extractTabEntries: typeof extractTabEntries;
|
|
100
|
+
diffTabIndexes: typeof diffTabIndexes;
|
|
101
|
+
appendLimited: typeof appendLimited;
|
|
102
|
+
withTimeout: typeof withTimeout;
|
|
103
|
+
};
|
|
80
104
|
export {};
|