Greasy Fork

Greasy Fork is available in English.

Universal DeepSeek Text Selection

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

当前为 2025-01-09 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
    }
})();