@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.
- package/README.md +9 -2
- package/README.zh-CN.md +9 -1
- package/SKILL.md +24 -0
- package/dist/bilibili.d.ts +6 -5
- package/dist/browser.d.ts +2 -1
- package/dist/browser.js +9 -1
- package/dist/cascade.d.ts +3 -2
- package/dist/clis/bbc/news.js +42 -0
- package/dist/clis/boss/search.d.ts +1 -0
- package/dist/clis/boss/search.js +47 -0
- package/dist/clis/ctrip/search.d.ts +1 -0
- package/dist/clis/ctrip/search.js +62 -0
- package/dist/clis/index.d.ts +8 -0
- package/dist/clis/index.js +16 -0
- package/dist/clis/reuters/search.d.ts +1 -0
- package/dist/clis/reuters/search.js +52 -0
- package/dist/clis/smzdm/search.d.ts +1 -0
- package/dist/clis/smzdm/search.js +66 -0
- package/dist/clis/weibo/hot.d.ts +1 -0
- package/dist/clis/weibo/hot.js +41 -0
- package/dist/clis/yahoo-finance/quote.d.ts +1 -0
- package/dist/clis/yahoo-finance/quote.js +74 -0
- package/dist/clis/youtube/search.d.ts +1 -0
- package/dist/clis/youtube/search.js +60 -0
- package/dist/engine.d.ts +2 -1
- package/dist/explore.js +1 -1
- package/dist/generate.js +2 -1
- package/dist/main.js +6 -4
- package/dist/pipeline/executor.d.ts +9 -0
- package/dist/pipeline/executor.js +88 -0
- package/dist/pipeline/index.d.ts +5 -0
- package/dist/pipeline/index.js +5 -0
- package/dist/pipeline/steps/browser.d.ts +12 -0
- package/dist/pipeline/steps/browser.js +68 -0
- package/dist/pipeline/steps/fetch.d.ts +5 -0
- package/dist/pipeline/steps/fetch.js +50 -0
- package/dist/pipeline/steps/intercept.d.ts +5 -0
- package/dist/pipeline/steps/intercept.js +75 -0
- package/dist/pipeline/steps/tap.d.ts +12 -0
- package/dist/pipeline/steps/tap.js +130 -0
- package/dist/pipeline/steps/transform.d.ts +8 -0
- package/dist/pipeline/steps/transform.js +53 -0
- package/dist/pipeline/template.d.ts +16 -0
- package/dist/pipeline/template.js +115 -0
- package/dist/pipeline/template.test.d.ts +4 -0
- package/dist/pipeline/template.test.js +102 -0
- package/dist/pipeline/transform.test.d.ts +4 -0
- package/dist/pipeline/transform.test.js +90 -0
- package/dist/pipeline.d.ts +5 -7
- package/dist/pipeline.js +5 -549
- package/dist/registry.d.ts +3 -2
- package/dist/runtime.d.ts +2 -1
- package/dist/types.d.ts +27 -0
- package/dist/types.js +7 -0
- package/package.json +6 -3
- package/src/bilibili.ts +9 -7
- package/src/browser.ts +8 -2
- package/src/cascade.ts +3 -2
- package/src/clis/bbc/news.ts +42 -0
- package/src/clis/boss/search.ts +47 -0
- package/src/clis/ctrip/search.ts +62 -0
- package/src/clis/index.ts +24 -0
- package/src/clis/reuters/search.ts +52 -0
- package/src/clis/smzdm/search.ts +66 -0
- package/src/clis/weibo/hot.ts +41 -0
- package/src/clis/yahoo-finance/quote.ts +74 -0
- package/src/clis/youtube/search.ts +60 -0
- package/src/engine.ts +2 -1
- package/src/explore.ts +1 -1
- package/src/generate.ts +3 -1
- package/src/main.ts +7 -5
- package/src/pipeline/executor.ts +98 -0
- package/src/pipeline/index.ts +6 -0
- package/src/pipeline/steps/browser.ts +67 -0
- package/src/pipeline/steps/fetch.ts +60 -0
- package/src/pipeline/steps/intercept.ts +78 -0
- package/src/pipeline/steps/tap.ts +137 -0
- package/src/pipeline/steps/transform.ts +50 -0
- package/src/pipeline/template.test.ts +107 -0
- package/src/pipeline/template.ts +101 -0
- package/src/pipeline/transform.test.ts +107 -0
- package/src/pipeline.ts +5 -529
- package/src/registry.ts +4 -2
- package/src/runtime.ts +3 -1
- package/src/types.ts +23 -0
- package/vitest.config.ts +7 -0
- package/dist/clis/github/search.js +0 -20
- package/dist/clis/github/trending.yaml +0 -58
- package/dist/promote.d.ts +0 -1
- package/dist/promote.js +0 -3
- package/dist/register.d.ts +0 -2
- package/dist/register.js +0 -2
- package/dist/scaffold.d.ts +0 -2
- package/dist/scaffold.js +0 -2
- package/dist/smoke.d.ts +0 -2
- package/dist/smoke.js +0 -2
- package/src/clis/github/search.ts +0 -21
- package/src/clis/github/trending.yaml +0 -58
- package/src/promote.ts +0 -3
- package/src/register.ts +0 -2
- package/src/scaffold.ts +0 -2
- package/src/smoke.ts +0 -2
- /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
|
[](https://www.npmjs.com/package/@jackwener/opencli)
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
- 🌐 **
|
|
14
|
+
- 🌐 **28+ 命令,16 个站点** — B站、知乎、小红书、Twitter、Reddit、GitHub、V2EX、Hacker 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
|
package/dist/bilibili.d.ts
CHANGED
|
@@ -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:
|
|
7
|
-
export declare function apiGet(page:
|
|
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:
|
|
12
|
-
export declare function getSelfUid(page:
|
|
13
|
-
export declare function resolveUid(page:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
+
});
|
package/dist/clis/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/clis/index.js
CHANGED
|
@@ -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 {};
|