From 2c292b0a8fda7e59a86262be24d60e08a73d9e43 Mon Sep 17 00:00:00 2001 From: gaoziman <2942894660@qq.com> Date: Wed, 24 Dec 2025 09:40:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=AF=BC=E5=87=BA=E5=8A=9F=E8=83=BD):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AF=B9=E8=AF=9D=E5=AF=BC=E5=87=BA=20API=20?= =?UTF-8?q?=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 GET /api/conversations/[id]/export 接口 - 支持 markdown/json/html/pdf 四种导出格式 - 添加用户身份验证和权限检查 - PDF 格式返回数据供客户端生成 --- .../api/conversations/[id]/export/route.ts | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 src/app/api/conversations/[id]/export/route.ts diff --git a/src/app/api/conversations/[id]/export/route.ts b/src/app/api/conversations/[id]/export/route.ts new file mode 100644 index 0000000..699afe4 --- /dev/null +++ b/src/app/api/conversations/[id]/export/route.ts @@ -0,0 +1,167 @@ +/** + * 对话导出 API + * GET /api/conversations/[id]/export?format=markdown|json|html|pdf + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/drizzle/db'; +import { conversations, messages } from '@/drizzle/schema'; +import { eq, asc } from 'drizzle-orm'; +import { getCurrentUser } from '@/lib/auth'; +import { + exportConversation, + getExportContentType, + generateExportFilename, + DEFAULT_EXPORT_OPTIONS, + type ExportFormat, + type ExportData, + type ExportMessageData, +} from '@/lib/export'; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + // 验证用户身份 + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: '未登录' }, { status: 401 }); + } + + const { id: conversationId } = await params; + + // 获取导出参数 + const searchParams = request.nextUrl.searchParams; + const format = (searchParams.get('format') || 'markdown') as ExportFormat; + const includeThinking = searchParams.get('includeThinking') !== 'false'; + const includeToolCalls = searchParams.get('includeToolCalls') !== 'false'; + const includeImages = searchParams.get('includeImages') !== 'false'; + + // 验证格式 + if (!['markdown', 'json', 'html', 'pdf'].includes(format)) { + return NextResponse.json( + { error: '不支持的导出格式' }, + { status: 400 } + ); + } + + // PDF 导出需要在客户端执行,服务端只返回数据 + if (format === 'pdf') { + // 对于 PDF,返回 JSON 数据让客户端生成 + const exportData = await getExportData(conversationId, user.userId); + if (!exportData) { + return NextResponse.json( + { error: '对话不存在或无权访问' }, + { status: 404 } + ); + } + return NextResponse.json({ + ...exportData, + exportFormat: 'pdf', + message: '请在客户端生成 PDF', + }); + } + + // 获取对话和消息数据 + const exportData = await getExportData(conversationId, user.userId); + if (!exportData) { + return NextResponse.json( + { error: '对话不存在或无权访问' }, + { status: 404 } + ); + } + + // 执行导出 + const content = await exportConversation(exportData, { + format, + includeThinking, + includeToolCalls, + includeImages, + }); + + // 生成文件名 + const filename = generateExportFilename(exportData.conversation.title, format); + const contentType = getExportContentType(format); + + // 返回文件 + return new NextResponse(content as string, { + headers: { + 'Content-Type': contentType, + 'Content-Disposition': `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`, + }, + }); + } catch (error) { + console.error('Export error:', error); + return NextResponse.json( + { error: '导出失败' }, + { status: 500 } + ); + } +} + +/** + * 获取导出数据 + */ +async function getExportData( + conversationId: string, + userId: string +): Promise { + // 获取对话 + const conversation = await db.query.conversations.findFirst({ + where: eq(conversations.conversationId, conversationId), + }); + + if (!conversation) { + return null; + } + + // 验证权限 + if (conversation.userId !== userId) { + return null; + } + + // 获取消息 + const messageList = await db.query.messages.findMany({ + where: eq(messages.conversationId, conversationId), + orderBy: [asc(messages.createdAt)], + }); + + // 构建导出数据 + const exportData: ExportData = { + exportInfo: { + exportedAt: new Date().toISOString(), + format: 'json', // 将在实际导出时更新 + version: '1.0', + }, + conversation: { + id: conversation.conversationId, + title: conversation.title, + model: conversation.model, + enableThinking: conversation.enableThinking || false, + tools: (conversation.tools as string[]) || [], + messageCount: conversation.messageCount || 0, + totalTokens: conversation.totalTokens || 0, + createdAt: conversation.createdAt?.toISOString() || new Date().toISOString(), + updatedAt: conversation.updatedAt?.toISOString() || new Date().toISOString(), + }, + messages: messageList.map((msg): ExportMessageData => ({ + id: msg.messageId, + role: msg.role as 'user' | 'assistant' | 'system', + content: msg.content, + thinkingContent: msg.thinkingContent, + toolCalls: msg.toolCalls as ExportMessageData['toolCalls'], + toolResults: msg.toolResults as ExportMessageData['toolResults'], + images: msg.images as string[] | null, + uploadedImages: msg.uploadedImages as string[] | null, + uploadedDocuments: msg.uploadedDocuments as ExportMessageData['uploadedDocuments'], + usedTools: msg.usedTools as string[] | null, + searchImages: msg.searchImages as ExportMessageData['searchImages'], + inputTokens: msg.inputTokens, + outputTokens: msg.outputTokens, + createdAt: msg.createdAt?.toISOString() || new Date().toISOString(), + })), + }; + + return exportData; +}