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