Skip to content

针对视频中间弹出题目的问题,用vibe coding改进了,可以解答单选、多选和判断题 #39

@Lovinghost

Description

@Lovinghost
(function () {
    // 检查页面是否已加载jQuery,如果没有则加载
    if (typeof window.jQuery === 'undefined') {
        const script = document.createElement('script');
        script.src = 'https://code.jquery.com/jquery-3.6.0.min.js';
        script.type = 'text/javascript';
        script.onload = function () {
            console.log("jQuery loaded.");
            initializePlayer();
        };
        document.head.appendChild(script);
    } else {
        initializePlayer();
    }

    function initializePlayer() {
        window.app = {
            configs: {
                playbackRate: 1, 
                autoplay: true, 
                retryInterval: 2000, 
                maxRetries: 10, 
                videoCheckInterval: 1000, 
            },
            _videoEl: null,
            _treeContainerEl: null,
            _isPlaying: false,
            _currentRetryCount: 0,
            _checkInterval: null,
            _cellData: {
                cells: 0, 
                nCells: 0, 
                currentCellIndex: 0, 
                currentNCellIndex: 0, 
                currentVideoTitle: "", 
            },
            
            _quizState: {
                active: false,
                optionsCount: 0,
                attemptIndex: 0,
                combinations: [],
                lastSubmitTime: 0
            },

            get cellData() {
                return this._cellData;
            },
            run() {
                console.log("%c=== 学习通自动刷课脚本 V9 完美闭环版启动 ===", "color:#4CAF50;font-size:16px;font-weight:bold");
                this._getTreeContainer();
                this._initCellData();
                this._videoEl = null;
                this._getVideoEl();
                this._clearCheckInterval();
                this.play();
            },

            nextUnit() {
                console.log("%c=== 准备切换到下一小节 ===", "color:#2196F3;font-size:14px");
                const el = this._getTreeContainer();
                const cells = el.children("ul").children("li");
                const nCells = $(cells.get(this._cellData.currentCellIndex)).find('.posCatalog_select:not(.firstLayer)');
                if (nCells.length > this._cellData.currentNCellIndex + 1) {
                    const nextNIndex = this._cellData.currentNCellIndex + 1;
                    console.log(`%c切换到同章节下一个视频: ${nextNIndex + 1}/${nCells.length}`, "color:#FF9800");
                    this.playCurrentIndex(nCells.get(nextNIndex));
                } else {
                    const nextIndex = this._cellData.currentCellIndex + 1;
                    if (nextIndex >= cells.length) {
                        console.log("%c==============本课程学习完成了==============", "color:#4CAF50;font-size:16px;font-weight:bold");
                        this._clearCheckInterval();
                        return;
                    }
                    console.log(`%c切换到下一个章节: ${nextIndex + 1}/${cells.length}`, "color:#FF9800");
                    this._cellData.currentCellIndex = nextIndex;
                    this._cellData.currentNCellIndex = 0;
                    this.playCurrentIndex();
                }
            },
            
            _clearCheckInterval() {
                if (this._checkInterval) {
                    clearInterval(this._checkInterval);
                    this._checkInterval = null;
                }
            },
            
            _startVideoMonitoring() {
                this._clearCheckInterval();
                this._checkInterval = setInterval(() => {
                    this._checkVideoStatus();
                }, this.configs.videoCheckInterval);
            },
            
            _generateCombinations(numOptions, isMulti) {
                const combos = [];
                if (!isMulti) {
                    for (let i = 0; i < numOptions; i++) combos.push([i]);
                } else {
                    const max = 1 << numOptions;
                    for (let i = 1; i < max; i++) {
                        const currentCombo = [];
                        for (let j = 0; j < numOptions; j++) {
                            if ((i & (1 << j)) !== 0) {
                                currentCombo.push(j);
                            }
                        }
                        combos.push(currentCombo);
                    }
                    combos.sort((a, b) => a.length - b.length);
                }
                return combos;
            },

            /// 【核心修复】:优先检测“继续学习”,再处理答题逻辑
            _handleVideoQuiz() {
                try {
                    const frameObj = $("iframe").eq(0).contents().find("iframe.ans-insertvideo-online");
                    if (frameObj.length === 0) {
                        this._quizState.active = false; 
                        return false;
                    }
                    
                    const doc = frameObj.contents();
                    
                    if (doc[0].defaultView && !doc[0].defaultView._alertOverridden) {
                        doc[0].defaultView.alert = function(msg) { 
                            console.log("%c拦截到系统弹窗: " + msg, "color:#9C27B0"); 
                        };
                        doc[0].defaultView._alertOverridden = true;
                    }

                    // ==========================================
                    // 步骤 1:最高优先级,寻找“通关”按钮(答对后出现的按钮)
                    // 只要出现这些按钮,不管选项还在不在,直接点!
                    // ==========================================
                    let continueBtn = null;
                    doc.find('.ans-videoquiz-button, button, a, span, div, input').each(function() {
                        const text = $(this).text().replace(/\s+/g, '');
                        const val = $(this).val() ? $(this).val().replace(/\s+/g, '') : '';
                        // 加入了“继续学习”作为首要匹配项
                        if (['继续学习', '继续', '关闭', '完成', '继续播放'].some(k => text === k || val === k) && $(this).is(':visible')) {
                            continueBtn = this;
                            return false; // 找到就退出循环
                        }
                    });

                    if (continueBtn) {
                        console.log("%c检测到 [继续学习/关闭] 按钮,执行二次点击以恢复播放...", "color:#4CAF50;font-weight:bold");
                        setTimeout(() => {
                            continueBtn.click();
                            continueBtn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
                        }, 500);
                        this._quizState.active = false; // 彻底重置答题状态
                        return true; 
                    }

                    // ==========================================
                    // 步骤 2:如果没有通关按钮,说明还在【答题中】
                    // ==========================================
                    const visibleOptions = doc.find('input[type="radio"]:visible, input[type="checkbox"]:visible');
                    const isMulti = doc.find('input[type="checkbox"]:visible').length > 0;

                    // 频率控制锁
                    const now = Date.now();
                    if (now - this._quizState.lastSubmitTime < 2500) {
                        return true; 
                    }

                    if (visibleOptions.length > 0) {
                        
                        // 寻找提交按钮
                        let submitBtn = null;
                        doc.find('.ans-videoquiz-submit, .btn-submit, button, span, div, a, input').each(function() {
                            const text = $(this).text().replace(/\s+/g, '');
                            const val = $(this).val() ? $(this).val().replace(/\s+/g, '') : '';
                            if (['提交', '确定', '确认'].some(k => text === k || val === k) && $(this).is(':visible')) {
                                submitBtn = this;
                                return false; 
                            }
                        });

                        // 初始化穷举序列
                        if (!this._quizState.active || this._quizState.optionsCount !== visibleOptions.length) {
                            this._quizState.active = true;
                            this._quizState.optionsCount = visibleOptions.length;
                            this._quizState.attemptIndex = 0;
                            this._quizState.combinations = this._generateCombinations(visibleOptions.length, isMulti);
                            console.log(`%c检测到新题目,生成 ${this._quizState.combinations.length} 种组合方案...`, "color:#FF9800;font-weight:bold");
                        }

                        // 如果所有组合都试完了
                        if (this._quizState.attemptIndex >= this._quizState.combinations.length) {
                            console.error("%c所有选项已穷举完毕,可能题目异常,暂停尝试", "color:#F44336;font-weight:bold");
                            this._quizState.lastSubmitTime = now + 10000;
                            return true;
                        }

                        // 勾选选项
                        const currentCombo = this._quizState.combinations[this._quizState.attemptIndex];
                        if (isMulti) visibleOptions.prop('checked', false);
                        currentCombo.forEach(idx => visibleOptions.get(idx).click());
                        console.log(`%c尝试选中组合序号: ${currentCombo.join(',')}`, "color:#2196F3");

                        // 执行点击提交
                        if (submitBtn) {
                            setTimeout(() => {
                                submitBtn.click();
                                submitBtn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
                                console.log("%c已点击 [提交/确定] 按钮", "color:#9C27B0;font-weight:bold");
                            }, 500);
                        } else {
                            console.log("%c正在寻找提交按钮,可能尚未渲染...", "color:#607D8B");
                        }

                        this._quizState.attemptIndex++;
                        this._quizState.lastSubmitTime = now;
                        return true;
                    }

                    if (this._quizState.active) {
                        this._quizState.active = false;
                    }
                    return false;

                } catch (e) {
                    console.error("检测/处理弹题失败:", e);
                    return false;
                }
            },

            _checkVideoStatus() {
                try {
                    const video = this._getVideoEl();
                    if (!video) return;
                    
                    if (video.paused && this._isPlaying) {
                        if (this._handleVideoQuiz()) return; 

                        console.log("%c检测到视频暂停,尝试恢复播放...", "color:#FF5722");
                        video.play().catch(e => console.error("恢复播放失败:", e));
                    }
                    
                    if (video.ended && this._isPlaying) {
                        console.log("%c检测到视频结束,准备切换下一个...", "color:#9C27B0");
                        this._isPlaying = false;
                        setTimeout(() => this.nextUnit(), 1000);
                    }
                } catch (e) {
                    console.error("视频状态检查失败:", e);
                }
            },
            
            _tryTimes: 0,
            async play() {
                try {
                    const el = this._getVideoEl();
                    if (el == null) {
                        if (document.getElementsByClassName("prev_title")[0] && 
                            document.getElementsByClassName("prev_title")[0].title !== "章节测验") {
                            throw new Error("视频元素为空");
                        }
                        $("#prevNextFocusNext").click();
                        setTimeout(() => this.play(), 2000);
                        return;
                    }
                    
                    this._tryTimes = 0;
                    this._isPlaying = true;
                    this._videoEventHandle();
                    el.playbackRate = this.configs.playbackRate;
                    
                    try {
                        await el.play();
                        console.log(`%c视频开始播放,倍速: ${el.playbackRate}x`, "color:#4CAF50");
                        this._startVideoMonitoring();
                    } catch (playError) {
                        console.error("视频播放失败:", playError);
                        this._handlePlayError(playError);
                    }
                } catch (e) {
                    if (this._tryTimes > this.configs.maxRetries) {
                        this._clearCheckInterval();
                        return;
                    }
                    this._tryTimes++;
                    setTimeout(() => this.play(), this.configs.retryInterval);
                }
            },
            
            _handlePlayError(error) {
                const video = this._getVideoEl();
                if (video) {
                    video.muted = true;
                    video.play().then(() => {
                        console.log("%c静音播放成功", "color:#4CAF50");
                        setTimeout(() => { video.muted = false; }, 2000);
                    }).catch(e => {
                        setTimeout(() => this.nextUnit(), 3000);
                    });
                }
            },
            
            playCurrentIndex(nCell) {
                if (!nCell) {
                    const el = this._getTreeContainer();
                    const cells = el.children("ul").children("li");
                    const nCells = $(cells.get(this._cellData.currentCellIndex)).find('.posCatalog_select:not(.firstLayer)');
                    nCell = nCells.get(this._cellData.currentNCellIndex);
                }
                
                const $nCell = $(nCell);
                const clickableSpan = $nCell.find(".posCatalog_name")[0];
                if (!clickableSpan) {
                    setTimeout(() => this.nextUnit(), 2000);
                    return;
                }
                
                $(clickableSpan).click(); 
                this._videoEl = null;
                this._isPlaying = false;

                setTimeout(() => {
                    this._initCellData();
                    if (this.configs.autoplay) this.play();
                }, 3000); 
            },
            
            _initCellData() {
                const el = this._getTreeContainer();
                const cells = el.children("ul").children("li");
                this._cellData.cells = cells.length;
                let nCellCounts = 0;
                let foundCurrent = false;
                cells.each((i, v) => {
                    const nCells = $(v).find('.posCatalog_select:not(.firstLayer)');
                    nCellCounts += nCells.length;
                    nCells.each((j, e) => {
                        const _el = $(e);
                        if (_el.hasClass("posCatalog_active")) {
                            this._cellData.currentCellIndex = i;
                            this._cellData.currentNCellIndex = j;
                            foundCurrent = true;
                            const titleSpan = _el.find('.posCatalog_name')[0];
                            if (titleSpan) {
                                this._cellData.currentVideoTitle = $(titleSpan).attr('title');
                            }
                        }
                    });
                });
                this._cellData.nCells = nCellCounts;
            },
            
            _getTreeContainer() {
                if (!this._treeContainerEl) {
                    const el = $('#coursetree');
                    if (el.length <= 0) throw new Error("找不到视频列表");
                    this._treeContainerEl = el;
                }
                return this._treeContainerEl;
            },
            
            _getVideoEl() {
                if (!this._videoEl) {
                    try {
                        const frameObj = $("iframe").eq(0).contents().find("iframe.ans-insertvideo-online");
                        if (frameObj.length === 0) return null;
                        this._videoEl = frameObj.contents().eq(0).find("video#video_html5_api").get(0);
                    } catch (e) {
                        return null;
                    }
                }
                if (!this._videoEl) throw new Error("视频组件Video未加载完成");
                return this._videoEl;
            },
            
            _videoEventHandle() {
                const el = this._videoEl;
                if (!el) return;
                
                el.removeEventListener("ended", this._handleVideoEnded);
                el.removeEventListener("loadedmetadata", this._handleVideoLoaded);
                el.removeEventListener("play", this._handleVideoPlay);
                el.removeEventListener("pause", this._handleVideoPause);
                
                el.addEventListener("ended", this._handleVideoEnded.bind(this));
                el.addEventListener("loadedmetadata", this._handleVideoLoaded.bind(this));
                el.addEventListener("play", this._handleVideoPlay.bind(this));
                el.addEventListener("pause", this._handleVideoPause.bind(this));
            },
            
            _handleVideoEnded(e) {
                this._isPlaying = false;
                this._clearCheckInterval();
                setTimeout(() => this.nextUnit(), 1000);
            },
            
            _handleVideoLoaded(e) {
                if (this.configs.autoplay && !this._isPlaying) this.play();
            },
            
            _handleVideoPlay(e) {
                this._isPlaying = true;
            },
            
            _handleVideoPause(e) {}
        };
        
        try {
            window.app.run();
            const preventPause = (e) => { e.stopPropagation(); e.preventDefault(); };
            document.addEventListener("mouseleave", preventPause);
            window.addEventListener("mouseleave", preventPause);
            document.addEventListener("mouseout", preventPause);
            window.addEventListener("mouseout", preventPause);
            
            window.addEventListener("blur", (e) => {
                const video = window.app._getVideoEl();
                if (video && video.paused && window.app._isPlaying) {
                    video.play().catch(err => {});
                }
            });
        } catch (error) {
            console.error("%c脚本运行失败: ", "color:#F44336;font-weight:bold", error.message);
        }
    }
})();``

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions