您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
使用AI总结网页内容的油猴脚本,采用Shadow DOM隔离样式
当前为
// ==UserScript== // @name AI网页内容总结(增强版) // @namespace http://tampermonkey.net/ // @version 1.4 // @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 // @require https://cdnjs.cloudflare.com/ajax/libs/markdown-it/13.0.1/markdown-it.min.js // @license MIT // ==/UserScript== (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; } // 保存配置 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 settingsPanelSelect = settingsPanel.querySelector('#config-select'); settingsPanelSelect.innerHTML = ` <option value="">--选择配置--</option> ${configNames.map(name => `<option value="${name}" ${name === currentConfigName ? 'selected' : ''}>${name}</option>` ).join('')} `; // 更新总结模态框的选择器 const modalSelect = modal.querySelector('.ai-config-select'); modalSelect.innerHTML = ` <option value="" ${!currentConfigName ? 'selected' : ''}>当前配置${!currentConfigName ? '(未保存)' : ''}</option> ${configNames.map(name => `<option value="${name}" ${name === currentConfigName ? 'selected' : ''}>${name}</option>` ).join('')} `; // 显示/隐藏删除配置按钮 const deleteConfigBtn = settingsPanel.querySelector('.delete-config-btn'); if (deleteConfigBtn) { deleteConfigBtn.style.display = currentConfigName ? 'inline-block' : 'none'; } } // 修改设置面板的事件处理 function initializeSettingsEvents(panel, modal, settingsOverlay) { const saveBtn = panel.querySelector('.save-btn'); const configSelect = panel.querySelector('#config-select'); // 更新"应用设置"按钮文本 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; panel.querySelector('#shortcut').value = selectedConfig.SHORTCUT; panel.querySelector('#prompt').value = selectedConfig.PROMPT; panel.querySelector('#model').value = selectedConfig.MODEL; } }); // 保存按钮点击事件 saveBtn.addEventListener('click', () => { const newShortcut = panel.querySelector('#shortcut').value.trim(); if (!validateShortcut(newShortcut)) { alert('快捷键格式不正确,请使用例如 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; } } // 更新两个面板的配置选择器 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'); // 根据是否选择了配置来显示/隐藏删除按钮 configSelect.addEventListener('change', (e) => { deleteBtn.style.display = e.target.value ? 'block' : 'none'; }); // 删除配置按钮点击事件 deleteBtn.addEventListener('click', () => { const configName = configSelect.value; if (configName && confirm(`确定要删除配置"${configName}"吗?`)) { const remainingConfigs = deleteConfig(configName, settingsPanel, modal); // 隐藏删除按钮 deleteBtn.style.display = 'none'; // 如果是删除当前使用的配置,更新模态框中的配置显示 if (configName === CONFIG.CURRENT_CONFIG_NAME) { const modalSelect = modal.querySelector('.ai-config-select'); 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> </div> <div class="form-group save-as-group" 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="buttons"> <button class="clear-cache-btn">恢复默认设置</button> <button class="delete-config-btn">删除此配置</button> <button class="save-as-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: flex-end; gap: 10px; margin-top: 20px; } .buttons button { padding: 8px 16px; 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: #8b4513cc !important; } .clear-cache-btn:hover { background: #c82333; } .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: #cd5c5ccc !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: #6b8e23cc !important; } .save-btn:hover, .confirm-save-as-btn:hover { background: #218838; } .cancel-save-as-btn { background: #6c757d; } .cancel-save-as-btn:hover { background: #5a6268; } .save-as-btn { background: #4682b4cc !important; } .save-as-btn:hover { background: #2980b9 !important; } `; // 创建新的覆盖层 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('缓存已清除,已恢复默认设置'); }); shadow.appendChild(style); return { panel, overlay: settingsOverlay }; } // 快捷键验证 function validateShortcut(shortcut) { const regex = /^((Ctrl|Alt|Shift|Meta)\+)*[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 0 0 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: none; 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 { 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: #f1f3f5; 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 { 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: #f8f9fa; 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-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-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; } `; // 创建按钮和拖动把手 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-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 }; } // 获取网页内容 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>` : ''} `; } // 调用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 md = createMarkdownRenderer(); // 添加超时检查 const timeout = setTimeout(() => { throw new Error('请求超时,请检查API URL、API Key和网络连接'); }, 20000); try { const response = await fetch(CONFIG.API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CONFIG.API_KEY}` }, body: JSON.stringify({ model: CONFIG.MODEL, messages: [ { role: 'system', content: CONFIG.PROMPT }, { role: 'user', content: content } ], max_tokens: CONFIG.MAX_TOKENS, temperature: 0.7, stream: true }) }); // 检查响应状态 if (!response.ok) { clearTimeout(timeout); throw new Error(`API请求失败 (${response.status}): 请检查API URL和Key是否正确`); } const reader = response.body.getReader(); const decoder = new TextDecoder("utf-8"); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); const lines = chunk.split('\n'); for (const line of lines) { if (line.trim() === '' || line.trim() === 'data: [DONE]') continue; const jsonLine = line.replace(/^data: /, ''); try { const parsedData = JSON.parse(jsonLine); if (parsedData.choices && parsedData.choices[0] && parsedData.choices[0].delta) { if (parsedData.choices[0].delta.content) { summary += parsedData.choices[0].delta.content; contentContainer.innerHTML = md.render(summary); } } } catch (e) { console.warn('忽略无法解析的行:', line); continue; } } } clearTimeout(timeout); return summary; } catch (error) { clearTimeout(timeout); 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 () => { // 检查是否配置了API Key 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 = window.markdownit().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); }); // 复制按钮功能 modal.querySelector('.ai-copy-btn').addEventListener('click', () => { const content = modal.querySelector('.ai-summary-content').textContent; navigator.clipboard.writeText(content).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) { const md = window.markdownit({ html: true, linkify: true, typographer: true, breaks: true }); contentContainer.innerHTML = md.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); 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 = 'block'; } }); // 另存为配置按钮事件 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); } }); } // 判断快捷键是否被按下 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; if (lower === 'alt') 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; } // 增强markdown-it配置 function createMarkdownRenderer() { return window.markdownit({ html: true, linkify: true, typographer: true, breaks: true }); } // 显示模态框 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 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 }; GM_setValue('containerPosition', position); } function loadPosition(container) { const savedPosition = GM_getValue('containerPosition'); if (savedPosition) { const currentWindowRatio = window.innerWidth / savedPosition.windowWidth; if (savedPosition.dockPosition === DOCK_POSITIONS.LEFT) { dockToLeft(container); } else if (savedPosition.dockPosition === DOCK_POSITIONS.RIGHT) { dockToRight(container); } else { const left = parseInt(savedPosition.left) * currentWindowRatio; const top = savedPosition.top; container.style.left = `${Math.min(left, window.innerWidth - container.offsetWidth)}px`; container.style.top = top; container.style.right = 'auto'; container.style.bottom = 'auto'; } } } function initializeDrag(container, dragHandle, shadow) { let isDragging = false; let currentX; let currentY; let initialX; let initialY; // 修改CSS样式,移除pointer-events: none const style = document.createElement('style'); style.textContent = ` .ai-summary-container { transition: none; } .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; transition: all 0.3s ease; } .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); 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'); 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; // 检查是否需要贴靠 if (e.clientX < DOCK_THRESHOLD) { dockToLeft(container); } else if (e.clientX > window.innerWidth - DOCK_THRESHOLD) { dockToRight(container); } else { // 确保容器不会超出屏幕范围 const maxX = window.innerWidth - containerWidth; currentX = Math.max(0, Math.min(newX, maxX)); currentY = Math.max(0, Math.min(newY, window.innerHeight - container.offsetHeight)); 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'); } }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; document.body.style.userSelect = 'auto'; savePosition(container); } }); window.addEventListener('resize', () => { loadPosition(container); }); } 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'; } // 初始化时加载配置 loadConfig(); // 初始化脚本 const elements = createElements(); initializeEvents(elements); // 检查配置是否完整,如果不完整,则自动显示设置面板 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。'); } })();