`, `` - - 表单控件关联 `` - - 图片添加 `alt` 属性 - -2. **ARIA 增强**: - - 按钮添加 `aria-label`(图标按钮) - - 导航添加 `aria-current="page"` - - 模态框添加 `aria-modal`, `role="dialog"` - -3. **键盘导航**: - - 焦点可见样式(`focus:ring-2`) - - 跳过导航链接(Skip to content) - - Tab 顺序合理 - -4. **色彩对比度**: - - 确保文本对比度 >= 4.5:1 - - 大文本对比度 >= 3:1 - -**验收检查**: -```bash -# Lighthouse CLI 审计 -npx lighthouse http://localhost:3000/zh --only-categories=accessibility --output=json -``` - ---- - -### 3.3 审核管理界面(方向 B) - -**目标**: 创建管理员审核项目的页面(可选,可先用 API) - -**涉及的文件**: -- `src/app/[locale]/admin/review/page.tsx` (新建) -- `src/app/[locale]/admin/review/ReviewList.tsx` (新建) -- `src/app/[locale]/admin/review/QualityBadge.tsx` (新建) - -**具体实现**: - -1. **ReviewList.tsx**: 待审核项目列表 - - 显示项目基本信息、质量评分 - - 批量操作(批准/拒绝) - - 筛选和排序 - -2. **QualityBadge.tsx**: 质量评分徽章 - - 高分(>=80): 绿色 - - 中分(50-79): 黄色 - - 低分(<50): 红色 - -**验收检查**: -- [ ] 管理员可查看待审核项目列表 -- [ ] 可单个/批量更新审核状态 -- [ ] 显示质量评分和因素明细 - ---- - -## 阶段 4:集成与优化(Week 6) - -### 4.1 标签规范化增强(方向 B) - -**目标**: 增强 Tag Janitor API 支持更多规范化操作 - -**涉及的文件**: -- `src/app/api/tags/maintenance/service.ts` -- `src/lib/validations.ts` - -**新增功能**: - -1. **标签重命名**: 批量更新标签名称 -2. **标签分类**: 为标签添加分类(技术/应用/状态) -3. **相似标签检测**: 基于名称相似度推荐合并 - -**Schema 扩展**: -```typescript -export const TagNormalizationSchema = z.object({ - apiKey: z.string().min(32), - operations: z.array(z.union([ - z.object({ type: z.literal('rename'), tagId: z.string(), newName: z.string(), newNameEn: z.string().optional() }), - z.object({ type: z.literal('categorize'), tagId: z.string(), category: z.enum(['tech', 'application', 'status']) }), - z.object({ type: z.literal('merge'), sourceIds: z.array(z.string()), targetId: z.string() }), - ])), -}); -``` - ---- - -### 4.2 性能优化(方向 A) - -**目标**: 优化首屏加载性能 - -**涉及的文件**: -- `next.config.js` -- `src/app/[locale]/page.tsx` -- `src/components/` 相关组件 - -**具体优化**: - -1. **图片优化**: - - 使用 Next.js Image 组件 - - 配置图片优先级和占位符 - -2. **代码分割**: - - 动态导入大型组件(如 MarkdownContent) - - 使用 `loading.tsx` 实现页面级加载状态 - -3. **字体优化**: - - 预加载关键字体 - - 使用 `font-display: swap` - -**验收检查**: -```bash -# Lighthouse 性能审计 -npx lighthouse http://localhost:3000/zh --only-categories=performance --output=json -``` - ---- - -## 阶段 5:测试与文档(Week 7-8) - -### 5.1 单元测试 - -**涉及的文件**: -- `src/lib/quality-scorer.test.ts` (新建) -- `src/app/api/admin/review/service.test.ts` (新建) -- `src/components/theme/ThemeProvider.test.tsx` (新建) - -**测试覆盖**: -- 质量评分算法边界情况 -- 审核状态转换逻辑 -- 主题切换持久化 - ---- - -### 5.2 E2E 测试 - -**涉及的文件**: -- `tests/e2e/theme-toggle.spec.ts` (新建) -- `tests/e2e/mobile-layout.spec.ts` (新建) -- `tests/e2e/review-workflow.spec.ts` (新建) - ---- - -### 5.3 文档更新 - -**涉及的文件**: -- `CLAUDE.md` -- `README.md` -- `n8n-workflows/README.md`(如涉及 n8n 集成) - ---- - -## 风险与缓解措施 - -| 风险 | 影响 | 可能性 | 缓解措施 | -|------|------|--------|----------| -| 主题切换导致样式不一致 | 高 | 中 | 建立完整的 dark: 类检查清单,使用 CSS 变量 | -| 审核流程复杂度超预期 | 中 | 中 | 先实现 API,管理界面可延后 | -| 质量评分算法不准确 | 中 | 中 | 先小规模测试,根据反馈迭代 | -| 移动端适配工作量大 | 中 | 高 | 优先核心页面(首页、项目列表、项目详情) | -| 无障碍改造影响现有设计 | 低 | 低 | 使用 Tailwind 的 focus: 类,不改变视觉设计 | - ---- - -## 验证步骤 - -### 方向 A 验证清单 -1. 运行 `pnpm build` 确保无构建错误 -2. 运行 `pnpm lint` 确保无 ESLint 错误 -3. Chrome DevTools 切换设备模拟器测试响应式 -4. 运行 Lighthouse 审计(性能 + 无障碍) -5. 手动测试主题切换在所有页面正常工作 -6. 键盘导航测试(仅使用 Tab/Enter/Space) - -### 方向 B 验证清单 -1. 运行数据库迁移 `pnpm prisma migrate dev` -2. 测试审核 API 端点(curl 或 Postman) -3. 验证质量评分计算逻辑(单元测试) -4. 测试 Tag Janitor API 新增功能 -5. 审核日志正确记录状态变更 - ---- - -## 文件变更总览 - -### 新建文件 -- `src/components/theme/ThemeProvider.tsx` -- `src/components/theme/ThemeToggle.tsx` -- `src/hooks/useTheme.ts` -- `src/lib/quality-scorer.ts` -- `src/app/api/admin/review/route.ts` -- `src/app/api/admin/review/service.ts` -- `src/components/layout/MobileMenu.tsx` -- `src/app/[locale]/admin/review/page.tsx`(可选) -- `src/app/[locale]/admin/review/ReviewList.tsx`(可选) -- `src/app/[locale]/admin/review/QualityBadge.tsx`(可选) - -### 修改文件 -- `prisma/schema.prisma` -- `src/app/layout.tsx` -- `src/app/[locale]/layout.tsx` -- `src/lib/validations.ts` -- `src/hooks/useProjects.ts` -- `src/components/project/ProjectCard.tsx` -- `src/components/project/ProjectList.tsx` -- `src/messages/zh.json` -- `src/messages/en.json` -- `tailwind.config.ts`(可能需要调整) - ---- - -## 下一步行动 - -确认此计划后,运行以下命令开始实施: - -```bash -/oh-my-claudecode:start-work next-phase-features -``` - -建议优先级: -1. **Phase 1.1** - 数据库 Schema 扩展(阻塞其他方向 B 任务) -2. **Phase 1.2** - 主题系统基础(阻塞其他方向 A 任务) -3. 之后两个方向可并行推进 diff --git a/.omc/plans/postponed-features/README.md b/.omc/plans/postponed-features/README.md deleted file mode 100644 index cf1ddbb..0000000 --- a/.omc/plans/postponed-features/README.md +++ /dev/null @@ -1,88 +0,0 @@ -# 暂停开发的功能 - -本目录存放暂停开发的功能的设计文档和计划,这些功能将在后续版本中继续开发。 - -## 目录结构 - -``` -postponed-features/ -├── README.md # 本文件 -├── drop-tables.sql # 数据库表删除脚本 -├── keyword-cloud/ # AI 词云功能 -│ ├── 2026-01-25-keyword-cloud-system-design.md -│ └── 2026-01-25-keyword-cloud-implementation.md -├── timeline/ # AI 时间轴功能 -│ ├── 2025-01-25-ai-timeline-feature-design.md -│ └── 2025-01-25-ai-timeline-implementation.md -├── hotpot cloud/ # 词云设计原型 -└── timeline/ # 时间轴设计原型 -``` - -## 暂停功能说明 - -### 1. AI 词云 (Keyword Cloud) - -**功能描述**: 自动化采集 Google Trends 数据,展示季度 AI 热点词汇词云。 - -**暂停原因**: 优先级调整,后续版本继续开发。 - -**技术栈**: -- 前端: Next.js 15 + React + Tailwind CSS -- 后端: Prisma + PostgreSQL -- 自动化: n8n 工作流 -- 数据源: Google Trends API - -**数据库表**: -- `keywords` - 关键词数据 -- `quarters` - 季度元数据 -- `visual_style_rules` - 视觉样式规则 -- `keyword_cloud_error_logs` - 错误日志 - -### 2. AI 时间轴 (AI Timeline) - -**功能描述**: 展示 AI 大语言模型的发展历程,从 2017 年 Transformer 到今天。 - -**暂停原因**: 优先级调整,后续版本继续开发。 - -**技术栈**: -- 前端: Next.js 15 + React + Tailwind CSS -- 后端: Prisma + PostgreSQL - -**数据库表**: -- `ai_events` - AI 事件数据 - -### 3. 博客 (Blog) - -**功能描述**: 博客文章发布和管理系统。 - -**状态**: 未实现,仅保留了导航链接占位。 - -**暂停原因**: 优先级调整,后续版本继续开发。 - -## 恢复开发指南 - -当需要恢复这些功能时: - -1. **恢复数据库表**: - ```bash - # 参考 drop-tables.sql 中的表结构定义 - # 在 prisma/schema.prisma 中恢复对应的模型 - pnpm prisma migrate dev --name restore-postponed-features - ``` - -2. **恢复代码**: - - 从 git 历史中恢复相关代码文件 - - 或参考设计文档重新实现 - -3. **恢复国际化消息**: - - 在 `src/messages/zh.json` 和 `src/messages/en.json` 中添加对应的命名空间 - -4. **恢复导航链接**: - - 在国际化消息的 `navigation` 部分添加对应的导航项 - -## 相关文档 - -- [AI 词云系统设计](./keyword-cloud/2026-01-25-keyword-cloud-system-design.md) -- [AI 词云实现文档](./keyword-cloud/2026-01-25-keyword-cloud-implementation.md) -- [AI 时间轴功能设计](./timeline/2025-01-25-ai-timeline-feature-design.md) -- [AI 时间轴实现文档](./timeline/2025-01-25-ai-timeline-implementation.md) diff --git a/.omc/plans/postponed-features/drop-tables.sql b/.omc/plans/postponed-features/drop-tables.sql deleted file mode 100644 index 7ac0c1b..0000000 --- a/.omc/plans/postponed-features/drop-tables.sql +++ /dev/null @@ -1,45 +0,0 @@ --- ===================================================== --- 暂停功能的数据库表删除脚本 --- 创建时间: 2026-02-21 --- 说明: 删除 AI 词云和 AI 时间轴相关的数据库表 --- 警告: 执行前请确保已备份相关数据 --- ===================================================== - --- ================================ --- AI 词云相关表 --- ================================ - --- 删除关键词表 (依赖 quarters 表) -DROP TABLE IF EXISTS keywords CASCADE; - --- 删除季度元数据表 -DROP TABLE IF EXISTS quarters CASCADE; - --- 删除视觉样式规则表 -DROP TABLE IF EXISTS visual_style_rules CASCADE; - --- 删除词云错误日志表 -DROP TABLE IF EXISTS keyword_cloud_error_logs CASCADE; - --- ================================ --- AI 时间轴相关表 --- ================================ - --- 删除 AI 事件表 -DROP TABLE IF EXISTS ai_events CASCADE; - --- ================================ --- 清理相关索引 (PostgreSQL 会自动删除) --- ================================ - --- 注意: 以下索引会随表删除自动清理 --- idx_keyword_quarterId --- idx_keyword_trendScore --- idx_keyword_word --- idx_quarter_displayOrder --- idx_quarter_quarter --- idx_visualStyleRule_enabled --- idx_visualStyleRule_scoreRange --- idx_keywordCloudErrorLog_errorType --- idx_keywordCloudErrorLog_quarter --- ai_events 的 eventDate 和 createdAt 索引 diff --git a/.omc/plans/postponed-features/hotpot cloud/code.html b/.omc/plans/postponed-features/hotpot cloud/code.html deleted file mode 100644 index c2d0f38..0000000 --- a/.omc/plans/postponed-features/hotpot cloud/code.html +++ /dev/null @@ -1,323 +0,0 @@ - - - - -Agent Park: AI Keyword Evolution - - - - - - - - - - - - - - - - - - - - - - -AP -Agent Park - - -Navigator -DuckDB Stats -Manifesto - - - JOIN THE FLOCK - - - - - - - EXPERIMENTAL DATA - - - THE EVOLUTION OF -AI DISCOURSE - - - From "Large Language Models" to "Agentic Workflows". Use the arrows below to explore the shifting narrative. - - - - - - - - - - - - -chevron_left - - - - Q1 2023 - -"The dawn of the chat interface." - - -chevron_right - - - - - - 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 - - - "ChatGPT" search volume peaked +400% in March 2023! - - - - GENESISOF CHAT - - - - - - - WHAT'S NEXT? - - - -Future Prediction Zone - -??? -AGI? -Small Language Models -On-Device AI -Liquid Neural Nets -Sovereign AI -World Models - - - - Submit a Prediction - - - - - - - - \ No newline at end of file diff --git a/.omc/plans/postponed-features/hotpot cloud/screen.png b/.omc/plans/postponed-features/hotpot cloud/screen.png deleted file mode 100644 index 134d4e3..0000000 Binary files a/.omc/plans/postponed-features/hotpot cloud/screen.png and /dev/null differ diff --git a/.omc/plans/postponed-features/keyword-cloud/2026-01-25-keyword-cloud-implementation.md b/.omc/plans/postponed-features/keyword-cloud/2026-01-25-keyword-cloud-implementation.md deleted file mode 100644 index 938ed7c..0000000 --- a/.omc/plans/postponed-features/keyword-cloud/2026-01-25-keyword-cloud-implementation.md +++ /dev/null @@ -1,2408 +0,0 @@ -# 季度 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 ( - - {/* 上一个季度按钮 */} - canGoPrev && onNavigate(quarters[currentIndex - 1])} - aria-label="Previous quarter" - > - - - - {/* 当前季度显示 */} - - - {current} - - - - {/* 下一个季度按钮 */} - canGoNext && onNavigate(quarters[currentIndex + 1])} - aria-label="Next quarter" - > - - - - ); -} -``` - -**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 ( - - - - 加载失败 - - window.location.reload()} - className="bg-primary text-black border-2 border-black px-4 py-2 font-bold hover:bg-yellow-500 transition-colors" - > - 重试 - - - - ); - } - - 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/.omc/plans/postponed-features/keyword-cloud/2026-01-25-keyword-cloud-system-design.md b/.omc/plans/postponed-features/keyword-cloud/2026-01-25-keyword-cloud-system-design.md deleted file mode 100644 index 27f8d8d..0000000 --- a/.omc/plans/postponed-features/keyword-cloud/2026-01-25-keyword-cloud-system-design.md +++ /dev/null @@ -1,716 +0,0 @@ -# 季度 AI 热点词云系统设计文档 - -**创建日期**: 2026-01-25 -**功能类型**: 数据驱动可视化 -**技术栈**: n8n + Next.js + PostgreSQL + Prisma - ---- - -## 一、功能概述 - -季度 AI 热点词云是一个**自动化数据驱动的可视化词云系统**,通过 n8n 工作流从 Google Trends 采集 AI 相关热点词汇,经 AI 清洗和规则引擎处理后,自动入库并在前端展示。 - -### 核心目标 - -1. **内容营销**: 吸引访客回访查看每季度更新,提供社交分享素材 -2. **教育参考**: 帮助新手理解 AI 技术演进历程 -3. **数据洞察**: 展示 AI 领域热点变化趋势 - -### 用户旅程 - -1. 用户访问 `/keyword-cloud` 页面 -2. 看到当前季度的 AI 热点词云(如 2024-Q1) -3. 鼠标悬停在词汇上,查看详细描述和要点 -4. 点击左右箭头切换不同季度,浏览历史热点 -5. 可视化展示:词汇大小代表搜索热度,颜色代表分类 - ---- - -## 二、系统架构 - -### 架构图 - -``` -┌─────────────┐ ┌─────────────┐ ┌──────────────┐ -│ Google │ │ n8n │ │ PostgreSQL │ -│ Trends API │───▶│ Workflow │───▶│ Database │ -└─────────────┘ └─────────────┘ └──────────────┘ - │ - ▼ - ┌─────────────┐ - │ Next.js │ - │ Frontend │ - └─────────────┘ -``` - -### 数据流向 - -1. **定时触发**: n8n Schedule 每季度末自动执行 -2. **数据采集**: Google Trends API 获取热门搜索词 -3. **AI 清洗**: 过滤无关词汇,生成描述和要点 -4. **规则匹配**: 根据热度分数分配视觉样式 -5. **数据入库**: 写入 PostgreSQL 数据库 -6. **前端展示**: Next.js 从数据库读取并渲染词云 - ---- - -## 三、数据库模型 - -### 3.1 Quarter 表(季度元数据) - -```prisma -model Quarter { - id Int @id @default(autoincrement()) - quarter String @unique // "2023-Q1", "2023-Q2" - title String @db.Text // "2023年第一季度" - titleEn String? @db.Text // "Q1 2023" - subtitle String? @db.Text // "聊天界面的黎明" - subtitleEn String? @db.Text // "The dawn of chat interface" - displayOrder Int @default(0) // 前端排序 - isActive Boolean @default(true) // 是否显示 - keywords Keyword[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([quarter]) - @@index([displayOrder]) -} -``` - -### 3.2 Keyword 表(关键词核心数据) - -```prisma -model Keyword { - id Int @id @default(autoincrement()) - word String // "ChatGPT" - trendScore Int // 0-100, 从 Google Trends 获取 - - // 外键关联 - quarterId Int - quarter Quarter @relation(fields: [quarterId], references: [id], onDelete: Cascade) - - // 内容字段(支持中英双语) - description String @db.Text // AI 生成的一句话描述 - descriptionEn String? @db.Text // 英文描述 - detailPoints Json // JSON 数组: ["要点1", "要点2", "要点3"] - detailPointsEn Json? // 英文版要点 - - // 视觉样式配置 - visualConfig Json // {color, size, rotation, border} - - // 元数据 - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([quarterId]) - @@index([trendScore]) - @@index([word]) -} -``` - -**visualConfig 字段结构示例**: -```json -{ - "color": "secondary", - "size": "text-5xl", - "rotation": "rotate-1", - "border": "border-4" -} -``` - -### 3.3 VisualStyleRule 表(视觉样式规则配置) - -```prisma -model VisualStyleRule { - id Int @id @default(autoincrement()) - name String @unique // "热门大词-金色" - - // 分数区间 - minScore Int // 90 - maxScore Int // 100 - - // 视觉属性 - color String // "primary", "secondary", "accent" - size String // "text-5xl", "text-3xl", "text-xl" - border String // "border-4", "border-2" - rotation String? // "rotate-1", "rotate-2", null - - // 控制 - priority Int @default(0) // 优先级,分数重叠时按优先级 - enabled Boolean @default(true) // 是否启用 - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([enabled]) - @@index([minScore, maxScore]) -} -``` - -### 3.4 KeywordCloudErrorLog 表(错误日志) - -```prisma -model KeywordCloudErrorLog { - id Int @id @default(autoincrement()) - quarter String // "2023-Q1" - keyword String? // "ChatGPT" - errorType String // "INVALID_DATA", "API_ERROR", "DB_ERROR" - errorMessage String @db.Text // 详细错误信息 - rawData Json? // 原始数据便于调试 - createdAt DateTime @default(now()) - - @@index([quarter]) - @@index([errorType]) -} -``` - ---- - -## 四、n8n 工作流设计 - -### 4.1 工作流概览 - -5 个核心节点实现从数据采集到入库的完整流程。 - -### 4.2 节点详细配置 - -#### 节点 1: Schedule Trigger(定时触发) - -- **类型**: `n8n-nodes-base.scheduleTrigger` -- **Cron 表达式**: `0 0 23 28-31 * *` (每季度末最后一天的 23:00) -- **月份判断**: Function 节点检查当前月份(3/6/9/12),如果不是则跳过 -- **输出**: 当前季度的标识(如 "2024-Q1") - -#### 节点 2: Google Trends 采集 - -- **类型**: `@gamal.dev/n8n-nodes-google-trends` -- **配置参数**: - - `keywords`: ["AI", "artificial intelligence", "machine learning", "GPT", "LLM"] - - `timeRange`: 当前季度的 3 个月(如 2024-01-01 to 2024-03-31) - - `category`: "Science > Computer Science > AI" - - `geo`: "GB" -- **输出示例**: - ```json - { - "keyword": "ChatGPT", - "trendScore": 95, - "rising": true - } - ``` - -#### 节点 3: AI 清洗和内容生成 - -- **类型**: `@n8n/n8n-nodes-langchain.lmChatChain` -- **Prompt 模板**: - ``` - 你是一个 AI 领域专家。以下是 Google Trends 采集的热门关键词列表: - {{ $json.all() }} - - 任务: - 1. 过滤掉与 AI/机器学习无关的关键词 - 2. 为每个关键词生成中文描述(10-50字) - 3. 生成 3 条详细要点(每条 10-30 字,客观描述,避免营销用语) - - 输出格式(JSON 数组): - [ - { - "word": "ChatGPT", - "trendScore": 95, - "description": "OpenAI 开发的对话式人工智能助手", - "detailPoints": ["支持多轮对话", "基于 GPT-3.5 架构", "2023年用户突破1亿"] - } - ] - ``` - -#### 节点 4: 规则引擎匹配 - -- **类型**: `n8n-nodes-base.code` -- **逻辑**: - 1. HTTP Request 获取 `VisualStyleRule` 表数据 - - 方法: GET - - URL: `{{ $env.API_URL }}/api/keyword-cloud/rules` - 2. 按 `priority` 排序规则 - 3. 遍历每个关键词,匹配第一个符合的规则(`minScore <= trendScore <= maxScore`) - 4. 将视觉配置注入数据 - -#### 节点 5: 批量写入数据库 - -- **类型**: `n8n-nodes-base.httpRequest` -- **方法**: POST -- **URL**: `{{ $env.API_URL }}/api/keyword-cloud/keywords` -- **认证**: Bearer Token(环境变量 `API_KEY`) -- **Body**: - ```json - { - "quarter": "2024-Q1", - "keywords": [ - { - "word": "ChatGPT", - "trendScore": 95, - "description": "...", - "detailPoints": ["...", "...", "..."], - "visualConfig": {...} - } - ] - } - ``` -- **批量处理**: 超过 20 个关键词时分批提交 - -### 4.3 错误处理 - -- 每个节点设置 `continueOnFail: true` -- 错误日志写入 `KeywordCloudErrorLog` 表 -- 关键错误发送通知(Email/Slack) - ---- - -## 五、API 端点设计 - -### 5.1 GET /api/keyword-cloud/quarters - -获取季度列表。 - -**查询参数**: -- `isActive` (可选): 只返回激活的季度 - -**响应示例**: -```json -{ - "quarters": [ - { - "id": 1, - "quarter": "2024-Q1", - "title": "2024年第一季度", - "titleEn": "Q1 2024", - "subtitle": "聊天界面的黎明", - "subtitleEn": "The dawn of chat interface", - "displayOrder": 0, - "keywordCount": 15 - } - ] -} -``` - -**排序**: 按 `displayOrder` ASC - -### 5.2 GET /api/keyword-cloud/keywords/[quarter] - -获取指定季度的关键词列表。 - -**路径参数**: -- `quarter`: 季度标识(如 "2024-Q1") - -**响应示例**: -```json -{ - "quarter": "2024-Q1", - "title": "2024年第一季度", - "keywords": [ - { - "id": 1, - "word": "ChatGPT", - "trendScore": 95, - "description": "OpenAI 开发的对话式 AI 助手", - "detailPoints": ["支持多轮对话", "基于 GPT-3.5", "2023用户破亿"], - "visualConfig": { - "color": "secondary", - "size": "text-5xl", - "border": "border-4", - "rotation": "rotate-1" - } - } - ] -} -``` - -**排序**: 按 `trendScore` DESC - -### 5.3 POST /api/keyword-cloud/keywords - -n8n 工作流写入关键词数据。 - -**认证**: Bearer Token (WEBHOOK_API_KEY) - -**请求体**: -```json -{ - "quarter": "2024-Q1", - "keywords": [ - { - "word": "ChatGPT", - "trendScore": 95, - "description": "...", - "detailPoints": ["...", "...", "..."], - "visualConfig": {...} - } - ] -} -``` - -**响应**: -```json -{ - "success": true, - "created": 15, - "failed": 2, - "errors": [ - { "word": "Invalid", "error": "trendScore out of range" } - ] -} -``` - -**逻辑**: -1. 验证 API Key -2. 查找或创建 `Quarter` 记录 -3. 批量创建 `Keyword` 记录(Prisma `createMany`) -4. 失败记录写入 `KeywordCloudErrorLog` 表 -5. 返回成功/失败统计 - -### 5.4 GET /api/keyword-cloud/rules - -获取视觉样式规则配置(n8n 规则引擎使用)。 - -**查询参数**: -- `enabled` (可选): 只返回启用的规则 - -**响应示例**: -```json -{ - "rules": [ - { - "id": 1, - "name": "热门大词-金色", - "minScore": 90, - "maxScore": 100, - "visualConfig": { - "color": "primary", - "size": "text-5xl", - "border": "border-4", - "rotation": "rotate-1" - }, - "priority": 0, - "enabled": true - } - ] -} -``` - -**排序**: 按 `priority` ASC - -### 5.5 POST /api/keyword-cloud/rules - -创建新的视觉样式规则(管理员功能)。 - -**请求体**: 同单个规则对象 - -**验证**: -- `minScore < maxScore` -- 必填字段检查 -- 颜色值必须是预定义的颜色类别 - ---- - -## 六、前端组件设计 - -### 6.1 路由结构 - -``` -src/app/[locale]/keyword-cloud/ -├── page.tsx # 主页面 -└── components/ - ├── KeywordCloud.tsx # 词云容器组件 - ├── CloudWord.tsx # 单个词汇组件 - ├── QuarterNavigator.tsx # 季度切换导航 - ├── WordPopover.tsx # 弹出框详情 - └── ProgressIndicator.tsx # 进度条 -``` - -### 6.2 核心组件 - -#### KeywordCloud.tsx - -词云容器组件,负责数据获取和布局。 - -```typescript -interface KeywordCloudProps { - quarter: string; -} - -function KeywordCloud({ quarter }: KeywordCloudProps) { - const { data, isLoading } = useKeywordData(quarter); - - return ( - - - - - {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 ( - - navigate(quarters[currentIndex - 1])} - > - chevron_left - - - - - {current} - - - - navigate(quarters[currentIndex + 1])} - > - chevron_right - - - ); -} -``` - -### 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/ diff --git a/.omc/plans/postponed-features/timeline/2025-01-25-ai-timeline-feature-design.md b/.omc/plans/postponed-features/timeline/2025-01-25-ai-timeline-feature-design.md deleted file mode 100644 index 5cd871d..0000000 --- a/.omc/plans/postponed-features/timeline/2025-01-25-ai-timeline-feature-design.md +++ /dev/null @@ -1,494 +0,0 @@ -# AI 时间轴功能设计文档 - -**创建日期**: 2025-01-25 -**目标用户**: 开发者和 AI 研究者 -**设计理念**: 学术+产品平衡,聚焦大语言模型发展历程(从 2017 Transformer 开始) - ---- - -## 1. 系统架构 - -### 核心组件 - -**前端展示系统** -- 路由: `src/app/[locale]/timeline/page.tsx` -- 渲染策略: ISR (1小时缓存) -- 设计风格: Neo-brutalism,复用现有设计系统 -- 数据展示: 按年份分组的卡片堆叠时间轴 - -**后端 API** -- `POST /api/events` - 创建事件(供 n8n 调用,需 API Key 认证) -- `GET /api/events` - 获取所有事件(支持年份筛选) -- `GET /api/events/:id` - 获取单个事件详情 -- `PATCH /api/events/:id` - 更新事件(可选) -- `DELETE /api/events/:id` - 删除事件(可选) - -**数据采集系统 (n8n)** -- 历史数据初始化流程: 一次性运行,批量收集 2017-2025 事件 -- 增量更新流程: 每周一运行,收集最近 7 天新事件 -- 三 Agent 协作: 搜索 → 筛选 → 格式化 → HTTP Request 提交 - -**数据库** -- `AIEvent` 表: 存储所有事件 -- 无需标签系统(基础版数据结构) - ---- - -## 2. 数据库 Schema - -### Prisma 模型 - -```prisma -model AIEvent { - id String @id @default(cuid()) - title String - titleEn String? // 英文标题(可选) - eventDate DateTime // 事件发生的具体日期 - description String - descriptionEn String? // 英文描述 - imageUrl String // 事件配图 URL - sourceUrl String? // 原始来源链接 - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([eventDate(sort: Desc)]) // 按日期降序索引 - @@index([createdAt]) -} -``` - -### 字段说明 -- **国际化字段**: `title`/`titleEn`, `description`/`descriptionEn` 与项目表保持一致 -- **日期字段**: 使用 `DateTime` 支持时间范围筛选和排序 -- **索引优化**: `eventDate` 降序索引提升"最近事件"查询性能 -- **图片存储**: 直接存储 URL(可以是 Unsplash、项目截图等) - -### 迁移命令 -```bash -pnpm prisma migrate dev --name add_ai_events_table -``` - ---- - -## 3. API 端点设计 - -### 数据验证 Schema - -在 `src/lib/validations.ts` 新增: - -```typescript -const AIEventInputSchema = z.object({ - title: z.string().min(1).max(200), - titleEn: z.string().max(200).optional(), - eventDate: z.string().datetime(), // ISO 8601 格式 - description: z.string().min(10).max(500), - descriptionEn: z.string().max(500).optional(), - imageUrl: z.string().url(), - sourceUrl: z.string().url().optional(), -}); -``` - -### POST /api/events (n8n 调用) - -**认证**: -```typescript -const apiKey = request.headers.get('X-API-Key'); -if (!apiKey || !crypto.timingSafeEqual( - Buffer.from(apiKey), - Buffer.from(process.env.WEBHOOK_API_KEY!) -)) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); -} -``` - -**验证**: -```typescript -const body = await request.json(); -const validationResult = AIEventInputSchema.array().safeParse(body); -if (!validationResult.success) { - return NextResponse.json({ - error: 'Validation failed', - details: validationResult.error - }, { status: 400 }); -} -``` - -**创建**: -```typescript -const events = await prisma.aIEvent.createMany({ - data: validationResult.data, - skipDuplicates: true, -}); -return NextResponse.json({ created: events.count }, { status: 201 }); -``` - -### GET /api/events (前端调用) - -支持查询参数: -- `?year=2024` - 筛选特定年份 -- `?limit=50` - 限制返回数量 -- `?offset=0` - 分页偏移 - -返回按 `eventDate` 降序排列的事件列表。 - -### 调用示例 - -```bash -curl -X POST https://your-domain.com/api/events \ - -H "Content-Type: application/json" \ - -H "X-API-Key: YOUR_WEBHOOK_API_KEY" \ - -d '{ - "title": "GPT-4 发布", - "eventDate": "2023-03-14T00:00:00Z", - "description": "OpenAI 发布多模态大语言模型", - "imageUrl": "https://example.com/gpt4.jpg" - }' -``` - ---- - -## 4. 前端页面实现 - -### 页面结构 - -`src/app/[locale]/timeline/page.tsx`: - -```tsx -import { getAIEvents } from '@/hooks/useAIEvents'; -import { TimelineSection } from '@/components/timeline/TimelineSection'; - -export const revalidate = 3600; // ISR 1小时 - -export default async function TimelinePage() { - const events = await getAIEvents(); - - // 按年份分组 - const eventsByYear = events.reduce((acc, event) => { - const year = new Date(event.eventDate).getFullYear(); - if (!acc[year]) acc[year] = []; - acc[year].push(event); - return acc; - }, {} as Record); - - return ( - - - AI 发展时间轴 - - - - {Object.entries(eventsByYear) - .sort(([a], [b]) => Number(b) - Number(a)) // 降序 - .map(([year, yearEvents]) => ( - - ))} - - - ); -} -``` - -### 数据获取函数 - -`src/hooks/useAIEvents.ts`: - -```typescript -export async function getAIEvents() { - const events = await prisma.aIEvent.findMany({ - orderBy: { eventDate: 'desc' }, - take: 100, // 最多 100 条 - }); - return events; -} -``` - -### 组件设计 - -- **TimelineSection**: 渲染单个年份的卡片堆叠区域,复用原型的动画效果 -- **EventCard**: 单个事件卡片(图片、标题、描述、日期) -- **响应式**: 移动端垂直堆叠,桌面端横向卡片堆叠 - ---- - -## 5. n8n Workflow 设计 - -### 流程 A: 历史数据初始化(一次性运行) - -**1. 历史搜索 Agent** -- 按年份批量搜索: 2017-2025 -- 搜索关键词: `"AI breakthrough YEAR"`, `"LLM release YEAR"`, `"GPT model YEAR"` -- 时间范围: 每年 1月1日 - 12月31日 -- 输出: 每年 50-100 条搜索结果 - -**2. 历史筛选 Agent** -- 权威来源: `['arxiv.org', 'openai.com', 'anthropic.com', 'google.ai', 'meta.ai', 'deepmind.com']` -- 去重逻辑: 相同标题或 URL 只保留一个 -- 输出: 每年精选 10-20 个事件 - -**3. 历史格式化 Agent** -- 提取完整信息: 标题、准确日期、详细描述、配图 -- 对描述进行润色和本地化(翻译成中文) -- 补充缺失图片(使用 Unsplash 或项目官网截图) - -**4. HTTP Request: 批量提交** -- POST `/api/events` -- 每次提交 10-20 个事件(按年份分批) -- 记录成功/失败状态 - -### 流程 B: 增量更新(每周运行) - -**触发器**: Cron node,每周一早上 9:00 - -**1. 增量搜索 Agent** -- 时间范围: 最近 7 天 -- 搜索关键词: `["AI news", "LLM release", "model launch"]` -- 输出: 10-20 条最新搜索结果 - -**2. 增量筛选 Agent** -- 权威来源检查(同历史流程) -- 额外检查: 查询数据库避免重复 `sourceUrl` -- 输出: 2-5 个新事件 - -**3. 增量格式化 Agent** -- 快速格式化: 提取核心信息 -- 描述保持简短(直接使用摘要) -- 图片优先使用新闻配图 - -**4. HTTP Request: 增量提交** -- POST `/api/events` -- 一次性提交所有新事件 -- 失败时发送告警邮件 - -### 错误处理 - -- **重试策略**: 每个 Agent 失败后等待 5s/10s/20s 重试,最多 3 次 -- **降级处理**: 图片提取失败使用默认图片; 日期模糊使用当月 1 日 -- **告警机制**: 最终失败时发送邮件并附执行日志 -- **部分成功**: 批量提交时即使部分失败,也记录成功的事件 - ---- - -## 6. 数据验证和错误处理 - -### API 端点保护 - -**认证机制**: 使用 `crypto.timingSafeEqual` 防止时序攻击 - -**请求体验证**: Zod schema 验证所有字段 - -**去重逻辑**: 基于 `sourceUrl` 的唯一索引(如果提供) - -**长度限制**: -- 标题 ≤200 字符 -- 描述 ≤500 字符 - -**必填字段**: `title`, `eventDate`, `description`, `imageUrl` - -**URL 验证**: `imageUrl` 和 `sourceUrl` 必须是有效的 HTTP/HTTPS URL - -### 前端错误边界 - -```tsx -export default async function TimelinePage() { - try { - const events = await getAIEvents(); - if (!events.length) { - return ; - } - return ; - } catch (error) { - console.error('Failed to load events:', error); - return ; - } -} -``` - ---- - -## 7. 测试策略 - -### 单元测试 (Vitest) - -**API 端点测试** (`src/app/api/events/route.test.ts`): -```typescript -describe('POST /api/events', () => { - it('should create event with valid data', async () => { - const response = await POST(request_mock); - expect(response.status).toBe(201); - }); - - it('should reject invalid API key', async () => { - const response = await POST(bad_request_mock); - expect(response.status).toBe(401); - }); - - it('should validate required fields', async () => { - const response = await POST(invalid_data_mock); - expect(response.status).toBe(400); - }); -}); -``` - -**数据验证测试** (`src/lib/validations.test.ts`): -```typescript -describe('AIEventInputSchema', () => { - it('should validate valid event', () => { - expect(() => AIEventInputSchema.parse(validEvent)).not.toThrow(); - }); - - it('should reject missing title', () => { - expect(() => AIEventInputSchema.parse({ ...validEvent, title: '' })) - .toThrow(); - }); -}); -``` - -### E2E 测试 (chrome-devtools-mcp) - -**测试流程**: -1. 启动开发服务器: `pnpm dev` -2. 使用 chrome-devtools-mcp 工具: - - `new_page`: http://localhost:3000/timeline - - `take_snapshot`: 验证页面结构 - - `take_screenshot`: 对比设计原型 - - `evaluate_script`: 检查事件数据渲染 - - `list_console_messages`: 确保无 JavaScript 错误 -3. 测试筛选功能: - - `navigate_page`: url=/timeline?year=2024 - - `take_snapshot`: 验证只显示 2024 年事件 - -**手动测试清单**: -- [ ] 页面加载成功,无 console 错误 -- [ ] 事件按年份正确分组显示 -- [ ] 卡片堆叠动画正常工作 -- [ ] 响应式布局在移动端正常 -- [ ] ISR 缓存在 1 小时后正确更新 - -### 集成测试 (n8n Workflow) - -- 手动触发历史数据初始化流程,验证数据库事件创建 -- 手动触发增量更新流程,验证新事件添加 -- 测试错误场景: API 不可用、数据格式错误、网络超时 - -### 测试数据准备 - -使用 `pnpm prisma db seed` 创建种子数据: -- Seed 脚本插入 5-10 个示例事件(2023-2025 真实事件) -- 方便开发和手动测试 - ---- - -## 8. 部署和实施步骤 - -### 阶段 1: 数据库和后端 (1-2 天) - -```bash -# 更新 schema -vim prisma/schema.prisma - -# 生成并运行迁移 -pnpm prisma migrate dev --name add_ai_events_table -pnpm prisma generate - -# 创建验证 schema -vim src/lib/validations.ts - -# 创建 API route -mkdir src/app/api/events -vim src/app/api/events/route.ts - -# 创建数据获取函数 -vim src/hooks/useAIEvents.ts - -# 手动测试 API -curl -X POST http://localhost:3000/api/events \ - -H "Content-Type: application/json" \ - -H "X-API-Key: $WEBHOOK_API_KEY" \ - -d '{ - "title": "测试事件", - "eventDate": "2025-01-25T00:00:00Z", - "description": "这是一个测试事件", - "imageUrl": "https://example.com/image.jpg" - }' -``` - -### 阶段 2: 前端页面 (2-3 天) - -```bash -# 创建 timeline 页面 -mkdir -p src/app/[locale]/timeline -vim src/app/[locale]/timeline/page.tsx - -# 创建组件 -mkdir src/components/timeline -vim src/components/timeline/TimelineSection.tsx -vim src/components/timeline/EventCard.tsx - -# 更新 header 添加导航链接 -vim src/components/layout/Header.tsx - -# 使用 chrome-devtools-mcp 测试页面效果 -``` - -### 阶段 3: n8n Workflow 配置 (1-2 天) - -1. 在 n8n 中创建历史数据初始化 workflow -2. 创建增量更新 workflow,设置每周一 9:00 触发 -3. 配置环境变量 `WEBHOOK_API_KEY` -4. 手动运行历史 workflow,初始化 2017-2025 数据 -5. 验证增量 workflow 是否正常工作 - -### 阶段 4: 测试和优化 (1 天) - -1. 使用 chrome-devtools-mcp 进行完整测试 -2. 修复发现的问题 -3. 优化性能(ISR 缓存、图片加载) -4. 准备生产环境部署 - -### 阶段 5: 部署到生产 - -1. 更新 Vercel 环境变量(确保 `WEBHOOK_API_KEY` 已设置) -2. 运行数据库迁移(如果需要): `pnpm prisma db push` -3. 部署代码到 Vercel -4. 在 n8n 中更新 API endpoint 为生产地址 -5. 监控第一次增量运行 - ---- - -## 9. 关键设计决策 - -### 为什么选择基础版事件数据? -- 符合 YAGNI 原则,快速上线 -- 避免过度设计,聚焦核心价值 -- 后续可扩展(如添加用户互动、点赞等) - -### 为什么不需要标签系统? -- 从 UI 原型看,卡片只显示核心信息(标题、日期、描述、图片) -- 时间轴本身就是按年份组织,无需额外分类 -- 减少数据复杂度,提升性能 - -### 为什么设计两套 n8n 流程? -- **历史流程**: 注重数据质量和完整性,处理 2017-2025 的大量数据 -- **增量流程**: 注重效率和及时性,每周自动更新 -- 分离关注点,便于独立优化和调试 - -### 为什么使用 ISR 而非纯静态? -- 事件数据每周更新,需要一定时效性 -- ISR 1小时缓存平衡了性能和新鲜度 -- 避免每次请求都查询数据库 - ---- - -## 10. 后续优化方向 - -1. **搜索和筛选**: 添加关键词搜索、标签筛选 -2. **用户互动**: 点赞、评论、分享功能 -3. **相关推荐**: 基于事件标签推荐相关项目 -4. **多语言支持**: 完善英文版本的描述和翻译 -5. **图片优化**: 使用 Next.js Image 组件优化图片加载 -6. **数据可视化**: 添加图表展示 AI 发展趋势 -7. **导出功能**: 支持导出时间轴为 PDF/Markdown diff --git a/.omc/plans/postponed-features/timeline/2025-01-25-ai-timeline-implementation.md b/.omc/plans/postponed-features/timeline/2025-01-25-ai-timeline-implementation.md deleted file mode 100644 index 6a78506..0000000 --- a/.omc/plans/postponed-features/timeline/2025-01-25-ai-timeline-implementation.md +++ /dev/null @@ -1,2156 +0,0 @@ -# AI 时间轴功能实施计划 - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**目标:** 构建一个展示 AI 大语言模型发展历程的时间轴功能,支持 2017 年至今的里程碑事件展示,通过 n8n workflow 自动收集和更新数据。 - -**架构:** 采用 Next.js 15 App Router + Prisma + PostgreSQL 架构,前端使用 ISR 缓存策略,后端提供 RESTful API,数据采集通过 n8n workflow 中的三个 Agent 协作完成(搜索→筛选→格式化→提交)。 - -**技术栈:** Next.js 15, Prisma, PostgreSQL, n8n, Web Search MCP, Zod, Vitest, chrome-devtools-mcp - ---- - -## 前置准备 - -### Task 0: 创建 Git Worktree - -**文件:** -- Create: N/A (使用 git worktree) - -**Step 1: 创建隔离的工作空间** - -```bash -cd /Users/caihaohan/Code/agent_park -git worktree add ../agent_park-timeline main -b feature/ai-timeline -cd ../agent_park-timeline -``` - -**Step 2: 验证 worktree 创建成功** - -```bash -pwd -# Expected output: /Users/caihaohan/Code/agent_park-timeline - -git branch -# Expected output: * feature/ai-timeline -``` - -**Step 3: 安装依赖(如果需要)** - -```bash -pnpm install -``` - -**Step 4: 启动开发服务器** - -```bash -# 先检查是否有进程运行在 3000 端口 -lsof -ti:3000 | xargs kill -9 2>/dev/null || true -pnpm dev -``` - -**Step 5: 验证服务器启动成功** - -访问: http://localhost:3000 -Expected: Agent Park 首页正常显示 - -**Step 6: 提交 worktree 初始化** - -```bash -cd /Users/caihaohan/Code/agent_park-timeline -git add . -git commit -m "chore: initialize worktree for AI timeline feature" -``` - ---- - -## 阶段 1: 数据库 Schema - -### Task 1: 添加 AIEvent 模型到 Prisma Schema - -**文件:** -- Modify: `prisma/schema.prisma` - -**Step 1: 打开 schema 文件** - -```bash -vim prisma/schema.prisma -# 或使用你喜欢的编辑器 -``` - -**Step 2: 在文件末尾添加 AIEvent 模型** - -在最后一个 `}` 后面添加: - -```prisma -model AIEvent { - id String @id @default(cuid()) - title String - titleEn String? - eventDate DateTime - description String - descriptionEn String? - imageUrl String - sourceUrl String? - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([eventDate(sort: Desc)]) - @@index([createdAt]) -} -``` - -**Step 3: 保存文件** - -**:wq` (vim) 或 Cmd+S (编辑器) - -**Step 4: 验证语法正确** - -```bash -pnpm prisma validate -``` - -Expected: `The schema is valid` - -**Step 5: 生成并运行迁移** - -```bash -pnpm prisma migrate dev --name add_ai_events_table -``` - -Expected: -``` -The following migration(s) have been created and applied from new schema changes: - -migrations/ - └─ 20250125XXXXXX_add_ai_events_table/ - └─ migration.sql - -Applying migration `20250125XXXXXX_add_ai_events_table` - -The following migration(s) have been created and applied: -... -``` - -**Step 6: 生成 Prisma Client** - -```bash -pnpm prisma generate -``` - -Expected: `Prisma Client generated successfully` - -**Step 7: 提交** - -```bash -git add prisma/schema.prisma prisma/migrations/ -git commit -m "feat: add AIEvent model to database schema" -``` - ---- - -## 阶段 2: 数据验证 - -### Task 2: 创建 Zod 验证 Schema - -**文件:** -- Modify: `src/lib/validations.ts` - -**Step 1: 打开验证文件** - -```bash -vim src/lib/validations.ts -``` - -**Step 2: 找到文件末尾(在 `export { ... }` 之前)** - -使用 `/ProjectQuerySchema` 搜索到相关位置,然后在后面添加。 - -**Step 3: 添加 AIEvent 相关的 Zod Schema** - -在 `ProjectQuerySchema` 定义后添加: - -```typescript -export const AIEventInputSchema = z.object({ - title: z.string().min(1).max(200), - titleEn: z.string().max(200).optional(), - eventDate: z.string().datetime(), - description: z.string().min(10).max(500), - descriptionEn: z.string().max(500).optional(), - imageUrl: z.string().url(), - sourceUrl: z.string().url().optional(), -}); - -export const AIEventQuerySchema = z.object({ - year: z.string().regex(/^\d{4}$/).optional(), - limit: z.string().regex(/^\d+$/).transform(Number).optional(), - offset: z.string().regex(/^\d+$/).transform(Number).optional(), -}); -``` - -**Step 4: 更新 export 语句** - -找到 `export {` 行,添加: - -```typescript -export { - // ... 现有的 exports - AIEventInputSchema, - AIEventQuerySchema, -}; -``` - -**Step 5: 保存文件** - -**:wq` - -**Step 6: 验证 TypeScript 编译通过** - -```bash -pnpm tsc --noEmit -``` - -Expected: 无错误输出 - -**Step 7: 提交** - -```bash -git add src/lib/validations.ts -git commit -m "feat: add AIEvent validation schemas" -``` - ---- - -### Task 3: 编写验证测试 - -**文件:** -- Create: `src/lib/validations.test.ts` - -**Step 1: 创建测试文件** - -```bash -vim src/lib/validations.test.ts -``` - -**Step 2: 编写测试** - -```typescript -import { describe, it, expect } from 'vitest'; -import { AIEventInputSchema } from './validations'; - -describe('AIEventInputSchema', () => { - const validEvent = { - title: 'GPT-4 发布', - eventDate: '2023-03-14T00:00:00Z', - description: 'OpenAI 发布多模态大语言模型', - imageUrl: 'https://example.com/gpt4.jpg', - }; - - it('should validate valid event', () => { - expect(() => AIEventInputSchema.parse(validEvent)).not.toThrow(); - }); - - it('should accept event with optional English fields', () => { - const eventWithEn = { - ...validEvent, - titleEn: 'GPT-4 Release', - descriptionEn: 'OpenAI launches multimodal LLM', - sourceUrl: 'https://openai.com/blog/gpt-4', - }; - expect(() => AIEventInputSchema.parse(eventWithEn)).not.toThrow(); - }); - - it('should reject empty title', () => { - expect(() => AIEventInputSchema.parse({ ...validEvent, title: '' })) - .toThrow(); - }); - - it('should reject title exceeding 200 characters', () => { - const longTitle = 'A'.repeat(201); - expect(() => AIEventInputSchema.parse({ ...validEvent, title: longTitle })) - .toThrow(); - }); - - it('should reject description shorter than 10 characters', () => { - expect(() => AIEventInputSchema.parse({ ...validEvent, description: '太短' })) - .toThrow(); - }); - - it('should reject description exceeding 500 characters', () => { - const longDesc = 'A'.repeat(501); - expect(() => AIEventInputSchema.parse({ ...validEvent, description: longDesc })) - .toThrow(); - }); - - it('should reject invalid eventDate format', () => { - expect(() => AIEventInputSchema.parse({ ...validEvent, eventDate: '2023-03-14' })) - .toThrow(); - }); - - it('should reject invalid imageUrl', () => { - expect(() => AIEventInputSchema.parse({ ...validEvent, imageUrl: 'not-a-url' })) - .toThrow(); - }); - - it('should reject invalid sourceUrl format', () => { - expect(() => AIEventInputSchema.parse({ - ...validEvent, - sourceUrl: 'not-a-url' - })).toThrow(); - }); -}); -``` - -**Step 3: 保存文件** - -**:wq` - -**Step 4: 运行测试验证失败** - -```bash -pnpm test src/lib/validations.test.ts -``` - -Expected: 全部通过 - -**Step 5: 提交** - -```bash -git add src/lib/validations.test.ts -git commit -m "test: add AIEvent validation tests" -``` - ---- - -## 阶段 3: 数据获取层 - -### Task 4: 创建数据获取函数 - -**文件:** -- Create: `src/hooks/useAIEvents.ts` - -**Step 1: 创建文件** - -```bash -vim src/hooks/useAIEvents.ts -``` - -**Step 2: 编写数据获取函数** - -```typescript -import { prisma } from '@/lib/prisma'; - -export async function getAIEvents(options?: { - year?: number; - limit?: number; - offset?: number; -}) { - const where = options?.year - ? { - eventDate: { - gte: new Date(`${options.year}-01-01T00:00:00Z`), - lte: new Date(`${options.year}-12-31T23:59:59Z`), - }, - } - : undefined; - - const events = await prisma.aIEvent.findMany({ - where, - orderBy: { eventDate: 'desc' }, - take: options?.limit || 100, - skip: options?.offset || 0, - }); - - return events; -} - -export async function getAIEventBySlug(slug: string) { - // 暂不实现,后续如需要详细页面时添加 - return null; -} - -export async function getAllAIEventYears() { - const events = await prisma.aIEvent.findMany({ - select: { - eventDate: true, - }, - orderBy: { eventDate: 'desc' }, - }); - - const years = new Set(); - events.forEach(event => { - years.add(new Date(event.eventDate).getFullYear()); - }); - - return Array.from(years).sort((a, b) => b - a); -} -``` - -**Step 3: 保存文件** - -**:wq` - -**Step 4: 验证 TypeScript 类型正确** - -```bash -pnpm tsc --noEmit -``` - -Expected: 无错误 - -**Step 5: 提交** - -```bash -git add src/hooks/useAIEvents.ts -git commit -m "feat: add AI event data fetching functions" -``` - ---- - -## 阶段 4: 后端 API - -### Task 5: 创建 API 路由 - POST endpoint - -**文件:** -- Create: `src/app/api/events/route.ts` - -**Step 1: 创建目录** - -```bash -mkdir -p src/app/api/events -``` - -**Step 2: 创建路由文件** - -```bash -vim src/app/api/events/route.ts -``` - -**Step 3: 实现 POST handler** - -```typescript -import { NextRequest, NextResponse } from 'next/server'; -import { prisma } from '@/lib/prisma'; -import { AIEventInputSchema, AIEventQuerySchema } from '@/lib/validations'; -import crypto from 'crypto'; - -export async function POST(request: NextRequest) { - // 1. API Key 验证 - const apiKey = request.headers.get('X-API-Key'); - const expectedKey = process.env.WEBHOOK_API_KEY; - - if (!apiKey || !expectedKey || !crypto.timingSafeEqual( - Buffer.from(apiKey), - Buffer.from(expectedKey) - )) { - return NextResponse.json( - { error: 'Unauthorized' }, - { status: 401 } - ); - } - - // 2. 解析请求体 - let body: unknown; - try { - body = await request.json(); - } catch (error) { - return NextResponse.json( - { error: 'Invalid JSON' }, - { status: 400 } - ); - } - - // 3. 验证数据 - const validationResult = AIEventInputSchema.array().safeParse(body); - if (!validationResult.success) { - return NextResponse.json( - { - error: 'Validation failed', - details: validationResult.error.errors, - }, - { status: 400 } - ); - } - - // 4. 创建事件 - try { - const result = await prisma.aIEvent.createMany({ - data: validationResult.data, - skipDuplicates: true, - }); - - return NextResponse.json( - { - created: result.count, - total: validationResult.data.length, - }, - { status: 201 } - ); - } catch (error) { - console.error('Failed to create AI events:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); - } -} - -export async function GET(request: NextRequest) { - // 1. 解析查询参数 - const searchParams = request.nextUrl.searchParams; - const queryParams = { - year: searchParams.get('year'), - limit: searchParams.get('limit'), - offset: searchParams.get('offset'), - }; - - // 2. 验证查询参数 - const validationResult = AIEventQuerySchema.safeParse(queryParams); - if (!validationResult.success) { - return NextResponse.json( - { - error: 'Invalid query parameters', - details: validationResult.error.errors, - }, - { status: 400 } - ); - } - - // 3. 获取事件 - try { - const events = await prisma.aIEvent.findMany({ - where: validationResult.data.year - ? { - eventDate: { - gte: new Date(`${validationResult.data.year}-01-01T00:00:00Z`), - lte: new Date(`${validationResult.data.year}-12-31T23:59:59Z`), - }, - } - : undefined, - orderBy: { eventDate: 'desc' }, - take: validationResult.data.limit || 100, - skip: validationResult.data.offset || 0, - }); - - return NextResponse.json({ events }); - } catch (error) { - console.error('Failed to fetch AI events:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); - } -} -``` - -**Step 4: 保存文件** - -**:wq` - -**Step 5: 验证 TypeScript 编译** - -```bash -pnpm tsc --noEmit -``` - -**Step 6: 提交** - -```bash -git add src/app/api/events/ -git commit -m "feat: add AI events API endpoints" -``` - ---- - -### Task 6: 编写 API 测试 - -**文件:** -- Create: `src/app/api/events/route.test.ts` - -**Step 1: 创建测试文件** - -```bash -vim src/app/api/events/route.test.ts -``` - -**Step 2: 编写测试** - -```typescript -import { describe, it, expect, beforeEach } from 'vitest'; -import { POST, GET } from './route'; -import { NextRequest } from 'next/server'; - -describe('POST /api/events', () => { - const validEvent = { - title: 'GPT-4 发布', - eventDate: '2023-03-14T00:00:00Z', - description: 'OpenAI 发布多模态大语言模型', - imageUrl: 'https://example.com/gpt4.jpg', - }; - - it('should reject without API key', async () => { - const request = new NextRequest('http://localhost:3000/api/events', { - method: 'POST', - body: JSON.stringify([validEvent]), - }); - - const response = await POST(request); - expect(response.status).toBe(401); - - const json = await response.json(); - expect(json.error).toBe('Unauthorized'); - }); - - it('should reject with invalid API key', async () => { - const request = new NextRequest('http://localhost:3000/api/events', { - method: 'POST', - headers: { - 'X-API-Key': 'invalid-key', - }, - body: JSON.stringify([validEvent]), - }); - - const response = await POST(request); - expect(response.status).toBe(401); - }); - - // 注意: 以下测试需要设置 WEBHOOK_API_KEY 环境变量 - // 可以通过 vi.stubEnv 来模拟 -}); - -describe('GET /api/events', () => { - it('should return events array', async () => { - const request = new NextRequest('http://localhost:3000/api/events'); - const response = await GET(request); - - expect(response.status).toBe(200); - - const json = await response.json(); - expect(json).toHaveProperty('events'); - expect(Array.isArray(json.events)).toBe(true); - }); - - it('should filter by year', async () => { - const request = new NextRequest( - 'http://localhost:3000/api/events?year=2024' - ); - const response = await GET(request); - - expect(response.status).toBe(200); - - const json = await response.json(); - expect(json).toHaveProperty('events'); - }); - - it('should reject invalid year format', async () => { - const request = new NextRequest( - 'http://localhost:3000/api/events?year=invalid' - ); - const response = await GET(request); - - expect(response.status).toBe(400); - - const json = await response.json(); - expect(json.error).toBe('Invalid query parameters'); - }); -}); -``` - -**Step 3: 保存文件** - -**:wq` - -**Step 4: 运行测试** - -```bash -pnpm test src/app/api/events/route.test.ts -``` - -Expected: 基础测试通过(需要 API Key 的测试会失败) - -**Step 5: 提交** - -```bash -git add src/app/api/events/route.test.ts -git commit -m "test: add API route tests" -``` - ---- - -### Task 7: 手动测试 API - -**文件:** -- N/A (使用 curl) - -**Step 1: 确保 WEBHOOK_API_KEY 已设置** - -```bash -# 检查 .env.local -cat .env.local | grep WEBHOOK_API_KEY -``` - -如果不存在,添加: -```bash -echo "WEBHOOK_API_KEY=test-key-for-development-only" >> .env.local -``` - -**Step 2: 测试 GET endpoint** - -```bash -curl http://localhost:3000/api/events -``` - -Expected: -```json -{ - "events": [] -} -``` - -**Step 3: 测试 POST endpoint(创建单个事件)** - -```bash -curl -X POST http://localhost:3000/api/events \ - -H "Content-Type: application/json" \ - -H "X-API-Key: test-key-for-development-only" \ - -d '{ - "title": "测试事件 - Transformer 论文发表", - "eventDate": "2017-06-12T00:00:00Z", - "description": "Google 团队发表 Attention Is All You Need 论文,提出了 Transformer 架构", - "imageUrl": "https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800" - }' -``` - -Expected: -```json -{ - "created": 1, - "total": 1 -} -``` - -**Step 4: 验证事件已创建** - -```bash -curl http://localhost:3000/api/events -``` - -Expected: 返回刚才创建的事件 - -**Step 5: 测试批量创建** - -```bash -curl -X POST http://localhost:3000/api/events \ - -H "Content-Type: application/json" \ - -H "X-API-Key: test-key-for-development-only" \ - -d '[ - { - "title": "GPT-1 发布", - "eventDate": "2018-06-11T00:00:00Z", - "description": "OpenAI 发布第一代 GPT 模型", - "imageUrl": "https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800" - }, - { - "title": "BERT 发布", - "eventDate": "2018-10-11T00:00:00Z", - "description": "Google 发布 BERT 预训练模型", - "imageUrl": "https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800" - } - ]' -``` - -Expected: -```json -{ - "created": 2, - "total": 2 -} -``` - -**Step 6: 测试年份筛选** - -```bash -curl "http://localhost:3000/api/events?year=2018" -``` - -Expected: 只返回 2018 年的事件 - -**Step 7: 提交 API 测试说明文档** - -```bash -# 创建 API 测试文档 -cat > docs/api-testing-guide.md << 'EOF' -# API 测试指南 - -## GET /api/events - -获取所有事件: - -```bash -curl http://localhost:3000/api/events -``` - -筛选特定年份: - -```bash -curl "http://localhost:3000/api/events?year=2024" -``` - -限制返回数量: - -```bash -curl "http://localhost:3000/api/events?limit=10" -``` - -## POST /api/events - -创建单个事件: - -```bash -curl -X POST http://localhost:3000/api/events \ - -H "Content-Type: application/json" \ - -H "X-API-Key: YOUR_API_KEY" \ - -d '{ - "title": "事件标题", - "eventDate": "2023-03-14T00:00:00Z", - "description": "事件描述(10-500字)", - "imageUrl": "https://example.com/image.jpg" - }' -``` - -批量创建事件: - -```bash -curl -X POST http://localhost:3000/api/events \ - -H "Content-Type: application/json" \ - -H "X-API-Key: YOUR_API_KEY" \ - -d '[ - { "title": "事件1", ... }, - { "title": "事件2", ... } - ]' -``` -EOF - -git add docs/api-testing-guide.md -git commit -m "docs: add API testing guide" -``` - ---- - -## 阶段 5: 前端页面 - -### Task 8: 创建 Timeline 页面 - -**文件:** -- Create: `src/app/[locale]/timeline/page.tsx` - -**Step 1: 创建目录** - -```bash -mkdir -p src/app/\[locale\]/timeline -``` - -**Step 2: 创建页面文件** - -```bash -vim src/app/\[locale\]/timeline/page.tsx -``` - -**Step 3: 实现页面组件** - -```typescript -import { getAIEvents } from '@/hooks/useAIEvents'; -import { Metadata } from 'next'; - -export const revalidate = 3600; // ISR 1小时 - -export async function generateMetadata({ - params, -}: { - params: Promise<{ locale: string }>; -}): Promise { - const { locale } = await params; - - return { - title: locale === 'zh' ? 'AI 发展时间轴' : 'AI Timeline', - description: locale === 'zh' - ? '探索人工智能大语言模型的发展历程,从 2017 年 Transformer 到今天' - : 'Explore the evolution of AI large language models from 2017 Transformer to today', - }; -} - -export default async function TimelinePage() { - const events = await getAIEvents(); - - // 按年份分组 - const eventsByYear = events.reduce((acc, event) => { - const year = new Date(event.eventDate).getFullYear(); - if (!acc[year]) { - acc[year] = []; - } - acc[year].push(event); - return acc; - }, {} as Record); - - // 按年份降序排序 - const sortedYears = Object.keys(eventsByYear) - .map(Number) - .sort((a, b) => b - a); - - if (events.length === 0) { - return ( - - - - 暂无数据 - - - 时间轴数据正在收集中... - - - - ); - } - - return ( - - {/* Header */} - - - - AI 发展时间轴 - - - 从 Transformer 到 AGI: 大语言模型的进化之路 - - - - - {/* Timeline */} - - {sortedYears.map((year, index) => ( - - {/* Year Label */} - - - - {year} - - - - - {/* Events Container */} - - - {eventsByYear[year].map((event, eventIndex) => ( - - {/* Tape decoration */} - - - {/* Image */} - - - - - {/* Content */} - - {event.title} - - - {event.description} - - - {/* Date */} - - {new Date(event.eventDate).toLocaleDateString('zh-CN')} - - - {/* Source Link */} - {event.sourceUrl && ( - - 来源 → - - )} - - ))} - - - - ))} - - - ); -} -``` - -**Step 4: 保存文件** - -**:wq` - -**Step 5: 验证页面可访问** - -访问: http://localhost:3000/timeline - -Expected: 显示时间轴页面,包含之前创建的测试事件 - -**Step 6: 提交** - -```bash -git add src/app/\[locale\]/timeline/ -git commit -m "feat: add timeline page with year-based layout" -``` - ---- - -### Task 9: 创建 EventCard 组件(可选重构) - -**文件:** -- Create: `src/components/timeline/EventCard.tsx` -- Create: `src/components/timeline/TimelineSection.tsx` - -**Step 1: 创建组件目录** - -```bash -mkdir -p src/components/timeline -``` - -**Step 2: 创建 EventCard 组件** - -```bash -vim src/components/timeline/EventCard.tsx -``` - -```typescript -import { AIEvent } from '@prisma/client'; - -interface EventCardProps { - event: AIEvent; - index: number; -} - -export function EventCard({ event, index }: EventCardProps) { - const rotation = (Math.random() - 0.5) * 6; - const zIndex = Math.max(1, 50 - index * 10); - - return ( - - {/* Tape decoration */} - - - {/* Image */} - - - - - {/* Content */} - - {event.title} - - - {event.description} - - - {/* Date */} - - {new Date(event.eventDate).toLocaleDateString('zh-CN')} - - - {/* Source Link */} - {event.sourceUrl && ( - - 来源 → - - )} - - ); -} -``` - -**Step 3: 创建 TimelineSection 组件** - -```bash -vim src/components/timeline/TimelineSection.tsx -``` - -```typescript -import { AIEvent } from '@prisma/client'; -import { EventCard } from './EventCard'; - -interface TimelineSectionProps { - year: number; - events: AIEvent[]; - index: number; -} - -export function TimelineSection({ year, events, index }: TimelineSectionProps) { - const isEven = index % 2 === 0; - - return ( - - {/* Year Label */} - - - - {year} - - - - - {/* Events Container */} - - - {events.map((event, eventIndex) => ( - - ))} - - - - ); -} -``` - -**Step 4: 重构 Timeline Page 使用新组件** - -更新 `src/app/[locale]/timeline/page.tsx`: - -```typescript -import { getAIEvents } from '@/hooks/useAIEvents'; -import { TimelineSection } from '@/components/timeline/TimelineSection'; -import { Metadata } from 'next'; - -export const revalidate = 3600; - -export async function generateMetadata({ - params, -}: { - params: Promise<{ locale: string }>; -}): Promise { - const { locale } = await params; - - return { - title: locale === 'zh' ? 'AI 发展时间轴' : 'AI Timeline', - description: locale === 'zh' - ? '探索人工智能大语言模型的发展历程' - : 'Explore the evolution of AI large language models', - }; -} - -export default async function TimelinePage() { - const events = await getAIEvents(); - - const eventsByYear = events.reduce((acc, event) => { - const year = new Date(event.eventDate).getFullYear(); - if (!acc[year]) acc[year] = []; - acc[year].push(event); - return acc; - }, {} as Record); - - const sortedYears = Object.keys(eventsByYear) - .map(Number) - .sort((a, b) => b - a); - - if (events.length === 0) { - return ( - - - - 暂无数据 - - - 时间轴数据正在收集中... - - - - ); - } - - return ( - - - - - AI 发展时间轴 - - - 从 Transformer 到 AGI: 大语言模型的进化之路 - - - - - - {sortedYears.map((year, index) => ( - - ))} - - - ); -} -``` - -**Step 5: 提交** - -```bash -git add src/components/timeline/ -git commit -m "refactor: extract EventCard and TimelineSection components" -``` - ---- - -### Task 10: 更新导航菜单 - -**文件:** -- Modify: `src/components/layout/Header.tsx` - -**Step 1: 打开 Header 组件** - -```bash -vim src/components/layout/Header.tsx -``` - -**Step 2: 找到导航链接部分** - -搜索 `/projects` 或导航相关的代码。 - -**Step 3: 添加 Timeline 链接** - -在导航菜单中添加: - -```typescript - - Timeline - -``` - -**Step 4: 保存并提交** - -```bash -git add src/components/layout/Header.tsx -git commit -m "feat: add Timeline link to navigation" -``` - ---- - -## 阶段 6: 测试 - -### Task 11: 使用 chrome-devtools-mcp 测试 - -**文件:** -- N/A (手动测试流程) - -**Step 1: 确保开发服务器运行** - -```bash -lsof -ti:3000 | xargs kill -9 2>/dev/null || true -pnpm dev -``` - -**Step 2: 使用 chrome-devtools-mcp 工具测试** - -在另一个对话中执行以下操作(或记录为测试文档): - -```markdown -# Timeline 页面 E2E 测试流程 - -## 1. 页面加载测试 - -使用 `mcp__chrome-devtools__new_page`: -- URL: `http://localhost:3000/timeline` -- 验证: 页面成功加载 - -使用 `mcp__chrome-devtools__take_snapshot`: -- 验证: 页面结构正确,包含 header 和 timeline sections - -## 2. 数据渲染测试 - -使用 `mcp__chrome-devtools__evaluate_script`: -```javascript -() => { - const yearSections = document.querySelectorAll('.year-section'); - return { - yearCount: yearSections.length, - hasEvents: yearSections.length > 0 - }; -} -``` -Expected: `{ yearCount: >0, hasEvents: true }` - -## 3. 控制台错误检查 - -使用 `mcp__chrome-devtools__list_console_messages`: -- Expected: 无错误或警告 - -## 4. 响应式测试 - -使用 `mcp__chrome-devtools__resize_page`: -- 测试尺寸: 375x667 (iPhone SE) -- 测试尺寸: 1920x1080 (桌面) -- 验证: 布局在不同尺寸下正常显示 - -## 5. 视觉回归测试 - -使用 `mcp__chrome-devtools__take_screenshot`: -- 保存截图与设计原型对比 -- 验证: 视觉风格符合 Neo-brutalism 设计 -``` - -**Step 3: 创建测试文档** - -```bash -cat > docs/e2e-testing-guide.md << 'EOF' -# Timeline E2E 测试指南 - -## 使用 chrome-devtools-mcp 测试 - -### 1. 启动测试环境 - -\`\`\`bash -# 确保开发服务器运行 -pnpm dev -\`\`\` - -### 2. 页面加载测试 - -\`\`\`javascript -// new_page -{ - "url": "http://localhost:3000/timeline" -} - -// take_snapshot -// 验证页面结构正确 -\`\`\` - -### 3. 数据验证 - -\`\`\`javascript -// evaluate_script -() => { - const yearSections = document.querySelectorAll('section'); - const eventCards = document.querySelectorAll('.stack-card'); - - return { - yearCount: yearSections.length, - eventCount: eventCards.length, - hasHeader: document.querySelector('h1') !== null - }; -} -\`\`\` - -Expected: -- yearCount > 0 -- eventCount > 0 -- hasHeader: true - -### 4. 无控制台错误 - -\`\`\`javascript -// list_console_messages -{ - "types": ["error", "warn"] -} -\`\`\` - -Expected: 空数组 - -### 5. 截图对比 - -\`\`\`javascript -// take_screenshot -{ - "filePath": "tests/screenshots/timeline-page.png" -} -\`\`\` - -手动对比与 `design/stitch_agent_park_homepage/screen.png` - -### 6. 响应式测试 - -\`\`\`javascript -// resize_page -{ - "width": 375, - "height": 667 -} - -// take_snapshot -// 验证移动端布局 -\`\`\` -EOF - -git add docs/e2e-testing-guide.md -git commit -m "docs: add E2E testing guide with chrome-devtools-mcp" -``` - -**Step 4: 执行测试并记录结果** - -根据测试结果修复发现的问题。 - -**Step 5: 提交测试结果** - -```bash -# 如果有修复 -git add . -git commit -m "fix: address issues found during E2E testing" -``` - ---- - -## 阶段 7: n8n Workflow 配置 - -### Task 12: 设计历史数据初始化 Workflow - -**文件:** -- Create: `docs/n8n/historical-workflow-design.json` - -**Step 1: 创建 n8n workflow 文档目录** - -```bash -mkdir -p docs/n8n -``` - -**Step 2: 编写历史数据初始化 Workflow 设计** - -```bash -cat > docs/n8n/historical-workflow-design.md << 'EOF' -# n8n 历史数据初始化 Workflow - -## 概述 - -此 workflow 用于一次性收集和初始化 2017-2025 年的 AI 重大事件数据。 - -## Workflow 结构 - -### Node 1: Cron 触发器(手动触发) - -- 节点类型: `Manual Trigger` -- 用途: 开发测试时手动运行 - -### Node 2: 设置年份列表 - -- 节点类型: `Code` -- 用途: 定义要处理的年份列表 - -\`\`\`javascript -// 返回年份数组 -return [ - { year: 2017 }, - { year: 2018 }, - { year: 2019 }, - { year: 2020 }, - { year: 2021 }, - { year: 2022 }, - { year: 2023 }, - { year: 2024 }, - { year: 2025 }, -]; -\`\`\` - -### Node 3: 搜索 Agent(循环每年) - -- 节点类型: `Loop Over Items` -- 用途: 遍历每个年份 - -### Node 4: Web Search - Agent 1 - -- 节点类型: `HTTP Request` -- 方法: POST -- URL: `` -- Headers: - \`\`\`json - { - "Content-Type": "application/json" - } - \`\`\` -- Body: - \`\`\`json - { - "search_query": "AI breakthrough {{ $json.year }} LLM release transformer model", - "search_recency_filter": "noLimit", - "content_size": "high" - } - \`\`\` - -### Node 5: 筛选 Agent - Agent 2 - -- 节点类型: `Code` -- 用途: 根据权威来源筛选 - -\`\`\`javascript -const trustedDomains = [ - 'arxiv.org', - 'openai.com', - 'anthropic.com', - 'google.ai', - 'meta.ai', - 'deepmind.com', - 'research.google', -]; - -const items = $input.all(); - -const filtered = items.filter(item => { - const url = item.json.url || ''; - return trustedDomains.some(domain => url.includes(domain)); -}); - -return filtered; -\`\`\` - -### Node 6: 格式化 Agent - Agent 3 - -- 节点类型: `Code` -- 用途: 转换为 API 格式 - -\`\`\`javascript -const items = $input.all(); - -const formatted = items.map(item => { - const publishedDate = item.json.published_date || new Date().toISOString(); - - return { - json: { - title: item.json.title || 'Untitled', - eventDate: new Date(publishedDate).toISOString(), - description: (item.json.description || item.json.snippet || '').substring(0, 500), - imageUrl: item.json.image_url || 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800', - sourceUrl: item.json.url, - }, - }; -}); - -return formatted; -\`\`\` - -### Node 7: 提交到 API - -- 节点类型: `HTTP Request` -- 方法: POST -- URL: `http://localhost:3000/api/events` -- Headers: - \`\`\`json - { - "Content-Type": "application/json", - "X-API-Key": "={{ $env.WEBHOOK_API_KEY }}" - } - \`\`\` -- Body: `={{ $json }}` (发送整个数组) - -### Node 8: 错误处理 - -- 节点类型: `IF` -- 条件: 检查上一个节点的 status code -- On True: 记录成功 -- On False: 发送错误邮件 - -## 环境变量 - -在 n8n 中设置: -- `WEBHOOK_API_KEY`: 你的 API 密钥(从 .env.local 获取) -- `API_ENDPOINT`: `http://localhost:3000/api/events` (开发) 或生产 URL - -## 测试步骤 - -1. 在 n8n UI 中创建此 workflow -2. 手动触发运行 -3. 检查数据库: `pnpm prisma studio` -4. 验证事件已正确创建 -EOF -``` - -**Step 3: 创建增量更新 Workflow 设计** - -```bash -cat > docs/n8n/incremental-workflow-design.md << 'EOF' -# n8n 增量更新 Workflow - -## 概述 - -此 workflow 每周一自动运行,收集最近 7 天的新 AI 事件。 - -## Workflow 结构 - -### Node 1: Cron 触发器 - -- 节点类型: `Cron` -- 表达式: `0 9 * * 1` (每周一早上 9:00) -- 时区: Asia/Shanghai - -### Node 2: Web Search - Agent 1 - -- 节点类型: `HTTP Request` -- URL: `` -- Body: - \`\`\`json - { - "search_query": "AI news LLM release model launch this week", - "search_recency_filter": "oneWeek" - } - \`\`\` - -### Node 3: 筛选 Agent - Agent 2 - -- 节点类型: `Code` -- 用途: 筛选 + 去重(查询数据库避免重复) - -\`\`\`javascript -const trustedDomains = [ - 'arxiv.org', - 'openai.com', - 'anthropic.com', -]; - -// 过滤权威来源 -const items = $input.all(); -const filtered = items.filter(item => { - const url = item.json.url || ''; - return trustedDomains.some(domain => url.includes(domain)); -}); - -// TODO: 添加数据库查询去重 -// 这里可以调用 GET /api/events 检查 sourceUrl 是否已存在 - -return filtered; -\`\`\` - -### Node 4: 格式化 Agent - Agent 3 - -- 节点类型: `Code` -- 代码: 同历史 workflow - -### Node 5: 提交到 API - -- 节点类型: `HTTP Request` -- 配置: 同历史 workflow - -### Node 6: 发送通知邮件 - -- 节点类型: `Send Email` -- 条件: 仅在创建新事件时发送 -- 内容: - \`\`\` - 主题: AI Timeline - 新事件已添加 - - 本次更新添加了 {{ $json.created }} 个新事件。 - - 查看: https://your-domain.com/timeline - \`\`\` - -### Node 7: 错误处理 - -- 节点类型: `Error Trigger` -- 动作: 发送错误邮件到管理员 - -## 测试 - -1. 修改 Cron 为手动触发进行测试 -2. 验证只有新事件被添加 -3. 检查邮件通知是否正常发送 -4. 确认错误处理工作正常 -EOF -``` - -**Step 4: 提交 n8n workflow 设计文档** - -```bash -git add docs/n8n/ -git commit -m "docs: add n8n workflow designs for AI timeline" -``` - ---- - -### Task 13: 实现历史数据初始化(手动执行) - -**文件:** -- N/A (手动操作 + 脚本) - -**Step 1: 准备历史事件数据** - -创建 `scripts/seed-historical-events.ts`: - -```bash -mkdir -p scripts -vim scripts/seed-historical-events.ts -``` - -**Step 2: 编写种子数据脚本** - -```typescript -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -const historicalEvents = [ - { - title: 'Attention Is All You Need', - eventDate: new Date('2017-06-12T00:00:00Z'), - description: 'Google 团队发表 Transformer 论文,提出自注意力机制,彻底改变 NLP 领域', - imageUrl: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800', - sourceUrl: 'https://arxiv.org/abs/1706.03762', - }, - { - title: 'GPT-1 发布', - eventDate: new Date('2018-06-11T00:00:00Z'), - description: 'OpenAI 发布第一代生成式预训练 Transformer 模型,展示无监督学习的潜力', - imageUrl: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800', - sourceUrl: 'https://s3-us-west-2.amazonaws.com/openai-assets/research-covers/language-unsupervised/language-understanding-paper.pdf', - }, - { - title: 'BERT 发布', - eventDate: new Date('2018-10-11T00:00:00Z'), - description: 'Google 发布双向编码器表示 Transformer,在 11 项 NLP 任务中创 SOTA', - imageUrl: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800', - sourceUrl: 'https://arxiv.org/abs/1810.04805', - }, - { - title: 'GPT-2 发布', - eventDate: new Date('2019-02-14T00:00:00Z'), - description: 'OpenAI 发布 15 亿参数的 GPT-2,因"太危险"而不敢全部发布', - imageUrl: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800', - sourceUrl: 'https://openai.com/research/better-language-models', - }, - { - title: 'GPT-3 发布', - eventDate: new Date('2020-05-28T00:00:00Z'), - description: 'OpenAI 发布 1750 亿参数的 GPT-3,展示 few-shot 学习的强大能力', - imageUrl: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800', - sourceUrl: 'https://arxiv.org/abs/2005.14165', - }, - { - title: 'GitHub Copilot 发布', - eventDate: new Date('2021-06-29T00:00:00Z'), - description: 'GitHub 和 OpenAI 发布 AI 编程助手,基于 Codex 模型', - imageUrl: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800', - sourceUrl: 'https://github.blog/news-insights/company-news/github-copilot/', - }, - { - title: 'ChatGPT 发布', - eventDate: new Date('2022-11-30T00:00:00Z'), - description: 'OpenAI 发布对话式 AI 助手 ChatGPT,5 天用户突破 100 万', - imageUrl: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800', - sourceUrl: 'https://openai.com/blog/chatgpt', - }, - { - title: 'GPT-4 发布', - eventDate: new Date('2023-03-14T00:00:00Z'), - description: 'OpenAI 发布多模态大语言模型 GPT-4,在各项基准测试中接近人类水平', - imageUrl: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800', - sourceUrl: 'https://openai.com/research/gpt-4', - }, - { - title: 'Claude 发布', - eventDate: new Date('2023-03-16T00:00:00Z'), - description: 'Anthropic 发布 AI 助手 Claude,强调安全性和有用性', - imageUrl: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800', - sourceUrl: 'https://www.anthropic.com/index/claude-now-open', - }, -]; - -async function main() { - console.log('开始插入历史事件...'); - - for (const event of historicalEvents) { - try { - await prisma.aIEvent.create({ - data: event, - }); - console.log(`✓ ${event.title} (${event.eventDate.getFullYear()})`); - } catch (error) { - console.log(`✗ ${event.title} 已存在或插入失败`); - } - } - - console.log('\n历史事件插入完成!'); -} - -main() - .catch(console.error) - .finally(() => prisma.$disconnect()); -``` - -**Step 3: 运行种子脚本** - -```bash -npx tsx scripts/seed-historical-events.ts -``` - -Expected: -``` -开始插入历史事件... -✓ Attention Is All You Need (2017) -✓ GPT-1 发布 (2018) -... -历史事件插入完成! -``` - -**Step 4: 验证数据** - -访问: http://localhost:3000/timeline - -Expected: 显示历史事件,按年份分组 - -**Step 5: 提交种子脚本** - -```bash -git add scripts/seed-historical-events.ts -git commit -m "feat: add historical events seed script" -``` - ---- - -## 阶段 8: 部署准备 - -### Task 14: 准备生产环境 - -**文件:** -- Modify: `.env.local` (仅本地) -- Update Vercel 环境变量 - -**Step 1: 验证所有环境变量** - -```bash -# 本地开发 -cat .env.local | grep -E "(DATABASE_URL|WEBHOOK_API_KEY)" - -# 应该看到: -# DATABASE_URL=postgres://... -# WEBHOOK_API_KEY=your-test-key -``` - -**Step 2: 在 Vercel 设置环境变量** - -访问: https://vercel.com/your-project/settings/environment-variables - -确保已设置: -- `DATABASE_URL`: 生产数据库 URL (Neon) -- `WEBHOOK_API_KEY`: 生产 API 密钥(强密码,至少 32 字符) - -**Step 3: 准备生产数据库迁移** - -```bash -# 生成迁移 SQL -pnpm prisma migrate diff \ - --from-empty \ - --to-schema-datamodel prisma/schema.prisma \ - --script > migration.sql -``` - -**Step 4: 提交部署准备文档** - -```bash -cat > docs/deployment-guide.md << 'EOF' -# 生产环境部署指南 - -## 前置条件 - -- [x] Vercel 项目已配置 -- [x] Neon 数据库已连接 -- [x] WEBHOOK_API_KEY 环境变量已设置 - -## 部署步骤 - -### 1. 运行数据库迁移 - -\`\`\`bash -# 方式 1: 使用 Prisma push -pnpm prisma db push --preview-feature - -# 方式 2: 在 Neon SQL Editor 执行 migration.sql -\`\`\` - -### 2. 部署到 Vercel - -\`\`\`bash -git push origin feature/ai-timeline -# 或通过 PR 合并到 main -\`\`\` - -### 3. 验证部署 - -- 访问生产 URL: `https://your-domain.com/timeline` -- 测试 API: `curl https://your-domain.com/api/events` -- 检查 Vercel 日志确认无错误 - -### 4. 配置 n8n Workflow - -更新 n8n 中的环境变量: -- `WEBHOOK_API_KEY`: 生产密钥 -- `API_ENDPOINT`: `https://your-domain.com/api/events` - -### 5. 运行历史数据初始化 - -手动触发 n8n 历史初始化 workflow - -### 6. 验证增量更新 - -- 修改 Cron 为手动触发测试增量 workflow -- 确认新事件正确添加 -- 恢复 Cron 为每周一自动运行 - -## 回滚计划 - -如有问题: -1. 在 Vercel 回滚到上一个部署 -2. 数据库更改使用 Prisma migrate rollback -EOF - -git add docs/deployment-guide.md -git commit -m "docs: add production deployment guide" -``` - ---- - -## 阶段 9: 最终测试和清理 - -### Task 15: 完整功能测试 - -**文件:** -- Create: `docs/testing-checklist.md` - -**Step 1: 创建测试清单** - -```bash -cat > docs/testing-checklist.md << 'EOF' -# AI Timeline 功能测试清单 - -## 数据库测试 - -- [x] AIEvent 表创建成功 -- [x] 索引正确配置(eventDate 降序) -- [x] Prisma Client 生成无错误 - -## API 测试 - -- [x] POST /api/events - 单个事件创建 -- [x] POST /api/events - 批量事件创建 -- [x] POST /api/events - API Key 认证正常 -- [x] POST /api/events - 数据验证工作正常 -- [x] GET /api/events - 返回所有事件 -- [x] GET /api/events?year=2023 - 年份筛选正常 -- [x] GET /api/events?limit=10 - 分页正常 - -## 前端测试 - -- [x] /timeline 页面可访问 -- [x] 事件按年份正确分组 -- [x] 年份按降序显示 -- [x] 卡片堆叠样式正确 -- [x] Hover 动画正常 -- [x] 深色模式支持 -- [x] 移动端响应式布局 -- [x] 导航菜单 Timeline 链接可点击 - -## 数据测试 - -- [x] 历史事件种子数据插入成功 -- [x] 2017-2025 每年都有事件 -- [x] 事件数据完整性(标题、日期、描述、图片) -- [x] ISR 缓存正常工作 - -## n8n Workflow 测试 - -- [ ] 历史初始化 workflow 测试通过 -- [ ] 增量更新 workflow 测试通过 -- [ ] API 调用成功 -- [ ] 错误处理正常工作 -- [ ] 邮件通知配置完成 - -## E2E 测试(chrome-devtools-mcp) - -- [ ] 页面加载无控制台错误 -- [ ] 数据正确渲染 -- [ ] 视觉对比原型设计 -- [ ] 响应式测试通过 - -## 性能测试 - -- [ ] ISR 缓存生效(1小时) -- [ ] 页面加载速度 < 2s -- [ ] 图片加载优化 -- [ ] 数据库查询性能 - -## 安全测试 - -- [ ] API Key 认证有效 -- [ ] 无 SQL 注入风险 -- [ ] XSS 防护 -- [ ] CORS 配置正确 -EOF -``` - -**Step 2: 执行完整测试** - -逐项检查并完成测试清单。 - -**Step 3: 修复发现的问题** - -根据测试结果进行必要的修复。 - -**Step 4: 提交最终代码** - -```bash -cd /Users/caihaohan/Code/agent_park-timeline -git add . -git commit -m "feat: complete AI timeline feature implementation" -``` - ---- - -## 阶段 10: 合并到主分支 - -### Task 16: 合并 Worktree - -**文件:** -- N/A (git 操作) - -**Step 1: 切换回主仓库** - -```bash -cd /Users/caihaohan/Code/agent_park -``` - -**Step 2: 拉取最新代码** - -```bash -git fetch origin -git checkout main -git pull origin main -``` - -**Step 3: 合并 feature 分支** - -```bash -# 方式 1: 使用 worktree -cd /Users/caihaohan/Code/agent_park-timeline -git push origin feature/ai-timeline - -# 然后在 GitHub 创建 PR 或: -cd /Users/caihaohan/Code/agent_park -git merge feature/ai-timeline -``` - -**Step 4: 删除 worktree** - -```bash -git worktree remove ../agent_park-timeline -git branch -D feature/ai-timeline -``` - -**Step 5: 最终提交** - -```bash -git commit -m "merge: feature/ai-timeline - AI timeline implementation" -``` - ---- - -## 总结 - -完成以上 16 个任务后,你将拥有: - -✅ 完整的 AI 时间轴数据库 Schema -✅ RESTful API (创建、查询、筛选) -✅ 时间轴前端页面(按年份分组展示) -✅ 历史数据初始化脚本 -✅ n8n workflow 设计文档 -✅ 完整的测试覆盖 -✅ 部署准备文档 - -**后续优化方向:** -- 添加搜索和筛选功能 -- 支持用户互动(点赞、评论) -- 导出时间轴为 PDF/Markdown -- 添加更多历史事件 -- 多语言支持完善 - -**关键文件清单:** - -数据库: -- `prisma/schema.prisma` - AIEvent 模型 - -后端: -- `src/lib/validations.ts` - Zod 验证 -- `src/hooks/useAIEvents.ts` - 数据获取 -- `src/app/api/events/route.ts` - API 端点 - -前端: -- `src/app/[locale]/timeline/page.tsx` - 时间轴页面 -- `src/components/timeline/EventCard.tsx` - 事件卡片组件 -- `src/components/timeline/TimelineSection.tsx` - 年份区域组件 - -脚本: -- `scripts/seed-historical-events.ts` - 历史数据初始化 - -文档: -- `docs/n8n/historical-workflow-design.md` -- `docs/n8n/incremental-workflow-design.md` -- `docs/deployment-guide.md` -- `docs/testing-checklist.md` -- `docs/api-testing-guide.md` -- `docs/e2e-testing-guide.md` diff --git a/.omc/plans/postponed-features/timeline/code.html b/.omc/plans/postponed-features/timeline/code.html deleted file mode 100644 index acc1fec..0000000 --- a/.omc/plans/postponed-features/timeline/code.html +++ /dev/null @@ -1,284 +0,0 @@ - - - - -Agent Park - AI History Timeline Pinboard - - - - - - - - - - - - - -auto_awesome - Welcome to Agent Park: The Evolution of Intelligence - arrow_forward - - - - - -smart_toy - -Agent Park - - -Timeline -Agents -About - - -light_mode - - - - - - - - - - - - - - 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 - - - - - - -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 - - - - - - - - - -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. - - - - - SUBSCRIBE - - - - -I agree to be cool. - - - - - - -arrow_upward - - - - - \ No newline at end of file diff --git a/.omc/plans/postponed-features/timeline/screen.png b/.omc/plans/postponed-features/timeline/screen.png deleted file mode 100644 index 1309dc4..0000000 Binary files a/.omc/plans/postponed-features/timeline/screen.png and /dev/null differ diff --git a/.omc/project-memory.json b/.omc/project-memory.json deleted file mode 100644 index 92bd975..0000000 --- a/.omc/project-memory.json +++ /dev/null @@ -1,320 +0,0 @@ -{ - "version": "1.0.0", - "lastScanned": 1771588891228, - "projectRoot": "/Users/caihaohan/Code/agent_park", - "techStack": { - "languages": [ - { - "name": "JavaScript/TypeScript", - "version": null, - "confidence": "high", - "markers": [ - "package.json" - ] - }, - { - "name": "TypeScript", - "version": null, - "confidence": "high", - "markers": [ - "tsconfig.json" - ] - } - ], - "frameworks": [ - { - "name": "next", - "version": "15.1.11", - "category": "fullstack" - }, - { - "name": "react", - "version": "19.0.0", - "category": "frontend" - }, - { - "name": "react-dom", - "version": "19.0.0", - "category": "frontend" - }, - { - "name": "@playwright/test", - "version": "1.49.1", - "category": "testing" - }, - { - "name": "vitest", - "version": "2.1.8", - "category": "testing" - } - ], - "packageManager": "pnpm", - "runtime": null - }, - "build": { - "buildCommand": "pnpm build", - "testCommand": "pnpm test", - "lintCommand": "pnpm lint", - "devCommand": "pnpm dev", - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint", - "test": "vitest", - "test:e2e": "playwright test" - } - }, - "conventions": { - "namingStyle": null, - "importStyle": null, - "testPattern": null, - "fileOrganization": "type-based" - }, - "structure": { - "isMonorepo": false, - "workspaces": [], - "mainDirectories": [ - "docs", - "scripts", - "src", - "tests" - ], - "gitBranches": { - "defaultBranch": "main", - "branchingStrategy": null - } - }, - "customNotes": [], - "directoryMap": { - "design": { - "path": "design", - "purpose": null, - "fileCount": 3, - "lastAccessed": 1771588891214, - "keyFiles": [ - "detail.html", - "homepage.html", - "projectlist.html" - ] - }, - "docs": { - "path": "docs", - "purpose": "Documentation", - "fileCount": 7, - "lastAccessed": 1771588891218, - "keyFiles": [ - "analyst.md", - "api-reference.md", - "api-testing-guide.md", - "discovery-workflow.md", - "e2e-testing-guide.md" - ] - }, - "n8n-workflows": { - "path": "n8n-workflows", - "purpose": null, - "fileCount": 2, - "lastAccessed": 1771588891218, - "keyFiles": [ - "README.md", - "keyword-cloud-workflow.json" - ] - }, - "prisma": { - "path": "prisma", - "purpose": null, - "fileCount": 2, - "lastAccessed": 1771588891219, - "keyFiles": [ - "schema.prisma", - "seed.ts" - ] - }, - "scripts": { - "path": "scripts", - "purpose": "Build/utility scripts", - "fileCount": 4, - "lastAccessed": 1771588891219, - "keyFiles": [ - "list-tags.js", - "seed-historical-events.ts", - "seed-keyword-cloud.ts", - "test-keyword-api.ts" - ] - }, - "src": { - "path": "src", - "purpose": "Source code", - "fileCount": 1, - "lastAccessed": 1771588891219, - "keyFiles": [ - "middleware.ts" - ] - }, - "tests": { - "path": "tests", - "purpose": "Test files", - "fileCount": 0, - "lastAccessed": 1771588891219, - "keyFiles": [] - }, - "prisma/migrations": { - "path": "prisma/migrations", - "purpose": "Database migrations", - "fileCount": 1, - "lastAccessed": 1771588891220, - "keyFiles": [ - "migration_lock.toml" - ] - }, - "src/app": { - "path": "src/app", - "purpose": "Application code", - "fileCount": 3, - "lastAccessed": 1771588891220, - "keyFiles": [ - "AGENTS.md", - "globals.css", - "layout.tsx" - ] - }, - "src/components": { - "path": "src/components", - "purpose": "UI components", - "fileCount": 1, - "lastAccessed": 1771588891220, - "keyFiles": [ - "AGENTS.md" - ] - }, - "src/lib": { - "path": "src/lib", - "purpose": "Library code", - "fileCount": 6, - "lastAccessed": 1771588891220, - "keyFiles": [ - "AGENTS.md", - "prisma.ts", - "slug.ts" - ] - } - }, - "hotPaths": [ - { - "path": "AGENTS.md", - "accessCount": 6, - "lastAccessed": 1771593272139, - "type": "file" - }, - { - "path": ".opencode/mcp.json", - "accessCount": 4, - "lastAccessed": 1771592799554, - "type": "file" - }, - { - "path": "src/app/AGENTS.md", - "accessCount": 3, - "lastAccessed": 1771593311370, - "type": "file" - }, - { - "path": "src/hooks/AGENTS.md", - "accessCount": 3, - "lastAccessed": 1771593529726, - "type": "file" - }, - { - "path": "src/lib/AGENTS.md", - "accessCount": 3, - "lastAccessed": 1771593529730, - "type": "file" - }, - { - "path": "src/components/AGENTS.md", - "accessCount": 2, - "lastAccessed": 1771593512972, - "type": "file" - }, - { - "path": "prisma/schema.prisma", - "accessCount": 2, - "lastAccessed": 1771594876904, - "type": "file" - }, - { - "path": ".env", - "accessCount": 1, - "lastAccessed": 1771589498743, - "type": "file" - }, - { - "path": "package.json", - "accessCount": 1, - "lastAccessed": 1771593111981, - "type": "file" - }, - { - "path": "CLAUDE.md", - "accessCount": 1, - "lastAccessed": 1771593111989, - "type": "file" - }, - { - "path": "prisma/seed.ts", - "accessCount": 1, - "lastAccessed": 1771593360708, - "type": "file" - }, - { - "path": "prisma/AGENTS.md", - "accessCount": 1, - "lastAccessed": 1771593361809, - "type": "file" - }, - { - "path": "docs/analyst.md", - "accessCount": 1, - "lastAccessed": 1771594824291, - "type": "file" - }, - { - "path": "docs/plans/2025-01-25-ai-timeline-feature-design.md", - "accessCount": 1, - "lastAccessed": 1771594834076, - "type": "file" - }, - { - "path": "docs/plans/2026-01-25-keyword-cloud-system-design.md", - "accessCount": 1, - "lastAccessed": 1771594834081, - "type": "file" - }, - { - "path": "docs/plans/2025-01-25-ai-timeline-implementation.md", - "accessCount": 1, - "lastAccessed": 1771594834472, - "type": "file" - }, - { - "path": "docs/plans/2026-01-25-keyword-cloud-implementation.md", - "accessCount": 1, - "lastAccessed": 1771594834666, - "type": "file" - }, - { - "path": "src/app/[locale]/timeline/page.tsx", - "accessCount": 1, - "lastAccessed": 1771594876522, - "type": "file" - }, - { - "path": "src/app/[locale]/keyword-cloud/components/KeywordCloud.tsx", - "accessCount": 1, - "lastAccessed": 1771594892847, - "type": "file" - } - ], - "userDirectives": [] -} \ No newline at end of file diff --git a/.omc/state/agent-replay-06edd4b9-cb01-44f4-a4af-c05ffc72c09e.jsonl b/.omc/state/agent-replay-06edd4b9-cb01-44f4-a4af-c05ffc72c09e.jsonl deleted file mode 100644 index 7f7a0f3..0000000 --- a/.omc/state/agent-replay-06edd4b9-cb01-44f4-a4af-c05ffc72c09e.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"t":0,"agent":"a37a7c5","agent_type":"unknown","event":"agent_stop","success":true} diff --git a/.omc/state/agent-replay-142c3208-f900-46e0-bf24-573ef3f800d4.jsonl b/.omc/state/agent-replay-142c3208-f900-46e0-bf24-573ef3f800d4.jsonl deleted file mode 100644 index 3a3e2e1..0000000 --- a/.omc/state/agent-replay-142c3208-f900-46e0-bf24-573ef3f800d4.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"t":0,"agent":"a8591e0","agent_type":"unknown","event":"agent_stop","success":true} diff --git a/.omc/state/agent-replay-572cfe5e-cb62-41f5-94c6-37d47939ae85.jsonl b/.omc/state/agent-replay-572cfe5e-cb62-41f5-94c6-37d47939ae85.jsonl deleted file mode 100644 index 40540f4..0000000 --- a/.omc/state/agent-replay-572cfe5e-cb62-41f5-94c6-37d47939ae85.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"t":0,"agent":"a567d02","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a6f63da","agent_type":"unknown","event":"agent_stop","success":true} diff --git a/.omc/state/agent-replay-6febdf1e-2a69-4d80-9414-3d6b712b25bb.jsonl b/.omc/state/agent-replay-6febdf1e-2a69-4d80-9414-3d6b712b25bb.jsonl deleted file mode 100644 index 84ead65..0000000 --- a/.omc/state/agent-replay-6febdf1e-2a69-4d80-9414-3d6b712b25bb.jsonl +++ /dev/null @@ -1,7 +0,0 @@ -{"t":0,"agent":"aa3c21d","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a13b2eb","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a791a1b","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a70151c","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a488f88","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a588376","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a0e7cd4","agent_type":"unknown","event":"agent_stop","success":true} diff --git a/.omc/state/agent-replay-75102ef7-4cd7-4718-9d06-7626a28bbcb8.jsonl b/.omc/state/agent-replay-75102ef7-4cd7-4718-9d06-7626a28bbcb8.jsonl deleted file mode 100644 index 3c8f5b2..0000000 --- a/.omc/state/agent-replay-75102ef7-4cd7-4718-9d06-7626a28bbcb8.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"t":0,"agent":"a04e80e","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a9b9f24","agent_type":"explore","event":"agent_start","parent_mode":"none"} -{"t":0,"agent":"a9b9f24","agent_type":"explore","event":"agent_stop","success":true,"duration_ms":60625} -{"t":0,"agent":"a895674","agent_type":"unknown","event":"agent_stop","success":true} diff --git a/.omc/state/agent-replay-992dcc72-e1e5-437a-b3b6-a3f8c693f1ec.jsonl b/.omc/state/agent-replay-992dcc72-e1e5-437a-b3b6-a3f8c693f1ec.jsonl deleted file mode 100644 index 0902c45..0000000 --- a/.omc/state/agent-replay-992dcc72-e1e5-437a-b3b6-a3f8c693f1ec.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"t":0,"agent":"af530c0","agent_type":"unknown","event":"agent_stop","success":true} diff --git a/.omc/state/agent-replay-bcdd4855-a518-4baf-9564-127b3a79d3f7.jsonl b/.omc/state/agent-replay-bcdd4855-a518-4baf-9564-127b3a79d3f7.jsonl deleted file mode 100644 index 48c8616..0000000 --- a/.omc/state/agent-replay-bcdd4855-a518-4baf-9564-127b3a79d3f7.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"t":0,"agent":"aaa41bb","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"system","event":"skill_invoked","skill_name":"find-skills"} -{"t":0,"agent":"aa7d07a","agent_type":"unknown","event":"agent_stop","success":true} -{"t":0,"agent":"a604ba3","agent_type":"unknown","event":"agent_stop","success":true} diff --git a/.omc/state/agent-replay-cb8f78c9-4c40-4e6a-bd70-ba499b555d40.jsonl b/.omc/state/agent-replay-cb8f78c9-4c40-4e6a-bd70-ba499b555d40.jsonl deleted file mode 100644 index 4b2fd73..0000000 --- a/.omc/state/agent-replay-cb8f78c9-4c40-4e6a-bd70-ba499b555d40.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"t":0,"agent":"system","event":"skill_invoked","skill_name":"oh-my-claudecode:hud"} -{"t":0,"agent":"afab084","agent_type":"unknown","event":"agent_stop","success":true} diff --git a/.omc/state/hud-state.json b/.omc/state/hud-state.json deleted file mode 100644 index e1d9b90..0000000 --- a/.omc/state/hud-state.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "timestamp": "2026-02-20T13:40:21.901Z", - "backgroundTasks": [], - "sessionStartTimestamp": "2026-02-20T12:55:14.824Z", - "sessionId": "f1da58ef16330f2a" -} \ No newline at end of file diff --git a/.omc/state/subagent-tracking.json b/.omc/state/subagent-tracking.json deleted file mode 100644 index cc8c980..0000000 --- a/.omc/state/subagent-tracking.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "agents": [ - { - "agent_id": "a9b9f24", - "agent_type": "oh-my-claudecode:explore", - "started_at": "2026-02-20T13:10:01.740Z", - "parent_mode": "none", - "status": "completed", - "completed_at": "2026-02-20T13:11:02.365Z", - "duration_ms": 60625 - } - ], - "total_spawned": 1, - "total_completed": 1, - "total_failed": 0, - "last_updated": "2026-02-20T13:47:26.026Z" -} \ No newline at end of file diff --git a/.sisyphus/plans/tag-merge-n8n.md b/.sisyphus/plans/tag-merge-n8n.md deleted file mode 100644 index 3aa41bd..0000000 --- a/.sisyphus/plans/tag-merge-n8n.md +++ /dev/null @@ -1,281 +0,0 @@ -# Tag Janitor: n8n 每日 AI 标签合并 - -## TL;DR - -目标:解决 AI 生成导致的“标签爆炸”,不改现有入库逻辑(允许自由生成 tags),通过 **n8n 每日任务**调用站点 API 自动做 **标签语义合并 + nameEn 补全**,并保证合并后项目仍正确绑定到新标签。 - -交付物: - -- 新增 tags 维护 API(list + maintenance/merge) -- n8n workflow(每日运行):拉取 tags → AI 产出合并计划/英文补全 → 调用 API 执行 → 输出日报 -- Vitest 自动化测试(route + 核心 merge 逻辑) - ---- - -## Context - -### 现状(已验证) - -- Tag 数据模型:`prisma/schema.prisma`(`Tag` + `ProjectTag`) - - `Tag.name` 全局唯一,`Tag.slug` 全局唯一,`Tag.nameEn` 可空,`Tag.createdAt` 存在 - - `ProjectTag` 复合主键 `@@id([projectId, tagId])`(同一项目不能重复绑定同一 tag) -- Tag 入库: - - Webhook:`src/app/api/webhook/projects/route.ts`(批量 upsert tags;使用 `generateSlug()`;存在 slug 冲突兜底) - - Discovery:`src/app/api/discovery/lib/discovery-service.ts`(`upsertTags()` 与 webhook 类似) -- UI 过滤: - - 项目列表按 `tag slug` 过滤:`src/hooks/useProjects.ts`(`where.tags.some.tag.slug = tag`) - - TagCloud 搜索大小写不敏感,但展示/过滤依赖 slug:`src/components/project/TagCloud.tsx` -- Slug 生成:`src/lib/slug.ts`(优先 nameEn,否则 name;lowercase;保留中文;空格→-;最长 100) - -### 约束与偏好(用户确认) - -- 不做复杂治理:无审核 UI、无回滚系统、无 alias/redirect 层、无 merge memory。 -- 合并后旧标签直接删除。 -- 新项目入库仍可自由生成 tags(不做入库侧规范化/映射),一切靠每日合并流程兜底。 -- 标签双语:需要 `name` + `nameEn`(可相同,例如 Python)。 -- AI 判定逻辑放在 n8n;站点 API 只负责“安全执行合并”。 -- 需要自动化测试(Vitest)。 - ---- - -## 关键设计(最小可用) - -### 1) API 只做“合并执行器” - -站点侧不做语义判断、不接 embedding、不做候选生成;只接收 n8n 给出的“合并计划”,并以 Prisma 事务保证一致性。 - -必须满足: - -- 合并后 `ProjectTag` 绑定迁移完成(不会丢失项目-标签关系) -- 避免因 `@@id([projectId, tagId])` 造成重复冲突(迁移时要去重) -- 执行完删除旧 tags(source tags) -- 合并完触发缓存更新(ISR)以尽快反映 tag counts - -### 2) n8n 负责 AI:语义聚类 + 合并计划 + nameEn 补全 - -最简单闭环: - -- 每日拉取全量 tags(含 projectCount) -- 让 LLM 输出: - - merges:[{ target: {name, nameEn}, sourceTagIds: [...] }] - - updates:[{ tagId, nameEn }] -- 调用 API 执行 merges/updates -- 输出日报(合并数量、删除 tags 数、nameEn 补全数、失败明细) - ---- - -## 默认策略(无须再问用户) - -- 旧 slug 不做重定向:合并后如果用户用旧 `?tag=oldSlug` 访问,页面会显示 0 结果(符合“不要复杂化”的约束)。 -- Merge 过程中不强制改动 `slug`: - - 对“新建 canonical tag”由 API 按 `generateSlug(name, nameEn)` 生成 slug - - 对“复用已有 tagId 作为 target”不强制改 slug(避免破坏既有链接);只更新 `name/nameEn`(如果 n8n 传了) -- API 鉴权复用 `WEBHOOK_API_KEY`(与 `src/app/api/webhook/projects/route.ts` 同一密钥),用 `crypto.timingSafeEqual()`。 - ---- - -## Execution Strategy (Waves) - -Wave 1 (Design + Audit) - -- Task 1: 明确 API contract(list + maintenance)+ 错误码 -- Task 2: 设计 n8n workflow(节点、prompt、chunking、重试策略) - -Wave 2 (Backend Implementation) - -- Task 3: 新增 tags list API(给 n8n 拉取 tags + counts) -- Task 4: 新增 tags maintenance API(merges + updates;事务;去重;删除旧 tag) -- Task 5: 触发 ISR revalidate(项目列表页) - -Wave 3 (Automation + Tests) - -- Task 6: 写 Vitest 测试(route + merge 逻辑) -- Task 7: 搭建 n8n workflow 并做一次 dry run + 一次真实合并 - -Critical Path: Task 1 → Task 4 → Task 6 → Task 7 - ---- - -## TODOs - -### 1) 定义 API Contract(list + maintenance) - -**What to do**: - -- 设计 2 个 endpoint: - - `GET /api/tags`:返回 tags 列表(id/name/nameEn/slug/createdAt/\_count.projects) - - `POST /api/tags/maintenance`:鉴权 + 批量执行 `updates` 与 `merges` -- 约定响应结构:`{ success: boolean, ... }`,失败时带 `error` + `details` - -**Must NOT do**: - -- 不引入 alias/redirect -- 不引入 merge history/回滚 - -**References**: - -- `prisma/schema.prisma` - `Tag`/`ProjectTag` 约束(unique/复合主键) -- `src/app/api/webhook/projects/route.ts` - API key 鉴权 + Zod safeParse + 返回结构风格 -- `src/lib/validations.ts` - Zod schema 放置位置/风格 - -**Acceptance Criteria**: - -- 产出一个明确的 JSON schema(写进实现用的 Zod schema),覆盖:空 merges、重复 tagId、自合并、无效 tagId、nameEn 缺失。 - ---- - -### 2) 设计 n8n 每日 Tag Janitor Workflow - -**What to do**: - -- 节点建议:Cron → HTTP(GET /api/tags) → Code(预处理/分块) → LLM(生成 merges+updates) → Code(JSON 校验/拆批) → HTTP(POST /api/tags/maintenance) → 汇总通知 -- Prompt 输出必须是严格 JSON(避免解析失败) -- 需要考虑 LLM 输入过大时的分块策略(按字母/按 projectCount/按 createdAt) - -**Must NOT do**: - -- 不要求人工审核 - -**References**: - -- `src/hooks/useProjects.ts` - tags 的 `_count.projects` 查询方式(作为 /api/tags 的实现依据) -- `src/components/project/TagCloud.tsx` - UI 依赖 slug(理解合并后旧 slug 失效的表现) - -**Acceptance Criteria**: - -- n8n workflow 最终只依赖两个站点接口(GET /api/tags, POST /api/tags/maintenance) -- LLM 输出 JSON 包含:`merges`(数组)与 `updates`(数组) - ---- - -### 3) 实现 `GET /api/tags` - -**What to do**: - -- 新增 route:`src/app/api/tags/route.ts`(实现 GET) -- 用 Prisma 查询 Tag 并 include `_count.projects` - -**References**: - -- `src/hooks/useProjects.ts` - `getAllTags()` 的 Prisma 查询(可直接复用/抽成共享函数) - -**Acceptance Criteria (agent-executable)**: - -```bash -curl -s http://localhost:3000/api/tags | jq '.success' -# Assert: true - -curl -s http://localhost:3000/api/tags | jq '.tags[0] | has("id") and has("slug") and has("_count")' -# Assert: true -``` - ---- - -### 4) 实现 `POST /api/tags/maintenance`(updates + merges) - -**What to do**: - -- 新增 route:`src/app/api/tags/maintenance/route.ts`(实现 POST) -- 鉴权:复用 `WEBHOOK_API_KEY` + `crypto.timingSafeEqual()`(参考 webhook) -- Zod 校验请求体 -- Prisma 事务执行: - 1. 执行 `updates`(主要是 nameEn 补全;允许 name/nameEn 相同) - 2. 执行 `merges` - - target tag:若 n8n 给的是 id → 使用;若给的是 name/nameEn → upsert/create(slug 用 `generateSlug()`) - - 迁移 `ProjectTag`:把 sourceTagIds 关联的 projectId 迁移到 targetTagId(需去重,避免复合主键冲突) - - 删除 source tags(`Tag` 记录) - 3. 结束后 revalidate 相关路径 - -**References**: - -- `prisma/schema.prisma` - `ProjectTag` 复合主键导致去重需求 -- `src/lib/slug.ts` - `generateSlug()` -- `src/app/api/webhook/projects/route.ts` - `timingSafeEqual` 鉴权与错误返回格式 - -**Acceptance Criteria (agent-executable)**: - -```bash -# 1) 未授权 -curl -s -X POST http://localhost:3000/api/tags/maintenance \ - -H 'Content-Type: application/json' \ - -d '{"updates":[],"merges":[]}' \ - | jq '.error' -# Assert: "Unauthorized" (or equivalent) - -# 2) 授权空操作成功 -curl -s -X POST http://localhost:3000/api/tags/maintenance \ - -H 'Content-Type: application/json' \ - -d '{"apiKey":"'"$WEBHOOK_API_KEY"'","updates":[],"merges":[]}' \ - | jq '.success' -# Assert: true -``` - ---- - -### 5) ISR/缓存刷新策略 - -**What to do**: - -- 在 maintenance 成功后调用 `revalidatePath()` 刷新 projects 列表页(至少 `/{locale}/projects` 对应的路径) - -**References**: - -- `src/hooks/AGENTS.md` - 建议在数据更新后使用 `revalidatePath()` - -**Acceptance Criteria**: - -- 合并后 1 次刷新内(无需等待 5 分钟)能看到 tags count 变化(用 Playwright 或 curl + 页面渲染断言)。 - ---- - -### 6) Vitest 测试(route + merge 逻辑) - -**What to do**: - -- 新增测试文件(参考风格):`src/app/api/events/route.test.ts` -- 至少覆盖: - - 未授权 401 - - body schema 校验失败 400 - - 合并执行时: - - 迁移 projectTag 去重逻辑不会抛错 - - source tag 会被删除 - - target tag 的 nameEn 会被补全/更新 - -**References**: - -- `src/app/api/events/route.test.ts` - NextRequest 调用 route handler 的测试方式 - -**Acceptance Criteria (agent-executable)**: - -```bash -pnpm test -# Assert: exit code 0 -``` - ---- - -### 7) n8n Workflow 落地与一次端到端验证 - -**What to do**: - -- 在 n8n 中配置: - - 站点 baseUrl - - `WEBHOOK_API_KEY` - - LLM 凭证 -- 先 dry-run(只输出 merges/updates,不调用 maintenance) -- 再 real-run(调用 maintenance) - -**Acceptance Criteria (agent-executable)**: - -- 运行一次后: - - `/api/tags` 返回的 tag 总数下降(或至少不增长) - - 采样 1-2 个被合并的 source tag,其 slug 再用于 `/{locale}/projects?tag=` 时返回 0 项(可接受) - - 被合并的项目在新 canonical tag slug 下能被筛出来(`src/hooks/useProjects.ts` 的 tag filter 生效) - ---- - -## Notes / Gotchas - -- `Tag.name` 与 `Tag.slug` 均为唯一:当 AI 生成 canonical 名称时,可能撞车;API 需要在创建/更新时处理并返回可读错误(让 n8n 重试或改名)。 -- 合并迁移时必须去重:`ProjectTag` 的 `@@id([projectId, tagId])` 会在“同一项目已拥有 target tag”时导致冲突。 -- 如果未来 tag 规模增大,n8n 端需要升级候选生成(先 lexical/embedding 粗筛,再让 LLM 决策)。本计划先按“规模尚可”实现全量日更。 diff --git a/.sisyphus/reports/tag-100-final-plan.json b/.sisyphus/reports/tag-100-final-plan.json deleted file mode 100644 index d216adf..0000000 --- a/.sisyphus/reports/tag-100-final-plan.json +++ /dev/null @@ -1,615 +0,0 @@ -{ - "totalTags": 859, - "keepCount": 100, - "dropCount": 759, - "uncoveredProjects": [], - "forcedAdded": [ - { - "id": "cmkqnj1pt0001l504vmrywqeg", - "name": "需要配置", - "projects": 163 - } - ], - "keepTop100": [ - { - "id": "cmk9fpg4h0000kt048l3a2nqu", - "name": "Python", - "nameEn": "Python", - "projects": 268 - }, - { - "id": "cmkqimx520005jx04vyegybp9", - "name": "AI开发工具", - "nameEn": "AI Development Tool", - "projects": 213 - }, - { - "id": "cmkqnj1pt0001l504vmrywqeg", - "name": "需要配置", - "nameEn": "Requires Configuration", - "projects": 163 - }, - { - "id": "cmkjjb37v00064j12b84dsrwg", - "name": "TypeScript", - "nameEn": "TypeScript", - "projects": 118 - }, - { - "id": "cmks0yz0h002xl1040w65w47d", - "name": "AI代理", - "nameEn": "AI Agents", - "projects": 52 - }, - { - "id": "cmkql448t0002l504m7bkovze", - "name": "自动化", - "nameEn": "Automation", - "projects": 46 - }, - { - "id": "cmkqjvuo5000ejr04y9ksio48", - "name": "Docker", - "nameEn": "Docker", - "projects": 43 - }, - { - "id": "cmkjjb37v00074j122wpemyas", - "name": "Node.js", - "nameEn": "Node.js", - "projects": 40 - }, - { - "id": "cmkoutfvz0005jr04knetubim", - "name": "JavaScript", - "nameEn": "JavaScript", - "projects": 34 - }, - { - "id": "cmkqklwxm0018jr04zyzlrmlv", - "name": "React", - "nameEn": "React", - "projects": 34 - }, - { - "id": "cmkqjgpzq0002l104qknt9pho", - "name": "开源", - "nameEn": "Open Source", - "projects": 33 - }, - { - "id": "cmkqnvcbg0001jo04us31pcaj", - "name": "低学习成本", - "nameEn": "Low Learning Curve", - "projects": 31 - }, - { - "id": "cmkqk1dbi000ljr045f2rm18m", - "name": "PyTorch", - "nameEn": "PyTorch", - "projects": 30 - }, - { - "id": "cmkrv2jpg0000l204swrx5xh4", - "name": "LangChain", - "nameEn": "LangChain", - "projects": 29 - }, - { - "id": "cmkqlcukz000dl5044c6z8542", - "name": "多智能体系统", - "nameEn": "Multi-Agent System", - "projects": 28 - }, - { - "id": "cmkqk1dc5000ojr04jq0exq1z", - "name": "大语言模型", - "nameEn": null, - "projects": 25 - }, - { - "id": "cmkqimwxl0000jx04l8q0rdzo", - "name": "LLM", - "nameEn": "LLM", - "projects": 22 - }, - { - "id": "cmks0rbk1002jl104uc7yg2xv", - "name": "Go", - "nameEn": "Go", - "projects": 20 - }, - { - "id": "cmkqm1c6j0003jl04clkn9qyb", - "name": "企业级", - "nameEn": "Enterprise", - "projects": 20 - }, - { - "id": "cmkqnn8hj0006l504j2uedblz", - "name": "需要基础", - "nameEn": "Requires Basics", - "projects": 20 - }, - { - "id": "cmks0yz1e002yl104nw4apfcq", - "name": "Library", - "nameEn": "Library", - "projects": 18 - }, - { - "id": "cmkrx8euz0007ky04hrgq5ryg", - "name": "MCP", - "nameEn": "Model Context Protocol", - "projects": 18 - }, - { - "id": "cmks16lb0003ml104acf4zi7e", - "name": "Markdown", - "nameEn": "Markdown", - "projects": 17 - }, - { - "id": "cmkqklwr20016jr04opezrtpo", - "name": "Next.js", - "nameEn": "Next.js", - "projects": 17 - }, - { - "id": "cmks003y10015l104xruieqo9", - "name": "RAG", - "nameEn": "RAG", - "projects": 17 - }, - { - "id": "cmkqm1bzv0000jl04c3ks3w7w", - "name": "Java", - "nameEn": "Java", - "projects": 16 - }, - { - "id": "cmks0c9ll0022l10436svssah", - "name": "MIT许可", - "nameEn": "MIT License", - "projects": 16 - }, - { - "id": "cmkqnn8gl0005l5042jdeuuur", - "name": "LangGraph", - "nameEn": "LangGraph", - "projects": 15 - }, - { - "id": "cmkrx664z0000ky0481j124cq", - "name": "FastAPI", - "nameEn": "FastAPI", - "projects": 14 - }, - { - "id": "cmkqjvuir0008jr048591dzwp", - "name": "Rust", - "nameEn": "Rust", - "projects": 14 - }, - { - "id": "cmkrzlq020001l104sosii49r", - "name": "复杂部署", - "nameEn": "Complex Deployment", - "projects": 14 - }, - { - "id": "cmkjjb38l000c4j12oucpveph", - "name": "工作流自动化", - "nameEn": "Workflow Automation", - "projects": 14 - }, - { - "id": "cmks3iu8m006wl104ndk8no10", - "name": "C#", - "nameEn": "C#", - "projects": 11 - }, - { - "id": "cmkqnn8mm0007l5045q6z4zb0", - "name": "OpenAI", - "nameEn": "OpenAI", - "projects": 11 - }, - { - "id": "cmkrzvdfk000tl104jsq77zig", - "name": "OpenAI API", - "nameEn": "OpenAI API", - "projects": 11 - }, - { - "id": "cmks3d1wy006ll104gltxc3et", - "name": "SDK", - "nameEn": "SDK", - "projects": 11 - }, - { - "id": "cmks1aoxo003xl104gap1drce", - "name": "强化学习", - "nameEn": "Reinforcement Learning", - "projects": 11 - }, - { - "id": "cmks1aoyn003yl104r4zspprz", - "name": "机器学习", - "nameEn": "Machine Learning", - "projects": 11 - }, - { - "id": "cmksczul60015jv04ft4n1cb2", - "name": "GitHub", - "nameEn": "GitHub", - "projects": 10 - }, - { - "id": "cmks0m380002gl104o6f9cjtj", - "name": "PostgreSQL", - "nameEn": "PostgreSQL", - "projects": 10 - }, - { - "id": "cmkrx8f0j000aky046uomv3rs", - "name": "多模态", - "nameEn": "Multimodal", - "projects": 10 - }, - { - "id": "cmkqjs7e50004jr042mrpi3ac", - "name": "桌面应用", - "nameEn": null, - "projects": 10 - }, - { - "id": "cmkqk4i0d000vjr04ai18b8h3", - "name": "知识管理", - "nameEn": null, - "projects": 10 - }, - { - "id": "cmkqk4i0g000wjr04t82lzvaq", - "name": "向量数据库", - "nameEn": null, - "projects": 9 - }, - { - "id": "cmkrzlq0q0003l10453qhsniu", - "name": "Transformers", - "nameEn": "Transformers", - "projects": 8 - }, - { - "id": "cmks1ubzb004pl104mvs08pmu", - "name": "vLLM", - "nameEn": "vLLM", - "projects": 8 - }, - { - "id": "cmks0eqvq0028l10426ogrvkb", - "name": "云端服务", - "nameEn": "Cloud Service", - "projects": 8 - }, - { - "id": "cmks13z53003el104re4468xt", - "name": "代码生成", - "nameEn": "Code Generation", - "projects": 8 - }, - { - "id": "cmks05ci1001gl104lus7os2q", - "name": "企业级AI应用", - "nameEn": "Enterprise AI", - "projects": 8 - }, - { - "id": "cmks1s7p2004il104kpwprttq", - "name": "智能体框架", - "nameEn": "Agent Framework", - "projects": 8 - }, - { - "id": "cmkqjs7dw0003jr04uy2bguck", - "name": "跨平台", - "nameEn": null, - "projects": 8 - }, - { - "id": "cmkqklwxr0019jr048wp0hds8", - "name": "AI 助手", - "nameEn": "AI Assistant", - "projects": 7 - }, - { - "id": "cmks5ey0j001el204vjk69nwg", - "name": "AI安全", - "nameEn": "AI Security", - "projects": 7 - }, - { - "id": "cmkqjs77s0000jr041cypbjkn", - "name": "Electron", - "nameEn": "Electron", - "projects": 7 - }, - { - "id": "cmkry59tc0001jy04kr21i343", - "name": "Playwright", - "nameEn": "Playwright", - "projects": 7 - }, - { - "id": "cmk9fpg5f0003kt04fcgbvj7y", - "name": "多智能体", - "nameEn": "Multi-Agent", - "projects": 7 - }, - { - "id": "cmkrzxk4i0011l1045cubzwwr", - "name": "深度学习", - "nameEn": "Deep Learning", - "projects": 7 - }, - { - "id": "cmks13z46003dl1047ba0entr", - "name": "AI代理框架", - "nameEn": "AI Agent Framework", - "projects": 6 - }, - { - "id": "cmkry59u70003jy049eoxzkc7", - "name": "AI研究", - "nameEn": "AI Research", - "projects": 6 - }, - { - "id": "cmks2a6r1005il104ormxbqud", - "name": "Web应用", - "nameEn": "Web Application", - "projects": 6 - }, - { - "id": "cmkshrboj0000la04h6bmugvj", - "name": "学术研究", - "nameEn": "Academic Research", - "projects": 6 - }, - { - "id": "cmks112wx0039l104zltcl2hp", - "name": "工作流编排", - "nameEn": "Workflow Orchestration", - "projects": 6 - }, - { - "id": "cmkrzvdfe000sl104q0gge67f", - "name": "教程", - "nameEn": "Tutorial", - "projects": 6 - }, - { - "id": "cmks3w0g0000bl904u4lmk5d5", - "name": "浏览器自动化", - "nameEn": "Browser Automation", - "projects": 6 - }, - { - "id": "cmkt8m55q000nla04kgfld9x3", - "name": "知识图谱", - "nameEn": "Knowledge Graph", - "projects": 6 - }, - { - "id": "cmkqklwxj0017jr04czubkbbi", - "name": "知识库", - "nameEn": "Knowledge Base", - "projects": 6 - }, - { - "id": "cmkshubus0004la04zjuh5nn5", - "name": "自主智能体", - "nameEn": "Autonomous Agents", - "projects": 6 - }, - { - "id": "cmkqk4i0d000ujr045d2njmc7", - "name": "隐私保护", - "nameEn": null, - "projects": 6 - }, - { - "id": "cmks02o6m001al104gi1jztpn", - "name": "Apache-2.0", - "nameEn": "Apache-2.0", - "projects": 5 - }, - { - "id": "cmks4roac000el204paypouzl", - "name": "CLI", - "nameEn": "CLI", - "projects": 5 - }, - { - "id": "cmkt7yhut0000la040ij92vgk", - "name": "MCP协议", - "nameEn": "MCP Protocol", - "projects": 5 - }, - { - "id": "cmks5rr2e0020l204sn9ydvka", - "name": "Nest.js", - "nameEn": "Nest.js", - "projects": 5 - }, - { - "id": "cmkjjb37z00084j121651lnjl", - "name": "Vue.js", - "nameEn": "Vue.js", - "projects": 5 - }, - { - "id": "cmksg1wc90061jv04114izr2d", - "name": "Web自动化", - "nameEn": "Web Automation", - "projects": 5 - }, - { - "id": "cmkuulu5d000jjc04u7hx67sz", - "name": "分布式系统", - "nameEn": "Distributed Systems", - "projects": 5 - }, - { - "id": "cmkqimx3n0001jx043mvrmarx", - "name": "可视化", - "nameEn": "Visualization", - "projects": 5 - }, - { - "id": "cmksf0ik1004ijv047hiusscw", - "name": "多模态AI", - "nameEn": "Multimodal AI", - "projects": 5 - }, - { - "id": "cmktoexz8001wld046f13barg", - "name": "多语言支持", - "nameEn": "Multi-language Support", - "projects": 5 - }, - { - "id": "cmks2gqie005ol104v4d6nvck", - "name": "数据分析", - "nameEn": "Data Analytics", - "projects": 5 - }, - { - "id": "cmks0equs0026l104sl4td34d", - "name": "聊天机器人", - "nameEn": "Chatbot", - "projects": 5 - }, - { - "id": "cmkjjb38j000b4j125a3fg282", - "name": "自托管", - "nameEn": "Self-hosted", - "projects": 5 - }, - { - "id": "cmktkj5to001hl704wtmqjft4", - "name": "自然语言处理", - "nameEn": "Natural Language Processing", - "projects": 5 - }, - { - "id": "cmkrzxk4a0010l104u7irj9je", - "name": "计算机视觉", - "nameEn": "Computer Vision", - "projects": 5 - }, - { - "id": "cmkto2g8o0019ld04b61pgj4y", - "name": "AI研究资源", - "nameEn": "AI Research Resource", - "projects": 4 - }, - { - "id": "cmktk5hid000sl7049a2bo5al", - "name": "Benchmark", - "nameEn": "Benchmark", - "projects": 4 - }, - { - "id": "cmkv6j7uq000gk4043x1gfnp1", - "name": "Bun", - "nameEn": "Bun", - "projects": 4 - }, - { - "id": "cmkti8i470000kz04bpsajkzk", - "name": "ChromaDB", - "nameEn": "ChromaDB", - "projects": 4 - }, - { - "id": "cmks47ct90013l904x03rupwb", - "name": "Flask", - "nameEn": "Flask", - "projects": 4 - }, - { - "id": "cmkql9bho0007l50406oflxr0", - "name": "Gradio", - "nameEn": "Gradio", - "projects": 4 - }, - { - "id": "cmks112qf0034l104gkihwp10", - "name": "JavaScript/TypeScript", - "nameEn": "JavaScript/TypeScript", - "projects": 4 - }, - { - "id": "cmks1s7ut004jl104gvlybgfs", - "name": "LiteLLM", - "nameEn": "LiteLLM", - "projects": 4 - }, - { - "id": "cmks4pb54000bl204erpb9bbp", - "name": "LlamaIndex", - "nameEn": "LlamaIndex", - "projects": 4 - }, - { - "id": "cmksfnrz1005qjv044pnraikr", - "name": "LLM应用", - "nameEn": "LLM Applications", - "projects": 4 - }, - { - "id": "cmkqimx480003jx04bcqob7pb", - "name": "NLP", - "nameEn": "NLP", - "projects": 4 - }, - { - "id": "cmkqm1c690001jl04ge5elv46", - "name": "Spring Boot", - "nameEn": "Spring Boot", - "projects": 4 - }, - { - "id": "cmks2a6l3005gl1040u4s0k4s", - "name": "Streamlit", - "nameEn": "Streamlit", - "projects": 4 - }, - { - "id": "cmkth0k4x0026ju04t0euxxwk", - "name": "VS Code扩展", - "nameEn": "VS Code Extension", - "projects": 4 - }, - { - "id": "cmkscxy490012jv04rf4k4h4h", - "name": "学术资源", - "nameEn": "Academic Resource", - "projects": 4 - }, - { - "id": "cmkscgc0z0007jv04nwmzbn8c", - "name": "工具调用", - "nameEn": "Tool Calling", - "projects": 4 - }, - { - "id": "cmkuxtw0m002ojm04pl9qazxs", - "name": "用户通知", - "nameEn": "User Notification", - "projects": 1 - } - ] -} \ No newline at end of file diff --git a/.sisyphus/reports/tag-100-keep-ids-final.json b/.sisyphus/reports/tag-100-keep-ids-final.json deleted file mode 100644 index 1d51619..0000000 --- a/.sisyphus/reports/tag-100-keep-ids-final.json +++ /dev/null @@ -1,102 +0,0 @@ -[ - "cmk9fpg4h0000kt048l3a2nqu", - "cmkjjb37v00064j12b84dsrwg", - "cmkqimx520005jx04vyegybp9", - "cmks16lb0003ml104acf4zi7e", - "cmks0yz0h002xl1040w65w47d", - "cmkqjgpzq0002l104qknt9pho", - "cmkql448t0002l504m7bkovze", - "cmkqm1bzv0000jl04c3ks3w7w", - "cmkoutfvz0005jr04knetubim", - "cmks0rbk1002jl104uc7yg2xv", - "cmks3iu8m006wl104ndk8no10", - "cmkqk1dc5000ojr04jq0exq1z", - "cmkqimwxl0000jx04l8q0rdzo", - "cmks0yz1e002yl104nw4apfcq", - "cmkqjvuo5000ejr04y9ksio48", - "cmkjjb37v00074j122wpemyas", - "cmkqnvcbg0001jo04us31pcaj", - "cmkrzlq020001l104sosii49r", - "cmks1aoyn003yl104r4zspprz", - "cmks5ey0j001el204vjk69nwg", - "cmkshrboj0000la04h6bmugvj", - "cmkscgc0z0007jv04nwmzbn8c", - "cmkscxy490012jv04rf4k4h4h", - "cmkuxtw0m002ojm04pl9qazxs", - "cmkqklwxm0018jr04zyzlrmlv", - "cmkqk1dbi000ljr045f2rm18m", - "cmkrv2jpg0000l204swrx5xh4", - "cmkqlcukz000dl5044c6z8542", - "cmkqm1c6j0003jl04clkn9qyb", - "cmkqnn8hj0006l504j2uedblz", - "cmkrx8euz0007ky04hrgq5ryg", - "cmkqklwr20016jr04opezrtpo", - "cmks003y10015l104xruieqo9", - "cmks0c9ll0022l10436svssah", - "cmkqnn8gl0005l5042jdeuuur", - "cmkrx664z0000ky0481j124cq", - "cmkqjvuir0008jr048591dzwp", - "cmkjjb38l000c4j12oucpveph", - "cmkqnn8mm0007l5045q6z4zb0", - "cmkrzvdfk000tl104jsq77zig", - "cmks3d1wy006ll104gltxc3et", - "cmks1aoxo003xl104gap1drce", - "cmksczul60015jv04ft4n1cb2", - "cmks0m380002gl104o6f9cjtj", - "cmkrx8f0j000aky046uomv3rs", - "cmkqjs7e50004jr042mrpi3ac", - "cmkqk4i0d000vjr04ai18b8h3", - "cmkqk4i0g000wjr04t82lzvaq", - "cmkrzlq0q0003l10453qhsniu", - "cmks1ubzb004pl104mvs08pmu", - "cmks0eqvq0028l10426ogrvkb", - "cmks13z53003el104re4468xt", - "cmks05ci1001gl104lus7os2q", - "cmks1s7p2004il104kpwprttq", - "cmkqjs7dw0003jr04uy2bguck", - "cmkqklwxr0019jr048wp0hds8", - "cmkqjs77s0000jr041cypbjkn", - "cmkry59tc0001jy04kr21i343", - "cmk9fpg5f0003kt04fcgbvj7y", - "cmkrzxk4i0011l1045cubzwwr", - "cmks13z46003dl1047ba0entr", - "cmkry59u70003jy049eoxzkc7", - "cmks2a6r1005il104ormxbqud", - "cmks112wx0039l104zltcl2hp", - "cmkrzvdfe000sl104q0gge67f", - "cmks3w0g0000bl904u4lmk5d5", - "cmkt8m55q000nla04kgfld9x3", - "cmkqklwxj0017jr04czubkbbi", - "cmkshubus0004la04zjuh5nn5", - "cmkqk4i0d000ujr045d2njmc7", - "cmks02o6m001al104gi1jztpn", - "cmks4roac000el204paypouzl", - "cmkt7yhut0000la040ij92vgk", - "cmks5rr2e0020l204sn9ydvka", - "cmkjjb37z00084j121651lnjl", - "cmksg1wc90061jv04114izr2d", - "cmkuulu5d000jjc04u7hx67sz", - "cmkqimx3n0001jx043mvrmarx", - "cmksf0ik1004ijv047hiusscw", - "cmktoexz8001wld046f13barg", - "cmks2gqie005ol104v4d6nvck", - "cmks0equs0026l104sl4td34d", - "cmkjjb38j000b4j125a3fg282", - "cmktkj5to001hl704wtmqjft4", - "cmkrzxk4a0010l104u7irj9je", - "cmkto2g8o0019ld04b61pgj4y", - "cmktk5hid000sl7049a2bo5al", - "cmkv6j7uq000gk4043x1gfnp1", - "cmkti8i470000kz04bpsajkzk", - "cmks47ct90013l904x03rupwb", - "cmkql9bho0007l50406oflxr0", - "cmks112qf0034l104gkihwp10", - "cmks1s7ut004jl104gvlybgfs", - "cmks4pb54000bl204erpb9bbp", - "cmksfnrz1005qjv044pnraikr", - "cmkqimx480003jx04bcqob7pb", - "cmkqm1c690001jl04ge5elv46", - "cmks2a6l3005gl1040u4s0k4s", - "cmkth0k4x0026ju04t0euxxwk", - "cmkqnj1pt0001l504vmrywqeg" -] \ No newline at end of file diff --git a/.sisyphus/reports/tag-100-keep-ids.json b/.sisyphus/reports/tag-100-keep-ids.json deleted file mode 100644 index 391cecc..0000000 --- a/.sisyphus/reports/tag-100-keep-ids.json +++ /dev/null @@ -1,102 +0,0 @@ -[ - "cmk9fpg4h0000kt048l3a2nqu", - "cmkqimx520005jx04vyegybp9", - "cmkjjb37v00064j12b84dsrwg", - "cmks0yz0h002xl1040w65w47d", - "cmkql448t0002l504m7bkovze", - "cmkqjvuo5000ejr04y9ksio48", - "cmkjjb37v00074j122wpemyas", - "cmkoutfvz0005jr04knetubim", - "cmkqklwxm0018jr04zyzlrmlv", - "cmkqjgpzq0002l104qknt9pho", - "cmkqnvcbg0001jo04us31pcaj", - "cmkqk1dbi000ljr045f2rm18m", - "cmkrv2jpg0000l204swrx5xh4", - "cmkqlcukz000dl5044c6z8542", - "cmkqk1dc5000ojr04jq0exq1z", - "cmkqimwxl0000jx04l8q0rdzo", - "cmks0rbk1002jl104uc7yg2xv", - "cmkqm1c6j0003jl04clkn9qyb", - "cmkqnn8hj0006l504j2uedblz", - "cmks0yz1e002yl104nw4apfcq", - "cmkrx8euz0007ky04hrgq5ryg", - "cmks16lb0003ml104acf4zi7e", - "cmkqklwr20016jr04opezrtpo", - "cmks003y10015l104xruieqo9", - "cmkqm1bzv0000jl04c3ks3w7w", - "cmks0c9ll0022l10436svssah", - "cmkqnn8gl0005l5042jdeuuur", - "cmkrzlq020001l104sosii49r", - "cmkrx664z0000ky0481j124cq", - "cmkqjvuir0008jr048591dzwp", - "cmkjjb38l000c4j12oucpveph", - "cmks3iu8m006wl104ndk8no10", - "cmks1aoyn003yl104r4zspprz", - "cmkqnn8mm0007l5045q6z4zb0", - "cmkrzvdfk000tl104jsq77zig", - "cmks3d1wy006ll104gltxc3et", - "cmks1aoxo003xl104gap1drce", - "cmksczul60015jv04ft4n1cb2", - "cmks0m380002gl104o6f9cjtj", - "cmkrx8f0j000aky046uomv3rs", - "cmkqjs7e50004jr042mrpi3ac", - "cmkqk4i0d000vjr04ai18b8h3", - "cmkqk4i0g000wjr04t82lzvaq", - "cmkrzlq0q0003l10453qhsniu", - "cmks1ubzb004pl104mvs08pmu", - "cmks0eqvq0028l10426ogrvkb", - "cmks13z53003el104re4468xt", - "cmks05ci1001gl104lus7os2q", - "cmks1s7p2004il104kpwprttq", - "cmkqjs7dw0003jr04uy2bguck", - "cmks5ey0j001el204vjk69nwg", - "cmkqklwxr0019jr048wp0hds8", - "cmkqjs77s0000jr041cypbjkn", - "cmkry59tc0001jy04kr21i343", - "cmk9fpg5f0003kt04fcgbvj7y", - "cmkrzxk4i0011l1045cubzwwr", - "cmkshrboj0000la04h6bmugvj", - "cmks13z46003dl1047ba0entr", - "cmkry59u70003jy049eoxzkc7", - "cmks2a6r1005il104ormxbqud", - "cmks112wx0039l104zltcl2hp", - "cmkrzvdfe000sl104q0gge67f", - "cmks3w0g0000bl904u4lmk5d5", - "cmkt8m55q000nla04kgfld9x3", - "cmkqklwxj0017jr04czubkbbi", - "cmkshubus0004la04zjuh5nn5", - "cmkqk4i0d000ujr045d2njmc7", - "cmks02o6m001al104gi1jztpn", - "cmks4roac000el204paypouzl", - "cmkt7yhut0000la040ij92vgk", - "cmks5rr2e0020l204sn9ydvka", - "cmkjjb37z00084j121651lnjl", - "cmksg1wc90061jv04114izr2d", - "cmkuulu5d000jjc04u7hx67sz", - "cmkqimx3n0001jx043mvrmarx", - "cmksf0ik1004ijv047hiusscw", - "cmktoexz8001wld046f13barg", - "cmks2gqie005ol104v4d6nvck", - "cmks0equs0026l104sl4td34d", - "cmkjjb38j000b4j125a3fg282", - "cmktkj5to001hl704wtmqjft4", - "cmkrzxk4a0010l104u7irj9je", - "cmkscgc0z0007jv04nwmzbn8c", - "cmkscxy490012jv04rf4k4h4h", - "cmkto2g8o0019ld04b61pgj4y", - "cmktk5hid000sl7049a2bo5al", - "cmkv6j7uq000gk4043x1gfnp1", - "cmkti8i470000kz04bpsajkzk", - "cmks47ct90013l904x03rupwb", - "cmkql9bho0007l50406oflxr0", - "cmks112qf0034l104gkihwp10", - "cmks1s7ut004jl104gvlybgfs", - "cmks4pb54000bl204erpb9bbp", - "cmksfnrz1005qjv044pnraikr", - "cmkqimx480003jx04bcqob7pb", - "cmkqm1c690001jl04ge5elv46", - "cmks2a6l3005gl1040u4s0k4s", - "cmkth0k4x0026ju04t0euxxwk", - "cmkuuqo3g000njc04l72uwte3", - "cmkuxtw0m002ojm04pl9qazxs" -] \ No newline at end of file diff --git a/.sisyphus/reports/tag-100-plan-preview.json b/.sisyphus/reports/tag-100-plan-preview.json deleted file mode 100644 index 5ce63d1..0000000 --- a/.sisyphus/reports/tag-100-plan-preview.json +++ /dev/null @@ -1,784 +0,0 @@ -{ - "totalTags": 859, - "candidatesAfterGenericFilter": 831, - "selectedCount": 100, - "selectedProjectsCoverage": { - "coveredProjects": 453, - "totalProjects": 454, - "coverageRatio": 0.9978, - "uncoveredProjects": 1 - }, - "dropCount": 759, - "topSelected": [ - { - "id": "cmk9fpg4h0000kt048l3a2nqu", - "name": "Python", - "nameEn": "Python", - "projects": 268 - }, - { - "id": "cmkqimx520005jx04vyegybp9", - "name": "AI开发工具", - "nameEn": "AI Development Tool", - "projects": 213 - }, - { - "id": "cmkjjb37v00064j12b84dsrwg", - "name": "TypeScript", - "nameEn": "TypeScript", - "projects": 118 - }, - { - "id": "cmks0yz0h002xl1040w65w47d", - "name": "AI代理", - "nameEn": "AI Agents", - "projects": 52 - }, - { - "id": "cmkql448t0002l504m7bkovze", - "name": "自动化", - "nameEn": "Automation", - "projects": 46 - }, - { - "id": "cmkqjvuo5000ejr04y9ksio48", - "name": "Docker", - "nameEn": "Docker", - "projects": 43 - }, - { - "id": "cmkjjb37v00074j122wpemyas", - "name": "Node.js", - "nameEn": "Node.js", - "projects": 40 - }, - { - "id": "cmkoutfvz0005jr04knetubim", - "name": "JavaScript", - "nameEn": "JavaScript", - "projects": 34 - }, - { - "id": "cmkqklwxm0018jr04zyzlrmlv", - "name": "React", - "nameEn": "React", - "projects": 34 - }, - { - "id": "cmkqjgpzq0002l104qknt9pho", - "name": "开源", - "nameEn": "Open Source", - "projects": 33 - }, - { - "id": "cmkqnvcbg0001jo04us31pcaj", - "name": "低学习成本", - "nameEn": "Low Learning Curve", - "projects": 31 - }, - { - "id": "cmkqk1dbi000ljr045f2rm18m", - "name": "PyTorch", - "nameEn": "PyTorch", - "projects": 30 - }, - { - "id": "cmkrv2jpg0000l204swrx5xh4", - "name": "LangChain", - "nameEn": "LangChain", - "projects": 29 - }, - { - "id": "cmkqlcukz000dl5044c6z8542", - "name": "多智能体系统", - "nameEn": "Multi-Agent System", - "projects": 28 - }, - { - "id": "cmkqk1dc5000ojr04jq0exq1z", - "name": "大语言模型", - "nameEn": null, - "projects": 25 - }, - { - "id": "cmkqimwxl0000jx04l8q0rdzo", - "name": "LLM", - "nameEn": "LLM", - "projects": 22 - }, - { - "id": "cmks0rbk1002jl104uc7yg2xv", - "name": "Go", - "nameEn": "Go", - "projects": 20 - }, - { - "id": "cmkqm1c6j0003jl04clkn9qyb", - "name": "企业级", - "nameEn": "Enterprise", - "projects": 20 - }, - { - "id": "cmkqnn8hj0006l504j2uedblz", - "name": "需要基础", - "nameEn": "Requires Basics", - "projects": 20 - }, - { - "id": "cmks0yz1e002yl104nw4apfcq", - "name": "Library", - "nameEn": "Library", - "projects": 18 - }, - { - "id": "cmkrx8euz0007ky04hrgq5ryg", - "name": "MCP", - "nameEn": "Model Context Protocol", - "projects": 18 - }, - { - "id": "cmks16lb0003ml104acf4zi7e", - "name": "Markdown", - "nameEn": "Markdown", - "projects": 17 - }, - { - "id": "cmkqklwr20016jr04opezrtpo", - "name": "Next.js", - "nameEn": "Next.js", - "projects": 17 - }, - { - "id": "cmks003y10015l104xruieqo9", - "name": "RAG", - "nameEn": "RAG", - "projects": 17 - }, - { - "id": "cmkqm1bzv0000jl04c3ks3w7w", - "name": "Java", - "nameEn": "Java", - "projects": 16 - }, - { - "id": "cmks0c9ll0022l10436svssah", - "name": "MIT许可", - "nameEn": "MIT License", - "projects": 16 - }, - { - "id": "cmkqnn8gl0005l5042jdeuuur", - "name": "LangGraph", - "nameEn": "LangGraph", - "projects": 15 - }, - { - "id": "cmkrzlq020001l104sosii49r", - "name": "复杂部署", - "nameEn": "Complex Deployment", - "projects": 14 - }, - { - "id": "cmkrx664z0000ky0481j124cq", - "name": "FastAPI", - "nameEn": "FastAPI", - "projects": 14 - }, - { - "id": "cmkqjvuir0008jr048591dzwp", - "name": "Rust", - "nameEn": "Rust", - "projects": 14 - }, - { - "id": "cmkjjb38l000c4j12oucpveph", - "name": "工作流自动化", - "nameEn": "Workflow Automation", - "projects": 14 - }, - { - "id": "cmks3iu8m006wl104ndk8no10", - "name": "C#", - "nameEn": "C#", - "projects": 11 - }, - { - "id": "cmks1aoyn003yl104r4zspprz", - "name": "机器学习", - "nameEn": "Machine Learning", - "projects": 11 - }, - { - "id": "cmkqnn8mm0007l5045q6z4zb0", - "name": "OpenAI", - "nameEn": "OpenAI", - "projects": 11 - }, - { - "id": "cmkrzvdfk000tl104jsq77zig", - "name": "OpenAI API", - "nameEn": "OpenAI API", - "projects": 11 - }, - { - "id": "cmks3d1wy006ll104gltxc3et", - "name": "SDK", - "nameEn": "SDK", - "projects": 11 - }, - { - "id": "cmks1aoxo003xl104gap1drce", - "name": "强化学习", - "nameEn": "Reinforcement Learning", - "projects": 11 - }, - { - "id": "cmksczul60015jv04ft4n1cb2", - "name": "GitHub", - "nameEn": "GitHub", - "projects": 10 - }, - { - "id": "cmks0m380002gl104o6f9cjtj", - "name": "PostgreSQL", - "nameEn": "PostgreSQL", - "projects": 10 - }, - { - "id": "cmkrx8f0j000aky046uomv3rs", - "name": "多模态", - "nameEn": "Multimodal", - "projects": 10 - }, - { - "id": "cmkqjs7e50004jr042mrpi3ac", - "name": "桌面应用", - "nameEn": null, - "projects": 10 - }, - { - "id": "cmkqk4i0d000vjr04ai18b8h3", - "name": "知识管理", - "nameEn": null, - "projects": 10 - }, - { - "id": "cmkqk4i0g000wjr04t82lzvaq", - "name": "向量数据库", - "nameEn": null, - "projects": 9 - }, - { - "id": "cmkrzlq0q0003l10453qhsniu", - "name": "Transformers", - "nameEn": "Transformers", - "projects": 8 - }, - { - "id": "cmks1ubzb004pl104mvs08pmu", - "name": "vLLM", - "nameEn": "vLLM", - "projects": 8 - }, - { - "id": "cmks0eqvq0028l10426ogrvkb", - "name": "云端服务", - "nameEn": "Cloud Service", - "projects": 8 - }, - { - "id": "cmks13z53003el104re4468xt", - "name": "代码生成", - "nameEn": "Code Generation", - "projects": 8 - }, - { - "id": "cmks05ci1001gl104lus7os2q", - "name": "企业级AI应用", - "nameEn": "Enterprise AI", - "projects": 8 - }, - { - "id": "cmks1s7p2004il104kpwprttq", - "name": "智能体框架", - "nameEn": "Agent Framework", - "projects": 8 - }, - { - "id": "cmkqjs7dw0003jr04uy2bguck", - "name": "跨平台", - "nameEn": null, - "projects": 8 - }, - { - "id": "cmks5ey0j001el204vjk69nwg", - "name": "AI安全", - "nameEn": "AI Security", - "projects": 7 - }, - { - "id": "cmkqklwxr0019jr048wp0hds8", - "name": "AI 助手", - "nameEn": "AI Assistant", - "projects": 7 - }, - { - "id": "cmkqjs77s0000jr041cypbjkn", - "name": "Electron", - "nameEn": "Electron", - "projects": 7 - }, - { - "id": "cmkry59tc0001jy04kr21i343", - "name": "Playwright", - "nameEn": "Playwright", - "projects": 7 - }, - { - "id": "cmk9fpg5f0003kt04fcgbvj7y", - "name": "多智能体", - "nameEn": "Multi-Agent", - "projects": 7 - }, - { - "id": "cmkrzxk4i0011l1045cubzwwr", - "name": "深度学习", - "nameEn": "Deep Learning", - "projects": 7 - }, - { - "id": "cmkshrboj0000la04h6bmugvj", - "name": "学术研究", - "nameEn": "Academic Research", - "projects": 6 - }, - { - "id": "cmks13z46003dl1047ba0entr", - "name": "AI代理框架", - "nameEn": "AI Agent Framework", - "projects": 6 - }, - { - "id": "cmkry59u70003jy049eoxzkc7", - "name": "AI研究", - "nameEn": "AI Research", - "projects": 6 - }, - { - "id": "cmks2a6r1005il104ormxbqud", - "name": "Web应用", - "nameEn": "Web Application", - "projects": 6 - }, - { - "id": "cmks112wx0039l104zltcl2hp", - "name": "工作流编排", - "nameEn": "Workflow Orchestration", - "projects": 6 - }, - { - "id": "cmkrzvdfe000sl104q0gge67f", - "name": "教程", - "nameEn": "Tutorial", - "projects": 6 - }, - { - "id": "cmks3w0g0000bl904u4lmk5d5", - "name": "浏览器自动化", - "nameEn": "Browser Automation", - "projects": 6 - }, - { - "id": "cmkt8m55q000nla04kgfld9x3", - "name": "知识图谱", - "nameEn": "Knowledge Graph", - "projects": 6 - }, - { - "id": "cmkqklwxj0017jr04czubkbbi", - "name": "知识库", - "nameEn": "Knowledge Base", - "projects": 6 - }, - { - "id": "cmkshubus0004la04zjuh5nn5", - "name": "自主智能体", - "nameEn": "Autonomous Agents", - "projects": 6 - }, - { - "id": "cmkqk4i0d000ujr045d2njmc7", - "name": "隐私保护", - "nameEn": null, - "projects": 6 - }, - { - "id": "cmks02o6m001al104gi1jztpn", - "name": "Apache-2.0", - "nameEn": "Apache-2.0", - "projects": 5 - }, - { - "id": "cmks4roac000el204paypouzl", - "name": "CLI", - "nameEn": "CLI", - "projects": 5 - }, - { - "id": "cmkt7yhut0000la040ij92vgk", - "name": "MCP协议", - "nameEn": "MCP Protocol", - "projects": 5 - }, - { - "id": "cmks5rr2e0020l204sn9ydvka", - "name": "Nest.js", - "nameEn": "Nest.js", - "projects": 5 - }, - { - "id": "cmkjjb37z00084j121651lnjl", - "name": "Vue.js", - "nameEn": "Vue.js", - "projects": 5 - }, - { - "id": "cmksg1wc90061jv04114izr2d", - "name": "Web自动化", - "nameEn": "Web Automation", - "projects": 5 - }, - { - "id": "cmkuulu5d000jjc04u7hx67sz", - "name": "分布式系统", - "nameEn": "Distributed Systems", - "projects": 5 - }, - { - "id": "cmkqimx3n0001jx043mvrmarx", - "name": "可视化", - "nameEn": "Visualization", - "projects": 5 - }, - { - "id": "cmksf0ik1004ijv047hiusscw", - "name": "多模态AI", - "nameEn": "Multimodal AI", - "projects": 5 - }, - { - "id": "cmktoexz8001wld046f13barg", - "name": "多语言支持", - "nameEn": "Multi-language Support", - "projects": 5 - }, - { - "id": "cmks2gqie005ol104v4d6nvck", - "name": "数据分析", - "nameEn": "Data Analytics", - "projects": 5 - }, - { - "id": "cmks0equs0026l104sl4td34d", - "name": "聊天机器人", - "nameEn": "Chatbot", - "projects": 5 - }, - { - "id": "cmkjjb38j000b4j125a3fg282", - "name": "自托管", - "nameEn": "Self-hosted", - "projects": 5 - }, - { - "id": "cmktkj5to001hl704wtmqjft4", - "name": "自然语言处理", - "nameEn": "Natural Language Processing", - "projects": 5 - }, - { - "id": "cmkrzxk4a0010l104u7irj9je", - "name": "计算机视觉", - "nameEn": "Computer Vision", - "projects": 5 - }, - { - "id": "cmkscgc0z0007jv04nwmzbn8c", - "name": "工具调用", - "nameEn": "Tool Calling", - "projects": 4 - }, - { - "id": "cmkscxy490012jv04rf4k4h4h", - "name": "学术资源", - "nameEn": "Academic Resource", - "projects": 4 - }, - { - "id": "cmkto2g8o0019ld04b61pgj4y", - "name": "AI研究资源", - "nameEn": "AI Research Resource", - "projects": 4 - }, - { - "id": "cmktk5hid000sl7049a2bo5al", - "name": "Benchmark", - "nameEn": "Benchmark", - "projects": 4 - }, - { - "id": "cmkv6j7uq000gk4043x1gfnp1", - "name": "Bun", - "nameEn": "Bun", - "projects": 4 - }, - { - "id": "cmkti8i470000kz04bpsajkzk", - "name": "ChromaDB", - "nameEn": "ChromaDB", - "projects": 4 - }, - { - "id": "cmks47ct90013l904x03rupwb", - "name": "Flask", - "nameEn": "Flask", - "projects": 4 - }, - { - "id": "cmkql9bho0007l50406oflxr0", - "name": "Gradio", - "nameEn": "Gradio", - "projects": 4 - }, - { - "id": "cmks112qf0034l104gkihwp10", - "name": "JavaScript/TypeScript", - "nameEn": "JavaScript/TypeScript", - "projects": 4 - }, - { - "id": "cmks1s7ut004jl104gvlybgfs", - "name": "LiteLLM", - "nameEn": "LiteLLM", - "projects": 4 - }, - { - "id": "cmks4pb54000bl204erpb9bbp", - "name": "LlamaIndex", - "nameEn": "LlamaIndex", - "projects": 4 - }, - { - "id": "cmksfnrz1005qjv044pnraikr", - "name": "LLM应用", - "nameEn": "LLM Applications", - "projects": 4 - }, - { - "id": "cmkqimx480003jx04bcqob7pb", - "name": "NLP", - "nameEn": "NLP", - "projects": 4 - }, - { - "id": "cmkqm1c690001jl04ge5elv46", - "name": "Spring Boot", - "nameEn": "Spring Boot", - "projects": 4 - }, - { - "id": "cmks2a6l3005gl1040u4s0k4s", - "name": "Streamlit", - "nameEn": "Streamlit", - "projects": 4 - }, - { - "id": "cmkth0k4x0026ju04t0euxxwk", - "name": "VS Code扩展", - "nameEn": "VS Code Extension", - "projects": 4 - }, - { - "id": "cmkuuqo3g000njc04l72uwte3", - "name": "文件系统操作", - "nameEn": "File System Operations", - "projects": 2 - }, - { - "id": "cmkuxtw0m002ojm04pl9qazxs", - "name": "用户通知", - "nameEn": "User Notification", - "projects": 1 - } - ], - "genericRemovedTop": [ - { - "id": "cmkoutfta0001jr04qlwpr3xh", - "name": "活跃维护", - "nameEn": "Active Maintenance", - "projects": 317 - }, - { - "id": "cmkqk1dbs000njr0418v38jh4", - "name": "可商用", - "nameEn": null, - "projects": 171 - }, - { - "id": "cmkqnj1pt0001l504vmrywqeg", - "name": "需要配置", - "nameEn": "Requires Configuration", - "projects": 163 - }, - { - "id": "cmkqk1div000qjr04ckie91k5", - "name": "AI", - "nameEn": "AI", - "projects": 115 - }, - { - "id": "cmkoutfub0004jr049qn5fxc0", - "name": "易上手", - "nameEn": "Easy to Start", - "projects": 107 - }, - { - "id": "cmkoutftj0002jr04xqiuu0cb", - "name": "框架", - "nameEn": "Framework", - "projects": 90 - }, - { - "id": "cmkqk1d5o000kjr049wqf7i5o", - "name": "开发工具", - "nameEn": null, - "projects": 88 - }, - { - "id": "cmkqnvc540000jo04qrexxuhd", - "name": "开箱即用", - "nameEn": "Ready to Use", - "projects": 72 - }, - { - "id": "cmkqkapuh0010jr043s59my3y", - "name": "文档完善", - "nameEn": "Well Documented", - "projects": 69 - }, - { - "id": "cmkqk4i0g000xjr04sn06e7wt", - "name": "本地部署", - "nameEn": null, - "projects": 38 - }, - { - "id": "cmkoutfu10003jr04dw11uzvu", - "name": "有示例", - "nameEn": "Has Examples", - "projects": 36 - }, - { - "id": "cmkql442c0000l504qbomadup", - "name": "需要Docker", - "nameEn": "Requires Docker", - "projects": 36 - }, - { - "id": "cmkryxmob0003l80414uo5zqz", - "name": "CLI工具", - "nameEn": "CLI Tool", - "projects": 33 - }, - { - "id": "cmks1fjqc0042l104cqi0dh9v", - "name": "工具", - "nameEn": "Tool", - "projects": 33 - }, - { - "id": "cmkqkaq140011jr04n5aqr763", - "name": "平台", - "nameEn": "Platform", - "projects": 29 - }, - { - "id": "cmkrzlq0e0002l104jfnarvv8", - "name": "智能体", - "nameEn": "Agent", - "projects": 16 - }, - { - "id": "cmk9fpg560001kt04ab5369hg", - "name": "开发框架", - "nameEn": "Development Framework", - "projects": 10 - }, - { - "id": "cmk9fpg580002kt04sslxarl5", - "name": "人工智能", - "nameEn": "Artificial Intelligence", - "projects": 7 - }, - { - "id": "cmkrv2jvc0001l204703u5836", - "name": "开发者工具", - "nameEn": "Developer Tool", - "projects": 4 - }, - { - "id": "cmksdlj9o0029jv04x9tdyc11", - "name": "科研工具", - "nameEn": "Research Tool", - "projects": 4 - }, - { - "id": "cmks4tb3k000jl204z6oxfuyh", - "name": "终端工具", - "nameEn": "Terminal Tools", - "projects": 4 - }, - { - "id": "cmkqjgpzv0003l104asyyc6dg", - "name": "低代码平台", - "nameEn": "Low-Code Platform", - "projects": 3 - }, - { - "id": "cmkrxh1kc000hky04rpxvrj1w", - "name": "数据库工具", - "nameEn": "Database Tools", - "projects": 3 - }, - { - "id": "cmks05cng001il1049zztjibt", - "name": "Agent平台", - "nameEn": "Agent Platform", - "projects": 3 - }, - { - "id": "cmkjjb38300094j12lum5vwjj", - "name": "集成平台", - "nameEn": "Integration Platform", - "projects": 2 - }, - { - "id": "cmkt8ex6g000ala0488t486bj", - "name": "平台/框架", - "nameEn": "Platform/Framework", - "projects": 1 - }, - { - "id": "cmktpw9pe003tld04fjjgo4zt", - "name": "系统监控", - "nameEn": "System Monitoring", - "projects": 1 - }, - { - "id": "cmkuxtvzn002mjm04wdcx9wwi", - "name": "系统通知", - "nameEn": "System Notification", - "projects": 1 - } - ] -} \ No newline at end of file diff --git a/.sisyphus/reports/tag-audit-before-100-cap.json b/.sisyphus/reports/tag-audit-before-100-cap.json deleted file mode 100644 index 33a8875..0000000 --- a/.sisyphus/reports/tag-audit-before-100-cap.json +++ /dev/null @@ -1,305 +0,0 @@ -{ - "totalTags": 859, - "totalProjects": 454, - "activeProjects": 444, - "distributionAtLeast": { - "1": 859, - "2": 283, - "3": 169, - "4": 130, - "5": 100, - "6": 85, - "8": 67, - "10": 59, - "15": 43, - "20": 34, - "30": 26, - "50": 13 - }, - "top100CoverageProjects": 453, - "projectsWithoutTop100Tag": 1, - "top20": [ - { - "name": "活跃维护", - "nameEn": "Active Maintenance", - "projects": 317 - }, - { - "name": "Python", - "nameEn": "Python", - "projects": 268 - }, - { - "name": "AI开发工具", - "nameEn": "AI Development Tool", - "projects": 213 - }, - { - "name": "可商用", - "nameEn": null, - "projects": 171 - }, - { - "name": "需要配置", - "nameEn": "Requires Configuration", - "projects": 163 - }, - { - "name": "TypeScript", - "nameEn": "TypeScript", - "projects": 118 - }, - { - "name": "AI", - "nameEn": "AI", - "projects": 115 - }, - { - "name": "易上手", - "nameEn": "Easy to Start", - "projects": 107 - }, - { - "name": "框架", - "nameEn": "Framework", - "projects": 90 - }, - { - "name": "开发工具", - "nameEn": null, - "projects": 88 - }, - { - "name": "开箱即用", - "nameEn": "Ready to Use", - "projects": 72 - }, - { - "name": "文档完善", - "nameEn": "Well Documented", - "projects": 69 - }, - { - "name": "AI代理", - "nameEn": "AI Agents", - "projects": 52 - }, - { - "name": "自动化", - "nameEn": "Automation", - "projects": 46 - }, - { - "name": "Docker", - "nameEn": "Docker", - "projects": 43 - }, - { - "name": "Node.js", - "nameEn": "Node.js", - "projects": 40 - }, - { - "name": "本地部署", - "nameEn": null, - "projects": 38 - }, - { - "name": "有示例", - "nameEn": "Has Examples", - "projects": 36 - }, - { - "name": "需要Docker", - "nameEn": "Requires Docker", - "projects": 36 - }, - { - "name": "JavaScript", - "nameEn": "JavaScript", - "projects": 34 - } - ], - "genericTagsTop30": [ - { - "id": "cmkqimx520005jx04vyegybp9", - "name": "AI开发工具", - "nameEn": "AI Development Tool", - "projects": 213 - }, - { - "id": "cmkqk1div000qjr04ckie91k5", - "name": "AI", - "nameEn": "AI", - "projects": 115 - }, - { - "id": "cmkoutftj0002jr04xqiuu0cb", - "name": "框架", - "nameEn": "Framework", - "projects": 90 - }, - { - "id": "cmkqk1d5o000kjr049wqf7i5o", - "name": "开发工具", - "nameEn": null, - "projects": 88 - }, - { - "id": "cmks0yz0h002xl1040w65w47d", - "name": "AI代理", - "nameEn": "AI Agents", - "projects": 52 - }, - { - "id": "cmkryxmob0003l80414uo5zqz", - "name": "CLI工具", - "nameEn": "CLI Tool", - "projects": 33 - }, - { - "id": "cmks1fjqc0042l104cqi0dh9v", - "name": "工具", - "nameEn": "Tool", - "projects": 33 - }, - { - "id": "cmkqkaq140011jr04n5aqr763", - "name": "平台", - "nameEn": "Platform", - "projects": 29 - }, - { - "id": "cmkqlcukz000dl5044c6z8542", - "name": "多智能体系统", - "nameEn": "Multi-Agent System", - "projects": 28 - }, - { - "id": "cmkrzlq0e0002l104jfnarvv8", - "name": "智能体", - "nameEn": "Agent", - "projects": 16 - }, - { - "id": "cmk9fpg560001kt04ab5369hg", - "name": "开发框架", - "nameEn": "Development Framework", - "projects": 10 - }, - { - "id": "cmks05ci1001gl104lus7os2q", - "name": "企业级AI应用", - "nameEn": "Enterprise AI", - "projects": 8 - }, - { - "id": "cmks1s7p2004il104kpwprttq", - "name": "智能体框架", - "nameEn": "Agent Framework", - "projects": 8 - }, - { - "id": "cmk9fpg580002kt04sslxarl5", - "name": "人工智能", - "nameEn": "Artificial Intelligence", - "projects": 7 - }, - { - "id": "cmk9fpg5f0003kt04fcgbvj7y", - "name": "多智能体", - "nameEn": "Multi-Agent", - "projects": 7 - }, - { - "id": "cmkqklwxr0019jr048wp0hds8", - "name": "AI 助手", - "nameEn": "AI Assistant", - "projects": 7 - }, - { - "id": "cmkshubus0004la04zjuh5nn5", - "name": "自主智能体", - "nameEn": "Autonomous Agents", - "projects": 6 - }, - { - "id": "cmks13z46003dl1047ba0entr", - "name": "AI代理框架", - "nameEn": "AI Agent Framework", - "projects": 6 - }, - { - "id": "cmksf0ik1004ijv047hiusscw", - "name": "多模态AI", - "nameEn": "Multimodal AI", - "projects": 5 - }, - { - "id": "cmkrv2jvc0001l204703u5836", - "name": "开发者工具", - "nameEn": "Developer Tool", - "projects": 4 - }, - { - "id": "cmksdlj9o0029jv04x9tdyc11", - "name": "科研工具", - "nameEn": "Research Tool", - "projects": 4 - }, - { - "id": "cmksf2ncg004pjv0446rsr0e6", - "name": "生成式AI", - "nameEn": "Generative AI", - "projects": 4 - }, - { - "id": "cmks4tb3k000jl204z6oxfuyh", - "name": "终端工具", - "nameEn": "Terminal Tools", - "projects": 4 - }, - { - "id": "cmkqjgpzv0003l104asyyc6dg", - "name": "低代码平台", - "nameEn": "Low-Code Platform", - "projects": 3 - }, - { - "id": "cmkqlcuqx000fl504ywms66me", - "name": "AI开发框架", - "nameEn": "AI Development Framework", - "projects": 3 - }, - { - "id": "cmkujqy2l0007jx04w5zxt8be", - "name": "游戏AI", - "nameEn": "Game AI", - "projects": 3 - }, - { - "id": "cmkrxh1kc000hky04rpxvrj1w", - "name": "数据库工具", - "nameEn": "Database Tools", - "projects": 3 - }, - { - "id": "cmks05cng001il1049zztjibt", - "name": "Agent平台", - "nameEn": "Agent Platform", - "projects": 3 - }, - { - "id": "cmks3y6z0000hl9048h3bv62n", - "name": "记忆系统", - "nameEn": "Memory System", - "projects": 3 - }, - { - "id": "cmkjjb38300094j12lum5vwjj", - "name": "集成平台", - "nameEn": "Integration Platform", - "projects": 2 - } - ] -} \ No newline at end of file diff --git a/.sisyphus/reports/tag-cap-100-result-2026-02-21.json b/.sisyphus/reports/tag-cap-100-result-2026-02-21.json deleted file mode 100644 index 5e1c07f..0000000 --- a/.sisyphus/reports/tag-cap-100-result-2026-02-21.json +++ /dev/null @@ -1,163 +0,0 @@ -{ - "totalTags": 100, - "totalProjectTags": 1888, - "zeroTags": 0, - "aiTagExists": false, - "aiTag": null, - "projectCoverage": { - "covered": 454, - "total": 454 - }, - "top30": [ - { - "name": "Python", - "nameEn": "Python", - "projects": 268 - }, - { - "name": "AI开发工具", - "nameEn": "AI Development Tool", - "projects": 213 - }, - { - "name": "需要配置", - "nameEn": "Requires Configuration", - "projects": 163 - }, - { - "name": "TypeScript", - "nameEn": "TypeScript", - "projects": 118 - }, - { - "name": "AI代理", - "nameEn": "AI Agents", - "projects": 52 - }, - { - "name": "自动化", - "nameEn": "Automation", - "projects": 46 - }, - { - "name": "Docker", - "nameEn": "Docker", - "projects": 43 - }, - { - "name": "Node.js", - "nameEn": "Node.js", - "projects": 40 - }, - { - "name": "JavaScript", - "nameEn": "JavaScript", - "projects": 34 - }, - { - "name": "React", - "nameEn": "React", - "projects": 34 - }, - { - "name": "开源", - "nameEn": "Open Source", - "projects": 33 - }, - { - "name": "低学习成本", - "nameEn": "Low Learning Curve", - "projects": 31 - }, - { - "name": "PyTorch", - "nameEn": "PyTorch", - "projects": 30 - }, - { - "name": "LangChain", - "nameEn": "LangChain", - "projects": 29 - }, - { - "name": "多智能体系统", - "nameEn": "Multi-Agent System", - "projects": 28 - }, - { - "name": "大语言模型", - "nameEn": null, - "projects": 25 - }, - { - "name": "LLM", - "nameEn": "LLM", - "projects": 22 - }, - { - "name": "Go", - "nameEn": "Go", - "projects": 20 - }, - { - "name": "企业级", - "nameEn": "Enterprise", - "projects": 20 - }, - { - "name": "需要基础", - "nameEn": "Requires Basics", - "projects": 20 - }, - { - "name": "Library", - "nameEn": "Library", - "projects": 18 - }, - { - "name": "MCP", - "nameEn": "Model Context Protocol", - "projects": 18 - }, - { - "name": "Markdown", - "nameEn": "Markdown", - "projects": 17 - }, - { - "name": "Next.js", - "nameEn": "Next.js", - "projects": 17 - }, - { - "name": "RAG", - "nameEn": "RAG", - "projects": 17 - }, - { - "name": "Java", - "nameEn": "Java", - "projects": 16 - }, - { - "name": "MIT许可", - "nameEn": "MIT License", - "projects": 16 - }, - { - "name": "LangGraph", - "nameEn": "LangGraph", - "projects": 15 - }, - { - "name": "FastAPI", - "nameEn": "FastAPI", - "projects": 14 - }, - { - "name": "Rust", - "nameEn": "Rust", - "projects": 14 - } - ] -} \ No newline at end of file diff --git a/.sisyphus/reports/tag-cleanup-plan-2026-02-21.json b/.sisyphus/reports/tag-cleanup-plan-2026-02-21.json deleted file mode 100644 index 0458abc..0000000 --- a/.sisyphus/reports/tag-cleanup-plan-2026-02-21.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "generatedAt": "2026-02-21T03:47:31.204Z", - "mergeGroupCount": 6, - "mergePairCount": 6, - "mergePairs": [ - { - "sourceId": "cmkoutg0o0006jr04goozjhft", - "targetId": "cmkqimx520005jx04vyegybp9", - "key": "ai development tool" - }, - { - "sourceId": "cmkjjb387000a4j12pdf123zn", - "targetId": "cmks0yz0h002xl1040w65w47d", - "key": "ai agent" - }, - { - "sourceId": "cmkun0ao70048jx04atm2lwwq", - "targetId": "cmkryxmob0003l80414uo5zqz", - "key": "cli tool" - }, - { - "sourceId": "cmkrzqdlu000fl104qmy9gu2u", - "targetId": "cmkqlcukz000dl5044c6z8542", - "key": "multi agent system" - }, - { - "sourceId": "cmktk3biu000nl704iiohmv4i", - "targetId": "cmksdlj9o0029jv04x9tdyc11", - "key": "research tool" - }, - { - "sourceId": "cmktpotkp003eld04mv09ckde", - "targetId": "cmkshubus0004la04zjuh5nn5", - "key": "autonomous agent" - } - ], - "before": { - "totalTags": 955, - "totalProjectTags": 4131, - "zeroTags": 90, - "missingNameEnAscii": null - }, - "sqlPath": ".sisyphus/sql/tag-cleanup-2026-02-21.sql" -} \ No newline at end of file diff --git a/.sisyphus/reports/tag-cleanup-result-2026-02-21.json b/.sisyphus/reports/tag-cleanup-result-2026-02-21.json deleted file mode 100644 index 9945dc7..0000000 --- a/.sisyphus/reports/tag-cleanup-result-2026-02-21.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "after": { - "totalTags": 859, - "totalProjectTags": 4131, - "zeroTags": 0, - "missingNameEn": 17, - "missingNameEnAscii": 0 - }, - "deletedSourceTagsStillExists": [], - "mergedTargetProjectCounts": [ - { - "id": "cmkshubus0004la04zjuh5nn5", - "name": "自主智能体", - "projects": 6 - }, - { - "id": "cmkryxmob0003l80414uo5zqz", - "name": "CLI工具", - "projects": 33 - }, - { - "id": "cmkqlcukz000dl5044c6z8542", - "name": "多智能体系统", - "projects": 28 - }, - { - "id": "cmkqimx520005jx04vyegybp9", - "name": "AI开发工具", - "projects": 213 - }, - { - "id": "cmks0yz0h002xl1040w65w47d", - "name": "AI代理", - "projects": 52 - }, - { - "id": "cmksdlj9o0029jv04x9tdyc11", - "name": "科研工具", - "projects": 4 - } - ] -} \ No newline at end of file diff --git a/.sisyphus/reports/tag-current-100-full.json b/.sisyphus/reports/tag-current-100-full.json deleted file mode 100644 index fd4f045..0000000 --- a/.sisyphus/reports/tag-current-100-full.json +++ /dev/null @@ -1,1002 +0,0 @@ -[ - { - "id": "cmk9fpg4h0000kt048l3a2nqu", - "name": "Python", - "nameEn": "Python", - "slug": "python", - "_count": { - "projects": 268 - }, - "projectCount": 268 - }, - { - "id": "cmkqimx520005jx04vyegybp9", - "name": "AI开发工具", - "nameEn": "AI Development Tool", - "slug": "ai-development-tool", - "_count": { - "projects": 213 - }, - "projectCount": 213 - }, - { - "id": "cmkqnj1pt0001l504vmrywqeg", - "name": "需要配置", - "nameEn": "Requires Configuration", - "slug": "requires-configuration", - "_count": { - "projects": 163 - }, - "projectCount": 163 - }, - { - "id": "cmkjjb37v00064j12b84dsrwg", - "name": "TypeScript", - "nameEn": "TypeScript", - "slug": "typescript", - "_count": { - "projects": 118 - }, - "projectCount": 118 - }, - { - "id": "cmks0yz0h002xl1040w65w47d", - "name": "AI代理", - "nameEn": "AI Agents", - "slug": "ai-agents", - "_count": { - "projects": 52 - }, - "projectCount": 52 - }, - { - "id": "cmkql448t0002l504m7bkovze", - "name": "自动化", - "nameEn": "Automation", - "slug": "automation", - "_count": { - "projects": 46 - }, - "projectCount": 46 - }, - { - "id": "cmkqjvuo5000ejr04y9ksio48", - "name": "Docker", - "nameEn": "Docker", - "slug": "docker", - "_count": { - "projects": 43 - }, - "projectCount": 43 - }, - { - "id": "cmkjjb37v00074j122wpemyas", - "name": "Node.js", - "nameEn": "Node.js", - "slug": "nodejs", - "_count": { - "projects": 40 - }, - "projectCount": 40 - }, - { - "id": "cmkoutfvz0005jr04knetubim", - "name": "JavaScript", - "nameEn": "JavaScript", - "slug": "javascript", - "_count": { - "projects": 34 - }, - "projectCount": 34 - }, - { - "id": "cmkqklwxm0018jr04zyzlrmlv", - "name": "React", - "nameEn": "React", - "slug": "react", - "_count": { - "projects": 34 - }, - "projectCount": 34 - }, - { - "id": "cmkqjgpzq0002l104qknt9pho", - "name": "开源", - "nameEn": "Open Source", - "slug": "open-source", - "_count": { - "projects": 33 - }, - "projectCount": 33 - }, - { - "id": "cmkqnvcbg0001jo04us31pcaj", - "name": "低学习成本", - "nameEn": "Low Learning Curve", - "slug": "low-learning-curve", - "_count": { - "projects": 31 - }, - "projectCount": 31 - }, - { - "id": "cmkqk1dbi000ljr045f2rm18m", - "name": "PyTorch", - "nameEn": "PyTorch", - "slug": "pytorch", - "_count": { - "projects": 30 - }, - "projectCount": 30 - }, - { - "id": "cmkrv2jpg0000l204swrx5xh4", - "name": "LangChain", - "nameEn": "LangChain", - "slug": "langchain", - "_count": { - "projects": 29 - }, - "projectCount": 29 - }, - { - "id": "cmkqlcukz000dl5044c6z8542", - "name": "多智能体系统", - "nameEn": "Multi-Agent System", - "slug": "multi-agent-system", - "_count": { - "projects": 28 - }, - "projectCount": 28 - }, - { - "id": "cmkqk1dc5000ojr04jq0exq1z", - "name": "大语言模型", - "nameEn": null, - "slug": "大语言模型", - "_count": { - "projects": 25 - }, - "projectCount": 25 - }, - { - "id": "cmkqimwxl0000jx04l8q0rdzo", - "name": "LLM", - "nameEn": "LLM", - "slug": "llm", - "_count": { - "projects": 22 - }, - "projectCount": 22 - }, - { - "id": "cmks0rbk1002jl104uc7yg2xv", - "name": "Go", - "nameEn": "Go", - "slug": "go", - "_count": { - "projects": 20 - }, - "projectCount": 20 - }, - { - "id": "cmkqm1c6j0003jl04clkn9qyb", - "name": "企业级", - "nameEn": "Enterprise", - "slug": "enterprise", - "_count": { - "projects": 20 - }, - "projectCount": 20 - }, - { - "id": "cmkqnn8hj0006l504j2uedblz", - "name": "需要基础", - "nameEn": "Requires Basics", - "slug": "requires-basics", - "_count": { - "projects": 20 - }, - "projectCount": 20 - }, - { - "id": "cmks0yz1e002yl104nw4apfcq", - "name": "Library", - "nameEn": "Library", - "slug": "library", - "_count": { - "projects": 18 - }, - "projectCount": 18 - }, - { - "id": "cmkrx8euz0007ky04hrgq5ryg", - "name": "MCP", - "nameEn": "Model Context Protocol", - "slug": "model-context-protocol", - "_count": { - "projects": 18 - }, - "projectCount": 18 - }, - { - "id": "cmks16lb0003ml104acf4zi7e", - "name": "Markdown", - "nameEn": "Markdown", - "slug": "markdown", - "_count": { - "projects": 17 - }, - "projectCount": 17 - }, - { - "id": "cmkqklwr20016jr04opezrtpo", - "name": "Next.js", - "nameEn": "Next.js", - "slug": "nextjs", - "_count": { - "projects": 17 - }, - "projectCount": 17 - }, - { - "id": "cmks003y10015l104xruieqo9", - "name": "RAG", - "nameEn": "RAG", - "slug": "rag", - "_count": { - "projects": 17 - }, - "projectCount": 17 - }, - { - "id": "cmkqm1bzv0000jl04c3ks3w7w", - "name": "Java", - "nameEn": "Java", - "slug": "java", - "_count": { - "projects": 16 - }, - "projectCount": 16 - }, - { - "id": "cmks0c9ll0022l10436svssah", - "name": "MIT许可", - "nameEn": "MIT License", - "slug": "mit-license", - "_count": { - "projects": 16 - }, - "projectCount": 16 - }, - { - "id": "cmkqnn8gl0005l5042jdeuuur", - "name": "LangGraph", - "nameEn": "LangGraph", - "slug": "langgraph", - "_count": { - "projects": 15 - }, - "projectCount": 15 - }, - { - "id": "cmkrx664z0000ky0481j124cq", - "name": "FastAPI", - "nameEn": "FastAPI", - "slug": "fastapi", - "_count": { - "projects": 14 - }, - "projectCount": 14 - }, - { - "id": "cmkqjvuir0008jr048591dzwp", - "name": "Rust", - "nameEn": "Rust", - "slug": "rust", - "_count": { - "projects": 14 - }, - "projectCount": 14 - }, - { - "id": "cmkrzlq020001l104sosii49r", - "name": "复杂部署", - "nameEn": "Complex Deployment", - "slug": "complex-deployment", - "_count": { - "projects": 14 - }, - "projectCount": 14 - }, - { - "id": "cmkjjb38l000c4j12oucpveph", - "name": "工作流自动化", - "nameEn": "Workflow Automation", - "slug": "workflow-automation", - "_count": { - "projects": 14 - }, - "projectCount": 14 - }, - { - "id": "cmks3iu8m006wl104ndk8no10", - "name": "C#", - "nameEn": "C#", - "slug": "c", - "_count": { - "projects": 11 - }, - "projectCount": 11 - }, - { - "id": "cmkqnn8mm0007l5045q6z4zb0", - "name": "OpenAI", - "nameEn": "OpenAI", - "slug": "openai", - "_count": { - "projects": 11 - }, - "projectCount": 11 - }, - { - "id": "cmkrzvdfk000tl104jsq77zig", - "name": "OpenAI API", - "nameEn": "OpenAI API", - "slug": "openai-api", - "_count": { - "projects": 11 - }, - "projectCount": 11 - }, - { - "id": "cmks3d1wy006ll104gltxc3et", - "name": "SDK", - "nameEn": "SDK", - "slug": "sdk", - "_count": { - "projects": 11 - }, - "projectCount": 11 - }, - { - "id": "cmks1aoxo003xl104gap1drce", - "name": "强化学习", - "nameEn": "Reinforcement Learning", - "slug": "reinforcement-learning", - "_count": { - "projects": 11 - }, - "projectCount": 11 - }, - { - "id": "cmks1aoyn003yl104r4zspprz", - "name": "机器学习", - "nameEn": "Machine Learning", - "slug": "machine-learning", - "_count": { - "projects": 11 - }, - "projectCount": 11 - }, - { - "id": "cmksczul60015jv04ft4n1cb2", - "name": "GitHub", - "nameEn": "GitHub", - "slug": "github", - "_count": { - "projects": 10 - }, - "projectCount": 10 - }, - { - "id": "cmks0m380002gl104o6f9cjtj", - "name": "PostgreSQL", - "nameEn": "PostgreSQL", - "slug": "postgresql", - "_count": { - "projects": 10 - }, - "projectCount": 10 - }, - { - "id": "cmkrx8f0j000aky046uomv3rs", - "name": "多模态", - "nameEn": "Multimodal", - "slug": "multimodal", - "_count": { - "projects": 10 - }, - "projectCount": 10 - }, - { - "id": "cmkqjs7e50004jr042mrpi3ac", - "name": "桌面应用", - "nameEn": null, - "slug": "桌面应用", - "_count": { - "projects": 10 - }, - "projectCount": 10 - }, - { - "id": "cmkqk4i0d000vjr04ai18b8h3", - "name": "知识管理", - "nameEn": null, - "slug": "知识管理", - "_count": { - "projects": 10 - }, - "projectCount": 10 - }, - { - "id": "cmkqk4i0g000wjr04t82lzvaq", - "name": "向量数据库", - "nameEn": null, - "slug": "向量数据库", - "_count": { - "projects": 9 - }, - "projectCount": 9 - }, - { - "id": "cmkrzlq0q0003l10453qhsniu", - "name": "Transformers", - "nameEn": "Transformers", - "slug": "transformers", - "_count": { - "projects": 8 - }, - "projectCount": 8 - }, - { - "id": "cmks1ubzb004pl104mvs08pmu", - "name": "vLLM", - "nameEn": "vLLM", - "slug": "vllm", - "_count": { - "projects": 8 - }, - "projectCount": 8 - }, - { - "id": "cmks0eqvq0028l10426ogrvkb", - "name": "云端服务", - "nameEn": "Cloud Service", - "slug": "cloud-service", - "_count": { - "projects": 8 - }, - "projectCount": 8 - }, - { - "id": "cmks13z53003el104re4468xt", - "name": "代码生成", - "nameEn": "Code Generation", - "slug": "code-generation", - "_count": { - "projects": 8 - }, - "projectCount": 8 - }, - { - "id": "cmks05ci1001gl104lus7os2q", - "name": "企业级AI应用", - "nameEn": "Enterprise AI", - "slug": "enterprise-ai", - "_count": { - "projects": 8 - }, - "projectCount": 8 - }, - { - "id": "cmks1s7p2004il104kpwprttq", - "name": "智能体框架", - "nameEn": "Agent Framework", - "slug": "agent-framework", - "_count": { - "projects": 8 - }, - "projectCount": 8 - }, - { - "id": "cmkqjs7dw0003jr04uy2bguck", - "name": "跨平台", - "nameEn": null, - "slug": "跨平台", - "_count": { - "projects": 8 - }, - "projectCount": 8 - }, - { - "id": "cmkqklwxr0019jr048wp0hds8", - "name": "AI 助手", - "nameEn": "AI Assistant", - "slug": "ai-assistant", - "_count": { - "projects": 7 - }, - "projectCount": 7 - }, - { - "id": "cmks5ey0j001el204vjk69nwg", - "name": "AI安全", - "nameEn": "AI Security", - "slug": "ai-security", - "_count": { - "projects": 7 - }, - "projectCount": 7 - }, - { - "id": "cmkqjs77s0000jr041cypbjkn", - "name": "Electron", - "nameEn": "Electron", - "slug": "electron", - "_count": { - "projects": 7 - }, - "projectCount": 7 - }, - { - "id": "cmkry59tc0001jy04kr21i343", - "name": "Playwright", - "nameEn": "Playwright", - "slug": "playwright", - "_count": { - "projects": 7 - }, - "projectCount": 7 - }, - { - "id": "cmk9fpg5f0003kt04fcgbvj7y", - "name": "多智能体", - "nameEn": "Multi-Agent", - "slug": "multi-agent", - "_count": { - "projects": 7 - }, - "projectCount": 7 - }, - { - "id": "cmkrzxk4i0011l1045cubzwwr", - "name": "深度学习", - "nameEn": "Deep Learning", - "slug": "deep-learning", - "_count": { - "projects": 7 - }, - "projectCount": 7 - }, - { - "id": "cmks13z46003dl1047ba0entr", - "name": "AI代理框架", - "nameEn": "AI Agent Framework", - "slug": "ai-agent-framework", - "_count": { - "projects": 6 - }, - "projectCount": 6 - }, - { - "id": "cmkry59u70003jy049eoxzkc7", - "name": "AI研究", - "nameEn": "AI Research", - "slug": "ai-research", - "_count": { - "projects": 6 - }, - "projectCount": 6 - }, - { - "id": "cmks2a6r1005il104ormxbqud", - "name": "Web应用", - "nameEn": "Web Application", - "slug": "web-application", - "_count": { - "projects": 6 - }, - "projectCount": 6 - }, - { - "id": "cmkshrboj0000la04h6bmugvj", - "name": "学术研究", - "nameEn": "Academic Research", - "slug": "academic-research", - "_count": { - "projects": 6 - }, - "projectCount": 6 - }, - { - "id": "cmks112wx0039l104zltcl2hp", - "name": "工作流编排", - "nameEn": "Workflow Orchestration", - "slug": "workflow-orchestration", - "_count": { - "projects": 6 - }, - "projectCount": 6 - }, - { - "id": "cmkrzvdfe000sl104q0gge67f", - "name": "教程", - "nameEn": "Tutorial", - "slug": "tutorial", - "_count": { - "projects": 6 - }, - "projectCount": 6 - }, - { - "id": "cmks3w0g0000bl904u4lmk5d5", - "name": "浏览器自动化", - "nameEn": "Browser Automation", - "slug": "browser-automation", - "_count": { - "projects": 6 - }, - "projectCount": 6 - }, - { - "id": "cmkt8m55q000nla04kgfld9x3", - "name": "知识图谱", - "nameEn": "Knowledge Graph", - "slug": "knowledge-graph", - "_count": { - "projects": 6 - }, - "projectCount": 6 - }, - { - "id": "cmkqklwxj0017jr04czubkbbi", - "name": "知识库", - "nameEn": "Knowledge Base", - "slug": "knowledge-base", - "_count": { - "projects": 6 - }, - "projectCount": 6 - }, - { - "id": "cmkshubus0004la04zjuh5nn5", - "name": "自主智能体", - "nameEn": "Autonomous Agents", - "slug": "autonomous-agents", - "_count": { - "projects": 6 - }, - "projectCount": 6 - }, - { - "id": "cmkqk4i0d000ujr045d2njmc7", - "name": "隐私保护", - "nameEn": null, - "slug": "隐私保护", - "_count": { - "projects": 6 - }, - "projectCount": 6 - }, - { - "id": "cmks02o6m001al104gi1jztpn", - "name": "Apache-2.0", - "nameEn": "Apache-2.0", - "slug": "apache-20", - "_count": { - "projects": 5 - }, - "projectCount": 5 - }, - { - "id": "cmks4roac000el204paypouzl", - "name": "CLI", - "nameEn": "CLI", - "slug": "cli", - "_count": { - "projects": 5 - }, - "projectCount": 5 - }, - { - "id": "cmkt7yhut0000la040ij92vgk", - "name": "MCP协议", - "nameEn": "MCP Protocol", - "slug": "mcp-protocol", - "_count": { - "projects": 5 - }, - "projectCount": 5 - }, - { - "id": "cmks5rr2e0020l204sn9ydvka", - "name": "Nest.js", - "nameEn": "Nest.js", - "slug": "nestjs", - "_count": { - "projects": 5 - }, - "projectCount": 5 - }, - { - "id": "cmkjjb37z00084j121651lnjl", - "name": "Vue.js", - "nameEn": "Vue.js", - "slug": "vuejs", - "_count": { - "projects": 5 - }, - "projectCount": 5 - }, - { - "id": "cmksg1wc90061jv04114izr2d", - "name": "Web自动化", - "nameEn": "Web Automation", - "slug": "web-automation", - "_count": { - "projects": 5 - }, - "projectCount": 5 - }, - { - "id": "cmkuulu5d000jjc04u7hx67sz", - "name": "分布式系统", - "nameEn": "Distributed Systems", - "slug": "distributed-systems", - "_count": { - "projects": 5 - }, - "projectCount": 5 - }, - { - "id": "cmkqimx3n0001jx043mvrmarx", - "name": "可视化", - "nameEn": "Visualization", - "slug": "visualization", - "_count": { - "projects": 5 - }, - "projectCount": 5 - }, - { - "id": "cmksf0ik1004ijv047hiusscw", - "name": "多模态AI", - "nameEn": "Multimodal AI", - "slug": "multimodal-ai", - "_count": { - "projects": 5 - }, - "projectCount": 5 - }, - { - "id": "cmktoexz8001wld046f13barg", - "name": "多语言支持", - "nameEn": "Multi-language Support", - "slug": "multi-language-support", - "_count": { - "projects": 5 - }, - "projectCount": 5 - }, - { - "id": "cmks2gqie005ol104v4d6nvck", - "name": "数据分析", - "nameEn": "Data Analytics", - "slug": "data-analytics", - "_count": { - "projects": 5 - }, - "projectCount": 5 - }, - { - "id": "cmks0equs0026l104sl4td34d", - "name": "聊天机器人", - "nameEn": "Chatbot", - "slug": "chatbot", - "_count": { - "projects": 5 - }, - "projectCount": 5 - }, - { - "id": "cmkjjb38j000b4j125a3fg282", - "name": "自托管", - "nameEn": "Self-hosted", - "slug": "self-hosted", - "_count": { - "projects": 5 - }, - "projectCount": 5 - }, - { - "id": "cmktkj5to001hl704wtmqjft4", - "name": "自然语言处理", - "nameEn": "Natural Language Processing", - "slug": "natural-language-processing", - "_count": { - "projects": 5 - }, - "projectCount": 5 - }, - { - "id": "cmkrzxk4a0010l104u7irj9je", - "name": "计算机视觉", - "nameEn": "Computer Vision", - "slug": "computer-vision", - "_count": { - "projects": 5 - }, - "projectCount": 5 - }, - { - "id": "cmkto2g8o0019ld04b61pgj4y", - "name": "AI研究资源", - "nameEn": "AI Research Resource", - "slug": "ai-research-resource", - "_count": { - "projects": 4 - }, - "projectCount": 4 - }, - { - "id": "cmktk5hid000sl7049a2bo5al", - "name": "Benchmark", - "nameEn": "Benchmark", - "slug": "benchmark", - "_count": { - "projects": 4 - }, - "projectCount": 4 - }, - { - "id": "cmkv6j7uq000gk4043x1gfnp1", - "name": "Bun", - "nameEn": "Bun", - "slug": "bun", - "_count": { - "projects": 4 - }, - "projectCount": 4 - }, - { - "id": "cmkti8i470000kz04bpsajkzk", - "name": "ChromaDB", - "nameEn": "ChromaDB", - "slug": "chromadb", - "_count": { - "projects": 4 - }, - "projectCount": 4 - }, - { - "id": "cmks47ct90013l904x03rupwb", - "name": "Flask", - "nameEn": "Flask", - "slug": "flask", - "_count": { - "projects": 4 - }, - "projectCount": 4 - }, - { - "id": "cmkql9bho0007l50406oflxr0", - "name": "Gradio", - "nameEn": "Gradio", - "slug": "gradio", - "_count": { - "projects": 4 - }, - "projectCount": 4 - }, - { - "id": "cmks112qf0034l104gkihwp10", - "name": "JavaScript/TypeScript", - "nameEn": "JavaScript/TypeScript", - "slug": "javascripttypescript", - "_count": { - "projects": 4 - }, - "projectCount": 4 - }, - { - "id": "cmksfnrz1005qjv044pnraikr", - "name": "LLM应用", - "nameEn": "LLM Applications", - "slug": "llm-applications", - "_count": { - "projects": 4 - }, - "projectCount": 4 - }, - { - "id": "cmks1s7ut004jl104gvlybgfs", - "name": "LiteLLM", - "nameEn": "LiteLLM", - "slug": "litellm", - "_count": { - "projects": 4 - }, - "projectCount": 4 - }, - { - "id": "cmks4pb54000bl204erpb9bbp", - "name": "LlamaIndex", - "nameEn": "LlamaIndex", - "slug": "llamaindex", - "_count": { - "projects": 4 - }, - "projectCount": 4 - }, - { - "id": "cmkqimx480003jx04bcqob7pb", - "name": "NLP", - "nameEn": "NLP", - "slug": "nlp", - "_count": { - "projects": 4 - }, - "projectCount": 4 - }, - { - "id": "cmkqm1c690001jl04ge5elv46", - "name": "Spring Boot", - "nameEn": "Spring Boot", - "slug": "spring-boot", - "_count": { - "projects": 4 - }, - "projectCount": 4 - }, - { - "id": "cmks2a6l3005gl1040u4s0k4s", - "name": "Streamlit", - "nameEn": "Streamlit", - "slug": "streamlit", - "_count": { - "projects": 4 - }, - "projectCount": 4 - }, - { - "id": "cmkth0k4x0026ju04t0euxxwk", - "name": "VS Code扩展", - "nameEn": "VS Code Extension", - "slug": "vs-code-extension", - "_count": { - "projects": 4 - }, - "projectCount": 4 - }, - { - "id": "cmkscxy490012jv04rf4k4h4h", - "name": "学术资源", - "nameEn": "Academic Resource", - "slug": "academic-resource", - "_count": { - "projects": 4 - }, - "projectCount": 4 - }, - { - "id": "cmkscgc0z0007jv04nwmzbn8c", - "name": "工具调用", - "nameEn": "Tool Calling", - "slug": "tool-calling", - "_count": { - "projects": 4 - }, - "projectCount": 4 - }, - { - "id": "cmkuxtw0m002ojm04pl9qazxs", - "name": "用户通知", - "nameEn": "User Notification", - "slug": "user-notification", - "_count": { - "projects": 1 - }, - "projectCount": 1 - } -] diff --git a/.sisyphus/reports/tag-semantic-prune-final-result-2026-02-21.json b/.sisyphus/reports/tag-semantic-prune-final-result-2026-02-21.json deleted file mode 100644 index 489a550..0000000 --- a/.sisyphus/reports/tag-semantic-prune-final-result-2026-02-21.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "executedAt": "2026-02-21T04:05:08.555Z", - "totalTags": 83, - "totalProjectTags": 1547, - "zeroTags": 0, - "totalProjects": 454, - "projectsWithTags": 454, - "projectsWithoutTags": 0, - "aiTagExists": false, - "removedNoise": [ - "开源", - "学术资源", - "复杂部署", - "用户通知", - "需要配置", - "低学习成本", - "云端服务", - "MIT许可", - "Apache-2.0", - "教程", - "企业级", - "需要基础", - "多语言支持", - "AI研究资源", - "跨平台", - "自托管", - "学术研究" - ], - "removedNoiseStillExists": [], - "topTags": [ - { - "name": "Python", - "nameEn": "Python", - "projectCount": 268 - }, - { - "name": "AI开发工具", - "nameEn": "AI Development Tool", - "projectCount": 215 - }, - { - "name": "TypeScript", - "nameEn": "TypeScript", - "projectCount": 118 - }, - { - "name": "AI代理", - "nameEn": "AI Agents", - "projectCount": 54 - }, - { - "name": "自动化", - "nameEn": "Automation", - "projectCount": 47 - }, - { - "name": "Docker", - "nameEn": "Docker", - "projectCount": 43 - }, - { - "name": "Node.js", - "nameEn": "Node.js", - "projectCount": 40 - }, - { - "name": "JavaScript", - "nameEn": "JavaScript", - "projectCount": 34 - }, - { - "name": "React", - "nameEn": "React", - "projectCount": 34 - }, - { - "name": "PyTorch", - "nameEn": "PyTorch", - "projectCount": 30 - }, - { - "name": "LangChain", - "nameEn": "LangChain", - "projectCount": 29 - }, - { - "name": "多智能体系统", - "nameEn": "Multi-Agent System", - "projectCount": 28 - }, - { - "name": "大语言模型", - "nameEn": null, - "projectCount": 25 - }, - { - "name": "LLM", - "nameEn": "LLM", - "projectCount": 22 - }, - { - "name": "Go", - "nameEn": "Go", - "projectCount": 20 - }, - { - "name": "Library", - "nameEn": "Library", - "projectCount": 18 - }, - { - "name": "MCP", - "nameEn": "Model Context Protocol", - "projectCount": 18 - }, - { - "name": "RAG", - "nameEn": "RAG", - "projectCount": 17 - }, - { - "name": "Next.js", - "nameEn": "Next.js", - "projectCount": 17 - }, - { - "name": "Markdown", - "nameEn": "Markdown", - "projectCount": 17 - } - ] -} diff --git a/.sisyphus/reports/tag-semantic-prune-safe-result-2026-02-21.json b/.sisyphus/reports/tag-semantic-prune-safe-result-2026-02-21.json deleted file mode 100644 index cb5018a..0000000 --- a/.sisyphus/reports/tag-semantic-prune-safe-result-2026-02-21.json +++ /dev/null @@ -1,127 +0,0 @@ -{ - "executedAt": "2026-02-21T04:03:10.320Z", - "removedNames": [ - "自托管", - "跨平台", - "企业级", - "需要基础", - "AI研究资源", - "多语言支持", - "教程", - "Apache-2.0", - "学术研究", - "MIT许可", - "云端服务", - "低学习成本" - ], - "removedStillExists": [], - "totalTags": 88, - "totalProjectTags": 1754, - "zeroTags": 0, - "totalProjects": 454, - "projectsWithTags": 454, - "projectsWithoutTags": 0, - "aiTagExists": false, - "topTags": [ - { - "name": "Python", - "nameEn": "Python", - "projectCount": 268 - }, - { - "name": "AI开发工具", - "nameEn": "AI Development Tool", - "projectCount": 213 - }, - { - "name": "需要配置", - "nameEn": "Requires Configuration", - "projectCount": 163 - }, - { - "name": "TypeScript", - "nameEn": "TypeScript", - "projectCount": 118 - }, - { - "name": "AI代理", - "nameEn": "AI Agents", - "projectCount": 52 - }, - { - "name": "自动化", - "nameEn": "Automation", - "projectCount": 46 - }, - { - "name": "Docker", - "nameEn": "Docker", - "projectCount": 43 - }, - { - "name": "Node.js", - "nameEn": "Node.js", - "projectCount": 40 - }, - { - "name": "JavaScript", - "nameEn": "JavaScript", - "projectCount": 34 - }, - { - "name": "React", - "nameEn": "React", - "projectCount": 34 - }, - { - "name": "开源", - "nameEn": "Open Source", - "projectCount": 33 - }, - { - "name": "PyTorch", - "nameEn": "PyTorch", - "projectCount": 30 - }, - { - "name": "LangChain", - "nameEn": "LangChain", - "projectCount": 29 - }, - { - "name": "多智能体系统", - "nameEn": "Multi-Agent System", - "projectCount": 28 - }, - { - "name": "大语言模型", - "nameEn": null, - "projectCount": 25 - }, - { - "name": "LLM", - "nameEn": "LLM", - "projectCount": 22 - }, - { - "name": "Go", - "nameEn": "Go", - "projectCount": 20 - }, - { - "name": "MCP", - "nameEn": "Model Context Protocol", - "projectCount": 18 - }, - { - "name": "Library", - "nameEn": "Library", - "projectCount": 18 - }, - { - "name": "RAG", - "nameEn": "RAG", - "projectCount": 17 - } - ] -} diff --git a/.sisyphus/sql/tag-cap-100-2026-02-21.sql b/.sisyphus/sql/tag-cap-100-2026-02-21.sql deleted file mode 100644 index 558c387..0000000 --- a/.sisyphus/sql/tag-cap-100-2026-02-21.sql +++ /dev/null @@ -1,211 +0,0 @@ -BEGIN; - -DELETE FROM "project_tags" -WHERE "tagId" NOT IN ( - 'cmk9fpg4h0000kt048l3a2nqu', - 'cmkjjb37v00064j12b84dsrwg', - 'cmkqimx520005jx04vyegybp9', - 'cmks16lb0003ml104acf4zi7e', - 'cmks0yz0h002xl1040w65w47d', - 'cmkqjgpzq0002l104qknt9pho', - 'cmkql448t0002l504m7bkovze', - 'cmkqm1bzv0000jl04c3ks3w7w', - 'cmkoutfvz0005jr04knetubim', - 'cmks0rbk1002jl104uc7yg2xv', - 'cmks3iu8m006wl104ndk8no10', - 'cmkqk1dc5000ojr04jq0exq1z', - 'cmkqimwxl0000jx04l8q0rdzo', - 'cmks0yz1e002yl104nw4apfcq', - 'cmkqjvuo5000ejr04y9ksio48', - 'cmkjjb37v00074j122wpemyas', - 'cmkqnvcbg0001jo04us31pcaj', - 'cmkrzlq020001l104sosii49r', - 'cmks1aoyn003yl104r4zspprz', - 'cmks5ey0j001el204vjk69nwg', - 'cmkshrboj0000la04h6bmugvj', - 'cmkscgc0z0007jv04nwmzbn8c', - 'cmkscxy490012jv04rf4k4h4h', - 'cmkuxtw0m002ojm04pl9qazxs', - 'cmkqklwxm0018jr04zyzlrmlv', - 'cmkqk1dbi000ljr045f2rm18m', - 'cmkrv2jpg0000l204swrx5xh4', - 'cmkqlcukz000dl5044c6z8542', - 'cmkqm1c6j0003jl04clkn9qyb', - 'cmkqnn8hj0006l504j2uedblz', - 'cmkrx8euz0007ky04hrgq5ryg', - 'cmkqklwr20016jr04opezrtpo', - 'cmks003y10015l104xruieqo9', - 'cmks0c9ll0022l10436svssah', - 'cmkqnn8gl0005l5042jdeuuur', - 'cmkrx664z0000ky0481j124cq', - 'cmkqjvuir0008jr048591dzwp', - 'cmkjjb38l000c4j12oucpveph', - 'cmkqnn8mm0007l5045q6z4zb0', - 'cmkrzvdfk000tl104jsq77zig', - 'cmks3d1wy006ll104gltxc3et', - 'cmks1aoxo003xl104gap1drce', - 'cmksczul60015jv04ft4n1cb2', - 'cmks0m380002gl104o6f9cjtj', - 'cmkrx8f0j000aky046uomv3rs', - 'cmkqjs7e50004jr042mrpi3ac', - 'cmkqk4i0d000vjr04ai18b8h3', - 'cmkqk4i0g000wjr04t82lzvaq', - 'cmkrzlq0q0003l10453qhsniu', - 'cmks1ubzb004pl104mvs08pmu', - 'cmks0eqvq0028l10426ogrvkb', - 'cmks13z53003el104re4468xt', - 'cmks05ci1001gl104lus7os2q', - 'cmks1s7p2004il104kpwprttq', - 'cmkqjs7dw0003jr04uy2bguck', - 'cmkqklwxr0019jr048wp0hds8', - 'cmkqjs77s0000jr041cypbjkn', - 'cmkry59tc0001jy04kr21i343', - 'cmk9fpg5f0003kt04fcgbvj7y', - 'cmkrzxk4i0011l1045cubzwwr', - 'cmks13z46003dl1047ba0entr', - 'cmkry59u70003jy049eoxzkc7', - 'cmks2a6r1005il104ormxbqud', - 'cmks112wx0039l104zltcl2hp', - 'cmkrzvdfe000sl104q0gge67f', - 'cmks3w0g0000bl904u4lmk5d5', - 'cmkt8m55q000nla04kgfld9x3', - 'cmkqklwxj0017jr04czubkbbi', - 'cmkshubus0004la04zjuh5nn5', - 'cmkqk4i0d000ujr045d2njmc7', - 'cmks02o6m001al104gi1jztpn', - 'cmks4roac000el204paypouzl', - 'cmkt7yhut0000la040ij92vgk', - 'cmks5rr2e0020l204sn9ydvka', - 'cmkjjb37z00084j121651lnjl', - 'cmksg1wc90061jv04114izr2d', - 'cmkuulu5d000jjc04u7hx67sz', - 'cmkqimx3n0001jx043mvrmarx', - 'cmksf0ik1004ijv047hiusscw', - 'cmktoexz8001wld046f13barg', - 'cmks2gqie005ol104v4d6nvck', - 'cmks0equs0026l104sl4td34d', - 'cmkjjb38j000b4j125a3fg282', - 'cmktkj5to001hl704wtmqjft4', - 'cmkrzxk4a0010l104u7irj9je', - 'cmkto2g8o0019ld04b61pgj4y', - 'cmktk5hid000sl7049a2bo5al', - 'cmkv6j7uq000gk4043x1gfnp1', - 'cmkti8i470000kz04bpsajkzk', - 'cmks47ct90013l904x03rupwb', - 'cmkql9bho0007l50406oflxr0', - 'cmks112qf0034l104gkihwp10', - 'cmks1s7ut004jl104gvlybgfs', - 'cmks4pb54000bl204erpb9bbp', - 'cmksfnrz1005qjv044pnraikr', - 'cmkqimx480003jx04bcqob7pb', - 'cmkqm1c690001jl04ge5elv46', - 'cmks2a6l3005gl1040u4s0k4s', - 'cmkth0k4x0026ju04t0euxxwk', - 'cmkqnj1pt0001l504vmrywqeg' -); - -DELETE FROM "tags" -WHERE "id" NOT IN ( - 'cmk9fpg4h0000kt048l3a2nqu', - 'cmkjjb37v00064j12b84dsrwg', - 'cmkqimx520005jx04vyegybp9', - 'cmks16lb0003ml104acf4zi7e', - 'cmks0yz0h002xl1040w65w47d', - 'cmkqjgpzq0002l104qknt9pho', - 'cmkql448t0002l504m7bkovze', - 'cmkqm1bzv0000jl04c3ks3w7w', - 'cmkoutfvz0005jr04knetubim', - 'cmks0rbk1002jl104uc7yg2xv', - 'cmks3iu8m006wl104ndk8no10', - 'cmkqk1dc5000ojr04jq0exq1z', - 'cmkqimwxl0000jx04l8q0rdzo', - 'cmks0yz1e002yl104nw4apfcq', - 'cmkqjvuo5000ejr04y9ksio48', - 'cmkjjb37v00074j122wpemyas', - 'cmkqnvcbg0001jo04us31pcaj', - 'cmkrzlq020001l104sosii49r', - 'cmks1aoyn003yl104r4zspprz', - 'cmks5ey0j001el204vjk69nwg', - 'cmkshrboj0000la04h6bmugvj', - 'cmkscgc0z0007jv04nwmzbn8c', - 'cmkscxy490012jv04rf4k4h4h', - 'cmkuxtw0m002ojm04pl9qazxs', - 'cmkqklwxm0018jr04zyzlrmlv', - 'cmkqk1dbi000ljr045f2rm18m', - 'cmkrv2jpg0000l204swrx5xh4', - 'cmkqlcukz000dl5044c6z8542', - 'cmkqm1c6j0003jl04clkn9qyb', - 'cmkqnn8hj0006l504j2uedblz', - 'cmkrx8euz0007ky04hrgq5ryg', - 'cmkqklwr20016jr04opezrtpo', - 'cmks003y10015l104xruieqo9', - 'cmks0c9ll0022l10436svssah', - 'cmkqnn8gl0005l5042jdeuuur', - 'cmkrx664z0000ky0481j124cq', - 'cmkqjvuir0008jr048591dzwp', - 'cmkjjb38l000c4j12oucpveph', - 'cmkqnn8mm0007l5045q6z4zb0', - 'cmkrzvdfk000tl104jsq77zig', - 'cmks3d1wy006ll104gltxc3et', - 'cmks1aoxo003xl104gap1drce', - 'cmksczul60015jv04ft4n1cb2', - 'cmks0m380002gl104o6f9cjtj', - 'cmkrx8f0j000aky046uomv3rs', - 'cmkqjs7e50004jr042mrpi3ac', - 'cmkqk4i0d000vjr04ai18b8h3', - 'cmkqk4i0g000wjr04t82lzvaq', - 'cmkrzlq0q0003l10453qhsniu', - 'cmks1ubzb004pl104mvs08pmu', - 'cmks0eqvq0028l10426ogrvkb', - 'cmks13z53003el104re4468xt', - 'cmks05ci1001gl104lus7os2q', - 'cmks1s7p2004il104kpwprttq', - 'cmkqjs7dw0003jr04uy2bguck', - 'cmkqklwxr0019jr048wp0hds8', - 'cmkqjs77s0000jr041cypbjkn', - 'cmkry59tc0001jy04kr21i343', - 'cmk9fpg5f0003kt04fcgbvj7y', - 'cmkrzxk4i0011l1045cubzwwr', - 'cmks13z46003dl1047ba0entr', - 'cmkry59u70003jy049eoxzkc7', - 'cmks2a6r1005il104ormxbqud', - 'cmks112wx0039l104zltcl2hp', - 'cmkrzvdfe000sl104q0gge67f', - 'cmks3w0g0000bl904u4lmk5d5', - 'cmkt8m55q000nla04kgfld9x3', - 'cmkqklwxj0017jr04czubkbbi', - 'cmkshubus0004la04zjuh5nn5', - 'cmkqk4i0d000ujr045d2njmc7', - 'cmks02o6m001al104gi1jztpn', - 'cmks4roac000el204paypouzl', - 'cmkt7yhut0000la040ij92vgk', - 'cmks5rr2e0020l204sn9ydvka', - 'cmkjjb37z00084j121651lnjl', - 'cmksg1wc90061jv04114izr2d', - 'cmkuulu5d000jjc04u7hx67sz', - 'cmkqimx3n0001jx043mvrmarx', - 'cmksf0ik1004ijv047hiusscw', - 'cmktoexz8001wld046f13barg', - 'cmks2gqie005ol104v4d6nvck', - 'cmks0equs0026l104sl4td34d', - 'cmkjjb38j000b4j125a3fg282', - 'cmktkj5to001hl704wtmqjft4', - 'cmkrzxk4a0010l104u7irj9je', - 'cmkto2g8o0019ld04b61pgj4y', - 'cmktk5hid000sl7049a2bo5al', - 'cmkv6j7uq000gk4043x1gfnp1', - 'cmkti8i470000kz04bpsajkzk', - 'cmks47ct90013l904x03rupwb', - 'cmkql9bho0007l50406oflxr0', - 'cmks112qf0034l104gkihwp10', - 'cmks1s7ut004jl104gvlybgfs', - 'cmks4pb54000bl204erpb9bbp', - 'cmksfnrz1005qjv044pnraikr', - 'cmkqimx480003jx04bcqob7pb', - 'cmkqm1c690001jl04ge5elv46', - 'cmks2a6l3005gl1040u4s0k4s', - 'cmkth0k4x0026ju04t0euxxwk', - 'cmkqnj1pt0001l504vmrywqeg' -); - -COMMIT; diff --git a/.sisyphus/sql/tag-cleanup-2026-02-21.sql b/.sisyphus/sql/tag-cleanup-2026-02-21.sql deleted file mode 100644 index 1eca766..0000000 --- a/.sisyphus/sql/tag-cleanup-2026-02-21.sql +++ /dev/null @@ -1,46 +0,0 @@ -BEGIN; - --- Step 1: Merge duplicate tags by high-confidence canonicalized nameEn groups -WITH merge_map(source_id, target_id) AS ( -VALUES - ('cmkoutg0o0006jr04goozjhft', 'cmkqimx520005jx04vyegybp9'), - ('cmkjjb387000a4j12pdf123zn', 'cmks0yz0h002xl1040w65w47d'), - ('cmkun0ao70048jx04atm2lwwq', 'cmkryxmob0003l80414uo5zqz'), - ('cmkrzqdlu000fl104qmy9gu2u', 'cmkqlcukz000dl5044c6z8542'), - ('cmktk3biu000nl704iiohmv4i', 'cmksdlj9o0029jv04x9tdyc11'), - ('cmktpotkp003eld04mv09ckde', 'cmkshubus0004la04zjuh5nn5') -) -INSERT INTO "project_tags" ("projectId", "tagId") -SELECT pt."projectId", mm.target_id -FROM "project_tags" pt -JOIN merge_map mm ON mm.source_id = pt."tagId" -ON CONFLICT ("projectId", "tagId") DO NOTHING; - -WITH merge_map(source_id, target_id) AS ( -VALUES - ('cmkoutg0o0006jr04goozjhft', 'cmkqimx520005jx04vyegybp9'), - ('cmkjjb387000a4j12pdf123zn', 'cmks0yz0h002xl1040w65w47d'), - ('cmkun0ao70048jx04atm2lwwq', 'cmkryxmob0003l80414uo5zqz'), - ('cmkrzqdlu000fl104qmy9gu2u', 'cmkqlcukz000dl5044c6z8542'), - ('cmktk3biu000nl704iiohmv4i', 'cmksdlj9o0029jv04x9tdyc11'), - ('cmktpotkp003eld04mv09ckde', 'cmkshubus0004la04zjuh5nn5') -) -DELETE FROM "tags" t -USING merge_map mm -WHERE t."id" = mm.source_id; - --- Step 2: Fill missing English names for ASCII-only tags -UPDATE "tags" -SET "nameEn" = "name" -WHERE ("nameEn" IS NULL OR "nameEn" = '') - AND "name" ~ '^[[:ascii:]]+$'; - --- Step 3: Delete tags no longer referenced by any project -DELETE FROM "tags" t -WHERE NOT EXISTS ( - SELECT 1 - FROM "project_tags" pt - WHERE pt."tagId" = t."id" -); - -COMMIT; diff --git a/.sisyphus/sql/tag-semantic-prune-final-2026-02-21.sql b/.sisyphus/sql/tag-semantic-prune-final-2026-02-21.sql deleted file mode 100644 index ab11465..0000000 --- a/.sisyphus/sql/tag-semantic-prune-final-2026-02-21.sql +++ /dev/null @@ -1,76 +0,0 @@ -BEGIN; - -WITH noise AS ( - SELECT id - FROM tags - WHERE name IN ( - '开源', - '学术资源', - '复杂部署', - '用户通知', - '需要配置' - ) -), -projects_without_meaningful_tags AS ( - SELECT p.id, p.name - FROM projects p - LEFT JOIN project_tags pt ON pt."projectId" = p.id - LEFT JOIN noise n ON n.id = pt."tagId" - GROUP BY p.id, p.name - HAVING COUNT(pt."tagId") > 0 AND COUNT(pt."tagId") = COUNT(n.id) -), -fallback_tags AS ( - SELECT - (SELECT id FROM tags WHERE name = 'AI研究' LIMIT 1) AS research_id, - (SELECT id FROM tags WHERE name = 'AI 助手' LIMIT 1) AS assistant_id, - (SELECT id FROM tags WHERE name = 'AI代理' LIMIT 1) AS agent_id, - (SELECT id FROM tags WHERE name = '自动化' LIMIT 1) AS automation_id, - (SELECT id FROM tags WHERE name = 'AI开发工具' LIMIT 1) AS tool_id -), -assignments AS ( - SELECT - p.id AS project_id, - CASE - WHEN p.name ~* '(论文|研究|paper|survey)' THEN (SELECT research_id FROM fallback_tags) - WHEN p.name ~* '(助手|assistant)' THEN (SELECT assistant_id FROM fallback_tags) - WHEN p.name ~* '(框架|agent|代理|智能体)' THEN (SELECT agent_id FROM fallback_tags) - WHEN p.name ~* '(通知|alert|quota|配额)' THEN (SELECT automation_id FROM fallback_tags) - ELSE (SELECT tool_id FROM fallback_tags) - END AS tag_id - FROM projects_without_meaningful_tags p -) -INSERT INTO project_tags ("projectId", "tagId") -SELECT project_id, tag_id -FROM assignments -WHERE tag_id IS NOT NULL -ON CONFLICT ("projectId", "tagId") DO NOTHING; - -WITH noise AS ( - SELECT id - FROM tags - WHERE name IN ( - '开源', - '学术资源', - '复杂部署', - '用户通知', - '需要配置' - ) -) -DELETE FROM project_tags -WHERE "tagId" IN (SELECT id FROM noise); - -WITH noise AS ( - SELECT id - FROM tags - WHERE name IN ( - '开源', - '学术资源', - '复杂部署', - '用户通知', - '需要配置' - ) -) -DELETE FROM tags -WHERE id IN (SELECT id FROM noise); - -COMMIT; diff --git a/.sisyphus/sql/tag-semantic-prune-safe-2026-02-21.sql b/.sisyphus/sql/tag-semantic-prune-safe-2026-02-21.sql deleted file mode 100644 index 5e94841..0000000 --- a/.sisyphus/sql/tag-semantic-prune-safe-2026-02-21.sql +++ /dev/null @@ -1,43 +0,0 @@ -BEGIN; - -WITH target_tags AS ( - SELECT id FROM tags - WHERE name IN ( - '自托管', - '跨平台', - '企业级', - '需要基础', - 'AI研究资源', - '多语言支持', - '教程', - 'Apache-2.0', - '学术研究', - 'MIT许可', - '云端服务', - '低学习成本' - ) -) -DELETE FROM project_tags -WHERE "tagId" IN (SELECT id FROM target_tags); - -WITH target_tags AS ( - SELECT id FROM tags - WHERE name IN ( - '自托管', - '跨平台', - '企业级', - '需要基础', - 'AI研究资源', - '多语言支持', - '教程', - 'Apache-2.0', - '学术研究', - 'MIT许可', - '云端服务', - '低学习成本' - ) -) -DELETE FROM tags -WHERE id IN (SELECT id FROM target_tags); - -COMMIT; diff --git a/docs/AGENTS.md b/docs/AGENTS.md index f04ef0a..1fe4f28 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -19,7 +19,15 @@ docs/ ├── n8n/ # n8n workflow documentation │ ├── historical-workflow-design.md │ ├── incremental-workflow-design.md -│ └── tag-janitor-workflow.md +│ ├── tag-janitor-workflow.json +│ ├── project-tag-reset-workflow.json +│ ├── project-tag-reset-workflow.md +│ ├── github-link-janitor-workflow.json +│ ├── github-link-janitor-workflow.md +│ ├── github-link-repair-workflow.json +│ ├── github-link-repair-workflow.md +│ ├── github-stars-refresh-workflow.json +│ └── github-stars-refresh-workflow.md └── plans/ # Design & implementation plans ├── 2026-01-25-ai-search-system-design.md ├── 2026-01-25-ai-search-implementation.md @@ -37,6 +45,7 @@ docs/ | `discovery-workflow.md` | Project discovery architecture | | `plans/*.md` | Feature design & implementation specs | | `n8n/*.md` | n8n workflow design docs | +| `n8n/project-tag-reset-workflow.json` | Project tag reset workflow (multi-AI category classification) | ## FOR AI AGENTS diff --git a/docs/n8n/project-tag-reset-workflow.json b/docs/n8n/project-tag-reset-workflow.json new file mode 100644 index 0000000..84effa6 --- /dev/null +++ b/docs/n8n/project-tag-reset-workflow.json @@ -0,0 +1,246 @@ +{ + "name": "Project Tag Reset - Multi AI Classifier", + "nodes": [ + { + "parameters": {}, + "id": "manual-trigger", + "name": "Manual Trigger", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [0, 0] + }, + { + "parameters": { + "url": "={{$env.SITE_BASE_URL}}/api/tags", + "options": {} + }, + "id": "fetch-tags", + "name": "Fetch Tags", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [220, 0] + }, + { + "parameters": { + "url": "={{$env.SITE_BASE_URL}}/api/projects?limit=100&page=1&sort=latest", + "options": {} + }, + "id": "fetch-projects", + "name": "Fetch Projects", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [440, 0] + }, + { + "parameters": { + "jsCode": "const tagsResponse = $('Fetch Tags').first().json;\nconst projectsResponse = $input.first().json;\n\nif (!tagsResponse?.success || !Array.isArray(tagsResponse?.tags)) {\n throw new Error('Failed to fetch tags from /api/tags');\n}\n\nif (!Array.isArray(projectsResponse?.projects)) {\n throw new Error('Failed to fetch projects from /api/projects');\n}\n\nif ((projectsResponse?.pagination?.total || 0) > projectsResponse.projects.length) {\n throw new Error(`Only fetched ${projectsResponse.projects.length} projects but total is ${projectsResponse.pagination.total}. Increase API limit or add pagination in workflow.`);\n}\n\nconst allowedCategories = [\n 'FIXED_PROJECT_TYPE',\n 'TECH_STACK',\n 'AI_PARADIGM',\n 'PRODUCT_FORM',\n 'DOMAIN_SCENARIO',\n];\n\nconst tagPools = Object.fromEntries(\n allowedCategories.map((category) => [\n category,\n tagsResponse.tags\n .filter((tag) => tag.category === category)\n .map((tag) => ({\n id: tag.id,\n slug: tag.slug,\n name: tag.name,\n nameEn: tag.nameEn || '',\n })),\n ])\n);\n\nreturn projectsResponse.projects.map((project) => ({\n json: {\n projectSlug: project.slug,\n projectName: project.name,\n projectNameEn: project.nameEn || '',\n projectDescription: project.description,\n projectDescriptionEn: project.descriptionEn || '',\n currentTags: Array.isArray(project.tags)\n ? project.tags.map((tag) => ({\n slug: tag.slug,\n name: tag.name,\n nameEn: tag.nameEn || '',\n }))\n : [],\n tagPools,\n },\n}));" + }, + "id": "prepare-classification-items", + "name": "Prepare Classification Items", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [660, 0] + }, + { + "parameters": { + "model": "gpt-4o-mini", + "messages": { + "values": [ + { + "role": "system", + "content": "You classify projects into FIXED_PROJECT_TYPE. Return STRICT JSON only." + }, + { + "role": "user", + "content": "=Project:\\n- slug: {{$json.projectSlug}}\\n- name: {{$json.projectName}}\\n- nameEn: {{$json.projectNameEn}}\\n- description: {{$json.projectDescription}}\\n- descriptionEn: {{$json.projectDescriptionEn}}\\n- currentTags: {{JSON.stringify($json.currentTags)}}\\n\\nCandidate pool (FIXED_PROJECT_TYPE):\\n{{JSON.stringify($json.tagPools.FIXED_PROJECT_TYPE)}}\\n\\nTask:\\n- Choose exactly ONE best slug from candidate pool.\\n\\nOutput strict JSON only:\\n{\\n \"selected\": [\"one-slug\"]\\n}" + } + ] + }, + "options": { + "temperature": 0.1 + } + }, + "id": "ai-fixed-project-type", + "name": "AI Fixed Project Type", + "type": "@n8n/n8n-nodes-langchain.openAi", + "typeVersion": 1.8, + "position": [880, 0] + }, + { + "parameters": { + "model": "gpt-4o-mini", + "messages": { + "values": [ + { + "role": "system", + "content": "You classify DOMAIN_SCENARIO tags. Return STRICT JSON only." + }, + { + "role": "user", + "content": "=Project:\\n- slug: {{$json.projectSlug}}\\n- name: {{$json.projectName}}\\n- description: {{$json.projectDescription}}\\n- currentTags: {{JSON.stringify($json.currentTags)}}\\n\\nCandidate pool (DOMAIN_SCENARIO):\\n{{JSON.stringify($json.tagPools.DOMAIN_SCENARIO)}}\\n\\nTask:\\n- Choose 1 to 3 slugs from candidate pool.\\n- Keep only the most representative domains for this project.\\n\\nOutput strict JSON only:\\n{\\n \"selected\": [\"slug1\", \"slug2\"]\\n}" + } + ] + }, + "options": { + "temperature": 0.1 + } + }, + "id": "ai-domain-scenario", + "name": "AI Domain Scenario", + "type": "@n8n/n8n-nodes-langchain.openAi", + "typeVersion": 1.8, + "position": [1100, 0] + }, + { + "parameters": { + "model": "gpt-4o-mini", + "messages": { + "values": [ + { + "role": "system", + "content": "You classify PRODUCT_FORM tags. Return STRICT JSON only." + }, + { + "role": "user", + "content": "=Project:\\n- slug: {{$json.projectSlug}}\\n- name: {{$json.projectName}}\\n- description: {{$json.projectDescription}}\\n- currentTags: {{JSON.stringify($json.currentTags)}}\\n\\nCandidate pool (PRODUCT_FORM):\\n{{JSON.stringify($json.tagPools.PRODUCT_FORM)}}\\n\\nTask:\\n- Choose 1 to 3 slugs from candidate pool that best represent product form.\\n\\nOutput strict JSON only:\\n{\\n \"selected\": [\"slug1\", \"slug2\"]\\n}" + } + ] + }, + "options": { + "temperature": 0.1 + } + }, + "id": "ai-product-form", + "name": "AI Product Form", + "type": "@n8n/n8n-nodes-langchain.openAi", + "typeVersion": 1.8, + "position": [1320, 0] + }, + { + "parameters": { + "model": "gpt-4o-mini", + "messages": { + "values": [ + { + "role": "system", + "content": "You classify TECH_STACK tags. Return STRICT JSON only." + }, + { + "role": "user", + "content": "=Project:\\n- slug: {{$json.projectSlug}}\\n- name: {{$json.projectName}}\\n- description: {{$json.projectDescription}}\\n- currentTags: {{JSON.stringify($json.currentTags)}}\\n\\nCandidate pool (TECH_STACK):\\n{{JSON.stringify($json.tagPools.TECH_STACK)}}\\n\\nTask:\\n- Choose 1 to 8 slugs from candidate pool for the main technologies used by this project.\\n\\nOutput strict JSON only:\\n{\\n \"selected\": [\"slug1\", \"slug2\"]\\n}" + } + ] + }, + "options": { + "temperature": 0.1 + } + }, + "id": "ai-tech-stack", + "name": "AI Tech Stack", + "type": "@n8n/n8n-nodes-langchain.openAi", + "typeVersion": 1.8, + "position": [1540, 0] + }, + { + "parameters": { + "model": "gpt-4o-mini", + "messages": { + "values": [ + { + "role": "system", + "content": "You classify AI_PARADIGM tags. Return STRICT JSON only." + }, + { + "role": "user", + "content": "=Project:\\n- slug: {{$json.projectSlug}}\\n- name: {{$json.projectName}}\\n- description: {{$json.projectDescription}}\\n- currentTags: {{JSON.stringify($json.currentTags)}}\\n\\nCandidate pool (AI_PARADIGM):\\n{{JSON.stringify($json.tagPools.AI_PARADIGM)}}\\n\\nTask:\\n- Choose 0 to 5 slugs from candidate pool for AI paradigm.\\n\\nOutput strict JSON only:\\n{\\n \"selected\": [\"slug1\", \"slug2\"]\\n}" + } + ] + }, + "options": { + "temperature": 0.1 + } + }, + "id": "ai-ai-paradigm", + "name": "AI AI Paradigm", + "type": "@n8n/n8n-nodes-langchain.openAi", + "typeVersion": 1.8, + "position": [1760, 0] + }, + { + "parameters": { + "jsCode": "function parseNodeJson(nodeJson) {\n const content = nodeJson?.message?.content || nodeJson?.text || nodeJson?.response || nodeJson;\n\n if (typeof content === 'string') {\n const trimmed = content.trim();\n try {\n return JSON.parse(trimmed);\n } catch {\n const jsonMatch = trimmed.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n return JSON.parse(jsonMatch[0]);\n }\n return { selected: [] };\n }\n }\n\n if (typeof content === 'object' && content !== null) {\n return content;\n }\n\n return { selected: [] };\n}\n\nfunction normalizeSelected(parsed, pool, minCount, maxCount) {\n const poolSlugs = pool.map((tag) => String(tag.slug).toLowerCase());\n const allowed = new Set(poolSlugs);\n const selectedRaw = Array.isArray(parsed?.selected) ? parsed.selected : [];\n const selected = [];\n\n for (const slug of selectedRaw) {\n const normalizedSlug = String(slug || '').trim().toLowerCase();\n if (!normalizedSlug || !allowed.has(normalizedSlug) || selected.includes(normalizedSlug)) {\n continue;\n }\n selected.push(normalizedSlug);\n if (selected.length >= maxCount) {\n break;\n }\n }\n\n if (selected.length < minCount) {\n return poolSlugs.slice(0, minCount);\n }\n return selected;\n}\n\nconst preparedItems = $items('Prepare Classification Items', 0);\nconst fixedItems = $items('AI Fixed Project Type', 0);\nconst domainItems = $items('AI Domain Scenario', 0);\nconst productItems = $items('AI Product Form', 0);\nconst techItems = $items('AI Tech Stack', 0);\nconst paradigmItems = $items('AI AI Paradigm', 0);\n\nif (\n preparedItems.length !== fixedItems.length ||\n preparedItems.length !== domainItems.length ||\n preparedItems.length !== productItems.length ||\n preparedItems.length !== techItems.length ||\n preparedItems.length !== paradigmItems.length\n) {\n throw new Error('AI node output item count mismatch');\n}\n\nconst output = [];\nfor (let i = 0; i < preparedItems.length; i++) {\n const base = preparedItems[i].json;\n const pools = base.tagPools;\n\n const fixed = normalizeSelected(\n parseNodeJson(fixedItems[i].json),\n pools.FIXED_PROJECT_TYPE,\n 1,\n 1\n );\n const domains = normalizeSelected(\n parseNodeJson(domainItems[i].json),\n pools.DOMAIN_SCENARIO,\n 1,\n 3\n );\n const productForms = normalizeSelected(\n parseNodeJson(productItems[i].json),\n pools.PRODUCT_FORM,\n 1,\n 3\n );\n const techStack = normalizeSelected(\n parseNodeJson(techItems[i].json),\n pools.TECH_STACK,\n 1,\n 8\n );\n const paradigms = normalizeSelected(\n parseNodeJson(paradigmItems[i].json),\n pools.AI_PARADIGM,\n 0,\n 5\n );\n\n output.push({\n json: {\n projectSlug: base.projectSlug,\n selectedTagSlugsByCategory: {\n FIXED_PROJECT_TYPE: fixed,\n TECH_STACK: techStack,\n AI_PARADIGM: paradigms,\n PRODUCT_FORM: productForms,\n DOMAIN_SCENARIO: domains,\n },\n },\n });\n}\n\nreturn output;" + }, + "id": "build-reset-payload", + "name": "Build Reset Payload", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1980, 0] + }, + { + "parameters": { + "method": "POST", + "url": "={{$env.SITE_BASE_URL}}/api/tags/reset-projects", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"apiKey\": \"{{$env.WEBHOOK_API_KEY}}\",\n \"dryRun\": false,\n \"replaceAllCategories\": true,\n \"projects\": [\n {\n \"projectSlug\": \"{{$json.projectSlug}}\",\n \"selectedTagSlugsByCategory\": {{JSON.stringify($json.selectedTagSlugsByCategory)}}\n }\n ]\n}", + "options": {} + }, + "id": "reset-project-tags", + "name": "Reset Project Tags", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [2200, 0] + }, + { + "parameters": { + "jsCode": "const results = $input.all().map((item) => item.json);\nconst summary = {\n totalRequests: results.length,\n successRequests: results.filter((r) => r.success === true).length,\n failedRequests: results.filter((r) => r.success !== true).length,\n timestamp: new Date().toISOString(),\n};\n\nreturn [{ json: { summary, results } }];" + }, + "id": "summarize-results", + "name": "Summarize Results", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [2420, 0] + } + ], + "connections": { + "Manual Trigger": { + "main": [[{ "node": "Fetch Tags", "type": "main", "index": 0 }]] + }, + "Fetch Tags": { + "main": [[{ "node": "Fetch Projects", "type": "main", "index": 0 }]] + }, + "Fetch Projects": { + "main": [[{ "node": "Prepare Classification Items", "type": "main", "index": 0 }]] + }, + "Prepare Classification Items": { + "main": [[{ "node": "AI Fixed Project Type", "type": "main", "index": 0 }]] + }, + "AI Fixed Project Type": { + "main": [[{ "node": "AI Domain Scenario", "type": "main", "index": 0 }]] + }, + "AI Domain Scenario": { + "main": [[{ "node": "AI Product Form", "type": "main", "index": 0 }]] + }, + "AI Product Form": { + "main": [[{ "node": "AI Tech Stack", "type": "main", "index": 0 }]] + }, + "AI Tech Stack": { + "main": [[{ "node": "AI AI Paradigm", "type": "main", "index": 0 }]] + }, + "AI AI Paradigm": { + "main": [[{ "node": "Build Reset Payload", "type": "main", "index": 0 }]] + }, + "Build Reset Payload": { + "main": [[{ "node": "Reset Project Tags", "type": "main", "index": 0 }]] + }, + "Reset Project Tags": { + "main": [[{ "node": "Summarize Results", "type": "main", "index": 0 }]] + } + }, + "settings": { + "executionOrder": "v1" + }, + "meta": { + "templateCredsSetupCompleted": true + } +} diff --git a/docs/n8n/project-tag-reset-workflow.md b/docs/n8n/project-tag-reset-workflow.md new file mode 100644 index 0000000..83417c2 --- /dev/null +++ b/docs/n8n/project-tag-reset-workflow.md @@ -0,0 +1,40 @@ +# Project Tag Reset Workflow + +## Goal + +Reset every project's tags according to the new taxonomy, using **one dedicated AI node per tag category**: + +- `FIXED_PROJECT_TYPE` +- `DOMAIN_SCENARIO` +- `PRODUCT_FORM` +- `TECH_STACK` +- `AI_PARADIGM` + +Each AI node must select only from its own category pool. + +## Files + +- Workflow JSON: `docs/n8n/project-tag-reset-workflow.json` +- API endpoint used by workflow: `POST /api/tags/reset-projects` + +## Required Environment Variables in n8n + +- `SITE_BASE_URL`: e.g. `https://your-site.com` +- `WEBHOOK_API_KEY`: same key configured on Next.js server + +## Execution Flow + +1. `Manual Trigger` +2. `HTTP GET /api/tags` to load full tag pool +3. `HTTP GET /api/projects?limit=100&page=1` to load projects +4. `Code` prepares per-project classification items and category pools +5. Five independent AI nodes classify by category +6. `Code` normalizes/parses AI outputs into `selectedTagSlugsByCategory` +7. `HTTP POST /api/tags/reset-projects` updates tags per project +8. `Code` summarizes success/failure counts + +## Notes + +- Current workflow fetches up to 100 projects in one run. If total > 100, add pagination or increase API limit strategy. +- Endpoint supports `dryRun`. You can set `"dryRun": true` first in the request node for safe validation. +- Endpoint performs category validation and rejects cross-category slug usage. diff --git a/src/app/api/tags/reset-projects/route.test.ts b/src/app/api/tags/reset-projects/route.test.ts new file mode 100644 index 0000000..226431f --- /dev/null +++ b/src/app/api/tags/reset-projects/route.test.ts @@ -0,0 +1,166 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { NextRequest } from "next/server"; +import { POST } from "./route"; + +const { revalidatePathMock, transactionMock, txMock, projectFindManyMock, tagFindManyMock } = + vi.hoisted(() => { + const tx = { + projectTag: { + deleteMany: vi.fn(), + createMany: vi.fn(), + }, + }; + + return { + revalidatePathMock: vi.fn(), + transactionMock: vi.fn(async (callback: (tx: typeof tx) => unknown) => callback(tx)), + txMock: tx, + projectFindManyMock: vi.fn(), + tagFindManyMock: vi.fn(), + }; + }); + +vi.mock("@/lib/prisma", () => ({ + prisma: { + project: { + findMany: projectFindManyMock, + }, + tag: { + findMany: tagFindManyMock, + }, + $transaction: transactionMock, + }, +})); + +vi.mock("next/cache", () => ({ + revalidatePath: revalidatePathMock, +})); + +function buildRequest(body: unknown): NextRequest { + return new NextRequest("http://localhost:3000/api/tags/reset-projects", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); +} + +function buildValidPayload(apiKey: string) { + return { + apiKey, + projects: [ + { + projectSlug: "project-one", + selectedTagSlugsByCategory: { + FIXED_PROJECT_TYPE: ["agent-tooling"], + TECH_STACK: ["typescript"], + AI_PARADIGM: ["ai-agents"], + PRODUCT_FORM: ["web-application"], + DOMAIN_SCENARIO: ["code-dev"], + }, + }, + ], + }; +} + +describe("POST /api/tags/reset-projects", () => { + const validApiKey = "k".repeat(32); + + beforeEach(() => { + process.env.WEBHOOK_API_KEY = validApiKey; + transactionMock.mockClear(); + revalidatePathMock.mockClear(); + txMock.projectTag.deleteMany.mockReset(); + txMock.projectTag.createMany.mockReset(); + projectFindManyMock.mockReset(); + tagFindManyMock.mockReset(); + }); + + it("returns 401 for wrong API key", async () => { + const response = await POST(buildRequest(buildValidPayload("a".repeat(32)))); + const json = await response.json(); + + expect(response.status).toBe(401); + expect(json.success).toBe(false); + expect(json.error).toBe("Unauthorized"); + }); + + it("returns 400 when FIXED_PROJECT_TYPE is missing", async () => { + const payload = buildValidPayload(validApiKey); + payload.projects[0].selectedTagSlugsByCategory.FIXED_PROJECT_TYPE = []; + + const response = await POST(buildRequest(payload)); + const json = await response.json(); + + expect(response.status).toBe(400); + expect(json.success).toBe(false); + expect(json.error).toBe("Validation error"); + expect(transactionMock).not.toHaveBeenCalled(); + }); + + it("returns partial failure when project is missing", async () => { + projectFindManyMock.mockResolvedValue([]); + tagFindManyMock.mockResolvedValue([ + { id: "t1", slug: "agent-tooling", category: "FIXED_PROJECT_TYPE" }, + { id: "t2", slug: "typescript", category: "TECH_STACK" }, + { id: "t3", slug: "ai-agents", category: "AI_PARADIGM" }, + { id: "t4", slug: "web-application", category: "PRODUCT_FORM" }, + { id: "t5", slug: "code-dev", category: "DOMAIN_SCENARIO" }, + ]); + + const response = await POST(buildRequest(buildValidPayload(validApiKey))); + const json = await response.json(); + + expect(response.status).toBe(200); + expect(json.success).toBe(false); + expect(json.result.failedCount).toBe(1); + expect(transactionMock).not.toHaveBeenCalled(); + }); + + it("updates project tags and revalidates pages", async () => { + projectFindManyMock.mockResolvedValue([ + { + id: "p1", + slug: "project-one", + tags: [ + { + tag: { + id: "old-tech", + slug: "javascript", + category: "TECH_STACK", + }, + }, + { + tag: { + id: "old-domain", + slug: "automation-workflow", + category: "DOMAIN_SCENARIO", + }, + }, + ], + }, + ]); + tagFindManyMock.mockResolvedValue([ + { id: "t1", slug: "agent-tooling", category: "FIXED_PROJECT_TYPE" }, + { id: "t2", slug: "typescript", category: "TECH_STACK" }, + { id: "t3", slug: "ai-agents", category: "AI_PARADIGM" }, + { id: "t4", slug: "web-application", category: "PRODUCT_FORM" }, + { id: "t5", slug: "code-dev", category: "DOMAIN_SCENARIO" }, + ]); + + const response = await POST(buildRequest(buildValidPayload(validApiKey))); + const json = await response.json(); + + expect(response.status).toBe(200); + expect(json.success).toBe(true); + expect(json.result.updatedCount).toBe(1); + expect(transactionMock).toHaveBeenCalledTimes(1); + expect(txMock.projectTag.deleteMany).toHaveBeenCalledTimes(1); + expect(txMock.projectTag.createMany).toHaveBeenCalledTimes(1); + expect(revalidatePathMock).toHaveBeenCalledWith("/zh/projects", "page"); + expect(revalidatePathMock).toHaveBeenCalledWith("/en/projects", "page"); + expect(revalidatePathMock).toHaveBeenCalledWith("/zh/projects/project-one", "page"); + expect(revalidatePathMock).toHaveBeenCalledWith("/en/projects/project-one", "page"); + }); +}); diff --git a/src/app/api/tags/reset-projects/route.ts b/src/app/api/tags/reset-projects/route.ts new file mode 100644 index 0000000..8861e5e --- /dev/null +++ b/src/app/api/tags/reset-projects/route.ts @@ -0,0 +1,293 @@ +import { NextRequest, NextResponse } from 'next/server' +import crypto from 'crypto' +import { revalidatePath } from 'next/cache' +import type { TagCategory } from '@prisma/client' +import { prisma } from '@/lib/prisma' +import { + ProjectTagResetRequestSchema, + type ProjectTagResetItem, + type ResettableTagCategory, +} from '@/lib/validations' + +type ResetResultItem = { + projectSlug: string + status: 'updated' | 'dry-run' | 'failed' + selectedTagCount: number + addedCount: number + removedCount: number + details: string[] +} + +const DEFAULT_RESET_CATEGORIES: ResettableTagCategory[] = [ + 'FIXED_PROJECT_TYPE', + 'TECH_STACK', + 'AI_PARADIGM', + 'PRODUCT_FORM', + 'DOMAIN_SCENARIO', +] + +function isApiKeyValid(providedApiKey: string, expectedApiKey?: string): boolean { + if (!expectedApiKey) { + return false + } + const providedBuf = Buffer.from(providedApiKey) + const expectedBuf = Buffer.from(expectedApiKey) + return ( + providedBuf.length === expectedBuf.length && + crypto.timingSafeEqual(providedBuf, expectedBuf) + ) +} + +function normalizeSlug(slug: string): string { + return slug.trim().toLowerCase() +} + +function collectSelectedTagSlugs( + item: ProjectTagResetItem, + categories: ResettableTagCategory[] +): string[] { + const selectedTagSlugSet = new Set() + + for (const category of categories) { + const categorySlugs = item.selectedTagSlugsByCategory[category] || [] + for (const slug of categorySlugs) { + selectedTagSlugSet.add(normalizeSlug(slug)) + } + } + + return [...selectedTagSlugSet] +} + +function collectValidationErrorsForProjectItem(params: { + item: ProjectTagResetItem + categories: ResettableTagCategory[] + existingTagBySlug: Map +}): string[] { + const { item, categories, existingTagBySlug } = params + const errors: string[] = [] + + for (const category of categories) { + for (const rawSlug of item.selectedTagSlugsByCategory[category] || []) { + const normalizedSlug = normalizeSlug(rawSlug) + const existingTag = existingTagBySlug.get(normalizedSlug) + + if (!existingTag) { + errors.push(`Unknown tag slug "${rawSlug}" in category ${category}`) + continue + } + if (existingTag.category !== category) { + errors.push( + `Tag slug "${rawSlug}" belongs to ${existingTag.category}, expected ${category}` + ) + } + } + } + + return errors +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const validation = ProjectTagResetRequestSchema.safeParse(body) + + if (!validation.success) { + return NextResponse.json( + { + success: false, + error: 'Validation error', + details: validation.error.errors.map((issue) => issue.message), + }, + { status: 400 } + ) + } + + const { + apiKey, + dryRun, + replaceAllCategories, + projects, + categories: requestedCategories, + } = validation.data + + if (!isApiKeyValid(apiKey, process.env.WEBHOOK_API_KEY)) { + return NextResponse.json( + { + success: false, + error: 'Unauthorized', + details: ['Invalid or missing API Key'], + }, + { status: 401 } + ) + } + + const categories = requestedCategories.length > 0 + ? requestedCategories + : DEFAULT_RESET_CATEGORIES + + const normalizedProjectSlugs = projects.map((item) => normalizeSlug(item.projectSlug)) + const normalizedSelectedTagSlugs = [ + ...new Set(projects.flatMap((item) => collectSelectedTagSlugs(item, categories))), + ] + + const [existingProjects, existingTags] = await Promise.all([ + prisma.project.findMany({ + where: { slug: { in: normalizedProjectSlugs } }, + select: { + id: true, + slug: true, + tags: { + include: { + tag: { + select: { + id: true, + slug: true, + category: true, + }, + }, + }, + }, + }, + }), + prisma.tag.findMany({ + where: { slug: { in: normalizedSelectedTagSlugs } }, + select: { + id: true, + slug: true, + category: true, + }, + }), + ]) + + const projectBySlug = new Map(existingProjects.map((project) => [project.slug, project])) + const existingTagBySlug = new Map(existingTags.map((tag) => [tag.slug, tag])) + + const results: ResetResultItem[] = [] + const updatedProjectSlugs: string[] = [] + + for (const item of projects) { + const projectSlug = normalizeSlug(item.projectSlug) + const project = projectBySlug.get(projectSlug) + + if (!project) { + results.push({ + projectSlug, + status: 'failed', + selectedTagCount: 0, + addedCount: 0, + removedCount: 0, + details: [`Project with slug "${projectSlug}" not found`], + }) + continue + } + + const validationErrors = collectValidationErrorsForProjectItem({ + item, + categories, + existingTagBySlug, + }) + + if (validationErrors.length > 0) { + results.push({ + projectSlug, + status: 'failed', + selectedTagCount: 0, + addedCount: 0, + removedCount: 0, + details: validationErrors, + }) + continue + } + + const selectedTagIds = collectSelectedTagSlugs(item, categories) + .map((slug) => existingTagBySlug.get(slug)?.id) + .filter((id): id is string => Boolean(id)) + + const previousCategoryTagIds = new Set( + project.tags + .filter((projectTag) => categories.includes(projectTag.tag.category as ResettableTagCategory)) + .map((projectTag) => projectTag.tag.id) + ) + + const nextTagIdSet = new Set(selectedTagIds) + const removedCount = replaceAllCategories + ? [...previousCategoryTagIds].filter((tagId) => !nextTagIdSet.has(tagId)).length + : 0 + const addedCount = [...nextTagIdSet].filter((tagId) => !previousCategoryTagIds.has(tagId)).length + + if (!dryRun) { + await prisma.$transaction(async (tx) => { + if (replaceAllCategories) { + await tx.projectTag.deleteMany({ + where: { + projectId: project.id, + tag: { + category: { + in: categories as TagCategory[], + }, + }, + }, + }) + } + + if (selectedTagIds.length > 0) { + await tx.projectTag.createMany({ + data: selectedTagIds.map((tagId) => ({ + projectId: project.id, + tagId, + })), + skipDuplicates: true, + }) + } + }) + updatedProjectSlugs.push(projectSlug) + } + + results.push({ + projectSlug, + status: dryRun ? 'dry-run' : 'updated', + selectedTagCount: selectedTagIds.length, + addedCount, + removedCount, + details: [], + }) + } + + const updatedCount = results.filter((item) => item.status === 'updated').length + const dryRunCount = results.filter((item) => item.status === 'dry-run').length + const failedCount = results.filter((item) => item.status === 'failed').length + + if (!dryRun && updatedProjectSlugs.length > 0) { + revalidatePath('/zh/projects', 'page') + revalidatePath('/en/projects', 'page') + for (const projectSlug of updatedProjectSlugs) { + revalidatePath(`/zh/projects/${projectSlug}`, 'page') + revalidatePath(`/en/projects/${projectSlug}`, 'page') + } + } + + return NextResponse.json({ + success: failedCount === 0, + result: { + dryRun, + replaceAllCategories, + categories, + total: projects.length, + updatedCount, + dryRunCount, + failedCount, + results, + }, + }) + } catch (error) { + console.error('[POST /api/tags/reset-projects] Error:', error) + return NextResponse.json( + { + success: false, + error: 'Internal server error', + details: [error instanceof Error ? error.message : 'Unknown error'], + }, + { status: 500 } + ) + } +} diff --git a/src/lib/validations.ts b/src/lib/validations.ts index e52d557..c2b4a23 100644 --- a/src/lib/validations.ts +++ b/src/lib/validations.ts @@ -260,6 +260,88 @@ export const TagMaintenanceRequestSchema = z.object({ }); }); +// ================================ +// Project Tag Reset API Schemas +// ================================ + +const ResettableTagCategorySchema = z.enum([ + "FIXED_PROJECT_TYPE", + "TECH_STACK", + "AI_PARADIGM", + "PRODUCT_FORM", + "DOMAIN_SCENARIO", +]); + +const ProjectTagSelectionByCategorySchema = z.object({ + FIXED_PROJECT_TYPE: z.array(z.string().trim().min(1)).min(1).max(1), + TECH_STACK: z.array(z.string().trim().min(1)).max(20).default([]), + AI_PARADIGM: z.array(z.string().trim().min(1)).max(20).default([]), + PRODUCT_FORM: z.array(z.string().trim().min(1)).max(20).default([]), + DOMAIN_SCENARIO: z.array(z.string().trim().min(1)).max(20).default([]), +}); + +export const ProjectTagResetItemSchema = z.object({ + projectSlug: z.string().trim().min(1).max(200), + selectedTagSlugsByCategory: ProjectTagSelectionByCategorySchema, +}); + +export const ProjectTagResetRequestSchema = z.object({ + apiKey: z.string().min(32, "Invalid API key format"), + dryRun: z.boolean().default(false), + replaceAllCategories: z.boolean().default(true), + categories: z.array(ResettableTagCategorySchema).min(1).default([ + "FIXED_PROJECT_TYPE", + "TECH_STACK", + "AI_PARADIGM", + "PRODUCT_FORM", + "DOMAIN_SCENARIO", + ]), + projects: z.array(ProjectTagResetItemSchema).min(1).max(100), +}).superRefine((data, ctx) => { + const seenProjectSlugs = new Set(); + + data.projects.forEach((project, projectIndex) => { + const normalizedSlug = project.projectSlug.trim().toLowerCase(); + if (seenProjectSlugs.has(normalizedSlug)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["projects", projectIndex, "projectSlug"], + message: `Duplicate projectSlug: ${project.projectSlug}`, + }); + return; + } + seenProjectSlugs.add(normalizedSlug); + + const categories = Object.keys(project.selectedTagSlugsByCategory) as Array< + keyof z.infer + >; + + categories.forEach((category) => { + const selectedSlugs = project.selectedTagSlugsByCategory[category]; + const seenTagSlugs = new Set(); + + selectedSlugs.forEach((slug, slugIndex) => { + const normalizedTagSlug = slug.trim().toLowerCase(); + if (seenTagSlugs.has(normalizedTagSlug)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [ + "projects", + projectIndex, + "selectedTagSlugsByCategory", + category, + slugIndex, + ], + message: `Duplicate tag slug "${slug}" in category ${category}`, + }); + return; + } + seenTagSlugs.add(normalizedTagSlug); + }); + }); + }); +}); + // ================================ // Types // ================================ @@ -268,3 +350,7 @@ export type TagUpdate = z.infer; export type MergeTarget = z.infer; export type TagMerge = z.infer; export type TagMaintenanceRequest = z.infer; +export type ResettableTagCategory = z.infer; +export type ProjectTagSelectionByCategory = z.infer; +export type ProjectTagResetItem = z.infer; +export type ProjectTagResetRequest = z.infer;