feat: 新增项目标签重置接口与n8n流程
This commit is contained in:
@@ -9,6 +9,7 @@ node_modules
|
||||
|
||||
# Next.js
|
||||
/.next/
|
||||
/.next_backup_*/
|
||||
/out/
|
||||
|
||||
# Production
|
||||
@@ -61,6 +62,8 @@ tmp/
|
||||
temp/
|
||||
.cache/
|
||||
.trending-workspace/
|
||||
.omc/
|
||||
.sisyphus/
|
||||
|
||||
# Package manager lock files (optional - uncomment if needed)
|
||||
# package-lock.json
|
||||
|
||||
@@ -1,551 +0,0 @@
|
||||
# AI 项目导航网站 - 下一阶段功能实施计划
|
||||
|
||||
**创建日期**: 2026-02-20
|
||||
**计划版本**: 1.0
|
||||
**预计工期**: 6-8 周
|
||||
|
||||
---
|
||||
|
||||
## 需求摘要
|
||||
|
||||
本计划涵盖两个并行推进的方向:
|
||||
|
||||
### 方向 A - 视觉与交互体验
|
||||
提升用户体验的视觉优化,包括深浅主题切换、移动端适配、无障碍访问和布局优化。
|
||||
|
||||
### 方向 B - 质量控制体系
|
||||
建立项目审核流程和质量评分机制,确保平台内容质量,包含审核状态工作流、质量评分、垃圾内容过滤和标签规范化。
|
||||
|
||||
---
|
||||
|
||||
## 验收标准(可测试)
|
||||
|
||||
### 方向 A 验收标准
|
||||
- [ ] **A1**: 用户可通过切换按钮在深色/浅色主题间切换,选择持久化到 localStorage
|
||||
- [ ] **A2**: 在 375px-768px-1440px 三个断点下,所有页面布局正确无溢出
|
||||
- [ ] **A3**: 通过 Lighthouse 无障碍审计得分 >= 90
|
||||
- [ ] **A4**: 所有交互元素可通过键盘访问(Tab 导航 + Enter/Space 激活)
|
||||
- [ ] **A5**: 首屏加载 LCP <= 2.5s(移动端 4G 网络)
|
||||
|
||||
### 方向 B 验收标准
|
||||
- [ ] **B1**: 新项目默认状态为 `PENDING_REVIEW`,管理员可将其变为 `APPROVED`/`REJECTED`
|
||||
- [ ] **B2**: 项目详情页显示质量评分(0-100),评分依据内容完整度和标签准确性
|
||||
- [ ] **B3**: 系统可自动标记疑似垃圾/低质项目(评分 < 30)
|
||||
- [ ] **B4**: Tag Janitor API 支持批量标签规范化操作
|
||||
- [ ] **B5**: 审核日志记录所有状态变更(谁、何时、从什么状态改为什么状态)
|
||||
|
||||
---
|
||||
|
||||
## 分阶段实施步骤
|
||||
|
||||
---
|
||||
|
||||
## 阶段 1:基础设施准备(Week 1)
|
||||
|
||||
### 1.1 数据库 Schema 扩展(方向 B)
|
||||
|
||||
**目标**: 为质量控制体系添加必要的数据库字段和模型
|
||||
|
||||
**涉及的文件**:
|
||||
- `prisma/schema.prisma`
|
||||
|
||||
**具体变更**:
|
||||
|
||||
```prisma
|
||||
// 新增:项目审核状态枚举
|
||||
enum ReviewStatus {
|
||||
PENDING_REVIEW // 待审核
|
||||
APPROVED // 已批准
|
||||
REJECTED // 已拒绝
|
||||
FLAGGED // 已标记(可疑内容)
|
||||
}
|
||||
|
||||
// 修改:Project 模型添加审核和质量字段
|
||||
model Project {
|
||||
// ... 现有字段 ...
|
||||
|
||||
// 审核相关
|
||||
reviewStatus ReviewStatus @default(PENDING_REVIEW)
|
||||
reviewedAt DateTime?
|
||||
reviewedBy String? // 审核人标识(未来可关联用户系统)
|
||||
|
||||
// 质量评分
|
||||
qualityScore Int? // 0-100 分
|
||||
qualityFactors Json? // 评分因素明细
|
||||
qualityUpdatedAt DateTime?
|
||||
|
||||
// 索引更新
|
||||
@@index([reviewStatus], map: "idx_project_reviewStatus")
|
||||
@@index([qualityScore], map: "idx_project_qualityScore")
|
||||
}
|
||||
|
||||
// 新增:审核日志模型
|
||||
model ReviewLog {
|
||||
id String @id @default(cuid())
|
||||
projectId String
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
fromStatus ReviewStatus
|
||||
toStatus ReviewStatus
|
||||
reason String? // 变更原因
|
||||
reviewedBy String? // 审核人
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([projectId], map: "idx_reviewLog_projectId")
|
||||
@@index([createdAt], map: "idx_reviewLog_createdAt")
|
||||
@@map("review_logs")
|
||||
}
|
||||
```
|
||||
|
||||
**验收检查**:
|
||||
```bash
|
||||
pnpm prisma migrate dev --name add_review_system
|
||||
pnpm prisma generate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.2 主题系统基础(方向 A)
|
||||
|
||||
**目标**: 建立主题切换的客户端基础设施
|
||||
|
||||
**涉及的文件**:
|
||||
- `src/components/theme/ThemeProvider.tsx` (新建)
|
||||
- `src/components/theme/ThemeToggle.tsx` (新建)
|
||||
- `src/app/layout.tsx`
|
||||
- `src/hooks/useTheme.ts` (新建)
|
||||
|
||||
**具体实现要点**:
|
||||
|
||||
1. **ThemeProvider.tsx**: 创建客户端主题上下文
|
||||
- 读取 localStorage 中的 `theme` 值
|
||||
- 支持 `light`、`dark`、`system` 三种模式
|
||||
- 通过 `next-themes` 或自定义 Context 实现
|
||||
|
||||
2. **ThemeToggle.tsx**: 主题切换按钮组件
|
||||
- 显示当前主题图标(太阳/月亮/系统)
|
||||
- 点击切换主题
|
||||
- 适配 neo-brutalism 设计风格
|
||||
|
||||
3. **layout.tsx 修改**: 包装 ThemeProvider
|
||||
```tsx
|
||||
<ThemeProvider attribute="class" defaultTheme="system">
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
```
|
||||
|
||||
**验收检查**:
|
||||
- 切换主题后刷新页面,主题保持不变
|
||||
- 系统主题变化时自动跟随(当选择 `system` 模式)
|
||||
|
||||
---
|
||||
|
||||
## 阶段 2:核心功能实现(Week 2-3)
|
||||
|
||||
### 2.1 主题切换 UI 集成(方向 A)
|
||||
|
||||
**目标**: 将主题切换功能集成到网站导航栏
|
||||
|
||||
**涉及的文件**:
|
||||
- `src/app/[locale]/layout.tsx`
|
||||
- `src/components/theme/ThemeToggle.tsx`
|
||||
- `src/messages/zh.json`
|
||||
- `src/messages/en.json`
|
||||
|
||||
**具体变更**:
|
||||
|
||||
1. 在导航栏右侧添加 ThemeToggle 按钮(LocaleSwitcher 旁边)
|
||||
2. 添加国际化文本:
|
||||
```json
|
||||
// zh.json
|
||||
"theme": {
|
||||
"light": "浅色模式",
|
||||
"dark": "深色模式",
|
||||
"system": "跟随系统"
|
||||
}
|
||||
|
||||
// en.json
|
||||
"theme": {
|
||||
"light": "Light Mode",
|
||||
"dark": "Dark Mode",
|
||||
"system": "System"
|
||||
}
|
||||
```
|
||||
|
||||
3. 确保所有 `dark:` Tailwind 类正确应用
|
||||
|
||||
**验收检查**:
|
||||
- [ ] 导航栏显示主题切换按钮
|
||||
- [ ] 点击切换立即生效,无闪烁
|
||||
- [ ] 两种主题下所有组件颜色正确
|
||||
|
||||
---
|
||||
|
||||
### 2.2 审核状态 API(方向 B)
|
||||
|
||||
**目标**: 创建项目审核相关的 API 端点
|
||||
|
||||
**涉及的文件**:
|
||||
- `src/lib/validations.ts`
|
||||
- `src/app/api/admin/review/route.ts` (新建)
|
||||
- `src/app/api/admin/review/service.ts` (新建)
|
||||
- `src/hooks/useProjects.ts`
|
||||
|
||||
**具体实现**:
|
||||
|
||||
1. **validations.ts**: 添加审核相关 Schema
|
||||
```typescript
|
||||
export const ReviewStatusEnum = z.enum([
|
||||
"PENDING_REVIEW", "APPROVED", "REJECTED", "FLAGGED"
|
||||
]);
|
||||
|
||||
export const UpdateReviewStatusSchema = z.object({
|
||||
apiKey: z.string().min(32),
|
||||
projectId: z.string().min(1),
|
||||
status: ReviewStatusEnum,
|
||||
reason: z.string().max(500).optional(),
|
||||
reviewedBy: z.string().max(100).optional(),
|
||||
});
|
||||
|
||||
export const GetPendingReviewsQuerySchema = z.object({
|
||||
limit: z.coerce.number().int().positive().max(50).default(20),
|
||||
offset: z.coerce.number().int().nonnegative().default(0),
|
||||
sortBy: z.enum(['createdAt', 'qualityScore']).default('createdAt'),
|
||||
sortOrder: z.enum(['asc', 'desc']).default('asc'),
|
||||
});
|
||||
```
|
||||
|
||||
2. **route.ts**: 实现 PATCH 端点更新审核状态
|
||||
|
||||
3. **service.ts**: 业务逻辑
|
||||
- 更新项目审核状态
|
||||
- 创建审核日志记录
|
||||
- 验证状态转换合法性
|
||||
|
||||
**验收检查**:
|
||||
```bash
|
||||
# 测试 API
|
||||
curl -X PATCH http://localhost:3000/api/admin/review \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"apiKey":"...","projectId":"xxx","status":"APPROVED"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.3 质量评分系统(方向 B)
|
||||
|
||||
**目标**: 实现自动计算项目质量评分的算法
|
||||
|
||||
**涉及的文件**:
|
||||
- `src/lib/quality-scorer.ts` (新建)
|
||||
- `src/app/api/webhook/projects/route.ts`
|
||||
- `src/lib/validations.ts`
|
||||
|
||||
**评分规则**(总分 100):
|
||||
|
||||
| 因素 | 分值 | 说明 |
|
||||
|------|------|------|
|
||||
| 描述长度 | 0-15 | 10-50字=5分,50-200字=10分,200-500字=15分 |
|
||||
| 内容完整性 | 0-20 | 有 content=10分,content > 200字=+10分 |
|
||||
| 标签数量 | 0-15 | 1-3个=5分,4-6个=10分,7-10个=15分 |
|
||||
| 链接完整性 | 0-20 | 有 GITHUB=10分,有 WEBSITE=+5分,有其他=+5分 |
|
||||
| 英文翻译 | 0-15 | nameEn 存在=5分,descriptionEn 存在=5分,contentEn 存在=5分 |
|
||||
| 媒体丰富度 | 0-15 | 未来扩展(截图、视频等) |
|
||||
|
||||
**具体实现**:
|
||||
|
||||
```typescript
|
||||
// quality-scorer.ts
|
||||
export function calculateQualityScore(project: ProjectInput): QualityScoreResult {
|
||||
const factors: QualityFactor[] = [];
|
||||
|
||||
// 描述长度评分
|
||||
const descLength = project.description.length;
|
||||
let descScore = 0;
|
||||
if (descLength >= 10 && descLength < 50) descScore = 5;
|
||||
else if (descLength >= 50 && descLength < 200) descScore = 10;
|
||||
else if (descLength >= 200 && descLength <= 500) descScore = 15;
|
||||
factors.push({ name: 'descriptionLength', score: descScore, maxScore: 15 });
|
||||
|
||||
// ... 其他评分逻辑
|
||||
|
||||
return {
|
||||
totalScore: factors.reduce((sum, f) => sum + f.score, 0),
|
||||
factors,
|
||||
flagged: totalScore < 30, // 低质量标记
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**验收检查**:
|
||||
- [ ] 新项目通过 webhook 导入时自动计算评分
|
||||
- [ ] 评分 < 30 的项目自动标记为 `FLAGGED`
|
||||
|
||||
---
|
||||
|
||||
## 阶段 3:UI 完善(Week 4-5)
|
||||
|
||||
### 3.1 移动端适配优化(方向 A)
|
||||
|
||||
**目标**: 优化移动端布局和交互体验
|
||||
|
||||
**涉及的文件**:
|
||||
- `src/app/[locale]/layout.tsx`
|
||||
- `src/components/layout/MobileMenu.tsx` (新建)
|
||||
- `src/components/project/ProjectCard.tsx`
|
||||
- `src/components/project/ProjectList.tsx`
|
||||
- `src/app/[locale]/projects/[id]/page.tsx`
|
||||
|
||||
**具体变更**:
|
||||
|
||||
1. **MobileMenu.tsx**: 实现汉堡菜单
|
||||
- 使用 Radix UI Dialog 或自定义实现
|
||||
- 包含导航链接、语言切换、主题切换
|
||||
- 平滑的打开/关闭动画
|
||||
|
||||
2. **layout.tsx**: 替换现有的简单按钮
|
||||
- 在移动端显示 MobileMenu
|
||||
- 添加 `aria-label` 和键盘支持
|
||||
|
||||
3. **ProjectCard.tsx**: 优化移动端布局
|
||||
- 字体大小调整
|
||||
- 触摸目标至少 44x44px
|
||||
- 标签横向滚动或折叠
|
||||
|
||||
**验收检查**:
|
||||
- [ ] Chrome DevTools 模拟 375px 宽度下无布局溢出
|
||||
- [ ] 所有按钮/链接触摸区域 >= 44x44px
|
||||
- [ ] 移动端菜单正常工作
|
||||
|
||||
---
|
||||
|
||||
### 3.2 无障碍访问优化(方向 A)
|
||||
|
||||
**目标**: 通过 Lighthouse 无障碍审计
|
||||
|
||||
**涉及的文件**:
|
||||
- 所有组件文件
|
||||
- `src/app/layout.tsx`
|
||||
|
||||
**具体变更**:
|
||||
|
||||
1. **语义化 HTML**:
|
||||
- 使用 `<nav>`, `<main>`, `<article>`, `<section>`
|
||||
- 表单控件关联 `<label>`
|
||||
- 图片添加 `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. 之后两个方向可并行推进
|
||||
@@ -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)
|
||||
@@ -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 索引
|
||||
@@ -1,323 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Agent Park: AI Keyword Evolution</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400&family=Work+Sans:wght@400;600;800&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: "#FFD700", // Bright MotherDuck Yellow
|
||||
secondary: "#7FB5FF", // Soft Blue
|
||||
accent: "#C39BD3", // Purple
|
||||
"background-light": "#F7F5F0", // Warm off-white
|
||||
"background-dark": "#1A1A1A", // Dark charcoal
|
||||
"border-dark": "#000000",
|
||||
},
|
||||
fontFamily: {
|
||||
display: ["'Space Mono'", "monospace"],
|
||||
body: ["'Work Sans'", "sans-serif"],
|
||||
},
|
||||
borderRadius: {
|
||||
DEFAULT: "4px",
|
||||
},
|
||||
boxShadow: {
|
||||
'hard': '4px 4px 0px 0px rgba(0,0,0,1)',
|
||||
'hard-sm': '2px 2px 0px 0px rgba(0,0,0,1)',
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style type="text/tailwindcss">
|
||||
.geometric-bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0.15;
|
||||
}
|
||||
.shape {
|
||||
position: absolute;
|
||||
border: 2px solid currentColor;
|
||||
opacity: 0.3;
|
||||
}
|
||||
.shape-sq { width: 40px; height: 40px; }
|
||||
.shape-tri {
|
||||
width: 0; height: 0;
|
||||
border-left: 20px solid transparent;
|
||||
border-right: 20px solid transparent;
|
||||
border-bottom: 35px solid currentColor;
|
||||
background: transparent;
|
||||
border-top: none;
|
||||
}
|
||||
.shape-line { height: 2px; width: 60px; background: currentColor; border: none; }
|
||||
.cloud-word {
|
||||
@apply transition-all duration-200 cursor-pointer inline-block whitespace-nowrap relative;
|
||||
}.popover {
|
||||
@apply absolute bottom-[calc(100%+12px)] left-1/2 -translate-x-1/2 w-48 bg-white dark:bg-gray-800 border-2 border-black shadow-hard-sm opacity-0 pointer-events-none transition-all duration-300 translate-y-2 z-50 text-left;
|
||||
}
|
||||
.cloud-word:hover .popover {
|
||||
@apply opacity-100 translate-y-0 pointer-events-auto;
|
||||
}
|
||||
.popover::after {
|
||||
content: '';
|
||||
@apply absolute -bottom-2 left-1/2 -translate-x-1/2 border-l-[8px] border-l-transparent border-r-[8px] border-r-transparent border-t-[8px] border-t-black;
|
||||
}
|
||||
.word-cluster {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
::-webkit-scrollbar { width: 12px }
|
||||
::-webkit-scrollbar-track { background: #f1f1f1; border-left: 2px solid black }
|
||||
::-webkit-scrollbar-thumb { background: #FFD700; border: 2px solid black }
|
||||
.nav-button {
|
||||
@apply flex items-center justify-center w-12 h-12 md:w-16 md:h-16 bg-white dark:bg-gray-800 border-4 border-black shadow-hard hover:translate-y-0.5 hover:shadow-none transition-all cursor-pointer;
|
||||
}
|
||||
.progress-step {
|
||||
@apply h-3 flex-1 border-2 border-black transition-colors duration-300;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background-light dark:bg-background-dark text-black dark:text-gray-100 font-body transition-colors duration-300 min-h-screen relative overflow-x-hidden">
|
||||
<div class="geometric-bg text-black dark:text-white">
|
||||
<div class="shape shape-sq top-[10%] left-[5%] rotate-12"></div>
|
||||
<div class="shape shape-tri top-[25%] left-[85%] rotate-45"></div>
|
||||
<div class="shape shape-line top-[45%] left-[15%] -rotate-12"></div>
|
||||
<div class="shape shape-sq top-[65%] left-[90%] -rotate-6"></div>
|
||||
<div class="shape shape-tri top-[80%] left-[10%] rotate-[160deg]"></div>
|
||||
<div class="shape shape-line top-[15%] left-[50%] rotate-90"></div>
|
||||
<div class="shape shape-sq top-[85%] left-[55%] rotate-45"></div>
|
||||
<div class="shape shape-line top-[70%] left-[30%] rotate-45"></div>
|
||||
<div class="shape shape-tri top-[5%] left-[70%] -rotate-12"></div>
|
||||
</div>
|
||||
<nav class="sticky top-0 z-[100] w-full border-b-2 border-black bg-primary dark:bg-yellow-600">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16 items-center">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-8 w-8 bg-black rounded-full flex items-center justify-center text-white font-bold border-2 border-white">AP</div>
|
||||
<span class="font-display font-bold text-xl tracking-tighter uppercase">Agent Park</span>
|
||||
</div>
|
||||
<div class="hidden md:flex space-x-8 font-display font-bold text-sm">
|
||||
<a class="underline decoration-4 underline-offset-4" href="#">Navigator</a>
|
||||
<a class="hover:underline decoration-2 underline-offset-4" href="#">DuckDB Stats</a>
|
||||
<a class="hover:underline decoration-2 underline-offset-4" href="#">Manifesto</a>
|
||||
</div>
|
||||
<button class="bg-white dark:bg-gray-800 border-2 border-black shadow-hard-sm px-4 py-1 font-display font-bold text-sm hover:translate-y-0.5 hover:shadow-none transition-all dark:text-white">
|
||||
JOIN THE FLOCK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<header class="relative z-10 pt-16 pb-8 text-center max-w-4xl mx-auto px-4">
|
||||
<div class="inline-block bg-accent dark:bg-purple-700 border-2 border-black px-3 py-1 font-display font-bold text-xs mb-4 shadow-hard-sm rotate-[-2deg]">
|
||||
EXPERIMENTAL DATA
|
||||
</div>
|
||||
<h1 class="font-display text-5xl md:text-7xl font-bold mb-6 tracking-tighter leading-tight">
|
||||
THE EVOLUTION OF <br/>
|
||||
<span class="text-transparent bg-clip-text bg-gradient-to-r from-blue-500 to-purple-500" style="-webkit-text-stroke: 1.5px black;">AI DISCOURSE</span>
|
||||
</h1>
|
||||
<p class="text-lg md:text-xl max-w-2xl mx-auto font-medium text-gray-700 dark:text-gray-300 mb-8">
|
||||
From "Large Language Models" to "Agentic Workflows". Use the arrows below to explore the shifting narrative.
|
||||
</p>
|
||||
</header>
|
||||
<main class="relative z-10 max-w-6xl mx-auto px-4 pb-32 overflow-visible">
|
||||
<div class="bg-white/50 dark:bg-black/20 backdrop-blur-sm border-4 border-black p-8 md:p-12 shadow-hard relative overflow-visible">
|
||||
<div class="flex gap-2 mb-12 max-w-md mx-auto">
|
||||
<div class="progress-step bg-primary"></div>
|
||||
<div class="progress-step bg-gray-200 dark:bg-gray-700"></div>
|
||||
<div class="progress-step bg-gray-200 dark:bg-gray-700"></div>
|
||||
<div class="progress-step border-dashed bg-transparent border-gray-400"></div>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row items-center justify-between gap-8 mb-12">
|
||||
<button class="nav-button order-2 md:order-1 opacity-50 cursor-not-allowed">
|
||||
<span class="material-symbols-outlined text-4xl">chevron_left</span>
|
||||
</button>
|
||||
<div class="text-center order-1 md:order-2 flex-1">
|
||||
<div class="inline-block bg-primary border-4 border-black px-10 py-4 shadow-hard font-display font-bold text-4xl md:text-5xl mb-4 rotate-1">
|
||||
Q1 2023
|
||||
</div>
|
||||
<p class="font-display text-lg text-gray-600 dark:text-gray-400 italic">"The dawn of the chat interface."</p>
|
||||
</div>
|
||||
<button class="nav-button order-3">
|
||||
<span class="material-symbols-outlined text-4xl">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative py-12 px-4">
|
||||
<div class="word-cluster">
|
||||
<span class="cloud-word border-4 border-black bg-secondary px-6 py-3 rounded-full text-4xl md:text-5xl font-black shadow-hard rotate-1">
|
||||
ChatGPT
|
||||
<div class="popover">
|
||||
<div class="bg-primary text-black font-display font-bold p-2 border-b-2 border-black text-sm uppercase">ChatGPT</div>
|
||||
<div class="p-3 space-y-2 text-xs font-medium dark:text-gray-100">
|
||||
<div class="flex gap-2 items-start">• <span>Mass consumer breakout</span></div>
|
||||
<div class="flex gap-2 items-start">• <span>GPT-3.5 architecture</span></div>
|
||||
<div class="flex gap-2 items-start">• <span>Conversational UI focus</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span class="cloud-word border-2 border-black bg-white dark:bg-gray-700 px-4 py-2 rounded-lg text-xl md:text-2xl font-bold -rotate-2">
|
||||
Parameters
|
||||
<div class="popover">
|
||||
<div class="bg-accent text-white font-display font-bold p-2 border-b-2 border-black text-sm uppercase">Parameters</div>
|
||||
<div class="p-3 space-y-2 text-xs font-medium dark:text-gray-100">
|
||||
<div class="flex gap-2 items-start">• <span>Internal model weights</span></div>
|
||||
<div class="flex gap-2 items-start">• <span>Scaling law metric</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span class="cloud-word border-4 border-black bg-accent text-white px-7 py-3 rounded-full text-2xl md:text-3xl font-bold shadow-hard-sm rotate-3">
|
||||
Hallucination
|
||||
<div class="popover">
|
||||
<div class="bg-white text-black font-display font-bold p-2 border-b-2 border-black text-sm uppercase">Hallucination</div>
|
||||
<div class="p-3 space-y-2 text-xs font-medium text-black">
|
||||
<div class="flex gap-2 items-start">• <span>Factually incorrect output</span></div>
|
||||
<div class="flex gap-2 items-start">• <span>Stochastic behavior</span></div>
|
||||
<div class="flex gap-2 items-start">• <span>Trust barrier</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span class="cloud-word border-2 border-black bg-gray-100 dark:bg-gray-600 px-3 py-1 rounded text-lg">
|
||||
Transformer
|
||||
<div class="popover">
|
||||
<div class="bg-secondary text-black font-display font-bold p-2 border-b-2 border-black text-sm uppercase">Transformer</div>
|
||||
<div class="p-3 space-y-2 text-xs font-medium dark:text-gray-100">
|
||||
<div class="flex gap-2 items-start">• <span>Attention mechanism</span></div>
|
||||
<div class="flex gap-2 items-start">• <span>Parallel processing</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span class="cloud-word border-4 border-black bg-white dark:bg-gray-700 px-6 py-3 rounded-xl text-3xl md:text-4xl font-semibold -rotate-1">
|
||||
Prompt Engineering
|
||||
<div class="popover">
|
||||
<div class="bg-primary text-black font-display font-bold p-2 border-b-2 border-black text-sm uppercase">Prompting</div>
|
||||
<div class="p-3 space-y-2 text-xs font-medium dark:text-gray-100">
|
||||
<div class="flex gap-2 items-start">• <span>Instruction crafting</span></div>
|
||||
<div class="flex gap-2 items-start">• <span>Few-shot prompting</span></div>
|
||||
<div class="flex gap-2 items-start">• <span>Iterative tuning</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span class="cloud-word border-2 border-black bg-white dark:bg-gray-700 px-4 py-2 rounded-full text-xl">
|
||||
OpenAI
|
||||
<div class="popover w-40">
|
||||
<div class="bg-black text-white font-display font-bold p-2 border-b-2 border-black text-xs uppercase">The Lab</div>
|
||||
<div class="p-2 space-y-1 text-[10px] font-medium dark:text-gray-100">
|
||||
<div>• Research pioneer</div>
|
||||
<div>• Closed-source shift</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span class="cloud-word border-2 border-black bg-primary px-5 py-2 rounded-md text-2xl font-bold rotate-6">
|
||||
LLM
|
||||
<div class="popover">
|
||||
<div class="bg-white text-black font-display font-bold p-2 border-b-2 border-black text-sm uppercase">LLM</div>
|
||||
<div class="p-3 space-y-2 text-xs font-medium text-black">
|
||||
<div class="flex gap-2 items-start">• <span>Large Language Model</span></div>
|
||||
<div class="flex gap-2 items-start">• <span>Next token prediction</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span class="cloud-word border-2 border-black bg-white dark:bg-gray-700 px-3 py-1 rounded text-base">RLHF</span>
|
||||
<span class="cloud-word border-4 border-black bg-secondary px-5 py-2.5 rounded-full text-3xl font-bold -rotate-3">Bard</span>
|
||||
<span class="cloud-word border-2 border-black bg-white dark:bg-gray-700 px-4 py-2 rounded-lg text-xl">Foundation Model</span>
|
||||
<span class="cloud-word border-2 border-black bg-accent text-white px-5 py-2 rounded-full text-2xl shadow-hard-sm">Emergence</span>
|
||||
<span class="cloud-word border-2 border-black bg-gray-100 px-6 py-2 rounded-xl text-2xl font-black rotate-1">Tokenization</span>
|
||||
<span class="cloud-word border-2 border-black bg-white px-4 py-1.5 rounded text-lg -rotate-6">Context Window</span>
|
||||
<span class="cloud-word border-2 border-black bg-primary px-5 py-2 rounded-full text-2xl font-bold">GPT-4</span>
|
||||
<span class="cloud-word border-4 border-black bg-secondary px-6 py-3 rounded text-3xl font-bold shadow-hard-sm">Few-Shot</span>
|
||||
<span class="cloud-word border-2 border-black bg-white px-3 py-1.5 rounded-md text-base">Zero-Shot</span>
|
||||
<span class="cloud-word border-2 border-black bg-accent/20 px-4 py-2 rounded-full text-xl italic">Multi-turn</span>
|
||||
<span class="cloud-word border-2 border-black bg-white px-4 py-2 rounded-lg text-xl font-bold rotate-2">Supervised Learning</span>
|
||||
</div>
|
||||
<div class="absolute -top-16 -right-8 md:right-0 bg-black text-white p-5 rounded-xl text-sm md:text-base font-display w-64 shadow-hard rotate-6 hidden lg:block">
|
||||
"ChatGPT" search volume peaked +400% in March 2023!
|
||||
<div class="absolute -bottom-2 left-1/2 -translate-x-1/2 w-4 h-4 bg-black transform rotate-45"></div>
|
||||
</div>
|
||||
<div class="absolute -bottom-10 -left-10 w-24 h-24 bg-primary rounded-full border-4 border-black flex items-center justify-center font-bold font-display text-xs text-center shadow-hard rotate-[-15deg] z-20">
|
||||
GENESIS<br/>OF CHAT
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section class="relative mt-24">
|
||||
<div class="text-center mb-10">
|
||||
<div class="inline-block bg-white dark:bg-gray-700 border-4 border-black border-dashed px-8 py-4 font-display font-bold text-3xl text-gray-500 transform -rotate-1">
|
||||
WHAT'S NEXT?
|
||||
</div>
|
||||
</div>
|
||||
<div class="max-w-3xl mx-auto bg-gray-100 dark:bg-gray-900 border-4 border-black border-dashed p-10 rounded-3xl relative opacity-80 hover:opacity-100 transition-opacity shadow-xl">
|
||||
<h3 class="font-display font-bold text-2xl text-center text-gray-500 uppercase tracking-widest mb-8">Future Prediction Zone</h3>
|
||||
<div class="word-cluster text-gray-400 italic">
|
||||
<span class="text-4xl">???</span>
|
||||
<span class="text-xl">AGI?</span>
|
||||
<span class="text-2xl">Small Language Models</span>
|
||||
<span class="text-3xl">On-Device AI</span>
|
||||
<span class="text-xl">Liquid Neural Nets</span>
|
||||
<span class="text-2xl">Sovereign AI</span>
|
||||
<span class="text-xl">World Models</span>
|
||||
</div>
|
||||
<div class="text-center mt-12">
|
||||
<button class="bg-primary text-black border-4 border-black px-8 py-3 font-bold font-display text-lg hover:bg-yellow-500 shadow-hard transform hover:-translate-y-1 transition-all">
|
||||
Submit a Prediction
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<footer class="bg-black text-white border-t-4 border-primary py-16 relative overflow-hidden z-[100]">
|
||||
<div class="absolute inset-0 opacity-10" style="background-image: radial-gradient(#ffffff 1px, transparent 1px); background-size: 20px 20px;"></div>
|
||||
<div class="max-w-7xl mx-auto px-4 relative z-10">
|
||||
<div class="grid md:grid-cols-4 gap-12 mb-16">
|
||||
<div class="col-span-1 md:col-span-2">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="h-10 w-10 bg-primary rounded-full flex items-center justify-center text-black font-bold border-2 border-white text-xl">AP</div>
|
||||
<h2 class="font-display font-bold text-3xl">AGENT PARK</h2>
|
||||
</div>
|
||||
<p class="font-body text-gray-400 max-w-md text-lg leading-relaxed">
|
||||
Curating the chaos of the AI revolution, one keyword at a time. Built with 💛 by the ducklings.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-display font-bold text-primary mb-6 text-lg uppercase tracking-widest">EXPLORE</h4>
|
||||
<ul class="space-y-3 font-body">
|
||||
<li><a class="hover:text-primary transition-colors text-gray-300" href="#">The Archive</a></li>
|
||||
<li><a class="hover:text-primary transition-colors text-gray-300" href="#">Trends Dashboard</a></li>
|
||||
<li><a class="hover:text-primary transition-colors text-gray-300" href="#">Contributor Guide</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-display font-bold text-primary mb-6 text-lg uppercase tracking-widest">CONNECT</h4>
|
||||
<div class="flex space-x-4">
|
||||
<a class="w-10 h-10 bg-white text-black rounded-lg flex items-center justify-center hover:bg-primary transition-colors border-2 border-black shadow-hard-sm" href="#">
|
||||
<span class="font-bold">X</span>
|
||||
</a>
|
||||
<a class="w-10 h-10 bg-white text-black rounded-lg flex items-center justify-center hover:bg-primary transition-colors border-2 border-black shadow-hard-sm" href="#">
|
||||
<span class="font-bold">Li</span>
|
||||
</a>
|
||||
<a class="w-10 h-10 bg-white text-black rounded-lg flex items-center justify-center hover:bg-primary transition-colors border-2 border-black shadow-hard-sm" href="#">
|
||||
<span class="font-bold">Gh</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-gray-800 pt-8 flex flex-col md:flex-row justify-between items-center text-sm font-mono text-gray-500">
|
||||
<p>© 2024 Agent Park. All rights reserved.</p>
|
||||
<div class="mt-4 md:mt-0 flex gap-8">
|
||||
<span class="hover:text-white cursor-pointer transition-colors">Privacy Policy</span>
|
||||
<span class="hover:text-white cursor-pointer transition-colors">Terms of Quack</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body></html>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 236 KiB |
-2408
File diff suppressed because it is too large
Load Diff
-716
@@ -1,716 +0,0 @@
|
||||
# 季度 AI 热点词云系统设计文档
|
||||
|
||||
**创建日期**: 2026-01-25
|
||||
**功能类型**: 数据驱动可视化
|
||||
**技术栈**: n8n + Next.js + PostgreSQL + Prisma
|
||||
|
||||
---
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
季度 AI 热点词云是一个**自动化数据驱动的可视化词云系统**,通过 n8n 工作流从 Google Trends 采集 AI 相关热点词汇,经 AI 清洗和规则引擎处理后,自动入库并在前端展示。
|
||||
|
||||
### 核心目标
|
||||
|
||||
1. **内容营销**: 吸引访客回访查看每季度更新,提供社交分享素材
|
||||
2. **教育参考**: 帮助新手理解 AI 技术演进历程
|
||||
3. **数据洞察**: 展示 AI 领域热点变化趋势
|
||||
|
||||
### 用户旅程
|
||||
|
||||
1. 用户访问 `/keyword-cloud` 页面
|
||||
2. 看到当前季度的 AI 热点词云(如 2024-Q1)
|
||||
3. 鼠标悬停在词汇上,查看详细描述和要点
|
||||
4. 点击左右箭头切换不同季度,浏览历史热点
|
||||
5. 可视化展示:词汇大小代表搜索热度,颜色代表分类
|
||||
|
||||
---
|
||||
|
||||
## 二、系统架构
|
||||
|
||||
### 架构图
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌──────────────┐
|
||||
│ Google │ │ n8n │ │ PostgreSQL │
|
||||
│ Trends API │───▶│ Workflow │───▶│ Database │
|
||||
└─────────────┘ └─────────────┘ └──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ Next.js │
|
||||
│ Frontend │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
### 数据流向
|
||||
|
||||
1. **定时触发**: n8n Schedule 每季度末自动执行
|
||||
2. **数据采集**: Google Trends API 获取热门搜索词
|
||||
3. **AI 清洗**: 过滤无关词汇,生成描述和要点
|
||||
4. **规则匹配**: 根据热度分数分配视觉样式
|
||||
5. **数据入库**: 写入 PostgreSQL 数据库
|
||||
6. **前端展示**: Next.js 从数据库读取并渲染词云
|
||||
|
||||
---
|
||||
|
||||
## 三、数据库模型
|
||||
|
||||
### 3.1 Quarter 表(季度元数据)
|
||||
|
||||
```prisma
|
||||
model Quarter {
|
||||
id Int @id @default(autoincrement())
|
||||
quarter String @unique // "2023-Q1", "2023-Q2"
|
||||
title String @db.Text // "2023年第一季度"
|
||||
titleEn String? @db.Text // "Q1 2023"
|
||||
subtitle String? @db.Text // "聊天界面的黎明"
|
||||
subtitleEn String? @db.Text // "The dawn of chat interface"
|
||||
displayOrder Int @default(0) // 前端排序
|
||||
isActive Boolean @default(true) // 是否显示
|
||||
keywords Keyword[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([quarter])
|
||||
@@index([displayOrder])
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Keyword 表(关键词核心数据)
|
||||
|
||||
```prisma
|
||||
model Keyword {
|
||||
id Int @id @default(autoincrement())
|
||||
word String // "ChatGPT"
|
||||
trendScore Int // 0-100, 从 Google Trends 获取
|
||||
|
||||
// 外键关联
|
||||
quarterId Int
|
||||
quarter Quarter @relation(fields: [quarterId], references: [id], onDelete: Cascade)
|
||||
|
||||
// 内容字段(支持中英双语)
|
||||
description String @db.Text // AI 生成的一句话描述
|
||||
descriptionEn String? @db.Text // 英文描述
|
||||
detailPoints Json // JSON 数组: ["要点1", "要点2", "要点3"]
|
||||
detailPointsEn Json? // 英文版要点
|
||||
|
||||
// 视觉样式配置
|
||||
visualConfig Json // {color, size, rotation, border}
|
||||
|
||||
// 元数据
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([quarterId])
|
||||
@@index([trendScore])
|
||||
@@index([word])
|
||||
}
|
||||
```
|
||||
|
||||
**visualConfig 字段结构示例**:
|
||||
```json
|
||||
{
|
||||
"color": "secondary",
|
||||
"size": "text-5xl",
|
||||
"rotation": "rotate-1",
|
||||
"border": "border-4"
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 VisualStyleRule 表(视觉样式规则配置)
|
||||
|
||||
```prisma
|
||||
model VisualStyleRule {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique // "热门大词-金色"
|
||||
|
||||
// 分数区间
|
||||
minScore Int // 90
|
||||
maxScore Int // 100
|
||||
|
||||
// 视觉属性
|
||||
color String // "primary", "secondary", "accent"
|
||||
size String // "text-5xl", "text-3xl", "text-xl"
|
||||
border String // "border-4", "border-2"
|
||||
rotation String? // "rotate-1", "rotate-2", null
|
||||
|
||||
// 控制
|
||||
priority Int @default(0) // 优先级,分数重叠时按优先级
|
||||
enabled Boolean @default(true) // 是否启用
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([enabled])
|
||||
@@index([minScore, maxScore])
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 KeywordCloudErrorLog 表(错误日志)
|
||||
|
||||
```prisma
|
||||
model KeywordCloudErrorLog {
|
||||
id Int @id @default(autoincrement())
|
||||
quarter String // "2023-Q1"
|
||||
keyword String? // "ChatGPT"
|
||||
errorType String // "INVALID_DATA", "API_ERROR", "DB_ERROR"
|
||||
errorMessage String @db.Text // 详细错误信息
|
||||
rawData Json? // 原始数据便于调试
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([quarter])
|
||||
@@index([errorType])
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、n8n 工作流设计
|
||||
|
||||
### 4.1 工作流概览
|
||||
|
||||
5 个核心节点实现从数据采集到入库的完整流程。
|
||||
|
||||
### 4.2 节点详细配置
|
||||
|
||||
#### 节点 1: Schedule Trigger(定时触发)
|
||||
|
||||
- **类型**: `n8n-nodes-base.scheduleTrigger`
|
||||
- **Cron 表达式**: `0 0 23 28-31 * *` (每季度末最后一天的 23:00)
|
||||
- **月份判断**: Function 节点检查当前月份(3/6/9/12),如果不是则跳过
|
||||
- **输出**: 当前季度的标识(如 "2024-Q1")
|
||||
|
||||
#### 节点 2: Google Trends 采集
|
||||
|
||||
- **类型**: `@gamal.dev/n8n-nodes-google-trends`
|
||||
- **配置参数**:
|
||||
- `keywords`: ["AI", "artificial intelligence", "machine learning", "GPT", "LLM"]
|
||||
- `timeRange`: 当前季度的 3 个月(如 2024-01-01 to 2024-03-31)
|
||||
- `category`: "Science > Computer Science > AI"
|
||||
- `geo`: "GB"
|
||||
- **输出示例**:
|
||||
```json
|
||||
{
|
||||
"keyword": "ChatGPT",
|
||||
"trendScore": 95,
|
||||
"rising": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 节点 3: AI 清洗和内容生成
|
||||
|
||||
- **类型**: `@n8n/n8n-nodes-langchain.lmChatChain`
|
||||
- **Prompt 模板**:
|
||||
```
|
||||
你是一个 AI 领域专家。以下是 Google Trends 采集的热门关键词列表:
|
||||
{{ $json.all() }}
|
||||
|
||||
任务:
|
||||
1. 过滤掉与 AI/机器学习无关的关键词
|
||||
2. 为每个关键词生成中文描述(10-50字)
|
||||
3. 生成 3 条详细要点(每条 10-30 字,客观描述,避免营销用语)
|
||||
|
||||
输出格式(JSON 数组):
|
||||
[
|
||||
{
|
||||
"word": "ChatGPT",
|
||||
"trendScore": 95,
|
||||
"description": "OpenAI 开发的对话式人工智能助手",
|
||||
"detailPoints": ["支持多轮对话", "基于 GPT-3.5 架构", "2023年用户突破1亿"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### 节点 4: 规则引擎匹配
|
||||
|
||||
- **类型**: `n8n-nodes-base.code`
|
||||
- **逻辑**:
|
||||
1. HTTP Request 获取 `VisualStyleRule` 表数据
|
||||
- 方法: GET
|
||||
- URL: `{{ $env.API_URL }}/api/keyword-cloud/rules`
|
||||
2. 按 `priority` 排序规则
|
||||
3. 遍历每个关键词,匹配第一个符合的规则(`minScore <= trendScore <= maxScore`)
|
||||
4. 将视觉配置注入数据
|
||||
|
||||
#### 节点 5: 批量写入数据库
|
||||
|
||||
- **类型**: `n8n-nodes-base.httpRequest`
|
||||
- **方法**: POST
|
||||
- **URL**: `{{ $env.API_URL }}/api/keyword-cloud/keywords`
|
||||
- **认证**: Bearer Token(环境变量 `API_KEY`)
|
||||
- **Body**:
|
||||
```json
|
||||
{
|
||||
"quarter": "2024-Q1",
|
||||
"keywords": [
|
||||
{
|
||||
"word": "ChatGPT",
|
||||
"trendScore": 95,
|
||||
"description": "...",
|
||||
"detailPoints": ["...", "...", "..."],
|
||||
"visualConfig": {...}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
- **批量处理**: 超过 20 个关键词时分批提交
|
||||
|
||||
### 4.3 错误处理
|
||||
|
||||
- 每个节点设置 `continueOnFail: true`
|
||||
- 错误日志写入 `KeywordCloudErrorLog` 表
|
||||
- 关键错误发送通知(Email/Slack)
|
||||
|
||||
---
|
||||
|
||||
## 五、API 端点设计
|
||||
|
||||
### 5.1 GET /api/keyword-cloud/quarters
|
||||
|
||||
获取季度列表。
|
||||
|
||||
**查询参数**:
|
||||
- `isActive` (可选): 只返回激活的季度
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"quarters": [
|
||||
{
|
||||
"id": 1,
|
||||
"quarter": "2024-Q1",
|
||||
"title": "2024年第一季度",
|
||||
"titleEn": "Q1 2024",
|
||||
"subtitle": "聊天界面的黎明",
|
||||
"subtitleEn": "The dawn of chat interface",
|
||||
"displayOrder": 0,
|
||||
"keywordCount": 15
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**排序**: 按 `displayOrder` ASC
|
||||
|
||||
### 5.2 GET /api/keyword-cloud/keywords/[quarter]
|
||||
|
||||
获取指定季度的关键词列表。
|
||||
|
||||
**路径参数**:
|
||||
- `quarter`: 季度标识(如 "2024-Q1")
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"quarter": "2024-Q1",
|
||||
"title": "2024年第一季度",
|
||||
"keywords": [
|
||||
{
|
||||
"id": 1,
|
||||
"word": "ChatGPT",
|
||||
"trendScore": 95,
|
||||
"description": "OpenAI 开发的对话式 AI 助手",
|
||||
"detailPoints": ["支持多轮对话", "基于 GPT-3.5", "2023用户破亿"],
|
||||
"visualConfig": {
|
||||
"color": "secondary",
|
||||
"size": "text-5xl",
|
||||
"border": "border-4",
|
||||
"rotation": "rotate-1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**排序**: 按 `trendScore` DESC
|
||||
|
||||
### 5.3 POST /api/keyword-cloud/keywords
|
||||
|
||||
n8n 工作流写入关键词数据。
|
||||
|
||||
**认证**: Bearer Token (WEBHOOK_API_KEY)
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"quarter": "2024-Q1",
|
||||
"keywords": [
|
||||
{
|
||||
"word": "ChatGPT",
|
||||
"trendScore": 95,
|
||||
"description": "...",
|
||||
"detailPoints": ["...", "...", "..."],
|
||||
"visualConfig": {...}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"created": 15,
|
||||
"failed": 2,
|
||||
"errors": [
|
||||
{ "word": "Invalid", "error": "trendScore out of range" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**逻辑**:
|
||||
1. 验证 API Key
|
||||
2. 查找或创建 `Quarter` 记录
|
||||
3. 批量创建 `Keyword` 记录(Prisma `createMany`)
|
||||
4. 失败记录写入 `KeywordCloudErrorLog` 表
|
||||
5. 返回成功/失败统计
|
||||
|
||||
### 5.4 GET /api/keyword-cloud/rules
|
||||
|
||||
获取视觉样式规则配置(n8n 规则引擎使用)。
|
||||
|
||||
**查询参数**:
|
||||
- `enabled` (可选): 只返回启用的规则
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "热门大词-金色",
|
||||
"minScore": 90,
|
||||
"maxScore": 100,
|
||||
"visualConfig": {
|
||||
"color": "primary",
|
||||
"size": "text-5xl",
|
||||
"border": "border-4",
|
||||
"rotation": "rotate-1"
|
||||
},
|
||||
"priority": 0,
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**排序**: 按 `priority` ASC
|
||||
|
||||
### 5.5 POST /api/keyword-cloud/rules
|
||||
|
||||
创建新的视觉样式规则(管理员功能)。
|
||||
|
||||
**请求体**: 同单个规则对象
|
||||
|
||||
**验证**:
|
||||
- `minScore < maxScore`
|
||||
- 必填字段检查
|
||||
- 颜色值必须是预定义的颜色类别
|
||||
|
||||
---
|
||||
|
||||
## 六、前端组件设计
|
||||
|
||||
### 6.1 路由结构
|
||||
|
||||
```
|
||||
src/app/[locale]/keyword-cloud/
|
||||
├── page.tsx # 主页面
|
||||
└── components/
|
||||
├── KeywordCloud.tsx # 词云容器组件
|
||||
├── CloudWord.tsx # 单个词汇组件
|
||||
├── QuarterNavigator.tsx # 季度切换导航
|
||||
├── WordPopover.tsx # 弹出框详情
|
||||
└── ProgressIndicator.tsx # 进度条
|
||||
```
|
||||
|
||||
### 6.2 核心组件
|
||||
|
||||
#### KeywordCloud.tsx
|
||||
|
||||
词云容器组件,负责数据获取和布局。
|
||||
|
||||
```typescript
|
||||
interface KeywordCloudProps {
|
||||
quarter: string;
|
||||
}
|
||||
|
||||
function KeywordCloud({ quarter }: KeywordCloudProps) {
|
||||
const { data, isLoading } = useKeywordData(quarter);
|
||||
|
||||
return (
|
||||
<div className="bg-white/50 dark:bg-black/20 backdrop-blur-sm border-4 border-black p-8 md:p-12 shadow-hard">
|
||||
<ProgressIndicator currentQuarter={quarter} totalQuarters={4} />
|
||||
<QuarterNavigator current={quarter} />
|
||||
<div className="word-cluster py-12">
|
||||
{data?.keywords.map((keyword) => (
|
||||
<CloudWord key={keyword.id} data={keyword} />
|
||||
))}
|
||||
</div>
|
||||
<FunFactBubble fact={data.funFact} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### CloudWord.tsx
|
||||
|
||||
单个词汇组件,应用视觉样式和悬停交互。
|
||||
|
||||
```typescript
|
||||
interface CloudWordProps {
|
||||
data: Keyword;
|
||||
}
|
||||
|
||||
function CloudWord({ data }: CloudWordProps) {
|
||||
const { word, visualConfig, description, detailPoints } = data;
|
||||
const { color, size, border, rotation } = visualConfig;
|
||||
const colorClass = colorMap[color];
|
||||
|
||||
return (
|
||||
<span className={cn(
|
||||
"cloud-word",
|
||||
border,
|
||||
"border-black",
|
||||
colorClass,
|
||||
"px-6 py-3",
|
||||
"rounded-full",
|
||||
size,
|
||||
"font-black",
|
||||
"shadow-hard",
|
||||
rotation,
|
||||
"transition-all",
|
||||
"hover:scale-105",
|
||||
"cursor-pointer"
|
||||
)}>
|
||||
{word}
|
||||
<WordPopover title={word} description={description} points={detailPoints} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### WordPopover.tsx
|
||||
|
||||
弹出框组件,显示词汇的详细信息。
|
||||
|
||||
```typescript
|
||||
interface WordPopoverProps {
|
||||
title: string;
|
||||
description: string;
|
||||
points: string[];
|
||||
}
|
||||
|
||||
function WordPopover({ title, description, points }: WordPopoverProps) {
|
||||
return (
|
||||
<div className="popover">
|
||||
<div className="bg-primary text-black font-display font-bold p-2 border-b-2 border-black text-sm uppercase">
|
||||
{title}
|
||||
</div>
|
||||
<div className="p-3 space-y-2 text-xs font-medium dark:text-gray-100">
|
||||
<div className="flex gap-2 items-start">
|
||||
<span>•</span>
|
||||
<span>{description}</span>
|
||||
</div>
|
||||
{points.map((point, i) => (
|
||||
<div key={i} className="flex gap-2 items-start">
|
||||
<span>•</span>
|
||||
<span>{point}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### QuarterNavigator.tsx
|
||||
|
||||
季度切换导航组件。
|
||||
|
||||
```typescript
|
||||
function QuarterNavigator({ current }: { current: string }) {
|
||||
const quarters = ["2023-Q1", "2023-Q2", "2023-Q3", "2023-Q4"];
|
||||
const currentIndex = quarters.indexOf(current);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-8">
|
||||
<button
|
||||
disabled={currentIndex === 0}
|
||||
className="nav-button"
|
||||
onClick={() => navigate(quarters[currentIndex - 1])}
|
||||
>
|
||||
<span className="material-symbols-outlined text-4xl">chevron_left</span>
|
||||
</button>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="bg-primary border-4 border-black px-10 py-4 shadow-hard font-display font-bold text-5xl">
|
||||
{current}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
disabled={currentIndex === quarters.length - 1}
|
||||
className="nav-button"
|
||||
onClick={() => navigate(quarters[currentIndex + 1])}
|
||||
>
|
||||
<span className="material-symbols-outlined text-4xl">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 数据获取
|
||||
|
||||
```typescript
|
||||
// src/hooks/useKeywordCloud.ts
|
||||
export async function getKeywordData(quarter: string) {
|
||||
const response = await fetch(`${API_URL}/api/keyword-cloud/keywords/${quarter}`);
|
||||
return response.json();
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 样式系统
|
||||
|
||||
复用项目现有的 Tailwind 配置和样式类:
|
||||
- `.cloud-word`: 词云词汇的基础样式
|
||||
- `.popover`: 弹出框样式(包括箭头)
|
||||
- `.word-cluster`: 词汇容器布局
|
||||
- `.nav-button`: 导航按钮样式
|
||||
|
||||
响应式断点:`md:`, `lg:`
|
||||
|
||||
---
|
||||
|
||||
## 七、错误处理和监控
|
||||
|
||||
### 7.1 分层错误处理
|
||||
|
||||
**n8n 工作流层**:
|
||||
- 每个节点 `continueOnFail: true`
|
||||
- 失败记录写入 `KeywordCloudErrorLog` 表
|
||||
- 关键错误发送通知
|
||||
|
||||
**API 层**:
|
||||
- Zod schema 验证
|
||||
- 部分成功响应模式
|
||||
- HTTP 状态码规范
|
||||
|
||||
**前端层**:
|
||||
- 友好的错误提示
|
||||
- Error Boundary
|
||||
- 重试机制
|
||||
|
||||
### 7.2 监控策略
|
||||
|
||||
- n8n 执行日志监控
|
||||
- 定期检查 `KeywordCloudErrorLog` 表
|
||||
- API 响应时间监控
|
||||
- 前端错误追踪
|
||||
|
||||
---
|
||||
|
||||
## 八、测试策略
|
||||
|
||||
### 8.1 单元测试(Vitest)
|
||||
|
||||
- API 路由处理逻辑
|
||||
- 规则引擎匹配算法
|
||||
- 数据验证 schemas
|
||||
|
||||
### 8.2 E2E 测试
|
||||
|
||||
- 季度切换功能
|
||||
- 弹出框交互
|
||||
- 响应式布局
|
||||
|
||||
### 8.3 n8n 工作流测试
|
||||
|
||||
- 使用测试环境 API 手动触发
|
||||
- 验证生成的数据质量
|
||||
- 检查规则匹配结果
|
||||
|
||||
---
|
||||
|
||||
## 九、部署指南
|
||||
|
||||
### 9.1 环境变量
|
||||
|
||||
```bash
|
||||
# .env.local
|
||||
DATABASE_URL="..."
|
||||
WEBHOOK_API_KEY="..."
|
||||
N8N_WEBHOOK_URL="https://your-n8n-instance.com/..."
|
||||
```
|
||||
|
||||
### 9.2 数据库迁移
|
||||
|
||||
```bash
|
||||
pnpm prisma migrate dev --name add_keyword_cloud_tables
|
||||
```
|
||||
|
||||
### 9.3 n8n 部署
|
||||
|
||||
1. 使用自托管 n8n 或 n8n Cloud
|
||||
2. 配置环境变量(API_URL, API_KEY)
|
||||
3. 设置 Cron 定时任务
|
||||
4. 测试工作流执行
|
||||
|
||||
### 9.4 初始化数据
|
||||
|
||||
1. 创建第一个 `Quarter` 记录(2023-Q1)
|
||||
2. 配置 3-5 条 `VisualStyleRule`:
|
||||
- 90-100: 热门大词(金色, text-5xl, border-4)
|
||||
- 70-89: 中等词汇(蓝色, text-3xl, border-2)
|
||||
- 50-69: 小词汇(紫色, text-xl, border-2)
|
||||
- 0-49: 长尾词(灰色, text-base, border-2)
|
||||
|
||||
### 9.5 监控设置
|
||||
|
||||
- n8n 执行日志告警
|
||||
- 错误日志定期检查
|
||||
- API 性能监控
|
||||
|
||||
---
|
||||
|
||||
## 十、未来扩展
|
||||
|
||||
1. **预测功能**: 基于历史数据预测下一个热点词汇
|
||||
2. **趋势分析**: 展示词汇热度的季度变化曲线
|
||||
3. **用户贡献**: 允许用户提交词汇建议
|
||||
4. **多维度**: 按技术栈、应用领域等维度分类
|
||||
5. **导出功能**: 导出季度报告(PDF/图片)
|
||||
|
||||
---
|
||||
|
||||
## 附录
|
||||
|
||||
### A. 颜色系统
|
||||
|
||||
```typescript
|
||||
const colorMap = {
|
||||
primary: "bg-primary", // Gold (#FFD700)
|
||||
secondary: "bg-secondary", // Blue (#7FB5FF)
|
||||
accent: "bg-accent", // Purple (#C39BD3)
|
||||
gray: "bg-gray-100", // Gray
|
||||
};
|
||||
```
|
||||
|
||||
### B. 字体大小映射
|
||||
|
||||
```typescript
|
||||
const sizeMap = {
|
||||
hot: "text-5xl", // 90-100 分
|
||||
medium: "text-3xl", // 70-89 分
|
||||
small: "text-xl", // 50-69 分
|
||||
tiny: "text-base", // 0-49 分
|
||||
};
|
||||
```
|
||||
|
||||
### C. 参考资源
|
||||
|
||||
- Google Trends API: https://trends.google.com/
|
||||
- n8n 文档: https://docs.n8n.io/
|
||||
- Tailwind CSS: https://tailwindcss.com/
|
||||
@@ -1,494 +0,0 @@
|
||||
# AI 时间轴功能设计文档
|
||||
|
||||
**创建日期**: 2025-01-25
|
||||
**目标用户**: 开发者和 AI 研究者
|
||||
**设计理念**: 学术+产品平衡,聚焦大语言模型发展历程(从 2017 Transformer 开始)
|
||||
|
||||
---
|
||||
|
||||
## 1. 系统架构
|
||||
|
||||
### 核心组件
|
||||
|
||||
**前端展示系统**
|
||||
- 路由: `src/app/[locale]/timeline/page.tsx`
|
||||
- 渲染策略: ISR (1小时缓存)
|
||||
- 设计风格: Neo-brutalism,复用现有设计系统
|
||||
- 数据展示: 按年份分组的卡片堆叠时间轴
|
||||
|
||||
**后端 API**
|
||||
- `POST /api/events` - 创建事件(供 n8n 调用,需 API Key 认证)
|
||||
- `GET /api/events` - 获取所有事件(支持年份筛选)
|
||||
- `GET /api/events/:id` - 获取单个事件详情
|
||||
- `PATCH /api/events/:id` - 更新事件(可选)
|
||||
- `DELETE /api/events/:id` - 删除事件(可选)
|
||||
|
||||
**数据采集系统 (n8n)**
|
||||
- 历史数据初始化流程: 一次性运行,批量收集 2017-2025 事件
|
||||
- 增量更新流程: 每周一运行,收集最近 7 天新事件
|
||||
- 三 Agent 协作: 搜索 → 筛选 → 格式化 → HTTP Request 提交
|
||||
|
||||
**数据库**
|
||||
- `AIEvent` 表: 存储所有事件
|
||||
- 无需标签系统(基础版数据结构)
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据库 Schema
|
||||
|
||||
### Prisma 模型
|
||||
|
||||
```prisma
|
||||
model AIEvent {
|
||||
id String @id @default(cuid())
|
||||
title String
|
||||
titleEn String? // 英文标题(可选)
|
||||
eventDate DateTime // 事件发生的具体日期
|
||||
description String
|
||||
descriptionEn String? // 英文描述
|
||||
imageUrl String // 事件配图 URL
|
||||
sourceUrl String? // 原始来源链接
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([eventDate(sort: Desc)]) // 按日期降序索引
|
||||
@@index([createdAt])
|
||||
}
|
||||
```
|
||||
|
||||
### 字段说明
|
||||
- **国际化字段**: `title`/`titleEn`, `description`/`descriptionEn` 与项目表保持一致
|
||||
- **日期字段**: 使用 `DateTime` 支持时间范围筛选和排序
|
||||
- **索引优化**: `eventDate` 降序索引提升"最近事件"查询性能
|
||||
- **图片存储**: 直接存储 URL(可以是 Unsplash、项目截图等)
|
||||
|
||||
### 迁移命令
|
||||
```bash
|
||||
pnpm prisma migrate dev --name add_ai_events_table
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. API 端点设计
|
||||
|
||||
### 数据验证 Schema
|
||||
|
||||
在 `src/lib/validations.ts` 新增:
|
||||
|
||||
```typescript
|
||||
const AIEventInputSchema = z.object({
|
||||
title: z.string().min(1).max(200),
|
||||
titleEn: z.string().max(200).optional(),
|
||||
eventDate: z.string().datetime(), // ISO 8601 格式
|
||||
description: z.string().min(10).max(500),
|
||||
descriptionEn: z.string().max(500).optional(),
|
||||
imageUrl: z.string().url(),
|
||||
sourceUrl: z.string().url().optional(),
|
||||
});
|
||||
```
|
||||
|
||||
### POST /api/events (n8n 调用)
|
||||
|
||||
**认证**:
|
||||
```typescript
|
||||
const apiKey = request.headers.get('X-API-Key');
|
||||
if (!apiKey || !crypto.timingSafeEqual(
|
||||
Buffer.from(apiKey),
|
||||
Buffer.from(process.env.WEBHOOK_API_KEY!)
|
||||
)) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
```
|
||||
|
||||
**验证**:
|
||||
```typescript
|
||||
const body = await request.json();
|
||||
const validationResult = AIEventInputSchema.array().safeParse(body);
|
||||
if (!validationResult.success) {
|
||||
return NextResponse.json({
|
||||
error: 'Validation failed',
|
||||
details: validationResult.error
|
||||
}, { status: 400 });
|
||||
}
|
||||
```
|
||||
|
||||
**创建**:
|
||||
```typescript
|
||||
const events = await prisma.aIEvent.createMany({
|
||||
data: validationResult.data,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
return NextResponse.json({ created: events.count }, { status: 201 });
|
||||
```
|
||||
|
||||
### GET /api/events (前端调用)
|
||||
|
||||
支持查询参数:
|
||||
- `?year=2024` - 筛选特定年份
|
||||
- `?limit=50` - 限制返回数量
|
||||
- `?offset=0` - 分页偏移
|
||||
|
||||
返回按 `eventDate` 降序排列的事件列表。
|
||||
|
||||
### 调用示例
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-domain.com/api/events \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_WEBHOOK_API_KEY" \
|
||||
-d '{
|
||||
"title": "GPT-4 发布",
|
||||
"eventDate": "2023-03-14T00:00:00Z",
|
||||
"description": "OpenAI 发布多模态大语言模型",
|
||||
"imageUrl": "https://example.com/gpt4.jpg"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 前端页面实现
|
||||
|
||||
### 页面结构
|
||||
|
||||
`src/app/[locale]/timeline/page.tsx`:
|
||||
|
||||
```tsx
|
||||
import { getAIEvents } from '@/hooks/useAIEvents';
|
||||
import { TimelineSection } from '@/components/timeline/TimelineSection';
|
||||
|
||||
export const revalidate = 3600; // ISR 1小时
|
||||
|
||||
export default async function TimelinePage() {
|
||||
const events = await getAIEvents();
|
||||
|
||||
// 按年份分组
|
||||
const eventsByYear = events.reduce((acc, event) => {
|
||||
const year = new Date(event.eventDate).getFullYear();
|
||||
if (!acc[year]) acc[year] = [];
|
||||
acc[year].push(event);
|
||||
return acc;
|
||||
}, {} as Record<number, typeof events>);
|
||||
|
||||
return (
|
||||
<div className="timeline-container">
|
||||
<header>
|
||||
<h1>AI 发展时间轴</h1>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{Object.entries(eventsByYear)
|
||||
.sort(([a], [b]) => Number(b) - Number(a)) // 降序
|
||||
.map(([year, yearEvents]) => (
|
||||
<TimelineSection
|
||||
key={year}
|
||||
year={year}
|
||||
events={yearEvents}
|
||||
/>
|
||||
))}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 数据获取函数
|
||||
|
||||
`src/hooks/useAIEvents.ts`:
|
||||
|
||||
```typescript
|
||||
export async function getAIEvents() {
|
||||
const events = await prisma.aIEvent.findMany({
|
||||
orderBy: { eventDate: 'desc' },
|
||||
take: 100, // 最多 100 条
|
||||
});
|
||||
return events;
|
||||
}
|
||||
```
|
||||
|
||||
### 组件设计
|
||||
|
||||
- **TimelineSection**: 渲染单个年份的卡片堆叠区域,复用原型的动画效果
|
||||
- **EventCard**: 单个事件卡片(图片、标题、描述、日期)
|
||||
- **响应式**: 移动端垂直堆叠,桌面端横向卡片堆叠
|
||||
|
||||
---
|
||||
|
||||
## 5. n8n Workflow 设计
|
||||
|
||||
### 流程 A: 历史数据初始化(一次性运行)
|
||||
|
||||
**1. 历史搜索 Agent**
|
||||
- 按年份批量搜索: 2017-2025
|
||||
- 搜索关键词: `"AI breakthrough YEAR"`, `"LLM release YEAR"`, `"GPT model YEAR"`
|
||||
- 时间范围: 每年 1月1日 - 12月31日
|
||||
- 输出: 每年 50-100 条搜索结果
|
||||
|
||||
**2. 历史筛选 Agent**
|
||||
- 权威来源: `['arxiv.org', 'openai.com', 'anthropic.com', 'google.ai', 'meta.ai', 'deepmind.com']`
|
||||
- 去重逻辑: 相同标题或 URL 只保留一个
|
||||
- 输出: 每年精选 10-20 个事件
|
||||
|
||||
**3. 历史格式化 Agent**
|
||||
- 提取完整信息: 标题、准确日期、详细描述、配图
|
||||
- 对描述进行润色和本地化(翻译成中文)
|
||||
- 补充缺失图片(使用 Unsplash 或项目官网截图)
|
||||
|
||||
**4. HTTP Request: 批量提交**
|
||||
- POST `/api/events`
|
||||
- 每次提交 10-20 个事件(按年份分批)
|
||||
- 记录成功/失败状态
|
||||
|
||||
### 流程 B: 增量更新(每周运行)
|
||||
|
||||
**触发器**: Cron node,每周一早上 9:00
|
||||
|
||||
**1. 增量搜索 Agent**
|
||||
- 时间范围: 最近 7 天
|
||||
- 搜索关键词: `["AI news", "LLM release", "model launch"]`
|
||||
- 输出: 10-20 条最新搜索结果
|
||||
|
||||
**2. 增量筛选 Agent**
|
||||
- 权威来源检查(同历史流程)
|
||||
- 额外检查: 查询数据库避免重复 `sourceUrl`
|
||||
- 输出: 2-5 个新事件
|
||||
|
||||
**3. 增量格式化 Agent**
|
||||
- 快速格式化: 提取核心信息
|
||||
- 描述保持简短(直接使用摘要)
|
||||
- 图片优先使用新闻配图
|
||||
|
||||
**4. HTTP Request: 增量提交**
|
||||
- POST `/api/events`
|
||||
- 一次性提交所有新事件
|
||||
- 失败时发送告警邮件
|
||||
|
||||
### 错误处理
|
||||
|
||||
- **重试策略**: 每个 Agent 失败后等待 5s/10s/20s 重试,最多 3 次
|
||||
- **降级处理**: 图片提取失败使用默认图片; 日期模糊使用当月 1 日
|
||||
- **告警机制**: 最终失败时发送邮件并附执行日志
|
||||
- **部分成功**: 批量提交时即使部分失败,也记录成功的事件
|
||||
|
||||
---
|
||||
|
||||
## 6. 数据验证和错误处理
|
||||
|
||||
### API 端点保护
|
||||
|
||||
**认证机制**: 使用 `crypto.timingSafeEqual` 防止时序攻击
|
||||
|
||||
**请求体验证**: Zod schema 验证所有字段
|
||||
|
||||
**去重逻辑**: 基于 `sourceUrl` 的唯一索引(如果提供)
|
||||
|
||||
**长度限制**:
|
||||
- 标题 ≤200 字符
|
||||
- 描述 ≤500 字符
|
||||
|
||||
**必填字段**: `title`, `eventDate`, `description`, `imageUrl`
|
||||
|
||||
**URL 验证**: `imageUrl` 和 `sourceUrl` 必须是有效的 HTTP/HTTPS URL
|
||||
|
||||
### 前端错误边界
|
||||
|
||||
```tsx
|
||||
export default async function TimelinePage() {
|
||||
try {
|
||||
const events = await getAIEvents();
|
||||
if (!events.length) {
|
||||
return <EmptyState message="暂无事件数据" />;
|
||||
}
|
||||
return <TimelineContent events={events} />;
|
||||
} catch (error) {
|
||||
console.error('Failed to load events:', error);
|
||||
return <ErrorState message="加载失败,请稍后重试" />;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试策略
|
||||
|
||||
### 单元测试 (Vitest)
|
||||
|
||||
**API 端点测试** (`src/app/api/events/route.test.ts`):
|
||||
```typescript
|
||||
describe('POST /api/events', () => {
|
||||
it('should create event with valid data', async () => {
|
||||
const response = await POST(request_mock);
|
||||
expect(response.status).toBe(201);
|
||||
});
|
||||
|
||||
it('should reject invalid API key', async () => {
|
||||
const response = await POST(bad_request_mock);
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
|
||||
it('should validate required fields', async () => {
|
||||
const response = await POST(invalid_data_mock);
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**数据验证测试** (`src/lib/validations.test.ts`):
|
||||
```typescript
|
||||
describe('AIEventInputSchema', () => {
|
||||
it('should validate valid event', () => {
|
||||
expect(() => AIEventInputSchema.parse(validEvent)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should reject missing title', () => {
|
||||
expect(() => AIEventInputSchema.parse({ ...validEvent, title: '' }))
|
||||
.toThrow();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### E2E 测试 (chrome-devtools-mcp)
|
||||
|
||||
**测试流程**:
|
||||
1. 启动开发服务器: `pnpm dev`
|
||||
2. 使用 chrome-devtools-mcp 工具:
|
||||
- `new_page`: http://localhost:3000/timeline
|
||||
- `take_snapshot`: 验证页面结构
|
||||
- `take_screenshot`: 对比设计原型
|
||||
- `evaluate_script`: 检查事件数据渲染
|
||||
- `list_console_messages`: 确保无 JavaScript 错误
|
||||
3. 测试筛选功能:
|
||||
- `navigate_page`: url=/timeline?year=2024
|
||||
- `take_snapshot`: 验证只显示 2024 年事件
|
||||
|
||||
**手动测试清单**:
|
||||
- [ ] 页面加载成功,无 console 错误
|
||||
- [ ] 事件按年份正确分组显示
|
||||
- [ ] 卡片堆叠动画正常工作
|
||||
- [ ] 响应式布局在移动端正常
|
||||
- [ ] ISR 缓存在 1 小时后正确更新
|
||||
|
||||
### 集成测试 (n8n Workflow)
|
||||
|
||||
- 手动触发历史数据初始化流程,验证数据库事件创建
|
||||
- 手动触发增量更新流程,验证新事件添加
|
||||
- 测试错误场景: API 不可用、数据格式错误、网络超时
|
||||
|
||||
### 测试数据准备
|
||||
|
||||
使用 `pnpm prisma db seed` 创建种子数据:
|
||||
- Seed 脚本插入 5-10 个示例事件(2023-2025 真实事件)
|
||||
- 方便开发和手动测试
|
||||
|
||||
---
|
||||
|
||||
## 8. 部署和实施步骤
|
||||
|
||||
### 阶段 1: 数据库和后端 (1-2 天)
|
||||
|
||||
```bash
|
||||
# 更新 schema
|
||||
vim prisma/schema.prisma
|
||||
|
||||
# 生成并运行迁移
|
||||
pnpm prisma migrate dev --name add_ai_events_table
|
||||
pnpm prisma generate
|
||||
|
||||
# 创建验证 schema
|
||||
vim src/lib/validations.ts
|
||||
|
||||
# 创建 API route
|
||||
mkdir src/app/api/events
|
||||
vim src/app/api/events/route.ts
|
||||
|
||||
# 创建数据获取函数
|
||||
vim src/hooks/useAIEvents.ts
|
||||
|
||||
# 手动测试 API
|
||||
curl -X POST http://localhost:3000/api/events \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: $WEBHOOK_API_KEY" \
|
||||
-d '{
|
||||
"title": "测试事件",
|
||||
"eventDate": "2025-01-25T00:00:00Z",
|
||||
"description": "这是一个测试事件",
|
||||
"imageUrl": "https://example.com/image.jpg"
|
||||
}'
|
||||
```
|
||||
|
||||
### 阶段 2: 前端页面 (2-3 天)
|
||||
|
||||
```bash
|
||||
# 创建 timeline 页面
|
||||
mkdir -p src/app/[locale]/timeline
|
||||
vim src/app/[locale]/timeline/page.tsx
|
||||
|
||||
# 创建组件
|
||||
mkdir src/components/timeline
|
||||
vim src/components/timeline/TimelineSection.tsx
|
||||
vim src/components/timeline/EventCard.tsx
|
||||
|
||||
# 更新 header 添加导航链接
|
||||
vim src/components/layout/Header.tsx
|
||||
|
||||
# 使用 chrome-devtools-mcp 测试页面效果
|
||||
```
|
||||
|
||||
### 阶段 3: n8n Workflow 配置 (1-2 天)
|
||||
|
||||
1. 在 n8n 中创建历史数据初始化 workflow
|
||||
2. 创建增量更新 workflow,设置每周一 9:00 触发
|
||||
3. 配置环境变量 `WEBHOOK_API_KEY`
|
||||
4. 手动运行历史 workflow,初始化 2017-2025 数据
|
||||
5. 验证增量 workflow 是否正常工作
|
||||
|
||||
### 阶段 4: 测试和优化 (1 天)
|
||||
|
||||
1. 使用 chrome-devtools-mcp 进行完整测试
|
||||
2. 修复发现的问题
|
||||
3. 优化性能(ISR 缓存、图片加载)
|
||||
4. 准备生产环境部署
|
||||
|
||||
### 阶段 5: 部署到生产
|
||||
|
||||
1. 更新 Vercel 环境变量(确保 `WEBHOOK_API_KEY` 已设置)
|
||||
2. 运行数据库迁移(如果需要): `pnpm prisma db push`
|
||||
3. 部署代码到 Vercel
|
||||
4. 在 n8n 中更新 API endpoint 为生产地址
|
||||
5. 监控第一次增量运行
|
||||
|
||||
---
|
||||
|
||||
## 9. 关键设计决策
|
||||
|
||||
### 为什么选择基础版事件数据?
|
||||
- 符合 YAGNI 原则,快速上线
|
||||
- 避免过度设计,聚焦核心价值
|
||||
- 后续可扩展(如添加用户互动、点赞等)
|
||||
|
||||
### 为什么不需要标签系统?
|
||||
- 从 UI 原型看,卡片只显示核心信息(标题、日期、描述、图片)
|
||||
- 时间轴本身就是按年份组织,无需额外分类
|
||||
- 减少数据复杂度,提升性能
|
||||
|
||||
### 为什么设计两套 n8n 流程?
|
||||
- **历史流程**: 注重数据质量和完整性,处理 2017-2025 的大量数据
|
||||
- **增量流程**: 注重效率和及时性,每周自动更新
|
||||
- 分离关注点,便于独立优化和调试
|
||||
|
||||
### 为什么使用 ISR 而非纯静态?
|
||||
- 事件数据每周更新,需要一定时效性
|
||||
- ISR 1小时缓存平衡了性能和新鲜度
|
||||
- 避免每次请求都查询数据库
|
||||
|
||||
---
|
||||
|
||||
## 10. 后续优化方向
|
||||
|
||||
1. **搜索和筛选**: 添加关键词搜索、标签筛选
|
||||
2. **用户互动**: 点赞、评论、分享功能
|
||||
3. **相关推荐**: 基于事件标签推荐相关项目
|
||||
4. **多语言支持**: 完善英文版本的描述和翻译
|
||||
5. **图片优化**: 使用 Next.js Image 组件优化图片加载
|
||||
6. **数据可视化**: 添加图表展示 AI 发展趋势
|
||||
7. **导出功能**: 支持导出时间轴为 PDF/Markdown
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,284 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Agent Park - AI History Timeline Pinboard</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com" rel="preconnect"/>
|
||||
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400&family=Syne:wght@400;700;800&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Round" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: "#FFDE03",
|
||||
secondary: "#FF90E8",
|
||||
"background-light": "#F9F7F2",
|
||||
"background-dark": "#121212",
|
||||
"surface-light": "#FFFFFF",
|
||||
"surface-dark": "#1E1E1E",
|
||||
},
|
||||
fontFamily: {
|
||||
display: ["'Syne'", "sans-serif"],
|
||||
mono: ["'Space Mono'", "monospace"],
|
||||
},
|
||||
boxShadow: {
|
||||
'hard': '4px 4px 0px 0px #000000',
|
||||
'hard-hover': '8px 8px 0px 0px #000000',
|
||||
'hard-dark': '4px 4px 0px 0px #FFFFFF',
|
||||
'card-stack': '2px 2px 0px 0px #000, 4px 4px 0px 0px #000, 6px 6px 0px 0px #000',
|
||||
},
|
||||
backgroundImage: {
|
||||
'grid-pattern': "linear-gradient(to right, #e5e5e5 1px, transparent 1px), linear-gradient(to bottom, #e5e5e5 1px, transparent 1px)",
|
||||
'grid-pattern-dark': "linear-gradient(to right, #333 1px, transparent 1px), linear-gradient(to bottom, #333 1px, transparent 1px)",
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style type="text/tailwindcss">
|
||||
@layer utilities {
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #F9F7F2;
|
||||
border-left: 2px solid black;
|
||||
}
|
||||
.dark ::-webkit-scrollbar-track {
|
||||
background: #121212;
|
||||
border-left: 2px solid white;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #FFDE03;
|
||||
border: 2px solid black;
|
||||
}
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: #FFDE03;
|
||||
border: 2px solid white;
|
||||
}
|
||||
.tape {
|
||||
background-color: rgba(255, 255, 255, 0.4);
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||
backdrop-filter: blur(2px);
|
||||
border: 1px solid rgba(255,255,255,0.6);
|
||||
}.stack-card {
|
||||
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}.stack-card:hover {
|
||||
z-index: 50 !important;
|
||||
transform: translateY(-20px) scale(1.05) rotate(0deg) !important;
|
||||
margin-right: 20px !important;
|
||||
margin-left: 20px !important;
|
||||
}.timeline-path {
|
||||
stroke-dasharray: 20 10;
|
||||
animation: dash 60s linear infinite;
|
||||
}
|
||||
@keyframes dash {
|
||||
to {
|
||||
stroke-dashoffset: -1000;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background-light dark:bg-background-dark text-black dark:text-gray-100 font-mono overflow-x-hidden selection:bg-secondary selection:text-black">
|
||||
<div class="fixed top-0 left-0 w-full z-50 border-b-2 border-black dark:border-white">
|
||||
<div class="bg-primary py-2 px-4 text-center border-b-2 border-black">
|
||||
<p class="text-xs font-bold tracking-widest uppercase flex items-center justify-center gap-2">
|
||||
<span class="material-icons-round text-sm">auto_awesome</span>
|
||||
Welcome to Agent Park: The Evolution of Intelligence
|
||||
<span class="material-icons-round text-sm">arrow_forward</span>
|
||||
</p>
|
||||
</div>
|
||||
<nav class="bg-background-light dark:bg-background-dark px-6 py-4 flex justify-between items-center transition-colors duration-300">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-black dark:bg-white text-white dark:text-black flex items-center justify-center border-2 border-transparent">
|
||||
<span class="material-icons-round text-2xl">smart_toy</span>
|
||||
</div>
|
||||
<span class="font-display font-black text-2xl tracking-tighter uppercase">Agent Park</span>
|
||||
</div>
|
||||
<div class="hidden md:flex gap-8 text-sm font-bold">
|
||||
<a class="hover:text-primary transition-colors uppercase decoration-2 underline-offset-4 hover:underline" href="#">Timeline</a>
|
||||
<a class="hover:text-primary transition-colors uppercase decoration-2 underline-offset-4 hover:underline" href="#">Agents</a>
|
||||
<a class="hover:text-primary transition-colors uppercase decoration-2 underline-offset-4 hover:underline" href="#">About</a>
|
||||
</div>
|
||||
<button class="w-10 h-10 border-2 border-black dark:border-white flex items-center justify-center hover:bg-black hover:text-white dark:hover:bg-white dark:hover:text-black transition-all shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] dark:shadow-[2px_2px_0px_0px_rgba(255,255,255,1)] active:translate-y-[2px] active:shadow-none" onclick="document.documentElement.classList.toggle('dark')">
|
||||
<span class="material-icons-round text-lg">light_mode</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="fixed inset-0 pointer-events-none z-0 opacity-10 dark:opacity-20 overflow-hidden">
|
||||
<div class="absolute inset-0 bg-[size:40px_40px] bg-grid-pattern dark:bg-grid-pattern-dark"></div>
|
||||
</div>
|
||||
<div class="relative z-10 pt-32 pb-20 px-4 max-w-[1600px] mx-auto">
|
||||
<header class="text-center mb-20 relative">
|
||||
<div class="inline-block relative">
|
||||
<svg class="absolute -top-6 -left-8 w-[120%] h-[150%] text-primary opacity-80 -z-10 animate-pulse" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M44.7,-51.2C57.1,-41.5,66.1,-27.6,68.9,-12.8C71.7,2,68.3,17.7,60.4,30.9C52.5,44.1,40.1,54.8,26.4,60.1C12.7,65.4,-2.3,65.3,-17.1,61.1C-31.9,56.9,-46.5,48.6,-56.3,36.4C-66.1,24.2,-71.1,8.1,-67.3,-5.7C-63.5,-19.5,-50.9,-31,-38.7,-40.8C-26.5,-50.6,-14.7,-58.7,0.1,-58.8C14.9,-59,29.8,-51.2,32.3,-60.9" fill="currentColor" transform="translate(100 100)"></path>
|
||||
</svg>
|
||||
<h1 class="font-display font-black text-6xl md:text-8xl tracking-tight leading-none text-black dark:text-white drop-shadow-sm">
|
||||
THE STORY <br/> OF A.I.
|
||||
</h1>
|
||||
</div>
|
||||
<p class="mt-6 text-lg md:text-xl font-mono max-w-2xl mx-auto bg-white dark:bg-black border border-black dark:border-white p-2 rotate-1 inline-block shadow-[4px_4px_0px_0px_#000] dark:shadow-[4px_4px_0px_0px_#fff]">
|
||||
Pinned. Stacked. Zigzagged.
|
||||
</p>
|
||||
</header>
|
||||
<main class="relative px-4 md:px-12">
|
||||
<div class="relative w-full">
|
||||
<div class="absolute inset-0 pointer-events-none hidden md:block -z-10" style="top: 15rem; bottom: 15rem;">
|
||||
<div class="absolute top-0 left-0 right-[4rem] h-4 bg-black dark:bg-white"></div>
|
||||
<div class="absolute top-0 right-[4rem] bottom-0 w-4 bg-black dark:bg-white"></div>
|
||||
<div class="absolute bottom-0 right-[4rem] left-0 h-4 bg-black dark:bg-white"></div>
|
||||
</div>
|
||||
<div class="relative min-h-[500px] mb-32 flex flex-col md:flex-row items-center md:items-start md:justify-start pt-20">
|
||||
<div class="absolute left-0 -top-8 z-20">
|
||||
<div class="bg-primary border-4 border-black px-6 py-2 -rotate-2 shadow-hard">
|
||||
<span class="font-display font-black text-3xl md:text-5xl">2024 — 2017</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full pl-0 md:pl-8 pr-0 md:pr-32 flex flex-nowrap overflow-x-visible items-center justify-start py-10 perspective-1000">
|
||||
<div class="stack-card relative w-72 h-96 flex-shrink-0 bg-surface-light dark:bg-surface-dark border-4 border-black dark:border-white p-4 shadow-hard -mr-48 md:-mr-56 z-30 rotate-[-2deg]">
|
||||
<div class="tape absolute -top-3 left-1/2 -translate-x-1/2 w-20 h-6 rotate-1"></div>
|
||||
<div class="h-40 bg-primary border-2 border-black dark:border-white mb-4 flex items-center justify-center overflow-hidden">
|
||||
<span class="material-icons-round text-6xl opacity-50">smart_toy</span>
|
||||
</div>
|
||||
<h3 class="font-display font-bold text-xl leading-none mb-2 uppercase">The Agent Era</h3>
|
||||
<p class="text-xs leading-snug opacity-80 line-clamp-4">Autonomous agents begin to populate the web. They plan, execute, and iterate. It's no longer just chat; it's action.</p>
|
||||
<div class="absolute bottom-4 right-4 text-[10px] font-bold bg-black text-white px-2">FIG 1.2</div>
|
||||
</div>
|
||||
<div class="stack-card relative w-72 h-96 flex-shrink-0 bg-secondary border-4 border-black dark:border-white p-4 shadow-hard -mr-48 md:-mr-56 z-20 rotate-[3deg]">
|
||||
<div class="tape absolute -top-4 right-8 w-24 h-8 rotate-3"></div>
|
||||
<div class="h-40 bg-white dark:bg-black border-2 border-black dark:border-white mb-4 flex items-center justify-center">
|
||||
<div class="grid grid-cols-2 gap-2 p-2 w-full h-full opacity-60">
|
||||
<div class="bg-black/10 dark:bg-white/10 rounded-full"></div>
|
||||
<div class="bg-black/10 dark:bg-white/10"></div>
|
||||
<div class="bg-black/10 dark:bg-white/10"></div>
|
||||
<div class="bg-black/10 dark:bg-white/10 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="font-display font-bold text-xl leading-none mb-2 uppercase">Multimodal</h3>
|
||||
<p class="text-xs leading-snug opacity-80 line-clamp-4">Vision, audio, and text merge into single unified models. AI now perceives the world as humans do.</p>
|
||||
<div class="absolute bottom-4 right-4 text-[10px] font-bold bg-black text-white px-2">FIG 1.1</div>
|
||||
</div>
|
||||
<div class="stack-card relative w-72 h-96 flex-shrink-0 bg-white dark:bg-gray-800 border-4 border-black dark:border-white p-4 shadow-hard -mr-48 md:-mr-56 z-10 rotate-[-1deg]">
|
||||
<div class="tape absolute -top-4 left-4 w-24 h-8 -rotate-2 bg-yellow-200/50"></div>
|
||||
<div class="relative border-2 border-black dark:border-white bg-gray-100 h-40 mb-4 overflow-hidden group">
|
||||
<img alt="Abstract AI Art" class="w-full h-full object-cover grayscale mix-blend-multiply group-hover:scale-110 transition-transform duration-500" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBd0bidYXReahZeHaO8_otbsOwtu_ky-BZEt5qAwkvMWUZayFj_lhA0WO4mW5rAg-BkCOnkGlBQMl_Vmce-cXrqLih3nxlly6yB7NHzSWgS_TiujzG79dYtRIdbhizZCFbTGu8fLVCwSrBmjr7ockX90-T8EA5xYet5tJf5Ip0xucPUUyusI0GmF6d99lc6csGj3Puqk2P8QSRwZEDgOvskQ2I_60FS9ZXGIK9tlhTzajIdPup4Kaz9d04o7-4MQpDMSOjbFR-rkUw"/>
|
||||
</div>
|
||||
<h3 class="font-display font-bold text-xl leading-none mb-2 uppercase">Generative Explosion</h3>
|
||||
<p class="text-xs leading-snug opacity-80 line-clamp-4">ChatGPT launches. The world changes overnight. LLMs become household utilities.</p>
|
||||
<div class="absolute bottom-4 right-4 text-[10px] font-bold bg-black text-white px-2">FIG 1.0</div>
|
||||
</div>
|
||||
<div class="stack-card relative w-72 h-96 flex-shrink-0 bg-primary border-4 border-black dark:border-white p-4 shadow-hard -mr-48 md:-mr-56 z-0 rotate-[2deg]">
|
||||
<div class="tape absolute -top-3 right-1/2 translate-x-1/2 w-16 h-8 rotate-0"></div>
|
||||
<div class="h-40 bg-surface-light dark:bg-surface-dark border-2 border-black dark:border-white mb-4 flex items-center justify-center">
|
||||
<span class="font-mono text-4xl font-black tracking-tighter">T</span>
|
||||
</div>
|
||||
<h3 class="font-display font-bold text-xl leading-none mb-2 uppercase">The Transformer</h3>
|
||||
<p class="text-xs leading-snug opacity-80 line-clamp-4">"Attention Is All You Need". The paper that killed RNNs and birthed the GPT architecture.</p>
|
||||
<div class="absolute bottom-4 right-4 text-[10px] font-bold bg-black text-white px-2">FIG 0.9</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative min-h-[500px] flex flex-col md:flex-row-reverse items-center md:items-start md:justify-start pt-20">
|
||||
<div class="absolute right-0 -top-8 z-20">
|
||||
<div class="bg-secondary border-4 border-black px-6 py-2 rotate-2 shadow-hard">
|
||||
<span class="font-display font-black text-3xl md:text-5xl">2016 — 1950</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full pl-0 md:pl-32 pr-0 md:pr-8 flex flex-nowrap flex-row-reverse overflow-x-visible items-center justify-start py-10 perspective-1000">
|
||||
<div class="stack-card relative w-72 h-96 flex-shrink-0 bg-surface-light dark:bg-surface-dark border-4 border-black dark:border-white p-4 shadow-hard -ml-48 md:-ml-56 z-30 rotate-[2deg]">
|
||||
<div class="tape absolute -top-4 right-4 w-16 h-8 rotate-12 bg-red-400/30"></div>
|
||||
<div class="relative border-2 border-black dark:border-white h-40 mb-4 overflow-hidden">
|
||||
<img alt="Go Board Game" class="w-full h-full object-cover grayscale contrast-125" src="https://lh3.googleusercontent.com/aida-public/AB6AXuD-xRSKl7qyBPowfXI_vdiJvvkVuLPFBQNsKpb0-4D_hlXpGHzNYv08vInYTw3Gt56PR_iHA6_ZbDL6a_veUQk4UZpwMa3_LnVJKVujkxpgKUVQCpfXmSImXaWqJuPHbmXGQF5yPiA00cYl3tHGrjOHekWDBGFJo7HboDYGfhne8CKhvcJhzfoAqqydkxOrvE6Jv_sfIVeI9rB_StLXI_4BmkAPGBgo4Dj4-x3Kignm-pBGEORawu_taKEhLLftAQ1ospTyZb3ouN4"/>
|
||||
<div class="absolute inset-0 bg-primary mix-blend-multiply opacity-30"></div>
|
||||
</div>
|
||||
<h3 class="font-display font-bold text-xl leading-none mb-2 uppercase">Move 37</h3>
|
||||
<p class="text-xs leading-snug opacity-80 line-clamp-4">AlphaGo defeats Lee Sedol. A move of "inhuman" intuition that shocked the world.</p>
|
||||
<div class="absolute bottom-4 right-4 text-[10px] font-bold bg-black text-white px-2">FIG 0.8</div>
|
||||
</div>
|
||||
<div class="stack-card relative w-72 h-96 flex-shrink-0 bg-black border-4 border-black dark:border-white p-4 shadow-hard -ml-48 md:-ml-56 z-20 rotate-[-3deg]">
|
||||
<div class="tape absolute -top-4 left-8 w-20 h-6 -rotate-3 bg-white/20"></div>
|
||||
<div class="h-40 bg-blue-800 border-2 border-white mb-4 flex items-center justify-center">
|
||||
<span class="material-icons-round text-white text-6xl">grid_on</span>
|
||||
</div>
|
||||
<h3 class="font-display font-bold text-xl text-white leading-none mb-2 uppercase">Deep Blue</h3>
|
||||
<p class="text-xs leading-snug text-gray-300 line-clamp-4">IBM's machine defeats Kasparov. Brute force calculation triumphs over human strategy.</p>
|
||||
<div class="absolute bottom-4 right-4 text-[10px] font-bold bg-white text-black px-2">FIG 0.5</div>
|
||||
</div>
|
||||
<div class="stack-card relative w-72 h-96 flex-shrink-0 bg-gray-200 dark:bg-gray-800 border-4 border-black dark:border-white p-4 shadow-hard -ml-48 md:-ml-56 z-10 rotate-[1deg]">
|
||||
<div class="tape absolute -top-3 right-1/2 translate-x-1/2 w-24 h-8 rotate-2 bg-blue-200/40"></div>
|
||||
<div class="h-40 border-2 border-black dark:border-white border-dashed mb-4 flex items-center justify-center opacity-50">
|
||||
<span class="material-icons-round text-5xl">ac_unit</span>
|
||||
</div>
|
||||
<h3 class="font-display font-bold text-xl leading-none mb-2 uppercase">The Winter</h3>
|
||||
<p class="text-xs leading-snug opacity-80 line-clamp-4">Funding dries up. The promises of early AI fail to materialize. Research goes underground.</p>
|
||||
<div class="absolute bottom-4 right-4 text-[10px] font-bold bg-black text-white px-2">FIG 0.2</div>
|
||||
</div>
|
||||
<div class="stack-card relative w-72 h-96 flex-shrink-0 bg-secondary border-4 border-black dark:border-white p-4 shadow-hard -ml-48 md:-ml-56 z-0 rotate-[-2deg]">
|
||||
<div class="tape absolute -top-4 right-4 w-20 h-6 -rotate-6"></div>
|
||||
<div class="relative border-2 border-dashed border-black dark:border-white mb-4 h-40 bg-white dark:bg-black flex items-center justify-center">
|
||||
<svg class="w-16 h-16 text-black dark:text-white" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" viewBox="0 0 24 24">
|
||||
<rect height="10" rx="2" width="18" x="3" y="11"></rect>
|
||||
<circle cx="12" cy="5" r="2"></circle>
|
||||
<path d="M12 7v4"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="font-display font-bold text-xl leading-none mb-2 uppercase">Imitation Game</h3>
|
||||
<p class="text-xs leading-snug opacity-80 line-clamp-4">Turing's question: "Can machines think?" The philosophical start of it all.</p>
|
||||
<div class="absolute bottom-4 right-4 text-[10px] font-bold bg-black text-white px-2">FIG 0.1</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<div class="mt-40 max-w-3xl mx-auto px-4">
|
||||
<div class="bg-primary border-4 border-black p-8 relative shadow-[12px_12px_0px_0px_#000] dark:shadow-[12px_12px_0px_0px_#fff]">
|
||||
<div class="absolute -top-10 -right-6 w-20 h-20 bg-white dark:bg-gray-800 border-4 border-black flex items-center justify-center rounded-full animate-bounce">
|
||||
<span class="material-icons-round text-4xl text-black dark:text-white">mail</span>
|
||||
</div>
|
||||
<h2 class="font-display font-black text-3xl md:text-5xl mb-4 text-black uppercase tracking-tight">Join the Park</h2>
|
||||
<p class="font-mono text-black mb-8 text-base font-bold">
|
||||
Subscribe to the Agent Park weekly zine. No spam, just ducks and data.
|
||||
</p>
|
||||
<form class="flex flex-col md:flex-row gap-4">
|
||||
<input class="flex-1 bg-white border-4 border-black px-6 py-4 font-mono focus:ring-0 focus:border-black focus:shadow-[4px_4px_0px_0px_#000] transition-all placeholder:text-gray-500 text-black text-lg" placeholder="Your email here..." type="email"/>
|
||||
<button class="bg-black text-white px-10 py-4 font-black border-4 border-transparent hover:bg-white hover:text-black hover:border-black transition-all hover:shadow-[6px_6px_0px_0px_#000] uppercase text-lg" type="button">
|
||||
SUBSCRIBE
|
||||
</button>
|
||||
</form>
|
||||
<div class="mt-6 flex items-center gap-3">
|
||||
<input class="w-6 h-6 border-4 border-black text-black focus:ring-0 rounded-none bg-white checked:bg-black" id="check" type="checkbox"/>
|
||||
<label class="text-sm font-black text-black uppercase" for="check">I agree to be cool.</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fixed bottom-6 right-6 z-50">
|
||||
<div class="bg-white dark:bg-black border-4 border-black dark:border-white p-3 shadow-hard dark:shadow-hard-dark cursor-pointer hover:-translate-y-2 transition-transform">
|
||||
<span class="material-icons-round text-3xl text-black dark:text-white">arrow_upward</span>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="bg-black dark:bg-white text-white dark:text-black py-16 mt-20 border-t-8 border-primary">
|
||||
<div class="max-w-7xl mx-auto px-6 flex flex-col md:flex-row justify-between items-center gap-12">
|
||||
<div class="text-center md:text-left">
|
||||
<h4 class="font-display font-black text-4xl uppercase tracking-tighter">Agent Park</h4>
|
||||
<p class="font-mono text-sm opacity-60 mt-3">© 2025 Synthetic History Project — Zigzagged and Zined.</p>
|
||||
</div>
|
||||
<div class="flex gap-10">
|
||||
<a class="font-black text-xl hover:text-primary dark:hover:text-primary transition-colors underline decoration-4 underline-offset-8" href="#">Twitter</a>
|
||||
<a class="font-black text-xl hover:text-primary dark:hover:text-primary transition-colors underline decoration-4 underline-offset-8" href="#">GitHub</a>
|
||||
<a class="font-black text-xl hover:text-primary dark:hover:text-primary transition-colors underline decoration-4 underline-offset-8" href="#">Discord</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body></html>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 188 KiB |
@@ -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": []
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{"t":0,"agent":"a37a7c5","agent_type":"unknown","event":"agent_stop","success":true}
|
||||
@@ -1 +0,0 @@
|
||||
{"t":0,"agent":"a8591e0","agent_type":"unknown","event":"agent_stop","success":true}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -1 +0,0 @@
|
||||
{"t":0,"agent":"af530c0","agent_type":"unknown","event":"agent_stop","success":true}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"timestamp": "2026-02-20T13:40:21.901Z",
|
||||
"backgroundTasks": [],
|
||||
"sessionStartTimestamp": "2026-02-20T12:55:14.824Z",
|
||||
"sessionId": "f1da58ef16330f2a"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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 决策)。本计划先按“规模尚可”实现全量日更。
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
@@ -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"
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
+10
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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<string>()
|
||||
|
||||
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, { id: string; slug: string; category: TagCategory }>
|
||||
}): 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<string>();
|
||||
|
||||
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<typeof ProjectTagSelectionByCategorySchema>
|
||||
>;
|
||||
|
||||
categories.forEach((category) => {
|
||||
const selectedSlugs = project.selectedTagSlugsByCategory[category];
|
||||
const seenTagSlugs = new Set<string>();
|
||||
|
||||
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<typeof TagUpdateSchema>;
|
||||
export type MergeTarget = z.infer<typeof MergeTargetSchema>;
|
||||
export type TagMerge = z.infer<typeof TagMergeSchema>;
|
||||
export type TagMaintenanceRequest = z.infer<typeof TagMaintenanceRequestSchema>;
|
||||
export type ResettableTagCategory = z.infer<typeof ResettableTagCategorySchema>;
|
||||
export type ProjectTagSelectionByCategory = z.infer<typeof ProjectTagSelectionByCategorySchema>;
|
||||
export type ProjectTagResetItem = z.infer<typeof ProjectTagResetItemSchema>;
|
||||
export type ProjectTagResetRequest = z.infer<typeof ProjectTagResetRequestSchema>;
|
||||
|
||||
Reference in New Issue
Block a user