// ==UserScript==
// @name SmartReader - AI Web Summarizer
// @namespace http://tampermonkey.net/
// @version 0.9.1
// @description 网页内容智能总结,支持自定义API和提示词
// @author Your Name
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @require https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// 配置项
let config = {
apiUrl: GM_getValue('apiUrl', 'https://api.openai.com/v1/chat/completions'),
apiKey: GM_getValue('apiKey', ''),
model: GM_getValue('model', 'gpt-3.5-turbo'),
prompt: GM_getValue('prompt', '请总结以下网页内容,使用markdown格式:\n\n'),
iconPosition: GM_getValue('iconPosition', { y: 20 })
};
// 等待外部库加载完成
function waitForLibrary(name, callback, maxAttempts = 50) {
let attempts = 0;
const checkLibrary = () => {
if (window[name]) {
callback(window[name]);
return;
}
attempts++;
if (attempts < maxAttempts) {
setTimeout(checkLibrary, 100);
}
};
checkLibrary();
}
// 创建图标
function createIcon() {
const icon = document.createElement('div');
icon.id = 'website-summary-icon';
icon.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`;
icon.style.cssText = `
position: fixed;
z-index: 999999;
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
color: #007AFF;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
right: 20px;
top: ${config.iconPosition.y || 20}px;
`;
icon.addEventListener('mouseover', () => {
icon.style.transform = 'scale(1.1)';
icon.style.boxShadow = '0 4px 15px rgba(0, 0, 0, 0.2)';
icon.style.color = '#0056b3';
});
icon.addEventListener('mouseout', () => {
icon.style.transform = 'scale(1)';
icon.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.1)';
icon.style.color = '#007AFF';
});
icon.addEventListener('click', async () => {
const container = document.getElementById('website-summary-container');
if (!container) return;
container.style.display = 'block';
container.querySelector('#website-summary-content').innerHTML = '<p style="text-align: center; color: #666;">正在获取总结...</p>';
try {
const content = getPageContent();
if (!content) throw new Error('无法获取页面内容');
const summary = await getSummary(content);
if (!summary) throw new Error('获取总结失败');
renderContent(summary);
} catch (error) {
console.error('总结过程出错:', error);
container.querySelector('#website-summary-content').innerHTML = `
<p style="text-align: center; color: #ff4444;">
获取总结失败:${error.message}<br>
请检查API配置是否正确
</p>`;
}
});
icon.addEventListener('contextmenu', (e) => createContextMenu(e, icon));
makeDraggable(icon);
document.body.appendChild(icon);
return icon;
}
// 创建UI元素
function createUI() {
const container = document.createElement('div');
container.id = 'website-summary-container';
container.style.cssText = `
position: fixed;
z-index: 999998;
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 16px;
width: 80%;
max-width: 800px;
max-height: 80vh;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: none;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
overflow: hidden;
`;
const header = document.createElement('div');
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
cursor: move;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
`;
const title = document.createElement('h3');
title.textContent = '网页总结';
title.style.margin = '0';
title.style.fontSize = '18px';
title.style.color = '#333';
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
gap: 8px;
align-items: center;
`;
const copyBtn = document.createElement('button');
copyBtn.textContent = '复制';
copyBtn.style.cssText = `
background: #4CAF50;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
`;
copyBtn.addEventListener('mouseover', () => {
copyBtn.style.backgroundColor = '#45a049';
});
copyBtn.addEventListener('mouseout', () => {
copyBtn.style.backgroundColor = '#4CAF50';
});
copyBtn.addEventListener('click', () => {
const content = document.getElementById('website-summary-content').innerText;
navigator.clipboard.writeText(content).then(() => {
const originalText = copyBtn.textContent;
copyBtn.textContent = '已复制';
setTimeout(() => {
copyBtn.textContent = originalText;
}, 2000);
}).catch(err => {
console.error('复制失败:', err);
});
});
const closeBtn = document.createElement('button');
closeBtn.textContent = '×';
closeBtn.style.cssText = `
background: none;
border: none;
font-size: 24px;
cursor: pointer;
padding: 0 8px;
color: #666;
transition: color 0.2s;
`;
closeBtn.addEventListener('mouseover', () => {
closeBtn.style.color = '#ff4444';
});
closeBtn.addEventListener('mouseout', () => {
closeBtn.style.color = '#666';
});
const content = document.createElement('div');
content.id = 'website-summary-content';
content.style.cssText = `
max-height: calc(80vh - 60px);
overflow-y: auto;
font-size: 14px;
line-height: 1.6;
padding: 8px 0;
`;
buttonContainer.appendChild(copyBtn);
buttonContainer.appendChild(closeBtn);
header.appendChild(title);
header.appendChild(buttonContainer);
container.appendChild(header);
container.appendChild(content);
document.body.appendChild(container);
closeBtn.addEventListener('click', () => {
container.style.display = 'none';
});
makeDraggable(container);
return container;
}
// 创建设置界面
function createSettingsUI() {
const settingsContainer = document.createElement('div');
settingsContainer.id = 'website-summary-settings';
settingsContainer.style.cssText = `
position: fixed;
z-index: 1000000;
background: rgba(255, 255, 255, 0.98);
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
padding: 20px;
width: 400px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: none;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
`;
const header = document.createElement('div');
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
cursor: move;
`;
const title = document.createElement('h3');
title.textContent = '设置';
title.style.margin = '0';
const closeBtn = document.createElement('button');
closeBtn.textContent = '×';
closeBtn.style.cssText = `
background: none;
border: none;
font-size: 24px;
cursor: pointer;
padding: 0 8px;
color: #666;
`;
const form = document.createElement('form');
form.style.cssText = `
display: flex;
flex-direction: column;
gap: 16px;
`;
// 创建输入字段
const apiUrlInput = document.createElement('input');
apiUrlInput.type = 'text';
apiUrlInput.id = 'apiUrl';
apiUrlInput.value = config.apiUrl;
apiUrlInput.placeholder = '输入API URL';
const apiKeyInput = document.createElement('input');
apiKeyInput.type = 'password';
apiKeyInput.id = 'apiKey';
apiKeyInput.value = config.apiKey;
apiKeyInput.placeholder = '输入API Key';
const modelInput = document.createElement('input');
modelInput.type = 'text';
modelInput.id = 'model';
modelInput.value = config.model;
modelInput.placeholder = '输入AI模型名称';
const promptInput = document.createElement('textarea');
promptInput.id = 'prompt';
promptInput.value = config.prompt;
promptInput.placeholder = '输入提示词';
promptInput.style.height = '100px';
promptInput.style.resize = 'vertical';
// 添加标签和输入框到表单
const addFormField = (label, input) => {
const fieldContainer = document.createElement('div');
fieldContainer.style.cssText = `
display: flex;
flex-direction: column;
gap: 4px;
`;
const labelElement = document.createElement('label');
labelElement.textContent = label;
labelElement.style.cssText = `
font-size: 14px;
color: #333;
font-weight: 500;
`;
input.style.cssText = `
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 6px;
font-family: inherit;
`;
fieldContainer.appendChild(labelElement);
fieldContainer.appendChild(input);
form.appendChild(fieldContainer);
};
addFormField('API URL', apiUrlInput);
addFormField('API Key', apiKeyInput);
addFormField('AI 模型', modelInput);
addFormField('提示词', promptInput);
const saveBtn = document.createElement('button');
saveBtn.textContent = '保存设置';
saveBtn.style.cssText = `
background: #007AFF;
color: white;
border: none;
padding: 10px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background-color 0.2s;
`;
saveBtn.addEventListener('mouseover', () => {
saveBtn.style.backgroundColor = '#0056b3';
});
saveBtn.addEventListener('mouseout', () => {
saveBtn.style.backgroundColor = '#007AFF';
});
// 修改保存逻辑
saveBtn.addEventListener('click', (e) => {
e.preventDefault();
// 获取表单值
const newApiUrl = apiUrlInput.value.trim();
const newApiKey = apiKeyInput.value.trim();
const newModel = modelInput.value.trim();
const newPrompt = promptInput.value.trim();
// 保存到GM存储
GM_setValue('apiUrl', newApiUrl);
GM_setValue('apiKey', newApiKey);
GM_setValue('model', newModel);
GM_setValue('prompt', newPrompt);
// 更新内存中的配置
config.apiUrl = newApiUrl;
config.apiKey = newApiKey;
config.model = newModel;
config.prompt = newPrompt;
// 显示保存成功提示
const successMsg = document.createElement('div');
successMsg.textContent = '设置已保存';
successMsg.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: #4CAF50;
color: white;
padding: 10px 20px;
border-radius: 4px;
z-index: 1000001;
font-size: 14px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
`;
document.body.appendChild(successMsg);
setTimeout(() => successMsg.remove(), 2000);
// 关闭设置界面
settingsContainer.style.display = 'none';
});
header.appendChild(title);
header.appendChild(closeBtn);
form.appendChild(saveBtn);
settingsContainer.appendChild(header);
settingsContainer.appendChild(form);
document.body.appendChild(settingsContainer);
closeBtn.addEventListener('click', () => {
settingsContainer.style.display = 'none';
});
makeDraggable(settingsContainer);
return settingsContainer;
}
// 修改右键菜单
function createContextMenu(e, icon) {
e.preventDefault();
const menu = document.createElement('div');
menu.style.cssText = `
position: fixed;
z-index: 1000000;
background: rgba(255, 255, 255, 0.98);
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 8px 0;
min-width: 150px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
`;
const menuItems = [
{ text: '打开设置', action: () => {
const settings = document.getElementById('website-summary-settings');
if (settings) {
settings.style.display = 'block';
}
}}
];
menuItems.forEach(item => {
const menuItem = document.createElement('div');
menuItem.textContent = item.text;
menuItem.style.cssText = `
padding: 8px 16px;
cursor: pointer;
transition: background-color 0.2s;
`;
menuItem.addEventListener('mouseover', () => {
menuItem.style.backgroundColor = 'rgba(0, 0, 0, 0.05)';
});
menuItem.addEventListener('mouseout', () => {
menuItem.style.backgroundColor = 'transparent';
});
menuItem.addEventListener('click', () => {
item.action();
menu.remove();
});
menu.appendChild(menuItem);
});
menu.style.left = `${e.clientX}px`;
menu.style.top = `${e.clientY}px`;
document.body.appendChild(menu);
const closeMenu = (e) => {
if (!menu.contains(e.target) && e.target !== icon) {
menu.remove();
}
};
document.addEventListener('click', closeMenu);
menu.addEventListener('click', () => {
document.removeEventListener('click', closeMenu);
});
}
// 实现拖拽功能
function makeDraggable(element) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
const header = element.querySelector('div') || element;
header.addEventListener('mousedown', dragMouseDown);
function dragMouseDown(e) {
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
document.addEventListener('mouseup', closeDragElement);
document.addEventListener('mousemove', elementDrag);
}
function elementDrag(e) {
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
if (element.id === 'website-summary-icon') {
const newTop = element.offsetTop - pos2;
const maxTop = window.innerHeight - element.offsetHeight;
const clampedTop = Math.max(0, Math.min(newTop, maxTop));
element.style.top = clampedTop + "px";
element.style.right = "20px";
element.style.left = "auto";
} else {
element.style.top = (element.offsetTop - pos2) + "px";
element.style.left = (element.offsetLeft - pos1) + "px";
}
}
function closeDragElement() {
document.removeEventListener('mouseup', closeDragElement);
document.removeEventListener('mousemove', elementDrag);
if (element.id === 'website-summary-icon') {
config.iconPosition = { y: element.offsetTop };
GM_setValue('iconPosition', config.iconPosition);
}
}
}
// 获取页面内容
function getPageContent() {
try {
const clone = document.body.cloneNode(true);
const elementsToRemove = clone.querySelectorAll('script, style, iframe, nav, header, footer, .ad, .advertisement, .social-share, .comment, .related-content');
elementsToRemove.forEach(el => el.remove());
return clone.innerText.replace(/\s+/g, ' ').trim().slice(0, 4000);
} catch (error) {
console.error('获取页面内容失败:', error);
return document.body.innerText.slice(0, 4000);
}
}
// 调用API获取总结
function getSummary(content) {
return new Promise((resolve, reject) => {
// 直接从config中获取API Key
const currentApiKey = config.apiKey;
const currentApiUrl = config.apiUrl;
const currentModel = config.model;
const currentPrompt = config.prompt;
console.log('当前API Key:', currentApiKey ? '已设置' : '未设置');
console.log('当前API URL:', currentApiUrl);
console.log('当前Model:', currentModel);
if (!currentApiKey || currentApiKey.trim() === '') {
resolve('请先设置API Key');
return;
}
const requestData = {
model: currentModel,
messages: [
{
role: 'system',
content: '你是一个专业的网页内容总结助手,善于使用markdown格式来组织信息。'
},
{
role: 'user',
content: currentPrompt + content
}
],
temperature: 0.7
};
GM_xmlhttpRequest({
method: 'POST',
url: currentApiUrl,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${currentApiKey}`
},
data: JSON.stringify(requestData),
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
if (data.error) {
console.error('API错误:', data.error);
resolve('API调用失败: ' + data.error.message);
return;
}
if (data.choices && data.choices[0] && data.choices[0].message) {
resolve(data.choices[0].message.content);
} else {
console.error('API响应格式错误:', data);
resolve('API响应格式错误,请检查配置。');
}
} catch (error) {
console.error('解析API响应失败:', error);
resolve('解析API响应失败,请检查网络连接。');
}
},
onerror: function(error) {
console.error('API调用失败:', error);
resolve('API调用失败,请检查网络连接和API配置。');
}
});
});
}
// 渲染Markdown
function renderContent(content) {
const container = document.getElementById('website-summary-content');
if (!container) return;
try {
if (!content) throw new Error('内容为空');
// 处理Markdown内容
let html = window.marked.parse(content);
container.innerHTML = html;
const style = document.createElement('style');
style.textContent = `
#website-summary-content {
font-size: 14px;
line-height: 1.6;
color: #333;
}
#website-summary-content h1,
#website-summary-content h2,
#website-summary-content h3 {
margin-top: 20px;
margin-bottom: 10px;
color: #222;
}
#website-summary-content p {
margin: 10px 0;
}
#website-summary-content code {
background: #f5f5f5;
padding: 2px 4px;
border-radius: 3px;
font-family: monospace;
}
#website-summary-content pre {
background: #f5f5f5;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
}
#website-summary-content blockquote {
border-left: 4px solid #ddd;
margin: 10px 0;
padding-left: 15px;
color: #666;
}
#website-summary-content ul,
#website-summary-content ol {
margin: 10px 0;
padding-left: 20px;
}
#website-summary-content li {
margin: 5px 0;
}
#website-summary-content table {
border-collapse: collapse;
width: 100%;
margin: 10px 0;
}
#website-summary-content th,
#website-summary-content td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
#website-summary-content th {
background: #f5f5f5;
}
`;
document.head.appendChild(style);
} catch (error) {
console.error('渲染内容失败:', error);
container.innerHTML = '<p style="text-align: center; color: #ff4444;">渲染内容失败,请刷新页面重试。</p>';
}
}
// 初始化
function init() {
try {
waitForLibrary('marked', (marked) => {
marked.setOptions({
breaks: true,
gfm: true
});
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
createIcon();
createUI();
createSettingsUI();
});
} else {
createIcon();
createUI();
createSettingsUI();
}
} catch (error) {
console.error('初始化失败:', error);
}
}
// 启动脚本
init();
})();