// ==UserScript== // @name X(推特)自动启用Grok双语翻译 // @version 2.2 // @description 自动调用Grok翻译推文、引用推文和回复。 // @author Gemini // @namespace http://tampermonkey.net/ // @match https://x.com/* // @match https://twitter.com/* // @grant none // @license MIT // @run-at document-start // @downloadURL https://update.greasyfork.icu/scripts/576023/X%EF%BC%88%E6%8E%A8%E7%89%B9%EF%BC%89%E8%87%AA%E5%8A%A8%E5%90%AF%E7%94%A8Grok%E5%8F%8C%E8%AF%AD%E7%BF%BB%E8%AF%91.user.js // @updateURL https://update.greasyfork.icu/scripts/576023/X%EF%BC%88%E6%8E%A8%E7%89%B9%EF%BC%89%E8%87%AA%E5%8A%A8%E5%90%AF%E7%94%A8Grok%E5%8F%8C%E8%AF%AD%E7%BF%BB%E8%AF%91.meta.js // ==/UserScript== (function () { 'use strict'; console.log("🚀 [X双语翻译] 脚本开始初始化 (v9.0 终极黑魔法版)..."); const API_URL = 'https://api.x.com/2/grok/translation.json'; const PUBLIC_BEARER = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA'; const state = { cache: new Map(), runtimeHeaders: {}, translationQueue: [], isProcessingQueue: false, headersReady: false }; // ================= 0. 注入全局 CSS 动画 ================= const style = document.createElement('style'); style.textContent = ` @keyframes x-translate-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .x-translate-loading-spinner { border: 2px solid rgba(29, 155, 240, 0.2); border-top-color: #1D9BF0; border-radius: 50%; width: 14px; height: 14px; animation: x-translate-spin 1s linear infinite; display: inline-block; vertical-align: middle; margin-right: 8px; } .x-auto-bilingual-wrapper { margin-top: 10px; padding: 10px 12px; border-left: 3px solid #1D9BF0; color: inherit; font-size: 15px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; background: rgba(136, 153, 166, 0.1); border-radius: 4px 8px 8px 4px; transition: all 0.3s ease; } `; document.head.appendChild(style); // ================= 1. 网络请求拦截 ================= function captureRuntimeHeaders(headers, url) { if (!headers) return; let normalized = {}; if (headers instanceof Headers) { normalized = Object.fromEntries(headers.entries()); } else if (Array.isArray(headers)) { normalized = Object.fromEntries(headers); } else { normalized = Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), String(v)])); } const interestingKeys = ['authorization', 'x-csrf-token', 'x-twitter-active-user', 'x-twitter-auth-type']; let capturedCount = 0; for (const key of interestingKeys) { if (normalized[key] && state.runtimeHeaders[key] !== normalized[key]) { state.runtimeHeaders[key] = normalized[key]; capturedCount++; } } if (capturedCount > 0 && state.runtimeHeaders['x-csrf-token']) { if(!state.headersReady) { state.headersReady = true; console.log("✅ [X双语翻译] 成功获取推特API凭证!"); } } } const originalFetch = window.fetch; window.fetch = async function (input, init) { try { const url = typeof input === 'string' ? input : input instanceof Request ? input.url : String(input); if (url.includes('api.x.com') || url.includes('twitter.com/i/api/')) { captureRuntimeHeaders(input instanceof Request ? input.headers : null, url); captureRuntimeHeaders(init?.headers, url); } } catch (e) {} return originalFetch.apply(this, arguments); }; const originalOpen = XMLHttpRequest.prototype.open; const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url) { this.__url = typeof url === 'string' ? url : String(url); this.__headers = {}; return originalOpen.apply(this, arguments); }; XMLHttpRequest.prototype.setRequestHeader = function (name, value) { if (this.__headers) this.__headers[String(name).toLowerCase()] = String(value); return originalSetRequestHeader.apply(this, arguments); }; XMLHttpRequest.prototype.send = function () { if (String(this.__url || '').includes('api.x.com') || String(this.__url || '').includes('twitter.com/i/api/')) { captureRuntimeHeaders(this.__headers, this.__url); } return originalSend.apply(this, arguments); }; // ================= 2. API 数据与请求处理器 ================= function extractTextFromResponse(rawText) { let fullTranslation = ""; const lines = rawText.trim().split('\n'); for (const line of lines) { if (!line.trim()) continue; try { const obj = JSON.parse(line); let chunk = ""; if (obj.result && obj.result.text) chunk = obj.result.text; else if (obj.translated_text) chunk = obj.translated_text; else if (obj.text) chunk = obj.text; if (chunk && typeof chunk === 'string') fullTranslation += chunk; } catch (e) {} } return fullTranslation.trim() || null; } function readCookie(name) { const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`)); return match ? decodeURIComponent(match[1]) : ''; } async function requestTranslation(tweetId) { if (state.cache.has(tweetId)) return state.cache.get(tweetId); const csrf = readCookie('ct0'); if (!csrf) throw new Error("等待授权"); const headers = { authorization: `Bearer ${decodeURIComponent(PUBLIC_BEARER)}`, 'content-type': 'text/plain;charset=UTF-8', 'x-csrf-token': csrf, 'x-twitter-active-user': 'yes', 'x-twitter-auth-type': readCookie('auth_token') ? 'OAuth2Session' : '', 'x-twitter-client-language': 'zh-cn' }; Object.assign(headers, state.runtimeHeaders); const response = await fetch(API_URL, { method: 'POST', mode: 'cors', credentials: 'include', headers: headers, body: JSON.stringify({ content_type: 'POST', id: tweetId, dst_lang: 'zh' }) }); const rawText = await response.text(); if (!response.ok) throw new Error(`HTTP ${response.status}`); const translatedText = extractTextFromResponse(rawText); if (!translatedText) throw new Error('提取译文为空'); state.cache.set(tweetId, translatedText); return translatedText; } // ================= 3. 黑魔法:通过 React Fiber 获取绝对精准的 Tweet ID ================= function getExactTweetId(container) { // 方案 1:React Fiber 内存穿透(99%命中率,可精准穿透无链接引用的卡片) let dom = container; while(dom && dom !== document.body) { const fiberKey = Object.keys(dom).find(k => k.startsWith('__reactFiber$')); if (fiberKey) { let fiber = dom[fiberKey]; let depth = 0; // 向上追溯组件树,寻找被注入的推文数据对象 while(fiber && depth < 20) { const p = fiber.memoizedProps; if (p && p.tweet && (p.tweet.rest_id || p.tweet.id_str)) { return p.tweet.rest_id || p.tweet.id_str; } fiber = fiber.return; depth++; } } dom = dom.parentElement; } // 方案 2:DOM 回退方案 (极少数情况) const parent = container.parentElement; if (parent) { const links = parent.querySelectorAll('a[href*="/status/"]'); for (let link of links) { const match = link.getAttribute('href').match(/\/status\/(\d+)/); if (match) return match[1]; } } const article = container.closest('article'); if (article) { const timeLink = article.querySelector('time')?.closest('a[href*="/status/"]'); if (timeLink) { const m = timeLink.getAttribute('href').match(/\/status\/(\d+)/); if (m) return m[1]; } } return null; } // ================= 4. 独立 UI 渲染逻辑 ================= function showLoadingUI(container) { if (container.nextElementSibling && container.nextElementSibling.classList.contains('x-auto-bilingual-wrapper')) return; const wrapper = document.createElement('div'); wrapper.className = 'x-auto-bilingual-wrapper'; wrapper.innerHTML = `
正在翻译中...
`; container.parentNode.insertBefore(wrapper, container.nextSibling); } function updateBilingualUI(container, translatedText) { let wrapper = container.nextElementSibling; if (!wrapper || !wrapper.classList.contains('x-auto-bilingual-wrapper')) { wrapper = document.createElement('div'); wrapper.className = 'x-auto-bilingual-wrapper'; container.parentNode.insertBefore(wrapper, container.nextSibling); } const decodedText = translatedText.replace(/\r\n/g, '\n').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); wrapper.textContent = decodedText; } function showErrorUI(container) { let wrapper = container.nextElementSibling; if (wrapper && wrapper.classList.contains('x-auto-bilingual-wrapper')) { wrapper.innerHTML = `翻译失败`; setTimeout(() => { wrapper.remove(); }, 3000); } } // ================= 5. 队列处理 ================= async function processQueue() { if (state.isProcessingQueue || state.translationQueue.length === 0) return; if (!state.headersReady) { setTimeout(processQueue, 1000); return; } state.isProcessingQueue = true; while (state.translationQueue.length > 0) { const { container, tweetId } = state.translationQueue.shift(); if (container.dataset.autoTranslated === 'done') continue; try { const translatedText = await requestTranslation(tweetId); updateBilingualUI(container, translatedText); container.dataset.autoTranslated = 'done'; } catch (error) { showErrorUI(container); container.dataset.autoTranslated = 'error'; } // 防封号安全延迟 await new Promise(resolve => setTimeout(resolve, 800)); } state.isProcessingQueue = false; } // ================= 6. 精准视口观察者 ================= function observeContainer(container) { if (container.dataset.autoTranslated) return; const lang = container.getAttribute('lang'); if (!lang || lang.startsWith('zh') || ['qme', 'und', 'zxx', 'art'].includes(lang)) { container.dataset.autoTranslated = 'skip'; return; } const tweetId = getExactTweetId(container); if (!tweetId) { container.dataset.autoTranslated = 'skip_no_id'; return; } container.dataset.autoTranslated = 'pending'; showLoadingUI(container); state.translationQueue.push({ container, tweetId }); processQueue(); } // ================= 7. DOM 监听与长推文处理 ================= const visibilityObserver = new IntersectionObserver((entries) => { for (const entry of entries) { if (entry.isIntersecting) { // 现在我们的观察单位是精确的文字容器,而不是整个大推文框 observeContainer(entry.target); visibilityObserver.unobserve(entry.target); } } }, { rootMargin: "150px" }); const domObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (!(node instanceof HTMLElement)) continue; // 直接捕捉新生成的推文文字块 if (node.getAttribute && node.getAttribute('data-testid') === 'tweetText') { visibilityObserver.observe(node); } else if (node.querySelectorAll) { const textContainers = node.querySelectorAll('[data-testid="tweetText"]'); textContainers.forEach(container => visibilityObserver.observe(container)); } } } }); document.addEventListener('click', (e) => { const target = e.target; if (target.tagName === 'SPAN' || target.tagName === 'DIV') { const text = target.textContent || ''; // 修复长推文展开导致的数据断裂问题 if (['Show more', '显示更多', '顯示更多'].some(kw => text.includes(kw))) { const article = target.closest('article'); if (article) { setTimeout(() => { const wrappers = article.querySelectorAll('.x-auto-bilingual-wrapper'); wrappers.forEach(w => w.remove()); const textContainers = article.querySelectorAll('[data-testid="tweetText"]'); textContainers.forEach(container => { container.removeAttribute('data-auto-translated'); observeContainer(container); }); }, 300); } } } }); function initDOMListener() { const textContainers = document.querySelectorAll('[data-testid="tweetText"]'); textContainers.forEach(container => visibilityObserver.observe(container)); domObserver.observe(document.body, { childList: true, subtree: true }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initDOMListener); } else { initDOMListener(); } })();