// ==UserScript== // @name 自动翻译助手(多引擎整合版) // @version 4.4.0 // @description 整合腾讯翻译、微软Edge翻译(独立实现)、Gitee AI、SiliconFlow、公共translate.service,支持多语言,可移动可缩放悬浮窗,白名单管理,三种显示模式,实时生效。 // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+PHJlY3Qgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiByeD0iMTYiIGZpbGw9IiMyOTUzZTgiLz48Y2lyY2xlIGN4PSIxNiIgY3k9IjE2IiByPSIxMiIgZmlsbD0iIzQyYTVmNSIvPjxwYXRoIGQ9Ik00IDE2YzAgNi42IDUuNCAxMiAxMiAxMnMxMi01LjQgMTItMTIiIGZpbGw9Im5vbmUiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMS41Ii8+PHBhdGggZD0iTTE2IDR2MjRNMjggMTZINCIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIxLjUiLz48Y2lyY2xlIGN4PSIxNiIgY3k9IjE2IiByPSI0IiBmaWxsPSIjMjk1M2U4Ii8+PC9zdmc+ // @run-at document-start // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_xmlhttpRequest // @grant unsafeWindow // @connect transmart.qq.com // @connect edge.microsoft.com // @connect api-edge.cognitive.microsofttranslator.com // @connect translate.zvo.cn // @connect gitee.com // @connect siliconflow.cn // @require https://unpkg.com/i18n-jsautotranslate@4.0.0/index.js // @license Apache-2.0 // ==/UserScript== (function() { 'use strict'; // ==================== 工具函数 ==================== function isMobile() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth <= 768; } function isDarkMode() { return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; } function getCurrentDomain() { return window.location.hostname; } // ==================== 语言映射 ==================== function mapToTencentLang(langCode) { const map = { 'chinese_simplified': 'zh', 'chinese_traditional': 'zh-TW', 'english': 'en', 'japanese': 'ja', 'korean': 'ko', 'french': 'fr', 'german': 'de', 'spanish': 'es', 'russian': 'ru', 'portuguese': 'pt', 'italian': 'it', 'dutch': 'nl', 'polish': 'pl', 'turkish': 'tr', 'vietnamese': 'vi', 'thai': 'th', 'indonesian': 'id', 'arabic': 'ar', 'hindi': 'hi' }; return map[langCode] || langCode; } // ==================== 腾讯翻译引擎(独立实现) ==================== class TencentTranslator { constructor(configManager) { this.configManager = configManager; this.active = false; this.pageTranslating = false; this.translationCache = new Map(); this.originalTextMap = new Map(); this.domObserver = null; this.continuousTimer = null; this.lastTranslationTime = 0; this.isObserving = false; this.domChangeTimer = null; this.mode = configManager.get('displayMode') || 'replace'; this.textColor = configManager.get('textColor') || '#0066cc'; this.bgColor = configManager.get('bgColor') || 'rgba(0,102,204,0.1)'; this._clientKey = null; } getClientKey() { if (this._clientKey) return this._clientKey; this._clientKey = 'browser-chrome-120.0-Windows_10-' + crypto.randomUUID() + '-' + Date.now(); return this._clientKey; } async init() { if (this.active) return true; this.active = true; console.log('腾讯翻译引擎初始化成功'); return true; } translateBatch(texts, fromLang = null) { return new Promise((resolve, reject) => { if (!texts || texts.length === 0) { resolve([]); return; } let targetLang = this.configManager.get('targetLanguage'); targetLang = mapToTencentLang(targetLang); GM_xmlhttpRequest({ method: "POST", url: "https://transmart.qq.com/api/imt", headers: { "Content-Type": "application/json", }, data: JSON.stringify({ header: { fn: 'auto_translation', session: '', client_key: this.getClientKey(), user: '' }, type: 'plain', model_category: 'normal', text_domain: 'general', source: { lang: 'auto', text_list: texts }, target: { lang: targetLang } }), onload: (res) => { if (res.status === 200) { try { const result = JSON.parse(res.responseText); const translations = result.auto_translation || []; resolve(translations); } catch (e) { reject(new Error('解析腾讯响应失败')); } } else { reject(new Error(`腾讯翻译失败: ${res.status}`)); } }, onerror: () => reject(new Error('网络请求失败')) }); }); } collectTextNodes() { const textNodes = []; const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { const parent = node.parentElement; if (!parent) return NodeFilter.FILTER_REJECT; const excludedSelectors = [ 'SCRIPT', 'STYLE', 'NOSCRIPT', 'CODE', 'PRE', '.translated-text', '.translation-popup', '.translating-indicator', '.yandex-translate-widget', '.translation-settings-modal', '[data-no-translate]', '.no-translate', '.notranslate', '.yandex-no-translate', '.yandex-script-element', '[data-protect-translation="true"]' ]; for (const sel of excludedSelectors) { if (parent.matches(sel) || parent.closest(sel)) return NodeFilter.FILTER_REJECT; } const ignoredClasses = this.configManager.get('ignoredClasses'); if (ignoredClasses) { const classes = ignoredClasses.split(',').map(c => c.trim()); for (const cls of classes) { if (parent.classList && parent.classList.contains(cls)) return NodeFilter.FILTER_REJECT; } } const ignoredIds = this.configManager.get('ignoredIds'); if (ignoredIds) { const ids = ignoredIds.split(',').map(id => id.trim()); for (const id of ids) { if (parent.id === id) return NodeFilter.FILTER_REJECT; } } const text = node.textContent.trim(); if (!text || text.length < 2) return NodeFilter.FILTER_REJECT; if (/^\d+$/.test(text)) return NodeFilter.FILTER_REJECT; if (/^\d+\.?\d*\s*[KMGT]?[B]?$/i.test(text)) return NodeFilter.FILTER_REJECT; if (/^\d+\.?\d*\s*%$/.test(text)) return NodeFilter.FILTER_REJECT; if (/^[\d\s\W]+$/.test(text)) return NodeFilter.FILTER_REJECT; if (!/[a-zA-Z\u0400-\u04FF\u4e00-\u9fff]/.test(text)) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } } ); let node; while (node = walker.nextNode()) { const text = node.textContent.trim(); if (text) { textNodes.push({ node: node, text: text, parent: node.parentElement, originalText: node.textContent }); } } return textNodes; } applyTranslation(nodeInfo, translation) { const originalNode = nodeInfo.node; const parent = nodeInfo.parent; const originalText = nodeInfo.originalText || originalNode.textContent; const elementId = 'translated_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); this.originalTextMap.set(elementId, originalText); const translatedElement = document.createElement('span'); translatedElement.className = 'translated-text'; translatedElement.setAttribute('data-original-id', elementId); translatedElement.setAttribute('translate', 'no'); translatedElement.setAttribute('data-no-translate', 'true'); translatedElement.style.display = 'inline'; if (this.mode === 'replace') { translatedElement.textContent = translation; translatedElement.title = `原文: ${originalText}`; translatedElement.style.color = this.textColor; translatedElement.addEventListener('mouseenter', () => { translatedElement.style.backgroundColor = this.bgColor; }); translatedElement.addEventListener('mouseleave', () => { translatedElement.style.backgroundColor = ''; }); } else if (this.mode === 'dual') { const srcSpan = document.createElement('span'); srcSpan.className = 'original-text'; srcSpan.textContent = originalText; srcSpan.setAttribute('translate', 'no'); srcSpan.setAttribute('data-no-translate', 'true'); const transSpan = document.createElement('span'); transSpan.className = 'translated-text'; let displayText = translation; if (!/^[\u4e00-\u9fff]/.test(translation)) { displayText = `(${translation})`; } transSpan.textContent = displayText; transSpan.style.color = this.textColor; transSpan.setAttribute('translate', 'no'); transSpan.setAttribute('data-no-translate', 'true'); translatedElement.appendChild(srcSpan); translatedElement.appendChild(transSpan); } else { // 'none' 无感模式 translatedElement.classList.add('no-style'); translatedElement.textContent = translation; translatedElement.style.color = ''; translatedElement.style.backgroundColor = ''; translatedElement.title = ''; } parent.replaceChild(translatedElement, originalNode); } async translateWholePage() { if (!this.configManager.shouldTranslate()) return; if (this.pageTranslating) return; if (!this.active) { const ok = await this.init(); if (!ok) return; } this.pageTranslating = true; console.log('开始腾讯翻译页面...'); let textNodes = this.collectTextNodes(); if (textNodes.length === 0) { this.pageTranslating = false; return; } const MAX_TEXT_LENGTH = 5000; const BATCH_SIZE = 50; let batches = []; let currentBatch = { nodes: [], totalLen: 0 }; for (const nodeInfo of textNodes) { const len = nodeInfo.text.length; if (len > MAX_TEXT_LENGTH) { if (currentBatch.nodes.length) batches.push(currentBatch); batches.push({ nodes: [{ ...nodeInfo, text: nodeInfo.text.substring(0, MAX_TEXT_LENGTH) }], totalLen: MAX_TEXT_LENGTH }); currentBatch = { nodes: [], totalLen: 0 }; continue; } if (currentBatch.totalLen + len > MAX_TEXT_LENGTH || currentBatch.nodes.length >= BATCH_SIZE) { batches.push(currentBatch); currentBatch = { nodes: [], totalLen: 0 }; } currentBatch.nodes.push(nodeInfo); currentBatch.totalLen += len; } if (currentBatch.nodes.length) batches.push(currentBatch); let translatedCount = 0; for (let i = 0; i < batches.length; i++) { const batch = batches[i]; const textsToTranslate = []; const nodeIndices = []; batch.nodes.forEach((nodeInfo, idx) => { const cacheKey = `${nodeInfo.text}|${this.configManager.get('targetLanguage')}|${this.mode}`; if (this.translationCache.has(cacheKey)) { const translation = this.translationCache.get(cacheKey); this.applyTranslation(nodeInfo, translation); translatedCount++; } else { textsToTranslate.push(nodeInfo.text); nodeIndices.push(idx); } }); if (textsToTranslate.length) { try { const translations = await this.translateBatch(textsToTranslate, null); translations.forEach((translation, ti) => { const nodeIdx = nodeIndices[ti]; const nodeInfo = batch.nodes[nodeIdx]; if (translation && translation !== nodeInfo.text) { this.applyTranslation(nodeInfo, translation); const cacheKey = `${nodeInfo.text}|${this.configManager.get('targetLanguage')}|${this.mode}`; this.translationCache.set(cacheKey, translation); translatedCount++; } }); } catch (err) { console.error('批次翻译失败:', err); } } await new Promise(r => setTimeout(r, 20)); } this.pageTranslating = false; this.lastTranslationTime = Date.now(); console.log(`腾讯翻译完成,翻译了 ${translatedCount} 个文本`); } async checkAndTranslate() { if (!this.configManager.shouldTranslate()) return; if (this.pageTranslating) return; const untranslated = this.collectTextNodes(); if (untranslated.length > 0) { console.log(`发现 ${untranslated.length} 个未翻译文本,重新翻译...`); await this.translateWholePage(); } } startDOMObservation() { if (this.isObserving) return; if (!this.domObserver) { this.domObserver = new MutationObserver(() => { if (this.pageTranslating) return; if (this.domChangeTimer) clearTimeout(this.domChangeTimer); this.domChangeTimer = setTimeout(() => { this.checkAndTranslate(); }, 800); }); } try { this.domObserver.observe(document.body, { childList: true, subtree: true, characterData: true }); this.isObserving = true; console.log('腾讯翻译DOM监听已启动'); } catch (e) { console.error('启动DOM监听失败', e); } } stopDOMObservation() { if (this.domObserver) { this.domObserver.disconnect(); this.isObserving = false; } if (this.domChangeTimer) { clearTimeout(this.domChangeTimer); this.domChangeTimer = null; } } startContinuousCheck() { if (this.continuousTimer) clearInterval(this.continuousTimer); this.continuousTimer = setInterval(() => { if (!this.pageTranslating && this.active && this.configManager.shouldTranslate()) { const now = Date.now(); if (now - this.lastTranslationTime > 30000) { this.checkAndTranslate(); } } }, 30000); } stopContinuousCheck() { if (this.continuousTimer) { clearInterval(this.continuousTimer); this.continuousTimer = null; } } async enable() { if (!this.configManager.shouldTranslate()) return false; const success = await this.init(); if (!success) return false; await this.translateWholePage(); this.startDOMObservation(); this.startContinuousCheck(); return true; } disable() { this.stopDOMObservation(); this.stopContinuousCheck(); // 恢复所有译文 document.querySelectorAll('.translated-text').forEach(el => { const originalId = el.dataset.originalId; if (originalId && this.originalTextMap.has(originalId)) { const originalText = this.originalTextMap.get(originalId); const textNode = document.createTextNode(originalText); el.parentNode.replaceChild(textNode, el); } }); this.originalTextMap.clear(); this.translationCache.clear(); this.active = false; console.log('腾讯翻译已禁用'); } async onLanguageChange() { if (!this.active) return; this.translationCache.clear(); await this.translateWholePage(); } async onIgnoreChange() { if (!this.active) return; document.querySelectorAll('.translated-text').forEach(el => { const originalId = el.dataset.originalId; if (originalId && this.originalTextMap.has(originalId)) { const originalText = this.originalTextMap.get(originalId); const textNode = document.createTextNode(originalText); el.parentNode.replaceChild(textNode, el); } }); this.originalTextMap.clear(); this.translationCache.clear(); await this.translateWholePage(); } async onModeColorChange() { this.mode = this.configManager.get('displayMode') || 'replace'; this.textColor = this.configManager.get('textColor') || '#0066cc'; this.bgColor = this.configManager.get('bgColor') || 'rgba(0,102,204,0.1)'; if (!this.active) return; document.querySelectorAll('.translated-text').forEach(el => { const originalId = el.dataset.originalId; if (originalId && this.originalTextMap.has(originalId)) { const originalText = this.originalTextMap.get(originalId); const textNode = document.createTextNode(originalText); el.parentNode.replaceChild(textNode, el); } }); this.originalTextMap.clear(); this.translationCache.clear(); await this.translateWholePage(); } } // ==================== 微软翻译引擎(独立实现,从网页翻译器移植) ==================== class MicrosoftTranslator { constructor(configManager) { this.configManager = configManager; this.active = false; this.pageTranslating = false; this.translationCache = new Map(); this.originalTextMap = new Map(); this.domObserver = null; this.continuousTimer = null; this.lastTranslationTime = 0; this.isObserving = false; this.domChangeTimer = null; this.mode = configManager.get('displayMode') || 'replace'; this.textColor = configManager.get('textColor') || '#0066cc'; this.bgColor = configManager.get('bgColor') || 'rgba(0,102,204,0.1)'; } // 与 TencentTranslator 相同的语言修正,但微软需要特殊映射 _fixLang(lang) { const map = { 'zh-CN': 'zh-Hans', 'zh-TW': 'zh-Hant', 'zh-Hans': 'zh-Hans', 'zh-Hant': 'zh-Hant', 'pt-PT': 'pt', 'pt-BR': 'pt' }; return map[lang] || lang.split('-')[0]; } async init() { if (this.active) return true; this.active = true; console.log('微软翻译引擎初始化成功'); return true; } translateBatch(texts, fromLang = null) { return new Promise((resolve, reject) => { if (!texts || texts.length === 0) { resolve([]); return; } let targetLang = this.configManager.get('targetLanguage'); // 将 chinese_simplified 等转为 zh-Hans const langMap = { 'chinese_simplified': 'zh-Hans', 'chinese_traditional': 'zh-Hant', 'english': 'en', 'japanese': 'ja', 'korean': 'ko', 'french': 'fr', 'german': 'de', 'spanish': 'es', 'russian': 'ru', 'portuguese': 'pt', 'italian': 'it', 'dutch': 'nl', 'polish': 'pl', 'turkish': 'tr', 'vietnamese': 'vi', 'thai': 'th', 'indonesian': 'id', 'arabic': 'ar', 'hindi': 'hi' }; const mapped = langMap[targetLang] || targetLang; const targetTl = this._fixLang(mapped); GM_xmlhttpRequest({ method: 'POST', url: `https://edge.microsoft.com/translate/translatetext?from=&to=${targetTl}&isEnterpriseClient=false`, headers: { 'Content-Type': 'application/json', 'Accept': '*/*', 'sec-ch-ua': '"Microsoft Edge";v="123", "Not:A-Brand";v="8", "Chromium";v="123"', 'sec-mesh-client-os': 'Windows', 'sec-mesh-client-edge-channel': 'stable', 'sec-mesh-client-edge-version': '123.0.0.0', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0' }, data: JSON.stringify(texts), timeout: 15000, onload: (res) => { if (res.status >= 200 && res.status < 300) { try { const data = JSON.parse(res.responseText); const results = []; if (Array.isArray(data)) { for (const item of data) { if (item.translations && item.translations[0]) { results.push(item.translations[0].text); } else if (typeof item === 'string') { results.push(item); } else { results.push(null); } } resolve(results); } else { reject(new Error('微软批量翻译返回格式异常')); } } catch (e) { reject(new Error('微软批量翻译数据解析失败: ' + e.message)); } } else { reject(new Error(`微软批量翻译请求失败 (HTTP ${res.status})`)); } }, ontimeout: () => reject(new Error('微软批量翻译请求超时')), onerror: () => reject(new Error('微软批量翻译网络连接失败')) }); }); } // 以下方法与 TencentTranslator 完全相同,仅 translateBatch 不同,故复用 collectTextNodes() { const textNodes = []; const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { const parent = node.parentElement; if (!parent) return NodeFilter.FILTER_REJECT; const excludedSelectors = [ 'SCRIPT', 'STYLE', 'NOSCRIPT', 'CODE', 'PRE', '.translated-text', '.translation-popup', '.translating-indicator', '.yandex-translate-widget', '.translation-settings-modal', '[data-no-translate]', '.no-translate', '.notranslate', '.yandex-no-translate', '.yandex-script-element', '[data-protect-translation="true"]' ]; for (const sel of excludedSelectors) { if (parent.matches(sel) || parent.closest(sel)) return NodeFilter.FILTER_REJECT; } const ignoredClasses = this.configManager.get('ignoredClasses'); if (ignoredClasses) { const classes = ignoredClasses.split(',').map(c => c.trim()); for (const cls of classes) { if (parent.classList && parent.classList.contains(cls)) return NodeFilter.FILTER_REJECT; } } const ignoredIds = this.configManager.get('ignoredIds'); if (ignoredIds) { const ids = ignoredIds.split(',').map(id => id.trim()); for (const id of ids) { if (parent.id === id) return NodeFilter.FILTER_REJECT; } } const text = node.textContent.trim(); if (!text || text.length < 2) return NodeFilter.FILTER_REJECT; if (/^\d+$/.test(text)) return NodeFilter.FILTER_REJECT; if (/^\d+\.?\d*\s*[KMGT]?[B]?$/i.test(text)) return NodeFilter.FILTER_REJECT; if (/^\d+\.?\d*\s*%$/.test(text)) return NodeFilter.FILTER_REJECT; if (/^[\d\s\W]+$/.test(text)) return NodeFilter.FILTER_REJECT; if (!/[a-zA-Z\u0400-\u04FF\u4e00-\u9fff]/.test(text)) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } } ); let node; while (node = walker.nextNode()) { const text = node.textContent.trim(); if (text) { textNodes.push({ node: node, text: text, parent: node.parentElement, originalText: node.textContent }); } } return textNodes; } applyTranslation(nodeInfo, translation) { const originalNode = nodeInfo.node; const parent = nodeInfo.parent; const originalText = nodeInfo.originalText || originalNode.textContent; const elementId = 'translated_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); this.originalTextMap.set(elementId, originalText); const translatedElement = document.createElement('span'); translatedElement.className = 'translated-text'; translatedElement.setAttribute('data-original-id', elementId); translatedElement.setAttribute('translate', 'no'); translatedElement.setAttribute('data-no-translate', 'true'); translatedElement.style.display = 'inline'; if (this.mode === 'replace') { translatedElement.textContent = translation; translatedElement.title = `原文: ${originalText}`; translatedElement.style.color = this.textColor; translatedElement.addEventListener('mouseenter', () => { translatedElement.style.backgroundColor = this.bgColor; }); translatedElement.addEventListener('mouseleave', () => { translatedElement.style.backgroundColor = ''; }); } else if (this.mode === 'dual') { const srcSpan = document.createElement('span'); srcSpan.className = 'original-text'; srcSpan.textContent = originalText; srcSpan.setAttribute('translate', 'no'); srcSpan.setAttribute('data-no-translate', 'true'); const transSpan = document.createElement('span'); transSpan.className = 'translated-text'; let displayText = translation; if (!/^[\u4e00-\u9fff]/.test(translation)) { displayText = `(${translation})`; } transSpan.textContent = displayText; transSpan.style.color = this.textColor; transSpan.setAttribute('translate', 'no'); transSpan.setAttribute('data-no-translate', 'true'); translatedElement.appendChild(srcSpan); translatedElement.appendChild(transSpan); } else { // 'none' 无感模式 translatedElement.classList.add('no-style'); translatedElement.textContent = translation; translatedElement.style.color = ''; translatedElement.style.backgroundColor = ''; translatedElement.title = ''; } parent.replaceChild(translatedElement, originalNode); } async translateWholePage() { if (!this.configManager.shouldTranslate()) return; if (this.pageTranslating) return; if (!this.active) { const ok = await this.init(); if (!ok) return; } this.pageTranslating = true; console.log('开始微软翻译页面...'); let textNodes = this.collectTextNodes(); if (textNodes.length === 0) { this.pageTranslating = false; return; } const MAX_TEXT_LENGTH = 5000; const BATCH_SIZE = 50; let batches = []; let currentBatch = { nodes: [], totalLen: 0 }; for (const nodeInfo of textNodes) { const len = nodeInfo.text.length; if (len > MAX_TEXT_LENGTH) { if (currentBatch.nodes.length) batches.push(currentBatch); batches.push({ nodes: [{ ...nodeInfo, text: nodeInfo.text.substring(0, MAX_TEXT_LENGTH) }], totalLen: MAX_TEXT_LENGTH }); currentBatch = { nodes: [], totalLen: 0 }; continue; } if (currentBatch.totalLen + len > MAX_TEXT_LENGTH || currentBatch.nodes.length >= BATCH_SIZE) { batches.push(currentBatch); currentBatch = { nodes: [], totalLen: 0 }; } currentBatch.nodes.push(nodeInfo); currentBatch.totalLen += len; } if (currentBatch.nodes.length) batches.push(currentBatch); let translatedCount = 0; for (let i = 0; i < batches.length; i++) { const batch = batches[i]; const textsToTranslate = []; const nodeIndices = []; batch.nodes.forEach((nodeInfo, idx) => { const cacheKey = `${nodeInfo.text}|${this.configManager.get('targetLanguage')}|${this.mode}`; if (this.translationCache.has(cacheKey)) { const translation = this.translationCache.get(cacheKey); this.applyTranslation(nodeInfo, translation); translatedCount++; } else { textsToTranslate.push(nodeInfo.text); nodeIndices.push(idx); } }); if (textsToTranslate.length) { try { const translations = await this.translateBatch(textsToTranslate, null); translations.forEach((translation, ti) => { const nodeIdx = nodeIndices[ti]; const nodeInfo = batch.nodes[nodeIdx]; if (translation && translation !== nodeInfo.text) { this.applyTranslation(nodeInfo, translation); const cacheKey = `${nodeInfo.text}|${this.configManager.get('targetLanguage')}|${this.mode}`; this.translationCache.set(cacheKey, translation); translatedCount++; } }); } catch (err) { console.error('微软批次翻译失败:', err); } } await new Promise(r => setTimeout(r, 20)); } this.pageTranslating = false; this.lastTranslationTime = Date.now(); console.log(`微软翻译完成,翻译了 ${translatedCount} 个文本`); } async checkAndTranslate() { if (!this.configManager.shouldTranslate()) return; if (this.pageTranslating) return; const untranslated = this.collectTextNodes(); if (untranslated.length > 0) { console.log(`发现 ${untranslated.length} 个未翻译文本,重新翻译...`); await this.translateWholePage(); } } startDOMObservation() { if (this.isObserving) return; if (!this.domObserver) { this.domObserver = new MutationObserver(() => { if (this.pageTranslating) return; if (this.domChangeTimer) clearTimeout(this.domChangeTimer); this.domChangeTimer = setTimeout(() => { this.checkAndTranslate(); }, 800); }); } try { this.domObserver.observe(document.body, { childList: true, subtree: true, characterData: true }); this.isObserving = true; console.log('微软翻译DOM监听已启动'); } catch (e) { console.error('启动DOM监听失败', e); } } stopDOMObservation() { if (this.domObserver) { this.domObserver.disconnect(); this.isObserving = false; } if (this.domChangeTimer) { clearTimeout(this.domChangeTimer); this.domChangeTimer = null; } } startContinuousCheck() { if (this.continuousTimer) clearInterval(this.continuousTimer); this.continuousTimer = setInterval(() => { if (!this.pageTranslating && this.active && this.configManager.shouldTranslate()) { const now = Date.now(); if (now - this.lastTranslationTime > 30000) { this.checkAndTranslate(); } } }, 30000); } stopContinuousCheck() { if (this.continuousTimer) { clearInterval(this.continuousTimer); this.continuousTimer = null; } } async enable() { if (!this.configManager.shouldTranslate()) return false; const success = await this.init(); if (!success) return false; await this.translateWholePage(); this.startDOMObservation(); this.startContinuousCheck(); return true; } disable() { this.stopDOMObservation(); this.stopContinuousCheck(); document.querySelectorAll('.translated-text').forEach(el => { const originalId = el.dataset.originalId; if (originalId && this.originalTextMap.has(originalId)) { const originalText = this.originalTextMap.get(originalId); const textNode = document.createTextNode(originalText); el.parentNode.replaceChild(textNode, el); } }); this.originalTextMap.clear(); this.translationCache.clear(); this.active = false; console.log('微软翻译已禁用'); } async onLanguageChange() { if (!this.active) return; this.translationCache.clear(); await this.translateWholePage(); } async onIgnoreChange() { if (!this.active) return; document.querySelectorAll('.translated-text').forEach(el => { const originalId = el.dataset.originalId; if (originalId && this.originalTextMap.has(originalId)) { const originalText = this.originalTextMap.get(originalId); const textNode = document.createTextNode(originalText); el.parentNode.replaceChild(textNode, el); } }); this.originalTextMap.clear(); this.translationCache.clear(); await this.translateWholePage(); } async onModeColorChange() { this.mode = this.configManager.get('displayMode') || 'replace'; this.textColor = this.configManager.get('textColor') || '#0066cc'; this.bgColor = this.configManager.get('bgColor') || 'rgba(0,102,204,0.1)'; if (!this.active) return; document.querySelectorAll('.translated-text').forEach(el => { const originalId = el.dataset.originalId; if (originalId && this.originalTextMap.has(originalId)) { const originalText = this.originalTextMap.get(originalId); const textNode = document.createTextNode(originalText); el.parentNode.replaceChild(textNode, el); } }); this.originalTextMap.clear(); this.translationCache.clear(); await this.translateWholePage(); } } // ==================== translate.js 适配器(用于其他引擎) ==================== class TranslateJSAdapter { constructor(configManager) { this.configManager = configManager; this.active = false; this.listenerStarted = false; this.currentLanguage = null; this.initialized = false; } init() { if (this.initialized || typeof translate === 'undefined') return; try { if (!this.configManager.get('autoDetectLocal')) { translate.language.setLocal(this.configManager.get('localLanguage') || 'english'); } translate.selectLanguageTag.show = false; translate.showOrigin = false; if (this.configManager.get('enableCache')) { translate.storage.enable(); } else { translate.storage.disable(); } this.applyIgnoreSettings(); this.initialized = true; console.log('translate.js 适配器初始化完成'); } catch (e) { console.error('translate.js 适配器初始化失败:', e); } } applyIgnoreSettings() { if (typeof translate === 'undefined') return; translate.ignore.class = []; translate.ignore.id = []; const ignoredClasses = this.configManager.get('ignoredClasses'); if (ignoredClasses) { ignoredClasses.split(',').map(c => c.trim()).filter(c => c).forEach(cls => translate.ignore.class.push(cls)); } const ignoredIds = this.configManager.get('ignoredIds'); if (ignoredIds) { ignoredIds.split(',').map(id => id.trim()).filter(id => id).forEach(id => translate.ignore.id.push(id)); } } startListener() { if (typeof translate === 'undefined' || this.listenerStarted) return; try { translate.listener.start(); this.listenerStarted = true; console.log('translate.js 动态监听已启动'); } catch (e) { if (!e.message?.includes('已经启动')) console.warn('启动监听失败:', e); } } stopListener() { if (typeof translate === 'undefined' || !this.listenerStarted) return; try { translate.listener.stop(); this.listenerStarted = false; } catch (e) {} } async enable() { if (!this.configManager.shouldTranslate()) return false; this.init(); const service = this.configManager.get('translateService'); if (service === 'tencent' || service === 'client.edge' || service === 'microsoft') { return false; } let serviceName = service; if (service === 'giteeAI') serviceName = 'giteeAI'; else if (service === 'siliconflow') serviceName = 'siliconflow'; else if (service === 'translate.service.public') serviceName = 'translate.service'; else serviceName = 'client.edge'; // 实际上不会走到这里,因为上面已经过滤了 try { translate.service.use(serviceName); } catch (e) { console.error('设置翻译服务失败:', e); return false; } const targetLang = this.configManager.get('targetLanguage'); if (this.currentLanguage === targetLang && this.active) return true; this.currentLanguage = targetLang; this.active = true; try { translate.changeLanguage(targetLang); this.startListener(); console.log(`translate.js 引擎 (${serviceName}) 翻译启动,目标语言: ${targetLang}`); return true; } catch (e) { console.error('translate.js 翻译失败:', e); this.active = false; return false; } } disable() { if (typeof translate === 'undefined') return; this.stopListener(); try { translate.reset(); this.active = false; this.currentLanguage = null; console.log('translate.js 翻译已禁用'); } catch (e) { console.error('禁用 translate.js 失败:', e); } } async onLanguageChange() { if (!this.active) return; const targetLang = this.configManager.get('targetLanguage'); if (this.currentLanguage === targetLang) return; this.currentLanguage = targetLang; try { translate.changeLanguage(targetLang); } catch (e) { console.error('切换语言失败:', e); } } async onIgnoreChange() { if (!this.active) return; this.applyIgnoreSettings(); try { translate.changeLanguage(this.currentLanguage); } catch (e) {} } } // ==================== 配置管理器 ==================== class ConfigManager { constructor() { this.defaultConfig = { enabled: true, targetLanguage: 'chinese_simplified', localLanguage: 'english', autoDetectLocal: false, autoTranslate: false, translateService: 'client.edge', // 默认使用微软Edge(独立实现) siteSpecificServices: {}, panelPosition: { x: 100, y: 100 }, panelWidth: 380, panelHeight: 500, panelOpacity: 1, ignoredClasses: '', ignoredIds: '', enableCache: true, whitelist: [], displayMode: 'replace', textColor: '#0066cc', bgColor: 'rgba(0,102,204,0.1)' }; this.config = this.loadConfig(); } loadConfig() { const saved = GM_getValue('translateConfig', null); if (saved) { if (saved.microsoftMode) { saved.displayMode = saved.microsoftMode; delete saved.microsoftMode; } if (saved.microsoftTextColor) { saved.textColor = saved.microsoftTextColor; delete saved.microsoftTextColor; } if (saved.microsoftBgColor) { saved.bgColor = saved.microsoftBgColor; delete saved.microsoftBgColor; } if (!saved.translateService) saved.translateService = 'client.edge'; if (!saved.siteSpecificServices) saved.siteSpecificServices = {}; if (saved.autoDetectLocal === undefined) saved.autoDetectLocal = false; const merged = { ...this.defaultConfig, ...saved }; if (merged.panelWidth < 280) merged.panelWidth = 280; if (merged.panelHeight < 300) merged.panelHeight = 300; return merged; } return { ...this.defaultConfig }; } saveConfig() { GM_setValue('translateConfig', this.config); } get(key) { return this.config[key]; } set(key, value) { this.config[key] = value; this.saveConfig(); } reset() { GM_deleteValue('translateConfig'); this.config = { ...this.defaultConfig }; this.saveConfig(); } isInWhitelist(domain) { return this.config.whitelist.includes(domain); } addToWhitelist(domain) { if (!this.isInWhitelist(domain)) { this.config.whitelist.push(domain); this.saveConfig(); return true; } return false; } removeFromWhitelist(domain) { const index = this.config.whitelist.indexOf(domain); if (index !== -1) { this.config.whitelist.splice(index, 1); this.saveConfig(); return true; } return false; } clearWhitelist() { this.config.whitelist = []; this.saveConfig(); } toggleWhitelistDomain(domain) { if (this.isInWhitelist(domain)) { this.removeFromWhitelist(domain); return false; } else { this.addToWhitelist(domain); return true; } } shouldTranslate() { if (this.isInWhitelist(getCurrentDomain())) return true; return this.config.enabled; } getSiteService(domain) { return this.config.siteSpecificServices[domain] || null; } setSiteService(domain, service) { if (service && service !== this.config.translateService) { this.config.siteSpecificServices[domain] = service; } else { delete this.config.siteSpecificServices[domain]; } this.saveConfig(); } getEffectiveService(domain) { const site = this.getSiteService(domain); return site || this.config.translateService; } } // ==================== 翻译管理器 ==================== class TranslateManager { constructor(configManager) { this.configManager = configManager; this.tencentTranslator = null; this.microsoftTranslator = null; this.translateJSAdapter = null; this.currentService = null; this.currentLanguage = null; } getTencentTranslator() { if (!this.tencentTranslator) { this.tencentTranslator = new TencentTranslator(this.configManager); } return this.tencentTranslator; } getMicrosoftTranslator() { if (!this.microsoftTranslator) { this.microsoftTranslator = new MicrosoftTranslator(this.configManager); } return this.microsoftTranslator; } getTranslateJSAdapter() { if (!this.translateJSAdapter) { this.translateJSAdapter = new TranslateJSAdapter(this.configManager); } return this.translateJSAdapter; } async disableAllEngines() { // 禁用腾讯 if (this.tencentTranslator && this.tencentTranslator.active) { this.tencentTranslator.disable(); } // 禁用微软 if (this.microsoftTranslator && this.microsoftTranslator.active) { this.microsoftTranslator.disable(); } // 禁用 translate.js if (this.translateJSAdapter && this.translateJSAdapter.active) { this.translateJSAdapter.disable(); } // 清除残留 document.querySelectorAll('.translated-text').forEach(el => { // 注意:已经由各引擎恢复,这里只做安全清理 console.warn('发现残留翻译元素,可能恢复不完全', el); }); this.currentService = null; this.currentLanguage = null; await new Promise(r => setTimeout(r, 50)); } async applyTranslationState() { const domain = getCurrentDomain(); const should = this.configManager.shouldTranslate(); const service = this.configManager.getEffectiveService(domain); const targetLang = this.configManager.get('targetLanguage'); if (!should) { await this.disableAllEngines(); console.log('翻译已禁用(全局关闭或不在白名单)'); return; } if (this.currentService === service && this.currentLanguage === targetLang) { return; } await this.disableAllEngines(); if (service === 'tencent') { const translator = this.getTencentTranslator(); const ok = await translator.enable(); if (ok) { this.currentService = service; this.currentLanguage = targetLang; console.log(`启用腾讯翻译引擎,目标语言: ${targetLang}`); } } else if (service === 'client.edge' || service === 'microsoft') { const translator = this.getMicrosoftTranslator(); const ok = await translator.enable(); if (ok) { this.currentService = service; this.currentLanguage = targetLang; console.log(`启用微软翻译引擎(独立实现),目标语言: ${targetLang}`); } } else { const adapter = this.getTranslateJSAdapter(); const ok = await adapter.enable(); if (ok) { this.currentService = service; this.currentLanguage = targetLang; console.log(`启用 translate.js 引擎 (${service}),目标语言: ${targetLang}`); } else { console.error(`启用引擎 ${service} 失败`); } } } async refresh() { await this.applyTranslationState(); } async forceTranslate() { if (!this.configManager.shouldTranslate()) { await this.disableAllEngines(); return; } const domain = getCurrentDomain(); const service = this.configManager.getEffectiveService(domain); const targetLang = this.configManager.get('targetLanguage'); await this.disableAllEngines(); await new Promise(r => setTimeout(r, 100)); if (service === 'tencent') { const translator = this.getTencentTranslator(); translator.translationCache.clear(); await translator.enable(); } else if (service === 'client.edge' || service === 'microsoft') { const translator = this.getMicrosoftTranslator(); translator.translationCache.clear(); await translator.enable(); } else { const adapter = this.getTranslateJSAdapter(); await adapter.enable(); } this.currentService = service; this.currentLanguage = targetLang; } async changeLanguage(targetLang, force = false) { if (targetLang === null) { await this.disableAllEngines(); this.currentLanguage = null; return; } this.configManager.set('targetLanguage', targetLang); if (force) { await this.forceTranslate(); } else { await this.refresh(); } } async startAutoTranslate() { await new Promise(r => setTimeout(r, 300)); await this.applyTranslationState(); } async onLanguageChange() { await this.forceTranslate(); } async onIgnoreChange() { if (this.tencentTranslator && this.tencentTranslator.active) { this.tencentTranslator.translationCache.clear(); this.tencentTranslator.originalTextMap.clear(); this.tencentTranslator.disable(); await this.tencentTranslator.enable(); } if (this.microsoftTranslator && this.microsoftTranslator.active) { this.microsoftTranslator.translationCache.clear(); this.microsoftTranslator.originalTextMap.clear(); this.microsoftTranslator.disable(); await this.microsoftTranslator.enable(); } if (this.translateJSAdapter && this.translateJSAdapter.active) { this.translateJSAdapter.onIgnoreChange(); } } async onModeColorChange() { if (this.tencentTranslator && this.tencentTranslator.active) { await this.tencentTranslator.onModeColorChange(); } if (this.microsoftTranslator && this.microsoftTranslator.active) { await this.microsoftTranslator.onModeColorChange(); } } async onConfigChange() { await this.forceTranslate(); } } // ==================== 提示管理器 ==================== class ToastManager { constructor() { this.container = null; } ensureContainer() { if (!this.container) { this.container = document.createElement('div'); this.container.id = 'translate-toast-container'; this.container.style.cssText = ` position: fixed !important; top: 20px !important; right: 20px !important; z-index: 2147483647 !important; pointer-events: none !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important; `; document.body.appendChild(this.container); } } show(message, type = 'success', duration = 2000) { this.ensureContainer(); const toast = document.createElement('div'); const bgColor = type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'; toast.style.cssText = ` background: ${bgColor} !important; color: white !important; padding: 12px 20px !important; border-radius: 12px !important; margin-bottom: 10px !important; box-shadow: 0 10px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.02) !important; animation: slideInRight 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55) !important; pointer-events: auto !important; font-size: 14px !important; font-weight: 500 !important; backdrop-filter: blur(8px) !important; `; toast.textContent = message; this.container.appendChild(toast); setTimeout(() => { toast.style.animation = 'slideOutRight 0.3s ease !important'; setTimeout(() => { if (this.container.contains(toast)) this.container.removeChild(toast); }, 300); }, duration); } } // ==================== UI管理器 ==================== class UIManager { constructor(configManager, translateManager) { this.configManager = configManager; this.translateManager = translateManager; this.panel = null; this.shadowRoot = null; this.toast = new ToastManager(); this.initialized = false; this.MIN_W = 280; this.MIN_H = 300; this.MAX_W_RATIO = 0.92; this.MAX_H_RATIO = 0.88; this.panelWidth = this.configManager.get('panelWidth') || 380; this.panelHeight = this.configManager.get('panelHeight') || 500; } ensureInitialized() { if (this.initialized) return; this.injectGlobalStyles(); this.createPanel(); this.bindEvents(); this.initialized = true; } injectGlobalStyles() { GM_addStyle(` @keyframes slideInRight { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOutRight { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } @keyframes fadeIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } .translate-resize-slider { accent-color: #6c5ce7; height: 6px; background: transparent; -webkit-appearance: none; appearance: none; width: 100%; } .translate-resize-slider::-webkit-slider-runnable-track { background: #d9c9ff; border-radius: 4px; height: 6px; } .translate-resize-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; border-radius: 50%; background: #6c5ce7; cursor: pointer; margin-top: -5px; border: 1px solid #5a4bd1; } .translate-resize-slider::-moz-range-track { background: #d9c9ff; border-radius: 4px; height: 6px; border: none; } .translate-resize-slider::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: #6c5ce7; cursor: pointer; border: 1px solid #5a4bd1; } .translate-resize-slider:focus { outline: none; } .translated-text.no-style { all: unset !important; display: inline !important; color: inherit !important; background: none !important; border: none !important; text-decoration: none !important; font: inherit !important; cursor: inherit !important; } `); } createPanel() { const container = document.createElement('div'); container.id = 'translate-panel-shadow-host'; container.style.cssText = ` position: fixed !important; z-index: 2147483646 !important; top: 0; left: 0; width: 0; height: 0; margin: 0; padding: 0; border: 0; background: transparent; `; const shadowRoot = container.attachShadow({ mode: 'closed' }); this.shadowRoot = shadowRoot; document.body.appendChild(container); const config = this.configManager.config; const domain = getCurrentDomain(); const languages = this.getSupportedLanguages(); const services = [ { value: 'tencent', name: '腾讯翻译(内置)' }, { value: 'client.edge', name: '微软翻译(Edge,独立)' }, // 整合后的微软翻译 { value: 'giteeAI', name: 'Gitee AI 大模型' }, { value: 'siliconflow', name: 'SiliconFlow AI (Qwen3)' }, { value: 'translate.service.public', name: '公共 translate.service' } ]; const currentEffective = this.configManager.getEffectiveService(domain); const siteService = this.configManager.getSiteService(domain); const whitelist = this.configManager.get('whitelist'); const maxW = window.innerWidth * this.MAX_W_RATIO; const maxH = window.innerHeight * this.MAX_H_RATIO; let pw = this.panelWidth, ph = this.panelHeight; if (pw > maxW) pw = Math.max(this.MIN_W, maxW); if (ph > maxH) ph = Math.max(this.MIN_H, maxH); if (pw < this.MIN_W) pw = this.MIN_W; if (ph < this.MIN_H) ph = this.MIN_H; this.panelWidth = pw; this.panelHeight = ph; const panelHTML = ` `; shadowRoot.innerHTML = panelHTML; this.panel = shadowRoot.getElementById('translate-panel'); this.panel.style.display = 'none'; const style = document.createElement('style'); style.textContent = ` .translate-switch-slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 3px; bottom: 3px; background-color: white; transition: .2s; border-radius: 50%; box-shadow: 0 1px 2px rgba(0,0,0,0.1); } .translate-switch input:checked + .translate-switch-slider { background-color: #8b5cf6 !important; } .translate-switch input:checked + .translate-switch-slider:before { transform: translateX(18px); } .translate-select:focus, .translate-input:focus, .translate-textarea:focus { outline: none; border-color: #8b5cf6; box-shadow: 0 0 0 2px rgba(139,92,246,0.1); } .translate-button:hover { transform: translateY(-1px); filter: brightness(1.05); } .translate-panel-close:hover { background: rgba(0,0,0,0.2); transform: scale(1.05); } .translate-panel-close:active { transform: scale(0.95); } .translate-panel-resize-toggle:hover { background: rgba(0,0,0,0.2); transform: scale(1.05); } .translate-panel-resize-toggle:active { transform: scale(0.95); } .whitelist-scroll { max-height: 114px; overflow-y: auto; border: 1px solid rgba(0,0,0,0.1); border-radius: 8px; margin-bottom: 8px; } .whitelist-item { display: flex; justify-content: space-between; align-items: center; padding: 6px 8px; border-bottom: 1px solid rgba(0,0,0,0.05); } .whitelist-item:last-child { border-bottom: none; } .whitelist-item:hover { background: ${isDarkMode() ? 'rgba(70,70,75,0.6)' : '#f0f0f0'}; } .whitelist-domain { word-break: break-all; flex: 1; font-size: 12px; } .whitelist-delete { background: #ef4444; color: white; border: none; border-radius: 4px; padding: 2px 8px; font-size: 11px; cursor: pointer; margin-left: 8px; transition: background 0.2s; } .whitelist-delete:hover { background: #dc2626; } #reset-size-btn:hover { background: #5a4bd1 !important; } `; shadowRoot.appendChild(style); // ---- 拖拽功能 ---- const header = shadowRoot.querySelector('.translate-panel-header'); let dragData = null; const onDragStart = (e) => { if (e.target.closest('button')) return; const rect = this.panel.getBoundingClientRect(); const clientX = e.touches ? e.touches[0].clientX : e.clientX; const clientY = e.touches ? e.touches[0].clientY : e.clientY; dragData = { offsetX: clientX - rect.left, offsetY: clientY - rect.top, startLeft: rect.left, startTop: rect.top }; document.addEventListener('mousemove', onDragMove); document.addEventListener('mouseup', onDragEnd); document.addEventListener('touchmove', onDragMoveTouch, { passive: false }); document.addEventListener('touchend', onDragEndTouch, { passive: false }); e.preventDefault(); }; const onDragMove = (e) => { if (!dragData) return; const clientX = e.clientX; const clientY = e.clientY; let left = clientX - dragData.offsetX; let top = clientY - dragData.offsetY; const maxLeft = window.innerWidth - this.panel.offsetWidth; const maxTop = window.innerHeight - this.panel.offsetHeight; left = Math.max(0, Math.min(left, maxLeft)); top = Math.max(0, Math.min(top, maxTop)); this.panel.style.left = left + 'px'; this.panel.style.top = top + 'px'; this.panel.style.transform = 'none'; }; const onDragMoveTouch = (e) => { if (!dragData) return; const touch = e.touches[0]; const clientX = touch.clientX; const clientY = touch.clientY; let left = clientX - dragData.offsetX; let top = clientY - dragData.offsetY; const maxLeft = window.innerWidth - this.panel.offsetWidth; const maxTop = window.innerHeight - this.panel.offsetHeight; left = Math.max(0, Math.min(left, maxLeft)); top = Math.max(0, Math.min(top, maxTop)); this.panel.style.left = left + 'px'; this.panel.style.top = top + 'px'; this.panel.style.transform = 'none'; e.preventDefault(); }; const onDragEnd = () => { if (dragData) { this.configManager.set('panelPosition', { x: parseInt(this.panel.style.left) || 0, y: parseInt(this.panel.style.top) || 0 }); dragData = null; } document.removeEventListener('mousemove', onDragMove); document.removeEventListener('mouseup', onDragEnd); document.removeEventListener('touchmove', onDragMoveTouch); document.removeEventListener('touchend', onDragEndTouch); }; const onDragEndTouch = (e) => { onDragEnd(); e.preventDefault(); }; header.addEventListener('mousedown', onDragStart); header.addEventListener('touchstart', onDragStart, { passive: false }); this.refreshWhitelistDisplay(); window._translatePanel = this.panel; } getSupportedLanguages() { return [ { value: 'chinese_simplified', name: '简体中文' }, { value: 'chinese_traditional', name: '繁體中文' }, { value: 'english', name: 'English' }, { value: 'spanish', name: 'Español' }, { value: 'french', name: 'Français' }, { value: 'german', name: 'Deutsch' }, { value: 'japanese', name: '日本語' }, { value: 'korean', name: '한국어' }, { value: 'russian', name: 'Русский' }, { value: 'portuguese', name: 'Português' }, { value: 'italian', name: 'Italiano' }, { value: 'dutch', name: 'Nederlands' }, { value: 'polish', name: 'Polski' }, { value: 'turkish', name: 'Türkçe' }, { value: 'vietnamese', name: 'Tiếng Việt' }, { value: 'thai', name: 'ไทย' }, { value: 'indonesian', name: 'Bahasa Indonesia' }, { value: 'arabic', name: 'العربية' }, { value: 'hindi', name: 'हिन्दी' } ]; } getColorPresets() { return [ { name: '默认蓝', value: '#0066cc', bg: 'rgba(0,102,204,0.1)' }, { name: '绿色', value: '#00b894', bg: 'rgba(0,184,148,0.1)' }, { name: '红色', value: '#ff4757', bg: 'rgba(255,71,87,0.1)' }, { name: '紫色', value: '#6c5ce7', bg: 'rgba(108,92,231,0.1)' }, { name: '橙色', value: '#ff9f43', bg: 'rgba(255,159,67,0.1)' }, { name: '深灰', value: '#2d3436', bg: 'rgba(45,52,54,0.1)' } ]; } refreshWhitelistDisplay() { const shadow = this.shadowRoot; if (!shadow) return; const container = shadow.getElementById('whitelist-container'); if (!container) return; const whitelist = this.configManager.get('whitelist'); const count = whitelist.length; const countSpan = shadow.getElementById('whitelist-count'); if (countSpan) countSpan.textContent = `当前白名单网站数: ${count}`; if (count === 0) { container.innerHTML = '
暂无白名单网站
'; return; } const itemsHtml = whitelist.map(domain => `
${this.escapeHtml(domain)}
`).join(''); container.innerHTML = `
${itemsHtml}
`; const deleteBtns = container.querySelectorAll('.whitelist-delete'); deleteBtns.forEach(btn => { btn.addEventListener('click', async (e) => { const domain = btn.getAttribute('data-domain'); if (domain && confirm(`确定从白名单中删除 "${domain}" 吗?`)) { this.configManager.removeFromWhitelist(domain); this.refreshWhitelistDisplay(); await this.translateManager.refresh(); this.toast.show(`已删除 ${domain},页面已恢复原文`, 'success', 2000); if (window.updateMenuItems) window.updateMenuItems(); } }); }); } escapeHtml(str) { return str.replace(/[&<>]/g, function(m) { if (m === '&') return '&'; if (m === '<') return '<'; if (m === '>') return '>'; return m; }); } bindEvents() { const shadow = this.shadowRoot; shadow.getElementById('translate-panel-close')?.addEventListener('click', () => this.togglePanel()); // 源语言 - 自动检测 const autoDetectCb = shadow.getElementById('auto-detect-local'); const localLangSelect = shadow.getElementById('translate-local-lang'); if (autoDetectCb) { autoDetectCb.addEventListener('change', (e) => { const checked = e.target.checked; this.configManager.set('autoDetectLocal', checked); localLangSelect.disabled = checked; this.translateManager.onConfigChange(); this.toast.show(checked ? '✅ 已启用源语言自动识别' : '✅ 已使用指定源语言'); }); } if (localLangSelect) { localLangSelect.addEventListener('change', (e) => { this.configManager.set('localLanguage', e.target.value); if (!this.configManager.get('autoDetectLocal')) { this.translateManager.onConfigChange(); this.toast.show('✅ 源语言已更新'); } }); } shadow.getElementById('translate-target-lang')?.addEventListener('change', async (e) => { this.configManager.set('targetLanguage', e.target.value); await this.translateManager.onLanguageChange(); this.toast.show('✅ 目标语言已更新'); }); // 全局引擎 shadow.getElementById('translate-service')?.addEventListener('change', async (e) => { const service = e.target.value; this.configManager.set('translateService', service); await this.translateManager.forceTranslate(); this.toast.show(`✅ 已切换至 ${e.target.options[e.target.selectedIndex].text},正在翻译...`); // 更新专用引擎显示 const domain = getCurrentDomain(); const effective = this.configManager.getEffectiveService(domain); const desc = shadow.querySelector('#site-specific-service')?.closest('.translate-control-group')?.querySelector('.translate-description:last-child'); if (desc) { const services = [ { value: 'tencent', name: '腾讯翻译(内置)' }, { value: 'client.edge', name: '微软翻译(Edge,独立)' }, { value: 'giteeAI', name: 'Gitee AI 大模型' }, { value: 'siliconflow', name: 'SiliconFlow AI (Qwen3)' }, { value: 'translate.service.public', name: '公共 translate.service' } ]; desc.textContent = `当前生效: ${services.find(s => s.value === effective)?.name || effective}`; } }); // 专用引擎 shadow.getElementById('save-site-service')?.addEventListener('click', async () => { const service = shadow.getElementById('site-specific-service').value; const domain = getCurrentDomain(); await this.translateManager.disableAllEngines(); this.configManager.setSiteService(domain, service || null); this.toast.show(`✅ 已为 ${domain} ${service ? '设置专用引擎' : '清除专用引擎'}`); const effective = this.configManager.getEffectiveService(domain); const desc = shadow.querySelector('#site-specific-service')?.closest('.translate-control-group')?.querySelector('.translate-description:last-child'); if (desc) { const services = [ { value: 'tencent', name: '腾讯翻译(内置)' }, { value: 'client.edge', name: '微软翻译(Edge,独立)' }, { value: 'giteeAI', name: 'Gitee AI 大模型' }, { value: 'siliconflow', name: 'SiliconFlow AI (Qwen3)' }, { value: 'translate.service.public', name: '公共 translate.service' } ]; desc.textContent = `当前生效: ${services.find(s => s.value === effective)?.name || effective}`; } await this.translateManager.forceTranslate(); }); shadow.getElementById('translate-clear-whitelist')?.addEventListener('click', async () => { if (confirm('确定清空所有白名单网站?')) { this.configManager.clearWhitelist(); this.toast.show('✅ 白名单已清空'); this.refreshWhitelistDisplay(); await this.translateManager.refresh(); if (window.updateMenuItems) window.updateMenuItems(); } }); const modeReplace = shadow.getElementById('mode-replace'); const modeDual = shadow.getElementById('mode-dual'); const modeNone = shadow.getElementById('mode-none'); const setMode = async (mode) => { if (this.configManager.get('displayMode') !== mode) { this.configManager.set('displayMode', mode); await this.translateManager.onModeColorChange(); [modeReplace, modeDual, modeNone].forEach(btn => { btn.style.background = '#e5e7eb'; btn.style.color = '#333'; }); if (mode === 'replace') { modeReplace.style.background = 'linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%)'; modeReplace.style.color = 'white'; } else if (mode === 'dual') { modeDual.style.background = 'linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%)'; modeDual.style.color = 'white'; } else { modeNone.style.background = 'linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%)'; modeNone.style.color = 'white'; } const modeName = { 'replace': '单显', 'dual': '双显', 'none': '无感' }[mode]; this.toast.show(`✅ 已切换为${modeName}模式(仅腾讯/微软引擎生效)`); } }; modeReplace?.addEventListener('click', () => setMode('replace')); modeDual?.addEventListener('click', () => setMode('dual')); modeNone?.addEventListener('click', () => setMode('none')); const colorPresets = shadow.querySelectorAll('.color-preset'); colorPresets.forEach(preset => { preset.addEventListener('click', async () => { const color = preset.dataset.color; const bg = preset.dataset.bg; this.configManager.set('textColor', color); this.configManager.set('bgColor', bg); await this.translateManager.onModeColorChange(); this.toast.show(`✅ 译文颜色已更新`); colorPresets.forEach(p => p.style.border = '2px solid transparent'); preset.style.border = `2px solid white`; const colorText = shadow.getElementById('color-text'); if (colorText) colorText.value = color; const colorPicker = shadow.getElementById('color-custom'); if (colorPicker) colorPicker.value = color; }); }); const colorCustom = shadow.getElementById('color-custom'); const colorText = shadow.getElementById('color-text'); const colorApply = shadow.getElementById('color-apply'); if (colorCustom) { colorCustom.addEventListener('change', async (e) => { const color = e.target.value; if (colorText) colorText.value = color; this.configManager.set('textColor', color); this.configManager.set('bgColor', color.replace('#', 'rgba(') + ',0.1)'); await this.translateManager.onModeColorChange(); this.toast.show(`✅ 自定义颜色已应用`); const presets = shadow.querySelectorAll('.color-preset'); presets.forEach(p => p.style.border = '2px solid transparent'); }); } if (colorApply) { colorApply.addEventListener('click', async () => { let color = colorText.value.trim(); if (!/^#[0-9A-Fa-f]{6}$/.test(color) && !/^#[0-9A-Fa-f]{3}$/.test(color)) { this.toast.show('颜色格式错误,请使用#RRGGBB格式', 'error'); return; } if (color.length === 4) { color = '#' + color[1]+color[1]+color[2]+color[2]+color[3]+color[3]; } this.configManager.set('textColor', color); this.configManager.set('bgColor', color.replace('#', 'rgba(') + ',0.1)'); await this.translateManager.onModeColorChange(); this.toast.show(`✅ 颜色已应用`); const colorPicker = shadow.getElementById('color-custom'); if (colorPicker) colorPicker.value = color; const presets = shadow.querySelectorAll('.color-preset'); presets.forEach(p => p.style.border = '2px solid transparent'); }); } const resizeToggle = shadow.getElementById('translate-panel-resize-toggle'); const resizeControls = shadow.getElementById('translate-resize-controls'); const widthSlider = shadow.getElementById('resize-width-slider'); const heightSlider = shadow.getElementById('resize-height-slider'); const widthValue = shadow.getElementById('resize-width-value'); const heightValue = shadow.getElementById('resize-height-value'); const resetBtn = shadow.getElementById('reset-size-btn'); if (resizeToggle) { resizeToggle.addEventListener('click', (e) => { e.stopPropagation(); if (resizeControls.style.display === 'none' || resizeControls.style.display === '') { resizeControls.style.display = 'flex'; this.updateSliderMax(); } else { resizeControls.style.display = 'none'; } }); } if (widthSlider) { widthSlider.addEventListener('input', () => { const w = parseInt(widthSlider.value); this.panel.style.width = w + 'px'; widthValue.textContent = w + 'px'; this.panelWidth = w; this.configManager.set('panelWidth', w); this.ensureInViewport(); }); } if (heightSlider) { heightSlider.addEventListener('input', () => { const h = parseInt(heightSlider.value); this.panel.style.height = h + 'px'; heightValue.textContent = h + 'px'; this.panelHeight = h; this.configManager.set('panelHeight', h); this.ensureInViewport(); }); } if (resetBtn) { resetBtn.addEventListener('click', () => { const defaultW = 380, defaultH = 500; this.panel.style.width = defaultW + 'px'; this.panel.style.height = defaultH + 'px'; widthSlider.value = defaultW; heightSlider.value = defaultH; widthValue.textContent = defaultW + 'px'; heightValue.textContent = defaultH + 'px'; this.panelWidth = defaultW; this.panelHeight = defaultH; this.configManager.set('panelWidth', defaultW); this.configManager.set('panelHeight', defaultH); this.ensureInViewport(); this.toast.show('✅ 已重置窗口尺寸', 'success'); }); } window.addEventListener('resize', () => { this.updateSliderMax(); this.ensureInViewport(); }); } updateSliderMax() { const shadow = this.shadowRoot; if (!shadow) return; const wSlider = shadow.getElementById('resize-width-slider'); const hSlider = shadow.getElementById('resize-height-slider'); const maxW = Math.min(800, window.innerWidth * this.MAX_W_RATIO); const maxH = Math.min(800, window.innerHeight * this.MAX_H_RATIO); if (wSlider) wSlider.max = maxW; if (hSlider) hSlider.max = maxH; let changed = false; if (this.panelWidth > maxW) { this.panelWidth = Math.max(this.MIN_W, maxW); this.panel.style.width = this.panelWidth + 'px'; if (wSlider) wSlider.value = this.panelWidth; const wv = shadow.getElementById('resize-width-value'); if (wv) wv.textContent = this.panelWidth + 'px'; this.configManager.set('panelWidth', this.panelWidth); changed = true; } if (this.panelHeight > maxH) { this.panelHeight = Math.max(this.MIN_H, maxH); this.panel.style.height = this.panelHeight + 'px'; if (hSlider) hSlider.value = this.panelHeight; const hv = shadow.getElementById('resize-height-value'); if (hv) hv.textContent = this.panelHeight + 'px'; this.configManager.set('panelHeight', this.panelHeight); changed = true; } if (changed) this.ensureInViewport(); } togglePanel() { this.ensureInitialized(); if (this.panel.style.display === 'none' || this.panel.style.display === '') { this.panel.style.display = 'flex'; const controls = this.shadowRoot.getElementById('translate-resize-controls'); if (controls && controls.style.display === 'flex') { this.updateSliderMax(); } this.ensureInViewport(); this.refreshWhitelistDisplay(); } else { this.panel.style.display = 'none'; } } ensureInViewport() { if (!this.panel || this.panel.style.display !== 'flex') return; let left = parseInt(this.panel.style.left); let top = parseInt(this.panel.style.top); if (isNaN(left)) left = 100; if (isNaN(top)) top = 100; const w = this.panel.offsetWidth, h = this.panel.offsetHeight; const maxX = window.innerWidth - w, maxY = window.innerHeight - h; let changed = false; if (left < 0) { left = 0; changed = true; } if (top < 0) { top = 0; changed = true; } if (left > maxX) { left = maxX; changed = true; } if (top > maxY) { top = maxY; changed = true; } if (changed) { this.panel.style.left = `${left}px`; this.panel.style.top = `${top}px`; this.configManager.set('panelPosition', { x: left, y: top }); } } } // ==================== 菜单管理 ==================== let menuManager = null; let uiManagerInstance = null; let configManagerInstance = null; let translateManagerInstance = null; function setupMenu(configManager, translateManager, uiManager) { function updateMenuItems() { if (menuManager) { if (menuManager.toggleEnabledId) GM_unregisterMenuCommand(menuManager.toggleEnabledId); if (menuManager.toggleWhitelistId) GM_unregisterMenuCommand(menuManager.toggleWhitelistId); if (menuManager.openSettingsId) GM_unregisterMenuCommand(menuManager.openSettingsId); } const enabled = configManager.get('enabled'); const domain = getCurrentDomain(); const inWhitelist = configManager.isInWhitelist(domain); const toggleEnabledText = enabled ? '❌ 关闭自动翻译' : '✅ 开启自动翻译'; const toggleWhitelistText = inWhitelist ? '⭐ 从白名单中移除' : '☆ 添加到白名单'; const toggleEnabledId = GM_registerMenuCommand(toggleEnabledText, async () => { const newState = !configManager.get('enabled'); configManager.set('enabled', newState); configManager.set('autoTranslate', newState); await translateManager.refresh(); if (uiManager) uiManager.toast.show(newState ? '✅ 全局翻译已开启' : '❌ 全局翻译已关闭'); updateMenuItems(); }); const toggleWhitelistId = GM_registerMenuCommand(toggleWhitelistText, async () => { const nowInWhitelist = configManager.toggleWhitelistDomain(domain); if (uiManager) { uiManager.toast.show(nowInWhitelist ? `✅ 已添加 ${domain} 到白名单` : `❌ 已从白名单移除 ${domain},页面已恢复原文`); uiManager.refreshWhitelistDisplay(); } await translateManager.refresh(); updateMenuItems(); }); const openSettingsId = GM_registerMenuCommand('⚙️ 翻译设置', () => { if (!uiManager) { uiManagerInstance = new UIManager(configManager, translateManager); uiManager = uiManagerInstance; } uiManager.togglePanel(); }); menuManager = { toggleEnabledId, toggleWhitelistId, openSettingsId }; } updateMenuItems(); window.updateMenuItems = updateMenuItems; } // ==================== 初始化 ==================== async function init() { if (document.readyState === 'loading') await new Promise(r => document.addEventListener('DOMContentLoaded', r)); try { if (typeof translate === 'undefined') { console.error('translate.js 未加载,请检查 @require 配置'); } const configManager = new ConfigManager(); const translateManager = new TranslateManager(configManager); configManagerInstance = configManager; translateManagerInstance = translateManager; const uiManager = new UIManager(configManager, translateManager); uiManagerInstance = uiManager; setupMenu(configManager, translateManager, uiManager); await translateManager.startAutoTranslate(); window.translateHelper = { config: configManager, translate: translateManager, ui: uiManagerInstance }; console.log('自动翻译助手 v4.4.0 已加载(集成独立微软翻译引擎)'); } catch (error) { console.error('翻译脚本初始化失败:', error); } } setTimeout(init, 100); })();