// ==UserScript== // @name GMGN Twitter翻译助手 // @namespace http://tampermonkey.net/ // @version 1.0.1 // @description 自动翻译GMGN网站上的Twitter用户简介信息 // @author 行止 // @license MIT // @supportURL https://x.com/MattMorote // @match https://gmgn.ai/* // @match https://*.gmgn.ai/* // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_notification // @connect api-free.deepl.com // @connect api.deepl.com // @connect translation.googleapis.com // @connect fanyi-api.baidu.com // @run-at document-idle // @downloadURL https://update.greasyfork.icu/scripts/555395/GMGN%20Twitter%E7%BF%BB%E8%AF%91%E5%8A%A9%E6%89%8B.user.js // @updateURL https://update.greasyfork.icu/scripts/555395/GMGN%20Twitter%E7%BF%BB%E8%AF%91%E5%8A%A9%E6%89%8B.meta.js // ==/UserScript== (function() { 'use strict'; // ========== 配置区域 ========== const CONFIG = { avatarSelector: 'a[href^="https://x.com/"], a[href*="x.com"]', avatarSVGSelector: 'svg[viewBox="0 0 16 16"][width="14px"]', communitySVGSelector: 'svg[viewBox="0 0 16 16"][width="14px"]', communityLinkSelector: 'a[href*="x.com/i/communities/"]', popupSelector: 'div[class*="px-14px"][class*="py-14px"]', popupSelectorAlt: 'div.flex.flex-col.flex-1[class*="px-14px"]', bioSelector: 'div[class*="text-["][class*="mt-12px"][class*="break-words"][class*="min-h-[36px]"]', bioSelectorAlt: 'div[class*="mt-12px"][class*="break-words"]', communityBioSelector: 'div[class*="text-text-100"][class*="mt-[5px"][class*="break-words"][class*="whitespace-pre-line"][class*="min-h-[36px]"]', communityBioSelectorAlt: 'div[class*="mt-[5px"][class*="break-words"][class*="whitespace-pre-line"]', translationClass: 'gmgn-translation-result', waitDelay: 50 }; // ========== 默认设置 ========== const DEFAULT_SETTINGS = { enabled: true, translationService: 'google', targetLanguage: 'zh', apiKey: '', appId: '', appSecret: '' }; // ========== 翻译服务配置 ========== const TRANSLATION_SERVICES = { google: { name: 'Google Translate', apiUrl: 'https://translation.googleapis.com/language/translate/v2', needsKey: true }, deepl: { name: 'DeepL', apiUrl: 'https://api-free.deepl.com/v2/translate', needsKey: true }, baidu: { name: '百度翻译', apiUrl: 'https://fanyi-api.baidu.com/api/trans/vip/translate', needsKey: true, needsAppId: true } }; // ========== 状态管理 ========== let isEnabled = true; let currentPopup = null; let translationCache = new Map(); let processingPopups = new Set(); let debounceTimer = null; let observer = null; let mouseoverTimer = null; const MAX_CACHE_SIZE = 100; const CACHE_CLEANUP_INTERVAL = 5 * 60 * 1000; // ========== 存储管理(替代chrome.storage)========== function getSettings() { const stored = GM_getValue('settings', null); if (stored) { return { ...DEFAULT_SETTINGS, ...stored }; } return DEFAULT_SETTINGS; } function saveSettings(settings) { GM_setValue('settings', settings); } // ========== 初始化 ========== function init() { const settings = getSettings(); isEnabled = settings.enabled !== false; if (isEnabled) { startMonitoring(); } // 注入样式 injectStyles(); // 定期清理缓存 setInterval(() => { if (translationCache.size > MAX_CACHE_SIZE) { const keysToDelete = Array.from(translationCache.keys()).slice(0, Math.floor(MAX_CACHE_SIZE / 2)); keysToDelete.forEach(key => translationCache.delete(key)); } }, CACHE_CLEANUP_INTERVAL); // 注册设置菜单 registerMenuCommands(); } // ========== 注入样式 ========== function injectStyles() { const style = document.createElement('style'); style.textContent = ` .gmgn-translation-result { margin-top: 12px; padding: 10px 12px; background: linear-gradient(135deg, #ff8c42 0%, #ff6b35 100%); border-radius: 8px; color: #fff; font-size: 14px; line-height: 1.6; box-shadow: 0 4px 6px rgba(255, 140, 66, 0.2); animation: slideIn 0.3s ease-out; } @keyframes slideIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } .gmgn-translation-content { display: flex; align-items: flex-start; gap: 8px; } .gmgn-translation-icon { font-size: 16px; flex-shrink: 0; margin-top: 2px; } .gmgn-translation-text { flex: 1; word-wrap: break-word; word-break: break-word; } .gmgn-translation-error { margin-top: 8px; padding: 8px; background: #fee; border: 1px solid #fcc; border-radius: 4px; color: #c00; font-size: 12px; } `; document.head.appendChild(style); } // ========== 注册菜单命令 ========== function registerMenuCommands() { GM_registerMenuCommand('⚙️ 打开设置', openSettings); GM_registerMenuCommand('🔄 切换启用/禁用', toggleEnabled); } // ========== 打开设置 ========== function openSettings() { const settings = getSettings(); const serviceOptions = Object.keys(TRANSLATION_SERVICES).map(key => `` ).join(''); const html = ` GMGN翻译助手设置

🌐 GMGN翻译助手

自动翻译Twitter简介

💡 如何获取API密钥:

`; // 将保存函数暴露到全局,供设置窗口调用 window.GMGN_SaveSettings = function(settings) { saveSettings(settings); isEnabled = settings.enabled; if (isEnabled) { startMonitoring(); } else { stopMonitoring(); } }; const win = window.open('', 'GMGN翻译助手设置', 'width=600,height=700,resizable=yes,scrollbars=yes'); win.document.write(html); win.document.close(); // 监听设置保存消息(降级方案) window.addEventListener('message', function(event) { if (event.data && event.data.type === 'GMGN_SAVE_SETTINGS') { window.GMGN_SaveSettings(event.data.settings); } }); } // ========== 切换启用/禁用 ========== function toggleEnabled() { const settings = getSettings(); settings.enabled = !settings.enabled; saveSettings(settings); isEnabled = settings.enabled; if (isEnabled) { startMonitoring(); GM_notification({ title: 'GMGN翻译助手', text: '翻译功能已启用', timeout: 2000 }); } else { stopMonitoring(); GM_notification({ title: 'GMGN翻译助手', text: '翻译功能已禁用', timeout: 2000 }); } } // ========== 开始监听 ========== function startMonitoring() { observer = new MutationObserver((mutations) => { if (!isEnabled) return; for (const mutation of mutations) { if (mutation.type !== 'childList' || mutation.addedNodes.length === 0) { continue; } for (const node of mutation.addedNodes) { if (node.nodeType === 1) { if (node.classList && node.classList.contains(CONFIG.translationClass)) { continue; } if (node.querySelector && node.querySelector(`.${CONFIG.translationClass}`)) { continue; } checkForPopup(node); } } } }); observer.observe(document.body, { childList: true, subtree: true, attributes: false, characterData: false }); document.addEventListener('mouseover', handleMouseOver, true); } // ========== 停止监听 ========== function stopMonitoring() { if (observer) { observer.disconnect(); observer = null; } document.removeEventListener('mouseover', handleMouseOver, true); if (debounceTimer) { clearTimeout(debounceTimer); debounceTimer = null; } if (mouseoverTimer) { clearTimeout(mouseoverTimer); mouseoverTimer = null; } } // ========== 检查浮窗出现 ========== function checkForPopup(node) { if (node.nodeType !== 1 || !node.offsetHeight || node.offsetHeight < 30) { return; } const popup = findPopupElement(node); if (popup && popup !== currentPopup) { currentPopup = popup; if (debounceTimer) { clearTimeout(debounceTimer); } debounceTimer = setTimeout(() => { handlePopupAppear(popup); }, 30); } } // ========== 查找浮窗元素 ========== function findPopupElement(element) { if (matchesSelector(element, CONFIG.popupSelector)) { return element; } const popup = element.querySelector?.(CONFIG.popupSelector); if (popup) { return popup; } let parent = element.parentElement; let depth = 0; while (parent && depth < 5) { if (matchesSelector(parent, CONFIG.popupSelector)) { return parent; } parent = parent.parentElement; depth++; } return null; } // ========== 匹配选择器 ========== function matchesSelector(element, selector) { if (!element || !selector) return false; try { if (element.matches) { return element.matches(selector); } if (element.matchesSelector) { return element.matchesSelector(selector); } if (element.msMatchesSelector) { return element.msMatchesSelector(selector); } if (selector.includes('class')) { const classMatch = selector.match(/\[class\*="([^"]+)"/); if (classMatch) { const className = classMatch[1]; return element.className && element.className.includes(className); } } } catch (e) { // 静默失败 } return false; } // ========== 处理鼠标悬停 ========== function handleMouseOver(event) { if (!isEnabled) return; const target = event.target; if (target.tagName !== 'A' && target.tagName !== 'SVG' && !target.closest('a') && !target.closest('svg')) { return; } if (isAvatarElement(target)) { if (mouseoverTimer) { clearTimeout(mouseoverTimer); } mouseoverTimer = setTimeout(() => { const popup = findPopupInDocument(); if (popup && popup !== currentPopup) { currentPopup = popup; handlePopupAppear(popup); } setTimeout(() => { const popup = findPopupInDocument(); if (popup && popup !== currentPopup) { currentPopup = popup; handlePopupAppear(popup); } }, CONFIG.waitDelay); mouseoverTimer = null; }, 20); } } // ========== 检查是否是头像元素 ========== function isAvatarElement(element) { if (!element) return false; const personalLink = element.closest('a[href^="https://x.com/"]'); if (personalLink && !personalLink.href.includes('/i/communities/')) { return true; } const communityLink = element.closest('a[href*="x.com/i/communities/"]'); if (communityLink) { return true; } const svg = element.closest('svg[viewBox="0 0 16 16"]'); if (svg) { const width = svg.getAttribute('width'); if (width === '14px' || width === '14') { const paths = svg.querySelectorAll('path'); if (paths.length >= 2) { const pathData = Array.from(paths).map(p => p.getAttribute('d')).join(''); if (pathData.length > 200) { return true; } } return true; } } if (element.closest('[class*="css-k008qs"]')) { return true; } return false; } // ========== 在文档中查找浮窗 ========== let lastPopupQuery = { time: 0, result: null }; const POPUP_QUERY_CACHE_TIME = 30; function findPopupInDocument() { const now = Date.now(); if (lastPopupQuery.result && (now - lastPopupQuery.time) < POPUP_QUERY_CACHE_TIME) { if (lastPopupQuery.result.parentElement && isVisible(lastPopupQuery.result)) { return lastPopupQuery.result; } } const selectors = [ CONFIG.popupSelector, CONFIG.popupSelectorAlt, 'div[class*="px-14px"][class*="py-14px"]', ]; for (const selector of selectors) { try { const element = document.querySelector(selector); if (element && isVisible(element) && containsTwitterContent(element)) { lastPopupQuery = { time: now, result: element }; return element; } } catch (e) { continue; } } lastPopupQuery = { time: now, result: null }; return null; } // ========== 检查是否包含Twitter内容 ========== function containsTwitterContent(element) { if (!element) return false; const hasTwitterLink = element.querySelector('a[href^="https://x.com/"], a[href*="x.com"]'); if (!hasTwitterLink) return false; const hasProfileButton = element.textContent.includes('See Profile on X') || element.textContent.includes('Following') || element.textContent.includes('Followers'); const hasCommunityButton = element.textContent.includes('View community on X') || element.textContent.includes('community'); return hasProfileButton || hasCommunityButton; } // ========== 检查是否是社群浮窗 ========== function isCommunityPopup(popupElement) { if (!popupElement) return false; const communityLink = popupElement.querySelector('a[href*="x.com/i/communities/"]'); if (communityLink) { return true; } const hasCommunityButton = popupElement.textContent.includes('View community on X') || (popupElement.textContent.includes('community') && popupElement.textContent.includes('Members')); if (hasCommunityButton) { return true; } const communityBio = popupElement.querySelector(CONFIG.communityBioSelector) || popupElement.querySelector(CONFIG.communityBioSelectorAlt) || popupElement.querySelector('div[class*="mt-[5px"][class*="break-words"][class*="whitespace-pre-line"]'); if (communityBio) { return true; } return false; } // ========== 检查元素是否可见 ========== function isVisible(element) { if (!element) return false; const style = window.getComputedStyle(element); return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'; } // ========== 处理浮窗出现 ========== function handlePopupAppear(popupElement) { if (!popupElement) return; const popupId = getPopupId(popupElement); if (processingPopups.has(popupId)) { return; } if (popupElement.querySelector(`.${CONFIG.translationClass}`)) { return; } processingPopups.add(popupId); let bioText = extractBioText(popupElement); if (!bioText || bioText.length < 8) { setTimeout(() => { if (popupElement.querySelector(`.${CONFIG.translationClass}`)) { processingPopups.delete(popupId); return; } bioText = extractBioText(popupElement); if (bioText && bioText.length >= 8) { translateAndDisplay(popupElement, bioText).finally(() => { setTimeout(() => { processingPopups.delete(popupId); }, 100); }); } else { processingPopups.delete(popupId); } }, 30); } else { translateAndDisplay(popupElement, bioText).finally(() => { setTimeout(() => { processingPopups.delete(popupId); }, 100); }); } } // ========== 获取浮窗唯一ID ========== function getPopupId(popupElement) { const bioText = extractBioText(popupElement); if (bioText) { return 'popup_' + bioText.substring(0, 50).replace(/\s/g, '_'); } const rect = popupElement.getBoundingClientRect(); return 'popup_' + Math.round(rect.top) + '_' + Math.round(rect.left); } // ========== 提取简介文本 ========== function extractBioText(popupElement) { if (!popupElement) return null; const isCommunity = isCommunityPopup(popupElement); if (isCommunity) { const selectors = [ CONFIG.communityBioSelector, CONFIG.communityBioSelectorAlt, 'div[class*="mt-[5px"][class*="break-words"][class*="whitespace-pre-line"]', 'div[class*="text-text-100"][class*="mt-[5px"]', ]; for (const selector of selectors) { try { const element = popupElement.querySelector(selector); if (element) { const text = element.innerText || element.textContent; const cleanText = text.trim(); if (isValidBioText(cleanText)) { return cleanText; } } } catch (e) { // 静默失败 } } } else { const selectors = [ CONFIG.bioSelector, CONFIG.bioSelectorAlt, 'div[class*="mt-12px"][class*="break-words"]', 'div[class*="text-["][class*="mt-12px"]', ]; for (const selector of selectors) { try { const element = popupElement.querySelector(selector); if (element) { const text = element.innerText || element.textContent; const cleanText = text.trim(); if (isValidBioText(cleanText)) { return cleanText; } } } catch (e) { // 继续尝试下一个 } } } return extractTextIntelligently(popupElement); } // ========== 验证是否是有效的简介文本 ========== function isValidBioText(text) { if (!text || text.length < 5) return false; const excludePatterns = [ /^(Joined|Following|Followers|Created|Members|\d+)/, /^https?:\/\//, /^@\w+/, /^(See Profile|View community)/, /^\d+\s*(Following|Followers|Members)/, /^(Dec|Nov|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct)\s+\d{4}$/, ]; for (const pattern of excludePatterns) { if (pattern.test(text)) { return false; } } return text.length >= 8 && !/^\d+$/.test(text) && !/^[^\w\s]+$/.test(text); } // ========== 智能提取文本 ========== function extractTextIntelligently(element) { const commonSelectors = ['div', 'p', 'span']; let bestText = ''; let bestScore = 0; for (const tag of commonSelectors) { const elements = element.querySelectorAll(`> ${tag}`); for (const el of elements) { const text = (el.innerText || el.textContent).trim(); if (!text || text.length < 8) continue; if (!isValidBioText(text)) continue; const score = calculateTextScore(el, text); if (score > bestScore) { bestText = text; bestScore = score; } } } if (!bestText) { const allTextElements = element.querySelectorAll('p, div, span'); for (const el of allTextElements) { const text = (el.innerText || el.textContent).trim(); if (!text || text.length < 8) continue; if (!isValidBioText(text)) continue; const score = calculateTextScore(el, text); if (score > bestScore) { bestText = text; bestScore = score; } } } return bestText || null; } // ========== 计算文本分数 ========== function calculateTextScore(el, text) { let score = text.length; const classList = el.className || ''; if (classList.includes('mt-12px') || classList.includes('mt-[5px') || classList.includes('mt-')) { score += 50; } if (classList.includes('break-words')) { score += 30; } if (classList.includes('text-[') || classList.includes('text-text-100')) { score += 20; } if (classList.includes('whitespace-pre-line')) { score += 15; } return score; } // ========== 翻译并显示 ========== async function translateAndDisplay(popupElement, bioText) { if (popupElement.querySelector(`.${CONFIG.translationClass}`)) { return; } const cacheKey = bioText.substring(0, 100); if (translationCache.has(cacheKey)) { const cached = translationCache.get(cacheKey); displayTranslation(popupElement, bioText, cached); return; } if (translationCache.size >= MAX_CACHE_SIZE) { const firstKey = translationCache.keys().next().value; translationCache.delete(firstKey); } try { const settings = getSettings(); const translatedText = await translateText(bioText, settings); if (!translatedText || translatedText.trim().length === 0) { throw new Error('翻译结果为空'); } translationCache.set(cacheKey, translatedText); displayTranslation(popupElement, bioText, translatedText); } catch (error) { let errorMessage = '翻译服务不可用'; if (error.message) { if (error.message.includes('超时')) { errorMessage = '翻译请求超时,请稍后重试'; } else if (error.message.includes('网络')) { errorMessage = '网络连接失败,请检查网络'; } else if (error.message.includes('API')) { errorMessage = 'API调用失败:' + error.message; } else { errorMessage = error.message; } } showError(popupElement, errorMessage); } } // ========== 翻译文本(使用GM_xmlhttpRequest)========== function translateText(text, settings) { return new Promise((resolve, reject) => { if (!settings.apiKey && TRANSLATION_SERVICES[settings.translationService]?.needsKey) { reject(new Error('请先在设置中配置API密钥')); return; } if (!text || text.trim().length === 0) { reject(new Error('翻译文本为空')); return; } if (text.length > 5000) { reject(new Error('翻译文本过长(超过5000字符)')); return; } switch (settings.translationService) { case 'google': translateWithGoogle(text, settings, resolve, reject); break; case 'deepl': translateWithDeepL(text, settings, resolve, reject); break; case 'baidu': if (!settings.appId || !settings.appSecret) { reject(new Error('百度翻译需要App ID和App Secret')); return; } translateWithBaidu(text, settings, resolve, reject); break; default: reject(new Error('不支持的翻译服务:' + settings.translationService)); } }); } // ========== Google Translate ========== function translateWithGoogle(text, settings, resolve, reject) { const url = `${TRANSLATION_SERVICES.google.apiUrl}?key=${settings.apiKey}`; GM_xmlhttpRequest({ method: 'POST', url: url, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ q: text, target: settings.targetLanguage, source: 'auto' }), onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); resolve(data.data.translations[0].translatedText); } catch (e) { reject(new Error('解析响应失败')); } } else { try { const error = JSON.parse(response.responseText); reject(new Error(error.error?.message || `HTTP ${response.status}`)); } catch (e) { reject(new Error(`HTTP ${response.status}: ${response.statusText}`)); } } }, onerror: function(error) { reject(new Error('网络连接失败')); }, ontimeout: function() { reject(new Error('请求超时')); }, timeout: 10000 }); } // ========== DeepL Translate ========== function translateWithDeepL(text, settings, resolve, reject) { const isFree = settings.apiKey.endsWith(':fx'); const apiUrl = isFree ? 'https://api-free.deepl.com/v2/translate' : 'https://api.deepl.com/v2/translate'; const langMap = { 'zh': 'ZH', 'en': 'EN', 'ja': 'JA', 'ko': 'KO', 'fr': 'FR', 'de': 'DE', 'es': 'ES' }; const targetLang = langMap[settings.targetLanguage] || settings.targetLanguage.toUpperCase(); const formData = new URLSearchParams(); formData.append('auth_key', settings.apiKey); formData.append('text', text); formData.append('target_lang', targetLang); GM_xmlhttpRequest({ method: 'POST', url: apiUrl, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: formData.toString(), onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); if (data.translations && data.translations.length > 0) { resolve(data.translations[0].text); } else { reject(new Error('DeepL返回数据格式错误')); } } catch (e) { reject(new Error('解析响应失败')); } } else { try { const error = JSON.parse(response.responseText); reject(new Error(error.message || error.error?.message || `HTTP ${response.status}`)); } catch (e) { reject(new Error(`HTTP ${response.status}: ${response.statusText}`)); } } }, onerror: function(error) { reject(new Error('网络连接失败')); }, ontimeout: function() { reject(new Error('请求超时')); }, timeout: 10000 }); } // ========== 百度翻译 ========== function translateWithBaidu(text, settings, resolve, reject) { const salt = Date.now().toString(); // 生成签名(简化版,使用MD5) const str = settings.appId + text + salt + settings.appSecret; const sign = md5(str); const params = new URLSearchParams({ q: text, from: 'auto', to: settings.targetLanguage, appid: settings.appId, salt: salt, sign: sign }); const url = `${TRANSLATION_SERVICES.baidu.apiUrl}?${params.toString()}`; GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); if (data.error_code) { reject(new Error(`百度翻译错误: ${data.error_msg}`)); } else { resolve(data.trans_result[0].dst); } } catch (e) { reject(new Error('解析响应失败')); } } else { reject(new Error(`HTTP ${response.status}: ${response.statusText}`)); } }, onerror: function(error) { reject(new Error('网络连接失败')); }, ontimeout: function() { reject(new Error('请求超时')); }, timeout: 10000 }); } // ========== MD5哈希(用于百度翻译签名)========== function md5(string) { // 完整的MD5实现 function md5cycle(x, k) { let 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, -40341101); c = ff(c, d, a, b, k[14], 17, -1502002290); b = ff(b, c, d, a, k[15], 22, 1236535329); a = gg(a, b, c, d, k[1], 5, -165796510); 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 add32(a, b) { return (a + b) & 0xFFFFFFFF; } function rhex(n) { let s = '', j = 0; for (; j < 4; j++) { s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + hex_chr[(n >> (j * 8)) & 0x0F]; } return s; } const hex_chr = '0123456789abcdef'.split(''); const utf8 = unescape(encodeURIComponent(string)); const len = utf8.length; const state = [1732584193, -271733879, -1732584194, 271733878]; let i; for (i = 64; i <= len; i += 64) { md5cycle(state, md5blk(utf8.substring(i - 64, i))); } utf8 = utf8.substring(i - 64); const tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; for (i = 0; i < utf8.length; i++) { tail[i >> 2] |= utf8.charCodeAt(i) << ((i % 4) << 3); } tail[i >> 2] |= 0x80 << ((i % 4) << 3); if (i > 55) { md5cycle(state, tail); for (i = 0; i < 16; i++) tail[i] = 0; } tail[14] = len * 8; md5cycle(state, tail); return rhex(state[0]) + rhex(state[1]) + rhex(state[2]) + rhex(state[3]); function md5blk(s) { const md5blks = []; let 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; } } // ========== 显示翻译结果 ========== function displayTranslation(popupElement, originalText, translatedText) { const existingTranslation = popupElement.querySelector(`.${CONFIG.translationClass}`); if (existingTranslation) { const translatedEl = existingTranslation.querySelector('.gmgn-translation-text'); if (translatedEl) { translatedEl.textContent = translatedText; } return; } const oldTranslation = popupElement.querySelector(`.${CONFIG.translationClass}`); if (oldTranslation) { oldTranslation.remove(); } const translationDiv = document.createElement('div'); translationDiv.className = CONFIG.translationClass; const contentDiv = document.createElement('div'); contentDiv.className = 'gmgn-translation-content'; const iconSpan = document.createElement('span'); iconSpan.className = 'gmgn-translation-icon'; iconSpan.textContent = '🌐'; const textSpan = document.createElement('span'); textSpan.className = 'gmgn-translation-text'; textSpan.textContent = translatedText; contentDiv.appendChild(iconSpan); contentDiv.appendChild(textSpan); translationDiv.appendChild(contentDiv); const bioElement = findBioElement(popupElement); if (bioElement && bioElement.parentNode) { const nextSibling = bioElement.nextSibling; if (nextSibling && nextSibling.classList && nextSibling.classList.contains(CONFIG.translationClass)) { return; } bioElement.parentNode.insertBefore(translationDiv, bioElement.nextSibling); } else { const lastChild = popupElement.lastElementChild; if (lastChild && lastChild.classList && lastChild.classList.contains(CONFIG.translationClass)) { return; } popupElement.appendChild(translationDiv); } } // ========== 查找简介元素 ========== function findBioElement(popupElement) { if (!popupElement) return null; const isCommunity = isCommunityPopup(popupElement); if (isCommunity) { const selectors = [ CONFIG.communityBioSelector, CONFIG.communityBioSelectorAlt, 'div[class*="mt-[5px"][class*="break-words"]', 'div[class*="text-text-100"][class*="mt-[5px"]', ]; for (const selector of selectors) { try { const element = popupElement.querySelector(selector); if (element) { const text = (element.innerText || element.textContent).trim(); if (isValidBioText(text)) { return element; } } } catch (e) { // 继续尝试下一个 } } } else { const selectors = [ CONFIG.bioSelector, CONFIG.bioSelectorAlt, 'div[class*="mt-12px"][class*="break-words"]', 'div[class*="text-["][class*="mt-12px"]', ]; for (const selector of selectors) { try { const element = popupElement.querySelector(selector); if (element) { const text = (element.innerText || element.textContent).trim(); if (isValidBioText(text)) { return element; } } } catch (e) { // 继续尝试下一个 } } } const allDivs = popupElement.querySelectorAll('div'); for (const div of allDivs) { const text = (div.innerText || div.textContent).trim(); if (isValidBioText(text)) { const classList = div.className || ''; if (classList.includes('mt-12px') || classList.includes('mt-[5px') || classList.includes('break-words')) { return div; } } } return null; } // ========== 显示错误 ========== function showError(popupElement, message) { const oldError = popupElement.querySelector('.gmgn-translation-error'); if (oldError) { oldError.remove(); } const error = document.createElement('div'); error.className = 'gmgn-translation-error'; error.textContent = `❌ ${message}`; error.style.cssText = ` padding: 8px 12px; margin-top: 10px; color: #d32f2f; background-color: #ffebee; border: 1px solid #ef9a9a; border-radius: 6px; font-size: 13px; line-height: 1.5; `; const bioElement = findBioElement(popupElement); if (bioElement && bioElement.parentNode) { bioElement.parentNode.insertBefore(error, bioElement.nextSibling); } else { popupElement.appendChild(error); } } // ========== 页面加载完成后初始化 ========== if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();