// ==UserScript== // @name AI Page Summarizer Pro // @name:zh-CN AI网页内容智能总结助手 // @namespace http://tampermonkey.net/ // @version 0.9.9.7 // @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', summaryWindowPositioned: false // 添加窗口位置状态跟踪 }; // 初始化配置 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 || { y: 20 }); config.shortcut = await scriptHandler.getValue('shortcut', config.shortcut); config.theme = await scriptHandler.getValue('theme', config.theme); config.summaryWindowPositioned = await scriptHandler.getValue('summaryWindowPositioned', false); console.log('加载的图标位置配置:', config.iconPosition); } // DOM 元素引用 const elements = { icon: null, container: null, settings: null, backdrop: null }; // 全局变量用于判断是否已经监听了键盘事件 let keyboardListenerActive = false; // 拖拽功能 function makeDraggable(element) { let isDragging = false; let startX, startY, startLeft, startTop; // 添加位置信息以便拖动 if (element.style.position !== 'fixed') { element.style.position = 'fixed'; } // 确保元素有初始位置 if (!element.style.bottom || !element.style.right) { element.style.bottom = '20px'; element.style.right = '20px'; } // 鼠标/触摸开始事件 function handleStart(e) { isDragging = true; // 计算起始位置 const rect = element.getBoundingClientRect(); // 将位置从right/bottom转换为left/top以便计算 const computedStyle = window.getComputedStyle(element); const right = parseInt(computedStyle.right || '0'); const bottom = parseInt(computedStyle.bottom || '0'); startLeft = window.innerWidth - right - rect.width; startTop = window.innerHeight - bottom - rect.height; // 记录鼠标/触摸起始位置 if (e.type === 'touchstart') { startX = e.touches[0].clientX; startY = e.touches[0].clientY; } else { startX = e.clientX; startY = e.clientY; e.preventDefault(); // 防止选中文本 } // 添加移动和结束事件监听 if (e.type === 'touchstart') { document.addEventListener('touchmove', handleMove, { passive: false }); document.addEventListener('touchend', handleEnd); } else { document.addEventListener('mousemove', handleMove); document.addEventListener('mouseup', handleEnd); } // 设置拖动中的样式 element.style.transition = 'none'; element.style.opacity = '1'; } // 鼠标/触摸移动事件 function handleMove(e) { if (!isDragging) return; let moveX, moveY; if (e.type === 'touchmove') { moveX = e.touches[0].clientX - startX; moveY = e.touches[0].clientY - startY; e.preventDefault(); // 防止页面滚动 } else { moveX = e.clientX - startX; moveY = e.clientY - startY; } // 计算新位置 let newLeft = startLeft + moveX; let newTop = startTop + moveY; // 边界检查 newLeft = Math.max(0, Math.min(window.innerWidth - element.offsetWidth, newLeft)); newTop = Math.max(0, Math.min(window.innerHeight - element.offsetHeight, newTop)); // 更新位置 (使用left/top) element.style.left = `${newLeft}px`; element.style.top = `${newTop}px`; // 清除right/bottom,避免冲突 element.style.right = ''; element.style.bottom = ''; } // 鼠标/触摸结束事件 function handleEnd() { if (!isDragging) return; isDragging = false; // 移除事件监听 document.removeEventListener('mousemove', handleMove); document.removeEventListener('mouseup', handleEnd); document.removeEventListener('touchmove', handleMove); document.removeEventListener('touchend', handleEnd); // 保存新位置 const rect = element.getBoundingClientRect(); const newX = rect.left; const newY = rect.top; // 转换回right/bottom定位 const right = window.innerWidth - newX - rect.width; const bottom = window.innerHeight - newY - rect.height; // 设置新位置,恢复transition element.style.left = ''; element.style.top = ''; element.style.right = `${right}px`; element.style.bottom = `${bottom}px`; element.style.transition = 'opacity 0.3s ease'; // 如果不是在悬停状态,恢复默认透明度 if (!element.matches(':hover')) { element.style.opacity = '0.8'; } // 保存位置到配置 config.iconPosition = { x: newX, y: newY }; saveConfig(); } // 添加事件监听 element.addEventListener('mousedown', handleStart); element.addEventListener('touchstart', handleStart, { 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 = '💡'; // 从配置中获取保存的位置 const savedPosition = config.iconPosition || {}; const hasValidPosition = typeof savedPosition.x === 'number' && typeof savedPosition.y === 'number'; // 计算位置样式 let positionStyle = ''; if (hasValidPosition) { // 使用保存的精确位置 positionStyle = ` top: ${savedPosition.y}px !important; left: ${savedPosition.x}px !important; right: auto !important; bottom: auto !important; `; } else { // 使用默认位置 positionStyle = ` bottom: 20px !important; right: 20px !important; `; } // 设置图标样式 icon.style.cssText = ` position: fixed; z-index: 2147483647 !important; ${positionStyle} 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(); e.stopPropagation(); showSettings(); }); // 支持双击打开设置(为Safari增加额外的交互方式) let lastClickTime = 0; icon.addEventListener('click', (e) => { const currentTime = new Date().getTime(); if (currentTime - lastClickTime < 300) { // 双击间隔300ms e.preventDefault(); e.stopPropagation(); showSettings(); } lastClickTime = currentTime; }); // 添加优化的拖动功能 makeIconDraggable(icon); // 确保 body 存在后再添加图标 if (document.body) { document.body.appendChild(icon); } else { document.addEventListener('DOMContentLoaded', () => { document.body.appendChild(icon); }); } // 将图标引用存储到elements对象中 elements.icon = icon; } // 专门为图标设计的拖动函数 function makeIconDraggable(icon) { let isDragging = false; let startX, startY, startLeft, startTop; // 鼠标/触摸开始事件 function handleStart(e) { isDragging = true; // 记录初始位置 const rect = icon.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; // 记录鼠标/触摸起始位置 if (e.type === 'touchstart') { startX = e.touches[0].clientX; startY = e.touches[0].clientY; } else { startX = e.clientX; startY = e.clientY; e.preventDefault(); // 防止选中文本 } // 设置拖动时的样式 icon.style.transition = 'none'; icon.style.opacity = '1'; // 添加移动和结束事件监听 if (e.type === 'touchstart') { document.addEventListener('touchmove', handleMove, { passive: false }); document.addEventListener('touchend', handleEnd); } else { document.addEventListener('mousemove', handleMove); document.addEventListener('mouseup', handleEnd); } } // 鼠标/触摸移动事件 function handleMove(e) { if (!isDragging) return; let moveX, moveY; if (e.type === 'touchmove') { moveX = e.touches[0].clientX - startX; moveY = e.touches[0].clientY - startY; e.preventDefault(); // 防止页面滚动 } else { moveX = e.clientX - startX; moveY = e.clientY - startY; } // 计算新位置 let newLeft = startLeft + moveX; let newTop = startTop + moveY; // 边界检查 newLeft = Math.max(0, Math.min(window.innerWidth - icon.offsetWidth, newLeft)); newTop = Math.max(0, Math.min(window.innerHeight - icon.offsetHeight, newTop)); // 更新位置 icon.style.left = `${newLeft}px`; icon.style.top = `${newTop}px`; icon.style.right = 'auto'; icon.style.bottom = 'auto'; } // 鼠标/触摸结束事件 function handleEnd() { if (!isDragging) return; isDragging = false; // 移除事件监听 document.removeEventListener('mousemove', handleMove); document.removeEventListener('mouseup', handleEnd); document.removeEventListener('touchmove', handleMove); document.removeEventListener('touchend', handleEnd); // 保存新位置 const rect = icon.getBoundingClientRect(); config.iconPosition = { x: rect.left, y: rect.top }; // 持久化保存位置 saveIconPosition(); // 恢复透明度过渡效果 icon.style.transition = 'opacity 0.3s ease'; if (!icon.matches(':hover')) { icon.style.opacity = '0.8'; } } // 添加事件监听 icon.addEventListener('mousedown', handleStart); icon.addEventListener('touchstart', handleStart, { passive: false }); // 处理窗口大小变化 window.addEventListener('resize', () => { const rect = icon.getBoundingClientRect(); // 如果图标超出视口范围,调整位置 if (rect.right > window.innerWidth) { icon.style.left = `${window.innerWidth - icon.offsetWidth}px`; } if (rect.bottom > window.innerHeight) { icon.style.top = `${window.innerHeight - icon.offsetHeight}px`; } // 更新保存的位置 config.iconPosition = { x: parseInt(icon.style.left), y: parseInt(icon.style.top) }; // 持久化保存位置 saveIconPosition(); }); } // 保存图标位置 function saveIconPosition() { scriptHandler.setValue('iconPosition', config.iconPosition); console.log('图标位置已保存:', config.iconPosition); } // 显示设置界面 function showSettings() { try { const settings = elements.settings || createSettingsUI(); settings.style.display = 'block'; showBackdrop(); setTimeout(() => settings.style.opacity = '1', 10); } catch (error) { console.error('显示设置界面失败:', error); alert('无法显示设置界面,请检查控制台以获取详细信息'); } } // 显示摘要 async function showSummary() { const container = elements.container || createSummaryUI(); const content = container.querySelector('#website-summary-content'); // 如果容器有自定义位置,保持原位置;否则重置到屏幕中心 const hasCustomPosition = container.hasAttribute('data-positioned'); if (!hasCustomPosition) { container.style.left = '50%'; container.style.top = '50%'; container.style.transform = 'translate(-50%, -50%)'; } // 显示容器和背景 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}
请检查控制台以获取详细错误信息

`; } } // 创建/显示背景 function showBackdrop() { if (!elements.backdrop) { const backdrop = document.createElement('div'); backdrop.id = 'website-summary-backdrop'; const isDark = config.theme === 'dark'; backdrop.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: ${isDark ? 'rgba(32, 33, 36, 0.75)' : 'rgba(250, 250, 252, 0.75)'}; backdrop-filter: blur(5px); z-index: 999997; display: none; opacity: 0; transition: opacity 0.3s ease; `; backdrop.addEventListener('click', (e) => { if (e.target === backdrop) { hideUI(); } }); document.body.appendChild(backdrop); elements.backdrop = backdrop; } else { // 更新背景颜色以匹配当前主题 const isDark = config.theme === 'dark'; elements.backdrop.style.backgroundColor = isDark ? 'rgba(32, 33, 36, 0.75)' : 'rgba(250, 250, 252, 0.75)'; } elements.backdrop.style.display = 'block'; setTimeout(() => elements.backdrop.style.opacity = '1', 10); } // 隐藏UI function hideUI() { // 隐藏背景 if (elements.backdrop) { elements.backdrop.style.opacity = '0'; setTimeout(() => elements.backdrop.style.display = 'none', 300); } // 隐藏摘要容器 if (elements.container) { elements.container.style.opacity = '0'; setTimeout(() => elements.container.style.display = 'none', 300); } } // 创建摘要UI function createSummaryUI() { const container = document.createElement('div'); container.id = 'website-summary-container'; const isDark = config.theme === 'dark'; let styles = ` position: fixed; z-index: 999998; background: ${isDark ? darkColors.containerBg : 'rgba(255, 255, 255, 0.98)'}; color: ${isDark ? darkColors.text : '#333'}; border-radius: ${browserSupport.isMobile ? '8px' : '12px'}; box-shadow: 0 8px 32px ${isDark ? 'rgba(0, 0, 0, 0.4)' : 'rgba(0, 0, 0, 0.08)'}; padding: ${browserSupport.isMobile ? '12px' : '16px'}; width: ${browserSupport.isMobile ? '92%' : '80%'}; max-width: ${browserSupport.isMobile ? '100%' : '800px'}; max-height: ${browserSupport.isMobile ? '85vh' : '80vh'}; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; display: none; left: 50%; top: 50%; transform: translate(-50%, -50%); overflow: hidden; opacity: 0; transition: opacity 0.3s ease; will-change: transform; -webkit-backface-visibility: hidden; backface-visibility: hidden; `; // 添加backdrop-filter(如果支持) if (browserSupport.hasBackdropFilter) { styles += 'backdrop-filter: blur(10px);'; styles += '-webkit-backdrop-filter: blur(10px);'; } container.style.cssText = styles; // 标题栏 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 ${isDark ? '#555' : '#eee'}; user-select: none; -webkit-user-select: none; `; // 标题 const title = document.createElement('h3'); title.textContent = '网页总结'; title.style.cssText = ` margin: 0; font-size: 18px; color: ${isDark ? '#e8eaed' : '#333'}; pointer-events: none; `; // 按钮容器 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: ${isDark ? '#8ab4f8' : '#4CAF50'}; color: ${isDark ? '#202124' : '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 = isDark ? '#aecbfa' : '#45a049'; }); copyBtn.addEventListener('mouseout', () => { copyBtn.style.backgroundColor = isDark ? '#8ab4f8' : '#4CAF50'; }); copyBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const content = document.getElementById('website-summary-content').innerText; navigator.clipboard.writeText(content).then(() => { copyBtn.textContent = '已复制'; setTimeout(() => copyBtn.textContent = '复制', 2000); }); }); // 关闭按钮 const closeBtn = document.createElement('button'); closeBtn.textContent = '×'; closeBtn.style.cssText = ` background: none; border: none; font-size: 24px; cursor: pointer; padding: 0 8px; color: ${isDark ? '#e8eaed' : '#666'}; transition: color 0.2s; `; closeBtn.addEventListener('mouseover', () => closeBtn.style.color = '#ff4444'); closeBtn.addEventListener('mouseout', () => closeBtn.style.color = isDark ? '#e8eaed' : '#666'); closeBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); hideUI(); }); // 内容区域 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; color: ${isDark ? '#e8eaed' : '#333'}; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; `; // 防止内容区域的滚动触发容器拖动 content.addEventListener('mousedown', (e) => { e.stopPropagation(); }); content.addEventListener('touchstart', (e) => { e.stopPropagation(); }, { passive: true }); // 组装界面 buttonContainer.appendChild(copyBtn); buttonContainer.appendChild(closeBtn); header.appendChild(title); header.appendChild(buttonContainer); container.appendChild(header); container.appendChild(content); document.body.appendChild(container); // 专门使用标题栏拖动 makeDraggableByHeader(container, header); elements.container = container; return container; } // 专门用于通过标题栏拖动的函数 function makeDraggableByHeader(element, handle) { let isDragging = false; let startX, startY, startLeft, startTop; // 鼠标/触摸开始事件 function handleStart(e) { isDragging = true; // 记录初始位置 const rect = element.getBoundingClientRect(); // 如果使用了transform-translate,则切换到绝对定位 if (element.style.transform && element.style.transform.includes('translate')) { element.style.transform = 'none'; element.style.left = rect.left + 'px'; element.style.top = rect.top + 'px'; // 标记元素已被手动定位 element.setAttribute('data-positioned', 'true'); } startLeft = rect.left; startTop = rect.top; // 记录鼠标/触摸起始位置 if (e.type === 'touchstart') { startX = e.touches[0].clientX; startY = e.touches[0].clientY; // 阻止默认行为只在触摸时需要 e.preventDefault(); } else { startX = e.clientX; startY = e.clientY; e.preventDefault(); } // 移除过渡效果 element.style.transition = 'none'; // 添加移动和结束事件监听 if (e.type === 'touchstart') { document.addEventListener('touchmove', handleMove, { passive: false }); document.addEventListener('touchend', handleEnd); document.addEventListener('touchcancel', handleEnd); } else { document.addEventListener('mousemove', handleMove); document.addEventListener('mouseup', handleEnd); } } // 鼠标/触摸移动事件 function handleMove(e) { if (!isDragging) return; let moveX, moveY; if (e.type === 'touchmove') { moveX = e.touches[0].clientX - startX; moveY = e.touches[0].clientY - startY; // 阻止默认滚动 e.preventDefault(); } else { moveX = e.clientX - startX; moveY = e.clientY - startY; } // 计算新位置 const newLeft = startLeft + moveX; const newTop = startTop + moveY; // 边界检查 const maxLeft = window.innerWidth - element.offsetWidth; const maxTop = window.innerHeight - element.offsetHeight; // 应用新位置 element.style.left = Math.max(0, Math.min(newLeft, maxLeft)) + 'px'; element.style.top = Math.max(0, Math.min(newTop, maxTop)) + 'px'; // 标记元素已被手动定位 element.setAttribute('data-positioned', 'true'); } // 鼠标/触摸结束事件 function handleEnd() { if (!isDragging) return; isDragging = false; // 移除事件监听 document.removeEventListener('mousemove', handleMove); document.removeEventListener('mouseup', handleEnd); document.removeEventListener('touchmove', handleMove); document.removeEventListener('touchend', handleEnd); document.removeEventListener('touchcancel', handleEnd); // 恢复过渡效果 element.style.transition = 'opacity 0.3s ease'; // 保存位置状态 saveWindowPosition(element); } // 保存窗口位置 function saveWindowPosition(element) { if (element.id === 'website-summary-container') { const rect = element.getBoundingClientRect(); // 只保存位置标记状态,不保存具体位置 config.summaryWindowPositioned = true; scriptHandler.setValue('summaryWindowPositioned', true); } } // 仅在指定的标题栏上添加事件监听 handle.addEventListener('mousedown', handleStart); handle.addEventListener('touchstart', handleStart, { passive: false }); // 处理窗口变化 window.addEventListener('resize', () => { if (element.hasAttribute('data-positioned')) { const rect = element.getBoundingClientRect(); // 如果窗口超出视口边界,调整位置 if (rect.right > window.innerWidth) { element.style.left = Math.max(0, window.innerWidth - element.offsetWidth) + 'px'; } if (rect.bottom > window.innerHeight) { element.style.top = Math.max(0, window.innerHeight - element.offsetHeight) + 'px'; } } }); // 如果用户离开窗口,确保释放拖动状态 window.addEventListener('blur', () => { if (isDragging) { handleEnd(); } }); // 检查是否应该恢复自定义位置 if (element.id === 'website-summary-container' && config.summaryWindowPositioned) { // 标记元素有自定义位置 element.setAttribute('data-positioned', 'true'); } } // 创建设置界面 function createSettingsUI() { const settingsContainer = document.createElement('div'); settingsContainer.id = 'website-summary-settings'; // 基础样式 const isDark = config.theme === 'dark'; settingsContainer.style.cssText = ` position: fixed; z-index: 1000000; background: ${isDark ? 'rgba(32, 33, 36, 0.98)' : 'rgba(255, 255, 255, 0.98)'}; color: ${isDark ? '#e8eaed' : '#333'}; border-radius: 12px; box-shadow: 0 4px 20px ${isDark ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.15)'}; padding: 20px; width: 400px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; display: none; left: 50%; top: 50%; transform: translate(-50%, -50%); will-change: transform; -webkit-backface-visibility: hidden; backface-visibility: hidden; `; if (browserSupport.hasBackdropFilter) { settingsContainer.style.backdropFilter = 'blur(10px)'; settingsContainer.style.webkitBackdropFilter = 'blur(10px)'; } // 标题栏 const header = document.createElement('div'); header.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; cursor: move; user-select: none; -webkit-user-select: none; `; const title = document.createElement('h3'); title.textContent = '设置'; title.style.cssText = ` margin: 0; color: ${isDark ? '#e8eaed' : '#333'}; pointer-events: none; `; const closeBtn = document.createElement('button'); closeBtn.textContent = '×'; closeBtn.style.cssText = ` background: none; border: none; font-size: 24px; cursor: pointer; padding: 0 8px; color: ${isDark ? '#e8eaed' : '#666'}; `; closeBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); settingsContainer.style.display = 'none'; if (elements.backdrop) { elements.backdrop.style.opacity = '0'; setTimeout(() => elements.backdrop.style.display = 'none', 300); } }); // 表单 const form = document.createElement('form'); form.style.cssText = 'display: flex; flex-direction: column; gap: 16px;'; // 创建输入字段函数 function createField(id, label, value, type = 'text', placeholder = '') { const container = document.createElement('div'); container.style.cssText = 'display: flex; flex-direction: column; gap: 4px;'; const labelElem = document.createElement('label'); labelElem.textContent = label; labelElem.style.cssText = `font-size: 14px; color: ${isDark ? '#e8eaed' : '#333'}; font-weight: 500;`; const input = document.createElement(type === 'textarea' ? 'textarea' : 'input'); if (type !== 'textarea') input.type = type; input.id = id; input.value = value; input.placeholder = placeholder; input.autocomplete = 'off'; input.setAttribute('data-form-type', 'other'); const baseStyle = ` width: 100%; padding: 8px; border: 1px solid ${isDark ? '#555' : '#ddd'}; border-radius: 6px; font-family: inherit; background: ${isDark ? '#202124' : '#fff'}; color: ${isDark ? '#e8eaed' : '#333'}; `; input.style.cssText = type === 'textarea' ? baseStyle + 'height: 100px; resize: vertical;' : baseStyle; container.appendChild(labelElem); container.appendChild(input); return { container, input }; } // 创建主题切换 function createThemeSwitch() { const container = document.createElement('div'); container.style.cssText = 'display: flex; align-items: center; gap: 12px; margin-bottom: 16px;'; const label = document.createElement('label'); label.textContent = '主题模式:'; label.style.cssText = `font-size: 14px; color: ${isDark ? '#e8eaed' : '#333'}; font-weight: 500;`; const themeSwitch = document.createElement('div'); themeSwitch.style.cssText = 'display: flex; gap: 8px;'; const createThemeButton = (themeName, text) => { const btn = document.createElement('button'); btn.textContent = text; btn.type = 'button'; const isActive = config.theme === themeName; btn.style.cssText = ` padding: 6px 12px; border-radius: 4px; border: 1px solid ${isDark ? '#555' : '#ddd'}; background: ${isActive ? (isDark ? '#555' : '#007AFF') : 'transparent'}; color: ${isActive ? '#fff' : (isDark ? '#e8eaed' : '#333')}; cursor: pointer; transition: all 0.2s; `; btn.addEventListener('click', async () => { config.theme = themeName; await scriptHandler.setValue('theme', themeName); // 重新创建设置界面而不是移除 const oldSettings = elements.settings; elements.settings = null; showSettings(); if (oldSettings) { oldSettings.remove(); } }); return btn; }; const lightBtn = createThemeButton('light', '浅色'); const darkBtn = createThemeButton('dark', '深色'); themeSwitch.appendChild(lightBtn); themeSwitch.appendChild(darkBtn); container.appendChild(label); container.appendChild(themeSwitch); return container; } // 创建字段 const apiUrlField = createField('apiUrl', 'API URL', config.apiUrl, 'text', '输入API URL'); const apiKeyField = createField('apiKey', 'API Key', config.apiKey, 'text', '输入API Key'); const modelField = createField('model', 'AI 模型', config.model, 'text', '输入AI模型名称'); const shortcutField = createField('shortcut', '快捷键', config.shortcut, 'text', '例如: option+a, ctrl+shift+s'); const promptField = createField('prompt', '提示词', config.prompt, 'textarea', '输入提示词'); // 添加主题切换 form.appendChild(createThemeSwitch()); // 添加字段到表单 form.appendChild(apiUrlField.container); form.appendChild(apiKeyField.container); form.appendChild(modelField.container); form.appendChild(shortcutField.container); form.appendChild(promptField.container); // 保存按钮 const saveBtn = document.createElement('button'); saveBtn.textContent = '保存设置'; saveBtn.type = 'button'; saveBtn.style.cssText = ` background: ${isDark ? '#8ab4f8' : '#007AFF'}; color: ${isDark ? '#202124' : '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 = isDark ? '#aecbfa' : '#0056b3'; }); saveBtn.addEventListener('mouseout', () => { saveBtn.style.backgroundColor = isDark ? '#8ab4f8' : '#007AFF'; }); // 保存逻辑 saveBtn.addEventListener('click', async (e) => { e.preventDefault(); // 获取并验证表单值 const newApiUrl = apiUrlField.input.value.trim(); const newApiKey = apiKeyField.input.value.trim(); const newModel = modelField.input.value.trim(); const newPrompt = promptField.input.value.trim(); const newShortcut = shortcutField.input.value.trim(); if (!newApiUrl || !newApiKey) { alert('请至少填写API URL和API Key'); return; } try { // 使用scriptHandler保存设置 await scriptHandler.setValue('apiUrl', newApiUrl); await scriptHandler.setValue('apiKey', newApiKey); await scriptHandler.setValue('model', newModel); await scriptHandler.setValue('prompt', newPrompt); await scriptHandler.setValue('shortcut', newShortcut); await scriptHandler.setValue('theme', config.theme); // 更新内存配置 config.apiUrl = newApiUrl; config.apiKey = newApiKey; config.model = newModel; config.prompt = newPrompt; config.shortcut = newShortcut; // 更新快捷键 keyManager.setup(); // 显示成功提示 showToast('设置已保存'); // 关闭设置 settingsContainer.style.display = 'none'; } catch (error) { console.error('保存设置失败:', error); showToast('保存设置失败,请重试'); } }); // 组装界面 header.appendChild(title); header.appendChild(closeBtn); form.appendChild(saveBtn); settingsContainer.appendChild(header); settingsContainer.appendChild(form); document.body.appendChild(settingsContainer); // 使用优化的拖拽功能,只允许通过标题栏拖动 makeDraggableByHeader(settingsContainer, header); elements.settings = settingsContainer; return settingsContainer; } // 获取页面内容 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, 5000); } catch (error) { return document.body.innerText.slice(0, 5000); } } // 修改深色模式颜色方案 const darkColors = { background: '#242526', // 更柔和的深色背景 containerBg: '#2d2d30', // 容器背景色 text: '#e4e6eb', // 更柔和的文字颜色 secondaryText: '#b0b3b8', // 次要文字颜色 border: '#3e4042', // 边框颜色 codeBackground: '#3a3b3c', // 代码块背景 blockquoteBorder: '#4a4b4d', // 引用块边框 blockquoteText: '#cacbcc', // 引用块文字 linkColor: '#4e89e8' // 链接颜色 }; // 修改 API 调用函数 function getSummary(content) { return new Promise((resolve, reject) => { const apiKey = config.apiKey.trim(); if (!apiKey) { reject(new Error('请先设置API Key')); return; } const requestData = { model: config.model, messages: [ { role: 'system', content: '你是一个专业的网页内容总结助手,善于使用markdown格式来组织信息。' }, { role: 'user', content: config.prompt + '\n\n' + content } ], temperature: 0.7, stream: false }; // 处理 URL let apiUrl = config.apiUrl.trim(); if (!apiUrl.startsWith('http://') && !apiUrl.startsWith('https://')) { apiUrl = 'https://' + apiUrl; } // 打印请求信息用于调试 console.log('发送请求到:', apiUrl); console.log('请求数据:', JSON.stringify(requestData, null, 2)); // 发送请求 const xhr = typeof GM_xmlhttpRequest !== 'undefined' ? GM_xmlhttpRequest : (typeof GM !== 'undefined' && GM.xmlHttpRequest); if (!xhr) { reject(new Error('不支持的环境:无法发送跨域请求')); return; } xhr({ method: 'POST', url: apiUrl, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, 'Accept': 'application/json' }, data: JSON.stringify(requestData), timeout: 30000, onload: function(response) { try { console.log('收到响应:', response.status); console.log('响应头:', response.responseHeaders); console.log('响应内容:', response.responseText); if (response.status === 429) { reject(new Error('API请求过于频繁,请稍后再试')); return; } if (response.status !== 200) { reject(new Error(`API请求失败: HTTP ${response.status}`)); return; } let data; try { data = JSON.parse(response.responseText); } catch (e) { console.error('JSON解析失败:', e); reject(new Error('API响应格式错误')); return; } if (data.error) { reject(new Error('API错误: ' + (data.error.message || JSON.stringify(data.error)))); return; } // 提取内容 let content = null; if (data.choices && Array.isArray(data.choices) && data.choices.length > 0) { const choice = data.choices[0]; if (choice.message && choice.message.content) { content = choice.message.content; } else if (choice.text) { content = choice.text; } } if (!content && data.response) { content = typeof data.response === 'string' ? data.response : JSON.stringify(data.response); } if (!content && data.content) { content = data.content; } if (content) { resolve(content.trim()); } else { reject(new Error('无法从API响应中提取内容')); } } catch (error) { console.error('处理响应时出错:', error); reject(new Error('处理响应失败: ' + error.message)); } }, onerror: function(error) { console.error('请求错误:', error); reject(new Error('请求失败: ' + (error.message || '网络错误'))); }, ontimeout: function() { reject(new Error('请求超时')); } }); }); } // 配置 Marked 渲染器 function configureMarked() { if (typeof marked === 'undefined') return; // 配置 marked 选项 marked.setOptions({ gfm: true, breaks: true, headerIds: true, mangle: false, smartLists: true, smartypants: true, highlight: function(code, lang) { return code; } }); // 自定义渲染器 const renderer = new marked.Renderer(); // 自定义标题渲染 - 移除 ## 前缀 renderer.heading = function(text, level) { return `${text}`; }; // 自定义列表项渲染 renderer.listitem = function(text) { return `
  • ${text}
  • `; }; // 自定义段落渲染 renderer.paragraph = function(text) { return `

    ${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}
    请刷新页面重试

    `; } } // 添加菜单命令 function registerMenuCommands() { scriptHandler.registerMenuCommand('显示网页总结 (快捷键: ' + config.shortcut + ')', showSummary); scriptHandler.registerMenuCommand('打开设置', showSettings); } // 启动脚本 waitForPageLoad(); })();