Reddit AI BotBuster (Enhanced UX - Complete)

Detects suspected bot accounts and AI-generated content on Reddit with a transparent, configurable, and research-based engine.

// ==UserScript==
// @name         Reddit AI BotBuster (Enhanced UX - Complete)
// @namespace    http://tampermonkey.net/
// @version      4.1.0
// @description  Detects suspected bot accounts and AI-generated content on Reddit with a transparent, configurable, and research-based engine.
// @match        https://www.reddit.com/*
// @match        https://old.reddit.com/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    /************************************
     * 1. STYLES & UI INJECTION
     ************************************/
    // Use GM_addStyle for compatibility, falling back to a manual injection.
    const addStyle = typeof GM_addStyle !== 'undefined' ? GM_addStyle : (css) => {
        const style = document.createElement('style');
        style.textContent = css;
        document.head.appendChild(style);
    };

    addStyle(`
        .botUsername { color: orange !important; font-size: 14px !important; font-weight: bold !important; }
        .botAndAiContentDetected { outline: 3px dashed purple !important; outline-offset: -3px; }
        /* Confidence-Based Highlighting */
        .aiContentLow { outline: 2px dashed #007bff !important; outline-offset: -2px; } /* Light Blue */
        .aiContentMid { outline: 3px dashed #0056b3 !important; outline-offset: -3px; } /* Medium Blue */
        .aiContentHigh { outline: 3px solid #00234d !important; outline-offset: -3px; }   /* Dark Blue */

        #botCounterPopup { position: fixed; top: 40px; right: 10px; width: 280px; z-index: 9999; background-color: rgba(248,248,248,0.9); backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px); border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.25); font-family: 'Verdana', sans-serif; font-size: 12px; border: 1px solid #ccc; user-select: none; }
        #botPopupHeader { display: flex; justify-content: space-between; align-items: center; font-weight: bold; padding: 10px; border-bottom: 1px solid #eee; cursor: pointer; }
        #settingsIcon { cursor: pointer; font-size: 16px; margin-left: 10px; }
        #settingsPanel { display: none; padding: 10px; border-top: 1px solid #eee; }
        #settingsPanel label { display: block; margin: 5px 0; }
        #settingsPanel input { width: 50px; margin-left: 10px; }
        #saveSettingsBtn { background-color: #007bff; color: white; border: none; border-radius: 4px; padding: 5px 10px; cursor: pointer; margin-top: 10px; }
        #saveSettingsBtn:hover { background-color: #0056b3; }
        #botDropdown { display: none; max-height: 300px; overflow-y: auto; padding: 5px 0; }
        #botDropdown a { display: block; padding: 3px 10px; text-decoration: none; color: #333; }
        #botDropdown a:hover { background-color: rgba(0,0,0,0.08); }
        /* On-Hover Tooltip for Score Breakdown */
        #aiScoreTooltip { position: fixed; display: none; background: #222; color: #fff; border-radius: 5px; padding: 8px; font-size: 12px; z-index: 10000; max-width: 300px; pointer-events: none; }
        #aiScoreTooltip ul { margin: 0; padding: 0 0 0 15px; }
        #aiScoreTooltip li { margin-bottom: 3px; }
    `);

    /************************************
     * 2. CONFIGURATION & STATE
     ************************************/
    // Use GM_getValue for persistent settings, with defaults.
    let BOT_THRESHOLD = GM_getValue("bot_threshold", 2.9);
    let AI_THRESHOLD = GM_getValue("ai_threshold", 4.0);

    // Confidence Tiers (relative to AI_THRESHOLD)
    const CONFIDENCE_MID_TIER = 2.5;
    const CONFIDENCE_HIGH_TIER = 5.0;

    // Heuristics patterns
    const suspiciousUserPatterns = [ /bot/i, /^[A-Za-z]+-[A-Za-z]+\d{4}$/, /^[A-Za-z]+[_-][A-Za-z]+\d{2,4}$/, /^[A-Za-z]+\d{4,}$/, /^(user|redditor)\d{6,}$/i ];
    const genericResponses = [ "i agree dude", "yes you are right", "well said", "totally agree", "i agree", "right you are", "well spoken, you are", "perfectly said this is", "lol", "nice", "true", "this.", "same", "agreed", "exactly", "preach" ];
    const scamLinkRegex = /\.(live|life|shop|xyz|buzz|top|click|fun|site|online|store|blog|app|digital|network|cloud)\b/i;
    const CONTENT_SELECTORS = [ 'div[data-testid="post-container"]', 'div[data-testid="comment"]', 'div.comment', 'div.link' ];
    const USERNAME_SELECTORS = 'a[href*="/user/"], a[href*="/u/"], a.author, a[data-click-id="user"]';

    // State
    let botCount = 0;
    let detectedBots = [];
    let detectionIndex = 0;

    /************************************
     * 3. UTILITY FUNCTIONS
     ************************************/
    function countSyllables(word) {
        word = word.toLowerCase();
        if (word.length <= 3) return 1;
        word = word.replace(/(?:[^laeiouy]es|ed|[^laeiouy]e)$/, '');
        word = word.replace(/^y/, '');
        const matches = word.match(/[aeiouy]{1,}/g);
        return matches ? matches.length : 1;
    }

    function computeReadabilityScore(text) {
        const sentenceMatches = text.match(/[^.!?]+[.!?]+/g);
        if (!sentenceMatches) return null;
        const sentences = sentenceMatches;
        const words = text.split(/\s+/).filter(w => w.length > 0);
        if (words.length === 0 || sentences.length === 0) return null;
        const wordCount = words.length;
        const sentenceCount = sentences.length;
        const syllableCount = words.reduce((acc, word) => acc + countSyllables(word), 0);
        return 206.835 - 1.015 * (wordCount / sentenceCount) - 84.6 * (syllableCount / wordCount);
    }

    /******************************************************************
     * 4. AI & BOT DETECTION ENGINES
     ******************************************************************/
    function computeAIScore(text, paragraphCount = 1) {
        let score = 0;
        let reasons = [];
        const lowerText = text.toLowerCase();
        const words = lowerText.split(/\s+/).filter(w => w.length > 0);
        const wordCount = words.length;

        if (wordCount < 25) return { score: 0, reasons: [] };

        if (/\bas an (ai|artificial intelligence)( language model)?\b/.test(lowerText)) {
            return { score: 10.0, reasons: ["Self-disclosed as an AI [+10.0]"] };
        }

        const aiFormulaicPhrases = [ "in conclusion", "furthermore", "moreover", "on the other hand", "it is important to note", "ultimately", "in summary", "delve deeper into", "explore the nuances of" ];
        let formulaicPhraseCount = 0;
        aiFormulaicPhrases.forEach(phrase => { if (lowerText.includes(phrase)) formulaicPhraseCount++; });
        if (formulaicPhraseCount > 0) {
            const points = formulaicPhraseCount * 1.2;
            score += points;
            reasons.push(`Formulaic Language [+${points.toFixed(1)}]`);
        }

        const contractions = lowerText.match(/\b(i'm|you're|they're|we're|can't|won't|didn't|isn't|it's)\b/g);
        if (wordCount > 70 && (!contractions || contractions.length < (wordCount / 100))) {
            score += 1.8;
            reasons.push("Lacks Contractions [+1.8]");
        }

        const complexSynonyms = { 'utilize': 'use', 'leverage': 'use', 'commence': 'start', 'facilitate': 'help', 'elucidate': 'explain', 'henceforth': 'from now on', 'nevertheless': 'but', 'demonstrate': 'show' };
        let complexWordCount = 0;
        words.forEach(word => { if (complexSynonyms[word]) complexWordCount++; });
        if (wordCount > 50 && complexWordCount > (wordCount / 75)) {
            const points = complexWordCount * 0.8;
            score += points;
            reasons.push(`Unnatural Synonyms [+${points.toFixed(1)}]`);
        }

        const personalPhrases = ["i think", "i feel", "i believe", "in my opinion", "in my experience"];
        const hasPersonalPhrase = personalPhrases.some(phrase => lowerText.includes(phrase));
        if (wordCount > 60 && !hasPersonalPhrase) {
            score += 1.0;
            reasons.push("Lacks Personal Opinion [+1.0]");
        }

        const sentencesArr = text.split(/[.!?]+/).map(s => s.trim()).filter(s => s.length > 0);
        if (sentencesArr.length > 3) {
            const lengths = sentencesArr.map(s => s.split(/\s+/).length);
            const avg = lengths.reduce((a, b) => a + b, 0) / lengths.length;
            const variance = lengths.reduce((a, b) => a + Math.pow(b - avg, 2), 0) / lengths.length;
            if (wordCount > 50 && variance < 12) {
                score += 1.5;
                reasons.push("Low Sentence Variance [+1.5]");
            }
            if (variance > 40) {
                score -= 1.0;
                reasons.push("High Burstiness (Human-like) [-1.0]");
            }
        }

        if (paragraphCount > 2 && wordCount > 80) {
            score += 1.5;
            reasons.push(`Well-structured (${paragraphCount} Paras) [+1.5]`);
        }

        if (/[\u{1F600}-\u{1F64F}]/gu.test(text)) { score -= 1.0; reasons.push("Contains Emojis [-1.0]"); }

        return { score: Math.max(0, score), reasons };
    }

    function computeUsernameBotScore(username) {
        let score = 0;
        suspiciousUserPatterns.forEach(pattern => {
            if (pattern.test(username)) { score += 1.5; }
        });
        const digits = username.match(/\d/g);
        if (digits && (digits.length / username.length) > 0.4) { score += 0.8; }
        return score;
    }

    function computeBotScore(elem) {
        let score = 0;
        const userElem = elem.querySelector(USERNAME_SELECTORS);
        if (userElem) {
            score += computeUsernameBotScore(userElem.innerText.trim());
        }
        const textContent = elem.innerText.toLowerCase().replace(/\s+/g, ' ').trim();
        if (genericResponses.includes(textContent) && textContent.length < 30) {
            score += 1.5;
        }
        const links = elem.querySelectorAll('a');
        links.forEach(link => {
            if (scamLinkRegex.test(link.href)) { score += 3.0; }
        });
        return score;
    }


    /************************************
     * 5. UI MANAGEMENT & POPUP
     ************************************/
    function createPopupAndTooltip() {
        let popup = document.createElement("div");
        popup.id = "botCounterPopup";
        popup.innerHTML = `
            <div id="botPopupHeader">
                <span>Detected bot/AI: 0</span>
                <span id="settingsIcon" title="Settings">⚙️</span>
            </div>
            <div id="settingsPanel">
                <label>AI Threshold: <input type="number" id="aiThresholdInput" step="0.1" min="1"></label>
                <label>Bot Threshold: <input type="number" id="botThresholdInput" step="0.1" min="1"></label>
                <button id="saveSettingsBtn">Save</button>
            </div>
            <div id="botDropdown"></div>`;
        document.body.appendChild(popup);
        document.getElementById("aiThresholdInput").value = AI_THRESHOLD;
        document.getElementById("botThresholdInput").value = BOT_THRESHOLD;

        let tooltip = document.createElement("div");
        tooltip.id = "aiScoreTooltip";
        document.body.appendChild(tooltip);

        document.getElementById("botPopupHeader").addEventListener("click", (e) => {
            if (e.target.id === "settingsIcon") {
                e.stopPropagation();
                const panel = document.getElementById("settingsPanel");
                panel.style.display = panel.style.display === "block" ? "none" : "block";
            } else {
                 const dropdown = document.getElementById("botDropdown");
                 dropdown.style.display = dropdown.style.display === "block" ? "none" : "block";
                 if(dropdown.style.display === 'none') document.getElementById('settingsPanel').style.display = 'none';
            }
        });

        document.getElementById("saveSettingsBtn").addEventListener("click", (e) => {
            e.stopPropagation();
            AI_THRESHOLD = parseFloat(document.getElementById("aiThresholdInput").value);
            BOT_THRESHOLD = parseFloat(document.getElementById("botThresholdInput").value);
            GM_setValue("ai_threshold", AI_THRESHOLD);
            GM_setValue("bot_threshold", BOT_THRESHOLD);
            e.target.innerText = "Saved!";
            setTimeout(() => { e.target.innerText = "Save"; }, 1500);
        });

        document.body.addEventListener('mouseover', (e) => {
            const flaggedElem = e.target.closest('[data-bot-detected="true"]');
            if (flaggedElem) {
                const reasonsHTML = JSON.parse(flaggedElem.dataset.aiReasons || '[]').map(r => `<li>${r}</li>`).join('');
                const botScore = parseFloat(flaggedElem.dataset.botScore).toFixed(1);
                const aiScore = parseFloat(flaggedElem.dataset.aiScore).toFixed(1);
                tooltip.innerHTML = `<strong>Bot Score:</strong> ${botScore} / ${BOT_THRESHOLD}<br><strong>AI Score:</strong> ${aiScore} / ${AI_THRESHOLD}<ul>${reasonsHTML}</ul>`;
                tooltip.style.display = 'block';
            }
        });
        document.body.addEventListener('mouseout', () => { tooltip.style.display = 'none'; });
        document.body.addEventListener('mousemove', (e) => {
            if (tooltip.style.display === 'block') {
                 tooltip.style.left = `${e.pageX + 15}px`;
                 tooltip.style.top = `${e.pageY + 15}px`;
            }
        });
    }

    function updatePopup() {
        document.querySelector("#botPopupHeader > span").innerText = `Detected bot/AI: ${botCount}`;
        const dropdown = document.getElementById("botDropdown");
        dropdown.innerHTML = "";
        if (detectedBots.length === 0) {
            dropdown.innerHTML = `<span style="padding: 3px 10px; color: #777; font-style: italic;">No bots/AI detected yet.</span>`;
        } else {
            detectedBots.sort((a,b) => b.aiScore - a.aiScore || b.botScore - a.botScore); // Sort by highest score
            detectedBots.forEach(item => {
                let link = document.createElement("a");
                link.href = "#" + item.elemID;
                link.innerText = `${item.username} (${item.reason})`;
                link.title = `Bot Score: ${item.botScore.toFixed(1)}, AI Score: ${item.aiScore.toFixed(1)}`;
                link.addEventListener('click', (e) => { e.preventDefault(); document.getElementById(item.elemID)?.scrollIntoView({ behavior: 'smooth', block: 'center' }); });
                dropdown.appendChild(link);
            });
        }
    }

    /************************************
     * 6. CORE DETECTION LOGIC
     ************************************/
    function highlightIfSuspected(elem) {
        if (elem.getAttribute("data-bot-detected")) return;

        const commentBody = elem.querySelector('div[data-testid="comment"] > div:nth-child(2) > div');
        let textToAnalyze = '', paragraphCount = 0;

        if (commentBody && commentBody.querySelectorAll('p').length > 0) {
            const paragraphs = Array.from(commentBody.querySelectorAll('p'));
            textToAnalyze = paragraphs.map(p => p.innerText).join('\n\n');
            paragraphCount = paragraphs.length;
        } else {
            const contentDiv = elem.querySelector('.md, .usertext-body');
            textToAnalyze = contentDiv ? contentDiv.innerText : (elem.innerText || '');
            paragraphCount = textToAnalyze.split(/\n\s*\n/).filter(line => line.trim().length > 10).length;
        }

        if (!textToAnalyze.trim()) return;

        const aiResult = computeAIScore(textToAnalyze, paragraphCount);
        const aiScore = aiResult.score;
        const botScore = computeBotScore(elem);

        const botFlag = botScore >= BOT_THRESHOLD;
        const aiFlag = aiScore >= AI_THRESHOLD;

        if (botFlag || aiFlag) {
            elem.setAttribute("data-bot-detected", "true");
            let reason = "";

            if (botFlag && aiFlag) {
                elem.classList.add("botAndAiContentDetected");
                reason = "Bot & AI";
            } else if (aiFlag) {
                if (aiScore >= AI_THRESHOLD + CONFIDENCE_HIGH_TIER) {
                    elem.classList.add("aiContentHigh");
                    reason = "AI (High Conf)";
                } else if (aiScore >= AI_THRESHOLD + CONFIDENCE_MID_TIER) {
                    elem.classList.add("aiContentMid");
                    reason = "AI (Mid Conf)";
                } else {
                    elem.classList.add("aiContentLow");
                    reason = "AI (Low Conf)";
                }
            } else { // botFlag only
                reason = "Bot";
            }

            if (botFlag) {
                const usernameElem = elem.querySelector(USERNAME_SELECTORS);
                if (usernameElem) usernameElem.classList.add("botUsername");
            }

            elem.dataset.aiScore = aiScore.toFixed(2);
            elem.dataset.botScore = botScore.toFixed(2);
            elem.dataset.aiReasons = JSON.stringify(aiResult.reasons);

            botCount++;
            detectionIndex++;
            const elemID = "botbuster-detected-" + detectionIndex;
            elem.setAttribute("id", elemID);
            const username = elem.querySelector(USERNAME_SELECTORS)?.innerText.trim() || "Unknown";

            detectedBots.push({ username, elemID, reason, botScore, aiScore });
            updatePopup();
        }
    }

    /************************************
     * 7. INITIALIZATION & OBSERVATION
     ************************************/
    function scanForBots(root = document) {
        const query = CONTENT_SELECTORS.map(s => `${s}:not([data-bot-detected])`).join(', ');
        root.querySelectorAll(query).forEach(highlightIfSuspected);
    }

    createPopupAndTooltip();
    setTimeout(() => scanForBots(document.body), 1500);

    const observer = new MutationObserver(mutations => {
        for (const mutation of mutations) {
            for (const node of mutation.addedNodes) {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    requestAnimationFrame(() => scanForBots(node));
                }
            }
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });

})();
长期地址
遇到问题?请前往 GitHub 提 Issues。