// ==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); } })();