// ==UserScript== // @name Universal DeepSeek Text Selection // @namespace http://tampermonkey.net/ // @version 3.7 // @description 通用型选中文本翻译/解释工具,支持复杂动态网页 // @author You // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @connect api.deepseek.com // @connect api.deepseek.ai // @connect * // @run-at document-start // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/523257/Universal%20DeepSeek%20Text%20Selection.user.js // @updateURL https://update.greasyfork.icu/scripts/523257/Universal%20DeepSeek%20Text%20Selection.meta.js // ==/UserScript== (function () { 'use strict'; const CONFIG = { API_KEY: '', API_URL: 'https://api.deepseek.com/v1/chat/completions', MAX_RETRIES: 3, RETRY_DELAY: 1000, RETRY_BACKOFF_FACTOR: 1.5, DEBOUNCE_DELAY: 200, SHORTCUTS: { translate: 'Alt+T', explain: 'Alt+E', summarize: 'Alt+S' }, MAX_TEXT_LENGTH: 5000, MIN_TEXT_LENGTH: 1, ERROR_DISPLAY_TIME: 3000, ANIMATION_DURATION: 200, MENU_FADE_DELAY: 150, CACHE_DURATION: 3600000, // 1小时 MAX_CACHE_ITEMS: 50, LOADING_MESSAGES: [ '正在思考中...', '处理中,请稍候...', '马上就好...', '正在分析文本...' ], LOADING_INTERVAL: 2000, MAX_RESULT_HEIGHT: 400, SCROLLBAR_WIDTH: 15, }; // 样式注入 GM_addStyle(` #ai-floating-menu { all: initial; position: fixed; z-index: 2147483647; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); padding: 5px; opacity: 1; visibility: visible; transition: opacity ${CONFIG.ANIMATION_DURATION}ms ease, visibility ${CONFIG.ANIMATION_DURATION}ms ease; font-family: system-ui, -apple-system, sans-serif; animation: fadeIn 0.3s ease; } @keyframes fadeIn { 0% { opacity: 0; transform: scale(0.9); } 100% { opacity: 1; transform: scale(1); } } #ai-floating-menu.hiding { opacity: 0; visibility: hidden; } #ai-floating-menu button { all: initial; display: block; width: 120px; margin: 3px; padding: 8px 12px; background: #2c3e50; color: white; border: none; border-radius: 4px; cursor: pointer; font-family: inherit; font-size: 14px; text-align: center; transition: all 0.2s; position: relative; overflow: hidden; } #ai-floating-menu button:hover { background: #34495e; transform: translateY(-1px); } #ai-floating-menu button:active { transform: translateY(1px); } #ai-floating-menu button.processing { pointer-events: none; opacity: 0.7; } #ai-floating-menu button.processing::after { content: ''; position: absolute; bottom: 0; left: 0; height: 2px; width: 100%; background: linear-gradient(to right, #3498db, #2ecc71); animation: loading-bar 2s infinite linear; } #ai-floating-menu .shortcut { float: right; font-size: 12px; opacity: 0.7; } #ai-result-box { all: initial; position: fixed; z-index: 2147483648; background: white; border-radius: 8px; box-shadow: 0 3px 15px rgba(0,0,0,0.2); padding: 15px; min-width: 200px; max-width: 500px; max-height: ${CONFIG.MAX_RESULT_HEIGHT}px; opacity: 1; visibility: visible; transition: opacity ${CONFIG.ANIMATION_DURATION}ms ease, visibility ${CONFIG.ANIMATION_DURATION}ms ease, transform 0.2s ease; font-family: system-ui, -apple-system, sans-serif; font-size: 14px; line-height: 1.6; color: #333; overflow: auto; transform: translateY(0); animation: fadeIn 0.3s ease; cursor: grab; user-select: none; } #ai-result-box .content { cursor: default; user-select: text; } #ai-result-box.hiding { opacity: 0; visibility: hidden; transform: translateY(10px); } #ai-result-box .close-btn { all: initial; position: absolute; top: 8px; right: 8px; width: 20px; height: 20px; line-height: 20px; text-align: center; background: #f0f0f0; border: none; border-radius: 50%; cursor: pointer; font-family: inherit; font-size: 14px; color: #666; transition: all 0.2s; } #ai-result-box .close-btn:hover { background: #e0e0e0; transform: rotate(90deg); } #ai-result-box .content { margin-top: 5px; white-space: pre-wrap; word-break: break-word; line-height: 1.6; font-size: 14px; color: #2c3e50; } #ai-result-box .error { color: #e74c3c; background: #fde8e7; padding: 10px; border-radius: 4px; margin-bottom: 10px; animation: shake 0.5s ease-in-out; } #ai-result-box .loading-container { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; text-align: center; } .loading-spinner { display: inline-block; width: 30px; height: 30px; border: 3px solid #f3f3f3; border-top: 3px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 10px; } .loading-text { color: #666; font-size: 14px; margin-top: 10px; min-height: 20px; transition: opacity 0.3s; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } @keyframes loading-bar { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } @keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } } @media (prefers-color-scheme: dark) { #ai-floating-menu, #ai-result-box { background: #2c3e50; color: #ecf0f1; } #ai-result-box .content { color: #ecf0f1; } #ai-result-box .error { background: #4a1c17; } .loading-text { color: #ecf0f1; } } `); // 工具函数 const utils = { debounce(func, wait) { let timeout; return function (...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; }, async retry(fn, retries = CONFIG.MAX_RETRIES, delay = CONFIG.RETRY_DELAY) { try { return await fn(); } catch (error) { if (retries === 0) throw error; await new Promise(resolve => setTimeout(resolve, delay)); return this.retry(fn, retries - 1, delay * CONFIG.RETRY_BACKOFF_FACTOR); } }, createLoadingSpinner() { return `
${CONFIG.LOADING_MESSAGES[0]}
`; }, isValidText(text) { return text && text.length >= CONFIG.MIN_TEXT_LENGTH && text.length <= CONFIG.MAX_TEXT_LENGTH; }, rotateLoadingMessage() { const loadingText = document.querySelector('.loading-text'); if (!loadingText) return; let currentIndex = 0; return setInterval(() => { currentIndex = (currentIndex + 1) % CONFIG.LOADING_MESSAGES.length; loadingText.style.opacity = '0'; setTimeout(() => { loadingText.textContent = CONFIG.LOADING_MESSAGES[currentIndex]; loadingText.style.opacity = '1'; }, 300); }, CONFIG.LOADING_INTERVAL); } }; // 缓存管理类 class CacheManager { static getKey(text, action) { return `${action}_${text}`; } static async get(text, action) { const key = this.getKey(text, action); const cached = GM_getValue(key); if (cached && Date.now() - cached.timestamp < CONFIG.CACHE_DURATION) { return cached.data; } return null; } static async set(text, action, data) { const key = this.getKey(text, action); const cache = { data, timestamp: Date.now() }; const keys = Object.keys(GM_getValue('cache_keys', {})); if (keys.length >= CONFIG.MAX_CACHE_ITEMS) { const oldestKey = keys[0]; GM_deleteValue(oldestKey); keys.shift(); } keys.push(key); GM_setValue('cache_keys', keys); GM_setValue(key, cache); } } // API调用类 class APIClient { static async call(text, action) { const cached = await CacheManager.get(text, action); if (cached) return cached; if (!utils.isValidText(text)) { throw new Error(`文本长度应在${CONFIG.MIN_TEXT_LENGTH}至${CONFIG.MAX_TEXT_LENGTH}字符之间`); } const prompts = { translate: '将以下内容翻译成中文,保持专业性和准确性:', explain: '请详细解释以下内容,如果包含专业术语请着重说明:', summarize: '请提炼以下内容的关键要点,以简洁的要点形式列出:' }; let retryCount = 0; const maxRetries = CONFIG.MAX_RETRIES; while (retryCount < maxRetries) { try { const response = await this.makeRequest(text, prompts[action]); const result = this.processResponse(response); await CacheManager.set(text, action, result); return result; } catch (error) { retryCount++; if (retryCount === maxRetries) throw error; await new Promise(resolve => setTimeout(resolve, CONFIG.RETRY_DELAY * Math.pow(CONFIG.RETRY_BACKOFF_FACTOR, retryCount)) ); } } } static async makeRequest(text, prompt) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: CONFIG.API_URL, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CONFIG.API_KEY}` }, data: JSON.stringify({ model: 'deepseek-chat', messages: [{ role: 'user', content: `${prompt}\n\n${text}` }], temperature: 0.7, max_tokens: 2000, presence_penalty: 0.6, frequency_penalty: 0.5 }), timeout: 30000, onload: resolve, onerror: reject, ontimeout: () => reject(new Error('请求超时')) }); }); } static processResponse(res) { if (res.status !== 200) { throw new Error(`API错误: ${res.status}`); } try { const data = JSON.parse(res.responseText); if (!data.choices?.[0]?.message?.content) { throw new Error('API返回格式错误'); } return data.choices[0].message.content; } catch (e) { throw new Error('解析响应失败'); } } static getErrorMessage(error) { const errorMessages = { 'Network Error': '网络连接失败', 'Timeout': '请求超时', 'API错误: 429': '请求过于频繁,请稍后再试', 'API错误: 401': 'API密钥无效', 'API错误: 403': '没有访问权限' }; return errorMessages[error.message] || error.message; } } // UI管理类 class UIManager { static ensureElementsExist() { if (!document.getElementById('ai-floating-menu')) { const menu = document.createElement('div'); menu.id = 'ai-floating-menu'; menu.style.display = 'none'; menu.innerHTML = ` `; document.body.appendChild(menu); } if (!document.getElementById('ai-result-box')) { const resultBox = document.createElement('div'); resultBox.id = 'ai-result-box'; resultBox.style.display = 'none'; resultBox.innerHTML = `
`; document.body.appendChild(resultBox); } } static async showMenu(x, y) { this.ensureElementsExist(); await this.hideAll(); const menu = document.getElementById('ai-floating-menu'); const { left, top } = this.calculateOptimalPosition(x, y, menu); menu.style.left = `${left}px`; menu.style.top = `${top}px`; menu.style.display = 'block'; menu.offsetHeight; // 触发重排 menu.classList.remove('hiding'); } static async showResult(content, x, y) { this.ensureElementsExist(); await this.hideMenu(); const resultBox = document.getElementById('ai-result-box'); const contentDiv = resultBox.querySelector('.content'); if (content.startsWith('错误:')) { contentDiv.classList.add('error'); setTimeout(() => { this.hideAll(); contentDiv.classList.remove('error'); }, CONFIG.ERROR_DISPLAY_TIME); } else { contentDiv.classList.remove('error'); } contentDiv.innerHTML = content; const { left, top } = this.calculateResultPosition(x, y, resultBox); resultBox.style.left = `${left}px`; resultBox.style.top = `${top}px`; resultBox.style.display = 'block'; resultBox.offsetHeight; // 触发重排 resultBox.classList.remove('hiding'); return content.includes('loading-container') ? utils.rotateLoadingMessage() : null; } static calculateOptimalPosition(x, y, element) { const margin = 10; const maxWidth = Math.min(500, window.innerWidth - 2 * margin); element.style.maxWidth = `${maxWidth}px`; let left = Math.max(margin, Math.min(x, window.innerWidth - element.offsetWidth - margin)); let top = Math.max(margin, Math.min(y, window.innerHeight - element.offsetHeight - margin)); return { left, top }; } static calculateResultPosition(x, y, element) { const margin = 20; const maxWidth = Math.min(500, window.innerWidth - 2 * margin); element.style.maxWidth = `${maxWidth}px`; const selection = window.getSelection(); let selectionRect = null; if (selection.rangeCount > 0) { selectionRect = selection.getRangeAt(0).getBoundingClientRect(); } let left, top; if (selectionRect) { // 优先显示在选区下方 left = selectionRect.left; top = selectionRect.bottom + margin; // 如果底部空间不足,则显示在选区上方 if (top + element.offsetHeight > window.innerHeight - margin) { top = Math.max(margin, selectionRect.top - element.offsetHeight - margin); } // 如果水平方向超出屏幕,进行调整 if (left + maxWidth > window.innerWidth - margin) { left = Math.max(margin, window.innerWidth - maxWidth - margin); } } else { // 如果没有选区,则根据鼠标位置 left = Math.max(margin, Math.min(x, window.innerWidth - maxWidth - margin)); top = Math.max(margin, Math.min(y, window.innerHeight - element.offsetHeight - margin)); } return { left, top }; } static async hideMenu() { const menu = document.getElementById('ai-floating-menu'); if (menu && menu.style.display !== 'none') { menu.classList.add('hiding'); await new Promise(resolve => setTimeout(resolve, CONFIG.ANIMATION_DURATION)); menu.style.display = 'none'; } } static async hideAll() { const menu = document.getElementById('ai-floating-menu'); const resultBox = document.getElementById('ai-result-box'); const promises = []; if (menu && menu.style.display !== 'none') { menu.classList.add('hiding'); promises.push(new Promise(resolve => setTimeout(resolve, CONFIG.ANIMATION_DURATION))); } if (resultBox && resultBox.style.display !== 'none') { resultBox.classList.add('hiding'); promises.push(new Promise(resolve => setTimeout(resolve, CONFIG.ANIMATION_DURATION))); } await Promise.all(promises); if (menu) menu.style.display = 'none'; if (resultBox) resultBox.style.display = 'none'; } } // 文本选择管理类 class SelectionManager { static getSelectedText() { let text = ''; let range = null; const selection = window.getSelection(); text = selection.toString().trim(); if (text && selection.rangeCount > 0) { range = selection.getRangeAt(0); return { text, range }; } try { const iframes = document.getElementsByTagName('iframe'); for (const iframe of iframes) { try { const iframeSelection = iframe.contentWindow.getSelection(); const iframeText = iframeSelection.toString().trim(); if (iframeText) { return { text: iframeText, range: iframeSelection.rangeCount > 0 ? iframeSelection.getRangeAt(0) : null }; } } catch (e) { console.debug('无法访问iframe内容:', e); } } } catch (e) { console.debug('处理iframe时出错:', e); } const activeElement = document.activeElement; if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) { const start = activeElement.selectionStart; const end = activeElement.selectionEnd; if (start !== end) { text = activeElement.value.substring(start, end).trim(); return { text, range: null }; } } return { text: '', range: null }; } } // 事件处理类 class EventHandler { static init() { UIManager.ensureElementsExist(); this.setupEventListeners(); this.setupIntersectionObserver(); this.setupResizeObserver(); this.setupDraggable(); } static setupEventListeners() { const menu = document.getElementById('ai-floating-menu'); const resultBox = document.getElementById('ai-result-box'); // 使用事件委托处理按钮点击 document.addEventListener('click', async (e) => { const button = e.target.closest('#ai-floating-menu button'); if (!button) return; const action = button.dataset.action; const { text } = SelectionManager.getSelectedText(); if (!text) return; button.classList.add('processing'); await this.handleAction(action, text, e.clientX, e.clientY); button.classList.remove('processing'); }); // 关闭按钮 resultBox.querySelector('.close-btn').addEventListener('click', () => { UIManager.hideAll(); }); // 点击外部隐藏菜单和结果框 document.addEventListener('mousedown', (e) => { if (!menu.contains(e.target) && !resultBox.contains(e.target)) { UIManager.hideAll(); } }, true); // 快捷键支持 document.addEventListener('keydown', (e) => { for (const [action, shortcut] of Object.entries(CONFIG.SHORTCUTS)) { const [modifier, key] = shortcut.split('+'); if (e[`${modifier.toLowerCase()}Key`] && e.key.toUpperCase() === key) { e.preventDefault(); const { text } = SelectionManager.getSelectedText(); if (text) { this.handleAction(action, text, e.clientX, e.clientY); } } } }); // 文本选择监听 this.addSelectionListeners(); // 触摸屏支持 document.addEventListener('touchend', (e) => { const { text } = SelectionManager.getSelectedText(); if (text) { const touch = e.changedTouches[0]; UIManager.showMenu(touch.clientX, touch.clientY); } }); } static setupDraggable() { const resultBox = document.getElementById('ai-result-box'); let isDragging = false; let currentX; let currentY; let initialX; let initialY; resultBox.addEventListener('mousedown', (e) => { if (e.target.classList.contains('close-btn') || e.target.closest('.content')) return; isDragging = true; initialX = e.clientX - resultBox.offsetLeft; initialY = e.clientY - resultBox.offsetTop; resultBox.style.cursor = 'grabbing'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; e.preventDefault(); currentX = e.clientX - initialX; currentY = e.clientY - initialY; // 限制在可视区域内 currentX = Math.max(0, Math.min(currentX, window.innerWidth - resultBox.offsetWidth)); currentY = Math.max(0, Math.min(currentY, window.innerHeight - resultBox.offsetHeight)); resultBox.style.left = `${currentX}px`; resultBox.style.top = `${currentY}px`; }); document.addEventListener('mouseup', () => { isDragging = false; resultBox.style.cursor = 'grab'; }); } static addSelectionListeners(target = document) { const handleSelection = utils.debounce(async (e) => { const { text, range } = SelectionManager.getSelectedText(); if (!text) { await UIManager.hideAll(); return; } let x = e?.clientX || 0; let y = e?.clientY || 0; if (range) { try { const rect = range.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { x = rect.right; y = rect.bottom + 5; } } catch (e) { console.debug('获取选区位置失败:', e); } } await UIManager.showMenu(x, y); }, CONFIG.DEBOUNCE_DELAY); target.addEventListener('mouseup', handleSelection); target.addEventListener('keyup', handleSelection); target.addEventListener('selectionchange', handleSelection); } static setupIntersectionObserver() { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (!entry.isIntersecting) { UIManager.hideAll(); } }); }); const menu = document.getElementById('ai-floating-menu'); const resultBox = document.getElementById('ai-result-box'); observer.observe(menu); observer.observe(resultBox); } static setupResizeObserver() { const observer = new ResizeObserver(utils.debounce(() => { const menu = document.getElementById('ai-floating-menu'); const resultBox = document.getElementById('ai-result-box'); if (menu.style.display === 'block' || resultBox.style.display === 'block') { UIManager.hideAll(); } }, 100)); observer.observe(document.body); } static async handleAction(action, text, x, y) { let loadingMessageInterval; try { await UIManager.hideAll(); loadingMessageInterval = await UIManager.showResult(utils.createLoadingSpinner(), x, y); const response = await APIClient.call(text, action); clearInterval(loadingMessageInterval); UIManager.showResult(response, x, y); } catch (error) { if (loadingMessageInterval) clearInterval(loadingMessageInterval); UIManager.showResult(`错误: ${error.message}`, x, y); } } } // 初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => EventHandler.init()); } else { EventHandler.init(); } })();