// process/document.js /** * 将长文档分割为可翻译的块。 * 此函数旨在将 Markdown 文本按照指定的 token 限制进行智能分块, * 以便适应大语言模型处理上下文长度的限制。 * * 主要策略: * 1. **Token 估算与初步判断**: * - 使用 `estimateTokenCount` 估算整个文档的 token 数。 * - 如果文档未超过 token 限制的 1.1 倍,则不进行分割,直接返回原文作为一个块。 * 2. **行级初步分割**: * - 遍历文本的每一行。 * - 跟踪当前块的 token 数和行内容。 * - 智能分割点选择: * - 当 `currentTokenCount + lineTokens > tokenLimit` 且当前块已有一定内容 (`currentTokenCount > tokenLimit * 0.1`) 时,进行分割。 * - 在非代码块内,如果遇到一级或二级 Markdown 标题 (`#` 或 `##`),并且当前块内容已超过限制的 50%,则在此标题前分割,以保持章节完整性。 * - 维护 `inCodeBlock` 状态,避免在代码块内部错误地根据标题分割。 * 3. **二次段落级分割(针对超大块)**: * - 对初步分割产生的每个块进行检查。 * - 如果某个块的 token 数仍然超过限制的 1.1 倍,则调用 `splitByParagraphs` 对其进行更细致的段落级分割。 * 4. **日志记录**:在关键步骤通过 `addProgressLog` (如果可用) 输出日志,方便追踪分割过程。 * * @param {string} markdown - 要分割的Markdown文本。 * @param {number} tokenLimit - 每块的最大token数。 * @param {string} [logContext=""] - 日志前缀,用于区分不同上下文的日志输出。 * @returns {Array} 分割后的文本块数组。 */ function splitMarkdownIntoChunks(markdown, tokenLimit, logContext = "") { const estimatedTokens = estimateTokenCount(markdown); if (typeof addProgressLog === "function") { addProgressLog(`${logContext} 估算总 token 数: ~${estimatedTokens}, 分段限制: ${tokenLimit}`); } if (estimatedTokens <= tokenLimit * 1.1) { if (typeof addProgressLog === "function") { addProgressLog(`${logContext} 文档未超过大小限制,不进行分割。`); } return [markdown]; } if (typeof addProgressLog === "function") { addProgressLog(`${logContext} 文档超过大小限制,开始分割...`); } const lines = markdown.split('\n'); const chunks = []; let currentChunkLines = []; let currentTokenCount = 0; let inCodeBlock = false; const headingRegex = /^(#+)\s+.*/; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineTokens = estimateTokenCount(line); if (line.trim().startsWith('```')) { inCodeBlock = !inCodeBlock; } let shouldSplit = false; if (currentChunkLines.length > 0) { if (currentTokenCount + lineTokens > tokenLimit) { if (currentTokenCount > tokenLimit * 0.1) { shouldSplit = true; } } else if (!inCodeBlock && headingRegex.test(line)) { const match = line.match(headingRegex); if (match && match[1].length <= 2 && currentTokenCount > tokenLimit * 0.5) { shouldSplit = true; } } } if (shouldSplit) { chunks.push(currentChunkLines.join('\n')); currentChunkLines = []; currentTokenCount = 0; } currentChunkLines.push(line); currentTokenCount += lineTokens; } if (currentChunkLines.length > 0) { chunks.push(currentChunkLines.join('\n')); } if (typeof addProgressLog === "function") { addProgressLog(`${logContext} 初始分割为 ${chunks.length} 个片段.`); } const finalChunks = []; for(let j = 0; j < chunks.length; j++) { const chunk = chunks[j]; const chunkTokens = estimateTokenCount(chunk); if (chunkTokens > tokenLimit * 1.1) { if (typeof addProgressLog === "function") { addProgressLog(`${logContext} 警告: 第 ${j+1} 段 (${chunkTokens} tokens) 仍然超过限制 ${tokenLimit}. 尝试段落分割.`); } const subChunks = splitByParagraphs(chunk, tokenLimit, logContext, j+1); finalChunks.push(...subChunks); } else { finalChunks.push(chunk); } } if (finalChunks.length !== chunks.length && typeof addProgressLog === "function") { addProgressLog(`${logContext} 二次分割后总片段数: ${finalChunks.length}`); } return finalChunks; } /** * 按段落分割过大的文本块。 * 当 `splitMarkdownIntoChunks` 初步分割后,某些块可能仍然过大, * 此函数尝试将这些超大块按照 Markdown 的段落(空行分隔)进一步细分。 * * 主要逻辑: * 1. **段落分割**:使用 `text.split('\n\n')` 将文本块分割成段落数组。 * 2. **逐段累加与分割**: * - 遍历每个段落。 * - 估算段落的 token 数。 * - 如果单个段落本身就超过 `tokenLimit * 1.1`,则直接将其作为一个独立的块(不再细分),并记录警告。 * - 否则,将段落加入当前子块,并累加 token 数。 * - 如果加入当前段落会导致子块超过 `tokenLimit`,并且子块中已有内容,则先将当前子块保存,然后开始新的子块。 * 3. **日志记录**:记录段落分割的过程和结果。 * * @param {string} text - 需要按段落分割的文本块。 * @param {number} tokenLimit - 每块的最大token数。 * @param {string} logContext - 日志前缀。 * @param {number} chunkIndex - 当前块在原始分割结果中的索引 (用于日志)。 * @returns {Array} 分割后的子块数组。 */ function splitByParagraphs(text, tokenLimit, logContext, chunkIndex) { if (typeof addProgressLog === "function") { addProgressLog(`${logContext} 对第 ${chunkIndex} 段进行段落分割...`); } const paragraphs = text.split('\n\n'); const chunks = []; let currentChunkLines = []; let currentTokenCount = 0; for (const paragraph of paragraphs) { const paragraphTokens = estimateTokenCount(paragraph); if (paragraphTokens > tokenLimit * 1.1) { if (typeof addProgressLog === "function") { addProgressLog(`${logContext} 警告: 第 ${chunkIndex} 段中的段落 (${paragraphTokens} tokens) 超过限制 ${tokenLimit}. 将尝试按原样处理.`); } if (currentChunkLines.length > 0) { chunks.push(currentChunkLines.join('\n\n')); } chunks.push(paragraph); // Keep the large paragraph as a single chunk currentChunkLines = []; currentTokenCount = 0; continue; } if (currentTokenCount + paragraphTokens > tokenLimit && currentChunkLines.length > 0) { chunks.push(currentChunkLines.join('\n\n')); currentChunkLines = []; currentTokenCount = 0; } currentChunkLines.push(paragraph); currentTokenCount += paragraphTokens; } if (currentChunkLines.length > 0) { chunks.push(currentChunkLines.join('\n\n')); } if (typeof addProgressLog === "function") { addProgressLog(`${logContext} 第 ${chunkIndex} 段分割为 ${chunks.length} 个子段.`); } return chunks; } /** * 翻译长文档,支持分段、表格保护、并发控制和自定义模型配置。 * * 核心流程: * 1. **参数准备与 Token 限制**: * - 确保 `tokenLimitInput` 被正确解析为数字。 * 2. **表格保护**: * - 调用 `protectMarkdownTables` (如果可用) 将 Markdown 中的表格替换为占位符 (如 `__TABLE_PLACEHOLDER_0__`), * 并将原始表格内容存储在 `tablePlaceholders` 对象中。这可以防止翻译API破坏表格结构。 * - 如果检测到表格,更新系统提示 `updatedSystemPrompt`,告知模型如何处理这些占位符。 * 3. **文本分块**: * - 使用 `splitMarkdownIntoChunks` 将经过表格保护处理的文本 (`processedText`) 分割成 `originalTextChunks`。 * 4. **API 配置构建**: * - 根据 `model` 参数是 'custom' 还是预定义模型,调用 `buildCustomApiConfig` 或 `buildPredefinedApiConfig` 来准备 API 请求所需的配置对象 (`apiConfig`)。 * - 对于自定义模型,会从 `modelConfig` 参数中获取详细配置(如端点、模型ID、请求格式等)。 * 5. **创建翻译任务队列 (`allTranslationTasks`)**: * - 将所有文本块的翻译任务添加到队列中。 * - 如果有受保护的表格,将每个表格的翻译也作为一个独立的任务添加到队列中。 * 6. **并发翻译与重试**: * - 遍历 `allTranslationTasks`,为每个任务创建一个异步翻译 Promise。 * - 使用 `acquireSlot` 和 `releaseSlot` 控制并发翻译的数量。 * - 对每个任务执行翻译: * - **文本块翻译**:调用 `translateMarkdown` (并根据模型类型传递必要的参数,如 `modelConfig` for custom)。 * - **表格翻译**:构造特定的系统提示和用户提示,指导模型仅翻译表格内容并保持结构,然后调用 `callTranslationApi`。翻译结果会经过 `extractTableFromTranslation` 清理。 * - 实现重试机制 (最多 `MAX_TRANSLATION_RETRIES` 次),使用 `getRetryDelay` 计算退避延迟。 * - 如果任务在多次重试后仍然失败,则记录错误,并将原文(或原始表格内容)作为翻译结果的兜底。 * - 将所有任务的翻译结果(成功或失败的兜底)存储在 `translationResults` Map 中,键为 `text-{index}` 或 `table-{index}`。 * 7. **等待所有任务完成**:使用 `Promise.all` 等待所有翻译 Promise 执行完毕。 * 8. **结果组装与表格还原**: * - **构建翻译后表格映射**:从 `translationResults` 中提取已翻译的表格内容,存入 `translatedTablePlaceholders`。 * - **还原分块中的表格**: * - 对 `originalTextChunks` 中的每个块,使用 `restoreMarkdownTables` 和原始 `tablePlaceholders` 还原其包含的原始表格,得到 `restoredOcrChunks`。 * - 对 `translationResults` 中每个文本块的翻译结果,使用 `restoreMarkdownTables` 和 `translatedTablePlaceholders` 还原其包含的已翻译表格,得到 `translatedTextChunks`。 * - **合并翻译文本**:将 `translatedTextChunks` 连接起来得到 `combinedTranslation`。 * - **最终表格还原**:为保险起见,再次对 `combinedTranslation` 使用 `translatedTablePlaceholders` 进行一次整体的表格占位符替换。 * 9. **返回结果**:返回一个对象,包含最终的完整翻译文本 `translatedText`,以及还原了表格的原文分块 `originalChunks` 和译文分块 `translatedTextChunks`。 * * @param {string} markdownText - 待翻译的Markdown文本。 * @param {string} targetLang - 目标语言代码 (如 'zh-CN', 'en')。 * @param {string} model - 使用的翻译模型名称 (如 'mistral', 'custom')。 * @param {string} apiKey - 对应翻译模型的 API 密钥。 * @param {Object | null} modelConfig - 当 `model` 为 "custom" 时,提供自定义模型的配置对象, * 包含 `apiEndpoint` (或 `apiBaseUrl`), `modelId`, `requestFormat`, `temperature`, `max_tokens` 等。 * @param {number | string} tokenLimitInput - 每个翻译分块的最大 token 限制。 * @param {function} acquireSlot - 用于获取并发执行槽位的函数。 * @param {function} releaseSlot - 用于释放并发执行槽位的函数。 * @param {string} [logContext=""] - 日志记录的上下文前缀。 * @param {string} [defaultSystemPrompt=""] - 默认的系统提示词。 * @param {string} [defaultUserPromptTemplate=""] - 默认的用户提示词模板 (应包含 `${content}` 和 `${targetLangName}` 占位符)。 * @param {boolean} [useCustomPrompts=false] - 是否使用自定义的提示词(如果为 false,则使用内置或默认提示词)。 * @returns {Promise} 一个包含翻译结果的对象,结构为: * `{ translatedText: string, originalChunks: Array, translatedTextChunks: Array }`。 * `originalChunks` 和 `translatedTextChunks` 是经过表格还原处理后的分块数组。 * @throws {Error} 如果自定义模型配置不完整或发生其他严重错误。 */ async function translateLongDocument( markdownText, targetLang, model, apiKey, modelConfig, // 新增参数 tokenLimitInput, acquireSlot, releaseSlot, logContext = "", defaultSystemPrompt = "", defaultUserPromptTemplate = "", useCustomPrompts = false ) { console.log('translateLongDocument: apiKey', apiKey); const tokenLimit = parseInt(tokenLimitInput, 10) || 2000; // 确保是数字,提供默认值 // 先进行表格保护处理 let processedText = markdownText; let tablePlaceholders = {}; let hasProtectedTables = false; if (typeof protectMarkdownTables === 'function') { const processed = protectMarkdownTables(markdownText); processedText = processed.processedText; tablePlaceholders = processed.tablePlaceholders; hasProtectedTables = Object.keys(tablePlaceholders).length > 0; if (hasProtectedTables) { console.log(`${logContext} 长文档中检测到 ${Object.keys(tablePlaceholders).length} 个表格,已进行特殊保护`); if (typeof addProgressLog === "function") { addProgressLog(`${logContext} 长文档翻译: 已保护 ${Object.keys(tablePlaceholders).length} 个表格结构,将作为整体处理`); } } } // 增加表格处理提示到系统提示中 let updatedSystemPrompt = defaultSystemPrompt; if (hasProtectedTables) { updatedSystemPrompt = defaultSystemPrompt + "\n\n注意:文档中的表格已被特殊标记为占位符(如__TABLE_PLACEHOLDER_0__),请直接翻译占位符以外的内容,保持占位符不变。表格将在后续步骤中单独处理。"; } // 继续原有的分块处理逻辑 - 使用处理后的文本 const originalTextChunks = splitMarkdownIntoChunks(processedText, tokenLimit, logContext); console.log(`${logContext} 文档分割为 ${originalTextChunks.length} 部分进行翻译 (Limit: ${tokenLimit})`); if (typeof addProgressLog === "function") { addProgressLog(`${logContext} 文档被分割为 ${originalTextChunks.length} 部分进行翻译`); } // 准备API配置用于文本和表格翻译 let apiConfig; if (model === "custom") { // 兼容 apiEndpoint 和 apiBaseUrl const endpoint = modelConfig.apiEndpoint || modelConfig.apiBaseUrl; if (!modelConfig || !endpoint || !modelConfig.modelId) { throw new Error('Custom model configuration is incomplete for translateLongDocument. API Endpoint (或 apiBaseUrl) and Model ID are required.'); } apiConfig = buildCustomApiConfig( apiKey, endpoint, // 兼容 apiEndpoint 和 apiBaseUrl modelConfig.modelId, // 使用传入的 modelConfig modelConfig.requestFormat, // 使用传入的 modelConfig modelConfig.temperature, modelConfig.max_tokens, { endpointMode: modelConfig.endpointMode || 'auto' } ); } else { // 预设模型 const settingsForModels = typeof loadSettings === 'function' ? loadSettings() : {}; const customModelSettings = settingsForModels && settingsForModels.customModelSettings ? settingsForModels.customModelSettings : {}; let temperature = 0.5; if (customModelSettings.temperature !== undefined && customModelSettings.temperature !== null && customModelSettings.temperature !== '') { const parsedTemp = parseFloat(customModelSettings.temperature); if (!Number.isNaN(parsedTemp)) { temperature = parsedTemp; } } let maxTokens = 8000; if (customModelSettings.max_tokens !== undefined && customModelSettings.max_tokens !== null && customModelSettings.max_tokens !== '') { const parsedMax = parseInt(customModelSettings.max_tokens, 10); if (!Number.isNaN(parsedMax) && parsedMax > 0) { maxTokens = parsedMax; } } let geminiEndpointDynamic = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent'; try { if (typeof loadModelConfig === 'function') { const gcfg = loadModelConfig('gemini'); const preferred = gcfg && (gcfg.preferredModelId || gcfg.modelId); if (preferred && typeof preferred === 'string' && preferred.trim()) { geminiEndpointDynamic = `https://generativelanguage.googleapis.com/v1beta/models/${preferred.trim()}:generateContent`; } } } catch (e) { console.warn('加载 Gemini 配置失败,将在长文档翻译中使用默认模型。', e); } let deeplxEndpointTemplate = 'https://api.deeplx.org//translate'; try { if (typeof loadModelConfig === 'function') { const dlcfg = loadModelConfig('deeplx'); if (dlcfg) { if (dlcfg.endpointTemplate && typeof dlcfg.endpointTemplate === 'string') { deeplxEndpointTemplate = dlcfg.endpointTemplate.trim() || deeplxEndpointTemplate; } else if (dlcfg.apiBaseUrlTemplate && typeof dlcfg.apiBaseUrlTemplate === 'string') { deeplxEndpointTemplate = dlcfg.apiBaseUrlTemplate.trim() || deeplxEndpointTemplate; } else if (dlcfg.apiBaseUrl && typeof dlcfg.apiBaseUrl === 'string') { const base = dlcfg.apiBaseUrl.trim(); if (base) { deeplxEndpointTemplate = base.endsWith('/') ? `${base}/translate` : `${base}//translate`; } } } } } catch (e) { console.warn('加载 DeepLX 配置失败,将在长文档翻译中使用默认模板。', e); } const predefinedConfigs = { "mistral": { endpoint: "https://api.mistral.ai/v1/chat/completions", modelName: "mistral-large-latest", headers: { "Content-Type": "application/json" }, bodyBuilder: (sys, user) => ({ model: "mistral-large-latest", messages: [ { role: "system", content: sys }, { role: "user", content: user } ], temperature: temperature, max_tokens: maxTokens }), responseExtractor: (data) => data?.choices?.[0]?.message?.content }, "deepseek": { endpoint: "https://api.deepseek.com/v1/chat/completions", modelName: "DeepSeek", headers: { "Content-Type": "application/json" }, bodyBuilder: (sys, user) => { let modelId = 'deepseek-chat'; try { const cfg = loadModelConfig && loadModelConfig('deepseek'); if (cfg && (cfg.preferredModelId || cfg.modelId)) modelId = cfg.preferredModelId || cfg.modelId; } catch (_) {} return { model: modelId, messages: [ { role: "system", content: sys }, { role: "user", content: user } ], temperature: temperature, max_tokens: maxTokens }; }, responseExtractor: (data) => data?.choices?.[0]?.message?.content }, "gemini": { endpoint: geminiEndpointDynamic, modelName: "Google Gemini", headers: { "Content-Type": "application/json" }, bodyBuilder: (sys, user) => ({ contents: [ { role: "user", parts: [{ text: `${sys}\n\n${user}` }] } ], generationConfig: { temperature: temperature, maxOutputTokens: maxTokens } }), responseExtractor: (data) => { if (data?.candidates && data.candidates.length > 0 && data.candidates[0].content) { const parts = data.candidates[0].content.parts; return parts && parts.length > 0 ? parts[0].text : ''; } return ''; } }, "gemini-preview": { endpoint: "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent", modelName: "Google gemini-2.5-flash-preview-05-20", headers: { "Content-Type": "application/json" }, bodyBuilder: (sys, user) => ({ contents: [ { role: "user", parts: [{ text: `${sys}\n\n${user}` }] } ], generationConfig: { temperature: temperature, maxOutputTokens: maxTokens, responseModalities: ["TEXT"], responseMimeType: "text/plain" } }), responseExtractor: (data) => { if (data?.candidates && data.candidates.length > 0 && data.candidates[0].content) { const parts = data.candidates[0].content.parts; return parts && parts.length > 0 ? parts[0].text : ''; } return ''; } }, "tongyi": { endpoint: "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions", modelName: "通义百炼", headers: { "Content-Type": "application/json" }, bodyBuilder: (sys, user) => { let modelId = 'qwen-turbo-latest'; try { const cfg = loadModelConfig && loadModelConfig('tongyi'); if (cfg && (cfg.preferredModelId || cfg.modelId)) modelId = cfg.preferredModelId || cfg.modelId; } catch (_) {} const isQwenMT = typeof modelId === 'string' && modelId.toLowerCase().includes('qwen-mt'); const mergedContent = isQwenMT ? `${sys}\n\n${user}`.trim() : null; return { model: modelId, messages: isQwenMT ? [ { role: "user", content: mergedContent } ] : [ { role: "system", content: sys }, { role: "user", content: user } ], temperature: temperature, max_tokens: maxTokens, enable_thinking: false }; }, responseExtractor: (data) => data?.choices?.[0]?.message?.content }, "volcano": { endpoint: "https://ark.cn-beijing.volces.com/api/v3/chat/completions", modelName: "火山引擎", headers: { "Content-Type": "application/json" }, bodyBuilder: (sys, user) => { let modelId = 'doubao-1-5-pro-32k-250115'; try { const cfg = loadModelConfig && loadModelConfig('volcano'); if (cfg && (cfg.preferredModelId || cfg.modelId)) modelId = cfg.preferredModelId || cfg.modelId; } catch (_) {} return { model: modelId, messages: [ { role: "system", content: sys }, { role: "user", content: user } ], temperature: temperature, max_tokens: Math.min(maxTokens, 16384) }; }, responseExtractor: (data) => data?.choices?.[0]?.message?.content }, "deeplx": { endpoint: deeplxEndpointTemplate, modelName: "DeepLX", headers: { "Content-Type": "application/json" }, bodyBuilder: (sys, user, ctx = {}) => { const payload = { text: ctx && ctx.processedText ? ctx.processedText : user }; const targetLangCode = (typeof mapToDeeplxLangCode === 'function') ? mapToDeeplxLangCode(ctx && ctx.targetLang ? ctx.targetLang : undefined) : undefined; if (targetLangCode) { payload.target_lang = targetLangCode; } if (ctx && ctx.sourceLang) { const src = (typeof mapToDeeplxLangCode === 'function') ? mapToDeeplxLangCode(ctx.sourceLang) : undefined; if (src) payload.source_lang = src; } return payload; }, responseExtractor: (data) => { if (!data) return ''; if (typeof data === 'string') return data; if (typeof data.text === 'string') return data.text; if (data.data) { if (typeof data.data === 'string') return data.data; if (typeof data.data.text === 'string') return data.data.text; } if (Array.isArray(data.translations) && data.translations.length > 0) { const first = data.translations[0]; if (typeof first === 'string') return first; if (first && typeof first.text === 'string') return first.text; } if (Array.isArray(data.alternatives) && data.alternatives.length > 0) { const alt = data.alternatives[0]; if (typeof alt === 'string') return alt; if (alt && typeof alt.text === 'string') return alt.text; } if (typeof data.result === 'string') return data.result; if (data.result && typeof data.result.text === 'string') return data.result.text; if (typeof data.translation === 'string') return data.translation; return null; } } }; if (!predefinedConfigs[model]) { throw new Error(`暂不支持模型 ${model} 的长文档处理。`); } apiConfig = buildPredefinedApiConfig(predefinedConfigs[model], apiKey); } // 创建所有翻译任务的统一队列(包括文本块和表格) const allTranslationTasks = []; // 添加所有文本块翻译任务 originalTextChunks.forEach((part, i) => { allTranslationTasks.push({ type: 'text', index: i, content: part, context: `${logContext} (Part ${i+1}/${originalTextChunks.length})` }); }); // 添加所有表格翻译任务(如果有) if (hasProtectedTables) { let tableIndex = 0; for (const [placeholder, tableContent] of Object.entries(tablePlaceholders)) { allTranslationTasks.push({ type: 'table', index: tableIndex, placeholder: placeholder, content: tableContent, context: `${logContext} (Table ${tableIndex+1}/${Object.keys(tablePlaceholders).length})` }); tableIndex++; } } if (typeof addProgressLog === "function") { addProgressLog(`${logContext} 总计待翻译任务: ${allTranslationTasks.length} (文本块: ${originalTextChunks.length}, 表格: ${hasProtectedTables ? Object.keys(tablePlaceholders).length : 0})`); } let hasErrors = false; const MAX_TRANSLATION_RETRIES = 3; const translationResults = new Map(); // 使用Map存储翻译结果 // 为所有任务创建翻译Promise const translationPromises = allTranslationTasks.map(async (task) => { const taskLogContext = task.context; let lastError = null; for (let attempt = 0; attempt <= MAX_TRANSLATION_RETRIES; attempt++) { const attemptNum = attempt + 1; if (typeof addProgressLog === "function") { addProgressLog(`${taskLogContext} 排队等待翻译槽 (尝试 ${attemptNum})...`); } await acquireSlot(); if (typeof addProgressLog === "function") { addProgressLog(`${taskLogContext} 翻译槽已获取。开始翻译 ${task.type === 'table' ? '表格' : '文本'} (尝试 ${attemptNum})...`); } try { let result; if (task.type === 'text') { // Step 1: 为任务预绑定提示词并入队,便于失败时做“真正队列替换” let boundPrompt = null; let requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2,8)}_t${task.index}`; try { if (typeof window !== 'undefined' && window.promptPoolUI && typeof window.promptPoolUI.getPromptForTranslation === 'function') { const p = window.promptPoolUI.getPromptForTranslation(); // 仅当池模式返回 id/system/user 时认为可用 if (p && p.id && p.systemPrompt && p.userPromptTemplate) { boundPrompt = p; if (typeof window.translationPromptPool !== 'undefined' && typeof window.translationPromptPool.enqueueRequest === 'function') { window.translationPromptPool.enqueueRequest(p.id, { requestId, model: (apiConfig && apiConfig.modelName) || model }); } } } } catch (e) { console.warn('[PromptPool] 预绑定提示词失败(跳过绑定):', e); } // 翻译文本块 //console.log('document.js 调用 translateMarkdown 参数:', { // useCustomPrompts, // defaultUserPromptTemplate, // defaultSystemPrompt, // modelConfig, // content: task.content, // targetLang, // model, // apiKey, // taskLogContext //}); if (model === 'custom') { result = await translateMarkdown( task.content, targetLang, model, apiKey, modelConfig, taskLogContext, updatedSystemPrompt, defaultUserPromptTemplate, useCustomPrompts, false, { boundPrompt, requestId } ); } else { result = await translateMarkdown( task.content, targetLang, model, apiKey, taskLogContext, updatedSystemPrompt, defaultUserPromptTemplate, useCustomPrompts, false, { boundPrompt, requestId } ); } //console.log('document.js translateMarkdown 返回:', result); translationResults.set('text-' + task.index, result); } else if (task.type === 'table') { // 翻译表格 let tableSystemPrompt = `你是一个精确翻译表格的助手。请将表格翻译成${targetLang},严格保持以下格式要求: 1. 保持所有表格分隔符(|)和结构完全不变 2. 保持表格对齐标记(:--:、:--、--:)不变 3. 保持表格的行数和列数完全一致 4. 保持数学公式、符号和百分比等专业内容不变 5. 翻译表格标题(如有)和表格内的文本内容 6. 表格内容与表格外内容要明确区分`; // 注入术语库(如启用且有命中) try { const settingsForGlossary = (typeof loadSettings === 'function') ? loadSettings() : {}; const glossaryEnabled = !!settingsForGlossary.enableGlossary; if (glossaryEnabled && typeof getGlossaryMatchesForText === 'function') { const matches = getGlossaryMatchesForText(task.content); if (matches && matches.length > 0 && typeof buildGlossaryInstruction === 'function') { const instr = buildGlossaryInstruction(matches, targetLang); if (instr) { tableSystemPrompt = tableSystemPrompt + "\n\n" + instr; if (typeof addProgressLog === 'function') { const names = matches.slice(0, 6).map(m => m.term).join(', '); addProgressLog(`${taskLogContext} [表格] 命中备择库 ${matches.length} 条:${names}${matches.length>6?'...':''}`); } } } } } catch (e) { console.warn('Glossary injection for table skipped due to error:', e); } // 用户提示词 const tableUserPrompt = `请将以下Markdown表格翻译成${targetLang},请确保完全保持表格结构和格式: ${task.content} 注意:请保持表格格式完全不变,包括所有的 | 符号、对齐标记、数学公式和符号。`; // 构建请求体 const requestBody = apiConfig.bodyBuilder ? apiConfig.bodyBuilder(tableSystemPrompt, tableUserPrompt, { processedText: task.content, rawText: task.content, targetLang, tablePlaceholder: task.placeholder, requestType: 'table' }) : { model: apiConfig.modelName, messages: [ { role: "system", content: tableSystemPrompt }, { role: "user", content: tableUserPrompt } ] }; // 调用API翻译表格 const translatedTable = await callTranslationApi(apiConfig, requestBody); // 提取和清理翻译结果中的表格部分 const cleanedTable = typeof extractTableFromTranslation === 'function' ? (extractTableFromTranslation(translatedTable) || task.content) : task.content; translationResults.set('table-' + task.index, { placeholder: task.placeholder, translatedContent: cleanedTable }); } if (typeof releaseSlot === "function") { releaseSlot(); } if (typeof addProgressLog === "function") { addProgressLog(`${taskLogContext} 翻译槽已释放 (成功)。`); } return; // 成功,退出重试循环 } catch (error) { // 释放翻译槽 if (typeof releaseSlot === "function") { releaseSlot(); } if (typeof addProgressLog === "function") { addProgressLog(`${taskLogContext} 翻译槽已释放 (失败)。`); } lastError = error; console.error(`${taskLogContext} 翻译失败 (尝试 ${attemptNum}/${MAX_TRANSLATION_RETRIES + 1}):`, error); if (typeof addProgressLog === "function") { addProgressLog(`${taskLogContext} 警告: 翻译失败 (尝试 ${attemptNum}/${MAX_TRANSLATION_RETRIES + 1}) - ${error.message}.`); } if (attempt < MAX_TRANSLATION_RETRIES) { const delay = typeof getRetryDelay === 'function' ? getRetryDelay(attempt) : Math.min(1000 * Math.pow(2, attempt), 30000); if (typeof addProgressLog === "function") { addProgressLog(`${taskLogContext} ${delay.toFixed(0)}ms 后重试...`); } await new Promise(resolve => setTimeout(resolve, delay)); } else { if (typeof addProgressLog === "function") { addProgressLog(`${taskLogContext} 已达最大重试次数 (${MAX_TRANSLATION_RETRIES + 1}次尝试),使用原文。`); } hasErrors = true; // 保存原始内容作为结果 if (task.type === 'text') { translationResults.set('text-' + task.index, `\n\n> **[翻译错误 (重试 ${MAX_TRANSLATION_RETRIES + 1} 次失败) - 保留原文 Part ${task.index+1}]**\n\n${task.content}\n\n`); } else if (task.type === 'table') { translationResults.set('table-' + task.index, { placeholder: task.placeholder, translatedContent: task.content }); } return; // 结束重试 } } } console.error(`${taskLogContext} Unexpected state reached after retry loop.`); if (typeof addProgressLog === "function") { addProgressLog(`${taskLogContext} 警告: 翻译重试逻辑结束后状态意外,保留原文。`); } hasErrors = true; // 安全兜底,保存原始内容 if (task.type === 'text') { translationResults.set('text-' + task.index, `\n\n> **[翻译意外失败 - 保留原文 Part ${task.index+1}]**\n\n${task.content}\n\n`); } else if (task.type === 'table') { translationResults.set('table-' + task.index, { placeholder: task.placeholder, translatedContent: task.content }); } }); // 等待所有并发翻译任务完成 try { await Promise.all(translationPromises); } catch (error) { console.error(`${logContext} An unexpected error occurred during Promise.all for translations:`, error); if (typeof addProgressLog === "function") { addProgressLog(`${logContext} 错误: 并发翻译过程中出现意外错误。`); } hasErrors = true; } if (hasErrors) { if (typeof addProgressLog === "function") { addProgressLog(`${logContext} 部分或全部翻译任务处理失败 (已完成重试)。`); } } else { if (typeof addProgressLog === "function") { addProgressLog(`${logContext} 所有翻译任务处理完成。`); } } // 构建翻译后表格占位符映射 let translatedTablePlaceholders = {}; if (hasProtectedTables) { for (let i = 0; i < Object.keys(tablePlaceholders).length; i++) { const tableResult = translationResults.get('table-' + i); if (tableResult && tableResult.placeholder) { translatedTablePlaceholders[tableResult.placeholder] = tableResult.translatedContent; } } } // 收集所有文本块的翻译结果(原文和译文都做表格还原) const restoredOcrChunks = []; const translatedTextChunks = []; for (let i = 0; i < originalTextChunks.length; i++) { let ocrChunk = originalTextChunks[i]; let translatedChunk = translationResults.get('text-' + i); // 原文分块还原原文表格 if (hasProtectedTables && typeof restoreMarkdownTables === 'function') { ocrChunk = await restoreMarkdownTables(ocrChunk, tablePlaceholders); } // 译文分块还原翻译后表格 if (hasProtectedTables && typeof restoreMarkdownTables === 'function') { translatedChunk = await restoreMarkdownTables(translatedChunk, translatedTablePlaceholders); } restoredOcrChunks.push(ocrChunk); translatedTextChunks.push(translatedChunk || originalTextChunks[i]); } // 合并已翻译的块 let combinedTranslation = translatedTextChunks.join('\n\n'); // 如果有表格,替换所有表格占位符(保险起见,整体再替换一遍) if (hasProtectedTables) { if (typeof addProgressLog === "function") { addProgressLog(`${logContext} 正在替换表格占位符...`); } for (let i = 0; i < Object.keys(translatedTablePlaceholders).length; i++) { const placeholder = Object.keys(translatedTablePlaceholders)[i]; const translatedContent = translatedTablePlaceholders[placeholder]; combinedTranslation = combinedTranslation.replace( placeholder, translatedContent ); } if (typeof addProgressLog === "function") { addProgressLog(`${logContext} 表格占位符替换完成。`); } } return { translatedText: combinedTranslation, originalChunks: restoredOcrChunks, // 现在是还原了表格的原文分块 translatedTextChunks: translatedTextChunks // 现在是还原了表格的译文分块 }; } // 将函数添加到processModule对象 if (typeof processModule !== 'undefined') { processModule.splitMarkdownIntoChunks = splitMarkdownIntoChunks; processModule.splitByParagraphs = splitByParagraphs; processModule.translateLongDocument = translateLongDocument; }