diff --git a/css/history_detail/02-layout/layout.css b/css/history_detail/02-layout/layout.css index 55f8439..6554686 100644 --- a/css/history_detail/02-layout/layout.css +++ b/css/history_detail/02-layout/layout.css @@ -15,6 +15,8 @@ body { max-width: 1200px; margin: 40px auto; height: 100vh; + display: flex; + flex-direction: column; background: var(--color-bg-base); border-radius: var(--radius-xl); /* 使用变量定义的轻量化阴影 */ @@ -87,8 +89,8 @@ body { .tab-content { background: transparent; padding: 8px 0; - height: 100vh; min-height: 300px; + flex: 1; margin-top: 0; /* 优化阅读体验的字体设置 */ font-size: 1.0625rem; /* 17px */ diff --git a/index.html b/index.html index 9998a36..f93f766 100644 --- a/index.html +++ b/index.html @@ -231,7 +231,7 @@ pointer-events: none; } /* 新增:右侧大 Logo 背景装饰 */ - .workspace-header::after { + /* .workspace-header::after { content: ''; position: absolute; right: -20px; @@ -242,10 +242,11 @@ background-repeat: no-repeat; background-position: center; background-size: contain; - opacity: 0.07; /* 极低透明度,仅作纹理 */ + 极低透明度,仅作纹理 + opacity: 0.07; transform: rotate(-10deg); pointer-events: none; - } + } */ .workspace-header-content { position: relative; z-index: 1; @@ -594,7 +595,7 @@
-
+
OCR 文档解析 -
+
diff --git a/input/English PDF Test.pdf b/input/English PDF Test.pdf new file mode 100644 index 0000000..d1544ba Binary files /dev/null and b/input/English PDF Test.pdf differ diff --git a/input/pdf测试.pdf b/input/pdf测试.pdf deleted file mode 100644 index 5856aac..0000000 Binary files a/input/pdf测试.pdf and /dev/null differ diff --git a/js/history/history_detail_scripts.js b/js/history/history_detail_scripts.js index 18bcd01..d55b086 100644 --- a/js/history/history_detail_scripts.js +++ b/js/history/history_detail_scripts.js @@ -535,6 +535,32 @@ async function triggerReprocess(includeTranslation) { } } +/** + * 显示 Tab 按钮加载状态 + * @param {string} tabId - Tab 按钮 ID + * @param {boolean} loading - 是否显示加载状态 + */ +function setTabLoadingState(tabId, loading) { + const btn = document.getElementById(tabId); + if (!btn) return; + + if (loading) { + // 保存原始内容 + btn.dataset.originalContent = btn.innerHTML; + // 显示加载动画 + btn.innerHTML = `
`; + btn.disabled = true; + btn.style.minWidth = '60px'; + } else { + // 恢复原始内容 + if (btn.dataset.originalContent) { + btn.innerHTML = btn.dataset.originalContent; + } + btn.disabled = false; + btn.style.minWidth = ''; + } +} + /** * 处理"Word文档"标签点击 * 如果没有OCR数据,询问用户是否需要生成 @@ -561,7 +587,14 @@ async function handleOcrTabClick() { ); if (confirmed) { - await triggerReprocess(false); // 仅OCR,不翻译 + // 显示加载状态 + setTabLoadingState('tab-ocr', true); + try { + await triggerReprocess(false); // 仅OCR,不翻译 + } finally { + // 恢复按钮状态 + setTabLoadingState('tab-ocr', false); + } } } @@ -591,7 +624,396 @@ async function handleTranslationTabClick() { ); if (confirmed) { - await triggerReprocess(true); // OCR + 翻译 + // 显示加载状态 + setTabLoadingState('tab-translation', true); + try { + await triggerReprocess(true); // OCR + 翻译 + } finally { + // 恢复按钮状态 + setTabLoadingState('tab-translation', false); + } + } +} + +/** + * 显示 PDF 对照确认对话框 + * 询问用户是否进行 MinerU 结构化翻译 + */ +async function showPdfCompareConfirmDialog() { + // 检查是否有 MinerU 数据(contentListJson) + const hasMinerUData = window.data && window.data.metadata && window.data.metadata.contentListJson; + + if (!hasMinerUData) { + // 没有 MinerU 数据,询问是否重新使用 MinerU 处理 + const confirmed = await showConfirmDialog( + 'PDF 对照视图', + '当前文档未使用 MinerU 引擎处理,无法进行结构化翻译。\n\n是否重新使用 MinerU 引擎处理文档?\n\n处理完成后将自动进行翻译并显示 PDF 对照视图。', + '开始处理', + '取消' + ); + + if (confirmed) { + // 使用 MinerU 重新处理并翻译 + await triggerReprocessWithMinerU(); + } else { + // 用户取消,跳转回原始文件标签页 + if (typeof showTab === 'function') { + showTab('original-file'); + } + } + return; + } + + // 有 MinerU 数据,询问是否进行翻译 + const confirmed = await showConfirmDialog( + 'PDF 对照视图', + '当前文档尚未进行 MinerU 结构化翻译。是否现在开始翻译?\n\n翻译完成后将显示原文与译文的 PDF 对照视图。', + '开始翻译', + '取消' + ); + + if (confirmed) { + await executeMinerUStructuredTranslation(); + } else { + // 用户取消,跳转回原始文件标签页 + if (typeof showTab === 'function') { + showTab('original-file'); + } + } +} + +/** + * 使用 MinerU 引擎重新处理文档并翻译 + */ +async function triggerReprocessWithMinerU() { + const docId = window.docIdForLocalStorage; + const docName = window.data ? window.data.name : '未知文档'; + + if (!docId) { + showToast('无法获取文档ID,请刷新页面重试。', 'error'); + return; + } + + // 检查是否有原始 PDF 数据 + const pdfBase64 = window.data?.metadata?.originalPdfBase64; + if (!pdfBase64) { + showToast('当前记录没有保存原始PDF数据,无法重新处理。', 'error'); + return; + } + + // 显示加载状态 + setTabLoadingState('tab-pdf-compare', true); + + // 保存当前 OCR 配置 + const savedEngine = localStorage.getItem('ocrEngine'); + const savedTranslationMode = localStorage.getItem('mineruTranslationMode'); + const savedMineruMode = localStorage.getItem('mineruMode'); + + try { + showToast('正在使用 MinerU 处理文档...', 'info'); + + // 临时设置 OCR 配置为 MinerU + 结构化翻译模式 + localStorage.setItem('ocrEngine', 'mineru'); + localStorage.setItem('mineruTranslationMode', 'structured'); + localStorage.setItem('mineruMode', 'txt'); + + // 将 PDF 转换为 Blob + const pdfBytes = Uint8Array.from(atob(pdfBase64), c => c.charCodeAt(0)); + const pdfBlob = new Blob([pdfBytes], { type: 'application/pdf' }); + const pdfFile = new File([pdfBlob], docName || 'document.pdf', { type: 'application/pdf' }); + + // 获取翻译模型配置 - 使用后端代理,不需要前端选择模型 + const settings = typeof loadSettings === 'function' ? loadSettings() : {}; + + // 调用 OCR 处理 + if (typeof window.processSinglePdf !== 'function') { + showToast('处理模块未加载,请刷新页面重试。', 'error'); + return; + } + + // 使用后端代理进行翻译,不需要传递 API Key + const result = await window.processSinglePdf( + pdfFile, + null, // mistralKeyObject - 使用 MinerU 不需要 + null, // translationKeyObject - 使用后端代理不需要 + 'tongyi', // 使用通义模型,后端代理会处理 + null, // translationModelConfig + settings.maxTokensPerChunk || 2000, + settings.targetLanguage || 'Chinese', + () => Promise.resolve(), // acquireSlot + () => {}, // releaseSlot + settings.defaultSystemPrompt || '', + settings.defaultUserPromptTemplate || '', + settings.useCustomPrompts || false, + null, // batchContext + () => {} // onFileSuccess + ); + + if (result.error) { + throw new Error(result.error); + } + + console.log('[triggerReprocessWithMinerU] 处理结果:', result); + console.log('[triggerReprocessWithMinerU] metadata:', result.metadata); + console.log('[triggerReprocessWithMinerU] contentListJson:', result.metadata?.contentListJson); + + // 更新 window.data + if (result.ocr) window.data.ocr = result.ocr; + if (result.translation) window.data.translation = result.translation; + if (result.images) window.data.images = result.images; + if (result.ocrChunks) window.data.ocrChunks = result.ocrChunks; + if (result.translatedChunks) window.data.translatedChunks = result.translatedChunks; + if (result.metadata) { + window.data.metadata = window.data.metadata || {}; + Object.assign(window.data.metadata, result.metadata); + } + + // 保存到 IndexedDB + if (typeof saveResultToDB === 'function') { + await saveResultToDB(window.data); + } + + showToast('处理完成!', 'success'); + + // 刷新页面显示 + if (typeof renderDetail === 'function') { + renderDetail(); + } + + // 自动跳转到原始文件标签页 + if (typeof showTab === 'function') { + showTab('original-file'); + } + + } catch (error) { + console.error('[triggerReprocessWithMinerU] 处理失败:', error); + showToast(`处理失败: ${error.message}`, 'error'); + } finally { + // 恢复原始 OCR 配置 + if (savedEngine !== null) localStorage.setItem('ocrEngine', savedEngine); + else localStorage.removeItem('ocrEngine'); + if (savedTranslationMode !== null) localStorage.setItem('mineruTranslationMode', savedTranslationMode); + else localStorage.removeItem('mineruTranslationMode'); + if (savedMineruMode !== null) localStorage.setItem('mineruMode', savedMineruMode); + else localStorage.removeItem('mineruMode'); + + setTabLoadingState('tab-pdf-compare', false); + } +} + +/** + * 执行 MinerU 结构化翻译 + * 通过后端 local-proxy 代理调用 LLM API,API Key 由后端管理 + */ +async function executeMinerUStructuredTranslation() { + const logPrefix = '[MinerU结构化翻译]'; + const PROXY_BASE = 'http://localhost:3456'; + + // 获取翻译配置 + const settings = typeof loadSettings === 'function' ? loadSettings() : {}; + const modelName = "tongyi"; + + + // 显示进度容器 + const tabContent = document.getElementById('tabContent'); + tabContent.innerHTML = ` +
+
+
+

正在执行 MinerU 结构化翻译...

+
+
+
准备开始翻译...
+
+
+
+
+
+

进度: 0%

+
+
+ + `; + + const logEl = document.getElementById('structured-translation-log'); + const progressBar = document.getElementById('structured-translation-progress-bar'); + const statusEl = document.getElementById('structured-translation-status'); + + // 日志函数 + const addLog = (msg) => { + const entry = document.createElement('div'); + entry.className = 'log-entry'; + entry.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`; + logEl.appendChild(entry); + logEl.scrollTop = logEl.scrollHeight; + console.log(`${logPrefix} ${msg}`); + }; + + // 进度回调 + const onProgress = (progress) => { + const pct = progress.percentage || 0; + progressBar.style.width = `${pct}%`; + statusEl.textContent = `进度: ${pct}% - ${progress.message || ''}`; + }; + + try { + addLog('初始化翻译器...'); + + // 检查 MinerUStructuredTranslation 是否可用 + if (typeof MinerUStructuredTranslation === 'undefined') { + throw new Error('MinerU 结构化翻译模块未加载'); + } + + const translator = new MinerUStructuredTranslation(); + addLog('翻译器初始化完成'); + + // 获取全局 data 对象 + const dataObj = window.data; + console.log('dataObj:', dataObj); + + if (!dataObj || !dataObj.metadata || !dataObj.metadata.contentListJson) { + console.log(!dataObj ,!dataObj.metadata , !dataObj.metadata.contentListJson); + + throw new Error('缺少必要的内容数据'); + } + + // 提取可翻译内容 + const contentListJson = dataObj.metadata.contentListJson; + addLog(`提取可翻译内容...`); + + const translatableContent = translator.extractTranslatableContent(contentListJson); + addLog(`提取了 ${translatableContent.length} 个片段`); + + // 分批 + const batches = translator.splitIntoBatches(translatableContent); + addLog(`分为 ${batches.length} 个批次`); + + // 获取目标语言 + const targetLang = settings.targetLanguage === 'custom' + ? (settings.customTargetLanguageName || 'Chinese') + : (settings.targetLanguage || 'Chinese'); + + // 翻译选项 - 通过后端代理,不需要 API Key + const translationOptions = { + useBackendProxy: true, + proxyBase: PROXY_BASE, + provider: modelName === 'custom' ? 'aliyun' : modelName // 默认使用 aliyun/通义 + }; + + // 执行翻译 + addLog(`开始翻译 (模型: ${modelName}, 目标语言: ${targetLang}, 通过后端代理)...`); + + const translatedContentList = await translator.translateBatches( + batches, + targetLang, + modelName, + null, // API Key 为 null,由后端代理处理 + { + ...translationOptions, + maxRetries: settings.structuredMaxRetries || 2, + retryDelay: settings.structuredRetryDelayMs || 800 + }, + onProgress, + () => Promise.resolve(), // acquireSlot + () => {} // releaseSlot + ); + + addLog('翻译完成,保存数据...'); + + // 保存到 metadata + if (!dataObj.metadata) dataObj.metadata = {}; + dataObj.metadata.translatedContentList = translatedContentList; + dataObj.metadata.supportsStructuredTranslation = true; + + // 收集失败项 + const failedItems = []; + translatedContentList.forEach((it, idx) => { + if (it && it.failed === true) { + failedItems.push({ + index: idx, + type: it.type, + page_idx: it.page_idx || 0, + text: translator.extractItemText ? translator.extractItemText(it) : (it.text || '') + }); + } + }); + dataObj.metadata.failedStructuredItems = failedItems; + dataObj.metadata.structuredFailedCount = failedItems.length; + + // 更新全局数据 + if (typeof data !== 'undefined') { + data.metadata = dataObj.metadata; + } + window.data = dataObj; + + // 保存到数据库 + if (typeof saveResultToDB === 'function') { + await saveResultToDB(dataObj); + addLog('数据已保存到数据库'); + } + + if (failedItems.length > 0) { + addLog(`注意: 有 ${failedItems.length} 个片段翻译失败`); + } + + addLog('正在加载 PDF 对照视图...'); + + // 短暂延迟后显示 PDF 对照视图 + setTimeout(() => { + if (typeof showTabImmediate === 'function') { + showTabImmediate('pdf-compare'); + } else if (typeof showTab === 'function') { + showTab('pdf-compare'); + } + }, 500); + + } catch (error) { + console.error(`${logPrefix} 翻译失败:`, error); + addLog(`错误: ${error.message}`); + + // 显示错误 + tabContent.innerHTML = ` +
+ +

翻译失败

+

${error.message}

+ +
+ `; + + if (typeof renderingTab !== 'undefined') renderingTab = null; + if (typeof console.timeEnd === 'function') console.timeEnd('[性能] showTab_总渲染'); } } diff --git a/js/history/history_detail_show_tab.js b/js/history/history_detail_show_tab.js index 1e2518d..21ec246 100644 --- a/js/history/history_detail_show_tab.js +++ b/js/history/history_detail_show_tab.js @@ -300,15 +300,14 @@ function showTabImmediate(tab) { if (DOM_CACHE.layout.meta) DOM_CACHE.layout.meta.style.display = 'none'; if (DOM_CACHE.layout.tabsContainer) DOM_CACHE.layout.tabsContainer.style.display = 'none'; - // 验证必要数据 - if (!data.metadata || !data.metadata.originalPdfBase64 || !data.metadata.contentListJson || !data.metadata.translatedContentList) { - const warn = `
` - + `无法进入"PDF对照":缺少必要的 MinerU 结构化翻译数据。` - + `
`; - document.getElementById('tabContent').innerHTML = warn; - if (typeof window.refreshTocList === 'function') window.refreshTocList(); - renderingTab = null; - console.timeEnd && console.timeEnd('[性能] showTab_总渲染'); + // 检查是否有必要的结构化翻译数据 + const hasStructuredData = data.metadata && data.metadata.originalPdfBase64 && data.metadata.contentListJson && data.metadata.translatedContentList; + + if (!hasStructuredData) { + // 缺少结构化翻译数据,弹出确认对话框询问用户 + (async () => { + await showPdfCompareConfirmDialog(); + })(); return; } diff --git a/js/process/main.js b/js/process/main.js index e553259..6703cd3 100644 --- a/js/process/main.js +++ b/js/process/main.js @@ -704,13 +704,23 @@ const fileType = fileToProcess.name.split('.').pop().toLowerCase(); // --- 翻译流程 (如果需要) --- if (selectedTranslationModelName !== 'none') { const translationKeyValue = translationKeyObject ? translationKeyObject.value : null; - if (!translationKeyValue) { + + // 检查是否使用后端代理模式(不需要前端 API Key) + const useBackendProxy = selectedTranslationModelName === 'tongyi' || selectedTranslationModelName === 'aliyun'; + + if (!translationKeyValue && !useBackendProxy) { if (typeof addProgressLog === "function") addProgressLog(`${logPrefix} 警告: 需要翻译但未提供有效的翻译 API Key。跳过翻译。`); currentTranslationContent = '[未翻译:缺少API Key]'; ocrChunks = [currentMarkdownContent]; translatedChunks = [currentTranslationContent]; } else { - if (typeof addProgressLog === "function") addProgressLog(`${logPrefix} 开始翻译 (${selectedTranslationModelName}, Key: ...${translationKeyValue.slice(-4)})`); + if (typeof addProgressLog === "function") { + if (useBackendProxy) { + addProgressLog(`${logPrefix} 开始翻译 (${selectedTranslationModelName}, 使用后端代理)`); + } else { + addProgressLog(`${logPrefix} 开始翻译 (${selectedTranslationModelName}, Key: ...${translationKeyValue.slice(-4)})`); + } + } // ===== MinerU 结构化翻译检测 ===== let shouldUseStructuredTranslation = false; @@ -776,6 +786,9 @@ const fileType = fileToProcess.name.split('.').pop().toLowerCase(); }); // 4. 执行批量翻译 + // 检查是否使用后端代理模式 + const useBackendProxy = !translationKeyValue && (selectedTranslationModelName === 'tongyi' || selectedTranslationModelName === 'aliyun'); + const translatedContentList = await structuredTranslator.translateBatches( batches, targetLanguageValue, @@ -783,6 +796,9 @@ const fileType = fileToProcess.name.split('.').pop().toLowerCase(); translationKeyValue, { ...translationOptions, + useBackendProxy, + provider: selectedTranslationModelName, + proxyBase: 'http://localhost:3456', // 允许从设置自定义重试,若无则用默认 maxRetries: (typeof loadSettings === 'function' ? (loadSettings().structuredMaxRetries || undefined) : undefined), retryDelay: (typeof loadSettings === 'function' ? (loadSettings().structuredRetryDelayMs || undefined) : undefined) @@ -1009,60 +1025,61 @@ const fileType = fileToProcess.name.split('.').pop().toLowerCase(); } const processedAt = new Date().toISOString(); - if (typeof saveResultToDB === "function") { - // 准备元数据 - const metadataToSave = {}; - // 如果是 MinerU 结构化翻译,保存额外的元数据 - if (ocrResult && ocrResult.metadata) { - // 保存 layoutJson 和 contentListJson - if (ocrResult.metadata.layoutJson) { - metadataToSave.layoutJson = ocrResult.metadata.layoutJson; - } - if (ocrResult.metadata.contentListJson) { - metadataToSave.contentListJson = ocrResult.metadata.contentListJson; - } - // 保存翻译后的结构化内容 - if (ocrResult.metadata.translatedContentList) { - metadataToSave.translatedContentList = ocrResult.metadata.translatedContentList; - } - // 保存原始 PDF(转为 base64) - if (ocrResult.metadata.originalPdf) { - try { - const pdfBlob = ocrResult.metadata.originalPdf; - const pdfArrayBuffer = await pdfBlob.arrayBuffer(); - metadataToSave.originalPdfBase64 = arrayBufferToBase64(pdfArrayBuffer); - if (typeof addProgressLog === "function") { - addProgressLog(`${logPrefix} 已保存原始 PDF (${Math.round(pdfBlob.size / 1024)} KB)`); - } - } catch (e) { - console.warn(`${logPrefix} 保存原始 PDF 失败:`, e); - } - } - // 标记支持结构化翻译 - metadataToSave.supportsStructuredTranslation = ocrResult.metadata.supportsStructuredTranslation; - // 持久化结构化失败项统计(如存在) - if (Array.isArray(ocrResult.metadata.failedStructuredItems)) { - metadataToSave.failedStructuredItems = ocrResult.metadata.failedStructuredItems; - } - if (typeof ocrResult.metadata.structuredFailedCount === 'number') { - metadataToSave.structuredFailedCount = ocrResult.metadata.structuredFailedCount; - } + // 准备元数据(在条件块外定义,以便返回值使用) + const metadataToSave = {}; + + // 如果是 MinerU 结构化翻译,保存额外的元数据 + if (ocrResult && ocrResult.metadata) { + // 保存 layoutJson 和 contentListJson + if (ocrResult.metadata.layoutJson) { + metadataToSave.layoutJson = ocrResult.metadata.layoutJson; } - - // 新增:对于所有 PDF 文件,如果还没有 originalPdfBase64,则从原始文件读取 - if (fileType === 'pdf' && !metadataToSave.originalPdfBase64) { + if (ocrResult.metadata.contentListJson) { + metadataToSave.contentListJson = ocrResult.metadata.contentListJson; + } + // 保存翻译后的结构化内容 + if (ocrResult.metadata.translatedContentList) { + metadataToSave.translatedContentList = ocrResult.metadata.translatedContentList; + } + // 保存原始 PDF(转为 base64) + if (ocrResult.metadata.originalPdf) { try { - const pdfArrayBuffer = await fileToProcess.arrayBuffer(); + const pdfBlob = ocrResult.metadata.originalPdf; + const pdfArrayBuffer = await pdfBlob.arrayBuffer(); metadataToSave.originalPdfBase64 = arrayBufferToBase64(pdfArrayBuffer); if (typeof addProgressLog === "function") { - addProgressLog(`${logPrefix} 已保存原始 PDF 用于查看 (${Math.round(fileToProcess.size / 1024)} KB)`); + addProgressLog(`${logPrefix} 已保存原始 PDF (${Math.round(pdfBlob.size / 1024)} KB)`); } } catch (e) { console.warn(`${logPrefix} 保存原始 PDF 失败:`, e); } } + // 标记支持结构化翻译 + metadataToSave.supportsStructuredTranslation = ocrResult.metadata.supportsStructuredTranslation; + // 持久化结构化失败项统计(如存在) + if (Array.isArray(ocrResult.metadata.failedStructuredItems)) { + metadataToSave.failedStructuredItems = ocrResult.metadata.failedStructuredItems; + } + if (typeof ocrResult.metadata.structuredFailedCount === 'number') { + metadataToSave.structuredFailedCount = ocrResult.metadata.structuredFailedCount; + } + } + // 新增:对于所有 PDF 文件,如果还没有 originalPdfBase64,则从原始文件读取 + if (fileType === 'pdf' && !metadataToSave.originalPdfBase64) { + try { + const pdfArrayBuffer = await fileToProcess.arrayBuffer(); + metadataToSave.originalPdfBase64 = arrayBufferToBase64(pdfArrayBuffer); + if (typeof addProgressLog === "function") { + addProgressLog(`${logPrefix} 已保存原始 PDF 用于查看 (${Math.round(fileToProcess.size / 1024)} KB)`); + } + } catch (e) { + console.warn(`${logPrefix} 保存原始 PDF 失败:`, e); + } + } + + if (typeof saveResultToDB === "function") { await saveResultToDB({ id: `${fileToProcess.name}_${fileToProcess.size}`, name: fileToProcess.name, @@ -1137,7 +1154,9 @@ const fileType = fileToProcess.name.split('.').pop().toLowerCase(); batchOutputLanguage: batchContext ? batchContext.outputLanguage : null, batchOriginalIndex: batchContext ? batchContext.originalIndex : null, batchAttempt: batchContext ? batchContext.attempt : null, - batchZip: batchContext ? batchContext.zipOutput : null + batchZip: batchContext ? batchContext.zipOutput : null, + // 返回 metadata,包含 contentListJson 等结构化翻译数据 + metadata: Object.keys(metadataToSave).length > 0 ? metadataToSave : null }; @@ -1186,3 +1205,9 @@ if (typeof processModule !== 'undefined') { } else { console.warn('main.js: processModule is undefined at the point of assignment.'); } + +// 也暴露到 window 上,以便在 history_detail.html 等页面使用 +if (typeof window !== 'undefined') { + window.processSinglePdf = processSinglePdf; + console.log('main.js: processSinglePdf exposed to window'); +} diff --git a/js/process/mineru-structured-translation.js b/js/process/mineru-structured-translation.js index 4eb7053..b5d9e41 100644 --- a/js/process/mineru-structured-translation.js +++ b/js/process/mineru-structured-translation.js @@ -701,7 +701,88 @@ ${jsonContent} * @returns {Promise} */ async callTranslationAPI(systemPrompt, userPrompt, model, apiKey, options = {}) { - // 复用 translation.js 的配置构建逻辑 + const settings = typeof loadSettings === 'function' ? loadSettings() : {}; + const temperature = (settings.customModelSettings && settings.customModelSettings.temperature) || 0.5; + const maxTokens = (settings.customModelSettings && settings.customModelSettings.max_tokens) || 8000; + + console.log('[MinerU Structured] callTranslationAPI 调用参数:', { + model, + hasApiKey: !!apiKey, + useBackendProxy: options.useBackendProxy, + options + }); + + // 后端代理模式 - API Key 由后端管理 + if (options.useBackendProxy) { + const proxyBase = options.proxyBase || 'http://localhost:3456'; + const provider = options.provider || 'aliyun'; + + // 后端代理端点映射 + const providerEndpoints = { + 'aliyun': `${proxyBase}/api/llm/aliyun/v1/chat/completions`, + 'tongyi': `${proxyBase}/api/llm/tongyi/v1/chat/completions`, + 'deepseek': `${proxyBase}/api/llm/deepseek/v1/chat/completions`, + 'openai': `${proxyBase}/api/llm/openai/v1/chat/completions`, + 'mistral': `${proxyBase}/api/llm/mistral/v1/chat/completions`, + 'zhipu': `${proxyBase}/api/llm/zhipu/v4/chat/completions`, + 'anthropic': `${proxyBase}/api/llm/anthropic/v1/messages`, + 'gemini': `${proxyBase}/api/llm/gemini/v1beta/models/gemini-pro:generateContent` + }; + + const endpoint = providerEndpoints[provider] || providerEndpoints['aliyun']; + + // 获取模型 ID + let modelId = 'qwen-turbo-latest'; + try { + const modelConfig = typeof loadModelConfig === 'function' ? loadModelConfig(provider) : null; + if (modelConfig && (modelConfig.preferredModelId || modelConfig.modelId)) { + modelId = modelConfig.preferredModelId || modelConfig.modelId; + } + } catch (e) { + console.warn('[MinerU Structured] 加载模型配置失败,使用默认模型:', e); + } + + const requestBody = { + model: modelId, + messages: [ + { role: "system", content: systemPrompt }, + { role: "user", content: userPrompt } + ], + temperature, + max_tokens: maxTokens + }; + + console.log('[MinerU Structured] 后端代理模式:', { endpoint, provider, modelId }); + + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestBody) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`翻译 API 请求失败 (${response.status}): ${errorText}`); + } + + const data = await response.json(); + const result = data?.choices?.[0]?.message?.content; + + if (!result) { + throw new Error('翻译 API 返回空结果'); + } + + // 清理指令块 + return this._stripInstructionBlocks(result); + + } catch (error) { + console.error('[MinerU Structured] 后端代理请求失败:', error); + throw error; + } + } + + // 原有逻辑 - 前端直接调用(需要 API Key) if (typeof processModule === 'undefined' || typeof processModule.buildPredefinedApiConfig !== 'function' || typeof processModule.buildCustomApiConfig !== 'function') { @@ -712,14 +793,6 @@ ${jsonContent} throw new Error('callTranslationApi 函数不可用'); } - console.log('[MinerU Structured] callTranslationAPI 调用参数:', { - model, - hasApiKey: !!apiKey, - options, - hasModelConfig: !!(options && options.modelConfig), - modelConfig: options ? options.modelConfig : null - }); - // 构建 API 配置 let apiConfig; if (model === 'custom') { @@ -756,10 +829,6 @@ ${jsonContent} ); } else { // 预设模型 - 从 translation.js 获取配置 - const settings = typeof loadSettings === 'function' ? loadSettings() : {}; - const temperature = (settings.customModelSettings && settings.customModelSettings.temperature) || 0.5; - const maxTokens = (settings.customModelSettings && settings.customModelSettings.max_tokens) || 8000; - // 简化:仅支持常用模型 // 前端发出的请求源头 const predefinedConfigs = { @@ -811,17 +880,25 @@ ${jsonContent} let result = await callTranslationApi(apiConfig, requestBody); // 清理指令块(防止系统提示词泄露到翻译结果中) + return this._stripInstructionBlocks(result); + } + + /** + * 清理指令块 + * @param {string} result + * @returns {string} + */ + _stripInstructionBlocks(result) { if (typeof stripInstructionBlocks === 'function') { - result = stripInstructionBlocks(result); + return stripInstructionBlocks(result); } else if (typeof processModule !== 'undefined' && typeof processModule.stripInstructionBlocks === 'function') { - result = processModule.stripInstructionBlocks(result); + return processModule.stripInstructionBlocks(result); } else { // 回退:手动清理 if (typeof result === 'string') { - result = result.replace(/\s*\[\[PBX_INSTR_START\]\][\s\S]*?\[\[PBX_INSTR_END\]\]\s*/gi, '').trim(); + return result.replace(/\s*\[\[PBX_INSTR_START\]\][\s\S]*?\[\[PBX_INSTR_END\]\]\s*/gi, '').trim(); } } - return result; } diff --git a/test.html b/test.html index a733dc4..a320e55 100644 --- a/test.html +++ b/test.html @@ -596,7 +596,7 @@ class="btn-mineru-primary" onclick="mineruOpenHistory()" > - 📂 读取并打开历史界面 + 📂 读取并打开历史界面(仅跳转,不会OCR)我平常使用这个按钮测试这个功能