paper-burner/local-proxy/routes/chat.js

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;