refactor: 移除 AI 词云、AI 时间轴和博客模块
暂停开发以下功能,将设计文档移至未完成计划文件夹: - AI 词云 (Keyword Cloud): 前端、API、数据库模型、n8n 工作流 - AI 时间轴 (AI Timeline): 前端、API、数据库模型 - 博客 (Blog): 导航链接占位 变更内容: - 删除词云和时间轴的前端页面及组件 - 删除 /api/keyword-cloud/* 和 /api/events/* API 端点 - 从 prisma/schema.prisma 移除 Keyword, Quarter, VisualStyleRule, KeywordCloudErrorLog, AIEvent 模型 - 从 validations.ts 移除相关 Zod Schema - 从国际化消息中移除 keywordCloud/timeline 命名空间 - 从导航菜单移除词云、时间轴、博客链接 - 更新 CLAUDE.md 移除词云系统文档 设计文档已移至 .omc/plans/postponed-features/ 供后续恢复开发参考
This commit is contained in:
@@ -1,494 +0,0 @@
|
||||
# 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 模型
|
||||
|
||||
```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、项目截图等)
|
||||
|
||||
### 迁移命令
|
||||
```bash
|
||||
pnpm prisma migrate dev --name add_ai_events_table
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. API 端点设计
|
||||
|
||||
### 数据验证 Schema
|
||||
|
||||
在 `src/lib/validations.ts` 新增:
|
||||
|
||||
```typescript
|
||||
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 调用)
|
||||
|
||||
**认证**:
|
||||
```typescript
|
||||
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 });
|
||||
}
|
||||
```
|
||||
|
||||
**验证**:
|
||||
```typescript
|
||||
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 });
|
||||
}
|
||||
```
|
||||
|
||||
**创建**:
|
||||
```typescript
|
||||
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` 降序排列的事件列表。
|
||||
|
||||
### 调用示例
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```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`:
|
||||
|
||||
```typescript
|
||||
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 验证**: `imageUrl` 和 `sourceUrl` 必须是有效的 HTTP/HTTPS URL
|
||||
|
||||
### 前端错误边界
|
||||
|
||||
```tsx
|
||||
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`):
|
||||
```typescript
|
||||
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`):
|
||||
```typescript
|
||||
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 天)
|
||||
|
||||
```bash
|
||||
# 更新 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 天)
|
||||
|
||||
```bash
|
||||
# 创建 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
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,716 +0,0 @@
|
||||
# 季度 AI 热点词云系统设计文档
|
||||
|
||||
**创建日期**: 2026-01-25
|
||||
**功能类型**: 数据驱动可视化
|
||||
**技术栈**: n8n + Next.js + PostgreSQL + Prisma
|
||||
|
||||
---
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
季度 AI 热点词云是一个**自动化数据驱动的可视化词云系统**,通过 n8n 工作流从 Google Trends 采集 AI 相关热点词汇,经 AI 清洗和规则引擎处理后,自动入库并在前端展示。
|
||||
|
||||
### 核心目标
|
||||
|
||||
1. **内容营销**: 吸引访客回访查看每季度更新,提供社交分享素材
|
||||
2. **教育参考**: 帮助新手理解 AI 技术演进历程
|
||||
3. **数据洞察**: 展示 AI 领域热点变化趋势
|
||||
|
||||
### 用户旅程
|
||||
|
||||
1. 用户访问 `/keyword-cloud` 页面
|
||||
2. 看到当前季度的 AI 热点词云(如 2024-Q1)
|
||||
3. 鼠标悬停在词汇上,查看详细描述和要点
|
||||
4. 点击左右箭头切换不同季度,浏览历史热点
|
||||
5. 可视化展示:词汇大小代表搜索热度,颜色代表分类
|
||||
|
||||
---
|
||||
|
||||
## 二、系统架构
|
||||
|
||||
### 架构图
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌──────────────┐
|
||||
│ Google │ │ n8n │ │ PostgreSQL │
|
||||
│ Trends API │───▶│ Workflow │───▶│ Database │
|
||||
└─────────────┘ └─────────────┘ └──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ Next.js │
|
||||
│ Frontend │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
### 数据流向
|
||||
|
||||
1. **定时触发**: n8n Schedule 每季度末自动执行
|
||||
2. **数据采集**: Google Trends API 获取热门搜索词
|
||||
3. **AI 清洗**: 过滤无关词汇,生成描述和要点
|
||||
4. **规则匹配**: 根据热度分数分配视觉样式
|
||||
5. **数据入库**: 写入 PostgreSQL 数据库
|
||||
6. **前端展示**: Next.js 从数据库读取并渲染词云
|
||||
|
||||
---
|
||||
|
||||
## 三、数据库模型
|
||||
|
||||
### 3.1 Quarter 表(季度元数据)
|
||||
|
||||
```prisma
|
||||
model Quarter {
|
||||
id Int @id @default(autoincrement())
|
||||
quarter String @unique // "2023-Q1", "2023-Q2"
|
||||
title String @db.Text // "2023年第一季度"
|
||||
titleEn String? @db.Text // "Q1 2023"
|
||||
subtitle String? @db.Text // "聊天界面的黎明"
|
||||
subtitleEn String? @db.Text // "The dawn of chat interface"
|
||||
displayOrder Int @default(0) // 前端排序
|
||||
isActive Boolean @default(true) // 是否显示
|
||||
keywords Keyword[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([quarter])
|
||||
@@index([displayOrder])
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Keyword 表(关键词核心数据)
|
||||
|
||||
```prisma
|
||||
model Keyword {
|
||||
id Int @id @default(autoincrement())
|
||||
word String // "ChatGPT"
|
||||
trendScore Int // 0-100, 从 Google Trends 获取
|
||||
|
||||
// 外键关联
|
||||
quarterId Int
|
||||
quarter Quarter @relation(fields: [quarterId], references: [id], onDelete: Cascade)
|
||||
|
||||
// 内容字段(支持中英双语)
|
||||
description String @db.Text // AI 生成的一句话描述
|
||||
descriptionEn String? @db.Text // 英文描述
|
||||
detailPoints Json // JSON 数组: ["要点1", "要点2", "要点3"]
|
||||
detailPointsEn Json? // 英文版要点
|
||||
|
||||
// 视觉样式配置
|
||||
visualConfig Json // {color, size, rotation, border}
|
||||
|
||||
// 元数据
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([quarterId])
|
||||
@@index([trendScore])
|
||||
@@index([word])
|
||||
}
|
||||
```
|
||||
|
||||
**visualConfig 字段结构示例**:
|
||||
```json
|
||||
{
|
||||
"color": "secondary",
|
||||
"size": "text-5xl",
|
||||
"rotation": "rotate-1",
|
||||
"border": "border-4"
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 VisualStyleRule 表(视觉样式规则配置)
|
||||
|
||||
```prisma
|
||||
model VisualStyleRule {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique // "热门大词-金色"
|
||||
|
||||
// 分数区间
|
||||
minScore Int // 90
|
||||
maxScore Int // 100
|
||||
|
||||
// 视觉属性
|
||||
color String // "primary", "secondary", "accent"
|
||||
size String // "text-5xl", "text-3xl", "text-xl"
|
||||
border String // "border-4", "border-2"
|
||||
rotation String? // "rotate-1", "rotate-2", null
|
||||
|
||||
// 控制
|
||||
priority Int @default(0) // 优先级,分数重叠时按优先级
|
||||
enabled Boolean @default(true) // 是否启用
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([enabled])
|
||||
@@index([minScore, maxScore])
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 KeywordCloudErrorLog 表(错误日志)
|
||||
|
||||
```prisma
|
||||
model KeywordCloudErrorLog {
|
||||
id Int @id @default(autoincrement())
|
||||
quarter String // "2023-Q1"
|
||||
keyword String? // "ChatGPT"
|
||||
errorType String // "INVALID_DATA", "API_ERROR", "DB_ERROR"
|
||||
errorMessage String @db.Text // 详细错误信息
|
||||
rawData Json? // 原始数据便于调试
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([quarter])
|
||||
@@index([errorType])
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、n8n 工作流设计
|
||||
|
||||
### 4.1 工作流概览
|
||||
|
||||
5 个核心节点实现从数据采集到入库的完整流程。
|
||||
|
||||
### 4.2 节点详细配置
|
||||
|
||||
#### 节点 1: Schedule Trigger(定时触发)
|
||||
|
||||
- **类型**: `n8n-nodes-base.scheduleTrigger`
|
||||
- **Cron 表达式**: `0 0 23 28-31 * *` (每季度末最后一天的 23:00)
|
||||
- **月份判断**: Function 节点检查当前月份(3/6/9/12),如果不是则跳过
|
||||
- **输出**: 当前季度的标识(如 "2024-Q1")
|
||||
|
||||
#### 节点 2: Google Trends 采集
|
||||
|
||||
- **类型**: `@gamal.dev/n8n-nodes-google-trends`
|
||||
- **配置参数**:
|
||||
- `keywords`: ["AI", "artificial intelligence", "machine learning", "GPT", "LLM"]
|
||||
- `timeRange`: 当前季度的 3 个月(如 2024-01-01 to 2024-03-31)
|
||||
- `category`: "Science > Computer Science > AI"
|
||||
- `geo`: "GB"
|
||||
- **输出示例**:
|
||||
```json
|
||||
{
|
||||
"keyword": "ChatGPT",
|
||||
"trendScore": 95,
|
||||
"rising": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 节点 3: AI 清洗和内容生成
|
||||
|
||||
- **类型**: `@n8n/n8n-nodes-langchain.lmChatChain`
|
||||
- **Prompt 模板**:
|
||||
```
|
||||
你是一个 AI 领域专家。以下是 Google Trends 采集的热门关键词列表:
|
||||
{{ $json.all() }}
|
||||
|
||||
任务:
|
||||
1. 过滤掉与 AI/机器学习无关的关键词
|
||||
2. 为每个关键词生成中文描述(10-50字)
|
||||
3. 生成 3 条详细要点(每条 10-30 字,客观描述,避免营销用语)
|
||||
|
||||
输出格式(JSON 数组):
|
||||
[
|
||||
{
|
||||
"word": "ChatGPT",
|
||||
"trendScore": 95,
|
||||
"description": "OpenAI 开发的对话式人工智能助手",
|
||||
"detailPoints": ["支持多轮对话", "基于 GPT-3.5 架构", "2023年用户突破1亿"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### 节点 4: 规则引擎匹配
|
||||
|
||||
- **类型**: `n8n-nodes-base.code`
|
||||
- **逻辑**:
|
||||
1. HTTP Request 获取 `VisualStyleRule` 表数据
|
||||
- 方法: GET
|
||||
- URL: `{{ $env.API_URL }}/api/keyword-cloud/rules`
|
||||
2. 按 `priority` 排序规则
|
||||
3. 遍历每个关键词,匹配第一个符合的规则(`minScore <= trendScore <= maxScore`)
|
||||
4. 将视觉配置注入数据
|
||||
|
||||
#### 节点 5: 批量写入数据库
|
||||
|
||||
- **类型**: `n8n-nodes-base.httpRequest`
|
||||
- **方法**: POST
|
||||
- **URL**: `{{ $env.API_URL }}/api/keyword-cloud/keywords`
|
||||
- **认证**: Bearer Token(环境变量 `API_KEY`)
|
||||
- **Body**:
|
||||
```json
|
||||
{
|
||||
"quarter": "2024-Q1",
|
||||
"keywords": [
|
||||
{
|
||||
"word": "ChatGPT",
|
||||
"trendScore": 95,
|
||||
"description": "...",
|
||||
"detailPoints": ["...", "...", "..."],
|
||||
"visualConfig": {...}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
- **批量处理**: 超过 20 个关键词时分批提交
|
||||
|
||||
### 4.3 错误处理
|
||||
|
||||
- 每个节点设置 `continueOnFail: true`
|
||||
- 错误日志写入 `KeywordCloudErrorLog` 表
|
||||
- 关键错误发送通知(Email/Slack)
|
||||
|
||||
---
|
||||
|
||||
## 五、API 端点设计
|
||||
|
||||
### 5.1 GET /api/keyword-cloud/quarters
|
||||
|
||||
获取季度列表。
|
||||
|
||||
**查询参数**:
|
||||
- `isActive` (可选): 只返回激活的季度
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"quarters": [
|
||||
{
|
||||
"id": 1,
|
||||
"quarter": "2024-Q1",
|
||||
"title": "2024年第一季度",
|
||||
"titleEn": "Q1 2024",
|
||||
"subtitle": "聊天界面的黎明",
|
||||
"subtitleEn": "The dawn of chat interface",
|
||||
"displayOrder": 0,
|
||||
"keywordCount": 15
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**排序**: 按 `displayOrder` ASC
|
||||
|
||||
### 5.2 GET /api/keyword-cloud/keywords/[quarter]
|
||||
|
||||
获取指定季度的关键词列表。
|
||||
|
||||
**路径参数**:
|
||||
- `quarter`: 季度标识(如 "2024-Q1")
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"quarter": "2024-Q1",
|
||||
"title": "2024年第一季度",
|
||||
"keywords": [
|
||||
{
|
||||
"id": 1,
|
||||
"word": "ChatGPT",
|
||||
"trendScore": 95,
|
||||
"description": "OpenAI 开发的对话式 AI 助手",
|
||||
"detailPoints": ["支持多轮对话", "基于 GPT-3.5", "2023用户破亿"],
|
||||
"visualConfig": {
|
||||
"color": "secondary",
|
||||
"size": "text-5xl",
|
||||
"border": "border-4",
|
||||
"rotation": "rotate-1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**排序**: 按 `trendScore` DESC
|
||||
|
||||
### 5.3 POST /api/keyword-cloud/keywords
|
||||
|
||||
n8n 工作流写入关键词数据。
|
||||
|
||||
**认证**: Bearer Token (WEBHOOK_API_KEY)
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"quarter": "2024-Q1",
|
||||
"keywords": [
|
||||
{
|
||||
"word": "ChatGPT",
|
||||
"trendScore": 95,
|
||||
"description": "...",
|
||||
"detailPoints": ["...", "...", "..."],
|
||||
"visualConfig": {...}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"created": 15,
|
||||
"failed": 2,
|
||||
"errors": [
|
||||
{ "word": "Invalid", "error": "trendScore out of range" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**逻辑**:
|
||||
1. 验证 API Key
|
||||
2. 查找或创建 `Quarter` 记录
|
||||
3. 批量创建 `Keyword` 记录(Prisma `createMany`)
|
||||
4. 失败记录写入 `KeywordCloudErrorLog` 表
|
||||
5. 返回成功/失败统计
|
||||
|
||||
### 5.4 GET /api/keyword-cloud/rules
|
||||
|
||||
获取视觉样式规则配置(n8n 规则引擎使用)。
|
||||
|
||||
**查询参数**:
|
||||
- `enabled` (可选): 只返回启用的规则
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "热门大词-金色",
|
||||
"minScore": 90,
|
||||
"maxScore": 100,
|
||||
"visualConfig": {
|
||||
"color": "primary",
|
||||
"size": "text-5xl",
|
||||
"border": "border-4",
|
||||
"rotation": "rotate-1"
|
||||
},
|
||||
"priority": 0,
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**排序**: 按 `priority` ASC
|
||||
|
||||
### 5.5 POST /api/keyword-cloud/rules
|
||||
|
||||
创建新的视觉样式规则(管理员功能)。
|
||||
|
||||
**请求体**: 同单个规则对象
|
||||
|
||||
**验证**:
|
||||
- `minScore < maxScore`
|
||||
- 必填字段检查
|
||||
- 颜色值必须是预定义的颜色类别
|
||||
|
||||
---
|
||||
|
||||
## 六、前端组件设计
|
||||
|
||||
### 6.1 路由结构
|
||||
|
||||
```
|
||||
src/app/[locale]/keyword-cloud/
|
||||
├── page.tsx # 主页面
|
||||
└── components/
|
||||
├── KeywordCloud.tsx # 词云容器组件
|
||||
├── CloudWord.tsx # 单个词汇组件
|
||||
├── QuarterNavigator.tsx # 季度切换导航
|
||||
├── WordPopover.tsx # 弹出框详情
|
||||
└── ProgressIndicator.tsx # 进度条
|
||||
```
|
||||
|
||||
### 6.2 核心组件
|
||||
|
||||
#### KeywordCloud.tsx
|
||||
|
||||
词云容器组件,负责数据获取和布局。
|
||||
|
||||
```typescript
|
||||
interface KeywordCloudProps {
|
||||
quarter: string;
|
||||
}
|
||||
|
||||
function KeywordCloud({ quarter }: KeywordCloudProps) {
|
||||
const { data, isLoading } = useKeywordData(quarter);
|
||||
|
||||
return (
|
||||
<div className="bg-white/50 dark:bg-black/20 backdrop-blur-sm border-4 border-black p-8 md:p-12 shadow-hard">
|
||||
<ProgressIndicator currentQuarter={quarter} totalQuarters={4} />
|
||||
<QuarterNavigator current={quarter} />
|
||||
<div className="word-cluster py-12">
|
||||
{data?.keywords.map((keyword) => (
|
||||
<CloudWord key={keyword.id} data={keyword} />
|
||||
))}
|
||||
</div>
|
||||
<FunFactBubble fact={data.funFact} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### CloudWord.tsx
|
||||
|
||||
单个词汇组件,应用视觉样式和悬停交互。
|
||||
|
||||
```typescript
|
||||
interface CloudWordProps {
|
||||
data: Keyword;
|
||||
}
|
||||
|
||||
function CloudWord({ data }: CloudWordProps) {
|
||||
const { word, visualConfig, description, detailPoints } = data;
|
||||
const { color, size, border, rotation } = visualConfig;
|
||||
const colorClass = colorMap[color];
|
||||
|
||||
return (
|
||||
<span className={cn(
|
||||
"cloud-word",
|
||||
border,
|
||||
"border-black",
|
||||
colorClass,
|
||||
"px-6 py-3",
|
||||
"rounded-full",
|
||||
size,
|
||||
"font-black",
|
||||
"shadow-hard",
|
||||
rotation,
|
||||
"transition-all",
|
||||
"hover:scale-105",
|
||||
"cursor-pointer"
|
||||
)}>
|
||||
{word}
|
||||
<WordPopover title={word} description={description} points={detailPoints} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### WordPopover.tsx
|
||||
|
||||
弹出框组件,显示词汇的详细信息。
|
||||
|
||||
```typescript
|
||||
interface WordPopoverProps {
|
||||
title: string;
|
||||
description: string;
|
||||
points: string[];
|
||||
}
|
||||
|
||||
function WordPopover({ title, description, points }: WordPopoverProps) {
|
||||
return (
|
||||
<div className="popover">
|
||||
<div className="bg-primary text-black font-display font-bold p-2 border-b-2 border-black text-sm uppercase">
|
||||
{title}
|
||||
</div>
|
||||
<div className="p-3 space-y-2 text-xs font-medium dark:text-gray-100">
|
||||
<div className="flex gap-2 items-start">
|
||||
<span>•</span>
|
||||
<span>{description}</span>
|
||||
</div>
|
||||
{points.map((point, i) => (
|
||||
<div key={i} className="flex gap-2 items-start">
|
||||
<span>•</span>
|
||||
<span>{point}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### QuarterNavigator.tsx
|
||||
|
||||
季度切换导航组件。
|
||||
|
||||
```typescript
|
||||
function QuarterNavigator({ current }: { current: string }) {
|
||||
const quarters = ["2023-Q1", "2023-Q2", "2023-Q3", "2023-Q4"];
|
||||
const currentIndex = quarters.indexOf(current);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-8">
|
||||
<button
|
||||
disabled={currentIndex === 0}
|
||||
className="nav-button"
|
||||
onClick={() => navigate(quarters[currentIndex - 1])}
|
||||
>
|
||||
<span className="material-symbols-outlined text-4xl">chevron_left</span>
|
||||
</button>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="bg-primary border-4 border-black px-10 py-4 shadow-hard font-display font-bold text-5xl">
|
||||
{current}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
disabled={currentIndex === quarters.length - 1}
|
||||
className="nav-button"
|
||||
onClick={() => navigate(quarters[currentIndex + 1])}
|
||||
>
|
||||
<span className="material-symbols-outlined text-4xl">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 数据获取
|
||||
|
||||
```typescript
|
||||
// src/hooks/useKeywordCloud.ts
|
||||
export async function getKeywordData(quarter: string) {
|
||||
const response = await fetch(`${API_URL}/api/keyword-cloud/keywords/${quarter}`);
|
||||
return response.json();
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 样式系统
|
||||
|
||||
复用项目现有的 Tailwind 配置和样式类:
|
||||
- `.cloud-word`: 词云词汇的基础样式
|
||||
- `.popover`: 弹出框样式(包括箭头)
|
||||
- `.word-cluster`: 词汇容器布局
|
||||
- `.nav-button`: 导航按钮样式
|
||||
|
||||
响应式断点:`md:`, `lg:`
|
||||
|
||||
---
|
||||
|
||||
## 七、错误处理和监控
|
||||
|
||||
### 7.1 分层错误处理
|
||||
|
||||
**n8n 工作流层**:
|
||||
- 每个节点 `continueOnFail: true`
|
||||
- 失败记录写入 `KeywordCloudErrorLog` 表
|
||||
- 关键错误发送通知
|
||||
|
||||
**API 层**:
|
||||
- Zod schema 验证
|
||||
- 部分成功响应模式
|
||||
- HTTP 状态码规范
|
||||
|
||||
**前端层**:
|
||||
- 友好的错误提示
|
||||
- Error Boundary
|
||||
- 重试机制
|
||||
|
||||
### 7.2 监控策略
|
||||
|
||||
- n8n 执行日志监控
|
||||
- 定期检查 `KeywordCloudErrorLog` 表
|
||||
- API 响应时间监控
|
||||
- 前端错误追踪
|
||||
|
||||
---
|
||||
|
||||
## 八、测试策略
|
||||
|
||||
### 8.1 单元测试(Vitest)
|
||||
|
||||
- API 路由处理逻辑
|
||||
- 规则引擎匹配算法
|
||||
- 数据验证 schemas
|
||||
|
||||
### 8.2 E2E 测试
|
||||
|
||||
- 季度切换功能
|
||||
- 弹出框交互
|
||||
- 响应式布局
|
||||
|
||||
### 8.3 n8n 工作流测试
|
||||
|
||||
- 使用测试环境 API 手动触发
|
||||
- 验证生成的数据质量
|
||||
- 检查规则匹配结果
|
||||
|
||||
---
|
||||
|
||||
## 九、部署指南
|
||||
|
||||
### 9.1 环境变量
|
||||
|
||||
```bash
|
||||
# .env.local
|
||||
DATABASE_URL="..."
|
||||
WEBHOOK_API_KEY="..."
|
||||
N8N_WEBHOOK_URL="https://your-n8n-instance.com/..."
|
||||
```
|
||||
|
||||
### 9.2 数据库迁移
|
||||
|
||||
```bash
|
||||
pnpm prisma migrate dev --name add_keyword_cloud_tables
|
||||
```
|
||||
|
||||
### 9.3 n8n 部署
|
||||
|
||||
1. 使用自托管 n8n 或 n8n Cloud
|
||||
2. 配置环境变量(API_URL, API_KEY)
|
||||
3. 设置 Cron 定时任务
|
||||
4. 测试工作流执行
|
||||
|
||||
### 9.4 初始化数据
|
||||
|
||||
1. 创建第一个 `Quarter` 记录(2023-Q1)
|
||||
2. 配置 3-5 条 `VisualStyleRule`:
|
||||
- 90-100: 热门大词(金色, text-5xl, border-4)
|
||||
- 70-89: 中等词汇(蓝色, text-3xl, border-2)
|
||||
- 50-69: 小词汇(紫色, text-xl, border-2)
|
||||
- 0-49: 长尾词(灰色, text-base, border-2)
|
||||
|
||||
### 9.5 监控设置
|
||||
|
||||
- n8n 执行日志告警
|
||||
- 错误日志定期检查
|
||||
- API 性能监控
|
||||
|
||||
---
|
||||
|
||||
## 十、未来扩展
|
||||
|
||||
1. **预测功能**: 基于历史数据预测下一个热点词汇
|
||||
2. **趋势分析**: 展示词汇热度的季度变化曲线
|
||||
3. **用户贡献**: 允许用户提交词汇建议
|
||||
4. **多维度**: 按技术栈、应用领域等维度分类
|
||||
5. **导出功能**: 导出季度报告(PDF/图片)
|
||||
|
||||
---
|
||||
|
||||
## 附录
|
||||
|
||||
### A. 颜色系统
|
||||
|
||||
```typescript
|
||||
const colorMap = {
|
||||
primary: "bg-primary", // Gold (#FFD700)
|
||||
secondary: "bg-secondary", // Blue (#7FB5FF)
|
||||
accent: "bg-accent", // Purple (#C39BD3)
|
||||
gray: "bg-gray-100", // Gray
|
||||
};
|
||||
```
|
||||
|
||||
### B. 字体大小映射
|
||||
|
||||
```typescript
|
||||
const sizeMap = {
|
||||
hot: "text-5xl", // 90-100 分
|
||||
medium: "text-3xl", // 70-89 分
|
||||
small: "text-xl", // 50-69 分
|
||||
tiny: "text-base", // 0-49 分
|
||||
};
|
||||
```
|
||||
|
||||
### C. 参考资源
|
||||
|
||||
- Google Trends API: https://trends.google.com/
|
||||
- n8n 文档: https://docs.n8n.io/
|
||||
- Tailwind CSS: https://tailwindcss.com/
|
||||
Reference in New Issue
Block a user