diff --git a/.claude/AGENTS.md b/.claude/AGENTS.md deleted file mode 100644 index 3856f02..0000000 --- a/.claude/AGENTS.md +++ /dev/null @@ -1,67 +0,0 @@ -# .claude/ - Custom Claude Configuration - - - -## 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] -``` - - diff --git a/.gitignore b/.gitignore index abb6b81..78a5f4f 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.opencode/mcp.json b/.opencode/mcp.json deleted file mode 100644 index c5e3fb9..0000000 --- a/.opencode/mcp.json +++ /dev/null @@ -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": "" - } - } - } -} diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 13382cb..0000000 --- a/AGENTS.md +++ /dev/null @@ -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**: `: ` (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 ` 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 - - diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 115433e..0000000 --- a/CLAUDE.md +++ /dev/null @@ -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 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 ` 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 diff --git a/docs/AGENTS.md b/docs/AGENTS.md deleted file mode 100644 index 19fee8d..0000000 --- a/docs/AGENTS.md +++ /dev/null @@ -1,66 +0,0 @@ -# docs/ - Project Documentation - - - -## 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 - - diff --git a/docs/analyst.md b/docs/analyst.md deleted file mode 100644 index e62f4b8..0000000 --- a/docs/analyst.md +++ /dev/null @@ -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、或者邮件订阅文案)展开详细方案吗? \ No newline at end of file diff --git a/docs/plans/2026-01-25-ai-search-implementation.md b/docs/plans/2026-01-25-ai-search-implementation.md deleted file mode 100644 index 11f3bff..0000000 --- a/docs/plans/2026-01-25-ai-search-implementation.md +++ /dev/null @@ -1,1145 +0,0 @@ -# AI 智能搜索系统实施计划 - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** 为项目列表添加 AI 语义搜索功能,支持自然语言查询并返回语义相关的项目,所有 AI 逻辑在 n8n 中实现,Next.js 应用保持纯净。 - -**Architecture:** 使用 Neon PostgreSQL 的 pgvector 扩展存储项目向量,n8n 工作流处理向量化(定时任务)和 AI 搜索(Webhook),Next.js API 路由作为代理转发请求到 n8n,前端添加切换按钮和相似度展示。 - -**Tech Stack:** Neon PostgreSQL + pgvector, n8n, OpenAI Embeddings API (text-embedding-3-small), Next.js 15 App Router, Prisma ORM, TypeScript, Tailwind CSS - ---- - -## 前置准备 - -### Task 0: 验证环境和依赖 - -**Step 1: 验证 Neon 数据库 pgvector 支持** - -连接到 Neon 数据库(使用 Prisma Studio 或 psql): - -```bash -# 使用 psql 连接 -psql $DATABASE_URL - -# 或使用 Neon SQL Editor: https://console.neon.tech -``` - -执行 SQL: - -```sql --- 检查 pgvector 扩展是否已安装 -SELECT * FROM pg_extension WHERE extname = 'vector'; - --- 如果未安装,安装它 -CREATE EXTENSION IF NOT EXISTS vector; - --- 验证安装 -SELECT vector_dims('[1,2,3]'::vector); -``` - -预期输出:`3` - ---- - -## Phase 1: 数据库迁移 - -### Task 1: 创建 Prisma Schema 迁移 - -**Files:** -- Modify: `prisma/schema.prisma` - -**Step 1: 添加向量字段到 Project 模型** - -在 `prisma/schema.prisma` 中找到 `model Project`,在字段列表末尾添加: - -```prisma -model Project { - // ... 现有字段(id, name, nameEn, slug, description, descriptionEn, content, contentEn, status, source, createdAt, updatedAt) - - // 新增:向量嵌入字段 - embedding vector(1536)? // pgvector 类型,1536维(OpenAI text-embedding-3-small) - embeddingUpdatedAt DateTime? // 记录向量化更新时间 -} -``` - -**Step 2: 创建迁移文件** - -```bash -# 生成迁移(会创建 SQL 文件) -pnpm prisma migrate dev --name add_project_embedding - -# 预期输出: -# ✓ The following migration has been created and applied from new schema changes: -# migrations/TIMESTAMP_add_project_embedding/migration.sql -``` - -**Step 3: 验证生成的 SQL** - -打开生成的迁移文件(例如 `prisma/migrations/20260125000000_add_project_embedding/migration.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; -``` - -**Step 4: 手动优化迁移(可选)** - -如果生成的 SQL 不包含索引,手动修改迁移文件添加上述索引 SQL。 - -**Step 5: 应用迁移** - -```bash -# 如果在上一步中已经自动应用,跳过此步骤 -# 否则手动应用: -pnpm prisma migrate deploy -``` - -**Step 6: 验证迁移** - -```sql --- 检查表结构 -\d "Project" - --- 应该看到: --- embedding | vector(1536) | --- embeddingUpdatedAt | timestamp | - --- 检查索引 -SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'Project'; - --- 应该看到: --- idx_project_embedding_cosine --- idx_project_embedding_null -``` - -**Step 7: 更新 Prisma Client** - -```bash -pnpm prisma generate -``` - -**Step 8: 提交** - -```bash -git add prisma/schema.prisma prisma/migrations -git commit -m "feat: 添加项目向量嵌入字段和 pgvector 索引" -``` - ---- - -## Phase 2: n8n 工作流配置 - -### Task 2: 配置 n8n 凭证 - -**Step 1: 获取 OpenAI API Key** - -访问 https://platform.openai.com/api-keys 创建新的 API Key。 - -**Step 2: 在 n8n 中添加 OpenAI 凭证** - -1. 打开 n8n 实例 -2. 导航到 **Credentials** → **New** -3. 选择 **OpenAI API** -4. 输入名称:`OpenAI Embeddings` -5. 粘贴 API Key -6. 保存 - -**Step 3: 添加 Neon 数据库凭证** - -1. 在 n8n 中导航到 **Credentials** → **New** -2. 选择 **PostgreSQL** -3. 输入名称:`Neon Database` -4. 配置连接: - - Host: 从 `DATABASE_URL` 提取(例如 `xxx.neon.tech`) - - Database: 数据库名(例如 `neondb`) - - User: 用户名 - - Password: 密码 - - SSL: 启用 -5. 点击 **Test Connection** 验证 -6. 保存 - -**注意**:不提交凭证到 git。 - ---- - -### Task 3: 创建向量化工作流 - -**Step 1: 创建新工作流** - -在 n8n 中: -1. 点击 **New Workflow** -2. 命名为:`Project Vectorization` - -**Step 2: 添加 Cron 触发器** - -1. 添加节点:**Cron** -2. 配置: - - Trigger Interval: `Every 5 minutes` - - Cron Expression: `*/5 * * * *` -3. 保存节点 - -**Step 3: 添加 PostgreSQL 节点(查询未向量化项目)** - -1. 添加节点:**Postgres**(使用 `Neon Database` 凭证) -2. 操作:**Execute Query** -3. 输入查询: - -```sql -SELECT id, name, "nameEn", description, "descriptionEn", content, "contentEn" -FROM "Project" -WHERE "embedding" IS NULL - AND "status" = 'ACTIVE' -LIMIT 20 -``` - -4. 测试节点(应该返回现有项目列表) -5. 保存节点 - -**Step 4: 添加 Function 节点(构造文本内容)** - -1. 添加节点:**Function** → **JavaScript** -2. 命名:`Construct Text Content` -3. 输入代码: - -```javascript -// 为每个项目构造用于向量化的文本内容 -const projects = $input.all(); - -return projects.map(item => { - const project = item.json; - - // 合并字段,按权重构造 - const parts = [ - project.name || '', - project.nameEn || '', - project.description || '', - project.descriptionEn || '', - // tags 稍后处理 - (project.content || '').substring(0, 500), - (project.contentEn || '').substring(0, 500) - ].filter(Boolean); - - const textContent = parts.join('\n\n'); - - return { - json: { - ...project, - textContent: textContent - } - }; -}); -``` - -4. 测试节点(应该添加 `textContent` 字段) -5. 保存节点 - -**Step 5: 添加 Split in Batches 节点** - -1. 添加节点:**Split In Batches** -2. 配置: - - Batch Size: `5`(每批 5 个项目,避免 API 限流) -3. 保存节点 - -**Step 6: 添加 OpenAI Embeddings 节点** - -1. 添加节点:**OpenAI** → **Embeddings** -2. 使用 `OpenAI Embeddings` 凭证 -3. 配置: - - Model: `text-embedding-3-small` - - Input: `{{ $json.textContent }}` -4. 测试节点(应该返回向量数组) -5. 保存节点 - -**Step 7: 添加 PostgreSQL 节点(更新 embedding)** - -1. 添加节点:**Postgres** -2. 操作:**Execute Query** -3. 输入查询: - -```sql -UPDATE "Project" -SET - "embedding" = '{{ $json.data[0].embedding }}'::vector, - "embeddingUpdatedAt" = NOW() -WHERE "id" = {{ $json.id }} -``` - -4. 测试节点 -5. 保存节点 - -**Step 8: 添加 Wait 节点(控制速率)** - -1. 添加节点:**Wait** -2. 配置: - - Amount: `1` 秒 -3. 保存节点(在 Split in Batches 和 OpenAI 之间) - -**Step 9: 连接节点** - -按顺序连接: -``` -Cron → Postgres (查询) → Function (构造) → Split in Batches → Wait → OpenAI Embeddings → Postgres (更新) → Loop back to Split in Batches -``` - -**Step 10: 测试完整工作流** - -1. 点击 **Test Workflow** -2. 手动触发 Cron 节点 -3. 验证: - - 查询返回未向量化项目 - - 文本内容正确构造 - - OpenAI API 成功调用 - - 数据库成功更新 - -**Step 11: 激活工作流** - -1. 点击 **Active** 开关 -2. 验证 Cron 调度正常运行 - -**Step 12: 验证向量化结果** - -```sql --- 检查已向量化项目数 -SELECT COUNT(*) FROM "Project" WHERE "embedding" IS NOT NULL; - --- 查看某个项目的向量 -SELECT id, name, "embeddingUpdatedAt" -FROM "Project" -WHERE "embedding" IS NOT NULL -LIMIT 5; -``` - ---- - -### Task 4: 创建 AI 搜索工作流 - -**Step 1: 创建新工作流** - -在 n8n 中: -1. 点击 **New Workflow** -2. 命名为:`AI Semantic Search` - -**Step 2: 添加 Webhook 触发器** - -1. 添加节点:**Webhook** -2. 配置: - - HTTP Method: `POST` - - Path: `ai-search` - - Response Mode: **When Last Node Finishes** -3. 记录 Webhook URL(例如 `https://your-n8n.com/webhook/ai-search`) -4. 测试 Webhook(curl): - -```bash -curl -X POST https://your-n8n.com/webhook/ai-search \ - -H "Content-Type: application/json" \ - -d '{"query":"test","locale":"zh","limit":5}' -``` - -5. 保存节点 - -**Step 3: 添加 OpenAI Embeddings 节点(生成查询向量)** - -1. 添加节点:**OpenAI** → **Embeddings** -2. 使用 `OpenAI Embeddings` 凭证 -3. 配置: - - Model: `text-embedding-3-small` - - Input: `{{ $json.query }}` -4. 测试节点 -5. 保存节点 - -**Step 4: 添加 PostgreSQL 节点(向量相似度搜索)** - -1. 添加节点:**Postgres** -2. 操作:**Execute Query** -3. 输入查询: - -```sql -SELECT - p.id, - p.name, - p."nameEn", - p.slug, - p.description, - p."descriptionEn", - p.status, - p."createdAt", - 1 - (p."embedding" <=> '{{ $json.data[0].embedding }}'::vector) as similarity -FROM "Project" p -WHERE p."embedding" IS NOT NULL - AND p.status = 'ACTIVE' -ORDER BY p."embedding" <=> '{{ $json.data[0].embedding }}'::vector -LIMIT {{ $json.limit || 20 }} -``` - -4. 测试节点(应该返回相似度排序的项目) -5. 保存节点 - -**Step 5: 添加 Function 节点(查询项目标签)** - -1. 添加节点:**Function** → **JavaScript** -2. 输入代码: - -```javascript -const results = $input.all(); -const projectIds = results.map(r => r.json.id).join(','); - -// 为每个项目查询标签 -return results.map(item => { - return { - json: { - ...item.json, - projectId: item.json.id - } - }; -}); -``` - -3. 保存节点 - -**Step 6: 添加 PostgreSQL 节点(查询标签)** - -1. 添加节点:**Postgres** -2. 操作:**Execute Query** -3. 输入查询: - -```sql -SELECT - t.id, - t.name, - t."nameEn", - t.slug, - pt."projectId" -FROM "Tag" t -INNER JOIN "ProjectTag" pt ON t.id = pt."tagId" -WHERE pt."projectId" IN (SELECT UNNEST(STRING_TO_ARRAY('{{ $json.projectIds }}', ','))::INTEGER) -``` - -4. 保存节点 - -**Step 7: 添加 Function 节点(合并标签)** - -1. 添加节点:**Function** → **JavaScript** -2. 输入代码: - -```javascript -const projects = $('Postgres').all(); -const tags = $('Postgres1').all(); - -// 合并标签到项目 -const results = projects.map(project => { - const projectTags = tags - .filter(t => t.json.projectId === project.json.id) - .map(t => ({ - id: t.json.id, - name: t.json.name, - nameEn: t.json.nameEn, - slug: t.json.slug - })); - - return { - json: { - project: { - ...project.json, - tags: projectTags - }, - similarity: project.json.similarity, - matchReason: `相似度: ${(project.json.similarity * 100).toFixed(0)}%` - } - }; -}); - -return results; -``` - -3. 测试节点 -4. 保存节点 - -**Step 8: 添加 Function 节点(格式化响应)** - -1. 添加节点:**Function** → **JavaScript** -2. 输入代码: - -```javascript -const results = $input.all(); -const searchTime = $now.toMillis() - $('Webhook').item.json.startTime; - -return { - json: { - results: results.map(r => r.json), - total: results.length, - searchTime: searchTime - } -}; -``` - -3. 保存节点 - -**Step 9: 连接节点** - -``` -Webhook → OpenAI Embeddings → Postgres (向量搜索) → Function (查询标签) → Postgres (标签) → Function (合并) → Function (格式化) → Response -``` - -**Step 10: 测试完整工作流** - -1. 点击 **Test Workflow** -2. 在 Webhook 节点中点击 **Listen for Test Event** -3. 发送测试请求: - -```bash -curl -X POST https://your-n8n.com/webhook/ai-search \ - -H "Content-Type: application/json" \ - -d '{"query":"视频生成工具","locale":"zh","limit":5}' -``` - -4. 验证响应格式: - -```json -{ - "results": [ - { - "project": { /* 项目数据 */ }, - "similarity": 0.89, - "matchReason": "相似度: 89%" - } - ], - "total": 5, - "searchTime": 1234 -} -``` - -**Step 11: 激活工作流** - -1. 点击 **Active** 开关 -2. 保存生产环境的 Webhook URL - -**Step 12: 配置环境变量** - -在 `.env.local` 中添加: - -```bash -N8N_AI_SEARCH_WEBHOOK=https://your-n8n.com/webhook/ai-search -``` - ---- - -## Phase 3: Next.js API 实现 - -### Task 5: 创建 AI 搜索 API 路由 - -**Files:** -- Create: `src/app/api/search/ai/route.ts` - -**Step 1: 创建 API 路由文件** - -创建 `src/app/api/search/ai/route.ts`: - -```typescript -import { NextResponse } from 'next/server' -import { ProjectQuerySchema } from '@/lib/validations' - -const N8N_WEBHOOK_URL = process.env.N8N_AI_SEARCH_WEBHOOK - -if (!N8N_WEBHOOK_URL) { - throw new Error('N8N_AI_SEARCH_WEBHOOK environment variable is not set') -} - -export async function POST(request: Request) { - try { - const body = await request.json() - - // 验证查询参数 - const validatedQuery = ProjectQuerySchema.parse(body) - - // 转发到 n8n 工作流 - const n8nResponse = await fetch(N8N_AI_SEARCH_WEBHOOK, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - query: validatedQuery.search, - locale: body.locale || 'zh', - limit: validatedQuery.limit, - filters: { - tags: validatedQuery.tags, - status: validatedQuery.status - } - }) - }) - - if (!n8nResponse.ok) { - throw new Error(`n8n webhook failed: ${n8nResponse.statusText}`) - } - - const results = await n8nResponse.json() - - return NextResponse.json(results) - - } catch (error) { - console.error('AI search error:', error) - - if (error instanceof Error && error.name === 'ZodError') { - return NextResponse.json( - { error: 'Invalid query parameters', details: error }, - { status: 400 } - ) - } - - return NextResponse.json( - { error: 'AI search failed', message: error instanceof Error ? error.message : 'Unknown error' }, - { status: 500 } - ) - } -} -``` - -**Step 2: 测试 API 路由** - -启动开发服务器: - -```bash -pnpm dev -``` - -测试 API: - -```bash -curl -X POST http://localhost:3000/api/search/ai \ - -H "Content-Type: application/json" \ - -d '{"search":"视频生成工具","limit":5}' -``` - -预期响应: - -```json -{ - "results": [...], - "total": 5, - "searchTime": 1234 -} -``` - -**Step 3: 提交** - -```bash -git add src/app/api/search/ai/route.ts -git commit -m "feat: 添加 AI 搜索 API 路由" -``` - ---- - -## Phase 4: 前端组件实现 - -### Task 6: 修改 SearchBar 组件 - -**Files:** -- Modify: `src/components/search/SearchBar.tsx` - -**Step 1: 读取现有组件** - -```bash -# 查看现有组件结构 -cat src/components/search/SearchBar.tsx -``` - -**Step 2: 添加 AI 模式状态** - -在组件中添加: - -```tsx -'use client' - -import { useState } from 'react' -import { Sparkles, Search } from 'lucide-react' -// ... 保留现有 imports - -export function SearchBar() { - // ... 保留现有状态 - const [aiMode, setAiMode] = useState(false) - const [loading, setLoading] = useState(false) - - // ... 保留现有逻辑 -} -``` - -**Step 3: 修改搜索处理函数** - -更新 `handleSearch` 函数: - -```tsx -const handleSearch = async () => { - if (!query.trim()) return - - setLoading(true) - try { - const endpoint = aiMode ? '/api/search/ai' : '/api/projects' - const response = await fetch(endpoint, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - search: query, - locale: 'zh', - limit: 20 - }) - }) - - if (!response.ok) { - throw new Error('Search failed') - } - - const data = await response.json() - - // 处理结果... - // if (aiMode) { - // setAIResults(data.results) - // } else { - // setProjects(data.results) - // } - - } catch (error) { - console.error('Search error:', error) - // 显示错误提示 - } finally { - setLoading(false) - } -} -``` - -**Step 4: 添加 AI 切换按钮** - -在搜索输入框旁边添加: - -```tsx -
- {/* 搜索输入框 */} - setQuery(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleSearch()} - placeholder={ - aiMode - ? "描述你想要的项目..." - : "搜索项目名称..." - } - className="flex-1 px-4 py-2 border-2 border-gray-300" - /> - - {/* AI 模式切换按钮 */} - - - {/* 搜索按钮 */} - -
- -{/* AI 模式提示 */} -{aiMode && ( -
- 💡 试试:"帮我找能生成视频的 AI 工具" -
-)} -``` - -**Step 5: 测试组件** - -1. 访问 `http://localhost:3000/zh/projects` -2. 点击 AI 切换按钮(应该变金色) -3. 输入查询并搜索 -4. 验证加载状态显示 - -**Step 6: 提交** - -```bash -git add src/components/search/SearchBar.tsx -git commit -m "feat: 添加 AI 搜索模式切换按钮" -``` - ---- - -### Task 7: 创建 AI 搜索结果组件 - -**Files:** -- Create: `src/components/search/AISearchResults.tsx` - -**Step 1: 创建组件文件** - -创建 `src/components/search/AISearchResults.tsx`: - -```tsx -'use client' - -import { ProjectCard } from '@/components/project/ProjectCard' -import type { Project } from '@/hooks/useProjects' - -interface AISearchResult { - project: Project - similarity: number - matchReason?: string -} - -interface AISearchResultsProps { - results: AISearchResult[] -} - -export function AISearchResults({ results }: AISearchResultsProps) { - if (results.length === 0) { - return ( -
- 未找到相关项目,试试其他描述吧 -
- ) - } - - return ( -
- {/* 相似度说明 */} -
- 匹配度: -
- - - -
-
- - {/* 结果列表 */} - {results.map(({ project, similarity, matchReason }) => ( -
- {/* 相似度指示条 */} -
- - {/* 项目卡片 */} -
- - - {/* AI 匹配信息 */} -
-
- - 匹配度: {(similarity * 100).toFixed(0)}% - - {matchReason && ( - {matchReason} - )} -
-
-
-
- ))} -
- ) -} - -function getSimilarityColor(score: number): string { - if (score > 0.8) return '#22c55e' // green-500 - if (score > 0.6) return '#eab308' // yellow-500 - return '#ef4444' // red-500 -} -``` - -**Step 2: 在项目列表页面中使用** - -修改 `src/app/[locale]/projects/page.tsx`: - -```tsx -// 在页面中添加 AI 结果状态 -const [aiResults, setAIResults] = useState([]) -const [isAIResult, setIsAIResult] = useState(false) - -// 在搜索处理中设置结果 -if (aiMode) { - setAIResults(data.results) - setIsAIResult(true) -} else { - setProjects(data.results) - setIsAIResult(false) -} - -// 在渲染中切换 -{isAIResult ? ( - -) : ( - -)} -``` - -**Step 3: 测试组件** - -1. 访问项目列表页面 -2. 启用 AI 模式 -3. 搜索查询 -4. 验证: - - 相似度指示条显示正确颜色 - - 匹配度百分比正确 - - 项目卡片正常渲染 - -**Step 4: 提交** - -```bash -git add src/components/search/AISearchResults.tsx -git add src/app/[locale]/projects/page.tsx -git commit -m "feat: 添加 AI 搜索结果展示组件" -``` - ---- - -## Phase 5: 测试与验证 - -### Task 8: 端到端测试 - -**Step 1: 测试向量化流程** - -```sql --- 检查向量化进度 -SELECT - COUNT(*) FILTER (WHERE "embedding" IS NOT NULL) as vectorized, - COUNT(*) FILTER (WHERE "embedding" IS NULL) as not_vectorized, - COUNT(*) as total -FROM "Project"; -``` - -预期:`not_vectorized` 应该逐渐减少(每 5 分钟更新一批)。 - -**Step 2: 测试 AI 搜索** - -```bash -# 测试自然语言查询 -curl -X POST http://localhost:3000/api/search/ai \ - -H "Content-Type: application/json" \ - -d '{"search":"帮我找能生成视频的 AI 工具","limit":10}' -``` - -验证: -- 返回结果与"视频生成"相关 -- 相似度分数合理(>0.6) -- 响应时间 < 3 秒 - -**Step 3: 测试边界情况** - -```bash -# 空查询 -curl -X POST http://localhost:3000/api/search/ai \ - -H "Content-Type: application/json" \ - -d '{"search":"","limit":10}' - -# 预期:400 错误 - -# 无结果查询 -curl -X POST http://localhost:3000/api/search/ai \ - -H "Content-Type: application/json" \ - -d '{"search":"xyzabc123不存在的项目","limit":10}' - -# 预期:空 results 数组 -``` - -**Step 4: 测试降级逻辑** - -在前端测试: -1. 停止 n8n 工作流 -2. 执行 AI 搜索 -3. 验证错误提示和降级到传统搜索 - ---- - -### Task 9: 性能验证 - -**Step 1: 测试搜索响应时间** - -```bash -# 使用 time 命令测量 -time curl -X POST http://localhost:3000/api/search/ai \ - -H "Content-Type: application/json" \ - -d '{"search":"视频生成","limit":20}' -``` - -目标:< 3 秒 - -**Step 2: 测试并发搜索** - -```bash -# 使用 apachebench 或类似工具 -ab -n 100 -c 10 -p search.json -T application/json \ - http://localhost:3000/api/search/ai -``` - -目标:能处理 10 并发无错误 - -**Step 3: 检查数据库查询性能** - -在 n8n PostgreSQL 节点中查看执行时间: - -```sql -EXPLAIN ANALYZE -SELECT ... (向量搜索查询) -``` - -目标:< 500ms - ---- - -### Task 10: 文档更新 - -**Files:** -- Modify: `README.md`(可选) -- Modify: `CLAUDE.md` - -**Step 1: 更新 CLAUDE.md** - -在 `CLAUDE.md` 中添加 AI 搜索部分: - -```markdown -## AI Search System - -项目支持 AI 语义搜索功能,基于 pgvector 和 n8n 工作流实现。 - -### Architecture - -- **Vector Database**: Neon PostgreSQL + pgvector extension -- **AI Services**: n8n workflows (vectorization + search) -- **Frontend**: Toggle button + similarity scores - -### Environment Variables - -```bash -# n8n Webhook -N8N_AI_SEARCH_WEBHOOK=https://your-n8n.com/webhook/ai-search -``` - -### n8n Workflows - -1. **Project Vectorization**: Cron job (every 5 min) to generate embeddings -2. **AI Semantic Search**: Webhook for natural language queries - -### Usage - -用户在项目列表页面点击 AI 搜索按钮(✨ 图标),输入自然语言描述,系统返回语义相关的项目。 -``` - -**Step 2: 提交** - -```bash -git add CLAUDE.md -git commit -m "docs: 添加 AI 搜索系统文档" -``` - ---- - -## Phase 6: 部署 - -### Task 11: 生产环境部署 - -**Step 1: 验证 n8n 工作流** - -1. 确保 n8n 实例在生产环境运行 -2. 激活两个工作流 -3. 验证 Webhook URL 可访问 - -**Step 2: 配置 Vercel 环境变量** - -在 Vercel Dashboard 中添加: - -```bash -N8N_AI_SEARCH_WEBHOOK=https://your-production-n8n.com/webhook/ai-search -``` - -**Step 3: 部署 Next.js 应用** - -```bash -# 推送到 main 分支触发部署 -git push origin main - -# 或手动部署 -vercel --prod -``` - -**Step 4: 验证生产环境** - -```bash -# 测试生产环境 API -curl -X POST https://your-domain.com/api/search/ai \ - -H "Content-Type: application/json" \ - -d '{"search":"视频生成","limit":5}' -``` - -**Step 5: 监控 n8n 执行** - -在 n8n Dashboard 中: -- 查看工作流执行历史 -- 检查错误日志 -- 监控 API 使用量 - -**Step 6: 设置告警(可选)** - -在 n8n 中配置错误告警: -- OpenAI API 失败 -- 数据库连接错误 -- Webhook 超时 - ---- - -## 总结 - -### 完成检查清单 - -- [ ] Phase 1: 数据库迁移完成 -- [ ] Phase 2: n8n 工作流配置完成 -- [ ] Phase 3: Next.js API 路由实现 -- [ ] Phase 4: 前端组件实现 -- [ ] Phase 5: 测试与验证通过 -- [ ] Phase 6: 生产环境部署成功 - -### 关键指标 - -- 向量化完成率: > 95% -- 搜索响应时间: < 3 秒 -- 相似度准确率: 用户满意 > 80% -- API 成本: < $5/月(假设 1000 次搜索/天) - -### 后续优化方向 - -1. 添加查询缓存(Redis) -2. 实现混合搜索(向量 + 关键词) -3. A/B 测试用户体验 -4. 多模态搜索(图片、语音) -5. 个性化排序 - ---- - -**计划创建日期**: 2026-01-25 -**预计完成时间**: 4-6 小时 -**难度等级**: 中等 diff --git a/docs/plans/2026-01-25-ai-search-system-design.md b/docs/plans/2026-01-25-ai-search-system-design.md deleted file mode 100644 index cee2272..0000000 --- a/docs/plans/2026-01-25-ai-search-system-design.md +++ /dev/null @@ -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 索引加速相似度搜索 -- 向量维度:1536(OpenAI 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 次 -- 失败记录日志 - -### 工作流 2:AI 搜索 - -**触发器**: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 - -``` - -### 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) diff --git a/docs/verification-report.md b/docs/verification-report.md deleted file mode 100644 index 6288a6b..0000000 --- a/docs/verification-report.md +++ /dev/null @@ -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. 可以部署到生产环境 diff --git a/oh-my-opencode.json b/oh-my-opencode.json deleted file mode 100644 index d9593b8..0000000 --- a/oh-my-opencode.json +++ /dev/null @@ -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": [""] - } - } -} diff --git a/oh-my-opencode.json.backup b/oh-my-opencode.json.backup deleted file mode 100644 index c8acf9a..0000000 --- a/oh-my-opencode.json.backup +++ /dev/null @@ -1,8 +0,0 @@ -{ - "lsp": { - "eslint": { - "command": "eslint", - "extensions": [".ts", ".tsx", ".js", ".jsx"] - } - } -} diff --git a/package.json b/package.json index 9d655ba..cb34e5b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/prisma/AGENTS.md b/prisma/AGENTS.md deleted file mode 100644 index b710910..0000000 --- a/prisma/AGENTS.md +++ /dev/null @@ -1,128 +0,0 @@ -# prisma/ - Database Layer - - - -## 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 - - diff --git a/project-structure.txt b/project-structure.txt deleted file mode 100644 index 4e00336..0000000 --- a/project-structure.txt +++ /dev/null @@ -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 项目搜索和过滤 -- **特点**: 多条件搜索、分页、状态过滤 -``` diff --git a/scripts/backfill-signal-hotness.ts b/scripts/backfill-signal-hotness.ts deleted file mode 100644 index faa3fa1..0000000 --- a/scripts/backfill-signal-hotness.ts +++ /dev/null @@ -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() - }) diff --git a/scripts/backfill-tag-taxonomy.ts b/scripts/backfill-tag-taxonomy.ts deleted file mode 100644 index 7a29d75..0000000 --- a/scripts/backfill-tag-taxonomy.ts +++ /dev/null @@ -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) { - 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() - }) diff --git a/scripts/dedupe-free-tags-to-canonical.ts b/scripts/dedupe-free-tags-to-canonical.ts deleted file mode 100644 index 1ab29b4..0000000 --- a/scripts/dedupe-free-tags-to-canonical.ts +++ /dev/null @@ -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> = [] - - 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() - }) diff --git a/scripts/list-tags.js b/scripts/list-tags.js deleted file mode 100644 index f1f4fe2..0000000 --- a/scripts/list-tags.js +++ /dev/null @@ -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()); diff --git a/scripts/sync-domain-scenarios.ts b/scripts/sync-domain-scenarios.ts deleted file mode 100644 index cb135a6..0000000 --- a/scripts/sync-domain-scenarios.ts +++ /dev/null @@ -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> = { - '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() - 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() - - 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() - }) diff --git a/src/AGENTS.md b/src/AGENTS.md deleted file mode 100644 index 7a9f8ac..0000000 --- a/src/AGENTS.md +++ /dev/null @@ -1,47 +0,0 @@ -# src/ - Source Code - - - -## 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`) - - diff --git a/src/app/AGENTS.md b/src/app/AGENTS.md deleted file mode 100644 index 8f4334b..0000000 --- a/src/app/AGENTS.md +++ /dev/null @@ -1,64 +0,0 @@ -# src/app/ - App Router Implementation - - - -## 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 diff --git a/src/components/AGENTS.md b/src/components/AGENTS.md deleted file mode 100644 index a798af1..0000000 --- a/src/components/AGENTS.md +++ /dev/null @@ -1,62 +0,0 @@ -# src/components/ - Component System - - - -## 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 diff --git a/src/hooks/AGENTS.md b/src/hooks/AGENTS.md deleted file mode 100644 index 5fe66d7..0000000 --- a/src/hooks/AGENTS.md +++ /dev/null @@ -1,51 +0,0 @@ -# src/hooks/ - Server-Side Data Fetching - - - -**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 diff --git a/src/lib/AGENTS.md b/src/lib/AGENTS.md deleted file mode 100644 index 8662a22..0000000 --- a/src/lib/AGENTS.md +++ /dev/null @@ -1,68 +0,0 @@ -# src/lib/ - Core Utilities - - - -## 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