// ==UserScript== // @name TransLite v26.1.6 // @namespace https://via.browser/ // @version 26.1.6 // @description TransLite: LLM批量翻译,MyMemory兜底,三指长按,可拖动按钮,HARDCODED全局配置 // @author rewwoxv. // @match *://*/* // @grant none // @run-at document_idle // ==/UserScript== (function() { 'use strict'; // ==================================================================================== // ★★★ 全局配置硬编码区 ★★★ // // 由于 Via 浏览器普通脚本无法使用 GM_setValue 跨域存储, // localStorage 受同源策略限制——切换域名后配置会重置。 // // 解决方案:将常用参数直接写在下方 HARDCODED 对象里, // 这些值在所有网页上作为"全局默认值"生效。 // // 网页内的"设置面板"保存的内容仅作为 当前域名的独立覆写(Override) —— // 切换到其他域名时,会回退到此处的硬编码值。 // ==================================================================================== var HARDCODED = { engine: 'llm', // 翻译引擎: 'llm' | 'mymemory' | 'baidu' | 'custom' apiBase: 'https://api.qnaigc.com/v1', // ← 在此填写 API Base URL(七牛云等) apiKey: '', // ← 在此填写你的 API Key(推荐硬编码,避免重复输入) model: 'z-ai/glm-4.5-air-free', // 模型名称 targetLang: 'Simplified Chinese', // 目标语言 translateMode: 'replace' // 'replace'=直接替换 / 'bilingual'=双语模式 // 其他参数请通过网页内的设置面板调整,保存后对当前域名生效 }; // ==================== 内部默认配置(不建议直接修改) ==================== var DEFAULT_CONFIG = Object.assign({ baiduAppId: '', baiduSecret: '', baiduFrom: 'auto', baiduTo: 'zh', customName: '', customApiUrl: '', customMethod: 'POST', customBodyTemplate: '', customHeaders: '', customResponsePath: '', customFrom: 'auto', customTo: 'zh', mmFrom: 'en', mmTo: 'zh', systemPrompt: 'You are a precise translator. I will give you multiple texts, each prefixed with [N] where N is a number. Translate ALL of them into {{targetLang}}. Output each translation on its own line, prefixed with the SAME [N] marker. Do NOT add any extra text, explanations, or notes. Output ONLY [N] translation lines. Example:\nInput:\n[0] Hello world\n[1] Good morning\nOutput:\n[0] 你好世界\n[1] 早上好', proxyPrefix: '', enableThinking: false, llmTemperature: 0.3, llmMaxTokens: 8192, llmTimeout: 60, btnColor: '#2563eb', btnSize: 46, btnOpacity: 0.9, toastDuration: 2500, enableThreeFingerLongPress: true, enableFloatBtn: false, threeFingerLongPressMs: 500, maxBatchChars: 5000, maxBatchParagraphs: 15, batchDelayMs: 3000, maxRetries: 3, retryTimes: 2, retryDelayBase: 2000, settingsScope: 'global', whitelistPages: [], blacklistPages: [], autoTranslate: false, settingsBtnY: 70, settingsBtnSide: 'right' }, HARDCODED); // HARDCODED 的值覆盖上面的同名字段 // ==================== 设置作用域管理 ==================== var GLOBAL_KEY = 'llm_translator_config'; var SCOPE_KEY = 'llm_translator_scope'; function getHostname() { try { return window.location.hostname; } catch(e) { return 'unknown'; } } function getScopeInfo() { try { var raw = localStorage.getItem(SCOPE_KEY); if (raw) { var s = JSON.parse(raw); return { settingsScope: s.settingsScope || 'global', whitelistPages: s.whitelistPages || [], blacklistPages: s.blacklistPages || [] }; } } catch(e) {} return { settingsScope: 'global', whitelistPages: [], blacklistPages: [] }; } function saveScopeInfo(scopeInfo) { try { localStorage.setItem(SCOPE_KEY, JSON.stringify(scopeInfo)); } catch(e) {} } function getConfigKey() { var si = getScopeInfo(); var hn = getHostname(); switch (si.settingsScope) { case 'perPage': return GLOBAL_KEY + '_' + hn; case 'whitelist': if (si.whitelistPages.indexOf(hn) !== -1) return GLOBAL_KEY + '_' + hn; return GLOBAL_KEY; case 'blacklist': if (si.blacklistPages.indexOf(hn) !== -1) return GLOBAL_KEY + '_' + hn; return GLOBAL_KEY; default: // 'global' return GLOBAL_KEY; } } /** * saveConfig: * ① 始终写入 GLOBAL_KEY(全局主副本,跨域读回时有兜底) * ② 若 scope 是 perPage/whitelist/blacklist 且当前域名命中,额外写入 per-page key * * 注:localStorage 受同源策略限制,切换到完全不同的 origin 后 * 仍需在 HARDCODED 里预写 apiKey 等高频字段才能真正全局生效。 */ function saveConfig(cfg) { var configJson = JSON.stringify(cfg); var hn = getHostname(); var scope = cfg.settingsScope || 'global'; var ppKey = GLOBAL_KEY + '_' + hn; // 计算是否需要写 per-page key var writePerPage = false; if (scope === 'perPage') { writePerPage = true; } else if (scope === 'whitelist' && (cfg.whitelistPages || []).indexOf(hn) !== -1) { writePerPage = true; } else if (scope === 'blacklist' && (cfg.blacklistPages || []).indexOf(hn) !== -1) { writePerPage = true; } try { localStorage.setItem(GLOBAL_KEY, configJson); // ★ 始终写全局主副本 if (writePerPage) { localStorage.setItem(ppKey, configJson); } else { // 清理当前域名可能残留的 per-page 覆写,避免混淆 try { localStorage.removeItem(ppKey); } catch(e2) {} } } catch(e) { console.warn('[LLM翻译] 无法保存配置:', e); } saveScopeInfo({ settingsScope: scope, whitelistPages: cfg.whitelistPages || [], blacklistPages: cfg.blacklistPages || [] }); } function loadConfig() { var key = getConfigKey(); var cfg = null; try { var saved = localStorage.getItem(key); if (saved) cfg = JSON.parse(saved); } catch(e) { console.warn('[LLM翻译] 读取配置失败:', e); } if (!cfg && key !== GLOBAL_KEY) { try { var gSaved = localStorage.getItem(GLOBAL_KEY); if (gSaved) cfg = JSON.parse(gSaved); } catch(e) {} } // 【修复合并顺序】:默认配置 < HARDCODED全局兜底 < 当前页面UI保存的配置 var finalConfig = Object.assign({}, DEFAULT_CONFIG, HARDCODED, cfg || {}); // 【保护机制】:特殊处理,如果 localStorage 中存在空值(例如之前未填写的 API Key),强制使用 HARDCODED 中的值 if (HARDCODED.apiKey && !finalConfig.apiKey) finalConfig.apiKey = HARDCODED.apiKey; if (HARDCODED.apiBase && !finalConfig.apiBase) finalConfig.apiBase = HARDCODED.apiBase; if (HARDCODED.model && !finalConfig.model) finalConfig.model = HARDCODED.model; return finalConfig; } var config = loadConfig(); // ==================== 状态管理 ==================== var translating = false; var progressEl = null; var backupMap = null; var bilingualElements = []; // ==================== MD5 工具 ==================== function md5(string) { function md5cycle(x, k) { var a = x[0], b = x[1], c = x[2], d = x[3]; a = ff(a, b, c, d, k[0], 7, -680876936); d = ff(d, a, b, c, k[1], 12, -389564586); c = ff(c, d, a, b, k[2], 17, 606105819); b = ff(b, c, d, a, k[3], 22, -1044525330); a = ff(a, b, c, d, k[4], 7, -176418897); d = ff(d, a, b, c, k[5], 12, 1200080426); c = ff(c, d, a, b, k[6], 17, -1473231341); b = ff(b, c, d, a, k[7], 22, -45705983); a = ff(a, b, c, d, k[8], 7, 1770035416); d = ff(d, a, b, c, k[9], 12, -1958414417); c = ff(c, d, a, b, k[10], 17, -42063); b = ff(b, c, d, a, k[11], 22, -1990404162); a = ff(a, b, c, d, k[12], 7, 1804603682); d = ff(d, a, b, c, k[13], 12, -403257723); c = ff(c, d, a, b, k[14], 17, 1236535329); b = ff(b, c, d, a, k[15], 22, -374384042); a = gg(a, b, c, d, k[1], 5, -168207062); d = gg(d, a, b, c, k[6], 9, -1069501632); c = gg(c, d, a, b, k[11], 14, 643717713); b = gg(b, c, d, a, k[0], 20, -373897302); a = gg(a, b, c, d, k[5], 5, -701558691); d = gg(d, a, b, c, k[10], 9, 38016083); c = gg(c, d, a, b, k[15], 14, -660478335); b = gg(b, c, d, a, k[4], 20, -405537848); a = gg(a, b, c, d, k[9], 5, 568446438); d = gg(d, a, b, c, k[14], 9, -1019803690); c = gg(c, d, a, b, k[3], 14, -187363961); b = gg(b, c, d, a, k[8], 20, 1163531501); a = gg(a, b, c, d, k[13], 5, -1444681467); d = gg(d, a, b, c, k[2], 9, -51403784); c = gg(c, d, a, b, k[7], 14, 1735328473); b = gg(b, c, d, a, k[12], 20, -1926607734); a = hh(a, b, c, d, k[5], 4, -378558); d = hh(d, a, b, c, k[8], 11, -2022574463); c = hh(c, d, a, b, k[11], 16, 1839030562); b = hh(b, c, d, a, k[14], 23, -35309556); a = hh(a, b, c, d, k[1], 4, -1530992060); d = hh(d, a, b, c, k[4], 11, 1272893353); c = hh(c, d, a, b, k[7], 16, -155497632); b = hh(b, c, d, a, k[10], 23, -1094730640); a = hh(a, b, c, d, k[13], 4, 681279174); d = hh(d, a, b, c, k[0], 11, -358537222); c = hh(c, d, a, b, k[3], 16, -722521979); b = hh(b, c, d, a, k[6], 23, 76029189); a = hh(a, b, c, d, k[9], 4, -640364487); d = hh(d, a, b, c, k[12], 11, -421815835); c = hh(c, d, a, b, k[15], 16, 530742520); b = hh(b, c, d, a, k[2], 23, -995338651); a = ii(a, b, c, d, k[0], 6, -198630844); d = ii(d, a, b, c, k[7], 10, 1126891415); c = ii(c, d, a, b, k[14], 15, -1416354905); b = ii(b, c, d, a, k[5], 21, -57434055); a = ii(a, b, c, d, k[12], 6, 1700485571); d = ii(d, a, b, c, k[3], 10, -1894986606); c = ii(c, d, a, b, k[10], 15, -1051523); b = ii(b, c, d, a, k[1], 21, -2054922799); a = ii(a, b, c, d, k[8], 6, 1873313359); d = ii(d, a, b, c, k[15], 10, -30611744); c = ii(c, d, a, b, k[6], 15, -1560198380); b = ii(b, c, d, a, k[13], 21, 1309151649); a = ii(a, b, c, d, k[4], 6, -145523070); d = ii(d, a, b, c, k[11], 10, -1120210379); c = ii(c, d, a, b, k[2], 15, 718787259); b = ii(b, c, d, a, k[9], 21, -343485551); x[0] = add32(a, x[0]); x[1] = add32(b, x[1]); x[2] = add32(c, x[2]); x[3] = add32(d, x[3]); } function cmn(q, a, b, x, s, t) { a = add32(add32(a, q), add32(x, t)); return add32((a << s) | (a >>> (32 - s)), b); } function ff(a, b, c, d, x, s, t) { return cmn((b & c) | ((~b) & d), a, b, x, s, t); } function gg(a, b, c, d, x, s, t) { return cmn((b & d) | (c & (~d)), a, b, x, s, t); } function hh(a, b, c, d, x, s, t) { return cmn(b ^ c ^ d, a, b, x, s, t); } function ii(a, b, c, d, x, s, t) { return cmn(c ^ (b | (~d)), a, b, x, s, t); } function md5blk(s) { var md5blks = [], i; for (i = 0; i < 64; i += 4) { md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i+1) << 8) + (s.charCodeAt(i+2) << 16) + (s.charCodeAt(i+3) << 24); } return md5blks; } var hex_chr = '0123456789abcdef'.split(''); function rhex(n) { var s = '', j = 0; for (; j < 4; j++) s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + hex_chr[(n >> (j * 8)) & 0x0F]; return s; } function hex(x) { for (var i = 0; i < x.length; i++) x[i] = rhex(x[i]); return x.join(''); } function add32(a, b) { return (a + b) & 0xFFFFFFFF; } function md5str(s) { var n = s.length, state = [1732584193, -271733879, -1732584194, 271733878], i; for (i = 64; i <= n; i += 64) { md5cycle(state, md5blk(s.substring(i - 64, i))); } s = s.substring(i - 64); var tail = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; for (i = 0; i < s.length; i++) tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); tail[i >> 2] |= 0x80 << ((i % 4) << 3); if (i > 55) { md5cycle(state, tail); tail = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; } tail[14] = n * 8; md5cycle(state, tail); return state; } return hex(md5str(string)); } // ==================== HTML/属性 转义 ==================== function escAttr(s) { if (!s) return ''; return String(s).replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(//g, '>'); } function escHtml(s) { if (!s) return ''; return String(s).replace(/&/g, '&').replace(//g, '>'); } // ==================== 统一的翻译入口 ==================== var hasTranslated = false; async function doTranslate() { if (translating) return; if (config.engine === 'llm' || !config.engine || config.engine === 'google') { if (!config.apiKey) { showToast('需要填写 API Key。请去七牛云官网 https://www.qiniu.com/ai/agent 或微信小程序免费申请', 'warn'); return; } if (!config.model) config.model = 'z-ai/glm-4.5-air-free'; } if (config.engine === 'baidu') { if (!config.baiduAppId || !config.baiduSecret) { showToast('百度翻译需要填写 APP ID 和密钥,请进入设置', 'warn'); return; } } if (config.engine === 'custom') { if (!config.customApiUrl) { showToast('自定义翻译平台需要填写 API 地址,请进入设置', 'warn'); return; } if (!config.customResponsePath) { showToast('自定义翻译平台需要填写响应结果提取路径', 'warn'); return; } } translating = true; updateBtnState('\u23F3', '#d97706'); try { if (config.translateMode === 'bilingual') { await translateBilingual(); } else { await translateReplace(); } updateBtnState('\u2705', '#16a34a'); hasTranslated = true; showToast('翻译完成', 'success'); } catch(err) { console.error('[LLM翻译]', err); updateBtnState('\u274C', '#dc2626'); showToast('翻译出错:' + err.message, 'error'); } finally { setTimeout(resetBtnState, 2000); translating = false; } } // ==================== UI:悬浮翻译按钮 ==================== var btn = null; function createFloatBtn() { if (btn) return btn; btn = document.createElement('div'); btn.id = 'llm-translator-btn'; btn.title = '点击翻译'; btn.style.cssText = 'position:fixed;bottom:80px;right:20px;z-index:99998;' + 'width:' + (config.btnSize || 46) + 'px;height:' + (config.btnSize || 46) + 'px;line-height:' + (config.btnSize || 46) + 'px;text-align:center;' + 'background:' + (config.btnColor || '#2563eb') + ';color:#fff;border-radius:50%;' + 'font-size:20px;cursor:pointer;box-shadow:0 4px 14px rgba(0,0,0,.3);' + 'user-select:none;-webkit-user-select:none;' + 'transition:transform .2s,background .2s;opacity:' + (config.btnOpacity || 0.9) + ';'; btn.textContent = '\uD83C\uDF10'; btn.addEventListener('click', function() { doTranslate(); }); return btn; } function removeFloatBtn() { if (btn && btn.parentNode) { btn.parentNode.removeChild(btn); btn = null; } } function updateBtnState(icon, color) { if (btn) { btn.textContent = icon; btn.style.background = color; } } function resetBtnState() { if (btn) { btn.textContent = '\uD83C\uDF10'; btn.style.background = config.btnColor || '#2563eb'; } } function applyFloatBtn() { if (config.enableFloatBtn) { var b = createFloatBtn(); if (document.body && !b.parentNode) document.body.appendChild(b); } else { removeFloatBtn(); } } // ==================== UI:可拖动吸附设置按钮 ★ 新设计 ★ ==================== var settingsBtn = null; // 圆形齿轮按钮 var settingsBtnTab = null; // 吸附后的窄条 var settingsState = 'expanded'; // 'expanded' | 'tab' | 'dragging' var settingsSnapSide = 'right'; // 'left' | 'right' var settingsY = 0; // 绝对 px 位置(top) var settingsDragOffsetY = 0; // 拖动偏移 var settingsDragStartY = 0; // 拖动起始 Y var settingsSnapTimeout = null; var settingsWasDragged = false; var settingsTapJustOpened = false; // 防止 endSettingsDrag 和 click 重复打开 var DRAG_THRESHOLD = 5; // 移动超过 5px 才算拖拽 function loadSettingsBtnPosition() { settingsSnapSide = config.settingsBtnSide || 'right'; settingsY = (config.settingsBtnY || 70) / 100 * window.innerHeight; } function saveSettingsBtnPosition() { config.settingsBtnSide = settingsSnapSide; config.settingsBtnY = Math.round(settingsY / window.innerHeight * 100); saveConfig(config); } function createSettingsBtnElements() { // 圆形齿轮按钮 settingsBtn = document.createElement('div'); settingsBtn.id = 'llm-translator-settings-btn'; settingsBtn.title = '翻译设置'; settingsBtn.style.cssText = 'position:fixed;z-index:100000;' + 'width:36px;height:36px;line-height:36px;text-align:center;' + 'background:rgba(80,80,80,0.8);color:#fff;border-radius:50%;' + 'font-size:18px;cursor:pointer;box-shadow:0 2px 10px rgba(0,0,0,.35);' + 'user-select:none;-webkit-user-select:none;' + 'transition:opacity .25s;opacity:1;'; settingsBtn.textContent = '\u2699\uFE0F'; // 吸附窄条(触发展开) settingsBtnTab = document.createElement('div'); settingsBtnTab.id = 'llm-translator-settings-tab'; settingsBtnTab.style.cssText = 'position:fixed;z-index:100000;' + 'width:8px;height:44px;' + 'background:rgba(80,80,80,0.55);' + 'border-radius:0 6px 6px 0;' + 'cursor:pointer;' + 'box-shadow:0 2px 8px rgba(0,0,0,.2);' + 'user-select:none;-webkit-user-select:none;' + 'transition:background .2s,width .2s;'; settingsBtnTab.textContent = ''; } function updateSettingsBtnAppearance() { if (!settingsBtn || !settingsBtnTab) return; if (settingsState === 'tab') { settingsBtn.style.display = 'none'; settingsBtnTab.style.display = 'block'; // 定位窄条到边缘 if (settingsSnapSide === 'left') { settingsBtnTab.style.left = '0px'; settingsBtnTab.style.right = 'auto'; settingsBtnTab.style.borderRadius = '0 6px 6px 0'; } else { settingsBtnTab.style.right = '0px'; settingsBtnTab.style.left = 'auto'; settingsBtnTab.style.borderRadius = '6px 0 0 6px'; } settingsBtnTab.style.top = Math.max(0, Math.min(settingsY - 22, window.innerHeight - 44)) + 'px'; } else { settingsBtnTab.style.display = 'none'; settingsBtn.style.display = 'block'; settingsBtn.style.opacity = '1'; if (settingsSnapSide === 'left') { settingsBtn.style.left = '4px'; settingsBtn.style.right = 'auto'; } else { settingsBtn.style.right = '4px'; settingsBtn.style.left = 'auto'; } settingsBtn.style.top = Math.max(4, Math.min(settingsY - 18, window.innerHeight - 40)) + 'px'; } } function expandSettingsFromTab(e) { if (e) e.preventDefault(); if (settingsState === 'expanded') return; settingsState = 'expanded'; updateSettingsBtnAppearance(); scheduleSettingsSnapBack(); } function scheduleSettingsSnapBack() { if (settingsSnapTimeout) clearTimeout(settingsSnapTimeout); settingsSnapTimeout = setTimeout(function() { if (settingsState === 'expanded') { settingsState = 'tab'; updateSettingsBtnAppearance(); } }, 3500); } // 鼠标/触摸拖动 function startSettingsDrag(e) { if (e.type === 'touchstart') { if (e.touches.length > 1) return; // 多指不处理 } e.preventDefault(); settingsState = 'dragging'; settingsWasDragged = false; settingsTapJustOpened = false; if (settingsSnapTimeout) { clearTimeout(settingsSnapTimeout); settingsSnapTimeout = null; } var clientY = e.touches ? e.touches[0].clientY : e.clientY; settingsDragStartY = clientY; settingsDragOffsetY = clientY - settingsY; settingsBtn.style.transition = 'none'; settingsBtn.style.opacity = '0.85'; settingsBtn.style.display = 'block'; settingsBtnTab.style.display = 'none'; document.addEventListener('mousemove', moveSettingsDrag, {passive: false}); document.addEventListener('mouseup', endSettingsDrag, {once: true}); document.addEventListener('touchmove', moveSettingsDrag, {passive: false}); document.addEventListener('touchend', endSettingsDrag, {once: true}); document.addEventListener('touchcancel', endSettingsDrag, {once: true}); } function moveSettingsDrag(e) { if (settingsState !== 'dragging') return; e.preventDefault(); var clientY = e.touches ? e.touches[0].clientY : e.clientY; // 超过阈值才算真正拖拽 if (Math.abs(clientY - settingsDragStartY) > DRAG_THRESHOLD) { settingsWasDragged = true; } if (!settingsWasDragged) return; // 还没到阈值,不移动 settingsY = Math.max(4, Math.min(clientY - settingsDragOffsetY, window.innerHeight - 40)); settingsBtn.style.top = settingsY - 18 + 'px'; // 在拖动到边缘附近时临时显示吸附方向 var clientX = e.touches ? e.touches[0].clientX : e.clientX; var nearLeft = clientX < window.innerWidth * 0.3; settingsBtn.style.left = nearLeft ? '4px' : 'auto'; settingsBtn.style.right = nearLeft ? 'auto' : '4px'; } function endSettingsDrag(e) { document.removeEventListener('mousemove', moveSettingsDrag); document.removeEventListener('touchmove', moveSettingsDrag); if (settingsState !== 'dragging') return; // 如果没真正拖拽(仅单击),直接打开设置面板 if (!settingsWasDragged) { settingsState = 'expanded'; settingsBtn.style.transition = 'opacity .25s'; updateSettingsBtnAppearance(); scheduleSettingsSnapBack(); // 标记已处理,防止后续 click 事件二次触发 settingsTapJustOpened = true; setTimeout(function() { settingsTapJustOpened = false; }, 300); // 延迟打开设置,避免 touch 序列干扰 setTimeout(function() { openSettingsModal(); }, 50); return; } // 决定吸附边 var btnRect = settingsBtn.getBoundingClientRect(); var centerX = btnRect.left + btnRect.width / 2; settingsSnapSide = (centerX < window.innerWidth / 2) ? 'left' : 'right'; // 吸附到边缘 + 窄条模式 settingsState = 'tab'; settingsBtn.style.transition = 'opacity .25s'; updateSettingsBtnAppearance(); saveSettingsBtnPosition(); scheduleSettingsSnapBack(); } // 设置按钮的点击处理 function onSettingsBtnTap(e) { if (settingsWasDragged) return; e.stopPropagation(); e.preventDefault(); openSettingsModal(); scheduleSettingsSnapBack(); } function setupSettingsButton() { createSettingsBtnElements(); loadSettingsBtnPosition(); // 默认窄条模式 settingsState = 'tab'; // 绑定事件 settingsBtn.addEventListener('touchstart', startSettingsDrag, {passive: false}); settingsBtn.addEventListener('mousedown', startSettingsDrag); settingsBtn.addEventListener('click', function(e) { // 如果 endSettingsDrag 已处理过(触摸单击),跳过 if (settingsTapJustOpened) return; if (settingsWasDragged) { settingsWasDragged = false; return; } onSettingsBtnTap(e); }); settingsBtnTab.addEventListener('click', expandSettingsFromTab); settingsBtnTab.addEventListener('touchstart', function(e) { e.preventDefault(); // 不启动拖动,直接展开 }, {passive: false}); settingsBtnTab.addEventListener('touchend', function(e) { e.preventDefault(); expandSettingsFromTab(e); }); } function injectUI() { if (!document.body) { setTimeout(injectUI, 100); return; } applyFloatBtn(); if (!settingsBtn) setupSettingsButton(); if (settingsBtn && !settingsBtn.parentNode) document.body.appendChild(settingsBtn); if (settingsBtnTab && !settingsBtnTab.parentNode) document.body.appendChild(settingsBtnTab); updateSettingsBtnAppearance(); } injectUI(); // ==================== Toast 提示 ==================== function showToast(msg, type) { var colors = { success: '#16a34a', warn: '#d97706', error: '#dc2626', info: '#2563eb' }; var toast = document.createElement('div'); toast.textContent = msg; toast.style.cssText = 'position:fixed;top:16px;left:50%;transform:translateX(-50%);z-index:100002;' + 'background:' + (colors[type] || colors.info) + ';color:#fff;' + 'padding:8px 20px;border-radius:20px;font-size:14px;' + 'font-family:system-ui,sans-serif;white-space:nowrap;' + 'box-shadow:0 4px 12px rgba(0,0,0,.25);' + 'opacity:0;transition:opacity .3s;pointer-events:none;'; if (!document.body) return; document.body.appendChild(toast); if (window.requestAnimationFrame) { window.requestAnimationFrame(function() { toast.style.opacity = '1'; }); } else { toast.style.opacity = '1'; } setTimeout(function() { toast.style.opacity = '0'; setTimeout(function() { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 300); }, config.toastDuration || 2500); } // ==================== 三指长按翻译(稳健版·防抖触控状态机) ==================== // 参考沉浸式翻译脚本的事件队列钩子机制: // 1. 使用 activePoints (Object) 按 identifier 累积触控点,解决手指分批到达问题 // 2. 仅在恰好三指时启动定时器,四指以上直接打断 // 3. touchmove 中使用 50px 宽松阈值(分轴判定),容忍光滑屏幕微小滑动 // 4. touchcancel 妥善处理,只清理触控点而不强行中断脚本 var touchStatus = { timer: null, activePoints: {}, // { [identifier]: {x, y} } triggered: false }; function registerThreeFingerLongPress(target) { if (!target || !target.addEventListener) return; // 核心逻辑:使用事件委托,降低对单一目标的依赖 target.addEventListener('touchstart', function(e) { if (!config.enableThreeFingerLongPress) return; // 记录当前所有触控点 for (var i = 0; i < e.changedTouches.length; i++) { var t = e.changedTouches[i]; touchStatus.activePoints[t.identifier] = { x: t.clientX, y: t.clientY }; } var count = Object.keys(touchStatus.activePoints).length; // 仅在刚好三指时开启计时 if (count === 3 && !touchStatus.triggered) { touchStatus.timer = setTimeout(function() { touchStatus.triggered = true; doTranslate(); }, config.threeFingerLongPressMs || 500); } else if (count > 3) { // 四指以上直接失效 clearTimeout(touchStatus.timer); } }, { passive: true }); var clearTouch = function(e) { for (var i = 0; i < e.changedTouches.length; i++) { delete touchStatus.activePoints[e.changedTouches[i].identifier]; } if (Object.keys(touchStatus.activePoints).length < 3) { clearTimeout(touchStatus.timer); touchStatus.triggered = false; } }; target.addEventListener('touchend', clearTouch, { passive: true }); target.addEventListener('touchcancel', clearTouch, { passive: true }); // 移动检测,增加容错,避免抖动导致长按取消 target.addEventListener('touchmove', function(e) { if (!touchStatus.timer) return; for (var i = 0; i < e.changedTouches.length; i++) { var t = e.changedTouches[i]; var start = touchStatus.activePoints[t.identifier]; if (start && (Math.abs(t.clientX - start.x) > 50 || Math.abs(t.clientY - start.y) > 50)) { clearTimeout(touchStatus.timer); touchStatus.timer = null; } } }, { passive: true }); } registerThreeFingerLongPress(document); // ==================== 自动翻译:检测非目标语言页面 ★ 新功能 ★ ==================== function getPageLang() { // 方法1: var htmlEl = document.documentElement; var lang = (htmlEl.getAttribute('lang') || htmlEl.getAttribute('xml:lang') || '').toLowerCase(); if (lang) { var dash = lang.indexOf('-'); return dash > 0 ? lang.slice(0, dash) : lang; } // 方法2:meta 标签 var meta = document.querySelector('meta[name="language"], meta[http-equiv="content-language"]'); if (meta) { var ml = (meta.getAttribute('content') || '').toLowerCase(); if (ml) { var d = ml.indexOf('-'); return d > 0 ? ml.slice(0, d) : ml; } } // 方法3:文字特征分析 var sample = ''; try { sample = (document.body ? document.body.innerText : '').slice(0, 600); } catch(e) {} sample = sample.replace(/\s/g, ''); if (!sample || sample.length < 20) return 'en'; var cjk = (sample.match(/[\u4e00-\u9fff\u3400-\u4dbf\uF900-\uFAFF]/g) || []).length; var jp = (sample.match(/[\u3040-\u309f\u30a0-\u30ff]/g) || []).length; var ko = (sample.match(/[\uac00-\ud7af\u1100-\u11ff]/g) || []).length; var total = sample.length; if (cjk / total > 0.2) return 'zh'; if (jp / total > 0.15) return 'ja'; if (ko / total > 0.15) return 'ko'; return 'en'; } function isTargetLang(pageLang) { var target = (config.targetLang || 'Simplified Chinese').toLowerCase(); if (target.indexOf('chinese') !== -1 || target.indexOf('中文') !== -1 || target.indexOf('简体') !== -1) { return pageLang === 'zh'; } if (target.indexOf('japanese') !== -1 || target.indexOf('日文') !== -1 || target.indexOf('日语') !== -1) { return pageLang === 'ja'; } if (target.indexOf('korean') !== -1 || target.indexOf('韩文') !== -1 || target.indexOf('韩语') !== -1) { return pageLang === 'ko'; } if (target.indexOf('english') !== -1 || target.indexOf('英文') !== -1 || target.indexOf('英语') !== -1) { return pageLang === 'en'; } // 默认:目标是中文,非中文才翻译 return pageLang === 'zh'; } function checkAutoTranslate() { if (!config.autoTranslate) return; setTimeout(function() { if (translating || hasTranslated) return; var pageLang = getPageLang(); if (!isTargetLang(pageLang)) { showToast('检测到非目标语言 (' + pageLang + '),自动翻译中...', 'info'); doTranslate(); } }, 1200); } // ==================== 动态内容监听 ==================== var mutationObserver = null; var mutationDebounceTimer = null; function startMutationObserver() { if (mutationObserver || !window.MutationObserver) return; mutationObserver = new MutationObserver(function(mutations) { var addedNodes = 0; for (var m = 0; m < mutations.length; m++) { addedNodes += mutations[m].addedNodes.length; } if (addedNodes > 3 && hasTranslated) { if (mutationDebounceTimer) clearTimeout(mutationDebounceTimer); mutationDebounceTimer = setTimeout(function() { showToast('检测到新内容加载,可再次点击翻译按钮', 'info'); }, 1500); } }); mutationObserver.observe(document.documentElement, { childList: true, subtree: true }); } if (document.body) { startMutationObserver(); } else { document.addEventListener('DOMContentLoaded', startMutationObserver); } // ==================== 设置弹窗(更新:作用域 + 七牛云介绍) ==================== var modalOverlay = null; function openSettingsModal() { if (modalOverlay) { closeSettingsModal(); return; } var cfg = loadConfig(); // 展开设置按钮以便操作 if (settingsState === 'tab') { settingsState = 'expanded'; updateSettingsBtnAppearance(); } if (settingsSnapTimeout) { clearTimeout(settingsSnapTimeout); settingsSnapTimeout = null; } modalOverlay = document.createElement('div'); modalOverlay.id = 'llm-translator-modal'; modalOverlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:999999;' + 'background:rgba(0,0,0,0.5);display:flex;align-items:flex-start;justify-content:center;' + 'overflow-y:auto;padding:12px 8px 40px;box-sizing:border-box;'; var panel = document.createElement('div'); panel.style.cssText = 'background:#f3f4f6;border-radius:16px;padding:16px;width:100%;max-width:480px;' + 'font-family:system-ui,-apple-system,sans-serif;color:#1f2937;box-sizing:border-box;' + 'box-shadow:0 8px 32px rgba(0,0,0,0.3);'; panel.innerHTML = buildSettingsHTML(cfg); modalOverlay.appendChild(panel); modalOverlay.addEventListener('click', function(e) { if (e.target === modalOverlay) closeSettingsModal(); }); document.body.appendChild(modalOverlay); bindSettingsEvents(cfg); } function closeSettingsModal() { if (modalOverlay && modalOverlay.parentNode) { modalOverlay.parentNode.removeChild(modalOverlay); } modalOverlay = null; // 关闭后恢复窄条 settingsState = 'tab'; updateSettingsBtnAppearance(); scheduleSettingsSnapBack(); } function buildSettingsHTML(cfg) { var engineLLM = cfg.engine === 'llm' ? ' selected' : ''; var engineMyMemory = cfg.engine === 'mymemory' ? ' selected' : ''; var engineBaidu = cfg.engine === 'baidu' ? ' selected' : ''; var engineCustom = cfg.engine === 'custom' ? ' selected' : ''; var modeReplace = cfg.translateMode === 'replace' ? ' selected' : ''; var modeBilingual = cfg.translateMode === 'bilingual' ? ' selected' : ''; var scopeGlobal = cfg.settingsScope === 'global' ? ' selected' : ''; var scopePerPage = cfg.settingsScope === 'perPage' ? ' selected' : ''; var scopeWhitelist = cfg.settingsScope === 'whitelist' ? ' selected' : ''; var scopeBlacklist = cfg.settingsScope === 'blacklist' ? ' selected' : ''; var threeFingerChecked = cfg.enableThreeFingerLongPress ? 'checked' : ''; var floatBtnChecked = cfg.enableFloatBtn ? 'checked' : ''; var autoTransChecked = cfg.autoTranslate ? 'checked' : ''; var llmSectionDisplay = (cfg.engine === 'llm' || !cfg.engine) ? 'block' : 'none'; var mmSectionDisplay = cfg.engine === 'mymemory' ? 'block' : 'none'; var baiduSectionDisplay = cfg.engine === 'baidu' ? 'block' : 'none'; var customSectionDisplay = cfg.engine === 'custom' ? 'block' : 'none'; var whitelistDisplay = cfg.settingsScope === 'whitelist' ? 'block' : 'none'; var blacklistDisplay = cfg.settingsScope === 'blacklist' ? 'block' : 'none'; var whitelistVal = (cfg.whitelistPages || []).join(', '); var blacklistVal = (cfg.blacklistPages || []).join(', '); return '' + '
仅白名单中的域名拥有独立设置,其余共用全局设置。
黑名单中的域名不继承全局设置,须单独配置。
推荐:七牛云免费 API,新用户免费发放 200万 tokens 额度,含多款免费模型(glm-4.5-air-free 等)。去 七牛云官网 或微信小程序"七牛云"免费申请 Key。
' + '' + '' + '' + '' + '' + '完全免费,无需注册,支持 ISO 639-1 语言代码。每日约1000字符限额。
' + '申请地址:https://api.fanyi.baidu.com
' + '' + '' + '开启后,打开非本语言网页时将自动触发翻译。默认关闭。
' + '设置按钮(⚙️)可拖动到屏幕边缘,松开后自动吸附为窄条。
' + '