Greasy Fork

Greasy Fork is available in English.

UOOC 学习助手_szu_v2

自动静音、二倍速、失焦不断播、自动连播、自动答题(视频中非测验)。递归可开关,修复直接跳过首测验视频小节的bug。

当前为 2025-12-24 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         UOOC 学习助手_szu_v2
// @namespace    https://github.com/xiaochai-123
// @version      1.4.2
// @description  自动静音、二倍速、失焦不断播、自动连播、自动答题(视频中非测验)。递归可开关,修复直接跳过首测验视频小节的bug。
// @license      GPL
// @match        *://uooc.net.cn/home/learn/index*
// @match        *://www.uooc.net.cn/home/learn/index*
// @match        *://uooconline.com/home/learn/index*
// @match        *://www.uooconline.com/home/learn/index*
// @grant        none
// ==/UserScript==

(function () {
    "use strict";

    // 配置常量
    const CONFIG = {
        playbackRate: 2,
        interval: {
            player: 500,
            cleaner: 800,
            quiz: 500,
            logic: 800
        }
    };

    // 自动播放和倍速静音
    function keepPlaying() {
        const video = document.getElementById("player_html5_api");
        if (video) {
            // 优化:仅在状态不一致时修改,减少DOM操作开销
            if (!video.muted) video.muted = true;
            if (video.playbackRate !== CONFIG.playbackRate) video.playbackRate = CONFIG.playbackRate;
            if (video.paused && !video.ended) {
                video.play().catch(() => {}); // 捕获在此处可能产生的Promise报错
            }
        }
        const playBtn = document.querySelector(".vjs-big-play-button");
        if (playBtn && playBtn.style.display !== 'none') {
            playBtn.click();
        }
    }
    setInterval(keepPlaying, CONFIG.interval.player);

    // 强力弹窗和遮罩清理函数
    setInterval(() => {
        let hasSmartVerification = false;
        document.querySelectorAll('.layui-layer').forEach(layer => {
            const layerText = layer.innerText || '';
            // 关键词检测
            if (['验证', '智能', '提交'].some(kw => layerText.includes(kw)) || layer.querySelector('.layui-layer-btn')) {
                hasSmartVerification = true;
            }
        });

        if (!hasSmartVerification) {
            document.querySelectorAll('.layui-layer-shade').forEach(e => e.remove());
            document.querySelectorAll('.layui-layer.layui-layer-page').forEach(e => {
                const layerText = e.innerText || '';
                if (!['验证', '智能', '提交'].some(kw => layerText.includes(kw))) {
                    e.remove();
                }
            });
            document.querySelectorAll('.vjs-mask').forEach(e => e.remove());
            
            let quizDom = document.querySelector(".smallTest-view");
            if (quizDom && quizDom.offsetTop > 600) {
                quizDom.style.display = "none";
            }
        }
    }, CONFIG.interval.cleaner);

    // 自动答题
    let lastQuizQuestion = null;
    function autoAnswerQuiz() {
        let quizLayer = document.querySelector("#quizLayer");
        if (quizLayer && quizLayer.style.display !== "none") {
            try {
                let videoDiv = document.querySelector("div[uooc-video]");
                if (!videoDiv) return;
                
                let source = videoDiv.getAttribute("source");
                if (!source) return;
                
                let quizList = JSON.parse(source).quiz || [];
                let quizQuestionElem = document.querySelector(".smallTest-view .ti-q-c");
                if (!quizQuestionElem) return;
                
                let quizQuestion = quizQuestionElem.innerHTML.trim();

                if (lastQuizQuestion === quizQuestion && quizLayer.classList.contains("answered")) {
                    return;
                }

                let quizIndex = quizList.findIndex(q => q.question === quizQuestion);
                if (quizIndex === -1) return;
                
                // 注意:平台数据通常是原始JS字符串,eval在此处必须保留
                let quizAnswer = eval(quizList[quizIndex].answer);
                let quizOptions = quizLayer.querySelector("div.ti-alist");
                
                if (quizOptions) {
                    for (let ans of quizAnswer) {
                        let labelIndex = ans.charCodeAt() - "A".charCodeAt();
                        // 增加安全校验
                        let opt = quizOptions.children[labelIndex];
                        if (opt) opt.click();
                    }
                    let btn = quizLayer.querySelector("button");
                    if (btn) btn.click();
                    
                    quizLayer.classList.add("answered");
                    lastQuizQuestion = quizQuestion;
                    console.log("自动答题已完成:", quizQuestion, quizAnswer.toString());
                }
            } catch (error) {
                console.log("自动答题发生错误:", error);
            }
        } else {
            lastQuizQuestion = null;
            let answeredLayer = document.querySelector("#quizLayer.answered");
            if (answeredLayer) answeredLayer.classList.remove("answered");
        }
    }
    setInterval(autoAnswerQuiz, CONFIG.interval.quiz);

    // ======= 递归连播开关和核心 =======
    let autoRecursiveFlag = false;

    // 创建控制按钮(样式优化)
    function createControlBtn() {
        let btn = document.createElement("button");
        btn.innerText = "递归上课:关闭";
        Object.assign(btn.style, {
            position: "fixed",
            top: "140px",
            right: "30px",
            zIndex: "9999",
            background: "#00796b",
            color: "#fff",
            border: "none",
            padding: "11px 22px",
            borderRadius: "8px",
            cursor: "pointer",
            boxShadow: "0 2px 12px rgba(0,0,0,0.18)",
            userSelect: "none"
        });
        
        btn.onclick = function () {
            autoRecursiveFlag = !autoRecursiveFlag;
            btn.innerText = "递归上课:" + (autoRecursiveFlag ? "开启" : "关闭");
            if (autoRecursiveFlag) {
                startUltimateCourseRush();
            }
        };
        document.body.appendChild(btn);
    }
    createControlBtn();

    function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }

    function clearShader() {
        let hasSmartVerification = false;
        document.querySelectorAll('.layui-layer').forEach(layer => {
            const layerText = layer.innerText || '';
            if (['验证', '智能', '提交'].some(kw => layerText.includes(kw)) || layer.querySelector('.layui-layer-btn')) {
                hasSmartVerification = true;
            }
        });

        if (!hasSmartVerification) {
            document.querySelectorAll("div.layui-layer-shade").forEach(shader => shader.remove());
            document.querySelectorAll('.vjs-mask').forEach(e => e.remove());
        }
    }

    function findCurrentVideoNode() {
        return document.querySelector('.basic.active') || 
               document.querySelector('.video-list li.current, .chapter-list li.current, .menu li.current, li.active, li.selected');
    }

    function findNextVideoInSameChapter(currentNode) {
        if (!currentNode) return null;
        let chapterContainer = currentNode.closest('ul, .chapter-list, .video-list');
        if (!chapterContainer) return null;
        
        let allVideoNodes = Array.from(chapterContainer.querySelectorAll('li, .basic'));
        let currentIndex = allVideoNodes.indexOf(currentNode);
        
        if (currentIndex === -1 || currentIndex >= allVideoNodes.length - 1) {
            return null;
        }
        
        for (let i = currentIndex + 1; i < allVideoNodes.length; i++) {
            let nextNode = allVideoNodes[i];
            // 增加空值检查
            if (nextNode.querySelector('.icon-video, span.icon-video') || (nextNode.classList.contains('taskpoint') && !nextNode.innerText.includes("测验"))) {
                return nextNode;
            }
        }
        return null;
    }

    function playNextVideoInChapter() {
        let current = findCurrentVideoNode();
        let next = findNextVideoInSameChapter(current);
        if (next) {
            let clickTarget = next.querySelector('a') || next;
            clickTarget.click();
            setTimeout(() => {
                const video = document.getElementById('player_html5_api');
                if (video) {
                    video.muted = true;
                    video.playbackRate = CONFIG.playbackRate;
                    video.play().catch(() => {});
                }
            }, 1000);
            return true;
        }
        return false;
    }

    function waitForVideoCompletion() {
        return new Promise(resolve => {
            const video = document.getElementById('player_html5_api');
            if (!video) {
                resolve();
                return;
            }
            video._recursiveResolve = resolve;
            video.onended = function() {
                setTimeout(() => {
                    if (video._recursiveResolve) {
                        video._recursiveResolve();
                    }
                }, 1500);
            };
            video.muted = true;
            video.playbackRate = CONFIG.playbackRate;
            
            const playBtn = document.querySelector(".vjs-big-play-button");
            if (playBtn) playBtn.click();
            
            setTimeout(() => {
                if (video.ended && video._recursiveResolve) {
                    video._recursiveResolve();
                }
            }, 500);
        });
    }

    function findCurrentChapter() {
        const currentVideoNode = findCurrentVideoNode();
        if (!currentVideoNode) return null;
        let node = currentVideoNode;
        while (node && node.parentElement) {
            if (node.parentElement.classList.contains('rank-1') || node.classList.contains('rank-1-item')) {
                return node;
            }
            node = node.parentElement;
        }
        return null;
    }

    async function searchUncompleteFromCurrentChapter() {
        const catalogRoot = document.querySelector("ul.rank-1");
        if (!catalogRoot) return;
        
        const chapters = Array.from(catalogRoot.children);
        const currentChapter = findCurrentChapter();
        let startIndex = 0;
        
        if (currentChapter) {
            startIndex = chapters.indexOf(currentChapter);
            if (startIndex === -1) startIndex = 0;
        }
        
        for (let i = startIndex; i < chapters.length; i++) {
            await checkActive(chapters[i]);
        }
    }

    async function checkActive(catalog) {
        if (!catalog) return;
        let children = catalog.children;
        let elem = catalog.firstElementChild;
        
        // 展开章节
        if (elem) {
            let iElement = elem.getElementsByTagName("i")[0];
            if (iElement && iElement.classList.contains("icon-xiangxia")) {
                elem.click();
            }
            await sleep(500);
        }
        
        for (let i = 1; i < children.length; i++) {
            const child = children[i];
            if (child.tagName === "DIV") {
                // 仅选择直接子元素,避免深度查询
                const resources = Array.from(child.querySelectorAll(':scope > .basic'));
                for (let r of resources) {
                    if (r.classList.contains('complete')) continue;
                    
                    let nameSpan = r.querySelector('.tag-source-name');
                    let nameText = nameSpan ? (nameSpan.innerText || '') : '';
                    
                    if (nameSpan && nameSpan.classList.contains('taskpoint') && nameText.includes('测验')) {
                        continue;
                    }
                    
                    if (r.querySelector('.icon-video') || /视频/.test(nameText)) {
                        r.click();
                        clearShader();
                        await waitForVideoCompletion();
                        while (playNextVideoInChapter()) {
                            await waitForVideoCompletion();
                        }
                        return; // 完成一个视频序列后返回上一层继续
                    }
                }
            } else if (child.tagName === "UL") {
                await searchUncomplete(child);
            }
        }
    }

    async function searchUncomplete(query) {
        if (!query) return;
        let catalog = query.children;
        for (let i = 0; i < catalog.length; i++) {
            await checkActive(catalog[i]);
        }
    }

    async function startUltimateCourseRush() {
        if (!autoRecursiveFlag) return;
        await searchUncompleteFromCurrentChapter();
    }

    function bindVideoEnded() {
        const video = document.getElementById('player_html5_api');
        if (video && !video._autoNextBound) {
            video._autoNextBound = true;
            video.onended = function () {
                if (autoRecursiveFlag) {
                    // 如果无法播放同章下一个,则触发递归寻找下一章
                    if (!playNextVideoInChapter()) {
                        setTimeout(() => { startUltimateCourseRush(); }, 1500);
                    }
                }
            };
        }
    }

    // 核心初始化逻辑 - 替换掉了 $(document).ready
    function init() {
        console.log("UOOC 学习助手 v1.4.2 已加载");
        setTimeout(() => { 
            if (autoRecursiveFlag) startUltimateCourseRush(); 
        }, 1000);

        // 递归保护循环
        setInterval(async function () {
            if (autoRecursiveFlag) {
                const video = document.getElementById('player_html5_api');
                // 增加判断:如果当前没有激活的节点,也尝试启动递归(防止卡死)
                if ((!video || video.ended) && !document.querySelector(".basic.active")) {
                    await startUltimateCourseRush();
                }
            }
        }, CONFIG.interval.logic);

        setInterval(bindVideoEnded, CONFIG.interval.logic);
    }

    // 原生 DOM Ready 检测
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        init();
    } else {
        window.addEventListener('load', init);
    }

})();