@jackwener/opencli 0.6.1 → 0.6.3

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.
Files changed (46) hide show
  1. package/.github/actions/setup-chrome/action.yml +26 -0
  2. package/.github/workflows/ci.yml +59 -3
  3. package/.github/workflows/e2e-headed.yml +37 -0
  4. package/README.md +20 -1
  5. package/README.zh-CN.md +1 -1
  6. package/TESTING.md +233 -0
  7. package/dist/bilibili.js +2 -2
  8. package/dist/browser.d.ts +1 -1
  9. package/dist/browser.js +12 -3
  10. package/dist/browser.test.js +56 -16
  11. package/dist/cli-manifest.json +39 -0
  12. package/dist/clis/boss/detail.d.ts +1 -0
  13. package/dist/clis/boss/detail.js +104 -0
  14. package/dist/clis/boss/search.js +2 -1
  15. package/dist/interceptor.test.d.ts +4 -0
  16. package/dist/interceptor.test.js +81 -0
  17. package/dist/output.test.d.ts +3 -0
  18. package/dist/output.test.js +60 -0
  19. package/dist/pipeline/executor.js +0 -6
  20. package/dist/pipeline/executor.test.d.ts +4 -0
  21. package/dist/pipeline/executor.test.js +145 -0
  22. package/dist/pipeline/steps/fetch.js +4 -3
  23. package/dist/registry.d.ts +2 -2
  24. package/package.json +4 -4
  25. package/src/bilibili.ts +2 -2
  26. package/src/browser.test.ts +54 -16
  27. package/src/browser.ts +11 -3
  28. package/src/clis/boss/detail.ts +115 -0
  29. package/src/clis/boss/search.ts +2 -1
  30. package/src/clis/twitter/notifications.ts +1 -1
  31. package/src/engine.ts +2 -2
  32. package/src/interceptor.test.ts +94 -0
  33. package/src/output.test.ts +69 -4
  34. package/src/pipeline/executor.test.ts +161 -0
  35. package/src/pipeline/executor.ts +0 -5
  36. package/src/pipeline/steps/fetch.ts +4 -3
  37. package/src/registry.ts +2 -2
  38. package/tests/e2e/browser-auth.test.ts +90 -0
  39. package/tests/e2e/browser-public.test.ts +169 -0
  40. package/tests/e2e/helpers.ts +63 -0
  41. package/tests/e2e/management.test.ts +106 -0
  42. package/tests/e2e/output-formats.test.ts +48 -0
  43. package/tests/e2e/public-commands.test.ts +56 -0
  44. package/tests/smoke/api-health.test.ts +72 -0
  45. package/tsconfig.json +1 -0
  46. package/vitest.config.ts +1 -1
@@ -0,0 +1,26 @@
1
+ name: Setup Chrome + xvfb
2
+ description: Install real Chrome and xvfb virtual display for headed browser testing
3
+
4
+ outputs:
5
+ chrome-path:
6
+ description: Path to the installed Chrome binary
7
+ value: ${{ steps.setup-chrome.outputs.chrome-path }}
8
+
9
+ runs:
10
+ using: composite
11
+ steps:
12
+ - name: Install real Chrome (stable)
13
+ uses: browser-actions/setup-chrome@v1
14
+ id: setup-chrome
15
+ with:
16
+ chrome-version: stable
17
+
18
+ - name: Verify Chrome installation
19
+ shell: bash
20
+ run: |
21
+ echo "Chrome path: ${{ steps.setup-chrome.outputs.chrome-path }}"
22
+ ${{ steps.setup-chrome.outputs.chrome-path }} --version
23
+
24
+ - name: Install xvfb for headed mode
25
+ shell: bash
26
+ run: sudo apt-get install -y xvfb
@@ -2,12 +2,16 @@ name: CI
2
2
 
3
3
  on:
4
4
  push:
5
- branches: [main]
5
+ branches: [main, dev]
6
6
  pull_request:
7
- branches: [main]
7
+ branches: [main, dev]
8
+ schedule:
9
+ - cron: '0 8 * * 1' # Weekly Monday 08:00 UTC — smoke tests
10
+ workflow_dispatch:
8
11
 
9
12
  jobs:
10
- check:
13
+ # ── Fast gate: typecheck + build ──
14
+ build:
11
15
  runs-on: ubuntu-latest
12
16
  steps:
13
17
  - uses: actions/checkout@v4
@@ -15,6 +19,7 @@ jobs:
15
19
  - uses: actions/setup-node@v4
16
20
  with:
17
21
  node-version: '22'
22
+ cache: 'npm'
18
23
 
19
24
  - name: Install dependencies
20
25
  run: npm ci
@@ -24,3 +29,54 @@ jobs:
24
29
 
25
30
  - name: Build
26
31
  run: npm run build
32
+
33
+ # ── Unit tests (vitest shard) ──
34
+ unit-test:
35
+ runs-on: ubuntu-latest
36
+ strategy:
37
+ matrix:
38
+ shard: [1, 2]
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+
42
+ - uses: actions/setup-node@v4
43
+ with:
44
+ node-version: '22'
45
+ cache: 'npm'
46
+
47
+ - name: Install dependencies
48
+ run: npm ci
49
+
50
+ - name: Run unit tests (shard ${{ matrix.shard }}/2)
51
+ run: npx vitest run src/ --reporter=verbose --shard=${{ matrix.shard }}/2
52
+
53
+ # ── Smoke tests (scheduled / manual only) ──
54
+ smoke-test:
55
+ if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
56
+ needs: build
57
+ runs-on: ubuntu-latest
58
+ steps:
59
+ - uses: actions/checkout@v4
60
+
61
+ - uses: actions/setup-node@v4
62
+ with:
63
+ node-version: '22'
64
+ cache: 'npm'
65
+
66
+ - name: Install dependencies
67
+ run: npm ci
68
+
69
+ - name: Setup Chrome + xvfb
70
+ uses: ./.github/actions/setup-chrome
71
+ id: setup-chrome
72
+
73
+ - name: Build
74
+ run: npm run build
75
+
76
+ - name: Run smoke tests
77
+ run: |
78
+ xvfb-run --auto-servernum --server-args="-screen 0 1280x720x24" \
79
+ npx vitest run tests/smoke/ --reporter=verbose
80
+ env:
81
+ OPENCLI_BROWSER_EXECUTABLE_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}
82
+ timeout-minutes: 15
@@ -0,0 +1,37 @@
1
+ name: E2E Headed Chrome
2
+
3
+ on:
4
+ push:
5
+ branches: [main, dev]
6
+ pull_request:
7
+ branches: [main, dev]
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ e2e-headed:
12
+ runs-on: ubuntu-latest
13
+ timeout-minutes: 20
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: actions/setup-node@v4
18
+ with:
19
+ node-version: '22'
20
+ cache: 'npm'
21
+
22
+ - name: Install dependencies
23
+ run: npm ci
24
+
25
+ - name: Setup Chrome + xvfb
26
+ uses: ./.github/actions/setup-chrome
27
+ id: setup-chrome
28
+
29
+ - name: Build
30
+ run: npm run build
31
+
32
+ - name: Run E2E tests (headed Chrome + xvfb)
33
+ run: |
34
+ xvfb-run --auto-servernum --server-args="-screen 0 1280x720x24" \
35
+ npx vitest run tests/e2e/ --reporter=verbose
36
+ env:
37
+ OPENCLI_BROWSER_EXECUTABLE_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}
package/README.md CHANGED
@@ -21,6 +21,7 @@ A CLI tool that turns **any website** into a command-line interface — bilibili
21
21
  - [Built-in Commands](#built-in-commands)
22
22
  - [Output Formats](#output-formats)
23
23
  - [For AI Agents (Developer Guide)](#for-ai-agents-developer-guide)
24
+ - [Testing](#testing)
24
25
  - [Troubleshooting](#troubleshooting)
25
26
  - [Releasing New Versions](#releasing-new-versions)
26
27
  - [License](#license)
@@ -138,7 +139,7 @@ npm install -g @jackwener/opencli@latest
138
139
  | **twitter** | `trending` `bookmarks` `profile` `search` `timeline` `following` `followers` `notifications` `post` `reply` `delete` `like` | 🔐 Browser |
139
140
  | **reddit** | `hot` `frontpage` `search` `subreddit` | 🔐 Browser |
140
141
  | **weibo** | `hot` | 🔐 Browser |
141
- | **boss** | `search` | 🔐 Browser |
142
+ | **boss** | `search` `detail` | 🔐 Browser |
142
143
  | **coupang** | `search` `add-to-cart` | 🔐 Browser |
143
144
  | **youtube** | `search` | 🔐 Browser |
144
145
  | **yahoo-finance** | `quote` | 🔐 Browser |
@@ -189,6 +190,24 @@ opencli cascade https://api.example.com/data
189
190
 
190
191
  Explore outputs to `.opencli/explore/<site>/` (manifest.json, endpoints.json, capabilities.json, auth.json).
191
192
 
193
+ ## Testing
194
+
195
+ See **[TESTING.md](./TESTING.md)** for the full testing guide, including:
196
+
197
+ - Current test coverage (unit + ~52 E2E tests across all 18 sites)
198
+ - How to run tests locally
199
+ - How to add tests when creating new adapters
200
+ - CI/CD pipeline with sharding
201
+ - Headless browser mode (`OPENCLI_HEADLESS=1`)
202
+
203
+ ```bash
204
+ # Quick start
205
+ npm run build
206
+ npx vitest run # All tests
207
+ npx vitest run src/ # Unit tests only
208
+ npx vitest run tests/e2e/ # E2E tests
209
+ ```
210
+
192
211
  ## Troubleshooting
193
212
 
194
213
  - **"Failed to connect to Playwright MCP Bridge"**
package/README.zh-CN.md CHANGED
@@ -138,7 +138,7 @@ npm install -g @jackwener/opencli@latest
138
138
  | **twitter** | `trending` `bookmarks` `profile` `search` `timeline` `following` `followers` `notifications` `post` `reply` `delete` `like` | 🔐 浏览器 |
139
139
  | **reddit** | `hot` `frontpage` `search` `subreddit` | 🔐 浏览器 |
140
140
  | **weibo** | `hot` | 🔐 浏览器 |
141
- | **boss** | `search` | 🔐 浏览器 |
141
+ | **boss** | `search` `detail` | 🔐 浏览器 |
142
142
  | **coupang** | `search` `add-to-cart` | 🔐 浏览器 |
143
143
  | **youtube** | `search` | 🔐 浏览器 |
144
144
  | **yahoo-finance** | `quote` | 🔐 浏览器 |
package/TESTING.md ADDED
@@ -0,0 +1,233 @@
1
+ # Testing Guide
2
+
3
+ > 面向开发者和 AI Agent 的测试参考手册。
4
+
5
+ ## 目录
6
+
7
+ - [测试架构](#测试架构)
8
+ - [当前覆盖范围](#当前覆盖范围)
9
+ - [本地运行测试](#本地运行测试)
10
+ - [如何添加新测试](#如何添加新测试)
11
+ - [CI/CD 流水线](#cicd-流水线)
12
+ - [浏览器模式](#浏览器模式)
13
+ - [站点兼容性](#站点兼容性)
14
+
15
+ ---
16
+
17
+ ## 测试架构
18
+
19
+ 测试分为三层,全部使用 **vitest** 运行:
20
+
21
+ ```
22
+ tests/
23
+ ├── e2e/ # E2E 集成测试(子进程运行真实 CLI)
24
+ │ ├── helpers.ts # runCli() 共享工具
25
+ │ ├── public-commands.test.ts # 公开 API 命令(无需浏览器)
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 可用性检测
32
+ src/
33
+ ├── *.test.ts # 单元测试(已有 8 个)
34
+ ```
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 健康 |
41
+
42
+ ---
43
+
44
+ ## 当前覆盖范围
45
+
46
+ ### 单元测试(8 个文件)
47
+
48
+ | 文件 | 覆盖内容 |
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 |
68
+
69
+ ### 烟雾测试
70
+
71
+ 公开 API 可用性(hackernews, v2ex×2, v2ex/topic)+ 全站点注册完整性检查。
72
+
73
+ ---
74
+
75
+ ## 本地运行测试
76
+
77
+ ### 前置条件
78
+
79
+ ```bash
80
+ npm ci # 安装依赖
81
+ npm run build # 编译(E2E 测试需要 dist/main.js)
82
+ ```
83
+
84
+ ### 运行命令
85
+
86
+ ```bash
87
+ # 全部单元测试
88
+ npx vitest run src/
89
+
90
+ # 全部 E2E 测试(会真实调用外部 API)
91
+ npx vitest run tests/e2e/
92
+
93
+ # 单个测试文件
94
+ npx vitest run tests/e2e/management.test.ts
95
+
96
+ # 全部测试(单元 + E2E)
97
+ npx vitest run
98
+
99
+ # 烟雾测试
100
+ npx vitest run tests/smoke/
101
+
102
+ # watch 模式(开发时推荐)
103
+ npx vitest src/
104
+ ```
105
+
106
+ ### 浏览器命令本地测试须知
107
+
108
+ - 无 `PLAYWRIGHT_MCP_EXTENSION_TOKEN` 时,opencli 自动启动一个独立浏览器实例
109
+ - `browser-public.test.ts` 使用 `tryBrowserCommand()`,站点反爬导致空数据时 warn + pass
110
+ - `browser-auth.test.ts` 验证 **graceful failure**(不 crash 不 hang 即通过)
111
+ - 如需测试完整登录态,保持 Chrome 登录态 + 设置 `PLAYWRIGHT_MCP_EXTENSION_TOKEN`,手动跑对应测试
112
+
113
+ ---
114
+
115
+ ## 如何添加新测试
116
+
117
+ ### 新增 YAML Adapter(如 `src/clis/producthunt/trending.yaml`)
118
+
119
+ 1. **无需额外操作**:`validate` 测试会自动覆盖 YAML 结构验证
120
+ 2. 根据 adapter 类型,在对应文件加一个 `it()` block:
121
+
122
+ ```typescript
123
+ // 如果 browser: false(公开 API)→ tests/e2e/public-commands.test.ts
124
+ it('producthunt trending returns data', async () => {
125
+ const { stdout, code } = await runCli(['producthunt', 'trending', '--limit', '3', '-f', 'json']);
126
+ expect(code).toBe(0);
127
+ const data = parseJsonOutput(stdout);
128
+ expect(Array.isArray(data)).toBe(true);
129
+ expect(data.length).toBeGreaterThanOrEqual(1);
130
+ expect(data[0]).toHaveProperty('title');
131
+ }, 30_000);
132
+ ```
133
+
134
+ ```typescript
135
+ // 如果 browser: true 但可公开访问 → tests/e2e/browser-public.test.ts
136
+ it('producthunt trending returns data', async () => {
137
+ const data = await tryBrowserCommand(['producthunt', 'trending', '--limit', '3', '-f', 'json']);
138
+ expectDataOrSkip(data, 'producthunt trending');
139
+ }, 60_000);
140
+ ```
141
+
142
+ ```typescript
143
+ // 如果 browser: true 且需登录 → tests/e2e/browser-auth.test.ts
144
+ it('producthunt me fails gracefully without login', async () => {
145
+ await expectGracefulAuthFailure(['producthunt', 'me', '-f', 'json'], 'producthunt me');
146
+ }, 60_000);
147
+ ```
148
+
149
+ ### 新增管理命令(如 `opencli export`)
150
+
151
+ 在 `tests/e2e/management.test.ts` 添加测试。
152
+
153
+ ### 新增内部模块
154
+
155
+ 在 `src/` 下对应位置创建 `*.test.ts`。
156
+
157
+ ### 决策流程图
158
+
159
+ ```
160
+ 新增功能 → 是内部模块? → 是 → src/ 下加 *.test.ts
161
+ ↓ 否
162
+ 是 CLI 命令? → browser: false? → tests/e2e/public-commands.test.ts
163
+ ↓ true
164
+ 公开数据? → tests/e2e/browser-public.test.ts
165
+ ↓ 需登录
166
+ tests/e2e/browser-auth.test.ts
167
+ ```
168
+
169
+ ---
170
+
171
+ ## CI/CD 流水线
172
+
173
+ ### ci.yml(主流水线)
174
+
175
+ | Job | 触发条件 | 内容 |
176
+ |---|---|---|
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 健康检查 |
180
+
181
+ ### e2e-headed.yml(E2E 测试)
182
+
183
+ | Job | 触发条件 | 内容 |
184
+ |---|---|---|
185
+ | **e2e-headed** | push/PR to main,dev | xvfb + real Chrome,全部 E2E 测试 |
186
+
187
+ E2E 使用 `browser-actions/setup-chrome` 安装真实 Chrome,配合 `xvfb-run` 提供虚拟显示器,以 headed 模式运行浏览器。
188
+
189
+ ### Sharding
190
+
191
+ 单元测试使用 vitest 内置 shard:
192
+
193
+ ```yaml
194
+ strategy:
195
+ matrix:
196
+ shard: [1, 2]
197
+ steps:
198
+ - run: npx vitest run src/ --shard=${{ matrix.shard }}/2
199
+ ```
200
+
201
+ ---
202
+
203
+ ## 浏览器模式
204
+
205
+ opencli 根据 `PLAYWRIGHT_MCP_EXTENSION_TOKEN` 环境变量自动选择模式:
206
+
207
+ | 条件 | 模式 | MCP 参数 | 使用场景 |
208
+ |---|---|---|---|
209
+ | Token 已设置 | Extension 模式 | `--extension` | 本地用户,连接已登录的 Chrome |
210
+ | Token 未设置 | Standalone 模式 | (无特殊 flag) | CI 或无扩展环境,自启浏览器 |
211
+
212
+ CI 中使用 `OPENCLI_BROWSER_EXECUTABLE_PATH` 指定真实 Chrome 路径:
213
+
214
+ ```yaml
215
+ env:
216
+ OPENCLI_BROWSER_EXECUTABLE_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}
217
+ ```
218
+
219
+ ---
220
+
221
+ ## 站点兼容性
222
+
223
+ 在 GitHub Actions 美国 runner 上,部分站点因地域限制或登录要求返回空数据。E2E 测试对这些站点使用 warn + pass 策略,不影响 CI 绿灯。
224
+
225
+ | 站点 | CI 状态 | 限制原因 |
226
+ |---|---|---|
227
+ | hackernews, bbc, v2ex | ✅ 返回数据 | 无限制 |
228
+ | yahoo-finance | ✅ 返回数据 | 无限制 |
229
+ | bilibili, zhihu, weibo, xiaohongshu | ⚠️ 空数据 | 地域限制(中国站点) |
230
+ | reddit, twitter, youtube | ⚠️ 空数据 | 需登录或 cookie |
231
+ | smzdm, boss, ctrip, coupang, xueqiu | ⚠️ 空数据 | 地域限制 / 需登录 |
232
+
233
+ > 使用 self-hosted runner(国内服务器)可解决地域限制问题。
package/dist/bilibili.js CHANGED
@@ -63,10 +63,10 @@ export async function apiGet(page, path, opts = {}) {
63
63
  return fetchJson(page, url);
64
64
  }
65
65
  export async function fetchJson(page, url) {
66
- const escapedUrl = url.replace(/"/g, '\\"');
66
+ const urlJs = JSON.stringify(url);
67
67
  return page.evaluate(`
68
68
  async () => {
69
- const res = await fetch("${escapedUrl}", { credentials: "include" });
69
+ const res = await fetch(${urlJs}, { credentials: "include" });
70
70
  return await res.json();
71
71
  }
72
72
  `);
package/dist/browser.d.ts CHANGED
@@ -50,7 +50,7 @@ export declare class Page implements IPage {
50
50
  selectTab(index: number): Promise<void>;
51
51
  networkRequests(includeStatic?: boolean): Promise<any>;
52
52
  consoleMessages(level?: string): Promise<any>;
53
- scroll(direction?: string, amount?: number): Promise<void>;
53
+ scroll(direction?: string, _amount?: number): Promise<void>;
54
54
  autoScroll(options?: {
55
55
  times?: number;
56
56
  delayMs?: number;
package/dist/browser.js CHANGED
@@ -169,7 +169,7 @@ export class Page {
169
169
  async consoleMessages(level = 'info') {
170
170
  return this.call('tools/call', { name: 'browser_console_messages', arguments: { level } });
171
171
  }
172
- async scroll(direction = 'down', amount = 500) {
172
+ async scroll(direction = 'down', _amount = 500) {
173
173
  await this.call('tools/call', { name: 'browser_press_key', arguments: { key: direction === 'down' ? 'PageDown' : 'PageUp' } });
174
174
  }
175
175
  async autoScroll(options = {}) {
@@ -303,6 +303,7 @@ export class PlaywrightMCP {
303
303
  return new Promise((resolve, reject) => {
304
304
  const isDebug = process.env.DEBUG?.includes('opencli:mcp');
305
305
  const debugLog = (msg) => isDebug && console.error(`[opencli:mcp] ${msg}`);
306
+ const useExtension = !!process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
306
307
  const extensionToken = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
307
308
  const tokenFingerprint = getTokenFingerprint(extensionToken);
308
309
  let stderrBuffer = '';
@@ -344,7 +345,9 @@ export class PlaywrightMCP {
344
345
  executablePath: process.env.OPENCLI_BROWSER_EXECUTABLE_PATH,
345
346
  });
346
347
  if (process.env.OPENCLI_VERBOSE) {
347
- console.error(`[opencli] Extension token: ${extensionToken ? `configured (fingerprint ${tokenFingerprint})` : 'missing'}`);
348
+ console.error(`[opencli] Mode: ${useExtension ? 'extension' : 'standalone'}`);
349
+ if (useExtension)
350
+ console.error(`[opencli] Extension token: fingerprint ${tokenFingerprint}`);
348
351
  }
349
352
  debugLog(`Spawning node ${mcpArgs.join(' ')}`);
350
353
  this._proc = spawn('node', mcpArgs, {
@@ -559,7 +562,13 @@ function appendLimited(current, chunk, limit) {
559
562
  return next.slice(-limit);
560
563
  }
561
564
  function buildMcpArgs(input) {
562
- const args = [input.mcpPath, '--extension'];
565
+ const args = [input.mcpPath];
566
+ if (!process.env.CI) {
567
+ // Local: always connect to user's running Chrome via MCP Bridge extension
568
+ args.push('--extension');
569
+ }
570
+ // CI: standalone mode — @playwright/mcp launches its own browser (headed by default).
571
+ // xvfb provides a virtual display for headed mode in GitHub Actions.
563
572
  if (input.executablePath) {
564
573
  args.push('--executable-path', input.executablePath);
565
574
  }
@@ -34,22 +34,62 @@ describe('browser helpers', () => {
34
34
  it('keeps only the tail of stderr buffers', () => {
35
35
  expect(__test__.appendLimited('12345', '67890', 8)).toBe('34567890');
36
36
  });
37
- it('builds Playwright MCP args with kebab-case executable path', () => {
38
- expect(__test__.buildMcpArgs({
39
- mcpPath: '/tmp/cli.js',
40
- executablePath: '/mnt/c/Program Files/Google/Chrome/Application/chrome.exe',
41
- })).toEqual([
42
- '/tmp/cli.js',
43
- '--extension',
44
- '--executable-path',
45
- '/mnt/c/Program Files/Google/Chrome/Application/chrome.exe',
46
- ]);
47
- expect(__test__.buildMcpArgs({
48
- mcpPath: '/tmp/cli.js',
49
- })).toEqual([
50
- '/tmp/cli.js',
51
- '--extension',
52
- ]);
37
+ it('builds extension MCP args in local mode (no CI)', () => {
38
+ const savedCI = process.env.CI;
39
+ delete process.env.CI;
40
+ try {
41
+ expect(__test__.buildMcpArgs({
42
+ mcpPath: '/tmp/cli.js',
43
+ executablePath: '/mnt/c/Program Files/Google/Chrome/Application/chrome.exe',
44
+ })).toEqual([
45
+ '/tmp/cli.js',
46
+ '--extension',
47
+ '--executable-path',
48
+ '/mnt/c/Program Files/Google/Chrome/Application/chrome.exe',
49
+ ]);
50
+ expect(__test__.buildMcpArgs({
51
+ mcpPath: '/tmp/cli.js',
52
+ })).toEqual([
53
+ '/tmp/cli.js',
54
+ '--extension',
55
+ ]);
56
+ }
57
+ finally {
58
+ if (savedCI !== undefined) {
59
+ process.env.CI = savedCI;
60
+ }
61
+ else {
62
+ delete process.env.CI;
63
+ }
64
+ }
65
+ });
66
+ it('builds standalone MCP args in CI mode', () => {
67
+ const savedCI = process.env.CI;
68
+ process.env.CI = 'true';
69
+ try {
70
+ // CI mode: no --extension — browser launches in standalone headed mode
71
+ expect(__test__.buildMcpArgs({
72
+ mcpPath: '/tmp/cli.js',
73
+ })).toEqual([
74
+ '/tmp/cli.js',
75
+ ]);
76
+ expect(__test__.buildMcpArgs({
77
+ mcpPath: '/tmp/cli.js',
78
+ executablePath: '/usr/bin/chromium',
79
+ })).toEqual([
80
+ '/tmp/cli.js',
81
+ '--executable-path',
82
+ '/usr/bin/chromium',
83
+ ]);
84
+ }
85
+ finally {
86
+ if (savedCI !== undefined) {
87
+ process.env.CI = savedCI;
88
+ }
89
+ else {
90
+ delete process.env.CI;
91
+ }
92
+ }
53
93
  });
54
94
  it('times out slow promises', async () => {
55
95
  await expect(__test__.withTimeoutMs(new Promise(() => { }), 10, 'timeout')).rejects.toThrow('timeout');
@@ -392,6 +392,44 @@
392
392
  "url"
393
393
  ]
394
394
  },
395
+ {
396
+ "site": "boss",
397
+ "name": "detail",
398
+ "description": "BOSS直聘查看职位详情",
399
+ "strategy": "cookie",
400
+ "browser": true,
401
+ "args": [
402
+ {
403
+ "name": "security_id",
404
+ "type": "str",
405
+ "required": true,
406
+ "help": "Security ID from search results (securityId field)"
407
+ }
408
+ ],
409
+ "type": "ts",
410
+ "modulePath": "boss/detail.js",
411
+ "domain": "www.zhipin.com",
412
+ "columns": [
413
+ "name",
414
+ "salary",
415
+ "experience",
416
+ "degree",
417
+ "city",
418
+ "district",
419
+ "description",
420
+ "skills",
421
+ "welfare",
422
+ "boss_name",
423
+ "boss_title",
424
+ "active_time",
425
+ "company",
426
+ "industry",
427
+ "scale",
428
+ "stage",
429
+ "address",
430
+ "url"
431
+ ]
432
+ },
395
433
  {
396
434
  "site": "boss",
397
435
  "name": "search",
@@ -467,6 +505,7 @@
467
505
  "degree",
468
506
  "skills",
469
507
  "boss",
508
+ "security_id",
470
509
  "url"
471
510
  ]
472
511
  },
@@ -0,0 +1 @@
1
+ export {};