// ==UserScript==
// @name chd刷课(chd)
// @namespace http://tampermonkey.net/
// @version 423
// @description 解决自动播放被阻止问题,确保视频持续播放,60分钟自动刷新,拦截弹窗,自动完成所有课程。
// @author chd
// @match *.hnsydwpx.cn/*
// @grant GM_addStyle
// @grant GM_log
// @grant GM_notification
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @run-at document-idle
// @connect 192.168.1.107
// @license MIT
// ==/UserScript==
(function () {
"use strict";
// 配置参数
const config = {
checkInterval: 10000,
interactionWait: 3000,
maxRetry: 300,
debugMode: true,
countdownDuration: 60 * 60, // 60分钟(秒数)
dataAttribute1: "li[data-v-290b612e]",
dataAttribute2: "div[data-v-6a18900e]",
};
// 添加UI指示器
GM_addStyle(`
.script-indicator {
position: fixed;
top: 20px;
right: 1400px;
background: linear-gradient(135deg, rgba(0,0,0,0.8), #B8860B);
color: white;
padding: 15px 22px;
border-radius: 10px;
box-shadow: 0 2px 5px rgba(0,0,0,0.4);
z-index: 9999;
font-size: 25px;
}
.script-indicator.error {
background: #F44336;
}
.pro-label {
position: absolute;
bottom: -10px;
right: 10px;
background: rgba(0, 0, 0, 0.5);
color: #FFD700;
padding: 5px 10px;
border-radius: 5px;
font-size: 14px;
font-weight: bold;
opacity: 0.8;
}
.countdown-display {
font-size: 14px;
margin-top: 5px;
opacity: 0.8;
}
`);
const indicator = document.createElement("div");
indicator.className = "script-indicator";
indicator.innerHTML =
'刷课脚本已经启动啦<div class="pro-label">Pro</div> <div class="countdown-display"></div>';
document.body.appendChild(indicator);
// 倒计时管理器
class CountdownManager {
constructor() {
this.timer = null;
this.startTime = null;
this.remaining = GM_getValue(
"countdownRemaining",
config.countdownDuration
);
this.init();
}
init() {
logDebug("CountdownManager: 初始化开始");
this.updateDisplay();
if (!GM_getValue("countdownRunning", false)) {
GM_setValue("countdownRunning", true);
this.startTime = Date.now();
this.start();
} else {
const elapsed = Math.floor(
(Date.now() - GM_getValue("countdownStartTime")) / 1000
);
this.remaining = Math.max(config.countdownDuration - elapsed, 0);
this.start();
}
logDebug("CountdownManager: 初始化完成");
}
start() {
logDebug("CountdownManager: 开始倒计时");
GM_setValue("countdownStartTime", Date.now());
this.timer = setInterval(() => {
this.remaining--;
GM_setValue("countdownRemaining", this.remaining);
if (this.remaining <= 0) {
this.handleTimeout();
return;
}
this.updateDisplay();
}, 1000);
}
updateDisplay() {
const minutes = Math.floor(this.remaining / 60);
const seconds = this.remaining % 60;
indicator.querySelector(
".countdown-display"
).textContent = `下次刷新: ${minutes
.toString()
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
}
handleTimeout() {
logDebug("CountdownManager: 倒计时结束,处理超时");
clearInterval(this.timer);
GM_setValue("countdownRunning", false);
GM_setValue("countdownRemaining", config.countdownDuration);
if (1) {
logDebug("CountdownManager: 60分钟倒计时结束,返回课程中心");
window.location.href = "https://www.hnsydwpx.cn/mineCourse";
} else {
// 如果在课程中心页面则重新开始倒计时
this.remaining = config.countdownDuration;
this.init();
}
}
notify(message) {
if (config.debugMode) {
GM_notification({
title: `[倒计时通知]`,
text: message,
timeout: 5000,
});
}
}
}
// 解决自动播放问题的视频控制器
class VideoController {
constructor() {
this.player = null;
this.retryCount = 0;
this.isWaitingInteraction = false;
logDebug("VideoController: 初始化开始");
this.init();
}
async init() {
try {
this.player = await this.waitForElement("#coursePlayer video");
this.addFakeInteractionLayer();
this.startMonitoring();
logDebug("VideoController: 初始化完成");
} catch (error) {
logDebug(`VideoController: 初始化失败: ${error.message}`);
indicator.classList.add("error");
indicator.textContent = "脚本初始化失败";
}
}
// 添加伪交互层解决自动播放限制
addFakeInteractionLayer() {
logDebug("VideoController: 添加伪交互层");
GM_addStyle(`
.interaction-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: transparent;
z-index: 9998;
cursor: pointer;
}
.interaction-notice {
position: fixed;
bottom: 80px;
right: 20px;
background: rgba(0,0,0,0.7);
color: white;
padding: 10px 15px;
border-radius: 4px;
z-index: 9999;
max-width: 300px;
font-size: 14px;
text-align: center;
box-shadow: 0 2px 10px rgba(0,0,0,0.5);
}
`);
// 创建覆盖层
const overlay = document.createElement("div");
overlay.className = "interaction-overlay";
overlay.onclick = () => this.handleUserInteraction();
document.body.appendChild(overlay);
// 添加提示
const notice = document.createElement("div");
notice.className = "interaction-notice";
notice.innerHTML =
"点击页面任意位置激活自动播放功能<br><small>3秒后自动尝试播放</small>";
document.body.appendChild(notice);
this.isWaitingInteraction = true;
setTimeout(() => {
if (this.isWaitingInteraction) {
this.handleUserInteraction();
notice.innerHTML = "已自动激活播放功能";
setTimeout(() => notice.remove(), 2000);
}
}, config.interactionWait);
}
// 处理用户交互
handleUserInteraction() {
if (!this.isWaitingInteraction) return;
logDebug("VideoController: 处理用户交互");
this.isWaitingInteraction = false;
document.querySelector(".interaction-overlay")?.remove();
document.querySelector(".interaction-notice")?.remove();
// 首次播放需要用户触发
this.playVideo()
.then(() => {
logDebug(`VideoController: 用户交互后自动播放已启动`);
})
.catch((error) => {
logDebug(`VideoController: 交互后播放失败: ${error}`);
this.notify(`交互后播放失败: ${error}`, "error");
});
}
// 新增检查覆盖层的方法
intervalCheck() {
removeSpecificOverlays();
this.checkPlayingProgress();
}
// 开始监控
startMonitoring() {
logDebug("VideoController: 开始监控视频播放状态");
this.monitorInterval = setInterval(() => {
if (
!this.isWaitingInteraction &&
this.player.paused &&
!this.player.ended
) {
logDebug("VideoController: 监控到视频播放状态异常");
this.playVideo();
logDebug("VideoController: 恢复视频播放");
}
// 检查覆盖层
this.intervalCheck();
}, config.checkInterval);
// 监听视频事件
this.player.addEventListener("pause", () => {
if (!this.isWaitingInteraction) {
logDebug("VideoController: 检测到视频等待交互,尝试移除交互层");
this.playVideo();
removeSpecificOverlays();
}
});
this.player.addEventListener("ended", () => {
logDebug("VideoController: 当前视频播放完毕");
this.nextChapter();
});
}
// 播放视频(处理自动播放限制)
async playVideo() {
if (this.retryCount >= config.maxRetry) {
logDebug("VideoController: 达到最大重试次数,停止尝试播放");
GM_notification({
title: "自动播放被阻止",
text: "请手动点击播放按钮",
timeout: 5000,
});
indicator.classList.add("error");
indicator.textContent = "自动播放被阻止";
return;
}
try {
const playPromise = this.player.play();
if (playPromise !== undefined) {
await playPromise;
this.retryCount = 0;
indicator.classList.remove("error");
indicator.innerHTML =
'刷课运行中……<div class="pro-label">Pro</div> <div class="countdown-display"></div>';
logDebug("VideoController: 视频播放成功");
}
} catch (error) {
this.retryCount++;
this.notify(
`播放失败 (${this.retryCount}/${config.maxRetry}): ${error}`,
"error"
);
logDebug(
`VideoController: 播放失败 (${this.retryCount}/${config.maxRetry}): ${error}`
);
// 尝试通过点击按钮播放
const playBtn = await this.waitForElement(
".xgplayer-play",
document,
1000
).catch(() => null);
if (playBtn) {
playBtn.click();
logDebug("VideoController: 已尝试点击播放按钮");
}
// 直接尝试静音播放
if (this.retryCount >= 1) {
this.player.muted = true;
this.player.play().catch((e) => {
logDebug(`VideoController: 静音播放也失败: ${e}`);
});
}
}
}
checkPlayingProgress() {
const items = document.querySelectorAll("li[data-v-290b612e]");
const ifChangeClass = false;
for (let item of items) {
if (item.className === "playlist_li_active") {
const progressElement = item.querySelector(".progress");
const progress = progressElement
? progressElement.textContent.trim()
: null;
if (typeof progress === "string" && progress.includes("%")) {
// 将百分数转换为数值
const progressValue = parseFloat(progress.replace("%", ""));
if (progressValue === 100) {
// item.click();
// 点击后等待视频加载并开始播放
// setTimeout(() => this.playVideo(), 10000);
console.log(`VideoController: 当前已满100%,切换章节`);
this.nextChapter();
}
}
return;
}
}
}
// 切换到下一章节
nextChapter() {
logDebug("VideoController: 当前视频播放完毕,尝试切换到下一章节");
const items = document.querySelectorAll(config.dataAttribute1);
for (let item of items) {
const progressElement = item.querySelector(".progress");
const progress = progressElement
? progressElement.textContent.trim()
: null;
logDebug(`章节进度文本内容: ${progress}`);
if (typeof progress === "string" && progress.includes("%")) {
// 将百分数转换为数值
const progressValue = parseFloat(progress.replace("%", ""));
logDebug(`当前章节进度数值: ${progressValue}`);
if (progressValue < 98) {
item.click();
// 点击后等待视频加载并开始播放
setTimeout(() => this.playVideo(), 10000);
logDebug(
`VideoController: 已切换到章节: ${
item.querySelector(".name").textContent
}`
);
return;
}
}
}
window.location.href = "https://www.hnsydwpx.cn/mineCourse";
logDebug("VideoController: 所有章节已完成,返回课程中心");
}
// 等待元素出现
waitForElement(selector, parent = document, timeout = 10000) {
logDebug(`VideoController: 开始等待元素 ${selector} 出现`);
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
const el = parent.querySelector(selector);
if (el) {
logDebug(`VideoController: 元素 ${selector} 已找到`);
resolve(el);
} else if (Date.now() - startTime < timeout) {
setTimeout(check, 500);
} else {
logDebug(`VideoController: 元素 ${selector} 未找到`);
reject(new Error(`元素未找到: ${selector}`));
}
};
check();
});
}
// 弹窗通知
notify(message, type = "info") {
if (config.debugMode) {
GM_notification({
title: `[视频控制]`,
text: message,
timeout: 5000,
});
}
}
}
// 查找包含“防作弊问答”的元素并获取其 Base64 编码
// 该函数用于在文档中通过 XPath 表达式查找包含“防作弊问答”文本的元素,
// 然后在其祖先元素中查找 img 元素,并获取其 Base64 编码形式的图片数据
function getBase64FromAntiCheatImage() {
logDebug(
"getBase64FromAntiCheatImage: 开始查找防作弊问答图片并获取 Base64 编码"
);
const xpath = "//span[contains(text(), '防作弊问答')]";
try {
// 使用 document.evaluate 方法根据给定的 XPath 表达式在文档中查找元素
// 这里指定返回第一个匹配的有序节点
const result = document.evaluate(
xpath,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
);
const antiCheatElement = result.singleNodeValue;
if (antiCheatElement) {
let parentElement = antiCheatElement.parentElement;
while (parentElement) {
const imgElement = parentElement.querySelector("img");
// 检查找到的 img 元素的 src 属性是否以"data:image"开头,
// 如果是,则表示是 Base64 编码的图片数据,提取并返回
if (imgElement && imgElement.src.startsWith("data:image")) {
logDebug(
"getBase64FromAntiCheatImage: 已获取到防作弊问答图片的 Base64 编码"
);
return imgElement.src.split(",")[1];
}
parentElement = parentElement.parentElement;
}
}
} catch (error) {
logDebug(
`getBase64FromAntiCheatImage: 在使用 XPath 查找元素时出错: ${error}`
);
}
logDebug("getBase64FromAntiCheatImage: 未找到防作弊问答图片的 Base64 编码");
return null;
}
// 构造发送到后端的请求数据
// 该函数接受一个 Base64 编码的图片数据,构造一个包含图片数据和 OCR 配置选项的对象
function buildRequestData(base64Image) {
logDebug("buildRequestData: 开始构造发送到后端的请求数据");
const options = {
lang: "eng",
oem: 3,
psm: 7,
};
try {
// 检查传入的 base64Image 是否为空,如果为空则抛出错误
if (!base64Image) {
throw new Error("传入的 base64Image 为空");
}
logDebug("buildRequestData: 请求数据构造完成");
return {
image: base64Image,
options: options,
};
} catch (error) {
logDebug(`buildRequestData: 构造请求数据时出错: ${error}`);
throw error;
}
}
// 发送请求到后端进行 OCR 识别
// 该函数接受构造好的请求数据,通过 GM_xmlhttpRequest 发送 POST 请求到后端服务器,
// 并根据响应结果进行处理,返回识别结果和算式计算结果的对象
function sendOCRRequest(requestData) {
logDebug("sendOCRRequest: 开始发送请求到后端进行 OCR 识别");
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "POST",
url: "http://192.168.1.107:3000/recognize",
headers: {
"Content-Type": "application/json",
},
data: JSON.stringify(requestData),
onload: function (response) {
if (response.status === 200) {
try {
// 将服务端返回的响应文本解析为 JSON 格式
const responseData = JSON.parse(response.responseText);
const recognitionResult = responseData.text;
const calculationResult = responseData.calculationResult;
logDebug("sendOCRRequest: 后端 OCR 识别请求成功,已获取结果");
resolve({ recognitionResult, calculationResult });
} catch (parseError) {
logDebug(`sendOCRRequest: 解析服务端响应时出错: ${parseError}`);
reject(new Error("解析服务端响应失败"));
}
} else {
logDebug(
`sendOCRRequest: 请求失败: ${response.statusText},状态码: ${response.status}`
);
reject(
new Error(
`请求失败: ${response.statusText},状态码: ${response.status}`
)
);
}
},
onerror: function (error) {
logDebug(`sendOCRRequest: 发送请求时出错: ${error}`);
reject(
new Error(`请求出错: ${error},可能是网络连接问题或服务器不可达`)
);
},
});
});
}
// 根据计算结果点击相应的答案
// 该函数接受一个计算结果,在页面中查找所有符合特定样式的 radio 标签,
// 如果标签的文本内容或其关联的 input 的值与计算结果匹配,则点击该 radio 标签的 input 元素
function clickCorrespondingAnswer(calculatedResult) {
logDebug("clickCorrespondingAnswer: 开始根据计算结果点击相应的答案");
try {
const radioLabels = document.querySelectorAll(
"label.el-radio.is-bordered.el-radio--large"
);
radioLabels.forEach((label) => {
const labelText = label.querySelector(".el-radio__label").textContent;
logDebug(`clickCorrespondingAnswer: 检查答案标签文本: ${labelText}`);
const inputValue = label.querySelector(
"input.el-radio__original"
).value;
logDebug(`clickCorrespondingAnswer: 检查答案输入值: ${inputValue}`);
if (
labelText === calculatedResult.toString() ||
inputValue === calculatedResult.toString()
) {
const inputElement = label.querySelector("input.el-radio__original");
inputElement.click();
logDebug(`clickCorrespondingAnswer: 已点击答案: ${labelText}`);
}
});
} catch (error) {
logDebug(`clickCorrespondingAnswer: 点击答案时出错: ${error}`);
}
}
function removeSpecificOverlays(videoController) {
// 获取所有匹配的元素,这里是 class 为".el-overlay"的元素
const overlays = document.querySelectorAll(".el-overlay");
// 遍历并删除每个元素
overlays.forEach((overlay) => {
// 检查元素的 display 属性是否不为 none,即存在覆盖层
if (overlay.style.display !== "none") {
logDebug("removeSpecificOverlays: 检测到覆盖层不存在display属性");
// 检查元素内部是否有特定的内容,这里检查 class 为".el-dialog__title"的元素的文本内容
const dialogTitle = overlay.querySelector(
".el-dialog__title, .el-message-box__title"
);
if (dialogTitle && dialogTitle.textContent === "防作弊问答") {
logDebug("removeSpecificOverlays: 检测到防作弊问答");
const base64Image = getBase64FromAntiCheatImage();
if (base64Image) {
const requestData = buildRequestData(base64Image);
sendOCRRequest(requestData)
.then(({ recognitionResult, calculationResult }) => {
logDebug(
`removeSpecificOverlays: 防作弊问答 OCR 识别结果: ${recognitionResult}`
);
if (calculationResult !== null) {
clickCorrespondingAnswer(calculationResult);
} else {
logDebug("removeSpecificOverlays: 未成功计算算式结果");
}
})
.catch((error) => {
logDebug(
`removeSpecificOverlays: 处理 OCR 请求结果时出错: ${error}`
);
});
} else {
logDebug("removeSpecificOverlays: 未找到“防作弊问答”相关区域");
}
} else {
try {
logDebug(
`removeSpecificOverlays: 非防作弊问答覆盖层": ${dialogTitle.textContent}`
);
overlay.remove();
logDebug("removeSpecificOverlays: 已移除非防作弊问答覆盖层");
} catch (error) {
logDebug(
`removeSpecificOverlays: 移除非防作弊问答覆盖层出错: ${error}`
);
}
}
// 检查 videoController 是否存在
if (videoController) {
videoController.playVideo();
// this.playVideo();
} else {
logDebug(
"removeSpecificOverlays: videoController 未定义,无法调用 playVideo 方法"
);
}
}
});
}
// 页面初始化
function check2425(items, index) {
logDebug("check2425: 开始检查课程章节");
items[index].click();
setTimeout(() => {
//先检查元素是否存在
const lists = document.querySelectorAll(
".el-tab-pane .el-row .list_title"
);
let button;
if (lists.length) {
if (index) logProcess("2024年章节未完成,程序优先学习2024年章节!");
for (let list of lists) {
let text;
if (
(text = list.querySelector(".el-progress__text span").innerText) ===
"100%"
)
continue;
logProcess(`第一个未学完的视频进度为:${text}`);
button = list.querySelector("button");
try {
button.click();
logProcess("已点击未学完视频的播放按钮");
break;
} catch (clickError) {
logDebug(`check2425: 点击操作失败: ${clickError}`);
}
}
} else {
logProcess("2024已经完成,学习2025年课程!");
if (!index) logProcess("全部完成啦!");
else check2425(items, 0);
}
new VideoController();
}, 2000);
}
function init() {
logDebug("init: 脚本初始化开始");
// 初始化倒计时
new CountdownManager();
// 只在对视频页面启用
if (
location.pathname.includes("/videoPlayback") ||
location.pathname.includes("/getcourseDetails")
) {
logProcess("当前页面为课程学习界面,启动视频控制");
new VideoController();
} else if (!location.pathname.includes("/mineCourse")) {
logProcess("当前页面不是课程中心,重定向到课程中心");
window.location.href = "https://www.hnsydwpx.cn/mineCourse";
} else if (location.pathname.includes("/mineCourse")) {
setTimeout(() => {
logProcess("进入课程中心页面,开始选择课程章节");
const items = document.querySelectorAll(
`.years ${config.dataAttribute2}`
);
check2425(items, 1);
}, 2000);
}
logDebug("init: 脚本初始化完成");
}
// 日志输出函数,区分调试和进程提示
function logDebug(message) {
if (config.debugMode) {
const now = new Date();
const timestamp = now.toLocaleTimeString([], {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
console.log(`[DEBUG] [${timestamp}] ${message}`);
}
}
function logProcess(message) {
const now = new Date();
const timestamp = now.toLocaleTimeString([], {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
console.log(`[PROCESS] [${timestamp}] ${message}`);
}
// 启动脚本
if (document.readyState === "complete") {
init();
} else {
window.addEventListener("load", init);
}
})();