1 line
200 KiB
JSON
1 line
200 KiB
JSON
{"repo": "7452323/quantumultx", "files": {"Userscripts/omnily.user.js": {"74b5252862202e0aec4a4f95dd4c4a101b9b50a2": {"t": 1785746592.0058243, "n": 30448, "h": "51d6f28f6cd0b047", "c": "// ==UserScript==\n// @name 网页翻译 · 多引擎\n// @namespace http://tampermonkey.net/\n// @version 1.5\n// @description 点击翻译整个页面/恢复原文。支持Google(免费)/百度通用(极速)/百度大模型(AI)/DeepSeek(高质量),双语/单语模式,翻译结果缓存。长按进入配置。\n// @author you\n// @match *://*/*\n// @icon data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌐</text></svg>\n// @grant GM_setValue\n// @grant GM_getValue\n// @grant GM_addStyle\n// @grant GM_xmlhttpRequest\n// @license MIT\n// ==/UserScript==\n\n(function () {\n \"use strict\";\n\n // ====== 配置 ======\n const cfg = {\n engine: GM_getValue(\"ds_engine\", \"google\"),\n mode: GM_getValue(\"ds_mode\", \"bilingual\"),\n // 百度通用翻译 (appid + secretKey)\n baiduAppId: GM_getValue(\"ds_baidu_id\", \"\"),\n baiduKey: GM_getValue(\"ds_baidu_key\", \"\"),\n // 百度大模型翻译 (只有一串 APP ID)\n baiduLlmAppId: GM_getValue(\"ds_baidu_llm_id\", \"\"),\n // DeepSeek\n dsKey: GM_getValue(\"ds_api_key\", \"\"),\n dsModel: GM_getValue(\"ds_model\", \"deepseek-chat\"),\n };\n\n // ====== 状态 ======\n let translated = false;\n let translating = false;\n let stopRequested = false;\n let originalTexts = null;\n const transCache = new Map();\n\n // ====== 样式 ======\n GM_addStyle(`\n #ds-fab {\n position: fixed; bottom: 24px; right: 24px; z-index: 999999;\n width: 52px; height: 52px; border-radius: 50%; background: #4f6ef7;\n color: #fff; border: none; font-size: 24px; cursor: pointer;\n box-shadow: 0 2px 12px rgba(79,110,247,0.4);\n transition: transform 0.15s, background 0.3s;\n display: flex; align-items: center; justify-content: center;\n user-select: none; -webkit-user-select: none;\n }\n #ds-fab:active { transform: scale(0.92); }\n #ds-fab.translated { background: #a6e3a1; }\n\n #ds-panel {\n position: fixed; bottom: 88px; right: 24px; z-index: 999999;\n width: 320px; max-height: 70vh; overflow-y: auto;\n background: #1e1e2e; border-radius: 16px; padding: 20px;\n box-shadow: 0 8px 40px rgba(0,0,0,0.5); color: #cdd6f4;\n font: 14px -apple-system, sans-serif; display: none; border: 1px solid #313244;\n }\n #ds-panel.show { display: block; }\n #ds-panel h3 {\n margin: 0 0 16px 0; font-size: 16px; color: #cba6f7;\n display: flex; justify-content: space-between; align-items: center;\n }\n #ds-panel h3 button { background: none; border: none; color: #a6adc8; cursor: pointer; font-size: 18px; padding: 0 4px; }\n #ds-panel h3 button:hover { color: #f38ba8; }\n #ds-panel label { display: block; margin-bottom: 4px; color: #a6adc8; font-size: 12px; margin-top: 8px; }\n #ds-panel label:first-of-type { margin-top: 0; }\n #ds-panel input, #ds-panel select {\n width: 100%; padding: 8px 12px; margin-bottom: 4px;\n background: #313244; border: 1px solid #45475a; border-radius: 8px;\n color: #cdd6f4; font-size: 13px; box-sizing: border-box; outline: none;\n }\n #ds-panel input:focus, #ds-panel select:focus { border-color: #4f6ef7; }\n #ds-panel .hint { font-size: 11px; color: #6c7086; margin: 2px 0 8px 0; }\n\n #ds-panel .engine-tab {\n display: flex; gap: 6px; margin-bottom: 6px;\n }\n #ds-panel .engine-tab button {\n flex: 1; padding: 7px; border: 1px solid #45475a; border-radius: 8px;\n background: #313244; color: #a6adc8; cursor: pointer; font-size: 12px; transition: all 0.2s;\n }\n #ds-panel .engine-tab button.active { background: #4f6ef7; border-color: #4f6ef7; color: #fff; }\n\n #ds-panel .baidu-subtab {\n display: flex; gap: 6px; margin: 6px 0 10px 0;\n }\n #ds-panel .baidu-subtab button {\n flex: 1; padding: 5px; border: 1px solid #45475a; border-radius: 6px;\n background: #313244; color: #a6adc8; cursor: pointer; font-size: 11px; transition: all 0.2s;\n }\n #ds-panel .baidu-subtab button.active { background: #4f6ef7; border-color: #4f6ef7; color: #fff; }\n\n #ds-panel .mode-tab {\n display: flex; gap: 6px; margin-bottom: 12px;\n }\n #ds-panel .mode-tab button {\n flex: 1; padding: 6px; border: 1px solid #45475a; border-radius: 8px;\n background: #313244; color: #a6adc8; cursor: pointer; font-size: 12px; transition: all 0.2s;\n }\n #ds-panel .mode-tab button.active { background: #585b70; border-color: #585b70; color: #fff; }\n\n #ds-panel .cfg-section { display: none; }\n #ds-panel .cfg-section.show { display: block; }\n #ds-panel .btn-row { display: flex; gap: 8px; margin-top: 12px; }\n #ds-panel .btn-row button {\n flex: 1; padding: 9px; border: none; border-radius: 8px;\n cursor: pointer; font-size: 13px; transition: opacity 0.2s;\n }\n #ds-panel .btn-row button:active { opacity: 0.8; }\n #ds-panel .btn-save { background: #4f6ef7; color: #fff; }\n #ds-panel .btn-close-cfg { background: #45475a; color: #cdd6f4; }\n #ds-panel .btn-clear-cache { background: #45475a; color: #f38ba8; }\n\n #ds-progress { margin-top: 10px; display: none; }\n #ds-progress.show { display: block; }\n #ds-progress .bar-wrap { width: 100%; height: 4px; background: #313244; border-radius: 2px; overflow: hidden; }\n #ds-progress .bar { height: 100%; width: 0%; background: #a6e3a1; border-radius: 2px; transition: width 0.3s; }\n #ds-progress .text { font-size: 11px; color: #a6adc8; margin-top: 4px; text-align: center; }\n\n #ds-toast {\n position: fixed; top: 20px; left: 50%; transform: translateX(-50%);\n z-index: 9999999; padding: 10px 24px; border-radius: 10px;\n font: 14px -apple-system, sans-serif; color: #fff;\n display: none; box-shadow: 0 4px 20px rgba(0,0,0,0.3);\n }\n #ds-toast.ok { background: #1e3a2e; }\n #ds-toast.err { background: #3a1e1e; }\n #ds-toast.info { background: #1e2a3a; }\n `);\n\n // ====== DOM ======\n document.body.insertAdjacentHTML(\"beforeend\", `\n <button id=\"ds-fab\">🌐</button>\n <div id=\"ds-panel\">\n <h3>\n ⚙️ 翻译设置\n <button id=\"ds-close-panel\">✕</button>\n </h3>\n\n <label>翻译引擎</label>\n <div class=\"engine-tab\">\n <button data-engine=\"google\" class=\"active\">🌍 Google</button>\n <button data-engine=\"baidu\">⚡ 百度</button>\n <button data-engine=\"deepseek\">🧠 DeepSeek</button>\n </div>\n\n <div class=\"cfg-section show\" id=\"ds-cfg-baidu-hint\" style=\"display:none\">\n <div class=\"baidu-subtab\">\n <button data-baidu=\"normal\" class=\"active\">通用翻译</button>\n <button data-baidu=\"llm\">大模型翻译</button>\n </div>\n </div>\n\n <label>显示模式</label>\n <div class=\"mode-tab\">\n <button data-mode=\"bilingual\" class=\"active\">📖 双语</button>\n <button data-mode=\"monolingual\">🔤 仅译文</button>\n </div>\n\n <div class=\"cfg-section show\" id=\"ds-cfg-google\">\n <div class=\"hint\">Google 翻译免费使用,无需配置</div>\n </div>\n <div class=\"cfg-section\" id=\"ds-cfg-baidu-normal\">\n <label>APP ID</label>\n <input id=\"ds-baidu-id\" placeholder=\"从 fanyi-api.baidu.com 获取\" value=\"${cfg.baiduAppId}\">\n <label>密钥</label>\n <input type=\"password\" id=\"ds-baidu-key\" placeholder=\"从 fanyi-api.baidu.com 获取\" value=\"${cfg.baiduKey}\">\n <div class=\"hint\">免费版每月 100 万字符</div>\n </div>\n <div class=\"cfg-section\" id=\"ds-cfg-baidu-llm\">\n <label>APP ID</label>\n <input type=\"password\" id=\"ds-baidu-llm-id\" placeholder=\"从控制台获取的单串 APP ID\" value=\"${cfg.baiduLlmAppId}\">\n <div class=\"hint\">免费版每月 100 万字符</div>\n </div>\n <div class=\"cfg-section\" id=\"ds-cfg-ds\">\n <label>API Key</label>\n <input type=\"password\" id=\"ds-ds-key\" placeholder=\"sk-xxxxxxxx\" value=\"${cfg.dsKey}\">\n <label>模型</label>\n <select id=\"ds-ds-model\">\n <option value=\"deepseek-chat\" ${cfg.dsModel === \"deepseek-chat\" ? \"selected\" : \"\"}>DeepSeek V3</option>\n <option value=\"deepseek-reasoner\" ${cfg.dsModel === \"deepseek-reasoner\" ? \"selected\" : \"\"}>DeepSeek R1</option>\n </select>\n <div class=\"hint\">质量高但速度较慢</div>\n </div>\n <div class=\"btn-row\">\n <button class=\"btn-save\" id=\"ds-save-btn\">💾 保存配置</button>\n <button class=\"btn-close-cfg\" id=\"ds-close-btn\">关闭</button>\n </div>\n <div class=\"hint\" style=\"text-align:center;margin-top:8px;cursor:pointer\" id=\"ds-clear-cache\">🗑️ 清除翻译缓存 (${transCache.size}条)</div>\n <div id=\"ds-progress\">\n <div class=\"bar-wrap\"><div class=\"bar\" id=\"ds-progress-bar\"></div></div>\n <div class=\"text\" id=\"ds-progress-text\">准备中...</div>\n </div>\n </div>\n <div id=\"ds-toast\"></div>\n `);\n\n // ====== DOM 引用 ======\n const fab = document.getElementById(\"ds-fab\");\n const panel = document.getElementById(\"ds-panel\");\n const progress = document.getElementById(\"ds-progress\");\n const progressBar = document.getElementById(\"ds-progress-bar\");\n const progressText = document.getElementById(\"ds-progress-text\");\n const toast = document.getElementById(\"ds-toast\");\n const cacheHint = document.getElementById(\"ds-clear-cache\");\n\n // ====== 百度子模式 ======\n let baiduMode = GM_getValue(\"ds_baidu_mode\", \"normal\");\n\n function showBaiduConfig() {\n document.getElementById(\"ds-cfg-baidu-normal\").classList.toggle(\"show\", baiduMode === \"normal\");\n document.getElementById(\"ds-cfg-baidu-llm\").classList.toggle(\"show\", baiduMode === \"llm\");\n document.querySelectorAll(\".baidu-subtab button\").forEach((b) => {\n b.classList.toggle(\"active\", b.dataset.baidu === baiduMode);\n });\n }\n\n document.querySelectorAll(\".baidu-subtab button\").forEach((btn) => {\n btn.addEventListener(\"click\", () => {\n document.querySelectorAll(\".baidu-subtab button\").forEach((b) => b.classList.remove(\"active\"));\n btn.classList.add(\"active\");\n baiduMode = btn.dataset.baidu;\n showBaiduConfig();\n });\n });\n\n // ====== 引擎切换 ======\n document.querySelectorAll(\".engine-tab button\").forEach((btn) => {\n btn.addEventListener(\"click\", () => {\n document.querySelectorAll(\".engine-tab button\").forEach((b) => b.classList.remove(\"active\"));\n btn.classList.add(\"active\");\n cfg.engine = btn.dataset.engine;\n document.getElementById(\"ds-cfg-google\").classList.toggle(\"show\", cfg.engine === \"google\");\n document.getElementById(\"ds-cfg-baidu-hint\").style.display = cfg.engine === \"baidu\" ? \"block\" : \"none\";\n document.getElementById(\"ds-cfg-baidu-normal\").classList.toggle(\"show\", cfg.engine === \"baidu\" && baiduMode === \"normal\");\n document.getElementById(\"ds-cfg-baidu-llm\").classList.toggle(\"show\", cfg.engine === \"baidu\" && baiduMode === \"llm\");\n document.getElementById(\"ds-cfg-ds\").classList.toggle(\"show\", cfg.engine === \"deepseek\");\n });\n });\n // 初始状态:显示百度子tab\n if (cfg.engine === \"baidu\") {\n document.getElementById(\"ds-cfg-baidu-hint\").style.display = \"block\";\n showBaiduConfig();\n }\n\n // ====== 模式切换 ======\n document.querySelectorAll(\".mode-tab button\").forEach((btn) => {\n btn.addEventListener(\"click\", () => {\n document.querySelectorAll(\".mode-tab button\").forEach((b) => b.classList.remove(\"active\"));\n btn.classList.add(\"active\");\n cfg.mode = btn.dataset.mode;\n });\n });\n\n // ====== 保存 / 关闭 ======\n document.getElementById(\"ds-save-btn\").addEventListener(\"click\", () => {\n saveConfig();\n showToast(\"✅ 配置已保存\", \"ok\");\n });\n document.getElementById(\"ds-close-btn\").addEventListener(\"click\", () => panel.classList.remove(\"show\"));\n document.getElementById(\"ds-close-panel\").addEventListener(\"click\", () => panel.classList.remove(\"show\"));\n\n cacheHint.addEventListener(\"click\", () => {\n transCache.clear();\n cacheHint.textContent = `🗑️ 清除翻译缓存 (0条)`;\n showToast(\"缓存已清除\", \"info\");\n });\n\n // ====== 保存配置 ======\n function saveConfig() {\n cfg.baiduAppId = document.getElementById(\"ds-baidu-id\").value.trim();\n cfg.baiduKey = document.getElementById(\"ds-baidu-key\").value.trim();\n cfg.baiduLlmAppId = document.getElementById(\"ds-baidu-llm-id\").value.trim();\n cfg.dsKey = document.getElementById(\"ds-ds-key\").value.trim();\n cfg.dsModel = document.getElementById(\"ds-ds-model\").value;\n\n GM_setValue(\"ds_engine\", cfg.engine);\n GM_setValue(\"ds_mode\", cfg.mode);\n GM_setValue(\"ds_baidu_id\", cfg.baiduAppId);\n GM_setValue(\"ds_baidu_key\", cfg.baiduKey);\n GM_setValue(\"ds_baidu_llm_id\", cfg.baiduLlmAppId);\n GM_setValue(\"ds_baidu_mode\", baiduMode);\n GM_setValue(\"ds_api_key\", cfg.dsKey);\n GM_setValue(\"ds_model\", cfg.dsModel);\n }\n\n // ====== 可拖动按钮 ======\n let isDragging = false, dragStartX = 0, dragStartY = 0, dragMoved = false;\n let longPressTimer = null, longPressStart = 0, isLongPress = false;\n\n fab.addEventListener(\"mousedown\", (e) => {\n if (e.button !== 0) return;\n isDragging = true; dragMoved = false;\n const rect = fab.getBoundingClientRect();\n dragStartX = e.clientX; dragStartY = e.clientY;\n fab._origLeft = rect.left; fab._origTop = rect.top;\n fab.style.cursor = \"grabbing\";\n fab.style.transition = \"none\";\n });\n\n document.addEventListener(\"mousemove\", (e) => {\n if (!isDragging) return;\n const dx = e.clientX - dragStartX, dy = e.clientY - dragStartY;\n if (Math.abs(dx) > 5 || Math.abs(dy) > 5) dragMoved = true;\n const vw = window.innerWidth, vh = window.innerHeight;\n const fw = 52, fh = 52;\n let nx = Math.max(0, Math.min(vw - fw, fab._origLeft + dx));\n let ny = Math.max(0, Math.min(vh - fh, fab._origTop + dy));\n fab.style.left = nx + \"px\"; fab.style.right = \"auto\";\n fab.style.top = ny + \"px\"; fab.style.bottom = \"auto\";\n });\n\n document.addEventListener(\"mouseup\", (e) => {\n if (!isDragging) return;\n isDragging = false;\n fab.style.transition = \"\";\n fab.style.cursor = \"\";\n if (dragMoved) { e.stopPropagation(); e.preventDefault(); }\n });\n\n fab.addEventListener(\"click\", (e) => {\n if (dragMoved) { e.stopPropagation(); e.preventDefault(); return; }\n if (\"ontouchstart\" in window) return;\n toggleTranslate();\n });\n\n fab.addEventListener(\"touchstart\", (e) => {\n const t = e.touches[0];\n isDragging = true; dragMoved = false;\n const rect = fab.getBoundingClientRect();\n dragStartX = t.clientX; dragStartY = t.clientY;\n fab._origLeft = rect.left; fab._origTop = rect.top;\n fab.style.transition = \"none\";\n longPressStart = Date.now(); isLongPress = false;\n longPressTimer = setTimeout(() => {\n if (!dragMoved) { isLongPress = true; navigator.vibrate?.(20); panel.classList.add(\"show\"); }\n }, 500);\n }, { passive: true });\n\n fab.addEventListener(\"touchmove\", (e) => {\n if (!isDragging) return;\n const t = e.touches[0];\n const dx = t.clientX - dragStartX, dy = t.clientY - dragStartY;\n if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {\n dragMoved = true;\n clearTimeout(longPressTimer);\n }\n const vw = window.innerWidth, vh = window.innerHeight;\n const fw = 52, fh = 52;\n let nx = Math.max(0, Math.min(vw - fw, fab._origLeft + dx));\n let ny = Math.max(0, Math.min(vh - fh, fab._origTop + dy));\n fab.style.left = nx + \"px\"; fab.style.right = \"auto\";\n fab.style.top = ny + \"px\"; fab.style.bottom = \"auto\";\n e.preventDefault();\n }, { passive: false });\n\n fab.addEventListener(\"touchend\", (e) => {\n isDragging = false;\n clearTimeout(longPressTimer);\n fab.style.transition = \"\";\n if (!dragMoved && !isLongPress && Date.now() - longPressStart < 500) {\n e.preventDefault();\n toggleTranslate();\n }\n }, { passive: false });\n\n fab.addEventListener(\"contextmenu\", (e) => { e.preventDefault(); panel.classList.toggle(\"show\"); });\n\n // ====== 切换 ======\n async function toggleTranslate() {\n if (translating) return;\n saveConfig();\n\n if (translating) {\n stopRequested = true;\n showToast(\"⏹️ 正在停止...\", \"info\");\n return;\n }\n if (translated) { restoreOriginal(); return; }\n\n if (cfg.engine === \"baidu\") {\n if (baiduMode === \"normal\" && (!cfg.baiduAppId || !cfg.baiduKey)) {\n showToast(\"请配置百度通用翻译(APP ID + 密钥)\", \"err\"); return;\n }\n if (baiduMode === \"llm\" && !cfg.baiduLlmAppId) {\n showToast(\"请配置百度大模型翻译 APP ID\", \"err\"); return;\n }\n }\n if (cfg.engine === \"deepseek\" && !cfg.dsKey) {\n showToast(\"请先配置 DeepSeek Key(长按图标设置)\", \"err\"); return;\n }\n\n await translatePage();\n }\n\n function restoreOriginal() {\n if (!originalTexts) return;\n originalTexts.forEach(({ node, text }) => { node.textContent = text; });\n translated = false; fab.classList.remove(\"translated\"); fab.textContent = \"🌐\";\n showToast(\"已恢复原文\", \"info\");\n }\n\n // ====== 翻译页面 ======\n async function translatePage() {\n translating = true;\n progress.classList.add(\"show\");\n setProgress(0, \"提取文本...\");\n\n try {\n const entries = [];\n const walk = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {\n acceptNode: (n) => {\n const p = n.parentElement;\n if (!p || p.closest(\"script,style,code,pre,#ds-panel,#ds-fab,#ds-toast,svg\"))\n return NodeFilter.FILTER_REJECT;\n const t = n.textContent.trim();\n if (t.length < 10) return NodeFilter.FILTER_REJECT;\n if (p.offsetWidth === 0 || p.offsetHeight === 0) return NodeFilter.FILTER_REJECT;\n return NodeFilter.FILTER_ACCEPT;\n },\n });\n while (walk.nextNode()) entries.push({ node: walk.currentNode, text: walk.currentNode.textContent });\n\n if (entries.length === 0) { showToast(\"没有可翻译的文本\", \"info\"); return; }\n originalTexts = entries;\n\n const nodes = entries.map((e) => e.node);\n const needApi = [];\n const cached = [];\n\n nodes.forEach((node, i) => {\n const key = node.textContent.trim();\n if (transCache.has(key)) {\n cached.push(i);\n } else {\n needApi.push(i);\n }\n });\n\n cached.forEach((i) => {\n const key = nodes[i].textContent.trim();\n const t = transCache.get(key);\n nodes[i].textContent = cfg.mode === \"bilingual\" ? key + \"\\n\" + t : t;\n });\n\n if (needApi.length > 0) {\n if (cfg.engine === \"google\") {\n await translateGoogle(nodes, needApi);\n } else if (cfg.engine === \"baidu\") {\n if (baiduMode === \"llm\") {\n await translateBaiduLLM(nodes, needApi);\n } else {\n await translateBaidu(nodes, needApi);\n }\n } else {\n await translateDeepSeek(nodes, needApi);\n }\n }\n\n cacheHint.textContent = `🗑️ 清除翻译缓存 (${transCache.size}条)`;\n\n translated = true;\n fab.classList.add(\"translated\"); fab.textContent = \"✅\";\n setProgress(100, \"✅ 翻译完成\");\n const fromCache = cached.length > 0 ? ` (缓存${cached.length}段)` : \"\";\n showToast(`翻译完成${fromCache},再点恢复原文`, \"ok\");\n } catch (err) {\n showToast(\"出错: \" + err.message, \"err\");\n }\n setTimeout(() => { progress.classList.remove(\"show\"); translating = false; }, 1500);\n }\n\n // ====== Google 翻译 ======\n async function translateGoogle(nodes, indices) {\n const batchSize = 5;\n for (let ki = 0; ki < indices.length; ki += batchSize) {\n if (stopRequested) { showToast(\"⏹️ 已停止\", \"info\"); break; }\n const batchIdx = indices.slice(ki, ki + batchSize);\n const pct = Math.min(100, Math.round(((ki + batchIdx.length) / indices.length) * 100));\n setProgress(pct, `Google 翻译中 ${Math.min(ki + batchIdx.length, indices.length)}/${indices.length}...`);\n\n for (const i of batchIdx) {\n if (stopRequested) break;\n const text = nodes[i].textContent.trim();\n try {\n const t = await callGoogle(text);\n if (t) {\n transCache.set(text, t);\n nodes[i].textContent = cfg.mode === \"bilingual\" ? text + \"\\n\" + t : t;\n }\n } catch (e) {\n console.warn(\"Google翻译失败:\", e.message?.slice(0, 30));\n }\n }\n }\n }\n\n function callGoogle(text) {\n return new Promise((resolve, reject) => {\n const url = \"https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=zh-CN&dt=t&q=\" + encodeURIComponent(text);\n GM_xmlhttpRequest({\n method: \"GET\", url,\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data && data[0]) {\n const result = data[0].map((r) => r[0]).join(\"\");\n resolve(result || null);\n } else {\n reject(new Error(\"无结果\"));\n }\n } catch { reject(new Error(\"解析失败\")); }\n },\n onerror: () => reject(new Error(\"网络错误\")),\n timeout: 15000,\n });\n });\n }\n\n // ====== 百度通用翻译(appid + key) ======\n async function translateBaidu(nodes, indices) {\n const sep = \"\\n|||ds-sep|||\\n\";\n const maxBytes = 5000;\n const batches = [];\n let cur = [], curBytes = 0;\n for (let ki = 0; ki < indices.length; ki++) {\n const t = nodes[indices[ki]].textContent.trim();\n const bytes = new Blob([t]).size;\n if (cur.length > 0 && curBytes + bytes + 20 > maxBytes) {\n batches.push(cur);\n cur = []; curBytes = 0;\n }\n cur.push({ idx: indices[ki], text: t, bytes });\n curBytes += bytes + 20;\n }\n if (cur.length) batches.push(cur);\n\n for (let b = 0; b < batches.length; b += 3) {\n if (stopRequested) { showToast(\"⏹️ 已停止\", \"info\"); break; }\n const chunk = batches.slice(b, b + 3);\n const pct = Math.min(100, Math.round(((b + chunk.length) / batches.length) * 100));\n setProgress(pct, `百度翻译中 ${Math.min((b + chunk.length) * 30, indices.length)}/${indices.length}...`);\n\n await Promise.all(chunk.map(async (batch) => {\n try {\n const joined = batch.map(x => x.text).join(sep);\n const result = await callBaidu(joined);\n if (result) {\n const parts = result.split(\"|||ds-sep|||\").map((s) => s.trim());\n batch.forEach((item, idx) => {\n const t = parts[idx];\n if (t && t !== item.text) {\n transCache.set(item.text, t);\n const node = nodes[item.idx];\n node.textContent = cfg.mode === \"bilingual\" ? item.text + \"\\n\" + t : t;\n }\n });\n }\n } catch (e) {\n console.warn(\"百度批量失败:\", e.message.slice(0, 30));\n for (const item of batch) {\n if (stopRequested) break;\n try {\n const r = await callBaidu(item.text);\n if (r) {\n transCache.set(item.text, r);\n nodes[item.idx].textContent = cfg.mode === \"bilingual\" ? item.text + \"\\n\" + r : r;\n }\n } catch {}\n }\n }\n }));\n }\n }\n\n function callBaidu(text) {\n return new Promise((resolve, reject) => {\n const appid = cfg.baiduAppId;\n const key = cfg.baiduKey;\n const salt = Date.now() + Math.random();\n const sign = md5(appid + text + salt + key);\n const url = `https://fanyi-api.baidu.com/api/trans/vip/translate?q=${encodeURIComponent(text)}&from=auto&to=zh&appid=${appid}&salt=${salt}&sign=${sign}`;\n GM_xmlhttpRequest({\n method: \"GET\", url,\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data.trans_result) resolve(data.trans_result.map((r) => r.dst).join(\"\\n\"));\n else if (data.error_code) reject(new Error(`[${data.error_code}] ${data.error_msg}`));\n else reject(new Error(\"返回异常\"));\n } catch { reject(new Error(\"解析失败\")); }\n },\n onerror: () => reject(new Error(\"网络错误\")),\n timeout: 15000,\n });\n });\n }\n\n // ====== 百度大模型翻译(单 APP ID) ======\n async function translateBaiduLLM(nodes, indices) {\n const batchSize = 5;\n for (let ki = 0; ki < indices.length; ki += batchSize) {\n if (stopRequested) { showToast(\"⏹️ 已停止\", \"info\"); break; }\n const batchIdx = indices.slice(ki, ki + batchSize);\n const pct = Math.min(100, Math.round(((ki + batchIdx.length) / indices.length) * 100));\n setProgress(pct, `百度大模型翻译中 ${Math.min(ki + batchIdx.length, indices.length)}/${indices.length}...`);\n\n for (const i of batchIdx) {\n if (stopRequested) break;\n const text = nodes[i].textContent.trim();\n try {\n const t = await callBaiduLLM(text);\n if (t) {\n transCache.set(text, t);\n nodes[i].textContent = cfg.mode === \"bilingual\" ? text + \"\\n\" + t : t;\n }\n } catch (e) {\n console.warn(\"百度大模型翻译失败:\", e.message?.slice(0, 30));\n }\n }\n }\n }\n\n function callBaiduLLM(text) {\n return new Promise((resolve, reject) => {\n const appid = cfg.baiduLlmAppId;\n const salt = Date.now() + Math.random();\n const sign = md5(appid + text + salt + appid);\n const url = `https://fanyi-api.baidu.com/api/trans/llm/translate?q=${encodeURIComponent(text)}&from=auto&to=zh&appid=${appid}&salt=${salt}&sign=${sign}`;\n GM_xmlhttpRequest({\n method: \"GET\", url,\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data.trans_result) resolve(data.trans_result.map((r) => r.dst).join(\"\\n\"));\n else if (data.result) resolve(data.result);\n else if (data.error_code) reject(new Error(`[${data.error_code}] ${data.error_msg}`));\n else reject(new Error(\"返回异常\"));\n } catch { reject(new Error(\"解析失败\")); }\n },\n onerror: () => reject(new Error(\"网络错误\")),\n timeout: 30000,\n });\n });\n }\n\n // ====== DeepSeek API ======\n async function translateDeepSeek(nodes, indices) {\n const batchSize = 20;\n const sep = \"\\n---\\n\";\n for (let ki = 0; ki < indices.length; ki += batchSize) {\n if (stopRequested) { showToast(\"⏹️ 已停止\", \"info\"); break; }\n const batchIdx = indices.slice(ki, ki + batchSize);\n const texts = batchIdx.map((i) => nodes[i].textContent.trim());\n const pct = Math.min(100, Math.round(((ki + batchIdx.length) / indices.length) * 100));\n setProgress(pct, `DeepSeek 翻译中 ${Math.min(ki + batchIdx.length, indices.length)}/${indices.length}...`);\n\n const result = await callDeepSeek(texts);\n if (result) {\n batchIdx.forEach((i, idx) => {\n const orig = texts[idx];\n if (result[idx]) {\n transCache.set(orig, result[idx]);\n nodes[i].textContent = cfg.mode === \"bilingual\" ? orig + \"\\n\" + result[idx] : result[idx];\n }\n });\n }\n }\n }\n\n function callDeepSeek(texts) {\n return new Promise((resolve) => {\n const joined = texts.map((t, i) => `[${i + 1}] ${t}`).join(\"\\n---\\n\");\n const prompt = `逐段翻译为简体中文,保持编号格式,只返回翻译结果:\\n\\n${joined}`;\n GM_xmlhttpRequest({\n method: \"POST\",\n url: \"https://api.deepseek.com/chat/completions\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${cfg.dsKey}` },\n data: JSON.stringify({\n model: cfg.dsModel,\n messages: [{ role: \"system\", content: \"逐段翻译,保持编号。\" }, { role: \"user\", content: prompt }],\n temperature: 0.3, max_tokens: 8192,\n }),\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data.choices && data.choices[0]) {\n const content = data.choices[0].message.content.trim();\n const results = [];\n for (let i = 0; i < texts.length; i++) {\n const re = new RegExp(`\\\\[${i + 1}\\\\]\\\\s*([\\\\s\\\\S]*?)(?=\\\\[${i + 2}\\\\]|$)`);\n const m = content.match(re);\n results.push(m ? m[1].trim() : \"\");\n }\n resolve(results);\n } else resolve(null);\n } catch { resolve(null); }\n },\n onerror: () => resolve(null),\n timeout: 60000,\n });\n });\n }\n\n // ====== MD5 ======\n function md5(str) {\n function rotateLeft(x, n) { return (x << n) | (x >>> (32 - n)); }\n function toHex(x) {\n var s = \"\";\n for (var i = 0; i < 4; i++) {\n s += (\"0\" + ((x >> (i * 8)) & 0xff).toString(16)).slice(-2);\n }\n return s;\n }\n function addUnsigned(x, y) {\n var lsw = (x & 0xffff) + (y & 0xffff);\n return ((x >>> 16) + (y >>> 16) + (lsw >>> 16)) << 16 | (lsw & 0xffff);\n }\n var i, j, aa, bb, cc, dd;\n var S = [[7, 12, 17, 22], [5, 9, 14, 20], [4, 11, 16, 23], [6, 10, 15, 21]];\n var T = [];\n for (i = 0; i < 64; i++) T[i] = Math.floor(Math.abs(Math.sin(i + 1)) * 0x100000000);\n str = unescape(encodeURIComponent(str));\n var n = str.length, bits = [];\n for (i = 0; i < n; i++) bits.push(str.charCodeAt(i));\n bits.push(0x80);\n var len = bits.length / 4 + 2;\n len = Math.ceil(len / 16) * 16;\n var x = [];\n for (i = 0; i < len; i++) x[i] = 0;\n for (i = 0; i < bits.length; i++) x[i >> 2] |= bits[i] << ((i % 4) * 8);\n x[x.length - 2] = n * 8;\n var a = 0x67452301, b = 0xefcdab89, c = 0x98badcfe, d = 0x10325476;\n for (i = 0; i < x.length; i += 16) {\n var X = x.slice(i, i + 16);\n aa = a; bb = b; cc = c; dd = d;\n for (j = 0; j < 64; j++) {\n var g, f;\n if (j < 16) { g = j; f = (bb & cc) | (~bb & dd); }\n else if (j < 32) { g = (5 * j + 1) % 16; f = (dd & bb) | (~dd & cc); }\n else if (j < 48) { g = (3 * j + 5) % 16; f = bb ^ cc ^ dd; }\n else { g = (7 * j) % 16; f = cc ^ (bb | ~dd); }\n var temp = dd; dd = cc; cc = bb;\n bb = addUnsigned(bb, rotateLeft(addUnsigned(addUnsigned(addUnsigned(aa, f), T[j]), X[g]), S[j >> 4][j & 3]));\n aa = temp;\n }\n a = addUnsigned(a, aa); b = addUnsigned(b, bb);\n c = addUnsigned(c, cc); d = addUnsigned(d, dd);\n }\n return (toHex(a) + toHex(b) + toHex(c) + toHex(d)).toLowerCase();\n }\n\n // ====== 工具 ======\n function setProgress(pct, text) { progressBar.style.width = pct + \"%\"; progressText.textContent = text; }\n function showToast(msg, type) { toast.textContent = msg; toast.className = type; toast.style.display = \"block\"; setTimeout(() => { toast.style.display = \"none\"; }, 3000); }\n function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }\n\n document.addEventListener(\"keydown\", (e) => { if (e.key === \"Escape\") panel.classList.remove(\"show\"); });\n\n const engName = cfg.engine === \"google\" ? \"Google\" : cfg.engine === \"baidu\" ? (\"百度\" + (baiduMode === \"llm\" ? \"·大模型\" : \"·通用\")) : \"DeepSeek\";\n const modeName = cfg.mode === \"bilingual\" ? \"双语\" : \"仅译文\";\n console.log(`✅ 网页翻译已加载 | ${engName} | ${modeName} | 点击翻译/恢复,长按配置`);\n})();\n"}, "997f05dde4bca31af676ebb202e3afd6a1a59663": {"t": 1785766100.844015, "n": 30448, "h": "51d6f28f6cd0b047", "c": "// ==UserScript==\n// @name 网页翻译 · 多引擎\n// @namespace http://tampermonkey.net/\n// @version 1.5\n// @description 点击翻译整个页面/恢复原文。支持Google(免费)/百度通用(极速)/百度大模型(AI)/DeepSeek(高质量),双语/单语模式,翻译结果缓存。长按进入配置。\n// @author you\n// @match *://*/*\n// @icon data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌐</text></svg>\n// @grant GM_setValue\n// @grant GM_getValue\n// @grant GM_addStyle\n// @grant GM_xmlhttpRequest\n// @license MIT\n// ==/UserScript==\n\n(function () {\n \"use strict\";\n\n // ====== 配置 ======\n const cfg = {\n engine: GM_getValue(\"ds_engine\", \"google\"),\n mode: GM_getValue(\"ds_mode\", \"bilingual\"),\n // 百度通用翻译 (appid + secretKey)\n baiduAppId: GM_getValue(\"ds_baidu_id\", \"\"),\n baiduKey: GM_getValue(\"ds_baidu_key\", \"\"),\n // 百度大模型翻译 (只有一串 APP ID)\n baiduLlmAppId: GM_getValue(\"ds_baidu_llm_id\", \"\"),\n // DeepSeek\n dsKey: GM_getValue(\"ds_api_key\", \"\"),\n dsModel: GM_getValue(\"ds_model\", \"deepseek-chat\"),\n };\n\n // ====== 状态 ======\n let translated = false;\n let translating = false;\n let stopRequested = false;\n let originalTexts = null;\n const transCache = new Map();\n\n // ====== 样式 ======\n GM_addStyle(`\n #ds-fab {\n position: fixed; bottom: 24px; right: 24px; z-index: 999999;\n width: 52px; height: 52px; border-radius: 50%; background: #4f6ef7;\n color: #fff; border: none; font-size: 24px; cursor: pointer;\n box-shadow: 0 2px 12px rgba(79,110,247,0.4);\n transition: transform 0.15s, background 0.3s;\n display: flex; align-items: center; justify-content: center;\n user-select: none; -webkit-user-select: none;\n }\n #ds-fab:active { transform: scale(0.92); }\n #ds-fab.translated { background: #a6e3a1; }\n\n #ds-panel {\n position: fixed; bottom: 88px; right: 24px; z-index: 999999;\n width: 320px; max-height: 70vh; overflow-y: auto;\n background: #1e1e2e; border-radius: 16px; padding: 20px;\n box-shadow: 0 8px 40px rgba(0,0,0,0.5); color: #cdd6f4;\n font: 14px -apple-system, sans-serif; display: none; border: 1px solid #313244;\n }\n #ds-panel.show { display: block; }\n #ds-panel h3 {\n margin: 0 0 16px 0; font-size: 16px; color: #cba6f7;\n display: flex; justify-content: space-between; align-items: center;\n }\n #ds-panel h3 button { background: none; border: none; color: #a6adc8; cursor: pointer; font-size: 18px; padding: 0 4px; }\n #ds-panel h3 button:hover { color: #f38ba8; }\n #ds-panel label { display: block; margin-bottom: 4px; color: #a6adc8; font-size: 12px; margin-top: 8px; }\n #ds-panel label:first-of-type { margin-top: 0; }\n #ds-panel input, #ds-panel select {\n width: 100%; padding: 8px 12px; margin-bottom: 4px;\n background: #313244; border: 1px solid #45475a; border-radius: 8px;\n color: #cdd6f4; font-size: 13px; box-sizing: border-box; outline: none;\n }\n #ds-panel input:focus, #ds-panel select:focus { border-color: #4f6ef7; }\n #ds-panel .hint { font-size: 11px; color: #6c7086; margin: 2px 0 8px 0; }\n\n #ds-panel .engine-tab {\n display: flex; gap: 6px; margin-bottom: 6px;\n }\n #ds-panel .engine-tab button {\n flex: 1; padding: 7px; border: 1px solid #45475a; border-radius: 8px;\n background: #313244; color: #a6adc8; cursor: pointer; font-size: 12px; transition: all 0.2s;\n }\n #ds-panel .engine-tab button.active { background: #4f6ef7; border-color: #4f6ef7; color: #fff; }\n\n #ds-panel .baidu-subtab {\n display: flex; gap: 6px; margin: 6px 0 10px 0;\n }\n #ds-panel .baidu-subtab button {\n flex: 1; padding: 5px; border: 1px solid #45475a; border-radius: 6px;\n background: #313244; color: #a6adc8; cursor: pointer; font-size: 11px; transition: all 0.2s;\n }\n #ds-panel .baidu-subtab button.active { background: #4f6ef7; border-color: #4f6ef7; color: #fff; }\n\n #ds-panel .mode-tab {\n display: flex; gap: 6px; margin-bottom: 12px;\n }\n #ds-panel .mode-tab button {\n flex: 1; padding: 6px; border: 1px solid #45475a; border-radius: 8px;\n background: #313244; color: #a6adc8; cursor: pointer; font-size: 12px; transition: all 0.2s;\n }\n #ds-panel .mode-tab button.active { background: #585b70; border-color: #585b70; color: #fff; }\n\n #ds-panel .cfg-section { display: none; }\n #ds-panel .cfg-section.show { display: block; }\n #ds-panel .btn-row { display: flex; gap: 8px; margin-top: 12px; }\n #ds-panel .btn-row button {\n flex: 1; padding: 9px; border: none; border-radius: 8px;\n cursor: pointer; font-size: 13px; transition: opacity 0.2s;\n }\n #ds-panel .btn-row button:active { opacity: 0.8; }\n #ds-panel .btn-save { background: #4f6ef7; color: #fff; }\n #ds-panel .btn-close-cfg { background: #45475a; color: #cdd6f4; }\n #ds-panel .btn-clear-cache { background: #45475a; color: #f38ba8; }\n\n #ds-progress { margin-top: 10px; display: none; }\n #ds-progress.show { display: block; }\n #ds-progress .bar-wrap { width: 100%; height: 4px; background: #313244; border-radius: 2px; overflow: hidden; }\n #ds-progress .bar { height: 100%; width: 0%; background: #a6e3a1; border-radius: 2px; transition: width 0.3s; }\n #ds-progress .text { font-size: 11px; color: #a6adc8; margin-top: 4px; text-align: center; }\n\n #ds-toast {\n position: fixed; top: 20px; left: 50%; transform: translateX(-50%);\n z-index: 9999999; padding: 10px 24px; border-radius: 10px;\n font: 14px -apple-system, sans-serif; color: #fff;\n display: none; box-shadow: 0 4px 20px rgba(0,0,0,0.3);\n }\n #ds-toast.ok { background: #1e3a2e; }\n #ds-toast.err { background: #3a1e1e; }\n #ds-toast.info { background: #1e2a3a; }\n `);\n\n // ====== DOM ======\n document.body.insertAdjacentHTML(\"beforeend\", `\n <button id=\"ds-fab\">🌐</button>\n <div id=\"ds-panel\">\n <h3>\n ⚙️ 翻译设置\n <button id=\"ds-close-panel\">✕</button>\n </h3>\n\n <label>翻译引擎</label>\n <div class=\"engine-tab\">\n <button data-engine=\"google\" class=\"active\">🌍 Google</button>\n <button data-engine=\"baidu\">⚡ 百度</button>\n <button data-engine=\"deepseek\">🧠 DeepSeek</button>\n </div>\n\n <div class=\"cfg-section show\" id=\"ds-cfg-baidu-hint\" style=\"display:none\">\n <div class=\"baidu-subtab\">\n <button data-baidu=\"normal\" class=\"active\">通用翻译</button>\n <button data-baidu=\"llm\">大模型翻译</button>\n </div>\n </div>\n\n <label>显示模式</label>\n <div class=\"mode-tab\">\n <button data-mode=\"bilingual\" class=\"active\">📖 双语</button>\n <button data-mode=\"monolingual\">🔤 仅译文</button>\n </div>\n\n <div class=\"cfg-section show\" id=\"ds-cfg-google\">\n <div class=\"hint\">Google 翻译免费使用,无需配置</div>\n </div>\n <div class=\"cfg-section\" id=\"ds-cfg-baidu-normal\">\n <label>APP ID</label>\n <input id=\"ds-baidu-id\" placeholder=\"从 fanyi-api.baidu.com 获取\" value=\"${cfg.baiduAppId}\">\n <label>密钥</label>\n <input type=\"password\" id=\"ds-baidu-key\" placeholder=\"从 fanyi-api.baidu.com 获取\" value=\"${cfg.baiduKey}\">\n <div class=\"hint\">免费版每月 100 万字符</div>\n </div>\n <div class=\"cfg-section\" id=\"ds-cfg-baidu-llm\">\n <label>APP ID</label>\n <input type=\"password\" id=\"ds-baidu-llm-id\" placeholder=\"从控制台获取的单串 APP ID\" value=\"${cfg.baiduLlmAppId}\">\n <div class=\"hint\">免费版每月 100 万字符</div>\n </div>\n <div class=\"cfg-section\" id=\"ds-cfg-ds\">\n <label>API Key</label>\n <input type=\"password\" id=\"ds-ds-key\" placeholder=\"sk-xxxxxxxx\" value=\"${cfg.dsKey}\">\n <label>模型</label>\n <select id=\"ds-ds-model\">\n <option value=\"deepseek-chat\" ${cfg.dsModel === \"deepseek-chat\" ? \"selected\" : \"\"}>DeepSeek V3</option>\n <option value=\"deepseek-reasoner\" ${cfg.dsModel === \"deepseek-reasoner\" ? \"selected\" : \"\"}>DeepSeek R1</option>\n </select>\n <div class=\"hint\">质量高但速度较慢</div>\n </div>\n <div class=\"btn-row\">\n <button class=\"btn-save\" id=\"ds-save-btn\">💾 保存配置</button>\n <button class=\"btn-close-cfg\" id=\"ds-close-btn\">关闭</button>\n </div>\n <div class=\"hint\" style=\"text-align:center;margin-top:8px;cursor:pointer\" id=\"ds-clear-cache\">🗑️ 清除翻译缓存 (${transCache.size}条)</div>\n <div id=\"ds-progress\">\n <div class=\"bar-wrap\"><div class=\"bar\" id=\"ds-progress-bar\"></div></div>\n <div class=\"text\" id=\"ds-progress-text\">准备中...</div>\n </div>\n </div>\n <div id=\"ds-toast\"></div>\n `);\n\n // ====== DOM 引用 ======\n const fab = document.getElementById(\"ds-fab\");\n const panel = document.getElementById(\"ds-panel\");\n const progress = document.getElementById(\"ds-progress\");\n const progressBar = document.getElementById(\"ds-progress-bar\");\n const progressText = document.getElementById(\"ds-progress-text\");\n const toast = document.getElementById(\"ds-toast\");\n const cacheHint = document.getElementById(\"ds-clear-cache\");\n\n // ====== 百度子模式 ======\n let baiduMode = GM_getValue(\"ds_baidu_mode\", \"normal\");\n\n function showBaiduConfig() {\n document.getElementById(\"ds-cfg-baidu-normal\").classList.toggle(\"show\", baiduMode === \"normal\");\n document.getElementById(\"ds-cfg-baidu-llm\").classList.toggle(\"show\", baiduMode === \"llm\");\n document.querySelectorAll(\".baidu-subtab button\").forEach((b) => {\n b.classList.toggle(\"active\", b.dataset.baidu === baiduMode);\n });\n }\n\n document.querySelectorAll(\".baidu-subtab button\").forEach((btn) => {\n btn.addEventListener(\"click\", () => {\n document.querySelectorAll(\".baidu-subtab button\").forEach((b) => b.classList.remove(\"active\"));\n btn.classList.add(\"active\");\n baiduMode = btn.dataset.baidu;\n showBaiduConfig();\n });\n });\n\n // ====== 引擎切换 ======\n document.querySelectorAll(\".engine-tab button\").forEach((btn) => {\n btn.addEventListener(\"click\", () => {\n document.querySelectorAll(\".engine-tab button\").forEach((b) => b.classList.remove(\"active\"));\n btn.classList.add(\"active\");\n cfg.engine = btn.dataset.engine;\n document.getElementById(\"ds-cfg-google\").classList.toggle(\"show\", cfg.engine === \"google\");\n document.getElementById(\"ds-cfg-baidu-hint\").style.display = cfg.engine === \"baidu\" ? \"block\" : \"none\";\n document.getElementById(\"ds-cfg-baidu-normal\").classList.toggle(\"show\", cfg.engine === \"baidu\" && baiduMode === \"normal\");\n document.getElementById(\"ds-cfg-baidu-llm\").classList.toggle(\"show\", cfg.engine === \"baidu\" && baiduMode === \"llm\");\n document.getElementById(\"ds-cfg-ds\").classList.toggle(\"show\", cfg.engine === \"deepseek\");\n });\n });\n // 初始状态:显示百度子tab\n if (cfg.engine === \"baidu\") {\n document.getElementById(\"ds-cfg-baidu-hint\").style.display = \"block\";\n showBaiduConfig();\n }\n\n // ====== 模式切换 ======\n document.querySelectorAll(\".mode-tab button\").forEach((btn) => {\n btn.addEventListener(\"click\", () => {\n document.querySelectorAll(\".mode-tab button\").forEach((b) => b.classList.remove(\"active\"));\n btn.classList.add(\"active\");\n cfg.mode = btn.dataset.mode;\n });\n });\n\n // ====== 保存 / 关闭 ======\n document.getElementById(\"ds-save-btn\").addEventListener(\"click\", () => {\n saveConfig();\n showToast(\"✅ 配置已保存\", \"ok\");\n });\n document.getElementById(\"ds-close-btn\").addEventListener(\"click\", () => panel.classList.remove(\"show\"));\n document.getElementById(\"ds-close-panel\").addEventListener(\"click\", () => panel.classList.remove(\"show\"));\n\n cacheHint.addEventListener(\"click\", () => {\n transCache.clear();\n cacheHint.textContent = `🗑️ 清除翻译缓存 (0条)`;\n showToast(\"缓存已清除\", \"info\");\n });\n\n // ====== 保存配置 ======\n function saveConfig() {\n cfg.baiduAppId = document.getElementById(\"ds-baidu-id\").value.trim();\n cfg.baiduKey = document.getElementById(\"ds-baidu-key\").value.trim();\n cfg.baiduLlmAppId = document.getElementById(\"ds-baidu-llm-id\").value.trim();\n cfg.dsKey = document.getElementById(\"ds-ds-key\").value.trim();\n cfg.dsModel = document.getElementById(\"ds-ds-model\").value;\n\n GM_setValue(\"ds_engine\", cfg.engine);\n GM_setValue(\"ds_mode\", cfg.mode);\n GM_setValue(\"ds_baidu_id\", cfg.baiduAppId);\n GM_setValue(\"ds_baidu_key\", cfg.baiduKey);\n GM_setValue(\"ds_baidu_llm_id\", cfg.baiduLlmAppId);\n GM_setValue(\"ds_baidu_mode\", baiduMode);\n GM_setValue(\"ds_api_key\", cfg.dsKey);\n GM_setValue(\"ds_model\", cfg.dsModel);\n }\n\n // ====== 可拖动按钮 ======\n let isDragging = false, dragStartX = 0, dragStartY = 0, dragMoved = false;\n let longPressTimer = null, longPressStart = 0, isLongPress = false;\n\n fab.addEventListener(\"mousedown\", (e) => {\n if (e.button !== 0) return;\n isDragging = true; dragMoved = false;\n const rect = fab.getBoundingClientRect();\n dragStartX = e.clientX; dragStartY = e.clientY;\n fab._origLeft = rect.left; fab._origTop = rect.top;\n fab.style.cursor = \"grabbing\";\n fab.style.transition = \"none\";\n });\n\n document.addEventListener(\"mousemove\", (e) => {\n if (!isDragging) return;\n const dx = e.clientX - dragStartX, dy = e.clientY - dragStartY;\n if (Math.abs(dx) > 5 || Math.abs(dy) > 5) dragMoved = true;\n const vw = window.innerWidth, vh = window.innerHeight;\n const fw = 52, fh = 52;\n let nx = Math.max(0, Math.min(vw - fw, fab._origLeft + dx));\n let ny = Math.max(0, Math.min(vh - fh, fab._origTop + dy));\n fab.style.left = nx + \"px\"; fab.style.right = \"auto\";\n fab.style.top = ny + \"px\"; fab.style.bottom = \"auto\";\n });\n\n document.addEventListener(\"mouseup\", (e) => {\n if (!isDragging) return;\n isDragging = false;\n fab.style.transition = \"\";\n fab.style.cursor = \"\";\n if (dragMoved) { e.stopPropagation(); e.preventDefault(); }\n });\n\n fab.addEventListener(\"click\", (e) => {\n if (dragMoved) { e.stopPropagation(); e.preventDefault(); return; }\n if (\"ontouchstart\" in window) return;\n toggleTranslate();\n });\n\n fab.addEventListener(\"touchstart\", (e) => {\n const t = e.touches[0];\n isDragging = true; dragMoved = false;\n const rect = fab.getBoundingClientRect();\n dragStartX = t.clientX; dragStartY = t.clientY;\n fab._origLeft = rect.left; fab._origTop = rect.top;\n fab.style.transition = \"none\";\n longPressStart = Date.now(); isLongPress = false;\n longPressTimer = setTimeout(() => {\n if (!dragMoved) { isLongPress = true; navigator.vibrate?.(20); panel.classList.add(\"show\"); }\n }, 500);\n }, { passive: true });\n\n fab.addEventListener(\"touchmove\", (e) => {\n if (!isDragging) return;\n const t = e.touches[0];\n const dx = t.clientX - dragStartX, dy = t.clientY - dragStartY;\n if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {\n dragMoved = true;\n clearTimeout(longPressTimer);\n }\n const vw = window.innerWidth, vh = window.innerHeight;\n const fw = 52, fh = 52;\n let nx = Math.max(0, Math.min(vw - fw, fab._origLeft + dx));\n let ny = Math.max(0, Math.min(vh - fh, fab._origTop + dy));\n fab.style.left = nx + \"px\"; fab.style.right = \"auto\";\n fab.style.top = ny + \"px\"; fab.style.bottom = \"auto\";\n e.preventDefault();\n }, { passive: false });\n\n fab.addEventListener(\"touchend\", (e) => {\n isDragging = false;\n clearTimeout(longPressTimer);\n fab.style.transition = \"\";\n if (!dragMoved && !isLongPress && Date.now() - longPressStart < 500) {\n e.preventDefault();\n toggleTranslate();\n }\n }, { passive: false });\n\n fab.addEventListener(\"contextmenu\", (e) => { e.preventDefault(); panel.classList.toggle(\"show\"); });\n\n // ====== 切换 ======\n async function toggleTranslate() {\n if (translating) return;\n saveConfig();\n\n if (translating) {\n stopRequested = true;\n showToast(\"⏹️ 正在停止...\", \"info\");\n return;\n }\n if (translated) { restoreOriginal(); return; }\n\n if (cfg.engine === \"baidu\") {\n if (baiduMode === \"normal\" && (!cfg.baiduAppId || !cfg.baiduKey)) {\n showToast(\"请配置百度通用翻译(APP ID + 密钥)\", \"err\"); return;\n }\n if (baiduMode === \"llm\" && !cfg.baiduLlmAppId) {\n showToast(\"请配置百度大模型翻译 APP ID\", \"err\"); return;\n }\n }\n if (cfg.engine === \"deepseek\" && !cfg.dsKey) {\n showToast(\"请先配置 DeepSeek Key(长按图标设置)\", \"err\"); return;\n }\n\n await translatePage();\n }\n\n function restoreOriginal() {\n if (!originalTexts) return;\n originalTexts.forEach(({ node, text }) => { node.textContent = text; });\n translated = false; fab.classList.remove(\"translated\"); fab.textContent = \"🌐\";\n showToast(\"已恢复原文\", \"info\");\n }\n\n // ====== 翻译页面 ======\n async function translatePage() {\n translating = true;\n progress.classList.add(\"show\");\n setProgress(0, \"提取文本...\");\n\n try {\n const entries = [];\n const walk = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {\n acceptNode: (n) => {\n const p = n.parentElement;\n if (!p || p.closest(\"script,style,code,pre,#ds-panel,#ds-fab,#ds-toast,svg\"))\n return NodeFilter.FILTER_REJECT;\n const t = n.textContent.trim();\n if (t.length < 10) return NodeFilter.FILTER_REJECT;\n if (p.offsetWidth === 0 || p.offsetHeight === 0) return NodeFilter.FILTER_REJECT;\n return NodeFilter.FILTER_ACCEPT;\n },\n });\n while (walk.nextNode()) entries.push({ node: walk.currentNode, text: walk.currentNode.textContent });\n\n if (entries.length === 0) { showToast(\"没有可翻译的文本\", \"info\"); return; }\n originalTexts = entries;\n\n const nodes = entries.map((e) => e.node);\n const needApi = [];\n const cached = [];\n\n nodes.forEach((node, i) => {\n const key = node.textContent.trim();\n if (transCache.has(key)) {\n cached.push(i);\n } else {\n needApi.push(i);\n }\n });\n\n cached.forEach((i) => {\n const key = nodes[i].textContent.trim();\n const t = transCache.get(key);\n nodes[i].textContent = cfg.mode === \"bilingual\" ? key + \"\\n\" + t : t;\n });\n\n if (needApi.length > 0) {\n if (cfg.engine === \"google\") {\n await translateGoogle(nodes, needApi);\n } else if (cfg.engine === \"baidu\") {\n if (baiduMode === \"llm\") {\n await translateBaiduLLM(nodes, needApi);\n } else {\n await translateBaidu(nodes, needApi);\n }\n } else {\n await translateDeepSeek(nodes, needApi);\n }\n }\n\n cacheHint.textContent = `🗑️ 清除翻译缓存 (${transCache.size}条)`;\n\n translated = true;\n fab.classList.add(\"translated\"); fab.textContent = \"✅\";\n setProgress(100, \"✅ 翻译完成\");\n const fromCache = cached.length > 0 ? ` (缓存${cached.length}段)` : \"\";\n showToast(`翻译完成${fromCache},再点恢复原文`, \"ok\");\n } catch (err) {\n showToast(\"出错: \" + err.message, \"err\");\n }\n setTimeout(() => { progress.classList.remove(\"show\"); translating = false; }, 1500);\n }\n\n // ====== Google 翻译 ======\n async function translateGoogle(nodes, indices) {\n const batchSize = 5;\n for (let ki = 0; ki < indices.length; ki += batchSize) {\n if (stopRequested) { showToast(\"⏹️ 已停止\", \"info\"); break; }\n const batchIdx = indices.slice(ki, ki + batchSize);\n const pct = Math.min(100, Math.round(((ki + batchIdx.length) / indices.length) * 100));\n setProgress(pct, `Google 翻译中 ${Math.min(ki + batchIdx.length, indices.length)}/${indices.length}...`);\n\n for (const i of batchIdx) {\n if (stopRequested) break;\n const text = nodes[i].textContent.trim();\n try {\n const t = await callGoogle(text);\n if (t) {\n transCache.set(text, t);\n nodes[i].textContent = cfg.mode === \"bilingual\" ? text + \"\\n\" + t : t;\n }\n } catch (e) {\n console.warn(\"Google翻译失败:\", e.message?.slice(0, 30));\n }\n }\n }\n }\n\n function callGoogle(text) {\n return new Promise((resolve, reject) => {\n const url = \"https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=zh-CN&dt=t&q=\" + encodeURIComponent(text);\n GM_xmlhttpRequest({\n method: \"GET\", url,\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data && data[0]) {\n const result = data[0].map((r) => r[0]).join(\"\");\n resolve(result || null);\n } else {\n reject(new Error(\"无结果\"));\n }\n } catch { reject(new Error(\"解析失败\")); }\n },\n onerror: () => reject(new Error(\"网络错误\")),\n timeout: 15000,\n });\n });\n }\n\n // ====== 百度通用翻译(appid + key) ======\n async function translateBaidu(nodes, indices) {\n const sep = \"\\n|||ds-sep|||\\n\";\n const maxBytes = 5000;\n const batches = [];\n let cur = [], curBytes = 0;\n for (let ki = 0; ki < indices.length; ki++) {\n const t = nodes[indices[ki]].textContent.trim();\n const bytes = new Blob([t]).size;\n if (cur.length > 0 && curBytes + bytes + 20 > maxBytes) {\n batches.push(cur);\n cur = []; curBytes = 0;\n }\n cur.push({ idx: indices[ki], text: t, bytes });\n curBytes += bytes + 20;\n }\n if (cur.length) batches.push(cur);\n\n for (let b = 0; b < batches.length; b += 3) {\n if (stopRequested) { showToast(\"⏹️ 已停止\", \"info\"); break; }\n const chunk = batches.slice(b, b + 3);\n const pct = Math.min(100, Math.round(((b + chunk.length) / batches.length) * 100));\n setProgress(pct, `百度翻译中 ${Math.min((b + chunk.length) * 30, indices.length)}/${indices.length}...`);\n\n await Promise.all(chunk.map(async (batch) => {\n try {\n const joined = batch.map(x => x.text).join(sep);\n const result = await callBaidu(joined);\n if (result) {\n const parts = result.split(\"|||ds-sep|||\").map((s) => s.trim());\n batch.forEach((item, idx) => {\n const t = parts[idx];\n if (t && t !== item.text) {\n transCache.set(item.text, t);\n const node = nodes[item.idx];\n node.textContent = cfg.mode === \"bilingual\" ? item.text + \"\\n\" + t : t;\n }\n });\n }\n } catch (e) {\n console.warn(\"百度批量失败:\", e.message.slice(0, 30));\n for (const item of batch) {\n if (stopRequested) break;\n try {\n const r = await callBaidu(item.text);\n if (r) {\n transCache.set(item.text, r);\n nodes[item.idx].textContent = cfg.mode === \"bilingual\" ? item.text + \"\\n\" + r : r;\n }\n } catch {}\n }\n }\n }));\n }\n }\n\n function callBaidu(text) {\n return new Promise((resolve, reject) => {\n const appid = cfg.baiduAppId;\n const key = cfg.baiduKey;\n const salt = Date.now() + Math.random();\n const sign = md5(appid + text + salt + key);\n const url = `https://fanyi-api.baidu.com/api/trans/vip/translate?q=${encodeURIComponent(text)}&from=auto&to=zh&appid=${appid}&salt=${salt}&sign=${sign}`;\n GM_xmlhttpRequest({\n method: \"GET\", url,\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data.trans_result) resolve(data.trans_result.map((r) => r.dst).join(\"\\n\"));\n else if (data.error_code) reject(new Error(`[${data.error_code}] ${data.error_msg}`));\n else reject(new Error(\"返回异常\"));\n } catch { reject(new Error(\"解析失败\")); }\n },\n onerror: () => reject(new Error(\"网络错误\")),\n timeout: 15000,\n });\n });\n }\n\n // ====== 百度大模型翻译(单 APP ID) ======\n async function translateBaiduLLM(nodes, indices) {\n const batchSize = 5;\n for (let ki = 0; ki < indices.length; ki += batchSize) {\n if (stopRequested) { showToast(\"⏹️ 已停止\", \"info\"); break; }\n const batchIdx = indices.slice(ki, ki + batchSize);\n const pct = Math.min(100, Math.round(((ki + batchIdx.length) / indices.length) * 100));\n setProgress(pct, `百度大模型翻译中 ${Math.min(ki + batchIdx.length, indices.length)}/${indices.length}...`);\n\n for (const i of batchIdx) {\n if (stopRequested) break;\n const text = nodes[i].textContent.trim();\n try {\n const t = await callBaiduLLM(text);\n if (t) {\n transCache.set(text, t);\n nodes[i].textContent = cfg.mode === \"bilingual\" ? text + \"\\n\" + t : t;\n }\n } catch (e) {\n console.warn(\"百度大模型翻译失败:\", e.message?.slice(0, 30));\n }\n }\n }\n }\n\n function callBaiduLLM(text) {\n return new Promise((resolve, reject) => {\n const appid = cfg.baiduLlmAppId;\n const salt = Date.now() + Math.random();\n const sign = md5(appid + text + salt + appid);\n const url = `https://fanyi-api.baidu.com/api/trans/llm/translate?q=${encodeURIComponent(text)}&from=auto&to=zh&appid=${appid}&salt=${salt}&sign=${sign}`;\n GM_xmlhttpRequest({\n method: \"GET\", url,\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data.trans_result) resolve(data.trans_result.map((r) => r.dst).join(\"\\n\"));\n else if (data.result) resolve(data.result);\n else if (data.error_code) reject(new Error(`[${data.error_code}] ${data.error_msg}`));\n else reject(new Error(\"返回异常\"));\n } catch { reject(new Error(\"解析失败\")); }\n },\n onerror: () => reject(new Error(\"网络错误\")),\n timeout: 30000,\n });\n });\n }\n\n // ====== DeepSeek API ======\n async function translateDeepSeek(nodes, indices) {\n const batchSize = 20;\n const sep = \"\\n---\\n\";\n for (let ki = 0; ki < indices.length; ki += batchSize) {\n if (stopRequested) { showToast(\"⏹️ 已停止\", \"info\"); break; }\n const batchIdx = indices.slice(ki, ki + batchSize);\n const texts = batchIdx.map((i) => nodes[i].textContent.trim());\n const pct = Math.min(100, Math.round(((ki + batchIdx.length) / indices.length) * 100));\n setProgress(pct, `DeepSeek 翻译中 ${Math.min(ki + batchIdx.length, indices.length)}/${indices.length}...`);\n\n const result = await callDeepSeek(texts);\n if (result) {\n batchIdx.forEach((i, idx) => {\n const orig = texts[idx];\n if (result[idx]) {\n transCache.set(orig, result[idx]);\n nodes[i].textContent = cfg.mode === \"bilingual\" ? orig + \"\\n\" + result[idx] : result[idx];\n }\n });\n }\n }\n }\n\n function callDeepSeek(texts) {\n return new Promise((resolve) => {\n const joined = texts.map((t, i) => `[${i + 1}] ${t}`).join(\"\\n---\\n\");\n const prompt = `逐段翻译为简体中文,保持编号格式,只返回翻译结果:\\n\\n${joined}`;\n GM_xmlhttpRequest({\n method: \"POST\",\n url: \"https://api.deepseek.com/chat/completions\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${cfg.dsKey}` },\n data: JSON.stringify({\n model: cfg.dsModel,\n messages: [{ role: \"system\", content: \"逐段翻译,保持编号。\" }, { role: \"user\", content: prompt }],\n temperature: 0.3, max_tokens: 8192,\n }),\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data.choices && data.choices[0]) {\n const content = data.choices[0].message.content.trim();\n const results = [];\n for (let i = 0; i < texts.length; i++) {\n const re = new RegExp(`\\\\[${i + 1}\\\\]\\\\s*([\\\\s\\\\S]*?)(?=\\\\[${i + 2}\\\\]|$)`);\n const m = content.match(re);\n results.push(m ? m[1].trim() : \"\");\n }\n resolve(results);\n } else resolve(null);\n } catch { resolve(null); }\n },\n onerror: () => resolve(null),\n timeout: 60000,\n });\n });\n }\n\n // ====== MD5 ======\n function md5(str) {\n function rotateLeft(x, n) { return (x << n) | (x >>> (32 - n)); }\n function toHex(x) {\n var s = \"\";\n for (var i = 0; i < 4; i++) {\n s += (\"0\" + ((x >> (i * 8)) & 0xff).toString(16)).slice(-2);\n }\n return s;\n }\n function addUnsigned(x, y) {\n var lsw = (x & 0xffff) + (y & 0xffff);\n return ((x >>> 16) + (y >>> 16) + (lsw >>> 16)) << 16 | (lsw & 0xffff);\n }\n var i, j, aa, bb, cc, dd;\n var S = [[7, 12, 17, 22], [5, 9, 14, 20], [4, 11, 16, 23], [6, 10, 15, 21]];\n var T = [];\n for (i = 0; i < 64; i++) T[i] = Math.floor(Math.abs(Math.sin(i + 1)) * 0x100000000);\n str = unescape(encodeURIComponent(str));\n var n = str.length, bits = [];\n for (i = 0; i < n; i++) bits.push(str.charCodeAt(i));\n bits.push(0x80);\n var len = bits.length / 4 + 2;\n len = Math.ceil(len / 16) * 16;\n var x = [];\n for (i = 0; i < len; i++) x[i] = 0;\n for (i = 0; i < bits.length; i++) x[i >> 2] |= bits[i] << ((i % 4) * 8);\n x[x.length - 2] = n * 8;\n var a = 0x67452301, b = 0xefcdab89, c = 0x98badcfe, d = 0x10325476;\n for (i = 0; i < x.length; i += 16) {\n var X = x.slice(i, i + 16);\n aa = a; bb = b; cc = c; dd = d;\n for (j = 0; j < 64; j++) {\n var g, f;\n if (j < 16) { g = j; f = (bb & cc) | (~bb & dd); }\n else if (j < 32) { g = (5 * j + 1) % 16; f = (dd & bb) | (~dd & cc); }\n else if (j < 48) { g = (3 * j + 5) % 16; f = bb ^ cc ^ dd; }\n else { g = (7 * j) % 16; f = cc ^ (bb | ~dd); }\n var temp = dd; dd = cc; cc = bb;\n bb = addUnsigned(bb, rotateLeft(addUnsigned(addUnsigned(addUnsigned(aa, f), T[j]), X[g]), S[j >> 4][j & 3]));\n aa = temp;\n }\n a = addUnsigned(a, aa); b = addUnsigned(b, bb);\n c = addUnsigned(c, cc); d = addUnsigned(d, dd);\n }\n return (toHex(a) + toHex(b) + toHex(c) + toHex(d)).toLowerCase();\n }\n\n // ====== 工具 ======\n function setProgress(pct, text) { progressBar.style.width = pct + \"%\"; progressText.textContent = text; }\n function showToast(msg, type) { toast.textContent = msg; toast.className = type; toast.style.display = \"block\"; setTimeout(() => { toast.style.display = \"none\"; }, 3000); }\n function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }\n\n document.addEventListener(\"keydown\", (e) => { if (e.key === \"Escape\") panel.classList.remove(\"show\"); });\n\n const engName = cfg.engine === \"google\" ? \"Google\" : cfg.engine === \"baidu\" ? (\"百度\" + (baiduMode === \"llm\" ? \"·大模型\" : \"·通用\")) : \"DeepSeek\";\n const modeName = cfg.mode === \"bilingual\" ? \"双语\" : \"仅译文\";\n console.log(`✅ 网页翻译已加载 | ${engName} | ${modeName} | 点击翻译/恢复,长按配置`);\n})();\n"}, "b7d3d14a1c407a90d9c29ca76bb4be6ac600d996": {"t": 1785766102.3089912, "n": 32299, "h": "dbb3d5229c5cac59", "c": "// ==UserScript==\n// @name 网页翻译 · 多引擎\n// @namespace http://tampermonkey.net/\n// @version 1.5\n// @description 点击翻译整个页面/恢复原文。支持Google(免费)/百度通用(极速)/百度大模型(AI)/DeepSeek(高质量),双语/单语模式,翻译结果缓存。长按进入配置。\n// @author you\n// @match *://*/*\n// @icon data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌐</text></svg>\n// @grant GM_setValue\n// @grant GM_getValue\n// @grant GM_addStyle\n// @grant GM_xmlhttpRequest\n// @license MIT\n// ==/UserScript==\n\n(function () {\n \"use strict\";\n\n // ====== 配置 ======\n const cfg = {\n engine: GM_getValue(\"ds_engine\", \"google\"),\n mode: GM_getValue(\"ds_mode\", \"bilingual\"),\n // 百度通用翻译 (appid + secretKey)\n baiduAppId: GM_getValue(\"ds_baidu_id\", \"\"),\n baiduKey: GM_getValue(\"ds_baidu_key\", \"\"),\n // 百度大模型翻译 (API_Key + Secret_Key)\n baiduApiKey: GM_getValue(\"ds_baidu_ak\", \"\"),\n baiduSecretKey: GM_getValue(\"ds_baidu_sk\", \"\"),\n // DeepSeek\n dsKey: GM_getValue(\"ds_api_key\", \"\"),\n dsModel: GM_getValue(\"ds_model\", \"deepseek-chat\"),\n };\n\n // ====== 状态 ======\n let translated = false;\n let translating = false;\n let stopRequested = false;\n let originalTexts = null;\n const transCache = new Map();\n\n // ====== 样式 ======\n GM_addStyle(`\n #ds-fab {\n position: fixed; bottom: 24px; right: 24px; z-index: 999999;\n width: 52px; height: 52px; border-radius: 50%; background: #4f6ef7;\n color: #fff; border: none; font-size: 24px; cursor: pointer;\n box-shadow: 0 2px 12px rgba(79,110,247,0.4);\n transition: transform 0.15s, background 0.3s;\n display: flex; align-items: center; justify-content: center;\n user-select: none; -webkit-user-select: none;\n }\n #ds-fab:active { transform: scale(0.92); }\n #ds-fab.translated { background: #a6e3a1; }\n\n #ds-panel {\n position: fixed; bottom: 88px; right: 24px; z-index: 999999;\n width: 320px; max-height: 70vh; overflow-y: auto;\n background: #1e1e2e; border-radius: 16px; padding: 20px;\n box-shadow: 0 8px 40px rgba(0,0,0,0.5); color: #cdd6f4;\n font: 14px -apple-system, sans-serif; display: none; border: 1px solid #313244;\n }\n #ds-panel.show { display: block; }\n #ds-panel h3 {\n margin: 0 0 16px 0; font-size: 16px; color: #cba6f7;\n display: flex; justify-content: space-between; align-items: center;\n }\n #ds-panel h3 button { background: none; border: none; color: #a6adc8; cursor: pointer; font-size: 18px; padding: 0 4px; }\n #ds-panel h3 button:hover { color: #f38ba8; }\n #ds-panel label { display: block; margin-bottom: 4px; color: #a6adc8; font-size: 12px; margin-top: 8px; }\n #ds-panel label:first-of-type { margin-top: 0; }\n #ds-panel input, #ds-panel select {\n width: 100%; padding: 8px 12px; margin-bottom: 4px;\n background: #313244; border: 1px solid #45475a; border-radius: 8px;\n color: #cdd6f4; font-size: 13px; box-sizing: border-box; outline: none;\n }\n #ds-panel input:focus, #ds-panel select:focus { border-color: #4f6ef7; }\n #ds-panel .hint { font-size: 11px; color: #6c7086; margin: 2px 0 8px 0; }\n\n #ds-panel .engine-tab {\n display: flex; gap: 6px; margin-bottom: 6px;\n }\n #ds-panel .engine-tab button {\n flex: 1; padding: 7px; border: 1px solid #45475a; border-radius: 8px;\n background: #313244; color: #a6adc8; cursor: pointer; font-size: 12px; transition: all 0.2s;\n }\n #ds-panel .engine-tab button.active { background: #4f6ef7; border-color: #4f6ef7; color: #fff; }\n\n #ds-panel .baidu-subtab {\n display: flex; gap: 6px; margin: 6px 0 10px 0;\n }\n #ds-panel .baidu-subtab button {\n flex: 1; padding: 5px; border: 1px solid #45475a; border-radius: 6px;\n background: #313244; color: #a6adc8; cursor: pointer; font-size: 11px; transition: all 0.2s;\n }\n #ds-panel .baidu-subtab button.active { background: #4f6ef7; border-color: #4f6ef7; color: #fff; }\n\n #ds-panel .mode-tab {\n display: flex; gap: 6px; margin-bottom: 12px;\n }\n #ds-panel .mode-tab button {\n flex: 1; padding: 6px; border: 1px solid #45475a; border-radius: 8px;\n background: #313244; color: #a6adc8; cursor: pointer; font-size: 12px; transition: all 0.2s;\n }\n #ds-panel .mode-tab button.active { background: #585b70; border-color: #585b70; color: #fff; }\n\n #ds-panel .cfg-section { display: none; }\n #ds-panel .cfg-section.show { display: block; }\n #ds-panel .btn-row { display: flex; gap: 8px; margin-top: 12px; }\n #ds-panel .btn-row button {\n flex: 1; padding: 9px; border: none; border-radius: 8px;\n cursor: pointer; font-size: 13px; transition: opacity 0.2s;\n }\n #ds-panel .btn-row button:active { opacity: 0.8; }\n #ds-panel .btn-save { background: #4f6ef7; color: #fff; }\n #ds-panel .btn-close-cfg { background: #45475a; color: #cdd6f4; }\n #ds-panel .btn-clear-cache { background: #45475a; color: #f38ba8; }\n\n #ds-progress { margin-top: 10px; display: none; }\n #ds-progress.show { display: block; }\n #ds-progress .bar-wrap { width: 100%; height: 4px; background: #313244; border-radius: 2px; overflow: hidden; }\n #ds-progress .bar { height: 100%; width: 0%; background: #a6e3a1; border-radius: 2px; transition: width 0.3s; }\n #ds-progress .text { font-size: 11px; color: #a6adc8; margin-top: 4px; text-align: center; }\n\n #ds-toast {\n position: fixed; top: 20px; left: 50%; transform: translateX(-50%);\n z-index: 9999999; padding: 10px 24px; border-radius: 10px;\n font: 14px -apple-system, sans-serif; color: #fff;\n display: none; box-shadow: 0 4px 20px rgba(0,0,0,0.3);\n }\n #ds-toast.ok { background: #1e3a2e; }\n #ds-toast.err { background: #3a1e1e; }\n #ds-toast.info { background: #1e2a3a; }\n `);\n\n // ====== DOM ======\n document.body.insertAdjacentHTML(\"beforeend\", `\n <button id=\"ds-fab\">🌐</button>\n <div id=\"ds-panel\">\n <h3>\n ⚙️ 翻译设置\n <button id=\"ds-close-panel\">✕</button>\n </h3>\n\n <label>翻译引擎</label>\n <div class=\"engine-tab\">\n <button data-engine=\"google\" class=\"active\">🌍 Google</button>\n <button data-engine=\"baidu\">⚡ 百度</button>\n <button data-engine=\"deepseek\">🧠 DeepSeek</button>\n </div>\n\n <div class=\"cfg-section show\" id=\"ds-cfg-baidu-hint\" style=\"display:none\">\n <div class=\"baidu-subtab\">\n <button data-baidu=\"normal\" class=\"active\">通用翻译</button>\n <button data-baidu=\"llm\">大模型翻译</button>\n </div>\n </div>\n\n <label>显示模式</label>\n <div class=\"mode-tab\">\n <button data-mode=\"bilingual\" class=\"active\">📖 双语</button>\n <button data-mode=\"monolingual\">🔤 仅译文</button>\n </div>\n\n <div class=\"cfg-section show\" id=\"ds-cfg-google\">\n <div class=\"hint\">Google 翻译免费使用,无需配置</div>\n </div>\n <div class=\"cfg-section\" id=\"ds-cfg-baidu-normal\">\n <label>APP ID</label>\n <input id=\"ds-baidu-id\" placeholder=\"从 fanyi-api.baidu.com 获取\" value=\"${cfg.baiduAppId}\">\n <label>密钥</label>\n <input type=\"password\" id=\"ds-baidu-key\" placeholder=\"从 fanyi-api.baidu.com 获取\" value=\"${cfg.baiduKey}\">\n <div class=\"hint\">免费版每月 100 万字符</div>\n </div>\n <div class=\"cfg-section\" id=\"ds-cfg-baidu-llm\">\n <label>API Key</label>\n <input type=\"password\" id=\"ds-baidu-ak\" placeholder=\"API Key(管理控制台获取)\" value=\"${cfg.baiduApiKey}\">\n <label>Secret Key</label>\n <input type=\"password\" id=\"ds-baidu-sk\" placeholder=\"Secret Key(管理控制台获取)\" value=\"${cfg.baiduSecretKey}\">\n <div class=\"hint\">免费版每月 100 万字符</div>\n </div>\n <div class=\"cfg-section\" id=\"ds-cfg-ds\">\n <label>API Key</label>\n <input type=\"password\" id=\"ds-ds-key\" placeholder=\"sk-xxxxxxxx\" value=\"${cfg.dsKey}\">\n <label>模型</label>\n <select id=\"ds-ds-model\">\n <option value=\"deepseek-chat\" ${cfg.dsModel === \"deepseek-chat\" ? \"selected\" : \"\"}>DeepSeek V3</option>\n <option value=\"deepseek-reasoner\" ${cfg.dsModel === \"deepseek-reasoner\" ? \"selected\" : \"\"}>DeepSeek R1</option>\n </select>\n <div class=\"hint\">质量高但速度较慢</div>\n </div>\n <div class=\"btn-row\">\n <button class=\"btn-save\" id=\"ds-save-btn\">💾 保存配置</button>\n <button class=\"btn-close-cfg\" id=\"ds-close-btn\">关闭</button>\n </div>\n <div class=\"hint\" style=\"text-align:center;margin-top:8px;cursor:pointer\" id=\"ds-clear-cache\">🗑️ 清除翻译缓存 (${transCache.size}条)</div>\n <div id=\"ds-progress\">\n <div class=\"bar-wrap\"><div class=\"bar\" id=\"ds-progress-bar\"></div></div>\n <div class=\"text\" id=\"ds-progress-text\">准备中...</div>\n </div>\n </div>\n <div id=\"ds-toast\"></div>\n `);\n\n // ====== DOM 引用 ======\n const fab = document.getElementById(\"ds-fab\");\n const panel = document.getElementById(\"ds-panel\");\n const progress = document.getElementById(\"ds-progress\");\n const progressBar = document.getElementById(\"ds-progress-bar\");\n const progressText = document.getElementById(\"ds-progress-text\");\n const toast = document.getElementById(\"ds-toast\");\n const cacheHint = document.getElementById(\"ds-clear-cache\");\n\n // ====== 百度子模式 ======\n let baiduMode = GM_getValue(\"ds_baidu_mode\", \"normal\");\n\n function showBaiduConfig() {\n document.getElementById(\"ds-cfg-baidu-normal\").classList.toggle(\"show\", baiduMode === \"normal\");\n document.getElementById(\"ds-cfg-baidu-llm\").classList.toggle(\"show\", baiduMode === \"llm\");\n document.querySelectorAll(\".baidu-subtab button\").forEach((b) => {\n b.classList.toggle(\"active\", b.dataset.baidu === baiduMode);\n });\n }\n\n document.querySelectorAll(\".baidu-subtab button\").forEach((btn) => {\n btn.addEventListener(\"click\", () => {\n document.querySelectorAll(\".baidu-subtab button\").forEach((b) => b.classList.remove(\"active\"));\n btn.classList.add(\"active\");\n baiduMode = btn.dataset.baidu;\n showBaiduConfig();\n });\n });\n\n // ====== 引擎切换 ======\n document.querySelectorAll(\".engine-tab button\").forEach((btn) => {\n btn.addEventListener(\"click\", () => {\n document.querySelectorAll(\".engine-tab button\").forEach((b) => b.classList.remove(\"active\"));\n btn.classList.add(\"active\");\n cfg.engine = btn.dataset.engine;\n document.getElementById(\"ds-cfg-google\").classList.toggle(\"show\", cfg.engine === \"google\");\n document.getElementById(\"ds-cfg-baidu-hint\").style.display = cfg.engine === \"baidu\" ? \"block\" : \"none\";\n document.getElementById(\"ds-cfg-baidu-normal\").classList.toggle(\"show\", cfg.engine === \"baidu\" && baiduMode === \"normal\");\n document.getElementById(\"ds-cfg-baidu-llm\").classList.toggle(\"show\", cfg.engine === \"baidu\" && baiduMode === \"llm\");\n document.getElementById(\"ds-cfg-ds\").classList.toggle(\"show\", cfg.engine === \"deepseek\");\n });\n });\n // 初始状态:显示百度子tab\n if (cfg.engine === \"baidu\") {\n document.getElementById(\"ds-cfg-baidu-hint\").style.display = \"block\";\n showBaiduConfig();\n }\n\n // ====== 模式切换 ======\n document.querySelectorAll(\".mode-tab button\").forEach((btn) => {\n btn.addEventListener(\"click\", () => {\n document.querySelectorAll(\".mode-tab button\").forEach((b) => b.classList.remove(\"active\"));\n btn.classList.add(\"active\");\n cfg.mode = btn.dataset.mode;\n });\n });\n\n // ====== 保存 / 关闭 ======\n document.getElementById(\"ds-save-btn\").addEventListener(\"click\", () => {\n saveConfig();\n showToast(\"✅ 配置已保存\", \"ok\");\n });\n document.getElementById(\"ds-close-btn\").addEventListener(\"click\", () => panel.classList.remove(\"show\"));\n document.getElementById(\"ds-close-panel\").addEventListener(\"click\", () => panel.classList.remove(\"show\"));\n\n cacheHint.addEventListener(\"click\", () => {\n transCache.clear();\n cacheHint.textContent = `🗑️ 清除翻译缓存 (0条)`;\n showToast(\"缓存已清除\", \"info\");\n });\n\n // ====== 保存配置 ======\n function saveConfig() {\n cfg.baiduAppId = document.getElementById(\"ds-baidu-id\").value.trim();\n cfg.baiduKey = document.getElementById(\"ds-baidu-key\").value.trim();\n cfg.baiduApiKey = document.getElementById(\"ds-baidu-ak\").value.trim();\n cfg.baiduSecretKey = document.getElementById(\"ds-baidu-sk\").value.trim();\n cfg.dsKey = document.getElementById(\"ds-ds-key\").value.trim();\n cfg.dsModel = document.getElementById(\"ds-ds-model\").value;\n\n GM_setValue(\"ds_engine\", cfg.engine);\n GM_setValue(\"ds_mode\", cfg.mode);\n GM_setValue(\"ds_baidu_id\", cfg.baiduAppId);\n GM_setValue(\"ds_baidu_key\", cfg.baiduKey);\n GM_setValue(\"ds_baidu_ak\", cfg.baiduApiKey);\n GM_setValue(\"ds_baidu_sk\", cfg.baiduSecretKey);\n GM_setValue(\"ds_baidu_mode\", baiduMode);\n GM_setValue(\"ds_api_key\", cfg.dsKey);\n GM_setValue(\"ds_model\", cfg.dsModel);\n }\n\n // ====== 可拖动按钮 ======\n let isDragging = false, dragStartX = 0, dragStartY = 0, dragMoved = false;\n let longPressTimer = null, longPressStart = 0, isLongPress = false;\n\n fab.addEventListener(\"mousedown\", (e) => {\n if (e.button !== 0) return;\n isDragging = true; dragMoved = false;\n const rect = fab.getBoundingClientRect();\n dragStartX = e.clientX; dragStartY = e.clientY;\n fab._origLeft = rect.left; fab._origTop = rect.top;\n fab.style.cursor = \"grabbing\";\n fab.style.transition = \"none\";\n });\n\n document.addEventListener(\"mousemove\", (e) => {\n if (!isDragging) return;\n const dx = e.clientX - dragStartX, dy = e.clientY - dragStartY;\n if (Math.abs(dx) > 5 || Math.abs(dy) > 5) dragMoved = true;\n const vw = window.innerWidth, vh = window.innerHeight;\n const fw = 52, fh = 52;\n let nx = Math.max(0, Math.min(vw - fw, fab._origLeft + dx));\n let ny = Math.max(0, Math.min(vh - fh, fab._origTop + dy));\n fab.style.left = nx + \"px\"; fab.style.right = \"auto\";\n fab.style.top = ny + \"px\"; fab.style.bottom = \"auto\";\n });\n\n document.addEventListener(\"mouseup\", (e) => {\n if (!isDragging) return;\n isDragging = false;\n fab.style.transition = \"\";\n fab.style.cursor = \"\";\n if (dragMoved) { e.stopPropagation(); e.preventDefault(); }\n });\n\n fab.addEventListener(\"click\", (e) => {\n if (dragMoved) { e.stopPropagation(); e.preventDefault(); return; }\n if (\"ontouchstart\" in window) return;\n toggleTranslate();\n });\n\n fab.addEventListener(\"touchstart\", (e) => {\n const t = e.touches[0];\n isDragging = true; dragMoved = false;\n const rect = fab.getBoundingClientRect();\n dragStartX = t.clientX; dragStartY = t.clientY;\n fab._origLeft = rect.left; fab._origTop = rect.top;\n fab.style.transition = \"none\";\n longPressStart = Date.now(); isLongPress = false;\n longPressTimer = setTimeout(() => {\n if (!dragMoved) { isLongPress = true; navigator.vibrate?.(20); panel.classList.add(\"show\"); }\n }, 500);\n }, { passive: true });\n\n fab.addEventListener(\"touchmove\", (e) => {\n if (!isDragging) return;\n const t = e.touches[0];\n const dx = t.clientX - dragStartX, dy = t.clientY - dragStartY;\n if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {\n dragMoved = true;\n clearTimeout(longPressTimer);\n }\n const vw = window.innerWidth, vh = window.innerHeight;\n const fw = 52, fh = 52;\n let nx = Math.max(0, Math.min(vw - fw, fab._origLeft + dx));\n let ny = Math.max(0, Math.min(vh - fh, fab._origTop + dy));\n fab.style.left = nx + \"px\"; fab.style.right = \"auto\";\n fab.style.top = ny + \"px\"; fab.style.bottom = \"auto\";\n e.preventDefault();\n }, { passive: false });\n\n fab.addEventListener(\"touchend\", (e) => {\n isDragging = false;\n clearTimeout(longPressTimer);\n fab.style.transition = \"\";\n if (!dragMoved && !isLongPress && Date.now() - longPressStart < 500) {\n e.preventDefault();\n toggleTranslate();\n }\n }, { passive: false });\n\n fab.addEventListener(\"contextmenu\", (e) => { e.preventDefault(); panel.classList.toggle(\"show\"); });\n\n // ====== 切换 ======\n async function toggleTranslate() {\n if (translating) return;\n saveConfig();\n\n if (translating) {\n stopRequested = true;\n showToast(\"⏹️ 正在停止...\", \"info\");\n return;\n }\n if (translated) { restoreOriginal(); return; }\n\n if (cfg.engine === \"baidu\") {\n if (baiduMode === \"normal\" && (!cfg.baiduAppId || !cfg.baiduKey)) {\n showToast(\"请配置百度通用翻译(APP ID + 密钥)\", \"err\"); return;\n }\n if (baiduMode === \"llm\" && (!cfg.baiduApiKey || !cfg.baiduSecretKey)) {\n showToast(\"请配置百度大模型翻译(API Key + Secret Key)\", \"err\"); return;\n }\n }\n if (cfg.engine === \"deepseek\" && !cfg.dsKey) {\n showToast(\"请先配置 DeepSeek Key(长按图标设置)\", \"err\"); return;\n }\n\n await translatePage();\n }\n\n function restoreOriginal() {\n if (!originalTexts) return;\n originalTexts.forEach(({ node, text }) => { node.textContent = text; });\n translated = false; fab.classList.remove(\"translated\"); fab.textContent = \"🌐\";\n showToast(\"已恢复原文\", \"info\");\n }\n\n // ====== 翻译页面 ======\n async function translatePage() {\n translating = true;\n progress.classList.add(\"show\");\n setProgress(0, \"提取文本...\");\n\n try {\n const entries = [];\n const walk = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {\n acceptNode: (n) => {\n const p = n.parentElement;\n if (!p || p.closest(\"script,style,code,pre,#ds-panel,#ds-fab,#ds-toast,svg\"))\n return NodeFilter.FILTER_REJECT;\n const t = n.textContent.trim();\n if (t.length < 10) return NodeFilter.FILTER_REJECT;\n if (p.offsetWidth === 0 || p.offsetHeight === 0) return NodeFilter.FILTER_REJECT;\n return NodeFilter.FILTER_ACCEPT;\n },\n });\n while (walk.nextNode()) entries.push({ node: walk.currentNode, text: walk.currentNode.textContent });\n\n if (entries.length === 0) { showToast(\"没有可翻译的文本\", \"info\"); return; }\n originalTexts = entries;\n\n const nodes = entries.map((e) => e.node);\n const needApi = [];\n const cached = [];\n\n nodes.forEach((node, i) => {\n const key = node.textContent.trim();\n if (transCache.has(key)) {\n cached.push(i);\n } else {\n needApi.push(i);\n }\n });\n\n cached.forEach((i) => {\n const key = nodes[i].textContent.trim();\n const t = transCache.get(key);\n nodes[i].textContent = cfg.mode === \"bilingual\" ? key + \"\\n\" + t : t;\n });\n\n if (needApi.length > 0) {\n if (cfg.engine === \"google\") {\n await translateGoogle(nodes, needApi);\n } else if (cfg.engine === \"baidu\") {\n if (baiduMode === \"llm\") {\n await translateBaiduLLM(nodes, needApi);\n } else {\n await translateBaidu(nodes, needApi);\n }\n } else {\n await translateDeepSeek(nodes, needApi);\n }\n }\n\n cacheHint.textContent = `🗑️ 清除翻译缓存 (${transCache.size}条)`;\n\n translated = true;\n fab.classList.add(\"translated\"); fab.textContent = \"✅\";\n setProgress(100, \"✅ 翻译完成\");\n const fromCache = cached.length > 0 ? ` (缓存${cached.length}段)` : \"\";\n showToast(`翻译完成${fromCache},再点恢复原文`, \"ok\");\n } catch (err) {\n showToast(\"出错: \" + err.message, \"err\");\n }\n setTimeout(() => { progress.classList.remove(\"show\"); translating = false; }, 1500);\n }\n\n // ====== Google 翻译 ======\n async function translateGoogle(nodes, indices) {\n const batchSize = 5;\n for (let ki = 0; ki < indices.length; ki += batchSize) {\n if (stopRequested) { showToast(\"⏹️ 已停止\", \"info\"); break; }\n const batchIdx = indices.slice(ki, ki + batchSize);\n const pct = Math.min(100, Math.round(((ki + batchIdx.length) / indices.length) * 100));\n setProgress(pct, `Google 翻译中 ${Math.min(ki + batchIdx.length, indices.length)}/${indices.length}...`);\n\n for (const i of batchIdx) {\n if (stopRequested) break;\n const text = nodes[i].textContent.trim();\n try {\n const t = await callGoogle(text);\n if (t) {\n transCache.set(text, t);\n nodes[i].textContent = cfg.mode === \"bilingual\" ? text + \"\\n\" + t : t;\n }\n } catch (e) {\n console.warn(\"Google翻译失败:\", e.message?.slice(0, 30));\n }\n }\n }\n }\n\n function callGoogle(text) {\n return new Promise((resolve, reject) => {\n const url = \"https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=zh-CN&dt=t&q=\" + encodeURIComponent(text);\n GM_xmlhttpRequest({\n method: \"GET\", url,\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data && data[0]) {\n const result = data[0].map((r) => r[0]).join(\"\");\n resolve(result || null);\n } else {\n reject(new Error(\"无结果\"));\n }\n } catch { reject(new Error(\"解析失败\")); }\n },\n onerror: () => reject(new Error(\"网络错误\")),\n timeout: 15000,\n });\n });\n }\n\n // ====== 百度通用翻译(appid + key) ======\n async function translateBaidu(nodes, indices) {\n const sep = \"\\n|||ds-sep|||\\n\";\n const maxBytes = 5000;\n const batches = [];\n let cur = [], curBytes = 0;\n for (let ki = 0; ki < indices.length; ki++) {\n const t = nodes[indices[ki]].textContent.trim();\n const bytes = new Blob([t]).size;\n if (cur.length > 0 && curBytes + bytes + 20 > maxBytes) {\n batches.push(cur);\n cur = []; curBytes = 0;\n }\n cur.push({ idx: indices[ki], text: t, bytes });\n curBytes += bytes + 20;\n }\n if (cur.length) batches.push(cur);\n\n for (let b = 0; b < batches.length; b += 3) {\n if (stopRequested) { showToast(\"⏹️ 已停止\", \"info\"); break; }\n const chunk = batches.slice(b, b + 3);\n const pct = Math.min(100, Math.round(((b + chunk.length) / batches.length) * 100));\n setProgress(pct, `百度翻译中 ${Math.min((b + chunk.length) * 30, indices.length)}/${indices.length}...`);\n\n await Promise.all(chunk.map(async (batch) => {\n try {\n const joined = batch.map(x => x.text).join(sep);\n const result = await callBaidu(joined);\n if (result) {\n const parts = result.split(\"|||ds-sep|||\").map((s) => s.trim());\n batch.forEach((item, idx) => {\n const t = parts[idx];\n if (t && t !== item.text) {\n transCache.set(item.text, t);\n const node = nodes[item.idx];\n node.textContent = cfg.mode === \"bilingual\" ? item.text + \"\\n\" + t : t;\n }\n });\n }\n } catch (e) {\n console.warn(\"百度批量失败:\", e.message.slice(0, 30));\n for (const item of batch) {\n if (stopRequested) break;\n try {\n const r = await callBaidu(item.text);\n if (r) {\n transCache.set(item.text, r);\n nodes[item.idx].textContent = cfg.mode === \"bilingual\" ? item.text + \"\\n\" + r : r;\n }\n } catch {}\n }\n }\n }));\n }\n }\n\n function callBaidu(text) {\n return new Promise((resolve, reject) => {\n const appid = cfg.baiduAppId;\n const key = cfg.baiduKey;\n const salt = Date.now() + Math.random();\n const sign = md5(appid + text + salt + key);\n const url = `https://fanyi-api.baidu.com/api/trans/vip/translate?q=${encodeURIComponent(text)}&from=auto&to=zh&appid=${appid}&salt=${salt}&sign=${sign}`;\n GM_xmlhttpRequest({\n method: \"GET\", url,\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data.trans_result) resolve(data.trans_result.map((r) => r.dst).join(\"\\n\"));\n else if (data.error_code) reject(new Error(`[${data.error_code}] ${data.error_msg}`));\n else reject(new Error(\"返回异常\"));\n } catch { reject(new Error(\"解析失败\")); }\n },\n onerror: () => reject(new Error(\"网络错误\")),\n timeout: 15000,\n });\n });\n }\n\n // ====== 百度大模型翻译(API Key + Secret Key) ======\n let baiduTokenCache = { token: null, expires: 0 };\n\n function getBaiduAccessToken() {\n return new Promise((resolve, reject) => {\n if (baiduTokenCache.token && Date.now() < baiduTokenCache.expires) {\n resolve(baiduTokenCache.token);\n return;\n }\n const url = \"https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=\" + encodeURIComponent(cfg.baiduApiKey) + \"&client_secret=\" + encodeURIComponent(cfg.baiduSecretKey);\n GM_xmlhttpRequest({\n method: \"POST\", url,\n headers: { \"Content-Type\": \"application/json\", \"Accept\": \"application/json\" },\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data.access_token) {\n baiduTokenCache.token = data.access_token;\n baiduTokenCache.expires = Date.now() + (data.expires_in - 60) * 1000;\n resolve(data.access_token);\n } else {\n reject(new Error(\"获取token失败: \" + (data.error_description || data.error || \"未知错误\")));\n }\n } catch { reject(new Error(\"解析token响应失败\")); }\n },\n onerror: () => reject(new Error(\"获取token网络错误\")),\n timeout: 10000,\n });\n });\n }\n\n async function translateBaiduLLM(nodes, indices) {\n // 大模型翻译批量效率高,每次批10条\n const batchSize = 10;\n for (let ki = 0; ki < indices.length; ki += batchSize) {\n if (stopRequested) { showToast(\"⏹️ 已停止\", \"info\"); break; }\n const batchIdx = indices.slice(ki, ki + batchSize);\n const pct = Math.min(100, Math.round(((ki + batchIdx.length) / indices.length) * 100));\n setProgress(pct, `百度大模型翻译中 ${Math.min(ki + batchIdx.length, indices.length)}/${indices.length}...`);\n\n for (const i of batchIdx) {\n if (stopRequested) break;\n const text = nodes[i].textContent.trim();\n try {\n const t = await callBaiduLLM(text);\n if (t) {\n transCache.set(text, t);\n nodes[i].textContent = cfg.mode === \"bilingual\" ? text + \"\\n\" + t : t;\n }\n } catch (e) {\n console.warn(\"百度大模型翻译失败:\", e.message?.slice(0, 30));\n }\n }\n }\n }\n\n function callBaiduLLM(text) {\n return new Promise((resolve, reject) => {\n getBaiduAccessToken().then((token) => {\n const url = \"https://aip.baidubce.com/rpc/2.0/mt/text/v1/llm_translate?access_token=\" + token;\n GM_xmlhttpRequest({\n method: \"POST\", url,\n headers: { \"Content-Type\": \"application/json\" },\n data: JSON.stringify({\n q: text,\n from: \"auto\",\n to: \"zh\",\n }),\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data.result && data.result.trans_result) {\n resolve(data.result.trans_result.map((r) => r.dst).join(\"\\n\"));\n } else if (data.error_code || data.error_msg) {\n reject(new Error(`[${data.error_code}] ${data.error_msg}`));\n } else if (data.result) {\n resolve(data.result);\n } else {\n reject(new Error(\"返回异常\"));\n }\n } catch { reject(new Error(\"解析失败\")); }\n },\n onerror: () => reject(new Error(\"网络错误\")),\n timeout: 30000,\n });\n }).catch(reject);\n });\n }\n\n // ====== DeepSeek API ======\n async function translateDeepSeek(nodes, indices) {\n const batchSize = 20;\n const sep = \"\\n---\\n\";\n for (let ki = 0; ki < indices.length; ki += batchSize) {\n if (stopRequested) { showToast(\"⏹️ 已停止\", \"info\"); break; }\n const batchIdx = indices.slice(ki, ki + batchSize);\n const texts = batchIdx.map((i) => nodes[i].textContent.trim());\n const pct = Math.min(100, Math.round(((ki + batchIdx.length) / indices.length) * 100));\n setProgress(pct, `DeepSeek 翻译中 ${Math.min(ki + batchIdx.length, indices.length)}/${indices.length}...`);\n\n const result = await callDeepSeek(texts);\n if (result) {\n batchIdx.forEach((i, idx) => {\n const orig = texts[idx];\n if (result[idx]) {\n transCache.set(orig, result[idx]);\n nodes[i].textContent = cfg.mode === \"bilingual\" ? orig + \"\\n\" + result[idx] : result[idx];\n }\n });\n }\n }\n }\n\n function callDeepSeek(texts) {\n return new Promise((resolve) => {\n const joined = texts.map((t, i) => `[${i + 1}] ${t}`).join(\"\\n---\\n\");\n const prompt = `逐段翻译为简体中文,保持编号格式,只返回翻译结果:\\n\\n${joined}`;\n GM_xmlhttpRequest({\n method: \"POST\",\n url: \"https://api.deepseek.com/chat/completions\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${cfg.dsKey}` },\n data: JSON.stringify({\n model: cfg.dsModel,\n messages: [{ role: \"system\", content: \"逐段翻译,保持编号。\" }, { role: \"user\", content: prompt }],\n temperature: 0.3, max_tokens: 8192,\n }),\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data.choices && data.choices[0]) {\n const content = data.choices[0].message.content.trim();\n const results = [];\n for (let i = 0; i < texts.length; i++) {\n const re = new RegExp(`\\\\[${i + 1}\\\\]\\\\s*([\\\\s\\\\S]*?)(?=\\\\[${i + 2}\\\\]|$)`);\n const m = content.match(re);\n results.push(m ? m[1].trim() : \"\");\n }\n resolve(results);\n } else resolve(null);\n } catch { resolve(null); }\n },\n onerror: () => resolve(null),\n timeout: 60000,\n });\n });\n }\n\n // ====== MD5 ======\n function md5(str) {\n function rotateLeft(x, n) { return (x << n) | (x >>> (32 - n)); }\n function toHex(x) {\n var s = \"\";\n for (var i = 0; i < 4; i++) {\n s += (\"0\" + ((x >> (i * 8)) & 0xff).toString(16)).slice(-2);\n }\n return s;\n }\n function addUnsigned(x, y) {\n var lsw = (x & 0xffff) + (y & 0xffff);\n return ((x >>> 16) + (y >>> 16) + (lsw >>> 16)) << 16 | (lsw & 0xffff);\n }\n var i, j, aa, bb, cc, dd;\n var S = [[7, 12, 17, 22], [5, 9, 14, 20], [4, 11, 16, 23], [6, 10, 15, 21]];\n var T = [];\n for (i = 0; i < 64; i++) T[i] = Math.floor(Math.abs(Math.sin(i + 1)) * 0x100000000);\n str = unescape(encodeURIComponent(str));\n var n = str.length, bits = [];\n for (i = 0; i < n; i++) bits.push(str.charCodeAt(i));\n bits.push(0x80);\n var len = bits.length / 4 + 2;\n len = Math.ceil(len / 16) * 16;\n var x = [];\n for (i = 0; i < len; i++) x[i] = 0;\n for (i = 0; i < bits.length; i++) x[i >> 2] |= bits[i] << ((i % 4) * 8);\n x[x.length - 2] = n * 8;\n var a = 0x67452301, b = 0xefcdab89, c = 0x98badcfe, d = 0x10325476;\n for (i = 0; i < x.length; i += 16) {\n var X = x.slice(i, i + 16);\n aa = a; bb = b; cc = c; dd = d;\n for (j = 0; j < 64; j++) {\n var g, f;\n if (j < 16) { g = j; f = (bb & cc) | (~bb & dd); }\n else if (j < 32) { g = (5 * j + 1) % 16; f = (dd & bb) | (~dd & cc); }\n else if (j < 48) { g = (3 * j + 5) % 16; f = bb ^ cc ^ dd; }\n else { g = (7 * j) % 16; f = cc ^ (bb | ~dd); }\n var temp = dd; dd = cc; cc = bb;\n bb = addUnsigned(bb, rotateLeft(addUnsigned(addUnsigned(addUnsigned(aa, f), T[j]), X[g]), S[j >> 4][j & 3]));\n aa = temp;\n }\n a = addUnsigned(a, aa); b = addUnsigned(b, bb);\n c = addUnsigned(c, cc); d = addUnsigned(d, dd);\n }\n return (toHex(a) + toHex(b) + toHex(c) + toHex(d)).toLowerCase();\n }\n\n // ====== 工具 ======\n function setProgress(pct, text) { progressBar.style.width = pct + \"%\"; progressText.textContent = text; }\n function showToast(msg, type) { toast.textContent = msg; toast.className = type; toast.style.display = \"block\"; setTimeout(() => { toast.style.display = \"none\"; }, 3000); }\n function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }\n\n document.addEventListener(\"keydown\", (e) => { if (e.key === \"Escape\") panel.classList.remove(\"show\"); });\n\n const engName = cfg.engine === \"google\" ? \"Google\" : cfg.engine === \"baidu\" ? (\"百度\" + (baiduMode === \"llm\" ? \"·大模型\" : \"·通用\")) : \"DeepSeek\";\n const modeName = cfg.mode === \"bilingual\" ? \"双语\" : \"仅译文\";\n console.log(`✅ 网页翻译已加载 | ${engName} | ${modeName} | 点击翻译/恢复,长按配置`);\n})();\n"}, "b386d4c9b9e305ab79035133115adae432eb1b80": {"t": 1785766103.7580943, "n": 25817, "h": "5c79fbc8eb0e36b6", "c": "// ==UserScript==\n// @name 网页翻译 · 多引擎\n// @namespace http://tampermonkey.net/\n// @version 1.4\n// @description 点击翻译整个页面/恢复原文。支持百度(极速)/Google(免费)/DeepSeek(高质量),双语/单语模式,翻译结果缓存。长按进入配置。\n// @author you\n// @match *://*/*\n// @icon data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌐</text></svg>\n// @grant GM_setValue\n// @grant GM_getValue\n// @grant GM_addStyle\n// @grant GM_xmlhttpRequest\n// @license MIT\n// ==/UserScript==\n\n(function () {\n \"use strict\";\n\n // ====== 配置 ======\n const cfg = {\n engine: GM_getValue(\"ds_engine\", \"google\"),\n mode: GM_getValue(\"ds_mode\", \"bilingual\"),\n baiduAppId: GM_getValue(\"ds_baidu_id\", \"\"),\n baiduKey: GM_getValue(\"ds_baidu_key\", \"\"),\n dsKey: GM_getValue(\"ds_api_key\", \"\"),\n dsModel: GM_getValue(\"ds_model\", \"deepseek-chat\"),\n };\n\n // ====== 状态 ======\n let translated = false;\n let translating = false;\n let stopRequested = false;\n let originalTexts = null;\n const transCache = new Map();\n\n // ====== 样式 ======\n GM_addStyle(`\n #ds-fab {\n position: fixed; bottom: 24px; right: 24px; z-index: 999999;\n width: 52px; height: 52px; border-radius: 50%; background: #4f6ef7;\n color: #fff; border: none; font-size: 24px; cursor: pointer;\n box-shadow: 0 2px 12px rgba(79,110,247,0.4);\n transition: transform 0.15s, background 0.3s;\n display: flex; align-items: center; justify-content: center;\n user-select: none; -webkit-user-select: none;\n }\n #ds-fab:active { transform: scale(0.92); }\n #ds-fab.translated { background: #a6e3a1; }\n\n #ds-panel {\n position: fixed; bottom: 88px; right: 24px; z-index: 999999;\n width: 320px; max-height: 70vh; overflow-y: auto;\n background: #1e1e2e; border-radius: 16px; padding: 20px;\n box-shadow: 0 8px 40px rgba(0,0,0,0.5); color: #cdd6f4;\n font: 14px -apple-system, sans-serif; display: none; border: 1px solid #313244;\n }\n #ds-panel.show { display: block; }\n #ds-panel h3 {\n margin: 0 0 16px 0; font-size: 16px; color: #cba6f7;\n display: flex; justify-content: space-between; align-items: center;\n }\n #ds-panel h3 button { background: none; border: none; color: #a6adc8; cursor: pointer; font-size: 18px; padding: 0 4px; }\n #ds-panel h3 button:hover { color: #f38ba8; }\n #ds-panel label { display: block; margin-bottom: 4px; color: #a6adc8; font-size: 12px; margin-top: 8px; }\n #ds-panel label:first-of-type { margin-top: 0; }\n #ds-panel input, #ds-panel select {\n width: 100%; padding: 8px 12px; margin-bottom: 4px;\n background: #313244; border: 1px solid #45475a; border-radius: 8px;\n color: #cdd6f4; font-size: 13px; box-sizing: border-box; outline: none;\n }\n #ds-panel input:focus, #ds-panel select:focus { border-color: #4f6ef7; }\n #ds-panel .hint { font-size: 11px; color: #6c7086; margin: 2px 0 8px 0; }\n\n #ds-panel .engine-tab {\n display: flex; gap: 6px; margin-bottom: 6px;\n }\n #ds-panel .engine-tab button {\n flex: 1; padding: 7px; border: 1px solid #45475a; border-radius: 8px;\n background: #313244; color: #a6adc8; cursor: pointer; font-size: 12px; transition: all 0.2s;\n }\n #ds-panel .engine-tab button.active { background: #4f6ef7; border-color: #4f6ef7; color: #fff; }\n\n #ds-panel .mode-tab {\n display: flex; gap: 6px; margin-bottom: 12px;\n }\n #ds-panel .mode-tab button {\n flex: 1; padding: 6px; border: 1px solid #45475a; border-radius: 8px;\n background: #313244; color: #a6adc8; cursor: pointer; font-size: 12px; transition: all 0.2s;\n }\n #ds-panel .mode-tab button.active { background: #585b70; border-color: #585b70; color: #fff; }\n\n #ds-panel .cfg-section { display: none; }\n #ds-panel .cfg-section.show { display: block; }\n #ds-panel .btn-row { display: flex; gap: 8px; margin-top: 12px; }\n #ds-panel .btn-row button {\n flex: 1; padding: 9px; border: none; border-radius: 8px;\n cursor: pointer; font-size: 13px; transition: opacity 0.2s;\n }\n #ds-panel .btn-row button:active { opacity: 0.8; }\n #ds-panel .btn-save { background: #4f6ef7; color: #fff; }\n #ds-panel .btn-close-cfg { background: #45475a; color: #cdd6f4; }\n #ds-panel .btn-clear-cache { background: #45475a; color: #f38ba8; }\n\n #ds-progress { margin-top: 10px; display: none; }\n #ds-progress.show { display: block; }\n #ds-progress .bar-wrap { width: 100%; height: 4px; background: #313244; border-radius: 2px; overflow: hidden; }\n #ds-progress .bar { height: 100%; width: 0%; background: #a6e3a1; border-radius: 2px; transition: width 0.3s; }\n #ds-progress .text { font-size: 11px; color: #a6adc8; margin-top: 4px; text-align: center; }\n\n #ds-toast {\n position: fixed; top: 20px; left: 50%; transform: translateX(-50%);\n z-index: 9999999; padding: 10px 24px; border-radius: 10px;\n font: 14px -apple-system, sans-serif; color: #fff;\n display: none; box-shadow: 0 4px 20px rgba(0,0,0,0.3);\n }\n #ds-toast.ok { background: #1e3a2e; }\n #ds-toast.err { background: #3a1e1e; }\n #ds-toast.info { background: #1e2a3a; }\n `);\n\n // ====== DOM ======\n document.body.insertAdjacentHTML(\"beforeend\", `\n <button id=\"ds-fab\">🌐</button>\n <div id=\"ds-panel\">\n <h3>\n ⚙️ 翻译设置\n <button id=\"ds-close-panel\">✕</button>\n </h3>\n\n <label>翻译引擎</label>\n <div class=\"engine-tab\">\n <button data-engine=\"google\" class=\"active\">🌍 Google</button>\n <button data-engine=\"baidu\">⚡ 百度翻译</button>\n <button data-engine=\"deepseek\">🧠 DeepSeek</button>\n </div>\n\n <label>显示模式</label>\n <div class=\"mode-tab\">\n <button data-mode=\"bilingual\" class=\"active\">📖 双语</button>\n <button data-mode=\"monolingual\">🔤 仅译文</button>\n </div>\n\n <div class=\"cfg-section show\" id=\"ds-cfg-google\">\n <div class=\"hint\">Google 翻译免费使用,无需配置</div>\n </div>\n <div class=\"cfg-section\" id=\"ds-cfg-baidu\">\n <label>APP ID</label>\n <input id=\"ds-baidu-id\" placeholder=\"从 fanyi-api.baidu.com 获取\" value=\"${cfg.baiduAppId}\">\n <label>密钥</label>\n <input type=\"password\" id=\"ds-baidu-key\" placeholder=\"从 fanyi-api.baidu.com 获取\" value=\"${cfg.baiduKey}\">\n <div class=\"hint\">免费版每月 100 万字符</div>\n </div>\n <div class=\"cfg-section\" id=\"ds-cfg-ds\">\n <label>API Key</label>\n <input type=\"password\" id=\"ds-ds-key\" placeholder=\"sk-xxxxxxxx\" value=\"${cfg.dsKey}\">\n <label>模型</label>\n <select id=\"ds-ds-model\">\n <option value=\"deepseek-chat\" ${cfg.dsModel === \"deepseek-chat\" ? \"selected\" : \"\"}>DeepSeek V3</option>\n <option value=\"deepseek-reasoner\" ${cfg.dsModel === \"deepseek-reasoner\" ? \"selected\" : \"\"}>DeepSeek R1</option>\n </select>\n <div class=\"hint\">质量高但速度较慢</div>\n </div>\n <div class=\"btn-row\">\n <button class=\"btn-save\" id=\"ds-save-btn\">💾 保存配置</button>\n <button class=\"btn-close-cfg\" id=\"ds-close-btn\">关闭</button>\n </div>\n <div class=\"hint\" style=\"text-align:center;margin-top:8px;cursor:pointer\" id=\"ds-clear-cache\">🗑️ 清除翻译缓存 (${transCache.size}条)</div>\n <div id=\"ds-progress\">\n <div class=\"bar-wrap\"><div class=\"bar\" id=\"ds-progress-bar\"></div></div>\n <div class=\"text\" id=\"ds-progress-text\">准备中...</div>\n </div>\n </div>\n <div id=\"ds-toast\"></div>\n `);\n\n // ====== DOM 引用 ======\n const fab = document.getElementById(\"ds-fab\");\n const panel = document.getElementById(\"ds-panel\");\n const progress = document.getElementById(\"ds-progress\");\n const progressBar = document.getElementById(\"ds-progress-bar\");\n const progressText = document.getElementById(\"ds-progress-text\");\n const toast = document.getElementById(\"ds-toast\");\n const cacheHint = document.getElementById(\"ds-clear-cache\");\n\n // ====== 引擎切换 ======\n document.querySelectorAll(\".engine-tab button\").forEach((btn) => {\n btn.addEventListener(\"click\", () => {\n document.querySelectorAll(\".engine-tab button\").forEach((b) => b.classList.remove(\"active\"));\n btn.classList.add(\"active\");\n cfg.engine = btn.dataset.engine;\n document.getElementById(\"ds-cfg-google\").classList.toggle(\"show\", cfg.engine === \"google\");\n document.getElementById(\"ds-cfg-baidu\").classList.toggle(\"show\", cfg.engine === \"baidu\");\n document.getElementById(\"ds-cfg-ds\").classList.toggle(\"show\", cfg.engine === \"deepseek\");\n });\n });\n\n // ====== 模式切换 ======\n document.querySelectorAll(\".mode-tab button\").forEach((btn) => {\n btn.addEventListener(\"click\", () => {\n document.querySelectorAll(\".mode-tab button\").forEach((b) => b.classList.remove(\"active\"));\n btn.classList.add(\"active\");\n cfg.mode = btn.dataset.mode;\n });\n });\n\n // ====== 保存 / 关闭 ======\n document.getElementById(\"ds-save-btn\").addEventListener(\"click\", () => {\n saveConfig();\n showToast(\"✅ 配置已保存\", \"ok\");\n });\n document.getElementById(\"ds-close-btn\").addEventListener(\"click\", () => panel.classList.remove(\"show\"));\n document.getElementById(\"ds-close-panel\").addEventListener(\"click\", () => panel.classList.remove(\"show\"));\n\n // 清除缓存\n cacheHint.addEventListener(\"click\", () => {\n transCache.clear();\n cacheHint.textContent = `🗑️ 清除翻译缓存 (0条)`;\n showToast(\"缓存已清除\", \"info\");\n });\n\n // ====== 保存配置 ======\n function saveConfig() {\n cfg.baiduAppId = document.getElementById(\"ds-baidu-id\").value.trim();\n cfg.baiduKey = document.getElementById(\"ds-baidu-key\").value.trim();\n cfg.dsKey = document.getElementById(\"ds-ds-key\").value.trim();\n cfg.dsModel = document.getElementById(\"ds-ds-model\").value;\n\n GM_setValue(\"ds_engine\", cfg.engine);\n GM_setValue(\"ds_mode\", cfg.mode);\n GM_setValue(\"ds_baidu_id\", cfg.baiduAppId);\n GM_setValue(\"ds_baidu_key\", cfg.baiduKey);\n GM_setValue(\"ds_api_key\", cfg.dsKey);\n GM_setValue(\"ds_model\", cfg.dsModel);\n }\n\n // ====== 可拖动按钮 ======\n let isDragging = false, dragStartX = 0, dragStartY = 0, dragMoved = false;\n let longPressTimer = null, longPressStart = 0, isLongPress = false;\n\n // 鼠标拖动\n fab.addEventListener(\"mousedown\", (e) => {\n if (e.button !== 0) return;\n isDragging = true; dragMoved = false;\n const rect = fab.getBoundingClientRect();\n dragStartX = e.clientX; dragStartY = e.clientY;\n fab._origLeft = rect.left; fab._origTop = rect.top;\n fab.style.cursor = \"grabbing\";\n fab.style.transition = \"none\";\n });\n\n document.addEventListener(\"mousemove\", (e) => {\n if (!isDragging) return;\n const dx = e.clientX - dragStartX, dy = e.clientY - dragStartY;\n if (Math.abs(dx) > 5 || Math.abs(dy) > 5) dragMoved = true;\n const vw = window.innerWidth, vh = window.innerHeight;\n const fw = 52, fh = 52;\n let nx = Math.max(0, Math.min(vw - fw, fab._origLeft + dx));\n let ny = Math.max(0, Math.min(vh - fh, fab._origTop + dy));\n fab.style.left = nx + \"px\"; fab.style.right = \"auto\";\n fab.style.top = ny + \"px\"; fab.style.bottom = \"auto\";\n });\n\n document.addEventListener(\"mouseup\", (e) => {\n if (!isDragging) return;\n isDragging = false;\n fab.style.transition = \"\";\n fab.style.cursor = \"\";\n if (dragMoved) { e.stopPropagation(); e.preventDefault(); }\n });\n\n fab.addEventListener(\"click\", (e) => {\n if (dragMoved) { e.stopPropagation(); e.preventDefault(); return; }\n if (\"ontouchstart\" in window) return;\n toggleTranslate();\n });\n\n // 触屏拖动+轻点+长按\n fab.addEventListener(\"touchstart\", (e) => {\n const t = e.touches[0];\n isDragging = true; dragMoved = false;\n const rect = fab.getBoundingClientRect();\n dragStartX = t.clientX; dragStartY = t.clientY;\n fab._origLeft = rect.left; fab._origTop = rect.top;\n fab.style.transition = \"none\";\n longPressStart = Date.now(); isLongPress = false;\n longPressTimer = setTimeout(() => {\n if (!dragMoved) { isLongPress = true; navigator.vibrate?.(20); panel.classList.add(\"show\"); }\n }, 500);\n }, { passive: true });\n\n fab.addEventListener(\"touchmove\", (e) => {\n if (!isDragging) return;\n const t = e.touches[0];\n const dx = t.clientX - dragStartX, dy = t.clientY - dragStartY;\n if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {\n dragMoved = true;\n clearTimeout(longPressTimer);\n }\n const vw = window.innerWidth, vh = window.innerHeight;\n const fw = 52, fh = 52;\n let nx = Math.max(0, Math.min(vw - fw, fab._origLeft + dx));\n let ny = Math.max(0, Math.min(vh - fh, fab._origTop + dy));\n fab.style.left = nx + \"px\"; fab.style.right = \"auto\";\n fab.style.top = ny + \"px\"; fab.style.bottom = \"auto\";\n e.preventDefault();\n }, { passive: false });\n\n fab.addEventListener(\"touchend\", (e) => {\n isDragging = false;\n clearTimeout(longPressTimer);\n fab.style.transition = \"\";\n if (!dragMoved && !isLongPress && Date.now() - longPressStart < 500) {\n e.preventDefault();\n toggleTranslate();\n }\n }, { passive: false });\n\n fab.addEventListener(\"contextmenu\", (e) => { e.preventDefault(); panel.classList.toggle(\"show\"); });\n\n // ====== 切换 ======\n async function toggleTranslate() {\n if (translating) return;\n saveConfig();\n\n if (translating) {\n stopRequested = true;\n showToast(\"⏹️ 正在停止...\", \"info\");\n return;\n }\n if (translated) { restoreOriginal(); return; }\n\n if (cfg.engine === \"baidu\" && (!cfg.baiduAppId || !cfg.baiduKey)) {\n showToast(\"请先配置百度翻译(长按图标设置)\", \"err\"); return;\n }\n if (cfg.engine === \"deepseek\" && !cfg.dsKey) {\n showToast(\"请先配置 DeepSeek Key(长按图标设置)\", \"err\"); return;\n }\n if (cfg.engine === \"google\") {\n // Google 免费接口,无需配置\n }\n\n await translatePage();\n }\n\n function restoreOriginal() {\n if (!originalTexts) return;\n originalTexts.forEach(({ node, text }) => { node.textContent = text; });\n translated = false; fab.classList.remove(\"translated\"); fab.textContent = \"🌐\";\n showToast(\"已恢复原文\", \"info\");\n }\n\n // ====== 翻译页面 ======\n async function translatePage() {\n translating = true;\n progress.classList.add(\"show\");\n setProgress(0, \"提取文本...\");\n\n try {\n const entries = [];\n const walk = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {\n acceptNode: (n) => {\n const p = n.parentElement;\n if (!p || p.closest(\"script,style,code,pre,#ds-panel,#ds-fab,#ds-toast,svg\"))\n return NodeFilter.FILTER_REJECT;\n const t = n.textContent.trim();\n if (t.length < 10) return NodeFilter.FILTER_REJECT;\n if (p.offsetWidth === 0 || p.offsetHeight === 0) return NodeFilter.FILTER_REJECT;\n return NodeFilter.FILTER_ACCEPT;\n },\n });\n while (walk.nextNode()) entries.push({ node: walk.currentNode, text: walk.currentNode.textContent });\n\n if (entries.length === 0) { showToast(\"没有可翻译的文本\", \"info\"); return; }\n originalTexts = entries;\n\n const nodes = entries.map((e) => e.node);\n const needApi = [];\n const cached = [];\n\n nodes.forEach((node, i) => {\n const key = node.textContent.trim();\n if (transCache.has(key)) {\n cached.push(i);\n } else {\n needApi.push(i);\n }\n });\n\n cached.forEach((i) => {\n const key = nodes[i].textContent.trim();\n const t = transCache.get(key);\n nodes[i].textContent = cfg.mode === \"bilingual\" ? key + \"\\n\" + t : t;\n });\n\n if (needApi.length > 0) {\n if (cfg.engine === \"google\") {\n await translateGoogle(nodes, needApi);\n } else if (cfg.engine === \"baidu\") {\n await translateBaidu(nodes, needApi);\n } else {\n await translateDeepSeek(nodes, needApi);\n }\n }\n\n cacheHint.textContent = `🗑️ 清除翻译缓存 (${transCache.size}条)`;\n\n translated = true;\n fab.classList.add(\"translated\"); fab.textContent = \"✅\";\n setProgress(100, \"✅ 翻译完成\");\n const fromCache = cached.length > 0 ? ` (缓存${cached.length}段)` : \"\";\n showToast(`翻译完成${fromCache},再点恢复原文`, \"ok\");\n } catch (err) {\n showToast(\"出错: \" + err.message, \"err\");\n }\n setTimeout(() => { progress.classList.remove(\"show\"); translating = false; }, 1500);\n }\n\n // ====== Google 翻译(免费接口) ======\n async function translateGoogle(nodes, indices) {\n const batchSize = 5;\n for (let ki = 0; ki < indices.length; ki += batchSize) {\n if (stopRequested) { showToast(\"⏹️ 已停止\", \"info\"); break; }\n const batchIdx = indices.slice(ki, ki + batchSize);\n const pct = Math.min(100, Math.round(((ki + batchIdx.length) / indices.length) * 100));\n setProgress(pct, `Google 翻译中 ${Math.min(ki + batchIdx.length, indices.length)}/${indices.length}...`);\n\n for (const i of batchIdx) {\n if (stopRequested) break;\n const text = nodes[i].textContent.trim();\n try {\n const t = await callGoogle(text);\n if (t) {\n transCache.set(text, t);\n nodes[i].textContent = cfg.mode === \"bilingual\" ? text + \"\\n\" + t : t;\n }\n } catch (e) {\n console.warn(\"Google翻译失败:\", e.message?.slice(0, 30));\n }\n }\n }\n }\n\n function callGoogle(text) {\n return new Promise((resolve, reject) => {\n const url = \"https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=zh-CN&dt=t&q=\" + encodeURIComponent(text);\n GM_xmlhttpRequest({\n method: \"GET\", url,\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data && data[0]) {\n const result = data[0].map((r) => r[0]).join(\"\");\n resolve(result || null);\n } else {\n reject(new Error(\"无结果\"));\n }\n } catch { reject(new Error(\"解析失败\")); }\n },\n onerror: () => reject(new Error(\"网络错误\")),\n timeout: 15000,\n });\n });\n }\n\n // ====== 百度翻译 ======\n async function translateBaidu(nodes, indices) {\n const sep = \"\\n|||ds-sep|||\\n\";\n const maxBytes = 5000;\n const batches = [];\n let cur = [], curBytes = 0;\n for (let ki = 0; ki < indices.length; ki++) {\n const t = nodes[indices[ki]].textContent.trim();\n const bytes = new Blob([t]).size;\n if (cur.length > 0 && curBytes + bytes + 20 > maxBytes) {\n batches.push(cur);\n cur = []; curBytes = 0;\n }\n cur.push({ idx: indices[ki], text: t, bytes });\n curBytes += bytes + 20;\n }\n if (cur.length) batches.push(cur);\n\n for (let b = 0; b < batches.length; b += 3) {\n if (stopRequested) { showToast(\"⏹️ 已停止\", \"info\"); break; }\n const chunk = batches.slice(b, b + 3);\n const pct = Math.min(100, Math.round(((b + chunk.length) / batches.length) * 100));\n setProgress(pct, `百度翻译中 ${Math.min((b + chunk.length) * 30, indices.length)}/${indices.length}...`);\n\n await Promise.all(chunk.map(async (batch) => {\n try {\n const joined = batch.map(x => x.text).join(sep);\n const result = await callBaidu(joined);\n if (result) {\n const parts = result.split(\"|||ds-sep|||\").map((s) => s.trim());\n batch.forEach((item, idx) => {\n const t = parts[idx];\n if (t && t !== item.text) {\n transCache.set(item.text, t);\n const node = nodes[item.idx];\n node.textContent = cfg.mode === \"bilingual\" ? item.text + \"\\n\" + t : t;\n }\n });\n }\n } catch (e) {\n console.warn(\"百度批量失败:\", e.message.slice(0, 30));\n for (const item of batch) {\n if (stopRequested) break;\n try {\n const r = await callBaidu(item.text);\n if (r) {\n transCache.set(item.text, r);\n nodes[item.idx].textContent = cfg.mode === \"bilingual\" ? item.text + \"\\n\" + r : r;\n }\n } catch {}\n }\n }\n }));\n }\n }\n\n function callBaidu(text) {\n return new Promise((resolve, reject) => {\n const appid = cfg.baiduAppId;\n const key = cfg.baiduKey;\n const salt = Date.now() + Math.random();\n const sign = md5(appid + text + salt + key);\n const url = `https://fanyi-api.baidu.com/api/trans/vip/translate?q=${encodeURIComponent(text)}&from=auto&to=zh&appid=${appid}&salt=${salt}&sign=${sign}`;\n GM_xmlhttpRequest({\n method: \"GET\", url,\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data.trans_result) resolve(data.trans_result.map((r) => r.dst).join(\"\\n\"));\n else if (data.error_code) reject(new Error(`[${data.error_code}] ${data.error_msg}`));\n else reject(new Error(\"返回异常\"));\n } catch { reject(new Error(\"解析失败\")); }\n },\n onerror: () => reject(new Error(\"网络错误\")),\n timeout: 15000,\n });\n });\n }\n\n // ====== DeepSeek API ======\n async function translateDeepSeek(nodes, indices) {\n const batchSize = 20;\n const sep = \"\\n---\\n\";\n for (let ki = 0; ki < indices.length; ki += batchSize) {\n if (stopRequested) { showToast(\"⏹️ 已停止\", \"info\"); break; }\n const batchIdx = indices.slice(ki, ki + batchSize);\n const texts = batchIdx.map((i) => nodes[i].textContent.trim());\n const pct = Math.min(100, Math.round(((ki + batchIdx.length) / indices.length) * 100));\n setProgress(pct, `DeepSeek 翻译中 ${Math.min(ki + batchIdx.length, indices.length)}/${indices.length}...`);\n\n const result = await callDeepSeek(texts);\n if (result) {\n batchIdx.forEach((i, idx) => {\n const orig = texts[idx];\n if (result[idx]) {\n transCache.set(orig, result[idx]);\n nodes[i].textContent = cfg.mode === \"bilingual\" ? orig + \"\\n\" + result[idx] : result[idx];\n }\n });\n }\n }\n }\n\n function callDeepSeek(texts) {\n return new Promise((resolve) => {\n const joined = texts.map((t, i) => `[${i + 1}] ${t}`).join(\"\\n---\\n\");\n const prompt = `逐段翻译为简体中文,保持编号格式,只返回翻译结果:\\n\\n${joined}`;\n GM_xmlhttpRequest({\n method: \"POST\",\n url: \"https://api.deepseek.com/chat/completions\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${cfg.dsKey}` },\n data: JSON.stringify({\n model: cfg.dsModel,\n messages: [{ role: \"system\", content: \"逐段翻译,保持编号。\" }, { role: \"user\", content: prompt }],\n temperature: 0.3, max_tokens: 8192,\n }),\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data.choices && data.choices[0]) {\n const content = data.choices[0].message.content.trim();\n const results = [];\n for (let i = 0; i < texts.length; i++) {\n const re = new RegExp(`\\\\[${i + 1}\\\\]\\\\s*([\\\\s\\\\S]*?)(?=\\\\[${i + 2}\\\\]|$)`);\n const m = content.match(re);\n results.push(m ? m[1].trim() : \"\");\n }\n resolve(results);\n } else resolve(null);\n } catch { resolve(null); }\n },\n onerror: () => resolve(null),\n timeout: 60000,\n });\n });\n }\n\n // ====== MD5 ======\n function md5(str) {\n function rotateLeft(x, n) { return (x << n) | (x >>> (32 - n)); }\n function toHex(x) {\n var s = \"\";\n for (var i = 0; i < 4; i++) {\n s += (\"0\" + ((x >> (i * 8)) & 0xff).toString(16)).slice(-2);\n }\n return s;\n }\n function addUnsigned(x, y) {\n var lsw = (x & 0xffff) + (y & 0xffff);\n return ((x >>> 16) + (y >>> 16) + (lsw >>> 16)) << 16 | (lsw & 0xffff);\n }\n var i, j, aa, bb, cc, dd;\n var S = [[7, 12, 17, 22], [5, 9, 14, 20], [4, 11, 16, 23], [6, 10, 15, 21]];\n var T = [];\n for (i = 0; i < 64; i++) T[i] = Math.floor(Math.abs(Math.sin(i + 1)) * 0x100000000);\n str = unescape(encodeURIComponent(str));\n var n = str.length, bits = [];\n for (i = 0; i < n; i++) bits.push(str.charCodeAt(i));\n bits.push(0x80);\n var len = bits.length / 4 + 2;\n len = Math.ceil(len / 16) * 16;\n var x = [];\n for (i = 0; i < len; i++) x[i] = 0;\n for (i = 0; i < bits.length; i++) x[i >> 2] |= bits[i] << ((i % 4) * 8);\n x[x.length - 2] = n * 8;\n var a = 0x67452301, b = 0xefcdab89, c = 0x98badcfe, d = 0x10325476;\n for (i = 0; i < x.length; i += 16) {\n var X = x.slice(i, i + 16);\n aa = a; bb = b; cc = c; dd = d;\n for (j = 0; j < 64; j++) {\n var g, f;\n if (j < 16) { g = j; f = (bb & cc) | (~bb & dd); }\n else if (j < 32) { g = (5 * j + 1) % 16; f = (dd & bb) | (~dd & cc); }\n else if (j < 48) { g = (3 * j + 5) % 16; f = bb ^ cc ^ dd; }\n else { g = (7 * j) % 16; f = cc ^ (bb | ~dd); }\n var temp = dd; dd = cc; cc = bb;\n bb = addUnsigned(bb, rotateLeft(addUnsigned(addUnsigned(addUnsigned(aa, f), T[j]), X[g]), S[j >> 4][j & 3]));\n aa = temp;\n }\n a = addUnsigned(a, aa); b = addUnsigned(b, bb);\n c = addUnsigned(c, cc); d = addUnsigned(d, dd);\n }\n return (toHex(a) + toHex(b) + toHex(c) + toHex(d)).toLowerCase();\n }\n\n // ====== 工具 ======\n function setProgress(pct, text) { progressBar.style.width = pct + \"%\"; progressText.textContent = text; }\n function showToast(msg, type) { toast.textContent = msg; toast.className = type; toast.style.display = \"block\"; setTimeout(() => { toast.style.display = \"none\"; }, 3000); }\n function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }\n\n document.addEventListener(\"keydown\", (e) => { if (e.key === \"Escape\") panel.classList.remove(\"show\"); });\n\n const engName = cfg.engine === \"google\" ? \"Google\" : cfg.engine === \"baidu\" ? \"百度\" : \"DeepSeek\";\n const modeName = cfg.mode === \"bilingual\" ? \"双语\" : \"仅译文\";\n console.log(`✅ 网页翻译已加载 | ${engName} | ${modeName} | 点击翻译/恢复,长按配置`);\n})();\n"}, "505a5646c89c3b4410edd0f048d9128d261715b2": {"t": 1785766105.3193126, "n": 23886, "h": "65d2dc395030da68", "c": "// ==UserScript==\n// @name 网页翻译 · 多引擎\n// @namespace http://tampermonkey.net/\n// @version 1.3\n// @description 点击翻译整个页面/恢复原文。支持百度(极速)和DeepSeek(高质量),双语/单语模式,翻译结果缓存。长按进入配置。\n// @author you\n// @match *://*/*\n// @icon data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌐</text></svg>\n// @grant GM_setValue\n// @grant GM_getValue\n// @grant GM_addStyle\n// @grant GM_xmlhttpRequest\n// @license MIT\n// ==/UserScript==\n\n(function () {\n \"use strict\";\n\n // ====== 配置 ======\n const cfg = {\n engine: GM_getValue(\"ds_engine\", \"baidu\"),\n mode: GM_getValue(\"ds_mode\", \"bilingual\"),\n baiduAppId: GM_getValue(\"ds_baidu_id\", \"\"),\n baiduKey: GM_getValue(\"ds_baidu_key\", \"\"),\n dsKey: GM_getValue(\"ds_api_key\", \"\"),\n dsModel: GM_getValue(\"ds_model\", \"deepseek-chat\"),\n };\n\n // ====== 状态 ======\n let translated = false;\n let translating = false;\n let stopRequested = false;\n let originalTexts = null;\n const transCache = new Map(); // 原文 → 译文\n\n // ====== 样式 ======\n GM_addStyle(`\n #ds-fab {\n position: fixed; bottom: 24px; right: 24px; z-index: 999999;\n width: 52px; height: 52px; border-radius: 50%; background: #4f6ef7;\n color: #fff; border: none; font-size: 24px; cursor: pointer;\n box-shadow: 0 2px 12px rgba(79,110,247,0.4);\n transition: transform 0.15s, background 0.3s;\n display: flex; align-items: center; justify-content: center;\n cursor: pointer; user-select: none; -webkit-user-select: none;\n }\n #ds-fab:active { transform: scale(0.92); }\n #ds-fab.translated { background: #a6e3a1; }\n\n #ds-panel {\n position: fixed; bottom: 88px; right: 24px; z-index: 999999;\n width: 320px; max-height: 70vh; overflow-y: auto;\n background: #1e1e2e; border-radius: 16px; padding: 20px;\n box-shadow: 0 8px 40px rgba(0,0,0,0.5); color: #cdd6f4;\n font: 14px -apple-system, sans-serif; display: none; border: 1px solid #313244;\n }\n #ds-panel.show { display: block; }\n #ds-panel h3 {\n margin: 0 0 16px 0; font-size: 16px; color: #cba6f7;\n display: flex; justify-content: space-between; align-items: center;\n }\n #ds-panel h3 button { background: none; border: none; color: #a6adc8; cursor: pointer; font-size: 18px; padding: 0 4px; }\n #ds-panel h3 button:hover { color: #f38ba8; }\n #ds-panel label { display: block; margin-bottom: 4px; color: #a6adc8; font-size: 12px; margin-top: 8px; }\n #ds-panel label:first-of-type { margin-top: 0; }\n #ds-panel input, #ds-panel select {\n width: 100%; padding: 8px 12px; margin-bottom: 4px;\n background: #313244; border: 1px solid #45475a; border-radius: 8px;\n color: #cdd6f4; font-size: 13px; box-sizing: border-box; outline: none;\n }\n #ds-panel input:focus, #ds-panel select:focus { border-color: #4f6ef7; }\n #ds-panel .hint { font-size: 11px; color: #6c7086; margin: 2px 0 8px 0; }\n\n #ds-panel .engine-tab {\n display: flex; gap: 6px; margin-bottom: 6px;\n }\n #ds-panel .engine-tab button {\n flex: 1; padding: 7px; border: 1px solid #45475a; border-radius: 8px;\n background: #313244; color: #a6adc8; cursor: pointer; font-size: 12px; transition: all 0.2s;\n }\n #ds-panel .engine-tab button.active { background: #4f6ef7; border-color: #4f6ef7; color: #fff; }\n\n #ds-panel .mode-tab {\n display: flex; gap: 6px; margin-bottom: 12px;\n }\n #ds-panel .mode-tab button {\n flex: 1; padding: 6px; border: 1px solid #45475a; border-radius: 8px;\n background: #313244; color: #a6adc8; cursor: pointer; font-size: 12px; transition: all 0.2s;\n }\n #ds-panel .mode-tab button.active { background: #585b70; border-color: #585b70; color: #fff; }\n\n #ds-panel .cfg-section { display: none; }\n #ds-panel .cfg-section.show { display: block; }\n #ds-panel .btn-row { display: flex; gap: 8px; margin-top: 12px; }\n #ds-panel .btn-row button {\n flex: 1; padding: 9px; border: none; border-radius: 8px;\n cursor: pointer; font-size: 13px; transition: opacity 0.2s;\n }\n #ds-panel .btn-row button:active { opacity: 0.8; }\n #ds-panel .btn-save { background: #4f6ef7; color: #fff; }\n #ds-panel .btn-close-cfg { background: #45475a; color: #cdd6f4; }\n #ds-panel .btn-clear-cache { background: #45475a; color: #f38ba8; }\n\n #ds-progress { margin-top: 10px; display: none; }\n #ds-progress.show { display: block; }\n #ds-progress .bar-wrap { width: 100%; height: 4px; background: #313244; border-radius: 2px; overflow: hidden; }\n #ds-progress .bar { height: 100%; width: 0%; background: #a6e3a1; border-radius: 2px; transition: width 0.3s; }\n #ds-progress .text { font-size: 11px; color: #a6adc8; margin-top: 4px; text-align: center; }\n\n #ds-toast {\n position: fixed; top: 20px; left: 50%; transform: translateX(-50%);\n z-index: 9999999; padding: 10px 24px; border-radius: 10px;\n font: 14px -apple-system, sans-serif; color: #fff;\n display: none; box-shadow: 0 4px 20px rgba(0,0,0,0.3);\n }\n #ds-toast.ok { background: #1e3a2e; }\n #ds-toast.err { background: #3a1e1e; }\n #ds-toast.info { background: #1e2a3a; }\n `);\n\n // ====== DOM ======\n document.body.insertAdjacentHTML(\"beforeend\", `\n <button id=\"ds-fab\">🌐</button>\n <div id=\"ds-panel\">\n <h3>\n ⚙️ 翻译设置\n <button id=\"ds-close-panel\">✕</button>\n </h3>\n\n <label>翻译引擎</label>\n <div class=\"engine-tab\">\n <button data-engine=\"baidu\" class=\"active\">⚡ 百度翻译</button>\n <button data-engine=\"deepseek\">🧠 DeepSeek</button>\n </div>\n\n <label>显示模式</label>\n <div class=\"mode-tab\">\n <button data-mode=\"bilingual\" class=\"active\">📖 双语</button>\n <button data-mode=\"monolingual\">🔤 仅译文</button>\n </div>\n\n <div class=\"cfg-section show\" id=\"ds-cfg-baidu\">\n <label>APP ID</label>\n <input id=\"ds-baidu-id\" placeholder=\"从 fanyi-api.baidu.com 获取\" value=\"${cfg.baiduAppId}\">\n <label>密钥</label>\n <input type=\"password\" id=\"ds-baidu-key\" placeholder=\"从 fanyi-api.baidu.com 获取\" value=\"${cfg.baiduKey}\">\n <div class=\"hint\">免费版每月 100 万字符</div>\n </div>\n <div class=\"cfg-section\" id=\"ds-cfg-ds\">\n <label>API Key</label>\n <input type=\"password\" id=\"ds-ds-key\" placeholder=\"sk-xxxxxxxx\" value=\"${cfg.dsKey}\">\n <label>模型</label>\n <select id=\"ds-ds-model\">\n <option value=\"deepseek-chat\" ${cfg.dsModel === \"deepseek-chat\" ? \"selected\" : \"\"}>DeepSeek V3</option>\n <option value=\"deepseek-reasoner\" ${cfg.dsModel === \"deepseek-reasoner\" ? \"selected\" : \"\"}>DeepSeek R1</option>\n </select>\n <div class=\"hint\">质量高但速度较慢</div>\n </div>\n <div class=\"btn-row\">\n <button class=\"btn-save\" id=\"ds-save-btn\">💾 保存配置</button>\n <button class=\"btn-close-cfg\" id=\"ds-close-btn\">关闭</button>\n </div>\n <div class=\"hint\" style=\"text-align:center;margin-top:8px;cursor:pointer\" id=\"ds-clear-cache\">🗑️ 清除翻译缓存 (${transCache.size}条)</div>\n <div id=\"ds-progress\">\n <div class=\"bar-wrap\"><div class=\"bar\" id=\"ds-progress-bar\"></div></div>\n <div class=\"text\" id=\"ds-progress-text\">准备中...</div>\n </div>\n </div>\n <div id=\"ds-toast\"></div>\n `);\n\n // ====== DOM 引用 ======\n const fab = document.getElementById(\"ds-fab\");\n const panel = document.getElementById(\"ds-panel\");\n const progress = document.getElementById(\"ds-progress\");\n const progressBar = document.getElementById(\"ds-progress-bar\");\n const progressText = document.getElementById(\"ds-progress-text\");\n const toast = document.getElementById(\"ds-toast\");\n const cacheHint = document.getElementById(\"ds-clear-cache\");\n\n // ====== 引擎切换 ======\n document.querySelectorAll(\".engine-tab button\").forEach((btn) => {\n btn.addEventListener(\"click\", () => {\n document.querySelectorAll(\".engine-tab button\").forEach((b) => b.classList.remove(\"active\"));\n btn.classList.add(\"active\");\n cfg.engine = btn.dataset.engine;\n document.getElementById(\"ds-cfg-baidu\").classList.toggle(\"show\", cfg.engine === \"baidu\");\n document.getElementById(\"ds-cfg-ds\").classList.toggle(\"show\", cfg.engine === \"deepseek\");\n });\n });\n\n // ====== 模式切换 ======\n document.querySelectorAll(\".mode-tab button\").forEach((btn) => {\n btn.addEventListener(\"click\", () => {\n document.querySelectorAll(\".mode-tab button\").forEach((b) => b.classList.remove(\"active\"));\n btn.classList.add(\"active\");\n cfg.mode = btn.dataset.mode;\n });\n });\n\n // ====== 保存 / 关闭 ======\n document.getElementById(\"ds-save-btn\").addEventListener(\"click\", () => {\n saveConfig();\n showToast(\"✅ 配置已保存\", \"ok\");\n });\n document.getElementById(\"ds-close-btn\").addEventListener(\"click\", () => panel.classList.remove(\"show\"));\n document.getElementById(\"ds-close-panel\").addEventListener(\"click\", () => panel.classList.remove(\"show\"));\n\n // 清除缓存\n cacheHint.addEventListener(\"click\", () => {\n transCache.clear();\n cacheHint.textContent = `🗑️ 清除翻译缓存 (0条)`;\n showToast(\"缓存已清除\", \"info\");\n });\n\n // ====== 保存配置 ======\n function saveConfig() {\n cfg.baiduAppId = document.getElementById(\"ds-baidu-id\").value.trim();\n cfg.baiduKey = document.getElementById(\"ds-baidu-key\").value.trim();\n cfg.dsKey = document.getElementById(\"ds-ds-key\").value.trim();\n cfg.dsModel = document.getElementById(\"ds-ds-model\").value;\n\n GM_setValue(\"ds_engine\", cfg.engine);\n GM_setValue(\"ds_mode\", cfg.mode);\n GM_setValue(\"ds_baidu_id\", cfg.baiduAppId);\n GM_setValue(\"ds_baidu_key\", cfg.baiduKey);\n GM_setValue(\"ds_api_key\", cfg.dsKey);\n GM_setValue(\"ds_model\", cfg.dsModel);\n }\n\n // ====== 可拖动按钮 ======\n let isDragging = false, dragStartX = 0, dragStartY = 0, dragMoved = false;\n let longPressTimer = null, longPressStart = 0, isLongPress = false;\n\n // 鼠标拖动\n fab.addEventListener(\"mousedown\", (e) => {\n if (e.button !== 0) return;\n isDragging = true; dragMoved = false;\n const rect = fab.getBoundingClientRect();\n dragStartX = e.clientX; dragStartY = e.clientY;\n fab._origLeft = rect.left; fab._origTop = rect.top;\n fab.style.cursor = \"grabbing\";\n fab.style.transition = \"none\";\n });\n\n document.addEventListener(\"mousemove\", (e) => {\n if (!isDragging) return;\n const dx = e.clientX - dragStartX, dy = e.clientY - dragStartY;\n if (Math.abs(dx) > 5 || Math.abs(dy) > 5) dragMoved = true;\n const vw = window.innerWidth, vh = window.innerHeight;\n const fw = 52, fh = 52;\n let nx = Math.max(0, Math.min(vw - fw, fab._origLeft + dx));\n let ny = Math.max(0, Math.min(vh - fh, fab._origTop + dy));\n fab.style.left = nx + \"px\"; fab.style.right = \"auto\";\n fab.style.top = ny + \"px\"; fab.style.bottom = \"auto\";\n });\n\n document.addEventListener(\"mouseup\", (e) => {\n if (!isDragging) return;\n isDragging = false;\n fab.style.transition = \"\";\n fab.style.cursor = \"\";\n if (dragMoved) { e.stopPropagation(); e.preventDefault(); }\n });\n\n fab.addEventListener(\"click\", (e) => {\n if (dragMoved) { e.stopPropagation(); e.preventDefault(); return; }\n if (\"ontouchstart\" in window) return;\n toggleTranslate();\n });\n\n // 触屏拖动+轻点+长按\n fab.addEventListener(\"touchstart\", (e) => {\n const t = e.touches[0];\n isDragging = true; dragMoved = false;\n const rect = fab.getBoundingClientRect();\n dragStartX = t.clientX; dragStartY = t.clientY;\n fab._origLeft = rect.left; fab._origTop = rect.top;\n fab.style.transition = \"none\";\n longPressStart = Date.now(); isLongPress = false;\n longPressTimer = setTimeout(() => {\n if (!dragMoved) { isLongPress = true; navigator.vibrate?.(20); panel.classList.add(\"show\"); }\n }, 500);\n }, { passive: true });\n\n fab.addEventListener(\"touchmove\", (e) => {\n if (!isDragging) return;\n const t = e.touches[0];\n const dx = t.clientX - dragStartX, dy = t.clientY - dragStartY;\n if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {\n dragMoved = true;\n clearTimeout(longPressTimer);\n }\n const vw = window.innerWidth, vh = window.innerHeight;\n const fw = 52, fh = 52;\n let nx = Math.max(0, Math.min(vw - fw, fab._origLeft + dx));\n let ny = Math.max(0, Math.min(vh - fh, fab._origTop + dy));\n fab.style.left = nx + \"px\"; fab.style.right = \"auto\";\n fab.style.top = ny + \"px\"; fab.style.bottom = \"auto\";\n e.preventDefault();\n }, { passive: false });\n\n fab.addEventListener(\"touchend\", (e) => {\n isDragging = false;\n clearTimeout(longPressTimer);\n fab.style.transition = \"\";\n if (!dragMoved && !isLongPress && Date.now() - longPressStart < 500) {\n e.preventDefault();\n toggleTranslate();\n }\n }, { passive: false });\n\n fab.addEventListener(\"contextmenu\", (e) => { e.preventDefault(); panel.classList.toggle(\"show\"); });\n\n // ====== 切换 ======\n async function toggleTranslate() {\n if (translating) return;\n saveConfig();\n\n if (translating) {\n stopRequested = true;\n showToast(\"⏹️ 正在停止...\", \"info\");\n return;\n }\n if (translated) { restoreOriginal(); return; }\n\n if (cfg.engine === \"baidu\" && (!cfg.baiduAppId || !cfg.baiduKey)) {\n showToast(\"请先配置百度翻译(长按图标设置)\", \"err\"); return;\n }\n if (cfg.engine === \"deepseek\" && !cfg.dsKey) {\n showToast(\"请先配置 DeepSeek Key(长按图标设置)\", \"err\"); return;\n }\n\n await translatePage();\n }\n\n function restoreOriginal() {\n if (!originalTexts) return;\n originalTexts.forEach(({ node, text }) => { node.textContent = text; });\n translated = false; fab.classList.remove(\"translated\"); fab.textContent = \"🌐\";\n showToast(\"已恢复原文\", \"info\");\n }\n\n // ====== 翻译页面 ======\n async function translatePage() {\n translating = true;\n progress.classList.add(\"show\");\n setProgress(0, \"提取文本...\");\n\n try {\n const entries = [];\n const walk = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {\n acceptNode: (n) => {\n const p = n.parentElement;\n if (!p || p.closest(\"script,style,code,pre,#ds-panel,#ds-fab,#ds-toast,svg\"))\n return NodeFilter.FILTER_REJECT;\n const t = n.textContent.trim();\n if (t.length < 10) return NodeFilter.FILTER_REJECT;\n if (p.offsetWidth === 0 || p.offsetHeight === 0) return NodeFilter.FILTER_REJECT;\n return NodeFilter.FILTER_ACCEPT;\n },\n });\n while (walk.nextNode()) entries.push({ node: walk.currentNode, text: walk.currentNode.textContent });\n\n if (entries.length === 0) { showToast(\"没有可翻译的文本\", \"info\"); return; }\n originalTexts = entries;\n\n const nodes = entries.map((e) => e.node);\n const needApi = []; // 需要调用 API 的节点索引\n const cached = []; // 有缓存的节点索引\n\n nodes.forEach((node, i) => {\n const key = node.textContent.trim();\n if (transCache.has(key)) {\n cached.push(i);\n } else {\n needApi.push(i);\n }\n });\n\n // 有缓存的直接填充\n cached.forEach((i) => {\n const key = nodes[i].textContent.trim();\n const t = transCache.get(key);\n nodes[i].textContent = cfg.mode === \"bilingual\" ? key + \"\\n\" + t : t;\n });\n\n // 需要调 API 的\n if (needApi.length > 0) {\n if (cfg.engine === \"baidu\") {\n await translateBaidu(nodes, needApi);\n } else {\n await translateDeepSeek(nodes, needApi);\n }\n }\n\n // 更新缓存计数\n cacheHint.textContent = `🗑️ 清除翻译缓存 (${transCache.size}条)`;\n\n translated = true;\n fab.classList.add(\"translated\"); fab.textContent = \"✅\";\n setProgress(100, \"✅ 翻译完成\");\n const fromCache = cached.length > 0 ? ` (缓存${cached.length}段)` : \"\";\n showToast(`翻译完成${fromCache},再点恢复原文`, \"ok\");\n } catch (err) {\n showToast(\"出错: \" + err.message, \"err\");\n }\n setTimeout(() => { progress.classList.remove(\"show\"); translating = false; }, 1500);\n }\n\n // ====== 百度翻译 ======\n async function translateBaidu(nodes, indices) {\n const sep = \"\\n|||ds-sep|||\\n\";\n const maxBytes = 5000;\n // 按字节长度分批\n const batches = [];\n let cur = [], curBytes = 0;\n for (let ki = 0; ki < indices.length; ki++) {\n const t = nodes[indices[ki]].textContent.trim();\n const bytes = new Blob([t]).size;\n if (cur.length > 0 && curBytes + bytes + 20 > maxBytes) {\n batches.push(cur);\n cur = []; curBytes = 0;\n }\n cur.push({ idx: indices[ki], text: t, bytes });\n curBytes += bytes + 20;\n }\n if (cur.length) batches.push(cur);\n\n // 并发执行所有批次(最多同时3批)\n for (let b = 0; b < batches.length; b += 3) {\n if (stopRequested) { showToast(\"⏹️ 已停止\", \"info\"); break; }\n const chunk = batches.slice(b, b + 3);\n const pct = Math.min(100, Math.round(((b + chunk.length) / batches.length) * 100));\n setProgress(pct, `百度翻译中 ${Math.min((b + chunk.length) * 30, indices.length)}/${indices.length}...`);\n\n await Promise.all(chunk.map(async (batch) => {\n try {\n const joined = batch.map(x => x.text).join(sep);\n const result = await callBaidu(joined);\n if (result) {\n const parts = result.split(\"|||ds-sep|||\").map((s) => s.trim());\n batch.forEach((item, idx) => {\n const t = parts[idx];\n if (t && t !== item.text) {\n transCache.set(item.text, t);\n const node = nodes[item.idx];\n node.textContent = cfg.mode === \"bilingual\" ? item.text + \"\\n\" + t : t;\n }\n });\n }\n } catch (e) {\n console.warn(\"百度批量失败:\", e.message.slice(0, 30));\n // 单条重试\n for (const item of batch) {\n if (stopRequested) break;\n try {\n const r = await callBaidu(item.text);\n if (r) {\n transCache.set(item.text, r);\n nodes[item.idx].textContent = cfg.mode === \"bilingual\" ? item.text + \"\\n\" + r : r;\n }\n } catch {}\n }\n }\n }));\n }\n }\n\n // ====== DeepSeek ======\n async function translateDeepSeek(nodes, indices) {\n const batchSize = 20;\n const sep = \"\\n---\\n\";\n for (let ki = 0; ki < indices.length; ki += batchSize) {\n if (stopRequested) { showToast(\"⏹️ 已停止\", \"info\"); break; }\n const batchIdx = indices.slice(ki, ki + batchSize);\n const texts = batchIdx.map((i) => nodes[i].textContent.trim());\n const pct = Math.min(100, Math.round(((ki + batchIdx.length) / indices.length) * 100));\n setProgress(pct, `DeepSeek 翻译中 ${Math.min(ki + batchIdx.length, indices.length)}/${indices.length}...`);\n\n const result = await callDeepSeek(texts);\n if (result) {\n batchIdx.forEach((i, idx) => {\n const orig = texts[idx];\n if (result[idx]) {\n transCache.set(orig, result[idx]);\n nodes[i].textContent = cfg.mode === \"bilingual\" ? orig + \"\\n\" + result[idx] : result[idx];\n }\n });\n }\n }\n }\n\n // ====== 百度 API ======\n function callBaidu(text) {\n return new Promise((resolve, reject) => {\n const appid = cfg.baiduAppId;\n const key = cfg.baiduKey;\n const salt = Date.now() + Math.random();\n const sign = md5(appid + text + salt + key);\n const url = `https://fanyi-api.baidu.com/api/trans/vip/translate?q=${encodeURIComponent(text)}&from=auto&to=zh&appid=${appid}&salt=${salt}&sign=${sign}`;\n GM_xmlhttpRequest({\n method: \"GET\", url,\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data.trans_result) resolve(data.trans_result.map((r) => r.dst).join(\"\\n\"));\n else if (data.error_code) reject(new Error(`[${data.error_code}] ${data.error_msg}`));\n else reject(new Error(\"返回异常\"));\n } catch { reject(new Error(\"解析失败\")); }\n },\n onerror: () => reject(new Error(\"网络错误\")),\n timeout: 15000,\n });\n });\n }\n\n // ====== DeepSeek API ======\n function callDeepSeek(texts) {\n return new Promise((resolve) => {\n const joined = texts.map((t, i) => `[${i + 1}] ${t}`).join(\"\\n---\\n\");\n const prompt = `逐段翻译为简体中文,保持编号格式,只返回翻译结果:\\n\\n${joined}`;\n GM_xmlhttpRequest({\n method: \"POST\",\n url: \"https://api.deepseek.com/chat/completions\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${cfg.dsKey}` },\n data: JSON.stringify({\n model: cfg.dsModel,\n messages: [{ role: \"system\", content: \"逐段翻译,保持编号。\" }, { role: \"user\", content: prompt }],\n temperature: 0.3, max_tokens: 8192,\n }),\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data.choices && data.choices[0]) {\n const content = data.choices[0].message.content.trim();\n const results = [];\n for (let i = 0; i < texts.length; i++) {\n const re = new RegExp(`\\\\[${i + 1}\\\\]\\\\s*([\\\\s\\\\S]*?)(?=\\\\[${i + 2}\\\\]|$)`);\n const m = content.match(re);\n results.push(m ? m[1].trim() : \"\");\n }\n resolve(results);\n } else resolve(null);\n } catch { resolve(null); }\n },\n onerror: () => resolve(null),\n timeout: 60000,\n });\n });\n }\n\n // ====== MD5 ======\n function md5(str) {\n function rotateLeft(x, n) { return (x << n) | (x >>> (32 - n)); }\n function toHex(x) {\n var s = \"\";\n for (var i = 0; i < 4; i++) {\n s += (\"0\" + ((x >> (i * 8)) & 0xff).toString(16)).slice(-2);\n }\n return s;\n }\n function addUnsigned(x, y) {\n var lsw = (x & 0xffff) + (y & 0xffff);\n return ((x >>> 16) + (y >>> 16) + (lsw >>> 16)) << 16 | (lsw & 0xffff);\n }\n var i, j, aa, bb, cc, dd;\n var S = [[7, 12, 17, 22], [5, 9, 14, 20], [4, 11, 16, 23], [6, 10, 15, 21]];\n var T = [];\n for (i = 0; i < 64; i++) T[i] = Math.floor(Math.abs(Math.sin(i + 1)) * 0x100000000);\n str = unescape(encodeURIComponent(str));\n var n = str.length, bits = [];\n for (i = 0; i < n; i++) bits.push(str.charCodeAt(i));\n bits.push(0x80);\n var len = bits.length / 4 + 2;\n len = Math.ceil(len / 16) * 16;\n var x = [];\n for (i = 0; i < len; i++) x[i] = 0;\n for (i = 0; i < bits.length; i++) x[i >> 2] |= bits[i] << ((i % 4) * 8);\n x[x.length - 2] = n * 8;\n var a = 0x67452301, b = 0xefcdab89, c = 0x98badcfe, d = 0x10325476;\n for (i = 0; i < x.length; i += 16) {\n var X = x.slice(i, i + 16);\n aa = a; bb = b; cc = c; dd = d;\n for (j = 0; j < 64; j++) {\n var g, f;\n if (j < 16) { g = j; f = (bb & cc) | (~bb & dd); }\n else if (j < 32) { g = (5 * j + 1) % 16; f = (dd & bb) | (~dd & cc); }\n else if (j < 48) { g = (3 * j + 5) % 16; f = bb ^ cc ^ dd; }\n else { g = (7 * j) % 16; f = cc ^ (bb | ~dd); }\n var temp = dd; dd = cc; cc = bb;\n bb = addUnsigned(bb, rotateLeft(addUnsigned(addUnsigned(addUnsigned(aa, f), T[j]), X[g]), S[j >> 4][j & 3]));\n aa = temp;\n }\n a = addUnsigned(a, aa); b = addUnsigned(b, bb);\n c = addUnsigned(c, cc); d = addUnsigned(d, dd);\n }\n return (toHex(a) + toHex(b) + toHex(c) + toHex(d)).toLowerCase();\n }\n\n // ====== 工具 ======\n function setProgress(pct, text) { progressBar.style.width = pct + \"%\"; progressText.textContent = text; }\n function showToast(msg, type) { toast.textContent = msg; toast.className = type; toast.style.display = \"block\"; setTimeout(() => { toast.style.display = \"none\"; }, 3000); }\n function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }\n\n document.addEventListener(\"keydown\", (e) => { if (e.key === \"Escape\") panel.classList.remove(\"show\"); });\n\n const engName = cfg.engine === \"baidu\" ? \"百度\" : \"DeepSeek\";\n const modeName = cfg.mode === \"bilingual\" ? \"双语\" : \"仅译文\";\n console.log(`✅ 网页翻译已加载 | ${engName} | ${modeName} | 点击翻译/恢复,长按配置`);\n})();\n"}, "337f47071a44aff8108b8c1c6063892c0de2b54a": {"t": 1785766107.9674854, "n": 24017, "h": "75fe4e487094b7e7", "c": "// ==UserScript==\n// @name 网页翻译 · 多引擎\n// @namespace http://tampermonkey.net/\n// @version 1.3\n// @description 点击翻译整个页面/恢复原文。支持百度(极速)和DeepSeek(高质量),双语/单语模式,翻译结果缓存。长按进入配置。\n// @author you\n// @match *://*/*\n// @icon data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌐</text></svg>\n// @grant GM_setValue\n// @grant GM_getValue\n// @grant GM_addStyle\n// @grant GM_xmlhttpRequest\n// @license MIT\n// ==/UserScript==\n\n(function () {\n \"use strict\";\n\n // ====== 配置 ======\n const cfg = {\n engine: GM_getValue(\"ds_engine\", \"baidu\"),\n mode: GM_getValue(\"ds_mode\", \"bilingual\"),\n baiduAppId: GM_getValue(\"ds_baidu_id\", \"\"),\n baiduKey: GM_getValue(\"ds_baidu_key\", \"\"),\n dsKey: GM_getValue(\"ds_api_key\", \"\"),\n dsModel: GM_getValue(\"ds_model\", \"deepseek-chat\"),\n };\n\n // ====== 状态 ======\n let translated = false;\n let translating = false;\n let stopRequested = false;\n let originalTexts = null;\n const transCache = new Map(); // 原文 → 译文\n\n // ====== 样式 ======\n GM_addStyle(`\n #ds-fab {\n position: fixed; bottom: 24px; right: 24px; z-index: 999999;\n width: 52px; height: 52px; border-radius: 50%; background: #4f6ef7;\n color: #fff; border: none; font-size: 24px; cursor: grab;\n box-shadow: 0 2px 12px rgba(79,110,247,0.4);\n transition: background 0.3s;\n display: flex; align-items: center; justify-content: center;\n user-select: none; -webkit-user-select: none; touch-action: none;\n }\n #ds-fab:active { cursor: grabbing; }\n #ds-fab.dragging { transition: none; cursor: grabbing; box-shadow: 0 4px 20px rgba(79,110,247,0.6); }\n #ds-fab.translated { background: #a6e3a1; }\n\n #ds-panel {\n position: fixed; bottom: 88px; right: 24px; z-index: 999999;\n width: 320px; max-height: 70vh; overflow-y: auto;\n background: #1e1e2e; border-radius: 16px; padding: 20px;\n box-shadow: 0 8px 40px rgba(0,0,0,0.5); color: #cdd6f4;\n font: 14px -apple-system, sans-serif; display: none; border: 1px solid #313244;\n }\n #ds-panel.show { display: block; }\n #ds-panel h3 {\n margin: 0 0 16px 0; font-size: 16px; color: #cba6f7;\n display: flex; justify-content: space-between; align-items: center;\n }\n #ds-panel h3 button { background: none; border: none; color: #a6adc8; cursor: pointer; font-size: 18px; padding: 0 4px; }\n #ds-panel h3 button:hover { color: #f38ba8; }\n #ds-panel label { display: block; margin-bottom: 4px; color: #a6adc8; font-size: 12px; margin-top: 8px; }\n #ds-panel label:first-of-type { margin-top: 0; }\n #ds-panel input, #ds-panel select {\n width: 100%; padding: 8px 12px; margin-bottom: 4px;\n background: #313244; border: 1px solid #45475a; border-radius: 8px;\n color: #cdd6f4; font-size: 13px; box-sizing: border-box; outline: none;\n }\n #ds-panel input:focus, #ds-panel select:focus { border-color: #4f6ef7; }\n #ds-panel .hint { font-size: 11px; color: #6c7086; margin: 2px 0 8px 0; }\n\n #ds-panel .engine-tab {\n display: flex; gap: 6px; margin-bottom: 6px;\n }\n #ds-panel .engine-tab button {\n flex: 1; padding: 7px; border: 1px solid #45475a; border-radius: 8px;\n background: #313244; color: #a6adc8; cursor: pointer; font-size: 12px; transition: all 0.2s;\n }\n #ds-panel .engine-tab button.active { background: #4f6ef7; border-color: #4f6ef7; color: #fff; }\n\n #ds-panel .mode-tab {\n display: flex; gap: 6px; margin-bottom: 12px;\n }\n #ds-panel .mode-tab button {\n flex: 1; padding: 6px; border: 1px solid #45475a; border-radius: 8px;\n background: #313244; color: #a6adc8; cursor: pointer; font-size: 12px; transition: all 0.2s;\n }\n #ds-panel .mode-tab button.active { background: #585b70; border-color: #585b70; color: #fff; }\n\n #ds-panel .cfg-section { display: none; }\n #ds-panel .cfg-section.show { display: block; }\n #ds-panel .btn-row { display: flex; gap: 8px; margin-top: 12px; }\n #ds-panel .btn-row button {\n flex: 1; padding: 9px; border: none; border-radius: 8px;\n cursor: pointer; font-size: 13px; transition: opacity 0.2s;\n }\n #ds-panel .btn-row button:active { opacity: 0.8; }\n #ds-panel .btn-save { background: #4f6ef7; color: #fff; }\n #ds-panel .btn-close-cfg { background: #45475a; color: #cdd6f4; }\n #ds-panel .btn-clear-cache { background: #45475a; color: #f38ba8; }\n\n #ds-progress { margin-top: 10px; display: none; }\n #ds-progress.show { display: block; }\n #ds-progress .bar-wrap { width: 100%; height: 4px; background: #313244; border-radius: 2px; overflow: hidden; }\n #ds-progress .bar { height: 100%; width: 0%; background: #a6e3a1; border-radius: 2px; transition: width 0.3s; }\n #ds-progress .text { font-size: 11px; color: #a6adc8; margin-top: 4px; text-align: center; }\n\n #ds-toast {\n position: fixed; top: 20px; left: 50%; transform: translateX(-50%);\n z-index: 9999999; padding: 10px 24px; border-radius: 10px;\n font: 14px -apple-system, sans-serif; color: #fff;\n display: none; box-shadow: 0 4px 20px rgba(0,0,0,0.3);\n }\n #ds-toast.ok { background: #1e3a2e; }\n #ds-toast.err { background: #3a1e1e; }\n #ds-toast.info { background: #1e2a3a; }\n `);\n\n // ====== DOM ======\n document.body.insertAdjacentHTML(\"beforeend\", `\n <button id=\"ds-fab\">🌐</button>\n <div id=\"ds-panel\">\n <h3>\n ⚙️ 翻译设置\n <button id=\"ds-close-panel\">✕</button>\n </h3>\n\n <label>翻译引擎</label>\n <div class=\"engine-tab\">\n <button data-engine=\"baidu\" class=\"active\">⚡ 百度翻译</button>\n <button data-engine=\"deepseek\">🧠 DeepSeek</button>\n </div>\n\n <label>显示模式</label>\n <div class=\"mode-tab\">\n <button data-mode=\"bilingual\" class=\"active\">📖 双语</button>\n <button data-mode=\"monolingual\">🔤 仅译文</button>\n </div>\n\n <div class=\"cfg-section show\" id=\"ds-cfg-baidu\">\n <label>APP ID</label>\n <input id=\"ds-baidu-id\" placeholder=\"从 fanyi-api.baidu.com 获取\" value=\"${cfg.baiduAppId}\">\n <label>密钥</label>\n <input type=\"password\" id=\"ds-baidu-key\" placeholder=\"从 fanyi-api.baidu.com 获取\" value=\"${cfg.baiduKey}\">\n <div class=\"hint\">免费版每月 100 万字符</div>\n </div>\n <div class=\"cfg-section\" id=\"ds-cfg-ds\">\n <label>API Key</label>\n <input type=\"password\" id=\"ds-ds-key\" placeholder=\"sk-xxxxxxxx\" value=\"${cfg.dsKey}\">\n <label>模型</label>\n <select id=\"ds-ds-model\">\n <option value=\"deepseek-chat\" ${cfg.dsModel === \"deepseek-chat\" ? \"selected\" : \"\"}>DeepSeek V3</option>\n <option value=\"deepseek-reasoner\" ${cfg.dsModel === \"deepseek-reasoner\" ? \"selected\" : \"\"}>DeepSeek R1</option>\n </select>\n <div class=\"hint\">质量高但速度较慢</div>\n </div>\n <div class=\"btn-row\">\n <button class=\"btn-save\" id=\"ds-save-btn\">💾 保存配置</button>\n <button class=\"btn-close-cfg\" id=\"ds-close-btn\">关闭</button>\n </div>\n <div class=\"hint\" style=\"text-align:center;margin-top:8px;cursor:pointer\" id=\"ds-clear-cache\">🗑️ 清除翻译缓存 (${transCache.size}条)</div>\n <div id=\"ds-progress\">\n <div class=\"bar-wrap\"><div class=\"bar\" id=\"ds-progress-bar\"></div></div>\n <div class=\"text\" id=\"ds-progress-text\">准备中...</div>\n </div>\n </div>\n <div id=\"ds-toast\"></div>\n `);\n\n // ====== DOM 引用 ======\n const fab = document.getElementById(\"ds-fab\");\n const panel = document.getElementById(\"ds-panel\");\n const progress = document.getElementById(\"ds-progress\");\n const progressBar = document.getElementById(\"ds-progress-bar\");\n const progressText = document.getElementById(\"ds-progress-text\");\n const toast = document.getElementById(\"ds-toast\");\n const cacheHint = document.getElementById(\"ds-clear-cache\");\n\n // ====== 引擎切换 ======\n document.querySelectorAll(\".engine-tab button\").forEach((btn) => {\n btn.addEventListener(\"click\", () => {\n document.querySelectorAll(\".engine-tab button\").forEach((b) => b.classList.remove(\"active\"));\n btn.classList.add(\"active\");\n cfg.engine = btn.dataset.engine;\n document.getElementById(\"ds-cfg-baidu\").classList.toggle(\"show\", cfg.engine === \"baidu\");\n document.getElementById(\"ds-cfg-ds\").classList.toggle(\"show\", cfg.engine === \"deepseek\");\n });\n });\n\n // ====== 模式切换 ======\n document.querySelectorAll(\".mode-tab button\").forEach((btn) => {\n btn.addEventListener(\"click\", () => {\n document.querySelectorAll(\".mode-tab button\").forEach((b) => b.classList.remove(\"active\"));\n btn.classList.add(\"active\");\n cfg.mode = btn.dataset.mode;\n });\n });\n\n // ====== 保存 / 关闭 ======\n document.getElementById(\"ds-save-btn\").addEventListener(\"click\", () => {\n saveConfig();\n showToast(\"✅ 配置已保存\", \"ok\");\n });\n document.getElementById(\"ds-close-btn\").addEventListener(\"click\", () => panel.classList.remove(\"show\"));\n document.getElementById(\"ds-close-panel\").addEventListener(\"click\", () => panel.classList.remove(\"show\"));\n\n // 清除缓存\n cacheHint.addEventListener(\"click\", () => {\n transCache.clear();\n cacheHint.textContent = `🗑️ 清除翻译缓存 (0条)`;\n showToast(\"缓存已清除\", \"info\");\n });\n\n // ====== 保存配置 ======\n function saveConfig() {\n cfg.baiduAppId = document.getElementById(\"ds-baidu-id\").value.trim();\n cfg.baiduKey = document.getElementById(\"ds-baidu-key\").value.trim();\n cfg.dsKey = document.getElementById(\"ds-ds-key\").value.trim();\n cfg.dsModel = document.getElementById(\"ds-ds-model\").value;\n\n GM_setValue(\"ds_engine\", cfg.engine);\n GM_setValue(\"ds_mode\", cfg.mode);\n GM_setValue(\"ds_baidu_id\", cfg.baiduAppId);\n GM_setValue(\"ds_baidu_key\", cfg.baiduKey);\n GM_setValue(\"ds_api_key\", cfg.dsKey);\n GM_setValue(\"ds_model\", cfg.dsModel);\n }\n\n // ====== 可拖动按钮 ======\n let isDragging = false, dragStartX = 0, dragStartY = 0, dragOrigX = 0, dragOrigY = 0, dragMoved = false;\n let pressTimer = null, pressStart = 0, isLongPress = false;\n\n // 鼠标拖动\n fab.addEventListener(\"mousedown\", (e) => {\n if (e.button !== 0) return;\n isDragging = true; dragMoved = false;\n const rect = fab.getBoundingClientRect();\n dragStartX = e.clientX; dragStartY = e.clientY;\n dragOrigX = rect.left; dragOrigY = rect.top;\n fab.classList.add(\"dragging\");\n fab.style.cursor = \"grabbing\";\n });\n\n document.addEventListener(\"mousemove\", (e) => {\n if (!isDragging) return;\n const dx = e.clientX - dragStartX;\n const dy = e.clientY - dragStartY;\n if (Math.abs(dx) > 5 || Math.abs(dy) > 5) dragMoved = true;\n const vw = window.innerWidth, vh = window.innerHeight;\n const fw = 52, fh = 52;\n let nx = Math.max(0, Math.min(vw - fw, dragOrigX + dx));\n let ny = Math.max(0, Math.min(vh - fh, dragOrigY + dy));\n fab.style.left = nx + \"px\";\n fab.style.right = \"auto\";\n fab.style.top = ny + \"px\";\n fab.style.bottom = \"auto\";\n });\n\n document.addEventListener(\"mouseup\", (e) => {\n if (!isDragging) return;\n isDragging = false;\n fab.classList.remove(\"dragging\");\n fab.style.cursor = \"grab\";\n if (dragMoved) {\n e.stopPropagation();\n e.preventDefault();\n }\n });\n\n fab.addEventListener(\"click\", (e) => {\n if (dragMoved) { e.stopPropagation(); e.preventDefault(); return; }\n if (\"ontouchstart\" in window) return;\n toggleTranslate();\n });\n\n // 触屏拖动\n fab.addEventListener(\"touchstart\", (e) => {\n const t = e.touches[0];\n isDragging = true; dragMoved = false;\n const rect = fab.getBoundingClientRect();\n dragStartX = t.clientX; dragStartY = t.clientY;\n dragOrigX = rect.left; dragOrigY = rect.top;\n fab.classList.add(\"dragging\");\n // 长按\n pressStart = Date.now(); isLongPress = false;\n pressTimer = setTimeout(() => {\n if (!dragMoved) { isLongPress = true; navigator.vibrate?.(20); panel.classList.add(\"show\"); }\n }, 500);\n }, { passive: true });\n\n fab.addEventListener(\"touchmove\", (e) => {\n if (!isDragging) return;\n const t = e.touches[0];\n const dx = t.clientX - dragStartX;\n const dy = t.clientY - dragStartY;\n if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {\n dragMoved = true;\n clearTimeout(pressTimer);\n }\n const vw = window.innerWidth, vh = window.innerHeight;\n const fw = 52, fh = 52;\n let nx = Math.max(0, Math.min(vw - fw, dragOrigX + dx));\n let ny = Math.max(0, Math.min(vh - fh, dragOrigY + dy));\n fab.style.left = nx + \"px\";\n fab.style.right = \"auto\";\n fab.style.top = ny + \"px\";\n fab.style.bottom = \"auto\";\n e.preventDefault();\n }, { passive: false });\n\n fab.addEventListener(\"touchend\", (e) => {\n isDragging = false;\n clearTimeout(pressTimer);\n fab.classList.remove(\"dragging\");\n if (!dragMoved && !isLongPress && Date.now() - pressStart < 500) {\n e.preventDefault();\n toggleTranslate();\n }\n }, { passive: false });\n\n fab.addEventListener(\"contextmenu\", (e) => { e.preventDefault(); panel.classList.toggle(\"show\"); });\n\n // ====== 切换 ======\n async function toggleTranslate() {\n if (translating) return;\n saveConfig();\n\n if (translating) {\n stopRequested = true;\n showToast(\"⏹️ 正在停止...\", \"info\");\n return;\n }\n if (translated) { restoreOriginal(); return; }\n\n if (cfg.engine === \"baidu\" && (!cfg.baiduAppId || !cfg.baiduKey)) {\n showToast(\"请先配置百度翻译(长按图标设置)\", \"err\"); return;\n }\n if (cfg.engine === \"deepseek\" && !cfg.dsKey) {\n showToast(\"请先配置 DeepSeek Key(长按图标设置)\", \"err\"); return;\n }\n\n await translatePage();\n }\n\n function restoreOriginal() {\n if (!originalTexts) return;\n originalTexts.forEach(({ node, text }) => { node.textContent = text; });\n translated = false; fab.classList.remove(\"translated\"); fab.textContent = \"🌐\";\n showToast(\"已恢复原文\", \"info\");\n }\n\n // ====== 翻译页面 ======\n async function translatePage() {\n translating = true;\n progress.classList.add(\"show\");\n setProgress(0, \"提取文本...\");\n\n try {\n const entries = [];\n const walk = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {\n acceptNode: (n) => {\n const p = n.parentElement;\n if (!p || p.closest(\"script,style,code,pre,#ds-panel,#ds-fab,#ds-toast,svg\"))\n return NodeFilter.FILTER_REJECT;\n const t = n.textContent.trim();\n if (t.length < 10) return NodeFilter.FILTER_REJECT;\n if (p.offsetWidth === 0 || p.offsetHeight === 0) return NodeFilter.FILTER_REJECT;\n return NodeFilter.FILTER_ACCEPT;\n },\n });\n while (walk.nextNode()) entries.push({ node: walk.currentNode, text: walk.currentNode.textContent });\n\n if (entries.length === 0) { showToast(\"没有可翻译的文本\", \"info\"); return; }\n originalTexts = entries;\n\n const nodes = entries.map((e) => e.node);\n const needApi = []; // 需要调用 API 的节点索引\n const cached = []; // 有缓存的节点索引\n\n nodes.forEach((node, i) => {\n const key = node.textContent.trim();\n if (transCache.has(key)) {\n cached.push(i);\n } else {\n needApi.push(i);\n }\n });\n\n // 有缓存的直接填充\n cached.forEach((i) => {\n const key = nodes[i].textContent.trim();\n const t = transCache.get(key);\n nodes[i].textContent = cfg.mode === \"bilingual\" ? key + \"\\n\" + t : t;\n });\n\n // 需要调 API 的\n if (needApi.length > 0) {\n if (cfg.engine === \"baidu\") {\n await translateBaidu(nodes, needApi);\n } else {\n await translateDeepSeek(nodes, needApi);\n }\n }\n\n // 更新缓存计数\n cacheHint.textContent = `🗑️ 清除翻译缓存 (${transCache.size}条)`;\n\n translated = true;\n fab.classList.add(\"translated\"); fab.textContent = \"✅\";\n setProgress(100, \"✅ 翻译完成\");\n const fromCache = cached.length > 0 ? ` (缓存${cached.length}段)` : \"\";\n showToast(`翻译完成${fromCache},再点恢复原文`, \"ok\");\n } catch (err) {\n showToast(\"出错: \" + err.message, \"err\");\n }\n setTimeout(() => { progress.classList.remove(\"show\"); translating = false; }, 1500);\n }\n\n // ====== 百度翻译 ======\n async function translateBaidu(nodes, indices) {\n const sep = \"\\n|||ds-sep|||\\n\";\n const maxBytes = 5000;\n // 按字节长度分批\n const batches = [];\n let cur = [], curBytes = 0;\n for (let ki = 0; ki < indices.length; ki++) {\n const t = nodes[indices[ki]].textContent.trim();\n const bytes = new Blob([t]).size;\n if (cur.length > 0 && curBytes + bytes + 20 > maxBytes) {\n batches.push(cur);\n cur = []; curBytes = 0;\n }\n cur.push({ idx: indices[ki], text: t, bytes });\n curBytes += bytes + 20;\n }\n if (cur.length) batches.push(cur);\n\n // 并发执行所有批次(最多同时3批)\n for (let b = 0; b < batches.length; b += 3) {\n if (stopRequested) { showToast(\"⏹️ 已停止\", \"info\"); break; }\n const chunk = batches.slice(b, b + 3);\n const pct = Math.min(100, Math.round(((b + chunk.length) / batches.length) * 100));\n setProgress(pct, `百度翻译中 ${Math.min((b + chunk.length) * 30, indices.length)}/${indices.length}...`);\n\n await Promise.all(chunk.map(async (batch) => {\n try {\n const joined = batch.map(x => x.text).join(sep);\n const result = await callBaidu(joined);\n if (result) {\n const parts = result.split(\"|||ds-sep|||\").map((s) => s.trim());\n batch.forEach((item, idx) => {\n const t = parts[idx];\n if (t && t !== item.text) {\n transCache.set(item.text, t);\n const node = nodes[item.idx];\n node.textContent = cfg.mode === \"bilingual\" ? item.text + \"\\n\" + t : t;\n }\n });\n }\n } catch (e) {\n console.warn(\"百度批量失败:\", e.message.slice(0, 30));\n // 单条重试\n for (const item of batch) {\n if (stopRequested) break;\n try {\n const r = await callBaidu(item.text);\n if (r) {\n transCache.set(item.text, r);\n nodes[item.idx].textContent = cfg.mode === \"bilingual\" ? item.text + \"\\n\" + r : r;\n }\n } catch {}\n }\n }\n }));\n }\n }\n\n // ====== DeepSeek ======\n async function translateDeepSeek(nodes, indices) {\n const batchSize = 20;\n const sep = \"\\n---\\n\";\n for (let ki = 0; ki < indices.length; ki += batchSize) {\n if (stopRequested) { showToast(\"⏹️ 已停止\", \"info\"); break; }\n const batchIdx = indices.slice(ki, ki + batchSize);\n const texts = batchIdx.map((i) => nodes[i].textContent.trim());\n const pct = Math.min(100, Math.round(((ki + batchIdx.length) / indices.length) * 100));\n setProgress(pct, `DeepSeek 翻译中 ${Math.min(ki + batchIdx.length, indices.length)}/${indices.length}...`);\n\n const result = await callDeepSeek(texts);\n if (result) {\n batchIdx.forEach((i, idx) => {\n const orig = texts[idx];\n if (result[idx]) {\n transCache.set(orig, result[idx]);\n nodes[i].textContent = cfg.mode === \"bilingual\" ? orig + \"\\n\" + result[idx] : result[idx];\n }\n });\n }\n }\n }\n\n // ====== 百度 API ======\n function callBaidu(text) {\n return new Promise((resolve, reject) => {\n const appid = cfg.baiduAppId;\n const key = cfg.baiduKey;\n const salt = Date.now() + Math.random();\n const sign = md5(appid + text + salt + key);\n const url = `https://fanyi-api.baidu.com/api/trans/vip/translate?q=${encodeURIComponent(text)}&from=auto&to=zh&appid=${appid}&salt=${salt}&sign=${sign}`;\n GM_xmlhttpRequest({\n method: \"GET\", url,\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data.trans_result) resolve(data.trans_result.map((r) => r.dst).join(\"\\n\"));\n else if (data.error_code) reject(new Error(`[${data.error_code}] ${data.error_msg}`));\n else reject(new Error(\"返回异常\"));\n } catch { reject(new Error(\"解析失败\")); }\n },\n onerror: () => reject(new Error(\"网络错误\")),\n timeout: 15000,\n });\n });\n }\n\n // ====== DeepSeek API ======\n function callDeepSeek(texts) {\n return new Promise((resolve) => {\n const joined = texts.map((t, i) => `[${i + 1}] ${t}`).join(\"\\n---\\n\");\n const prompt = `逐段翻译为简体中文,保持编号格式,只返回翻译结果:\\n\\n${joined}`;\n GM_xmlhttpRequest({\n method: \"POST\",\n url: \"https://api.deepseek.com/chat/completions\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${cfg.dsKey}` },\n data: JSON.stringify({\n model: cfg.dsModel,\n messages: [{ role: \"system\", content: \"逐段翻译,保持编号。\" }, { role: \"user\", content: prompt }],\n temperature: 0.3, max_tokens: 8192,\n }),\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data.choices && data.choices[0]) {\n const content = data.choices[0].message.content.trim();\n const results = [];\n for (let i = 0; i < texts.length; i++) {\n const re = new RegExp(`\\\\[${i + 1}\\\\]\\\\s*([\\\\s\\\\S]*?)(?=\\\\[${i + 2}\\\\]|$)`);\n const m = content.match(re);\n results.push(m ? m[1].trim() : \"\");\n }\n resolve(results);\n } else resolve(null);\n } catch { resolve(null); }\n },\n onerror: () => resolve(null),\n timeout: 60000,\n });\n });\n }\n\n // ====== MD5 ======\n function md5(str) {\n function rotateLeft(x, n) { return (x << n) | (x >>> (32 - n)); }\n function toHex(x) {\n var s = \"\";\n for (var i = 0; i < 4; i++) {\n s += (\"0\" + ((x >> (i * 8)) & 0xff).toString(16)).slice(-2);\n }\n return s;\n }\n function addUnsigned(x, y) {\n var lsw = (x & 0xffff) + (y & 0xffff);\n return ((x >>> 16) + (y >>> 16) + (lsw >>> 16)) << 16 | (lsw & 0xffff);\n }\n var i, j, aa, bb, cc, dd;\n var S = [[7, 12, 17, 22], [5, 9, 14, 20], [4, 11, 16, 23], [6, 10, 15, 21]];\n var T = [];\n for (i = 0; i < 64; i++) T[i] = Math.floor(Math.abs(Math.sin(i + 1)) * 0x100000000);\n str = unescape(encodeURIComponent(str));\n var n = str.length, bits = [];\n for (i = 0; i < n; i++) bits.push(str.charCodeAt(i));\n bits.push(0x80);\n var len = bits.length / 4 + 2;\n len = Math.ceil(len / 16) * 16;\n var x = [];\n for (i = 0; i < len; i++) x[i] = 0;\n for (i = 0; i < bits.length; i++) x[i >> 2] |= bits[i] << ((i % 4) * 8);\n x[x.length - 2] = n * 8;\n var a = 0x67452301, b = 0xefcdab89, c = 0x98badcfe, d = 0x10325476;\n for (i = 0; i < x.length; i += 16) {\n var X = x.slice(i, i + 16);\n aa = a; bb = b; cc = c; dd = d;\n for (j = 0; j < 64; j++) {\n var g, f;\n if (j < 16) { g = j; f = (bb & cc) | (~bb & dd); }\n else if (j < 32) { g = (5 * j + 1) % 16; f = (dd & bb) | (~dd & cc); }\n else if (j < 48) { g = (3 * j + 5) % 16; f = bb ^ cc ^ dd; }\n else { g = (7 * j) % 16; f = cc ^ (bb | ~dd); }\n var temp = dd; dd = cc; cc = bb;\n bb = addUnsigned(bb, rotateLeft(addUnsigned(addUnsigned(addUnsigned(aa, f), T[j]), X[g]), S[j >> 4][j & 3]));\n aa = temp;\n }\n a = addUnsigned(a, aa); b = addUnsigned(b, bb);\n c = addUnsigned(c, cc); d = addUnsigned(d, dd);\n }\n return (toHex(a) + toHex(b) + toHex(c) + toHex(d)).toLowerCase();\n }\n\n // ====== 工具 ======\n function setProgress(pct, text) { progressBar.style.width = pct + \"%\"; progressText.textContent = text; }\n function showToast(msg, type) { toast.textContent = msg; toast.className = type; toast.style.display = \"block\"; setTimeout(() => { toast.style.display = \"none\"; }, 3000); }\n function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }\n\n document.addEventListener(\"keydown\", (e) => { if (e.key === \"Escape\") panel.classList.remove(\"show\"); });\n\n const engName = cfg.engine === \"baidu\" ? \"百度\" : \"DeepSeek\";\n const modeName = cfg.mode === \"bilingual\" ? \"双语\" : \"仅译文\";\n console.log(`✅ 网页翻译已加载 | ${engName} | ${modeName} | 点击翻译/恢复,长按配置`);\n})();\n"}, "8fb743634b4de5074d96a7cec529f150a38a353c": {"t": 1785766109.4729054, "n": 21526, "h": "68c6fbe89d3127c1", "c": "// ==UserScript==\n// @name 网页翻译 · 多引擎\n// @namespace http://tampermonkey.net/\n// @version 1.3\n// @description 点击翻译整个页面/恢复原文。支持百度(极速)和DeepSeek(高质量),双语/单语模式,翻译结果缓存。长按进入配置。\n// @author you\n// @match *://*/*\n// @icon data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌐</text></svg>\n// @grant GM_setValue\n// @grant GM_getValue\n// @grant GM_addStyle\n// @grant GM_xmlhttpRequest\n// @license MIT\n// ==/UserScript==\n\n(function () {\n \"use strict\";\n\n // ====== 配置 ======\n const cfg = {\n engine: GM_getValue(\"ds_engine\", \"baidu\"),\n mode: GM_getValue(\"ds_mode\", \"bilingual\"),\n baiduAppId: GM_getValue(\"ds_baidu_id\", \"\"),\n baiduKey: GM_getValue(\"ds_baidu_key\", \"\"),\n dsKey: GM_getValue(\"ds_api_key\", \"\"),\n dsModel: GM_getValue(\"ds_model\", \"deepseek-chat\"),\n };\n\n // ====== 状态 ======\n let translated = false;\n let translating = false;\n let stopRequested = false;\n let originalTexts = null;\n const transCache = new Map(); // 原文 → 译文\n\n // ====== 样式 ======\n GM_addStyle(`\n #ds-fab {\n position: fixed; bottom: 24px; right: 24px; z-index: 999999;\n width: 52px; height: 52px; border-radius: 50%; background: #4f6ef7;\n color: #fff; border: none; font-size: 24px; cursor: pointer;\n box-shadow: 0 2px 12px rgba(79,110,247,0.4);\n transition: transform 0.15s, background 0.3s;\n display: flex; align-items: center; justify-content: center;\n user-select: none; -webkit-user-select: none; touch-action: manipulation;\n }\n #ds-fab:active { transform: scale(0.92); }\n #ds-fab.translated { background: #a6e3a1; }\n\n #ds-panel {\n position: fixed; bottom: 88px; right: 24px; z-index: 999999;\n width: 320px; max-height: 70vh; overflow-y: auto;\n background: #1e1e2e; border-radius: 16px; padding: 20px;\n box-shadow: 0 8px 40px rgba(0,0,0,0.5); color: #cdd6f4;\n font: 14px -apple-system, sans-serif; display: none; border: 1px solid #313244;\n }\n #ds-panel.show { display: block; }\n #ds-panel h3 {\n margin: 0 0 16px 0; font-size: 16px; color: #cba6f7;\n display: flex; justify-content: space-between; align-items: center;\n }\n #ds-panel h3 button { background: none; border: none; color: #a6adc8; cursor: pointer; font-size: 18px; padding: 0 4px; }\n #ds-panel h3 button:hover { color: #f38ba8; }\n #ds-panel label { display: block; margin-bottom: 4px; color: #a6adc8; font-size: 12px; margin-top: 8px; }\n #ds-panel label:first-of-type { margin-top: 0; }\n #ds-panel input, #ds-panel select {\n width: 100%; padding: 8px 12px; margin-bottom: 4px;\n background: #313244; border: 1px solid #45475a; border-radius: 8px;\n color: #cdd6f4; font-size: 13px; box-sizing: border-box; outline: none;\n }\n #ds-panel input:focus, #ds-panel select:focus { border-color: #4f6ef7; }\n #ds-panel .hint { font-size: 11px; color: #6c7086; margin: 2px 0 8px 0; }\n\n #ds-panel .engine-tab {\n display: flex; gap: 6px; margin-bottom: 6px;\n }\n #ds-panel .engine-tab button {\n flex: 1; padding: 7px; border: 1px solid #45475a; border-radius: 8px;\n background: #313244; color: #a6adc8; cursor: pointer; font-size: 12px; transition: all 0.2s;\n }\n #ds-panel .engine-tab button.active { background: #4f6ef7; border-color: #4f6ef7; color: #fff; }\n\n #ds-panel .mode-tab {\n display: flex; gap: 6px; margin-bottom: 12px;\n }\n #ds-panel .mode-tab button {\n flex: 1; padding: 6px; border: 1px solid #45475a; border-radius: 8px;\n background: #313244; color: #a6adc8; cursor: pointer; font-size: 12px; transition: all 0.2s;\n }\n #ds-panel .mode-tab button.active { background: #585b70; border-color: #585b70; color: #fff; }\n\n #ds-panel .cfg-section { display: none; }\n #ds-panel .cfg-section.show { display: block; }\n #ds-panel .btn-row { display: flex; gap: 8px; margin-top: 12px; }\n #ds-panel .btn-row button {\n flex: 1; padding: 9px; border: none; border-radius: 8px;\n cursor: pointer; font-size: 13px; transition: opacity 0.2s;\n }\n #ds-panel .btn-row button:active { opacity: 0.8; }\n #ds-panel .btn-save { background: #4f6ef7; color: #fff; }\n #ds-panel .btn-close-cfg { background: #45475a; color: #cdd6f4; }\n #ds-panel .btn-clear-cache { background: #45475a; color: #f38ba8; }\n\n #ds-progress { margin-top: 10px; display: none; }\n #ds-progress.show { display: block; }\n #ds-progress .bar-wrap { width: 100%; height: 4px; background: #313244; border-radius: 2px; overflow: hidden; }\n #ds-progress .bar { height: 100%; width: 0%; background: #a6e3a1; border-radius: 2px; transition: width 0.3s; }\n #ds-progress .text { font-size: 11px; color: #a6adc8; margin-top: 4px; text-align: center; }\n\n #ds-toast {\n position: fixed; top: 20px; left: 50%; transform: translateX(-50%);\n z-index: 9999999; padding: 10px 24px; border-radius: 10px;\n font: 14px -apple-system, sans-serif; color: #fff;\n display: none; box-shadow: 0 4px 20px rgba(0,0,0,0.3);\n }\n #ds-toast.ok { background: #1e3a2e; }\n #ds-toast.err { background: #3a1e1e; }\n #ds-toast.info { background: #1e2a3a; }\n `);\n\n // ====== DOM ======\n document.body.insertAdjacentHTML(\"beforeend\", `\n <button id=\"ds-fab\">🌐</button>\n <div id=\"ds-panel\">\n <h3>\n ⚙️ 翻译设置\n <button id=\"ds-close-panel\">✕</button>\n </h3>\n\n <label>翻译引擎</label>\n <div class=\"engine-tab\">\n <button data-engine=\"baidu\" class=\"active\">⚡ 百度翻译</button>\n <button data-engine=\"deepseek\">🧠 DeepSeek</button>\n </div>\n\n <label>显示模式</label>\n <div class=\"mode-tab\">\n <button data-mode=\"bilingual\" class=\"active\">📖 双语</button>\n <button data-mode=\"monolingual\">🔤 仅译文</button>\n </div>\n\n <div class=\"cfg-section show\" id=\"ds-cfg-baidu\">\n <label>APP ID</label>\n <input id=\"ds-baidu-id\" placeholder=\"从 fanyi-api.baidu.com 获取\" value=\"${cfg.baiduAppId}\">\n <label>密钥</label>\n <input type=\"password\" id=\"ds-baidu-key\" placeholder=\"从 fanyi-api.baidu.com 获取\" value=\"${cfg.baiduKey}\">\n <div class=\"hint\">免费版每月 100 万字符</div>\n </div>\n <div class=\"cfg-section\" id=\"ds-cfg-ds\">\n <label>API Key</label>\n <input type=\"password\" id=\"ds-ds-key\" placeholder=\"sk-xxxxxxxx\" value=\"${cfg.dsKey}\">\n <label>模型</label>\n <select id=\"ds-ds-model\">\n <option value=\"deepseek-chat\" ${cfg.dsModel === \"deepseek-chat\" ? \"selected\" : \"\"}>DeepSeek V3</option>\n <option value=\"deepseek-reasoner\" ${cfg.dsModel === \"deepseek-reasoner\" ? \"selected\" : \"\"}>DeepSeek R1</option>\n </select>\n <div class=\"hint\">质量高但速度较慢</div>\n </div>\n <div class=\"btn-row\">\n <button class=\"btn-save\" id=\"ds-save-btn\">💾 保存配置</button>\n <button class=\"btn-close-cfg\" id=\"ds-close-btn\">关闭</button>\n </div>\n <div class=\"hint\" style=\"text-align:center;margin-top:8px;cursor:pointer\" id=\"ds-clear-cache\">🗑️ 清除翻译缓存 (${transCache.size}条)</div>\n <div id=\"ds-progress\">\n <div class=\"bar-wrap\"><div class=\"bar\" id=\"ds-progress-bar\"></div></div>\n <div class=\"text\" id=\"ds-progress-text\">准备中...</div>\n </div>\n </div>\n <div id=\"ds-toast\"></div>\n `);\n\n // ====== DOM 引用 ======\n const fab = document.getElementById(\"ds-fab\");\n const panel = document.getElementById(\"ds-panel\");\n const progress = document.getElementById(\"ds-progress\");\n const progressBar = document.getElementById(\"ds-progress-bar\");\n const progressText = document.getElementById(\"ds-progress-text\");\n const toast = document.getElementById(\"ds-toast\");\n const cacheHint = document.getElementById(\"ds-clear-cache\");\n\n // ====== 引擎切换 ======\n document.querySelectorAll(\".engine-tab button\").forEach((btn) => {\n btn.addEventListener(\"click\", () => {\n document.querySelectorAll(\".engine-tab button\").forEach((b) => b.classList.remove(\"active\"));\n btn.classList.add(\"active\");\n cfg.engine = btn.dataset.engine;\n document.getElementById(\"ds-cfg-baidu\").classList.toggle(\"show\", cfg.engine === \"baidu\");\n document.getElementById(\"ds-cfg-ds\").classList.toggle(\"show\", cfg.engine === \"deepseek\");\n });\n });\n\n // ====== 模式切换 ======\n document.querySelectorAll(\".mode-tab button\").forEach((btn) => {\n btn.addEventListener(\"click\", () => {\n document.querySelectorAll(\".mode-tab button\").forEach((b) => b.classList.remove(\"active\"));\n btn.classList.add(\"active\");\n cfg.mode = btn.dataset.mode;\n });\n });\n\n // ====== 保存 / 关闭 ======\n document.getElementById(\"ds-save-btn\").addEventListener(\"click\", () => {\n saveConfig();\n showToast(\"✅ 配置已保存\", \"ok\");\n });\n document.getElementById(\"ds-close-btn\").addEventListener(\"click\", () => panel.classList.remove(\"show\"));\n document.getElementById(\"ds-close-panel\").addEventListener(\"click\", () => panel.classList.remove(\"show\"));\n\n // 清除缓存\n cacheHint.addEventListener(\"click\", () => {\n transCache.clear();\n cacheHint.textContent = `🗑️ 清除翻译缓存 (0条)`;\n showToast(\"缓存已清除\", \"info\");\n });\n\n // ====== 保存配置 ======\n function saveConfig() {\n cfg.baiduAppId = document.getElementById(\"ds-baidu-id\").value.trim();\n cfg.baiduKey = document.getElementById(\"ds-baidu-key\").value.trim();\n cfg.dsKey = document.getElementById(\"ds-ds-key\").value.trim();\n cfg.dsModel = document.getElementById(\"ds-ds-model\").value;\n\n GM_setValue(\"ds_engine\", cfg.engine);\n GM_setValue(\"ds_mode\", cfg.mode);\n GM_setValue(\"ds_baidu_id\", cfg.baiduAppId);\n GM_setValue(\"ds_baidu_key\", cfg.baiduKey);\n GM_setValue(\"ds_api_key\", cfg.dsKey);\n GM_setValue(\"ds_model\", cfg.dsModel);\n }\n\n // ====== 长按 / 点击 ======\n let pressTimer = null, pressStart = 0, isLongPress = false;\n\n fab.addEventListener(\"touchstart\", () => {\n pressStart = Date.now(); isLongPress = false;\n pressTimer = setTimeout(() => {\n isLongPress = true; navigator.vibrate?.(20); panel.classList.add(\"show\");\n }, 500);\n }, { passive: true });\n\n fab.addEventListener(\"touchend\", (e) => {\n clearTimeout(pressTimer);\n if (!isLongPress && Date.now() - pressStart < 500) { e.preventDefault(); toggleTranslate(); }\n }, { passive: false });\n\n fab.addEventListener(\"click\", () => { if (\"ontouchstart\" in window) return; toggleTranslate(); });\n fab.addEventListener(\"contextmenu\", (e) => { e.preventDefault(); panel.classList.toggle(\"show\"); });\n\n // ====== 切换 ======\n async function toggleTranslate() {\n if (translating) return;\n saveConfig();\n\n if (translating) {\n stopRequested = true;\n showToast(\"⏹️ 正在停止...\", \"info\");\n return;\n }\n if (translated) { restoreOriginal(); return; }\n\n if (cfg.engine === \"baidu\" && (!cfg.baiduAppId || !cfg.baiduKey)) {\n showToast(\"请先配置百度翻译(长按图标设置)\", \"err\"); return;\n }\n if (cfg.engine === \"deepseek\" && !cfg.dsKey) {\n showToast(\"请先配置 DeepSeek Key(长按图标设置)\", \"err\"); return;\n }\n\n await translatePage();\n }\n\n function restoreOriginal() {\n if (!originalTexts) return;\n originalTexts.forEach(({ node, text }) => { node.textContent = text; });\n translated = false; fab.classList.remove(\"translated\"); fab.textContent = \"🌐\";\n showToast(\"已恢复原文\", \"info\");\n }\n\n // ====== 翻译页面 ======\n async function translatePage() {\n translating = true;\n progress.classList.add(\"show\");\n setProgress(0, \"提取文本...\");\n\n try {\n const entries = [];\n const walk = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {\n acceptNode: (n) => {\n const p = n.parentElement;\n if (!p || p.closest(\"script,style,code,pre,#ds-panel,#ds-fab,#ds-toast,svg\"))\n return NodeFilter.FILTER_REJECT;\n const t = n.textContent.trim();\n if (t.length < 10) return NodeFilter.FILTER_REJECT;\n if (p.offsetWidth === 0 || p.offsetHeight === 0) return NodeFilter.FILTER_REJECT;\n return NodeFilter.FILTER_ACCEPT;\n },\n });\n while (walk.nextNode()) entries.push({ node: walk.currentNode, text: walk.currentNode.textContent });\n\n if (entries.length === 0) { showToast(\"没有可翻译的文本\", \"info\"); return; }\n originalTexts = entries;\n\n const nodes = entries.map((e) => e.node);\n const needApi = []; // 需要调用 API 的节点索引\n const cached = []; // 有缓存的节点索引\n\n nodes.forEach((node, i) => {\n const key = node.textContent.trim();\n if (transCache.has(key)) {\n cached.push(i);\n } else {\n needApi.push(i);\n }\n });\n\n // 有缓存的直接填充\n cached.forEach((i) => {\n const key = nodes[i].textContent.trim();\n const t = transCache.get(key);\n nodes[i].textContent = cfg.mode === \"bilingual\" ? key + \"\\n\" + t : t;\n });\n\n // 需要调 API 的\n if (needApi.length > 0) {\n if (cfg.engine === \"baidu\") {\n await translateBaidu(nodes, needApi);\n } else {\n await translateDeepSeek(nodes, needApi);\n }\n }\n\n // 更新缓存计数\n cacheHint.textContent = `🗑️ 清除翻译缓存 (${transCache.size}条)`;\n\n translated = true;\n fab.classList.add(\"translated\"); fab.textContent = \"✅\";\n setProgress(100, \"✅ 翻译完成\");\n const fromCache = cached.length > 0 ? ` (缓存${cached.length}段)` : \"\";\n showToast(`翻译完成${fromCache},再点恢复原文`, \"ok\");\n } catch (err) {\n showToast(\"出错: \" + err.message, \"err\");\n }\n setTimeout(() => { progress.classList.remove(\"show\"); translating = false; }, 1500);\n }\n\n // ====== 百度翻译 ======\n async function translateBaidu(nodes, indices) {\n const sep = \"\\n|||ds-sep|||\\n\";\n const maxBytes = 5000;\n // 按字节长度分批\n const batches = [];\n let cur = [], curBytes = 0;\n for (let ki = 0; ki < indices.length; ki++) {\n const t = nodes[indices[ki]].textContent.trim();\n const bytes = new Blob([t]).size;\n if (cur.length > 0 && curBytes + bytes + 20 > maxBytes) {\n batches.push(cur);\n cur = []; curBytes = 0;\n }\n cur.push({ idx: indices[ki], text: t, bytes });\n curBytes += bytes + 20;\n }\n if (cur.length) batches.push(cur);\n\n // 并发执行所有批次(最多同时3批)\n for (let b = 0; b < batches.length; b += 3) {\n if (stopRequested) { showToast(\"⏹️ 已停止\", \"info\"); break; }\n const chunk = batches.slice(b, b + 3);\n const pct = Math.min(100, Math.round(((b + chunk.length) / batches.length) * 100));\n setProgress(pct, `百度翻译中 ${Math.min((b + chunk.length) * 30, indices.length)}/${indices.length}...`);\n\n await Promise.all(chunk.map(async (batch) => {\n try {\n const joined = batch.map(x => x.text).join(sep);\n const result = await callBaidu(joined);\n if (result) {\n const parts = result.split(\"|||ds-sep|||\").map((s) => s.trim());\n batch.forEach((item, idx) => {\n const t = parts[idx];\n if (t && t !== item.text) {\n transCache.set(item.text, t);\n const node = nodes[item.idx];\n node.textContent = cfg.mode === \"bilingual\" ? item.text + \"\\n\" + t : t;\n }\n });\n }\n } catch (e) {\n console.warn(\"百度批量失败:\", e.message.slice(0, 30));\n // 单条重试\n for (const item of batch) {\n if (stopRequested) break;\n try {\n const r = await callBaidu(item.text);\n if (r) {\n transCache.set(item.text, r);\n nodes[item.idx].textContent = cfg.mode === \"bilingual\" ? item.text + \"\\n\" + r : r;\n }\n } catch {}\n }\n }\n }));\n }\n }\n\n // ====== DeepSeek ======\n async function translateDeepSeek(nodes, indices) {\n const batchSize = 20;\n const sep = \"\\n---\\n\";\n for (let ki = 0; ki < indices.length; ki += batchSize) {\n if (stopRequested) { showToast(\"⏹️ 已停止\", \"info\"); break; }\n const batchIdx = indices.slice(ki, ki + batchSize);\n const texts = batchIdx.map((i) => nodes[i].textContent.trim());\n const pct = Math.min(100, Math.round(((ki + batchIdx.length) / indices.length) * 100));\n setProgress(pct, `DeepSeek 翻译中 ${Math.min(ki + batchIdx.length, indices.length)}/${indices.length}...`);\n\n const result = await callDeepSeek(texts);\n if (result) {\n batchIdx.forEach((i, idx) => {\n const orig = texts[idx];\n if (result[idx]) {\n transCache.set(orig, result[idx]);\n nodes[i].textContent = cfg.mode === \"bilingual\" ? orig + \"\\n\" + result[idx] : result[idx];\n }\n });\n }\n }\n }\n\n // ====== 百度 API ======\n function callBaidu(text) {\n return new Promise((resolve, reject) => {\n const appid = cfg.baiduAppId;\n const key = cfg.baiduKey;\n const salt = Date.now() + Math.random();\n const sign = md5(appid + text + salt + key);\n const url = `https://fanyi-api.baidu.com/api/trans/vip/translate?q=${encodeURIComponent(text)}&from=auto&to=zh&appid=${appid}&salt=${salt}&sign=${sign}`;\n GM_xmlhttpRequest({\n method: \"GET\", url,\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data.trans_result) resolve(data.trans_result.map((r) => r.dst).join(\"\\n\"));\n else if (data.error_code) reject(new Error(`[${data.error_code}] ${data.error_msg}`));\n else reject(new Error(\"返回异常\"));\n } catch { reject(new Error(\"解析失败\")); }\n },\n onerror: () => reject(new Error(\"网络错误\")),\n timeout: 15000,\n });\n });\n }\n\n // ====== DeepSeek API ======\n function callDeepSeek(texts) {\n return new Promise((resolve) => {\n const joined = texts.map((t, i) => `[${i + 1}] ${t}`).join(\"\\n---\\n\");\n const prompt = `逐段翻译为简体中文,保持编号格式,只返回翻译结果:\\n\\n${joined}`;\n GM_xmlhttpRequest({\n method: \"POST\",\n url: \"https://api.deepseek.com/chat/completions\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${cfg.dsKey}` },\n data: JSON.stringify({\n model: cfg.dsModel,\n messages: [{ role: \"system\", content: \"逐段翻译,保持编号。\" }, { role: \"user\", content: prompt }],\n temperature: 0.3, max_tokens: 8192,\n }),\n onload: (res) => {\n try {\n const data = JSON.parse(res.responseText);\n if (data.choices && data.choices[0]) {\n const content = data.choices[0].message.content.trim();\n const results = [];\n for (let i = 0; i < texts.length; i++) {\n const re = new RegExp(`\\\\[${i + 1}\\\\]\\\\s*([\\\\s\\\\S]*?)(?=\\\\[${i + 2}\\\\]|$)`);\n const m = content.match(re);\n results.push(m ? m[1].trim() : \"\");\n }\n resolve(results);\n } else resolve(null);\n } catch { resolve(null); }\n },\n onerror: () => resolve(null),\n timeout: 60000,\n });\n });\n }\n\n // ====== MD5 ======\n function md5(str) {\n function rotateLeft(x, n) { return (x << n) | (x >>> (32 - n)); }\n function toHex(x) {\n var s = \"\";\n for (var i = 0; i < 4; i++) {\n s += (\"0\" + ((x >> (i * 8)) & 0xff).toString(16)).slice(-2);\n }\n return s;\n }\n function addUnsigned(x, y) {\n var lsw = (x & 0xffff) + (y & 0xffff);\n return ((x >>> 16) + (y >>> 16) + (lsw >>> 16)) << 16 | (lsw & 0xffff);\n }\n var i, j, aa, bb, cc, dd;\n var S = [[7, 12, 17, 22], [5, 9, 14, 20], [4, 11, 16, 23], [6, 10, 15, 21]];\n var T = [];\n for (i = 0; i < 64; i++) T[i] = Math.floor(Math.abs(Math.sin(i + 1)) * 0x100000000);\n str = unescape(encodeURIComponent(str));\n var n = str.length, bits = [];\n for (i = 0; i < n; i++) bits.push(str.charCodeAt(i));\n bits.push(0x80);\n var len = bits.length / 4 + 2;\n len = Math.ceil(len / 16) * 16;\n var x = [];\n for (i = 0; i < len; i++) x[i] = 0;\n for (i = 0; i < bits.length; i++) x[i >> 2] |= bits[i] << ((i % 4) * 8);\n x[x.length - 2] = n * 8;\n var a = 0x67452301, b = 0xefcdab89, c = 0x98badcfe, d = 0x10325476;\n for (i = 0; i < x.length; i += 16) {\n var X = x.slice(i, i + 16);\n aa = a; bb = b; cc = c; dd = d;\n for (j = 0; j < 64; j++) {\n var g, f;\n if (j < 16) { g = j; f = (bb & cc) | (~bb & dd); }\n else if (j < 32) { g = (5 * j + 1) % 16; f = (dd & bb) | (~dd & cc); }\n else if (j < 48) { g = (3 * j + 5) % 16; f = bb ^ cc ^ dd; }\n else { g = (7 * j) % 16; f = cc ^ (bb | ~dd); }\n var temp = dd; dd = cc; cc = bb;\n bb = addUnsigned(bb, rotateLeft(addUnsigned(addUnsigned(addUnsigned(aa, f), T[j]), X[g]), S[j >> 4][j & 3]));\n aa = temp;\n }\n a = addUnsigned(a, aa); b = addUnsigned(b, bb);\n c = addUnsigned(c, cc); d = addUnsigned(d, dd);\n }\n return (toHex(a) + toHex(b) + toHex(c) + toHex(d)).toLowerCase();\n }\n\n // ====== 工具 ======\n function setProgress(pct, text) { progressBar.style.width = pct + \"%\"; progressText.textContent = text; }\n function showToast(msg, type) { toast.textContent = msg; toast.className = type; toast.style.display = \"block\"; setTimeout(() => { toast.style.display = \"none\"; }, 3000); }\n function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }\n\n document.addEventListener(\"keydown\", (e) => { if (e.key === \"Escape\") panel.classList.remove(\"show\"); });\n\n const engName = cfg.engine === \"baidu\" ? \"百度\" : \"DeepSeek\";\n const modeName = cfg.mode === \"bilingual\" ? \"双语\" : \"仅译文\";\n console.log(`✅ 网页翻译已加载 | ${engName} | ${modeName} | 点击翻译/恢复,长按配置`);\n})();\n"}}}} |