Greasy Fork

Greasy Fork is available in English.

USACO题面翻译

翻译USACO题面,支持云存储。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         USACO题面翻译
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  翻译USACO题面,支持云存储。
// @author       zhoukeyv
// @license      GNU GPLv3
// @match        *://*.usaco.org/index.php?page=viewproblem2*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @connect      api.github.com
// @connect      raw.githubusercontent.com
// @connect      generativelanguage.googleapis.com
// @connect      api.deepseek.com
// @connect      api.openai.com
// @connect      dashscope.aliyuncs.com
// @connect      open.bigmodel.cn
// @connect      api.moonshot.cn
// @connect      api.siliconflow.cn
// @connect      127.0.0.1
// @connect      localhost
// @connect      *
// ==/UserScript==

(function() {
    'use strict';

    const GITHUB_REPO = "zhoukeyv/USACO-translate-storage";
    const ENCODED_TOKEN = "Z2l0aHViX3BhdF8xMUI3Q1dXTVkwdDdaTjN1cFN4UVdHX3pUNnUydG01ZkpzWG02eGVQTUZOejNQZUdyTVV2VXY0aHR6bG15Skh6cXpCU1VaQ1FLWFlrT0lOWTJs";
    const GITHUB_TOKEN = atob(ENCODED_TOKEN);

    const AI_MODELS = {
        "ds-chat": { group: "DeepSeek", name: "DeepSeek V3", protocol: "openai", url: "https://api.deepseek.com/chat/completions", actualModel: "deepseek-chat" },
        "ds-reasoner": { group: "DeepSeek", name: "DeepSeek R1", protocol: "openai", url: "https://api.deepseek.com/chat/completions", actualModel: "deepseek-reasoner" },

        "gemini-3.1-pro": { group: "Gemini", name: "Gemini 3.1 Pro", protocol: "gemini", url: "", actualModel: "gemini-3.1-pro-preview" },
        "gemini-3.1-flash": { group: "Gemini", name: "Gemini 3.1 Flash", protocol: "gemini", url: "", actualModel: "gemini-3.1-flash-preview" },
        "gemini-3.1-flash-lite": { group: "Gemini", name: "Gemini 3.1 Flash-Lite", protocol: "gemini", url: "", actualModel: "gemini-3.1-flash-lite-preview" },
        "gemini-2.5-pro": { group: "Gemini", name: "Gemini 2.5 Pro", protocol: "gemini", url: "", actualModel: "gemini-2.5-pro" },
        "gemini-2.5-flash": { group: "Gemini", name: "Gemini 2.5 Flash", protocol: "gemini", url: "", actualModel: "gemini-2.5-flash" },

        "gpt-4o": { group: "OpenAI", name: "GPT-4o", protocol: "openai", url: "https://api.openai.com/v1/chat/completions", actualModel: "gpt-4o" },
        "gpt-4o-mini": { group: "OpenAI", name: "GPT-4o-mini", protocol: "openai", url: "https://api.openai.com/v1/chat/completions", actualModel: "gpt-4o-mini" },
        "o3-mini": { group: "OpenAI", name: "o3-mini", protocol: "openai", url: "https://api.openai.com/v1/chat/completions", actualModel: "o3-mini" },
        "o1-mini": { group: "OpenAI", name: "o1-mini", protocol: "openai", url: "https://api.openai.com/v1/chat/completions", actualModel: "o1-mini" },

        "qwen-max": { group: "通义千问", name: "Qwen Max", protocol: "openai", url: "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions", actualModel: "qwen-max" },
        "qwen-plus": { group: "通义千问", name: "Qwen Plus", protocol: "openai", url: "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions", actualModel: "qwen-plus" },
        "qwen-turbo": { group: "通义千问", name: "Qwen Turbo", protocol: "openai", url: "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions", actualModel: "qwen-turbo" },

        "glm-4-plus": { group: "智谱GLM", name: "GLM-4 Plus", protocol: "openai", url: "https://open.bigmodel.cn/api/paas/v4/chat/completions", actualModel: "glm-4-plus" },
        "glm-4-air": { group: "智谱GLM", name: "GLM-4 Air", protocol: "openai", url: "https://open.bigmodel.cn/api/paas/v4/chat/completions", actualModel: "glm-4-air" },
        "glm-4-flash": { group: "智谱GLM", name: "GLM-4 Flash", protocol: "openai", url: "https://open.bigmodel.cn/api/paas/v4/chat/completions", actualModel: "glm-4-flash" },

        "kimi-8k": { group: "Kimi", name: "Moonshot v1 8K", protocol: "openai", url: "https://api.moonshot.cn/v1/chat/completions", actualModel: "moonshot-v1-8k" },
        "kimi-32k": { group: "Kimi", name: "Moonshot v1 32K", protocol: "openai", url: "https://api.moonshot.cn/v1/chat/completions", actualModel: "moonshot-v1-32k" },
        "kimi-128k": { group: "Kimi", name: "Moonshot v1 128K", protocol: "openai", url: "https://api.moonshot.cn/v1/chat/completions", actualModel: "moonshot-v1-128k" },

        "sf-ds-v3": { group: "硅基流动", name: "DeepSeek V3", protocol: "openai", url: "https://api.siliconflow.cn/v1/chat/completions", actualModel: "deepseek-ai/DeepSeek-V3" },
        "sf-ds-r1": { group: "硅基流动", name: "DeepSeek R1", protocol: "openai", url: "https://api.siliconflow.cn/v1/chat/completions", actualModel: "deepseek-ai/DeepSeek-R1" },
        "sf-qwen-72b": { group: "硅基流动", name: "Qwen 2.5 72B Instruct", protocol: "openai", url: "https://api.siliconflow.cn/v1/chat/completions", actualModel: "Qwen/Qwen2.5-72B-Instruct" },
        "sf-llama-3": { group: "硅基流动", name: "Llama 3.3 70B", protocol: "openai", url: "https://api.siliconflow.cn/v1/chat/completions", actualModel: "meta-llama/Llama-3.3-70B-Instruct" },

        "custom": { group: "高级选项", name: "自定义模型", protocol: "custom", url: "", actualModel: "" }
    };

    let savedPresetId = GM_getValue("ai_preset_id", "gemini-3.1-pro");
    if (!AI_MODELS[savedPresetId]) savedPresetId = "gemini-3.1-pro";

    // 【旧版API Key迁移逻辑】无缝将旧全局Key绑定到当前选择的模型上
    let oldUsacoKey = GM_getValue("gemini_api_key", "");
    if (oldUsacoKey && !GM_getValue(`ai_api_key_${savedPresetId}`, "")) {
        GM_setValue(`ai_api_key_${savedPresetId}`, oldUsacoKey);
        GM_setValue("gemini_api_key", "");
    }

    let customBaseUrl = GM_getValue("ai_custom_url", "http://127.0.0.1:11434/v1/chat/completions");
    let customModelName = GM_getValue("ai_custom_model", "");
    let enableLocalCache = GM_getValue("usaco_enable_cache", true);

    function gmFetch(url, options) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: options.method || 'GET',
                url: url,
                headers: options.headers || {},
                data: options.body,
                onload: function(response) {
                    const isOk = response.status >= 200 && response.status < 300;
                    resolve({
                        ok: isOk,
                        status: response.status,
                        json: async () => {
                            try { return JSON.parse(response.responseText); }
                            catch(e) { return { error: { message: response.responseText || "Unknown Error" } }; }
                        }
                    });
                },
                onerror: function(err) { reject(new Error("网络请求被拦截或失败,请检查网络")); },
                ontimeout: function() { reject(new Error("网络请求超时")); }
            });
        });
    }

    function injectSettingsUI() {
        const fab = document.createElement('button');
        fab.className = 'btn btn-default';
        fab.textContent = '翻译设置';

        const savedLeft = GM_getValue("usaco_fab_left", "20px");
        const savedTop = GM_getValue("usaco_fab_top", "auto");
        const savedBottom = GM_getValue("usaco_fab_bottom", "20px");

        fab.style.cssText = `position: fixed; left: ${savedLeft}; top: ${savedTop}; bottom: ${savedBottom}; z-index: 9998; box-shadow: 0 4px 10px rgba(0,0,0,0.2); cursor: pointer; user-select: none; background: #2b3e50; color: white; border: none; outline: none; padding: 8px 15px; border-radius: 20px; font-weight: bold; transition: background 0.3s;`;
        fab.onfocus = () => fab.blur();
        fab.onmouseenter = () => fab.style.background = '#1a252f';
        fab.onmouseleave = () => fab.style.background = '#2b3e50';

        let isDragging = false, hasDragged = false, startX, startY, startLeft, startTop;
        fab.addEventListener('mousedown', (e) => {
            isDragging = true; hasDragged = false; startX = e.clientX; startY = e.clientY;
            startLeft = fab.getBoundingClientRect().left; startTop = fab.getBoundingClientRect().top;
            fab.style.bottom = 'auto'; fab.style.right = 'auto'; fab.style.left = startLeft + 'px'; fab.style.top = startTop + 'px';
            fab.style.cursor = 'grabbing';
        });
        document.addEventListener('mousemove', (e) => {
            if (!isDragging) return;
            const dx = e.clientX - startX, dy = e.clientY - startY;
            if (Math.abs(dx) > 3 || Math.abs(dy) > 3) hasDragged = true;
            fab.style.left = (startLeft + dx) + 'px'; fab.style.top = (startTop + dy) + 'px';
        });
        document.addEventListener('mouseup', () => {
            if (isDragging) {
                isDragging = false; fab.style.cursor = 'pointer';
                GM_setValue('usaco_fab_left', fab.style.left); GM_setValue('usaco_fab_top', fab.style.top); GM_setValue('usaco_fab_bottom', 'auto');
            }
        });

        let groups = {};
        for (const [id, info] of Object.entries(AI_MODELS)) {
            if (!groups[info.group]) groups[info.group] = '';
            groups[info.group] += `<option value="${id}">${info.name}</option>`;
        }
        let optionsHtml = '';
        for (const [groupName, options] of Object.entries(groups)) {
            optionsHtml += `<optgroup label="—— ${groupName} ——">${options}</optgroup>`;
        }

        const modalOverlay = document.createElement('div');
        modalOverlay.id = 'gemini-settings-overlay';
        modalOverlay.style.cssText = `display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 9999; justify-content: center; align-items: flex-start; padding-top: 10vh; overflow-y: auto;`;
        modalOverlay.innerHTML = `
            <div style="background: #fff; padding: 25px; border-radius: 8px; width: 450px; max-width: 90%; box-shadow: 0 10px 30px rgba(0,0,0,0.2); border: 1px solid #ccc; font-family: sans-serif; margin-bottom: 50px;">
                <h4 style="margin-top: 0; color: #333; border-bottom: 2px solid #2b3e50; padding-bottom: 10px; font-weight: bold;">AI 翻译配置</h4>

                <div style="margin-bottom: 15px;">
                    <label style="display: block; font-weight: bold; margin-bottom: 5px; color: #555; font-size: 13px;">请选择 AI 模型</label>
                    <select id="ai-input-preset" style="width: 100%; padding: 8px 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; cursor: pointer; font-size: 14px; background: #fdfdfd;">
                        ${optionsHtml}
                    </select>
                </div>

                <div id="ai-custom-fields" style="display: none; padding: 10px; background: #f5f7fa; border: 1px dashed #ccc; border-radius: 6px; margin-bottom: 15px;">
                    <div style="margin-bottom: 10px;">
                        <label style="display: block; font-weight: bold; margin-bottom: 3px; color: #555; font-size: 12px;">自定义接口地址</label>
                        <input type="text" id="ai-input-customurl" autocomplete="off" style="width: 100%; padding: 5px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; font-family: monospace; font-size: 12px;">
                    </div>
                    <div>
                        <label style="display: block; font-weight: bold; margin-bottom: 3px; color: #555; font-size: 12px;">自定义底层模型名称</label>
                        <input type="text" id="ai-input-custommodel" autocomplete="off" style="width: 100%; padding: 5px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; font-family: monospace; font-size: 12px;">
                    </div>
                </div>

                <div style="margin-bottom: 15px;">
                    <label style="display: block; font-weight: bold; margin-bottom: 5px; color: #555; font-size: 13px;">API Key <span style="color:#888; font-weight:normal; font-size:12px;">(独立保存)</span></label>
                    <input type="text" id="ai-input-key" autocomplete="off" spellcheck="false" style="width: 100%; padding: 8px 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; -webkit-text-security: disc; font-family: monospace;" placeholder="请输入该模型的 API Key">
                </div>

                <div style="background: #f9f9f9; padding: 10px; border-radius: 6px; border: 1px solid #eee; margin-bottom: 25px;">
                    <label style="display: flex; align-items: center; color: #333; cursor: pointer; font-size: 13px;">
                        <input type="checkbox" id="ai-input-cache" style="margin-right: 8px;">启用本地缓存
                    </label>
                </div>

                <div style="display: flex; justify-content: flex-end; gap: 10px;">
                    <button id="ai-btn-cancel" class="btn btn-default btn-sm" style="padding:6px 15px; outline:none;">取消</button>
                    <button id="ai-btn-save" class="btn btn-success btn-sm" style="padding:6px 15px; font-weight: bold; background: #2b3e50; border: none; outline:none;">保存配置</button>
                </div>
            </div>`;

        document.body.appendChild(fab); document.body.appendChild(modalOverlay);

        const selPreset = document.getElementById('ai-input-preset');
        const customFields = document.getElementById('ai-custom-fields');
        const inputKey = document.getElementById('ai-input-key');

        // 模型切换时,自动填充对应的 API Key
        selPreset.addEventListener('change', (e) => {
            const preset = e.target.value;
            if (preset === 'custom') customFields.style.display = 'block';
            else customFields.style.display = 'none';
            inputKey.value = GM_getValue(`ai_api_key_${preset}`, "");
        });

        fab.addEventListener('click', () => {
            if (hasDragged) return;
            selPreset.value = savedPresetId;
            document.getElementById('ai-input-customurl').value = customBaseUrl;
            document.getElementById('ai-input-custommodel').value = customModelName;
            document.getElementById('ai-input-cache').checked = enableLocalCache;
            // 触发 change 事件以自动加载对应 Key
            selPreset.dispatchEvent(new Event('change'));
            modalOverlay.style.display = 'flex';
        });

        document.getElementById('ai-btn-cancel').onclick = function() { this.blur(); modalOverlay.style.display = 'none'; };
        document.getElementById('ai-btn-save').onclick = function() {
            this.blur();
            const selectedPreset = selPreset.value;
            GM_setValue("ai_preset_id", selectedPreset);
            GM_setValue("ai_custom_url", document.getElementById('ai-input-customurl').value.trim());
            GM_setValue("ai_custom_model", document.getElementById('ai-input-custommodel').value.trim());
            
            // 绑定保存当前的 API Key 到所选模型
            GM_setValue(`ai_api_key_${selectedPreset}`, inputKey.value.trim());
            
            GM_setValue("usaco_enable_cache", document.getElementById('ai-input-cache').checked);
            location.reload();
        };
    }

    function cleanHTMLForAI(node) {
        const visualMathClasses = ['.MathJax_Preview', '.MathJax', '.MathJax_Display', '.mjx-chtml', '.mjx-container', '.katex-html', '.base'];
        node.querySelectorAll(visualMathClasses.join(', ')).forEach(el => el.remove());
        node.querySelectorAll('script[type^="math/tex"], annotation').forEach(el => {
            let tex = el.textContent.replace(/^\s*\$+|\$+\s*$/g, '').trim();
            const varEl = document.createElement('var'); varEl.textContent = tex;
            if (el.tagName.toLowerCase() === 'annotation') {
                const mathWrapper = el.closest('math') || el.closest('.katex') || el.closest('span');
                if (mathWrapper && mathWrapper.parentNode) { mathWrapper.parentNode.insertBefore(varEl, mathWrapper); mathWrapper.remove(); }
            } else { el.parentNode.insertBefore(varEl, el); el.remove(); }
        });
        node.querySelectorAll('[style*="display: none"], .hidden').forEach(el => el.remove());
        return node.innerHTML.trim();
    }

    function getProblemFileName() {
        const match = location.href.match(/cpid=(\d+)/);
        if (match) return `cpid_${match[1]}.html`;
        const title = document.querySelector('.prb-title') || document.querySelector('h2');
        let hash = btoa(unescape(encodeURIComponent(title ? title.textContent : location.href))).replace(/[^a-zA-Z0-9]/g, '');
        return `custom_${hash.slice(0, 20)}.html`;
    }

    function utf8_to_b64(str) { return btoa(unescape(encodeURIComponent(str))); }
    function b64_to_utf8(str) { return decodeURIComponent(escape(atob(str))); }

    async function fetchFromGitHub(fileName) {
        if (enableLocalCache) {
            let localCache = GM_getValue("usaco_local_" + fileName, null);
            if (localCache) return { html: localCache, source: '翻译结果' };
        }
        const apiUrl = `https://api.github.com/repos/${GITHUB_REPO}/contents/${fileName}`;
        const headers = { 'Accept': 'application/vnd.github+json', 'Authorization': `Bearer ${GITHUB_TOKEN}`, 'X-GitHub-Api-Version': '2022-11-28' };
        try {
            const res = await gmFetch(apiUrl, { headers });
            if (res.ok) {
                const data = await res.json();
                const htmlContent = b64_to_utf8(data.content.replace(/\n/g, ''));
                if (enableLocalCache) GM_setValue("usaco_local_" + fileName, htmlContent);
                return { html: htmlContent, source: '翻译结果' };
            }
        } catch(e) {}
        return null;
    }

    async function pushToGitHub(fileName, htmlContent) {
        const apiUrl = `https://api.github.com/repos/${GITHUB_REPO}/contents/${fileName}`;
        const headers = { 'Accept': 'application/vnd.github+json', 'Authorization': `Bearer ${GITHUB_TOKEN}`, 'X-GitHub-Api-Version': '2022-11-28' };
        let fileSha = null;
        try { const getRes = await gmFetch(apiUrl, { headers }); if (getRes.ok) fileSha = (await getRes.json()).sha; } catch (e) {}
        const body = { message: `Auto Translate: ${fileName}`, content: utf8_to_b64(htmlContent), branch: 'main' };
        if (fileSha) body.sha = fileSha;
        const putRes = await gmFetch(apiUrl, { method: 'PUT', headers, body: JSON.stringify(body) });
        if (!putRes.ok) throw new Error("GitHub HTTP " + putRes.status);
    }

    const addUSACOTranslateButton = () => {
        let titleContainer = document.querySelector('.prb-title') || document.querySelector('h2');
        if (!titleContainer || titleContainer.querySelector('.gemini-trans-btn')) return;

        const btn = document.createElement('button');
        btn.className = 'gemini-trans-btn';
        btn.style.cssText = 'margin-left: 15px; color: #fff; border: 1px solid #4cae4c; background-color: #5cb85c; font-weight: bold; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 14px; outline: none !important; transition: all 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.1);';
        btn.textContent = '翻译';

        btn.onmouseenter = () => { btn.style.backgroundColor = '#449d44'; };
        btn.onmouseleave = () => { btn.style.backgroundColor = '#5cb85c'; };

        let contentContainer = document.querySelector('.problem-text') || titleContainer.parentElement;
        btn.onclick = (e) => {
            e.preventDefault();
            btn.blur();
            startTranslationFlow(contentContainer, btn, btn.textContent === '重新翻译');
        };

        const h2 = titleContainer.tagName === 'H2' ? titleContainer : titleContainer.querySelector('h2');
        if (h2) { btn.style.verticalAlign = 'middle'; h2.appendChild(btn); } else { titleContainer.appendChild(btn); }
    };

    function renderTranslatedUI(section, btn, translatedHTML, sourceStr) {
        let safeText = translatedHTML.replace(/(<[^>]+>|\$\$[\s\S]*?\$\$|\$[^\$\n]+\$|\\\[[\s\S]*?\\\]|\\\([\s\S]*?\\\))/g, (m) => m.replace(/\*/g, '___STAR___'));
        safeText = safeText.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>').replace(/\*([^*]+)\*/g, '<em>$1</em>').replace(/`([^`]+)`/g, '<code>$1</code>');
        const renderHTML = safeText.replace(/___STAR___/g, '*');

        let transContainer = section.querySelector('.translated-zh-section');
        if (!transContainer) {
            transContainer = document.createElement('div');
            transContainer.className = 'translated-zh-section';
            transContainer.style.cssText = 'margin-top: 15px; margin-bottom: 20px; padding: 15px; background-color: #fcfcfc; border: 1px solid #ddd; border-left: 4px solid #5cb85c; border-radius: 4px; position: relative; color: #333; box-shadow: 0 2px 5px rgba(0,0,0,0.05);';
            section.insertBefore(transContainer, section.firstChild);
        }

        transContainer.innerHTML = `
            <div style="font-weight: bold; color: #0366d6; margin-bottom: 10px; border-bottom: 1px dashed #eee; padding-bottom: 5px; display:flex; justify-content: space-between; align-items:center;">
                <span class="source-status-text">${sourceStr}</span>
                <button class="gemini-copy-btn" style="padding: 4px 10px; font-size: 12px; cursor: pointer; border-radius: 4px; border: 1px solid #d0d7de !important; background-color: #f6f8fa; color: #24292f; outline: none !important; box-shadow: 0 1px 2px rgba(0,0,0,0.05) !important; transition: 0.2s;">复制 Markdown</button>
            </div>
            <div class="gemini-trans-content" style="font-size: 14px; line-height: 1.6;">${renderHTML}</div>
        `;

        const copyBtn = transContainer.querySelector('.gemini-copy-btn');
        copyBtn.onmouseenter = () => { copyBtn.style.backgroundColor = '#eef1f4'; copyBtn.style.borderColor = '#9097a3'; };
        copyBtn.onmouseleave = () => { copyBtn.style.backgroundColor = '#f6f8fa'; copyBtn.style.borderColor = '#d0d7de'; };

        copyBtn.onclick = function() {
            let mkd = translatedHTML.replace(/<var>([\s\S]*?)<\/var>/gi, (m, i) => '$' + i.replace(/<[^>]+>/g, '').replace(/^\s*\$+|\$+\s*$/g, '').trim() + '$');
            mkd = mkd.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, '\n```text\n$1\n```\n').replace(/<strong>(.*?)<\/strong>/gi, '**$1**').replace(/<em>(.*?)<\/em>/gi, '*$1*').replace(/<code>(.*?)<\/code>/gi, '`$1`');
            mkd = mkd.replace(/<br\s*\/?>/gi, '\n').replace(/<\/p>/gi, '\n\n').replace(/<li>/gi, '- ').replace(/<\/li>/gi, '\n').replace(/<\/h[1-6]>/gi, '\n\n').replace(/<[^>]+>/g, '');
            const ta = document.createElement('textarea'); ta.innerHTML = mkd;
            navigator.clipboard.writeText(ta.value.replace(/\n{3,}/g, '\n\n').trim()).then(() => {
                copyBtn.textContent = '已复制!';
                copyBtn.style.backgroundColor = '#d4edda';
                setTimeout(() => {
                    copyBtn.textContent = '复制 Markdown';
                    copyBtn.style.backgroundColor = '#f6f8fa';
                }, 2000);
            });
        };

        const contentDiv = transContainer.querySelector('.gemini-trans-content');

        contentDiv.querySelectorAll('pre').forEach(pre => {
            const wrapper = document.createElement('div');
            wrapper.style.cssText = 'margin: 15px 0; background-color: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; overflow: hidden;';
            pre.parentNode.insertBefore(wrapper, pre);
            const header = document.createElement('div');
            header.style.cssText = 'display: flex; justify-content: flex-end; padding: 4px 8px; background-color: #eaedf0; border-bottom: 1px solid #d0d7de;';
            const preCopyBtn = document.createElement('button');
            preCopyBtn.textContent = '复制';
            preCopyBtn.style.cssText = 'padding: 2px 8px; font-size: 12px; cursor: pointer; outline: none !important; box-shadow: 0 1px 2px rgba(0,0,0,0.05) !important; border-radius: 4px; background: #ffffff; border: 1px solid #d0d7de !important; color: #656d76; transition: all 0.2s;';

            preCopyBtn.onmouseenter = () => { preCopyBtn.style.backgroundColor = '#f0f3f6'; preCopyBtn.style.borderColor = '#9097a3'; };
            preCopyBtn.onmouseleave = () => { preCopyBtn.style.backgroundColor = '#ffffff'; preCopyBtn.style.borderColor = '#d0d7de'; };

            preCopyBtn.onclick = function() {
                let textToCopy = pre.innerText || pre.textContent;
                navigator.clipboard.writeText(textToCopy.trim()).then(() => {
                    preCopyBtn.textContent = '已复制!';
                    preCopyBtn.style.backgroundColor = '#d4edda';
                    setTimeout(() => {
                        preCopyBtn.textContent = '复制';
                        preCopyBtn.style.backgroundColor = '#ffffff';
                    }, 2000);
                });
            };

            // 组装 HTML 结构
            header.appendChild(preCopyBtn);
            wrapper.appendChild(header);
            wrapper.appendChild(pre);

            pre.style.cssText = 'margin: 0 !important; padding: 12px 15px !important; display: block !important; overflow-x: auto !important; background: transparent !important; border: none !important; font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace !important; font-size: 13px !important; color: #1f2328 !important; line-height: 1.5 !important;';
        });

        if (typeof MathJax !== 'undefined') MathJax.Hub.Queue(["Typeset", MathJax.Hub, contentDiv]);
        btn.textContent = '重新翻译'; btn.disabled = false;
    }

    async function startTranslationFlow(section, btn, forceAI = false) {
        try {
            const fileName = getProblemFileName();

            if (!forceAI) {
                btn.textContent = '查询题库...'; btn.disabled = true;
                const cloudData = await fetchFromGitHub(fileName);
                if (cloudData) { renderTranslatedUI(section, btn, cloudData.html, cloudData.source); return; }
            }

            // 获取当前选择模型的独立 API Key
            const currentApiKey = GM_getValue(`ai_api_key_${savedPresetId}`, "");
            if (!currentApiKey) {
                btn.textContent = '翻译'; btn.disabled = false;
                alert(`无可用翻译记录!请点击【翻译设置】,为当前模型配置对应的 API Key!`);
                return;
            }

            btn.textContent = '翻译中...'; btn.disabled = true;
            const clone = section.cloneNode(true);
            if (clone.querySelector('.translated-zh-section')) clone.querySelector('.translated-zh-section').remove();
            cleanHTMLForAI(clone);

            const preBlocks = [];
            clone.querySelectorAll('pre').forEach((pre, index) => { preBlocks.push(pre.outerHTML); const placeholder = document.createElement('div'); placeholder.id = `gemini-pre-placeholder-${index}`; placeholder.textContent = `[PRE_BLOCK_${index}]`; pre.parentNode.replaceChild(placeholder, pre); });

            const htmlToTranslate = clone.innerHTML.trim();
            if (!htmlToTranslate) { btn.textContent = '无内容'; btn.disabled = false; return; }

            let rawAIResponse = await callAIEngine(htmlToTranslate, currentApiKey);

            let translatedHTML = rawAIResponse.replace(/^```(?:html)?\s*/i, '').replace(/\s*```$/i, '').trim();

            preBlocks.forEach((preHTML, index) => {
                const regex = new RegExp(`<div[^>]*id="gemini-pre-placeholder-${index}"[^>]*>.*?</div\\s*>`, 'gi');
                const textRegex = new RegExp(`\\[PRE_BLOCK_${index}\\]`, 'gi');
                if (regex.test(translatedHTML)) translatedHTML = translatedHTML.replace(regex, preHTML); else translatedHTML = translatedHTML.replace(textRegex, preHTML);
            });

            if (enableLocalCache) GM_setValue("usaco_local_" + fileName, translatedHTML);
            renderTranslatedUI(section, btn, translatedHTML, forceAI ? '翻译结果' : '翻译结果');

            pushToGitHub(fileName, translatedHTML).catch(e => {
                const statusSpan = section.querySelector('.source-status-text');
                if (statusSpan) statusSpan.innerHTML += ' <span style="color:red; font-size:12px;">(⚠️ 云端同步失败)</span>';
            });

        } catch (error) {
            console.error(error);
            btn.textContent = '翻译失败'; btn.disabled = false;
            alert("API 报错: " + error.message);
        }
    }

    async function callAIEngine(htmlText, apiKey) {
        let prompt = `你是一名资深的 USACO 算法竞赛教练,你的任务是将给定的英文 HTML 题目完美意译为符合中文算法选手阅读习惯的题面。
        【🎯 核心语感】消除欧式句式。条件前置。术语规范:Subsequence -> 子序列 | Substring -> 子串 | Permutation -> 排列 | modulo -> 取模。保留 Farmer John 等原题设定。
        【🛡️ 公式排版与清洗】原 HTML 中可能存在渲染废料(如 xx, NN 重复)。请你主动剔除乱码废料,只保留真正的数学源码,并转换为 Markdown 格式 ($N$)。保留 [PRE_BLOCK_X] 占位符。
        【⚠️ 严格指令】请直接输出最终的纯净 HTML 代码,不要输出任何代码块标记(如 \`\`\`html),也绝对不要输出任何思考过程、解释说明或 <think> 标签!`;

        const modelInfo = AI_MODELS[savedPresetId] || AI_MODELS['gemini-3.1-pro'];
        let result = "";

        let finalUrl = modelInfo.url;
        let finalModel = modelInfo.actualModel;

        if (savedPresetId === 'custom') {
            finalUrl = customBaseUrl;
            finalModel = customModelName;
        }

        if (modelInfo.protocol === "openai" || savedPresetId === "custom") {
            if (!finalUrl) throw new Error("自定义接口地址为空!");

            const response = await gmFetch(finalUrl, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
                body: JSON.stringify({
                    model: finalModel,
                    messages: [
                        { role: "system", content: "你是一个专业的 USACO 算法竞赛翻译助手。" },
                        { role: "user", content: prompt + `\n\n【原文】:\n${htmlText}` }
                    ],
                    temperature: 0.4
                })
            });
            const data = await response.json();
            if (!response.ok) throw new Error(data.error?.message || "OpenAI / 第三方 API 请求失败");

            result = data.choices[0].message.content;

        } else {
            const url = `https://generativelanguage.googleapis.com/v1beta/models/${finalModel}:generateContent?key=${apiKey}`;
            const response = await gmFetch(url, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ contents: [{ parts: [{ text: prompt + `\n\n【原文】:\n${htmlText}` }] }], generationConfig: { temperature: 0.4 } })
            });
            const data = await response.json();
            if (!response.ok) throw new Error(data.error?.message || "Gemini API 请求失败");
            const candidate = data.candidates?.[0];
            if (!candidate?.content) throw new Error(`已被 Gemini 安全策略拦截 (原因: ${candidate?.finishReason})。`);
            result = candidate.content.parts[0].text;
        }

        return result;
    }

    injectSettingsUI();
    setTimeout(addUSACOTranslateButton, 1000);
})();