Greasy Fork

来自缓存

Greasy Fork is available in English.

优学院讨论区匿名替换真实姓名 v0.1

让我看看都是谁在评论!

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         优学院讨论区匿名替换真实姓名 v0.1
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  让我看看都是谁在评论!
// @author       青柍kk
// @match        https://discussion.ulearning.cn/pc.html*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      discussion.ulearning.cn
// @connect courseapi.ulearning.cn
// @connect      api.ulearning.cn
// @connect      www.ulearning.cn
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- --- 配置区 (根据提供信息填写) --- ---

    // 1. 成员列表 API 的完整 URL (获取全班/全课程用户名单的分页接口基础URL)
    const MEMBER_API_URL = 'https://courseapi.ulearning.cn/student/list?ocId=147496&isDesc=1&pn=1&ps=10&lang=zh'; // 成员列表API基础URL

    // 1a. 成员列表 API 分页相关字段名确认
    const MEMBER_API_PAGE_PARAM = 'pn';// 页码参数名 (来自 URL)
    const MEMBER_API_PAGESIZE_PARAM = 'ps';// 页面大小参数名 (来自 URL)
    const MEMBER_API_TOTAL_FIELD = 'total';// 返回JSON中总数字段 (来自成员列表JSON)
    const MEMBER_API_PAGESIZE_FIELD = 'pageSize';// 返回JSON中页面大小字段 (来自成员列表JSON)
    const MEMBER_API_LIST_FIELD = 'list';// 返回JSON中用户数字段 (来自成员列表JSON)
    const MEMBER_API_USERID_FIELD = 'userId';// 用户对象中用户ID字段 (来自成员列表JSON)
    const MEMBER_API_USERNAME_FIELD = 'name';// 用户对象中姓名字段 (来自成员列表JSON)

    // 1b. **尝试** 成员列表 API 页面大小 (尝试一次性获取更多,减少请求次数, API可能有限制)
    const MEMBER_API_PREFERRED_PAGESIZE = 200; // 尝试每页获取200个,如果不行API会按自己的限制来

    // 2. 讨论帖子列表 API 的 URL 模板 (获取当前讨论页面帖子内容的接口模板)
    const TOPIC_INFO_API_TEMPLATE = 'https://courseapi.ulearning.cn/topic/topicInfo?pn={pageNum}&ps={pageSize}&ocId={ocId}&discussionId={discussionId}&teacherId=&classId=&orderType=&mine=false&keyword='; // 讨论帖子API模板

    // 2a. 讨论帖子 API 结果解析确认
    const TOPIC_INFO_LIST_PATH = ['result', 'pageInfo', 'list']; // 帖子列表在JSON中的路径
    const TOPIC_INFO_USERID_FIELD = 'userID';// 帖子对象中用户ID字段

    // 3. 评论区容器的 CSS 选择器 (包含所有评论和分页控件)
    const COMMENT_CONTAINER_SELECTOR = '.contentDetails'; // 包含所有评论和分页的容器

    // 4. 单个评论项/块的 CSS 选择器
    const COMMENT_ITEM_SELECTOR = '.infoText'; // 单个评论块

    // 5. 用户名元素相对于单个评论项的 CSS 选择器 (显示"匿名"的地方)
    const USERNAME_SELECTOR = '.teacherTipsTop .span1'; // 显示用户名的元素

    // 6. 当前页码的 CSS 选择器 (分页控件中高亮的页码)
    const PAGINATION_CURRENT_PAGE_SELECTOR = '.pagination-wrap .active'; // 分页控件当前页码

    // 7. 讨论区每页帖子数量 (必须与 topicInfo API 的 ps 参数一致)
    const DISCUSSION_PAGE_SIZE = 20; // 讨论区每页帖子数

    // --- --- 配置区结束 --- ---


    let userIdToNameMap = null; // userID -> 真实姓名的映射
    let isFetchingUserMap = false; // 是否正在获取用户映射的标记
    let userMapPromise = null; // 保存获取用户映射的 Promise
    let currentOcId = null; // 当前课程/组织 ID
    let currentDiscussionId = null; // 当前讨论 ID
    let authToken = null; // 认证 Token
    let lastProcessedPage = -1; // 记录上次处理的页码,防止重复处理

    // --- 样式 ---
    GM_addStyle(`
        .real-name-revealed {
            color: #007bff !important; /* 蓝色 */
            font-weight: bold !important;
            cursor: help; /* 添加一个鼠标悬停提示 */
            position: relative;
            background-color: rgba(0, 123, 255, 0.08); /* 更淡的蓝色背景 */
            padding: 0px 3px;
            border-radius: 3px;
            border: 1px solid rgba(0, 123, 255, 0.2); /* 浅蓝色边框 */
        }
        /* 可选:在名字后面加个标记 */
        .real-name-revealed::after {
            content: ' (R)'; /* R = Revealed / Real */
            font-size: 0.8em;
            color: #6c757d; /* 灰色 */
            margin-left: 3px;
            font-weight: normal; /* 标记不需要加粗 */
        }
        /* 可选: 鼠标悬停显示原始 UserID */
        .real-name-revealed:hover::before {
            content: "ID:" attr(data-original-userid); /* 显示 "ID:xxxx" */
            position: absolute;
            bottom: 105%; /* 稍微向上一点 */
            left: 50%;
            transform: translateX(-50%);
            background-color: #444; /* 深灰色背景 */
            color: white;
            padding: 3px 6px;
            border-radius: 4px;
            font-size: 10px; /* 稍小一点 */
            line-height: 1.2;
            white-space: nowrap;
            z-index: 1000; /* 确保在顶层 */
            box-shadow: 0 1px 3px rgba(0,0,0,0.2);
        }
    `);

    // --- 工具函数 ---

    function getParamsFromUrl() {
        const params = {};
        const hash = window.location.hash;
        if (hash.includes('?')) {
            const urlParams = new URLSearchParams(hash.substring(hash.indexOf('?')));
            params.ocId = urlParams.get('ocId');
            params.discussionId = urlParams.get('discussionId');
        }
        return params;
    }

    function getTokenFromCookie() {
        const cookies = document.cookie;
        const tokenMatch = /token=([^;]+)/.exec(cookies);
        if (tokenMatch && tokenMatch[1]) {
            return tokenMatch[1];
        }
        console.error("未能从 Cookie 中找到 token!");
        return null;
    }

    function getCurrentPageNumber() {
        try {
            const activePageElement = document.querySelector(PAGINATION_CURRENT_PAGE_SELECTOR);
            if (activePageElement) {
                const pageNum = parseInt(activePageElement.textContent.trim(), 10);
                return (isNaN(pageNum) || pageNum < 1) ? 1 : pageNum;
            }
        } catch (e) {
            console.error("获取当前页码时出错:", e);
        }
        console.warn(`无法找到当前页码元素 (${PAGINATION_CURRENT_PAGE_SELECTOR}),默认返回页码 1`);
        return 1;
    }

     // 安全地从嵌套对象获取值
    function getNestedValue(obj, pathArray) {
        return pathArray.reduce((currentValue, key) => {
            return (currentValue && typeof currentValue === 'object' && key in currentValue) ? currentValue[key] : undefined;
        }, obj);
    }


    // --- API 请求函数 ---

    // 获取 *所有* 成员列表 (处理分页)
    function fetchAllUsers() {
        if (isFetchingUserMap) return userMapPromise;
        if (userIdToNameMap) return Promise.resolve(userIdToNameMap);
        if (!authToken) return Promise.reject("缺少认证 Token (fetchAllUsers)");

        isFetchingUserMap = true;
        userIdToNameMap = {};
        console.log("[成员列表] 开始获取所有成员数据...");

        function fetchPage(pageNumber) {
            return new Promise((resolvePage) => { // 改为只 resolve,在 Promise.all 外处理失败
                let pageUrl;
                try {
                    pageUrl = new URL(MEMBER_API_URL);
                    pageUrl.searchParams.set(MEMBER_API_PAGE_PARAM, pageNumber);
                    pageUrl.searchParams.set(MEMBER_API_PAGESIZE_PARAM, MEMBER_API_PREFERRED_PAGESIZE); // 尝试设置期望的页面大小
                } catch (e) {
                    console.error("[成员列表] 处理 URL 时出错:", e);
                    resolvePage({ success: false, error: "URL 处理错误" }); return;
                }
                const urlForPage = pageUrl.toString();
                console.log(`  [成员列表] 获取第 ${pageNumber} 页 (尝试 ps=${MEMBER_API_PREFERRED_PAGESIZE})...`);

                GM_xmlhttpRequest({
                    method: 'GET', url: urlForPage, responseType: 'json', timeout: 30000,
                    headers: { 'AUTHORIZATION': authToken },
                    onload: function(response) {
                        if (response.status >= 200 && response.status < 300 && response.response) {
                            const data = response.response;
                            const userList = getNestedValue(data, [MEMBER_API_LIST_FIELD]); // 使用确认的字段名
                            if (userList && Array.isArray(userList)) {
                                userList.forEach(user => {
                                    const userId = getNestedValue(user, [MEMBER_API_USERID_FIELD]);
                                    const userName = getNestedValue(user, [MEMBER_API_USERNAME_FIELD]);
                                    if (userId && userName) {
                                        userIdToNameMap[userId] = userName.trim();
                                    }
                                });
                                resolvePage({ success: true, data: data }); // 传递原始数据
                            } else {
                                console.error(`  [成员列表] 第 ${pageNumber} 页数据结构不符 (${MEMBER_API_LIST_FIELD} 路径错误):`, data);
                                resolvePage({ success: false, error: "数据结构错误" });
                            }
                        } else {
                            console.error(`  [成员列表] 获取第 ${pageNumber} 页失败,状态码: ${response.status}`);
                            resolvePage({ success: false, error: `HTTP ${response.status}` });
                        }
                    },
                    onerror: (error) => { console.error(`  [成员列表] 第 ${pageNumber} 页网络错误:`, error); resolvePage({ success: false, error: "网络错误" }); },
                    ontimeout: () => { console.error(`  [成员列表] 第 ${pageNumber} 页请求超时`); resolvePage({ success: false, error: "请求超时" }); }
                });
            });
        }

        userMapPromise = new Promise(async (resolve, reject) => {
            try {
                console.log("  [成员列表] 获取第一页以确定总页数...");
                const firstPageResult = await fetchPage(1);

                if (!firstPageResult.success || !firstPageResult.data) {
                     reject(`获取第一页成员列表失败: ${firstPageResult.error || '未知错误'}`);
                     isFetchingUserMap = false; return;
                }

                const firstPageData = firstPageResult.data;
                const totalUsers = parseInt(getNestedValue(firstPageData, [MEMBER_API_TOTAL_FIELD]), 10);
                const actualPageSize = parseInt(getNestedValue(firstPageData, [MEMBER_API_PAGESIZE_FIELD]), 10);

                if (isNaN(totalUsers) || isNaN(actualPageSize) || actualPageSize <= 0) {
                    console.error("  [成员列表] 无法从第一页获取有效分页信息。", firstPageData);
                    if (Object.keys(userIdToNameMap).length > 0) {
                        console.warn("  [成员列表] 将仅使用第一页数据。"); resolve(userIdToNameMap);
                    } else { reject("分页信息无效"); }
                    isFetchingUserMap = false; return;
                }

                const totalPages = Math.ceil(totalUsers / actualPageSize); // 使用API返回的实际页面大小计算总页数
                console.log(`  [成员列表] 总用户数: ${totalUsers}, 实际每页大小: ${actualPageSize}, 总页数: ${totalPages}`);

                if (totalPages > 1) {
                    const pagePromises = [];
                    for (let i = 2; i <= totalPages; i++) {
                        pagePromises.push(fetchPage(i));
                    }
                    console.log(`  [成员列表] 并发获取第 2 页到第 ${totalPages} 页...`);
                    const results = await Promise.all(pagePromises); // 等待所有请求完成
                    // 检查是否有失败的请求
                    const failedPages = results.filter(r => !r.success).length;
                    if (failedPages > 0) {
                        console.warn(`  [成员列表] ${failedPages} 个页面获取失败或处理出错。`);
                    }
                    console.log("  [成员列表] 所有页面请求完成。");
                }

                console.log(`[成员列表] 最终处理了 ${Object.keys(userIdToNameMap).length} 个用户映射 (API报告总数 ${totalUsers})。`);
                isFetchingUserMap = false;
                resolve(userIdToNameMap);

            } catch (error) {
                console.error("[成员列表] 获取所有分页数据时出错:", error);
                isFetchingUserMap = false;
                if (Object.keys(userIdToNameMap).length > 0) {
                    console.warn("[成员列表] 过程出错,将仅使用部分数据。"); resolve(userIdToNameMap);
                } else { reject(error); }
            }
        });
        return userMapPromise;
    }

    // 获取 *指定页* 的讨论帖子列表数据
    function fetchTopicInfoPageData(ocId, discussionId, pageNum, pageSize) {
         if (!authToken) return Promise.reject("缺少认证 Token (fetchTopicInfoPageData)");
         if (!ocId || !discussionId) return Promise.reject("缺少 ocId 或 discussionId");

        let apiUrl = TOPIC_INFO_API_TEMPLATE
            .replace('{ocId}', encodeURIComponent(ocId))
            .replace('{discussionId}', encodeURIComponent(discussionId))
            .replace('{pageNum}', encodeURIComponent(pageNum))
            .replace('{pageSize}', encodeURIComponent(pageSize));

        console.log(`[讨论帖子] 获取第 ${pageNum} 页 (ps=${pageSize})...`);

        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET', url: apiUrl, responseType: 'json', timeout: 15000,
                headers: { 'AUTHORIZATION': authToken },
                onload: function(response) {
                    if (response.status >= 200 && response.status < 300 && response.response) {
                        const data = response.response;
                        // 使用确认的路径和字段名解析
                        const postList = getNestedValue(data, TOPIC_INFO_LIST_PATH);
                        if (data && data.code === 1 && postList && Array.isArray(postList)) {
                             console.log(`  [讨论帖子] 第 ${pageNum} 页获取成功,帖子数: ${postList.length}`);
                             resolve(postList); // 返回帖子列表数组
                        } else {
                             console.error(`  [讨论帖子] 第 ${pageNum} 页数据结构不符或 code 不为 1:`, data);
                             reject(`讨论帖子API数据错误 (code:${data?.code})`);
                        }
                    } else {
                         console.error(`  [讨论帖子] 获取第 ${pageNum} 页失败,状态码: ${response.status}`);
                         reject(`获取讨论帖子失败,状态码: ${response.status}`);
                    }
                },
                onerror: (error) => { console.error(`  [讨论帖子] 第 ${pageNum} 页网络错误:`, error); reject("网络错误"); },
                ontimeout: () => { console.error(`  [讨论帖子] 第 ${pageNum} 页请求超时`); reject("请求超时"); }
            });
        });
    }


    // --- 核心处理逻辑 ---

    // 处理当前可见的评论及其回复
    function processVisibleComments(topicInfoPageData, userMap) {
         if (!topicInfoPageData || topicInfoPageData.length === 0) {
             console.log("没有当前页的讨论帖子数据可供处理。"); return;
         }
         if (!userMap) {
             console.warn("用户映射数据尚未准备好,无法进行名称替换。");
         }

         console.log(`准备使用 ${topicInfoPageData.length} 条主帖子数据处理页面元素...`);
         const commentElements = document.querySelectorAll(COMMENT_ITEM_SELECTOR); // 获取所有主评论元素
         console.log(`在页面上找到 ${commentElements.length} 个主评论元素 (${COMMENT_ITEM_SELECTOR})。`);

         const processLength = Math.min(topicInfoPageData.length, commentElements.length);
         if (processLength === 0) { console.log("没有可匹配的主帖子数据和 DOM 元素。"); return; }
         if (topicInfoPageData.length !== commentElements.length) {
              console.warn(`警告:API主帖子数 (${topicInfoPageData.length}) 与页面元素数 (${commentElements.length}) 不匹配!将只处理前 ${processLength} 个。`);
         }

         let totalReplaced = 0;
         for (let i = 0; i < processLength; i++) {
             const mainPostData = topicInfoPageData[i];       // 当前主帖的 API 数据
             const mainCommentElement = commentElements[i]; // 页面上对应的主评论元素

             // 1. 处理主评论作者
             try {
                 const usernameElement = mainCommentElement.querySelector(USERNAME_SELECTOR); // 主评论用户名元素的选择器
                 if (usernameElement && usernameElement.textContent.trim() === '匿名') {
                     const userId = getNestedValue(mainPostData, [TOPIC_INFO_USERID_FIELD]); // 主帖的用户 ID
                     if (userId && userMap && userMap[userId]) {
                         const realName = userMap[userId];
                         usernameElement.textContent = realName;
                         usernameElement.classList.add('real-name-revealed');
                         usernameElement.setAttribute('data-original-userid', userId);
                         totalReplaced++;
                     }
                 } else if (usernameElement && usernameElement.textContent.trim() !== '匿名' && !usernameElement.classList.contains('real-name-revealed')) {
                      const userId = getNestedValue(mainPostData, [TOPIC_INFO_USERID_FIELD]);
                      if (userId) {
                           usernameElement.classList.add('real-name-revealed');
                           usernameElement.setAttribute('data-original-userid', userId);
                      }
                 }
             } catch (e) {
                  console.error(`处理第 ${i} 个主评论作者时出错:`, e, mainCommentElement);
             }

             // 2. 处理该主评论下的回复 (二级评论)
             const subListData = getNestedValue(mainPostData, ['subList', 'list']); // 从主帖数据获取回复列表
             if (subListData && Array.isArray(subListData) && subListData.length > 0) {
                 // 查找当前主评论元素下的所有回复元素
                 const replyElements = mainCommentElement.querySelectorAll('.secondText'); // 选择器是 .secondText
                 console.log(`  主帖 #${i + 1}: 找到 ${subListData.length} 条回复数据 和 ${replyElements.length} 个回复元素。`);

                 const replyProcessLength = Math.min(subListData.length, replyElements.length);
                 if (subListData.length !== replyElements.length) {
                      console.warn(`  主帖 #${i + 1}: 回复数据数 (${subListData.length}) 与回复元素数 (${replyElements.length}) 不匹配!将只处理前 ${replyProcessLength} 个。`);
                 }

                 for (let j = 0; j < replyProcessLength; j++) {
                     const replyData = subListData[j];// 当前回复的 API 数据
                     const replyElement = replyElements[j];// 页面上对应的回复元素

                     try {
                         // 查找回复元素中的名字 span
                         const replyNameElement = replyElement.querySelector('.name'); // 回复名字的选择器是 .name
                         if (replyNameElement && replyNameElement.textContent.trim() === '匿名') {
                             const replyUserId = getNestedValue(replyData, [TOPIC_INFO_USERID_FIELD]); // 回复者的 UserID
                             if (replyUserId && userMap && userMap[replyUserId]) {
                                 const realName = userMap[replyUserId];
                                 // console.log(`    回复 #${j + 1}: UserID ${replyUserId} -> ${realName}`);
                                 replyNameElement.textContent = realName;
                                 replyNameElement.classList.add('real-name-revealed');
                                 replyNameElement.setAttribute('data-original-userid', replyUserId);
                                 totalReplaced++;
                             } else if (replyUserId && !userMap) {
                                 // 等待映射加载
                             } else if (replyUserId) {
                                 // console.log(`    回复 #${j + 1}: 未在映射中找到 UserID ${replyUserId}`);
                             }
                         } else if (replyNameElement && replyNameElement.textContent.trim() !== '匿名' && !replyNameElement.classList.contains('real-name-revealed')) {
                              const replyUserId = getNestedValue(replyData, [TOPIC_INFO_USERID_FIELD]);
                              if (replyUserId) {
                                  replyNameElement.classList.add('real-name-revealed');
                                  replyNameElement.setAttribute('data-original-userid', replyUserId);
                              }
                         }
                     } catch (e) {
                           console.error(`处理主帖 #${i + 1} 的第 ${j} 条回复时出错:`, e, replyElement);
                     }
                 }
             }
         }
         console.log(`名称替换完成,本轮共替换了 ${totalReplaced} 个"匿名"(包括主评论和回复)。`);
    }

    // --- 主执行函数和监听器 ---
    let processingScheduled = false; // 防抖标记

    async function runReplacement() {
        const currentPage = getCurrentPageNumber();
        // 如果页面和上次处理的相同,则不执行,除非用户映射尚未加载
        if (currentPage === lastProcessedPage && userIdToNameMap) {
            // console.log(`页面 ${currentPage} 已处理过,跳过。`);
            return;
        }

        if (processingScheduled) { console.log("任务已在计划中,跳过本次触发。"); return; }
        processingScheduled = true;
        console.log(`--- 开始执行第 ${currentPage} 页名称替换流程 ---`);

        const params = getParamsFromUrl();
        if (!params.ocId || !params.discussionId) {
            console.error("无法从 URL 获取 ocId 或 discussionId。");
            processingScheduled = false; return;
        }
        currentOcId = params.ocId; currentDiscussionId = params.discussionId;
        if (!authToken) authToken = getTokenFromCookie();
        if (!authToken) { console.error("无法获取 Token。"); processingScheduled = false; return; }

        try {
             // 确保用户映射已加载或正在加载
             const userMap = await fetchAllUsers(); // 获取或等待用户映射 Promise

             // 获取当前页的帖子数据
             const topicInfoPageData = await fetchTopicInfoPageData(currentOcId, currentDiscussionId, currentPage, DISCUSSION_PAGE_SIZE);

             // 处理可见评论
             processVisibleComments(topicInfoPageData, userMap);
             lastProcessedPage = currentPage; // 记录已成功处理的页码

        } catch (error) {
            console.error(`执行第 ${currentPage} 页替换时发生错误:`, error);
            // 出错时不清空 lastProcessedPage,允许下次重试
        } finally {
             console.log(`--- 第 ${currentPage} 页名称替换流程结束 ---`);
             setTimeout(() => { processingScheduled = false; }, 300); // 300ms 防抖
        }
    }

    // --- 启动与监听 ---

    console.log("优学院匿名替换脚本 (v0.5) 启动...");

    // 1. 获取初始 Token
    authToken = getTokenFromCookie();
    if (!authToken) {
        console.warn("脚本启动时未获取到 Token,请确保已登录。");
    }

    // 2. 监听 DOM 变化 (用于处理翻页或动态加载)
    function observeContentChanges() {
        const targetNode = document.querySelector(COMMENT_CONTAINER_SELECTOR);
        if (!targetNode) {
            console.error(`无法找到评论容器 ${COMMENT_CONTAINER_SELECTOR} 进行监听,1秒后重试...`);
            setTimeout(observeContentChanges, 1000);
            return;
        }

        console.log(`开始监听 ${COMMENT_CONTAINER_SELECTOR} 的 DOM 变化以触发更新...`);
        const observer = new MutationObserver((mutationsList) => {
            let relevantChange = false;
            for (const mutation of mutationsList) {
                // 主要检测子节点变化(评论列表更新)或分页控件激活状态变化
                if (mutation.type === 'childList' ||
                    (mutation.type === 'attributes' && mutation.attributeName === 'class' && mutation.target.matches && mutation.target.matches(PAGINATION_CURRENT_PAGE_SELECTOR)))
                {
                     // 如果检测到分页控件的 active class 变化,强制重置上次处理页码
                     if (mutation.type === 'attributes' && mutation.target.matches(PAGINATION_CURRENT_PAGE_SELECTOR)) {
                         const newPageNum = getCurrentPageNumber();
                         if (newPageNum !== lastProcessedPage) {
                             console.log(`检测到分页控件活动页码变化为: ${newPageNum}`);
                             lastProcessedPage = -1; // 强制重新处理
                             relevantChange = true;
                             break;
                         }
                     } else if (mutation.type === 'childList') {
                          // 更宽松的检测:只要有子节点变化就认为可能需要更新
                          relevantChange = true;
                          break;
                     }
                }
            }

            if (relevantChange) {
                 console.log("检测到相关 DOM 变化,计划运行名称替换...");
                 runReplacement(); // 调用主替换函数(内部有防抖)
            }
        });

        observer.observe(targetNode, {
            childList: true, subtree: true, attributes: true, attributeFilter: ['class']
        });
    }

    // 3. 延迟启动首次运行和监听器,给页面足够时间加载初始内容
    setTimeout(() => {
        runReplacement(); // 首次运行
        observeContentChanges(); // 启动监听器
    }, 1500); // 延迟 1.5 秒启动

})();