您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
选中文本后调用 OpenAI Compatible API 将其翻译为中文,支持历史记录、收藏夹及整页翻译
当前为
// ==UserScript== // @name 大模型中文翻译助手 // @name:en LLM powered WebPage Translator to Chinese // @namespace http://tampermonkey.net/ // @version 1.3.14 // @description 选中文本后调用 OpenAI Compatible API 将其翻译为中文,支持历史记录、收藏夹及整页翻译 // @description:en Select text and call OpenAI Compatible API to translate it to Chinese, supports history, favorites and full page translation // @author tzh // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @license MIT // ==/UserScript== (function () { 'use strict'; // 可配置的设置 const defaultSettings = { apiEndpoint: 'https://api.deepseek.com/v1/chat/completions', // 默认 OpenAI API 端点 apiKey: '', // 设置您的 API 密钥 model: 'deepseek-chat', // 可以根据需要更改为其他模型 systemPrompt: '你是一个翻译助手。我会为你提供待翻译的文本,以及之前已经翻译过的上下文(如果有)。请参考这些上下文,将文本准确地翻译成中文,保持原文的意思、风格和格式。在充分保留原文意思的情况下使用符合中文习惯的表达。只返回翻译结果,不需要解释。', useStreaming: false, // 默认启用流式响应 temperature: 0.3, // 控制翻译结果的随机性,值越低越准确,值越高越有创意 maxHistoryItems: 50, // 最大历史记录数 maxFavoritesItems: 100, // 最大收藏数 showSourceLanguage: false, // 是否显示源语言 autoDetectLanguage: true, // 是否自动检测语言 detectArticleContent: true, // 是否自动识别文章主体内容 contextSize: 3, // 翻译时使用的上下文数量 useTranslationContext: true, // 是否启用翻译上下文 fullPageTranslationSelector: 'body', // 整页翻译时的选择器 fullPageMaxSegmentLength: 2000, // 整页翻译时的最大分段长度 excludeSelectors: 'script, style, noscript, iframe, img, svg, canvas', // 翻译时排除的元素 apiConfigs: [ // 多API配置 { name: 'DeepSeek', apiEndpoint: 'https://api.deepseek.com/v1/chat/completions', apiKey: '', model: 'deepseek-chat', } ], currentApiIndex: 0, // 当前使用的API索引 currentTab: 'general', // 默认显示普通设置标签 }; // 初始化设置 let settings = GM_getValue('translatorSettings', defaultSettings); // 全文翻译相关状态 let isTranslatingFullPage = false; // 是否正在进行全文翻译 let isTranslationPaused = false; // 是否已暂停翻译 let translationSegments = []; // 存储页面分段内容 let lastTranslatedIndex = -1; // 最后翻译的段落索引 let originalTexts = []; // 存储原始文本 // 确保设置中包含apiConfigs字段 if (!settings.apiConfigs) { settings.apiConfigs = [ { name: '默认API', apiEndpoint: settings.apiEndpoint, apiKey: settings.apiKey, model: settings.model, } ]; settings.currentApiIndex = 0; GM_setValue('translatorSettings', settings); } // 从当前选择的API配置中同步主要设置 function syncApiSettings() { if (settings.apiConfigs && settings.apiConfigs.length > 0 && settings.currentApiIndex >= 0 && settings.currentApiIndex < settings.apiConfigs.length) { const currentApi = settings.apiConfigs[settings.currentApiIndex]; const oldEndpoint = settings.apiEndpoint; const oldModel = settings.model; settings.apiEndpoint = currentApi.apiEndpoint; settings.apiKey = currentApi.apiKey; settings.model = currentApi.model; // 检查是否有变化 if (oldEndpoint !== settings.apiEndpoint || oldModel !== settings.model) { console.log(`API设置已同步 - 索引: ${settings.currentApiIndex}, 端点: ${settings.apiEndpoint}, 模型: ${settings.model} (变更: ${oldEndpoint} -> ${settings.apiEndpoint}, ${oldModel} -> ${settings.model})`); } else { console.log(`API设置已同步 - 索引: ${settings.currentApiIndex}, 端点: ${settings.apiEndpoint}, 模型: ${settings.model} (无变更)`); } GM_setValue('translatorSettings', settings); } else { console.warn('无法同步API设置 - 无效的配置或索引', { hasConfigs: Boolean(settings.apiConfigs), configsLength: settings.apiConfigs ? settings.apiConfigs.length : 0, currentIndex: settings.currentApiIndex }); } } // 初始同步 syncApiSettings(); let translationHistory = GM_getValue('translationHistory', []); let translationFavorites = GM_getValue('translationFavorites', []); // 跟踪当前活跃的翻译按钮 let activeTranslateButton = null; let lastSelectedText = ''; let lastSelectionRect = null; // 监听整个文档的mousedown事件,防止点击翻译按钮时丢失选择 document.addEventListener('mousedown', function (e) { // 检查点击是否发生在翻译按钮上 if (e.target.classList.contains('translate-button') || e.target.closest('.translate-button')) { e.stopPropagation(); e.preventDefault(); } }, true); // 使用捕获阶段,确保在其他处理程序之前执行 // 语言检测函数 function detectLanguage(text) { return new Promise((resolve) => { // 简单的语言检测逻辑 const chineseRegex = /[\u4e00-\u9fa5]/; const englishRegex = /[a-zA-Z]/; const japaneseRegex = /[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/; const koreanRegex = /[\uAC00-\uD7AF\u1100-\u11FF]/; if (chineseRegex.test(text)) resolve('中文'); else if (englishRegex.test(text)) resolve('英语'); else if (japaneseRegex.test(text)) resolve('日语'); else if (koreanRegex.test(text)) resolve('韩语'); else resolve('未知'); }); } // 创建设置面板 function createSettingsPanel() { const settingsPanel = document.createElement('div'); settingsPanel.id = 'translator-settings-panel'; settingsPanel.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 500px; background-color: white; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); z-index: 10000; height: 90vh; display: none; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-size: 14px; line-height: 1.5; color: #333; `; // 创建Tab栏 const tabsHtml = ` <div class="settings-header" style="padding: 0; flex: 0 0 auto;"> <div class="settings-tabs" style="border-bottom: 1px solid #eee;"> <div class="tab-buttons" style="display: flex;"> <button id="general-tab-btn" class="tab-btn ${settings.currentTab === 'general' ? 'active' : ''}" style="flex: 1; padding: 12px 15px; background: none; border: none; border-bottom: 2px solid ${settings.currentTab === 'general' ? '#4285f4' : 'transparent'}; cursor: pointer; color: ${settings.currentTab === 'general' ? '#4285f4' : '#666'}; font-size: 14px; font-weight: 500; outline: none;"> 翻译设置 </button> <button id="api-tab-btn" class="tab-btn ${settings.currentTab === 'api' ? 'active' : ''}" style="flex: 1; padding: 12px 15px; background: none; border: none; border-bottom: 2px solid ${settings.currentTab === 'api' ? '#4285f4' : 'transparent'}; cursor: pointer; color: ${settings.currentTab === 'api' ? '#4285f4' : '#666'}; font-size: 14px; font-weight: 500; outline: none;"> API 管理 </button> </div> </div> </div> `; // 创建内容区域的容器 const contentContainerHtml = ` <div class="settings-content" style="flex: 1 1 auto; overflow-y: auto; padding: 20px;"> <div id="general-tab" class="tab-content" style="display: ${settings.currentTab === 'general' ? 'block' : 'none'};"> <h2 style="margin: 0 0 20px 0; font-size: 18px; font-weight: 500; color: #333;">翻译设置</h2> <div style="margin-bottom: 20px;"> <label for="systemPrompt" style="display: block; margin-bottom: 8px; color: #333; font-weight: 500;">系统提示词:</label> <textarea id="systemPrompt" style="width: 100%; height: 100px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; line-height: 1.5; resize: vertical; font-family: inherit;">${settings.systemPrompt}</textarea> </div> <div style="margin-bottom: 20px;"> <label for="temperature" style="display: block; margin-bottom: 8px; color: #333; font-weight: 500;">随机性(Temperature)</label> <div style="display: flex; align-items: center;"> <input type="range" id="temperature" min="0" max="1" step="0.1" value="${settings.temperature}" style="flex-grow: 1; height: 4px; background: #ddd; border-radius: 2px; outline: none; -webkit-appearance: none;"> <span id="temperature-value" style="margin-left: 15px; min-width: 30px; color: #666;">${settings.temperature}</span> </div> <div style="font-size: 12px; color: #666; margin-top: 8px;"> 值越低翻译越准确,值越高结果越具有创意性。翻译任务建议使用较低的值。 </div> </div> <div style="margin-bottom: 15px;"> <label style="display: flex; align-items: center; color: #333;"> <input type="checkbox" id="showSourceLanguage" ${settings.showSourceLanguage ? 'checked' : ''} style="margin: 0 8px 0 0; width: 16px; height: 16px;"> <span style="font-weight: 500;">显示原文</span> </label> <div style="font-size: 12px; color: #666; margin-top: 8px; margin-left: 24px;"> 启用后将在翻译结果上方显示原文 </div> </div> <div style="margin-bottom: 15px;"> <label style="display: flex; align-items: center; color: #333;"> <input type="checkbox" id="useStreaming" ${settings.useStreaming ? 'checked' : ''} style="margin: 0 8px 0 0; width: 16px; height: 16px;"> <span style="font-weight: 500;">启用流式响应(实时显示翻译结果)</span> </label> <div style="font-size: 12px; color: #666; margin-top: 8px; margin-left: 24px;"> 如果遇到翻译失败问题,可以尝试关闭此选项 </div> </div> <div style="margin-bottom: 15px;"> <label style="display: flex; align-items: center; color: #333;"> <input type="checkbox" id="useTranslationContext" ${settings.useTranslationContext ? 'checked' : ''} style="margin: 0 8px 0 0; width: 16px; height: 16px;"> <span style="font-weight: 500;">启用翻译上下文</span> </label> <div style="font-size: 12px; color: #666; margin-top: 8px; margin-left: 24px;"> 启用后将使用之前翻译过的内容作为上下文,提高翻译连贯性 </div> </div> <div style="margin-bottom: 20px;"> <label for="contextSize" style="display: block; margin-bottom: 8px; color: #333; font-weight: 500;">上下文数量:</label> <div style="display: flex; align-items: center;"> <input type="number" id="contextSize" min="1" max="10" value="${settings.contextSize}" style="width: 60px; padding: 6px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;"> <span style="margin-left: 10px; color: #666;">段落</span> </div> <div style="font-size: 12px; color: #666; margin-top: 8px;"> 使用前面已翻译段落作为上下文提升翻译连贯性,建议设置1-5之间,设置较大值会降低翻译速度,消耗更多API配额 </div> </div> <div style="margin-bottom: 15px;"> <label style="display: flex; align-items: center; color: #333;"> <input type="checkbox" id="detectArticleContent" ${settings.detectArticleContent ? 'checked' : ''} style="margin: 0 8px 0 0; width: 16px; height: 16px;"> <span style="font-weight: 500;">智能识别文章主体内容</span> </label> <div style="font-size: 12px; color: #666; margin-top: 8px; margin-left: 24px;"> 启用后将自动识别文章主要内容区域,避免翻译无关内容 </div> </div> <div style="margin-bottom: 20px;"> <label for="fullPageTranslationSelector" style="display: block; margin-bottom: 8px; color: #333; font-weight: 500;">整页翻译选择器:</label> <input type="text" id="fullPageTranslationSelector" value="${settings.fullPageTranslationSelector}" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;"> <div style="font-size: 12px; color: #666; margin-top: 8px;"> CSS选择器,用于指定翻译哪些区域的内容 </div> </div> <div style="margin-bottom: 20px;"> <label for="excludeSelectors" style="display: block; margin-bottom: 8px; color: #333; font-weight: 500;">排除翻译的元素:</label> <input type="text" id="excludeSelectors" value="${settings.excludeSelectors}" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;"> <div style="font-size: 12px; color: #666; margin-top: 8px;"> CSS选择器,指定要排除翻译的元素 </div> </div> </div> <div id="api-tab" class="tab-content" style="display: ${settings.currentTab === 'api' ? 'block' : 'none'};"> <h2 style="margin: 0 0 20px 0; font-size: 18px; font-weight: 500; color: #333;">API 管理</h2> <button id="add-api-btn" style="width: 100%; padding: 12px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; margin-bottom: 20px;"> + 添加新 API </button> <div id="api-list"> ${settings.apiConfigs.map((api, index) => ` <div class="api-item" style="margin-bottom: 15px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; position: relative; ${index === settings.currentApiIndex ? 'background-color: #f0f8ff; border-color: #4285f4;' : ''}"> <div style="position: absolute; top: 15px; right: 15px;"> ${index === settings.currentApiIndex ? '<span style="color: #4CAF50; font-weight: 500;">✓ 当前使用</span>' : `<button class="use-api-btn" data-index="${index}" style="padding: 4px 12px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 8px; font-size: 12px;">使用</button>` } <button class="edit-api-btn" data-index="${index}" style="padding: 4px 12px; background-color: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 8px; font-size: 12px;">编辑</button> ${settings.apiConfigs.length > 1 ? `<button class="delete-api-btn" data-index="${index}" style="padding: 4px 12px; background-color: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">删除</button>` : '' } </div> <div style="margin-bottom: 8px;"><strong style="color: #333;">名称:</strong> <span style="color: #666;">${api.name}</span></div> <div style="margin-bottom: 8px;"><strong style="color: #333;">端点:</strong> <span style="color: #666;">${api.apiEndpoint}</span></div> <div style="margin-bottom: 8px;"><strong style="color: #333;">密钥:</strong> <span style="color: #666;">${api.apiKey ? '******' + api.apiKey.substring(api.apiKey.length - 4) : '未设置'}</span></div> <div><strong style="color: #333;">模型:</strong> <span style="color: #666;">${api.model}</span></div> </div> `).join('')} </div> </div> </div> `; // 创建API编辑表单 const apiFormHtml = ` <div id="api-form" style="display: none; padding: 20px; overflow-y: auto; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: white;"> <h2 id="api-form-title" style="margin: 0 0 20px 0; font-size: 18px; font-weight: 500; color: #333;">添加 API</h2> <input type="hidden" id="api-form-index" value="-1"> <div style="margin-bottom: 20px;"> <label for="api-name" style="display: block; margin-bottom: 8px; color: #333; font-weight: 500;">API 名称:</label> <input type="text" id="api-name" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;" placeholder="例如:OpenAI、Azure、DeepSeek"> </div> <div style="margin-bottom: 20px;"> <label for="api-endpoint" style="display: block; margin-bottom: 8px; color: #333; font-weight: 500;">API 端点:</label> <input type="text" id="api-endpoint" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;" placeholder="例如:https://api.openai.com/v1/chat/completions"> </div> <div style="margin-bottom: 20px;"> <label for="api-key" style="display: block; margin-bottom: 8px; color: #333; font-weight: 500;">API 密钥:</label> <input type="password" id="api-key" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;" placeholder="输入您的API密钥"> <div style="font-size: 12px; color: #666; margin-top: 8px;"> 编辑现有API时,如不需要修改密钥请留空 </div> </div> <div style="margin-bottom: 20px;"> <label for="api-model" style="display: block; margin-bottom: 8px; color: #333; font-weight: 500;">模型名称:</label> <input type="text" id="api-model" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;" placeholder="例如:gpt-3.5-turbo"> </div> <div style="text-align: right;"> <button id="cancel-api-form" style="padding: 8px 20px; background: #f5f5f5; color: #333; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; margin-right: 10px; cursor: pointer;">取消</button> <button id="save-api-form" style="padding: 8px 20px; background-color: #4285f4; color: white; border: none; border-radius: 4px; font-size: 14px; cursor: pointer;">保存</button> </div> </div> `; // 创建底部按钮区域 const footerHtml = ` <div class="settings-footer" style="padding: 15px 20px; border-top: 1px solid #eee; text-align: right; background: white; border-radius: 0 0 8px 8px; flex: 0 0 auto;"> <button id="cancel-settings" style="padding: 8px 20px; background: #f5f5f5; color: #333; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; margin-right: 10px; cursor: pointer;">取消</button> <button id="save-settings" style="padding: 8px 20px; background-color: #4285f4; color: white; border: none; border-radius: 4px; font-size: 14px; cursor: pointer;">保存</button> </div> `; settingsPanel.innerHTML = tabsHtml + contentContainerHtml + apiFormHtml + footerHtml; document.body.appendChild(settingsPanel); // 添加温度滑块值变化的事件监听器 document.getElementById('temperature').addEventListener('input', function() { document.getElementById('temperature-value').textContent = this.value; }); // 工具函数:更新设置面板视图 function updateSettingsPanelView() { // 更新标签页状态 document.getElementById('general-tab').style.display = settings.currentTab === 'general' ? 'block' : 'none'; document.getElementById('api-tab').style.display = settings.currentTab === 'api' ? 'block' : 'none'; document.getElementById('api-form').style.display = 'none'; // 更新标签按钮样式 const generalTabBtn = document.getElementById('general-tab-btn'); const apiTabBtn = document.getElementById('api-tab-btn'); if (settings.currentTab === 'general') { generalTabBtn.style.borderBottom = '2px solid #4285f4'; generalTabBtn.style.color = '#4285f4'; apiTabBtn.style.borderBottom = '2px solid transparent'; apiTabBtn.style.color = '#666'; } else { apiTabBtn.style.borderBottom = '2px solid #4285f4'; apiTabBtn.style.color = '#4285f4'; generalTabBtn.style.borderBottom = '2px solid transparent'; generalTabBtn.style.color = '#666'; } // 更新表单值 document.getElementById('systemPrompt').value = settings.systemPrompt; document.getElementById('temperature').value = settings.temperature; document.getElementById('temperature-value').textContent = settings.temperature; document.getElementById('showSourceLanguage').checked = settings.showSourceLanguage; document.getElementById('useStreaming').checked = settings.useStreaming; document.getElementById('useTranslationContext').checked = settings.useTranslationContext; document.getElementById('contextSize').value = settings.contextSize; document.getElementById('detectArticleContent').checked = settings.detectArticleContent; document.getElementById('fullPageTranslationSelector').value = settings.fullPageTranslationSelector; document.getElementById('excludeSelectors').value = settings.excludeSelectors; // 更新API列表 updateApiList(); } // 事件处理程序 document.getElementById('general-tab-btn').addEventListener('click', function() { settings.currentTab = 'general'; updateSettingsPanelView(); }); document.getElementById('api-tab-btn').addEventListener('click', function() { settings.currentTab = 'api'; updateSettingsPanelView(); }); // 添加API按钮事件 document.getElementById('add-api-btn').addEventListener('click', function() { document.getElementById('api-tab').style.display = 'none'; document.getElementById('api-form').style.display = 'block'; document.getElementById('api-form-title').textContent = '添加 API'; document.getElementById('api-form-index').value = '-1'; document.getElementById('api-name').value = ''; document.getElementById('api-endpoint').value = ''; document.getElementById('api-key').value = ''; document.getElementById('api-model').value = ''; }); // API表单取消按钮事件 document.getElementById('cancel-api-form').addEventListener('click', function() { document.getElementById('api-form').style.display = 'none'; document.getElementById('api-tab').style.display = 'block'; }); // API表单保存按钮事件 document.getElementById('save-api-form').addEventListener('click', function() { const index = parseInt(document.getElementById('api-form-index').value); const apiConfig = { name: document.getElementById('api-name').value, apiEndpoint: document.getElementById('api-endpoint').value, model: document.getElementById('api-model').value }; const apiKey = document.getElementById('api-key').value; if (apiKey) { apiConfig.apiKey = apiKey; } else if (index !== -1) { apiConfig.apiKey = settings.apiConfigs[index].apiKey; } if (index === -1) { settings.apiConfigs.push(apiConfig); } else { settings.apiConfigs[index] = apiConfig; } document.getElementById('api-form').style.display = 'none'; document.getElementById('api-tab').style.display = 'block'; updateApiList(); }); // 设置面板的取消和保存按钮事件 document.getElementById('cancel-settings').addEventListener('click', function() { settingsPanel.style.display = 'none'; }); document.getElementById('save-settings').addEventListener('click', function() { settings = { ...settings, systemPrompt: document.getElementById('systemPrompt').value, useStreaming: document.getElementById('useStreaming').checked, showSourceLanguage: document.getElementById('showSourceLanguage').checked, detectArticleContent: document.getElementById('detectArticleContent').checked, temperature: parseFloat(document.getElementById('temperature').value), fullPageTranslationSelector: document.getElementById('fullPageTranslationSelector').value, excludeSelectors: document.getElementById('excludeSelectors').value, useTranslationContext: document.getElementById('useTranslationContext').checked, contextSize: parseInt(document.getElementById('contextSize').value) || 3, currentTab: settings.currentTab }; GM_setValue('translatorSettings', settings); settingsPanel.style.display = 'none'; }); // 绑定API列表项的事件处理程序 function bindApiListEvents() { document.querySelectorAll('.use-api-btn').forEach(button => { button.addEventListener('click', function() { const index = parseInt(this.dataset.index); settings.currentApiIndex = index; updateApiList(); }); }); document.querySelectorAll('.edit-api-btn').forEach(button => { button.addEventListener('click', function() { const index = parseInt(this.dataset.index); const api = settings.apiConfigs[index]; document.getElementById('api-form-title').textContent = '编辑 API'; document.getElementById('api-form-index').value = index; document.getElementById('api-name').value = api.name; document.getElementById('api-endpoint').value = api.apiEndpoint; document.getElementById('api-key').value = ''; document.getElementById('api-model').value = api.model; // 在内容区域显示API表单,而不是隐藏内容区域 document.getElementById('api-form').style.display = 'block'; document.getElementById('api-tab').style.display = 'none'; }); }); document.querySelectorAll('.delete-api-btn').forEach(button => { button.addEventListener('click', function() { const index = parseInt(this.dataset.index); if (confirm('确定要删除这个API配置吗?')) { settings.apiConfigs.splice(index, 1); if (settings.currentApiIndex === index) { settings.currentApiIndex = 0; } else if (settings.currentApiIndex > index) { settings.currentApiIndex--; } updateApiList(); } }); }); } // 更新API列表的函数 function updateApiList() { const apiListHtml = settings.apiConfigs.map((api, index) => ` <div class="api-item" style="margin-bottom: 15px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; position: relative; ${index === settings.currentApiIndex ? 'background-color: #f0f8ff; border-color: #4285f4;' : ''}"> <div style="position: absolute; top: 15px; right: 15px;"> ${index === settings.currentApiIndex ? '<span style="color: #4CAF50; font-weight: 500;">✓ 当前使用</span>' : `<button class="use-api-btn" data-index="${index}" style="padding: 4px 12px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 8px; font-size: 12px;">使用</button>` } <button class="edit-api-btn" data-index="${index}" style="padding: 4px 12px; background-color: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 8px; font-size: 12px;">编辑</button> ${settings.apiConfigs.length > 1 ? `<button class="delete-api-btn" data-index="${index}" style="padding: 4px 12px; background-color: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">删除</button>` : '' } </div> <div style="margin-bottom: 8px;"><strong style="color: #333;">名称:</strong> <span style="color: #666;">${api.name}</span></div> <div style="margin-bottom: 8px;"><strong style="color: #333;">端点:</strong> <span style="color: #666;">${api.apiEndpoint}</span></div> <div style="margin-bottom: 8px;"><strong style="color: #333;">密钥:</strong> <span style="color: #666;">${api.apiKey ? '******' + api.apiKey.substring(api.apiKey.length - 4) : '未设置'}</span></div> <div><strong style="color: #333;">模型:</strong> <span style="color: #666;">${api.model}</span></div> </div> `).join(''); document.getElementById('api-list').innerHTML = apiListHtml; bindApiListEvents(); } // 显示和隐藏面板的方法 const show = () => { settingsPanel.style.display = 'flex'; settingsPanel.style.flexDirection = 'column'; updateSettingsPanelView(); }; const hide = () => { settingsPanel.style.display = 'none'; }; return { panel: settingsPanel, show: show, hide: hide, updateView: updateSettingsPanelView }; } function createSettingsButton() { const settingsButton = document.createElement('div'); settingsButton.id = 'translator-settings-button'; settingsButton.innerHTML = '⚙️'; settingsButton.title = '翻译设置'; settingsButton.style.cssText = ` position: fixed; bottom: 20px; right: 20px; width: 40px; height: 40px; background-color: rgba(66, 133, 244, 0.8); color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 20px; cursor: pointer; z-index: 9999; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); `; document.body.appendChild(settingsButton); const settingsPanel = createSettingsPanel(); settingsButton.addEventListener('click', function() { // 显示面板并设置flex布局 settingsPanel.panel.style.display = 'flex'; settingsPanel.panel.style.flexDirection = 'column'; // 强制确保有一个有效的当前标签 if (!settings.currentTab || (settings.currentTab !== 'general' && settings.currentTab !== 'api')) { settings.currentTab = 'general'; } // 由于没有updateSettingsPanelView函数,直接更新当前标签的显示 const generalTab = document.getElementById('general-tab'); const apiTab = document.getElementById('api-tab'); if (generalTab && apiTab) { generalTab.style.display = settings.currentTab === 'general' ? 'block' : 'none'; apiTab.style.display = settings.currentTab === 'api' ? 'block' : 'none'; } }); return settingsButton; } // 创建翻译整页按钮 function createTranslatePageButton() { const button = document.createElement('div'); button.id = 'translate-page-button'; button.innerHTML = '翻译整页'; button.title = '翻译当前页面内容'; button.style.cssText = ` position: fixed; bottom: 20px; right: 70px; padding: 8px 12px; background-color: rgba(66, 133, 244, 0.8); color: white; border-radius: 20px; font-size: 14px; cursor: pointer; z-index: 9999; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); display: flex; align-items: center; justify-content: center; `; document.body.appendChild(button); button.addEventListener('click', function () { if (isTranslatingFullPage) { alert('正在翻译中,请稍候...'); return; } translateFullPage(); }); return button; } // 创建翻译进度条 function createProgressBar() { const progressContainer = document.createElement('div'); progressContainer.id = 'translation-progress-container'; progressContainer.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 4px; background-color: #f3f3f3; z-index: 10001; display: none; `; const progressBar = document.createElement('div'); progressBar.id = 'translation-progress-bar'; progressBar.style.cssText = ` height: 100%; width: 0%; background-color: #4285f4; transition: width 0.3s; `; progressContainer.appendChild(progressBar); document.body.appendChild(progressContainer); return progressContainer; } // 深度优先遍历DOM树 function extractTextNodesFromElement(element, textSegments, depth = 0, excludeElements = null, isInReferencesSection = false, referencesSectionElement = null) { // 如果没有传入排除元素,使用全局的 if (excludeElements === null) { excludeElements = settings.excludeSelectors ? Array.from(document.querySelectorAll(settings.excludeSelectors)) : []; } // 检查是否进入了参考文献区域 if (element === referencesSectionElement) { isInReferencesSection = true; } // 如果这个元素在排除列表中,跳过 if (excludeElements.includes(element)) { return; } try { // 只对元素节点检查样式 if (element.nodeType === Node.ELEMENT_NODE) { // 检查元素是否隐藏 const style = window.getComputedStyle(element); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { return; } // 特殊处理参考文献条目 // 参考文献条目通常以数字加方括号开头,如 [1], [2] 等 if (isInReferencesSection && /^\s*\[\d+\]/.test(element.textContent)) { console.log("检测到参考文献条目:", element.textContent.substring(0, 50)); textSegments.push({ text: element.textContent, element: element, htmlContent: element.innerHTML, isReferenceItem: true, isInReferencesSection: true, original: element.textContent }); return; // 参考文献条目作为整体处理,不再递归子节点 } // 对于块级元素,我们在处理完所有子节点后添加换行标记 const isBlockElement = style.display === 'block' || style.display === 'flex' || style.display === 'grid' || element.tagName === 'DIV' || element.tagName === 'P' || element.tagName === 'SECTION' || element.tagName === 'ARTICLE' || element.tagName === 'LI'; // 递归处理子节点 for (let i = 0; i < element.childNodes.length; i++) { extractTextNodesFromElement(element.childNodes[i], textSegments, depth + 1, excludeElements, isInReferencesSection, referencesSectionElement); } // 添加换行标记 if (isBlockElement && element.textContent.trim() && element.childNodes.length > 0) { textSegments.push({ isNewLine: true }); } } else if (element.nodeType === Node.TEXT_NODE) { // 文本节点 const text = element.textContent.trim(); if (text) { textSegments.push({ text: text, node: element, original: element.textContent, parentElement: element.parentElement, isInReferencesSection: isInReferencesSection }); } } } catch (error) { console.warn("处理元素时出错:", error); } } // 合并文本段落,特殊处理参考文献条目 function mergeTextSegments(textSegments) { // 合并相邻文本段落,特殊处理参考文献条目 const mergedSegments = []; let currentSegment = { text: '', nodes: [], isReferenceItem: false }; let referenceItems = []; for (const segment of textSegments) { // 如果是参考文献条目,单独处理 if (segment.isReferenceItem) { // 如果当前段落有内容,先保存 if (currentSegment.text.trim()) { mergedSegments.push({ ...currentSegment }); currentSegment = { text: '', nodes: [], isReferenceItem: false }; } // 添加参考文献条目 referenceItems.push(segment); mergedSegments.push({ text: segment.text, element: segment.element, htmlContent: segment.htmlContent, isReferenceItem: true, nodes: [{ node: segment.element, original: segment.original }] }); continue; } // 如果是参考文献区域但非条目,可能需要特殊处理 if (segment.isInReferencesSection && !segment.isNewLine) { // 可能属于参考文献区域的普通文本,保持段落结构 if (currentSegment.isInReferencesSection !== true) { // 如果当前段落不是参考文献区域,创建新段落 if (currentSegment.text.trim()) { mergedSegments.push({ ...currentSegment }); } currentSegment = { text: segment.text, nodes: segment.node ? [{ node: segment.node, original: segment.original }] : [], isInReferencesSection: true }; } else { // 属于同一参考文献区域段落,合并文本 currentSegment.text += (currentSegment.text ? ' ' : '') + segment.text; if (segment.node) { currentSegment.nodes.push({ node: segment.node, original: segment.original, parentElement: segment.parentElement }); } } continue; } if (segment.isNewLine) { if (currentSegment.text.trim()) { mergedSegments.push({ ...currentSegment }); currentSegment = { text: '', nodes: [], isReferenceItem: false }; } continue; } currentSegment.text += (currentSegment.text ? ' ' : '') + segment.text; if (segment.node) { currentSegment.nodes.push({ node: segment.node, original: segment.original, parentElement: segment.parentElement }); } // 如果当前段落已经足够长,创建新段落 if (currentSegment.text.length >= settings.fullPageMaxSegmentLength) { mergedSegments.push({ ...currentSegment }); currentSegment = { text: '', nodes: [], isReferenceItem: false }; } } // 添加最后一个段落 if (currentSegment.text.trim()) { mergedSegments.push(currentSegment); } console.log(`提取到${mergedSegments.length}个文本段落,其中参考文献条目${referenceItems.length}个`); return mergedSegments; } // 修改extractPageContent函数,使用合并函数 function extractPageContent() { console.log("开始提取页面内容"); // 如果启用了自动识别文章主体,尝试识别 if (settings.detectArticleContent) { const rawSegments = detectMainContent(); if (rawSegments) { console.log("成功识别到文章主体内容"); return mergeTextSegments(rawSegments); } else { console.log("未能识别出文章主体,回退到常规翻译模式"); } } const contentSelector = settings.fullPageTranslationSelector || 'body'; const contentElement = document.querySelector(contentSelector); if (!contentElement) { console.error(`未找到匹配选择器的元素: ${contentSelector}`); return []; } // 获取所有需要排除的元素 const excludeElements = settings.excludeSelectors ? Array.from(document.querySelectorAll(settings.excludeSelectors)) : []; // 检测参考文献区域 const referencesElements = Array.from(document.querySelectorAll('h2, h3, h4')).filter(el => el.textContent.toLowerCase().includes('reference') || el.textContent.toLowerCase().includes('bibliography') || el.textContent.includes('参考文献') ); let isInReferencesSection = false; let referencesSectionElement = null; if (referencesElements.length > 0) { referencesSectionElement = referencesElements[0]; console.log("检测到参考文献区域:", referencesSectionElement.textContent); } // 存储提取的文本段落 const textSegments = []; // 深度优先遍历DOM树 extractTextNodesFromElement(contentElement, textSegments); // 合并相邻文本段落 return mergeTextSegments(textSegments); } // 识别网页中的文章主体内容 function detectMainContent() { // 可能的文章主体容器选择器 const possibleArticleSelectors = [ 'article', '.article', '.post', '.content', '.post-content', '.article-content', '.entry-content', '.main-content', 'main', '#main', '#content', '.story', '[itemprop="articleBody"]', '[role="main"]', // 添加Divi主题常用的内容容器选择器 '.et_pb_post_content', '.et_pb_module.et_pb_post_content', '.et_pb_text_inner', '.et_pb_post_content_0_tb_body', '.et_builder_inner_content', // 添加更多通用选择器 '.single-content', '.single-post-content', '.page-content', '.post-text' ]; // 尝试这些选择器,寻找包含最多内容的元素 let bestElement = null; let maxTextLength = 0; for (const selector of possibleArticleSelectors) { const elements = document.querySelectorAll(selector); for (const element of elements) { // 计算文本内容长度 const textLength = element.textContent.trim().length; // 如果比之前找到的更长,更新最佳元素 if (textLength > maxTextLength) { maxTextLength = textLength; bestElement = element; } } } // 如果找到了合适的元素 if (bestElement && maxTextLength > 300) { // 降低阈值,从500改为300个字符,更容易识别较短的文章 console.log(`检测到文章主体: ${bestElement.tagName}${bestElement.id ? '#'+bestElement.id : ''}${bestElement.className ? '.'+bestElement.className.replace(/\s+/g, '.') : ''}`); // 获取所有需要排除的元素 const excludeElements = settings.excludeSelectors ? Array.from(document.querySelectorAll(settings.excludeSelectors)) : []; // 检测参考文献区域 const referencesElements = Array.from(bestElement.querySelectorAll('h2, h3, h4')).filter(el => el.textContent.toLowerCase().includes('reference') || el.textContent.toLowerCase().includes('bibliography') || el.textContent.includes('参考文献') ); let isInReferencesSection = false; let referencesSectionElement = null; if (referencesElements.length > 0) { referencesSectionElement = referencesElements[0]; console.log("在文章主体中检测到参考文献区域:", referencesSectionElement.textContent); } // 存储提取的文本段落 const textSegments = []; // 深度优先遍历DOM树 extractTextNodesFromElement(bestElement, textSegments, 0, excludeElements, isInReferencesSection, referencesSectionElement); return textSegments.length > 0 ? textSegments : null; } // 尝试基于内容区域比例的检测策略 if (!bestElement) { console.log("尝试使用内容区域比例检测策略"); // 获取所有段落和文本块 const textBlocks = Array.from(document.querySelectorAll('p, article, .post, .content, div > h1 + p, div > h2 + p')); // 按照父元素对文本块进行分组 const contentGroups = {}; for (const block of textBlocks) { if (!block.textContent.trim()) continue; // 获取所有父元素,直到body let parent = block.parentElement; while (parent && parent.tagName !== 'BODY') { const key = parent.tagName + (parent.id ? '#' + parent.id : '') + (parent.className ? '.' + parent.className.replace(/\s+/g, '.') : ''); contentGroups[key] = contentGroups[key] || { element: parent, textLength: 0 }; contentGroups[key].textLength += block.textContent.trim().length; parent = parent.parentElement; } } // 找出包含最多文本内容的容器 let bestContentGroup = null; let maxGroupTextLength = 0; for (const key in contentGroups) { if (contentGroups[key].textLength > maxGroupTextLength) { maxGroupTextLength = contentGroups[key].textLength; bestContentGroup = contentGroups[key]; } } if (bestContentGroup && maxGroupTextLength > 300) { console.log(`通过内容区域比例检测到文章主体: ${bestContentGroup.element.tagName}${bestContentGroup.element.id ? '#'+bestContentGroup.element.id : ''}${bestContentGroup.element.className ? '.'+bestContentGroup.element.className.replace(/\s+/g, '.') : ''}`); // 获取所有需要排除的元素 const excludeElements = settings.excludeSelectors ? Array.from(document.querySelectorAll(settings.excludeSelectors)) : []; // 存储提取的文本段落 const textSegments = []; // 深度优先遍历DOM树 extractTextNodesFromElement(bestContentGroup.element, textSegments, 0, excludeElements, false, null); return textSegments.length > 0 ? textSegments : null; } } return null; } // 翻译整个页面 function translateFullPage() { // 确保使用最新的API设置 syncApiSettings(); console.log('全文翻译 - 确保使用最新API设置:', settings.apiEndpoint, settings.model); if (!settings.apiKey) { alert('请先在设置中配置API密钥'); const settingsPanel = document.getElementById('translator-settings-panel') || createSettingsPanel(); settingsPanel.style.display = 'block'; return; } // 如果当前已经在翻译中但被暂停了,则恢复翻译 if (isTranslatingFullPage && isTranslationPaused) { isTranslationPaused = false; const pauseBtn = document.getElementById('pause-translation-button'); if (pauseBtn) { pauseBtn.textContent = '暂停翻译'; pauseBtn.title = '暂停当前的翻译任务'; } // 恢复翻译,从最后翻译的段落的下一段开始 translateNextSegment(lastTranslatedIndex + 1); return; } // 如果当前正在翻译且未暂停,则不做任何操作 if (isTranslatingFullPage && !isTranslationPaused) { alert('正在翻译中,请稍候...'); return; } // 开始新的翻译任务 isTranslatingFullPage = true; isTranslationPaused = false; lastTranslatedIndex = -1; // 清除可能存在的暂停/停止按钮 removeControlButtons(); // 清除可能存在的状态提示元素 const existingStatusElement = document.getElementById('translation-status'); if (existingStatusElement) { existingStatusElement.remove(); } // 提取页面内容 translationSegments = extractPageContent(); if (translationSegments.length === 0) { alert('未找到可翻译的内容'); isTranslatingFullPage = false; return; } // 显示进度条 const progressContainer = document.getElementById('translation-progress-container') || createProgressBar(); const progressBar = document.getElementById('translation-progress-bar'); progressContainer.style.display = 'block'; progressBar.style.width = '0%'; // 创建一个状态提示 const statusElement = document.createElement('div'); statusElement.id = 'translation-status'; statusElement.style.cssText = ` position: fixed; top: 10px; left: 50%; transform: translateX(-50%); background-color: rgba(66, 133, 244, 0.9); color: white; padding: 8px 15px; border-radius: 20px; font-size: 14px; z-index: 10001; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); transition: background-color 0.3s; min-width: 200px; text-align: center; `; statusElement.textContent = `正在翻译 (0/${translationSegments.length})`; document.body.appendChild(statusElement); // 记录所有段落的原始文本,用于恢复 originalTexts = translationSegments.map(segment => ({ nodes: segment.nodes.map(n => ({ node: n.node, text: n.original })) })); // 创建用于切换原文/译文的按钮 createToggleButton(); // 创建暂停和停止按钮 createControlButtons(); // 开始翻译第一个段落 translateNextSegment(0); } // 创建暂停和停止按钮 function createControlButtons() { // 创建暂停按钮 const pauseButton = document.createElement('div'); pauseButton.id = 'pause-translation-button'; pauseButton.style.cssText = ` position: fixed; bottom: 20px; right: 310px; padding: 8px 12px; background-color: rgba(255, 152, 0, 0.8); color: white; border-radius: 20px; font-size: 14px; cursor: pointer; z-index: 9999; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); `; pauseButton.textContent = '暂停翻译'; pauseButton.title = '暂停当前的翻译任务'; document.body.appendChild(pauseButton); // 创建停止按钮 const stopButton = document.createElement('div'); stopButton.id = 'stop-translation-button'; stopButton.style.cssText = ` position: fixed; bottom: 20px; right: 240px; padding: 8px 12px; background-color: rgba(244, 67, 54, 0.8); color: white; border-radius: 20px; font-size: 14px; cursor: pointer; z-index: 9999; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); `; stopButton.textContent = '停止翻译'; stopButton.title = '停止当前的翻译任务'; document.body.appendChild(stopButton); // 绑定暂停按钮事件 pauseButton.addEventListener('click', function() { if (!isTranslatingFullPage) return; isTranslationPaused = !isTranslationPaused; if (isTranslationPaused) { pauseButton.textContent = '继续翻译'; pauseButton.title = '继续未完成的翻译任务'; // 更新状态提示 const statusElement = document.getElementById('translation-status'); if (statusElement) { statusElement.textContent = `翻译已暂停 (${lastTranslatedIndex + 1}/${translationSegments.length})`; } // 显示切换按钮,允许在暂停时切换查看原文/译文 const toggleButton = document.getElementById('toggle-translation-button'); if (toggleButton) toggleButton.style.display = 'block'; } else { pauseButton.textContent = '暂停翻译'; pauseButton.title = '暂停当前的翻译任务'; // 恢复翻译 translateNextSegment(lastTranslatedIndex + 1); } }); // 绑定停止按钮事件 stopButton.addEventListener('click', function() { if (!isTranslatingFullPage) return; // 确认是否要停止翻译 if (confirm('确定要停止翻译吗?已翻译的内容将保留。')) { // 停止翻译 isTranslatingFullPage = false; isTranslationPaused = false; // 更新状态提示 const statusElement = document.getElementById('translation-status'); if (statusElement) { statusElement.textContent = `翻译已停止 (${lastTranslatedIndex + 1}/${translationSegments.length})`; // 添加关闭按钮 if (!statusElement.querySelector('.close-btn')) { const closeButton = document.createElement('span'); closeButton.className = 'close-btn'; closeButton.style.cssText = ` margin-left: 10px; cursor: pointer; font-weight: bold; `; closeButton.textContent = '×'; closeButton.addEventListener('click', function() { statusElement.remove(); const progressContainer = document.getElementById('translation-progress-container'); if (progressContainer) progressContainer.style.display = 'none'; }); statusElement.appendChild(closeButton); } } // 显示切换按钮 const toggleButton = document.getElementById('toggle-translation-button'); if (toggleButton) toggleButton.style.display = 'block'; // 删除控制按钮 removeControlButtons(); } }); } // 移除控制按钮 function removeControlButtons() { const pauseButton = document.getElementById('pause-translation-button'); if (pauseButton) pauseButton.remove(); const stopButton = document.getElementById('stop-translation-button'); if (stopButton) stopButton.remove(); } // 创建切换按钮 function createToggleButton() { // 检查是否已存在切换按钮 let toggleButton = document.getElementById('toggle-translation-button'); if (!toggleButton) { toggleButton = document.createElement('div'); toggleButton.id = 'toggle-translation-button'; toggleButton.style.cssText = ` position: fixed; bottom: 20px; right: 180px; padding: 8px 12px; background-color: rgba(66, 133, 244, 0.8); color: white; border-radius: 20px; font-size: 14px; cursor: pointer; z-index: 9999; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); display: none; `; toggleButton.textContent = '查看原文'; toggleButton.dataset.showing = 'translation'; document.body.appendChild(toggleButton); // 用于跟踪当前显示状态 let isShowingTranslation = true; // 创建映射,用于跟踪每个节点的翻译状态 const nodeTranslationMap = new Map(); toggleButton.addEventListener('click', function () { isShowingTranslation = !isShowingTranslation; toggleButton.textContent = isShowingTranslation ? '查看原文' : '查看译文'; toggleButton.dataset.showing = isShowingTranslation ? 'translation' : 'original'; if (isShowingTranslation) { // 恢复译文 translationSegments.forEach((segment, index) => { // 特殊处理参考文献条目 if (segment.isReferenceItem && segment.translation && segment.element) { segment.element.innerHTML = segment.translation; return; } if (segment.translation) { if (segment.nodes.length === 1) { // 单节点情况,直接应用翻译 const nodeInfo = segment.nodes[0]; if (nodeInfo.node && nodeInfo.node.nodeType === Node.TEXT_NODE) { nodeInfo.node.textContent = segment.translation; // 记录这个节点已经被翻译过 nodeTranslationMap.set(nodeInfo.node, true); } } else { // 多节点情况,使用比例分配 const totalOriginalLength = segment.nodes.reduce( (sum, nodeInfo) => sum + (nodeInfo.original ? nodeInfo.original.length : 0), 0); if (totalOriginalLength > 0) { let startPos = 0; for (let i = 0; i < segment.nodes.length; i++) { const nodeInfo = segment.nodes[i]; if (nodeInfo.node && nodeInfo.node.nodeType === Node.TEXT_NODE && nodeInfo.original) { // 计算该节点在原始文本中的比例 const ratio = nodeInfo.original.length / totalOriginalLength; // 计算应该分配给该节点的翻译文本长度 const chunkLength = Math.round(segment.translation.length * ratio); // 提取翻译文本的一部分 let chunk = ''; if (i === segment.nodes.length - 1) { // 最后一个节点,获取剩余所有文本 chunk = segment.translation.substring(startPos); } else { // 非最后节点,按比例获取 chunk = segment.translation.substring(startPos, startPos + chunkLength); startPos += chunkLength; } // 更新节点文本 nodeInfo.node.textContent = chunk; // 记录这个节点已经被翻译过 nodeTranslationMap.set(nodeInfo.node, true); } } } } } }); } else { // 恢复原文 - 使用原始数据而不是依赖当前DOM状态 nodeTranslationMap.clear(); // 清除翻译状态记录 // 先处理参考文献条目 translationSegments.forEach((segment) => { if (segment.isReferenceItem && segment.element && segment.originalHtml) { segment.element.innerHTML = segment.originalHtml; } }); // 再处理普通段落 originalTexts.forEach((originalSegment) => { originalSegment.nodes.forEach(nodeInfo => { if (nodeInfo.node && nodeInfo.node.nodeType === Node.TEXT_NODE) { nodeInfo.node.textContent = nodeInfo.text; } }); }); } }); } return toggleButton; } // 添加一个专门用于翻译参考文献条目的函数 function translateReferenceItem(referenceItem, callback) { if (!referenceItem || !referenceItem.text) { callback(null); return; } console.log("翻译参考文献条目:", referenceItem.text.substring(0, 50) + "..."); // 如果有HTML内容,使用更精确的方法 if (referenceItem.htmlContent) { // 提取HTML中的纯文本部分 const tempDiv = document.createElement('div'); tempDiv.innerHTML = referenceItem.htmlContent; // 递归处理HTML元素,只翻译文本节点 function processNode(node, callback) { if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent.trim(); if (text && text.length > 1) { // 翻译文本节点 translateText(text, function (result) { if (result.type === 'complete' || result.type === 'stream-end') { node.textContent = result.content; callback(true); } else { // 保持原文 callback(false); } }, false); } else { callback(true); // 空文本,直接继续 } } else if (node.nodeType === Node.ELEMENT_NODE) { // 对于元素节点,递归处理其子节点 const childNodes = Array.from(node.childNodes); processNodeSequentially(childNodes, 0, function () { callback(true); }); } else { callback(true); // 其他类型节点,直接继续 } } // 按顺序处理节点列表 function processNodeSequentially(nodes, index, finalCallback) { if (index >= nodes.length) { finalCallback(); return; } processNode(nodes[index], function (success) { // 无论成功与否,继续处理下一个节点 setTimeout(() => processNodeSequentially(nodes, index + 1, finalCallback), 10); }); } // 开始处理整个HTML片段 processNodeSequentially(Array.from(tempDiv.childNodes), 0, function () { // 处理完成后,返回完整的翻译后HTML callback(tempDiv.innerHTML); }); } else { // 如果没有HTML内容,直接翻译文本 translateText(referenceItem.text, function (result) { if (result.type === 'complete' || result.type === 'stream-end') { callback(result.content); } else { callback(null); } }, false); } } // 创建翻译按钮 function createTranslateButton() { const button = document.createElement('div'); button.className = 'translate-button'; button.innerHTML = '翻译'; button.style.cssText = ` position: absolute; background-color: rgba(66, 133, 244, 0.8); color: white; padding: 5px 10px; border-radius: 4px; font-size: 14px; cursor: pointer; z-index: 9999; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); opacity: 0.8; transition: opacity 0.2s; `; button.addEventListener('mouseover', function () { button.style.opacity = '1'; }); button.addEventListener('mouseout', function () { button.style.opacity = '0.8'; }); return button; } // 创建历史记录面板 function createHistoryPanel() { const panel = document.createElement('div'); panel.id = 'translator-history-panel'; panel.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 600px; max-height: 80vh; background-color: white; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); z-index: 10000; display: none; padding: 20px; overflow-y: auto; `; panel.innerHTML = ` <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;"> <h2 style="margin: 0;">翻译历史</h2> <div style="display: flex; gap: 10px;"> <button id="clear-history" style="padding: 5px 10px; color: #666;">清除历史</button> <div id="close-history" style="cursor: pointer; font-size: 20px; color: #666;">×</div> </div> </div> <div id="history-list"></div> `; document.body.appendChild(panel); // 清除历史记录按钮 document.getElementById('clear-history').addEventListener('click', function () { if (confirm('确定要清除所有翻译历史吗?')) { translationHistory = []; GM_setValue('translationHistory', []); updateHistoryList(); } }); // 关闭按钮 document.getElementById('close-history').addEventListener('click', function () { panel.style.display = 'none'; }); return panel; } // 创建收藏夹面板 function createFavoritesPanel() { const panel = document.createElement('div'); panel.id = 'translator-favorites-panel'; panel.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 600px; max-height: 80vh; background-color: white; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); z-index: 10000; display: none; padding: 20px; overflow-y: auto; `; panel.innerHTML = ` <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;"> <h2 style="margin: 0;">收藏夹</h2> <div style="display: flex; gap: 10px;"> <button id="clear-favorites" style="padding: 5px 10px; color: #666;">清除收藏</button> <div id="close-favorites" style="cursor: pointer; font-size: 20px; color: #666;">×</div> </div> </div> <div id="favorites-list"></div> `; document.body.appendChild(panel); // 清除收藏按钮 document.getElementById('clear-favorites').addEventListener('click', function () { if (confirm('确定要清除所有收藏吗?')) { translationFavorites = []; GM_setValue('translationFavorites', []); updateFavoritesList(); } }); // 关闭按钮 document.getElementById('close-favorites').addEventListener('click', function () { panel.style.display = 'none'; }); return panel; } // 更新历史记录列表 function updateHistoryList() { const historyList = document.getElementById('history-list'); if (!historyList) return; if (translationHistory.length === 0) { historyList.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">暂无翻译历史</div>'; return; } // 清除之前的内容 historyList.innerHTML = ''; // 为每个历史记录创建DOM元素 translationHistory.forEach((item, index) => { const historyItem = document.createElement('div'); historyItem.style.cssText = 'border-bottom: 1px solid #eee; padding: 10px 0;'; const header = document.createElement('div'); header.style.cssText = 'display: flex; justify-content: space-between; margin-bottom: 5px;'; const timestamp = document.createElement('span'); timestamp.style.color = '#666'; timestamp.textContent = item.timestamp; const buttons = document.createElement('div'); const copyBtn = document.createElement('button'); copyBtn.textContent = '复制'; copyBtn.style.cssText = 'padding: 2px 5px; margin-right: 5px;'; copyBtn.addEventListener('click', function () { copyTranslation(index); }); const favoriteBtn = document.createElement('button'); favoriteBtn.textContent = '收藏'; favoriteBtn.style.cssText = 'padding: 2px 5px;'; favoriteBtn.addEventListener('click', function () { addToFavorites(translationHistory[index]); favoriteBtn.textContent = '已收藏'; favoriteBtn.disabled = true; }); buttons.appendChild(copyBtn); buttons.appendChild(favoriteBtn); header.appendChild(timestamp); header.appendChild(buttons); const sourceDiv = document.createElement('div'); sourceDiv.style.marginBottom = '5px'; sourceDiv.innerHTML = `<strong>原文:</strong>${item.source}`; const translationDiv = document.createElement('div'); translationDiv.innerHTML = `<strong>译文:</strong>${item.translation}`; historyItem.appendChild(header); historyItem.appendChild(sourceDiv); historyItem.appendChild(translationDiv); historyList.appendChild(historyItem); }); } // 更新收藏夹列表 function updateFavoritesList() { const favoritesList = document.getElementById('favorites-list'); if (!favoritesList) return; if (translationFavorites.length === 0) { favoritesList.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">暂无收藏内容</div>'; return; } // 清除之前的内容 favoritesList.innerHTML = ''; // 为每个收藏项创建DOM元素 translationFavorites.forEach((item, index) => { const favoriteItem = document.createElement('div'); favoriteItem.style.cssText = 'border-bottom: 1px solid #eee; padding: 10px 0;'; const header = document.createElement('div'); header.style.cssText = 'display: flex; justify-content: space-between; margin-bottom: 5px;'; const timestamp = document.createElement('span'); timestamp.style.color = '#666'; timestamp.textContent = item.timestamp; const buttons = document.createElement('div'); const copyBtn = document.createElement('button'); copyBtn.textContent = '复制'; copyBtn.style.cssText = 'padding: 2px 5px; margin-right: 5px;'; copyBtn.addEventListener('click', function () { copyFavorite(index); }); const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除'; deleteBtn.style.cssText = 'padding: 2px 5px;'; deleteBtn.addEventListener('click', function () { removeFromFavorites(index); }); buttons.appendChild(copyBtn); buttons.appendChild(deleteBtn); header.appendChild(timestamp); header.appendChild(buttons); const sourceDiv = document.createElement('div'); sourceDiv.style.marginBottom = '5px'; sourceDiv.innerHTML = `<strong>原文:</strong>${item.source}`; const translationDiv = document.createElement('div'); translationDiv.style.marginBottom = '5px'; translationDiv.innerHTML = `<strong>译文:</strong>${item.translation}`; const sourceUrlDiv = document.createElement('div'); sourceUrlDiv.style.cssText = 'font-size: 12px; color: #666;'; const sourceTitleDiv = document.createElement('div'); sourceTitleDiv.style.marginBottom = '2px'; sourceTitleDiv.innerHTML = '<strong>来源:</strong>'; const sourceLink = document.createElement('a'); sourceLink.href = item.url; sourceLink.target = '_blank'; sourceLink.style.cssText = 'color: #4285f4; text-decoration: none;'; sourceLink.textContent = item.title || item.url; sourceTitleDiv.appendChild(sourceLink); const urlDiv = document.createElement('div'); urlDiv.style.wordBreak = 'break-all'; urlDiv.textContent = item.url; sourceUrlDiv.appendChild(sourceTitleDiv); sourceUrlDiv.appendChild(urlDiv); favoriteItem.appendChild(header); favoriteItem.appendChild(sourceDiv); favoriteItem.appendChild(translationDiv); favoriteItem.appendChild(sourceUrlDiv); favoritesList.appendChild(favoriteItem); }); } // 添加到历史记录 function addToHistory(source, translation) { const timestamp = new Date().toLocaleString(); translationHistory.unshift({ source, translation, timestamp }); // 限制历史记录数量 if (translationHistory.length > settings.maxHistoryItems) { translationHistory.pop(); } GM_setValue('translationHistory', translationHistory); updateHistoryList(); } // 添加到收藏夹 function addToFavorites(item) { // 添加URL信息 item.url = window.location.href; item.title = document.title; // 检查是否已存在 if (!translationFavorites.some(fav => fav.source === item.source)) { translationFavorites.unshift(item); // 限制收藏数量 if (translationFavorites.length > settings.maxFavoritesItems) { translationFavorites.pop(); } GM_setValue('translationFavorites', translationFavorites); updateFavoritesList(); } } // 从收藏夹移除 function removeFromFavorites(index) { translationFavorites.splice(index, 1); GM_setValue('translationFavorites', translationFavorites); updateFavoritesList(); } // 复制翻译结果 function copyTranslation(index) { const item = translationHistory[index]; if (item) { navigator.clipboard.writeText(item.translation).then(() => { alert('已复制到剪贴板!'); }); } } // 复制收藏的翻译 function copyFavorite(index) { const item = translationFavorites[index]; if (item) { navigator.clipboard.writeText(item.translation).then(() => { alert('已复制到剪贴板!'); }); } } // 注册(不可用)菜单命令 GM_registerMenuCommand('翻译整页', translateFullPage); GM_registerMenuCommand('查看翻译历史', function () { const panel = document.getElementById('translator-history-panel') || createHistoryPanel(); panel.style.display = 'block'; updateHistoryList(); }); GM_registerMenuCommand('查看收藏夹', function () { const panel = document.getElementById('translator-favorites-panel') || createFavoritesPanel(); panel.style.display = 'block'; updateFavoritesList(); }); // 创建翻译弹窗 function createTranslationPopup() { // 主容器 const popup = document.createElement('div'); popup.className = 'translation-popup'; popup.style.cssText = ` position: absolute; background-color: white; min-width: 200px; max-width: 400px; max-height: 80vh; display: flex; flex-direction: column; padding: 0; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); z-index: 10000; font-size: 14px; line-height: 1.5; overflow: hidden; user-select: none; /* 防止拖动时选中文本 */ `; // ===== 顶部区域(可拖动) ===== const header = document.createElement('div'); header.className = 'translation-header'; header.style.cssText = ` display: flex; justify-content: space-between; align-items: center; padding: 10px 15px; border-bottom: 1px solid #eee; flex-shrink: 0; cursor: move; /* 指示可拖动 */ background-color: #f9f9f9; /* 轻微背景色以区分可拖动区域 */ border-radius: 8px 8px 0 0; position: relative; /* 确保关闭按钮定位正确 */ `; // 拖动提示图标 const dragHandleIcon = document.createElement('div'); dragHandleIcon.className = 'drag-handle-icon'; dragHandleIcon.innerHTML = '⋮⋮'; dragHandleIcon.style.cssText = ` margin-right: 6px; color: #999; font-size: 10px; transform: rotate(90deg); display: inline-block; `; // 源语言显示区域 const sourceLanguage = document.createElement('div'); sourceLanguage.className = 'source-language'; sourceLanguage.style.cssText = ` color: #666; font-size: 12px; margin-left: 8px; flex-grow: 1; `; sourceLanguage.textContent = '源语言: 加载中...'; // 关闭按钮 const closeButton = document.createElement('div'); closeButton.className = 'close-button'; closeButton.innerHTML = '×'; closeButton.style.cssText = ` width: 20px; height: 20px; text-align: center; line-height: 20px; font-size: 18px; cursor: pointer; color: #666; background-color: white; border-radius: 50%; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-left: 10px; `; // 组装头部 header.appendChild(dragHandleIcon); header.appendChild(sourceLanguage); header.appendChild(closeButton); // ===== 内容区域(可滚动) ===== const contentWrapper = document.createElement('div'); contentWrapper.className = 'content-wrapper'; contentWrapper.style.cssText = ` padding: 10px 15px; flex-grow: 1; overflow-y: auto; max-height: calc(80vh - 90px); /* 减去头部和底部的高度 */ `; // 创建内容区域 const content = document.createElement('div'); content.className = 'translation-content'; content.style.cssText = ` white-space: pre-wrap; `; // 添加内容区域到内容包装器 contentWrapper.appendChild(content); // ===== 底部区域 ===== const footer = document.createElement('div'); footer.className = 'translation-footer'; footer.style.cssText = ` display: flex; justify-content: flex-end; gap: 10px; padding: 10px 15px; border-top: 1px solid #eee; background-color: white; flex-shrink: 0; `; // 复制按钮 const copyButton = document.createElement('button'); copyButton.className = 'copy-button'; copyButton.textContent = '复制译文'; copyButton.style.cssText = ` padding: 5px 10px; background-color: #4285f4; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; `; // 收藏按钮 const favoriteButton = document.createElement('button'); favoriteButton.className = 'favorite-button'; favoriteButton.textContent = '收藏'; favoriteButton.style.cssText = ` padding: 5px 10px; background-color: #fbbc05; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; `; // 添加按钮到底部 footer.appendChild(copyButton); footer.appendChild(favoriteButton); // ===== 组装整个弹窗 ===== popup.appendChild(header); popup.appendChild(contentWrapper); popup.appendChild(footer); // ===== 事件处理 ===== // 绑定事件 - 改进版 let isDragging = false; let offsetX, offsetY; let popupRect = null; // 存储当前的鼠标事件处理函数,用于后续移除 let mouseMoveHandler, mouseUpHandler; // 关闭按钮事件 closeButton.addEventListener('click', function() { if (typeof popup.cleanup === 'function') { popup.cleanup(); } popup.remove(); }); // 复制按钮事件 copyButton.addEventListener('click', function() { const translationText = content.textContent; navigator.clipboard.writeText(translationText) .then(() => { copyButton.textContent = '✓ 已复制'; setTimeout(() => { copyButton.textContent = '复制译文'; }, 2000); }) .catch(err => { console.error('无法复制文本: ', err); }); }); // 收藏按钮事件 favoriteButton.addEventListener('click', function() { const translationText = content.textContent; const sourceText = sourceLanguage.textContent.replace('源语言: ', ''); addToFavorites({ source: sourceText, translation: translationText }); favoriteButton.textContent = '✓ 已收藏'; setTimeout(() => { favoriteButton.textContent = '收藏'; }, 2000); }); // 鼠标按下事件 - 拖动开始 header.addEventListener('mousedown', function(e) { // 如果点击的是关闭按钮,不启动拖动 if (e.target === closeButton) return; isDragging = true; // 获取弹窗的当前位置和尺寸 popupRect = popup.getBoundingClientRect(); // 调整偏移量,只考虑相对于视口的位置,不考虑滚动 offsetX = e.clientX - popupRect.left; offsetY = e.clientY - popupRect.top; // 添加临时样式增强拖动体验 popup.style.transition = 'none'; popup.style.opacity = '0.95'; popup.style.boxShadow = '0 5px 15px rgba(0, 0, 0, 0.3)'; document.body.style.cursor = 'move'; // 防止文本选择和其他默认行为 e.preventDefault(); // 临时添加鼠标移动和释放事件处理函数 mouseMoveHandler = handleMouseMove; mouseUpHandler = handleMouseUp; document.addEventListener('mousemove', mouseMoveHandler); document.addEventListener('mouseup', mouseUpHandler); }); // 鼠标移动处理函数 function handleMouseMove(e) { if (!isDragging) return; // 计算新位置,考虑页面滚动偏移量 const left = e.clientX - offsetX + window.scrollX; const top = e.clientY - offsetY + window.scrollY; // 防止拖出窗口边界 const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; const maxLeft = window.scrollX + windowWidth - 100; // 保留至少100px在视口内 const maxTop = window.scrollY + windowHeight - 50; // 保留至少50px在视口内 // 设置约束后的位置 popup.style.left = `${Math.max(window.scrollX, Math.min(maxLeft, left))}px`; popup.style.top = `${Math.max(window.scrollY, Math.min(maxTop, top))}px`; // 防止事件传递给下层元素 e.stopPropagation(); e.preventDefault(); } // 鼠标释放处理函数 function handleMouseUp() { if (!isDragging) return; isDragging = false; // 恢复正常样式 popup.style.opacity = '1'; popup.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.2)'; document.body.style.cursor = 'auto'; // 移除临时事件处理函数 document.removeEventListener('mousemove', mouseMoveHandler); document.removeEventListener('mouseup', mouseUpHandler); } // 清理函数 - 当弹窗被移除时调用 function cleanup() { // 确保移除所有事件监听器,防止内存泄漏 document.removeEventListener('mousemove', mouseMoveHandler); document.removeEventListener('mouseup', mouseUpHandler); } // 将清理函数和关键元素引用附加到弹窗上,方便外部访问 popup.cleanup = cleanup; popup.content = content; popup.sourceLanguage = sourceLanguage; popup.contentWrapper = contentWrapper; return popup; } // 创建加载动画 function createLoadingAnimation() { const loading = document.createElement('div'); loading.className = 'loading-animation'; loading.style.cssText = ` display: inline-block; width: 20px; height: 20px; border: 3px solid rgba(0, 0, 0, 0.1); border-radius: 50%; border-top-color: #4285f4; animation: spin 1s ease-in-out infinite; margin-right: 10px; `; const style = document.createElement('style'); style.textContent = ` @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; document.head.appendChild(style); return loading; } // 调用 API 进行翻译 function translateText(text, callback, retryWithoutStreaming = false, context = null) { // 确保使用最新的API设置 syncApiSettings(); if (!settings.apiKey) { callback({ type: 'error', content: '错误:请先设置 API 密钥' }); return; } // 调试模式 const debugMode = true; const debugLog = debugMode ? console.log : function () { }; // 创建内容收集器和状态跟踪 let collectedContent = ''; let lastProcessedLength = 0; // 跟踪上次处理的响应长度 let responseBuffer = ''; // 用于存储部分响应,解决跨块JSON问题 // 确定是否使用流式响应 const useStreaming = retryWithoutStreaming ? false : settings.useStreaming; // 通知回调开始处理 if (useStreaming) { callback({ type: 'stream-start', content: '' }); } debugLog(`开始翻译请求,文本长度: ${text.length}, 使用流式响应: ${useStreaming}, 模型: ${settings.model}`); // 准备消息数组 const messages = [ { role: "system", content: settings.systemPrompt + " 请保持原文本的段落格式,每个段落之间应当保留一个空行。" } ]; // 如果有上下文且启用了上下文功能,添加上下文消息 if (settings.useTranslationContext && context && context.length > 0) { messages.push({ role: "system", content: "以下是之前已经翻译的上下文,供你参考以保持翻译的一致性和连贯性:" }); // 添加上下文 context.forEach(ctx => { messages.push({ role: "user", content: ctx.original }); messages.push({ role: "assistant", content: ctx.translation }); }); } // 添加当前要翻译的文本 messages.push({ role: "user", content: text }); debugLog(messages) debugLog(settings.temperature) GM_xmlhttpRequest({ method: 'POST', url: settings.apiEndpoint, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${settings.apiKey}` }, responseType: useStreaming ? 'stream' : '', timeout: 60000, data: JSON.stringify({ model: settings.model, messages: messages, temperature: settings.temperature, stream: useStreaming, top_p: 1, frequency_penalty: 0, presence_penalty: 0, max_tokens: 4000 }), onloadstart: function () { if (useStreaming) { debugLog("流式请求已开始"); } else { debugLog("标准请求已开始"); debugLog(settings.temperature) } }, onprogress: function (response) { if (!useStreaming) return; // 非流式模式不处理进度更新 try { // 获取完整响应文本 const responseText = response.responseText || ''; // 如果响应没变长,不处理 if (!responseText || responseText.length <= lastProcessedLength) { return; } debugLog(`进度更新: 响应总长度=${responseText.length}, 上次处理位置=${lastProcessedLength}`); // 获取新增内容 const newChunk = responseText.substring(lastProcessedLength); debugLog(`接收新数据: ${newChunk.length}字节`); // 更新处理位置要放在处理数据前,防止数据处理期间发生新的onprogress事件 lastProcessedLength = responseText.length; // 追加到缓冲区并处理 responseBuffer += newChunk; // 不同API可能有不同的流式输出格式,我们需要支持多种格式 // 1. OpenAI风格: "data: {\"id\":\"...\",\"choices\":[{\"delta\":{\"content\":\"文本片段\"}}]}\n" // 2. 简单文本块风格: 直接是文本内容 let newContent = ''; let dataMatches = []; // 尝试提取所有"data: {...}"格式的行 const regex = /data: ({.+?})\n/g; let match; // 收集所有匹配项 while ((match = regex.exec(responseBuffer)) !== null) { dataMatches.push(match[0]); try { // 提取JSON部分并解析 const jsonStr = match[1]; const data = JSON.parse(jsonStr); // 从JSON提取文本内容 if (data.choices && data.choices[0]) { if (data.choices[0].delta && data.choices[0].delta.content) { // OpenAI风格的delta格式 newContent += data.choices[0].delta.content; } else if (data.choices[0].text) { // 有些API使用text字段 newContent += data.choices[0].text; } else if (data.choices[0].message && data.choices[0].message.content) { // 完整消息格式 newContent += data.choices[0].message.content; } } } catch (parseError) { // 解析出错,记录但继续处理 debugLog(`JSON解析失败: ${parseError.message}, 内容: ${match[0].substring(0, 50)}...`); } } // 检查是否找到了标准格式的数据行 if (dataMatches.length > 0) { // 从缓冲区中移除已处理的部分,但保留可能不完整的最后部分 const lastMatchEndIndex = regex.lastIndex; if (lastMatchEndIndex > 0 && lastMatchEndIndex < responseBuffer.length) { responseBuffer = responseBuffer.substring(lastMatchEndIndex); } else { responseBuffer = ''; } debugLog(`找到${dataMatches.length}个数据块,提取了${newContent.length}字符的内容`); } else { // 如果没有找到标准格式,尝试按行分割处理 const lines = responseBuffer.split('\n'); // 保留最后一行作为可能的不完整行 responseBuffer = lines.pop() || ''; // 处理每一行 for (const line of lines) { // 尝试提取"data: "后面的内容 if (line.trim().startsWith('data: ')) { try { const content = line.substring(6).trim(); if (content === '[DONE]') { debugLog("收到流结束标记"); continue; } // 尝试解析JSON const data = JSON.parse(content); if (data.choices && data.choices[0]) { if (data.choices[0].delta && data.choices[0].delta.content) { newContent += data.choices[0].delta.content; } else if (data.choices[0].text) { newContent += data.choices[0].text; } else if (data.choices[0].message && data.choices[0].message.content) { newContent += data.choices[0].message.content; } } } catch (e) { // JSON解析失败,可能是普通文本或特殊格式 debugLog(`处理行出错: ${e.message}, 行内容: ${line.substring(0, 50)}...`); } } else if (line.trim() && !line.includes('event:') && !line.includes('id:')) { // 如果不是控制行且非空,可能是直接的文本内容 // 一些API直接发送文本而不是JSON newContent += line + '\n'; } } } // 如果有新内容 if (newContent) { collectedContent += newContent; debugLog(`新增内容: ${newContent.length}字符, 当前总内容: ${collectedContent.length}字符`); // 发送更新 callback({ type: 'stream-progress', content: collectedContent }); } else if (dataMatches.length > 0 || responseBuffer.includes('data:')) { // 找到了数据行但没有提取到内容,可能是API发送的是控制消息 debugLog("收到数据但未提取到新内容,可能是控制消息"); } else { debugLog("本次更新没有提取到新内容"); } } catch (e) { console.error("处理流数据错误:", e); debugLog("错误详情:", e.stack); // 即使出错也更新位置,防止重复处理导致死循环 if (response && response.responseText) { lastProcessedLength = response.responseText.length; } } }, onload: function (response) { try { // 检查HTTP状态码 if (response.status && response.status !== 200) { // 非200状态码,记录错误但不替换原文 console.error(`API返回非200状态码: ${response.status}`); let errorMsg = `API返回错误状态码: ${response.status}`; // 尝试从响应中提取更详细的错误信息 if (response.responseText) { try { const errorData = JSON.parse(response.responseText); if (errorData.error) { errorMsg += ` - ${errorData.error.message || errorData.error.type || JSON.stringify(errorData.error)}`; } } catch (e) { // 无法解析JSON,使用原始响应内容 if (response.responseText.length < 100) { errorMsg += ` - ${response.responseText}`; } } } callback({ type: 'error', content: errorMsg, statusCode: response.status }); return; } // 检查是否已经收集到内容(通过流式API) if (useStreaming && collectedContent) { // 已收集到了内容 - 直接使用收集到的内容 debugLog("onload: 使用已收集的流式内容,长度:", collectedContent.length); callback({ type: 'stream-end', content: collectedContent }); return; } debugLog(`onload: ${useStreaming ? '没有收集到流式内容' : '使用标准响应'}, 尝试处理完整响应`); // 在流式模式下,如果没有responseText但我们仍然到达onload,这可能是一个API限制 if (useStreaming && (!response || typeof response.responseText !== 'string' || response.responseText.trim() === '')) { // 检查是否有响应头,可能表明API连接是成功的 if (response && response.responseHeaders) { // 尝试从缓冲区中恢复,有时onload触发时缓冲区仍有未处理的内容 if (responseBuffer.trim()) { debugLog("onload: 从缓冲区恢复内容,尝试解析"); try { // 处理缓冲区中的内容 let content = ''; const lines = responseBuffer.split('\n').filter(line => line.trim()); for (const line of lines) { if (line.startsWith('data: ') && line !== 'data: [DONE]') { try { const jsonStr = line.substring(6).trim(); const data = JSON.parse(jsonStr); if (data.choices && data.choices[0]) { if (data.choices[0].delta && data.choices[0].delta.content) { content += data.choices[0].delta.content; } else if (data.choices[0].message && data.choices[0].message.content) { content += data.choices[0].message.content; } } } catch (e) { debugLog("缓冲区解析出错:", e.message); } } } if (content) { // 找到了内容,使用它 callback({ type: 'complete', content: content.trim() }); return; } } catch (e) { debugLog("缓冲区处理失败:", e.message); } } if (response.responseHeaders.includes("content-type:text/event-stream")) { debugLog("onload: 检测到有效的SSE响应头,但无responseText。尝试使用非流式模式重试..."); // 自动重试:使用非流式模式 if (!retryWithoutStreaming) { debugLog("切换到非流式模式重试翻译请求"); translateText(text, callback, true); return; } else { // 如果这是重试尝试但仍然失败,则报告错误 throw new Error("即使在非流式模式下,API也未能返回翻译内容。请检查API密钥和网络连接。"); } } else { // 其他内容类型,可能是API错误 throw new Error(`响应没有包含预期的内容。响应头: ${response.responseHeaders.substring(0, 100)}...`); } } else { // 完全无效的响应 throw new Error("响应对象无效或不包含内容。请检查API端点和密钥是否正确。"); } } // 有responseText,处理它 const responseText = response.responseText; debugLog(`获取到响应文本,长度: ${responseText.length}`); // 检查是否是SSE格式(以'data: '开头) if (responseText.trim().startsWith('data: ')) { debugLog("检测到SSE格式响应,解析数据..."); // 处理SSE格式 const lines = responseText.split('\n'); let fullContent = ''; let processedCount = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith('data: ') && line !== 'data: [DONE]') { try { // 提取JSON部分 const jsonData = line.substring(6); const data = JSON.parse(jsonData); processedCount++; if (data.choices && data.choices[0]) { // 对于非流式响应中的SSE格式,提取content if (data.choices[0].delta && data.choices[0].delta.content) { fullContent += data.choices[0].delta.content; } // 支持普通响应格式 else if (data.choices[0].message && data.choices[0].message.content) { fullContent += data.choices[0].message.content; } // 支持一些API的不同格式 else if (data.choices[0].text) { fullContent += data.choices[0].text; } } } catch (e) { console.error("处理单行SSE数据出错:", e, "行内容:", line); } } } debugLog(`解析了${processedCount}行SSE数据,提取了${fullContent.length}字符的内容`); if (fullContent) { callback({ type: 'complete', content: fullContent.trim() }); return; } else { throw new Error("无法从SSE响应中提取内容"); } } // 尝试作为单个JSON对象解析 try { debugLog("尝试解析为标准JSON响应"); const data = JSON.parse(responseText); if (data.error) { callback({ type: 'error', content: `错误:${data.error.message || JSON.stringify(data.error)}` }); } else if (data.choices && data.choices[0]) { // 提取不同格式的内容 let content = ''; if (data.choices[0].message && data.choices[0].message.content) { content = data.choices[0].message.content; } else if (data.choices[0].text) { content = data.choices[0].text; } if (content) { callback({ type: 'complete', content: content.trim() }); } else { throw new Error("API响应中未找到内容字段"); } } else { throw new Error("API响应格式不符合预期"); } } catch (e) { // JSON解析失败,可能是纯文本响应或其他格式 debugLog("JSON解析失败,尝试其他格式:", e.message); // 如果响应看起来像纯文本,直接使用 if (responseText && !responseText.startsWith('{') && !responseText.startsWith('[') && !responseText.includes('<!DOCTYPE')) { debugLog("响应似乎是纯文本,直接返回"); callback({ type: 'complete', content: responseText.trim() }); return; } // 其他情况下,报告错误 throw new Error(`非标准响应格式: ${e.message}`); } } catch (e) { // 安全地获取响应预览,避免undefined错误 let responsePreview = "无法获取响应内容"; try { if (response && typeof response.responseText === 'string') { responsePreview = response.responseText.substring(0, 200); } else if (response) { // 尝试获取响应头信息作为调试参考 responsePreview = JSON.stringify(response).substring(0, 300); } } catch (previewError) { responsePreview = `获取响应预览时出错: ${previewError.message}`; } console.error("响应处理错误:", e.message); console.error("错误详情:", e.stack); console.error("响应信息:", responsePreview); // 给用户一些有用的建议 let errorMessage = e.message; if (e.message.includes("API返回了成功响应") || e.message.includes("无法从SSE响应中提取内容")) { errorMessage += "<br><br>建议:<br>1. 检查API密钥是否有足够的使用额度<br>2. 尝试减少翻译文本长度<br>3. <b>在设置面板中禁用流式响应</b>"; } else if (e.message.includes("非标准响应格式")) { errorMessage += "<br><br>可能原因:<br>1. API响应格式与脚本不兼容<br>2. API返回了错误信息<br>3. API密钥可能无效"; } callback({ type: 'error', content: `解析响应时出错:${errorMessage}<br><br><small>响应信息: ${escapeHtml(responsePreview)}</small>` }); } }, onerror: function (error) { console.error("API请求错误:", error); // 获取状态码 const statusCode = error.status || 0; let errorMessage = error.statusText || '无法连接到 API'; // 添加状态码到错误信息 if (statusCode > 0) { errorMessage = `(${statusCode}) ${errorMessage}`; } // 如果是流式请求失败,尝试非流式请求 if (useStreaming && !retryWithoutStreaming) { debugLog(`流式请求失败,状态码: ${statusCode},尝试使用非流式模式重试...`); translateText(text, callback, true); return; } callback({ type: 'error', content: `请求错误:${errorMessage}`, statusCode: statusCode }); } }); } // 辅助函数:转义HTML,防止XSS function escapeHtml(text) { return text .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } // 显示翻译结果 async function showTranslation(text, rect) { // 确保使用最新的API设置 syncApiSettings(); console.log('显示翻译弹窗 - 确保使用最新API设置:', settings.apiEndpoint, settings.model); // 移除之前的弹窗,确保清理事件处理函数 const oldPopups = document.querySelectorAll('.translation-popup'); oldPopups.forEach(popup => { // 如果有清理函数,调用它 if (popup.cleanup && typeof popup.cleanup === 'function') { popup.cleanup(); } popup.remove(); }); // 创建新弹窗 const popup = createTranslationPopup(); // 通过附加到弹窗上的引用获取内容元素 const content = popup.content; const sourceLanguage = popup.sourceLanguage; const contentWrapper = popup.contentWrapper; // 添加加载动画 const loadingAnimation = createLoadingAnimation(); content.innerHTML = ''; // 清空内容 content.appendChild(loadingAnimation); content.appendChild(document.createTextNode('正在翻译...')); // 先添加到DOM,以便获取尺寸 document.body.appendChild(popup); // 获取窗口尺寸和弹窗尺寸 const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; const popupWidth = popup.offsetWidth; const popupHeight = popup.offsetHeight; // 计算初始位置(选中文字的右侧) let left = window.scrollX + rect.right + 10; // 右侧间隔10px let top = window.scrollY + rect.top; // 检查右侧空间是否足够,如果不够则改为左侧显示 if (left + popupWidth > window.scrollX + windowWidth) { left = window.scrollX + rect.left - popupWidth - 10; // 左侧间隔10px } // 如果左侧也没有足够空间,则居中显示 if (left < window.scrollX) { left = window.scrollX + (windowWidth - popupWidth) / 2; } // 确保弹窗不超出视窗底部 if (top + popupHeight > window.scrollY + windowHeight) { top = window.scrollY + windowHeight - popupHeight - 10; // 底部间隔10px } // 确保弹窗不超出视窗顶部 if (top < window.scrollY) { top = window.scrollY + 10; // 顶部间隔10px } // 设置弹窗位置 popup.style.left = `${left}px`; popup.style.top = `${top}px`; // 检测语言并翻译 if (settings.autoDetectLanguage) { try { const language = await detectLanguage(text); sourceLanguage.textContent = `源语言: ${language}`; // 显示原文(如果启用) if (settings.showSourceLanguage) { // 创建源文本元素 const sourceText = document.createElement('div'); sourceText.style.cssText = ` margin-bottom: 10px; padding: 5px; background-color: #f8f9fa; border-radius: 4px; font-size: 13px; color: #666; `; sourceText.textContent = text; // 在内容前添加源文本 contentWrapper.insertBefore(sourceText, content); } // 翻译文本 translateText(text, result => { // 清除加载动画和加载文本 content.innerHTML = ''; // 确保结果正确显示 if (typeof result === 'object') { if (result.type === 'error') { content.textContent = `错误: ${result.content}`; } else if (result.type === 'complete' || result.type === 'stream-end') { content.textContent = result.content; // 添加到历史记录 addToHistory(text, result.content); } else if (result.content) { content.textContent = result.content; } else { content.textContent = '无法解析翻译结果'; console.error('未能正确解析翻译结果', result); } } else if (typeof result === 'string') { content.textContent = result; } else { content.textContent = '翻译过程发生错误'; console.error('翻译回调收到意外类型的结果', result); } }); } catch (error) { console.error('语言检测失败:', error); sourceLanguage.textContent = '源语言: 检测失败'; // 继续翻译流程 translateText(text, result => { // ... 与上面相同的翻译处理逻辑 ... }); } } else { sourceLanguage.textContent = '源语言: 未检测'; // 显示原文(如果启用) if (settings.showSourceLanguage) { // 创建源文本元素 const sourceText = document.createElement('div'); sourceText.style.cssText = ` margin-bottom: 10px; padding: 5px; background-color: #f8f9fa; border-radius: 4px; font-size: 13px; color: #666; `; sourceText.textContent = text; // 在内容前添加源文本 contentWrapper.insertBefore(sourceText, content); } // 翻译文本 translateText(text, result => { // 清除加载动画和加载文本 content.innerHTML = ''; // 确保结果正确显示 if (typeof result === 'object') { if (result.type === 'error') { content.textContent = `错误: ${result.content}`; } else if (result.type === 'complete' || result.type === 'stream-end') { content.textContent = result.content; // 添加到历史记录 addToHistory(text, result.content); } else if (result.content) { content.textContent = result.content; } else { content.textContent = '无法解析翻译结果'; console.error('未能正确解析翻译结果', result); } } else if (typeof result === 'string') { content.textContent = result; } else { content.textContent = '翻译过程发生错误'; console.error('翻译回调收到意外类型的结果', result); } }); } // 将选择的文本保存为全局变量,用于收藏功能 lastSelectedText = text; } // 监听选择事件 document.addEventListener('mouseup', function (e) { // 如果点击的是翻译按钮或翻译弹窗,不处理 if (e.target.classList.contains('translate-button') || e.target.closest('.translate-button') || e.target.classList.contains('translation-popup') || e.target.closest('.translation-popup')) { return; } const selection = window.getSelection(); const selectedText = selection.toString().trim(); // 清除之前的翻译按钮 if (activeTranslateButton) { activeTranslateButton.remove(); activeTranslateButton = null; } if (selectedText.length > 0) { const range = selection.getRangeAt(0); const rect = range.getBoundingClientRect(); // 保存最后选择的文本和位置 lastSelectedText = selectedText; lastSelectionRect = { top: rect.top, left: rect.left, bottom: rect.bottom, right: rect.right }; const translateButton = createTranslateButton(); translateButton.style.top = `${window.scrollY + rect.bottom + 5}px`; translateButton.style.left = `${window.scrollX + rect.right - 50}px`; translateButton.addEventListener('click', function (event) { event.stopPropagation(); event.preventDefault(); // 显示翻译 showTranslation(lastSelectedText, lastSelectionRect); }); document.body.appendChild(translateButton); activeTranslateButton = translateButton; } }); // 在初始化时创建UI组件 function initUI() { createSettingsButton(); createTranslatePageButton(); createProgressBar(); } // 初始化UI settings.currentTab = 'general' initUI(); // 重试失败的翻译 function retryFailedTranslations() { // 确保使用最新的API设置 syncApiSettings(); console.log('重试失败翻译 - 确保使用最新API设置:', settings.apiEndpoint, settings.model); // 确认有段落需要翻译 if (!translationSegments || translationSegments.length === 0) { alert('没有找到需要重试的翻译段落'); return; } // 收集失败项 const failedSegments = []; for (let i = 0; i < translationSegments.length; i++) { if (translationSegments[i].error) { failedSegments.push(i); } } if (failedSegments.length === 0) { alert('没有找到失败的翻译段落'); return; } // 确认是否重试 if (!confirm(`找到${failedSegments.length}个失败的翻译段落,是否重试?`)) { return; } // 更新状态 const statusElement = document.getElementById('translation-status'); if (statusElement) { statusElement.textContent = `正在重试失败的翻译 (0/${failedSegments.length})`; // 移除之前的错误摘要 const oldSummary = statusElement.querySelector('.error-summary'); if (oldSummary) oldSummary.remove(); } // 重置错误计数 translationSegments.errorCount = 0; // 开始重试 isTranslatingFullPage = true; isTranslationPaused = false; // 创建控制按钮 createControlButtons(); // 递归重试 function retryNext(index) { if (index >= failedSegments.length) { // 重试完成 if (statusElement) { const newErrors = translationSegments.errorCount || 0; statusElement.textContent = `重试完成 (${failedSegments.length}/${failedSegments.length})${newErrors > 0 ? ` (仍有${newErrors}个错误)` : ''}`; } isTranslatingFullPage = false; return; } // 如果暂停,不继续 if (isTranslationPaused || !isTranslatingFullPage) { return; } // 更新状态 if (statusElement) { statusElement.textContent = `正在重试失败的翻译 (${index + 1}/${failedSegments.length})`; } // 获取当前要重试的段落索引 const segmentIndex = failedSegments[index]; // 清除之前的错误标记 if (translationSegments[segmentIndex].nodes && translationSegments[segmentIndex].nodes.length > 0) { const nodeInfo = translationSegments[segmentIndex].nodes[0]; if (nodeInfo.node && nodeInfo.node.parentElement) { const errorMark = nodeInfo.node.parentElement.querySelector('.translation-error-mark'); if (errorMark) errorMark.remove(); } } // 清除错误状态 delete translationSegments[segmentIndex].error; // 翻译该段落 translateText(translationSegments[segmentIndex].text, function (result) { if (result.type === 'complete' || result.type === 'stream-end') { // 翻译成功,更新 translationSegments[segmentIndex].translation = result.content; // 更新DOM if (translationSegments[segmentIndex].nodes && translationSegments[segmentIndex].nodes.length > 0) { // 复用现有逻辑更新DOM if (translationSegments[segmentIndex].nodes.length === 1) { const nodeInfo = translationSegments[segmentIndex].nodes[0]; if (nodeInfo.node && nodeInfo.node.nodeType === Node.TEXT_NODE) { nodeInfo.node.textContent = result.content; if (nodeInfo.node.parentElement) { nodeInfo.node.parentElement.style.fontWeight = 'inherit'; nodeInfo.node.parentElement.dataset.translated = 'true'; } } } else { // 复杂节点结构的处理略过,会在前面代码相同逻辑处理 } } // 继续下一个 setTimeout(() => retryNext(index + 1), 50); } else if (result.type === 'error') { // 仍然出错 console.error(`重试段落 ${segmentIndex + 1} 翻译失败:`, result.content); translationSegments[segmentIndex].error = result.content; // 错误计数 translationSegments.errorCount = (translationSegments.errorCount || 0) + 1; // 添加错误标记 if (translationSegments[segmentIndex].nodes && translationSegments[segmentIndex].nodes.length > 0) { const nodeInfo = translationSegments[segmentIndex].nodes[0]; if (nodeInfo.node && nodeInfo.node.parentElement) { // 在段落旁创建错误标记 const errorMark = document.createElement('span'); errorMark.className = 'translation-error-mark'; errorMark.innerHTML = '⚠️'; errorMark.title = `翻译失败: ${result.content}`; errorMark.style.cssText = ` color: #ff4d4f; cursor: pointer; margin-left: 5px; font-size: 16px; `; errorMark.addEventListener('click', function() { alert(`翻译错误: ${result.content}`); }); nodeInfo.node.parentElement.appendChild(errorMark); // 确保不修改原文内容,防止错误码替换原文 if (nodeInfo.original && nodeInfo.node.nodeType === Node.TEXT_NODE) { if (nodeInfo.node.textContent !== nodeInfo.original) { nodeInfo.node.textContent = nodeInfo.original; } } } } // 继续下一个 setTimeout(() => retryNext(index + 1), 50); } }, false); } // 开始重试第一个 retryNext(0); } // 递归翻译段落 let translatedCount = 0; // 添加一些全局错误处理和减速变量 let consecutiveErrors = 0; let translationDelay = 50; // 初始延迟时间(ms) function translateNextSegment(index) { // 如果已暂停,不继续翻译 if (isTranslationPaused) { return; } // 记录最后翻译的索引 lastTranslatedIndex = index - 1; // 获取进度条和状态元素 const progressBar = document.getElementById('translation-progress-bar'); const statusElement = document.getElementById('translation-status'); if (index >= translationSegments.length) { // 全部翻译完成 if (progressBar) progressBar.style.width = '100%'; // 获取错误统计 const totalErrors = translationSegments.errorCount || 0; const errorInfo = totalErrors > 0 ? ` (${totalErrors}个错误)` : ''; if (statusElement) { statusElement.textContent = `翻译完成 (${translationSegments.length}/${translationSegments.length})${errorInfo}`; // 如果有错误,添加错误摘要信息 if (totalErrors > 0) { // 移除之前的错误警告(如果有) const oldWarning = statusElement.querySelector('.error-warning'); if (oldWarning) oldWarning.remove(); const errorSummary = document.createElement('div'); errorSummary.className = 'error-summary'; errorSummary.style.cssText = ` margin-top: 10px; color: #ff4d4f; font-size: 13px; padding: 8px; background-color: rgba(255, 77, 79, 0.1); border-radius: 4px; `; // 创建错误摘要内容 let summaryText = `翻译过程中遇到了${totalErrors}个错误。`; // 添加解决建议 if (totalErrors > 5) { summaryText += `可能原因:<br> 1. API密钥额度不足或API限制<br> 2. 网络连接不稳定<br> 3. 翻译内容过长或API不兼容<br><br> 建议:<br> · 检查API设置和密钥<br> · 在设置中禁用流式响应<br> · 尝试使用不同的API提供商`; } errorSummary.innerHTML = summaryText; statusElement.appendChild(errorSummary); // 添加重试错误项按钮 const retryButton = document.createElement('button'); retryButton.textContent = '重试失败项'; retryButton.style.cssText = ` margin-top: 10px; padding: 5px 10px; background-color: #ff4d4f; color: white; border: none; border-radius: 4px; cursor: pointer; `; retryButton.addEventListener('click', function() { retryFailedTranslations(); }); errorSummary.appendChild(retryButton); } } // 添加一个关闭按钮 if (statusElement && !statusElement.querySelector('.close-btn')) { const closeButton = document.createElement('span'); closeButton.className = 'close-btn'; closeButton.style.cssText = ` margin-left: 10px; cursor: pointer; font-weight: bold; `; closeButton.textContent = '×'; closeButton.addEventListener('click', function () { statusElement.remove(); const progressContainer = document.getElementById('translation-progress-container'); if (progressContainer) progressContainer.style.display = 'none'; }); statusElement.appendChild(closeButton); } // 显示切换按钮 const toggleButton = document.getElementById('toggle-translation-button'); if (toggleButton) toggleButton.style.display = 'block'; // 移除控制按钮 removeControlButtons(); isTranslatingFullPage = false; return; } // 更新进度 const progress = Math.round((index / translationSegments.length) * 100); if (progressBar) progressBar.style.width = `${progress}%`; if (statusElement) statusElement.textContent = `正在翻译 (${index}/${translationSegments.length})`; // 如果段落文本为空,直接跳到下一个 if (!translationSegments[index].text.trim()) { translateNextSegment(index + 1); return; } // 获取上下文 let context = []; if (settings.useTranslationContext && settings.contextSize > 0) { // 从当前段落往前获取已翻译的段落作为上下文 for (let i = index - 1; i >= 0 && context.length < settings.contextSize; i--) { if (translationSegments[i] && translationSegments[i].translation) { context.unshift({ original: translationSegments[i].text, translation: translationSegments[i].translation }); } } } // 处理参考文献条目 - 特殊处理以保留格式 if (translationSegments[index].isReferenceItem) { translateReferenceItem(translationSegments[index], function (translatedHtml) { if (translatedHtml) { // 保存翻译结果 translationSegments[index].translation = translatedHtml; // 保存原始HTML以便切换回来 if (!translationSegments[index].originalHtml && translationSegments[index].element) { translationSegments[index].originalHtml = translationSegments[index].element.innerHTML; } // 更新DOM显示翻译结果 if (translationSegments[index].element) { // 将翻译后的HTML设置到元素中 translationSegments[index].element.innerHTML = translatedHtml; } // 更新最后翻译的索引 lastTranslatedIndex = index; // 继续翻译下一个段落 setTimeout(() => translateNextSegment(index + 1), 50); } else { // 翻译失败时继续下一个 console.error(`参考文献条目 ${index + 1} 翻译失败`); // 更新最后翻译的索引 lastTranslatedIndex = index; setTimeout(() => translateNextSegment(index + 1), 50); } }); return; } // 常规段落翻译 translateText(translationSegments[index].text, function (result) { // 如果已暂停或停止,不处理翻译结果 if (isTranslationPaused || !isTranslatingFullPage) { return; } if (result.type === 'complete' || result.type === 'stream-end') { // 翻译成功 translatedCount++; translationSegments[index].translation = result.content; // 更新DOM中的文本,保留原始样式 if (translationSegments[index].nodes && translationSegments[index].nodes.length > 0) { // 使用更智能的方式分配翻译结果 if (translationSegments[index].nodes.length === 1) { // 只有一个节点的简单情况 const nodeInfo = translationSegments[index].nodes[0]; if (nodeInfo.node && nodeInfo.node.nodeType === Node.TEXT_NODE) { nodeInfo.node.textContent = result.content; } } else { // 多个节点的情况,尝试保留原始结构 // 计算原始内容中每个节点文本的比例 const totalOriginalLength = translationSegments[index].nodes.reduce( (sum, nodeInfo) => sum + (nodeInfo.original ? nodeInfo.original.length : 0), 0); if (totalOriginalLength > 0) { // 按照原始文本长度比例分配翻译后的文本 let startPos = 0; for (let i = 0; i < translationSegments[index].nodes.length; i++) { const nodeInfo = translationSegments[index].nodes[i]; if (nodeInfo.node && nodeInfo.node.nodeType === Node.TEXT_NODE && nodeInfo.original) { // 计算该节点在原始文本中的比例 const ratio = nodeInfo.original.length / totalOriginalLength; // 计算应该分配给该节点的翻译文本长度 const chunkLength = Math.round(result.content.length * ratio); // 提取翻译文本的一部分 let chunk = ''; if (i === translationSegments[index].nodes.length - 1) { // 最后一个节点,获取剩余所有文本 chunk = result.content.substring(startPos); } else { // 非最后节点,按比例获取 chunk = result.content.substring(startPos, startPos + chunkLength); startPos += chunkLength; } // 更新节点文本 nodeInfo.node.textContent = chunk; } } } else { // 回退方案:如果无法计算比例,则将所有文本放在第一个节点 let foundFirstNode = false; for (let i = 0; i < translationSegments[index].nodes.length; i++) { const nodeInfo = translationSegments[index].nodes[i]; if (nodeInfo.node && nodeInfo.node.nodeType === Node.TEXT_NODE) { if (!foundFirstNode) { nodeInfo.node.textContent = result.content; foundFirstNode = true; } else { nodeInfo.node.textContent = ''; } } } } } } // 更新最后翻译的索引 lastTranslatedIndex = index; // 重置连续错误计数,因为成功了一次 consecutiveErrors = 0; // 如果之前增加了延迟,且现在连续成功,则尝试逐步恢复 if (translationDelay > 50 && translatedCount % 5 === 0) { // 逐步减少延迟,但不低于初始值 translationDelay = Math.max(50, translationDelay * 0.8); console.log(`连续翻译成功,减少延迟至${translationDelay}ms`); // 更新延迟信息显示 if (statusElement && statusElement.querySelector('.delay-info')) { statusElement.querySelector('.delay-info').textContent = `已自动调整延迟至${Math.round(translationDelay)}ms`; // 如果延迟已经降回正常,移除通知并恢复颜色 if (translationDelay <= 60) { statusElement.querySelector('.delay-info').remove(); statusElement.style.backgroundColor = 'rgba(66, 133, 244, 0.9)'; } } } // 继续下一个段落 setTimeout(() => translateNextSegment(index + 1), translationDelay); } else if (result.type === 'error') { // 翻译出错,记录错误并继续下一个 console.error(`段落 ${index + 1} 翻译失败:`, result.content); translationSegments[index].error = result.content; // 更新最后翻译的索引 lastTranslatedIndex = index; // 错误计数 translationSegments.errorCount = (translationSegments.errorCount || 0) + 1; consecutiveErrors++; // 如果连续错误过多,自动增加延迟 if (consecutiveErrors >= 3) { // 增加延迟,但最大不超过2秒 translationDelay = Math.min(2000, translationDelay * 1.5); console.log(`检测到连续错误,自动增加翻译延迟至${translationDelay}ms`); // 更新状态显示 if (statusElement && !statusElement.querySelector('.delay-info')) { const delayInfo = document.createElement('div'); delayInfo.className = 'delay-info'; delayInfo.style.cssText = ` font-size: 12px; margin-top: 5px; `; delayInfo.textContent = `已自动增加延迟至${translationDelay}ms以减轻API负载`; statusElement.appendChild(delayInfo); } else if (statusElement && statusElement.querySelector('.delay-info')) { statusElement.querySelector('.delay-info').textContent = `已自动增加延迟至${translationDelay}ms以减轻API负载`; } // 改变状态颜色提示错误 statusElement.style.backgroundColor = 'rgba(255, 152, 0, 0.9)'; } // 显示错误提示标记在段落旁 if (translationSegments[index].nodes && translationSegments[index].nodes.length > 0) { const nodeInfo = translationSegments[index].nodes[0]; if (nodeInfo.node && nodeInfo.node.parentElement) { // 在段落旁创建错误标记 const errorMark = document.createElement('span'); errorMark.className = 'translation-error-mark'; errorMark.innerHTML = '⚠️'; errorMark.title = `翻译失败: ${result.content}`; errorMark.style.cssText = ` color: #ff4d4f; cursor: pointer; margin-left: 5px; font-size: 16px; `; errorMark.addEventListener('click', function() { alert(`翻译错误: ${result.content}`); }); nodeInfo.node.parentElement.appendChild(errorMark); // 重要:确保不修改原文内容,防止错误码替换原文 // 如果有通过错误处理设置了节点文本,恢复原始文本 if (nodeInfo.original && nodeInfo.node.nodeType === Node.TEXT_NODE) { if (nodeInfo.node.textContent !== nodeInfo.original) { nodeInfo.node.textContent = nodeInfo.original; } } } } // 更新状态面板显示总体进度和错误信息 if (statusElement) { const totalErrors = translationSegments.errorCount; const errorInfo = totalErrors > 0 ? ` (${totalErrors}个错误)` : ''; statusElement.textContent = `正在翻译 (${index + 1}/${translationSegments.length})${errorInfo}`; // 如果错误超过阈值,添加暂停建议 if (totalErrors > 5 && !statusElement.querySelector('.error-warning')) { const warningElement = document.createElement('div'); warningElement.className = 'error-warning'; warningElement.style.cssText = ` margin-top: 5px; color: #ff4d4f; font-size: 12px; `; warningElement.textContent = `检测到多处翻译错误,可能是API限制或网络问题。考虑暂停翻译后再继续。`; statusElement.appendChild(warningElement); } } // 继续下一个段落 setTimeout(() => translateNextSegment(index + 1), translationDelay); } }, false, context); // 添加context参数 } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址