// ==UserScript==
// @name Universal DeepSeek Text Selection
// @namespace http://tampermonkey.net/
// @version 3.1
// @description 通用型选中文本翻译/解释工具,支持复杂动态网页
// @author tangwang
// @license MIT
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect api.deepseek.com
// @connect api.deepseek.ai
// @connect *
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
API_KEY: '',
API_URL: '',
MAX_RETRIES: 3,
RETRY_DELAY: 1000,
DEBOUNCE_DELAY: 200,
SHORTCUTS: {
translate: 'Alt+T',
explain: 'Alt+E',
summarize: 'Alt+S'
}
};
// 样式注入
GM_addStyle(`
#ai-floating-menu {
all: initial;
position: fixed;
z-index: 2147483647;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
padding: 5px;
display: none;
font-family: system-ui, -apple-system, sans-serif;
animation: fadeIn 0.2s ease-in-out;
}
#ai-floating-menu button {
all: initial;
display: block;
width: 120px;
margin: 3px;
padding: 8px 12px;
background: #2c3e50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
font-size: 14px;
text-align: center;
transition: all 0.2s;
}
#ai-floating-menu button:hover {
background: #34495e;
transform: translateY(-1px);
}
#ai-floating-menu button:active {
transform: translateY(1px);
}
#ai-floating-menu .shortcut {
float: right;
font-size: 12px;
opacity: 0.7;
}
#ai-result-box {
all: initial;
position: fixed;
z-index: 2147483647;
background: white;
border-radius: 8px;
box-shadow: 0 3px 15px rgba(0,0,0,0.2);
padding: 15px;
min-width: 200px;
max-width: 500px;
max-height: 400px;
display: none;
font-family: system-ui, -apple-system, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #333;
overflow: auto;
animation: fadeIn 0.2s ease-in-out;
}
#ai-result-box .close-btn {
all: initial;
position: absolute;
top: 8px;
right: 8px;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
background: #f0f0f0;
border: none;
border-radius: 50%;
cursor: pointer;
font-family: inherit;
font-size: 14px;
color: #666;
transition: all 0.2s;
}
#ai-result-box .close-btn:hover {
background: #e0e0e0;
transform: rotate(90deg);
}
#ai-result-box .content {
margin-top: 5px;
white-space: pre-wrap;
word-break: break-word;
}
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
`);
// 创建UI元素
const menu = document.createElement('div');
menu.id = 'ai-floating-menu';
menu.innerHTML = `
<button data-action="translate">翻译为中文 <span class="shortcut">Alt+T</span></button>
<button data-action="explain">解释内容 <span class="shortcut">Alt+E</span></button>
<button data-action="summarize">总结要点 <span class="shortcut">Alt+S</span></button>
`;
const resultBox = document.createElement('div');
resultBox.id = 'ai-result-box';
resultBox.innerHTML = `
<button class="close-btn">×</button>
<div class="content"></div>
`;
// 工具函数
const utils = {
debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
},
async retry(fn, retries = CONFIG.MAX_RETRIES, delay = CONFIG.RETRY_DELAY) {
try {
return await fn();
} catch (error) {
if (retries === 0) throw error;
await new Promise(resolve => setTimeout(resolve, delay));
return this.retry(fn, retries - 1, delay * 2);
}
},
createLoadingSpinner() {
return '<div class="loading-spinner"></div> 处理中...';
}
};
// API调用类
class APIClient {
static async call(text, action) {
const prompts = {
translate: '将以下内容翻译成中文:',
explain: '请解释以下内容:',
summarize: '请总结以下内容的要点:'
};
return utils.retry(async () => {
const response = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: CONFIG.API_URL,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CONFIG.API_KEY}`
},
data: JSON.stringify({
model: 'deepseek-chat',
messages: [{
role: 'user',
content: `${prompts[action]}${text}`
}],
temperature: 0.7,
max_tokens: 1000
}),
onload: res => {
if (res.status === 200) {
try {
const data = JSON.parse(res.responseText);
if (data.choices?.[0]?.message?.content) {
resolve(data.choices[0].message.content);
} else {
reject(new Error('API返回格式错误'));
}
} catch (e) {
reject(new Error('解析响应失败'));
}
} else {
reject(new Error(`API错误: ${res.status}`));
}
},
onerror: () => reject(new Error('网络请求失败')),
ontimeout: () => reject(new Error('请求超时'))
});
});
return response;
});
}
}
// UI管理类
class UIManager {
static ensureElementsExist() {
if (!document.getElementById('ai-floating-menu')) {
document.body.appendChild(menu);
}
if (!document.getElementById('ai-result-box')) {
document.body.appendChild(resultBox);
}
}
static showMenu(x, y) {
this.ensureElementsExist();
menu.style.left = `${Math.max(0, Math.min(x, window.innerWidth - menu.offsetWidth))}px`;
menu.style.top = `${Math.max(0, Math.min(y, window.innerHeight - menu.offsetHeight))}px`;
menu.style.display = 'block';
}
static showResult(content, x, y) {
this.ensureElementsExist();
const contentDiv = resultBox.querySelector('.content');
contentDiv.innerHTML = content;
const maxWidth = Math.min(500, window.innerWidth - 40);
resultBox.style.maxWidth = `${maxWidth}px`;
let left = Math.max(10, Math.min(x + 10, window.innerWidth - maxWidth - 20));
let top = Math.max(10, Math.min(y, window.innerHeight - resultBox.offsetHeight - 20));
resultBox.style.left = `${left}px`;
resultBox.style.top = `${top}px`;
resultBox.style.display = 'block';
}
static hideAll() {
menu.style.display = 'none';
resultBox.style.display = 'none';
}
}
// 文本选择管理类
class SelectionManager {
static getSelectedText() {
let text = '';
let range = null;
// 检查常规选择
const selection = window.getSelection();
text = selection.toString().trim();
if (text && selection.rangeCount > 0) {
range = selection.getRangeAt(0);
return { text, range };
}
// 检查iframe
try {
const iframes = document.getElementsByTagName('iframe');
for (const iframe of iframes) {
try {
const iframeSelection = iframe.contentWindow.getSelection();
const iframeText = iframeSelection.toString().trim();
if (iframeText) {
return {
text: iframeText,
range: iframeSelection.rangeCount > 0 ? iframeSelection.getRangeAt(0) : null
};
}
} catch (e) {}
}
} catch (e) {}
// 检查输入框
const activeElement = document.activeElement;
if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
const start = activeElement.selectionStart;
const end = activeElement.selectionEnd;
if (start !== end) {
text = activeElement.value.substring(start, end).trim();
return { text, range: null };
}
}
return { text: '', range: null };
}
}
// 事件处理类
class EventHandler {
static init() {
UIManager.ensureElementsExist();
// 菜单按钮点击
menu.addEventListener('click', async (e) => {
const button = e.target.closest('button');
if (!button) return;
const action = button.dataset.action;
const { text } = SelectionManager.getSelectedText();
if (!text) return;
await this.handleAction(action, text, e.clientX, e.clientY);
});
// 关闭按钮
resultBox.querySelector('.close-btn').addEventListener('click', () => {
UIManager.hideAll();
});
// 点击外部关闭
document.addEventListener('mousedown', (e) => {
if (!menu.contains(e.target) && !resultBox.contains(e.target)) {
UIManager.hideAll();
}
}, true);
// 快捷键
document.addEventListener('keydown', (e) => {
for (const [action, shortcut] of Object.entries(CONFIG.SHORTCUTS)) {
const [modifier, key] = shortcut.split('+');
if (e[`${modifier.toLowerCase()}Key`] && e.key.toUpperCase() === key) {
e.preventDefault();
const { text } = SelectionManager.getSelectedText();
if (text) {
this.handleAction(action, text, e.clientX, e.clientY);
}
}
}
});
// 选择文本
this.addSelectionListeners();
this.observeDynamicContent();
}
static async handleAction(action, text, x, y) {
UIManager.hideAll();
UIManager.showResult(utils.createLoadingSpinner(), x, y);
try {
const response = await APIClient.call(text, action);
UIManager.showResult(response, x, y);
} catch (error) {
UIManager.showResult(`错误: ${error.message}`, x, y);
}
}
static addSelectionListeners(target = document) {
const handleSelection = utils.debounce((e) => {
const { text, range } = SelectionManager.getSelectedText();
if (!text) {
UIManager.hideAll();
return;
}
let x = e?.clientX || 0;
let y = e?.clientY || 0;
if (range) {
try {
const rect = range.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
x = rect.right;
y = rect.bottom + 5;
}
} catch (e) {}
}
UIManager.showMenu(x, y);
}, CONFIG.DEBOUNCE_DELAY);
target.addEventListener('mouseup', handleSelection, true);
target.addEventListener('keyup', handleSelection, true);
target.addEventListener('selectionchange', handleSelection, true);
}
static observeDynamicContent() {
const observer = new MutationObserver(utils.debounce(() => {
document.querySelectorAll('iframe').forEach(iframe => {
try {
if (iframe.contentDocument) {
this.addSelectionListeners(iframe.contentDocument);
}
} catch (e) {}
});
}, CONFIG.DEBOUNCE_DELAY));
observer.observe(document.body, {
childList: true,
subtree: true
});
}
}
// 初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => EventHandler.init());
} else {
EventHandler.init();
}
})();