您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自动总结网页文章内容,支持多种格式输出,适用于各类文章网站
当前为
// ==UserScript== // @name 网页文章总结助手 // @namespace http://tampermonkey.net/ // @version 0.1.3 // @description 自动总结网页文章内容,支持多种格式输出,适用于各类文章网站 // @author h7ml <[email protected]> // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_getResourceText // @connect api.gptgod.online // @connect api.deepseek.com // @resource marked https://cdn.bootcdn.net/ajax/libs/marked/4.3.0/marked.min.js // @resource highlight https://cdn.bootcdn.net/ajax/libs/highlight.js/11.7.0/highlight.min.js // @resource highlightStyle https://cdn.bootcdn.net/ajax/libs/highlight.js/11.7.0/styles/github.min.css // ==/UserScript== (function () { 'use strict'; // 加载 marked.js 库 function loadMarkedJS() { return new Promise((resolve) => { try { const markedScript = GM_getResourceText('marked'); if (markedScript) { // 创建一个函数来执行 marked.js 的内容 const executeMarked = new Function(markedScript); executeMarked(); resolve(); } else { console.error('无法加载 marked.js'); resolve(); // 即使加载失败也继续 } } catch (error) { console.error('加载 marked.js 失败:', error); resolve(); // 即使加载失败也继续 } }); } // 加载 highlight.js 库 function loadHighlightJS() { return new Promise((resolve) => { try { const highlightScript = GM_getResourceText('highlight'); const highlightStyle = GM_getResourceText('highlightStyle'); if (highlightScript) { // 创建一个函数来执行 highlight.js 的内容 const executeHighlight = new Function(highlightScript); executeHighlight(); } if (highlightStyle) { const style = document.createElement('style'); style.textContent = highlightStyle; document.head.appendChild(style); } resolve(); } catch (error) { console.error('加载 highlight.js 失败:', error); resolve(); // 即使加载失败也继续 } }); } // 创建样式 - 使用普通 CSS const style = document.createElement('style'); style.textContent = ` /* 基础样式 */ #article-summary-app { position: fixed; top: 1rem; right: 1rem; width: 24rem; z-index: 2147483647; min-width: 300px; min-height: 200px; resize: both; overflow: auto; cursor: move; font-family: system-ui, -apple-system, sans-serif; font-size: 0.875rem; line-height: 1.5; color: #374151; background-color: white; border-radius: 0.5rem; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); transition: all 0.2s; } /* 头部样式 */ #summary-header { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 1rem; background-color: #2563eb; color: white; } #summary-header h3 { margin: 0; font-size: 1rem; font-weight: 600; } #summary-header-actions { display: flex; gap: 0.5rem; } .header-btn { background: transparent; border: none; color: white; cursor: pointer; padding: 0.25rem; border-radius: 0.25rem; display: flex; align-items: center; justify-content: center; width: 1.5rem; height: 1.5rem; transition: background-color 0.2s; } .header-btn:hover { background-color: rgba(255, 255, 255, 0.2); } /* 主体内容 */ #summary-body { padding: 1rem; } /* 配置面板 */ #config-section { margin-bottom: 1rem; } #configToggle { display: flex; justify-content: space-between; align-items: center; padding: 0.625rem 0.75rem; background-color: #f3f4f6; border-radius: 0.25rem; cursor: pointer; font-weight: 500; transition: background-color 0.2s; } #configToggle:hover { background-color: #e5e7eb; } #configToggle.collapsed .toggle-icon { transform: rotate(-90deg); } .toggle-icon { transition: transform 0.2s; } #configPanel { max-height: 500px; overflow: hidden; transition: all 0.3s ease-out; opacity: 1; margin-top: 0.75rem; } #configPanel.collapsed { max-height: 0; opacity: 0; margin-top: 0; } /* 表单元素 */ .form-group { margin-bottom: 0.75rem; } .form-label { display: block; margin-bottom: 0.375rem; font-weight: 500; color: #374151; } .form-select, .form-input { width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 0.25rem; background-color: #f9fafb; transition: all 0.2s; } .form-select:focus, .form-input:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 1px #3b82f6; } /* 格式选择按钮 */ #formatOptions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.75rem; } .format-btn { padding: 0.375rem 0.75rem; background-color: #f3f4f6; border: 1px solid #d1d5db; border-radius: 0.25rem; font-size: 0.875rem; cursor: pointer; transition: all 0.2s; } .format-btn:hover { background-color: #e5e7eb; } .format-btn.active { background-color: #2563eb; color: white; border-color: #2563eb; } /* 生成按钮 */ #generateBtn { width: 100%; padding: 0.625rem; background-color: #2563eb; color: white; border: none; border-radius: 0.25rem; font-weight: 500; cursor: pointer; transition: background-color 0.2s; display: flex; justify-content: center; align-items: center; gap: 0.5rem; } #generateBtn:hover { background-color: #1d4ed8; } #generateBtn:disabled { background-color: #93c5fd; cursor: not-allowed; } /* 结果区域 */ #summaryResult { margin-top: 1rem; display: none; } #summaryHeader { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; } #summaryHeader h4 { margin: 0; font-size: 1rem; font-weight: 600; color: #1f2937; } #summaryActions { display: flex; gap: 0.5rem; } .action-btn { padding: 0.25rem 0.5rem; background-color: #f3f4f6; border: 1px solid #d1d5db; border-radius: 0.25rem; font-size: 0.75rem; cursor: pointer; display: flex; align-items: center; gap: 0.25rem; transition: background-color 0.2s; } .action-btn:hover { background-color: #e5e7eb; } #summaryContent { border: 1px solid #e5e7eb; border-radius: 0.25rem; padding: 1rem; background-color: #f9fafb; max-height: 400px; overflow-y: auto; line-height: 1.625; } /* Markdown 样式 */ #summaryContent.markdown-body { font-family: system-ui, -apple-system, sans-serif; } #summaryContent h1 { font-size: 1.25rem; margin: 0.5rem 0; font-weight: 600; color: #111827; } #summaryContent h2 { font-size: 1.125rem; margin: 0.5rem 0; font-weight: 600; color: #111827; } #summaryContent h3 { font-size: 1rem; margin: 0.5rem 0; font-weight: 600; color: #111827; } #summaryContent ul, #summaryContent ol { padding-left: 1.5rem; margin: 0.5rem 0; } #summaryContent p { margin: 0.5rem 0; } #summaryContent pre { background-color: #f3f4f6; padding: 0.75rem; border-radius: 0.25rem; overflow-x: auto; margin: 0.5rem 0; } #summaryContent code { font-family: ui-monospace, monospace; font-size: 0.875rem; background-color: #f3f4f6; padding: 0.125rem 0.375rem; border-radius: 0.25rem; } /* 加载指示器 */ #loadingIndicator { display: none; text-align: center; padding: 1.25rem 0; } .spinner { width: 2.5rem; height: 2.5rem; margin: 0 auto; border: 3px solid #e5e7eb; border-radius: 9999px; border-top-color: #2563eb; animation: spin 1s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* 响应式调整 */ @media (max-width: 640px) { #article-summary-app { width: 90%; right: 5%; left: 5%; } } /* 工具提示 */ .tooltip { position: relative; } .tooltip:hover::after { content: attr(data-tooltip); position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); padding: 0.25rem 0.5rem; background-color: #1f2937; color: white; border-radius: 0.25rem; font-size: 0.75rem; white-space: nowrap; z-index: 10; } /* 图标 */ .icon { width: 1rem; height: 1rem; display: inline-block; } /* 添加拖拽相关样式 */ .draggable { user-select: none; cursor: move; } .resizable { resize: both; overflow: auto; } /* 总结内容样式优化 */ .summary-container { font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; color: #333; padding: 1rem; } .summary-container h1 { font-size: 1.5rem; color: #1a365d; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 2px solid #e2e8f0; } .summary-container h2 { font-size: 1.25rem; color: #2d3748; margin: 1.5rem 0 1rem; } .summary-container ul { list-style: none; padding-left: 1.5rem; margin: 1rem 0; } .summary-container li { position: relative; padding-left: 1.5rem; margin-bottom: 0.5rem; } .summary-container li:before { content: "•"; color: #4299e1; font-weight: bold; position: absolute; left: 0; } .summary-container strong { color: #2b6cb0; font-weight: 600; } .summary-container code { background: #f7fafc; padding: 0.2rem 0.4rem; border-radius: 0.25rem; font-family: monospace; font-size: 0.9em; color: #4a5568; border: 1px solid #edf2f7; } /* 最小化图标样式 */ #article-summary-icon { position: fixed; right: 20px; top: 20px; width: 40px; height: 40px; background-color: #2563eb; border-radius: 50%; display: none; justify-content: center; align-items: center; cursor: pointer; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); z-index: 2147483647; transition: all 0.3s ease; } #article-summary-icon:hover { transform: scale(1.1); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } #article-summary-icon svg { width: 24px; height: 24px; color: white; } /* 修改主应用容器的过渡效果 */ #article-summary-app { transition: all 0.3s ease; transform-origin: top right; } .app-minimized { transform: scale(0); opacity: 0; pointer-events: none; } `; document.head.appendChild(style); // 创建应用容器 const app = document.createElement('div'); app.id = 'article-summary-app'; // 创建HTML内容 - 使用更现代的UI设计 app.innerHTML = ` <div id="summary-header"> <h3>文章总结助手</h3> <div id="summary-header-actions"> <button id="toggleMaxBtn" class="header-btn" data-tooltip="最大化"> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M8 3v3a2 2 0 01-2 2H3m18 0h-3a2 2 0 01-2-2V3m0 18v-3a2 2 0 012-2h3M3 16h3a2 2 0 012 2v3"></path> </svg> </button> <button id="toggleMinBtn" class="header-btn" data-tooltip="最小化"> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M20 12H4"></path> </svg> </button> </div> </div> <div id="summary-body"> <div id="config-section"> <div id="configToggle"> <span>配置选项</span> <svg class="icon toggle-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M19 9l-7 7-7-7"></path> </svg> </div> <div id="configPanel"> <div class="form-group"> <label class="form-label" for="apiService">API服务</label> <select id="apiService" class="form-select"> <option value="gptgod">GPT God</option> <option value="deepseek">DeepSeek</option> <option value="custom">自定义</option> </select> </div> <div id="customApiUrlContainer" class="form-group" style="display: none;"> <label class="form-label" for="customApiUrl">API地址</label> <input type="text" id="customApiUrl" class="form-input" placeholder="https://api.example.com/v1/chat/completions"> </div> <div class="form-group"> <label class="form-label" for="apiKey">API Key</label> <input type="password" id="apiKey" class="form-input" placeholder="sk-..."> </div> <div class="form-group"> <label class="form-label" for="modelName">模型</label> <input type="text" id="modelName" class="form-input" placeholder="gpt-4o-all"> </div> <div class="form-group"> <label class="form-label">输出格式</label> <div id="formatOptions"> <span class="format-btn active" data-format="markdown">Markdown</span> <span class="format-btn" data-format="bullet">要点列表</span> <span class="format-btn" data-format="paragraph">段落</span> </div> </div> </div> </div> <button type="button" id="generateBtn"> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path> </svg> 生成总结 </button> <div id="summaryResult"> <div id="summaryHeader"> <h4>文章总结</h4> <div id="summaryActions"> <button id="copyBtn" class="action-btn" data-tooltip="复制到剪贴板"> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"></path> </svg> 复制 </button> </div> </div> <div id="summaryContent" class="markdown-body"> <textarea class="content-textarea resizable"></textarea> </div> </div> <div id="loadingIndicator"> <div class="spinner"></div> <p>正在生成总结,请稍候...</p> </div> </div> `; document.body.appendChild(app); // 加载 Markdown 处理库 Promise.all([loadMarkedJS(), loadHighlightJS()]).then(() => { console.log('Markdown 渲染库加载完成'); // 如果 marked 库加载成功,配置它 if (window.marked) { marked.setOptions({ renderer: new marked.Renderer(), highlight: function (code, lang) { if (window.hljs) { const language = lang && hljs.getLanguage(lang) ? lang : 'plaintext'; return hljs.highlight(code, { language }).value; } return code; }, langPrefix: 'hljs language-', pedantic: false, gfm: true, breaks: true, sanitize: false, smartypants: false, xhtml: false }); } }).catch(error => { console.error('加载 Markdown 渲染库失败:', error); }); // 获取DOM元素 const apiServiceSelect = document.getElementById('apiService'); const customApiUrlContainer = document.getElementById('customApiUrlContainer'); const customApiUrlInput = document.getElementById('customApiUrl'); const apiKeyInput = document.getElementById('apiKey'); const modelNameInput = document.getElementById('modelName'); const generateBtn = document.getElementById('generateBtn'); const summaryResult = document.getElementById('summaryResult'); const summaryContent = document.getElementById('summaryContent'); const loadingIndicator = document.getElementById('loadingIndicator'); const configToggle = document.getElementById('configToggle'); const configPanel = document.getElementById('configPanel'); const toggleMaxBtn = document.getElementById('toggleMaxBtn'); const toggleMinBtn = document.getElementById('toggleMinBtn'); const formatBtns = document.querySelectorAll('.format-btn'); const copyBtn = document.getElementById('copyBtn'); // 默认设置 const DEFAULT_API_SERVICE = 'gptgod'; const DEFAULT_CONFIGS = { gptgod: { url: 'https://api.gptgod.online/v1/chat/completions', model: 'gpt-4o-all', key: 'sk-L1rbJXBp3aDrZLgyrUq8FugKU54FxElTbzt7RfnBaWgHOtFj' }, deepseek: { url: 'https://api.deepseek.com/v1/chat/completions', model: 'deepseek-chat', key: '' }, custom: { url: '', model: '', key: '' } }; const DEFAULT_FORMAT = 'markdown'; // 从存储中恢复设置 const savedConfigs = GM_getValue('apiConfigs', DEFAULT_CONFIGS); const savedApiService = GM_getValue('apiService', DEFAULT_API_SERVICE); const savedFormat = GM_getValue('outputFormat', DEFAULT_FORMAT); const savedConfigCollapsed = GM_getValue('configCollapsed', false); // 设置输入框的值 const currentConfig = savedConfigs[savedApiService]; if (currentConfig) { apiKeyInput.value = currentConfig.key; modelNameInput.value = currentConfig.model; if (savedApiService === 'custom') { customApiUrlInput.value = currentConfig.url; customApiUrlContainer.style.display = 'block'; } } apiServiceSelect.value = savedApiService; // 设置输出格式按钮状态 formatBtns.forEach(btn => { if (btn.dataset.format === savedFormat) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); // 设置配置面板折叠状态 if (savedConfigCollapsed) { configPanel.classList.add('collapsed'); configToggle.querySelector('.toggle-icon').style.transform = 'rotate(-90deg)'; } // 事件监听 apiServiceSelect.addEventListener('change', function () { const service = this.value; const configs = GM_getValue('apiConfigs', DEFAULT_CONFIGS); const currentConfig = configs[service]; // 更新输入框的值 apiKeyInput.value = currentConfig.key; modelNameInput.value = currentConfig.model; if (service === 'custom') { customApiUrlInput.value = currentConfig.url; customApiUrlContainer.style.display = 'block'; } else { customApiUrlContainer.style.display = 'none'; } GM_setValue('apiService', service); }); // 修改输入框的事件监听 apiKeyInput.addEventListener('change', function () { const service = apiServiceSelect.value; const configs = GM_getValue('apiConfigs', DEFAULT_CONFIGS); configs[service].key = this.value; GM_setValue('apiConfigs', configs); }); customApiUrlInput.addEventListener('change', function () { const service = apiServiceSelect.value; const configs = GM_getValue('apiConfigs', DEFAULT_CONFIGS); configs[service].url = this.value; GM_setValue('apiConfigs', configs); }); modelNameInput.addEventListener('change', function () { const service = apiServiceSelect.value; const configs = GM_getValue('apiConfigs', DEFAULT_CONFIGS); configs[service].model = this.value; GM_setValue('apiConfigs', configs); }); // 配置面板折叠/展开 configToggle.addEventListener('click', function () { configPanel.classList.toggle('collapsed'); const isCollapsed = configPanel.classList.contains('collapsed'); const toggleIcon = this.querySelector('.toggle-icon'); toggleIcon.style.transform = isCollapsed ? 'rotate(-90deg)' : ''; GM_setValue('configCollapsed', isCollapsed); }); // 拖拽相关变量声明 let isDragging = false; let currentX; let currentY; let initialX; let initialY; const header = document.getElementById('summary-header'); header.addEventListener('mousedown', dragStart); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', dragEnd); function dragStart(e) { initialX = e.clientX - app.offsetLeft; initialY = e.clientY - app.offsetTop; isDragging = true; } function drag(e) { if (isDragging) { e.preventDefault(); currentX = e.clientX - initialX; currentY = e.clientY - initialY; // 确保不超出屏幕边界 currentX = Math.max(0, Math.min(currentX, window.innerWidth - app.offsetWidth)); currentY = Math.max(0, Math.min(currentY, window.innerHeight - app.offsetHeight)); app.style.left = currentX + "px"; app.style.top = currentY + "px"; } } function dragEnd() { isDragging = false; // 保存位置 GM_setValue('appPosition', { x: currentX, y: currentY }); } // 添加最大化/最小化功能 let isMaximized = false; let previousSize = {}; toggleMaxBtn.addEventListener('click', () => { if (!isMaximized) { // 保存当前大小和位置 previousSize = { width: app.style.width, height: app.style.height, left: app.style.left, top: app.style.top }; // 最大化 app.style.width = '100%'; app.style.height = '100vh'; app.style.left = '0'; app.style.top = '0'; toggleMaxBtn.innerHTML = ` <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M8 3v3a2 2 0 01-2 2H3m18 0h-3a2 2 0 01-2-2V3m0 18v-3a2 2 0 012-2h3M3 16h3a2 2 0 012 2v3"></path> </svg> `; } else { // 恢复之前的大小和位置 Object.assign(app.style, previousSize); toggleMaxBtn.innerHTML = ` <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"></path> </svg> `; } isMaximized = !isMaximized; }); // 输出格式选择 formatBtns.forEach(btn => { btn.addEventListener('click', function () { formatBtns.forEach(b => b.classList.remove('active')); this.classList.add('active'); GM_setValue('outputFormat', this.dataset.format); }); }); // 复制按钮功能 copyBtn.addEventListener('click', function () { const outputFormat = document.querySelector('.format-btn.active').dataset.format; let textToCopy; if (outputFormat === 'markdown') { // 获取原始的 Markdown 文本 textToCopy = summaryContent.getAttribute('data-markdown') || summaryContent.textContent; } else { textToCopy = summaryContent.textContent; } navigator.clipboard.writeText(textToCopy).then(() => { const originalHTML = this.innerHTML; this.innerHTML = ` <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M5 13l4 4L19 7"></path> </svg> 已复制 `; setTimeout(() => { this.innerHTML = originalHTML; }, 2000); }).catch(err => { console.error('复制失败:', err); alert('复制失败,请手动选择文本复制'); }); }); // 修改 simpleMarkdownRender 函数 function simpleMarkdownRender(text) { // 首先将文本包装在容器中 let html = '<div class="summary-container">'; // 处理文本内容 const content = text // 处理标题 .replace(/^# (.*$)/gm, '<h1>$1</h1>') .replace(/^## (.*$)/gm, '<h2>$1</h2>') .replace(/^### (.*$)/gm, '<h3>$1</h3>') // 处理列表 .replace(/^\d+\.\s+\*\*(.*?)\*\*:([\s\S]*?)(?=(?:\d+\.|$))/gm, (match, title, items) => { const listItems = items .split(/\n\s*-\s+/) .filter(item => item.trim()) .map(item => `<li>${item.trim()}</li>`) .join(''); return `<h2>${title}</h2><ul>${listItems}</ul>`; }) // 处理加粗 .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') // 处理代码 .replace(/`([^`]+)`/g, '<code>$1</code>') // 处理普通段落 .replace(/([^\n]+)(?:\n|$)/g, (match, p1) => { if (!p1.startsWith('<') && p1.trim()) { return `<p>${p1}</p>`; } return p1; }); // 关闭容器 html += content + '</div>'; return html; } // 修改生成总结按钮的事件处理 generateBtn.addEventListener('click', async function () { const apiService = apiServiceSelect.value; const configs = GM_getValue('apiConfigs', DEFAULT_CONFIGS); const currentConfig = configs[apiService]; const apiKey = apiKeyInput.value.trim(); const modelName = modelNameInput.value.trim(); const customApiUrl = customApiUrlInput.value.trim(); if (!apiKey) { alert('请输入有效的 API Key'); return; } // 保存当前配置 currentConfig.key = apiKey; currentConfig.model = modelName; if (apiService === 'custom') { currentConfig.url = customApiUrl; } GM_setValue('apiConfigs', configs); if (apiService === 'custom' && !customApiUrl) { alert('请输入自定义API地址'); return; } // 显示加载指示器 loadingIndicator.style.display = 'block'; generateBtn.disabled = true; generateBtn.innerHTML = ` <svg class="icon spinner" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="10" stroke-opacity="0.25" stroke-dasharray="30" stroke-dashoffset="0"></circle> <circle cx="12" cy="12" r="10" stroke-dasharray="30" stroke-dashoffset="15"></circle> </svg> 生成中... `; try { const content = await getArticleContent(); if (!content || content.length < 10) { throw new Error('无法获取文章内容或内容太短'); } const summary = await generateSummary(content, apiService, apiKey, customApiUrl, modelName, currentConfig.model); // 显示结果 if (currentConfig.model === 'markdown') { // 保存原始 Markdown 文本用于复制 summaryContent.setAttribute('data-markdown', summary); // 尝试使用 marked.js 渲染,如果不可用则使用简单渲染 if (window.marked) { summaryContent.innerHTML = marked.parse(summary); // 如果有 highlight.js,应用代码高亮 if (window.hljs) { document.querySelectorAll('#summaryContent pre code').forEach((block) => { hljs.highlightElement(block); }); } } else { // 使用优化后的简单渲染 summaryContent.innerHTML = simpleMarkdownRender(summary); } } else { summaryContent.innerHTML = simpleMarkdownRender(summary); } summaryResult.style.display = 'block'; } catch (error) { let errorMsg = error.message; // 提供更友好的错误信息 if (errorMsg.includes('Authentication Fails') || errorMsg.includes('no such user')) { errorMsg = 'API Key 无效或已过期,请更新您的 API Key'; } else if (errorMsg.includes('rate limit')) { errorMsg = 'API 调用次数已达上限,请稍后再试'; } alert('生成总结失败:' + errorMsg); console.error('API 错误详情:', error); } finally { // 隐藏加载指示器 loadingIndicator.style.display = 'none'; generateBtn.disabled = false; generateBtn.innerHTML = ` <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path> </svg> 生成总结 `; } }); // 获取文章内容 async function getArticleContent() { // 常见的文章内容选择器 const selectors = [ // 微信公众号 '#js_content', // 知乎 '.RichText', // 简书 '.article-content', // 掘金 '.article-content', // CSDN '#article_content', // 博客园 '#cnblogs_post_body', // 通用文章容器 'article', '.article', '.post-content', '.content', '.entry-content', '.article-content', // 如果找不到特定容器,尝试获取主要内容区域 'main', '#main', '.main' ]; // 尝试使用不同的选择器获取内容 for (const selector of selectors) { const element = document.querySelector(selector); if (element) { // 移除不需要的元素 const clone = element.cloneNode(true); const removeSelectors = [ 'script', 'style', 'iframe', 'nav', 'header', 'footer', '.advertisement', '.ad', '.ads', '.social-share', '.related-posts', '.comments', '.comment', '.author-info', '.article-meta', '.article-info', '.article-header', '.article-footer' ]; removeSelectors.forEach(selector => { const elements = clone.querySelectorAll(selector); elements.forEach(el => el.remove()); }); // 获取文本内容 let content = clone.innerText.trim(); // 清理文本 content = content .replace(/\s+/g, ' ') // 将多个空白字符替换为单个空格 .replace(/\n\s*\n/g, '\n') // 将多个空行替换为单个换行 .trim(); if (content.length > 100) { // 确保内容足够长 return content; } } } // 如果上述方法都失败,尝试获取整个页面的主要内容 const body = document.body.cloneNode(true); const removeSelectors = [ 'script', 'style', 'iframe', 'nav', 'header', 'footer', '.advertisement', '.ad', '.ads', '.social-share', '.related-posts', '.comments', '.comment', '.author-info', '.article-meta', '.article-info', '.article-header', '.article-footer', '#article-summary-app' // 移除我们的应用界面 ]; removeSelectors.forEach(selector => { const elements = body.querySelectorAll(selector); elements.forEach(el => el.remove()); }); let content = body.innerText.trim(); // 清理文本 content = content .replace(/\s+/g, ' ') // 将多个空白字符替换为单个空格 .replace(/\n\s*\n/g, '\n') // 将多个空行替换为单个换行 .trim(); if (content.length < 100) { throw new Error('无法获取足够的文章内容'); } return content; } // 生成总结 async function generateSummary(content, apiService, apiKey, customApiUrl, modelName, outputFormat) { let apiEndpoint; if (apiService === 'deepseek') { apiEndpoint = 'https://api.deepseek.com/v1/chat/completions'; } else if (apiService === 'gptgod') { apiEndpoint = 'https://api.gptgod.online/v1/chat/completions'; } else { apiEndpoint = customApiUrl; } // 根据输出格式调整系统提示 let systemPrompt; switch (outputFormat) { case 'markdown': systemPrompt = "请用中文总结以下文章的主要内容,以标准Markdown格式输出,包括标题、小标题和要点。确保格式规范,便于阅读。"; break; case 'bullet': systemPrompt = "请用中文总结以下文章的主要内容,以简洁的要点列表形式输出,每个要点前使用'- '标记。"; break; case 'paragraph': systemPrompt = "请用中文总结以下文章的主要内容,以连贯的段落形式输出,突出文章的核心观点和结论。"; break; default: systemPrompt = "请用中文总结以下文章的主要内容,以简洁的方式列出重点。"; } const messages = [ { role: "system", content: systemPrompt }, { role: "user", content: content } ]; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: apiEndpoint, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, data: JSON.stringify({ model: modelName, messages: messages, stream: false }), onload: function (response) { try { console.log('API响应状态码:', response.status); console.log('API响应内容:', response.responseText.substring(0, 200) + '...'); // 检查是否为HTML响应(通常表示错误页面) if (response.responseText.trim().startsWith('<')) { reject(new Error(`API返回了HTML而不是JSON (状态码: ${response.status})`)); return; } if (response.status >= 400) { try { const data = JSON.parse(response.responseText); reject(new Error(data.error?.message || `请求失败 (${response.status})`)); } catch (e) { reject(new Error(`请求失败 (${response.status}): ${response.responseText.substring(0, 100)}`)); } return; } const data = JSON.parse(response.responseText); if (data.error) { reject(new Error(data.error.message)); } else if (data.choices && data.choices[0] && data.choices[0].message) { resolve(data.choices[0].message.content); } else { console.error('异常API响应结构:', data); reject(new Error('API 返回格式异常')); } } catch (error) { console.error('解析响应失败:', error, response.responseText.substring(0, 200)); reject(new Error(`解析API响应失败: ${error.message}`)); } }, onerror: function (error) { console.error('请求错误:', error); reject(new Error('网络请求失败')); } }); }); } // 在 body 中添加最小化图标 const iconElement = document.createElement('div'); iconElement.id = 'article-summary-icon'; iconElement.innerHTML = ` <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M9 12h6m-6 4h6m2-10H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V8a2 2 0 00-2-2z"></path> </svg> `; document.body.appendChild(iconElement); // 修改最小化/最大化按钮的处理逻辑 toggleMinBtn.addEventListener('click', () => { app.classList.add('app-minimized'); iconElement.style.display = 'flex'; // 保存状态 GM_setValue('appMinimized', true); }); // 添加图标点击事件 iconElement.addEventListener('click', () => { app.classList.remove('app-minimized'); iconElement.style.display = 'none'; // 保存状态 GM_setValue('appMinimized', false); }); // 恢复上次的显示状态 const wasMinimized = GM_getValue('appMinimized', false); if (wasMinimized) { app.classList.add('app-minimized'); iconElement.style.display = 'flex'; } else { app.classList.remove('app-minimized'); iconElement.style.display = 'none'; } // 添加拖拽功能到图标 let iconDragging = false; let iconInitialX; let iconInitialY; let iconCurrentX; let iconCurrentY; iconElement.addEventListener('mousedown', (e) => { iconDragging = true; iconInitialX = e.clientX - iconElement.offsetLeft; iconInitialY = e.clientY - iconElement.offsetTop; iconElement.style.cursor = 'grabbing'; }); document.addEventListener('mousemove', (e) => { if (iconDragging) { e.preventDefault(); iconCurrentX = e.clientX - iconInitialX; iconCurrentY = e.clientY - iconInitialY; // 确保图标不会超出屏幕边界 iconCurrentX = Math.max(0, Math.min(iconCurrentX, window.innerWidth - iconElement.offsetWidth)); iconCurrentY = Math.max(0, Math.min(iconCurrentY, window.innerHeight - iconElement.offsetHeight)); iconElement.style.left = iconCurrentX + 'px'; iconElement.style.top = iconCurrentY + 'px'; iconElement.style.right = 'auto'; } }); document.addEventListener('mouseup', () => { if (iconDragging) { iconDragging = false; iconElement.style.cursor = 'pointer'; // 保存图标位置 GM_setValue('iconPosition', { x: iconCurrentX, y: iconCurrentY }); } }); // 恢复图标位置 const savedIconPosition = GM_getValue('iconPosition', null); if (savedIconPosition) { iconElement.style.right = 'auto'; iconElement.style.left = savedIconPosition.x + 'px'; iconElement.style.top = savedIconPosition.y + 'px'; } // 添加双击还原功能 let lastClickTime = 0; iconElement.addEventListener('click', (e) => { const currentTime = new Date().getTime(); const timeDiff = currentTime - lastClickTime; if (timeDiff < 300) { // 双击 // 重置位置 iconElement.style.right = '20px'; iconElement.style.left = 'auto'; iconElement.style.top = '20px'; GM_setValue('iconPosition', null); } lastClickTime = currentTime; }); })();