diff --git a/docs/plans/2025-01-25-ai-timeline-feature-design.md b/docs/plans/2025-01-25-ai-timeline-feature-design.md new file mode 100644 index 0000000..5cd871d --- /dev/null +++ b/docs/plans/2025-01-25-ai-timeline-feature-design.md @@ -0,0 +1,494 @@ +# AI 时间轴功能设计文档 + +**创建日期**: 2025-01-25 +**目标用户**: 开发者和 AI 研究者 +**设计理念**: 学术+产品平衡,聚焦大语言模型发展历程(从 2017 Transformer 开始) + +--- + +## 1. 系统架构 + +### 核心组件 + +**前端展示系统** +- 路由: `src/app/[locale]/timeline/page.tsx` +- 渲染策略: ISR (1小时缓存) +- 设计风格: Neo-brutalism,复用现有设计系统 +- 数据展示: 按年份分组的卡片堆叠时间轴 + +**后端 API** +- `POST /api/events` - 创建事件(供 n8n 调用,需 API Key 认证) +- `GET /api/events` - 获取所有事件(支持年份筛选) +- `GET /api/events/:id` - 获取单个事件详情 +- `PATCH /api/events/:id` - 更新事件(可选) +- `DELETE /api/events/:id` - 删除事件(可选) + +**数据采集系统 (n8n)** +- 历史数据初始化流程: 一次性运行,批量收集 2017-2025 事件 +- 增量更新流程: 每周一运行,收集最近 7 天新事件 +- 三 Agent 协作: 搜索 → 筛选 → 格式化 → HTTP Request 提交 + +**数据库** +- `AIEvent` 表: 存储所有事件 +- 无需标签系统(基础版数据结构) + +--- + +## 2. 数据库 Schema + +### Prisma 模型 + +```prisma +model AIEvent { + id String @id @default(cuid()) + title String + titleEn String? // 英文标题(可选) + eventDate DateTime // 事件发生的具体日期 + description String + descriptionEn String? // 英文描述 + imageUrl String // 事件配图 URL + sourceUrl String? // 原始来源链接 + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([eventDate(sort: Desc)]) // 按日期降序索引 + @@index([createdAt]) +} +``` + +### 字段说明 +- **国际化字段**: `title`/`titleEn`, `description`/`descriptionEn` 与项目表保持一致 +- **日期字段**: 使用 `DateTime` 支持时间范围筛选和排序 +- **索引优化**: `eventDate` 降序索引提升"最近事件"查询性能 +- **图片存储**: 直接存储 URL(可以是 Unsplash、项目截图等) + +### 迁移命令 +```bash +pnpm prisma migrate dev --name add_ai_events_table +``` + +--- + +## 3. API 端点设计 + +### 数据验证 Schema + +在 `src/lib/validations.ts` 新增: + +```typescript +const AIEventInputSchema = z.object({ + title: z.string().min(1).max(200), + titleEn: z.string().max(200).optional(), + eventDate: z.string().datetime(), // ISO 8601 格式 + description: z.string().min(10).max(500), + descriptionEn: z.string().max(500).optional(), + imageUrl: z.string().url(), + sourceUrl: z.string().url().optional(), +}); +``` + +### POST /api/events (n8n 调用) + +**认证**: +```typescript +const apiKey = request.headers.get('X-API-Key'); +if (!apiKey || !crypto.timingSafeEqual( + Buffer.from(apiKey), + Buffer.from(process.env.WEBHOOK_API_KEY!) +)) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); +} +``` + +**验证**: +```typescript +const body = await request.json(); +const validationResult = AIEventInputSchema.array().safeParse(body); +if (!validationResult.success) { + return NextResponse.json({ + error: 'Validation failed', + details: validationResult.error + }, { status: 400 }); +} +``` + +**创建**: +```typescript +const events = await prisma.aIEvent.createMany({ + data: validationResult.data, + skipDuplicates: true, +}); +return NextResponse.json({ created: events.count }, { status: 201 }); +``` + +### GET /api/events (前端调用) + +支持查询参数: +- `?year=2024` - 筛选特定年份 +- `?limit=50` - 限制返回数量 +- `?offset=0` - 分页偏移 + +返回按 `eventDate` 降序排列的事件列表。 + +### 调用示例 + +```bash +curl -X POST https://your-domain.com/api/events \ + -H "Content-Type: application/json" \ + -H "X-API-Key: YOUR_WEBHOOK_API_KEY" \ + -d '{ + "title": "GPT-4 发布", + "eventDate": "2023-03-14T00:00:00Z", + "description": "OpenAI 发布多模态大语言模型", + "imageUrl": "https://example.com/gpt4.jpg" + }' +``` + +--- + +## 4. 前端页面实现 + +### 页面结构 + +`src/app/[locale]/timeline/page.tsx`: + +```tsx +import { getAIEvents } from '@/hooks/useAIEvents'; +import { TimelineSection } from '@/components/timeline/TimelineSection'; + +export const revalidate = 3600; // ISR 1小时 + +export default async function TimelinePage() { + const events = await getAIEvents(); + + // 按年份分组 + const eventsByYear = events.reduce((acc, event) => { + const year = new Date(event.eventDate).getFullYear(); + if (!acc[year]) acc[year] = []; + acc[year].push(event); + return acc; + }, {} as Record); + + return ( +
+
+

AI 发展时间轴

+
+ +
+ {Object.entries(eventsByYear) + .sort(([a], [b]) => Number(b) - Number(a)) // 降序 + .map(([year, yearEvents]) => ( + + ))} +
+
+ ); +} +``` + +### 数据获取函数 + +`src/hooks/useAIEvents.ts`: + +```typescript +export async function getAIEvents() { + const events = await prisma.aIEvent.findMany({ + orderBy: { eventDate: 'desc' }, + take: 100, // 最多 100 条 + }); + return events; +} +``` + +### 组件设计 + +- **TimelineSection**: 渲染单个年份的卡片堆叠区域,复用原型的动画效果 +- **EventCard**: 单个事件卡片(图片、标题、描述、日期) +- **响应式**: 移动端垂直堆叠,桌面端横向卡片堆叠 + +--- + +## 5. n8n Workflow 设计 + +### 流程 A: 历史数据初始化(一次性运行) + +**1. 历史搜索 Agent** +- 按年份批量搜索: 2017-2025 +- 搜索关键词: `"AI breakthrough YEAR"`, `"LLM release YEAR"`, `"GPT model YEAR"` +- 时间范围: 每年 1月1日 - 12月31日 +- 输出: 每年 50-100 条搜索结果 + +**2. 历史筛选 Agent** +- 权威来源: `['arxiv.org', 'openai.com', 'anthropic.com', 'google.ai', 'meta.ai', 'deepmind.com']` +- 去重逻辑: 相同标题或 URL 只保留一个 +- 输出: 每年精选 10-20 个事件 + +**3. 历史格式化 Agent** +- 提取完整信息: 标题、准确日期、详细描述、配图 +- 对描述进行润色和本地化(翻译成中文) +- 补充缺失图片(使用 Unsplash 或项目官网截图) + +**4. HTTP Request: 批量提交** +- POST `/api/events` +- 每次提交 10-20 个事件(按年份分批) +- 记录成功/失败状态 + +### 流程 B: 增量更新(每周运行) + +**触发器**: Cron node,每周一早上 9:00 + +**1. 增量搜索 Agent** +- 时间范围: 最近 7 天 +- 搜索关键词: `["AI news", "LLM release", "model launch"]` +- 输出: 10-20 条最新搜索结果 + +**2. 增量筛选 Agent** +- 权威来源检查(同历史流程) +- 额外检查: 查询数据库避免重复 `sourceUrl` +- 输出: 2-5 个新事件 + +**3. 增量格式化 Agent** +- 快速格式化: 提取核心信息 +- 描述保持简短(直接使用摘要) +- 图片优先使用新闻配图 + +**4. HTTP Request: 增量提交** +- POST `/api/events` +- 一次性提交所有新事件 +- 失败时发送告警邮件 + +### 错误处理 + +- **重试策略**: 每个 Agent 失败后等待 5s/10s/20s 重试,最多 3 次 +- **降级处理**: 图片提取失败使用默认图片; 日期模糊使用当月 1 日 +- **告警机制**: 最终失败时发送邮件并附执行日志 +- **部分成功**: 批量提交时即使部分失败,也记录成功的事件 + +--- + +## 6. 数据验证和错误处理 + +### API 端点保护 + +**认证机制**: 使用 `crypto.timingSafeEqual` 防止时序攻击 + +**请求体验证**: Zod schema 验证所有字段 + +**去重逻辑**: 基于 `sourceUrl` 的唯一索引(如果提供) + +**长度限制**: +- 标题 ≤200 字符 +- 描述 ≤500 字符 + +**必填字段**: `title`, `eventDate`, `description`, `imageUrl` + +**URL 验证**: `imageUrl` 和 `sourceUrl` 必须是有效的 HTTP/HTTPS URL + +### 前端错误边界 + +```tsx +export default async function TimelinePage() { + try { + const events = await getAIEvents(); + if (!events.length) { + return ; + } + return ; + } catch (error) { + console.error('Failed to load events:', error); + return ; + } +} +``` + +--- + +## 7. 测试策略 + +### 单元测试 (Vitest) + +**API 端点测试** (`src/app/api/events/route.test.ts`): +```typescript +describe('POST /api/events', () => { + it('should create event with valid data', async () => { + const response = await POST(request_mock); + expect(response.status).toBe(201); + }); + + it('should reject invalid API key', async () => { + const response = await POST(bad_request_mock); + expect(response.status).toBe(401); + }); + + it('should validate required fields', async () => { + const response = await POST(invalid_data_mock); + expect(response.status).toBe(400); + }); +}); +``` + +**数据验证测试** (`src/lib/validations.test.ts`): +```typescript +describe('AIEventInputSchema', () => { + it('should validate valid event', () => { + expect(() => AIEventInputSchema.parse(validEvent)).not.toThrow(); + }); + + it('should reject missing title', () => { + expect(() => AIEventInputSchema.parse({ ...validEvent, title: '' })) + .toThrow(); + }); +}); +``` + +### E2E 测试 (chrome-devtools-mcp) + +**测试流程**: +1. 启动开发服务器: `pnpm dev` +2. 使用 chrome-devtools-mcp 工具: + - `new_page`: http://localhost:3000/timeline + - `take_snapshot`: 验证页面结构 + - `take_screenshot`: 对比设计原型 + - `evaluate_script`: 检查事件数据渲染 + - `list_console_messages`: 确保无 JavaScript 错误 +3. 测试筛选功能: + - `navigate_page`: url=/timeline?year=2024 + - `take_snapshot`: 验证只显示 2024 年事件 + +**手动测试清单**: +- [ ] 页面加载成功,无 console 错误 +- [ ] 事件按年份正确分组显示 +- [ ] 卡片堆叠动画正常工作 +- [ ] 响应式布局在移动端正常 +- [ ] ISR 缓存在 1 小时后正确更新 + +### 集成测试 (n8n Workflow) + +- 手动触发历史数据初始化流程,验证数据库事件创建 +- 手动触发增量更新流程,验证新事件添加 +- 测试错误场景: API 不可用、数据格式错误、网络超时 + +### 测试数据准备 + +使用 `pnpm prisma db seed` 创建种子数据: +- Seed 脚本插入 5-10 个示例事件(2023-2025 真实事件) +- 方便开发和手动测试 + +--- + +## 8. 部署和实施步骤 + +### 阶段 1: 数据库和后端 (1-2 天) + +```bash +# 更新 schema +vim prisma/schema.prisma + +# 生成并运行迁移 +pnpm prisma migrate dev --name add_ai_events_table +pnpm prisma generate + +# 创建验证 schema +vim src/lib/validations.ts + +# 创建 API route +mkdir src/app/api/events +vim src/app/api/events/route.ts + +# 创建数据获取函数 +vim src/hooks/useAIEvents.ts + +# 手动测试 API +curl -X POST http://localhost:3000/api/events \ + -H "Content-Type: application/json" \ + -H "X-API-Key: $WEBHOOK_API_KEY" \ + -d '{ + "title": "测试事件", + "eventDate": "2025-01-25T00:00:00Z", + "description": "这是一个测试事件", + "imageUrl": "https://example.com/image.jpg" + }' +``` + +### 阶段 2: 前端页面 (2-3 天) + +```bash +# 创建 timeline 页面 +mkdir -p src/app/[locale]/timeline +vim src/app/[locale]/timeline/page.tsx + +# 创建组件 +mkdir src/components/timeline +vim src/components/timeline/TimelineSection.tsx +vim src/components/timeline/EventCard.tsx + +# 更新 header 添加导航链接 +vim src/components/layout/Header.tsx + +# 使用 chrome-devtools-mcp 测试页面效果 +``` + +### 阶段 3: n8n Workflow 配置 (1-2 天) + +1. 在 n8n 中创建历史数据初始化 workflow +2. 创建增量更新 workflow,设置每周一 9:00 触发 +3. 配置环境变量 `WEBHOOK_API_KEY` +4. 手动运行历史 workflow,初始化 2017-2025 数据 +5. 验证增量 workflow 是否正常工作 + +### 阶段 4: 测试和优化 (1 天) + +1. 使用 chrome-devtools-mcp 进行完整测试 +2. 修复发现的问题 +3. 优化性能(ISR 缓存、图片加载) +4. 准备生产环境部署 + +### 阶段 5: 部署到生产 + +1. 更新 Vercel 环境变量(确保 `WEBHOOK_API_KEY` 已设置) +2. 运行数据库迁移(如果需要): `pnpm prisma db push` +3. 部署代码到 Vercel +4. 在 n8n 中更新 API endpoint 为生产地址 +5. 监控第一次增量运行 + +--- + +## 9. 关键设计决策 + +### 为什么选择基础版事件数据? +- 符合 YAGNI 原则,快速上线 +- 避免过度设计,聚焦核心价值 +- 后续可扩展(如添加用户互动、点赞等) + +### 为什么不需要标签系统? +- 从 UI 原型看,卡片只显示核心信息(标题、日期、描述、图片) +- 时间轴本身就是按年份组织,无需额外分类 +- 减少数据复杂度,提升性能 + +### 为什么设计两套 n8n 流程? +- **历史流程**: 注重数据质量和完整性,处理 2017-2025 的大量数据 +- **增量流程**: 注重效率和及时性,每周自动更新 +- 分离关注点,便于独立优化和调试 + +### 为什么使用 ISR 而非纯静态? +- 事件数据每周更新,需要一定时效性 +- ISR 1小时缓存平衡了性能和新鲜度 +- 避免每次请求都查询数据库 + +--- + +## 10. 后续优化方向 + +1. **搜索和筛选**: 添加关键词搜索、标签筛选 +2. **用户互动**: 点赞、评论、分享功能 +3. **相关推荐**: 基于事件标签推荐相关项目 +4. **多语言支持**: 完善英文版本的描述和翻译 +5. **图片优化**: 使用 Next.js Image 组件优化图片加载 +6. **数据可视化**: 添加图表展示 AI 发展趋势 +7. **导出功能**: 支持导出时间轴为 PDF/Markdown