Greasy Fork

Universal DeepSeek Text Selection

通用型选中文本翻译/解释工具,支持复杂动态网页

目前为 2025-01-09 提交的版本。查看 最新版本

// ==UserScript==
// @name         Universal DeepSeek Text Selection
// @namespace    http://tampermonkey.net/
// @version      3.1
// @description  通用型选中文本翻译/解释工具,支持复杂动态网页
// @author       tangwang
// @license MIT
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      api.deepseek.com
// @connect      api.deepseek.ai
// @connect      *
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    const CONFIG = {
        API_KEY: '',
        API_URL: '',
        MAX_RETRIES: 3,
        RETRY_DELAY: 1000,
        DEBOUNCE_DELAY: 200,
        SHORTCUTS: {
            translate: 'Alt+T',
            explain: 'Alt+E',
            summarize: 'Alt+S'
        }
    };

    // 样式注入
    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;
            display: none;
            font-family: system-ui, -apple-system, sans-serif;
            animation: fadeIn 0.2s ease-in-out;
        }
        #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;
        }
        #ai-floating-menu button:hover {
            background: #34495e;
            transform: translateY(-1px);
        }
        #ai-floating-menu button:active {
            transform: translateY(1px);
        }
        #ai-floating-menu .shortcut {
            float: right;
            font-size: 12px;
            opacity: 0.7;
        }
        #ai-result-box {
            all: initial;
            position: fixed;
            z-index: 2147483647;
            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: 400px;
            display: none;
            font-family: system-ui, -apple-system, sans-serif;
            font-size: 14px;
            line-height: 1.6;
            color: #333;
            overflow: auto;
            animation: fadeIn 0.2s ease-in-out;
        }
        #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;
        }
        .loading-spinner {
            display: inline-block;
            width: 20px;
            height: 20px;
            border: 2px solid #f3f3f3;
            border-top: 2px solid #3498db;
            border-radius: 50%;
            animation: spin 1s linear infinite;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        @keyframes fadeIn {
            from { opacity: 0; }
            to { opacity: 1; }
        }
    `);

    // 创建UI元素
    const menu = document.createElement('div');
    menu.id = 'ai-floating-menu';
    menu.innerHTML = `
        <button data-action="translate">翻译为中文 <span class="shortcut">Alt+T</span></button>
        <button data-action="explain">解释内容 <span class="shortcut">Alt+E</span></button>
        <button data-action="summarize">总结要点 <span class="shortcut">Alt+S</span></button>
    `;

    const resultBox = document.createElement('div');
    resultBox.id = 'ai-result-box';
    resultBox.innerHTML = `
        <button class="close-btn">×</button>
        <div class="content"></div>
    `;

    // 工具函数
    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 * 2);
            }
        },

        createLoadingSpinner() {
            return '<div class="loading-spinner"></div> 处理中...';
        }
    };

    // API调用类
    class APIClient {
        static async call(text, action) {
            const prompts = {
                translate: '将以下内容翻译成中文:',
                explain: '请解释以下内容:',
                summarize: '请总结以下内容的要点:'
            };

            return utils.retry(async () => {
                const response = await 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: `${prompts[action]}${text}`
                            }],
                            temperature: 0.7,
                            max_tokens: 1000
                        }),
                        onload: res => {
                            if (res.status === 200) {
                                try {
                                    const data = JSON.parse(res.responseText);
                                    if (data.choices?.[0]?.message?.content) {
                                        resolve(data.choices[0].message.content);
                                    } else {
                                        reject(new Error('API返回格式错误'));
                                    }
                                } catch (e) {
                                    reject(new Error('解析响应失败'));
                                }
                            } else {
                                reject(new Error(`API错误: ${res.status}`));
                            }
                        },
                        onerror: () => reject(new Error('网络请求失败')),
                        ontimeout: () => reject(new Error('请求超时'))
                    });
                });
                return response;
            });
        }
    }

    // UI管理类
    class UIManager {
        static ensureElementsExist() {
            if (!document.getElementById('ai-floating-menu')) {
                document.body.appendChild(menu);
            }
            if (!document.getElementById('ai-result-box')) {
                document.body.appendChild(resultBox);
            }
        }

        static showMenu(x, y) {
            this.ensureElementsExist();
            menu.style.left = `${Math.max(0, Math.min(x, window.innerWidth - menu.offsetWidth))}px`;
            menu.style.top = `${Math.max(0, Math.min(y, window.innerHeight - menu.offsetHeight))}px`;
            menu.style.display = 'block';
        }

        static showResult(content, x, y) {
            this.ensureElementsExist();
            const contentDiv = resultBox.querySelector('.content');
            contentDiv.innerHTML = content;

            const maxWidth = Math.min(500, window.innerWidth - 40);
            resultBox.style.maxWidth = `${maxWidth}px`;

            let left = Math.max(10, Math.min(x + 10, window.innerWidth - maxWidth - 20));
            let top = Math.max(10, Math.min(y, window.innerHeight - resultBox.offsetHeight - 20));

            resultBox.style.left = `${left}px`;
            resultBox.style.top = `${top}px`;
            resultBox.style.display = 'block';
        }

        static hideAll() {
            menu.style.display = 'none';
            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 };
            }

            // 检查iframe
            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) {}
                }
            } catch (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();

            // 菜单按钮点击
            menu.addEventListener('click', async (e) => {
                const button = e.target.closest('button');
                if (!button) return;

                const action = button.dataset.action;
                const { text } = SelectionManager.getSelectedText();
                if (!text) return;

                await this.handleAction(action, text, e.clientX, e.clientY);
            });

            // 关闭按钮
            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();
            this.observeDynamicContent();
        }

        static async handleAction(action, text, x, y) {
            UIManager.hideAll();
            UIManager.showResult(utils.createLoadingSpinner(), x, y);

            try {
                const response = await APIClient.call(text, action);
                UIManager.showResult(response, x, y);
            } catch (error) {
                UIManager.showResult(`错误: ${error.message}`, x, y);
            }
        }

        static addSelectionListeners(target = document) {
            const handleSelection = utils.debounce((e) => {
                const { text, range } = SelectionManager.getSelectedText();
                if (!text) {
                    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) {}
                }

                UIManager.showMenu(x, y);
            }, CONFIG.DEBOUNCE_DELAY);

            target.addEventListener('mouseup', handleSelection, true);
            target.addEventListener('keyup', handleSelection, true);
            target.addEventListener('selectionchange', handleSelection, true);
        }

        static observeDynamicContent() {
            const observer = new MutationObserver(utils.debounce(() => {
                document.querySelectorAll('iframe').forEach(iframe => {
                    try {
                        if (iframe.contentDocument) {
                            this.addSelectionListeners(iframe.contentDocument);
                        }
                    } catch (e) {}
                });
            }, CONFIG.DEBOUNCE_DELAY));

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        }
    }

    // 初始化
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => EventHandler.init());
    } else {
        EventHandler.init();
    }
})();