chore: 清理历史辅助文件

This commit is contained in:
2026-04-18 18:32:43 +08:00
parent 31b1469e39
commit 8731811e74
25 changed files with 7 additions and 4153 deletions
-67
View File
@@ -1,67 +0,0 @@
# .claude/ - Custom Claude Configuration
<!-- Parent: ../AGENTS.md -->
## OVERVIEW
Custom Claude Code agents, commands, and settings for the agent_park project.
## STRUCTURE
```
.claude/
├── settings.json # Claude Code project settings
├── agents/ # Custom agent definitions
│ ├── content-explorer-agent.md
│ └── api-submitter-agent.md
└── commands/ # Custom slash commands
└── discover-projects.md
```
## CUSTOM AGENTS
### content-explorer-agent
- **Purpose**: Explore GitHub projects and generate structured data
- **Capabilities**: Browser automation, content extraction, JSON generation
- **Output**: ProjectInputSchema-compliant JSON
### api-submitter-agent
- **Purpose**: Submit exploration results to production API
- **Capabilities**: Batch task updates, API submission, retry logic
- **Requires**: `WEBHOOK_API_KEY` environment variable
## CUSTOM COMMANDS
### /discover-projects
Automated project discovery workflow:
```bash
/discover-projects [count] [--batch=N]
```
- Default: 10 tasks, batch size 3
- Coordinates content-explorer + api-submitter agents
## FOR AI AGENTS
### When Adding Agents/Commands
1. Create `.md` file with agent/command definition
2. Follow existing templates (frontmatter + instructions)
3. Test with `/command-name` before committing
### Agent Template Structure
```markdown
---
name: agent-name
description: What this agent does
tools: [Read, Grep, Glob, Bash] # Allowed tools
---
# Role
[Agent role description]
# Instructions
[Detailed instructions]
```
<!-- MANUAL: Additional notes can be added below -->
+6 -2
View File
@@ -64,6 +64,11 @@ temp/
.trending-workspace/
.omc/
.sisyphus/
.claude/
.opencode/
.playwright-mcp/
oh-my-opencode.json
oh-my-opencode.json.backup
# Package manager lock files (optional - uncomment if needed)
# package-lock.json
@@ -86,5 +91,4 @@ dump.rdb
sessions/
*.session
# Claude Code settings (machine-specific)
.claude/settings.json
# Local agent/tooling artifacts
-22
View File
@@ -1,22 +0,0 @@
{
"mcpServers": {
"n8n-mcp": {
"type": "local",
"command": "/opt/homebrew/bin/n8n-mcp",
"args": [],
"env": {
"MCP_MODE": "stdio",
"LOG_LEVEL": "error",
"DISABLE_CONSOLE_OUTPUT": "true",
"N8N_API_URL": "https://n8n.mzaxd.fun",
"N8N_API_KEY": "${N8N_API_KEY}",
"HTTP_PROXY": "",
"HTTPS_PROXY": "",
"http_proxy": "",
"https_proxy": "",
"npm_config_proxy": "",
"npm_config_https_proxy": ""
}
}
}
}
-296
View File
@@ -1,296 +0,0 @@
# PROJECT KNOWLEDGE BASE
**Generated:** 2026-02-01 | **Updated:** 2026-02-20
**Commit:** N/A (new generation)
**Branch:** main
## OVERVIEW
Next.js 15 multilingual AI project navigation website with App Router, TypeScript, Prisma ORM, and Tailwind CSS neo-brutalism design. Features automated project discovery, quarterly AI keyword cloud system, and AI Timeline historical events.
## STRUCTURE
```
agent_park/
├── src/
│ ├── app/ # Next.js App Router (locale-prefixed routes)
│ │ ├── [locale]/ # Internationalized routes (zh/en)
│ │ │ ├── projects/ # Project listing and details
│ │ │ ├── keyword-cloud/ # Quarterly AI trends
│ │ │ └── layout.tsx # Header/footer/Announcement
│ │ ├── api/ # API routes (no locale prefix)
│ │ │ ├── webhook/ # Project ingestion
│ │ │ ├── discovery/ # Automated exploration tasks
│ │ │ ├── search/ # AI-powered search
│ │ │ └── keyword-cloud/ # Keywords API
│ │ ├── components/ # React components (organized by domain)
│ │ ├── lib/ # Validation, utilities, Prisma client
│ │ ├── hooks/ # Server-side data fetching
│ │ └── i18n/ # next-intl configuration
│ └── layout.tsx # Root layout (locale default: zh)
├── prisma/
│ └── schema.prisma # Database models (multilingual support)
├── .claude/
│ ├── agents/ # Custom Claude agents
│ └── commands/ # Claude commands
├── scripts/ # Seed/test scripts
├── public/ # Static assets
└── CLAUDE.md # This file (existing)
```
## WHERE TO LOOK
| Task | Location | Notes |
| ------------------- | ------------------------ | ------------------------------------------- |
| Add new page | `src/app/[locale]/` | Follow locale routing pattern |
| Create API endpoint | `src/app/api/` | Use `crypto.timingSafeEqual()` for auth |
| Add component | `src/components/` | Neo-brutalism: 0px radius, bold borders |
| Database migration | `prisma/schema.prisma` | Run `pnpm prisma migrate dev` after changes |
| Add validation | `src/lib/validations.ts` | Follow existing Zod schema patterns |
| Fetch data | `src/hooks/` | Server functions, NOT React hooks |
| Webhook | `src/app/api/webhook/` | Multi-level deduplication strategy |
| Discovery task | `src/app/api/discovery/` | Task management API |
| i18n messages | `src/messages/` | Update BOTH `zh.json` AND `en.json` |
## CODE MAP
```
Main Entry Points:
├── src/app/[locale]/layout.tsx → Root layout (header/footer)
├── src/app/[locale]/page.tsx → Home page (ISR 5min)
├── src/app/layout.tsx → Root layout (fonts, locale)
└── src/middleware.ts → Locale detection (next-intl)
Data Layer:
├── src/hooks/useProjects.ts → Projects/tags/search
├── src/hooks/useKeywordCloud.ts → Keywords/quarters/rules
└── src/lib/prisma.ts → Prisma client singleton
API Routes:
├── src/app/api/webhook/projects/route.ts → Project ingestion
├── src/app/api/discovery/tasks/route.ts → Task CRUD
├── src/app/api/search/ai/route.ts → AI search
└── src/app/api/keyword-cloud/[...]/route.ts → Keywords API
Complex Components:
├── src/components/project/MarkdownContent.tsx (269 lines) → Markdown renderer
├── src/app/api/discovery/lib/discovery-service.ts → Deduplication logic
└── src/app/api/webhook/projects/route.ts → Webhook handler
```
## CONVENTIONS
### Multilingual Architecture
- **Default locale**: Chinese (`zh`), not English
- **Route pattern**: `/{locale}/path` (locale prefix ALWAYS required)
- **Field naming**: Dual-language pairs (`name`/`nameEn`, `description`/`descriptionEn`)
- **Content selection**: `locale === 'en' && fieldEn ? fieldEn : field`
- **Translation files**: Update BOTH `src/messages/zh.json` AND `src/messages/en.json`
### Type Safety
- **Strict mode**: Enabled with `noUncheckedIndexedAccess`, `noImplicitReturns`, `noFallthroughCasesInSwitch`
- **Path aliases**: `@/*` maps to `./src/*`
- **Validation**: All API inputs via Zod schemas in `src/lib/validations.ts`
### Database Patterns
- **Multi-level deduplication**: GitHub URL → Website URL → slug match (highest → lowest priority)
- **Cascade deletion**: `ExternalLink` and `ProjectTag` auto-delete on project delete
- **Composite indexes**: Optimized for frequent queries (`status+createdAt`, `type+url`)
- **Constraints**: `Project.slug` unique, `Tag.name` unique, `ExternalLink(projectId, url)` unique
### API Authentication
- **Method**: `crypto.timingSafeEqual()` for timing-safe comparison (prevents timing attacks)
- **Key location**: Request body (NOT headers)
- **Validation**: WEBHOOK_API_KEY (32+ chars)
### Design System (Neo-Brutalism)
- **Corners**: 0px radius (sharp)
- **Shadows**: 4px solid borders (`shadow-neo` class)
- **Colors**: Primary gold (#FFD700), secondary orange (#ff6f00)
- **Typography**: Space Mono (headings), Inter (body)
- **Dark mode**: Class-based with `dark:` prefix
### Git Commits
- **Format**: `<type>: <description>` (Chinese lowercase types)
- **Types**: `feat` (新功能), `fix` (修复), `refactor` (重构), `chore` (杂项)
- **Examples**:
- `feat: 新增项目发现任务系统`
- `fix: 修复 ESLint 错误`
## ANTI-PATTERNS (THIS PROJECT)
### Environment Variables
- **NEVER** use `vercel.json` for env vars (use Vercel Dashboard)
- **NEVER** commit `.env.local` files or environment variables
- **ALWAYS** verify API responses with `WEBHOOK_API_KEY` when testing endpoints
### Database Operations
- **NEVER** modify schema without running migrations
- **ALWAYS** run `pnpm prisma migrate dev --name <migration_name>` after schema changes
- **ALWAYS** run `pnpm prisma generate` after migrations
### Development Workflow
- **NEVER** skip pre-commit hooks (no `--no-verify`, `--no-gpg-sign` flags)
- **NEVER** commit unless explicitly requested by orchestrator
- **ALWAYS** run `pnpm build` before committing
- **MUST** use chrome-devtools-mcp to verify frontend changes
### Content Standards
- **NEVER** include dynamic data (star/fork counts) in project content
- **ALWAYS** use GitHub Badge for dynamic stats
- **NEVER** use mechanical translation - write naturally for each locale
- **ALWAYS** avoid marketing terminology in descriptions
### Security
- **NEVER** use string comparison for API keys (timing attacks)
- **ALWAYS** use `crypto.timingSafeEqual()` for authentication
- **ALWAYS** validate inputs with Zod schemas
- **NEVER** log raw API keys or sensitive data
## UNIQUE STYLES
### Project Discovery System
- **Architecture**: Dual-agent system (`content-explorer-agent` + `api-submitter-agent`)
- **Workflow**: URL input → PENDING tasks → batch processing → COMPLETED/FAILED
- **Deduplication**: Multi-level strategy (GitHub → Website → slug)
- **Retry logic**: Exponential backoff, max 3 attempts
- **Environment**: `WEBHOOK_API_KEY` required for submitter agent
- See: `src/app/AGENTS.md` for app-specific patterns
### Keyword Cloud System
- **Workflow**: n8n automation → API → PostgreSQL → Next.js
- **Data**: Quarterly snapshots with visual style rules
- **API**: Rate-limited, requires API key
- **Error logging**: `KeywordCloudErrorLog` table
- See: `src/app/AGENTS.md` for app-specific patterns
### Component System
- **Design**: Neo-brutalism with 0px corners, 4px solid borders, gold/orange theme
- **Organization**: layout/locale/project/search/ui (4 domains), project/ (11 files, highest)
- **Largest component**: `MarkdownContent.tsx` (269 lines) with complex react-markdown overrides
- **See**: `src/components/AGENTS.md` for component-specific patterns
### Data Access Layer
- **Validation**: Centralized Zod schemas in `src/lib/validations.ts`
- **Utilities**: Class merging (`cn()`), slug generation, Prisma client singleton
- **GitHub integration**: Badge-based approach, cached API stats (5min)
- **See**: `src/lib/AGENTS.md` for library patterns
### Server-Side Hooks
- **Purpose**: Server functions for data fetching, NOT React hooks
- **Query optimization**: N+1 prevention, selective loading, index utilization
- **ISR strategy**: 5-minute revalidation on project details
- **See**: `src/hooks/AGENTS.md` for data fetching patterns
## COMMANDS
```bash
pnpm dev # Start dev server (Next.js 15)
pnpm build # Production build
pnpm start # Start production server
pnpm lint # ESLint check
pnpm test # Vitest unit tests
pnpm test:e2e # Playwright E2E tests
pnpm prisma migrate dev # Run migrations
pnpm prisma db seed # Seed database
pnpm prisma studio # Database inspector
```
## NOTES
### Project Discovery Gotchas
- Tag upsert follows: 1) Find by name, 2) Try upsert by slug, 3) Use existing if slug conflicts
- Webhook updates ALL project fields, replaces ALL tags/links (no partial updates)
- Deduplication logic in `src/app/api/discovery/lib/discovery-service.ts:16-80`
### i18n Gotchas
- Missing translation keys display as `missing_key_name` in UI
- All new UI text requires BOTH `zh.json` AND `en.json` updates
- Test both locales: `/{zh}/path` and `/{en}/path`
### Database Gotchas
- `Project.slug` globally unique - use existing project if slug conflicts
- `Tag.name` globally unique - upsert handles conflicts via slug fallback
- Run `pnpm prisma studio` to inspect state during development
### Performance Considerations
- **ISR**: Project detail pages revalidate every 5 minutes (`revalidate = 300`)
- **Indexes**: Composite on `(status, createdAt)`, `(type, url)` for optimized queries
- **N+1 prevention**: Batch tag queries in webhook (`useProjects.ts`)
### Missing CI/CD
- No GitHub Actions workflows (rely on Vercel deployment)
- No automated testing in CI pipeline
- Test frameworks configured (Vitest, Playwright) but not extensively used
### LSP Errors
- Missing Prisma models in `src/hooks/useKeywordCloud.ts` and `scripts/seed-keyword-cloud.ts`:
- Run `pnpm prisma migrate dev` to generate keyword cloud tables
- Models: `Quarter`, `Keyword`, `VisualStyleRule`, `keywordCloudErrorLog`
### Subdirectories
- **src/app/**: App Router architecture, locale routing, API structure (see `src/app/AGENTS.md`)
- **src/components/**: Neo-brutalism design system, component organization (see `src/components/AGENTS.md`)
- **src/lib/**: Validation layer, utilities, GitHub integration (see `src/lib/AGENTS.md`)
- **src/hooks/**: Server-side data fetching, query optimization (see `src/hooks/AGENTS.md`)
## N8N PRODUCTION WORKFLOWS
Production workflows tagged with "生产" in n8n instance:
| Workflow | Status | Nodes | Purpose |
|----------|--------|-------|---------|
| 项目描述向量化 | ✅ Active | 11 | Generate embeddings for project descriptions (RAG) |
| Github项目分析入库 | ⏸️ Inactive | 16 | Analyze GitHub repos and ingest into database |
| RAG项目搜索 | ✅ Active | 6 | AI-powered project search using vector similarity |
| 每日Github Trending项目计划新增 | ✅ Active | 17 | Daily trending repos → discovery tasks |
| Topic项目计划新增 | ⏸️ Inactive | 16 | Topic-based project discovery tasks |
### Workflow Data Flow
```
GitHub Trending/Topic → Discovery Tasks (PENDING)
Content Explorer Agent
Project Ingestion API
向量化 Workflow → Embeddings
RAG项目搜索 ← User Query
```
## AI TIMELINE SYSTEM
Historical AI events tracking system:
- **Model**: `AIEvent` in `prisma/schema.prisma`
- **Fields**: title/titleEn, eventDate, description/descriptionEn, imageUrl, sourceUrl
- **API**: `src/app/api/events/route.ts`
- **Frontend**: `src/app/[locale]/timeline/`
- **Index**: `eventDate` (descending) for chronological queries
<!-- MANUAL: Additional notes can be added below -->
-460
View File
@@ -1,460 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development Commands
### Build & Run
```bash
pnpm dev # Start development server (Next.js 15)
pnpm build # Build for production
pnpm start # Start production server
pnpm lint # Run ESLint
```
### Testing
```bash
pnpm test # Run Vitest unit tests
pnpm test:e2e # Run Playwright E2E tests
```
### Database
```bash
pnpm prisma migrate dev # Run database migrations
pnpm prisma migrate dev --name init # Create initial migration
pnpm prisma db seed # Seed database (uses ts-node)
pnpm prisma studio # Open Prisma Studio for database inspection
```
## Architecture Overview
This is a **Next.js 15 multilingual AI project navigation website** using the App Router architecture with the following key components:
### i18n Architecture (next-intl)
- **Locales**: `zh` (default) and `en`
- **Route pattern**: `/{locale}/path` (always prefixed with locale)
- **Middleware**: `src/middleware.ts` handles locale detection and routing
- **i18n config**: `src/i18n/request.ts` loads locale messages from `src/messages/{locale}.json`
- **Messages**: Translation files at `src/messages/zh.json` and `src/messages/en.json`
### App Router Structure
```
src/app/
├── [locale]/ # Locale-scoped routes
│ ├── page.tsx # Home page
│ ├── projects/ # Projects listing and details
│ │ ├── page.tsx # Projects list
│ │ └── [id]/ # Individual project details (slug-based)
│ └── layout.tsx # Locale layout (header, footer)
├── api/ # API routes (no locale prefix)
│ └── webhook/projects/route.ts # Webhook for project ingestion
└── layout.tsx # Root layout
```
### Database (Prisma + PostgreSQL)
- **Provider**: Neon (serverless PostgreSQL)
- **Schema**: `prisma/schema.prisma` defines models: `Project`, `Tag`, `ExternalLink`, `ProjectTag`
- **Enums**: `ProjectStatus` (ACTIVE/ARCHIVED), `LinkType` (WEBSITE/GITHUB/HUGGINGFACE/PAPER)
- **Client singleton**: `src/lib/prisma.ts` exports Prisma client instance
- **Multilingual fields**: Most models have `name`/`nameEn`, `description`/`descriptionEn`, `content`/`contentEn` pairs
- **Cascade deletions**: `ExternalLink` and `ProjectTag` use `onDelete: Cascade` - deleting a project automatically cleans up its links and tag connections
- **Key constraints**:
- `Project.slug`: Unique
- `Tag.name`: Unique (tag names are globally unique)
- `Tag.slug`: Unique
- `ExternalLink`: `@@unique([projectId, url])` (each project can't have duplicate URLs)
- **Indexes**: `idx_project_status_createdAt`, `idx_project_slug`, `idx_tag_slug`, `idx_link_projectId`, `idx_link_type`, `idx_link_type_url` (composite for efficient URL-based deduplication)
#### Neon Database Setup
- **Dashboard**: https://console.neon.tech
- **Connection String Format**: `postgres://[user]:[password]@[host]/[database]?sslmode=require`
- **Vercel Integration**: Set `DATABASE_URL` environment variable in Vercel Dashboard (do NOT use `vercel.json` env references)
- **Free Tier**: 0.5GB storage, 300 hours compute/month
- **Run Migrations**: After deployment, run `pnpm prisma db push` or use Neon's SQL Editor to create tables
### Webhook Deduplication Strategy
The webhook at `src/app/api/webhook/projects/route.ts` implements a **multi-level deduplication** strategy to prevent duplicate projects:
1. **GitHub URL exact match** (highest priority) - via `ExternalLink` table using `idx_link_type_url` index
2. **Website URL exact match** - via `ExternalLink` table using `idx_link_type_url` index
3. **slug match** (fallback) - via `Project.slug` field
When updating an existing project, the webhook:
- Updates all project fields (name, description, content, status, source)
- Replaces all tags (deletes old `ProjectTag` connections via `ProjectTag` table, creates new ones)
- Replaces all links (deletes old `ExternalLink` entries, creates new ones)
**Tag handling special case**: Due to `Tag.name` unique constraint, tag upsert follows:
1. First try to find existing tag by name
2. If not found, try upsert by slug
3. If slug conflicts, use the existing tag with that slug
### Data Fetching (Server-Side)
- **Location**: `src/hooks/useProjects.ts` (server functions, not React hooks)
- **Functions**: `getProjects()`, `getProjectBySlug()`, `getAllTags()`, `getTagsWithProjectCounts()`
- **Usage**: Directly called in Server Components and route handlers
- **Query transformation**: `getProjects()` and `getProjectBySlug()` flatten the `ProjectTag` junction table structure to return tags directly
- **ISR**: Project detail pages use `export const revalidate = 300` (5 minutes) at `src/app/[locale]/projects/[id]/page.tsx`
### Validation (Zod)
- **Schemas**: `src/lib/validations.ts` defines all Zod schemas
- `ProjectInputSchema`: Validates incoming project data (1-10 tags, 1-10 links required)
- `WebhookPayloadSchema`: Validates webhook requests with API key (1-100 projects per request)
- `ProjectQuerySchema`: Validates query parameters (search, tags, status, page, limit)
### Styling (Tailwind CSS)
- **Neo-brutalism design**: Sharp corners (0px radius), bold borders (4px shadows), hard edges
- **Theme colors**:
- Primary: Gold (#FFD700)
- Secondary: Orange (#ff6f00)
- Background light: #F5F2EB, dark: #121212
- Surface light: #FFFFFF, dark: #1E1E1E
- **Typography**: Space Mono (headings), Inter (body)
- **Dark mode**: Class-based with `dark:` prefix
- **Config**: `tailwind.config.ts` extends theme with custom colors, shadows, and animations
### Content Rendering
- **Markdown**: Project content fields support Markdown via `react-markdown`
- **Plugins**: `rehype-raw`, `rehype-sanitize`, `rehype-shiki`, `remark-gfm`
- **Usage**: `ProjectDetail` component renders `content`/`contentEn` as Markdown
### UI Components
- **Radix UI primitives**: `@radix-ui/react-slot`, `@radix-ui/react-navigation-menu`, `@radix-ui/react-dropdown-menu`, `@radix-ui/react-separator`
- **Lucide React icons**: Used throughout the app (package imports optimized via `experimental.optimizePackageImports`)
- **Custom components**: `src/components/` organized by domain
- `layout/`: Header, Footer, AnnouncementBar
- `locale/`: LocaleSwitcher
- `project/`: ProjectCard, ProjectList, ProjectDetail, ProjectSidebar, RelatedProjects, TagCloud, ExternalLinkCard, ShareButtons, MarkdownContent, GitHubBadges, GitHubTextStatsCard
- `search/`: SearchBar
- `ui/`: Base UI components (buttons, cards, etc.)
### Next.js Configuration
- **next.config.js**:
- `next-intl` plugin wrapper for i18n
- Image domains: localhost, *.anthropic.com, img.shields.io
- Lucide-react package import optimization
- **tsconfig.json**: ES2017 target, strict mode enabled, noUncheckedIndexedAccess enabled
- **Testing**: Vitest for unit tests, Playwright for E2E tests (configured but not extensively used yet)
#### Next.js 15 Breaking Change: Async Params
**Critical**: Next.js 15 changed `params` and `searchParams` to be **async** (Promise type).
- In page components: `params: Promise<{ locale: string }>` and `searchParams: Promise<{ key?: string }>`
- In API routes: `{ params }: { params: Promise<{ id: string }> }`
- **Always await these params before using them**:
```typescript
// ✅ Correct
const { locale } = await params;
const resolvedSearchParams = await searchParams;
const quarter = resolvedSearchParams.quarter || 'default';
// ❌ Wrong (causes runtime errors in Next.js 15)
const { locale } = params; // params is a Promise, not an object
const quarter = searchParams.quarter;
```
### Project Discovery System
项目发现系统是自动化探索和收录 AI 项目的核心功能,采用**双 Agent 协作架构**实现上下文隔离:
#### 架构组件
1. **自定义 Agents** (`.claude/agents/`):
- `content-explorer-agent`: 项目内容探索专家,批量探索项目并生成结构化数据
- 使用 `agent-browser` 子任务并行探索 GitHub 项目
- 应用严格的内容质量标准(客观描述、避免营销术语、不写入动态数据)
- 生成符合 `ProjectInputSchema` 的 JSON 数据
- `api-submitter-agent`: API 提交专家,处理探索结果的提交和状态更新
- 批量标记任务为 IN_PROGRESS
- 提交探索数据到完成 API
- 自动重试失败的提交(指数退避,最多3次)
2. **Claude Commands** (`.claude/commands/`):
- `/discover-projects`: 主命令,协调探索和提交流程
- 参数解析(任务数量、批次大小)
- 分批处理(默认每批3个任务)
- Agent 调度和进度显示
- 结果汇总和错误报告
3. **API Endpoints** (`src/app/api/discovery/`):
- `POST /api/discovery/tasks`: 创建新的探索任务(支持批量)
- `GET /api/discovery/tasks`: 获取待处理任务列表(支持 status/limit/offset 过滤)
- `PATCH /api/discovery/tasks/{id}`: 更新任务状态
- `POST /api/discovery/tasks/{id}/complete`: 完成任务并提交项目数据
- `GET /api/webhook/check-duplicates`: 检查项目是否已存在(URL 去重)
4. **Database Model**:
- `ProjectDiscoveryTask`: 任务追踪表
- 状态: PENDING → IN_PROGRESS → COMPLETED/FAILED
- 原始数据: `sourceUrl`, `sourceType`
- 探索结果: `explorationData` (JSON), `explorationSummary`
- 错误处理: `errorMessage`, `retryCount`, `lastRetryAt`
- 索引: `idx_task_status_created`, `idx_task_source_url`, `idx_task_project_id`
#### 数据流转
```
用户输入 URL → 创建 PENDING 任务 → /discover-projects 命令
分批获取任务(每批3个)
Content Explorer Agent (并行探索) → 探索结果 JSON
API Submitter Agent (提交到生产环境 API)
更新任务状态 → COMPLETED/FAILED
```
#### 质量标准
- **数据模板**: `.claude/schemas/project-content-template.md`
- **描述要求**: 清晰说明功能、突出价值、避免营销术语、10-500字
- **内容要求**: 从 README 提取并重新组织、不机械翻译、符合中文表达习惯
- **链接要求**: 必须包含 GITHUB 链接、所有链接可访问
- **标签要求**: 1-10 个标签、技术/应用/状态分类
- **动态数据处理**: Star/Fork 数量等动态数据不写入内容,使用 GitHub Badge 显示
#### 环境变量
- `WEBHOOK_API_KEY`: 生产环境 API 密钥(必需,用于认证)
#### 使用示例
```bash
# 处理默认10个任务(每批3个)
/discover-projects
# 处理指定数量的任务
/discover-projects 5
# 自定义批次大小
/discover-projects 9 --batch=2
# 处理所有待处理任务
/discover-projects all --batch=5
```
## MCP Servers Usage (按需使用)
1. **context7**: 不确定 API 用法时查阅最新文档
2. **chrome-devtools-mcp**: 查看页面效果、调试 UI 修复 BUG
3. **web-search-prime**: 默认联网搜索工具
4. **vision-mcp-server**: 图片/视频理解
## Git Commits
Git 提交信息遵循约定式提交格式(详见上方 Code Quality & Standards → Git Commit Conventions)。
## Development Workflow
- Before viewing the page, check if there's already a project running at localhost:3000. If yes, access it directly; if no, then run `pnpm dev`
- The dev server runs with hot-reload enabled for fast iteration
- The middleware handles locale detection and routing automatically - no manual locale configuration needed
- When adding new translations, update both `src/messages/zh.json` and `src/messages/en.json`
- Database changes require running `pnpm prisma migrate dev` to update the schema
- **Always verify API responses** with `WEBHOOK_API_KEY` when testing webhook or discovery endpoints
- **Use `pnpm prisma studio`** to inspect database state during development
## Common Development Patterns
### Adding a New API Endpoint
1. Create route file in `src/app/api/` (e.g., `src/app/api/your-endpoint/route.ts`)
2. Import validation schemas from `@/lib/validations`
3. Use `crypto.timingSafeEqual()` for API key authentication (see webhook route:36-43)
4. Implement partial success mode for batch operations (see webhook route:55-67)
5. Return structured errors with `NextResponse.json()`
### Adding New Database Fields
1. Update `prisma/schema.prisma` with new fields
2. Run `pnpm prisma migrate dev --name your_migration_name`
3. Update Zod schemas in `src/lib/validations.ts`
4. Update TypeScript types in `src/hooks/useProjects.ts` if needed
5. Regenerate Prisma client: `pnpm prisma generate`
### Creating Custom Agents
1. Create agent file in `.claude/agents/your-agent.md`
2. Define agent role, capabilities, and task instructions
3. Reference existing agents (`content-explorer-agent`, `api-submitter-agent`) as templates
4. Test via corresponding Claude Command in `.claude/commands/`
### Working with Multilingual Content
- All user-facing content should have both Chinese (`name`, `description`) and English (`nameEn`, `descriptionEn`) versions
- Use `src/messages/zh.json` and `src/messages/en.json` for UI translations
- For project data, prefer Chinese as primary language with English as optional
- When creating content, avoid mechanical translation - write naturally for each locale
### Creating New Pages (Next.js 15)
When adding new pages in the App Router, remember that **params and searchParams are Promises**:
```typescript
// Page component structure
interface PageProps {
params: Promise<{ locale: string; id?: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
export default async function MyPage({ params, searchParams }: PageProps) {
// MUST await params and searchParams
const { locale, id } = await params;
const { filter, sort } = await searchParams;
// Now you can use the values
// ...
}
// For generateMetadata
export async function generateMetadata({
params
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params;
// ...
}
```
For API routes with dynamic parameters:
```typescript
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
// ...
}
```
## Code Quality & Standards
### TypeScript Configuration
- **Strict mode enabled** with additional safety flags: `noUncheckedIndexedAccess`, `noImplicitReturns`, `noFallthroughCasesInSwitch`
- Path alias: `@/*` maps to `./src/*`
- Target: ES2017 for modern browser support
### ESLint Configuration
- **Config**: `.eslintrc.json` extends `next/core-web-vitals` and `prettier`
- **Console rules**: `no-console` warns on `console.log` but allows `console.warn` and `console.error`
- **Prettier integration**: `eslint-config-prettier` disables conflicting ESLint rules
- **Prettier config**: `.prettierrc.json` for code formatting
### Validation & Security
- **API Authentication**: Webhook uses timing-safe comparison (`crypto.timingSafeEqual`) to prevent timing attacks
- **Input Validation**: All API inputs use Zod schemas with detailed error messages
- **SQL Injection Prevention**: Prisma ORM with parameterized queries
- **Data Sanitization**: Markdown content sanitized with `rehype-sanitize` plugin
### Error Handling Patterns
- **Webhook**: Partial success mode - continues processing remaining projects even if individual projects fail
- **Database**: Unique constraints use try-catch with fallback logic (e.g., tag slug conflicts in webhook)
- **Console**: Use `console.warn()` for operational logs, `console.error()` for errors
### Git Commit Conventions
- **Format**: `<type>: <description>` (type in lowercase Chinese: feat/fix/refactor/chore)
- **Types**: `feat` (新功能), `fix` (修复), `refactor` (重构), `chore` (杂项)
- **Examples**:
- `feat: 新增项目发现任务系统`
- `fix: 修复 ESLint 警告`
- `refactor: 重构项目内容标准实现职责分离`
## Testing Strategy
### Unit Tests (Vitest)
- Location: Test files co-located with source code (e.g., `*.test.ts`)
- Run: `pnpm test` for all tests, `pnpm test <pattern>` for specific tests
- Configuration: Vitest with `@testing-library/jest-dom` matchers
### E2E Tests (Playwright)
- Location: `tests/e2e/` or co-located with features
- Run: `pnpm test:e2e` to execute all E2E tests
- Usage: Focus on critical user journeys (project browsing, search, locale switching)
## Performance Considerations
### Database Optimization
- **Index Strategy**: Composite indexes on frequently queried fields (status+createdAt, type+url)
- **N+1 Prevention**: Batch queries for tags (see webhook route:94-98)
- **Connection Pooling**: Prisma client singleton pattern (`src/lib/prisma.ts`)
### Frontend Performance
- **ISR**: Project detail pages revalidated every 5 minutes (`revalidate = 300`)
- **Package Optimization**: Lucide-react imports optimized via `experimental.optimizePackageImports`
- **Image Domains**: Pre-configured for localhost, *.anthropic.com, img.shields.io
### API Rate Limiting
- Webhook: Max 100 projects per request
- Discovery tasks: Max 50 tasks per batch creation
- All queries: Max 100 items per page (enforced via Zod schemas)
## Troubleshooting
### Common Issues
**Database Connection Errors**
- Verify `DATABASE_URL` is set in `.env.local`
- Check Neon dashboard for database status
- Run `pnpm prisma db push` to sync schema if needed
**Webhook Authentication Failures**
- Ensure `WEBHOOK_API_KEY` is set and matches (32+ characters)
- Check that API key is sent in request body
- Verify timing-safe comparison is used (never log raw API keys)
**Missing Translations**
- All new UI text must be added to both `src/messages/zh.json` and `src/messages/en.json`
- Missing keys will display as `missing_key_name` in the UI
- Test both locales (switch via URL path `/zh/` or `/en/`)
**Build Errors After Schema Changes**
- Run `pnpm prisma generate` to regenerate Prisma client
- Restart dev server after schema changes
- Check for TypeScript errors in generated types
**Next.js 15 Async Params Errors**
- If you see errors like `params.locale is undefined` or `Cannot read properties of undefined`, you likely forgot to `await` the params
- Check that all page components properly await: `const { locale } = await params;`
- Check that all API routes properly await: `const { id } = await params;`
- See "Next.js Configuration → Next.js 15 Breaking Change" above for examples
**Discovery Task Failures**
- Check `src/app/api/discovery/lib/discovery-service.ts:16-80` for deduplication logic
- Verify `WEBHOOK_API_KEY` is set for api-submitter-agent
- Use `pnpm prisma studio` to inspect task status and error messages
- Check browser console for agent-browser errors (requires chrome-devtools-mcp)
### Debugging Tips
1. **Enable Prisma Query Logging**:
```typescript
// In src/lib/prisma.ts
export const prisma = new PrismaClient({
log: ['query', 'error', 'warn'],
})
```
2. **Check Database State**:
```bash
pnpm prisma studio
# Opens at http://localhost:5555
```
3. **Test API Endpoints**:
```bash
# Create discovery task
curl -X POST http://localhost:3000/api/discovery/tasks \
-H "Content-Type: application/json" \
-d '{"apiKey":"YOUR_KEY","tasks":[{"sourceUrl":"https://github.com/user/repo"}]}'
```
4. **Validate Request Payloads**:
- All schemas are in `src/lib/validations.ts`
- Check schema errors in API response `details` field
- Ensure all required fields are present
### Environment Variables Checklist
For local development, ensure these are set in `.env.local`:
```bash
# Database (Neon PostgreSQL)
DATABASE_URL="postgres://[user]:[password]@[host]/[database]?sslmode=require"
# Webhook API Key (32+ characters)
WEBHOOK_API_KEY="your-secret-api-key-min-32-chars"
```
For production deployment (Vercel):
- Set `DATABASE_URL` in Vercel Dashboard (Environment Variables)
- Set `WEBHOOK_API_KEY` in Vercel Dashboard
- Run `pnpm prisma db push` after first deployment to create tables
- Do NOT use `vercel.json` for environment variables
-66
View File
@@ -1,66 +0,0 @@
# docs/ - Project Documentation
<!-- Parent: ../AGENTS.md -->
## OVERVIEW
Technical documentation, design plans, and implementation guides for the agent_park project.
## STRUCTURE
```
docs/
├── api-reference.md # API endpoint documentation
├── api-testing-guide.md # API testing instructions
├── discovery-workflow.md # Project discovery system docs
├── e2e-testing-guide.md # Playwright E2E guide
├── verification-report.md # Feature verification reports
├── analyst.md # Product analysis notes
├── n8n/ # n8n workflow documentation
│ ├── historical-workflow-design.md
│ ├── incremental-workflow-design.md
│ ├── frontier-signals-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
├── 2025-01-25-ai-timeline-feature-design.md
├── 2025-01-25-ai-timeline-implementation.md
├── 2026-01-25-keyword-cloud-system-design.md
└── 2026-01-25-keyword-cloud-implementation.md
```
## KEY DOCUMENTS
| Document | Purpose |
|----------|---------|
| `api-reference.md` | Complete API documentation |
| `discovery-workflow.md` | Project discovery architecture |
| `plans/*.md` | Feature design & implementation specs |
| `n8n/*.md` | n8n workflow design docs |
| `n8n/frontier-signals-workflow.md` | Multi-source frontier signals ingestion workflow (AI Agent filter) |
| `n8n/project-tag-reset-workflow.json` | Project tag reset workflow (multi-AI category classification) |
## FOR AI AGENTS
### When Adding Documentation
1. Place design docs in `plans/` with date prefix
2. Update this AGENTS.md with new file entries
3. Follow existing markdown formatting
### Related Files
- `CLAUDE.md` - Main project context (root)
- `AGENTS.md` - Project knowledge base (root)
- `.claude/` - Custom agents & commands
<!-- MANUAL: Additional notes can be added below -->
-158
View File
@@ -1,158 +0,0 @@
---
## 🎯 现状问题诊断
### 1. 信息架构问题
- **标签体系混乱**:有的项目标了"本地部署/macOS/iOS/Android"4个标签),有的只有"✨",标准不统一
- **缺乏分类维度**:用户无法快速筛选"开源/闭源"、"可商用/个人项目"、"多智能体/单智能体"
- **没有时间维度**:看不出项目是新发布还是经典项目,对 Agent 这个快速迭代的领域很关键
### 2. 用户体验断层
- **卡片信息过载/不足并存**:描述长短不一,但缺少关键决策信息(GitHub stars?是否开源?演示链接?)
- **无快速筛选**:当项目超过20个时,浏览成本会指数级上升
- **缺乏"为什么值得关注"**:单纯罗列不如策展(Curation),需要策展人视角的推荐理由
### 3. 技术呈现问题
- **移动端适配**:从代码结构看是响应式,但卡片在手机上可能过于拥挤
- **无暗色模式**:开发者群体对暗色模式有强需求,实现成本低(CSS media query
- **加载性能**:如果 n8n 工作流生成的是静态 Markdown,建议预渲染为 HTML 提升首屏速度
---
## 💡 轻量级但高价值的功能设计
基于"不做重,但要做巧"的原则,推荐以下功能:
### 阶段一:核心体验完善(2周内)
**1. 极简标签系统(三层维度)**
```
类型标签:🤖 Chatbot | 🔄 Multi-Agent | 🧠 Memory | 👁️ Vision | 🛠️ Tool Use
形态标签:📦 开源 | ☁️ SaaS | 💻 本地部署 | 📱 App
热度标签:🔥 Trending | ⭐ Classic | 🆕 New
```
*实现:纯 CSS 过滤,无需后端,前端 JS 筛选即可*
**2. "30秒决策"信息卡片**
每个项目卡片补充3个关键字段(n8n工作流抓取时补充):
- **GitHub Stars**(如果是开源)
- **体验方式**Live Demo / 下载 / 仅代码
- **适用场景**:一句话场景(如"适合搭建个人知识库")
**3. 每日/每周精选(Newsletter 化)**
不增加功能,而是**内容运营策略**:
- 首页顶部固定"本周编辑推荐"(3个项目+一句话推荐理由)
- 底部增加邮件订阅框(用 Buttondown 或 Revue,零成本)
- 归档页面按周聚合(`/week-04-2025`
### 阶段二:社区感与互动(1个月内)
**4. "使用报告"轻互动**
不同于评论系统(太重),采用**投票+标签**:
- "你在用吗?" 👍 / 👎(匿名)
- "适用场景"多选标签(用户可添加,类似 StackOverflow 的标签系统)
- 数据存储:Airtable 或 Notion API(轻量级数据库)
**5. Agent 项目 Twitter 趋势墙**
无需自己生成内容,聚合展示:
- 嵌入 Twitter List(创建一个"Agent Builders"列表)
- 或展示特定 hashtag(如 `#AIAgent`)的最新热门推文
- 实现:Twitter 嵌入式时间线,零维护成本
**6. 极简提交表单优化**
当前"立即提交"大概率跳转到表单,优化为:
- 预填项目模板(GitHub URL 自动抓取信息)
- 支持提交者写"推荐语"(策展人视角)
- 审核流:GitHub Issues 或 Airtable 表单(不用开发后台)
### 阶段三:开发者工具化(2个月内)
**7. Agent 项目 RSS 聚合**
开发者刚需:一站式追踪所有 Agent 项目更新
- 为每个项目生成 RSS 源监控(GitHub releases
- 提供聚合 RSS(用户订阅一个即可看全站更新)
- 技术:RSSHub 或 n8n 自动生成
**8. "Agent 构建模式"分类**
垂直领域细分(这是你的差异化):
- **ReAct 模式**项目集合
- **Plan-and-Execute**项目
- **Multi-Agent 协作框架**
- **Function Calling 工具库**
帮助开发者按技术方案选型,而非只看功能。
---
## 📋 分阶段实施计划( Roadmap )
### Week 1-2:基础优化
- [ ] 统一标签体系(3层9个标签以内)
- [ ] 修改项目卡片模板(增加 Stars/Demo 链接字段)
- [ ] 增加暗色模式(`prefers-color-scheme` 媒体查询)
- [ ] 优化移动端卡片布局(单列+横向滚动标签)
### Week 3-4:内容运营
- [ ] 建立编辑推荐机制(每周手动精选3个)
- [ ] 上线邮件订阅(嵌入 Buttondown
- [ ] 优化提交表单(GitHub URL 自动拉取)
- [ ] 创建 Twitter/X 账号同步发布精选
### Month 2:互动功能
- [ ] 增加轻量级投票系统(用 Upstash Redis 或 Airtable
- [ ] 上线 RSS 订阅功能
- [ ] 增加"技术模式"分类维度
- [ ] 发布首份《Agent Landscape 月报》(PDF 轻量报告)
### Month 3:生态扩展
- [ ] 推出"Agent Builder 访谈"(轻量级文字访谈,每月2期)
- [ ] 建立 Discord/Telegram 群组(社区沉淀)
- [ ] 尝试"项目雷达"功能(预测下周可能火的项目)
---
## 💰 变现可能性分析(从轻到重)
基于"保持轻量"的前提,按可行性排序:
### 1. **策展付费(轻量,推荐优先尝试)**
- **模式**:每周付费 Newsletter($5/月或$50/年),提供更深度的项目分析、代码解读、创始人访谈
- **可行性**:⭐⭐⭐⭐⭐ 你已经用 n8n 做内容聚合,增加深度分析即可,无需改技术架构
- **受众**:Agent 开发者愿意为高质量信息付费,参考 Lenny's Newsletter(产品经理领域)
### 2. **精准职位板(轻量)**
- **模式**"Agent 相关岗位"板块,公司付费发帖($100/月)
- **可行性**:⭐⭐⭐⭐ 当下 Agent 工程师需求旺盛,但供给分散,你的受众正是招聘方想要的
- **优势**:比大型招聘站更精准,比 LinkedIn 更垂直
### 3. **开源项目赞助分成(超轻量)**
- **模式**:项目卡片增加"赞助该项目"按钮,跳转 GitHub Sponsors,你收取 5-10% 导流费或获得 affiliate 返点
- **可行性**:⭐⭐⭐ 需要与项目方谈合作,但初期可以作为增值服务免费提供,建立信任后变现
### 4. **轻量级广告/赞助(需谨慎)**
- **模式**:接受 Agent 框架、云服务(如 LangSmith、Langfuse)的广告位
- **可行性**:⭐⭐⭐ 需要流量基础(月UV 1万+),且要保持克制避免破坏体验
- **建议**:以"赞助商推荐"形式融入内容,而非 banner 广告
### 5. **数据/洞察服务(未来方向)**
- **模式**:出售 Agent 趋势数据报告(GitHub 增长趋势、技术栈迁移方向)
- **可行性**:⭐⭐ 需要积累 6-12 个月数据,且需要品牌背书
- **风险**:会变重,建议保持轻量,只出季度免费报告建立权威性
### ❌ 不建议的变现方式
- **付费墙阻断访问**:违背导航站开放属性
- **复杂的会员系统**:开发与维护成本高
- **交易佣金**:涉及支付、合规,太重
---
## 🚀 立即可做的3个改动(本周)
如果你只想快速优化,先做这三件:
1. **标签标准化**:删除所有重复/冗长标签,只用 类型+形态 二维(如 `🤖Chatbot` `📦开源`
2. **增加"一键体验"按钮**:在卡片上直接放 🔗Demo 或 💻GitHub 图标,减少用户点击成本
3. **顶部增加时间线**:小字标注"本周新增 X 个项目,共收录 Y 个",营造更新感
**核心建议**:Agent Park 现在的定位应该是 **"Agent 爱好者的 Hacker News + Product Hunt 混合体"**,保持策展人(Curator)视角比做全量数据库更有价值。你的 n8n 工作流是护城河,但**人工精选的品味**才是核心竞争力。
需要我针对某个具体功能(比如 n8n 工作流优化方案、暗色模式 CSS、或者邮件订阅文案)展开详细方案吗?
File diff suppressed because it is too large Load Diff
@@ -1,502 +0,0 @@
# AI 智能搜索系统设计文档
**日期**: 2026-01-25
**作者**: Claude Code
**状态**: 设计阶段
## 概述
为项目列表添加 AI 语义搜索功能,用户可通过自然语言描述需求,系统通过向量相似度匹配返回相关项目,而非传统的关键词模糊搜索。
### 核心目标
- ✅ 支持自然语言查询(如"帮我找做图像生成的项目")
- ✅ 基于向量相似度的语义匹配
- ✅ 完全在 n8n 中实现 AI 逻辑,Next.js 应用保持纯净
- ✅ 利用现有 Neon 数据库的 pgvector 扩展
---
## 架构设计
### 整体架构
```
前端(Next.js
API 代理(/api/search/ai
n8n 工作流(AI 逻辑)
Neon 数据库(pgvector
```
### 核心组件
**数据层(Neon + pgvector**
- `Project` 表添加 `embedding` 字段存储向量
- HNSW 索引加速相似度搜索
- 向量维度:1536OpenAI text-embedding-3-small
**服务层(n8n 工作流)**
- **向量化工作流**:定时扫描未向量化项目,调用 OpenAI API 生成向量
- **AI 搜索工作流**:接收查询 → 生成向量 → 相似度搜索 → 返回结果
**前端层(Next.js**
- 搜索框增加 AI 模式切换按钮
- 显示相似度评分和匹配原因
---
## 数据库设计
### Schema 修改
```prisma
model Project {
// ... 现有字段
// 新增:向量嵌入字段
embedding vector(1536)? // pgvector 类型
embeddingUpdatedAt DateTime? // 向量化更新时间
}
```
### 迁移 SQL
```sql
-- 启用 pgvector 扩展
CREATE EXTENSION IF NOT EXISTS vector;
-- 添加向量列
ALTER TABLE "Project" ADD COLUMN "embedding" vector(1536);
ALTER TABLE "Project" ADD COLUMN "embeddingUpdatedAt" TIMESTAMP;
-- 创建 HNSW 索引(余弦距离)
CREATE INDEX idx_project_embedding_cosine
ON "Project" USING hnsw ("embedding" vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- 复合索引(查找未向量化项目)
CREATE INDEX idx_project_embedding_null
ON "Project" ("id")
WHERE "embedding" IS NULL;
```
### 向量化内容策略
**字段权重分配:**
- 名称(40%):`name` + `nameEn`
- 描述(40%):`description` + `descriptionEn`
- 标签(15%):`tags`(逗号连接)
- 详细内容(5%):`content` + `contentEn`(截取前 500 字)
**示例输入文本:**
```
AI Video Generator
一个基于人工智能的视频生成工具,可以自动从文本生成高质量视频...
人工智能, 视频生成, AIGC
详细功能介绍...
```
---
## n8n 工作流设计
### 工作流 1:定时向量化
**触发器**Cron 表达式(每 5 分钟)
```
*/5 * * * *
```
**流程**
```
触发器
查询未向量化项目(LIMIT 20
批量处理(每批 5 个)
构造文本内容(合并字段)
调用 OpenAI Embeddings API
更新数据库 embedding 字段
等待 1 秒(控制速率)
下一批
```
**PostgreSQL 查询**
```sql
SELECT id, name, "nameEn", description, "descriptionEn",
content, "contentEn"
FROM "Project"
WHERE "embedding" IS NULL
AND "status" = 'ACTIVE'
LIMIT 20
```
**错误处理**
- API 限流:指数退避重试(1s → 2s → 4s)
- 最多重试 3 次
- 失败记录日志
### 工作流 2AI 搜索
**触发器**Webhook`/webhook/ai-search`
**流程**
```
Webhook 接收查询
接收参数:{ query, locale, limit, filters }
调用 OpenAI Embeddings API(生成查询向量)
PostgreSQL 向量相似度搜索
应用过滤条件(tags, status
格式化结果(添加相似度评分)
返回 JSON 响应
```
**PostgreSQL 查询**
```sql
SELECT
id, name, "nameEn", slug, description, "descriptionEn",
1 - (embedding <=> '{{ query_vector }}'::vector) as similarity
FROM "Project"
WHERE "embedding" IS NOT NULL
AND "status" = 'ACTIVE'
ORDER BY embedding <=> '{{ query_vector }}'::vector
LIMIT {{ limit || 20 }}
```
**响应格式**
```json
{
"results": [
{
"project": { /* */ },
"similarity": 0.89,
"matchReason": "项目名称和描述与图像生成高度相关"
}
],
"total": 42,
"searchTime": 156
}
```
---
## Next.js API 设计
### 路由配置
**文件**`src/app/api/search/ai/route.ts`
```typescript
import { NextResponse } from 'next/server'
const N8N_WEBHOOK_URL = process.env.N8N_AI_SEARCH_WEBHOOK
export async function POST(request: Request) {
try {
const body = await request.json()
// 转发到 n8n 工作流
const n8nResponse = await fetch(N8N_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
const results = await n8nResponse.json()
return NextResponse.json(results)
} catch (error) {
return NextResponse.json(
{ error: 'Search failed' },
{ status: 500 }
)
}
}
```
**请求格式**
```json
{
"query": "帮我找做图像生成的AI工具",
"locale": "zh",
"limit": 20,
"filters": {
"tags": ["AIGC"],
"status": "ACTIVE"
}
}
```
---
## 前端交互设计
### UI 组件修改
**文件**`src/components/search/SearchBar.tsx`
**功能**
- 添加 AI 模式切换按钮(Sparkles 图标)
- AI 模式时按钮高亮(金色背景)
- 不同模式的占位符提示
- 加载状态优化
**关键代码**
```tsx
<button
onClick={() => setAiMode(!aiMode)}
className={`
px-3 py-2 border-2 transition-all
${aiMode
? 'bg-yellow-400 border-yellow-500 text-black'
: 'bg-white border-gray-300 text-gray-600'
}
`}
title="AI 语义搜索"
>
<Sparkles className="w-5 h-5" />
</button>
```
### AI 搜索结果展示
**文件**`src/components/search/AISearchResults.tsx`
**功能**
- 显示相似度指示条(左侧彩色条)
- 相似度评分(0-100%
- 匹配原因说明(可选)
- 颜色编码:绿色(>0.8)、黄色(>0.6)、红色(<0.6
### 搜索模式对比
| 特性 | 传统搜索 | AI 搜索 |
|------|---------|---------|
| 占位符 | "搜索项目名称..." | "描述你想要的项目..." |
| 匹配方式 | 关键词模糊匹配 | 向量语义相似度 |
| 返回速度 | 极快(<100ms | 较快(1-3s |
| 结果增强 | 无 | 相似度评分 + 匹配原因 |
| 提示信息 | 无 | "💡 试试:'帮我找能生成视频的 AI 工具'" |
---
## 环境变量配置
### Next.js 应用(`.env.local`
```bash
# Neon 数据库(已有)
DATABASE_URL=postgres://...
# n8n Webhook
N8N_AI_SEARCH_WEBHOOK=https://your-n8n-instance.com/webhook/ai-search
N8N_WEBHOOK_API_KEY=your-webhook-key # 可选,用于安全验证
```
### n8n 工作流
```bash
# OpenAI API(用于 Embeddings
OPENAI_API_KEY=sk-...
# Neon 数据库(与 Next.js 共享)
NEON_DATABASE_URL=postgres://...
```
---
## 错误处理与优化
### 错误处理策略
**n8n 工作流**
- API 限流:指数退避重试
- API Key 无效:发送告警
- 数据库查询失败:返回友好错误信息
- 向量未就绪:提示用户稍后重试
**前端**
- 10 秒超时限制
- 超时或错误时自动降级到传统搜索
- Toast 消息提示用户
### 性能优化
**数据库查询**
- 只返回必要字段(不返回 content)
- 使用 HNSW 索引
- 设置查询超时(5s
**缓存策略(可选)**
- Redis 缓存常见查询结果(5 分钟 TTL)
- 内存缓存热门查询
### 监控指标
- 平均搜索响应时间
- API 调用次数/成本
- 向量化完成率
- 错误率
---
## 成本估算
### OpenAI Embeddings API
**定价**
- text-embedding-3-small$0.00002 / 1K tokens
**估算**
- 单个项目(500 tokens):$0.00001
- 1000 个项目:$0.01
- 单次搜索(10 tokens):$0.0000002
- 1000 次搜索:$0.0002
**月度预算**:$5 可处理 50 万个项目或 2500 万次搜索
### Neon 免费套餐
**限制**
- 存储:0.5GB
- 计算:300 小时/月
**向量存储**
- 单个项目(1536 维):3KB(半精度)
- 1000 个项目:3MB
- 10,000 个项目:30MB ✅
**结论**:免费套餐完全够用(可支持 5,000-10,000 个项目)
---
## 测试计划
### 单元测试
```typescript
// src/__tests__/search.test.ts
describe('AI Search', () => {
it('should handle empty query', async () => {
const response = await fetch('/api/search/ai', {
method: 'POST',
body: JSON.stringify({ query: '' })
})
expect(response.status).toBe(400)
})
it('should fallback to traditional search on error', async () => {
// 测试错误降级逻辑
})
})
```
### E2E 测试
```typescript
// tests/e2e/ai-search.spec.ts
test('AI 搜索功能', async ({ page }) => {
await page.goto('/zh/projects')
await page.click('[data-testid="ai-mode-toggle"]')
await page.fill('input[name="search"]', '视频生成工具')
await page.press('input[name="search"]', 'Enter')
await expect(page.locator('.ai-search-results')).toBeVisible()
})
```
### 集成测试
- 测试 n8n 工作流端到端
- 验证向量搜索结果准确性
- 测试错误场景(API 限流、数据库连接失败)
---
## 部署清单
### 数据库准备
- [x] 运行数据库迁移
- [x] 验证 pgvector 扩展已启用
- [x] 检查 HNSW 索引创建成功
### n8n 配置
- [ ] 创建向量化工作流
- [ ] 配置 Cron 触发器(每 5 分钟)
- [ ] 配置 PostgreSQL 节点
- [ ] 配置 OpenAI Embeddings 节点
- [ ] 添加错误处理和重试逻辑
- [ ] 创建 AI 搜索工作流
- [ ] 配置 Webhook 触发器
- [ ] 配置 OpenAI Embeddings 节点
- [ ] 配置 PostgreSQL 向量查询
- [ ] 配置结果格式化
- [ ] 测试工作流
- [ ] 手动触发向量化流程
- [ ] 测试 Webhook 搜索
- [ ] 验证错误处理
### Next.js 部署
- [ ] 添加环境变量(N8N_WEBHOOK_URL
- [ ] 创建 API 路由(`/api/search/ai`
- [ ] 更新 SearchBar 组件
- [ ] 创建 AISearchResults 组件
- [ ] 本地测试完整流程
- [ ] 部署到 Vercel
### 验证步骤
1. **向量化测试**
```sql
-- 检查已向量化项目数
SELECT COUNT(*) FROM "Project" WHERE embedding IS NOT NULL;
```
2. **搜索测试**
- 输入自然语言查询
- 验证返回结果相关性
- 检查相似度评分
3. **性能测试**
- 测量平均响应时间
- 验证并发处理能力
---
## 后续优化方向
1. **混合搜索**:结合关键词搜索和向量搜索,提升准确率
2. **查询缓存**Redis 缓存热门查询结果
3. **A/B 测试**:对比传统搜索和 AI 搜索的用户体验
4. **多模态搜索**:支持图片、语音输入
5. **个性化排序**:基于用户历史行为优化结果
6. **自动标签建议**AI 分析项目内容推荐标签
---
## 参考资料
- [Neon pgvector 文档](https://neon.com/docs/extensions/pgvector)
- [OpenAI Embeddings API](https://platform.openai.com/docs/guides/embeddings)
- [pgvector GitHub](https://github.com/pgvector/pgvector)
- [HNSW 算法论文](https://arxiv.org/abs/1603.09320)
-88
View File
@@ -1,88 +0,0 @@
# Timeline 页面修复验证报告
## 验证时间
2025-01-27
## 验证方法
使用 chrome-devtools-mcp 自动化测试工具
## 验证结果
### ✅ 页面结构
- 标题: "THE STORY OF A.I."
- 副标题: "Pinned. Stacked. Zigzagged."
- 7个年份分组 (2023 → 2017)
- 12个事件卡片正确渲染
- Newsletter 订阅区域正常
- Back to top 按钮存在
### ✅ 动画效果验证
**CSS Transition 配置:**
```css
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
```
**Hover 效果实测:**
- z-index: 50 (从40提升到50) ✅
- transform: scale(1.05) translateY(-20px) ✅
- marginRight: 20px (从-224px增加) ✅
- rotation: 0deg (从旋转角度变正) ✅
### ✅ 视觉元素
- 背景网格 (40px 网格,10% 透明度) ✅
- 装饰性 SVG 形状 (脉动动画) ✅
- Tape 装饰 (12个,半透明 + 模糊) ✅
- 年份标签 (交替左右布局,±2度旋转) ✅
- 时间线连接线 (垂直渐变线) ✅
- 时间线圆点 (每个年份一个) ✅
### ✅ 数据完整性
- 2017年: Transformer 论文
- 2018年: GPT-1, BERT (共3个,含测试数据)
- 2019年: GPT-2
- 2020年: GPT-3
- 2021年: GitHub Copilot
- 2022年: ChatGPT
- 2023年: GPT-4, Claude
### ✅ 控制台检查
- 无错误
- 仅1个资源预加载警告(可忽略)
## 与原始设计对比
### 已实现
- ✅ 标题风格: "THE STORY OF A.I."
- ✅ 单一年份标签(您要求的)
- ✅ 卡片堆叠效果
- ✅ Hover 动画(上浮 + 缩放 + 旋转归零)
- ✅ Tape 装饰(半透明 + 模糊)
- ✅ 背景网格
- ✅ 装饰性 SVG 形状
- ✅ Newsletter 区域
- ✅ Back to top 按钮
### 设计差异(已修复)
- ✅ 动画过渡曲线:cubic-bezier(0.25, 0.8, 0.25, 1)
- ✅ Hover z-index 提升:50
- ✅ Transform 包含 translateY(-20px) 和 scale(1.05)
- ✅ Margin 调整实现展开效果
## 性能指标
- 页面加载时间: ~6s (首次编译)
- 后续导航: <1s
- 动画帧率: 60fps (smooth)
- 总卡片区: 12
- 总年份: 7
## 结论
**所有核心功能已实现并与原始设计对齐**
- 动画效果流畅
- 视觉风格匹配
- 数据完整准确
- 用户体验良好
## 建议
1. CSS 已正确加载到 globals.css
2. 动画效果已验证工作正常
3. 可以部署到生产环境
-11
View File
@@ -1,11 +0,0 @@
{
"lsp": {
"tsserver": {
"command": [
"node",
"/Users/caihaohan/Code/agent_park/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/tsserver"
],
"extensions": [""]
}
}
}
-8
View File
@@ -1,8 +0,0 @@
{
"lsp": {
"eslint": {
"command": "eslint",
"extensions": [".ts", ".tsx", ".js", ".jsx"]
}
}
}
+1 -5
View File
@@ -8,11 +8,7 @@
"start": "next start",
"lint": "next lint",
"test": "vitest",
"test:e2e": "playwright test",
"taxonomy:backfill": "ts-node --compiler-options '{\"module\":\"CommonJS\",\"moduleResolution\":\"node\"}' scripts/backfill-tag-taxonomy.ts",
"signals:backfill-hot": "ts-node --compiler-options '{\"module\":\"CommonJS\",\"moduleResolution\":\"node\"}' scripts/backfill-signal-hotness.ts",
"domains:sync": "ts-node --compiler-options '{\"module\":\"CommonJS\",\"moduleResolution\":\"node\"}' scripts/sync-domain-scenarios.ts",
"tags:dedupe-free": "ts-node --compiler-options '{\"module\":\"CommonJS\",\"moduleResolution\":\"node\"}' scripts/dedupe-free-tags-to-canonical.ts"
"test:e2e": "playwright test"
},
"dependencies": {
"@prisma/client": "^6.1.0",
-128
View File
@@ -1,128 +0,0 @@
# prisma/ - Database Layer
<!-- Parent: ../AGENTS.md -->
## OVERVIEW
Prisma ORM configuration for PostgreSQL (Neon serverless) with multilingual support, vector embeddings, and automated workflows integration.
## KEY FILES
| File | Purpose |
|------|---------|
| `schema.prisma` | Database models, enums, indexes |
| `seed.ts` | Initial data seeding script |
| `migrations/` | Migration history |
## DATABASE MODELS
### Core Models
| Model | Purpose | Key Fields |
|-------|---------|------------|
| `Project` | AI projects | name/nameEn, slug (unique), description, content, embedding (vector) |
| `Tag` | Project tags | name (unique), slug (unique) |
| `ExternalLink` | Project links | type (WEBSITE/GITHUB/HUGGINGFACE/PAPER), url |
| `ProjectTag` | Junction table | projectId, tagId |
### Keyword Cloud System
| Model | Purpose |
|-------|---------|
| `Quarter` | Quarterly metadata |
| `Keyword` | Trending keywords with visual config |
| `VisualStyleRule` | Color/size/border rules by score range |
| `KeywordCloudErrorLog` | Error tracking for n8n workflows |
### Discovery System
| Model | Purpose |
|-------|---------|
| `ProjectDiscoveryTask` | Exploration task tracking |
### AI Timeline System
| Model | Purpose |
|-------|---------|
| `AIEvent` | Historical AI events with dates |
## ENUMS
- `ProjectStatus`: ACTIVE | ARCHIVED
- `LinkType`: WEBSITE | GITHUB | HUGGINGFACE | PAPER
- `TaskStatus`: PENDING | IN_PROGRESS | COMPLETED | FAILED
## INDEXES
| Index | Model | Purpose |
|-------|-------|---------|
| `idx_project_slug` | Project | Unique slug lookup |
| `idx_project_status_createdAt` | Project | Filter by status, sort by date |
| `idx_project_embedding_cosine` | Project | Vector similarity search |
| `idx_link_type_url` | ExternalLink | URL deduplication |
| `idx_task_status_created` | ProjectDiscoveryTask | Task queue queries |
| `idx_keyword_quarterId` | Keyword | Quarterly keyword lookup |
| `idx_quarter_displayOrder` | Quarter | Quarter ordering |
## MULTILINGUAL PATTERN
Most models follow the dual-language pattern:
```
name: String // Chinese (primary)
nameEn: String? // English (optional)
description: String
descriptionEn: String?
content: String?
contentEn: String?
```
## CASCADE DELETIONS
- `ExternalLink``Project` (onDelete: Cascade)
- `ProjectTag``Project` and `Tag` (onDelete: Cascade)
- `Keyword``Quarter` (onDelete: Cascade)
## FOR AI AGENTS
### Working With This Directory
1. **After schema changes**:
```bash
pnpm prisma migrate dev --name description
pnpm prisma generate
```
2. **Seed database**:
```bash
pnpm prisma db seed
```
3. **Inspect database**:
```bash
pnpm prisma studio
```
### Common Patterns
- **Unique constraints**: Handle with try-catch + fallback (see webhook tag upsert)
- **Multilingual queries**: Always check both `field` and `fieldEn` variants
- **Vector search**: Use `Unsupported("vector")` type with pgvector extension
### Anti-Patterns
- NEVER modify schema without running migrations
- NEVER skip `pnpm prisma generate` after schema changes
- NEVER use raw SQL when Prisma methods are available
- NEVER commit `.env` files with DATABASE_URL
## N8N INTEGRATION
n8n workflows interact with these models:
- **Keywords**: Written by keyword-cloud workflow
- **ProjectDiscoveryTask**: Created by trending/topic workflows
- **Project**: Updated by analysis workflows
- **Tag**: Maintained by tag-janitor workflow
<!-- MANUAL: Additional notes can be added below -->
-148
View File
@@ -1,148 +0,0 @@
# 项目目录结构
## 📁 根目录结构
### 🎯 主要源代码目录
```
src/
├── app/ # Next.js App Router (核心应用)
│ ├── [locale]/ # 国际化路由
│ │ ├── keyword-cloud/ # 词云功能页面
│ │ ├── projects/ # 项目页面
│ │ └── timeline/ # 时间线功能页面
│ └── api/ # API 路由
│ ├── discovery/ # 项目发现 API
│ ├── events/ # 事件 API
│ ├── keyword-cloud/ # 词云 API
│ ├── projects/ # 项目 API
│ ├── search/ # 搜索 API
│ └── webhook/ # Webhook API
├── components/ # React 组件
│ ├── layout/ # 布局组件
│ ├── locale/ # 国际化组件
│ ├── project/ # 项目相关组件
│ ├── search/ # 搜索相关组件
│ └── timeline/ # 时间线相关组件
├── hooks/ # 服务端数据获取函数
├── i18n/ # 国际化配置
├── lib/ # 工具库
│ └── github/ # GitHub 相关工具
└── messages/ # 翻译文件
```
### ⚙️ 配置文件
```
├── next.config.js # Next.js 配置
├── tailwind.config.ts # Tailwind CSS 配置
├── tsconfig.json # TypeScript 配置
├── components.json # Radix UI 组件配置
├── .eslintrc.json # ESLint 配置
└── .prettierrc.json # Prettier 配置
```
### 🗄️ 数据库配置
```
prisma/
├── schema.prisma # 数据库模式
└── migrations/ # 数据库迁移
```
### 🔧 自定义 Agents
```
.claude/
└── agents/ # Claude Code 自定义 Agent
```
### 🔄 n8n 工作流
```
n8n-workflows/
├── keyword-cloud-workflow.json # 词云采集工作流
└── README.md # 工作流文档
docs/n8n/
├── historical-workflow-design.md # 历史工作流设计
├── incremental-workflow-design.md # 增量工作流设计
└── tag-janitor-workflow.json # 标签清理工作流
```
### 📚 文档
```
docs/
├── api-reference.md # API 参考
├── api-testing-guide.md # API 测试指南
├── discovery-workflow.md # 项目发现工作流
├── e2e-testing-guide.md # E2E 测试指南
├── plans/ # 项目规划文档
└── n8n/ # n8n 相关文档
```
### 🎨 设计资源
```
design/
├── hotpot cloud/ # Hotpot 云相关设计
│ └── screen.png
└── timeline/ # 时间线设计
└── screen.png
```
### 📜 数据库种子脚本
```
scripts/
├── list-tags.js # 标签列表脚本
├── seed-historical-events.ts # 历史事件种子数据
├── seed-keyword-cloud.ts # 词云种子数据
└── test-keyword-api.ts # 词云 API 测试
```
### 🧪 测试
```
tests/
└── screenshots/ # 截图测试
```
### 🚀 其他配置
```
opencode/ # OpenCode 配置
└── mcp.json # MCP 服务器配置
.env # 环境变量
.env.example # 环境变量模板
AGENTS.md # Agent 文档
CLAUDE.md # 项目说明文档
package.json # 项目依赖
```
## 🎯 特殊功能目录
### 1. 项目发现系统 (Project Discovery)
- **位置**: `src/app/api/discovery/`
- **功能**: 自动化探索和收录 AI 项目
- **特点**: 双 Agent 协作架构
### 2. 词云系统 (Keyword Cloud)
- **位置**: `src/app/[locale]/keyword-cloud/` 和 `src/app/api/keyword-cloud/`
- **功能**: 季度 AI 热点词云展示
- **特点**: n8n 自动化采集、实时数据展示
### 3. 时间线系统 (Timeline)
- **位置**: `src/app/[locale]/timeline/`
- **功能**: AI 历史事件时间线
- **特点**: 渐进式加载、历史事件展示
### 4. n8n 工作流
- **位置**: `n8n-workflows/` 和 `docs/n8n/`
- **功能**:
- keyword-cloud-workflow.json: Google Trends 数据采集
- tag-janitor-workflow.json: 标签清理自动化
- **特点**: 季度触发、错误处理、API 集成
### 5. 项目管理系统
- **位置**: `src/app/[locale]/projects/` 和 `src/app/api/projects/`
- **功能**: AI 项目展示、搜索、分类
- **特点**: 多语言支持、标签系统、Webhook 集成
### 6. 搜索系统
- **位置**: `src/app/api/search/` 和 `src/components/search/`
- **功能**: AI 项目搜索和过滤
- **特点**: 多条件搜索、分页、状态过滤
```
-76
View File
@@ -1,76 +0,0 @@
import { PrismaClient } from '@prisma/client'
import { computeSignalHotness } from '../src/lib/signal-hotness'
import type { SignalSource } from '../src/lib/validations'
const prisma = new PrismaClient()
function isSignalSource(value: string): value is SignalSource {
return (
value === 'hacker_news' ||
value === 'github' ||
value === 'arxiv' ||
value === 'hugging_face' ||
value === 'reddit' ||
value === 'product_hunt'
)
}
async function main() {
console.log('[signals-hot] start backfill')
const rows = await prisma.signal.findMany({
select: {
id: true,
source: true,
engagement: true,
publishedAt: true,
hotScore: true,
isHot: true,
},
})
let updated = 0
let skipped = 0
for (const row of rows) {
if (!isSignalSource(row.source)) {
skipped += 1
continue
}
const next = computeSignalHotness({
source: row.source,
engagement: row.engagement,
publishedAt: row.publishedAt,
})
if (row.hotScore === next.hotScore && row.isHot === next.isHot) {
continue
}
await prisma.signal.update({
where: { id: row.id },
data: {
hotScore: next.hotScore,
isHot: next.isHot,
},
})
updated += 1
}
console.log('[signals-hot] done', {
total: rows.length,
updated,
skippedUnknownSource: skipped,
})
}
main()
.catch((error) => {
console.error('[signals-hot] failed', error)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})
-136
View File
@@ -1,136 +0,0 @@
import { PrismaClient } from '@prisma/client'
import {
FIXED_PROJECT_TYPE_TAGS,
inferProjectTypeSlug,
inferTagCategory,
} from '../src/lib/tag-taxonomy'
const prisma = new PrismaClient()
async function ensureFixedProjectTypeTags() {
const result = await Promise.all(
FIXED_PROJECT_TYPE_TAGS.map((item) =>
prisma.tag.upsert({
where: { slug: item.slug },
update: {
name: item.name,
nameEn: item.nameEn,
category: 'FIXED_PROJECT_TYPE',
},
create: {
name: item.name,
nameEn: item.nameEn,
slug: item.slug,
category: 'FIXED_PROJECT_TYPE',
},
})
)
)
return new Map(result.map((tag) => [tag.slug, tag.id]))
}
async function backfillTagCategories() {
const tags = await prisma.tag.findMany()
let updated = 0
for (const tag of tags) {
const inferredCategory = inferTagCategory({
slug: tag.slug,
name: tag.name,
nameEn: tag.nameEn,
})
if (tag.category !== inferredCategory) {
await prisma.tag.update({
where: { id: tag.id },
data: { category: inferredCategory },
})
updated += 1
}
}
return updated
}
async function backfillProjectFixedTypes(fixedTypeIdMap: Map<string, string>) {
const projects = await prisma.project.findMany({
where: { status: 'ACTIVE' },
include: {
tags: {
include: {
tag: true,
},
},
},
})
const createData: Array<{ projectId: string; tagId: string }> = []
for (const project of projects) {
const fixedTypeSlug = inferProjectTypeSlug({
name: project.name,
nameEn: project.nameEn,
description: project.description,
descriptionEn: project.descriptionEn,
tags: project.tags.map((tag) => ({
slug: tag.tag.slug,
name: tag.tag.name,
nameEn: tag.tag.nameEn,
})),
})
const fixedTypeTagId = fixedTypeIdMap.get(fixedTypeSlug)
if (!fixedTypeTagId) {
throw new Error(`Missing fixed project type tag: ${fixedTypeSlug}`)
}
createData.push({
projectId: project.id,
tagId: fixedTypeTagId,
})
}
const removed = await prisma.projectTag.deleteMany({
where: {
tag: {
category: 'FIXED_PROJECT_TYPE',
},
},
})
const inserted = await prisma.projectTag.createMany({
data: createData,
skipDuplicates: true,
})
return {
projectCount: projects.length,
inserted: inserted.count,
removed: removed.count,
}
}
async function main() {
console.log('[taxonomy] start')
const fixedTypeIdMap = await ensureFixedProjectTypeTags()
const updatedTagCategoryCount = await backfillTagCategories()
const projectResult = await backfillProjectFixedTypes(fixedTypeIdMap)
console.log('[taxonomy] done', {
updatedTagCategoryCount,
projectCount: projectResult.projectCount,
insertedProjectTypeLinks: projectResult.inserted,
removedOldProjectTypeLinks: projectResult.removed,
})
}
main()
.catch((error) => {
console.error('[taxonomy] failed', error)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})
-184
View File
@@ -1,184 +0,0 @@
import { PrismaClient, type TagCategory } from '@prisma/client'
const prisma = new PrismaClient()
type DedupeRule = {
sourceSlug: string
targetSlug: string
reason: string
}
const DEDUPE_RULES: DedupeRule[] = [
{
sourceSlug: 'ai-development-tool',
targetSlug: 'code-dev',
reason: 'AI 开发工具与开发者工具/代码领域语义重复',
},
{
sourceSlug: 'code-generation',
targetSlug: 'code-dev',
reason: '代码生成与开发者工具/代码领域语义重复',
},
{
sourceSlug: 'library',
targetSlug: 'code-dev',
reason: 'Library 与开发者工具/代码领域语义重复',
},
{
sourceSlug: '知识管理',
targetSlug: 'knowledge-rag',
reason: '知识管理与知识管理/检索/RAG 领域语义重复',
},
{
sourceSlug: 'knowledge-graph',
targetSlug: 'knowledge-rag',
reason: '知识图谱归并到知识管理/检索/RAG 领域',
},
{
sourceSlug: '向量数据库',
targetSlug: 'knowledge-rag',
reason: '向量数据库在本项目中归并到知识管理/检索/RAG 领域',
},
{
sourceSlug: 'ai-security',
targetSlug: 'security-privacy',
reason: 'AI 安全与安全/隐私领域语义重复',
},
{
sourceSlug: '隐私保护',
targetSlug: 'security-privacy',
reason: '隐私保护与安全/隐私领域语义重复',
},
{
sourceSlug: 'data-analytics',
targetSlug: 'data-bi',
reason: '数据分析与数据分析/BI/可视化领域语义重复',
},
{
sourceSlug: 'visualization',
targetSlug: 'data-bi',
reason: '可视化与数据分析/BI/可视化领域语义重复',
},
{
sourceSlug: 'computer-vision',
targetSlug: 'vision-multimodal',
reason: '计算机视觉与计算机视觉/多模态领域语义重复',
},
{
sourceSlug: 'enterprise-ai',
targetSlug: 'enterprise-office',
reason: '企业级 AI 应用与企业应用/办公领域语义重复',
},
{
sourceSlug: 'chatbot',
targetSlug: 'enterprise-office',
reason: '聊天机器人与企业应用/办公领域语义重复',
},
]
const CANONICAL_CATEGORIES: TagCategory[] = ['DOMAIN_SCENARIO', 'FIXED_PROJECT_TYPE']
async function dedupeRule(rule: DedupeRule) {
return prisma.$transaction(async (tx) => {
const source = await tx.tag.findUnique({
where: { slug: rule.sourceSlug },
include: { _count: { select: { projects: true } } },
})
if (!source) {
return {
status: 'skipped' as const,
sourceSlug: rule.sourceSlug,
targetSlug: rule.targetSlug,
message: 'source_not_found',
}
}
if (source.category !== 'FREE_TAG') {
return {
status: 'skipped' as const,
sourceSlug: rule.sourceSlug,
targetSlug: rule.targetSlug,
message: `source_not_free_tag:${source.category}`,
}
}
const target = await tx.tag.findUnique({
where: { slug: rule.targetSlug },
include: { _count: { select: { projects: true } } },
})
if (!target) {
return {
status: 'skipped' as const,
sourceSlug: rule.sourceSlug,
targetSlug: rule.targetSlug,
message: 'target_not_found',
}
}
if (!CANONICAL_CATEGORIES.includes(target.category)) {
return {
status: 'skipped' as const,
sourceSlug: rule.sourceSlug,
targetSlug: rule.targetSlug,
message: `target_not_canonical:${target.category}`,
}
}
const sourceLinks = await tx.projectTag.findMany({
where: { tagId: source.id },
select: { projectId: true },
})
if (sourceLinks.length > 0) {
await tx.projectTag.createMany({
data: sourceLinks.map((link) => ({
projectId: link.projectId,
tagId: target.id,
})),
skipDuplicates: true,
})
}
await tx.tag.delete({
where: { id: source.id },
})
return {
status: 'deduped' as const,
sourceSlug: rule.sourceSlug,
targetSlug: rule.targetSlug,
movedProjectLinks: sourceLinks.length,
sourceProjectCount: source._count.projects,
reason: rule.reason,
}
})
}
async function main() {
console.log('[dedupe-free-tags] start')
const results: Array<Record<string, unknown>> = []
for (const rule of DEDUPE_RULES) {
const result = await dedupeRule(rule)
results.push(result)
}
const dedupedCount = results.filter((item) => item.status === 'deduped').length
const skippedCount = results.filter((item) => item.status === 'skipped').length
console.log('[dedupe-free-tags] done', {
totalRules: DEDUPE_RULES.length,
dedupedCount,
skippedCount,
})
console.log('[dedupe-free-tags] results', results)
}
main()
.catch((error) => {
console.error('[dedupe-free-tags] failed', error)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})
-39
View File
@@ -1,39 +0,0 @@
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function main() {
const tags = await prisma.tag.findMany({
include: {
_count: {
select: { projects: true }
}
}
});
console.log('=== Tag Statistics ===\n');
console.log('Total tags:', tags.length);
const sortedTags = tags.sort((a, b) => b._count.projects - a._count.projects);
console.log('\n=== Tags by Project Count ===');
sortedTags.forEach((tag, idx) => {
console.log(`${idx + 1}. ${tag.name}: ${tag._count.projects} projects`);
});
const unusedTags = tags.filter(t => t._count.projects === 0);
console.log(`\n=== Unused Tags (${unusedTags.length}) ===`);
unusedTags.forEach(tag => console.log(`- ${tag.name}`));
const lowActivityTags = tags.filter(t => t._count.projects > 0 && t._count.projects <= 2);
console.log(`\n=== Low Activity Tags (1-2 projects) (${lowActivityTags.length}) ===`);
lowActivityTags.forEach(tag => console.log(`- ${tag.name}: ${tag._count.projects} projects`));
const highActivityTags = tags.filter(t => t._count.projects >= 5);
console.log(`\n=== High Activity Tags (5+ projects) (${highActivityTags.length}) ===`);
highActivityTags.forEach(tag => console.log(`- ${tag.name}: ${tag._count.projects} projects`));
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());
-320
View File
@@ -1,320 +0,0 @@
import { PrismaClient } from '@prisma/client'
import {
DOMAIN_SCENARIO_PRESET_TAGS,
type DomainScenarioPresetSlug,
} from '../src/lib/tag-taxonomy'
const prisma = new PrismaClient()
type DomainRule = {
slug: DomainScenarioPresetSlug
aliasSlugs: string[]
keywords: string[]
minScore: number
minKeywordHits: number
}
const DOMAIN_RULES: DomainRule[] = [
{
slug: 'code-dev',
aliasSlugs: [
'code-dev',
'ai-development-tool',
'code-generation',
'tool-calling',
'vs-code-extension',
'cli',
'library',
'agent-framework',
'ai-agent-framework',
],
keywords: ['code', 'coding', 'programming', 'developer', '编程', '代码', '开发工具'],
minScore: 2,
minKeywordHits: 2,
},
{
slug: 'automation-workflow',
aliasSlugs: [
'automation-workflow',
'automation',
'workflow-automation',
'workflow-orchestration',
'web-automation',
'browser-automation',
'rpa',
],
keywords: ['workflow', 'automation', 'orchestration', 'rpa', '自动化', '工作流', '编排'],
minScore: 2,
minKeywordHits: 1,
},
{
slug: 'knowledge-rag',
aliasSlugs: [
'knowledge-rag',
'knowledge-management',
'知识管理',
'knowledge-base',
'knowledge-graph',
'rag',
'向量数据库',
'vector-database',
'llamaindex',
],
keywords: ['knowledge', 'retrieval', 'memory', 'rag', '知识', '检索', '记忆', '知识库'],
minScore: 2,
minKeywordHits: 1,
},
{
slug: 'education-research',
aliasSlugs: [
'education-research',
'docs-tutorial',
'ai-research',
'markdown',
'tutorial',
'guide',
'paper',
'benchmark',
],
keywords: ['tutorial', 'guide', 'docs', 'paper', 'course', '教程', '指南', '文档', '论文', '课程'],
minScore: 2,
minKeywordHits: 1,
},
{
slug: 'model-inference',
aliasSlugs: [
'model-inference',
'inference-model',
'llm',
'大语言模型',
'transformers',
'pytorch',
'machine-learning',
'deep-learning',
'reinforcement-learning',
'vllm',
],
keywords: ['model', 'inference', 'training', 'finetune', '模型', '推理', '训练', '微调'],
minScore: 2,
minKeywordHits: 1,
},
{
slug: 'api-integration',
aliasSlugs: [
'api-integration',
'model-context-protocol',
'sdk',
'api-gateway',
],
keywords: ['api', 'sdk', 'protocol', 'integration', 'mcp', '接口', '协议', '集成'],
minScore: 2,
minKeywordHits: 1,
},
{
slug: 'vision-multimodal',
aliasSlugs: ['vision-multimodal', 'computer-vision', 'multimodal', 'multimodal-ai'],
keywords: ['vision', 'image', 'video', 'multimodal', '视觉', '图像', '视频', '多模态'],
minScore: 2,
minKeywordHits: 1,
},
{
slug: 'data-bi',
aliasSlugs: ['data-bi', 'data-analytics', '数据分析', 'visualization'],
keywords: ['analytics', 'dashboard', 'visualization', 'business intelligence', '数据分析', '可视化'],
minScore: 2,
minKeywordHits: 1,
},
{
slug: 'security-privacy',
aliasSlugs: ['security-privacy', 'ai-security', '隐私保护'],
keywords: ['security', 'privacy', 'safety', '安全', '隐私'],
minScore: 2,
minKeywordHits: 1,
},
{
slug: 'enterprise-office',
aliasSlugs: ['enterprise-office', 'enterprise-ai', 'chatbot'],
keywords: ['enterprise', 'office', 'collaboration', 'crm', 'erp', '企业', '办公', '协作'],
minScore: 2,
minKeywordHits: 1,
},
{
slug: 'finance',
aliasSlugs: ['finance', 'fintech'],
keywords: ['finance', 'financial', 'trading', 'fintech', 'quant', '金融', '交易', '量化', '风控'],
minScore: 1,
minKeywordHits: 1,
},
{
slug: 'medical-biomed',
aliasSlugs: ['medical-biomed', 'medical', 'healthcare', 'biomedical'],
keywords: ['medical', 'healthcare', 'medicine', 'biomed', '医疗', '医学', '医药', '生物医学'],
minScore: 1,
minKeywordHits: 1,
},
]
const FALLBACK_BY_FIXED_PROJECT_TYPE: Partial<Record<string, DomainScenarioPresetSlug>> = {
'agent-tooling': 'code-dev',
'inference-model': 'model-inference',
'docs-tutorial': 'education-research',
}
function normalize(value: string): string {
return value.toLowerCase().trim()
}
function inferDomainSlugs(input: {
name: string
nameEn?: string | null
description: string
descriptionEn?: string | null
tagSlugs: string[]
}): DomainScenarioPresetSlug[] {
const normalizedTagSet = new Set(input.tagSlugs.map((slug) => normalize(slug)))
const text = normalize(
[input.name, input.nameEn || '', input.description, input.descriptionEn || '', ...input.tagSlugs].join(' ')
)
const scoredDomains: Array<{ slug: DomainScenarioPresetSlug; score: number }> = []
for (const rule of DOMAIN_RULES) {
const tagHits = rule.aliasSlugs.reduce(
(acc, slug) => acc + (normalizedTagSet.has(normalize(slug)) ? 1 : 0),
0
)
const keywordHits = rule.keywords.reduce(
(acc, keyword) => acc + (text.includes(normalize(keyword)) ? 1 : 0),
0
)
const score = tagHits * 3 + keywordHits
if (score >= rule.minScore && (tagHits > 0 || keywordHits >= rule.minKeywordHits)) {
scoredDomains.push({ slug: rule.slug, score })
}
}
if (scoredDomains.length > 0) {
return scoredDomains
.sort((a, b) => b.score - a.score)
.slice(0, 3)
.map((item) => item.slug)
}
const fixedProjectType = input.tagSlugs.find((slug) => FALLBACK_BY_FIXED_PROJECT_TYPE[slug])
if (fixedProjectType) {
return [FALLBACK_BY_FIXED_PROJECT_TYPE[fixedProjectType]!]
}
return ['code-dev']
}
async function main() {
console.log('[domain] start sync')
const canonicalTagIdMap = new Map<DomainScenarioPresetSlug, string>()
for (const domainTag of DOMAIN_SCENARIO_PRESET_TAGS) {
const tag = await prisma.tag.upsert({
where: { slug: domainTag.slug },
update: {
name: domainTag.name,
nameEn: domainTag.nameEn,
category: 'DOMAIN_SCENARIO',
},
create: {
name: domainTag.name,
nameEn: domainTag.nameEn,
slug: domainTag.slug,
category: 'DOMAIN_SCENARIO',
},
})
canonicalTagIdMap.set(domainTag.slug, tag.id)
}
const clearedLinks = await prisma.projectTag.deleteMany({
where: {
tagId: {
in: Array.from(canonicalTagIdMap.values()),
},
},
})
const projects = await prisma.project.findMany({
where: { status: 'ACTIVE' },
select: {
id: true,
name: true,
nameEn: true,
description: true,
descriptionEn: true,
tags: {
select: {
tag: {
select: {
slug: true,
},
},
},
},
},
})
const createData: Array<{ projectId: string; tagId: string }> = []
const distribution = new Map<DomainScenarioPresetSlug, number>()
for (const domainTag of DOMAIN_SCENARIO_PRESET_TAGS) {
distribution.set(domainTag.slug, 0)
}
for (const project of projects) {
const tagSlugs = project.tags.map((item) => item.tag.slug)
const domainSlugs = inferDomainSlugs({
name: project.name,
nameEn: project.nameEn,
description: project.description,
descriptionEn: project.descriptionEn,
tagSlugs,
})
for (const domainSlug of domainSlugs) {
const domainTagId = canonicalTagIdMap.get(domainSlug)
if (!domainTagId) {
throw new Error(`Missing canonical domain tag id: ${domainSlug}`)
}
createData.push({
projectId: project.id,
tagId: domainTagId,
})
distribution.set(domainSlug, (distribution.get(domainSlug) || 0) + 1)
}
}
const insertedLinks = await prisma.projectTag.createMany({
data: createData,
skipDuplicates: true,
})
const distributionSummary = DOMAIN_SCENARIO_PRESET_TAGS.map((item) => ({
slug: item.slug,
name: item.name,
projectCount: distribution.get(item.slug) || 0,
}))
console.log('[domain] done', {
canonicalDomainTagCount: DOMAIN_SCENARIO_PRESET_TAGS.length,
clearedCanonicalDomainLinks: clearedLinks.count,
insertedDomainLinks: insertedLinks.count,
activeProjectCount: projects.length,
})
console.log('[domain] distribution', distributionSummary)
}
main()
.catch((error) => {
console.error('[domain] failed', error)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})
-47
View File
@@ -1,47 +0,0 @@
# src/ - Source Code
<!-- Parent: ../AGENTS.md -->
## OVERVIEW
Application source code organized by Next.js 15 App Router conventions with internationalization support.
## STRUCTURE
```
src/
├── app/ # Next.js App Router (pages + API)
├── components/ # React components (neo-brutalism design)
├── hooks/ # Server-side data fetching (NOT React hooks)
├── lib/ # Utilities, validation, Prisma client
├── i18n/ # next-intl configuration
├── messages/ # Translation files (zh.json, en.json)
└── middleware.ts # Locale detection & routing
```
## KEY DIRECTORIES
| Directory | Purpose | See |
|-----------|---------|-----|
| `app/` | App Router pages & API routes | `app/AGENTS.md` |
| `components/` | UI components by domain | `components/AGENTS.md` |
| `hooks/` | Server data fetching functions | `hooks/AGENTS.md` |
| `lib/` | Core utilities & validation | `lib/AGENTS.md` |
## FOR AI AGENTS
### When Adding New Features
1. **Pages**: Add to `app/[locale]/` following locale pattern
2. **API**: Add to `app/api/` with Zod validation
3. **Components**: Add to appropriate domain folder
4. **Translations**: Update BOTH `zh.json` AND `en.json`
### Common Patterns
- **Locale routing**: `middleware.ts` handles locale detection
- **Data fetching**: Use functions from `hooks/` in Server Components
- **Validation**: All schemas in `lib/validations.ts`
- **Styling**: Neo-brutalism with Tailwind (see `components/AGENTS.md`)
<!-- MANUAL: Additional notes can be added below -->
-64
View File
@@ -1,64 +0,0 @@
# src/app/ - App Router Implementation
<!-- Parent: ../AGENTS.md -->
## OVERVIEW
Next.js 15 App Router with next-intl locale routing and multilingual project discovery platform
## STRUCTURE
- `[locale]/` - Locale-scoped routes (zh/en), home, projects, keyword-cloud, timeline
- `api/` - API endpoints (webhook, discovery, keyword-cloud, search, events, tags)
- `globals.css` - Tailwind + neo-brutalism styles
- `layout.tsx` - Root layout
## KEY FILES
| File | Purpose |
|------|---------|
| `[locale]/page.tsx` | Home page |
| `[locale]/projects/page.tsx` | Project listing |
| `[locale]/projects/[id]/page.tsx` | Project detail (ISR 5min) |
| `[locale]/keyword-cloud/page.tsx` | Quarterly keyword cloud |
| `[locale]/timeline/page.tsx` | AI historical events timeline |
| `api/webhook/projects/route.ts` | Project ingestion webhook |
| `api/discovery/tasks/route.ts` | Discovery task CRUD |
| `api/search/ai/route.ts` | AI-powered RAG search |
| `api/events/route.ts` | AI Timeline events API |
| `api/tags/route.ts` | Tags CRUD |
| `api/tags/maintenance/route.ts` | Tag cleanup (n8n integration) |
## WHERE TO LOOK
- **Locale routing**: `src/middleware.ts`, `src/app/[locale]/layout.tsx`
- **API patterns**: `src/app/api/webhook/projects/route.ts`
- **ISR configuration**: `src/app/[locale]/projects/[id]/page.tsx:93`
- **Discovery system**: `src/app/api/discovery/tasks/route.ts`
## CONVENTIONS
- **Middleware**: `createMiddleware({ locales: ['zh', 'en'], defaultLocale='zh', localePrefix='always' })`
- **Route params**: Always `params: Promise<{...}>`, await before use
- **Locale setup**: `setRequestLocale(locale)` in layouts/pages
- **Translations**: `getTranslations('namespace')` for server-side
- **API auth**: `crypto.timingSafeEqual()` for timing-safe API key validation
- **Webhook**: Partial success mode - continue processing if individual items fail
- **Deduplication**: Multi-level (GitHub URL → Website URL → slug) via `discovery-service`
- **ISR**: Export `revalidate = N` constant in page files
- **Parallel queries**: `Promise.all()` for independent data fetching
- **N+1 prevention**: Batch queries with `findMany({ where: { field: { in: [...] } } })`
- **Content selection**: `locale === 'en' && fieldEn ? fieldEn : field` pattern
## ANTI-PATTERNS
- NEVER bypass locale routing (API routes excluded)
- NEVER use `crypto.compare()` or string comparison for API keys (timing attacks)
- NEVER skip Zod validation in API routes
- NEVER embed API keys in error responses (log only)
- NEVER process webhook without partial success mode
- NEVER run queries in loops (N+1 problem)
- NEVER use client-side translations in server components
- NEVER skip `setRequestLocale()` in locale routes
- NEVER omit ISR for dynamic content (5+ minute revalidation)
- NEVER return raw database errors to clients
-62
View File
@@ -1,62 +0,0 @@
# src/components/ - Component System
<!-- Parent: ../AGENTS.md -->
## Neo-Brutalism Design Principles
**Visual Identity:**
- Sharp corners (0px radius) - no rounded edges
- Bold borders (4px solid shadows) for depth
- High contrast colors with hard edges
- Gold (#FFD700) primary, Orange (#ff6f00) secondary
- Space Mono headings, Inter body text
**Component Styling:**
```tsx
// Base button pattern - no border-radius, hard shadows
className="border-4 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]
hover:translate-x-1 hover:translate-y-1 hover:shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]"
```
## Component Organization
```
src/components/
├── layout/ # Layout primitives (Header, Footer, AnnouncementBar)
├── locale/ # I18n utilities (LocaleSwitcher)
├── project/ # Project-specific components (ProjectCard, ProjectDetail)
├── search/ # Search functionality (SearchBar)
└── ui/ # Base UI components (Button, Card, Badge)
```
## Component Patterns
### Composition Pattern
- Build complex components from small reusable primitives
- Example: `ProjectCard` = `Card` + `Badge` + `ExternalLink`
### Server Components First
- Default to Server Components for data fetching
- Use Client Components (`"use client"`) only for interactivity
- Pass server data via props to client components
### Responsive Design
- Mobile-first Tailwind classes: `grid-cols-1 md:grid-cols-2 lg:grid-cols-3`
- Breakpoints: md (768px), lg (1024px)
### Accessibility
- Semantic HTML elements
- Keyboard navigation support
- ARIA labels where needed
## State Management
- Server state: Prisma queries in Server Components
- Client state: React hooks (useState, useEffect) for interactivity
- Global state: Context providers for locale, theme
-51
View File
@@ -1,51 +0,0 @@
# src/hooks/ - Server-Side Data Fetching
<!-- Parent: ../AGENTS.md -->
**Purpose**: Server-side data fetching functions (NOT React hooks). Exported functions for use in Server Components and API routes.
## Architecture
- **Server Functions Only**: No React hooks, no client-side state
- **Direct Prisma Queries**: ORM queries with optimization patterns
- **ISR Support**: Used by Next.js App Router for incremental static revalidation
## Key Functions
### Project Data (`useProjects.ts`)
- `getProjects()`: List projects with tags, pagination, filtering (search/tags/status)
- Flattens `ProjectTag` junction table to return tags directly
- Optimized for list queries with selective field loading
- `getProjectBySlug(slug)`: Get single project by slug
- Used by ISR pages with 5-min revalidation
- Includes all tags and external links
- `getAllTags()`: All tags with project counts
- `getTagsWithProjectCounts()`: Tags sorted by popularity (count DESC)
### Keyword Cloud (`useKeywordCloud.ts`)
- `getAllQuarters()`: List all quarters with metadata
- `getQuarterByQuarter(quarter)`: Single quarter with keyword count
- `getKeywordsByQuarter(quarter)`: All keywords for a quarter
- `getVisualStyleRules()`: Visual style configuration for word cloud
## Query Optimization Patterns
**N+1 Prevention**: Batch queries for related data (tags, links)
**Selective Loading**: Only fetch required fields via Prisma `select`
**Index Utilization**: Leverages database indexes (status+createdAt, slug, etc.)
**Pagination**: Limit/offset to prevent large result sets
## ISR Strategy
- **Revalidation**: 5-minute revalidate on project detail pages
- **Stale Data Acceptable**: Project listings tolerate slight staleness
- **Cache Busting**: Use `revalidatePath()` when data changes via webhook
## Best Practices
- Always import via `@/hooks/use*` (not relative paths)
- Return plain objects/arrays, never Prisma model instances
- Handle errors at route level, not in these functions
- Add new functions to existing files or create new `use*.ts` files
-68
View File
@@ -1,68 +0,0 @@
# src/lib/ - Core Utilities
<!-- Parent: ../AGENTS.md -->
## Overview
Shared utilities and infrastructure layers used across the application.
## Key Files
### Validation Layer (`validations.ts`)
**Purpose**: Input validation and type safety using Zod schemas
- `ProjectInputSchema`: Project data validation (1+ tags, 1-10 links)
- `WebhookPayloadSchema`: Webhook request validation with API key auth
- `KeywordInputSchema`: Keyword cloud data with visual config
- `ProjectQuerySchema`: Query parameter validation (search, tags, status, pagination)
- `ExternalLinkSchema`: URL validation with http/https protocol check
- `TaskStatusEnum`: Discovery task states (PENDING/IN_PROGRESS/COMPLETED/FAILED)
### Prisma Client (`prisma.ts`)
**Purpose**: Singleton database client pattern
```typescript
import { prisma } from "@/lib/prisma";
```
### Utilities
- `utils.ts`: `cn()` - Tailwind class merging with clsx/twMerge
- `slug.ts`: `generateSlug()` - URL-friendly slug generation (prefers English, max 100 chars)
### GitHub Integration
- `github/api.ts`: `getGitHubStats()` - Fetch repo stats (stars, forks, license) with 5min cache
- `github/badges.ts`: `parseGitHubUrl()`, `getGitHubStarsBadgeUrl()`, `getAllGitHubBadgeUrls()`
## Usage Patterns
**Validation**:
```typescript
import { ProjectInputSchema } from "@/lib/validations";
const validated = ProjectInputSchema.parse(inputData);
```
**Database**:
```typescript
import { prisma } from "@/lib/prisma";
const projects = await prisma.project.findMany();
```
**GitHub Badges**:
```typescript
import { getGitHubBadgesFromLinks } from "@/lib/github/badges";
const { stars } = getGitHubBadgesFromLinks(project.externalLinks);
```
## Key Design Decisions
- Singleton Prisma client prevents connection pool exhaustion in dev
- All enums use Zod for runtime type checking
- API key minimum length: 32 characters
- URL validation enforces http/https protocol only