@jackwener/opencli 0.1.0 → 0.1.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 +594 -0
- package/README.md +116 -38
- package/README.zh-CN.md +143 -0
- package/SKILL.md +154 -102
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +35 -1
- package/dist/cascade.d.ts +45 -0
- package/dist/cascade.js +180 -0
- package/dist/clis/bilibili/hot.yaml +38 -0
- package/dist/clis/github/trending.yaml +58 -0
- package/dist/clis/hackernews/top.yaml +36 -0
- package/dist/clis/index.d.ts +2 -1
- package/dist/clis/index.js +3 -1
- package/dist/clis/reddit/hot.yaml +46 -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/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/zhihu/hot.yaml +42 -0
- package/dist/clis/zhihu/question.js +39 -0
- package/dist/clis/zhihu/search.yaml +55 -0
- package/dist/explore.d.ts +23 -13
- package/dist/explore.js +293 -422
- package/dist/main.js +17 -0
- package/dist/pipeline.js +238 -2
- package/dist/synthesize.d.ts +11 -8
- package/dist/synthesize.js +142 -118
- package/package.json +4 -2
- package/src/browser.ts +33 -1
- package/src/cascade.ts +217 -0
- package/src/clis/index.ts +4 -1
- package/src/clis/reddit/hot.yaml +46 -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/xiaohongshu/feed.yaml +32 -0
- package/src/clis/xiaohongshu/notifications.yaml +38 -0
- package/src/clis/xiaohongshu/search.ts +71 -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/explore.ts +303 -465
- package/src/main.ts +14 -0
- package/src/pipeline.ts +239 -2
- package/src/synthesize.ts +142 -137
- package/dist/clis/zhihu/search.js +0 -58
- package/src/clis/zhihu/search.ts +0 -65
- /package/dist/clis/zhihu/{search.d.ts → question.d.ts} +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
site: bilibili
|
|
2
|
+
name: hot
|
|
3
|
+
description: B站热门视频
|
|
4
|
+
domain: www.bilibili.com
|
|
5
|
+
|
|
6
|
+
args:
|
|
7
|
+
limit:
|
|
8
|
+
type: int
|
|
9
|
+
default: 20
|
|
10
|
+
description: Number of videos
|
|
11
|
+
|
|
12
|
+
pipeline:
|
|
13
|
+
- navigate: https://www.bilibili.com
|
|
14
|
+
|
|
15
|
+
- evaluate: |
|
|
16
|
+
(async () => {
|
|
17
|
+
const res = await fetch('https://api.bilibili.com/x/web-interface/popular?ps=${{ args.limit }}&pn=1', {
|
|
18
|
+
credentials: 'include'
|
|
19
|
+
});
|
|
20
|
+
const data = await res.json();
|
|
21
|
+
return (data?.data?.list || []).map((item) => ({
|
|
22
|
+
title: item.title,
|
|
23
|
+
author: item.owner?.name,
|
|
24
|
+
play: item.stat?.view,
|
|
25
|
+
danmaku: item.stat?.danmaku,
|
|
26
|
+
}));
|
|
27
|
+
})()
|
|
28
|
+
|
|
29
|
+
- map:
|
|
30
|
+
rank: ${{ index + 1 }}
|
|
31
|
+
title: ${{ item.title }}
|
|
32
|
+
author: ${{ item.author }}
|
|
33
|
+
play: ${{ item.play }}
|
|
34
|
+
danmaku: ${{ item.danmaku }}
|
|
35
|
+
|
|
36
|
+
- limit: ${{ args.limit }}
|
|
37
|
+
|
|
38
|
+
columns: [rank, title, author, play, danmaku]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
site: github
|
|
2
|
+
name: trending
|
|
3
|
+
description: GitHub trending repositories
|
|
4
|
+
domain: github.com
|
|
5
|
+
|
|
6
|
+
args:
|
|
7
|
+
language:
|
|
8
|
+
type: str
|
|
9
|
+
default: ""
|
|
10
|
+
description: "Programming language filter (e.g. python, rust)"
|
|
11
|
+
since:
|
|
12
|
+
type: str
|
|
13
|
+
default: daily
|
|
14
|
+
description: "Time range: daily, weekly, monthly"
|
|
15
|
+
limit:
|
|
16
|
+
type: int
|
|
17
|
+
default: 20
|
|
18
|
+
description: Number of repos
|
|
19
|
+
|
|
20
|
+
pipeline:
|
|
21
|
+
- evaluate: |
|
|
22
|
+
(async () => {
|
|
23
|
+
const lang = '${{ args.language }}' ? '/${{ args.language }}' : '';
|
|
24
|
+
const res = await fetch(`https://github.com/trending${lang}?since=${{ args.since }}`, {
|
|
25
|
+
headers: { 'Accept': 'text/html' }
|
|
26
|
+
});
|
|
27
|
+
const html = await res.text();
|
|
28
|
+
const parser = new DOMParser();
|
|
29
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
30
|
+
const rows = doc.querySelectorAll('article.Box-row');
|
|
31
|
+
return Array.from(rows).map(row => {
|
|
32
|
+
const nameEl = row.querySelector('h2 a');
|
|
33
|
+
const descEl = row.querySelector('p');
|
|
34
|
+
const langEl = row.querySelector('[itemprop="programmingLanguage"]');
|
|
35
|
+
const starsEl = row.querySelectorAll('a.Link--muted');
|
|
36
|
+
const todayEl = row.querySelector('span.d-inline-block.float-sm-right');
|
|
37
|
+
return {
|
|
38
|
+
repo: nameEl?.textContent?.trim()?.replace(/\s+/g, '') || '',
|
|
39
|
+
description: descEl?.textContent?.trim() || '',
|
|
40
|
+
language: langEl?.textContent?.trim() || '',
|
|
41
|
+
stars: starsEl[0]?.textContent?.trim() || '',
|
|
42
|
+
forks: starsEl[1]?.textContent?.trim() || '',
|
|
43
|
+
today: todayEl?.textContent?.trim() || ''
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
})()
|
|
47
|
+
|
|
48
|
+
- map:
|
|
49
|
+
rank: ${{ index + 1 }}
|
|
50
|
+
repo: ${{ item.repo }}
|
|
51
|
+
description: ${{ item.description }}
|
|
52
|
+
language: ${{ item.language }}
|
|
53
|
+
stars: ${{ item.stars }}
|
|
54
|
+
today: ${{ item.today }}
|
|
55
|
+
|
|
56
|
+
- limit: ${{ args.limit }}
|
|
57
|
+
|
|
58
|
+
columns: [rank, repo, language, stars, today]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
site: hackernews
|
|
2
|
+
name: top
|
|
3
|
+
description: Hacker News top stories
|
|
4
|
+
domain: news.ycombinator.com
|
|
5
|
+
strategy: public
|
|
6
|
+
browser: false
|
|
7
|
+
|
|
8
|
+
args:
|
|
9
|
+
limit:
|
|
10
|
+
type: int
|
|
11
|
+
default: 20
|
|
12
|
+
description: Number of stories
|
|
13
|
+
|
|
14
|
+
pipeline:
|
|
15
|
+
- fetch:
|
|
16
|
+
url: https://hacker-news.firebaseio.com/v0/topstories.json
|
|
17
|
+
|
|
18
|
+
- limit: 30
|
|
19
|
+
|
|
20
|
+
- map:
|
|
21
|
+
id: ${{ item }}
|
|
22
|
+
|
|
23
|
+
- fetch:
|
|
24
|
+
url: https://hacker-news.firebaseio.com/v0/item/${{ item.id }}.json
|
|
25
|
+
|
|
26
|
+
- map:
|
|
27
|
+
rank: ${{ index + 1 }}
|
|
28
|
+
title: ${{ item.title }}
|
|
29
|
+
score: ${{ item.score }}
|
|
30
|
+
author: ${{ item.by }}
|
|
31
|
+
comments: ${{ item.descendants }}
|
|
32
|
+
url: ${{ item.url }}
|
|
33
|
+
|
|
34
|
+
- limit: ${{ args.limit }}
|
|
35
|
+
|
|
36
|
+
columns: [rank, title, score, author, comments]
|
package/dist/clis/index.d.ts
CHANGED
package/dist/clis/index.js
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
site: reddit
|
|
2
|
+
name: hot
|
|
3
|
+
description: Reddit 热门帖子
|
|
4
|
+
domain: www.reddit.com
|
|
5
|
+
|
|
6
|
+
args:
|
|
7
|
+
subreddit:
|
|
8
|
+
type: str
|
|
9
|
+
default: ""
|
|
10
|
+
description: "Subreddit name (e.g. programming). Empty for frontpage"
|
|
11
|
+
limit:
|
|
12
|
+
type: int
|
|
13
|
+
default: 20
|
|
14
|
+
description: Number of posts
|
|
15
|
+
|
|
16
|
+
pipeline:
|
|
17
|
+
- navigate: https://www.reddit.com
|
|
18
|
+
|
|
19
|
+
- evaluate: |
|
|
20
|
+
(async () => {
|
|
21
|
+
const sub = '${{ args.subreddit }}';
|
|
22
|
+
const path = sub ? '/r/' + sub + '/hot.json' : '/hot.json';
|
|
23
|
+
const res = await fetch(path + '?limit=${{ args.limit }}&raw_json=1', {
|
|
24
|
+
credentials: 'include'
|
|
25
|
+
});
|
|
26
|
+
const d = await res.json();
|
|
27
|
+
return (d?.data?.children || []).map(c => ({
|
|
28
|
+
title: c.data.title,
|
|
29
|
+
subreddit: c.data.subreddit_name_prefixed,
|
|
30
|
+
score: c.data.score,
|
|
31
|
+
comments: c.data.num_comments,
|
|
32
|
+
author: c.data.author,
|
|
33
|
+
url: 'https://www.reddit.com' + c.data.permalink,
|
|
34
|
+
}));
|
|
35
|
+
})()
|
|
36
|
+
|
|
37
|
+
- map:
|
|
38
|
+
rank: ${{ index + 1 }}
|
|
39
|
+
title: ${{ item.title }}
|
|
40
|
+
subreddit: ${{ item.subreddit }}
|
|
41
|
+
score: ${{ item.score }}
|
|
42
|
+
comments: ${{ item.comments }}
|
|
43
|
+
|
|
44
|
+
- limit: ${{ args.limit }}
|
|
45
|
+
|
|
46
|
+
columns: [rank, title, subreddit, score, comments]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
site: twitter
|
|
2
|
+
name: trending
|
|
3
|
+
description: Twitter/X trending topics
|
|
4
|
+
domain: x.com
|
|
5
|
+
|
|
6
|
+
args:
|
|
7
|
+
limit:
|
|
8
|
+
type: int
|
|
9
|
+
default: 20
|
|
10
|
+
description: Number of trends to show
|
|
11
|
+
|
|
12
|
+
pipeline:
|
|
13
|
+
- navigate: https://x.com/explore/tabs/trending
|
|
14
|
+
|
|
15
|
+
- evaluate: |
|
|
16
|
+
(async () => {
|
|
17
|
+
const cookies = document.cookie.split(';').reduce((acc, c) => {
|
|
18
|
+
const [k, v] = c.trim().split('=');
|
|
19
|
+
acc[k] = v;
|
|
20
|
+
return acc;
|
|
21
|
+
}, {});
|
|
22
|
+
const csrfToken = cookies['ct0'] || '';
|
|
23
|
+
const bearerToken = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
|
|
24
|
+
const res = await fetch('/i/api/2/guide.json?include_page_configuration=true', {
|
|
25
|
+
credentials: 'include',
|
|
26
|
+
headers: { 'x-twitter-active-user': 'yes', 'x-csrf-token': csrfToken, 'authorization': 'Bearer ' + bearerToken }
|
|
27
|
+
});
|
|
28
|
+
const data = await res.json();
|
|
29
|
+
const trends = data?.timeline?.instructions?.[1]?.addEntries?.entries || [];
|
|
30
|
+
return trends.filter(e => e.content?.timelineModule).flatMap(e => e.content.timelineModule.items || []).map(t => t?.item?.content?.trend).filter(Boolean);
|
|
31
|
+
})()
|
|
32
|
+
|
|
33
|
+
- map:
|
|
34
|
+
rank: ${{ index + 1 }}
|
|
35
|
+
topic: ${{ item.name }}
|
|
36
|
+
tweets: ${{ item.tweetCount || 'N/A' }}
|
|
37
|
+
|
|
38
|
+
- limit: ${{ args.limit }}
|
|
39
|
+
|
|
40
|
+
columns: [rank, topic, tweets]
|
|
@@ -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,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,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,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/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;
|