// ==UserScript== // @name Auto Translate Pro (UA Spoofing Edition) // @namespace auto-translate-pro-ua // @version 4.0.0 // @description 高性能网页翻译:独立环境UA伪装、双语无缝替换、多引擎智能调度 // @match *://*/* // @exclude *://translate.google.*/* // @exclude *://fanyi.baidu.com/* // @exclude *://transmart.qq.com/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @connect translate.googleapis.com // @connect api-edge.cognitive.microsofttranslator.com // @connect edge.microsoft.com // @connect transmart.qq.com // @connect www2.deepl.com // @connect translate.alibaba.com // @connect m.youdao.com // @connect fanyi.baidu.com // @run-at document-end // @downloadURL none // ==/UserScript== (async () => { 'use strict'; if (document.contentType === 'application/xml') return; let cfg = { engine: await GM_getValue('at_engine', 'microsoft'), lang: await GM_getValue('at_lang', (navigator.language || 'zh-CN').split('-')[0].toLowerCase()), auto: await GM_getValue('at_auto', true), bilingual: await GM_getValue('at_bilingual', false), hlColor: await GM_getValue('at_hlColor', '#4a9eff') }; const saveCfg = (k, v) => { cfg[k] = v; GM_setValue('at_' + k, v); }; let state = { busy: false, count: 0 }; const Cache = new Map(); const getCache = t => Cache.get(t); const setCache = (t, r) => { if (Cache.size >= 5000) Cache.delete(Cache.keys().next().value); Cache.set(t, r); }; // 网络请求基类:动态注入引擎专属 UA const fetchAPI = (opts, customUA) => new Promise((resolve, reject) => { if (customUA) { opts.headers = opts.headers || {}; opts.headers['User-Agent'] = customUA; } GM_xmlhttpRequest({ ...opts, timeout: 15000, onload: resolve, onerror: reject, ontimeout: reject }); }); const uuid = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); // 引擎配置与独立 UA 伪装 const LC = { zh:'zh', en:'en', ja:'ja', ko:'ko', fr:'fr', de:'de', es:'es', ru:'ru', it:'it' }; const Engines = { microsoft: { ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.236.72', token: null, exp: 0, async auth() { if (this.token && Date.now() < this.exp) return; const r = await fetchAPI({ method: 'GET', url: 'https://edge.microsoft.com/translate/auth' }, this.ua); this.token = r.responseText; this.exp = Date.now() + 480000; }, async batch(texts, lang) { await this.auth(); const to = lang === 'zh' ? 'zh-Hans' : (LC[lang] || lang); const out = new Array(texts.length).fill(null); for (let i = 0; i < texts.length; i += 25) { try { const chunk = texts.slice(i, i + 25); const r = await fetchAPI({ method: 'POST', url: `https://api-edge.cognitive.microsofttranslator.com/translate?from=&to=${to}&api-version=3.0`, headers: { 'authorization': `Bearer ${this.token}`, 'Content-Type': 'application/json' }, data: JSON.stringify(chunk.map(Text => ({ Text }))), }, this.ua); if (r.status === 200) JSON.parse(r.responseText).forEach((res, j) => { out[i + j] = res.translations[0].text; }); } catch (e) {} } return out; } }, google: { ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', async translate(text, lang) { const r = await fetchAPI({ method: 'GET', url: `https://translate.googleapis.com/translate_a/single?client=gtx&dt=t&sl=auto&tl=${LC[lang]||lang}&q=${encodeURIComponent(text)}` }, this.ua); return JSON.parse(r.responseText)[0].map(s => s[0]).join(''); } }, tencent: { ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.289 Safari/537.36 QQBrowser/11.1.0.400', ck: null, async translate(text, lang) { this.ck = this.ck || `browser-chrome-120.0-os-${uuid()}-${Date.now()}`; const r = await fetchAPI({ method: 'POST', url: 'https://transmart.qq.com/api/imt', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ header: { fn: 'auto_translation', client_key: this.ck }, type: 'plain', model_category: 'normal', text_domain: 'general', source: { lang: 'auto', text_list: [text] }, target: { lang: LC[lang] || lang } }) }, this.ua); return JSON.parse(r.responseText).auto_translation[0]; } }, deepl: { ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', async translate(text, lang) { const id = 1e4 * Math.round(1e4 * Math.random()); const body = JSON.stringify({ jsonrpc: '2.0', method: 'LMT_handle_jobs', id, params: { jobs: [{ kind: 'default', sentences: [{ text, id: 0, prefix: '' }], raw_en_context_before: [], raw_en_context_after: [], preferred_num_beams: 4 }], lang: { preference: { weight: {}, default: 'default' }, source_lang_user_selected: 'auto', target_lang: lang.toUpperCase() }, commonJobParams: { mode: 'translate', browserType: 1 }, timestamp: Date.now() + 1000 } }).replace('hod":"', ((id + 3) % 13 === 0 || (id + 5) % 29 === 0) ? 'hod" : "' : 'hod": "'); const r = await fetchAPI({ method: 'POST', url: 'https://www2.deepl.com/jsonrpc?method=LMT_handle_jobs', headers: { 'Content-Type': 'application/json', 'Origin': 'https://www.deepl.com' }, data: body }, this.ua); return JSON.parse(r.responseText).result.translations[0].beams[0].sentences[0].text; } }, alibaba: { ua: 'Mozilla/5.0 (Linux; Android 11; SM-G9810) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/89.0.4389.105 Mobile Safari/537.36 AliApp(TB/10.2.13)', token: null, async translate(text, lang) { if (!this.token) { const authR = await fetchAPI({ method: 'GET', url: 'https://translate.alibaba.com/api/translate/csrftoken' }, this.ua); this.token = JSON.parse(authR.responseText).token; } const b = '----WebKitFormBoundary' + uuid().slice(0, 16); const r = await fetchAPI({ method: 'POST', url: 'https://translate.alibaba.com/api/translate/text', headers: { 'content-type': `multipart/form-data; boundary=${b}`, 'origin': 'https://translate.alibaba.com' }, data: `--${b}\r\nContent-Disposition: form-data; name="srcLang"\r\n\r\nauto\r\n--${b}\r\nContent-Disposition: form-data; name="tgtLang"\r\n\r\n${LC[lang]||lang}\r\n--${b}\r\nContent-Disposition: form-data; name="domain"\r\n\r\ngeneral\r\n--${b}\r\nContent-Disposition: form-data; name="query"\r\n\r\n${text}\r\n--${b}\r\nContent-Disposition: form-data; name="_csrf"\r\n\r\n${this.token}\r\n--${b}--\r\n` }, this.ua); return JSON.parse(r.responseText).data.translateText; } }, baidu: { ua: 'Mozilla/5.0 (Linux; Android 12; HarmonyOS) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Mobile Safari/537.36 baiduboxapp/13.1.0.10', token: null, gtk: null, async translate(text, lang) { if (!this.token || !this.gtk) { const rAuth = await fetchAPI({ method: 'GET', url: 'https://fanyi.baidu.com' }, this.ua); this.token = /token:\s*['"]([^'"]+)/.exec(rAuth.responseText)?.[1]; this.gtk = /['"](\d{6}\.\d{9})['"]/.exec(rAuth.responseText)?.[1]; } const sign = (t, gtk) => { let [f, m] = gtk.split('.').map(Number); const S = (a, b) => { for(let c=0; c='a'?d.charCodeAt(0)-87:+d; d=b.charAt(c+1)==='+'?a>>>d:a<>6|192, c&63|128); else bytes.push(c>>12|224, c>>6&63|128, c&63|128); } for(let b of bytes) v = S(v+b, '+-a^+6'); v = S(v, '+-3^+b+-f') ^ m; if(v < 0) v = (v & 2147483647) + 2147483648; return `${v %= 1e6}.${v^f}`; }; const r = await fetchAPI({ method: 'POST', url: 'https://fanyi.baidu.com/basetrans', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': 'https://fanyi.baidu.com/' }, data: `query=${encodeURIComponent(text)}&from=auto&to=${LC[lang]||lang}&token=${this.token}&sign=${sign(text, this.gtk)}` }, this.ua); return JSON.parse(r.responseText).trans[0].dst; } } }; const isTarget = (txt) => { if (txt.length < 2 || /^[\d\s.,!?]+$/.test(txt)) return true; if (cfg.lang === 'zh') return (txt.match(/[\u4e00-\u9fa5]/g)?.length || 0) > txt.length * 0.3; if (cfg.lang === 'en') return /^[\x20-\x7E\s]+$/.test(txt); return false; }; const getTexts = (root) => { const nodes = [], skip = new Set(['SCRIPT','STYLE','CODE','PRE','SVG','NOSCRIPT','IFRAME']); const walker = document.createTreeWalker(root, 4, { acceptNode(n) { const p = n.parentElement; if (!p || p.hasAttribute('d-at') || skip.has(p.tagName) || /translate-ui|katex|hljs|prism/i.test(p.className)) return 2; if (!n.nodeValue.trim() || isTarget(n.nodeValue.trim())) return 2; return 1; } }); while (walker.nextNode()) nodes.push(walker.currentNode); return nodes; }; const applyDOM = (n, ori, trans) => { if (!n.parentElement || n.parentElement.hasAttribute('d-at')) return; const w = document.createElement('span'); w.setAttribute('d-at', '1'); w.setAttribute('d-ori', ori); if (cfg.bilingual) { w.className = 'at-bi-wrap'; w.innerHTML = `${ori}${trans}`; } else { w.textContent = trans; } n.replaceWith(w); state.count++; }; const trStart = async () => { if (state.busy) return; state.busy = true; uiUpdate(); const nodes = getTexts(document.body); if (!nodes.length) { state.busy = false; uiUpdate(); return; } const engine = Engines[cfg.engine], txts = nodes.map(n => n.nodeValue.trim()); let res = new Array(txts.length).fill(null), unCIdx = [], unCTxt = []; txts.forEach((t, i) => { let c = getCache(t); if (c) res[i] = c; else { unCIdx.push(i); unCTxt.push(t); } }); if (unCTxt.length > 0) { if (engine.batch) { const br = await engine.batch(unCTxt, cfg.lang); br.forEach((r, i) => { if (r) { res[unCIdx[i]] = r; setCache(unCTxt[i], r); } }); } else { for (let i = 0; i < unCTxt.length; i += 5) { await Promise.allSettled(unCTxt.slice(i, i+5).map(async (t, j) => { try { let r = await engine.translate(t, cfg.lang); if (r) { res[unCIdx[i+j]] = r; setCache(t, r); } } catch(e){} })); } } } nodes.forEach((n, i) => { if (res[i] && res[i] !== txts[i]) applyDOM(n, txts[i], res[i]); }); state.busy = false; uiUpdate(); }; const trRestore = () => { document.querySelectorAll('[d-at="1"]').forEach(w => { let ori = w.getAttribute('d-ori'); if (ori) w.replaceWith(document.createTextNode(ori)); }); state.count = 0; uiUpdate(); }; let tMut; new MutationObserver(m => { if (!cfg.auto || state.busy) return; clearTimeout(tMut); tMut = setTimeout(() => { if (m.some(x => x.addedNodes.length > 0)) trStart(); }, 600); }).observe(document.body, { childList: true, subtree: true }); GM_addStyle(` .at-bi-wrap { display: inline; } .at-ori { display: inline; opacity: 0.5; margin-right: 6px; font-size: 0.95em; } .at-tra { display: inline; color: var(--at-hl, #4a9eff); font-weight: 500; } .at-ui { position: fixed; bottom: 20px; right: 20px; z-index: 2147483647; font-family: system-ui; } .at-fab { width: 44px; height: 44px; border-radius: 50%; background: rgba(0,0,0,0.6); color: #fff; cursor: pointer; border: none; backdrop-filter: blur(8px); transition: 0.2s; display:flex; align-items:center; justify-content:center;} .at-fab.act { background: #4a9eff; } .at-fab.bsy { animation: atp 1.5s infinite; } @keyframes atp { 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: 200px; background: rgba(255,255,255,0.95); backdrop-filter: blur(10px); border-radius: 12px; padding: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.15); opacity: 0; pointer-events: none; transform: translateY(10px); transition: 0.2s; } .at-pnl.opn { opacity: 1; pointer-events: auto; transform: translateY(0); } .at-pnl select { width: 100%; padding: 6px; margin-top: 4px; margin-bottom: 10px; border-radius: 6px; border: 1px solid #ddd; outline: none; } .at-tog { width: 40px; height: 22px; background: #ccc; border-radius: 12px; position: relative; cursor: pointer; transition: 0.2s; } .at-tog.on { background: #4a9eff; } .at-tog::after { content: ''; position: absolute; width: 18px; height: 18px; background: #fff; border-radius: 50%; top: 2px; left: 2px; transition: 0.2s; } .at-tog.on::after { left: 20px; } .at-btn { width: 48%; padding: 8px 0; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; } @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;} } `); document.documentElement.style.setProperty('--at-hl', cfg.hlColor); const ui = document.createElement('div'); ui.className = 'at-ui'; ui.innerHTML = `
引擎 语言
双语对照
`; document.body.appendChild(ui); const el = id => document.getElementById(id); el('atEng').value = cfg.engine; el('atLng').value = cfg.lang; const uiUpdate = () => { el('atFab').classList.toggle('act', cfg.auto); el('atFab').classList.toggle('bsy', state.busy); }; el('atFab').onclick = e => { e.stopPropagation(); el('atPnl').classList.toggle('opn'); }; document.addEventListener('click', e => { if (!ui.contains(e.target)) el('atPnl').classList.remove('opn'); }); el('atEng').onchange = e => saveCfg('engine', e.target.value); el('atLng').onchange = e => saveCfg('lang', e.target.value); el('atBi').onclick = () => { saveCfg('bilingual', !cfg.bilingual); el('atBi').classList.toggle('on', cfg.bilingual); if (state.count > 0) { trRestore(); trStart(); } }; el('atTra').onclick = () => { el('atPnl').classList.remove('opn'); saveCfg('auto', true); trRestore(); trStart(); }; el('atRst').onclick = () => { el('atPnl').classList.remove('opn'); saveCfg('auto', false); trRestore(); }; if (cfg.auto && !isTarget(document.title)) setTimeout(trStart, 800); })();