// ==UserScript== // @name 华医网全能助手(修复版) // @namespace https://github.com/yourname/ // @version 3.1.1 // @description 自动静音播放、自动切换视频、自动关闭“检测到异常插件”拦截弹窗、智能考试;修复“有未学课程却提示没有更多未学课程” // @match *://*.91huayi.com/course_ware/course_ware_polyv.aspx?* // @match *://*.91huayi.com/course_ware/course_ware_cc.aspx?* // @match *://*.91huayi.com/pages/exam.aspx?* // @match *://*.91huayi.com/pages/exam_result.aspx?* // @match *://*.91huayi.com/pages/course.aspx* // @match *://*.91huayi.com/course_ware/course_list.aspx* // @match *://*.91huayi.com/course_ware/course.aspx* // @match *://*.91huayi.com/course_ware/course_ware.aspx* // @match *://*.91huayi.com/pages/*.aspx* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_openInTab // @grant GM_addStyle // @run-at document-idle // @license MIT // ==/UserScript== /* ============================================================ * 修复说明(v3.0.0) * * 【问题一】有未学课程,却提示“没有更多未学课程” * 这条提示只可能出现在“视频页目录扫描”这一段,旧版有三个致命限制: * 1. 判断可学状态时,要求目录项里的 ' + ' ' + ' ' + ' ' + ''; (document.body || document.documentElement).appendChild(host); panel = host; panelLog = root.getElementById('log'); root.getElementById('close').onclick = function () { host.style.display = 'none'; debugLog('面板已隐藏,按 ' + CFG.panelHotkey + ' 可再次显示'); }; root.getElementById('mode').onchange = function () { currentMode = this.value; GM_setValue(MODE_KEY, currentMode); debugLog('模式切换为: ' + this.options[this.selectedIndex].text); }; root.getElementById('next').onclick = function () { jumpToNextCourse(true); }; // 手动强制跳 root.getElementById('exam').onclick = function () { startExamQueueManually(); }; document.addEventListener('keydown', function (ev) { if (ev.altKey && (ev.key === 'h' || ev.key === 'H')) { panel.style.display = (panel.style.display === 'none') ? '' : 'none'; } }); debugLog('🚀 脚本启动成功(v3.1.1)'); } catch (e) { log('面板创建失败: ' + e.message); } } /* ==================== 弹窗猎杀(修复“检测到异常插件”) ==================== */ let popupKilled = 0; function hitKeyword(t) { if (!t) return false; for (let i = 0; i < CFG.popupKeywords.length; i++) { if (t.indexOf(CFG.popupKeywords[i]) >= 0) return true; } return false; } function installDialogBlockers() { wins().forEach(function (w) { ['alert', 'confirm', 'prompt'].forEach(function (name) { try { const raw = w[name]; if (typeof raw !== 'function' || raw.__hyBlocked) return; const faked = function (msg) { const t = String(msg == null ? '' : msg); if (hitKeyword(t.replace(/\s+/g, ''))) { debugLog('拦截 ' + name + '():' + t.slice(0, 60)); popupKilled++; return name === 'confirm' ? true : (name === 'prompt' ? '' : undefined); } try { return raw.apply(w, arguments); } catch (e) { return undefined; } }; hideSource(faked, raw.toString()); try { faked.__hyBlocked = true; } catch (e) {} w[name] = faked; } catch (e) {} }); }); } // 只隐藏、不删除:站点自己的 window.onload 里会去 click 这些节点, // 直接 remove 会让站点抛出 "Cannot read properties of undefined (reading 'click')"。 function suppressPopup(el) { try { el.style.setProperty('display', 'none', 'important'); el.style.setProperty('visibility', 'hidden', 'important'); el.style.setProperty('pointer-events', 'none', 'important'); el.setAttribute('aria-hidden', 'true'); const parent = el.parentElement; if (parent && parent.classList && parent.classList.contains('layui-layer-shade')) { parent.style.setProperty('display', 'none', 'important'); parent.style.setProperty('pointer-events', 'none', 'important'); } } catch (e) {} } function isSuppressed(el) { return el.getAttribute && el.getAttribute('aria-hidden') === 'true'; } // 返回本次扫描关闭的弹窗数量 function swallowPopupOnce() { let killed = 0; // 1) SweetAlert2(站点全站加载了 sweetalert2-7.28.4) document.querySelectorAll('.swal2-container, .swal2-popup').forEach(function (el) { if (!hitKeyword(textOf(el))) return; killed++; const btn = el.querySelector('.swal2-confirm') || el.querySelector('.swal2-cancel') || el.querySelector('.swal2-close'); if (btn) { try { btn.click(); } catch (e) {} } setTimeout(function () { if (!isSuppressed(el)) suppressPopup(el); }, CFG.popupForceRemoveMs); }); // 2) layui layer(考试相关弹窗都是这一套) document.querySelectorAll('.layui-layer').forEach(function (el) { if (!hitKeyword(textOf(el))) return; killed++; const btn = el.querySelector('.layui-layer-btn0') || el.querySelector('.layui-layer-btn1') || el.querySelector('.layui-layer-close'); if (btn) { try { btn.click(); } catch (e) {} } setTimeout(function () { if (!isSuppressed(el)) suppressPopup(el); }, CFG.popupForceRemoveMs); }); // 3) 其它通用遮罩层 / 自定义弹窗 document.querySelectorAll('[role="dialog"], .modal, .dialog, .popup, .mask-layer').forEach(function (el) { if (!hitKeyword(textOf(el))) return; killed++; const txt = ['确定', '知道了', '我已知晓', '关闭', '继续', 'OK']; let clicked = false; const btns = el.querySelectorAll('button, input[type=button], input[type=submit], a'); btns.forEach(function (b) { if (clicked) return; const s = ((b.tagName === 'INPUT' ? b.value : b.innerText) || '').replace(/\s+/g, ''); if (txt.indexOf(s) >= 0) { try { b.click(); clicked = true; } catch (e) {} } }); if (!clicked) setTimeout(function () { if (!isSuppressed(el)) suppressPopup(el); }, CFG.popupForceRemoveMs); }); // 没有“还在显示”的 layui 弹层了,顺手隐藏残留遮罩(避免页面被灰罩挡住点不动) let liveLayer = false; document.querySelectorAll('.layui-layer').forEach(function (el) { if (!isSuppressed(el)) liveLayer = true; }); if (!liveLayer) { document.querySelectorAll('.layui-layer-shade').forEach(function (s) { suppressPopup(s); }); } if (killed) { popupKilled += killed; debugLog('已自动关闭拦截弹窗 x' + killed + '(累计 ' + popupKilled + ')'); } return killed; } function isPopupish(node) { try { const cls = (typeof node.className === 'string' ? node.className : '') + ' ' + (node.id || ''); return /swal2|layui|modal|dialog|popup|mask|shade/i.test(cls); } catch (e) { return false; } } function initPopupKiller() { installDialogBlockers(); swallowPopupOnce(); setInterval(swallowPopupOnce, CFG.popupScanMs); // 只对“看起来像弹窗”的新增节点做即时补刀,避免视频页高频变动导致空转 let timer = null; const schedule = function (delay) { if (timer) return; timer = setTimeout(function () { timer = null; swallowPopupOnce(); }, delay); }; const observer = new MutationObserver(function (mutations) { for (let i = 0; i < mutations.length; i++) { const nodes = mutations[i].addedNodes; for (let j = 0; j < nodes.length; j++) { const node = nodes[j]; if (node.nodeType === 1 && isPopupish(node)) { schedule(0); return; } } } }); const target = document.body || document.documentElement; if (target) observer.observe(target, { childList: true, subtree: true }); } /* ==================== 弹窗拦截(问答 / 签到 / 温馨提示 / 答题模式) ==================== */ // 站点自己的“温馨提示”层(拖拽提醒 / 付费提醒等)。 // 结构线索来自实测:页面上有 #div_tip / #div_tip1,class 都是 hidden_tips, // 靠去掉 hidden_tips(或改 display)来显示。命中文案后优先点“确定”, // 点不到就整层隐藏——这样不会走到站点那套刷新逻辑、把这一节从头再放一遍。 const TIP_TEXT_RE = /温馨提示|不能拖拽|完整地学完|耐心地听|不能快进|认真观看|解锁全部内容/; const TIPCLICK_KEY = 'hy_tip_clicks'; function findConfirm(el) { const cands = el.querySelectorAll('input[type=button], input[type=submit], button, a, span, div'); for (let i = 0; i < cands.length; i++) { const c = cands[i]; const t = (c.tagName === 'INPUT' ? (c.value || '') : (c.innerText || '')).replace(/\s+/g, ''); if (/^(确定|知道了|我已知晓|我知道了|好的|关闭|继续观看|OK)$/i.test(t)) return c; } return el.querySelector('.layui-layer-btn0, .pv-tip-ok, [class*=confirm]'); } // “确定”按钮本身如果就是刷新/跳转,点了反而会丢掉本节进度 function confirmLooksLikeReload(btn) { const oc = (btn.getAttribute && btn.getAttribute('onclick')) || ''; return /reload|refresh|location\s*\.\s*href|location\s*\.\s*replace/i.test(oc); } // 一分钟内点过几次“确定”(跨刷新保留,用来防刷新死循环) function tipClickHistory() { const now = Date.now(); const arr = loadData(TIPCLICK_KEY) || []; return arr.filter(function (t) { return now - t < 60000; }); } function closeSiteTips() { let n = 0; const list = document.querySelectorAll('[id^=div_tip], .hidden_tips, .pv-tip, [class*=tipbox], [class*=tip_box]'); list.forEach(function (el) { if (isHidden(el) || isSuppressed(el)) return; if (!TIP_TEXT_RE.test(textOf(el))) return; n++; const btn = findConfirm(el); // 同一个 DOM 节点只点一次;点完没消失,下一轮直接隐藏 if (btn && !el._hyClicked) { if (confirmLooksLikeReload(btn)) { debugLog('提示层的“确定”看着会刷新页面,跳过点击,直接隐藏'); suppressPopup(el); return; } const hist = tipClickHistory(); if (hist.length >= 5) { debugLog('提示层一分钟内已点 5 次还在弹,判定点“确定”会刷新页面,改为只隐藏'); suppressPopup(el); return; } el._hyClicked = true; hist.push(Date.now()); saveData(TIPCLICK_KEY, hist); try { btn.click(); debugLog('已点掉站点提示层(“确定”)'); return; } catch (e) {} } suppressPopup(el); debugLog('已隐藏站点提示层'); }); return n; } function initPopupBlocker() { setInterval(closeSiteTips, 500); // 拖拽提醒一来就立刻处理 setInterval(function () { try { if (window.player && typeof window.player.j2s_pauseVideo === 'function' && !window.player.j2s_pauseVideo.__hyBlocked) { const raw = window.player.j2s_pauseVideo; const faked = function () { debugLog('拦截 pauseVideo'); return false; }; hideSource(faked, raw.toString()); try { faked.__hyBlocked = true; } catch (e) {} window.player.j2s_pauseVideo = faked; } if (typeof window.initialSign !== 'undefined' && !window.initialSign.__hyBlocked) { const faked = function () { debugLog('自动签到'); if (typeof window.addPlaySign === 'function') window.addPlaySign(); window.isInitialSign = true; }; try { faked.__hyBlocked = true; } catch (e) {} window.initialSign = faked; } } catch (e) {} }, 500); setInterval(function () { try { const ask = document.querySelector('.pv-ask-head'); if (ask) { const skip = document.querySelector('.pv-ask-skip'); if (skip) { skip.click(); debugLog('已跳过课堂问答'); } } const sign = document.querySelector('.signBtn'); if (sign) { sign.click(); debugLog('已跳过签到'); } const tip = document.querySelector('button[onclick="closeBangZhu()"], button[onclick="closeProcessbarTip()"]'); if (tip) { tip.click(); debugLog('已关闭温馨提示'); } const know = document.querySelector('input.rig_btn[value="知道了"]'); if (know && document.getElementById('div_processbar_tip') && document.getElementById('div_processbar_tip').style.display !== 'none') { know.click(); } // “选择答题模式”弹窗 → 自动选“正式考试” const modeDialog = document.querySelector('.layui-layer-content'); if (modeDialog) { const text = modeDialog.innerText || ''; if (text.indexOf('选择答题模式') >= 0 || text.indexOf('考试模式') >= 0 || text.indexOf('练习模式') >= 0) { const btns = modeDialog.parentElement.querySelectorAll('.layui-layer-btn0, .layui-layer-btn1'); for (let i = 0; i < btns.length; i++) { const t = btns[i].innerText || ''; if (t.indexOf('正式考试') >= 0 || t.indexOf('考试') >= 0) { btns[i].click(); debugLog('已选择“正式考试”模式'); break; } } } } } catch (e) {} try { const video = document.querySelector('video'); if (video && video.paused && video.currentTime > 0 && video.currentTime < video.duration - 1) { video.muted = true; video.volume = 0; video.play().catch(function () {}); debugLog('视频意外暂停,已恢复'); } } catch (e) {} }, 3000); const observer = new MutationObserver(function (mutations) { for (let i = 0; i < mutations.length; i++) { const nodes = mutations[i].addedNodes; for (let j = 0; j < nodes.length; j++) { const node = nodes[j]; if (node.nodeType !== 1) continue; const text = node.innerText || ''; if (text.indexOf('选择答题模式') >= 0) { const btns = node.querySelectorAll('.layui-layer-btn0, .layui-layer-btn1'); for (let k = 0; k < btns.length; k++) { const t = btns[k].innerText || ''; if (t.indexOf('正式考试') >= 0 || t.indexOf('考试') >= 0) { btns[k].click(); debugLog('自动选择“正式考试”'); break; } } } if (node.classList && node.classList.contains('study_diaog')) { suppressPopup(node); // 隐藏即可,删除会让站点 onload 里的 click 报错 debugLog('已隐藏 study_diaog'); } } } }); if (document.body) observer.observe(document.body, { childList: true, subtree: true }); } /* ==================== 视频播放 ==================== */ function setupVideo(video) { if (video._hySetup) return; video._hySetup = true; video.muted = true; video.volume = 0; const playVideo = function () { if (video.readyState >= 2) { video.play().catch(function () {}); } else { video.addEventListener('canplay', function () { video.play().catch(function () {}); }, { once: true }); } }; setTimeout(playVideo, 1000); setInterval(function () { // 注意:这里不再要求 currentTime > 0——刚进页面时它是 0, // 之前那条限制导致“视频压根没起来”时守护完全不起作用。 const d = video.duration; const notAtEnd = !isFinite(d) || d <= 0 || video.currentTime < d - 1; if (video.paused && !video.ended && notAtEnd) { video.muted = true; video.volume = 0; const p = video.play(); if (p && p.catch) p.catch(function () {}); debugLog('守护恢复播放'); } }, CFG.videoGuardMs); let endHandled = false; video.addEventListener('ended', function () { // 防御:占位视频、试看片段、播放器切源都会触发 ended。 // 必须确认真播到了结尾,否则就是“本节没学完就跳走”。 const d = video.duration, c = video.currentTime; if (isFinite(d) && d > 0 && d < 60) { debugLog('忽略过短的 ended(时长 ' + Math.round(d) + 's)'); return; } if (isFinite(d) && d > 0 && c < d - 3) { debugLog('忽略未播到结尾的 ended'); return; } if (endHandled) return; endHandled = true; debugLog('视频播放结束'); setTimeout(function () { // 先看本课状态,能识别就按状态走 const entries = collectEntries(); const idx = currentIndex(entries); const st = idx >= 0 ? entryStatus(entries[idx]) : ''; debugLog('本课状态:' + (st || '未知')); if (st === 'exam' && currentMode === 'video_exam') { const cwid = getCwidFromUrl(); if (cwid) { markExamPending(cwid); debugLog('进入考试: /pages/exam.aspx?cwid=' + cwid); setTimeout(function () { window.location.href = '/pages/exam.aspx?cwid=' + cwid; }, randomDelay(1000, 3000)); return; } } // 其它情况统一走“找下一课” jumpToNextCourse(); }, 2000); }); } function observeVideos() { const obs = new MutationObserver(function () { document.querySelectorAll('video').forEach(function (v) { setupVideo(v); }); }); if (document.body) obs.observe(document.body, { childList: true, subtree: true }); document.querySelectorAll('video').forEach(function (v) { setupVideo(v); }); } // 页面里是否还有播放器(polyv 播放器可能渲染在 iframe 里,只看