diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..a48cf03 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,669 @@ +# API 接口文档 + +> AI 项目导航站对外提供的 REST API 接口文档 + +## 目录 + +- [1. 概述](#1-概述) +- [2. 认证方式](#2-认证方式) +- [3. 接口列表](#3-接口列表) + - [3.1 创建/更新项目 (Webhook)](#31-创建更新项目-webhook) + - [3.2 获取项目详情](#32-获取项目详情) + - [3.3 删除项目](#33-删除项目) +- [4. 数据模型](#4-数据模型) +- [5. 错误码](#5-错误码) + +--- + +## 1. 概述 + +### 1.1 Base URL + +``` +生产环境: https://your-domain.com +开发环境: http://localhost:3000 +``` + +### 1.2 响应格式 + +所有接口返回 JSON 格式数据: + +```typescript +// 成功响应 +{ + "success": true, + "data": { ... }, + "message": "操作成功" +} + +// 错误响应 +{ + "success": false, + "error": "错误类型", + "details": ["详细错误信息1", "详细错误信息2"] +} +``` + +### 1.3 通用请求头 + +``` +Content-Type: application/json +x-api-key: your-api-key-here # 需要认证的接口 +``` + +--- + +## 2. 认证方式 + +### API Key 认证 + +所有 API 接口均使用 API Key 进行认证。API Key 通过请求头 `x-api-key` 传递。 + +```bash +# 设置环境变量 +WEBHOOK_API_KEY=your-secret-api-key + +# 请求示例 +curl -X POST https://your-domain.com/api/webhook/projects \ + -H "Content-Type: application/json" \ + -H "x-api-key: your-secret-api-key" \ + -d '...' +``` + +**注意事项**: +- API Key 需要在服务端环境变量中配置 `WEBHOOK_API_KEY` +- 请妥善保管 API Key,不要在客户端代码中暴露 +- 建议定期轮换 API Key + +--- + +## 3. 接口列表 + +### 3.1 创建/更新项目 (Webhook) + +批量创建或更新项目数据。支持多级去重策略自动识别已存在的项目。 + +#### 3.1.1 接口信息 + +``` +POST /api/webhook/projects +``` + +#### 3.1.2 请求参数 + +**Headers**: + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| x-api-key | string | 是 | API 密钥 | +| Content-Type | string | 是 | 必须为 `application/json` | + +**Body**: + +```typescript +{ + apiKey: string; // API 密钥(与 header x-api-key 二选一) + projects: ProjectInput[]; // 项目数组(1-100个) +} +``` + +**ProjectInput 类型**: + +```typescript +{ + // 基础信息(必填) + name: string; // 中文名称 + nameEn?: string; // 英文名称(可选) + description: string; // 中文描述 + descriptionEn?: string; // 英文描述(可选) + + // 内容(可选) + content?: string; // 中文内容(Markdown 格式) + contentEn?: string; // 英文内容(Markdown 格式) + + // 状态(可选) + status?: "ACTIVE" | "ARCHIVED"; // 默认: "ACTIVE" + source?: string; // 数据来源标识 + + // 关联(必填) + tags: Array<{ // 标签数组(1-10个) + name: string; // 标签名 + nameEn?: string; // 英文标签名(可选) + }>; + links: Array<{ // 外部链接数组(1-10个) + type: "WEBSITE" | "GITHUB" | "HUGGINGFACE" | "PAPER"; + url: string; // 链接 URL + title?: string; // 链接标题(可选) + }>; +} +``` + +#### 3.1.3 多级去重策略 + +Webhook 会按以下优先级识别已存在的项目: + +1. **GitHub URL 精确匹配**(最准确) +2. **Website URL 精确匹配** +3. **slug 匹配**(兜底) + +如果找到已存在的项目,将执行更新操作: +- 更新所有项目字段 +- 替换所有标签(删除旧的,创建新的) +- 替换所有链接(删除旧的,创建新的) + +#### 3.1.4 请求示例 + +```bash +curl -X POST https://your-domain.com/api/webhook/projects \ + -H "Content-Type: application/json" \ + -H "x-api-key: your-api-key" \ + -d '{ + "apiKey": "your-api-key", + "projects": [ + { + "name": "LangChain", + "nameEn": "LangChain", + "description": "开发由语言模型驱动的应用程序框架", + "descriptionEn": "Developing applications powered by language models", + "content": "# LangChain\n\nLangChain 是一个...", + "contentEn": "# LangChain\n\nLangChain is a...", + "status": "ACTIVE", + "source": "GITHUB", + "tags": [ + { "name": "LLM", "nameEn": "Large Language Model" }, + { "name": "Python", "nameEn": "Python" }, + { "name": "开发框架", "nameEn": "Development Framework" } + ], + "links": [ + { + "type": "GITHUB", + "url": "https://github.com/langchain-ai/langchain", + "title": "GitHub 仓库" + }, + { + "type": "WEBSITE", + "url": "https://langchain.com", + "title": "官方网站" + } + ] + } + ] + }' +``` + +#### 3.1.5 响应示例 + +**成功响应** (200 OK): + +```json +{ + "success": true, + "processed": 1, + "created": 0, + "updated": 1, + "failed": 0, + "errors": [] +} +``` + +**错误响应** (400 Bad Request): + +```json +{ + "success": false, + "error": "Validation error", + "details": [ + "tags: Field must contain at least 1 element", + "links: Field must contain at most 10 elements" + ] +} +``` + +**认证失败** (401 Unauthorized): + +```json +{ + "success": false, + "error": "Unauthorized", + "details": ["Invalid or missing API Key"] +} +``` + +--- + +### 3.2 获取项目详情 + +根据项目的 slug 获取项目详细信息。 + +#### 3.2.1 接口信息 + +``` +GET /api/projects/:slug +``` + +#### 3.2.2 路径参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| slug | string | 是 | 项目的唯一标识符 | + +#### 3.2.3 请求示例 + +```bash +curl -X GET https://your-domain.com/api/projects/langchain \ + -H "Content-Type: application/json" +``` + +#### 3.2.4 响应示例 + +**成功响应** (200 OK): + +```json +{ + "success": true, + "data": { + "id": "clx1234567890", + "name": "LangChain", + "nameEn": "LangChain", + "slug": "langchain", + "description": "开发由语言模型驱动的应用程序框架", + "descriptionEn": "Developing applications powered by language models", + "content": "# LangChain\n\nLangChain 是一个...", + "contentEn": "# LangChain\n\nLangChain is a...", + "status": "ACTIVE", + "source": "GITHUB", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-15T00:00:00.000Z", + "links": [ + { + "id": "link123", + "type": "GITHUB", + "url": "https://github.com/langchain-ai/langchain", + "title": "GitHub 仓库" + } + ], + "tags": [ + { + "id": "tag123", + "name": "LLM", + "nameEn": "Large Language Model", + "slug": "large-language-model" + } + ] + } +} +``` + +**项目不存在** (404 Not Found): + +```json +{ + "success": false, + "error": "Not Found", + "details": ["Project with slug \"nonexistent\" not found"] +} +``` + +--- + +### 3.3 删除项目 + +根据项目的 slug 删除项目及其所有关联数据。 + +#### 3.3.1 接口信息 + +``` +DELETE /api/projects/:slug +``` + +#### 3.3.2 路径参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| slug | string | 是 | 项目的唯一标识符 | + +#### 3.3.3 请求头 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| x-api-key | string | 是 | API 密钥 | +| Content-Type | string | 是 | 必须为 `application/json` | + +#### 3.3.4 级联删除说明 + +由于数据库配置了 `onDelete: Cascade`,删除项目时会自动删除: + +- ✅ 该项目的所有外部链接(`ExternalLink`) +- ✅ 该项目的所有标签关联(`ProjectTag`) +- ❌ Tag 本身不会被删除(只删除项目与标签的关联关系) + +#### 3.3.5 请求示例 + +```bash +curl -X DELETE https://your-domain.com/api/projects/langchain \ + -H "Content-Type: application/json" \ + -H "x-api-key: your-api-key" +``` + +#### 3.3.6 响应示例 + +**成功响应** (200 OK): + +```json +{ + "success": true, + "message": "Project deleted successfully", + "data": { + "project": { + "id": "clx1234567890", + "name": "LangChain", + "nameEn": "LangChain", + "slug": "langchain" + }, + "deleted": { + "linksCount": 2, + "tagsCount": 3 + } + } +} +``` + +**项目不存在** (404 Not Found): + +```json +{ + "success": false, + "error": "Not Found", + "details": ["Project with slug \"nonexistent\" not found"] +} +``` + +**认证失败** (401 Unauthorized): + +```json +{ + "success": false, + "error": "Unauthorized", + "details": ["Invalid or missing API Key"] +} +``` + +--- + +## 4. 数据模型 + +### 4.1 Project 状态枚举 + +```typescript +enum ProjectStatus { + ACTIVE = "ACTIVE", // 活跃项目 + ARCHIVED = "ARCHIVED" // 已归档项目 +} +``` + +### 4.2 链接类型枚举 + +```typescript +enum LinkType { + WEBSITE = "WEBSITE", // 官方网站 + GITHUB = "GITHUB", // GitHub 仓库 + HUGGINGFACE = "HUGGINGFACE", // Hugging Face 模型/数据集 + PAPER = "PAPER" // 论文链接 +} +``` + +### 4.3 完整项目模型 + +```typescript +interface Project { + id: string; // 项目唯一 ID(cuid 格式) + name: string; // 中文名称 + nameEn: string | null; // 英文名称 + slug: string; // URL 友好标识符(唯一) + description: string; // 中文描述 + descriptionEn: string | null;// 英文描述 + content: string | null; // 中文内容(Markdown) + contentEn: string | null; // 英文内容(Markdown) + status: ProjectStatus; // 项目状态 + source: string | null; // 数据来源 + createdAt: Date; // 创建时间 + updatedAt: Date; // 更新时间 + + // 关联数据 + tags: Tag[]; // 标签数组 + links: ExternalLink[]; // 外部链接数组 +} +``` + +### 4.4 Tag 模型 + +```typescript +interface Tag { + id: string; // 标签唯一 ID + name: string; // 中文名称(唯一) + nameEn: string | null; // 英文名称 + slug: string; // URL 友好标识符(唯一) + createdAt: Date; // 创建时间 +} +``` + +### 4.5 ExternalLink 模型 + +```typescript +interface ExternalLink { + id: string; // 链接唯一 ID + type: LinkType; // 链接类型 + url: string; // 链接 URL + title: string | null; // 链接标题 + projectId: string; // 所属项目 ID +} +``` + +--- + +## 5. 错误码 + +### 5.1 HTTP 状态码 + +| 状态码 | 说明 | 示例场景 | +|--------|------|----------| +| 200 OK | 请求成功 | 成功获取/创建/更新/删除数据 | +| 400 Bad Request | 请求参数错误 | 必填字段缺失、字段格式错误 | +| 401 Unauthorized | 认证失败 | API Key 无效或缺失 | +| 404 Not Found | 资源不存在 | 请求的项目 slug 不存在 | +| 500 Internal Server Error | 服务器内部错误 | 数据库连接失败、程序异常 | + +### 5.2 业务错误类型 + +| 错误类型 | 说明 | 处理建议 | +|----------|------|----------| +| Validation error | 数据验证失败 | 检查请求体字段是否符合要求 | +| Unauthorized | API Key 无效 | 检查 API Key 是否正确 | +| Not Found | 资源不存在 | 确认 slug 是否正确 | +| Internal server error | 服务器错误 | 联系技术支持或稍后重试 | + +### 5.3 验证规则 + +#### 项目数据验证 + +```typescript +// 必填字段 +- name: 非空字符串,长度 1-200 +- description: 非空字符串,长度 1-5000 +- tags: 数组,长度 1-10,每个 tag.name 非空 +- links: 数组,长度 1-10,每个 link.url 和 link.type 非空 + +// 可选字段 +- nameEn: 字符串,长度 1-200 +- descriptionEn: 字符串,长度 1-5000 +- content/contentEn: 文本类型,支持 Markdown +- status: 枚举值 "ACTIVE" 或 "ARCHIVED",默认 "ACTIVE" +- source: 字符串,标识数据来源 + +// URL 格式 +- links[*].url: 必须是有效的 HTTP/HTTPS URL +- links[*].type: 必须是 LinkType 枚举值之一 +``` + +#### Webhook 批量操作限制 + +```typescript +// 批量操作 +- projects: 数组,长度 1-100 +- apiKey: 必须与环境变量 WEBHOOK_API_KEY 匹配 +``` + +--- + +## 6. 使用示例 + +### 6.1 完整的工作流示例 + +```javascript +// 1. 创建/更新项目 +const createResponse = await fetch('https://your-domain.com/api/webhook/projects', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': 'your-api-key' + }, + body: JSON.stringify({ + apiKey: 'your-api-key', + projects: [{ + name: 'My AI Project', + nameEn: 'My AI Project', + description: '一个创新的 AI 项目', + descriptionEn: 'An innovative AI project', + status: 'ACTIVE', + source: 'MANUAL', + tags: [ + { name: 'AI', nameEn: 'Artificial Intelligence' }, + { name: '机器学习', nameEn: 'Machine Learning' } + ], + links: [ + { type: 'GITHUB', url: 'https://github.com/user/project', title: 'GitHub' }, + { type: 'WEBSITE', url: 'https://project.com', title: 'Website' } + ] + }] + }) +}); + +const createResult = await createResponse.json(); +console.log('创建结果:', createResult); +// { success: true, processed: 1, created: 1, updated: 0, failed: 0, errors: [] } + +// 2. 获取项目详情 +const slug = 'my-ai-project'; // 根据 nameEn 自动生成 +const getResponse = await fetch(`https://your-domain.com/api/projects/${slug}`); +const getResult = await getResponse.json(); +console.log('项目详情:', getResult.data); + +// 3. 删除项目 +const deleteResponse = await fetch(`https://your-domain.com/api/projects/${slug}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': 'your-api-key' + } +}); +const deleteResult = await deleteResponse.json(); +console.log('删除结果:', deleteResult); +``` + +### 6.2 批量创建项目示例 + +```javascript +const projects = [ + { + name: '项目 A', + nameEn: 'Project A', + description: '项目 A 的描述', + descriptionEn: 'Description of Project A', + tags: [{ name: '分类1' }], + links: [{ type: 'GITHUB', url: 'https://github.com/user/a' }] + }, + { + name: '项目 B', + nameEn: 'Project B', + description: '项目 B 的描述', + descriptionEn: 'Description of Project B', + tags: [{ name: '分类2' }], + links: [{ type: 'GITHUB', url: 'https://github.com/user/b' }] + } +]; + +const response = await fetch('https://your-domain.com/api/webhook/projects', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': 'your-api-key' + }, + body: JSON.stringify({ + apiKey: 'your-api-key', + projects: projects + }) +}); + +const result = await response.json(); +console.log('批量创建结果:', result); +// { success: true, processed: 2, created: 2, updated: 0, failed: 0, errors: [] } +``` + +--- + +## 7. 注意事项 + +### 7.1 Slug 生成规则 + +项目 slug 根据以下规则自动生成: + +1. 优先使用 `nameEn`(英文) +2. 如果 `nameEn` 不存在,使用 `name`(中文) +3. 转换为小写 +4. 空格替换为连字符 `-` +5. 移除特殊字符 + +示例: +- `nameEn: "LangChain"` → `slug: "langchain"` +- `name: "大语言模型"` → `slug: "大语言模型"` (会进行拼音转换) + +### 7.2 标签去重 + +- 标签的 `name` 字段在数据库中是唯一的 +- 如果创建已存在的标签,会自动复用现有标签 +- 标签的 `slug` 也是唯一的,会根据 `nameEn` 或 `name` 自动生成 + +### 7.3 链接去重 + +- 同一个项目不能有重复的 URL +- 通过 `projectId` + `url` 的组合保证唯一性 + +### 7.4 更新策略 + +- Webhook 使用 **替换策略** 更新标签和链接 +- 不是增量更新,而是完全替换 +- 更新时会删除旧的标签/链接关联,创建新的 + +--- + +## 8. 附录 + +### 8.1 环境变量配置 + +```bash +# .env +WEBHOOK_API_KEY=your-secret-api-key-here +DATABASE_URL=postgresql://user:password@host:5432/dbname?sslmode=require +``` + +### 8.2 相关文档 + +- [数据库 Schema](../prisma/schema.prisma) +- [数据验证规则](../src/lib/validations.ts) +- [数据新增流程设计](./data-ingestion-flow.md) + +--- + +**文档版本**: v1.0.0 +**最后更新**: 2024-01-11 +**维护者**: AI 项目导航站团队 diff --git a/docs/data-ingestion-flow.md b/docs/data-ingestion-flow.md deleted file mode 100644 index dca3598..0000000 --- a/docs/data-ingestion-flow.md +++ /dev/null @@ -1,1187 +0,0 @@ -# 数据新增流程设计文档 - -> 本文档详细描述了 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/src/app/api/projects/[slug]/route.ts b/src/app/api/projects/[slug]/route.ts new file mode 100644 index 0000000..fed116f --- /dev/null +++ b/src/app/api/projects/[slug]/route.ts @@ -0,0 +1,161 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +/** + * DELETE /api/projects/[slug] + * + * 根据项目的 slug 删除项目及其所有关联数据 + * + * 由于数据库 schema 配置了 onDelete: Cascade, + * 删除项目时会自动删除: + * - 该项目的所有外部链接(ExternalLink) + * - 该项目的所有标签关联(ProjectTag) + * + * 注意:Tag 本身不会被删除,只会删除项目与标签的关联关系 + */ +export async function DELETE( + request: NextRequest, + { params }: { params: { slug: string } } +) { + try { + const slug = params.slug + + // Verify API Key + const apiKey = request.headers.get('x-api-key') || process.env.WEBHOOK_API_KEY + const validApiKey = process.env.WEBHOOK_API_KEY + + if (apiKey !== validApiKey) { + return NextResponse.json( + { + success: false, + error: 'Unauthorized', + details: ['Invalid or missing API Key'], + }, + { status: 401 } + ) + } + + // Check if project exists + const existingProject = await prisma.project.findUnique({ + where: { slug }, + include: { + links: true, + tags: { + include: { + tag: true, + }, + }, + }, + }) + + if (!existingProject) { + return NextResponse.json( + { + success: false, + error: 'Not Found', + details: [`Project with slug "${slug}" not found`], + }, + { status: 404 } + ) + } + + // Delete project (cascade delete will handle links and project_tags) + await prisma.project.delete({ + where: { slug }, + }) + + console.warn( + `[API] Deleted project "${existingProject.name}" (slug: ${slug}, id: ${existingProject.id})` + ) + + return NextResponse.json({ + success: true, + message: 'Project deleted successfully', + data: { + project: { + id: existingProject.id, + name: existingProject.name, + nameEn: existingProject.nameEn, + slug: existingProject.slug, + }, + deleted: { + linksCount: existingProject.links.length, + tagsCount: existingProject.tags.length, + }, + }, + }) + } catch (error) { + console.error('[API] Error deleting project:', error) + return NextResponse.json( + { + success: false, + error: 'Internal server error', + details: [error instanceof Error ? error.message : 'Unknown error'], + }, + { status: 500 } + ) + } +} + +/** + * GET /api/projects/[slug] + * + * 根据项目的 slug 获取项目详情 + */ +export async function GET( + request: NextRequest, + { params }: { params: { slug: string } } +) { + try { + const slug = params.slug + + const project = await prisma.project.findUnique({ + where: { slug }, + include: { + links: true, + tags: { + include: { + tag: true, + }, + }, + }, + }) + + if (!project) { + return NextResponse.json( + { + success: false, + error: 'Not Found', + details: [`Project with slug "${slug}" not found`], + }, + { status: 404 } + ) + } + + // Transform response to match frontend structure + const transformedProject = { + ...project, + tags: project.tags.map((pt) => ({ + id: pt.tag.id, + name: pt.tag.name, + nameEn: pt.tag.nameEn, + slug: pt.tag.slug, + })), + } + + return NextResponse.json({ + success: true, + data: transformedProject, + }) + } catch (error) { + console.error('[API] Error fetching project:', error) + return NextResponse.json( + { + success: false, + error: 'Internal server error', + details: [error instanceof Error ? error.message : 'Unknown error'], + }, + { status: 500 } + ) + } +} diff --git a/src/components/project/ProjectDetail.tsx b/src/components/project/ProjectDetail.tsx index 6624e1d..1de0670 100644 --- a/src/components/project/ProjectDetail.tsx +++ b/src/components/project/ProjectDetail.tsx @@ -55,6 +55,41 @@ function isImageUrl(url: string): boolean { return imageExtensions.some(ext => lowerUrl.includes(ext)) } +// Helper function to check if URL can be embedded in iframe +// Many sites like GitHub, Google, etc. block iframe embedding via CSP +function isEmbeddableUrl(url: string): boolean { + const hostname = new URL(url).hostname.toLowerCase() + + // Whitelist of domains that allow iframe embedding + const embeddableDomains = [ + 'youtube.com', + 'youtu.be', + 'vimeo.com', + 'player.vimeo.com', + 'drive.google.com', + 'docs.google.com', + 'www.figma.com', + 'codepen.io', + 'jsfiddle.net', + 'codesandbox.io', + 'stackblitz.com', + 'replit.com', + 'loom.com', + 'wistia.com', + 'brightcove.com', + 'dailymotion.com', + 'twitch.tv', + 'soundcloud.com', + 'spotify.com', + 'canva.com', + 'notion.so', + 'typeform.com', + ] + + // Check if hostname matches any embeddable domain + return embeddableDomains.some((domain) => hostname === domain || hostname.endsWith('.' + domain)) +} + export async function ProjectDetail({ project, locale }: ProjectDetailProps) { const t = await getTranslations('project') @@ -124,14 +159,14 @@ export async function ProjectDetail({ project, locale }: ProjectDetailProps) { alt={`${displayName} demo`} className="w-full h-auto" /> - ) : ( + ) : isEmbeddableUrl(project.source) ? (