Discord Auto Translator (Ctrl+Enter)

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

// ==UserScript==
// @name         Discord Auto Translator (Ctrl+Enter)
// @namespace    http://tampermonkey.net/
// @version      1.2
// @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

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

    function parseSlateNode(node) {
        // 1. 如果是文本节点,直接返回其内容
        if (node.nodeType === Node.TEXT_NODE) {
            return node.textContent;
        }

        // 2. 如果不是元素节点,则忽略
        if (node.nodeType !== Node.ELEMENT_NODE) {
            return '';
        }

        const element = node;

        // 3. 明确忽略 Slate.js 内部使用的 "噪音" 节点
        if (element.hasAttribute('data-slate-zero-width') || element.hasAttribute('data-slate-spacer')) {
            return '';
        }

        // 4. 特殊处理表情符号 (标准和自定义)
        if (element.tagName === 'IMG' && element.classList.contains('emoji') && element.alt) {
            // alt 属性包含了我们需要的文本代码,例如 ":rage:" 或 ":Sydney:"
            return element.alt;
        }

        // 5. 特殊处理剧透 (Spoiler)
        // Discord 使用一个带有 'spoilerText' 类名的 span 来包裹剧透内容
        if (element.matches('[class*="spoilerText"]')) {
            let spoilerContent = '';
            for (const child of element.childNodes) {
                spoilerContent += parseSlateNode(child);
            }
            return `||${spoilerContent}||`;
        }

        // 6. 默认行为:递归处理所有子节点
        // 这会正确处理普通文本、加粗、斜体、以及提及(@user)等容器元素
        let content = '';
        for (const child of element.childNodes) {
            content += parseSlateNode(child);
        }
        return content;
    }

    function extractTextFromDiscordInput() {
        console.log('[Discord Translator] 开始提取输入框内容 (Slate.js 精确版)...');

        // 定位到 Discord 的 Slate 编辑器
        const editor = document.querySelector('.textArea__74017.textAreaSlate__74017 [role="textbox"][data-slate-editor="true"]');
        if (!editor) {
            console.error('[Discord Translator] 找不到 Slate 编辑器');
            return '';
        }

        let result = '';
        // 编辑器中的每一行都是一个直接子元素
        const lines = Array.from(editor.children);

        lines.forEach((lineElement, index) => {
            // 对每一行应用我们精确的解析函数
            result += parseSlateNode(lineElement);

            // 在行与行之间添加换行符,但最后一行后面不加
            if (index < lines.length - 1) {
                result += '\n';
            }
        });

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

    // 调用 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或关注我们的公众号极客氢云获取最新地址