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/github-project-analysis-prompt.md b/github-project-analysis-prompt.md new file mode 100644 index 0000000..b0731f6 --- /dev/null +++ b/github-project-analysis-prompt.md @@ -0,0 +1,511 @@ +# GitHub 项目分析 Agent (增强版 - 支持图片) + +## 任务目标 + +分析单个 GitHub 项目并生成符合 webhook API 规范的 JSON 数据,用于项目入库。目标是生成包含丰富图文内容的高质量项目介绍页面。 + +## 输入数据 + +- **GitHub 项目 URL**: {{ $json.githubUrl }} + +## 可用工具 + +- **MCP Client (web_reader)**: 访问 GitHub 仓库页面及相关链接 + +--- + +## 执行步骤 + +### Step 1: 使用 MCP 获取项目基础信息 + +调用 MCP 工具访问 GitHub 仓库,提取: +- 仓库名称和描述 +- README.md 完整内容(**保留 Markdown 格式和图片链接**) +- GitHub Topics +- 主要编程语言 +- Stars、Forks 数量 +- 最新更新时间 +- 许可证类型 +- 主页 URL +- Releases 信息(如果有的话) + +**关键操作:提取图片列表** +- 从 README.md 中提取所有图片链接(`![alt](src)` 或 ``) +- 记录图片类型:架构图、截图、流程图、logo、GIF 动图等 +- 对于相对路径图片,转换为 GitHub 绝对 URL: + ``` + https://raw.githubusercontent.com/[owner]/[repo]/[branch]/[path] + ``` + +### Step 2: 深度理解项目价值和内容 + +从 README 内容中提炼(尽量保留图片): + +**项目用途**: +- 这个项目解决什么核心问题? +- 主要功能是什么? +- 有什么独特价值? +- 与同类项目相比的优势? + +**适用场景**: +- 谁会使用这个项目? +- 典型使用场景是什么? +- 属于哪个应用领域? + +**技术特点**: +- 使用了什么技术栈? +- 有什么技术亮点或创新点? +- 架构设计特点是什么? + +**如何使用**: +- 安装步骤 +- 配置说明 +- 快速开始指南 +- 常见操作 + +### Step 3: 生成中英双语内容 + +#### 3.1 name / nameEn + +- 通常使用英文名称 +- 如果有中文品牌名,使用原名 + +#### 3.2 description / descriptionEn ⚠️ 重要 + +简短的一句话总结,10-500 字符 + +**中文格式**: +``` +[项目名] 是一个[用途定位]的[类型],通过[核心特点]实现[价值主张] +``` + +示例: +``` +AutoGen 是一个由微软开发的多智能体应用框架,通过分层设计和可扩展架构,简化了构建能够自主运行或与人类协作的多智能体工作流程的开发流程 +``` + +**英文格式**: +``` +[Project] is a [type] for [purpose], featuring [key characteristics] +``` + +#### 3.3 content / contentEn ⚠️ 关键 - 严格结构 + 图片支持 + +完整的 Markdown 文档,最多 10000 字符。**必须严格按结构组织,并在适当位置嵌入图片**: + +```markdown +# 项目用途 + +[2-3句话详细描述项目的核心功能和解决的问题,说明项目的核心价值主张] + +[如果有项目 logo 或主展示图,在此插入] +![项目展示图](图片URL) + +# 适用场景 + +[列出3-5个典型使用场景] +- **[场景1名称]**:[具体说明,包含适用对象和具体用途] +- **[场景2名称]**:[具体说明,包含适用对象和具体用途] +- **[场景3名称]**:[具体说明,包含适用对象和具体用途] + +[如果有场景示意图,在此插入] +![架构图或场景图](图片URL) + +# 核心功能 + +[列出项目的主要功能特性,4-8项] +- **[功能1名称]**:[一句话说明这个功能的作用和价值] +- **[功能2名称]**:[一句话说明这个功能的作用和价值] +- **[功能3名称]**:[一句话说明这个功能的作用和价值] +- **[功能4名称]**:[一句话说明这个功能的作用和价值] + +[如果有功能截图,在此插入] +![功能演示截图](图片URL) + +# 技术架构 + +[详细说明技术架构和亮点,3-6点] +- **[技术1]**:[基于什么技术/框架,有什么特点] +- **[技术2]**:[支持什么具体特性,带来什么好处] +- **[技术3]**:[采用什么架构模式,解决什么问题] + +[**架构图优先在此位置插入**] +![系统架构图](图片URL) + +> 📐 **架构说明**:[对架构图的补充说明,描述主要组件、数据流向、技术栈等] + +# 如何使用 + +[详细的安装和配置步骤,6-10个步骤] +- **[步骤1标题]**:[具体操作,如安装命令、下载链接等] + ```bash + [命令示例] + ``` +- **[步骤2标题]**:[配置说明,如环境变量、配置文件等] +- **[步骤3标题]**:[创建或初始化项目] +- **[步骤4标题]**:[核心功能使用方法] +- **[步骤5标题]**:[常见操作说明] +- **[步骤6标题]**:[高级功能或最佳实践] + +[**如果有安装演示截图,在对应步骤后插入**] +![安装配置截图](图片URL) + +# 快速示例 + +[提供完整的、可运行的代码示例,10-20行代码] +```python/[javascript] +[从 README 中提取的实际代码示例,不要捏造] +``` + +[对代码示例的说明] + +[**如果有代码运行结果截图,在此插入**] +![运行效果截图](图片URL) + +# 实际效果展示 + +[如果有 GIF 动图或截图展示实际使用效果,在此集中展示] + +[**GIF 动图展示核心流程**] +![核心流程演示](GIF图片URL) + +[**界面截图展示**] +![界面截图1](图片URL) +![界面截图2](图片URL) + +> 💡 **效果说明**:[对截图/动图展示的功能进行说明] + +# 定价/成本 + +[说明项目的经济成本,明确透明] +- 开源免费:[如果完全免费,明确说明"完全免费和开源"] +- API 成本:[如果需要调用付费 API,说明相关成本] +- 企业版/付费版:[如果有商业版本,说明定价方案] +- 自部署成本:[如果需要自己部署,说明资源需求] + +# 常见问题 + +[3-5个 FAQ] +- Q: [问题1]? + A: [详细回答,2-3句话] +- Q: [问题2]? + A: [详细回答,2-3句话] +- Q: [问题3]? + A: [详细回答,2-3句话] +``` + +**英文版本保持相同结构**: + +```markdown +# Overview + +[2-3 sentences describing core functionality and value proposition] + +![Project Overview](Image URL) + +# Use Cases + +- **[Case 1]**: [Specific explanation] +- **[Case 2]**: [Specific explanation] + +![Use Case Diagram](Image URL) + +# Key Features + +- **[Feature 1]**: [One-sentence explanation] +- **[Feature 2]**: [One-sentence explanation] + +![Feature Screenshot](Image URL) + +# Technical Architecture + +- **[Tech 1]**: [Details] +- **[Tech 2]**: [Details] + +![Architecture Diagram](Image URL) + +> 📐 **Architecture Notes**: [Additional explanation of the architecture] + +# How to Use + +- **[Step 1]**: [Specific actions] + ```bash + [Commands] + ``` +- **[Step 2]**: [Configuration] + +![Setup Screenshot](Image URL) + +# Quick Example + +```python/[javascript] +[Code example from README] +``` + +![Result Screenshot](Image URL) + +# Live Demo + +![Demo GIF](GIF URL) + +![Interface Screenshot](Image URL) + +> 💡 **Demo Notes**: [Explanation of what's shown] + +# Pricing/Cost + +- Open Source: [Free or cost details] +- API Costs: [If applicable] + +# FAQ + +- Q: [Question 1]? + A: [Detailed answer] +``` + +--- + +### Step 4: 图片处理和质量控制 + +#### 4.1 图片 URL 规范化 + +**规则**: +1. 对于相对路径图片(如 `docs/architecture.png`),转换为: + ``` + https://raw.githubusercontent.com/[owner]/[repo]/[default-branch]/docs/architecture.png + ``` + +2. 对于已使用 `https://github.com/.../raw/...` 的链接,保持不变 + +3. 对于外部图片(如 imgur、cloudinary 等),保持原链接 + +4. **特殊处理**: + - 如果是 GitHub Issues/Comments 中的图片,通常在 `https://user-images.githubusercontent.com/` + - 如果是 docs 网站链接(如 `https://project.dev/images/...`),保持原链接 + +#### 4.2 图片选择优先级 + +**必须包含的图片类型**(按优先级): +1. **系统架构图**:展示技术栈、组件关系 +2. **功能演示 GIF**:展示核心工作流程 +3. **界面截图**:展示 UI/UX +4. **安装/配置截图**:帮助用户快速上手 +5. **数据流程图**:展示数据流向 +6. **部署架构图**:展示部署方案 + +**选择性包含**: +- Logo(可在顶部添加一次) +- 团队照片(非必需) +- 会议照片(非必需) + +**限制条件**: +- 最多包含 **15 张图片**(避免内容过于冗长) +- 优先选择高质量、信息量大的图片 +- 如果图片过大(>2MB),建议使用缩略图或描述替代 + +#### 4.3 图片描述规范 + +每个图片后应添加简短说明: + +```markdown +![架构图](图片URL) + +> 📐 **架构说明**:本项目采用微服务架构,包含 API Gateway、服务注册中心、3个核心微服务,使用 Redis 作为缓存,MySQL 作为持久化存储。 +``` + +特殊说明标签: +- `📐 架构说明` - 架构图 +- `💡 效果说明` - 功能演示 +- `⚙️ 配置说明` - 配置截图 +- `🎯 使用说明` - 操作演示 + +--- + +### Step 5: 提取标签 (6-20个) + +优先级顺序: +1. **核心技术**:LLM、Multi-Agent、Computer Vision、RAG +2. **编程语言**:Python、TypeScript、Rust +3. **框架/库**:React、PyTorch、LangChain +4. **应用领域**:NLP、Chatbot、Automation、DevOps +5. **公司/组织**:Microsoft、OpenAI、Meta + +--- + +### Step 6: 构造链接数组 ⚠️ 严格枚举值 + +**link.type 必须严格使用以下 4 种枚举值之一(全大写)**: + +| type 值 | 适用场景 | 示例 | +|---------|----------|------| +| `GITHUB` | GitHub 仓库地址 | `https://github.com/xxx/xxx` | +| `WEBSITE` | 官方文档/官网/博客/PyPI/npm | `https://example.com/docs` | +| `HUGGINGFACE` | Hugging Face 模型页 | `https://huggingface.co/xxx` | +| `PAPER` | 论文/Arxiv 链接 | `https://arxiv.org/abs/xxx` | + +--- + +### Step 7: 计算质量评分 + +```javascript +score = 0 +if (description.length >= 20 && description.length <= 200) score += 10 +if (content.length >= 1000) score += 15 +if (content.includes("# 如何使用") || content.includes("# How to Use")) score += 15 +if (content.includes("```")) score += 10 +if (content.includes("![") && content.match(/!\[.*\]\(.*\)/g).length >= 3) score += 15 // 🆕 包含3+图片 +if (content.includes("# 技术架构") || content.includes("# Technical Architecture")) score += 10 // 🆕 有架构说明 +if (stars >= 1000) score += 20 +else if (stars >= 100) score += 10 +if (最近30天有更新) score += 20 +else if (最近180天有更新) score += 10 +if (有文档链接) score += 10 +if (forks >= 10) score += 10 +``` + +--- + +## 🔴 关键输出要求(必须严格遵守) + +**⚠️ 直接返回纯 JSON 对象,严禁使用以下格式:** + +- ❌ **不要使用代码块标记**:禁止使用 ` ```json ` 或 ` ``` ` 包裹输出 +- ❌ **不要添加额外包装层**:禁止添加 `"output"`、`"data"` 等外层字段 +- ❌ **不要添加注释或解释**:禁止在 JSON 外添加任何文字说明 + +**✅ 正确的输出格式示例:** +```json +{"success": true, "project": {...}, "qualityScore": 95, "qualityPassed": true, "metadata": {...}} +``` + +**❌ 错误的输出格式示例:** +``` +```json +{ + "output": { + "success": true, + "project": {...} + } +} +``` +``` + +**检查方法**: +- 输出必须以 `{` 开头,以 `}` 结尾 +- 第一层必须直接包含 `success`、`project`、`qualityScore` 等字段 +- 不包含任何 Markdown 代码块标记 + +--- + +## ⚠️ 输出格式要求 + +**直接返回纯 JSON,不要使用代码块标记**: + +```json +{ + "success": true, + "project": { + "name": "项目名称", + "nameEn": "Project Name", + "description": "一句话描述,10-500字符", + "descriptionEn": "One sentence description, 10-500 chars", + "content": "# 项目用途\n\n完整Markdown内容,**包含图片链接**、架构图、使用示例等...", + "contentEn": "# Overview\n\nFull Markdown content with **image links**, architecture diagrams, usage examples...", + "status": "ACTIVE", + "source": "N8N_WORKFLOW", + "tags": [ + { "name": "核心技术", "nameEn": "Core Tech" } + ], + "links": [ + { "type": "GITHUB", "url": "...", "title": "GitHub 仓库" } + ] + }, + "qualityScore": 95, + "qualityPassed": true, + "metadata": { + "stars": 数量, + "forks": 数量, + "language": "主要语言", + "lastUpdate": "YYYY-MM-DD", + "analyzedAt": "ISO 8601格式", + "imageCount": 8 // 🆕 提取的图片数量 + } +} +``` + +--- + +## 📋 质量检查清单(生成前自查) + +### 基础要求 +- ✅ description 长度在 10-500 字符之间 +- ✅ content 包含完整的 8 个部分(项目用途、适用场景、核心功能、技术架构、如何使用、快速示例、实际效果展示、定价/成本、常见问题) +- ✅ content 包含至少一个代码示例(从 README 提取) +- ✅ content 包含至少 3 张图片(架构图、功能截图、演示 GIF 等) +- ✅ 图片 URL 已转换为可直接访问的绝对路径 +- ✅ 每张图片后有简短说明(使用 > 引用格式) +- ✅ tags 数量在 6-20 个之间 +- ✅ links 包含至少 GITHUB 类型链接 +- ✅ 所有枚举值使用全大写(ACTIVE、GITHUB、WEBSITE 等) +- ✅ 中英文内容结构一致 +- ✅ 没有使用 ```json 代码块包裹输出 + +### 图片质量检查 +- ✅ 架构图包含说明文字,解释主要组件和关系 +- ✅ 代码示例后有运行结果截图(如果有) +- ✅ "如何使用"部分的关键步骤有截图辅助说明 +- ✅ 所有图片链接可直接访问(非相对路径) +- ✅ 图片数量控制在 15 张以内,选择最具代表性的 + +--- + +## 🎯 最佳实践示例 + +### 好的架构图插入示例: + +```markdown +# 技术架构 + +LangChain.js 基于 TypeScript 重新实现,采用模块化设计: + +- **TypeScript + ESM**: 原生支持类型推断和 tree-shaking +- **模块化架构**: 核心 @langchain/core 与集成包分离,减小包体积 +- **Web-first**: 专为浏览器和 Edge Runtime 优化 + +![LangChain.js 架构图](https://raw.githubusercontent.com/langchain-ai/langchainjs/main/docs/static/img/architecture.png) + +> 📐 **架构说明**:左侧为 LangChain Core 核心模块(包含 Chains、Prompts、Models 等基础抽象),右侧为集成包(支持 OpenAI、Anthropic、向量数据库等)。底层统一使用 @langchain/core 的标准接口,上层应用可灵活组合不同集成。 +``` + +### 好的功能展示示例: + +```markdown +# 实际效果展示 + +通过对话式接口创建 Multi-Agent 系统: + +![Multi-Agent 创建流程](https://github.com/microsoft/autogen/raw/main/website/static/dev/chat-creation-demo.gif) + +> 💡 **效果说明**:用户输入"创建一个多智能体系统用于代码审查",Agent 会自动: +> 1. 创建 Assistant Agent(负责代码分析) +> 2. 创建 User Proxy Agent(负责执行代码) +> 3. 配置两人之间的对话模式 +> 4. 自动生成初始提示词 + +以下是一个真实的对话示例: + +![对话示例](https://raw.githubusercontent.com/microsoft/autogen/main/docs/images/chat-example.png) +``` + +--- + +## ⚠️ 最终检查清单(输出前必须确认) + +在返回结果前,请确认: +- [ ] 输出以 `{` 开头,以 `}` 结尾 +- [ ] 没有任何 Markdown 代码块标记(```json 或 ```) +- [ ] 第一层直接包含 `success` 字段(没有 `output` 包装) +- [ ] 没有在 JSON 外添加任何文字说明 +- [ ] 图片是项目介绍的重要组成部分,已妥善处理 + +**🔴 最后提醒:直接输出纯 JSON 对象,不要用代码块包裹,不要添加包装层!** diff --git a/package.json b/package.json index dfbdf32..56f05fc 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", "eslint": "^9", - "eslint-config-next": "15.1.6", + "eslint-config-next": "15.1.11", "eslint-config-prettier": "^9.1.0", "postcss": "^8", "prettier": "^3.4.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 376fe8b..6533e1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,8 +97,8 @@ importers: specifier: ^9 version: 9.39.2(jiti@1.21.7) eslint-config-next: - specifier: 15.1.6 - version: 15.1.6(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + specifier: 15.1.11 + version: 15.1.11(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint-config-prettier: specifier: ^9.1.0 version: 9.1.2(eslint@9.39.2(jiti@1.21.7)) @@ -591,8 +591,8 @@ packages: '@next/env@15.1.11': resolution: {integrity: sha512-yp++FVldfLglEG5LoS2rXhGypPyoSOyY0kxZQJ2vnlYJeP8o318t5DrDu5Tqzr03qAhDWllAID/kOCsXNLcwKw==} - '@next/eslint-plugin-next@15.1.6': - resolution: {integrity: sha512-+slMxhTgILUntZDGNgsKEYHUvpn72WP1YTlkmEhS51vnVd7S9jEEy0n9YAMcI21vUG4akTw9voWH02lrClt/yw==} + '@next/eslint-plugin-next@15.1.11': + resolution: {integrity: sha512-jpAu+46v5FF/TO8YUdOBHn/Wr4SCiU4IgjQ45S9Nn3vR4nZVS2SR+m9lpxcCv/xqMUoYuYFQZUP0H/ptw0W6+w==} '@next/swc-darwin-arm64@15.1.9': resolution: {integrity: sha512-sQF6MfW4nk0PwMYYq8xNgqyxZJGIJV16QqNDgaZ5ze9YoVzm4/YNx17X0exZudayjL9PF0/5RGffDtzXapch0Q==} @@ -2086,8 +2086,8 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - eslint-config-next@15.1.6: - resolution: {integrity: sha512-Wd1uy6y7nBbXUSg9QAuQ+xYEKli5CgUhLjz1QHW11jLDis5vK5XB3PemL6jEmy7HrdhaRFDz+GTZ/3FoH+EUjg==} + eslint-config-next@15.1.11: + resolution: {integrity: sha512-RK5q3f8CKMTwNXULOqd2TAsz+7kA5+5fy5YK7T6SeczLFOuOUcuJOGlYUbyoeU6+UKQrpFsYgCz71hI1F9q5Cg==} peerDependencies: eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 typescript: '>=3.3.1' @@ -4200,7 +4200,7 @@ snapshots: '@next/env@15.1.11': {} - '@next/eslint-plugin-next@15.1.6': + '@next/eslint-plugin-next@15.1.11': dependencies: fast-glob: 3.3.1 @@ -5665,9 +5665,9 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-next@15.1.6(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): + eslint-config-next@15.1.11(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@next/eslint-plugin-next': 15.1.6 + '@next/eslint-plugin-next': 15.1.11 '@rushstack/eslint-patch': 1.15.0 '@typescript-eslint/eslint-plugin': 8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/parser': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) diff --git a/src/app/api/projects/[slug]/route.ts b/src/app/api/projects/[slug]/route.ts new file mode 100644 index 0000000..353f598 --- /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: Promise<{ slug: string }> } +) { + try { + const { slug } = await params + + // 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: Promise<{ slug: string }> } +) { + try { + const { slug } = await params + + 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/MarkdownContent.tsx b/src/components/project/MarkdownContent.tsx index 0484410..cfe0b56 100644 --- a/src/components/project/MarkdownContent.tsx +++ b/src/components/project/MarkdownContent.tsx @@ -213,15 +213,19 @@ const components: Components = { ), - // Images + // Images - wrapped in container for size control img: ({ src, alt, ...props }) => ( - {alt} +
+
+ {alt} +
+
), // Horizontal rule diff --git a/src/components/project/ProjectDetail.tsx b/src/components/project/ProjectDetail.tsx index 6624e1d..b7a6105 100644 --- a/src/components/project/ProjectDetail.tsx +++ b/src/components/project/ProjectDetail.tsx @@ -48,13 +48,6 @@ function formatDate(date: Date | string, locale: string): string { } } -// Helper function to check if URL is an image -function isImageUrl(url: string): boolean { - const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp'] - const lowerUrl = url.toLowerCase() - return imageExtensions.some(ext => lowerUrl.includes(ext)) -} - export async function ProjectDetail({ project, locale }: ProjectDetailProps) { const t = await getTranslations('project') @@ -114,26 +107,6 @@ export async function ProjectDetail({ project, locale }: ProjectDetailProps) { ))} - - {/* Featured Image/Video Placeholder - only show if source exists */} - {project.source && ( -
- {isImageUrl(project.source) ? ( - {`${displayName} - ) : ( -