// ==UserScript== // @name 华医网全自动助手 -多功能 // @namespace http://tampermonkey.net/ // @version 2.0.0 // @description 支持仅视频、视频+答题、仅答题三种模式自由切换;完美拦截所有弹窗;智能跳过选修/互动;防并发机制;V2.4.0考试逻辑。 // @match *://*.91huayi.com/course_ware/course_ware_polyv.aspx?* // @match *://*.91huayi.com/course_ware/course_ware_cc.aspx?* // @match *://*.91huayi.com/course_ware/course_ware.aspx?* // @match *://*.91huayi.com/course_ware/course_list.aspx?* // @match *://*.91huayi.com/pages/course.aspx* // @match *://*.91huayi.com/pages/exam.aspx* // @match *://*.91huayi.com/pages/exam_result.aspx* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_openInTab // @grant GM_addStyle // @run-at document-idle // ==/UserScript== (function () { 'use strict'; // ==================== 全局状态与配置 ==================== const STATE_KEY = 'hy_multi_mode_state_v2'; let state = GM_getValue(STATE_KEY, { mode: 'video_exam' }); // mode: 'video_exam' | 'video_only' | 'exam_only' function saveState() { GM_setValue(STATE_KEY, state); } const log = msg => console.log(`[华医助手] ${msg}`); const sleep = ms => new Promise(r => setTimeout(r, ms)); const randomDelay = (min = 800, max = 2500) => Math.floor(Math.random() * (max - min + 1) + min); const BASE_URL = window.location.origin; // ==================== 并发锁 ==================== const LOCK_VIDEO_KEY = 'huayi_lock_video'; const LOCK_EXAM_KEY = 'huayi_lock_exam'; function isLocked(key) { const v = GM_getValue(key); if (!v) return false; if (v.expire && v.expire < Date.now()) { GM_deleteValue(key); return false; } return true; } function acquireLock(key, sec) { if (isLocked(key)) return false; GM_setValue(key, { time: Date.now(), expire: Date.now() + sec * 1000 }); return true; } function releaseLock(key) { GM_deleteValue(key); } // ==================== 反自动化检测 ==================== try { window.blockAbnormalPlugin = function () { }; } catch (e) { } // ==================== 选修/互动识别 ==================== function isOptionalOrInteractive(el) { const labels = el.querySelectorAll('h3 label'); for (let label of labels) { const t = label.innerText.trim(); if (t === '选修' || t === '互动') return true; } // 兼容右侧列表 if (el.innerText && (el.innerText.includes('选修') || el.innerText.includes('互动'))) return true; return false; } // ==================== UI 控制面板 ==================== function createPanel() { if (document.getElementById('hy-panel')) return; const panel = document.createElement('div'); panel.id = 'hy-panel'; document.body.appendChild(panel); GM_addStyle(` #hy-panel { position:fixed; right:12px; bottom:12px; z-index:99999; background:rgba(15,23,42,0.94); color:#e2e8f0; padding:14px 16px; border-radius:12px; font-size:13px; width:250px; font-family:system-ui,sans-serif; box-shadow:0 6px 20px rgba(0,0,0,0.5); border:1px solid rgba(56,189,248,0.3); } .hy-title { font-weight:bold; font-size:15px; margin-bottom:10px; color:#38bdf8; display:flex; justify-content:space-between; } .hy-row { margin-bottom:8px; } .hy-select { width:100%; padding:5px; border-radius:6px; background:#1e293b; color:#fff; border:1px solid #475569; outline:none; cursor:pointer; } .hy-select option { background:#1e293b; } #hy-status { font-size:11px; color:#94a3b8; margin:8px 0; padding:6px 8px; background:rgba(0,0,0,0.3); border-radius:6px; min-height:20px; } .hy-btns { display:flex; gap:6px; margin-top:8px; } .hy-btn { flex:1; padding:6px; border:none; border-radius:6px; color:#fff; cursor:pointer; font-size:12px; transition:0.2s; } .hy-btn:hover { opacity:0.85; } .btn-video { background:linear-gradient(135deg,#22c55e,#16a34a); } .btn-exam { background:linear-gradient(135deg,#3b82f6,#2563eb); } `); panel.innerHTML = `
🚀 华医助手 V2.0
初始化中...
`; document.getElementById('hy-mode').addEventListener('change', e => { state.mode = e.target.value; saveState(); updateStatus('模式已切换'); }); document.getElementById('hy-btn-next').addEventListener('click', jumpToNextCourse); document.getElementById('hy-btn-exam').addEventListener('click', startExamQueueManually); } function updateStatus(text) { const el = document.getElementById('hy-status'); if (el) el.innerText = text; } // ==================== 增强版弹窗拦截器 ==================== let signBlocked = false; function initPopupBlocker() { // 1. 源头劫持 const hookPause = setInterval(() => { try { if (window.player) { if (window.player.j2s_pauseVideo) window.player.j2s_pauseVideo = () => log('已拦截 pauseVideo'); if (typeof window.initialSign !== 'undefined') window.initialSign = () => { log('自动签到'); if (typeof window.addPlaySign === 'function') window.addPlaySign(); window.isInitialSign = true; }; signBlocked = true; clearInterval(hookPause); } } catch (e) { } }, 500); // 2. 定时扫描清理 setInterval(() => { const modal = document.querySelector('.study_diaog'); if (modal) { modal.remove(); try { if (window.player?.j2s_resumeVideo) window.player.j2s_resumeVideo(); } catch (e) { } try { if (typeof addPlaySign === 'function') addPlaySign(); } catch (e) { } const v = document.querySelector('video'); if (v && v.paused) playVideo(v); } ['div_processbar_tip', 'div_tip', 'div_tip1'].forEach(id => { const el = document.getElementById(id); if (el && el.style.display !== 'none') el.style.display = 'none'; }); try { const layerContent = document.querySelector('.layui-layer-content'); if (layerContent && (layerContent.innerText.includes('课件准备中') || layerContent.innerText.includes('请刷新后重新进入'))) { const btn = document.querySelector('.layui-layer-btn0'); if (btn) { btn.click(); setTimeout(() => location.reload(), 800); } } const layerDialog = document.querySelector('.layui-layer'); if (layerDialog && !layerContent?.innerText.includes('课件准备中')) { if (typeof window.layer !== 'undefined' && window.layer.closeAll) window.layer.closeAll(); else layerDialog.remove(); } } catch (e) { } }, 400); // 3. MutationObserver 实时拦截 const observer = new MutationObserver(mutations => { for (const m of mutations) for (const node of m.addedNodes) { if (node.nodeType !== 1) continue; const text = node.innerText || ''; if (text.includes('课件准备中') || text.includes('请刷新后重新进入')) { const btn = node.querySelector('.layui-layer-btn0') || Array.from(node.querySelectorAll('input,button')).find(b => b.value.includes('确定')); if (btn) { btn.click(); setTimeout(() => location.reload(), 800); } } const dialog = node.classList?.contains('study_diaog') ? node : node.querySelector?.('.study_diaog'); if (dialog) { dialog.remove(); try { if (window.player?.j2s_resumeVideo) window.player.j2s_resumeVideo(); } catch (e) { } try { if (typeof addPlaySign === 'function') addPlaySign(); } catch (e) { } } } }); if (document.body) observer.observe(document.body, { childList: true, subtree: true }); } // ==================== 视频播放核心 ==================== function playVideo(video, retry = 0) { if (!video) return; video.muted = true; video.volume = 0; if (video.readyState >= 2) { video.play().then(() => updateStatus('▶ 播放中')).catch(e => { log(`播放失败: ${e.message},重试 ${retry + 1}/5`); if (retry < 4) setTimeout(() => playVideo(video, retry + 1), 1500); else { updateStatus('⚠ 需手动点击播放'); const once = () => { video.play(); document.removeEventListener('click', once); document.removeEventListener('keydown', once); }; document.addEventListener('click', once); document.addEventListener('keydown', once); } }); } else { log('视频数据未就绪,等待加载...'); const onCanPlay = () => { video.removeEventListener('canplay', onCanPlay); video.play().catch(e => log('canplay播放失败')); }; video.addEventListener('canplay', onCanPlay, { once: true }); setTimeout(() => { if (video.paused && video.readyState < 2) { video.load(); setTimeout(() => playVideo(video, retry), 1000); } }, 5000); } } let healthCheckStarted = false; function setupVideo(video) { if (video._hyFixed) return; video._hyFixed = true; log('设置视频监听'); setTimeout(() => playVideo(video), 1000); // 守护轮询 setInterval(() => { video.muted = true; video.volume = 0; try { if (window.player?.j2s_setVolume) window.player.j2s_setVolume(0); } catch (e) { } if (video.paused && video.currentTime > 0 && video.currentTime < video.duration - 1) { if (window.player && typeof window.player.j2s_resumeVideo === 'function') window.player.j2s_resumeVideo(); else playVideo(video); } }, 3000); // 健康检测 if (!healthCheckStarted) { healthCheckStarted = true; let lastTime = 0, stuckCount = 0; setInterval(() => { const current = video.currentTime, duration = video.duration; if (duration && current >= duration - 0.5) return; if (current === lastTime) { if (video.readyState < 2) { stuckCount++; updateStatus(`⚠ 卡顿修复(${stuckCount})`); if (stuckCount >= 2) setTimeout(() => location.reload(), randomDelay(3000, 6000)); else { video.load(); setTimeout(() => playVideo(video), 1000); } } } else { stuckCount = 0; lastTime = current; } }, 20000); } } function observeVideos() { const observer = new MutationObserver(mutations => { for (const m of mutations) for (const node of m.addedNodes) { if (node.nodeName === 'VIDEO') setupVideo(node); if (node.querySelectorAll) node.querySelectorAll('video').forEach(v => setupVideo(v)); } }); if (document.body) observer.observe(document.body, { childList: true, subtree: true }); document.querySelectorAll('video').forEach(v => setupVideo(v)); // iframe 兼容 let cnt = 0; const iInt = setInterval(() => { cnt++; document.querySelectorAll('iframe').forEach(iframe => { try { const v = (iframe.contentDocument || iframe.contentWindow?.document)?.querySelector('video'); if (v && !v._hyFixed) { setupVideo(v); cnt = 999; } } catch (e) { } }); if (cnt > 10) clearInterval(iInt); }, 3000); } function watchExamButton() { const examBtn = document.getElementById('jrks'); if (!examBtn) { setTimeout(watchExamButton, 2000); return; } const check = setInterval(() => { if (examBtn.getAttribute('disabled') === null) { clearInterval(check); updateStatus('✅ 视频播放完成'); if (state.mode === 'video_exam') { const cwid = window.location.href.match(/cwid=([^&]+)/)?.[1]; if (cwid && !isLocked(LOCK_EXAM_KEY)) openInNewTab(`/pages/exam.aspx?cwid=${cwid}`); } setTimeout(() => jumpToNextCourse(), randomDelay(1500, 3500)); } }, 3000); } // ==================== 跳课逻辑 ==================== function openInNewTab(url) { if(!url) return; let fullUrl = url.startsWith('http') ? url : new URL(url, BASE_URL).href; GM_openInTab(fullUrl, {active:true, insert:true}); } function jumpToNextCourse() { const url = window.location.href; if (url.includes('/pages/course.aspx')) { const courses = document.querySelectorAll('.course'); for (let course of courses) { if (isOptionalOrInteractive(course)) continue; const statusSpan = course.querySelector('h3 > span'); const playSpan = course.querySelector('.play_process'); const link = course.querySelector('h3 > a.f14blue'); if (!link) continue; if (statusSpan && (statusSpan.innerText.includes('已完成') || statusSpan.innerText.includes('待考试'))) continue; if (playSpan) { const m = parseInt(playSpan.getAttribute('data-maxtime')), t = parseInt(playSpan.getAttribute('data-totaltime')); if (m >= t && t > 0) continue; } window.location.href = link.href; return; } updateStatus('🎉 全部必修视频已学完'); return; } const items = document.querySelectorAll('li.lis-inside-content'); const currentCwid = url.match(/cwid=([^&]+)/)?.[1]; for (let li of items) { if (li.classList.contains('current-playing')) continue; if (isOptionalOrInteractive(li)) continue; const btn = li.querySelector('button'); const btnText = btn ? btn.innerText.trim() : ''; if (btnText === '未学习' || li.innerText.includes('未学习') || btnText === '学习中') { const onclickAttr = li.getAttribute('onclick'); if (currentCwid && onclickAttr && onclickAttr.includes(`cwid=${currentCwid}`)) continue; const urlMatch = onclickAttr?.match(/href='([^']+)'/); if (urlMatch && urlMatch[1] !== window.location.href) { window.location.href = urlMatch[1]; return; } } } updateStatus('当前列表无未学必修课程'); } function autoSelectCourseOnListPage() { if (state.mode === 'exam_only') return; // 仅答题模式不自动点视频 const courses = document.querySelectorAll('.course'); for (let course of courses) { if (isOptionalOrInteractive(course)) continue; const statusSpan = course.querySelector('h3 > span'); const playSpan = course.querySelector('.play_process'); const link = course.querySelector('h3 > a.f14blue'); if (!link) continue; if (statusSpan && (statusSpan.innerText.includes('已完成') || statusSpan.innerText.includes('待考试'))) continue; if (playSpan) { const m = parseInt(playSpan.getAttribute('data-maxtime')), t = parseInt(playSpan.getAttribute('data-totaltime')); if (m >= t && t > 0) continue; } window.location.href = link.href; return; } } // ==================== 考试模块 (V2.4.0 修复版) ==================== const db = { load: (k, d) => { const v = GM_getValue(k); return v === undefined ? d : JSON.parse(v); }, save: (k, d) => GM_setValue(k, JSON.stringify(d)), clear: k => GM_deleteValue(k) }; const correctAnswersDB = { key: 'correct_answers_db_v12', get: () => db.load(correctAnswersDB.key, {}), set: d => db.save(correctAnswersDB.key, d) }; const wrongAttemptsDB = { key: 'wrong_attempts_db_v12', get: () => db.load(wrongAttemptsDB.key, {}), set: d => db.save(wrongAttemptsDB.key, d) }; const examQueueDB = { key: 'exam_queue_db_v12', get: () => db.load(examQueueDB.key, []), set: d => db.save(examQueueDB.key, d) }; const questionOptionMapDB = { key: 'question_option_map_v12', get: () => db.load(questionOptionMapDB.key, {}), set: d => db.save(questionOptionMapDB.key, d) }; const normalize = txt => txt ? txt.trim().replace(/^\d+、\s*/, '').replace(/[()()\s]/g, '') : ''; const getOptionInfo = el => { const lbl = el.querySelector('label'); if (!lbl) return null; const m = lbl.innerText.trim().match(/^([A-Z])、/); return { letter: m?.[1], content: normalize(lbl.innerText), element: el.querySelector('input.qo_name') }; }; function startExamQueueManually() { handleCourseListPage(); } function handleCourseListPage() { log("扫描待考课程(仅必修)..."); const courses = document.querySelectorAll('.course'); const pendingExams = []; courses.forEach(c => { if (isOptionalOrInteractive(c)) return; const statusSpan = c.querySelector('h3 > span'); const link = c.querySelector('h3 > a.f14blue'); if (link && statusSpan && statusSpan.innerText.includes('待考试')) { const cwidMatch = link.getAttribute('href').match(/cwid=([a-f0-9-]+)/); if (cwidMatch) pendingExams.push({ cwid: cwidMatch[1], name: link.innerText.trim() }); } }); if (pendingExams.length > 0) { examQueueDB.set(pendingExams); updateStatus(`发现 ${pendingExams.length} 个待考科目`); openInNewTab(`/pages/exam.aspx?cwid=${pendingExams[0].cwid}`); } else { updateStatus("无必修待考试课程"); } } async function handleExamPage() { if (!acquireLock(LOCK_EXAM_KEY, 1800)) { updateStatus("考试已在其他标签页进行"); return; } window.addEventListener('beforeunload', () => releaseLock(LOCK_EXAM_KEY)); updateStatus("正在智能作答..."); const correctAnswers = correctAnswersDB.get(), wrongAttempts = wrongAttemptsDB.get(), questions = document.querySelectorAll('.tablestyle'), qMap = {}; for (const qEl of questions) { const qKey = normalize(qEl.querySelector('.q_name').innerText); const opts = Array.from(qEl.querySelectorAll('tbody tr')).map(getOptionInfo).filter(Boolean); const currentMap = {}; opts.forEach(o => { if(o.content) currentMap[o.content] = o.letter; }); qMap[qKey] = currentMap; let isAnswered = false; const knownCorrects = correctAnswers[qKey] || []; for (const k of knownCorrects) { const match = opts.find(o => o.content === k.content); if (match) { match.element.click(); isAnswered = true; break; } } if (!isAnswered) { const knownWrongs = wrongAttempts[qKey] || []; const wrongContents = knownWrongs.map(w => w.content); for (const opt of opts) { if (!wrongContents.includes(opt.content)) { opt.element.click(); isAnswered = true; break; } } } if (!isAnswered && opts.length > 0) { opts[0].element.click(); isAnswered = true; } if (!isAnswered) { releaseLock(LOCK_EXAM_KEY); return; } await sleep(randomDelay(500, 1500)); } questionOptionMapDB.set(qMap); await sleep(randomDelay(2000, 3000)); document.getElementById('btn_submit')?.click(); } async function handleResultPage() { const qMap = questionOptionMapDB.get(), correctAnswers = correctAnswersDB.get(), wrongAttempts = wrongAttemptsDB.get(); const isPassed = !!document.querySelector('.tips_text')?.innerText.includes('考试通过'); if (!isPassed) { document.querySelectorAll('.state_cour_ul .state_cour_lis').forEach(item => { const qKey = normalize(item.querySelector('.state_lis_text:first-of-type').title); const ansEl = item.querySelectorAll('.state_lis_text')[1]; if (!ansEl) return; const userContent = normalize(ansEl.innerText.replace(/【您的答案:|】/g, '')); const isCorrect = item.querySelector('.state_error').src.includes('bar_img.png'); const userLetter = (qMap[qKey] || {})[userContent]; if (!userLetter) return; const bit = { letter: userLetter, content: userContent }; if (isCorrect) { if (!correctAnswers[qKey]) correctAnswers[qKey] = []; if (!correctAnswers[qKey].some(k => k.content === bit.content)) correctAnswers[qKey].push(bit); if (wrongAttempts[qKey]) { wrongAttempts[qKey] = wrongAttempts[qKey].filter(k => k.content !== bit.content); if (wrongAttempts[qKey].length === 0) delete wrongAttempts[qKey]; } } else { if (!wrongAttempts[qKey]) wrongAttempts[qKey] = []; if (!wrongAttempts[qKey].some(k => k.content === bit.content)) wrongAttempts[qKey].push(bit); } }); correctAnswersDB.set(correctAnswers); wrongAttemptsDB.set(wrongAttempts); const retryBtn = Array.from(document.querySelectorAll('input.state_foot_btn')).find(b => b.value === '重新考试'); if (retryBtn) retryBtn.click(); return; } questionOptionMapDB.clear(); const pendingBtns = Array.from(document.querySelectorAll('input.state_lis_btn')).filter(b => b.value.trim() === '待考试'); if (pendingBtns.length > 0) { await sleep(1000); pendingBtns[0].click(); return; } let queue = examQueueDB.get(); const currentCwid = new URLSearchParams(window.location.search).get('cwid'); queue = queue.filter(e => e.cwid !== currentCwid); examQueueDB.set(queue); if (queue.length > 0) { window.location.href = `${BASE_URL}/pages/exam.aspx?cwid=${queue[0].cwid}`; } else { releaseLock(LOCK_EXAM_KEY); updateStatus('🎉 所有考试已完成'); } } // ==================== 路由与主入口 ==================== function main() { createPanel(); const path = window.location.pathname; if (path.includes('/pages/course.aspx')) { if (state.mode !== 'video_only') handleCourseListPage(); // 扫描考试队列 setTimeout(autoSelectCourseOnListPage, 2000); // 自动跳转视频(如果是仅答题模式,内部会拦截) } else if (path.includes('course_ware/')) { if (state.mode === 'exam_only') { updateStatus('📝 仅答题模式,不播放视频'); return; } if (!acquireLock(LOCK_VIDEO_KEY, 7200)) { updateStatus("已有视频在播"); return; } window.addEventListener('beforeunload', () => releaseLock(LOCK_VIDEO_KEY)); initPopupBlocker(); observeVideos(); watchExamButton(); // 画质切换 setTimeout(() => { const qBtn = document.querySelector('.pv-quality-btn'); if(qBtn) qBtn.click(); setTimeout(() => { const f = Array.from(document.querySelectorAll('.pv-quality-select div')).find(d => d.innerText === '流畅'); if(f) f.click(); }, 500); }, 3000); } else if (path.includes('/exam.aspx')) { if (state.mode !== 'video_only') handleExamPage(); } else if (path.includes('/exam_result.aspx')) { if (state.mode !== 'video_only') handleResultPage(); } } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', main); else main(); })();