# 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`