// ==UserScript== // @name AI Page Summarizer Pro // @name:zh-CN AI网页内容智能总结助手 // @namespace http://tampermonkey.net/ // @version 0.9.9.3 // @description 网页内容智能总结,支持自定义API和提示词 // @description:zh-CN 网页内容智能总结,支持自定义API和提示词 // @author Your Name // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_addStyle // @grant GM.xmlHttpRequest // @grant GM.setValue // @grant GM.getValue // @grant GM.registerMenuCommand // @grant GM.addStyle // @grant window.fetch // @grant window.localStorage // @connect api.openai.com // @connect * // @require https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js // @run-at document-start // @noframes // @license MIT // @compatible chrome // @compatible firefox // @compatible edge // @compatible opera // @compatible safari // @compatible android // @downloadURL none // ==/UserScript== (function() { 'use strict'; // 添加全局错误处理 window.addEventListener('error', function(event) { console.error('脚本错误:', event.error); if (event.error && event.error.stack) { console.error('错误堆栈:', event.error.stack); } }); window.addEventListener('unhandledrejection', function(event) { console.error('未处理的Promise错误:', event.reason); }); // 兼容性检查 const browserSupport = { hasGM: typeof GM !== 'undefined', hasGMFunctions: typeof GM_getValue !== 'undefined', hasLocalStorage: (function() { try { localStorage.setItem('test', 'test'); localStorage.removeItem('test'); return true; } catch (e) { return false; } })(), hasBackdropFilter: (function() { const el = document.createElement('div'); return typeof el.style.backdropFilter !== 'undefined' || typeof el.style.webkitBackdropFilter !== 'undefined'; })(), isMobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent), isSafari: /^((?!chrome|android).)*safari/i.test(navigator.userAgent) }; // 兼容性处理层 const scriptHandler = { // 存储值 setValue: async function(key, value) { try { if (browserSupport.hasGMFunctions) { GM_setValue(key, value); return true; } else if (browserSupport.hasGM && GM.setValue) { await GM.setValue(key, value); return true; } else if (browserSupport.hasLocalStorage) { localStorage.setItem('ws_' + key, JSON.stringify(value)); return true; } return false; } catch (error) { console.error('存储值失败:', error); return false; } }, // 获取值 getValue: async function(key, defaultValue) { try { if (browserSupport.hasGMFunctions) { return GM_getValue(key, defaultValue); } else if (browserSupport.hasGM && GM.getValue) { return await GM.getValue(key, defaultValue); } else if (browserSupport.hasLocalStorage) { const value = localStorage.getItem('ws_' + key); return value ? JSON.parse(value) : defaultValue; } return defaultValue; } catch (error) { console.error('获取值失败:', error); return defaultValue; } }, // HTTP请求 xmlHttpRequest: function(details) { return new Promise((resolve, reject) => { const handleResponse = (response) => { resolve(response); }; const handleError = (error) => { reject(new Error('请求错误: ' + error.message)); }; if (browserSupport.hasGMFunctions && typeof GM_xmlhttpRequest !== 'undefined') { GM_xmlhttpRequest({ ...details, onload: handleResponse, onerror: handleError, ontimeout: details.ontimeout }); } else if (browserSupport.hasGM && typeof GM !== 'undefined' && GM.xmlHttpRequest) { GM.xmlHttpRequest({ ...details, onload: handleResponse, onerror: handleError, ontimeout: details.ontimeout }); } else { fetch(details.url, { method: details.method, headers: details.headers, body: details.data, mode: 'cors', credentials: 'omit' }) .then(async response => { const text = await response.text(); handleResponse({ status: response.status, responseText: text, responseHeaders: [...response.headers].join('\n') }); }) .catch(handleError); } }).then(response => { if (details.onload) { details.onload(response); } return response; }).catch(error => { if (details.onerror) { details.onerror(error); } throw error; }); }, // 注册菜单命令 registerMenuCommand: function(name, fn) { try { if (browserSupport.hasGMFunctions) { GM_registerMenuCommand(name, fn); return true; } else if (browserSupport.hasGM && GM.registerMenuCommand) { GM.registerMenuCommand(name, fn); return true; } return false; } catch (error) { console.log('注册菜单命令失败:', error); return false; } }, // 添加样式 addStyle: function(css) { try { if (browserSupport.hasGMFunctions) { GM_addStyle(css); return true; } else if (browserSupport.hasGM && GM.addStyle) { GM.addStyle(css); return true; } else { const style = document.createElement('style'); style.textContent = css; document.head.appendChild(style); return true; } } catch (error) { console.error('添加样式失败:', error); return false; } } }; // 配置项 let config = { apiUrl: 'https://api.openai.com/v1/chat/completions', apiKey: '', model: 'gpt-3.5-turbo', theme: 'light', prompt: `You are a professional content summarizer in chinese. Your task is to create a clear, concise, and well-structured summary of the webpage content. Follow these guidelines: 1. Output Format: - Use ## for main sections - Use bullet points (•) for key points and details - Use bold for important terms - Use blockquotes for notable quotes 2. Content Structure: ## 核心观点 • Key points here... ## 关键信息 • Important details here... ## 市场情绪 • Market sentiment here... ## 专家观点 • Expert opinions here... ## 总结 • Final summary here... 3. Writing Style: - Clear and concise language - Professional tone - Logical flow - Easy to understand - Focus on essential information 4. Important Rules: - DO NOT show your reasoning process - DO NOT include meta-commentary - DO NOT explain your methodology - DO NOT use phrases like "this summary shows" or "the content indicates" - Start directly with the content summary - Make sure bullet points (•) are in the same line with text - Use ## for main section headers Remember: Focus on delivering the information directly without any meta-analysis or explanation of your process.`, iconPosition: { y: 20 }, shortcut: 'option+a' }; // 初始化配置 async function initConfig() { config.apiUrl = await scriptHandler.getValue('apiUrl', config.apiUrl); config.apiKey = await scriptHandler.getValue('apiKey', config.apiKey); config.model = await scriptHandler.getValue('model', config.model); config.prompt = await scriptHandler.getValue('prompt', config.prompt); config.iconPosition = await scriptHandler.getValue('iconPosition', config.iconPosition); config.shortcut = await scriptHandler.getValue('shortcut', config.shortcut); config.theme = await scriptHandler.getValue('theme', config.theme); } // DOM 元素引用 const elements = { icon: null, container: null, settings: null, backdrop: null }; // 全局变量用于判断是否已经监听了键盘事件 let keyboardListenerActive = false; // 拖拽功能 function makeDraggable(element) { const header = element.querySelector('div') || element; let startX = 0, startY = 0; let elementX = 0, elementY = 0; let dragging = false; let lastTouchTime = 0; // 处理触摸事件 function handleTouchStart(e) { const touch = e.touches[0]; const currentTime = new Date().getTime(); const tapLength = currentTime - lastTouchTime; // 检测双击 if (tapLength < 500 && tapLength > 0) { e.preventDefault(); return; } lastTouchTime = currentTime; startDrag(touch); } function handleTouchMove(e) { if (!dragging) return; e.preventDefault(); const touch = e.touches[0]; move(touch); } function handleTouchEnd() { stopDrag(); } // 处理鼠标事件 function handleMouseDown(e) { if (e.button !== 0) return; // 只响应左键 e.preventDefault(); startDrag(e); } function handleMouseMove(e) { if (!dragging) return; e.preventDefault(); move(e); } function handleMouseUp() { stopDrag(); } function startDrag(e) { dragging = true; // 记录起始位置 startX = e.clientX; startY = e.clientY; const rect = element.getBoundingClientRect(); elementX = rect.left; elementY = rect.top; // 设置样式 if (element.id === 'website-summary-icon') { element.style.transition = 'none'; element.style.opacity = '0.9'; } // 添加事件监听 if (browserSupport.isMobile) { document.addEventListener('touchmove', handleTouchMove, { passive: false }); document.addEventListener('touchend', handleTouchEnd); document.addEventListener('touchcancel', handleTouchEnd); } else { document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); } } function move(e) { // 计算新位置 const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; if (element.id === 'website-summary-icon') { // 仅垂直移动图标 const newY = elementY + deltaY; const maxY = window.innerHeight - element.offsetHeight - 10; element.style.top = Math.max(10, Math.min(newY, maxY)) + 'px'; } else { // 自由移动其他元素 const maxX = window.innerWidth - element.offsetWidth; const maxY = window.innerHeight - element.offsetHeight; element.style.left = Math.max(0, Math.min(elementX + deltaX, maxX)) + 'px'; element.style.top = Math.max(0, Math.min(elementY + deltaY, maxY)) + 'px'; element.style.transform = 'none'; } } function stopDrag() { if (!dragging) return; dragging = false; // 恢复样式 if (element.id === 'website-summary-icon') { element.style.transition = 'transform 0.2s ease, box-shadow 0.2s ease'; element.style.opacity = '1'; // 保存图标位置 config.iconPosition = { y: element.offsetTop }; scriptHandler.setValue('iconPosition', config.iconPosition); } // 移除事件监听 if (browserSupport.isMobile) { document.removeEventListener('touchmove', handleTouchMove); document.removeEventListener('touchend', handleTouchEnd); document.removeEventListener('touchcancel', handleTouchEnd); } else { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); } } // 添加事件监听 if (browserSupport.isMobile) { header.addEventListener('touchstart', handleTouchStart, { passive: false }); } else { header.addEventListener('mousedown', handleMouseDown); } // 防止iOS Safari的滚动橡皮筋效果 if (browserSupport.isMobile) { document.body.addEventListener('touchmove', function(e) { if (dragging) { e.preventDefault(); } }, { passive: false }); } } // 显示提示消息 function showToast(message) { const toast = document.createElement('div'); toast.textContent = message; const baseStyle = ` position: fixed; left: 50%; transform: translateX(-50%); background: #4CAF50; color: white; padding: ${browserSupport.isMobile ? '12px 24px' : '10px 20px'}; border-radius: 4px; z-index: 1000001; font-size: ${browserSupport.isMobile ? '16px' : '14px'}; box-shadow: 0 2px 5px rgba(0,0,0,0.2); text-align: center; max-width: ${browserSupport.isMobile ? '90%' : '300px'}; word-break: break-word; `; // 在移动设备上显示在底部,否则显示在顶部 const position = browserSupport.isMobile ? 'bottom: 80px;' : 'top: 20px;'; toast.style.cssText = baseStyle + position; document.body.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; toast.style.transition = 'opacity 0.3s ease'; setTimeout(() => toast.remove(), 300); }, 2000); } // 快捷键处理 const keyManager = { setup() { try { // 移除旧的监听器 if (keyboardListenerActive) { document.removeEventListener('keydown', this._handleKeyDown); } // 添加新的监听器 this._handleKeyDown = (e) => { // 忽略输入框中的按键 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable || e.target.getAttribute('role') === 'textbox') { return; } // 解析配置的快捷键 const shortcutParts = config.shortcut.toLowerCase().split('+'); // 获取主键(非修饰键) const mainKey = shortcutParts.filter(part => !['alt', 'option', 'ctrl', 'control', 'shift', 'cmd', 'command', 'meta'] .includes(part) )[0] || 'a'; // 检查所需的修饰键 const needAlt = shortcutParts.some(p => p === 'alt' || p === 'option'); const needCtrl = shortcutParts.some(p => p === 'ctrl' || p === 'control'); const needShift = shortcutParts.some(p => p === 'shift'); const needMeta = shortcutParts.some(p => p === 'cmd' || p === 'command' || p === 'meta'); // 检查按键是否匹配 const isMainKeyMatched = e.key.toLowerCase() === mainKey || e.code.toLowerCase() === 'key' + mainKey || e.keyCode === mainKey.toUpperCase().charCodeAt(0); // 检查修饰键是否匹配 const modifiersMatch = e.altKey === needAlt && e.ctrlKey === needCtrl && e.shiftKey === needShift && e.metaKey === needMeta; if (isMainKeyMatched && modifiersMatch) { console.log('快捷键触发成功:', config.shortcut); e.preventDefault(); e.stopPropagation(); showSummary(); return false; } }; // 使用捕获阶段来确保我们能先捕获到事件 document.addEventListener('keydown', this._handleKeyDown, true); keyboardListenerActive = true; // 设置全局访问方法 window.activateSummary = showSummary; console.log('快捷键已设置:', config.shortcut); return true; } catch (error) { console.error('设置快捷键失败:', error); return false; } } }; // 等待页面加载完成 function waitForPageLoad() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeScript); } else { initializeScript(); } } // 初始化脚本 async function initializeScript() { try { // 等待marked库加载 await waitForMarked(); // 初始化配置 await initConfig(); // 添加全局样式 addGlobalStyles(); // 创建图标 createIcon(); // 设置快捷键 keyManager.setup(); // 注册菜单命令 registerMenuCommands(); console.log('AI Page Summarizer Pro 初始化完成'); } catch (error) { console.error('初始化失败:', error); } } // 等待marked库加载 function waitForMarked() { return new Promise((resolve) => { if (window.marked) { window.marked.setOptions({ breaks: true, gfm: true }); resolve(); } else { const checkMarked = setInterval(() => { if (window.marked) { clearInterval(checkMarked); window.marked.setOptions({ breaks: true, gfm: true }); resolve(); } }, 100); // 10秒后超时 setTimeout(() => { clearInterval(checkMarked); console.warn('marked库加载超时,继续初始化'); resolve(); }, 10000); } }); } // 添加全局样式 function addGlobalStyles() { const css = ` #website-summary-icon * { box-sizing: border-box !important; margin: 0 !important; padding: 0 !important; } #website-summary-icon span { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; line-height: 1 !important; } `; scriptHandler.addStyle(css); } // 创建图标 function createIcon() { // 检查是否已存在图标 const existingIcon = document.getElementById('website-summary-icon'); if (existingIcon) { existingIcon.remove(); } // 创建图标元素 const icon = document.createElement('div'); icon.id = 'website-summary-icon'; icon.innerHTML = '💡'; icon.style.cssText = ` position: fixed; z-index: 2147483647 !important; bottom: 20px; right: 20px; width: auto !important; height: auto !important; padding: 8px !important; font-size: ${browserSupport.isMobile ? '20px' : '24px'} !important; line-height: 1 !important; cursor: pointer !important; user-select: none !important; -webkit-user-select: none !important; visibility: visible !important; opacity: 0.8; transition: opacity 0.3s ease !important; border-radius: 8px !important; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1) !important; `; // 添加鼠标悬停效果 icon.addEventListener('mouseover', () => { icon.style.opacity = '1'; }); icon.addEventListener('mouseout', () => { icon.style.opacity = '0.8'; }); // 添加点击事件 icon.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); await showSummary(); }); // 阻止右键菜单 icon.addEventListener('contextmenu', (e) => { e.preventDefault(); }); // 添加拖动功能 makeDraggable(icon); // 确保 body 存在后再添加图标 if (document.body) { document.body.appendChild(icon); } } // 显示设置界面 function showSettings() { const settings = elements.settings || createSettingsUI(); settings.style.display = 'block'; } // 显示摘要 async function showSummary() { const container = elements.container || createSummaryUI(); const content = container.querySelector('#website-summary-content'); // 显示容器和背景 showBackdrop(); container.style.display = 'block'; setTimeout(() => container.style.opacity = '1', 10); // 显示加载中 content.innerHTML = `
正在获取总结...
`; try { // 获取页面内容 const pageContent = getPageContent(); if (!pageContent || pageContent.trim().length === 0) { throw new Error('无法获取页面内容'); } console.log('页面内容长度:', pageContent.length); console.log('API配置:', { url: config.apiUrl, model: config.model, contentLength: pageContent.length }); // 获取总结 const summary = await getSummary(pageContent); if (!summary || summary.trim().length === 0) { throw new Error('API返回内容为空'); } // 添加样式并渲染内容 addMarkdownStyles(); await renderContent(summary); } catch (error) { console.error('总结失败:', error); content.innerHTML = `
获取总结失败:${error.message}
请检查控制台以获取详细错误信息
${text}
`; }; // 自定义代码块渲染 renderer.code = function(code, language) { return `${code}
`;
};
// 自定义引用块渲染
renderer.blockquote = function(quote) {
return `${quote}`; }; // 设置渲染器 marked.setOptions({ renderer }); } // 修改 Markdown 样式 function addMarkdownStyles() { const styleId = 'website-summary-markdown-styles'; if (document.getElementById(styleId)) return; const isDark = config.theme === 'dark'; const style = document.createElement('style'); style.id = styleId; // 定义颜色变量 const colors = { light: { text: '#2c3e50', background: '#ffffff', border: '#e2e8f0', link: '#2563eb', linkHover: '#1d4ed8', code: '#f8fafc', codeBorder: '#e2e8f0', blockquote: '#f8fafc', blockquoteBorder: '#3b82f6', heading: '#1e293b', hr: '#e2e8f0', marker: '#64748b' }, dark: { text: '#e2e8f0', background: '#1e293b', border: '#334155', link: '#60a5fa', linkHover: '#93c5fd', code: '#1e293b', codeBorder: '#334155', blockquote: '#1e293b', blockquoteBorder: '#60a5fa', heading: '#f1f5f9', hr: '#334155', marker: '#94a3b8' } }; const c = isDark ? colors.dark : colors.light; style.textContent = ` #website-summary-content { font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Arial, sans-serif; line-height: 1.7; color: ${c.text}; font-size: 15px; padding: 20px; max-width: 800px; margin: 0 auto; } #website-summary-content h2 { font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Helvetica Neue", Arial, sans-serif; font-weight: 600; line-height: 1.3; margin: 1.8em 0 1em; color: ${c.heading}; font-size: 1.6em; letter-spacing: -0.01em; } #website-summary-content h3 { font-size: 1.3em; margin: 1.5em 0 0.8em; color: ${c.heading}; font-weight: 600; line-height: 1.4; } #website-summary-content p { margin: 0.8em 0; line-height: 1.75; letter-spacing: 0.01em; } #website-summary-content ul { margin: 0.6em 0; padding-left: 0.5em; list-style: none; } #website-summary-content ul li { display: flex; align-items: baseline; margin: 0.4em 0; line-height: 1.6; letter-spacing: 0.01em; } #website-summary-content ul li .bullet { color: ${c.marker}; margin-right: 0.7em; font-weight: normal; flex-shrink: 0; } #website-summary-content ul li .text { flex: 1; } #website-summary-content blockquote { margin: 1.2em 0; padding: 0.8em 1.2em; background: ${c.blockquote}; border-left: 4px solid ${c.blockquoteBorder}; border-radius: 6px; color: ${isDark ? '#cbd5e1' : '#475569'}; font-style: italic; } #website-summary-content blockquote p { margin: 0.4em 0; } #website-summary-content code { font-family: "SF Mono", Menlo, Monaco, Consolas, monospace; font-size: 0.9em; background: ${c.code}; border: 1px solid ${c.codeBorder}; border-radius: 4px; padding: 0.2em 0.4em; } #website-summary-content pre { background: ${c.code}; border: 1px solid ${c.codeBorder}; border-radius: 8px; padding: 1.2em; overflow-x: auto; margin: 1.2em 0; } #website-summary-content pre code { background: none; border: none; padding: 0; font-size: 0.9em; line-height: 1.6; } #website-summary-content strong { font-weight: 600; color: ${isDark ? '#f1f5f9' : '#1e293b'}; } #website-summary-content em { font-style: italic; color: ${isDark ? '#cbd5e1' : '#475569'}; } #website-summary-content hr { margin: 2em 0; border: none; border-top: 1px solid ${c.hr}; } #website-summary-content table { width: 100%; border-collapse: collapse; margin: 1.2em 0; font-size: 0.95em; } #website-summary-content th, #website-summary-content td { padding: 0.8em; border: 1px solid ${c.border}; text-align: left; } #website-summary-content th { background: ${c.code}; font-weight: 600; } #website-summary-content img { max-width: 100%; height: auto; border-radius: 8px; margin: 1em 0; } @media (max-width: 768px) { #website-summary-content { font-size: 14px; padding: 16px; } #website-summary-content h2 { font-size: 1.4em; } #website-summary-content h3 { font-size: 1.2em; } } `; document.head.appendChild(style); } // 修改渲染内容函数 async function renderContent(content) { const container = document.getElementById('website-summary-content'); if (!container) return; try { if (!content || content.trim().length === 0) { throw new Error('内容为空'); } // 确保 marked 已加载并配置 if (typeof marked === 'undefined') { throw new Error('Markdown 渲染器未加载'); } // 配置 marked configureMarked(); // 渲染 Markdown const html = marked.parse(content); // 清空容器 container.innerHTML = ''; // 创建临时容器 const temp = document.createElement('div'); temp.innerHTML = html; // 逐段落输出,提升性能 const fragments = Array.from(temp.children); const typeDelay = 20; // 每段延迟(毫秒) for (let fragment of fragments) { container.appendChild(fragment); await new Promise(resolve => setTimeout(resolve, typeDelay)); } } catch (error) { console.error('渲染内容失败:', error); container.innerHTML = `
渲染内容失败:${error.message}
请刷新页面重试