XBlock

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

Advertisement:

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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