- 实现 GET /api/conversations/[id]/export 接口 - 支持 markdown/json/html/pdf 四种导出格式 - 添加用户身份验证和权限检查 - PDF 格式返回数据供客户端生成
168 lines
5.0 KiB
TypeScript
168 lines
5.0 KiB
TypeScript
/**
|
||
* 对话导出 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;
|
||
}
|