@jackwener/opencli 1.2.5 → 1.3.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.
@@ -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 setup` verifies Browser Bridge connectivity; `opencli doctor` diagnoses daemon, extension, and live browser connectivity.
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 from npm Package**
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,26 +270,14 @@ 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 the full testing guide, including:
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
 
329
277
  - **"Extension not connected"**
330
278
  - Ensure the opencli Browser Bridge extension is installed and **enabled** in `chrome://extensions`.
279
+ - **"attach failed: Cannot access a chrome-extension:// URL"**
280
+ - Another Chrome extension (e.g. youmind, New Tab Override, or AI assistant extensions) may be interfering. Try **disabling other extensions** temporarily, then retry.
331
281
  - **Empty data returns or 'Unauthorized' error**
332
282
  - 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.
333
283
  - **Node API errors**
@@ -336,15 +286,6 @@ npx vitest run tests/e2e/ # E2E tests
336
286
  - Check daemon status: `curl localhost:19825/status`
337
287
  - View extension logs: `curl localhost:19825/logs`
338
288
 
339
- ## Releasing New Versions
340
-
341
- ```bash
342
- npm version patch # 0.1.0 → 0.1.1
343
- npm version minor # 0.1.0 → 0.2.0
344
- git push --follow-tags
345
- ```
346
-
347
- The CI will automatically build, create a GitHub release, and publish to npm.
348
289
 
349
290
  ## Star History
350
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 setup` 检查 Browser Bridge 连通性;`opencli doctor` 诊断 daemon、扩展和浏览器连接状态
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
- **方式二:从 npm 包加载**
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
 
@@ -311,6 +274,8 @@ opencli cascade https://api.example.com/data
311
274
 
312
275
  - **"Extension not connected" 报错**
313
276
  - 确保你当前的 Chrome 已安装且**开启了** opencli Browser Bridge 扩展(在 `chrome://extensions` 中检查)。
277
+ - **"attach failed: Cannot access a chrome-extension:// URL" 报错**
278
+ - 其他 Chrome 扩展(如 youmind、New Tab Override 或 AI 助手类扩展)可能产生冲突。请尝试**暂时禁用其他扩展**后重试。
314
279
  - **返回空数据,或者报错 "Unauthorized"**
315
280
  - Chrome 里的登录态可能已经过期。请打开当前 Chrome 页面,在新标签页重新手工登录或刷新该页面。
316
281
  - **Node API 错误 (如 parseArgs, fs 等)**
@@ -319,15 +284,6 @@ opencli cascade https://api.example.com/data
319
284
  - 检查 daemon 状态:`curl localhost:19825/status`
320
285
  - 查看扩展日志:`curl localhost:19825/logs`
321
286
 
322
- ## 版本发布
323
-
324
- ```bash
325
- npm version patch # 0.1.0 → 0.1.1
326
- npm version minor # 0.1.0 → 0.2.0
327
-
328
- # 推送 tag,GitHub Actions 将自动执行发版和 npm 发布
329
- git push --follow-tags
330
- ```
331
287
 
332
288
  ## Star History
333
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 setup # Interactive Browser Bridge setup and connectivity check
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, validate, verify, help)
29
- │ └── output-formats.test.ts # 输出格式(json/yaml/csv/md)
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
- ├── *.test.ts # 单元测试(已有 8 个)
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
- ### 单元测试(8 个文件)
46
+ ### 单元测试(31 个文件)
47
47
 
48
- | 文件 | 覆盖内容 |
48
+ | 领域 | 文件 |
49
49
  |---|---|
50
- | `browser.test.ts` | JSON-RPC、tab 管理、extension/standalone 模式切换 |
51
- | `engine.test.ts` | 命令发现与执行 |
52
- | `registry.test.ts` | 命令注册与策略分配 |
53
- | `output.test.ts` | 输出格式渲染 |
54
- | `doctor.test.ts` | Token 诊断 |
55
- | `coupang.test.ts` | 数据归一化 |
56
- | `pipeline/template.test.ts` | 模板表达式求值 |
57
- | `pipeline/transform.test.ts` | 数据变换步骤 |
58
-
59
- ### E2E 测试(~52 个用例)
60
-
61
- | 文件 | 覆盖站点/功能 | 测试数 |
62
- |---|---|---|
63
- | `public-commands.test.ts` | hackernews/top, v2ex/hot, v2ex/latest, v2ex/topic | 5 |
64
- | `browser-public.test.ts` | bbc, bilibili×3, weibo, zhihu×2, reddit×2, twitter, xueqiu×2, reuters, youtube, smzdm, boss, ctrip, coupang, xiaohongshu, yahoo-finance, v2ex/daily | 21 |
65
- | `browser-auth.test.ts` | bilibili/me,dynamic,favorite,history,following + twitter/bookmarks,timeline,notifications + v2ex/me,notifications + xueqiu/feed,watchlist + xiaohongshu/feed,notifications | 14 |
66
- | `management.test.ts` | list×5 格式, validate×3 级别, verify, --version, --help, unknown cmd | 12 |
67
- | `output-formats.test.ts` | json, yaml, csv, md 格式验证 | 5 |
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
- 公开 API 可用性(hackernews, v2ex×2, v2ex/topic)+ 全站点注册完整性检查。
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
- # 全部测试(单元 + E2E)
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
- - `browser-public.test.ts` 使用 `tryBrowserCommand()`,站点反爬导致空数据时 warn + pass
110
- - `browser-auth.test.ts` 验证 **graceful failure**(不 crash 不 hang 即通过)
111
- - 如需测试完整登录态,保持 Chrome 登录态并安装 Browser Bridge 扩展,手动跑对应测试
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. **无需额外操作**:`validate` 测试会自动覆盖 YAML 结构验证
120
- 2. 根据 adapter 类型,在对应文件加一个 `it()` block
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
- `src/` 下对应位置创建 `*.test.ts`。
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
- | **build** | push/PR to main,dev | typecheck + build |
178
- | **unit-test** | push/PR to main,dev | 单元测试,2 shard 并行 |
179
- | **smoke-test** | 每周一 08:00 UTC / 手动 | xvfb + real Chrome,外部 API 健康检查 |
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(E2E 测试)
198
+ ### `e2e-headed.yml`
182
199
 
183
200
  | Job | 触发条件 | 内容 |
184
201
  |---|---|---|
185
- | **e2e-headed** | push/PR to main,dev | xvfb + real Chrome,全部 E2E 测试 |
202
+ | `e2e-headed` | push/PR `main`,`dev`,或手动触发 | 安装真实 Chrome,`xvfb-run` 执行 `tests/e2e/` |
186
203
 
187
- E2E 使用 `browser-actions/setup-chrome` 安装真实 Chrome,配合 `xvfb-run` 提供虚拟显示器,以 headed 模式运行浏览器。
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
- | 扩展未安装 | CLI 报错提示安装 | 需要安装 Browser Bridge 扩展 |
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
- GitHub Actions 美国 runner 上,部分站点因地域限制或登录要求返回空数据。E2E 测试对这些站点使用 warn + pass 策略,不影响 CI 绿灯。
241
+ GitHub Actions 的美国 runner 上,部分站点会因为地域限制、登录要求或反爬而返回空数据。当前 E2E 对这些场景采用 warn + pass 策略,避免偶发站点限制把整条 CI 打红。
224
242
 
225
- | 站点 | CI 状态 | 限制原因 |
243
+ | 站点 | CI 表现 | 常见原因 |
226
244
  |---|---|---|
227
- | hackernews, bbc, v2ex | 返回数据 | 无限制 |
228
- | yahoo-finance | 返回数据 | 无限制 |
229
- | bilibili, zhihu, weibo, xiaohongshu | ⚠️ 空数据 | 地域限制(中国站点) |
230
- | reddit, twitter, youtube | ⚠️ 空数据 | 需登录或 cookie |
231
- | smzdm, boss, ctrip, coupang, xueqiu | ⚠️ 空数据 | 地域限制 / 需登录 |
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
- > 使用 self-hosted runner(国内服务器)可解决地域限制问题。
251
+ > 如果需要更稳定的浏览器 E2E 结果,优先使用具备目标站点网络可达性的 self-hosted runner
@@ -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
- // Post-load settle: SPA frameworks need extra time to render after load event
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 settleMs = options?.settleMs ?? 1000;
151
- await new Promise(resolve => setTimeout(resolve, settleMs));
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`, { signal: controller.signal });
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`, { signal: controller.signal });
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
  });
@@ -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
+ }