Greasy Fork

来自缓存

Greasy Fork is available in English.

小黑盒全能净化引擎 (终极版 V4.0)

全栈式小黑盒论坛内容过滤:支持帖子过滤(关键词/0赞0评)、评论区净化(插眼/关键词/纯表情/楼中楼),完美适配单页应用。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         小黑盒全能净化引擎 (终极版 V4.0)
// @namespace    https://heybox.com/
// @version      4.0.0
// @description  全栈式小黑盒论坛内容过滤:支持帖子过滤(关键词/0赞0评)、评论区净化(插眼/关键词/纯表情/楼中楼),完美适配单页应用。
// @author       架构师AI & kun
// @match        https://www.xiaoheihe.cn/app/bbs/*
// @icon         https://www.xiaoheihe.cn/favicon.ico
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // ==========================================
    // 1. 配置中心 (自动持久化)
    // ==========================================
    const Config = {
        data: {
            postKeywords: [], // 帖子屏蔽词
            commentKeywords: ["副屏", "插眼", "cy", "攻略"], // 评论屏蔽词
            blockZeroComment: false,
            blockZeroLike: false,
            blockPureEmoji: false, // 新增:屏蔽纯表情/空评论
            hideMode: 'remove', // 'remove' 完全隐藏, 'dim' 虚化半透明
            stats: { postsBlocked: 0, commentsBlocked: 0 }
        },
        load() {
            const saved = GM_getValue('hb_ultimate_config_v4');
            if (saved) {
                this.data = { ...this.data, ...JSON.parse(saved) };
            }
        },
        save() {
            GM_setValue('hb_ultimate_config_v4', JSON.stringify(this.data));
            UI.updateStats();
        }
    };

    // ==========================================
    // 2. 核心引擎 (双擎驱动 + SPA适配)
    // ==========================================
    const Engine = {
        debounceTimer: null,
        
        run() {
            this.filterPosts();
            this.filterComments();
        },

        // 模块A:帖子列表净化
        filterPosts() {
            const posts = document.querySelectorAll('a.hb-cpt__bbs-content');
            let newlyBlocked = 0;

            posts.forEach(post => {
                if (post.dataset.hbProcessed === Config.data.hideMode) return;

                const title = post.querySelector('.bbs-content__title')?.innerText || '';
                const content = post.querySelector('.bbs-content__content')?.innerText || '';
                const text = (title + content).toLowerCase();
                
                const commentEl = post.querySelector('.content-list__comment-cnt');
                const likeEl = post.querySelector('.content-list__like-cnt');
                
                const comments = commentEl ? parseInt(commentEl.textContent.replace(/[^0-9]/g, '') || '0', 10) : -1;
                const likes = likeEl ? parseInt(likeEl.textContent.replace(/[^0-9]/g, '') || '0', 10) : -1;

                let shouldBlock = false;

                if (Config.data.postKeywords.length > 0) {
                    shouldBlock = Config.data.postKeywords.some(k => text.includes(k.toLowerCase()));
                }
                if (!shouldBlock && Config.data.blockZeroComment && comments === 0) shouldBlock = true;
                if (!shouldBlock && Config.data.blockZeroLike && likes === 0) shouldBlock = true;

                if (shouldBlock) {
                    if (!post.dataset.hbBlocked) newlyBlocked++;
                    post.dataset.hbBlocked = 'true';
                    post.dataset.hbProcessed = Config.data.hideMode;
                    
                    if (Config.data.hideMode === 'remove') {
                        post.style.display = 'none';
                    } else {
                        post.style.display = '';
                        post.style.opacity = '0.1';
                        post.style.pointerEvents = 'none';
                    }
                } else {
                    post.dataset.hbBlocked = 'false';
                    post.dataset.hbProcessed = '';
                    post.style.display = '';
                    post.style.opacity = '1';
                    post.style.pointerEvents = 'auto';
                }
            });

            if (newlyBlocked > 0) {
                Config.data.stats.postsBlocked += newlyBlocked;
                Config.save();
            }
        },

        // 模块B:评论区净化 (主评论 + 楼中楼 + 纯表情)
        filterComments() {
            let newlyBlocked = 0;
            const selectors = '.comment-item__content, .children-item__comment-content';
            
            document.querySelectorAll(selectors).forEach(content => {
                // 向上寻找该评论的最外层容器(兼容顶层评论和楼中楼)
                const commentWrapper = content.closest('.link-comment__comment-item') || content.closest('.comment-children-item');
                
                if (!commentWrapper || commentWrapper.dataset.hbProcessed === 'true') return;

                const text = (content.innerText || "").trim();
                const hasText = text.length > 0;
                let shouldBlock = false;

                // 规则1:官方插眼类 (.cy)
                if (content.classList.contains('cy')) {
                    shouldBlock = true;
                }

                // 规则2:自定义关键词匹配
                if (!shouldBlock && Config.data.commentKeywords.length > 0) {
                    shouldBlock = Config.data.commentKeywords.some(k => text.toLowerCase().includes(k.toLowerCase()));
                }

                // 规则3:纯表情/空评论 (没有文字的评论,通常只包含<img>表情或完全为空)
                if (!shouldBlock && Config.data.blockPureEmoji && !hasText) {
                    shouldBlock = true;
                }

                if (shouldBlock) {
                    commentWrapper.style.display = 'none';
                    commentWrapper.dataset.hbProcessed = 'true';
                    newlyBlocked++;
                }
            });

            if (newlyBlocked > 0) {
                Config.data.stats.commentsBlocked += newlyBlocked;
                Config.save();
            }
        },

        observe() {
            const observer = new MutationObserver(() => {
                clearTimeout(this.debounceTimer);
                this.debounceTimer = setTimeout(() => this.run(), 300); 
            });
            // 监听整个 body 应对单页应用(SPA)的无刷新路由跳转
            observer.observe(document.body, { childList: true, subtree: true });
        },

        refreshAll() {
            // 清除帖子状态并恢复显示
            document.querySelectorAll('a.hb-cpt__bbs-content').forEach(p => {
                p.dataset.hbProcessed = '';
                p.style.display = '';
                p.style.opacity = '1';
                p.style.pointerEvents = 'auto';
            });
            // 清除评论状态并恢复显示
            document.querySelectorAll('.link-comment__comment-item, .comment-children-item').forEach(c => {
                c.dataset.hbProcessed = '';
                c.style.display = '';
            });
            // 重新扫描
            this.run();
        }
    };

    // ==========================================
    // 3. UI控制器 (Shadow DOM 隔离)
    // ==========================================
    const UI = {
        shadowRoot: null,

        init() {
            const host = document.createElement('div');
            host.id = 'hb-ultimate-host';
            document.body.appendChild(host);
            this.shadowRoot = host.attachShadow({ mode: 'open' });
            
            this.render();
            this.bindEvents();
            this.makeDraggable();
            
            GM_registerMenuCommand("⚙️ 净化设置", () => {
                const panel = this.shadowRoot.querySelector('#panel');
                panel.style.display = panel.style.display === 'none' ? 'flex' : 'none';
            });
        },

        render() {
            const style = `
                <style>
                    :host { all: initial; }
                    #panel { position: fixed; bottom: 30px; right: 30px; width: 320px; background: #fff; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.15); font-family: sans-serif; z-index: 2147483647; overflow: hidden; display: flex; flex-direction: column; }
                    .header { background: #1a1a1a; color: #fff; padding: 12px 16px; cursor: move; display: flex; justify-content: space-between; align-items: center; user-select: none; }
                    .header h3 { margin: 0; font-size: 14px; font-weight: 600; }
                    .close-btn { cursor: pointer; font-size: 16px; opacity: 0.7; }
                    .close-btn:hover { opacity: 1; }
                    .body { padding: 16px; font-size: 13px; color: #333; max-height: 550px; overflow-y: auto; }
                    .section { margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px dashed #eee; }
                    .section:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
                    .section-title { font-weight: bold; margin-bottom: 10px; color: #1890ff; font-size: 13px; display: flex; align-items: center; gap: 4px;}
                    .section-title::before { content: ''; display: inline-block; width: 3px; height: 12px; background: #1890ff; border-radius: 2px; }
                    .row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
                    input[type="text"] { width: 100%; padding: 6px 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; margin-bottom: 8px; font-size: 12px; }
                    .tag-cloud { display: flex; flex-wrap: wrap; gap: 6px; }
                    .tag { background: #f0f0f0; padding: 2px 8px; border-radius: 12px; font-size: 11px; display: flex; align-items: center; gap: 4px; }
                    .tag span { cursor: pointer; color: #ff4d4f; font-weight: bold; }
                    .switch { position: relative; display: inline-block; width: 34px; height: 20px; }
                    .switch input { opacity: 0; width: 0; height: 0; }
                    .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 20px; }
                    .slider:before { position: absolute; content: ""; height: 14px; width: 14px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; }
                    input:checked + .slider { background-color: #1890ff; }
                    input:checked + .slider:before { transform: translateX(14px); }
                    select { padding: 4px; border-radius: 4px; border: 1px solid #ddd; font-size: 12px; outline: none; }
                    .stats { font-size: 11px; color: #888; text-align: center; margin-top: 10px; background: #f9f9f9; padding: 8px; border-radius: 6px; }
                    #trigger { position: fixed; bottom: 30px; right: 30px; width: 44px; height: 44px; background: #1a1a1a; color: #fff; border-radius: 50%; display: flex; justify-content: center; align-items: center; cursor: pointer; z-index: 2147483646; box-shadow: 0 4px 12px rgba(0,0,0,0.2); transition: transform 0.2s; font-size: 20px; }
                    #trigger:hover { transform: scale(1.1); }
                </style>
            `;

            const getTagsHTML = (arr, type) => arr.map(k => `<div class="tag">${k} <span data-word="${k}" data-type="${type}">×</span></div>`).join('');

            const html = `
                ${style}
                <div id="trigger" style="display: none;">🛡️</div>
                <div id="panel">
                    <div class="header">
                        <h3>终极净化引擎 V4.0</h3>
                        <div class="close-btn" id="hide-panel">✕</div>
                    </div>
                    <div class="body">
                        <div class="section">
                            <div class="section-title">列表帖子过滤</div>
                            <input type="text" id="kw-post-input" placeholder="输入帖子屏蔽词回车...">
                            <div class="tag-cloud" id="kw-post-list">${getTagsHTML(Config.data.postKeywords, 'post')}</div>
                            <div style="margin-top: 10px;">
                                <div class="row">
                                    <span>屏蔽 0 评论帖子</span>
                                    <label class="switch"><input type="checkbox" id="chk-0-comment" ${Config.data.blockZeroComment ? 'checked' : ''}><span class="slider"></span></label>
                                </div>
                                <div class="row">
                                    <span>屏蔽 0 点赞帖子</span>
                                    <label class="switch"><input type="checkbox" id="chk-0-like" ${Config.data.blockZeroLike ? 'checked' : ''}><span class="slider"></span></label>
                                </div>
                                <div class="row">
                                    <span>列表处理模式</span>
                                    <select id="sel-mode">
                                        <option value="remove" ${Config.data.hideMode === 'remove' ? 'selected' : ''}>完全隐藏</option>
                                        <option value="dim" ${Config.data.hideMode === 'dim' ? 'selected' : ''}>极度虚化</option>
                                    </select>
                                </div>
                            </div>
                        </div>

                        <div class="section">
                            <div class="section-title">评论区过滤 (含楼中楼)</div>
                            <input type="text" id="kw-comment-input" placeholder="输入评论屏蔽词回车...">
                            <div class="tag-cloud" id="kw-comment-list">${getTagsHTML(Config.data.commentKeywords, 'comment')}</div>
                            <div style="margin-top: 10px;">
                                <div class="row" title="屏蔽只有图片表情或完全空白的无意义评论">
                                    <span>屏蔽纯表情/空评论</span>
                                    <label class="switch"><input type="checkbox" id="chk-pure-emoji" ${Config.data.blockPureEmoji ? 'checked' : ''}><span class="slider"></span></label>
                                </div>
                            </div>
                        </div>

                        <div class="stats">
                            已净化:帖子 <b id="stat-posts">${Config.data.stats.postsBlocked}</b> | 评论 <b id="stat-comments">${Config.data.stats.commentsBlocked}</b>
                        </div>
                    </div>
                </div>
            `;
            this.shadowRoot.innerHTML = html;
        },

        bindEvents() {
            const root = this.shadowRoot;
            
            root.getElementById('hide-panel').onclick = () => {
                root.getElementById('panel').style.display = 'none';
                root.getElementById('trigger').style.display = 'flex';
            };
            root.getElementById('trigger').onclick = () => {
                root.getElementById('panel').style.display = 'flex';
                root.getElementById('trigger').style.display = 'none';
            };

            // 关键词输入处理
            const handleInput = (inputId, targetArray) => {
                const input = root.getElementById(inputId);
                input.addEventListener('keypress', (e) => {
                    if (e.key === 'Enter' && input.value.trim()) {
                        const word = input.value.trim();
                        if (!Config.data[targetArray].includes(word)) {
                            Config.data[targetArray].push(word);
                            Config.save();
                            this.refreshTags();
                            Engine.refreshAll();
                        }
                        input.value = '';
                    }
                });
            };
            handleInput('kw-post-input', 'postKeywords');
            handleInput('kw-comment-input', 'commentKeywords');

            // 关键词删除
            const handleTagRemove = (e) => {
                if (e.target.tagName === 'SPAN') {
                    const word = e.target.dataset.word;
                    const type = e.target.dataset.type;
                    const targetArray = type === 'post' ? 'postKeywords' : 'commentKeywords';
                    
                    Config.data[targetArray] = Config.data[targetArray].filter(k => k !== word);
                    Config.save();
                    this.refreshTags();
                    Engine.refreshAll();
                }
            };
            root.getElementById('kw-post-list').addEventListener('click', handleTagRemove);
            root.getElementById('kw-comment-list').addEventListener('click', handleTagRemove);

            // 开关状态绑定
            const bindSwitch = (id, key) => {
                root.getElementById(id).onchange = (e) => { 
                    Config.data[key] = e.target.checked; 
                    Config.save(); 
                    Engine.refreshAll(); 
                };
            };
            bindSwitch('chk-0-comment', 'blockZeroComment');
            bindSwitch('chk-0-like', 'blockZeroLike');
            bindSwitch('chk-pure-emoji', 'blockPureEmoji');

            // 模式选择
            root.getElementById('sel-mode').onchange = (e) => { 
                Config.data.hideMode = e.target.value; 
                Config.save(); 
                Engine.refreshAll(); 
            };
        },

        refreshTags() {
            const getTagsHTML = (arr, type) => arr.map(k => `<div class="tag">${k} <span data-word="${k}" data-type="${type}">×</span></div>`).join('');
            this.shadowRoot.getElementById('kw-post-list').innerHTML = getTagsHTML(Config.data.postKeywords, 'post');
            this.shadowRoot.getElementById('kw-comment-list').innerHTML = getTagsHTML(Config.data.commentKeywords, 'comment');
        },

        updateStats() {
            if (!this.shadowRoot) return;
            const pStat = this.shadowRoot.getElementById('stat-posts');
            const cStat = this.shadowRoot.getElementById('stat-comments');
            if (pStat) pStat.innerText = Config.data.stats.postsBlocked;
            if (cStat) cStat.innerText = Config.data.stats.commentsBlocked;
        },

        makeDraggable() {
            const panel = this.shadowRoot.getElementById('panel');
            const header = this.shadowRoot.querySelector('.header');
            let isDragging = false, currentX, currentY, initialX, initialY, xOffset = 0, yOffset = 0;

            header.addEventListener('mousedown', (e) => {
                initialX = e.clientX - xOffset; initialY = e.clientY - yOffset;
                if (e.target === header || e.target.tagName === 'H3') isDragging = true;
            });
            window.addEventListener('mouseup', () => { initialX = currentX; initialY = currentY; isDragging = false; });
            window.addEventListener('mousemove', (e) => {
                if (isDragging) {
                    e.preventDefault();
                    currentX = e.clientX - initialX; currentY = e.clientY - initialY;
                    xOffset = currentX; yOffset = currentY;
                    panel.style.transform = `translate3d(${currentX}px, ${currentY}px, 0)`;
                }
            });
        }
    };

    // ==========================================
    // 4. 启动引导
    // ==========================================
    const Boot = () => {
        Config.load();
        UI.init();
        Engine.run();     
        Engine.observe(); 
    };

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', Boot);
    } else {
        Boot();
    }
})();