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

13 KiB

AI 时间轴功能设计文档

创建日期: 2025-01-25 目标用户: 开发者和 AI 研究者 设计理念: 学术+产品平衡,聚焦大语言模型发展历程(从 2017 Transformer 开始)


1. 系统架构

核心组件

前端展示系统

  • 路由: src/app/[locale]/timeline/page.tsx
  • 渲染策略: ISR (1小时缓存)
  • 设计风格: Neo-brutalism,复用现有设计系统
  • 数据展示: 按年份分组的卡片堆叠时间轴

后端 API

  • POST /api/events - 创建事件(供 n8n 调用,需 API Key 认证)
  • GET /api/events - 获取所有事件(支持年份筛选)
  • GET /api/events/:id - 获取单个事件详情
  • PATCH /api/events/:id - 更新事件(可选)
  • DELETE /api/events/:id - 删除事件(可选)

数据采集系统 (n8n)

  • 历史数据初始化流程: 一次性运行,批量收集 2017-2025 事件
  • 增量更新流程: 每周一运行,收集最近 7 天新事件
  • 三 Agent 协作: 搜索 → 筛选 → 格式化 → HTTP Request 提交

数据库

  • AIEvent 表: 存储所有事件
  • 无需标签系统(基础版数据结构)

2. 数据库 Schema

Prisma 模型

model AIEvent {
  id          String   @id @default(cuid())
  title       String
  titleEn     String?  // 英文标题(可选)
  eventDate   DateTime // 事件发生的具体日期
  description String
  descriptionEn String? // 英文描述
  imageUrl    String   // 事件配图 URL
  sourceUrl   String?  // 原始来源链接

  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  @@index([eventDate(sort: Desc)]) // 按日期降序索引
  @@index([createdAt])
}

字段说明

  • 国际化字段: title/titleEn, description/descriptionEn 与项目表保持一致
  • 日期字段: 使用 DateTime 支持时间范围筛选和排序
  • 索引优化: eventDate 降序索引提升"最近事件"查询性能
  • 图片存储: 直接存储 URL(可以是 Unsplash、项目截图等)

迁移命令

pnpm prisma migrate dev --name add_ai_events_table

3. API 端点设计

数据验证 Schema

src/lib/validations.ts 新增:

const AIEventInputSchema = z.object({
  title: z.string().min(1).max(200),
  titleEn: z.string().max(200).optional(),
  eventDate: z.string().datetime(), // ISO 8601 格式
  description: z.string().min(10).max(500),
  descriptionEn: z.string().max(500).optional(),
  imageUrl: z.string().url(),
  sourceUrl: z.string().url().optional(),
});

POST /api/events (n8n 调用)

认证:

const apiKey = request.headers.get('X-API-Key');
if (!apiKey || !crypto.timingSafeEqual(
  Buffer.from(apiKey),
  Buffer.from(process.env.WEBHOOK_API_KEY!)
)) {
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

验证:

const body = await request.json();
const validationResult = AIEventInputSchema.array().safeParse(body);
if (!validationResult.success) {
  return NextResponse.json({
    error: 'Validation failed',
    details: validationResult.error
  }, { status: 400 });
}

创建:

const events = await prisma.aIEvent.createMany({
  data: validationResult.data,
  skipDuplicates: true,
});
return NextResponse.json({ created: events.count }, { status: 201 });

GET /api/events (前端调用)

支持查询参数:

  • ?year=2024 - 筛选特定年份
  • ?limit=50 - 限制返回数量
  • ?offset=0 - 分页偏移

返回按 eventDate 降序排列的事件列表。

调用示例

curl -X POST https://your-domain.com/api/events \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_WEBHOOK_API_KEY" \
  -d '{
    "title": "GPT-4 发布",
    "eventDate": "2023-03-14T00:00:00Z",
    "description": "OpenAI 发布多模态大语言模型",
    "imageUrl": "https://example.com/gpt4.jpg"
  }'

4. 前端页面实现

页面结构

src/app/[locale]/timeline/page.tsx:

import { getAIEvents } from '@/hooks/useAIEvents';
import { TimelineSection } from '@/components/timeline/TimelineSection';

export const revalidate = 3600; // ISR 1小时

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>);

  return (
    <div className="timeline-container">
      <header>
        <h1>AI 发展时间轴</h1>
      </header>

      <main>
        {Object.entries(eventsByYear)
          .sort(([a], [b]) => Number(b) - Number(a)) // 降序
          .map(([year, yearEvents]) => (
            <TimelineSection
              key={year}
              year={year}
              events={yearEvents}
            />
          ))}
      </main>
    </div>
  );
}

数据获取函数

src/hooks/useAIEvents.ts:

export async function getAIEvents() {
  const events = await prisma.aIEvent.findMany({
    orderBy: { eventDate: 'desc' },
    take: 100, // 最多 100 条
  });
  return events;
}

组件设计

  • TimelineSection: 渲染单个年份的卡片堆叠区域,复用原型的动画效果
  • EventCard: 单个事件卡片(图片、标题、描述、日期)
  • 响应式: 移动端垂直堆叠,桌面端横向卡片堆叠

5. n8n Workflow 设计

流程 A: 历史数据初始化(一次性运行)

1. 历史搜索 Agent

  • 按年份批量搜索: 2017-2025
  • 搜索关键词: "AI breakthrough YEAR", "LLM release YEAR", "GPT model YEAR"
  • 时间范围: 每年 1月1日 - 12月31日
  • 输出: 每年 50-100 条搜索结果

2. 历史筛选 Agent

  • 权威来源: ['arxiv.org', 'openai.com', 'anthropic.com', 'google.ai', 'meta.ai', 'deepmind.com']
  • 去重逻辑: 相同标题或 URL 只保留一个
  • 输出: 每年精选 10-20 个事件

3. 历史格式化 Agent

  • 提取完整信息: 标题、准确日期、详细描述、配图
  • 对描述进行润色和本地化(翻译成中文)
  • 补充缺失图片(使用 Unsplash 或项目官网截图)

4. HTTP Request: 批量提交

  • POST /api/events
  • 每次提交 10-20 个事件(按年份分批)
  • 记录成功/失败状态

流程 B: 增量更新(每周运行)

触发器: Cron node,每周一早上 9:00

1. 增量搜索 Agent

  • 时间范围: 最近 7 天
  • 搜索关键词: ["AI news", "LLM release", "model launch"]
  • 输出: 10-20 条最新搜索结果

2. 增量筛选 Agent

  • 权威来源检查(同历史流程)
  • 额外检查: 查询数据库避免重复 sourceUrl
  • 输出: 2-5 个新事件

3. 增量格式化 Agent

  • 快速格式化: 提取核心信息
  • 描述保持简短(直接使用摘要)
  • 图片优先使用新闻配图

4. HTTP Request: 增量提交

  • POST /api/events
  • 一次性提交所有新事件
  • 失败时发送告警邮件

错误处理

  • 重试策略: 每个 Agent 失败后等待 5s/10s/20s 重试,最多 3 次
  • 降级处理: 图片提取失败使用默认图片; 日期模糊使用当月 1 日
  • 告警机制: 最终失败时发送邮件并附执行日志
  • 部分成功: 批量提交时即使部分失败,也记录成功的事件

6. 数据验证和错误处理

API 端点保护

认证机制: 使用 crypto.timingSafeEqual 防止时序攻击

请求体验证: Zod schema 验证所有字段

去重逻辑: 基于 sourceUrl 的唯一索引(如果提供)

长度限制:

  • 标题 ≤200 字符
  • 描述 ≤500 字符

必填字段: title, eventDate, description, imageUrl

URL 验证: imageUrlsourceUrl 必须是有效的 HTTP/HTTPS URL

前端错误边界

export default async function TimelinePage() {
  try {
    const events = await getAIEvents();
    if (!events.length) {
      return <EmptyState message="暂无事件数据" />;
    }
    return <TimelineContent events={events} />;
  } catch (error) {
    console.error('Failed to load events:', error);
    return <ErrorState message="加载失败,请稍后重试" />;
  }
}

7. 测试策略

单元测试 (Vitest)

API 端点测试 (src/app/api/events/route.test.ts):

describe('POST /api/events', () => {
  it('should create event with valid data', async () => {
    const response = await POST(request_mock);
    expect(response.status).toBe(201);
  });

  it('should reject invalid API key', async () => {
    const response = await POST(bad_request_mock);
    expect(response.status).toBe(401);
  });

  it('should validate required fields', async () => {
    const response = await POST(invalid_data_mock);
    expect(response.status).toBe(400);
  });
});

数据验证测试 (src/lib/validations.test.ts):

describe('AIEventInputSchema', () => {
  it('should validate valid event', () => {
    expect(() => AIEventInputSchema.parse(validEvent)).not.toThrow();
  });

  it('should reject missing title', () => {
    expect(() => AIEventInputSchema.parse({ ...validEvent, title: '' }))
      .toThrow();
  });
});

E2E 测试 (chrome-devtools-mcp)

测试流程:

  1. 启动开发服务器: pnpm dev
  2. 使用 chrome-devtools-mcp 工具:
    • new_page: http://localhost:3000/timeline
    • take_snapshot: 验证页面结构
    • take_screenshot: 对比设计原型
    • evaluate_script: 检查事件数据渲染
    • list_console_messages: 确保无 JavaScript 错误
  3. 测试筛选功能:
    • navigate_page: url=/timeline?year=2024
    • take_snapshot: 验证只显示 2024 年事件

手动测试清单:

  • 页面加载成功,无 console 错误
  • 事件按年份正确分组显示
  • 卡片堆叠动画正常工作
  • 响应式布局在移动端正常
  • ISR 缓存在 1 小时后正确更新

集成测试 (n8n Workflow)

  • 手动触发历史数据初始化流程,验证数据库事件创建
  • 手动触发增量更新流程,验证新事件添加
  • 测试错误场景: API 不可用、数据格式错误、网络超时

测试数据准备

使用 pnpm prisma db seed 创建种子数据:

  • Seed 脚本插入 5-10 个示例事件(2023-2025 真实事件)
  • 方便开发和手动测试

8. 部署和实施步骤

阶段 1: 数据库和后端 (1-2 天)

# 更新 schema
vim prisma/schema.prisma

# 生成并运行迁移
pnpm prisma migrate dev --name add_ai_events_table
pnpm prisma generate

# 创建验证 schema
vim src/lib/validations.ts

# 创建 API route
mkdir src/app/api/events
vim src/app/api/events/route.ts

# 创建数据获取函数
vim src/hooks/useAIEvents.ts

# 手动测试 API
curl -X POST http://localhost:3000/api/events \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $WEBHOOK_API_KEY" \
  -d '{
    "title": "测试事件",
    "eventDate": "2025-01-25T00:00:00Z",
    "description": "这是一个测试事件",
    "imageUrl": "https://example.com/image.jpg"
  }'

阶段 2: 前端页面 (2-3 天)

# 创建 timeline 页面
mkdir -p src/app/[locale]/timeline
vim src/app/[locale]/timeline/page.tsx

# 创建组件
mkdir src/components/timeline
vim src/components/timeline/TimelineSection.tsx
vim src/components/timeline/EventCard.tsx

# 更新 header 添加导航链接
vim src/components/layout/Header.tsx

# 使用 chrome-devtools-mcp 测试页面效果

阶段 3: n8n Workflow 配置 (1-2 天)

  1. 在 n8n 中创建历史数据初始化 workflow
  2. 创建增量更新 workflow,设置每周一 9:00 触发
  3. 配置环境变量 WEBHOOK_API_KEY
  4. 手动运行历史 workflow,初始化 2017-2025 数据
  5. 验证增量 workflow 是否正常工作

阶段 4: 测试和优化 (1 天)

  1. 使用 chrome-devtools-mcp 进行完整测试
  2. 修复发现的问题
  3. 优化性能(ISR 缓存、图片加载)
  4. 准备生产环境部署

阶段 5: 部署到生产

  1. 更新 Vercel 环境变量(确保 WEBHOOK_API_KEY 已设置)
  2. 运行数据库迁移(如果需要): pnpm prisma db push
  3. 部署代码到 Vercel
  4. 在 n8n 中更新 API endpoint 为生产地址
  5. 监控第一次增量运行

9. 关键设计决策

为什么选择基础版事件数据?

  • 符合 YAGNI 原则,快速上线
  • 避免过度设计,聚焦核心价值
  • 后续可扩展(如添加用户互动、点赞等)

为什么不需要标签系统?

  • 从 UI 原型看,卡片只显示核心信息(标题、日期、描述、图片)
  • 时间轴本身就是按年份组织,无需额外分类
  • 减少数据复杂度,提升性能

为什么设计两套 n8n 流程?

  • 历史流程: 注重数据质量和完整性,处理 2017-2025 的大量数据
  • 增量流程: 注重效率和及时性,每周自动更新
  • 分离关注点,便于独立优化和调试

为什么使用 ISR 而非纯静态?

  • 事件数据每周更新,需要一定时效性
  • ISR 1小时缓存平衡了性能和新鲜度
  • 避免每次请求都查询数据库

10. 后续优化方向

  1. 搜索和筛选: 添加关键词搜索、标签筛选
  2. 用户互动: 点赞、评论、分享功能
  3. 相关推荐: 基于事件标签推荐相关项目
  4. 多语言支持: 完善英文版本的描述和翻译
  5. 图片优化: 使用 Next.js Image 组件优化图片加载
  6. 数据可视化: 添加图表展示 AI 发展趋势
  7. 导出功能: 支持导出时间轴为 PDF/Markdown