您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
使用OpenAI或Google Gemini API快速总结网页内容
// ==UserScript== // @name 网页内容摘要器 // @namespace http://tampermonkey.net/ // @version 0.1 // @description 使用OpenAI或Google Gemini API快速总结网页内容 // @author Your Name // @match *://*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_addStyle // @run-at document-end // @license MIT // @require https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js // ==/UserScript== (function() { 'use strict'; // 常量定义 const DEFAULT_OPENAI_URL = "https://api.openai.com/v1/chat/completions"; const DEFAULT_GEMINI_URL = "https://generativelanguage.googleapis.com"; const DEFAULT_HOTKEY = "s"; const HOTKEY_MODIFIER = "alt"; const DEFAULT_WIDTH = 500; // 默认宽度 const DEFAULT_HEIGHT = 600; // 默认高度 const MIN_WIDTH = 300; // 最小宽度 const MIN_HEIGHT = 200; // 最小高度 // 默认配置 const DEFAULT_CONFIGS = { apis: [ { id: "openai-default", name: "OpenAI默认", type: "openai", baseUrl: DEFAULT_OPENAI_URL, apiKey: "", models: ["gpt-3.5-turbo", "gpt-4", "gpt-4-turbo"] }, { id: "gemini-default", name: "Gemini默认", type: "gemini", baseUrl: DEFAULT_GEMINI_URL, apiKey: "", models: ["gemini-pro", "gemini-1.5-pro", "gemini-1.5-flash"] } ], prompts: [ { id: "summary-default", name: "一般摘要", content: "请用中文总结以下网页内容的要点,用简洁的语言描述主要信息。请使用Markdown格式输出,以提高可读性。", apiType: "openai", apiId: "openai-default", model: "gpt-3.5-turbo" }, { id: "detailed-summary", name: "详细摘要", content: "请详细分析以下网页内容,提供全面的中文摘要,包括主要观点、关键数据和结论。使用Markdown格式输出,合理使用标题、列表、引用等元素增强可读性。", apiType: "openai", apiId: "openai-default", model: "gpt-3.5-turbo" }, { id: "structured-summary", name: "结构化摘要", content: "请以结构化的方式分析并总结以下网页内容。使用Markdown语法,创建包含以下部分的摘要:\n\n1. **主要内容**: 用1-2段话概述主要内容\n2. **关键点**: 使用项目符号列出3-5个最重要的观点\n3. **细节与数据**: 提取文章中的重要数据和具体细节\n4. **结论**: 总结文章的结论或观点\n\n确保使用合适的Markdown标题、列表、强调和引用格式。", apiType: "openai", apiId: "openai-default", model: "gpt-3.5-turbo" }, { id: "gemini-summary", name: "Gemini摘要", content: "请用中文总结以下网页内容的要点,用简洁的语言描述主要信息。请使用Markdown格式输出,以提高可读性。", apiType: "gemini", apiId: "gemini-default", model: "gemini-pro" } ], settings: { hotkey: DEFAULT_HOTKEY, autoExpand: false, lastUsedPromptId: "summary-default" // 记录上次使用的提示词ID }, position: null, size: { width: DEFAULT_WIDTH } }; // 状态变量 let configs = GM_getValue("summarizer_configs", DEFAULT_CONFIGS); let isDragging = false; let dragOffsetX = 0; let dragOffsetY = 0; let summarizer = null; let isProcessing = false; let currentRequest = null; // 保存当前请求的引用 // 添加CSS样式 function addStyles() { GM_addStyle(` #web-summarizer { position: fixed; top: 0; right: 0; width: ${DEFAULT_WIDTH}px; height: 100vh; background-color: #fff; border-left: 1px solid #ccc; font-family: Arial, sans-serif; z-index: 10000; display: none; overflow: hidden; transition: none; display: flex; flex-direction: column; min-width: ${MIN_WIDTH}px; resize: none; box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1); } #summarizer-header { padding: 8px 10px; background-color: #f5f5f5; border-bottom: 1px solid #ddd; display: flex; justify-content: space-between; align-items: center; user-select: none; flex-shrink: 0; } #summarizer-resize-handle { position: absolute; top: 0; left: 0; width: 5px; height: 100%; cursor: ew-resize; background-color: transparent; z-index: 10002; } #summarizer-resize-handle:hover { background-color: rgba(0, 0, 0, 0.05); } .resizing #summarizer-resize-handle { background-color: rgba(66, 133, 244, 0.2); } #summarizer-title { font-weight: bold; font-size: 14px; margin: 0; } #summarizer-close { background: none; border: none; font-size: 16px; cursor: pointer; padding: 0 5px; } #summarizer-body { padding: 15px; overflow-y: auto; flex-grow: 1; } #summarizer-controls { margin-bottom: 15px; display: flex; flex-direction: column; } #prompt-select { width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; } .summarizer-btn { padding: 8px 12px; background-color: #4285f4; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 8px; font-size: 13px; } .button-group { display: flex; gap: 8px; } .summarizer-btn:hover { background-color: #3367d6; } .summarizer-btn:disabled { background-color: #b3cefb; cursor: not-allowed; } #summarizer-result { border: 1px solid #ddd; border-radius: 4px; padding: 15px; background-color: #f9f9f9; font-size: 14px; line-height: 1.6; max-height: none; height: auto; flex-grow: 1; overflow-y: auto; word-wrap: break-word; display: none; margin-top: 15px; } /* Markdown 样式 */ #summarizer-result h1, #summarizer-result h2, #summarizer-result h3, #summarizer-result h4, #summarizer-result h5, #summarizer-result h6 { margin-top: 1.5em; margin-bottom: 0.5em; line-height: 1.2; font-weight: 600; } #summarizer-result h1 { font-size: 1.8em; border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; } #summarizer-result h2 { font-size: 1.5em; border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; } #summarizer-result h3 { font-size: 1.3em; } #summarizer-result h4 { font-size: 1.1em; } #summarizer-result p { margin-top: 0.5em; margin-bottom: 1em; } #summarizer-result ul, #summarizer-result ol { margin-top: 0.5em; margin-bottom: 1em; padding-left: 2em; } #summarizer-result li { margin: 0.3em 0; } #summarizer-result code { background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; font-family: monospace; padding: 0.2em 0.4em; font-size: 0.9em; } #summarizer-result pre { background-color: #f6f8fa; border-radius: 3px; padding: 1em; overflow: auto; margin: 1em 0; } #summarizer-result pre code { background-color: transparent; padding: 0; white-space: pre; } #summarizer-result blockquote { margin: 1em 0; padding: 0 1em; color: #6a737d; border-left: 0.25em solid #dfe2e5; } #summarizer-result table { border-collapse: collapse; width: 100%; margin: 1em 0; } #summarizer-result table th, #summarizer-result table td { padding: 6px 13px; border: 1px solid #dfe2e5; } #summarizer-result table tr { background-color: #fff; border-top: 1px solid #c6cbd1; } #summarizer-result table tr:nth-child(2n) { background-color: #f6f8fa; } #summarizer-result img { max-width: 100%; } #summarizer-result a { color: #0366d6; text-decoration: none; } #summarizer-result a:hover { text-decoration: underline; } #summarizer-result hr { height: 0.25em; padding: 0; margin: 24px 0; background-color: #e1e4e8; border: 0; } /* 模态设置窗口样式 */ #settings-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 10001; display: none; justify-content: center; align-items: center; } #settings-container { background-color: #fff; border-radius: 5px; box-shadow: 0 2px 15px rgba(0, 0, 0, 0.3); width: 500px; max-width: 90%; max-height: 90vh; display: flex; flex-direction: column; overflow: hidden; } #settings-header { padding: 10px 15px; background-color: #f5f5f5; border-bottom: 1px solid #ddd; display: flex; justify-content: space-between; align-items: center; } #settings-title { font-weight: bold; font-size: 16px; margin: 0; } #settings-close { background: none; border: none; font-size: 18px; cursor: pointer; padding: 0 5px; } #settings-body { padding: 15px; overflow-y: auto; max-height: 70vh; flex-grow: 1; } #settings-footer { padding: 10px 15px; background-color: #f5f5f5; border-top: 1px solid #ddd; display: flex; justify-content: flex-end; gap: 10px; } .settings-tabs { display: flex; border-bottom: 1px solid #ddd; margin-bottom: 15px; } .settings-tab { padding: 8px 12px; cursor: pointer; background-color: #f5f5f5; border: 1px solid #ddd; border-bottom: none; margin-right: 5px; border-top-left-radius: 4px; border-top-right-radius: 4px; } .settings-tab.active { background-color: #fff; border-bottom: 1px solid #fff; margin-bottom: -1px; } .settings-panel { display: none; max-height: 350px; overflow-y: auto; } .settings-panel.active { display: block; } .config-item { margin-bottom: 15px; border: 1px solid #ddd; border-radius: 4px; } .config-header { background-color: #f5f5f5; padding: 8px 10px; cursor: pointer; border-bottom: 1px solid #ddd; } .config-body { padding: 10px; display: none; } .config-body.expanded { display: block; } .dragging { opacity: 0.9; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); } .form-group { margin-bottom: 10px; } .form-group label { display: block; margin-bottom: 5px; font-weight: bold; } .form-group input, .form-group textarea, .form-group select { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; } .form-group textarea { min-height: 80px; resize: vertical; } #add-api-btn, #add-prompt-btn { margin-bottom: 15px; } .model-list { border: 1px solid #ddd; border-radius: 4px; padding: 5px; max-height: 100px; overflow-y: auto; margin-top: 5px; } .model-item { display: flex; align-items: center; margin-bottom: 5px; } .delete-btn { background-color: #f44336; color: white; } .delete-btn:hover { background-color: #d32f2f; } .spinner { display: inline-block; width: 20px; height: 20px; border: 3px solid rgba(0, 0, 0, 0.1); border-radius: 50%; border-top-color: #4285f4; animation: spin 1s ease infinite; margin-right: 10px; vertical-align: middle; } @keyframes spin { to { transform: rotate(360deg); } } #stop-btn { background-color: #f44336; } #stop-btn:hover { background-color: #d32f2f; } `); } // 创建UI界面 function createUI() { try { // 创建主容器 summarizer = document.createElement('div'); summarizer.id = 'web-summarizer'; // 设置初始状态为隐藏 summarizer.style.display = 'none'; // 如果有保存的大小,应用它 if (configs.size && configs.size.width) { summarizer.style.width = configs.size.width + 'px'; } // 创建调整大小的手柄 const resizeHandle = document.createElement('div'); resizeHandle.id = 'summarizer-resize-handle'; // 创建头部 const header = document.createElement('div'); header.id = 'summarizer-header'; header.innerHTML = ` <div id="summarizer-title">网页内容摘要器</div> <button id="summarizer-close">×</button> `; // 创建主体内容 const body = document.createElement('div'); body.id = 'summarizer-body'; // 创建控制区域 const controls = document.createElement('div'); controls.id = 'summarizer-controls'; // 创建提示词选择下拉框 const promptSelect = document.createElement('select'); promptSelect.id = 'prompt-select'; configs.prompts.forEach(prompt => { const option = document.createElement('option'); option.value = prompt.id; option.textContent = prompt.name; // 如果是上次使用的提示词,则默认选中 if (prompt.id === configs.settings.lastUsedPromptId) { option.selected = true; } promptSelect.appendChild(option); }); // 创建按钮 const buttonGroup = document.createElement('div'); buttonGroup.className = 'button-group'; buttonGroup.innerHTML = ` <button id="summarize-btn" class="summarizer-btn">摘要</button> <button id="settings-btn" class="summarizer-btn">设置</button> `; controls.appendChild(promptSelect); controls.appendChild(buttonGroup); // 创建结果区域 const result = document.createElement('div'); result.id = 'summarizer-result'; // 组装主界面 body.appendChild(controls); body.appendChild(result); summarizer.appendChild(resizeHandle); summarizer.appendChild(header); summarizer.appendChild(body); // 创建设置模态窗口 const settingsModal = document.createElement('div'); settingsModal.id = 'settings-modal'; const settingsContainer = document.createElement('div'); settingsContainer.id = 'settings-container'; const settingsHeader = document.createElement('div'); settingsHeader.id = 'settings-header'; settingsHeader.innerHTML = ` <div id="settings-title">设置</div> <button id="settings-close">×</button> `; const settingsBody = document.createElement('div'); settingsBody.id = 'settings-body'; settingsBody.innerHTML = ` <div class="settings-tabs"> <div class="settings-tab active" data-tab="api-settings">API配置</div> <div class="settings-tab" data-tab="prompt-settings">提示词配置</div> <div class="settings-tab" data-tab="general-settings">常规设置</div> </div> <div id="api-settings" class="settings-panel active"> <div id="api-list"></div> <button id="add-api-btn" class="summarizer-btn">添加新API配置</button> </div> <div id="prompt-settings" class="settings-panel"> <div id="prompt-list"></div> <button id="add-prompt-btn" class="summarizer-btn">添加新提示词</button> </div> <div id="general-settings" class="settings-panel"> <div class="form-group"> <label for="hotkey-input">快捷键:</label> <input type="text" id="hotkey-input" maxlength="1" value="${configs.settings.hotkey}"> <small>单个字母或数字,与Alt键组合使用</small> </div> <div class="form-group"> <label> <input type="checkbox" id="auto-expand" ${configs.settings.autoExpand ? 'checked' : ''}> 自动展开设置项 </label> </div> </div> `; const settingsFooter = document.createElement('div'); settingsFooter.id = 'settings-footer'; settingsFooter.innerHTML = ` <button id="save-settings-btn" class="summarizer-btn">保存设置</button> <button id="cancel-settings-btn" class="summarizer-btn">取消</button> `; // 组装设置模态窗口 settingsContainer.appendChild(settingsHeader); settingsContainer.appendChild(settingsBody); settingsContainer.appendChild(settingsFooter); settingsModal.appendChild(settingsContainer); // 添加到页面 document.body.appendChild(summarizer); document.body.appendChild(settingsModal); // 初始化API和提示词列表 updateApiList(); updatePromptList(); // 绑定事件 bindAllEvents(); console.log("[网页内容摘要器] UI创建完成"); } catch (error) { console.error("[网页内容摘要器] 创建UI失败:", error); } } // 更新API配置列表 function updateApiList() { const apiList = document.getElementById('api-list'); if (!apiList) return; apiList.innerHTML = ''; configs.apis.forEach(api => { const apiItem = document.createElement('div'); apiItem.className = 'config-item'; apiItem.innerHTML = ` <div class="config-header" data-id="${api.id}"> ${api.name} (${api.type === 'openai' ? 'OpenAI兼容' : 'Gemini'}) </div> <div class="config-body" id="api-${api.id}"> <div class="form-group"> <label>API名称:</label> <input type="text" class="api-name" value="${api.name}"> </div> <div class="form-group"> <label>API类型:</label> <select class="api-type"> <option value="openai" ${api.type === 'openai' ? 'selected' : ''}>OpenAI兼容</option> <option value="gemini" ${api.type === 'gemini' ? 'selected' : ''}>Gemini</option> </select> </div> <div class="form-group base-url-group" ${api.type === 'gemini' ? 'style="display: none;"' : ''}> <label>基础URL:</label> <input type="text" class="api-base-url" value="${api.baseUrl}" ${api.type === 'gemini' ? 'readonly' : ''}> <small>例如: https://api.openai.com/v1/chat/completions</small> </div> <div class="form-group"> <label>API密钥:</label> <input type="password" class="api-key" value="${api.apiKey}"> </div> <div class="form-group"> <label>可用模型:</label> <small>每行输入一个模型名称</small> <textarea class="api-models">${api.models.join('\n')}</textarea> </div> <div class="button-group"> <button class="summarizer-btn test-api-btn">测试API连通性</button> <button class="summarizer-btn delete-btn delete-api-btn">删除</button> </div> </div> `; apiList.appendChild(apiItem); }); // 重新绑定配置项展开/折叠事件 bindConfigHeaderEvents(); } // 更新提示词列表 function updatePromptList() { const promptList = document.getElementById('prompt-list'); if (!promptList) return; promptList.innerHTML = ''; configs.prompts.forEach(prompt => { const promptItem = document.createElement('div'); promptItem.className = 'config-item'; // 获取关联的API const api = configs.apis.find(a => a.id === prompt.apiId); promptItem.innerHTML = ` <div class="config-header" data-id="${prompt.id}"> ${prompt.name} (${prompt.apiType === 'openai' ? 'OpenAI' : 'Gemini'}) </div> <div class="config-body" id="prompt-${prompt.id}"> <div class="form-group"> <label>提示词名称:</label> <input type="text" class="prompt-name" value="${prompt.name}"> </div> <div class="form-group"> <label>提示词内容:</label> <textarea class="prompt-content">${prompt.content}</textarea> </div> <div class="form-group"> <label>API类型:</label> <select class="prompt-api-type"> <option value="openai" ${prompt.apiType === 'openai' ? 'selected' : ''}>OpenAI兼容</option> <option value="gemini" ${prompt.apiType === 'gemini' ? 'selected' : ''}>Gemini</option> </select> </div> <div class="form-group prompt-api-select-group"> <label>选择API配置:</label> <select class="prompt-api-id"> ${getApiOptionsHtml(prompt.apiType, prompt.apiId)} </select> </div> <div class="form-group prompt-model-select-group"> <label>选择模型:</label> <select class="prompt-model"> ${getModelOptionsHtml(prompt.apiId, prompt.model)} </select> </div> <div class="button-group"> <button class="summarizer-btn delete-btn delete-prompt-btn">删除</button> </div> </div> `; promptList.appendChild(promptItem); }); // 重新绑定配置项展开/折叠事件 bindConfigHeaderEvents(); } // 生成API选项HTML function getApiOptionsHtml(apiType, selectedApiId) { return configs.apis .filter(api => api.type === apiType) .map(api => `<option value="${api.id}" ${api.id === selectedApiId ? 'selected' : ''}>${api.name}</option>`) .join(''); } // 生成模型选项HTML function getModelOptionsHtml(apiId, selectedModel) { const api = configs.apis.find(a => a.id === apiId); if (!api) return ''; return api.models .map(model => `<option value="${model}" ${model === selectedModel ? 'selected' : ''}>${model}</option>`) .join(''); } // 绑定事件 function bindAllEvents() { try { // 移除可能的旧事件监听器 const elementsToRebind = [ 'summarizer-close', 'summarizer-header', 'summarize-btn', 'settings-btn', 'add-api-btn', 'add-prompt-btn', 'save-settings-btn', 'cancel-settings-btn' ]; elementsToRebind.forEach(id => { const el = document.getElementById(id); if (el) { const newEl = el.cloneNode(true); el.parentNode.replaceChild(newEl, el); } }); // 解绑旧的全局事件 document.removeEventListener('change', handleDynamicChangeEvents); document.removeEventListener('click', handleDynamicClickEvents); document.removeEventListener('mousemove', drag); document.removeEventListener('mouseup', stopDrag); // 重新绑定所有事件 bindEvents(); } catch (error) { console.error("[网页内容摘要器] 重新绑定事件失败:", error); } } // 绑定事件 function bindEvents() { try { // 关闭按钮 const closeBtn = document.getElementById('summarizer-close'); if (closeBtn) { closeBtn.addEventListener('click', () => { summarizer.style.display = 'none'; }); } // 拖拽功能 const header = document.getElementById('summarizer-header'); if (header) { header.addEventListener('mousedown', startDrag); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', stopDrag); } // 调整大小功能 const resizeHandle = document.getElementById('summarizer-resize-handle'); if (resizeHandle) { resizeHandle.addEventListener('mousedown', startResize); // 监听器在startResize中动态添加 } // 摘要按钮 const summarizeBtn = document.getElementById('summarize-btn'); if (summarizeBtn) { summarizeBtn.addEventListener('click', generateSummary); } // 设置按钮 const settingsBtn = document.getElementById('settings-btn'); if (settingsBtn) { settingsBtn.addEventListener('click', () => { const settingsModal = document.getElementById('settings-modal'); if (settingsModal) { settingsModal.style.display = 'flex'; } }); } // 设置模态窗口关闭按钮 const settingsCloseBtn = document.getElementById('settings-close'); if (settingsCloseBtn) { settingsCloseBtn.addEventListener('click', () => { const settingsModal = document.getElementById('settings-modal'); if (settingsModal) { settingsModal.style.display = 'none'; } }); } // 点击模态窗口背景关闭 const settingsModal = document.getElementById('settings-modal'); if (settingsModal) { settingsModal.addEventListener('click', (e) => { if (e.target === settingsModal) { settingsModal.style.display = 'none'; } }); } // 设置页签切换 document.querySelectorAll('.settings-tab').forEach(tab => { tab.addEventListener('click', function() { // 激活选中的标签 document.querySelectorAll('.settings-tab').forEach(t => t.classList.remove('active')); this.classList.add('active'); // 显示对应的面板 const tabId = this.getAttribute('data-tab'); const targetPanel = document.getElementById(tabId); if (targetPanel) { document.querySelectorAll('.settings-panel').forEach(panel => { panel.classList.remove('active'); }); targetPanel.classList.add('active'); } }); }); // 绑定配置项展开/折叠事件 bindConfigHeaderEvents(); // 添加新API按钮 const addApiBtn = document.getElementById('add-api-btn'); if (addApiBtn) { addApiBtn.addEventListener('click', addNewApiConfig); } // 添加新提示词按钮 const addPromptBtn = document.getElementById('add-prompt-btn'); if (addPromptBtn) { addPromptBtn.addEventListener('click', addNewPrompt); } // 保存设置按钮 const saveSettingsBtn = document.getElementById('save-settings-btn'); if (saveSettingsBtn) { saveSettingsBtn.addEventListener('click', saveSettings); } // 取消按钮 const cancelSettingsBtn = document.getElementById('cancel-settings-btn'); if (cancelSettingsBtn) { cancelSettingsBtn.addEventListener('click', () => { const settingsModal = document.getElementById('settings-modal'); if (settingsModal) { settingsModal.style.display = 'none'; updateApiList(); updatePromptList(); } }); } // 使用事件委托处理动态元素的事件 document.addEventListener('change', handleDynamicChangeEvents); document.addEventListener('click', handleDynamicClickEvents); console.log("[网页内容摘要器] 事件绑定完成"); } catch (error) { console.error("[网页内容摘要器] 绑定事件失败:", error); } } // 处理动态元素的change事件 function handleDynamicChangeEvents(e) { try { if (e.target && e.target.classList.contains('api-type')) { const type = e.target.value; const container = e.target.closest('.config-body'); const baseUrlGroup = container.querySelector('.base-url-group'); const baseUrlInput = container.querySelector('.api-base-url'); if (type === 'gemini') { baseUrlGroup.style.display = 'none'; baseUrlInput.value = DEFAULT_GEMINI_URL; baseUrlInput.setAttribute('readonly', 'readonly'); } else { baseUrlGroup.style.display = 'block'; baseUrlInput.removeAttribute('readonly'); } } if (e.target && e.target.classList.contains('prompt-api-type')) { const type = e.target.value; const container = e.target.closest('.config-body'); const apiSelect = container.querySelector('.prompt-api-id'); apiSelect.innerHTML = getApiOptionsHtml(type, ''); updateModelOptions(apiSelect); } if (e.target && e.target.classList.contains('prompt-api-id')) { updateModelOptions(e.target); } } catch (error) { console.error("[网页内容摘要器] 处理动态change事件失败:", error); } } // 处理动态元素的click事件 function handleDynamicClickEvents(e) { try { if (e.target && e.target.classList.contains('delete-api-btn')) { const container = e.target.closest('.config-item'); const id = container.querySelector('.config-header').getAttribute('data-id'); if (confirm('确定要删除这个API配置吗?这可能会影响依赖它的提示词。')) { deleteApiConfig(id); } } if (e.target && e.target.classList.contains('delete-prompt-btn')) { const container = e.target.closest('.config-item'); const id = container.querySelector('.config-header').getAttribute('data-id'); if (confirm('确定要删除这个提示词吗?')) { deletePrompt(id); } } if (e.target && e.target.classList.contains('test-api-btn')) { const container = e.target.closest('.config-body'); const apiType = container.querySelector('.api-type').value; const baseUrl = container.querySelector('.api-base-url').value; const apiKey = container.querySelector('.api-key').value; const models = container.querySelector('.api-models').value.split('\n').filter(m => m.trim()); testApiConnection(apiType, baseUrl, apiKey, models[0] || (apiType === 'openai' ? 'gpt-3.5-turbo' : 'gemini-pro')); } } catch (error) { console.error("[网页内容摘要器] 处理动态click事件失败:", error); } } // 开始拖拽 function startDrag(e) { if (e.target.id === 'summarizer-header' || e.target.id === 'summarizer-title') { isDragging = true; dragOffsetX = e.clientX - summarizer.getBoundingClientRect().left; dragOffsetY = e.clientY - summarizer.getBoundingClientRect().top; e.preventDefault(); // 添加拖拽中的样式 summarizer.classList.add('dragging'); document.body.style.userSelect = 'none'; } } // 拖拽中 function drag(e) { if (isDragging) { const newLeft = e.clientX - dragOffsetX; const newTop = e.clientY - dragOffsetY; // 限制在视窗范围内 const maxLeft = window.innerWidth - summarizer.offsetWidth; const maxTop = window.innerHeight - summarizer.offsetHeight; const limitedLeft = Math.max(0, Math.min(newLeft, maxLeft)); const limitedTop = Math.max(0, Math.min(newTop, maxTop)); summarizer.style.left = limitedLeft + 'px'; summarizer.style.top = limitedTop + 'px'; summarizer.style.right = 'auto'; e.preventDefault(); } } // 停止拖拽 function stopDrag() { if (isDragging) { isDragging = false; // 移除拖拽中的样式 summarizer.classList.remove('dragging'); document.body.style.userSelect = ''; // 保存位置到配置中 configs.position = { left: summarizer.style.left, top: summarizer.style.top }; GM_setValue("summarizer_configs", configs); } } // 更新模型选项 function updateModelOptions(apiSelect) { const apiId = apiSelect.value; const container = apiSelect.closest('.config-body'); const modelSelect = container.querySelector('.prompt-model'); modelSelect.innerHTML = getModelOptionsHtml(apiId, ''); } // 添加新API配置 function addNewApiConfig() { const newId = 'api-' + Date.now(); const newApi = { id: newId, name: "新API配置", type: "openai", baseUrl: DEFAULT_OPENAI_URL, apiKey: "", models: ["gpt-3.5-turbo"] }; configs.apis.push(newApi); updateApiList(); // 重新绑定所有事件 bindAllEvents(); // 展开新创建的配置项 setTimeout(() => { const newHeader = document.querySelector(`.config-header[data-id="${newId}"]`); if (newHeader) { newHeader.click(); } }, 100); } // 添加新提示词 function addNewPrompt() { const newId = 'prompt-' + Date.now(); const defaultApi = configs.apis.find(a => a.type === 'openai') || configs.apis[0]; const newPrompt = { id: newId, name: "新提示词", content: "请总结以下网页内容:", apiType: defaultApi ? defaultApi.type : "openai", apiId: defaultApi ? defaultApi.id : "", model: defaultApi && defaultApi.models.length > 0 ? defaultApi.models[0] : "" }; configs.prompts.push(newPrompt); updatePromptList(); updatePromptSelect(); // 重新绑定所有事件 bindAllEvents(); // 展开新创建的配置项 setTimeout(() => { const newHeader = document.querySelector(`.config-header[data-id="${newId}"]`); if (newHeader) { newHeader.click(); } }, 100); } // 删除API配置 function deleteApiConfig(id) { configs.apis = configs.apis.filter(api => api.id !== id); // 更新依赖此API的提示词 configs.prompts.forEach(prompt => { if (prompt.apiId === id) { const newApi = configs.apis.find(a => a.type === prompt.apiType); if (newApi) { prompt.apiId = newApi.id; prompt.model = newApi.models.length > 0 ? newApi.models[0] : ""; } } }); updateApiList(); updatePromptList(); updatePromptSelect(); // 重新绑定所有事件 bindAllEvents(); } // 删除提示词 function deletePrompt(id) { configs.prompts = configs.prompts.filter(prompt => prompt.id !== id); updatePromptList(); updatePromptSelect(); // 重新绑定所有事件 bindAllEvents(); } // 保存设置 function saveSettings() { try { // 保存API配置 const newApis = []; document.querySelectorAll('#api-list .config-item').forEach(item => { const header = item.querySelector('.config-header'); const body = item.querySelector('.config-body'); const id = header.getAttribute('data-id'); newApis.push({ id: id, name: body.querySelector('.api-name').value, type: body.querySelector('.api-type').value, baseUrl: body.querySelector('.api-base-url').value, apiKey: body.querySelector('.api-key').value, models: body.querySelector('.api-models').value.split('\n').filter(m => m.trim()) }); }); // 保存提示词配置 const newPrompts = []; document.querySelectorAll('#prompt-list .config-item').forEach(item => { const header = item.querySelector('.config-header'); const body = item.querySelector('.config-body'); const id = header.getAttribute('data-id'); newPrompts.push({ id: id, name: body.querySelector('.prompt-name').value, content: body.querySelector('.prompt-content').value, apiType: body.querySelector('.prompt-api-type').value, apiId: body.querySelector('.prompt-api-id').value, model: body.querySelector('.prompt-model').value }); }); // 保存常规设置 const hotkey = document.getElementById('hotkey-input').value; const autoExpand = document.getElementById('auto-expand').checked; // 保存位置信息 const position = configs.position; // 更新配置 configs.apis = newApis; configs.prompts = newPrompts; configs.settings.hotkey = hotkey || DEFAULT_HOTKEY; configs.settings.autoExpand = autoExpand; configs.settings.lastUsedPromptId = document.getElementById('prompt-select').value || configs.settings.lastUsedPromptId; configs.position = position; // 保存到GM存储 GM_setValue("summarizer_configs", configs); // 更新UI const settingsModal = document.getElementById('settings-modal'); if (settingsModal) { settingsModal.style.display = 'none'; } updateApiList(); updatePromptList(); // 更新选择框 updatePromptSelect(); // 更新热键监听 document.removeEventListener('keydown', hotkeyHandler); addHotkeyListener(); // 重新绑定所有事件 bindAllEvents(); alert('设置已保存'); } catch (error) { console.error("[网页内容摘要器] 保存设置失败:", error); alert('保存设置失败: ' + error.message); } } // 更新提示词选择下拉框 function updatePromptSelect() { const promptSelect = document.getElementById('prompt-select'); if (!promptSelect) return; // 保存当前选中的提示词ID const selectedPromptId = promptSelect.value || configs.settings.lastUsedPromptId; promptSelect.innerHTML = ''; configs.prompts.forEach(prompt => { const option = document.createElement('option'); option.value = prompt.id; option.textContent = prompt.name; // 恢复选中状态 if (prompt.id === selectedPromptId) { option.selected = true; } promptSelect.appendChild(option); }); } // 测试API连接 function testApiConnection(apiType, baseUrl, apiKey, model) { if (!apiKey) { alert('请先输入API密钥'); return; } if (apiType === 'openai') { // 测试OpenAI兼容API GM_xmlhttpRequest({ method: 'POST', url: baseUrl, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, data: JSON.stringify({ model: model, messages: [ { role: 'user', content: 'Hello, this is a test message. Please respond with "API connection successful".' } ], max_tokens: 50 }), onload: function(response) { try { const data = JSON.parse(response.responseText); if (data.choices && data.choices.length > 0) { alert('API连接成功!'); } else { alert('API响应格式不正确: ' + response.responseText); } } catch (e) { alert('API响应解析失败: ' + e.message); } }, onerror: function(response) { alert('API连接失败: ' + response.statusText); } }); } else if (apiType === 'gemini') { // 测试Gemini API const url = `${baseUrl}/v1beta/models/${model}:generateContent?key=${apiKey}`; GM_xmlhttpRequest({ method: 'POST', url: url, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ contents: [ { parts: [ { text: 'Hello, this is a test message. Please respond with "API connection successful".' } ] } ], generationConfig: { maxOutputTokens: 50 } }), onload: function(response) { try { const data = JSON.parse(response.responseText); if (data.candidates && data.candidates.length > 0) { alert('API连接成功!'); } else { alert('API响应格式不正确: ' + response.responseText); } } catch (e) { alert('API响应解析失败: ' + e.message); } }, onerror: function(response) { alert('API连接失败: ' + response.statusText); } }); } } // 生成摘要 function generateSummary() { // 如果当前正在处理,则表示这是停止操作 if (isProcessing) { stopSummary(); return; } // 获取选中的提示词 const promptId = document.getElementById('prompt-select').value; const prompt = configs.prompts.find(p => p.id === promptId); if (!prompt) { alert('请先选择或创建一个提示词'); return; } // 保存当前使用的提示词ID configs.settings.lastUsedPromptId = promptId; GM_setValue("summarizer_configs", configs); // 获取关联的API配置 const api = configs.apis.find(a => a.id === prompt.apiId); if (!api) { alert('提示词关联的API配置不存在,请检查设置'); return; } if (!api.apiKey) { alert('请先设置API密钥'); return; } // 获取网页内容 const pageContent = getPageContent(); if (!pageContent) { alert('无法提取页面内容'); return; } // 显示处理中状态 const summarizeBtn = document.getElementById('summarize-btn'); const resultDiv = document.getElementById('summarizer-result'); // 将摘要按钮改为停止按钮 summarizeBtn.disabled = false; summarizeBtn.id = 'stop-btn'; summarizeBtn.innerHTML = '<span class="spinner"></span>停止摘要'; resultDiv.style.display = 'block'; resultDiv.innerHTML = '<div style="text-align: center; padding: 20px;"><span class="spinner"></span> 正在生成摘要,请稍候...</div>'; isProcessing = true; // A根据API类型发送请求 if (prompt.apiType === 'openai') { // 调用OpenAI兼容API currentRequest = GM_xmlhttpRequest({ method: 'POST', url: api.baseUrl, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${api.apiKey}` }, data: JSON.stringify({ model: prompt.model, messages: [ { role: 'user', content: `${prompt.content}\n\n${pageContent}` } ], max_tokens: 2000 // 增加token数量,确保返回完整内容 }), onload: function(response) { try { // 检查请求是否已经被中止 if (this.readyState === 4 && this.status === 0) { resultDiv.innerHTML = '<div style="padding: 15px;">摘要已取消</div>'; resetSummaryButton(); return; } const data = JSON.parse(response.responseText); if (data.choices && data.choices.length > 0) { const summaryText = data.choices[0].message.content; // 渲染Markdown resultDiv.innerHTML = renderMarkdown(summaryText); // 确保显示完整内容 setTimeout(() => { resultDiv.scrollTop = 0; }, 100); } else { resultDiv.innerHTML = '<div style="color: red; padding: 15px;">生成摘要失败: API响应格式不正确</div>'; console.error('API响应:', data); } } catch (e) { resultDiv.innerHTML = `<div style="color: red; padding: 15px;">生成摘要失败: ${e.message}</div>`; console.error('API响应解析失败:', e, response.responseText); } resetSummaryButton(); }, onerror: function(response) { resultDiv.innerHTML = `<div style="color: red; padding: 15px;">生成摘要失败: ${response.statusText || '请求错误'}</div>`; resetSummaryButton(); } }); } else if (prompt.apiType === 'gemini') { // 调用Gemini API const url = `${api.baseUrl}/v1beta/models/${prompt.model}:generateContent?key=${api.apiKey}`; currentRequest = GM_xmlhttpRequest({ method: 'POST', url: url, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ contents: [ { parts: [ { text: `${prompt.content}\n\n${pageContent}` } ] } ], generationConfig: { maxOutputTokens: 2000 // 增加token数量,确保返回完整内容 } }), onload: function(response) { try { // 检查请求是否已经被中止 if (this.readyState === 4 && this.status === 0) { resultDiv.innerHTML = '<div style="padding: 15px;">摘要已取消</div>'; resetSummaryButton(); return; } const data = JSON.parse(response.responseText); if (data.candidates && data.candidates.length > 0) { const summaryText = data.candidates[0].content.parts[0].text; // 渲染Markdown resultDiv.innerHTML = renderMarkdown(summaryText); // 确保显示完整内容 setTimeout(() => { resultDiv.scrollTop = 0; }, 100); } else { resultDiv.innerHTML = '<div style="color: red; padding: 15px;">生成摘要失败: API响应格式不正确</div>'; console.error('API响应:', data); } } catch (e) { resultDiv.innerHTML = `<div style="color: red; padding: 15px;">生成摘要失败: ${e.message}</div>`; console.error('API响应解析失败:', e, response.responseText); } resetSummaryButton(); }, onerror: function(response) { resultDiv.innerHTML = `<div style="color: red; padding: 15px;">生成摘要失败: ${response.statusText || '请求错误'}</div>`; resetSummaryButton(); } }); } } // 停止摘要 function stopSummary() { if (currentRequest && typeof currentRequest.abort === 'function') { currentRequest.abort(); } // 重置按钮和状态 resetSummaryButton(); console.log("[网页内容摘要器] 摘要已停止"); } // 重置摘要按钮 function resetSummaryButton() { const stopBtn = document.getElementById('stop-btn'); if (stopBtn) { stopBtn.id = 'summarize-btn'; stopBtn.textContent = '摘要'; stopBtn.disabled = false; } isProcessing = false; currentRequest = null; } // 获取页面内容 function getPageContent() { try { // 获取主要内容 let content = ''; // 尝试获取文章内容区 const articleElements = document.querySelectorAll('article, .article, .post, .content, main'); if (articleElements.length > 0) { // 使用最大的内容区 let maxTextElement = null; let maxTextLength = 0; articleElements.forEach(el => { const textLength = el.textContent.trim().length; if (textLength > maxTextLength) { maxTextLength = textLength; maxTextElement = el; } }); if (maxTextElement) { content = maxTextElement.textContent; } } // 如果没有找到明确的内容区,尝试从段落获取 if (!content || content.length < 100) { const paragraphs = document.querySelectorAll('p'); const paragraphTexts = []; paragraphs.forEach(p => { // 过滤掉短段落和导航/页脚/侧边栏等区域的段落 const text = p.textContent.trim(); if (text.length > 20 && !isElementInNavOrFooter(p)) { paragraphTexts.push(text); } }); content = paragraphTexts.join('\n\n'); } // 确保内容不为空 if (!content || content.length < 50) { // 退化方案:获取所有可见文本 content = document.body.innerText; } // 添加页面标题 const pageTitle = document.title; content = `标题: ${pageTitle}\n\n${content}`; // 过滤内容 content = content .replace(/\s+/g, ' ') // 替换多个空白字符为单个空格 .replace(/\n{3,}/g, '\n\n') // 替换多个换行为两个换行 .trim(); // 限制内容长度 const maxLength = 6000; // 设置合理的长度限制 if (content.length > maxLength) { content = content.substring(0, maxLength) + "\n\n... (内容已截断)"; } return content; } catch (e) { console.error('获取页面内容失败:', e); return document.title + '\n\n' + document.body.innerText.substring(0, 6000); } } // 判断元素是否在导航或页脚中 function isElementInNavOrFooter(element) { let current = element; while (current && current !== document.body) { const tagName = current.tagName.toLowerCase(); const className = (current.className || '').toLowerCase(); const id = (current.id || '').toLowerCase(); if ( tagName === 'nav' || tagName === 'footer' || tagName === 'header' || className.includes('nav') || className.includes('menu') || className.includes('footer') || className.includes('sidebar') || id.includes('nav') || id.includes('menu') || id.includes('footer') || id.includes('sidebar') ) { return true; } current = current.parentElement; } return false; } // 渲染Markdown内容 function renderMarkdown(text) { try { // 确保marked库已加载 if (typeof marked === 'undefined') { console.error('[网页内容摘要器] Marked库未加载'); return text; } // 配置marked选项 marked.setOptions({ breaks: true, gfm: true, headerIds: false, mangle: false, sanitize: false, smartLists: true }); // 渲染Markdown return marked.parse(text); } catch (error) { console.error('[网页内容摘要器] Markdown渲染失败:', error); return text; } } // 初始化 function initialize() { try { // 添加CSS样式 addStyles(); // 创建UI界面 createUI(); // 注册油猴菜单命令 GM_registerMenuCommand("打开/关闭摘要器", toggleSummarizer); // 添加热键监听 addHotkeyListener(); console.log("[网页内容摘要器] 初始化完成,按Alt+" + configs.settings.hotkey.toUpperCase() + "打开摘要器"); } catch (error) { console.error("[网页内容摘要器] 初始化失败:", error); // 尝试重新初始化 setTimeout(() => { try { console.log("[网页内容摘要器] 尝试重新初始化..."); // 清理之前的UI(如果有) const oldSummarizer = document.getElementById('web-summarizer'); if (oldSummarizer) { oldSummarizer.remove(); } // 重新创建UI createUI(); console.log("[网页内容摘要器] 重新初始化完成"); } catch (e) { console.error("[网页内容摘要器] 重新初始化失败:", e); } }, 2000); } } // 切换摘要器显示状态 function toggleSummarizer() { try { if (!summarizer) { console.log("[网页内容摘要器] 摘要器未初始化,尝试重新初始化"); initialize(); return; } if (summarizer.style.display === 'none' || summarizer.style.display === '') { summarizer.style.display = 'flex'; } else { summarizer.style.display = 'none'; } } catch (error) { console.error("[网页内容摘要器] 切换显示状态失败:", error); } } // 添加热键监听 function addHotkeyListener() { try { // 先移除旧的事件监听(如果有)以避免重复 document.removeEventListener('keydown', hotkeyHandler); // 添加新的事件监听 document.addEventListener('keydown', hotkeyHandler); } catch (error) { console.error("[网页内容摘要器] 添加热键监听失败:", error); } } // 热键处理函数 function hotkeyHandler(e) { if (e.altKey && e.key.toLowerCase() === configs.settings.hotkey.toLowerCase()) { e.preventDefault(); toggleSummarizer(); } } // 绑定配置项展开/折叠事件 function bindConfigHeaderEvents() { document.querySelectorAll('.config-header').forEach(header => { // 移除旧事件以避免重复绑定 const newHeader = header.cloneNode(true); header.parentNode.replaceChild(newHeader, header); newHeader.addEventListener('click', function() { const id = this.getAttribute('data-id'); const body = this.nextElementSibling; if (body.classList.contains('expanded')) { body.classList.remove('expanded'); } else { if (!configs.settings.autoExpand) { document.querySelectorAll('.config-body').forEach(b => b.classList.remove('expanded')); } body.classList.add('expanded'); } }); }); } // 开始调整大小 let isResizing = false; let startX = 0; let startWidth = 0; function startResize(e) { // 防止事件传播 e.stopPropagation(); e.preventDefault(); isResizing = true; startX = e.clientX; startWidth = summarizer.offsetWidth; // 添加调整大小时的事件监听 document.addEventListener('mousemove', resize); document.addEventListener('mouseup', stopResize); // 添加调整大小时的样式 document.body.style.userSelect = 'none'; summarizer.classList.add('resizing'); } // 调整大小中 function resize(e) { if (!isResizing) return; // 计算新宽度 - 这里与之前不同,向左拖动会增加宽度 const changeX = startX - e.clientX; const newWidth = Math.max(MIN_WIDTH, startWidth + changeX); // 限制最大宽度 const maxWidth = window.innerWidth * 0.8; summarizer.style.width = Math.min(newWidth, maxWidth) + 'px'; } // 停止调整大小 function stopResize() { if (!isResizing) return; isResizing = false; // 移除事件监听 document.removeEventListener('mousemove', resize); document.removeEventListener('mouseup', stopResize); // 移除样式 document.body.style.userSelect = ''; summarizer.classList.remove('resizing'); // 保存大小到配置 configs.size = { width: summarizer.offsetWidth }; GM_setValue("summarizer_configs", configs); } // 主函数 // 等待DOM完全加载后再初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { initialize(); } })();