@jackwener/opencli 0.4.0 → 0.4.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/CLI-CREATOR.md CHANGED
@@ -3,6 +3,94 @@
3
3
  > 本文档教你(或 AI Agent)如何为 OpenCLI 添加一个新网站的命令。
4
4
  > 从零到发布,覆盖 API 发现、方案选择、适配器编写、测试验证全流程。
5
5
 
6
+ ---
7
+
8
+ ## ⚠️ AI Agent 开发者必读:用 Playwright MCP Bridge 探索
9
+
10
+ > [!CAUTION]
11
+ > **你(AI Agent)必须通过 Playwright MCP Bridge 打开浏览器去访问目标网站!**
12
+ > 不要只靠 `opencli explore` 命令或静态分析来发现 API。
13
+ > 你拥有 Playwright MCP 工具,必须主动用它们浏览网页、观察网络请求、模拟用户交互。
14
+
15
+ ### 为什么?
16
+
17
+ 很多 API 是**懒加载**的(用户必须点击某个按钮/标签才会触发网络请求)。字幕、评论、关注列表等深层数据不会在页面首次加载时出现在 Network 面板中。**如果你不主动去浏览和交互页面,你永远发现不了这些 API。**
18
+
19
+ ### AI Agent 探索工作流(必须遵循)
20
+
21
+ ```
22
+ Step 0: 用 Playwright MCP 打开浏览器
23
+
24
+ Step 1: 导航到目标页面,观察页面结构
25
+
26
+ Step 2: 查看 Network 请求(browser_network_requests)
27
+
28
+ Step 3: 模拟用户交互(点击按钮/标签/展开评论)
29
+
30
+ Step 4: 再次查看 Network,发现新触发的 API
31
+
32
+ Step 5: 分析 API 的请求参数、响应结构、鉴权方式
33
+
34
+ Step 6: 编写适配器代码
35
+ ```
36
+
37
+ ### 具体操作步骤
38
+
39
+ **Step 0: 打开浏览器**
40
+ ```
41
+ 工具: browser_navigate
42
+ URL: https://www.bilibili.com/video/BV1xxxxx
43
+ ```
44
+
45
+ **Step 1: 获取页面快照,了解页面结构**
46
+ ```
47
+ 工具: browser_snapshot
48
+ → 观察页面上有哪些可交互元素(按钮、标签、链接)
49
+ ```
50
+
51
+ **Step 2: 查看已有的网络请求**
52
+ ```
53
+ 工具: browser_network_requests
54
+ → 筛选出 JSON API 端点(忽略静态资源)
55
+ → 记录 URL pattern、请求头、响应结构
56
+ ```
57
+
58
+ **Step 3: 模拟用户交互发现深层 API**
59
+ ```
60
+ 工具: browser_click (点击"字幕"按钮、"评论"标签、"关注"链接等)
61
+ 工具: browser_wait_for (等待数据加载)
62
+ ```
63
+
64
+ **Step 4: 再次抓包,发现新 API**
65
+ ```
66
+ 工具: browser_network_requests
67
+ → 对比 Step 2,找出新触发的 API 端点
68
+ ```
69
+
70
+ **Step 5: 用 evaluate 测试 API 可行性**
71
+ ```
72
+ 工具: browser_evaluate
73
+ 代码: async () => {
74
+ const res = await fetch('https://api.bilibili.com/x/player/wbi/v2?bvid=BV1xxx&cid=123',
75
+ { credentials: 'include' });
76
+ return await res.json();
77
+ }
78
+ → 验证返回的数据结构和字段
79
+ → 如果返回空/403:检查是否需要签名(Wbi)或特殊 Header
80
+ ```
81
+
82
+ ### 常犯错误
83
+
84
+ | ❌ 错误做法 | ✅ 正确做法 |
85
+ |------------|------------|
86
+ | 只用 `opencli explore` 命令,等结果自动出来 | 用 MCP Bridge 打开浏览器,主动浏览页面 |
87
+ | 直接在代码里 `fetch(url)`,不看浏览器实际请求 | 先在浏览器中确认 API 可用,再写代码 |
88
+ | 页面打开后直接抓包,期望所有 API 都出现 | 模拟点击交互(展开评论/切换标签/加载更多) |
89
+ | 遇到 HTTP 200 但空数据就放弃 | 检查是否需要 Wbi 签名或 Cookie 鉴权 |
90
+ | 完全依赖 `__INITIAL_STATE__` 拿所有数据 | `__INITIAL_STATE__` 只有首屏数据,深层数据要调 API |
91
+
92
+ ---
93
+
6
94
  ## 核心流程
7
95
 
8
96
  ```
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  [![npm](https://img.shields.io/npm/v/@jackwener/opencli)](https://www.npmjs.com/package/@jackwener/opencli)
9
9
 
10
- A CLI tool that turns **any website** into a command-line interface. **46 commands** across **17 sites** — bilibili, zhihu, xiaohongshu, twitter, reddit, xueqiu, github, v2ex, hackernews, bbc, weibo, boss, yahoo-finance, reuters, smzdm, ctrip, youtube — powered by browser session reuse and AI-native discovery.
10
+ A CLI tool that turns **any website** into a command-line interface. **47 commands** across **17 sites** — bilibili, zhihu, xiaohongshu, twitter, reddit, xueqiu, github, v2ex, hackernews, bbc, weibo, boss, yahoo-finance, reuters, smzdm, ctrip, youtube — powered by browser session reuse and AI-native discovery.
11
11
 
12
12
  ## ✨ Highlights
13
13
 
@@ -82,7 +82,7 @@ Public API commands (`hackernews`, `github search`, `v2ex`) need no browser at a
82
82
 
83
83
  | Site | Commands | Mode |
84
84
  |------|----------|------|
85
- | **bilibili** | `hot` `search` `me` `favorite` `history` `feed` `user-videos` `subtitle` `dynamic` `ranking` | 🔐 Browser |
85
+ | **bilibili** | `hot` `search` `me` `favorite` `history` `feed` `user-videos` `subtitle` `dynamic` `ranking` `following` | 🔐 Browser |
86
86
  | **zhihu** | `hot` `search` `question` | 🔐 Browser |
87
87
  | **xiaohongshu** | `search` `notifications` `feed` `me` `user` | 🔐 Browser |
88
88
  | **xueqiu** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` | 🔐 Browser |
@@ -95,7 +95,7 @@ Public API commands (`hackernews`, `github search`, `v2ex`) need no browser at a
95
95
  | **reuters** | `search` | 🔐 Browser |
96
96
  | **smzdm** | `search` | 🔐 Browser |
97
97
  | **ctrip** | `search` | 🔐 Browser |
98
- | **github** | `trending` `search` | 🔐 / 🌐 |
98
+ | **github** | `search` | 🌐 Public |
99
99
  | **v2ex** | `hot` `latest` `topic` | 🌐 Public |
100
100
  | **hackernews** | `top` | 🌐 Public |
101
101
  | **bbc** | `news` | 🌐 Public |
package/README.zh-CN.md CHANGED
@@ -11,7 +11,7 @@ OpenCLI 通过 Chrome 浏览器 + [Playwright MCP Bridge](https://github.com/nic
11
11
 
12
12
  ## ✨ 亮点
13
13
 
14
- - 🌐 **46 个命令,17 个站点** — B站、知乎、小红书、Twitter、Reddit、雪球(xueqiu)、GitHub、V2EX、Hacker News、BBC、微博、BOSS直聘、Yahoo Finance、路透社、什么值得买、携程、YouTube
14
+ - 🌐 **47 个命令,17 个站点** — B站、知乎、小红书、Twitter、Reddit、雪球(xueqiu)、GitHub、V2EX、Hacker News、BBC、微博、BOSS直聘、Yahoo Finance、路透社、什么值得买、携程、YouTube
15
15
  - 🔐 **零风控** — 复用 Chrome 登录态,无需存储任何凭证
16
16
  - 🤖 **AI 原生** — `explore` 自动发现 API,`synthesize` 生成适配器,`cascade` 探测认证策略
17
17
  - 🚀 **动态加载引擎** — 只需将 `.ts` 或 `.yaml` 适配器放入 `clis/` 文件夹即可自动注册生效
@@ -83,7 +83,7 @@ npm install -g @jackwener/opencli@latest
83
83
 
84
84
  | 站点 | 命令 | 模式 |
85
85
  |------|------|------|
86
- | **bilibili** | `hot` `search` `me` `favorite` `history` `feed` `user-videos` `subtitle` `dynamic` `ranking` | 🔐 浏览器 |
86
+ | **bilibili** | `hot` `search` `me` `favorite` `history` `feed` `user-videos` `subtitle` `dynamic` `ranking` `following` | 🔐 浏览器 |
87
87
  | **zhihu** | `hot` `search` `question` | 🔐 浏览器 |
88
88
  | **xiaohongshu** | `search` `notifications` `feed` `me` `user` | 🔐 浏览器 |
89
89
  | **xueqiu** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` | 🔐 浏览器 |
@@ -96,7 +96,7 @@ npm install -g @jackwener/opencli@latest
96
96
  | **reuters** | `search` | 🔐 浏览器 |
97
97
  | **smzdm** | `search` | 🔐 浏览器 |
98
98
  | **ctrip** | `search` | 🔐 浏览器 |
99
- | **github** | `trending` `search` | 🔐 / 🌐 |
99
+ | **github** | `search` | 🌐 公共 API |
100
100
  | **v2ex** | `hot` `latest` `topic` | 🌐 公共 API |
101
101
  | **hackernews** | `top` | 🌐 公共 API |
102
102
  | **bbc** | `news` | 🌐 公共 API |
package/SKILL.md CHANGED
@@ -52,6 +52,7 @@ opencli bilibili user-videos --uid 12345 # 用户投稿
52
52
  opencli bilibili subtitle --bvid BV1xxx # 获取视频字幕 (支持 --lang zh-CN)
53
53
  opencli bilibili dynamic --limit 10 # 动态
54
54
  opencli bilibili ranking --limit 10 # 排行榜
55
+ opencli bilibili following --limit 20 # 我的关注列表 (支持 --uid 查看他人)
55
56
 
56
57
  # 知乎 (browser)
57
58
  opencli zhihu hot --limit 10 # 知乎热榜
@@ -70,9 +71,10 @@ opencli xueqiu hot-stock --limit 10 # 雪球热门股票榜
70
71
  opencli xueqiu stock --symbol SH600519 # 查看股票实时行情
71
72
  opencli xueqiu watchlist # 获取自选股/持仓列表
72
73
  opencli xueqiu feed # 我的关注 timeline
74
+ opencli xueqiu hot --limit 10 # 雪球热榜
75
+ opencli xueqiu search --keyword "特斯拉" # 搜索
73
76
 
74
- # GitHub (trending=browser, search=public)
75
- opencli github trending --limit 10 # GitHub Trending
77
+ # GitHub (public)
76
78
  opencli github search --keyword "cli" # 搜索仓库
77
79
 
78
80
  # Twitter/X (browser)
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build-time CLI manifest compiler.
4
+ *
5
+ * Scans all YAML/TS CLI definitions and pre-compiles them into a single
6
+ * manifest.json for instant cold-start registration (no runtime YAML parsing).
7
+ *
8
+ * Usage: npx tsx src/build-manifest.ts
9
+ * Output: dist/cli-manifest.json
10
+ */
11
+ export {};
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build-time CLI manifest compiler.
4
+ *
5
+ * Scans all YAML/TS CLI definitions and pre-compiles them into a single
6
+ * manifest.json for instant cold-start registration (no runtime YAML parsing).
7
+ *
8
+ * Usage: npx tsx src/build-manifest.ts
9
+ * Output: dist/cli-manifest.json
10
+ */
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import yaml from 'js-yaml';
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ const CLIS_DIR = path.resolve(__dirname, 'clis');
17
+ const OUTPUT = path.resolve(__dirname, '..', 'dist', 'cli-manifest.json');
18
+ function scanYaml(filePath, site) {
19
+ try {
20
+ const raw = fs.readFileSync(filePath, 'utf-8');
21
+ const def = yaml.load(raw);
22
+ if (!def || typeof def !== 'object')
23
+ return null;
24
+ const strategyStr = def.strategy ?? (def.browser === false ? 'public' : 'cookie');
25
+ const strategy = strategyStr.toUpperCase();
26
+ const browser = def.browser ?? (strategy !== 'PUBLIC');
27
+ const args = [];
28
+ if (def.args && typeof def.args === 'object') {
29
+ for (const [argName, argDef] of Object.entries(def.args)) {
30
+ args.push({
31
+ name: argName,
32
+ type: argDef?.type ?? 'str',
33
+ default: argDef?.default,
34
+ required: argDef?.required ?? false,
35
+ help: argDef?.description ?? argDef?.help ?? '',
36
+ choices: argDef?.choices,
37
+ });
38
+ }
39
+ }
40
+ return {
41
+ site: def.site ?? site,
42
+ name: def.name ?? path.basename(filePath, path.extname(filePath)),
43
+ description: def.description ?? '',
44
+ domain: def.domain,
45
+ strategy: strategy.toLowerCase(),
46
+ browser,
47
+ args,
48
+ columns: def.columns,
49
+ pipeline: def.pipeline,
50
+ timeout: def.timeout,
51
+ type: 'yaml',
52
+ };
53
+ }
54
+ catch (err) {
55
+ process.stderr.write(`Warning: failed to parse ${filePath}: ${err.message}\n`);
56
+ return null;
57
+ }
58
+ }
59
+ function scanTs(filePath, site) {
60
+ // TS adapters self-register via cli() at import time.
61
+ // We record their module path for lazy dynamic import.
62
+ const baseName = path.basename(filePath, path.extname(filePath));
63
+ const relativePath = `${site}/${baseName}.js`;
64
+ return {
65
+ site,
66
+ name: baseName,
67
+ description: '',
68
+ strategy: 'cookie',
69
+ browser: true,
70
+ args: [],
71
+ type: 'ts',
72
+ modulePath: relativePath,
73
+ };
74
+ }
75
+ // Main
76
+ const manifest = [];
77
+ if (fs.existsSync(CLIS_DIR)) {
78
+ for (const site of fs.readdirSync(CLIS_DIR)) {
79
+ const siteDir = path.join(CLIS_DIR, site);
80
+ if (!fs.statSync(siteDir).isDirectory())
81
+ continue;
82
+ for (const file of fs.readdirSync(siteDir)) {
83
+ const filePath = path.join(siteDir, file);
84
+ if (file.endsWith('.yaml') || file.endsWith('.yml')) {
85
+ const entry = scanYaml(filePath, site);
86
+ if (entry)
87
+ manifest.push(entry);
88
+ }
89
+ else if ((file.endsWith('.ts') && !file.endsWith('.d.ts') && file !== 'index.ts') ||
90
+ (file.endsWith('.js') && !file.endsWith('.d.js') && file !== 'index.js')) {
91
+ manifest.push(scanTs(filePath, site));
92
+ }
93
+ }
94
+ }
95
+ }
96
+ // Ensure output directory exists
97
+ fs.mkdirSync(path.dirname(OUTPUT), { recursive: true });
98
+ fs.writeFileSync(OUTPUT, JSON.stringify(manifest, null, 2));
99
+ const yamlCount = manifest.filter(e => e.type === 'yaml').length;
100
+ const tsCount = manifest.filter(e => e.type === 'ts').length;
101
+ console.log(`✅ Manifest compiled: ${manifest.length} entries (${yamlCount} YAML, ${tsCount} TS) → ${OUTPUT}`);