mirror of
https://github.com/jackwener/OpenCLI.git
synced 2026-09-14 18:25:42 +08:00
2883620ba2
* feat: add gitee adapters and docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: compact gitee trending table output Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: add gitee adapter documentation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(gitee): avoid faking user index values --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: jackwener <jakevingoo@gmail.com>
164 lines
5.0 KiB
TypeScript
164 lines
5.0 KiB
TypeScript
import { CliError } from '@jackwener/opencli/errors';
|
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
|
|
interface GiteeSearchResult {
|
|
rank: number;
|
|
name: string;
|
|
language: string;
|
|
description: string;
|
|
stars: string;
|
|
url: string;
|
|
}
|
|
|
|
const GITEE_SEARCH_URL = 'https://gitee.com/search';
|
|
const GITEE_SEARCH_WIDGET = 'wong1slagnlmzwvsu5ya';
|
|
const GITEE_SEARCH_API = `https://so.gitee.com/v1/search/widget/${GITEE_SEARCH_WIDGET}`;
|
|
const MAX_LIMIT = 50;
|
|
|
|
function clampLimit(value: unknown): number {
|
|
const parsed = Number(value);
|
|
if (Number.isNaN(parsed)) return 10;
|
|
return Math.max(1, Math.min(parsed, MAX_LIMIT));
|
|
}
|
|
|
|
function normalizeText(value: string): string {
|
|
return value.replace(/\s+/g, ' ').trim();
|
|
}
|
|
|
|
function normalizeStars(value: unknown): string {
|
|
let raw = '';
|
|
if (typeof value === 'number') raw = String(value);
|
|
else if (typeof value === 'string') raw = value;
|
|
else if (Array.isArray(value) && value.length > 0) raw = String(value[0] ?? '');
|
|
|
|
const compact = normalizeText(raw).replace(/\s+/g, '');
|
|
if (!compact) return '-';
|
|
const match = compact.match(/\d+(?:[.,]\d+)?(?:[kKmMwW]|\u4E07)?/);
|
|
return match ? match[0] : '-';
|
|
}
|
|
|
|
function getFirstText(value: unknown): string {
|
|
if (typeof value === 'string') return value;
|
|
if (typeof value === 'number') return String(value);
|
|
if (Array.isArray(value)) {
|
|
for (const item of value) {
|
|
if (typeof item === 'string' || typeof item === 'number') {
|
|
return String(item);
|
|
}
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
return value as Record<string, unknown>;
|
|
}
|
|
|
|
function normalizeUrl(value: string): string | null {
|
|
try {
|
|
const parsed = new URL(value, 'https://gitee.com');
|
|
const host = parsed.hostname.toLowerCase();
|
|
if (host !== 'gitee.com' && host !== 'www.gitee.com') return null;
|
|
|
|
const parts = parsed.pathname.split('/').filter(Boolean);
|
|
if (parts.length !== 2) return null;
|
|
return `https://gitee.com/${parts[0]}/${parts[1]}`;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
cli({
|
|
site: 'gitee',
|
|
name: 'search',
|
|
description: 'Search repositories on Gitee',
|
|
domain: 'gitee.com',
|
|
strategy: Strategy.PUBLIC,
|
|
browser: true,
|
|
args: [
|
|
{ name: 'keyword', positional: true, required: true, help: 'Search keyword' },
|
|
{ name: 'limit', type: 'int', default: 10, help: 'Number of results (max 50)' },
|
|
],
|
|
columns: ['rank', 'name', 'language', 'stars', 'description', 'url'],
|
|
func: async (page, args) => {
|
|
const keyword = String(args.keyword ?? '').trim();
|
|
if (!keyword) {
|
|
throw new CliError('INVALID_ARGUMENT', 'Keyword is required', 'Provide a search keyword');
|
|
}
|
|
|
|
const limit = clampLimit(args.limit);
|
|
const encodedKeyword = encodeURIComponent(keyword);
|
|
const searchUrl = `${GITEE_SEARCH_URL}?q=${encodedKeyword}&type=repository`;
|
|
const fetchSize = Math.max(10, limit);
|
|
const apiUrl = new URL(GITEE_SEARCH_API);
|
|
apiUrl.searchParams.set('q', keyword);
|
|
apiUrl.searchParams.set('from', '0');
|
|
apiUrl.searchParams.set('size', String(fetchSize));
|
|
|
|
await page.goto(searchUrl);
|
|
await page.wait(2);
|
|
|
|
const response = await fetch(apiUrl.toString(), {
|
|
headers: {
|
|
Accept: 'application/json',
|
|
'User-Agent': 'Mozilla/5.0',
|
|
Referer: searchUrl,
|
|
},
|
|
});
|
|
if (!response.ok) {
|
|
throw new CliError(
|
|
'REQUEST_FAILED',
|
|
`Failed to request Gitee search API: ${response.status}`,
|
|
'Try again later or verify network access to so.gitee.com',
|
|
);
|
|
}
|
|
|
|
const payload = await response.json() as unknown;
|
|
const payloadRecord = asRecord(payload);
|
|
const hitsRecord = asRecord(payloadRecord?.hits);
|
|
const rawRows = Array.isArray(hitsRecord?.hits) ? hitsRecord.hits : [];
|
|
|
|
if (rawRows.length === 0) {
|
|
throw new CliError(
|
|
'NOT_FOUND',
|
|
'No Gitee repository search results found',
|
|
'Try a different keyword or check whether Gitee search API changed',
|
|
);
|
|
}
|
|
|
|
const seen = new Set<string>();
|
|
const rows: GiteeSearchResult[] = [];
|
|
for (let i = 0; i < rawRows.length && rows.length < limit; i++) {
|
|
const row = asRecord(rawRows[i]);
|
|
const fields = asRecord(row?.fields);
|
|
if (!fields) continue;
|
|
|
|
const name = normalizeText(getFirstText(fields.title));
|
|
const repoUrl = normalizeUrl(getFirstText(fields.url));
|
|
if (!name || !repoUrl) continue;
|
|
if (seen.has(repoUrl)) continue;
|
|
seen.add(repoUrl);
|
|
|
|
rows.push({
|
|
rank: rows.length + 1,
|
|
name,
|
|
language: normalizeText(getFirstText(fields.langs)) || '-',
|
|
description: normalizeText(getFirstText(fields.description)) || '-',
|
|
stars: normalizeStars(fields['count.star']),
|
|
url: repoUrl,
|
|
});
|
|
}
|
|
|
|
if (rows.length === 0) {
|
|
throw new CliError(
|
|
'NOT_FOUND',
|
|
'No valid Gitee repository results parsed',
|
|
'Try a different keyword or check whether Gitee search API changed',
|
|
);
|
|
}
|
|
|
|
return rows;
|
|
},
|
|
});
|