feat(导出功能): 添加对话导出 API 路由
- 实现 GET /api/conversations/[id]/export 接口 - 支持 markdown/json/html/pdf 四种导出格式 - 添加用户身份验证和权限检查 - PDF 格式返回数据供客户端生成
This commit is contained in:
parent
6c411438e0
commit
2c292b0a8f
167
src/app/api/conversations/[id]/export/route.ts
Normal file
167
src/app/api/conversations/[id]/export/route.ts
Normal 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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user