首先感谢kiss翻译开源作者的贡献,本人也是非常喜欢这个项目,就是在使用中有些痛点,所以我个人就搞了一个自己觉得好用的脚本,推荐给大家,如果大家有喜欢的可以点个赞。下面是操作步骤:
1.浏览器要能安装“油猴”插件
2.使用下面的脚本中“******”设置成自己使用的模型调用(支持openai公益和第三方api)
// --- ⚠️ 用户配置区 ---
// 请将 YOUR_API_ENDPOINT 替换为你的第三方中转API地址
const API_ENDPOINT = 'https://********/v1/chat/completions';
// 请将 YOUR_API_KEY 替换为你的API Key
const API_KEY = '*********';
// 你想使用的模型
const MODEL = 'gpt-4.1-nano';
3.完整脚本:
// ==UserScript==
// @name 划词翻译 (基于OpenAI中转API) - 居中最终版
// @name:en 划词翻译
// @namespace http://tampermonkey.net/
// @version 1.3
// @description 增加单例运行保护,彻底解决重复执行问题。在网页上选择文本后,一个半透明的按钮会出现在选区中央,点击即可调用OpenAI API进行翻译。
// @description:en Added singleton guard to prevent duplicate script execution. After selecting text, a semi-transparent button appears in the center of the selection for translation.
// @author AI Assistant
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect *
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- 新增:单例运行保护 ---
// 如果脚本已在该页面运行,则直接退出,防止重复加载和事件绑定
if (window.isAiTranslatorScriptRunning) {
console.log('划词翻译脚本已在运行,本次加载被阻止。');
return;
}
window.isAiTranslatorScriptRunning = true;
// --- 保护结束 ---
//--- ⚠️ 用户配置区 ---
// 请将 YOUR_API_ENDPOINT 替换为你的第三方中转API地址
const API_ENDPOINT = 'https://********/v1/chat/completions';
// 请将 YOUR_API_KEY 替换为你的API Key
const API_KEY = '*********';
// 你想使用的模型
const MODEL = 'gpt-4.1-nano';
// 翻译指令
const PROMPT_TEMPLATE = `Translate the following text into simplified Chinese, keeping the original formatting as much as possible. Provide only the translated content, without any extra explanations or introductory phrases.\n\nText to translate:\n"""\n{text}\n"""`;
// --- 配置区结束 ---
if (API_ENDPOINT === 'YOUR_API_ENDPOINT' || API_KEY === 'YOUR_API_KEY') {
alert('【划词翻译脚本提示】\n\n请先编辑脚本,在顶部的“用户配置区”填入您的API地址和API Key!');
return;
}
GM_addStyle(`
#ai-translator-btn {
position: absolute;
width: 10px;
height: 10px;
background-color: #ff4757;
border-radius: 50%;
cursor: pointer;
z-index: 99999;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
border: 2px solid white;
transform: translate(-50%, -50%);
opacity: 0.5;
transition: transform 0.2s ease, opacity 0.2s ease;
}
#ai-translator-btn:hover {
transform: translate(-50%, -50%) scale(1.2);
opacity: 1;
}
#ai-translator-popup {
position: absolute;
z-index: 99998;
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 15px;
max-width: 400px;
min-width: 200px;
font-size: 14px;
line-height: 1.6;
color: #333;
text-align: left;
white-space: pre-wrap;
word-wrap: break-word;
transform: translate(-50%, -50%);
}
`);
let translateBtn = null;
let translatePopup = null;
function cleanup() {
const existingBtn = document.getElementById('ai-translator-btn');
if (existingBtn) {
existingBtn.remove();
}
const existingPopup = document.getElementById('ai-translator-popup');
if (existingPopup) {
existingPopup.remove();
}
translateBtn = null;
translatePopup = null;
}
document.addEventListener('mouseup', function(e) {
if (e.target.id === 'ai-translator-btn' || (translatePopup && translatePopup.contains(e.target))) {
return;
}
cleanup();
const selection = window.getSelection();
if (selection.isCollapsed || selection.toString().trim().length === 0) {
return; // 如果没有选择内容或选择已折叠,则直接返回
}
const selectedText = selection.toString().trim();
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
const centerX = window.scrollX + rect.left + rect.width / 2;
const centerY = window.scrollY + rect.top + rect.height / 2;
translateBtn = document.createElement('div');
translateBtn.id = 'ai-translator-btn';
document.body.appendChild(translateBtn);
translateBtn.style.top = `${centerY}px`;
translateBtn.style.left = `${centerX}px`;
translateBtn.addEventListener('click', function(event) {
event.stopPropagation();
showTranslationPopup(selectedText, centerY, centerX);
if (translateBtn) translateBtn.style.display = 'none';
});
});
document.addEventListener('mousedown', function(e) {
if (translatePopup && !translatePopup.contains(e.target) && e.target.id !== 'ai-translator-btn') {
cleanup();
}
});
function showTranslationPopup(text, top, left) {
// 再次清理以防万一
cleanup();
translatePopup = document.createElement('div');
translatePopup.id = 'ai-translator-popup';
translatePopup.textContent = '翻译中...';
document.body.appendChild(translatePopup);
translatePopup.style.top = `${top}px`;
translatePopup.style.left = `${left}px`;
callOpenAI(text);
}
function callOpenAI(text) {
GM_xmlhttpRequest({
method: 'POST',
url: API_ENDPOINT,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
data: JSON.stringify({
model: MODEL,
messages: [{ role: 'user', content: PROMPT_TEMPLATE.replace('{text}', text) }],
temperature: 0.7,
stream: false
}),
onload: function(response) {
if (translatePopup) {
try {
const data = JSON.parse(response.responseText);
if (data.choices && data.choices.length > 0) {
const translatedText = data.choices[0].message.content.trim();
translatePopup.textContent = translatedText;
} else {
const errorMsg = data.error ? data.error.message : 'API返回格式不正确。';
translatePopup.textContent = `翻译失败:${errorMsg}`;
}
} catch (e) {
translatePopup.textContent = '翻译失败:无法解析返回结果。';
}
}
},
onerror: function(response) {
if (translatePopup) {
translatePopup.textContent = `翻译失败:网络请求错误。状态: ${response.status}`;
}
},
ontimeout: function() {
if (translatePopup) {
translatePopup.textContent = '翻译失败:请求超时。';
}
}
});
}
})();
4.效果如下 :选中要翻译的文本,在中心出现一个半透明的小红点(这样做的目的是尽可能减少整体页面的影响,我就是觉得kiss这个图标有点太耀眼了,所以想才有了制作这个脚本的想法)





