diff --git a/design/hotpot cloud/code.html b/design/hotpot cloud/code.html new file mode 100644 index 0000000..c2d0f38 --- /dev/null +++ b/design/hotpot cloud/code.html @@ -0,0 +1,323 @@ + + + + +Agent Park: AI Keyword Evolution + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+ +
+
+ EXPERIMENTAL DATA +
+

+ THE EVOLUTION OF
+AI DISCOURSE +

+

+ From "Large Language Models" to "Agentic Workflows". Use the arrows below to explore the shifting narrative. +

+
+
+
+
+
+
+
+
+
+
+ +
+
+ Q1 2023 +
+

"The dawn of the chat interface."

+
+ +
+
+
+ + ChatGPT +
+
ChatGPT
+
+
Mass consumer breakout
+
GPT-3.5 architecture
+
Conversational UI focus
+
+
+
+ + Parameters +
+
Parameters
+
+
Internal model weights
+
Scaling law metric
+
+
+
+ + Hallucination +
+
Hallucination
+
+
Factually incorrect output
+
Stochastic behavior
+
Trust barrier
+
+
+
+ + Transformer +
+
Transformer
+
+
Attention mechanism
+
Parallel processing
+
+
+
+ + Prompt Engineering +
+
Prompting
+
+
Instruction crafting
+
Few-shot prompting
+
Iterative tuning
+
+
+
+ + OpenAI +
+
The Lab
+
+
• Research pioneer
+
• Closed-source shift
+
+
+
+ + LLM +
+
LLM
+
+
Large Language Model
+
Next token prediction
+
+
+
+RLHF +Bard +Foundation Model +Emergence +Tokenization +Context Window +GPT-4 +Few-Shot +Zero-Shot +Multi-turn +Supervised Learning +
+ +
+ GENESIS
OF CHAT +
+
+
+
+
+
+ WHAT'S NEXT? +
+
+
+

Future Prediction Zone

+
+??? +AGI? +Small Language Models +On-Device AI +Liquid Neural Nets +Sovereign AI +World Models +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/design/hotpot cloud/screen.png b/design/hotpot cloud/screen.png new file mode 100644 index 0000000..134d4e3 Binary files /dev/null and b/design/hotpot cloud/screen.png differ diff --git a/design/timeline/code.html b/design/timeline/code.html new file mode 100644 index 0000000..acc1fec --- /dev/null +++ b/design/timeline/code.html @@ -0,0 +1,284 @@ + + + + +Agent Park - AI History Timeline Pinboard + + + + + + + + + + +
+
+

+auto_awesome + Welcome to Agent Park: The Evolution of Intelligence + arrow_forward +

+
+ +
+
+
+
+
+
+
+ + + +

+ THE STORY
OF A.I. +

+
+

+ Pinned. Stacked. Zigzagged. +

+
+
+
+ +
+
+
+2024 — 2017 +
+
+
+
+
+
+smart_toy +
+

The Agent Era

+

Autonomous agents begin to populate the web. They plan, execute, and iterate. It's no longer just chat; it's action.

+
FIG 1.2
+
+
+
+
+
+
+
+
+
+
+
+

Multimodal

+

Vision, audio, and text merge into single unified models. AI now perceives the world as humans do.

+
FIG 1.1
+
+
+
+
+Abstract AI Art +
+

Generative Explosion

+

ChatGPT launches. The world changes overnight. LLMs become household utilities.

+
FIG 1.0
+
+
+
+
+T +
+

The Transformer

+

"Attention Is All You Need". The paper that killed RNNs and birthed the GPT architecture.

+
FIG 0.9
+
+
+
+
+
+
+2016 — 1950 +
+
+
+
+
+
+Go Board Game +
+
+

Move 37

+

AlphaGo defeats Lee Sedol. A move of "inhuman" intuition that shocked the world.

+
FIG 0.8
+
+
+
+
+grid_on +
+

Deep Blue

+

IBM's machine defeats Kasparov. Brute force calculation triumphs over human strategy.

+
FIG 0.5
+
+
+
+
+ac_unit +
+

The Winter

+

Funding dries up. The promises of early AI fail to materialize. Research goes underground.

+
FIG 0.2
+
+
+
+
+ + + + + +
+

Imitation Game

+

Turing's question: "Can machines think?" The philosophical start of it all.

+
FIG 0.1
+
+
+
+
+
+
+
+
+mail +
+

Join the Park

+

+ Subscribe to the Agent Park weekly zine. No spam, just ducks and data. +

+
+ + +
+
+ + +
+
+
+
+
+
+arrow_upward +
+
+ + + \ No newline at end of file diff --git a/design/timeline/screen.png b/design/timeline/screen.png new file mode 100644 index 0000000..1309dc4 Binary files /dev/null and b/design/timeline/screen.png differ diff --git a/docs/plans/2026-01-25-keyword-cloud-implementation.md b/docs/plans/2026-01-25-keyword-cloud-implementation.md new file mode 100644 index 0000000..938ed7c --- /dev/null +++ b/docs/plans/2026-01-25-keyword-cloud-implementation.md @@ -0,0 +1,2408 @@ +# 季度 AI 热点词云系统实施计划 + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**目标:** 构建一个自动化数据驱动的可视化词云系统,通过 n8n 工作流从 Google Trends 采集 AI 热点词汇,自动入库并在 Next.js 前端展示。 + +**架构:** n8n 工作流定时采集 Google Trends 数据 → AI 清洗生成内容 → 规则引擎分配视觉样式 → 写入 PostgreSQL → Next.js 从数据库读取并渲染交互式词云。 + +**技术栈:** Next.js 15, Prisma, PostgreSQL, n8n, @gamal.dev/n8n-nodes-google-trends, LangChain, Tailwind CSS + +--- + +## 前置准备 + +### Task 0: 环境检查和依赖安装 + +**文件:** +- 检查: `package.json` +- 检查: `.env.local` +- 检查: `prisma/schema.prisma` + +**Step 1: 检查项目依赖** + +确认以下依赖已安装: +```bash +cat package.json | grep -E "(prisma|@radix|lucide|tailwind)" +``` + +如果没有缺失的依赖,继续。如果有,运行: +```bash +pnpm add prisma @prisma/client +``` + +**Step 2: 验证数据库连接** + +```bash +# 检查 .env.local 是否有 DATABASE_URL +grep DATABASE_URL .env.local +``` + +如果存在,继续。如果不存在,提示用户配置。 + +**Step 3: 验证 n8n 环境** + +确认你有访问 n8n 实例的权限: +- 自托管: 检查 n8n 是否在本地运行 (`localhost:5678`) +- n8n Cloud: 确认有账号和登录凭据 + +**Step 4: 创建工作目录** + +```bash +# 创建 API 路由目录 +mkdir -p src/app/api/keyword-cloud + +# 创建组件目录 +mkdir -p src/app/[locale]/keyword-cloud/components + +# 创建 hooks 目录 +ls src/hooks/useKeywordCloud.ts || echo "需要创建 hooks 文件" +``` + +--- + +## 第一阶段: 数据库层 + +### Task 1: 添加 Prisma 模型定义 + +**文件:** +- 修改: `prisma/schema.prisma` + +**Step 1: 在 schema.prisma 末尾添加新模型** + +```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]) +} + +// 关键词核心数据表 +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]) +} + +// 视觉样式规则配置表 +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]) +} + +// 错误日志表 +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]) +} +``` + +**Step 2: 生成并运行迁移** + +```bash +pnpm prisma migrate dev --name add_keyword_cloud_tables +``` + +预期输出: +``` +✔ Generated Prisma Client +✔ The following migration 20260125xxxxxx_add_keyword_cloud_tables has been created and applied from src-schema.prisma: + + create table "Quarter" ... + create table "Keyword" ... + create table "VisualStyleRule" ... + create table "KeywordCloudErrorLog" ... +``` + +**Step 3: 重新生成 Prisma Client** + +```bash +pnpm prisma generate +``` + +预期输出: +``` +✔ Generated Prisma Client to ./node_modules/.prisma/client in XXXms +``` + +**Step 4: 验证表结构** + +```bash +pnpm prisma studio +``` + +在打开的 Prisma Studio 中检查新表是否正确创建。 + +**Step 5: 提交** + +```bash +git add prisma/schema.prisma prisma/migrations +git commit -m "feat: 添加关键词词云系统数据库模型" +``` + +--- + +### Task 2: 添加 Zod 验证 Schema + +**文件:** +- 修改: `src/lib/validations.ts` + +**Step 1: 在 validations.ts 中添加关键词词云相关的 Schema** + +在文件末尾添加: + +```typescript +// 视觉配置 Schema +const VisualConfigSchema = z.object({ + color: z.enum(['primary', 'secondary', 'accent', 'gray']), + size: z.enum(['text-5xl', 'text-4xl', 'text-3xl', 'text-2xl', 'text-xl', 'text-lg', 'text-base']), + border: z.enum(['border-4', 'border-2']), + rotation: z.string().regex(/^-?rotate-\d+$/).nullable().optional(), +}); + +// 关键词输入 Schema +export const KeywordInputSchema = z.object({ + word: z.string().min(1).max(100), + trendScore: z.number().int().min(0).max(100), + description: z.string().min(10).max(500), + descriptionEn: z.string().max(500).optional(), + detailPoints: z.array(z.string().min(5).max(100)).min(1).max(5), + detailPointsEn: z.array(z.string().max(100)).max(5).optional(), + visualConfig: VisualConfigSchema, +}); + +// 批量写入关键词请求 Schema +export const BatchKeywordsRequestSchema = z.object({ + quarter: z.string().regex(/^\d{4}-Q[1-4]$/, "格式应为 YYYY-QN"), + keywords: z.array(KeywordInputSchema).min(1).max(50), +}); + +// 季度 Schema +export const QuarterSchema = z.object({ + quarter: z.string().regex(/^\d{4}-Q[1-4]$/), + title: z.string().min(1).max(200), + titleEn: z.string().max(200).optional(), + subtitle: z.string().max(500).optional(), + subtitleEn: z.string().max(500).optional(), + displayOrder: z.number().int().min(0).default(0), + isActive: z.boolean().default(true), +}); + +// 视觉规则 Schema +export const VisualStyleRuleSchema = z.object({ + name: z.string().min(1).max(100), + minScore: z.number().int().min(0).max(100), + maxScore: z.number().int().min(0).max(100), + color: z.enum(['primary', 'secondary', 'accent', 'gray']), + size: z.enum(['text-5xl', 'text-4xl', 'text-3xl', 'text-2xl', 'text-xl', 'text-lg', 'text-base']), + border: z.enum(['border-4', 'border-2']), + rotation: z.string().regex(/^-?rotate-\d+$/).nullable().optional(), + priority: z.number().int().min(0).default(0), + enabled: z.boolean().default(true), +}).refine(data => data.minScore < data.maxScore, { + message: "minScore 必须小于 maxScore", +}); + +// API 响应 Schema +export const KeywordCloudResponseSchema = z.object({ + success: z.boolean(), + data: z.any().optional(), + error: z.string().optional(), +}); +``` + +**Step 2: 运行 TypeScript 检查** + +```bash +pnpm tsc --noEmit +``` + +确保没有类型错误。 + +**Step 3: 提交** + +```bash +git add src/lib/validations.ts +git commit -m "feat: 添加关键词词云系统 Zod 验证 Schema" +``` + +--- + +### Task 3: 创建数据访问层函数 + +**文件:** +- 创建: `src/hooks/useKeywordCloud.ts` + +**Step 1: 创建服务器端数据获取函数** + +```typescript +import { prisma } from '@/lib/prisma'; +import type { Quarter, Keyword, VisualStyleRule } from '@prisma/client'; + +// 类型定义 +export type KeywordWithVisual = Keyword & { + visualConfig: { + color: string; + size: string; + border: string; + rotation?: string; + }; +}; + +export type QuarterWithKeywords = Quarter & { + keywords: KeywordWithVisual[]; + _count?: { keywords: number }; +}; + +/** + * 获取所有季度列表 + */ +export async function getAllQuarters( + options?: { isActive?: boolean } +): Promise { + const where = options?.isActive !== undefined + ? { isActive: options.isActive } + : {}; + + return prisma.quarter.findMany({ + where, + orderBy: { displayOrder: 'asc' }, + }); +} + +/** + * 获取单个季度的详情(包含关键词计数) + */ +export async function getQuarterByQuarter( + quarter: string +): Promise { + const quarterData = await prisma.quarter.findUnique({ + where: { quarter }, + include: { + _count: { + select: { keywords: true }, + }, + }, + }); + + return quarterData; +} + +/** + * 获取指定季度的所有关键词 + */ +export async function getKeywordsByQuarter( + quarter: string +): Promise { + const quarterData = await prisma.quarter.findUnique({ + where: { quarter }, + include: { + keywords: { + orderBy: { trendScore: 'desc' }, + }, + }, + }); + + // 转换 visualConfig 从 JSON 到对象 + if (quarterData) { + quarterData.keywords = quarterData.keywords.map(kw => ({ + ...kw, + visualConfig: typeof kw.visualConfig === 'string' + ? JSON.parse(kw.visualConfig) + : kw.visualConfig, + })); + } + + return quarterData; +} + +/** + * 获取所有启用的视觉规则 + */ +export async function getVisualStyleRules( + options?: { enabled?: boolean } +): Promise { + const where = options?.enabled !== undefined + ? { enabled: options.enabled } + : {}; + + return prisma.visualStyleRule.findMany({ + where, + orderBy: [ + { priority: 'asc' }, + { minScore: 'desc' }, + ], + }); +} + +/** + * 创建或更新季度 + */ +export async function upsertQuarter( + quarter: string, + data: { + title: string; + titleEn?: string; + subtitle?: string; + subtitleEn?: string; + displayOrder?: number; + } +): Promise { + return prisma.quarter.upsert({ + where: { quarter }, + update: data, + create: { + quarter, + ...data, + }, + }); +} + +/** + * 批量创建关键词 + */ +export async function createKeywords( + quarterId: number, + keywords: Array<{ + word: string; + trendScore: number; + description: string; + descriptionEn?: string; + detailPoints: string[]; + detailPointsEn?: string[]; + visualConfig: Record; + }> +): Promise<{ created: number; failed: number; errors: Array<{ word: string; error: string }> }> { + const errors: Array<{ word: string; error: string }> = []; + let created = 0; + + for (const kw of keywords) { + try { + await prisma.keyword.create({ + data: { + quarterId, + word: kw.word, + trendScore: kw.trendScore, + description: kw.description, + descriptionEn: kw.descriptionEn, + detailPoints: kw.detailPoints as any, // Prisma Json 类型 + detailPointsEn: kw.detailPointsEn as any, + visualConfig: kw.visualConfig as any, + }, + }); + created++; + } catch (error) { + errors.push({ + word: kw.word, + error: error instanceof Error ? error.message : 'Unknown error', + }); + } + } + + return { created, failed: errors.length, errors }; +} + +/** + * 记录错误日志 + */ +export async function logKeywordCloudError( + data: { + quarter: string; + keyword?: string; + errorType: string; + errorMessage: string; + rawData?: any; + } +): Promise { + await prisma.keywordCloudErrorLog.create({ + data, + }); +} +``` + +**Step 2: 运行 TypeScript 检查** + +```bash +pnpm tsc --noEmit +``` + +**Step 3: 提交** + +```bash +git add src/hooks/useKeywordCloud.ts +git commit -m "feat: 添加关键词词云数据访问层函数" +``` + +--- + +## 第二阶段: API 层 + +### Task 4: 创建获取季度列表 API + +**文件:** +- 创建: `src/app/api/keyword-cloud/quarters/route.ts` + +**Step 1: 创建 API 路由处理函数** + +```typescript +import { NextResponse } from 'next/server'; +import { getAllQuarters } from '@/hooks/useKeywordCloud'; + +export const dynamic = 'force-dynamic'; + +/** + * GET /api/keyword-cloud/quarters + * 获取季度列表 + */ +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const isActive = searchParams.get('isActive'); + + const quarters = await getAllQuarters( + isActive !== null ? { isActive: isActive === 'true' } : undefined + ); + + // 为每个季度添加关键词计数 + const quartersWithCount = await Promise.all( + quarters.map(async (q) => { + const { prisma } = await import('@/lib/prisma'); + const count = await prisma.keyword.count({ + where: { quarterId: q.id }, + }); + return { + ...q, + keywordCount: count, + }; + }) + ); + + return NextResponse.json({ + success: true, + quarters: quartersWithCount, + }); + } catch (error) { + console.error('Error fetching quarters:', error); + return NextResponse.json( + { + success: false, + error: 'Failed to fetch quarters', + }, + { status: 500 } + ); + } +} +``` + +**Step 2: 测试 API** + +```bash +# 启动开发服务器(如果未运行) +pnpm dev + +# 在另一个终端测试 +curl http://localhost:3000/api/keyword-cloud/quarters +``` + +预期输出(空数组,因为还没有数据): +```json +{ + "success": true, + "quarters": [] +} +``` + +**Step 3: 提交** + +```bash +git add src/app/api/keyword-cloud/quarters/route.ts +git commit -m "feat: 添加获取季度列表 API" +``` + +--- + +### Task 5: 创建获取关键词 API + +**文件:** +- 创建: `src/app/api/keyword-cloud/keywords/[quarter]/route.ts` + +**Step 1: 创建 API 路由处理函数** + +```typescript +import { NextResponse } from 'next/server'; +import { getKeywordsByQuarter } from '@/hooks/useKeywordCloud'; + +export const dynamic = 'force-dynamic'; + +/** + * GET /api/keyword-cloud/keywords/[quarter] + * 获取指定季度的关键词 + */ +export async function GET( + request: Request, + { params }: { params: { quarter: string } } +) { + try { + const { quarter } = params; + + // 验证 quarter 格式 + if (!/^\d{4}-Q[1-4]$/.test(quarter)) { + return NextResponse.json( + { + success: false, + error: 'Invalid quarter format. Expected: YYYY-QN', + }, + { status: 400 } + ); + } + + const data = await getKeywordsByQuarter(quarter); + + if (!data) { + return NextResponse.json( + { + success: false, + error: `Quarter ${quarter} not found`, + }, + { status: 404 } + ); + } + + return NextResponse.json({ + success: true, + quarter: data.quarter, + title: data.title, + titleEn: data.titleEn, + subtitle: data.subtitle, + subtitleEn: data.subtitleEn, + keywords: data.keywords, + }); + } catch (error) { + console.error('Error fetching keywords:', error); + return NextResponse.json( + { + success: false, + error: 'Failed to fetch keywords', + }, + { status: 500 } + ); + } +} +``` + +**Step 2: 测试 API(需要先有数据,先跳过测试)** + +暂时不测试,因为还没有数据。稍后在 Task 7 中会测试。 + +**Step 3: 提交** + +```bash +git add src/app/api/keyword-cloud/keywords/[quarter]/route.ts +git commit -m "feat: 添加获取关键词 API" +``` + +--- + +### Task 6: 创建批量写入关键词 API + +**文件:** +- 创建: `src/app/api/keyword-cloud/keywords/route.ts` + +**Step 1: 创建 API 路由处理函数** + +```typescript +import { NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { BatchKeywordsRequestSchema } from '@/lib/validations'; +import { upsertQuarter, createKeywords, logKeywordCloudError } from '@/hooks/useKeywordCloud'; +import crypto from 'crypto'; + +export const dynamic = 'force-dynamic'; + +/** + * POST /api/keyword-cloud/keywords + * 批量写入关键词(n8n 工作流使用) + */ +export async function POST(request: Request) { + try { + // 1. 验证 API Key + const body = await request.json(); + const { apiKey, ...requestData } = body; + + if (!apiKey) { + return NextResponse.json( + { + success: false, + error: 'Missing API key', + }, + { status: 401 } + ); + } + + const expectedApiKey = process.env.WEBHOOK_API_KEY; + if (!expectedApiKey) { + console.error('WEBHOOK_API_KEY not configured'); + return NextResponse.json( + { + success: false, + error: 'Server configuration error', + }, + { status: 500 } + ); + } + + // 使用 timing-safe 比较防止时序攻击 + try { + const apiKeyBuffer = Buffer.from(apiKey, 'utf-8'); + const expectedBuffer = Buffer.from(expectedApiKey, 'utf-8'); + + if (apiKeyBuffer.length !== expectedBuffer.length || + !crypto.timingSafeEqual(apiKeyBuffer, expectedBuffer)) { + return NextResponse.json( + { + success: false, + error: 'Invalid API key', + }, + { status: 401 } + ); + } + } catch (error) { + return NextResponse.json( + { + success: false, + error: 'Authentication failed', + }, + { status: 401 } + ); + } + + // 2. 验证请求数据 + const validationResult = BatchKeywordsRequestSchema.safeParse(requestData); + + if (!validationResult.success) { + return NextResponse.json( + { + success: false, + error: 'Validation failed', + details: validationResult.error.errors, + }, + { status: 400 } + ); + } + + const { quarter, keywords } = validationResult.data; + + // 3. 创建或更新季度记录 + const quarterData = await upsertQuarter(quarter, { + title: `${quarter.replace('-', '年')}季度`, + titleEn: quarter.replace('-', ' '), + }); + + // 4. 批量创建关键词 + const result = await createKeywords(quarterData.id, keywords); + + // 5. 记录错误 + for (const error of result.errors) { + await logKeywordCloudError({ + quarter, + keyword: error.word, + errorType: 'DB_ERROR', + errorMessage: error.error, + }); + } + + // 6. 返回结果 + return NextResponse.json({ + success: true, + created: result.created, + failed: result.failed, + errors: result.errors, + }); + } catch (error) { + console.error('Error creating keywords:', error); + + // 记录未捕获的错误 + try { + await logKeywordCloudError({ + quarter: 'unknown', + errorType: 'API_ERROR', + errorMessage: error instanceof Error ? error.message : 'Unknown error', + rawData: { error }, + }); + } catch (logError) { + console.error('Failed to log error:', logError); + } + + return NextResponse.json( + { + success: false, + error: 'Failed to create keywords', + }, + { status: 500 } + ); + } +} +``` + +**Step 2: 测试 API(需要先有数据,先跳过)** + +暂时不测试,稍后在 Task 7 中会测试。 + +**Step 3: 提交** + +```bash +git add src/app/api/keyword-cloud/keywords/route.ts +git commit -m "feat: 添加批量写入关键词 API" +``` + +--- + +### Task 7: 创建获取视觉规则 API + +**文件:** +- 创建: `src/app/api/keyword-cloud/rules/route.ts` + +**Step 1: 创建 API 路由处理函数** + +```typescript +import { NextResponse } from 'next/server'; +import { getVisualStyleRules } from '@/hooks/useKeywordCloud'; + +export const dynamic = 'force-dynamic'; + +/** + * GET /api/keyword-cloud/rules + * 获取视觉样式规则配置 + */ +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const enabled = searchParams.get('enabled'); + + const rules = await getVisualStyleRules( + enabled !== null ? { enabled: enabled === 'true' } : undefined + ); + + // 转换规则格式以匹配前端期望 + const formattedRules = rules.map(rule => ({ + id: rule.id, + name: rule.name, + minScore: rule.minScore, + maxScore: rule.maxScore, + visualConfig: { + color: rule.color, + size: rule.size, + border: rule.border, + rotation: rule.rotation, + }, + priority: rule.priority, + enabled: rule.enabled, + })); + + return NextResponse.json({ + success: true, + rules: formattedRules, + }); + } catch (error) { + console.error('Error fetching visual style rules:', error); + return NextResponse.json( + { + success: false, + error: 'Failed to fetch rules', + }, + { status: 500 } + ); + } +} +``` + +**Step 2: 提交** + +```bash +git add src/app/api/keyword-cloud/rules/route.ts +git commit -m "feat: 添加获取视觉规则 API" +``` + +--- + +## 第三阶段: 前端组件 + +### Task 8: 创建 CloudWord 组件 + +**文件:** +- 创建: `src/app/[locale]/keyword-cloud/components/CloudWord.tsx` + +**Step 1: 创建单个词汇组件** + +```typescript +'use client'; + +import React from 'react'; +import { cn } from '@/lib/utils'; + +interface VisualConfig { + color: 'primary' | 'secondary' | 'accent' | 'gray'; + size: string; + border: string; + rotation?: string; +} + +interface KeywordData { + id: number; + word: string; + trendScore: number; + description: string; + detailPoints: string[]; + visualConfig: VisualConfig; +} + +interface CloudWordProps { + data: KeywordData; +} + +// 颜色映射 +const colorMap: Record = { + primary: 'bg-primary', + secondary: 'bg-secondary', + accent: 'bg-accent', + gray: 'bg-gray-100 dark:bg-gray-700', +}; + +export function CloudWord({ data }: CloudWordProps) { + const { word, visualConfig, description, detailPoints } = data; + const { color, size, border, rotation } = visualConfig; + const colorClass = colorMap[color]; + + return ( + + {word} + + + ); +} + +interface WordPopoverProps { + title: string; + description: string; + points: string[]; + titleColor: string; +} + +function WordPopover({ title, description, points, titleColor }: WordPopoverProps) { + const titleColorClass = colorMap[titleColor] || 'bg-gray-100'; + + return ( +
+ {/* 标题栏 */} +
+ {title} +
+ + {/* 内容区域 */} +
+
+ + {description} +
+ {points.map((point, i) => ( +
+ + {point} +
+ ))} +
+ + {/* 箭头 */} +
+
+ ); +} +``` + +**Step 2: 提交** + +```bash +git add src/app/[locale]/keyword-cloud/components/CloudWord.tsx +git commit -m "feat: 添加 CloudWord 组件" +``` + +--- + +### Task 9: 创建 QuarterNavigator 组件 + +**文件:** +- 创建: `src/app/[locale]/keyword-cloud/components/QuarterNavigator.tsx` + +**Step 1: 创建季度导航组件** + +```typescript +'use client'; + +import React from 'react'; +import MaterialSymbol from '@/components/ui/MaterialSymbol'; + +interface QuarterNavigatorProps { + current: string; + quarters: string[]; + onNavigate: (quarter: string) => void; +} + +export function QuarterNavigator({ current, quarters, onNavigate }: QuarterNavigatorProps) { + const currentIndex = quarters.indexOf(current); + const canGoPrev = currentIndex > 0; + const canGoNext = currentIndex < quarters.length - 1; + + return ( +
+ {/* 上一个季度按钮 */} + + + {/* 当前季度显示 */} +
+
+ {current} +
+
+ + {/* 下一个季度按钮 */} + +
+ ); +} +``` + +**Step 2: 提交** + +```bash +git add src/app/[locale]/keyword-cloud/components/QuarterNavigator.tsx +git commit -m "feat: 添加 QuarterNavigator 组件" +``` + +--- + +### Task 10: 创建 ProgressIndicator 组件 + +**文件:** +- 创建: `src/app/[locale]/keyword-cloud/components/ProgressIndicator.tsx` + +**Step 1: 创建进度条组件** + +```typescript +'use client'; + +import React from 'react'; +import { cn } from '@/lib/utils'; + +interface ProgressIndicatorProps { + currentQuarter: string; + totalQuarters: number; + quarters: string[]; +} + +export function ProgressIndicator({ currentQuarter, totalQuarters, quarters }: ProgressIndicatorProps) { + const currentIndex = quarters.indexOf(currentQuarter); + + return ( +
+ {quarters.map((quarter, index) => { + const isCompleted = index <= currentIndex; + const isCurrent = index === currentIndex; + const isLast = index === quarters.length - 1; + + return ( +
+ ); + })} +
+ ); +} +``` + +**Step 2: 提交** + +```bash +git add src/app/[locale]/keyword-cloud/components/ProgressIndicator.tsx +git commit -m "feat: 添加 ProgressIndicator 组件" +``` + +--- + +### Task 11: 创建 KeywordCloud 主容器组件 + +**文件:** +- 创建: `src/app/[locale]/keyword-cloud/components/KeywordCloud.tsx` + +**Step 1: 创建词云容器组件** + +```typescript +'use client'; + +import React, { useState, useEffect } from 'react'; +import { CloudWord } from './CloudWord'; +import { QuarterNavigator } from './QuarterNavigator'; +import { ProgressIndicator } from './ProgressIndicator'; +import { useKeywordCloud } from '@/hooks/useKeywordCloudClient'; + +interface KeywordData { + id: number; + word: string; + trendScore: number; + description: string; + detailPoints: string[]; + visualConfig: { + color: string; + size: string; + border: string; + rotation?: string; + }; +} + +interface QuarterData { + quarter: string; + title: string; + titleEn?: string; + subtitle?: string; + subtitleEn?: string; + keywords: KeywordData[]; +} + +export function KeywordCloud({ initialQuarter }: { initialQuarter: string }) { + const [currentQuarter, setCurrentQuarter] = useState(initialQuarter); + const [quarters, setQuarters] = useState([]); + const { data, isLoading, error } = useKeywordCloud(currentQuarter); + + // 加载季度列表 + useEffect(() => { + async function loadQuarters() { + try { + const response = await fetch('/api/keyword-cloud/quarters'); + const json = await response.json(); + if (json.success) { + const quarterStrings = json.quarters.map((q: any) => q.quarter); + setQuarters(quarterStrings); + } + } catch (error) { + console.error('Failed to load quarters:', error); + } + } + loadQuarters(); + }, []); + + const handleNavigate = (quarter: string) => { + setCurrentQuarter(quarter); + // 更新 URL 而不刷新页面 + const url = new URL(window.location.href); + url.searchParams.set('quarter', quarter); + window.history.pushState({}, '', url.toString()); + }; + + if (isLoading) { + return ( +
+
+
+

加载中...

+
+
+ ); + } + + if (error || !data) { + return ( +
+
+

+ 加载失败 +

+ +
+
+ ); + } + + return ( +
+ {/* 进度条 */} + + + {/* 季度导航 */} + + + {/* 词云区域 */} +
+
+ {data.keywords.map((keyword: KeywordData) => ( + + ))} +
+
+ + {/* 装饰元素 */} + {data.keywords.length > 0 && ( +
+ "{data.keywords[0].word}" 是本季度最热门词汇! +
+
+ )} +
+ ); +} +``` + +**Step 2: 提交** + +```bash +git add src/app/[locale]/keyword-cloud/components/KeywordCloud.tsx +git commit -m "feat: 添加 KeywordCloud 主容器组件" +``` + +--- + +### Task 12: 创建客户端数据获取 Hook + +**文件:** +- 创建: `src/hooks/useKeywordCloudClient.ts` + +**Step 1: 创建客户端 Hook** + +```typescript +'use client'; + +import { useState, useEffect } from 'react'; + +interface KeywordData { + id: number; + word: string; + trendScore: number; + description: string; + detailPoints: string[]; + visualConfig: { + color: string; + size: string; + border: string; + rotation?: string; + }; +} + +interface QuarterData { + quarter: string; + title: string; + titleEn?: string; + subtitle?: string; + subtitleEn?: string; + keywords: KeywordData[]; +} + +export function useKeywordCloud(quarter: string) { + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function fetchData() { + setIsLoading(true); + setError(null); + + try { + const response = await fetch(`/api/keyword-cloud/keywords/${quarter}`); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const json = await response.json(); + + if (!json.success) { + throw new Error(json.error || 'Unknown error'); + } + + setData(json); + } catch (err) { + setError(err instanceof Error ? err : new Error('Unknown error')); + } finally { + setIsLoading(false); + } + } + + if (quarter) { + fetchData(); + } + }, [quarter]); + + return { data, isLoading, error }; +} +``` + +**Step 2: 提交** + +```bash +git add src/hooks/useKeywordCloudClient.ts +git commit -m "feat: 添加关键词词云客户端数据获取 Hook" +``` + +--- + +### Task 13: 创建词云页面 + +**文件:** +- 创建: `src/app/[locale]/keyword-cloud/page.tsx` + +**Step 1: 创建主页面** + +```typescript +import { KeywordCloud } from './components/KeywordCloud'; +import { redirect } from 'next/navigation'; + +interface PageProps { + params: { + locale: string; + }; + searchParams: { + quarter?: string; + }; +} + +export default function KeywordCloudPage({ searchParams }: PageProps) { + // 如果 URL 中有 quarter 参数,使用它;否则使用默认季度 + const quarter = searchParams.quarter || '2024-Q1'; + + return ( +
+ {/* 页面标题 */} +
+
+ AI 热点追踪 +
+

+ 季度 AI 热点词云 +

+

+ 从 "大型语言模型" 到 "Agent 工作流"。探索 AI 话语的演变历程。 +

+
+ + {/* 词云组件 */} + +
+ ); +} +``` + +**Step 2: 测试页面** + +```bash +# 访问页面 +open http://localhost:3000/zh/keyword-cloud +``` + +预期输出: 应该看到页面布局,但由于还没有数据,会显示"加载中"或错误提示。 + +**Step 3: 提交** + +```bash +git add src/app/[locale]/keyword-cloud/page.tsx +git commit -m "feat: 添加关键词词云页面" +``` + +--- + +## 第四阶段: n8n 工作流 + +### Task 14: 创建 n8n 工作流配置文件 + +**文件:** +- 创建: `n8n-workflows/keyword-cloud-workflow.json` + +**Step 1: 创建 n8n 工作流 JSON 配置** + +```json +{ + "name": "Keyword Cloud Data Collection", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "cron": "0 0 23 28-31 * *" + } + ] + } + }, + "id": "schedule-trigger", + "name": "Schedule Trigger", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.1, + "position": [250, 300] + }, + { + "parameters": { + "functionCode": "// 检查当前月份是否是季度末(3,6,9,12)\nconst now = new Date();\nconst month = now.getMonth() + 1; // 1-12\nconst isQuarterEnd = [3, 6, 9, 12].includes(month);\n\nif (!isQuarterEnd) {\n return [];\n}\n\n// 计算季度标识\nconst year = now.getFullYear();\nconst quarter = Math.ceil(month / 3);\nconst quarterString = `${year}-Q${quarter}`;\n\n// 计算季度的开始和结束日期\nconst quarterStart = new Date(year, (quarter - 1) * 3, 1);\nconst quarterEnd = new Date(year, quarter * 3, 0);\n\nreturn [{\n json: {\n quarter: quarterString,\n startDate: quarterStart.toISOString().split('T')[0],\n endDate: quarterEnd.toISOString().split('T')[0]\n }\n}];" + }, + "id": "calculate-quarter", + "name": "Calculate Quarter", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [450, 300] + }, + { + "parameters": { + "resource": "interestOverTime", + "operation": "get", + "searchTerms": "AI,artificial intelligence,machine learning,GPT,LLM,ChatGPT,transformer", + "timeRange": "={{ $json.startDate }} {{ $json.endDate }}", + "category": "0", + "geo": "GB" + }, + "id": "google-trends", + "name": "Google Trends", + "type": "@gamal.dev/n8n-nodes-google-trends", + "typeVersion": 1, + "position": [650, 300] + }, + { + "parameters": { + "functionCode": "// 从 Google Trends 数据中提取热门词汇\nconst trends = $input.all();\nconst keywords = [];\n\n// 假设 Google Trends 返回数据包含关键词和分数\nfor (const trend of trends) {\n const data = trend.json;\n \n if (data.timeline) {\n for (const [keyword, values] of Object.entries(data.timeline)) {\n if (Array.isArray(values) && values.length > 0) {\n // 计算平均分数\n const avgScore = values.reduce((a, b) => a + b, 0) / values.length;\n keywords.push({\n json: {\n word: keyword,\n trendScore: Math.round(avgScore),\n quarter: $('Calculate Quarter').item.json.quarter\n }\n });\n }\n }\n }\n}\n\nreturn keywords;" + }, + "id": "extract-keywords", + "name": "Extract Keywords", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [850, 300] + }, + { + "parameters": { + "values": { + "string": [ + { + "name": "apiKey", + "value": "={{ $env.API_KEY }}" + } + ] + }, + "options": {} + }, + "id": "set-api-key", + "name": "Set API Key", + "type": "n8n-nodes-base.set", + "typeVersion": 3.2, + "position": [1050, 300] + }, + { + "parameters": { + "url": "={{ $env.API_URL }}/api/keyword-cloud/rules", + "options": {} + }, + "id": "get-visual-rules", + "name": "Get Visual Rules", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [1250, 300] + }, + { + "parameters": { + "jsCode": "// 获取关键词和规则\nconst keywords = $('Extract Keywords').all();\nconst rulesData = $('Get Visual Rules').first().json;\nconst rules = rulesData.rules || [];\n\n// 规则按优先级排序\nrules.sort((a, b) => a.priority - b.priority);\n\n// 为每个关键词匹配规则\nconst processed = keywords.map(item => {\n const keyword = item.json;\n \n // 查找匹配的规则\n const matchedRule = rules.find(rule => \n keyword.trendScore >= rule.minScore && \n keyword.trendScore <= rule.maxScore\n );\n \n const visualConfig = matchedRule ? matchedRule.visualConfig : {\n color: 'gray',\n size: 'text-base',\n border: 'border-2',\n rotation: null\n };\n \n return {\n json: {\n ...keyword,\n visualConfig\n }\n };\n});\n\nreturn processed;" + }, + "id": "match-visual-rules", + "name": "Match Visual Rules", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1450, 300] + }, + { + "parameters": { + "url": "={{ $env.API_URL }}/api/keyword-cloud/keywords", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"apiKey\": \"{{ $env.API_KEY }}\",\n \"quarter\": \"{{ $('Calculate Quarter').item.json.quarter }}\",\n \"keywords\": {{ $json.all().map(item => ({\n word: item.json.word,\n trendScore: item.json.trendScore,\n description: \"AI生成的描述\", // TODO: 使用 AI 节点生成\n detailPoints: [\"要点1\", \"要点2\", \"要点3\"] // TODO: 使用 AI 节点生成\n })) }}\n}", + "options": {} + }, + "id": "send-to-api", + "name": "Send to API", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [1650, 300], + "credentials": { + "httpHeaderAuth": { + "id": "1", +n "name": "API Key Auth" + } + } + } + ], + "connections": { + "Schedule Trigger": { + "main": [ + [ + { + "node": "Calculate Quarter", + "type": "main", + "index": 0 + } + ] + ] + }, + "Calculate Quarter": { + "main": [ + [ + { + "node": "Google Trends", + "type": "main", + "index": 0 + } + ] + ] + }, + "Google Trends": { + "main": [ + [ + { + "node": "Extract Keywords", + "type": "main", + "index": 0 + } + ] + ] + }, + "Extract Keywords": { + "main": [ + [ + { + "node": "Set API Key", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set API Key": { + "main": [ + [ + { + "node": "Get Visual Rules", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get Visual Rules": { + "main": [ + [ + { + "node": "Match Visual Rules", + "type": "main", + "index": 0 + } + ] + ] + }, + "Match Visual Rules": { + "main": [ + [ + { + "node": "Send to API", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {}, + "settings": { + "executionOrder": "v1" + }, + "staticData": null, + "tags": [], + "triggerCount": 0, + "updatedAt": "2026-01-25T00:00:00.000Z", + "versionId": "1" +} +``` + +**Step 2: 提交** + +```bash +git add n8n-workflows/keyword-cloud-workflow.json +git commit -m "feat: 添加 n8n 关键词词云工作流配置" +``` + +--- + +## 第五阶段:初始化数据 + +### Task 15: 初始化视觉规则数据 + +**文件:** +- 创建: `scripts/seed-keyword-cloud.ts` + +**Step 1: 创建种子数据脚本** + +```typescript +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function main() { + console.log('开始初始化关键词词云数据...'); + + // 1. 创建视觉样式规则 + const rules = [ + { + name: '热门大词-金色', + minScore: 90, + maxScore: 100, + color: 'primary', + size: 'text-5xl', + border: 'border-4', + rotation: 'rotate-1', + priority: 0, + enabled: true, + }, + { + name: '中等词汇-蓝色', + minScore: 70, + maxScore: 89, + color: 'secondary', + size: 'text-3xl', + border: 'border-4', + rotation: 'rotate-2', + priority: 1, + enabled: true, + }, + { + name: '小词汇-紫色', + minScore: 50, + maxScore: 69, + color: 'accent', + size: 'text-xl', + border: 'border-2', + rotation: '-rotate-1', + priority: 2, + enabled: true, + }, + { + name: '长尾词-灰色', + minScore: 0, + maxScore: 49, + color: 'gray', + size: 'text-base', + border: 'border-2', + rotation: null, + priority: 3, + enabled: true, + }, + ]; + + for (const rule of rules) { + await prisma.visualStyleRule.upsert({ + where: { name: rule.name }, + update: rule, + create: rule, + }); + console.log(`✓ 创建规则: ${rule.name}`); + } + + // 2. 创建示例季度(2023-Q1) + const quarter = await prisma.quarter.upsert({ + where: { quarter: '2023-Q1' }, + update: {}, + create: { + quarter: '2023-Q1', + title: '2023年第一季度', + titleEn: 'Q1 2023', + subtitle: '聊天界面的黎明', + subtitleEn: 'The dawn of chat interface', + displayOrder: 0, + isActive: true, + }, + }); + console.log(`✓ 创建季度: ${quarter.quarter}`); + + // 3. 创建示例关键词 + const sampleKeywords = [ + { + word: 'ChatGPT', + trendScore: 98, + description: 'OpenAI 开发的对话式人工智能助手,支持多轮对话', + detailPoints: ['基于 GPT-3.5 架构', '2023年用户突破1亿', '引领对话式AI热潮'], + visualConfig: { + color: 'secondary', + size: 'text-5xl', + border: 'border-4', + rotation: 'rotate-1', + }, + }, + { + word: 'GPT-4', + trendScore: 92, + description: 'OpenAI 发布的多模态大型语言模型', + detailPoints: ['支持图像输入', '推理能力显著提升', '上下文窗口扩大'], + visualConfig: { + color: 'primary', + size: 'text-4xl', + border: 'border-4', + rotation: 'rotate-2', + }, + }, + { + word: 'LLM', + trendScore: 88, + description: 'Large Language Model,大型语言模型', + detailPoints: ['基于Transformer架构', '参数规模达十亿级', '涌现能力'], + visualConfig: { + color: 'secondary', + size: 'text-3xl', + border: 'border-4', + rotation: '-rotate-1', + }, + }, + { + word: 'Prompt Engineering', + trendScore: 85, + description: '提示词工程,优化AI模型输入的技术', + detailPoints: ['Few-shot prompting', '思维链提示', '迭代优化'], + visualConfig: { + color: 'accent', + size: 'text-3xl', + border: 'border-2', + rotation: 'rotate-1', + }, + }, + { + word: 'Transformer', + trendScore: 75, + description: '基于自注意力机制的神经网络架构', + detailPoints: ['并行计算能力强', '成为LLM基础架构', '2017年Google提出'], + visualConfig: { + color: 'secondary', + size: 'text-2xl', + border: 'border-2', + rotation: null, + }, + }, + ]; + + for (const kw of sampleKeywords) { + await prisma.keyword.create({ + data: { + quarterId: quarter.id, + ...kw, + detailPoints: kw.detailPoints as any, + visualConfig: kw.visualConfig as any, + }, + }); + console.log(`✓ 创建关键词: ${kw.word}`); + } + + console.log('\n初始化完成!'); +} + +main() + .catch((e) => { + console.error('错误:', e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); +``` + +**Step 2: 运行种子数据脚本** + +```bash +pnpm tsx scripts/seed-keyword-cloud.ts +``` + +预期输出: +``` +开始初始化关键词词云数据... +✓ 创建规则: 热门大词-金色 +✓ 创建规则: 中等词汇-蓝色 +✓ 创建规则: 小词汇-紫色 +✓ 创建规则: 长尾词-灰色 +✓ 创建季度: 2023-Q1 +✓ 创建关键词: ChatGPT +✓ 创建关键词: GPT-4 +✓ 创建关键词: LLM +✓ 创建关键词: Prompt Engineering +✓ 创建关键词: Transformer + +初始化完成! +``` + +**Step 3: 测试前端页面** + +```bash +# 访问页面 +open http://localhost:3000/zh/keyword-cloud?quarter=2023-Q1 +``` + +预期输出: 应该看到 2023-Q1 的词云,包含 5 个示例词汇。 + +**Step 4: 提交** + +```bash +git add scripts/seed-keyword-cloud.ts +git commit -m "feat: 添加关键词词云种子数据脚本" +``` + +--- + +## 第六阶段:集成测试 + +### Task 16: 端到端测试 + +**文件:** +- 创建: `tests/e2e/keyword-cloud.spec.ts` + +**Step 1: 创建 E2E 测试** + +```typescript +import { test, expect } from '@playwright/test'; + +test.describe('关键词词云系统', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/zh/keyword-cloud?quarter=2023-Q1'); + }); + + test('应该显示词云页面', async ({ page }) => { + await expect(page.locator('h1')).toContainText('季度 AI 热点词云'); + }); + + test('应该显示季度导航', async ({ page }) => { + await expect(page.locator('text=2023-Q1')).toBeVisible(); + }); + + test('应该显示关键词', async ({ page }) => { + await expect(page.locator('text=ChatGPT')).toBeVisible(); + await expect(page.locator('text=GPT-4')).toBeVisible(); + await expect(page.locator('text=LLM')).toBeVisible(); + }); + + test('鼠标悬停应该显示弹出框', async ({ page }) => { + const chatgptWord = page.locator('text=ChatGPT').first(); + + await chatgptWord.hover(); + + await expect(page.locator('.popover')).toBeVisible(); + await expect(page.locator('.popover')).toContainText('OpenAI 开发的对话式'); + }); + + test('季度切换按钮状态正确', async ({ page }) => { + const prevButton = page.locator('button[aria-label="Previous quarter"]'); + const nextButton = page.locator('button[aria-label="Next quarter"]'); + + // 2023-Q1 是第一个季度,所以应该禁用"上一个"按钮 + await expect(prevButton).toBeDisabled(); + await expect(nextButton).not.toBeDisabled(); + }); + + test('API 端点测试: 获取季度列表', async ({ request }) => { + const response = await request.get('/api/keyword-cloud/quarters'); + const data = await response.json(); + + expect(response.ok()).toBeTruthy(); + expect(data.success).toBe(true); + expect(data.quarters).toBeInstanceOf(Array); + expect(data.quarters.length).toBeGreaterThan(0); + }); + + test('API 端点测试: 获取关键词', async ({ request }) => { + const response = await request.get('/api/keyword-cloud/keywords/2023-Q1'); + const data = await response.json(); + + expect(response.ok()).toBeTruthy(); + expect(data.success).toBe(true); + expect(data.quarter).toBe('2023-Q1'); + expect(data.keywords).toBeInstanceOf(Array); + expect(data.keywords.length).toBeGreaterThan(0); + }); + + test('API 端点测试: 获取视觉规则', async ({ request }) => { + const response = await request.get('/api/keyword-cloud/rules'); + const data = await response.json(); + + expect(response.ok()).toBeTruthy(); + expect(data.success).toBe(true); + expect(data.rules).toBeInstanceOf(Array); + expect(data.rules.length).toBeGreaterThan(0); + }); +}); +``` + +**Step 2: 运行 E2E 测试** + +```bash +pnpm test:e2e +``` + +预期输出: 所有测试通过。 + +**Step 3: 提交** + +```bash +git add tests/e2e/keyword-cloud.spec.ts +git commit -m "test: 添加关键词词云 E2E 测试" +``` + +--- + +## 第七阶段:n8n 集成 + +### Task 17: 配置 n8n 环境变量 + +**文件:** +- 修改: `.env.local` + +**Step 1: 添加 n8n 所需的环境变量** + +```bash +# n8n 工作流配置 +N8N_API_URL=http://localhost:3000 +N8N_API_KEY=your-webhook-api-key-here +``` + +**注意**: `N8N_API_KEY` 应该与你的项目 `WEBHOOK_API_KEY` 相同,或者为 n8n 创建专门的 API Key。 + +**Step 2: 提交(不要提交实际密钥)** + +```bash +git add .env.local.example # 如果有示例文件 +git commit -m "chore: 添加 n8n 环境变量配置" +``` + +--- + +### Task 18: 导入并测试 n8n 工作流 + +**Step 1: 在 n8n 中导入工作流** + +1. 打开 n8n 界面 (`localhost:5678` 或你的 n8n Cloud 实例) +2. 点击 "Import from File" +3. 选择 `n8n-workflows/keyword-cloud-workflow.json` +4. 保存工作流 + +**Step 2: 配置工作流凭证** + +1. 点击 "Send to API" 节点 +2. 配置 HTTP Header Auth 凭证: + - Name: `Authorization` + - Value: `Bearer YOUR_API_KEY` +3. 保存 + +**Step 3: 配置环境变量** + +在 n8n 中设置以下环境变量: +- `API_URL`: `http://localhost:3000` (或你的生产环境 URL) +- `API_KEY`: 你的 `WEBHOOK_API_KEY` + +**Step 4: 手动测试工作流** + +1. 在 n8n 中点击 "Execute Workflow" +2. 观察每个节点的执行结果 +3. 检查数据库中是否创建了新数据 + +**Step 5: 验证数据** + +```bash +# 使用 Prisma Studio 检查数据 +pnpm prisma studio + +# 或使用 psql +psql $DATABASE_URL -c "SELECT * FROM \"Keyword\" ORDER BY \"createdAt\" DESC LIMIT 5;" +``` + +**Step 6: 提交 n8n 工作流文档** + +创建 `n8n-workflows/README.md`: + +```markdown +# n8n 工作流文档 + +本目录包含 Agent Park 的 n8n 工作流配置。 + +## 关键词词云工作流 + +**文件**: `keyword-cloud-workflow.json` + +### 功能 + +自动采集 Google Trends 数据,生成 AI 热点词汇词云。 + +### 执行流程 + +1. **Schedule Trigger**: 每季度末最后一天的 23:00 自动触发 +2. **Calculate Quarter**: 计算当前季度标识和时间范围 +3. **Google Trends**: 采集热门搜索词 +4. **Extract Keywords**: 提取关键词和热度分数 +5. **Get Visual Rules**: 获取视觉样式规则 +6. **Match Visual Rules**: 为关键词匹配视觉样式 +7. **Send to API**: 写入数据库 + +### 环境变量 + +- `API_URL`: API 端点 URL +- `API_KEY`: Webhook API Key + +### 手动执行 + +在 n8n 界面中点击 "Execute Workflow" 按钮。 + +### 调试 + +检查每个节点的输出,确保数据格式正确。 +``` + +**Step 7: 提交** + +```bash +git add n8n-workflows/README.md +git commit -m "docs: 添加 n8n 工作流文档" +``` + +--- + +## 第八阶段:部署和监控 + +### Task 19: 配置生产环境 + +**文件:** +- 创建: `scripts/deploy-keyword-cloud.sh` + +**Step 1: 创建部署脚本** + +```bash +#!/bin/bash + +echo "🚀 部署关键词词云系统..." + +# 1. 运行数据库迁移 +echo "📦 运行数据库迁移..." +pnpm prisma migrate deploy + +# 2. 生成 Prisma Client +echo "🔧 生成 Prisma Client..." +pnpm prisma generate + +# 3. 初始化种子数据(仅首次) +echo "🌱 初始化种子数据..." +pnpm tsx scripts/seed-keyword-cloud.ts + +# 4. 构建应用 +echo "🏗️ 构建应用..." +pnpm build + +# 5. 提示配置 n8n +echo "" +echo "✅ 部署完成!" +echo "" +echo "⚠️ 下一步:" +echo "1. 在 n8n 中导入工作流: n8n-workflows/keyword-cloud-workflow.json" +echo "2. 配置 n8n 环境变量: API_URL, API_KEY" +echo "3. 测试工作流执行" +echo "" +``` + +**Step 2: 赋予执行权限** + +```bash +chmod +x scripts/deploy-keyword-cloud.sh +``` + +**Step 3: 提交** + +```bash +git add scripts/deploy-keyword-cloud.sh +git commit -m "chore: 添加关键词词云部署脚本" +``` + +--- + +### Task 20: 添加监控和告警 + +**文件:** +- 创建: `src/app/api/keyword-cloud/health/route.ts` + +**Step 1: 创建健康检查端点** + +```typescript +import { NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +export const dynamic = 'force-dynamic'; + +/** + * GET /api/keyword-cloud/health + * 健康检查端点(用于监控) + */ +export async function GET() { + try { + // 检查数据库连接 + await prisma.$queryRaw`SELECT 1`; + + // 统计数据 + const quarterCount = await prisma.quarter.count(); + const keywordCount = await prisma.keyword.count(); + const ruleCount = await prisma.visualStyleRule.count({ + where: { enabled: true }, + }); + const errorCount = await prisma.keywordCloudErrorLog.count({ + where: { + createdAt: { + gte: new Date(Date.now() - 24 * 60 * 60 * 1000), // 最近24小时 + }, + }, + }); + + return NextResponse.json({ + success: true, + status: 'healthy', + stats: { + quarters: quarterCount, + keywords: keywordCount, + activeRules: ruleCount, + recentErrors: errorCount, + }, + timestamp: new Date().toISOString(), + }); + } catch (error) { + console.error('Health check failed:', error); + + return NextResponse.json( + { + success: false, + status: 'unhealthy', + error: error instanceof Error ? error.message : 'Unknown error', + timestamp: new Date().toISOString(), + }, + { status: 503 } + ); + } +} +``` + +**Step 2: 测试健康检查** + +```bash +curl http://localhost:3000/api/keyword-cloud/health +``` + +预期输出: +```json +{ + "success": true, + "status": "healthy", + "stats": { + "quarters": 1, + "keywords": 5, + "activeRules": 4, + "recentErrors": 0 + }, + "timestamp": "2026-01-25T..." +} +``` + +**Step 3: 提交** + +```bash +git add src/app/api/keyword-cloud/health/route.ts +git commit -m "feat: 添加关键词词云健康检查端点" +``` + +--- + +## 第九阶段:文档和清理 + +### Task 21: 更新项目文档 + +**文件:** +- 修改: `CLAUDE.md` + +**Step 1: 在 CLAUDE.md 中添加关键词词云系统说明** + +在 "Architecture Overview" 部分后添加: + +```markdown +### Keyword Cloud System (季度 AI 热点词云) + +**功能**: 自动化采集 Google Trends 数据,展示季度 AI 热点词汇词云。 + +**数据流**: n8n 工作流 → AI 清洗 → 规则匹配 → PostgreSQL → Next.js 前端 + +**数据库表**: +- `Quarter`: 季度元数据 +- `Keyword`: 关键词数据和视觉配置 +- `VisualStyleRule`: 视觉样式规则配置 +- `KeywordCloudErrorLog`: 错误日志 + +**API 端点**: +- `GET /api/keyword-cloud/quarters`: 获取季度列表 +- `GET /api/keyword-cloud/keywords/[quarter]`: 获取关键词 +- `GET /api/keyword-cloud/rules`: 获取视觉规则 +- `POST /api/keyword-cloud/keywords`: 批量写入(n8n使用) +- `GET /api/keyword-cloud/health`: 健康检查 + +**前端路由**: `/[locale]/keyword-cloud` + +**n8n 工作流**: `n8n-workflows/keyword-cloud-workflow.json` + +**初始化**: +```bash +pnpm tsx scripts/seed-keyword-cloud.ts +``` +``` + +**Step 2: 提交** + +```bash +git add CLAUDE.md +git commit -m "docs: 更新 CLAUDE.md 添加关键词词云系统说明" +``` + +--- + +### Task 22: 最终代码审查和测试 + +**Step 1: 运行完整测试套件** + +```bash +# 单元测试 +pnpm test + +# E2E 测试 +pnpm test:e2e + +# TypeScript 检查 +pnpm tsc --noEmit + +# ESLint 检查 +pnpm lint +``` + +确保所有检查通过。 + +**Step 2: 手动测试所有功能** + +1. 访问 `/zh/keyword-cloud` +2. 测试季度切换 +3. 测试词汇悬停弹出框 +4. 测试 API 端点 +5. 检查响应式布局(移动端) + +**Step 3: 性能检查** + +```bash +# 构建生产版本 +pnpm build + +# 检查包大小 +pnpm build --analyze +``` + +**Step 4: 安全检查** + +```bash +# 检查依赖漏洞 +pnpm audit +``` + +**Step 5: 提交最终更新** + +```bash +git add . +git commit -m "chore: 关键词词云系统实施完成" +``` + +--- + +## 总结 + +实施计划包含 22 个任务,分为 9 个阶段: + +1. ✅ 前置准备 +2. ✅ 数据库层(3 个任务) +3. ✅ API 层(4 个任务) +4. ✅ 前端组件(6 个任务) +5. ✅ n8n 工作流配置(2 个任务) +6. ✅ 初始化数据(2 个任务) +7. ✅ 集成测试(1 个任务) +8. ✅ n8n 集成(2 个任务) +9. ✅ 部署和监控(2 个任务) + +**总预计时间**: 8-12 小时 + +**技术栈**: +- Next.js 15 (App Router) +- Prisma ORM +- PostgreSQL +- n8n +- Tailwind CSS + +**关键特性**: +- ✅ 自动化数据采集 +- ✅ AI 清洗和内容生成 +- ✅ 规则引擎视觉样式匹配 +- ✅ 交互式词云展示 +- ✅ 多语言支持 +- ✅ 响应式设计 +- ✅ 错误处理和监控 diff --git a/docs/plans/2026-01-25-keyword-cloud-system-design.md b/docs/plans/2026-01-25-keyword-cloud-system-design.md new file mode 100644 index 0000000..27f8d8d --- /dev/null +++ b/docs/plans/2026-01-25-keyword-cloud-system-design.md @@ -0,0 +1,716 @@ +# 季度 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 ( +
+ + +
+ {data?.keywords.map((keyword) => ( + + ))} +
+ +
+ ); +} +``` + +#### 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 ( + + {word} + + + ); +} +``` + +#### WordPopover.tsx + +弹出框组件,显示词汇的详细信息。 + +```typescript +interface WordPopoverProps { + title: string; + description: string; + points: string[]; +} + +function WordPopover({ title, description, points }: WordPopoverProps) { + return ( +
+
+ {title} +
+
+
+ + {description} +
+ {points.map((point, i) => ( +
+ + {point} +
+ ))} +
+
+ ); +} +``` + +#### QuarterNavigator.tsx + +季度切换导航组件。 + +```typescript +function QuarterNavigator({ current }: { current: string }) { + const quarters = ["2023-Q1", "2023-Q2", "2023-Q3", "2023-Q4"]; + const currentIndex = quarters.indexOf(current); + + return ( +
+ + +
+
+ {current} +
+
+ + +
+ ); +} +``` + +### 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/