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

256 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 聊天历史路由
* 复用 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);
}
// 文档 ID 验证(支持 UUID 和旧格式的 filename-based ID
function isValidDocumentId(id) {
if (!id || typeof id !== 'string') return false;
// UUID 格式
if (isValidUUID(id)) return true;
// 旧格式filename_timestamp用于向后兼容或迁移提示
// 不再接受非 UUID 格式
return false;
}
// 获取文档的聊天历史
router.get('/:documentId/history', async (req, res, next) => {
try {
const { documentId } = req.params;
const { limit = 100, before } = req.query;
// 验证 UUID 格式
if (!isValidUUID(documentId)) {
console.warn(`[Chat] Invalid document ID format: ${documentId.substring(0, 50)}...`);
return res.status(400).json({
error: 'Invalid document ID format. Expected UUID.',
hint: 'Document ID should be a UUID like "550e8400-e29b-41d4-a716-446655440000". If you see a filename-based ID, the document may have been created before the UUID fix.'
});
}
// 验证和规范化参数
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;