browser-web-search 0.2.3 → 0.3.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.
@@ -0,0 +1,96 @@
1
+ /* @meta
2
+ {
3
+ "name": "weixin/search",
4
+ "description": "搜索微信公众号文章 (通过搜狗微信)",
5
+ "domain": "weixin.sogou.com",
6
+ "args": {
7
+ "query": {"required": true, "description": "搜索关键词"},
8
+ "count": {"required": false, "description": "返回数量 (默认 10)"}
9
+ },
10
+ "readOnly": true,
11
+ "example": "bws weixin/search AI"
12
+ }
13
+ */
14
+
15
+ async function(args) {
16
+ const query = args.query;
17
+ if (!query) {
18
+ return {error: 'Missing query parameter'};
19
+ }
20
+ const maxCount = Math.min(parseInt(args.count) || 10, 20);
21
+
22
+ // 通过 fetch 请求搜狗微信搜索页面
23
+ const searchUrl = 'https://weixin.sogou.com/weixin?type=2&s_from=input&query=' + encodeURIComponent(query);
24
+
25
+ const resp = await fetch(searchUrl, {
26
+ credentials: 'include',
27
+ headers: {
28
+ 'Accept': 'text/html,application/xhtml+xml'
29
+ }
30
+ });
31
+
32
+ if (!resp.ok) {
33
+ return {error: 'HTTP ' + resp.status, hint: 'Open weixin.sogou.com in browser first'};
34
+ }
35
+
36
+ const html = await resp.text();
37
+
38
+ // 检查是否需要验证码
39
+ if (html.includes('请输入验证码') || html.includes('antispider')) {
40
+ return {
41
+ error: 'Captcha required',
42
+ hint: 'Please visit weixin.sogou.com in browser and complete verification first',
43
+ url: searchUrl
44
+ };
45
+ }
46
+
47
+ // 解析 HTML
48
+ const parser = new DOMParser();
49
+ const doc = parser.parseFromString(html, 'text/html');
50
+
51
+ const items = [];
52
+ const results = doc.querySelectorAll('.news-list li, ul.news-list > li');
53
+
54
+ if (results.length === 0) {
55
+ return {
56
+ error: 'No results found',
57
+ hint: 'Try different keywords or check weixin.sogou.com manually',
58
+ query: query
59
+ };
60
+ }
61
+
62
+ results.forEach((item, i) => {
63
+ if (i >= maxCount) return;
64
+
65
+ const titleEl = item.querySelector('h3 a, .txt-box h3 a');
66
+ const summaryEl = item.querySelector('p.txt-info, .txt-box p');
67
+ const sourceEl = item.querySelector('a.account, .s-p a[href*="gzh"]');
68
+ const timeEl = item.querySelector('.s-p span:last-child, span.s2');
69
+ const imgEl = item.querySelector('img');
70
+
71
+ if (titleEl) {
72
+ items.push({
73
+ rank: i + 1,
74
+ title: titleEl.textContent?.trim().replace(/\s+/g, ' ') || '',
75
+ url: titleEl.href || '',
76
+ summary: summaryEl?.textContent?.trim().replace(/\s+/g, ' ') || '',
77
+ account: sourceEl?.textContent?.trim() || '',
78
+ time: timeEl?.textContent?.trim() || '',
79
+ cover: imgEl?.src || imgEl?.getAttribute('data-src') || ''
80
+ });
81
+ }
82
+ });
83
+
84
+ if (items.length === 0) {
85
+ return {
86
+ error: 'Failed to parse results',
87
+ hint: 'Page structure may have changed'
88
+ };
89
+ }
90
+
91
+ return {
92
+ query: query,
93
+ count: items.length,
94
+ items
95
+ };
96
+ }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }