# 季度 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 清洗和内容生成 - ✅ 规则引擎视觉样式匹配 - ✅ 交互式词云展示 - ✅ 多语言支持 - ✅ 响应式设计 - ✅ 错误处理和监控