实现了AI助手

This commit is contained in:
2025-06-24 14:09:12 +08:00
parent 7d139b22db
commit 8a4ccd5ef2
31 changed files with 2883 additions and 252 deletions
+167
View File
@@ -0,0 +1,167 @@
import { NextRequest, NextResponse } from 'next/server'
import { AIClient } from '@/lib/ai-assistant-openai'
import { getUser } from '@/lib/auth'
import { ComponentService } from '@/lib/services/component-service'
import { Component } from '@prisma/client'
type Message = {
role: 'user' | 'assistant'
content: string
} | {
role: 'card'
components: Component[]
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
// 验证用户身份
const user = await getUser(request)
if (!user) {
return NextResponse.json(
{ message: '请先登录' },
{ status: 401 }
)
}
const client = new AIClient()
const conversation = await client.getConversation((await params).id, user.id)
if (!conversation) {
return NextResponse.json(
{ message: '对话不存在或无权限访问' },
{ status: 404 }
)
}
// 转换为Markdown
const filteredMessages = conversation.messages.filter((m, index) => !(index == 0 || m.role == 'user' && typeof m.content != 'string'))
const res: Message[] = []
let appendMessage: Message | null = null
for (const message of filteredMessages) {
let content: string
if (typeof message.content === "string") {
content = message.content
} else {
const contentParts: string[] = []
for (const part of message.content) {
if (part.type === 'text') {
contentParts.push(part.text)
} else if (part.type === 'tool_use') {
contentParts.push(`**工具调用**: ${part.name}\n\`\`\`\n${JSON.stringify(part.input, null, 2)}\n\`\`\``)
if (part.name === "show-components") {
const ids = (part.input as { component_ids: string[] }).component_ids
const components = await ComponentService.getComponentsByIds(ids)
appendMessage = {
role: 'card',
components: components.filter(c => !!c)
}
}
}
}
content = contentParts.join('\n\n')
}
res.push({
...message,
content
})
if (appendMessage) {
res.push(appendMessage)
appendMessage = null
}
}
return NextResponse.json(res)
} catch (error) {
console.error('获取对话详情错误:', error)
return NextResponse.json(
{ message: '获取对话详情失败' },
{ status: 500 }
)
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
// 验证用户身份
const user = await getUser(request)
if (!user) {
return NextResponse.json(
{ message: '请先登录' },
{ status: 401 }
)
}
const client = new AIClient()
const success = await client.deleteConversation(params.id, user.id)
if (!success) {
return NextResponse.json(
{ message: '对话不存在或无权限删除' },
{ status: 404 }
)
}
return NextResponse.json({ message: '对话删除成功' })
} catch (error) {
console.error('删除对话错误:', error)
return NextResponse.json(
{ message: '删除对话失败' },
{ status: 500 }
)
}
}
export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
// 验证用户身份
const user = await getUser(request)
if (!user) {
return NextResponse.json(
{ message: '请先登录' },
{ status: 401 }
)
}
const { title } = await request.json()
if (!title || typeof title !== 'string') {
return NextResponse.json(
{ message: '请提供有效的标题' },
{ status: 400 }
)
}
const client = new AIClient()
const success = await client.updateConversationTitle(params.id, user.id, title)
if (!success) {
return NextResponse.json(
{ message: '对话不存在或无权限修改' },
{ status: 404 }
)
}
return NextResponse.json({ message: '标题更新成功' })
} catch (error) {
console.error('更新对话标题错误:', error)
return NextResponse.json(
{ message: '更新对话标题失败' },
{ status: 500 }
)
}
}
+32
View File
@@ -0,0 +1,32 @@
import { NextRequest, NextResponse } from 'next/server'
import { AIClient } from '@/lib/ai-assistant-openai'
import { getUser } from '@/lib/auth'
export async function GET(request: NextRequest) {
try {
// 验证用户身份
const user = await getUser(request)
if (!user) {
return NextResponse.json(
{ message: '请先登录' },
{ status: 401 }
)
}
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '20')
const client = new AIClient()
const conversations = await client.getUserConversations(user.id, page, limit)
return NextResponse.json(conversations)
} catch (error) {
console.error('获取对话历史错误:', error)
return NextResponse.json(
{ message: '获取对话历史失败' },
{ status: 500 }
)
}
}
+80
View File
@@ -0,0 +1,80 @@
import { NextRequest, NextResponse } from 'next/server'
import { AIClient } from '@/lib/ai-assistant'
import { getUser } from '@/lib/auth'
export async function POST(request: NextRequest) {
try {
// 验证用户身份
const user = await getUser(request)
if (!user) {
return NextResponse.json(
{ message: '请先登录' },
{ status: 401 }
)
}
const body = await request.json()
const { prompt, conversationId } = body
if (!prompt || !prompt.trim()) {
return NextResponse.json(
{ message: '请输入您的问题' },
{ status: 400 }
)
}
let client = new AIClient() // 创建流式响应
const encoder = new TextEncoder()
let newConversationId: string | null = null
const stream = new ReadableStream({
async start(controller) {
try {
for await (const segment of client.processQuery(prompt, user.id, conversationId)) {
// 检查是否是新对话ID
if (segment.startsWith('conversation_id:')) {
newConversationId = segment.substring('conversation_id:'.length)
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ conversationId: newConversationId })}\n\n`))
continue
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ content: segment })}\n\n`))
}
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
controller.close()
} catch (error) {
console.error('AI 处理错误:', error)
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
error: error instanceof Error ? error.message : '处理请求时发生错误'
})}\n\n`))
controller.close()
}
}
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
})
} catch (error) {
console.error('AI 查询错误:', error)
return NextResponse.json(
{ message: 'AI 服务暂时不可用,请稍后再试' },
{ status: 500 }
)
}
}
export async function GET() {
return NextResponse.json({
message: '欢迎使用 PC DIY 商店 AI 助手!',
examples: [
'推荐一些价格在 1000-2000 元的显卡',
'查找华硕品牌的主板',
'什么是最新的 CPU 配件',
'显示所有配件类型'
]
})
}
+3 -8
View File
@@ -1,19 +1,14 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { ComponentService } from '@/lib/services/component-service'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
) { try {
const { id } = await params
const component = await prisma.component.findUnique({
where: { id: id },
include: {
componentType: true
}
})
const component = await ComponentService.getComponentById(id)
if (!component) {
return NextResponse.json(
+13 -54
View File
@@ -1,10 +1,11 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { ComponentService } from '@/lib/services/component-service'
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const typeId = searchParams.get('type')
const typeId = searchParams.get('type') // 注意:这里仍然叫type,但实际是typeId
const brand = searchParams.get('brand')
const minPrice = searchParams.get('minPrice')
const maxPrice = searchParams.get('maxPrice')
@@ -12,61 +13,19 @@ export async function GET(request: NextRequest) {
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '12')
const where: any = {}
if (typeId) {
where.componentTypeId = typeId
}
if (brand) {
where.brand = {
contains: brand,
mode: 'insensitive'
}
}
if (minPrice || maxPrice) {
where.price = {}
if (minPrice) where.price.gte = parseFloat(minPrice)
if (maxPrice) where.price.lte = parseFloat(maxPrice)
}
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ brand: { contains: search, mode: 'insensitive' } },
{ model: { contains: search, mode: 'insensitive' } },
{ description: { contains: search, mode: 'insensitive' } }
]
}
const [components, total] = await Promise.all([
prisma.component.findMany({
where,
include: {
componentType: true
},
orderBy: {
createdAt: 'desc'
},
skip: (page - 1) * limit,
take: limit
}),
prisma.component.count({ where })
])
const totalPages = Math.ceil(total / limit)
const result = await ComponentService.queryComponents({
typeId: typeId || undefined, // 将null转换为undefined
brand: brand || undefined,
minPrice: minPrice ? parseFloat(minPrice) : undefined,
maxPrice: maxPrice ? parseFloat(maxPrice) : undefined,
search: search || undefined,
page,
limit
})
return NextResponse.json({
components,
pagination: {
page,
limit,
total,
totalPages,
hasNext: page < totalPages,
hasPrev: page > 1
}
components: result.components,
pagination: result.pagination
})
} catch (error) {
console.error('Components fetch error:', error)