江西职培在线网课助手

江西职培在线自动化助手

// ==UserScript==
// @name         江西职培在线网课助手
// @namespace    jiangxizhipeizaixian
// @version      1.1
// @description  江西职培在线自动化助手
// @author       Nanako660
// @match        https://jiangxi.zhipeizaixian.com/study/video*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=zhipeizaixian.com
// @require      https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
// @grant        GM_xmlhttpRequest
// @grant        GM_log
// @grant        GM_notification
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      furina.one
// @license      MIT
// ==/UserScript==
(function () {
    'use strict';

    // #region 全局配置
    // 自动播放视频
    var isAutoPlay = GM_getValue('isAutoPlay', true);
    // 自动下一节
    var isAutoNext = GM_getValue('isAutoNext', true);
    // 自动跳转最新课程
    var isAutoJump = GM_getValue('isAutoJump', true);
    // 自动过人机验证
    var isAutoVerify = GM_getValue('isAutoVerify', false);
    // 自动发送邮件通知
    var isEmailNotice = GM_getValue('isEmailNotice', true);
    // 主循环间隔
    var mainLoopInterval = GM_getValue('mainLoopInterval', 1000);
    // 调试模式
    var isDebug = GM_getValue('isDebug', false);
    // 邮件地址
    var localEmail = GM_getValue('localEmail', null);
    // #endregion

    // #region 常量
    // 人机验证弹窗class
    const POP_WINDOWS_CLASS = '.zhipei-modal-content';
    // 验证码弹窗特征class
    const CODE_CLASS = '.code_box___32BrH';
    // 人脸弹窗特征class
    const FACE_CLASS = '.video_box___2zomT';
    // 标题class
    const TITLE_CLASS = '.header_box_title___1INxv';
    // 视频控件class
    const VIDEO_ID = 'J_prismPlayer';
    // 目录class
    const CONTENTS_CLASS = '.content_box___1fOQp';
    // 目录子项class
    const CONTENTS_ITEM_CLASS = '.content_box_wrap___ZdoU3';
    // 目录子项标题class
    const CONTENTS_ITEM_TITLE_CLASS = '.units_title___1Js-7';
    // 目录子项时长class
    const CONTENTS_ITEM_DURATION_CLASS = '.time_box___1PlPI';
    // 目录子项链接class
    const CONTENTS_ITEM_LINK_CLASS = 'a.units_wrap_box___1ncip';
    // 目录子项完成状态class
    const CONTENTS_ITEM_STATUS_CLASS = '.anticon-check-circle';
    // 课程下一节按钮class
    const NEXT_BUTTON_CLASS = '.next_button___YGZWZ';
    // 邮箱发送API
    const EMAIL_API_URL = 'https://furina.one/api/email.php';
    // #endregion

    // #region 标志位
    var isPopWindows = false;

    const FinishType = {
        NOT_FINISH: 0,
        FINISHED: 1,
        UPPER_LIMIT: 2
    };
    var finishType = FinishType.NOT_FINISH;

    // 验证类型
    const VerifyType = {
        NONE: 0,
        CODE: 1,
        FACE: 2
    };
    var verifyType = VerifyType.NONE;
    // #endregion

    // #region 全局变量
    // 主循环
    var MainLoop;

    // 获取视频控件
    var checkVideoInterval;

    // 全局悬浮窗
    var shadowWindows;

    var updateDebugInfoInterval;
    // #endregion

    /**
    * 打印调试信息
    * @param {string} message - 要打印的消息
    */
    function print(message) {
        if (isDebug) {
            console.log(message);
        }
    }

    // 等待视频加载完成
    checkVideoInterval = setInterval(function () {
        if (getVideo()) {
            Main();
            clearInterval(checkVideoInterval);
        }
    }, 1000);

    function Main() {
        print("职培在线网课助手脚本开始运行...");

        if (isAutoJump) {
            autoJumpToLatestCourse();
        }

        // 创建信息悬浮窗
        createFloatingWindow();

        // 主循环
        mainLoop();
    }

    function mainLoop() {
        MainLoop = setInterval(function () {

            if (finishType === FinishType.FINISHED || finishType === FinishType.UPPER_LIMIT) {
                alert("当前课程已完成或者每日学习时长已达8小时上限,脚本停止执行...");
                clearInterval(MainLoop);
                return;
            }

            // 更新调试信息
            if (isDebug) {
                updateDebugInfo();
            }

            // 检测弹窗
            let popWindows = getPopWindows();
            if (isPopWindows) {
                print("等待人机验证...");
                if (!popWindows) {
                    isPopWindows = false;
                    print("人机验证完成,继续播放视频...");
                    window.location.reload();
                }
                return;
            }
            if (popWindows) {
                isPopWindows = true;
                checkPopWindowsType(popWindows);

                // 截取弹窗并发送邮件
                if (isEmailNotice) {
                    setTimeout(() => {
                        sendNoticeEmail(POP_WINDOWS_CLASS);
                    }, 3000);
                }
            }

            // 视频播放完毕,自动播放下一节
            if (isAutoNext) {
                let video = getVideo();
                if (video.currentTime >= video.duration) {
                    getNextButtonAndClick();
                    return;
                }
            }

            // 自动播放视频
            if (isAutoPlay && !isPopWindows) {
                let video = getVideo();
                if (video.paused) {
                    print("视频暂停,尝试继续播放...");
                    video.volume = 0;
                    video.play();
                }
            }
        }, mainLoopInterval);
    }

    function getPopWindows() {
        var popWindows = document.querySelector(POP_WINDOWS_CLASS)
        if (popWindows) {
            return popWindows;
        }
        return null;
    }

    function checkPopWindowsType(popWindows) {
        let code = popWindows.querySelector(CODE_CLASS);
        let face = popWindows.querySelector(FACE_CLASS);
        if (code) {
            verifyType = VerifyType.CODE;
            print("获取到人机验证弹窗,类型为验证码验证...");
            print(code.querySelector('img').src);
        } else if (face) {
            verifyType = VerifyType.FACE;
            print("获取到人机验证弹窗,类型为人脸验证...");
        } else {
            finishType = FinishType.UPPER_LIMIT;
            print("今日学习时长已达8小时上限...");
        }
    }

    function sendNoticeEmail(item = null) {
        let title;
        if (verifyType === VerifyType.CODE) {
            title = "验证码验证";
        } else if (verifyType === VerifyType.FACE) {
            title = "人脸验证";
        } else {
            print("无弹窗,不发送邮件通知。");
            return;
        }
        captureScreenshot(item).then(dataURL => {
            let subject = `江西职培在线网课助手${title}弹窗通知`;
            let lvideo = getVideo();

            let message = `
            <div style="font-family: Arial, sans-serif; color: #333; line-height: 1.6;">
                <h2 style="color: #4CAF50; border-bottom: 2px solid #4CAF50; padding-bottom: 5px;">
                    江西职培在线网课助手 - ${title}弹窗通知
                </h2>

                <p>课程信息:</p>

                <table style="width: 100%; border-collapse: collapse; margin-top: 15px;">
                    <tr>
                        <td style="padding: 8px; border: 1px solid #ddd; background-color: #f9f9f9; width: 30%;">
                            <strong>课程标题:</strong>
                        </td>
                        <td style="padding: 8px; border: 1px solid #ddd;">${getTitle()}</td>
                    </tr>
                    <tr>
                        <td style="padding: 8px; border: 1px solid #ddd; background-color: #f9f9f9;">
                            <strong>视频时长:</strong>
                        </td>
                        <td style="padding: 8px; border: 1px solid #ddd;">${lvideo.duration.toFixed(0)} 秒</td>
                    </tr>
                    <tr>
                        <td style="padding: 8px; border: 1px solid #ddd; background-color: #f9f9f9;">
                            <strong>已看时长:</strong>
                        </td>
                        <td style="padding: 8px; border: 1px solid #ddd;">${lvideo.currentTime.toFixed(0)} 秒</td>
                    </tr>
                </table>

                <p style="margin-top: 20px;">${title}弹窗截图:</p>
                <div style="text-align: center; margin: 20px 0;">
                    <img src="${dataURL}" alt="人${title}弹窗截图" style="max-width: 100%; border: 1px solid #ddd; padding: 5px; background-color: #f9f9f9;" />
                </div>

                <p>请尽快处理${title}弹窗,以免影响课程进度!</p>
                <p style="color: #777; font-size: 12px;">
                    此邮件由江西职培在线网课助手自动发送,如有反馈可直接回复邮件,请勿泄露个人信息。
                </p>
            </div>
            `;
            //print(dataURL);
            sendEmail(localEmail, subject, message);
        });
    }

    function getNextButtonAndClick() {
        var nextButton = document.querySelector(NEXT_BUTTON_CLASS);
        if (nextButton) {
            print("当前视频播放完毕,尝试自动播放下一节...");
            nextButton.click();
        }
    }

    function autoJumpToLatestCourse() {
        var checkCourse = setInterval(() => {
            print("检测当前课程是否完成...");
            var contents = getContents();
            if (contents) {
                checkCurrentCourceIsCompleted(getContentsData(contents), true);
                clearInterval(checkCourse);
            }
        }, 1000);
    }

    function getTitle() {
        var title = document.querySelector(TITLE_CLASS);
        if (title) {
            return title.innerText;
        }
        return null;
    }

    /**
    * 获取页面中的视频元素
    * @returns {HTMLVideoElement|null} 返回视频元素,如果未找到视频元素,则返回 null。
    */
    function getVideo() {
        const videoContainer = document.getElementById(VIDEO_ID);
        if (!videoContainer) return null;

        const video = videoContainer.querySelector('video');
        return video || null;
    }

    function createFloatingWindow() {
        var floatingWindow = document.createElement('div');
        shadowWindows = floatingWindow.attachShadow({ mode: 'open' });

        // 添加样式
        var style = document.createElement('style');
        style.textContent = `
        #contentContainer {
            max-height: 50vh;
            overflow-y: auto;
            #padding: 10px;
            #border: 1px solid #ccc;
        }
        #debugWindow {
            position: fixed;
            top: 80px;
            right: 10px;
            background-color: #f0f0f0;
            color: #333;
            padding: 20px;
            border-radius: 15px;
            box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.3);
            z-index: 9999;
            #cursor: move;
            width: 320px;
            font-family: Arial, sans-serif;
            user-select: none;
        }
        h4 {
            margin-top: 0;
            margin-bottom: 10px;
            font-size: 18px;
            font-weight: bold;
            color: #444;
        }
        h3 {
            margin-top: 0;
            margin-bottom: 10px;
            font-size: 16px;
            font-weight: bold;
            color: #444;
        }
        p {
            margin: 8px 0;
            font-size: 14px;
            line-height: 1.5;
        }
        .func {
            margin: 8px 0;
            font-size: 15px;
            font-weight: bold;
            line-height: 1.2;
            color: #66ccff;
        }
        .highlight {
            margin: 8px 0;
            font-size: 16px;
            font-weight: bold;
            line-height: 1.5;
            color: red;
        }
        input[type="email"] {
            width: calc(100% - 100px);
            padding: 5px;
            border: 1px solid #ccc;
            border-radius: 5px;
        }
        button {
            margin-top: 10px;
            margin-bottom: 10px;
            padding: 5px 10px;
            background-color: #007bff;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
        }
        button:hover {
            background-color: #0056b3;
        }
        label {
            display: flex;
            align-items: center;
            #margin-bottom: 10px;
            font-size: 14px;
        }
        label input[type="checkbox"] {
            margin-right: 10px;
        }
        #debugContent {
            display: none;  /* 默认隐藏调试信息内容 */
        }
        `;
        shadowWindows.appendChild(style);

        // 添加内容
        var content = document.createElement('div');
        content.id = 'debugWindow';
        content.innerHTML = `
        <div id="contentContainer">
            <h3 id='title'>职培在线网课助手</h4>
            <h4>功能列表:</h3>
            <label>
                <p class="func">🔇 静音后台自动播放</p>
                <input type="checkbox" id="autoPlay" /> 
            </label>
            <label>
                <p class="func">⏭️ 自动播放下一节</p>
                <input type="checkbox" id="autoNext" /> 
            </label>
            <label>
                <p class="func">🛫 自动跳转最新节</p>
                <input type="checkbox" id="autoJump" /> 
            </label>
            <label">
                <p class="func">
                    <del>🤖 自动过人机验证</del>
                    <input type="checkbox" id="autoVerify" disabled /> 
                </p>
            </label>
            <label>
                <p class="func">📧 邮件提醒人机验证</p>
                <input type="checkbox" id="emailNotice" /> 
            </label>
            <h4>使用说明:</h4>
            <p class="highlight">填写邮箱用于接收人机验证通知邮件</p1>
            <p class="highlight">推荐使用QQ邮箱,手机下载QQ邮箱App设置通知优先级为高,以免错过通知</p>
            <p class="highlight"></p>
            <h4>配置信息:</h4>
            <p>邮箱地址:</p>
            <div></div>
            <input type="email" id="emailInput" placeholder="输入邮箱地址" />
            <div></div>
            <button id="saveEmail">保存邮箱</button>
            <div></div>
            <label>
                DebugMode <input type="checkbox" id="debugMode" /> 
            </label>
            <div id="debugContent">
                <h4>调试信息</h4>
                <div id="debugInfo">初始化调试信息...</div>
                <h4>调试按钮</h4>
                <p class="highlight">调试用,没事别乱点</p>
                <button id="backMyClass">返回我的班级</button>
                <div></div>
                <button id="testButton1">测试按钮1</button>
            </div>
        </div>
        `;
        shadowWindows.appendChild(content);

        // 将浮动窗口添加到文档中
        document.body.appendChild(floatingWindow);

        // 获取页面元素
        let autoPlay = shadowWindows.querySelector('#autoPlay');
        let autoNext = shadowWindows.querySelector('#autoNext');
        let autoJump = shadowWindows.querySelector('#autoJump');
        let autoVerify = shadowWindows.querySelector('#autoVerify');
        let emailNotice = shadowWindows.querySelector('#emailNotice');
        let emailInput = shadowWindows.querySelector('#emailInput');
        let saveEmailButton = shadowWindows.querySelector('#saveEmail');
        let debugMode = shadowWindows.querySelector('#debugMode');
        let debugContent = shadowWindows.querySelector('#debugContent');
        let backMyClassButton = shadowWindows.querySelector('#backMyClass');
        let testButton1 = shadowWindows.querySelector('#testButton1');

        // 初始化状态
        autoPlay.checked = isAutoPlay;
        autoNext.checked = isAutoNext;
        autoJump.checked = isAutoJump;
        autoVerify.checked = isAutoVerify;  // 自动过人机验证功能暂时关闭
        emailNotice.checked = isEmailNotice;

        shadowWindows.querySelector('#debugMode').checked = isDebug;
        let savedEmail = localEmail;
        emailInput.value = savedEmail;
        debugContent.style.display = isDebug ? 'block' : 'none';

        // 监听复选框状态变化
        autoPlay.addEventListener('change', function () {
            isAutoPlay = this.checked;
            GM_setValue('isAutoPlay', isAutoPlay);
        });

        autoNext.addEventListener('change', function () {
            isAutoNext = this.checked;
            GM_setValue('isAutoNext', isAutoNext);
        });

        autoJump.addEventListener('change', function () {
            isAutoJump = this.checked;
            GM_setValue('isAutoJump', isAutoJump);
            if (this.checked) {
                autoJumpToLatestCourse();
            }
        });

        autoVerify.addEventListener('change', function () {
            isAutoVerify = this.checked;
            GM_setValue('isAutoVerify', isAutoVerify);
        });

        emailNotice.addEventListener('change', function () {
            isEmailNotice = this.checked;
            GM_setValue('isEmailNotice', isEmailNotice);
        });

        debugMode.addEventListener('change', function () {
            isDebug = this.checked;
            GM_setValue('isDebug', isDebug);
            shadowWindows.querySelector('#debugContent').style.display = isDebug ? 'block' : 'none';
        });

        saveEmailButton.addEventListener('click', function () {
            var email = emailInput.value.trim();
            if (email) {
                GM_setValue('localEmail', email);
                alert('邮箱地址已保存!');
            } else {
                alert('请输入有效的邮箱地址!');
            }
        });

        backMyClassButton.addEventListener('click', function () {
            window.location.href = 'https://admin-jiangxi.zhipeizaixian.com/train-center/mine/student/subPages/student/class';
        });

        testButton1.addEventListener('click', function () {
            //checkCurrentCourceIsCompleted(true);
            sendNoticeEmail();
        });
    }

    function makeDraggable(element) {
        var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        element.onmousedown = dragMouseDown;

        function dragMouseDown(e) {
            e = e || window.event;
            e.preventDefault();
            pos3 = e.clientX;
            pos4 = e.clientY;
            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;
        }

        function elementDrag(e) {
            e = e || window.event;
            e.preventDefault();
            pos1 = pos3 - e.clientX;
            pos2 = pos4 - e.clientY;
            pos3 = e.clientX;
            pos4 = e.clientY;

            // 计算新的位置
            var newTop = element.offsetTop - pos2;
            var newLeft = element.offsetLeft - pos1;

            // 限制拖动范围,避免拖出屏幕
            var minLeft = 0;
            var minTop = 0;
            var maxLeft = window.innerWidth - element.offsetWidth;
            var maxTop = window.innerHeight - element.offsetHeight;

            // 限制 left 和 top 的最小最大值
            newLeft = Math.max(minLeft, Math.min(newLeft, maxLeft));
            newTop = Math.max(minTop, Math.min(newTop, maxTop));

            // 设置窗口的新位置,并保持固定宽度
            element.style.top = newTop + "px";
            element.style.left = newLeft + "px";
            element.style.width = '300px';  // 强制宽度固定,避免拖动时缩小
        }

        function closeDragElement() {
            document.onmouseup = null;
            document.onmousemove = null;
        }
    }

    function updateDebugInfo() {
        var video = getVideo();
        if (video) {
            var debugInfo = shadowWindows.getElementById('debugInfo');
            var info = `
                <strong>视频标题:</strong> ${getTitle()} <br>
                <strong>视频时长:</strong> ${video.duration.toFixed(0)} 秒 <br>
                <strong>播放时间:</strong> ${video.currentTime.toFixed(0)} 秒 <br>
                <strong>播放倍速:</strong> ${video.playbackRate}x <br>
                <strong>播放状态:</strong> ${video.paused ? '暂停' : '播放中'} <br>
                <strong>音量:</strong> ${Math.round(video.volume * 100)} % <br>
            `;
            debugInfo.innerHTML = info;
        }
    }

    function getContents() {
        var contents = document.querySelector(CONTENTS_CLASS);
        return contents;
    }

    function getContentsData(contents) {
        var result = {};

        if (contents) {
            print("成功获取目录父级元素");

            // 获取所有子元素
            var childElements = contents.querySelectorAll(CONTENTS_ITEM_CLASS);
            if (childElements.length > 0) {
                print('开始解析目录子集元素...');
                var parsedData = []; // 存储解析后的数据

                childElements.forEach(function (childElement, index) {
                    // 解析每个子元素的信息
                    var title = childElement.querySelector(CONTENTS_ITEM_TITLE_CLASS)?.textContent || '未找到标题';
                    var time = childElement.querySelector(CONTENTS_ITEM_DURATION_CLASS)?.textContent || '未找到时长';
                    var link = childElement.querySelector(CONTENTS_ITEM_LINK_CLASS)?.href || '未找到链接';
                    var completed = childElement.querySelector(CONTENTS_ITEM_STATUS_CLASS) ? '已完成' : '未完成';

                    // 将解析的信息存储在对象中
                    var item = {
                        index: index + 1,
                        title: title,
                        time: time,
                        link: link,
                        completed: completed
                    };

                    // 将对象添加到数组中
                    parsedData.push(item);
                });

                // 将解析后的数据存储在结果对象中
                result.data = parsedData;
            } else {
                print("未找到目录子元素");
                result.data = [];
            }
        } else {
            print("未找到目录父级元素");
            result.data = [];
        }

        return result.data;
    }

    // 查找第一个未完成的课程
    function findFirstIncompleteCourse(contents) {
        for (let element of contents) {
            if (element.completed === '未完成') {
                return element;  // 返回第一个未完成的课程对象
            }
        }
        return null;  // 如果没有未完成的课程,返回 null
    }

    /**
    * 检查当前课程是否已完成
    * @param {Array} contents - 课程目录数组
    * @param {boolean} autoNext - 如果为 true,且课程已完成,则自动跳转到下一个未完成的课程
    * @returns {boolean} - 返回当前课程是否已完成
    */
    function checkCurrentCourceIsCompleted(contents, autoNext = false) {
        // 获取当前页面的标题
        let currentTitle = getTitle();

        // 查找与当前标题匹配的课程
        let currentCourse = contents.find(element => element.title === currentTitle);

        if (currentCourse) {
            print(`检查当前课程是否已完成:${currentCourse.title}:${currentCourse.completed}`);
            if (currentCourse.completed === '已完成' && autoNext) {
                let nextCourse = findFirstIncompleteCourse(contents);
                if (nextCourse) {
                    print(`跳转到未完成的课程:${nextCourse.title}`);
                    window.location.href = nextCourse.link;  // 跳转到第一个未完成课程的链接
                }
                else {
                    print("所有课程已完成!");
                    finishType = FinishType.FINISHED;
                }
            }
            return currentCourse.completed === '已完成';
        }

        return false;  // 如果未找到当前课程,返回 false
    }

    /**
     * 发送邮件
     * @param {string} recipient - 收件人邮箱地址
     * @param {string} subject - 邮件主题
     * @param {string} message - 邮件内容
     * @param {string} image - 图片路径、URL或base64编码,可选
     */
    function sendEmail(recipient, subject, message, image = null) {
        // 检查收件人邮箱地址是否为空
        if (!recipient) {
            print("邮件通知发送失败,邮箱地址不正确!");
            alert("邮件通知发送失败,邮箱地址不正确!");
            return;
        }

        // 如果图片存在
        if (image) {
            // 检查是否是base64编码
            if (image.startsWith('data:image/')) {
                // 图片是base64编码
                message += `<br><img src="${image}" alt="邮件图片" />`;
            } else if (image.startsWith('http://') || image.startsWith('https://')) {
                // 图片是URL
                message += `<br><img src="${image}" alt="邮件图片" />`;
            } else {
                print("图片路径无效!");
                return;
            }
        }

        // 邮件发送数据
        const emailData = {
            recipient: recipient,
            subject: subject || "默认主题", // 如果没有提供主题,使用默认主题
            message: message || "默认内容" // 如果没有提供内容,使用默认内容
        };

        // 将emailData转换为JSON字符串
        const jsonData = JSON.stringify(emailData);

        // 调用PHP接口的URL
        const apiUrl = EMAIL_API_URL;

        // 使用GM_xmlhttpRequest发送POST请求
        GM_xmlhttpRequest({
            method: "POST",
            url: apiUrl,
            data: jsonData,
            headers: {
                "Content-Type": "application/json"
            },
            onload: function (response) {
                if (response.status === 200) {
                    // 处理成功响应
                    print("邮件发送成功: " + response.responseText);
                } else {
                    // 处理失败响应
                    print("邮件通知发送失败: " + response.status + " - " + response.responseText);
                    alert("邮件通知发送失败: " + response.status + " - " + response.responseText);
                }
            },
            onerror: function (error) {
                // 处理错误
                print("邮件通知发送失败,请求错误: " + error);
                alert("邮件通知发送失败,请求错误: " + error);
            }
        });
    }

    /**
     * 截取网页内容并返回base64编码的图片
     * @param {string} [selector] - 要截取的元素选择器,若为空则截取整个网页
     * @returns {Promise<string>} - 返回base64编码的图片数据URL
     */
    function captureScreenshot(selector = null) {
        return new Promise((resolve, reject) => {
            let element = document.body; // 默认截取整个网页

            // 如果提供了选择器,尝试查找元素
            if (selector) {
                element = document.querySelector(selector);
                if (!element) {
                    print("指定的元素未找到!");
                    reject("指定的元素未找到!");
                    return;
                }
            }

            // 使用 html2canvas 捕获截图
            html2canvas(element).then(canvas => {
                // 将 canvas 转换为 base64 编码的图像
                const dataURL = canvas.toDataURL('image/png');
                resolve(dataURL);
            }).catch(error => {
                print("截图失败: " + error);
                reject(error);
            });
        });
    }

})();