Aloazny123 / Twitter自用增强

// ==UserScript==
// @name         Twitter自用增强
// @namespace    http://viayoo.com/bhivy
// @version      3.4
// @description  自用脚本,干掉部分机械人发言,过滤推特(Twitter)评论区,去广告,尝试解锁敏感限制。
// @author       Via && Deepseek
// @match       https://x.com/*
// @exclude     https://x.com/*/status/*/video/*
// @exclude     https://x.com/*/status/*/photo/*
// @exclude     https://x.com/*/status/*/mediaViewer*
// @exclude     https://x.com/settings
// @exclude     https://x.com/i/*/creators/
// @exclude     https://x.com/i/bookmarks
// @exclude     https://x.com/i/premium-business
// @exclude     https://x.com/*/lists
// @exclude     https://x.com/i/follow_people
// @exclude     https://x.com/i/chat
// @exclude     https://x.com/notifications
// @exclude     https://x.com/i/grok
// @icon         data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAA0pJREFUWAntVk1oE1EQnnlJbFK3KUq9VJPYWgQVD/5QD0qpfweL1YJQoZAULBRPggp6kB78PQn14kHx0jRB0UO9REVFb1YqVBEsbZW2SbVS0B6apEnbbMbZ6qbZdTempqCHPAjvzcw3P5mdmfcAiquYgX+cAVwu/+5AdDMQnSPCHUhQA0hf+Rxy2OjicIvzm+qnKhito0qpb2wvJhWeJgCPP7oPELeHvdJ1VSGf3eOPnSWga0S0Qo9HxEkEusDBuNjbEca8G291nlBxmgDc/ukuIvAJxI6wr+yKCsq1ewLxQ2lZfpQLo8oQ4ZXdCkfnACrGWpyDCl+oQmVn5xuVPU102e2P3qoJkFOhzVb9S7KSnL5jJs/mI+As01PJFPSlZeFSZZoAGBRXBZyq9lk5NrC+e7pJ5en30c+JWk59pZ5vRDOuhAD381c/H/FKz1SMNgCE16rg505r5TT0uLqme93d0fbq+1SeLSeU83Ke0RHYFPGVPcjQfNDUwIa7M665+dQAEEjZoMwZMcEF9RxIDAgBQ2mCcqJ0Z0b+h4MNbZ4RnyOSDbNmE2iRk5jCNgIIckFoZAs4IgfLGrlKGjkzS16iwj6pV9I4mUvCPf73JVytH9nRJj24QHrqU8NCIWrMaGqAC+Ut/3ZzAS63cx4v2K/x/IvQBOCwWzu5KmJGwEJ5PIgeG9nQBDDcXPpFoDjJ7ThvBC6EZxXWkJG+JgAFwGM4KBAOcibeGCn8FQ/hyajXPmSk+1sACogn4hYk7OdiHDFSWipPkPWSmY6mCzIghEEuxJvcEYUvxIdhX2mvmSHDDPBF9AJRnDZTyp+P40671JYLbxiAohDxSTfQIg4oNxgPzCWPHaWQBViOf2jGqVwBaEaxGbAqOFMrp+SefC8eNhoFIY5lXzpmtnMGUB2IbU3JdIqVW9m5zcxINn/hAYKiIexdaTh4srHKORMAP0b28PNgJyGt5gvHzQVYx91QpVcwpRFl/p63HSR1DLbid1OcTpAJQOG7u+KH+aI5Qwj13IsamU5vkUSIc8uGLDa8OtoivV8U5HcydFLtT7hlSDVy2nfxI2Ibg9awuVU8IeJAOMF5m2B6jFs1tM5R9rS3GRP5uSuiihn4DzPwA7z7GDH+43gqAAAAAElFTkSuQmCC
// @license       MIT
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_setClipboard
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    const api = {
        getValue: (key, def) => {
            if (typeof GM_getValue !== 'undefined') return GM_getValue(key, def);
            const val = localStorage.getItem(key);
            return val === null ? def : JSON.parse(val);
        },
        setValue: (key, val) => {
            if (typeof GM_setValue !== 'undefined') { GM_setValue(key, val); } 
            else { localStorage.setItem(key, JSON.stringify(val)); }
        },
        setClipboard: (text) => {
            if (typeof GM_setClipboard !== 'undefined') { GM_setClipboard(text); } 
            else { navigator.clipboard.writeText(text); }
        }
    };

    const DEFAULT_CONFIG = { 
        badUsers: ["1-5线",".约炮‍","@ruantang992","@xiaonm88","MrMinstrel","chu男无偿","上门服务","上门破处","免费约p","免费破处","同城上门","同城无偿","听主人的话","处男免费","处男无偿","宇宙第一骚","找萢友","寻固炮","寻男大固炮","想找单男","找个搭子","找个男大弟弟","母狗找主人","每晚准时大秀","每晚准时色播","约.炮.‍","约炮.‍"], 
        badWords: ["/.*快手.*(.{4,8}).*(?:牛[逼皮]|审核|竟然|过审|[藏忍顶]不住|看[立爽]了)/","/30\\+.*(体制内|太[色瑟涩骚sao]).*(?:\\@|顶不住)/","/(?:dd?|个?|线上|线下)+的?(?:姐姐|弟弟|哥哥|妹妹).{0,6}/i","/(?:主人|爸爸)快来(?:领|找)我.{0,6}$/","/(?:同城|附近).*(?:满足|草|操|艹|男大|女大|体育生|母狗)/","/(?:喜欢我|颠勺|大|这个骚货|线下我就曰|线下骚货)+/","/(?:她好(瑟|色|骚|[Ss][Aa][Oo])|我不行了|刷了半天|就她的主页能打)+/","/(?:小|母)狗(?:求|要|想|主人|爸爸|哥哥|抱抱|操|艹|草|抱走){2,}/","/(?:急需?|找)(?:一个|一位)?(?:主人|爸爸|固[萢跑炮狍]|搭子).{0,6}$/","/(?:推特|第一骚)\\s*\\@.+/","/(?:法我|被法|滴滴|想|来|一){3,}/","/(?:骚|[Ss][Aa][Oo]|比她|货){2,}.*没人比她.{2,4}\\s*\\@/","/.{10,21}\\s*玩的就是[反|返]差\\s*\\@/","/^(?:来|有)个(?:真人|单男|母狗|骚逼).+一下/","/^(?:男大|女大)大?(?:哥哥|弟弟|姐姐|妹妹)快?来$/","/^(?:线上|线下)?(?:蹲|找)一?个(?:男|女)?(?:长期|疼人|临时|温柔)?(?:固[萢跑炮狍]|搭子|主人|爸爸|哥哥|弟弟|主|[Ss])/","/^(?=.*我是真人)(?=.*线[上下][的吗])/","/^..找..(?:主人|主|爸爸|固[萢跑炮狍]|搭子).{0,6}$/","/^[\\p{Emoji}\\n]+$/u","/^刚(?:分手|被|操|做|完|艹|草)想被(?:操|艹|草|爱).{0,4}/","/^有?万达广场附近的吗$/","/^有没有(?:单男|母狗|骚逼).{0,6}$/","/^线下[Dd][Dd]$/","/^谁来当我(?:主人|爸爸|狗狗|小狗|骚母狗).{0,6}$/","/pan\\.(?:quark\\.cn|xunlei\\.com)|drive\\.uc\\.cn/","/体制内[的得]?.{3,8}[的得]很\\s*\\@/","/她太.{1,6}了.*我真顶不住\\s*\\@/","/小(?:狗|🐶|[Mm])(?:在线|想[和跟])(?:等|找|大家|主人|主|爸爸|固[萢跑炮狍]|搭子|调|你).{0,6}/","/晚上开课.+/","/有(哥哥|弟弟).*线(上|下)(约|吗)/i","/有没有离[得的]近[得的].{0,4}$/","/比她(?:骚|好看)的没/","/线下(?:骚|[Ss][Aa][Oo]|比她|货)+/","/(去|在|直到)快手.*(.{4,8}).*(绝了|审核|尺度|推荐|正常|藏了).*[??。]/","@designksh1","@ruantang992","@xiaonm88"]
    };

    let config = Object.assign({ disabledDefaultWords: [], disabledDefaultUsers: [] }, api.getValue('filterConfig', DEFAULT_CONFIG));
    config.badUsers = (config.badUsers || []).filter(u => !DEFAULT_CONFIG.badUsers.includes(u));
    config.badWords = (config.badWords || []).filter(w => !DEFAULT_CONFIG.badWords.includes(w));

    const compileRules = (words) => words.map(r => {
        try {
            if (r.startsWith('/') && r.lastIndexOf('/') > 0) {
                const idx = r.lastIndexOf('/');
                return new RegExp(r.substring(1, idx), r.substring(idx + 1));
            }
            return new RegExp(r);
        } catch (e) { return null; }
    }).filter(Boolean);

    let compiledRules = compileRules([
        ...config.badWords,
        ...DEFAULT_CONFIG.badWords.filter(w => !config.disabledDefaultWords.includes(w))
    ]);

    let PROCESSED_TWEETS = new WeakSet();
    const FILTER_CACHE = new Map();

    const cleanText = (text) => {
        if (!text) return "";
        return text.replace(/[\u0000-\u001F\u007F-\u009F\u00AD\u061C\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF\u200D\u200C\u180E\u202F\u205F\u2060\uFE00-\uFE0F]/g, '').trim();
    };

    const isSpamBot = (tweetTextEl, tweetNode) => {
        if (!tweetTextEl) return false;
        const pureText = cleanText(tweetTextEl.innerText || "");
        const nameEl = tweetNode?.querySelector('[data-testid="User-Name"]');
        const nickName = nameEl ? cleanText(nameEl.innerText.split(/[\n·]/)[0]) : "";
        const nameEmojiEls = nameEl ? nameEl.querySelectorAll('img[src*="emoji"]') : [];
        const tweetEmojiEls = tweetTextEl.querySelectorAll('img[src*="emoji"]');
        const spamEmojiList = ["⬆️","🍓","🌸","💊","👠","💎","💯","🍆","🔮","🍑","🌈","🚪","🅱️","🔞","❤"];
        let nameEmojiCount = 0;
        for (let img of nameEmojiEls) { if (spamEmojiList.includes(img.getAttribute('alt'))) nameEmojiCount++; }
        const nameSpamStrict = /(?:约[ .]?炮|chu男|破处|处男|寻|找|蹲|求|想找|一个)(?:免费|无偿|单男|固[萢跑炮狍]|搭子|主[人子]|哥哥|弟弟|男大|女大|线上|线下)|(?:每晚|准时)(?:大秀|色播|直播)|(?:母狗|骚货|第一骚|体育生|女大|男大)(?:求|找|寻|固[萢跑炮狍]|[萢跑狍]友|搭子|主[人子])|[(\uff08](?:饥渴|处女|寂寞|男大来)|(?:附近的|来|DD)|(?:资源|小马开大车|萝莉|幼钕){2,}/i;
        const nameSpamSuspect = /(?:同城|附近|寻|蹲|约见?|无|真实|免费|偿|个|丝丝|小猫|茜茜|萝莉|学姐|乔|欣|萌|成人|限制|破除|破限|AI|上门|速配|服务|满足|线下|联系|野战|安排|哥哥|弟弟|男大|女大|单男|体育生|固[萢跑炮狍]|[萢跑狍]友){2,}|(?:|1\-5|对接|主页|置顶|无套路|[首原]创|真实|全国|覆盖|可靠|同城|约见|附近|牵线|头像|[点看]我){2,}/i;
        if (nickName) {
            if (nameSpamStrict.test(nickName)) return true;
            if (nameEmojiCount > 0 && nameSpamSuspect.test(nickName)) return true;
        }
        let totalSpamEmojiCount = nameEmojiCount;
        for (let img of tweetEmojiEls) { if (spamEmojiList.includes(img.getAttribute('alt'))) totalSpamEmojiCount++; }
        if (totalSpamEmojiCount >= 6) return true;
        const hasPunctuation = /[\u3002\uff0c\uff1f\uff01\uff1b\uff1a\u3001.?!,;:]/.test(pureText);
        if (tweetEmojiEls.length > 0 && !hasPunctuation && pureText.length < 4) return true;
        if (tweetEmojiEls.length >= 4 && tweetEmojiEls.length <= 7 && /^[0-9a-zA-Z\s]*$/.test(pureText)) return true;
        const specialSymbols = /[𖬺༄𖫹𖬚✫꙳⸝𖨋༺꙼꙽༻ꦿ⬆↑⇑ꪕꜥ꩕ꦼꥏ⦹⦸ꥎꦾ꩔Ꜥꪑ⧉ꥐ꧀꩖꧎ꧏ꧑꩗꧁❀©ꥑ༘⋆࿐𓆩𓆪ꕤ༉✧ꦂ𓂃࿔𓏸𓃠]/g;
        const symbolMatches = pureText.match(specialSymbols);
        if (symbolMatches && symbolMatches.length >= 3) return true;
        return false;
    };

    const twitterContentUnblocker = (() => {
        const TARGET_ENDPOINTS = ['UserTweets', 'TweetDetail', 'SearchTimeline', 'HomeTimeline'];
        const rewriteResponse = (rawText) => {
            try {
                return rawText
                    .replace(/"possibly_sensitive":\s*true/g, '"possibly_sensitive":false')
                    .replace(/"profile_interstitial_type":\s*"sensitive_media"/g, '"profile_interstitial_type":""')
                    .replace(/"mediaVisibilityResults":/g, '"mediaVisibilityResults_bypass":');
            } catch (e) { return rawText; }
        };
        return () => {
            const XHR = XMLHttpRequest.prototype;
            const send = XHR.send;
            const open = XHR.open;
            XHR.open = function(method, url) { this._url = url; return open.apply(this, arguments);};
            XHR.send = function() {
                if (this._url && TARGET_ENDPOINTS.some(ep => this._url.includes(ep))) {
                    const responseTextGetter = Object.getOwnPropertyDescriptor(XHR, 'responseText').get;
                    Object.defineProperty(this, 'responseText', {
                        get: function() {
                            const originalText = responseTextGetter.call(this);
                            return typeof originalText === 'string' ? rewriteResponse(originalText) : originalText;
                        },
                        configurable: true
                    });
                }
                return send.apply(this, arguments);
            };
        };
    })();

    const twitterAdCleaner = (() => {
        const PROCESSED_NODES = new WeakSet();
        const AD_SELECTORS = ["div[data-testid='placementTracking']", "button[aria-label='Grok actions']","button[aria-label='Grok 操作']", "aside[role='complementary']"];
        const isAdText = (t) => ['Promoted', '广告', '推廣', '広告', 'Sponsorisé'].includes(t);
        const processNode = (node) => {
            if (!node || node.nodeType !== 1 || PROCESSED_NODES.has(node)) return;
            try {
                if (node.getAttribute('data-testid') === 'cellInnerDiv') {
                    const hasAdSignal = node.querySelector(AD_SELECTORS[0]);
                    const adLabel = Array.from(node.querySelectorAll('article span')).find(s => isAdText(s.textContent));
                    if (hasAdSignal || adLabel) {
                        node.setAttribute('data-filtered', 'true');
                        PROCESSED_NODES.add(node);
                        return;
                    }
                }
                if (node.matches?.(AD_SELECTORS[1]) || node.matches?.(AD_SELECTORS[2])) {
                    const isGrok = node.matches(AD_SELECTORS[1]);
                    const isAdSidebar = node.innerText && isAdText(node.innerText.split('\n')[0]);
                    if (isGrok || isAdSidebar) {
                        node.setAttribute('data-filtered', 'true');
                    }
                }
            } catch (e) {}
        };
        return () => {
            const observer = new MutationObserver((m) => {
                requestAnimationFrame(() => m.forEach(mu => mu.addedNodes.forEach(processNode)));
            });
            observer.observe(document.body, { childList: true, subtree: true });
            document.querySelectorAll('div[data-testid="cellInnerDiv"]').forEach(processNode);
        };
    })();

    const quickFilterUI = (tweetData, tweetNode) => {
        const existing = document.getElementById('tf-quick-panel');
        if (existing) existing.remove();
        const box = document.createElement('div');
        box.id = 'tf-quick-panel';
        const shadow = box.attachShadow({mode: 'closed'});
        const style = document.createElement('style');
        style.textContent = `
            :host { position: fixed; inset: 0; z-index: 1000000; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.5); font-family: -apple-system, system-ui; }
            .card { background: white; width: 320px; padding: 20px; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); }
            @media (prefers-color-scheme: dark) { .card { background: #15202b; color: white; } }
            h4 { margin: 0 0 15px 0; font-size: 16px; }
            .opt { background: rgba(128,128,128,0.1); padding: 10px; border-radius: 8px; margin-bottom: 8px; cursor: pointer; font-size: 13px; word-break: break-all; border: 1px solid transparent; }
            .opt:hover { border-color: #1d9bf0; background: rgba(29,155,240,0.1); }
            textarea { width: 100%; height: 60px; margin-top: 5px; border-radius: 8px; border: 1px solid #ccc; padding: 5px; font-size: 12px; resize: none; box-sizing: border-box; }
            .btns { display: flex; gap: 8px; margin-top: 15px; }
            button { flex: 1; padding: 8px; border-radius: 20px; border: none; font-weight: bold; cursor: pointer; }
            .add { background: #1d9bf0; color: white; }
            .cancel { background: #eee; color: #333; }
        `;
        const cleanVal = cleanText(tweetData.text);
        const regexVal = `/${cleanVal.substring(0, 10).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*/`;
        const getFilterName = (name) => {
            if (!name) return "";
            const parts = name.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]|\u200D/);
            if (parts.length <= 1) return name;
            let maxPart = "";
            for (let i = 0; i < parts.length; i++) {
                const p = parts[i].trim();
                if (p.length >= maxPart.length) maxPart = p;
            }
            return maxPart || name;
        };
        const filterName = getFilterName(tweetData.userName);
        shadow.appendChild(style);
        const card = document.createElement('div');
        card.className = 'card';
        card.innerHTML = `
            <h4>快速过滤</h4>
            <div class="opt" id="addU">屏蔽用户ID: <b>${tweetData.userId}</b></div>
            <div class="opt" id="addN">屏蔽名称: <b>${filterName}</b></div>
            <div class="opt" id="addR">提取正则: <code>${regexVal}</code></div>
            <div class="label">自定义规则:</div>
            <textarea id="customR">${cleanVal}</textarea>
            <div class="btns">
                <button class="cancel" id="cbtn">取消</button>
                <button class="add" id="abtn">确认添加</button>
            </div>
        `;
        shadow.appendChild(card);
        document.body.appendChild(box);
        const close = () => box.remove();
        shadow.getElementById('cbtn').onclick = close;
        const syncToMainPanel = (type, value) => {
            const mainPlugin = document.getElementById('tf-plugin');
            if (mainPlugin && mainPlugin.shadowRoot) {
                const targetTextarea = mainPlugin.shadowRoot.getElementById(type);
                if (targetTextarea) {
                    const currentLines = targetTextarea.value.split('\n').map(s => s.trim()).filter(s => s);
                    if (!currentLines.includes(value)) {
                        currentLines.push(value);
                        targetTextarea.value = currentLines.join('\n');
                    }
                }
            }
        };
        shadow.getElementById('addU').onclick = () => { shadow.getElementById('customR').value = tweetData.userId; shadow.getElementById('abtn').click(); };
        shadow.getElementById('addN').onclick = () => { 
            const val = filterName;
            if (!DEFAULT_CONFIG.badUsers.includes(val) && !config.badUsers.includes(val)) {
                config.badUsers.push(val);
                syncToMainPanel('users', val);
            }
            api.setValue('filterConfig', config);
            if (tweetNode) {
                const pCell = tweetNode.closest('[data-testid="cellInnerDiv"]') || tweetNode;
                pCell.setAttribute('data-filtered', 'true');
            }
            close();
        };
        shadow.getElementById('addR').onclick = () => { shadow.getElementById('customR').value = regexVal; shadow.getElementById('abtn').click(); };
        shadow.getElementById('abtn').onclick = () => {
            const val = shadow.getElementById('customR').value.trim();
            if (!val) return;
            if (val.startsWith('@')) {
                if (!DEFAULT_CONFIG.badUsers.includes(val) && !config.badUsers.includes(val)) {
                    config.badUsers.push(val);
                    syncToMainPanel('users', val);
                }
                config.disabledDefaultUsers = config.disabledDefaultUsers.filter(x => x !== val);
            } else {
                if (!DEFAULT_CONFIG.badWords.includes(val) && !config.badWords.includes(val)) {
                    config.badWords.push(val);
                    syncToMainPanel('words', val);
                }
                config.disabledDefaultWords = config.disabledDefaultWords.filter(x => x !== val);
                compiledRules = compileRules(config.badWords);
            }
            api.setValue('filterConfig', config);
            if (tweetNode) {
                const pCell = tweetNode.closest('[data-testid="cellInnerDiv"]') || tweetNode;
                pCell.setAttribute('data-filtered', 'true');
            }
            close();
        };
    };
    
    const addFilterBtn = (tweet, data) => {
        const group = tweet.querySelector('div[role="group"]:last-child');
        if (!group || group.querySelector('.tf-qbtn')) return;
        const insert = () => {
            const btn = document.createElement('div');
            const reference = group.firstElementChild;
            btn.className = (reference ? reference.className : 'css-175oi2r r-18u37iz r-1h0z5md r-13awgt0') + ' tf-qbtn';
            btn.style = 'display:flex;align-items:center;justify-content:center;cursor:pointer;opacity:0.6;';
            btn.innerHTML = '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M10.32 12.56L4.12 4H19.88L13.68 12.56V19.64L10.32 21.44V12.56ZM3 2H21L12 14.5V22L8 20V14.5L3 2Z"/></svg>';
            btn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); quickFilterUI(data, tweet); };
            group.appendChild(btn);
        };
        if (group.children.length > 0) {
            insert();
        } else {
            setTimeout(insert, 150);
        }
    };

    const processFilterNode = (tweet) => {
        if (!tweet || PROCESSED_TWEETS.has(tweet)) return;
        const pCell = tweet.closest('[data-testid="cellInnerDiv"]') || tweet;
        const textEl = tweet.querySelector('[data-testid="tweetText"]');
        const tweetLink = tweet.querySelector('a[href*="/status/"]');
        const tweetId = tweetLink ? tweetLink.getAttribute('href').split('/status/')[1]?.split('?')[0] : null;
        const avatar = tweet.querySelector('[data-testid^="UserAvatar-Container-"]');
        const userId = avatar ? '@' + avatar.getAttribute('data-testid').replace('UserAvatar-Container-', '') : null;
        const nameContainer = tweet.querySelector('[data-testid="User-Name"]');
        const userName = nameContainer ? nameContainer.innerText.split('\n')[0] : null;
        const rawText = textEl ? textEl.innerText : "";

        if (tweetId && FILTER_CACHE.get(tweetId)) {
            pCell.setAttribute('data-filtered', 'true');
            PROCESSED_TWEETS.add(tweet); return;
        }

        const activeBadUsers = [
            ...config.badUsers,
            ...DEFAULT_CONFIG.badUsers.filter(u => !config.disabledDefaultUsers.includes(u))
        ];

        const isSpam = isSpamBot(textEl, tweet);
        const shouldHide = isSpam || activeBadUsers.some(u => {
            const t = u.toLowerCase();
            return (userId && userId.toLowerCase() === t) || (userName && userName.toLowerCase().includes(t));
        }) || (textEl && compiledRules.some(r => r.test(cleanText(rawText))));
        if (shouldHide) {
            pCell.setAttribute('data-filtered', 'true');
            if (tweetId) {
                FILTER_CACHE.set(tweetId, true);
                if (FILTER_CACHE.size > 1000) {
                    const firstKey = FILTER_CACHE.keys().next().value;
                    FILTER_CACHE.delete(firstKey);
                }
            }
        } else {
            addFilterBtn(tweet, { userId, userName, text: rawText });
        }
        PROCESSED_TWEETS.add(tweet);
    };

    const filterObserver = new MutationObserver((mutations) => {
        if (shouldExclude()) return;
        requestAnimationFrame(() => {
            for (let i = 0; i < mutations.length; i++) {
                mutations[i].addedNodes.forEach(node => {
                    if (node.nodeType === 1) {
                        const tweet = node.matches('article[data-testid="tweet"]') ? node : node.querySelector('article[data-testid="tweet"]');
                        if (tweet) processFilterNode(tweet);
                        else if (node.getAttribute?.('data-testid') === 'cellInnerDiv') {
                            setTimeout(() => { const r = node.querySelector('article[data-testid="tweet"]'); if (r) processFilterNode(r); }, 100);
                        }
                    }
                });
            }
        });
    });

    const createUI = () => {
        const globalStyle = document.createElement('style');
        globalStyle.textContent = `[data-filtered="true"] { display: none !important; }`;
        document.head.appendChild(globalStyle);
        const container = document.createElement('div');
        container.id = 'tf-plugin';
        document.body.appendChild(container);
        const shadow = container.attachShadow({ mode: 'open' });
        const style = document.createElement('style');
        style.textContent = `
            #mask { position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: none; z-index: 999998; backdrop-filter: blur(4px); }
            #fab { position: fixed; right: 0; top: 50%; transform: translateY(-50%); width: 42px; height: 42px; background: rgba(255, 255, 255, 0.6); color: #000; border-radius: 21px 0 0 21px; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: -2px 0 12px rgba(0,0,0,0.1); z-index: 999997; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); padding-left: 4px; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.3); border-right: none; }
            #fab.idle { opacity: 0.3; transform: translateY(-50%) translateX(25px); }
            #panel { display: none; position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 360px; max-width: 90vw; height: 500px; max-height: 85vh; background: rgba(255,255,255,0.8); backdrop-filter: blur(16px); border-radius: 24px; z-index: 999999; border: 1px solid rgba(255,255,255,0.4); font-family: -apple-system, system-ui; box-shadow: 0 20px 60px rgba(0,0,0,0.25); overflow: hidden; }
            .header { display: flex; justify-content: space-between; align-items: center; padding: 20px 20px 10px 20px; }
            .header h3 { margin: 0; font-size: 18px; font-weight: 800; }
            .close-btn { cursor: pointer; padding: 4px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: background 0.2s; }
            .close-btn:hover { background: rgba(0,0,0,0.1); }
            .tabs { display: flex; padding: 0 20px; border-bottom: 1px solid rgba(0,0,0,0.05); gap: 15px; }
            .tab { font-size: 13px; font-weight: bold; color: #536471; padding-bottom: 8px; cursor: pointer; border-bottom: 2px solid transparent; }
            .tab.active { color: #1d9bf0; border-bottom-color: #1d9bf0; }
            .view-slider { display: flex; width: 300%; height: calc(100% - 95px); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
            .page { width: 33.333%; height: 100%; box-sizing: border-box; padding: 15px 20px; overflow-y: auto; display: flex; flex-direction: column; }
            label { display: block; font-size: 12px; font-weight: bold; margin-bottom: 4px; color: #536471; }
            textarea { width: 100%; height: 140px; border: 1px solid rgba(0,0,0,0.1); border-radius: 12px; padding: 10px; margin-bottom: 12px; box-sizing: border-box; resize: none; outline: none; transition: all 0.2s; background: rgba(255,255,255,0.5); font-size: 13px; }
            textarea:focus { border-color: #1d9bf0; background: #fff; box-shadow: 0 0 0 3px rgba(29,155,240,0.1); }
            .miui-list { display: flex; flex-direction: column; gap: 8px; }
            .miui-item { display: flex; justify-content: space-between; align-items: center; background: rgba(0,0,0,0.02); padding: 10px 12px; border-radius: 12px; border: 1px solid rgba(0,0,0,0.03); }
            .miui-text { font-size: 13px; font-family: monospace; word-break: break-all; max-width: 75%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
            .miui-switch { position: relative; width: 44px; height: 24px; background: #e3e3e3; border-radius: 12px; cursor: pointer; transition: background 0.2s; -webkit-tap-highlight-color: transparent; }
            .miui-switch::after { content: ''; position: absolute; width: 20px; height: 20px; border-radius: 50%; background: #fff; top: 2px; left: 2px; transition: transform 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.2); }
            .miui-switch.checked { background: #00cc66; }
            .miui-switch.checked::after { transform: translateX(20px); }
            .btn-group { display: flex; flex-direction: column; gap: 10px; margin-top: auto; }
            button { padding: 11px; border-radius: 12px; border: none; font-weight: bold; cursor: pointer; transition: all 0.2s; font-size: 13px; }
            .save { background: #1d9bf0; color: #fff; }
            .save:hover { background: #1a8cd8; }
            .backup-btn { background: rgba(0,0,0,0.05); color: #000; }
            .backup-btn:hover { background: rgba(0,0,0,0.08); }
            .reset { background: #f4212e; color: #fff; }
            .reset:hover { background: #dc1f29; }
            @media (prefers-color-scheme: dark) {
                #fab { background: rgba(0, 0, 0, 0.6); color: #fff; }
                #panel { background: rgba(21, 32, 43, 0.85); color: #fff; border: 1px solid rgba(255,255,255,0.1); }
                .close-btn:hover { background: rgba(255,255,255,0.1); }
                .tabs { border-bottom-color: rgba(255,255,255,0.05); }
                .tab { color: #8b98a5; }
                .tab.active { color: #1d9bf0; }
                label { color: #8b98a5; }
                textarea { background: rgba(0,0,0,0.3); color: #fff; border-color: rgba(255,255,255,0.1); }
                textarea:focus { background: rgba(0,0,0,0.5); }
                .miui-item { background: rgba(255,255,255,0.03); border-color: rgba(255,255,255,0.02); }
                .miui-switch { background: #3e4144; }
                .backup-btn { background: rgba(255,255,255,0.1); color: #fff; }
                .backup-btn:hover { background: rgba(255,255,255,0.15); }
            }
        `;
        shadow.appendChild(style);
        const div = document.createElement('div');
        div.innerHTML = `
            <div id="mask"></div>
            <div id="fab"><svg viewBox="0 0 24 24" width="22"><path fill="currentColor" d="M21 4H3c-.55 0-1 .45-1 1v1.59c0 .53.21 1.04.59 1.41l6.41 6.41V20c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-5.59l6.41-6.41c.38-.37.59-.88.59-1.41V5c0-.55-.45-1-1-1zM4 6h16v1.17L13.17 14H10.83L4 7.17V6z"/></svg></div>
            <div id="panel">
                <div class="header">
                    <h3>过滤设置</h3>
                    <div class="close-btn" id="closeX"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg></div>
                </div>
                <div class="tabs">
                    <div class="tab active" data-idx="0">用户规则</div>
                    <div class="tab" data-idx="1">默认规则</div>
                    <div class="tab" data-idx="2">规则备份</div>
                </div>
                <div class="view-slider" id="slider">
                    <div class="page">
                        <label>屏蔽用户 ID</label>
                        <textarea id="users" placeholder="@username">${config.badUsers.join('\n')}</textarea>
                        <label>关键词或正则</label>
                        <textarea id="words" placeholder="/正则/i">${config.badWords.join('\n')}</textarea>
                        <button class="save" id="saveBtn">保存并应用</button>
                    </div>
                    <div class="page">
                        <div class="miui-list" id="defaultRulesList"></div>
                    </div>
                    <div class="page">
                        <div class="btn-group">
                            <button class="backup-btn" id="copyBtn">导出规则 (复制到剪贴板)</button>
                            <button class="backup-btn" id="importBtn">导入规则</button>
                            <button class="reset" id="resetBtn">恢复默认规则</button>
                        </div>
                    </div>
                </div>
            </div>
        `;
        shadow.appendChild(div);

        const panel = shadow.getElementById('panel'), fab = shadow.getElementById('fab'), mask = shadow.getElementById('mask');
        const slider = shadow.getElementById('slider'), tabs = shadow.querySelectorAll('.tab'), defContainer = shadow.getElementById('defaultRulesList');
        let hideTimer;

        const resetTimer = () => {
            fab.classList.remove('idle');
            clearTimeout(hideTimer);
            hideTimer = setTimeout(() => { if (panel.style.display !== 'block') fab.classList.add('idle'); }, 3000);
        };

        const toggleUI = (show) => {
            panel.style.display = show ? 'block' : 'none';
            mask.style.display = show ? 'block' : 'none';
            if (!show) resetTimer();
        };

        const updateVisibility = () => { container.style.display = shouldExclude() ? 'none' : 'block'; };

        tabs.forEach(t => {
            t.onclick = () => {
                tabs.forEach(tab => tab.classList.remove('active'));
                t.classList.add('active');
                slider.style.transform = `translateX(-${t.getAttribute('data-idx') * 33.333}%)`;
            };
        });

        const renderDefaultRules = () => {
            defContainer.innerHTML = '';
            const createSection = (title, items, type) => {
                const label = document.createElement('label');
                label.innerText = title;
                label.style.marginTop = '10px';
                defContainer.appendChild(label);
                items.forEach(item => {
                    const row = document.createElement('div');
                    row.className = 'miui-item';
                    const text = document.createElement('div');
                    text.className = 'miui-text';
                    text.innerText = item;
                    text.title = item;
                    const sw = document.createElement('div');
                    const isDisabled = type === 'user' ? config.disabledDefaultUsers.includes(item) : config.disabledDefaultWords.includes(item);
                    sw.className = `miui-switch${isDisabled ? '' : ' checked'}`;
                    sw.onclick = () => {
                        if (type === 'user') {
                            if (config.disabledDefaultUsers.includes(item)) {
                                config.disabledDefaultUsers = config.disabledDefaultUsers.filter(x => x !== item);
                            } else {
                                config.disabledDefaultUsers.push(item);
                            }
                        } else {
                            if (config.disabledDefaultWords.includes(item)) {
                                config.disabledDefaultWords = config.disabledDefaultWords.filter(x => x !== item);
                            } else {
                                config.disabledDefaultWords.push(item);
                            }
                        }
                        api.setValue('filterConfig', config);
                        sw.classList.toggle('checked');
                    };
                    row.appendChild(text);
                    row.appendChild(sw);
                    defContainer.appendChild(row);
                });
            };
            if(DEFAULT_CONFIG.badUsers.length) createSection('默认屏蔽用户', DEFAULT_CONFIG.badUsers, 'user');
            if(DEFAULT_CONFIG.badWords.length) createSection('默认屏蔽关键词/正则', DEFAULT_CONFIG.badWords, 'word');
        };
        renderDefaultRules();

        fab.onclick = () => toggleUI(true);
        mask.onclick = () => toggleUI(false);
        shadow.getElementById('closeX').onclick = () => toggleUI(false);
        fab.onmouseenter = () => fab.classList.remove('idle');
        fab.onmouseleave = resetTimer;
        shadow.getElementById('saveBtn').onclick = () => {
            const inputUsers = shadow.getElementById('users').value.split('\n').map(s => s.trim()).filter(s => s);
            const inputWords = shadow.getElementById('words').value.split('\n').map(s => s.trim()).filter(s => s);
            config.badUsers = inputUsers.filter(u => !DEFAULT_CONFIG.badUsers.includes(u));
            config.badWords = inputWords.filter(w => !DEFAULT_CONFIG.badWords.includes(w));
            api.setValue('filterConfig', config);
            location.reload();
        };
        shadow.getElementById('copyBtn').onclick = () => { api.setClipboard(JSON.stringify(config)); alert('已复制'); };
        shadow.getElementById('resetBtn').onclick = () => {
            if (confirm("确定要恢复默认规则吗?(您的自定义规则将被保留)")) {
                config.disabledDefaultUsers = [];
                config.disabledDefaultWords = [];
                api.setValue('filterConfig', config);
                location.reload();
            }
        };
        shadow.getElementById('importBtn').onclick = () => {
            const j = prompt("请粘贴导出的 JSON 规则:");
            if (j) {
                try {
                    const p = JSON.parse(j);
                    if (p.badUsers || p.badWords) {
                        const newData = {
                            badUsers: Array.isArray(p.badUsers) ? p.badUsers : [],
                            badWords: Array.isArray(p.badWords) ? p.badWords : [],
                            disabledDefaultUsers: Array.isArray(p.disabledDefaultUsers) ? p.disabledDefaultUsers : [],
                            disabledDefaultWords: Array.isArray(p.disabledDefaultWords) ? p.disabledDefaultWords : []
                        };
                        api.setValue('filterConfig', newData);
                        location.reload();
                    } else {
                        alert("规则格式不匹配(需包含badUsers或badWords)");
                    }
                } catch (e) {
                    alert("JSON解析失败,请检查格式");
                }
            }
        };
        window.addEventListener('popstate', updateVisibility);
        document.addEventListener('click', () => setTimeout(updateVisibility, 200));
        resetTimer();
        updateVisibility();
    };

    const shouldExclude = (() => {
        const ex = /https:\/\/x\.com\/(?:.*\/status\/.*\/(?:video|photo|mediaViewer.*)|settings|i\/.*\/creators\/|i\/bookmarks|i\/premium-business|.*\/lists|i\/follow_people|i\/chat|notifications|i\/grok)/;
        let lastUrl = ''; let lastResult = false;
        return () => {
            const curr = location.href; if (curr === lastUrl) return lastResult;
            const prevResult = lastResult;
            lastUrl = curr; lastResult = ex.test(curr);
            if (lastResult) {
                filterObserver.disconnect();
            } else {
                filterObserver.observe(document.body, { childList: true, subtree: true });
                if (prevResult === true) {
                    PROCESSED_TWEETS = new WeakSet();
                    requestAnimationFrame(() => {
                        document.querySelectorAll('article[data-testid="tweet"]').forEach(processFilterNode);
                    });
                }
            }
            return lastResult;
        };
    })();

    twitterContentUnblocker();
    const checkReady = setInterval(() => {
        const main = document.querySelector('main');
        if (main) { clearInterval(checkReady); createUI(); twitterAdCleaner(); if (!shouldExclude()) document.querySelectorAll('article[data-testid="tweet"]').forEach(processFilterNode); }
    }, 1000);
})();