// process/translation.js // 辅助函数:构建预定义 API 配置 /** * 为预定义的翻译模型构建 API 请求配置。 * 此函数接收一个基础的 API 配置对象 (`apiConfig`) 和 API 密钥 (`key`), * 然后根据模型名称(从 `apiConfig.modelName` 中提取并转换为小写)来设置特定的认证头部。 * * 主要逻辑: * 1. **配置浅拷贝**:创建 `apiConfig` 和 `apiConfig.headers` 的浅拷贝,以避免修改原始对象。 * 2. **认证头部设置**: * - 将 `config.modelName` 转为小写进行比较。 * - 如果模型名称包含 "claude",则在头部设置 `x-api-key`。 * - 如果模型名称包含 "gemini",则将 API 密钥作为查询参数 `key` 追加到端点 URL。 * 它会正确处理端点 URL 中可能已存在的查询参数。 * - 对于其他模型(如 Mistral, DeepSeek 等),默认在头部设置 `Authorization: Bearer {key}`。 * 3. **返回配置**:返回更新后的配置对象。 * * @param {Object} apiConfig - 预定义模型的初始配置对象,通常包含 `endpoint`, `modelName`, `headers`, `bodyBuilder`, `responseExtractor`。 * @param {string} key - 用于认证的 API 密钥。 * @returns {Object} 添加了认证头部(或更新了端点)的完整 API 配置对象。 */ function buildPredefinedApiConfig(apiConfig, key) { const config = { ...apiConfig }; // 浅拷贝 config.headers = { ...config.headers }; // 浅拷贝 headers // 设置认证 - 添加防御性检查,避免在modelName为undefined时调用toLowerCase() const modelNameLower = config.modelName ? config.modelName.toLowerCase() : ''; if (modelNameLower.includes('claude')) { config.headers['x-api-key'] = key; } else if (modelNameLower.includes('gemini')) { // Correctly handle potential existing query parameters let baseUrl = config.endpoint.split('?')[0]; config.endpoint = `${baseUrl}?key=${key}`; } else if (modelNameLower.includes('deeplx')) { const encodedKey = encodeURIComponent(key.trim()); const placeholderPatterns = ['{API_KEY}', '{api_key}', '{apiKey}', '{key}', '__API_KEY__', '', '', ':API_KEY', ':api_key', ':key', '${API_KEY}', '${api_key}']; let endpoint = config.endpoint || ''; let replaced = false; placeholderPatterns.forEach(function(pattern) { if (endpoint.includes(pattern)) { endpoint = endpoint.replace(new RegExp(pattern, 'g'), encodedKey); replaced = true; } }); if (!replaced) { // 如果模板中未包含占位符,则尝试在末尾追加 Key if (!endpoint.endsWith('/')) endpoint += '/'; endpoint += encodedKey; } config.endpoint = endpoint; // DeepLX 接口通常不需要 Authorization 头,确保移除可能的残留 if (config.headers['Authorization']) delete config.headers['Authorization']; } else { config.headers['Authorization'] = `Bearer ${key}`; } return config; } function buildInstructionBlock(content) { const trimmed = (content || '').trim(); if (!trimmed) return ''; return `[[PBX_INSTR_START]] ${trimmed} [[PBX_INSTR_END]] `; } function stripInstructionBlocks(text) { if (typeof text !== 'string') return text; return text.replace(/\s*\[\[PBX_INSTR_START\]\][\s\S]*?\[\[PBX_INSTR_END\]\]\s*/gi, '').trim(); } function joinUrlSegments(base, segment) { if (!base) return segment || ''; if (!segment) return base; const hasTrailingSlash = base.endsWith('/'); const hasLeadingSlash = segment.startsWith('/'); if (hasTrailingSlash && hasLeadingSlash) { return base + segment.slice(1); } if (!hasTrailingSlash && !hasLeadingSlash) { return `${base}/${segment}`; } return base + segment; } function appendQueryParam(urlString, param, value) { try { const url = new URL(urlString); url.searchParams.set(param, value); return url.toString(); } catch (error) { const sanitized = urlString.replace(new RegExp(`([?&])${param}=[^&]*`, 'i'), '$1').replace(/[?&]$/, ''); const separator = sanitized.includes('?') ? '&' : '?'; return `${sanitized}${separator}${encodeURIComponent(param)}=${encodeURIComponent(value)}`; } } function extractGeminiModelId(pathname) { if (typeof pathname !== 'string') return null; const match = pathname.match(/\/models\/([^/:]+)(?::generatecontent)?$/i); return match ? match[1] : null; } function normalizeGeminiEndpoint(baseUrlInput, modelIdInput, requestFormat) { if (!baseUrlInput || typeof baseUrlInput !== 'string') { throw new Error('自定义 Gemini 模型需要提供有效的 API 地址'); } let url; try { url = new URL(baseUrlInput.trim()); } catch (error) { try { url = new URL(`https://${baseUrlInput.trim()}`); } catch (_) { throw new Error('Gemini API Base URL 必须包含协议,例如 https://generativelanguage.googleapis.com'); } } url.searchParams.delete('key'); let path = url.pathname || ''; if (path.length > 1 && path.endsWith('/')) { path = path.slice(0, -1); } const defaultModelId = requestFormat === 'gemini-preview' ? 'gemini-2.5-flash-preview-05-20' : 'gemini-2.0-flash'; const inferredModelId = extractGeminiModelId(path); const resolvedModelId = (modelIdInput && modelIdInput.trim()) || inferredModelId || defaultModelId; const lowerPath = (path || '').toLowerCase(); const endsWithGenerate = /:generatecontent$/i.test(lowerPath); if (!path || path === '/') { path = `/v1beta/models/${resolvedModelId}:generateContent`; } else if (/\/models\/$/i.test(path)) { path = `${path}${resolvedModelId}:generateContent`; } else if (/\/models$/i.test(path)) { path = `${path}/${resolvedModelId}:generateContent`; } else if (/\/models\/[^/]+$/i.test(path)) { path = path.replace(/\/models\/[^/]+$/i, `/models/${resolvedModelId}`); if (!endsWithGenerate) { path = `${path}:generateContent`; } } else if (/\/v1beta$/i.test(path) || /\/v1$/i.test(path)) { path = `${path}/models/${resolvedModelId}:generateContent`; } else if (!endsWithGenerate) { path = `${path}/v1beta/models/${resolvedModelId}:generateContent`; } if (!/:generatecontent$/i.test(path)) { path = `${path}:generateContent`; } url.pathname = path; url.search = ''; return { endpoint: url.toString(), modelName: resolvedModelId }; } function normalizeOpenAIEndpoint(baseApiUrlInput, format, endpointMode = 'auto', targetSegment) { if (!baseApiUrlInput || typeof baseApiUrlInput !== 'string') { throw new Error('自定义模型需要提供 API Base URL'); } const trimmed = baseApiUrlInput.trim(); if (!trimmed) { throw new Error('自定义模型需要提供 API Base URL'); } const mode = endpointMode || 'auto'; const normalizedSegment = (targetSegment ? targetSegment : (format === 'anthropic' ? 'messages' : 'chat/completions')) .replace(/^\/+/, ''); const lower = trimmed.toLowerCase(); const base = trimmed.replace(/\/+$/, ''); const v1Segment = normalizedSegment.startsWith('v1/') ? normalizedSegment : `v1/${normalizedSegment}`; const terminalPaths = [ normalizedSegment, `/${normalizedSegment}`, v1Segment, `/${v1Segment}` ]; if (terminalPaths.some(path => lower.endsWith(path))) { return base; } if (mode === 'manual') { return base; } if (mode === 'chat') { return joinUrlSegments(base, normalizedSegment); } if (/\/v1$/.test(lower)) { return joinUrlSegments(base, normalizedSegment); } return joinUrlSegments(base, v1Segment); } const DEEPLX_LANG_CODE_MAP = { 'bulgarian': 'BG', 'bg': 'BG', 'български': 'BG', '保加利亚语': 'BG', 'chinese': 'ZH', 'zh': 'ZH', 'zh-cn': 'ZH', 'zh_cn': 'ZH', '中文': 'ZH', '中文(简体)': 'ZH', '简体中文': 'ZH', 'traditional chinese': 'ZH', 'zh-tw': 'ZH', 'zh_tw': 'ZH', '中文(繁体)': 'ZH', '繁体中文': 'ZH', 'czech': 'CS', 'cs': 'CS', 'čeština': 'CS', '捷克语': 'CS', 'danish': 'DA', 'da': 'DA', 'dansk': 'DA', '丹麦语': 'DA', 'dutch': 'NL', 'nl': 'NL', 'nederlands': 'NL', '荷兰语': 'NL', 'english': 'EN', 'en': 'EN', 'english (uk)': 'EN', 'english (gb)': 'EN', 'en-gb': 'EN', 'english (us)': 'EN', 'en-us': 'EN', 'english (american)': 'EN', '英语': 'EN', 'estonian': 'ET', 'et': 'ET', 'eesti': 'ET', '爱沙尼亚语': 'ET', 'finnish': 'FI', 'fi': 'FI', 'suomi': 'FI', '芬兰语': 'FI', 'french': 'FR', 'fr': 'FR', 'français': 'FR', '法语': 'FR', 'german': 'DE', 'de': 'DE', 'deutsch': 'DE', '德语': 'DE', 'greek': 'EL', 'el': 'EL', 'ελληνικά': 'EL', '希腊语': 'EL', 'hungarian': 'HU', 'hu': 'HU', 'magyar': 'HU', '匈牙利语': 'HU', 'italian': 'IT', 'it': 'IT', 'italiano': 'IT', '意大利语': 'IT', 'japanese': 'JA', 'ja': 'JA', '日本語': 'JA', '日语': 'JA', 'latvian': 'LV', 'lv': 'LV', 'latviešu': 'LV', '拉脱维亚语': 'LV', 'lithuanian': 'LT', 'lt': 'LT', 'lietuvių': 'LT', '立陶宛语': 'LT', 'polish': 'PL', 'pl': 'PL', 'polski': 'PL', '波兰语': 'PL', 'portuguese': 'PT', 'pt': 'PT', 'português': 'PT', 'portuguese (portugal)': 'PT', '葡萄牙语': 'PT', '葡萄牙语(葡萄牙)': 'PT', '葡萄牙语(巴西)': 'PT', 'portuguese (brazil)': 'PT', 'pt-br': 'PT', 'romanian': 'RO', 'ro': 'RO', 'română': 'RO', '罗马尼亚语': 'RO', 'russian': 'RU', 'ru': 'RU', 'русский': 'RU', '俄语': 'RU' }; const DEEPLX_LANG_DISPLAY = { 'BG': { zh: '保加利亚语', native: 'Български' }, 'ZH': { zh: '中文', native: '中文' }, 'CS': { zh: '捷克语', native: 'Česky' }, 'DA': { zh: '丹麦语', native: 'Dansk' }, 'NL': { zh: '荷兰语', native: 'Nederlands' }, 'EN': { zh: '英语', native: 'English' }, 'ET': { zh: '爱沙尼亚语', native: 'Eesti' }, 'FI': { zh: '芬兰语', native: 'Suomi' }, 'FR': { zh: '法语', native: 'Français' }, 'DE': { zh: '德语', native: 'Deutsch' }, 'EL': { zh: '希腊语', native: 'Ελληνικά' }, 'HU': { zh: '匈牙利语', native: 'Magyar' }, 'IT': { zh: '意大利语', native: 'Italiano' }, 'JA': { zh: '日语', native: '日本語' }, 'LV': { zh: '拉脱维亚语', native: 'Latviešu' }, 'LT': { zh: '立陶宛语', native: 'Lietuvių' }, 'PL': { zh: '波兰语', native: 'Polski' }, 'PT': { zh: '葡萄牙语', native: 'Português' }, 'RO': { zh: '罗马尼亚语', native: 'Română' }, 'RU': { zh: '俄语', native: 'Русский' } }; function mapToDeeplxLangCode(targetLang) { if (!targetLang) return undefined; const normalized = String(targetLang).trim(); if (!normalized) return undefined; const lower = normalized.toLowerCase(); if (DEEPLX_LANG_CODE_MAP[lower]) { return DEEPLX_LANG_CODE_MAP[lower]; } if (/^[a-z]{2}$/i.test(normalized)) { return normalized.toUpperCase(); } if (/^[a-z]{2}-[a-z]{2}$/i.test(normalized)) { return normalized.toUpperCase(); } return undefined; } if (typeof window !== 'undefined') { window.mapToDeeplxLangCode = mapToDeeplxLangCode; window.DEEPLX_LANG_CODE_MAP = DEEPLX_LANG_CODE_MAP; window.DEEPLX_LANG_DISPLAY = DEEPLX_LANG_DISPLAY; if (typeof window.updateDeeplxTargetLangHint === 'function') { try { window.updateDeeplxTargetLangHint(); } catch (e) { console.warn('updateDeeplxTargetLangHint failed', e); } } } // 辅助函数:构建自定义 API 配置 /** * 为用户自定义的翻译模型构建 API 请求配置(翻译模块专用,避免与聊天模块冲突)。 * 此函数根据用户提供的基础 URL、模型 ID、请求格式、密钥以及可选的温度和最大 token 数, * 生成一个完整的、可用于调用自定义翻译 API 的配置对象。 * * 主要逻辑: * 1. **格式归一化**:根据 `customRequestFormat` 统一为小写格式,默认视为 `openai`。 * 2. **端点规范化**: * - 对于 OpenAI 兼容(含默认)与 Anthropic 格式,使用 `normalizeOpenAIEndpoint` 根据格式智能拼接 `/v1/chat/completions` 或 `/v1/messages` 等常用后缀,避免重复追加。 * - 对于 Gemini (`gemini` / `gemini-preview`),通过 `normalizeGeminiEndpoint` 解析或自动补全 `/v1beta/models/{modelId}:generateContent` 路径,并移除可能残留的 `key` 查询参数。 * - Gemini 端点会在后续步骤中附加 `?key={API Key}` 查询参数。 * 3. **配置对象初始化**:创建包含 `endpoint`, `modelName`, `headers`, `bodyBuilder`, `responseExtractor` 的配置。 * 4. **按格式生成请求构造器与响应解析器**: * - `openai`:使用 Bearer Token 认证,构建传统聊天补全请求体,并从 `choices[0].message.content` 中提取结果。 * - `anthropic`:设置 `x-api-key` 与 `anthropic-version` 头部,构建 Claude 兼容消息体,从 `content[0].text` 中提取结果。 * - `gemini` / `gemini-preview`:将系统提示与用户提示合并到 Gemini `contents` 结构,配置 `generationConfig`,并从 `candidates[0].content.parts[0].text` 中提取结果(预览版本额外指定 `responseModalities`)。 * - 其他未知格式:回退到 OpenAI 兼容请求结构并发出警告。 * 5. **返回配置**:最终返回可直接用于请求翻译 API 的配置对象。 * * @param {string} key - API 密钥。 * @param {string} baseApiUrlInput - 用户提供的 API 基础 URL (例如 `https://api.example.com` 或 `https://api.gemini.example/v1beta/models/gemini-pro:generateContent`)。 * @param {string} customModelId - 用户指定的模型 ID (例如 `gpt-3.5-turbo`, `claude-2`, `gemini-pro`)。 * @param {string} customRequestFormat - 请求体和响应体的格式类型 (如 'openai', 'anthropic', 'gemini')。 * @param {number} [temperature] - (可选) 模型生成时的温度参数。 * @param {number} [max_tokens] - (可选) 模型生成的最大 token 数。 * @returns {Object} 构建好的 API 配置对象,包含 `endpoint`, `modelName`, `headers`, `bodyBuilder`, `responseExtractor`。 */ const buildCustomApiConfigForTranslation = function(key, baseApiUrlInput, customModelId, customRequestFormat, temperature, max_tokens, options = {}) { const format = (customRequestFormat || 'openai').toLowerCase(); let effectiveModelId = (customModelId && customModelId.trim()) || ''; const endpointMode = options.endpointMode || 'auto'; let finalApiEndpoint; if (format === 'gemini' || format === 'gemini-preview') { const geminiInfo = normalizeGeminiEndpoint(baseApiUrlInput, effectiveModelId, format); finalApiEndpoint = appendQueryParam(geminiInfo.endpoint, 'key', key); effectiveModelId = geminiInfo.modelName; } else { finalApiEndpoint = normalizeOpenAIEndpoint(baseApiUrlInput, format, endpointMode); } const config = { endpoint: finalApiEndpoint, modelName: effectiveModelId, headers: { 'Content-Type': 'application/json' }, bodyBuilder: null, responseExtractor: null }; const temperatureValue = temperature ?? 0.5; const maxTokensValue = max_tokens ?? 8000; const modelToUse = effectiveModelId || customModelId; switch (format) { case 'openai': config.headers['Authorization'] = `Bearer ${key}`; config.bodyBuilder = (sys_prompt, user_prompt) => ({ model: modelToUse, messages: [{ role: "system", content: sys_prompt }, { role: "user", content: user_prompt }], temperature: temperatureValue, max_tokens: maxTokensValue }); config.responseExtractor = (data) => data?.choices?.[0]?.message?.content; break; case 'anthropic': config.headers['x-api-key'] = key; config.headers['anthropic-version'] = '2023-06-01'; config.bodyBuilder = (sys_prompt, user_prompt) => ({ model: modelToUse, system: sys_prompt, messages: [{ role: "user", content: user_prompt }], temperature: temperatureValue, max_tokens: maxTokensValue }); config.responseExtractor = (data) => data?.content?.[0]?.text; break; case 'gemini': case 'gemini-preview': config.modelName = effectiveModelId; config.headers = { 'Content-Type': 'application/json' }; const geminiMaxTokens = max_tokens ?? 8192; config.bodyBuilder = (sys_prompt, user_prompt) => ({ contents: [{ role: "user", parts: [{ text: `${sys_prompt}\n\n${user_prompt}` }] }], generationConfig: { temperature: temperatureValue, maxOutputTokens: geminiMaxTokens, ...(format === 'gemini-preview' ? { responseModalities: ["TEXT"], responseMimeType: 'text/plain' } : {}) } }); config.responseExtractor = (data) => data?.candidates?.[0]?.content?.parts?.[0]?.text; break; default: config.headers['Authorization'] = `Bearer ${key}`; config.bodyBuilder = (sys_prompt, user_prompt) => ({ model: modelToUse, messages: [{ role: "system", content: sys_prompt }, { role: "user", content: user_prompt }], temperature: temperatureValue, max_tokens: maxTokensValue }); config.responseExtractor = (data) => data?.choices?.[0]?.message?.content; console.warn(`Unsupported custom request format: ${customRequestFormat}. Defaulting to OpenAI-like structure.`); break; } return config; } /** * 翻译单个 Markdown 文本块,支持预定义模型和自定义模型,并可选择性处理内嵌的表格占位符。 * * 主要步骤: * 1. **参数处理与兼容性**: * - 由于函数签名在支持自定义模型配置 (`modelConfig`) 时变得复杂,通过检查 `arguments` 来正确解析传入的参数, * 特别是当 `model` 为 "custom" 时,`modelConfigForCustom` 从 `arguments[4]` 获取,后续参数依次顺延。 * 2. **表格预处理** (如果 `actualProcessTablePlaceholders` 为 true 且 `protectMarkdownTables` 函数可用): * - 调用 `protectMarkdownTables` 将 Markdown 中的表格替换为占位符 (如 `__TABLE_PLACEHOLDER_0__`)。 * - 存储原始表格内容在 `tablePlaceholders` 中。 * - 如果检测到表格,则在系统提示中追加说明,告知模型如何处理这些占位符(即保持不变)。 * 3. **构建 Prompt**: * - 初始化 `systemPrompt` 和 `userPrompt`。 * - 如果使用了表格保护,则向 `systemPrompt` 追加关于如何处理表格占位符的指示。 * - 如果未使用自定义提示 (`!actualUseCustomPrompts`) 或者自定义提示为空,则调用 `getBuiltInPrompts` (如果可用) 获取内置的针对目标语言的提示模板,否则使用非常基础的兜底提示。 * - **替换模板变量**:在最终的 `userPrompt` 中,将 `${targetLangName}` 替换为实际的目标语言名称,将 `${content}` 替换为经过表格预处理的文本 (`processedText`)。 * - *警告检查*:如果最终的 `userPrompt` 未包含 `processedText`,则打印警告,因为模型可能无法接收到待翻译内容。 * 4. **构建 API 配置 (`apiConfig`)**: * - 如果 `model` 是 "custom": * - 检查 `modelConfigForCustom` 是否有效(包含端点和模型 ID)。 * - 调用 `buildCustomApiConfig` 生成配置。 * - 否则(预定义模型): * - 从全局设置 (`loadSettings`) 中获取温度和最大 token 数等参数。 * - 定义一个包含各预设模型(如 'deepseek', 'gemini', 'mistral', 'tongyi-...', 'volcano-...')详细配置的 `predefinedConfigs` 对象。 * 每个模型的配置包括 `endpoint`, `modelName`, `headers`, `bodyBuilder`, `responseExtractor`。 * - 检查选定的 `model` 是否在 `predefinedConfigs` 中,如果不在则抛出错误。 * - 调用 `buildPredefinedApiConfig` 生成配置。 * 5. **构建请求体 (`requestBody`)**: * - 使用 `apiConfig.bodyBuilder` (如果存在) 并传入 `systemPrompt` 和 `userPrompt` 来构建请求体。 * - 如果 `bodyBuilder` 不存在,则构建一个通用的包含 `model` 和 `messages` (system + user) 的请求体。 * 6. **调用翻译 API**: * - 调用 `callTranslationApi` (应为实际的 fetch 调用封装) 并传入 `apiConfig` 和 `requestBody`,获取翻译结果 `result`。 * 7. **表格后处理** (如果之前进行了表格保护且 `actualProcessTablePlaceholders` 为 true 且 `extractTableFromTranslation` 函数可用): * - **逐个翻译表格内容**: * - 遍历 `tablePlaceholders` 中的每个原始表格。 * - 为每个表格构建特定的翻译提示(强调保持结构,仅翻译文本)。 * - 再次调用 `callTranslationApi` 翻译该表格。 * - 使用 `extractTableFromTranslation` 从翻译结果中提取纯净的表格 Markdown。 * - 在主翻译结果 `finalResult` (初始为 `result`) 中,用翻译后的表格替换其占位符。 * - 如果表格翻译或提取失败,则用原始表格替换占位符作为兜底。 * - 返回包含已翻译并恢复表格的 `finalResult`。 * 8. **直接返回结果**:如果未进行表格处理,则直接返回步骤 6 中得到的 `result`。 * * @param {string} markdown - 待翻译的 Markdown 文本块。 * @param {string} targetLang - 目标翻译语言代码 (如 'zh-CN', 'en')。 * @param {string} model - 使用的翻译模型名称 (如 'mistral', 'custom', 'deepseek')。 * @param {string} apiKey - 对应翻译模型的 API 密钥。 * @param {string} [logContext=""] - (或 `modelConfig` 当 `model`='custom') 日志记录的上下文前缀。如果 `model` 为 "custom",此位置应为 `modelConfig` 对象,后续参数顺延。 * @param {string} [defaultSystemPrompt=""] - (顺延参数) 翻译时使用的默认系统提示词。 * @param {string} [defaultUserPromptTemplate=""] - (顺延参数) 翻译时使用的默认用户提示词模板 (应包含 `${content}` 和 `${targetLangName}` 占位符)。 * @param {boolean} [useCustomPrompts=false] - (顺延参数) 是否使用用户自定义的提示词。 * @param {boolean} [processTablePlaceholders=true] - (顺延参数) 是否对文本中的 Markdown 表格进行占位符保护和独立翻译处理。 * @returns {Promise} 翻译后的 Markdown 文本块。如果处理了表格,则表格内容也会被翻译并恢复到文本中。 * @throws {Error} 如果模型名称不支持、自定义模型配置不完整,或在API调用过程中发生不可恢复的错误。 */ async function translateMarkdown( markdown, targetLang, model, apiKey, logContext = "", defaultSystemPrompt = "", defaultUserPromptTemplate = "", useCustomPrompts = false, processTablePlaceholders = true, options = {} ) { //console.log('translateMarkdown 分块内容:', markdown); let actualLogContext = logContext; let actualDefaultSystemPrompt = defaultSystemPrompt; let actualDefaultUserPromptTemplate = defaultUserPromptTemplate; let actualUseCustomPrompts = useCustomPrompts; let actualProcessTablePlaceholders = processTablePlaceholders; let modelConfigForCustom = null; if (model === "custom") { modelConfigForCustom = arguments[4]; // 这是从调用处传来的 modelConfig actualLogContext = arguments[5] !== undefined ? arguments[5] : ""; actualDefaultSystemPrompt = arguments[6] !== undefined ? arguments[6] : ""; actualDefaultUserPromptTemplate = arguments[7] !== undefined ? arguments[7] : ""; actualUseCustomPrompts = arguments[8] !== undefined ? arguments[8] : false; actualProcessTablePlaceholders = arguments[9] !== undefined ? arguments[9] : true; options = arguments[10] !== undefined ? arguments[10] : {}; } // 表格预处理 - 仅当需要处理表格时 let processedText = markdown; let tablePlaceholders = {}; let hasProtectedTables = false; if (actualProcessTablePlaceholders && typeof protectMarkdownTables === 'function') { const processed = protectMarkdownTables(markdown); processedText = processed.processedText; tablePlaceholders = processed.tablePlaceholders; hasProtectedTables = Object.keys(tablePlaceholders).length > 0; if (hasProtectedTables) { console.log(`${actualLogContext} 检测到 ${Object.keys(tablePlaceholders).length} 个表格,已进行特殊保护`); if (typeof addProgressLog === "function") { addProgressLog(`${actualLogContext} 检测到 ${Object.keys(tablePlaceholders).length} 个表格,将作为整体处理`); } } } // 构建 prompt - 集成提示词池支持 let systemPrompt = actualDefaultSystemPrompt; let userPrompt = actualDefaultUserPromptTemplate; // 增加表格处理提示 let tableHandlingNote = ""; if (hasProtectedTables) { tableHandlingNote = "\n\n注意:文档中的表格已被特殊标记为占位符(如__TABLE_PLACEHOLDER_0__),请直接翻译占位符以外的内容,保持占位符不变。表格将在后续步骤中单独处理。"; } // 检查是否应该使用提示词池(支持外部绑定覆盖) let usePromptPool = false; let promptFromPool = null; // 尝试从提示词池UI获取提示词(如果可用) if (!options.boundPrompt && typeof window !== 'undefined' && window.promptPoolUI) { const poolPrompt = window.promptPoolUI.getPromptForTranslation(); if (poolPrompt) { // 仅当提示词非空时才启用提示词池 const sys = (poolPrompt.systemPrompt || '').trim(); const usr = (poolPrompt.userPromptTemplate || '').trim(); if (sys && usr) { promptFromPool = poolPrompt; usePromptPool = true; } } } // 如果调用方传入 boundPrompt,则优先使用 if (options.boundPrompt && options.boundPrompt.id && options.boundPrompt.systemPrompt && options.boundPrompt.userPromptTemplate) { promptFromPool = options.boundPrompt; usePromptPool = true; } // 根据提示词来源设置提示词 if (usePromptPool && promptFromPool) { // 使用提示词池的提示词(再次防御非空) const sys = (promptFromPool.systemPrompt || '').trim(); const usr = (promptFromPool.userPromptTemplate || '').trim(); if (sys && usr) { systemPrompt = sys + tableHandlingNote; userPrompt = usr; console.log(`[翻译] 使用提示词池的提示词`); } else { usePromptPool = false; // 回退 } } if (!usePromptPool) { if (!actualUseCustomPrompts || !systemPrompt || !userPrompt) { // 使用内置模板或后备方案 if (typeof getBuiltInPrompts === "function") { const prompts = getBuiltInPrompts(targetLang); systemPrompt = prompts.systemPrompt + tableHandlingNote; userPrompt = prompts.userPromptTemplate; } else { // 兜底 systemPrompt = "You are a professional document translation assistant." + tableHandlingNote; userPrompt = "Please translate the following content into the target language:\n\n${content}"; } } else { // 使用自定义提示词(再次检查非空) const sys = (actualDefaultSystemPrompt || '').trim(); const usr = (actualDefaultUserPromptTemplate || '').trim(); if (sys && usr) { systemPrompt = sys + tableHandlingNote; userPrompt = usr; } else { // 回退到内置 if (typeof getBuiltInPrompts === "function") { const prompts = getBuiltInPrompts(targetLang); systemPrompt = prompts.systemPrompt + tableHandlingNote; userPrompt = prompts.userPromptTemplate; } else { systemPrompt = "You are a professional document translation assistant." + tableHandlingNote; userPrompt = "Please translate the following content into the target language:\n\n${content}"; } } } } // 注入:翻译备择库(术语库)命中与提示注入 try { const settingsForGlossary = (typeof loadSettings === 'function') ? loadSettings() : {}; const glossaryEnabled = !!settingsForGlossary.enableGlossary; if (glossaryEnabled && typeof getGlossaryMatchesForText === 'function') { const matches = getGlossaryMatchesForText(processedText); if (matches && matches.length > 0) { const instr = (typeof buildGlossaryInstruction === 'function') ? buildGlossaryInstruction(matches, targetLang) : ''; if (instr) { systemPrompt = (systemPrompt || '') + "\n\n" + instr; // 获取实际限制的数量 let actualLimit = 50; // 默认 if (typeof loadGlossarySets === 'function') { const sets = loadGlossarySets(); const setIds = Object.keys(sets || {}); for (const id of setIds) { const set = sets[id]; if (set && set.enabled && set.maxTermsInPrompt) { actualLimit = set.maxTermsInPrompt; break; } } } const actualCount = Math.min(matches.length, actualLimit); if (typeof addProgressLog === 'function') { const names = matches.slice(0, 6).map(m => m.term).join(', '); const filterInfo = matches.length > actualCount ? ` (已过滤至 ${actualCount} 条)` : ''; addProgressLog(`${actualLogContext} 命中术语库 ${matches.length} 条${filterInfo}:${names}${matches.length>6?'...':''}`); } } } } } catch (e) { console.warn('Glossary injection skipped due to error:', e); } // 替换模板变量 - 使用预处理后的文本 userPrompt = userPrompt .replace(/\$\{targetLangName\}/g, targetLang) .replace(/\$\{content\}/g, processedText); if (!userPrompt.includes(processedText)) { console.warn('警告:当前 userPrompt 模板未包含 ${content} 占位符,AI 无法获得正文内容!'); //console.warn('当前 userPrompt:', userPrompt); //console.warn('当前 processedText:', processedText); } //console.log('translateMarkdown defaultUserPromptTemplate:', actualDefaultUserPromptTemplate); //console.log('translateMarkdown userPrompt (before replace):', userPrompt); // 构建 API 配置 let apiConfig; if (model === "custom") { // 使用 modelConfigForCustom if (!modelConfigForCustom || (!modelConfigForCustom.apiEndpoint && !modelConfigForCustom.apiBaseUrl) || !modelConfigForCustom.modelId) { throw new Error('Custom model configuration is incomplete. API Endpoint (或 apiBaseUrl) and Model ID are required.'); } //console.log('translateMarkdown activeModelConfig:', modelConfigForCustom); apiConfig = buildCustomApiConfigForTranslation( apiKey, modelConfigForCustom.apiEndpoint || modelConfigForCustom.apiBaseUrl, modelConfigForCustom.modelId, modelConfigForCustom.requestFormat || 'openai', modelConfigForCustom.temperature !== undefined ? modelConfigForCustom.temperature : 0.5, modelConfigForCustom.max_tokens !== undefined ? modelConfigForCustom.max_tokens : 8000, { endpointMode: modelConfigForCustom.endpointMode || 'auto' } ); } else { // 预设模型 // 获取翻译参数 const settings = typeof loadSettings === "function" ? loadSettings() : {}; const temperature = (settings.customModelSettings && settings.customModelSettings.temperature) || 0.5; const maxTokens = (settings.customModelSettings && settings.customModelSettings.max_tokens) || 8000; // 允许从配置覆盖部分预设端点/模型(例如 Gemini 选择具体模型) 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) { /* ignore and use default */ } 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 = { 'aliyun': { // 所有翻译请求都指向后端代理,由后端决定使用哪个模型 endpoint: (typeof window !== 'undefined' && window.ProxyConfig) ? window.ProxyConfig.getLLMProxyUrl('tongyi', '/v1/chat/completions') : ((window.PBX_PROXY_BASE_URL || 'http://localhost:3456') + '/api/llm/tongyi/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 } }; // 检查选择的模型是否在预设配置中 if (!predefinedConfigs[model]) { throw new Error(`不支持的翻译模型: ${model}`); } apiConfig = buildPredefinedApiConfig(predefinedConfigs[model], apiKey); } // 构建请求体 const bodyBuilderContext = { processedText, targetLang, originalText: markdown, hasProtectedTables, tablePlaceholders, options, requestType: 'initial', instructionBlock: buildInstructionBlock(systemPrompt) }; const requestBody = apiConfig.bodyBuilder ? apiConfig.bodyBuilder(systemPrompt, userPrompt, bodyBuilderContext) : { model: apiConfig.modelName, messages: [ { role: "system", content: systemPrompt }, { role: "user", content: userPrompt } ] }; // 实际调用(记录提示词池使用成功率 + 队列入队/出队) let result; const poolPromptId = (usePromptPool && promptFromPool && promptFromPool.id) ? promptFromPool.id : null; // 入队(如调用方未预入队,则在此兜底入队) const requestId = options.requestId || `req_${Date.now()}_${Math.random().toString(36).slice(2,8)}`; if (!options.requestId && poolPromptId && typeof window.translationPromptPool !== 'undefined' && typeof window.translationPromptPool.enqueueRequest === 'function') { window.translationPromptPool.enqueueRequest(poolPromptId, { requestId, model: apiConfig.modelName || 'unknown' }); } const startTimeMs = Date.now(); let primaryError = null; try { // 出队(开始执行,不再算“待迁移”) if (poolPromptId && typeof window.translationPromptPool !== 'undefined' && typeof window.translationPromptPool.dequeueRequest === 'function') { window.translationPromptPool.dequeueRequest(poolPromptId, requestId); } result = await callTranslationApi(apiConfig, requestBody); result = stripInstructionBlocks(result); if (poolPromptId && typeof window.translationPromptPool !== 'undefined' && typeof window.translationPromptPool.recordPromptUsage === 'function') { window.translationPromptPool.recordPromptUsage( poolPromptId, true, Date.now() - startTimeMs, null, { model: apiConfig.modelName || 'unknown', endpoint: apiConfig.endpoint || '' } ); } } catch (e) { // 出队(失败也确保不再算“待迁移”) if (poolPromptId && typeof window.translationPromptPool !== 'undefined' && typeof window.translationPromptPool.dequeueRequest === 'function') { window.translationPromptPool.dequeueRequest(poolPromptId, requestId); } if (poolPromptId && typeof window.translationPromptPool !== 'undefined' && typeof window.translationPromptPool.recordPromptUsage === 'function') { window.translationPromptPool.recordPromptUsage( poolPromptId, false, Date.now() - startTimeMs, e && e.message ? e.message : String(e), { model: apiConfig.modelName || 'unknown', endpoint: apiConfig.endpoint || '' } ); } primaryError = e; } // 即时切换并重试一次(谨慎):仅在提示词池模式、允许失败切换、存在健康替代时执行 if (!result && poolPromptId && typeof window.translationPromptPool !== 'undefined') { try { const cfgOk = (typeof window.translationPromptPool.getHealthConfig === 'function') ? window.translationPromptPool.getHealthConfig() : null; const canSwitch = cfgOk && cfgOk.switchOnFailure; const newPrompt = (typeof window.translationPromptPool.selectHealthyPrompt === 'function') ? window.translationPromptPool.selectHealthyPrompt(poolPromptId) : null; if (canSwitch && newPrompt && newPrompt.id !== poolPromptId) { if (typeof addProgressLog === 'function') { addProgressLog(`${actualLogContext} 首次失败,尝试切换至健康提示词并重试一次...`); } // 重建基于新提示词的 prompts let retrySystemPrompt = (newPrompt.systemPrompt || '') + tableHandlingNote; let retryUserPrompt = (newPrompt.userPromptTemplate || '') .replace(/\$\{targetLangName\}/g, targetLang) .replace(/\$\{content\}/g, processedText); // 重新评估术语库命中并注入(确保重试也带有一致的术语指引) try { const st2 = (typeof loadSettings === 'function') ? loadSettings() : {}; if (st2 && st2.enableGlossary && typeof getGlossaryMatchesForText === 'function') { const matches2 = getGlossaryMatchesForText(processedText); if (matches2 && matches2.length > 0 && typeof buildGlossaryInstruction === 'function') { const instr2 = buildGlossaryInstruction(matches2, targetLang); if (instr2) retrySystemPrompt = retrySystemPrompt + "\n\n" + instr2; } } } catch (e) { console.warn('Glossary injection (retry) skipped:', e); } // 入队 + 出队(重试请求) const retryRequestId = `${requestId}_r1`; if (typeof window.translationPromptPool.enqueueRequest === 'function') { window.translationPromptPool.enqueueRequest(newPrompt.id, { requestId: retryRequestId, model: apiConfig.modelName || 'unknown' }); } if (typeof window.translationPromptPool.dequeueRequest === 'function') { window.translationPromptPool.dequeueRequest(newPrompt.id, retryRequestId); } const retryBody = apiConfig.bodyBuilder ? apiConfig.bodyBuilder(retrySystemPrompt, retryUserPrompt, { ...bodyBuilderContext, requestType: 'retry' }) : { model: apiConfig.modelName, messages: [ { role: 'system', content: retrySystemPrompt }, { role: 'user', content: retryUserPrompt } ] }; const retryStart = Date.now(); try { result = await callTranslationApi(apiConfig, retryBody); result = stripInstructionBlocks(result); if (typeof window.translationPromptPool.recordPromptUsage === 'function') { window.translationPromptPool.recordPromptUsage( newPrompt.id, true, Date.now() - retryStart, null, { model: apiConfig.modelName || 'unknown', endpoint: apiConfig.endpoint || '' } ); } if (typeof window !== 'undefined' && window.isProcessing && window.promptPoolUI) { // 将会话锁定到新的健康提示词 window.promptPoolUI.sessionLockedPrompt = newPrompt; } if (typeof addProgressLog === 'function') { addProgressLog(`${actualLogContext} 重试成功。`); } } catch (e2) { if (typeof window.translationPromptPool.recordPromptUsage === 'function') { window.translationPromptPool.recordPromptUsage( newPrompt.id, false, Date.now() - retryStart, e2 && e2.message ? e2.message : String(e2), { model: apiConfig.modelName || 'unknown', endpoint: apiConfig.endpoint || '' } ); } if (typeof addProgressLog === 'function') { addProgressLog(`${actualLogContext} 重试失败:${e2.message}`); } } } } catch (swErr) { // 保守处理:任何切换逻辑错误都不影响主异常流 console.warn('Immediate switch-retry failed silently:', swErr); } } if (!result) { // 两次均失败,抛出原始异常 throw primaryError || new Error('调用翻译 API 失败'); } // 如果存在表格保护处理且需要处理表格占位符,恢复表格 if (hasProtectedTables && actualProcessTablePlaceholders && typeof extractTableFromTranslation === 'function') { if (typeof addProgressLog === "function") { addProgressLog(`${actualLogContext} 翻译主文本完成,正在处理表格...`); } // 获取表格翻译结果并替换 let finalResult = result; for (const [placeholder, tableContent] of Object.entries(tablePlaceholders)) { try { const tableSystemPrompt = `你是一个精确翻译表格的助手。请将表格翻译成${targetLang},严格保持以下格式要求: 1. 保持所有表格分隔符(|)和结构完全不变 2. 保持表格对齐标记(:--:、:--、--:)不变 3. 保持表格的行数和列数完全一致 4. 保持数学公式、符号和百分比等专业内容不变 5. 翻译表格标题(如有)和表格内的文本内容 6. 表格内容与表格外内容要明确区分`; const tableUserPrompt = `请将以下Markdown表格翻译成${targetLang},请确保完全保持表格结构和格式: ${tableContent} 注意:请保持表格格式完全不变,包括所有的 | 符号、对齐标记、数学公式和符号。`; const tableRequestBody = apiConfig.bodyBuilder ? apiConfig.bodyBuilder(tableSystemPrompt, tableUserPrompt, { processedText: tableContent, rawText: tableContent, targetLang, tablePlaceholder: placeholder, requestType: 'table' }) : { model: apiConfig.modelName, messages: [ { role: "system", content: tableSystemPrompt }, { role: "user", content: tableUserPrompt } ] }; if (typeof addProgressLog === "function") { addProgressLog(`${actualLogContext} 正在翻译表格...`); } const translatedTable = await callTranslationApi(apiConfig, tableRequestBody); const cleanedTable = extractTableFromTranslation(stripInstructionBlocks(translatedTable)) || tableContent; finalResult = finalResult.replace(placeholder, cleanedTable); } catch (tableError) { console.error(`表格翻译失败:`, tableError); if (typeof addProgressLog === "function") { addProgressLog(`${actualLogContext} 表格翻译失败: ${tableError.message},将使用原表格`); } finalResult = finalResult.replace(placeholder, tableContent); } } if (typeof addProgressLog === "function") { addProgressLog(`${actualLogContext} 表格处理完成。`); } return finalResult; } return result; } // 将函数添加到processModule对象 if (typeof processModule !== 'undefined') { processModule.buildPredefinedApiConfig = buildPredefinedApiConfig; processModule.buildCustomApiConfig = buildCustomApiConfigForTranslation; // 使用重命名后的函数 processModule.translateMarkdown = translateMarkdown; processModule.stripInstructionBlocks = stripInstructionBlocks; processModule.buildInstructionBlock = buildInstructionBlock; }