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

49 KiB
Raw Blame History

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 助手 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: 运行种子脚本

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.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