Greasy Fork is available in English.
全栈式小黑盒论坛内容过滤:支持帖子过滤(关键词/0赞0评)、评论区净化(插眼/关键词/纯表情/楼中楼),完美适配单页应用。
// ==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();
}
})();