知乎推荐页标签屏蔽器 (Zhihu Tag Blocker)

以往都是根据标题关键词屏蔽, 效果不好, 经实验, 通过标签来屏蔽, 效果显著, 使用体验大大提升.

// ==UserScript==
// @name         知乎推荐页标签屏蔽器 (Zhihu Tag Blocker)
// @namespace    http://tampermonkey.net/
// @version      1.8
// @description  以往都是根据标题关键词屏蔽, 效果不好, 经实验, 通过标签来屏蔽, 效果显著, 使用体验大大提升.
// @author       ChatGPT
// @match        https://www.zhihu.com/
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @connect      www.zhihu.com
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const STORAGE_KEY_TAGS = 'ZhihuBlockedTags';
    const STORAGE_KEY_FUZZY = 'ZhihuFuzzyMatchEnabled';
    const STORAGE_KEY_BLOCK_ZHUANLAN = 'ZhihuBlockZhuanlan';
    const STORAGE_KEY_CONFIG = 'ZhihuBlockerConfig';
    const LOG_PREFIX = '【知乎标签屏蔽器】';

    const FEED_ITEM_SELECTOR = '.Card.TopstoryItem.TopstoryItem-isRecommend';
    const TITLE_LINK_SELECTOR = '.ContentItem-title a[data-za-detail-view-element_name="Title"]';
    const TAG_CONTENT_SELECTOR = '.QuestionHeader-tags .css-1gomreu';

    const tagCache = new Map();
    let isDarkMode = false;
    let menuCommands = [];

    function loadBlockedTags() {
        const tagsStr = GM_getValue(STORAGE_KEY_TAGS, '');
        return tagsStr ? tagsStr.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0) : [];
    }

    function saveBlockedTags(tags) {
        const uniqueTags = Array.from(new Set(tags.map(tag => tag.trim()).filter(tag => tag.length > 0)));
        GM_setValue(STORAGE_KEY_TAGS, uniqueTags.join(', '));
        console.log(`${LOG_PREFIX}已保存屏蔽标签:`, uniqueTags);
    }

    function isFuzzyMatchEnabled() {
        return GM_getValue(STORAGE_KEY_FUZZY, false);
    }

    function setFuzzyMatchEnabled(enabled) {
        GM_setValue(STORAGE_KEY_FUZZY, enabled);
    }

    function isBlockZhuanlanEnabled() {
        return GM_getValue(STORAGE_KEY_BLOCK_ZHUANLAN, false);
    }

    function setBlockZhuanlanEnabled(enabled) {
        GM_setValue(STORAGE_KEY_BLOCK_ZHUANLAN, enabled);
    }

    function detectDarkMode() {
        return document.documentElement.getAttribute('data-theme') === 'dark' ||
            window.matchMedia('(prefers-color-scheme: dark)').matches;
    }

    function getMenuStyles() {
        const baseStyles = {
            position: 'absolute',
            border: '1px solid #e1e4e8',
            borderRadius: '4px',
            boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
            padding: '5px 0',
            zIndex: '2000',
            minWidth: '150px',
            fontSize: '14px'
        };

        if (isDarkMode) {
            return {
                ...baseStyles,
                background: '#1a1a1a',
                borderColor: '#434343',
                color: '#e6e6e6'
            };
        } else {
            return {
                ...baseStyles,
                background: 'white',
                color: '#175199'
            };
        }
    }

    function getMenuItemStyles() {
        const baseStyles = {
            padding: '5px 10px',
            cursor: 'pointer',
            whiteSpace: 'nowrap'
        };

        if (isDarkMode) {
            return {
                ...baseStyles,
                hoverBackground: '#2d2d2d'
            };
        } else {
            return {
                ...baseStyles,
                hoverBackground: '#f6f6f6'
            };
        }
    }

    function registerMenuCommands() {
        // 清除之前注册(不可用)的命令
        menuCommands.forEach(cmd => {
            try {
                // Tampermonkey 没有直接删除命令的API,这里主要是为了重置数组
            } catch (e) {}
        });
        menuCommands = [];

        const isFuzzy = isFuzzyMatchEnabled();
        const blockZhuanlan = isBlockZhuanlanEnabled();

        menuCommands.push(GM_registerMenuCommand("🏷️ 查看/编辑屏蔽标签", editBlockedTags));
        menuCommands.push(GM_registerMenuCommand(`🔍 模糊匹配: ${isFuzzy ? '✅ 开启' : '❌ 关闭'}`, toggleFuzzyMatch));
        menuCommands.push(GM_registerMenuCommand(`📰 屏蔽知乎专栏/文章: ${blockZhuanlan ? '✅ 开启' : '❌ 关闭'}`, toggleBlockZhuanlan));
        menuCommands.push(GM_registerMenuCommand("📤 导出配置", exportConfig));
        menuCommands.push(GM_registerMenuCommand("📥 导入配置", importConfig));
    }

    function editBlockedTags() {
        const currentTags = loadBlockedTags().join(', ');
        const newTagsStr = prompt(
            `${LOG_PREFIX} 请输入要屏蔽的标签,多个标签用逗号分隔:\n\n注意:如果启用了模糊匹配,标签将会作为关键词进行匹配(同时匹配标签和标题)。`,
            currentTags
        );

        if (newTagsStr !== null) {
            const newTags = newTagsStr.split(',').map(tag => tag.trim());
            saveBlockedTags(newTags);
            showStatusMessage(`已更新屏蔽标签: ${newTags.length}个`);
            checkAndHideAllItems();
        }
    }

    function toggleFuzzyMatch() {
        const currentStatus = isFuzzyMatchEnabled();
        const newStatus = !currentStatus;
        setFuzzyMatchEnabled(newStatus);

        showStatusMessage(`模糊匹配 ${newStatus ? '✅ 已开启' : '❌ 已关闭'}`);
        registerMenuCommands(); // 刷新菜单
        checkAndHideAllItems();
    }

    function toggleBlockZhuanlan() {
        const currentStatus = isBlockZhuanlanEnabled();
        const newStatus = !currentStatus;
        setBlockZhuanlanEnabled(newStatus);

        showStatusMessage(`屏蔽知乎专栏/文章 ${newStatus ? '✅ 已开启' : '❌ 已关闭'}`);
        registerMenuCommands(); // 刷新菜单
        checkAndHideAllItems();
    }

    // 添加状态提示函数
    function showStatusMessage(message) {
        // 移除已有的提示
        const existingMsg = document.getElementById('zh-tag-blocker-status');
        if (existingMsg) existingMsg.remove();

        const statusMsg = document.createElement('div');
        statusMsg.id = 'zh-tag-blocker-status';
        statusMsg.textContent = `${LOG_PREFIX}${message}`;
        statusMsg.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            background: ${isDarkMode ? '#1a1a1a' : '#fff'};
            color: ${isDarkMode ? '#fff' : '#000'};
            border: 1px solid ${isDarkMode ? '#434343' : '#e1e4e8'};
            padding: 10px 15px;
            border-radius: 4px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.2);
            z-index: 10000;
            font-size: 14px;
            max-width: 300px;
            word-wrap: break-word;
        `;

        document.body.appendChild(statusMsg);

        // 3秒后自动消失
        setTimeout(() => {
            if (statusMsg.parentNode) {
                statusMsg.parentNode.removeChild(statusMsg);
            }
        }, 3000);
    }

    function exportConfig() {
        const config = {
            blockedTags: loadBlockedTags(),
            fuzzyMatch: isFuzzyMatchEnabled(),
            blockZhuanlan: isBlockZhuanlanEnabled(),
            exportTime: new Date().toISOString()
        };

        const configStr = JSON.stringify(config, null, 2);
        const blob = new Blob([configStr], { type: 'application/json' });
        const url = URL.createObjectURL(blob);

        const a = document.createElement('a');
        a.href = url;
        a.download = `zhihu-tag-blocker-config-${new Date().toISOString().split('T')[0]}.json`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);

        showStatusMessage('配置导出成功!');
    }

    function importConfig() {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = '.json';

        input.onchange = function(event) {
            const file = event.target.files[0];
            if (!file) return;

            const reader = new FileReader();
            reader.onload = function(e) {
                try {
                    const config = JSON.parse(e.target.result);

                    if (!Array.isArray(config.blockedTags) || typeof config.fuzzyMatch !== 'boolean') {
                        throw new Error('无效的配置文件格式');
                    }

                    if (confirm(`${LOG_PREFIX}是否导入配置?\n屏蔽标签: ${config.blockedTags.length}个\n模糊匹配: ${config.fuzzyMatch ? '开启' : '关闭'}\n屏蔽专栏: ${config.blockZhuanlan ? '开启' : '关闭'}`)) {
                        saveBlockedTags(config.blockedTags);
                        setFuzzyMatchEnabled(config.fuzzyMatch);
                        setBlockZhuanlanEnabled(config.blockZhuanlan || false);
                        registerMenuCommands();
                        checkAndHideAllItems();
                        showStatusMessage('配置导入成功!');
                    }
                } catch (error) {
                    alert(`${LOG_PREFIX}配置文件解析失败: ${error.message}`);
                }
            };
            reader.readAsText(file);
        };

        input.click();
    }

    function checkBlockHit(tags, title, blockedTags, fuzzyEnabled) {
        const matchedTags = [];
        let isBlocked = false;

        if (fuzzyEnabled) {
            // 模糊匹配:同时检查标签和标题
            for (const blockedWord of blockedTags) {
                if (!blockedWord) continue;
                
                // 检查标签
                let tagMatched = false;
                for (const tag of tags) {
                    if (tag.includes(blockedWord)) {
                        tagMatched = true;
                        isBlocked = true;
                        if (!matchedTags.includes(blockedWord)) matchedTags.push(blockedWord);
                        break;
                    }
                }
                
                // 如果标签没匹配到,检查标题
                if (!tagMatched && title && title.includes(blockedWord)) {
                    isBlocked = true;
                    if (!matchedTags.includes(blockedWord)) matchedTags.push(blockedWord);
                }
            }
        } else {
            // 精确匹配:只检查标签
            for (const blockedTag of blockedTags) {
                if (tags.includes(blockedTag)) {
                    isBlocked = true;
                    matchedTags.push(blockedTag);
                }
            }
        }

        return { isBlocked, matchedTags };
    }

    function isZhuanlanLink(link) {
        return link && (link.includes('zhuanlan.zhihu.com') || link.includes('//zhuanlan.zhihu.com/'));
    }

    function isZhuanlanArticle(feedItem) {
        // 方法1: 检查链接是否包含zhuanlan
        const titleLink = feedItem.querySelector(TITLE_LINK_SELECTOR);
        if (titleLink) {
            const href = titleLink.getAttribute('href');
            if (isZhuanlanLink(href)) {
                return true;
            }
        }

        // 方法2: 检查是否有专栏特定的类名或结构
        const articleItem = feedItem.querySelector('.ContentItem.ArticleItem');
        if (articleItem) {
            return true;
        }

        // 方法3: 检查是否有专栏特定的元数据 (可能不准确,但保留作为参考)
        const metaElement = feedItem.querySelector('[data-za-extra-module*="专栏"]');
        if (metaElement) {
            return true;
        }

        return false;
    }

    /**
     * 在标题前添加 [文章] 或 [回答] 标记
     * @param {HTMLElement} feedItem - 整个信息流卡片元素
     * @param {HTMLElement} titleLink - 标题链接元素
     * @param {boolean} isArticle - 是否为专栏文章
     */
    function createTypeTag(feedItem, titleLink, isArticle) {
        const existingTag = feedItem.querySelector('.zh-tag-type');
        if (existingTag) return;

        const tag = document.createElement('span');
        tag.className = 'zh-tag-type';
        const typeText = isArticle ? '文章' : '回答';
        // 专栏文章使用橙色,回答使用知乎默认蓝色 (或深色模式下的白色)
        const bgColor = isArticle ? '#ff9900' : (isDarkMode ? '#2d2d2d' : '#175199');
        const color = isArticle ? 'white' : (isDarkMode ? '#e6e6e6' : 'white');

        tag.textContent = `[${typeText}]`;
        tag.style.cssText = `
            margin-right: 8px;
            padding: 2px 6px;
            background-color: ${bgColor};
            color: ${color};
            border-radius: 4px;
            font-size: 12px;
            font-weight: normal;
            vertical-align: middle;
            display: inline-block;
            white-space: nowrap;
        `;

        const titleWrapper = titleLink.parentElement;
        if (titleWrapper) {
            // 插入到标题链接之前
            titleWrapper.insertBefore(tag, titleLink);
        }
    }

    function fetchQuestionTags(questionUrl) {
        if (tagCache.has(questionUrl)) {
            return Promise.resolve(tagCache.get(questionUrl));
        }

        const fullUrl = questionUrl.startsWith('http') ? questionUrl : `https:${questionUrl}`;

        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: fullUrl,
                onload: function(response) {
                    if (response.status !== 200) {
                        tagCache.set(questionUrl, []);
                        return resolve([]);
                    }

                    try {
                        const parser = new DOMParser();
                        const doc = parser.parseFromString(response.responseText, "text/html");
                        const tags = Array.from(doc.querySelectorAll(TAG_CONTENT_SELECTOR))
                        .map(el => el.textContent.trim());
                        tagCache.set(questionUrl, tags);
                        resolve(tags);
                    } catch (e) {
                        tagCache.set(questionUrl, []);
                        resolve([]);
                    }
                },
                onerror: function() {
                    tagCache.set(questionUrl, []);
                    resolve([]);
                }
            });
        });
    }

    async function checkAndHideItem(feedItem) {
        if (feedItem.dataset.zhTagsProcessed === 'true') {
            applyHideRule(feedItem);
            return;
        }

        const titleLink = feedItem.querySelector(TITLE_LINK_SELECTOR);
        if (!titleLink) {
            feedItem.dataset.zhTagsProcessed = 'true';
            return;
        }

        const questionUrl = titleLink.getAttribute('href');
        if (!questionUrl) {
            feedItem.dataset.zhTagsProcessed = 'true';
            return;
        }

        // 1. 检查是否为知乎专栏/文章,并添加标记
        const isArticle = isZhuanlanArticle(feedItem);
        createTypeTag(feedItem, titleLink, isArticle);

        // 2. 检查是否开启了屏蔽专栏并隐藏
        if (isBlockZhuanlanEnabled() && isArticle) {
            if (feedItem.style.display !== 'none') {
                feedItem.style.display = 'none';
                console.log(`${LOG_PREFIX}已隐藏知乎专栏/文章: "${titleLink.textContent}"`);
            }
            feedItem.dataset.zhTagsProcessed = 'true';
            return;
        }

        // 3. 对于非专栏内容或未开启屏蔽专栏时,继续获取标签并根据标签屏蔽
        const tags = await fetchQuestionTags(questionUrl);
        feedItem.dataset.zhQuestionTags = JSON.stringify(tags);
        applyHideRule(feedItem, titleLink, tags);
        createBlockerIcon(feedItem, titleLink, tags);
        feedItem.dataset.zhTagsProcessed = 'true';
    }

    function applyHideRule(feedItem, titleLink = null, tags = null) {
        // 如果内容是文章且未被屏蔽,则确保它显示
        const isArticle = isZhuanlanArticle(feedItem);
        if (isArticle) {
            if (isBlockZhuanlanEnabled()) {
                 // 如果已开启屏蔽,则保持隐藏(理论上在 checkAndHideItem 中已处理,这里是二次保险)
                feedItem.style.display = 'none';
                return;
            } else {
                // 如果未开启屏蔽,则显示
                feedItem.style.display = '';
                return;
            }
        }

        // 非文章内容,按标签规则处理
        if (!tags) {
            try {
                tags = JSON.parse(feedItem.dataset.zhQuestionTags || '[]');
            } catch (e) {
                tags = [];
            }
        }

        if (!titleLink) {
            titleLink = feedItem.querySelector(TITLE_LINK_SELECTOR);
        }

        const titleText = titleLink ? titleLink.textContent : '';

        const blockedTags = loadBlockedTags();
        const fuzzyEnabled = isFuzzyMatchEnabled();
        const { isBlocked, matchedTags } = checkBlockHit(tags, titleText, blockedTags, fuzzyEnabled);

        if (isBlocked && feedItem.style.display !== 'none') {
            feedItem.style.display = 'none';
            console.log(`${LOG_PREFIX}已隐藏问题 "${titleText}" (匹配模式: ${fuzzyEnabled ? '模糊' : '精确'}, 命中词: ${matchedTags.join(', ')})`);
        } else if (!isBlocked) {
            feedItem.style.display = '';
        }
    }

    function createBlockerIcon(feedItem, titleLink, tags) {
        if (feedItem.querySelector('.zh-tag-blocker-icon')) {
            return;
        }

        const icon = document.createElement('span');
        icon.className = 'zh-tag-blocker-icon';
        icon.innerHTML = '🏷️';
        icon.style.cssText = `
            margin-left: 8px;
            cursor: pointer;
            font-size: 14px;
            color: #8590a6;
            vertical-align: top;
            line-height: 1.5;
            z-index: 1000;
        `;
        icon.title = '点击显示/编辑屏蔽标签菜单';

        const titleWrapper = titleLink.parentElement;
        // 确保在类型标记之后,标题链接之后插入
        const referenceNode = titleWrapper.querySelector('.zh-tag-type') ? titleLink.nextSibling : titleLink;
        if (titleWrapper) {
            titleWrapper.appendChild(icon);
        }

        icon.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            showTagContextMenu(icon, tags);
        });
    }

    function showTagContextMenu(targetElement, tags) {
        document.querySelectorAll('.zh-tag-blocker-menu').forEach(menu => menu.remove());

        const menu = document.createElement('div');
        menu.className = 'zh-tag-blocker-menu';

        const styles = getMenuStyles();
        Object.assign(menu.style, styles);

        const blockedTags = loadBlockedTags();
        const itemStyles = getMenuItemStyles();

        if (tags.length === 0) {
            const item = document.createElement('div');
            item.textContent = '未找到标签';
            Object.assign(item.style, {
                padding: '5px 10px',
                color: '#8590a6',
                fontSize: '12px'
            });
            menu.appendChild(item);
        } else {
            tags.forEach(tag => {
                const isBlocked = blockedTags.includes(tag);
                const item = document.createElement('div');
                item.textContent = `${isBlocked ? '🚫 (取消屏蔽)' : '➕ (添加屏蔽)'} ${tag}`;
                Object.assign(item.style, itemStyles);
                item.style.color = isBlocked ? 'red' : (isDarkMode ? '#e6e6e6' : '#175199');

                item.addEventListener('mouseover', () => {
                    item.style.backgroundColor = itemStyles.hoverBackground;
                });
                item.addEventListener('mouseout', () => {
                    item.style.backgroundColor = 'transparent';
                });

                item.addEventListener('click', (e) => {
                    e.stopPropagation();
                    let updatedTags = loadBlockedTags();

                    const currentlyBlocked = updatedTags.includes(tag);
                    if (currentlyBlocked) {
                        updatedTags = updatedTags.filter(t => t !== tag);
                    } else {
                        updatedTags.push(tag);
                    }

                    saveBlockedTags(updatedTags);
                    checkAndHideAllItems();

                    showStatusMessage(`${currentlyBlocked ? '取消屏蔽' : '添加屏蔽'}标签: ${tag}`);

                    const newIsBlocked = !currentlyBlocked;
                    item.textContent = `${newIsBlocked ? '🚫 (取消屏蔽)' : '➕ (添加屏蔽)'} ${tag}`;
                    item.style.color = newIsBlocked ? 'red' : (isDarkMode ? '#e6e6e6' : '#175199');
                });

                menu.appendChild(item);
            });
        }

        const rect = targetElement.getBoundingClientRect();
        menu.style.top = `${rect.bottom + window.scrollY + 5}px`;
        menu.style.left = `${rect.left + window.scrollX}px`;

        document.body.appendChild(menu);

        const closeMenu = (e) => {
            if (!menu.contains(e.target) && e.target !== targetElement) {
                menu.remove();
                document.removeEventListener('click', closeMenu);
            }
        };
        setTimeout(() => document.addEventListener('click', closeMenu), 0);
    }

    function checkAndHideAllItems() {
        document.querySelectorAll(FEED_ITEM_SELECTOR).forEach(item => {
            // 强制重新处理所有未处理或需要重新应用规则的项
            item.dataset.zhTagsProcessed = 'false';
            checkAndHideItem(item);
        });
    }

    function initObserver() {
        const targetNode = document.querySelector('#root') || document.body;

        if (!targetNode) return;

        const observer = new MutationObserver((mutationsList) => {
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === 1 && node.matches(FEED_ITEM_SELECTOR)) {
                            checkAndHideItem(node);
                        } else if (node.nodeType === 1 && node.querySelector(FEED_ITEM_SELECTOR)) {
                            node.querySelectorAll(FEED_ITEM_SELECTOR).forEach(item => checkAndHideItem(item));
                        }
                    }
                }
            }
        });

        observer.observe(targetNode, { childList: true, subtree: true });
    }

    function main() {
        isDarkMode = detectDarkMode();
        registerMenuCommands();
        checkAndHideAllItems();
        initObserver();

        console.log(`${LOG_PREFIX}脚本已加载 - 版本 1.8`);
        console.log(`${LOG_PREFIX}当前设置: 模糊匹配 ${isFuzzyMatchEnabled() ? '开启' : '关闭'}, 屏蔽专栏 ${isBlockZhuanlanEnabled() ? '开启' : '关闭'}`);
    }

    main();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址