Safari AI Summary Pro

Safari 专用 AI 页面总结工具,采用毛玻璃UI,支持暗黑模式,优化运行效率

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Safari AI Summary Pro
// @namespace    http://tampermonkey.net/
// @version      1.8
// @description  Safari 专用 AI 页面总结工具,采用毛玻璃UI,支持暗黑模式,优化运行效率
// @author       Justin Ye
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @grant        unsafeWindow
// @connect      api.openai.com
// @connect      *
// ==/UserScript==

(function() {
'use strict';

// 兼容性处理:获取正确的 window 对象
let targetWindow = window;
try {
    if (typeof unsafeWindow !== 'undefined') targetWindow = unsafeWindow;
    else if (window.unsafeWindow) targetWindow = window.unsafeWindow;
} catch (e) {
    console.warn('unsafeWindow access failed, falling back to window', e);
}

// --- 核心工具函数 ---

    // 封装 GM_xmlhttpRequest 为 Promise,解决跨域问题
    function gmFetch(url, options) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: options.method || 'GET',
                url: url,
                headers: options.headers,
                data: options.body,
                onload: (response) => {
                    if (response.status >= 200 && response.status < 300) {
                        resolve({
                            json: () => Promise.resolve(JSON.parse(response.responseText)),
                            text: () => Promise.resolve(response.responseText),
                            status: response.status
                        });
                    } else {
                        reject(new Error(`HTTP error! status: ${response.status} ${response.statusText || ''}`));
                    }
                },
                onerror: (error) => {
                    console.error('GM_xmlhttpRequest error:', error);
                    reject(new Error('Network error: Failed to fetch'));
                },
                ontimeout: () => {
                    reject(new Error('Request timeout'));
                }
            });
        });
    }

    // 配置管理
    const newPrompt = `你是一个专业的中文内容总结器。你的任务是分析提供的网页内容,**识别内容类型(例如:新闻报道、研究报告、普通文章、市场分析等)**,并在此基础上创建一个清晰、简洁、结构良好的中文总结。**总结必须严格依据原文内容,不得进行任何推测、假设或添加原文中未包含的信息。**

请严格遵守以下指南:

1.  **内容类型识别与结构确定:**
    *   首先识别原文的内容类型。
    *   根据内容类型,确定最合适的分段和总结重点。总结的结构应该逻辑清晰,反映原文的核心信息。
    *   你可以使用 **\`##\`** 作为主要分段的标题,例如:对于新闻可以使用“事件概述”、“关键进展”等,对于报告可以使用“主要发现”、“数据支持”等。不必拘泥于原文的固定分段,但要确保覆盖核心要点。

2.  **输出格式:**
    *   使用 **\`##\`** 表示主要段落标题。
    *   使用 **\`•\`** 表示段落内的关键点和细节。
    *   使用 **粗体** 突出重要术语、概念或关键信息。
    *   使用 **\`>\`** 表示引人注目的原文引述(如果适用)。

3.  **内容要求:**
    *   总结必须**严格忠于原文**,不允许加入任何个人观点、推测或假设。
    *   **识别并保留原文中的重要数据、数字、统计信息或关键事实。**
    *   根据识别的内容类型,调整总结的侧重点,但**所有信息必须来源于原文**。

4.  **写作风格:**
    *   语言清晰简洁。
    *   专业且客观的语调。
    *   逻辑流畅。
    *   易于理解。
    *   聚焦于原文的核心信息和重要细节。

5.  **重要规则:**
    *   **DO NOT show your reasoning process.** (不要显示你的思考过程或内部步骤。)`;

    const defaultConfig = {
        apiUrl: 'https://api.openai.com/v1/chat/completions',
        apiKey: '',
        model: 'gpt-3.5-turbo',
        prompt: newPrompt,
        theme: 'auto', // auto, light, dark
        shortcut: 'Alt+A'
    };

    const GM = {
        setValue: (key, value) => {
            if (typeof GM_setValue !== 'undefined') GM_setValue(key, value);
            else localStorage.setItem(`safari_pro_${key}`, JSON.stringify(value));
        },
        getValue: (key, defaultValue) => {
            if (typeof GM_getValue !== 'undefined') return GM_getValue(key, defaultValue);
            const value = localStorage.getItem(`safari_pro_${key}`);
            return value ? JSON.parse(value) : defaultValue;
        }
    };

    let config = {
        apiUrl: GM.getValue('apiUrl', defaultConfig.apiUrl),
        apiKey: GM.getValue('apiKey', defaultConfig.apiKey),
        model: GM.getValue('model', defaultConfig.model),
        prompt: GM.getValue('prompt', defaultConfig.prompt),
        theme: GM.getValue('theme', defaultConfig.theme),
        shortcut: GM.getValue('shortcut', defaultConfig.shortcut)
    };

    // Lazy Load Marked.js
    let markedLoaded = false;
    const loadMarked = () => {
        // Check if already loaded in page
        if (typeof targetWindow.marked !== 'undefined') {
            markedLoaded = true;
            return Promise.resolve();
        }

        return new Promise((resolve, reject) => {
            const script = document.createElement('script');
            script.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js';
            script.onload = () => {
                markedLoaded = true;
                // Configure marked in page context
                if (targetWindow.marked) {
                    targetWindow.marked.setOptions({ breaks: true, gfm: true });
                }
                resolve();
            };
            script.onerror = reject;
            document.head.appendChild(script);
        });
    };

    // --- UI 构建 (Glassmorphism) ---

    const style = document.createElement('style');
    style.textContent = `
        :root {
            --glass-bg: rgba(255, 255, 255, 0.75);
            --glass-border: rgba(255, 255, 255, 0.5);
            --glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
            --text-primary: #333333;
            --text-secondary: #666666;
            --accent-color: #007AFF; /* Safari Blue */
            --accent-hover: #0056b3;
            --input-bg: rgba(255, 255, 255, 0.5);
            --radius-lg: 16px;
            --radius-md: 12px;
            --radius-sm: 8px;
            --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
        }

        @media (prefers-color-scheme: dark) {
            :root {
                --glass-bg: rgba(30, 30, 30, 0.75);
                --glass-border: rgba(255, 255, 255, 0.1);
                --glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
                --text-primary: #f5f5f5;
                --text-secondary: #a0a0a0;
                --accent-color: #0A84FF;
                --accent-hover: #409CFF;
                --input-bg: rgba(0, 0, 0, 0.3);
            }
        }

        /* 强制主题覆盖 */
        [data-theme="light"] {
            --glass-bg: rgba(255, 255, 255, 0.75);
            --glass-border: rgba(255, 255, 255, 0.5);
            --text-primary: #333333;
            --text-secondary: #666666;
            --input-bg: rgba(255, 255, 255, 0.5);
        }
        [data-theme="dark"] {
            --glass-bg: rgba(30, 30, 30, 0.75);
            --glass-border: rgba(255, 255, 255, 0.1);
            --text-primary: #f5f5f5;
            --text-secondary: #a0a0a0;
            --input-bg: rgba(0, 0, 0, 0.3);
        }

        .sas-glass {
            background: var(--glass-bg);
            backdrop-filter: blur(20px);
            -webkit-backdrop-filter: blur(20px);
            border: 1px solid var(--glass-border);
            box-shadow: var(--glass-shadow);
        }

        .sas-container {
            position: fixed;
            z-index: 2147483647;
            font-family: var(--font-family);
            color: var(--text-primary);
            transition: all 0.3s ease;
        }

        /* 悬浮球 */
        .sas-fab {
            right: 24px;
            bottom: 24px;
            width: 48px;
            height: 48px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            user-select: none;
            font-size: 24px;
            transition: transform 0.2s, opacity 0.2s;
        }
        .sas-fab:hover { transform: scale(1.05); }
        .sas-fab:active { transform: scale(0.95); }

        /* 主面板 */
        .sas-panel {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%) scale(0.95);
            width: 90%;
            max-width: 600px;
            max-height: 85vh;
            border-radius: var(--radius-lg);
            display: flex;
            flex-direction: column;
            opacity: 0;
            pointer-events: none;
            transition: opacity 0.2s, transform 0.2s;
        }
        .sas-panel.show {
            opacity: 1;
            pointer-events: auto;
            transform: translate(-50%, -50%) scale(1);
        }

        .sas-header {
            padding: 16px 20px;
            border-bottom: 1px solid var(--glass-border);
            display: flex;
            justify-content: space-between;
            align-items: center;
            font-weight: 600;
            font-size: 18px;
        }

        .sas-close-btn, .sas-icon-btn {
            background: none;
            border: none;
            color: var(--text-secondary);
            cursor: pointer;
            font-size: 20px;
            padding: 4px;
            border-radius: 50%;
            transition: background 0.2s;
            display: flex;
            align-items: center;
            justify-content: center;
            width: 32px;
            height: 32px;
        }
        .sas-close-btn:hover, .sas-icon-btn:hover { background: rgba(128,128,128,0.1); }

        .sas-content {
            padding: 20px;
            overflow-y: auto;
            flex: 1;
            display: flex;
            flex-direction: column;
            gap: 20px;
        }
        
        #sas-settings-panel {
            background: rgba(128,128,128,0.05);
            padding: 15px;
            border-radius: var(--radius-md);
            border: 1px solid var(--glass-border);
        }

        /* 表单元素 */
        .sas-input-group { margin-bottom: 16px; }
        .sas-label {
            display: block;
            font-size: 13px;
            color: var(--text-secondary);
            margin-bottom: 6px;
            font-weight: 500;
        }
        .sas-input, .sas-textarea, .sas-select {
            width: 100%;
            padding: 10px 12px;
            border-radius: var(--radius-md);
            border: 1px solid var(--glass-border);
            background: var(--input-bg);
            color: var(--text-primary);
            font-family: inherit;
            font-size: 14px;
            box-sizing: border-box;
            transition: border-color 0.2s;
        }
        .sas-input:focus, .sas-textarea:focus, .sas-select:focus {
            outline: none;
            border-color: var(--accent-color);
        }
        .sas-textarea { min-height: 100px; resize: vertical; }

        .sas-btn {
            background: var(--accent-color);
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: var(--radius-md);
            font-weight: 500;
            cursor: pointer;
            width: 100%;
            font-size: 15px;
            transition: background 0.2s;
        }
        .sas-btn:hover { background: var(--accent-hover); }
        .sas-btn.secondary {
            background: transparent;
            border: 1px solid var(--glass-border);
            color: var(--text-primary);
        }
        .sas-btn.secondary:hover { background: rgba(128,128,128,0.1); }

        /* Markdown 内容优化 */
        .sas-markdown {
            font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Arial, sans-serif;
            line-height: 1.7;
            font-size: 15px;
            color: var(--text-primary);
            padding: 10px 0;
        }

        .sas-markdown h1, .sas-markdown h2 {
            font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Helvetica Neue", Arial, sans-serif;
            font-weight: 600;
            line-height: 1.3;
            margin: 1.8em 0 1em;
            color: var(--text-primary);
            font-size: 1.6em;
            letter-spacing: -0.01em;
        }

        .sas-markdown h3 {
            font-size: 1.3em;
            margin: 1.5em 0 0.8em;
            color: var(--text-primary);
            font-weight: 600;
            line-height: 1.4;
        }

        .sas-markdown p {
            margin: 0.8em 0;
            line-height: 1.75;
            letter-spacing: 0.01em;
            text-align: left;
            color: var(--text-primary);
        }

        .sas-markdown ul {
            margin: 0.6em 0;
            padding-left: 0.5em;
            list-style: none;
        }

        .sas-markdown ul li {
            display: flex;
            align-items: baseline;
            margin: 0.4em 0;
            line-height: 1.6;
            letter-spacing: 0.01em;
        }

        .sas-markdown ul li .bullet {
            color: var(--text-secondary);
            margin-right: 0.7em;
            font-weight: normal;
            flex-shrink: 0;
        }

        .sas-markdown ul li .text {
            flex: 1;
        }

        .sas-markdown strong {
            color: var(--text-primary);
            font-weight: 600;
        }

        .sas-markdown blockquote {
            margin: 1.2em 0;
            padding: 0.8em 1.2em;
            background: rgba(128,128,128,0.05);
            border-left: 4px solid var(--accent-color);
            border-radius: 6px;
            color: var(--text-secondary);
            font-style: italic;
        }

        .sas-markdown code {
            font-family: "SF Mono", Menlo, Monaco, Consolas, monospace;
            font-size: 0.9em;
            background: rgba(128,128,128,0.15);
            border: 1px solid var(--glass-border);
            border-radius: 4px;
            padding: 0.2em 0.4em;
            color: var(--accent-color);
        }

        .sas-markdown pre {
            background: rgba(128,128,128,0.1);
            border: 1px solid var(--glass-border);
            border-radius: 8px;
            padding: 1.2em;
            overflow-x: auto;
            margin: 1.2em 0;
        }

        /* 底部操作栏 */
        .sas-actions {
            padding: 16px 20px;
            border-top: 1px solid var(--glass-border);
            display: flex;
            gap: 10px;
            justify-content: flex-end;
        }

        /* 动画 */
        @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
        .sas-loading {
            display: inline-block;
            width: 20px;
            height: 20px;
            border: 2px solid rgba(128,128,128,0.3);
            border-radius: 50%;
            border-top-color: var(--accent-color);
            animation: spin 1s linear infinite;
            margin-right: 8px;
            vertical-align: middle;
        }
    `;
    document.head.appendChild(style);

    // --- DOM 元素创建 ---

    // 悬浮按钮
    const fab = document.createElement('div');
    fab.className = 'sas-container sas-glass sas-fab';
    // 使用 SF Symbols 风格的 SVG 图标
    fab.innerHTML = `
        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path d="M19 3H5C3.89 3 3 3.89 3 5V19C3 20.11 3.89 21 5 21H19C20.11 21 21 20.11 21 19V5C21 3.89 20.11 3 19 3ZM19 19H5V5H19V19Z" fill="currentColor"/>
            <path d="M7 7H17V9H7V7Z" fill="currentColor"/>
            <path d="M7 11H17V13H7V11Z" fill="currentColor"/>
            <path d="M7 15H14V17H7V15Z" fill="currentColor"/>
        </svg>
    `;
    fab.title = 'AI 页面总结 (右键设置)';
    document.body.appendChild(fab);

    // 主面板
    const panel = document.createElement('div');
    panel.className = 'sas-container sas-glass sas-panel';
    document.body.appendChild(panel);

    // --- 逻辑处理 ---

    let isPanelOpen = false;
    let isSettingsOpen = false;

    function applyTheme() {
        const theme = config.theme;
        if (theme === 'auto') {
            panel.removeAttribute('data-theme');
        } else {
            panel.setAttribute('data-theme', theme);
        }
    }

    function renderPanel() {
        applyTheme();
        
        // 如果没有 API Key,默认展开设置
        if (!config.apiKey) isSettingsOpen = true;

        const settingsHtml = `
            <div id="sas-settings-panel" style="display: ${isSettingsOpen ? 'block' : 'none'}">
                <div class="sas-input-group">
                    <label class="sas-label">API URL</label>
                    <input type="text" class="sas-input" id="sas-api-url" value="${config.apiUrl}">
                </div>
                <div class="sas-input-group">
                    <label class="sas-label">API Key</label>
                    <input type="password" class="sas-input" id="sas-api-key" value="${config.apiKey}" placeholder="sk-...">
                </div>
                <div class="sas-input-group">
                    <label class="sas-label">模型 (Model)</label>
                    <input type="text" class="sas-input" id="sas-model" value="${config.model}">
                </div>
                <div class="sas-input-group">
                    <label class="sas-label">提示词 (Prompt)</label>
                    <textarea class="sas-textarea" id="sas-prompt">${config.prompt}</textarea>
                </div>
                <div class="sas-input-group">
                    <label class="sas-label">快捷键 (Shortcut) - 点击输入,Backspace 清除</label>
                    <input type="text" class="sas-input" id="sas-shortcut" value="${config.shortcut}" placeholder="例如: Alt+A" readonly>
                </div>
                <div class="sas-input-group">
                    <label class="sas-label">主题</label>
                    <select class="sas-select" id="sas-theme">
                        <option value="auto" ${config.theme === 'auto' ? 'selected' : ''}>跟随系统</option>
                        <option value="light" ${config.theme === 'light' ? 'selected' : ''}>浅色</option>
                        <option value="dark" ${config.theme === 'dark' ? 'selected' : ''}>深色</option>
                    </select>
                </div>
                <button class="sas-btn" id="sas-save-settings">保存配置</button>
            </div>
        `;

        const resultHtml = `<div id="sas-result-area" class="sas-markdown"><p>点击下方按钮开始生成总结...</p></div>`;

        const pageTitle = document.title.length > 20 ? document.title.substring(0, 20) + '...' : document.title;

        panel.innerHTML = `
            <div class="sas-header">
                <span>${pageTitle}</span>
                <div style="display:flex; gap:8px;">
                    <button class="sas-icon-btn" id="sas-toggle-settings" title="设置">
                        <svg width="20" height="20" viewBox="0 0 24 24" 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.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="sas-close-btn">×</button>
                </div>
            </div>
            <div class="sas-content">
                ${settingsHtml}
                ${resultHtml}
            </div>
            <div class="sas-actions">
                <button class="sas-btn" id="sas-start-summary">开始总结</button>
            </div>
        `;

        // 绑定事件
        panel.querySelector('.sas-close-btn').onclick = closePanel;
        document.getElementById('sas-toggle-settings').onclick = () => {
            isSettingsOpen = !isSettingsOpen;
            const settingsPanel = document.getElementById('sas-settings-panel');
            settingsPanel.style.display = isSettingsOpen ? 'block' : 'none';
        };

        document.getElementById('sas-save-settings').onclick = saveSettings;
        document.getElementById('sas-start-summary').onclick = startSummary;
        
        // 快捷键录入
        const shortcutInput = document.getElementById('sas-shortcut');
        shortcutInput.addEventListener('keydown', (e) => {
            e.preventDefault();
            e.stopPropagation(); // 防止冲突

            if (e.key === 'Backspace' || e.key === 'Delete') {
                shortcutInput.value = '';
                return;
            }

            const keys = [];
            if (e.ctrlKey) keys.push('Ctrl');
            if (e.altKey) keys.push('Alt');
            if (e.shiftKey) keys.push('Shift');
            if (e.metaKey) keys.push('Meta');
            
            let key = e.key.toUpperCase();
            // 同样使用 e.code 处理
            if (e.code.startsWith('Key')) {
                key = e.code.slice(3).toUpperCase();
            } else if (e.code.startsWith('Digit')) {
                key = e.code.slice(5);
            }

            if (!['CONTROL', 'ALT', 'SHIFT', 'META', 'BACKSPACE', 'DELETE'].includes(key)) {
                keys.push(key);
            }
            
            if (keys.length > 0) {
                shortcutInput.value = keys.join('+');
            }
        });
    }

    function openPanel() {
        if (!panel.innerHTML) renderPanel(); // 初始化
        else {
             // 重新绑定事件或更新状态(如果需要)
             // 这里简单处理,每次打开都重新渲染以保证状态最新,或者只更新显示
             renderPanel();
        }
        panel.classList.add('show');
        isPanelOpen = true;
    }

    function closePanel() {
        panel.classList.remove('show');
        isPanelOpen = false;
    }

    function saveSettings() {
        config.apiUrl = document.getElementById('sas-api-url').value;
        config.apiKey = document.getElementById('sas-api-key').value;
        config.model = document.getElementById('sas-model').value;
        config.prompt = document.getElementById('sas-prompt').value;
        config.theme = document.getElementById('sas-theme').value;
        config.shortcut = document.getElementById('sas-shortcut').value;

        GM.setValue('apiUrl', config.apiUrl);
        GM.setValue('apiKey', config.apiKey);
        GM.setValue('model', config.model);
        GM.setValue('prompt', config.prompt);
        GM.setValue('theme', config.theme);
        GM.setValue('shortcut', config.shortcut);

        alert('配置已保存');
        applyTheme();
        // 保持设置面板打开或关闭取决于用户当前状态,这里不做改变
    }

    async function startSummary() {
        // 尝试从输入框获取最新配置(如果用户修改了但没点保存)
        const apiKeyInput = document.getElementById('sas-api-key');
        if (apiKeyInput) {
            config.apiKey = apiKeyInput.value;
            // 也可以顺便更新其他配置
            config.apiUrl = document.getElementById('sas-api-url').value;
            config.model = document.getElementById('sas-model').value;
            config.prompt = document.getElementById('sas-prompt').value;
        }

        if (!config.apiKey) {
            alert('请先配置 API Key');
            isSettingsOpen = true;
            renderPanel();
            return;
        }

        const resultArea = document.getElementById('sas-result-area');
        const startBtn = document.getElementById('sas-start-summary');
        
        startBtn.disabled = true;
        startBtn.innerHTML = '<span class="sas-loading"></span>生成中...';
        resultArea.innerHTML = '<p>正在分析页面内容,请稍候...</p>';

        try {
            // 1. 获取页面内容
            const pageContent = document.body.innerText.substring(0, 6000); // 增加字符限制

            // 2. 加载 marked.js
            await loadMarked();

            // 3. 调用 API
            const response = await gmFetch(config.apiUrl, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${config.apiKey}`
                },
                body: JSON.stringify({
                    model: config.model,
                    messages: [
                        { role: 'system', content: config.prompt },
                        { role: 'user', content: pageContent }
                    ]
                })
            });

            const data = await response.json();
            if (data.choices && data.choices[0]) {
                let markdown = data.choices[0].message.content;

                // 移除可能存在的 Markdown 代码块包裹,确保渲染为阅读模式
                markdown = markdown.replace(/^```(markdown)?\s*/i, '').replace(/\s*```$/, '');

                // 使用 targetWindow.marked
                const markedFunc = targetWindow.marked ? targetWindow.marked.parse : (text) => text;
                resultArea.innerHTML = markedFunc(markdown);
                
                // 添加复制按钮
                const copyBtn = document.createElement('button');
                copyBtn.className = 'sas-btn secondary';
                copyBtn.style.marginTop = '10px';
                copyBtn.textContent = '复制结果';
                copyBtn.onclick = () => {
                    navigator.clipboard.writeText(markdown);
                    copyBtn.textContent = '已复制!';
                    setTimeout(() => copyBtn.textContent = '复制结果', 2000);
                };
                resultArea.appendChild(copyBtn);
            } else {
                throw new Error('API 返回格式异常');
            }

        } catch (error) {
            resultArea.innerHTML = `<p style="color: #ff3b30;">出错啦: ${error.message}</p>`;
            console.error(error);
        } finally {
            startBtn.disabled = false;
            startBtn.innerHTML = '重新总结';
        }
    }

    // --- 拖拽功能 ---
    let isDragging = false;
    let dragStartX, dragStartY;
    let initialRight, initialBottom;

    fab.addEventListener('mousedown', (e) => {
        if (e.button !== 0) return; // 仅左键
        isDragging = false;
        dragStartX = e.clientX;
        dragStartY = e.clientY;
        
        const rect = fab.getBoundingClientRect();
        initialRight = window.innerWidth - rect.right;
        initialBottom = window.innerHeight - rect.bottom;

        const onMouseMove = (e) => {
            const dx = e.clientX - dragStartX;
            const dy = e.clientY - dragStartY;
            
            if (Math.abs(dx) > 5 || Math.abs(dy) > 5) isDragging = true;

            if (isDragging) {
                fab.style.right = `${initialRight - dx}px`;
                fab.style.bottom = `${initialBottom - dy}px`;
            }
        };

        const onMouseUp = () => {
            document.removeEventListener('mousemove', onMouseMove);
            document.removeEventListener('mouseup', onMouseUp);
        };

        document.addEventListener('mousemove', onMouseMove);
        document.addEventListener('mouseup', onMouseUp);
    });

    // --- 事件监听 ---

    fab.addEventListener('click', (e) => {
        e.stopPropagation(); // 阻止事件冒泡
        if (!isDragging) {
            if (isPanelOpen) closePanel();
            else openPanel();
        }
    });

    fab.addEventListener('contextmenu', (e) => {
        e.preventDefault();
        e.stopPropagation(); // 阻止事件冒泡
        if (!isDragging) {
            isSettingsOpen = true;
            openPanel();
        }
    });

    // 点击外部关闭
    document.addEventListener('click', (e) => {
        // 确保点击的不是面板内部或悬浮球
        if (isPanelOpen && !panel.contains(e.target) && !fab.contains(e.target)) {
            closePanel();
        }
    });
    
    // 阻止面板内部点击事件冒泡到 document
    panel.addEventListener('click', (e) => {
        e.stopPropagation();
    });

    // 全局快捷键监听
    document.addEventListener('keydown', (e) => {
        if (!config.shortcut) return;
        
        const keys = [];
        if (e.ctrlKey) keys.push('Ctrl');
        if (e.altKey) keys.push('Alt');
        if (e.shiftKey) keys.push('Shift');
        if (e.metaKey) keys.push('Meta');
        
        // 使用 e.code 来获取物理按键,避免大小写和布局问题
        // e.key 可能会受到输入法影响
        let key = e.key.toUpperCase();
        
        // 特殊处理一些按键
        if (e.code.startsWith('Key')) {
            key = e.code.slice(3).toUpperCase();
        } else if (e.code.startsWith('Digit')) {
            key = e.code.slice(5);
        }

        if (!['CONTROL', 'ALT', 'SHIFT', 'META'].includes(key)) {
            keys.push(key);
        }
        
        const currentShortcut = keys.join('+');
        if (currentShortcut === config.shortcut) {
            e.preventDefault();
            e.stopPropagation();
            if (isPanelOpen) {
                closePanel();
            } else {
                openPanel();
                // 快捷键直接开始总结
                startSummary();
            }
        }
    });

    // --- 菜单命令 ---
    if (typeof GM_registerMenuCommand !== 'undefined') {
        GM_registerMenuCommand("打开面板", () => openPanel());
    }

})();