diff --git a/js/app.js b/js/app.js
index f7637f6..8af1d75 100644
--- a/js/app.js
+++ b/js/app.js
@@ -2255,8 +2255,14 @@ async function handleReadClick() {
});
// 保存到数据库
+ let savedDoc = record;
if (typeof window.storageAdapter !== 'undefined' && typeof window.storageAdapter.saveResultToDB === 'function') {
- await window.storageAdapter.saveResultToDB(record);
+ const result = await window.storageAdapter.saveResultToDB(record);
+ // 后端模式:使用返回的 UUID
+ if (result && result.id) {
+ savedDoc = result;
+ console.log('[仅阅读] 后端返回的 UUID:', result.id);
+ }
} else if (typeof saveResultToDB === 'function') {
await saveResultToDB(record);
} else {
@@ -2269,8 +2275,8 @@ async function handleReadClick() {
console.log('[仅阅读] 数据已保存,准备跳转...');
- // 跳转到历史详情页面
- window.location.href = `views/history/history_detail.html?id=${encodeURIComponent(recordId)}`;
+ // 跳转到历史详情页面(使用保存后的 ID)
+ window.location.href = `views/history/history_detail.html?id=${encodeURIComponent(savedDoc.id)}`;
} catch (error) {
console.error('保存文件到历史记录失败:', error);
showNotification(`保存失败: ${error.message}`, 'error');
diff --git a/js/chatbot/core/chat-history-manager.js b/js/chatbot/core/chat-history-manager.js
index 773872c..0409cec 100644
--- a/js/chatbot/core/chat-history-manager.js
+++ b/js/chatbot/core/chat-history-manager.js
@@ -49,6 +49,13 @@
return;
}
+ // 检查 ID 是否是 UUID 格式(后端要求)
+ const isUUID = docId && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(docId);
+ if (!isUUID) {
+ console.warn('[saveSingleMessage] Non-UUID document ID, skipping backend save:', docId?.substring(0, 30));
+ return;
+ }
+
try {
await window.storageAdapter.saveChatMessage(docId, {
role: message.role,
@@ -69,11 +76,17 @@
try {
let history = [];
- if (isBackendMode()) {
- // 后端模式:从 API 加载
+ // 检查 ID 是否是 UUID 格式(后端要求)
+ const isUUID = docId && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(docId);
+
+ if (isBackendMode() && isUUID) {
+ // 后端模式:从 API 加载(只有 UUID 格式的 ID 才能调用后端)
history = await window.storageAdapter.loadChatHistory(docId);
} else {
- // 前端模式:localStorage
+ // 前端模式或非 UUID ID:使用 localStorage
+ if (!isUUID && isBackendMode()) {
+ console.warn('[loadChatHistory] Non-UUID document ID, using localStorage fallback:', docId?.substring(0, 30));
+ }
const raw = localStorage.getItem('chatHistory_' + docId);
if (raw) {
history = JSON.parse(raw);
diff --git a/js/chatbot/core/content-processor.js b/js/chatbot/core/content-processor.js
index 415af39..e482547 100644
--- a/js/chatbot/core/content-processor.js
+++ b/js/chatbot/core/content-processor.js
@@ -81,14 +81,19 @@ function splitContentSmart(content, maxChunk = 8192) {
}
/**
- * 生成当前文档的唯一 ID。
- * 该 ID 用于区分不同文档的聊天上下文或相关数据存储 (如思维导图数据)。
- * ID 的生成基于文档名称、图片数量、OCR 文本长度和翻译文本长度的组合,
- * 以期在实际使用中具有足够的唯一性。
+ * 获取当前文档的唯一 ID。
+ * 优先使用文档的 id 字段(后端返回的 UUID),
+ * 如果没有则基于文档名称、图片数量、OCR 文本长度和翻译文本长度生成唯一性 ID。
*
* @returns {string} 当前文档的唯一 ID。
*/
function getCurrentDocId() {
+ // 优先使用文档的 id 字段(后端 UUID)
+ if (window.data && window.data.id) {
+ return window.data.id;
+ }
+
+ // 降级:基于内容生成 ID(前端模式或旧数据)
const doc = getCurrentDocContent();
// 用文件名+图片数量+ocr长度+translation长度做唯一性(可根据实际情况调整)
return `${doc.name || 'unknown'}_${(doc.images||[]).length}_${(doc.ocr||'').length}_${(doc.translation||'').length}`;
diff --git a/js/history/history_detail_render.js b/js/history/history_detail_render.js
index 886a216..124e1ea 100644
--- a/js/history/history_detail_render.js
+++ b/js/history/history_detail_render.js
@@ -61,14 +61,16 @@ async function renderDetail() {
// 标签按钮(原始文件、OCR、仅翻译、分块对比、PDF对照)现在始终可见
// 仅保留数据检测用于默认标签选择
- const hasMinerUStructuredData =
- data.metadata &&
- data.metadata.originalPdfBase64 &&
- data.metadata.contentListJson &&
- data.metadata.translatedContentList &&
- data.metadata.supportsStructuredTranslation === true;
+ // 兼容嵌套的 metadata 结构
+ const meta = data?.metadata?.metadata || data?.metadata || {};
- const hasOriginalPdf = !!(data.metadata && data.metadata.originalPdfBase64);
+ const hasMinerUStructuredData =
+ meta.originalPdfBase64 &&
+ meta.contentListJson &&
+ meta.translatedContentList &&
+ meta.supportsStructuredTranslation === true;
+
+ const hasOriginalPdf = !!(meta.originalPdfBase64);
document.getElementById('fileName').textContent = data.name;
if (fileMetaTimeEl) {
diff --git a/js/history/history_detail_show_tab.js b/js/history/history_detail_show_tab.js
index 2932ff3..8b709f2 100644
--- a/js/history/history_detail_show_tab.js
+++ b/js/history/history_detail_show_tab.js
@@ -61,6 +61,9 @@ function showTab(tab) {
* 原 showTab 逻辑移至此处
*/
function showTabImmediate(tab) {
+ // 确保 data 引用 window.data(数据由 renderDetail 设置)
+ const data = window.data;
+
// 确保 DOM 缓存已初始化
DOM_CACHE.ensureInitialized();
@@ -159,10 +162,22 @@ function showTabImmediate(tab) {
let activeContentElement = null; // 用于 applyAnnotationsToContent
const significantTokenTypes = ['paragraph', 'heading', 'code', 'table', 'blockquote', 'list', 'html', 'hr'];
+ // 数据校验:确保 data 已加载
+ if (!data) {
+ console.error('[showTab] window.data is not loaded!');
+ document.getElementById('tabContent').innerHTML = `
+
`;
+ renderingTab = null;
+ return;
+ }
+
// ---- 增加日志 ----
// 日志现在可以准确反映 globalCurrentContentIdentifier
console.log(`[showTab - ${tab}] 即将渲染。当前 window.globalCurrentContentIdentifier:`, window.globalCurrentContentIdentifier);
- if (data && data.annotations) {
+ if (data.annotations) {
console.log(`[showTab - ${tab}] data.annotations (长度 ${data.annotations.length}):`, JSON.parse(JSON.stringify(data.annotations)));
} else {
console.log(`[showTab - ${tab}] data.annotations 不可用或为空。`);
@@ -201,8 +216,12 @@ function showTabImmediate(tab) {
// ========== 原始 PDF 查看器(iframe 嵌入官方 pdf.js viewer) ==========
if (DOM_CACHE.tabs.originalFile) DOM_CACHE.tabs.originalFile.classList.add('active');
- // 检查数据
- if (!data.metadata || !data.metadata.originalPdfBase64) {
+ // 检查数据(先检查 data 是否存在)
+ // 兼容嵌套的 metadata 结构:data.metadata.originalPdfBase64 或 data.metadata.metadata.originalPdfBase64
+ const pdfBase64 = data?.metadata?.originalPdfBase64
+ || data?.metadata?.metadata?.originalPdfBase64;
+
+ if (!data || !pdfBase64) {
document.getElementById('tabContent').innerHTML = `
@@ -235,14 +254,14 @@ function showTabImmediate(tab) {
(async () => {
try {
// base64 → Uint8Array
- const base64 = data.metadata.originalPdfBase64;
+ const base64 = pdfBase64;
const raw = atob(base64);
const bytes = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
- // 统一走后端暴露的 /pdfjs 静态资源,避免 Vite 在开发环境下
- // 将 PDF.js viewer 的脚本按模块脚本处理,导致 viewer.js 加载报错。
- const viewerBase = '/pdfjs/web/viewer.html';
+ // 统一走后端暴露的 /pdfjs 静态资源,避免 Vite 在开发环境下
+ // 将 PDF.js viewer 的脚本按模块脚本处理,导致 viewer.js 加载报错。
+ const viewerBase = '/pdfjs/web/viewer.html';
const iframe = document.getElementById('pdf-viewer-iframe');
const loading = document.getElementById('pdf-viewer-loading');
@@ -301,7 +320,9 @@ function showTabImmediate(tab) {
if (DOM_CACHE.tabs.pdfCompare) DOM_CACHE.tabs.pdfCompare.classList.add('active');
// 检查是否有必要的结构化翻译数据
- const hasStructuredData = data.metadata && data.metadata.originalPdfBase64 && data.metadata.contentListJson && data.metadata.translatedContentList;
+ // 兼容嵌套的 metadata 结构
+ const metaForCompare = data?.metadata?.metadata || data?.metadata || {};
+ const hasStructuredData = metaForCompare.originalPdfBase64 && metaForCompare.contentListJson && metaForCompare.translatedContentList;
if (!hasStructuredData) {
// 缺少结构化翻译数据,弹出确认对话框询问用户
@@ -335,10 +356,10 @@ function showTabImmediate(tab) {
console.log('[PDFCompareView] 开始初始化 PDF 对照视图');
await pdfCompareView.initialize(
- data.metadata.originalPdfBase64,
- data.metadata.contentListJson,
- data.metadata.translatedContentList,
- data.metadata.layoutJson // 传入 layoutJson
+ metaForCompare.originalPdfBase64,
+ metaForCompare.contentListJson,
+ metaForCompare.translatedContentList,
+ metaForCompare.layoutJson // 传入 layoutJson
);
// 为多轮检索生成chunks(如果还没有的话)
@@ -347,8 +368,8 @@ function showTabImmediate(tab) {
if (typeof generateChunksFromContentList === 'function') {
const chunks = generateChunksFromContentList(
- data.metadata.contentListJson,
- data.metadata.translatedContentList
+ metaForCompare.contentListJson,
+ metaForCompare.translatedContentList
);
window.data.ocrChunks = chunks.ocrChunks;
window.data.translatedChunks = chunks.translatedChunks;
diff --git a/js/process/main.js b/js/process/main.js
index c530a75..a35d8d9 100644
--- a/js/process/main.js
+++ b/js/process/main.js
@@ -370,7 +370,11 @@ const fileType = fileToProcess.name.split('.').pop().toLowerCase();
refRecord.metadata.failedStructuredItems = newFailed;
refRecord.metadata.structuredFailedCount = newFailed.length;
refRecord.time = new Date().toISOString();
- await saveResultToDB(refRecord);
+ if (window.storageAdapter && typeof window.storageAdapter.saveResultToDB === 'function') {
+ await window.storageAdapter.saveResultToDB(refRecord);
+ } else {
+ await saveResultToDB(refRecord);
+ }
if (typeof addProgressLog === "function") {
addProgressLog(`${logPrefix} 已将 ${translatedSubset.length} 个片段写回历史记录 ${refId},剩余失败 ${newFailed.length} 个`);
@@ -463,7 +467,11 @@ const fileType = fileToProcess.name.split('.').pop().toLowerCase();
});
refRecord.translation = (refRecord.translatedChunks || []).join('\n\n');
refRecord.time = new Date().toISOString();
- await saveResultToDB(refRecord);
+ if (window.storageAdapter && typeof window.storageAdapter.saveResultToDB === 'function') {
+ await window.storageAdapter.saveResultToDB(refRecord);
+ } else {
+ await saveResultToDB(refRecord);
+ }
if (typeof addProgressLog === "function") addProgressLog(`${logPrefix} 已将 ${translatedPieces.length} 个失败片段写回历史记录 ${refId}`);
// 准备返回对象并跳过后续的常规保存逻辑
@@ -1081,44 +1089,56 @@ const fileType = fileToProcess.name.split('.').pop().toLowerCase();
}
}
- if (typeof saveResultToDB === "function") {
- await saveResultToDB({
- id: `${fileToProcess.name}_${fileToProcess.size}`,
- name: fileToProcess.name,
- size: fileToProcess.size,
- time: processedAt,
- ocr: currentMarkdownContent,
- translation: currentTranslationContent,
- images: currentImagesData,
- ocrChunks: ocrChunks,
- translatedChunks: translatedChunks,
- fileType: fileType,
- targetLanguage: targetLanguageValue,
- relativePath: relativePath,
- sourceArchive: sourceArchive,
- originalContent: originalEncoding === 'text' ? originalContent : null,
- originalEncoding: originalEncoding,
- originalBinary: originalEncoding && originalEncoding !== 'text' && originalBinary ? arrayBufferToBase64(originalBinary) : null,
- originalExtension: originalExtension,
- // 新增:模型元信息(OCR/翻译)
- ocrEngine: usedOcrEngine || ocrEngineForLog || (typeof window !== 'undefined' ? (window.ocrSettingsManager?.getCurrentConfig()?.engine || null) : null),
- ocrSource: usedOcrSource || null,
- translationModelName: selectedTranslationModelName || 'none',
- translationModelCustomName: (selectedTranslationModelName === 'custom' && translationModelConfig && (translationModelConfig.displayName || translationModelConfig.name)) ? (translationModelConfig.displayName || translationModelConfig.name) : null,
- translationModelId: (selectedTranslationModelName === 'custom' && translationModelConfig && translationModelConfig.modelId) ? translationModelConfig.modelId : null,
- batchId: batchContext ? batchContext.id : null,
- batchOrder: batchContext ? batchContext.order : null,
- batchTotal: batchContext ? batchContext.total : null,
- batchTemplate: batchContext ? batchContext.template : null,
- batchFormats: batchContext ? batchContext.formats : null,
- batchStartedAt: batchContext ? batchContext.startedAt : null,
- batchOutputLanguage: batchContext ? batchContext.outputLanguage : null,
- batchOriginalIndex: batchContext ? batchContext.originalIndex : null,
- batchAttempt: batchContext ? batchContext.attempt : null,
- batchZip: batchContext ? batchContext.zipOutput : null,
- // 新增:MinerU 结构化翻译元数据
- metadata: Object.keys(metadataToSave).length > 0 ? metadataToSave : null
- });
+ // 保存文档记录
+ const documentData = {
+ id: `${fileToProcess.name}_${fileToProcess.size}`,
+ name: fileToProcess.name,
+ size: fileToProcess.size,
+ time: processedAt,
+ ocr: currentMarkdownContent,
+ translation: currentTranslationContent,
+ images: currentImagesData,
+ ocrChunks: ocrChunks,
+ translatedChunks: translatedChunks,
+ fileType: fileType,
+ targetLanguage: targetLanguageValue,
+ relativePath: relativePath,
+ sourceArchive: sourceArchive,
+ originalContent: originalEncoding === 'text' ? originalContent : null,
+ originalEncoding: originalEncoding,
+ originalBinary: originalEncoding && originalEncoding !== 'text' && originalBinary ? arrayBufferToBase64(originalBinary) : null,
+ originalExtension: originalExtension,
+ // 新增:模型元信息(OCR/翻译)
+ ocrEngine: usedOcrEngine || ocrEngineForLog || (typeof window !== 'undefined' ? (window.ocrSettingsManager?.getCurrentConfig()?.engine || null) : null),
+ ocrSource: usedOcrSource || null,
+ translationModelName: selectedTranslationModelName || 'none',
+ translationModelCustomName: (selectedTranslationModelName === 'custom' && translationModelConfig && (translationModelConfig.displayName || translationModelConfig.name)) ? (translationModelConfig.displayName || translationModelConfig.name) : null,
+ translationModelId: (selectedTranslationModelName === 'custom' && translationModelConfig && translationModelConfig.modelId) ? translationModelConfig.modelId : null,
+ batchId: batchContext ? batchContext.id : null,
+ batchOrder: batchContext ? batchContext.order : null,
+ batchTotal: batchContext ? batchContext.total : null,
+ batchTemplate: batchContext ? batchContext.template : null,
+ batchFormats: batchContext ? batchContext.formats : null,
+ batchStartedAt: batchContext ? batchContext.startedAt : null,
+ batchOutputLanguage: batchContext ? batchContext.outputLanguage : null,
+ batchOriginalIndex: batchContext ? batchContext.originalIndex : null,
+ batchAttempt: batchContext ? batchContext.attempt : null,
+ batchZip: batchContext ? batchContext.zipOutput : null,
+ // 新增:MinerU 结构化翻译元数据
+ metadata: Object.keys(metadataToSave).length > 0 ? metadataToSave : null
+ };
+
+ let savedDocId = documentData.id;
+ if (typeof window !== 'undefined' && window.storageAdapter && typeof window.storageAdapter.saveResultToDB === 'function') {
+ // 后端模式:使用 storageAdapter
+ const result = await window.storageAdapter.saveResultToDB(documentData);
+ if (result && result.id) {
+ savedDocId = result.id;
+ console.log(`${logPrefix} 后端返回的文档 UUID:`, savedDocId);
+ }
+ } else if (typeof saveResultToDB === "function") {
+ // 前端模式:使用本地 IndexedDB
+ await saveResultToDB(documentData);
}
if (typeof onFileSuccess === 'function') {
@@ -1126,6 +1146,7 @@ const fileType = fileToProcess.name.split('.').pop().toLowerCase();
}
return {
file: fileToProcess,
+ id: savedDocId, // 后端 UUID 或本地 ID
markdown: currentMarkdownContent,
translation: currentTranslationContent,
images: currentImagesData,
diff --git a/js/storage/storage-adapter.js b/js/storage/storage-adapter.js
index 3d4b8c3..c453b30 100644
--- a/js/storage/storage-adapter.js
+++ b/js/storage/storage-adapter.js
@@ -190,7 +190,26 @@ class BackendStorage {
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('saveResultToDB', document);
}
- await this.fetchAPI('/documents', { method: 'POST', body: JSON.stringify(document) });
+
+ // 判断是更新还是创建:UUID 格式的 ID 表示更新现有文档
+ const isUUID = document.id && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(document.id);
+
+ if (isUUID) {
+ // 更新现有文档
+ await this.fetchAPI(`/documents/${document.id}`, {
+ method: 'PUT',
+ body: JSON.stringify(document)
+ });
+ return document;
+ } else {
+ // 创建新文档(后端会生成 UUID)
+ const response = await this.fetchAPI('/documents', {
+ method: 'POST',
+ body: JSON.stringify(document)
+ });
+ // 返回后端生成的完整文档(包含 UUID)
+ return response;
+ }
} catch (error) {
console.error('Failed to save document to backend, falling back to local:', error);
return this._fallbackTo('saveResultToDB', document);
diff --git a/local-proxy/routes/chat.js b/local-proxy/routes/chat.js
index 96d4443..c640627 100644
--- a/local-proxy/routes/chat.js
+++ b/local-proxy/routes/chat.js
@@ -16,6 +16,16 @@ 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 {
@@ -24,7 +34,11 @@ router.get('/:documentId/history', async (req, res, next) => {
// 验证 UUID 格式
if (!isValidUUID(documentId)) {
- return res.status(400).json({ error: 'Invalid document ID format' });
+ 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.'
+ });
}
// 验证和规范化参数
diff --git a/local-proxy/routes/documents.js b/local-proxy/routes/documents.js
index 9fadf4c..7fac363 100644
--- a/local-proxy/routes/documents.js
+++ b/local-proxy/routes/documents.js
@@ -171,23 +171,24 @@ router.post('/', async (req, res, next) => {
'ocrProvider', 'ocrText', 'ocr', 'ocrMetadata', 'ocrEngine', 'ocrSource',
'translationModel', 'translatedText', 'translation', 'translationMetadata',
'translationModelName', 'summary', 'toc', 'processingTime', 'errorMessage',
- 'id', 'userId', 'createdAt', 'updatedAt'
+ 'id', 'userId', 'createdAt', 'updatedAt',
+ 'metadata' // 添加 metadata 到已知字段,避免嵌套保存
];
for (const [key, value] of Object.entries(body)) {
if (!knownFields.includes(key) && value !== undefined) {
metadataFields[key] = value;
}
}
- // 合并原有 metadata
+ // 合并原有 metadata 的内容(而不是保存 metadata 字段本身)
if (body.metadata && typeof body.metadata === 'object') {
Object.assign(metadataFields, body.metadata);
}
- schemaFields.metadata = Object.keys(metadataFields).length > 0 ? metadataFields : body.metadata;
+ schemaFields.metadata = Object.keys(metadataFields).length > 0 ? metadataFields : null;
- // 移除 undefined 字段
+ // 移除 undefined 字段,并排除前端传入的 id(使用数据库自动生成的 UUID)
const cleanData = {};
for (const [key, value] of Object.entries(schemaFields)) {
- if (value !== undefined) {
+ if (value !== undefined && key !== 'id') {
cleanData[key] = value;
}
}
@@ -216,13 +217,80 @@ router.post('/', async (req, res, next) => {
router.put('/:id', async (req, res, next) => {
try {
const { id } = req.params;
+ const body = { ...req.body };
+
+ // 字段映射:前端字段名 -> 数据库字段名
+ const fieldMapping = {
+ name: 'fileName',
+ size: 'fileSize',
+ ocr: 'ocrText',
+ translation: 'translatedText',
+ ocrEngine: 'ocrProvider',
+ translationModelName: 'translationModel'
+ };
+
+ // 应用字段映射
+ for (const [frontendField, dbField] of Object.entries(fieldMapping)) {
+ if (body[frontendField] !== undefined && body[dbField] === undefined) {
+ body[dbField] = body[frontendField];
+ }
+ }
+
+ // 提取已知字段,其余放入 metadata
+ const knownFields = [
+ 'fileName', 'fileSize', 'fileType', 'filePath', 'status',
+ 'ocrProvider', 'ocrText', 'ocrMetadata',
+ 'translationModel', 'translatedText', 'translationMetadata',
+ 'summary', 'toc', 'processingTime', 'errorMessage'
+ ];
+
+ const updateData = {};
+ const metadataUpdate = {};
+
+ for (const [key, value] of Object.entries(body)) {
+ if (key === 'id' || key === 'userId' || key === 'createdAt' || key === 'updatedAt') {
+ continue; // 跳过只读字段
+ }
+ if (knownFields.includes(key)) {
+ updateData[key] = value;
+ } else if (key === 'metadata') {
+ // 合并 metadata
+ metadataUpdate['metadata'] = value;
+ } else if (!key.startsWith('_')) {
+ // 其他字段放入 metadata(排除内部字段)
+ metadataUpdate[key] = value;
+ }
+ }
+
+ // 如果有需要放入 metadata 的字段,需要先读取现有 metadata 再合并
+ if (Object.keys(metadataUpdate).length > 0) {
+ const existingDoc = await prisma.document.findFirst({
+ where: { id, userId: req.user.id },
+ select: { metadata: true }
+ });
+
+ if (existingDoc) {
+ const existingMeta = existingDoc.metadata || {};
+ const newMeta = { ...existingMeta };
+
+ for (const [key, value] of Object.entries(metadataUpdate)) {
+ if (key === 'metadata' && typeof value === 'object') {
+ Object.assign(newMeta, value);
+ } else {
+ newMeta[key] = value;
+ }
+ }
+
+ updateData.metadata = newMeta;
+ }
+ }
await prisma.document.updateMany({
where: {
id,
userId: req.user.id
},
- data: req.body
+ data: updateData
});
res.json({ success: true });
diff --git a/vite.config.js b/vite.config.js
index c48181e..c31a9fd 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,4 +1,6 @@
import { resolve } from 'path';
+import { createReadStream, existsSync } from 'fs';
+import { join } from 'path';
// 可选的前端构建配置:不改变现有直出模式
// - 输入:根目录 index.html 与 admin/index.html
@@ -16,10 +18,6 @@ export default {
target: 'http://localhost:3456',
changeOrigin: true
},
- '/pdfjs': {
- target: 'http://localhost:3456',
- changeOrigin: true
- },
'/mineru': {
target: 'http://localhost:3456',
changeOrigin: true
@@ -34,6 +32,40 @@ export default {
optimizeDeps: {
exclude: ['pdfjs-dist']
},
+ // 开发服务器中间件:服务 pdfjs 静态文件
+ plugins: [{
+ name: 'serve-pdfjs',
+ configureServer(server) {
+ server.middlewares.use((req, res, next) => {
+ if (req.url && req.url.startsWith('/pdfjs/')) {
+ const filePath = join(process.cwd(), req.url);
+ if (existsSync(filePath)) {
+ // 设置正确的 MIME 类型
+ const ext = filePath.split('.').pop()?.toLowerCase();
+ const mimeTypes = {
+ 'html': 'text/html',
+ 'js': 'application/javascript',
+ 'css': 'text/css',
+ 'json': 'application/json',
+ 'png': 'image/png',
+ 'jpg': 'image/jpeg',
+ 'gif': 'image/gif',
+ 'svg': 'image/svg+xml',
+ 'woff': 'font/woff',
+ 'woff2': 'font/woff2',
+ 'ttf': 'font/ttf',
+ 'pdf': 'application/pdf'
+ };
+ const contentType = mimeTypes[ext] || 'application/octet-stream';
+ res.setHeader('Content-Type', contentType);
+ createReadStream(filePath).pipe(res);
+ return;
+ }
+ }
+ next();
+ });
+ }
+ }],
build: {
outDir: 'dist',
emptyOutDir: true,