@jackwener/opencli 0.1.1 → 0.1.2

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 (103) hide show
  1. package/README.md +9 -2
  2. package/README.zh-CN.md +9 -1
  3. package/SKILL.md +24 -0
  4. package/dist/bilibili.d.ts +6 -5
  5. package/dist/browser.d.ts +2 -1
  6. package/dist/browser.js +9 -1
  7. package/dist/cascade.d.ts +3 -2
  8. package/dist/clis/bbc/news.js +42 -0
  9. package/dist/clis/boss/search.d.ts +1 -0
  10. package/dist/clis/boss/search.js +47 -0
  11. package/dist/clis/ctrip/search.d.ts +1 -0
  12. package/dist/clis/ctrip/search.js +62 -0
  13. package/dist/clis/index.d.ts +8 -0
  14. package/dist/clis/index.js +16 -0
  15. package/dist/clis/reuters/search.d.ts +1 -0
  16. package/dist/clis/reuters/search.js +52 -0
  17. package/dist/clis/smzdm/search.d.ts +1 -0
  18. package/dist/clis/smzdm/search.js +66 -0
  19. package/dist/clis/weibo/hot.d.ts +1 -0
  20. package/dist/clis/weibo/hot.js +41 -0
  21. package/dist/clis/yahoo-finance/quote.d.ts +1 -0
  22. package/dist/clis/yahoo-finance/quote.js +74 -0
  23. package/dist/clis/youtube/search.d.ts +1 -0
  24. package/dist/clis/youtube/search.js +60 -0
  25. package/dist/engine.d.ts +2 -1
  26. package/dist/explore.js +1 -1
  27. package/dist/generate.js +2 -1
  28. package/dist/main.js +6 -4
  29. package/dist/pipeline/executor.d.ts +9 -0
  30. package/dist/pipeline/executor.js +88 -0
  31. package/dist/pipeline/index.d.ts +5 -0
  32. package/dist/pipeline/index.js +5 -0
  33. package/dist/pipeline/steps/browser.d.ts +12 -0
  34. package/dist/pipeline/steps/browser.js +68 -0
  35. package/dist/pipeline/steps/fetch.d.ts +5 -0
  36. package/dist/pipeline/steps/fetch.js +50 -0
  37. package/dist/pipeline/steps/intercept.d.ts +5 -0
  38. package/dist/pipeline/steps/intercept.js +75 -0
  39. package/dist/pipeline/steps/tap.d.ts +12 -0
  40. package/dist/pipeline/steps/tap.js +130 -0
  41. package/dist/pipeline/steps/transform.d.ts +8 -0
  42. package/dist/pipeline/steps/transform.js +53 -0
  43. package/dist/pipeline/template.d.ts +16 -0
  44. package/dist/pipeline/template.js +115 -0
  45. package/dist/pipeline/template.test.d.ts +4 -0
  46. package/dist/pipeline/template.test.js +102 -0
  47. package/dist/pipeline/transform.test.d.ts +4 -0
  48. package/dist/pipeline/transform.test.js +90 -0
  49. package/dist/pipeline.d.ts +5 -7
  50. package/dist/pipeline.js +5 -549
  51. package/dist/registry.d.ts +3 -2
  52. package/dist/runtime.d.ts +2 -1
  53. package/dist/types.d.ts +27 -0
  54. package/dist/types.js +7 -0
  55. package/package.json +6 -3
  56. package/src/bilibili.ts +9 -7
  57. package/src/browser.ts +8 -2
  58. package/src/cascade.ts +3 -2
  59. package/src/clis/bbc/news.ts +42 -0
  60. package/src/clis/boss/search.ts +47 -0
  61. package/src/clis/ctrip/search.ts +62 -0
  62. package/src/clis/index.ts +24 -0
  63. package/src/clis/reuters/search.ts +52 -0
  64. package/src/clis/smzdm/search.ts +66 -0
  65. package/src/clis/weibo/hot.ts +41 -0
  66. package/src/clis/yahoo-finance/quote.ts +74 -0
  67. package/src/clis/youtube/search.ts +60 -0
  68. package/src/engine.ts +2 -1
  69. package/src/explore.ts +1 -1
  70. package/src/generate.ts +3 -1
  71. package/src/main.ts +7 -5
  72. package/src/pipeline/executor.ts +98 -0
  73. package/src/pipeline/index.ts +6 -0
  74. package/src/pipeline/steps/browser.ts +67 -0
  75. package/src/pipeline/steps/fetch.ts +60 -0
  76. package/src/pipeline/steps/intercept.ts +78 -0
  77. package/src/pipeline/steps/tap.ts +137 -0
  78. package/src/pipeline/steps/transform.ts +50 -0
  79. package/src/pipeline/template.test.ts +107 -0
  80. package/src/pipeline/template.ts +101 -0
  81. package/src/pipeline/transform.test.ts +107 -0
  82. package/src/pipeline.ts +5 -529
  83. package/src/registry.ts +4 -2
  84. package/src/runtime.ts +3 -1
  85. package/src/types.ts +23 -0
  86. package/vitest.config.ts +7 -0
  87. package/dist/clis/github/search.js +0 -20
  88. package/dist/clis/github/trending.yaml +0 -58
  89. package/dist/promote.d.ts +0 -1
  90. package/dist/promote.js +0 -3
  91. package/dist/register.d.ts +0 -2
  92. package/dist/register.js +0 -2
  93. package/dist/scaffold.d.ts +0 -2
  94. package/dist/scaffold.js +0 -2
  95. package/dist/smoke.d.ts +0 -2
  96. package/dist/smoke.js +0 -2
  97. package/src/clis/github/search.ts +0 -21
  98. package/src/clis/github/trending.yaml +0 -58
  99. package/src/promote.ts +0 -3
  100. package/src/register.ts +0 -2
  101. package/src/scaffold.ts +0 -2
  102. package/src/smoke.ts +0 -2
  103. /package/dist/clis/{github/search.d.ts → bbc/news.d.ts} +0 -0
package/README.md CHANGED
@@ -7,11 +7,10 @@
7
7
 
8
8
  [![npm](https://img.shields.io/npm/v/@jackwener/opencli)](https://www.npmjs.com/package/@jackwener/opencli)
9
9
 
10
- OpenCLI turns any website into a command-line tool by bridging your Chrome browser through [Playwright MCP](https://github.com/nichochar/playwright-mcp). No passwords stored, no tokens leaked it just rides your existing browser session.
10
+ A CLI tool that turns **any website** into a command-line interface. **28+ commands** across **16 sites** bilibili, zhihu, xiaohongshu, twitter, reddit, 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
 
14
- - 🌐 **25+ commands, 8 sites** — Bilibili, Zhihu, GitHub, Twitter/X, Reddit, V2EX, Xiaohongshu, Hacker News
15
14
  - 🔐 **Account-safe** — Reuses Chrome's logged-in state; your credentials never leave the browser
16
15
  - 🤖 **AI Agent ready** — `explore` discovers APIs, `synthesize` generates adapters, `cascade` finds auth strategies
17
16
  - 📝 **Declarative YAML** — Most adapters are ~30 lines of YAML pipeline
@@ -86,9 +85,17 @@ Public API commands (`hackernews`, `github search`, `v2ex`) need no browser at a
86
85
  | **xiaohongshu** | `search` `notifications` `feed` | 🔐 Browser |
87
86
  | **twitter** | `trending` | 🔐 Browser |
88
87
  | **reddit** | `hot` | 🔐 Browser |
88
+ | **weibo** | `hot` | 🔐 Browser |
89
+ | **boss** | `search` | 🔐 Browser |
90
+ | **youtube** | `search` | 🔐 Browser |
91
+ | **yahoo-finance** | `quote` | 🔐 Browser |
92
+ | **reuters** | `search` | 🔐 Browser |
93
+ | **smzdm** | `search` | 🔐 Browser |
94
+ | **ctrip** | `search` | 🔐 Browser |
89
95
  | **github** | `trending` `search` | 🔐 / 🌐 |
90
96
  | **v2ex** | `hot` `latest` `topic` | 🌐 Public |
91
97
  | **hackernews** | `top` | 🌐 Public |
98
+ | **bbc** | `news` | 🌐 Public |
92
99
 
93
100
  ## 🎨 Output Formats
94
101
 
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
- - 🌐 **25+ 命令,8 个站点** — B站、知乎、GitHub、Twitter/X、Reddit、V2EX、小红书、Hacker News
14
+ - 🌐 **28+ 命令,16 个站点** — B站、知乎、小红书、Twitter、Reddit、GitHub、V2EXHacker News、BBC、微博、BOSS直聘、Yahoo Finance、路透社、什么值得买、携程、YouTube
15
15
  - 🔐 **零风控** — 复用 Chrome 登录态,无需存储任何凭证
16
16
  - 🤖 **AI 原生** — `explore` 自动发现 API,`synthesize` 生成适配器,`cascade` 探测认证策略
17
17
  - 📝 **声明式 YAML** — 大部分适配器只需 ~30 行 YAML
@@ -86,9 +86,17 @@ npm install -g @jackwener/opencli@latest
86
86
  | **xiaohongshu** | `search` `notifications` `feed` | 🔐 浏览器 |
87
87
  | **twitter** | `trending` | 🔐 浏览器 |
88
88
  | **reddit** | `hot` | 🔐 浏览器 |
89
+ | **weibo** | `hot` | 🔐 浏览器 |
90
+ | **boss** | `search` | 🔐 浏览器 |
91
+ | **youtube** | `search` | 🔐 浏览器 |
92
+ | **yahoo-finance** | `quote` | 🔐 浏览器 |
93
+ | **reuters** | `search` | 🔐 浏览器 |
94
+ | **smzdm** | `search` | 🔐 浏览器 |
95
+ | **ctrip** | `search` | 🔐 浏览器 |
89
96
  | **github** | `trending` `search` | 🔐 / 🌐 |
90
97
  | **v2ex** | `hot` `latest` `topic` | 🌐 公共 API |
91
98
  | **hackernews** | `top` | 🌐 公共 API |
99
+ | **bbc** | `news` | 🌐 公共 API |
92
100
 
93
101
  ## 🎨 输出格式
94
102
 
package/SKILL.md CHANGED
@@ -78,6 +78,30 @@ opencli v2ex topic --id 1024 # 主题详情
78
78
 
79
79
  # Hacker News (public)
80
80
  opencli hackernews top --limit 10 # Top stories
81
+
82
+ # BBC (public)
83
+ opencli bbc news --limit 10 # BBC News RSS headlines
84
+
85
+ # 微博 (browser)
86
+ opencli weibo hot --limit 10 # 微博热搜
87
+
88
+ # BOSS直聘 (browser)
89
+ opencli boss search --query "AI agent" # 搜索职位
90
+
91
+ # YouTube (browser)
92
+ opencli youtube search --query "rust" # 搜索视频
93
+
94
+ # Yahoo Finance (browser)
95
+ opencli yahoo-finance quote --symbol AAPL # 股票行情
96
+
97
+ # Reuters (browser)
98
+ opencli reuters search --query "AI" # 路透社搜索
99
+
100
+ # 什么值得买 (browser)
101
+ opencli smzdm search --keyword "耳机" # 搜索好价
102
+
103
+ # 携程 (browser)
104
+ opencli ctrip search --query "三亚" # 搜索目的地
81
105
  ```
82
106
 
83
107
  ### Management Commands
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Bilibili shared helpers: WBI signing, authenticated fetch, nav data, UID resolution.
3
3
  */
4
+ import type { IPage } from './types.js';
4
5
  export declare function stripHtml(s: string): string;
5
6
  export declare function payloadData(payload: any): any;
6
- export declare function wbiSign(page: any, params: Record<string, any>): Promise<Record<string, string>>;
7
- export declare function apiGet(page: any, path: string, opts?: {
7
+ export declare function wbiSign(page: IPage, params: Record<string, any>): Promise<Record<string, string>>;
8
+ export declare function apiGet(page: IPage, path: string, opts?: {
8
9
  params?: Record<string, any>;
9
10
  signed?: boolean;
10
11
  }): Promise<any>;
11
- export declare function fetchJson(page: any, url: string): Promise<any>;
12
- export declare function getSelfUid(page: any): Promise<string>;
13
- export declare function resolveUid(page: any, input: string): Promise<string>;
12
+ export declare function fetchJson(page: IPage, url: string): Promise<any>;
13
+ export declare function getSelfUid(page: IPage): Promise<string>;
14
+ export declare function resolveUid(page: IPage, input: string): Promise<string>;
package/dist/browser.d.ts CHANGED
@@ -2,10 +2,11 @@
2
2
  * Browser interaction via Playwright MCP Bridge extension.
3
3
  * Connects to an existing Chrome browser through the extension's stdio JSON-RPC.
4
4
  */
5
+ import type { IPage } from './types.js';
5
6
  /**
6
7
  * Page abstraction wrapping JSON-RPC calls to Playwright MCP.
7
8
  */
8
- export declare class Page {
9
+ export declare class Page implements IPage {
9
10
  private _send;
10
11
  private _recv;
11
12
  constructor(_send: (msg: string) => void, _recv: () => Promise<any>);
package/dist/browser.js CHANGED
@@ -8,6 +8,14 @@ import * as fs from 'node:fs';
8
8
  import * as os from 'node:os';
9
9
  import * as path from 'node:path';
10
10
  import { formatSnapshot } from './snapshotFormatter.js';
11
+ // Read version from package.json (single source of truth)
12
+ const __browser_dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ const PKG_VERSION = (() => { try {
14
+ return JSON.parse(fs.readFileSync(path.resolve(__browser_dirname, '..', 'package.json'), 'utf-8')).version;
15
+ }
16
+ catch {
17
+ return '0.0.0';
18
+ } })();
11
19
  const EXTENSION_LOCK_TIMEOUT = parseInt(process.env.OPENCLI_EXTENSION_LOCK_TIMEOUT ?? '120', 10);
12
20
  const EXTENSION_LOCK_POLL = parseInt(process.env.OPENCLI_EXTENSION_LOCK_POLL_INTERVAL ?? '1', 10);
13
21
  const CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
@@ -159,7 +167,7 @@ export class PlaywrightMCP {
159
167
  const initMsg = jsonRpcRequest('initialize', {
160
168
  protocolVersion: '2024-11-05',
161
169
  capabilities: {},
162
- clientInfo: { name: 'opencli', version: '0.1.0' },
170
+ clientInfo: { name: 'opencli', version: PKG_VERSION },
163
171
  });
164
172
  this._proc.stdin?.write(initMsg);
165
173
  // Wait for initialize response, then send initialized notification
package/dist/cascade.d.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  * automatically finds the minimum-privilege strategy that works.
11
11
  */
12
12
  import { Strategy } from './registry.js';
13
+ import type { IPage } from './types.js';
13
14
  interface ProbeResult {
14
15
  strategy: Strategy;
15
16
  success: boolean;
@@ -27,14 +28,14 @@ interface CascadeResult {
27
28
  * Probe an endpoint with a specific strategy.
28
29
  * Returns whether the probe succeeded and basic response info.
29
30
  */
30
- export declare function probeEndpoint(page: any, url: string, strategy: Strategy, opts?: {
31
+ export declare function probeEndpoint(page: IPage, url: string, strategy: Strategy, opts?: {
31
32
  timeout?: number;
32
33
  }): Promise<ProbeResult>;
33
34
  /**
34
35
  * Run the cascade: try each strategy in order until one works.
35
36
  * Returns the simplest working strategy.
36
37
  */
37
- export declare function cascadeProbe(page: any, url: string, opts?: {
38
+ export declare function cascadeProbe(page: IPage, url: string, opts?: {
38
39
  maxStrategy?: Strategy;
39
40
  timeout?: number;
40
41
  }): Promise<CascadeResult>;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * BBC News headlines — public RSS feed, no browser needed.
3
+ * Source: bb-sites/bbc/news.js
4
+ */
5
+ import { cli, Strategy } from '../../registry.js';
6
+ cli({
7
+ site: 'bbc',
8
+ name: 'news',
9
+ description: 'BBC News headlines (RSS)',
10
+ domain: 'www.bbc.com',
11
+ strategy: Strategy.PUBLIC,
12
+ args: [
13
+ { name: 'limit', type: 'int', default: 20, help: 'Number of headlines (max 50)' },
14
+ ],
15
+ columns: ['rank', 'title', 'description', 'url'],
16
+ func: async (page, kwargs) => {
17
+ const count = Math.min(kwargs.limit || 20, 50);
18
+ const resp = await fetch('https://feeds.bbci.co.uk/news/rss.xml');
19
+ if (!resp.ok)
20
+ return [];
21
+ const xml = await resp.text();
22
+ // Simple XML parsing without DOMParser (works in Node)
23
+ const items = [];
24
+ const itemRegex = /<item>([\s\S]*?)<\/item>/g;
25
+ let match;
26
+ while ((match = itemRegex.exec(xml)) && items.length < count) {
27
+ const block = match[1];
28
+ const title = block.match(/<title><!\[CDATA\[(.*?)\]\]>|<title>(.*?)<\/title>/)?.[1] || block.match(/<title>(.*?)<\/title>/)?.[1] || '';
29
+ const desc = block.match(/<description><!\[CDATA\[(.*?)\]\]>|<description>(.*?)<\/description>/)?.[1] || block.match(/<description>(.*?)<\/description>/)?.[1] || '';
30
+ const link = block.match(/<link>(.*?)<\/link>/)?.[1] || block.match(/<guid[^>]*>(.*?)<\/guid>/)?.[1] || '';
31
+ if (title) {
32
+ items.push({
33
+ rank: items.length + 1,
34
+ title: title.trim(),
35
+ description: desc.trim().substring(0, 200),
36
+ url: link.trim(),
37
+ });
38
+ }
39
+ }
40
+ return items;
41
+ },
42
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ /**
2
+ * BOSS直聘 job search — browser cookie API.
3
+ * Source: bb-sites/boss/search.js
4
+ */
5
+ import { cli, Strategy } from '../../registry.js';
6
+ cli({
7
+ site: 'boss',
8
+ name: 'search',
9
+ description: 'BOSS直聘搜索职位',
10
+ domain: 'www.zhipin.com',
11
+ strategy: Strategy.COOKIE,
12
+ args: [
13
+ { name: 'query', required: true, help: 'Search keyword (e.g. AI agent, 前端)' },
14
+ { name: 'city', default: '101010100', help: 'City code (101010100=北京, 101020100=上海, 101210100=杭州, 101280100=广州)' },
15
+ { name: 'limit', type: 'int', default: 15, help: 'Number of results' },
16
+ ],
17
+ columns: ['name', 'salary', 'company', 'city', 'experience', 'degree', 'boss', 'url'],
18
+ func: async (page, kwargs) => {
19
+ await page.goto('https://www.zhipin.com');
20
+ await page.wait(2);
21
+ const data = await page.evaluate(`
22
+ (async () => {
23
+ const params = new URLSearchParams({
24
+ scene: '1', query: '${kwargs.query.replace(/'/g, "\\'")}',
25
+ city: '${kwargs.city || '101010100'}', page: '1', pageSize: '15',
26
+ experience: '', degree: '', payType: '', partTime: '',
27
+ industry: '', scale: '', stage: '', position: '',
28
+ jobType: '', salary: '', multiBusinessDistrict: '', multiSubway: ''
29
+ });
30
+ const resp = await fetch('/wapi/zpgeek/search/joblist.json?' + params.toString(), {credentials: 'include'});
31
+ if (!resp.ok) return {error: 'HTTP ' + resp.status};
32
+ const d = await resp.json();
33
+ if (d.code !== 0) return {error: d.message || 'API error'};
34
+ const zpData = d.zpData || {};
35
+ return (zpData.jobList || []).map(j => ({
36
+ name: j.jobName, salary: j.salaryDesc, company: j.brandName,
37
+ city: j.cityName, experience: j.jobExperience, degree: j.jobDegree,
38
+ boss: j.bossName + ' · ' + j.bossTitle,
39
+ url: j.encryptJobId ? 'https://www.zhipin.com/job_detail/' + j.encryptJobId + '.html' : ''
40
+ }));
41
+ })()
42
+ `);
43
+ if (!Array.isArray(data))
44
+ return [];
45
+ return data.slice(0, kwargs.limit || 15);
46
+ },
47
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,62 @@
1
+ /**
2
+ * 携程旅行搜索 — browser cookie, multi-strategy.
3
+ * Source: bb-sites/ctrip/search.js (simplified to suggestion API)
4
+ */
5
+ import { cli, Strategy } from '../../registry.js';
6
+ cli({
7
+ site: 'ctrip',
8
+ name: 'search',
9
+ description: '携程旅行搜索',
10
+ domain: 'www.ctrip.com',
11
+ strategy: Strategy.COOKIE,
12
+ args: [
13
+ { name: 'query', required: true, help: 'Search keyword (city or attraction)' },
14
+ { name: 'limit', type: 'int', default: 15, help: 'Number of results' },
15
+ ],
16
+ columns: ['rank', 'name', 'type', 'score', 'price', 'url'],
17
+ func: async (page, kwargs) => {
18
+ const limit = kwargs.limit || 15;
19
+ await page.goto('https://www.ctrip.com');
20
+ await page.wait(2);
21
+ const data = await page.evaluate(`
22
+ (async () => {
23
+ const query = '${kwargs.query.replace(/'/g, "\\'")}';
24
+ const limit = ${limit};
25
+
26
+ // Strategy 1: Suggestion API
27
+ try {
28
+ const suggestUrl = 'https://m.ctrip.com/restapi/h5api/searchapp/search?action=onekeyali&keyword=' + encodeURIComponent(query);
29
+ const resp = await fetch(suggestUrl, {credentials: 'include'});
30
+ if (resp.ok) {
31
+ const d = await resp.json();
32
+ const raw = d.data || d.result || d;
33
+ if (raw && typeof raw === 'object') {
34
+ // Flatten all result categories
35
+ const items = [];
36
+ for (const key of Object.keys(raw)) {
37
+ const list = Array.isArray(raw[key]) ? raw[key] : [];
38
+ for (const item of list) {
39
+ if (items.length >= limit) break;
40
+ items.push({
41
+ rank: items.length + 1,
42
+ name: item.word || item.name || item.title || '',
43
+ type: item.type || item.tpName || key,
44
+ score: item.score || '',
45
+ price: item.price || item.minPrice || '',
46
+ url: item.url || item.surl || '',
47
+ });
48
+ }
49
+ }
50
+ if (items.length > 0) return items;
51
+ }
52
+ }
53
+ } catch(e) {}
54
+
55
+ return {error: 'No results for: ' + query};
56
+ })()
57
+ `);
58
+ if (!Array.isArray(data))
59
+ return [];
60
+ return data;
61
+ },
62
+ });
@@ -12,3 +12,11 @@ import './bilibili/user-videos.js';
12
12
  import './github/search.js';
13
13
  import './zhihu/question.js';
14
14
  import './xiaohongshu/search.js';
15
+ import './bbc/news.js';
16
+ import './weibo/hot.js';
17
+ import './boss/search.js';
18
+ import './yahoo-finance/quote.js';
19
+ import './reuters/search.js';
20
+ import './smzdm/search.js';
21
+ import './ctrip/search.js';
22
+ import './youtube/search.js';
@@ -16,3 +16,19 @@ import './github/search.js';
16
16
  import './zhihu/question.js';
17
17
  // xiaohongshu
18
18
  import './xiaohongshu/search.js';
19
+ // bbc
20
+ import './bbc/news.js';
21
+ // weibo
22
+ import './weibo/hot.js';
23
+ // boss
24
+ import './boss/search.js';
25
+ // yahoo-finance
26
+ import './yahoo-finance/quote.js';
27
+ // reuters
28
+ import './reuters/search.js';
29
+ // smzdm
30
+ import './smzdm/search.js';
31
+ // ctrip
32
+ import './ctrip/search.js';
33
+ // youtube
34
+ import './youtube/search.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Reuters news search — API with HTML fallback.
3
+ * Source: bb-sites/reuters/search.js
4
+ */
5
+ import { cli, Strategy } from '../../registry.js';
6
+ cli({
7
+ site: 'reuters',
8
+ name: 'search',
9
+ description: 'Reuters 路透社新闻搜索',
10
+ domain: 'www.reuters.com',
11
+ strategy: Strategy.COOKIE,
12
+ args: [
13
+ { name: 'query', required: true, help: 'Search query' },
14
+ { name: 'limit', type: 'int', default: 10, help: 'Number of results (max 40)' },
15
+ ],
16
+ columns: ['rank', 'title', 'date', 'section', 'url'],
17
+ func: async (page, kwargs) => {
18
+ const count = Math.min(kwargs.limit || 10, 40);
19
+ await page.goto('https://www.reuters.com');
20
+ await page.wait(2);
21
+ const data = await page.evaluate(`
22
+ (async () => {
23
+ const count = ${count};
24
+ const apiQuery = JSON.stringify({
25
+ keyword: '${kwargs.query.replace(/'/g, "\\'")}',
26
+ offset: 0, orderby: 'display_date:desc', size: count, website: 'reuters'
27
+ });
28
+ const apiUrl = 'https://www.reuters.com/pf/api/v3/content/fetch/articles-by-search-v2?query=' + encodeURIComponent(apiQuery);
29
+ try {
30
+ const resp = await fetch(apiUrl, {credentials: 'include'});
31
+ if (resp.ok) {
32
+ const data = await resp.json();
33
+ const articles = data.result?.articles || data.articles || [];
34
+ if (articles.length > 0) {
35
+ return articles.slice(0, count).map((a, i) => ({
36
+ rank: i + 1,
37
+ title: a.title || a.headlines?.basic || '',
38
+ date: (a.display_date || a.published_time || '').split('T')[0],
39
+ section: a.taxonomy?.section?.name || '',
40
+ url: a.canonical_url ? 'https://www.reuters.com' + a.canonical_url : '',
41
+ }));
42
+ }
43
+ }
44
+ } catch(e) {}
45
+ return {error: 'Reuters API unavailable'};
46
+ })()
47
+ `);
48
+ if (!Array.isArray(data))
49
+ return [];
50
+ return data;
51
+ },
52
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 什么值得买搜索好价 — browser cookie, HTML parse.
3
+ * Source: bb-sites/smzdm/search.js
4
+ */
5
+ import { cli, Strategy } from '../../registry.js';
6
+ cli({
7
+ site: 'smzdm',
8
+ name: 'search',
9
+ description: '什么值得买搜索好价',
10
+ domain: 'www.smzdm.com',
11
+ strategy: Strategy.COOKIE,
12
+ args: [
13
+ { name: 'keyword', required: true, help: 'Search keyword' },
14
+ { name: 'limit', type: 'int', default: 20, help: 'Number of results' },
15
+ ],
16
+ columns: ['rank', 'title', 'price', 'mall', 'comments', 'url'],
17
+ func: async (page, kwargs) => {
18
+ const q = encodeURIComponent(kwargs.keyword);
19
+ const limit = kwargs.limit || 20;
20
+ await page.goto('https://www.smzdm.com');
21
+ await page.wait(2);
22
+ const data = await page.evaluate(`
23
+ (async () => {
24
+ const q = '${q}';
25
+ const limit = ${limit};
26
+ // Try youhui channel first, then home
27
+ for (const channel of ['youhui', 'home']) {
28
+ try {
29
+ const resp = await fetch('https://search.smzdm.com/ajax/?c=' + channel + '&s=' + q + '&p=1&v=b', {
30
+ credentials: 'include',
31
+ headers: {'X-Requested-With': 'XMLHttpRequest'}
32
+ });
33
+ if (!resp.ok) continue;
34
+ const html = await resp.text();
35
+ if (html.indexOf('feed-row-wide') === -1) continue;
36
+ const parser = new DOMParser();
37
+ const doc = parser.parseFromString(html, 'text/html');
38
+ const items = doc.querySelectorAll('li.feed-row-wide');
39
+ const results = [];
40
+ items.forEach((li, i) => {
41
+ if (results.length >= limit) return;
42
+ const titleEl = li.querySelector('h5.feed-block-title > a')
43
+ || li.querySelector('h5 > a');
44
+ if (!titleEl) return;
45
+ const title = (titleEl.getAttribute('title') || titleEl.textContent || '').trim();
46
+ const url = titleEl.getAttribute('href') || '';
47
+ const priceEl = li.querySelector('.z-highlight');
48
+ const price = priceEl ? priceEl.textContent.trim() : '';
49
+ let mall = '';
50
+ const extrasSpan = li.querySelector('.z-feed-foot-r .feed-block-extras span');
51
+ if (extrasSpan) mall = extrasSpan.textContent.trim();
52
+ const commentEl = li.querySelector('.feed-btn-comment');
53
+ const comments = commentEl ? parseInt(commentEl.textContent.trim()) || 0 : 0;
54
+ results.push({rank: results.length + 1, title, price, mall, comments, url});
55
+ });
56
+ if (results.length > 0) return results;
57
+ } catch(e) { continue; }
58
+ }
59
+ return {error: 'No results'};
60
+ })()
61
+ `);
62
+ if (!Array.isArray(data))
63
+ return [];
64
+ return data;
65
+ },
66
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Weibo hot search — browser cookie API.
3
+ * Source: bb-sites/weibo/hot.js
4
+ */
5
+ import { cli, Strategy } from '../../registry.js';
6
+ cli({
7
+ site: 'weibo',
8
+ name: 'hot',
9
+ description: '微博热搜',
10
+ domain: 'weibo.com',
11
+ strategy: Strategy.COOKIE,
12
+ args: [
13
+ { name: 'limit', type: 'int', default: 30, help: 'Number of items (max 50)' },
14
+ ],
15
+ columns: ['rank', 'word', 'hot_value', 'category', 'label', 'url'],
16
+ func: async (page, kwargs) => {
17
+ const count = Math.min(kwargs.limit || 30, 50);
18
+ await page.goto('https://weibo.com');
19
+ await page.wait(2);
20
+ const data = await page.evaluate(`
21
+ (async () => {
22
+ const resp = await fetch('/ajax/statuses/hot_band', {credentials: 'include'});
23
+ if (!resp.ok) return {error: 'HTTP ' + resp.status};
24
+ const data = await resp.json();
25
+ if (!data.ok) return {error: 'API error'};
26
+ const bandList = data.data?.band_list || [];
27
+ return bandList.map((item, i) => ({
28
+ rank: item.realpos || (i + 1),
29
+ word: item.word,
30
+ hot_value: item.num || 0,
31
+ category: item.category || '',
32
+ label: item.label_name || '',
33
+ url: 'https://s.weibo.com/weibo?q=' + encodeURIComponent('#' + item.word + '#')
34
+ }));
35
+ })()
36
+ `);
37
+ if (!Array.isArray(data))
38
+ return [];
39
+ return data.slice(0, count);
40
+ },
41
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Yahoo Finance stock quote — multi-strategy API fallback.
3
+ * Source: bb-sites/yahoo-finance/quote.js
4
+ */
5
+ import { cli, Strategy } from '../../registry.js';
6
+ cli({
7
+ site: 'yahoo-finance',
8
+ name: 'quote',
9
+ description: 'Yahoo Finance 股票行情',
10
+ domain: 'finance.yahoo.com',
11
+ strategy: Strategy.COOKIE,
12
+ args: [
13
+ { name: 'symbol', required: true, help: 'Stock ticker (e.g. AAPL, MSFT, TSLA)' },
14
+ ],
15
+ columns: ['symbol', 'name', 'price', 'change', 'changePercent', 'open', 'high', 'low', 'volume', 'marketCap'],
16
+ func: async (page, kwargs) => {
17
+ const symbol = kwargs.symbol.toUpperCase().trim();
18
+ await page.goto(`https://finance.yahoo.com/quote/${encodeURIComponent(symbol)}/`);
19
+ await page.wait(3);
20
+ const data = await page.evaluate(`
21
+ (async () => {
22
+ const sym = '${symbol}';
23
+
24
+ // Strategy 1: v8 chart API
25
+ try {
26
+ const chartUrl = 'https://query1.finance.yahoo.com/v8/finance/chart/' + encodeURIComponent(sym) + '?interval=1d&range=1d';
27
+ const resp = await fetch(chartUrl);
28
+ if (resp.ok) {
29
+ const d = await resp.json();
30
+ const chart = d?.chart?.result?.[0];
31
+ if (chart) {
32
+ const meta = chart.meta || {};
33
+ const prevClose = meta.previousClose || meta.chartPreviousClose;
34
+ const price = meta.regularMarketPrice;
35
+ const change = price != null && prevClose != null ? (price - prevClose) : null;
36
+ const changePct = change != null && prevClose ? ((change / prevClose) * 100) : null;
37
+ return {
38
+ symbol: meta.symbol || sym, name: meta.shortName || meta.longName || sym,
39
+ price: price != null ? Number(price.toFixed(2)) : null,
40
+ change: change != null ? change.toFixed(2) : null,
41
+ changePercent: changePct != null ? changePct.toFixed(2) + '%' : null,
42
+ open: chart.indicators?.quote?.[0]?.open?.[0] || null,
43
+ high: meta.regularMarketDayHigh || null,
44
+ low: meta.regularMarketDayLow || null,
45
+ volume: meta.regularMarketVolume || null,
46
+ marketCap: null, currency: meta.currency, exchange: meta.exchangeName,
47
+ };
48
+ }
49
+ }
50
+ } catch(e) {}
51
+
52
+ // Strategy 2: Parse from page
53
+ const titleEl = document.querySelector('title');
54
+ const priceEl = document.querySelector('[data-testid="qsp-price"]');
55
+ const changeEl = document.querySelector('[data-testid="qsp-price-change"]');
56
+ const changePctEl = document.querySelector('[data-testid="qsp-price-change-percent"]');
57
+ if (priceEl) {
58
+ return {
59
+ symbol: sym,
60
+ name: titleEl ? titleEl.textContent.split('(')[0].trim() : sym,
61
+ price: priceEl.textContent.replace(/,/g, ''),
62
+ change: changeEl ? changeEl.textContent : null,
63
+ changePercent: changePctEl ? changePctEl.textContent : null,
64
+ open: null, high: null, low: null, volume: null, marketCap: null,
65
+ };
66
+ }
67
+ return {error: 'Could not fetch quote for ' + sym};
68
+ })()
69
+ `);
70
+ if (!data || data.error)
71
+ return [];
72
+ return [data];
73
+ },
74
+ });
@@ -0,0 +1 @@
1
+ export {};