Greasy Fork

Greasy Fork is available in English.

Website Summary

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

当前为 2025-03-14 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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();
})();