49 KiB
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: 创建隔离的工作空间
cd /Users/caihaohan/Code/agent_park
git worktree add ../agent_park-timeline main -b feature/ai-timeline
cd ../agent_park-timeline
Step 2: 验证 worktree 创建成功
pwd
# Expected output: /Users/caihaohan/Code/agent_park-timeline
git branch
# Expected output: * feature/ai-timeline
Step 3: 安装依赖(如果需要)
pnpm install
Step 4: 启动开发服务器
# 先检查是否有进程运行在 3000 端口
lsof -ti:3000 | xargs kill -9 2>/dev/null || true
pnpm dev
Step 5: 验证服务器启动成功
访问: http://localhost:3000 Expected: Agent Park 首页正常显示
Step 6: 提交 worktree 初始化
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 文件
vim prisma/schema.prisma
# 或使用你喜欢的编辑器
Step 2: 在文件末尾添加 AIEvent 模型
在最后一个 } 后面添加:
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: 验证语法正确
pnpm prisma validate
Expected: The schema is valid
Step 5: 生成并运行迁移
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
pnpm prisma generate
Expected: Prisma Client generated successfully
Step 7: 提交
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: 打开验证文件
vim src/lib/validations.ts
Step 2: 找到文件末尾(在 export { ... } 之前)
使用 /ProjectQuerySchema 搜索到相关位置,然后在后面添加。
Step 3: 添加 AIEvent 相关的 Zod Schema
在 ProjectQuerySchema 定义后添加:
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 { 行,添加:
export {
// ... 现有的 exports
AIEventInputSchema,
AIEventQuerySchema,
};
Step 5: 保存文件
**:wq`
Step 6: 验证 TypeScript 编译通过
pnpm tsc --noEmit
Expected: 无错误输出
Step 7: 提交
git add src/lib/validations.ts
git commit -m "feat: add AIEvent validation schemas"
Task 3: 编写验证测试
文件:
- Create:
src/lib/validations.test.ts
Step 1: 创建测试文件
vim src/lib/validations.test.ts
Step 2: 编写测试
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: 运行测试验证失败
pnpm test src/lib/validations.test.ts
Expected: 全部通过
Step 5: 提交
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: 创建文件
vim src/hooks/useAIEvents.ts
Step 2: 编写数据获取函数
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<number>();
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 类型正确
pnpm tsc --noEmit
Expected: 无错误
Step 5: 提交
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: 创建目录
mkdir -p src/app/api/events
Step 2: 创建路由文件
vim src/app/api/events/route.ts
Step 3: 实现 POST handler
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 编译
pnpm tsc --noEmit
Step 6: 提交
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: 创建测试文件
vim src/app/api/events/route.test.ts
Step 2: 编写测试
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: 运行测试
pnpm test src/app/api/events/route.test.ts
Expected: 基础测试通过(需要 API Key 的测试会失败)
Step 5: 提交
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 已设置
# 检查 .env.local
cat .env.local | grep WEBHOOK_API_KEY
如果不存在,添加:
echo "WEBHOOK_API_KEY=test-key-for-development-only" >> .env.local
Step 2: 测试 GET endpoint
curl http://localhost:3000/api/events
Expected:
{
"events": []
}
Step 3: 测试 POST endpoint(创建单个事件)
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:
{
"created": 1,
"total": 1
}
Step 4: 验证事件已创建
curl http://localhost:3000/api/events
Expected: 返回刚才创建的事件
Step 5: 测试批量创建
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:
{
"created": 2,
"total": 2
}
Step 6: 测试年份筛选
curl "http://localhost:3000/api/events?year=2018"
Expected: 只返回 2018 年的事件
Step 7: 提交 API 测试说明文档
# 创建 API 测试文档
cat > docs/api-testing-guide.md << 'EOF'
# API 测试指南
## GET /api/events
获取所有事件:
```bash
curl http://localhost:3000/api/events
筛选特定年份:
curl "http://localhost:3000/api/events?year=2024"
限制返回数量:
curl "http://localhost:3000/api/events?limit=10"
POST /api/events
创建单个事件:
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"
}'
批量创建事件:
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: 创建页面文件
vim src/app/\[locale\]/timeline/page.tsx
Step 3: 实现页面组件
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<Metadata> {
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<number, typeof events>);
// 按年份降序排序
const sortedYears = Object.keys(eventsByYear)
.map(Number)
.sort((a, b) => b - a);
if (events.length === 0) {
return (
<div className="min-h-screen bg-background-light dark:bg-background-dark flex items-center justify-center">
<div className="text-center">
<h1 className="font-display font-black text-4xl mb-4">
暂无数据
</h1>
<p className="font-mono text-gray-600">
时间轴数据正在收集中...
</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-background-light dark:bg-background-dark">
{/* Header */}
<header className="pt-32 pb-16 px-4 max-w-[1600px] mx-auto">
<div className="text-center">
<h1 className="font-display font-black text-6xl md:text-8xl tracking-tight leading-none text-black dark:text-white mb-6">
AI 发展时间轴
</h1>
<p className="font-mono text-lg md:text-xl max-w-2xl mx-auto bg-white dark:bg-black border border-black dark:border-white p-4 shadow-hard">
从 Transformer 到 AGI: 大语言模型的进化之路
</p>
</div>
</header>
{/* Timeline */}
<main className="px-4 md:px-12 max-w-[1600px] mx-auto pb-32">
{sortedYears.map((year, index) => (
<section
key={year}
className={`relative min-h-[500px] mb-32 flex ${
index % 2 === 0 ? 'flex-row' : 'flex-row-reverse'
}`}
>
{/* Year Label */}
<div
className={`absolute ${index % 2 === 0 ? 'left-0' : 'right-0'} -top-8 z-20`}
>
<div
className={`${
index % 2 === 0 ? 'bg-primary' : 'bg-secondary'
} border-4 border-black px-6 py-2 ${
index % 2 === 0 ? '-rotate-2' : 'rotate-2'
} shadow-hard`}
>
<span className="font-display font-black text-3xl md:text-5xl">
{year}
</span>
</div>
</div>
{/* Events Container */}
<div
className={`w-full ${
index % 2 === 0 ? 'pl-0 md:pl-8 pr-0 md:pr-32' : 'pl-0 md:pl-32 pr-0 md:pr-8'
} pt-20`}
>
<div className="flex flex-nowrap overflow-x-visible items-center justify-start">
{eventsByYear[year].map((event, eventIndex) => (
<div
key={event.id}
className="stack-card relative w-72 h-96 flex-shrink-0 bg-surface-light dark:bg-surface-dark border-4 border-black dark:border-white p-4 shadow-hard -mr-48 md:-mr-56 hover:z-50 hover:-translate-y-5 hover:scale-105 transition-all duration-300"
style={{
zIndex: Math.max(1, 50 - eventIndex * 10),
transform: `rotate(${(Math.random() - 0.5) * 6}deg)`,
}}
>
{/* Tape decoration */}
<div
className="tape absolute -top-3 left-1/2 -translate-x-1/2 w-20 h-6"
/>
{/* Image */}
<div className="h-40 bg-primary border-2 border-black dark:border-white mb-4 flex items-center justify-center overflow-hidden">
<img
src={event.imageUrl}
alt={event.title}
className="w-full h-full object-cover"
/>
</div>
{/* Content */}
<h3 className="font-display font-bold text-xl leading-none mb-2 uppercase">
{event.title}
</h3>
<p className="text-xs leading-snug opacity-80 line-clamp-4 mb-4">
{event.description}
</p>
{/* Date */}
<div className="absolute bottom-4 left-4 text-[10px] font-bold bg-black text-white px-2">
{new Date(event.eventDate).toLocaleDateString('zh-CN')}
</div>
{/* Source Link */}
{event.sourceUrl && (
<a
href={event.sourceUrl}
target="_blank"
rel="noopener noreferrer"
className="absolute bottom-4 right-4 text-[10px] font-bold underline"
>
来源 →
</a>
)}
</div>
))}
</div>
</div>
</section>
))}
</main>
</div>
);
}
Step 4: 保存文件
**:wq`
Step 5: 验证页面可访问
访问: http://localhost:3000/timeline
Expected: 显示时间轴页面,包含之前创建的测试事件
Step 6: 提交
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: 创建组件目录
mkdir -p src/components/timeline
Step 2: 创建 EventCard 组件
vim src/components/timeline/EventCard.tsx
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 (
<div
className="stack-card relative w-72 h-96 flex-shrink-0 bg-surface-light dark:bg-surface-dark border-4 border-black dark:border-white p-4 shadow-hard -mr-48 md:-mr-56 hover:z-50 hover:-translate-y-5 hover:scale-105 transition-all duration-300"
style={{
zIndex,
transform: `rotate(${rotation}deg)`,
}}
>
{/* Tape decoration */}
<div className="tape absolute -top-3 left-1/2 -translate-x-1/2 w-20 h-6" />
{/* Image */}
<div className="h-40 bg-primary border-2 border-black dark:border-white mb-4 flex items-center justify-center overflow-hidden">
<img
src={event.imageUrl}
alt={event.title}
className="w-full h-full object-cover"
/>
</div>
{/* Content */}
<h3 className="font-display font-bold text-xl leading-none mb-2 uppercase">
{event.title}
</h3>
<p className="text-xs leading-snug opacity-80 line-clamp-4 mb-4">
{event.description}
</p>
{/* Date */}
<div className="absolute bottom-4 left-4 text-[10px] font-bold bg-black text-white px-2">
{new Date(event.eventDate).toLocaleDateString('zh-CN')}
</div>
{/* Source Link */}
{event.sourceUrl && (
<a
href={event.sourceUrl}
target="_blank"
rel="noopener noreferrer"
className="absolute bottom-4 right-4 text-[10px] font-bold underline"
>
来源 →
</a>
)}
</div>
);
}
Step 3: 创建 TimelineSection 组件
vim src/components/timeline/TimelineSection.tsx
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 (
<section className={`relative min-h-[500px] mb-32 flex ${isEven ? 'flex-row' : 'flex-row-reverse'}`}>
{/* Year Label */}
<div className={`absolute ${isEven ? 'left-0' : 'right-0'} -top-8 z-20`}>
<div
className={`${isEven ? 'bg-primary' : 'bg-secondary'} border-4 border-black px-6 py-2 ${isEven ? '-rotate-2' : 'rotate-2'} shadow-hard`}
>
<span className="font-display font-black text-3xl md:text-5xl">
{year}
</span>
</div>
</div>
{/* Events Container */}
<div
className={`w-full ${isEven ? 'pl-0 md:pl-8 pr-0 md:pr-32' : 'pl-0 md:pl-32 pr-0 md:pr-8'} pt-20`}
>
<div className="flex flex-nowrap overflow-x-visible items-center justify-start">
{events.map((event, eventIndex) => (
<EventCard
key={event.id}
event={event}
index={eventIndex}
/>
))}
</div>
</div>
</section>
);
}
Step 4: 重构 Timeline Page 使用新组件
更新 src/app/[locale]/timeline/page.tsx:
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<Metadata> {
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<number, typeof events>);
const sortedYears = Object.keys(eventsByYear)
.map(Number)
.sort((a, b) => b - a);
if (events.length === 0) {
return (
<div className="min-h-screen bg-background-light dark:bg-background-dark flex items-center justify-center">
<div className="text-center">
<h1 className="font-display font-black text-4xl mb-4">
暂无数据
</h1>
<p className="font-mono text-gray-600">
时间轴数据正在收集中...
</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-background-light dark:bg-background-dark">
<header className="pt-32 pb-16 px-4 max-w-[1600px] mx-auto">
<div className="text-center">
<h1 className="font-display font-black text-6xl md:text-8xl tracking-tight leading-none text-black dark:text-white mb-6">
AI 发展时间轴
</h1>
<p className="font-mono text-lg md:text-xl max-w-2xl mx-auto bg-white dark:bg-black border border-black dark:border-white p-4 shadow-hard">
从 Transformer 到 AGI: 大语言模型的进化之路
</p>
</div>
</header>
<main className="px-4 md:px-12 max-w-[1600px] mx-auto pb-32">
{sortedYears.map((year, index) => (
<TimelineSection
key={year}
year={year}
events={eventsByYear[year]}
index={index}
/>
))}
</main>
</div>
);
}
Step 5: 提交
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 组件
vim src/components/layout/Header.tsx
Step 2: 找到导航链接部分
搜索 /projects 或导航相关的代码。
Step 3: 添加 Timeline 链接
在导航菜单中添加:
<Link
href="/timeline"
className="hover:text-primary transition-colors uppercase decoration-2 underline-offset-4 hover:underline"
>
Timeline
</Link>
Step 4: 保存并提交
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: 确保开发服务器运行
lsof -ti:3000 | xargs kill -9 2>/dev/null || true
pnpm dev
Step 2: 使用 chrome-devtools-mcp 工具测试
在另一个对话中执行以下操作(或记录为测试文档):
# 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: 提交测试结果
# 如果有修复
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 文档目录
mkdir -p docs/n8n
Step 2: 编写历史数据初始化 Workflow 设计
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: `<web-search-mcp-endpoint>`
- 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 设计
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: `<web-search-mcp-endpoint>`
- 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 设计文档
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:
mkdir -p scripts
vim scripts/seed-historical-events.ts
Step 2: 编写种子数据脚本
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: 运行种子脚本
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: 提交种子脚本
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: 验证所有环境变量
# 本地开发
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: 准备生产数据库迁移
# 生成迁移 SQL
pnpm prisma migrate diff \
--from-empty \
--to-schema-datamodel prisma/schema.prisma \
--script > migration.sql
Step 4: 提交部署准备文档
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: 创建测试清单
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: 提交最终代码
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: 切换回主仓库
cd /Users/caihaohan/Code/agent_park
Step 2: 拉取最新代码
git fetch origin
git checkout main
git pull origin main
Step 3: 合并 feature 分支
# 方式 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
git worktree remove ../agent_park-timeline
git branch -D feature/ai-timeline
Step 5: 最终提交
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.mddocs/n8n/incremental-workflow-design.mddocs/deployment-guide.mddocs/testing-checklist.mddocs/api-testing-guide.mddocs/e2e-testing-guide.md