feat(导出功能): 添加对话导出 API 路由

- 实现 GET /api/conversations/[id]/export 接口
- 支持 markdown/json/html/pdf 四种导出格式
- 添加用户身份验证和权限检查
- PDF 格式返回数据供客户端生成
This commit is contained in:
gaoziman 2025-12-24 09:40:28 +08:00
parent 6c411438e0
commit 2c292b0a8f

View File

@ -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<ExportData | null> {
// 获取对话
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;
}