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