小红书作者ID过滤器

过滤并隐藏小红书网页版中作者ID包含特定字词的帖子

// ==UserScript==
// @name         小红书作者ID过滤器
// @namespace    http://tampermonkey.net/
// @version      4.0
// @description  过滤并隐藏小红书网页版中作者ID包含特定字词的帖子
// @author       ObenK
// @license      MIT
// @match        https://www.xiaohongshu.com/*
// @match        https://www.xiaohongshu.com/explore*
// @match        https://www.xiaohongshu.com/search_result*
// @match        https://www.xiaohongshu.com/user/profile/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // 添加CSS样式
    const style = document.createElement('style');
    style.textContent = `
        .xhs-filter-hidden {
            display: none !important;
            visibility: hidden !important;
            height: 0 !important;
            overflow: hidden !important;
            margin: 0 !important;
            padding: 0 !important;
        }
        
        .xhs-title-hidden .title,
        .xhs-title-hidden .footer .title,
        .xhs-title-hidden [class*="title"] {
            display: none !important;
            visibility: hidden !important;
            height: 0 !important;
            overflow: hidden !important;
            margin: 0 !important;
            padding: 0 !important;
        }
    `;
    document.head.appendChild(style);

    // 配置类
    class Config {
        static get DEFAULT_KEYWORDS() {
            return ['丰田', '搬运工', '代购', '广告'];
        }

        static get HIDE_TITLES() {
            return GM_getValue('hideTitles', false);
        }

        static set HIDE_TITLES(value) {
            GM_setValue('hideTitles', value);
        }

        static get BLOCKED_KEYWORDS() {
            const saved = GM_getValue('blockedKeywords', null);
            if (saved === null) {
                // 首次安装时设置默认关键词
                this.BLOCKED_KEYWORDS = this.DEFAULT_KEYWORDS;
                return this.DEFAULT_KEYWORDS;
            }
            return saved;
        }

        static set BLOCKED_KEYWORDS(keywords) {
            GM_setValue('blockedKeywords', keywords);
        }

        static addKeyword(keyword) {
            const keywords = this.BLOCKED_KEYWORDS;
            const normalizedKeyword = keyword.trim();
            if (normalizedKeyword && !keywords.some(k => k.toLowerCase() === normalizedKeyword.toLowerCase())) {
                keywords.push(normalizedKeyword);
                this.BLOCKED_KEYWORDS = keywords;
                return true;
            }
            return false;
        }

        static removeKeyword(keyword) {
            const keywords = this.BLOCKED_KEYWORDS;
            const index = keywords.indexOf(keyword);
            if (index > -1) {
                keywords.splice(index, 1);
                this.BLOCKED_KEYWORDS = keywords;
            }
        }

        static resetToDefault() {
            this.BLOCKED_KEYWORDS = this.DEFAULT_KEYWORDS;
        }

        // 统计相关方法
        static getKeywordStats() {
            return GM_getValue('keywordStats', {});
        }

        static incrementKeywordHit(keyword) {
            const stats = this.getKeywordStats();
            stats[keyword] = (stats[keyword] || 0) + 1;
            GM_setValue('keywordStats', stats);
        }

        static resetKeywordStats() {
            GM_setValue('keywordStats', {});
        }
    }
    // 高性能过滤器类
    class PostFilter {
        constructor() {
            this.processedPosts = new WeakSet();
            this.observer = null;
            this.retryTimer = null;
            this.batchTimer = null;
            this.pendingPosts = [];
            this.keywordRegex = null;
            this.init();
        }

        init() {
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', () => this.startFiltering());
            } else {
                this.startFiltering();
            }

            this.updateKeywordRegex();
            this.setupApiInterceptor(); // 添加API拦截
        }

        updateKeywordRegex() {
            const keywords = Config.BLOCKED_KEYWORDS;
            if (keywords.length === 0) {
                this.keywordRegex = null;
            } else {
                // 使用更精确的正则表达式,确保大小写不敏感匹配
                const escapedKeywords = keywords.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
                this.keywordRegex = new RegExp(escapedKeywords.join('|'), 'i');
            }
        }

        startFiltering() {
            this.filterExistingPosts();
            this.setupMutationObserver();
            setTimeout(() => this.filterExistingPosts(), 100);
            setTimeout(() => this.filterExistingPosts(), 500);
        }

        filterExistingPosts() {
            if (!this.keywordRegex) return;

            const posts = this.getAllPosts();
            const newPosts = posts.filter(post => !this.processedPosts.has(post));

            if (newPosts.length === 0) return;

            this.processPosts(newPosts);
        }

        processPosts(posts) {
            if (!this.keywordRegex) return;

            let filteredCount = 0;

            for (const post of posts) {
                if (this.processedPosts.has(post)) continue;

                const authorId = this.getAuthorId(post);
                if (!authorId) continue;

                // 使用正则表达式一次性匹配所有关键词
                const match = authorId.match(this.keywordRegex);
                if (match) {
                    this.hidePost(post, authorId);
                    filteredCount++;

                    // 更新统计
                    try {
                        Config.incrementKeywordHit(match[0]);
                    } catch (e) {
                        console.error('[小红书过滤器] 统计更新失败:', e);
                    }
                }

                this.processedPosts.add(post);
            }

            if (filteredCount > 0) {
                console.log(`[小红书过滤器] 处理了 ${posts.length} 个帖子,过滤了 ${filteredCount} 个`);
            }
        }

        setupMutationObserver() {
            // 增强的MutationObserver,处理无限滚动
            this.observer = new MutationObserver((mutations) => {
                let hasNewPosts = false;
                const allNewPosts = [];

                mutations.forEach(mutation => {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            // 处理节点本身和子节点
                            const posts = this.getPostsFromNode(node);
                            if (posts.length > 0) {
                                allNewPosts.push(...posts);
                                hasNewPosts = true;
                            }

                            // 处理延迟加载的内容
                            setTimeout(() => {
                                const delayedPosts = this.getPostsFromNode(node);
                                const newDelayedPosts = delayedPosts.filter(post => !this.processedPosts.has(post));
                                if (newDelayedPosts.length > 0) {
                                    this.processPosts(newDelayedPosts);
                                }
                            }, 200);
                        }
                    });
                });

                if (allNewPosts.length > 0) {
                    const newPosts = allNewPosts.filter(post => !this.processedPosts.has(post));
                    if (newPosts.length > 0) {
                        this.processPosts(newPosts);
                    }
                }
            });

            this.observer.observe(document.body, {
                childList: true,
                subtree: true
            });

            // 额外监听滚动事件,处理无限滚动
            let scrollTimer = null;
            window.addEventListener('scroll', () => {
                if (scrollTimer) clearTimeout(scrollTimer);
                scrollTimer = setTimeout(() => {
                    this.filterExistingPosts();
                }, 300);
            });
        }

        getAllPosts() {
            // 增强选择器,覆盖更多小红书页面结构
            const selectors = [
                '.note-item',
                '[data-testid="note-item"]',
                '.feeds-container .note-item',
                '.search-container .note-item',
                '[class*="note-item"]'
            ];

            const posts = [];
            selectors.forEach(selector => {
                const elements = document.querySelectorAll(selector);
                elements.forEach(el => posts.push(el));
            });

            return [...new Set(posts)];
        }

        getPostsFromNode(node) {
            if (!node || !node.querySelectorAll) return [];

            const selectors = [
                '.note-item',
                '[data-testid="note-item"]',
                '[class*="note-item"]'
            ];

            const posts = [];
            selectors.forEach(selector => {
                const elements = node.querySelectorAll(selector);
                elements.forEach(el => posts.push(el));
            });

            return [...new Set(posts)];
        }

        getAuthorId(post) {
            // 基于提供的HTML结构,优先查找.author-wrapper下的.name元素
            const authorWrapper = post.querySelector('.author-wrapper');
            if (authorWrapper) {
                const nameElement = authorWrapper.querySelector('.name');
                if (nameElement) {
                    return nameElement.textContent?.trim();
                }
            }

            // 备用选择器
            const selectors = [
                '.author .name',
                '.author-name',
                '[data-testid="author-name"]',
                '.user-name',
                '.nickname',
                'a[href*="/user/profile/"] span',
                '[class*="author"] [class*="name"]'
            ];

            for (const selector of selectors) {
                const element = post.querySelector(selector);
                if (element) {
                    const text = element.textContent?.trim();
                    if (text) return text;
                }
            }

            // 从用户链接中提取
            const userLink = post.querySelector('a[href*="/user/profile/"]');
            if (userLink) {
                const nameSpan = userLink.querySelector('span');
                if (nameSpan) {
                    return nameSpan.textContent?.trim();
                }
                const linkText = userLink.textContent?.trim();
                if (linkText && linkText !== '') {
                    return linkText;
                }
            }

            return null;
        }

        hidePost(post, authorId) {
            // 使用CSS类替代直接样式操作
            post.classList.add('xhs-filter-hidden');
            post.setAttribute('data-filtered', 'true');
            post.setAttribute('data-filter-reason', `作者ID包含屏蔽关键词: ${authorId}`);

            console.log(`[小红书过滤器] 已隐藏作者 "${authorId}" 的帖子`);
        }

        // 强制重新过滤所有帖子
        forceReFilter() {
            // 重新显示所有被隐藏的帖子
            this.restoreHiddenPosts();

            // 清空已处理集合
            this.processedPosts = new WeakSet();

            // 重新更新正则表达式
            this.updateKeywordRegex();

            // 重新过滤所有帖子
            this.filterExistingPosts();

            console.log('[小红书过滤器] 已强制重新过滤所有帖子');
        }

        // 恢复所有被隐藏的帖子
        restoreHiddenPosts() {
            const hiddenPosts = document.querySelectorAll('[data-filtered="true"]');
            hiddenPosts.forEach(post => {
                post.classList.remove('xhs-filter-hidden');
                post.removeAttribute('data-filtered');
                post.removeAttribute('data-filter-reason');
            });
            console.log(`[小红书过滤器] 已恢复 ${hiddenPosts.length} 个被隐藏的帖子`);
        }

        // 验证API拦截有效性
        setupApiInterceptor() {
            const self = this;

            // 测试API拦截是否生效
            console.log('[API验证] 开始设置API拦截器...');

            // 增强的Fetch拦截 - 验证有效性
            const originalFetch = window.fetch;
            window.fetch = async function(...args) {
                const url = args[0];
                const urlStr = typeof url === 'string' ? url : (url?.url || url?.href || '');

                // 更精确的API检测
                const isXHSApi = urlStr.includes('/api/sns/web/v1/feed') ||
                               urlStr.includes('/api/sns/web/v1/search') ||
                               urlStr.includes('/api/sns/web/v1/homefeed') ||
                               urlStr.includes('/api/sns/web/v1/search/notes') ||
                               urlStr.includes('/api/sns/web/v1/search/notes/v1');

                if (isXHSApi) {
                    console.log(`[API检测] 拦截到小红书API: ${urlStr}`);

                    try {
                        const response = await originalFetch.apply(this, args);
                        const clone = response.clone();
                        const data = await clone.json();

                        console.log('[API数据] 成功获取API响应');

                        // 验证数据结构
                        if (data && typeof data === 'object') {
                            let filteredCount = 0;
                            let totalItems = 0;

                            // 检测所有可能的数据结构
                            const items = data.data?.items || data.data?.notes || data.data || [];
                            if (Array.isArray(items)) {
                                totalItems = items.length;

                                // 过滤逻辑
                                    const filteredItems = items.filter(item => {
                                        const user = item?.note_card?.user || item?.user;
                                        if (user) {
                                            const authorId = user.nickname || user.name || user.display_name || '';
                                            const shouldFilter = self.shouldFilterAuthor(authorId);
                                            if (shouldFilter) {
                                                // 获取匹配的关键词并更新统计
                                                const match = authorId.match(self.keywordRegex);
                                                if (match) {
                                                    Config.incrementKeywordHit(match[0]);
                                                    console.log(`[API过滤] 过滤作者: ${authorId} (关键词: ${match[0]})`);
                                                }
                                            }
                                            return !shouldFilter;
                                        }
                                        return true;
                                    });

                                    filteredCount = totalItems - filteredItems.length;

                                // 更新数据结构
                                if (data.data?.items) {
                                    data.data.items = filteredItems;
                                } else if (data.data?.notes) {
                                    data.data.notes = filteredItems;
                                } else if (Array.isArray(data.data)) {
                                    data.data = filteredItems;
                                }

                                if (filteredCount > 0) {
                                    console.log(`[API过滤] 从 ${totalItems} 个帖子中过滤了 ${filteredCount} 个`);
                                }
                            }
                        }

                        return new Response(JSON.stringify(data), {
                            status: response.status,
                            statusText: response.statusText,
                            headers: response.headers
                        });
                    } catch (e) {
                        console.error('[API拦截异常]', e);
                        return originalFetch.apply(this, args);
                    }
                }

                return originalFetch.apply(this, args);
            };

            // 增强XHR拦截 - 验证有效性
            const originalXHROpen = XMLHttpRequest.prototype.open;
            const originalXHRSend = XMLHttpRequest.prototype.send;

            XMLHttpRequest.prototype.open = function(method, url) {
                this._xhsFilterUrl = typeof url === 'string' ? url : '';
                return originalXHROpen.apply(this, arguments);
            };

            XMLHttpRequest.prototype.send = function() {
                const originalOnReadyStateChange = this.onreadystatechange;

                this.onreadystatechange = function() {
                    if (this.readyState === 4 && this._xhsFilterUrl) {
                        const isXHSApi = this._xhsFilterUrl.includes('/api/sns/web/v1/feed') ||
                                       this._xhsFilterUrl.includes('/api/sns/web/v1/search') ||
                                       this._xhsFilterUrl.includes('/api/sns/web/v1/homefeed') ||
                                       this._xhsFilterUrl.includes('/api/sns/web/v1/search/notes');

                        if (isXHSApi) {
                            console.log(`[XHR检测] 拦截到小红书XHR: ${this._xhsFilterUrl}`);

                            try {
                                const data = JSON.parse(this.responseText);
                                let filteredCount = 0;
                                let totalItems = 0;

                                // 检测所有可能的数据结构
                                const items = data.data?.items || data.data?.notes || data.data || [];
                                if (Array.isArray(items)) {
                                    totalItems = items.length;

                                    const filteredItems = items.filter(item => {
                                        const user = item?.note_card?.user || item?.user;
                                        if (user) {
                                            const authorId = user.nickname || user.name || user.display_name || '';
                                            const shouldFilter = self.shouldFilterAuthor(authorId);
                                            if (shouldFilter) {
                                                // 获取匹配的关键词并更新统计
                                                const match = authorId.match(self.keywordRegex);
                                                if (match) {
                                                    Config.incrementKeywordHit(match[0]);
                                                    console.log(`[XHR过滤] 过滤作者: ${authorId} (关键词: ${match[0]})`);
                                                }
                                            }
                                            return !shouldFilter;
                                        }
                                        return true;
                                    });

                                    filteredCount = totalItems - filteredItems.length;

                                    // 更新响应数据
                                    if (data.data?.items) {
                                        data.data.items = filteredItems;
                                    } else if (data.data?.notes) {
                                        data.data.notes = filteredItems;
                                    } else if (Array.isArray(data.data)) {
                                        data.data = filteredItems;
                                    }

                                    if (filteredCount > 0) {
                                        console.log(`[XHR过滤] 从 ${totalItems} 个帖子中过滤了 ${filteredCount} 个`);
                                    }
                                }

                                try {
                                    Object.defineProperty(this, 'responseText', {
                                        value: JSON.stringify(data),
                                        writable: true,
                                        configurable: true
                                    });
                                } catch (e) {
                                    console.warn('[XHR修改异常]', e);
                                }
                            } catch (e) {
                                console.error('[XHR拦截异常]', e);
                            }
                        }
                    }

                    if (originalOnReadyStateChange) {
                        originalOnReadyStateChange.call(this);
                    }
                };

                return originalXHRSend.apply(this, arguments);
            };
        }

        // 判断是否应该过滤作者
        shouldFilterAuthor(authorId) {
            if (!this.keywordRegex || !authorId || typeof authorId !== 'string') return false;
            return this.keywordRegex.test(authorId.trim());
        }
    }

    // 可视化管理类
    class VisualizationManager {
        static createVisualizationPanel() {
            const panel = document.createElement('div');
            panel.id = 'xhs-visualization-panel';
            panel.innerHTML = `
                <div style="
                    position: fixed;
                    top: 50%;
                    left: 50%;
                    transform: translate(-50%, -50%);
                    background: white;
                    border: 1px solid #ddd;
                    border-radius: 12px;
                    padding: 25px;
                    box-shadow: 0 8px 32px rgba(0,0,0,0.15);
                    z-index: 10001;
                    max-width: 600px;
                    width: 95%;
                    max-height: 80vh;
                    overflow-y: auto;
                    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                ">
                    <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
                        <h3 style="margin: 0; color: #333; font-size: 20px;">📊 数据可视化中心</h3>
                        <button onclick="this.parentElement.parentElement.parentElement.remove()" style="
                            background: none;
                            border: none;
                            font-size: 24px;
                            cursor: pointer;
                            color: #666;
                        ">×</button>
                    </div>

                    <!-- 热力图 -->
                    <div style="margin-bottom: 25px;">
                        <h4 style="margin: 0 0 15px 0; color: #5a7d9a; font-size: 16px;">🌡️ 时间屏蔽热力图</h4>
                        <div id="heatmap-container" style="background: #f8f9fa; border-radius: 8px; padding: 15px;">
                            ${this.renderHeatmap()}
                        </div>
                    </div>

                    <!-- 词云动画 -->
                    <div style="margin-bottom: 25px;">
                        <h4 style="margin: 0 0 15px 0; color: #5a7d9a; font-size: 16px;">☁️ 动态词云</h4>
                        <div id="wordcloud-container" style="background: #f8f9fa; border-radius: 8px; padding: 20px; text-align: center;">
                            ${this.renderWordCloud()}
                        </div>
                    </div>

                    <!-- 统计图表 -->
                    <div style="margin-bottom: 20px;">
                        <h4 style="margin: 0 0 15px 0; color: #5a7d9a; font-size: 16px;">📊 统计图表</h4>
                        <div id="stats-container" style="background: #f8f9fa; border-radius: 8px; padding: 20px;">
                            ${this.renderStats()}
                        </div>
                    </div>
                </div>

                <div style="
                    position: fixed;
                    top: 0;
                    left: 0;
                    right: 0;
                    bottom: 0;
                    background: rgba(0,0,0,0.5);
                    z-index: 10000;
                "></div>
            `;

            document.body.appendChild(panel);

            // 添加ESC键关闭功能
            const escHandler = (e) => {
                if (e.key === 'Escape') {
                    e.preventDefault();
                    e.stopPropagation();
                    panel.remove();
                    document.removeEventListener('keydown', escHandler, true);
                }
            };
            document.addEventListener('keydown', escHandler, true);

            // 点击遮罩关闭
            const clickHandler = (e) => {
                if (e.target === panel || e.target === panel.lastElementChild) {
                    panel.remove();
                    document.removeEventListener('keydown', escHandler, true);
                    document.removeEventListener('click', clickHandler);
                }
            };
            panel.addEventListener('click', clickHandler);
        }

        static renderHeatmap() {
            const stats = Config.getKeywordStats();
            const keywords = Config.BLOCKED_KEYWORDS;

            if (keywords.length === 0) {
                return '<p style="color: #666; text-align: center;">暂无数据</p>';
            }

            // 获取当前时间,只显示到当前小时
            const now = new Date();
            const currentHour = now.getHours();

            // 只显示到当前小时的真实数据
            const hours = Array.from({length: currentHour + 1}, (_, i) => i);
            const heatmapData = hours.map(hour => {
                // 使用真实统计数据的简化版本
                const totalHits = Object.values(stats).reduce((sum, hits) => sum + hits, 0);
                const intensity = totalHits > 0 ? Math.min(0.8, (Object.values(stats).reduce((sum, hits) => sum + hits, 0) / 100)) : 0.1;
                return { hour, intensity };
            });

            const cells = heatmapData.map(data => {
                const opacity = Math.min(0.8, data.intensity * (data.hour + 1) / 24);
                const color = `rgba(143, 179, 208, ${opacity})`;
                return `<div style="
                    width: 30px;
                    height: 20px;
                    background: ${color};
                    border-radius: 3px;
                    display: inline-block;
                    margin: 1px;
                    cursor: pointer;
                    transition: transform 0.2s;
                " title="${data.hour}:00 - ${Math.round(opacity * 100)}%"></div>`;
            }).join('');

            return `
                <div style="display: flex; flex-wrap: wrap; gap: 2px; justify-content: center;">
                    ${cells}
                    <div style="width: 100%; text-align: center; margin-top: 10px; font-size: 12px; color: #666;">
                        00:00 - ${currentHour}:00 (当前时间)
                    </div>
                </div>
            `;
        }

        static renderWordCloud() {
            const stats = Config.getKeywordStats();
            const keywords = Config.BLOCKED_KEYWORDS;

            if (keywords.length === 0) {
                return '<p style="color: #666; text-align: center;">暂无关键词</p>';
            }

            const sortedKeywords = keywords
                .map(keyword => ({ keyword, hits: stats[keyword] || 0 }))
                .sort((a, b) => b.hits - a.hits);

            const maxHits = Math.max(...sortedKeywords.map(item => item.hits), 1);

            const words = sortedKeywords.map(item => {
                const size = 12 + (item.hits / maxHits) * 24;
                const opacity = 0.4 + (item.hits / maxHits) * 0.6;
                return `<span style="
                    font-size: ${size}px;
                    color: rgba(90, 125, 154, ${opacity});
                    margin: 5px;
                    display: inline-block;
                    transition: all 0.3s ease;
                    cursor: pointer;
                " onmouseover="this.style.transform='scale(1.1)'" onmouseout="this.style.transform='scale(1)'">${item.keyword}</span>`;
            }).join('');

            return `<div style="line-height: 1.5;">${words}</div>`;
        }

        static renderStats() {
            const stats = Config.getKeywordStats();
            const keywords = Config.BLOCKED_KEYWORDS;

            if (keywords.length === 0) {
                return '<p style="color: #666; text-align: center;">暂无数据</p>';
            }

            const sortedKeywords = keywords
                .map(keyword => ({ keyword, hits: stats[keyword] || 0 }))
                .sort((a, b) => b.hits - a.hits)
                .slice(0, 8); // 只显示前8个

            const maxHits = Math.max(...sortedKeywords.map(item => item.hits), 1);

            const bars = sortedKeywords.map((item, index) => {
                const height = 20 + (item.hits / maxHits) * 80;
                const color = `hsl(200, 50%, ${50 + (item.hits / maxHits) * 30}%)`;

                return `
                    <div style="
                        display: inline-block;
                        margin: 0 5px;
                        text-align: center;
                    ">
                        <div style="
                            width: 30px;
                            height: ${height}px;
                            background: ${color};
                            border-radius: 3px 3px 0 0;
                            position: relative;
                            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
                            transition: all 0.3s ease;
                        " title="${item.keyword}: ${item.hits}次">
                            <div style="
                                position: absolute;
                                bottom: -20px;
                                left: 50%;
                                transform: translateX(-50%);
                                font-size: 10px;
                                color: #666;
                                white-space: nowrap;
                            ">${item.keyword}</div>
                        </div>
                    </div>
                `;
            }).join('');

            return `<div style="text-align: center; padding: 20px 0;">${bars}</div>`;
        }
    }

    // 管理界面
    class SettingsUI {
        static createPanel() {
            const panel = document.createElement('div');
            panel.id = 'xhs-filter-panel';
            panel.innerHTML = `
                <div style="
                    position: fixed;
                    top: 50%;
                    left: 50%;
                    transform: translate(-50%, -50%);
                    background: white;
                    border: 1px solid #ddd;
                    border-radius: 12px;
                    padding: 25px;
                    box-shadow: 0 8px 32px rgba(0,0,0,0.15);
                    z-index: 10000;
                    max-width: 600px;
                    width: 95%;
                    max-height: 80vh;
                    overflow-y: auto;
                    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                ">
                    <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
                        <h3 style="margin: 0; color: #333; font-size: 20px;">📊 小红书作者ID过滤器</h3>
                        <div style="display: flex; gap: 10px;">
                            <button id="show-visualization" style="
                                padding: 6px 12px;
                                background: #8fb3d0;
                                color: white;
                                border: none;
                                border-radius: 4px;
                                cursor: pointer;
                                font-size: 12px;
                            ">📊 可视化</button>
                            <button onclick="this.parentElement.parentElement.parentElement.parentElement.remove()" style="
                                background: none;
                                border: none;
                                font-size: 24px;
                                cursor: pointer;
                                color: #666;
                            ">×</button>
                        </div>
                    </div>

                    <div style="margin-bottom: 15px;">
                        <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
                            <label style="font-weight: bold;">屏蔽关键词:</label>
                            <span style="color: #666; font-size: 13px;">共 ${Config.BLOCKED_KEYWORDS.length} 个</span>
                        </div>
                        <div id="keyword-list" style="margin-bottom: 10px; max-height: 200px; overflow-y: auto; border: 1px solid #eee; border-radius: 4px; padding: 8px;">
                            ${this.renderKeywords()}
                        </div>

                        <div style="display: flex; gap: 10px;">
                            <input type="text" id="new-keyword" placeholder="输入关键词" style="
                                flex: 1;
                                padding: 8px;
                                border: 1px solid #ddd;
                                border-radius: 4px;
                            ">
                            <button id="add-keyword" style="
                                padding: 8px 16px;
                                background: #ff2442;
                                color: white;
                                border: none;
                                border-radius: 4px;
                                cursor: pointer;
                            ">添加</button>
                        </div>
                    </div>

                    <div style="margin-bottom: 15px;">
                        <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
                            <label style="font-weight: bold;">关键词统计:</label>
                            <button id="reset-stats" style="
                                padding: 4px 8px;
                                background: #ff9800;
                                color: white;
                                border: none;
                                border-radius: 4px;
                                cursor: pointer;
                                font-size: 12px;
                            ">清零</button>
                        </div>
                        <div id="keyword-stats" style="margin-bottom: 10px; max-height: 150px; overflow-y: auto; border: 1px solid #eee; border-radius: 4px; padding: 8px; font-size: 13px;">
                            ${this.renderKeywordStats()}
                        </div>
                    </div>

                    <div style="margin-bottom: 15px;">
                        <div style="display: flex; align-items: center; margin-bottom: 10px;">
                            <input type="checkbox" id="hide-titles-checkbox" ${Config.HIDE_TITLES ? 'checked' : ''} style="
                                margin-right: 8px;
                                width: 16px;
                                height: 16px;
                                cursor: pointer;
                                appearance: auto;
                                -webkit-appearance: checkbox;
                                -moz-appearance: checkbox;
                                opacity: 1;
                                visibility: visible;
                                position: static;
                            ">
                            <label for="hide-titles-checkbox" style="font-weight: bold; cursor: pointer;">
                                隐藏帖子标题
                            </label>
                        </div>
                        <div style="font-size: 12px; color: #666; margin-left: 24px;">
                            开启后将隐藏所有帖子的标题内容
                        </div>
                    </div>

                    <div style="display: flex; gap: 10px; margin-bottom: 15px;">
                        <button id="copy-keywords" style="
                            flex: 1;
                            padding: 8px 16px;
                            background: #2196f3;
                            color: white;
                            border: none;
                            border-radius: 4px;
                            cursor: pointer;
                        ">复制全部</button>
                        <button id="paste-keywords" style="
                            flex: 1;
                            padding: 8px 16px;
                            background: #4caf50;
                            color: white;
                            border: none;
                            border-radius: 4px;
                            cursor: pointer;
                        ">批量粘贴</button>
                    </div>
                    <div style="text-align: right;">
                        <button id="reset-keywords" style="
                            padding: 8px 16px;
                            background: #ff9800;
                            color: white;
                            border: none;
                            border-radius: 4px;
                            cursor: pointer;
                            margin-right: 10px;
                        ">重置默认</button>
                        <button id="close-panel" style="
                            padding: 8px 16px;
                            background: #666;
                            color: white;
                            border: none;
                            border-radius: 4px;
                            cursor: pointer;
                        ">关闭</button>
                    </div>
                </div>

                <div style="
                    position: fixed;
                    top: 0;
                    left: 0;
                    right: 0;
                    bottom: 0;
                    background: rgba(0,0,0,0.5);
                    z-index: 9999;
                "></div>
            `;

            document.body.appendChild(panel);

            // 自动聚焦到输入框
            setTimeout(() => {
                const input = panel.querySelector('#new-keyword');
                if (input) {
                    input.focus();
                    input.select();
                }
            }, 100);

            // 绑定事件
            panel.querySelector('#add-keyword').addEventListener('click', () => {
                const input = panel.querySelector('#new-keyword');
                const keyword = input.value.trim();
                if (keyword) {
                    const added = Config.addKeyword(keyword);
                    if (added) {
                        input.value = '';
                        panel.querySelector('#keyword-list').innerHTML = this.renderKeywords();
                        panel.querySelector('span[style*="color: #666"]').textContent = `共 ${Config.BLOCKED_KEYWORDS.length} 个`;

                        // 强制重新过滤所有帖子
                        window.xhsFilter.forceReFilter();

                        input.focus(); // 添加后重新聚焦
                    } else {
                        alert(`关键词 "${keyword}" 已存在!`);
                        input.focus(); // 错误提示后重新聚焦
                    }
                }
            });

            // 绑定删除关键词事件(使用事件委托)
            panel.querySelector('#keyword-list').addEventListener('click', (e) => {
                if (e.target.classList.contains('delete-keyword')) {
                    const keyword = e.target.dataset.keyword;
                    if (confirm(`确定要删除关键词 "${keyword}" 吗?`)) {
                        Config.removeKeyword(keyword);
                        // 重新渲染关键词列表
                        panel.querySelector('#keyword-list').innerHTML = this.renderKeywords();
                        // 更新计数
                        panel.querySelector('span[style*="color: #666"]').textContent = `共 ${Config.BLOCKED_KEYWORDS.length} 个`;
                        // 重新过滤
                        window.xhsFilter.forceReFilter();
                    }
                }
            });

            // 绑定可视化按钮事件
            panel.querySelector('#show-visualization').addEventListener('click', () => {
                VisualizationManager.createVisualizationPanel();
            });

            panel.querySelector('#close-panel').addEventListener('click', () => {
                panel.remove();
            });

            // 点击遮罩关闭
            panel.lastElementChild.addEventListener('click', () => {
                panel.remove();
            });

            // 重置默认关键词
            panel.querySelector('#reset-keywords').addEventListener('click', () => {
                if (confirm('确定要重置为默认关键词吗?这将清除所有自定义设置。')) {
                    Config.resetToDefault();
                    panel.querySelector('#keyword-list').innerHTML = this.renderKeywords();
                    location.reload();
                }
            });

            // 复制关键词功能
            panel.querySelector('#copy-keywords').addEventListener('click', () => {
                const keywords = Config.BLOCKED_KEYWORDS;
                if (keywords.length === 0) {
                    alert('暂无关键词可复制!');
                    return;
                }

                const keywordsText = keywords.join(',');
                navigator.clipboard.writeText(keywordsText).then(() => {
                    alert(`已复制 ${keywords.length} 个关键词到剪贴板!`);
                }).catch(() => {
                    // 降级方案
                    const textarea = document.createElement('textarea');
                    textarea.value = keywordsText;
                    textarea.style.position = 'fixed';
                    textarea.style.opacity = '0';
                    document.body.appendChild(textarea);
                    textarea.select();
                    document.execCommand('copy');
                    document.body.removeChild(textarea);
                    alert(`已复制 ${keywords.length} 个关键词到剪贴板!`);
                });
            });

            // 批量粘贴关键词功能
            panel.querySelector('#paste-keywords').addEventListener('click', () => {
                navigator.clipboard.readText().then(text => {
                    const keywords = text.split(/[,,、\s]+/).filter(k => k.trim());
                    if (keywords.length === 0) {
                        alert('剪贴板中没有有效的关键词!');
                        return;
                    }

                    let addedCount = 0;
                    keywords.forEach(keyword => {
                        if (Config.addKeyword(keyword)) {
                            addedCount++;
                        }
                    });

                    if (addedCount > 0) {
                        panel.querySelector('#keyword-list').innerHTML = this.renderKeywords();
                        panel.querySelector('span[style*="color: #666"]').textContent = `共 ${Config.BLOCKED_KEYWORDS.length} 个`;
                        alert(`成功添加 ${addedCount} 个新关键词!`);
                    } else {
                        alert('所有关键词都已存在,没有添加新的!');
                    }
                }).catch(() => {
                    // 降级方案:使用prompt输入
                    const text = prompt('请输入关键词,用逗号分隔:');
                    if (text) {
                        const keywords = text.split(/[,,、\s]+/).filter(k => k.trim());
                        let addedCount = 0;
                        keywords.forEach(keyword => {
                            if (Config.addKeyword(keyword)) {
                                addedCount++;
                            }
                        });

                        if (addedCount > 0) {
                            panel.querySelector('#keyword-list').innerHTML = this.renderKeywords();
                            panel.querySelector('span[style*="color: #666"]').textContent = `共 ${Config.BLOCKED_KEYWORDS.length} 个`;
                            alert(`成功添加 ${addedCount} 个新关键词!`);
                        } else {
                            alert('所有关键词都已存在,没有添加新的!');
                        }
                    }
                });
            });

            // 统计清零按钮
            panel.querySelector('#reset-stats').addEventListener('click', () => {
                if (confirm('确定要清零所有关键词的统计信息吗?')) {
                    Config.resetKeywordStats();
                    panel.querySelector('#keyword-stats').innerHTML = this.renderKeywordStats();
                    alert('统计信息已清零!');
                }
            });

            // 绑定隐藏标题复选框事件
            panel.querySelector('#hide-titles-checkbox').addEventListener('change', (e) => {
                Config.HIDE_TITLES = e.target.checked;
                window.xhsTitleHider.applyTitleHiding();
                console.log(`[小红书过滤器] 标题隐藏功能已${e.target.checked ? '启用' : '禁用'}`);
            });

            // 回车添加关键词
            panel.querySelector('#new-keyword').addEventListener('keypress', (e) => {
                if (e.key === 'Enter') {
                    panel.querySelector('#add-keyword').click();
                }
            });

            // ESC键退出编辑界面(使用捕获阶段确保优先级)
            const escHandler = (e) => {
                if (e.key === 'Escape') {
                    e.preventDefault();
                    e.stopPropagation();
                    panel.remove();
                    document.removeEventListener('keydown', escHandler, true);
                }
            };
            document.addEventListener('keydown', escHandler, true);

            // 点击面板外部关闭
            const clickHandler = (e) => {
                if (e.target === panel || e.target === panel.lastElementChild) {
                    panel.remove();
                    document.removeEventListener('keydown', escHandler, true);
                    document.removeEventListener('click', clickHandler);
                }
            };
            panel.addEventListener('click', clickHandler);

            // 确保ESC键在输入框中也能工作
            panel.querySelector('#new-keyword').addEventListener('keydown', (e) => {
                if (e.key === 'Escape') {
                    e.preventDefault();
                    e.stopPropagation();
                    panel.remove();
                    document.removeEventListener('keydown', escHandler, true);
                    document.removeEventListener('click', clickHandler);
                }
            });
        }

        static renderKeywords() {
            const keywords = Config.BLOCKED_KEYWORDS;
            if (keywords.length === 0) {
                return '<p style="color: #666; font-style: italic;">暂无屏蔽关键词</p>';
            }

            return `
                <div style="
                    display: flex;
                    flex-wrap: wrap;
                    gap: 8px;
                    margin-bottom: 10px;
                    max-height: 150px;
                    overflow-y: auto;
                    padding: 5px;
                ">
                    ${keywords.map(keyword => `
                        <span style="
                            display: inline-flex;
                            align-items: center;
                            background: #f0f0f0;
                            border: 1px solid #ddd;
                            border-radius: 16px;
                            padding: 4px 10px;
                            font-size: 13px;
                            color: #333;
                            cursor: default;
                            transition: all 0.2s;
                        " onmouseover="this.style.background='#ffe0e0'" onmouseout="this.style.background='#f0f0f0'">
                            ${keyword}
                            <span class="delete-keyword" data-keyword="${keyword.replace(/"/g, '"')}" style="
                                background: none;
                                border: none;
                                color: #ff2442;
                                cursor: pointer;
                                font-size: 16px;
                                margin-left: 6px;
                                padding: 0;
                                width: 16px;
                                height: 16px;
                                display: flex;
                                align-items: center;
                                justify-content: center;
                                user-select: none;
                            ">×</span>
                        </span>
                    `).join('')}
                </div>
            `;
        }

        static renderKeywordStats() {
            const stats = Config.getKeywordStats();
            const keywords = Config.BLOCKED_KEYWORDS;

            if (keywords.length === 0) {
                return '<p style="color: #666; font-style: italic;">暂无统计信息</p>';
            }

            // 按命中次数降序排序
            const sortedKeywords = keywords
                .map(keyword => ({ keyword, hits: stats[keyword] || 0 }))
                .sort((a, b) => b.hits - a.hits);

            // 计算最大命中次数用于比例显示
            const maxHits = Math.max(...sortedKeywords.map(item => item.hits), 1);

            let totalHits = 0;
            const statsHtml = sortedKeywords.map(item => {
                totalHits += item.hits;
                const percentage = (item.hits / maxHits) * 100;

                return `
                    <div style="display: flex; align-items: center; margin-bottom: 6px; gap: 8px;">
                        <span style="font-size: 12px; min-width: 60px; white-space: nowrap;">${item.keyword}</span>
                        <div style="flex: 1; background: #f5f5f5; border-radius: 10px; height: 6px; overflow: hidden;">
                            <div style="background: #8fb3d0; height: 100%; width: ${percentage}%; transition: width 0.3s ease;"></div>
                        </div>
                        <span style="font-size: 12px; color: #5a7d9a; font-weight: bold; min-width: 20px; text-align: right;">${item.hits}</span>
                    </div>
                `;
            }).join('');

            return `
                <div>
                    <div style="display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid #eee; margin-bottom: 6px; font-weight: bold; font-size: 13px;">
                        <span>总计</span>
                        <span style="color: #5a7d9a;">${totalHits}</span>
                    </div>
                    ${statsHtml}
                </div>
            `;
        }
    }

    // 键盘快捷键管理
    class KeyboardShortcuts {
        static init() {
            let isPanelOpen = false;

            document.addEventListener('keydown', (e) => {
                // 检测 Ctrl+Shift+K 或 Cmd+Shift+K
                if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'K') {
                    e.preventDefault();

                    // 检查是否已有面板打开
                    const existingPanel = document.getElementById('xhs-filter-panel');
                    if (existingPanel) {
                        existingPanel.remove();
                        isPanelOpen = false;
                    } else {
                        SettingsUI.createPanel();
                        isPanelOpen = true;
                    }
                }
            });

            console.log('[小红书过滤器] 快捷键已启用:Ctrl+Shift+K 或 Cmd+Shift+K');
        }
    }

    // 标题隐藏管理器
    class TitleHider {
        constructor() {
            this.observer = null;
            this.init();
        }

        init() {
            this.applyTitleHiding();
            this.setupMutationObserver();
        }

        applyTitleHiding() {
            const shouldHide = Config.HIDE_TITLES;
            const container = document.body;
            
            if (shouldHide) {
                container.classList.add('xhs-title-hidden');
                console.log('[小红书过滤器] 已启用标题隐藏功能');
            } else {
                container.classList.remove('xhs-title-hidden');
                console.log('[小红书过滤器] 已禁用标题隐藏功能');
            }
        }

        setupMutationObserver() {
            // 监听新内容加载
            this.observer = new MutationObserver((mutations) => {
                mutations.forEach(mutation => {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            // 如果启用了标题隐藏,对新加载的内容应用
                            if (Config.HIDE_TITLES) {
                                this.hideTitlesInNode(node);
                            }
                        }
                    });
                });
            });

            this.observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        }

        hideTitlesInNode(node) {
            if (!node || !node.querySelectorAll) return;
            
            // 查找并隐藏标题元素
            const titleSelectors = [
                '.title',
                '.footer .title',
                '[class*="title"]',
                'a[data-v-a264b01a].title',
                'span[data-v-51ec0135]'
            ];

            titleSelectors.forEach(selector => {
                const elements = node.querySelectorAll(selector);
                elements.forEach(el => {
                    if (el.textContent && el.textContent.trim()) {
                        el.style.display = 'none';
                        el.style.visibility = 'hidden';
                    }
                });
            });
        }

        toggleTitleHiding() {
            const newState = !Config.HIDE_TITLES;
            Config.HIDE_TITLES = newState;
            this.applyTitleHiding();
            
            // 更新设置面板中的复选框状态
            const checkbox = document.getElementById('hide-titles-checkbox');
            if (checkbox) {
                checkbox.checked = newState;
            }
        }
    }

    // 初始化
    function init() {
        // 创建全局过滤器实例
        window.xhsFilter = new PostFilter();
        
        // 创建标题隐藏管理器
        window.xhsTitleHider = new TitleHider();

            // 注册菜单命令
            GM_registerMenuCommand('设置过滤器', () => {
                SettingsUI.createPanel();
            });

            // 注册可视化菜单命令
            GM_registerMenuCommand('📊 数据可视化', () => {
                VisualizationManager.createVisualizationPanel();
            });

        // 注册键盘快捷键
        KeyboardShortcuts.init();

        // 添加全局移除关键词方法
        window.xhsFilter.removeKeyword = (keyword) => {
            Config.removeKeyword(keyword);
            // 刷新页面以重新过滤
            location.reload();
        };

        console.log('[小红书过滤器] 已启动');
    }

    // 启动脚本
    init();
})();