From cae5686d2d0011ef37e24c72c8abdf48c1c1f95e Mon Sep 17 00:00:00 2001 From: Caihaohan Date: Tue, 30 Dec 2025 14:14:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20GitHub=20=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E6=95=B0=E6=8D=AE=E5=B1=95=E7=A4=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 GitHub 统计卡片和徽章组件,在项目卡片、详情页和侧边栏中展示 GitHub stars、forks 等统计数据 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- design/detail.html | 66 +- design/projectlist.html | 9 + docs/data-ingestion-flow.md | 1187 +++++++++++++++++ next.config.js | 3 +- src/app/[locale]/projects/[id]/page.tsx | 2 +- src/components/project/GitHubBadges.tsx | 74 + .../project/GitHubTextStatsCard.tsx | 138 ++ src/components/project/ProjectCard.tsx | 48 +- src/components/project/ProjectDetail.tsx | 5 + src/components/project/ProjectSidebar.tsx | 43 +- src/lib/github/api.ts | 92 ++ src/lib/github/badges.ts | 140 ++ 12 files changed, 1766 insertions(+), 41 deletions(-) create mode 100644 docs/data-ingestion-flow.md create mode 100644 src/components/project/GitHubBadges.tsx create mode 100644 src/components/project/GitHubTextStatsCard.tsx create mode 100644 src/lib/github/api.ts create mode 100644 src/lib/github/badges.ts diff --git a/design/detail.html b/design/detail.html index 8376ef9..d238591 100644 --- a/design/detail.html +++ b/design/detail.html @@ -220,24 +220,72 @@ python main.py
diff --git a/design/projectlist.html b/design/projectlist.html index 4c95303..d8b122a 100644 --- a/design/projectlist.html +++ b/design/projectlist.html @@ -172,9 +172,18 @@ Automation Workflow
+
View Details arrow_forward + +
+ + + +42.5k +
+
diff --git a/docs/data-ingestion-flow.md b/docs/data-ingestion-flow.md new file mode 100644 index 0000000..dca3598 --- /dev/null +++ b/docs/data-ingestion-flow.md @@ -0,0 +1,1187 @@ +# 数据新增流程设计文档 + +> 本文档详细描述了 AI 项目导航站的数据获取、处理和维护的完整流程 + +## 目录 + +- [1. 概述](#1-概述) +- [2. 数据源发现](#2-数据源发现) +- [3. 深度数据提取](#3-深度数据提取) +- [4. 数据标准化处理](#4-数据标准化处理) +- [5. 质量控制机制](#5-质量控制机制) +- [6. 数据维护策略](#6-数据维护策略) +- [7. 技术实现架构](#7-技术实现架构) +- [8. 实施路径](#8-实施路径) + +--- + +## 1. 概述 + +### 1.1 流程全景 + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Discovery │ -> │ Fetch │ -> │ Parse │ -> │ Validate │ +│ 发现项目 │ │ 深度抓取 │ │ 解析标准化 │ │ 质量验证 │ +└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ + ↓ ↓ ↓ ↓ +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ 多源种子 │ │ 完整元数据 │ │ 结构化数据 │ │ 质量评分 │ +└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ + ↓ + ┌─────────────┐ + │ Store │ + │ 入库存储 │ + └─────────────┘ + ↓ + ┌─────────────┐ + │ Maintain │ + │ 持续维护 │ + └─────────────┘ +``` + +### 1.2 核心目标 + +- **完整性**:获取项目的多维度信息(基础、统计、内容、关系、更新、社区) +- **准确性**:通过多源交叉验证和质量评分确保数据可靠 +- **时效性**:定期更新活跃项目,及时下架失效项目 +- **可扩展性**:模块化架构便于添加新数据源 + +--- + +## 2. 数据源发现 + +### 2.1 主要数据平台 + +| 平台 | 数据类型 | API 能力 | 数据量级 | +|------|---------|---------|---------| +| **GitHub** | 代码项目 | REST API + GraphQL | ★★★★★ | +| **Hugging Face** | 模型/数据集/空间 | REST API | ★★★★☆ | +| **Papers with Code** | 论文+代码 | Web Scraping | ★★★☆☆ | +| **arXiv** | 论文预印本 | REST API | ★★★★☆ | +| **Product Hunt** | AI产品 | 无公开API | ★★☆☆☆ | +| **AI导航站** | 聚合列表 | Web Scraping | ★★☆☆☆ | + +### 2.2 项目发现渠道 + +#### 2.2.1 趋势榜单 + +``` +GitHub Trending: +https://github.com/trending +- 参数:since=daily/weekly/monthly, language={python,typescript,...} +- 获取:spike_count、stars、forks、description + +Hugging Face Trending: +https://huggingface.co/api/models +- 参数:sort=downloads/likes, trending=true +- 获取:modelId、downloads、likes、pipeline_tag +``` + +#### 2.2.2 社区讨论 + +- **Reddit**: r/MachineLearning、r/artificial、r/LocalLLaMA 高赞帖 +- **Hacker News**: AI相关front page故事 +- **Twitter/X**: AI influencer(Andrew Ng、Yann LeCun等)转发 +- **Discord/Slack**: AI社区热帖 + +#### 2.2.3 论文与代码关联 + +``` +Papers with Code: +- Tasks分类:https://paperswithcode.com/tasks +- Leaderboards: https://paperswithcode.com/leaderboards +- 关联GitHub仓库 +``` + +#### 2.2.4 聚合站点 + +- FutureTools、There's An AI For That、AI Valley +- 需注意:这些站点数据源自上游,去重更重要 + +### 2.3 发现优先级 + +``` +P0: GitHub Trending + Hugging Face Trending(每日) +P1: Papers with Code新入库论文(每周) +P2: Reddit/HN高赞讨论(每周) +P3: Product Hunt AI产品(每月) +P4: AI导航站爬取(按需) +``` + +--- + +## 3. 深度数据提取 + +### 3.1 提取维度架构 + +```yaml +项目元数据模型: + 基础层: + - 名称: name, nameEn + - 描述: description, descriptionEn (100-500字) + - 主页: homepage_url + - 许可证: license + + 统计层: + - GitHub: stars, forks, watchers, open_issues + - Hugging Face: downloads, likes, discussions + - 变化趋势: stars_delta_7d, stars_delta_30d + + 内容层: + - README: 完整Markdown内容 (content/contentEn) + - 文档: docs_url, wiki_url + - 演示: demo_url, video_url + + 技术层: + - 编程语言: languages (按代码量排序) + - 依赖项: dependencies (package.json, requirements.txt) + - 框架: frameworks (PyTorch, TensorFlow, LangChain...) + - 模型类型: LLM, Diffusion, Computer Vision... + + 关系层: + - 作者: author, author_url + - 组织: organization, org_url + - 相关项目: similar_projects, forks_from + + 更新层: + - 首次发布: created_at + - 最后更新: updated_at + - 最后提交: pushed_at + - 版本历史: releases, tags + + 社区层: + - 贡献者: contributors_count, top_contributors + - Issue活动: open_issues, closed_issues, issue_response_time + - 讨论质量: discussions_count, avg_engagement +``` + +### 3.2 GitHub 深度提取方案 + +#### 3.2.1 API 调用策略 + +```javascript +// 推荐使用 GraphQL 一次性获取,减少请求次数 +const query = ` + query($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) { + # 基础信息 + name + description + homepageUrl + licenseInfo { key name } + url + + # 统计数据 + stargazers { totalCount } + forks { totalCount } + watchers { totalCount } + openIssues: issues(states: OPEN) { totalCount } + + # 内容 + readme: object(expression: "HEAD:README.md") { + ... on Blob { text } + } + + # 技术 + languages(orderBy: {field: SIZE, direction: DESC}, first: 10) { + edges { node { name } size } + } + + # 更新时间 + createdAt + updatedAt + pushedAt + + # 发布版本 + releases(last: 5, orderBy: {field: CREATED_AT, direction: DESC}) { + nodes { tagName name publishedAt } + } + + # 贡献者 + contributors: mentionableUsers(first: 20) { + nodes { login name url } + } + + # Topics (标签) + repositoryTopics(first: 20) { + nodes { topic { name } } + } + + # 依赖关系 + defaultBranchRef { + target { + ... on Commit { + history(first: 1) { + nodes { + ... on Commit { + file(path: "package.json") { + ... on Blob { text } + } + } + } + } + } + } + } + } + } +`; +``` + +#### 3.2.2 REST API 补充 + +```javascript +// 获取 issue 活跃度 +const issuesActivity = await fetch( + `https://api.github.com/repos/${owner}/${repo}/issues?state=all&per_page=100&sort=comments` +); + +// 获取 star 历史(需第三方服务如 star-history.com) +const starHistory = await fetch( + `https://api.star-history.com/svg?repos=${owner}/${repo}&type=Date` +); + +// 获取社区健康度 +const communityProfile = await fetch( + `https://api.github.com/repos/${owner}/${repo}/community/profile` +); +``` + +### 3.3 Hugging Face 深度提取方案 + +#### 3.3.1 模型 API + +```javascript +const model = await fetch(`https://huggingface.co/api/models/${modelId}`); + +// 返回结构 +{ + modelId: "meta-llama/Llama-2-7b", + author: "meta-llama", + downloads: 5000000, + likes: 12000, + lastModified: "2024-01-15T00:00:00.000Z", + + // 标签体系 + tags: ["transformers", "pytorch", "llm", "arxiv:2307.12345"], + pipeline_tag: "text-generation", + + // README 中的 YAML Frontmatter + cardData: { + license: "llama2", + tags: ["llm", "generative"], + datasets: ["commoncrawl"], + metrics: ["perplexity"], + model_index: { + "text-generation": [ + { name: "Llama-2-7b", model: "?" } + ] + } + } +} +``` + +#### 3.3.2 README 解析 + +Hugging Face 的 README 通常包含结构化的 YAML 元数据: + +```yaml +--- +license: llama2 +tags: +- llm +- generative +- text generation +datasets: +- commoncrawl +- c4 +metrics: +- perplexity +--- + +# Llama 2 7B + +[Markdown 内容...] +``` + +需要解析并合并这些元数据。 + +### 3.4 Papers with Code 提取 + +```javascript +// 该平台无公开API,需网页抓取 +const paperPage = await fetch(`https://paperswithcode.com/paper/${paperSlug}`); + +// 提取字段 +{ + title: "Attention Is All You Need", + titleEn: "Attention Is All You Need", + authors: ["Ashish Vaswani", ...], + published: "2017-06-12", + arxiv_id: "1706.03762", + pdf_url: "https://arxiv.org/pdf/1706.03762.pdf", + + // 代码实现 + frameworks: ["PyTorch", "TensorFlow"], + implementations: [ + { name: "Tensor2Tensor", github: "tensorflow/tensor2tensor", stars: 12000 }, + { name: "Harvard NLP", github: "harvardnlp/annotated-transformer", stars: 5000 } + ], + + // 任务与指标 + tasks: ["machine-translation", "language-modeling"], + benchmarks: ["WMT 2014 En-De", "WMT 2014 En-Fr"], + sota_scores: { "BLEU": 28.4 } +} +``` + +--- + +## 4. 数据标准化处理 + +### 4.1 统一数据结构 + +所有数据源最终转换为 `ProjectInputSchema` 格式: + +```typescript +interface ProjectInput { + // 基础信息(必填) + name: string; // 中文名称(如无则翻译) + nameEn: string; // 英文名称 + description: string; // 中文描述(100-500字) + descriptionEn: string; // 英文描述 + slug: string; // URL友好标识符 + + // 内容(必填) + content: string; // 中文README(Markdown) + contentEn: string; // 英文README + + // 状态 + status: "ACTIVE" | "ARCHIVED"; + source: "GITHUB" | "HUGGING_FACE" | "PAPERS_WITH_CODE" | "MANUAL"; + + // 关联(必填) + tags: string[]; // 1-10个标签 + externalLinks: ExternalLink[]; // 1-10个链接 +} +``` + +### 4.2 标签智能生成 + +#### 4.2.1 标签分类体系 + +```yaml +技术栈标签: + - 来源: GitHub languages, HF tags + - 示例: Python, TypeScript, PyTorch, TensorFlow + +应用领域标签: + - 来源: README关键词, HF pipeline_tag, PwC tasks + - 示例: Computer Vision, NLP, Reinforcement Learning + +模型类型标签: + - 来源: README, paper tags + - 示例: LLM, Diffusion, GAN, Transformer + +框架标签: + - 来源: dependencies, README + - 示例: LangChain, Gradio, Streamlit, FastAPI + +商业状态标签: + - 来源: license, homepage + - 示例: Open Source, Commercial, Research Only +``` + +#### 4.2.2 标签提取算法 + +```javascript +async function extractTags(project) { + const tags = new Set(); + + // 1. 从平台标签直接获取 + if (project.githubTopics) { + project.githubTopics.forEach(t => tags.add(normalizeTag(t))); + } + + // 2. 从 HF pipeline_tag 获取 + if (project.pipelineTag) { + tags.add(normalizeTag(project.pipelineTag)); + } + + // 3. NLP 关键词提取(使用NER) + const keywords = await extractKeywords(project.descriptionEn); + keywords.forEach(kw => { + if (isTechnicalTerm(kw)) tags.add(normalizeTag(kw)); + }); + + // 4. 编程语言映射 + if (project.languages) { + Object.keys(project.languages).forEach(lang => { + tags.add(normalizeTag(lang)); + }); + } + + // 5. 去重与标准化 + return Array.from(tags) + .filter(t => t.length >= 2 && t.length <= 30) + .map(t => applyTagAlias(t)); // "LLM" -> "Large Language Model" +} +``` + +#### 4.2.3 标签标准化规则 + +```javascript +const tagAliases = { + "LLM": "Large Language Model", + "llm": "Large Language Model", + "GPT": "Generative Pre-trained Transformer", + "CV": "Computer Vision", + "NLP": "Natural Language Processing" +}; + +const tagSynonyms = { + "diffusion": ["stable-diffusion", "ddpm", "score-based"], + "transformer": ["attention", "self-attention"], + "fine-tuning": ["finetuning", "fine_tuning"] +}; +``` + +### 4.3 多语言内容生成 + +#### 4.3.1 翻译策略 + +```javascript +async function translateProject(project, sourceLang, targetLang) { + // 1. 名称翻译(保留专有名词) + const translatedName = await translateText(project.name, { + preserveTerms: ["Transformer", "Diffusion", "LLaMA"], + format: "title" + }); + + // 2. 描述翻译 + const translatedDesc = await translateText(project.description, { + maxLength: 500, + preserveFormatting: true + }); + + // 3. README 分段翻译 + const translatedContent = await translateMarkdown(project.content, { + skipCodeBlocks: true, + preserveLinks: true, + preserveImages: true + }); + + return { + name: targetLang === 'zh' ? translatedName : project.name, + nameEn: targetLang === 'en' ? translatedName : project.name, + // ... + }; +} +``` + +#### 4.3.2 翻译质量检查 + +```javascript +function validateTranslation(original, translated) { + const checks = { + lengthRatio: translated.length / original.length, + // 异常检测:中译英应在0.6-1.5倍之间 + hasPreservedTerms: original.match(/[A-Z]{2,}/g).every(term => + translated.includes(term) + ), + noBrokenFormatting: !translated.includes('```') || translated.match(/```/g).length % 2 === 0, + noImageLoss: (original.match(/!\[.*\]\(.*\)/g) || []).length === + (translated.match(/!\[.*\]\(.*\)/g) || []).length + }; + + return Object.values(checks).every(v => v === true); +} +``` + +### 4.4 Slug 生成规则 + +```javascript +function generateSlug(name, nameEn, existingSlugs) { + // 1. 优先使用英文 + let slug = nameEn + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, ''); + + // 2. 检查冲突 + let finalSlug = slug; + let counter = 1; + while (existingSlugs.includes(finalSlug)) { + finalSlug = `${slug}-${counter}`; + counter++; + } + + return finalSlug; +} +``` + +--- + +## 5. 质量控制机制 + +### 5.1 质量评分模型 + +```javascript +function calculateQualityScore(project) { + const scores = { + completeness: 0, // 完整性 (0-30) + freshness: 0, // 时效性 (0-25) + activity: 0, // 活跃度 (0-25) + authority: 0, // 权威性 (0-10) + usability: 0 // 可用性 (0-10) + }; + + // 1. 完整性评分 (30分) + if (project.name && project.description) scores.completeness += 10; + if (project.content && project.content.length > 500) scores.completeness += 10; + if (project.externalLinks.length >= 2) scores.completeness += 5; + if (project.tags.length >= 3) scores.completeness += 5; + + // 2. 时效性评分 (25分) + const daysSinceUpdate = (Date.now() - new Date(project.updatedAt)) / (1000 * 60 * 60 * 24); + if (daysSinceUpdate < 30) scores.freshness = 25; + else if (daysSinceUpdate < 90) scores.freshness = 20; + else if (daysSinceUpdate < 180) scores.freshness = 15; + else if (daysSinceUpdate < 365) scores.freshness = 10; + else scores.freshness = 5; + + // 3. 活跃度评分 (25分) + const stars = project.stars || 0; + if (stars > 10000) scores.activity += 10; + else if (stars > 1000) scores.activity += 7; + else if (stars > 100) scores.activity += 5; + else if (stars > 10) scores.activity += 3; + + const recentCommits = project.recentCommits || 0; + if (recentCommits > 10) scores.activity += 15; + else if (recentCommits > 5) scores.activity += 10; + else if (recentCommits > 0) scores.activity += 5; + + // 4. 权威性评分 (10分) + if (project.isOfficialOrg) scores.authority += 5; + if (project.hasPaperBacking) scores.authority += 3; + if (project.stars > 5000) scores.authority += 2; + + // 5. 可用性评分 (10分) + if (project.hasInstallationGuide) scores.usability += 4; + if (project.hasDemo) scores.usability += 3; + if (project.hasDocumentation) scores.usability += 3; + + // 总分 + const totalScore = Object.values(scores).reduce((a, b) => a + b, 0); + + return { + totalScore, + breakdown: scores, + quality: totalScore >= 70 ? 'HIGH' : totalScore >= 40 ? 'MEDIUM' : 'LOW' + }; +} +``` + +### 5.2 垃圾项目检测 + +```javascript +function detectSpamProject(project) { + const signals = []; + + // 1. 描述异常相似 + if (isDescriptionTemplate(project.description)) { + signals.push('template_description'); + } + + // 2. Star 增长异常 + const starGrowthRate = project.stars / project.daysSinceCreated; + if (starGrowthRate > 1000 && project.daysSinceCreated < 7) { + signals.push('suspicious_star_growth'); + } + + // 3. 内容过短 + if (project.content.length < 100) { + signals.push('minimal_content'); + } + + // 4. 缺少基本链接 + if (!project.externalLinks.some(l => l.type === 'GITHUB' || l.type === 'WEBSITE')) { + signals.push('missing_repository'); + } + + // 5. 关键词堆砌 + const keywordDensity = calculateKeywordDensity(project.description); + if (keywordDensity > 0.3) { + signals.push('keyword_stuffing'); + } + + return { + isSpam: signals.length >= 3, + signals, + confidence: signals.length / 5 + }; +} +``` + +### 5.3 去重策略 + +利用现有 Webhook 的多级去重机制: + +```javascript +async function deduplicateProject(newProject) { + const { githubUrl, websiteUrl, slug } = newProject; + + // P0: GitHub URL 精确匹配 + const githubMatch = await prisma.externalLink.findUnique({ + where: { url_type: { url: githubUrl, type: 'GITHUB' } }, + include: { project: true } + }); + if (githubMatch) { + return { exists: true, project: githubMatch.project, reason: 'GITHUB_URL' }; + } + + // P1: Website URL 精确匹配 + if (websiteUrl) { + const websiteMatch = await prisma.externalLink.findUnique({ + where: { url_type: { url: websiteUrl, type: 'WEBSITE' } }, + include: { project: true } + }); + if (websiteMatch) { + return { exists: true, project: websiteMatch.project, reason: 'WEBSITE_URL' }; + } + } + + // P2: Slug 匹配 + const slugMatch = await prisma.project.findUnique({ + where: { slug } + }); + if (slugMatch) { + return { exists: true, project: slugMatch, reason: 'SLUG' }; + } + + return { exists: false }; +} +``` + +--- + +## 6. 数据维护策略 + +### 6.1 增量更新机制 + +```javascript +// 更新优先级 +const UPDATE_PRIORITIES = { + HIGH: { interval: '7d', condition: 'stars > 1000 && updated < 7d ago' }, + MEDIUM: { interval: '30d', condition: 'stars > 100 && updated < 30d ago' }, + LOW: { interval: '90d', condition: 'stars <= 100' } +}; + +async function scheduleUpdate(project) { + const priority = determineUpdatePriority(project); + + // 使用 BullMQ 队列 + await updateQueue.add('refresh-project', { + projectId: project.id, + source: project.source + }, { + delay: parseInterval(priority.interval), + attempts: 3, + backoff: { type: 'exponential', delay: 5000 } + }); +} +``` + +### 6.2 生命周期管理 + +```javascript +async function manageProjectLifecycle(project) { + const daysSinceUpdate = (Date.now() - new Date(project.updatedAt)) / (1000 * 60 * 60 * 24); + + // 1. 活跃项目(90天内更新) + if (daysSinceUpdate < 90) { + await prisma.project.update({ + where: { id: project.id }, + data: { status: 'ACTIVE' } + }); + } + + // 2. 不活跃项目(90-365天) + else if (daysSinceUpdate < 365) { + // 检查是否仍在维护 + const stillActive = await checkMaintenanceStatus(project); + if (!stillActive) { + await prisma.project.update({ + where: { id: project.id }, + data: { status: 'ARCHIVED' } + }); + } + } + + // 3. 长期未更新(超过365天) + else { + await prisma.project.update({ + where: { id: project.id }, + data: { status: 'ARCHIVED' } + }); + } +} +``` + +### 6.3 死链检测 + +```javascript +async function checkExternalLinks() { + const links = await prisma.externalLink.findMany(); + + for (const link of links) { + try { + const response = await fetch(link.url, { + method: 'HEAD', + timeout: 5000 + }); + + if (response.status === 404) { + // 标记失效 + await prisma.externalLink.update({ + where: { id: link.id }, + data: { valid: false } + }); + } else if (response.status >= 400) { + // 标记异常 + await prisma.externalLink.update({ + where: { id: link.id }, + data: { valid: false, lastError: response.status } + }); + } + } catch (error) { + // 网络错误,标记待重检 + await prisma.externalLink.update({ + where: { id: link.id }, + data: { lastCheckFailed: true } + }); + } + } + + // 移除长期失效的链接 + await prisma.externalLink.deleteMany({ + where: { + valid: false, + updatedAt: { lt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) } + } + }); +} +``` + +### 6.4 热度衰减算法 + +```javascript +function calculateTrendingScore(project) { + const BASE_SCORE = project.stars || 0; + + // 时间衰减(半衰期30天) + const daysSinceUpdate = (Date.now() - new Date(project.updatedAt)) / (1000 * 60 * 60 * 24); + const timeDecay = Math.pow(0.5, daysSinceUpdate / 30); + + // 增长加权(最近7天的star增长) + const recentGrowth = (project.stars - project.stars7dAgo) || 0; + const growthBonus = recentGrowth * 2; + + // 社区活跃度 + const activityBonus = (project.recentCommits || 0) * 10 + + (project.issuesClosedLastWeek || 0) * 5; + + return (BASE_SCORE * timeDecay) + growthBonus + activityBonus; +} +``` + +--- + +## 7. 技术实现架构 + +### 7.1 系统架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 调度层 │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 定时任务 │ │ 事件触发 │ │ 手动触发 │ │ +│ │ (cron) │ │ (webhook) │ │ (admin) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 采集层 │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ GitHub │ │ Hugging Face │ │ Papers w/ │ │ +│ │ Adapter │ │ Adapter │ │ Code Adapter │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 解析层 │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 数据标准化 │ │ 标签生成 │ │ 多语言翻译 │ │ +│ │ (normalizer) │ │ (tagger) │ │ (translator)│ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 验证层 │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 质量评分 │ │ 去重检测 │ │ 垃圾过滤 │ │ +│ │ (scorer) │ │ (deduper) │ │ (spam-filter)│ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 存储层 │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Webhook │ │ Prisma │ │ PostgreSQL │ │ +│ │ API │ │ ORM │ │ Database │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 任务队列 │ +│ ┌──────────────────────────────┐ │ +│ │ BullMQ Queue │ │ +│ │ - 采集任务 │ │ +│ │ - 更新任务 │ │ +│ │ - 死链检测 │ │ +│ └──────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 7.2 目录结构 + +``` +src/ +├── lib/ +│ ├── scrapers/ # 数据采集器 +│ │ ├── base.ts # 基础采集器接口 +│ │ ├── github.ts # GitHub采集器 +│ │ ├── huggingface.ts # HF采集器 +│ │ └── paperswithcode.ts +│ │ +│ ├── processors/ # 数据处理器 +│ │ ├── normalizer.ts # 数据标准化 +│ │ ├── tagger.ts # 标签生成 +│ │ ├── translator.ts # 多语言翻译 +│ │ └── slugify.ts # Slug生成 +│ │ +│ ├── validators/ # 数据验证器 +│ │ ├── scorer.ts # 质量评分 +│ │ ├── deduper.ts # 去重检测 +│ │ └── spam-filter.ts # 垃圾过滤 +│ │ +│ └── queue/ # 任务队列 +│ ├── producer.ts # 任务生产者 +│ ├── consumer.ts # 任务消费者 +│ └── jobs/ # 任务定义 +│ ├── fetch-project.ts +│ ├── refresh-project.ts +│ └── check-links.ts +│ +├── app/ +│ └── api/ +│ └── admin/ # 管理API +│ ├── ingest/ +│ │ └── route.ts # 手动触发采集 +│ └── maintenance/ +│ └── route.ts # 手动触发维护 +│ +└── scripts/ + ├── ingest-trending.ts # 采集trending项目 + ├── refresh-all.ts # 刷新所有项目 + └── health-check.ts # 系统健康检查 +``` + +### 7.3 核心接口定义 + +#### 7.3.1 采集器接口 + +```typescript +// src/lib/scrapers/base.ts +export interface ProjectScraper { + // 识别平台 + platform: ProjectSource; + + // 从URL识别是否属于该平台 + canHandle(url: string): boolean; + + // 获取项目基础信息 + fetchBasic(url: string): Promise; + + // 获取项目完整信息 + fetchFull(url: string): Promise; + + // 获取趋势列表 + fetchTrending(options?: TrendingOptions): Promise; +} + +export interface BasicProjectInfo { + name: string; + description: string; + homepage?: string; + repository: string; + stars?: number; +} + +export interface FullProjectInfo extends BasicProjectInfo { + content: string; + languages: Record; + tags: string[]; + contributors: number; + lastUpdated: Date; + // ... +} +``` + +#### 7.3.2 处理器接口 + +```typescript +// src/lib/processors/normalizer.ts +export async function normalizeProject( + rawProject: FullProjectInfo, + source: ProjectSource +): Promise { + // 1. 基础字段映射 + const base = { + name: rawProject.name, + nameEn: rawProject.name, + description: rawProject.description, + descriptionEn: rawProject.description, + // ... + }; + + // 2. 内容处理 + const content = processMarkdown(rawProject.content); + + // 3. 标签生成 + const tags = await extractTags(rawProject); + + // 4. Slug生成 + const slug = generateSlug(base.name, base.nameEn); + + // 5. 多语言翻译 + const translated = await translateIfNeeded(base, content); + + return { + ...base, + ...translated, + slug, + tags, + content, + source, + externalLinks: buildExternalLinks(rawProject), + status: 'ACTIVE' + }; +} +``` + +### 7.4 任务队列配置 + +```typescript +// src/lib/queue/producer.ts +import { Queue } from 'bullmq'; +import Redis from 'ioredis'; + +const connection = new Redis({ + host: process.env.REDIS_HOST, + port: 6379, + maxRetriesPerRequest: 3 +}); + +export const ingestQueue = new Queue('project-ingestion', { connection }); + +export async function scheduleIngest(url: string) { + await ingestQueue.add('ingest-project', { url }, { + attempts: 3, + backoff: { type: 'exponential', delay: 5000 }, + removeOnComplete: { count: 1000 }, + removeOnFail: { count: 5000 } + }); +} + +export async function scheduleBulkIngest(urls: string[]) { + const jobs = urls.map(url => ({ + name: 'ingest-project', + data: { url } + })); + + await ingestQueue.addBulk(jobs); +} +``` + +```typescript +// src/lib/queue/consumer.ts +import { Worker } from 'bullmq'; +import { scrapeProject } from '../scrapers'; +import { normalizeProject } from '../processors/normalizer'; +import { validateProject } from '../validators'; +import { prisma } from '../prisma'; + +const worker = new Worker('project-ingestion', async (job) => { + const { url } = job.data; + + // 1. 识别平台并采集 + const scraper = identifyScraper(url); + const rawProject = await scraper.fetchFull(url); + + // 2. 标准化处理 + const normalized = await normalizeProject(rawProject, scraper.platform); + + // 3. 质量验证 + const validation = await validateProject(normalized); + + if (!validation.passed) { + throw new Error(`Validation failed: ${validation.reasons.join(', ')}`); + } + + // 4. 去重检测 + const existing = await checkDuplicate(normalized); + if (existing.exists) { + return { action: 'skipped', reason: 'duplicate', projectId: existing.project.id }; + } + + // 5. 写入数据库(通过Webhook API) + const response = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/webhook/projects`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + apiKey: process.env.WEBHOOK_API_KEY, + projects: [normalized] + }) + }); + + if (!response.ok) { + throw new Error(`Failed to store project: ${response.statusText}`); + } + + return { action: 'created', projectId: result.id }; +}, { connection }); +``` + +--- + +## 8. 实施路径 + +### 8.1 阶段规划 + +#### Phase 1: GitHub MVP(Week 1-2) + +- [ ] 实现 GitHub 采集器(REST + GraphQL) +- [ ] 实现基础标签提取(topics + languages) +- [ ] 实现数据标准化流程 +- [ ] 集成现有 Webhook API +- [ ] 添加基础质量评分 + +**交付物**:能从 GitHub URL 采集完整项目信息 + +#### Phase 2: Trending 自动化(Week 3) + +- [ ] 实现 GitHub Trending 解析 +- [ ] 配置定时任务(每日凌晨) +- [ ] 实现去重逻辑 +- [ ] 添加监控告警 + +**交付物**:每日自动采集 trending 项目 + +#### Phase 3: Hugging Face 集成(Week 4) + +- [ ] 实现 HF 采集器 +- [ ] 解析 HF YAML 元数据 +- [ ] 实现 HF Trending 采集 +- [ ] 扩展标签体系(pipeline_tag) + +**交付物**:支持 HF 模型/数据集 + +#### Phase 4: 智能化增强(Week 5-6) + +- [ ] 实现 NLP 标签提取 +- [ ] 集成翻译 API(DeepL 或 GPT-4) +- [ ] 实现质量评分模型 +- [ ] 添加垃圾项目检测 + +**交付物**:自动化标签生成和翻译 + +#### Phase 5: 维护系统(Week 7) + +- [ ] 实现增量更新机制 +- [ ] 实现死链检测 +- [ ] 实现热度衰减算法 +- [ ] 添加生命周期管理 + +**交付物**:数据自动维护 + +#### Phase 6: 扩展数据源(Week 8+) + +- [ ] Papers with Code 集成 +- [ ] Reddit/HN 讨论挖掘 +- [ ] Product Hunt 集成 +- [ ] AI导航站爬取 + +**交付物**:多源数据融合 + +### 8.2 监控指标 + +```yaml +采集指标: + - 每日新增项目数: target >= 20 + - 采集成功率: target >= 95% + - API调用次数: 监控配额使用 + +质量指标: + - 高质量项目占比: target >= 70% + - 垃圾项目过滤率: target >= 98% + - 去重准确率: target >= 99% + +维护指标: + - 死链检测覆盖率: 100% + - 更新及时性: 活跃项目7天内更新 + - 数据新鲜度: 90%项目在90天内更新 +``` + +### 8.3 技术选型 + +```yaml +任务队列: BullMQ (基于Redis) +定时任务: node-cron +爬虫框架: axios + cheerio +NLP处理: OpenAI API / Hugging Face Inference API +翻译服务: DeepL API / OpenAI API +监控告警: Sentry + 自定义webhook +``` + +--- + +## 9. 附录 + +### 9.1 API 密钥配置 + +```bash +# .env.local +GITHUB_TOKEN=ghp_xxxxx +HUGGING_FACE_TOKEN=hf_xxxxx +DEEPL_API_KEY=xxxxx +OPENAI_API_KEY=sk-xxxxx +WEBHOOK_API_KEY=xxxxx +REDIS_HOST=localhost +``` + +### 9.2 参考资源 + +- GitHub REST API: https://docs.github.com/en/rest +- GitHub GraphQL API: https://docs.github.com/en/graphql +- Hugging Face API: https://huggingface.co/docs/huggingface_hub/guides/huggingface_hub_pipelines +- Papers with Code: https://paperswithcode.com/docs/api.html +- DeepL API: https://www.deepl.com/docs-api + +--- + +**文档版本**: v1.0 +**最后更新**: 2024年 +**维护者**: AI项目导航站团队 diff --git a/next.config.js b/next.config.js index 274de71..4269219 100644 --- a/next.config.js +++ b/next.config.js @@ -7,7 +7,8 @@ const nextConfig = { images: { remotePatterns: [ { hostname: 'localhost' }, - { hostname: '*.anthropic.com' } + { hostname: '*.anthropic.com' }, + { hostname: 'img.shields.io' } ] }, experimental: { diff --git a/src/app/[locale]/projects/[id]/page.tsx b/src/app/[locale]/projects/[id]/page.tsx index 70e41e6..28f7605 100644 --- a/src/app/[locale]/projects/[id]/page.tsx +++ b/src/app/[locale]/projects/[id]/page.tsx @@ -33,7 +33,7 @@ export default async function ProjectDetailPage({ return (
-
+
{/* Back Button */} + ) +} + +/** + * GitHub Stars 紧凑型组件 - 用于项目卡片 + * 在较小的空间内显示 GitHub Stars 数量 + */ +export function GitHubStatsCompact({ + starsUrl, + className = '' +}: { + starsUrl?: string | null + className?: string +}) { + if (!starsUrl) { + return null + } + + return ( +
+ + + + Stars +
+ ) +} diff --git a/src/components/project/GitHubTextStatsCard.tsx b/src/components/project/GitHubTextStatsCard.tsx new file mode 100644 index 0000000..1451b84 --- /dev/null +++ b/src/components/project/GitHubTextStatsCard.tsx @@ -0,0 +1,138 @@ +import Link from 'next/link' +import Image from 'next/image' + +/** + * GitHub Statistics 数据接口 - 支持文本值或徽章 URL + */ +export interface GitHubTextStats { + owner?: string | null + repo?: string | null + stars?: string | null // 文本值(如 "142k")或徽章 URL + forks?: string | null // 文本值(如 "34k")或徽章 URL + issues?: string | null // 文本值(如 "892")或徽章 URL + license?: string | null // 文本值(如 "MIT")或徽章 URL +} + +interface GitHubTextStatsCardProps { + stats: GitHubTextStats + className?: string +} + +/** + * 检查字符串是否是 URL + */ +function isUrl(str: string): boolean { + return str.startsWith('http://') || str.startsWith('https://') +} + +/** + * 渲染统计值 - 支持文本或徽章图片 + */ +function renderStatValue(value: string) { + if (isUrl(value)) { + return ( + Stat + ) + } + return
{value}
+} + +/** + * GitHub Statistics Card Component - 混合模式版本 + * 支持文本显示和徽章图片显示,符合 Neo-brutalism 设计风格 + * + * 设计原型来源: design/detail.html 第 223-289 行 + */ +export function GitHubTextStatsCard({ stats, className = '' }: GitHubTextStatsCardProps) { + // 如果没有任何统计数据,不显示卡片 + if (!stats.stars && !stats.forks && !stats.issues && !stats.license) { + return null + } + + return ( +
+ {/* Header */} +

+ + + + GitHub Statistics +

+ + {/* Stats Grid - 2 columns */} +
+ {/* Stars */} + {stats.stars && stats.owner && stats.repo && ( + +
+ + + + {renderStatValue(stats.stars)} +
+ + )} + + {/* Forks */} + {stats.forks && stats.owner && stats.repo && ( + +
+ + + + {renderStatValue(stats.forks)} +
+ + )} + + {/* Issues */} + {stats.issues && stats.owner && stats.repo && ( + +
+ + + + + {renderStatValue(stats.issues)} +
+ + )} + + {/* License */} + {stats.license && stats.owner && stats.repo && ( + +
+ + + + {renderStatValue(stats.license)} +
+ + )} +
+
+ ) +} diff --git a/src/components/project/ProjectCard.tsx b/src/components/project/ProjectCard.tsx index d4d40b9..a95f480 100644 --- a/src/components/project/ProjectCard.tsx +++ b/src/components/project/ProjectCard.tsx @@ -1,5 +1,8 @@ import Link from 'next/link' +import Image from 'next/image' import { getTranslations } from 'next-intl/server' +import { GitHubStatsCompact } from './GitHubBadges' +import { getGitHubBadgesFromLinks } from '@/lib/github/badges' interface ProjectCardProps { project: { @@ -15,6 +18,12 @@ interface ProjectCardProps { nameEn?: string | null slug: string }> + links?: Array<{ + id: string + type: string + url: string + title?: string | null + }> } locale: string featured?: boolean @@ -37,6 +46,9 @@ export async function ProjectCard({ project, locale, featured = false }: Project const displayName = locale === 'en' && project.nameEn ? project.nameEn : project.name const displayDescription = locale === 'en' && project.descriptionEn ? project.descriptionEn : project.description + // Generate GitHub badge URLs + const badges = project.links ? getGitHubBadgesFromLinks(project.links as any) : { stars: null, forks: null } + // Helper to get display name for tag based on locale const getTagName = (tag: { name: string; nameEn?: string | null }) => { return locale === 'en' && tag.nameEn ? tag.nameEn : tag.name @@ -73,14 +85,34 @@ export async function ProjectCard({ project, locale, featured = false }: Project ))}
- {/* View Details Link */} - - {t('viewDetails')}{' '} - - + {/* Footer with View Details and Stars */} +
+ {/* View Details Link */} + + {t('viewDetails')}{' '} + + + + {/* GitHub Stars Badge */} + {badges.stars && ( +
+ + + + Stars +
+ )} +
) diff --git a/src/components/project/ProjectDetail.tsx b/src/components/project/ProjectDetail.tsx index ab305ef..d36c04a 100644 --- a/src/components/project/ProjectDetail.tsx +++ b/src/components/project/ProjectDetail.tsx @@ -2,6 +2,8 @@ import Link from 'next/link' import { getTranslations } from 'next-intl/server' import { MarkdownContent } from './MarkdownContent' import { ShareButtons } from './ShareButtons' +import { GitHubBadges } from './GitHubBadges' +import { getGitHubBadgesFromLinks } from '@/lib/github/badges' interface ProjectDetailProps { project: { @@ -54,6 +56,9 @@ export async function ProjectDetail({ project, locale }: ProjectDetailProps) { locale === 'en' && project.descriptionEn ? project.descriptionEn : project.description const displayContent = locale === 'en' && project.contentEn ? project.contentEn : project.content + // Generate GitHub badge URLs + const badges = getGitHubBadgesFromLinks(project.links as any) + // Get category from first tag const category = project.tags[0]?.name || 'AI Agent' const categoryEn = project.tags[0]?.nameEn || project.tags[0]?.name || 'AI Agent' diff --git a/src/components/project/ProjectSidebar.tsx b/src/components/project/ProjectSidebar.tsx index 47dac43..7b88899 100644 --- a/src/components/project/ProjectSidebar.tsx +++ b/src/components/project/ProjectSidebar.tsx @@ -1,5 +1,7 @@ import Link from 'next/link' import { getTranslations } from 'next-intl/server' +import { GitHubTextStatsCard } from './GitHubTextStatsCard' +import { getGitHubInfoFromLinks } from '@/lib/github/badges' interface ProjectSidebarProps { project: { @@ -37,6 +39,9 @@ export async function ProjectSidebar({ project, locale }: ProjectSidebarProps) { const t = await getTranslations('project') const displayName = locale === 'en' && project.nameEn ? project.nameEn : project.name + // 获取 GitHub 统计数据 + const githubInfo = getGitHubInfoFromLinks(project.links) + return (
- - {/* Stats Section */} -
-
-
- {t('license')} - MIT -
-
- {t('pricing')} - Free -
-
- {t('status')} - Active -
-
- {t('type')} - {t('openSource')} -
-
-
)} + {/* GitHub Statistics Card */} + {githubInfo.owner && githubInfo.repo && ( + + )} + {/* CTA Card */}
diff --git a/src/lib/github/api.ts b/src/lib/github/api.ts new file mode 100644 index 0000000..f44c2fd --- /dev/null +++ b/src/lib/github/api.ts @@ -0,0 +1,92 @@ +/** + * GitHub API 服务 + * 获取仓库的统计数据(stars, forks, issues, license 等) + */ + +export interface GitHubStats { + stargazers_count: number + forks_count: number + open_issues_count: number + license: { key: string; name: string } | null + pushed_at: string +} + +/** + * 从 GitHub API 获取仓库统计信息 + * @param owner - 仓库所有者 + * @param repo - 仓库名称 + * @returns GitHub 统计数据或 null + */ +export async function getGitHubStats( + owner: string, + repo: string +): Promise { + try { + const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { + headers: { + Accept: 'application/vnd.github.v3+json', + // 如果需要更高的速率限制,可以添加 GitHub token + // Authorization: `token ${process.env.GITHUB_TOKEN}`, + }, + next: { revalidate: 300 } // 缓存 5 分钟 + }) + + if (!response.ok) { + console.error(`GitHub API error: ${response.status}`) + return null + } + + const data = await response.json() + return { + stargazers_count: data.stargazers_count || 0, + forks_count: data.forks_count || 0, + open_issues_count: data.open_issues_count || 0, + license: data.license || null, + pushed_at: data.pushed_at || '' + } + } catch (error) { + console.error('Failed to fetch GitHub stats:', error) + return null + } +} + +/** + * 格式化数字(如 142000 -> 142k) + */ +export function formatNumber(num: number): string { + if (num >= 1000000) { + return (num / 1000000).toFixed(1).replace(/\.0$/, '') + 'M' + } + if (num >= 1000) { + return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'k' + } + return num.toString() +} + +/** + * 格式化相对时间(如 "2 days ago") + */ +export function formatRelativeTime(dateString: string, locale: string = 'zh'): string { + if (!dateString) return '' + + const date = new Date(dateString) + const now = new Date() + const diffMs = now.getTime() - date.getTime() + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)) + + if (locale === 'en') { + if (diffDays === 0) return 'today' + if (diffDays === 1) return 'yesterday' + if (diffDays < 7) return `${diffDays} days ago` + if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago` + if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago` + return `${Math.floor(diffDays / 365)} years ago` + } else { + if (diffDays === 0) return '今天' + if (diffDays === 1) return '昨天' + if (diffDays < 7) return `${diffDays} 天前` + if (diffDays < 30) return `${Math.floor(diffDays / 7)} 周前` + if (diffDays < 365) return `${Math.floor(diffDays / 30)} 月前` + return `${Math.floor(diffDays / 365)} 年前` + } +} diff --git a/src/lib/github/badges.ts b/src/lib/github/badges.ts new file mode 100644 index 0000000..39ae221 --- /dev/null +++ b/src/lib/github/badges.ts @@ -0,0 +1,140 @@ +import { ExternalLink, LinkType } from '@prisma/client' + +/** + * 从 GitHub URL 提取 owner 和 repo + * @param url - GitHub 仓库 URL + * @returns owner 和 repo,或 null 如果无法解析 + */ +export function parseGitHubUrl(url: string): { owner: string; repo: string } | null { + const patterns = [ + /github\.com\/([^\/]+)\/([^\/\?#]+)/, + /github\.com\/([^\/]+)\/([^\/\?#]+)\.git/ + ] + + for (const pattern of patterns) { + const match = url.match(pattern) + if (match) { + const repo = match[2].replace(/\.git$/, '').replace(/\/$/, '') + return { owner: match[1], repo } + } + } + + return null +} + +/** + * 生成 GitHub Stars 徽章 URL + * @param owner - 仓库所有者 + * @param repo - 仓库名称 + * @returns Shields.io 徽章 URL + */ +export function getGitHubStarsBadgeUrl(owner: string, repo: string): string { + return `https://img.shields.io/github/stars/${owner}/${repo}?style=social` +} + +/** + * 生成所有 GitHub 徽章 URL + * @param owner - 仓库所有者 + * @param repo - 仓库名称 + * @returns 包含所有徽章 URL 的对象 + */ +export function getAllGitHubBadgeUrls(owner: string, repo: string): { + stars: string + forks: string + issues: string + license: string + lastCommit: string +} { + return { + stars: `https://img.shields.io/github/stars/${owner}/${repo}?style=social`, + forks: `https://img.shields.io/github/forks/${owner}/${repo}?style=social`, + issues: `https://img.shields.io/github/issues/${owner}/${repo}`, + license: `https://img.shields.io/github/license/${owner}/${repo}`, + lastCommit: `https://img.shields.io/github/last-commit/${owner}/${repo}` + } +} + +/** + * 从项目链接中提取 GitHub Stars 徽章 URL + * @param links - 项目外部链接数组 + * @returns stars 徽章 URL 或 null + */ +export function getGitHubBadgesFromLinks( + links: ExternalLink[] +): { stars: string | null } { + const githubLink = links.find(link => link.type === 'GITHUB') + + if (!githubLink) { + return { stars: null } + } + + const parsed = parseGitHubUrl(githubLink.url) + + if (!parsed) { + return { stars: null } + } + + return { + stars: getGitHubStarsBadgeUrl(parsed.owner, parsed.repo) + } +} + +/** + * 从项目链接中提取 GitHub 信息(用于文本显示) + * @param links - 项目外部链接数组 + * @returns GitHub 统计数据对象 + */ +export function getGitHubInfoFromLinks( + links: Array<{ type: string; url: string }> +): { + owner: string | null + repo: string | null + stars: string | null + forks: string | null + issues: string | null + license: string | null + lastCommit: string | null +} { + const githubLink = links.find(link => link.type === 'GITHUB') + + if (!githubLink) { + return { + owner: null, + repo: null, + stars: null, + forks: null, + issues: null, + license: null, + lastCommit: null + } + } + + const parsed = parseGitHubUrl(githubLink.url) + + if (!parsed) { + return { + owner: null, + repo: null, + stars: null, + forks: null, + issues: null, + license: null, + lastCommit: null + } + } + + // 返回徽章 URL,组件中可以使用这些 URL 获取实际数据 + // 或者通过 GitHub API 获取实际数字 + const badgeUrls = getAllGitHubBadgeUrls(parsed.owner, parsed.repo) + + return { + owner: parsed.owner, + repo: parsed.repo, + // 暂时返回徽章 URL,后续可以通过 API 获取实际数字 + stars: badgeUrls.stars, + forks: badgeUrls.forks, + issues: badgeUrls.issues, + license: badgeUrls.license, + lastCommit: badgeUrls.lastCommit + } +}