🎉 first commit
This commit is contained in:
93
app/routes/api.chat.$action/delete.server.ts
Normal file
93
app/routes/api.chat.$action/delete.server.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import type { ActionFunctionArgs } from '@remix-run/node';
|
||||
import { deleteChat, getUserChatById } from '~/lib/.server/chat';
|
||||
import { errorResponse, successResponse } from '~/utils/api-response';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
|
||||
const logger = createScopedLogger('api.chat.delete');
|
||||
|
||||
export type HandleDeleteActionArgs = ActionFunctionArgs & {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理删除聊天操作
|
||||
*/
|
||||
export async function handleDeleteAction({ request, userId }: HandleDeleteActionArgs) {
|
||||
if (request.method !== 'DELETE' && request.method !== 'POST') {
|
||||
return errorResponse(405, '请求方法不支持');
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取请求数据
|
||||
const formData = await request.formData();
|
||||
const id = formData.get('id')?.toString();
|
||||
const idsString = formData.get('ids')?.toString();
|
||||
|
||||
let ids: string[] | undefined;
|
||||
if (idsString) {
|
||||
try {
|
||||
ids = JSON.parse(idsString);
|
||||
} catch (error) {
|
||||
logger.error('解析 ids 参数失败', error);
|
||||
return errorResponse(400, 'ids 参数格式无效');
|
||||
}
|
||||
}
|
||||
|
||||
if (!id && (!ids || !Array.isArray(ids) || ids.length === 0)) {
|
||||
return errorResponse(400, '缺少有效的聊天ID');
|
||||
}
|
||||
|
||||
if (id) {
|
||||
const chat = await getUserChatById(id, userId);
|
||||
if (!chat) {
|
||||
return errorResponse(404, '未找到聊天记录或无权限操作');
|
||||
}
|
||||
|
||||
await deleteChat(id);
|
||||
logger.debug(`用户 ${userId} 删除了聊天 ${id}`);
|
||||
|
||||
return successResponse(id, '删除聊天成功');
|
||||
}
|
||||
|
||||
const idsToDelete = ids as string[];
|
||||
const results = {
|
||||
success: [] as string[],
|
||||
failed: [] as string[],
|
||||
totalMessagesDeleted: 0,
|
||||
};
|
||||
|
||||
for (const chatId of idsToDelete) {
|
||||
try {
|
||||
const chat = await getUserChatById(chatId, userId);
|
||||
if (!chat) {
|
||||
results.failed.push(chatId);
|
||||
continue;
|
||||
}
|
||||
|
||||
const messageCount = chat.messages?.length || 0;
|
||||
|
||||
await deleteChat(chatId);
|
||||
results.success.push(chatId);
|
||||
results.totalMessagesDeleted += messageCount;
|
||||
|
||||
logger.debug(`用户 ${userId} 删除了聊天 ${chatId},级联删除了 ${messageCount} 条消息及其关联数据`);
|
||||
} catch (error) {
|
||||
logger.error(`删除聊天 ${chatId} 失败`, error);
|
||||
results.failed.push(chatId);
|
||||
}
|
||||
}
|
||||
|
||||
return successResponse(
|
||||
{
|
||||
results,
|
||||
totalSuccess: results.success.length,
|
||||
totalFailed: results.failed.length,
|
||||
totalMessagesDeleted: results.totalMessagesDeleted,
|
||||
},
|
||||
'删除聊天成功',
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('删除聊天失败', error);
|
||||
return errorResponse(500, '删除聊天失败');
|
||||
}
|
||||
}
|
||||
167
app/routes/api.chat.$action/fork.server.ts
Normal file
167
app/routes/api.chat.$action/fork.server.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import type { ActionFunctionArgs } from '@remix-run/node';
|
||||
import { getUserChatById } from '~/lib/.server/chat';
|
||||
import { prisma } from '~/lib/.server/prisma';
|
||||
import { errorResponse, successResponse } from '~/utils/api-response';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
|
||||
const logger = createScopedLogger('api.chat.fork');
|
||||
|
||||
export type HandleForkActionArgs = ActionFunctionArgs & {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理复制聊天操作
|
||||
*/
|
||||
export async function handleForkAction({ request, userId }: HandleForkActionArgs) {
|
||||
try {
|
||||
const { sourceChatId, messageId } = await request.json();
|
||||
|
||||
if (!sourceChatId) {
|
||||
return errorResponse(400, '源聊天ID不能为空');
|
||||
}
|
||||
|
||||
const sourceChat = await getUserChatById(sourceChatId, userId);
|
||||
if (!sourceChat) {
|
||||
return errorResponse(404, '找不到源聊天');
|
||||
}
|
||||
|
||||
// 使用事务处理整个复制过程,确保数据一致性
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
logger.debug(`开始复制聊天 ${sourceChatId} 的数据...`);
|
||||
|
||||
const metadata =
|
||||
sourceChat.metadata &&
|
||||
typeof sourceChat.metadata === 'object' &&
|
||||
!Array.isArray(sourceChat.metadata) &&
|
||||
sourceChat.metadata !== null
|
||||
? (sourceChat.metadata as Record<string, any>)
|
||||
: undefined;
|
||||
|
||||
// 创建新聊天
|
||||
const newChat = await tx.chat.create({
|
||||
data: {
|
||||
userId,
|
||||
description: `${sourceChat.description || 'Chat'} (Copy)`,
|
||||
urlId: sourceChat.urlId || undefined,
|
||||
metadata,
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug(`为用户 ${userId} 创建了聊天副本: ${newChat.id}`);
|
||||
|
||||
// 检查是否有消息需要复制
|
||||
if (sourceChat.messages && sourceChat.messages.length > 0) {
|
||||
// 根据messageId过滤消息
|
||||
let messagesToCopy = sourceChat.messages;
|
||||
|
||||
// 如果指定了messageId,过滤消息
|
||||
if (messageId) {
|
||||
const targetIndex = messagesToCopy.findIndex((msg) => msg.id === messageId);
|
||||
|
||||
if (targetIndex === -1) {
|
||||
await tx.chat.delete({ where: { id: newChat.id } });
|
||||
logger.warn('在聊天中找不到指定的消息', { sourceChatId, messageId });
|
||||
return errorResponse(404, '在聊天中找不到指定的消息');
|
||||
}
|
||||
|
||||
// 只保留从 0 到 targetIndex 的消息
|
||||
messagesToCopy = messagesToCopy.slice(0, targetIndex + 1);
|
||||
|
||||
logger.debug(`将复制聊天 ${sourceChatId} 的前 ${messagesToCopy.length} 条消息(到消息ID: ${messageId})`);
|
||||
} else {
|
||||
logger.debug(`将复制聊天 ${sourceChatId} 的全部 ${messagesToCopy.length} 条消息`);
|
||||
}
|
||||
|
||||
// 准备批量创建消息的数据
|
||||
// 由于 prisma 中 output 与 input 类型不一致,需要手动复制 https://github.com/prisma/prisma/issues/9247
|
||||
const messageCreateData = messagesToCopy.map((msg) => ({
|
||||
chatId: newChat.id,
|
||||
userId,
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
annotations: msg.annotations || undefined,
|
||||
metadata: msg.metadata || undefined,
|
||||
parts: msg.parts || undefined,
|
||||
revisionId: msg.revisionId || undefined,
|
||||
isDiscarded: msg.isDiscarded || false,
|
||||
}));
|
||||
|
||||
logger.debug('批量创建消息数据', JSON.stringify(messageCreateData));
|
||||
|
||||
// 使用批量创建消息函数创建消息
|
||||
await tx.message.createMany({
|
||||
data: messageCreateData,
|
||||
});
|
||||
|
||||
logger.debug(`为聊天 ${newChat.id} 批量创建了 ${messageCreateData.length} 条消息`);
|
||||
|
||||
const newMessages = await tx.message.findMany({
|
||||
where: { chatId: newChat.id },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
|
||||
// 创建映射:原消息ID -> 新消息对象
|
||||
const messageMapping = messagesToCopy.reduce(
|
||||
(map, oldMsg, index) => {
|
||||
map[oldMsg.id] = newMessages[index];
|
||||
return map;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
);
|
||||
|
||||
const pageToCreate = messagesToCopy
|
||||
.filter((msg) => msg.page != null)
|
||||
.map((msg) => {
|
||||
const page = msg.page!;
|
||||
return {
|
||||
messageId: messageMapping[msg.id].id,
|
||||
pages: JSON.parse(JSON.stringify(page.pages)),
|
||||
};
|
||||
});
|
||||
|
||||
// 批量创建 Page 项目数据
|
||||
if (pageToCreate.length > 0) {
|
||||
await tx.page.createMany({
|
||||
data: pageToCreate,
|
||||
});
|
||||
logger.debug(`为聊天 ${newChat.id} 批量创建了 ${pageToCreate.length} 个Page项目`);
|
||||
}
|
||||
|
||||
// 收集需要创建的区块数据
|
||||
const sectionsToCreate = [];
|
||||
for (const msg of messagesToCopy) {
|
||||
if (msg.sections && msg.sections.length > 0) {
|
||||
for (const section of msg.sections) {
|
||||
sectionsToCreate.push({
|
||||
messageId: messageMapping[msg.id].id,
|
||||
type: section.type,
|
||||
action: section.action,
|
||||
actionId: section.actionId,
|
||||
pageName: section.pageName,
|
||||
content: section.content,
|
||||
domId: section.domId,
|
||||
sort: section.sort,
|
||||
rootDomId: section.rootDomId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 批量创建区块数据
|
||||
if (sectionsToCreate.length > 0) {
|
||||
await tx.section.createMany({
|
||||
data: sectionsToCreate,
|
||||
});
|
||||
logger.debug(`为聊天 ${newChat.id} 批量创建了 ${sectionsToCreate.length} 个区块`);
|
||||
}
|
||||
}
|
||||
|
||||
// 返回新聊天ID
|
||||
return successResponse(newChat.id, '聊天复制成功');
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('复制聊天失败:', error);
|
||||
return errorResponse(500, '服务器处理请求失败');
|
||||
}
|
||||
}
|
||||
51
app/routes/api.chat.$action/list.server.ts
Normal file
51
app/routes/api.chat.$action/list.server.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { LoaderFunctionArgs } from '@remix-run/node';
|
||||
import { getUserChats } from '~/lib/.server/chat';
|
||||
import { errorResponse, successResponse } from '~/utils/api-response';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
|
||||
const logger = createScopedLogger('api.chat.list');
|
||||
|
||||
export type HandleListLoaderArgs = LoaderFunctionArgs & {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理获取聊天列表操作
|
||||
*/
|
||||
export async function handleListLoader({ request, userId }: HandleListLoaderArgs) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const searchQuery = url.searchParams.get('q') || '';
|
||||
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
||||
const offset = parseInt(url.searchParams.get('offset') || '0', 10);
|
||||
|
||||
logger.debug(`获取用户 ${userId} 的聊天列表,搜索: ${searchQuery}, 限制: ${limit}, 偏移: ${offset}`);
|
||||
|
||||
const { chats, total } = await getUserChats(userId, limit, offset);
|
||||
|
||||
// 如果有搜索关键词,过滤结果
|
||||
let filteredChats = chats;
|
||||
if (searchQuery) {
|
||||
filteredChats = chats.filter((chat) => chat.description?.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||
}
|
||||
|
||||
return successResponse(
|
||||
{
|
||||
chats: filteredChats.map((chat) => ({
|
||||
id: chat.id,
|
||||
urlId: chat.urlId,
|
||||
description: chat.description,
|
||||
timestamp: chat.updatedAt,
|
||||
lastMessage: chat.messages[0]?.content,
|
||||
})),
|
||||
total: searchQuery ? filteredChats.length : total,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
'获取聊天列表成功',
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('获取聊天列表失败', error);
|
||||
return errorResponse(500, '获取聊天列表失败');
|
||||
}
|
||||
}
|
||||
74
app/routes/api.chat.$action/route.tsx
Normal file
74
app/routes/api.chat.$action/route.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { type ActionFunctionArgs, type LoaderFunctionArgs } from '@remix-run/node';
|
||||
import { requireAuth } from '~/lib/.server/auth';
|
||||
import { errorResponse } from '~/utils/api-response';
|
||||
import { handleDeleteAction } from './delete.server';
|
||||
import { handleForkAction } from './fork.server';
|
||||
import { handleListLoader } from './list.server';
|
||||
import { handleUpdateAction } from './update.server';
|
||||
|
||||
/**
|
||||
* 动态路由处理聊天相关操作
|
||||
* 支持的操作:
|
||||
* - list: 获取聊天列表(GET请求)
|
||||
* - delete: 删除聊天
|
||||
* - update: 更新聊天
|
||||
* - fork: 复制聊天
|
||||
*/
|
||||
|
||||
/**
|
||||
* 处理GET请求,用于获取数据
|
||||
*/
|
||||
export async function loader(args: LoaderFunctionArgs) {
|
||||
const authResult = await requireAuth(args.request, { isApi: true });
|
||||
|
||||
if (authResult instanceof Response) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
const userId = authResult.userInfo?.sub;
|
||||
if (!userId) {
|
||||
return errorResponse(401, '用户未登录');
|
||||
}
|
||||
|
||||
// 获取操作类型
|
||||
const { action } = args.params;
|
||||
|
||||
// 根据操作类型分发到不同的处理函数
|
||||
switch (action) {
|
||||
case 'list':
|
||||
return handleListLoader({ ...args, userId });
|
||||
default:
|
||||
return errorResponse(400, `不支持的操作: ${action}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理非GET请求,用于修改数据
|
||||
*/
|
||||
export async function action(args: ActionFunctionArgs) {
|
||||
const authResult = await requireAuth(args.request, { isApi: true });
|
||||
|
||||
if (authResult instanceof Response) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
const userId = authResult.userInfo?.sub;
|
||||
if (!userId) {
|
||||
return errorResponse(401, '用户未登录');
|
||||
}
|
||||
|
||||
// 获取操作类型
|
||||
const { action } = args.params;
|
||||
|
||||
// 根据操作类型分发到不同的处理函数
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
return handleDeleteAction({ ...args, userId });
|
||||
case 'update':
|
||||
return handleUpdateAction({ ...args, userId });
|
||||
case 'fork':
|
||||
return handleForkAction({ ...args, userId });
|
||||
default:
|
||||
return errorResponse(400, `不支持的操作: ${action}`);
|
||||
}
|
||||
}
|
||||
61
app/routes/api.chat.$action/update.server.ts
Normal file
61
app/routes/api.chat.$action/update.server.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { ActionFunctionArgs } from '@remix-run/node';
|
||||
import { getUserChatById, updateChat } from '~/lib/.server/chat';
|
||||
import { errorResponse, successResponse } from '~/utils/api-response';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
|
||||
const logger = createScopedLogger('api.chat.update');
|
||||
|
||||
export type HandleUpdateActionArgs = ActionFunctionArgs & {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理更新聊天操作
|
||||
*/
|
||||
export async function handleUpdateAction({ request, userId }: HandleUpdateActionArgs) {
|
||||
// 只接受POST请求
|
||||
if (request.method !== 'POST') {
|
||||
return errorResponse(405, '请求方法不支持');
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const id = formData.get('id') as string;
|
||||
const description = formData.get('description') as string;
|
||||
|
||||
logger.debug(`处理聊天更新请求,ID: ${id}, 描述: ${description}`);
|
||||
|
||||
if (!id) {
|
||||
return errorResponse(400, '缺少聊天ID');
|
||||
}
|
||||
|
||||
if (!description || description.trim() === '') {
|
||||
return errorResponse(400, '描述不能为空');
|
||||
}
|
||||
|
||||
// 验证聊天记录是否属于当前用户
|
||||
const chat = await getUserChatById(id, userId);
|
||||
if (!chat) {
|
||||
return errorResponse(404, '未找到聊天记录或无权限操作');
|
||||
}
|
||||
|
||||
// 更新描述
|
||||
const updatedChat = await updateChat(id, { description });
|
||||
|
||||
logger.debug(`用户 ${userId} 更新了聊天 ${id} 的描述`);
|
||||
|
||||
return successResponse(
|
||||
{
|
||||
chat: {
|
||||
id: updatedChat.id,
|
||||
description: updatedChat.description,
|
||||
timestamp: updatedChat.updatedAt,
|
||||
},
|
||||
},
|
||||
'更新聊天描述成功',
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('更新聊天描述失败', error);
|
||||
return errorResponse(500, '更新聊天描述失败');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user