@sovovs/bycli 2.1.37 → 2.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/README.zh-CN.md +21 -0
- package/cli-manifest.json +626 -0
- package/clis/52pojie/search.js +32 -0
- package/clis/baidu/search.js +68 -0
- package/clis/bing/search.js +58 -0
- package/clis/csdn/search.js +34 -0
- package/clis/gitlab/search.js +36 -0
- package/clis/ima/ax.js +656 -0
- package/clis/ima/knowledge.js +95 -0
- package/clis/ima/native-api.js +169 -0
- package/clis/ima/native-client.js +70 -0
- package/clis/ima/utils.js +48 -0
- package/clis/so/search.js +36 -0
- package/clis/sogou/search.js +35 -0
- package/clis/threads/search.js +33 -0
- package/clis/yandex/search.js +43 -0
- package/dist/src/browser/daemon-client.d.ts +4 -1
- package/dist/src/browser/page.d.ts +6 -0
- package/dist/src/browser/page.js +20 -0
- package/dist/src/types.d.ts +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { cli, Strategy } from '@sovovs/bycli/registry';
|
|
2
|
+
import { ArgumentError, CommandExecutionError } from '@sovovs/bycli/errors';
|
|
3
|
+
import { emptySearchResults, requireBoundedInteger, requireSearchQuery, runBrowserStep } from '../_shared/search-adapter.js';
|
|
4
|
+
|
|
5
|
+
const SORTS = ['relevance', 'latest', 'replies', 'views'];
|
|
6
|
+
|
|
7
|
+
export function buildSearchUrl({ keyword, limit = 10, page = 1, section, sort }) {
|
|
8
|
+
const url = new URL('https://www.52pojie.cn/search.php'); url.searchParams.set('srchtxt', keyword); url.searchParams.set('page', String(page)); url.searchParams.set('perpage', String(limit));
|
|
9
|
+
if (section) url.searchParams.set('section', section); if (sort) url.searchParams.set('sort', sort); return url.toString();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function extractItems() {
|
|
13
|
+
const body = document.body?.textContent || ''; const blocked = /验证码|访问频繁|captcha|access denied/i.test(body); const items = []; const seen = new Set(); const numberFrom = (card, selector) => { const value = (card.querySelector(selector)?.textContent || '').replace(/[^\d.]/g, ''); return value ? Number(value) : null; };
|
|
14
|
+
for (const card of document.querySelectorAll('.forum-item, .search-list li, .sltm')) {
|
|
15
|
+
const anchor = card.querySelector('.thread-title[href], h3 a[href], h2 a[href]'); if (!anchor || !/^https?:/i.test(anchor.href) || seen.has(anchor.href)) continue; seen.add(anchor.href); const time = card.querySelector('time');
|
|
16
|
+
items.push({ title: (anchor.textContent || '').trim(), url: anchor.href, snippet: (card.querySelector('.summary, .desc, p')?.textContent || '').trim(), displayUrl: new URL(anchor.href).hostname + new URL(anchor.href).pathname, source: '52pojie', resultType: 'thread', author: (card.querySelector('.author, .username')?.textContent || '').trim() || null, publishedAt: time?.getAttribute('datetime') || null, score: null, extra: { replies: numberFrom(card, '.replies, .reply'), views: numberFrom(card, '.views, .view'), section: (card.querySelector('.section, .forum')?.textContent || '').trim() || null } });
|
|
17
|
+
}
|
|
18
|
+
return { blocked, items };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const command = cli({
|
|
22
|
+
site: '52pojie', name: 'search', access: 'read', description: 'Search 52pojie', domain: 'www.52pojie.cn', strategy: Strategy.PUBLIC, browser: true,
|
|
23
|
+
args: [{ name: 'keyword', positional: true, required: true, help: 'Search query' }, { name: 'limit', type: 'int', default: 10, help: 'Number of threads (1-50)' }, { name: 'page', type: 'int', default: 1, help: 'Result page number' }, { name: 'section', help: 'Forum section identifier' }, { name: 'sort', help: 'Sort order: relevance, latest, replies, views' }],
|
|
24
|
+
columns: ['rank', 'title', 'url', 'snippet', 'displayUrl', 'source', 'resultType', 'author', 'publishedAt', 'score', 'extra'],
|
|
25
|
+
func: async (page, kwargs) => {
|
|
26
|
+
const keyword = requireSearchQuery(kwargs.keyword); const limit = requireBoundedInteger(kwargs.limit, 10, 1, 50, '--limit'); const pageNumber = requireBoundedInteger(kwargs.page, 1, 1, 100, '--page'); if (kwargs.sort && !SORTS.includes(String(kwargs.sort))) throw new ArgumentError(`--sort must be one of: ${SORTS.join(', ')}`);
|
|
27
|
+
await runBrowserStep('52pojie search navigation', () => page.goto(buildSearchUrl({ keyword, limit, page: pageNumber, section: kwargs.section, sort: kwargs.sort }))); await page.wait({ selector: '.forum-item, .search-list, .sltm', timeout: 8 }).catch(() => page.wait(2).catch(() => {})); const data = await runBrowserStep('52pojie search extraction', () => page.evaluate(`(${extractItems.toString()})()`));
|
|
28
|
+
if (data?.blocked) throw new CommandExecutionError('52pojie search was blocked by a verification page', 'Complete the verification in the browser and retry.'); if (!data?.items?.length) throw emptySearchResults('52pojie', keyword); return data.items.slice(0, limit).map((item, index) => ({ rank: (pageNumber - 1) * limit + index + 1, ...item }));
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const __test__ = { command, buildSearchUrl };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { cli, Strategy } from '@sovovs/bycli/registry';
|
|
2
|
+
import { ArgumentError, CommandExecutionError } from '@sovovs/bycli/errors';
|
|
3
|
+
import { emptySearchResults, requireBoundedInteger, requireNonNegativeInteger, requireSearchQuery, runBrowserStep, toHttpsUrl } from '../_shared/search-adapter.js';
|
|
4
|
+
|
|
5
|
+
const TYPES = ['web', 'news', 'image', 'video'];
|
|
6
|
+
const FILETYPES = ['pdf', 'doc', 'xls', 'ppt', 'rtf', 'all'];
|
|
7
|
+
|
|
8
|
+
export function buildSearchUrl({ keyword, limit = 10, page = 1, site, filetype, platform, time }) {
|
|
9
|
+
const url = new URL('https://www.baidu.com/s');
|
|
10
|
+
url.searchParams.set('wd', keyword);
|
|
11
|
+
url.searchParams.set('pn', String((page - 1) * limit));
|
|
12
|
+
if (site) url.searchParams.set('si', site);
|
|
13
|
+
if (filetype) url.searchParams.set('ft', filetype);
|
|
14
|
+
if (platform) url.searchParams.set('ie', platform === 'mobile' ? 'utf-8' : 'utf-8');
|
|
15
|
+
if (time) url.searchParams.set('gpc', `stf=${time}`);
|
|
16
|
+
return url.toString();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function validateChoice(value, choices, flag) {
|
|
20
|
+
if (value !== undefined && !choices.includes(String(value))) throw new ArgumentError(`--${flag} must be one of: ${choices.join(', ')}`);
|
|
21
|
+
return value === undefined ? undefined : String(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function extractItems() {
|
|
25
|
+
const blocked = /验证码|安全验证|百度安全验证|访问过于频繁|请完成验证/.test(document.body?.textContent || '');
|
|
26
|
+
const items = [];
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
for (const card of document.querySelectorAll('.result, .c-container')) {
|
|
29
|
+
const anchor = card.querySelector('h3 a, h3 a[href]');
|
|
30
|
+
if (!anchor) continue;
|
|
31
|
+
const url = anchor.href || '';
|
|
32
|
+
if (!/^https?:/i.test(url) || seen.has(url)) continue;
|
|
33
|
+
seen.add(url);
|
|
34
|
+
items.push({ title: (anchor.textContent || '').trim(), url, snippet: (card.querySelector('.c-abstract, .content-right_8Zs40, .c-span-last')?.textContent || '').trim(), displayUrl: (card.querySelector('.c-showurl, .c-color-gray2')?.textContent || '').trim(), resultType: 'web', extra: {} });
|
|
35
|
+
}
|
|
36
|
+
return { blocked, items };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const command = cli({
|
|
40
|
+
site: 'baidu', name: 'search', access: 'read', description: 'Search Baidu', domain: 'www.baidu.com', strategy: Strategy.PUBLIC, browser: true,
|
|
41
|
+
args: [
|
|
42
|
+
{ name: 'keyword', positional: true, required: true, help: 'Search query' },
|
|
43
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Number of results (1-50)' },
|
|
44
|
+
{ name: 'page', type: 'int', default: 1, help: 'Result page number' },
|
|
45
|
+
{ name: 'site', help: 'Restrict results to a domain' },
|
|
46
|
+
{ name: 'filetype', help: 'File type: pdf, doc, xls, ppt, rtf, all' },
|
|
47
|
+
{ name: 'platform', help: 'Client platform: pc or mobile' },
|
|
48
|
+
{ name: 'time', help: 'Recent time filter understood by Baidu' },
|
|
49
|
+
],
|
|
50
|
+
columns: ['rank', 'title', 'url', 'snippet', 'displayUrl', 'source', 'resultType', 'author', 'publishedAt', 'score', 'extra'],
|
|
51
|
+
func: async (page, kwargs) => {
|
|
52
|
+
const keyword = requireSearchQuery(kwargs.keyword);
|
|
53
|
+
const limit = requireBoundedInteger(kwargs.limit, 10, 1, 50, '--limit');
|
|
54
|
+
const pageNumber = requireBoundedInteger(kwargs.page, 1, 1, 100, '--page');
|
|
55
|
+
const filetype = validateChoice(kwargs.filetype, FILETYPES, 'filetype');
|
|
56
|
+
const platform = validateChoice(kwargs.platform, ['pc', 'mobile'], 'platform');
|
|
57
|
+
const raw = await runBrowserStep('Baidu search navigation', () => page.goto(buildSearchUrl({ keyword, limit, page: pageNumber, site: kwargs.site, filetype, platform, time: kwargs.time })));
|
|
58
|
+
void raw;
|
|
59
|
+
await page.wait({ selector: '.result, .c-container', timeout: 8 }).catch(() => page.wait(2).catch(() => {}));
|
|
60
|
+
const data = await runBrowserStep('Baidu search extraction', () => page.evaluate(`(${extractItems.toString()})()`));
|
|
61
|
+
if (data?.blocked) throw new CommandExecutionError('Baidu search was blocked by a verification page', 'Complete the verification in the browser and retry.');
|
|
62
|
+
const items = data?.items || [];
|
|
63
|
+
if (!items.length) throw emptySearchResults('Baidu', keyword);
|
|
64
|
+
return items.slice(0, limit).map((item, index) => ({ rank: (pageNumber - 1) * limit + index + 1, ...item, source: 'baidu', author: null, publishedAt: null, score: null }));
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export const __test__ = { command, buildSearchUrl };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { cli, Strategy } from '@sovovs/bycli/registry';
|
|
2
|
+
import { ArgumentError, CommandExecutionError } from '@sovovs/bycli/errors';
|
|
3
|
+
import { emptySearchResults, requireBoundedInteger, requireSearchQuery, runBrowserStep } from '../_shared/search-adapter.js';
|
|
4
|
+
|
|
5
|
+
const FRESHNESS = ['day', 'week', 'month', 'year'];
|
|
6
|
+
const ANSWERS = ['web', 'news', 'video', 'images'];
|
|
7
|
+
const SAFE = ['off', 'moderate', 'strict'];
|
|
8
|
+
|
|
9
|
+
export function buildSearchUrl({ keyword, limit = 10, page = 1, freshness, market, answer, safe }) {
|
|
10
|
+
const url = new URL('https://www.bing.com/search');
|
|
11
|
+
url.searchParams.set('q', keyword);
|
|
12
|
+
url.searchParams.set('count', String(limit));
|
|
13
|
+
url.searchParams.set('first', String((page - 1) * limit + 1));
|
|
14
|
+
if (freshness) url.searchParams.set('freshness', freshness);
|
|
15
|
+
if (market) url.searchParams.set('cc', String(market).split('-').pop().toUpperCase());
|
|
16
|
+
if (answer && answer !== 'web') url.searchParams.set('scope', answer);
|
|
17
|
+
if (safe) url.searchParams.set('safesearch', safe);
|
|
18
|
+
return url.toString();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function validate(value, choices, flag) {
|
|
22
|
+
if (value !== undefined && !choices.includes(String(value))) throw new ArgumentError(`--${flag} must be one of: ${choices.join(', ')}`);
|
|
23
|
+
return value === undefined ? undefined : String(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function extractItems() {
|
|
27
|
+
const blocked = /unusual traffic|verify you are human|captcha|access denied/i.test(document.body?.textContent || '');
|
|
28
|
+
const items = [];
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
for (const card of document.querySelectorAll('li.b_algo')) {
|
|
31
|
+
const anchor = card.querySelector('h2 a[href]');
|
|
32
|
+
if (!anchor || !/^https?:/i.test(anchor.href) || seen.has(anchor.href)) continue;
|
|
33
|
+
seen.add(anchor.href);
|
|
34
|
+
items.push({ title: (anchor.textContent || '').trim(), url: anchor.href, snippet: (card.querySelector('.b_caption p')?.textContent || '').trim(), displayUrl: (card.querySelector('cite')?.textContent || '').trim(), resultType: 'web', extra: {} });
|
|
35
|
+
}
|
|
36
|
+
return { blocked, items };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const command = cli({
|
|
40
|
+
site: 'bing', name: 'search', access: 'read', description: 'Search Bing', domain: 'www.bing.com', strategy: Strategy.PUBLIC, browser: true,
|
|
41
|
+
args: [
|
|
42
|
+
{ name: 'keyword', positional: true, required: true, help: 'Search query' }, { name: 'limit', type: 'int', default: 10, help: 'Number of results (1-50)' }, { name: 'page', type: 'int', default: 1, help: 'Result page number' },
|
|
43
|
+
{ name: 'freshness', help: 'Time range: day, week, month, year' }, { name: 'market', help: 'Market code such as en-US or zh-CN' }, { name: 'answer', help: 'Result scope: web, news, video, images' }, { name: 'safe', help: 'Safe search: off, moderate, strict' },
|
|
44
|
+
],
|
|
45
|
+
columns: ['rank', 'title', 'url', 'snippet', 'displayUrl', 'source', 'resultType', 'author', 'publishedAt', 'score', 'extra'],
|
|
46
|
+
func: async (page, kwargs) => {
|
|
47
|
+
const keyword = requireSearchQuery(kwargs.keyword); const limit = requireBoundedInteger(kwargs.limit, 10, 1, 50, '--limit'); const pageNumber = requireBoundedInteger(kwargs.page, 1, 1, 100, '--page');
|
|
48
|
+
const freshness = validate(kwargs.freshness, FRESHNESS, 'freshness'); const answer = validate(kwargs.answer, ANSWERS, 'answer'); const safe = validate(kwargs.safe, SAFE, 'safe');
|
|
49
|
+
await runBrowserStep('Bing search navigation', () => page.goto(buildSearchUrl({ keyword, limit, page: pageNumber, freshness, market: kwargs.market, answer, safe })));
|
|
50
|
+
await page.wait({ selector: 'li.b_algo', timeout: 8 }).catch(() => page.wait(2).catch(() => {}));
|
|
51
|
+
const data = await runBrowserStep('Bing search extraction', () => page.evaluate(`(${extractItems.toString()})()`));
|
|
52
|
+
if (data?.blocked) throw new CommandExecutionError('Bing search was blocked by a verification page', 'Complete the verification in the browser and retry.');
|
|
53
|
+
if (!data?.items?.length) throw emptySearchResults('Bing', keyword);
|
|
54
|
+
return data.items.slice(0, limit).map((item, index) => ({ rank: (pageNumber - 1) * limit + index + 1, ...item, source: 'bing', author: null, publishedAt: null, score: null }));
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export const __test__ = { command, buildSearchUrl };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { cli, Strategy } from '@sovovs/bycli/registry';
|
|
2
|
+
import { ArgumentError, CommandExecutionError } from '@sovovs/bycli/errors';
|
|
3
|
+
import { emptySearchResults, requireBoundedInteger, requireSearchQuery, runBrowserStep } from '../_shared/search-adapter.js';
|
|
4
|
+
|
|
5
|
+
const TYPES = ['all', 'blog', 'download', 'course']; const SORTS = ['relevance', 'latest', 'hot']; const TIMES = ['day', 'week', 'month', 'year'];
|
|
6
|
+
|
|
7
|
+
export function buildSearchUrl({ keyword, limit = 10, page = 1, contentType = 'all', sort, time }) {
|
|
8
|
+
const url = new URL('https://so.csdn.net/so/search'); url.searchParams.set('q', keyword); url.searchParams.set('p', String(page)); url.searchParams.set('t', contentType); url.searchParams.set('size', String(limit));
|
|
9
|
+
if (sort) url.searchParams.set('sort', sort); if (time) url.searchParams.set('time', time); return url.toString();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function extractItems() {
|
|
13
|
+
const blocked = /登录|验证码|访问异常|captcha|sign in/i.test(document.body?.textContent || '') && !document.querySelector('.search-result-info, .search-result'); const items = []; const seen = new Set();
|
|
14
|
+
for (const card of document.querySelectorAll('.search-result-info, .search-result')) {
|
|
15
|
+
const anchor = card.querySelector('h3 a[href], h4 a[href]'); if (!anchor || !/^https?:/i.test(anchor.href) || seen.has(anchor.href)) continue;
|
|
16
|
+
seen.add(anchor.href); const time = card.querySelector('time'); items.push({ rank: items.length + 1, title: (anchor.textContent || '').trim(), url: anchor.href, snippet: (card.querySelector('.search-result-desc, p')?.textContent || '').trim(), displayUrl: new URL(anchor.href).hostname + new URL(anchor.href).pathname, source: 'csdn', resultType: 'article', author: (card.querySelector('.author, .user-name')?.textContent || '').trim() || null, publishedAt: time?.getAttribute('datetime') || null, score: null, extra: {} });
|
|
17
|
+
}
|
|
18
|
+
return { blocked, items };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const command = cli({
|
|
22
|
+
site: 'csdn', name: 'search', access: 'read', description: 'Search CSDN', domain: 'so.csdn.net', strategy: Strategy.PUBLIC, browser: true,
|
|
23
|
+
args: [{ name: 'keyword', positional: true, required: true, help: 'Search query' }, { name: 'limit', type: 'int', default: 10, help: 'Number of results (1-50)' }, { name: 'page', type: 'int', default: 1, help: 'Result page number' }, { name: 'content-type', help: 'Content type: all, blog, download, course' }, { name: 'sort', help: 'Sort order: relevance, latest, hot' }, { name: 'time', help: 'Time range: day, week, month, year' }],
|
|
24
|
+
columns: ['rank', 'title', 'url', 'snippet', 'displayUrl', 'source', 'resultType', 'author', 'publishedAt', 'score', 'extra'],
|
|
25
|
+
func: async (page, kwargs) => {
|
|
26
|
+
const keyword = requireSearchQuery(kwargs.keyword); const limit = requireBoundedInteger(kwargs.limit, 10, 1, 50, '--limit'); const pageNumber = requireBoundedInteger(kwargs.page, 1, 1, 100, '--page'); const contentType = kwargs['content-type'] ?? kwargs.contentType ?? 'all';
|
|
27
|
+
if (!TYPES.includes(String(contentType))) throw new ArgumentError(`--content-type must be one of: ${TYPES.join(', ')}`); if (kwargs.sort && !SORTS.includes(String(kwargs.sort))) throw new ArgumentError(`--sort must be one of: ${SORTS.join(', ')}`); if (kwargs.time && !TIMES.includes(String(kwargs.time))) throw new ArgumentError(`--time must be one of: ${TIMES.join(', ')}`);
|
|
28
|
+
await runBrowserStep('CSDN search navigation', () => page.goto(buildSearchUrl({ keyword, limit, page: pageNumber, contentType, sort: kwargs.sort, time: kwargs.time }))); await page.wait({ selector: '.search-result-info, .search-result', timeout: 8 }).catch(() => page.wait(2).catch(() => {}));
|
|
29
|
+
const data = await runBrowserStep('CSDN search extraction', () => page.evaluate(`(${extractItems.toString()})()`)); if (data?.blocked) throw new CommandExecutionError('CSDN search requires login or was blocked', 'Open CSDN in the browser, complete any prompt, and retry.'); if (!data?.items?.length) throw emptySearchResults('CSDN', keyword);
|
|
30
|
+
return data.items.slice(0, limit).map((item, index) => ({ ...item, rank: (pageNumber - 1) * limit + index + 1 }));
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const __test__ = { command, buildSearchUrl };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { cli, Strategy } from '@sovovs/bycli/registry';
|
|
2
|
+
import { ArgumentError, CommandExecutionError } from '@sovovs/bycli/errors';
|
|
3
|
+
import { emptySearchResults, requireBoundedInteger, requireSearchQuery, runBrowserStep } from '../_shared/search-adapter.js';
|
|
4
|
+
|
|
5
|
+
const SCOPES = ['projects', 'issues', 'merge_requests', 'commits', 'blobs', 'users'];
|
|
6
|
+
const ORDER = ['created_at', 'updated_at', 'latest_activity_at']; const SORT = ['asc', 'desc'];
|
|
7
|
+
|
|
8
|
+
export function buildSearchUrl({ keyword, limit = 10, page = 1, scope = 'projects', orderBy, sort }) {
|
|
9
|
+
const url = new URL('https://gitlab.com/search'); url.searchParams.set('search', keyword); url.searchParams.set('page', String(page)); url.searchParams.set('per_page', String(limit));
|
|
10
|
+
if (scope) url.searchParams.set('scope', scope); if (orderBy) url.searchParams.set('order_by', orderBy); if (sort) url.searchParams.set('sort', sort); return url.toString();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function extractItems(scope) {
|
|
14
|
+
const blocked = /sign in|login|access denied|captcha/i.test(document.body?.textContent || '') && !document.querySelector('.search-result-row, .search-results'); const items = []; const seen = new Set();
|
|
15
|
+
for (const card of document.querySelectorAll('.search-result-row, .search-result-item, .search-results li')) {
|
|
16
|
+
const anchor = card.querySelector('a.gl-link[href], a[href*="gitlab.com/"]'); if (!anchor || !/^https?:/i.test(anchor.href) || seen.has(anchor.href)) continue;
|
|
17
|
+
seen.add(anchor.href); const time = card.querySelector('time'); const type = scope === 'issues' ? 'issue' : scope === 'merge_requests' ? 'merge_request' : scope === 'projects' ? 'project' : scope || 'result';
|
|
18
|
+
items.push({ title: (anchor.textContent || '').trim(), url: anchor.href, snippet: (card.querySelector('.description, .description p, p')?.textContent || '').trim(), displayUrl: new URL(anchor.href).hostname + new URL(anchor.href).pathname, source: 'gitlab', resultType: type, author: (card.querySelector('.author, [data-testid="author"]')?.textContent || '').trim() || null, publishedAt: time?.getAttribute('datetime') || null, score: null, extra: {} });
|
|
19
|
+
}
|
|
20
|
+
return { blocked, items };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const command = cli({
|
|
24
|
+
site: 'gitlab', name: 'search', access: 'read', description: 'Search GitLab', domain: 'gitlab.com', strategy: Strategy.PUBLIC, browser: true,
|
|
25
|
+
args: [{ name: 'keyword', positional: true, required: true, help: 'Search query' }, { name: 'limit', type: 'int', default: 10, help: 'Number of results (1-50)' }, { name: 'page', type: 'int', default: 1, help: 'Result page number' }, { name: 'scope', help: 'Search scope: projects, issues, merge_requests, commits, blobs, users' }, { name: 'order-by', help: 'Order by: created_at, updated_at, latest_activity_at' }, { name: 'sort', help: 'Sort direction: asc or desc' }],
|
|
26
|
+
columns: ['rank', 'title', 'url', 'snippet', 'displayUrl', 'source', 'resultType', 'author', 'publishedAt', 'score', 'extra'],
|
|
27
|
+
func: async (page, kwargs) => {
|
|
28
|
+
const keyword = requireSearchQuery(kwargs.keyword); const limit = requireBoundedInteger(kwargs.limit, 10, 1, 50, '--limit'); const pageNumber = requireBoundedInteger(kwargs.page, 1, 1, 100, '--page'); const scope = kwargs.scope || 'projects'; const orderBy = kwargs['order-by'] ?? kwargs.orderBy; const sort = kwargs.sort;
|
|
29
|
+
if (!SCOPES.includes(String(scope))) throw new ArgumentError(`--scope must be one of: ${SCOPES.join(', ')}`); if (orderBy && !ORDER.includes(String(orderBy))) throw new ArgumentError(`--order-by must be one of: ${ORDER.join(', ')}`); if (sort && !SORT.includes(String(sort))) throw new ArgumentError('--sort must be one of: asc, desc');
|
|
30
|
+
await runBrowserStep('GitLab search navigation', () => page.goto(buildSearchUrl({ keyword, limit, page: pageNumber, scope, orderBy, sort }))); await page.wait({ selector: '.search-result-row, .search-results', timeout: 8 }).catch(() => page.wait(2).catch(() => {}));
|
|
31
|
+
const data = await runBrowserStep('GitLab search extraction', () => page.evaluate(`(${extractItems.toString()})(${JSON.stringify(scope)})`)); if (data?.blocked) throw new CommandExecutionError('GitLab search requires sign-in or was blocked', 'Open GitLab in the browser, sign in if needed, and retry.'); if (!data?.items?.length) throw emptySearchResults('GitLab', keyword);
|
|
32
|
+
return data.items.slice(0, limit).map((item, index) => ({ rank: (pageNumber - 1) * limit + index + 1, ...item }));
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const __test__ = { command, buildSearchUrl };
|