Files
agent-park/src/app/api/webhook/projects/route.ts
T
mzaxdandClaude ba1fab65c6 feat: 实现 Agent Park AI 项目导航网站核心功能
完成 Next.js 14+ 全栈应用的 MVP 实现,包含以下功能:

## 项目设置
- 初始化 Next.js 14+ 项目,配置 TypeScript 严格模式
- 配置 Tailwind CSS、ESLint、Prettier 代码质量工具
- 集成 shadcn/ui 组件库和 next-intl 国际化方案

## 数据层
- 配置 PostgreSQL + Prisma ORM
- 定义 Project、Tag、ExternalLink 数据模型
- 实现种子数据脚本(5个AI项目)

## 用户故事 1:浏览和搜索 AI 项目
- 实现首页,展示热门标签云和精选项目
- 实现项目列表页,支持搜索和标签筛选
- 创建 TagCloud、ProjectCard、ProjectList、SearchBar 组件
- 实现 ISR 缓存策略优化性能

## 用户故事 2:查看项目详细信息
- 实现项目详情页,显示完整信息
- 创建 ExternalLinkCard 组件展示外部链接
- 实现安全的 target="_blank" 外链跳转

## 用户故事 4:数据更新和管理
- 实现 webhook API 端点接收 n8n 数据推送
- 实现 API Key 身份验证
- 支持部分成功模式的批量数据处理
- 完善的错误处理和日志记录

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-25 15:12:09 +08:00

204 lines
5.8 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import {
WebhookPayloadSchema,
ProjectInputSchema,
type WebhookPayload,
type ProjectInput,
} from '@/lib/validations'
export async function POST(request: NextRequest) {
const startTime = Date.now()
try {
const body = await request.json()
// Validate payload
const validationResult = WebhookPayloadSchema.safeParse(body)
if (!validationResult.success) {
return NextResponse.json(
{
success: false,
error: 'Validation error',
details: validationResult.error.errors.map((e) => e.message),
},
{ status: 400 }
)
}
const payload = validationResult.data as WebhookPayload
// Verify API Key
const apiKey = process.env.WEBHOOK_API_KEY
if (payload.apiKey !== apiKey) {
return NextResponse.json(
{
success: false,
error: 'Unauthorized',
details: ['Invalid or missing API Key'],
},
{ status: 401 }
)
}
// Process projects with partial success mode
const results = {
processed: payload.projects.length,
created: 0,
updated: 0,
failed: 0,
errors: [] as Array<{
index: number
field: string
message: string
value: any
}>,
}
for (let i = 0; i < payload.projects.length; i++) {
const projectData = payload.projects[i]
// Validate individual project
const projectValidation = ProjectInputSchema.safeParse(projectData)
if (!projectValidation.success) {
results.failed++
results.errors.push({
index: i,
field: projectValidation.error.errors[0].path.join('.'),
message: projectValidation.error.errors[0].message,
value: projectData,
})
continue
}
try {
const validProject = projectValidation.data as ProjectInput
// Upsert tags
const tagConnections = await Promise.all(
validProject.tags.map(async (tag) => {
const slug =
tag.nameEn?.toLowerCase().replace(/\s+/g, '-') ||
tag.name.toLowerCase().replace(/\s+/g, '-')
return prisma.tag.upsert({
where: { slug },
update: {},
create: {
name: tag.name,
nameEn: tag.nameEn || null,
slug,
},
})
})
)
// Generate slug for project
const slug =
validProject.nameEn?.toLowerCase().replace(/\s+/g, '-') ||
validProject.name.toLowerCase().replace(/\s+/g, '-')
// Upsert project
const existingProject = await prisma.project.findUnique({
where: { slug },
})
if (existingProject) {
// Update existing project
await prisma.project.update({
where: { id: existingProject.id },
data: {
name: validProject.name,
nameEn: validProject.nameEn || null,
description: validProject.description,
descriptionEn: validProject.descriptionEn || null,
content: validProject.content || null,
contentEn: validProject.contentEn || null,
status: validProject.status as any,
source: validProject.source || null,
tags: {
set: tagConnections.map((t) => ({ id: t.id })),
},
},
})
// Update links (delete old ones, create new ones)
await prisma.externalLink.deleteMany({
where: { projectId: existingProject.id },
})
await prisma.externalLink.createMany({
data: validProject.links.map((link) => ({
type: link.type as any,
url: link.url,
title: link.title || null,
projectId: existingProject.id,
})),
})
results.updated++
} else {
// Create new project
await prisma.project.create({
data: {
name: validProject.name,
nameEn: validProject.nameEn || null,
slug,
description: validProject.description,
descriptionEn: validProject.descriptionEn || null,
content: validProject.content || null,
contentEn: validProject.contentEn || null,
status: validProject.status as any,
source: validProject.source || null,
tags: {
connect: tagConnections.map((t) => ({ id: t.id })),
},
links: {
create: validProject.links.map((link) => ({
type: link.type as any,
url: link.url,
title: link.title || null,
})),
},
},
})
results.created++
}
} catch (error) {
results.failed++
results.errors.push({
index: i,
field: 'general',
message: error instanceof Error ? error.message : 'Unknown error',
value: projectData,
})
}
}
const duration = Date.now() - startTime
// Log request
console.log(
`[Webhook] Processed ${results.processed} projects in ${duration}ms: ${results.created} created, ${results.updated} updated, ${results.failed} failed`
)
return NextResponse.json({
success: true,
...results,
})
} catch (error) {
console.error('[Webhook] Error:', error)
return NextResponse.json(
{
success: false,
error: 'Internal server error',
details: [error instanceof Error ? error.message : 'Unknown error'],
},
{ status: 500 }
)
}
}