XBlock

屏蔽微博、知乎、B站、X(Twitter)含关键词的内容,支持自定义管理。内容默认折叠,点击可查看原内容,查看后可收起。支持导入/导出配置。

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

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

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

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

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

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @name         XBlock
// @namespace    http://tampermonkey.net/
// @version      2.6.4
// @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,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
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 1. 通用默认关键词
    const DEFAULT_KEYWORDS_COMMON = [
        'coser','COSER','漫展','JK'
    ];

    const DEFAULT_KEYWORDS_X = [
        'sao的很','sao货','太涩了','30+'
    ];

    const STORAGE_KEY = 'keyword_blocker_words';
    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';

    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 getKeywordMaskBarVisible(keyword) {
        if (Object.prototype.hasOwnProperty.call(KEYWORD_MASK_VISIBILITY, keyword)) {
            return !!KEYWORD_MASK_VISIBILITY[keyword];
        }
        return !isMasterMaskSwitchOn();
    }

    function setKeywordMaskBarVisible(keyword, visible) {
        KEYWORD_MASK_VISIBILITY[keyword] = !!visible;
        saveKeywordMaskVisibility(KEYWORD_MASK_VISIBILITY);
    }

    function removeKeywordMaskBarVisible(keyword) {
        if (Object.prototype.hasOwnProperty.call(KEYWORD_MASK_VISIBILITY, keyword)) {
            delete KEYWORD_MASK_VISIBILITY[keyword];
            saveKeywordMaskVisibility(KEYWORD_MASK_VISIBILITY);
        }
    }

    let PENDING_MASTER_MASK_SWITCH = null;
    let PENDING_KEYWORD_MASK_VISIBILITY = null;

    function getKeywordMaskBarVisibleForUI(keyword) {
        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, keyword)) {
            return !!visibilityMap[keyword];
        }
        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 loadKeywords() {
        let keywords = [];
        try {
            const saved = localStorage.getItem(STORAGE_KEY);
            if (saved) {
                keywords = JSON.parse(saved);
            } else {
                keywords = [...DEFAULT_KEYWORDS_COMMON];
            }

            if (getCurrentSite() === 'twitter') {
                const isOldData = !keywords.includes('30+');
                if (isOldData) {
                    const newXWords = DEFAULT_KEYWORDS_X.filter(word => !keywords.includes(word));
                    keywords.unshift(...newXWords);
                    saveKeywords(keywords);
                }
            }
            return keywords;

        } catch (e) {
            console.error('加载屏蔽词失败:', e);
            return [...DEFAULT_KEYWORDS_COMMON, ...DEFAULT_KEYWORDS_X];
        }
    }

    function saveKeywords(keywords) {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(keywords));
    }

    // 禁用网站管理
    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_KEYWORDS = loadKeywords();

    // 网站特定的配置
    const siteConfigs = {
        zhihu: {
            containerSelector: '.ContentItem',
            titleSelector: '.ContentItem-title a',
            logPrefix: '知乎'
        },
        xiaohongshu: {
            containerSelector: 'section.note-item',
            titleSelector: 'a.title, .title',
            logPrefix: '小红书'
        },
        bilibili: {
            containerSelector: '.bili-feed-card, .bili-video-card',
            titleSelector: '.bili-video-card__info--tit, .bili-video-card__info--tit a',
            logPrefix: 'B站'
        },
        weibo: {
            containerSelector: '.wbpro-scroller-item',
            titleSelector: '.wbpro-feed-content .detail_wbtext_4CRf9',
            logPrefix: '微博'
        },
        twitter: {
            containerSelector: 'div[data-testid="cellInnerDiv"]',
            titleSelector: 'div[data-testid="tweetText"]',
            logPrefix: 'X(Twitter)'
        }
    };

    // --- UI 创建部分 ---
    function createManagementUI() {
        const style = document.createElement('style');
        style.textContent = `
            /* 非 Twitter 的浮动按钮样式隔离类名 */
            .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; left: -350px; top: 50%; transform: translateY(-50%); z-index: 9999; width: 320px; max-height: 80vh; background: white; border: 1px solid #d9d9d9; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.15); transition: left 0.3s ease; font-family: sans-serif; display: flex; flex-direction: column; }
            #keyword-blocker-panel.show { left: 20px; }

            .kb-panel-header { padding: 16px 40px 10px 16px; border-bottom: 1px solid #f0f0f0; background: #fafafa; border-radius: 8px 8px 0 0; flex-shrink: 0; }
            .kb-title-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
            .kb-panel-title { margin: 0; font-size: 16px; font-weight: 500; color: #262626; flex: 1; }
            .kb-input-group { display: flex; gap: 8px; }
            .kb-input { flex: 1; padding: 8px 12px; border: 1px solid #d9d9d9; border-radius: 4px; font-size: 14px; outline: none; }
            .kb-input:focus { border-color: #1890ff; }
            .kb-btn { padding: 8px 16px; background: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; flex-shrink: 0; }
            .kb-list-container { flex: 1; overflow-y: auto; min-height: 0; }
            .kb-list { list-style: none; margin: 0; padding: 0; }
            .kb-list-item { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid #f0f0f0; }
            .kb-keyword { flex: 1; font-size: 14px; color: #262626; word-break: break-all; margin-right: 8px; }
            .kb-action-group { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
            .kb-visibility-btn { padding: 4px 8px; background: #f5f5f5; color: #666; border: 1px solid #d9d9d9; border-radius: 3px; cursor: pointer; font-size: 12px; flex-shrink: 0; transition: all 0.2s; }
            .kb-visibility-btn.active { background: #e6f4ff; color: #1890ff; border-color: #91caff; }
            .kb-delete-btn { padding: 4px 8px; background: #ff4d4f; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px; flex-shrink: 0; }
            .kb-stats { padding: 12px 16px; background: #f9f9f9; border-top: 1px solid #f0f0f0; font-size: 12px; color: #666; text-align: center; border-radius: 0 0 8px 8px; flex-shrink: 0; }
            .kb-close-btn { position: absolute; top: 10px; right: 10px; background: none; border: none; font-size: 18px; cursor: pointer; color: #999; }
            .kb-disable-site-btn { padding: 4px 8px; background: #8c8c8c; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 10px; margin-left: 12px; }
            .kb-confirm-group { display: flex; gap: 8px; }
            .kb-confirm-btn { padding: 4px 8px; border: none; border-radius: 3px; cursor: pointer; font-size: 12px; color: white;}
            .kb-confirm-delete { background: #ff4d4f; }
            .kb-confirm-cancel { background: #8c8c8c; }

            /* 数据管理行样式 */
            .kb-data-row { display: flex; justify-content: flex-end; gap: 10px; margin-top: 10px; }
            .kb-data-btn { padding: 4px 10px; background: #fff; border: 1px solid #d9d9d9; border-radius: 4px; cursor: pointer; font-size: 12px; color: #666; transition: all 0.2s; }
            .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: 10px; }
            .kb-setting-label { font-size: 12px; color: #666; }
            .kb-setting-btn { padding: 4px 10px; background: #fff; border: 1px solid #d9d9d9; border-radius: 4px; cursor: pointer; font-size: 12px; color: #666; transition: all 0.2s; }
            .kb-setting-btn:hover { color: #1890ff; border-color: #1890ff; }
            .kb-setting-btn.active { color: #1890ff; border-color: #1890ff; background: #e6f4ff; }
            .kb-save-btn { padding: 5px 12px; background: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; }
            .kb-save-btn:hover { background: #40a9ff; }

            /* --- 遮罩层与收起按钮样式 --- */
            .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);

        // 为了避免 Twitter 元素继承了浮动按钮的 CSS 导致样式错乱,我们给予它独立的 ID
        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';

        const siteNames = {'zhihu':'知乎','xiaohongshu':'小红书','bilibili':'B站','weibo':'微博','twitter':'X / Twitter'};
        const siteName = siteNames[getCurrentSite()] || '当前网站';
        const isDisabled = isCurrentSiteDisabled();
        const masterMaskSwitchOn = isMasterMaskSwitchOn();

        panel.innerHTML = `
            <button class="kb-close-btn" id="kb-close">×</button>
            <div class="kb-panel-header">
                <div class="kb-title-row">
                    <h3 class="kb-panel-title">${isDisabled ? '⚠️ 屏蔽已暂停' : '屏蔽词管理'}</h3>
                    <button class="kb-disable-site-btn" id="kb-disable-site">${isDisabled ? '启用屏蔽' : '暂停屏蔽'}</button>
                </div>
                <div class="kb-input-group">
                    <input type="text" id="kb-input" class="kb-input" placeholder="添加关键词..." />
                    <button id="kb-add-btn" class="kb-btn">新增</button>
                </div>
                <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">
                    <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>
            <div class="kb-list-container"><ul id="kb-list" class="kb-list"></ul></div>
            <div class="kb-stats">当前 ${siteName} 共有 <span id="kb-count">0</span> 个屏蔽词</div>
        `;
        document.body.appendChild(panel);
        return { toggleBtn, panel };
    }

    // X (Twitter) 原生导航项克隆对齐逻辑,完全隔离非Twitter的CSS,保证继承100%原生样式
    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) {
                // 原汁原味克隆节点,这样就不会丢掉任何原生 flex 排版和边距类名了
                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 = '屏蔽词';
                
                // 替换 SVG,恢复为你 2.6.0 原本的那个未加修饰的图标代码
                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(); // 关键修正:阻止事件冒泡到全局,避免瞬间被 document.onclick 再次关掉
                    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 renderKeywordList() {
        const list = document.getElementById('kb-list');
        const count = document.getElementById('kb-count');
        if (!list) return;

        list.innerHTML = '';
        count.textContent = BLOCK_KEYWORDS.length;

        BLOCK_KEYWORDS.forEach((keyword, index) => {
            const li = document.createElement('li');
            li.className = 'kb-list-item';
            li.dataset.index = index;
            const isVisible = getKeywordMaskBarVisibleForUI(keyword);
            li.innerHTML = `<span class="kb-keyword">${keyword}</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);
        });
    }

    // UI 事件绑定
    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 list = document.getElementById('kb-list');
        const disableSiteBtn = document.getElementById('kb-disable-site');

        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');

        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) => {
            // 每次点击都需要重新获取当前 DOM 里的按钮(防止 Twitter 下因重绘导致的旧节点失效)
            const currentToggleBtn = document.getElementById(toggleBtnId);
            if (panel.classList.contains('show') && !panel.contains(e.target) && (!currentToggleBtn || !currentToggleBtn.contains(e.target))) {
                toggle();
            }
        };

        addBtn.onclick = (e) => {
            e.stopPropagation();
            const val = input.value.trim();
            if(!val) return;
            const words = val.split(/[,,/]/).map(w => w.trim()).filter(w => w && !BLOCK_KEYWORDS.includes(w));
            if(words.length) {
                BLOCK_KEYWORDS.unshift(...words);
                saveKeywords(BLOCK_KEYWORDS);
                renderKeywordList();
                input.value = '';
            }
        };

        input.onkeypress = (e) => { if(e.key === 'Enter') addBtn.click(); };

        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();
            };
        }

        list.onclick = (e) => {
            e.stopPropagation();
            const idx = parseInt(e.target.dataset.index);
            if (Number.isNaN(idx)) return;
            const keyword = BLOCK_KEYWORDS[idx];

            if (e.target.classList.contains('kb-visibility-btn')) {
                const nextVisible = !getKeywordMaskBarVisibleForUI(keyword);
                PENDING_KEYWORD_MASK_VISIBILITY[keyword] = nextVisible;
                renderKeywordList();
            } else if (e.target.classList.contains('kb-delete-btn')) {
                const li = e.target.closest('li');
                li.innerHTML = `<span class="kb-keyword">${keyword}</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')) {
                delete PENDING_KEYWORD_MASK_VISIBILITY[keyword];
                removeKeywordMaskBarVisible(keyword);
                BLOCK_KEYWORDS.splice(idx, 1);
                saveKeywords(BLOCK_KEYWORDS);
                renderKeywordList();
            } else if (e.target.classList.contains('kb-confirm-cancel')) {
                renderKeywordList();
            }
        };

        disableSiteBtn.onclick = (e) => {
            e.stopPropagation();
            if(isCurrentSiteDisabled()) {
                enableCurrentSite();
                alert('已启用屏蔽,请刷新生效');
            } else {
                disableCurrentSite();
                alert('已暂停当前网站屏蔽,请刷新页面生效');
            }
            toggle();
        };

        exportBtn.onclick = (e) => {
            e.stopPropagation();
            try {
                const data = {
                    keywords: BLOCK_KEYWORDS,
                    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 importedKeywords = [];

                    if (Array.isArray(parsed)) {
                        importedKeywords = parsed;
                    } else if (parsed && Array.isArray(parsed.keywords)) {
                        importedKeywords = parsed.keywords;
                        if (typeof parsed.maskBarMasterSwitch === 'boolean') {
                            saveMasterMaskSwitch(parsed.maskBarMasterSwitch);
                        }
                        if (parsed.keywordMaskVisibility && typeof parsed.keywordMaskVisibility === 'object' && !Array.isArray(parsed.keywordMaskVisibility)) {
                            KEYWORD_MASK_VISIBILITY = { ...KEYWORD_MASK_VISIBILITY, ...parsed.keywordMaskVisibility };
                            PENDING_KEYWORD_MASK_VISIBILITY = { ...KEYWORD_MASK_VISIBILITY };
                            saveKeywordMaskVisibility(KEYWORD_MASK_VISIBILITY);
                        }
                    } else {
                        alert('导入失败:JSON 格式不正确');
                        return;
                    }

                    const oldLength = BLOCK_KEYWORDS.length;
                    BLOCK_KEYWORDS = [...new Set([...BLOCK_KEYWORDS, ...importedKeywords])];
                    saveKeywords(BLOCK_KEYWORDS);
                    renderKeywordList();

                    const addedCount = BLOCK_KEYWORDS.length - oldLength;
                    alert(`导入成功:新增 ${addedCount} 个关键词,当前共 ${BLOCK_KEYWORDS.length} 个关键词`);
                    location.reload();
                } catch (err) {
                    console.error('导入失败:', err);
                    alert('导入失败:请确保是正确的 JSON 文件');
                }
                importInput.value = '';
            };
            reader.readAsText(file);
        };
    }

    // --- 核心屏蔽逻辑 ---

    function createMaskElement(keyword, onClick) {
        const mask = document.createElement('div');
        mask.className = 'kb-mask-overlay';
        mask.innerHTML = `<span>🙈 已折叠包含 <span class="kb-mask-keyword">${keyword}</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 processContentElement(element, config) {
        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';
                return;
            }
        }

        const titleEl = element.querySelector(config.titleSelector);
        const text = titleEl ? titleEl.textContent : element.textContent;
        const matchedKeyword = BLOCK_KEYWORDS.find(k => text.includes(k));

        if (!matchedKeyword) return;

        element.setAttribute('data-kb-status', 'processed');

        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(matchedKeyword);
        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(matchedKeyword, 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 debounce(func, wait) {
        let timeout;
        return function(...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func(...args), wait);
        };
    }

    const debouncedProcess = debounce(processAllContent, 500);

    // 主程序
    function init() {
        if (isCurrentSiteDisabled()) {
            console.log(`屏蔽器: ${getCurrentSite()} 已禁用`);
            if (document.body) createManagementUI();
            else window.addEventListener('DOMContentLoaded', createManagementUI);
            if(document.body) initUIEvents();
            else window.addEventListener('load', initUIEvents);
            return;
        }

        console.log(`屏蔽器已启动: ${getCurrentSite()}`);

        setInterval(() => {
            processAllContent();
            if (getCurrentSite() === 'twitter') ensureTwitterTogglePlacement();
        }, 2000);

        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;
            }
            if(shouldRun) {
                debouncedProcess();
                if (getCurrentSite() === 'twitter') ensureTwitterTogglePlacement();
            }
        });

        const startObserver = () => {
            if (document.body) {
                observer.observe(document.body, observerConfig);
                createManagementUI();
                renderKeywordList();
                initUIEvents();
                processAllContent();
                if (getCurrentSite() === 'twitter') ensureTwitterTogglePlacement();
            } else {
                setTimeout(startObserver, 1000);
            }
        };

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

        window.addEventListener('scroll', () => {
             setTimeout(debouncedProcess, 300);
        }, { passive: true });
    }

    init();
})();