// ==UserScript== // @name AI Chat翻译助手 // @namespace https://translate-assistant.com // @license lsr // @version 2.7.1 // @description 智能AI翻译助手,支持全页翻译、划词翻译、悬浮翻译等多种模式,集成多家翻译API,自动检测语言并智能显示翻译按钮(增强360极速浏览器X兼容性) // @author Your Name // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // @grant GM_notification // @grant GM_setClipboard // @grant GM_addStyle // @run-at document-end // @icon https://translate-assistant.com/favicon.ico // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js // @downloadURL https://update.greasyfork.icu/scripts/558853/AI%20Chat%E7%BF%BB%E8%AF%91%E5%8A%A9%E6%89%8B.user.js // @updateURL https://update.greasyfork.icu/scripts/558853/AI%20Chat%E7%BF%BB%E8%AF%91%E5%8A%A9%E6%89%8B.meta.js // ==/UserScript== (function() { 'use strict'; // MD5加密函数(用于百度、有道等API的签名生成) function md5(str) { const rotateLeft = function(lValue, iShiftBits) { return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits)); }; const addUnsigned = function(lX, lY) { const lX4 = lX & 0x40000000; const lY4 = lY & 0x40000000; const lX8 = lX & 0x80000000; const lY8 = lY & 0x80000000; const lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF); if (lX4 & lY4) return (lResult ^ 0x80000000 ^ lX8 ^ lY8); if (lX4 | lY4) { if (lResult & 0x40000000) return (lResult ^ 0xC0000000 ^ lX8 ^ lY8); else return (lResult ^ 0x40000000 ^ lX8 ^ lY8); } else return (lResult ^ lX8 ^ lY8); }; const F = function(x, y, z) { return (x & y) | ((~x) & z); }; const G = function(x, y, z) { return (x & z) | (y & (~z)); }; const H = function(x, y, z) { return (x ^ y ^ z); }; const I = function(x, y, z) { return (y ^ (x | (~z))); }; const FF = function(a, b, c, d, x, s, ac) { a = addUnsigned(a, addUnsigned(addUnsigned(F(b, c, d), x), ac)); return addUnsigned(rotateLeft(a, s), b); }; const GG = function(a, b, c, d, x, s, ac) { a = addUnsigned(a, addUnsigned(addUnsigned(G(b, c, d), x), ac)); return addUnsigned(rotateLeft(a, s), b); }; const HH = function(a, b, c, d, x, s, ac) { a = addUnsigned(a, addUnsigned(addUnsigned(H(b, c, d), x), ac)); return addUnsigned(rotateLeft(a, s), b); }; const II = function(a, b, c, d, x, s, ac) { a = addUnsigned(a, addUnsigned(addUnsigned(I(b, c, d), x), ac)); return addUnsigned(rotateLeft(a, s), b); }; const convertToWordArray = function(str) { let lWordCount; const lMessageLength = str.length; const lNumberOfWords_temp1 = lMessageLength + 8; const lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64; const lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16; const lWordArray = new Array(lNumberOfWords - 1); let lBytePosition = 0; let lByteCount = 0; while (lByteCount < lMessageLength) { lWordCount = (lByteCount - (lByteCount % 4)) / 4; lBytePosition = (lByteCount % 4) * 8; lWordArray[lWordCount] = (lWordArray[lWordCount] | (str.charCodeAt(lByteCount) << lBytePosition)); lByteCount++; } lWordCount = (lByteCount - (lByteCount % 4)) / 4; lBytePosition = (lByteCount % 4) * 8; lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition); lWordArray[lNumberOfWords - 2] = lMessageLength << 3; lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29; return lWordArray; }; const wordToHex = function(lValue) { let wordToHexValue = '', wordToHexValue_temp = '', lByte, lCount; for (lCount = 0; lCount <= 3; lCount++) { lByte = (lValue >>> (lCount * 8)) & 255; wordToHexValue_temp = '0' + lByte.toString(16); wordToHexValue = wordToHexValue + wordToHexValue_temp.substr(wordToHexValue_temp.length - 2, 2); } return wordToHexValue; }; const x = convertToWordArray(str); let a = 0x67452301, b = 0xEFCDAB89, c = 0x98BADCFE, d = 0x10325476; const S11=7, S12=12, S13=17, S14=22; const S21=5, S22=9 , S23=14, S24=20; const S31=4, S32=11, S33=16, S34=23; const S41=6, S42=10, S43=15, S44=21; for (let k = 0; k < x.length; k += 16) { const AA = a, BB = b, CC = c, DD = d; a = FF(a, b, c, d, x[k+0], S11, 0xD76AA478); d = FF(d, a, b, c, x[k+1], S12, 0xE8C7B756); c = FF(c, d, a, b, x[k+2], S13, 0x242070DB); b = FF(b, c, d, a, x[k+3], S14, 0xC1BDCEEE); a = FF(a, b, c, d, x[k+4], S11, 0xF57C0FAF); d = FF(d, a, b, c, x[k+5], S12, 0x4787C62A); c = FF(c, d, a, b, x[k+6], S13, 0xA8304613); b = FF(b, c, d, a, x[k+7], S14, 0xFD469501); a = FF(a, b, c, d, x[k+8], S11, 0x698098D8); d = FF(d, a, b, c, x[k+9], S12, 0x8B44F7AF); c = FF(c, d, a, b, x[k+10], S13, 0xFFFF5BB1); b = FF(b, c, d, a, x[k+11], S14, 0x895CD7BE); a = FF(a, b, c, d, x[k+12], S11, 0x6B901122); d = FF(d, a, b, c, x[k+13], S12, 0xFD987193); c = FF(c, d, a, b, x[k+14], S13, 0xA679438E); b = FF(b, c, d, a, x[k+15], S14, 0x49B40821); a = GG(a, b, c, d, x[k+1], S21, 0xF61E2562); d = GG(d, a, b, c, x[k+6], S22, 0xC040B340); c = GG(c, d, a, b, x[k+11], S23, 0x265E5A51); b = GG(b, c, d, a, x[k+0], S24, 0xE9B6C7AA); a = GG(a, b, c, d, x[k+5], S21, 0xD62F105D); d = GG(d, a, b, c, x[k+10], S22, 0x2441453); c = GG(c, d, a, b, x[k+15], S23, 0xD8A1E681); b = GG(b, c, d, a, x[k+4], S24, 0xE7D3FBC8); a = GG(a, b, c, d, x[k+9], S21, 0x21E1CDE6); d = GG(d, a, b, c, x[k+14], S22, 0xC33707D6); c = GG(c, d, a, b, x[k+3], S23, 0xF4D50D87); b = GG(b, c, d, a, x[k+8], S24, 0x455A14ED); a = GG(a, b, c, d, x[k+13], S21, 0xA9E3E905); d = GG(d, a, b, c, x[k+2], S22, 0xFCEFA3F8); c = GG(c, d, a, b, x[k+7], S23, 0x676F02D9); b = GG(b, c, d, a, x[k+12], S24, 0x8D2A4C8A); a = HH(a, b, c, d, x[k+5], S31, 0xFFFA3942); d = HH(d, a, b, c, x[k+8], S32, 0x8771F681); c = HH(c, d, a, b, x[k+11], S33, 0x6D9D6122); b = HH(b, c, d, a, x[k+14], S34, 0xFDE5380C); a = HH(a, b, c, d, x[k+1], S31, 0xA4BEEA44); d = HH(d, a, b, c, x[k+4], S32, 0x4BDECFA9); c = HH(c, d, a, b, x[k+7], S33, 0xF6BB4B60); b = HH(b, c, d, a, x[k+10], S34, 0xBEBFBC70); a = HH(a, b, c, d, x[k+13], S31, 0x289B7EC6); d = HH(d, a, b, c, x[k+0], S32, 0xEAA127FA); c = HH(c, d, a, b, x[k+3], S33, 0xD4EF3085); b = HH(b, c, d, a, x[k+6], S34, 0x4881D05); a = HH(a, b, c, d, x[k+9], S31, 0xD9D4D039); d = HH(d, a, b, c, x[k+12], S32, 0xE6DB99E5); c = HH(c, d, a, b, x[k+15], S33, 0x1FA27CF8); b = HH(b, c, d, a, x[k+2], S34, 0xC4AC5665); a = II(a, b, c, d, x[k+0], S41, 0xF4292244); d = II(d, a, b, c, x[k+7], S42, 0x432AFF97); c = II(c, d, a, b, x[k+14], S43, 0xAB9423A7); b = II(b, c, d, a, x[k+5], S44, 0xFC93A039); a = II(a, b, c, d, x[k+12], S41, 0x655B59C3); d = II(d, a, b, c, x[k+3], S42, 0x8F0CCC92); c = II(c, d, a, b, x[k+10], S43, 0xFFEFF47D); b = II(b, c, d, a, x[k+1], S44, 0x85845DD1); a = II(a, b, c, d, x[k+8], S41, 0x6FA87E4F); d = II(d, a, b, c, x[k+15], S42, 0xFE2CE6E0); c = II(c, d, a, b, x[k+6], S43, 0xA3014314); b = II(b, c, d, a, x[k+13], S44, 0x4E0811A1); a = II(a, b, c, d, x[k+4], S41, 0xF7537E82); d = II(d, a, b, c, x[k+11], S42, 0xBD3AF235); c = II(c, d, a, b, x[k+2], S43, 0x2AD7D2BB); b = II(b, c, d, a, x[k+9], S44, 0xEB86D391); a = addUnsigned(a, AA); b = addUnsigned(b, BB); c = addUnsigned(c, CC); d = addUnsigned(d, DD); } return (wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d)).toLowerCase(); } // 简单的字符串哈希函数(用于生成缓存键) function hash(str) { let hash = 5381; for (let i = 0; i < str.length; i++) { hash = ((hash << 5) + hash) + str.charCodeAt(i); } return hash.toString(36); } // 域名匹配函数 function matchDomain(domainPattern, currentDomain) { // 处理通配符模式 if (domainPattern.startsWith('*.')) { const suffix = domainPattern.substring(2); return currentDomain === suffix || currentDomain.endsWith('.' + suffix); } // 精确匹配 return domainPattern === currentDomain; } // 检查当前域名是否被屏蔽 function isDomainBlocked() { const blockedDomains = GM_getValue('blocked_domains', ''); if (!blockedDomains) return false; const domains = blockedDomains.split('\n').map(d => d.trim()).filter(d => d); const currentDomain = window.location.hostname; const isBlocked = domains.some(domain => matchDomain(domain, currentDomain)); if (isBlocked) { console.log(`域名 ${currentDomain} 被屏蔽`); } return isBlocked; } // 全局配置对象 const config = { translation: { defaultService: 'google', autoDetect: true, sourceLang: 'auto', targetLang: 'zh-CN', timeout: 10000, maxRetries: 3, concurrentRequests: 15 }, ui: { theme: 'light', panelPosition: { x: 100, y: 100 }, panelLocked: false, showProgress: true, showWordCount: true }, features: { fullPageTranslation: true, wordTranslation: true, hoverTranslation: true, paragraphTranslation: true, cacheEnabled: true, autoUpdate: true, languageDetection: true, autoShowButton: true, wordTranslate: false, // 划词翻译功能默认关闭 hoverTranslate: false, // 悬浮翻译功能默认关闭 domainBlocked: false // 域名是否被屏蔽 }, shortcut: { translate: 'Ctrl+Shift+T', togglePanel: 'Alt+T' } }; // API服务配置 const apiServices = { google: { name: 'Google Translate', endpoint: 'https://translate.googleapis.com/translate_a/single', requiresKey: false }, baidu: { name: 'Baidu Translate', endpoint: 'https://fanyi-api.baidu.com/api/trans/vip/translate', requiresKey: true }, youdao: { name: 'Youdao Translate', endpoint: 'https://openapi.youdao.com/api', requiresKey: true }, siliconflow: { name: 'SiliconFlow', endpoint: 'https://api.siliconflow.cn/v1/chat/completions', requiresKey: true }, deepseek: { name: 'DeepSeek', endpoint: 'https://api.deepseek.com/v1/chat/completions', requiresKey: true }, custom: { name: 'Custom Model', endpoint: '', // 将在运行时从设置中获取 requiresKey: true } }; // 全局语言映射函数 function mapLangCode(langCode, service) { const langMaps = { baidu: { 'zh-CN': 'zh', 'zh-TW': 'cht', 'en': 'en', 'ja': 'jp', 'ko': 'kor', 'fr': 'fra', 'de': 'de', 'es': 'spa', 'ru': 'ru', 'pt': 'pt', 'it': 'it', 'ar': 'ara', 'hi': 'hi', 'auto': 'auto' }, youdao: { 'zh-CN': 'zh-CHS', 'zh-TW': 'zh-CHT', 'en': 'en', 'ja': 'ja', 'ko': 'ko', 'fr': 'fr', 'de': 'de', 'es': 'es', 'ru': 'ru', 'pt': 'pt', 'it': 'it', 'ar': 'ar', 'hi': 'hi', 'auto': 'auto' } }; if (langMaps[service] && langMaps[service][langCode]) { return langMaps[service][langCode]; } return langCode; } // 语言检测类 class LanguageDetector { constructor() { this.pageLanguage = null; this.systemLanguage = navigator.language || navigator.userLanguage; } // 检测页面语言 detectPageLanguage() { // 优先从HTML标签获取 let lang = document.documentElement.lang || document.querySelector('meta[http-equiv="content-language"]')?.content || document.querySelector('meta[name="language"]')?.content; if (!lang) { // 通过文本内容判断 lang = this.detectByContent(); } this.pageLanguage = this.normalizeLangCode(lang); console.log('检测到页面语言:', this.pageLanguage); return this.pageLanguage; } // 通过内容检测语言 detectByContent() { const sampleText = document.body.innerText.substring(0, 3000); // 统计各类字符 const chineseChars = sampleText.match(/[\u4e00-\u9fa5]/g); const englishChars = sampleText.match(/[a-zA-Z]/g); const japaneseChars = sampleText.match(/[\u3040-\u309f\u30a0-\u30ff]/g); const koreanChars = sampleText.match(/[\uac00-\ud7af]/g); const arabicChars = sampleText.match(/[\u0600-\u06FF]/g); const counts = { 'zh': chineseChars ? chineseChars.length : 0, 'en': englishChars ? englishChars.length : 0, 'ja': japaneseChars ? japaneseChars.length : 0, 'ko': koreanChars ? koreanChars.length : 0, 'ar': arabicChars ? arabicChars.length : 0 }; // 计算中文字符占比 const totalChars = Object.values(counts).reduce((sum, count) => sum + count, 0); const chineseRatio = totalChars > 0 ? counts['zh'] / totalChars : 0; // 如果中文字符占比超过25%,认为是中文页面 if (chineseRatio > 0.25) { return 'zh'; } // 检查是否包含明显的中文标点符号和常用汉字 const chinesePunctuation = sampleText.match(/[,。!?;:''()【】《》]/g); const commonChineseWords = sampleText.match(/[的一是不了人我在有他这为之大来以个中上们]/g); const punctuationCount = chinesePunctuation ? chinesePunctuation.length : 0; const commonWordsCount = commonChineseWords ? commonChineseWords.length : 0; // 如果有足够多的中文标点和常用字,也认为是中文 if (punctuationCount > 5 && commonWordsCount > 10) { return 'zh'; } // 否则找出最多的语言 const maxLang = Object.keys(counts).reduce((a, b) => counts[a] > counts[b] ? a : b); return maxLang; } // 标准化语言代码 normalizeLangCode(langCode) { if (!langCode) return 'en'; langCode = langCode.toLowerCase(); const map = { 'zh': 'zh-CN', 'zh-cn': 'zh-CN', 'zh-hans': 'zh-CN', 'zh-tw': 'zh-TW', 'zh-hk': 'zh-TW', 'zh-hant': 'zh-TW', 'en': 'en', 'en-us': 'en', 'en-gb': 'en', 'ja': 'ja', 'ko': 'ko', 'fr': 'fr', 'de': 'de', 'es': 'es', 'ru': 'ru', 'pt': 'pt', 'it': 'it', 'ar': 'ar', 'hi': 'hi' }; return map[langCode] || langCode; } // 判断是否需要显示翻译按钮 shouldShowTranslateButton() { const pageLang = this.detectPageLanguage(); const sysLang = this.normalizeLangCode(this.systemLanguage); console.log('页面语言:', pageLang, '系统语言:', sysLang); // 如果都是中文(简体或繁体),不显示翻译按钮 const pageIsChinese = pageLang === 'zh-CN' || pageLang === 'zh-TW'; const sysIsChinese = sysLang === 'zh-CN' || sysLang === 'zh-TW'; if (pageIsChinese && sysIsChinese) { return false; } // 如果语言不匹配,显示翻译按钮 return pageLang !== sysLang; } } // 核心翻译引擎类 class TranslationEngine { constructor() { // 初始化时使用最佳可用服务 this.currentService = this.getBestAvailableService(); this.cache = new TranslationCache(); this.requestQueue = new RequestQueue(config.translation.concurrentRequests); this.retryCount = 0; this.isPaused = false; this.isStopped = false; // 服务错误计数,用于自动降级 this.serviceErrorCount = {}; this.maxErrorCount = 3; // 连续错误3次后降级 } // 暂停翻译 pause() { this.isPaused = true; console.log('翻译已暂停'); } // 继续翻译 resume() { this.isPaused = false; console.log('翻译已继续'); } // 停止翻译 stop() { this.isStopped = true; this.isPaused = false; this.requestQueue.clear(); console.log('翻译已停止'); } // 重置状态 reset() { this.isStopped = false; this.isPaused = false; } // 翻译主方法 async translate(text, options = {}) { // 检查是否停止 if (this.isStopped) { throw new Error('翻译已停止'); } // 等待暂停结束 while (this.isPaused && !this.isStopped) { await new Promise(resolve => setTimeout(resolve, 100)); } const { sourceLang = config.translation.sourceLang, targetLang = config.translation.targetLang, service = this.currentService, retry = 0 } = options; if (!text || text.trim() === '') { throw new Error('翻译文本不能为空'); } // 检查缓存 const cacheKey = this.generateCacheKey(text, sourceLang, targetLang, service); const cachedResult = this.cache.get(cacheKey); if (cachedResult) { return cachedResult; } try { const result = await this.retryWithBackoff(async () => { return this.requestQueue.add(() => { return this.callTranslationAPI(text, sourceLang, targetLang, service); }); }, retry); this.cache.set(cacheKey, result); // 重置错误计数 this.serviceErrorCount[service] = 0; return result; } catch (error) { console.error('翻译失败:', error); // 增加错误计数 this.serviceErrorCount[service] = (this.serviceErrorCount[service] || 0) + 1; // 如果当前服务连续错误达到阈值,尝试降级 if (this.serviceErrorCount[service] >= this.maxErrorCount) { console.log(`服务${service}连续错误${this.maxErrorCount}次,尝试降级`); const nextService = this.getNextAvailableService(service); if (nextService && nextService !== service) { GM_notification({ text: `服务${apiServices[service].name}出现问题,已自动切换到${apiServices[nextService].name}`, title: 'AI Chat翻译助手', timeout: 5000 }); this.currentService = nextService; this.serviceErrorCount[service] = 0; // 重置原服务错误计数 // 同步更新面板和设置页面的服务选择器 if (settingsManager) { settingsManager.syncServiceSelectors(); } // 递归调用新的服务 return this.translate(text, { sourceLang, targetLang, service: nextService }); } else { // 如果没有下一个可用服务,回退到Google并提示用户 if (service !== 'google') { GM_notification({ text: `所有配置的服务均不可用,已回退到Google翻译,请检查API配置`, title: 'AI Chat翻译助手', timeout: 5000 }); this.currentService = 'google'; this.serviceErrorCount[service] = 0; // 重置原服务错误计数 // 同步更新面板和设置页面的服务选择器 if (settingsManager) { settingsManager.syncServiceSelectors(); } // 递归调用Google服务 return this.translate(text, { sourceLang, targetLang, service: 'google' }); } } } // 如果当前服务失败,尝试使用最佳可用服务 const bestService = this.getBestAvailableService(); if (service !== bestService) { console.log(`尝试使用最佳可用服务${bestService}`); return this.translate(text, { sourceLang, targetLang, service: bestService }); } throw error; } } // 智能重试机制 async retryWithBackoff(fn, retryCount = 0) { const maxRetries = config.translation.maxRetries; try { return await fn(); } catch (error) { if (retryCount >= maxRetries || this.isStopped) { throw error; } const delay = Math.pow(2, retryCount) * 500 + Math.random() * 500; console.log(`重试中 (${retryCount + 1}/${maxRetries})...`); await new Promise(resolve => setTimeout(resolve, delay)); return this.retryWithBackoff(fn, retryCount + 1); } } // 生成缓存键 generateCacheKey(text, sourceLang, targetLang, service) { return `${service}_${sourceLang}_${targetLang}_${hash(text)}`; } // 调用翻译API async callTranslationAPI(text, sourceLang, targetLang, service) { const serviceConfig = apiServices[service]; if (!serviceConfig) { throw new Error(`不支持的翻译服务: ${service}`); } if (serviceConfig.requiresKey) { // 特殊处理百度翻译的密钥检查 if (service === 'baidu') { const appid = GM_getValue('baidu_appid'); const secret = GM_getValue('baidu_secret'); if (!appid || !secret) { throw new Error(`翻译服务${service}需要API密钥,请在设置中配置`); } } else { const apiKey = GM_getValue(`api_key_${service}`); if (!apiKey) { throw new Error(`翻译服务${service}需要API密钥,请在设置中配置`); } } } switch (service) { case 'google': return this.callGoogleAPI(text, sourceLang, targetLang); case 'baidu': return this.callBaiduAPI(text, sourceLang, targetLang); case 'youdao': return this.callYoudaoAPI(text, sourceLang, targetLang); case 'siliconflow': return this.callSiliconFlowAPI(text, sourceLang, targetLang); case 'deepseek': return this.callDeepSeekAPI(text, sourceLang, targetLang); case 'custom': return this.callCustomModelAPI(text, sourceLang, targetLang); default: throw new Error(`未实现的翻译服务: ${service}`); } } // Google翻译API async callGoogleAPI(text, sourceLang, targetLang) { return new Promise((resolve, reject) => { const maxLength = 5000; if (text.length > maxLength) { this.translateLongText(text, sourceLang, targetLang, 'google') .then(resolve) .catch(reject); return; } GM_xmlhttpRequest({ method: 'GET', url: `${apiServices.google.endpoint}?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`, timeout: config.translation.timeout, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }, onload: (response) => { try { const data = JSON.parse(response.responseText); if (data && data[0] && Array.isArray(data[0])) { const translatedText = data[0].map(item => item[0]).filter(Boolean).join(''); const detectedLang = data[2] || sourceLang; resolve({ text: translatedText, sourceLang: detectedLang, targetLang }); } else { reject(new Error('Google翻译返回格式异常')); } } catch (error) { reject(new Error('解析Google翻译结果失败: ' + error.message)); } }, onerror: (error) => { reject(new Error('Google翻译请求失败')); }, ontimeout: () => { reject(new Error('Google翻译请求超时')); } }); }); } // 百度翻译API async callBaiduAPI(text, sourceLang, targetLang) { const appid = GM_getValue('baidu_appid'); const secretKey = GM_getValue('baidu_secret'); if (!appid || !secretKey) { console.error('百度API密钥未配置,请检查设置页面是否已保存'); throw new Error('百度翻译API密钥未配置'); } const salt = Date.now(); // 百度API要求:签名 = md5(appid + q + salt + 密钥) // 注意:q 是原始文本,不是URL编码后的 const sign = md5(appid + text + salt + secretKey); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: apiServices.baidu.endpoint, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: `q=${encodeURIComponent(text)}&from=${this.mapLangCode(sourceLang, 'baidu')}&to=${this.mapLangCode(targetLang, 'baidu')}&appid=${appid}&salt=${salt}&sign=${sign}`, timeout: config.translation.timeout, onload: (response) => { try { const data = JSON.parse(response.responseText); console.log('百度翻译API响应:', data); if (data.trans_result && data.trans_result.length > 0) { const translatedText = data.trans_result.map(item => item.dst).join('\n'); resolve({ text: translatedText, sourceLang, targetLang }); } else if (data.error_code) { let errorMsg = `百度翻译错误: ${data.error_code}`; // 添加常见错误代码说明 const errorCodes = { '52001': '请求超时', '52002': '系统错误', '52003': '未授权用户', '54000': '必填参数为空', '54001': '签名错误', '54003': '访问频率受限', '54004': '账户余额不足', '54005': '长query请求频繁', '58000': '客户端IP非法', '58001': '译文语言方向不支持', '58002': '服务当前已关闭' }; if (errorCodes[data.error_code]) { errorMsg += ` (${errorCodes[data.error_code]})`; } reject(new Error(errorMsg)); } else { reject(new Error('百度翻译返回格式异常')); } } catch (error) { reject(new Error('解析百度翻译结果失败')); } }, onerror: (error) => { console.error('百度翻译网络错误:', error); reject(new Error('百度翻译请求失败')); }, ontimeout: () => reject(new Error('百度翻译请求超时')) }); }); } // 有道翻译API async callYoudaoAPI(text, sourceLang, targetLang) { const appid = GM_getValue('youdao_appid'); const secretKey = GM_getValue('youdao_secret'); if (!appid || !secretKey) { throw new Error('有道翻译API密钥未配置'); } const salt = Date.now(); const sign = md5(appid + text + salt + secretKey); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: apiServices.youdao.endpoint, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: `q=${encodeURIComponent(text)}&from=${this.mapLangCode(sourceLang, 'youdao')}&to=${this.mapLangCode(targetLang, 'youdao')}&appKey=${appid}&salt=${salt}&sign=${sign}`, timeout: config.translation.timeout, onload: (response) => { try { const data = JSON.parse(response.responseText); if (data.errorCode === '0' && data.translation) { const translatedText = data.translation.join('\n'); resolve({ text: translatedText, sourceLang, targetLang }); } else { reject(new Error(`有道翻译错误: ${data.errorCode}`)); } } catch (error) { reject(new Error('解析有道翻译结果失败')); } }, onerror: () => reject(new Error('有道翻译请求失败')), ontimeout: () => reject(new Error('有道翻译请求超时')) }); }); } // 硅基流动API async callSiliconFlowAPI(text, sourceLang, targetLang) { const apiKey = GM_getValue('api_key_siliconflow'); if (!apiKey) { throw new Error('硅基流动API密钥未配置'); } return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: apiServices.siliconflow.endpoint, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, data: JSON.stringify({ model: 'Qwen/Qwen2.5-7B-Instruct', messages: [ { role: 'system', content: `你是一个专业翻译助手。请将以下文本翻译成${this.getLangName(targetLang)},只返回翻译结果,不要包含任何解释。` }, { role: 'user', content: text } ], max_tokens: 4096, temperature: 0.1 }), timeout: config.translation.timeout * 2, onload: (response) => { try { const data = JSON.parse(response.responseText); if (data.choices && data.choices.length > 0) { const translatedText = data.choices[0].message.content.trim(); resolve({ text: translatedText, sourceLang, targetLang }); } else { reject(new Error('硅基流动翻译返回格式异常')); } } catch (error) { reject(new Error('解析硅基流动翻译结果失败')); } }, onerror: () => reject(new Error('硅基流动翻译请求失败')), ontimeout: () => reject(new Error('硅基流动翻译请求超时')) }); }); } // DeepSeek API async callDeepSeekAPI(text, sourceLang, targetLang) { const apiKey = GM_getValue('api_key_deepseek'); if (!apiKey) { throw new Error('DeepSeek API密钥未配置'); } return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: apiServices.deepseek.endpoint, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, data: JSON.stringify({ model: 'deepseek-chat', messages: [ { role: 'system', content: `You are a professional translator. Translate the text to ${this.getLangName(targetLang)}. Return only the translation, no explanations.` }, { role: 'user', content: text } ], max_tokens: 4096, temperature: 0.1 }), timeout: config.translation.timeout * 2, onload: (response) => { try { const data = JSON.parse(response.responseText); if (data.choices && data.choices.length > 0) { const translatedText = data.choices[0].message.content.trim(); resolve({ text: translatedText, sourceLang, targetLang }); } else { reject(new Error('DeepSeek翻译返回格式异常')); } } catch (error) { reject(new Error('解析DeepSeek翻译结果失败')); } }, onerror: () => reject(new Error('DeepSeek翻译请求失败')), ontimeout: () => reject(new Error('DeepSeek翻译请求超时')) }); }); } // 自定义AI模型API async callCustomModelAPI(text, sourceLang, targetLang) { const customEndpoint = GM_getValue('custom_model_endpoint'); const customApiKey = GM_getValue('custom_model_apikey'); const customModelName = GM_getValue('custom_model_name') || 'gpt-3.5-turbo'; if (!customEndpoint) { throw new Error('自定义模型API端点未配置'); } if (!customApiKey) { throw new Error('自定义模型API密钥未配置'); } return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: customEndpoint, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${customApiKey}` }, data: JSON.stringify({ model: customModelName, messages: [ { role: 'system', content: `You are a professional translator. Translate the text to ${this.getLangName(targetLang)}. Return only the translation, no explanations.` }, { role: 'user', content: text } ], max_tokens: 4096, temperature: 0.1 }), timeout: config.translation.timeout * 2, onload: (response) => { try { const data = JSON.parse(response.responseText); if (data.choices && data.choices.length > 0) { const translatedText = data.choices[0].message.content.trim(); resolve({ text: translatedText, sourceLang, targetLang }); } else if (data.result) { // 兼容某些API返回格式 const translatedText = data.result; resolve({ text: translatedText, sourceLang, targetLang }); } else { reject(new Error('自定义模型翻译返回格式异常')); } } catch (error) { reject(new Error('解析自定义模型翻译结果失败: ' + error.message)); } }, onerror: (error) => reject(new Error('自定义模型翻译请求失败: ' + error.statusText)), ontimeout: () => reject(new Error('自定义模型翻译请求超时')) }); }); } // 语言代码映射 mapLangCode(langCode, service) { const langMaps = { baidu: { 'zh-CN': 'zh', 'zh-TW': 'cht', 'en': 'en', 'ja': 'jp', 'ko': 'kor', 'fr': 'fra', 'de': 'de', 'es': 'spa', 'ru': 'ru', 'pt': 'pt', 'it': 'it', 'ar': 'ara', 'hi': 'hi', 'auto': 'auto' }, youdao: { 'zh-CN': 'zh-CHS', 'zh-TW': 'zh-CHT', 'en': 'en', 'ja': 'ja', 'ko': 'ko', 'fr': 'fr', 'de': 'de', 'es': 'es', 'ru': 'ru', 'pt': 'pt', 'it': 'it', 'ar': 'ar', 'hi': 'hi', 'auto': 'auto' } }; if (langMaps[service] && langMaps[service][langCode]) { return langMaps[service][langCode]; } return langCode; } // 获取语言名称 getLangName(langCode) { const langNames = { 'zh-CN': '中文', 'zh-TW': '繁体中文', 'en': 'English', 'ja': '日语', 'ko': '韩语', 'fr': '法语', 'de': '德语', 'es': '西班牙语', 'ru': '俄语', 'pt': '葡萄牙语', 'it': '意大利语', 'ar': '阿拉伯语', 'hi': '印地语', 'auto': '自动检测' }; return langNames[langCode] || langCode; } // 检查可用的翻译服务并按优先级排序 getAvailableServices() { const services = []; // 按优先级顺序检查API密钥 const servicePriority = [ { name: 'deepseek', key: 'api_key_deepseek' }, { name: 'siliconflow', key: 'api_key_siliconflow' }, { name: 'youdao', key: 'youdao_appid' }, { name: 'baidu', key: 'baidu_appid' }, { name: 'google', key: null } // Google不需要API密钥 ]; for (const service of servicePriority) { if (service.key === null || GM_getValue(service.key)) { services.push(service.name); } } // 检查自定义AI模型是否配置 const customName = GM_getValue('custom_model_name'); const customEndpoint = GM_getValue('custom_model_endpoint'); const customApiKey = GM_getValue('custom_model_apikey'); if (customName && customEndpoint && customApiKey) { services.push('custom'); } return services; } // 获取最高优先级的可用服务 getBestAvailableService() { const availableServices = this.getAvailableServices(); return availableServices.length > 0 ? availableServices[0] : 'google'; } // 从指定服务列表获取最高优先级的可用服务 getBestAvailableServiceFromList(servicesList) { // 按优先级顺序检查 const servicePriority = ['deepseek', 'siliconflow', 'youdao', 'baidu', 'google']; for (const service of servicePriority) { if (servicesList.includes(service)) { return service; } } // 检查自定义AI模型 if (servicesList.includes('custom')) { return 'custom'; } return 'google'; // 默认返回Google } // 获取下一个可用服务(用于降级) getNextAvailableService(currentService) { const availableServices = this.getAvailableServices(); const currentIndex = availableServices.indexOf(currentService); // 如果当前服务不在可用服务列表中,返回最高优先级服务 if (currentIndex === -1) { return availableServices.length > 0 ? availableServices[0] : 'google'; } // 返回下一个可用服务,如果没有则返回Google return availableServices[currentIndex + 1] || 'google'; } // 检查服务是否已配置 isServiceConfigured(service) { switch (service) { case 'google': return true; // Google不需要配置 case 'baidu': return GM_getValue('baidu_appid') && GM_getValue('baidu_secret'); case 'youdao': return GM_getValue('youdao_appid') && GM_getValue('youdao_secret'); case 'siliconflow': return !!GM_getValue('api_key_siliconflow'); case 'deepseek': return !!GM_getValue('api_key_deepseek'); case 'custom': const customName = GM_getValue('custom_model_name'); const customEndpoint = GM_getValue('custom_model_endpoint'); const customApiKey = GM_getValue('custom_model_apikey'); return customName && customEndpoint && customApiKey; default: return false; } } // 翻译长文本 async translateLongText(text, sourceLang, targetLang, service) { const maxLength = 4000; const segments = []; for (let i = 0; i < text.length; i += maxLength) { segments.push(text.substring(i, i + maxLength)); } const results = await Promise.all( segments.map(segment => this.translate(segment, { sourceLang, targetLang, service })) ); const translatedText = results.map(result => result.text).join(''); return { text: translatedText, sourceLang, targetLang }; } } // 翻译缓存类 class TranslationCache { constructor() { this.memoryCache = new Map(); this.maxMemorySize = 1000; } get(key) { if (this.memoryCache.has(key)) { return this.memoryCache.get(key); } const localStorageKey = `translation_cache_${key}`; const cached = localStorage.getItem(localStorageKey); if (cached) { try { const result = JSON.parse(cached); if (Date.now() - result.timestamp < 7 * 24 * 60 * 60 * 1000) { this.set(key, result.data); return result.data; } else { localStorage.removeItem(localStorageKey); } } catch (error) { console.error('解析缓存失败:', error); } } return null; } set(key, data) { if (this.memoryCache.size >= this.maxMemorySize) { const firstKey = this.memoryCache.keys().next().value; this.memoryCache.delete(firstKey); } this.memoryCache.set(key, data); const localStorageKey = `translation_cache_${key}`; localStorage.setItem(localStorageKey, JSON.stringify({ timestamp: Date.now(), data: data })); } clear() { this.memoryCache.clear(); for (let key in localStorage) { if (key.startsWith('translation_cache_')) { localStorage.removeItem(key); } } } } // 请求队列类 class RequestQueue { constructor(maxConcurrent) { this.maxConcurrent = maxConcurrent; this.queue = []; this.running = 0; } add(task) { return new Promise((resolve, reject) => { this.queue.push({ task, resolve, reject }); this.processQueue(); }); } processQueue() { if (this.running >= this.maxConcurrent || this.queue.length === 0) { return; } const { task, resolve, reject } = this.queue.shift(); this.running++; Promise.resolve() .then(task) .then(resolve) .catch(reject) .finally(() => { this.running--; this.processQueue(); }); } clear() { this.queue = []; } } // 全页翻译器类 class FullPageTranslator { constructor() { this.translatedNodes = new WeakSet(); this.observer = null; this.isTranslating = false; } // 获取所有需要翻译的文本节点 getTextNodes(root = document.body) { const textNodes = []; const walker = document.createTreeWalker( root, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { // 过滤掉不需要翻译的元素 const parent = node.parentElement; if (!parent) return NodeFilter.FILTER_REJECT; const tagName = parent.tagName.toLowerCase(); // 排除script, style, code等标签 if (['script', 'style', 'code', 'pre', 'textarea', 'input', 'select'].includes(tagName)) { return NodeFilter.FILTER_REJECT; } // 排除翻译面板和悬浮球 if (parent.closest('#translation-panel, #translate-float-button, #settings-panel')) { return NodeFilter.FILTER_REJECT; } // 只保留有文字内容的节点 const text = node.textContent.trim(); if (text.length > 0) { return NodeFilter.FILTER_ACCEPT; } return NodeFilter.FILTER_REJECT; } } ); let node; while (node = walker.nextNode()) { textNodes.push(node); } return textNodes; } // 分批处理文本节点 batchNodes(nodes, batchSize = 50) { const batches = []; for (let i = 0; i < nodes.length; i += batchSize) { batches.push(nodes.slice(i, i + batchSize)); } return batches; } // 翻译单个文本节点 async translateNode(node, targetLang, service) { if (this.translatedNodes.has(node)) { return; } const originalText = node.textContent.trim(); if (!originalText || originalText.length === 0) { return; } try { const result = await translationEngine.translate(originalText, 'auto', targetLang, service); const translatedText = typeof result === 'string' ? result : result.text; if (translatedText && translatedText !== originalText) { // 保存原文到自定义属性 if (!node.parentElement.hasAttribute('data-original-text')) { node.parentElement.setAttribute('data-original-text', originalText); } node.textContent = translatedText; this.translatedNodes.add(node); } } catch (error) { console.error('翻译节点失败:', error); } } // 主翻译方法 async translate(targetLang, service, progressCallback) { if (this.isTranslating) { throw new Error('翻译正在进行中'); } this.isTranslating = true; try { // 1. 获取所有文本节点 progressCallback(5); const textNodes = this.getTextNodes(); console.log(`找到 ${textNodes.length} 个文本节点`); if (textNodes.length === 0) { throw new Error('未找到可翻译的文本'); } // 2. 分批处理 progressCallback(10); const batches = this.batchNodes(textNodes, 30); console.log(`分为 ${batches.length} 个批次`); // 3. 逐批翻译 for (let i = 0; i < batches.length; i++) { const batch = batches[i]; const progress = 10 + Math.floor((i / batches.length) * 85); progressCallback(progress); // 并行翻译批次中的所有节点 await Promise.all( batch.map(node => this.translateNode(node, targetLang, service)) ); // 每批次之间稍微延迟,避免过载 await new Promise(resolve => setTimeout(resolve, 100)); } progressCallback(100); console.log('全页翻译完成'); } finally { this.isTranslating = false; } } // 还原原文 restore() { const elements = document.querySelectorAll('[data-original-text]'); elements.forEach(element => { const originalText = element.getAttribute('data-original-text'); if (originalText && element.firstChild && element.firstChild.nodeType === Node.TEXT_NODE) { element.firstChild.textContent = originalText; element.removeAttribute('data-original-text'); } }); this.translatedNodes = new WeakSet(); console.log('已还原原文'); } } // 设置管理类 class SettingsManager { constructor() { this.settingsPanel = null; } // 打开设置面板 open() { if (this.settingsPanel && $('#settings-panel').length) { $('#settings-panel').show(); return; } this.createSettingsPanel(); $('#settings-panel').show(); } // 创建设置面板 createSettingsPanel() { const settingsHTML = `

AI Chat翻译助手 - 设置

🔑 API密钥配置

免费服务,无需密钥
配置自定义AI翻译服务,支持OpenAI兼容的API接口

⌨️ 快捷键设置

用于触发段落翻译
用于显示/隐藏翻译面板

🌐 翻译设置

当页面语言与系统语言不同时自动显示
缓存翻译结果以提高速度
选中文本时自动翻译
鼠标悬停时自动翻译
在这些域名下将禁用所有翻译功能
`; $('body').append(settingsHTML); this.loadSettingsStyles(); this.bindSettingsEvents(); this.loadSettings(); } // 加载设置样式 loadSettingsStyles() { const styles = ` .settings-panel { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 99999999; display: none; backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px); } .settings-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.6); } .settings-content { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; border-radius: 16px; box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3); width: 90%; max-width: 850px; max-height: 90vh; display: flex; flex-direction: column; overflow: hidden; } .settings-header { padding: 24px 28px; border-bottom: 1px solid #edf2f7; display: flex; justify-content: space-between; align-items: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; } .settings-header h2 { margin: 0; font-size: 22px; font-weight: 600; color: white; letter-spacing: 0.5px; } .close-settings { width: 36px; height: 36px; border: none; background: rgba(255, 255, 255, 0.2); font-size: 24px; cursor: pointer; border-radius: 8px; color: white; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; } .close-settings:hover { background: rgba(255, 255, 255, 0.3); transform: scale(1.05); } .settings-body { padding: 28px; overflow-y: auto; flex: 1; background: #f8fafc; } .settings-section { margin-bottom: 36px; background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); border: 1px solid #edf2f7; } .settings-section:last-child { margin-bottom: 0; } .settings-section h3 { margin: 0 0 20px 0; font-size: 18px; font-weight: 600; color: #2d3748; display: flex; align-items: center; gap: 10px; } .settings-section h3::before { content: ''; display: block; width: 4px; height: 20px; background: linear-gradient(to bottom, #667eea, #764ba2); border-radius: 2px; } .api-config-grid, .shortcut-config, .translation-config { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 20px; } .config-item { display: flex; flex-direction: column; gap: 8px; } .config-item label { font-size: 15px; font-weight: 500; color: #4a5568; } .config-item input[type="text"], .config-item input[type="password"], .config-item select, .config-item textarea { padding: 12px 14px; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 14px; transition: all 0.2s ease; background: white; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); } .config-item input:focus, .config-item select:focus, .config-item textarea:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); } .config-item input:disabled { background: #f1f5f9; color: #a0aec0; } .config-item small { font-size: 13px; color: #718096; line-height: 1.4; } .config-item input[type="checkbox"] { width: 18px; height: 18px; margin-right: 8px; accent-color: #667eea; border-radius: 4px; } .settings-footer { padding: 20px 28px; border-top: 1px solid #edf2f7; display: flex; gap: 16px; justify-content: flex-end; background: white; } .settings-footer button { padding: 12px 24px; border: none; border-radius: 8px; font-size: 15px; font-weight: 500; cursor: pointer; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); display: flex; align-items: center; gap: 8px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif, 'Microsoft YaHei', sans-serif, emoji, 'Apple Color Emoji', 'Segoe UI Emoji'; } .settings-footer button:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .settings-footer button:active { transform: translateY(0); } .btn-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; font-weight: 600; } .btn-primary:hover { background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%); box-shadow: 0 6px 16px rgba(102, 126, 234, 0.3); } .btn-secondary { background: white; color: #4a5568; border: 1px solid #e2e8f0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif, 'Microsoft YaHei', sans-serif, emoji, 'Apple Color Emoji', 'Segoe UI Emoji'; } .btn-secondary:hover { background: #f8fafc; border-color: #cbd5e0; color: #2d3748; } .btn-error { background: linear-gradient(135deg, #ff6b6b 0%, #ff5252 100%); color: white; } .btn-error:hover { background: linear-gradient(135deg, #ff5252 0%, #ff4444 100%); box-shadow: 0 6px 16px rgba(255, 107, 107, 0.3); } @media (max-width: 768px) { .api-config-grid, .shortcut-config, .translation-config { grid-template-columns: 1fr; } .settings-content { width: 95%; max-width: none; } .language-selector-row { flex-direction: column; align-items: stretch; gap: 8px; } .language-label { min-width: auto; text-align: left; } .lang-select, .service-select { max-width: 100%; } .selector-row { flex-direction: column; align-items: stretch; gap: 12px; } .selector-row .swap-btn { align-self: center; } } @media (max-width: 480px) { .translation-panel { width: 95vw; right: 2.5vw; left: 2.5vw; } .language-selector-row { flex-direction: column; align-items: stretch; gap: 8px; } .language-label { min-width: auto; text-align: left; } .lang-select, .service-select { max-width: 100%; } .selector-row { flex-direction: column; align-items: stretch; gap: 12px; } .selector-row .swap-btn { align-self: center; } } `; GM_addStyle(styles); } // 绑定设置事件 bindSettingsEvents() { $('.close-settings, .settings-overlay').on('click', () => this.close()); $('#save-settings').on('click', () => this.saveSettings()); $('#reset-settings').on('click', () => this.resetSettings()); $('#clear-cache').on('click', () => this.clearCache()); // 绑定API连接测试按钮事件 $('.test-connection-btn').on('click', (e) => { const service = $(e.target).data('service'); this.testServiceConnection(service); }); } // 加载设置 loadSettings() { // API密钥 $('#api-baidu-appid').val(GM_getValue('baidu_appid', '')); $('#api-baidu-secret').val(GM_getValue('baidu_secret', '')); $('#api-youdao-appid').val(GM_getValue('youdao_appid', '')); $('#api-youdao-secret').val(GM_getValue('youdao_secret', '')); $('#api-siliconflow').val(GM_getValue('api_key_siliconflow', '')); $('#api-deepseek').val(GM_getValue('api_key_deepseek', '')); // 自定义AI模型配置 $('#custom-model-name').val(GM_getValue('custom_model_name', '')); $('#custom-model-endpoint').val(GM_getValue('custom_model_endpoint', '')); $('#custom-model-apikey').val(GM_getValue('custom_model_apikey', '')); // 快捷键 $('#shortcut-translate').val(GM_getValue('shortcut_translate', 'Ctrl+Shift+T')); $('#shortcut-toggle').val(GM_getValue('shortcut_toggle', 'Alt+T')); // 翻译设置 $('#default-service').val(GM_getValue('default_service', 'google')); $('#default-target-lang').val(GM_getValue('default_target_lang', 'zh-CN')); $('#auto-show-button').prop('checked', GM_getValue('auto_show_button', true)); $('#enable-cache').prop('checked', GM_getValue('enable_cache', true)); const wordTranslateValue = GM_getValue('word_translate', false); const hoverTranslateValue = GM_getValue('hover_translate', false); $('#word-translate').prop('checked', wordTranslateValue).attr('data-temp-state', wordTranslateValue ? 'enabled' : 'disabled'); $('#hover-translate').prop('checked', hoverTranslateValue).attr('data-temp-state', hoverTranslateValue ? 'enabled' : 'disabled'); // 域名屏蔽列表 const blockedDomains = GM_getValue('blocked_domains', ''); $('#blocked-domains').val(blockedDomains); } // 保存设置 async saveSettings() { // 收集API密钥配置 const apiConfigs = { baidu: { appId: $('#api-baidu-appid').val(), secret: $('#api-baidu-secret').val() }, youdao: { appId: $('#api-youdao-appid').val(), secret: $('#api-youdao-secret').val() }, siliconflow: { key: $('#api-siliconflow').val() }, deepseek: { key: $('#api-deepseek').val() } }; // 保存API密钥 GM_setValue('baidu_appid', apiConfigs.baidu.appId); GM_setValue('baidu_secret', apiConfigs.baidu.secret); GM_setValue('youdao_appid', apiConfigs.youdao.appId); GM_setValue('youdao_secret', apiConfigs.youdao.secret); GM_setValue('api_key_siliconflow', apiConfigs.siliconflow.key); GM_setValue('api_key_deepseek', apiConfigs.deepseek.key); // 保存自定义AI模型配置 GM_setValue('custom_model_name', $('#custom-model-name').val()); GM_setValue('custom_model_endpoint', $('#custom-model-endpoint').val()); GM_setValue('custom_model_apikey', $('#custom-model-apikey').val()); // 检查配置的API密钥是否可用 const availableServices = ['google']; // Google总是可用 // 检查其他服务 for (const [service, config] of Object.entries(apiConfigs)) { if (this.isServiceConfigured(service, config)) { try { const testResult = await this.performTestTranslation(service, 'hello', 'en', 'zh-CN', config.key || config.appId, config.appId, config.secret); if (testResult.success) { availableServices.push(service); } } catch (error) { console.warn(`服务${service}连接测试失败:`, error); // 显示测试失败的通知 GM_notification({ text: `服务${apiServices[service].name}连接测试失败,请检查配置`, title: 'AI Chat翻译助手', timeout: 3000 }); } } } // 检查自定义AI模型配置 const customModelName = $('#custom-model-name').val(); const customModelEndpoint = $('#custom-model-endpoint').val(); const customModelApiKey = $('#custom-model-apikey').val(); if (customModelName && customModelEndpoint && customModelApiKey) { try { const testResult = await this.performTestTranslation('custom', 'hello', 'en', 'zh-CN', customModelApiKey, null, null); if (testResult.success) { availableServices.push('custom'); } } catch (error) { console.warn('自定义AI模型连接测试失败:', error); // 显示测试失败的通知 GM_notification({ text: '自定义AI模型连接测试失败,请检查配置', title: 'AI Chat翻译助手', timeout: 3000 }); } } // 如果没有可用服务,提示用户 if (availableServices.length <= 1) { // 只有Google可用 const confirmResult = confirm('没有配置有效的API密钥,将只能使用Google翻译服务。是否继续保存设置?'); if (!confirmResult) { return; // 用户取消保存 } } // 如果翻译引擎存在,更新当前服务为最佳可用服务 if (translationEngine) { translationEngine.currentService = translationEngine.getBestAvailableServiceFromList(availableServices); console.log('更新当前服务为:', translationEngine.currentService); } // 快捷键 GM_setValue('shortcut_translate', $('#shortcut-translate').val()); GM_setValue('shortcut_toggle', $('#shortcut-toggle').val()); // 翻译设置 GM_setValue('default_service', $('#default-service').val()); GM_setValue('default_target_lang', $('#default-target-lang').val()); GM_setValue('auto_show_button', $('#auto-show-button').prop('checked')); GM_setValue('enable_cache', $('#enable-cache').prop('checked')); GM_setValue('word_translate', $('#word-translate').prop('checked')); GM_setValue('hover_translate', $('#hover-translate').prop('checked')); // 域名屏蔽列表 GM_setValue('blocked_domains', $('#blocked-domains').val()); // 更新配置 config.translation.defaultService = $('#default-service').val(); config.translation.targetLang = $('#default-target-lang').val(); config.features.autoShowButton = $('#auto-show-button').prop('checked'); config.features.cacheEnabled = $('#enable-cache').prop('checked'); // 获取临时状态并应用到全局配置 const wordTranslateTempState = $('#word-translate').attr('data-temp-state'); const hoverTranslateTempState = $('#hover-translate').attr('data-temp-state'); if (wordTranslateTempState) { const isEnabled = wordTranslateTempState === 'enabled'; config.features.wordTranslate = isEnabled; GM_setValue('word_translate', isEnabled); } if (hoverTranslateTempState) { const isEnabled = hoverTranslateTempState === 'enabled'; config.features.hoverTranslate = isEnabled; GM_setValue('hover_translate', isEnabled); } config.shortcut.translate = $('#shortcut-translate').val(); config.shortcut.togglePanel = $('#shortcut-toggle').val(); // 同步更新面板上的按钮状态 if (translationPanel) { const wordTranslateBtn = $('#translate-btn'); const hoverTranslateBtn = $('#hover-translate-btn'); if (config.features.wordTranslate) { wordTranslateBtn.addClass('active').attr('data-temp-state', 'enabled'); } else { wordTranslateBtn.removeClass('active').attr('data-temp-state', 'disabled'); } if (config.features.hoverTranslate) { hoverTranslateBtn.addClass('active').attr('data-temp-state', 'enabled'); } else { hoverTranslateBtn.removeClass('active').attr('data-temp-state', 'disabled'); } } // 同步更新服务选择器 this.syncServiceSelectors(); GM_notification({ text: '设置已保存', title: 'AI Chat翻译助手', timeout: 2000 }); this.close(); } // 重置设置 resetSettings() { if (confirm('确定要重置所有设置为默认值吗?')) { // 清除所有设置 GM_deleteValue('baidu_appid'); GM_deleteValue('baidu_secret'); GM_deleteValue('youdao_appid'); GM_deleteValue('youdao_secret'); GM_deleteValue('api_key_siliconflow'); GM_deleteValue('api_key_deepseek'); GM_deleteValue('shortcut_translate'); GM_deleteValue('shortcut_toggle'); GM_deleteValue('default_service'); GM_deleteValue('default_target_lang'); GM_deleteValue('auto_show_button'); GM_deleteValue('enable_cache'); GM_deleteValue('word_translate'); GM_deleteValue('hover_translate'); GM_deleteValue('blocked_domains'); this.loadSettings(); // 同步更新面板上的按钮临时状态 if (translationPanel) { $('#translate-btn').removeClass('active').attr('data-temp-state', 'disabled'); $('#hover-translate-btn').removeClass('active').attr('data-temp-state', 'disabled'); } GM_notification({ text: '设置已重置为默认值', title: 'AI Chat翻译助手', timeout: 2000 }); } } // 清除缓存 clearCache() { if (confirm('确定要清除所有翻译缓存吗?')) { translationEngine.cache.clear(); GM_notification({ text: '缓存已清除', title: 'AI Chat翻译助手', timeout: 2000 }); } } // 检查服务是否已配置 isServiceConfigured(service, config) { switch (service) { case 'baidu': return config.appId && config.secret; case 'youdao': return config.appId && config.secret; case 'siliconflow': case 'deepseek': return config.key; default: return false; } } // 同步服务选择器 syncServiceSelectors() { if (translationEngine) { // 更新设置页面和面板中的服务选择器 $('#default-service').val(translationEngine.currentService); $('#translation-service').val(translationEngine.currentService); console.log('同步服务选择器到:', translationEngine.currentService); } } // 测试服务连接 async testServiceConnection(service) { const testBtn = $(`.test-connection-btn[data-service='${service}']`); const originalText = testBtn.text(); // 更新按钮状态 testBtn.text('⏳ 测试中...').prop('disabled', true); try { // 获取对应服务的API密钥 let apiKey = null; let appId = null; let secretKey = null; switch (service) { case 'baidu': appId = $('#api-baidu-appid').val(); secretKey = $('#api-baidu-secret').val(); if (!appId || !secretKey) { throw new Error('请填写完整的百度翻译API密钥'); } break; case 'youdao': appId = $('#api-youdao-appid').val(); secretKey = $('#api-youdao-secret').val(); if (!appId || !secretKey) { throw new Error('请填写完整的有道翻译API密钥'); } break; case 'siliconflow': apiKey = $('#api-siliconflow').val(); if (!apiKey) { throw new Error('请填写硅基流动API密钥'); } break; case 'deepseek': apiKey = $('#api-deepseek').val(); if (!apiKey) { throw new Error('请填写DeepSeek API密钥'); } break; case 'custom': apiKey = $('#custom-model-apikey').val(); if (!apiKey) { throw new Error('请填写自定义AI模型API密钥'); } break; case 'google': // Google翻译不需要密钥,直接测试 break; } // 执行测试翻译 const testText = 'hello'; const result = await this.performTestTranslation(service, testText, 'en', 'zh-CN', apiKey, appId, secretKey); if (result.success) { testBtn.text('✅ 连接成功').removeClass('btn-secondary').addClass('btn-primary'); setTimeout(() => { testBtn.text(originalText).removeClass('btn-primary').addClass('btn-secondary').prop('disabled', false); }, 2000); GM_notification({ text: `${apiServices[service].name}连接测试成功`, title: 'AI Chat翻译助手', timeout: 3000 }); } else { throw new Error(result.error || '连接测试失败'); } } catch (error) { testBtn.text('❌ 连接失败').removeClass('btn-secondary').addClass('btn-error'); setTimeout(() => { testBtn.text(originalText).removeClass('btn-error').addClass('btn-secondary').prop('disabled', false); }, 2000); GM_notification({ text: `${apiServices[service].name}连接测试失败: ${error.message}`, title: 'AI Chat翻译助手', timeout: 5000 }); console.error(`服务${service}连接测试失败:`, error); } } // 执行测试翻译 async performTestTranslation(service, text, sourceLang, targetLang, apiKey, appId, secretKey) { try { // 为不同服务创建测试请求 switch (service) { case 'google': return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `${apiServices.google.endpoint}?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`, timeout: 5000, onload: (response) => { try { const data = JSON.parse(response.responseText); if (data && data[0] && Array.isArray(data[0])) { resolve({ success: true, result: data[0][0][0] }); } else { reject(new Error('Google翻译返回格式异常')); } } catch (parseError) { reject(new Error('解析Google翻译结果失败')); } }, onerror: () => reject(new Error('网络请求失败')), ontimeout: () => reject(new Error('请求超时')) }); }); case 'baidu': // 使用与正式翻译相同的密钥读取方式 const baiduAppId = GM_getValue('baidu_appid'); const baiduSecretKey = GM_getValue('baidu_secret'); if (!baiduAppId || !baiduSecretKey) { throw new Error('百度翻译API密钥未配置'); } const baiduSalt = Date.now(); const baiduSign = md5(baiduAppId + text + baiduSalt + baiduSecretKey); // 使用正确的语言映射 const baiduFromLang = mapLangCode(sourceLang, 'baidu'); const baiduToLang = mapLangCode(targetLang, 'baidu'); console.log('百度翻译测试参数:', { appId: baiduAppId, salt: baiduSalt, from: baiduFromLang, to: baiduToLang }); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: apiServices.baidu.endpoint, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: `q=${encodeURIComponent(text)}&from=${baiduFromLang}&to=${baiduToLang}&appid=${baiduAppId}&salt=${baiduSalt}&sign=${baiduSign}`, timeout: 5000, onload: (response) => { try { const data = JSON.parse(response.responseText); console.log('百度翻译连接测试响应:', data); if (data.trans_result && data.trans_result.length > 0) { resolve({ success: true, result: data.trans_result[0].dst }); } else if (data.error_code) { reject(new Error(`百度翻译错误: ${data.error_code}`)); } else { reject(new Error('百度翻译返回格式异常')); } } catch (parseError) { reject(new Error('解析百度翻译结果失败')); } }, onerror: () => reject(new Error('网络请求失败')), ontimeout: () => reject(new Error('请求超时')) }); }); case 'youdao': const youdaoSalt = Date.now(); const youdaoSign = md5(appId + text + youdaoSalt + secretKey); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: apiServices.youdao.endpoint, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: `q=${encodeURIComponent(text)}&from=${sourceLang}&to=${targetLang}&appKey=${appId}&salt=${youdaoSalt}&sign=${youdaoSign}`, timeout: 5000, onload: (response) => { try { const data = JSON.parse(response.responseText); if (data.errorCode === '0' && data.translation) { resolve({ success: true, result: data.translation[0] }); } else { reject(new Error(`有道翻译错误: ${data.errorCode}`)); } } catch (parseError) { reject(new Error('解析有道翻译结果失败')); } }, onerror: () => reject(new Error('网络请求失败')), ontimeout: () => reject(new Error('请求超时')) }); }); case 'siliconflow': return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: apiServices.siliconflow.endpoint, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, data: JSON.stringify({ model: 'Qwen/Qwen2.5-7B-Instruct', messages: [ { role: 'system', content: '你是一个专业翻译助手。请将以下文本翻译成中文,只返回翻译结果,不要包含任何解释。' }, { role: 'user', content: text } ], max_tokens: 100, temperature: 0.1 }), timeout: 10000, onload: (response) => { try { const data = JSON.parse(response.responseText); if (data.choices && data.choices.length > 0) { resolve({ success: true, result: data.choices[0].message.content.trim() }); } else { reject(new Error('硅基流动翻译返回格式异常')); } } catch (parseError) { reject(new Error('解析硅基流动翻译结果失败')); } }, onerror: () => reject(new Error('网络请求失败')), ontimeout: () => reject(new Error('请求超时')) }); }); case 'deepseek': return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: apiServices.deepseek.endpoint, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, data: JSON.stringify({ model: 'deepseek-chat', messages: [ { role: 'system', content: 'You are a professional translator. Translate the text to Chinese. Return only the translation, no explanations.' }, { role: 'user', content: text } ], max_tokens: 100, temperature: 0.1 }), timeout: 10000, onload: (response) => { try { const data = JSON.parse(response.responseText); if (data.choices && data.choices.length > 0) { resolve({ success: true, result: data.choices[0].message.content.trim() }); } else { reject(new Error('DeepSeek翻译返回格式异常')); } } catch (parseError) { reject(new Error('解析DeepSeek翻译结果失败')); } }, onerror: () => reject(new Error('网络请求失败')), ontimeout: () => reject(new Error('请求超时')) }); }); case 'custom': const customEndpoint = $('#custom-model-endpoint').val(); const customModelName = $('#custom-model-name').val() || 'gpt-3.5-turbo'; if (!customEndpoint) { throw new Error('请填写自定义AI模型API端点'); } return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: customEndpoint, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, data: JSON.stringify({ model: customModelName, messages: [ { role: 'system', content: 'You are a professional translator. Translate the text to Chinese. Return only the translation, no explanations.' }, { role: 'user', content: text } ], max_tokens: 100, temperature: 0.1 }), timeout: 10000, onload: (response) => { try { const data = JSON.parse(response.responseText); if (data.choices && data.choices.length > 0) { resolve({ success: true, result: data.choices[0].message.content.trim() }); } else if (data.result) { // 兼容某些API返回格式 resolve({ success: true, result: data.result }); } else { reject(new Error('自定义AI模型翻译返回格式异常')); } } catch (parseError) { reject(new Error('解析自定义AI模型翻译结果失败: ' + parseError.message)); } }, onerror: (error) => reject(new Error('网络请求失败: ' + error.statusText)), ontimeout: () => reject(new Error('请求超时')) }); }); default: throw new Error(`不支持的翻译服务: ${service}`); } } catch (error) { return { success: false, error: error.message }; } } // 关闭设置面板 close() { $('#settings-panel').hide(); } } // 全局变量 let translationEngine; let languageDetector; let settingsManager; let translationPanel; // UI控制面板类 class TranslationPanel { constructor() { this.isVisible = false; this.isMinimized = false; this.isLocked = false; this.position = config.ui.panelPosition; this.progress = 0; this.wordCount = { source: 0, target: 0 }; this.isPaused = false; this.isTranslating = false; this.init(); } init() { // 初始化语言检测器 languageDetector = new LanguageDetector(); // 检查是否需要显示悬浮球 if (languageDetector.shouldShowTranslateButton()) { this.createPanel(); this.bindEvents(); this.loadPosition(); this.initFloatButtonDrag(); // 检查并应用之前保存的隐藏状态 const isHidden = GM_getValue('floatButtonHidden', false); if (isHidden) { setTimeout(() => { const floatButton = $('#translate-float-button'); const toggleButton = $('#toggle-float-button'); floatButton.hide(); toggleButton.attr('title', '显示翻译按钮'); toggleButton.html('👁️‍🗨️'); }, 100); } } else { console.log('页面语言与系统语言相同,不显示翻译悬浮球'); } } createPanel() { const panelHTML = `
🌐
👁️
`; console.log('开始创建面板和悬浮球'); $('body').append(panelHTML); console.log('面板和悬浮球已添加到DOM'); this.loadStyles(); // 确保悬浮球存在 setTimeout(() => { const floatButton = $('#translate-float-button'); if (floatButton.length > 0) { console.log('悬浮球创建成功'); } else { console.error('悬浮球创建失败'); } }, 100); } loadStyles() { const styles = ` .translation-panel { position: fixed; top: 100px; right: 20px; width: 400px; background: white; border-radius: 18px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); z-index: 9999998; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif, 'Microsoft YaHei', sans-serif; -webkit-transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); } .panel-header { display: -webkit-flex; display: flex; -webkit-justify-content: space-between; justify-content: space-between; -webkit-align-items: center; align-items: center; padding: 20px 24px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 18px 18px 0 0; cursor: move; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); } .panel-title { font-weight: 600; font-size: 18px; letter-spacing: 0.5px; } .panel-controls { display: -webkit-flex; display: flex; gap: 10px; } .control-btn { width: 36px; height: 36px; border: none; background: rgba(255, 255, 255, 0.2); color: white; border-radius: 10px; cursor: pointer; font-size: 18px; display: -webkit-flex; display: flex; -webkit-align-items: center; align-items: center; -webkit-justify-content: center; justify-content: center; -webkit-transition: all 0.2s ease; transition: all 0.2s ease; } .control-btn:hover { background: rgba(255, 255, 255, 0.3); -webkit-transform: translateY(-2px); transform: translateY(-2px); } .control-btn:active { -webkit-transform: translateY(0); transform: translateY(0); } .panel-content { padding: 24px; } .progress-section { margin-bottom: 24px; } .progress-bar { width: 100%; height: 10px; background: #edf2f7; border-radius: 5px; overflow: hidden; margin-bottom: 12px; box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05); } .progress-fill { height: 100%; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); border-radius: 5px; -webkit-transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1); transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 2px 4px rgba(102, 126, 234, 0.2); } .progress-text { font-size: 14px; color: #718096; text-align: center; font-weight: 500; } .word-count-section { display: -webkit-flex; display: flex; -webkit-justify-content: space-around; justify-content: space-around; margin-bottom: 24px; padding: 18px; background: #f8fafc; border-radius: 12px; font-size: 15px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); border: 1px solid #edf2f7; } .word-count-item { display: -webkit-flex; display: flex; -webkit-align-items: center; align-items: center; gap: 8px; } .word-count-item .label { color: #718096; font-weight: 500; } .word-count-item .count { font-weight: 600; color: #2d3748; } .language-selector-section { display: -webkit-flex; display: flex; -webkit-flex-direction: column; flex-direction: column; gap: 12px; margin-bottom: 20px; } .language-selector-row { display: -webkit-flex; display: flex; -webkit-align-items: center; align-items: center; gap: 12px; width: 100%; } .language-label { font-size: 14px; color: #4a5568; font-weight: 500; min-width: 60px; text-align: left; } .lang-select, .service-select { flex: 1; padding: 14px 16px; border: 1px solid #e2e8f0; border-radius: 12px; background: white; font-size: 15px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); max-width: calc(100% - 80px); } .lang-select:focus, .service-select:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1); } .lang-select::-webkit-input-placeholder, .service-select::-webkit-input-placeholder { color: #a0aec0; } .swap-btn { width: 44px; height: 44px; border: 1px solid #e2e8f0; border-radius: 12px; background: white; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 20px; transition: all 0.3s ease; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); } .swap-btn:hover { background: #f1f5f9; -webkit-transform: rotate(180deg); transform: rotate(180deg); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .service-selector-section { margin-bottom: 24px; } .selector-row { display: -webkit-flex; display: flex; -webkit-align-items: center; align-items: center; gap: 12px; margin-bottom: 24px; } .selector-row .swap-btn { width: 44px; height: 44px; flex-shrink: 0; } .selector-row .service-select { flex: 1; max-width: none; } .action-buttons-section { display: -webkit-flex; display: flex; -webkit-flex-direction: column; flex-direction: column; gap: 12px; } .toggle-buttons-row, .control-buttons-row { display: -webkit-flex; display: flex; -webkit-flex-direction: row; flex-direction: row; gap: 12px; } .toggle-buttons-row .action-btn, .control-buttons-row .action-btn { flex: 1; min-width: 0; } .action-btn { padding: 14px 18px; border: none; border-radius: 12px; cursor: pointer; font-size: 15px; font-weight: 500; -webkit-transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); display: -webkit-flex; display: flex; -webkit-align-items: center; align-items: center; -webkit-justify-content: center; justify-content: center; gap: 8px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif, 'Microsoft YaHei', sans-serif, emoji, 'Apple Color Emoji', 'Segoe UI Emoji'; min-height: 50px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .action-btn:hover { -webkit-transform: translateY(-3px); transform: translateY(-3px); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1); } .action-btn:active { -webkit-transform: translateY(0); transform: translateY(0); } .action-btn.primary-btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; flex: 1 1 100%; padding: 16px; font-weight: 600; letter-spacing: 0.5px; } .action-btn.primary-btn:hover { box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3); background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%); } .action-btn.secondary-btn { background: white; color: #4a5568; border: 1px solid #e2e8f0; flex: 1; } .action-btn.secondary-btn:hover { background: #f8fafc; border-color: #cbd5e0; color: #2d3748; } .toggle-btn { position: relative; } .toggle-indicator { font-size: 14px; transition: all 0.2s ease; } .toggle-btn.active .toggle-indicator { color: #48bb78; } .toggle-btn:not(.active) .toggle-indicator { color: #cbd5e0; } .translation-panel.minimized .panel-content { display: none; } .translate-float-button { position: fixed; right: 25px; bottom: 90px; width: 32px; height: 32px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 50%; display: -webkit-flex !important; display: flex !important; -webkit-align-items: center; align-items: center; -webkit-justify-content: center; justify-content: center; font-size: 14px; cursor: pointer; box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); z-index: 9999997; -webkit-transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); -webkit-user-select: none; user-select: none; border: 2px solid white; } .translate-float-button.draggable { cursor: grab; } .translate-float-button.dragging { cursor: grabbing; } .toggle-float-button { position: fixed; right: 25px; bottom: 130px; width: 32px; height: 32px; background: rgba(102, 126, 234, 0.8); border-radius: 50%; display: -webkit-flex !important; display: flex !important; -webkit-align-items: center; align-items: center; -webkit-justify-content: center; justify-content: center; font-size: 14px; cursor: pointer; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); z-index: 9999996; -webkit-transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); -webkit-user-select: none; user-select: none; border: 2px solid white; opacity: 0.7; } .toggle-float-button:hover { opacity: 1; -webkit-transform: scale(1.1); transform: scale(1.1); box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); } .toggle-float-button:active { -webkit-transform: scale(0.9); transform: scale(0.9); } .translate-float-button:hover { -webkit-transform: scale(1.15); transform: scale(1.15); box-shadow: 0 10px 30px rgba(102, 126, 234, 0.5); } .translate-float-button:active { -webkit-transform: scale(0.95); transform: scale(0.95); } .translate-result-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 99999999; display: flex; align-items: center; justify-content: center; } .translate-result-content { background: white; border-radius: 16px; width: 90%; max-width: 800px; max-height: 80vh; display: flex; flex-direction: column; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); } .result-header { padding: 20px 24px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; } .result-header h3 { margin: 0; font-size: 18px; font-weight: 600; } .result-close { width: 32px; height: 32px; border: none; background: transparent; font-size: 24px; cursor: pointer; border-radius: 6px; color: #666; } .result-close:hover { background: #f0f0f0; } .result-body { padding: 24px; overflow-y: auto; flex: 1; } .text-section { margin-bottom: 20px; } .text-section:last-child { margin-bottom: 0; } .text-section h4 { margin: 0 0 12px 0; font-size: 14px; color: #666; font-weight: 500; } .text-content { padding: 16px; background: #f8f9fa; border-radius: 10px; font-size: 15px; line-height: 1.6; color: #333; white-space: pre-wrap; word-break: break-word; } .translated-content { background: #e8f4f8; } .result-footer { padding: 16px 24px; border-top: 1px solid #e0e0e0; display: flex; gap: 12px; justify-content: flex-end; } .result-btn { padding: 10px 20px; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; -webkit-transition: all 0.2s; /* Webkit兼容 */ transition: all 0.2s; } .result-btn-primary { background: #667eea; color: white; } .result-btn-primary:hover { background: #5568d3; } .result-btn-primary:active, .result-btn-secondary:active { transform: translateY(0); } .result-btn-secondary { background: #e0e0e0; color: #333; } .result-btn-secondary:hover { background: #d0d0d0; } .result-btn-secondary:active { transform: translateY(0); } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .progress-fill.loading { animation: pulse 1.5s infinite; } @keyframes spin { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } } .loading-spinner { display: inline-block; animation: spin 1s linear infinite; font-size: 16px; } @keyframes fadeIn { from { opacity: 0; -webkit-transform: translateY(10px); transform: translateY(10px); } to { opacity: 1; -webkit-transform: translateY(0); transform: translateY(0); } } .translation-tooltip { position: absolute; background: rgba(0, 0, 0, 0.9); color: white; padding: 12px 16px; border-radius: 8px; font-size: 14px; line-height: 1.5; z-index: 9999999; max-width: 300px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); animation: fadeIn 0.2s ease-out; pointer-events: none; } .translation-tooltip .tooltip-header { font-weight: bold; margin-bottom: 6px; color: #66ccff; } .translation-tooltip .tooltip-content { word-break: break-word; } .translation-tooltip .tooltip-loading { display: inline-block; animation: spin 1s linear infinite; margin-right: 8px; } `; GM_addStyle(styles); } bindEvents() { // 使用事件委托确保动态元素也能绑定 $(document).on('click', '.minimize-btn', () => this.toggleMinimize()); $(document).on('click', '.settings-btn', () => { console.log('顶部设置按钮点击'); if (settingsManager) { settingsManager.open(); } else { console.error('settingsManager 未初始化'); } }); $(document).on('click', '#open-settings-btn', () => { console.log('底部设置按钮点击'); if (settingsManager) { settingsManager.open(); } else { console.error('settingsManager 未初始化'); GM_notification({ text: '设置功能初始化失败,请刷新页面', title: 'AI Chat翻译助手', timeout: 3000 }); } }); $(document).on('click', '.close-btn', () => this.hide()); // 悬浮球点击事件 - 增强版本(提高360浏览器兼容性) // 使用事件委托确保即使DOM元素重新创建也能正常工作 $(document).on('click', '#translate-float-button', (e) => { e.preventDefault(); e.stopPropagation(); console.log('悬浮球被点击'); console.log('当前面板状态:', this.isVisible); console.log('面板元素是否存在:', $('#translation-panel').length > 0); this.toggle(); }); // 悬浮球隐藏/显示按钮事件 $(document).on('click', '#toggle-float-button', (e) => { e.preventDefault(); e.stopPropagation(); const floatButton = $('#translate-float-button'); const toggleButton = $('#toggle-float-button'); if (floatButton.is(':visible')) { // 隐藏悬浮球 floatButton.hide(); toggleButton.attr('title', '显示翻译按钮'); toggleButton.html('👁️‍🗨️'); // 保存隐藏状态 GM_setValue('floatButtonHidden', true); } else { // 显示悬浮球 floatButton.show(); toggleButton.attr('title', '隐藏翻译按钮'); toggleButton.html('👁️'); // 保存显示状态 GM_setValue('floatButtonHidden', false); } }); // 只在支持的浏览器中添加touch事件 if ('ontouchstart' in window) { $(document).on('touchstart', '#translate-float-button', (e) => { e.preventDefault(); e.stopPropagation(); console.log('悬浮球被触摸'); this.toggle(); }); } // 切换按钮事件 $(document).on('click', '.toggle-btn', (e) => { const btn = $(e.target).closest('.toggle-btn'); const feature = btn.data('feature'); btn.toggleClass('active'); // 仅更新临时状态,不立即应用设置 if (feature === 'word-translate') { // 临时保存状态,等待保存设置时才真正应用 btn.attr('data-temp-state', btn.hasClass('active') ? 'enabled' : 'disabled'); // 同步更新设置页面的复选框临时状态 if ($('#word-translate').length > 0) { $('#word-translate').prop('checked', btn.hasClass('active')); } } else if (feature === 'hover-translate') { // 临时保存状态,等待保存设置时才真正应用 btn.attr('data-temp-state', btn.hasClass('active') ? 'enabled' : 'disabled'); // 同步更新设置页面的复选框临时状态 if ($('#hover-translate').length > 0) { $('#hover-translate').prop('checked', btn.hasClass('active')); } } console.log(`${feature} 功能临时${btn.hasClass('active') ? '启用' : '禁用'}`); }); $(document).on('click', '#translate-btn', () => this.handleTranslate()); $(document).on('click', '#full-page-translate-btn', () => this.handleFullPageTranslate()); $(document).on('click', '#pause-translate-btn', () => this.handlePauseTranslate()); $(document).on('click', '#resume-translate-btn', () => this.handleResumeTranslate()); $(document).on('click', '#restore-original-btn', () => this.handleRestoreOriginal()); $(document).on('click', '#save-panel-settings-btn', () => this.handleSavePanelSettings()); $(document).on('click', '.swap-btn', () => this.swapLanguages()); this.initDrag(); this.bindShortcuts(); } bindShortcuts() { $(document).on('keydown', (e) => { if (this.matchShortcut(e, config.shortcut.translate)) { e.preventDefault(); this.handleTranslate(); } if (this.matchShortcut(e, config.shortcut.togglePanel)) { e.preventDefault(); this.toggle(); } // Ctrl+U 快捷键隐藏/显示悬浮球 if (e.ctrlKey && e.key.toLowerCase() === 'u') { e.preventDefault(); const floatButton = $('#translate-float-button'); const toggleButton = $('#toggle-float-button'); if (floatButton.is(':visible')) { // 隐藏悬浮球 floatButton.hide(); toggleButton.attr('title', '显示翻译按钮'); toggleButton.html('👁️‍🗨️'); // 保存隐藏状态 GM_setValue('floatButtonHidden', true); } else { // 显示悬浮球 floatButton.show(); toggleButton.attr('title', '隐藏翻译按钮'); toggleButton.html('👁️'); // 保存显示状态 GM_setValue('floatButtonHidden', false); } } }); // 监听文本选择事件,实现自动翻译 let selectionTimeout; $(document).on('mouseup keyup', () => { clearTimeout(selectionTimeout); selectionTimeout = setTimeout(() => { const selectedText = window.getSelection().toString().trim(); if (selectedText && selectedText.length > 0) { // 检查选中的文本是否为非中文 const chineseChars = selectedText.match(/[\u4e00-\u9fa5]/g); const chineseRatio = chineseChars ? chineseChars.length / selectedText.length : 0; // 如果中文字符比例小于50%,则自动翻译 if (chineseRatio < 0.5) { // 延迟一小段时间确保选择完成后再翻译 setTimeout(() => { this.handleTranslate(); }, 300); } } }, 500); // 500ms防抖延迟 }); // 实现鼠标悬停翻译功能 let hoverTimeout; let currentHoverElement = null; // 优化:使用更精确的选择器和更快的文本检测 $(document).on('mouseover', 'p, div:not(:empty), span:not(:empty), h1, h2, h3, h4, h5, h6, li, td, th', function(e) { // 检查悬浮翻译功能是否启用 if (!config.features.hoverTranslate) { return; } const element = $(this); // 优化:快速过滤明显不需要翻译的元素 if (element.closest('#translation-panel, #translate-float-button, #settings-panel, .translation-tooltip').length > 0) { return; } const text = element.text().trim(); // 优化过滤条件:文本长度适中,且包含非中文字符 if (text.length > 5 && text.length < 300) { // 缩小范围以提高性能 // 优化:快速检测是否包含非中文字符 if (/[a-zA-Z]/.test(text)) { // 先检查是否有英文字母 const chineseChars = text.match(/[\u4e00-\u9fa5]/g); const chineseRatio = chineseChars ? chineseChars.length / text.length : 0; // 如果中文字符比例小于90%,则可能是需要翻译的文本 if (chineseRatio < 0.9) { currentHoverElement = element; // 优化:减少延迟时间到300ms以提高响应速度 hoverTimeout = setTimeout(() => { // 再次检查元素是否仍然存在且功能启用 if (currentHoverElement && config.features.hoverTranslate) { translationPanel.handleHoverTranslate(element, text); } }, 300); // 减少到300ms延迟触发 } } } }); $(document).on('mouseout', 'p, div, span, h1, h2, h3, h4, h5, h6, li, td, th', function(e) { clearTimeout(hoverTimeout); currentHoverElement = null; }); } matchShortcut(e, shortcut) { const parts = shortcut.toLowerCase().split('+').map(p => p.trim()); const key = parts[parts.length - 1]; const hasCtrl = parts.includes('ctrl') ? e.ctrlKey : !e.ctrlKey; const hasShift = parts.includes('shift') ? e.shiftKey : !e.shiftKey; const hasAlt = parts.includes('alt') ? e.altKey : !e.altKey; if (parts.includes('ctrl') && !e.ctrlKey) return false; if (parts.includes('shift') && !e.shiftKey) return false; if (parts.includes('alt') && !e.altKey) return false; return e.key.toLowerCase() === key.toLowerCase(); } initDrag() { const panel = $('#translation-panel'); const header = $('.panel-header'); let isDragging = false; let startX, startY, startLeft, startTop; header.on('mousedown', (e) => { if (this.isLocked) return; isDragging = true; startX = e.clientX; startY = e.clientY; const offset = panel.offset(); startLeft = offset.left; startTop = offset.top; }); $(document).on('mousemove', (e) => { if (!isDragging) return; const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; panel.css({ left: startLeft + deltaX + 'px', top: startTop + deltaY + 'px', right: 'auto' }); }); $(document).on('mouseup', () => { if (isDragging) { isDragging = false; this.savePosition(); } }); } // 初始化悬浮球拖拽功能(限制只能垂直移动) initFloatButtonDrag() { const button = $('#translate-float-button'); const toggleButton = $('#toggle-float-button'); let isDragging = false; let startY, startBottom; const rightPos = 25; // 固定右侧位置 const toggleOffset = 40; // 隐藏按钮与悬浮球的垂直距离 // 添加拖拽样式 button.addClass('draggable'); button.on('mousedown', (e) => { isDragging = true; startY = e.clientY; startBottom = parseInt(button.css('bottom')) || 90; button.addClass('dragging'); e.preventDefault(); }); $(document).on('mousemove.floatButton', (e) => { if (!isDragging) return; const deltaY = startY - e.clientY; // 垂直方向变化 let newBottom = startBottom + deltaY; // 限制移动范围 const windowHeight = $(window).height(); newBottom = Math.max(20, Math.min(newBottom, windowHeight - 50)); button.css({ bottom: newBottom + 'px', right: rightPos + 'px' }); // 同时移动隐藏按钮 toggleButton.css({ bottom: (newBottom + toggleOffset) + 'px', right: rightPos + 'px' }); }); $(document).on('mouseup.floatButton', () => { if (isDragging) { isDragging = false; button.removeClass('dragging'); // 保存位置 const currentPosition = { bottom: parseInt(button.css('bottom')) }; GM_setValue('floatButtonPosition', currentPosition); } }); } async handleTranslate() { // 检查域名是否被屏蔽 if (config.features.domainBlocked) { console.log('当前域名已被屏蔽,无法使用翻译功能'); return; } const selectedText = window.getSelection().toString().trim(); if (!selectedText) { GM_notification({ text: '请先选择要翻译的文本', title: 'AI Chat翻译助手', timeout: 2000 }); return; } // 检查划词翻译功能是否启用 if (!config.features.wordTranslate) { console.log('划词翻译功能已禁用,当前配置状态:', config.features.wordTranslate); console.log('保存的设置值:', GM_getValue('word_translate')); return; } this.show(); const sourceLang = $('#source-lang').val(); const targetLang = $('#target-lang').val(); const service = $('#translation-service').val(); this.isTranslating = true; this.updateProgress(0); $('.progress-text').text('翻译中...'); $('.progress-fill').addClass('loading'); try { translationEngine.reset(); // 显示加载动画 $('.loading-spinner').show(); const result = await translationEngine.translate(selectedText, { sourceLang, targetLang, service }); this.updateProgress(100); $('.progress-text').text('翻译完成'); $('.progress-fill').removeClass('loading'); // 隐藏加载动画 $('.loading-spinner').hide(); this.updateWordCount(selectedText.length, result.text.length); // 使用悬浮提示框显示翻译结果 this.showTooltipTranslation(selectedText, result.text); } catch (error) { console.error('翻译失败:', error); this.updateProgress(0); $('.progress-text').text('翻译失败'); $('.progress-fill').removeClass('loading'); // 隐藏加载动画 $('.loading-spinner').hide(); GM_notification({ text: '翻译失败: ' + error.message, title: 'AI Chat翻译助手', timeout: 3000 }); } finally { this.isTranslating = false; } } showTranslationResult(originalText, translatedText) { $('.translate-result-overlay').remove(); const resultHTML = `

翻译结果

原文

${this.escapeHtml(originalText)}

译文

${this.escapeHtml(translatedText)}
`; $('body').append(resultHTML); $('.result-close, .close-result, .translate-result-overlay').on('click', (e) => { if (e.target === e.currentTarget) { $('.translate-result-overlay').remove(); } }); $('.copy-result').on('click', () => { GM_setClipboard(translatedText); GM_notification({ text: '译文已复制到剪贴板', title: 'AI Chat翻译助手', timeout: 2000 }); }); } // 显示悬浮提示框翻译结果 showTooltipTranslation(originalText, translatedText) { // 移除已存在的提示框 $('.translation-tooltip').remove(); // 获取选中文本的位置 const selection = window.getSelection(); if (selection.rangeCount === 0) return; const range = selection.getRangeAt(0); const rect = range.getBoundingClientRect(); // 创建提示框 const tooltipHTML = `
翻译结果
${this.escapeHtml(translatedText)}
`; $('body').append(tooltipHTML); // 定位提示框 const tooltip = $('.translation-tooltip'); const tooltipWidth = tooltip.outerWidth(); const tooltipHeight = tooltip.outerHeight(); // 计算提示框位置,优先显示在选中文本下方 let left = rect.left + (rect.width / 2) - (tooltipWidth / 2); let top = rect.bottom + 10; // 确保提示框在视窗内 const windowWidth = $(window).width(); const windowHeight = $(window).height(); if (left < 10) left = 10; if (left + tooltipWidth > windowWidth - 10) left = windowWidth - tooltipWidth - 10; if (top + tooltipHeight > windowHeight - 10) { // 如果下方空间不足,显示在上方 top = rect.top - tooltipHeight - 10; } tooltip.css({ left: left + 'px', top: top + 'px' }); // 点击其他地方关闭提示框 const closeTooltip = (e) => { if (!tooltip.is(e.target) && tooltip.has(e.target).length === 0) { tooltip.remove(); $(document).off('click', closeTooltip); } }; setTimeout(() => { $(document).on('click', closeTooltip); }, 100); } // 处理悬停翻译 async handleHoverTranslate(element, text) { // 检查域名是否被屏蔽 if (config.features.domainBlocked) { console.log('当前域名已被屏蔽,无法使用翻译功能'); return; } // 检查悬浮翻译功能是否启用 if (!config.features.hoverTranslate) { return; } // 移除已存在的提示框 $('.translation-tooltip').remove(); // 获取元素位置 const rect = element[0].getBoundingClientRect(); // 显示加载提示框 const loadingTooltipHTML = `
翻译中... 🔄
${this.escapeHtml(text.substring(0, 100))}...
`; $('body').append(loadingTooltipHTML); // 定位提示框 const tooltip = $('.translation-tooltip'); const tooltipWidth = tooltip.outerWidth(); const tooltipHeight = tooltip.outerHeight(); // 计算提示框位置,优先显示在元素下方 let left = rect.left + (rect.width / 2) - (tooltipWidth / 2); let top = rect.bottom + 10; // 确保提示框在视窗内 const windowWidth = $(window).width(); const windowHeight = $(window).height(); if (left < 10) left = 10; if (left + tooltipWidth > windowWidth - 10) left = windowWidth - tooltipWidth - 10; if (top + tooltipHeight > windowHeight - 10) { // 如果下方空间不足,显示在上方 top = rect.top - tooltipHeight - 10; } tooltip.css({ left: left + 'px', top: top + 'px' }); try { const sourceLang = $('#source-lang').val(); const targetLang = $('#target-lang').val(); const service = $('#translation-service').val(); // 优化:使用Promise.race实现超时控制,避免长时间等待 const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('翻译超时')), 5000) ); const result = await Promise.race([ translationEngine.translate(text, { sourceLang, targetLang, service }), timeoutPromise ]); // 更新提示框内容 tooltip.find('.tooltip-header').html('翻译结果'); tooltip.find('.tooltip-content').html(this.escapeHtml(result.text)); // 点击其他地方关闭提示框 const closeTooltip = (e) => { if (!tooltip.is(e.target) && tooltip.has(e.target).length === 0) { tooltip.remove(); $(document).off('click', closeTooltip); } }; setTimeout(() => { $(document).on('click', closeTooltip); }, 100); } catch (error) { console.error('悬停翻译失败:', error); tooltip.find('.tooltip-header').html('翻译失败'); tooltip.find('.tooltip-content').html(error.message || '翻译失败,请重试'); } } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } swapLanguages() { const source = $('#source-lang').val(); const target = $('#target-lang').val(); if (source !== 'auto') { $('#source-lang').val(target); $('#target-lang').val(source); } } updateProgress(progress) { this.progress = progress; $('.progress-fill').css('width', `${progress}%`); } updateWordCount(source, target) { this.wordCount = { source, target }; $('.source-count').text(source); $('.target-count').text(target); } toggleMinimize() { this.isMinimized = !this.isMinimized; $('#translation-panel').toggleClass('minimized'); $('.minimize-btn').text(this.isMinimized ? '+' : '−'); } toggle() { this.isVisible = !this.isVisible; console.log('切换面板状态:', this.isVisible ? '显示' : '隐藏'); console.log('面板元素状态:', $('#translation-panel').css('display')); if (this.isVisible) { this.show(); } else { this.hide(); } } show() { console.log('显示翻译面板'); let panel = $('#translation-panel'); // 如果面板不存在,尝试重新创建 if (panel.length === 0) { console.warn('面板元素不存在,尝试重新创建'); this.createPanel(); panel = $('#translation-panel'); // 重新绑定事件 this.bindEvents(); // 重新加载位置 this.loadPosition(); // 应用当前配置 this.applyCurrentConfig(); } if (panel.length > 0) { panel.fadeIn(300); this.isVisible = true; console.log('面板显示成功'); // 确保面板在视口中可见 this.ensurePanelVisible(); } else { console.error('面板元素无法创建,无法显示'); GM_notification({ text: '无法显示翻译面板,请刷新页面重试', title: 'AI Chat翻译助手', timeout: 3000 }); } } hide() { console.log('隐藏翻译面板'); const panel = $('#translation-panel'); if (panel.length > 0) { panel.fadeOut(300); this.isVisible = false; console.log('面板隐藏成功'); } else { console.warn('面板元素不存在,可能已被移除'); this.isVisible = false; } } // 全页翻译功能 async handleFullPageTranslate() { // 检查域名是否被屏蔽 if (config.features.domainBlocked) { console.log('当前域名已被屏蔽,无法使用翻译功能'); return; } console.log('开始全页翻译'); if (this.isTranslating) { GM_notification({ text: '正在翻译中,请稍后...', title: 'AI Chat翻译助手', timeout: 2000 }); return; } const targetLang = $('#target-lang').val(); const service = $('#translation-service').val(); this.isTranslating = true; this.isPaused = false; this.updateProgress(0); $('.progress-text').text('扫描页面中...'); // 显示暂停按钮 $('#pause-translate-btn').show(); $('#resume-translate-btn').hide(); try { // 创建全页翻译器实例 if (!window.fullPageTranslator) { window.fullPageTranslator = new FullPageTranslator(); } // 开始翻译 await window.fullPageTranslator.translate(targetLang, service, (progress) => { // 检查是否已暂停 if (!this.isPaused) { this.updateProgress(progress); $('.progress-text').text(`翻译中... ${progress}%`); } }); this.updateProgress(100); $('.progress-text').text('翻译完成'); // 隐藏暂停按钮 $('#pause-translate-btn').hide(); $('#resume-translate-btn').hide(); GM_notification({ text: '全页翻译完成!', title: 'AI Chat翻译助手', timeout: 2000 }); } catch (error) { console.error('全页翻译失败:', error); this.updateProgress(0); $('.progress-text').text('翻译失败'); // 隐藏暂停按钮 $('#pause-translate-btn').hide(); $('#resume-translate-btn').hide(); GM_notification({ text: '全页翻译失败: ' + error.message, title: 'AI Chat翻译助手', timeout: 3000 }); } finally { this.isTranslating = false; setTimeout(() => { if (!this.isTranslating) { this.updateProgress(0); $('.progress-text').text('准备就绪'); } }, 2000); } } // 还原原文功能 handleRestoreOriginal() { // 检查域名是否被屏蔽 if (config.features.domainBlocked) { console.log('当前域名已被屏蔽,无法使用翻译功能'); return; } console.log('还原原文'); if (this.isTranslating) { GM_notification({ text: '请等待翻译完成后再还原', title: 'AI Chat翻译助手', timeout: 2000 }); return; } if (window.fullPageTranslator) { window.fullPageTranslator.restore(); GM_notification({ text: '已还原为原文', title: 'AI Chat翻译助手', timeout: 2000 }); } else { GM_notification({ text: '没有可还原的内容', title: 'AI Chat翻译助手', timeout: 2000 }); } } // 暂停翻译功能 handlePauseTranslate() { if (this.isTranslating && translationEngine) { translationEngine.pause(); this.isPaused = true; $('#pause-translate-btn').hide(); $('#resume-translate-btn').show(); $('.progress-text').text('翻译已暂停'); GM_notification({ text: '翻译已暂停', title: 'AI Chat翻译助手', timeout: 2000 }); } } // 继续翻译功能 handleResumeTranslate() { if (this.isPaused && translationEngine) { translationEngine.resume(); this.isPaused = false; $('#resume-translate-btn').hide(); $('#pause-translate-btn').show(); $('.progress-text').text('继续翻译中...'); GM_notification({ text: '翻译已继续', title: 'AI Chat翻译助手', timeout: 2000 }); } } // 保存面板设置功能 handleSavePanelSettings() { // 保存语言和服务设置到全局配置 const sourceLang = $('#source-lang').val(); const targetLang = $('#target-lang').val(); const service = $('#translation-service').val(); // 更新全局配置 config.translation.sourceLang = sourceLang; config.translation.targetLang = targetLang; config.translation.defaultService = service; // 保存到GM存储 GM_setValue('default_source_lang', sourceLang); GM_setValue('default_target_lang', targetLang); GM_setValue('default_service', service); // 处理悬浮翻译和划词翻译的临时状态 const wordTranslateBtn = $('#translate-btn'); const hoverTranslateBtn = $('#hover-translate-btn'); // 获取临时状态并应用到全局配置 const wordTranslateTempState = wordTranslateBtn.attr('data-temp-state'); const hoverTranslateTempState = hoverTranslateBtn.attr('data-temp-state'); if (wordTranslateTempState) { const isEnabled = wordTranslateTempState === 'enabled'; config.features.wordTranslate = isEnabled; GM_setValue('word_translate', isEnabled); } if (hoverTranslateTempState) { const isEnabled = hoverTranslateTempState === 'enabled'; config.features.hoverTranslate = isEnabled; GM_setValue('hover_translate', isEnabled); // 同步更新设置页面的复选框 if ($('#hover-translate').length > 0) { $('#hover-translate').prop('checked', isEnabled); } } // 如果设置管理器存在,也更新设置页面的对应值 if (settingsManager) { $('#default-source-lang').val(sourceLang); $('#default-target-lang').val(targetLang); $('#default-service').val(service); } GM_notification({ text: '面板设置已保存', title: 'AI Chat翻译助手', timeout: 2000 }); } savePosition() { const panel = $('#translation-panel'); const offset = panel.offset(); this.position = { x: offset.left, y: offset.top }; GM_setValue('panel_position', JSON.stringify(this.position)); } loadPosition() { const savedPosition = GM_getValue('panel_position'); if (savedPosition) { try { const position = JSON.parse(savedPosition); $('#translation-panel').css({ left: position.x + 'px', top: position.y + 'px', right: 'auto' }); } catch (error) { console.error('加载面板位置失败:', error); } } } // 确保面板在视口内可见 ensurePanelVisible() { const panel = $('#translation-panel'); if (panel.length > 0) { const panelRect = panel[0].getBoundingClientRect(); const windowWidth = $(window).width(); const windowHeight = $(window).height(); let newX = parseFloat(panel.css('left')) || 0; let newY = parseFloat(panel.css('top')) || 0; let changed = false; // 检查右边界 if (panelRect.right > windowWidth) { newX -= (panelRect.right - windowWidth + 20); changed = true; } // 检查左边界 if (panelRect.left < 0) { newX += (-panelRect.left + 20); changed = true; } // 检查下边界 if (panelRect.bottom > windowHeight) { newY -= (panelRect.bottom - windowHeight + 20); changed = true; } // 检查上边界 if (panelRect.top < 0) { newY += (-panelRect.top + 20); changed = true; } // 如果位置有变化,更新面板位置 if (changed) { panel.css({ left: Math.max(20, newX) + 'px', top: Math.max(20, newY) + 'px', right: 'auto' }); // 保存新位置 this.savePosition(); } } } // 应用当前配置到重新创建的面板 applyCurrentConfig() { // 应用语言设置 $('#source-lang').val(config.translation.sourceLang || 'auto'); $('#target-lang').val(config.translation.targetLang || 'zh-CN'); // 应用服务设置 $('#translation-service').val(config.translation.defaultService || 'google'); // 应用功能开关状态 if (config.features.wordTranslate) { $('#translate-btn').addClass('active').attr('data-temp-state', 'enabled'); } else { $('#translate-btn').removeClass('active').attr('data-temp-state', 'disabled'); } if (config.features.hoverTranslate) { $('#hover-translate-btn').addClass('active').attr('data-temp-state', 'enabled'); } else { $('#hover-translate-btn').removeClass('active').attr('data-temp-state', 'disabled'); } } } // 初始化应用 function init() { console.log('AI Chat翻译助手开始初始化'); // 等待jQuery加载 if (typeof $ === 'undefined') { console.log('等待jQuery加载...'); setTimeout(init, 100); return; } // 检查域名是否被屏蔽 if (isDomainBlocked()) { console.log('当前域名已被屏蔽,禁用翻译功能'); config.features.domainBlocked = true; return; // 直接返回,不初始化任何功能 } // 初始化核心组件 try { translationEngine = new TranslationEngine(); languageDetector = new LanguageDetector(); settingsManager = new SettingsManager(); translationPanel = new TranslationPanel(); // 加载保存的配置 loadSavedConfig(); // 检测语言并决定是否显示 if (config.features.autoShowButton) { const shouldShow = languageDetector.shouldShowTranslateButton(); console.log('是否需要显示翻译按钮:', shouldShow); if (shouldShow) { $('#translate-float-button').show(); } else { $('#translate-float-button').hide(); } } // 注册菜单命令 GM_registerMenuCommand('⚙️ 打开设置', () => { if (settingsManager) { settingsManager.open(); } else { console.error('设置管理器未初始化'); } }); GM_registerMenuCommand('🌐 打开翻译面板', () => { if (translationPanel) { translationPanel.show(); } else { console.error('翻译面板未初始化'); } }); GM_registerMenuCommand('🗑️ 清除缓存', () => { if (confirm('确定要清除所有翻译缓存吗?')) { if (translationEngine && translationEngine.cache) { translationEngine.cache.clear(); GM_notification({ text: '缓存已清除', title: 'AI Chat翻译助手', timeout: 2000 }); } } }); console.log('AI Chat翻译助手初始化完成'); console.log('设置管理器:', settingsManager ? '已初始化' : '未初始化'); } catch (error) { console.error('AI Chat翻译助手初始化失败:', error); } } // 加载保存的配置 function loadSavedConfig() { config.translation.defaultService = GM_getValue('default_service', 'google'); config.translation.sourceLang = GM_getValue('default_source_lang', 'auto'); config.translation.targetLang = GM_getValue('default_target_lang', 'zh-CN'); config.features.autoShowButton = GM_getValue('auto_show_button', true); config.features.cacheEnabled = GM_getValue('enable_cache', true); config.features.wordTranslate = GM_getValue('word_translate', false); // 加载划词翻译设置 config.features.hoverTranslate = GM_getValue('hover_translate', false); // 加载悬浮翻译设置 config.features.domainBlocked = isDomainBlocked(); // 检查域名是否被屏蔽 config.shortcut.translate = GM_getValue('shortcut_translate', 'Ctrl+Shift+T'); config.shortcut.togglePanel = GM_getValue('shortcut_toggle', 'Alt+T'); // 应用到UI $('#translation-service').val(config.translation.defaultService); $('#source-lang').val(config.translation.sourceLang); $('#target-lang').val(config.translation.targetLang); // 设置划词翻译和悬浮翻译按钮状态及临时状态 if (config.features.wordTranslate) { $('#translate-btn').addClass('active').attr('data-temp-state', 'enabled'); } else { $('#translate-btn').removeClass('active').attr('data-temp-state', 'disabled'); } if (config.features.hoverTranslate) { $('#hover-translate-btn').addClass('active').attr('data-temp-state', 'enabled'); } else { $('#hover-translate-btn').removeClass('active').attr('data-temp-state', 'disabled'); } } // 页面加载完成后初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();