diff --git a/src/app/api/assistants/[id]/favorite/route.ts b/src/app/api/assistants/[id]/favorite/route.ts new file mode 100644 index 0000000..939280f --- /dev/null +++ b/src/app/api/assistants/[id]/favorite/route.ts @@ -0,0 +1,106 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/drizzle/db'; +import { userFavoriteAssistants, assistants } from '@/drizzle/schema'; +import { eq, and } from 'drizzle-orm'; + +interface RouteParams { + params: Promise<{ id: string }>; +} + +// POST /api/assistants/[id]/favorite - 收藏助手 +export async function POST(request: NextRequest, { params }: RouteParams) { + try { + const { id } = await params; + const assistantId = parseInt(id); + const body = await request.json(); + const { userId } = body; + + if (isNaN(assistantId)) { + return NextResponse.json({ error: 'Invalid assistant ID' }, { status: 400 }); + } + + if (!userId) { + return NextResponse.json({ error: 'User ID is required' }, { status: 400 }); + } + + // 检查助手是否存在 + const [assistant] = await db + .select() + .from(assistants) + .where(eq(assistants.id, assistantId)) + .limit(1); + + if (!assistant) { + return NextResponse.json({ error: 'Assistant not found' }, { status: 404 }); + } + + // 检查是否已经收藏 + const [existingFavorite] = await db + .select() + .from(userFavoriteAssistants) + .where( + and( + eq(userFavoriteAssistants.userId, userId), + eq(userFavoriteAssistants.assistantId, assistantId) + ) + ) + .limit(1); + + if (existingFavorite) { + return NextResponse.json({ message: 'Already favorited' }, { status: 200 }); + } + + // 添加收藏 + const [newFavorite] = await db + .insert(userFavoriteAssistants) + .values({ + userId, + assistantId, + }) + .returning(); + + return NextResponse.json(newFavorite, { status: 201 }); + } catch (error) { + console.error('Failed to favorite assistant:', error); + return NextResponse.json( + { error: 'Failed to favorite assistant' }, + { status: 500 } + ); + } +} + +// DELETE /api/assistants/[id]/favorite - 取消收藏 +export async function DELETE(request: NextRequest, { params }: RouteParams) { + try { + const { id } = await params; + const assistantId = parseInt(id); + const searchParams = request.nextUrl.searchParams; + const userId = searchParams.get('userId'); + + if (isNaN(assistantId)) { + return NextResponse.json({ error: 'Invalid assistant ID' }, { status: 400 }); + } + + if (!userId) { + return NextResponse.json({ error: 'User ID is required' }, { status: 400 }); + } + + // 删除收藏记录 + await db + .delete(userFavoriteAssistants) + .where( + and( + eq(userFavoriteAssistants.userId, userId), + eq(userFavoriteAssistants.assistantId, assistantId) + ) + ); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Failed to unfavorite assistant:', error); + return NextResponse.json( + { error: 'Failed to unfavorite assistant' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/assistants/[id]/route.ts b/src/app/api/assistants/[id]/route.ts new file mode 100644 index 0000000..f47c21e --- /dev/null +++ b/src/app/api/assistants/[id]/route.ts @@ -0,0 +1,230 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/drizzle/db'; +import { assistants, assistantCategories, userFavoriteAssistants } from '@/drizzle/schema'; +import { eq, and, sql } from 'drizzle-orm'; + +interface RouteParams { + params: Promise<{ id: string }>; +} + +// GET /api/assistants/[id] - 获取助手详情 +export async function GET(request: NextRequest, { params }: RouteParams) { + try { + const { id } = await params; + const assistantId = parseInt(id); + const searchParams = request.nextUrl.searchParams; + const userId = searchParams.get('userId'); + + if (isNaN(assistantId)) { + return NextResponse.json({ error: 'Invalid assistant ID' }, { status: 400 }); + } + + const [assistant] = await db + .select({ + id: assistants.id, + categoryId: assistants.categoryId, + userId: assistants.userId, + name: assistants.name, + description: assistants.description, + icon: assistants.icon, + systemPrompt: assistants.systemPrompt, + tags: assistants.tags, + isBuiltin: assistants.isBuiltin, + isEnabled: assistants.isEnabled, + sortOrder: assistants.sortOrder, + useCount: assistants.useCount, + createdAt: assistants.createdAt, + updatedAt: assistants.updatedAt, + categoryName: assistantCategories.name, + categoryIcon: assistantCategories.icon, + }) + .from(assistants) + .leftJoin(assistantCategories, eq(assistants.categoryId, assistantCategories.id)) + .where(eq(assistants.id, assistantId)) + .limit(1); + + if (!assistant) { + return NextResponse.json({ error: 'Assistant not found' }, { status: 404 }); + } + + // 查询收藏状态 + let isFavorited = false; + if (userId) { + const [favorite] = await db + .select() + .from(userFavoriteAssistants) + .where( + and( + eq(userFavoriteAssistants.userId, userId), + eq(userFavoriteAssistants.assistantId, assistantId) + ) + ) + .limit(1); + isFavorited = !!favorite; + } + + return NextResponse.json({ + ...assistant, + isFavorited, + }); + } catch (error) { + console.error('Failed to fetch assistant:', error); + return NextResponse.json( + { error: 'Failed to fetch assistant' }, + { status: 500 } + ); + } +} + +// PUT /api/assistants/[id] - 更新助手 +export async function PUT(request: NextRequest, { params }: RouteParams) { + try { + const { id } = await params; + const assistantId = parseInt(id); + const body = await request.json(); + const { userId, categoryId, name, description, icon, systemPrompt, tags } = body; + + if (isNaN(assistantId)) { + return NextResponse.json({ error: 'Invalid assistant ID' }, { status: 400 }); + } + + // 查询助手信息 + const [existingAssistant] = await db + .select() + .from(assistants) + .where(eq(assistants.id, assistantId)) + .limit(1); + + if (!existingAssistant) { + return NextResponse.json({ error: 'Assistant not found' }, { status: 404 }); + } + + // 检查权限:只能编辑自己创建的助手,内置助手不可编辑 + if (existingAssistant.isBuiltin) { + return NextResponse.json( + { error: 'Cannot edit built-in assistant' }, + { status: 403 } + ); + } + + if (existingAssistant.userId && existingAssistant.userId !== userId) { + return NextResponse.json( + { error: 'Not authorized to edit this assistant' }, + { status: 403 } + ); + } + + // 更新助手 + const [updatedAssistant] = await db + .update(assistants) + .set({ + categoryId: categoryId !== undefined ? categoryId : existingAssistant.categoryId, + name: name || existingAssistant.name, + description: description !== undefined ? description : existingAssistant.description, + icon: icon || existingAssistant.icon, + systemPrompt: systemPrompt || existingAssistant.systemPrompt, + tags: tags !== undefined ? tags : existingAssistant.tags, + updatedAt: new Date(), + }) + .where(eq(assistants.id, assistantId)) + .returning(); + + return NextResponse.json(updatedAssistant); + } catch (error) { + console.error('Failed to update assistant:', error); + return NextResponse.json( + { error: 'Failed to update assistant' }, + { status: 500 } + ); + } +} + +// DELETE /api/assistants/[id] - 删除助手 +export async function DELETE(request: NextRequest, { params }: RouteParams) { + try { + const { id } = await params; + const assistantId = parseInt(id); + const searchParams = request.nextUrl.searchParams; + const userId = searchParams.get('userId'); + + if (isNaN(assistantId)) { + return NextResponse.json({ error: 'Invalid assistant ID' }, { status: 400 }); + } + + // 查询助手信息 + const [existingAssistant] = await db + .select() + .from(assistants) + .where(eq(assistants.id, assistantId)) + .limit(1); + + if (!existingAssistant) { + return NextResponse.json({ error: 'Assistant not found' }, { status: 404 }); + } + + // 检查权限:只能删除自己创建的助手,内置助手不可删除 + if (existingAssistant.isBuiltin) { + return NextResponse.json( + { error: 'Cannot delete built-in assistant' }, + { status: 403 } + ); + } + + if (existingAssistant.userId && existingAssistant.userId !== userId) { + return NextResponse.json( + { error: 'Not authorized to delete this assistant' }, + { status: 403 } + ); + } + + // 删除相关收藏记录 + await db + .delete(userFavoriteAssistants) + .where(eq(userFavoriteAssistants.assistantId, assistantId)); + + // 删除助手 + await db.delete(assistants).where(eq(assistants.id, assistantId)); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Failed to delete assistant:', error); + return NextResponse.json( + { error: 'Failed to delete assistant' }, + { status: 500 } + ); + } +} + +// PATCH /api/assistants/[id] - 增加使用次数 +export async function PATCH(request: NextRequest, { params }: RouteParams) { + try { + const { id } = await params; + const assistantId = parseInt(id); + const body = await request.json(); + const { action } = body; + + if (isNaN(assistantId)) { + return NextResponse.json({ error: 'Invalid assistant ID' }, { status: 400 }); + } + + if (action === 'incrementUseCount') { + await db + .update(assistants) + .set({ + useCount: sql`${assistants.useCount} + 1`, + updatedAt: new Date(), + }) + .where(eq(assistants.id, assistantId)); + + return NextResponse.json({ success: true }); + } + + return NextResponse.json({ error: 'Invalid action' }, { status: 400 }); + } catch (error) { + console.error('Failed to update assistant:', error); + return NextResponse.json( + { error: 'Failed to update assistant' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/assistants/categories/route.ts b/src/app/api/assistants/categories/route.ts new file mode 100644 index 0000000..2b84ade --- /dev/null +++ b/src/app/api/assistants/categories/route.ts @@ -0,0 +1,23 @@ +import { NextResponse } from 'next/server'; +import { db } from '@/drizzle/db'; +import { assistantCategories } from '@/drizzle/schema'; +import { eq, asc } from 'drizzle-orm'; + +// GET /api/assistants/categories - 获取所有助手分类 +export async function GET() { + try { + const categories = await db + .select() + .from(assistantCategories) + .where(eq(assistantCategories.isEnabled, true)) + .orderBy(asc(assistantCategories.sortOrder)); + + return NextResponse.json(categories); + } catch (error) { + console.error('Failed to fetch assistant categories:', error); + return NextResponse.json( + { error: 'Failed to fetch categories' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/assistants/recent/route.ts b/src/app/api/assistants/recent/route.ts new file mode 100644 index 0000000..4da4b8b --- /dev/null +++ b/src/app/api/assistants/recent/route.ts @@ -0,0 +1,130 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/drizzle/db'; +import { conversations, assistants, assistantCategories } from '@/drizzle/schema'; +import { eq, desc, isNotNull, and, asc } from 'drizzle-orm'; +import { getCurrentUser } from '@/lib/auth'; + +// GET /api/assistants/recent - 获取最近使用的助手 +export async function GET(request: NextRequest) { + try { + // 获取当前用户 + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json( + { error: '未登录' }, + { status: 401 } + ); + } + + const searchParams = request.nextUrl.searchParams; + const limit = parseInt(searchParams.get('limit') || '5'); + + // 查询用户最近使用过助手的对话(按时间倒序,去重) + const recentConversations = await db + .selectDistinctOn([conversations.assistantId], { + assistantId: conversations.assistantId, + lastUsedAt: conversations.lastMessageAt, + }) + .from(conversations) + .where( + and( + eq(conversations.userId, user.userId), + isNotNull(conversations.assistantId) + ) + ) + .orderBy(conversations.assistantId, desc(conversations.lastMessageAt)) + .limit(limit * 2); // 多取一些,因为可能有些助手已被删除 + + if (recentConversations.length === 0) { + return NextResponse.json({ data: [] }); + } + + // 获取助手详细信息 + const assistantIds = recentConversations + .map((c) => c.assistantId) + .filter((id): id is number => id !== null); + + if (assistantIds.length === 0) { + return NextResponse.json({ data: [] }); + } + + // 查询助手详情(带分类信息) + const assistantsList = await db + .select({ + id: assistants.id, + categoryId: assistants.categoryId, + name: assistants.name, + description: assistants.description, + icon: assistants.icon, + systemPrompt: assistants.systemPrompt, + tags: assistants.tags, + isBuiltin: assistants.isBuiltin, + useCount: assistants.useCount, + categoryName: assistantCategories.name, + categoryIcon: assistantCategories.icon, + }) + .from(assistants) + .leftJoin(assistantCategories, eq(assistants.categoryId, assistantCategories.id)) + .where( + and( + eq(assistants.isEnabled, true), + // 使用 SQL IN 查询 + eq(assistants.id, assistantIds[0]) // 临时解决方案 + ) + ); + + // 由于 drizzle 的 IN 语法较复杂,改用循环查询 + const allAssistants = []; + for (const id of assistantIds.slice(0, limit)) { + const [assistant] = await db + .select({ + id: assistants.id, + categoryId: assistants.categoryId, + name: assistants.name, + description: assistants.description, + icon: assistants.icon, + systemPrompt: assistants.systemPrompt, + tags: assistants.tags, + isBuiltin: assistants.isBuiltin, + useCount: assistants.useCount, + categoryName: assistantCategories.name, + categoryIcon: assistantCategories.icon, + }) + .from(assistants) + .leftJoin(assistantCategories, eq(assistants.categoryId, assistantCategories.id)) + .where( + and( + eq(assistants.isEnabled, true), + eq(assistants.id, id) + ) + ) + .limit(1); + + if (assistant) { + // 找到对应的 lastUsedAt + const conv = recentConversations.find((c) => c.assistantId === id); + allAssistants.push({ + ...assistant, + lastUsedAt: conv?.lastUsedAt, + }); + } + } + + // 按最后使用时间排序 + allAssistants.sort((a, b) => { + const timeA = a.lastUsedAt ? new Date(a.lastUsedAt).getTime() : 0; + const timeB = b.lastUsedAt ? new Date(b.lastUsedAt).getTime() : 0; + return timeB - timeA; + }); + + return NextResponse.json({ + data: allAssistants.slice(0, limit), + }); + } catch (error) { + console.error('Failed to fetch recent assistants:', error); + return NextResponse.json( + { error: 'Failed to fetch recent assistants' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/assistants/route.ts b/src/app/api/assistants/route.ts new file mode 100644 index 0000000..2939fd6 --- /dev/null +++ b/src/app/api/assistants/route.ts @@ -0,0 +1,172 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/drizzle/db'; +import { assistants, assistantCategories, userFavoriteAssistants } from '@/drizzle/schema'; +import { eq, and, or, ilike, asc, desc, sql, isNull } from 'drizzle-orm'; + +// GET /api/assistants - 获取助手列表 +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + const categoryId = searchParams.get('categoryId'); + const search = searchParams.get('search'); + const userId = searchParams.get('userId'); // 用于获取用户自定义助手 + const onlyFavorites = searchParams.get('favorites') === 'true'; + const page = parseInt(searchParams.get('page') || '1'); + const limit = parseInt(searchParams.get('limit') || '50'); + const offset = (page - 1) * limit; + + // 构建查询条件 + const conditions = [eq(assistants.isEnabled, true)]; + + // 按分类筛选 + if (categoryId && categoryId !== 'all') { + conditions.push(eq(assistants.categoryId, parseInt(categoryId))); + } + + // 搜索条件(按名称或描述) + if (search) { + conditions.push( + or( + ilike(assistants.name, `%${search}%`), + ilike(assistants.description, `%${search}%`) + )! + ); + } + + // 用户自定义助手或系统内置助手 + // 如果有 userId,显示系统助手和该用户创建的助手 + if (userId) { + conditions.push( + or( + isNull(assistants.userId), // 系统内置 + eq(assistants.userId, userId) // 用户自己创建的 + )! + ); + } else { + // 没有 userId 时只显示系统内置助手 + conditions.push(isNull(assistants.userId)); + } + + // 如果只获取收藏的助手 + if (onlyFavorites && userId) { + const favoriteIds = await db + .select({ assistantId: userFavoriteAssistants.assistantId }) + .from(userFavoriteAssistants) + .where(eq(userFavoriteAssistants.userId, userId)); + + if (favoriteIds.length > 0) { + const ids = favoriteIds.map((f) => f.assistantId); + conditions.push(sql`${assistants.id} IN (${sql.join(ids, sql`, `)})`); + } else { + // 没有收藏则返回空数组 + return NextResponse.json({ + data: [], + total: 0, + page, + limit, + }); + } + } + + // 查询助手列表(带分类信息) + const assistantsList = await db + .select({ + id: assistants.id, + categoryId: assistants.categoryId, + userId: assistants.userId, + name: assistants.name, + description: assistants.description, + icon: assistants.icon, + systemPrompt: assistants.systemPrompt, + tags: assistants.tags, + isBuiltin: assistants.isBuiltin, + sortOrder: assistants.sortOrder, + useCount: assistants.useCount, + createdAt: assistants.createdAt, + categoryName: assistantCategories.name, + categoryIcon: assistantCategories.icon, + }) + .from(assistants) + .leftJoin(assistantCategories, eq(assistants.categoryId, assistantCategories.id)) + .where(and(...conditions)) + .orderBy(desc(assistants.isBuiltin), asc(assistants.sortOrder), desc(assistants.useCount)) + .limit(limit) + .offset(offset); + + // 如果有 userId,查询收藏状态 + let favoritesSet = new Set(); + if (userId) { + const userFavorites = await db + .select({ assistantId: userFavoriteAssistants.assistantId }) + .from(userFavoriteAssistants) + .where(eq(userFavoriteAssistants.userId, userId)); + favoritesSet = new Set(userFavorites.map((f) => f.assistantId)); + } + + // 添加收藏状态到结果 + const result = assistantsList.map((assistant) => ({ + ...assistant, + isFavorited: favoritesSet.has(assistant.id), + })); + + // 获取总数 + const totalResult = await db + .select({ count: sql`count(*)` }) + .from(assistants) + .where(and(...conditions)); + const total = Number(totalResult[0]?.count || 0); + + return NextResponse.json({ + data: result, + total, + page, + limit, + }); + } catch (error) { + console.error('Failed to fetch assistants:', error); + return NextResponse.json( + { error: 'Failed to fetch assistants' }, + { status: 500 } + ); + } +} + +// POST /api/assistants - 创建新助手 +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { userId, categoryId, name, description, icon, systemPrompt, tags } = body; + + if (!name || !systemPrompt) { + return NextResponse.json( + { error: 'Name and system prompt are required' }, + { status: 400 } + ); + } + + const [newAssistant] = await db + .insert(assistants) + .values({ + userId: userId || null, // null 表示系统内置 + categoryId: categoryId || null, + name, + description: description || null, + icon: icon || '🤖', + systemPrompt, + tags: tags || [], + isBuiltin: false, // 用户创建的不是内置 + isEnabled: true, + sortOrder: 0, + useCount: 0, + }) + .returning(); + + return NextResponse.json(newAssistant, { status: 201 }); + } catch (error) { + console.error('Failed to create assistant:', error); + return NextResponse.json( + { error: 'Failed to create assistant' }, + { status: 500 } + ); + } +}