feat(API): 添加对话分享 API 路由
- 新增 /api/conversations/[id]/share 路由 - POST: 创建分享链接 - GET: 获取分享信息 - DELETE: 删除分享 - 新增 /api/share/[code] 路由获取分享内容 - 支持选择性消息分享和内容控制
This commit is contained in:
parent
ea438eea72
commit
abc6cdbcfd
203
src/app/api/conversations/[id]/share/route.ts
Normal file
203
src/app/api/conversations/[id]/share/route.ts
Normal 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, // 新增:选择的消息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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
130
src/app/api/share/[code]/route.ts
Normal file
130
src/app/api/share/[code]/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user