屏蔽微博、知乎、B站、X(Twitter)含关键词或正则的内容。修复了暂停屏蔽后面板/按钮消失无法恢复的问题。包含极致精简规则库与规则指南。
// ==UserScript==
// @name XBlock
// @namespace http://tampermonkey.net/
// @version 2.6.14
// @description 屏蔽微博、知乎、B站、X(Twitter)含关键词或正则的内容。修复了暂停屏蔽后面板/按钮消失无法恢复的问题。包含极致精简规则库与规则指南。
// @author werflala
// @match https://www.zhihu.com/*
// @match https://www.xiaohongshu.com/*
// @match https://www.bilibili.com/
// @match https://www.bilibili.com/?*
// @match https://www.bilibili.com/v/*
// @match https://search.bilibili.com/*
// @match https://weibo.com/*
// @match https://www.weibo.com/*
// @match https://s.weibo.com/*
// @match https://twitter.com/*
// @match https://x.com/*
// @icon data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBwgHBgkIBwgKCgkLDRYPDQwMDRsUFRAWIB0iIiAdHx8kKDQsJCYxJx8fLT0tMTU3Ojo6Iys/RD84QzQ5OjcBCgoKDQwNGg8PGjclHyU3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3N//AABEIAL8AywMBIgACEQEDEQH/xAAbAAEAAgMBAQAAAAAAAAAAAAAABgcDBAUCAf/EAD0QAAICAQICBgcECAcBAAAAAAABAgMEBREGIRIiMUFRcVJhgZGhsdETQnLBFBUjMzNT/EABkBAQADAQEAAAAAAAAAAAAAAAADBAUBAv/EACURAQACAgICAQQDAQAAAAAAAAABAgMEERIhMTITQVFhFCIzkf/aAAw0 Broadway
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const DEFAULT_RULES_COMMON = [];
const DEFAULT_RULES_X = [];
const STORAGE_KEY = 'keyword_blocker_words_v2';
const DISABLED_SITES_KEY = 'keyword_blocker_disabled_sites';
const MASK_BAR_MASTER_KEY = 'keyword_blocker_mask_bar_master';
const KEYWORD_MASK_VISIBILITY_KEY = 'keyword_blocker_keyword_mask_visibility';
const PANEL_POS_KEY = 'keyword_blocker_panel_pos';
function parseRegexString(str) {
if (typeof str !== 'string') return null;
const trimmed = str.trim();
const match = trimmed.match(/^\/(.+)\/([a-z]*)$/i);
if (match) {
return {
pattern: match[1],
flags: match[2]
};
}
return null;
}
function isRegexRule(str) {
return parseRegexString(str) !== null;
}
function isMasterMaskSwitchOn() {
try {
const saved = localStorage.getItem(MASK_BAR_MASTER_KEY);
return saved ? JSON.parse(saved) === true : false;
} catch (e) {
return false;
}
}
function saveMasterMaskSwitch(enabled) {
localStorage.setItem(MASK_BAR_MASTER_KEY, JSON.stringify(!!enabled));
}
function loadKeywordMaskVisibility() {
try {
const saved = localStorage.getItem(KEYWORD_MASK_VISIBILITY_KEY);
const data = saved ? JSON.parse(saved) : {};
return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
} catch (e) {
return {};
}
}
function saveKeywordMaskVisibility(map) {
localStorage.setItem(KEYWORD_MASK_VISIBILITY_KEY, JSON.stringify(map));
}
let KEYWORD_MASK_VISIBILITY = loadKeywordMaskVisibility();
function getRuleKey(rule) {
return rule.type + '::' + rule.value;
}
function getKeywordMaskBarVisible(rule) {
const key = getRuleKey(rule);
if (Object.prototype.hasOwnProperty.call(KEYWORD_MASK_VISIBILITY, key)) {
return !!KEYWORD_MASK_VISIBILITY[key];
}
return !isMasterMaskSwitchOn();
}
function removeKeywordMaskBarVisible(rule) {
const key = getRuleKey(rule);
if (Object.prototype.hasOwnProperty.call(KEYWORD_MASK_VISIBILITY, key)) {
delete KEYWORD_MASK_VISIBILITY[key];
saveKeywordMaskVisibility(KEYWORD_MASK_VISIBILITY);
}
}
let PENDING_MASTER_MASK_SWITCH = null;
let PENDING_KEYWORD_MASK_VISIBILITY = null;
function getKeywordMaskBarVisibleForUI(rule) {
const key = getRuleKey(rule);
const visibilityMap = PENDING_KEYWORD_MASK_VISIBILITY || KEYWORD_MASK_VISIBILITY;
const masterSwitch = PENDING_MASTER_MASK_SWITCH === null ? isMasterMaskSwitchOn() : PENDING_MASTER_MASK_SWITCH;
if (Object.prototype.hasOwnProperty.call(visibilityMap, key)) {
return !!visibilityMap[key];
}
return !masterSwitch;
}
function getCurrentSite() {
const hostname = window.location.hostname;
if (hostname.includes('zhihu.com')) return 'zhihu';
if (hostname.includes('xiaohongshu.com')) return 'xiaohongshu';
if (hostname.includes('bilibili.com')) return 'bilibili';
if (hostname.includes('weibo.com')) return 'weibo';
if (hostname.includes('x.com') || hostname.includes('twitter.com')) return 'twitter';
return 'unknown';
}
function normalizeRule(r) {
if (typeof r === 'string') {
return { type: isRegexRule(r) ? 'content' : 'name', value: r.trim() };
}
if (r && typeof r.value === 'string') {
return { type: r.type === 'name' ? 'name' : 'content', value: r.value.trim() };
}
return null;
}
function loadKeywords() {
let rules = [];
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
const parsed = JSON.parse(saved);
if (Array.isArray(parsed)) {
rules = parsed.map(normalizeRule).filter(Boolean);
}
} else {
rules = [...DEFAULT_RULES_COMMON];
}
if (getCurrentSite() === 'twitter') {
const existingKeys = new Set(rules.map(getRuleKey));
DEFAULT_RULES_X.forEach(r => {
if (!existingKeys.has(getRuleKey(r))) {
rules.unshift(r);
}
});
saveKeywords(rules);
}
return rules;
} catch (e) {
console.error('加载屏蔽规则失败:', e);
return [...DEFAULT_RULES_COMMON, ...DEFAULT_RULES_X];
}
}
function saveKeywords(rules) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(rules));
}
function saveDisabledSites(sites) {
localStorage.setItem(DISABLED_SITES_KEY, JSON.stringify(sites));
}
function loadDisabledSites() {
try {
const saved = localStorage.getItem(DISABLED_SITES_KEY);
return saved ? JSON.parse(saved) : [];
} catch (e) {
return [];
}
}
function isCurrentSiteDisabled() {
const disabledSites = loadDisabledSites();
return disabledSites.includes(getCurrentSite());
}
function disableCurrentSite() {
const disabledSites = loadDisabledSites();
const currentSite = getCurrentSite();
if (!disabledSites.includes(currentSite)) {
disabledSites.push(currentSite);
saveDisabledSites(disabledSites);
}
}
function enableCurrentSite() {
const disabledSites = loadDisabledSites();
const currentSite = getCurrentSite();
const index = disabledSites.indexOf(currentSite);
if (index > -1) {
disabledSites.splice(index, 1);
saveDisabledSites(disabledSites);
}
}
let BLOCK_RULES = loadKeywords();
let currentTab = 'name';
const siteConfigs = {
zhihu: {
containerSelector: '.ContentItem',
nameSelector: '.UserLink, .AuthorInfo-name',
titleSelector: '.ContentItem-title a, .RichText',
logPrefix: '知乎'
},
xiaohongshu: {
containerSelector: 'section.note-item',
nameSelector: '.author-wrapper .name',
titleSelector: 'a.title, .title, .desc',
logPrefix: '小红书'
},
bilibili: {
containerSelector: '.bili-feed-card, .bili-video-card',
nameSelector: '.bili-video-card__info--author',
titleSelector: '.bili-video-card__info--tit, .bili-video-card__info--tit a',
logPrefix: 'B站'
},
weibo: {
containerSelector: '.wbpro-scroller-item',
nameSelector: '.head-info_name_3_22m',
titleSelector: '.wbpro-feed-content .detail_wbtext_4CRf9',
logPrefix: '微博'
},
twitter: {
containerSelector: 'div[data-testid="cellInnerDiv"]',
nameSelector: '[data-testid="User-Name"]',
titleSelector: '[data-testid="tweetText"]',
logPrefix: 'X(Twitter)'
}
};
const BUILDER_PRESETS = {
single_emoji: {
placeholder: '此预设自动匹配纯 Emoji 内容 (单行或多行)',
defaultText: '纯 Emoji 内容屏蔽 (无需修改)'
},
lines_sym_emoji_2: {
placeholder: '此项无需输入关键词,自动匹配“1行符号+1行Emoji”',
defaultText: '第1行: 符号\n第2行: Emoji'
},
lines_sym_emoji_4: {
placeholder: '此项无需输入关键词,自动匹配“1行符号+1行Emoji+1行符号+1行Emoji”',
defaultText: '第1行: 符号\n第2行: Emoji\n第3行: 符号\n第4行: Emoji'
},
branch: {
placeholder: '前缀: 加微信\n选项: 领券, 抽奖, 特价\n后缀: (可空)',
defaultText: '前缀: 加微信\n选项: 领券, 抽奖'
},
order: {
placeholder: '按出现先后顺序匹配,例如:优惠, 促销, 免费',
defaultText: '优惠, 促销, 免费'
},
any_order: {
placeholder: '无序同时包含,例如:抽奖, 转发, 关注',
defaultText: '抽奖, 转发, 关注'
},
or: {
placeholder: '满足任意一个即可,例如:广告, 推广, 商务合作',
defaultText: '广告, 推广, 商务合作'
},
single: {
placeholder: '精准转义特殊符号,例如:C++ 或 Node.js',
defaultText: 'C++'
},
starts_with: {
placeholder: '匹配以某些词开头的标题/内容,例如:推广, 赞助, 广告',
defaultText: '推广, 赞助, 广告'
},
user_mention: {
placeholder: '此预设自动匹配连续 2 个以上的 @/提及 灌水评论',
defaultText: '连续@提及打扰 (无需修改)'
}
};
function createManagementUI() {
if (document.getElementById('keyword-blocker-panel')) return;
const style = document.createElement('style');
style.textContent = `
.kb-floating-btn { position: fixed; left: 12px; top: 50%; transform: translateY(-50%); z-index: 10000; background: #1890ff; color: white; border: none; border-radius: 5px; padding: 8px 6px; cursor: pointer; font-size: 12px; line-height: 1.1; box-shadow: 0 2px 6px rgba(0,0,0,0.15); transition: all 0.3s ease; writing-mode: vertical-lr; text-orientation: mixed; }
.kb-floating-btn:hover { background: #40a9ff; transform: translateY(-50%) scale(1.05); }
#keyword-blocker-panel { position: fixed; z-index: 9999; width: 360px; max-height: 92vh; background: white; border: 1px solid #d9d9d9; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.15); font-family: sans-serif; display: none; flex-direction: column; }
#keyword-blocker-panel.show { display: flex; }
.kb-panel-header { padding: 10px 12px 8px 12px; border-bottom: 1px solid #f0f0f0; background: #fafafa; border-radius: 8px 8px 0 0; flex-shrink: 0; cursor: move; user-select: none; position: relative; }
.kb-header-actions { position: absolute; right: 10px; top: 8px; display: flex; align-items: center; gap: 6px; z-index: 10; }
.kb-title-row { display: flex; align-items: center; position: relative; min-height: 22px; margin-bottom: 4px; }
.kb-brand-logo { font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, sans-serif; font-weight: 700; font-size: 11px; letter-spacing: 1px; color: #cf1322; background: linear-gradient(180deg, #ffffff 0%, #fff2f0 100%); border: 1px solid #ff7875; border-radius: 8px; padding: 1px 7px; flex-shrink: 0; box-shadow: inset 0 1px 0 #ffffff, 0 1px 4px rgba(207, 19, 34, 0.18); text-shadow: 0 1px 0 rgba(255, 255, 255, 0.9); z-index: 2; }
.kb-panel-title { position: absolute; left: 50%; transform: translateX(-50%); margin: 0; font-size: 12px; font-weight: 600; color: #262626; letter-spacing: 0.3px; whitespace: nowrap; pointer-events: none; }
.kb-close-btn { position: static; background: none; border: none; font-size: 16px; cursor: pointer; color: #999; line-height: 1; padding: 0 2px; transition: color 0.2s; }
.kb-close-btn:hover { color: #ff4d4f; }
.kb-disable-site-btn { padding: 2px 7px; background: #fff; color: #666; border: 1px solid #d9d9d9; border-radius: 4px; cursor: pointer; font-size: 10px; transition: all 0.2s; }
.kb-disable-site-btn:hover { background: #e6f4ff; color: #1677ff; border-color: #91caff; }
.kb-input-group { display: flex; gap: 6px; margin-bottom: 8px; }
.kb-select { padding: 5px; border: 1px solid #d9d9d9; border-radius: 4px; font-size: 12px; outline: none; background: #fff; color: #1890ff; font-weight: bold; }
.kb-input { flex: 1; padding: 5px 8px; border: 1px solid #d9d9d9; border-radius: 4px; font-size: 12px; outline: none; }
.kb-input:focus { border-color: #1890ff; }
.kb-btn { padding: 5px 10px; background: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; flex-shrink: 0; font-weight: 600; }
.kb-tabs { display: flex; border-bottom: 1px solid #e8e8e8; margin-top: 6px; background: #f0f0f0; border-radius: 4px 4px 0 0; }
.kb-tab { flex: 1; text-align: center; padding: 6px 0; cursor: pointer; font-size: 11px; font-weight: 600; color: #666; transition: all 0.2s; }
.kb-tab.active { background: #fff; color: #1890ff; border-top: 2px solid #1890ff; }
.kb-list-container { flex: 1; overflow-y: auto; min-height: 80px; max-height: 160px; background: #fff; border-bottom: 1px solid #f0f0f0; }
.kb-list { list-style: none; margin: 0; padding: 0; }
.kb-list-item { display: flex; justify-content: space-between; align-items: center; padding: 6px 12px; border-bottom: 1px solid #f0f0f0; }
.kb-keyword { flex: 1; font-size: 12px; color: #262626; word-break: break-all; margin-right: 8px; }
.kb-tag-regex { color: #8a2be2; border: 1px solid #8a2be2; border-radius: 3px; padding: 0 3px; font-size: 10px; margin-right: 4px; }
.kb-action-group { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
.kb-visibility-btn { padding: 2px 5px; background: #f5f5f5; color: #666; border: 1px solid #d9d9d9; border-radius: 3px; cursor: pointer; font-size: 11px; flex-shrink: 0; }
.kb-visibility-btn.active { background: #e6f4ff; color: #1890ff; border-color: #91caff; }
.kb-delete-btn { padding: 2px 5px; background: #ff4d4f; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 11px; flex-shrink: 0; }
.kb-edit-section { padding: 8px 12px; background: #fafafa; border-top: 1px solid #f0f0f0; }
.kb-edit-title { font-size: 11px; color: #666; margin-bottom: 4px; display: flex; justify-content: space-between; align-items: center; }
.kb-textarea { width: 100%; height: 50px; box-sizing: border-box; padding: 6px; border: 1px solid #d9d9d9; border-radius: 4px; font-size: 11px; outline: none; resize: vertical; font-family: inherit; }
.kb-apply-btn { padding: 3px 8px; background: #52c41a; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 10px; }
.kb-builder-container { padding: 10px 12px; overflow-y: auto; max-height: 340px; font-size: 11px; color: #333; background: #fff; }
.kb-guide-box { background: #e6f7ff; border: 1px solid #91caff; border-radius: 4px; padding: 6px 8px; margin-bottom: 8px; line-height: 1.4; color: #00308f; }
.kb-builder-form { display: flex; flex-direction: column; gap: 6px; }
.kb-form-item { display: flex; flex-direction: column; gap: 2px; }
.kb-form-label { font-weight: bold; color: #555; }
.kb-builder-input { padding: 4px 6px; border: 1px solid #d9d9d9; border-radius: 3px; font-size: 11px; outline: none; }
.kb-preview-box { background: #f5f5f5; border: 1px solid #e8e8e8; border-radius: 4px; padding: 6px; font-family: monospace; word-break: break-all; margin-top: 4px; color: #8a2be2; font-weight: bold; }
.kb-builder-actions { display: flex; gap: 6px; margin-top: 6px; }
.kb-guide-container { padding: 10px 12px; overflow-y: auto; max-height: 320px; font-size: 11px; color: #333; background: #fff; line-height: 1.5; }
.kb-guide-card { background: #fafafa; border: 1px solid #f0f0f0; border-radius: 4px; padding: 6px 8px; margin-bottom: 6px; }
.kb-guide-card b { color: #1890ff; font-size: 11px; }
.kb-guide-card p { margin: 3px 0 0 0; color: #555; font-size: 11px; }
.kb-guide-card code { background: #e6f4ff; color: #0958d9; padding: 1px 4px; border-radius: 3px; font-family: monospace; }
.kb-stats { padding: 4px 12px; background: #f9f9f9; font-size: 11px; color: #666; text-align: center; }
.kb-confirm-group { display: flex; gap: 4px; }
.kb-confirm-btn { padding: 2px 5px; border: none; border-radius: 3px; cursor: pointer; font-size: 11px; color: white;}
.kb-confirm-delete { background: #ff4d4f; }
.kb-confirm-cancel { background: #8c8c8c; }
.kb-footer { padding: 8px 12px; border-top: 1px solid #f0f0f0; background: #fff; border-radius: 0 0 8px 8px; }
.kb-data-row { display: flex; justify-content: space-between; gap: 6px; }
.kb-data-btn { flex: 1; padding: 4px 0; text-align: center; background: #fff; border: 1px solid #d9d9d9; border-radius: 4px; cursor: pointer; font-size: 11px; color: #666; }
.kb-data-btn:hover { color: #1890ff; border-color: #1890ff; }
.kb-setting-row { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-top: 4px; }
.kb-setting-label { font-size: 11px; color: #666; }
.kb-setting-btn { padding: 2px 6px; background: #fff; border: 1px solid #d9d9d9; border-radius: 4px; cursor: pointer; font-size: 11px; color: #666; }
.kb-setting-btn.active { color: #1890ff; border-color: #1890ff; background: #e6f4ff; }
.kb-footer-credit { margin-top: 6px; font-size: 10px; color: #999; text-align: center; }
.kb-version-highlight { background: #f0f5ff; color: #2f54eb !important; border: 1px solid #d6e4ff; padding: 1px 6px; border-radius: 8px; font-weight: 600; font-size: 10px; display: inline-block; letter-spacing: 0.3px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03); text-shadow: none; }
.kb-mask-overlay { background: #f7f7f7; border: 1px dashed #d9d9d9; border-radius: 8px; padding: 4px 10px; margin: 5px 0; cursor: pointer; text-align: center; font-size: 13px; color: #999; user-select: none; transition: all 0.2s; display: flex; justify-content: center; align-items: center; min-height: 24px; width: 100%; box-sizing: border-box; }
.kb-mask-overlay:hover { background: #f0f0f0; color: #666; border-color: #bbb; }
.kb-mask-keyword { font-weight: bold; color: #ff4d4f; margin: 0 4px; background: rgba(255, 77, 79, 0.1); padding: 0 4px; border-radius: 3px; }
.kb-hidden-content { display: none !important; }
.kb-remask-btn { display: block; width: 100%; text-align: center; padding: 2px 0; margin-top: 5px; background: #f5f5f5; color: #888; font-size: 12px; cursor: pointer; border-radius: 4px; border: 1px solid #eee; transition: all 0.2s; user-select: none; }
.kb-remask-btn:hover { background: #e6e6e6; color: #555; }
`;
document.head.appendChild(style);
const isTwitter = getCurrentSite() === 'twitter';
const toggleBtnId = isTwitter ? 'keyword-blocker-toggle-twitter' : 'keyword-blocker-toggle';
const toggleBtn = document.createElement(isTwitter ? 'a' : 'button');
toggleBtn.id = toggleBtnId;
if (isTwitter) {
document.body.classList.add('kb-twitter-mode');
toggleBtn.style.display = 'none';
document.body.appendChild(toggleBtn);
ensureTwitterTogglePlacement();
} else {
toggleBtn.className = 'kb-floating-btn';
toggleBtn.textContent = '屏蔽词';
document.body.appendChild(toggleBtn);
}
const panel = document.createElement('div');
panel.id = 'keyword-blocker-panel';
try {
const pos = JSON.parse(localStorage.getItem(PANEL_POS_KEY));
if (pos && pos.left && pos.top) {
panel.style.left = pos.left;
panel.style.top = pos.top;
} else {
panel.style.left = '20px';
panel.style.top = '100px';
}
} catch(e) {
panel.style.left = '20px';
panel.style.top = '100px';
}
const siteNames = {'zhihu':'知乎','xiaohongshu':'小红书','bilibili':'B站','weibo':'微博','twitter':'X / Twitter'};
const siteName = siteNames[getCurrentSite()] || '当前网站';
const isDisabled = isCurrentSiteDisabled();
const masterMaskSwitchOn = isMasterMaskSwitchOn();
panel.innerHTML = `
<div class="kb-panel-header" id="kb-drag-handle">
<div class="kb-header-actions">
<button class="kb-disable-site-btn" id="kb-disable-site">${isDisabled ? '启用屏蔽' : '暂停屏蔽'}</button>
<button class="kb-close-btn" id="kb-close">×</button>
</div>
<div class="kb-title-row">
<div class="kb-brand-logo">XBlock</div>
<h3 class="kb-panel-title" id="kb-header-title">${isDisabled ? '⚠️ 屏蔽已暂停' : '屏蔽规则管理'}</h3>
</div>
<div class="kb-input-group" id="kb-quick-add-group">
<select id="kb-type-select" class="kb-select">
<option value="content" selected>加正文</option>
<option value="name">加昵称</option>
</select>
<input type="text" id="kb-input" class="kb-input" placeholder="输入拦截词(支持正则/xxx/u)..." />
<button id="kb-add-btn" class="kb-btn">添加</button>
</div>
<div class="kb-tabs">
<div class="kb-tab active" id="kb-tab-name">昵称规则</div>
<div class="kb-tab" id="kb-tab-content">正文规则</div>
<div class="kb-tab" id="kb-tab-regex">⚡正则向导</div>
<div class="kb-tab" id="kb-tab-guide">📖规则指南</div>
</div>
</div>
<div class="kb-list-container" id="kb-view-list"><ul id="kb-list" class="kb-list"></ul></div>
<div class="kb-edit-section" id="kb-view-edit">
<div class="kb-edit-title">
<span>编辑当前分类规则 (每行一条)</span>
<button id="kb-apply-textarea-btn" class="kb-apply-btn">应用修改</button>
</div>
<textarea id="kb-edit-textarea" class="kb-textarea" placeholder="在此批量直接编辑当前标签下的规则..."></textarea>
</div>
<div class="kb-builder-container" id="kb-view-builder" style="display:none;">
<div class="kb-guide-box">
<b>💡 正则匹配向导:</b><br>
选择所需模式,填入关键词,即可自动生成无误的标准正则规则。
</div>
<div class="kb-builder-form">
<div class="kb-form-item">
<span class="kb-form-label">匹配模式下拉选择:</span>
<select id="kb-builder-mode" class="kb-builder-input">
<option value="single_emoji">纯 Emoji 屏蔽 (仅由一个或多个 Emoji 组成)</option>
<option value="lines_sym_emoji_2">共 2 行刷屏 (第1行符号 + 第2行Emoji)</option>
<option value="lines_sym_emoji_4">共 4 行刷屏 (符号 + Emoji 交替出现)</option>
<option value="branch">前后缀+多选一分支 (如: 加微信 + 领券/抽奖)</option>
<option value="order">顺序链条 (如: 优惠 ... 促销 ... 免费)</option>
<option value="any_order">无序同时包含 (如: 同时出现 抽奖 和 转发)</option>
<option value="or">满足任意一个 (如: 出现 广告 或 推广 或 合作)</option>
<option value="single">转义精准短语 (带有 C++ 或 $ 等符号)</option>
<option value="starts_with">指定开头匹配 (如: 以 推广 或 赞助 开头)</option>
<option value="user_mention">连续 @/提及 灌水 (连续 2 个以上的艾特打扰)</option>
</select>
</div>
<div class="kb-form-item">
<span class="kb-form-label" id="kb-kw-label">参数与关键词:</span>
<textarea id="kb-builder-kws" class="kb-textarea" style="height:55px;"></textarea>
</div>
<div class="kb-form-item">
<span class="kb-form-label">预览生成的正则规则:</span>
<div class="kb-preview-box" id="kb-builder-preview">/.../</div>
</div>
<div class="kb-builder-actions">
<button id="kb-builder-copy" class="kb-data-btn" style="flex:1;">复制规则</button>
<button id="kb-builder-import-content" class="kb-btn" style="flex:1; background:#8a2be2;">一键导入正文</button>
<button id="kb-builder-import-name" class="kb-btn" style="flex:1; background:#1890ff;">一键导入昵称</button>
</div>
</div>
</div>
<div class="kb-guide-container" id="kb-view-guide" style="display:none;">
<div class="kb-guide-card">
<b>🔰 1. 零基础入门:普通词 vs 正则表达式</b>
<p><b>① 普通词(精准字串)</b> — 直接写词,如 <code>加微信</code>。只要贴子或昵称包含这三个字就直接拦截。<br>
<b>② 正则规则(推荐)</b> — 用斜杠 <code>/</code> 包裹,如 <code>/.*?微信.*?领券/i</code>。能跳过中间干扰字,专治各种同义词变形与变体。<br><br>
<b>🔍 正则定位符秒懂秘籍:</b><br>
• 最前面的 <code>.*?</code> —— 代表<b>允许前面出现任意数量的无关上下文文字</b>;<br>
• 中间处的 <code>.*?</code> —— 代表<b>允许中间跳过任意数量的干扰词</b>;<br>
• 开头的 <code>^</code> 符号 —— 代表<b>强行锁定必须从第1个字开始匹配</b>;<br>
• 结尾的 <code>$</code> 符号 —— 代表<b>强行锁定必须匹配到最后一个字</b>。<br><br>
<b>📌 分类建议</b>:顶部添加时,“加昵称”仅对发帖人用户名生效;“加正文”对帖子或评论内容生效。</p>
</div>
<div class="kb-guide-card">
<b>💡 2. 常用实战模板(直接复制套用)</b>
<p><b>① 按顺序精准抓词(前面或中间有干扰字都能抓)</b><br>
<code>/.*?微信.*?领券/i</code><br>
→ 匹配:"【今日福利】添加<b>微信</b>公众号免费<b>领券</b>"<br>
→ 匹配:"加<b>微信</b>送大额<b>领券</b>福利"<br><br>
<b>② 同义词/拼写变体(| 分隔,任意位置出现即拦截)</b><br>
<code>/.*?(微信|V信|vx|微信号)/i</code><br>
→ 最前带有 <code>.*?</code>,能准确捕获推文中任意位置出现的拼写变体<br>
→ 修饰符 <code>/i</code> 会自动忽略大小写(如 VX 和 vx 都能生效)<br><br>
<b>③ 无序同时包含(同时出现 A 和 B,顺序与位置不限)</b><br>
<code>/(?=.*转发)(?=.*抽奖)/</code><br>
→ 匹配:"<b>转发</b>本条动态参与<b>抽奖</b>"<br>
→ 匹配:"<b>抽奖</b>活动开启,请及时<b>转发</b>"<br><br>
<b>④ 屏蔽纯 Emoji / 表情符号刷屏</b><br>
<code>/^[\\s\\n\\r\\uFE0F\\u200D\\p{Extended_Pictographic}]+$/u</code><br>
→ 带有 <code>^</code> 和 <code>$</code> 锁定全条,专门干掉整条回复全由 😄🎉🔥 等表情或空白组成的刷屏<br><br>
<b>⑤ 指定开头匹配(拦截以特定词开头的广告标题)</b><br>
<code>/^(推广|广告|赞助)/</code><br>
→ 带有 <code>^</code> 锁定开头,匹配以 "推广" 或 "广告" 开头的内容<br><br>
<b>⑥ 符号转义(带有 +、?、*、$ 等正则表达式保留字)</b><br>
<code>/.*?C\+\+/</code> 或 <code>/.*?\$9\.9/</code><br>
→ 将 <code>+</code> 写作 <code>\+</code>、<code>$</code> 写作 <code>\$</code>,防止正则语法解析错误</p>
</div>
<div class="kb-guide-card">
<b>⚡ 3. 极速上手:使用【正则向导】零代码生成</b>
<p>无需手写任何正则符号!点击顶部的 <b>【⚡正则向导】</b> 标签:<br>
1. <b>选择模式</b>:如“同义词/多选一”或“顺序链条”;<br>
2. <b>填写词汇</b>:按提示输入想要屏蔽的关键词;<br>
3. <b>一键导入</b>:点击 <b>“一键导入正文”</b> 或 <b>“一键导入昵称”</b> 即可自动写入规则列表并生效!向导已自动为您补充好前缀通配符。</p>
</div>
<div class="kb-guide-card">
<b>🚀 4. 高效使用技巧与避坑指南</b>
<p>• <b>切忌使用过短的通用字</b>:例如单字 <code>/微/</code> 会误杀“微博”、“微风”等正常讨论。建议组合上下文(如 <code>/.*?微信.*?领/</code>)。<br>
• <b>利用大正则清理冗余规则</b>:当有了覆盖广的同义词正则 <code>/.*?(词A|词B)/</code>,可删除原有的单词规则,规则库越精简,页面运行越流畅。<br>
• <b>备份与恢复</b>:底部提供 <b>“导出配置”</b> 按钮,可保存为 JSON 文件;在其他设备上可通过 <b>“导入配置”</b> 一键同步。</p>
</div>
</div>
<div class="kb-stats">当前 ${siteName} 共有 <span id="kb-count">0</span> 条规则</div>
<div class="kb-footer">
<div class="kb-setting-row">
<span class="kb-setting-label">屏蔽条总开关(开=默认隐藏)</span>
<button id="kb-mask-master-toggle" class="kb-setting-btn ${masterMaskSwitchOn ? 'active' : ''}">${masterMaskSwitchOn ? '开启' : '关闭'}</button>
</div>
<div class="kb-setting-row" style="margin-bottom: 8px;">
<span class="kb-setting-label">切换后点保存生效</span>
<button id="kb-save-visibility-btn" class="kb-save-btn">保存并刷新</button>
</div>
<div class="kb-data-row">
<button id="kb-export-btn" class="kb-data-btn">导出配置</button>
<button id="kb-import-btn" class="kb-data-btn">导入配置</button>
<input type="file" id="kb-import-input" style="display:none" accept=".json">
</div>
<div class="kb-footer-credit">
<div><span class="kb-version-tag kb-version-highlight">v2.6.14</span></div>
<div style="margin-top: 3px;">For TGFC BY TGFC</div>
</div>
</div>
`;
document.body.appendChild(panel);
makeDraggable(panel, panel.querySelector('#kb-drag-handle'));
}
function makeDraggable(panel, dragHandle) {
let isDragging = false;
let startX, startY, initialLeft, initialTop;
dragHandle.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT' || e.target.tagName === 'BUTTON') return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = panel.getBoundingClientRect();
initialLeft = rect.left;
initialTop = rect.top;
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
let newX = initialLeft + dx;
let newY = initialTop + dy;
const maxW = panel.offsetWidth;
const maxH = panel.offsetHeight;
if (newX < 0) newX = 0;
if (newY < 0) newY = 0;
if (newX + maxW > window.innerWidth) newX = window.innerWidth - maxW;
if (newY + maxH > window.innerHeight) newY = window.innerHeight - maxH;
panel.style.left = `${newX}px`;
panel.style.top = `${newY}px`;
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
localStorage.setItem(PANEL_POS_KEY, JSON.stringify({
left: panel.style.left,
top: panel.style.top
}));
}
});
}
function ensureTwitterTogglePlacement() {
if (getCurrentSite() !== 'twitter') return;
try {
const currentToggle = document.getElementById('keyword-blocker-toggle-twitter');
if (!currentToggle) return;
const nav = document.querySelector('nav[aria-label="主要"]') || document.querySelector('nav[role="navigation"]');
const bookmarkLink = nav?.querySelector('a[href="/i/bookmarks"]');
if (!nav || !bookmarkLink || !bookmarkLink.firstElementChild) return;
const signature = bookmarkLink.className + '|' + (bookmarkLink.firstElementChild.className || '');
if (currentToggle.dataset.kbTwitterSignature !== signature) {
const clonedLink = bookmarkLink.cloneNode(true);
clonedLink.id = 'keyword-blocker-toggle-twitter';
clonedLink.setAttribute('href', '#');
clonedLink.setAttribute('aria-label', '屏蔽词');
clonedLink.removeAttribute('data-testid');
clonedLink.style.cssText = '';
const textWrap = clonedLink.querySelector('div[dir="ltr"]');
const textSpans = textWrap ? textWrap.querySelectorAll('span') : [];
if (textSpans[0]) textSpans[0].textContent = '屏蔽词';
const svg = clonedLink.querySelector('svg');
if (svg) {
svg.setAttribute('viewBox', '0 0 24 24');
svg.innerHTML = '<g><path d="M3 7.75C3 6.78 3.78 6 4.75 6h14.5c.97 0 1.75.78 1.75 1.75S20.22 9.5 19.25 9.5H4.75C3.78 9.5 3 8.72 3 7.75zm0 8.5C3 15.28 3.78 14.5 4.75 14.5h14.5c.97 0 1.75.78 1.75 1.75S20.22 18 19.25 18H4.75C3.78 18 3 17.22 3 16.25z"></path><circle cx="8" cy="7.75" r="2.25" fill="white"></circle><circle cx="16" cy="16.25" r="2.25" fill="white"></circle><path d="M8 4.5a3.25 3.25 0 1 0 0 6.5 3.25 3.25 0 0 0 0-6.5zm0 1.5a1.75 1.75 0 1 1 0 3.5A1.75 1.75 0 0 1 8 6zm8 7a3.25 3.25 0 1 0 0 6.5 3.25 3.25 0 0 0 0-6.5zm0 1.5a1.75 1.75 0 1 1 0 3.5 1.75 1.75 0 0 1 0-3.5z"></path></g>';
}
clonedLink.dataset.kbTwitterSignature = signature;
clonedLink.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const panel = document.getElementById('keyword-blocker-panel');
if (panel) panel.classList.toggle('show');
});
currentToggle.replaceWith(clonedLink);
}
const finalToggle = document.getElementById('keyword-blocker-toggle-twitter');
if (bookmarkLink.nextElementSibling !== finalToggle) {
nav.insertBefore(finalToggle, bookmarkLink.nextSibling);
}
finalToggle.style.display = '';
} catch (e) {
console.error('X 按钮定位失败:', e);
}
}
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function generateRegexFromInput() {
const mode = document.getElementById('kb-builder-mode')?.value;
const rawText = document.getElementById('kb-builder-kws')?.value || '';
if (mode === 'single_emoji') {
return `/^[\\s\\n\\r\\uFE0F\\u200D\\p{Extended_Pictographic}]+$/u`;
}
if (mode === 'lines_sym_emoji_2') {
return `/^[\\p{P}\\p{S}]+\\r?\\n\\p{Extended_Pictographic}+$/u`;
}
if (mode === 'lines_sym_emoji_4') {
return `/^[\\p{P}\\p{S}]+\\r?\\n\\p{Extended_Pictographic}+\\r?\\n[\\p{P}\\p{S}]+\\r?\\n\\p{Extended_Pictographic}+$/u`;
}
if (mode === 'user_mention') {
return `/.*?(@[\\w_\\u4e00-\\u9fa5]+\\s*){2,}/u`;
}
if (mode === 'branch') {
let prefix = '', suffix = '', choices = [];
const lines = rawText.split('\n');
lines.forEach(l => {
if (l.startsWith('前缀:')) prefix = l.replace('前缀:', '').trim();
else if (l.startsWith('后缀:')) suffix = l.replace('后缀:', '').trim();
else if (l.startsWith('选项:')) choices = l.replace('选项:', '').split(/[,,]/).map(c => c.trim()).filter(Boolean);
});
if (choices.length === 0) {
choices = rawText.split(/[,,\n]/).map(c => c.trim()).filter(Boolean);
}
if (choices.length === 0) return '/.../';
const branchStr = `(${choices.map(escapeRegExp).join('|')})`;
const pStr = prefix ? `${escapeRegExp(prefix)}.*?` : '';
const sStr = suffix ? `.*?${escapeRegExp(suffix)}` : '';
return `/.*?${pStr}${branchStr}${sStr}/`;
}
const kws = rawText.split(/[\n,,]/).map(k => k.trim()).filter(Boolean);
if (kws.length === 0) return '/.../';
let result = '';
if (mode === 'order') {
result = '.*?' + kws.map(escapeRegExp).join('.*?');
} else if (mode === 'any_order') {
result = kws.map(k => `(?=.*${escapeRegExp(k)})`).join('');
} else if (mode === 'or') {
result = '.*?' + `(${kws.map(escapeRegExp).join('|')})`;
} else if (mode === 'single') {
result = '.*?' + escapeRegExp(kws.join(''));
} else if (mode === 'starts_with') {
result = `^(${kws.map(escapeRegExp).join('|')})`;
}
return `/${result}/`;
}
function renderKeywordList() {
const list = document.getElementById('kb-list');
const count = document.getElementById('kb-count');
const tabName = document.getElementById('kb-tab-name');
const tabContent = document.getElementById('kb-tab-content');
const tabRegex = document.getElementById('kb-tab-regex');
const tabGuide = document.getElementById('kb-tab-guide');
const viewList = document.getElementById('kb-view-list');
const viewEdit = document.getElementById('kb-view-edit');
const viewBuilder = document.getElementById('kb-view-builder');
const viewGuide = document.getElementById('kb-view-guide');
const quickAddGroup = document.getElementById('kb-quick-add-group');
const textarea = document.getElementById('kb-edit-textarea');
if (!list) return;
list.innerHTML = '';
count.textContent = BLOCK_RULES.length;
const nameCount = BLOCK_RULES.filter(r => r.type === 'name').length;
const contentCount = BLOCK_RULES.filter(r => r.type === 'content').length;
if (tabName) tabName.textContent = `昵称规则 (${nameCount})`;
if (tabContent) tabContent.textContent = `正文规则 (${contentCount})`;
if (currentTab === 'guide') {
viewList.style.display = 'none';
viewEdit.style.display = 'none';
quickAddGroup.style.display = 'none';
viewBuilder.style.display = 'none';
if (viewGuide) viewGuide.style.display = 'block';
if (tabGuide) tabGuide.classList.add('active');
if (tabName) tabName.classList.remove('active');
if (tabContent) tabContent.classList.remove('active');
if (tabRegex) tabRegex.classList.remove('active');
return;
}
if (viewGuide) viewGuide.style.display = 'none';
if (tabGuide) tabGuide.classList.remove('active');
if (currentTab === 'regex-builder') {
viewList.style.display = 'none';
viewEdit.style.display = 'none';
quickAddGroup.style.display = 'none';
viewBuilder.style.display = 'block';
tabRegex.classList.add('active');
tabName.classList.remove('active');
tabContent.classList.remove('active');
return;
}
viewList.style.display = 'block';
viewEdit.style.display = 'block';
quickAddGroup.style.display = 'flex';
viewBuilder.style.display = 'none';
tabRegex.classList.remove('active');
const filteredRules = BLOCK_RULES.filter(r => r.type === currentTab);
if (textarea) {
textarea.value = filteredRules.map(r => r.value).join('\n');
}
if (filteredRules.length === 0) {
list.innerHTML = '<li class="kb-list-item" style="color:#999; justify-content:center; font-size:12px;">暂无匹配的规则</li>';
return;
}
filteredRules.forEach((rule) => {
const index = BLOCK_RULES.findIndex(r => getRuleKey(r) === getRuleKey(rule));
const li = document.createElement('li');
li.className = 'kb-list-item';
li.dataset.index = index;
const isVisible = getKeywordMaskBarVisibleForUI(rule);
const isRegex = isRegexRule(rule.value);
const safeVal = rule.value.replace(/</g, "<").replace(/>/g, ">");
const valHtml = isRegex ? `<span class="kb-tag-regex">正则</span>${safeVal}` : safeVal;
li.innerHTML = `
<span class="kb-keyword">${valHtml}</span>
<div class="kb-action-group">
<button class="kb-visibility-btn ${isVisible ? 'active' : ''}" data-index="${index}">${isVisible ? '显示条' : '隐藏条'}</button>
<button class="kb-delete-btn" data-index="${index}">删除</button>
</div>
`;
list.appendChild(li);
});
}
function initUIEvents() {
const toggleBtnId = getCurrentSite() === 'twitter' ? 'keyword-blocker-toggle-twitter' : 'keyword-blocker-toggle';
const toggleBtn = document.getElementById(toggleBtnId);
const panel = document.getElementById('keyword-blocker-panel');
const closeBtn = document.getElementById('kb-close');
const addBtn = document.getElementById('kb-add-btn');
const input = document.getElementById('kb-input');
const typeSelect = document.getElementById('kb-type-select');
const tabName = document.getElementById('kb-tab-name');
const tabContent = document.getElementById('kb-tab-content');
const tabRegex = document.getElementById('kb-tab-regex');
const tabGuide = document.getElementById('kb-tab-guide');
const list = document.getElementById('kb-list');
const disableSiteBtn = document.getElementById('kb-disable-site');
const headerTitle = document.getElementById('kb-header-title');
const exportBtn = document.getElementById('kb-export-btn');
const importBtn = document.getElementById('kb-import-btn');
const importInput = document.getElementById('kb-import-input');
const maskMasterToggleBtn = document.getElementById('kb-mask-master-toggle');
const saveVisibilityBtn = document.getElementById('kb-save-visibility-btn');
const applyTextareaBtn = document.getElementById('kb-apply-textarea-btn');
const textarea = document.getElementById('kb-edit-textarea');
const builderMode = document.getElementById('kb-builder-mode');
const builderKws = document.getElementById('kb-builder-kws');
const builderPreview = document.getElementById('kb-builder-preview');
const builderCopyBtn = document.getElementById('kb-builder-copy');
const builderImportContentBtn = document.getElementById('kb-builder-import-content');
const builderImportNameBtn = document.getElementById('kb-builder-import-name');
if (getCurrentSite() === 'twitter') ensureTwitterTogglePlacement();
const toggle = () => {
panel.classList.toggle('show');
const currentToggleBtn = document.getElementById(toggleBtnId);
if (getCurrentSite() !== 'twitter' && currentToggleBtn) {
currentToggleBtn.style.display = panel.classList.contains('show') ? 'none' : 'block';
}
};
if (toggleBtn) toggleBtn.onclick = toggle;
closeBtn.onclick = toggle;
document.onclick = (e) => {
const currentToggleBtn = document.getElementById(toggleBtnId);
if (panel.classList.contains('show') && !panel.contains(e.target) && (!currentToggleBtn || !currentToggleBtn.contains(e.target))) {
toggle();
}
};
tabName.onclick = (e) => {
e.stopPropagation();
currentTab = 'name';
tabName.classList.add('active');
tabContent.classList.remove('active');
tabRegex.classList.remove('active');
if (tabGuide) tabGuide.classList.remove('active');
renderKeywordList();
};
tabContent.onclick = (e) => {
e.stopPropagation();
currentTab = 'content';
tabContent.classList.add('active');
tabName.classList.remove('active');
tabRegex.classList.remove('active');
if (tabGuide) tabGuide.classList.remove('active');
renderKeywordList();
};
tabRegex.onclick = (e) => {
e.stopPropagation();
currentTab = 'regex-builder';
renderKeywordList();
if (builderMode) {
builderMode.dispatchEvent(new Event('change'));
}
};
if (tabGuide) {
tabGuide.onclick = (e) => {
e.stopPropagation();
currentTab = 'guide';
renderKeywordList();
};
}
const updatePreview = () => {
if (builderPreview) builderPreview.textContent = generateRegexFromInput();
};
if (builderMode) {
builderMode.onchange = () => {
const modeVal = builderMode.value;
const preset = BUILDER_PRESETS[modeVal];
if (preset && builderKws) {
builderKws.placeholder = preset.placeholder;
builderKws.value = preset.defaultText;
}
updatePreview();
};
}
if (builderKws) builderKws.oninput = updatePreview;
if (builderCopyBtn) {
builderCopyBtn.onclick = (e) => {
e.stopPropagation();
const regStr = generateRegexFromInput();
if (regStr === '/.../') return;
navigator.clipboard.writeText(regStr).then(() => alert('已复制正则规则:' + regStr));
};
}
const handleBuilderImport = (type) => {
const regStr = generateRegexFromInput();
if (regStr === '/.../') {
alert('请先输入关键词!');
return;
}
const newRule = { type, value: regStr };
const exists = BLOCK_RULES.some(r => getRuleKey(r) === getRuleKey(newRule));
if (!exists) {
BLOCK_RULES.unshift(newRule);
saveKeywords(BLOCK_RULES);
currentTab = type;
renderKeywordList();
processAllContent();
alert(`已成功导入到【${type === 'name' ? '昵称' : '正文'}规则】!`);
} else {
alert('该正则规则已存在!');
}
};
if (builderImportContentBtn) builderImportContentBtn.onclick = (e) => { e.stopPropagation(); handleBuilderImport('content'); };
if (builderImportNameBtn) builderImportNameBtn.onclick = (e) => { e.stopPropagation(); handleBuilderImport('name'); };
addBtn.onclick = (e) => {
e.stopPropagation();
const val = input.value.trim();
const type = typeSelect.value;
if(!val) return;
const parsed = parseRegexString(val);
if (parsed) {
try {
new RegExp(parsed.pattern, parsed.flags || 'is');
} catch (err) {
alert('正则表达式语法有误:' + err.message);
return;
}
}
const newRule = { type, value: val };
const exists = BLOCK_RULES.some(r => getRuleKey(r) === getRuleKey(newRule));
if (!exists) {
BLOCK_RULES.unshift(newRule);
saveKeywords(BLOCK_RULES);
currentTab = type;
if (type === 'name') {
tabName.classList.add('active');
tabContent.classList.remove('active');
} else {
tabContent.classList.add('active');
tabName.classList.remove('active');
}
renderKeywordList();
input.value = '';
processAllContent();
}
};
input.onkeypress = (e) => { if(e.key === 'Enter') addBtn.click(); };
applyTextareaBtn.onclick = (e) => {
e.stopPropagation();
const lines = textarea.value.split('\n').map(l => l.trim()).filter(Boolean);
const otherTypeRules = BLOCK_RULES.filter(r => r.type !== currentTab);
const updatedTabRules = [];
for (let line of lines) {
const parsed = parseRegexString(line);
if (parsed) {
try {
new RegExp(parsed.pattern, parsed.flags || 'is');
} catch (err) {
alert(`规则 [${line}] 正则语法错误,已被跳过`);
continue;
}
}
updatedTabRules.push({ type: currentTab, value: line });
}
BLOCK_RULES = [...otherTypeRules, ...updatedTabRules];
saveKeywords(BLOCK_RULES);
renderKeywordList();
processAllContent();
alert('规则修改已应用!');
};
PENDING_MASTER_MASK_SWITCH = isMasterMaskSwitchOn();
PENDING_KEYWORD_MASK_VISIBILITY = { ...KEYWORD_MASK_VISIBILITY };
if (maskMasterToggleBtn) {
maskMasterToggleBtn.onclick = (e) => {
e.stopPropagation();
const nextState = !PENDING_MASTER_MASK_SWITCH;
PENDING_MASTER_MASK_SWITCH = nextState;
maskMasterToggleBtn.textContent = nextState ? '开启' : '关闭';
maskMasterToggleBtn.classList.toggle('active', nextState);
renderKeywordList();
};
}
if (saveVisibilityBtn) {
saveVisibilityBtn.onclick = (e) => {
e.stopPropagation();
saveMasterMaskSwitch(PENDING_MASTER_MASK_SWITCH);
KEYWORD_MASK_VISIBILITY = { ...PENDING_KEYWORD_MASK_VISIBILITY };
saveKeywordMaskVisibility(KEYWORD_MASK_VISIBILITY);
location.reload();
};
}
disableSiteBtn.onclick = (e) => {
e.stopPropagation();
if(isCurrentSiteDisabled()) {
enableCurrentSite();
disableSiteBtn.textContent = '暂停屏蔽';
if (headerTitle) headerTitle.innerHTML = '屏蔽规则管理';
processAllContent();
} else {
disableCurrentSite();
disableSiteBtn.textContent = '启用屏蔽';
if (headerTitle) headerTitle.textContent = '⚠️ 屏蔽已暂停';
// 还原所有隐藏的项目
document.querySelectorAll('[data-kb-status="processed"]').forEach(el => {
el.style.display = '';
Array.from(el.children).forEach(child => child.style.display = '');
const mask = el.querySelector('.kb-mask-overlay');
if (mask) mask.remove();
const remask = el.querySelector('.kb-remask-btn');
if (remask) remask.remove();
el.removeAttribute('data-kb-status');
});
}
};
list.onclick = (e) => {
e.stopPropagation();
const idx = parseInt(e.target.dataset.index);
if (Number.isNaN(idx) || !BLOCK_RULES[idx]) return;
const rule = BLOCK_RULES[idx];
if (e.target.classList.contains('kb-visibility-btn')) {
const nextVisible = !getKeywordMaskBarVisibleForUI(rule);
PENDING_KEYWORD_MASK_VISIBILITY[getRuleKey(rule)] = nextVisible;
renderKeywordList();
} else if (e.target.classList.contains('kb-delete-btn')) {
const li = e.target.closest('li');
const isRegex = isRegexRule(rule.value);
const safeVal = rule.value.replace(/</g, "<").replace(/>/g, ">");
const valHtml = isRegex ? `<span class="kb-tag-regex">正则</span>${safeVal}` : safeVal;
li.innerHTML = `
<span class="kb-keyword">${valHtml}</span>
<div class="kb-action-group">
<button class="kb-confirm-btn kb-confirm-delete" data-index="${idx}">确认</button>
<button class="kb-confirm-btn kb-confirm-cancel" data-index="${idx}">取消</button>
</div>
`;
} else if (e.target.classList.contains('kb-confirm-delete')) {
removeKeywordMaskBarVisible(rule);
delete PENDING_KEYWORD_MASK_VISIBILITY[getRuleKey(rule)];
BLOCK_RULES.splice(idx, 1);
saveKeywords(BLOCK_RULES);
renderKeywordList();
} else if (e.target.classList.contains('kb-confirm-cancel')) {
renderKeywordList();
}
};
exportBtn.onclick = (e) => {
e.stopPropagation();
try {
const data = {
keywords: BLOCK_RULES,
maskBarMasterSwitch: isMasterMaskSwitchOn(),
keywordMaskVisibility: KEYWORD_MASK_VISIBILITY
};
const dataStr = JSON.stringify(data, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'keyword-blocker-data.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (err) {
console.error('导出失败:', err);
alert('导出失败,请查看控制台');
}
};
importBtn.onclick = (e) => {
e.stopPropagation();
importInput.click();
};
importInput.onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
try {
const parsed = JSON.parse(event.target.result);
let importedRules = [];
if (Array.isArray(parsed)) {
importedRules = parsed.map(normalizeRule).filter(Boolean);
} else if (parsed && Array.isArray(parsed.keywords)) {
importedRules = parsed.keywords.map(normalizeRule).filter(Boolean);
if (typeof parsed.maskBarMasterSwitch === 'boolean') {
saveMasterMaskSwitch(parsed.maskBarMasterSwitch);
}
if (parsed.keywordMaskVisibility && typeof parsed.keywordMaskVisibility === 'object' && !Array.isArray(parsed.keywordMaskVisibility)) {
KEYWORD_MASK_VISIBILITY = { ...parsed.keywordMaskVisibility };
PENDING_KEYWORD_MASK_VISIBILITY = { ...KEYWORD_MASK_VISIBILITY };
saveKeywordMaskVisibility(KEYWORD_MASK_VISIBILITY);
}
} else {
alert('导入失败:JSON 格式不正确');
return;
}
BLOCK_RULES = importedRules;
saveKeywords(BLOCK_RULES);
renderKeywordList();
alert(`导入成功:已完全替换当前规则,共导入 ${BLOCK_RULES.length} 条规则`);
location.reload();
} catch (err) {
console.error('导入失败:', err);
alert('导入失败:请确保是正确的 JSON 文件');
}
importInput.value = '';
};
reader.readAsText(file);
};
}
function createMaskElement(matchedText, onClick) {
const mask = document.createElement('div');
mask.className = 'kb-mask-overlay';
const safeText = matchedText.replace(/</g, "<").replace(/>/g, ">");
mask.innerHTML = `<span>🙈 已折叠包含 <span class="kb-mask-keyword">${safeText}</span> 的内容 (点击查看)</span>`;
mask.onclick = (e) => {
e.stopPropagation();
e.preventDefault();
onClick(mask);
};
return mask;
}
function createRemaskButton(onRemask) {
const btn = document.createElement('div');
btn.className = 'kb-remask-btn';
btn.innerText = '🔼 收起 (重新屏蔽)';
btn.onclick = (e) => {
e.stopPropagation();
e.preventDefault();
onRemask();
btn.remove();
};
return btn;
}
function checkRuleMatch(targetText, ruleValue) {
if (!targetText) return false;
let kw = ruleValue.trim();
const parsed = parseRegexString(kw);
if (parsed) {
try {
let flags = parsed.flags || '';
if (!flags.includes('i')) flags += 'i';
if (!flags.includes('s')) flags += 's';
return new RegExp(parsed.pattern, flags).test(targetText);
} catch (e) {
return false;
}
}
return targetText.toLowerCase().includes(kw.toLowerCase());
}
function getElementFullText(elements) {
let text = "";
elements.forEach(el => {
let str = "";
el.childNodes.forEach(node => {
if (node.nodeType === Node.TEXT_NODE) {
str += node.textContent;
} else if (node.nodeType === Node.ELEMENT_NODE) {
if (node.tagName === 'IMG' && node.getAttribute('alt')) {
str += node.getAttribute('alt');
} else {
str += node.textContent;
}
}
});
text += str + " ";
});
return text;
}
function processContentElement(element, config) {
if (isCurrentSiteDisabled()) return;
if (element.getAttribute('data-kb-status') === 'processed') return;
const site = getCurrentSite();
if (site === 'bilibili') {
if (element.classList.contains('bili-video-card') && element.classList.contains('is-rcmd') && !element.classList.contains('enable-no-interest')) {
element.style.display = 'none';
element.setAttribute('data-kb-status', 'processed');
return;
}
}
const nameEls = config.nameSelector ? element.querySelectorAll(config.nameSelector) : [];
const titleEls = config.titleSelector ? element.querySelectorAll(config.titleSelector) : [];
let nameText = getElementFullText(nameEls);
let contentText = getElementFullText(titleEls);
if (!nameText && !contentText) {
contentText = element.textContent || '';
}
let matchedRule = null;
for (let rule of BLOCK_RULES) {
let targetText = (rule.type === 'name') ? nameText : contentText;
if (checkRuleMatch(targetText, rule.value)) {
matchedRule = rule;
break;
}
}
element.setAttribute('data-kb-status', 'processed');
if (!matchedRule) return;
let targetContent = element;
let insertParent = element.parentNode;
if (site === 'zhihu' && element.closest('.Card.TopstoryItem')) {
targetContent = element.closest('.Card.TopstoryItem');
insertParent = targetContent.parentNode;
targetContent.setAttribute('data-kb-status', 'processed');
} else if (site === 'bilibili') {
targetContent = element.closest('.feed-card') || element.closest('.bili-feed-card') || element;
insertParent = targetContent.parentNode;
} else if (site === 'twitter') {
targetContent = element;
insertParent = element;
}
const showMaskBar = getKeywordMaskBarVisible(matchedRule);
if (!showMaskBar) {
targetContent.style.display = 'none';
return;
}
const hideTarget = () => {
if (site === 'twitter') {
Array.from(targetContent.children).forEach(child => {
if (!child.classList.contains('kb-mask-overlay') && !child.classList.contains('kb-remask-btn')) {
child.style.display = 'none';
}
});
} else {
targetContent.classList.add('kb-hidden-content');
}
};
const showTarget = () => {
if (site === 'twitter') {
Array.from(targetContent.children).forEach(child => {
if (!child.classList.contains('kb-mask-overlay') && !child.classList.contains('kb-remask-btn')) {
child.style.display = '';
}
});
} else {
targetContent.classList.remove('kb-hidden-content');
}
};
let mask = null;
const handleRemask = () => {
hideTarget();
if (mask) mask.style.display = 'flex';
const existingRemaskBtn = targetContent.querySelector('.kb-remask-btn');
if (existingRemaskBtn) existingRemaskBtn.remove();
};
const handleReveal = () => {
showTarget();
if (mask) mask.style.display = 'none';
const existingRemaskBtn = targetContent.querySelector('.kb-remask-btn');
if (existingRemaskBtn) existingRemaskBtn.remove();
const remaskBtn = createRemaskButton(handleRemask);
targetContent.appendChild(remaskBtn);
};
mask = createMaskElement(matchedRule.value, handleReveal);
hideTarget();
if (site === 'twitter') {
targetContent.appendChild(mask);
} else if (insertParent) {
insertParent.insertBefore(mask, targetContent);
}
}
function processAllContent() {
const site = getCurrentSite();
const config = siteConfigs[site];
if (!config) return;
const elements = document.querySelectorAll(config.containerSelector);
elements.forEach(el => processContentElement(el, config));
}
function init() {
console.log(`屏蔽器启动: ${getCurrentSite()}`);
createManagementUI();
renderKeywordList();
initUIEvents();
setInterval(processAllContent, 300);
const observerConfig = { childList: true, subtree: true };
const observer = new MutationObserver((mutations) => {
let shouldRun = false;
for(let m of mutations) {
if(m.addedNodes.length) {
shouldRun = true;
break;
}
}
if(shouldRun) {
processAllContent();
if (getCurrentSite() === 'twitter') ensureTwitterTogglePlacement();
}
});
const startObserver = () => {
if (document.body) {
observer.observe(document.body, observerConfig);
processAllContent();
if (getCurrentSite() === 'twitter') ensureTwitterTogglePlacement();
} else {
setTimeout(startObserver, 300);
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startObserver);
} else {
startObserver();
}
window.addEventListener('scroll', processAllContent, { passive: true });
}
init();
})();