Created
October 3, 2025 11:00
-
-
Save aoaim/9f2673e31238f2a547aac1444939be92 to your computer and use it in GitHub Desktop.
文献标题翻译 | Academic Paper Title Translator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name 文献标题翻译 | Academic Paper Title Translator | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.0.1 | |
| // @description 自动翻译 PubMed、Europe PMC、Semantic Scholar、Google Scholar 等学术网站上的论文标题和摘要为中文 | Auto-translate academic paper titles and abstracts to Chinese | |
| // @description:zh-CN 自动翻译学术网站上的论文标题和摘要为中文,支持多种翻译 API,智能并发控制 | |
| // @description:en Auto-translate academic paper titles and abstracts to Chinese with multiple translation APIs and intelligent concurrency control | |
| // @author Miao & Claude Sonnet 4.5 | |
| // @homepage https://github.com/yourusername/academic-translator | |
| // @supportURL https://github.com/yourusername/academic-translator/issues | |
| // @license MIT | |
| // @match https://pubmed.ncbi.nlm.nih.gov/* | |
| // @match https://europepmc.org/* | |
| // @match https://www.semanticscholar.org/* | |
| // @match https://scholar.google.com/* | |
| // @match https://scholar.google.co.*/* | |
| // @grant GM_xmlhttpRequest | |
| // @connect translate.googleapis.com | |
| // @connect edge.microsoft.com | |
| // @connect api.cognitive.microsofttranslator.com | |
| // @connect api-free.deepl.com | |
| // @connect api.deepl.com | |
| // @connect api.niutrans.com | |
| // @connect * | |
| // @run-at document-idle | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| /** | |
| * ╔══════════════════════════════════════════════════════════════════════════════╗ | |
| * ║ 文献标题翻译 v1.0 ║ | |
| * ║ Academic Paper Title Translator for Tampermonkey ║ | |
| * ║ Build by Miao and Claude Sonnet 4.5 with ♥ ║ | |
| * ╚══════════════════════════════════════════════════════════════════════════════╝ | |
| * | |
| * ╔══════════════════════════════════════════════════════════════════════════════╗ | |
| * ║ 使用说明(新手必读) ║ | |
| * ╚══════════════════════════════════════════════════════════════════════════════╝ | |
| * | |
| * 【快速开始 - 零基础也能用】 | |
| * | |
| * [1] 第一步:安装 Tampermonkey 浏览器插件 | |
| * - Chrome/Edge: 在扩展商店搜索 "Violentmonkey" 并安装 | |
| * - Firefox: 在附加组件商店搜索 "Violentmonkey" 并安装 | |
| * - Safari: 在 App Store 搜索 "Userscripts" 或者 "Tampermonkey" 并安装 | |
| * | |
| * [2] 第二步:安装本脚本 | |
| * - 点击 Violentmonkey 图标 → + Creat a new script → 粘贴代码 → Save & Close | |
| * - 点击 Userscripts 图标 → Open Extension Page → + New JS → 粘贴代码 → Save | |
| * - 或者:新建脚本,复制粘贴本脚本全部内容,按 Ctrl+S 保存 | |
| * | |
| * [3] 第三步:开始使用(无需配置!) | |
| * - 打开 PubMed、Google Scholar 等学术网站 | |
| * - 脚本会自动翻译论文标题为中文 | |
| * - 翻译结果显示在原标题下方,斜体灰色 | |
| * | |
| * 【默认配置 - 开箱即用】 | |
| * | |
| * + 使用微软免费翻译(无需注册,无需 API Key) | |
| * + 自动翻译所有论文标题 | |
| * + 不翻译摘要(避免干扰阅读) | |
| * + 智能跳过中文内容 | |
| * | |
| * 【进阶配置(可选)】 | |
| * | |
| * 如果你想自定义配置,请往下滚动到 "配置区域",修改 CONFIG 对象: | |
| * | |
| * [*] 修改翻译服务商: | |
| * titleProvider: 'microsoft' // 改成 'google' 或 'deepl' 或 'openai' | |
| * | |
| * [*] 启用摘要翻译: | |
| * translateAbstract: true // 改成 true 启用,false 关闭(默认) | |
| * | |
| * [*] 使用大模型翻译(需要 API Key): | |
| * 第 1 步:申请 API Key(任选其一): | |
| * - OpenAI: https://platform.openai.com/api-keys | |
| * - DeepSeek: https://platform.deepseek.com/ (推荐,国内便宜) | |
| * - Gemini: https://makersuite.google.com/app/apikey | |
| * | |
| * 第 2 步:填入配置(找到下面的 openai 配置项): | |
| * openai: { | |
| * apiKey: '这里填入你的 API Key', // 把引号里的内容替换成你的 Key | |
| * model: 'gpt-5-mini', // 根据服务商选择模型 | |
| * baseURL: 'https://api.openai.com/v1' // 根据服务商修改地址 | |
| * } | |
| * | |
| * 第 3 步:修改翻译提供商: | |
| * titleProvider: 'openai' // 改成 'openai' | |
| * | |
| * 【常见问题】 | |
| * | |
| * Q: 为什么没有翻译? | |
| * A: 1) 检查 Tampermonkey 是否启用 | |
| * 2) 检查脚本是否启用(Tampermonkey 图标显示数字) | |
| * 3) 刷新页面重试 | |
| * | |
| * Q: 可以同时翻译多个网站吗? | |
| * A: 可以!脚本自动识别 PubMed、Europe PMC、Semantic Scholar、Google Scholar | |
| * | |
| * Q: 翻译速度慢怎么办? | |
| * A: 1) 免费 API 有速率限制,这是正常的 | |
| * 2) 可以修改 maxConcurrent(并发数)和 requestDelay(延迟) | |
| * 3) 建议:免费 API 使用 maxConcurrent: 1, requestDelay: 500 | |
| * | |
| * Q: 可以翻译成其他语言吗? | |
| * A: 目前只支持翻译成中文,如需其他语言请修改翻译 API 参数 | |
| * | |
| * Q: 如何关闭脚本? | |
| * A: 点击 Tampermonkey 图标,找到本脚本,点击开关即可 | |
| * | |
| * 【详细功能说明】 | |
| * | |
| * 本脚本自动翻译学术网站上的论文标题为中文,支持以下网站和页面: | |
| * | |
| * 【PubMed】 | |
| * 1. 主页 - Trending Articles 论文标题 | |
| * 2. 搜索结果页 - 论文标题列表 | |
| * 3. 文章详情页 - 主标题 | |
| * 4. 文章详情页 - Similar articles 论文标题(动态加载) | |
| * 5. 文章详情页 - Cited by 论文标题(动态加载) | |
| * | |
| * 【Europe PMC】 | |
| * 1. 文章详情页 - 主标题 | |
| * 2. 搜索结果页 - 论文标题列表 | |
| * 3. 文章详情页 - Similar articles 论文标题 | |
| * | |
| * 【Semantic Scholar】 | |
| * 1. 搜索结果页 - 论文标题列表 | |
| * 2. 文章详情页 - 主标题 | |
| * 3. 文章详情页 - Citations 论文标题 | |
| * 4. 文章详情页 - References 论文标题 | |
| * 5. 个人主页 - 论文标题列表 | |
| * | |
| * 【Google Scholar】 | |
| * 1. 搜索结果页 - 论文标题列表 | |
| * | |
| * 【摘要翻译】可选功能 | |
| * - PubMed、Europe PMC 文章详情页的摘要 | |
| * - 默认关闭,需要在 CONFIG 中设置 translateAbstract: true 启用 | |
| * - 翻译结果显示在摘要下方,带有独立的样式区块 | |
| * - 可单独配置摘要翻译提供商(推荐使用大模型 API) | |
| * | |
| * 【翻译样式】 | |
| * - 标题翻译:斜体显示(font-style: italic)、透明度 60%(opacity: 0.6) | |
| * - 摘要翻译:独立区块显示,带有浅色背景和左侧边框 | |
| * - 继承父元素颜色 | |
| * - 位于原文下方,自动换行 | |
| * | |
| * 【智能特性】 | |
| * - 自动检测语言,跳过中文内容 | |
| * - 防止重复翻译 | |
| * - 支持动态加载内容(MutationObserver) | |
| * - 多层防护机制确保翻译质量 | |
| */ | |
| // ╔══════════════════════════════════════════════════════════════════════════════╗ | |
| // ║ 配置区域(从这里开始修改) ║ | |
| // ╚══════════════════════════════════════════════════════════════════════════════╝ | |
| // | |
| // [!] 提示:如果你不懂代码,只需要: | |
| // 1. 保持默认设置,无需修改任何内容 | |
| // 2. 如果想启用摘要翻译,把 translateAbstract 改成 true | |
| // 3. 如果想用大模型,填写 apiKey 并修改 titleProvider | |
| // | |
| const CONFIG = { | |
| // ========== 翻译提供商设置 ========== | |
| // 【标题翻译提供商】可选值: | |
| // 免费:'google', 'microsoft' | |
| // 付费:'deepl', 'niutrans' | |
| // 大模型:'openai', 'deepseek', 'gemini', 'siliconflow', 'groq' | |
| // 自定义:'openai-compatible' | |
| // | |
| // > 新手推荐:'microsoft'(微软免费翻译,无需配置) | |
| titleProvider: 'microsoft', | |
| // 【摘要翻译开关】是否翻译论文摘要 | |
| // [说明] | |
| // - false:只翻译标题(推荐,不影响阅读) | |
| // - true:同时翻译标题和摘要(翻译较慢,消耗 API 额度) | |
| translateAbstract: false, | |
| // 【摘要翻译提供商】可选值同上 | |
| // | |
| // > 留空 '' 表示和标题使用相同的提供商 | |
| // > 如果想让摘要用大模型,可以改成:'deepseek' 等(记得填 API Key) | |
| abstractProvider: '', // 留空则使用 titleProvider | |
| // ========== 免费翻译 API 配置 ========== | |
| // 【Google 翻译】免费,无需任何配置 | |
| // [使用方法] 把上面的 titleProvider 改成 'google' | |
| google: { | |
| // + 完全免费,无需注册 | |
| // + 翻译质量:**** | |
| // + 速度:快 | |
| }, | |
| // 【Microsoft 翻译】免费,无需任何配置(默认) | |
| // [使用方法] 默认已启用,无需修改 | |
| microsoft: { | |
| // + 完全免费,无需注册 | |
| // + 翻译质量:**** | |
| // + 速度:快 | |
| // + 稳定性:高 | |
| }, | |
| // 【DeepL 翻译】需要注册,需要 API Key | |
| // [使用方法] | |
| // 第 1 步:访问 https://www.deepl.com/pro-api 注册账号 | |
| // 第 2 步:复制你的 API Key | |
| // 第 3 步:把 Key 粘贴到下面 apiKey 的引号里 | |
| // 第 4 步:把上面的 titleProvider 改成 'deepl' | |
| deepl: { | |
| apiKey: '', // <-- 把你的 DeepL API Key 粘贴到这里(替换两个引号之间的内容) | |
| useFreeApi: true, // true = 免费版(每月 50 万字符),false = 付费版 | |
| // + 翻译质量:*****(最高) | |
| // + 专业术语准确 | |
| }, | |
| // 【小牛翻译】需要注册,需要 API Key | |
| // [使用方法] | |
| // 第 1 步:访问 https://niutrans.com/documents/contents/trans_text 注册账号 | |
| // 第 2 步:复制你的 API Key | |
| // 第 3 步:把 Key 粘贴到下面 apiKey 的引号里 | |
| // 第 4 步:把上面的 titleProvider 改成 'niutrans' | |
| niutrans: { | |
| apiKey: '', // <-- 把你的小牛翻译 API Key 粘贴到这里 | |
| // + 每日 100 积分 | |
| // + 文本翻译:1 积分/2000 字符 | |
| }, | |
| // ========== 大模型翻译 API 配置 ========== | |
| // 【OpenAI】官方服务 | |
| // 注册地址:https://platform.openai.com/api-keys | |
| openai: { | |
| apiKey: '', | |
| model: 'gpt-4.5-mini', | |
| temperature: 0.3, | |
| }, | |
| // 【DeepSeek】国内推荐,便宜好用 | |
| // 注册地址:https://platform.deepseek.com/ | |
| deepseek: { | |
| apiKey: '', | |
| model: 'deepseek-chat', | |
| temperature: 0.3, | |
| }, | |
| // 【Google Gemini】速度快,免费额度大 | |
| // 注册地址:https://makersuite.google.com/app/apikey | |
| gemini: { | |
| apiKey: '', | |
| model: 'gemini-2.5-flash', | |
| temperature: 0.1, | |
| }, | |
| // 【硅基流动】支持多种开源模型 | |
| // 注册地址:https://siliconflow.cn/ | |
| siliconflow: { | |
| apiKey: '', | |
| model: 'deepseek-ai/DeepSeek-V3', | |
| temperature: 0.3, | |
| }, | |
| // 【Groq】超快推理速度 | |
| // 注册地址:https://console.groq.com/ | |
| groq: { | |
| apiKey: '', | |
| model: 'openai/gpt-oss-120b', | |
| temperature: 0.3, | |
| }, | |
| // 【OpenAI 兼容接口】自定义配置 | |
| // 适用于任何支持 OpenAI API 格式的服务 | |
| 'openai-compatible': { | |
| apiKey: '', | |
| model: '', | |
| baseURL: '', // 例如:https://api.example.com/v1 | |
| temperature: 0.3, | |
| }, | |
| // ========== 并发和速率控制 ========== | |
| // 【并发数量】同时进行的翻译请求数 | |
| // [说明] 数字越大,翻译越快,但可能被限速 | |
| // | |
| // > 新手推荐设置: | |
| // - 如果用免费 API(Google/Microsoft):设为 1 | |
| // - 如果用大模型 API:设为 3-5 | |
| maxConcurrent: 1, | |
| // 【请求延迟】翻译请求之间的等待时间(毫秒) | |
| // [说明] 数字越大,翻译越慢,但不容易被限速 | |
| // | |
| // > 新手推荐设置: | |
| // - 如果用免费 API:设为 500(半秒) | |
| // - 如果用大模型 API:设为 200(0.2 秒) | |
| requestDelay: 500, | |
| // ========== 翻译设置 ========== | |
| targetLanguage: 'zh-CN', // 目标语言(建议不要改) | |
| translationStyle: '简体中文', // 翻译风格(建议不要改) | |
| }; | |
| // ╔══════════════════════════════════════════════════════════════════════════════╗ | |
| // ║ 以下不要修改,除非你明确知道你在做什么! ║ | |
| // ╚══════════════════════════════════════════════════════════════════════════════╝ | |
| // ╔══════════════════════════════════════════════════════════════════════════════╗ | |
| // ║ 工具函数 | Utilities ║ | |
| // ╚══════════════════════════════════════════════════════════════════════════════╝ | |
| /** | |
| * 翻译队列管理器 | |
| * 实现并发控制和延迟控制,避免触发 API 速率限制 | |
| * | |
| * @class TranslationQueue | |
| * @description 管理翻译请求的队列,控制并发数量和请求间隔 | |
| */ | |
| class TranslationQueue { | |
| constructor(maxConcurrent, delay) { | |
| this.maxConcurrent = maxConcurrent; | |
| this.delay = delay; | |
| this.queue = []; | |
| this.activeCount = 0; | |
| } | |
| async add(translateFunc) { | |
| return new Promise((resolve, reject) => { | |
| this.queue.push({ translateFunc, resolve, reject }); | |
| this.process(); | |
| }); | |
| } | |
| async process() { | |
| if (this.activeCount >= this.maxConcurrent || this.queue.length === 0) { | |
| return; | |
| } | |
| const { translateFunc, resolve, reject } = this.queue.shift(); | |
| this.activeCount++; | |
| try { | |
| const result = await translateFunc(); | |
| resolve(result); | |
| } catch (error) { | |
| reject(error); | |
| } finally { | |
| this.activeCount--; | |
| // 添加延迟后处理下一个任务 | |
| if (this.queue.length > 0) { | |
| setTimeout(() => this.process(), this.delay); | |
| } | |
| } | |
| } | |
| } | |
| // 创建翻译队列实例 | |
| const translationQueue = new TranslationQueue(CONFIG.maxConcurrent, CONFIG.requestDelay); | |
| // 失败计数器 | |
| const failureCount = {}; | |
| const MAX_FAILURES = 3; | |
| let scriptStopped = false; | |
| /** | |
| * 检测文本是否主要是中文 | |
| */ | |
| function isChinese(text) { | |
| const chineseRegex = /[\u4e00-\u9fa5]/g; | |
| const chineseChars = text.match(chineseRegex); | |
| if (!chineseChars) return false; | |
| // 如果中文字符占比超过 30%,认为是中文文本 | |
| const chineseRatio = chineseChars.length / text.length; | |
| return chineseRatio > 0.3; | |
| } | |
| /** | |
| * 调用 Google 翻译(免费) | |
| */ | |
| function translateWithGoogle(text) { | |
| return new Promise((resolve, reject) => { | |
| const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=zh-CN&dt=t&q=${encodeURIComponent(text)}`; | |
| GM_xmlhttpRequest({ | |
| method: 'GET', | |
| url: url, | |
| headers: { | |
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' | |
| }, | |
| onload: function(response) { | |
| try { | |
| if (response.status !== 200) { | |
| reject(`Google 翻译请求失败:HTTP ${response.status}`); | |
| return; | |
| } | |
| const result = JSON.parse(response.responseText); | |
| if (result && result[0]) { | |
| let translation = ''; | |
| result[0].forEach(item => { | |
| if (item[0]) { | |
| translation += item[0]; | |
| } | |
| }); | |
| resolve(translation.trim()); | |
| } else { | |
| reject('Google 翻译返回格式错误'); | |
| } | |
| } catch (e) { | |
| reject(`Google 翻译解析失败:${e.message}`); | |
| } | |
| }, | |
| onerror: function(error) { | |
| reject('Google 翻译网络请求失败:' + error); | |
| } | |
| }); | |
| }); | |
| } | |
| /** | |
| * 调用 Microsoft 翻译(免费) | |
| * 使用 Microsoft Edge 翻译接口 | |
| */ | |
| function translateWithMicrosoft(text) { | |
| return new Promise((resolve, reject) => { | |
| // 第一步:获取授权令牌 | |
| GM_xmlhttpRequest({ | |
| method: 'GET', | |
| url: 'https://edge.microsoft.com/translate/auth', | |
| onload: function(authResponse) { | |
| try { | |
| if (authResponse.status !== 200) { | |
| reject(`Microsoft 授权失败:HTTP ${authResponse.status}`); | |
| return; | |
| } | |
| const authToken = authResponse.responseText; | |
| // 第二步:使用授权令牌进行翻译 | |
| GM_xmlhttpRequest({ | |
| method: 'POST', | |
| url: 'https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=zh-Hans&from=en', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${authToken}` | |
| }, | |
| data: JSON.stringify([{ text: text }]), | |
| onload: function(translateResponse) { | |
| try { | |
| if (translateResponse.status !== 200) { | |
| reject(`Microsoft 翻译请求失败:HTTP ${translateResponse.status}`); | |
| return; | |
| } | |
| const result = JSON.parse(translateResponse.responseText); | |
| if (result && result[0] && result[0].translations && result[0].translations[0]) { | |
| const translation = result[0].translations[0].text.trim(); | |
| resolve(translation); | |
| } else { | |
| reject('Microsoft 翻译返回格式错误'); | |
| } | |
| } catch (e) { | |
| reject(`Microsoft 翻译解析失败:${e.message}`); | |
| } | |
| }, | |
| onerror: function(error) { | |
| reject('Microsoft 翻译请求失败:' + error); | |
| } | |
| }); | |
| } catch (e) { | |
| reject(`Microsoft 授权解析失败:${e.message}`); | |
| } | |
| }, | |
| onerror: function(error) { | |
| reject('Microsoft 授权请求失败:' + error); | |
| } | |
| }); | |
| }); | |
| } | |
| /** | |
| * 调用 DeepL 翻译(需要 API Key) | |
| */ | |
| function translateWithDeepL(text) { | |
| return new Promise((resolve, reject) => { | |
| if (!CONFIG.deepl.apiKey) { | |
| reject('DeepL API Key 未配置'); | |
| return; | |
| } | |
| const baseUrl = CONFIG.deepl.useFreeApi | |
| ? 'https://api-free.deepl.com/v2/translate' | |
| : 'https://api.deepl.com/v2/translate'; | |
| const params = new URLSearchParams({ | |
| text: text, | |
| target_lang: 'ZH', | |
| auth_key: CONFIG.deepl.apiKey | |
| }); | |
| GM_xmlhttpRequest({ | |
| method: 'POST', | |
| url: baseUrl, | |
| headers: { | |
| 'Content-Type': 'application/x-www-form-urlencoded' | |
| }, | |
| data: params.toString(), | |
| onload: function(response) { | |
| try { | |
| const result = JSON.parse(response.responseText); | |
| if (result.translations && result.translations[0]) { | |
| resolve(result.translations[0].text); | |
| } else { | |
| reject('DeepL 翻译响应格式错误'); | |
| } | |
| } catch (e) { | |
| reject('DeepL 翻译解析失败:' + e.message); | |
| } | |
| }, | |
| onerror: function(error) { | |
| reject('DeepL 翻译网络请求失败:' + error); | |
| } | |
| }); | |
| }); | |
| } | |
| /** | |
| * 调用小牛翻译(需要 API Key) | |
| */ | |
| function translateWithNiutrans(text) { | |
| return new Promise((resolve, reject) => { | |
| if (!CONFIG.niutrans.apiKey) { | |
| reject('小牛翻译 API Key 未配置'); | |
| return; | |
| } | |
| GM_xmlhttpRequest({ | |
| method: 'POST', | |
| url: 'https://api.niutrans.com/NiuTransServer/translation', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| data: JSON.stringify({ | |
| src_text: text, | |
| from: 'en', | |
| to: 'zh', | |
| apikey: CONFIG.niutrans.apiKey | |
| }), | |
| onload: function(response) { | |
| try { | |
| if (response.status !== 200) { | |
| reject(`小牛翻译请求失败:HTTP ${response.status}`); | |
| return; | |
| } | |
| const result = JSON.parse(response.responseText); | |
| if (result.tgt_text) { | |
| resolve(result.tgt_text.trim()); | |
| } else if (result.error_msg) { | |
| reject(`小牛翻译错误:${result.error_msg}`); | |
| } else { | |
| reject('小牛翻译返回格式错误'); | |
| } | |
| } catch (e) { | |
| reject(`小牛翻译解析失败:${e.message}`); | |
| } | |
| }, | |
| onerror: function(error) { | |
| reject('小牛翻译网络请求失败:' + error); | |
| } | |
| }); | |
| }); | |
| } | |
| /** | |
| * 调用 Gemini 翻译(使用 OpenAI 兼容格式) | |
| */ | |
| function translateWithGemini(text) { | |
| return new Promise((resolve, reject) => { | |
| const config = CONFIG.gemini; | |
| if (!config || !config.apiKey) { | |
| reject('Gemini API Key 未配置'); | |
| return; | |
| } | |
| const messages = [ | |
| { | |
| role: 'system', | |
| content: '你是一个专业的学术论文翻译助手。你的任务是将英文翻译成简体中文。重要规则:只输出翻译结果,不要输出任何解释、说明、注释或其他内容。' | |
| }, | |
| { | |
| role: 'user', | |
| content: text | |
| } | |
| ]; | |
| GM_xmlhttpRequest({ | |
| method: 'POST', | |
| url: `https://generativelanguage.googleapis.com/v1beta/models/${config.model}:generateContent?key=${config.apiKey}`, | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| data: JSON.stringify({ | |
| contents: [{ | |
| role: 'user', | |
| parts: [{ | |
| text: `你是一个专业的学术论文翻译助手。请将以下英文翻译成简体中文。 | |
| 重要要求: | |
| - 只输出中文翻译 | |
| - 不要输出英文原文 | |
| - 不要添加任何解释、说明或其他内容 | |
| - 保持学术术语的准确性 | |
| 英文原文: | |
| ${text} | |
| 中文翻译:` | |
| }] | |
| }], | |
| generationConfig: { | |
| temperature: config.temperature || 0.1, | |
| maxOutputTokens: 4096, | |
| } | |
| }), | |
| onload: function(response) { | |
| try { | |
| if (response.status !== 200) { | |
| reject(`Gemini 翻译请求失败:HTTP ${response.status}`); | |
| return; | |
| } | |
| const result = JSON.parse(response.responseText); | |
| if (result.candidates && result.candidates[0] && | |
| result.candidates[0].content && result.candidates[0].content.parts && | |
| result.candidates[0].content.parts[0]) { | |
| const translation = result.candidates[0].content.parts[0].text.trim(); | |
| resolve(translation); | |
| } else { | |
| console.error('[Gemini 错误] 响应数据:', result); | |
| reject('Gemini 翻译返回格式错误'); | |
| } | |
| } catch (e) { | |
| reject(`Gemini 翻译解析失败:${e.message}`); | |
| } | |
| }, | |
| onerror: function(error) { | |
| reject('Gemini 翻译网络请求失败:' + error); | |
| } | |
| }); | |
| }); | |
| } | |
| /** | |
| * 调用大模型翻译(通用函数) | |
| * | |
| * @description 支持所有 OpenAI Chat Completions API 格式的服务 | |
| * @param {string} text - 要翻译的文本 | |
| * @param {string} provider - 服务商名称 | |
| * @returns {Promise<string>} 翻译结果 | |
| */ | |
| function translateWithLLM(text, provider) { | |
| return new Promise((resolve, reject) => { | |
| const config = CONFIG[provider]; | |
| if (!config || !config.apiKey) { | |
| reject(`${provider} API Key 未配置`); | |
| return; | |
| } | |
| // 服务商对应的 baseURL | |
| const baseURLMap = { | |
| 'openai': 'https://api.openai.com/v1', | |
| 'deepseek': 'https://api.deepseek.com', | |
| 'siliconflow': 'https://api.siliconflow.cn/v1', | |
| 'groq': 'https://api.groq.com/openai/v1', | |
| 'openai-compatible': config.baseURL || '' | |
| }; | |
| const baseURL = baseURLMap[provider]; | |
| if (!baseURL) { | |
| reject(`未知的服务商:${provider}`); | |
| return; | |
| } | |
| const messages = [ | |
| { | |
| role: 'system', | |
| content: '你是一个专业的学术论文翻译助手。你的任务是将英文翻译成简体中文。重要规则:只输出翻译结果,不要输出任何解释、说明、注释或其他内容。' | |
| }, | |
| { | |
| role: 'user', | |
| content: `请将以下英文翻译成简体中文,只输出翻译结果:\n\n${text}` | |
| } | |
| ]; | |
| GM_xmlhttpRequest({ | |
| method: 'POST', | |
| url: `${baseURL}/chat/completions`, | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${config.apiKey}` | |
| }, | |
| data: JSON.stringify({ | |
| model: config.model, | |
| messages: messages, | |
| temperature: config.temperature || 0.3 | |
| }), | |
| onload: function(response) { | |
| try { | |
| if (response.status !== 200) { | |
| reject(`${provider} 翻译请求失败:HTTP ${response.status}`); | |
| return; | |
| } | |
| const result = JSON.parse(response.responseText); | |
| if (result.choices && result.choices[0] && result.choices[0].message) { | |
| const translation = result.choices[0].message.content.trim(); | |
| resolve(translation); | |
| } else { | |
| reject(`${provider} 翻译返回格式错误`); | |
| } | |
| } catch (e) { | |
| reject(`${provider} 翻译解析失败:${e.message}`); | |
| } | |
| }, | |
| onerror: function(error) { | |
| reject(`${provider} 翻译网络请求失败:` + error); | |
| } | |
| }); | |
| }); | |
| } | |
| /** | |
| * 根据配置选择翻译 API | |
| * @param {string} text - 要翻译的文本 | |
| * @param {string} type - 翻译类型:'title' 或 'abstract' | |
| */ | |
| function translate(text, type = 'title') { | |
| // 检查脚本是否已停止 | |
| if (scriptStopped) { | |
| return Promise.reject('脚本已因连续失败而停止'); | |
| } | |
| // 根据类型选择提供商 | |
| let provider; | |
| if (type === 'abstract' && CONFIG.abstractProvider) { | |
| provider = CONFIG.abstractProvider; | |
| } else { | |
| provider = CONFIG.titleProvider; | |
| } | |
| // 控制台日志:开始翻译 | |
| const typeText = type === 'abstract' ? '摘要' : '标题'; | |
| const preview = text.length > 50 ? text.substring(0, 50) + '...' : text; | |
| console.log(`%c[翻译${typeText}] %c使用 ${provider} 翻译:%c${preview}`, | |
| 'color: #4CAF50; font-weight: bold', | |
| 'color: #2196F3', | |
| 'color: #666'); | |
| // 选择对应的翻译函数 | |
| let translateFunc; | |
| // 通用大模型服务商列表(使用 OpenAI 兼容格式) | |
| const llmProviders = ['openai', 'deepseek', 'siliconflow', 'groq', 'openai-compatible']; | |
| if (llmProviders.includes(provider)) { | |
| // 使用通用大模型翻译函数 | |
| translateFunc = () => translateWithLLM(text, provider); | |
| } else { | |
| // 使用专用翻译函数 | |
| switch(provider) { | |
| case 'google': | |
| translateFunc = () => translateWithGoogle(text); | |
| break; | |
| case 'microsoft': | |
| translateFunc = () => translateWithMicrosoft(text); | |
| break; | |
| case 'deepl': | |
| translateFunc = () => translateWithDeepL(text); | |
| break; | |
| case 'niutrans': | |
| translateFunc = () => translateWithNiutrans(text); | |
| break; | |
| case 'gemini': | |
| translateFunc = () => translateWithGemini(text); | |
| break; | |
| default: | |
| console.error(`%c[翻译错误] 未知的 API 提供商:${provider}`, 'color: #f44336; font-weight: bold'); | |
| return Promise.reject(`未知的 API 提供商:${provider}`); | |
| } | |
| } | |
| // 通过队列管理器执行翻译,并记录结果 | |
| return translationQueue.add(translateFunc) | |
| .then(result => { | |
| // 翻译成功,重置该提供商的失败计数 | |
| failureCount[provider] = 0; | |
| console.log(`%c[翻译成功] %c${result}`, | |
| 'color: #4CAF50; font-weight: bold', | |
| 'color: #333'); | |
| return result; | |
| }) | |
| .catch(error => { | |
| // 增加失败计数 | |
| failureCount[provider] = (failureCount[provider] || 0) + 1; | |
| console.error(`%c[翻译失败] %c${provider} 翻译失败(${failureCount[provider]}/${MAX_FAILURES}):%c${error}`, | |
| 'color: #f44336; font-weight: bold', | |
| 'color: #2196F3', | |
| 'color: #f44336'); | |
| // 检查是否达到最大失败次数 | |
| if (failureCount[provider] >= MAX_FAILURES) { | |
| scriptStopped = true; | |
| console.error(`%c[严重错误] %c${provider} 连续失败 ${MAX_FAILURES} 次,脚本已停止运行`, | |
| 'color: #f44336; font-weight: bold; font-size: 14px', | |
| 'color: #f44336; font-size: 14px'); | |
| alert(`翻译服务 ${provider} 连续失败 ${MAX_FAILURES} 次,脚本已停止运行。\n\n请检查:\n1. API Key 是否正确\n2. 网络连接是否正常\n3. API 额度是否充足\n\n刷新页面后脚本将重新启动。`); | |
| } | |
| throw error; | |
| }); | |
| } | |
| /** | |
| * 为摘要元素添加翻译 | |
| * 摘要翻译与标题翻译的区别: | |
| * 1. 翻译文本较长,需要分段处理 | |
| * 2. 翻译结果显示在摘要下方,作为独立的翻译区块 | |
| * 3. 需要保留原文格式(段落、斜体等) | |
| */ | |
| async function addAbstractTranslation(abstractElement, getTextFunc) { | |
| // 检查配置开关 | |
| if (!CONFIG.translateAbstract) { | |
| return; | |
| } | |
| // 检查脚本是否已停止 | |
| if (scriptStopped) { | |
| return; | |
| } | |
| // 检查是否已翻译或正在翻译 | |
| if (abstractElement.dataset.translated === 'true' || abstractElement.dataset.translating === 'true') { | |
| return; | |
| } | |
| // 检查是否已存在翻译 | |
| if (abstractElement.querySelector('.abstract-translation-container') || | |
| abstractElement.parentElement.querySelector('.abstract-translation-container')) { | |
| abstractElement.dataset.translated = 'true'; | |
| return; | |
| } | |
| const originalText = getTextFunc(abstractElement); | |
| if (!originalText || originalText.trim().length === 0) { | |
| return; | |
| } | |
| // 检测是否是中文 | |
| if (isChinese(originalText)) { | |
| abstractElement.dataset.translated = 'true'; | |
| return; | |
| } | |
| // 创建加载动画(使用与翻译结果相同的样式) | |
| const loadingDiv = document.createElement('div'); | |
| loadingDiv.className = 'abstract-translation-loading'; | |
| loadingDiv.style.cssText = 'margin-top: 16px; padding: 12px; background-color: rgba(0, 0, 0, 0.02); border-left: 3px solid rgba(0, 0, 0, 0.1); border-radius: 4px; line-height: 1.6; opacity: 0.8;'; | |
| loadingDiv.textContent = '正在翻译摘要...'; | |
| // 添加脉冲动画 | |
| const style = document.createElement('style'); | |
| if (!document.getElementById('translation-loading-style')) { | |
| style.id = 'translation-loading-style'; | |
| style.textContent = ` | |
| @keyframes translation-pulse { | |
| 0%, 100% { opacity: 0.4; } | |
| 50% { opacity: 0.8; } | |
| } | |
| .translation-loading, .abstract-translation-loading { | |
| animation: translation-pulse 1.5s ease-in-out infinite; | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| } | |
| // 插入加载动画 | |
| abstractElement.parentElement.insertBefore(loadingDiv, abstractElement.nextSibling); | |
| try { | |
| // 标记为正在翻译 | |
| abstractElement.dataset.translating = 'true'; | |
| // 调用翻译 API(使用摘要专用提供商) | |
| const translation = await translate(originalText, 'abstract'); | |
| // 移除加载动画 | |
| if (loadingDiv && loadingDiv.parentElement) { | |
| loadingDiv.remove(); | |
| } | |
| // 再次检查是否已存在翻译 | |
| if (abstractElement.querySelector('.abstract-translation-container') || | |
| abstractElement.parentElement.querySelector('.abstract-translation-container')) { | |
| abstractElement.dataset.translated = 'true'; | |
| abstractElement.dataset.translating = 'false'; | |
| return; | |
| } | |
| // 处理段落:智能分段(支持多种换行格式) | |
| // 尝试按双换行分段,如果没有则按单换行分段,如果还是只有一段则按句号分段 | |
| let paragraphs = translation.split(/\n\n+/).map(p => p.trim()).filter(p => p.length > 0); | |
| // 如果只有一个段落,尝试按单换行分 | |
| if (paragraphs.length === 1) { | |
| const singleNewlineSplit = translation.split(/\n/).map(p => p.trim()).filter(p => p.length > 0); | |
| if (singleNewlineSplit.length > 1) { | |
| paragraphs = singleNewlineSplit; | |
| } | |
| } | |
| // 创建折叠容器 | |
| const collapseContainer = document.createElement('div'); | |
| collapseContainer.className = 'abstract-translation-container'; | |
| collapseContainer.style.cssText = 'margin-top: 12px;'; | |
| // 创建折叠按钮 | |
| const toggleButton = document.createElement('button'); | |
| toggleButton.className = 'abstract-translation-toggle'; | |
| toggleButton.style.cssText = 'background: none; border: none; color: #0071bc; cursor: pointer; padding: 4px 0; font-size: 14px; text-decoration: underline; opacity: 0.8;'; | |
| toggleButton.textContent = '▶ 显示中文翻译'; | |
| // 创建翻译内容容器 | |
| const translationDiv = document.createElement('div'); | |
| translationDiv.className = 'abstract-translation-zh'; | |
| translationDiv.style.cssText = 'display: none; margin-top: 4px; padding: 8px 12px; background-color: rgba(0, 0, 0, 0.02); border-left: 2px solid rgba(0, 0, 0, 0.15); opacity: 0.85;'; | |
| // 如果只有一个段落,直接显示文本 | |
| if (paragraphs.length === 1) { | |
| translationDiv.textContent = paragraphs[0]; | |
| } else { | |
| // 多个段落时,用 <p> 标签包裹每个段落 | |
| paragraphs.forEach(para => { | |
| const p = document.createElement('p'); | |
| p.style.cssText = 'margin: 0 0 0.75em 0;'; | |
| p.textContent = para; | |
| translationDiv.appendChild(p); | |
| }); | |
| // 移除最后一个段落的底部边距 | |
| const lastP = translationDiv.lastChild; | |
| if (lastP) lastP.style.marginBottom = '0'; | |
| } | |
| // 添加点击事件 | |
| toggleButton.addEventListener('click', function() { | |
| const isVisible = translationDiv.style.display !== 'none'; | |
| if (isVisible) { | |
| translationDiv.style.display = 'none'; | |
| toggleButton.textContent = '▶ 显示中文翻译'; | |
| } else { | |
| translationDiv.style.display = 'block'; | |
| toggleButton.textContent = '▼ 收起中文翻译'; | |
| } | |
| }); | |
| // 组装元素 | |
| collapseContainer.appendChild(toggleButton); | |
| collapseContainer.appendChild(translationDiv); | |
| // 插入到摘要后面 | |
| abstractElement.parentElement.insertBefore(collapseContainer, abstractElement.nextSibling); | |
| // 标记为已翻译 | |
| abstractElement.dataset.translated = 'true'; | |
| abstractElement.dataset.translating = 'false'; | |
| } catch (error) { | |
| // 移除加载动画 | |
| if (loadingDiv && loadingDiv.parentElement) { | |
| loadingDiv.remove(); | |
| } | |
| console.error(`%c[摘要翻译失败] %c${error}`, | |
| 'color: #f44336; font-weight: bold', | |
| 'color: #f44336'); | |
| abstractElement.dataset.translating = 'false'; | |
| } | |
| } | |
| /** | |
| * 为标题元素添加翻译 | |
| */ | |
| async function addTranslation(titleElement, getTextFunc, createTranslationFunc) { | |
| // 检查脚本是否已停止 | |
| if (scriptStopped) { | |
| return; | |
| } | |
| // 检查是否已翻译或正在翻译 | |
| if (titleElement.dataset.translated === 'true' || titleElement.dataset.translating === 'true') { | |
| return; | |
| } | |
| // 检查是否已经存在翻译元素(防止重复翻译) | |
| // 检查标题元素内部和父元素 | |
| const existingTranslation = titleElement.querySelector('.translation-zh') || | |
| titleElement.parentElement?.querySelector('.translation-zh'); | |
| if (existingTranslation) { | |
| titleElement.dataset.translated = 'true'; | |
| return; | |
| } | |
| const originalText = getTextFunc(titleElement); | |
| if (!originalText || originalText.trim().length === 0) { | |
| return; | |
| } | |
| // 检测是否是中文,如果是则不翻译 | |
| if (isChinese(originalText)) { | |
| titleElement.dataset.translated = 'true'; | |
| return; | |
| } | |
| // 创建加载动画(使用与翻译结果相同的样式) | |
| const loadingElement = document.createElement('span'); | |
| loadingElement.className = 'translation-loading'; | |
| loadingElement.style.cssText = 'display: block; margin-top: 4px; line-height: 1.4; font-style: italic; opacity: 0.6; font-weight: normal;'; | |
| loadingElement.textContent = '翻译中...'; | |
| // 添加脉冲动画 | |
| const style = document.createElement('style'); | |
| if (!document.getElementById('translation-loading-style')) { | |
| style.id = 'translation-loading-style'; | |
| style.textContent = ` | |
| @keyframes translation-pulse { | |
| 0%, 100% { opacity: 0.4; } | |
| 50% { opacity: 0.8; } | |
| } | |
| .translation-loading, .abstract-translation-loading { | |
| animation: translation-pulse 1.5s ease-in-out infinite; | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| } | |
| // 将加载动画插入到标题内部(与翻译结果位置一致) | |
| titleElement.appendChild(loadingElement); | |
| try { | |
| // 标记为正在翻译 | |
| titleElement.dataset.translating = 'true'; | |
| // 调用翻译 API(使用标题提供商) | |
| const translation = await translate(originalText, 'title'); | |
| // 移除加载动画 | |
| if (loadingElement && loadingElement.parentElement) { | |
| loadingElement.remove(); | |
| } | |
| // 再次检查是否已存在翻译(防止并发问题) | |
| const checkExisting = titleElement.querySelector('.translation-zh') || | |
| titleElement.parentElement?.querySelector('.translation-zh'); | |
| if (checkExisting) { | |
| titleElement.dataset.translated = 'true'; | |
| titleElement.dataset.translating = 'false'; | |
| return; | |
| } | |
| // 创建并插入翻译元素 | |
| createTranslationFunc(titleElement, translation); | |
| // 标记为已翻译 | |
| titleElement.dataset.translated = 'true'; | |
| titleElement.dataset.translating = 'false'; | |
| } catch (error) { | |
| // 移除加载动画 | |
| if (loadingElement && loadingElement.parentElement) { | |
| loadingElement.remove(); | |
| } | |
| console.error(`%c[标题翻译失败] %c${error}`, | |
| 'color: #f44336; font-weight: bold', | |
| 'color: #f44336'); | |
| titleElement.dataset.translating = 'false'; | |
| // 可选:显示错误提示 | |
| // createTranslationFunc(titleElement, `[翻译失败:${error}]`); | |
| } | |
| } | |
| // ╔══════════════════════════════════════════════════════════════════════════════╗ | |
| // ║ 网站特定处理 | Site Processors ║ | |
| // ╚══════════════════════════════════════════════════════════════════════════════╝ | |
| /** | |
| * PubMed 处理器 | |
| * | |
| * @description 处理 PubMed 网站的论文标题和摘要翻译 | |
| * @website https://pubmed.ncbi.nlm.nih.gov/ | |
| * @supports | |
| * - 主页 (Home): Trending Articles | |
| * - 搜索结果页 (Search): 论文标题列表 | |
| * - 文章详情页 (Detail): 主标题、Similar articles、Cited by、摘要 | |
| */ | |
| async function processPubMed() { | |
| // 收集所有需要翻译的标题元素 | |
| const titleElements = []; | |
| // 1. 主页 - Trending Articles 区域的论文标题 | |
| const trendingArticles = document.querySelectorAll('.homepage-trending-and-latest .full-docsum a[href^="/"]'); | |
| trendingArticles.forEach(titleElement => { | |
| if (!titleElement.querySelector('.docsum-authors') && !titleElement.classList.contains('see-more')) { | |
| titleElements.push({ | |
| element: titleElement, | |
| getText: (el) => { | |
| const clone = el.cloneNode(true); | |
| const existingTranslation = clone.querySelector('.translation-zh'); | |
| if (existingTranslation) existingTranslation.remove(); | |
| return clone.textContent.trim(); | |
| }, | |
| createTranslation: (el, translation) => { | |
| if (el.querySelector('.translation-zh')) return; | |
| const translationSpan = document.createElement('span'); | |
| translationSpan.className = 'translation-zh'; | |
| translationSpan.style.cssText = 'display: block; margin-top: 4px; line-height: 1.4; font-style: italic; opacity: 0.6; font-weight: normal;'; | |
| translationSpan.textContent = translation; | |
| el.appendChild(translationSpan); | |
| } | |
| }); | |
| } | |
| }); | |
| // 2. 搜索结果页 + Similar articles + Cited by | |
| const docsumTitles = document.querySelectorAll('.docsum-title'); | |
| docsumTitles.forEach(titleElement => { | |
| titleElements.push({ | |
| element: titleElement, | |
| getText: (el) => { | |
| const clone = el.cloneNode(true); | |
| const existingTranslation = clone.querySelector('.translation-zh'); | |
| if (existingTranslation) existingTranslation.remove(); | |
| return clone.textContent.trim(); | |
| }, | |
| createTranslation: (el, translation) => { | |
| if (el.querySelector('.translation-zh')) return; | |
| const translationSpan = document.createElement('span'); | |
| translationSpan.className = 'translation-zh'; | |
| translationSpan.style.cssText = 'display: block; margin-top: 4px; line-height: 1.4; font-style: italic; opacity: 0.6; font-weight: normal;'; | |
| translationSpan.textContent = translation; | |
| el.appendChild(translationSpan); | |
| } | |
| }); | |
| }); | |
| // 3. 文章详情页 - 主标题 | |
| const detailTitleSelectors = [ | |
| 'h1.heading-title', | |
| '.heading-title', | |
| 'h1[class*="heading"]', | |
| 'main h1', | |
| 'article h1', | |
| ]; | |
| let detailTitle = null; | |
| for (const selector of detailTitleSelectors) { | |
| const elements = document.querySelectorAll(selector); | |
| for (const el of elements) { | |
| if (el.textContent.trim().length > 0 && | |
| !el.closest('.similar-articles') && | |
| !el.closest('.articles-from-the-same-journal') && | |
| !el.classList.contains('docsum-title')) { | |
| detailTitle = el; | |
| break; | |
| } | |
| } | |
| if (detailTitle) break; | |
| } | |
| if (detailTitle) { | |
| titleElements.push({ | |
| element: detailTitle, | |
| getText: (el) => { | |
| const clone = el.cloneNode(true); | |
| const existingTranslation = clone.querySelector('.translation-zh'); | |
| if (existingTranslation) existingTranslation.remove(); | |
| return clone.textContent.trim(); | |
| }, | |
| createTranslation: (el, translation) => { | |
| if (el.querySelector('.translation-zh')) return; | |
| const translationSpan = document.createElement('span'); | |
| translationSpan.className = 'translation-zh'; | |
| translationSpan.style.cssText = 'display: block; margin-top: 4px; line-height: 1.4; font-style: italic; opacity: 0.6; font-weight: normal;'; | |
| translationSpan.textContent = translation; | |
| el.appendChild(translationSpan); | |
| } | |
| }); | |
| } | |
| // 翻译所有标题(并发) | |
| await Promise.all(titleElements.map(item => | |
| addTranslation(item.element, item.getText, item.createTranslation) | |
| )); | |
| // 标题全部翻译完成后,再翻译摘要 | |
| if (CONFIG.translateAbstract) { | |
| const abstractElement = document.querySelector('div.abstract#abstract'); | |
| if (abstractElement) { | |
| await addAbstractTranslation( | |
| abstractElement, | |
| (el) => { | |
| const clone = el.cloneNode(true); | |
| const title = clone.querySelector('h2.title'); | |
| if (title) title.remove(); | |
| const keywords = clone.querySelector('p strong.sub-title'); | |
| if (keywords && keywords.textContent.includes('Keywords')) { | |
| keywords.parentElement.remove(); | |
| } | |
| const existingTranslation = clone.querySelector('.abstract-translation-zh'); | |
| if (existingTranslation) existingTranslation.remove(); | |
| return clone.textContent.trim(); | |
| } | |
| ); | |
| } | |
| } | |
| } | |
| /** | |
| * Europe PMC 处理 | |
| * 支持的页面类型: | |
| * - 文章详情页 (Detail): 主标题、Similar articles | |
| * - 搜索结果页 (Search): 论文标题列表(与 Similar articles 使用相同选择器) | |
| * | |
| * 注意:Europe PMC 的搜索结果页和 Similar articles 使用相同的 HTML 结构, | |
| * 因此使用统一的选择器 h3.citation-title a 即可覆盖两种情况 | |
| */ | |
| async function processEuropePMC() { | |
| // 1. 文章详情页 - 主标题 | |
| // 选择器:h1#article--current--title, h1.article-metadata-title | |
| // 示例 HTML: | |
| // <h1 id="article--current--title" class="article-metadata-title">TRIM13 prevents...</h1> | |
| const detailTitleSelectors = [ | |
| 'h1#article--current--title', | |
| 'h1.article-metadata-title', | |
| 'h1[id*="article"][id*="title"]', | |
| ]; | |
| let detailTitle = null; | |
| for (const selector of detailTitleSelectors) { | |
| detailTitle = document.querySelector(selector); | |
| if (detailTitle && detailTitle.textContent.trim().length > 0) { | |
| break; | |
| } | |
| detailTitle = null; | |
| } | |
| if (detailTitle) { | |
| addTranslation( | |
| detailTitle, | |
| (el) => { | |
| const clone = el.cloneNode(true); | |
| const existingTranslation = clone.querySelector('.translation-zh'); | |
| if (existingTranslation) existingTranslation.remove(); | |
| return clone.textContent.trim(); | |
| }, | |
| (el, translation) => { | |
| if (el.querySelector('.translation-zh')) return; | |
| const translationSpan = document.createElement('span'); | |
| translationSpan.className = 'translation-zh'; | |
| translationSpan.style.cssText = 'display: block; margin-top: 4px; line-height: 1.4; font-style: italic; opacity: 0.6; font-weight: normal;'; | |
| translationSpan.textContent = translation; | |
| el.appendChild(translationSpan); | |
| } | |
| ); | |
| } | |
| // 2. 搜索结果页 + Similar articles - 统一使用 h3.citation-title a | |
| const titleElements = []; | |
| const citationTitles = document.querySelectorAll('h3.citation-title a'); | |
| citationTitles.forEach(titleElement => { | |
| titleElements.push({ | |
| element: titleElement, | |
| getText: (el) => { | |
| const clone = el.cloneNode(true); | |
| const existingTranslation = clone.querySelector('.translation-zh'); | |
| if (existingTranslation) existingTranslation.remove(); | |
| return clone.textContent.trim(); | |
| }, | |
| createTranslation: (el, translation) => { | |
| if (el.querySelector('.translation-zh')) return; | |
| const translationSpan = document.createElement('span'); | |
| translationSpan.className = 'translation-zh'; | |
| translationSpan.style.cssText = 'display: block; margin-top: 4px; line-height: 1.4; font-style: italic; opacity: 0.6; font-weight: normal;'; | |
| translationSpan.textContent = translation; | |
| el.appendChild(translationSpan); | |
| } | |
| }); | |
| }); | |
| // 翻译所有标题 | |
| await Promise.all(titleElements.map(item => | |
| addTranslation(item.element, item.getText, item.createTranslation) | |
| )); | |
| // 标题翻译完成后,再翻译摘要 | |
| if (CONFIG.translateAbstract) { | |
| const abstractSelectors = [ | |
| 'div#article--abstract--content.abstract', | |
| 'div.abstract-content#eng-abstract', | |
| ]; | |
| let abstractElement = null; | |
| for (const selector of abstractSelectors) { | |
| abstractElement = document.querySelector(selector); | |
| if (abstractElement && abstractElement.textContent.trim().length > 0) { | |
| break; | |
| } | |
| abstractElement = null; | |
| } | |
| if (abstractElement) { | |
| await addAbstractTranslation( | |
| abstractElement, | |
| (el) => { | |
| const clone = el.cloneNode(true); | |
| const existingTranslation = clone.querySelector('.abstract-translation-zh'); | |
| if (existingTranslation) existingTranslation.remove(); | |
| return clone.textContent.trim(); | |
| } | |
| ); | |
| } | |
| } | |
| } | |
| // ==================== 主函数 ==================== | |
| /** | |
| * Semantic Scholar 处理 | |
| * 支持的页面类型: | |
| * - 搜索结果页 (Search): 论文标题列表 | |
| * - 文章详情页 (Detail): 主标题、Citations、References | |
| * - 个人主页 (Profile): 论文标题列表 | |
| * | |
| * 注意:除了详情页主标题外,其他所有标题都使用 .cl-paper-title 类 | |
| */ | |
| async function processSemanticScholar() { | |
| // 1. 文章详情页 - 主标题 | |
| // 选择器:h1[data-test-id="paper-detail-title"] | |
| // 示例 HTML: | |
| // <h1 data-test-id="paper-detail-title">Trim and Fill: A Simple Funnel...</h1> | |
| const detailTitle = document.querySelector('h1[data-test-id="paper-detail-title"]'); | |
| if (detailTitle && detailTitle.textContent.trim().length > 0) { | |
| addTranslation( | |
| detailTitle, | |
| (el) => { | |
| const clone = el.cloneNode(true); | |
| const existingTranslation = clone.querySelector('.translation-zh'); | |
| if (existingTranslation) existingTranslation.remove(); | |
| return clone.textContent.trim(); | |
| }, | |
| (el, translation) => { | |
| if (el.querySelector('.translation-zh')) return; | |
| const translationSpan = document.createElement('span'); | |
| translationSpan.className = 'translation-zh'; | |
| translationSpan.style.cssText = 'display: block; margin-top: 4px; line-height: 1.4; font-style: italic; opacity: 0.6; font-weight: normal;'; | |
| translationSpan.textContent = translation; | |
| el.appendChild(translationSpan); | |
| } | |
| ); | |
| } | |
| // 2. 搜索结果页 + Citations + References + 个人主页 | |
| const titleElements = []; | |
| const paperTitles = document.querySelectorAll('.cl-paper-title'); | |
| paperTitles.forEach(titleElement => { | |
| titleElements.push({ | |
| element: titleElement, | |
| getText: (el) => { | |
| const clone = el.cloneNode(true); | |
| const existingTranslation = clone.querySelector('.translation-zh'); | |
| if (existingTranslation) existingTranslation.remove(); | |
| return clone.textContent.trim(); | |
| }, | |
| createTranslation: (el, translation) => { | |
| if (el.querySelector('.translation-zh')) return; | |
| const translationSpan = document.createElement('span'); | |
| translationSpan.className = 'translation-zh'; | |
| translationSpan.style.cssText = 'display: block; margin-top: 4px; line-height: 1.4; font-style: italic; opacity: 0.6; font-weight: normal;'; | |
| translationSpan.textContent = translation; | |
| el.appendChild(translationSpan); | |
| } | |
| }); | |
| }); | |
| // 翻译所有标题 | |
| await Promise.all(titleElements.map(item => | |
| addTranslation(item.element, item.getText, item.createTranslation) | |
| )); | |
| } | |
| /** | |
| * Google Scholar 处理 | |
| * 支持的页面类型: | |
| * - 搜索结果页 (Search): 论文标题列表 | |
| * | |
| * 注意:Google Scholar 只有搜索功能,没有详情页 | |
| */ | |
| async function processGoogleScholar() { | |
| const titleElements = []; | |
| const searchTitles = document.querySelectorAll('h3.gs_rt'); | |
| searchTitles.forEach(titleElement => { | |
| titleElements.push({ | |
| element: titleElement, | |
| getText: (el) => { | |
| const clone = el.cloneNode(true); | |
| const existingTranslation = clone.querySelector('.translation-zh'); | |
| if (existingTranslation) existingTranslation.remove(); | |
| return clone.textContent.trim(); | |
| }, | |
| createTranslation: (el, translation) => { | |
| if (el.querySelector('.translation-zh')) return; | |
| const translationSpan = document.createElement('span'); | |
| translationSpan.className = 'translation-zh'; | |
| translationSpan.style.cssText = 'display: block; margin-top: 4px; line-height: 1.4; font-style: italic; opacity: 0.6; font-weight: normal;'; | |
| translationSpan.textContent = translation; | |
| el.appendChild(translationSpan); | |
| } | |
| }); | |
| }); | |
| // 翻译所有标题 | |
| await Promise.all(titleElements.map(item => | |
| addTranslation(item.element, item.getText, item.createTranslation) | |
| )); | |
| } | |
| /** | |
| * 根据当前网站选择对应的处理函数 | |
| */ | |
| function processCurrentSite() { | |
| const hostname = window.location.hostname; | |
| if (hostname.includes('pubmed.ncbi.nlm.nih.gov')) { | |
| processPubMed(); | |
| } else if (hostname.includes('europepmc.org')) { | |
| processEuropePMC(); | |
| } else if (hostname.includes('semanticscholar.org')) { | |
| processSemanticScholar(); | |
| } else if (hostname.includes('scholar.google')) { | |
| processGoogleScholar(); | |
| } | |
| } | |
| /** | |
| * 使用 MutationObserver 监听 DOM 变化 | |
| */ | |
| function observeChanges() { | |
| const observer = new MutationObserver((mutations) => { | |
| // 延迟执行,避免频繁触发 | |
| setTimeout(() => { | |
| processCurrentSite(); | |
| }, 500); | |
| }); | |
| observer.observe(document.body, { | |
| childList: true, | |
| subtree: true | |
| }); | |
| } | |
| // ==================== 初始化 ==================== | |
| // 输出启动信息 | |
| console.log('%c文献标题翻译 v1.0', 'color: #4CAF50; font-size: 16px; font-weight: bold'); | |
| console.log('%cAcademic Paper Title Translator', 'color: #2196F3; font-size: 12px'); | |
| console.log('%cBuild by Miao and Claude Sonnet 4.5 with ♥', 'color: #FF5722; font-size: 12px'); | |
| console.log(''); | |
| // 输出配置信息 | |
| const providerNameMap = { | |
| 'google': 'Google 翻译', | |
| 'microsoft': 'Microsoft 翻译', | |
| 'deepl': 'DeepL 翻译', | |
| 'niutrans': '小牛翻译', | |
| 'openai': 'OpenAI', | |
| 'deepseek': 'DeepSeek', | |
| 'gemini': 'Google Gemini', | |
| 'siliconflow': '硅基流动', | |
| 'groq': 'Groq', | |
| 'openai-compatible': 'OpenAI 兼容接口' | |
| }; | |
| const titleProviderName = providerNameMap[CONFIG.titleProvider] || CONFIG.titleProvider; | |
| const abstractProviderName = CONFIG.abstractProvider | |
| ? (providerNameMap[CONFIG.abstractProvider] || CONFIG.abstractProvider) | |
| : '与标题相同'; | |
| console.log('%c[配置信息]', 'color: #FF9800; font-weight: bold'); | |
| console.log(` 标题翻译提供商:%c${titleProviderName}`, 'color: #2196F3; font-weight: bold'); | |
| console.log(` 摘要翻译提供商:%c${abstractProviderName}`, 'color: #2196F3; font-weight: bold'); | |
| console.log(` 摘要翻译开关:%c${CONFIG.translateAbstract ? '已启用' : '已关闭'}`, | |
| CONFIG.translateAbstract ? 'color: #4CAF50; font-weight: bold' : 'color: #999'); | |
| console.log(` 并发数量:${CONFIG.maxConcurrent}`); | |
| console.log(` 请求延迟:${CONFIG.requestDelay}ms`); | |
| // 显示大模型配置信息 | |
| const allLlmProviders = ['openai', 'deepseek', 'gemini', 'siliconflow', 'groq', 'openai-compatible']; | |
| const usedProviders = [CONFIG.titleProvider, CONFIG.abstractProvider].filter(p => allLlmProviders.includes(p)); | |
| if (usedProviders.length > 0) { | |
| usedProviders.forEach(provider => { | |
| const config = CONFIG[provider]; | |
| if (config && config.model) { | |
| console.log(` ${providerNameMap[provider]} 模型:${config.model}`); | |
| } | |
| }); | |
| } | |
| console.log(''); | |
| // 页面加载完成后执行 | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', () => { | |
| processCurrentSite(); | |
| observeChanges(); | |
| }); | |
| } else { | |
| processCurrentSite(); | |
| observeChanges(); | |
| } | |
| console.log('%c[脚本已启动] 正在监听页面变化...', 'color: #4CAF50; font-weight: bold'); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment