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