// ==UserScript==
// @name AI网页内容总结
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 使用AI总结网页内容的油猴脚本
// @author bizhanrensheng
// @match *://*/*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @require https://cdnjs.cloudflare.com/ajax/libs/markdown-it/13.0.1/markdown-it.min.js
// @license MIT
// @homepageURL https://xr.imyaigc.com
// ==/UserScript==
(function() {
'use strict';
// 默认配置
// Replace DEFAULT_CONFIG with the new API settings
const DEFAULT_CONFIG = {
API_URL: 'https://api.x.ai/v1/chat/completions',
API_KEY: 'xai-bizhan(这里用你注册获得的API)',
MAX_TOKENS: 10000,
SHORTCUT: 'Alt+S',
PROMPT: 'Please summarize the following webpage content in Markdown format, covering main points, key information, and details. Make it thorough, accurate, and organized.',
MODEL: 'grok-beta'
};
// Then, update the `summarizeContent` function to match the API format you need
async function summarizeContent(content) {
return new Promise(async (resolve, reject) => {
const contentContainer = document.querySelector('.ai-summary-content');
contentContainer.innerHTML = '
Generating summary
';
let summary = '';
const md = createMarkdownRenderer();
// Set a timeout to handle potential request delays
const timeout = setTimeout(() => {
reject(new Error('Request timed out. Please check API URL, API Key, and network connection.'));
}, 20000);
try {
const response = await fetch(CONFIG.API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CONFIG.API_KEY}`
},
body: JSON.stringify({
model: CONFIG.MODEL,
messages: [
{ role: 'system', content: CONFIG.PROMPT },
{ role: 'user', content: content }
],
max_tokens: CONFIG.MAX_TOKENS,
temperature: 0,
stream: false
})
});
// Check if the response is OK
if (!response.ok) {
clearTimeout(timeout);
throw new Error(`API request failed (${response.status}): Please check API URL and Key`);
}
const result = await response.json();
clearTimeout(timeout);
if (result && result.choices && result.choices[0] && result.choices[0].message) {
summary = result.choices[0].message.content;
contentContainer.innerHTML = md.render(summary);
resolve(summary);
} else {
throw new Error("Unexpected API response format.");
}
} catch (error) {
clearTimeout(timeout);
reject(error);
}
});
}
// 获取配置
let CONFIG = {};
function loadConfig() {
CONFIG = {
API_URL: GM_getValue('API_URL', DEFAULT_CONFIG.API_URL),
API_KEY: GM_getValue('API_KEY', DEFAULT_CONFIG.API_KEY),
MAX_TOKENS: GM_getValue('MAX_TOKENS', DEFAULT_CONFIG.MAX_TOKENS),
SHORTCUT: GM_getValue('SHORTCUT', DEFAULT_CONFIG.SHORTCUT),
PROMPT: GM_getValue('PROMPT', DEFAULT_CONFIG.PROMPT),
MODEL: GM_getValue('MODEL', DEFAULT_CONFIG.MODEL)
};
return CONFIG;
}
// 保存配置
function saveConfig(newConfig) {
Object.keys(newConfig).forEach(key => {
GM_setValue(key, newConfig[key]);
});
CONFIG = { ...CONFIG, ...newConfig };
}
// 添加样式
GM_addStyle(` #ai-summary-root .ai-summary-container{position:fixed;bottom:20px;right:20px;display:flex;align-items:center;z-index:99998;user-select:none;align-items:stretch;box-shadow:0 2px 5px rgba(0,0,0,.2);height:30px}#ai-summary-root .ai-settings-panel,#ai-summary-root .ai-summary-modal{position:fixed;transform:translate(-50%,-50%);box-shadow:0 4px 20px rgba(0,0,0,.15)}#ai-summary-root .ai-summary-container .ai-drag-handle{width:15px;height:100%;background-color:rgba(75,85,99,.8);border-radius:5px 0 0 5px;cursor:move;margin-right:1px;display:flex;align-items:center;justify-content:center}#ai-summary-root .ai-summary-container .ai-drag-handle::before{content:"⋮";color:#f3f4f6;font-size:16px;transform:rotate(90deg)}#ai-summary-root .ai-summary-container .ai-summary-btn{padding:5px 15px;background-color:rgba(75,85,99,.8);color:#f3f4f6;border:none;border-radius:0 4px 4px 0;cursor:pointer;font-size:12px;transition:.3s;height:100%;line-height:1;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}#ai-summary-root .ai-summary-container .ai-summary-btn:hover{background-color:rgba(75,85,99,.9)}#ai-summary-root .ai-summary-modal{display:none;top:50%;left:50%;width:80%;max-width:800px;max-height:80vh;background:#f8f9fa;border-radius:8px;z-index:99999;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}#ai-summary-root .ai-summary-modal *{box-sizing:border-box}#ai-summary-root .ai-summary-overlay{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);z-index:99998}#ai-summary-root .ai-summary-modal .ai-summary-header{padding:15px 20px;background:#f1f3f5;border-bottom:1px solid #dee2e6;display:flex;justify-content:space-between;align-items:center}#ai-summary-root .ai-summary-modal .ai-summary-header h3{color:#495057;margin:0;padding:0;font-size:16px;font-weight:600;line-height:1.4;font-family:inherit}#ai-summary-root .ai-summary-modal .ai-summary-close{background:0 0;border:none;font-size:20px;cursor:pointer;color:#6c757d;padding:0 5px;line-height:1;font-family:inherit}#ai-summary-root .ai-summary-modal .ai-summary-close:hover{color:#495057}#ai-summary-root .ai-summary-modal .ai-summary-content{padding:20px;overflow-y:auto;max-height:calc(80vh - 130px);line-height:1.6;color:#374151;font-size:15px;font-family:inherit}#ai-summary-root .ai-summary-modal .ai-summary-content h1{font-size:1.8em;margin:1.5em 0 .8em;padding-bottom:.3em;border-bottom:2px solid #e5e7eb;font-weight:600;line-height:1.3;color:#1f2937}#ai-summary-root .ai-summary-modal .ai-summary-content h2{font-size:1.5em;margin:1.3em 0 .7em;padding-bottom:.2em;border-bottom:1px solid #e5e7eb;font-weight:600;line-height:1.3;color:#1f2937}#ai-summary-root .ai-summary-modal .ai-summary-content h3{font-size:1.3em;margin:1.2em 0 .6em;font-weight:600;line-height:1.3;color:#1f2937}#ai-summary-root .ai-summary-modal .ai-summary-content p{margin:1em 0;line-height:1.8;color:inherit}#ai-summary-root .ai-summary-modal .ai-summary-content ol,#ai-summary-root .ai-summary-modal .ai-summary-content ul{margin:1em 0;padding-left:2em;line-height:1.6}#ai-summary-root .ai-summary-modal .ai-summary-content li{margin:.5em 0;line-height:inherit;color:inherit}#ai-summary-root .ai-summary-modal .ai-summary-content blockquote{margin:1em 0;padding:.5em 1em;border-left:4px solid #60a5fa;background:#f3f4f6;color:#4b5563;font-style:normal}#ai-summary-root .ai-summary-modal .ai-summary-content code{background:#f3f4f6;padding:.2em .4em;border-radius:3px;font-family:Consolas,Monaco,"Courier New",monospace;font-size:.9em;color:#d946ef;white-space:pre-wrap}#ai-summary-root .ai-summary-modal .ai-summary-content pre{background:#1f2937;color:#e5e7eb;padding:1em;border-radius:6px;overflow-x:auto;margin:1em 0;white-space:pre;word-wrap:normal}#ai-summary-root .ai-summary-modal .ai-summary-content pre code{background:0 0;color:inherit;padding:0;border-radius:0;font-size:inherit;white-space:pre}#ai-summary-root .ai-summary-modal .ai-summary-content table{border-collapse:collapse;width:100%;margin:1em 0;font-size:inherit}#ai-summary-root .ai-summary-modal .ai-summary-content td,#ai-summary-root .ai-summary-modal .ai-summary-content th{border:1px solid #d1d5db;padding:.5em;text-align:left;color:inherit;background:0 0}#ai-summary-root .ai-summary-modal .ai-summary-content th{background:#f9fafb;font-weight:600}#ai-summary-root .ai-summary-modal .ai-summary-footer{padding:15px 20px;border-top:1px solid #dee2e6;display:flex;justify-content:flex-end;gap:10px;background:#f8f9fa}#ai-summary-root .ai-settings-btn,#ai-summary-root .ai-summary-modal .ai-retry-btn{padding:8px;background:#6c757d;color:#fff;border:none;border-radius:4px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:background .3s;width:36px;height:36px;min-width:36px;line-height:1}#ai-summary-root .ai-settings-btn:hover,#ai-summary-root .ai-settings-panel .cancel-btn:hover,#ai-summary-root .ai-settings-panel .save-btn:hover,#ai-summary-root .ai-summary-modal .ai-copy-btn:hover,#ai-summary-root .ai-summary-modal .ai-retry-btn:hover{background:#5a6268}#ai-summary-root .ai-summary-modal .ai-retry-btn svg{width:20px;height:20px}#ai-summary-root .ai-summary-modal .ai-copy-btn{padding:8px 16px;background:#6c757d;color:#fff;border:none;border-radius:4px;cursor:pointer;display:inline-flex;align-items:center;gap:8px;transition:background .3s;font-size:14px;line-height:1;font-family:inherit}#ai-summary-root .ai-summary-modal .ai-loading{text-align:center;padding:20px;color:#6c757d;font-family:inherit}#ai-summary-root .ai-summary-modal .ai-loading-dots:after{content:'.';animation:1.5s steps(5,end) infinite dots}@keyframes dots{0%,20%{content:'.'}40%{content:'..'}60%{content:'...'}100%,80%{content:''}}#ai-summary-root .ai-settings-panel{display:none;top:50%;left:50%;width:90%;max-width:500px;background:#fff;padding:20px;border-radius:8px;z-index:100000}#ai-summary-root .ai-settings-panel textarea{resize:vertical;max-width:100%;height:100px;resize:vertical}#ai-summary-root .ai-settings-panel h3{margin:0 0 20px;padding-bottom:10px;border-bottom:1px solid #dee2e6;color:#495057;font-size:18px}#ai-summary-root .ai-settings-panel .form-group{margin-bottom:15px}#ai-summary-root .ai-settings-panel label{display:block;margin-bottom:5px;color:#495057;font-weight:500}#ai-summary-root .ai-settings-panel input,#ai-summary-root .ai-settings-panel textarea{max-width:100%;box-sizing:border-box;width:100%;padding:8px 12px;border:1px solid #ced4da;border-radius:4px;font-size:14px;line-height:1.5}#ai-summary-root .ai-settings-panel button{padding:8px 16px;border:none;border-radius:4px;cursor:pointer;font-size:14px;transition:.3s}#ai-summary-root .ai-settings-panel .cancel-btn,#ai-summary-root .ai-settings-panel .save-btn{background:#6c757d;color:#fff}#ai-summary-root .ai-settings-panel .clear-cache-btn{padding:8px 16px;background:#dc3545;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:14px;transition:.3s;margin-right:auto}#ai-summary-root .ai-settings-panel .clear-cache-btn:hover{background:#c82333}#ai-summary-root .ai-settings-panel .buttons{display:flex;justify-content:flex-end;gap:10px;margin-top:20px} `);
// 创建设置面板
function createSettingsPanel() {
const panel = document.createElement('div');
panel.className = 'ai-settings-panel';
panel.innerHTML = `
设置
API URL
API Key
模型
最大Token数
快捷键
总结提示词
${CONFIG.PROMPT}
清除缓存
取消
保存
`;
const overlay = document.createElement('div');
overlay.className = 'ai-summary-overlay';
// 事件监听
panel.querySelector('.save-btn').addEventListener('click', () => {
const newConfig = {
API_URL: panel.querySelector('#api-url').value,
API_KEY: panel.querySelector('#api-key').value,
MAX_TOKENS: parseInt(panel.querySelector('#max-tokens').value),
SHORTCUT: panel.querySelector('#shortcut').value,
PROMPT: panel.querySelector('#prompt').value,
MODEL: panel.querySelector('#model').value
};
saveConfig(newConfig);
panel.style.display = 'none';
document.querySelector('.ai-summary-overlay').style.display = 'none';
});
panel.querySelector('.cancel-btn').addEventListener('click', () => {
panel.style.display = 'none';
document.querySelector('.ai-summary-overlay').style.display = 'none';
});
// 清除缓存按钮事件
panel.querySelector('.clear-cache-btn').addEventListener('click', () => {
const keys = ['API_URL', 'API_KEY', 'MAX_TOKENS', 'SHORTCUT', 'PROMPT', 'MODEL'];
keys.forEach(key => GM_setValue(key, undefined)); // 设置为undefined模拟删除
// 重置为默认配置
CONFIG = { ...DEFAULT_CONFIG };
// 更新输入框的值
panel.querySelector('#api-url').value = CONFIG.API_URL;
panel.querySelector('#api-key').value = CONFIG.API_KEY;
panel.querySelector('#max-tokens').value = CONFIG.MAX_TOKENS;
panel.querySelector('#shortcut').value = CONFIG.SHORTCUT;
panel.querySelector('#prompt').value = CONFIG.PROMPT;
panel.querySelector('#model').value = CONFIG.MODEL;
alert('缓存已清除,已恢复默认设置');
});
return panel;
}
// 创建DOM元素
function createElements() {
// 创建根容器,并添加唯一ID
const rootContainer = document.createElement('div');
rootContainer.id = 'ai-summary-root';
// 创建容器和拖动把手
const container = document.createElement('div');
container.className = 'ai-summary-container';
const dragHandle = document.createElement('div');
dragHandle.className = 'ai-drag-handle';
const button = document.createElement('button');
button.className = 'ai-summary-btn';
button.textContent = '总结网页';
// 在footer中添加设置按钮
const settingsBtn = document.createElement('button');
settingsBtn.className = 'ai-settings-btn';
settingsBtn.title = '设置';
settingsBtn.innerHTML = `
`;
container.appendChild(dragHandle);
container.appendChild(button);
document.body.appendChild(container);
// 创建模态框和遮罩层
const modal = document.createElement('div');
modal.className = 'ai-summary-modal';
modal.innerHTML = `
网页内容总结
×
复制内容
`;
const overlay = document.createElement('div');
overlay.className = 'ai-summary-overlay';
document.body.appendChild(overlay);
document.body.appendChild(modal);
modal.querySelector('.ai-summary-footer').insertBefore(
settingsBtn,
modal.querySelector('.ai-retry-btn')
);
const settingsPanel = createSettingsPanel();
// 将容器添加到根容器
rootContainer.appendChild(container);
rootContainer.appendChild(overlay);
rootContainer.appendChild(modal);
rootContainer.appendChild(settingsPanel);
document.body.appendChild(rootContainer); // 将根容器添加到body
return { container, button, modal, overlay, dragHandle, settingsBtn, settingsPanel, rootContainer };
}
// 获取网页内容
function getPageContent() {
const title = document.title;
const content = document.body.innerText;
return { title, content };
}
// 显示错误信息
function showError(container, error, details = '') {
container.innerHTML = `
错误: ${error}
${details ? `
${details}
` : ''}
`;
}
// 调用API进行总结
async function summarizeContent(content) {
return new Promise(async (resolve, reject) => {
const contentContainer = document.querySelector('.ai-summary-content');
contentContainer.innerHTML = '
正在生成总结
';
let summary = '';
const md = createMarkdownRenderer();
// 添加超时检查
const timeout = setTimeout(() => {
reject(new Error('请求超时,请检查API URL、API Key和网络连接'));
}, 20000);
try {
const response = await fetch(CONFIG.API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CONFIG.API_KEY}`
},
body: JSON.stringify({
model: CONFIG.MODEL,
messages: [
{ role: 'system', content: CONFIG.PROMPT },
{ role: 'user', content: content }
],
max_tokens: CONFIG.MAX_TOKENS,
temperature: 0.7,
stream: true
})
});
// 检查响应状态
if (!response.ok) {
clearTimeout(timeout);
throw new Error(`API请求失败 (${response.status}): 请检查API URL和Key是否正确`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split('\n');
for (const line of lines) {
if (line.trim() === '' || line.trim() === 'data: [DONE]') continue;
const jsonLine = line.replace(/^data: /, '');
try {
const parsedData = JSON.parse(jsonLine);
if (parsedData.choices && parsedData.choices[0] && parsedData.choices[0].delta) {
if (parsedData.choices[0].delta.content) {
summary += parsedData.choices[0].delta.content;
contentContainer.innerHTML = md.render(summary);
}
}
} catch (e) {
console.warn('忽略无法解析的行:', line);
continue;
}
}
}
clearTimeout(timeout);
resolve(summary);
} catch (error) {
clearTimeout(timeout);
reject(error);
}
});
}
// 初始化拖动功能
function initializeDrag(container, dragHandle) {
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
dragHandle.addEventListener('mousedown', (e) => {
isDragging = true;
initialX = e.clientX - container.offsetLeft;
initialY = e.clientY - container.offsetTop;
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
// 确保不会拖出屏幕
const maxX = window.innerWidth - container.offsetWidth;
const maxY = window.innerHeight - container.offsetHeight;
currentX = Math.max(0, Math.min(currentX, maxX));
currentY = Math.max(0, Math.min(currentY, maxY));
container.style.left = currentX + 'px';
container.style.top = currentY + 'px';
container.style.right = 'auto';
container.style.bottom = 'auto';
}
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
}
// 初始化事件监听
function initializeEvents(elements) {
const { container, button, modal, overlay, dragHandle, settingsBtn, settingsPanel, rootContainer } = elements;
// 初始化拖动功能
initializeDrag(container, dragHandle);
// 点击按钮显示模态框
button.addEventListener('click', async () => {
// 检查是否是第一次点击且未配置API
if (!CONFIG.API_KEY || CONFIG.API_KEY === '' || CONFIG.API_KEY === 'YOUR_API_KEY') {
settingsPanel.style.display = 'block';
overlay.style.display = 'block';
return;
}
showModal(modal, overlay);
const contentContainer = modal.querySelector('.ai-summary-content');
try {
if (!CONFIG.API_URL || CONFIG.API_URL === 'YOUR_API_URL') {
throw new Error('请先配置API URL');
}
if (!CONFIG.API_KEY || CONFIG.API_KEY === 'YOUR_API_KEY') {
throw new Error('请先配置API Key');
}
const { content } = getPageContent();
const summary = await summarizeContent(content);
if (summary) {
contentContainer.innerHTML = window.markdownit().render(summary);
}
} catch (error) {
console.error('Summary Error:', error);
showError(contentContainer, error.message);
}
});
// 关闭模态框
modal.querySelector('.ai-summary-close').addEventListener('click', () => {
hideModal(modal, overlay);
});
overlay.addEventListener('click', () => {
hideModal(modal, overlay);
settingsPanel.style.display = 'none';
});
// 复制按钮功能
modal.querySelector('.ai-copy-btn').addEventListener('click', () => {
const content = modal.querySelector('.ai-summary-content').textContent;
navigator.clipboard.writeText(content).then(() => {
const copyBtn = modal.querySelector('.ai-copy-btn');
const originalText = copyBtn.innerHTML;
copyBtn.innerHTML = '已复制!';
setTimeout(() => {
copyBtn.innerHTML = originalText;
}, 2000);
});
});
// 添加快捷键支持
document.addEventListener('keydown', (e) => {
if (e.altKey && e.key.toLowerCase() === 's') {
button.click();
}
if (e.key === 'Escape' && modal.style.display === 'block') {
hideModal(modal, overlay);
}
});
// 添加重试按钮事件处理
modal.querySelector('.ai-retry-btn').addEventListener('click', async () => {
const contentContainer = modal.querySelector('.ai-summary-content');
try {
const { content } = getPageContent();
const summary = await summarizeContent(content);
if (summary) {
const md = window.markdownit({
html: true,
linkify: true,
typographer: true,
breaks: true
});
contentContainer.innerHTML = md.render(summary);
}
} catch (error) {
console.error('Retry Error:', error);
showError(contentContainer, error.message);
}
});
const allSettingsButtons = rootContainer.querySelectorAll('.ai-settings-btn');
allSettingsButtons.forEach(btn => {
btn.addEventListener('click', () => {
// 更新设置面板中的值
settingsPanel.querySelector('#api-url').value = CONFIG.API_URL;
settingsPanel.querySelector('#api-key').value = CONFIG.API_KEY;
settingsPanel.querySelector('#max-tokens').value = CONFIG.MAX_TOKENS;
settingsPanel.querySelector('#shortcut').value = CONFIG.SHORTCUT;
settingsPanel.querySelector('#prompt').value = CONFIG.PROMPT;
settingsPanel.querySelector('#model').value = CONFIG.MODEL;
settingsPanel.style.display = 'block';
overlay.style.display = 'block';
});
});
}
// 增强markdown-it配置
function createMarkdownRenderer() {
return window.markdownit({
html: true,
linkify: true,
typographer: true,
breaks: true
});
}
// 显示模态框
function showModal(modal, overlay) {
modal.style.display = 'block';
overlay.style.display = 'block';
}
// 隐藏模态框
function hideModal(modal, overlay) {
modal.style.display = 'none';
overlay.style.display = 'none';
}
// 初始化时加载配置
loadConfig();
// 初始化脚本
const elements = createElements();
initializeEvents(elements);
})();