// ==UserScript== // @name 🐮华夏教师研培网|自动连播答题助手 // @namespace local.huaxia.course-autopilot // @version 1.0.0 // @description 华夏教师研培网课程自动连播:网页最小化后持续播放,自动处理内置题目,课程结束后自动进入下一节。 // @author Course AutoPilot // @match http://study.huaxiajiaoshiyanpei.com/* // @match https://study.huaxiajiaoshiyanpei.com/* // @run-at document-start // @grant none // ==/UserScript== (() => { "use strict"; if (window.__courseAutoPilotV1) { return; } const state = { endedHandled: false, lastAdvanceAt: 0, lastQuestionKey: "", questionAttempts: 0, originalTitle: document.title || "" }; Object.defineProperty(window, "__courseAutoPilotV1", { value: state, configurable: false, enumerable: false, writable: false }); const NEXT_COURSE_TEXT = "\u4E0B\u4E00\u8BFE\u7A0B"; const CONFIRM_TEXT = "\u786E\u5B9A"; const CAPITAL_QUESTION_TEXT = "\u9996\u90FD"; const CAPITAL_ANSWER = "\u5317\u4EAC"; const LOGIN_EXPIRED_TEXT = "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55"; const QUESTION_TITLE_SUFFIX = " [\u6709\u9898\u5F85\u5904\u7406]"; const LOGIN_TITLE_SUFFIX = " [\u8BF7\u91CD\u65B0\u767B\u5F55]"; function forceVisibleState() { const descriptors = { hidden: () => false, visibilityState: () => "visible" }; for (const propertyName of Object.keys(descriptors)) { for (const target of [document, Document.prototype]) { try { Object.defineProperty(target, propertyName, { configurable: true, enumerable: true, get: descriptors[propertyName] }); } catch { // The other target still provides a useful fallback. } } } } function stopVisibilityEvents(event) { event.stopImmediatePropagation(); } forceVisibleState(); document.addEventListener("visibilitychange", stopVisibilityEvents, true); window.addEventListener("visibilitychange", stopVisibilityEvents, true); function textOf(element) { return (element?.textContent || "").replace(/\s+/g, " ").trim(); } function getNextCourseButton() { const direct = document.querySelector(".next_video .dflex:last-child"); if (direct) { return direct; } return [...document.querySelectorAll("button, div")].find((element) => { return textOf(element) === NEXT_COURSE_TEXT && element.closest(".next_video"); }) || null; } function getCurrentVideo() { return document.querySelector("#mse video") || document.querySelector("video"); } function getPlayer() { return document.querySelector(".xgplayer"); } function advanceCourse() { const now = Date.now(); if (state.endedHandled || now - state.lastAdvanceAt < 5000) { return; } const button = getNextCourseButton(); if (!button) { state.endedHandled = true; console.warn("[Course AutoPilot] Video ended, but no next-course control was found."); return; } state.endedHandled = true; state.lastAdvanceAt = now; console.info("[Course AutoPilot] Video ended; opening the next course."); window.setTimeout(() => button.click(), 500); } function solveQuestion(questionText) { const normalized = questionText .replace(/[?\uFF1F]/g, "") .replace(/\s+/g, ""); if (normalized.includes(CAPITAL_QUESTION_TEXT)) { return CAPITAL_ANSWER; } const arithmetic = questionText.match( /(-?\d+(?:\.\d+)?)\s*([+\-*\u00D7\u00F7/])\s*(-?\d+(?:\.\d+)?)/ ); if (!arithmetic) { return null; } const left = Number(arithmetic[1]); const operator = arithmetic[2]; const right = Number(arithmetic[3]); if (!Number.isFinite(left) || !Number.isFinite(right)) { return null; } switch (operator) { case "+": return left + right; case "-": return left - right; case "*": case "\u00D7": return left * right; case "/": case "\u00F7": return right === 0 ? null : left / right; default: return null; } } function setInputValue(input, value) { const valueSetter = Object.getOwnPropertyDescriptor( HTMLInputElement.prototype, "value" )?.set; if (valueSetter) { valueSetter.call(input, String(value)); } else { input.value = String(value); } input.dispatchEvent(new Event("input", { bubbles: true })); input.dispatchEvent(new Event("change", { bubbles: true })); } function findDialogButton(dialog) { return [...dialog.querySelectorAll("button")].find((button) => { return textOf(button) === CONFIRM_TEXT; }) || null; } function setTitleSuffix(suffix) { if (!state.originalTitle) { state.originalTitle = document.title || ""; } if (suffix) { if (!document.title.includes(suffix)) { document.title = state.originalTitle + suffix; } return; } if ( document.title.endsWith(QUESTION_TITLE_SUFFIX) || document.title.endsWith(LOGIN_TITLE_SUFFIX) ) { document.title = state.originalTitle; } } function handleLoginExpired() { const bodyText = textOf(document.body); if (bodyText.includes(LOGIN_EXPIRED_TEXT)) { setTitleSuffix(LOGIN_TITLE_SUFFIX); return true; } return false; } function handleBuiltInQuestion() { const title = document.querySelector(".exam_title"); if (!title) { state.lastQuestionKey = ""; state.questionAttempts = 0; return false; } const questionText = textOf(title); const key = `${questionText}|${state.questionAttempts}`; if (state.lastQuestionKey !== key) { state.lastQuestionKey = key; } const dialog = title.closest(".el-dialog") || title.closest("[role='dialog']") || document.querySelector(".el-dialog"); const input = dialog?.querySelector("input"); const answer = solveQuestion(questionText); if (!dialog || !input || answer === null) { setTitleSuffix(QUESTION_TITLE_SUFFIX); console.warn("[Course AutoPilot] Question requires manual review:", questionText); return true; } if (state.questionAttempts >= 3) { setTitleSuffix(QUESTION_TITLE_SUFFIX); return true; } state.questionAttempts += 1; setInputValue(input, answer); window.setTimeout(() => { const confirmButton = findDialogButton(dialog); if (confirmButton) { confirmButton.click(); console.info("[Course AutoPilot] Built-in question answered:", questionText); } }, 250); return true; } function tick() { forceVisibleState(); const video = getCurrentVideo(); const player = getPlayer(); const ended = Boolean( video?.ended || [...(player?.classList || [])].includes("xgplayer-ended") ); if (ended) { advanceCourse(); } else { state.endedHandled = false; } if (video) { video.controls = false; } const loginExpired = handleLoginExpired(); const questionVisible = handleBuiltInQuestion(); if (!loginExpired && !questionVisible) { setTitleSuffix(""); } } function start() { if (!document.documentElement) { window.setTimeout(start, 0); return; } new MutationObserver(tick).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["class"] }); window.setInterval(tick, 750); tick(); console.info("[Course AutoPilot] Loaded."); } start(); })();