Files
agent-park/docs/plans/2025-01-25-ai-timeline-implementation.md
T

2157 lines
49 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<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 类型正确**
```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<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: 提交**
```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 (
<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 组件**
```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 (
<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`:
```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<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: 提交**
```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
<Link
href="/timeline"
className="hover:text-primary transition-colors uppercase decoration-2 underline-offset-4 hover:underline"
>
Timeline
</Link>
```
**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: `<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 设计**
```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: `<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 设计文档**
```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 助手 ChatGPT5 天用户突破 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`