@jackwener/opencli 1.2.6 → 1.3.1
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/.github/workflows/release.yml +7 -0
- package/README.md +3 -64
- package/README.zh-CN.md +2 -48
- package/SKILL.md +1 -3
- package/TESTING.md +87 -69
- package/dist/browser/cdp.js +5 -4
- package/dist/browser/daemon-client.js +9 -3
- package/dist/browser/discover.js +3 -1
- package/dist/browser/dom-helpers.d.ts +8 -0
- package/dist/browser/dom-helpers.js +33 -0
- package/dist/browser/page.js +9 -5
- package/dist/cli.js +2 -9
- package/dist/daemon.d.ts +8 -0
- package/dist/daemon.js +53 -6
- package/dist/doctor.js +14 -2
- package/dist/doctor.test.js +15 -19
- package/docs/developer/testing.md +87 -69
- package/docs/guide/browser-bridge.md +0 -1
- package/docs/guide/getting-started.md +1 -1
- package/docs/guide/troubleshooting.md +1 -1
- package/docs/index.md +1 -1
- package/docs/zh/guide/browser-bridge.md +0 -1
- package/package.json +1 -1
- package/src/browser/cdp.ts +5 -3
- package/src/browser/daemon-client.ts +9 -3
- package/src/browser/discover.ts +3 -1
- package/src/browser/dom-helpers.ts +34 -0
- package/src/browser/page.ts +9 -4
- package/src/cli.ts +2 -10
- package/src/daemon.ts +57 -7
- package/src/doctor.test.ts +17 -25
- package/src/doctor.ts +14 -2
- package/.github/workflows/release-please.yml +0 -25
- package/dist/setup.d.ts +0 -10
- package/dist/setup.js +0 -66
- package/src/setup.ts +0 -69
|
@@ -35,3 +35,10 @@ jobs:
|
|
|
35
35
|
run: npm publish --provenance --access public
|
|
36
36
|
env:
|
|
37
37
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
38
|
+
|
|
39
|
+
- name: Trigger website rebuild
|
|
40
|
+
uses: peter-evans/repository-dispatch@v3
|
|
41
|
+
with:
|
|
42
|
+
token: ${{ secrets.WEBSITE_DEPLOY_TOKEN }}
|
|
43
|
+
repository: jackwener/opencli-website
|
|
44
|
+
event-type: version-released
|
package/README.md
CHANGED
|
@@ -18,32 +18,13 @@ Turn ANY Electron application into a CLI tool! Recombine, script, and extend app
|
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
-
## Table of Contents
|
|
22
|
-
|
|
23
|
-
- [Highlights](#highlights)
|
|
24
|
-
- [Prerequisites](#prerequisites)
|
|
25
|
-
- [Quick Start](#quick-start)
|
|
26
|
-
- [Built-in Commands](#built-in-commands)
|
|
27
|
-
- [Desktop App Adapters](#desktop-app-adapters)
|
|
28
|
-
- [External CLI Hub](#external-cli-hub)
|
|
29
|
-
- [Download Support](#download-support)
|
|
30
|
-
- [Output Formats](#output-formats)
|
|
31
|
-
- [For AI Agents (Developer Guide)](#for-ai-agents-developer-guide)
|
|
32
|
-
- [Remote Chrome (Server/Headless)](#remote-chrome-serverheadless)
|
|
33
|
-
- [Testing](#testing)
|
|
34
|
-
- [Troubleshooting](#troubleshooting)
|
|
35
|
-
- [Releasing New Versions](#releasing-new-versions)
|
|
36
|
-
- [License](#license)
|
|
37
|
-
|
|
38
|
-
---
|
|
39
|
-
|
|
40
21
|
## Highlights
|
|
41
22
|
|
|
42
23
|
- **CLI All Electron** — CLI-ify apps like Antigravity Ultra! Now AI can control itself natively using cc/openclaw!
|
|
43
24
|
- **Account-safe** — Reuses Chrome's logged-in state; your credentials never leave the browser.
|
|
44
25
|
- **AI Agent ready** — `explore` discovers APIs, `synthesize` generates adapters, `cascade` finds auth strategies.
|
|
45
26
|
- **External CLI Hub** — Discover, auto-install, and passthrough commands to any external CLI (gh, obsidian, docker, kubectl, etc). Zero setup.
|
|
46
|
-
- **Self-healing setup** — `opencli
|
|
27
|
+
- **Self-healing setup** — `opencli doctor` diagnoses and auto-starts the daemon, extension, and live browser connectivity.
|
|
47
28
|
- **Dynamic Loader** — Simply drop `.ts` or `.yaml` adapters into the `clis/` folder for auto-registration.
|
|
48
29
|
- **Dual-Engine Architecture** — Supports both YAML declarative data pipelines and robust browser runtime TypeScript injections.
|
|
49
30
|
|
|
@@ -65,11 +46,7 @@ You can install the extension via either method:
|
|
|
65
46
|
2. Unzip the file and open `chrome://extensions`, enable **Developer mode** (top-right toggle).
|
|
66
47
|
3. Click **Load unpacked** and select the unzipped folder.
|
|
67
48
|
|
|
68
|
-
**Method 2: Load
|
|
69
|
-
1. After installing opencli via npm, open `chrome://extensions` and enable **Developer mode**.
|
|
70
|
-
2. Click **Load unpacked** and select `node_modules/@jackwener/opencli/extension` directory.
|
|
71
|
-
|
|
72
|
-
**Method 3: Load Source (For Developers)**
|
|
49
|
+
**Method 2: Load Source (For Developers)**
|
|
73
50
|
1. Open `chrome://extensions` and enable **Developer mode**.
|
|
74
51
|
2. Click **Load unpacked** and select the `extension/` directory from this repository.
|
|
75
52
|
|
|
@@ -78,7 +55,6 @@ That's it! The daemon auto-starts when you run any browser command. No tokens, n
|
|
|
78
55
|
> **Tip**: Use `opencli doctor` for ongoing diagnosis:
|
|
79
56
|
> ```bash
|
|
80
57
|
> opencli doctor # Check extension + daemon connectivity
|
|
81
|
-
> opencli doctor --live # Also test live browser commands
|
|
82
58
|
> ```
|
|
83
59
|
|
|
84
60
|
## Quick Start
|
|
@@ -167,7 +143,6 @@ Run `opencli list` for the live registry.
|
|
|
167
143
|
| **weread** | `shelf` `search` `book` `highlights` `notes` `notebooks` `ranking` | Browser |
|
|
168
144
|
| **douban** | `search` `top250` `subject` `marks` `reviews` | Browser |
|
|
169
145
|
|
|
170
|
-
> **Bloomberg note**: The RSS-backed Bloomberg listing commands (`main`, section feeds, `feeds`) work without a browser. `bloomberg news` is for standard Bloomberg story/article pages that your current Chrome session can already access. Audio and some other non-standard pages may fail, and OpenCLI does not bypass Bloomberg paywall or entitlement checks.
|
|
171
146
|
|
|
172
147
|
### External CLI Hub
|
|
173
148
|
|
|
@@ -252,20 +227,7 @@ opencli zhihu download "https://zhuanlan.zhihu.com/p/xxx" --output ./zhihu
|
|
|
252
227
|
opencli zhihu download "https://zhuanlan.zhihu.com/p/xxx" --download-images
|
|
253
228
|
```
|
|
254
229
|
|
|
255
|
-
### Pipeline Step (for YAML adapters)
|
|
256
230
|
|
|
257
|
-
The `download` step can be used in YAML pipelines:
|
|
258
|
-
|
|
259
|
-
```yaml
|
|
260
|
-
pipeline:
|
|
261
|
-
- fetch: https://api.example.com/media
|
|
262
|
-
- download:
|
|
263
|
-
url: ${{ item.imageUrl }}
|
|
264
|
-
dir: ./downloads
|
|
265
|
-
filename: ${{ item.title | sanitize }}.jpg
|
|
266
|
-
concurrency: 5
|
|
267
|
-
skip_existing: true
|
|
268
|
-
```
|
|
269
231
|
|
|
270
232
|
## Output Formats
|
|
271
233
|
|
|
@@ -308,21 +270,7 @@ Explore outputs to `.opencli/explore/<site>/` (manifest.json, endpoints.json, ca
|
|
|
308
270
|
|
|
309
271
|
## Testing
|
|
310
272
|
|
|
311
|
-
See **[TESTING.md](./TESTING.md)** for
|
|
312
|
-
|
|
313
|
-
- Current test coverage (unit + E2E tests across browser and desktop adapters)
|
|
314
|
-
- How to run tests locally
|
|
315
|
-
- How to add tests when creating new adapters
|
|
316
|
-
- CI/CD pipeline with sharding
|
|
317
|
-
- Headless browser mode (`OPENCLI_HEADLESS=1`)
|
|
318
|
-
|
|
319
|
-
```bash
|
|
320
|
-
# Quick start
|
|
321
|
-
npm run build
|
|
322
|
-
npx vitest run # All tests
|
|
323
|
-
npx vitest run src/ # Unit tests only
|
|
324
|
-
npx vitest run tests/e2e/ # E2E tests
|
|
325
|
-
```
|
|
273
|
+
See **[TESTING.md](./TESTING.md)** for how to run and write tests.
|
|
326
274
|
|
|
327
275
|
## Troubleshooting
|
|
328
276
|
|
|
@@ -338,15 +286,6 @@ npx vitest run tests/e2e/ # E2E tests
|
|
|
338
286
|
- Check daemon status: `curl localhost:19825/status`
|
|
339
287
|
- View extension logs: `curl localhost:19825/logs`
|
|
340
288
|
|
|
341
|
-
## Releasing New Versions
|
|
342
|
-
|
|
343
|
-
```bash
|
|
344
|
-
npm version patch # 0.1.0 → 0.1.1
|
|
345
|
-
npm version minor # 0.1.0 → 0.2.0
|
|
346
|
-
git push --follow-tags
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
The CI will automatically build, create a GitHub release, and publish to npm.
|
|
350
289
|
|
|
351
290
|
## Star History
|
|
352
291
|
|
package/README.zh-CN.md
CHANGED
|
@@ -20,31 +20,13 @@ CLI all electron!现在支持把所有 electron 应用 CLI 化,从而组合
|
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
23
|
-
## 目录
|
|
24
|
-
|
|
25
|
-
- [亮点](#亮点)
|
|
26
|
-
- [前置要求](#前置要求)
|
|
27
|
-
- [快速开始](#快速开始)
|
|
28
|
-
- [内置命令](#内置命令)
|
|
29
|
-
- [桌面应用适配器](#桌面应用适配器)
|
|
30
|
-
- [外部 CLI 枢纽](#外部-cli-枢纽)
|
|
31
|
-
- [下载支持](#下载支持)
|
|
32
|
-
- [输出格式](#输出格式)
|
|
33
|
-
- [致 AI Agent(开发者指南)](#致-ai-agent开发者指南)
|
|
34
|
-
- [远程 Chrome(服务器/无头环境)](#远程-chrome服务器无头环境)
|
|
35
|
-
- [常见问题排查](#常见问题排查)
|
|
36
|
-
- [版本发布](#版本发布)
|
|
37
|
-
- [License](#license)
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
23
|
## 亮点
|
|
42
24
|
|
|
43
25
|
- **CLI All Electron** — 支持把所有 electron 应用(如 Antigravity Ultra)CLI 化,让 AI 控制自己!
|
|
44
26
|
- **多站点覆盖** — 覆盖 B站、知乎、小红书、Twitter、Reddit,以及多种桌面应用
|
|
45
27
|
- **零风控** — 复用 Chrome 登录态,无需存储任何凭证
|
|
46
28
|
- **外部 CLI 枢纽** — 统一发现、自动安装、透传执行 `gh`、`docker`、`kubectl` 等本地 CLI
|
|
47
|
-
- **自修复配置** — `opencli
|
|
29
|
+
- **自修复配置** — `opencli doctor` 自动启动 daemon,诊断扩展和浏览器连接状态
|
|
48
30
|
- **AI 原生** — `explore` 自动发现 API,`synthesize` 生成适配器,`cascade` 探测认证策略
|
|
49
31
|
- **动态加载引擎** — 声明式的 `.yaml` 或者底层定制的 `.ts` 适配器,放入 `clis/` 文件夹即可自动注册生效
|
|
50
32
|
|
|
@@ -66,11 +48,7 @@ OpenCLI 通过轻量化的 **Browser Bridge** Chrome 扩展 + 微型 daemon 与
|
|
|
66
48
|
2. 解压后打开 Chrome 的 `chrome://extensions`,启用右上角的 **开发者模式**。
|
|
67
49
|
3. 点击 **加载已解压的扩展程序**,选择解压后的文件夹。
|
|
68
50
|
|
|
69
|
-
|
|
70
|
-
1. 通过 npm 安装 opencli 后,打开 `chrome://extensions`,启用 **开发者模式**。
|
|
71
|
-
2. 点击 **加载已解压的扩展程序**,选择 `node_modules/@jackwener/opencli/extension` 目录。
|
|
72
|
-
|
|
73
|
-
**方式三:加载源码(针对开发者)**
|
|
51
|
+
**方式二:加载源码(针对开发者)**
|
|
74
52
|
1. 同样在 `chrome://extensions` 开启 **开发者模式**。
|
|
75
53
|
2. 点击 **加载已解压的扩展程序**,选择本仓库代码树中的 `extension/` 文件夹。
|
|
76
54
|
|
|
@@ -79,7 +57,6 @@ OpenCLI 通过轻量化的 **Browser Bridge** Chrome 扩展 + 微型 daemon 与
|
|
|
79
57
|
> **Tip**:后续诊断用 `opencli doctor`:
|
|
80
58
|
> ```bash
|
|
81
59
|
> opencli doctor # 检查扩展和 daemon 连通性
|
|
82
|
-
> opencli doctor --live # 额外测试浏览器命令
|
|
83
60
|
> ```
|
|
84
61
|
|
|
85
62
|
## 快速开始
|
|
@@ -168,7 +145,6 @@ npm install -g @jackwener/opencli@latest
|
|
|
168
145
|
| **weread** | `shelf` `search` `book` `highlights` `notes` `notebooks` `ranking` | 浏览器 |
|
|
169
146
|
| **douban** | `search` `top250` `subject` `marks` `reviews` | 浏览器 |
|
|
170
147
|
|
|
171
|
-
> **Bloomberg 说明**:Bloomberg 的 RSS 列表命令(`main`、各栏目 feed、`feeds`)无需浏览器即可使用。`bloomberg news` 适用于当前 Chrome 会话本身就能访问的标准 Bloomberg 文章页。音频页和部分非标准页面可能失败,OpenCLI 也不会绕过 Bloomberg 的付费墙、登录或权限校验。
|
|
172
148
|
|
|
173
149
|
### 外部 CLI 枢纽
|
|
174
150
|
|
|
@@ -253,20 +229,7 @@ opencli zhihu download "https://zhuanlan.zhihu.com/p/xxx" --output ./zhihu
|
|
|
253
229
|
opencli zhihu download "https://zhuanlan.zhihu.com/p/xxx" --download-images
|
|
254
230
|
```
|
|
255
231
|
|
|
256
|
-
### Pipeline Step(用于 YAML 适配器)
|
|
257
|
-
|
|
258
|
-
`download` step 可以在 YAML 管线中使用:
|
|
259
232
|
|
|
260
|
-
```yaml
|
|
261
|
-
pipeline:
|
|
262
|
-
- fetch: https://api.example.com/media
|
|
263
|
-
- download:
|
|
264
|
-
url: ${{ item.imageUrl }}
|
|
265
|
-
dir: ./downloads
|
|
266
|
-
filename: ${{ item.title | sanitize }}.jpg
|
|
267
|
-
concurrency: 5
|
|
268
|
-
skip_existing: true
|
|
269
|
-
```
|
|
270
233
|
|
|
271
234
|
## 输出格式
|
|
272
235
|
|
|
@@ -321,15 +284,6 @@ opencli cascade https://api.example.com/data
|
|
|
321
284
|
- 检查 daemon 状态:`curl localhost:19825/status`
|
|
322
285
|
- 查看扩展日志:`curl localhost:19825/logs`
|
|
323
286
|
|
|
324
|
-
## 版本发布
|
|
325
|
-
|
|
326
|
-
```bash
|
|
327
|
-
npm version patch # 0.1.0 → 0.1.1
|
|
328
|
-
npm version minor # 0.1.0 → 0.2.0
|
|
329
|
-
|
|
330
|
-
# 推送 tag,GitHub Actions 将自动执行发版和 npm 发布
|
|
331
|
-
git push --follow-tags
|
|
332
|
-
```
|
|
333
287
|
|
|
334
288
|
## Star History
|
|
335
289
|
|
package/SKILL.md
CHANGED
|
@@ -244,9 +244,7 @@ opencli install <name> # Auto-install an external CLI (e.g., gh, obsidian)
|
|
|
244
244
|
opencli register <name> # Register a local custom CLI for unified discovery
|
|
245
245
|
opencli validate # Validate all CLI definitions
|
|
246
246
|
opencli validate bilibili # Validate specific site
|
|
247
|
-
opencli
|
|
248
|
-
opencli doctor # Diagnose daemon, extension, and browser connectivity
|
|
249
|
-
opencli doctor --live # Also test live browser connectivity
|
|
247
|
+
opencli doctor # Diagnose browser bridge (auto-starts daemon, includes live test)
|
|
250
248
|
```
|
|
251
249
|
|
|
252
250
|
### AI Agent Workflow
|
package/TESTING.md
CHANGED
|
@@ -18,57 +18,72 @@
|
|
|
18
18
|
|
|
19
19
|
测试分为三层,全部使用 **vitest** 运行:
|
|
20
20
|
|
|
21
|
-
```
|
|
21
|
+
```text
|
|
22
22
|
tests/
|
|
23
23
|
├── e2e/ # E2E 集成测试(子进程运行真实 CLI)
|
|
24
|
-
│ ├── helpers.ts # runCli() 共享工具
|
|
25
|
-
│ ├── public-commands.test.ts # 公开 API
|
|
24
|
+
│ ├── helpers.ts # runCli() / parseJsonOutput() 共享工具
|
|
25
|
+
│ ├── public-commands.test.ts # 公开 API 命令
|
|
26
26
|
│ ├── browser-public.test.ts # 浏览器命令(公开数据)
|
|
27
|
-
│ ├── browser-auth.test.ts # 需登录命令(graceful failure
|
|
28
|
-
│ ├── management.test.ts # 管理命令(list
|
|
29
|
-
│ └── output-formats.test.ts #
|
|
30
|
-
├── smoke/
|
|
31
|
-
│ └── api-health.test.ts # 外部 API
|
|
27
|
+
│ ├── browser-auth.test.ts # 需登录命令(graceful failure)
|
|
28
|
+
│ ├── management.test.ts # 管理命令(list / validate / verify / help)
|
|
29
|
+
│ └── output-formats.test.ts # 输出格式校验
|
|
30
|
+
├── smoke/
|
|
31
|
+
│ └── api-health.test.ts # 外部 API、adapter 定义、命令注册健康检查
|
|
32
32
|
src/
|
|
33
|
-
|
|
33
|
+
└── **/*.test.ts # 单元测试(当前 31 个文件)
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
| 层 | 位置 | 运行方式 | 用途 |
|
|
37
|
-
|
|
38
|
-
| 单元测试 | `src/**/*.test.ts` | `npx vitest run src/` |
|
|
39
|
-
| E2E 测试 | `tests/e2e/*.test.ts` | `npx vitest run tests/e2e/` | 真实 CLI 命令执行 |
|
|
40
|
-
| 烟雾测试 | `tests/smoke/*.test.ts` | `npx vitest run tests/smoke/` | 外部 API
|
|
36
|
+
| 层 | 位置 | 当前文件数 | 运行方式 | 用途 |
|
|
37
|
+
|---|---|---:|---|---|
|
|
38
|
+
| 单元测试 | `src/**/*.test.ts` | 31 | `npx vitest run src/` | 内部模块、pipeline、adapter 工具函数 |
|
|
39
|
+
| E2E 测试 | `tests/e2e/*.test.ts` | 5 | `npx vitest run tests/e2e/` | 真实 CLI 命令执行 |
|
|
40
|
+
| 烟雾测试 | `tests/smoke/*.test.ts` | 1 | `npx vitest run tests/smoke/` | 外部 API 与注册完整性 |
|
|
41
41
|
|
|
42
42
|
---
|
|
43
43
|
|
|
44
44
|
## 当前覆盖范围
|
|
45
45
|
|
|
46
|
-
### 单元测试(
|
|
46
|
+
### 单元测试(31 个文件)
|
|
47
47
|
|
|
48
|
-
|
|
|
48
|
+
| 领域 | 文件 |
|
|
49
49
|
|---|---|
|
|
50
|
-
| `browser.test.ts`
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
|
65
|
-
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
50
|
+
| 核心运行时与输出 | `src/browser.test.ts`, `src/browser/dom-snapshot.test.ts`, `src/build-manifest.test.ts`, `src/capabilityRouting.test.ts`, `src/doctor.test.ts`, `src/engine.test.ts`, `src/interceptor.test.ts`, `src/output.test.ts`, `src/plugin.test.ts`, `src/registry.test.ts`, `src/snapshotFormatter.test.ts` |
|
|
51
|
+
| pipeline 与下载 | `src/download/index.test.ts`, `src/pipeline/executor.test.ts`, `src/pipeline/template.test.ts`, `src/pipeline/transform.test.ts` |
|
|
52
|
+
| 站点 / adapter 逻辑 | `src/clis/apple-podcasts/commands.test.ts`, `src/clis/apple-podcasts/utils.test.ts`, `src/clis/bloomberg/utils.test.ts`, `src/clis/chaoxing/utils.test.ts`, `src/clis/coupang/utils.test.ts`, `src/clis/google/utils.test.ts`, `src/clis/grok/ask.test.ts`, `src/clis/twitter/timeline.test.ts`, `src/clis/weread/utils.test.ts`, `src/clis/xiaohongshu/creator-note-detail.test.ts`, `src/clis/xiaohongshu/creator-notes-summary.test.ts`, `src/clis/xiaohongshu/creator-notes.test.ts`, `src/clis/xiaohongshu/user-helpers.test.ts`, `src/clis/xiaoyuzhou/utils.test.ts`, `src/clis/youtube/transcript-group.test.ts`, `src/clis/zhihu/download.test.ts` |
|
|
53
|
+
|
|
54
|
+
这些测试覆盖的重点包括:
|
|
55
|
+
|
|
56
|
+
- Browser Bridge、DOM snapshot、interceptor、capability routing
|
|
57
|
+
- manifest 生成、命令发现、插件安装与注册表
|
|
58
|
+
- 输出格式渲染与 snapshot formatting
|
|
59
|
+
- pipeline 模板求值、执行器与变换步骤
|
|
60
|
+
- 各站点 adapter 的数据归一化、参数处理与容错逻辑
|
|
61
|
+
|
|
62
|
+
### E2E 测试(5 个文件)
|
|
63
|
+
|
|
64
|
+
| 文件 | 当前覆盖范围 |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `tests/e2e/public-commands.test.ts` | `bloomberg`、`apple-podcasts`、`hackernews`、`v2ex`、`xiaoyuzhou`、`google suggest` 等公开命令 |
|
|
67
|
+
| `tests/e2e/browser-public.test.ts` | `bbc`、`bloomberg`、`bilibili`、`weibo`、`zhihu`、`reddit`、`twitter`、`xueqiu`、`reuters`、`youtube`、`smzdm`、`boss`、`ctrip`、`coupang`、`xiaohongshu`、`google`、`yahoo-finance`、`v2ex daily` |
|
|
68
|
+
| `tests/e2e/browser-auth.test.ts` | `bilibili`、`twitter`、`v2ex`、`xueqiu`、`linux-do`、`xiaohongshu` 的需登录命令 graceful failure |
|
|
69
|
+
| `tests/e2e/management.test.ts` | `list`、`validate`、`verify`、`--version`、`--help`、unknown command |
|
|
70
|
+
| `tests/e2e/output-formats.test.ts` | `json` / `yaml` / `csv` / `md` 输出格式校验 |
|
|
71
|
+
|
|
72
|
+
### 烟雾测试(1 个文件)
|
|
73
|
+
|
|
74
|
+
| 文件 | 当前覆盖范围 |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `tests/smoke/api-health.test.ts` | `hackernews`、`v2ex` 公开 API 可用性,`validate` 全量 adapter 校验,以及命令注册表基础完整性 |
|
|
68
77
|
|
|
69
|
-
###
|
|
78
|
+
### 快速核对命令
|
|
70
79
|
|
|
71
|
-
|
|
80
|
+
需要刷新测试清单时,直接以仓库文件为准:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
find src -name '*.test.ts' | sort
|
|
84
|
+
find tests/e2e -name '*.test.ts' | sort
|
|
85
|
+
find tests/smoke -name '*.test.ts' | sort
|
|
86
|
+
```
|
|
72
87
|
|
|
73
88
|
---
|
|
74
89
|
|
|
@@ -78,7 +93,7 @@ src/
|
|
|
78
93
|
|
|
79
94
|
```bash
|
|
80
95
|
npm ci # 安装依赖
|
|
81
|
-
npm run build # 编译(E2E 测试需要 dist/main.js)
|
|
96
|
+
npm run build # 编译(E2E / smoke 测试需要 dist/main.js)
|
|
82
97
|
```
|
|
83
98
|
|
|
84
99
|
### 运行命令
|
|
@@ -87,18 +102,19 @@ npm run build # 编译(E2E 测试需要 dist/main.js)
|
|
|
87
102
|
# 全部单元测试
|
|
88
103
|
npx vitest run src/
|
|
89
104
|
|
|
90
|
-
# 全部 E2E 测试(会真实调用外部 API
|
|
105
|
+
# 全部 E2E 测试(会真实调用外部 API / 浏览器)
|
|
91
106
|
npx vitest run tests/e2e/
|
|
92
107
|
|
|
108
|
+
# 全部 smoke 测试
|
|
109
|
+
npx vitest run tests/smoke/
|
|
110
|
+
|
|
93
111
|
# 单个测试文件
|
|
112
|
+
npx vitest run src/clis/apple-podcasts/commands.test.ts
|
|
94
113
|
npx vitest run tests/e2e/management.test.ts
|
|
95
114
|
|
|
96
|
-
#
|
|
115
|
+
# 全部测试
|
|
97
116
|
npx vitest run
|
|
98
117
|
|
|
99
|
-
# 烟雾测试
|
|
100
|
-
npx vitest run tests/smoke/
|
|
101
|
-
|
|
102
118
|
# watch 模式(开发时推荐)
|
|
103
119
|
npx vitest src/
|
|
104
120
|
```
|
|
@@ -106,9 +122,10 @@ npx vitest src/
|
|
|
106
122
|
### 浏览器命令本地测试须知
|
|
107
123
|
|
|
108
124
|
- opencli 通过 Browser Bridge 扩展连接已运行的 Chrome 浏览器
|
|
109
|
-
- `
|
|
110
|
-
- `browser-
|
|
111
|
-
-
|
|
125
|
+
- E2E 测试通过 `tests/e2e/helpers.ts` 里的 `runCli()` 调用已构建的 `dist/main.js`
|
|
126
|
+
- `browser-public.test.ts` 使用 `tryBrowserCommand()`,站点反爬或地域限制导致空数据时会 warn + pass
|
|
127
|
+
- `browser-auth.test.ts` 验证 **graceful failure**,重点是不 crash、不 hang、错误信息可控
|
|
128
|
+
- 如需测试完整登录态,保持 Chrome 登录态并安装 Browser Bridge 扩展,再手动运行对应测试
|
|
112
129
|
|
|
113
130
|
---
|
|
114
131
|
|
|
@@ -116,8 +133,8 @@ npx vitest src/
|
|
|
116
133
|
|
|
117
134
|
### 新增 YAML Adapter(如 `src/clis/producthunt/trending.yaml`)
|
|
118
135
|
|
|
119
|
-
1.
|
|
120
|
-
2. 根据 adapter
|
|
136
|
+
1. `opencli validate` 的 E2E / smoke 测试会覆盖 adapter 结构校验
|
|
137
|
+
2. 根据 adapter 类型,在对应测试文件补一个 `it()` block
|
|
121
138
|
|
|
122
139
|
```typescript
|
|
123
140
|
// 如果 browser: false(公开 API)→ tests/e2e/public-commands.test.ts
|
|
@@ -148,15 +165,15 @@ it('producthunt me fails gracefully without login', async () => {
|
|
|
148
165
|
|
|
149
166
|
### 新增管理命令(如 `opencli export`)
|
|
150
167
|
|
|
151
|
-
在 `tests/e2e/management.test.ts`
|
|
168
|
+
在 `tests/e2e/management.test.ts` 添加测试;如果新命令会影响输出格式,也同步补 `tests/e2e/output-formats.test.ts`。
|
|
152
169
|
|
|
153
170
|
### 新增内部模块
|
|
154
171
|
|
|
155
|
-
|
|
172
|
+
在对应源码旁创建 `*.test.ts`,优先和被测模块放在同一目录下,便于发现与维护。
|
|
156
173
|
|
|
157
174
|
### 决策流程图
|
|
158
175
|
|
|
159
|
-
```
|
|
176
|
+
```text
|
|
160
177
|
新增功能 → 是内部模块? → 是 → src/ 下加 *.test.ts
|
|
161
178
|
↓ 否
|
|
162
179
|
是 CLI 命令? → browser: false? → tests/e2e/public-commands.test.ts
|
|
@@ -170,32 +187,33 @@ it('producthunt me fails gracefully without login', async () => {
|
|
|
170
187
|
|
|
171
188
|
## CI/CD 流水线
|
|
172
189
|
|
|
173
|
-
### ci.yml
|
|
190
|
+
### `ci.yml`
|
|
174
191
|
|
|
175
192
|
| Job | 触发条件 | 内容 |
|
|
176
193
|
|---|---|---|
|
|
177
|
-
|
|
|
178
|
-
|
|
|
179
|
-
|
|
|
194
|
+
| `build` | push/PR 到 `main`,`dev` | `tsc --noEmit` + `npm run build` |
|
|
195
|
+
| `unit-test` | push/PR 到 `main`,`dev` | Node `20` 与 `22` 双版本运行 `src/` 单元测试,按 `2` shard 并行 |
|
|
196
|
+
| `smoke-test` | `schedule` 或 `workflow_dispatch` | 安装真实 Chrome,`xvfb-run` 执行 `tests/smoke/` |
|
|
180
197
|
|
|
181
|
-
### e2e-headed.yml
|
|
198
|
+
### `e2e-headed.yml`
|
|
182
199
|
|
|
183
200
|
| Job | 触发条件 | 内容 |
|
|
184
201
|
|---|---|---|
|
|
185
|
-
|
|
|
202
|
+
| `e2e-headed` | push/PR 到 `main`,`dev`,或手动触发 | 安装真实 Chrome,`xvfb-run` 执行 `tests/e2e/` |
|
|
186
203
|
|
|
187
|
-
E2E
|
|
204
|
+
E2E 与 smoke 都使用 `./.github/actions/setup-chrome` 准备真实 Chrome,并通过 `OPENCLI_BROWSER_EXECUTABLE_PATH` 注入浏览器路径。
|
|
188
205
|
|
|
189
206
|
### Sharding
|
|
190
207
|
|
|
191
|
-
单元测试使用 vitest 内置 shard
|
|
208
|
+
单元测试使用 vitest 内置 shard,并在 Node `20` / `22` 两个版本上运行:
|
|
192
209
|
|
|
193
210
|
```yaml
|
|
194
211
|
strategy:
|
|
195
212
|
matrix:
|
|
213
|
+
node-version: ['20', '22']
|
|
196
214
|
shard: [1, 2]
|
|
197
215
|
steps:
|
|
198
|
-
- run: npx vitest run src/ --shard=${{ matrix.shard }}/2
|
|
216
|
+
- run: npx vitest run src/ --reporter=verbose --shard=${{ matrix.shard }}/2
|
|
199
217
|
```
|
|
200
218
|
|
|
201
219
|
---
|
|
@@ -206,8 +224,8 @@ opencli 通过 Browser Bridge 扩展连接浏览器:
|
|
|
206
224
|
|
|
207
225
|
| 条件 | 模式 | 使用场景 |
|
|
208
226
|
|---|---|---|
|
|
209
|
-
| 扩展已安装 | Extension 模式 | 本地用户,连接已登录的 Chrome |
|
|
210
|
-
|
|
|
227
|
+
| 扩展已安装 / 已连接 | Extension 模式 | 本地用户,连接已登录的 Chrome |
|
|
228
|
+
| 无扩展 token | CLI 自行拉起浏览器 | CI、无登录态或纯自动化场景 |
|
|
211
229
|
|
|
212
230
|
CI 中使用 `OPENCLI_BROWSER_EXECUTABLE_PATH` 指定真实 Chrome 路径:
|
|
213
231
|
|
|
@@ -220,14 +238,14 @@ env:
|
|
|
220
238
|
|
|
221
239
|
## 站点兼容性
|
|
222
240
|
|
|
223
|
-
|
|
241
|
+
GitHub Actions 的美国 runner 上,部分站点会因为地域限制、登录要求或反爬而返回空数据。当前 E2E 对这些场景采用 warn + pass 策略,避免偶发站点限制把整条 CI 打红。
|
|
224
242
|
|
|
225
|
-
| 站点 | CI
|
|
243
|
+
| 站点 | CI 表现 | 常见原因 |
|
|
226
244
|
|---|---|---|
|
|
227
|
-
| hackernews
|
|
228
|
-
| yahoo-finance |
|
|
229
|
-
| bilibili
|
|
230
|
-
| reddit
|
|
231
|
-
| smzdm
|
|
245
|
+
| `hackernews`、`bbc`、`v2ex`、`bloomberg` | 通常返回数据 | 公开接口或公开页面 |
|
|
246
|
+
| `yahoo-finance`、`google` | 通常返回数据 | 页面公开,但仍可能受限流影响 |
|
|
247
|
+
| `bilibili`、`zhihu`、`weibo`、`xiaohongshu`、`xueqiu` | 容易空数据 | 地域限制、反爬、登录要求 |
|
|
248
|
+
| `reddit`、`twitter`、`youtube` | 容易空数据 | 登录态、cookie、机器人检测 |
|
|
249
|
+
| `smzdm`、`boss`、`ctrip`、`coupang`、`linux-do` | 结果波动较大 | 地域限制、风控或页面结构变动 |
|
|
232
250
|
|
|
233
|
-
>
|
|
251
|
+
> 如果需要更稳定的浏览器 E2E 结果,优先使用具备目标站点网络可达性的 self-hosted runner。
|
package/dist/browser/cdp.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { WebSocket } from 'ws';
|
|
11
11
|
import { wrapForEval } from './utils.js';
|
|
12
12
|
import { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
|
|
13
|
-
import { clickJs, typeTextJs, pressKeyJs, waitForTextJs, scrollJs, autoScrollJs, networkRequestsJs, } from './dom-helpers.js';
|
|
13
|
+
import { clickJs, typeTextJs, pressKeyJs, waitForTextJs, scrollJs, autoScrollJs, networkRequestsJs, waitForDomStableJs, } from './dom-helpers.js';
|
|
14
14
|
const CDP_SEND_TIMEOUT = 30_000; // 30s per command
|
|
15
15
|
export class CDPBridge {
|
|
16
16
|
_ws = null;
|
|
@@ -145,10 +145,11 @@ class CDPPage {
|
|
|
145
145
|
.catch(() => { }); // Don't fail if event times out
|
|
146
146
|
await this.bridge.send('Page.navigate', { url });
|
|
147
147
|
await loadPromise;
|
|
148
|
-
//
|
|
148
|
+
// Smart settle: use DOM stability detection instead of fixed sleep.
|
|
149
|
+
// settleMs is now a timeout cap (default 1000ms), not a fixed wait.
|
|
149
150
|
if (options?.waitUntil !== 'none') {
|
|
150
|
-
const
|
|
151
|
-
await
|
|
151
|
+
const maxMs = options?.settleMs ?? 1000;
|
|
152
|
+
await this.evaluate(waitForDomStableJs(maxMs, Math.min(500, maxMs)));
|
|
152
153
|
}
|
|
153
154
|
}
|
|
154
155
|
async evaluate(js) {
|
|
@@ -16,7 +16,10 @@ export async function isDaemonRunning() {
|
|
|
16
16
|
try {
|
|
17
17
|
const controller = new AbortController();
|
|
18
18
|
const timer = setTimeout(() => controller.abort(), 2000);
|
|
19
|
-
const res = await fetch(`${DAEMON_URL}/status`, {
|
|
19
|
+
const res = await fetch(`${DAEMON_URL}/status`, {
|
|
20
|
+
headers: { 'X-OpenCLI': '1' },
|
|
21
|
+
signal: controller.signal,
|
|
22
|
+
});
|
|
20
23
|
clearTimeout(timer);
|
|
21
24
|
return res.ok;
|
|
22
25
|
}
|
|
@@ -31,7 +34,10 @@ export async function isExtensionConnected() {
|
|
|
31
34
|
try {
|
|
32
35
|
const controller = new AbortController();
|
|
33
36
|
const timer = setTimeout(() => controller.abort(), 2000);
|
|
34
|
-
const res = await fetch(`${DAEMON_URL}/status`, {
|
|
37
|
+
const res = await fetch(`${DAEMON_URL}/status`, {
|
|
38
|
+
headers: { 'X-OpenCLI': '1' },
|
|
39
|
+
signal: controller.signal,
|
|
40
|
+
});
|
|
35
41
|
clearTimeout(timer);
|
|
36
42
|
if (!res.ok)
|
|
37
43
|
return false;
|
|
@@ -58,7 +64,7 @@ export async function sendCommand(action, params = {}) {
|
|
|
58
64
|
const timer = setTimeout(() => controller.abort(), 30000);
|
|
59
65
|
const res = await fetch(`${DAEMON_URL}/command`, {
|
|
60
66
|
method: 'POST',
|
|
61
|
-
headers: { 'Content-Type': 'application/json' },
|
|
67
|
+
headers: { 'Content-Type': 'application/json', 'X-OpenCLI': '1' },
|
|
62
68
|
body: JSON.stringify(command),
|
|
63
69
|
signal: controller.signal,
|
|
64
70
|
});
|
package/dist/browser/discover.js
CHANGED
|
@@ -12,7 +12,9 @@ export { isDaemonRunning };
|
|
|
12
12
|
export async function checkDaemonStatus() {
|
|
13
13
|
try {
|
|
14
14
|
const port = parseInt(process.env.OPENCLI_DAEMON_PORT ?? '19825', 10);
|
|
15
|
-
const res = await fetch(`http://127.0.0.1:${port}/status
|
|
15
|
+
const res = await fetch(`http://127.0.0.1:${port}/status`, {
|
|
16
|
+
headers: { 'X-OpenCLI': '1' },
|
|
17
|
+
});
|
|
16
18
|
const data = await res.json();
|
|
17
19
|
return { running: true, extensionConnected: data.extensionConnected };
|
|
18
20
|
}
|
|
@@ -18,3 +18,11 @@ export declare function scrollJs(direction: string, amount: number): string;
|
|
|
18
18
|
export declare function autoScrollJs(times: number, delayMs: number): string;
|
|
19
19
|
/** Generate JS to read performance resource entries as network requests */
|
|
20
20
|
export declare function networkRequestsJs(includeStatic: boolean): string;
|
|
21
|
+
/**
|
|
22
|
+
* Generate JS to wait until the DOM stabilizes (no mutations for `quietMs`),
|
|
23
|
+
* with a hard cap at `maxMs`. Uses MutationObserver in the browser.
|
|
24
|
+
*
|
|
25
|
+
* Returns as soon as the page stops changing, avoiding unnecessary fixed waits.
|
|
26
|
+
* If document.body is not available, falls back to a fixed sleep of maxMs.
|
|
27
|
+
*/
|
|
28
|
+
export declare function waitForDomStableJs(maxMs: number, quietMs: number): string;
|
|
@@ -138,3 +138,36 @@ export function networkRequestsJs(includeStatic) {
|
|
|
138
138
|
})()
|
|
139
139
|
`;
|
|
140
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Generate JS to wait until the DOM stabilizes (no mutations for `quietMs`),
|
|
143
|
+
* with a hard cap at `maxMs`. Uses MutationObserver in the browser.
|
|
144
|
+
*
|
|
145
|
+
* Returns as soon as the page stops changing, avoiding unnecessary fixed waits.
|
|
146
|
+
* If document.body is not available, falls back to a fixed sleep of maxMs.
|
|
147
|
+
*/
|
|
148
|
+
export function waitForDomStableJs(maxMs, quietMs) {
|
|
149
|
+
return `
|
|
150
|
+
new Promise(resolve => {
|
|
151
|
+
if (!document.body) {
|
|
152
|
+
setTimeout(() => resolve('nobody'), ${maxMs});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
let timer = null;
|
|
156
|
+
let cap = null;
|
|
157
|
+
const done = (reason) => {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
clearTimeout(cap);
|
|
160
|
+
obs.disconnect();
|
|
161
|
+
resolve(reason);
|
|
162
|
+
};
|
|
163
|
+
const resetQuiet = () => {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
timer = setTimeout(() => done('quiet'), ${quietMs});
|
|
166
|
+
};
|
|
167
|
+
const obs = new MutationObserver(resetQuiet);
|
|
168
|
+
obs.observe(document.body, { childList: true, subtree: true, attributes: true });
|
|
169
|
+
resetQuiet();
|
|
170
|
+
cap = setTimeout(() => done('capped'), ${maxMs});
|
|
171
|
+
})
|
|
172
|
+
`;
|
|
173
|
+
}
|