Burning Vocabulary类似版

仿真Burning Vocabulary,带翻译功能,在经济学人、彭博社网站上对单词进行标注并显示中文翻译(智能识别多个相关单词,只标注选中部分)

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Burning Vocabulary类似版
// @namespace    http://tampermonkey.net/
// @version      70.1
// @description  仿真Burning Vocabulary,带翻译功能,在经济学人、彭博社网站上对单词进行标注并显示中文翻译(智能识别多个相关单词,只标注选中部分)
// @author       TCH
// @match        *://www.economist.com
// @match        *://www.bloomberg.com
// @include      *://*economist.com/*
// @include      *://*bloomberg.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM_xmlhttpRequest
// @connect      dashscope.aliyuncs.com
// @require      https://scriptcat.org/lib/513/2.1.0/ElementGetter.js#sha256=aQF7JFfhQ7Hi+weLrBlOsY24Z2ORjaxgZNoni7pAz5U=
// @license      tangchuanhui
// ==/UserScript==
 
 
 
 
(function() {
	// ==================== 千问API配置 ====================
	const QIANWEN_API_KEY = 'sk-ee2c525aaba8427aa01049c0d90f7c9a'; // 请替换为你的千问API Key
	const QIANWEN_API_URL = 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation';
 
	// ==================== 辅助函数 ====================
 
 
	// 从全文中查找所有以部分单词开头的完整单词(去重)
	function findAllCompleteWords(partialWord, fullText) {
		if (!partialWord || !partialWord.trim()) {
			return [];
		}
 
		partialWord = partialWord.trim().toLowerCase();
		const wordBoundaryRegex = /[\s\.,;:!?\-—()\[\]{}"'⁰¹²³⁴⁵⁶⁷⁸⁹]/;
 
		// 清理文本:去除上标数字和翻译标记
		const cleanText = fullText.replace(/[⁰¹²³⁴⁵⁶⁷⁸⁹]/g, '').replace(/\[[^\]]+\]/g, '');
 
		const completeWords = new Set(); // 使用Set自动去重
		let currentPos = 0;
 
		// 遍历全文查找所有匹配
		while (currentPos < cleanText.length) {
			// 查找部分单词的下一个出现位置
			let foundPos = cleanText.toLowerCase().indexOf(partialWord, currentPos);
 
			if (foundPos === -1) {
				break; // 没有更多匹配
			}
 
			// 检查是否是单词开头(前面是空格或文本开头)
			const isWordStart = foundPos === 0 || wordBoundaryRegex.test(cleanText[foundPos - 1]);
 
			if (isWordStart) {
				// 向后拓展获取完整单词
				let endPos = foundPos + partialWord.length;
				while (endPos < cleanText.length && !wordBoundaryRegex.test(cleanText[endPos])) {
					endPos++;
				}
 
				const completeWord = cleanText.substring(foundPos, endPos).trim();
				if (completeWord.length > 0) {
					completeWords.add(completeWord);
				}
			}
 
			currentPos = foundPos + 1;
		}
 
		return Array.from(completeWords);
	}
 
	// 从文本中查找部分单词并拓展为完整单词(单个)
	function findAndExpandWord(partialWord, text) {
		// 如果部分单词为空,返回null
		if (!partialWord || !partialWord.trim()) {
			return null;
		}
 
		partialWord = partialWord.trim();
		const wordBoundaryRegex = /[\s\.,;:!?\-—()\[\]{}"']/;
 
		// 在文本中查找部分单词(带空格前缀,确保是单词边界)
		const searchPattern = ' ' + partialWord;
		let startIndex = text.indexOf(searchPattern);
 
		// 如果在开头找不到(可能是句首),尝试不带空格查找
		if (startIndex === -1) {
			startIndex = 0;
			if (!text.startsWith(partialWord)) {
				return partialWord; // 找不到,返回原始单词
			}
		} else {
			startIndex++; // 跳过前面的空格
		}
 
		// 向后拓展直到遇到单词边界
		let endIndex = startIndex + partialWord.length;
		while (endIndex < text.length && !wordBoundaryRegex.test(text[endIndex])) {
			endIndex++;
		}
 
		const completeWord = text.substring(startIndex, endIndex).trim();
		return completeWord;
	}
 
	// 提取单词所在的完整句子
	function extractSentence(word, fullText) {
		// 过滤掉文本中的上标数字(⁰¹²³⁴⁵⁶⁷⁸⁹)
		const cleanText = fullText.replace(/[⁰¹²³⁴⁵⁶⁷⁸⁹]/g, '');
 
		// 查找单词在文本中的位置
		word = " " + word;
		const wordIndex = cleanText.indexOf(word);
		if (wordIndex === -1) {
			return null; // 如果找不到,返回null
		}
 
		// 向前查找句子开始(句号、问号、感叹号或文本开头)
		let sentenceStart = wordIndex;
		const sentenceStartRegex = /[.!?]\s+/;
		while (sentenceStart > 0) {
			if (sentenceStartRegex.test(cleanText.substring(sentenceStart - 2, sentenceStart + 1))) {
				break;
			}
			sentenceStart--;
		}
 
		// 向后查找句子结束
		let sentenceEnd = wordIndex + word.length;
		while (sentenceEnd < cleanText.length) {
			if (sentenceStartRegex.test(cleanText.substring(sentenceEnd, sentenceEnd + 2))) {
				sentenceEnd++;
				break;
			}
			sentenceEnd++;
		}
 
		// 返回清理后的句子,并进一步清理可能残留的标注标记
		const sentence = cleanText.substring(sentenceStart, sentenceEnd).trim();
		// 也过滤掉中文翻译标记 [xxx]
		return sentence.replace(/\[[^\]]+\]/g, '');
	}
 
	// 调用千问API获取翻译
	function getTranslationFromQianwen(word, sentence) {
		return new Promise((resolve, reject) => {
			if (!QIANWEN_API_KEY || QIANWEN_API_KEY === 'YOUR_API_KEY_HERE') {
				console.warn('未配置千问API Key,使用默认翻译');
				resolve('翻译');
				return;
			}
 
			// 如果句子为空,直接返回
			if (!sentence || sentence.trim() === '') {
				console.warn('句子为空,无法翻译');
				resolve('翻译');
				return;
			}
 
			const prompt = `请翻译句子"${sentence}"中的单词"${word}"在这个语境下的中文意思。只需要返回1-6个汉字的简短翻译,不要解释。`;
 
			GM_xmlhttpRequest({
				method: 'POST',
				url: QIANWEN_API_URL,
				headers: {
					'Content-Type': 'application/json',
					'Authorization': `Bearer ${QIANWEN_API_KEY}`
				},
				data: JSON.stringify({
					model: "qwen-turbo",
					input: {
						messages: [{
								role: "system",
								content: "你是一个专业的英语翻译助手,只返回简短的中文翻译,不要任何解释。"
							},
							{
								role: "user",
								content: prompt
							}
						]
					},
					parameters: {
						max_tokens: 50,
						temperature: 0.3,
						result_format: "message"
					}
				}),
				onload: function(response) {
					try {
						console.log('千问API响应状态:', response.status);
						console.log('千问API响应内容:', response.responseText);
 
						const result = JSON.parse(response.responseText);
 
						// 检查是否有错误
						if (result.code || result.error) {
							console.error('千问API返回错误:', result.message || result.error);
							resolve('翻译');
							return;
						}
 
						// 尝试多种可能的响应格式
						let translation = null;
 
						// 格式1: result.output.choices[0].message.content
						if (result.output && result.output.choices && result.output.choices[0] && result.output.choices[0].message) {
							translation = result.output.choices[0].message.content;
						}
						// 格式2: result.output.text
						else if (result.output && result.output.text) {
							translation = result.output.text;
						}
						// 格式3: result.choices[0].message.content
						else if (result.choices && result.choices[0] && result.choices[0].message) {
							translation = result.choices[0].message.content;
						}
						// 格式4: result.text
						else if (result.text) {
							translation = result.text;
						}
 
						if (translation) {
							resolve(translation.trim());
						} else {
							console.error('无法从响应中提取翻译,响应结构:', result);
							resolve('翻译');
						}
					} catch (error) {
						console.error('解析千问API响应失败:', error);
						console.error('原始响应:', response.responseText);
						resolve('翻译');
					}
				},
				onerror: function(error) {
					console.error('千问API调用失败:', error);
					resolve('翻译');
				}
			});
		});
	}
 
 
	// 统一的渲染函数,接受容器选择器作为参数
	function rendering(containerSelector) {
		// 根据选择器获取容器
		const container = containerSelector === '.body-content' 
			? document.querySelector('.body-content')
			: document.getElementsByTagName("body")[0];
		
		var allsText = container.innerHTML;
 
		function makeTranslate(completeWord, translation) {
			// 如果有翻译,添加到显示中
			const translationText = translation ? `[${translation}]` : '';
 
      var num = -1;
 
      var rHtml = new RegExp("\<.*?\>", "ig"); //匹配html元素
      var aHtml = allsText.match(rHtml); //存放html元素的数组
      allsText = allsText.replace(rHtml, '{~}'); //替换html标签
			
			// 使用正则表达式 + g 标志替换所有匹配
			var rWord = new RegExp(completeWord.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), "g");
			allsText = allsText.replace(rWord, completeWord + translationText); //替换翻译
 
      allsText = allsText.replace(/{~}/g, function() { //恢复html标签
        num++;
        return aHtml[num];
      });
    }
 
	function makeOnceColor(searchVal, nColor, tot) {
      searchVal = " " + searchVal;
 
			var sKey = "<span name='addSpan' style='color:" + nColor + ";'><sup style='vertical-align: super; font-size: 0.75em;'>" + tot + "</sup>" + searchVal + "</span>";
      var num = -1;
      var rStr = new RegExp(searchVal, "g");
 
      var rHtml = new RegExp("\<.*?\>", "ig"); //匹配html元素
      var aHtml = allsText.match(rHtml); //存放html元素的数组
      allsText = allsText.replace(rHtml, '{~}'); //替换html标签
 
      allsText = allsText.replace(rStr, sKey); //替换key
 
      allsText = allsText.replace(/{~}/g, function() { //恢复html标签
        num++;
        return aHtml[num];
      });
    }
 
	function makecolor(searchVal, completeWord, nColor, tot, translation) {
		var oDiv = containerSelector === '.body-content' 
			? document.querySelector('.body-content')
			: document.getElementsByTagName("body")[0];
      var sText = oDiv.innerHTML;
 
		// 如果有翻译,添加到显示中
		const translationText = translation ? `[${translation}]` : '';
		var sKey = "<span name='addSpan' style='color:" + nColor + ";'><sup style='vertical-align: super; font-size: 0.75em;'>" + tot + "</sup>" + searchVal + "</span>";
      searchVal = " " + searchVal;
 
      var num = -1;
      var rStr = new RegExp(searchVal, "g");
      var rHtml = new RegExp("\<.*?\>", "ig"); //匹配html元素
      var aHtml = sText.match(rHtml); //存放html元素的数组
      sText = sText.replace(rHtml, '{~}'); //替换html标签
			sText = sText.replace(completeWord, completeWord + translationText); //替换翻译
      sText = sText.replace(rStr, sKey); //替换key
      sText = sText.replace(/{~}/g, function() { //恢复html标签
        num++;
        return aHtml[num];
      });
      oDiv.innerHTML = sText;
    }
 
    //alert("开始整体染色1");
 
		let list_value = GM_listValues();
 
		//alert("开始渲染按钮");
		let div = document.createElement("div");
		div.style = "position:fixed; z-index:90;bottom:20px; left: 0; margin: auto; right: 0;text-align:center;"
		div.innerHTML = '<span id="biaozhubiaozhu"style="width:150rpx;z-index:100;margin:15px;background-color: red;font-size: 30px;border-color: red;border-radius: 5px;" >标注</span><span id="quxiaobiaozhu" style="width:150rpx;margin:15px;background-color: black;font-size: 30px;color: white;border-radius: 5px;">取消</span>';
 
		// 异步加载所有翻译
		(async function() {
			const fullText = container.innerText;
 
    for (var i = 0; i < list_value.length; i++) {
      let tot = GM_getValue(list_value[i], 0);
 
				// 从全文中找到所有以list_value[i]开头的完整单词
				const completeWords = findAllCompleteWords(list_value[i], fullText);
				console.log(`部分单词 "${list_value[i]}" 在全文中找到 ${completeWords.length} 个完整单词:`, completeWords);
 
				// 为每个完整单词分别获取翻译
				for (const completeWord of completeWords) {
					// 提取包含该完整单词的句子
					const sentence = extractSentence(completeWord, fullText);
					if (sentence === null) {
						console.warn(`无法为单词 "${completeWord}" 提取句子`);
						continue;
					}
 
					// 使用完整单词获取翻译
					const translation = await getTranslationFromQianwen(completeWord, sentence);
					console.log(`完整单词 "${completeWord}" 的翻译: ${translation}`);
 
					// 先把所有的翻译加上
					makeTranslate(completeWord, translation);
				}
				makeOnceColor(list_value[i], "red", tot);
			}
			container.innerHTML = allsText;
			
			// 重新添加按钮(因为上面的 innerHTML 替换会删除按钮)
			document.body.append(div);
		})();
 
    //监听选择文本的动作
    var selectionFirst = null;
    var selectionSecond = null;
    document.addEventListener("selectionchange", () => {
      selectionFirst = selectionSecond;
      selectionSecond = document.getSelection()
        .toString();
    });
 
    //alert("整体染色结束1");
 
		document.onclick = async function(event) {
      if (event.target.id == "biaozhubiaozhu") {
 
        selectionFirst = selectionSecond; //在有些浏览器,需要把这句去除
        if (selectionFirst !== null && selectionFirst !== void 0 && selectionFirst.toString()) {
					// 获取完整页面文本
					const bodyContent = document.querySelector('.body-content') || document.getElementsByTagName("body")[0];
					const fullText = bodyContent.innerText;
					let selectedText = selectionFirst.trim();
					let completeWord;
 
					// 清理选中文本,得到完整单词(去除前后的标点符号)
					selectedText = selectedText.replace(/^[^\w]+|[^\w]+$/g, '');
 
					// 提取包含该单词的句子
					const sentence = extractSentence(selectedText, fullText);
 
					// 获取存储的计数
					let tot = GM_getValue(selectedText, 0);
 
					// 如果是第一次标注,获取翻译并高亮显示
					if (tot === 0) {
						try {
 
							// 从全文中找到所有以selectedText开头的完整单词
							const completeWords = findAllCompleteWords(selectedText, fullText);
							console.log(`部分单词 "${selectedText}" 在全文中找到 ${completeWords.length} 个完整单词:`, completeWords);
 
							// 为每个完整单词分别获取翻译
							for (completeWord of completeWords) {
								// 提取包含该完整单词的句子
								const sentence = extractSentence(completeWord, fullText);
								if (sentence === null) {
									console.warn(`无法为单词 "${completeWord}" 提取句子`);
									continue;
								}
 
								// 使用完整单词获取翻译
								const translation = await getTranslationFromQianwen(completeWord, sentence);
								console.log(`完整单词 "${completeWord}" 的翻译: ${translation}`);
 
								// 但只标注用户实际选中的部分(list_value[i])
								makecolor(selectedText, completeWord, "red", 1, translation);
							}
						} catch (error) {
							console.error('获取翻译失败:', error);
							makecolor(selectedText, completeWord, "red", 1, "翻译");
						}
					}
					// 只保存计数,不保存翻译
					GM_setValue(selectedText, tot + 1);
 
        }
 
      } else if (event.target.id == "quxiaobiaozhu") {
 
        selectionFirst = selectionSecond; //在有些浏览器,需要把这句去除
 
        if (selectionFirst !== null && selectionFirst !== void 0 && selectionFirst.toString()) {
					// 清理选中文本
					let completeWord = selectionFirst.trim().replace(/^[^\w]+|[^\w]+$/g, '');
					GM_deleteValue(completeWord);
        }
      }
    };
    document.body.append(div);
	}
 
 	// ==================== ElementGetter 已通过 @require 引入 ====================
	// ElementGetter 官方库地址: https://bbs.tampermonkey.net.cn/thread-2726-1-1.html
	// 使用 elmGetter 对象进行异步元素获取
 
 
  elmGetter.get('p[class="ArticleBodyText_articleBodyContent__17wqE typography_articleBody__3UcBa paywall"]').then(div1 => {
 
    ////删除广告
    document.querySelector('.media-ui-FullWidthAd_fullWidthAdWrapper-fClHZteIk3k-').style.display = 'none';
    //删除浏览器版本低的提示
    const targetElement1 = document.querySelector('.unsupported-browser-notification-container');
    if (targetElement1) {
      targetElement1.style.display = 'none';
      targetElement1.style.visibility = 'hidden';
    } else {
      //alert('未找到浏览器版本低的提示');
    }
    
		rendering('.body-content');
	});
 
	elmGetter.get('main[style]').then(div1 => {
		rendering('body');
	});
 
 
 
 
})();