Greasy Fork

Website Summary

网页内容智能总结,支持自定义API和提示词

目前为 2025-03-14 提交的版本。查看 最新版本

// ==UserScript==
// @name         Website Summary
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  网页内容智能总结,支持自定义API和提示词
// @author       Your Name
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @require      https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // 配置项
    let config = {
        apiUrl: GM_getValue('apiUrl', 'https://api.openai.com/v1/chat/completions'),
        apiKey: GM_getValue('apiKey', ''),
        model: GM_getValue('model', 'gpt-3.5-turbo'),
        prompt: GM_getValue('prompt', '请总结以下网页内容,使用markdown格式:\n\n'),
        iconPosition: GM_getValue('iconPosition', { y: 20 })
    };

    // 等待外部库加载完成
    function waitForLibrary(name, callback, maxAttempts = 50) {
        let attempts = 0;
        const checkLibrary = () => {
            if (window[name]) {
                callback(window[name]);
                return;
            }
            attempts++;
            if (attempts < maxAttempts) {
                setTimeout(checkLibrary, 100);
            }
        };
        checkLibrary();
    }

    // 创建图标
    function createIcon() {
        const icon = document.createElement('div');
        icon.id = 'website-summary-icon';
        icon.innerHTML = `
            <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                <path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>
        `;

        icon.style.cssText = `
            position: fixed;
            z-index: 999999;
            width: 40px;
            height: 40px;
            background: rgba(255, 255, 255, 0.95);
            border-radius: 20px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            transition: all 0.3s ease;
            color: #007AFF;
            backdrop-filter: blur(10px);
            -webkit-backdrop-filter: blur(10px);
            right: 20px;
            top: ${config.iconPosition.y || 20}px;
        `;

        icon.addEventListener('mouseover', () => {
            icon.style.transform = 'scale(1.1)';
            icon.style.boxShadow = '0 4px 15px rgba(0, 0, 0, 0.2)';
            icon.style.color = '#0056b3';
        });

        icon.addEventListener('mouseout', () => {
            icon.style.transform = 'scale(1)';
            icon.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.1)';
            icon.style.color = '#007AFF';
        });

        icon.addEventListener('click', async () => {
            const container = document.getElementById('website-summary-container');
            if (!container) return;

            container.style.display = 'block';
            container.querySelector('#website-summary-content').innerHTML = '<p style="text-align: center; color: #666;">正在获取总结...</p>';

            try {
                const content = getPageContent();
                if (!content) throw new Error('无法获取页面内容');
                const summary = await getSummary(content);
                if (!summary) throw new Error('获取总结失败');
                renderContent(summary);
            } catch (error) {
                console.error('总结过程出错:', error);
                container.querySelector('#website-summary-content').innerHTML = `
                    <p style="text-align: center; color: #ff4444;">
                        获取总结失败:${error.message}<br>
                        请检查API配置是否正确
                    </p>`;
            }
        });

        icon.addEventListener('contextmenu', (e) => createContextMenu(e, icon));
        makeDraggable(icon);
        document.body.appendChild(icon);
        return icon;
    }

    // 创建UI元素
    function createUI() {
        const container = document.createElement('div');
        container.id = 'website-summary-container';
        container.style.cssText = `
            position: fixed;
            z-index: 999998;
            background: rgba(255, 255, 255, 0.95);
            border-radius: 12px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            padding: 16px;
            width: 80%;
            max-width: 800px;
            max-height: 80vh;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            display: none;
            backdrop-filter: blur(10px);
            -webkit-backdrop-filter: blur(10px);
            left: 50%;
            top: 50%;
            transform: translate(-50%, -50%);
            overflow: hidden;
        `;

        const header = document.createElement('div');
        header.style.cssText = `
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 12px;
            cursor: move;
            padding-bottom: 8px;
            border-bottom: 1px solid #eee;
        `;

        const title = document.createElement('h3');
        title.textContent = '网页总结';
        title.style.margin = '0';
        title.style.fontSize = '18px';
        title.style.color = '#333';

        const buttonContainer = document.createElement('div');
        buttonContainer.style.cssText = `
            display: flex;
            gap: 8px;
            align-items: center;
        `;

        const copyBtn = document.createElement('button');
        copyBtn.textContent = '复制';
        copyBtn.style.cssText = `
            background: #4CAF50;
            color: white;
            border: none;
            padding: 6px 12px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.2s;
        `;

        copyBtn.addEventListener('mouseover', () => {
            copyBtn.style.backgroundColor = '#45a049';
        });

        copyBtn.addEventListener('mouseout', () => {
            copyBtn.style.backgroundColor = '#4CAF50';
        });

        copyBtn.addEventListener('click', () => {
            const content = document.getElementById('website-summary-content').innerText;
            navigator.clipboard.writeText(content).then(() => {
                const originalText = copyBtn.textContent;
                copyBtn.textContent = '已复制';
                setTimeout(() => {
                    copyBtn.textContent = originalText;
                }, 2000);
            }).catch(err => {
                console.error('复制失败:', err);
            });
        });

        const closeBtn = document.createElement('button');
        closeBtn.textContent = '×';
        closeBtn.style.cssText = `
            background: none;
            border: none;
            font-size: 24px;
            cursor: pointer;
            padding: 0 8px;
            color: #666;
            transition: color 0.2s;
        `;

        closeBtn.addEventListener('mouseover', () => {
            closeBtn.style.color = '#ff4444';
        });

        closeBtn.addEventListener('mouseout', () => {
            closeBtn.style.color = '#666';
        });

        const content = document.createElement('div');
        content.id = 'website-summary-content';
        content.style.cssText = `
            max-height: calc(80vh - 60px);
            overflow-y: auto;
            font-size: 14px;
            line-height: 1.6;
            padding: 8px 0;
        `;

        buttonContainer.appendChild(copyBtn);
        buttonContainer.appendChild(closeBtn);
        header.appendChild(title);
        header.appendChild(buttonContainer);
        container.appendChild(header);
        container.appendChild(content);
        document.body.appendChild(container);

        closeBtn.addEventListener('click', () => {
            container.style.display = 'none';
        });

        makeDraggable(container);
        return container;
    }

    // 创建设置界面
    function createSettingsUI() {
        const settingsContainer = document.createElement('div');
        settingsContainer.id = 'website-summary-settings';
        settingsContainer.style.cssText = `
            position: fixed;
            z-index: 1000000;
            background: rgba(255, 255, 255, 0.98);
            border-radius: 12px;
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
            padding: 20px;
            width: 400px;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            display: none;
            backdrop-filter: blur(10px);
            -webkit-backdrop-filter: blur(10px);
            left: 50%;
            top: 50%;
            transform: translate(-50%, -50%);
        `;

        const header = document.createElement('div');
        header.style.cssText = `
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20px;
            cursor: move;
        `;

        const title = document.createElement('h3');
        title.textContent = '设置';
        title.style.margin = '0';

        const closeBtn = document.createElement('button');
        closeBtn.textContent = '×';
        closeBtn.style.cssText = `
            background: none;
            border: none;
            font-size: 24px;
            cursor: pointer;
            padding: 0 8px;
            color: #666;
        `;

        const form = document.createElement('form');
        form.style.cssText = `
            display: flex;
            flex-direction: column;
            gap: 16px;
        `;

        // 创建输入字段
        const apiUrlInput = document.createElement('input');
        apiUrlInput.type = 'text';
        apiUrlInput.id = 'apiUrl';
        apiUrlInput.value = config.apiUrl;
        apiUrlInput.placeholder = '输入API URL';

        const apiKeyInput = document.createElement('input');
        apiKeyInput.type = 'password';
        apiKeyInput.id = 'apiKey';
        apiKeyInput.value = config.apiKey;
        apiKeyInput.placeholder = '输入API Key';

        const modelInput = document.createElement('input');
        modelInput.type = 'text';
        modelInput.id = 'model';
        modelInput.value = config.model;
        modelInput.placeholder = '输入AI模型名称';

        const promptInput = document.createElement('textarea');
        promptInput.id = 'prompt';
        promptInput.value = config.prompt;
        promptInput.placeholder = '输入提示词';
        promptInput.style.height = '100px';
        promptInput.style.resize = 'vertical';

        // 添加标签和输入框到表单
        const addFormField = (label, input) => {
            const fieldContainer = document.createElement('div');
            fieldContainer.style.cssText = `
                display: flex;
                flex-direction: column;
                gap: 4px;
            `;

            const labelElement = document.createElement('label');
            labelElement.textContent = label;
            labelElement.style.cssText = `
                font-size: 14px;
                color: #333;
                font-weight: 500;
            `;

            input.style.cssText = `
                width: 100%;
                padding: 8px;
                border: 1px solid #ddd;
                border-radius: 6px;
                font-family: inherit;
            `;

            fieldContainer.appendChild(labelElement);
            fieldContainer.appendChild(input);
            form.appendChild(fieldContainer);
        };

        addFormField('API URL', apiUrlInput);
        addFormField('API Key', apiKeyInput);
        addFormField('AI 模型', modelInput);
        addFormField('提示词', promptInput);

        const saveBtn = document.createElement('button');
        saveBtn.textContent = '保存设置';
        saveBtn.style.cssText = `
            background: #007AFF;
            color: white;
            border: none;
            padding: 10px;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
            font-weight: 500;
            transition: background-color 0.2s;
        `;

        saveBtn.addEventListener('mouseover', () => {
            saveBtn.style.backgroundColor = '#0056b3';
        });

        saveBtn.addEventListener('mouseout', () => {
            saveBtn.style.backgroundColor = '#007AFF';
        });

        // 修改保存逻辑
        saveBtn.addEventListener('click', (e) => {
            e.preventDefault();

            // 获取表单值
            const newApiUrl = apiUrlInput.value.trim();
            const newApiKey = apiKeyInput.value.trim();
            const newModel = modelInput.value.trim();
            const newPrompt = promptInput.value.trim();

            // 保存到GM存储
            GM_setValue('apiUrl', newApiUrl);
            GM_setValue('apiKey', newApiKey);
            GM_setValue('model', newModel);
            GM_setValue('prompt', newPrompt);

            // 更新内存中的配置
            config.apiUrl = newApiUrl;
            config.apiKey = newApiKey;
            config.model = newModel;
            config.prompt = newPrompt;

            // 显示保存成功提示
            const successMsg = document.createElement('div');
            successMsg.textContent = '设置已保存';
            successMsg.style.cssText = `
                position: fixed;
                top: 20px;
                left: 50%;
                transform: translateX(-50%);
                background: #4CAF50;
                color: white;
                padding: 10px 20px;
                border-radius: 4px;
                z-index: 1000001;
                font-size: 14px;
                box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            `;
            document.body.appendChild(successMsg);
            setTimeout(() => successMsg.remove(), 2000);

            // 关闭设置界面
            settingsContainer.style.display = 'none';
        });

        header.appendChild(title);
        header.appendChild(closeBtn);
        form.appendChild(saveBtn);
        settingsContainer.appendChild(header);
        settingsContainer.appendChild(form);
        document.body.appendChild(settingsContainer);

        closeBtn.addEventListener('click', () => {
            settingsContainer.style.display = 'none';
        });

        makeDraggable(settingsContainer);
        return settingsContainer;
    }

    // 修改右键菜单
    function createContextMenu(e, icon) {
        e.preventDefault();
        const menu = document.createElement('div');
        menu.style.cssText = `
            position: fixed;
            z-index: 1000000;
            background: rgba(255, 255, 255, 0.98);
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            padding: 8px 0;
            min-width: 150px;
            backdrop-filter: blur(10px);
            -webkit-backdrop-filter: blur(10px);
        `;

        const menuItems = [
            { text: '打开设置', action: () => {
                const settings = document.getElementById('website-summary-settings');
                if (settings) {
                    settings.style.display = 'block';
                }
            }}
        ];

        menuItems.forEach(item => {
            const menuItem = document.createElement('div');
            menuItem.textContent = item.text;
            menuItem.style.cssText = `
                padding: 8px 16px;
                cursor: pointer;
                transition: background-color 0.2s;
            `;
            menuItem.addEventListener('mouseover', () => {
                menuItem.style.backgroundColor = 'rgba(0, 0, 0, 0.05)';
            });
            menuItem.addEventListener('mouseout', () => {
                menuItem.style.backgroundColor = 'transparent';
            });
            menuItem.addEventListener('click', () => {
                item.action();
                menu.remove();
            });
            menu.appendChild(menuItem);
        });

        menu.style.left = `${e.clientX}px`;
        menu.style.top = `${e.clientY}px`;
        document.body.appendChild(menu);

        const closeMenu = (e) => {
            if (!menu.contains(e.target) && e.target !== icon) {
                menu.remove();
            }
        };
        document.addEventListener('click', closeMenu);
        menu.addEventListener('click', () => {
            document.removeEventListener('click', closeMenu);
        });
    }

    // 实现拖拽功能
    function makeDraggable(element) {
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        const header = element.querySelector('div') || element;

        header.addEventListener('mousedown', dragMouseDown);

        function dragMouseDown(e) {
            e.preventDefault();
            pos3 = e.clientX;
            pos4 = e.clientY;
            document.addEventListener('mouseup', closeDragElement);
            document.addEventListener('mousemove', elementDrag);
        }

        function elementDrag(e) {
            e.preventDefault();
            pos1 = pos3 - e.clientX;
            pos2 = pos4 - e.clientY;
            pos3 = e.clientX;
            pos4 = e.clientY;

            if (element.id === 'website-summary-icon') {
                const newTop = element.offsetTop - pos2;
                const maxTop = window.innerHeight - element.offsetHeight;
                const clampedTop = Math.max(0, Math.min(newTop, maxTop));
                element.style.top = clampedTop + "px";
                element.style.right = "20px";
                element.style.left = "auto";
            } else {
                element.style.top = (element.offsetTop - pos2) + "px";
                element.style.left = (element.offsetLeft - pos1) + "px";
            }
        }

        function closeDragElement() {
            document.removeEventListener('mouseup', closeDragElement);
            document.removeEventListener('mousemove', elementDrag);
            if (element.id === 'website-summary-icon') {
                config.iconPosition = { y: element.offsetTop };
                GM_setValue('iconPosition', config.iconPosition);
            }
        }
    }

    // 获取页面内容
    function getPageContent() {
        try {
            const clone = document.body.cloneNode(true);
            const elementsToRemove = clone.querySelectorAll('script, style, iframe, nav, header, footer, .ad, .advertisement, .social-share, .comment, .related-content');
            elementsToRemove.forEach(el => el.remove());
            return clone.innerText.replace(/\s+/g, ' ').trim().slice(0, 4000);
        } catch (error) {
            console.error('获取页面内容失败:', error);
            return document.body.innerText.slice(0, 4000);
        }
    }

    // 调用API获取总结
    function getSummary(content) {
        return new Promise((resolve, reject) => {
            // 直接从config中获取API Key
            const currentApiKey = config.apiKey;
            const currentApiUrl = config.apiUrl;
            const currentModel = config.model;
            const currentPrompt = config.prompt;

            console.log('当前API Key:', currentApiKey ? '已设置' : '未设置');
            console.log('当前API URL:', currentApiUrl);
            console.log('当前Model:', currentModel);

            if (!currentApiKey || currentApiKey.trim() === '') {
                resolve('请先设置API Key');
                return;
            }

            const requestData = {
                model: currentModel,
                messages: [
                    {
                        role: 'system',
                        content: '你是一个专业的网页内容总结助手,善于使用markdown格式来组织信息。'
                    },
                    {
                        role: 'user',
                        content: currentPrompt + content
                    }
                ],
                temperature: 0.7
            };

            GM_xmlhttpRequest({
                method: 'POST',
                url: currentApiUrl,
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${currentApiKey}`
                },
                data: JSON.stringify(requestData),
                onload: function(response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        if (data.error) {
                            console.error('API错误:', data.error);
                            resolve('API调用失败: ' + data.error.message);
                            return;
                        }
                        if (data.choices && data.choices[0] && data.choices[0].message) {
                            resolve(data.choices[0].message.content);
                        } else {
                            console.error('API响应格式错误:', data);
                            resolve('API响应格式错误,请检查配置。');
                        }
                    } catch (error) {
                        console.error('解析API响应失败:', error);
                        resolve('解析API响应失败,请检查网络连接。');
                    }
                },
                onerror: function(error) {
                    console.error('API调用失败:', error);
                    resolve('API调用失败,请检查网络连接和API配置。');
                }
            });
        });
    }

    // 渲染Markdown
    function renderContent(content) {
        const container = document.getElementById('website-summary-content');
        if (!container) return;

        try {
            if (!content) throw new Error('内容为空');

            // 处理Markdown内容
            let html = window.marked.parse(content);
            container.innerHTML = html;

            const style = document.createElement('style');
            style.textContent = `
                #website-summary-content {
                    font-size: 14px;
                    line-height: 1.6;
                    color: #333;
                }
                #website-summary-content h1,
                #website-summary-content h2,
                #website-summary-content h3 {
                    margin-top: 20px;
                    margin-bottom: 10px;
                    color: #222;
                }
                #website-summary-content p {
                    margin: 10px 0;
                }
                #website-summary-content code {
                    background: #f5f5f5;
                    padding: 2px 4px;
                    border-radius: 3px;
                    font-family: monospace;
                }
                #website-summary-content pre {
                    background: #f5f5f5;
                    padding: 15px;
                    border-radius: 5px;
                    overflow-x: auto;
                }
                #website-summary-content blockquote {
                    border-left: 4px solid #ddd;
                    margin: 10px 0;
                    padding-left: 15px;
                    color: #666;
                }
                #website-summary-content ul,
                #website-summary-content ol {
                    margin: 10px 0;
                    padding-left: 20px;
                }
                #website-summary-content li {
                    margin: 5px 0;
                }
                #website-summary-content table {
                    border-collapse: collapse;
                    width: 100%;
                    margin: 10px 0;
                }
                #website-summary-content th,
                #website-summary-content td {
                    border: 1px solid #ddd;
                    padding: 8px;
                    text-align: left;
                }
                #website-summary-content th {
                    background: #f5f5f5;
                }
            `;
            document.head.appendChild(style);
        } catch (error) {
            console.error('渲染内容失败:', error);
            container.innerHTML = '<p style="text-align: center; color: #ff4444;">渲染内容失败,请刷新页面重试。</p>';
        }
    }

    // 初始化
    function init() {
        try {
            waitForLibrary('marked', (marked) => {
                marked.setOptions({
                    breaks: true,
                    gfm: true
                });
            });

            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', () => {
                    createIcon();
                    createUI();
                    createSettingsUI();
                });
            } else {
                createIcon();
                createUI();
                createSettingsUI();
            }
        } catch (error) {
            console.error('初始化失败:', error);
        }
    }

    // 启动脚本
    init();
})();