242 lines
5.8 KiB
JavaScript
242 lines
5.8 KiB
JavaScript
/**
|
|
* 聊天历史路由
|
|
* 复用 server/src/routes/chat.js 的逻辑
|
|
*/
|
|
|
|
import express from 'express';
|
|
import { prisma } from '../db/client.js';
|
|
|
|
const router = express.Router();
|
|
|
|
// 允许的聊天角色
|
|
const ALLOWED_ROLES = ['user', 'assistant'];
|
|
|
|
// UUID 验证
|
|
function isValidUUID(id) {
|
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
|
|
}
|
|
|
|
// 获取文档的聊天历史
|
|
router.get('/:documentId/history', async (req, res, next) => {
|
|
try {
|
|
const { documentId } = req.params;
|
|
const { limit = 100, before } = req.query;
|
|
|
|
// 验证 UUID 格式
|
|
if (!isValidUUID(documentId)) {
|
|
return res.status(400).json({ error: 'Invalid document ID format' });
|
|
}
|
|
|
|
// 验证和规范化参数
|
|
const limitNum = Math.min(Math.max(parseInt(limit) || 100, 1), 1000);
|
|
|
|
// 验证文档所有权
|
|
const document = await prisma.document.findFirst({
|
|
where: {
|
|
id: documentId,
|
|
userId: req.user.id
|
|
}
|
|
});
|
|
|
|
if (!document) {
|
|
return res.status(404).json({ error: 'Document not found' });
|
|
}
|
|
|
|
// 构建查询条件
|
|
const where = {
|
|
documentId,
|
|
userId: req.user.id
|
|
};
|
|
|
|
if (before) {
|
|
where.timestamp = { lt: new Date(before) };
|
|
}
|
|
|
|
// 获取消息
|
|
const messages = await prisma.chatMessage.findMany({
|
|
where,
|
|
orderBy: { timestamp: 'desc' },
|
|
take: limitNum,
|
|
select: {
|
|
id: true,
|
|
role: true,
|
|
content: true,
|
|
timestamp: true,
|
|
metadata: true
|
|
}
|
|
});
|
|
|
|
// 反转顺序,使最早的消息在前
|
|
const sortedMessages = messages.reverse();
|
|
|
|
res.json({
|
|
messages: sortedMessages,
|
|
hasMore: messages.length === limitNum
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// 添加聊天消息
|
|
router.post('/:documentId/history', async (req, res, next) => {
|
|
try {
|
|
const { documentId } = req.params;
|
|
const { role, content, metadata } = req.body;
|
|
|
|
// 验证 UUID 格式
|
|
if (!isValidUUID(documentId)) {
|
|
return res.status(400).json({ error: 'Invalid document ID format' });
|
|
}
|
|
|
|
// 输入验证
|
|
if (!role || typeof role !== 'string') {
|
|
return res.status(400).json({ error: 'Role is required' });
|
|
}
|
|
|
|
if (!ALLOWED_ROLES.includes(role)) {
|
|
return res.status(400).json({ error: `Role must be one of: ${ALLOWED_ROLES.join(', ')}` });
|
|
}
|
|
|
|
if (!content || typeof content !== 'string') {
|
|
return res.status(400).json({ error: 'Content is required' });
|
|
}
|
|
|
|
// 限制内容长度
|
|
const maxContentLength = 100000;
|
|
if (content.length > maxContentLength) {
|
|
return res.status(400).json({ error: `Content too long (max ${maxContentLength} characters)` });
|
|
}
|
|
|
|
// 验证文档所有权
|
|
const document = await prisma.document.findFirst({
|
|
where: {
|
|
id: documentId,
|
|
userId: req.user.id
|
|
}
|
|
});
|
|
|
|
if (!document) {
|
|
return res.status(404).json({ error: 'Document not found' });
|
|
}
|
|
|
|
// 创建消息
|
|
const message = await prisma.chatMessage.create({
|
|
data: {
|
|
documentId,
|
|
userId: req.user.id,
|
|
role,
|
|
content,
|
|
metadata
|
|
}
|
|
});
|
|
|
|
res.status(201).json(message);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// 批量添加聊天消息
|
|
router.post('/:documentId/history/batch', async (req, res, next) => {
|
|
try {
|
|
const { documentId } = req.params;
|
|
const { messages } = req.body;
|
|
|
|
// 验证 UUID 格式
|
|
if (!isValidUUID(documentId)) {
|
|
return res.status(400).json({ error: 'Invalid document ID format' });
|
|
}
|
|
|
|
// 输入验证
|
|
if (!Array.isArray(messages)) {
|
|
return res.status(400).json({ error: 'Messages must be an array' });
|
|
}
|
|
|
|
// 限制批量大小
|
|
const maxBatchSize = 1000;
|
|
if (messages.length > maxBatchSize) {
|
|
return res.status(400).json({ error: `Batch size too large (max ${maxBatchSize} messages)` });
|
|
}
|
|
|
|
// 验证文档所有权
|
|
const document = await prisma.document.findFirst({
|
|
where: {
|
|
id: documentId,
|
|
userId: req.user.id
|
|
}
|
|
});
|
|
|
|
if (!document) {
|
|
return res.status(404).json({ error: 'Document not found' });
|
|
}
|
|
|
|
// 验证每条消息的基本格式
|
|
const validMessages = messages.filter(msg => {
|
|
return msg && typeof msg === 'object' &&
|
|
ALLOWED_ROLES.includes(msg.role) &&
|
|
typeof msg.content === 'string';
|
|
});
|
|
|
|
if (validMessages.length !== messages.length) {
|
|
return res.status(400).json({ error: 'Some messages have invalid format' });
|
|
}
|
|
|
|
// 批量创建消息
|
|
const createdMessages = await prisma.chatMessage.createMany({
|
|
data: validMessages.map(msg => ({
|
|
documentId,
|
|
userId: req.user.id,
|
|
role: msg.role,
|
|
content: msg.content,
|
|
timestamp: msg.timestamp ? new Date(msg.timestamp) : undefined,
|
|
metadata: msg.metadata
|
|
})),
|
|
skipDuplicates: true
|
|
});
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
count: createdMessages.count
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// 清空文档的聊天历史
|
|
router.delete('/:documentId/history', async (req, res, next) => {
|
|
try {
|
|
const { documentId } = req.params;
|
|
|
|
// 验证 UUID 格式
|
|
if (!isValidUUID(documentId)) {
|
|
return res.status(400).json({ error: 'Invalid document ID format' });
|
|
}
|
|
|
|
// 验证文档所有权
|
|
const document = await prisma.document.findFirst({
|
|
where: {
|
|
id: documentId,
|
|
userId: req.user.id
|
|
}
|
|
});
|
|
|
|
if (!document) {
|
|
return res.status(404).json({ error: 'Document not found' });
|
|
}
|
|
|
|
await prisma.chatMessage.deleteMany({
|
|
where: {
|
|
documentId,
|
|
userId: req.user.id
|
|
}
|
|
});
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
export default router; |