feat(API): 添加对话分享 API 路由

- 新增 /api/conversations/[id]/share 路由
  - POST: 创建分享链接
  - GET: 获取分享信息
  - DELETE: 删除分享
- 新增 /api/share/[code] 路由获取分享内容
- 支持选择性消息分享和内容控制
This commit is contained in:
gaoziman 2025-12-24 15:58:12 +08:00
parent ea438eea72
commit abc6cdbcfd
2 changed files with 333 additions and 0 deletions

View File

@ -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, // 新增选择的消息IDnull 表示全部
} = 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 });
}
}

View File

@ -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 });
}
}