// ==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 * // @downloadURL https://update.greasyfork.icu/scripts/573025/USACO%E9%A2%98%E9%9D%A2%E7%BF%BB%E8%AF%91.user.js // @updateURL https://update.greasyfork.icu/scripts/573025/USACO%E9%A2%98%E9%9D%A2%E7%BF%BB%E8%AF%91.meta.js // ==/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] += ``; } let optionsHtml = ''; for (const [groupName, options] of Object.entries(groups)) { optionsHtml += ``; } 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 = `
$1');
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 = `
]*>([\s\S]*?)<\/pre>/gi, '\n```text\n$1\n```\n').replace(/(.*?)<\/strong>/gi, '**$1**').replace(/(.*?)<\/em>/gi, '*$1*').replace(/(.*?)<\/code>/gi, '`$1`');
mkd = mkd.replace(/
/gi, '\n').replace(/<\/p>/gi, '\n\n').replace(//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(`]*id="gemini-pre-placeholder-${index}"[^>]*>.*?`, '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 += ' (⚠️ 云端同步失败)';
});
} 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),也绝对不要输出任何思考过程、解释说明或 标签!`;
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);
})();