XBlock

屏蔽微博、知乎、B站、X(Twitter)含关键词或正则的内容。修复了暂停屏蔽后面板/按钮消失无法恢复的问题。包含极致精简规则库与规则指南。

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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, "&lt;").replace(/>/g, "&gt;");
            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, "&lt;").replace(/>/g, "&gt;");
                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, "&lt;").replace(/>/g, "&gt;");
        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();
})();