Greasy Fork

来自缓存

Greasy Fork is available in English.

龙空信息降噪器 v0.4.1

让您的龙空浏览体验回归平静与高效。新增显示用户名功能。安装任何插件,在浏览器运行任何代码前,请先问问AI,防止代码中包含恶意攻击内容。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         龙空信息降噪器 v0.4.1
// @namespace    http://tampermonkey.net/
// @version      0.4.1
// @description  让您的龙空浏览体验回归平静与高效。新增显示用户名功能。安装任何插件,在浏览器运行任何代码前,请先问问AI,防止代码中包含恶意攻击内容。
// @author       liudev & AI Assistant
// @match        https://www.lkong.com/forum/*
// @match        https://www.lkong.com/thread/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        unsafeWindow
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ================== 1. 数据存储与加载 ==================

    const STORAGE_KEY_USERS = 'lkong_blocked_users';
    const STORAGE_KEY_TITLE_KEYWORDS = 'lkong_blocked_title_keywords';
    const STORAGE_KEY_REPLY_KEYWORDS = 'lkong_blocked_reply_keywords';

    let blockedUsers = []; // { userId: string, deepBlock: boolean }[]
    let blockedTitleKeywords = new Set();
    let blockedReplyKeywords = new Set();

    async function loadBlockedData() {
        try {
            const [storedUsersStr, storedTitleKeywords, storedReplyKeywords] = await Promise.all([
                GM_getValue(STORAGE_KEY_USERS, '[]'),
                GM_getValue(STORAGE_KEY_TITLE_KEYWORDS, '[]'),
                GM_getValue(STORAGE_KEY_REPLY_KEYWORDS, '[]')
            ]);

            // --- 用户数据迁移与加载 ---
            let usersData = JSON.parse(storedUsersStr);
            if (usersData.length > 0 && typeof usersData[0] === 'string') {
                // 旧版数据 (string[]), 迁移到新版 ({ userId, deepBlock })
                console.log('LKong Blocker: 检测到旧版用户数据,正在迁移...');
                blockedUsers = usersData.map(userId => ({ userId: userId, deepBlock: false }));
                await saveBlockedData(); // 迁移后立即保存
            } else {
                blockedUsers = usersData;
            }

            blockedTitleKeywords = new Set(JSON.parse(storedTitleKeywords));
            blockedReplyKeywords = new Set(JSON.parse(storedReplyKeywords));
        } catch (e) {
            console.error('LKong Blocker: 加载噪声名单失败', e);
            blockedUsers = [];
            blockedTitleKeywords = new Set();
            blockedReplyKeywords = new Set();
        }
    }

    async function saveBlockedData() {
        try {
            await Promise.all([
                GM_setValue(STORAGE_KEY_USERS, JSON.stringify(blockedUsers)),
                GM_setValue(STORAGE_KEY_TITLE_KEYWORDS, JSON.stringify(Array.from(blockedTitleKeywords))),
                GM_setValue(STORAGE_KEY_REPLY_KEYWORDS, JSON.stringify(Array.from(blockedReplyKeywords)))
            ]);
        } catch(e) {
            console.error('LKong Blocker: 保存噪声名单失败', e);
        }
    }

    // ================== 辅助功能:API获取用户名 ==================
    async function fetchUserName(userId) {
        if (!userId) return null;
        const query = {
            "operationName": "ViewUserContentsPage",
            "variables": { "uid": parseInt(userId, 10), "page": 1, "isDigest": false },
            "query": "query ViewUserContentsPage($uid: Int!, $isDigest: Boolean!, $page: Int) {\n  content: userReplies(uid: $uid, isDigest: $isDigest, page: $page) {\n    author {\n      name\n      __typename\n    }\n    __typename\n  }\n}"
        };

        try {
            const response = await fetch("https://api.lkong.com/api", {
                "method": "POST",
                "credentials": "include", // ★★★ 必须加上这一行 ★★★
                "headers": { "content-type": "application/json" },
                "body": JSON.stringify(query)
            });
            const json = await response.json();

            // 如果未登录,返回特定错误以便调试
            if (json.errors) {
                console.warn('LK-Blocker: API返回权限错误,可能登录状态失效', json.errors);
                return null;
            }

            const replies = json?.data?.content;
            if (replies && replies.length > 0 && replies[0].author) {
                return replies[0].author.name;
            }
            return null; // 有权限,但用户真的没发过贴,或者被全站屏蔽了
        } catch (error) {
            console.error("LK-Blocker: API获取用户名网络异常", error);
            return null;
        }
    }

    // ================== 2. 核心处理逻辑 ==================

    // --- 2.1 论坛列表页:过滤帖子标题 ---
    function processThreadsData(threads) {
        if (!threads || !Array.isArray(threads)) return;
        for (const thread of threads) {
            const uid = (thread.author?.uid || thread.authorid)?.toString();
            const tid = thread.tid?.toString();
            const title = thread.subject || '';

            if (!uid || !tid) continue;

            const threadLink = document.querySelector(`a[href*="/thread/${tid}"]`);
            if (!threadLink) continue;

            // Find the containing thread item using multiple fallbacks (avoid fragile css-xxxxx)
            const threadItem = threadLink.closest('.css-760i8n')
                || threadLink.closest('div[class*="thread"]')
                || threadLink.closest('article')
                || threadLink.closest('li')
                || threadLink.closest('[data-tid]');
            if (!threadItem || threadItem.dataset.lkProcessed === 'true') continue;
            threadItem.dataset.lkProcessed = 'true';

            if (blockedUsers.some(user => user.userId === uid)) {
                console.log(`LKong Blocker: 已按用户 [${uid}] 净化帖子 (TID: ${tid})。`);
                threadItem.style.display = 'none';
                continue;
            }

            const currentTitle = title || threadLink.textContent;
            for (const keyword of blockedTitleKeywords) {
                if (keyword && currentTitle.includes(keyword)) {
                    console.log(`LKong Blocker: 已按标题关键词 [${keyword}] 净化帖子 (标题: ${currentTitle})。`);
                    threadItem.style.display = 'none';
                    threadItem.dataset.lkKeywordBlocked = 'true';
                    break;
                }
            }
            if (threadItem.dataset.lkKeywordBlocked === 'true') continue;

            let authorContainer = threadItem.querySelector('.author');
            if (!authorContainer) {
                // fallback to author link or nearest container
                const authorLink = threadItem.querySelector('a[href^="/user/"]') || threadItem.querySelector('a[href*="author="]');
                authorContainer = authorLink ? (authorLink.closest('div') || authorLink) : null;
            }
            if (authorContainer) addBlockButton(authorContainer, uid, threadItem);
        }
    }

    // --- 列表页添加按钮:点击后调用API获取名字再屏蔽 ---
    function addBlockButton(anchorElement, userId, threadItem) {
        if (anchorElement.querySelector('.lk-block-btn')) return;

        const blockButton = document.createElement('a');
        blockButton.href = '#';
        blockButton.textContent = '[净化]';
        blockButton.className = 'lk-block-btn';
        blockButton.style.cssText = 'margin-left: 8px; font-size: 12px; color: #999;';

        blockButton.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            // ============ 1. 优先尝试从 DOM 提取用户名 ============
            let domUserName = '';

            if (threadItem) {
                // 根据你提供的 HTML 结构: .author 下的第一个链接通常是用户名
                const authorLink = threadItem.querySelector('.author a[href*="/user/"]');

                if (authorLink) {
                    domUserName = authorLink.textContent.trim();
                } else {
                    // 备用:搜索结果页有时候结构不一样,或者是 avatar
                    const avatarImg = threadItem.querySelector('.ant-avatar img, img[class*="avatar"]');
                    if (avatarImg && avatarImg.alt) {
                        domUserName = avatarImg.alt;
                    }
                }
            }
            // ===================================================
            // 如果提取到了名字,就显示名字;否则显示ID
            const displayName = domUserName || `ID:${userId}`;
            confirmationModal.show(`确定要净化用户: 【${displayName}】 吗?\n该用户的帖子将从列表中消失。`, async () => {

                let finalName = domUserName;
                // ============ 2. 如果DOM没抓到,才请求API ============
                if (!finalName) {
                    try {
                       const apiName = await fetchUserName(userId);
                       if (apiName) finalName = apiName;
                    } catch(err) {
                        console.error(err);
                    }
                }

                // 默认值
                if (!finalName) finalName = '未知用户';
                // ============ 3. 保存 ============
                if (!blockedUsers.some(u => u.userId === userId)) {
                    blockedUsers.push({ userId: userId, deepBlock: false, userName: finalName });
                    await saveBlockedData();

                    // 隐藏当前行
                    if (threadItem) {
                        threadItem.style.display = 'none';
                        // 有时候列表由虚线分隔,把分隔线也隐藏可能更好看,但这取决于具体CSS
                        if(threadItem.nextElementSibling && threadItem.nextElementSibling.tagName === 'HR') {
                             threadItem.nextElementSibling.style.display = 'none';
                        }
                    }
                }
            });
        });
        anchorElement.appendChild(blockButton);
    }

    // --- 2.2 帖子详情页:过滤回帖内容 ---
    function processPost(postElement) {
        // 使用两个状态:pending表示处理中,true表示已彻底处理
        if (postElement.dataset.lkPostProcessed === 'true' || postElement.dataset.lkPostProcessed === 'pending') {
            return;
        }
        postElement.dataset.lkPostProcessed = 'pending';

        let retryCount = 0;
        const maxRetries = 50; // 轮询15次,每次间隔100毫秒 (总等待约 1.5 秒),给React框架挂载元素的时间

        function attemptExtraction() {
            let userId = null;

            // 尝试第一种链接: a[href*="?author="] (只看TA)
            const authorFilterLink = postElement.querySelector('a[href*="?author="]');
            if (authorFilterLink) {
                try {
                    // 加入第二个参数保证如果取到相对路径也能正常解析
                    const url = new URL(authorFilterLink.href, window.location.origin);
                    userId = url.searchParams.get('author');
                } catch (e) { /* 忽略无效URL */ }
            }

            // 尝试第二种: a[href^="/user/"] (用户主页链接)
            if (!userId) {
                const userProfileLink = postElement.querySelector('a[href^="/user/"]');
                if (userProfileLink) {
                    userId = userProfileLink.href.split('/').pop();
                }
            }

            // 【核心修复】:如果没有获取到ID,并且还没有超时,我们再稍微等一等页面挂载DOM
            if (!userId && retryCount < maxRetries) {
                retryCount++;
                setTimeout(attemptExtraction, 1000);
                return;
            }

            // 到这一步,无论成败,代表彻底完成了检索操作
            postElement.dataset.lkPostProcessed = 'true';

            // ============ 后面才是正式的过滤/执行逻辑 ============

            if (userId) {
                // 执行添加屏蔽按钮
                addPurifyButtons(postElement, userId);

                const blockedUser = blockedUsers.find(u => u.userId === userId);
                if (blockedUser && blockedUser.deepBlock) {
                    console.log(`LKong Blocker: 已按用户 [${userId}] (深度屏蔽) 净化此楼层。`);
                    postElement.style.display = 'none';
                    return; // 用户屏蔽优先,直接阻断
                }
            } else {
                console.log(`LKong Blocker: 未能提取到楼层发帖人ID (可能是帖子架构异常或网络太慢),该楼层仅应用关键词过滤。`);
            }

            // 关键词屏蔽检查 (在用户未被屏蔽 或 取不到用户的退拽保护下执行)
            const contentDiv = postElement.querySelector('.main-content');
            if (contentDiv) {
                const contentText = contentDiv.textContent || '';
                for (const keyword of blockedReplyKeywords) {
                    if (keyword && contentText.includes(keyword)) {
                        console.log(`LKong Blocker: 已按回帖关键词 [${keyword}] 净化此楼层。`);
                        postElement.style.display = 'none';
                        return;
                    }
                }
            }

            // 最后添加折叠按钮
            addFoldingFeature(postElement);
        }

        // 启动获取检测逻辑
        attemptExtraction();
    }
    // --- 2.3 帖子详情页:添加净化按钮 ---
    function addPurifyButtons(postElement, userId) {
        // 1. 定位操作栏 (放按钮的地方)
        const findActionsContainer = (el) => {
            if(!el) return null;
            // 尝试在当前元素或子元素找
            let target = el.querySelector && el.querySelector('.css-9ph873, .css-1feda4v, .post-actions, .actions');
            if (target) return target;
            // 尝试去父级找(适应 React 渲染层级偏差)
            if (el.parentElement) {
                target = el.parentElement.querySelector('.css-9ph873, .css-1feda4v, .post-actions, .actions');
                if (target) return target;
            }
            return null;
        };

        const actionsContainer = findActionsContainer(postElement);
        if (!actionsContainer || actionsContainer.querySelector('.lk-purify-btn')) return;

        const reportBtn = actionsContainer.querySelector('.css-rhu9bd, .css-j5tknx, a[title*="举报"]');

        const createButton = (text, className, titlePrefix, isDeep) => {
            const btn = document.createElement('div');
            btn.className = `lk-action-wrapper ${className}`;
            btn.innerHTML = `<span>${text}</span>`;
            btn.title = `${titlePrefix} (ID:${userId})`;

            btn.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();

                let foundName = '';

                // ============ 核心逻辑:向上雷达扫描 ============
                let pointer = btn.parentElement;
                let levels = 0;

                // 向上爬 7 层,这足以跳出 .main-content 进入 .main-content-wrap
                while(pointer && levels < 7) {

                    // 场景 1: 楼主层布局 (Top-level layout)
                    // 对应你提供的第二段代码: .user-wrapper > strong
                    const userWrapper = pointer.querySelector('.user-wrapper strong');
                    if (userWrapper) {
                        foundName = userWrapper.textContent;
                        console.log(`LK-Blocker: 命中楼主布局 (Level ${levels})`);
                        break;
                    }

                    // 场景 2: 普通回复布局 (Reply layout)
                    // 对应代码: .left-area > .user > h2
                    // 只要容器内有 .left-area,我们就在这个范围内细找
                    const leftArea = pointer.querySelector('.left-area');
                    if (leftArea) {
                        const h2 = leftArea.querySelector('.user h2');
                        // 排除只显示"楼主"字样的情况,必须找用户名
                        if (h2) {
                            // 优先取 h2 下的第一个 span(它最干净,不含其他杂质)
                            const nameSpan = h2.querySelector('span:first-child');

                            if (nameSpan) {
                                foundName = nameSpan.textContent.trim();
                            } else {
                                // 只有实在找不到span,才拿整个h2,但要做字符串清洗
                                // 暴力清洗:只要空格前的第一部分
                                foundName = h2.textContent.split(/[\s\n\t]+|楼主|Lv\./)[0].trim();
                            }

                            console.log(`LK-Blocker: 命中回帖布局 (Level ${levels})`);
                            break;
                        }
                    }

                    // 场景 3: 响应式/移动端窄屏布局
                    // 有时候 .left-area 没了,但头像还在,图片 alt 是最稳的
                    const avatarImg = pointer.querySelector('img.ant-avatar-image, .ant-avatar img');
                    if (avatarImg && avatarImg.alt && avatarImg.alt.length > 0) {
                        // 防止取到 "avatar" 这种无效文本,只取看似名字的
                        if (avatarImg.alt !== 'avatar') {
                             foundName = avatarImg.alt;
                             // 不break,因为上面两个文本查找更精准,这个作为备选先存着
                             // 但如果是楼主布局,通常上面那个user-wrapper已经命中了
                        }
                    }

                    pointer = pointer.parentElement;
                    levels++;
                }

                // 数据清洗
                if (foundName) foundName = foundName.replace(/[\r\n\t]/g, '').trim();

                // ===========================================

                const displayName = foundName || `ID:${userId}`;
                const actionText = isDeep ? '深度净化' : '净化';
                const warningText = isDeep ? '他的所有主题和回帖都将被隐藏。' : '他的所有主题都将被隐藏。';
                const confirmMsg = `确定要${actionText}用户: 【${displayName}】\n(ID: ${userId}) 吗?\n${warningText}`;

                confirmationModal.show(confirmMsg, async () => {
                    const existingUser = blockedUsers.find(u => u.userId === userId);
                    if (existingUser) {
                        if (existingUser.deepBlock !== isDeep) {
                            existingUser.deepBlock = isDeep;
                            if (foundName) existingUser.userName = foundName;
                            await saveBlockedData();
                        }
                    } else {
                        const nameToSave = foundName || '未知用户';
                        blockedUsers.push({ userId: userId, deepBlock: isDeep, userName: nameToSave });
                        await saveBlockedData();
                    }
                    alert(`用户 ${displayName} 已被${isDeep ? '深度' : ''}净化。`);
                    window.location.reload();
                });
            });
            return btn;
        };

        const purifyBtn = createButton('净化', 'lk-purify-btn', '净化', false);
        const deepPurifyBtn = createButton('深度净化', 'lk-deep-purify-btn', '深度净化', true);

        if (reportBtn) {
            actionsContainer.insertBefore(purifyBtn, reportBtn);
            actionsContainer.insertBefore(deepPurifyBtn, reportBtn);
        } else {
            actionsContainer.appendChild(purifyBtn);
            actionsContainer.appendChild(deepPurifyBtn);
        }
    }
    function initPostObserver() {
        // Use multiple fallbacks to find a stable root to observe
        const targetNode = document.querySelector('div.css-xt623x')
            || document.querySelector('div.css-1gnk3bx')
            || document.getElementById('__next')
            || document.querySelector('div[data-reactroot]');
        if (!targetNode) {
            setTimeout(initPostObserver, 500);
            return;
        }

        const findPostForContent = (contentEl) => {
            return contentEl.closest('.css-1pp9a0y')
                || contentEl.closest('div.posts-ancor')
                || contentEl.closest('div[class*="post"]')
                || contentEl.closest('article')
                || contentEl.closest('li')
                || contentEl.closest('[data-floor]')
                || contentEl.parentElement;
        };

        // 1. 首次加载时处理已有帖子:以 .thread-content 为锚点,找到所属帖子容器
        targetNode.querySelectorAll('.main-content').forEach(contentEl => {
            const postEl = findPostForContent(contentEl);
            if (postEl) processPost(postEl);
        });

        // 2. 创建观察器处理动态加载:当有新节点加入时,查找其子树中的 .thread-content
        const observer = new MutationObserver((mutationsList) => {
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === 1) {
                            if (node.matches && node.matches('.main-content')) {
                                const postEl = findPostForContent(node);
                                if (postEl) processPost(postEl);
                            }
                            node.querySelectorAll && node.querySelectorAll('.main-content').forEach(contentEl => {
                                const postEl = findPostForContent(contentEl);
                                if (postEl) processPost(postEl);
                            });
                        }
                    });
                }
            }
        });

        observer.observe(targetNode, { childList: true, subtree: true });
        console.log("LKong Blocker: 帖子内容监视器已启动。");
    }

   // --- 2.4 帖子折叠功能 ---
   function addFoldingFeature(postElement) {
       const content = postElement.querySelector('.main-content');
       // Based on user feedback, the button should be next to the floor number (e.g., #1, #2).
       // The floor number is inside a div with the class 'right-area'.
       const rightArea = postElement.querySelector('.right-area');

       if (!content || !rightArea || rightArea.querySelector('.fold-button')) {
           return; // Skip if essential elements are missing or button exists
       }

       if (content.offsetHeight > 600) {
           const foldButton = document.createElement('a');
           foldButton.textContent = '折叠';
           foldButton.className = 'fold-button';
           foldButton.href = 'javascript:void(0);';
           foldButton.dataset.folded = 'false';

           // Prepend the button to the 'right-area' div to place it before the floor number.
           rightArea.prepend(foldButton);

           foldButton.addEventListener('click', (e) => {
               e.preventDefault();
               const isFolded = foldButton.dataset.folded === 'true';
               if (isFolded) {
                   content.classList.remove('folded');
                   foldButton.textContent = '折叠';
                   foldButton.dataset.folded = 'false';
               } else {
                   content.classList.add('folded');
                   foldButton.textContent = '展开';
                   foldButton.dataset.folded = 'true';
               }
           });
       }
   }

   function createFoldAllButton() {
       if (document.getElementById('fold-all-btn')) return;

       const foldAllBtn = document.createElement('div');
       foldAllBtn.id = 'fold-all-btn';
       foldAllBtn.textContent = '一键折叠';
       document.body.appendChild(foldAllBtn);

       foldAllBtn.addEventListener('click', () => {
           const foldButtons = document.querySelectorAll('.fold-button');
           foldButtons.forEach(button => {
               if (button.dataset.folded === 'false') {
                   button.click();
               }
           });
       });
   }

    // ================== 3. 数据拦截 (用于论坛列表页) ==================
    // (这部分无需修改)
    function handleApiResponse(responseText) {
        try {
            const data = JSON.parse(responseText);
            const threads = data?.data?.threads;
            if (threads) {
                setTimeout(() => processThreadsData(threads), 500);
            }
        } catch (e) { /* 忽略 */ }
    }
    const originalXhrSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = function(...args) {
        this.addEventListener('load', function() {
            if (this.responseURL?.includes('api.lkong.com/api') && this.status === 200) {
                if (this.responseType === 'blob') this.response.text().then(handleApiResponse);
                else handleApiResponse(this.responseText);
            }
        });
        return originalXhrSend.apply(this, args);
    };
    const originalFetch = unsafeWindow.fetch;
    unsafeWindow.fetch = async function(...args) {
        const response = await originalFetch(...args);
        const url = args[0] instanceof Request ? args[0].url : args[0];
        if (typeof url === 'string' && url.includes('api.lkong.com/api')) {
            response.clone().text().then(handleApiResponse);
        }
        return response;
    };


    // ================== 4. UI 与 DOM 相关操作 ==================
    function handleInitialData() {
        const nextDataScript = document.getElementById('__NEXT_DATA__');
        if (!nextDataScript) return;
        try {
            const data = JSON.parse(nextDataScript.textContent);
            const allThreads = data?.props?.pageProps?.threads || [];
            if (data?.props?.pageProps?.source?.topThreads) {
                allThreads.push(...data.props.pageProps.source.topThreads);
            }
            if (allThreads.length > 0) setTimeout(() => processThreadsData(allThreads), 100);
        } catch (e) {
            console.error('LKong Blocker: 解析 __NEXT_DATA__ 失败', e);
        }
    }

    // (修改) 扩展管理UI以支持三类屏蔽
    function createManagerUI() {
        GM_addStyle(`
            /* --- General --- */
            #lk-block-manager-btn {position: fixed; bottom: 135px; right: 20px; background-color: #007bff; color: white; padding: 10px 15px; border-radius: 5px; cursor: pointer; z-index: 9999; font-size: 14px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); }
            #lk-block-manager-btn:hover {
                transform: translateY(-2px);
                box-shadow: 0 6px 12px rgba(0,0,0,0.2);
            }

            #lk-block-manager-panel {
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                width: 550px;
                max-width: 95vw;
                max-height: 90vh;
                background-color: #fcfcfc;
                border: 1px solid #e0e0e0;
                border-radius: 12px;
                z-index: 10000;
                box-shadow: 0 5px 20px rgba(0,0,0,0.2);
                display: none;
                flex-direction: column;
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            }

            /* --- Header --- */
            .lk-manager-header {
                padding: 16px 24px;
                border-bottom: 1px solid #e0e0e0;
            }
            .lk-manager-header h3 {
                margin: 0;
                text-align: center;
                font-size: 18px;
                font-weight: 600;
                color: #212121;
            }

            /* --- Body & Tabs --- */
            .lk-manager-body {
                padding: 0 24px 24px 24px;
                overflow-y: auto;
                flex-grow: 1;
            }
            .lk-tabs {
                display: flex;
                border-bottom: 1px solid #e0e0e0;
                margin: 0 -24px 20px -24px; /* Extend to panel edges */
                padding: 0 24px;
            }
            .lk-tab {
                padding: 12px 16px;
                cursor: pointer;
                border-bottom: 2px solid transparent;
                margin-bottom: -1px; /* Overlap the container border */
                font-size: 15px;
                color: #666;
                transition: all 0.2s ease;
            }
            .lk-tab:hover {
                background-color: #f5f5f5;
                color: #333;
            }
            .lk-tab.active {
                color: #2196F3;
                font-weight: 600;
                border-bottom-color: #2196F3;
            }
            .lk-tab-content { display: none; }
            .lk-tab-content.active { display: block; }

            /* --- Form Elements --- */
            .lk-manager-body textarea {
                width: 100%;
                box-sizing: border-box;
                height: 250px;
                margin-bottom: 10px;
                font-family: "SF Mono", "Fira Code", "Consolas", monospace;
                resize: vertical;
                border: 1px solid #ccc;
                border-radius: 6px;
                padding: 8px 12px;
                font-size: 13px;
                transition: border-color 0.2s ease, box-shadow 0.2s ease;
            }
            .lk-manager-body textarea:focus {
                outline: none;
                border-color: #2196F3;
                box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2);
            }
            .lk-manager-body p {
                font-size: 13px;
                color: #666;
                margin-top: 0;
                margin-bottom: 10px;
                line-height: 1.5;
            }

            /* --- Footer --- */
            .lk-manager-footer {
                padding: 16px 24px;
                border-top: 1px solid #e0e0e0;
                display: flex;
                justify-content: flex-end;
                background-color: #f5f5f5;
                border-bottom-left-radius: 12px;
                border-bottom-right-radius: 12px;
                gap: 12px;
            }

            /* --- Unified Button Styles --- */
            .lk-btn {
                padding: 9px 18px;
                cursor: pointer;
                border: 1px solid transparent;
                border-radius: 6px;
                font-size: 14px;
                font-weight: 500;
                text-align: center;
                transition: all 0.2s ease-in-out;
                -webkit-font-smoothing: antialiased;
            }
            .lk-btn:hover {
                transform: translateY(-1px);
                box-shadow: 0 2px 4px rgba(0,0,0,0.08);
            }
            .lk-btn:active {
                transform: translateY(0);
                box-shadow: none;
                filter: brightness(0.95);
            }

            /* Button Color Modifiers */
            .lk-btn.lk-btn-primary { background-color: #4CAF50; color: white; border-color: #4CAF50; }
            .lk-btn.lk-btn-secondary { background-color: #2196F3; color: white; border-color: #2196F3; }
            .lk-btn.lk-btn-danger { background-color: #f44336; color: white; border-color: #f44336; }
            .lk-btn.lk-btn-default { background-color: #f0f0f0; color: #333; border-color: #ccc; }
            .lk-btn.lk-btn-default:hover { background-color: #e0e0e0; border-color: #bbb; }

            /* --- User List Specifics --- */
            #lk-blocked-users-list {
                height: 220px;
                overflow-y: auto;
                border: 1px solid #e0e0e0;
                padding: 8px;
                margin-bottom: 15px;
                border-radius: 6px;
                background-color: #fff;
            }
            .lk-user-item {
                display: flex;
                align-items: center;
                justify-content: space-between;
                padding: 6px 8px;
                border-radius: 4px;
                transition: background-color 0.2s ease;
            }
            .lk-user-item:not(:last-child) {
                border-bottom: 1px solid #f0f0f0;
            }
            .lk-user-item:hover {
                background-color: #f5f5f5;
            }
            .lk-user-item label {
                flex-grow: 1;
                display: flex;
                align-items: center; /* Vertical alignment for checkbox */
                cursor: pointer;
            }
            .lk-user-item input[type="checkbox"] {
                margin-right: 12px;
                width: 16px;
                height: 16px;
                accent-color: #2196F3;
            }
            .lk-user-item .user-id {
                font-family: "SF Mono", "Fira Code", "Consolas", monospace;
                font-size: 14px;
                color: #333;
            }
            .lk-user-item .remove-btn {
                margin-left: 15px;
                color: #f44336;
                cursor: pointer;
                font-weight: bold;
                font-size: 20px;
                line-height: 1;
                transition: color 0.2s ease, transform 0.2s ease;
            }
            .lk-user-item .remove-btn:hover {
                color: #d32f2f;
                transform: scale(1.2);
            }

            /* --- Add User Form & Import/Export --- */
            .lk-add-user-form {
                display: flex;
                gap: 10px;
                margin-top: 15px;
            }
            .lk-add-user-form input {
                flex-grow: 1;
                padding: 9px 12px;
                border: 1px solid #ccc;
                border-radius: 6px;
                transition: border-color 0.2s ease, box-shadow 0.2s ease;
            }
            .lk-add-user-form input:focus {
                outline: none;
                border-color: #2196F3;
                box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2);
            }
            .lk-add-user-form button { /* Uses .lk-btn styles now */
                flex-shrink: 0;
            }

            .lk-import-export-section {
                margin-top: 20px;
                padding-top: 20px;
                border-top: 1px solid #e0e0e0; /* Replaces <hr> */
            }
            .lk-import-export-section textarea {
                height: 100px;
            }
            .lk-import-export-buttons {
                display: flex;
                gap: 10px;
                margin-top: 10px;
            }
            .lk-import-export-buttons button {
                flex-grow: 1;
            }

            /* --- Confirmation Modal --- */
            #lk-confirm-modal-overlay {
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background-color: rgba(0, 0, 0, 0.5);
                z-index: 10001; /* Above panel, below modal */
                display: none;
                align-items: center;
                justify-content: center;
            }
            #lk-confirm-modal {
                background-color: #fcfcfc;
                padding: 24px;
                border-radius: 12px;
                box-shadow: 0 5px 20px rgba(0,0,0,0.25);
                width: 400px;
                max-width: 90vw;
                z-index: 10002;
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
                transform: scale(0.95);
                opacity: 0;
                transition: transform 0.2s ease-out, opacity 0.2s ease-out;
            }
            #lk-confirm-modal.visible {
               transform: scale(1);
               opacity: 1;
            }
            #lk-confirm-modal h4 {
                margin-top: 0;
                margin-bottom: 12px;
                font-size: 18px;
                font-weight: 600;
                color: #212121;
                text-align: center;
            }
            #lk-confirm-modal p {
                margin-top: 0;
                margin-bottom: 24px;
                font-size: 15px;
                color: #666;
                line-height: 1.6;
                text-align: center;
            }
            .lk-confirm-actions {
                display: flex;
                justify-content: flex-end;
                gap: 12px;
            }
            .lk-confirm-hint {
               margin-top: 16px;
               text-align: center;
               font-size: 12px;
               color: #999;
            }

           /* --- Post Folding --- */
           .fold-button {
               margin-right: 10px;
               color: #1890ff;
               cursor: pointer;
               font-size: 14px;
           }
           .fold-button:hover {
               text-decoration: underline;
           }
           .main-content.folded {
               display: none;
           }

           /* --- LK custom action wrapper & buttons (avoid using site css-xxxxx) --- */
           .lk-action-wrapper {
               display: inline-flex;
               align-items: center;
               margin-right: 6px;
               cursor: pointer;
           }
           .lk-purify-btn, .lk-deep-purify-btn {
               padding: 4px 8px;
               background: transparent;
               color: #666;
               border-radius: 4px;
               font-size: 13px;
               cursor: pointer;
               margin-left: 6px;
           }
           .lk-purify-btn:hover, .lk-deep-purify-btn:hover {
               color: #2196F3;
           }

           #fold-all-btn { position: fixed; bottom: 90px; right: 20px; background-color: #007bff; color: white; padding: 10px 15px; border-radius: 5px; cursor: pointer; z-index: 9999; font-size: 14px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); }
           #fold-all-btn:hover {
                transform: translateY(-2px);
                box-shadow: 0 4px 10px rgba(0,0,0,0.2);
           }

           .lk-user-item {
                /* 修改这一项,让它稍微高一点好放两行文字 */
                align-items: flex-start;
                padding: 10px;
            }
            .lk-user-info { display: flex; flex-direction: column; margin-left: 10px; flex-grow: 1; }
            .lk-user-name { font-weight: bold; color: #333; font-size: 14px; }
            .lk-user-id-link { font-size: 12px; color: #999; text-decoration: none; margin-top: 4px; }
            .lk-user-id-link:hover { color: #2196F3; text-decoration: underline; }
            #lk-update-names-btn { font-size: 12px; padding: 5px 10px; }
        `);

        const managerBtn = document.createElement('div');
        managerBtn.id = 'lk-block-manager-btn';
        managerBtn.textContent = '管理噪声';
        document.body.appendChild(managerBtn);

        const panel = document.createElement('div');
        panel.id = 'lk-block-manager-panel';
        panel.innerHTML = `
            <div class="lk-manager-header"><h3>噪声管理</h3></div>
            <div class="lk-manager-body">
                <div class="lk-tabs">
                    <div class="lk-tab active" data-tab="users">用户</div>
                    <div class="lk-tab" data-tab="title_keywords">标题关键词</div>
                    <div class="lk-tab" data-tab="reply_keywords">回帖关键词</div>
                </div>
                <div id="lk-tab-users" class="lk-tab-content active">
                    <p>勾选“深度屏蔽”后,该用户的回帖也会在帖子页面被隐藏。</p>
                    <!-- 新增:功能栏 -->
                    <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
                        <span style="font-weight:bold; color:#555;">屏蔽列表</span>
                        <button id="lk-update-names-btn" class="lk-btn lk-btn-default">↺ 自动获取所有未知昵称</button>
                    </div>
                    <div id="lk-blocked-users-list"></div>
                    <div class="lk-add-user-form">
                        <input type="text" id="lk-new-user-id" placeholder="输入用户ID,多个用逗号/空格/换行分隔">
                        <button id="lk-add-user-btn" class="lk-btn lk-btn-secondary">添加</button>
                    </div>
                    <div class="lk-import-export-section">
                        <p>批量导入/导出 (格式: userId,deepBlock):</p>
                        <textarea id="lk-import-export-textarea"></textarea>
                        <div class="lk-import-export-buttons">
                            <button id="lk-export-btn" class="lk-btn lk-btn-default">导出到剪贴板</button>
                            <button id="lk-import-btn" class="lk-btn lk-btn-default">从文本框导入</button>
                            <button id="lk-clear-all-users-btn" class="lk-btn lk-btn-danger" style="margin-left: auto;">清空所有ID</button>
                        </div>
                    </div>
                </div>
                <div id="lk-tab-title_keywords" class="lk-tab-content">
                    <p>每行一个关键词。帖子标题包含任意一个词都会在列表页被隐藏。</p>
                    <textarea id="lk-blocked-title-keywords-textarea"></textarea>
                </div>
                <div id="lk-tab-reply_keywords" class="lk-tab-content">
                    <p>每行一个关键词。帖子内的回帖如果包含任意一个词,该楼层将被隐藏。</p>
                    <textarea id="lk-blocked-reply-keywords-textarea"></textarea>
                </div>
            </div>
            <div class="lk-manager-footer">
                <button id="lk-close-btn" class="lk-btn lk-btn-default">关闭</button>
                <button id="lk-save-btn" class="lk-btn lk-btn-primary">保存并刷新</button>
            </div>
        `;
        document.body.appendChild(panel);

        const userListContainer = panel.querySelector('#lk-blocked-users-list');
        const addUserBtn = panel.querySelector('#lk-add-user-btn');
        const newUserIdInput = panel.querySelector('#lk-new-user-id');

        function renderBlockedUsersList() {
            userListContainer.innerHTML = '';

            if (blockedUsers.length === 0) {
                 userListContainer.innerHTML = '<div style="text-align:center;color:#ccc;padding:20px;">名单为空</div>';
                 return;
            }

            blockedUsers.forEach(user => {
                const displayName = user.userName || '❓ 未获取昵称';
                const displayClass = user.userName ? 'lk-user-name' : 'lk-user-name style="color:#999"';

                const item = document.createElement('div');
                item.className = 'lk-user-item';
                item.innerHTML = `
                    <label style="margin-top:2px;">
                        <input type="checkbox" class="deep-block-cb" data-userid="${user.userId}" ${user.deepBlock ? 'checked' : ''}>
                    </label>
                    <div class="lk-user-info">
                        <span ${displayClass}>${displayName}</span>
                        <a href="/user/${user.userId}" target="_blank" class="lk-user-id-link">ID: ${user.userId} (点击访问主页)</a>
                    </div>
                    <span class="remove-btn" data-userid="${user.userId}" title="移除">×</span>
                `;
                userListContainer.appendChild(item);
            });

            // 重新绑定事件
            userListContainer.querySelectorAll('.deep-block-cb').forEach(cb => {
                cb.addEventListener('change', (e) => {
                    const targetUser = blockedUsers.find(u => u.userId === e.target.dataset.userid);
                    if (targetUser) {
                        targetUser.deepBlock = e.target.checked;
                        saveBlockedData();
                    }
                });
            });

            userListContainer.querySelectorAll('.remove-btn').forEach(btn => {
                btn.addEventListener('click', (e) => {
                    const uid = e.target.dataset.userid;
                    const uInfo = blockedUsers.find(u => u.userId === uid);
                    const name = uInfo && uInfo.userName ? uInfo.userName : uid;

                    confirmationModal.show(`确定移除 ${name} 吗?`, () => {
                        blockedUsers = blockedUsers.filter(u => u.userId !== uid);
                        saveBlockedData();
                        renderBlockedUsersList();
                    });
                });
            });
        }

        const updateNamesBtn = panel.querySelector('#lk-update-names-btn');
        updateNamesBtn.addEventListener('click', async () => {
            const unknownList = blockedUsers.filter(u => !u.userName || u.userName === '未知用户' || u.userName === '❓ 未获取昵称' || u.userName.includes('获取失败'));

            if (unknownList.length === 0) {
                alert('所有用户的昵称都已经是最新的了!');
                return;
            }

            const confirmUpdate = confirm(`发现 ${unknownList.length} 个用户没有记录昵称。是否立即通过API请求获取?\n(这可能需要几秒钟)`);
            if (!confirmUpdate) return;

            updateNamesBtn.disabled = true;
            let successCount = 0;

            for (let i = 0; i < unknownList.length; i++) {
                const user = unknownList[i];
                updateNamesBtn.textContent = `获取中 (${i + 1}/${unknownList.length})...`;

                // 为了防止并发过高被API拦截,每个请求间隔 300 毫秒
                if (i > 0) await new Promise(r => setTimeout(r, 300));

                const name = await fetchUserName(user.userId);
                if (name) {
                    user.userName = name;
                    successCount++;
                } else {
                    user.userName = "获取失败(无动态)"; // 标记,避免下次反复请求死循环
                }
            }

            await saveBlockedData();
            renderBlockedUsersList();

            updateNamesBtn.disabled = false;
            updateNamesBtn.textContent = '↺ 自动获取所有未知昵称';
            alert(`完成!成功更新了 ${successCount} 个用户的昵称。`);
        });

        addUserBtn.addEventListener('click', () => {
            const idInputs = newUserIdInput.value.split(/[,\s\n]+/);
            let newUsersAdded = false;

            for (const id of idInputs) {
                const userId = id.trim();
                // Validate that it is a numerical ID and not empty
                if (userId && /^\d+$/.test(userId)) {
                    // Check if it already exists to avoid duplicates
                    if (!blockedUsers.some(u => u.userId === userId)) {
                        blockedUsers.push({ userId, deepBlock: false });
                        newUsersAdded = true;
                    }
                }
            }

            // After processing all IDs, save the updated list and re-render
            if (newUsersAdded) {
                saveBlockedData();
                renderBlockedUsersList();
            }
            // Clear the input field
            newUserIdInput.value = '';
        });

        // --- Keyboard Shortcut for Add User ---
        newUserIdInput.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                e.preventDefault(); // Prevent form submission if it were in a form
                addUserBtn.click();
            }
        });

        // --- Import/Export Logic ---
        const exportBtn = panel.querySelector('#lk-export-btn');
        const importBtn = panel.querySelector('#lk-import-btn');
        const importExportTextarea = panel.querySelector('#lk-import-export-textarea');

        exportBtn.addEventListener('click', () => {
            const exportData = blockedUsers.map(user => `${user.userId},${user.deepBlock}`).join('\n');
            importExportTextarea.value = exportData;
            navigator.clipboard.writeText(exportData).then(() => {
                alert('已导出并复制到剪贴板!');
            }).catch(err => {
                console.error('LKong Blocker: 复制到剪贴板失败', err);
                alert('导出成功,但自动复制失败。请手动复制。');
            });
        });

        importBtn.addEventListener('click', () => {
            const importData = importExportTextarea.value.trim();
            if (!importData) {
                alert('导入内容为空。');
                return;
            }

            confirmationModal.show('确定要从文本框导入吗?这将合并现有列表,重复的用户ID将被覆盖。', () => {
                const lines = importData.split('\n');
                const importedUsersMap = new Map();
                let invalidLines = 0;

                for (const line of lines) {
                    const trimmedLine = line.trim();
                    if (!trimmedLine) continue; // Skip empty lines

                    const parts = trimmedLine.split(',');
                    const userId = parts[0].trim();

                    if (!/^\d+$/.test(userId)) {
                        invalidLines++;
                        continue;
                    }

                    if (parts.length === 1) {
                        // Old format: just userId, default deepBlock to false
                        importedUsersMap.set(userId, { userId, deepBlock: false });
                    } else if (parts.length >= 2) {
                        // New format: userId,deepBlock
                        const deepBlock = parts[1].trim().toLowerCase() === 'true';
                        importedUsersMap.set(userId, { userId, deepBlock });
                    } else {
                        // Any other case is considered invalid
                        invalidLines++;
                    }
                }

                if (invalidLines > 0) {
                    alert(`有 ${invalidLines} 行格式不正确,已被忽略。`);
                }

                if (importedUsersMap.size === 0) {
                    alert('没有解析到有效的用户数据。');
                    return;
                }

                // Merge logic
                const existingUsersMap = new Map(blockedUsers.map(u => [u.userId, u]));

                for (const [userId, user] of importedUsersMap.entries()) {
                    existingUsersMap.set(userId, user);
                }

                blockedUsers = Array.from(existingUsersMap.values());
                blockedUsers.sort((a, b) => parseInt(a.userId, 10) - parseInt(b.userId, 10)); // Sort for consistency

                saveBlockedData();
                renderBlockedUsersList();
                alert(`导入成功!共处理 ${importedUsersMap.size} 个用户。`);
                importExportTextarea.value = ''; // Clear textarea after import
            });
        });

        const clearAllUsersBtn = panel.querySelector('#lk-clear-all-users-btn');
        clearAllUsersBtn.addEventListener('click', () => {
            confirmationModal.show('您确定要清空所有已屏蔽的用户ID吗?此操作无法撤销。', () => {
                blockedUsers = [];
                saveBlockedData();
                renderBlockedUsersList();
                alert('所有用户ID已被清空。');
            });
        });


        const tabs = panel.querySelectorAll('.lk-tab');
        const titleKeywordTextarea = panel.querySelector('#lk-blocked-title-keywords-textarea');
        const replyKeywordTextarea = panel.querySelector('#lk-blocked-reply-keywords-textarea');
        const saveBtn = panel.querySelector('#lk-save-btn');
        const closeBtn = panel.querySelector('#lk-close-btn');

        tabs.forEach(tab => {
            tab.addEventListener('click', () => {
                panel.querySelector('.lk-tab.active').classList.remove('active');
                panel.querySelector('.lk-tab-content.active').classList.remove('active');
                tab.classList.add('active');
                panel.querySelector(`#lk-tab-${tab.dataset.tab}`).classList.add('active');
            });
        });

        // --- Panel Keyboard Shortcuts & Visibility ---
        let panelKeydownHandler = null;

        const hidePanel = () => {
            panel.style.display = 'none';
            if (panelKeydownHandler) {
                window.removeEventListener('keydown', panelKeydownHandler);
                panelKeydownHandler = null;
            }
        };

        const showPanel = () => {
            renderBlockedUsersList();
            titleKeywordTextarea.value = Array.from(blockedTitleKeywords).join('\n');
            replyKeywordTextarea.value = Array.from(blockedReplyKeywords).join('\n');
            panel.style.display = 'flex';

            panelKeydownHandler = (e) => {
                if (panel.style.display !== 'flex') return;

                // Check for active element to avoid conflicts
                const activeEl = document.activeElement;
                if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA') && e.key !== 'Escape') {
                    // Allow typing in inputs, but let Escape work
                    if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
                        // still allow save shortcut from inputs
                    } else {
                        return;
                    }
                }

                if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
                    e.preventDefault();
                    saveBtn.click();
                } else if (e.key === 'Escape') {
                    e.preventDefault();
                    hidePanel();
                }
            };
            window.addEventListener('keydown', panelKeydownHandler);
        };

        managerBtn.addEventListener('click', showPanel);
        closeBtn.addEventListener('click', hidePanel);

        saveBtn.addEventListener('click', async () => {
            blockedTitleKeywords = new Set(titleKeywordTextarea.value.split('\n').map(kw => kw.trim()).filter(Boolean));
            blockedReplyKeywords = new Set(replyKeywordTextarea.value.split('\n').map(kw => kw.trim()).filter(Boolean));

            await saveBlockedData();

            alert('关键词名单已保存!页面将刷新以应用更改。');
            hidePanel(); // Use the new function to ensure listener removal
            window.location.reload();
        });
    }

    function createConfirmationModal() {
        const modalOverlay = document.createElement('div');
        modalOverlay.id = 'lk-confirm-modal-overlay';

        modalOverlay.innerHTML = `
            <div id="lk-confirm-modal">
                <h4>确认操作</h4>
                <p id="lk-confirm-msg">您确定要执行此操作吗?</p>
                <div class="lk-confirm-actions">
                    <button id="lk-confirm-cancel-btn" class="lk-btn lk-btn-default">取消</button>
                    <button id="lk-confirm-ok-btn" class="lk-btn lk-btn-primary">确认</button>
                </div>
                <div class="lk-confirm-hint">按 Enter 确认, Esc 取消</div>
            </div>
        `;
        document.body.appendChild(modalOverlay);

        const modal = modalOverlay.querySelector('#lk-confirm-modal');
        const cancelBtn = modalOverlay.querySelector('#lk-confirm-cancel-btn');
        let okBtn = modalOverlay.querySelector('#lk-confirm-ok-btn');

        // This will hold the currently active keydown handler
        let keydownHandler = null;

        const hide = () => {
            if (keydownHandler) {
                window.removeEventListener('keydown', keydownHandler, true);
                keydownHandler = null;
            }
            modal.classList.remove('visible');
            setTimeout(() => { modalOverlay.style.display = 'none'; }, 200);
        };

        modalOverlay.addEventListener('click', (e) => {
            if (e.target.id === 'lk-confirm-modal-overlay') {
                hide();
            }
        });

        cancelBtn.addEventListener('click', hide);

        return {
            show: (message, onConfirm) => {
                modal.querySelector('#lk-confirm-msg').textContent = message;

                // Clone and replace the button to ensure old listeners are removed
                const newOkBtn = okBtn.cloneNode(true);
                okBtn.parentNode.replaceChild(newOkBtn, okBtn);
                okBtn = newOkBtn;

                const confirmAndHide = () => {
                    onConfirm();
                    hide();
                };

                okBtn.addEventListener('click', confirmAndHide);

                // Define and add the keydown listener for this specific showing
                keydownHandler = (e) => {
                    // Only act if the modal is visible
                    if (modalOverlay.style.display !== 'flex') return;

                    if (e.key === 'Enter') {
                        e.preventDefault();
                        confirmAndHide();
                    } else if (e.key === 'Escape') {
                        e.preventDefault();
                        hide();
                    }
                };
                window.addEventListener('keydown', keydownHandler, true);

                modalOverlay.style.display = 'flex';
                setTimeout(() => {
                    modal.classList.add('visible');
                    okBtn.focus(); // Focus the confirm button
                }, 10);
            }
        };
    }

    // ================== 5. 启动脚本 ==================
    let confirmationModal; // Make it accessible script-wide

    async function main() {
        await loadBlockedData();

        if (document.body) {
            createManagerUI();
            confirmationModal = createConfirmationModal(); // Initialize the modal

            // 根据当前页面路径,执行不同的核心逻辑
            if (window.location.pathname.startsWith('/thread/')) {
                // 在帖子详情页,启动帖子内容监视器
                initPostObserver();
                createFoldAllButton();
            } else if (window.location.pathname.startsWith('/forum/')) {
                // 在论坛列表页,处理首屏数据
                handleInitialData();
            }

        } else {
            document.addEventListener('DOMContentLoaded', main, { once: true });
        }
    }

    // 脚本启动
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', main, { once: true });
    } else {
        main();
    }
})();