dlzmoe / linuxdo-scripts

linux.do 增强插件,功能持续更新,欢迎提出新想法!
https://greasyfork.org/scripts/501827
354 stars 13 forks source link

【功能请求】增加一个总结所有评论的选项 #60

Open Passerby1011 opened 6 days ago

Passerby1011 commented 6 days ago

提出你的创意和功能想法,如果有参考的实现方法可以贴上链接。 https://linux.do/t/topic/186528

dlzmoe commented 5 days ago

image 这个佬很强,主要是获取全文数据这块我卡住了,所以没做这个功能,期待有更强的佬帮我完善一下。

Passerby1011 commented 5 days ago

主要是现在的脚本界面易用性太差了,佬如果有能力可以合并进增强脚本了,把界面改的易用一点

dlzmoe commented 5 days ago

主要是现在的脚本界面易用性太差了,佬如果有能力可以合并进增强脚本了,把界面改的易用一点

他的算法加密了,我技术不行,搞不出来 😫

Passerby1011 commented 3 days ago

主要是现在的脚本界面易用性太差了,佬如果有能力可以合并进增强脚本了,把界面改的易用一点

他的算法加密了,我技术不行,搞不出来 😫

悄悄的说一声,这个加密claude可以直接读,我用claude改了一下UI,prompt可能有点问题,帖子的处理逻辑完全没动

// ==UserScript==
// @name         Linux.do 回复内容总结 (修改版)
// @namespace    http://tampermonkey.net/
// @version      0.4
// @description  总结Linux.do论坛帖子中指定楼层之间的回复内容,修改了UI
// @author       NullUser
// @match        https://linux.do/*
// @icon         https://linux.do/uploads/default/optimized/1X/3a18b4b0da3e8cf96f7eea15241c3d251f28a39b_2_180x180.png
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    async function summarizeSomething(txt) {
        const API_KEY = GM_getValue('apiKey', '');
        const url = GM_getValue('apiUrl', 'https://xxxx.xxxx.xxxx/v1/chat/completions');
        const model = GM_getValue('model', 'deepseek-chat');

        const headers = {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${API_KEY}`
        };

        const data = {
            model: model,
            messages: [
                { role: 'system', content: '你可以帮我很好的总结帖子内容,所有的回复都以中文返回。' },
                { role: 'user', content: txt }
            ],
            stream: false
        };

        try {
            const response = await fetch(url, {
                method: 'POST',
                headers: headers,
                body: JSON.stringify(data)
            });

            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }

            const result = await response.json();
            console.log(result.choices[0].message.content);
            return result.choices[0].message.content;
        } catch (error) {
            console.error('Error fetching data:', error);
            return null;
        }
    }

    function formatDialogues(data) {
        const dialogues = data.post_stream.posts.map(post => {
            const {cooked, username, reply_to_user} = post;
            return reply_to_user ? `${username}回复${reply_to_user.username}说:${cooked}` : `${username}说:${cooked}`;
        });
        console.log(dialogues);
        return dialogues;
    }

    function safeSlice(arr, start, end) {
        start = Math.max(start, 0);
        end = end === undefined || end > arr.length ? arr.length : end;
        return arr.slice(start, end);
    }

    async function postid_to_url(building, startFloor, endFloor) {
        try {
            const url = `https://linux.do/t/${building}/post_ids.json?post_number=0&limit=99999`;
            const response = await fetch(url, fetchOptions);
            const data = await response.json();
            const post_ids = safeSlice(data.post_ids, startFloor - 1, endFloor - 1);

            if (startFloor == 1) {
                const url = `https://linux.do/t/${building}.json`;
                const response = await fetch(url, fetchOptions);
                const data = await response.json();
                const first_post_id = data.post_stream.posts[0].id;
                post_ids.unshift(first_post_id);
            }

            const chunk_size = 200;
            const urls = [];
            for (let i = 0; i < post_ids.length; i += chunk_size) {
                const chunk = post_ids.slice(i, i + chunk_size);
                const post_ids_param = chunk.map(id => `post_ids[]=${id}`).join('&');
                const url = `https://linux.do/t/${building}/posts.json?${post_ids_param}&include_suggested=false`;
                urls.push(url);
            }

            console.log(urls);
            return urls;
        } catch (error) {
            console.error(error);
            throw error;
        }
    }

    async function json_to_txt(building, startFloor, endFloor) {
        try {
            const urls = await postid_to_url(building, startFloor, endFloor);
            let dialogues = [];
            for (const url of urls) {
                const response = await fetch(url, fetchOptions);
                const json = await response.json();
                const formatted = formatDialogues(json);
                dialogues = dialogues.concat(formatted);
            }
            console.log(dialogues);
            return dialogues;
        } catch (error) {
            console.error(error);
        }
    }

    async function main(building, startFloor, endFloor) {
        try {
            const dialogues = await json_to_txt(building, startFloor, endFloor);
            const txt = "Role: Summarization AssistantProfileauthor: LangGPTversion: 1.0language: 中文description: 本助手专注于总结论坛帖子的评论区,提供整体风向和多样内容的概要。Skills1. 能够快速扫描和解析大量评论信息。2. 精确捕捉评论中的主要观点和情感倾向。3. 提供简洁明了的总结,助您一眼把握评论区的整体风向。4. 识别评论中的不同主题或关注点,并分别总结。Background(可选项):Goals(可选项):OutputFormat(可选项):Rules1. 理解评论内容的多样性,确保总结涵盖不同观点。2. 避免偏见,提供中立的评论风向分析。3. 保持简洁,确保总结信息易于阅读和理解。Workflows1. 收集并扫描论坛帖子的评论区。2. 分析评论的整体情感倾向和主要观点。3. 总结评论区的大致风向和多样内容。4. 向用户提供最终总结,并解释评论区的主题分布。Init友好的欢迎用户,并介绍 LangGPT, 介绍完后将 LangGPT 的结构化提示词模板打印出来。请总结以下对话内容:\n" + dialogues.join('\n');
            const summary = await summarizeSomething(txt);
            console.log(summary);
            return summary;
        } catch (error) {
            console.error("Error in main function:", error);
            return error;
        }
    }

    const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
    const fetchOptions = {
        headers: {
            'accept': 'application/json, text/javascript, */*; q=0.01',
            'accept-language': 'zh-CN,zh;q=0.9',
            'discourse-logged-in': 'true',
            'discourse-present': 'true',
            'x-csrf-token': csrfToken,
            'x-requested-with': 'XMLHttpRequest'
        },
        'method': 'GET',
        'mode': 'cors',
        'credentials': 'include'
    };

    // 创建侧边栏
    const sidebar = document.createElement('div');
    sidebar.style.cssText = `
        position: fixed;
        top: 0;
        right: -500px; /* 增加侧边栏宽度 */
        width: 400px; /* 增加侧边栏宽度 */
        height: 100vh;
        background-color: white;
        box-shadow: -2px 0 5px rgba(0,0,0,0.2);
        transition: right 0.3s;
        z-index: 1000;
        overflow-y: auto;
        padding: 20px; /* 添加内边距 */
    `;
    document.body.appendChild(sidebar);

    // 创建悬浮按钮
    const toggleButton = document.createElement('div');
    toggleButton.style.cssText = `
        position: fixed;
        top: 50%;
        right: 0;
        width: 20px;
        height: 40px;
        background-color: #f0f0f0;
        border-radius: 5px 0 0 5px;
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
        z-index: 1001;
    `;
    toggleButton.innerHTML = '◀';
    toggleButton.addEventListener('click', () => {
        if (sidebar.style.right === '0px') {
            sidebar.style.right = '-500px'; // 更新为新宽度
            toggleButton.innerHTML = '◀';
        } else {
            sidebar.style.right = '0px';
            toggleButton.innerHTML = '▶';
        }
    });
    document.body.appendChild(toggleButton);

    // 创建设置按钮
    const settingsButton = document.createElement('div');
    settingsButton.style.cssText = `
        position: absolute;
        top: 10px;
        right: 10px;
        cursor: pointer;
        font-size: 20px;
    `;
    settingsButton.innerHTML = '⚙️';
    settingsButton.addEventListener('click', showSettings);
    sidebar.appendChild(settingsButton);

    // 创建表单
    const form = document.createElement('form');
    form.innerHTML = `
        <h3>总结帖子内容</h3>
        <label for="building">帖子ID:</label>
        <input type="number" id="building" name="building" required style="width: 100%;"><br><br>
        <label for="startFloor">起始楼层:</label>
        <input type="number" id="startFloor" name="startFloor" required style="width: 100%;"><br><br>
        <label for="endFloor">结束楼层:</label>
        <input type="number" id="endFloor" name="endFloor" required style="width: 100%;"><br><br>
        <button type="submit">总结</button>
    `;
    sidebar.appendChild(form);

    // 创建结果显示区域
    const resultArea = document.createElement('div');
    resultArea.id = 'result-area';
    resultArea.style.marginTop = '20px';
    resultArea.style.border = '1px solid #ccc';
    resultArea.style.padding = '10px';
    resultArea.style.maxHeight = '500px'; // 限制高度
    resultArea.style.overflowY = 'auto'; // 添加滚动条
    resultArea.style.backgroundColor = '#f9f9f9'; // 设置背景颜色
    sidebar.appendChild(resultArea);

// 处理表单提交
    form.addEventListener('submit', async function(e) {
        e.preventDefault();
        const building = parseInt(form.building.value);
        const startFloor = parseInt(form.startFloor.value);
        const endFloor = parseInt(form.endFloor.value);

        if (endFloor < startFloor) {
            alert("结束楼层不能小于起始楼层");
            return;
        }

        const maxFloors = 500;
        if (endFloor - startFloor > maxFloors) {
            alert(`一次最多只能总结${maxFloors}层`);
            return;
        }

        displayResultAsMarkdown("正在总结中...");

        try {
            const result = await main(building, startFloor, endFloor);
            let summary = `以下为https://linux.do/t/topic/${building}的${startFloor}层--${endFloor}层的总结:\n\n`;
            displayResultAsMarkdown(summary + result);
        } catch (error) {
            displayResultAsMarkdown("总结出错");
            console.error("Error:", error);
        }
    });

    // 显示设置界面
    function showSettings() {
        const settingsForm = document.createElement('form');
        settingsForm.innerHTML = `
            <h3>设置</h3>
            <label for="apiKey">API密钥:</label>
            <input type="text" id="apiKey" value="${GM_getValue('apiKey', '')}" style="width: 100%;"><br><br>
            <label for="model">模型:</label>
            <input type="text" id="model" value="${GM_getValue('model', 'deepseek-chat')}" style="width: 100%;"><br><br>
            <label for="apiUrl">API地址:</label>
            <input type="text" id="apiUrl" value="${GM_getValue('apiUrl', 'https://oneapi.passerbywtj.club/v1/chat/completions')}" style="width: 100%;"><br><br>
            <button type="submit">保存</button>
        `;
        settingsForm.style.cssText = `
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: white;
            z-index: 1002;
            padding: 20px;
            box-sizing: border-box;
        `;

        settingsForm.addEventListener('submit', function(e) {
            e.preventDefault();
            GM_setValue('apiKey', settingsForm.querySelector('#apiKey').value);
            GM_setValue('model', settingsForm.querySelector('#model').value);
            GM_setValue('apiUrl', settingsForm.querySelector('#apiUrl').value);
            sidebar.removeChild(settingsForm);
        });

        // 添加关闭按钮
        const closeButton = document.createElement('button');
        closeButton.textContent = '关闭';
        closeButton.style.cssText = `
            position: absolute;
            top: 10px;
            right: 10px;
        `;
        closeButton.addEventListener('click', (e) => {
            e.preventDefault();
            sidebar.removeChild(settingsForm);
        });
        settingsForm.appendChild(closeButton);

        sidebar.appendChild(settingsForm);
    }

    function displayResultAsMarkdown(text) {
        const resultDiv = document.createElement('div');
        resultDiv.style.whiteSpace = 'pre-wrap'; // 保留换行和空格
        resultDiv.style.wordWrap = 'break-word'; // 长单词自动换行
        resultDiv.style.fontFamily = 'monospace'; // 使用等宽字体
        resultDiv.style.fontSize = '14px';
        resultDiv.textContent = text;

        const existingResult = sidebar.querySelector('#result-area');
        if (existingResult) {
            existingResult.innerHTML = '';
            existingResult.appendChild(resultDiv);
        }
    }

    // 从URL提取帖子ID
    function extractTopicId() {
        let url = window.location.href;
        const matches = url.match(/\/topic\/(\d+)/);
        return matches ? matches[1] : null;
    }

    // 更新楼层号
    function updateFloornumber() {
        setTimeout(() => {
            const lastFloor = Number(document.getElementsByClassName('timeline-replies')[0]?.dataset.lastReply?.split('last-reply')[1]);
            if (lastFloor) {
                const startFloor = Math.max(1, parseInt(lastFloor) - 200);
                form.startFloor.value = startFloor;
                form.endFloor.value = lastFloor;
            }
        }, 500);
    }

    // 监控URL变化并更新表单
    function monitorURLChangeAndUpdateButton() {
        let currentTopicId = extractTopicId();
        setInterval(() => {
            const newTopicId = extractTopicId();
            if (newTopicId !== currentTopicId) {
                currentTopicId = newTopicId;
                form.building.value = extractTopicId();
                updateFloornumber();
            }
        }, 1000);
    }

    // 初始化
    form.building.value = extractTopicId();
    updateFloornumber();
    monitorURLChangeAndUpdateButton();
})();
dlzmoe commented 2 days ago

这个很不错啊,我将会参考使用。