Linux DO 关键词过滤器

根据关键词智能过滤 Linux Do 论坛上的水贴,支持 SPA 页面切换

// ==UserScript==
// @name         Linux DO 关键词过滤器
// @namespace    http://tampermonkey.net/
// @version      0.5
// @description  根据关键词智能过滤 Linux Do 论坛上的水贴,支持 SPA 页面切换
// @author       whiteSnow
// @match        https://linux.do/*
// @match        https://*.discourse.org/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 序列化处理(解决正则表达式无法直接存储的问题)
    function serializeKeywordGroups(groups) {
        return JSON.stringify(groups, (key, value) => {
            if (value instanceof RegExp) {
                return { __type: 'RegExp', source: value.source, flags: value.flags };
            }
            return value;
        });
    }

    // 反序列化处理
    function deserializeKeywordGroups(jsonStr) {
        return JSON.parse(jsonStr, (key, value) => {
            if (value?.__type === 'RegExp') {
                return new RegExp(value.source, value.flags);
            }
            return value;
        });
    }
    // 关键词分组配置 - 使用更精确的匹配模式
    const DEFAULT_KEYWORD_GROUPS = {
        '水贴': [
            { words: ['恭喜', '通过',"赞",'好'], minCount: 1,maxLength:10 },  // 同时包含这些词中至少2个
            { words: ['骚扰'], context: ['欢迎', '邮箱'],maxLength:10 },  // 包含"骚扰"且上下文中有"欢迎"或"邮箱"
            { words: ['申请'], context: ['通过', '审核', '等待'], maxLength: 10 },  // 包含"申请"且上下文有相关词,且内容较短
            { exact: '期待ing' },  // 精确匹配
            { regex: /\b申请.*?(通过|成功)\b/i,maxLength:10 }  // 正则匹配
        ],
        '感谢':[
            { words: ['感谢','谢谢'],maxLength:15},
        ],
        '大佬':[
            { words: ['大佬','太强了','厉害','牛逼','牛','大佬牛逼','大佬厉害','大佬牛','tql','666'],maxLength:15},
        ],
        '打卡':[
            { words: ['打卡','签到','签个到','打个卡','来了','来来来','前排','支持','围观'],maxLength:15},
            // { words: ['Mark', 'mark', '马克'],maxLength:10},   //添加mark,by snowsoul
            { regex: /\b(mark|马克)\b/i,maxLength:10},   //添加mark,by snowsoul
        ],
        '技术': [
            { words: ['代码', '问题'], minCount: 1, context: ['解决', '实现', '如何'],maxLength:20 },
            { words: ['教程', '学习'], context: ['方法', '步骤'] ,maxLength:50},
            { regex: /\b(bug|error|exception|failed)\b/i,maxLength:20 }
        ],
    };

    let keywordGroups = DEFAULT_KEYWORD_GROUPS;
    if (!GM_getValue("defaultKeywordGroups")) {
        GM_setValue("defaultKeywordGroups", serializeKeywordGroups(DEFAULT_KEYWORD_GROUPS));
    }
    
    // 添加过滤统计面板样式
    GM_addStyle(`
        #filter-stats {
            position: fixed;
            top: 70px;
            left: 20px;
            background: #fff;
            border: 1px solid #ddd;
            border-radius: 5px;
            padding: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            z-index: 1000;
            font-size: 14px;
            min-width: 180px;
        }
        #filter-stats h3 {
            margin: 0 0 10px 0;
            font-size: 16px;
            border-bottom: 1px solid #eee;
            padding-bottom: 5px;
        }
        .filter-group {
            display: flex;
            justify-content: space-between;
            margin-bottom: 5px;
        }
        .filter-count {
            font-weight: bold;
            color: #e45735;
        }
        .filter-toggle {
            cursor: pointer;
            user-select: none;
            display: block;
            margin-top: 10px;
            text-align: center;
            color: #0088cc;
        }
        .filter-settings {
            cursor: pointer;
            user-select: none;
            display: block;
            margin-top: 5px;
            text-align: center;
            color: #0088cc;
            font-size: 12px;
        }
        .filtered-post {
            opacity: 0.4;
            position: relative;
        }
        .filtered-post::before {
            content: attr(data-filter-reason);
            position: absolute;
            top: 10px;
            right: 10px;
            background: rgba(228, 87, 53, 0.8);
            color: white;
            padding: 2px 8px;
            border-radius: 3px;
            font-size: 12px;
            z-index: 10;
        }
        .filtered-post.hidden {
            display: none !important;
        }
        .confidence-high::before {
            background: rgba(228, 87, 53, 0.9);
        }
        .confidence-medium::before {
            background: rgba(255, 152, 0, 0.9);
        }
        .confidence-low::before {
            background: rgba(158, 158, 158, 0.9);
        }
        #filter-settings-panel {
            display: none;
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.2);
            z-index: 1001;
            width: 400px;
            max-height: 80vh;
            overflow-y: auto;
        }
        .settings-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
        }
        .settings-close {
            cursor: pointer;
            font-size: 20px;
        }
        .settings-group {
            margin-bottom: 15px;
            border-bottom: 1px solid #eee;
            padding-bottom: 10px;
        }
        .settings-group-title {
            font-weight: bold;
            margin-bottom: 8px;
        }
        .settings-threshold {
            margin-top: 15px;
        }
        .settings-threshold label {
            display: block;
            margin-bottom: 5px;
        }
        .settings-actions {
            margin-top: 15px;
            text-align: right;
        }
        .settings-actions button {
            padding: 5px 15px;
            margin-left: 10px;
            border-radius: 4px;
            border: 1px solid #ddd;
            background: #f5f5f5;
            cursor: pointer;
        }
        .settings-actions button.save {
            background: #0088cc;
            color: white;
            border-color: #0088cc;
        }
        #filter-debug {
            position: fixed;
            bottom: 10px;
            right: 10px;
            background: rgba(0,0,0,0.7);
            color: white;
            padding: 5px 10px;
            border-radius: 3px;
            font-size: 12px;
            z-index: 1001;
            display: none;
        }
        #filter-stats {
            position: fixed;
            cursor: default;
        }
        #filter-stats h3 {
            cursor: grab;
            padding: 8px 12px; /* 增加点击区域 */
            margin: 0;
        }
        #filter-stats h3:active {
            cursor: grabbing;
        }
        /* 新增圆形按钮样式 */
        .filter-toggle-button {
            position: fixed;
            width: 40px;
            height: 40px;
            border-radius: 50%;
            background: rgb(209.1, 239.7, 255);
            cursor: move;
            box-shadow: 0 2px 10px rgba(0,0,0,0.2);
            display: flex;
            align-items: center;
            justify-content: center;
            color: black;
            font-size: 14px;
            z-index: 9999;
            transition: transform 0.2s;
        }
        .filter-toggle-button:hover {
            transform: scale(1.1);
        }

        /* 调整统计面板为弹出式 */
        #filter-stats {
            display: none;
            position: fixed;
            min-width: 250px;
            z-index: 1001;
            border-radius: 8px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.2);
        }
        #filter-stats.visible {
            display: block;
            animation: fadeIn 0.3s;
        }
        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(-10px); }
            to { opacity: 1; transform: translateY(0); }
        }
        
    `);
    // 修改最后三个属性的样式,因为使用 fixed 定位,故需要修改为 left/top ,方便计算位置


// 修改创建面板的函数
function createFilterStatsPanel() {
    // 创建圆形按钮
    const toggleBtn = document.createElement('div');
    toggleBtn.className = 'filter-toggle-button';
    toggleBtn.innerHTML = '滤';
    document.body.appendChild(toggleBtn);

    // 获取当前隐藏状态
    const hideFiltered = GM_getValue('hideFiltered', false);

    // 创建统计面板
    const panel = document.createElement('div');
    panel.id = 'filter-stats';
    // panel.innerHTML = `
    //     <h3>关键词过滤统计</h3>
    //     <div id="filter-groups"></div>
    //     <div class="filter-settings" id="open-settings">过滤设置</div>
    // `;
    panel.innerHTML = `
        <h3>关键词过滤统计</h3>
        <div id="filter-groups"></div>
        <div class="filter-toggle" id="toggle-filter">${hideFiltered ? '显示' : '隐藏'}已过滤帖子</div>
        <div class="filter-settings" id="open-settings">过滤设置</div>
    `;
    // 位置计算逻辑
    function calculatePosition(btnRect) {
        const panelWidth = 250;
        const panelHeight = 200;
        const viewportWidth = window.innerWidth;
        const viewportHeight = window.innerHeight;

        // 左下显示
        if (btnRect.left + panelWidth + 60 > viewportWidth - 20 && btnRect.top + panelHeight < viewportHeight - 20) {
            return {
                left: btnRect.left - panelWidth - 10,
                top: btnRect.top
            };
        }
        // 左上显示
        else if (btnRect.left + panelWidth + 60 > viewportWidth - 20 && btnRect.top + panelHeight > viewportHeight - 20) {
            return {
                left: btnRect.left - panelWidth - 10,
                top: btnRect.top - panelHeight - 10
            };
        }
        // 右上显示
        else if (btnRect.top + panelHeight > viewportHeight - 20 ) {
            return {
                left: btnRect.left + btnRect.width + 10,
                top: btnRect.top - panelHeight - 10
            };
        }
        // 其他情况右下显示
        else {
            return {
                left: btnRect.left + btnRect.width + 10,
                top: btnRect.top
            };
        }

    }

    // 点击按钮切换面板
    toggleBtn.addEventListener('click', function(e) {
        e.stopPropagation();
        const isVisible = panel.classList.contains('visible');
        
        if (!isVisible) {
            const btnRect = toggleBtn.getBoundingClientRect();
            const pos = calculatePosition(btnRect);
            
            panel.style.left = `${pos.left}px`;
            panel.style.top = `${pos.top}px`;
            panel.classList.add('visible');
        } else {
            panel.classList.remove('visible');
        }
    });

    // 点击外部区域关闭面板
    // document.addEventListener('click', function(e) {
    //     if (!panel.contains(e.target) && !toggleBtn.contains(e.target)) {
    //         panel.classList.remove('visible');
    //     }
    // });

    // 保持原有拖动功能(修改为拖动按钮)
    let isDragging = false;
    let startX, startY, initialX, initialY;

    toggleBtn.addEventListener('mousedown', startDrag);
    document.addEventListener('mousemove', drag);
    document.addEventListener('mouseup', stopDrag);

    // 恢复保存的位置
    const savedPosition = GM_getValue('buttonPosition', { x: '20px', y: '70px' });
    toggleBtn.style.left = savedPosition.x;
    toggleBtn.style.top = savedPosition.y;

    function startDrag(e) {
        isDragging = true;
        startX = e.clientX;
        startY = e.clientY;
        initialX = toggleBtn.offsetLeft;
        initialY = toggleBtn.offsetTop;
        toggleBtn.style.cursor = 'grabbing';
    }

    function drag(e) {
        if (!isDragging) return;

        const dx = e.clientX - startX;
        const dy = e.clientY - startY;

        const newX = Math.max(0, Math.min(window.innerWidth - toggleBtn.offsetWidth, initialX + dx));
        const newY = Math.max(0, Math.min(window.innerHeight - toggleBtn.offsetHeight, initialY + dy));

        toggleBtn.style.left = `${newX}px`;
        toggleBtn.style.top = `${newY}px`;
    }

    function stopDrag() {
        isDragging = false;
        toggleBtn.style.cursor = 'pointer';
        GM_setValue('buttonPosition', {
            x: toggleBtn.style.left,
            y: toggleBtn.style.top
        });
    }

    document.body.appendChild(panel);

    // 保持原有设置面板和过滤功能...

    // 添加切换过滤显示/隐藏的事件
    document.getElementById('toggle-filter').addEventListener('click', function() {
        const hideFiltered = !GM_getValue('hideFiltered', false);
        GM_setValue('hideFiltered', hideFiltered);

        // 更新按钮文本
        this.textContent = hideFiltered ? '显示已过滤帖子' : '隐藏已过滤帖子';

        // 应用隐藏/显示状态
        applyFilterVisibility();
    });

    // 添加设置面板打开事件
    document.getElementById('open-settings').addEventListener('click', function() {
        openSettingsPanel();
    });

    // 创建调试面板
    const debugPanel = document.createElement('div');
    debugPanel.id = 'filter-debug';
    document.body.appendChild(debugPanel);

}
    

// 应用过滤帖子的隐藏/显示状态
function applyFilterVisibility() {
    const hideFiltered = GM_getValue('hideFiltered', false);
    const filteredPosts = document.querySelectorAll('.filtered-post');

    filteredPosts.forEach(post => {
        if (hideFiltered) {
            post.classList.add('hidden');
        } else {
            post.classList.remove('hidden');
        }
    });
}

    // 创建设置面板
function createSettingsPanel() {
    // 如果已存在,则不重复创建
    if (document.getElementById('filter-settings-panel')) return document.getElementById('filter-settings-panel');

    const panel = document.createElement('div');
    panel.id = 'filter-settings-panel';

    let groupsHtml = '';
    for (const group in keywordGroups) {
        // 获取该组的启用状态
        const enabled = GM_getValue(`enable-${group}`, true);

        groupsHtml += `
            <div class="settings-group">
                <div class="settings-group-title">
                    <input type="checkbox" id="enable-${group}" ${enabled ? 'checked' : ''}>
                    <label for="enable-${group}">${group}</label>
                </div>
                <div class="group-rules" id="rules-${group}">
                    <div class="rule-count">${keywordGroups[group].length}条规则</div>
                    <button class="edit-rules" data-group="${group}">编辑规则</button>
                </div>
            </div>
        `;
    }

    panel.innerHTML = `
        <div class="settings-header">
            <h3>过滤设置</h3>
            <span class="settings-close" id="close-settings">×</span>
        </div>
        <div class="settings-tabs">
            <div class="tab active" data-tab="general">常规设置</div>
            <div class="tab" data-tab="groups">分组管理</div>
            <div class="tab" data-tab="advanced">高级设置</div>
        </div>
        <div class="tab-content" id="tab-general">
            <div class="settings-option">
                <label>
                    <input type="checkbox" id="auto-hide-filtered" ${GM_getValue('hideFiltered', false) ? 'checked' : ''}>
                    自动隐藏已过滤的帖子
                </label>
            </div>
            <div class="settings-length-threshold">
                <label for="filter-length-threshold">长度阈值 (0-100):</label>
                <input type="range" id="filter-length-threshold" min="0" max="100" value="${GM_getValue('filterLengthThreshold', 20)}" style="width: 100%">
                <div class="threshold-display">
                    <span id="length-threshold-value">${GM_getValue('filterLengthThreshold', 20)}</span>
                    <span class="threshold-description">
                        (只要帖子长度低于该值, 且置信度较低,就会被过滤)
                    </span>
                </div>
            </div>
            <div class="settings-threshold">
                <label for="confidence-threshold">置信度阈值 (0-100):</label>
                <input type="range" id="confidence-threshold" min="0" max="100" value="${GM_getValue('confidenceThreshold', 60)}" style="width: 100%">
                <div class="threshold-display">
                    <span id="threshold-value">${GM_getValue('confidenceThreshold', 60)}</span>
                    <span class="threshold-description">
                        (较低的值会过滤更多帖子,但可能误判;较高的值过滤更精确,但可能遗漏)
                    </span>
                </div>
            </div>
        </div>
        <div class="tab-content" id="tab-groups" style="display:none">
            ${groupsHtml}
            <div class="add-group">
                <button id="add-new-group">+ 添加新分组</button>
            </div>
        </div>
        <div class="tab-content" id="tab-advanced" style="display:none">
            <div class="settings-option">
                <label>
                    <input type="checkbox" id="enable-debug" ${GM_getValue('enableDebug', false) ? 'checked' : ''}>
                    启用调试模式
                </label>
            </div>
            <div class="settings-option">
                <label>
                    <input type="checkbox" id="highlight-only" ${GM_getValue('highlightOnly', false) ? 'checked' : ''}>
                    仅高亮不隐藏(覆盖自动隐藏设置)
                </label>
            </div>
            <div class="export-import">
                <button id="export-settings">导出设置</button>
                <button id="import-settings">导入设置</button>
            </div>
        </div>
        <div class="settings-actions">
            <button id="reset-settings">重置</button>
            <button id="save-settings" class="save">保存</button>
        </div>
    `;

    document.body.appendChild(panel);

    // 添加事件监听
    document.getElementById('close-settings').addEventListener('click', closeSettingsPanel);
    document.getElementById('filter-length-threshold').addEventListener('input', function() {
        document.getElementById('length-threshold-value').textContent = this.value;
    });
    document.getElementById('confidence-threshold').addEventListener('input', function() {
        document.getElementById('threshold-value').textContent = this.value;
    });
    document.getElementById('save-settings').addEventListener('click', saveSettings);
    document.getElementById('reset-settings').addEventListener('click', resetSettings);

    // 添加标签切换功能
    document.querySelectorAll('.settings-tabs .tab').forEach(tab => {
        tab.addEventListener('click', function() {
            // 移除所有标签的active类
            document.querySelectorAll('.settings-tabs .tab').forEach(t => t.classList.remove('active'));
            // 添加当前标签的active类
            this.classList.add('active');

            // 隐藏所有内容
            document.querySelectorAll('.tab-content').forEach(content => {
                content.style.display = 'none';
            });

            // 显示当前标签对应的内容
            const tabId = this.getAttribute('data-tab');
            document.getElementById(`tab-${tabId}`).style.display = 'block';
        });
    });

    // 添加编辑规则按钮事件
    document.querySelectorAll('.edit-rules').forEach(button => {
        button.addEventListener('click', function() {
            const group = this.getAttribute('data-group');
            openRuleEditor(group);
        });
    });

    // 添加新分组按钮事件
    document.getElementById('add-new-group').addEventListener('click', addNewGroup);

    // 添加导入导出功能
    document.getElementById('export-settings').addEventListener('click', exportSettings);
    document.getElementById('import-settings').addEventListener('click', importSettings);

    return panel;
}

// 打开规则编辑器
function openRuleEditor(group) {
    // 创建规则编辑器面板
    const editorPanel = document.createElement('div');
    editorPanel.id = 'rule-editor-panel';
    editorPanel.className = 'settings-panel';

    const rules = keywordGroups[group] || [];
    let rulesHtml = '';

    rules.forEach((rule, index) => {
        rulesHtml += createRuleHtml(rule, index);
    });

    editorPanel.innerHTML = `
        <div class="settings-header">
            <h3>编辑 "${group}" 规则</h3>
            <span class="settings-close" id="close-rule-editor">×</span>
        </div>
        <div class="rules-container" id="rules-container">
            ${rulesHtml}
        </div>
        <div class="rule-actions">
            <button id="add-rule">+ 添加规则</button>
        </div>
        <div class="settings-actions">
            <button id="cancel-rules">取消</button>
            <button id="save-rules" class="save">保存规则</button>
        </div>
    `;

    document.body.appendChild(editorPanel);

    // 添加事件监听
    document.getElementById('close-rule-editor').addEventListener('click', () => {
        document.body.removeChild(editorPanel);
    });

    document.getElementById('cancel-rules').addEventListener('click', () => {
        document.body.removeChild(editorPanel);
    });

    document.getElementById('add-rule').addEventListener('click', () => {
        const container = document.getElementById('rules-container');
        const newIndex = container.children.length;
        const newRuleHtml = createRuleHtml({words: []}, newIndex);

        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = newRuleHtml;
        container.appendChild(tempDiv.firstElementChild);

        // 添加删除规则按钮事件
        document.querySelector(`.rule-item[data-index="${newIndex}"] .delete-rule`).addEventListener('click', function() {
            const ruleItem = this.closest('.rule-item');
            ruleItem.parentNode.removeChild(ruleItem);
        });
    });

    document.getElementById('save-rules').addEventListener('click', () => {
        saveRules(group);
        document.body.removeChild(editorPanel);
    });

    // 添加删除规则按钮事件
    document.querySelectorAll('.delete-rule').forEach(button => {
        button.addEventListener('click', function() {
            const ruleItem = this.closest('.rule-item');
            ruleItem.parentNode.removeChild(ruleItem);
        });
    });
}

// 创建规则HTML
function createRuleHtml(rule, index) {
    let ruleHtml = `
        <div class="rule-item" data-index="${index}">
            <div class="rule-header">
                <span class="rule-type">规则 #${index + 1}</span>
                <button class="delete-rule">删除</button>
            </div>
            <div class="rule-content">
    `;

    // 词组匹配
    if (rule.words) {
        const words = Array.isArray(rule.words) ? rule.words.join(', ') : '';
        const minCount = rule.minCount || 1;
        const maxLength = rule.maxLength || '';

        ruleHtml += `
            <div class="rule-option">
                <label>
                    <input type="radio" name="rule-type-${index}" value="words" checked>
                    词组匹配
                </label>
                <div class="rule-details">
                    <div class="rule-field">
                        <label>关键词(逗号分隔):</label>
                        <input type="text" class="rule-words" value="${words}">
                    </div>
                    <div class="rule-field">
                        <label>最小匹配数:</label>
                        <input type="number" class="rule-min-count" value="${minCount}" min="1">
                    </div>
                    <div class="rule-field">
                        <label>最大内容长度:</label>
                        <input type="number" class="rule-max-length" value="${maxLength}" placeholder="不限">
                    </div>
                </div>
            </div>
        `;

        // 上下文匹配
        if (rule.context) {
            const context = Array.isArray(rule.context) ? rule.context.join(', ') : '';

            ruleHtml += `
                <div class="rule-field">
                    <label>上下文词(逗号分隔):</label>
                    <input type="text" class="rule-context" value="${context}">
                </div>
            `;
        } else {
            ruleHtml += `
                <div class="rule-field">
                    <label>上下文词(逗号分隔):</label>
                    <input type="text" class="rule-context" placeholder="可选">
                </div>
            `;
        }
    }
    // 精确匹配
    else if (rule.exact) {
        ruleHtml += `
            <div class="rule-option">
                <label>
                    <input type="radio" name="rule-type-${index}" value="exact" checked>
                    精确匹配
                </label>
                <div class="rule-details">
                    <div class="rule-field">
                        <label>精确文本:</label>
                        <input type="text" class="rule-exact" value="${rule.exact}">
                    </div>
                </div>
            </div>
        `;
    }
    // 正则匹配
    else if (rule.regex) {
        const regexStr = rule.regex.toString();
        const regexPattern = regexStr.substring(1, regexStr.lastIndexOf('/'));
        const regexFlags = regexStr.substring(regexStr.lastIndexOf('/') + 1);

        ruleHtml += `
            <div class="rule-option">
                <label>
                    <input type="radio" name="rule-type-${index}" value="regex" checked>
                    正则匹配
                </label>
                <div class="rule-details">
                    <div class="rule-field">
                        <label>正则表达式:</label>
                        <input type="text" class="rule-regex-pattern" value="${regexPattern}">
                    </div>
                    <div class="rule-field">
                        <label>正则标志:</label>
                        <input type="text" class="rule-regex-flags" value="${regexFlags}" placeholder="i">
                    </div>
                </div>
            </div>
        `;
    }

    ruleHtml += `
            </div>
        </div>
    `;

    return ruleHtml;
}

// 保存规则
function saveRules(group) {
    const ruleItems = document.querySelectorAll('.rule-item');
    const newRules = [];

    ruleItems.forEach(item => {
        const index = item.getAttribute('data-index');
        const ruleType = item.querySelector(`input[name="rule-type-${index}"]:checked`).value;

        let rule = {};

        if (ruleType === 'words') {
            const wordsInput = item.querySelector('.rule-words').value;
            const words = wordsInput.split(',').map(word => word.trim()).filter(word => word);

            if (words.length > 0) {
                rule.words = words;

                const minCount = parseInt(item.querySelector('.rule-min-count').value);
                if (!isNaN(minCount) && minCount > 0) {
                    rule.minCount = minCount;
                }

                const maxLength = parseInt(item.querySelector('.rule-max-length').value);
                if (!isNaN(maxLength) && maxLength > 0) {
                    rule.maxLength = maxLength;
                }

                const contextInput = item.querySelector('.rule-context').value;
                if (contextInput) {
                    const context = contextInput.split(',').map(ctx => ctx.trim()).filter(ctx => ctx);
                    if (context.length > 0) {
                        rule.context = context;
                    }
                }

                newRules.push(rule);
            }
        } else if (ruleType === 'exact') {
            const exact = item.querySelector('.rule-exact').value.trim();
            if (exact) {
                rule.exact = exact;
                newRules.push(rule);
            }
        } else if (ruleType === 'regex') {
            const pattern = item.querySelector('.rule-regex-pattern').value.trim();
            const flags = item.querySelector('.rule-regex-flags').value.trim() || 'i';

            if (pattern) {
                try {
                    rule.regex = new RegExp(pattern, flags);
                    newRules.push(rule);
                } catch (e) {
                    alert(`正则表达式错误: ${e.message}`);
                }
            }
        }
    });

    // 更新关键词组
    keywordGroups[group] = newRules;
s
    // 更新规则计数显示
    const ruleCountElement = document.querySelector(`#rules-${group} .rule-count`);
    if (ruleCountElement) {
        ruleCountElement.textContent = `${newRules.length}条规则`;
    }

    // 重新应用过滤
    filterPosts();
}

// 添加新分组
function addNewGroup() {
    const groupName = prompt('请输入新分组名称:');
    if (groupName && groupName.trim()) {
        // 检查是否已存在
        if (keywordGroups[groupName]) {
            alert('该分组名称已存在!');
            return;
        }

        // 添加新分组
        keywordGroups[groupName] = [];
        GM_setValue(`enable-${groupName}`, true);

        // 重新创建设置面板
        const oldPanel = document.getElementById('filter-settings-panel');
        if (oldPanel) {
            document.body.removeChild(oldPanel);
        }

        const newPanel = createSettingsPanel();
        newPanel.style.display = 'block';

        // 切换到分组管理标签
        document.querySelector('.tab[data-tab="groups"]').click();
    }
}

// 导出设置
function exportSettings() {
    const settings = {
        keywordGroups: keywordGroups,
        filterLengthThreshold: GM_getValue('filterlengthThreshold', 20),
        confidenceThreshold: GM_getValue('confidenceThreshold', 60),
        hideFiltered: GM_getValue('hideFiltered', false),
        enableDebug: GM_getValue('enableDebug', false),
        highlightOnly: GM_getValue('highlightOnly', false)
    };

    // 为每个分组添加启用状态
    for (const group in keywordGroups) {
        settings[`enable-${group}`] = GM_getValue(`enable-${group}`, true);
    }

    const blob = new Blob([JSON.stringify(settings, (key, value) => {
        // 特殊处理正则表达式
        if (value instanceof RegExp) {
            return {
                __regex: true,
                source: value.source,
                flags: value.flags
            };
        }
        return value;
    }, 2)], {type: 'application/json'});

    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'discourse-filter-settings.json';
    a.click();

    URL.revokeObjectURL(url);
}

// 导入设置
function importSettings() {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = '.json';

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

        const reader = new FileReader();
        reader.onload = function(e) {
            try {
                const settings = JSON.parse(e.target.result, (key, value) => {
                    // 恢复正则表达式
                    if (value && value.__regex) {
                        return new RegExp(value.source, value.flags);
                    }
                    return value;
                });

                // 更新关键词组
                if (settings.keywordGroups) {
                    Object.assign(keywordGroups, settings.keywordGroups);
                }

                // 更新其他设置
                if (settings.filterLengthThreshold !== undefined) {
                    GM_setValue('filterLengthThreshold', settings.filterLengthThreshold);
                }
                if (settings.confidenceThreshold !== undefined) {
                    GM_setValue('confidenceThreshold', settings.confidenceThreshold);
                }

                if (settings.hideFiltered !== undefined) {
                    GM_setValue('hideFiltered', settings.hideFiltered);
                }

                if (settings.enableDebug !== undefined) {
                    GM_setValue('enableDebug', settings.enableDebug);
                }

                if (settings.highlightOnly !== undefined) {
                    GM_setValue('highlightOnly', settings.highlightOnly);
                }

                // 更新分组启用状态
                for (const group in keywordGroups) {
                    if (settings[`enable-${group}`] !== undefined) {
                        GM_setValue(`enable-${group}`, settings[`enable-${group}`]);
                    }
                }

                // 重新创建设置面板
                const oldPanel = document.getElementById('filter-settings-panel');
                if (oldPanel) {
                    document.body.removeChild(oldPanel);
                }

                const newPanel = createSettingsPanel();
                newPanel.style.display = 'block';

                // 重新应用过滤
                filterPosts();

                alert('设置导入成功!');
            } catch (error) {
                alert(`导入失败: ${error.message}`);
            }
        };
        reader.readAsText(file);
    };

    input.click();
}

// 保存设置
function saveSettings() {
    const filterLengthThreshold = parseInt(document.getElementById('filter-length-threshold').value);
    GM_setValue('filterLengthThreshold', filterLengthThreshold);
    const confidenceThreshold = parseInt(document.getElementById('confidence-threshold').value);
    GM_setValue('confidenceThreshold', confidenceThreshold);

    // 保存分组启用状态
    for (const group in keywordGroups) {
        const checkbox = document.getElementById(`enable-${group}`);
        if (checkbox) GM_setValue(`enable-${group}`, checkbox.checked);
    }

    // 保存自动隐藏设置
    const autoHide = document.getElementById('auto-hide-filtered').checked;
    GM_setValue('hideFiltered', autoHide);

    // 保存调试模式设置
    const enableDebug = document.getElementById('enable-debug')?.checked || false;
    GM_setValue('enableDebug', enableDebug);

    // 保存仅高亮设置
    const highlightOnly = document.getElementById('highlight-only')?.checked || false;
    GM_setValue('highlightOnly', highlightOnly);

    // 更新过滤器面板上的切换按钮文本
    const toggleButton = document.getElementById('toggle-filter');
    if (toggleButton) {
        toggleButton.textContent = autoHide ? '显示已过滤帖子' : '隐藏已过滤帖子';
    }

    // 保存自定义规则
    GM_setValue("customKeywordGroups", serializeKeywordGroups(keywordGroups));

    closeSettingsPanel();
    filterPosts(); // 重新应用过滤
}

// 添加更多样式
GM_addStyle(`
    .settings-tabs {
        display: flex;
        border-bottom: 1px solid #ddd;
        margin-bottom: 15px;
    }
    .tab {
        padding: 8px 15px;
        cursor: pointer;
        border-bottom: 2px solid transparent;
    }
    .tab.active {
        border-bottom: 2px solid #0088cc;
        font-weight: bold;
    }
    .rule-item {
        border: 1px solid #ddd;
        border-radius: 5px;
        padding: 10px;
        margin-bottom: 10px;
        background: #f9f9f9;
    }
    .rule-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 10px;
    }
    .rule-type {
        font-weight: bold;
    }
    .delete-rule {
        background: #ff4d4d;
        color: white;
        border: none;
        border-radius: 3px;
        padding: 3px 8px;
        cursor: pointer;
    }
    .rule-field {
        margin-bottom: 8px;
    }
    .rule-field label {
        display: block;
        margin-bottom: 3px;
        font-size: 12px;
        color: #666;
    }
    .rule-field input {
        width: 100%;
        padding: 5px;
        border: 1px solid #ddd;
        border-radius: 3px;
    }
    .rule-actions {
        margin: 15px 0;
    }
    #add-rule, #add-new-group {
        background: #4CAF50;
        color: white;
        border: none;
        border-radius: 3px;
        padding: 5px 10px;
        cursor: pointer;
    }
    .threshold-display {
        display: flex;
        align-items: center;
        margin-top: 5px;
    }
    #length-threshold-value {
        font-weight: bold;
        margin-right: 10px;
    }
    #threshold-value {
        font-weight: bold;
        margin-right: 10px;
    }
    .threshold-description {
        font-size: 12px;
        color: #666;
    }
    .export-import {
        margin-top: 15px;
    }
    .export-import button {
        margin-right: 10px;
        padding: 5px 10px;
        background: #f5f5f5;
        border: 1px solid #ddd;
        border-radius: 3px;
        cursor: pointer;
    }
    #rule-editor-panel {
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        background: white;
        padding: 20px;
        border-radius: 8px;
        box-shadow: 0 4px 20px rgba(0,0,0,0.2);
        z-index: 1001;
        width: 500px;
        max-height: 80vh;
        overflow-y: auto;
    }
    .rules-container {
        max-height: 400px;
        overflow-y: auto;
        margin-bottom: 15px;
    }
    .group-rules {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-top: 5px;
    }
    .rule-count {
        font-size: 12px;
        color: #666;
    }
    .edit-rules {
        background: #0088cc;
        color: white;
        border: none;
        border-radius: 3px;
        padding: 3px 8px;
        cursor: pointer;
        font-size: 12px;
    }
    .settings-option {
        margin-bottom: 15px;
    }
`);


    // 打开设置面板
    function openSettingsPanel() {
        const panel = document.getElementById('filter-settings-panel') || createSettingsPanel();
        panel.style.display = 'block';

        // 加载当前设置
        const filterLengthThreshold = GM_getValue('filterLengthThreshold', 20);
        document.getElementById('filter-length-threshold').value = filterLengthThreshold;
        document.getElementById('length-threshold-value').textContent = filterLengthThreshold;
        const confidenceThreshold = GM_getValue('confidenceThreshold', 60);
        document.getElementById('confidence-threshold').value = confidenceThreshold;
        document.getElementById('threshold-value').textContent = confidenceThreshold;

        for (const group in keywordGroups) {
            const enabled = GM_getValue(`enable-${group}`, true);
            const checkbox = document.getElementById(`enable-${group}`);
            if (checkbox) checkbox.checked = enabled;
        }
    }

    // 关闭设置面板
    function closeSettingsPanel() {
        const panel = document.getElementById('filter-settings-panel');
        if (panel) panel.style.display = 'none';
    }

    // 重置设置
    function resetSettings() {
        GM_setValue('filterLengthThreshold', 20);
        document.getElementById('filter-length-threshold').value = 20;
        document.getElementById('length-threshold-value').value = 20;
        GM_setValue('confidenceThreshold', 60);
        document.getElementById('confidence-threshold').value = 60;
        document.getElementById('threshold-value').textContent = 60;

        // 重新加载默认配置
        keywordGroups = deserializeKeywordGroups(GM_getValue("defaultKeywordGroups", DEFAULT_KEYWORD_GROUPS));

        for (const group in keywordGroups) {
            GM_setValue(`enable-${group}`, true);
            const checkbox = document.getElementById(`enable-${group}`);
            if (checkbox) checkbox.checked = true;
        }

        // 重新创建设置面板
        const oldPanel = document.getElementById('filter-settings-panel');
        if (oldPanel) {
            document.body.removeChild(oldPanel);
        }

        const newPanel = createSettingsPanel();
        newPanel.style.display = 'block';

        // 立即应用新规则
        filterPosts();
    }

    // 更新过滤统计
    function updateFilterStats(stats) {
        const container = document.getElementById('filter-groups');
        if (!container) return;

        container.innerHTML = '';

        for (const group in stats) {
            if (stats[group] > 0) {
                const groupDiv = document.createElement('div');
                groupDiv.className = 'filter-group';
                groupDiv.innerHTML = `
                    <span class="filter-name">${group}:</span>
                    <span class="filter-count">${stats[group]}</span>
                `;
                container.appendChild(groupDiv);
            }
        }

        // 添加总计
        const total = Object.values(stats).reduce((sum, count) => sum + count, 0);
        if (total > 0) {
            const totalDiv = document.createElement('div');
            totalDiv.className = 'filter-group';
            totalDiv.innerHTML = `
                <span class="filter-name"><strong>总计:</strong></span>
                <span class="filter-count">${total}</span>
            `;
            container.appendChild(totalDiv);
        }
    }

    // 检查帖子是否包含关键词 - 改进的匹配逻辑
    function checkPostForKeywords(post) {
        const postContent = post.querySelector('.cooked')?.textContent || '';
        const postContentLower = postContent.toLowerCase();
        const stats = {};
        const confidenceThreshold = GM_getValue('confidenceThreshold', 60);
        const filterLengthThreshold = GM_getValue('filterLengthThreshold', 20);

        // 移除之前的过滤标记
        post.classList.remove('filtered-post', 'confidence-high', 'confidence-medium', 'confidence-low');
        post.removeAttribute('data-filter-reason');

        for (const group in keywordGroups) {
            // 检查该组是否启用
            if (!GM_getValue(`enable-${group}`, true)) {
                stats[group] = 0;
                continue;
            }

            stats[group] = 0;
            let maxConfidence = 0;
            let matchReason = '';

            // 遍历该组的所有匹配规则
            for (const rule of keywordGroups[group]) {
                let confidence = 0;
                let matched = false;

                // 1. 精确匹配
                if (rule.exact && postContentLower === (rule.exact.toLowerCase())) {
                    confidence = 95;
                    matched = true;
                    matchReason = `精确匹配: "${rule.exact}"`;
                }

                // 2. 正则匹配
                else if (rule.regex && rule.regex.test(postContent)) {
                    confidence = 90;
                    matched = true;
                    matchReason = `正则匹配: ${rule.regex.toString()}`;
                }

                // 3. 词组匹配
                else if (rule.words) {
                    // 计算匹配的词数
                    const matchedWords = rule.words.filter(word =>
                        postContentLower.includes(word.toLowerCase())
                    );

                    // 检查是否达到最小匹配数
                    const minCount = rule.minCount || 1;
                    if (matchedWords.length >= minCount) {
                        matched = true;

                        // 基础置信度: 匹配词数/总词数的比例
                        confidence = (matchedWords.length / rule.words.length) * 70;

                        // 上下文匹配加分
                        if (rule.context) {
                            const contextMatches = rule.context.filter(ctx =>
                                postContentLower.includes(ctx.toLowerCase())
                            );
                            if (contextMatches.length > 0) {
                                confidence += 20 * (contextMatches.length / rule.context.length);
                            } else {
                                confidence -= 30; // 没有上下文匹配则降低置信度
                            }
                        }

                        // 内容长度检查
                        if (rule.maxLength && postContent.length <= rule.maxLength) {
                            confidence += 10;
                        } else if (rule.maxLength) {
                            // 内容长度超过设定值时动态扣除置信度
                            const lengthRatio = postContent.length / rule.maxLength;
                            if (lengthRatio > 5) {
                                // 内容长度过长时,直接将置信度置为0
                                confidence = 0;
                            } else if (lengthRatio > 2) {
                                // 根据长度比例动态扣除置信度,长度越长扣除越多
                                confidence -= Math.min(50, 20 * (lengthRatio - 1));
                            }
                        }

                        matchReason = `匹配词: ${matchedWords.join(', ')}`;
                    }
                }

                // 更新最高置信度
                if (matched && confidence > maxConfidence) {
                    maxConfidence = confidence;
                    stats[group] = 1;
                }
            }

            // 修改,如果置信度超过阈值且长度低于阈值,标记为过滤
            if (maxConfidence >= confidenceThreshold && postContent.length < filterLengthThreshold) {
                post.classList.add('filtered-post');
                post.setAttribute('data-filter-reason', `${group} (${Math.round(maxConfidence)}%): ${matchReason}`);

                // 根据置信度添加不同的样式
                if (maxConfidence >= 85) {
                    post.classList.add('confidence-high');
                } else if (maxConfidence >= 70) {
                    post.classList.add('confidence-medium');
                } else {
                    post.classList.add('confidence-low');
                }

                break; // 一个帖子只归类到一个组
            }
        }

        return stats;
    }

    // 过滤帖子并统计
// 更新过滤帖子函数,支持新的设置
function filterPosts() {
    // 查找所有可能的帖子容器
    const postContainers = [
        ...document.querySelectorAll('.topic-post'),                // 帖子详情页
        ...document.querySelectorAll('.topic-list-item'),           // 帖子列表页
        ...document.querySelectorAll('[id^="ember"] .topic-post')   // SPA 动态加载的帖子
    ];

    const stats = {};

    // 初始化统计对象
    for (const group in keywordGroups) {
        stats[group] = 0;
    }

    // 调试信息
    const debugPanel = document.getElementById('filter-debug');
    const enableDebug = GM_getValue('enableDebug', false);

    if (debugPanel) {
        debugPanel.textContent = `找到 ${postContainers.length} 个帖子容器`;
        debugPanel.style.display = (postContainers.length > 0 && enableDebug) ? 'block' : 'none';

        // 5秒后隐藏调试信息
        if (enableDebug) {
            setTimeout(() => {
                debugPanel.style.display = 'none';
            }, 5000);
        }
    }

    postContainers.forEach(post => {
        const postStats = checkPostForKeywords(post);
        for (const group in postStats) {
            stats[group] += postStats[group];
        }
    });

    updateFilterStats(stats);

    // 应用当前的隐藏/显示状态
    applyFilterVisibility();
}

// 更新应用过滤可见性函数,支持仅高亮模式
function applyFilterVisibility() {
    const hideFiltered = GM_getValue('hideFiltered', false);
    const highlightOnly = GM_getValue('highlightOnly', false);
    const filteredPosts = document.querySelectorAll('.filtered-post');

    filteredPosts.forEach(post => {
        if (hideFiltered && !highlightOnly) {
            post.classList.add('hidden');
        } else {
            post.classList.remove('hidden');
        }
    });
}

    // 监听 URL 变化
    function observeUrlChanges() {
        let lastUrl = location.href;

        // 创建一个新的 MutationObserver 实例
        const observer = new MutationObserver(() => {
            if (lastUrl !== location.href) {
                lastUrl = location.href;
                console.log('URL changed to:', lastUrl);

                // 延迟执行,等待 SPA 内容加载
                setTimeout(() => {
                    filterPosts();
                    setupPostStreamObserver();
                }, 1000);
            }
        });

        // 开始观察 document.body 的变化
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // 监听帖子流的变化
    function setupPostStreamObserver() {
        // 查找可能的帖子容器
        const postStreams = [
            document.querySelector('.post-stream'),
            document.querySelector('.topic-list'),
            ...document.querySelectorAll('[id^="ember"] .post-stream')
        ].filter(el => el !== null);

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

        // 为每个容器创建观察器
        postStreams.forEach(container => {
            const observer = new MutationObserver(mutations => {
                let shouldFilter = false;

                mutations.forEach(mutation => {
                    if (mutation.addedNodes.length) {
                        mutation.addedNodes.forEach(node => {
                            if (node.nodeType === 1 && (
                                node.classList?.contains('topic-post') ||
                                node.classList?.contains('topic-list-item') ||
                                node.querySelector?.('.topic-post')
                            )) {
                                shouldFilter = true;
                            }
                        });
                    }
                });

                if (shouldFilter) {
                    filterPosts();
                }
            });

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

    // 监听 DOM 变化,处理 SPA 动态加载的内容
    function observeDomChanges() {
        const observer = new MutationObserver(mutations => {
            let newEmberContainers = false;
            let newPostsAdded = false;

            mutations.forEach(mutation => {
                if (mutation.addedNodes.length) {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === 1) {
                            // 检查是否是 Ember 容器
                            if (node.id && node.id.startsWith('ember')) {
                                newEmberContainers = true;
                            }

                            // 检查是否包含帖子
                            if (node.classList?.contains('topic-post') ||
                                node.classList?.contains('topic-list-item') ||
                                node.querySelector?.('.topic-post, .topic-list-item')) {
                                newPostsAdded = true;
                            }
                        }
                    });
                }
            });

            if (newEmberContainers || newPostsAdded) {
                // 延迟执行,等待内容完全加载
                setTimeout(() => {
                    filterPosts();
                    setupPostStreamObserver();
                }, 500);
            }
        });

        // 观察 #main-outlet 和整个 body
        const mainOutlet = document.getElementById('main-outlet');
        if (mainOutlet) {
            observer.observe(mainOutlet, { childList: true, subtree: true });
        } else {
            observer.observe(document.body, { childList: true, subtree: true });
        }
    }

    // 初始化
function init() {
    // 等待页面完全加载
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', onReady);
    } else {
        onReady();
    }

    function onReady() {
        // 初始化规则
        const savedData = GM_getValue('customKeywordGroups');
        if (savedData) {
            keywordGroups = deserializeKeywordGroups(savedData);
        }

        // 初始化设置
        if (GM_getValue('filterLengthThreshold') === undefined) {
            GM_setValue('filterLengthThreshold', 20);
        }
        if (GM_getValue('confidenceThreshold') === undefined) {
            GM_setValue('confidenceThreshold', 60);
            for (const group in keywordGroups) {
                GM_setValue(`enable-${group}`, true);
            }
        }

        // 初始化隐藏状态设置(如果未设置)
        if (GM_getValue('hideFiltered') === undefined) {
            GM_setValue('hideFiltered', false);
        }

        createFilterStatsPanel();

        // 初始过滤
        setTimeout(() => {
            filterPosts();
        }, 1000);

        // 设置各种观察器
        observeUrlChanges();
        setupPostStreamObserver();
        observeDomChanges();

        // 添加页面可见性变化监听
        document.addEventListener('visibilitychange', () => {
            if (!document.hidden) {
                // 页面重新变为可见时,重新过滤
                setTimeout(() => {
                    filterPosts();
                }, 500);
            }
        });
    // 恢复面板位置,by snowsoul
    const panel = document.getElementById('filter-stats');
    if (panel) {
        const savedPosition = GM_getValue('panelPosition', null);
        if (savedPosition) {
            panel.style.left = savedPosition.x;
            panel.style.top = savedPosition.y;
            panel.style.right = 'unset'; // 取消原有的right定位
        }
    }

    }
}

    init();
})();