/** * 聊天历史路由 * 复用 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;