DoubanCommentDelete

Delete your comments in group posts

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         DoubanCommentDelete
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Delete your comments in group posts
// @author       HouBo
// @match        *://www.douban.com/group/topic/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @license MIT
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

;(async () => {
    'use strict';

    // —— 工具函数 ——
    function getCk() {
        const match = document.cookie.match(/(?:^|; )ck=([^;]+)/);
        return match ? match[1] : '';
    }

    function logToUI(text, type = 'info') {
        const colorMap = {
            info: '#1890ff',
            success: '#52c41a',
            warn: '#faad14',
            error: '#f5222d'
        };
        const log = document.createElement('div');
        log.textContent = text;
        Object.assign(log.style, {
            fontSize: '14px',
            padding: '4px 0',
            color: colorMap[type] || '#000'
        });
        logContainer.appendChild(log);
        logContainer.scrollTop = logContainer.scrollHeight;
    }

    function createButton(text, style, handler) {
        const btn = document.createElement('a');
        btn.href = 'javascript:void(0)';
        btn.textContent = text;
        Object.assign(btn.style, {
            padding: '6px 10px',
            margin: '0 4px',
            borderRadius: '4px',
            fontSize: '14px',
            textDecoration: 'none',
            cursor: 'pointer',
            ...style
        });
        btn.addEventListener('click', handler);
        return btn;
    }

    // —— 获取或输入用户 ID ——
    async function fetchOrPromptUserId() {
        let stored = await GM_getValue('userId', null);
        if (!stored) {
            const input = prompt('请输入你的用户 ID(可在个人主页 URL 中找到):');
            if (input && input.trim()) {
                await GM_setValue('userId', input.trim());
                return input.trim();
            }
            return null;
        }
        return stored;
    }

    const userId = await fetchOrPromptUserId();
    if (!userId) {
        alert('未输入用户 ID,脚本已停止。');
        return;
    }

    GM_registerMenuCommand('修改用户 ID', async () => {
        const newId = prompt('重新输入你的用户 ID:', userId || '');
        if (newId && newId.trim()) {
            await GM_setValue('userId', newId.trim());
            location.reload();
        }
    });

    // 显示当前 ID
    const idBox = document.createElement('div');
    idBox.textContent = `当前用户 ID: ${userId}`;
    Object.assign(idBox.style, {
        position: 'fixed',
        bottom: '10px',
        left: '10px',
        padding: '8px 12px',
        background: 'rgba(0,0,0,0.7)',
        color: '#fff',
        borderRadius: '6px',
        fontSize: '14px',
        zIndex: 9999,
        fontFamily: 'monospace'
    });
    document.body.appendChild(idBox);

    // —— 初始化变量 ——
    const tid = location.href.match(/topic\/(\d+)\//)?.[1];
    if (!tid) {
        console.error('无法提取话题 ID');
        return;
    }

    const ck = getCk();
    if (!ck) {
        alert('未获取到 ck(登录凭证),请重新登录豆瓣后重试。');
        return;
    }

    const topicAdminOpts = $('.topic-admin-opts')[0];
    if (!topicAdminOpts) {
        console.error('找不到操作区域');
        return;
    }

    // —— 创建控制面板 ——
    const controlPanel = document.createElement('div');
    controlPanel.innerHTML = `
        <div style="margin: 12px 0; padding: 12px; border: 1px solid #e8e8e8; border-radius: 8px; background: #fafafa; font-family: Arial, sans-serif;">
            <h3 style="margin: 0 0 10px; color: #333;">自动删除我的评论</h3>
            <div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
                <label style="font-weight:bold; color:#333;">起始页:</label>
                <input type="number" id="page-start-input" value="1" min="1" style="width: 60px; padding: 4px; border: 1px solid #ccc; border-radius: 4px;">
                <div id="progress-bar" style="flex: 1; height: 6px; background: #eee; border-radius: 3px; overflow: hidden; display: none;">
                    <div id="progress-fill" style="height: 100%; width: 0%; background: #1890ff; transition: width 0.3s;"></div>
                </div>
            </div>
            <div id="button-container" style="margin-top: 10px;"></div>
            <div id="log-container" style="max-height: 200px; overflow-y: auto; margin-top: 10px; font-size: 14px; color: #555; line-height: 1.5;"></div>
        </div>
    `;
    topicAdminOpts.appendChild(controlPanel);

    const progressBar = document.getElementById('progress-bar');
    const progressFill = document.getElementById('progress-fill');
    const buttonContainer = document.getElementById('button-container');
    const logContainer = document.getElementById('log-container');

    let isRunning = false;
    let isPaused = false;
    let totalDeleted = 0;
    let currentPage = 1;
    let totalPages = null;

    // —— 控制按钮 ——
    const startBtn = createButton('▶️ 开始删除', { background: '#1890ff', color: 'white' }, startDeletion);
    const pauseBtn = createButton('⏸️ 暂停', { background: '#faad14', color: 'white', display: 'none' }, () => { isPaused = true; pauseBtn.style.display = 'none'; resumeBtn.style.display = 'inline-block'; logToUI('⏸️ 已暂停,点击“继续”恢复。', 'warn'); });
    const resumeBtn = createButton('▶️ 继续', { background: '#52c41a', color: 'white', display: 'none' }, () => { isPaused = false; resumeBtn.style.display = 'none'; pauseBtn.style.display = 'inline-block'; });
    const stopBtn = createButton('⏹️ 停止', { background: '#f5222d', color: 'white' }, () => {
        isRunning = false;
        isPaused = false;
        logToUI(`⏹️ 已停止。共删除 ${totalDeleted} 条评论。`, 'warn');
        updateButtons(false);
    });

    buttonContainer.appendChild(startBtn);
    buttonContainer.appendChild(pauseBtn);
    buttonContainer.appendChild(resumeBtn);
    buttonContainer.appendChild(stopBtn);

    function updateButtons(running) {
        startBtn.style.display = running ? 'none' : 'inline-block';
        pauseBtn.style.display = running && !isPaused ? 'inline-block' : 'none';
        resumeBtn.style.display = running && isPaused ? 'inline-block' : 'none';
        stopBtn.style.display = running ? 'inline-block' : 'none';
    }

    // —— 延迟函数 ——
    function randomDelay(min = 800, max = 1500) {
        return new Promise(resolve => setTimeout(resolve, Math.random() * (max - min) + min));
    }

    // —— 删除单条评论(带重试)——
    async function delComment(commentElement) {
        const cid = $(commentElement).data('cid');
        const content = $(commentElement).find('.markdown').text().trim().substring(0, 25);

        for (let i = 0; i < 3; i++) {
            try {
                await $.post(`/j/group/topic/${tid}/remove_comment`, { ck, cid });
                totalDeleted++;
                logToUI(`成功删除:"${content}"`, 'success');
                return true;
            } catch (err) {
                if (i === 2) {
                    logToUI(`删除失败(重试3次):"${content}"`, 'error');
                    return false;
                }
                await randomDelay(1000, 2000);
            }
        }
    }

    // —— 删除当前页所有自己的评论 ——
    async function delPageComment() {
        const comments = $('.topic-reply li');
        const myComments = Array.from(comments).filter(el => $(el).data('author-id') == userId);

        if (myComments.length === 0) {
            logToUI(`🔍 第 ${currentPage} 页:未找到属于你的评论。`);
            return;
        }

        logToUI(`🔍 第 ${currentPage} 页:发现 ${myComments.length} 条属于你的评论,开始删除...`);

        for (const comment of myComments) {
            if (!isRunning || isPaused) break;
            await delComment(comment);
            await randomDelay();
        }
    }

    // —— 跳转下一页 ——
    async function gotoNextPage() {
        const nextLink = $('a:contains("后页")').attr('href');
        if (!nextLink) return false;

        try {
            const data = await $.ajax({ url: nextLink, method: 'GET' });
            const $newDom = $('<div>').html(data);

            $('#comments').html($newDom.find('#comments').html());
            $('#popular-comments, h3:contains("最赞回应")').remove();
            $('.paginator').html($newDom.find('.paginator').html());

            currentPage++;
            updateProgress();
            return true;
        } catch (e) {
            logToUI('❌ 加载下一页失败', 'error');
            return false;
        }
    }

    // —— 更新进度条 ——
    function updateProgress() {
        const percent = totalPages ? Math.round((currentPage / totalPages) * 100) : 0;
        progressFill.style.width = `${Math.min(percent, 100)}%`;
    }

    // —— 主删除流程 ——
    async function startDeletion() {
        const startPageInput = document.getElementById('page-start-input').value;
        const startPage = Math.max(1, parseInt(startPageInput, 10) || 1);

        isRunning = true;
        isPaused = false;
        totalDeleted = 0;
        currentPage = startPage;
        updateButtons(true);
        logContainer.innerHTML = '';
        progressBar.style.display = 'block';
        progressFill.style.width = '0%';

        logToUI(`🚀 开始从第 ${startPage} 页删除评论...`, 'info');

        try {
            await goToPage(startPage);
            updateProgress();

            while (isRunning) {
                if (isPaused) {
                    await new Promise(resolve => {
                        const check = () => isPaused && isRunning ? setTimeout(check, 500) : resolve();
                        check();
                    });
                }

                await delPageComment();
                const hasNext = await gotoNextPage();
                if (!hasNext) {
                    logToUI('已到最后一页,删除完成!', 'success');
                    break;
                }
            }
        } catch (e) {
            logToUI(`❌ 发生错误: ${e.message}`, 'error');
        } finally {
            if (isRunning) {
                logToUI(`✅ 全部完成!共删除 ${totalDeleted} 条评论。`, 'success');
            }
            isRunning = false;
            updateButtons(false);
        }
    }

    // —— 跳转到指定页 ——
    function goToPage(pageNum) {
        if (pageNum === 1) {
            $('#popular-comments, h3:contains("最赞回应")').remove();
            return Promise.resolve();
        }

        const url = `/group/topic/${tid}/?start=${(pageNum - 1) * 100}`;
        return $.ajax({
            url,
            method: 'GET',
            success: data => {
                const $newDom = $('<div>').html(data);
                $('.topic-reply').html($newDom.find('.topic-reply').html());
                $('.paginator').html($newDom.find('.paginator').html());
                currentPage = pageNum;
                updateProgress();
            },
            error: () => logToUI(`❌ 无法加载第 ${pageNum} 页`, 'error')
        });
    }

    // —— 移除广告 ——
    $('#gdt-ad-container, #dale_group_topic_inner_middle').remove();

    logToUI('✅ 脚本已就绪,请设置起始页并点击“开始删除”。');

})();