Greasy Fork is available in English.
翻译USACO题面,支持云存储。
// ==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);
})();