NodeSeek AI 内容总结

使用自定义AI API总结NodeSeek帖子的内容,并提供设置面板。

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         NodeSeek AI 内容总结
// @name:en      NodeSeek AI Content Summarizer
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  使用自定义AI API总结NodeSeek帖子的内容,并提供设置面板。
// @description:en Use a custom AI API to summarize the content of NodeSeek posts, with a settings panel.
// @author       Gemini
// @match        https://www.nodeseek.com/post-*
// @match        https://www.deepflood.com/post-*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=nodeseek.com
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @connect      *
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- 样式定义 ---
    // 使用 GM_addStyle 添加 CSS 样式,避免污染页面
    GM_addStyle(`
        /* 控制面板容器 */
        .ns-ai-container {
            margin: 15px 0;
            padding: 15px;
            border: 1px solid #e0e0e0;
            border-radius: 8px;
            background-color: #f9f9f9;
            box-shadow: 0 2px 4px rgba(0,0,0,0.05);
        }

        /* 按钮通用样式 */
        .ns-ai-btn {
            padding: 8px 15px;
            border: 1px solid #ccc;
            border-radius: 5px;
            background-color: #fff;
            color: #333;
            cursor: pointer;
            font-size: 14px;
            margin-right: 10px;
            transition: all 0.2s ease;
        }

        .ns-ai-btn:hover {
            background-color: #f0f0f0;
            border-color: #bbb;
        }

        .ns-ai-btn-primary {
            background-color: #007bff;
            color: white;
            border-color: #007bff;
        }

        .ns-ai-btn-primary:hover {
            background-color: #0056b3;
            border-color: #0056b3;
        }

        /* 总结内容显示区域 */
        #ns-ai-summary-output {
            margin-top: 15px;
            padding: 15px;
            border: 1px dashed #ccc;
            border-radius: 5px;
            background-color: #fff;
            white-space: pre-wrap; /* 保持换行 */
            line-height: 1.6;
            color: #333;
        }

        /* 加载动画 */
        .ns-ai-loader {
            border: 4px solid #f3f3f3;
            border-radius: 50%;
            border-top: 4px solid #3498db;
            width: 20px;
            height: 20px;
            animation: spin 1s linear infinite;
            display: inline-block;
            vertical-align: middle;
            margin-left: 10px;
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        /* 设置弹窗样式 */
        .ns-ai-modal {
            display: none;
            position: fixed;
            z-index: 9999;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            overflow: auto;
            background-color: rgba(0,0,0,0.5);
            justify-content: center;
            align-items: center;
        }

        .ns-ai-modal-content {
            background-color: #fefefe;
            margin: auto;
            padding: 20px;
            border: 1px solid #888;
            width: 90%;
            max-width: 500px;
            border-radius: 8px;
            box-shadow: 0 4px 8px rgba(0,0,0,0.2);
        }

        .ns-ai-modal-content h2 {
            margin-top: 0;
            border-bottom: 1px solid #eee;
            padding-bottom: 10px;
        }

        .ns-ai-modal-content label {
            display: block;
            margin-top: 15px;
            margin-bottom: 5px;
            font-weight: bold;
        }

        .ns-ai-modal-content input {
            width: calc(100% - 20px);
            padding: 8px 10px;
            border: 1px solid #ccc;
            border-radius: 4px;
        }

        .ns-ai-modal-buttons {
            margin-top: 20px;
            text-align: right;
        }
    `);

    // --- HTML 结构 ---
    // 创建UI元素并插入到页面中
    function createUI() {
        // *** 修改点: 将目标元素从 .post-header 改为 .post-title ***
        const postTitle = document.querySelector('.post-title');
        if (!postTitle) {
            console.error('[NodeSeek AI] 无法找到帖子标题元素 .post-title。');
            return;
        }

        // 创建主容器
        const container = document.createElement('div');
        container.className = 'ns-ai-container';

        // 创建按钮
        const summarizeBtn = document.createElement('button');
        summarizeBtn.textContent = '一键总结';
        summarizeBtn.id = 'ns-ai-summarize-btn';
        summarizeBtn.className = 'ns-ai-btn ns-ai-btn-primary';

        const settingsBtn = document.createElement('button');
        settingsBtn.textContent = '设置';
        settingsBtn.id = 'ns-ai-settings-btn';
        settingsBtn.className = 'ns-ai-btn';

        // 创建总结输出区域
        const summaryOutput = document.createElement('div');
        summaryOutput.id = 'ns-ai-summary-output';
        summaryOutput.style.display = 'none'; // 默认隐藏

        // 组装UI
        container.appendChild(summarizeBtn);
        container.appendChild(settingsBtn);
        container.appendChild(summaryOutput);

        // *** 修改点: 插入到 .post-title 元素的后面 ***
        postTitle.parentNode.insertBefore(container, postTitle.nextSibling);

        // 创建设置弹窗
        createSettingsModal();

        // 绑定事件
        summarizeBtn.addEventListener('click', handleSummarize);
        settingsBtn.addEventListener('click', openSettingsModal);
    }

    // 创建设置弹窗的HTML
    function createSettingsModal() {
        const modal = document.createElement('div');
        modal.id = 'ns-ai-settings-modal';
        modal.className = 'ns-ai-modal';
        modal.innerHTML = `
            <div class="ns-ai-modal-content">
                <h2>AI API 设置</h2>
                <p>请填入兼容 OpenAI 格式的 API 信息。</p>
                <label for="ns-ai-api-url">API 地址 (URL):</label>
                <input type="text" id="ns-ai-api-url" placeholder="例如: https://api.openai.com/v1/chat/completions">

                <label for="ns-ai-api-key">密钥 (API Key):</label>
                <input type="password" id="ns-ai-api-key" placeholder="请输入您的 API Key">

                <label for="ns-ai-api-model">模型 (Model):</label>
                <input type="text" id="ns-ai-api-model" placeholder="例如: gpt-3.5-turbo">

                <div class="ns-ai-modal-buttons">
                    <button id="ns-ai-save-settings" class="ns-ai-btn ns-ai-btn-primary">保存</button>
                    <button id="ns-ai-cancel-settings" class="ns-ai-btn">取消</button>
                </div>
            </div>
        `;
        document.body.appendChild(modal);

        // 绑定弹窗内部事件
        document.getElementById('ns-ai-save-settings').addEventListener('click', saveSettings);
        document.getElementById('ns-ai-cancel-settings').addEventListener('click', closeSettingsModal);
        modal.addEventListener('click', (e) => {
            if (e.target === modal) {
                closeSettingsModal();
            }
        });
    }

    // --- 功能函数 ---

    // 处理总结按钮点击事件
    function handleSummarize() {
        const outputDiv = document.getElementById('ns-ai-summary-output');
        const summarizeBtn = document.getElementById('ns-ai-summarize-btn');
        outputDiv.style.display = 'block';
        outputDiv.innerHTML = '正在分析内容,请稍候... <div class="ns-ai-loader"></div>';
        summarizeBtn.disabled = true;

        // 1. 获取配置
        const apiUrl = GM_getValue('apiUrl');
        const apiKey = GM_getValue('apiKey');
        const model = GM_getValue('apiModel');

        if (!apiUrl || !apiKey || !model) {
            outputDiv.innerHTML = '⚠️ 配置不完整,请点击“设置”按钮填写 API 信息。';
            summarizeBtn.disabled = false;
            return;
        }

        // 2. 提取帖子内容
        const postContentElement = document.querySelector('article.post-content');
        if (!postContentElement) {
            outputDiv.innerHTML = '❌ 错误:无法找到帖子内容元素 `article.post-content`。';
            summarizeBtn.disabled = false;
            return;
        }
        // 使用 innerText 获取纯文本,去除HTML标签
        const postText = postContentElement.innerText.trim();

        if (postText.length < 50) { // 内容太短,不进行总结
             outputDiv.innerHTML = 'ℹ️ 内容过短,无需总结。';
             summarizeBtn.disabled = false;
             return;
        }

        // 3. 调用AI API
        callAiApi(apiUrl, apiKey, model, postText);
    }

    // 调用AI进行总结
    function callAiApi(url, key, model, text) {
        const outputDiv = document.getElementById('ns-ai-summary-output');
        const summarizeBtn = document.getElementById('ns-ai-summarize-btn');

        const prompt = `你是一个内容总结助手。请你用中文、精炼、客观、分点的形式总结以下帖子的核心内容,不要添加任何自己的评论或补充信息。帖子内容如下:\n\n---\n\n${text}`;

        GM_xmlhttpRequest({
            method: 'POST',
            url: url,
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${key}`
            },
            data: JSON.stringify({
                model: model,
                messages: [{ role: 'user', content: prompt }],
                temperature: 0.5, // 较低的温度使输出更具确定性
            }),
            timeout: 60000, // 60秒超时
            onload: function(response) {
                try {
                    const data = JSON.parse(response.responseText);
                    const summary = data.choices[0]?.message?.content;
                    if (summary) {
                        outputDiv.innerHTML = `<strong>🤖 AI 总结:</strong><br>${summary}`;
                    } else {
                        console.error('API 响应解析失败:', data);
                        outputDiv.innerText = `❌ API 响应格式不正确,请检查浏览器控制台获取详细信息。`;
                    }
                } catch (e) {
                    console.error('解析API响应时出错:', e);
                    outputDiv.innerText = `❌ 解析API响应失败,可能是网络问题或API返回了非JSON格式的数据。`;
                } finally {
                    summarizeBtn.disabled = false;
                }
            },
            onerror: function(error) {
                console.error('GM_xmlhttpRequest error:', error);
                outputDiv.innerText = '❌ 请求API失败,请检查网络连接、API地址是否正确,或查看浏览器控制台。';
                summarizeBtn.disabled = false;
            },
            ontimeout: function() {
                outputDiv.innerText = '❌ 请求超时,请检查网络或API服务是否可用。';
                summarizeBtn.disabled = false;
            }
        });
    }

    // 打开设置弹窗
    function openSettingsModal() {
        // 加载已保存的配置
        document.getElementById('ns-ai-api-url').value = GM_getValue('apiUrl', '');
        document.getElementById('ns-ai-api-key').value = GM_getValue('apiKey', '');
        document.getElementById('ns-ai-api-model').value = GM_getValue('apiModel', 'gpt-3.5-turbo');
        // 显示弹窗
        document.getElementById('ns-ai-settings-modal').style.display = 'flex';
    }

    // 关闭设置弹窗
    function closeSettingsModal() {
        document.getElementById('ns-ai-settings-modal').style.display = 'none';
    }

    // 保存设置
    function saveSettings() {
        const apiUrl = document.getElementById('ns-ai-api-url').value.trim();
        const apiKey = document.getElementById('ns-ai-api-key').value.trim();
        const apiModel = document.getElementById('ns-ai-api-model').value.trim();

        if (!apiUrl || !apiKey || !apiModel) {
            alert('API 地址、密钥和模型不能为空!');
            return;
        }

        GM_setValue('apiUrl', apiUrl);
        GM_setValue('apiKey', apiKey);
        GM_setValue('apiModel', apiModel);

        alert('设置已保存!');
        closeSettingsModal();
    }

    // --- 脚本启动 ---
    // 等待页面加载完成后执行
    window.addEventListener('load', createUI, false);

})();