browser-web-search 0.2.3 → 0.3.0
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/package.json +1 -1
- package/sites/36kr/article.js +51 -0
- package/sites/36kr/search.js +55 -0
- package/sites/netease/hot.js +51 -0
- package/sites/qqnews/hot.js +53 -0
- package/sites/sina/hot.js +86 -0
- package/sites/thepaper/hot.js +50 -0
- package/sites/toutiao/feed.js +92 -0
- package/sites/weibo/hot.js +56 -0
- package/sites/douban/comments.js +0 -58
- package/sites/douban/movie-hot.js +0 -64
- package/sites/douban/movie-top.js +0 -65
- package/sites/douban/movie.js +0 -117
- package/sites/douban/search.js +0 -90
- package/sites/douban/top250.js +0 -73
- package/sites/github/fork.js +0 -38
- package/sites/github/issue-create.js +0 -42
- package/sites/github/issues.js +0 -32
- package/sites/github/me.js +0 -22
- package/sites/github/pr-create.js +0 -55
- package/sites/github/repo.js +0 -27
package/package.json
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "36kr/article",
|
|
4
|
+
"description": "36氪文章详情",
|
|
5
|
+
"domain": "36kr.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"id": {"required": true, "description": "文章ID"}
|
|
8
|
+
},
|
|
9
|
+
"readOnly": true,
|
|
10
|
+
"example": "bws 36kr/article 123456789"
|
|
11
|
+
}
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
async function(args) {
|
|
15
|
+
if (!args.id) return {error: 'Missing argument: id', hint: 'Provide an article ID'};
|
|
16
|
+
|
|
17
|
+
const resp = await fetch('https://36kr.com/api/post/' + args.id, {
|
|
18
|
+
credentials: 'include'
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (!resp.ok) {
|
|
22
|
+
return {error: 'HTTP ' + resp.status, hint: 'Article may not exist or 36kr.com needs login'};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let data;
|
|
26
|
+
try {
|
|
27
|
+
data = await resp.json();
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return {error: 'Failed to parse response', hint: '36kr API may have changed'};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!data || data.code !== 0 || !data.data) {
|
|
33
|
+
return {error: data.message || 'Article not found', hint: 'Check the article ID'};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const article = data.data;
|
|
37
|
+
return {
|
|
38
|
+
id: article.id,
|
|
39
|
+
title: article.title || '',
|
|
40
|
+
summary: article.summary || '',
|
|
41
|
+
content: article.content || article.text || '',
|
|
42
|
+
author: article.author?.name || article.user?.name || '',
|
|
43
|
+
published_at: article.published_at || article.created_at || '',
|
|
44
|
+
url: 'https://36kr.com/p/' + article.id,
|
|
45
|
+
cover: article.cover || '',
|
|
46
|
+
views: article.views_count || 0,
|
|
47
|
+
comments: article.comments_count || 0,
|
|
48
|
+
likes: article.likes_count || 0,
|
|
49
|
+
tags: (article.tags || []).map(t => t.name || t)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "36kr/search",
|
|
4
|
+
"description": "36氪文章搜索",
|
|
5
|
+
"domain": "36kr.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"keyword": {"required": true, "description": "搜索关键词"},
|
|
8
|
+
"count": {"required": false, "description": "返回数量 (默认 20, 最多 50)"}
|
|
9
|
+
},
|
|
10
|
+
"readOnly": true,
|
|
11
|
+
"example": "bws 36kr/search AI"
|
|
12
|
+
}
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
async function(args) {
|
|
16
|
+
if (!args.keyword) return {error: 'Missing argument: keyword', hint: 'Provide a search keyword'};
|
|
17
|
+
const count = Math.min(parseInt(args.count) || 20, 50);
|
|
18
|
+
|
|
19
|
+
const resp = await fetch('https://36kr.com/api/search-column/mainsite?per_page=' + count + '&page=1&keyword=' + encodeURIComponent(args.keyword), {
|
|
20
|
+
credentials: 'include'
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (!resp.ok) {
|
|
24
|
+
return {error: 'HTTP ' + resp.status, hint: 'Open 36kr.com in browser first'};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let data;
|
|
28
|
+
try {
|
|
29
|
+
data = await resp.json();
|
|
30
|
+
} catch (e) {
|
|
31
|
+
return {error: 'Failed to parse response', hint: '36kr API may have changed'};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!data || data.code !== 0 || !data.data) {
|
|
35
|
+
return {error: data.message || 'No data returned', hint: 'Try a different keyword'};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const items = (data.data.items || []).map(item => ({
|
|
39
|
+
id: item.id,
|
|
40
|
+
title: item.title || '',
|
|
41
|
+
summary: item.summary || item.description || '',
|
|
42
|
+
author: item.author || item.user?.name || '',
|
|
43
|
+
published_at: item.published_at || item.created_at || '',
|
|
44
|
+
url: 'https://36kr.com/p/' + item.id,
|
|
45
|
+
cover: item.cover || '',
|
|
46
|
+
views: item.views_count || 0,
|
|
47
|
+
comments: item.comments_count || 0
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
keyword: args.keyword,
|
|
52
|
+
count: items.length,
|
|
53
|
+
items
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "netease/hot",
|
|
4
|
+
"description": "网易新闻热榜",
|
|
5
|
+
"domain": "www.163.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"count": {"required": false, "description": "返回数量 (默认 20, 最多 50)"}
|
|
8
|
+
},
|
|
9
|
+
"readOnly": true,
|
|
10
|
+
"example": "bws netease/hot"
|
|
11
|
+
}
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
async function(args) {
|
|
15
|
+
const maxCount = Math.min(parseInt(args.count) || 20, 50);
|
|
16
|
+
|
|
17
|
+
const resp = await fetch('https://m.163.com/fe/api/hot/news/flow', {
|
|
18
|
+
credentials: 'include'
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (!resp.ok) {
|
|
22
|
+
return {error: 'HTTP ' + resp.status, hint: 'Open www.163.com in browser first'};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let data;
|
|
26
|
+
try {
|
|
27
|
+
data = await resp.json();
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return {error: 'Failed to parse response', hint: 'Netease API may have changed'};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!data || !data.data || !data.data.list) {
|
|
33
|
+
return {error: 'No data returned', hint: 'Netease API may have changed'};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const items = data.data.list.slice(0, maxCount).map((item, i) => ({
|
|
37
|
+
rank: i + 1,
|
|
38
|
+
id: item.docid,
|
|
39
|
+
title: item.title || '',
|
|
40
|
+
cover: item.imgsrc || '',
|
|
41
|
+
source: item.source || '',
|
|
42
|
+
time: item.ptime || '',
|
|
43
|
+
url: 'https://www.163.com/dy/article/' + item.docid + '.html',
|
|
44
|
+
mobileUrl: 'https://m.163.com/dy/article/' + item.docid + '.html'
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
count: items.length,
|
|
49
|
+
items
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "qqnews/hot",
|
|
4
|
+
"description": "腾讯新闻热榜",
|
|
5
|
+
"domain": "news.qq.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"count": {"required": false, "description": "返回数量 (默认 20, 最多 50)"}
|
|
8
|
+
},
|
|
9
|
+
"readOnly": true,
|
|
10
|
+
"example": "bws qqnews/hot"
|
|
11
|
+
}
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
async function(args) {
|
|
15
|
+
const maxCount = Math.min(parseInt(args.count) || 20, 50);
|
|
16
|
+
|
|
17
|
+
const resp = await fetch('https://r.inews.qq.com/gw/event/hot_ranking_list?page_size=50', {
|
|
18
|
+
credentials: 'include'
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (!resp.ok) {
|
|
22
|
+
return {error: 'HTTP ' + resp.status, hint: 'Open news.qq.com in browser first'};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let data;
|
|
26
|
+
try {
|
|
27
|
+
data = await resp.json();
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return {error: 'Failed to parse response', hint: 'QQ News API may have changed'};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!data || !data.idlist || !data.idlist[0] || !data.idlist[0].newslist) {
|
|
33
|
+
return {error: 'No data returned', hint: 'QQ News API may have changed'};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const list = data.idlist[0].newslist.slice(1, maxCount + 1);
|
|
37
|
+
const items = list.map((item, i) => ({
|
|
38
|
+
rank: i + 1,
|
|
39
|
+
id: item.id,
|
|
40
|
+
title: item.title || '',
|
|
41
|
+
desc: item.abstract || '',
|
|
42
|
+
cover: item.miniProShareImage || '',
|
|
43
|
+
source: item.source || '',
|
|
44
|
+
hot: item.hotEvent?.hotScore || 0,
|
|
45
|
+
url: 'https://new.qq.com/rain/a/' + item.id,
|
|
46
|
+
mobileUrl: 'https://view.inews.qq.com/k/' + item.id
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
count: items.length,
|
|
51
|
+
items
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "sina/hot",
|
|
4
|
+
"description": "新浪新闻热榜",
|
|
5
|
+
"domain": "news.sina.com.cn",
|
|
6
|
+
"args": {
|
|
7
|
+
"type": {"required": false, "description": "分类: all/china/world/society/sports/finance/ent/tech/mil (默认 all)"},
|
|
8
|
+
"count": {"required": false, "description": "返回数量 (默认 20, 最多 50)"}
|
|
9
|
+
},
|
|
10
|
+
"readOnly": true,
|
|
11
|
+
"example": "bws sina/hot tech"
|
|
12
|
+
}
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
async function(args) {
|
|
16
|
+
const typeMap = {
|
|
17
|
+
'all': {www: 'news', params: 'www_www_all_suda_suda'},
|
|
18
|
+
'china': {www: 'news', params: 'news_china_suda'},
|
|
19
|
+
'world': {www: 'news', params: 'news_world_suda'},
|
|
20
|
+
'society': {www: 'news', params: 'news_society_suda'},
|
|
21
|
+
'sports': {www: 'sports', params: 'sports_suda'},
|
|
22
|
+
'finance': {www: 'finance', params: 'finance_0_suda'},
|
|
23
|
+
'ent': {www: 'ent', params: 'ent_suda'},
|
|
24
|
+
'tech': {www: 'tech', params: 'tech_news_suda'},
|
|
25
|
+
'mil': {www: 'news', params: 'news_mil_suda'}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const type = args.type || 'all';
|
|
29
|
+
const config = typeMap[type] || typeMap['all'];
|
|
30
|
+
const maxCount = Math.min(parseInt(args.count) || 20, 50);
|
|
31
|
+
|
|
32
|
+
const now = new Date();
|
|
33
|
+
const year = now.getFullYear();
|
|
34
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
35
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
36
|
+
const dateStr = '' + year + month + day;
|
|
37
|
+
|
|
38
|
+
const url = 'https://top.' + config.www + '.sina.com.cn/ws/GetTopDataList.php?top_type=day&top_cat=' + config.params + '&top_time=' + dateStr + '&top_show_num=50';
|
|
39
|
+
|
|
40
|
+
const resp = await fetch(url, {credentials: 'include'});
|
|
41
|
+
|
|
42
|
+
if (!resp.ok) {
|
|
43
|
+
return {error: 'HTTP ' + resp.status, hint: 'Open news.sina.com.cn in browser first'};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let text;
|
|
47
|
+
try {
|
|
48
|
+
text = await resp.text();
|
|
49
|
+
} catch (e) {
|
|
50
|
+
return {error: 'Failed to fetch response', hint: 'Sina API may have changed'};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Parse JSONP: var data = {...};
|
|
54
|
+
let data;
|
|
55
|
+
try {
|
|
56
|
+
const prefix = 'var data = ';
|
|
57
|
+
if (!text.startsWith(prefix)) {
|
|
58
|
+
return {error: 'Invalid response format', hint: 'Sina API may have changed'};
|
|
59
|
+
}
|
|
60
|
+
let jsonStr = text.slice(prefix.length).trim();
|
|
61
|
+
if (jsonStr.endsWith(';')) jsonStr = jsonStr.slice(0, -1);
|
|
62
|
+
data = JSON.parse(jsonStr);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
return {error: 'Failed to parse response', hint: 'Sina API may have changed'};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!data || !data.data) {
|
|
68
|
+
return {error: 'No data returned', hint: 'Sina API may have changed'};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const items = data.data.slice(0, maxCount).map((item, i) => ({
|
|
72
|
+
rank: i + 1,
|
|
73
|
+
id: item.id,
|
|
74
|
+
title: item.title || '',
|
|
75
|
+
source: item.media || '',
|
|
76
|
+
hot: parseFloat((item.top_num || '0').replace(/,/g, '')),
|
|
77
|
+
time: (item.create_date || '') + ' ' + (item.create_time || ''),
|
|
78
|
+
url: item.url || ''
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
type: type,
|
|
83
|
+
count: items.length,
|
|
84
|
+
items
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "thepaper/hot",
|
|
4
|
+
"description": "澎湃新闻热榜",
|
|
5
|
+
"domain": "www.thepaper.cn",
|
|
6
|
+
"args": {
|
|
7
|
+
"count": {"required": false, "description": "返回数量 (默认 20, 最多 50)"}
|
|
8
|
+
},
|
|
9
|
+
"readOnly": true,
|
|
10
|
+
"example": "bws thepaper/hot"
|
|
11
|
+
}
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
async function(args) {
|
|
15
|
+
const maxCount = Math.min(parseInt(args.count) || 20, 50);
|
|
16
|
+
|
|
17
|
+
const resp = await fetch('https://cache.thepaper.cn/contentapi/wwwIndex/rightSidebar', {
|
|
18
|
+
credentials: 'include'
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (!resp.ok) {
|
|
22
|
+
return {error: 'HTTP ' + resp.status, hint: 'Open www.thepaper.cn in browser first'};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let data;
|
|
26
|
+
try {
|
|
27
|
+
data = await resp.json();
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return {error: 'Failed to parse response', hint: 'Thepaper API may have changed'};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!data || !data.data || !data.data.hotNews) {
|
|
33
|
+
return {error: 'No data returned', hint: 'Thepaper API may have changed'};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const items = data.data.hotNews.slice(0, maxCount).map((item, i) => ({
|
|
37
|
+
rank: i + 1,
|
|
38
|
+
id: item.contId,
|
|
39
|
+
title: item.name || '',
|
|
40
|
+
cover: item.pic || '',
|
|
41
|
+
hot: parseInt(item.praiseTimes) || 0,
|
|
42
|
+
url: 'https://www.thepaper.cn/newsDetail_forward_' + item.contId,
|
|
43
|
+
mobileUrl: 'https://m.thepaper.cn/newsDetail_forward_' + item.contId
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
count: items.length,
|
|
48
|
+
items
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "toutiao/feed",
|
|
4
|
+
"description": "今日头条分类新闻(支持关键词过滤)",
|
|
5
|
+
"domain": "www.toutiao.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"category": {"required": false, "description": "分类: hot/tech/entertainment/sports/finance/military/world/game/car (默认 hot)"},
|
|
8
|
+
"keyword": {"required": false, "description": "关键词过滤(可选)"},
|
|
9
|
+
"count": {"required": false, "description": "返回数量 (默认 20, 最多 50)"}
|
|
10
|
+
},
|
|
11
|
+
"readOnly": true,
|
|
12
|
+
"example": "bws toutiao/feed tech --keyword AI"
|
|
13
|
+
}
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
async function(args) {
|
|
17
|
+
const categoryMap = {
|
|
18
|
+
'all': '__all__',
|
|
19
|
+
'hot': 'news_hot',
|
|
20
|
+
'tech': 'news_tech',
|
|
21
|
+
'entertainment': 'news_entertainment',
|
|
22
|
+
'sports': 'news_sports',
|
|
23
|
+
'finance': 'news_finance',
|
|
24
|
+
'military': 'news_military',
|
|
25
|
+
'world': 'news_world',
|
|
26
|
+
'game': 'news_game',
|
|
27
|
+
'car': 'news_car',
|
|
28
|
+
'society': 'news_society',
|
|
29
|
+
'fashion': 'news_fashion',
|
|
30
|
+
'travel': 'news_travel',
|
|
31
|
+
'history': 'news_history',
|
|
32
|
+
'food': 'news_food'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const category = categoryMap[args.category] || categoryMap['hot'];
|
|
36
|
+
const maxCount = Math.min(parseInt(args.count) || 20, 50);
|
|
37
|
+
const keyword = args.keyword ? args.keyword.toLowerCase() : null;
|
|
38
|
+
|
|
39
|
+
const resp = await fetch('https://www.toutiao.com/api/pc/feed/?category=' + category + '&max_behot_time=0', {
|
|
40
|
+
credentials: 'include'
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!resp.ok) {
|
|
44
|
+
return {error: 'HTTP ' + resp.status, hint: 'Open www.toutiao.com in browser first'};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let data;
|
|
48
|
+
try {
|
|
49
|
+
data = await resp.json();
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return {error: 'Failed to parse response', hint: 'Toutiao API may have changed'};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!data || !data.data) {
|
|
55
|
+
return {error: 'No data returned', hint: 'Open www.toutiao.com in browser first'};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const results = [];
|
|
59
|
+
for (const item of data.data) {
|
|
60
|
+
const title = item.title || '';
|
|
61
|
+
const abstract = item.abstract || '';
|
|
62
|
+
const keywords = item.keywords || '';
|
|
63
|
+
const source = item.source || item.media_name || '';
|
|
64
|
+
|
|
65
|
+
// Filter by keyword if provided
|
|
66
|
+
if (keyword) {
|
|
67
|
+
const searchText = (title + ' ' + abstract + ' ' + keywords).toLowerCase();
|
|
68
|
+
if (!searchText.includes(keyword)) continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
results.push({
|
|
72
|
+
title,
|
|
73
|
+
snippet: abstract.substring(0, 300),
|
|
74
|
+
source,
|
|
75
|
+
time: item.datetime || '',
|
|
76
|
+
url: item.article_url || item.display_url || item.share_url || '',
|
|
77
|
+
tag: item.tag || '',
|
|
78
|
+
hot_value: item.hot || 0,
|
|
79
|
+
comment_count: item.comment_count || 0
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (results.length >= maxCount) break;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
category: args.category || 'hot',
|
|
87
|
+
keyword: args.keyword || null,
|
|
88
|
+
count: results.length,
|
|
89
|
+
total_fetched: data.data.length,
|
|
90
|
+
results
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "weibo/hot",
|
|
4
|
+
"description": "微博热搜榜",
|
|
5
|
+
"domain": "weibo.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"count": {"required": false, "description": "返回数量 (默认 30, 最多 50)"}
|
|
8
|
+
},
|
|
9
|
+
"readOnly": true,
|
|
10
|
+
"example": "bws weibo/hot"
|
|
11
|
+
}
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
async function(args) {
|
|
15
|
+
const maxCount = Math.min(parseInt(args.count) || 30, 50);
|
|
16
|
+
|
|
17
|
+
const resp = await fetch('https://weibo.com/ajax/side/hotSearch', {
|
|
18
|
+
credentials: 'include',
|
|
19
|
+
headers: {
|
|
20
|
+
'Referer': 'https://weibo.com/'
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (!resp.ok) {
|
|
25
|
+
return {error: 'HTTP ' + resp.status, hint: 'Open weibo.com in browser first'};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let data;
|
|
29
|
+
try {
|
|
30
|
+
data = await resp.json();
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return {error: 'Failed to parse response', hint: 'Weibo API may have changed'};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!data || !data.data || !data.data.realtime) {
|
|
36
|
+
return {error: 'No data returned', hint: 'Open weibo.com and login first'};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const items = data.data.realtime.slice(0, maxCount).map((item, i) => {
|
|
40
|
+
const title = item.word || item.word_scheme || '';
|
|
41
|
+
return {
|
|
42
|
+
rank: i + 1,
|
|
43
|
+
id: item.mid || item.word_scheme || '',
|
|
44
|
+
title: title,
|
|
45
|
+
tag: item.label_name || '',
|
|
46
|
+
hot: item.num || 0,
|
|
47
|
+
url: 'https://s.weibo.com/weibo?q=' + encodeURIComponent(title),
|
|
48
|
+
mobileUrl: 'https://m.weibo.cn/search?containerid=100103type%3D1%26q%3D' + encodeURIComponent(title)
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
count: items.length,
|
|
54
|
+
items
|
|
55
|
+
};
|
|
56
|
+
}
|
package/sites/douban/comments.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/* @meta
|
|
2
|
-
{
|
|
3
|
-
"name": "douban/comments",
|
|
4
|
-
"description": "Get short reviews/comments for a Douban movie or TV show",
|
|
5
|
-
"domain": "movie.douban.com",
|
|
6
|
-
"args": {
|
|
7
|
-
"id": {"required": true, "description": "Douban subject ID (e.g. 1292052)"},
|
|
8
|
-
"sort": {"required": false, "description": "Sort order: new_score (default, hot), time (newest first)"},
|
|
9
|
-
"count": {"required": false, "description": "Number of comments (default: 20, max: 50)"}
|
|
10
|
-
},
|
|
11
|
-
"capabilities": ["network"],
|
|
12
|
-
"readOnly": true,
|
|
13
|
-
"example": "ping-browser site douban/comments 1292052"
|
|
14
|
-
}
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
async function(args) {
|
|
18
|
-
if (!args.id) return {error: 'Missing argument: id'};
|
|
19
|
-
const id = String(args.id).trim();
|
|
20
|
-
const sort = args.sort || 'new_score';
|
|
21
|
-
const count = Math.min(parseInt(args.count) || 20, 50);
|
|
22
|
-
|
|
23
|
-
if (sort !== 'new_score' && sort !== 'time') {
|
|
24
|
-
return {error: 'Invalid sort. Use "new_score" (hot) or "time" (newest)'};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const url = 'https://movie.douban.com/j/subject/' + id + '/comments?start=0&limit=' + count + '&status=P&sort=' + sort;
|
|
28
|
-
|
|
29
|
-
const resp = await fetch(url, {credentials: 'include'});
|
|
30
|
-
if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Not logged in?'};
|
|
31
|
-
const d = await resp.json();
|
|
32
|
-
|
|
33
|
-
if (d.retcode !== 1 || !d.result) return {error: 'Failed to fetch comments', hint: 'Invalid ID or not logged in?'};
|
|
34
|
-
|
|
35
|
-
const ratingMap = {'1': '很差', '2': '较差', '3': '还行', '4': '推荐', '5': '力荐'};
|
|
36
|
-
|
|
37
|
-
const comments = (d.result.normal || []).map(function(c) {
|
|
38
|
-
var userId = c.user?.path?.match(/people\/([^/]+)/)?.[1];
|
|
39
|
-
return {
|
|
40
|
-
id: c.id,
|
|
41
|
-
author: c.user?.name || '',
|
|
42
|
-
author_id: userId || '',
|
|
43
|
-
rating: c.rating ? parseInt(c.rating) : null,
|
|
44
|
-
rating_label: c.rating_word || ratingMap[c.rating] || '',
|
|
45
|
-
content: c.content || '',
|
|
46
|
-
votes: c.votes || 0,
|
|
47
|
-
date: c.time || ''
|
|
48
|
-
};
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
subject_id: id,
|
|
53
|
-
sort: sort,
|
|
54
|
-
total: d.result.total_num || 0,
|
|
55
|
-
count: comments.length,
|
|
56
|
-
comments: comments
|
|
57
|
-
};
|
|
58
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/* @meta
|
|
2
|
-
{
|
|
3
|
-
"name": "douban/movie-hot",
|
|
4
|
-
"description": "Get hot/trending movies or TV shows on Douban by tag",
|
|
5
|
-
"domain": "movie.douban.com",
|
|
6
|
-
"args": {
|
|
7
|
-
"type": {"required": false, "description": "Type: movie (default) or tv"},
|
|
8
|
-
"tag": {"required": false, "description": "Tag filter (default: 热门). Movies: 热门/最新/豆瓣高分/冷门佳片/华语/欧美/韩国/日本. TV: 热门/国产剧/综艺/美剧/日剧/韩剧/日本动画/纪录片"},
|
|
9
|
-
"count": {"required": false, "description": "Number of results (default: 20, max: 50)"}
|
|
10
|
-
},
|
|
11
|
-
"capabilities": ["network"],
|
|
12
|
-
"readOnly": true,
|
|
13
|
-
"example": "ping-browser site douban/movie-hot movie 豆瓣高分"
|
|
14
|
-
}
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
async function(args) {
|
|
18
|
-
const type = (args.type || 'movie').toLowerCase();
|
|
19
|
-
if (type !== 'movie' && type !== 'tv') return {error: 'Invalid type. Use "movie" or "tv"'};
|
|
20
|
-
|
|
21
|
-
const tag = args.tag || '热门';
|
|
22
|
-
const count = Math.min(parseInt(args.count) || 20, 50);
|
|
23
|
-
|
|
24
|
-
const url = 'https://movie.douban.com/j/search_subjects?type=' + type
|
|
25
|
-
+ '&tag=' + encodeURIComponent(tag)
|
|
26
|
-
+ '&page_limit=' + count
|
|
27
|
-
+ '&page_start=0';
|
|
28
|
-
|
|
29
|
-
const resp = await fetch(url, {credentials: 'include'});
|
|
30
|
-
if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Not logged in?'};
|
|
31
|
-
const d = await resp.json();
|
|
32
|
-
|
|
33
|
-
if (!d.subjects) return {error: 'No data returned', hint: 'Invalid tag or not logged in?'};
|
|
34
|
-
|
|
35
|
-
const items = d.subjects.map(function(s, i) {
|
|
36
|
-
return {
|
|
37
|
-
rank: i + 1,
|
|
38
|
-
id: s.id,
|
|
39
|
-
title: s.title,
|
|
40
|
-
rating: s.rate ? parseFloat(s.rate) : null,
|
|
41
|
-
cover: s.cover,
|
|
42
|
-
url: s.url,
|
|
43
|
-
playable: s.playable,
|
|
44
|
-
is_new: s.is_new,
|
|
45
|
-
episodes_info: s.episodes_info || null
|
|
46
|
-
};
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// Also fetch available tags for reference
|
|
50
|
-
var tagsResp = await fetch('https://movie.douban.com/j/search_tags?type=' + type + '&source=index', {credentials: 'include'});
|
|
51
|
-
var availableTags = [];
|
|
52
|
-
if (tagsResp.ok) {
|
|
53
|
-
var tagsData = await tagsResp.json();
|
|
54
|
-
availableTags = tagsData.tags || [];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
type: type,
|
|
59
|
-
tag: tag,
|
|
60
|
-
count: items.length,
|
|
61
|
-
available_tags: availableTags,
|
|
62
|
-
items: items
|
|
63
|
-
};
|
|
64
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/* @meta
|
|
2
|
-
{
|
|
3
|
-
"name": "douban/movie-top",
|
|
4
|
-
"description": "Get top rated movies by genre from Douban charts",
|
|
5
|
-
"domain": "movie.douban.com",
|
|
6
|
-
"args": {
|
|
7
|
-
"genre": {"required": false, "description": "Genre (default: 剧情). Options: 剧情/喜剧/动作/爱情/科幻/动画/悬疑/惊悚/恐怖/纪录片/短片/情色/音乐/歌舞/家庭/儿童/传记/历史/战争/犯罪/西部/奇幻/冒险/灾难/武侠/古装/运动/黑色电影"},
|
|
8
|
-
"count": {"required": false, "description": "Number of results (default: 20, max: 50)"}
|
|
9
|
-
},
|
|
10
|
-
"capabilities": ["network"],
|
|
11
|
-
"readOnly": true,
|
|
12
|
-
"example": "ping-browser site douban/movie-top 科幻 10"
|
|
13
|
-
}
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
async function(args) {
|
|
17
|
-
// Genre name to type ID mapping
|
|
18
|
-
const genreMap = {
|
|
19
|
-
'剧情': 11, '喜剧': 24, '动作': 5, '爱情': 13, '科幻': 17,
|
|
20
|
-
'动画': 25, '悬疑': 10, '惊悚': 19, '恐怖': 20, '纪录片': 1,
|
|
21
|
-
'短片': 23, '情色': 6, '音乐': 14, '歌舞': 7, '家庭': 28,
|
|
22
|
-
'儿童': 8, '传记': 2, '历史': 4, '战争': 22, '犯罪': 3,
|
|
23
|
-
'西部': 27, '奇幻': 26, '冒险': 15, '灾难': 12, '武侠': 29,
|
|
24
|
-
'古装': 30, '运动': 18, '黑色电影': 31
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const genre = args.genre || '剧情';
|
|
28
|
-
const typeId = genreMap[genre];
|
|
29
|
-
if (!typeId) return {error: 'Unknown genre: ' + genre, hint: 'Available: ' + Object.keys(genreMap).join(', ')};
|
|
30
|
-
|
|
31
|
-
const count = Math.min(parseInt(args.count) || 20, 50);
|
|
32
|
-
|
|
33
|
-
const url = 'https://movie.douban.com/j/chart/top_list?type=' + typeId
|
|
34
|
-
+ '&interval_id=100%3A90&action=&start=0&limit=' + count;
|
|
35
|
-
|
|
36
|
-
const resp = await fetch(url, {credentials: 'include'});
|
|
37
|
-
if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Not logged in?'};
|
|
38
|
-
const data = await resp.json();
|
|
39
|
-
|
|
40
|
-
if (!Array.isArray(data)) return {error: 'Unexpected response format'};
|
|
41
|
-
|
|
42
|
-
const items = data.map(function(s) {
|
|
43
|
-
return {
|
|
44
|
-
rank: s.rank,
|
|
45
|
-
id: s.id,
|
|
46
|
-
title: s.title,
|
|
47
|
-
rating: parseFloat(s.score) || null,
|
|
48
|
-
votes: s.vote_count,
|
|
49
|
-
types: s.types,
|
|
50
|
-
regions: s.regions,
|
|
51
|
-
actors: (s.actors || []).slice(0, 5),
|
|
52
|
-
release_date: s.release_date,
|
|
53
|
-
cover: s.cover_url,
|
|
54
|
-
url: s.url,
|
|
55
|
-
playable: s.is_playable
|
|
56
|
-
};
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
genre: genre,
|
|
61
|
-
count: items.length,
|
|
62
|
-
available_genres: Object.keys(genreMap),
|
|
63
|
-
items: items
|
|
64
|
-
};
|
|
65
|
-
}
|
package/sites/douban/movie.js
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/* @meta
|
|
2
|
-
{
|
|
3
|
-
"name": "douban/movie",
|
|
4
|
-
"description": "Get detailed movie/TV info with rating, cast, and hot reviews from Douban",
|
|
5
|
-
"domain": "movie.douban.com",
|
|
6
|
-
"args": {
|
|
7
|
-
"id": {"required": true, "description": "Douban subject ID (e.g. 1292052 for The Shawshank Redemption)"}
|
|
8
|
-
},
|
|
9
|
-
"capabilities": ["network"],
|
|
10
|
-
"readOnly": true,
|
|
11
|
-
"example": "ping-browser site douban/movie 1292052"
|
|
12
|
-
}
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
async function(args) {
|
|
16
|
-
if (!args.id) return {error: 'Missing argument: id'};
|
|
17
|
-
const id = String(args.id).trim();
|
|
18
|
-
|
|
19
|
-
// Fetch structured data from the JSON API
|
|
20
|
-
const apiResp = await fetch('https://movie.douban.com/j/subject_abstract?subject_id=' + id, {credentials: 'include'});
|
|
21
|
-
if (!apiResp.ok) return {error: 'HTTP ' + apiResp.status, hint: 'Not logged in or invalid ID?'};
|
|
22
|
-
const apiData = await apiResp.json();
|
|
23
|
-
if (apiData.r !== 0 || !apiData.subject) return {error: 'Subject not found', hint: 'Check the ID'};
|
|
24
|
-
|
|
25
|
-
const s = apiData.subject;
|
|
26
|
-
|
|
27
|
-
// Also fetch the HTML page for richer data (summary, rating distribution, hot comments)
|
|
28
|
-
const pageResp = await fetch('https://movie.douban.com/subject/' + id + '/', {credentials: 'include'});
|
|
29
|
-
let summary = '', ratingDist = {}, hotComments = [], recommendations = [], votes = null, info = '';
|
|
30
|
-
|
|
31
|
-
if (pageResp.ok) {
|
|
32
|
-
const html = await pageResp.text();
|
|
33
|
-
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
34
|
-
|
|
35
|
-
// Summary
|
|
36
|
-
const summaryEl = doc.querySelector('[property="v:summary"]');
|
|
37
|
-
summary = summaryEl ? summaryEl.textContent.trim() : '';
|
|
38
|
-
|
|
39
|
-
// Vote count
|
|
40
|
-
const votesEl = doc.querySelector('[property="v:votes"]');
|
|
41
|
-
votes = votesEl ? parseInt(votesEl.textContent) : null;
|
|
42
|
-
|
|
43
|
-
// Info block
|
|
44
|
-
const infoEl = doc.querySelector('#info');
|
|
45
|
-
info = infoEl ? infoEl.innerText || infoEl.textContent.trim() : '';
|
|
46
|
-
|
|
47
|
-
// Rating distribution
|
|
48
|
-
doc.querySelectorAll('.ratings-on-weight .item').forEach(function(el) {
|
|
49
|
-
var star = el.querySelector('span:first-child');
|
|
50
|
-
var pct = el.querySelector('.rating_per');
|
|
51
|
-
if (star && pct) ratingDist[star.textContent.trim()] = pct.textContent.trim();
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// Hot comments
|
|
55
|
-
doc.querySelectorAll('#hot-comments .comment-item').forEach(function(el) {
|
|
56
|
-
var author = el.querySelector('.comment-info a');
|
|
57
|
-
var rating = el.querySelector('.comment-info .rating');
|
|
58
|
-
var content = el.querySelector('.short');
|
|
59
|
-
var voteCount = el.querySelector('.vote-count');
|
|
60
|
-
var date = el.querySelector('.comment-time');
|
|
61
|
-
hotComments.push({
|
|
62
|
-
author: author ? author.textContent.trim() : '',
|
|
63
|
-
rating: rating ? rating.title : '',
|
|
64
|
-
content: content ? content.textContent.trim() : '',
|
|
65
|
-
votes: voteCount ? parseInt(voteCount.textContent) || 0 : 0,
|
|
66
|
-
date: date ? date.textContent.trim() : ''
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// Recommendations
|
|
71
|
-
doc.querySelectorAll('.recommendations-bd dl').forEach(function(dl) {
|
|
72
|
-
var a = dl.querySelector('dd a');
|
|
73
|
-
if (a) {
|
|
74
|
-
var recId = a.href?.match(/subject\/(\d+)/)?.[1];
|
|
75
|
-
recommendations.push({title: a.textContent.trim(), id: recId, url: a.href});
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Parse info block for structured fields
|
|
81
|
-
const parseInfo = function(text) {
|
|
82
|
-
const result = {};
|
|
83
|
-
const lines = text.split('\n').map(function(l) { return l.trim(); }).filter(Boolean);
|
|
84
|
-
lines.forEach(function(line) {
|
|
85
|
-
var m = line.match(/^(.+?):\s*(.+)$/);
|
|
86
|
-
if (m) result[m[1].trim()] = m[2].trim();
|
|
87
|
-
});
|
|
88
|
-
return result;
|
|
89
|
-
};
|
|
90
|
-
const infoFields = parseInfo(info);
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
id: s.id,
|
|
94
|
-
title: s.title,
|
|
95
|
-
subtype: s.subtype,
|
|
96
|
-
is_tv: s.is_tv,
|
|
97
|
-
rating: parseFloat(s.rate) || null,
|
|
98
|
-
votes: votes,
|
|
99
|
-
rating_distribution: ratingDist,
|
|
100
|
-
directors: s.directors,
|
|
101
|
-
actors: s.actors,
|
|
102
|
-
types: s.types,
|
|
103
|
-
region: s.region,
|
|
104
|
-
duration: s.duration,
|
|
105
|
-
release_year: s.release_year,
|
|
106
|
-
episodes_count: s.episodes_count || null,
|
|
107
|
-
imdb: infoFields['IMDb'] || null,
|
|
108
|
-
alias: infoFields['又名'] || null,
|
|
109
|
-
language: infoFields['语言'] || null,
|
|
110
|
-
release_date: infoFields['上映日期'] || infoFields['首播'] || null,
|
|
111
|
-
summary: summary,
|
|
112
|
-
playable: s.playable,
|
|
113
|
-
url: s.url,
|
|
114
|
-
hot_comments: hotComments,
|
|
115
|
-
recommendations: recommendations
|
|
116
|
-
};
|
|
117
|
-
}
|
package/sites/douban/search.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/* @meta
|
|
2
|
-
{
|
|
3
|
-
"name": "douban/search",
|
|
4
|
-
"description": "Search Douban across movies, books, and music",
|
|
5
|
-
"domain": "www.douban.com",
|
|
6
|
-
"args": {
|
|
7
|
-
"keyword": {"required": true, "description": "Search keyword (Chinese or English)"}
|
|
8
|
-
},
|
|
9
|
-
"capabilities": ["network"],
|
|
10
|
-
"readOnly": true,
|
|
11
|
-
"example": "ping-browser site douban/search 三体"
|
|
12
|
-
}
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
async function(args) {
|
|
16
|
-
if (!args.keyword) return {error: 'Missing argument: keyword'};
|
|
17
|
-
const q = encodeURIComponent(args.keyword);
|
|
18
|
-
|
|
19
|
-
// Try the rich search_suggest endpoint (requires www.douban.com origin)
|
|
20
|
-
var resp;
|
|
21
|
-
var usedFallback = false;
|
|
22
|
-
try {
|
|
23
|
-
resp = await fetch('https://www.douban.com/j/search_suggest?q=' + q, {credentials: 'include'});
|
|
24
|
-
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
25
|
-
} catch (e) {
|
|
26
|
-
// Fallback: use movie.douban.com subject_suggest (works cross-subdomain via same eTLD+1 cookies)
|
|
27
|
-
try {
|
|
28
|
-
resp = await fetch('/j/subject_suggest?q=' + q, {credentials: 'include'});
|
|
29
|
-
usedFallback = true;
|
|
30
|
-
} catch (e2) {
|
|
31
|
-
return {error: 'Search failed: ' + e2.message, hint: 'Not logged in? Navigate to www.douban.com first.'};
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Not logged in?'};
|
|
36
|
-
var d = await resp.json();
|
|
37
|
-
|
|
38
|
-
if (usedFallback) {
|
|
39
|
-
// subject_suggest returns an array directly
|
|
40
|
-
var items = (Array.isArray(d) ? d : []).map(function(c, i) {
|
|
41
|
-
return {
|
|
42
|
-
rank: i + 1,
|
|
43
|
-
id: c.id,
|
|
44
|
-
type: c.type === 'movie' ? 'movie' : c.type === 'b' ? 'book' : c.type || 'unknown',
|
|
45
|
-
title: c.title,
|
|
46
|
-
subtitle: c.sub_title || '',
|
|
47
|
-
rating: null,
|
|
48
|
-
info: '',
|
|
49
|
-
year: c.year || null,
|
|
50
|
-
cover: c.img || c.pic || null,
|
|
51
|
-
url: c.url
|
|
52
|
-
};
|
|
53
|
-
});
|
|
54
|
-
return {
|
|
55
|
-
keyword: args.keyword,
|
|
56
|
-
count: items.length,
|
|
57
|
-
results: items,
|
|
58
|
-
suggestions: [],
|
|
59
|
-
note: 'Limited results (movie/book only). For richer results, navigate to www.douban.com first.'
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Rich search_suggest response with cards
|
|
64
|
-
var cards = (d.cards || []).map(function(c, i) {
|
|
65
|
-
var id = c.url && c.url.match(/subject\/(\d+)/);
|
|
66
|
-
id = id ? id[1] : null;
|
|
67
|
-
var ratingMatch = c.card_subtitle && c.card_subtitle.match(/([\d.]+)分/);
|
|
68
|
-
return {
|
|
69
|
-
rank: i + 1,
|
|
70
|
-
id: id,
|
|
71
|
-
type: c.type || 'unknown',
|
|
72
|
-
title: c.title,
|
|
73
|
-
subtitle: c.abstract || '',
|
|
74
|
-
rating: ratingMatch ? parseFloat(ratingMatch[1]) : null,
|
|
75
|
-
info: c.card_subtitle || '',
|
|
76
|
-
year: c.year || null,
|
|
77
|
-
cover: c.cover_url || null,
|
|
78
|
-
url: c.url
|
|
79
|
-
};
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
var suggestions = d.words || [];
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
keyword: args.keyword,
|
|
86
|
-
count: cards.length,
|
|
87
|
-
results: cards,
|
|
88
|
-
suggestions: suggestions
|
|
89
|
-
};
|
|
90
|
-
}
|
package/sites/douban/top250.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/* @meta
|
|
2
|
-
{
|
|
3
|
-
"name": "douban/top250",
|
|
4
|
-
"description": "Get Douban Top 250 movies list",
|
|
5
|
-
"domain": "movie.douban.com",
|
|
6
|
-
"args": {
|
|
7
|
-
"start": {"required": false, "description": "Start position (default: 0, step by 25). Use 0 for #1-25, 25 for #26-50, etc."}
|
|
8
|
-
},
|
|
9
|
-
"capabilities": ["network"],
|
|
10
|
-
"readOnly": true,
|
|
11
|
-
"example": "ping-browser site douban/top250"
|
|
12
|
-
}
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
async function(args) {
|
|
16
|
-
const start = parseInt(args.start) || 0;
|
|
17
|
-
|
|
18
|
-
const resp = await fetch('https://movie.douban.com/top250?start=' + start, {credentials: 'include'});
|
|
19
|
-
if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Not logged in?'};
|
|
20
|
-
const html = await resp.text();
|
|
21
|
-
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
22
|
-
|
|
23
|
-
const items = [];
|
|
24
|
-
doc.querySelectorAll('.grid_view .item').forEach(function(el) {
|
|
25
|
-
var rank = el.querySelector('.pic em');
|
|
26
|
-
var titleEl = el.querySelector('.hd a .title');
|
|
27
|
-
var otherTitleEl = el.querySelector('.hd a .other');
|
|
28
|
-
var ratingEl = el.querySelector('.rating_num');
|
|
29
|
-
var link = el.querySelector('.hd a');
|
|
30
|
-
var quoteEl = el.querySelector('.quote .inq') || el.querySelector('.quote span');
|
|
31
|
-
var infoEl = el.querySelector('.bd p');
|
|
32
|
-
|
|
33
|
-
// Vote count is in a span like "3268455人评价"
|
|
34
|
-
var voteSpans = el.querySelectorAll('.bd div span');
|
|
35
|
-
var votes = null;
|
|
36
|
-
voteSpans.forEach(function(sp) {
|
|
37
|
-
var m = sp.textContent.match(/(\d+)人评价/);
|
|
38
|
-
if (m) votes = parseInt(m[1]);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
var id = link?.href?.match(/subject\/(\d+)/)?.[1];
|
|
42
|
-
|
|
43
|
-
// Parse info line for director, year, region, genre
|
|
44
|
-
var infoText = infoEl ? infoEl.textContent.trim() : '';
|
|
45
|
-
var lines = infoText.split('\n').map(function(l) { return l.trim(); }).filter(Boolean);
|
|
46
|
-
var directorLine = lines[0] || '';
|
|
47
|
-
var metaLine = lines[1] || '';
|
|
48
|
-
var metaParts = metaLine.split('/').map(function(p) { return p.trim(); });
|
|
49
|
-
|
|
50
|
-
items.push({
|
|
51
|
-
rank: rank ? parseInt(rank.textContent) : null,
|
|
52
|
-
id: id,
|
|
53
|
-
title: titleEl ? titleEl.textContent.trim() : '',
|
|
54
|
-
other_title: otherTitleEl ? otherTitleEl.textContent.trim().replace(/^\s*\/\s*/, '') : '',
|
|
55
|
-
rating: ratingEl ? parseFloat(ratingEl.textContent) : null,
|
|
56
|
-
votes: votes,
|
|
57
|
-
quote: quoteEl ? quoteEl.textContent.trim() : '',
|
|
58
|
-
year: metaParts[0] || '',
|
|
59
|
-
region: metaParts[1] || '',
|
|
60
|
-
genre: metaParts[2] || '',
|
|
61
|
-
url: link ? link.href : ''
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
start: start,
|
|
67
|
-
count: items.length,
|
|
68
|
-
total: 250,
|
|
69
|
-
has_more: start + items.length < 250,
|
|
70
|
-
next_start: start + items.length < 250 ? start + 25 : null,
|
|
71
|
-
items: items
|
|
72
|
-
};
|
|
73
|
-
}
|
package/sites/github/fork.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/* @meta
|
|
2
|
-
{
|
|
3
|
-
"name": "github/fork",
|
|
4
|
-
"description": "Fork a GitHub repository",
|
|
5
|
-
"domain": "github.com",
|
|
6
|
-
"args": {
|
|
7
|
-
"repo": {"required": true, "description": "Repository to fork (owner/repo)"}
|
|
8
|
-
},
|
|
9
|
-
"capabilities": ["network"],
|
|
10
|
-
"readOnly": false,
|
|
11
|
-
"example": "ping-browser site github/fork epiral/bb-sites"
|
|
12
|
-
}
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
async function(args) {
|
|
16
|
-
if (!args.repo) return {error: 'Missing argument: repo'};
|
|
17
|
-
|
|
18
|
-
const resp = await fetch('https://api.github.com/repos/' + args.repo + '/forks', {
|
|
19
|
-
method: 'POST',
|
|
20
|
-
credentials: 'include',
|
|
21
|
-
headers: {'Content-Type': 'application/json'},
|
|
22
|
-
body: JSON.stringify({})
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
if (!resp.ok) {
|
|
26
|
-
const status = resp.status;
|
|
27
|
-
if (status === 401 || status === 403) return {error: 'HTTP ' + status, hint: 'Not logged in to GitHub'};
|
|
28
|
-
if (status === 404) return {error: 'Repo not found: ' + args.repo};
|
|
29
|
-
return {error: 'HTTP ' + status};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const fork = await resp.json();
|
|
33
|
-
return {
|
|
34
|
-
full_name: fork.full_name,
|
|
35
|
-
url: fork.html_url,
|
|
36
|
-
clone_url: fork.clone_url
|
|
37
|
-
};
|
|
38
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/* @meta
|
|
2
|
-
{
|
|
3
|
-
"name": "github/issue-create",
|
|
4
|
-
"description": "Create a GitHub issue",
|
|
5
|
-
"domain": "github.com",
|
|
6
|
-
"args": {
|
|
7
|
-
"repo": {"required": true, "description": "owner/repo format"},
|
|
8
|
-
"title": {"required": true, "description": "Issue title"},
|
|
9
|
-
"body": {"required": false, "description": "Issue body (markdown)"}
|
|
10
|
-
},
|
|
11
|
-
"capabilities": ["network"],
|
|
12
|
-
"readOnly": false,
|
|
13
|
-
"example": "ping-browser site github/issue-create epiral/bb-sites --title \"[reddit/me] returns empty\" --body \"Description here\""
|
|
14
|
-
}
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
async function(args) {
|
|
18
|
-
if (!args.repo) return {error: 'Missing argument: repo'};
|
|
19
|
-
if (!args.title) return {error: 'Missing argument: title'};
|
|
20
|
-
|
|
21
|
-
const resp = await fetch('https://api.github.com/repos/' + args.repo + '/issues', {
|
|
22
|
-
method: 'POST',
|
|
23
|
-
credentials: 'include',
|
|
24
|
-
headers: {'Content-Type': 'application/json'},
|
|
25
|
-
body: JSON.stringify({title: args.title, body: args.body || ''})
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
if (!resp.ok) {
|
|
29
|
-
const status = resp.status;
|
|
30
|
-
if (status === 401 || status === 403) return {error: 'HTTP ' + status, hint: 'Not logged in to GitHub'};
|
|
31
|
-
if (status === 404) return {error: 'Repo not found: ' + args.repo};
|
|
32
|
-
return {error: 'HTTP ' + status};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const issue = await resp.json();
|
|
36
|
-
return {
|
|
37
|
-
number: issue.number,
|
|
38
|
-
title: issue.title,
|
|
39
|
-
url: issue.html_url,
|
|
40
|
-
state: issue.state
|
|
41
|
-
};
|
|
42
|
-
}
|
package/sites/github/issues.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/* @meta
|
|
2
|
-
{
|
|
3
|
-
"name": "github/issues",
|
|
4
|
-
"description": "获取 GitHub 仓库的 issue 列表",
|
|
5
|
-
"domain": "github.com",
|
|
6
|
-
"args": {
|
|
7
|
-
"repo": {"required": true, "description": "owner/repo format"},
|
|
8
|
-
"state": {"required": false, "description": "open, closed, or all (default: open)"}
|
|
9
|
-
},
|
|
10
|
-
"capabilities": ["network"],
|
|
11
|
-
"readOnly": true,
|
|
12
|
-
"example": "ping-browser site github/issues epiral/ping-browser"
|
|
13
|
-
}
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
async function(args) {
|
|
17
|
-
if (!args.repo) return {error: 'Missing argument: repo'};
|
|
18
|
-
const state = args.state || 'open';
|
|
19
|
-
const resp = await fetch('https://api.github.com/repos/' + args.repo + '/issues?state=' + state + '&per_page=30', {credentials: 'include'});
|
|
20
|
-
if (!resp.ok) return {error: 'HTTP ' + resp.status};
|
|
21
|
-
const issues = await resp.json();
|
|
22
|
-
return {
|
|
23
|
-
repo: args.repo, state, count: issues.length,
|
|
24
|
-
issues: issues.map(i => ({
|
|
25
|
-
number: i.number, title: i.title, state: i.state,
|
|
26
|
-
url: i.html_url,
|
|
27
|
-
author: i.user?.login, labels: i.labels?.map(l => l.name),
|
|
28
|
-
comments: i.comments, created_at: i.created_at,
|
|
29
|
-
is_pr: !!i.pull_request
|
|
30
|
-
}))
|
|
31
|
-
};
|
|
32
|
-
}
|
package/sites/github/me.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/* @meta
|
|
2
|
-
{
|
|
3
|
-
"name": "github/me",
|
|
4
|
-
"description": "获取当前 GitHub 登录用户信息",
|
|
5
|
-
"domain": "github.com",
|
|
6
|
-
"args": {},
|
|
7
|
-
"capabilities": ["network"],
|
|
8
|
-
"readOnly": true
|
|
9
|
-
}
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
async function(args) {
|
|
13
|
-
const resp = await fetch('https://api.github.com/user', {credentials: 'include'});
|
|
14
|
-
if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: resp.status === 401 ? 'Not logged into github.com' : 'API error'};
|
|
15
|
-
const d = await resp.json();
|
|
16
|
-
return {
|
|
17
|
-
login: d.login, name: d.name, bio: d.bio,
|
|
18
|
-
url: d.html_url || ('https://github.com/' + d.login),
|
|
19
|
-
public_repos: d.public_repos, followers: d.followers, following: d.following,
|
|
20
|
-
created_at: d.created_at
|
|
21
|
-
};
|
|
22
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/* @meta
|
|
2
|
-
{
|
|
3
|
-
"name": "github/pr-create",
|
|
4
|
-
"description": "Create a GitHub pull request",
|
|
5
|
-
"domain": "github.com",
|
|
6
|
-
"args": {
|
|
7
|
-
"repo": {"required": true, "description": "Target repo (owner/repo)"},
|
|
8
|
-
"title": {"required": true, "description": "PR title"},
|
|
9
|
-
"head": {"required": true, "description": "Source branch (user:branch or branch)"},
|
|
10
|
-
"base": {"required": false, "description": "Target branch (default: main)"},
|
|
11
|
-
"body": {"required": false, "description": "PR description (markdown)"}
|
|
12
|
-
},
|
|
13
|
-
"capabilities": ["network"],
|
|
14
|
-
"readOnly": false,
|
|
15
|
-
"example": "ping-browser site github/pr-create epiral/bb-sites --title \"feat(weibo): add hot adapter\" --head myuser:feat-weibo --body \"Adds weibo/hot.js\""
|
|
16
|
-
}
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
async function(args) {
|
|
20
|
-
if (!args.repo) return {error: 'Missing argument: repo'};
|
|
21
|
-
if (!args.title) return {error: 'Missing argument: title'};
|
|
22
|
-
if (!args.head) return {error: 'Missing argument: head', hint: 'Provide source branch as "user:branch" or "branch"'};
|
|
23
|
-
|
|
24
|
-
const resp = await fetch('https://api.github.com/repos/' + args.repo + '/pulls', {
|
|
25
|
-
method: 'POST',
|
|
26
|
-
credentials: 'include',
|
|
27
|
-
headers: {'Content-Type': 'application/json'},
|
|
28
|
-
body: JSON.stringify({
|
|
29
|
-
title: args.title,
|
|
30
|
-
head: args.head,
|
|
31
|
-
base: args.base || 'main',
|
|
32
|
-
body: args.body || ''
|
|
33
|
-
})
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
if (!resp.ok) {
|
|
37
|
-
const status = resp.status;
|
|
38
|
-
if (status === 401 || status === 403) return {error: 'HTTP ' + status, hint: 'Not logged in to GitHub'};
|
|
39
|
-
if (status === 404) return {error: 'Repo not found: ' + args.repo};
|
|
40
|
-
if (status === 422) {
|
|
41
|
-
const d = await resp.json().catch(() => null);
|
|
42
|
-
const msg = d?.errors?.[0]?.message || d?.message || 'Validation failed';
|
|
43
|
-
return {error: msg, hint: 'Check that the head branch exists and has commits ahead of base'};
|
|
44
|
-
}
|
|
45
|
-
return {error: 'HTTP ' + status};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const pr = await resp.json();
|
|
49
|
-
return {
|
|
50
|
-
number: pr.number,
|
|
51
|
-
title: pr.title,
|
|
52
|
-
url: pr.html_url,
|
|
53
|
-
state: pr.state
|
|
54
|
-
};
|
|
55
|
-
}
|
package/sites/github/repo.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/* @meta
|
|
2
|
-
{
|
|
3
|
-
"name": "github/repo",
|
|
4
|
-
"description": "获取 GitHub 仓库信息",
|
|
5
|
-
"domain": "github.com",
|
|
6
|
-
"args": {
|
|
7
|
-
"repo": {"required": true, "description": "owner/repo format (e.g. epiral/ping-browser)"}
|
|
8
|
-
},
|
|
9
|
-
"capabilities": ["network"],
|
|
10
|
-
"readOnly": true,
|
|
11
|
-
"example": "ping-browser site github/repo epiral/ping-browser"
|
|
12
|
-
}
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
async function(args) {
|
|
16
|
-
if (!args.repo) return {error: 'Missing argument: repo', hint: 'Use owner/repo format'};
|
|
17
|
-
const resp = await fetch('https://api.github.com/repos/' + args.repo, {credentials: 'include'});
|
|
18
|
-
if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: resp.status === 404 ? 'Repo not found: ' + args.repo : 'API error'};
|
|
19
|
-
const d = await resp.json();
|
|
20
|
-
return {
|
|
21
|
-
full_name: d.full_name, description: d.description, language: d.language,
|
|
22
|
-
url: d.html_url || ('https://github.com/' + d.full_name),
|
|
23
|
-
stars: d.stargazers_count, forks: d.forks_count, open_issues: d.open_issues_count,
|
|
24
|
-
created_at: d.created_at, updated_at: d.updated_at, default_branch: d.default_branch,
|
|
25
|
-
topics: d.topics, license: d.license?.spdx_id
|
|
26
|
-
};
|
|
27
|
-
}
|