chore: 初始化 Agent Park v2 项目
- 更新项目章程,从模板更新为完整版本,包含 TypeScript 严格模式、组件优先架构和 TDD 原则 - 添加项目配置文件(.claude/settings.json、.gitignore、CLAUDE.md) - 添加完整的 specs 目录,包含需求、契约和文档 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# 规范质量检查清单: Agent Park - AI项目导航网站
|
||||
|
||||
**目的**: 在继续规划之前验证规范的完整性和质量
|
||||
**创建时间**: 2025-12-25
|
||||
**功能**: [spec.md](../spec.md)
|
||||
|
||||
## 内容质量
|
||||
|
||||
- [x] 无实现细节(语言、框架、API)
|
||||
- [x] 专注于用户价值和业务需求
|
||||
- [x] 为非技术利益相关者编写
|
||||
- [x] 所有必需章节已完成
|
||||
|
||||
## 需求完整性
|
||||
|
||||
- [x] 没有 [NEEDS CLARIFICATION] 标记剩余
|
||||
- [x] 需求是可测试且明确的
|
||||
- [x] 成功标准是可衡量的
|
||||
- [x] 成功标准是技术无关的(无实现细节)
|
||||
- [x] 所有验收场景已定义
|
||||
- [x] 边缘情况已识别
|
||||
- [x] 范围明确界定
|
||||
- [x] 依赖关系和假设已识别
|
||||
|
||||
## 功能准备就绪
|
||||
|
||||
- [x] 所有功能需求都有明确的验收标准
|
||||
- [x] 用户场景覆盖主要流程
|
||||
- [x] 功能满足成功标准中定义的可衡量结果
|
||||
- [x] 没有实现细节泄漏到规范中
|
||||
|
||||
## 备注
|
||||
|
||||
- 标记为不完整的项目需要在 `/speckit.clarify` 或 `/speckit.plan` 之前更新规范
|
||||
- 所有检查项目均已通过验证,规范质量符合要求
|
||||
- 规范已准备好进入下一阶段(规划或澄清)
|
||||
@@ -0,0 +1,456 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Agent Park Webhook API
|
||||
description: |
|
||||
API 接口用于接收 n8n 工作流程推送的 AI 项目数据更新。
|
||||
|
||||
**认证方式**: 请求头中携带 `X-API-Key` 进行身份验证。
|
||||
|
||||
**数据处理模式**: 部分成功模式 - 部分项目验证失败时,有效项目正常入库,失败项目记录错误信息。
|
||||
|
||||
**响应格式**: JSON
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Agent Park Team
|
||||
email: support@agent-park.example
|
||||
|
||||
servers:
|
||||
- url: http://localhost:3000
|
||||
description: 本地开发环境
|
||||
- url: https://agent-park.example.com
|
||||
description: 生产环境
|
||||
|
||||
tags:
|
||||
- name: webhook
|
||||
description: Webhook 接口
|
||||
- name: projects
|
||||
description: 项目数据管理
|
||||
|
||||
paths:
|
||||
/api/webhook/projects:
|
||||
post:
|
||||
tags:
|
||||
- webhook
|
||||
- projects
|
||||
summary: 接收 n8n 推送的项目数据
|
||||
description: |
|
||||
接收 n8n 工作流程推送的 AI 项目数据,验证后批量更新到数据库。
|
||||
|
||||
**认证**: 请求头中必须包含有效的 `X-API-Key`
|
||||
|
||||
**处理流程**:
|
||||
1. 验证 API Key
|
||||
2. 验证请求数据格式
|
||||
3. 逐个验证项目数据
|
||||
4. 有效项目入库(创建或更新)
|
||||
5. 返回处理结果
|
||||
|
||||
**部分成功模式**: 即使部分项目验证失败,也会继续处理其他项目。
|
||||
operationId: upsertProjects
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/WebhookPayload'
|
||||
examples:
|
||||
success_single:
|
||||
summary: 单个项目示例
|
||||
value:
|
||||
apiKey: "sk_live_1234567890abcdefghijklmnopqrstuvwxyz"
|
||||
projects:
|
||||
- name: "Claude"
|
||||
nameEn: "Claude"
|
||||
slug: "claude"
|
||||
description: "Anthropic 开发的 AI 助手"
|
||||
descriptionEn: "AI assistant by Anthropic"
|
||||
content: "Claude 的详细介绍..."
|
||||
contentEn: "Detailed description of Claude..."
|
||||
status: "ACTIVE"
|
||||
source: "n8n-daily-job"
|
||||
tags:
|
||||
- name: "代码助手"
|
||||
nameEn: "Code Assistant"
|
||||
links:
|
||||
- type: "WEBSITE"
|
||||
url: "https://www.anthropic.com/claude"
|
||||
title: "Official Website"
|
||||
- type: "GITHUB"
|
||||
url: "https://github.com/anthropics"
|
||||
success_batch:
|
||||
summary: 批量项目示例
|
||||
value:
|
||||
apiKey: "sk_live_1234567890abcdefghijklmnopqrstuvwxyz"
|
||||
projects:
|
||||
- name: "GPT-4"
|
||||
slug: "gpt-4"
|
||||
description: "OpenAI 的大型语言模型"
|
||||
status: "ACTIVE"
|
||||
source: "n8n-daily-job"
|
||||
tags:
|
||||
- name: "语言模型"
|
||||
- name: "对话AI"
|
||||
links:
|
||||
- type: "WEBSITE"
|
||||
url: "https://openai.com/gpt-4"
|
||||
- name: "Midjourney"
|
||||
slug: "midjourney"
|
||||
description: "AI 图像生成工具"
|
||||
status: "ACTIVE"
|
||||
source: "n8n-daily-job"
|
||||
tags:
|
||||
- name: "图像生成"
|
||||
links:
|
||||
- type: "WEBSITE"
|
||||
url: "https://www.midjourney.com"
|
||||
partial_failure:
|
||||
summary: 部分失败示例
|
||||
value:
|
||||
apiKey: "sk_live_1234567890abcdefghijklmnopqrstuvwxyz"
|
||||
projects:
|
||||
- name: "Valid Project"
|
||||
slug: "valid-project"
|
||||
description: "这是一个有效的项目"
|
||||
tags:
|
||||
- name: "测试"
|
||||
links:
|
||||
- type: "WEBSITE"
|
||||
url: "https://example.com"
|
||||
- name: "" # 无效:名称为空
|
||||
slug: "invalid-project"
|
||||
description: "x"
|
||||
tags: []
|
||||
links: []
|
||||
responses:
|
||||
'200':
|
||||
description: 处理完成(可能包含部分失败)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/WebhookResponse'
|
||||
examples:
|
||||
success_all:
|
||||
summary: 全部成功
|
||||
value:
|
||||
success: true
|
||||
processed: 3
|
||||
created: 2
|
||||
updated: 1
|
||||
failed: 0
|
||||
errors: []
|
||||
partial_success:
|
||||
summary: 部分成功
|
||||
value:
|
||||
success: true
|
||||
processed: 3
|
||||
created: 1
|
||||
updated: 1
|
||||
failed: 1
|
||||
errors:
|
||||
- index: 1
|
||||
field: "description"
|
||||
message: "Description too short (minimum 10 characters)"
|
||||
value: { "description": "x" }
|
||||
validation_error:
|
||||
summary: 验证错误(请求级别)
|
||||
value:
|
||||
success: false
|
||||
error: "Validation error"
|
||||
details:
|
||||
- "At least one project is required"
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
ApiKeyAuth:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: X-API-Key
|
||||
description: |
|
||||
API Key 认证。n8n 在请求头中携带预共享的密钥。
|
||||
|
||||
格式: `X-API-Key: sk_live_32_characters_minimum`
|
||||
|
||||
schemas:
|
||||
# ================================
|
||||
# Enums
|
||||
# ================================
|
||||
ProjectStatus:
|
||||
type: string
|
||||
enum: [ACTIVE, ARCHIVED]
|
||||
description: 项目状态
|
||||
x-enum-descriptions:
|
||||
ACTIVE: 活跃项目
|
||||
ARCHIVED: 已归档项目
|
||||
|
||||
LinkType:
|
||||
type: string
|
||||
enum: [WEBSITE, GITHUB, HUGGINGFACE, PAPER]
|
||||
description: 链接类型
|
||||
x-enum-descriptions:
|
||||
WEBSITE: 官方网站
|
||||
GITHUB: GitHub 仓库
|
||||
HUGGINGFACE: HuggingFace 模型/数据集
|
||||
PAPER: 学术论文
|
||||
|
||||
# ================================
|
||||
# Domain Models
|
||||
# ================================
|
||||
ExternalLink:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- url
|
||||
properties:
|
||||
type:
|
||||
$ref: '#/components/schemas/LinkType'
|
||||
url:
|
||||
type: string
|
||||
format: uri
|
||||
description: 链接地址
|
||||
example: "https://github.com/example/project"
|
||||
title:
|
||||
type: string
|
||||
maxLength: 200
|
||||
description: 链接标题(可选)
|
||||
example: "Source Code"
|
||||
|
||||
Tag:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 50
|
||||
description: 标签名称(中文)
|
||||
example: "图像生成"
|
||||
nameEn:
|
||||
type: string
|
||||
maxLength: 50
|
||||
description: 标签名称(英文,可选)
|
||||
example: "Image Generation"
|
||||
|
||||
ProjectInput:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- slug
|
||||
- description
|
||||
- tags
|
||||
- links
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 200
|
||||
description: 项目名称(中文)
|
||||
example: "Claude"
|
||||
nameEn:
|
||||
type: string
|
||||
maxLength: 200
|
||||
description: 项目名称(英文,可选)
|
||||
example: "Claude"
|
||||
slug:
|
||||
type: string
|
||||
pattern: '^[a-z0-9-]+$'
|
||||
description: URL 友好标识符(唯一)
|
||||
example: "claude"
|
||||
description:
|
||||
type: string
|
||||
minLength: 10
|
||||
maxLength: 500
|
||||
description: 项目简介(中文)
|
||||
example: "Anthropic 开发的 AI 助手,擅长分析、写作和编程任务"
|
||||
descriptionEn:
|
||||
type: string
|
||||
maxLength: 500
|
||||
description: 项目简介(英文,可选)
|
||||
example: "AI assistant by Anthropic, excels at analysis, writing, and coding"
|
||||
content:
|
||||
type: string
|
||||
maxLength: 10000
|
||||
description: 详细介绍(中文,可选)
|
||||
example: "Claude 是由 Anthropic 开发的下一代 AI 助手..."
|
||||
contentEn:
|
||||
type: string
|
||||
maxLength: 10000
|
||||
description: 详细介绍(英文,可选)
|
||||
example: "Claude is a next-generation AI assistant..."
|
||||
status:
|
||||
$ref: '#/components/schemas/ProjectStatus'
|
||||
description: 项目状态(默认 ACTIVE)
|
||||
source:
|
||||
type: string
|
||||
maxLength: 100
|
||||
description: 数据来源标识(如 n8n 流程名称)
|
||||
example: "n8n-daily-job"
|
||||
tags:
|
||||
type: array
|
||||
minItems: 1
|
||||
maxItems: 10
|
||||
items:
|
||||
$ref: '#/components/schemas/Tag'
|
||||
description: 项目标签(至少一个)
|
||||
links:
|
||||
type: array
|
||||
minItems: 1
|
||||
maxItems: 10
|
||||
items:
|
||||
$ref: '#/components/schemas/ExternalLink'
|
||||
description: 外部链接(至少一个)
|
||||
|
||||
# ================================
|
||||
# Request/Response Models
|
||||
# ================================
|
||||
WebhookPayload:
|
||||
type: object
|
||||
required:
|
||||
- apiKey
|
||||
- projects
|
||||
properties:
|
||||
apiKey:
|
||||
type: string
|
||||
minLength: 32
|
||||
description: API 密钥(用于认证)
|
||||
pattern: '^[a-zA-Z0-9_]{32,}$'
|
||||
example: "sk_live_1234567890abcdefghijklmnopqrstuvwxyz"
|
||||
projects:
|
||||
type: array
|
||||
minItems: 1
|
||||
maxItems: 100
|
||||
items:
|
||||
$ref: '#/components/schemas/ProjectInput'
|
||||
description: 要创建/更新的项目列表
|
||||
|
||||
WebhookResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
description: 请求是否成功(部分失败仍返回 true)
|
||||
processed:
|
||||
type: integer
|
||||
description: 处理的项目总数
|
||||
created:
|
||||
type: integer
|
||||
description: 新创建的项目数
|
||||
updated:
|
||||
type: integer
|
||||
description: 更新的项目数
|
||||
failed:
|
||||
type: integer
|
||||
description: 失败的项目数
|
||||
errors:
|
||||
type: array
|
||||
description: 失败项目的错误详情
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
index:
|
||||
type: integer
|
||||
description: 失败项目在请求中的索引(从 0 开始)
|
||||
field:
|
||||
type: string
|
||||
description: 验证失败的字段名
|
||||
message:
|
||||
type: string
|
||||
description: 错误消息
|
||||
value:
|
||||
type: object
|
||||
description: 导致错误的值(调试用)
|
||||
|
||||
ErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: false
|
||||
error:
|
||||
type: string
|
||||
description: 错误类型
|
||||
details:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: 详细错误信息
|
||||
|
||||
# ================================
|
||||
# Common Responses
|
||||
# ================================
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: 未授权 - API Key 无效或缺失
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
success: false
|
||||
error: "Unauthorized"
|
||||
details: ["Invalid or missing API Key"]
|
||||
|
||||
BadRequest:
|
||||
description: 请求格式错误或验证失败
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
validation_error:
|
||||
summary: 验证错误
|
||||
value:
|
||||
success: false
|
||||
error: "Validation error"
|
||||
details:
|
||||
- "At least one project is required"
|
||||
invalid_json:
|
||||
summary: JSON 格式错误
|
||||
value:
|
||||
success: false
|
||||
error: "Invalid JSON"
|
||||
details:
|
||||
- "Unexpected end of JSON input"
|
||||
|
||||
InternalServerError:
|
||||
description: 服务器内部错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
success: false
|
||||
error: "Internal server error"
|
||||
details:
|
||||
- "Database connection failed"
|
||||
|
||||
# ================================
|
||||
# Examples
|
||||
# ================================
|
||||
x-webhook-examples:
|
||||
n8n-configuration:
|
||||
description: n8n HTTP Request 节点配置示例
|
||||
method: POST
|
||||
url: "https://agent-park.example.com/api/webhook/projects"
|
||||
headers:
|
||||
X-API-Key: "sk_live_32_characters_minimum"
|
||||
Content-Type: "application/json"
|
||||
body:
|
||||
apiKey: "{{$env.WEBHOOK_API_KEY}}"
|
||||
projects:
|
||||
- name: "{{$json.projectName}}"
|
||||
slug: "{{$json.projectSlug}}"
|
||||
description: "{{$json.description}}"
|
||||
tags:
|
||||
- name: "{{$json.tag}}"
|
||||
links:
|
||||
- type: "WEBSITE"
|
||||
url: "{{$json.website}}"
|
||||
@@ -0,0 +1,524 @@
|
||||
# 数据模型设计: Agent Park - AI项目导航网站
|
||||
|
||||
**功能分支**: `001-ai-project-navigator`
|
||||
**创建时间**: 2025-12-25
|
||||
**状态**: 完成
|
||||
|
||||
## 概述
|
||||
|
||||
本文档定义了 Agent Park 项目的数据模型,包括实体定义、关系、验证规则和状态转换。数据模型使用 Prisma Schema 定义,存储在 PostgreSQL 数据库中。
|
||||
|
||||
---
|
||||
|
||||
## 实体关系图 (ERD)
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌──────────────────┐
|
||||
│ Project │<─────│ Tag │ │ ExternalLink │
|
||||
│ │ N:M │ │ 1:N │ │
|
||||
└─────────────┘ └─────────────┘ └──────────────────┘
|
||||
│ 1 │ N
|
||||
│ │
|
||||
└────────────────────────────────────────────┘
|
||||
1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实体定义
|
||||
|
||||
### 1. Project (AI项目)
|
||||
|
||||
代表一个AI工具、应用或研究项目。
|
||||
|
||||
#### 字段
|
||||
|
||||
| 字段名 | 类型 | 约束 | 默认值 | 描述 |
|
||||
|--------|------|------|--------|------|
|
||||
| `id` | String | Primary Key | cuid() | 主键 |
|
||||
| `name` | String | NOT NULL | - | 项目名称(中文) |
|
||||
| `nameEn` | String? | Nullable | - | 项目名称(英文) |
|
||||
| `slug` | String | UNIQUE, NOT NULL | - | URL 友好标识符 |
|
||||
| `description` | String | NOT NULL, Min(10) | - | 项目简介(中文) |
|
||||
| `descriptionEn` | String? | Nullable | - | 项目简介(英文) |
|
||||
| `content` | Text? | Nullable | - | 详细介绍(中文) |
|
||||
| `contentEn` | Text? | Nullable | - | 详细介绍(英文) |
|
||||
| `status` | Enum | NOT NULL | ACTIVE | 项目状态 |
|
||||
| `source` | String? | Nullable | - | 数据来源标识(如 n8n) |
|
||||
| `createdAt` | DateTime | NOT NULL | now() | 收录时间 |
|
||||
| `updatedAt` | DateTime | NOT NULL | now() | 更新时间 |
|
||||
|
||||
#### 状态枚举
|
||||
|
||||
```prisma
|
||||
enum ProjectStatus {
|
||||
ACTIVE # 活跃项目
|
||||
ARCHIVED # 已归档项目
|
||||
}
|
||||
```
|
||||
|
||||
#### 索引
|
||||
|
||||
- `idx_project_status_createdAt`: (status, createdAt) - 用于按状态和时间筛选
|
||||
- `idx_project_slug`: (slug) UNIQUE - 用于详情页路由
|
||||
|
||||
#### 关系
|
||||
|
||||
- `tags`: 与 Tag 的多对多关系
|
||||
- `links`: 与 ExternalLink 的一对多关系
|
||||
|
||||
---
|
||||
|
||||
### 2. Tag (标签)
|
||||
|
||||
代表AI项目的特征标记,如"图像生成"、"代码助手"等。
|
||||
|
||||
#### 字段
|
||||
|
||||
| 字段名 | 类型 | 约束 | 默认值 | 描述 |
|
||||
|--------|------|------|--------|------|
|
||||
| `id` | String | Primary Key | cuid() | 主键 |
|
||||
| `name` | String | UNIQUE, NOT NULL | - | 标签名称(中文) |
|
||||
| `nameEn` | String? | Nullable | - | 标签名称(英文) |
|
||||
| `slug` | String | UNIQUE, NOT NULL | - | URL 友好标识符 |
|
||||
| `createdAt` | DateTime | NOT NULL | now() | 创建时间 |
|
||||
|
||||
#### 索引
|
||||
|
||||
- `idx_tag_slug`: (slug) UNIQUE - 用于标签筛选页面
|
||||
|
||||
#### 关系
|
||||
|
||||
- `projects`: 与 Project 的多对多关系
|
||||
|
||||
---
|
||||
|
||||
### 3. ExternalLink (外部链接)
|
||||
|
||||
代表项目的外部来源链接,如官网、GitHub、HuggingFace、论文链接。
|
||||
|
||||
#### 字段
|
||||
|
||||
| 字段名 | 类型 | 约束 | 默认值 | 描述 |
|
||||
|--------|------|------|--------|------|
|
||||
| `id` | String | Primary Key | cuid() | 主键 |
|
||||
| `type` | Enum | NOT NULL | - | 链接类型 |
|
||||
| `url` | String | NOT NULL | - | 链接地址 |
|
||||
| `title` | String? | Nullable | - | 链接标题(可选) |
|
||||
| `projectId` | String | Foreign Key | - | 关联的项目ID |
|
||||
|
||||
#### 链接类型枚举
|
||||
|
||||
```prisma
|
||||
enum LinkType {
|
||||
WEBSITE # 官方网站
|
||||
GITHUB # GitHub 仓库
|
||||
HUGGINGFACE # HuggingFace 模型/数据集
|
||||
PAPER # 学术论文
|
||||
}
|
||||
```
|
||||
|
||||
#### 索引
|
||||
|
||||
- `idx_link_projectId`: (projectId) - 用于查询项目的外部链接
|
||||
- `idx_link_type`: (type) - 用于按类型筛选链接
|
||||
|
||||
#### 关系
|
||||
|
||||
- `project`: 与 Project 的多对一关系(级联删除)
|
||||
|
||||
---
|
||||
|
||||
## 关系定义
|
||||
|
||||
### Project ↔ Tag (多对多)
|
||||
|
||||
使用中间表 `_ProjectTags` 维护多对多关系。
|
||||
|
||||
```prisma
|
||||
model Project {
|
||||
// ... 其他字段
|
||||
tags Tag[]
|
||||
}
|
||||
|
||||
model Tag {
|
||||
// ... 其他字段
|
||||
projects Project[]
|
||||
}
|
||||
```
|
||||
|
||||
**级联规则**: 删除项目时不删除标签(标签可能被其他项目使用)
|
||||
|
||||
### Project ↔ ExternalLink (一对多)
|
||||
|
||||
一个项目可以有多个外部链接。
|
||||
|
||||
```prisma
|
||||
model Project {
|
||||
// ... 其他字段
|
||||
links ExternalLink[]
|
||||
}
|
||||
|
||||
model ExternalLink {
|
||||
id String @id @default(cuid())
|
||||
type LinkType
|
||||
url String
|
||||
title String?
|
||||
projectId String
|
||||
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
```
|
||||
|
||||
**级联规则**: 删除项目时级联删除所有关联的外部链接
|
||||
|
||||
---
|
||||
|
||||
## Prisma Schema 完整定义
|
||||
|
||||
```prisma
|
||||
// prisma/schema.prisma
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["postgresqlExtensions"]
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Enums
|
||||
// ================================
|
||||
|
||||
enum ProjectStatus {
|
||||
ACTIVE
|
||||
ARCHIVED
|
||||
}
|
||||
|
||||
enum LinkType {
|
||||
WEBSITE
|
||||
GITHUB
|
||||
HUGGINGFACE
|
||||
PAPER
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Models
|
||||
// ================================
|
||||
|
||||
model Project {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
nameEn String?
|
||||
slug String @unique
|
||||
description String
|
||||
descriptionEn String?
|
||||
content String? @db.Text
|
||||
contentEn String? @db.Text
|
||||
status ProjectStatus @default(ACTIVE)
|
||||
source String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
tags Tag[]
|
||||
links ExternalLink[]
|
||||
|
||||
// Indexes
|
||||
@@index([status, createdAt], map: "idx_project_status_createdAt")
|
||||
@@index([slug], map: "idx_project_slug")
|
||||
@@map("projects")
|
||||
}
|
||||
|
||||
model Tag {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
nameEn String?
|
||||
slug String @unique
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
projects Project[]
|
||||
|
||||
// Indexes
|
||||
@@index([slug], map: "idx_tag_slug")
|
||||
@@map("tags")
|
||||
}
|
||||
|
||||
model ExternalLink {
|
||||
id String @id @default(cuid())
|
||||
type LinkType
|
||||
url String
|
||||
title String?
|
||||
projectId String
|
||||
|
||||
// Relations
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
|
||||
// Indexes
|
||||
@@index([projectId], map: "idx_link_projectId")
|
||||
@@index([type], map: "idx_link_type")
|
||||
@@map("external_links")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Zod 验证 Schema
|
||||
|
||||
配合 Prisma 类型,使用 Zod 进行运行时数据验证。
|
||||
|
||||
```typescript
|
||||
// src/lib/validations.ts
|
||||
|
||||
import { z } from 'zod'
|
||||
|
||||
// ================================
|
||||
// Enums
|
||||
// ================================
|
||||
|
||||
export const ProjectStatusEnum = z.enum(['ACTIVE', 'ARCHIVED'])
|
||||
export const LinkTypeEnum = z.enum(['WEBSITE', 'GITHUB', 'HUGGINGFACE', 'PAPER'])
|
||||
|
||||
// ================================
|
||||
// Base Schemas
|
||||
// ================================
|
||||
|
||||
export const ExternalLinkSchema = z.object({
|
||||
type: LinkTypeEnum,
|
||||
url: z.string().url('Invalid URL format'),
|
||||
title: z.string().max(200).optional()
|
||||
})
|
||||
|
||||
export const TagSchema = z.object({
|
||||
name: z.string().min(1).max(50),
|
||||
nameEn: z.string().max(50).optional()
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Project Schemas
|
||||
// ================================
|
||||
|
||||
export const ProjectBaseSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
nameEn: z.string().max(200).optional(),
|
||||
description: z.string().min(10).max(500),
|
||||
descriptionEn: z.string().max(500).optional(),
|
||||
content: z.string().max(10000).optional(),
|
||||
contentEn: z.string().max(10000).optional(),
|
||||
status: ProjectStatusEnum.default('ACTIVE'),
|
||||
source: z.string().max(100).optional()
|
||||
})
|
||||
|
||||
export const ProjectInputSchema = ProjectBaseSchema.extend({
|
||||
tags: z.array(TagSchema).min(1, 'At least one tag is required').max(10),
|
||||
links: z.array(ExternalLinkSchema).min(1, 'At least one link is required').max(10)
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Webhook Schemas
|
||||
// ================================
|
||||
|
||||
export const WebhookAuthSchema = z.object({
|
||||
apiKey: z.string().min(32, 'Invalid API key format')
|
||||
})
|
||||
|
||||
export const WebhookPayloadSchema = WebhookAuthSchema.extend({
|
||||
projects: z.array(ProjectInputSchema).min(1).max(100)
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Query Schemas
|
||||
// ================================
|
||||
|
||||
export const ProjectQuerySchema = z.object({
|
||||
search: z.string().max(100).optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
status: ProjectStatusEnum.optional(),
|
||||
page: z.coerce.number().int().positive().default(1),
|
||||
limit: z.coerce.number().int().positive().max(100).default(20)
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Types
|
||||
// ================================
|
||||
|
||||
export type ExternalLink = z.infer<typeof ExternalLinkSchema>
|
||||
export type Tag = z.infer<typeof TagSchema>
|
||||
export type ProjectInput = z.infer<typeof ProjectInputSchema>
|
||||
export type WebhookPayload = z.infer<typeof WebhookPayloadSchema>
|
||||
export type ProjectQuery = z.infer<typeof ProjectQuerySchema>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据迁移策略
|
||||
|
||||
### 初始化迁移
|
||||
|
||||
```bash
|
||||
# 创建初始迁移
|
||||
npx prisma migrate dev --name init
|
||||
|
||||
# 生成 Prisma Client
|
||||
npx prisma generate
|
||||
|
||||
# 推送 schema 到数据库(开发环境)
|
||||
npx prisma db push
|
||||
```
|
||||
|
||||
### 生产环境部署
|
||||
|
||||
```bash
|
||||
# 应用待处理的迁移
|
||||
npx prisma migrate deploy
|
||||
|
||||
# 重置数据库(仅开发环境,慎用!)
|
||||
npx prisma migrate reset
|
||||
```
|
||||
|
||||
### 迁移命名约定
|
||||
|
||||
- `init`: 初始化 schema
|
||||
- `add_project_content`: 添加项目内容字段
|
||||
- `add_external_link_title`: 添加链接标题字段
|
||||
- `add_status_index`: 添加状态索引
|
||||
|
||||
---
|
||||
|
||||
## 种子数据
|
||||
|
||||
### 假数据示例 (20-30个项目)
|
||||
|
||||
```typescript
|
||||
// prisma/seed.ts
|
||||
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import { ProjectStatus, LinkType } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
async function main() {
|
||||
// 创建标签
|
||||
const tagImageGen = await prisma.tag.upsert({
|
||||
where: { slug: 'image-generation' },
|
||||
update: {},
|
||||
create: {
|
||||
name: '图像生成',
|
||||
nameEn: 'Image Generation',
|
||||
slug: 'image-generation'
|
||||
}
|
||||
})
|
||||
|
||||
const tagCodeAssistant = await prisma.tag.upsert({
|
||||
where: { slug: 'code-assistant' },
|
||||
update: {},
|
||||
create: {
|
||||
name: '代码助手',
|
||||
nameEn: 'Code Assistant',
|
||||
slug: 'code-assistant'
|
||||
}
|
||||
})
|
||||
|
||||
const tagDataAnalysis = await prisma.tag.upsert({
|
||||
where: { slug: 'data-analysis' },
|
||||
update: {},
|
||||
create: {
|
||||
name: '数据分析',
|
||||
nameEn: 'Data Analysis',
|
||||
slug: 'data-analysis'
|
||||
}
|
||||
})
|
||||
|
||||
// 创建项目
|
||||
await prisma.project.upsert({
|
||||
where: { slug: 'claude' },
|
||||
update: {},
|
||||
create: {
|
||||
name: 'Claude',
|
||||
nameEn: 'Claude',
|
||||
slug: 'claude',
|
||||
description: 'Anthropic 开发的 AI 助手,擅长分析、写作和编程任务。',
|
||||
descriptionEn: 'AI assistant by Anthropic, excels at analysis, writing, and coding tasks.',
|
||||
content: 'Claude 是由 Anthropic 开发的下一代 AI 助手。它基于 Constitutional AI 方法训练,强调安全性、诚实性和有用性。Claude 擅长长文本分析、创意写作、编程辅助等多种任务。',
|
||||
contentEn: 'Claude is a next-generation AI assistant developed by Anthropic. Trained using Constitutional AI methods, it emphasizes safety, honesty, and helpfulness. Claude excels at long-text analysis, creative writing, coding assistance, and more.',
|
||||
status: ProjectStatus.ACTIVE,
|
||||
source: 'manual',
|
||||
tags: {
|
||||
connect: [{ id: tagCodeAssistant.id }]
|
||||
},
|
||||
links: {
|
||||
create: [
|
||||
{
|
||||
type: LinkType.WEBSITE,
|
||||
url: 'https://www.anthropic.com/claude',
|
||||
title: 'Official Website'
|
||||
},
|
||||
{
|
||||
type: LinkType.GITHUB,
|
||||
url: 'https://github.com/anthropics/anthropic-sdk-python',
|
||||
title: 'Python SDK'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 更多项目...
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect()
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据完整性约束
|
||||
|
||||
### 字段级约束
|
||||
|
||||
| 表 | 字段 | 约束 |
|
||||
|---|------|------|
|
||||
| projects | name | NOT NULL, Max(200) |
|
||||
| projects | description | NOT NULL, Min(10), Max(500) |
|
||||
| projects | slug | UNIQUE, URL-friendly |
|
||||
| tags | name | UNIQUE, NOT NULL, Max(50) |
|
||||
| external_links | url | NOT NULL, Valid URL |
|
||||
|
||||
### 业务规则
|
||||
|
||||
1. **项目必填字段**: name、description、至少一个 tag、至少一个 link
|
||||
2. **标签唯一性**: 同名标签不能重复创建
|
||||
3. **链接类型限制**: 每个项目每种类型的链接最多 5 个
|
||||
4. **项目状态**: 新项目默认为 ACTIVE,仅可手动设置为 ARCHIVED
|
||||
5. **删除保护**: 删除项目时级联删除链接,但保留标签
|
||||
|
||||
---
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
1. **索引优化**: 为常用查询字段(status、slug、projectId)创建索引
|
||||
2. **查询优化**: 使用 Prisma 的 `select` 和 `include` 精确获取数据
|
||||
3. **分页查询**: 使用 `cursor` 或 `offset` 分页避免一次性加载大量数据
|
||||
4. **全文搜索**: 如需高级搜索,可使用 PostgreSQL 的全文搜索功能
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
数据模型设计遵循以下原则:
|
||||
|
||||
- **类型安全**: Prisma + Zod 提供端到端类型安全
|
||||
- **国际化支持**: 核心字段提供中英双语版本
|
||||
- **灵活性**: 标签系统而非固定分类,适应 AI 领域快速变化
|
||||
- **可扩展性**: 清晰的实体关系,便于后续功能扩展
|
||||
- **性能优先**: 合理的索引设计,优化查询性能
|
||||
@@ -0,0 +1,157 @@
|
||||
# 实施计划: Agent Park - AI项目导航网站
|
||||
|
||||
**分支**: `001-ai-project-navigator` | **日期**: 2025-12-25 | **规范**: [spec.md](./spec.md)
|
||||
**输入**: 来自 `/specs/001-ai-project-navigator/spec.md` 的功能规范
|
||||
|
||||
## 摘要
|
||||
|
||||
构建一个名为 Agent Park 的全网AI项目导航网站,采用 Anthropic Claude 风格设计。网站支持中英双语,使用 Next.js 14+ App Router、shadcn/ui 组件库、Prisma ORM + PostgreSQL 数据库、Tailwind CSS。用户可以浏览、搜索AI项目,查看项目详情和外部链接。前期使用20-30个假数据构建,后期通过 webhook 接口接收 n8n 工作流程推送的更新数据。
|
||||
|
||||
## 技术背景
|
||||
|
||||
**语言/版本**: TypeScript 5.0+ (严格模式)
|
||||
**主要依赖**:
|
||||
- Next.js 14+ (App Router)
|
||||
- shadcn/ui (组件库)
|
||||
- Prisma (ORM)
|
||||
- PostgreSQL (数据库)
|
||||
- Tailwind CSS (样式)
|
||||
- next-intl (国际化)
|
||||
- Zod (数据验证)
|
||||
|
||||
**存储**: PostgreSQL (通过 Prisma ORM)
|
||||
**测试**: Vitest + React Testing Library + Playwright
|
||||
**目标平台**: Web (响应式设计 - 桌面/平板/手机)
|
||||
**项目类型**: 全栈 Web 应用
|
||||
**性能目标**:
|
||||
- 首次内容绘制(FCP) < 1.8s
|
||||
- 最大内容绘制(LCP) < 2.5s
|
||||
- 首次字节时间(TTFB) < 800ms
|
||||
- 初始 JavaScript < 200KB gzipped
|
||||
|
||||
**约束条件**:
|
||||
- TypeScript 严格模式强制启用
|
||||
- 单元测试覆盖率 80%+
|
||||
- 所有代码必须通过 ESLint 检查
|
||||
- 禁止使用 @ts-ignore
|
||||
|
||||
**规模/范围**:
|
||||
- 前期: 20-30个项目数据
|
||||
- 页面: 首页、项目列表、项目详情页
|
||||
- API: 1个 webhook 接口 (POST /api/webhook/projects)
|
||||
|
||||
## 章程检查
|
||||
|
||||
*门控: 必须在阶段 0 研究前通过. 阶段 1 设计后重新检查. *
|
||||
|
||||
### 阶段 0 前检查
|
||||
|
||||
| 原则 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| I. TypeScript 严格模式与类型安全 | ✅ 通过 | 项目使用 TypeScript 5.0+ 严格模式,所有 API 使用 Zod 验证 |
|
||||
| II. 组件优先架构 | ✅ 通过 | 使用 Next.js App Router,优先 Server Components |
|
||||
| III. 测试驱动开发(不可协商) | ✅ 通过 | 规划使用 Vitest + React Testing Library + Playwright,目标 80%+ 覆盖率 |
|
||||
| IV. 性能优先 | ✅ 通过 | 定义了明确的 Core Web Vitals 目标,使用 ISR 和 SSG 优化 |
|
||||
| V. 用户体验一致性 | ✅ 通过 | 使用 shadcn/ui 统一设计系统,支持响应式和深色模式 |
|
||||
| VI. 代码质量与可维护性 | ✅ 通过 | 使用 ESLint + Prettier,遵循 TypeScript 最佳实践 |
|
||||
|
||||
### 阶段 1 后重新检查
|
||||
|
||||
| 原则 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| 所有阶段 0 门控项 | ✅ 保持 | 设计确认符合所有章程原则 |
|
||||
| 数据模型类型安全 | ✅ 通过 | Prisma 生成类型,配合 Zod 运行时验证 |
|
||||
| API 合同完整性 | ✅ 通过 | webhook 接口使用 Zod schema 验证输入 |
|
||||
|
||||
**结论**: 项目设计完全符合章程要求,无违规项。
|
||||
|
||||
## 项目结构
|
||||
|
||||
### 文档(此功能)
|
||||
|
||||
```
|
||||
specs/001-ai-project-navigator/
|
||||
├── plan.md # 此文件 (/speckit.plan 命令输出)
|
||||
├── research.md # 阶段 0 输出 - 技术选型研究
|
||||
├── data-model.md # 阶段 1 输出 - 数据模型设计
|
||||
├── quickstart.md # 阶段 1 输出 - 快速开始指南
|
||||
├── contracts/ # 阶段 1 输出 - API 合同
|
||||
│ └── webhook.yaml # OpenAPI 3.0 规范
|
||||
└── tasks.md # 阶段 2 输出 (/speckit.tasks 命令)
|
||||
```
|
||||
|
||||
### 源代码(仓库根目录)
|
||||
|
||||
```
|
||||
agent-park-v2/
|
||||
├── prisma/
|
||||
│ ├── schema.prisma # Prisma 数据模型定义
|
||||
│ └── seed.ts # 假数据种子脚本
|
||||
├── public/
|
||||
│ └── images/ # 静态图片资源
|
||||
├── src/
|
||||
│ ├── app/ # Next.js App Router 目录
|
||||
│ │ ├── [locale]/ # next-intl 国际化路由
|
||||
│ │ │ ├── layout.tsx # 根布局
|
||||
│ │ │ ├── page.tsx # 首页
|
||||
│ │ │ ├── projects/ # 项目列表页
|
||||
│ │ │ │ ├── page.tsx
|
||||
│ │ │ │ └── [id]/ # 项目详情页
|
||||
│ │ │ │ └── page.tsx
|
||||
│ │ │ └── api/ # API 路由
|
||||
│ │ │ └── webhook/
|
||||
│ │ │ └── projects/
|
||||
│ │ │ └── route.ts # Webhook 端点
|
||||
│ │ ├── globals.css # Tailwind 全局样式
|
||||
│ │ └── layout.tsx # 根布局 (国际化)
|
||||
│ ├── components/ # React 组件
|
||||
│ │ ├── ui/ # shadcn/ui 组件 (自动生成)
|
||||
│ │ ├── layout/ # 布局组件
|
||||
│ │ │ ├── Header.tsx
|
||||
│ │ │ ├── Footer.tsx
|
||||
│ │ │ └── Navigation.tsx
|
||||
│ │ ├── project/ # 项目相关组件
|
||||
│ │ │ ├── ProjectCard.tsx
|
||||
│ │ │ ├── ProjectList.tsx
|
||||
│ │ │ ├── ProjectDetail.tsx
|
||||
│ │ │ └── TagCloud.tsx
|
||||
│ │ └── search/ # 搜索组件
|
||||
│ │ ├── SearchBar.tsx
|
||||
│ │ └── SearchResults.tsx
|
||||
│ ├── lib/ # 工具库
|
||||
│ │ ├── prisma.ts # Prisma 客户端单例
|
||||
│ │ ├── utils.ts # 通用工具函数
|
||||
│ │ └── validations.ts # Zod 验证 schemas
|
||||
│ ├── hooks/ # 自定义 Hooks
|
||||
│ │ ├── useSearch.ts
|
||||
│ │ └── useProjects.ts
|
||||
│ ├── types/ # TypeScript 类型定义
|
||||
│ │ └── index.ts
|
||||
│ ├── messages/ # next-intl 翻译文件
|
||||
│ │ ├── en.json
|
||||
│ │ └── zh.json
|
||||
│ └── styles/ # 额外样式文件
|
||||
├── tests/ # 测试文件
|
||||
│ ├── unit/ # 单元测试
|
||||
│ ├── integration/ # 集成测试
|
||||
│ └── e2e/ # E2E 测试 (Playwright)
|
||||
├── next.config.js # Next.js 配置
|
||||
├── tailwind.config.js # Tailwind CSS 配置
|
||||
├── tsconfig.json # TypeScript 配置
|
||||
├── components.json # shadcn/ui 配置
|
||||
├── .env.example # 环境变量示例
|
||||
├── package.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
**结构决策**: 采用标准的 Next.js App Router 单体项目结构。所有源代码在 `src/` 目录下,使用 `app/` 目录进行路由。Prisma schema 在根目录 `prisma/` 文件夹。组件按功能分目录组织,shadcn/ui 组件放在 `components/ui/` 自动管理。
|
||||
|
||||
## 复杂度跟踪
|
||||
|
||||
*仅在章程检查有必须证明的违规时填写*
|
||||
|
||||
无违规项。项目设计完全符合章程要求。
|
||||
|
||||
| 违规 | 为什么需要 | 拒绝更简单替代方案的原因 |
|
||||
|-----------|------------|-------------------------------------|
|
||||
| - | - | - |
|
||||
@@ -0,0 +1,519 @@
|
||||
# 快速开始指南: Agent Park - AI项目导航网站
|
||||
|
||||
**功能分支**: `001-ai-project-navigator`
|
||||
**创建时间**: 2025-12-25
|
||||
**状态**: 完成
|
||||
|
||||
## 概述
|
||||
|
||||
本指南提供 Agent Park 项目的完整开发环境设置和快速启动步骤。
|
||||
|
||||
---
|
||||
|
||||
## 前置要求
|
||||
|
||||
### 必需软件
|
||||
|
||||
| 软件 | 版本要求 | 用途 |
|
||||
|------|----------|------|
|
||||
| Node.js | 18.17+ | 运行时环境 |
|
||||
| pnpm | 8.0+ | 包管理器 |
|
||||
| PostgreSQL | 14+ | 数据库 |
|
||||
| Git | 最新版 | 版本控制 |
|
||||
|
||||
### 可选软件
|
||||
|
||||
| 软件 | 用途 |
|
||||
|------|------|
|
||||
| Docker | 容器化数据库(开发环境) |
|
||||
| VS Code | 推荐的代码编辑器 |
|
||||
|
||||
### 检查安装
|
||||
|
||||
```bash
|
||||
# 检查 Node.js 版本
|
||||
node --version # 应该 >= v18.17.0
|
||||
|
||||
# 检查 pnpm 版本
|
||||
pnpm --version # 应该 >= 8.0.0
|
||||
|
||||
# 检查 PostgreSQL
|
||||
psql --version # 应该 >= 14.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 项目初始化
|
||||
|
||||
### 1. 创建 Next.js 项目
|
||||
|
||||
```bash
|
||||
# 使用 pnpm 创建项目(推荐使用 shadcn/ui init 自动配置)
|
||||
pnpm create next-app@latest agent-park-v2 --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
|
||||
|
||||
# 或使用 shadcn/ui 的 init 命令(更便捷)
|
||||
npx shadcn@latest init
|
||||
```
|
||||
|
||||
**交互式选项**:
|
||||
- TypeScript: Yes
|
||||
- ESLint: Yes
|
||||
- Tailwind CSS: Yes
|
||||
- `src/` directory: Yes
|
||||
- App Router: Yes
|
||||
- Import alias: `@/*`
|
||||
|
||||
### 2. 安装依赖
|
||||
|
||||
```bash
|
||||
cd agent-park-v2
|
||||
|
||||
# 核心依赖
|
||||
pnpm add next-intl zod clsx tailwind-merge
|
||||
|
||||
# Prisma
|
||||
pnpm add @prisma/client
|
||||
pnpm add -D prisma
|
||||
|
||||
# shadcn/ui(如未使用 init 命令)
|
||||
npx shadcn@latest init
|
||||
|
||||
# 开发依赖
|
||||
pnpm add -D @types/node @types/react @types/react-dom vitest @testing-library/react @testing-library/jest-dom @playwright/test
|
||||
```
|
||||
|
||||
### 3. 安装 shadcn/ui 组件
|
||||
|
||||
```bash
|
||||
# 安装常用组件
|
||||
npx shadcn@latest add button card input textarea badge
|
||||
npx shadcn@latest add skeleton separator dialog
|
||||
npx shadcn@latest add navigation-menu dropdown-menu
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据库设置
|
||||
|
||||
### 方案 A: 使用 Docker(推荐)
|
||||
|
||||
```bash
|
||||
# 启动 PostgreSQL 容器
|
||||
docker run --name agent-park-db \
|
||||
-e POSTGRES_USER=agentpark \
|
||||
-e POSTGRES_PASSWORD=agentpark \
|
||||
-e POSTGRES_DB=agent_park \
|
||||
-p 5432:5432 \
|
||||
-d postgres:16-alpine
|
||||
|
||||
# 等待数据库启动
|
||||
docker logs -f agent-park-db
|
||||
```
|
||||
|
||||
### 方案 B: 本地 PostgreSQL
|
||||
|
||||
```bash
|
||||
# 创建数据库
|
||||
createdb agent_park
|
||||
|
||||
# 或使用 psql
|
||||
psql -U postgres -c "CREATE DATABASE agent_park;"
|
||||
```
|
||||
|
||||
### 配置环境变量
|
||||
|
||||
```bash
|
||||
# 创建 .env 文件
|
||||
cp .env.example .env
|
||||
|
||||
# 编辑 .env
|
||||
DATABASE_URL="postgresql://agentpark:agentpark@localhost:5432/agent_park"
|
||||
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
|
||||
WEBHOOK_API_KEY="sk_live_$(openssl rand -base64 32 | tr -d '=+/' | cut -c1-32)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prisma 配置
|
||||
|
||||
### 1. 初始化 Prisma
|
||||
|
||||
```bash
|
||||
npx prisma init
|
||||
```
|
||||
|
||||
### 2. 编写 Schema
|
||||
|
||||
将以下内容写入 `prisma/schema.prisma`:
|
||||
|
||||
```prisma
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["postgresqlExtensions"]
|
||||
}
|
||||
|
||||
enum ProjectStatus {
|
||||
ACTIVE
|
||||
ARCHIVED
|
||||
}
|
||||
|
||||
enum LinkType {
|
||||
WEBSITE
|
||||
GITHUB
|
||||
HUGGINGFACE
|
||||
PAPER
|
||||
}
|
||||
|
||||
model Project {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
nameEn String?
|
||||
slug String @unique
|
||||
description String
|
||||
descriptionEn String?
|
||||
content String? @db.Text
|
||||
contentEn String? @db.Text
|
||||
status ProjectStatus @default(ACTIVE)
|
||||
source String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
tags Tag[]
|
||||
links ExternalLink[]
|
||||
|
||||
@@index([status, createdAt])
|
||||
@@index([slug])
|
||||
@@map("projects")
|
||||
}
|
||||
|
||||
model Tag {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
nameEn String?
|
||||
slug String @unique
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
projects Project[]
|
||||
|
||||
@@index([slug])
|
||||
@@map("tags")
|
||||
}
|
||||
|
||||
model ExternalLink {
|
||||
id String @id @default(cuid())
|
||||
type LinkType
|
||||
url String
|
||||
title String?
|
||||
projectId String
|
||||
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([projectId])
|
||||
@@index([type])
|
||||
@@map("external_links")
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 执行迁移
|
||||
|
||||
```bash
|
||||
# 创建初始迁移
|
||||
npx prisma migrate dev --name init
|
||||
|
||||
# 生成 Prisma Client
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
### 4. 加载种子数据
|
||||
|
||||
创建 `prisma/seed.ts`:
|
||||
|
||||
```typescript
|
||||
import { PrismaClient, ProjectStatus, LinkType } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
async function main() {
|
||||
// 创建标签
|
||||
const tags = await Promise.all([
|
||||
prisma.tag.upsert({
|
||||
where: { slug: 'code-assistant' },
|
||||
update: {},
|
||||
create: { name: '代码助手', nameEn: 'Code Assistant', slug: 'code-assistant' }
|
||||
}),
|
||||
prisma.tag.upsert({
|
||||
where: { slug: 'image-generation' },
|
||||
update: {},
|
||||
create: { name: '图像生成', nameEn: 'Image Generation', slug: 'image-generation' }
|
||||
})
|
||||
])
|
||||
|
||||
// 创建项目
|
||||
await prisma.project.upsert({
|
||||
where: { slug: 'claude' },
|
||||
update: {},
|
||||
create: {
|
||||
name: 'Claude',
|
||||
nameEn: 'Claude',
|
||||
slug: 'claude',
|
||||
description: 'Anthropic 开发的 AI 助手,擅长分析、写作和编程任务。',
|
||||
descriptionEn: 'AI assistant by Anthropic, excels at analysis, writing, and coding.',
|
||||
status: ProjectStatus.ACTIVE,
|
||||
tags: { connect: tags.map(t => ({ id: t.id })) },
|
||||
links: {
|
||||
create: [
|
||||
{ type: LinkType.WEBSITE, url: 'https://www.anthropic.com/claude', title: 'Official Website' },
|
||||
{ type: LinkType.GITHUB, url: 'https://github.com/anthropics', title: 'GitHub' }
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
console.log('Seed data loaded successfully!')
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect()
|
||||
})
|
||||
```
|
||||
|
||||
```bash
|
||||
# 运行种子脚本
|
||||
npx ts-node prisma/seed.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 国际化配置
|
||||
|
||||
### 1. 配置 next-intl
|
||||
|
||||
安装依赖:
|
||||
|
||||
```bash
|
||||
pnpm add next-intl
|
||||
```
|
||||
|
||||
### 2. 创建消息文件
|
||||
|
||||
```bash
|
||||
mkdir -p src/messages
|
||||
```
|
||||
|
||||
`src/messages/en.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"common": {
|
||||
"search": "Search",
|
||||
"loading": "Loading...",
|
||||
"noResults": "No results found"
|
||||
},
|
||||
"home": {
|
||||
"title": "AI Project Navigator",
|
||||
"subtitle": "Discover and explore AI projects"
|
||||
},
|
||||
"project": {
|
||||
"details": "Project Details",
|
||||
"externalLinks": "External Links",
|
||||
"viewProject": "View Project"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`src/messages/zh.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"common": {
|
||||
"search": "搜索",
|
||||
"loading": "加载中...",
|
||||
"noResults": "未找到结果"
|
||||
},
|
||||
"home": {
|
||||
"title": "AI 项目导航",
|
||||
"subtitle": "发现和探索 AI 项目"
|
||||
},
|
||||
"project": {
|
||||
"details": "项目详情",
|
||||
"externalLinks": "外部链接",
|
||||
"viewProject": "查看项目"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 配置 Next.js
|
||||
|
||||
更新 `next.config.js`:
|
||||
|
||||
```javascript
|
||||
const createNextIntlPlugin = require('next-intl/plugin')
|
||||
|
||||
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts')
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{ hostname: 'localhost' },
|
||||
{ hostname: '*.anthropic.com' }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = withNextIntl(nextConfig)
|
||||
```
|
||||
|
||||
创建 `src/i18n/request.ts`:
|
||||
|
||||
```typescript
|
||||
import { getRequestConfig } from 'next-intl/server'
|
||||
|
||||
export default getRequestConfig(async ({ locale }) => ({
|
||||
messages: (await import(`../messages/${locale}.json`)).default
|
||||
}))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 目录结构创建
|
||||
|
||||
```bash
|
||||
# 创建目录结构
|
||||
mkdir -p src/components/layout
|
||||
mkdir -p src/components/project
|
||||
mkdir -p src/components/search
|
||||
mkdir -p src/lib
|
||||
mkdir -p src/hooks
|
||||
mkdir -p src/types
|
||||
mkdir -p tests/unit
|
||||
mkdir -p tests/integration
|
||||
mkdir -p tests/e2e
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 启动开发服务器
|
||||
|
||||
```bash
|
||||
# 启动开发服务器
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
访问:
|
||||
- 英文: http://localhost:3000/en
|
||||
- 中文: http://localhost:3000/zh
|
||||
|
||||
---
|
||||
|
||||
## 验证设置
|
||||
|
||||
### 1. 检查 Prisma 连接
|
||||
|
||||
```bash
|
||||
npx prisma studio
|
||||
```
|
||||
|
||||
访问 http://localhost:5555 查看数据库数据。
|
||||
|
||||
### 2. 测试 API 端点
|
||||
|
||||
```bash
|
||||
# 测试 webhook(需要先创建 .env 中的 WEBHOOK_API_KEY)
|
||||
curl -X POST http://localhost:3000/api/webhook/projects \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: your-api-key" \
|
||||
-d '{
|
||||
"apiKey": "your-api-key",
|
||||
"projects": [{
|
||||
"name": "Test Project",
|
||||
"slug": "test-project",
|
||||
"description": "This is a test project for verification",
|
||||
"tags": [{"name": "测试"}],
|
||||
"links": [{"type": "WEBSITE", "url": "https://example.com"}]
|
||||
}]
|
||||
}'
|
||||
```
|
||||
|
||||
### 3. 运行测试
|
||||
|
||||
```bash
|
||||
# 单元测试
|
||||
pnpm test
|
||||
|
||||
# E2E 测试
|
||||
pnpm test:e2e
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: Prisma 迁移失败?
|
||||
|
||||
```bash
|
||||
# 重置数据库(开发环境)
|
||||
npx prisma migrate reset
|
||||
|
||||
# 或手动删除迁移历史
|
||||
rm -rf prisma/migrations
|
||||
npx prisma migrate dev --name init
|
||||
```
|
||||
|
||||
### Q: TypeScript 错误?
|
||||
|
||||
```bash
|
||||
# 重新生成 Prisma Client
|
||||
npx prisma generate
|
||||
|
||||
# 重启 TypeScript 服务器(VS Code)
|
||||
# Cmd+Shift+P -> "TypeScript: Restart TS Server"
|
||||
```
|
||||
|
||||
### Q: 端口被占用?
|
||||
|
||||
```bash
|
||||
# 查找占用 3000 端口的进程
|
||||
lsof -i :3000 # macOS/Linux
|
||||
netstat -ano | findstr :3000 # Windows
|
||||
|
||||
# 或使用不同端口
|
||||
PORT=3001 pnpm dev
|
||||
```
|
||||
|
||||
### Q: shadcn/ui 组件不工作?
|
||||
|
||||
```bash
|
||||
# 重新安装组件
|
||||
npx shadcn@latest add [component-name]
|
||||
|
||||
# 或手动检查 components.json 配置
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
设置完成后,继续以下步骤:
|
||||
|
||||
1. **创建基础布局**: Header、Footer、Navigation
|
||||
2. **实现项目列表页**: 支持搜索和标签筛选
|
||||
3. **实现项目详情页**: 显示完整信息和外部链接
|
||||
4. **实现 Webhook API**: 接收 n8n 数据推送
|
||||
5. **编写测试**: 单元测试和 E2E 测试
|
||||
6. **样式定制**: 调整 Tailwind 主题实现 Claude 风格
|
||||
|
||||
参考文档:
|
||||
- [data-model.md](./data-model.md) - 数据模型设计
|
||||
- [contracts/webhook.yaml](./contracts/webhook.yaml) - API 接口规范
|
||||
- [research.md](./research.md) - 技术选型研究
|
||||
@@ -0,0 +1,509 @@
|
||||
# 技术研究报告: Agent Park - AI项目导航网站
|
||||
|
||||
**功能分支**: `001-ai-project-navigator`
|
||||
**创建时间**: 2025-12-25
|
||||
**状态**: 完成
|
||||
|
||||
## 概述
|
||||
|
||||
本文档记录了 Agent Park 项目的技术选型决策和最佳实践研究。针对 Next.js 全栈应用、shadcn/ui 组件库、PostgreSQL + Prisma ORM、Tailwind CSS 和国际化支持等技术进行了深入研究。
|
||||
|
||||
---
|
||||
|
||||
## 1. Next.js 14+ App Router
|
||||
|
||||
### Decision: 选择 Next.js 14+ (App Router) 作为全栈框架
|
||||
|
||||
**Rationale**:
|
||||
- **Server Components 优先**: App Router 默认使用 Server Components,减少客户端 JavaScript 体积,提升性能
|
||||
- **内置 API Routes**: 通过 Route Handlers 可以轻松构建 webhook 接口,无需单独的后端服务器
|
||||
- **文件系统路由**: 直观的路由结构,便于维护和扩展
|
||||
- **优秀的 SEO 支持**: 服务端渲染确保搜索引擎可以正确索引内容
|
||||
- **Vercel 部署优化**: 原生支持 Vercel 平台,零配置部署
|
||||
|
||||
**Alternatives considered**:
|
||||
- **Remix**: 同样优秀的全栈框架,但社区和生态系统相对较小
|
||||
- **Nuxt.js (Vue)**: 团队更熟悉 React 生态系统
|
||||
- **SvelteKit**: 学习曲线较高,生态系统相对不成熟
|
||||
|
||||
### 最佳实践
|
||||
|
||||
1. **优先使用 Server Components**: 仅在需要交互性(状态、事件处理、浏览器 API)时添加 `'use client'` 指令
|
||||
2. **使用动态导入**: 对于客户端组件,使用 `dynamic()` 进行代码分割
|
||||
3. **利用并行路由**: 对于复杂布局,使用插槽和并行路由提升用户体验
|
||||
4. **使用 Server Actions**: 表单提交和状态变更优先使用 Server Actions 而非 API Routes
|
||||
5. **启用增量静态再生 (ISR)**: 对于项目列表页面,使用 ISR 减少 API 调用
|
||||
|
||||
### TypeScript 配置
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. shadcn/ui 组件库
|
||||
|
||||
### Decision: 选择 shadcn/ui 作为 UI 组件库
|
||||
|
||||
**Rationale**:
|
||||
- **代码而非 npm 包**: 组件代码直接复制到项目中,完全可控和可定制
|
||||
- **基于 Radix UI**: 无障碍访问性良好,符合 WAI-ARIA 标准
|
||||
- **Tailwind CSS 集成**: 完美配合 Tailwind CSS 进行样式定制
|
||||
- **类型安全**: 完整的 TypeScript 类型定义
|
||||
- **Anthropic Claude 风格**: 可以通过定制主题实现类似 Claude 的简洁优雅风格
|
||||
|
||||
**Alternatives considered**:
|
||||
- **Mantine**: 功能丰富但体积较大,定制性较差
|
||||
- **Chakra UI**: API 设计优秀,但性能不如 Radix UI
|
||||
- **Material-UI**: 风格过于固定,难以实现 Claude 风格
|
||||
|
||||
### 最佳实践
|
||||
|
||||
1. **使用 CLI 安装**: `npx shadcn@latest add [component]` 自动添加依赖和配置
|
||||
2. **自定义主题**: 通过 CSS 变量定制颜色、圆角、阴影等
|
||||
3. **复用组件模式**: 参考现有组件创建新的组合组件
|
||||
4. **保持组件更新**: 定期运行 `npx shadcn@latest diff` 检查组件更新
|
||||
|
||||
### Anthropic Claude 风格定制
|
||||
|
||||
```css
|
||||
/* globals.css - Claude 风格配色 */
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 5.9% 10%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Prisma ORM + PostgreSQL
|
||||
|
||||
### Decision: 选择 Prisma 作为 ORM,PostgreSQL 作为数据库
|
||||
|
||||
**Rationale**:
|
||||
- **类型安全**: Prisma 自动生成 TypeScript 类型,与 strict mode 完美配合
|
||||
- **声明式 Schema**: 直观的数据模型定义,支持关系和约束
|
||||
- **迁移管理**: 内置版本控制的迁移系统,便于团队协作
|
||||
- **开发体验**: Prisma Studio 提供可视化的数据库浏览和编辑
|
||||
- **PostgreSQL 优势**: 成熟稳定、支持全文搜索、JSON 数据类型、性能优秀
|
||||
|
||||
**Alternatives considered**:
|
||||
- **Drizzle ORM**: 性能更优但生态系统较新,迁移功能较弱
|
||||
- **TypeORM**: 体积较大,类型安全性不如 Prisma
|
||||
- **MongoDB + Mongoose**: 对于关系型数据不够自然
|
||||
|
||||
### 最佳实践
|
||||
|
||||
1. **使用 Prisma Accelerate**: 生产环境使用连接池提升性能
|
||||
2. **启用查询日志**: 开发环境启用 `log: ['query', 'info', 'warn', 'error']`
|
||||
3. **使用事务**: 对于多表操作使用 `$transaction` 确保数据一致性
|
||||
4. **批量操作**: 使用 `createMany` 和 `updateMany` 而非循环单条操作
|
||||
5. **选择特定字段**: 使用 `select` 减少数据传输量
|
||||
|
||||
### Schema 设计模式
|
||||
|
||||
```prisma
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
model Project {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
nameEn String?
|
||||
slug String @unique
|
||||
description String
|
||||
descriptionEn String?
|
||||
content String? @db.Text
|
||||
contentEn String? @db.Text
|
||||
status ProjectStatus @default(ACTIVE)
|
||||
source String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
tags Tag[]
|
||||
links ExternalLink[]
|
||||
|
||||
@@index([status, createdAt])
|
||||
@@index([slug])
|
||||
}
|
||||
|
||||
enum ProjectStatus {
|
||||
ACTIVE
|
||||
ARCHIVED
|
||||
}
|
||||
|
||||
model Tag {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
nameEn String?
|
||||
slug String @unique
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
projects Project[]
|
||||
|
||||
@@index([slug])
|
||||
}
|
||||
|
||||
model ExternalLink {
|
||||
id String @id @default(cuid())
|
||||
type LinkType
|
||||
url String
|
||||
title String?
|
||||
projectId String
|
||||
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([projectId])
|
||||
@@index([type])
|
||||
}
|
||||
|
||||
enum LinkType {
|
||||
WEBSITE
|
||||
GITHUB
|
||||
HUGGINGFACE
|
||||
PAPER
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Tailwind CSS
|
||||
|
||||
### Decision: 选择 Tailwind CSS 作为样式解决方案
|
||||
|
||||
**Rationale**:
|
||||
- **实用优先**: 快速构建 UI,无需切换上下文编写 CSS
|
||||
- **高度可定制**: 通过配置文件完全控制设计系统
|
||||
- **生产优化**: 自动清除未使用的样式,保持 CSS 体积最小
|
||||
- **响应式优先**: 移动优先的断点系统,适配各种设备
|
||||
- **与 shadcn/ui 完美集成**: 组件库基于 Tailwind CSS 构建
|
||||
|
||||
**Alternatives considered**:
|
||||
- **CSS Modules**: 组件隔离性好但维护成本高,不支持设计复用
|
||||
- **Styled Components**: 运行时注入样式,性能不如 Tailwind
|
||||
- **Emotion**: 类似 Styled Components,同样有性能问题
|
||||
|
||||
### 最佳实践
|
||||
|
||||
1. **使用组件变体**: 使用 `clsx` 或 `tailwind-merge` 处理条件类名
|
||||
2. **提取公共模式**: 将重复的类组合提取为组件或工具函数
|
||||
3. **使用 @apply 谨慎**: 仅在确实减少代码重复时使用
|
||||
4. **启用 JIT 模式**: 确保使用最新版本的 JIT 编译器
|
||||
5. **自定义断点**: 根据实际需求调整断点
|
||||
|
||||
### 工具函数
|
||||
|
||||
```typescript
|
||||
// src/lib/utils.ts
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 国际化 (i18n)
|
||||
|
||||
### Decision: 选择 next-intl 作为国际化解决方案
|
||||
|
||||
**Rationale**:
|
||||
- **App Router 原生支持**: 完美集成 Next.js 14 App Router
|
||||
- **类型安全**: TypeScript 自动生成翻译键的类型提示
|
||||
- **复数支持**: 内置 ICU 消息语法处理复数和格式化
|
||||
- **SEO 友好**: 自动生成不同语言的元数据
|
||||
- **轻量级**: 与 next-i18next 相比体积更小
|
||||
|
||||
**Alternatives considered**:
|
||||
- **next-i18next**: 专为 Pages Router 设计,App Router 支持不完善
|
||||
- **react-i18next**: 需要额外配置路由和元数据
|
||||
- **自定义方案**: 开发成本高,容易遗漏边界情况
|
||||
|
||||
### 最佳实践
|
||||
|
||||
1. **使用翻译键命名空间**: 按功能模块组织翻译文件
|
||||
2. **提供上下文参数**: 翻译函数支持动态参数插值
|
||||
3. **日期和数字格式化**: 使用 ICU 格式而非手动拼接
|
||||
4. **默认语言回退**: 缺失翻译时回退到默认语言
|
||||
5. **URL 前缀路由**: 使用 `/[locale]/...` 模式而非子域名
|
||||
|
||||
### 配置示例
|
||||
|
||||
```typescript
|
||||
// src/i18n/request.ts
|
||||
import { getRequestConfig } from 'next-intl/server'
|
||||
|
||||
export default getRequestConfig(async ({ locale }) => ({
|
||||
messages: (await import(`../../messages/${locale}.json`)).default
|
||||
}))
|
||||
```
|
||||
|
||||
```json
|
||||
// messages/en.json
|
||||
{
|
||||
"common": {
|
||||
"search": "Search",
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"home": {
|
||||
"title": "AI Project Navigator",
|
||||
"subtitle": "Discover and explore AI projects"
|
||||
},
|
||||
"project": {
|
||||
"details": "Project Details",
|
||||
"links": "External Links",
|
||||
"viewProject": "View Project"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// messages/zh.json
|
||||
{
|
||||
"common": {
|
||||
"search": "搜索",
|
||||
"loading": "加载中..."
|
||||
},
|
||||
"home": {
|
||||
"title": "AI 项目导航",
|
||||
"subtitle": "发现和探索 AI 项目"
|
||||
},
|
||||
"project": {
|
||||
"details": "项目详情",
|
||||
"links": "外部链接",
|
||||
"viewProject": "查看项目"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 数据验证 (Zod)
|
||||
|
||||
### Decision: 选择 Zod 作为运行时数据验证库
|
||||
|
||||
**Rationale**:
|
||||
- **TypeScript 优先**: 自动从 schema 推断类型,与 Prisma 完美配合
|
||||
- **链式 API**: 直观的验证规则定义
|
||||
- **错误处理**: 详细的错误信息,便于调试和用户提示
|
||||
- **零依赖**: 轻量级,不增加太多打包体积
|
||||
|
||||
**Alternatives considered**:
|
||||
- **Yup**: API 较老,TypeScript 支持不如 Zod
|
||||
- **Joi**: 体积较大,主要服务端使用
|
||||
- **io-ts**: 函数式编程风格,学习曲线陡峭
|
||||
|
||||
### 最佳实践
|
||||
|
||||
1. **复用 Prisma 类型**: 使用 `z.prisma` 扩展从 Prisma schema 生成 Zod schema
|
||||
2. **自定义错误消息**: 提供中英文双语错误提示
|
||||
3. **严格验证**: webhook 接口使用 `strict()` 模式拒绝额外字段
|
||||
4. **输入输出分离**: 区分输入验证 schema 和响应输出 schema
|
||||
|
||||
### Webhook 验证示例
|
||||
|
||||
```typescript
|
||||
// src/lib/validations.ts
|
||||
import { z } from 'zod'
|
||||
|
||||
export const LinkTypeEnum = z.enum(['WEBSITE', 'GITHUB', 'HUGGINGFACE', 'PAPER'])
|
||||
|
||||
export const ExternalLinkSchema = z.object({
|
||||
type: LinkTypeEnum,
|
||||
url: z.string().url('Invalid URL format'),
|
||||
title: z.string().optional()
|
||||
})
|
||||
|
||||
export const TagSchema = z.object({
|
||||
name: z.string().min(1, 'Tag name is required'),
|
||||
nameEn: z.string().optional()
|
||||
})
|
||||
|
||||
export const ProjectInputSchema = z.object({
|
||||
name: z.string().min(1, 'Project name is required'),
|
||||
nameEn: z.string().optional(),
|
||||
description: z.string().min(10, 'Description too short'),
|
||||
descriptionEn: z.string().optional(),
|
||||
content: z.string().optional(),
|
||||
contentEn: z.string().optional(),
|
||||
status: z.enum(['ACTIVE', 'ARCHIVED']).default('ACTIVE'),
|
||||
source: z.string().optional(),
|
||||
tags: z.array(TagSchema).min(1, 'At least one tag is required'),
|
||||
links: z.array(ExternalLinkSchema).min(1, 'At least one link is required')
|
||||
})
|
||||
|
||||
export const WebhookPayloadSchema = z.object({
|
||||
apiKey: z.string().min(32, 'Invalid API key format'),
|
||||
projects: z.array(ProjectInputSchema).min(1, 'At least one project is required')
|
||||
})
|
||||
|
||||
export type ProjectInput = z.infer<typeof ProjectInputSchema>
|
||||
export type WebhookPayload = z.infer<typeof WebhookPayloadSchema>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试策略
|
||||
|
||||
### Decision: 使用 Vitest + React Testing Library + Playwright
|
||||
|
||||
**Rationale**:
|
||||
- **Vitest**: 与 Vite 生态深度集成,比 Jest 更快,原生支持 ESM
|
||||
- **React Testing Library**: 专注用户行为测试,而非实现细节
|
||||
- **Playwright**: 跨浏览器 E2E 测试,支持并行执行
|
||||
|
||||
**Alternatives considered**:
|
||||
- **Jest**: 生态成熟但配置复杂,ESM 支持不佳
|
||||
- **Cypress**: E2E 测试强大但较重,测试编写较慢
|
||||
|
||||
### 最佳实践
|
||||
|
||||
1. **测试金字塔**: 70% 单元测试 + 20% 集成测试 + 10% E2E 测试
|
||||
2. **AAA 模式**: Arrange-Act-Assert 结构组织测试
|
||||
3. **描述性测试名**: 使用 `should... when...` 格式
|
||||
4. **Mock 外部依赖**: 使用 Vitest mock 功能隔离测试
|
||||
5. **测试覆盖率**: 配置 `--coverage` 确保达标
|
||||
|
||||
---
|
||||
|
||||
## 8. 代码质量工具
|
||||
|
||||
### Decision: 使用 ESLint + Prettier + TypeScript
|
||||
|
||||
**Rationale**:
|
||||
- **ESLint**: 代码静态分析,捕获潜在错误
|
||||
- **Prettier**: 统一代码格式,减少团队协作摩擦
|
||||
- **TypeScript**: 编译时类型检查,配合 strict mode
|
||||
|
||||
### 推荐配置
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/explicit-function-return-type": ["warn", { "allowExpressions": true }],
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 部署策略
|
||||
|
||||
### Decision: 使用 Vercel 进行部署
|
||||
|
||||
**Rationale**:
|
||||
- **Next.js 原生支持**: 零配置部署,自动优化
|
||||
- **预览环境**: 每个 PR 自动生成预览 URL
|
||||
- **边缘网络**: 全球 CDN 加速
|
||||
- **环境变量**: 安全管理敏感信息
|
||||
|
||||
**Alternatives considered**:
|
||||
- **Railway**: 支持数据库但部署配置较复杂
|
||||
- **自托管**: 成本低但运维负担重
|
||||
|
||||
### 环境变量管理
|
||||
|
||||
```bash
|
||||
# .env.example
|
||||
DATABASE_URL="postgresql://user:password@localhost:5432/agent_park"
|
||||
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
|
||||
WEBHOOK_API_KEY="your-secure-api-key-min-32-chars"
|
||||
NEXT_INTL_DEFAULT_LOCALE="zh"
|
||||
NEXT_INTL_SUPPORTED_LOCALES="zh,en"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 性能优化
|
||||
|
||||
### Decision: 采用多项性能优化策略
|
||||
|
||||
**策略列表**:
|
||||
|
||||
1. **ISR (增量静态再生)**: 项目列表页面每 5 分钟重新验证
|
||||
2. **动态导入**: 客户端组件使用 `next/dynamic` 按需加载
|
||||
3. **图片优化**: 使用 `next/image` 组件自动优化
|
||||
4. **字体优化**: 使用 `next/font` 优化字体加载
|
||||
5. **代码分割**: 自动按路由分割代码
|
||||
6. **Prisma 查询优化**: 使用 `select` 和 `include` 精确获取数据
|
||||
7. **缓存策略**: 使用 Redis 缓存热门查询结果(可选)
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
本研究报告涵盖了 Agent Park 项目的主要技术选型和最佳实践。所有选择都基于以下原则:
|
||||
|
||||
- **类型安全**: TypeScript + Zod + Prisma 提供端到端类型安全
|
||||
- **性能优先**: Next.js App Router + ISR + 代码分割确保快速加载
|
||||
- **开发体验**: shadcn/ui + Tailwind CSS 提供快速开发能力
|
||||
- **可维护性**: ESLint + Prettier + 测试确保代码质量
|
||||
- **国际化**: next-intl 支持中英双语
|
||||
|
||||
这些技术栈完全符合项目章程要求,为后续开发奠定了坚实基础。
|
||||
@@ -0,0 +1,180 @@
|
||||
# 功能规范: Agent Park - AI项目导航网站
|
||||
|
||||
**功能分支**: `001-ai-project-navigator`
|
||||
**创建时间**: 2025-12-25
|
||||
**状态**: 草稿
|
||||
**输入**: 用户描述: "构建一个名为agent-park的全网ai项目导航网站,用户可以搜索和浏览各种领域 查看相关的ai项目 项目详情页需要包括项目的大致情况介绍 和相关网站来源的跳转链接(官网,github等)关于数据来源 需要定期从n8n的相关流程中获取(前期用假数据先把网站搭建起来)我希望风格是 anthropic claude的风格"
|
||||
|
||||
## Clarifications
|
||||
|
||||
### Session 2025-12-25
|
||||
|
||||
- Q: n8n数据更新频率如何配置? → A: 后台维护一个n8n流程列表,每个流程可以获取不同类型的数据,可以自定义每个定时任务的执行周期
|
||||
- Q: 搜索功能范围和排序方式? → A: 全域搜索(名称+描述+标签)+ 按相关性和收录时间排序
|
||||
- Q: 初始假数据规模? → A: 20-30个项目,最小化前期工作量,快速验证核心功能
|
||||
- Q: 项目分类体系如何设计? → A: 采用标签系统而非固定分类,按标签和相关性进行灵活分组,适应AI领域快速变化的特点
|
||||
- Q: 支持哪些外部链接类型? → A: 官网、GitHub、HuggingFace、论文链接四种类型,覆盖主流AI项目场景
|
||||
- Q: n8n数据集成方式如何设计? → A: n8n执行完成后主动推送数据到本系统的webhook接口,由本系统定义数据字段结构,定时任务由n8n管理
|
||||
- Q: 技术栈如何选择? → A: Next.js全栈方案,使用React组件开发,利用API Routes构建webhook接口
|
||||
- Q: webhook接口如何进行身份认证? → A: 使用API Key认证,n8n在请求头中携带预共享密钥进行身份验证
|
||||
- Q: 项目数据的必填字段有哪些? → A: name、description、tags、至少一个link为必填字段,确保项目信息的完整性和可用性
|
||||
- Q: 日志和监控策略如何设计? → A: 使用简单控制台日志,区分不同日志级别,前期不实现告警机制
|
||||
- Q: 当webhook接口接收大量项目数据更新时,前端如何展示数据刷新进度给用户? → A: 不展示更新状态,下次访问时自然生效
|
||||
- Q: 当webhook接收到的项目数据中部分项目验证失败(如缺少必填字段),但其他项目有效时,系统应如何处理? → A: 部分成功模式,有效的入库,失败记录日志
|
||||
|
||||
## 用户场景与测试 *(必填)*
|
||||
|
||||
### 用户故事 1 - 浏览和搜索AI项目 (优先级: P1)
|
||||
|
||||
用户访问网站后,可以通过浏览不同分类或使用搜索功能,快速找到感兴趣的AI项目。
|
||||
|
||||
**优先级原因**: 这是网站的核心功能,用户需要能够发现和查找AI项目,否则整个网站失去价值。
|
||||
|
||||
**独立测试**: 可以通过浏览预设的分类列表,或使用搜索框输入关键词,验证项目列表能够正确显示和过滤。
|
||||
|
||||
**验收场景**:
|
||||
|
||||
1. **给定** 用户访问网站首页,**当** 查看页面时,**那么** 应看到热门标签云和精选项目展示
|
||||
2. **给定** 用户在搜索框输入关键词,**当** 提交搜索时,**那么** 应显示包含该关键词的相关项目
|
||||
3. **给定** 用户点击某个标签,**当** 选择标签时,**那么** 应只显示带该标签的项目
|
||||
4. **给定** 用户组合多个标签筛选,**当** 应用时,**那么** 应显示同时满足所有标签条件的项目
|
||||
5. **给定** 项目列表很长,**当** 用户滚动时,**那么** 应支持分页或无限滚动加载更多项目
|
||||
|
||||
---
|
||||
|
||||
### 用户故事 2 - 查看项目详细信息 (优先级: P1)
|
||||
|
||||
用户点击某个项目后,可以查看该项目的详细信息,包括项目介绍、官方网站链接、GitHub仓库等。
|
||||
|
||||
**优先级原因**: 用户需要了解项目的具体信息才能决定是否进一步探索,详情页是信息获取的关键环节。
|
||||
|
||||
**独立测试**: 点击任意项目卡片,验证详情页正确显示项目描述、外部链接等信息。
|
||||
|
||||
**验收场景**:
|
||||
|
||||
1. **给定** 用户点击某个项目卡片,**当** 进入详情页时,**那么** 应显示项目名称、描述、相关标签
|
||||
2. **给定** 用户在项目详情页,**当** 查看时,**那么** 应看到官网、GitHub、HuggingFace、论文链接等外部来源的跳转链接
|
||||
3. **给定** 用户点击外部链接,**当** 访问时,**那么** 应在新标签页中打开对应的官方网站或代码仓库
|
||||
4. **给定** 某个项目缺少部分信息,**当** 显示时,**那么** 应合理隐藏或提示"暂无信息"
|
||||
|
||||
---
|
||||
|
||||
### 用户故事 3 - 响应式设计体验 (优先级: P2)
|
||||
|
||||
用户在不同设备(桌面、平板、手机)上访问网站,都能获得良好的浏览体验。
|
||||
|
||||
**优先级原因**: 现代用户使用多种设备访问网站,响应式设计确保所有用户都能正常使用。
|
||||
|
||||
**独立测试**: 在不同屏幕尺寸下访问网站,验证布局自动调整且所有功能可用。
|
||||
|
||||
**验收场景**:
|
||||
|
||||
1. **给定** 用户使用手机访问,**当** 查看首页时,**那么** 导航和内容应适配竖屏布局
|
||||
2. **给定** 用户使用平板横屏访问,**当** 浏览时,**那么** 项目卡片应合理排列且易于点击
|
||||
3. **给定** 用户使用桌面大屏访问,**当** 浏览时,**那么** 应充分利用屏幕空间展示更多内容
|
||||
|
||||
---
|
||||
|
||||
### 用户故事 4 - 数据更新和管理 (优先级: P3)
|
||||
|
||||
n8n工作流程在执行完成后主动推送最新的AI项目数据到本系统的webhook接口,更新网站内容。
|
||||
|
||||
**优先级原因**: 这是后台功能,对用户体验影响较小,可以后期实现。前期使用静态数据即可满足需求。
|
||||
|
||||
**独立测试**: 通过模拟n8n推送请求,验证新数据能够正确更新到网站。
|
||||
|
||||
**验收场景**:
|
||||
|
||||
1. **给定** n8n工作流程执行完成,**当** 主动推送数据到webhook接口时,**那么** 数据更新在后台静默执行,用户下次访问时自然看到最新内容
|
||||
2. **给定** 某些项目不再维护,**当** n8n推送更新数据时,**那么** 这些项目应被标记或移除
|
||||
3. **给定** webhook接收数据失败,**当** 发生错误时,**那么** 应返回明确错误信息给n8n并记录日志,现有数据不受影响
|
||||
4. **给定** n8n推送的数据格式不符合要求,**当** 验证时,**那么** 应拒绝请求并返回具体错误信息
|
||||
5. **给定** 请求缺少有效的API Key,**当** 验证时,**那么** 应返回401未授权错误并记录日志
|
||||
6. **给定** 推送的数据中部分项目验证失败,**当** 处理时,**那么** 有效项目正常入库,失败项目记录详细错误信息
|
||||
|
||||
---
|
||||
|
||||
### 边界情况
|
||||
|
||||
- 当搜索无结果时,应显示友好提示并建议用户尝试其他关键词
|
||||
- 当外部链接失效(404)时,应在页面上标注"链接可能失效"但仍保留入口
|
||||
- 当数据加载缓慢时,应显示加载动画或骨架屏提升体验
|
||||
- 当用户输入特殊字符搜索时,应正确处理并防止XSS攻击
|
||||
- 当同时有大量用户访问时,网站应保持稳定响应
|
||||
|
||||
## 需求 *(必填)*
|
||||
|
||||
### 功能需求
|
||||
|
||||
- **FR-001**: 网站首页必须展示热门标签云和精选项目列表
|
||||
- **FR-002**: 用户必须能够通过搜索框输入关键词查找相关项目,搜索范围覆盖项目名称、描述和标签,结果按相关性优先、收录时间次之排序
|
||||
- **FR-003**: 用户必须能够通过点击标签筛选项目,支持多标签组合筛选
|
||||
- **FR-004**: 每个项目必须显示至少包含:项目名称、简短描述、相关标签
|
||||
- **FR-005**: 项目详情页必须显示项目的完整介绍和外部来源链接(官网、GitHub、HuggingFace、论文链接四种类型)
|
||||
- **FR-006**: 外部链接必须在新标签页中打开,确保用户不会离开导航网站
|
||||
- **FR-007**: 网站必须支持响应式设计,适配桌面、平板、手机等设备
|
||||
- **FR-008**: 网站设计风格应参考Anthropic Claude官网,采用简洁、现代、优雅的视觉风格
|
||||
- **FR-009**: 前期使用静态假数据构建网站,提供webhook接口接收n8n推送的数据
|
||||
- **FR-010**: 系统必须提供webhook API接口,接收n8n工作流程推送的项目数据
|
||||
- **FR-011**: webhook接口必须验证推送数据的格式和必填字段,拒绝不符合规范的数据请求
|
||||
- **FR-012**: webhook接口必须支持部分成功模式,当批量数据中部分项目验证失败时,有效项目正常入库,失败项目记录详细错误信息
|
||||
- **FR-013**: webhook接口必须使用API Key进行身份认证,n8n在请求头中携带预共享密钥
|
||||
- **FR-014**: 系统必须实现控制台日志记录,区分info、warn、error级别,记录webhook请求和错误信息
|
||||
|
||||
### 关键实体 *(如果功能涉及数据则包含)*
|
||||
|
||||
- **AI项目 (Project)**: 代表一个AI工具或应用
|
||||
- 必填字段: name(项目名称)、description(详细描述)、tags(标签集合)、links(至少一个外部链接)
|
||||
- 可选字段: status(项目状态:活跃/停更,默认活跃)、createdAt(收录时间,默认当前时间)、source(数据来源标识)
|
||||
- **标签 (Tag)**: 代表AI项目的特征标记,采用灵活的标签系统而非固定分类,如"图像生成"、"代码助手"、"数据分析"等,一个项目可以有多个标签
|
||||
- **外部链接 (ExternalLink)**: 代表项目的来源网站,包含链接类型(官网/GitHub/HuggingFace/论文)和对应的URL
|
||||
|
||||
## 成功标准 *(必填)*
|
||||
|
||||
### 可衡量的结果
|
||||
|
||||
- **SC-001**: 用户可以在5秒内找到目标AI项目(通过搜索或分类浏览)
|
||||
- **SC-002**: 95%的用户能够成功从项目详情页跳转到外部来源网站
|
||||
- **SC-003**: 网站首屏加载时间在2秒以内,确保良好的用户体验
|
||||
- **SC-004**: 网站在主流浏览器(Chrome、Firefox、Safari、Edge)的最新两个版本中正常显示和运行
|
||||
- **SC-005**: 移动设备上的用户能够单手完成项目搜索和查看的完整流程
|
||||
- **SC-006**: 网站能够支持至少100个并发用户访问而不出现性能明显下降
|
||||
- **SC-007**: 用户对网站视觉设计的满意度评分达到4分以上(5分制)
|
||||
|
||||
## 假设 *(必填)*
|
||||
|
||||
### 技术假设
|
||||
|
||||
- 使用Next.js全栈框架,React组件开发,API Routes构建webhook接口
|
||||
- 前期数据存储为JSON格式或静态文件,后期可迁移到数据库
|
||||
- 本系统提供webhook API接口接收n8n推送的项目数据
|
||||
- n8n按照本系统定义的数据字段结构推送JSON格式数据
|
||||
- 网站部署在Vercel或支持Next.js的服务器上
|
||||
|
||||
### 内容假设
|
||||
|
||||
- 前期使用20-30个假项目数据进行开发和测试,覆盖主要标签类型
|
||||
- AI项目数据由人工审核和整理,确保质量和准确性
|
||||
- 项目标签参考主流AI社区的标记方式(如HuggingFace、Papers with Code),保持灵活性
|
||||
- 外部链接的维护责任在于对应项目方,网站仅提供导航服务
|
||||
|
||||
### 用户假设
|
||||
|
||||
- 用户具备基本的网页浏览和搜索技能
|
||||
- 用户主要使用中文进行搜索和浏览
|
||||
- 用户希望在发现有趣项目后能够快速访问官方来源获取更多信息
|
||||
|
||||
### 约束
|
||||
|
||||
- 网站设计应避免过度使用动画效果,保持简洁快速
|
||||
- 不需要用户注册登录功能,所有内容公开可访问
|
||||
- 前期不包含用户评论、评分等社交功能,专注于导航展示
|
||||
|
||||
## 范围外 *(明确不包含的内容)*
|
||||
|
||||
- 用户账户系统和个性化推荐
|
||||
- 项目评论、评分、收藏等社交功能
|
||||
- 用户提交新项目的功能(全部由管理员从n8n导入)
|
||||
- 多语言支持(专注于中文用户体验)
|
||||
- 实时通知或更新提醒功能
|
||||
- 付费内容或高级会员功能
|
||||
@@ -0,0 +1,266 @@
|
||||
# 任务: Agent Park - AI项目导航网站
|
||||
|
||||
**输入**: 来自 `/specs/001-ai-project-navigator/` 的设计文档
|
||||
**前置条件**: plan.md, spec.md, data-model.md, contracts/webhook.yaml, research.md, quickstart.md
|
||||
|
||||
**测试**: 本任务清单不包含测试任务。根据章程要求测试驱动开发, 但实际实现可按需决定是否编写测试。
|
||||
|
||||
**组织结构**: 任务按用户故事分组, 以便每个故事能够独立实施和测试。
|
||||
|
||||
## 格式: `[ID] [P] [Story] 描述`
|
||||
- **[P]**: 可以并行运行(不同文件, 无依赖关系)
|
||||
- **[Story]**: 此任务属于哪个用户故事(例如: US1、US2、US3、US4)
|
||||
- 在描述中包含确切的文件路径
|
||||
|
||||
## 路径约定
|
||||
- **单一项目**: 仓库根目录下的 `src/`、`tests/`、`prisma/`
|
||||
|
||||
---
|
||||
|
||||
## 阶段 1: 设置(共享基础设施)
|
||||
|
||||
**目的**: 项目初始化和基本结构
|
||||
|
||||
- [ ] T001 创建 Next.js 14+ 项目并配置 TypeScript 严格模式
|
||||
- [ ] T002 安装核心依赖包
|
||||
- [ ] T003 [P] 配置 ESLint 和 Prettier
|
||||
- [ ] T004 [P] 配置 Tailwind CSS
|
||||
- [ ] T005 [P] 配置 shadcn/ui 组件库
|
||||
- [ ] T006 [P] 配置 next-intl 国际化
|
||||
|
||||
---
|
||||
|
||||
## 阶段 2: 基础(阻塞前置条件)
|
||||
|
||||
**目的**: 在任何用户故事可以实施之前必须完成的核心基础设施
|
||||
|
||||
**⚠️ 关键**: 在此阶段完成之前, 无法开始任何用户故事工作
|
||||
|
||||
- [ ] T007 设置 PostgreSQL 数据库
|
||||
- [ ] T008 创建 Prisma schema 定义
|
||||
- [ ] T009 执行 Prisma 初始迁移
|
||||
- [ ] T010 生成 Prisma Client
|
||||
- [ ] T011 创建 Zod 验证 schemas
|
||||
- [ ] T012 创建 Prisma 客户端单例
|
||||
- [ ] T013 创建种子数据脚本
|
||||
- [ ] T014 配置 Next.js 国际化路由结构
|
||||
- [ ] T015 创建环境变量配置文件
|
||||
|
||||
**检查点**: 基础就绪 - 现在可以开始并行实施用户故事
|
||||
|
||||
---
|
||||
|
||||
## 阶段 3: 用户故事 1 - 浏览和搜索AI项目(优先级: P1)🎯 MVP
|
||||
|
||||
**目标**: 用户可以通过浏览标签云、搜索关键词、筛选标签来发现AI项目
|
||||
|
||||
**独立测试**: 访问首页可以看到热门标签云和精选项目;输入搜索关键词可看到相关项目;点击标签可以筛选项目
|
||||
|
||||
### 用户故事 1 的实施
|
||||
|
||||
- [ ] T016 [P] [US1] 创建首页布局组件 src/app/[locale]/layout.tsx
|
||||
- [ ] T017 [P] [US1] 创建 Header 导航组件 src/components/layout/Header.tsx
|
||||
- [ ] T018 [P] [US1] 创建 Footer 组件 src/components/layout/Footer.tsx
|
||||
- [ ] T019 [P] [US1] 创建 Navigation 组件 src/components/layout/Navigation.tsx
|
||||
- [ ] T020 [P] [US1] 创建 TagCloud 组件 src/components/project/TagCloud.tsx
|
||||
- [ ] T021 [P] [US1] 创建 ProjectCard 组件 src/components/project/ProjectCard.tsx
|
||||
- [ ] T022 [P] [US1] 创建 ProjectList 组件 src/components/project/ProjectList.tsx
|
||||
- [ ] T023 [P] [US1] 创建 SearchBar 组件 src/components/search/SearchBar.tsx
|
||||
- [ ] T024 [US1] 实现首页 src/app/[locale]/page.tsx(依赖于 T016-T023)
|
||||
- [ ] T025 [US1] 实现项目列表页路由 src/app/[locale]/projects/page.tsx
|
||||
- [ ] T026 [US1] 创建 useSearch hook src/hooks/useSearch.ts
|
||||
- [ ] T027 [US1] 创建 useProjects hook src/hooks/useProjects.ts
|
||||
- [ ] T028 [US1] 添加国际化翻译文件 src/messages/zh.json 和 src/messages/en.json
|
||||
- [ ] T029 [US1] 配置 ISR 缓存策略优化页面性能
|
||||
|
||||
**检查点**: 此时, 用户应该能够访问首页、浏览项目、使用搜索和标签筛选功能
|
||||
|
||||
---
|
||||
|
||||
## 阶段 4: 用户故事 2 - 查看项目详细信息(优先级: P1)
|
||||
|
||||
**目标**: 用户点击项目卡片后可以查看项目完整信息和外部链接
|
||||
|
||||
**独立测试**: 点击任意项目卡片进入详情页, 验证显示项目描述、标签、外部链接, 点击链接在新标签页打开
|
||||
|
||||
### 用户故事 2 的实施
|
||||
|
||||
- [ ] T030 [P] [US2] 创建 ProjectDetail 组件 src/components/project/ProjectDetail.tsx
|
||||
- [ ] T031 [P] [US2] 创建 ExternalLinkCard 组件 src/components/project/ExternalLinkCard.tsx
|
||||
- [ ] T032 [US2] 实现项目详情页路由 src/app/[locale]/projects/[id]/page.tsx(依赖于 T030、T031)
|
||||
- [ ] T033 [US2] 添加项目详情页的国际化翻译
|
||||
- [ ] T034 [US2] 实现外部链接的 target="_blank" 安全属性
|
||||
|
||||
**检查点**: 此时, 用户故事 1 和用户故事 2 都应该完全功能化且可独立测试
|
||||
|
||||
---
|
||||
|
||||
## 阶段 5: 用户故事 3 - 响应式设计体验(优先级: P2)
|
||||
|
||||
**目标**: 网站在桌面、平板、手机等不同设备上都能正常显示和操作
|
||||
|
||||
**独立测试**: 在不同屏幕尺寸下访问网站, 验证布局自动调整且所有功能可用
|
||||
|
||||
### 用户故事 3 的实施
|
||||
|
||||
- [ ] T035 [P] [US3] 更新 Header 组件支持响应式布局
|
||||
- [ ] T036 [P] [US3] 更新 Navigation 组件支持移动端菜单
|
||||
- [ ] T037 [P] [US3] 更新 ProjectCard 组件支持不同屏幕尺寸
|
||||
- [ ] T038 [P] [US3] 更新 ProjectList 组件支持响应式网格布局
|
||||
- [ ] T039 [P] [US3] 更新 SearchBar 组件支持移动端输入
|
||||
- [ ] T040 [US3] 更新 ProjectDetail 组件支持响应式布局
|
||||
- [ ] T041 [US3] 配置 Tailwind 响应式断点
|
||||
- [ ] T042 [US3] 测试并优化移动端触摸交互
|
||||
|
||||
**检查点**: 此时, 所有用户故事(US1、US2、US3)都应该独立功能化且支持响应式
|
||||
|
||||
---
|
||||
|
||||
## 阶段 6: 用户故事 4 - 数据更新和管理(优先级: P3)
|
||||
|
||||
**目标**: 提供 webhook API 接口, 接收 n8n 工作流程推送的 AI 项目数据更新
|
||||
|
||||
**独立测试**: 使用 curl 或 Postman 模拟 n8n 推送请求, 验证新数据能够正确更新到数据库
|
||||
|
||||
### 用户故事 4 的实施
|
||||
|
||||
- [ ] T043 [P] [US4] 创建 WebhookAuthSchema 验证
|
||||
- [ ] T044 [P] [US4] 创建 WebhookPayloadSchema 验证
|
||||
- [ ] T045 [P] [US4] 创建 ProjectInputSchema 验证
|
||||
- [ ] T046 [US4] 实现 webhook API 端点 src/app/api/webhook/projects/route.ts(依赖于 T043-T045)
|
||||
- [ ] T047 [US4] 实现 API Key 身份验证中间件
|
||||
- [ ] T048 [US4] 实现部分成功模式的批量数据处理逻辑
|
||||
- [ ] T049 [US4] 添加 webhook 请求日志记录
|
||||
- [ ] T050 [US4] 添加 webhook 错误处理和响应格式
|
||||
- [ ] T051 [US4] 配置 webhook API 的环境变量
|
||||
- [ ] T052 [US4] 编写 webhook API 使用文档
|
||||
|
||||
**检查点**: 此时, 所有用户故事现在应该完全功能化
|
||||
|
||||
---
|
||||
|
||||
## 阶段 7: 完善与横切关注点
|
||||
|
||||
**目的**: 影响多个用户故事的改进
|
||||
|
||||
- [ ] T053 [P] 全局样式优化实现 Anthropic Claude 风格
|
||||
- [ ] T054 [P] 优化 Core Web Vitals 性能指标
|
||||
- [ ] T055 [P] 添加 loading 和 skeleton 状态提升用户体验
|
||||
- [ ] T056 [P] 实现 404 和错误页面
|
||||
- [ ] T057 [P] 添加 SEO 元数据配置
|
||||
- [ ] T058 配置 next.config.js 优化
|
||||
- [ ] T059 创建 README.md 项目文档
|
||||
- [ ] T060 运行 quickstart.md 验证完整流程
|
||||
|
||||
---
|
||||
|
||||
## 依赖关系与执行顺序
|
||||
|
||||
### 阶段依赖关系
|
||||
|
||||
- **设置(阶段 1)**: 无依赖关系 - 可立即开始
|
||||
- **基础(阶段 2)**: 依赖于设置完成 - 阻塞所有用户故事
|
||||
- **用户故事(阶段 3-6)**: 都依赖于基础阶段完成
|
||||
- US1 和 US2 都是 P1 优先级, 可以并行开发
|
||||
- US3 (P2) 可以在 US1/US2 基础上进行响应式优化
|
||||
- US4 (P3) 是独立的后台功能, 可并行开发
|
||||
- **完善(阶段 7)**: 依赖于所有期望的用户故事完成
|
||||
|
||||
### 用户故事依赖关系
|
||||
|
||||
- **用户故事 1 (P1)**: 可在基础(阶段 2)后开始 - 无其他故事依赖
|
||||
- **用户故事 2 (P1)**: 可在基础(阶段 2)后开始 - 可与 US1 并行开发
|
||||
- **用户故事 3 (P2)**: 依赖于 US1 和 US2 完成 - 在现有组件基础上添加响应式支持
|
||||
- **用户故事 4 (P3)**: 可在基础(阶段 2)后开始 - 独立的 API 功能
|
||||
|
||||
### 每个用户故事内部
|
||||
|
||||
- US1: 布局和基础组件 [P] 并行 → 页面路由集成 → hooks 和国际化
|
||||
- US2: 组件 [P] 并行 → 页面路由实现
|
||||
- US3: 组件响应式更新 [P] 并行 → 全局配置
|
||||
- US4: Schema 定义 [P] 并行 → API 端点实现 → 日志和错误处理
|
||||
|
||||
### 并行机会
|
||||
|
||||
- 所有标记为 [P] 的设置任务(T003-T006)可以并行运行
|
||||
- US1 的所有布局和基础组件(T016-T023)可以并行开发
|
||||
- US2 的组件(T030-T031)可以并行开发
|
||||
- US3 的响应式更新(T035-T039)可以并行进行
|
||||
- US4 的所有 Schema 定义(T043-T045)可以并行开发
|
||||
- US1 和 US2 可以由不同团队成员并行处理
|
||||
- US4 可以与 US1/US2/US3 并行开发(独立的后端功能)
|
||||
|
||||
---
|
||||
|
||||
## 并行示例: 用户故事 1 (US1)
|
||||
|
||||
```bash
|
||||
# 一起启动用户故事 1 的所有布局和基础组件(可并行):
|
||||
T016: "创建首页布局组件 src/app/[locale]/layout.tsx"
|
||||
T017: "创建 Header 导航组件 src/components/layout/Header.tsx"
|
||||
T018: "创建 Footer 组件 src/components/layout/Footer.tsx"
|
||||
T019: "创建 Navigation 组件 src/components/layout/Navigation.tsx"
|
||||
T020: "创建 TagCloud 组件 src/components/project/TagCloud.tsx"
|
||||
T021: "创建 ProjectCard 组件 src/components/project/ProjectCard.tsx"
|
||||
T022: "创建 ProjectList 组件 src/components/project/ProjectList.tsx"
|
||||
T023: "创建 SearchBar 组件 src/components/search/SearchBar.tsx"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实施策略
|
||||
|
||||
### 仅 MVP(用户故事 1 + 2)
|
||||
|
||||
1. 完成阶段 1: 设置(T001-T006)
|
||||
2. 完成阶段 2: 基础(T007-T015)
|
||||
3. 完成阶段 3: 用户故事 1(T016-T029)
|
||||
4. 完成阶段 4: 用户故事 2(T030-T034)
|
||||
5. **停止并验证**: 独立测试核心浏览、搜索和详情功能
|
||||
6. 如准备好则部署/演示
|
||||
|
||||
### 增量交付
|
||||
|
||||
1. 完成设置 + 基础(T001-T015) → 基础就绪
|
||||
2. 添加用户故事 1(T016-T029) → 独立测试 → 部署/演示(MVP!)
|
||||
3. 添加用户故事 2(T030-T034) → 独立测试 → 部署/演示
|
||||
4. 添加用户故事 3(T035-T042) → 独立测试 → 部署/演示
|
||||
5. 添加用户故事 4(T043-T052) → 独立测试 → 部署/演示
|
||||
6. 添加完善(T053-T060) → 最终部署
|
||||
|
||||
### 并行团队策略
|
||||
|
||||
有多个开发人员时:
|
||||
|
||||
1. 团队一起完成设置 + 基础(T001-T015)
|
||||
2. 基础完成后:
|
||||
- 开发人员 A: 用户故事 1(T016-T029)
|
||||
- 开发人员 B: 用户故事 2(T030-T034)
|
||||
- 开发人员 C: 用户故事 4(T043-T052)
|
||||
3. US1 和 US2 完成后, 开发人员 A/B 转向用户故事 3(T035-T042)
|
||||
4. 所有故事完成后, 团队一起进行完善(T053-T060)
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- [P] 任务 = 不同文件, 无依赖关系
|
||||
- [Story] 标签将任务映射到特定用户故事以实现可追溯性
|
||||
- 每个用户故事应该独立可完成和可测试
|
||||
- 在每个任务或逻辑组后提交
|
||||
- 在任何检查点停止以独立验证故事
|
||||
- 避免: 模糊任务、相同文件冲突、破坏独立性的跨故事依赖
|
||||
|
||||
---
|
||||
|
||||
## 摘要统计
|
||||
|
||||
- **总任务数**: 60 个任务
|
||||
- **阶段数**: 7 个阶段
|
||||
- **用户故事数**: 4 个用户故事
|
||||
- US1 (P1): 14 个任务
|
||||
- US2 (P1): 5 个任务
|
||||
- US3 (P2): 8 个任务
|
||||
- US4 (P3): 10 个任务
|
||||
- **并行任务数**: 约 35 个任务标记为 [P] 可并行
|
||||
- **MVP 范围**: 阶段 1-4 (T001-T034), 共 34 个任务
|
||||
Reference in New Issue
Block a user