256 lines
6.4 KiB
JavaScript
256 lines
6.4 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);
|
||
}
|
||
|
||
// 文档 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; |