// ==UserScript== // @name AI Page Summarizer Pro // @namespace http://tampermonkey.net/ // @version 0.9.8 // @description 网页内容智能总结,支持自定义API和提示词 // @author Your Name // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_addStyle // @require https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js // @run-at document-idle // @downloadURL none // ==/UserScript== (function() { 'use strict'; // 配置项 let config = { apiUrl: GM_getValue('apiUrl', 'https://api.openai.com/v1/chat/completions'), apiKey: GM_getValue('apiKey', ''), model: GM_getValue('model', 'gpt-3.5-turbo'), prompt: GM_getValue('prompt', `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 Markdown formatting - Start with a brief overview - Use appropriate headings (h2, h3) - Include bullet points for key points - Use bold for important terms - Use blockquotes for notable quotes - Use code blocks for technical content 2. Content Structure: - Main Topic/Title - Key Points - Important Details - Conclusions/Summary 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 5. Length Guidelines: - Overview: 1-2 sentences - Key Points: 3-5 bullet points - Important Details: 2-3 paragraphs - Summary: 1-2 sentences Remember: Focus on delivering the information directly without any meta-analysis or explanation of your process.`), iconPosition: GM_getValue('iconPosition', { y: 20 }), shortcut: GM_getValue('shortcut', 'option+a') }; // DOM 元素引用 const elements = { icon: null, container: null, settings: null, backdrop: null }; // 全局变量用于判断是否已经监听了键盘事件 let keyboardListenerActive = false; // 快捷键处理 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'); // 检查按键是否匹配 - 同时检查key和code const isMainKeyMatched = e.key.toLowerCase() === mainKey || (e.code && e.code.toLowerCase() === 'key' + mainKey); // 检查修饰键是否匹配 if (isMainKeyMatched && e.altKey === needAlt && e.ctrlKey === needCtrl && e.shiftKey === needShift && e.metaKey === needMeta) { 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; } }, // 测试快捷键是否工作 test() { try { if (!keyboardListenerActive) { console.warn('键盘监听器未激活,请先调用 keyManager.setup()'); return false; } // 解析当前快捷键 const parts = config.shortcut.toLowerCase().split('+'); const mainKey = parts.filter(part => !['alt', 'option', 'ctrl', 'control', 'shift', 'cmd', 'command', 'meta'] .includes(part) )[0] || 'a'; // 创建事件对象 const eventOptions = { key: mainKey, code: 'Key' + mainKey.toUpperCase(), altKey: parts.includes('alt') || parts.includes('option'), ctrlKey: parts.includes('ctrl') || parts.includes('control'), shiftKey: parts.includes('shift'), metaKey: parts.includes('cmd') || parts.includes('command') || parts.includes('meta'), bubbles: true, cancelable: true }; console.log('模拟快捷键按下:', JSON.stringify(eventOptions)); const event = new KeyboardEvent('keydown', eventOptions); // 由于 KeyboardEvent 的限制,某些属性可能无法正确设置,所以我们通过这种方式确认 if (!event.altKey && eventOptions.altKey) { console.warn('注意: 无法在模拟事件中设置 altKey 属性'); } // 分发事件 document.dispatchEvent(event); // 因为可能无法模拟事件,所以直接提供一个调用方法 console.log('您也可以通过控制台调用 window.activateSummary() 来直接触发'); return true; } catch (error) { console.error('测试快捷键失败:', error); return false; } } }; // 等待依赖库加载 function waitForDependencies(callback) { if (window.marked) { window.marked.setOptions({ breaks: true, gfm: true }); callback(); return; } setTimeout(() => waitForDependencies(callback), 100); } // 创建图标 function createIcon() { const icon = document.createElement('div'); icon.id = 'website-summary-icon'; // Mac风格书本图标 icon.innerHTML = ` `; icon.style.cssText = ` position: fixed; z-index: 999999; width: 40px; height: 40px; border-radius: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); display: flex; align-items: center; justify-content: center; cursor: pointer; transition: transform 0.2s ease, box-shadow 0.2s ease; backdrop-filter: blur(5px); right: 20px; top: ${config.iconPosition.y || 20}px; touch-action: none; will-change: transform; `; icon.addEventListener('mouseover', () => { icon.style.transform = 'scale(1.1)'; icon.style.boxShadow = '0 4px 12px rgba(49, 130, 206, 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', showSummary); icon.addEventListener('contextmenu', (e) => { e.preventDefault(); showSettings(); }); makeDraggable(icon); document.body.appendChild(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) throw new Error('无法获取页面内容'); const summary = await getSummary(pageContent); if (!summary) throw new Error('获取总结失败'); renderContent(summary); } catch (error) { content.innerHTML = `

获取总结失败:${error.message}
请检查API配置是否正确

`; } } // 创建/显示背景 function showBackdrop() { if (!elements.backdrop) { const backdrop = document.createElement('div'); backdrop.id = 'website-summary-backdrop'; backdrop.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: 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; } 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'; container.style.cssText = ` position: fixed; z-index: 999998; background: rgba(255, 255, 255, 0.9); border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); padding: 16px; width: 80%; max-width: 800px; max-height: 80vh; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; display: none; backdrop-filter: blur(10px); left: 50%; top: 50%; transform: translate(-50%, -50%); overflow: hidden; opacity: 0; transition: opacity 0.3s ease; `; // 标题栏 const header = document.createElement('div'); header.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; cursor: move; padding-bottom: 8px; border-bottom: 1px solid #eee; `; // 标题 const title = document.createElement('h3'); title.textContent = '网页总结'; title.style.cssText = 'margin: 0; font-size: 18px; color: #333;'; // 按钮容器 const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = 'display: flex; gap: 8px; align-items: center;'; // 复制按钮 const copyBtn = document.createElement('button'); copyBtn.textContent = '复制'; copyBtn.style.cssText = ` background: #4CAF50; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background-color 0.2s; `; copyBtn.addEventListener('mouseover', () => copyBtn.style.backgroundColor = '#45a049'); copyBtn.addEventListener('mouseout', () => copyBtn.style.backgroundColor = '#4CAF50'); copyBtn.addEventListener('click', () => { const content = document.getElementById('website-summary-content').innerText; navigator.clipboard.writeText(content).then(() => { 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: #666; transition: color 0.2s; `; closeBtn.addEventListener('mouseover', () => closeBtn.style.color = '#ff4444'); closeBtn.addEventListener('mouseout', () => closeBtn.style.color = '#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; `; // 组装界面 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'; settingsContainer.style.cssText = ` position: fixed; z-index: 1000000; background: rgba(255, 255, 255, 0.98); border-radius: 12px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); padding: 20px; width: 400px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; display: none; backdrop-filter: blur(10px); left: 50%; top: 50%; transform: translate(-50%, -50%); `; // 标题栏 const header = document.createElement('div'); header.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; cursor: move;'; const title = document.createElement('h3'); title.textContent = '设置'; title.style.margin = '0'; const closeBtn = document.createElement('button'); closeBtn.textContent = '×'; closeBtn.style.cssText = 'background: none; border: none; font-size: 24px; cursor: pointer; padding: 0 8px; color: #666;'; 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: #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 #ddd; border-radius: 6px; font-family: inherit;'; input.style.cssText = type === 'textarea' ? baseStyle + 'height: 100px; resize: vertical;' : baseStyle; container.appendChild(labelElem); container.appendChild(input); return { container, input }; } // 创建字段 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(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: #007AFF; color: white; border: none; padding: 10px; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; transition: background-color 0.2s; `; saveBtn.addEventListener('mouseover', () => saveBtn.style.backgroundColor = '#0056b3'); saveBtn.addEventListener('mouseout', () => saveBtn.style.backgroundColor = '#007AFF'); // 保存逻辑 saveBtn.addEventListener('click', (e) => { e.preventDefault(); // 获取并验证表单值 const newApiUrl = 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; } // 保存到存储 GM_setValue('apiUrl', newApiUrl); GM_setValue('apiKey', newApiKey); GM_setValue('model', newModel); GM_setValue('prompt', newPrompt); GM_setValue('shortcut', newShortcut); // 更新内存配置 config.apiUrl = newApiUrl; config.apiKey = newApiKey; config.model = newModel; config.prompt = newPrompt; config.shortcut = newShortcut; // 更新快捷键 keyManager.setup(); // 显示成功提示 showToast('设置已保存'); // 关闭设置 settingsContainer.style.display = 'none'; }); // 组装界面 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 showToast(message) { const toast = document.createElement('div'); toast.textContent = message; toast.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: #4CAF50; color: white; padding: 10px 20px; border-radius: 4px; z-index: 1000001; font-size: 14px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); `; document.body.appendChild(toast); setTimeout(() => toast.remove(), 2000); } // 拖拽功能 function makeDraggable(element) { const header = element.querySelector('div') || element; let startX = 0, startY = 0; let elementX = 0, elementY = 0; let dragging = false; // 鼠标事件 header.addEventListener('mousedown', startDrag); // 触摸事件 header.addEventListener('touchstart', (e) => { const touch = e.touches[0]; startDrag({ clientX: touch.clientX, clientY: touch.clientY, preventDefault: () => e.preventDefault() }); }, { passive: false }); function startDrag(e) { e.preventDefault(); dragging = true; // 记录起始位置 startX = e.clientX; startY = e.clientY; elementX = element.offsetLeft; elementY = element.offsetTop; // 设置样式 if (element.id === 'website-summary-icon') { element.style.transition = 'none'; element.style.opacity = '0.9'; } // 添加移动和结束事件 document.addEventListener('mousemove', onDrag); document.addEventListener('touchmove', onTouchDrag, { passive: false }); document.addEventListener('mouseup', stopDrag); document.addEventListener('touchend', stopDrag); } function onDrag(e) { if (!dragging) return; move(e.clientX, e.clientY); } function onTouchDrag(e) { if (!dragging) return; e.preventDefault(); const touch = e.touches[0]; move(touch.clientX, touch.clientY); } function move(clientX, clientY) { // 计算新位置 const deltaX = clientX - startX; const deltaY = 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 { // 自由移动其他元素 element.style.left = (elementX + deltaX) + 'px'; element.style.top = (elementY + deltaY) + 'px'; // 重置transform以避免与translate(-50%, -50%)冲突 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 }; GM_setValue('iconPosition', config.iconPosition); } // 移除事件监听器 document.removeEventListener('mousemove', onDrag); document.removeEventListener('touchmove', onTouchDrag); document.removeEventListener('mouseup', stopDrag); document.removeEventListener('touchend', stopDrag); } } // 获取页面内容 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); } } // 调用API获取总结 function getSummary(content) { return new Promise((resolve, reject) => { const apiKey = config.apiKey.trim(); if (!apiKey) { resolve('请先设置API Key'); return; } const requestData = { model: config.model, messages: [ { role: 'system', content: '你是一个专业的网页内容总结助手,善于使用markdown格式来组织信息。' }, { role: 'user', content: config.prompt + '\n\n' + content } ], temperature: 0.7 }; GM_xmlhttpRequest({ method: 'POST', url: config.apiUrl, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, data: JSON.stringify(requestData), onload: function(response) { try { const data = JSON.parse(response.responseText); if (data.error) { resolve('API调用失败: ' + data.error.message); return; } if (data.choices && data.choices[0] && data.choices[0].message) { resolve(data.choices[0].message.content); } else { resolve('API响应格式错误,请检查配置。'); } } catch (error) { resolve('解析API响应失败,请检查网络连接。'); } }, onerror: function() { resolve('API调用失败,请检查网络连接和API配置。'); } }); }); } // 渲染Markdown内容 function renderContent(content) { const container = document.getElementById('website-summary-content'); if (!container) return; try { if (!content) throw new Error('内容为空'); // 渲染Markdown let html = window.marked.parse(content); container.innerHTML = html; // 添加样式 addMarkdownStyles(); } catch (error) { container.innerHTML = '

渲染内容失败,请刷新页面重试。

'; } } // 添加Markdown样式 function addMarkdownStyles() { const styleId = 'website-summary-styles'; if (document.getElementById(styleId)) return; const style = document.createElement('style'); style.id = styleId; style.textContent = ` #website-summary-content { font-size: 14px; line-height: 1.6; color: #333; } #website-summary-content h1, #website-summary-content h2, #website-summary-content h3 { margin-top: 20px; margin-bottom: 10px; color: #222; } #website-summary-content p { margin: 10px 0; } #website-summary-content code { background: #f5f5f5; padding: 2px 4px; border-radius: 3px; font-family: monospace; } #website-summary-content pre { background: #f5f5f5; padding: 15px; border-radius: 5px; overflow-x: auto; } #website-summary-content blockquote { border-left: 4px solid #ddd; margin: 10px 0; padding-left: 15px; color: #666; } #website-summary-content ul, #website-summary-content ol { margin: 10px 0; padding-left: 20px; } #website-summary-content li { margin: 5px 0; } #website-summary-content table { border-collapse: collapse; width: 100%; margin: 10px 0; } #website-summary-content th, #website-summary-content td { border: 1px solid #ddd; padding: 8px; text-align: left; } #website-summary-content th { background: #f5f5f5; } `; document.head.appendChild(style); } // 添加菜单命令 function registerMenuCommands() { try { GM_registerMenuCommand('显示网页总结 (快捷键: ' + config.shortcut + ')', showSummary); GM_registerMenuCommand('打开设置', showSettings); } catch (error) { console.log('注册菜单命令失败:', error); } } // 初始化 function init() { // 等待页面完全加载 if (document.readyState !== 'complete') { window.addEventListener('load', init); return; } createIcon(); // 设置快捷键 const keySetupSuccess = keyManager.setup(); // 在页面获得焦点时重新注册快捷键 window.addEventListener('focus', () => keyManager.setup()); // 注册菜单命令 registerMenuCommands(); console.log('AI Page Summarizer Pro 初始化完成'); console.log('快捷键:', config.shortcut); // 在页面加载完成后测试快捷键 setTimeout(() => { keyManager.test(); console.log('快捷键使用提示: 按下 ' + config.shortcut + ' 触发网页总结,或右键点击图标打开设置'); console.log('也可以通过控制台调用 window.activateSummary() 直接触发'); }, 2000); } // 启动应用 waitForDependencies(init); })();