您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
使用AI总结网页内容的油猴脚本,采用Shadow DOM隔离样式
// ==UserScript== // @name AI网页内容总结(增强版) // @namespace http://tampermonkey.net/ // @version 1.7 // @description 使用AI总结网页内容的油猴脚本,采用Shadow DOM隔离样式 // @author Jinfeng // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAAXNSR0IArs4c6QAABMBJREFUeF7tnU2LHGUQx/81wTXBBGRmtqfHCF7Vi+hBjYecA7mbHBTUi3fxBQ+iURERxLugJwPJFzAfIERQQchFIUchTk9Pd1bQkGB2t2TAwdlhd55+3rq6e2qvXS/9/H9PdXdNzfYQ9E9UARLNrsmhAIQ3gQJQAMIKCKfXClAAwgoIp9cKUADCCgin1wpQAMIKCKfXCmg7gGlZnuFdvMaEp8B4mggD4TVZpWfGpfFo8JGVU0Bj5wpg5q28KD9hprcB9AKeU+2hJCE4A8im5Y8gPF+7WpESSkFwAjCZlh8T4YNIWoiFlYBgDWA2+/O5Pd77ue2XnaMo1w3BGkCWl98AeENsm9aQuE4I9gCm5Q0QXlrVgUBg8Fp52mRTFwR7AHnxF0Ana9iI4inqgOAAoFy7zdNkYB2zTqWzfP35r55LbAjWYpkW0DUAcyAxISiAiuUXC4ICqAggViUoAAsAMSAoAEsAoSEoAAcAISEEA7Bostr0FOTbGIa4MQcDsNhIVQAURXH6wT59DcYLcz8C3+jR8TeT5GTmuCEru5keoysH+s/QF0LtAGazu+Pd/Xs3iWj7wGKZ8x4dfyY2hNAAfC9HtQPI8vIqgJcP3WnMl9PR8BXbXWhjHwOADwQJADsAHj1CtNtpMnjcRlBb21gAXCEIACgmAKWHC8d/pMnwtK2oNvYxAczPo8o9cPl8BQCUVwBcOOIS9F06Gr5qI6it7cYDyPO/033c/wWg8Yp4t/nE1rPjU6dmtqLa2G88gLlYWcaPMN15F8QXwbTb6+Eq7/W/TFO6ayOmi21nAbSlEXOBtuxzGMDlhq7x9wBfAaT9TRWkACITUgCRBTaFVwAmhSIfVwCRBTaFVwAmhSIfVwCRBTaF7wQAyXmASWDT8cYCqNqISc8DTAKbjre+EZOeB5gENh1vbAUsTtzUCWZ5KToPMAlsOt5+ANMiA9Ho8IXyJE2Gj5lEkDzefgC57DzAF17rAUjPAzYegPQ8QAH4KiDs3/pLkLB+3ukbC6BqI+atgHCA1jdiwvp5p29sBVRtxLwVEA6gABRAu/9L0pefVoCvgp7+nQCg84D/d0Ht3w3t4jxguahMnwavFmDtAHQecBBBMABVG7EuzgNa9dXETOcBB0ogWAVUbcQynQfIAtB5QKR7QNUK0HlAAwB49kKi7p1oxEQV9EyuADwF9HVXAL4Kevo3FkDVRsxz/eLuOhETRtDYCrB5DBXW0Cu9AvCSz9+5AQDWvevBf4HtjsBZmgxX3wCwdknWnwVN8vIaAefaLVScs2fg+3EyOG8T3RpAlhefAfS+TZINsv00TQZWr/W3BrCzs/PE/X/2bxFha4OENS6Vme89/FDvyX6//7vReMnAGsDcd5LfeY/An9sk6rwt4a10e/CV7TqdAMyTZNPyBxDOLBL6voGw5XGuj5L+WVvx5/bOAOY/4jOdlZcAegfAMZfkHfDZA/iL0fbgQyJ64LIeZwCLZFlZvohdvN7Wn7GyFY0ZBQi/EuM3PsbfjofDn2xjLNt7A/BJrr4elyAVL4wCWgFhdHSOogCcpQvjqADC6OgcRQE4SxfGUQGE0dE5igJwli6MowIIo6NzFAXgLF0YRwUQRkfnKArAWbowjv8CcMJrjudowOkAAAAASUVORK5CYII= // @match *://*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM.xmlHttpRequest // @connect * // @connect pieces-os-azure.vercel.app // @connect api.ephone.ai // @connect snowy-forest-7d66.ttjmggm.workers.dev // @connect generativelanguage.googleapis.com // @connect free-api.jinfeng-li.us.kg // @require https://cdnjs.cloudflare.com/ajax/libs/markdown-it/13.0.1/markdown-it.min.js // @license Apache-2.0 // ==/UserScript== // Copyright 2024 Jinfeng // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. (function() { 'use strict'; // 默认配置 const DEFAULT_CONFIG = { API_URL: 'https://api.openai.com/v1/chat/completions', API_KEY: 'sk-randomKey1234567890', MAX_TOKENS: 4000, SHORTCUT: 'Alt+S', PROMPT: '请用markdown格式全面总结以下网页内容,包含主要观点、关键信息和重要细节。总结需要完整、准确、有条理。', MODEL: 'gpt-4o-mini', CURRENT_CONFIG_NAME: '' // 用于存储当前使用的配置名称 }; // 获取配置 let CONFIG = {}; function loadConfig() { CONFIG = { API_URL: GM_getValue('API_URL', DEFAULT_CONFIG.API_URL), API_KEY: GM_getValue('API_KEY', DEFAULT_CONFIG.API_KEY), MAX_TOKENS: GM_getValue('MAX_TOKENS', DEFAULT_CONFIG.MAX_TOKENS), SHORTCUT: GM_getValue('SHORTCUT', DEFAULT_CONFIG.SHORTCUT), PROMPT: GM_getValue('PROMPT', DEFAULT_CONFIG.PROMPT), MODEL: GM_getValue('MODEL', DEFAULT_CONFIG.MODEL), CURRENT_CONFIG_NAME: GM_getValue('CURRENT_CONFIG_NAME', DEFAULT_CONFIG.CURRENT_CONFIG_NAME) }; // 如果存在已保存的当前配置名称,则加载该配置 if (CONFIG.CURRENT_CONFIG_NAME) { const savedConfig = loadSavedConfig(CONFIG.CURRENT_CONFIG_NAME); if (savedConfig) { CONFIG = { ...savedConfig, CURRENT_CONFIG_NAME: CONFIG.CURRENT_CONFIG_NAME }; } } return CONFIG; } // 预定义的提示词模版 const PROMPT_TEMPLATES = [ { title: "通用网页总结", content: "请用markdown格式全面总结以下网页内容,包含主要观点、关键信息和重要细节。总结需要完整、准确、有条理。" }, { title: "学术论文总结", content: "请用markdown格式总结这篇学术论文,包含以下要点:\n1. 研究目的和背景\n2. 研究方法\n3. 主要发现\n4. 结论和意义\n请确保总结准确、专业,并突出论文的创新点。" }, { title: "新闻事件总结", content: "请用markdown格式总结这则新闻,包含以下要点:\n1. 事件梗概(时间、地点、人物)\n2. 事件经过\n3. 影响和意义\n4. 各方反应\n请确保总结客观、准确,并突出新闻的重要性。" }, { title: "一句话概括", content: "请用一句简洁但信息量充足的话概括这段内容的核心要点。要求:不超过50个字,通俗易懂,突出重点。" }, { title: "知乎专业解答", content: "请以知乎回答的风格总结这段内容。要求:\n1. 开头要吸引眼球\n2. 分点论述,层次分明\n3. 使用专业术语\n4. 适当举例佐证\n5. 语气要专业且自信\n6. 结尾点题升华\n注意:要用markdown格式,保持知乎体特有的严谨专业但不失亲和力的风格。" }, { title: "表格化总结", content: "请将内容重点提取并整理成markdown表格格式。表格应当包含以下列:\n| 主题/概念 | 核心要点 | 补充说明 |\n要求条理清晰,重点突出,易于阅读。" }, { title: "深度分析", content: "请对内容进行深度分析,包含:\n1. 表层信息提炼\n2. 深层原因分析\n3. 可能的影响和发展\n4. 个人见解和建议\n注意:分析要有洞察力,观点要有独特性,论述要有逻辑性。使用markdown格式。" }, { title: "轻松幽默风", content: "请用轻松幽默的语气总结这段内容。要求:\n1. 口语化表达\n2. 适当使用梗和比喻\n3. 保持内容准确性\n4. 增加趣味性类比\n注意:幽默要得体,不失专业性。使用markdown格式。" }, { title: "要点清单", content: "请将内容整理成简洁的要点清单,要求:\n1. 用markdown的项目符号格式\n2. 每点都简洁明了(不超过20字)\n3. 按重要性排序\n4. 分类呈现(如适用)\n5. 突出关键词或数字" }, { title: "ELI5通俗解释", content: "请用简单易懂的语言解释这段内容,就像向一个五年级学生解释一样。要求:\n1. 使用简单的词汇\n2. 多用比喻和类比\n3. 避免专业术语\n4. 循序渐进地解释\n注意:解释要生动有趣,便于理解,但不能有失准确性。" }, { title: "观点对比", content: "请以对比的形式总结文中的不同观点或方面:\n\n### 正面观点/优势\n- 观点1\n- 观点2\n\n### 负面观点/劣势\n- 观点1\n- 观点2\n\n### 中立分析\n综合以上观点的分析和建议\n\n注意:要客观公正,论据充分。" }, { title: "Q&A模式", content: "请将内容重点转化为问答形式,要求:\n1. 问题要简洁清晰\n2. 答案要详细准确\n3. 由浅入深\n4. 覆盖核心知识点\n格式:\nQ1: [问题]\nA1: [答案]\n\n注意:问答要有逻辑性,便于理解和记忆。" }, { title: "商务简报", content: "请以商务简报的形式总结内容:\n\n### 执行摘要\n[一段概述]\n\n### 关键发现\n- 发现1\n- 发现2\n\n### 数据支撑\n[列出关键数据]\n\n### 行动建议\n1. 建议1\n2. 建议2\n\n注意:简报风格要专业、简洁、重点突出。" }, { title: "时间轴梳理", content: "请将内容按时间顺序整理成清晰的时间轴:\n\n### 时间轴\n- [时间点1]:事件/进展描述\n- [时间点2]:事件/进展描述\n\n### 关键节点分析\n[分析重要时间节点的意义]\n\n注意:要突出重要时间节点,并分析其意义。" }, { title: "观点提炼", content: "请提炼这段内容中的核心观点,按逻辑顺序列出。每个观点需要简洁明了,突出其关键性。要求:\n- 使用简洁的语言\n- 突出观点的主旨\n- 按照论点的层次组织" }, { title: "趋势预测", content: "请基于这段内容分析其背后的趋势,预测未来可能的发展方向。要求:\n- 提出一个清晰的趋势分析框架\n- 分析现有数据和信息如何推动这一趋势\n- 预测可能的行业影响和未来趋势\n- 提供具体的建议或行动步骤" }, { title: "关键问题分析", content: "请对文中提出的关键问题进行详细分析,包含以下要点:\n1. 问题的背景与成因\n2. 当前解决方案及其效果\n3. 可能的解决方案和优缺点\n4. 解决这一问题的长期影响和潜在风险\n要求:分析要有深度,确保逻辑严密,提出建设性意见。" }, { title: "对话式总结", content: "请将内容总结为对话式的形式,类似于对话问答。要求:\n- 通过模拟两个人的对话来呈现信息\n- 每个问题要简洁明了\n- 答案要准确、易懂,避免过于专业的术语\n- 对话可以适当加入互动与思考" }, { title: "SWOT分析", content: "请对这段内容进行SWOT分析(优势、劣势、机会、威胁)。要求:\n- 优势:列出文中描述的优势\n- 劣势:列出可能的劣势或挑战\n- 机会:分析潜在的机会\n- 威胁:分析可能面临的威胁" }, { title: "情景假设", content: "请基于这段内容,设定一个假设情景并进行分析。要求:\n- 提供假设情景的背景和设定\n- 根据现有内容推演可能的结果\n- 讨论可能面临的挑战与解决方案\n- 结合现实情况,给出合理的建议" }, { title: "步骤指南", content: "请将这段内容总结成一个清晰的操作步骤指南。要求:\n- 每一步操作清晰简洁\n- 每一步的目标或目的要明确\n- 适当提供示例或注意事项\n- 步骤顺序按逻辑组织" } ]; // 保存配置 function saveConfig(newConfig, configName = '') { // 保存基本配置到 GM storage Object.keys(newConfig).forEach(key => { GM_setValue(key, newConfig[key]); }); // 更新当前配置名称 if (configName) { GM_setValue('CURRENT_CONFIG_NAME', configName); // 如果选择了已保存的配置,也将其保存到 saved_configs const savedConfigs = getAllConfigs(); savedConfigs[configName] = { ...newConfig }; GM_setValue('saved_configs', savedConfigs); } // 更新内存中的配置 CONFIG = { ...CONFIG, ...newConfig, CURRENT_CONFIG_NAME: configName || CONFIG.CURRENT_CONFIG_NAME }; } // 更新配置选择器 function updateConfigSelectors(settingsPanel, modal) { const configs = getAllConfigs(); const configNames = Object.keys(configs); const currentConfigName = CONFIG.CURRENT_CONFIG_NAME; // 更新所有配置选择器的函数 const updateSelect = (select, includeCurrentConfig = false) => { if (!select) return; let options = []; // 添加默认选项 if (includeCurrentConfig) { options.push(`<option value="" ${!currentConfigName ? 'selected' : ''}>当前配置${!currentConfigName ? '(未保存)' : ''}</option>`); } else { options.push(`<option value="">--选择配置--</option>`); } // 添加已保存的配置 options = options.concat(configNames.map(name => `<option value="${name}" ${name === currentConfigName ? 'selected' : ''}>${name}</option>` )); select.innerHTML = options.join(''); }; // 更新设置面板的选择器 if (settingsPanel) { const settingsPanelSelect = settingsPanel.querySelector('#config-select'); updateSelect(settingsPanelSelect, false); // 更新操作按钮的显示状态 const configSelected = settingsPanelSelect.value !== ""; // 显示/隐藏删除配置按钮 const deleteConfigBtn = settingsPanel.querySelector('.delete-config-btn'); if (deleteConfigBtn) { deleteConfigBtn.style.display = configSelected ? 'inline-block' : 'none'; } // 显示/隐藏重命名配置按钮 const renameConfigBtn = settingsPanel.querySelector('.rename-config-btn'); if (renameConfigBtn) { renameConfigBtn.style.display = configSelected ? 'inline-block' : 'none'; } } // 更新总结模态框的选择器 if (modal) { const modalSelect = modal.querySelector('.ai-config-select'); updateSelect(modalSelect, true); } } // 重命名配置的函数 function renameConfig(oldName, newName) { if (oldName === newName) return false; const configs = getAllConfigs(); if (!configs[oldName]) { alert('找不到要重命名的配置'); return false; } // 保存配置到新名称 configs[newName] = configs[oldName]; // 删除旧配置 delete configs[oldName]; // 保存更新后的配置 GM_setValue('saved_configs', configs); // 如果重命名的是当前使用的配置,更新当前配置名称 if (CONFIG.CURRENT_CONFIG_NAME === oldName) { CONFIG.CURRENT_CONFIG_NAME = newName; GM_setValue('CURRENT_CONFIG_NAME', newName); } return true; } // 初始化重命名相关的事件监听 function initializeRenameEvents(settingsPanel, modal) { const renameBtn = settingsPanel.querySelector('.rename-config-btn'); if (!renameBtn) return; renameBtn.addEventListener('click', () => { const currentConfigName = settingsPanel.querySelector('#config-select').value; if (!currentConfigName) { alert('请先选择要重命名的配置'); return; } // 显示重命名输入组 let renameGroup = settingsPanel.querySelector('.rename-input-group'); if (!renameGroup) { renameGroup = document.createElement('div'); renameGroup.className = 'rename-input-group'; renameGroup.innerHTML = ` <input type="text" id="config-rename" placeholder="输入新配置名称"> <button class="confirm-rename-btn">确认重命名</button> <button class="cancel-rename-btn">取消</button> `; // 插入到按钮组之前 settingsPanel.querySelector('.buttons').insertAdjacentElement('beforebegin', renameGroup); } renameGroup.style.display = 'flex'; // 设置输入框的默认值为当前配置名 const renameInput = renameGroup.querySelector('#config-rename'); renameInput.value = currentConfigName; renameInput.focus(); renameInput.select(); }); // 代理事件处理 settingsPanel.addEventListener('click', (e) => { if (e.target.classList.contains('confirm-rename-btn')) { const oldName = settingsPanel.querySelector('#config-select').value; const newName = settingsPanel.querySelector('#config-rename').value.trim(); if (!newName) { alert('请输入新配置名称'); return; } if (renameConfig(oldName, newName)) { // 更新选择器 updateConfigSelectors(settingsPanel, modal); // 隐藏重命名输入组 settingsPanel.querySelector('.rename-input-group').style.display = 'none'; alert('重命名成功'); } } else if (e.target.classList.contains('cancel-rename-btn')) { // 隐藏重命名输入组 settingsPanel.querySelector('.rename-input-group').style.display = 'none'; } }); } // 修改设置面板的事件处理 function initializeSettingsEvents(panel, modal, settingsOverlay) { const saveBtn = panel.querySelector('.save-btn'); const configSelect = panel.querySelector('#config-select'); const shortcutInput = panel.querySelector('#shortcut'); // 更新快捷键输入框的占位符提示 const isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform); shortcutInput.placeholder = isMac ? '例如: Option+S, ⌘+Shift+Y' : '例如: Alt+S, Ctrl+Shift+Y'; // 更新"应用设置"按钮文本 saveBtn.textContent = '保存并应用'; // 配置选择变更事件 configSelect.addEventListener('change', (e) => { const selectedConfig = loadSavedConfig(e.target.value); if (selectedConfig) { // 更新设置面板中的输入值 panel.querySelector('#api-url').value = selectedConfig.API_URL; panel.querySelector('#api-key').value = selectedConfig.API_KEY; panel.querySelector('#max-tokens').value = selectedConfig.MAX_TOKENS; // 根据系统显示适当的快捷键格式 shortcutInput.value = getSystemShortcutDisplay(selectedConfig.SHORTCUT); panel.querySelector('#prompt').value = selectedConfig.PROMPT; panel.querySelector('#model').value = selectedConfig.MODEL; } }); // 保存按钮点击事件 saveBtn.addEventListener('click', () => { let newShortcut = panel.querySelector('#shortcut').value.trim(); // 统一将 Option 转换为 Alt 存储 newShortcut = newShortcut.replace(/Option\+/g, 'Alt+'); if (!validateShortcut(newShortcut)) { alert(isMac ? '快捷键格式不正确,请使用例如 Option+S, ⌘+Shift+Y 的格式。' : '快捷键格式不正确,请使用例如 Alt+S, Ctrl+Shift+Y 的格式。' ); return; } const selectedConfigName = configSelect.value; const newConfig = { API_URL: panel.querySelector('#api-url').value.trim(), API_KEY: panel.querySelector('#api-key').value.trim(), MAX_TOKENS: parseInt(panel.querySelector('#max-tokens').value) || DEFAULT_CONFIG.MAX_TOKENS, SHORTCUT: newShortcut || DEFAULT_CONFIG.SHORTCUT, PROMPT: panel.querySelector('#prompt').value.trim() || DEFAULT_CONFIG.PROMPT, MODEL: panel.querySelector('#model').value.trim() || DEFAULT_CONFIG.MODEL }; // 保存配置并更新当前配置名称 saveConfig(newConfig, selectedConfigName); // 更新两个面板中的配置选择器 updateConfigSelectors(panel, modal); // 关闭设置面板 panel.style.display = 'none'; settingsOverlay.style.display = 'none'; alert(`配置已保存并应用${selectedConfigName ? `(当前配置:${selectedConfigName})` : ''}`); }); } function getAllConfigs() { return GM_getValue('saved_configs', {}); } function saveConfigAs(name, config) { const configs = getAllConfigs(); configs[name] = config; GM_setValue('saved_configs', configs); } // 删除配置函数 function deleteConfig(name, panel, modal) { const configs = getAllConfigs(); delete configs[name]; GM_setValue('saved_configs', configs); // 如果删除的是当前正在使用的配置,重置为默认配置 if (name === CONFIG.CURRENT_CONFIG_NAME) { const defaultConfig = { ...DEFAULT_CONFIG, CURRENT_CONFIG_NAME: '' }; Object.keys(defaultConfig).forEach(key => { GM_setValue(key, defaultConfig[key]); }); CONFIG = defaultConfig; // 更新设置面板中的输入值为默认值 if (panel) { panel.querySelector('#api-url').value = DEFAULT_CONFIG.API_URL; panel.querySelector('#api-key').value = DEFAULT_CONFIG.API_KEY; panel.querySelector('#max-tokens').value = DEFAULT_CONFIG.MAX_TOKENS; panel.querySelector('#shortcut').value = DEFAULT_CONFIG.SHORTCUT; panel.querySelector('#prompt').value = DEFAULT_CONFIG.PROMPT; panel.querySelector('#model').value = DEFAULT_CONFIG.MODEL; } } // 保存更新后的配置 GM_setValue('saved_configs', configs); // 更新两个面板的配置选择器 updateConfigSelectors(panel, modal); return Object.keys(configs).length; } // 删除配置按钮事件处理 function initializeDeleteConfigButton(settingsPanel, modal) { const deleteBtn = settingsPanel.querySelector('.delete-config-btn'); const configSelect = settingsPanel.querySelector('#config-select'); // 删除配置按钮点击事件 deleteBtn.addEventListener('click', () => { const configName = configSelect.value; if (!configName) { alert('请先选择要删除的配置'); return; } if (confirm(`确定要删除配置"${configName}"吗?`)) { deleteConfig(configName, settingsPanel, modal); // 如果删除的是当前正在使用的配置,更新模态框中的配置显示 if (configName === CONFIG.CURRENT_CONFIG_NAME) { const modalSelect = modal.querySelector('.ai-config-select'); if (modalSelect) { modalSelect.value = ''; } // 如果有重试按钮,触发重新生成总结 const retryBtn = modal.querySelector('.ai-retry-btn'); if (retryBtn) { retryBtn.click(); } } alert(`配置"${configName}"已删除${configName === CONFIG.CURRENT_CONFIG_NAME ? ',已恢复默认配置' : ''}`); } }); } function loadSavedConfig(name) { const configs = getAllConfigs(); return configs[name]; } // 创建设置面板 function createSettingsPanel(shadow) { const panel = document.createElement('div'); panel.className = 'ai-settings-panel'; panel.innerHTML = ` <h3>设置</h3> <div class="form-group"> <label for="api-url">API URL</label> <input type="text" id="api-url" value="${CONFIG.API_URL}"> </div> <div class="form-group"> <label for="api-key">API Key</label> <input type="text" id="api-key" value="${CONFIG.API_KEY}"> </div> <div class="form-group"> <label for="model">模型</label> <input type="text" id="model" value="${CONFIG.MODEL}"> </div> <div class="form-group"> <label for="max-tokens">最大Token数</label> <input type="number" id="max-tokens" value="${CONFIG.MAX_TOKENS}"> </div> <div class="form-group"> <label for="shortcut">快捷键 (例如: Alt+S, Ctrl+Shift+Y)</label> <input type="text" id="shortcut" value="${CONFIG.SHORTCUT}"> </div> <div class="form-group"> <label for="prompt">总结提示词</label> <textarea id="prompt">${CONFIG.PROMPT}</textarea> </div> <div class="form-group config-select-group"> <label for="config-select">当前配置</label> <select class="ai-config-select" id="config-select"> <option value="">--选择配置--</option> ${Object.keys(getAllConfigs()).map(name => `<option value="${name}">${name}</option>` ).join('')} </select> <select class="ai-prompt-template-select" id="prompt-template-select"> <option value="">--提示词模版--</option> ${PROMPT_TEMPLATES.map(template => `<option value="${template.title}">${template.title}</option>` ).join('')} </select> </div> <div class="form-group save-as-group buttons" style="display: none;"> <label for="config-name">配置名称</label> <div class="save-as-input-group"> <input type="text" id="config-name" placeholder="输入配置名称"> <button class="confirm-save-as-btn">保存配置</button> <button class="cancel-save-as-btn">取消</button> </div> </div> <div class="form-group rename-group buttons" style="display: none;"> <label for="rename-config">重命名配置</label> <div class="rename-input-group"> <input type="text" id="rename-config" placeholder="输入新配置名称"> <button class="confirm-rename-btn">确认重命名</button> <button class="cancel-rename-btn">取消</button> </div> </div> <div class="buttons"> <button class="clear-cache-btn">恢复默认设置</button> <button class="delete-config-btn">删除此配置</button> <button class="save-as-btn">另存为新配置</button> <button class="rename-config-btn">重命名配置</button> <button class="cancel-btn">关闭</button> <button class="save-btn">应用设置</button> </div> `; // 样式定义在Shadow DOM内部 const style = document.createElement('style'); style.textContent = ` .ai-settings-panel { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); width: 90%; max-width: 600px; max-height: 80vh; overflow-y: auto; box-sizing: border-box; font-family: Microsoft Yahei,PingFang SC,HanHei SC,Arial; font-size: 15px; z-index: 100001; } .ai-settings-panel h3 { margin: 0 0 20px 0; padding-bottom: 10px; border-bottom: 1px solid #dee2e6; color: #495057; font-size: 18px; font-weight: 900; } .form-group { margin-bottom: 15px; } .form-group label { display: block; margin-bottom: 5px; color: #495057; font-weight: 600; } .form-group input, .form-group textarea { width: 100%; padding: 8px 12px; border: 1px solid #ced4da; border-radius: 4px; font-size: 14px; box-sizing: border-box; background: #fff; color: #495057; } .form-group input:focus, .form-group textarea:focus { outline: none; border-color: #60a5fa; box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.2); } .form-group textarea { height: 100px; resize: vertical; font-family: Microsoft Yahei,PingFang SC,HanHei SC,Arial; } .form-group.config-select-group { display: flex; align-items: center; gap: 10px; } .form-group.config-select-group label { flex: 0 0 auto; margin-bottom: 0; } .form-group:not(.config-select-group) { display: block; /* 恢复其他form-group的默认布局 */ } .buttons { display: flex; justify-content: space-around; gap: 10px; margin-top: 20px; } .buttons button { padding: 8px 8px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: bold; transition: background 0.3s; color: #fff; } .cancel-btn { background: #6c757d; } .cancel-btn:hover { background: #5a6268; } .clear-cache-btn { background: #b47474cc !important; } .clear-cache-btn:hover { background: #c82333 !important; } .ai-config-select { padding: 6px 12px; border: 1px solid #ced4da; border-radius: 4px; font-size: 14px; background: #fff; color: #495057; margin-right: 10px; } .save-as-group { margin-top: 10px; padding-top: 10px; border-top: 1px solid #dee2e6; } .delete-config-btn { background: #b47474cc !important; } .delete-config-btn:hover { background: #c82333 !important; } .save-as-input-group { display: flex; gap: 10px; align-items: center; } .save-as-input-group input { flex: 1; } .save-as-input-group button { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: bold; color: #fff; } .save-btn, .confirm-save-as-btn { background: #617043cc !important; } .save-btn:hover, .confirm-save-as-btn:hover { background: #218838 !important; } .cancel-save-as-btn { background: #6c757d; } .cancel-save-as-btn:hover { background: #5a6268; } .save-as-btn, .rename-config-btn { background: #647f96cc !important; } .save-as-btn:hover, .rename-config-btn:hover { background: #2980b9 !important; } .ai-prompt-template-select { padding: 6px 12px; border: 1px solid #ced4da; border-radius: 4px; font-size: 14px; background: #fff; color: #495057; margin-left: 10px; flex-grow: 1; } .form-group.config-select-group { display: flex; align-items: center; gap: 10px; flex-wrap: nowrap; } .ai-config-select { flex-grow: 1; } .rename-input-group { display: none; gap: 10px; margin: 10px 0; padding: 10px 0; border-top: 1px solid #dee2e6; } .rename-input-group input { flex: 1; padding: 8px 12px; border: 1px solid #ced4da; border-radius: 4px; font-size: 14px; } .rename-input-group button { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: bold; color: #fff; } .rename-input-group .confirm-rename-btn { background: #617043cc; } .rename-input-group .confirm-rename-btn:hover { background: #218838; } .rename-input-group .cancel-rename-btn { background: #6c757d; } .rename-input-group .cancel-rename-btn:hover { background: #5a6268; } `; // 创建新的覆盖层 const settingsOverlay = document.createElement('div'); settingsOverlay.className = 'ai-settings-overlay'; settingsOverlay.style.display = 'none'; // 默认隐藏 // 添加点击覆盖层关闭设置面板的事件 settingsOverlay.addEventListener('click', () => { panel.style.display = 'none'; settingsOverlay.style.display = 'none'; }); // 定义样式 const overlayStyle = document.createElement('style'); overlayStyle.textContent = ` .ai-settings-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 100000; /* 确保覆盖层在设置面板下方 */ } `; shadow.appendChild(overlayStyle); shadow.appendChild(settingsOverlay); shadow.appendChild(panel); // 事件监听 panel.querySelector('.save-btn').addEventListener('click', () => { const newShortcut = panel.querySelector('#shortcut').value.trim(); if (!validateShortcut(newShortcut)) { alert('快捷键格式不正确,请使用例如 Alt+S, Ctrl+Shift+Y 的格式。'); return; } const newConfig = { API_URL: panel.querySelector('#api-url').value.trim(), API_KEY: panel.querySelector('#api-key').value.trim(), MAX_TOKENS: parseInt(panel.querySelector('#max-tokens').value) || DEFAULT_CONFIG.MAX_TOKENS, SHORTCUT: newShortcut || DEFAULT_CONFIG.SHORTCUT, PROMPT: panel.querySelector('#prompt').value.trim() || DEFAULT_CONFIG.PROMPT, MODEL: panel.querySelector('#model').value.trim() || DEFAULT_CONFIG.MODEL }; saveConfig(newConfig); panel.style.display = 'none'; settingsOverlay.style.display = 'none'; }); panel.querySelector('.cancel-btn').addEventListener('click', () => { panel.style.display = 'none'; settingsOverlay.style.display = 'none'; }); // 清除缓存按钮事件 panel.querySelector('.clear-cache-btn').addEventListener('click', () => { const keys = ['API_URL', 'API_KEY', 'MAX_TOKENS', 'SHORTCUT', 'PROMPT', 'MODEL']; keys.forEach(key => GM_setValue(key, undefined)); // 设置为undefined模拟删除 // 重置为默认配置 CONFIG = { ...DEFAULT_CONFIG }; // 更新输入框的值 panel.querySelector('#api-url').value = CONFIG.API_URL; panel.querySelector('#api-key').value = CONFIG.API_KEY; panel.querySelector('#max-tokens').value = CONFIG.MAX_TOKENS; panel.querySelector('#shortcut').value = CONFIG.SHORTCUT; panel.querySelector('#prompt').value = CONFIG.PROMPT; panel.querySelector('#model').value = CONFIG.MODEL; alert('缓存已清除,已恢复默认设置'); }); // 添加提示词模版选择的事件处理 const promptTemplateSelect = panel.querySelector('#prompt-template-select'); const promptTextarea = panel.querySelector('#prompt'); promptTemplateSelect.addEventListener('change', (e) => { const selectedTemplate = PROMPT_TEMPLATES.find(t => t.title === e.target.value); if (selectedTemplate) { promptTextarea.value = selectedTemplate.content; } }); shadow.appendChild(style); return { panel, overlay: settingsOverlay }; } // 快捷键验证 function validateShortcut(shortcut) { // 更新正则表达式以支持 Option 键 const regex = /^((Ctrl|Alt|Shift|Meta|Option)\+)*[A-Za-z]$/; return regex.test(shortcut); } // 创建DOM元素并使用 Shadow DOM function createElements() { // 创建根容器 const rootContainer = document.createElement('div'); rootContainer.id = 'ai-summary-root'; // 附加 Shadow DOM const shadow = rootContainer.attachShadow({ mode: 'open' }); // 创建样式和结构 const style = document.createElement('style'); style.textContent = ` .ai-summary-container { position: fixed; bottom: 20px; right: 20px; display: flex; align-items: center; z-index: 99990; user-select: none; align-items: stretch; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); height: 30px; background-color: rgba(75, 85, 99, 0.8); border-radius: 5px; } .ai-drag-handle { width: 15px; height: 100%; background-color: rgba(75, 85, 99, 0.5); border-radius: 5px; cursor: move; margin-right: 1px; display: flex; align-items: center; justify-content: center; } .ai-drag-handle::before { content: "⋮"; color: #f3f4f6; font-size: 16px; transform: rotate(90deg); } .ai-summary-btn { padding: 5px 15px; background-color: rgba(75, 85, 99, 0.8); color: #f3f4f6; border: 1px solid rgba(75, 85, 99, 0.6); border-radius: 0 4px 4px 0; cursor: pointer; font-size: 12px; transition: all 0.3s; height: 100%; line-height: 1; font-family: Microsoft Yahei,PingFang SC,HanHei SC,Arial; } .ai-summary-btn:hover { background-color: rgba(75, 85, 99, 0.9); } .ai-summary-btn:active { transform: scale(0.95); transition: transform 0.1s; } .ai-summary-modal { user-select: none; display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 80%; max-width: 800px; max-height: 80vh; background: #f8f9fa; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); border-radius: 8px; z-index: 99995; overflow: hidden; font-family: Microsoft Yahei,PingFang SC,HanHei SC,Arial; } .ai-summary-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 99994; } .ai-summary-header { padding: 15px 20px; background: #e7ebee; border-bottom: 1px solid #dee2e6; display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; z-index: 1; } .ai-summary-header h3 { color: #495057; margin: 0; padding: 0; font-size: 18px; font-weight: 900; line-height: 1.4; font-family: inherit; } .ai-summary-close { background: none; border: none; font-size: 20px; cursor: pointer; color: #6c757d; padding: 0 5px; line-height: 1; font-family: inherit; } .ai-summary-close:hover { color: #495057; } .ai-summary-content { user-select: text; padding: 20px; overflow-y: auto; max-height: calc(80vh - 130px); line-height: 1.6; color: #374151; font-size: 15px; font-family: inherit; -webkit-overflow-scrolling: touch; /* 改善移动端滚动体验 */ } .ai-summary-content h1 { font-size: 1.8em; margin: 1.5em 0 0.8em; padding-bottom: 0.3em; border-bottom: 2px solid #e5e7eb; font-weight: 600; line-height: 1.3; color: #1f2937; } .ai-summary-content h2 { font-size: 1.5em; margin: 1.3em 0 0.7em; padding-bottom: 0.2em; border-bottom: 1px solid #e5e7eb; font-weight: 600; line-height: 1.3; color: #1f2937; } .ai-summary-content h3 { font-size: 1.3em; margin: 1.2em 0 0.6em; font-weight: 600; line-height: 1.3; color: #1f2937; } .ai-summary-content p { margin: 1em 0; line-height: 1.8; color: inherit; } .ai-summary-content ul, .ai-summary-content ol { margin: 1em 0; padding-left: 2em; line-height: 1.6; } .ai-summary-content li { margin: 0.5em 0; line-height: inherit; color: inherit; } .ai-summary-content blockquote { margin: 1em 0; padding: 0.5em 1em; border-left: 4px solid #60a5fa; background: #f3f4f6; color: #4b5563; font-style: normal; } .ai-summary-content code { background: #f3f4f6; padding: 0.2em 0.4em; border-radius: 3px; font-family: Consolas, Monaco, "Courier New", monospace; font-size: 0.9em; color: #d946ef; white-space: pre-wrap; } .ai-summary-content pre { background: #1f2937; color: #e5e7eb; padding: 1em; border-radius: 6px; overflow-x: auto; margin: 1em 0; white-space: pre; word-wrap: normal; } .ai-summary-content pre code { background: none; color: inherit; padding: 0; border-radius: 0; font-size: inherit; white-space: pre; } .ai-summary-content table { border-collapse: collapse; width: 100%; margin: 1em 0; font-size: inherit; } .ai-summary-content th, .ai-summary-content td { border: 1px solid #d1d5db; padding: 0.5em; text-align: left; color: inherit; background: none; } .ai-summary-content th { background: #f9fafb; font-weight: 600; } .ai-summary-footer { padding: 15px 20px; border-top: 1px solid #dee2e6; display: flex; justify-content: flex-end; gap: 10px; align-items: center; position: sticky; bottom: 0; background: #f0f2f4; z-index: 1; } .ai-summary-footer button { padding: 8px 16px; background: #6c757d; color: #fff; border: none; border-radius: 4px; cursor: pointer; display: inline-flex; align-items: center; gap: 8px; transition: background 0.3s; font-size: 14px; line-height: 1; font-family: inherit; } .ai-summary-footer button:hover { background: #5a6268; } .ai-download-btn svg, .ai-retry-btn svg, .ai-copy-btn svg, .ai-settings-btn svg { width: 20px; height: 20px; } .ai-loading { text-align: center; padding: 20px; color: #6c757d; font-family: inherit; } .ai-loading-dots:after { content: '.'; animation: dots 1.5s steps(5, end) infinite; } @keyframes dots { 0%, 20% { content: '.'; } 40% { content: '..'; } 60% { content: '...'; } 80%, 100% { content: ''; } } .ai-download-btn, .ai-summary-btn, .ai-retry-btn, .ai-copy-btn, .ai-settings-btn { z-index: 99991; position: relative; } /* 优化移动端响应式布局 */ @media (max-width: 768px) { .ai-settings-panel, .ai-summary-modal { width: 95%; max-height: 90vh; } .ai-summary-footer { flex-wrap: wrap; gap: 8px; } .ai-summary-container { bottom: 10px; right: 10px; } } .ai-summary-modal, .ai-summary-overlay, .ai-settings-panel { transition: opacity 0.2s ease-in-out; } .buttons button:active { transform: translateY(1px); } * { margin: 0; padding: 0; box-sizing: border-box; } .ai-summary-header, .ai-summary-footer, .ai-summary-close, ai-download-btn, .ai-settings-btn, .ai-retry-btn, .ai-copy-btn { user-select: none; } `; // 创建按钮和拖动把手 const container = document.createElement('div'); container.className = 'ai-summary-container'; container.innerHTML = ` <div class="ai-drag-handle"></div> <button class="ai-summary-btn">总结网页</button> `; // 创建模态框 const modal = document.createElement('div'); modal.className = 'ai-summary-modal'; modal.innerHTML = ` <div class="ai-summary-header"> <h3>网页内容总结</h3> <button class="ai-summary-close">×</button> </div> <div class="ai-summary-content"></div> <div class="ai-summary-footer"> <select class="ai-config-select"> <option value="">当前配置</option> ${Object.keys(getAllConfigs()).map(name => `<option value="${name}">${name}</option>` ).join('')} </select> <button class="ai-settings-btn" title="打开设置"> <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="3"></circle> <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path> </svg> </button> <button class="ai-retry-btn" title="重新总结"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M21 12a9 9 0 11-2.3-6M21 3v6h-6"></path> </svg> </button> <button class="ai-download-btn" title="下载总结"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> <polyline points="7 10 12 15 17 10"></polyline> <line x1="12" y1="15" x2="12" y2="3"></line> </svg> <span>下载总结</span> </button> <button class="ai-copy-btn"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> </svg> <span>复制总结</span> </button> </div> `; // 创建遮罩层 const overlay = document.createElement('div'); overlay.className = 'ai-summary-overlay'; // 创建设置面板 const { panel: settingsPanel, overlay: settingsOverlay } = createSettingsPanel(shadow); // 将所有元素添加到Shadow DOM shadow.appendChild(style); shadow.appendChild(container); shadow.appendChild(modal); shadow.appendChild(overlay); shadow.appendChild(settingsPanel); // 将根容器添加到body document.body.appendChild(rootContainer); return { container, button: container.querySelector('.ai-summary-btn'), modal, overlay, dragHandle: container.querySelector('.ai-drag-handle'), settingsPanel, settingsOverlay, // 返回新的覆盖层引用 shadow, downloadBtn: modal.querySelector('.ai-download-btn') }; } // 获取网页内容 function getPageContent() { const title = document.title; const content = document.body.innerText; return { title, content }; } // 显示错误信息 function showError(container, error, details = '') { container.innerHTML = ` <div class="ai-summary-error" style="color: red;"> <strong>错误:</strong> ${error} </div> ${details ? `<div class="ai-summary-debug">${details}</div>` : ''} `; } // 创建全局的markdown渲染器实例 const markdownRenderer = window.markdownit({ html: true, linkify: true, typographer: true, breaks: true }); // 全局变量,用于存储原始的 Markdown 文本 let originalMarkdownText = ''; // 打字机效果函数 function typeWriter(element, text, renderMarkdown, speed = 30, step = 5) { let index = 0; element.innerHTML = ''; // 清空内容 function type() { if (index < text.length) { const currentIndex = Math.min(index + step, text.length); const currentText = text.substring(0, currentIndex); // 使用 markdownRenderer 渲染当前文本 element.innerHTML = renderMarkdown(currentText); index = currentIndex; // 使用 requestAnimationFrame 代替 setTimeout requestAnimationFrame(type); } else { // 确保完全渲染 element.innerHTML = renderMarkdown(text); } } type(); } // 调用API进行总结 async function summarizeContent(content, shadow) { const contentContainer = shadow.querySelector('.ai-summary-content'); contentContainer.innerHTML = '<div class="ai-loading">正在生成总结<span class="ai-loading-dots"></span></div>'; let summary = ''; // 添加超时检查 const timeoutId = setTimeout(() => { contentContainer.innerHTML = ` <p>错误: 请求超时,请检查API URL、API Key和网络连接</p> `; // 由于无法直接 reject,这里只更新 DOM }, 20000); try { const requestPromise = new Promise((resolve, reject) => { console.log("Sending request to:", CONFIG.API_URL); // Log the URL console.log("Request headers:", { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CONFIG.API_KEY}` // Log the authorization header (carefully, don't expose your key publicly!) }); console.log("Request body:", { model: CONFIG.MODEL, messages: [ { role: 'system', content: CONFIG.PROMPT }, { role: 'user', content: content } ], max_tokens: CONFIG.MAX_TOKENS, temperature: 0.7, stream: false }); GM.xmlHttpRequest({ method: 'POST', url: CONFIG.API_URL, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CONFIG.API_KEY}` }, data: JSON.stringify({ model: CONFIG.MODEL, messages: [ { role: 'system', content: CONFIG.PROMPT }, { role: 'user', content: content } ], max_tokens: CONFIG.MAX_TOKENS, temperature: 0.7, stream: false // 使用非流式请求 }), onload: function(response) { console.log("Response status:", response.status); // Log the response status console.log("Response headers:", response.responseHeaders); // Log response headers console.log("Response text:", response.responseText); // Log the response body if (response.status >= 200 && response.status < 300) { try { const result = JSON.parse(response.responseText); summary = result.choices[0].message.content; // 存储完整的总结文本到全局变量 originalMarkdownText = summary; // 使用打字机效果逐步显示总结 typeWriter(contentContainer, summary, markdownRenderer.render.bind(markdownRenderer), 30, 5); clearTimeout(timeoutId); // 清除超时 resolve(summary); } catch (e) { clearTimeout(timeoutId); // 清除超时 reject(new Error(`解析响应失败: ${e.message}`)); } } else { clearTimeout(timeoutId); // 清除超时 reject(new Error(`API请求失败 (${response.status}): 请检查API URL和Key是否正确`)); } }, onerror: function(error) { console.error('请求错误:', error); clearTimeout(timeoutId); // 清除超时 reject(new Error('网络请求错误')); }, ontimeout: function() { clearTimeout(timeoutId); // 清除超时 reject(new Error('请求超时')); } }); }); // 等待请求完成或超时 summary = await requestPromise; return summary; } catch (error) { clearTimeout(timeoutId); // 清除超时 console.error('总结生成错误:', error); contentContainer.innerHTML = ` <p>错误: ${error.message}</p> `; throw error; } } // 初始化事件监听 function initializeEvents(elements) { const { container, button, modal, overlay, dragHandle, settingsPanel, settingsOverlay, shadow } = elements; // 初始化删除配置按钮 initializeDeleteConfigButton(settingsPanel, modal); // 初始化拖动功能 initializeDrag(container, dragHandle, shadow); // 点击按钮显示模态框 button.addEventListener('click', async () => { if (!CONFIG.API_KEY) { alert('请先配置API Key。'); settingsPanel.style.display = 'block'; settingsOverlay.style.display = 'block'; shadow.querySelector('.ai-summary-overlay').style.display = 'block'; return; } showModal(modal, overlay); const contentContainer = modal.querySelector('.ai-summary-content'); try { if (!CONFIG.API_URL) { throw new Error('请先配置API URL'); } const { content } = getPageContent(); if (!content.trim()) { throw new Error('网页内容为空,无法生成总结。'); } const summary = await summarizeContent(content, shadow); if (summary) { // contentContainer.innerHTML = markdownRenderer.render(summary); } } catch (error) { console.error('Summary Error:', error); showError(contentContainer, error.message); } }); // 关闭模态框 modal.querySelector('.ai-summary-close').addEventListener('click', () => { hideModal(modal, overlay); }); // 点击总结页面外的覆盖层关闭模态框 overlay.addEventListener('click', () => { hideModal(modal, overlay); }); // 下载按钮功能 elements.downloadBtn.addEventListener('click', () => { // 检查 originalMarkdownText 是否有内容 if (!originalMarkdownText) { alert('总结内容尚未生成或已失效。'); return; } // 提取第一行并去除markdown语法 let firstLine = originalMarkdownText.split('\n')[0].trim(); firstLine = firstLine.replace(/^#+\s*/, ''); // 移除开头的'#'和空格 if (!firstLine) { alert('总结内容格式错误,无法生成文件名。'); return; } // 生成安全的文件名 let safeFirstLine = firstLine.length > 30 ? firstLine.substring(0, 30) : firstLine; // 移除文件名中的非法字符 safeFirstLine = safeFirstLine.replace(/[<>:"/\\|?*]/g, ''); // 替换空格为下划线 const encodedFirstLine = encodeURIComponent(safeFirstLine).replace(/%20/g, '_'); const fileName = `网页总结-${encodedFirstLine}.md`; // 创建 Blob 并触发下载 const blob = new Blob([originalMarkdownText], { type: 'text/markdown;charset=utf-8' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.setAttribute('href', url); link.setAttribute('download', fileName); document.body.appendChild(link); link.click(); document.body.removeChild(link); }); // 复制按钮功能 modal.querySelector('.ai-copy-btn').addEventListener('click', () => { // 检查 originalMarkdownText 是否有内容 if (!originalMarkdownText) { alert('总结内容尚未生成或已失效。'); return; } // 使用保存的原始markdown文本 navigator.clipboard.writeText(originalMarkdownText).then(() => { const copyBtn = modal.querySelector('.ai-copy-btn'); const textSpan = copyBtn.querySelector('span'); const originalText = textSpan.textContent; textSpan.textContent = '已复制!'; textSpan.style.opacity = '0.7'; setTimeout(() => { textSpan.textContent = originalText; textSpan.style.opacity = '1'; }, 2000); }).catch(() => { alert('复制失败,请手动复制内容。'); }); }); // 添加快捷键支持 document.addEventListener('keydown', (e) => { if (isShortcutPressed(e, CONFIG.SHORTCUT)) { e.preventDefault(); button.click(); } if (e.key === 'Escape') { // 优先关闭设置面板 if (settingsPanel.style.display === 'block') { settingsPanel.style.display = 'none'; settingsOverlay.style.display = 'none'; } // 然后关闭总结模态框 if (modal.style.display === 'block') { hideModal(modal, overlay); } } }); // 添加重试按钮事件处理 modal.querySelector('.ai-retry-btn').addEventListener('click', async () => { const contentContainer = modal.querySelector('.ai-summary-content'); contentContainer.innerHTML = '<div class="ai-loading">正在重新生成总结<span class="ai-loading-dots"></span></div>'; try { const { content } = getPageContent(); if (!content.trim()) { throw new Error('网页内容为空,无法生成总结。'); } const summary = await summarizeContent(content, shadow); if (summary) { contentContainer.innerHTML = markdownRenderer.render(summary); } } catch (error) { console.error('Retry Error:', error); showError(contentContainer, error.message); } }); // 设置按钮功能(现在在模态框底部) modal.querySelector('.ai-settings-btn').addEventListener('click', () => { // 更新设置面板中的值 settingsPanel.querySelector('#api-url').value = CONFIG.API_URL; settingsPanel.querySelector('#api-key').value = CONFIG.API_KEY; settingsPanel.querySelector('#max-tokens').value = CONFIG.MAX_TOKENS; settingsPanel.querySelector('#shortcut').value = CONFIG.SHORTCUT; settingsPanel.querySelector('#prompt').value = CONFIG.PROMPT; settingsPanel.querySelector('#model').value = CONFIG.MODEL; settingsPanel.style.display = 'block'; settingsOverlay.style.display = 'block'; }); // 关闭设置面板时,隐藏其覆盖层 settingsPanel.querySelector('.cancel-btn').addEventListener('click', () => { settingsPanel.style.display = 'none'; settingsOverlay.style.display = 'none'; }); settingsPanel.querySelector('#config-select').addEventListener('change', (e) => { const selectedConfig = loadSavedConfig(e.target.value); const configSelected = e.target.value !== ""; // 更新表单值 if (selectedConfig) { settingsPanel.querySelector('#api-url').value = selectedConfig.API_URL; settingsPanel.querySelector('#api-key').value = selectedConfig.API_KEY; settingsPanel.querySelector('#max-tokens').value = selectedConfig.MAX_TOKENS; settingsPanel.querySelector('#shortcut').value = selectedConfig.SHORTCUT; settingsPanel.querySelector('#prompt').value = selectedConfig.PROMPT; settingsPanel.querySelector('#model').value = selectedConfig.MODEL; } // 更新按钮显示状态 settingsPanel.querySelector('.delete-config-btn').style.display = configSelected ? 'inline-block' : 'none'; settingsPanel.querySelector('.rename-config-btn').style.display = configSelected ? 'inline-block' : 'none'; }); // 另存为配置按钮事件 settingsPanel.querySelector('.save-as-btn').addEventListener('click', () => { const saveAsGroup = settingsPanel.querySelector('.save-as-group'); saveAsGroup.style.display = 'block'; }); // 保存新配置事件 settingsPanel.querySelector('#config-name').addEventListener('keyup', (e) => { if (e.key === 'Enter') { const configName = e.target.value.trim(); if (configName) { const newConfig = { API_URL: settingsPanel.querySelector('#api-url').value.trim(), API_KEY: settingsPanel.querySelector('#api-key').value.trim(), MAX_TOKENS: parseInt(settingsPanel.querySelector('#max-tokens').value), SHORTCUT: settingsPanel.querySelector('#shortcut').value.trim(), PROMPT: settingsPanel.querySelector('#prompt').value.trim(), MODEL: settingsPanel.querySelector('#model').value.trim() }; saveConfigAs(configName, newConfig); updateConfigSelectors(); settingsPanel.querySelector('.save-as-group').style.display = 'none'; e.target.value = ''; alert('配置已保存'); } } }); // 删除配置按钮事件 settingsPanel.querySelector('.delete-config-btn').addEventListener('click', () => { const configSelect = settingsPanel.querySelector('#config-select'); const configName = configSelect.value; if (configName && confirm(`确定要删除配置"${configName}"吗?`)) { deleteConfig(configName); // 如果删除的是当前正在使用的配置,则清除当前配置名称 if (configName === CONFIG.CURRENT_CONFIG_NAME) { CONFIG.CURRENT_CONFIG_NAME = ''; GM_setValue('CURRENT_CONFIG_NAME', ''); } updateConfigSelectors(); settingsPanel.querySelector('.delete-config-btn').style.display = 'none'; } }); // 总结面板中的配置选择事件 modal.querySelector('.ai-config-select').addEventListener('change', async (e) => { const configName = e.target.value; if (configName) { // 选择了已保存的配置 const selectedConfig = loadSavedConfig(configName); if (selectedConfig) { CONFIG = { ...selectedConfig, CURRENT_CONFIG_NAME: configName }; saveConfig(CONFIG); GM_setValue('CURRENT_CONFIG_NAME', configName); // 使用新配置重新生成总结 modal.querySelector('.ai-retry-btn').click(); } } else { // 如果选择了"当前配置",则恢复到未保存的当前配置状态 CONFIG.CURRENT_CONFIG_NAME = ''; GM_setValue('CURRENT_CONFIG_NAME', ''); // 注意:这里不需要重置其他配置项,保持当前的设置不变 } }); // 总结模态框中的配置选择事件 modal.querySelector('.ai-config-select').addEventListener('change', async (e) => { const configName = e.target.value; if (configName) { const selectedConfig = loadSavedConfig(configName); if (selectedConfig) { saveConfig(selectedConfig, configName); // 重新生成总结 modal.querySelector('.ai-retry-btn').click(); } } else { // 选择了"当前配置"选项 saveConfig(CONFIG, ''); } // 同步更新设置面板的选择器 updateConfigSelectors(settingsPanel, modal); }); // 初始化设置面板的事件 initializeSettingsEvents(settingsPanel, modal, settingsOverlay); // 初始化时更新一次选择器 updateConfigSelectors(settingsPanel, modal); // 另存为配置按钮事件 settingsPanel.querySelector('.save-as-btn').addEventListener('click', () => { const saveAsGroup = settingsPanel.querySelector('.save-as-group'); saveAsGroup.style.display = 'block'; settingsPanel.querySelector('#config-name').focus(); // 自动聚焦到输入框 }); // 取消保存配置 settingsPanel.querySelector('.cancel-save-as-btn').addEventListener('click', () => { const saveAsGroup = settingsPanel.querySelector('.save-as-group'); saveAsGroup.style.display = 'none'; settingsPanel.querySelector('#config-name').value = ''; }); // 保存配置的函数 function saveCurrentConfig(configName) { if (configName) { // 从设置面板获取当前的所有设置值 const newConfig = { API_URL: settingsPanel.querySelector('#api-url').value.trim(), API_KEY: settingsPanel.querySelector('#api-key').value.trim(), MAX_TOKENS: parseInt(settingsPanel.querySelector('#max-tokens').value) || DEFAULT_CONFIG.MAX_TOKENS, SHORTCUT: settingsPanel.querySelector('#shortcut').value.trim() || DEFAULT_CONFIG.SHORTCUT, PROMPT: settingsPanel.querySelector('#prompt').value.trim() || DEFAULT_CONFIG.PROMPT, MODEL: settingsPanel.querySelector('#model').value.trim() || DEFAULT_CONFIG.MODEL }; // 检查配置名是否已存在 if (getAllConfigs()[configName] && !confirm(`配置"${configName}"已存在,是否覆盖?`)) { return false; } // 保存配置到存储中 saveConfigAs(configName, newConfig); // 更新当前配置 CONFIG = { ...newConfig, CURRENT_CONFIG_NAME: configName }; GM_setValue('CURRENT_CONFIG_NAME', configName); // 更新两个面板中的配置选择器 updateConfigSelectors(settingsPanel, modal); // 重置并隐藏保存表单 settingsPanel.querySelector('.save-as-group').style.display = 'none'; settingsPanel.querySelector('#config-name').value = ''; alert('配置已保存并设为当前配置'); return true; } return false; } // 确认保存配置按钮事件 settingsPanel.querySelector('.confirm-save-as-btn').addEventListener('click', () => { const configName = settingsPanel.querySelector('#config-name').value.trim(); saveCurrentConfig(configName); }); // 保存新配置事件(回车键) settingsPanel.querySelector('#config-name').addEventListener('keyup', (e) => { if (e.key === 'Enter') { const configName = e.target.value.trim(); saveCurrentConfig(configName); } }); // 重命名按钮事件 settingsPanel.querySelector('.rename-config-btn').addEventListener('click', () => { const configSelect = settingsPanel.querySelector('#config-select'); const currentConfigName = configSelect.value; if (!currentConfigName) { alert('请先选择要重命名的配置'); return; } const renameGroup = settingsPanel.querySelector('.rename-group'); const renameInput = settingsPanel.querySelector('#rename-config'); // 设置输入框的默认值为当前配置名 renameInput.value = currentConfigName; // 显示重命名输入组 renameGroup.style.display = 'block'; // 聚焦输入框并选中文本 renameInput.focus(); renameInput.select(); }); // 确认重命名按钮事件 settingsPanel.querySelector('.confirm-rename-btn').addEventListener('click', () => { const configSelect = settingsPanel.querySelector('#config-select'); const oldName = configSelect.value; const newName = settingsPanel.querySelector('#rename-config').value.trim(); if (!oldName) { alert('请先选择要重命名的配置'); return; } if (!newName) { alert('请输入新的配置名称'); return; } if (oldName === newName) { alert('新名称与原名称相同'); return; } // 检查新名称是否已存在 const configs = getAllConfigs(); if (configs[newName] && !confirm(`配置名"${newName}"已存在,是否覆盖?`)) { return; } // 执行重命名操作 if (renameConfig(oldName, newName)) { // 更新选择器 updateConfigSelectors(settingsPanel, modal); // 隐藏重命名输入组 settingsPanel.querySelector('.rename-group').style.display = 'none'; // 清空输入框 settingsPanel.querySelector('#rename-config').value = ''; alert('重命名成功'); } }); // 取消重命名按钮事件 settingsPanel.querySelector('.cancel-rename-btn').addEventListener('click', () => { const renameGroup = settingsPanel.querySelector('.rename-group'); renameGroup.style.display = 'none'; settingsPanel.querySelector('#rename-config').value = ''; }); // 初始化重命名相关的事件 initializeRenameEvents(elements.settingsPanel, elements.modal); } // 判断快捷键是否被按下 function isShortcutPressed(event, shortcut) { const keys = shortcut.split('+'); let ctrl = false, alt = false, shift = false, meta = false, key = null; keys.forEach(k => { const lower = k.toLowerCase(); if (lower === 'ctrl') ctrl = true; // 将 Option 键映射到 Alt 键,因为在 Mac 中 Option 键触发的是 altKey if (lower === 'alt' || lower === 'option') alt = true; if (lower === 'shift') shift = true; if (lower === 'meta') meta = true; if (lower.length === 1 && /^[a-z]$/.test(lower)) key = lower; }); if (key && event.key.toLowerCase() === key) { return event.ctrlKey === ctrl && event.altKey === alt && event.shiftKey === shift && event.metaKey === meta; } return false; } // 多系统适配的快捷键显示 function getSystemShortcutDisplay(shortcut) { const isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform); if (!isMac) return shortcut; // 为 Mac 系统转换快捷键显示 return shortcut.replace(/Alt\+/g, 'Option+') .replace(/Ctrl\+/g, '⌘+') .replace(/Meta\+/g, '⌘+'); } // 显示模态框 function showModal(modal, overlay) { modal.style.display = 'block'; overlay.style.display = 'block'; } // 隐藏模态框 function hideModal(modal, overlay) { modal.style.display = 'none'; overlay.style.display = 'none'; } const DOCK_POSITIONS = { LEFT: 'left', RIGHT: 'right', NONE: 'none' }; const DEBOUNCE_TIME = 10; // 防抖时间 const FOLD_DELAY = 1000; // 折叠延迟时间 const DOCK_THRESHOLD = 100; // 贴靠触发阈值 function savePosition(container) { const position = { left: container.style.left, top: container.style.top, right: container.style.right, bottom: container.style.bottom, dockPosition: container.dataset.dockPosition || DOCK_POSITIONS.NONE, windowWidth: window.innerWidth, windowHeight: window.innerHeight }; GM_setValue('containerPosition', position); } function loadPosition(container) { const savedPosition = GM_getValue('containerPosition'); if (savedPosition) { const currentWindowRatio = window.innerWidth / savedPosition.windowWidth; const heightRatio = window.innerHeight / (savedPosition.windowHeight || window.innerHeight); if (savedPosition.dockPosition === DOCK_POSITIONS.LEFT) { dockToLeft(container); } else if (savedPosition.dockPosition === DOCK_POSITIONS.RIGHT) { dockToRight(container); } else { // 计算新位置时考虑容器尺寸 const containerWidth = container.offsetWidth; const containerHeight = container.offsetHeight; // 计算并约束水平位置 const left = parseInt(savedPosition.left) * currentWindowRatio; const maxLeft = window.innerWidth - containerWidth; const safeLeft = Math.max(0, Math.min(left, maxLeft)); // 计算并约束垂直位置 const rawTop = parseInt(savedPosition.top); let safeTop; if (rawTop * heightRatio > window.innerHeight - containerHeight) { // 如果计算后的位置会超出窗口底部,则放置在可见区域内 safeTop = window.innerHeight - containerHeight - 20; // 20px作为底部边距 } else { // 否则保持相对位置 safeTop = Math.max(0, Math.min(rawTop * heightRatio, window.innerHeight - containerHeight)); } // 应用安全位置 container.style.left = `${safeLeft}px`; container.style.top = `${safeTop}px`; container.style.right = 'auto'; container.style.bottom = 'auto'; } } } function initializeDrag(container, dragHandle, shadow) { let isDragging = false; let currentX; let currentY; let initialX; let initialY; let foldTimeout; const style = document.createElement('style'); style.textContent = ` .ai-summary-container { transition: transform 0.3s ease; } .ai-summary-container.docked { transition: all 0.3s ease; } .ai-drag-handle { pointer-events: auto !important; } .ai-summary-container.docked .ai-summary-btn { width: 0; padding: 0; opacity: 0; overflow: hidden; border-color: rgba(75, 85, 99, 0); transition: all 0.3s ease, border-color 0.3s ease; } .ai-summary-container.docked.show-btn .ai-summary-btn { width: 80px; padding: 5px 15px; opacity: 1; } .ai-summary-container.docked:hover .ai-summary-btn { width: 80px; padding: 5px 15px; opacity: 1; } .ai-summary-container.right-dock { right: 0 !important; left: auto !important; } .ai-summary-container.left-dock { left: 0 !important; right: auto !important; } `; shadow.appendChild(style); // 鼠标进入和离开事件处理 container.addEventListener('mouseenter', () => { clearTimeout(foldTimeout); // 清除之前的折叠计时器 if (container.classList.contains('docked')) { container.classList.add('show-btn'); } }); container.addEventListener('mouseleave', () => { if (container.classList.contains('docked')) { // 设置延迟折叠 foldTimeout = setTimeout(() => { container.classList.remove('show-btn'); }, FOLD_DELAY); } }); // 防抖函数 function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } loadPosition(container); dragHandle.addEventListener('mousedown', (e) => { isDragging = true; const rect = container.getBoundingClientRect(); initialX = e.clientX - rect.left; initialY = e.clientY - rect.top; // 开始拖动时,先记录当前位置 if (container.classList.contains('right-dock')) { currentX = window.innerWidth - container.offsetWidth; } else if (container.classList.contains('left-dock')) { currentX = 0; } else { currentX = rect.left; } currentY = rect.top; container.classList.remove('docked', 'right-dock', 'left-dock', 'show-btn'); container.dataset.dockPosition = DOCK_POSITIONS.NONE; document.body.style.userSelect = 'none'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; e.preventDefault(); const newX = e.clientX - initialX; const newY = e.clientY - initialY; const containerWidth = container.offsetWidth; const containerHeight = container.offsetHeight; if (e.clientX < DOCK_THRESHOLD) { dockToLeft(container); container.classList.add('show-btn'); // 贴靠时立即显示按钮 } else if (e.clientX > window.innerWidth - DOCK_THRESHOLD) { dockToRight(container); container.classList.add('show-btn'); // 贴靠时立即显示按钮 } else { const maxX = window.innerWidth - containerWidth; const maxY = window.innerHeight - containerHeight; currentX = Math.max(0, Math.min(newX, maxX)); currentY = Math.max(0, Math.min(newY, maxY)); container.style.left = `${currentX}px`; container.style.top = `${currentY}px`; container.style.right = 'auto'; container.dataset.dockPosition = DOCK_POSITIONS.NONE; container.classList.remove('docked', 'right-dock', 'left-dock', 'show-btn'); } }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; document.body.style.userSelect = 'auto'; savePosition(container); } }); // 使用防抖处理窗口调整 const debouncedLoadPosition = debounce(() => { loadPosition(container); }, DEBOUNCE_TIME); window.addEventListener('resize', debouncedLoadPosition); } function dockToLeft(container) { container.classList.add('docked', 'left-dock'); container.dataset.dockPosition = DOCK_POSITIONS.LEFT; container.style.left = '0'; container.style.right = 'auto'; } function dockToRight(container) { container.classList.add('docked', 'right-dock'); container.dataset.dockPosition = DOCK_POSITIONS.RIGHT; container.style.right = '0'; container.style.left = 'auto'; } // 1. 加载配置 loadConfig(); // 2. 创建元素 const elements = createElements(); // 3. 初始化事件 initializeEvents(elements); // 4. 检查配置是否完整 if (!CONFIG.API_URL || !CONFIG.API_KEY) { elements.settingsPanel.style.display = 'block'; elements.shadow.querySelector('.ai-summary-overlay').style.display = 'block'; alert('请先配置API URL和API Key。'); } })();