// ==UserScript== // @name AI Page Summarizer Pro // @name:zh-CN AI网页内容智能总结助手 // @namespace http://tampermonkey.net/ // @version 0.9.9.2 // @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() { // 检查是否已存在图标 let icon = document.getElementById('website-summary-icon'); if (icon) { icon.remove(); } icon = document.createElement('div'); icon.id = 'website-summary-icon'; // 使用星星emoji icon.innerHTML = `🌟`; // 基础样式 let styles = ` position: fixed !important; z-index: 2147483647 !important; width: ${browserSupport.isMobile ? '48px' : '40px'} !important; height: ${browserSupport.isMobile ? '48px' : '40px'} !important; border-radius: ${browserSupport.isMobile ? '24px' : '20px'} !important; background: rgba(255, 255, 255, ${browserSupport.isSafari ? '0.95' : '0.9'}) !important; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important; display: flex !important; align-items: center !important; justify-content: center !important; cursor: pointer !important; transition: transform 0.2s ease, box-shadow 0.2s ease !important; right: ${browserSupport.isMobile ? '16px' : '20px'} !important; top: ${config.iconPosition.y || (browserSupport.isMobile ? '80px' : '20px')}px !important; touch-action: none !important; will-change: transform !important; user-select: none !important; -webkit-user-select: none !important; -webkit-tap-highlight-color: transparent !important; pointer-events: auto !important; visibility: visible !important; opacity: 1 !important; `; // 添加backdrop-filter(如果支持) if (browserSupport.hasBackdropFilter) { styles += ` backdrop-filter: blur(5px) !important; -webkit-backdrop-filter: blur(5px) !important; `; } icon.style.cssText = styles; // 添加事件监听器 icon.addEventListener('mouseover', () => { icon.style.transform = 'scale(1.1)'; icon.style.boxShadow = '0 4px 12px rgba(255, 215, 0, 0.3)'; }); icon.addEventListener('mouseout', () => { icon.style.transform = 'scale(1)'; icon.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.1)'; }); icon.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); showSummary(); }); icon.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); showSettings(); }); // 确保body存在 if (!document.body) { document.addEventListener('DOMContentLoaded', () => { document.body.appendChild(icon); makeDraggable(icon); }); } else { document.body.appendChild(icon); makeDraggable(icon); } elements.icon = icon; return 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}
请检查控制台以获取详细错误信息

`; } } // 创建/显示背景 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; `; // 添加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'}; `; // 标题 const title = document.createElement('h3'); title.textContent = '网页总结'; title.style.cssText = `margin: 0; font-size: 18px; color: ${isDark ? '#e8eaed' : '#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: ${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', () => { 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', 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'}; `; // 组装界面 buttonContainer.appendChild(copyBtn); buttonContainer.appendChild(closeBtn); header.appendChild(title); header.appendChild(buttonContainer); container.appendChild(header); container.appendChild(content); document.body.appendChild(container); makeDraggable(container); elements.container = container; return container; } // 创建设置界面 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; 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.cssText = `margin: 0; color: ${isDark ? '#e8eaed' : '#333'};`; 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', () => settingsContainer.style.display = 'none'); // 表单 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); makeDraggable(settingsContainer); 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(); })();