From e7b6c360c12d899da8b36d169677333bd65d95a8 Mon Sep 17 00:00:00 2001 From: MT-Fire <798521692@qq.com> Date: Tue, 10 Mar 2026 10:40:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=A0=87=E5=87=86=E7=BF=BB=E8=AF=91?= =?UTF-8?q?=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/history/history_detail_scripts.js | 144 +++++++++++++++++---------- js/process/translation.js | 2 +- 2 files changed, 90 insertions(+), 56 deletions(-) diff --git a/js/history/history_detail_scripts.js b/js/history/history_detail_scripts.js index 3d70539..18bcd01 100644 --- a/js/history/history_detail_scripts.js +++ b/js/history/history_detail_scripts.js @@ -192,54 +192,26 @@ async function performOcr(file, onProgress) { * @returns {Promise} 翻译后的文本 */ async function performTranslation(markdown, onProgress) { - // 获取翻译设置 - const settings = typeof loadSettings === 'function' ? loadSettings() : {}; + // 目标语言固定为中文 + const targetLangName = '中文'; - const targetLang = settings.targetLanguage || 'zh-CN'; - const targetLangName = targetLang === 'custom' - ? (settings.customTargetLanguageName || '中文') - : { 'zh-CN': '中文', 'en': 'English', 'ja': '日本語', 'ko': '한국어' }[targetLang] || targetLang; + // 翻译提示词(不依赖 getBuiltInPrompts) + const systemPrompt = '你是一个专业的文档翻译助手,擅长将文本精确翻译为简体中文,同时保留原始的 Markdown 格式。'; + const userPromptTemplate = '请将以下英文内容翻译成简体中文。请保持原有的 Markdown 格式和结构。只返回翻译后的内容,不要添加任何解释或说明。\n\n${content}'; - const selectedModel = settings.selectedTranslationModel || 'mistral'; - - // 获取 API Key - let apiKey = ''; - let modelConfig = null; - - if (selectedModel === 'custom') { - // 自定义模型配置 - modelConfig = settings.translationModelConfig || settings.customModelConfig || null; - if (!modelConfig) { - throw new Error('请先配置自定义翻译模型'); - } - } else { - // 预设模型 - 从 Key 管理器获取 API Key - if (typeof loadModelKeys === 'function') { - const keys = loadModelKeys(selectedModel); - const validKey = keys.find(k => k && k.value && (k.status === 'valid' || k.status === 'untested')); - if (validKey) { - apiKey = validKey.value.trim(); - } - } - - // 回退到 localStorage - if (!apiKey) { - const legacyKey = localStorage.getItem(`${selectedModel}ApiKeys`) || localStorage.getItem('translationApiKeys'); - if (legacyKey) { - apiKey = legacyKey.trim(); - } - } - - if (!apiKey) { - throw new Error(`请先配置 ${selectedModel} 模型的 API Key`); - } - } + // 强制使用 'aliyun' 模型(后端代理模式,无需前端 API Key) + // translation.js 的 predefinedConfigs 只配置了 'aliyun' 指向后端代理 + const selectedModel = 'aliyun'; + const apiKey = ''; // 后端代理不需要前端 API Key // 分段翻译长文本 const chunks = splitMarkdownIntoChunks(markdown, 2000); let translatedText = ''; const totalChunks = chunks.length; + console.log('[performTranslation] 分块数:', totalChunks); + console.log('[performTranslation] 输入markdown长度:', markdown?.length); + onProgress && onProgress(0, totalChunks, '正在翻译...'); // 临时禁用 promptPoolUI,避免历史详情页访问不存在的 UI 元素 @@ -251,28 +223,24 @@ async function performTranslation(markdown, onProgress) { onProgress && onProgress(i + 1, totalChunks, `翻译中 (${i + 1}/${totalChunks})`); try { - // 构建翻译选项 - const translateOptions = {}; - - // 如果是自定义模型,需要传入 modelConfig - if (selectedModel === 'custom' && modelConfig) { - translateOptions.modelConfig = modelConfig; - } - - // 调用 translateMarkdown 函数 - 不传 boundPrompt,让它使用内置提示词 + // 调用 translateMarkdown 函数 + // 'aliyun' 模型通过后端代理调用通义千问,无需前端 API Key const chunkResult = await translateMarkdown( chunks[i], targetLangName, selectedModel, apiKey, '[历史详情页翻译]', // logContext - '', // defaultSystemPrompt - 空值会触发内置提示词 - '', // defaultUserPromptTemplate - 空值会触发内置提示词 - false, // useCustomPrompts - true, // processTablePlaceholders - translateOptions + systemPrompt, // 直接传入中文翻译的系统提示词 + userPromptTemplate, // 直接传入中文翻译的用户提示词模板 + true, // useCustomPrompts = true,使用传入的提示词 + true, // processTablePlaceholders + {} // translateOptions - 使用默认配置 ); + console.log(`[performTranslation] 第${i + 1}块翻译完成, 结果长度:`, chunkResult?.length); + console.log(`[performTranslation] 第${i + 1}块翻译结果前100字符:`, chunkResult?.substring(0, 100)); + translatedText += chunkResult + '\n\n'; } catch (error) { console.error(`[performTranslation] 翻译第 ${i + 1} 块失败:`, error); @@ -285,6 +253,7 @@ async function performTranslation(markdown, onProgress) { window.promptPoolUI = originalPromptPoolUI; } + console.log('[performTranslation] 翻译完成, 最终长度:', translatedText.trim().length); return translatedText.trim(); } @@ -295,13 +264,19 @@ async function performTranslation(markdown, onProgress) { * @returns {string[]} 分割后的块数组 */ function splitMarkdownIntoChunks(markdown, maxChars = 2000) { + console.log('[splitMarkdownIntoChunks] 输入长度:', markdown?.length, 'maxChars:', maxChars); + console.log('[splitMarkdownIntoChunks] 输入前100字符:', markdown?.substring(0, 100)); + const chunks = []; const lines = markdown.split('\n'); + console.log('[splitMarkdownIntoChunks] 行数:', lines.length); + let currentChunk = ''; for (const line of lines) { if (currentChunk.length + line.length + 1 > maxChars) { if (currentChunk.trim()) { + console.log('[splitMarkdownIntoChunks] 添加块:', chunks.length + 1, '长度:', currentChunk.length); chunks.push(currentChunk.trim()); } currentChunk = line + '\n'; @@ -311,9 +286,60 @@ function splitMarkdownIntoChunks(markdown, maxChars = 2000) { } if (currentChunk.trim()) { + console.log('[splitMarkdownIntoChunks] 添加最后块:', chunks.length + 1, '长度:', currentChunk.length); chunks.push(currentChunk.trim()); } + console.log('[splitMarkdownIntoChunks] 最终块数:', chunks.length); + chunks.forEach((chunk, i) => console.log(`[splitMarkdownIntoChunks] 块${i + 1} 长度:`, chunk.length)); + + return chunks.length > 0 ? chunks : [markdown]; +} + +/** + * 根据标题分割 Markdown 文本为块数组(用于分块对比视图) + * @param {string} markdown - Markdown 文本 + * @returns {string[]} 分割后的块数组 + */ +function generateChunks(markdown) { + if (!markdown || typeof markdown !== 'string') return []; + + const lines = markdown.split(/\r?\n/); + const chunks = []; + let buffer = []; + let inCode = false; + + function flush() { + if (buffer.length) { + chunks.push(buffer.join('\n').trim()); + buffer = []; + } + } + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // 处理代码块 + if (/^\s*```/.test(line)) { + inCode = !inCode; + buffer.push(line); + continue; + } + + if (inCode) { + buffer.push(line); + continue; + } + + // 标题作为新分块的起点 + if (/^\s*#/.test(line)) { + flush(); + } + + buffer.push(line); + } + + flush(); return chunks.length > 0 ? chunks : [markdown]; } @@ -474,6 +500,13 @@ async function triggerReprocess(includeTranslation) { showToast(`${msg || '翻译中'} (${current}/${total})`, 'info', 5000); }); window.data.translation = translationResult; + + // 生成分块数据用于"分块对比"视图 + const ocrChunks = generateChunks(ocrResult.markdown); + const translatedChunks = generateChunks(translationResult); + window.data.ocrChunks = ocrChunks; + window.data.translatedChunks = translatedChunks; + console.log('[triggerReprocess] 生成分块: ocrChunks=', ocrChunks.length, 'translatedChunks=', translatedChunks.length); } catch (translationError) { console.error('[triggerReprocess] 翻译失败:', translationError); showToast(`翻译失败: ${translationError.message},但 OCR 已完成`, 'warning'); @@ -490,7 +523,8 @@ async function triggerReprocess(includeTranslation) { renderDetail(); } if (typeof showTab === 'function') { - showTab(includeTranslation ? 'translation' : 'ocr'); + // 翻译成功后显示分块对比,否则显示 OCR + showTab(includeTranslation && window.data.translation ? 'chunk-compare' : 'ocr'); } showToast('处理完成!', 'success'); diff --git a/js/process/translation.js b/js/process/translation.js index 3c699ce..970fc04 100644 --- a/js/process/translation.js +++ b/js/process/translation.js @@ -703,7 +703,7 @@ async function translateMarkdown( const predefinedConfigs = { 'aliyun': { // 所有翻译请求都指向后端代理,由后端决定使用哪个模型 - endpoint: 'http://localhost:3456/api/llm/dashscope/v1/chat/completions', + endpoint: 'http://localhost:3456/api/llm/tongyi/v1/chat/completions', modelName: '通义百炼', headers: { 'Content-Type': 'application/json' }, bodyBuilder: (sys, user) => {