从零到一:打造“终极全能网课助手”的开发心路历程


最近完成了一个项目——“全能网课助手”,它能实现从自动选课、倍速播放到自动答题的全流程自动化。这个过程踩了不少坑。

阶段一:需求的诞生——只想“偷个懒”
一切始于最朴素的需求,最初的核心需求被拆解为三个基本目标:

自动找课:能自动找到没学完的课程并点进去。
自动播放:进入视频页后,能自动开始播放,最好能倍速。
自动答题:解决视频中途弹出的计算题。
有了这三个目标,正式开始。

阶段二:V1.0 雏形——核心功能的逐个击破
采用“分而治之”的策略,先在不同的页面上,把核心功能一个个实现。

  1. 攻克视频播放页:一切从 querySelector 开始
    这是最直观、最容易入手的一环。

思路:

找到页面上的 <video> 元素。

调用它的 .play() 方法。

设置它的 .playbackRate 属性来控制倍速。

监听它的 ended 事件,结束后跳转回列表页。

遇到的第一个坑:动态加载的视频元素
有时候脚本运行时,视频元素还没被页面的 JavaScript 完全加载出来,document.querySelector('video') 返回 null,导致脚本报错。最初用 setTimeout 延迟执行,但这很不稳定。
解决方案:引入 MutationObserver。这是一个强大的 API,能“监视”DOM 的变化。让它监视整个 document,一旦有新节点被添加,就检查是不是想要的

  1. 征服课程列表页:从静态到动态
    搞定了播放页,回到课程列表页,处理“自动选课”和“自动翻页”。

思路:

获取所有未完成的课程行。
遍历它们,找到第一个进度不是 100% 的。
点击该行的“继续学习”按钮。
如果本页都完成了,就找到“下一页”按钮并点击。
实现:
通过审查元素,发现未完成的课程行有一个特定的 ng-repeat 属性,进度信息在一个 标签里。于是用 document.querySelectorAll 拿到所有行,写了个循环来判断。
翻页逻辑也类似,找到当前页码,然后点击它的下一个兄弟元素。

遇到的第二个坑:框架生成的“假”点击事件
发现对“继续学习”按钮调用 .click() 方法居然没反应!F12 查看元素,发现它绑定的是 AngularJS 的 ng-click 事件。这种由前端框架绑定的事件,有时无法通过简单的 DOM API 触发。
解决方案:模拟一个真实的鼠标点击事件。通过 new MouseEvent('click', { bubbles: true, cancelable: true }) 创建一个事件对象,然后用 element.dispatchEvent() 派发出去。果然,页面成功跳转了!

阶段三:V2.0 整合与健壮性——让它成为一个“产品”
核心功能都实现了,但代码散乱,体验也很差。需要把它们整合起来,并提升健壮性。

思路:

页面路由:脚本需要知道自己当前在哪个页面,然后执行对应的逻辑。
UI交互:用户可能想手动调速,或者临时关闭自动答题。需要一个控制面板。
配置持久化:用户的设置(比如倍速)应该被记住。
实现:

主逻辑 mainLogic():在脚本入口,通过 window.location.href.includes() 判断 URL,如果包含课程列表页的特征字符串,就调用 handleCourseListPage();如果包含视频播放页的,就调用 handleVideoPage()。一个简单的页面路由器就成型了。
控制面板:使用 GM_addStyle 和 document.body.insertAdjacentHTML 动态创建了一个悬浮的控制面板。包含了速度输入框、预设按钮、答题开关等。还给它加了拖动功能,提升用户体验。
数据存储:利用油猴提供的 GM_setValue 和 GM_getValue API,将用户的播放速度、自动答题开关状态保存下来。每次启动脚本时先读取配置,确保体验的一致性。
到这里,脚本已经相当好用了。它能自动循环地完成整个学习流程,并且提供了足够的用户控制选项。

阶段四:V3.0/3.7 体验飞跃——“黑盒”变“透明”
虽然功能强大,但用户在使用时会有一个很大的痛点:脚本到底在干嘛? 它是在找课?卡住了?还是已经完成了?整个过程像个黑盒。为了解决这个问题,决定引入一个全新的特性——全局状态监视器。

思路:

在页面角落创建一个小悬浮窗,专门用来显示脚本的当前状态。
用不同的颜色来表示不同的状态(例如:蓝色-进行中,绿色-成功,黄色-警告,红色-错误)。
在代码的每一个关键步骤,都去更新这个悬浮窗里的信息。
实现:

UI创建:和控制面板类似,用 HTML 和 CSS 创建了一个简洁的状态栏。
中央状态管理函数 updateStatus(message, type):封装了一个核心函数,它负责更新状态栏的文本和样式。比如调用 updateStatus('找到目标课程,准备跳转…', 'info'),状态栏就会显示蓝底的提示信息。
深度集成:把这个函数“埋”进了代码的各个角落:
开始找课时,调用 updateStatus('正在扫描课程…')。
找到视频时,调用 updateStatus('视频已锁定,准备播放…', 'success')。
遇到答题时,调用 updateStatus('检测到题目,正在作答…', 'warning')。
等待超时时,调用 updateStatus('等待元素超时,请检查页面', 'error')。
全部完成后,调用 updateStatus('🎉 所有课程均已完成!', 'success')。
这个改进带来了质的飞跃。用户可以实时看到脚本的“心路历程”,极大地提升了信任感和体验的确定性。

最终思考与总结
回顾整个开发过程,总结出几点心得:

从简到繁,迭代开发:不要一开始就想做一个完美的“大而全”的东西。先把最核心的功能实现,然后再逐步添加UI、提升健壮性、优化体验。
拥抱现代 Web API:像 MutationObserver 这样的 API 是解决动态内容问题的利器,远比 setInterval 轮询要高效和优雅。
注重用户体验:一个功能再强大的工具,如果用户不知道它在干什么,体验也是糟糕的。一个简单的状态监视器,就能让“后台工具”变得“前台化”,让用户感到安心。
保护劳动成果:在分享的同时,也要考虑如何保护自己的代码。学习并使用代码混淆,是一种在开源精神与个人权益之间取得平衡的好方法。
从一行 document.querySelector 开始,到一个拥有状态反馈、可配置、全自动的“终极助手”,这个过程不仅锻炼了编码能力,更深刻体会到“以用户为中心”进行软件设计的乐趣。

终极全能网课助手 (v3.7) - 实现逻辑详解

I. 概述 (Overview)

本脚本的核心设计理念是实现“一次启动,全程托管”的全自动学习体验。它通过模拟用户的常规操作(页面跳转、点击、播放、答题)来完成整个学习流程,并引入了全新的实时状态监视器,为用户提供了前所未有的透明度和掌控感。

脚本的执行逻辑是状态驱动页面感知的。它首先判断当前所在的页面(课程列表页 vs. 视频播放页),然后执行该页面对应的自动化任务。

II. 核心特性与技术实现

  1. 全自动课程导航 (Fully Automatic Course Navigation)

    • 逻辑: 脚本自动寻找列表中第一个未完成(进度 < 100%)的课程,并进入学习。

    • 实现:

      • 目标识别: 在课程列表页,脚本通过 document.querySelectorAll 精准定位到所有未通过的课程行 (tr[ng-repeat="item in model.notPass.list"])。

      • 进度判断: 遍历每一行,查找包含“课程进度”字样的 <span> 标签,并解析其进度值。如果进度不是 100%,则将其标记为下一个学习目标。

      • 模拟点击: 找到目标课程后,脚本会定位到“继续学习”按钮 (a[ng-click^="events.checkHasPhoto"])。关键在于,它不使用简单的 .click() 方法,而是通过 new MouseEvent() 创建一个真实的点击事件,并使用 dispatchEvent() 触发。这能更好地兼容 AngularJS 等前端框架,确保点击事件被正确监听和处理。

      • 自动翻页: 如果当前页所有课程均已完成,脚本会查找分页组件,定位“当前页”按钮 (a.current),并点击其下一个兄弟元素 (nextElementSibling) 来实现翻页,直到找不到下一页或所有课程完成。

      • 动态内容监控: 考虑到课程列表可能是动态加载的,脚本使用 MutationObserver 监听课程表格 <tbody> 的变化。一旦内容发生改变(如翻页后),就会重新触发扫描逻辑,确保不会错过任何课程。

  2. 鲁棒的视频播放控制 (Robust Video Playback Control)

    • 逻辑: 在视频播放页,脚本必须稳定地找到视频元素,并对其进行播放、倍速、静音等控制,同时处理播放结束后的跳转。

    • 实现:

      • 视频元素查找 (findAndSetupVideoRobustly): 这是脚本最关键的鲁棒性设计之一。由于视频 <video> 元素可能是由 JavaScript 动态插入到页面中的,直接查询可能会失败。因此,脚本采用双重保障机制:

        1. 立即查询: 首先尝试用 document.querySelector('video') 直接查找。

        2. 备用方案 (MutationObserver): 如果没找到,则启动一个 MutationObserver 来监听整个 document 的节点添加事件。一旦有任何节点被添加到页面中,它就会检查这个节点是不是 <video> 标签或者其子孙节点中是否包含 <video>。一旦找到,立即捕获该元素,执行后续操作,并停止该观察者以节省性能。

      • 事件绑定 (handleVideoFound):

        • canplay: 监听此事件确保视频已准备好播放,然后调用 .play() 方法,并通过 Promise 的 .catch() 块处理可能出现的“自动播放失败”异常。

        • ended: 监听视频播放结束事件。结束后,延迟3秒,然后通过 window.location.href 导航回课程列表页,形成自动化闭环。

      • 用户设置持久化: 用户的播放速度和自动答题开关设置,通过 GM_setValueGM_getValue 存储在油猴脚本的本地存储中,确保下次打开时配置依然生效。

  3. 智能自动答题 (Intelligent Auto-Answering)

    • 逻辑: 在视频播放过程中,实时监控是否有弹题窗口出现,并自动计算答案并提交。

    • 实现:

      • 弹窗监控: 同样利用 MutationObserver 监控 document.body。这种方式远比 setInterval 轮询检查高效。一旦检测到DOM变化,就触发 solveMathQuiz 函数。

      • 问题解析: solveMathQuiz 函数首先检查答题弹窗 (.m-exam-dialog) 是否存在。然后,提取问题文本,使用正则表达式 (\d+)\s*([+\-*/])\s*(\d+) 匹配出两个操作数和一个操作符。

      • 答案计算: 使用 switch 语句根据操作符计算出正确答案。

      • 选项匹配与提交: 遍历所有选项,将其文本内容转换为数字,与计算结果进行比对。找到匹配的选项后,模拟点击其对应的单选框 (input[type="radio"]),并延迟一个随机的短暂时间(500-1000ms)后,点击提交按钮,模拟人类作答的延迟。

  4. ✨ 全新状态监视器 (Brand New Status Monitor)

    • 逻辑: 这是 v3.7 的核心用户体验升级。它提供一个全局的、非侵入式的悬浮窗,实时反馈脚本的当前状态。

    • 实现:

      • UI创建: 脚本启动时,通过 document.body.insertAdjacentHTML 将状态监视器的 HTML 结构注入到页面底部。同时,使用 GM_addStyle 添加对应的 CSS 样式,实现其悬浮、美观的布局和不同状态的颜色定义。

      • 中央状态更新函数 (updateStatus): 这是整个状态反馈系统的核心。它接收两个参数:message (要显示的消息) 和 type (状态类型,如 info, success, warning, error)。

      • 状态与样式联动: 该函数通过修改消息元素的 textContentclassName 来工作。CSS 中预设了 .status-info, .status-success 等类名,它们分别对应不同的背景色和文字颜色。当 updateStatus 函数改变类名时,监视器的外观也随之改变。

      • 深度集成: 脚本在每一个关键执行步骤(如“开始扫描”、“找到课程”、“跳转中”、“正在播放”、“检测到题目”等)都调用了 updateStatus 函数,从而实现了精细、实时的状态播报。

III. 整体工作流 (Overall Workflow)

  1. 脚本注入: 用户访问 zdkj.v.zzu.edu.cn 网站下的任何页面,脚本启动。

  2. 初始化: 创建状态监视器UI,并进入 mainLogic 函数。

  3. 路由判断:

    • If URL 包含课程列表页标识 (/center/myStudy/goods/detail):

      • 执行 handleCourseListPage 逻辑。

      • 状态监视器显示:“正在课程列表页寻找目标...”。

      • 找到未完成课程后,状态变为:“找到目标...3秒后跳转...”。

      • 执行跳转。

    • Else If URL 包含视频播放页标识 (/play/#/learn/):

      • 执行 handleVideoPage 逻辑。

      • 状态监视器显示:“当前在视频播放页,加载助手中...”。

      • 加载控制面板,并用 MutationObserver 寻找 <video> 元素。

      • 找到视频后,状态变为:“视频元素已锁定,准备播放...”。

      • 开始播放,状态变为:“▶️ 正在以 X.Xx 速度播放视频...”。

      • 若弹出题目,状态变为:“检测到题目,正在自动作答...”。

      • 视频播放结束,状态变为:“✅ 视频播放完毕,准备返回...”。

      • 跳转回课程列表页。

  4. 循环: 页面跳转回课程列表页后,脚本重新执行第3步,形成一个从“找课”到“看完”再到“找下一课”的完美闭环,直至所有课程完成。

  5. 任务完成: 当在课程列表页找不到任何未完成课程,也无法翻页时,脚本判断所有任务已完成,弹出提示,状态监视器显示:“🎉 所有课程均已完成!”。

源代码

// ==UserScript==
// @name         终极全能网课助手 (自动选课 + 视频控制 + 自动答题) v3.7
// @namespace    https://halo.kai666.asia/
// @version      3.7
// @description  增加了一个全局状态监视器悬浮窗,实时显示脚本的当前工作状态(如:寻找课程、播放视频、自动答题、任务完成等),并用不同颜色提示,极大提升了用户体验和透明度。
// @author       WaNG
// @match        https://*.zzu.edu.cn/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        unsafeWindow
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';
    const SCRIPT_NAME = "[全能助手 v3.7]";

    // ====================================================================
    // 1. 全局配置
    // ====================================================================
    const MAX_WAIT_SECONDS = 20;
    const courseListPageUrl = 'center/myStudy/goods/detail';
    const videoPageUrlIdentifier = '/play/#/learn/';
    const targetYear = '2025';

    const quizSelectors = {
        quizContainer: '.m-exam-dialog',
        questionText: '[data-id="topic"]',
        options: '.d-slt',
        optionValueText: '.ipt-txt-content',
        optionRadioButton: 'input[type="radio"]',
        submitButton: 'button[data-action="answer"]'
    };

    let videoElement = null;
    let videoObserver = null;
    const config = {
        isAutoAnswerEnabled: GM_getValue('autoAnswerEnabled', true),
        lastPlaybackRate: GM_getValue('lastPlaybackRate', 1.0),
    };

    // ====================================================================
    // 2. ✨ 全局UI & 状态管理器  ✨
    // ====================================================================
    function createStatusPanel() {
        const panelHTML = `
            <div id="gm-status-panel">
                <div id="gm-status-header">脚本状态</div>
                <div id="gm-status-message">初始化中...</div>
            </div>
        `;
        document.body.insertAdjacentHTML('beforeend', panelHTML);
    }

    function addStatusPanelStyles() {
        GM_addStyle(`
            #gm-status-panel {
                position: fixed;
                bottom: 20px;
                right: 20px;
                width: 250px;
                z-index: 100000;
                border-radius: 8px;
                background-color: #fff;
                box-shadow: 0 5px 15px rgba(0,0,0,0.3);
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
                overflow: hidden;
            }
            #gm-status-header {
                padding: 8px 12px;
                background-color: #6c757d;
                color: white;
                font-weight: bold;
                font-size: 14px;
            }
            #gm-status-message {
                padding: 12px;
                font-size: 14px;
                color: #333;
                background-color: #e9ecef;
                transition: background-color 0.3s ease;
            }
            #gm-status-message.status-info { background-color: #cfe2ff; color: #084298; }
            #gm-status-message.status-success { background-color: #d1e7dd; color: #0f5132; }
            #gm-status-message.status-warning { background-color: #fff3cd; color: #664d03; }
            #gm-status-message.status-error { background-color: #f8d7da; color: #842029; }
        `);
    }

    function updateStatus(message, type = 'info') {
        const statusElement = document.getElementById('gm-status-message');
        if (statusElement) {
            statusElement.textContent = message;
            statusElement.className = `status-${type}`;
        }
        // 同时在控制台打印,方便调试
        switch(type) {
            case 'error': console.error(`${SCRIPT_NAME} ${message}`); break;
            case 'warning': console.warn(`${SCRIPT_NAME} ${message}`); break;
            default: console.log(`${SCRIPT_NAME} ${message}`);
        }
    }

    // ====================================================================
    // 3. 核心辅助函数
    // ====================================================================
    function waitForElement(selector, callback, timeout) {
        const startTime = Date.now();
        const interval = setInterval(() => {
            const element = document.querySelector(selector);
            if (element) {
                updateStatus(`关键元素 "${selector}" 已找到!`, 'success');
                clearInterval(interval);
                callback();
            } else if (Date.now() - startTime > timeout) {
                clearInterval(interval);
                updateStatus(`等待超时: 未找到 "${selector}"。请刷新或检查页面。`, 'error');
            }
        }, 500);
    }

    // ====================================================================
    // 4. 页面路由主逻辑
    // ====================================================================
    function mainLogic() {
        const currentPageUrl = window.location.href;
        if (currentPageUrl.includes(courseListPageUrl)) {
            updateStatus('当前在课程列表页,准备扫描...');
            waitForElement('.class-table tbody', handleCourseListPage, MAX_WAIT_SECONDS * 1000);
        } else if (currentPageUrl.includes(videoPageUrlIdentifier)) {
            updateStatus('当前在视频播放页,加载助手中...');
            handleVideoPage();
        } else {
            updateStatus('当前页面非目标,脚本待命中...', 'info');
        }
    }

    // ====================================================================
    // 5. 课程列表页逻辑
    // ====================================================================
    function handleCourseListPage() {
        const tableBody = document.querySelector('.class-table tbody');
        if (!tableBody) {
            updateStatus('课程列表tbody未找到!', 'error');
            return;
        }

        const observer = new MutationObserver(() => {
            clearTimeout(observer.debounce);
            observer.debounce = setTimeout(processCurrentPage, 500);
        });
        observer.observe(tableBody, { childList: true });
        processCurrentPage();
    }

    async function processCurrentPage() {
        updateStatus('正在扫描当前页课程...', 'info');
        const courseRows = document.querySelectorAll('.class-table tr[ng-repeat="item in model.notPass.list"]');
        let foundIncomplete = false;

        for (const row of courseRows) {
            const allSpans = row.querySelectorAll('.info .mt5 span');
            const progressSpan = Array.from(allSpans).find(span => span.innerText.includes('课程进度:'));
            const progressLabel = progressSpan ? progressSpan.querySelector('label') : null;

            if (!progressLabel || progressLabel.innerText.trim() === '100%') continue;

            const courseName = row.querySelector('.info .tit a').title;
            const nextCourseButton = row.querySelector('a[ng-click^="events.checkHasPhoto"]');

            if (nextCourseButton) {
                foundIncomplete = true;
                updateStatus(`找到目标: [${progressLabel.innerText}] ${courseName},3秒后跳转...`, 'info');
                setTimeout(() => {
                    updateStatus('正在执行跳转...', 'info');
                    const clickEvent = new MouseEvent('click', {
                        bubbles: true,
                        cancelable: true,
                        view: unsafeWindow
                    });
                    nextCourseButton.dispatchEvent(clickEvent);
                }, 3000);
                return;
            }
        }

        if (!foundIncomplete) {
            updateStatus('本页课程均已完成,检查翻页...', 'info');
            const paginationContainer = document.querySelector('.m-pages .pages-num');
            if (!paginationContainer) {
                 updateStatus('🎉 所有课程均已完成!任务结束。', 'success');
                 alert("所有课程进度均为100%,任务完成!");
                 return;
            }
            const currentPageButton = paginationContainer.querySelector('a.current');
            const nextPageButton = currentPageButton ? currentPageButton.nextElementSibling : null;
            if (nextPageButton && nextPageButton.tagName === 'A') {
                updateStatus(`准备翻页到第 ${nextPageButton.innerText} 页...`, 'info');
                setTimeout(() => nextPageButton.click(), 3000);
            } else {
                updateStatus('🎉 所有课程均已完成!任务结束。', 'success');
                alert("所有课程进度均为100%,任务完成!");
            }
        }
    }


    // ====================================================================
    // 6. 视频播放页逻辑
    // ====================================================================
    function handleVideoPage() {
        addPanelStyles();
        createControlPanel();
        makePanelDraggable();
        addEventListenersToPanel();
        findAndSetupVideoRobustly();
        const quizObserver = new MutationObserver(() => setTimeout(solveMathQuiz, 300));
        quizObserver.observe(document.body, { childList: true, subtree: true });
        updateStatus('视频页助手加载完毕,监视中...', 'info');
    }

    function findAndSetupVideoRobustly() {
        const existingVideo = document.querySelector('video');
        if (existingVideo) {
            handleVideoFound(existingVideo);
            return;
        }
        videoObserver = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                if (mutation.addedNodes.length) {
                    for (const node of mutation.addedNodes) {
                        if (node.tagName === 'VIDEO') {
                            handleVideoFound(node);
                            return;
                        }
                        if (typeof node.querySelector === 'function') {
                            const video = node.querySelector('video');
                            if (video) {
                                handleVideoFound(video);
                                return;
                            }
                        }
                    }
                }
            }
        });
        videoObserver.observe(document.documentElement, { childList: true, subtree: true });
    }

    function handleVideoFound(video) {
        if (videoElement) return; // 防止重复初始化
        videoElement = video;
        updateStatus('视频元素已锁定,准备播放...', 'info');

        if (videoObserver) {
            videoObserver.disconnect();
            videoObserver = null;
        }

        setPlaybackRate(config.lastPlaybackRate);

        videoElement.addEventListener('ended', () => {
            updateStatus('✅ 视频播放结束!3秒后返回...', 'success');
            setTimeout(() => {
                window.location.href = `https://zdkj.v.zzu.edu.cn/center/myStudy/goods/detail?year=${targetYear}`;
            }, 3000);
        });

        videoElement.muted = true;
        videoElement.addEventListener('canplay', () => {
            const playPromise = videoElement.play();
            if (playPromise !== undefined) {
                playPromise.catch(error => {
                    updateStatus('自动播放失败,请手动点击播放', 'error');
                });
            }
        }, { once: true });
    }

    // ====================================================================
    // 7. 视频页核心功能
    // ====================================================================
    function solveMathQuiz() {
        if (!config.isAutoAnswerEnabled) return;
        const quizContainer = document.querySelector(quizSelectors.quizContainer);
        if (!quizContainer) return;

        const questionElement = quizContainer.querySelector(quizSelectors.questionText);
        const submitButton = quizContainer.querySelector(quizSelectors.submitButton);
        if (!questionElement || !submitButton || submitButton.disabled) return;

        updateStatus('检测到题目,正在自动作答...', 'warning');

        const questionText = questionElement.textContent.trim();
        const match = questionText.replace(/x|X|×/g, '*').replace(/÷/g, '/').match(/(\d+)\s*([+\-*/])\s*(\d+)/);
        if (match) {
            const num1 = parseFloat(match[1]), operator = match[2], num2 = parseFloat(match[3]);
            let result;
            switch (operator) {
                case '+': result = num1 + num2; break;
                case '-': result = num1 - num2; break;
                case '*': result = num1 * num2; break;
                case '/': result = num1 / num2; break;
                default: return;
            }

            const optionElements = quizContainer.querySelectorAll(quizSelectors.options);
            let answerFound = false;
            for (const option of optionElements) {
                const optionValueText = option.querySelector(quizSelectors.optionValueText).textContent.trim();
                const optionValue = parseFloat(optionValueText);
                if (optionValue === result) {
                    option.querySelector(quizSelectors.optionRadioButton).click();
                    answerFound = true;
                    break;
                }
            }
            if (answerFound) {
                setTimeout(() => {
                    submitButton.click();
                    updateStatus(`▶️ 正在以 ${videoElement.playbackRate}x 速度播放...`, 'info');
                }, 500 + Math.random() * 500);
            }
        }
    }

    function setPlaybackRate(rate) {
        if (!videoElement) return;
        const newRate = parseFloat(rate);
        if (!isNaN(newRate) && newRate > 0) {
            videoElement.playbackRate = newRate;
            config.lastPlaybackRate = newRate;
            GM_setValue('lastPlaybackRate', newRate);
            updateStatus(`▶️ 正在以 ${newRate}x 速度播放...`, 'info');
            const speedInput = document.getElementById('kh-speed-input');
            if (speedInput) speedInput.value = newRate;
        }
    }

    // ====================================================================
    // 8. UI 及其他辅助函数
    // ====================================================================
    function jumpToTime(timeStr) {
        if (videoElement) {
            const parts = timeStr.split(':').map(part => parseInt(part, 10)).reverse();
            let seconds = 0;
            if (parts.length > 0) seconds += parts[0];
            if (parts.length > 1) seconds += parts[1] * 60;
            if (parts.length > 2) seconds += parts[2] * 3600;
            if (!isNaN(seconds)) {
                videoElement.currentTime = seconds;
            }
        }
    }

    function createControlPanel() {
        const panelHTML = `
            <div id="course-helper-panel">
                <div id="kh-header">全能网课助手v3.7 (可拖动)</div>
                <div id="kh-content">
                    <div class="kh-section">
                        <label>播放速度:</label>
                        <input type="number" id="kh-speed-input" value="${config.lastPlaybackRate}" min="0.1" step="0.1">
                        <button id="kh-set-speed-btn">设置</button>
                    </div>
                    <div class="kh-section kh-presets">
                        <button data-speed="1">1.0x</button>
                        <button data-speed="1.5">1.5x</button>
                        <button data-speed="2">2.0x</button>
                        <button data-speed="4">4.0x</button>
                        <button data-speed="8">8.0x</button>
                    </div>
                    <div class="kh-section">
                        <label>时间跳转:</label>
                        <input type="text" id="kh-time-input" placeholder="分:秒 或 时:分:秒">
                        <button id="kh-jump-time-btn">跳转</button>
                    </div>
                    <div class="kh-section kh-toggle">
                        <label for="kh-auto-answer-toggle">自动答题</label>
                        <label class="kh-switch">
                            <input type="checkbox" id="kh-auto-answer-toggle" ${config.isAutoAnswerEnabled ? 'checked' : ''}>
                            <span class="kh-slider"></span>
                        </label>
                    </div>
                </div>
            </div>
        `;
        document.body.insertAdjacentHTML('beforeend', panelHTML);
    }

    function addPanelStyles() {
        GM_addStyle(`
            #course-helper-panel {
                position: fixed;
                top: 100px;
                left: 20px;
                z-index: 99999;
                background: #f1f1f1;
                border: 1px solid #ccc;
                border-radius: 8px;
                box-shadow: 0 4px 8px rgba(0,0,0,0.2);
                font-family: sans-serif;
                width: 280px;
            }
            #kh-header {
                padding: 10px;
                background: #3498db;
                color: white;
                font-weight: bold;
                cursor: move;
                border-radius: 8px 8px 0 0;
                text-align: center;
            }
            #kh-content {
                padding: 15px;
                display: flex;
                flex-direction: column;
                gap: 15px;
            }
            .kh-section {
                display: flex;
                align-items: center;
                gap: 8px;
            }
            .kh-section label {
                flex-shrink: 0;
                font-size: 14px;
            }
            .kh-section input[type="number"],
            .kh-section input[type="text"] {
                width: 100%;
                padding: 5px;
                border: 1px solid #ccc;
                border-radius: 4px;
            }
            .kh-section button {
                padding: 5px 10px;
                border: none;
                background: #3498db;
                color: white;
                border-radius: 4px;
                cursor: pointer;
                transition: background 0.2s;
            }
            .kh-section button:hover {
                background: #2980b9;
            }
            .kh-presets button {
                flex-grow: 1;
                background: #95a5a6;
            }
            .kh-presets button:hover {
                background: #7f8c8d;
            }
            .kh-toggle {
                justify-content: space-between;
            }
            .kh-switch {
                position: relative;
                display: inline-block;
                width: 50px;
                height: 24px;
            }
            .kh-switch input {
                opacity: 0;
                width: 0;
                height: 0;
            }
            .kh-slider {
                position: absolute;
                cursor: pointer;
                top: 0;
                left: 0;
                right: 0;
                bottom: 0;
                background-color: #ccc;
                transition: .4s;
                border-radius: 24px;
            }
            .kh-slider:before {
                position: absolute;
                content: "";
                height: 16px;
                width: 16px;
                left: 4px;
                bottom: 4px;
                background-color: white;
                transition: .4s;
                border-radius: 50%;
            }
            input:checked + .kh-slider {
                background-color: #2196F3;
            }
            input:checked + .kh-slider:before {
                transform: translateX(26px);
            }
        `);
    }

    function makePanelDraggable() {
        const panel = document.getElementById('course-helper-panel');
        const header = document.getElementById('kh-header');
        let isDragging = false, offsetX, offsetY;
        header.addEventListener('mousedown', (e) => {
            isDragging = true;
            offsetX = e.clientX - panel.offsetLeft;
            offsetY = e.clientY - panel.offsetTop;
            document.body.style.userSelect = 'none';
        });
        document.addEventListener('mousemove', (e) => {
            if (isDragging) {
                panel.style.left = `${e.clientX - offsetX}px`;
                panel.style.top = `${e.clientY - offsetY}px`;
            }
        });
        document.addEventListener('mouseup', () => {
            isDragging = false;
            document.body.style.userSelect = 'auto';
        });
    }

    function addEventListenersToPanel() {
        document.getElementById('kh-set-speed-btn').addEventListener('click', () => setPlaybackRate(document.getElementById('kh-speed-input').value));
        document.querySelector('.kh-presets').addEventListener('click', (e) => {
            if (e.target.tagName === 'BUTTON') setPlaybackRate(e.target.dataset.speed);
        });
        document.getElementById('kh-jump-time-btn').addEventListener('click', () => jumpToTime(document.getElementById('kh-time-input').value));
        document.getElementById('kh-time-input').addEventListener('keydown', (e) => {
            if (e.key === 'Enter') jumpToTime(e.target.value);
        });
        document.getElementById('kh-auto-answer-toggle').addEventListener('change', (e) => {
            config.isAutoAnswerEnabled = e.target.checked;
            GM_setValue('autoAnswerEnabled', config.isAutoAnswerEnabled);
            updateStatus(`自动答题已${config.isAutoAnswerEnabled ? '开启' : '关闭'}`, 'info');
        });
    }


    // ====================================================================
    // 9. 脚本启动入口
    // ====================================================================
    (function initialize() {
        addStatusPanelStyles();
        createStatusPanel();
        setTimeout(mainLogic, 500); // 稍作延迟以确保body完全加载
    })();

})();