📄国开自动刷课(全自动刷完所有课程,但不考试)

国开(国家开放大学)自动刷课(不答题考试) 支持自动访问线上链接、查看资料附件、观看视频、自动查看页面。

// ==UserScript==
        // @name             📄国开自动刷课(全自动刷完所有课程,但不考试)
        // @namespace        有事联系V:caicats
        // @version          1.0.0
        // @description      国开(国家开放大学)自动刷课(不答题考试) 支持自动访问线上链接、查看资料附件、观看视频、自动查看页面。
        // @author           shanran
        // @match          *://lms.ouchn.cn/course/*
        // @match          *://lms.ouchn.cn/user/courses*
        // @original-author  shanran & caicats
        // @original-license GPL-3.0
        // @license          GPL-3.0
        // ==/UserScript==


        // 设置视频播放速度 建议最大4-8倍速 不然可能会卡 没有最大值
        // 并且直接挂载到window上
        window.playbackRate = 8;

        // 设置各种不同类型的课程任务之间的时间延迟,以便脚本在进行自动化学习时可以更好地模拟人类操作。
        const interval = {
            loadCourse: 6000, // 加载课程列表的延迟时间
            viewPage: 6000, // 查看页面类型课程的延迟时间
            onlineVideo: 3000, // 播放在线视频课程的延迟时间
            webLink: 3000, // 点击线上链接类型课程的延迟时间
            forum: 3000, // 发帖子给论坛课程的延迟时间
            material: 3000, // 查看附件类型课程的延迟时间
            other: 3000 // 处理其他未知类型课程的延迟时间
        };

        (async function (window, document) {

            // 保存值到本地存储
            function GM_setValue(name, value) {
                localStorage.setItem(name, JSON.stringify(value));
            }

            //从本地存储获取值
            function GM_getValue(name, defaultValue) {
                const value = localStorage.getItem(name);
                if (value === null) {
                    return defaultValue;
                }
                try {
                    return JSON.parse(value);
                } catch (e) {
                    console.error(`Error parsing stored value for ${name}:`, e);
                    return defaultValue;
                }
            }

            // 运行
            main();

            // 使用正则表达式从当前 URL 中提取出课程 ID。
            async function getCourseId() {
                // 判断是否在课程页面
                if(/lms.ouchn.cn\/course\//.test(window.location.href)) {
                    const courseId = (await waitForElement("#courseId", interval.loadCourse))?.value;
                    return courseId;
                }
                return null;
            }

            // 创建返回到课程列表页面的函数。
            async function returnCoursePage(waitTime = 500) {
                const backElement = await waitForElement("a.full-screen-mode-back", waitTime);
                if (backElement) {
                    backElement?.click();
                } else {
                    throw new Error("异常 无法获取到返回课程列表页面的元素!");
                }
            }

            // 返回到一级页面(我的课程中心)
            async function returnToCourseCenter(waitTime = 500) {
                console.log("返回到课程中心页面");
                window.location.href = "https://lms.ouchn.cn/user/courses#/";
            }

            // 将中文类型名称转换为英文枚举值。
            function getTypeEum(type) {
                switch (type) {
                    case "页面":
                        return "page";
                    case "音视频教材":
                        return "online_video";
                    case "线上链接":
                        return "web_link";
                    case "讨论":
                        console.log("讨论页面...");
                        return "forum";
                    case "参考资料":
                        return "material";
                    default:
                        return null;
                }
            }

            /**
             * 等待指定元素出现
             * 返回一个Promise对象,对document.querySelector封装了一下
             * @param selector dom选择器,像document.querySelector一样
             * @param waitTime 等待时间 单位: ms
             */
            async function waitForElement(selector, waitTime = 1000, maxCount = 10) {
                let count = 0;
                return new Promise(resolve => {
                    let timeId = setInterval(() => {
                        const element = document.querySelector(selector);
                        if (element || count >= maxCount) {
                            clearInterval(timeId);
                            resolve(element || null);
                        }
                        count++;
                    }, waitTime);
                });
            }

            /**
             * 等待多个指定元素出现
             * 返回一个Promise对象,对document.querySelectorAll封装了一下
             * @param selector dom选择器,像document.querySelectorAll一样
             * @param waitTime 等待时间 单位: ms
             */
            async function waitForElements(selector, waitTime = 1000, maxCount = 10) {
                let count = 0;
                return new Promise(resolve => {
                    let timeId = setInterval(() => {
                        const element = document.querySelectorAll(selector);
                        if (element || count >= maxCount) {
                            clearInterval(timeId);
                            resolve(element || null);
                        }
                        count++;
                    }, waitTime);
                });
            }

            // 等待指定时间
            function wait(ms) {
                return new Promise(resolve => { setTimeout(resolve, ms); });
            }

            /**
             * 该函数用于添加学习行为时长
             */
            function addLearningBehavior(activity_id, activity_type) {
                const duration = Math.ceil(Math.random() * 300 + 40);
                const data = JSON.stringify({
                    activity_id,
                    activity_type,
                    browser: 'chrome',
                    course_id: globalData.course.id,
                    course_code: globalData.course.courseCode,
                    course_name: globalData.course.name,
                    org_id: globalData.course.orgId,
                    org_name: globalData.user.orgName,
                    org_code: globalData.user.orgCode,
                    dep_id: globalData.dept.id,
                    dep_name: globalData.dept.name,
                    dep_code: globalData.dept.code,
                    user_agent: window.navigator.userAgent,
                    user_id: globalData.user.id,
                    user_name: globalData.user.name,
                    user_no: globalData.user.userNo,
                    visit_duration: duration
                });
                const url = 'https://lms.ouchn.cn/statistics/api/user-visits';
                return new Promise((resolve, reject) => {
                    $.ajax({
                        url,
                        data,
                        type: "POST",
                        cache: false,
                        contentType: "text/plain;charset=UTF-8",
                        complete: resolve
                    });
                });
            }

            // 打开并播放在线视频课程。
            async function openOnlineVideo() {
                // 等待 video 或 audio 元素加载完成
                const videoElem = await waitForElement('video');
                let audioElem = null;

                if (!videoElem) {
                    audioElem = await waitForElement('audio');
                }

                if (videoElem) {
                    // 处理视频元素
                    console.log("正在播放视频中...");

                    // 设置播放速率
                    videoElem.playbackRate = playbackRate;

                    // 监听播放速率变化事件并重新设置播放速率
                    videoElem.addEventListener('ratechange', function () {
                        videoElem.playbackRate = playbackRate;
                    });

                    // 监听视频播放结束事件
                    videoElem.addEventListener('ended', returnCoursePage);

                    // 延迟一会儿以等待视频加载
                    await wait(interval.onlineVideo);

                    // // 每隔一段时间检查是否暂停,并模拟点击继续播放并设置声音音量为0
                    setInterval(() => {
                        videoElem.volume = 0;
                        if (document.querySelector("i.mvp-fonts.mvp-fonts-play")) {
                            document.querySelector("i.mvp-fonts.mvp-fonts-play").click();
                        }
                    }, interval.onlineVideo);

                } else if (audioElem) {
                    // 处理音频元素
                    console.log("正在播放音频中...");

                    // 监听音频播放结束事件
                    audioElem.addEventListener("ended", returnCoursePage);

                    // 延迟一会儿以等待音频加载
                    await wait(interval.onlineVideo);

                    // 每隔一段时间检查是否暂停,并模拟点击继续播放
                    setInterval(() => {
                        audioElem.volume = 0;
                        if (document.querySelector("i.font.font-audio-play")) {
                            document.querySelector("i.font.font-audio-play").click();
                        }
                    }, interval.onlineVideo);
                }
            }

            // 打开并查看页面类型课程。
            function openViewPage() {
                // 当页面被加载完毕后延迟一会直接返回课程首页
                setTimeout(returnCoursePage, interval.viewPage);
            }

            // 打开并点击线上链接类型课程。
            async function openWebLink() {
                // 等待获取open-link-button元素
                const ElementOpenLinkButton = await waitForElement(".open-link-button", interval.webLink);

                // 设置元素属性让它不会弹出新标签并设置href为空并模拟点击
                ElementOpenLinkButton.target = "_self";
                ElementOpenLinkButton.href = "javascript:void(0);";
                ElementOpenLinkButton.click();

                // 等待一段时间后执行returnCoursePage函数
                setTimeout(returnCoursePage, interval.webLink);
            }
            function openApiMaterial() { // 用API去完成查看附件
                const id = document.URL.match(/.*\/\/lms.ouchn.cn\/course\/[0-9]+\/learning-activity\/full-screen.+\/([0-9]+)/)[1];
                const res = new Promise((resolve, reject) => {
                    $.ajax({
                        url: `https://lms.ouchn.cn/api/activities/${id}`,
                        type: "GET",
                        success: resolve,
                        error: reject
                    })
                });
                res.then(async ({ uploads: uploadsModels }) => {
                    uploadsModels.forEach(async ({ id: uploadId }) => {
                        await wait(interval.material);
                        await new Promise(resolve => $.ajax({
                            url: `https://lms.ouchn.cn/api/course/activities-read/${id}`,
                            type: "POST",
                            data: JSON.stringify({ upload_id: uploadId }),
                            contentType: "application/json",
                            dataType: "JSON",
                            success: resolve,
                            error: resolve
                        }));
                    });

                    await wait(interval.material);
                    returnCoursePage();
                });
                res.catch((xhr, status, error) => {
                    console.log(`这里出现了一个异常 | status: ${status}`);
                    console.dir(error, xhr, status);
                });

            }

            // 打开课程任务并查找已有帖子进行回复
            async function openForum() {
                // 先等待页面完全加载
                console.log('进入讨论页面(三级页面),等待页面加载完成...');
                await wait(interval.forum * 3);  // 增加等待时间,确保JS渲染完成

                // 设置唯一标识符,用于页面间通信
                const replyId = 'forum_reply_' + Date.now();

                // 清除所有之前的回帖标识
                clearPreviousReplyIds();

                // 将当前回帖标识加上"active"前缀,用于四级页面检索
                localStorage.setItem('active_reply_id', replyId);
                localStorage.setItem(replyId, 'waiting'); // 设置初始状态为等待中
                console.log(`设置回帖标识: ${replyId}, 状态: waiting, 并设为活动标识`);

                // 查找第一篇帖子的可见DOM元素
                console.log('查找第一篇帖子的可见DOM元素...');

                // 尝试查找可见的帖子元素(标题、内容等)
                const visibleSelectors = [
                    // 常见的帖子标题和内容选择器
                    '.title',
                    '.topic-title',
                    '.post-title',
                    '.thread-title',
                    '.discussion-title',
                    // 帖子内容区域
                    '.content',
                    '.post-content',
                    '.topic-content',
                    '.thread-content',
                    // 帖子项容器
                    '.item',
                    '.post-item',
                    '.topic-item',
                    '.thread-item',
                    '.discussion-item',
                    // 列表项
                    'li.item',
                    '.list-item',
                    // 通用选择器
                    '[role="article"]',
                    '[role="listitem"]',
                    // 包含特定文本的元素
                    'div:not(:empty)',
                    'p:not(:empty)',
                    'span:not(:empty)'
                ];

                let firstPostElement = null;
                let elementFound = false;

                // 首先尝试查找可点击的元素
                for (const selector of visibleSelectors) {
                    console.log(`尝试查找可点击的帖子元素: ${selector}`);
                    const elements = document.querySelectorAll(selector);

                    for (const element of elements) {
                        // 检查元素是否可见
                        if (element.offsetParent !== null &&
                            element.style.display !== 'none' &&
                            element.style.visibility !== 'hidden') {

                            // 检查元素或其父元素是否可点击
                            const clickableElement = element.closest('a') ||
                                                   element.closest('button') ||
                                                   element.closest('[role="button"]') ||
                                                   element.closest('[onclick]') ||
                                                   element.closest('[class*="clickable"]') ||
                                                   element.closest('[class*="selectable"]');

                            if (clickableElement) {
                                console.log('找到可点击的帖子元素:', clickableElement);
                                firstPostElement = clickableElement;
                                elementFound = true;
                                break;
                            }

                            // 如果元素本身包含文本内容,可能是帖子标题或内容
                            const text = element.textContent.trim();
                            if (text.length > 10 && !text.includes('回复') && !text.includes('发表')) {
                                console.log('找到可能的帖子内容元素:', element);
                                firstPostElement = element;
                                elementFound = true;
                                break;
                            }
                        }
                    }

                    if (elementFound) break;
                }

                // 如果上面的方法都找不到,尝试直接找帖子链接
                if (!firstPostElement) {
                    console.log("尝试直接查找帖子链接...");
                    const linkSelectors = [
                        'a[href*="topic"]',
                        'a[href*="discussion"]',
                        'a[href*="thread"]',
                        'a[href*="forum"]',
                        'a[href*="post"]',
                        '.topic-list a',
                        '.discussion-list a',
                        '.thread-list a',
                        'a.topic-title'
                    ];

                    for (const selector of linkSelectors) {
                        console.log(`尝试链接选择器: ${selector}`);
                        const links = document.querySelectorAll(selector);
                        if (links && links.length > 0) {
                            firstPostElement = links[0];
                            console.log(`找到帖子链接: ${firstPostElement.href || '无href属性'}`);
                            break;
                        }
                    }
                }

                if (!firstPostElement) {
                    console.error("无法找到任何可见的帖子元素,尝试查找列表容器...");

                    // 尝试查找列表容器
                    const listSelectors = [
                        '.list',
                        '.topic-list',
                        '.post-list',
                        '.thread-list',
                        '.discussion-list',
                        '[role="list"]',
                        'ul',
                        'ol'
                    ];

                    let listContainer = null;
                    for (const selector of listSelectors) {
                        listContainer = document.querySelector(selector);
                        if (listContainer) {
                            console.log(`找到列表容器: ${selector}`);
                            // 查找第一个非空的子元素
                            const children = Array.from(listContainer.children);
                            for (const child of children) {
                                if (child.textContent.trim().length > 0) {
                                    firstPostElement = child;
                                    console.log('找到第一个非空列表项');
                                    break;
                                }
                            }
                            break;
                        }
                    }
                }

                // 最后的尝试 - 查找所有链接
                if (!firstPostElement) {
                    console.log("最后尝试:查找所有可见链接...");
                    const allLinks = document.querySelectorAll('a');
                    for (const link of allLinks) {
                        // 跳过导航链接和空链接
                        if (link.href &&
                            !link.href.includes('javascript:') &&
                            !link.href.includes('#') &&
                            link.offsetParent !== null &&
                            !link.textContent.includes('登录(不可用)') &&
                            !link.textContent.includes('注册(不可用)') &&
                            !link.textContent.includes('忘记密码')) {

                            console.log(`找到一个可能的链接: ${link.textContent} - ${link.href}`);
                            firstPostElement = link;
                            break;
                        }
                    }
                }

                if (!firstPostElement) {
                    console.error("无法找到任何帖子元素,准备返回课程页面");
                    // 更新回帖状态为错误
                    localStorage.setItem(replyId, 'error');
                    setTimeout(returnCoursePage, interval.forum);
                    return;
                }

                // 尝试点击找到的元素
                console.log('尝试点击帖子元素');
                try {
                    // 如果元素本身不可点击,尝试模拟点击事件
                    if (!firstPostElement.click) {
                        const clickEvent = new MouseEvent('click', {
                            view: window,
                            bubbles: true,
                            cancelable: true
                        });
                        firstPostElement.dispatchEvent(clickEvent);
                    } else {
                        firstPostElement.click();
                    }

                    console.log('已触发点击事件');

                    // 等待一段时间,确保新窗口打开
                    await wait(interval.forum);

                    // 记录当前页面的回帖标识
                    window.forumReplyId = replyId;
                    console.log(`已保存回帖标识: ${replyId}, 开始等待回帖完成`);

                    // 开始轮询检查回帖状态,并设置超时
                    checkReplyStatus(replyId);
                    setReplyTimeout(replyId, 60); // 设置60秒超时

                } catch (e) {
                    console.error('点击帖子元素失败:', e);
                    localStorage.setItem(replyId, 'error'); // 标记为错误
                    setTimeout(returnCoursePage, interval.forum);
                }
            }

            // 清除之前的回帖标识
            function clearPreviousReplyIds() {
                try {
                    // 查找并删除可能的过期标识
                    for (let i = 0; i < localStorage.length; i++) {
                        const key = localStorage.key(i);
                        if (key && key.startsWith('forum_reply_') && key !== 'active_reply_id') {
                            console.log(`清除旧回帖标识: ${key}`);
                            localStorage.removeItem(key);
                        }
                    }
                    // 确保没有活动状态标识
                    localStorage.removeItem('active_reply_id');
                } catch (e) {
                    console.error('清除过期标识失败:', e);
                }
            }

            // 设置回帖超时
            function setReplyTimeout(replyId, seconds) {
                console.log(`设置回帖超时: ${replyId}, ${seconds}秒`);
                setTimeout(() => {
                    const status = localStorage.getItem(replyId);
                    if (status === 'waiting') {
                        console.log(`回帖超时: ${replyId}, 自动标记为完成`);
                        localStorage.setItem(replyId, 'completed');
                        // 触发storage事件
                        localStorage.setItem(`${replyId}_timestamp`, Date.now().toString());
                    }
                }, seconds * 1000);
            }

            // 检查回帖状态的函数
            function checkReplyStatus(replyId) {
                console.log(`检查回帖状态: ${replyId}`);
                const status = localStorage.getItem(replyId);

                if (status === 'completed' || status === 'error') {
                    console.log(`回帖${status === 'completed' ? '已完成' : '失败'},标识: ${replyId}, 准备返回课程页面`);
                    try {
                        localStorage.removeItem(replyId); // 清理
                        localStorage.removeItem('active_reply_id'); // 清理活动标识
                    } catch (e) {
                        console.error('清理localStorage失败:', e);
                    }
                    setTimeout(returnCoursePage, interval.forum);
                } else {
                    // 继续等待,每2秒检查一次
                    console.log(`回帖仍在进行中,标识: ${replyId}, 继续等待...`);
                    setTimeout(() => checkReplyStatus(replyId), 2000);
                }
            }

            // 处理四级页面的回帖操作
            async function replyForum() {
                console.log('进入四级页面(回帖页面),等待页面加载完成...');
                await wait(interval.forum * 3); // 延长等待时间确保页面完全加载

                // 优先从active_reply_id获取标识
                let replyId = localStorage.getItem('active_reply_id');

                if (replyId) {
                    console.log(`从活动标识获取回帖ID: ${replyId}`);
                }

                // 如果没有活动标识,使用之前的方法尝试查找
                if (!replyId) {
                    // 尝试从URL参数中获取
                    try {
                        const params = new URLSearchParams(window.location.search);
                        replyId = params.get('replyId');
                    } catch (e) {
                        console.log('URL参数中没有找到replyId');
                    }

                    // 尝试从localStorage中查找等待中的回帖标识
                    if (!replyId) {
                        console.log('尝试从localStorage查找等待中的回帖标识');
                        for (let i = 0; i < localStorage.length; i++) {
                            const key = localStorage.key(i);
                            if (key && key.startsWith('forum_reply_') && localStorage.getItem(key) === 'waiting') {
                                replyId = key;
                                console.log(`找到等待中的回帖标识: ${replyId}`);
                                break;
                            }
                        }
                    }
                }

                if (!replyId) {
                    console.log('没有找到回帖标识,创建新标识');
                    replyId = 'forum_reply_' + Date.now();
                    localStorage.setItem(replyId, 'waiting');
                }

                console.log(`当前回帖标识: ${replyId}`);

                // 首先查找并点击输入框激活编辑器
                console.log('查找输入框以激活编辑器...');
                const inputSelectors = [
                    'input[placeholder*="讨论"]',
                    'input[placeholder*="回复"]',
                    'input.ivu-input',
                    '.reply-input',
                    '.comment-input',
                    'textarea[placeholder*="回复"]',
                    'textarea[placeholder*="讨论"]'
                ];

                let inputElem = null;
                for (const selector of inputSelectors) {
                    console.log(`尝试查找输入框: ${selector}`);
                    inputElem = await waitForElement(selector, interval.forum/3, 3);
                    if (inputElem) {
                        console.log(`找到输入框,使用选择器: ${selector}`);
                        break;
                    }
                }

                if (inputElem) {
                    console.log('点击输入框激活编辑器');
                    try {
                        // 尝试不同的方法来激活输入框
                        inputElem.focus();
                        inputElem.click();

                        // 触发各种可能的事件
                        const events = ['focus', 'click', 'mousedown', 'mouseup', 'change'];
                        events.forEach(eventType => {
                            const event = new Event(eventType, { bubbles: true });
                            inputElem.dispatchEvent(event);
                        });

                        // 等待编辑器激活
                        console.log('等待编辑器激活...');
                        await wait(interval.forum);
                    } catch (e) {
                        console.error('激活输入框失败:', e);
                    }
                } else {
                    console.log('未找到输入框,尝试直接查找编辑区域');
                }

                // 查找编辑区域
                console.log('查找可编辑区域...');
                const editorSelectors = [
                    '.simditor-body[contenteditable="true"]',
                    '[contenteditable="true"]',
                    '.simditor-body.needsclick[contenteditable="true"]',
                    '.reply-editor [contenteditable]',
                    '.comment-editor [contenteditable]',
                    '.post-editor [contenteditable]'
                ];

                let editorElem = null;
                for (const selector of editorSelectors) {
                    console.log(`尝试查找编辑区域: ${selector}`);
                    editorElem = await waitForElement(selector, interval.forum/3, 3);
                    if (editorElem) {
                        console.log(`找到编辑区域,使用选择器: ${selector}`);
                        break;
                    }
                }

                if (!editorElem) {
                    console.error("无法找到编辑区域,尝试查找回复按钮...");

                    // 尝试查找"回复"按钮,可能需要先点击
                    const replyBtnSelectors = [
                        'button:contains("回复")',
                        'a:contains("回复")',
                        '.reply-btn',
                        '.comment-btn',
                        'button.reply',
                        'a.reply-link'
                    ];

                    let replyBtn = null;
                    for (const selector of replyBtnSelectors) {
                        // 处理jQuery特有的:contains选择器
                        if (selector.includes(':contains')) {
                            const text = selector.match(/:contains\("(.+)"\)/)[1];
                            const buttons = Array.from(document.querySelectorAll('button, a')).filter(el =>
                                el.textContent.includes(text)
                            );
                            if (buttons.length > 0) {
                                replyBtn = buttons[0];
                                console.log(`找到回复按钮,文本包含: ${text}`);
                                break;
                            }
                        } else {
                            replyBtn = document.querySelector(selector);
                            if (replyBtn) {
                                console.log(`找到回复按钮,使用选择器: ${selector}`);
                                break;
                            }
                        }
                    }

                    if (replyBtn) {
                        console.log('点击回复按钮');
                        replyBtn.click();

                        // 点击后等待回帖框出现
                        await wait(interval.forum);

                        // 再次尝试查找编辑区域
                        for (const selector of editorSelectors) {
                            editorElem = await waitForElement(selector, interval.forum/3, 3);
                            if (editorElem) {
                                console.log(`点击回复按钮后找到编辑区域,使用选择器: ${selector}`);
                                break;
                            }
                        }
                    }
                }

                if (!editorElem) {
                    console.error("无法找到编辑区域,准备关闭页面");
                    window.close();
                    return;
                }

                // 在找到编辑区域后,先点击它以确保激活
                console.log('点击编辑区域确保激活');
                try {
                    editorElem.focus();
                    editorElem.click();
                } catch (e) {
                    console.error('点击编辑区域失败:', e);
                }

                await wait(500);

                // 查找提交按钮
                const submitSelectors = [
                    // 优先使用带有"发表回帖"文本的按钮
                    'button.ivu-btn.ivu-btn-primary:contains("发表回帖")',
                    'button.w-88.ivu-btn.ivu-btn-primary',
                    'button.ivu-btn.ivu-btn-primary:not([type="submit"])',
                    '.ivu-btn.ivu-btn-primary span:contains("发表")',
                    '.ivu-btn.ivu-btn-primary span:contains("回帖")',
                    // 其他可能的选择器
                    'button[type="button"].ivu-btn.ivu-btn-primary',
                    'button.submit-reply',
                    'button.post-reply',
                    // 之前的选择器作为备选
                    'button:contains("提交")',
                    'button:contains("回复")',
                    'button.submit',
                    'button.reply-submit',
                    '.reply-footer button',
                    '.post-btn',
                    '.submit-btn',
                    'button.ivu-btn-primary:not(.ivu-btn-ghost)',
                    'button[type="submit"]'
                ];

                let submitBtn = null;
                for (const selector of submitSelectors) {
                    // 处理jQuery特有的:contains选择器
                    if (selector.includes(':contains')) {
                        const text = selector.match(/:contains\("(.+)"\)/)[1];
                        // 尝试匹配按钮本身或其子元素中的文本
                        let buttons = Array.from(document.querySelectorAll('button')).filter(el =>
                            el.textContent.includes(text) ||
                            Array.from(el.querySelectorAll('span')).some(span => span.textContent.includes(text))
                        );

                        if (buttons.length === 0 && selector.includes('.ivu-btn')) {
                            // 特殊处理ivu-btn类型按钮的span子元素
                            const spans = Array.from(document.querySelectorAll('.ivu-btn span')).filter(span =>
                                span.textContent.includes(text)
                            );
                            buttons = spans.map(span => span.closest('button')).filter(btn => btn !== null);
                        }

                        if (buttons.length > 0) {
                            submitBtn = buttons[0];
                            console.log(`找到提交按钮,文本包含: ${text}`);
                            break;
                        }
                    } else {
                        submitBtn = document.querySelector(selector);
                        if (submitBtn) {
                            console.log(`找到提交按钮,使用选择器: ${selector}`);
                            break;
                        }
                    }
                }

                // 如果上面的选择器都没找到,尝试查找所有含有"发表"或"回帖"文本的按钮
                if (!submitBtn) {
                    console.log("尝试查找所有含有发表或回帖文本的按钮");
                    const allButtons = document.querySelectorAll('button');

                    for (const btn of allButtons) {
                        const text = btn.textContent.trim().toLowerCase();
                        if (text.includes('发表') || text.includes('回帖') || text.includes('提交') || text.includes('回复')) {
                            submitBtn = btn;
                            console.log(`找到提交按钮,含有文本: ${text}`);
                            break;
                        }
                    }
                }

                // 如果还找不到,尝试寻找特定类名的按钮
                if (!submitBtn) {
                    console.log("尝试通过样式和位置定位提交按钮");
                    // 查找页面上的主要按钮(通常是底部的大按钮)
                    const primaryButtons = document.querySelectorAll('.ivu-btn-primary');
                    if (primaryButtons.length > 0) {
                        // 尝试找到最后一个(通常是提交按钮)
                        submitBtn = primaryButtons[primaryButtons.length - 1];
                        console.log("根据位置找到可能的提交按钮");
                    }
                }

                if (!submitBtn) {
                    console.error("无法找到提交按钮,准备关闭页面");

                    // 输出所有按钮用于调试
                    console.log("页面上所有按钮:");
                    const allButtons = document.querySelectorAll('button');
                    for (let i = 0; i < allButtons.length; i++) {
                        console.log(`按钮${i+1}: class="${allButtons[i].className}", text="${allButtons[i].textContent.trim()}", type="${allButtons[i].type}"`);
                    }

                    window.close();
                    return;
                }

                // 记录找到的按钮信息
                console.log("找到的提交按钮详细信息:");
                console.log(`- 类名: ${submitBtn.className}`);
                console.log(`- 文本: ${submitBtn.textContent.trim()}`);
                console.log(`- 类型: ${submitBtn.type}`);
                console.log(`- HTML: ${submitBtn.outerHTML}`);

                // 填写回帖内容
                console.log('填写回帖内容');
                const timestamp = Date.now();
                try {
                    // 尝试多种方式设置内容
                    const content = `学习了,感谢分享!${timestamp}`;

                    // 1. 直接设置innerHTML
                    editorElem.innerHTML = `<p>${content}</p>`;
                    console.log('方法1: 设置innerHTML');

                    // 2. 使用execCommand
                    document.execCommand('selectAll', false, null);
                    document.execCommand('insertText', false, content);
                    console.log('方法2: 使用execCommand');

                    // 3. 创建文本节点并插入
                    if (editorElem.innerHTML === "<p><br></p>" || editorElem.innerHTML === "") {
                        const p = document.createElement('p');
                        p.textContent = content;
                        editorElem.innerHTML = '';
                        editorElem.appendChild(p);
                        console.log('方法3: 创建并插入文本节点');
                    }

                    // 4. 尝试查找相关的textarea并更新其值
                    try {
                        const textareas = document.querySelectorAll('textarea');
                        if (textareas.length > 0) {
                            for (const textarea of textareas) {
                                textarea.value = content;
                                console.log('更新相关textarea');
                                // 触发change事件
                                const event = new Event('input', { bubbles: true });
                                textarea.dispatchEvent(event);

                                const changeEvent = new Event('change', { bubbles: true });
                                textarea.dispatchEvent(changeEvent);
                            }
                        }
                    } catch (e) {
                        console.error('更新textarea失败:', e);
                    }

                    console.log('回帖内容设置完成');
                } catch (e) {
                    console.error('设置回帖内容失败:', e);
                }

                // 等待一会再提交
                await wait(interval.forum);

                // 检查是否确实填入了内容
                console.log('检查编辑区域内容:', editorElem.innerHTML);

                // 点击提交
                console.log('点击提交按钮');
                try {
                    submitBtn.click();
                    console.log('提交按钮点击完成');
                } catch (e) {
                    console.error('点击提交按钮失败:', e);
                }

                // 等待提交完成
                console.log('等待回帖提交完成...');
                await wait(interval.forum * 2);

                // 检查是否提交成功,或者有错误信息
                const errorMessages = document.querySelectorAll('.error-message, .alert-error, .ivu-message-error');
                let finalStatus = 'completed'; // 默认设置为完成

                if (errorMessages.length > 0) {
                    console.error('提交过程中出现错误:', errorMessages[0].textContent);

                    // 如果有权限错误,标记为error
                    if (errorMessages[0].textContent.includes('权限')) {
                        console.log('权限错误,可能需要其他方式回帖');
                        finalStatus = 'error';
                    }
                }

                // 更新状态并确保写入成功
                try {
                    localStorage.setItem(replyId, finalStatus);
                    // 验证写入
                    const verifyStatus = localStorage.getItem(replyId);
                    if (verifyStatus !== finalStatus) {
                        console.error(`状态写入验证失败,期望: ${finalStatus}, 实际: ${verifyStatus}`);
                        // 重试一次
                        localStorage.setItem(replyId, finalStatus);
                    }
                } catch (e) {
                    console.error('更新localStorage失败:', e);
                }

                console.log(`回帖${finalStatus === 'completed' ? '成功' : '失败'},更新标识: ${replyId} -> ${finalStatus}`);

                // 确保状态更新后再关闭窗口
                setTimeout(() => {
                    try {
                        // 再次确认状态已正确设置
                        const finalCheck = localStorage.getItem(replyId);
                        if (finalCheck !== finalStatus) {
                            console.log(`关闭前发现状态不匹配,重新设置为: ${finalStatus}`);
                            localStorage.setItem(replyId, finalStatus);
                        }
                        console.log(`关闭窗口前的最终状态: ${localStorage.getItem(replyId)}`);
                    } catch (e) {
                        console.error('最终状态检查失败:', e);
                    }

                    // 使用storage事件确保跨窗口通信
                    try {
                        // 触发一个特殊的storage事件来确保状态更新被检测到
                        localStorage.setItem(`${replyId}_timestamp`, Date.now().toString());
                        localStorage.setItem(replyId, finalStatus);
                    } catch (e) {
                        console.error('触发storage事件失败:', e);
                    }

                    window.close();
                }, interval.forum);
            }

            // 添加storage事件监听器,用于跨窗口通信
            window.addEventListener('storage', function(e) {
                // 检查是否是回帖状态更新
                if (e.key && e.key.startsWith('forum_reply_')) {
                    console.log(`检测到回帖状态更新: ${e.key} -> ${e.newValue}`);
                    // 如果当前页面正在等待这个回帖完成,主动触发状态检查
                    if (window.forumReplyId === e.key) {
                        checkReplyStatus(e.key);
                    }
                }
            });

            // 课程首页处理
            async function courseIndex() {
                const courseId = await getCourseId();
                if (!courseId) {
                    console.error("无法获取课程ID");
                    return;
                }

                await new Promise(resolve => {
                    console.log("正在展开所有课程任务");
                    let timeId = setInterval(() => {
                        const allCollapsedElement = document.querySelector("i.icon.font.font-toggle-all-collapsed");
                        const allExpandedElement = document.querySelector("i.icon.font.font-toggle-all-expanded");
                        if (!allExpandedElement) {
                            if (allCollapsedElement) {
                                allCollapsedElement.click();
                            }
                        }
                        if (!allCollapsedElement && !allExpandedElement) { throw new Error("无法展开所有课程 可能是元素已更改,请联系作者更新。"); } {
                            console.log("课程展开完成。");
                            clearInterval(timeId);
                            resolve();
                        }
                    }, interval.loadCourse);
                });


                console.log("正在获取加载的课程任务");
                const courseElements = await waitForElements('.learning-activity .clickable-area', interval.loadCourse);

                const courseElement = Array.from(courseElements).find(elem => {
                    const type = $(elem.querySelector('i.font[original-title]')).attr('original-title'); // 获取该课程任务的类型
                    // const status = $(elem.querySelector('span.item-status')).text(); // 获取该课程任务是否进行中
                    // 👆上行代码由于无法获取到课程任务是否已关闭,目前暂时注释掉


                    const typeEum = getTypeEum(type);

                    if (!typeEum) {
                        return false;
                    }

                    const completes = elem.querySelector('.ivu-tooltip-inner b').textContent === "已完成" ? true : false;

                    // const result = status === "进行中" && typeEum != null && completes === false;
                    const result = typeEum != null && completes === false;
                    if (result) {
                        GM_setValue(`typeEum-${courseId}`, typeEum);
                    }
                    return result;
                });

                if (courseElement) {
                    console.log("发现未完成的课程任务");
                    $(courseElement).click();
                } else {
                    console.log("课程任务可能全部完成了,返回课程中心");
                    // 所有课程已完成,记录该课程ID为已完成
                    const completedCourses = GM_getValue('completedCourses', []);
                    if (!completedCourses.includes(courseId)) {
                        completedCourses.push(courseId);
                        GM_setValue('completedCourses', completedCourses);
                        console.log(`已将课程 ${courseId} 标记为已完成,不会再次学习该课程`);
                    }
                    // 返回课程中心
                    returnToCourseCenter();
                }
            }

            // 处理一级页面(课程中心)
            async function courseCenterIndex() {
                console.log("正在课程中心页面,检索未完成的课程...");

                // 获取已标记为完成的课程列表
                const completedCourses = GM_getValue('completedCourses', []);
                if (completedCourses.length > 0) {
                    console.log(`已有 ${completedCourses.length} 个课程被标记为已完成: ${completedCourses.join(', ')}`);
                }

                // 等待页面完全加载,延长等待时间
                await wait(interval.loadCourse * 3);

                // 首先尝试获取DOM结构,用于调试
                console.log("页面结构分析中...");
                const mainContainer = document.querySelector('#app') || document.querySelector('.container-main');

                if (mainContainer) {
                    console.log("找到主容器");

                    // 各种可能的课程卡片选择器(从具体到通用)
                    const selectors = [
                        '.my-course-list .course-item',
                        '.course-list .course-item',
                        '.course-list-wrapper .course-item',
                        '.el-card.course-item',
                        '.course-panel',
                        '.my-course-panel',
                        '[class*="course-item"]',
                        '.el-card',
                        '.card'
                    ];

                    let courseCards = null;

                    // 尝试所有可能的选择器
                    for (const selector of selectors) {
                        console.log(`尝试使用选择器: ${selector}`);
                        courseCards = await waitForElements(selector, interval.loadCourse, 5);

                        if (courseCards && courseCards.length > 0) {
                            console.log(`使用选择器 ${selector} 找到 ${courseCards.length} 个课程卡片`);
                            break;
                        }
                    }

                    // 如果还是找不到课程卡片,记录更详细的DOM结构
                    if (!courseCards || courseCards.length === 0) {
                        console.log("无法找到课程卡片,开始分析DOM结构...");

                        // 记录主要容器的内容结构
                        console.log("主容器内容结构:", mainContainer.innerHTML.substring(0, 500) + "...");

                        // 查找所有可能的容器元素
                        const possibleContainers = mainContainer.querySelectorAll('.container, .wrapper, .list, .panel, .content, .card-container');
                        console.log(`找到 ${possibleContainers.length} 个可能的容器元素`);

                        for (let i = 0; i < possibleContainers.length; i++) {
                            console.log(`容器 ${i+1} 结构: `, possibleContainers[i].outerHTML.substring(0, 300) + "...");
                        }

                        // 查找所有链接,看是否有课程链接
                        const allLinks = mainContainer.querySelectorAll('a[href*="/course/"]');
                        console.log(`找到 ${allLinks.length} 个课程链接`);

                        if (allLinks.length > 0) {
                            // 直接使用找到的课程链接
                            console.log("基于课程链接遍历");

                            for (let i = 0; i < allLinks.length; i++) {
                                const link = allLinks[i];
                                // 提取链接中的课程ID
                                const courseIdMatch = link.href.match(/\/course\/(\d+)/);
                                if (!courseIdMatch) continue;

                                const courseId = courseIdMatch[1];

                                // 检查课程是否已标记为完成
                                if (completedCourses.includes(courseId)) {
                                    console.log(`跳过已标记为完成的课程: ${courseId}`);
                                    continue;
                                }

                                const card = link.closest('.card') || link.closest('.panel') || link.closest('.item') || link.parentElement;

                                // 查找课程完成度信息
                                const progressText = getProgressText(card);

                                if (progressText) {
                                    console.log(`课程${i+1}(ID: ${courseId})进度文本: ${progressText}`);
                                    const progressMatch = progressText.match(/(\d+)%/) || progressText.match(/(\d+)/);

                                    if (progressMatch && progressMatch[1]) {
                                        const progressPercent = parseInt(progressMatch[1]);
                                        console.log(`课程${i+1}(ID: ${courseId})完成度: ${progressPercent}%`);

                                        // 如果完成度低于90%,点击进入该课程
                                        if (progressPercent < 90) {
                                            console.log(`找到完成度低于90%的课程: ${progressPercent}%,准备进入该课程`);
                                            link.click();
                                            return; // 结束函数,进入二级页面
                                        }
                                    }
                                } else {
                                    console.log(`课程${i+1}(ID: ${courseId})无法找到进度信息`);
                                }
                            }
                        }

                        console.log("没有找到完成度低于90%的未完成课程,所有课程可能已完成");
                        return;
                    }

                    console.log(`找到 ${courseCards.length} 个课程卡片,开始遍历`);

                    // 遍历所有课程卡片,寻找完成度低于90%的课程
                    for (let i = 0; i < courseCards.length; i++) {
                        const card = courseCards[i];

                        // 找到课程卡片中的链接元素
                        const courseLink = card.querySelector('a.course-link') ||
                                          card.querySelector('a[href*="/course/"]') ||
                                          card.querySelector('a');

                        if (!courseLink || !courseLink.href) {
                            console.log(`课程${i+1}找不到有效链接,跳过`);
                            continue;
                        }

                        // 提取链接中的课程ID
                        const courseIdMatch = courseLink.href.match(/\/course\/(\d+)/);
                        if (!courseIdMatch) {
                            console.log(`课程${i+1}无法提取课程ID,跳过`);
                            continue;
                        }

                        const courseId = courseIdMatch[1];

                        // 检查课程是否已标记为完成
                        if (completedCourses.includes(courseId)) {
                            console.log(`跳过已标记为完成的课程: ${courseId}`);
                            continue;
                        }

                        // 获取进度文本
                        const progressText = getProgressText(card);

                        if (!progressText) {
                            console.log(`课程${i+1}(ID: ${courseId})无法找到进度信息,记录整个卡片内容:`);
                            console.log(card.innerHTML);
                            continue;
                        }

                        console.log(`课程${i+1}(ID: ${courseId})进度文本: ${progressText}`);

                        const progressMatch = progressText.match(/(\d+)%/) || progressText.match(/(\d+)/);

                        if (progressMatch && progressMatch[1]) {
                            const progressPercent = parseInt(progressMatch[1]);
                            console.log(`课程${i+1}(ID: ${courseId})完成度: ${progressPercent}%`);

                            // 如果完成度低于90%,点击进入该课程
                            if (progressPercent < 90) {
                                console.log(`找到完成度低于90%的课程: ${progressPercent}%,准备进入该课程`);

                                if (courseLink) {
                                    console.log("点击进入课程: " + courseLink.href);
                                    courseLink.click();
                                    return; // 结束函数,进入二级页面
                                } else {
                                    console.log("找不到课程链接元素,打印卡片内容:");
                                    console.log(card.innerHTML);
                                }
                            }
                        } else {
                            console.log(`无法解析课程${i+1}(ID: ${courseId})的完成度百分比,文本内容: ${progressText}`);
                        }
                    }
                } else {
                    console.log("未找到主容器,尝试直接搜索课程链接");

                    // 尝试直接查找课程链接
                    const courseLinks = document.querySelectorAll('a[href*="/course/"]');

                    if (courseLinks && courseLinks.length > 0) {
                        console.log(`找到 ${courseLinks.length} 个课程链接,尝试查找进度信息`);

                        for (let i = 0; i < courseLinks.length; i++) {
                            const link = courseLinks[i];

                            // 提取链接中的课程ID
                            const courseIdMatch = link.href.match(/\/course\/(\d+)/);
                            if (!courseIdMatch) continue;

                            const courseId = courseIdMatch[1];

                            // 检查课程是否已标记为完成
                            if (completedCourses.includes(courseId)) {
                                console.log(`跳过已标记为完成的课程: ${courseId}`);
                                continue;
                            }

                            const card = link.closest('.card') || link.closest('.panel') || link.closest('.item') || link.parentElement;

                            if (card) {
                                const progressText = getProgressText(card);

                                if (progressText) {
                                    console.log(`课程${i+1}(ID: ${courseId})进度文本: ${progressText}`);
                                    const progressMatch = progressText.match(/(\d+)%/) || progressText.match(/(\d+)/);

                                    if (progressMatch && progressMatch[1]) {
                                        const progressPercent = parseInt(progressMatch[1]);
                                        console.log(`课程${i+1}(ID: ${courseId})完成度: ${progressPercent}%`);

                                        if (progressPercent < 90) {
                                            console.log(`找到完成度低于90%的课程: ${progressPercent}%,准备进入`);
                                            link.click();
                                            return;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }

                console.log("没有找到完成度低于90%的未完成课程,所有课程可能已完成");
            }

            /**
             * 获取进度文本的辅助函数
             * @param {Element} card - 课程卡片元素
             * @returns {string|null} - 进度文本或null
             */
            function getProgressText(card) {
                // 尝试各种可能的选择器查找进度元素
                const progressSelectors = [
                    '.course-progress-text',
                    '.progress-text',
                    '[class*="progress"]',
                    '[class*="percent"]',
                    '.course-item-footer',
                    '.footer',
                    '.status',
                    '.complete'
                ];

                let progressElement = null;

                for (const selector of progressSelectors) {
                    progressElement = card.querySelector(selector);
                    if (progressElement) break;
                }

                if (!progressElement) {
                    // 尝试查找包含"%"的任意元素
                    const allElements = card.querySelectorAll('*');
                    for (const el of allElements) {
                        if (el.textContent && el.textContent.includes('%')) {
                            progressElement = el;
                            break;
                        }
                    }
                }

                return progressElement ? progressElement.textContent.trim() : null;
            }

            function main() {
                // 判断当前在哪个页面
                // 一级页面:课程中心
                if (/https:\/\/lms.ouchn.cn\/user\/courses/m.test(document.URL)) {
                    console.log("当前在一级页面(课程中心)");
                    courseCenterIndex();
                }
                // 二级页面:课程首页
                else if (/https:\/\/lms.ouchn.cn\/course\/\d+\/ng.*#\//m.test(document.URL)) {
                    console.log("当前在二级页面(课程首页)");
                    courseIndex();
                }
                // 四级页面:帖子回复页面 - 在三级页面之前检查,因为可能有相似的URL模式
                else if (/https:\/\/lms.ouchn.cn\/course\/\d+\/learning-activity\/\d+/m.test(document.URL) ||
                         document.referrer.includes('learning-activity/full-screen') ||
                         document.URL.includes('forum') ||
                         document.URL.includes('topic') ||
                         document.URL.includes('discussion')) {
                    console.log("检测到可能是四级页面(帖子回复页面)");
                    replyForum();
                    return;
                }
                // 三级页面:具体任务页面
                else if (/http[s]?:\/\/lms.ouchn.cn\/course\/\d+\/learning-activity\/full-screen[#]?\//.test(window.location.href)) {
                    console.log("当前在三级页面(具体任务页面)");
                    const courseId = window.location.href.match(/http[s]?:\/\/lms.ouchn.cn\/course\/(\d+)/)[1];
                    const activity_id = window.location.href.match(/http[s]?:\/\/lms.ouchn.cn\/course\/\d+\/learning-activity\/full-screen[#]?\/(\d+)/)[1];
                    const typeEum = GM_getValue(`typeEum-${courseId}`, null);
                    addLearningBehavior(activity_id, typeEum);
                    switch (typeEum) {
                        case "page":
                            console.log("正在查看页面。");
                            openViewPage();
                            return;
                        case "online_video":
                            openOnlineVideo();
                            return;
                        case "web_link":
                            console.log("正在点击外部链接~");
                            openWebLink();
                            return;
                        case "forum":
                            console.log("准备查找帖子并回复...");
                            openForum();
                            return;
                        case "material":
                            console.log("正在给课件发送已阅读状态");
                            openApiMaterial();
                            return;
                        default:
                            setTimeout(returnCoursePage, interval.other);
                            return;
                    }
                }
            }
        })(window, document);

QingJ © 2025

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