Discord Auto Translator (Ctrl+Enter)

Discord 自动翻译脚本 - Ctrl+Enter 翻译成英文后发送

当前为 2025-06-17 提交的版本,查看 最新版本

// ==UserScript==
// @name         Discord Auto Translator (Ctrl+Enter)
// @namespace    http://tampermonkey.net/
// @version      1.1
// @license MIT
// @description  Discord 自动翻译脚本 - Ctrl+Enter 翻译成英文后发送
// @author       You
// @match        https://discord.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=discord.com
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @connect      openrouter.ai
// ==/UserScript==

(function() {
    'use strict';

    console.log('[Discord Translator] 脚本已加载');

    // ========== 配置部分 ==========
    // 翻译提示词 - 可以根据需要修改
    const TRANSLATION_PROMPT = `You are a professional translator. Translate the following text to English.
Keep the original format, including markdown syntax, mentions (@username), and special Discord formatting.
Only return the translated text without any explanation or additional content.
If the text is already in English, return it as is.`;

    // OpenRouter 配置
    const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1/chat/completions';
    const MODEL = 'google/gemini-2.5-flash-preview-05-20';

    // ========== API Key 管理 ==========
    let apiKey = GM_getValue('openrouter_api_key', '');

    if (!apiKey) {
        apiKey = prompt('[Discord Translator] 请输入您的 OpenRouter API Key:');
        if (apiKey) {
            GM_setValue('openrouter_api_key', apiKey);
            console.log('[Discord Translator] API Key 已保存');
        } else {
            console.error('[Discord Translator] 未提供 API Key,脚本将无法工作');
            alert('未提供 API Key,翻译功能将无法使用!');
        }
    }

    // ========== 状态管理 ==========
    let isTranslating = false;
    let translatedContent = null;
    let shouldTranslate = false;
    let isProgrammaticEnter = false; // 新增:标记是否是程序触发的 Enter

    // ========== 工具函数 ==========

    // 从 Discord 输入框提取纯文本内容
    function extractTextFromDiscordInput() {
        console.log('[Discord Translator] 开始提取输入框内容...');

        const textArea = document.querySelector('.textArea__74017.textAreaSlate__74017 [role="textbox"]');
        if (!textArea) {
            console.error('[Discord Translator] 找不到输入框');
            return '';
        }

        // 获取所有文本内容,保留格式
        let content = '';
        const walker = document.createTreeWalker(
            textArea,
            NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
            null,
            false
        );

        let node;
        while (node = walker.nextNode()) {
            if (node.nodeType === Node.TEXT_NODE) {
                // 文本节点
                const text = node.textContent;
                if (text && !text.match(/^[\u200B\uFEFF\u00A0]+$/)) {
                    content += text;
                }
            } else if (node.nodeType === Node.ELEMENT_NODE) {
                // 元素节点
                if (node.tagName === 'DIV' && node.parentElement === textArea) {
                    // 新行
                    if (content && !content.endsWith('\n')) {
                        content += '\n';
                    }
                } else if (node.classList.contains('wrapper_f61d60') && node.getAttribute('role') === 'button') {
                    // @提及
                    content += node.textContent;
                }
            }
        }

        // 清理末尾的换行
        content = content.replace(/\n+$/, '');

        console.log('[Discord Translator] 提取的内容:', content);
        return content;
    }

    // 调用 OpenRouter API 进行翻译
    async function translateText(text) {
        console.log('[Discord Translator] 开始翻译文本:', text);

        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'POST',
                url: OPENROUTER_BASE_URL,
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${apiKey}`,
                    'HTTP-Referer': 'https://discord.com',
                    'X-Title': 'Discord Auto Translator'
                },
                data: JSON.stringify({
                    model: MODEL,
                    messages: [
                        {
                            role: 'system',
                            content: TRANSLATION_PROMPT
                        },
                        {
                            role: 'user',
                            content: text
                        }
                    ],
                    temperature: 0.3,
                    max_tokens: 4096
                }),
                onload: function(response) {
                    console.log('[Discord Translator] API 响应状态:', response.status);
                    console.log('[Discord Translator] API 响应内容:', response.responseText);

                    if (response.status === 200) {
                        try {
                            const data = JSON.parse(response.responseText);
                            const translatedText = data.choices[0].message.content;
                            console.log('[Discord Translator] 翻译结果:', translatedText);
                            resolve(translatedText);
                        } catch (error) {
                            console.error('[Discord Translator] 解析响应失败:', error);
                            reject(error);
                        }
                    } else {
                        console.error('[Discord Translator] API 请求失败:', response.status, response.responseText);
                        reject(new Error(`API request failed: ${response.status}`));
                    }
                },
                onerror: function(error) {
                    console.error('[Discord Translator] 网络请求失败:', error);
                    reject(error);
                }
            });
        });
    }

    // ========== 请求拦截 ==========

    // 保存原始的 XMLHttpRequest
    const originalXHR = window.XMLHttpRequest;
    const originalOpen = originalXHR.prototype.open;
    const originalSend = originalXHR.prototype.send;

    // 拦截 open 方法
    originalXHR.prototype.open = function(method, url, ...args) {
        this._method = method;
        this._url = url;
        console.log('[Discord Translator] XHR Open:', method, url);
        return originalOpen.apply(this, [method, url, ...args]);
    };

    // 拦截 send 方法
    originalXHR.prototype.send = function(data) {
        console.log('[Discord Translator] XHR Send 被调用, URL:', this._url);

        // 检查是否是发送消息的请求
        if (this._method === 'POST' && this._url && this._url.includes('/api/v9/channels/') && this._url.includes('/messages')) {
            console.log('[Discord Translator] 检测到发送消息请求');
            console.log('[Discord Translator] 原始请求数据:', data);
            console.log('[Discord Translator] shouldTranslate:', shouldTranslate);
            console.log('[Discord Translator] translatedContent:', translatedContent);

            if (shouldTranslate && translatedContent) {
                try {
                    // 解析原始请求数据
                    const requestData = JSON.parse(data);
                    console.log('[Discord Translator] 解析的请求数据:', requestData);

                    // 替换内容为翻译后的内容
                    requestData.content = translatedContent;
                    data = JSON.stringify(requestData);

                    console.log('[Discord Translator] 修改后的请求数据:', data);
                    console.log('[Discord Translator] 成功替换为翻译内容!');

                    // 重置状态 - 在成功发送后才重置
                    shouldTranslate = false;
                    translatedContent = null;
                } catch (error) {
                    console.error('[Discord Translator] 修改请求数据失败:', error);
                }
            }
        }

        return originalSend.apply(this, [data]);
    };

    // ========== 键盘事件处理 ==========

    document.addEventListener('keydown', async function(event) {
        // 检查是否在输入框中
        const target = event.target;
        const isInTextbox = target && target.getAttribute('role') === 'textbox' && target.classList.contains('editor__1b31f');

        if (!isInTextbox) {
            return;
        }

        console.log('[Discord Translator] 键盘事件:', event.key, 'Ctrl:', event.ctrlKey, 'isProgrammatic:', isProgrammaticEnter);

        // Enter 键被按下
        if (event.key === 'Enter') {
            if (event.ctrlKey) {
                // Ctrl+Enter: 翻译后发送
                console.log('[Discord Translator] 检测到 Ctrl+Enter');

                if (!apiKey) {
                    alert('请先设置 API Key!');
                    event.preventDefault();
                    event.stopPropagation();
                    return;
                }

                if (isTranslating) {
                    console.log('[Discord Translator] 正在翻译中,请稍候...');
                    event.preventDefault();
                    event.stopPropagation();
                    return;
                }

                // 提取输入框内容
                const content = extractTextFromDiscordInput();
                if (!content.trim()) {
                    console.log('[Discord Translator] 输入框为空,不进行翻译');
                    return;
                }

                // 阻止默认发送
                event.preventDefault();
                event.stopPropagation();

                // 开始翻译
                isTranslating = true;
                shouldTranslate = true;

                try {
                    console.log('[Discord Translator] 开始翻译过程...');
                    translatedContent = await translateText(content);
                    console.log('[Discord Translator] 翻译完成,准备发送');

                    // 标记下一个 Enter 是程序触发的
                    isProgrammaticEnter = true;

                    // 模拟正常的 Enter 按键来触发发送
                    setTimeout(() => {
                        const newEvent = new KeyboardEvent('keydown', {
                            key: 'Enter',
                            code: 'Enter',
                            keyCode: 13,
                            which: 13,
                            bubbles: true,
                            cancelable: true
                        });
                        target.dispatchEvent(newEvent);

                        // 重置标记
                        setTimeout(() => {
                            isProgrammaticEnter = false;
                        }, 100);
                    }, 100);

                } catch (error) {
                    console.error('[Discord Translator] 翻译失败:', error);
                    alert('翻译失败: ' + error.message);
                    shouldTranslate = false;
                    translatedContent = null;
                } finally {
                    isTranslating = false;
                }

            } else {
                // 仅 Enter: 检查是否是程序触发的
                if (isProgrammaticEnter) {
                    console.log('[Discord Translator] 检测到程序触发的 Enter,保持翻译状态');
                    // 不要重置状态
                } else {
                    console.log('[Discord Translator] 检测到用户按下 Enter(无 Ctrl),正常发送');
                    shouldTranslate = false;
                    translatedContent = null;
                }
            }
        }
    }, true); // 使用捕获阶段

    // ========== 清理 API Key 功能 ==========
    window.clearTranslatorAPIKey = function() {
        GM_setValue('openrouter_api_key', '');
        console.log('[Discord Translator] API Key 已清除');
        alert('API Key 已清除,刷新页面后需要重新输入');
    };

    console.log('[Discord Translator] 脚本初始化完成');
    console.log('[Discord Translator] 提示:在控制台输入 clearTranslatorAPIKey() 可以清除保存的 API Key');
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址