// ==UserScript== // @name Auto Translate Pro (TWP Core) // @namespace auto-translate-twp // @version 6.0.0 // @description 集成 TWP 核心谷歌接口:极速翻译、完美双语、智能鉴权、移动端适配。 // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @connect translate.googleapis.com // @connect translate-pa.googleapis.com // @connect api-edge.cognitive.microsofttranslator.com // @connect edge.microsoft.com // @connect transmart.qq.com // @run-at document-end // @downloadURL none // ==/UserScript== (async () => { 'use strict'; // 1. 安全守卫 if (document.contentType === 'application/xml') return; /* ═══════════════════════════════════════════════════════════════ * CORE CONFIGURATION & STATE * ═══════════════════════════════════════════════════════════════ */ const sysLang = (navigator.language || 'zh').split('-')[0]; const cfg = { eng: await GM_getValue('at_eng', 'google_twp'), // 默认 TWP 谷歌 lng: await GM_getValue('at_lng', sysLang), auto: await GM_getValue('at_auto', true), bi: await GM_getValue('at_bi', true), // 默认开启双语 hl: await GM_getValue('at_hl', '#4a9eff') }; const setCfg = (k, v) => { cfg[k] = v; GM_setValue('at_' + k, v); }; let isWorking = false; const cache = new Map(); /* ═══════════════════════════════════════════════════════════════ * TWP GOOGLE CORE (移植自 Traduzir-paginas-web) * ═══════════════════════════════════════════════════════════════ */ const fetchAPI = (opts) => new Promise((res, rej) => GM_xmlhttpRequest({ ...opts, onload: res, onerror: rej, ontimeout: rej })); const GoogleTWP = { auth: null, authExp: 0, // TWP 的备用密钥 (Base64解码后) fallbackKey: 'AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520', async getAuth() { if (this.auth && Date.now() < this.authExp) return this.auth; // 尝试动态获取 Key (模拟 TWP findAuth 逻辑) try { // 使用一个较新的 translate_http JS 文件路径来提取 Key const r = await fetchAPI({ method: 'GET', url: 'https://translate.googleapis.com/_/translate_http/_/js/k=translate_http.tr.en_US.YusFYy3P_ro.O/am=AAg/d=1/exm=el_conf/ed=1/rs=AN8SPfq1Hb8iJRleQqQc8zhdzXmF9E56eQ/m=el_main' }); const match = r.responseText.match(/['"]x-goog-api-key['"]\s*:\s*['"](\w{39})['"]/i); if (match && match[1]) { this.auth = match[1]; this.authExp = Date.now() + 3600 * 1000; // 1小时缓存 return this.auth; } } catch (e) { console.warn('TWP Auth fetch failed, using fallback.'); } this.auth = this.fallbackKey; this.authExp = Date.now() + 3600 * 1000; return this.auth; }, async translate(texts, targetLang) { const key = await this.getAuth(); const target = targetLang === 'zh' ? 'zh-CN' : targetLang; // 使用 translate-pa 接口 (TWP 核心) // 这是一个非官方但高性能的 API,支持 JSON 格式的请求体 try { const r = await fetchAPI({ method: 'POST', url: 'https://translate-pa.googleapis.com/v1/translateHtml', headers: { 'Content-Type': 'application/application/json+protobuf', 'X-goog-api-key': key }, // TWP 的请求体构造逻辑: [[text_array, source, target], "te"] data: JSON.stringify([ [texts, 'auto', target], 'te' ]) }); if (r.status === 200) { const json = JSON.parse(r.responseText); // 解析响应: json[0] 包含翻译后的文本数组 if (json && json[0] && Array.isArray(json[0])) { return json[0]; } } } catch (e) { console.error('TWP API Error:', e); } // Fallback: 如果高级接口失败,回退到标准 GTX 接口 (兼容性最好) console.log('Fallback to GTX'); return Promise.all(texts.map(async t => { try { const r = await fetchAPI({ method: 'GET', url: `https://translate.googleapis.com/translate_a/single?client=gtx&dt=t&sl=auto&tl=${target}&q=${encodeURIComponent(t)}` }); return JSON.parse(r.responseText)[0].map(s => s[0]).join(''); } catch(e) { return null; } })); } }; /* ═══════════════════════════════════════════════════════════════ * OTHER ENGINES (Backup) * ═══════════════════════════════════════════════════════════════ */ const Engines = { google_twp: GoogleTWP, microsoft: { tk: null, exp: 0, async translate(texts, to) { if (!this.tk || Date.now() > this.exp) { const r = await fetchAPI({ method: 'GET', url: 'https://edge.microsoft.com/translate/auth' }); this.tk = r.responseText; this.exp = Date.now() + 480000; } const toLang = to === 'zh' ? 'zh-Hans' : to; const out = new Array(texts.length).fill(null); // 微软 Batch 限制 25 for (let i=0; i ({ Text }))) }); JSON.parse(r.responseText).forEach((res, j) => out[i+j] = res.translations[0].text); } catch(e){} } return out; } } }; /* ═══════════════════════════════════════════════════════════════ * ISOMORPHIC DOM & BILINGUAL LOGIC * ═══════════════════════════════════════════════════════════════ */ const SKIP = new Set(['SCRIPT', 'STYLE', 'CODE', 'PRE', 'SVG', 'NOSCRIPT', 'TEXTAREA', 'INPUT']); const isTarget = (t) => { if (t.length < 2 || /^[\d\s.,!?@#$%^&*()]+$/.test(t)) return true; if (cfg.lng === 'zh') return (t.match(/[\u4e00-\u9fa5]/g)?.length || 0) > t.length * 0.3; if (cfg.lng === 'en') return /^[\x20-\x7E\s]+$/.test(t); return false; }; // 1. 高速 DOM 遍历 const getNodes = (root) => { const nodes = []; const walker = document.createTreeWalker(root, 4, { acceptNode(n) { const p = n.parentElement; if (!p || p.closest('.at-w') || SKIP.has(p.tagName) || p.isContentEditable || p.closest('.at-ui')) return 2; if (!n.nodeValue.trim() || isTarget(n.nodeValue.trim())) return 2; return 1; } }); while (walker.nextNode()) nodes.push(walker.currentNode); return nodes; }; // 2. 双语结构构造 (Isomorphic Wrapper) // 结构: 原文译文 const applyTrans = (node, ori, dst) => { if (!node.parentElement || node.parentElement.closest('.at-w')) return; const w = document.createElement('span'); w.className = 'at-w'; w.dataset.o = ori; // 备份原文 // 使用 CSS 变量控制间隔,确保谷歌搜索等紧凑布局正常显示 w.innerHTML = `${ori}${dst}`; node.replaceWith(w); }; // 3. 核心流水线 const run = async () => { if (isWorking) return; isWorking = true; document.getElementById('at-btn')?.classList.add('at-busy'); const nodes = getNodes(document.body); if (nodes.length) { const texts = nodes.map(n => n.nodeValue.trim()); const unC = [], unCIdx = [], res = new Array(texts.length).fill(null); // 读缓存 texts.forEach((t, i) => { const c = cache.get(t); if (c) res[i] = c; else { unC.push(t); unCIdx.push(i); } }); // 请求 API if (unC.length) { const apiRes = await Engines[cfg.eng].translate(unC, cfg.lng); if (apiRes) { apiRes.forEach((r, i) => { if (r) { res[unCIdx[i]] = r; if (cache.size > 5000) cache.delete(cache.keys().next().value); cache.set(unC[i], r); } }); } } // 写 DOM nodes.forEach((n, i) => { if (res[i] && res[i] !== texts[i]) applyTrans(n, texts[i], res[i]); }); } isWorking = false; document.getElementById('at-btn')?.classList.remove('at-busy'); }; const restore = () => document.querySelectorAll('.at-w').forEach(w => w.replaceWith(document.createTextNode(w.dataset.o))); const toggleBi = () => document.body.classList.toggle('at-bi-mode', cfg.bi); /* ═══════════════════════════════════════════════════════════════ * DYNAMIC UI & STYLES * ═══════════════════════════════════════════════════════════════ */ GM_addStyle(` /* 核心同构样式:默认只显示译文 (替换模式) */ .at-w { display: inline; } .at-src { display: none; } .at-dst { color: ${cfg.hl}; } /* 双语模式:原文+译文 */ body.at-bi-mode .at-src { display: inline; opacity: 0.6; font-size: 0.9em; margin-right: 6px; /* 谷歌搜索标题优化: 避免双语换行破坏结构 */ vertical-align: baseline; } /* UI 组件 */ .at-ui { position: fixed; bottom: 20px; right: 20px; z-index: 999999; font-family: system-ui, sans-serif; } .at-btn { width: 44px; height: 44px; border-radius: 50%; background: rgba(0,0,0,0.6); color: #fff; border: none; cursor: pointer; backdrop-filter: blur(8px); transition: 0.2s; display:flex; align-items:center; justify-content:center; box-shadow: 0 4px 12px rgba(0,0,0,0.2); } .at-btn:hover { transform: scale(1.05); } .at-btn.at-on { background: #4a9eff; } .at-busy { animation: at-pulse 1s infinite; } @keyframes at-pulse { 0% { box-shadow: 0 0 0 0 rgba(74,158,255,0.6); } 100% { box-shadow: 0 0 0 10px rgba(74,158,255,0); } } .at-pnl { position: absolute; bottom: 56px; right: 0; width: 220px; background: rgba(255,255,255,0.95); backdrop-filter: blur(12px); border-radius: 12px; padding: 16px; box-shadow: 0 8px 24px rgba(0,0,0,0.15); opacity: 0; pointer-events: none; transform: translateY(10px); transition: 0.2s; color: #333; } .at-pnl.opn { opacity: 1; pointer-events: auto; transform: translateY(0); } .at-pnl select { width: 100%; padding: 6px; margin: 4px 0 12px; border-radius: 6px; border: 1px solid #ddd; outline: none; font-size: 13px; } .at-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-size: 13px; font-weight: 600; } .at-sw { width: 36px; height: 20px; background: #ccc; border-radius: 10px; position: relative; cursor: pointer; transition: 0.2s; } .at-sw.on { background: #4a9eff; } .at-sw::after { content: ''; position: absolute; width: 16px; height: 16px; background: #fff; border-radius: 50%; top: 2px; left: 2px; transition: 0.2s; } .at-sw.on::after { left: 18px; } .at-acts { display: flex; gap: 8px; } .at-act { flex: 1; padding: 8px; border: none; border-radius: 6px; font-weight: bold; cursor: pointer; font-size: 13px; } @media (prefers-color-scheme: dark) { .at-pnl { background: rgba(30,30,30,0.95); color: #eee; } .at-pnl select { background: #444; color:#eee; border-color:#555;} } `); const ui = document.createElement('div'); ui.className = 'at-ui'; ui.innerHTML = `
ENGINE TARGET
双语对照
`; document.body.appendChild(ui); toggleBi(); /* ═══════════════════════════════════════════════════════════════ * EVENTS * ═══════════════════════════════════════════════════════════════ */ const $ = id => document.getElementById(id); $('at-eng').value = cfg.eng; $('at-lng').value = cfg.lng; $('at-btn').onclick = (e) => { e.stopPropagation(); $('at-pnl').classList.toggle('opn'); }; document.onclick = (e) => { if (!ui.contains(e.target)) $('at-pnl').classList.remove('opn'); }; $('at-eng').onchange = e => setCfg('eng', e.target.value); $('at-lng').onchange = e => setCfg('lng', e.target.value); $('at-bi').onclick = () => { setCfg('bi', !cfg.bi); $('at-bi').classList.toggle('on', cfg.bi); toggleBi(); // 纯 CSS 切换,0 延迟 }; $('at-run').onclick = () => { $('at-pnl').classList.remove('opn'); setCfg('auto', true); $('at-btn').classList.add('at-on'); restore(); run(); }; $('at-rst').onclick = () => { $('at-pnl').classList.remove('opn'); setCfg('auto', false); $('at-btn').classList.remove('at-on'); restore(); }; // 智能防抖监听 let timer; new MutationObserver(muts => { if (!cfg.auto || isWorking) return; clearTimeout(timer); timer = setTimeout(() => { if (muts.some(m => Array.from(m.addedNodes).some(n => n.nodeType === 1))) run(); }, 600); }).observe(document.body, { childList: true, subtree: true }); // 启动 if (cfg.auto && !isTarget(document.title)) setTimeout(run, 500); })();