feat(API): 添加助手管理接口

- 新增助手 CRUD 接口 (GET/POST/PUT/DELETE)
- 新增助手分类查询接口
- 新增助手收藏/取消收藏接口
- 新增最近使用助手查询接口
- 支持按分类、搜索关键词筛选助手
This commit is contained in:
gaoziman 2025-12-20 20:45:56 +08:00
parent ee112a5ea3
commit 34aa3e50cf
5 changed files with 661 additions and 0 deletions

View File

@ -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 }
);
}
}

View File

@ -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 }
);
}
}

View File

@ -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 }
);
}
}

View File

@ -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 }
);
}
}

View File

@ -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<number>();
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<number>`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 }
);
}
}