Greasy Fork

来自缓存

Greasy Fork is available in English.

网页元素屏蔽器

集成原生CSS极速注入、Shadow DOM隔离、DOM结构拦截、广告域封杀与正则文本拦截。新增免代码的「积木组合模式」,支持复杂逻辑条件过滤。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         网页元素屏蔽器
// @namespace    http://tampermonkey.net/
// @version      0.6
// @description  集成原生CSS极速注入、Shadow DOM隔离、DOM结构拦截、广告域封杀与正则文本拦截。新增免代码的「积木组合模式」,支持复杂逻辑条件过滤。
// @author       JerryChiang
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    /**
     * 工具函数:防抖,用于优化高频触发事件
     */
    function debounce(func, wait) {
        let timeout;
        return function (...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    }

    /**
     * 核心数据与配置管理模块
     */
    class StorageManager {
        constructor() {
            this.domain = window.location.hostname;
            this.flashList = GM_getValue('pro_blocker_flash_domains', {});
        }

        getData() {
            return {
                static: GM_getValue('blocks', {})[this.domain] || [],
                dynamic: GM_getValue('dynamicBlocks', {})[this.domain] || [],
                regex: GM_getValue('regexBlocks', {})[this.domain] || [],
                attribute: GM_getValue('attrBlocks', {})[this.domain] || [],
                structural: GM_getValue('structBlocks', {})[this.domain] || [],
                complex: GM_getValue('complexBlocks', {})[this.domain] || [], // 新增:积木组合模式数据
                config: GM_getValue('config', {})[this.domain] || { mode: 'auto' }
            };
        }

        saveData(type, rules) {
            const keyMap = { 
                'static': 'blocks', 
                'dynamic': 'dynamicBlocks', 
                'regex': 'regexBlocks', 
                'attribute': 'attrBlocks', 
                'structural': 'structBlocks',
                'complex': 'complexBlocks'
            };
            const key = keyMap[type];
            const allData = GM_getValue(key, {});
            
            if (rules.length === 0) {
                delete allData[this.domain];
            } else {
                allData[this.domain] = rules;
            }
            
            GM_setValue(key, allData);
            
            if (type !== 'regex' && type !== 'complex') {
                BlockEngine.applyCSSRules(); 
            }
        }

        addRule(type, rule) {
            const data = this.getData()[type];
            const isDuplicate = data.some(item => 
                (type === 'regex' && item.regex === rule.regex) ||
                (type === 'static' && item.selector === rule.selector) ||
                (type === 'dynamic' && item.className === rule.className) ||
                (type === 'attribute' && item.attrSelector === rule.attrSelector) ||
                (type === 'structural' && item.structSelector === rule.structSelector) ||
                (type === 'complex' && JSON.stringify(item.conditions) === JSON.stringify(rule.conditions))
            );
            
            if (!isDuplicate) {
                data.push(rule);
                this.saveData(type, data);
            }
        }

        removeRule(type, index) {
            const data = this.getData()[type];
            if (data[index]) {
                data.splice(index, 1);
                this.saveData(type, data);
            }
        }
        
        clearDomain() {
            ['blocks', 'dynamicBlocks', 'regexBlocks', 'attrBlocks', 'structBlocks', 'complexBlocks', 'config'].forEach(key => {
                const data = GM_getValue(key, {});
                delete data[this.domain];
                GM_setValue(key, data);
            });
            if (this.flashList[this.domain]) {
                delete this.flashList[this.domain];
                GM_setValue('pro_blocker_flash_domains', this.flashList);
            }
        }

        markAsFlashing() {
            if (!this.flashList[this.domain]) {
                this.flashList[this.domain] = true;
                GM_setValue('pro_blocker_flash_domains', this.flashList);
                console.info(`[Pro Blocker] 检测到广告闪现,已将 ${this.domain} 加入极速预判名单。`);
            }
        }

        toggleMode() {
            const currentMode = this.getData().config.mode;
            const nextMode = currentMode === 'auto' ? 'preemptive' : 'auto';
            const allConfig = GM_getValue('config', {});
            allConfig[this.domain] = { mode: nextMode };
            GM_setValue('config', allConfig);
            return nextMode;
        }
    }

    const storage = new StorageManager();

    /**
     * 拦截引擎:负责底层 DOM 与 CSS 控制
     */
    class BlockEngine {
        static styleElementId = 'pro-blocker-core-css';

        static fastInject() {
            const data = storage.getData();
            const isFlashingDomain = storage.flashList[storage.domain];
            const isPreemptive = data.config.mode === 'preemptive';

            if (isFlashingDomain || isPreemptive) {
                this.applyCSSRules();
            }
        }

        static applyCSSRules() {
            const data = storage.getData();
            let cssText = '';
            
            const hideCSS = '{ display: none !important; opacity: 0 !important; visibility: hidden !important; pointer-events: none !important; z-index: -2147483648 !important; height: 0 !important; width: 0 !important; position: absolute !important; }\n';

            data.static.forEach(r => r.selector && (cssText += `${r.selector} ${hideCSS}`));
            data.dynamic.forEach(r => r.className && (cssText += `.${r.className} ${hideCSS}`));
            data.attribute.forEach(r => r.attrSelector && (cssText += `${r.attrSelector} ${hideCSS}`));
            data.structural.forEach(r => r.structSelector && (cssText += `${r.structSelector} ${hideCSS}`));

            if (!cssText) return;

            let styleEl = document.getElementById(this.styleElementId);
            if (!styleEl) {
                styleEl = document.createElement('style');
                styleEl.id = this.styleElementId;
                const target = document.head || document.documentElement;
                target.appendChild(styleEl);
            }
            styleEl.textContent = cssText;
        }

        static applyRegexRules(targetNode = document.body) {
            const data = storage.getData();
            if (!data.regex || data.regex.length === 0 || !targetNode) return;

            data.regex.forEach(rule => {
                try {
                    const regex = new RegExp(rule.regex);
                    const walker = document.createTreeWalker(targetNode, NodeFilter.SHOW_TEXT, null, false);
                    let node;
                    while ((node = walker.nextNode())) {
                        if (regex.test(node.textContent)) {
                            let element = node.parentElement;
                            for (let i = 0; i < rule.level; i++) {
                                if (element.parentElement && element.parentElement !== document.body) {
                                    element = element.parentElement;
                                } else break;
                            }
                            if (element && element.style.display !== 'none') {
                                element.style.setProperty('display', 'none', 'important');
                            }
                        }
                    }
                } catch (e) {
                    console.error('[Pro Blocker] 正则解析异常:', e);
                }
            });
        }

        // 新增:解析并应用积木组合模式规则
        static applyComplexRules(targetNode = document.body) {
            const data = storage.getData();
            if (!data.complex || data.complex.length === 0 || !targetNode) return;

            const root = targetNode.nodeType === Node.ELEMENT_NODE ? targetNode : targetNode.parentElement;
            if (!root) return;

            data.complex.forEach(rule => {
                try {
                    // 性能优化:尝试提取前置基础选择器,避免全局扫描
                    let baseSelector = '*';
                    if (rule.logic === 'AND') {
                        let parts = [];
                        rule.conditions.forEach(c => {
                            if (c.type === 'class' && c.operator === 'contains' && /^[a-zA-Z0-9\-_]+$/.test(c.value)) parts.push(`.${c.value}`);
                            if (c.type === 'id' && c.operator === 'equals' && /^[a-zA-Z0-9\-_]+$/.test(c.value)) parts.push(`#${c.value}`);
                        });
                        if (parts.length > 0) baseSelector = parts.join('');
                    }

                    // 限制扫描的标签类型,排除脚本、样式及大范围的 body
                    const elements = baseSelector === '*' 
                        ? root.querySelectorAll('div, span, a, p, img, li, ul, iframe, section, article, aside') 
                        : root.querySelectorAll(baseSelector);

                    elements.forEach(el => {
                        // 兜底机制:跳过文本量过大的结构层容器,防止误杀全站
                        if (baseSelector === '*' && el.textContent.length > 3000) return; 

                        const results = rule.conditions.map(c => {
                            let val = '';
                            if (c.type === 'text') val = el.textContent || '';
                            else if (c.type === 'class') val = typeof el.className === 'string' ? el.className : '';
                            else if (c.type === 'id') val = el.id || '';

                            if (c.operator === 'contains') return val.includes(c.value);
                            if (c.operator === 'not_contains') return val !== '' && !val.includes(c.value);
                            if (c.operator === 'equals') return val.trim() === c.value.trim();
                            return false;
                        });

                        const isMatch = rule.logic === 'AND' ? results.every(r => r) : results.some(r => r);

                        if (isMatch) {
                            let target = el;
                            for (let i = 0; i < rule.level; i++) {
                                if (target.parentElement && target.parentElement !== document.body && target.parentElement !== document.documentElement) {
                                    target = target.parentElement;
                                } else break;
                            }
                            if (target.style.display !== 'none') {
                                target.style.setProperty('display', 'none', 'important');
                                target.style.setProperty('opacity', '0', 'important');
                            }
                        }
                    });
                } catch(e) {
                    console.error('[Pro Blocker] 积木规则执行错误:', e);
                }
            });
        }

        static startObserver() {
            // 合并文本规则与积木规则的防抖更新
            const debouncedDynamicApply = debounce(() => {
                this.applyRegexRules();
                this.applyComplexRules();
            }, 300);
            
            const observer = new MutationObserver((mutations) => {
                let shouldCheck = false;
                for (let mutation of mutations) {
                    if (mutation.addedNodes.length > 0) {
                        shouldCheck = true;
                        
                        const styleEl = document.getElementById(this.styleElementId);
                        if (styleEl && document.body && styleEl.nextSibling) {
                            document.body.appendChild(styleEl);
                            storage.markAsFlashing(); 
                        }
                        break;
                    }
                }
                if (shouldCheck) debouncedDynamicApply();
            });

            window.addEventListener('DOMContentLoaded', () => {
                this.applyCSSRules();
                this.applyRegexRules();
                this.applyComplexRules();
                observer.observe(document.body || document.documentElement, { childList: true, subtree: true });
            });
        }

        static generateOptimalSelector(element) {
            if (element.id && !/^\d/.test(element.id) && !/[a-zA-Z0-9]{8,}/.test(element.id)) return `#${element.id}`;
            let path = [];
            while (element && element.nodeType === Node.ELEMENT_NODE && element.tagName.toLowerCase() !== 'body' && element.tagName.toLowerCase() !== 'html') {
                let selector = element.tagName.toLowerCase();
                if (element.className && typeof element.className === 'string') {
                    const classes = element.className.trim().split(/\s+/).filter(c => /^[a-zA-Z\-_]+$/.test(c) && !/[a-zA-Z0-9]{10,}/.test(c));
                    if (classes.length > 0) selector += '.' + classes.join('.');
                }
                let sibling = element, nth = 1;
                while (sibling = sibling.previousElementSibling) {
                    if (sibling.tagName.toLowerCase() === element.tagName.toLowerCase()) nth++;
                }
                if (nth > 1) selector += `:nth-of-type(${nth})`;
                path.unshift(selector);
                element = element.parentElement;
            }
            return path.join(' > ');
        }

        static generateStructuralSelector(element) {
            let path = [];
            let current = element;
            while (current && current.nodeType === Node.ELEMENT_NODE && current.tagName.toLowerCase() !== 'html') {
                let tagName = current.tagName.toLowerCase();
                if (tagName === 'body') {
                    path.unshift('body');
                    break;
                }
                if (current.id && !/^\d/.test(current.id) && !/[a-zA-Z0-9]{8,}/.test(current.id)) {
                    path.unshift(`#${current.id}`);
                    break; 
                }
                let nth = 1, sibling = current.previousElementSibling;
                while (sibling) { nth++; sibling = sibling.previousElementSibling; }
                path.unshift(`${tagName}:nth-child(${nth})`);
                current = current.parentElement;
            }
            return path.join(' > ');
        }

        static extractResourceDomain(element) {
            let urls = [];
            if (element.src) urls.push(element.src);
            if (element.href) urls.push(element.href);
            const mediaChild = element.querySelector('img, iframe, video, a');
            if (mediaChild) {
                if (mediaChild.src) urls.push(mediaChild.src);
                if (mediaChild.href) urls.push(mediaChild.href);
            }
            for (let url of urls) {
                try {
                    const urlObj = new URL(url);
                    if (urlObj.hostname && !urlObj.hostname.includes(window.location.hostname) && !url.startsWith('data:')) {
                        return { full: url, domain: urlObj.hostname };
                    }
                } catch(e) {}
            }
            return null;
        }
    }

    /**
     * 用户交互界面:基于 Shadow DOM 隔离
     */
    class UIManager {
        constructor() {
            const existingHost = document.getElementById('pro-blocker-ui-host');
            if (existingHost) existingHost.remove();

            this.shadowHost = document.createElement('div');
            this.shadowHost.id = 'pro-blocker-ui-host';
            this.shadowHost.style.cssText = 'position: fixed; z-index: 2147483647; top: 0; left: 0; width: 0; height: 0; overflow: visible;';
            this.shadowRoot = this.shadowHost.attachShadow({ mode: 'closed' });
            this.injectStyles();
            document.documentElement.appendChild(this.shadowHost);
            
            this.highlightEl = null;
            this._previewAffectedElements = []; // 新增:用于存储规则预览时受影响的元素状态
            this._handleMouseOver = this._handleMouseOver.bind(this);
            this._handleClick = this._handleClick.bind(this);
        }

        injectStyles() {
            const style = document.createElement('style');
            style.textContent = `
                :host { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
                .panel {
                    position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
                    background: rgba(255, 255, 255, 0.98); backdrop-filter: blur(12px);
                    border: 1px solid rgba(0,0,0,0.1); padding: 24px; border-radius: 12px;
                    box-shadow: 0 12px 48px rgba(0,0,0,0.25); width: 480px; max-height: 85vh; overflow-y: auto; color: #333;
                }
                h3 { margin-top: 0; font-size: 18px; font-weight: 600; color: #111; margin-bottom: 16px; border-bottom: 1px solid #eee; padding-bottom: 8px;}
                p { font-size: 13px; margin: 0 0 12px 0; color: #555; line-height: 1.5; word-break: break-all; }
                .code-box { background: #f4f4f4; border: 1px solid #ddd; padding: 6px; border-radius: 4px; font-family: monospace; font-size: 11px; margin-top: 4px; display: block;}
                .btn-group { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
                button {
                    padding: 8px 12px; border: none; border-radius: 6px; cursor: pointer;
                    font-size: 13px; font-weight: 500; transition: all 0.2s; flex: 1;
                    display: flex; align-items: center; justify-content: center;
                }
                button:hover { filter: brightness(0.9); transform: translateY(-1px); }
                button:active { transform: translateY(0); }
                .btn-primary { background: #007AFF; color: #fff; }
                .btn-success { background: #34C759; color: #fff; }
                .btn-danger { background: #FF3B30; color: #fff; }
                .btn-warning { background: #FF9500; color: #fff; }
                .btn-dark { background: #212529; color: #fff; }
                .btn-info { background: #17a2b8; color: #fff; }
                .btn-outline { background: transparent; border: 1px solid #ccc; color: #333; }
                
                label { font-size: 13px; font-weight: 600; display: block; margin-bottom: 6px; color: #444; }
                input[type="text"], input[type="number"], select {
                    width: 100%; padding: 10px; margin-bottom: 15px; border: 1px solid #ddd; 
                    border-radius: 6px; box-sizing: border-box; outline: none; font-size: 14px;
                    transition: border-color 0.2s;
                }
                input[type="text"]:focus, input[type="number"]:focus, select:focus { border-color: #007AFF; }
                
                .rule-list { list-style: none; padding: 0; margin: 0; }
                .rule-item {
                    display: flex; justify-content: space-between; align-items: center;
                    padding: 10px; border-bottom: 1px solid #f0f0f0; background: #fafafa;
                    border-radius: 6px; margin-bottom: 8px; font-size: 12px; word-break: break-all;
                }
                .rule-content { flex: 1; padding-right: 10px; }
                .tag { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: #eee; margin-right: 6px; font-weight: bold; }
                .tag.attr { background: #E1BEE7; color: #4A148C; }
                .tag.struct { background: #FFE082; color: #FF6F00; }
                .tag.complex { background: #E3F2FD; color: #1565C0; }
                .status-bar { padding: 10px; background: #f8f9fa; border-radius: 6px; margin-bottom: 15px; font-size: 12px; border: 1px solid #e9ecef; }
            `;
            this.shadowRoot.appendChild(style);
        }

        injectHighlightStyle() {
            let style = document.getElementById('pro-blocker-highlight-style');
            if (!style) {
                style = document.createElement('style');
                style.id = 'pro-blocker-highlight-style';
                style.textContent = `
                    .pro-blocker-highlight {
                        outline: 3px solid #FF3B30 !important; outline-offset: -3px !important;
                        background-color: rgba(255, 59, 48, 0.15) !important; cursor: crosshair !important;
                        transition: outline 0.1s ease-in-out !important; box-shadow: 0 0 10px rgba(255,59,48,0.5) !important;
                    }
                `;
                document.head.appendChild(style);
            }
        }

        makeDraggable(panel) {
            const header = panel.querySelector('h3');
            if (!header) return;

            header.style.cursor = 'grab';
            header.style.userSelect = 'none';

            let isDragging = false;
            let startX, startY, initialLeft, initialTop;

            const onMouseDown = (e) => {
                if (e.target !== header && !header.contains(e.target)) return;
                isDragging = true;
                header.style.cursor = 'grabbing';
                const rect = panel.getBoundingClientRect();
                panel.style.transform = 'none';
                panel.style.left = rect.left + 'px';
                panel.style.top = rect.top + 'px';
                startX = e.clientX;
                startY = e.clientY;
                initialLeft = rect.left;
                initialTop = rect.top;
                e.preventDefault();
            };

            const onMouseMove = (e) => {
                if (!isDragging) return;
                const dx = e.clientX - startX;
                const dy = e.clientY - startY;
                panel.style.left = `${initialLeft + dx}px`;
                panel.style.top = `${initialTop + dy}px`;
            };

            const onMouseUp = () => {
                if (isDragging) {
                    isDragging = false;
                    header.style.cursor = 'grab';
                }
            };

            header.addEventListener('mousedown', onMouseDown);
            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);

            panel._cleanupDrag = () => {
                document.removeEventListener('mousemove', onMouseMove);
                document.removeEventListener('mouseup', onMouseUp);
            };
        }

        startSelection() {
            this.injectHighlightStyle();
            document.body.addEventListener('mouseover', this._handleMouseOver, true);
            document.body.addEventListener('click', this._handleClick, true);
            document.body.addEventListener('contextmenu', (e) => { 
                e.preventDefault(); 
                this.stopSelection(); 
            }, { once: true });
        }

        stopSelection() {
            document.body.removeEventListener('mouseover', this._handleMouseOver, true);
            document.body.removeEventListener('click', this._handleClick, true);
            if (this.highlightEl) {
                this.highlightEl.classList.remove('pro-blocker-highlight');
                this.highlightEl = null;
            }
        }

        _handleMouseOver(e) {
            if (e.target.closest('#pro-blocker-ui-host')) return;
            if (this.highlightEl) this.highlightEl.classList.remove('pro-blocker-highlight');
            this.highlightEl = e.target;
            this.highlightEl.classList.add('pro-blocker-highlight');
        }

        _handleClick(e) {
            e.preventDefault(); e.stopPropagation();
            if (e.target.closest('#pro-blocker-ui-host')) return;
            this.stopSelection();
            this.showActionPanel(e.target);
        }

        showActionPanel(element) {
            this.clearPanel();
            const panel = document.createElement('div');
            panel.className = 'panel';
            
            const selector = BlockEngine.generateOptimalSelector(element);
            const structSelector = BlockEngine.generateStructuralSelector(element);
            const resourceInfo = BlockEngine.extractResourceDomain(element);
            const domainHtml = resourceInfo ? `<p style="color:#D32F2F; border-left: 3px solid #F44336; padding-left: 8px;"><strong>发现第三方资源域:</strong><br>${resourceInfo.domain}</p>` : '';

            panel.innerHTML = `
                <h3 title="按住可拖动窗口">确认屏蔽策略</h3>
                <p><strong>常规语义路径:</strong><br><span class="code-box">${selector}</span></p>
                <p><strong>物理结构路径:</strong><br><span class="code-box">${structSelector}</span></p>
                ${domainHtml}
                
                <div class="btn-group">
                    <button class="btn-primary" id="btn-static">静态路径拦截</button>
                    <button class="btn-success" id="btn-dynamic">动态类名拦截</button>
                </div>
                <div class="btn-group">
                    <button class="btn-dark" id="btn-struct" style="flex:100%;">🎯 按物理结构拦截 (无视ID/类名随机化)</button>
                </div>
                ${resourceInfo ? `
                <div class="btn-group">
                    <button class="btn-danger" id="btn-domain" style="flex:100%; font-weight:bold;">🔥 彻底封杀该广告来源域名 (推荐)</button>
                </div>` : ''}
                
                <div class="btn-group" style="margin-top: 15px; border-top: 1px solid #eee; padding-top: 15px;">
                    <button class="btn-warning" id="btn-preview">🔍 预览效果</button>
                    <button class="btn-outline" id="btn-cancel">取消配置</button>
                </div>
            `;

            this.makeDraggable(panel);
            this.shadowRoot.appendChild(panel);
            let isPreviewing = false;

            panel.querySelector('#btn-static').addEventListener('click', () => {
                storage.addRule('static', { selector: selector, type: 'static' });
                this.clearPanel();
            });

            panel.querySelector('#btn-dynamic').addEventListener('click', () => {
                const primaryClass = element.className.split(/\s+/)[0];
                if (!primaryClass) return alert('当前元素无有效类名,请选择其他拦截方式。');
                storage.addRule('dynamic', { className: primaryClass, type: 'dynamic' });
                this.clearPanel();
            });

            panel.querySelector('#btn-struct').addEventListener('click', () => {
                storage.addRule('structural', { structSelector: structSelector, type: 'structural' });
                this.clearPanel();
            });

            if (resourceInfo) {
                panel.querySelector('#btn-domain').addEventListener('click', () => {
                    const attrSel = `[src*="${resourceInfo.domain}"], [href*="${resourceInfo.domain}"], [data-src*="${resourceInfo.domain}"]`;
                    storage.addRule('attribute', { attrSelector: attrSel, type: 'attribute', domain: resourceInfo.domain });
                    this.clearPanel();
                });
            }

            panel.querySelector('#btn-preview').addEventListener('click', (e) => {
                isPreviewing = !isPreviewing;
                element.style.setProperty('display', isPreviewing ? 'none' : '', 'important');
                e.target.textContent = isPreviewing ? '👁 恢复显示' : '🔍 预览效果';
            });

            panel.querySelector('#btn-cancel').addEventListener('click', () => {
                if (isPreviewing) element.style.display = '';
                this.clearPanel();
            });
        }

        showRegexPanel() {
            this.clearPanel();
            const panel = document.createElement('div');
            panel.className = 'panel';
            
            panel.innerHTML = `
                <h3 title="按住可拖动窗口">添加拦截规则</h3>
                <p>通过组合条件或正则表达式,实现对复杂动态广告的精准拦截。</p>
                
                <label for="regex-mode">匹配模式</label>
                <select id="regex-mode">
                    <option value="contains">基础文本模式 (包含指定字符即隐藏)</option>
                    <option value="builder" selected>积木组合模式 (免代码,支持多条件逻辑运算) ✨</option>
                    <option value="regex">正则表达式模式 (适合高级用户)</option>
                </select>

                <!-- 基础/正则模式 UI -->
                <div id="standard-ui" style="display: none;">
                    <label for="regex-input">拦截内容</label>
                    <input type="text" id="regex-input" placeholder="输入要屏蔽的关键词或正则表达式片段..." />
                </div>

                <!-- 积木组合模式 UI -->
                <div id="builder-ui" style="background: #f8f9fa; padding: 12px; border-radius: 8px; border: 1px solid #e9ecef; margin-bottom: 15px;">
                    <label style="margin-bottom: 8px;">总体逻辑网关</label>
                    <select id="builder-logic" style="margin-bottom: 12px;">
                        <option value="AND">满足以下【全部】条件才拦截 (AND)</option>
                        <option value="OR">满足以下【任意】条件即拦截 (OR)</option>
                    </select>
                    
                    <label>详细拦截条件</label>
                    <div id="builder-conditions"></div>
                    <button id="btn-add-condition" class="btn-outline" style="width: 100%; margin-top: 8px; border-style: dashed; padding: 6px;">+ 添加新条件块</button>
                </div>

                <label for="regex-level">向上隐藏层级 (0为仅隐藏自身,1为隐藏直接父级节点)</label>
                <input type="number" id="regex-level" value="0" min="0" max="10" />

                <div class="btn-group" style="margin-top: 15px;">
                    <button class="btn-warning" id="btn-preview-regex">🔍 预览效果</button>
                    <button class="btn-primary" id="btn-save-regex">保存并应用</button>
                    <button class="btn-outline" id="btn-close-regex">取消</button>
                </div>
            `;

            this.makeDraggable(panel);
            this.shadowRoot.appendChild(panel);

            const modeSelect = panel.querySelector('#regex-mode');
            const standardUI = panel.querySelector('#standard-ui');
            const builderUI = panel.querySelector('#builder-ui');
            const conditionsContainer = panel.querySelector('#builder-conditions');

            // 模式切换联动
            modeSelect.addEventListener('change', (e) => {
                if (e.target.value === 'builder') {
                    standardUI.style.display = 'none';
                    builderUI.style.display = 'block';
                    if (conditionsContainer.children.length === 0) addConditionRow();
                } else {
                    standardUI.style.display = 'block';
                    builderUI.style.display = 'none';
                }
            });

            // 添加单个积木条件
            const addConditionRow = () => {
                const row = document.createElement('div');
                row.className = 'condition-row';
                row.style.cssText = 'display: flex; gap: 6px; margin-bottom: 8px; align-items: center;';
                row.innerHTML = `
                    <select class="cond-type" style="width: 35%; margin: 0; padding: 8px;">
                        <option value="text">元素文本</option>
                        <option value="class">类名(Class)</option>
                        <option value="id">标识符(ID)</option>
                    </select>
                    <select class="cond-op" style="width: 25%; margin: 0; padding: 8px;">
                        <option value="contains">包含</option>
                        <option value="equals">等于</option>
                        <option value="not_contains">不包含</option>
                    </select>
                    <input type="text" class="cond-val" style="flex: 1; margin: 0; padding: 8px;" placeholder="设定值..." />
                    <button class="btn-danger btn-remove-cond" style="flex: none; width: 32px; padding: 0; min-width: 32px; height: 35px;">✕</button>
                `;
                row.querySelector('.btn-remove-cond').addEventListener('click', () => {
                    row.remove();
                    if (conditionsContainer.children.length === 0) addConditionRow(); // 保证至少有一个条件
                });
                conditionsContainer.appendChild(row);
            };

            // 初始化呈现一行默认积木
            addConditionRow();
            panel.querySelector('#btn-add-condition').addEventListener('click', addConditionRow);

            let isPreviewing = false;

            const resetPreview = () => {
                if (isPreviewing) {
                    this._previewAffectedElements.forEach(item => {
                        if (item.el) {
                            item.el.style.display = item.origDisplay;
                            item.el.style.opacity = item.origOpacity;
                        }
                    });
                    this._previewAffectedElements = [];
                    isPreviewing = false;
                    const previewBtn = panel.querySelector('#btn-preview-regex');
                    if (previewBtn) previewBtn.textContent = '🔍 预览效果';
                }
            };

            // 监听面板内输入变化,自动取消已失效的预览状态
            panel.addEventListener('input', resetPreview);
            panel.addEventListener('change', resetPreview);

            panel.querySelector('#btn-preview-regex').addEventListener('click', (e) => {
                if (isPreviewing) {
                    resetPreview();
                    return;
                }

                const mode = modeSelect.value;
                const level = parseInt(panel.querySelector('#regex-level').value, 10);
                this._previewAffectedElements = [];

                if (mode === 'builder') {
                    const logic = panel.querySelector('#builder-logic').value;
                    const rows = conditionsContainer.querySelectorAll('.condition-row');
                    const conditions = [];
                    let isValueMissing = false;

                    rows.forEach(row => {
                        const type = row.querySelector('.cond-type').value;
                        const op = row.querySelector('.cond-op').value;
                        const val = row.querySelector('.cond-val').value.trim();
                        if (!val) isValueMissing = true;
                        conditions.push({ type, operator: op, value: val });
                    });

                    if (isValueMissing || conditions.length === 0) {
                        alert('规则校验失败:请完整填写所有积木条件的值再进行预览。');
                        return;
                    }

                    // 性能优化前置:尝试提取基础选择器
                    let baseSelector = '*';
                    if (logic === 'AND') {
                        let parts = [];
                        conditions.forEach(c => {
                            if (c.type === 'class' && c.operator === 'contains' && /^[a-zA-Z0-9\-_]+$/.test(c.value)) parts.push(`.${c.value}`);
                            if (c.type === 'id' && c.operator === 'equals' && /^[a-zA-Z0-9\-_]+$/.test(c.value)) parts.push(`#${c.value}`);
                        });
                        if (parts.length > 0) baseSelector = parts.join('');
                    }

                    const root = document.body;
                    const elements = baseSelector === '*' 
                        ? root.querySelectorAll('div, span, a, p, img, li, ul, iframe, section, article, aside') 
                        : root.querySelectorAll(baseSelector);

                    elements.forEach(el => {
                        if (baseSelector === '*' && el.textContent.length > 3000) return; 

                        const results = conditions.map(c => {
                            let val = '';
                            if (c.type === 'text') val = el.textContent || '';
                            else if (c.type === 'class') val = typeof el.className === 'string' ? el.className : '';
                            else if (c.type === 'id') val = el.id || '';

                            if (c.operator === 'contains') return val.includes(c.value);
                            if (c.operator === 'not_contains') return val !== '' && !val.includes(c.value);
                            if (c.operator === 'equals') return val.trim() === c.value.trim();
                            return false;
                        });

                        const isMatch = logic === 'AND' ? results.every(r => r) : results.some(r => r);

                        if (isMatch) {
                            let target = el;
                            for (let i = 0; i < level; i++) {
                                if (target.parentElement && target.parentElement !== document.body && target.parentElement !== document.documentElement) {
                                    target = target.parentElement;
                                } else break;
                            }
                            if (target.style.display !== 'none') {
                                this._previewAffectedElements.push({ el: target, origDisplay: target.style.display, origOpacity: target.style.opacity });
                                target.style.setProperty('display', 'none', 'important');
                                target.style.setProperty('opacity', '0', 'important');
                            }
                        }
                    });

                } else {
                    const text = panel.querySelector('#regex-input').value.trim();
                    if (!text) {
                        alert('规则校验失败:请输入有效的匹配内容再进行预览。');
                        return;
                    }
                    
                    let regexRule = text;
                    if (mode === 'contains') {
                        const escapedText = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                        regexRule = `.*${escapedText}.*`;
                    }

                    try {
                        const regex = new RegExp(regexRule);
                        const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
                        let node;
                        while ((node = walker.nextNode())) {
                            if (regex.test(node.textContent)) {
                                let target = node.parentElement;
                                for (let i = 0; i < level; i++) {
                                    if (target.parentElement && target.parentElement !== document.body) {
                                        target = target.parentElement;
                                    } else break;
                                }
                                if (target && target.style.display !== 'none') {
                                    this._previewAffectedElements.push({ el: target, origDisplay: target.style.display, origOpacity: target.style.opacity });
                                    target.style.setProperty('display', 'none', 'important');
                                    target.style.setProperty('opacity', '0', 'important');
                                }
                            }
                        }
                    } catch (err) {
                        alert('规则校验失败:正则表达式存在语法错误。');
                        return;
                    }
                }

                isPreviewing = true;
                e.target.textContent = '👁 恢复显示';
            });

            panel.querySelector('#btn-save-regex').addEventListener('click', () => {
                const mode = modeSelect.value;
                const level = parseInt(panel.querySelector('#regex-level').value, 10);

                if (mode === 'builder') {
                    const logic = panel.querySelector('#builder-logic').value;
                    const rows = conditionsContainer.querySelectorAll('.condition-row');
                    const conditions = [];
                    let isValueMissing = false;

                    rows.forEach(row => {
                        const type = row.querySelector('.cond-type').value;
                        const op = row.querySelector('.cond-op').value;
                        const val = row.querySelector('.cond-val').value.trim();
                        if (!val) isValueMissing = true;
                        conditions.push({ type, operator: op, value: val });
                    });

                    if (isValueMissing || conditions.length === 0) {
                        alert('校验失败:请完整填写所有积木条件的值。');
                        return;
                    }

                    storage.addRule('complex', { logic, conditions, level, type: 'complex' });
                    BlockEngine.applyComplexRules();
                    
                } else {
                    const text = panel.querySelector('#regex-input').value.trim();
                    if (!text) {
                        alert('校验失败:请输入有效的匹配内容。');
                        return;
                    }
                    
                    let regexRule = text;
                    if (mode === 'contains') {
                        const escapedText = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                        regexRule = `.*${escapedText}.*`;
                    }

                    storage.addRule('regex', { regex: regexRule, level: level, type: 'regex' });
                    BlockEngine.applyRegexRules();
                }

                this.clearPanel();
            });

            panel.querySelector('#btn-close-regex').addEventListener('click', () => this.clearPanel());
        }

        showManager() {
            this.clearPanel();
            const panel = document.createElement('div');
            panel.className = 'panel';
            const data = storage.getData();
            let rulesHTML = '<ul class="rule-list">';

            const renderItem = (type, content, index, tagClass = '') => `
                <li class="rule-item">
                    <div class="rule-content">
                        <span class="tag ${tagClass}">${type}</span> ${content}
                    </div>
                    <button class="btn-danger btn-delete" style="flex:none; width:60px; padding: 4px;" data-type="${type}" data-index="${index}">删除</button>
                </li>
            `;

            data.static.forEach((r, i) => rulesHTML += renderItem('静态', r.selector, i));
            data.dynamic.forEach((r, i) => rulesHTML += renderItem('动态', `类名: ${r.className}`, i));
            data.regex.forEach((r, i) => rulesHTML += renderItem('正则', `匹配: ${r.regex} (层级: ${r.level})`, i));
            data.attribute.forEach((r, i) => rulesHTML += renderItem('域封杀', `阻断: ${r.domain}`, i, 'attr'));
            data.structural.forEach((r, i) => rulesHTML += renderItem('位置', r.structSelector, i, 'struct'));
            
            // 渲染积木组合规则
            data.complex.forEach((r, i) => {
                const formatOp = (op) => op === 'contains' ? '包含' : (op === 'equals' ? '等于' : '不包含');
                const formatType = (t) => t === 'text' ? '文本' : (t === 'class' ? '类名' : 'ID');
                const condText = r.conditions.map(c => `[${formatType(c.type)} ${formatOp(c.operator)} "${c.value}"]`).join(` <span style="color:#007AFF; font-weight:bold;">${r.logic}</span> `);
                rulesHTML += renderItem('积木', `${condText} (层级: ${r.level})`, i, 'complex');
            });

            if (rulesHTML === '<ul class="rule-list">') {
                rulesHTML = '<p style="text-align:center; color:#999; margin: 20px 0;">当前域名暂无屏蔽规则</p>';
            } else { rulesHTML += '</ul>'; }

            const modeText = data.config.mode === 'preemptive' ? '强制极速预判 (防闪现)' : '智能自动';
            const flashStatus = storage.flashList[storage.domain] ? '<span style="color:red; font-weight:bold;">已记录闪现特征,系统采用极速注入</span>' : '运行良好';

            panel.innerHTML = `
                <h3 title="按住可拖动窗口">规则与防御管理 (${storage.domain})</h3>
                
                <div class="status-bar">
                    <div><strong>防御策略:</strong> ${modeText}</div>
                    <div style="margin-top:4px;"><strong>系统评估:</strong> ${flashStatus}</div>
                </div>

                <div style="max-height: 250px; overflow-y: auto; margin-bottom: 15px; border: 1px solid #eee; border-radius: 6px;">
                    ${rulesHTML}
                </div>
                
                <div class="btn-group">
                    <button class="btn-info" id="btn-toggle-mode" style="flex:100%;">🚀 切换防御策略</button>
                </div>
                <div class="btn-group" style="margin-top: 10px;">
                    <button class="btn-outline" id="btn-clear-all">清除所有规则</button>
                    <button class="btn-primary" id="btn-close-manager">完成</button>
                </div>
            `;

            this.makeDraggable(panel);
            this.shadowRoot.appendChild(panel);

            panel.querySelectorAll('.btn-delete').forEach(btn => {
                btn.addEventListener('click', (e) => {
                    const typeMap = { '静态': 'static', '动态': 'dynamic', '正则': 'regex', '域封杀': 'attribute', '位置': 'structural', '积木': 'complex' };
                    const type = typeMap[e.target.getAttribute('data-type')];
                    const index = parseInt(e.target.getAttribute('data-index'), 10);
                    storage.removeRule(type, index);
                    this.showManager();
                    if (type !== 'regex' && type !== 'complex') BlockEngine.applyCSSRules(); 
                    else window.location.reload(); 
                });
            });

            panel.querySelector('#btn-toggle-mode').addEventListener('click', () => {
                const newMode = storage.toggleMode();
                alert(`策略已调整为:${newMode === 'preemptive' ? '极速预判模式' : '智能自动模式'}\n页面即将刷新以应用变更配置。`);
                window.location.reload();
            });

            panel.querySelector('#btn-clear-all').addEventListener('click', () => {
                if (confirm('警告:此操作将清空当前域名下的所有拦截规则和配置,且不可恢复。确认继续?')) {
                    storage.clearDomain();
                    window.location.reload();
                }
            });

            panel.querySelector('#btn-close-manager').addEventListener('click', () => this.clearPanel());
        }

        clearPanel() {
            if (this._previewAffectedElements && this._previewAffectedElements.length > 0) {
                this._previewAffectedElements.forEach(item => {
                    if (item.el) {
                        item.el.style.display = item.origDisplay;
                        item.el.style.opacity = item.origOpacity;
                    }
                });
                this._previewAffectedElements = [];
            }

            const oldPanel = this.shadowRoot.querySelector('.panel');
            if (oldPanel && typeof oldPanel._cleanupDrag === 'function') {
                oldPanel._cleanupDrag();
            }
            this.shadowRoot.innerHTML = '';
            this.injectStyles();
        }
    }

    // ================= 初始化与执行流 =================

    BlockEngine.fastInject();
    BlockEngine.startObserver();

    if (window.self === window.top) {
        let uiInstance = null;
        function getUI() {
            if (!uiInstance) uiInstance = new UIManager();
            return uiInstance;
        }

        GM_registerMenuCommand('🖱 手动选择屏蔽元素', () => getUI().startSelection());
        GM_registerMenuCommand('📝 添加文本/正则/积木规则', () => getUI().showRegexPanel()); 
        GM_registerMenuCommand('⚙️ 管理规则与防御策略', () => getUI().showManager());
    }

})();