云南省干部在线学习学院

点击播放课程后使用超级鹰打码平台通过验证, 并自动播放

// ==UserScript==
// @name         云南省干部在线学习学院
// @namespace    https://github.com/chiupam
// @version      1.5
// @description  点击播放课程后使用超级鹰打码平台通过验证, 并自动播放
// @author       chiupam
// @match        https://www.ynsgbzx.cn/index.aspx
// @match        https://www.ynsgbzx.cn//index.aspx
// @match        https://www.ynsgbzx.cn//login.aspx*
// @match        https://www.ynsgbzx.cn/userpage.aspx
// @match        https://www.ynsgbzx.cn//userpage.aspx
// @match        https://www.ynsgbzx.cn/play/play.aspx*
// @match        https://www.ynsgbzx.cn/play/right.html*
// @match        https://www.ynsgbzx.cn/play/Right1.aspx*
// @icon         https://www.ynsgbzx.cn/favicon.ico
// @grant        GM_xmlhttpRequest
// @license      GNU GPLv3
// ==/UserScript==
 
(async function() {
  'use strict';
  
  // 云南省干部在线学习学院登录账号密码
  const loginUserName = "";
  const loginPassWord = "";
 
  // 超级鹰打码设置,具体参考 https://www.chaojiying.com/api-5.html
  const codeUserName = "";
  const codePassWord = "";
  const codeSoftId = "";
 
  // 1: 支持非首页课程播放, 但占用更多内存 2: 占用更少内存, 但不支持非首页课程播放
  const studyType = 1; 
 
  // 动态插入日志容器到页面,支持彻底删除已有容器
  function createLogContainer(clearLogs = false) {
    const existingLogContainer = document.getElementById('log-container');
 
    // 如果容器已存在且clearLogs为true,则彻底移除该容器
    if (existingLogContainer && clearLogs) {
      existingLogContainer.remove(); // 删除日志容器
      return null; // 返回null以表明容器已删除
    } 
    
    // 如果容器不存在,则创建新容器
    if (!existingLogContainer) {
      const logContainer = document.createElement('div');
      logContainer.id = 'log-container';
      logContainer.style.padding = '10px';
      logContainer.style.backgroundColor = '#333';
      logContainer.style.color = '#fff';
      logContainer.style.border = '1px solid #ddd';
      logContainer.style.maxHeight = `${window.innerHeight / 3}px`;
      logContainer.style.maxWidth = '400px';
      logContainer.style.overflowY = 'auto';
      logContainer.style.position = 'fixed';
      logContainer.style.top = `${window.innerHeight / 10}px`;
      logContainer.style.left = '10px';
      logContainer.style.zIndex = '1000';
 
      document.body.appendChild(logContainer); // 将日志容器添加到页面中
      return logContainer;
    }
    
    return existingLogContainer; // 返回已存在的容器
  }
 
  // 添加日志到日志容器
  function logPage(logContainer, message) {
    console.log(message); // 同时在控制台输出日志
 
    const logEntry = document.createElement('div');
    logEntry.textContent = message; // 设置日志内容
    logEntry.style.whiteSpace = 'nowrap'; // 禁止换行
    logEntry.style.overflow = 'hidden'; // 超出部分隐藏
    logEntry.style.textOverflow = 'ellipsis'; // 超出部分显示省略号
    logContainer.appendChild(logEntry); // 将日志内容添加到日志容器中
    logContainer.scrollTop = logContainer.scrollHeight; // 自动滚动到容器底部
  }
 
  // 格式化日期
  function formatDate() {
    // 获取 UTC 时间的毫秒数,并加上 8 小时的毫秒数 (8 * 60 * 60 * 1000)
    const offsetMilliseconds = 8 * 60 * 60 * 1000;
    const beijingTime = new Date(new Date().getTime() + offsetMilliseconds);
 
    // 格式化为 YYYY-MM-DD HH:MM:SS
    return beijingTime.toISOString().replace('T', ' ').split('.')[0];
  }
 
  // 格式化播放时长
  function formatTime(seconds) {
    const hours = Math.floor(seconds / 3600); // 1小时 = 3600秒
    const minutes = Math.floor((seconds % 3600) / 60); // 1分钟 = 60秒
    const secs = Math.floor(seconds % 60); // 余下的秒数
 
    // 使用padStart确保两位数格式,不足两位的补0
    const formattedHours = String(hours).padStart(2, '0');
    const formattedMinutes = String(minutes).padStart(2, '0');
    const formattedSeconds = String(secs).padStart(2, '0');
 
    if (formattedHours === '00') { // 如果没有小时数,只显示分钟和秒数
      return `${formattedMinutes}:${formattedSeconds}`;
    } else { // 如果有小时数,显示小时、分钟和秒数
      return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`
    }
  }
 
  async function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
 
  function findElement(xpath) {
    return document.evaluate(
      xpath,
      document,
      null,
      XPathResult.FIRST_ORDERED_NODE_TYPE,
      null
    ).singleNodeValue;
  }
 
  // 生成随机浮点数数组
  function generateFloatNumbers(strLength) {
    const minValue = 0.6; // 生成随机浮点数的下限
    const maxValue = 0.9; // 生成随机浮点数的上限
    const targetMinSum = strLength * 0.5; // 生成随机浮点数的总和下限
    const targetMaxSum = strLength * 1.6; // 生成随机浮点数的总和上限
 
    let numbers; // 生成的随机浮点数数组
    let totalSum; // 随机浮点数的总和
 
    do {
      numbers = []; // 清空数组
      totalSum = 0; // 重置总和
 
      for (let i = 0; i < strLength; i++) {
        const randomFloat = Math.random() * (maxValue - minValue) + minValue; // 生成一个在 minValue 和 maxValue 之间的随机浮点数
        numbers.push(randomFloat); // 将生成的随机浮点数添加到数组中
        totalSum += randomFloat; // 计算随机浮点数的总和
      }
    } while (totalSum < targetMinSum || totalSum > targetMaxSum); // 如果随机浮点数的总和不在 targetMinSum 和 targetMaxSum 之间,则重新生成随机浮点数
 
    return numbers; // 返回生成的随机浮点数数组
  }
 
  // 输入字符串
  async function enterStrings(string, inputFieldXpath, submitButtonXpath = null) {
    const inputField = findElement(inputFieldXpath); // 获取输入框元素
 
    if (inputField) {
      await sleep(Math.random() * 800); // 随机等待 0.8 秒
      inputField.click(); // 点击输入框
      inputField.value = ''; // 清空输入框内容
      const sleepTimeList = generateFloatNumbers(string.length); // 生成随机浮点数数组
      for (let i = 0; i < string.length; i++) {
        await sleep(sleepTimeList[i] * 1000); // 根据随机浮点数数组中的值,等待相应的时间
        inputField.value += string[i]; // 输入字符
      };
 
      // 如果存在提交按钮元素, 则点击提交按钮
      if (submitButtonXpath) {
        const submitButton = findElement(submitButtonXpath); // 获取提交按钮元素
        if (submitButton) {
          await sleep(Math.random() * 800); // 随机等待 0.8 秒
          submitButton.click(); // 点击提交按钮
        }
      }
    }
  }
 
  // 超级鹰打码
  function uploadImageToServer(imgBase64) {
    return new Promise(function(resolve, reject) {
      GM_xmlhttpRequest({
        method: 'POST',
        url: 'https://upload.chaojiying.net/Upload/Processing.php',
        headers: {
          'Content-Type': 'application/json'
        },
        data: JSON.stringify({
          user: codeUserName, // 用户名
          pass: codePassWord, // 密码
          softid: codeSoftId, // 软件ID
          codetype: '4004', // 验证码类型
          file_base64: imgBase64, // 图片的base64编码
        }),
        onload: function(response) {
          const jsonResponse = JSON.parse(response.responseText);
          resolve(jsonResponse.pic_str);  // 请求成功时返回数据
        },
        onerror: function(error) {
          reject('请求出错: ' + error);
        }
      });
    });
  }
 
  function captureImageDataURL(imageElement) {
    const canvas = document.createElement('canvas'); // 创建一个canvas元素
    canvas.width = imageElement.width; // 设置canvas的宽度为图片的宽度
    canvas.height = imageElement.height; // 设置canvas的高度为图片的高度
    const ctx = canvas.getContext('2d'); // 获取canvas的2D绘图上下文
    ctx.drawImage(imageElement, 0, 0, canvas.width, canvas.height); // 将图片绘制到canvas上
    const imageDataURL = canvas.toDataURL('image/png'); // 将canvas内容转换为base64编码的PNG图片
    return imageDataURL.split(',')[1]; // 返回base64编码的图片数据
  }
 
  const allCredentialsFilled = loginUserName && loginPassWord && codeUserName && codePassWord && codeSoftId; // 检查所有凭据是否已填写
  const currentURL = window.location.href; // 获取当前页面的URL
  const channel = new BroadcastChannel('page-control-channel'); // 创建一个新的广播频道
  window.addEventListener('load', async function() {
 
    if (currentURL.includes("index.aspx") && allCredentialsFilled) {
      // 首页触发登录行为
      if (document.querySelector("#go")) {
        window.location.href = 'https://www.ynsgbzx.cn/userpage.aspx'
      } else {
        const logContainer = createLogContainer(); // 创建日志容器
        logPage(logContainer, `程序执行时间: ${formatDate()}`); // 记录日志
        const ImageCheck = findElement('//*[@id="ImageCheck"]'); // 获取验证码图片元素
        logPage(logContainer, `账号: ${loginUserName}`); // 记录日志
        await enterStrings(loginUserName, '//*[@id="LoginView1_Login1_UserName"]'); // 输入账号
        logPage(logContainer, `密码: ${loginPassWord}`); // 记录日志
        await enterStrings(loginPassWord, '//*[@id="LoginView1_Login1_Password"]'); // 输入密码
        const imageDataURL = captureImageDataURL(ImageCheck); // 获取验证码图片的base64编码
        const picStrValue = await uploadImageToServer(imageDataURL); // 上传验证码图片到服务器并获取识别结果
        logPage(logContainer, `验证码: ${picStrValue}`); // 记录日志
        await enterStrings(
          picStrValue, // 验证码字符串
          '//*[@id="LoginView1_Login1_txtValidate"]',
          '//*[@id="LoginView1_Login1_LoginButton"]'
        ); // 输入验证码并点击登录按钮
      }
 
    } else if (currentURL.includes('login.aspx') && allCredentialsFilled) {
      // 未登录状态下点击 "个人空间" 时触发
      const logContainer = createLogContainer(); // 创建日志容器
      logPage(logContainer, `程序执行时间: ${formatDate()}`); // 记录日志
      const ImageCheck = findElement('//*[@id="ImageCheck"]'); // 获取验证码图片元素
      logPage(logContainer, `账号: ${loginUserName}`); // 记录日志
      await enterStrings(loginUserName, '//*[@id="Login1_UserName"]'); // 输入账号
      logPage(logContainer, `密码: ${loginPassWord}`); // 记录日志
      await enterStrings(loginPassWord, '//*[@id="Login1_Password"]'); // 输入密码
      const imageDataURL = captureImageDataURL(ImageCheck); // 获取验证码图片的base64编码
      const picStrValue = await uploadImageToServer(imageDataURL); // 上传验证码图片到服务器并获取识别结果
      logPage(logContainer, `验证码: ${picStrValue}`); // 记录日志
      await enterStrings(picStrValue, '//*[@id="Login1_txtValidate"]', '//*[@id="Login1_LoginButton"]'); // 输入验证码并点击登录按钮
 
    } else if (currentURL.includes('userpage.aspx') && !currentURL.includes('login.aspx')) {
      channel.onmessage = (event) => event.data.action === 'refreshPage' && location.reload();
 
      // 登录状态下点击 "个人空间" 时触发
      const logContainer = createLogContainer(); // 创建日志容器
      logPage(logContainer, `程序执行时间: ${formatDate()}`); // 记录日志
      const markElement = document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li.hidden-sm > mark");
      const hoursText = markElement.textContent; // 获取学时文本内容
      if (hoursText === "10分") {
        return logPage(logContainer, `您今日获得学时已达上限10分`); // 如果已获得10分学时,则退出函数
      } else {
        const learnedHoursMatch = hoursText.match(/(\d+(\.\d+)?)/); // 匹配数字及小数部分
        logPage(logContainer, `您今日已获得学时 ${learnedHoursMatch[1]}/10 学时`); // 记录日志
      }
      
      const rows = document.querySelectorAll('tbody tr'); // 获取所有课程行
      for (var i = 0; i < rows.length; i++) { // 遍历所有课程行
        var courseName = rows[i].querySelector('input[type="hidden"]').value; // 获取课件名称
        var progress = rows[i].querySelector('.progress-bar').textContent.trim(); // 获取学习进度
        var playButton = rows[i].querySelector('a[href*="redirect.aspx"]'); // 获取播放按钮
        if (progress !== '100%') { // 如果学习进度不是100%
          logPage(logContainer, `课件名称: ${courseName}`); // 记录日志
          logPage(logContainer, `学习进度: ${progress}, 准备学习该课程`); // 记录日志
          for (let i = 5; i > 0; i--) {
            if (i % 2 !== 0) logPage(logContainer, `${i}秒后进入学习`); // 仅在奇数秒时记录日志
            await sleep(975); // 等待接近1秒
          }
          // 读取 sutdyType 的值判断使用哪种方式进入学习页面
          return studyType === 1 ? playButton.click() : window.location.href = playButton.getAttribute('href');
        }
      }
 
      if (studyType === 1) {
        logPage(logContainer, `本页课程已全部学完`); // 记录日志
        logPage(logContainer, `如果有其他页, 请手动翻页`); // 记录日志
      } else {
        logPage(logContainer, `第一页全部课件已经学完`); // 记录日志
        logPage(logContainer, `请在 "课件中心" 内新选课件`); // 记录日志
        logPage(logContainer, `暂无法学习非第一页的课件`); // 记录日志
      }
 
    } else if (currentURL.includes('play.aspx')) {
      // 重写alert函数,当弹出提示框时根据用户设置执行相应操作
      unsafeWindow.alert = () => { 
        if (studyType === 1) {
          channel.postMessage({ action: 'refreshPage' });
          return window.close(); // 关闭当前页面
        }
        window.location.href = "https://www.ynsgbzx.cn/userpage.aspx"; 
      };
 
      // 针对收到不同信号的处理
      channel.onmessage = async (event) => {
        if (event.data.action === 'closePlayPage') {
          window.close(); // 接收到关闭信号时关闭页面
        } else if (event.data.action === 'href') {
          window.location.href = "https://www.ynsgbzx.cn/userpage.aspx";
        }
      };
 
      const logContainer = createLogContainer(); // 创建日志容器
      logPage(logContainer, `程序执行时间: ${formatDate()}`); // 记录日志
 
      if (codeUserName && codePassWord && codeSoftId) {
        const ImageCheck = findElement('//*[@id="ImageCheck"]'); // 获取验证码图片元素
        const imageDataURL = captureImageDataURL(ImageCheck); // 获取验证码图片的base64编码
        const picStrValue = await uploadImageToServer(imageDataURL); // 上传验证码图片到服务器并获取识别结果
        logPage(logContainer, `验证码: ${picStrValue}`); // 记录日志
        await enterStrings(picStrValue, '//*[@id="validanswer"]', '//*[@id="btnvalidanswer"]'); // 输入验证码并点击验证按钮
        createLogContainer(true); // 删除日志容器
      } else {
        logPage(logContainer, `无法完成自动打码, 请选择进行以下操作`); // 记录日志
        logPage(logContainer, `1、在脚本中填写超级鹰打码的相关参数`); // 记录日志
        logPage(logContainer, `2、手动输入验证码并提交`); // 记录日志
      }
    } else if (currentURL.includes('right.html')) {
      // 移除无用标签页
      channel.onmessage = (event) => {
        if (event.data.action === 'remove') {
          document.querySelector("body > div.jumbotron.yx-page-jumbotron").remove();
          document.querySelector("body > div.main_box > div.play_kcnr").remove()
        }
      };
    } else if (currentURL.includes('Right1.aspx')) {
      // 进入真实播放页面时触发
      const logContainer = createLogContainer(); // 创建日志容器
      logPage(logContainer, `程序执行时间: ${formatDate()}`); // 记录日志
      var video; // 声明视频变量
      const intervalVideo = setInterval(() => {
        // 延迟获取视频元素,确保页面加载完成
        if (!video) {
          video = document.querySelector('video'); // 获取第一个视频元素
          if (!video) {
            logPage(logContainer, '未找到视频, 等待加载...');
            return; // 如果未找到视频元素,继续等待
          } else {
            clearInterval(intervalVideo); // 视频播放完毕, 停止循环
            logPage(logContainer, '找到视频, 开始监控学习进度');
            channel.postMessage({ action: 'remove' });
          }
        }
 
        video.muted = true; // 强制静音
        if (video.paused) {video.play();} // 播放视频
 
        // 轮询检查并设置视频播放进度
        const lnode = document.querySelector("#lnode");
        if (lnode) {
          const intervalProgress = setInterval(function() {
            let percentage; // 计算进度
            if (video.paused) {
              video.play(); // 播放视频
              return; // 如果视频暂停,则继续播放
            } else {
              clearInterval(intervalProgress); // 停止轮询
              const content = lnode.textContent; // 获取文本内容
              const startIndex = content.indexOf(':') + 1; // 找到冒号的位置
              const endIndex = content.indexOf('-', startIndex); // 从冒号后面开始获取字符串,并找到第一个 '-' 的位置
              const number = content.substring(startIndex, endIndex).trim(); // 提取数字
              percentage = (number * 60 / video.duration) * 100; // 计算进度
              const percentageToFixed = percentage.toFixed(2); // 将进度百分比转换为字符串并保留两位小数
              percentage > 1 ? percentage -= 1 : percentage = 0; // 如果进度大于1,则减去1,否则设为0
              video.currentTime = video.duration * (percentage / 100); // 设置视频播放位置
              logPage(logContainer, `已学习: ${formatTime(video.currentTime)}(${percentageToFixed}%)`); // 输出当前播放时间
            }
          });
        }
        
        // 进度监听器的实现
        const milestones = [0.24, 0.49, 0.74, 0.99]; // 定义进度百分比
        let milestoneIndex = 0; // 当前进度索引
        function handleTimeUpdate() {
          const progress = video.currentTime / video.duration;
          if (progress >= milestones[milestoneIndex]) {
            logPage(logContainer, `视频播放达到${milestones[milestoneIndex] * 100}%`);
            milestoneIndex++;
            if (milestoneIndex >= milestones.length) {
              video.removeEventListener('timeupdate', handleTimeUpdate);
            }
          }
        }
 
        // 事件监听器部分
        video.addEventListener('ended', () => {
          studyType === 1 // 判断使用何种方式进行重定向
            ? (channel.postMessage({ action: 'refreshPage' }), channel.postMessage({ action: 'closePlayPage' }))
            : channel.postMessage({ action: 'href' });
        }); // 事件监听器:视频播放结束时
        video.addEventListener('pause', () => {video.play()}); // 事件监听器:视频暂停时自动播放
        video.addEventListener('volumechange', () => {video.muted = true}); // 事件监听器:视频音量变化时强制静音
        video.addEventListener('timeupdate', handleTimeUpdate); // 事件监听器:已学习的进度
      }, 1000); // 每秒检查一次
    }
  });
 
})();