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

404 lines
9.5 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/document.js 的逻辑
*/
import express from 'express';
import { prisma } from '../db/client.js';
const router = express.Router();
// 允许的状态值白名单
const ALLOWED_STATUSES = ['PENDING', 'PROCESSING', 'COMPLETED', 'FAILED'];
// ==================== 文档 CRUD ====================
// 获取文档列表
router.get('/', async (req, res, next) => {
try {
const { page = 1, limit = 20, status } = req.query;
const where = {
userId: req.user.id,
...(status && ALLOWED_STATUSES.includes(status) && { status })
};
const pageNum = Math.max(parseInt(page) || 1, 1);
const limitNum = Math.min(Math.max(parseInt(limit) || 20, 1), 100);
const documents = await prisma.document.findMany({
where,
orderBy: { createdAt: 'desc' },
skip: (pageNum - 1) * limitNum,
take: limitNum,
select: {
id: true,
fileName: true,
fileSize: true,
fileType: true,
status: true,
ocrProvider: true,
ocrText: true,
translationModel: true,
translatedText: true,
processingTime: true,
createdAt: true,
updatedAt: true,
metadata: true,
ocrMetadata: true,
translationMetadata: true,
summary: true,
toc: true
}
});
const total = await prisma.document.count({ where });
// 字段映射:适配前端期望的字段名,并从 metadata 中提取嵌套字段
const mappedDocs = documents.map(doc => {
const meta = doc.metadata || {};
return {
...doc,
name: doc.fileName,
size: doc.fileSize,
time: doc.createdAt,
ocr: doc.ocrText,
translation: doc.translatedText,
ocrEngine: doc.ocrProvider,
translationModelName: doc.translationModel,
// 从 metadata 中提取的字段
ocrChunks: meta.ocrChunks || [],
translatedChunks: meta.translatedChunks || [],
images: meta.images || [],
metadata: meta
};
});
res.json({
documents: mappedDocs,
pagination: {
page: pageNum,
limit: limitNum,
total,
totalPages: Math.ceil(total / limitNum)
}
});
} catch (error) {
next(error);
}
});
// 获取单个文档详情
router.get('/:id', async (req, res, next) => {
try {
const { id } = req.params;
const document = await prisma.document.findFirst({
where: {
id,
userId: req.user.id
},
include: {
annotations: true,
semanticGroups: true
}
});
if (!document) {
return res.status(404).json({ error: 'Document not found' });
}
// 字段映射:适配前端期望的字段名,并从 metadata 中提取嵌套字段
const meta = document.metadata || {};
const mappedDoc = {
...document,
name: document.fileName,
size: document.fileSize,
time: document.createdAt,
ocr: document.ocrText,
translation: document.translatedText,
ocrEngine: document.ocrProvider,
translationModelName: document.translationModel,
// 从 metadata 中提取的字段
ocrChunks: meta.ocrChunks || [],
translatedChunks: meta.translatedChunks || [],
images: meta.images || [],
metadata: meta
};
res.json(mappedDoc);
} catch (error) {
next(error);
}
});
// 创建文档记录
router.post('/', async (req, res, next) => {
try {
const body = { ...req.body };
// 前端字段映射name -> fileName, size -> fileSize
if (body.name && !body.fileName) {
body.fileName = body.name;
}
if (body.size && !body.fileSize) {
body.fileSize = body.size;
}
// Schema 中定义的字段
const schemaFields = {
fileName: body.fileName || body.name,
fileSize: body.fileSize || body.size,
fileType: body.fileType,
filePath: body.filePath,
status: body.status || 'PENDING',
ocrProvider: body.ocrProvider || body.ocrEngine,
ocrText: body.ocrText || body.ocr,
ocrMetadata: body.ocrMetadata,
translationModel: body.translationModel || body.translationModelName,
translatedText: body.translatedText || body.translation,
translationMetadata: body.translationMetadata,
summary: body.summary,
toc: body.toc,
processingTime: body.processingTime,
errorMessage: body.errorMessage
};
// 其他字段保存到 metadata
const metadataFields = {};
const knownFields = [
'fileName', 'name', 'fileSize', 'size', 'fileType', 'filePath', 'status',
'ocrProvider', 'ocrText', 'ocr', 'ocrMetadata', 'ocrEngine', 'ocrSource',
'translationModel', 'translatedText', 'translation', 'translationMetadata',
'translationModelName', 'summary', 'toc', 'processingTime', 'errorMessage',
'id', 'userId', 'createdAt', 'updatedAt'
];
for (const [key, value] of Object.entries(body)) {
if (!knownFields.includes(key) && value !== undefined) {
metadataFields[key] = value;
}
}
// 合并原有 metadata
if (body.metadata && typeof body.metadata === 'object') {
Object.assign(metadataFields, body.metadata);
}
schemaFields.metadata = Object.keys(metadataFields).length > 0 ? metadataFields : body.metadata;
// 移除 undefined 字段
const cleanData = {};
for (const [key, value] of Object.entries(schemaFields)) {
if (value !== undefined) {
cleanData[key] = value;
}
}
const document = await prisma.document.create({
data: {
userId: req.user.id,
...cleanData
}
});
// 返回时添加前端需要的字段
const responseData = {
...document,
name: document.fileName,
size: document.fileSize
};
res.status(201).json(responseData);
} catch (error) {
next(error);
}
});
// 更新文档
router.put('/:id', async (req, res, next) => {
try {
const { id } = req.params;
await prisma.document.updateMany({
where: {
id,
userId: req.user.id
},
data: req.body
});
res.json({ success: true });
} catch (error) {
next(error);
}
});
// 删除文档
router.delete('/:id', async (req, res, next) => {
try {
const { id } = req.params;
const document = await prisma.document.findFirst({
where: {
id,
userId: req.user.id
}
});
if (document) {
await prisma.document.delete({
where: { id }
});
}
res.json({ success: true });
} catch (error) {
next(error);
}
});
// ==================== 标注管理 ====================
// 保存标注
router.post('/:id/annotations', async (req, res, next) => {
try {
const { id } = req.params;
const annotation = await prisma.annotation.create({
data: {
userId: req.user.id,
documentId: id,
...req.body
}
});
res.status(201).json(annotation);
} catch (error) {
next(error);
}
});
// 获取文档的所有标注
router.get('/:id/annotations', async (req, res, next) => {
try {
const { id } = req.params;
const annotations = await prisma.annotation.findMany({
where: {
documentId: id,
userId: req.user.id
}
});
res.json(annotations);
} catch (error) {
next(error);
}
});
// 更新标注
router.put('/:documentId/annotations/:annotationId', async (req, res, next) => {
try {
const { documentId, annotationId } = req.params;
await prisma.annotation.updateMany({
where: {
id: annotationId,
documentId,
userId: req.user.id
},
data: req.body
});
res.json({ success: true });
} catch (error) {
next(error);
}
});
// 删除标注
router.delete('/:documentId/annotations/:annotationId', async (req, res, next) => {
try {
const { documentId, annotationId } = req.params;
await prisma.annotation.deleteMany({
where: {
id: annotationId,
documentId,
userId: req.user.id
}
});
res.json({ success: true });
} catch (error) {
next(error);
}
});
// ==================== 意群数据 ====================
// 保存意群数据
router.post('/:id/semantic-groups', async (req, res, next) => {
try {
const { id } = req.params;
const { groups, version, source } = req.body;
// 验证文档所有权
const document = await prisma.document.findFirst({
where: {
id,
userId: req.user.id
}
});
if (!document) {
return res.status(404).json({ error: 'Document not found' });
}
const semanticGroup = await prisma.semanticGroup.upsert({
where: { documentId: id },
update: { groups, version, source },
create: {
documentId: id,
groups,
version,
source
}
});
res.json(semanticGroup);
} catch (error) {
next(error);
}
});
// 获取意群数据
router.get('/:id/semantic-groups', async (req, res, next) => {
try {
const { id } = req.params;
// 验证文档所有权
const document = await prisma.document.findFirst({
where: {
id,
userId: req.user.id
}
});
if (!document) {
return res.status(404).json({ error: 'Document not found' });
}
const semanticGroup = await prisma.semanticGroup.findUnique({
where: { documentId: id }
});
if (!semanticGroup) {
return res.status(404).json({ error: 'Semantic groups not found' });
}
res.json(semanticGroup);
} catch (error) {
next(error);
}
});
export default router;