diff --git a/src/app/api/conversations/[id]/share/route.ts b/src/app/api/conversations/[id]/share/route.ts new file mode 100644 index 0000000..7d7a13c --- /dev/null +++ b/src/app/api/conversations/[id]/share/route.ts @@ -0,0 +1,203 @@ +/** + * 对话分享 API + * POST /api/conversations/[id]/share - 创建分享 + * GET /api/conversations/[id]/share - 获取对话的分享列表 + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/drizzle/db'; +import { conversations, sharedConversations } from '@/drizzle/schema'; +import { eq, and } from 'drizzle-orm'; +import { getCurrentUser } from '@/lib/auth'; +import { nanoid } from 'nanoid'; + +/** + * 生成短链接码(8位字母数字) + */ +function generateShareCode(): string { + return nanoid(8); +} + +/** + * 创建分享 + */ +export async function POST( + 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 body = await request.json(); + const { + title, + description, + includeThinking = true, + includeToolCalls = false, + includeImages = true, + selectedMessageIds = null, // 新增:选择的消息ID,null 表示全部 + } = body; + + // 验证对话存在且属于当前用户 + const conversation = await db.query.conversations.findFirst({ + where: eq(conversations.conversationId, conversationId), + }); + + if (!conversation) { + return NextResponse.json({ error: '对话不存在' }, { status: 404 }); + } + + if (conversation.userId !== user.userId) { + return NextResponse.json({ error: '无权分享此对话' }, { status: 403 }); + } + + // 生成分享信息 + const shareId = nanoid(); + const shareCode = generateShareCode(); + + // 创建分享记录 + await db.insert(sharedConversations).values({ + shareId, + conversationId, + userId: user.userId, + shareCode, + title: title || conversation.title, + description, + includeThinking, + includeToolCalls, + includeImages, + snapshotAt: new Date(), // 记录快照时间点 + selectedMessageIds, // 新增:存储选择的消息ID + }); + + // 构建分享 URL + const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'; + const shareUrl = `${baseUrl}/share/${shareCode}`; + + return NextResponse.json({ + shareId, + shareCode, + shareUrl, + title: title || conversation.title, + }); + } catch (error) { + console.error('Create share error:', error); + return NextResponse.json({ error: '创建分享失败' }, { status: 500 }); + } +} + +/** + * 获取对话的分享列表 + */ +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 conversation = await db.query.conversations.findFirst({ + where: eq(conversations.conversationId, conversationId), + }); + + if (!conversation) { + return NextResponse.json({ error: '对话不存在' }, { status: 404 }); + } + + if (conversation.userId !== user.userId) { + return NextResponse.json({ error: '无权访问' }, { status: 403 }); + } + + // 获取分享列表 + const shares = await db.query.sharedConversations.findMany({ + where: and( + eq(sharedConversations.conversationId, conversationId), + eq(sharedConversations.isActive, true) + ), + orderBy: (shares, { desc }) => [desc(shares.createdAt)], + }); + + // 构建分享 URL + const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'; + + return NextResponse.json({ + shares: shares.map((share) => ({ + shareId: share.shareId, + shareCode: share.shareCode, + shareUrl: `${baseUrl}/share/${share.shareCode}`, + title: share.title, + description: share.description, + viewCount: share.viewCount, + includeThinking: share.includeThinking, + includeToolCalls: share.includeToolCalls, + includeImages: share.includeImages, + createdAt: share.createdAt, + })), + }); + } catch (error) { + console.error('Get shares error:', error); + return NextResponse.json({ error: '获取分享列表失败' }, { status: 500 }); + } +} + +/** + * 删除分享 + */ +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + // 验证用户身份 + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: '未登录' }, { status: 401 }); + } + + // 从查询参数获取 shareId + const searchParams = request.nextUrl.searchParams; + const shareId = searchParams.get('shareId'); + + if (!shareId) { + return NextResponse.json({ error: '缺少 shareId 参数' }, { status: 400 }); + } + + // 验证分享存在且属于当前用户 + const share = await db.query.sharedConversations.findFirst({ + where: eq(sharedConversations.shareId, shareId), + }); + + if (!share) { + return NextResponse.json({ error: '分享不存在' }, { status: 404 }); + } + + if (share.userId !== user.userId) { + return NextResponse.json({ error: '无权删除此分享' }, { status: 403 }); + } + + // 软删除(设置 isActive 为 false) + await db + .update(sharedConversations) + .set({ isActive: false, updatedAt: new Date() }) + .where(eq(sharedConversations.shareId, shareId)); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Delete share error:', error); + return NextResponse.json({ error: '删除分享失败' }, { status: 500 }); + } +} diff --git a/src/app/api/share/[code]/route.ts b/src/app/api/share/[code]/route.ts new file mode 100644 index 0000000..beedf05 --- /dev/null +++ b/src/app/api/share/[code]/route.ts @@ -0,0 +1,130 @@ +/** + * 公开分享内容 API + * GET /api/share/[code] - 获取分享的对话内容(无需登录) + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/drizzle/db'; +import { sharedConversations, conversations, messages } from '@/drizzle/schema'; +import { eq, asc, and, lte, inArray } from 'drizzle-orm'; + +/** + * 获取分享的对话内容 + */ +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ code: string }> } +) { + try { + const { code: shareCode } = await params; + + // 查找分享记录 + const share = await db.query.sharedConversations.findFirst({ + where: eq(sharedConversations.shareCode, shareCode), + }); + + if (!share) { + return NextResponse.json({ error: '分享不存在' }, { status: 404 }); + } + + // 检查分享是否激活 + if (!share.isActive) { + return NextResponse.json({ error: '分享已失效' }, { status: 410 }); + } + + // 获取对话信息 + const conversation = await db.query.conversations.findFirst({ + where: eq(conversations.conversationId, share.conversationId), + }); + + if (!conversation) { + return NextResponse.json({ error: '对话不存在' }, { status: 404 }); + } + + // 获取消息列表 + // 优先级:selectedMessageIds > snapshotAt > 全部 + let messageList; + + if (share.selectedMessageIds && share.selectedMessageIds.length > 0) { + // 如果指定了消息ID,按消息ID过滤 + messageList = await db.query.messages.findMany({ + where: and( + eq(messages.conversationId, share.conversationId), + inArray(messages.messageId, share.selectedMessageIds) + ), + orderBy: [asc(messages.createdAt)], + }); + } else if (share.snapshotAt) { + // 否则按快照时间过滤 + messageList = await db.query.messages.findMany({ + where: and( + eq(messages.conversationId, share.conversationId), + lte(messages.createdAt, share.snapshotAt) + ), + orderBy: [asc(messages.createdAt)], + }); + } else { + // 获取全部消息 + messageList = await db.query.messages.findMany({ + where: eq(messages.conversationId, share.conversationId), + orderBy: [asc(messages.createdAt)], + }); + } + + // 根据分享设置过滤消息内容 + const filteredMessages = messageList.map((msg) => ({ + id: msg.messageId, + role: msg.role, + content: msg.content, + // 根据设置决定是否包含思考内容 + thinkingContent: share.includeThinking ? msg.thinkingContent : null, + // 根据设置决定是否包含工具调用 + toolCalls: share.includeToolCalls ? msg.toolCalls : null, + // 根据设置决定是否包含图片 + images: share.includeImages ? msg.images : null, + searchImages: share.includeImages ? msg.searchImages : null, + uploadedImages: share.includeImages ? msg.uploadedImages : null, + // 使用的工具(简化显示) + usedTools: msg.usedTools, + // Token 统计 + inputTokens: msg.inputTokens, + outputTokens: msg.outputTokens, + // 时间 + createdAt: msg.createdAt, + })); + + // 更新查看次数(异步,不阻塞响应) + db.update(sharedConversations) + .set({ + viewCount: (share.viewCount || 0) + 1, + updatedAt: new Date(), + }) + .where(eq(sharedConversations.shareId, share.shareId)) + .then(() => {}) + .catch((err) => console.error('Update view count error:', err)); + + return NextResponse.json({ + // 分享信息 + share: { + title: share.title, + description: share.description, + viewCount: (share.viewCount || 0) + 1, + createdAt: share.createdAt, + }, + // 对话信息 + conversation: { + id: conversation.conversationId, + title: conversation.title, + model: conversation.model, + messageCount: filteredMessages.length, // 使用快照中的实际消息数量 + totalTokens: conversation.totalTokens, + createdAt: conversation.createdAt, + }, + // 消息列表 + messages: filteredMessages, + }); + } catch (error) { + console.error('Get shared content error:', error); + return NextResponse.json({ error: '获取分享内容失败' }, { status: 500 }); + } +}