Files

1 line
200 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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"}}}}