@jackwener/opencli 0.1.0 → 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/CLI-CREATOR.md +594 -0
- package/README.md +124 -39
- package/README.zh-CN.md +151 -0
- package/SKILL.md +178 -102
- package/dist/bilibili.d.ts +6 -5
- package/dist/browser.d.ts +3 -1
- package/dist/browser.js +44 -2
- package/dist/cascade.d.ts +46 -0
- package/dist/cascade.js +180 -0
- package/dist/clis/bbc/news.js +42 -0
- package/dist/clis/bilibili/hot.yaml +38 -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/hackernews/top.yaml +36 -0
- package/dist/clis/index.d.ts +10 -1
- package/dist/clis/index.js +19 -1
- package/dist/clis/reddit/hot.yaml +46 -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/twitter/trending.yaml +40 -0
- package/dist/clis/v2ex/hot.yaml +25 -0
- package/dist/clis/v2ex/latest.yaml +25 -0
- package/dist/clis/v2ex/topic.yaml +27 -0
- package/dist/clis/weibo/hot.d.ts +1 -0
- package/dist/clis/weibo/hot.js +41 -0
- package/dist/clis/xiaohongshu/feed.yaml +32 -0
- package/dist/clis/xiaohongshu/notifications.yaml +38 -0
- package/dist/clis/xiaohongshu/search.d.ts +5 -0
- package/dist/clis/xiaohongshu/search.js +68 -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/clis/zhihu/hot.yaml +42 -0
- package/dist/clis/zhihu/question.d.ts +1 -0
- package/dist/clis/zhihu/question.js +39 -0
- package/dist/clis/zhihu/search.yaml +55 -0
- package/dist/engine.d.ts +2 -1
- package/dist/explore.d.ts +23 -13
- package/dist/explore.js +293 -422
- package/dist/generate.js +2 -1
- package/dist/main.js +21 -2
- 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 -313
- package/dist/registry.d.ts +3 -2
- package/dist/runtime.d.ts +2 -1
- package/dist/synthesize.d.ts +11 -8
- package/dist/synthesize.js +142 -118
- package/dist/types.d.ts +27 -0
- package/dist/types.js +7 -0
- package/package.json +9 -4
- package/src/bilibili.ts +9 -7
- package/src/browser.ts +41 -3
- package/src/cascade.ts +218 -0
- 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 +28 -1
- package/src/clis/reddit/hot.yaml +46 -0
- package/src/clis/reuters/search.ts +52 -0
- package/src/clis/smzdm/search.ts +66 -0
- package/src/clis/v2ex/hot.yaml +5 -9
- package/src/clis/v2ex/latest.yaml +5 -8
- package/src/clis/v2ex/topic.yaml +27 -0
- package/src/clis/weibo/hot.ts +41 -0
- package/src/clis/xiaohongshu/feed.yaml +32 -0
- package/src/clis/xiaohongshu/notifications.yaml +38 -0
- package/src/clis/xiaohongshu/search.ts +71 -0
- package/src/clis/yahoo-finance/quote.ts +74 -0
- package/src/clis/youtube/search.ts +60 -0
- package/src/clis/zhihu/hot.yaml +22 -8
- package/src/clis/zhihu/question.ts +45 -0
- package/src/clis/zhihu/search.yaml +55 -0
- package/src/engine.ts +2 -1
- package/src/explore.ts +303 -465
- package/src/generate.ts +3 -1
- package/src/main.ts +18 -2
- 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 -292
- package/src/registry.ts +4 -2
- package/src/runtime.ts +3 -1
- package/src/synthesize.ts +142 -137
- package/src/types.ts +23 -0
- package/vitest.config.ts +7 -0
- package/dist/clis/github/search.js +0 -20
- package/dist/clis/zhihu/search.js +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/clis/zhihu/search.ts +0 -65
- 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/dist/clis/{zhihu → boss}/search.d.ts +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
site: v2ex
|
|
2
|
+
name: hot
|
|
3
|
+
description: V2EX 热门话题
|
|
4
|
+
domain: www.v2ex.com
|
|
5
|
+
strategy: public
|
|
6
|
+
browser: false
|
|
7
|
+
|
|
8
|
+
args:
|
|
9
|
+
limit:
|
|
10
|
+
type: int
|
|
11
|
+
default: 20
|
|
12
|
+
description: Number of topics
|
|
13
|
+
|
|
14
|
+
pipeline:
|
|
15
|
+
- fetch:
|
|
16
|
+
url: https://www.v2ex.com/api/topics/hot.json
|
|
17
|
+
|
|
18
|
+
- map:
|
|
19
|
+
rank: ${{ index + 1 }}
|
|
20
|
+
title: ${{ item.title }}
|
|
21
|
+
replies: ${{ item.replies }}
|
|
22
|
+
|
|
23
|
+
- limit: ${{ args.limit }}
|
|
24
|
+
|
|
25
|
+
columns: [rank, title, replies]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
site: v2ex
|
|
2
|
+
name: latest
|
|
3
|
+
description: V2EX 最新话题
|
|
4
|
+
domain: www.v2ex.com
|
|
5
|
+
strategy: public
|
|
6
|
+
browser: false
|
|
7
|
+
|
|
8
|
+
args:
|
|
9
|
+
limit:
|
|
10
|
+
type: int
|
|
11
|
+
default: 20
|
|
12
|
+
description: Number of topics
|
|
13
|
+
|
|
14
|
+
pipeline:
|
|
15
|
+
- fetch:
|
|
16
|
+
url: https://www.v2ex.com/api/topics/latest.json
|
|
17
|
+
|
|
18
|
+
- map:
|
|
19
|
+
rank: ${{ index + 1 }}
|
|
20
|
+
title: ${{ item.title }}
|
|
21
|
+
replies: ${{ item.replies }}
|
|
22
|
+
|
|
23
|
+
- limit: ${{ args.limit }}
|
|
24
|
+
|
|
25
|
+
columns: [rank, title, replies]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
site: v2ex
|
|
2
|
+
name: topic
|
|
3
|
+
description: V2EX 主题详情和回复
|
|
4
|
+
domain: www.v2ex.com
|
|
5
|
+
strategy: public
|
|
6
|
+
browser: false
|
|
7
|
+
|
|
8
|
+
args:
|
|
9
|
+
id:
|
|
10
|
+
type: str
|
|
11
|
+
required: true
|
|
12
|
+
description: Topic ID
|
|
13
|
+
|
|
14
|
+
pipeline:
|
|
15
|
+
- fetch:
|
|
16
|
+
url: https://www.v2ex.com/api/topics/show.json
|
|
17
|
+
params:
|
|
18
|
+
id: ${{ args.id }}
|
|
19
|
+
|
|
20
|
+
- map:
|
|
21
|
+
title: ${{ item.title }}
|
|
22
|
+
replies: ${{ item.replies }}
|
|
23
|
+
url: ${{ item.url }}
|
|
24
|
+
|
|
25
|
+
- limit: 1
|
|
26
|
+
|
|
27
|
+
columns: [title, replies, url]
|
|
@@ -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,32 @@
|
|
|
1
|
+
site: xiaohongshu
|
|
2
|
+
name: feed
|
|
3
|
+
description: "小红书首页推荐 Feed (via Pinia Store Action)"
|
|
4
|
+
domain: www.xiaohongshu.com
|
|
5
|
+
strategy: intercept
|
|
6
|
+
browser: true
|
|
7
|
+
|
|
8
|
+
args:
|
|
9
|
+
limit:
|
|
10
|
+
type: int
|
|
11
|
+
default: 20
|
|
12
|
+
description: Number of items to return
|
|
13
|
+
|
|
14
|
+
columns: [title, author, likes, type, url]
|
|
15
|
+
|
|
16
|
+
pipeline:
|
|
17
|
+
- navigate: https://www.xiaohongshu.com/explore
|
|
18
|
+
- wait: 3
|
|
19
|
+
- tap:
|
|
20
|
+
store: feed
|
|
21
|
+
action: fetchFeeds
|
|
22
|
+
capture: homefeed
|
|
23
|
+
select: data.items
|
|
24
|
+
timeout: 8
|
|
25
|
+
- map:
|
|
26
|
+
id: ${{ item.id }}
|
|
27
|
+
title: ${{ item.note_card.display_title }}
|
|
28
|
+
type: ${{ item.note_card.type }}
|
|
29
|
+
author: ${{ item.note_card.user.nickname }}
|
|
30
|
+
likes: ${{ item.note_card.interact_info.liked_count }}
|
|
31
|
+
url: https://www.xiaohongshu.com/explore/${{ item.id }}
|
|
32
|
+
- limit: ${{ args.limit | default(20) }}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
site: xiaohongshu
|
|
2
|
+
name: notifications
|
|
3
|
+
description: "小红书通知 (mentions/likes/connections)"
|
|
4
|
+
domain: www.xiaohongshu.com
|
|
5
|
+
strategy: intercept
|
|
6
|
+
browser: true
|
|
7
|
+
|
|
8
|
+
args:
|
|
9
|
+
type:
|
|
10
|
+
type: str
|
|
11
|
+
default: mentions
|
|
12
|
+
description: "Notification type: mentions, likes, or connections"
|
|
13
|
+
limit:
|
|
14
|
+
type: int
|
|
15
|
+
default: 20
|
|
16
|
+
description: Number of notifications to return
|
|
17
|
+
|
|
18
|
+
columns: [rank, user, action, content, note, time]
|
|
19
|
+
|
|
20
|
+
pipeline:
|
|
21
|
+
- navigate: https://www.xiaohongshu.com/notification
|
|
22
|
+
- wait: 3
|
|
23
|
+
- tap:
|
|
24
|
+
store: notification
|
|
25
|
+
action: getNotification
|
|
26
|
+
args:
|
|
27
|
+
- ${{ args.type | default('mentions') }}
|
|
28
|
+
capture: /you/
|
|
29
|
+
select: data.message_list
|
|
30
|
+
timeout: 8
|
|
31
|
+
- map:
|
|
32
|
+
rank: ${{ index + 1 }}
|
|
33
|
+
user: ${{ item.user_info.nickname }}
|
|
34
|
+
action: ${{ item.title }}
|
|
35
|
+
content: ${{ item.comment_info.content }}
|
|
36
|
+
note: ${{ item.item_info.content }}
|
|
37
|
+
time: ${{ item.time }}
|
|
38
|
+
- limit: ${{ args.limit | default(20) }}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Xiaohongshu search — trigger search via Pinia store + XHR interception.
|
|
3
|
+
* Inspired by bb-sites/xiaohongshu/search.js but adapted for opencli pipeline.
|
|
4
|
+
*/
|
|
5
|
+
import { cli, Strategy } from '../../registry.js';
|
|
6
|
+
cli({
|
|
7
|
+
site: 'xiaohongshu',
|
|
8
|
+
name: 'search',
|
|
9
|
+
description: '搜索小红书笔记',
|
|
10
|
+
domain: 'www.xiaohongshu.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', 'author', 'likes', 'type'],
|
|
17
|
+
func: async (page, kwargs) => {
|
|
18
|
+
await page.goto('https://www.xiaohongshu.com');
|
|
19
|
+
await page.wait(2);
|
|
20
|
+
const data = await page.evaluate(`
|
|
21
|
+
(async () => {
|
|
22
|
+
const app = document.querySelector('#app')?.__vue_app__;
|
|
23
|
+
const pinia = app?.config?.globalProperties?.$pinia;
|
|
24
|
+
if (!pinia?._s) return {error: 'Page not ready'};
|
|
25
|
+
|
|
26
|
+
const searchStore = pinia._s.get('search');
|
|
27
|
+
if (!searchStore) return {error: 'Search store not found'};
|
|
28
|
+
|
|
29
|
+
let captured = null;
|
|
30
|
+
const origOpen = XMLHttpRequest.prototype.open;
|
|
31
|
+
const origSend = XMLHttpRequest.prototype.send;
|
|
32
|
+
XMLHttpRequest.prototype.open = function(m, u) { this.__url = u; return origOpen.apply(this, arguments); };
|
|
33
|
+
XMLHttpRequest.prototype.send = function(b) {
|
|
34
|
+
if (this.__url?.includes('search/notes')) {
|
|
35
|
+
const x = this;
|
|
36
|
+
const orig = x.onreadystatechange;
|
|
37
|
+
x.onreadystatechange = function() { if (x.readyState === 4 && !captured) { try { captured = JSON.parse(x.responseText); } catch {} } if (orig) orig.apply(this, arguments); };
|
|
38
|
+
}
|
|
39
|
+
return origSend.apply(this, arguments);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
searchStore.mutateSearchValue('${kwargs.keyword}');
|
|
44
|
+
await searchStore.loadMore();
|
|
45
|
+
await new Promise(r => setTimeout(r, 800));
|
|
46
|
+
} finally {
|
|
47
|
+
XMLHttpRequest.prototype.open = origOpen;
|
|
48
|
+
XMLHttpRequest.prototype.send = origSend;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!captured?.success) return {error: captured?.msg || 'Search failed'};
|
|
52
|
+
return (captured.data?.items || []).map(i => ({
|
|
53
|
+
title: i.note_card?.display_title || '',
|
|
54
|
+
type: i.note_card?.type || '',
|
|
55
|
+
url: 'https://www.xiaohongshu.com/explore/' + i.id,
|
|
56
|
+
author: i.note_card?.user?.nickname || '',
|
|
57
|
+
likes: i.note_card?.interact_info?.liked_count || '0',
|
|
58
|
+
}));
|
|
59
|
+
})()
|
|
60
|
+
`);
|
|
61
|
+
if (!Array.isArray(data))
|
|
62
|
+
return [];
|
|
63
|
+
return data.slice(0, kwargs.limit).map((item, i) => ({
|
|
64
|
+
rank: i + 1,
|
|
65
|
+
...item,
|
|
66
|
+
}));
|
|
67
|
+
},
|
|
68
|
+
});
|
|
@@ -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 {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YouTube search — innertube API via browser session.
|
|
3
|
+
* Source: bb-sites/youtube/search.js
|
|
4
|
+
*/
|
|
5
|
+
import { cli, Strategy } from '../../registry.js';
|
|
6
|
+
cli({
|
|
7
|
+
site: 'youtube',
|
|
8
|
+
name: 'search',
|
|
9
|
+
description: 'Search YouTube videos',
|
|
10
|
+
domain: 'www.youtube.com',
|
|
11
|
+
strategy: Strategy.COOKIE,
|
|
12
|
+
args: [
|
|
13
|
+
{ name: 'query', required: true, help: 'Search query' },
|
|
14
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Max results (max 50)' },
|
|
15
|
+
],
|
|
16
|
+
columns: ['rank', 'title', 'channel', 'views', 'duration', 'url'],
|
|
17
|
+
func: async (page, kwargs) => {
|
|
18
|
+
const limit = Math.min(kwargs.limit || 20, 50);
|
|
19
|
+
await page.goto('https://www.youtube.com');
|
|
20
|
+
await page.wait(2);
|
|
21
|
+
const data = await page.evaluate(`
|
|
22
|
+
(async () => {
|
|
23
|
+
const cfg = window.ytcfg?.data_ || {};
|
|
24
|
+
const apiKey = cfg.INNERTUBE_API_KEY;
|
|
25
|
+
const context = cfg.INNERTUBE_CONTEXT;
|
|
26
|
+
if (!apiKey || !context) return {error: 'YouTube config not found'};
|
|
27
|
+
|
|
28
|
+
const resp = await fetch('/youtubei/v1/search?key=' + apiKey + '&prettyPrint=false', {
|
|
29
|
+
method: 'POST', credentials: 'include',
|
|
30
|
+
headers: {'Content-Type': 'application/json'},
|
|
31
|
+
body: JSON.stringify({context, query: '${kwargs.query.replace(/'/g, "\\'")}'})
|
|
32
|
+
});
|
|
33
|
+
if (!resp.ok) return {error: 'HTTP ' + resp.status};
|
|
34
|
+
|
|
35
|
+
const data = await resp.json();
|
|
36
|
+
const contents = data.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents || [];
|
|
37
|
+
const videos = [];
|
|
38
|
+
for (const section of contents) {
|
|
39
|
+
for (const item of (section.itemSectionRenderer?.contents || [])) {
|
|
40
|
+
if (item.videoRenderer && videos.length < ${limit}) {
|
|
41
|
+
const v = item.videoRenderer;
|
|
42
|
+
videos.push({
|
|
43
|
+
rank: videos.length + 1,
|
|
44
|
+
title: v.title?.runs?.[0]?.text || '',
|
|
45
|
+
channel: v.ownerText?.runs?.[0]?.text || '',
|
|
46
|
+
views: v.viewCountText?.simpleText || v.shortViewCountText?.simpleText || '',
|
|
47
|
+
duration: v.lengthText?.simpleText || 'LIVE',
|
|
48
|
+
url: 'https://www.youtube.com/watch?v=' + v.videoId
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return videos;
|
|
54
|
+
})()
|
|
55
|
+
`);
|
|
56
|
+
if (!Array.isArray(data))
|
|
57
|
+
return [];
|
|
58
|
+
return data;
|
|
59
|
+
},
|
|
60
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
site: zhihu
|
|
2
|
+
name: hot
|
|
3
|
+
description: 知乎热榜
|
|
4
|
+
domain: www.zhihu.com
|
|
5
|
+
|
|
6
|
+
args:
|
|
7
|
+
limit:
|
|
8
|
+
type: int
|
|
9
|
+
default: 20
|
|
10
|
+
description: Number of items to return
|
|
11
|
+
|
|
12
|
+
pipeline:
|
|
13
|
+
- navigate: https://www.zhihu.com
|
|
14
|
+
|
|
15
|
+
- evaluate: |
|
|
16
|
+
(async () => {
|
|
17
|
+
const res = await fetch('https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total?limit=50', {
|
|
18
|
+
credentials: 'include'
|
|
19
|
+
});
|
|
20
|
+
const d = await res.json();
|
|
21
|
+
return (d?.data || []).map((item) => {
|
|
22
|
+
const t = item.target || {};
|
|
23
|
+
return {
|
|
24
|
+
title: t.title,
|
|
25
|
+
url: 'https://www.zhihu.com/question/' + t.id,
|
|
26
|
+
answer_count: t.answer_count,
|
|
27
|
+
follower_count: t.follower_count,
|
|
28
|
+
heat: item.detail_text || '',
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
})()
|
|
32
|
+
|
|
33
|
+
- map:
|
|
34
|
+
rank: ${{ index + 1 }}
|
|
35
|
+
title: ${{ item.title }}
|
|
36
|
+
heat: ${{ item.heat }}
|
|
37
|
+
answers: ${{ item.answer_count }}
|
|
38
|
+
url: ${{ item.url }}
|
|
39
|
+
|
|
40
|
+
- limit: ${{ args.limit }}
|
|
41
|
+
|
|
42
|
+
columns: [rank, title, heat, answers]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
cli({
|
|
3
|
+
site: 'zhihu',
|
|
4
|
+
name: 'question',
|
|
5
|
+
description: '知乎问题详情和回答',
|
|
6
|
+
domain: 'www.zhihu.com',
|
|
7
|
+
strategy: Strategy.COOKIE,
|
|
8
|
+
args: [
|
|
9
|
+
{ name: 'id', required: true, help: 'Question ID (numeric)' },
|
|
10
|
+
{ name: 'limit', type: 'int', default: 5, help: 'Number of answers' },
|
|
11
|
+
],
|
|
12
|
+
columns: ['rank', 'author', 'votes', 'content'],
|
|
13
|
+
func: async (page, kwargs) => {
|
|
14
|
+
const { id, limit = 5 } = kwargs;
|
|
15
|
+
const stripHtml = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').trim();
|
|
16
|
+
// Fetch question detail and answers in parallel via evaluate
|
|
17
|
+
const result = await page.evaluate(`
|
|
18
|
+
async () => {
|
|
19
|
+
const [qResp, aResp] = await Promise.all([
|
|
20
|
+
fetch('https://www.zhihu.com/api/v4/questions/${id}?include=data[*].detail,excerpt,answer_count,follower_count,visit_count', {credentials: 'include'}),
|
|
21
|
+
fetch('https://www.zhihu.com/api/v4/questions/${id}/answers?limit=${limit}&offset=0&sort_by=default&include=data[*].content,voteup_count,comment_count,author', {credentials: 'include'})
|
|
22
|
+
]);
|
|
23
|
+
if (!qResp.ok || !aResp.ok) return { error: true };
|
|
24
|
+
const q = await qResp.json();
|
|
25
|
+
const a = await aResp.json();
|
|
26
|
+
return { question: q, answers: a.data || [] };
|
|
27
|
+
}
|
|
28
|
+
`);
|
|
29
|
+
if (!result || result.error)
|
|
30
|
+
throw new Error('Failed to fetch question. Are you logged in?');
|
|
31
|
+
const answers = (result.answers ?? []).slice(0, Number(limit)).map((a, i) => ({
|
|
32
|
+
rank: i + 1,
|
|
33
|
+
author: a.author?.name ?? 'anonymous',
|
|
34
|
+
votes: a.voteup_count ?? 0,
|
|
35
|
+
content: stripHtml(a.content ?? '').slice(0, 200),
|
|
36
|
+
}));
|
|
37
|
+
return answers;
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
site: zhihu
|
|
2
|
+
name: search
|
|
3
|
+
description: 知乎搜索
|
|
4
|
+
domain: www.zhihu.com
|
|
5
|
+
|
|
6
|
+
args:
|
|
7
|
+
keyword:
|
|
8
|
+
type: str
|
|
9
|
+
required: true
|
|
10
|
+
description: Search keyword
|
|
11
|
+
limit:
|
|
12
|
+
type: int
|
|
13
|
+
default: 10
|
|
14
|
+
description: Number of results
|
|
15
|
+
|
|
16
|
+
pipeline:
|
|
17
|
+
- navigate: https://www.zhihu.com
|
|
18
|
+
|
|
19
|
+
- evaluate: |
|
|
20
|
+
(async () => {
|
|
21
|
+
const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').replace(/<em>/g, '').replace(/<\/em>/g, '').trim();
|
|
22
|
+
const res = await fetch('https://www.zhihu.com/api/v4/search_v3?q=' + encodeURIComponent('${{ args.keyword }}') + '&t=general&offset=0&limit=${{ args.limit }}', {
|
|
23
|
+
credentials: 'include'
|
|
24
|
+
});
|
|
25
|
+
const d = await res.json();
|
|
26
|
+
return (d?.data || [])
|
|
27
|
+
.filter(item => item.type === 'search_result')
|
|
28
|
+
.map(item => {
|
|
29
|
+
const obj = item.object || {};
|
|
30
|
+
const q = obj.question || {};
|
|
31
|
+
return {
|
|
32
|
+
type: obj.type,
|
|
33
|
+
title: strip(obj.title || q.name || ''),
|
|
34
|
+
excerpt: strip(obj.excerpt || '').substring(0, 100),
|
|
35
|
+
author: obj.author?.name || '',
|
|
36
|
+
votes: obj.voteup_count || 0,
|
|
37
|
+
url: obj.type === 'answer'
|
|
38
|
+
? 'https://www.zhihu.com/question/' + q.id + '/answer/' + obj.id
|
|
39
|
+
: obj.type === 'article'
|
|
40
|
+
? 'https://zhuanlan.zhihu.com/p/' + obj.id
|
|
41
|
+
: 'https://www.zhihu.com/question/' + obj.id,
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
})()
|
|
45
|
+
|
|
46
|
+
- map:
|
|
47
|
+
rank: ${{ index + 1 }}
|
|
48
|
+
title: ${{ item.title }}
|
|
49
|
+
type: ${{ item.type }}
|
|
50
|
+
author: ${{ item.author }}
|
|
51
|
+
votes: ${{ item.votes }}
|
|
52
|
+
|
|
53
|
+
- limit: ${{ args.limit }}
|
|
54
|
+
|
|
55
|
+
columns: [rank, title, type, author, votes]
|
package/dist/engine.d.ts
CHANGED
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
* CLI discovery: finds YAML/TS CLI definitions and registers them.
|
|
3
3
|
*/
|
|
4
4
|
import { type CliCommand } from './registry.js';
|
|
5
|
+
import type { IPage } from './types.js';
|
|
5
6
|
export declare function discoverClis(...dirs: string[]): void;
|
|
6
|
-
export declare function executeCommand(cmd: CliCommand, page:
|
|
7
|
+
export declare function executeCommand(cmd: CliCommand, page: IPage | null, kwargs: Record<string, any>, debug?: boolean): Promise<any>;
|
package/dist/explore.d.ts
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Deep Explore: intelligent API discovery with response analysis.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* analyzes
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Flow:
|
|
9
|
-
* 1. Navigate to target URL
|
|
10
|
-
* 2. Auto-scroll to trigger lazy loading
|
|
11
|
-
* 3. Capture network requests (with body analysis)
|
|
12
|
-
* 4. For each JSON response: detect list fields, infer columns, analyze auth
|
|
13
|
-
* 5. Detect frontend framework (Vue/React/Pinia/Next.js)
|
|
14
|
-
* 6. Generate structured capabilities.json
|
|
4
|
+
* Navigates to the target URL, auto-scrolls to trigger lazy loading,
|
|
5
|
+
* captures network traffic, analyzes JSON responses, and automatically
|
|
6
|
+
* infers CLI capabilities from discovered API endpoints.
|
|
15
7
|
*/
|
|
16
|
-
export declare function
|
|
17
|
-
export declare function
|
|
8
|
+
export declare function detectSiteName(url: string): string;
|
|
9
|
+
export declare function slugify(value: string): string;
|
|
10
|
+
export interface DiscoveredStore {
|
|
11
|
+
type: 'pinia' | 'vuex';
|
|
12
|
+
id: string;
|
|
13
|
+
actions: string[];
|
|
14
|
+
stateKeys: string[];
|
|
15
|
+
}
|
|
16
|
+
export declare function exploreUrl(url: string, opts: {
|
|
17
|
+
BrowserFactory: new () => any;
|
|
18
|
+
site?: string;
|
|
19
|
+
goal?: string;
|
|
20
|
+
authenticated?: boolean;
|
|
21
|
+
outDir?: string;
|
|
22
|
+
waitSeconds?: number;
|
|
23
|
+
query?: string;
|
|
24
|
+
clickLabels?: string[];
|
|
25
|
+
auto?: boolean;
|
|
26
|
+
}): Promise<Record<string, any>>;
|
|
27
|
+
export declare function renderExploreSummary(result: Record<string, any>): string;
|