// ==UserScript==
// @name 智慧树共享课刷课助手(不干扰页面加载)
// @namespace zhihuishu-safe-load
// @version 11.0.0
// @description 修复页面加载异常,可拖动面板+精准识别课程+25分钟自动切课+自动答题
// @author 安全加载适配
// @match *://*.zhihuishu.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @connect tk.enncy.cn
// @run-at document-end // 关键修复:等页面DOM完全加载完再执行
// ==/UserScript==
(function () {
'use strict';
// ===================== 基础配置 =====================
const DEFAULT_CONFIG = {
watchTime: 25, // 单课时长(分钟)
playbackRate: 1.25, // 播放倍速
autoSwitch: true, // 自动切课
autoPlay: true, // 自动播放
autoClosePopup: true, // 自动关弹窗
autoAnswer: true, // 自动答题
reviewCompleted: true, // 复看已完成课程
useOnlineBankFirst: true, // 优先言溪题库
};
// 言溪题库配置
const YANXI_BANK_CONFIG = {
url: "https://tk.enncy.cn/query",
token: "0abc866e094c4714936e88a84ae3cb93",
};
// 全局变量
let state = {
currentCourseIndex: 0,
currentTime: 0,
isRunning: false,
totalCourses: 0,
};
let config = { ...DEFAULT_CONFIG };
let allCourseList = [];
let selectedCourseList = [];
let localQuestionBank = {};
let timer = null;
let popupTimer = null;
let answerTimer = null;
let panel = null;
let isDragging = false;
let dragOffset = { x: 0, y: 0 };
let pageLoaded = false; // 页面加载完成标记
// ===================== 【核心修复】等待页面完全加载 =====================
// 等智慧树页面加载完成,课程容器出现,再执行脚本
function waitPageLoad() {
return new Promise((resolve) => {
let checkCount = 0;
const maxCheck = 60; // 最多等30秒,避免无限等待
const checkTimer = setInterval(() => {
checkCount++;
// 检查页面是否加载完成:共享课容器出现,且不是加载中状态
const courseContainer = document.querySelector(".datalist, .course-list, .sharing-new-style");
const isLoading = document.querySelector(".loading, [class*='加载更多']") !== null;
if (courseContainer && !isLoading) {
clearInterval(checkTimer);
pageLoaded = true;
resolve(true);
} else if (checkCount >= maxCheck) {
clearInterval(checkTimer);
resolve(false);
}
}, 500);
});
}
// ===================== 存储相关 =====================
function loadAllData() {
try { config = { ...DEFAULT_CONFIG, ...JSON.parse(GM_getValue("zhs_safe_config")) } } catch (e) { }
try { state = { ...state, ...JSON.parse(GM_getValue("zhs_safe_state")) } } catch (e) { }
try { selectedCourseList = JSON.parse(GM_getValue("zhs_safe_selected")) || [] } catch (e) { }
try { localQuestionBank = JSON.parse(GM_getValue("zhs_safe_bank")) || {} } catch (e) { }
}
function saveAllData() {
GM_setValue("zhs_safe_config", JSON.stringify(config));
GM_setValue("zhs_safe_state", JSON.stringify(state));
GM_setValue("zhs_safe_selected", JSON.stringify(selectedCourseList));
GM_setValue("zhs_safe_bank", JSON.stringify(localQuestionBank));
}
// ===================== 【修复】课程识别逻辑,等页面加载完再执行 =====================
function getAllCourseCards() {
if (!pageLoaded) return [];
let courseList = [];
// 精准适配你的共享课DOM结构
const shareCourseSelectors = [
".datalist dl",
".sharing-new-style .datalist dl",
".course-list dl",
".shared-course-wrap dl",
".course-card-item",
".course-item",
];
for (const selector of shareCourseSelectors) {
const elements = document.querySelectorAll(selector);
elements.forEach((el, index) => {
// 提取课程标题,过滤无关内容
const titleEl = el.querySelector("h3, .course-title, .course-name, dt");
let title = "";
if (titleEl) {
title = titleEl.textContent.trim().replace(/\s+/g, " ");
} else {
const fullText = el.innerText.trim().replace(/\s+/g, " ");
title = fullText.split("在学:")[0].split("进度:")[0].split("·")[0].trim();
}
// 过滤无效内容
if (!title || title.length < 4 || /登录|注册|客服|帮助|加载|暂无/.test(title)) return;
if (courseList.some(item => item.title === title)) return;
courseList.push({
id: btoa(title + index),
index: index,
title: title,
element: el
});
});
}
allCourseList = courseList;
return courseList;
}
// ===================== 面板拖动功能 =====================
function initDrag() {
const header = panel.querySelector(".panel-header");
if (!header) return;
header.addEventListener("mousedown", (e) => {
isDragging = true;
const rect = panel.getBoundingClientRect();
dragOffset.x = e.clientX - rect.left;
dragOffset.y = e.clientY - rect.top;
e.preventDefault();
});
document.addEventListener("mousemove", (e) => {
if (!isDragging) return;
let newLeft = e.clientX - dragOffset.x;
let newTop = e.clientY - dragOffset.y;
// 边界限制
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const panelWidth = panel.offsetWidth;
const panelHeight = panel.offsetHeight;
newLeft = Math.max(0, Math.min(newLeft, windowWidth - panelWidth));
newTop = Math.max(0, Math.min(newTop, windowHeight - panelHeight));
panel.style.left = newLeft + "px";
panel.style.top = newTop + "px";
panel.style.right = "unset";
});
document.addEventListener("mouseup", () => { isDragging = false; });
document.addEventListener("mouseleave", () => { isDragging = false; });
}
// ===================== 面板创建 =====================
function createPanel() {
const existingPanel = document.getElementById("zhs-safe-panel");
if (existingPanel) existingPanel.remove();
panel = document.createElement("div");
panel.id = "zhs-safe-panel";
panel.style.position = "fixed";
panel.style.top = "20px";
panel.style.right = "20px";
panel.style.zIndex = "999999";
panel.innerHTML = `
运行状态:
未运行
当前课程:
0/0
计时:
00:00 / ${config.watchTime}分钟
`;
document.body.appendChild(panel);
initDrag();
}
// ===================== 刷新课程列表 =====================
function refreshCourseList() {
const courseList = getAllCourseCards();
const listBox = document.getElementById("courseList");
if (!pageLoaded) {
listBox.innerHTML = `页面还在加载中,请稍等...
`;
return;
}
if (courseList.length === 0) {
listBox.innerHTML = `未找到课程,请确保在共享课列表页
`;
return;
}
let html = "";
courseList.forEach(course => {
const isChecked = selectedCourseList.some(item => item.id === course.id);
html += `
`;
});
listBox.innerHTML = html;
bindCourseCheckEvent();
state.totalCourses = selectedCourseList.length;
updateUI();
console.log(`[智慧树助手] 成功加载${courseList.length}门共享课`);
}
// 绑定课程勾选事件
function bindCourseCheckEvent() {
document.querySelectorAll("#courseList input[type=checkbox]").forEach(checkbox => {
checkbox.onchange = () => {
const courseId = checkbox.dataset.id;
const course = allCourseList.find(item => item.id === courseId);
if (!course) return;
if (checkbox.checked) {
if (!selectedCourseList.some(item => item.id === courseId)) {
selectedCourseList.push(course);
}
} else {
selectedCourseList = selectedCourseList.filter(item => item.id !== courseId);
}
saveAllData();
state.totalCourses = selectedCourseList.length;
updateUI();
};
});
}
// ===================== 刷课核心逻辑 =====================
function startWatch() {
if (selectedCourseList.length === 0) {
alert("请先勾选要刷的课程!");
return;
}
if (state.isRunning) return;
state.isRunning = true;
state.currentTime = 0;
state.currentCourseIndex = 0;
saveAllData();
updateUI();
openTargetCourse();
}
function stopWatch() {
state.isRunning = false;
saveAllData();
updateUI();
clearAllTimer();
}
function openTargetCourse() {
if (!state.isRunning) return;
if (state.currentCourseIndex >= selectedCourseList.length) {
alert("✅ 全部选中的课程已刷完!");
stopWatch();
return;
}
const targetCourse = selectedCourseList[state.currentCourseIndex];
if (!targetCourse || !targetCourse.element) {
stopWatch();
return;
}
targetCourse.element.click();
updateUI();
setTimeout(() => {
startTimer();
setupVideo();
}, 2500); // 延迟更久,等视频页面加载完成
}
function startTimer() {
clearInterval(timer);
timer = setInterval(() => {
if (!state.isRunning) return;
state.currentTime++;
saveAllData();
updateUI();
if (state.currentTime >= config.watchTime * 60) {
clearInterval(timer);
state.currentTime = 0;
state.currentCourseIndex++;
saveAllData();
backToCourseList();
setTimeout(() => {
openTargetCourse();
}, 3500);
}
}, 1000);
}
function backToCourseList() {
const backBtn = document.querySelector('a:has-text("返回学堂"), .back-btn, a[href*="onlinestuh5"], .header-left a');
if (backBtn && backBtn.offsetParent !== null) {
backBtn.click();
} else {
window.location.href = "https://onlineweb.zhihuishu.com/onlinestuh5";
}
}
function setupVideo() {
let attempts = 0;
const detectTimer = setInterval(() => {
const video = document.querySelector("video");
if (video) {
clearInterval(detectTimer);
video.playbackRate = config.playbackRate;
if (config.autoPlay) {
video.play().catch(() => { });
}
video.addEventListener("ended", () => {
setTimeout(() => {
const nextBtn = document.querySelector(".next-btn, .nav-next, .catalog-item.active + .catalog-item");
if (nextBtn) nextBtn.click();
}, 1500);
});
} else if (attempts > 40) {
clearInterval(detectTimer);
}
attempts++;
}, 500);
}
// ===================== 自动答题&弹窗处理 =====================
function startPopupMonitor() {
if (popupTimer) clearInterval(popupTimer);
popupTimer = setInterval(() => {
if (!config.autoClosePopup) return;
const continueBtns = document.querySelectorAll('.dialog-footer .btn, .vjs-modal-dialog .vjs-close-button, .continue-btn, .confirm-btn, button[title="继续播放"]');
continueBtns.forEach(btn => {
if (btn.offsetParent !== null) btn.click();
});
const closeBtns = document.querySelectorAll('.close, .btn-close, .modal-close, .dialog-close, [class*="close"]');
closeBtns.forEach(btn => {
if (btn.offsetParent !== null && !btn.closest(".video-js")) btn.click();
});
window.onbeforeunload = null;
}, 2000);
}
function startAnswerMonitor() {
if (answerTimer) clearInterval(answerTimer);
answerTimer = setInterval(() => {
if (!config.autoAnswer) return;
const popupSelectors = [
'.question-modal', '.test-popup', '.dialog-box:has(.question-stem)',
'.el-dialog:has(.question-title)', '.popupsbox:has(.option-item)'
];
let answerPopup = null;
for (const sel of popupSelectors) {
const el = document.querySelector(sel);
if (el && el.offsetParent !== null) {
answerPopup = el;
break;
}
}
if (answerPopup) handleAnswerPopup(answerPopup);
}, 1000);
}
async function handleAnswerPopup(popup) {
if (popup.dataset.processed === "true") return;
popup.dataset.processed = "true";
const stemEl = popup.querySelector('.question-stem, .question-title, .stem, .title h3');
const stem = stemEl ? stemEl.textContent.trim().replace(/\s+/g, "") : "";
if (!stem) {
popup.dataset.processed = "false";
return;
}
const optionEls = Array.from(popup.querySelectorAll('.option-item, .answer-option, .option, .el-radio, .el-checkbox')).filter(el => el.offsetParent !== null);
if (optionEls.length === 0) {
popup.dataset.processed = "false";
return;
}
const optionList = optionEls.map(el => {
const textEl = el.querySelector('.option-text, .label, span') || el;
return { el: el, text: textEl.textContent.trim().replace(/\s+/g, "") };
});
const optionTextList = optionList.map(opt => opt.text);
const isMulti = popup.querySelector('.el-checkbox') !== null;
const questionType = isMulti ? "multi" : "single";
await new Promise(r => setTimeout(r, 1000 + Math.random() * 1000));
let correctAnswers = [];
if (config.useOnlineBankFirst) {
correctAnswers = await searchYanxiBank(stem, optionTextList, questionType);
}
if (correctAnswers.length === 0) {
for (const key in localQuestionBank) {
const cleanKey = key.replace(/\s+/g, "");
if (stem.includes(cleanKey) || cleanKey.includes(stem)) {
correctAnswers = localQuestionBank[key];
break;
}
}
}
let selected = [];
if (correctAnswers.length > 0) {
correctAnswers.forEach(answer => {
const cleanAnswer = answer.replace(/\s+/g, "");
const target = optionList.find(opt => opt.text.includes(cleanAnswer) || cleanAnswer.includes(opt.text));
if (target) {
target.el.click();
selected.push(target.text);
}
});
} else {
const randomCount = isMulti ? Math.floor(Math.random() * optionList.length) + 1 : 1;
const randomOptions = [...optionList].sort(() => 0.5 - Math.random()).slice(0, randomCount);
randomOptions.forEach(opt => {
opt.el.click();
selected.push(opt.text);
});
}
await new Promise(r => setTimeout(r, 800 + Math.random() * 500));
const submitBtns = popup.querySelectorAll('.submit-btn, .confirm-btn, .el-button--primary, button:has-text("确定"), button:has-text("提交")');
submitBtns.forEach(btn => {
if (btn.offsetParent !== null) btn.click();
});
setTimeout(() => {
popup.dataset.processed = "false";
}, 2000);
}
async function searchYanxiBank(title, options, type) {
return new Promise((resolve) => {
const queryParams = new URLSearchParams({
token: YANXI_BANK_CONFIG.token,
title: title.trim(),
options: options.join("\n"),
type: type
});
GM_xmlhttpRequest({
method: "GET",
url: `${YANXI_BANK_CONFIG.url}?${queryParams.toString()}`,
timeout: 5000,
onload: (res) => {
try {
const result = JSON.parse(res.responseText);
if (result.code === 0 && result.data?.answer) {
let answer = result.data.answer;
answer = Array.isArray(answer) ? answer : [answer.toString().trim()];
resolve(answer);
} else {
resolve([]);
}
} catch (e) {
resolve([]);
}
},
onerror: () => resolve([]),
ontimeout: () => resolve([])
});
});
}
// ===================== 工具函数 =====================
function clearAllTimer() {
if (timer) clearInterval(timer);
if (popupTimer) clearInterval(popupTimer);
if (answerTimer) clearInterval(answerTimer);
}
function updateUI() {
const statusText = document.getElementById("statusText");
const courseText = document.getElementById("courseText");
const timeText = document.getElementById("timeText");
const progressFill = document.getElementById("progressFill");
if (statusText) statusText.textContent = state.isRunning ? "运行中" : "已停止";
if (courseText) courseText.textContent = `${state.currentCourseIndex + 1}/${state.totalCourses}`;
if (timeText) timeText.textContent = `${formatTime(state.currentTime)} / ${config.watchTime}分钟`;
if (progressFill) progressFill.style.width = `${Math.min((state.currentTime / (config.watchTime * 60)) * 100, 100)}%`;
}
function formatTime(seconds) {
const m = Math.floor(seconds / 60).toString().padStart(2, "0");
const s = (seconds % 60).toString().padStart(2, "0");
return `${m}:${s}`;
}
function startHumanSimulate() {
setInterval(() => {
if (!state.isRunning) return;
document.dispatchEvent(new MouseEvent("mousemove", {
clientX: Math.random() * 1000 + 200,
clientY: Math.random() * 600 + 100
}));
window.scrollBy(0, Math.random() > 0.5 ? 10 : -10);
}, 30000 + Math.random() * 20000);
}
// ===================== 事件绑定 =====================
function bindEvents() {
document.getElementById("refreshBtn").onclick = refreshCourseList;
document.getElementById("startBtn").onclick = startWatch;
document.getElementById("stopBtn").onclick = stopWatch;
}
// ===================== 【核心】初始化流程,等页面加载完再执行 =====================
async function init() {
loadAllData();
createPanel();
bindEvents();
startPopupMonitor();
startAnswerMonitor();
startHumanSimulate();
// 等待页面加载完成
await waitPageLoad();
// 页面加载完成后,刷新课程列表
refreshCourseList();
}
// 页面完全加载后再初始化
if (document.readyState === "complete") {
init();
} else {
window.addEventListener("load", init);
}
})();