diff --git a/docs/plans/2025-01-25-ai-timeline-implementation.md b/docs/plans/2025-01-25-ai-timeline-implementation.md new file mode 100644 index 0000000..6a78506 --- /dev/null +++ b/docs/plans/2025-01-25-ai-timeline-implementation.md @@ -0,0 +1,2156 @@ +# AI 时间轴功能实施计划 + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**目标:** 构建一个展示 AI 大语言模型发展历程的时间轴功能,支持 2017 年至今的里程碑事件展示,通过 n8n workflow 自动收集和更新数据。 + +**架构:** 采用 Next.js 15 App Router + Prisma + PostgreSQL 架构,前端使用 ISR 缓存策略,后端提供 RESTful API,数据采集通过 n8n workflow 中的三个 Agent 协作完成(搜索→筛选→格式化→提交)。 + +**技术栈:** Next.js 15, Prisma, PostgreSQL, n8n, Web Search MCP, Zod, Vitest, chrome-devtools-mcp + +--- + +## 前置准备 + +### Task 0: 创建 Git Worktree + +**文件:** +- Create: N/A (使用 git worktree) + +**Step 1: 创建隔离的工作空间** + +```bash +cd /Users/caihaohan/Code/agent_park +git worktree add ../agent_park-timeline main -b feature/ai-timeline +cd ../agent_park-timeline +``` + +**Step 2: 验证 worktree 创建成功** + +```bash +pwd +# Expected output: /Users/caihaohan/Code/agent_park-timeline + +git branch +# Expected output: * feature/ai-timeline +``` + +**Step 3: 安装依赖(如果需要)** + +```bash +pnpm install +``` + +**Step 4: 启动开发服务器** + +```bash +# 先检查是否有进程运行在 3000 端口 +lsof -ti:3000 | xargs kill -9 2>/dev/null || true +pnpm dev +``` + +**Step 5: 验证服务器启动成功** + +访问: http://localhost:3000 +Expected: Agent Park 首页正常显示 + +**Step 6: 提交 worktree 初始化** + +```bash +cd /Users/caihaohan/Code/agent_park-timeline +git add . +git commit -m "chore: initialize worktree for AI timeline feature" +``` + +--- + +## 阶段 1: 数据库 Schema + +### Task 1: 添加 AIEvent 模型到 Prisma Schema + +**文件:** +- Modify: `prisma/schema.prisma` + +**Step 1: 打开 schema 文件** + +```bash +vim prisma/schema.prisma +# 或使用你喜欢的编辑器 +``` + +**Step 2: 在文件末尾添加 AIEvent 模型** + +在最后一个 `}` 后面添加: + +```prisma +model AIEvent { + id String @id @default(cuid()) + title String + titleEn String? + eventDate DateTime + description String + descriptionEn String? + imageUrl String + sourceUrl String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([eventDate(sort: Desc)]) + @@index([createdAt]) +} +``` + +**Step 3: 保存文件** + +**:wq` (vim) 或 Cmd+S (编辑器) + +**Step 4: 验证语法正确** + +```bash +pnpm prisma validate +``` + +Expected: `The schema is valid` + +**Step 5: 生成并运行迁移** + +```bash +pnpm prisma migrate dev --name add_ai_events_table +``` + +Expected: +``` +The following migration(s) have been created and applied from new schema changes: + +migrations/ + └─ 20250125XXXXXX_add_ai_events_table/ + └─ migration.sql + +Applying migration `20250125XXXXXX_add_ai_events_table` + +The following migration(s) have been created and applied: +... +``` + +**Step 6: 生成 Prisma Client** + +```bash +pnpm prisma generate +``` + +Expected: `Prisma Client generated successfully` + +**Step 7: 提交** + +```bash +git add prisma/schema.prisma prisma/migrations/ +git commit -m "feat: add AIEvent model to database schema" +``` + +--- + +## 阶段 2: 数据验证 + +### Task 2: 创建 Zod 验证 Schema + +**文件:** +- Modify: `src/lib/validations.ts` + +**Step 1: 打开验证文件** + +```bash +vim src/lib/validations.ts +``` + +**Step 2: 找到文件末尾(在 `export { ... }` 之前)** + +使用 `/ProjectQuerySchema` 搜索到相关位置,然后在后面添加。 + +**Step 3: 添加 AIEvent 相关的 Zod Schema** + +在 `ProjectQuerySchema` 定义后添加: + +```typescript +export const AIEventInputSchema = z.object({ + title: z.string().min(1).max(200), + titleEn: z.string().max(200).optional(), + eventDate: z.string().datetime(), + description: z.string().min(10).max(500), + descriptionEn: z.string().max(500).optional(), + imageUrl: z.string().url(), + sourceUrl: z.string().url().optional(), +}); + +export const AIEventQuerySchema = z.object({ + year: z.string().regex(/^\d{4}$/).optional(), + limit: z.string().regex(/^\d+$/).transform(Number).optional(), + offset: z.string().regex(/^\d+$/).transform(Number).optional(), +}); +``` + +**Step 4: 更新 export 语句** + +找到 `export {` 行,添加: + +```typescript +export { + // ... 现有的 exports + AIEventInputSchema, + AIEventQuerySchema, +}; +``` + +**Step 5: 保存文件** + +**:wq` + +**Step 6: 验证 TypeScript 编译通过** + +```bash +pnpm tsc --noEmit +``` + +Expected: 无错误输出 + +**Step 7: 提交** + +```bash +git add src/lib/validations.ts +git commit -m "feat: add AIEvent validation schemas" +``` + +--- + +### Task 3: 编写验证测试 + +**文件:** +- Create: `src/lib/validations.test.ts` + +**Step 1: 创建测试文件** + +```bash +vim src/lib/validations.test.ts +``` + +**Step 2: 编写测试** + +```typescript +import { describe, it, expect } from 'vitest'; +import { AIEventInputSchema } from './validations'; + +describe('AIEventInputSchema', () => { + const validEvent = { + title: 'GPT-4 发布', + eventDate: '2023-03-14T00:00:00Z', + description: 'OpenAI 发布多模态大语言模型', + imageUrl: 'https://example.com/gpt4.jpg', + }; + + it('should validate valid event', () => { + expect(() => AIEventInputSchema.parse(validEvent)).not.toThrow(); + }); + + it('should accept event with optional English fields', () => { + const eventWithEn = { + ...validEvent, + titleEn: 'GPT-4 Release', + descriptionEn: 'OpenAI launches multimodal LLM', + sourceUrl: 'https://openai.com/blog/gpt-4', + }; + expect(() => AIEventInputSchema.parse(eventWithEn)).not.toThrow(); + }); + + it('should reject empty title', () => { + expect(() => AIEventInputSchema.parse({ ...validEvent, title: '' })) + .toThrow(); + }); + + it('should reject title exceeding 200 characters', () => { + const longTitle = 'A'.repeat(201); + expect(() => AIEventInputSchema.parse({ ...validEvent, title: longTitle })) + .toThrow(); + }); + + it('should reject description shorter than 10 characters', () => { + expect(() => AIEventInputSchema.parse({ ...validEvent, description: '太短' })) + .toThrow(); + }); + + it('should reject description exceeding 500 characters', () => { + const longDesc = 'A'.repeat(501); + expect(() => AIEventInputSchema.parse({ ...validEvent, description: longDesc })) + .toThrow(); + }); + + it('should reject invalid eventDate format', () => { + expect(() => AIEventInputSchema.parse({ ...validEvent, eventDate: '2023-03-14' })) + .toThrow(); + }); + + it('should reject invalid imageUrl', () => { + expect(() => AIEventInputSchema.parse({ ...validEvent, imageUrl: 'not-a-url' })) + .toThrow(); + }); + + it('should reject invalid sourceUrl format', () => { + expect(() => AIEventInputSchema.parse({ + ...validEvent, + sourceUrl: 'not-a-url' + })).toThrow(); + }); +}); +``` + +**Step 3: 保存文件** + +**:wq` + +**Step 4: 运行测试验证失败** + +```bash +pnpm test src/lib/validations.test.ts +``` + +Expected: 全部通过 + +**Step 5: 提交** + +```bash +git add src/lib/validations.test.ts +git commit -m "test: add AIEvent validation tests" +``` + +--- + +## 阶段 3: 数据获取层 + +### Task 4: 创建数据获取函数 + +**文件:** +- Create: `src/hooks/useAIEvents.ts` + +**Step 1: 创建文件** + +```bash +vim src/hooks/useAIEvents.ts +``` + +**Step 2: 编写数据获取函数** + +```typescript +import { prisma } from '@/lib/prisma'; + +export async function getAIEvents(options?: { + year?: number; + limit?: number; + offset?: number; +}) { + const where = options?.year + ? { + eventDate: { + gte: new Date(`${options.year}-01-01T00:00:00Z`), + lte: new Date(`${options.year}-12-31T23:59:59Z`), + }, + } + : undefined; + + const events = await prisma.aIEvent.findMany({ + where, + orderBy: { eventDate: 'desc' }, + take: options?.limit || 100, + skip: options?.offset || 0, + }); + + return events; +} + +export async function getAIEventBySlug(slug: string) { + // 暂不实现,后续如需要详细页面时添加 + return null; +} + +export async function getAllAIEventYears() { + const events = await prisma.aIEvent.findMany({ + select: { + eventDate: true, + }, + orderBy: { eventDate: 'desc' }, + }); + + const years = new Set(); + events.forEach(event => { + years.add(new Date(event.eventDate).getFullYear()); + }); + + return Array.from(years).sort((a, b) => b - a); +} +``` + +**Step 3: 保存文件** + +**:wq` + +**Step 4: 验证 TypeScript 类型正确** + +```bash +pnpm tsc --noEmit +``` + +Expected: 无错误 + +**Step 5: 提交** + +```bash +git add src/hooks/useAIEvents.ts +git commit -m "feat: add AI event data fetching functions" +``` + +--- + +## 阶段 4: 后端 API + +### Task 5: 创建 API 路由 - POST endpoint + +**文件:** +- Create: `src/app/api/events/route.ts` + +**Step 1: 创建目录** + +```bash +mkdir -p src/app/api/events +``` + +**Step 2: 创建路由文件** + +```bash +vim src/app/api/events/route.ts +``` + +**Step 3: 实现 POST handler** + +```typescript +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { AIEventInputSchema, AIEventQuerySchema } from '@/lib/validations'; +import crypto from 'crypto'; + +export async function POST(request: NextRequest) { + // 1. API Key 验证 + const apiKey = request.headers.get('X-API-Key'); + const expectedKey = process.env.WEBHOOK_API_KEY; + + if (!apiKey || !expectedKey || !crypto.timingSafeEqual( + Buffer.from(apiKey), + Buffer.from(expectedKey) + )) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + // 2. 解析请求体 + let body: unknown; + try { + body = await request.json(); + } catch (error) { + return NextResponse.json( + { error: 'Invalid JSON' }, + { status: 400 } + ); + } + + // 3. 验证数据 + const validationResult = AIEventInputSchema.array().safeParse(body); + if (!validationResult.success) { + return NextResponse.json( + { + error: 'Validation failed', + details: validationResult.error.errors, + }, + { status: 400 } + ); + } + + // 4. 创建事件 + try { + const result = await prisma.aIEvent.createMany({ + data: validationResult.data, + skipDuplicates: true, + }); + + return NextResponse.json( + { + created: result.count, + total: validationResult.data.length, + }, + { status: 201 } + ); + } catch (error) { + console.error('Failed to create AI events:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} + +export async function GET(request: NextRequest) { + // 1. 解析查询参数 + const searchParams = request.nextUrl.searchParams; + const queryParams = { + year: searchParams.get('year'), + limit: searchParams.get('limit'), + offset: searchParams.get('offset'), + }; + + // 2. 验证查询参数 + const validationResult = AIEventQuerySchema.safeParse(queryParams); + if (!validationResult.success) { + return NextResponse.json( + { + error: 'Invalid query parameters', + details: validationResult.error.errors, + }, + { status: 400 } + ); + } + + // 3. 获取事件 + try { + const events = await prisma.aIEvent.findMany({ + where: validationResult.data.year + ? { + eventDate: { + gte: new Date(`${validationResult.data.year}-01-01T00:00:00Z`), + lte: new Date(`${validationResult.data.year}-12-31T23:59:59Z`), + }, + } + : undefined, + orderBy: { eventDate: 'desc' }, + take: validationResult.data.limit || 100, + skip: validationResult.data.offset || 0, + }); + + return NextResponse.json({ events }); + } catch (error) { + console.error('Failed to fetch AI events:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} +``` + +**Step 4: 保存文件** + +**:wq` + +**Step 5: 验证 TypeScript 编译** + +```bash +pnpm tsc --noEmit +``` + +**Step 6: 提交** + +```bash +git add src/app/api/events/ +git commit -m "feat: add AI events API endpoints" +``` + +--- + +### Task 6: 编写 API 测试 + +**文件:** +- Create: `src/app/api/events/route.test.ts` + +**Step 1: 创建测试文件** + +```bash +vim src/app/api/events/route.test.ts +``` + +**Step 2: 编写测试** + +```typescript +import { describe, it, expect, beforeEach } from 'vitest'; +import { POST, GET } from './route'; +import { NextRequest } from 'next/server'; + +describe('POST /api/events', () => { + const validEvent = { + title: 'GPT-4 发布', + eventDate: '2023-03-14T00:00:00Z', + description: 'OpenAI 发布多模态大语言模型', + imageUrl: 'https://example.com/gpt4.jpg', + }; + + it('should reject without API key', async () => { + const request = new NextRequest('http://localhost:3000/api/events', { + method: 'POST', + body: JSON.stringify([validEvent]), + }); + + const response = await POST(request); + expect(response.status).toBe(401); + + const json = await response.json(); + expect(json.error).toBe('Unauthorized'); + }); + + it('should reject with invalid API key', async () => { + const request = new NextRequest('http://localhost:3000/api/events', { + method: 'POST', + headers: { + 'X-API-Key': 'invalid-key', + }, + body: JSON.stringify([validEvent]), + }); + + const response = await POST(request); + expect(response.status).toBe(401); + }); + + // 注意: 以下测试需要设置 WEBHOOK_API_KEY 环境变量 + // 可以通过 vi.stubEnv 来模拟 +}); + +describe('GET /api/events', () => { + it('should return events array', async () => { + const request = new NextRequest('http://localhost:3000/api/events'); + const response = await GET(request); + + expect(response.status).toBe(200); + + const json = await response.json(); + expect(json).toHaveProperty('events'); + expect(Array.isArray(json.events)).toBe(true); + }); + + it('should filter by year', async () => { + const request = new NextRequest( + 'http://localhost:3000/api/events?year=2024' + ); + const response = await GET(request); + + expect(response.status).toBe(200); + + const json = await response.json(); + expect(json).toHaveProperty('events'); + }); + + it('should reject invalid year format', async () => { + const request = new NextRequest( + 'http://localhost:3000/api/events?year=invalid' + ); + const response = await GET(request); + + expect(response.status).toBe(400); + + const json = await response.json(); + expect(json.error).toBe('Invalid query parameters'); + }); +}); +``` + +**Step 3: 保存文件** + +**:wq` + +**Step 4: 运行测试** + +```bash +pnpm test src/app/api/events/route.test.ts +``` + +Expected: 基础测试通过(需要 API Key 的测试会失败) + +**Step 5: 提交** + +```bash +git add src/app/api/events/route.test.ts +git commit -m "test: add API route tests" +``` + +--- + +### Task 7: 手动测试 API + +**文件:** +- N/A (使用 curl) + +**Step 1: 确保 WEBHOOK_API_KEY 已设置** + +```bash +# 检查 .env.local +cat .env.local | grep WEBHOOK_API_KEY +``` + +如果不存在,添加: +```bash +echo "WEBHOOK_API_KEY=test-key-for-development-only" >> .env.local +``` + +**Step 2: 测试 GET endpoint** + +```bash +curl http://localhost:3000/api/events +``` + +Expected: +```json +{ + "events": [] +} +``` + +**Step 3: 测试 POST endpoint(创建单个事件)** + +```bash +curl -X POST http://localhost:3000/api/events \ + -H "Content-Type: application/json" \ + -H "X-API-Key: test-key-for-development-only" \ + -d '{ + "title": "测试事件 - Transformer 论文发表", + "eventDate": "2017-06-12T00:00:00Z", + "description": "Google 团队发表 Attention Is All You Need 论文,提出了 Transformer 架构", + "imageUrl": "https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800" + }' +``` + +Expected: +```json +{ + "created": 1, + "total": 1 +} +``` + +**Step 4: 验证事件已创建** + +```bash +curl http://localhost:3000/api/events +``` + +Expected: 返回刚才创建的事件 + +**Step 5: 测试批量创建** + +```bash +curl -X POST http://localhost:3000/api/events \ + -H "Content-Type: application/json" \ + -H "X-API-Key: test-key-for-development-only" \ + -d '[ + { + "title": "GPT-1 发布", + "eventDate": "2018-06-11T00:00:00Z", + "description": "OpenAI 发布第一代 GPT 模型", + "imageUrl": "https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800" + }, + { + "title": "BERT 发布", + "eventDate": "2018-10-11T00:00:00Z", + "description": "Google 发布 BERT 预训练模型", + "imageUrl": "https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800" + } + ]' +``` + +Expected: +```json +{ + "created": 2, + "total": 2 +} +``` + +**Step 6: 测试年份筛选** + +```bash +curl "http://localhost:3000/api/events?year=2018" +``` + +Expected: 只返回 2018 年的事件 + +**Step 7: 提交 API 测试说明文档** + +```bash +# 创建 API 测试文档 +cat > docs/api-testing-guide.md << 'EOF' +# API 测试指南 + +## GET /api/events + +获取所有事件: + +```bash +curl http://localhost:3000/api/events +``` + +筛选特定年份: + +```bash +curl "http://localhost:3000/api/events?year=2024" +``` + +限制返回数量: + +```bash +curl "http://localhost:3000/api/events?limit=10" +``` + +## POST /api/events + +创建单个事件: + +```bash +curl -X POST http://localhost:3000/api/events \ + -H "Content-Type: application/json" \ + -H "X-API-Key: YOUR_API_KEY" \ + -d '{ + "title": "事件标题", + "eventDate": "2023-03-14T00:00:00Z", + "description": "事件描述(10-500字)", + "imageUrl": "https://example.com/image.jpg" + }' +``` + +批量创建事件: + +```bash +curl -X POST http://localhost:3000/api/events \ + -H "Content-Type: application/json" \ + -H "X-API-Key: YOUR_API_KEY" \ + -d '[ + { "title": "事件1", ... }, + { "title": "事件2", ... } + ]' +``` +EOF + +git add docs/api-testing-guide.md +git commit -m "docs: add API testing guide" +``` + +--- + +## 阶段 5: 前端页面 + +### Task 8: 创建 Timeline 页面 + +**文件:** +- Create: `src/app/[locale]/timeline/page.tsx` + +**Step 1: 创建目录** + +```bash +mkdir -p src/app/\[locale\]/timeline +``` + +**Step 2: 创建页面文件** + +```bash +vim src/app/\[locale\]/timeline/page.tsx +``` + +**Step 3: 实现页面组件** + +```typescript +import { getAIEvents } from '@/hooks/useAIEvents'; +import { Metadata } from 'next'; + +export const revalidate = 3600; // ISR 1小时 + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + + return { + title: locale === 'zh' ? 'AI 发展时间轴' : 'AI Timeline', + description: locale === 'zh' + ? '探索人工智能大语言模型的发展历程,从 2017 年 Transformer 到今天' + : 'Explore the evolution of AI large language models from 2017 Transformer to today', + }; +} + +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); + + // 按年份降序排序 + const sortedYears = Object.keys(eventsByYear) + .map(Number) + .sort((a, b) => b - a); + + if (events.length === 0) { + return ( +
+
+

+ 暂无数据 +

+

+ 时间轴数据正在收集中... +

+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+

+ AI 发展时间轴 +

+

+ 从 Transformer 到 AGI: 大语言模型的进化之路 +

+
+
+ + {/* Timeline */} +
+ {sortedYears.map((year, index) => ( +
+ {/* Year Label */} +
+
+ + {year} + +
+
+ + {/* Events Container */} +
+
+ {eventsByYear[year].map((event, eventIndex) => ( +
+ {/* Tape decoration */} +
+ + {/* Image */} +
+ {event.title} +
+ + {/* Content */} +

+ {event.title} +

+

+ {event.description} +

+ + {/* Date */} +
+ {new Date(event.eventDate).toLocaleDateString('zh-CN')} +
+ + {/* Source Link */} + {event.sourceUrl && ( + + 来源 → + + )} +
+ ))} +
+
+
+ ))} +
+
+ ); +} +``` + +**Step 4: 保存文件** + +**:wq` + +**Step 5: 验证页面可访问** + +访问: http://localhost:3000/timeline + +Expected: 显示时间轴页面,包含之前创建的测试事件 + +**Step 6: 提交** + +```bash +git add src/app/\[locale\]/timeline/ +git commit -m "feat: add timeline page with year-based layout" +``` + +--- + +### Task 9: 创建 EventCard 组件(可选重构) + +**文件:** +- Create: `src/components/timeline/EventCard.tsx` +- Create: `src/components/timeline/TimelineSection.tsx` + +**Step 1: 创建组件目录** + +```bash +mkdir -p src/components/timeline +``` + +**Step 2: 创建 EventCard 组件** + +```bash +vim src/components/timeline/EventCard.tsx +``` + +```typescript +import { AIEvent } from '@prisma/client'; + +interface EventCardProps { + event: AIEvent; + index: number; +} + +export function EventCard({ event, index }: EventCardProps) { + const rotation = (Math.random() - 0.5) * 6; + const zIndex = Math.max(1, 50 - index * 10); + + return ( +
+ {/* Tape decoration */} +
+ + {/* Image */} +
+ {event.title} +
+ + {/* Content */} +

+ {event.title} +

+

+ {event.description} +

+ + {/* Date */} +
+ {new Date(event.eventDate).toLocaleDateString('zh-CN')} +
+ + {/* Source Link */} + {event.sourceUrl && ( + + 来源 → + + )} +
+ ); +} +``` + +**Step 3: 创建 TimelineSection 组件** + +```bash +vim src/components/timeline/TimelineSection.tsx +``` + +```typescript +import { AIEvent } from '@prisma/client'; +import { EventCard } from './EventCard'; + +interface TimelineSectionProps { + year: number; + events: AIEvent[]; + index: number; +} + +export function TimelineSection({ year, events, index }: TimelineSectionProps) { + const isEven = index % 2 === 0; + + return ( +
+ {/* Year Label */} +
+
+ + {year} + +
+
+ + {/* Events Container */} +
+
+ {events.map((event, eventIndex) => ( + + ))} +
+
+
+ ); +} +``` + +**Step 4: 重构 Timeline Page 使用新组件** + +更新 `src/app/[locale]/timeline/page.tsx`: + +```typescript +import { getAIEvents } from '@/hooks/useAIEvents'; +import { TimelineSection } from '@/components/timeline/TimelineSection'; +import { Metadata } from 'next'; + +export const revalidate = 3600; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + + return { + title: locale === 'zh' ? 'AI 发展时间轴' : 'AI Timeline', + description: locale === 'zh' + ? '探索人工智能大语言模型的发展历程' + : 'Explore the evolution of AI large language models', + }; +} + +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); + + const sortedYears = Object.keys(eventsByYear) + .map(Number) + .sort((a, b) => b - a); + + if (events.length === 0) { + return ( +
+
+

+ 暂无数据 +

+

+ 时间轴数据正在收集中... +

+
+
+ ); + } + + return ( +
+
+
+

+ AI 发展时间轴 +

+

+ 从 Transformer 到 AGI: 大语言模型的进化之路 +

+
+
+ +
+ {sortedYears.map((year, index) => ( + + ))} +
+
+ ); +} +``` + +**Step 5: 提交** + +```bash +git add src/components/timeline/ +git commit -m "refactor: extract EventCard and TimelineSection components" +``` + +--- + +### Task 10: 更新导航菜单 + +**文件:** +- Modify: `src/components/layout/Header.tsx` + +**Step 1: 打开 Header 组件** + +```bash +vim src/components/layout/Header.tsx +``` + +**Step 2: 找到导航链接部分** + +搜索 `/projects` 或导航相关的代码。 + +**Step 3: 添加 Timeline 链接** + +在导航菜单中添加: + +```typescript + + Timeline + +``` + +**Step 4: 保存并提交** + +```bash +git add src/components/layout/Header.tsx +git commit -m "feat: add Timeline link to navigation" +``` + +--- + +## 阶段 6: 测试 + +### Task 11: 使用 chrome-devtools-mcp 测试 + +**文件:** +- N/A (手动测试流程) + +**Step 1: 确保开发服务器运行** + +```bash +lsof -ti:3000 | xargs kill -9 2>/dev/null || true +pnpm dev +``` + +**Step 2: 使用 chrome-devtools-mcp 工具测试** + +在另一个对话中执行以下操作(或记录为测试文档): + +```markdown +# Timeline 页面 E2E 测试流程 + +## 1. 页面加载测试 + +使用 `mcp__chrome-devtools__new_page`: +- URL: `http://localhost:3000/timeline` +- 验证: 页面成功加载 + +使用 `mcp__chrome-devtools__take_snapshot`: +- 验证: 页面结构正确,包含 header 和 timeline sections + +## 2. 数据渲染测试 + +使用 `mcp__chrome-devtools__evaluate_script`: +```javascript +() => { + const yearSections = document.querySelectorAll('.year-section'); + return { + yearCount: yearSections.length, + hasEvents: yearSections.length > 0 + }; +} +``` +Expected: `{ yearCount: >0, hasEvents: true }` + +## 3. 控制台错误检查 + +使用 `mcp__chrome-devtools__list_console_messages`: +- Expected: 无错误或警告 + +## 4. 响应式测试 + +使用 `mcp__chrome-devtools__resize_page`: +- 测试尺寸: 375x667 (iPhone SE) +- 测试尺寸: 1920x1080 (桌面) +- 验证: 布局在不同尺寸下正常显示 + +## 5. 视觉回归测试 + +使用 `mcp__chrome-devtools__take_screenshot`: +- 保存截图与设计原型对比 +- 验证: 视觉风格符合 Neo-brutalism 设计 +``` + +**Step 3: 创建测试文档** + +```bash +cat > docs/e2e-testing-guide.md << 'EOF' +# Timeline E2E 测试指南 + +## 使用 chrome-devtools-mcp 测试 + +### 1. 启动测试环境 + +\`\`\`bash +# 确保开发服务器运行 +pnpm dev +\`\`\` + +### 2. 页面加载测试 + +\`\`\`javascript +// new_page +{ + "url": "http://localhost:3000/timeline" +} + +// take_snapshot +// 验证页面结构正确 +\`\`\` + +### 3. 数据验证 + +\`\`\`javascript +// evaluate_script +() => { + const yearSections = document.querySelectorAll('section'); + const eventCards = document.querySelectorAll('.stack-card'); + + return { + yearCount: yearSections.length, + eventCount: eventCards.length, + hasHeader: document.querySelector('h1') !== null + }; +} +\`\`\` + +Expected: +- yearCount > 0 +- eventCount > 0 +- hasHeader: true + +### 4. 无控制台错误 + +\`\`\`javascript +// list_console_messages +{ + "types": ["error", "warn"] +} +\`\`\` + +Expected: 空数组 + +### 5. 截图对比 + +\`\`\`javascript +// take_screenshot +{ + "filePath": "tests/screenshots/timeline-page.png" +} +\`\`\` + +手动对比与 `design/stitch_agent_park_homepage/screen.png` + +### 6. 响应式测试 + +\`\`\`javascript +// resize_page +{ + "width": 375, + "height": 667 +} + +// take_snapshot +// 验证移动端布局 +\`\`\` +EOF + +git add docs/e2e-testing-guide.md +git commit -m "docs: add E2E testing guide with chrome-devtools-mcp" +``` + +**Step 4: 执行测试并记录结果** + +根据测试结果修复发现的问题。 + +**Step 5: 提交测试结果** + +```bash +# 如果有修复 +git add . +git commit -m "fix: address issues found during E2E testing" +``` + +--- + +## 阶段 7: n8n Workflow 配置 + +### Task 12: 设计历史数据初始化 Workflow + +**文件:** +- Create: `docs/n8n/historical-workflow-design.json` + +**Step 1: 创建 n8n workflow 文档目录** + +```bash +mkdir -p docs/n8n +``` + +**Step 2: 编写历史数据初始化 Workflow 设计** + +```bash +cat > docs/n8n/historical-workflow-design.md << 'EOF' +# n8n 历史数据初始化 Workflow + +## 概述 + +此 workflow 用于一次性收集和初始化 2017-2025 年的 AI 重大事件数据。 + +## Workflow 结构 + +### Node 1: Cron 触发器(手动触发) + +- 节点类型: `Manual Trigger` +- 用途: 开发测试时手动运行 + +### Node 2: 设置年份列表 + +- 节点类型: `Code` +- 用途: 定义要处理的年份列表 + +\`\`\`javascript +// 返回年份数组 +return [ + { year: 2017 }, + { year: 2018 }, + { year: 2019 }, + { year: 2020 }, + { year: 2021 }, + { year: 2022 }, + { year: 2023 }, + { year: 2024 }, + { year: 2025 }, +]; +\`\`\` + +### Node 3: 搜索 Agent(循环每年) + +- 节点类型: `Loop Over Items` +- 用途: 遍历每个年份 + +### Node 4: Web Search - Agent 1 + +- 节点类型: `HTTP Request` +- 方法: POST +- URL: `` +- Headers: + \`\`\`json + { + "Content-Type": "application/json" + } + \`\`\` +- Body: + \`\`\`json + { + "search_query": "AI breakthrough {{ $json.year }} LLM release transformer model", + "search_recency_filter": "noLimit", + "content_size": "high" + } + \`\`\` + +### Node 5: 筛选 Agent - Agent 2 + +- 节点类型: `Code` +- 用途: 根据权威来源筛选 + +\`\`\`javascript +const trustedDomains = [ + 'arxiv.org', + 'openai.com', + 'anthropic.com', + 'google.ai', + 'meta.ai', + 'deepmind.com', + 'research.google', +]; + +const items = $input.all(); + +const filtered = items.filter(item => { + const url = item.json.url || ''; + return trustedDomains.some(domain => url.includes(domain)); +}); + +return filtered; +\`\`\` + +### Node 6: 格式化 Agent - Agent 3 + +- 节点类型: `Code` +- 用途: 转换为 API 格式 + +\`\`\`javascript +const items = $input.all(); + +const formatted = items.map(item => { + const publishedDate = item.json.published_date || new Date().toISOString(); + + return { + json: { + title: item.json.title || 'Untitled', + eventDate: new Date(publishedDate).toISOString(), + description: (item.json.description || item.json.snippet || '').substring(0, 500), + imageUrl: item.json.image_url || 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800', + sourceUrl: item.json.url, + }, + }; +}); + +return formatted; +\`\`\` + +### Node 7: 提交到 API + +- 节点类型: `HTTP Request` +- 方法: POST +- URL: `http://localhost:3000/api/events` +- Headers: + \`\`\`json + { + "Content-Type": "application/json", + "X-API-Key": "={{ $env.WEBHOOK_API_KEY }}" + } + \`\`\` +- Body: `={{ $json }}` (发送整个数组) + +### Node 8: 错误处理 + +- 节点类型: `IF` +- 条件: 检查上一个节点的 status code +- On True: 记录成功 +- On False: 发送错误邮件 + +## 环境变量 + +在 n8n 中设置: +- `WEBHOOK_API_KEY`: 你的 API 密钥(从 .env.local 获取) +- `API_ENDPOINT`: `http://localhost:3000/api/events` (开发) 或生产 URL + +## 测试步骤 + +1. 在 n8n UI 中创建此 workflow +2. 手动触发运行 +3. 检查数据库: `pnpm prisma studio` +4. 验证事件已正确创建 +EOF +``` + +**Step 3: 创建增量更新 Workflow 设计** + +```bash +cat > docs/n8n/incremental-workflow-design.md << 'EOF' +# n8n 增量更新 Workflow + +## 概述 + +此 workflow 每周一自动运行,收集最近 7 天的新 AI 事件。 + +## Workflow 结构 + +### Node 1: Cron 触发器 + +- 节点类型: `Cron` +- 表达式: `0 9 * * 1` (每周一早上 9:00) +- 时区: Asia/Shanghai + +### Node 2: Web Search - Agent 1 + +- 节点类型: `HTTP Request` +- URL: `` +- Body: + \`\`\`json + { + "search_query": "AI news LLM release model launch this week", + "search_recency_filter": "oneWeek" + } + \`\`\` + +### Node 3: 筛选 Agent - Agent 2 + +- 节点类型: `Code` +- 用途: 筛选 + 去重(查询数据库避免重复) + +\`\`\`javascript +const trustedDomains = [ + 'arxiv.org', + 'openai.com', + 'anthropic.com', +]; + +// 过滤权威来源 +const items = $input.all(); +const filtered = items.filter(item => { + const url = item.json.url || ''; + return trustedDomains.some(domain => url.includes(domain)); +}); + +// TODO: 添加数据库查询去重 +// 这里可以调用 GET /api/events 检查 sourceUrl 是否已存在 + +return filtered; +\`\`\` + +### Node 4: 格式化 Agent - Agent 3 + +- 节点类型: `Code` +- 代码: 同历史 workflow + +### Node 5: 提交到 API + +- 节点类型: `HTTP Request` +- 配置: 同历史 workflow + +### Node 6: 发送通知邮件 + +- 节点类型: `Send Email` +- 条件: 仅在创建新事件时发送 +- 内容: + \`\`\` + 主题: AI Timeline - 新事件已添加 + + 本次更新添加了 {{ $json.created }} 个新事件。 + + 查看: https://your-domain.com/timeline + \`\`\` + +### Node 7: 错误处理 + +- 节点类型: `Error Trigger` +- 动作: 发送错误邮件到管理员 + +## 测试 + +1. 修改 Cron 为手动触发进行测试 +2. 验证只有新事件被添加 +3. 检查邮件通知是否正常发送 +4. 确认错误处理工作正常 +EOF +``` + +**Step 4: 提交 n8n workflow 设计文档** + +```bash +git add docs/n8n/ +git commit -m "docs: add n8n workflow designs for AI timeline" +``` + +--- + +### Task 13: 实现历史数据初始化(手动执行) + +**文件:** +- N/A (手动操作 + 脚本) + +**Step 1: 准备历史事件数据** + +创建 `scripts/seed-historical-events.ts`: + +```bash +mkdir -p scripts +vim scripts/seed-historical-events.ts +``` + +**Step 2: 编写种子数据脚本** + +```typescript +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +const historicalEvents = [ + { + title: 'Attention Is All You Need', + eventDate: new Date('2017-06-12T00:00:00Z'), + description: 'Google 团队发表 Transformer 论文,提出自注意力机制,彻底改变 NLP 领域', + imageUrl: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800', + sourceUrl: 'https://arxiv.org/abs/1706.03762', + }, + { + title: 'GPT-1 发布', + eventDate: new Date('2018-06-11T00:00:00Z'), + description: 'OpenAI 发布第一代生成式预训练 Transformer 模型,展示无监督学习的潜力', + imageUrl: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800', + sourceUrl: 'https://s3-us-west-2.amazonaws.com/openai-assets/research-covers/language-unsupervised/language-understanding-paper.pdf', + }, + { + title: 'BERT 发布', + eventDate: new Date('2018-10-11T00:00:00Z'), + description: 'Google 发布双向编码器表示 Transformer,在 11 项 NLP 任务中创 SOTA', + imageUrl: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800', + sourceUrl: 'https://arxiv.org/abs/1810.04805', + }, + { + title: 'GPT-2 发布', + eventDate: new Date('2019-02-14T00:00:00Z'), + description: 'OpenAI 发布 15 亿参数的 GPT-2,因"太危险"而不敢全部发布', + imageUrl: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800', + sourceUrl: 'https://openai.com/research/better-language-models', + }, + { + title: 'GPT-3 发布', + eventDate: new Date('2020-05-28T00:00:00Z'), + description: 'OpenAI 发布 1750 亿参数的 GPT-3,展示 few-shot 学习的强大能力', + imageUrl: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800', + sourceUrl: 'https://arxiv.org/abs/2005.14165', + }, + { + title: 'GitHub Copilot 发布', + eventDate: new Date('2021-06-29T00:00:00Z'), + description: 'GitHub 和 OpenAI 发布 AI 编程助手,基于 Codex 模型', + imageUrl: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800', + sourceUrl: 'https://github.blog/news-insights/company-news/github-copilot/', + }, + { + title: 'ChatGPT 发布', + eventDate: new Date('2022-11-30T00:00:00Z'), + description: 'OpenAI 发布对话式 AI 助手 ChatGPT,5 天用户突破 100 万', + imageUrl: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800', + sourceUrl: 'https://openai.com/blog/chatgpt', + }, + { + title: 'GPT-4 发布', + eventDate: new Date('2023-03-14T00:00:00Z'), + description: 'OpenAI 发布多模态大语言模型 GPT-4,在各项基准测试中接近人类水平', + imageUrl: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800', + sourceUrl: 'https://openai.com/research/gpt-4', + }, + { + title: 'Claude 发布', + eventDate: new Date('2023-03-16T00:00:00Z'), + description: 'Anthropic 发布 AI 助手 Claude,强调安全性和有用性', + imageUrl: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800', + sourceUrl: 'https://www.anthropic.com/index/claude-now-open', + }, +]; + +async function main() { + console.log('开始插入历史事件...'); + + for (const event of historicalEvents) { + try { + await prisma.aIEvent.create({ + data: event, + }); + console.log(`✓ ${event.title} (${event.eventDate.getFullYear()})`); + } catch (error) { + console.log(`✗ ${event.title} 已存在或插入失败`); + } + } + + console.log('\n历史事件插入完成!'); +} + +main() + .catch(console.error) + .finally(() => prisma.$disconnect()); +``` + +**Step 3: 运行种子脚本** + +```bash +npx tsx scripts/seed-historical-events.ts +``` + +Expected: +``` +开始插入历史事件... +✓ Attention Is All You Need (2017) +✓ GPT-1 发布 (2018) +... +历史事件插入完成! +``` + +**Step 4: 验证数据** + +访问: http://localhost:3000/timeline + +Expected: 显示历史事件,按年份分组 + +**Step 5: 提交种子脚本** + +```bash +git add scripts/seed-historical-events.ts +git commit -m "feat: add historical events seed script" +``` + +--- + +## 阶段 8: 部署准备 + +### Task 14: 准备生产环境 + +**文件:** +- Modify: `.env.local` (仅本地) +- Update Vercel 环境变量 + +**Step 1: 验证所有环境变量** + +```bash +# 本地开发 +cat .env.local | grep -E "(DATABASE_URL|WEBHOOK_API_KEY)" + +# 应该看到: +# DATABASE_URL=postgres://... +# WEBHOOK_API_KEY=your-test-key +``` + +**Step 2: 在 Vercel 设置环境变量** + +访问: https://vercel.com/your-project/settings/environment-variables + +确保已设置: +- `DATABASE_URL`: 生产数据库 URL (Neon) +- `WEBHOOK_API_KEY`: 生产 API 密钥(强密码,至少 32 字符) + +**Step 3: 准备生产数据库迁移** + +```bash +# 生成迁移 SQL +pnpm prisma migrate diff \ + --from-empty \ + --to-schema-datamodel prisma/schema.prisma \ + --script > migration.sql +``` + +**Step 4: 提交部署准备文档** + +```bash +cat > docs/deployment-guide.md << 'EOF' +# 生产环境部署指南 + +## 前置条件 + +- [x] Vercel 项目已配置 +- [x] Neon 数据库已连接 +- [x] WEBHOOK_API_KEY 环境变量已设置 + +## 部署步骤 + +### 1. 运行数据库迁移 + +\`\`\`bash +# 方式 1: 使用 Prisma push +pnpm prisma db push --preview-feature + +# 方式 2: 在 Neon SQL Editor 执行 migration.sql +\`\`\` + +### 2. 部署到 Vercel + +\`\`\`bash +git push origin feature/ai-timeline +# 或通过 PR 合并到 main +\`\`\` + +### 3. 验证部署 + +- 访问生产 URL: `https://your-domain.com/timeline` +- 测试 API: `curl https://your-domain.com/api/events` +- 检查 Vercel 日志确认无错误 + +### 4. 配置 n8n Workflow + +更新 n8n 中的环境变量: +- `WEBHOOK_API_KEY`: 生产密钥 +- `API_ENDPOINT`: `https://your-domain.com/api/events` + +### 5. 运行历史数据初始化 + +手动触发 n8n 历史初始化 workflow + +### 6. 验证增量更新 + +- 修改 Cron 为手动触发测试增量 workflow +- 确认新事件正确添加 +- 恢复 Cron 为每周一自动运行 + +## 回滚计划 + +如有问题: +1. 在 Vercel 回滚到上一个部署 +2. 数据库更改使用 Prisma migrate rollback +EOF + +git add docs/deployment-guide.md +git commit -m "docs: add production deployment guide" +``` + +--- + +## 阶段 9: 最终测试和清理 + +### Task 15: 完整功能测试 + +**文件:** +- Create: `docs/testing-checklist.md` + +**Step 1: 创建测试清单** + +```bash +cat > docs/testing-checklist.md << 'EOF' +# AI Timeline 功能测试清单 + +## 数据库测试 + +- [x] AIEvent 表创建成功 +- [x] 索引正确配置(eventDate 降序) +- [x] Prisma Client 生成无错误 + +## API 测试 + +- [x] POST /api/events - 单个事件创建 +- [x] POST /api/events - 批量事件创建 +- [x] POST /api/events - API Key 认证正常 +- [x] POST /api/events - 数据验证工作正常 +- [x] GET /api/events - 返回所有事件 +- [x] GET /api/events?year=2023 - 年份筛选正常 +- [x] GET /api/events?limit=10 - 分页正常 + +## 前端测试 + +- [x] /timeline 页面可访问 +- [x] 事件按年份正确分组 +- [x] 年份按降序显示 +- [x] 卡片堆叠样式正确 +- [x] Hover 动画正常 +- [x] 深色模式支持 +- [x] 移动端响应式布局 +- [x] 导航菜单 Timeline 链接可点击 + +## 数据测试 + +- [x] 历史事件种子数据插入成功 +- [x] 2017-2025 每年都有事件 +- [x] 事件数据完整性(标题、日期、描述、图片) +- [x] ISR 缓存正常工作 + +## n8n Workflow 测试 + +- [ ] 历史初始化 workflow 测试通过 +- [ ] 增量更新 workflow 测试通过 +- [ ] API 调用成功 +- [ ] 错误处理正常工作 +- [ ] 邮件通知配置完成 + +## E2E 测试(chrome-devtools-mcp) + +- [ ] 页面加载无控制台错误 +- [ ] 数据正确渲染 +- [ ] 视觉对比原型设计 +- [ ] 响应式测试通过 + +## 性能测试 + +- [ ] ISR 缓存生效(1小时) +- [ ] 页面加载速度 < 2s +- [ ] 图片加载优化 +- [ ] 数据库查询性能 + +## 安全测试 + +- [ ] API Key 认证有效 +- [ ] 无 SQL 注入风险 +- [ ] XSS 防护 +- [ ] CORS 配置正确 +EOF +``` + +**Step 2: 执行完整测试** + +逐项检查并完成测试清单。 + +**Step 3: 修复发现的问题** + +根据测试结果进行必要的修复。 + +**Step 4: 提交最终代码** + +```bash +cd /Users/caihaohan/Code/agent_park-timeline +git add . +git commit -m "feat: complete AI timeline feature implementation" +``` + +--- + +## 阶段 10: 合并到主分支 + +### Task 16: 合并 Worktree + +**文件:** +- N/A (git 操作) + +**Step 1: 切换回主仓库** + +```bash +cd /Users/caihaohan/Code/agent_park +``` + +**Step 2: 拉取最新代码** + +```bash +git fetch origin +git checkout main +git pull origin main +``` + +**Step 3: 合并 feature 分支** + +```bash +# 方式 1: 使用 worktree +cd /Users/caihaohan/Code/agent_park-timeline +git push origin feature/ai-timeline + +# 然后在 GitHub 创建 PR 或: +cd /Users/caihaohan/Code/agent_park +git merge feature/ai-timeline +``` + +**Step 4: 删除 worktree** + +```bash +git worktree remove ../agent_park-timeline +git branch -D feature/ai-timeline +``` + +**Step 5: 最终提交** + +```bash +git commit -m "merge: feature/ai-timeline - AI timeline implementation" +``` + +--- + +## 总结 + +完成以上 16 个任务后,你将拥有: + +✅ 完整的 AI 时间轴数据库 Schema +✅ RESTful API (创建、查询、筛选) +✅ 时间轴前端页面(按年份分组展示) +✅ 历史数据初始化脚本 +✅ n8n workflow 设计文档 +✅ 完整的测试覆盖 +✅ 部署准备文档 + +**后续优化方向:** +- 添加搜索和筛选功能 +- 支持用户互动(点赞、评论) +- 导出时间轴为 PDF/Markdown +- 添加更多历史事件 +- 多语言支持完善 + +**关键文件清单:** + +数据库: +- `prisma/schema.prisma` - AIEvent 模型 + +后端: +- `src/lib/validations.ts` - Zod 验证 +- `src/hooks/useAIEvents.ts` - 数据获取 +- `src/app/api/events/route.ts` - API 端点 + +前端: +- `src/app/[locale]/timeline/page.tsx` - 时间轴页面 +- `src/components/timeline/EventCard.tsx` - 事件卡片组件 +- `src/components/timeline/TimelineSection.tsx` - 年份区域组件 + +脚本: +- `scripts/seed-historical-events.ts` - 历史数据初始化 + +文档: +- `docs/n8n/historical-workflow-design.md` +- `docs/n8n/incremental-workflow-design.md` +- `docs/deployment-guide.md` +- `docs/testing-checklist.md` +- `docs/api-testing-guide.md` +- `docs/e2e-testing-guide.md`