// ==UserScript==
// @name 东奥会计继续教育看课自动答题 v2.0
// @namespace http://tampermonkey.net/
// @version 3.0.0
// @description 自动看课答题 + 顺序切课,全程单标签。v2.0 修复:①登录态识别(未登录→尝试点登录免验证直登/等待手动登录,确认已登录才跳转,杜绝"没登录就跳→被踢→死循环")②单标签全链路强制(window.open + target=_blank + 表单 + 中键全部改同标签,根治多开导致"不能同时学习多个视频"中断)③会话过期恢复链(落到 guangdong 等落地页自动回 jxjy 入口重新免验证登录,带循环保护防死循环)④视频黑屏/加载失败看门狗(刷新兜底 + 恢复链兜底)⑤答题循环加固(任何分支都续跑)⑥课程/讲次列表翻页 ⑦暂停恢复后各监听不丢失
// @author conanxz
// @match *://study.dongao.cn/*
// @match *://jxjycwweb.dongao.cn/*
// @match *://jixuweb.dongao.cn/*
// @match *://*.dongao.cn/*
// @match *://dongao.cn/*
// @match *://jxjy.czt.zj.gov.cn/*
// @grant none
// @run-at document-start
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// 调试开关:true 时视频页 3 秒后模拟"已学完"直接触发切课,仅用于验证流程;正式使用务必 false
const DEBUG = false;
const VER = '3.0.0';
const BLACKFIX = false; // v2.0.9 黑屏修复模式:true 时学完不自动切课(仅调试用),false=正常自动切课
// ═══ 常量 ═══
const STATE_KEY = 'dongao18_state';
const REC_KEY = 'dongao18_rec'; // 异常恢复计数(循环保护)
const BREAK_KEY = 'dongao18_break'; // 断点记录(诊断用)
const ENTRY_URL = 'https://jxjy.czt.zj.gov.cn/front/jxjy.html';
const INDEX_URL = 'https://study.dongao.cn/study/index/index';
const LEARN_HOSTS = ['study.dongao.cn', 'jixuweb.dongao.cn', 'jxjycwweb.dongao.cn'];
const MAX_REC = 6; // 30 分钟内异常恢复超过 6 次 → 判定登录卡死,自动暂停求助
const REC_WINDOW_MS = 30 * 60 * 1000;
// ═══ 通用小工具 ═══
function readJson(key, dflt) {
try { const raw = localStorage.getItem(key); if (raw) return JSON.parse(raw); } catch (e) {}
return dflt;
}
function writeJson(key, val) { try { localStorage.setItem(key, JSON.stringify(val)); } catch (e) {} }
function loadState() {
const s = readJson(STATE_KEY, null);
if (s && typeof s === 'object') { if (s.version !== VER) s.version = VER; return s; }
return { running: true, version: VER };
}
function saveState(s) { writeJson(STATE_KEY, s); }
function getAccount() {
const m = location.search.match(/accountId=(\d+)/);
if (m) { try { localStorage.setItem('dongao18_account', m[1]); } catch (e) {} return m[1]; }
try { const saved = localStorage.getItem('dongao18_account'); if (saved) return saved; } catch (e) {}
return '33598545';
}
function myCourseUrl() { return 'https://study.dongao.cn/study/u/myCourse?accountId=' + getAccount(); }
// ═══ 异常恢复计数(循环保护)═══
function recBump(where) {
let r = readJson(REC_KEY, null);
if (!r || (Date.now() - r.ts) > REC_WINDOW_MS) r = { n: 0, ts: Date.now(), log: [] };
r.n++;
r.log.push(new Date().toTimeString().slice(0, 8) + ' ' + where);
if (r.log.length > 12) r.log = r.log.slice(-12);
writeJson(REC_KEY, r);
return r.n;
}
function recReset() { writeJson(REC_KEY, { n: 0, ts: Date.now(), log: [] }); }
// 异常恢复:回到浙江门户入口,重新识别登录态/免验证登录
// —— 跨页保持暂停(v2.2.0):跳转 URL 带 dop_paused=1,新页读到即保持暂停 ——
function withPause(url) {
// v2.2.1:对称状态传递——跳转时把源页 running 状态带给目标页,
// 否则跨域 localStorage 不同步(暂停后恢复,别的域还停在旧状态)
// v2.2.2:改用 #hash 传状态——hash 不发给服务器,不会触发平台
// 「Url的加密Key有异常」校验(query 加参数会被平台签名校验拒绝)
try {
if (url.indexOf('dop_run=1') === -1 && url.indexOf('dop_paused=1') === -1) {
const tag = loadState().running ? 'dop_run=1' : 'dop_paused=1';
url += (url.indexOf('#') === -1 ? '#' : '&') + tag;
}
// v2.3.0:跳转时把学分总进度带给目标域(hash 不发服务器,不触发加密Key校验)
const c = readJson(CREDIT_KEY, null);
if (c && isFinite(c.total) && c.total > 0 && url.indexOf('dop_credit=') === -1) {
url += (url.indexOf('#') === -1 ? '#' : '&') + 'dop_credit=' + c.total + '/' + c.earned;
}
} catch (e) {}
return url;
}
function go(url) { window.location.href = withPause(url); }
function goRecover(where) {
const n = recBump(where);
if (n > MAX_REC) {
const s = loadState(); s.running = false; saveState(s);
log('⚠️ 异常恢复已连续 ' + n + ' 次(' + where + '),疑似登录卡死,已自动暂停');
setStatus('🔴 恢复失败已暂停,请手动登录');
setCur('手动登录后点「▶ 开始」继续');
refreshPanel();
return;
}
log('异常恢复(' + n + '/' + MAX_REC + '):' + where + ' → 回浙江门户入口');
setStatus('🟡 会话异常,恢复中…');
go(ENTRY_URL);
}
function saveBreak(tag) {
writeJson(BREAK_KEY, { tag: tag, url: location.href, ts: Date.now(), time: new Date().toLocaleString() });
}
// ═══ 悬浮面板 ═══
function createPanel() {
try {
if (document.getElementById('dop')) return;
if (!document.body) { setTimeout(createPanel, 200); return; }
const d = document.createElement('div');
d.id = 'dop';
d.innerHTML = `
东奥自动看课 v${VER} −
✅ 脚本已加载
等待中…
`;
document.body.appendChild(d);
document.getElementById('dop-min').onclick = () => {
document.getElementById('dop-box').classList.add('min');
document.getElementById('dop-badge').classList.add('show');
};
document.getElementById('dop-badge').onclick = () => {
document.getElementById('dop-box').classList.remove('min');
document.getElementById('dop-badge').classList.remove('show');
};
document.getElementById('dop-btn').onclick = () => toggleRun();
document.getElementById('dop-rst').onclick = () => resetAll();
const exp = document.getElementById('dop-exp');
if (exp) exp.onclick = () => exportLog();
refreshPanel();
} catch (e) {
console.error('[东奥2.0] 面板创建失败:', e);
}
}
// 日志持久化(环形缓冲,固定 500 条防堆积)
const LOG_KEY = 'dongao18_log';
const CREDIT_KEY = 'dongao18_credit'; // 学分总进度缓存 {total, earned}
const LOG_MAX = 500;
function addLog(m) {
const el = document.getElementById('dop-log');
const now = new Date();
const t = now.toTimeString().slice(0, 8);
if (el) { el.textContent += t + ' ' + m + '\n'; el.scrollTop = el.scrollHeight; }
// 持久化:完整时间戳 + 消息,写当前域 localStorage 环形缓冲
try {
let arr = readJson(LOG_KEY, []);
if (!Array.isArray(arr)) arr = [];
arr.push({ t: now.toLocaleString('zh-CN', { hour12: false }), m: String(m) });
if (arr.length > LOG_MAX) arr = arr.slice(-LOG_MAX);
localStorage.setItem(LOG_KEY, JSON.stringify(arr));
} catch (e) {}
}
function setCur(t) { const el = document.getElementById('dop-cur'); if (el) el.textContent = t; }
function setCredit() {
const el = document.getElementById('dop-credit');
if (!el) return;
try {
const c = readJson(CREDIT_KEY, null);
if (c && isFinite(c.total) && c.total > 0) {
const pct = (c.earned / c.total * 100).toFixed(1);
el.textContent = '总进度:' + pct + '%(' + c.earned + ' / ' + c.total + ' 学分)';
} else {
el.textContent = '总进度:暂无(回一次「我的课程」页自动统计)';
}
} catch (e) {}
}
function setPg(t) { const el = document.getElementById('dop-pg'); if (el) el.textContent = t; }
function setBar(pct) {
const bar = document.getElementById('dop-bar');
const i = bar ? bar.querySelector('i') : null;
if (!bar || !i) return;
if (pct == null) { bar.style.display = 'none'; return; }
bar.style.display = 'block';
i.style.width = Math.max(0, Math.min(100, pct)) + '%';
}
function setStatus(t) { const el = document.getElementById('dop-status'); if (el) el.textContent = t; }
function refreshPanel() {
const s = loadState();
const btn = document.getElementById('dop-btn');
if (!btn) return;
if (s.running) {
btn.textContent = '⏸ 暂停';
btn.classList.remove('paused');
} else {
btn.textContent = '▶ 开始';
btn.classList.add('paused');
}
setCredit();
}
function toggleRun() {
const s = loadState();
s.running = !s.running;
saveState(s);
refreshPanel();
if (s.running) {
addLog('▶ 已启动/恢复自动学习');
setStatus('🟢 运行中');
recReset();
main();
} else {
addLog('⏸ 已暂停(当前页停止自动操作)');
setStatus('🔴 已暂停');
}
}
function resetAll() {
if (!confirm('确认重置自动学习状态?(清空断点/恢复计数,回到学习首页重新开始)')) return;
try {
localStorage.removeItem(STATE_KEY);
localStorage.removeItem(REC_KEY);
localStorage.removeItem(BREAK_KEY);
sessionStorage.removeItem('dongao_lecture_list');
sessionStorage.removeItem('dop_fail_cnt');
} catch (e) {}
addLog('状态已重置,走门户链重新进入学习');
setCur(''); setPg(''); setBar(null); refreshPanel();
setTimeout(() => startPortalEntry(), 800);
}
function exportLog() {
try {
const arr = readJson(LOG_KEY, []);
const lines = [];
lines.push('东奥自动看课 v' + VER + ' 运行日志');
lines.push('时间: ' + new Date().toLocaleString('zh-CN', { hour12: false }));
lines.push('页面: ' + location.href);
lines.push('UA: ' + (navigator.userAgent || '').slice(0, 120));
const s = loadState();
lines.push('状态: ' + JSON.stringify(s));
lines.push('账户: ' + (getAccount() || '未识别'));
const stage = readJson('dongao18_stage', '');
if (stage) lines.push('阶段: ' + stage);
lines.push('日志条数: ' + (Array.isArray(arr) ? arr.length : 0));
lines.push('────────────────────────────');
if (Array.isArray(arr) && arr.length) {
arr.forEach(x => lines.push('[' + (x.t || '?') + '] ' + (x.m || '')));
} else {
lines.push('(本页面尚无日志记录)');
}
const text = lines.join('\n');
// 复制到剪贴板(优先 Clipboard API,失败用 execCommand 兜底)
function fallback() {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed'; ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); } catch (e) {}
document.body.removeChild(ta);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(() => {
addLog('📋 日志已复制到剪贴板(' + arr.length + ' 条)');
}).catch(() => { fallback(); addLog('📋 日志已复制(降级)'); });
} else { fallback(); addLog('📋 日志已复制到剪贴板'); }
} catch (e) {
console.error('[东奥2.0] 导出日志失败:', e);
addLog('⚠️ 导出日志失败:' + e.message);
}
}
function log(...a) {
const msg = a.map(x => (typeof x === 'object' ? JSON.stringify(x) : String(x))).join(' ');
console.log('[东奥2.0]', ...a);
addLog(msg);
}
// 防当前页重复初始化
if (window.__dongao18) {
console.log('[东奥2.0] 本页已初始化,跳过');
return;
}
window.__dongao18 = true;
// ═══════════════════════════════════════════════════
// 单标签强制(v2.0 核心修复②:根治多开)
// 说明:``、表单 target、鼠标中键/修饰键点击都不走 window.open,
// 仅劫持 window.open 拦不住 → 必须叠加“点击捕获拦截 + target 属性剥离”双保险。
// ═══════════════════════════════════════════════════
(function enforceSingleTab() {
// 1) window.open 全部改同标签(http(s) 跳本页;其它一律吞掉,不开新窗)
const _origOpen = window.open;
window.open = function (url) {
// v2.0.2:支持相对路径(讲次点击常用 window.open('/xxx/videoPlay?...'),相对地址要基于当前页解析成绝对地址)
if (typeof url === 'string' && url) {
let abs = null;
try { abs = new URL(url, location.href).href; } catch (e) { abs = null; }
if (abs && /^https?:/i.test(abs)) {
log('拦截 window.open → 同标签跳转:', abs);
go(abs);
return null;
}
}
log('拦截 window.open(非链接调用,已吞掉防多开)', url);
return null;
};
void _origOpen;
// 2) 点击捕获阶段拦截:target!=_self、ctrl/shift/中键点击 → 同标签
function sameTabify(a) {
try {
const href = a.getAttribute('href');
if (!href || href === '#' || /^\s*javascript:/i.test(href)) return false;
const url = new URL(href, location.href);
if (!/^https?:/i.test(url.protocol)) return false;
log('拦截新标签点击 → 同标签跳转:', url.href);
go(url.href);
return true;
} catch (e) { return false; }
}
function onClick(e) {
try {
const a = e.target && e.target.closest ? e.target.closest('a[href]') : null;
if (!a) return;
const t = (a.getAttribute('target') || '').toLowerCase();
const newTabByMod = e.ctrlKey || e.metaKey || e.shiftKey || e.button === 1;
if ((t && t !== '_self') || newTabByMod) {
e.preventDefault();
e.stopPropagation();
sameTabify(a);
}
} catch (err) {}
}
document.addEventListener('click', onClick, true);
document.addEventListener('auxclick', onClick, true); // 鼠标中键
// 3) 兜底:DOM 里出现 target 就剥掉(覆盖 JS 动态插的链接、表单、键盘触发)
function killTargets(root) {
try {
if (!root) return;
if (root.nodeType === 1 && root.matches && root.matches('a[target],area[target],form[target],base[target]')) {
root.removeAttribute('target');
}
if (root.querySelectorAll) {
root.querySelectorAll('a[target],area[target],form[target],base[target]').forEach(el => el.removeAttribute('target'));
}
} catch (e) {}
}
try {
const mo = new MutationObserver(muts => {
for (const m of muts) {
if (m.type === 'attributes') { killTargets(m.target); }
else { m.addedNodes && m.addedNodes.forEach(killTargets); }
}
});
mo.observe(document.documentElement || document, {
childList: true, subtree: true, attributes: true, attributeFilter: ['target']
});
killTargets(document.documentElement || document);
} catch (e) {}
})();
// ═══ 文本查找/点击 ═══
function matchText(els, texts) {
for (const t of texts) {
const el = els.find(e => {
const tx = (e.textContent || e.value || e.getAttribute('title') || '').trim();
return tx === t || tx.indexOf(t) !== -1;
});
if (el) return el;
}
return null;
}
function findByText(texts, root) {
root = root || document;
const sel1 = 'a,button,input,select,textarea,[role=button],.btn,.button,.el-button,li[onclick]';
let el = matchText(Array.from(root.querySelectorAll(sel1)), texts);
if (el) return el;
const sel2 = 'span[onclick],div[onclick],span.clickable,div.clickable';
return matchText(Array.from(root.querySelectorAll(sel2)), texts);
}
function clickByText(texts, root) {
const el = findByText(texts, root);
if (el) { log('点击:', (el.textContent || el.value || texts[0]).trim()); el.click(); return true; }
return false;
}
// ═══════════════════════════════════════════════════
// A. 浙江门户入口:登录态识别(v2.0 核心修复①)
// 规则(按泳杰酱需求):
// - 识别登录状态;未登录 → 先尝试点「登录」免验证直登,不行就静静等待手动登录
// - 只有【确认已登录】才跳 https://study.dongao.cn/study/index/index
// - 旧版盲等 5 秒就跳是死循环根源:没登上 → 被踢 guangdong → 又回入口 → 再跳…
// ═══════════════════════════════════════════════════
function jxjyProbe() {
if (!document.body) return 'loading';
// 首选硬信号:页面隐藏域 input#isLogin(2026-09-24 实测:'1'=已登录,页面同时有“退出登录”和用户名)
const flag = document.getElementById('isLogin');
if (flag) {
const v = (flag.value || '').trim();
if (v === '1') return 'in';
if (v === '0') return 'out';
}
// 兜底:文字启发式
const txt = document.body.innerText || '';
const hasLogout = /退出登录|个人中心|我的学习/.test(txt);
const loginEl = findByText(['登录', '登入', '登陆'], document);
if (hasLogout) return 'in';
if (loginEl) return 'out';
return 'unknown';
}
function onJxjyEntry() {
if (!loadState().running) return;
if (window.__dop_jxjy) return;
window.__dop_jxjy = true;
setStatus('🟡 浙江门户入口|登录态检测中…');
let clicked = 0, probes = 0;
const step = () => {
if (!loadState().running) { setStatus('🔴 已暂停'); return; }
const st = jxjyProbe();
setCur('登录状态:' + ({ in: '✅ 已登录', out: '❌ 未登录', unknown: '🔍 检测中', loading: '⏳ 页面加载中' })[st]);
if (st === 'in') {
// v2.0.6:不再裸跳 study 首页(冷访问会因浙江 study 会话过期被重定向到 hlj.dongao.cn 等区域落地页),
// 改走门户官方链 golearncenter → 选学校 → 东奥「继续学习」,重建浙江学习会话后自然进入学习
log('✅ 识别到已登录,走门户链重建学习会话');
setStatus('🟢 已登录,进入学习…');
startPortalEntry();
return;
}
if (st === 'out') {
if (clicked < 2) {
clicked++;
log('未登录,尝试点「登录」免验证直登(' + clicked + '/2)');
setStatus('🟡 未登录,尝试免验证登录…');
if (clickByText(['登录', '登入', '登陆'])) { setTimeout(step, 2500); return; }
}
probes++;
if (probes === 20) log('仍未登录,开始等待手动登录(页面持续检测,登上后自动跳转)');
setStatus('⏳ 等待登录(可手动完成登录)');
setTimeout(step, 3000);
return;
}
// unknown / loading:多探几次,别被“按钮还没渲染”误判
probes++;
if (probes <= 20) { setTimeout(step, 1500); return; }
log('登录态持续无法识别(探测30秒),放行到学习首页,由那边兜底裁决');
go(INDEX_URL);
};
log('当前:浙江门户入口,开始识别登录状态');
step();
}
// v2.0.6:门户链重建学习会话(替代裸跳 study 首页,防被重定向到 hlj 等区域页)
function currentYear() { try { return String(new Date().getFullYear()); } catch (e) { return '2026'; } }
function setStage(s) { writeJson('dongao18_stage', s); }
function clrStage() { try { localStorage.removeItem('dongao18_stage'); } catch (e) {} }
function startPortalEntry() {
log('走浙江门户链重建学习会话(golearncenter→选学校→东奥继续学习)');
setStage('golearn');
go('https://jxjy.czt.zj.gov.cn/front/golearncenterNew.html');
}
function onZjPortal() {
if (!loadState().running) return;
const u = location.href;
if (u.indexOf('/front/jxjy.html') !== -1) { onJxjyEntry(); return; }
// 网络继续教育学习页:点当年「继续学习」进选学校
if (u.indexOf('golearncenterNew') !== -1) {
setStatus('🟢 门户链|继续教育学习页');
let n = 0;
const go = () => {
if (!loadState().running) return;
const a = Array.from(document.querySelectorAll('a[href*="goSelectSchoolNew"]')).find(
x => { const t = (x.textContent || '').trim(); return t === '继续学习' || t.indexOf('继续学习') !== -1; }) || null;
if (a) { setStage('select'); log('点击「继续学习」进入选学校'); a.click(); return; }
if (++n <= 12) { setTimeout(go, 1500); return; }
log('未找到「继续学习」链接,直接进选学校页');
setStage('select');
go('https://jxjy.czt.zj.gov.cn/front/goSelectSchoolNew.html?syear=' + currentYear());
};
go();
return;
}
// 选学校页:点东奥「继续学习」(onclick=gonetschool(...))
if (u.indexOf('goSelectSchoolNew') !== -1) {
setStatus('🟢 门户链|选学校页');
let n = 0;
const go = () => {
if (!loadState().running) return;
const all = Array.from(document.querySelectorAll('a,button,span[onclick],div[onclick]'));
let el = all.find(x => (x.getAttribute('onclick') || '').indexOf('gonetschool') !== -1) || null;
if (!el) el = all.find(x => (x.textContent || '').trim() === '继续学习') || null;
if (el) { clrStage(); log('点击东奥「继续学习」进入学习(重建会话)'); el.click(); return; }
if (++n <= 12) { setTimeout(go, 1500); return; }
clrStage();
log('选学校页未找到「继续学习」,退回学习首页');
go(INDEX_URL);
};
go();
return;
}
// 门户其它子页:走学习链重建
log('处于门户子页,走学习链重建会话');
startPortalEntry();
}
// ═══ B. 学习首页:点「去学习」 ═══
function onIndex() {
if (!loadState().running) return;
log('当前:学习首页,自动点击「去学习」进入学习列表');
setStatus('🟢 运行中|学习首页');
let n = 0;
const go = () => {
if (!loadState().running) { setStatus('🔴 已暂停'); return; }
let el = document.querySelector('a[href*="/study/u/myCourse"]');
if (!el) {
const all = Array.from(document.querySelectorAll('a,button'));
el = all.find(x => { const t = (x.textContent || '').trim(); return t === '去学习' || t.indexOf('去学习') !== -1; }) || null;
}
if (el) {
log('点击「去学习」进入学习列表:', (el.getAttribute('href') || el.textContent.trim()));
setCur('正在进入学习列表…');
el.click();
} else if (++n <= 20) {
log('未找到「去学习」入口,1.5s 后重试(' + n + '/20)');
setTimeout(go, 1500);
} else {
log('未找到「去学习」入口,直接尝试课程列表地址兜底');
go(myCourseUrl());
}
};
go();
}
// ═══ 列表翻页小工具(课程列表/讲次列表通用)═══
function findNextPage() {
const els = Array.from(document.querySelectorAll('a,button,li'));
for (const el of els) {
const t = (el.textContent || '').trim();
if (t !== '下一页' && t !== '下页' && t !== '>' && t !== '»' && t !== '›') continue;
if (el.disabled) continue;
if (el.closest && el.closest('.disabled, .disabled a, [disabled]')) continue;
if (el.tagName === 'LI') { const a = el.querySelector('a'); return a || el; }
return el;
}
return document.querySelector('a.next, .pagination .next a, li.next > a') || null;
}
// ═══ C. 我的课程页:找第一门未完成的课(支持翻页)═══
// —— 学分总进度(v2.3.0):myCourse 表格累加 总学分/已学学分 ——
function parseCredits() {
try {
for (const tb of document.querySelectorAll('table')) {
const first = tb.querySelector('tr');
if (!first) continue;
const heads = Array.from(first.querySelectorAll('th,td')).map(x => (x.textContent || '').trim());
const ti = heads.findIndex(h => /总学分/.test(h));
const ei = heads.findIndex(h => /已学学分/.test(h));
if (ti === -1 || ei === -1) continue;
let total = 0, earned = 0, rows = 0;
for (const tr of tb.querySelectorAll('tr')) {
if (tr === first) continue;
const tds = Array.from(tr.querySelectorAll('td'));
if (tds.length <= Math.max(ti, ei)) continue;
if (/合计|总计|小计/.test(tr.textContent || '')) continue;
const tv = parseFloat(((tds[ti].textContent) || '').replace(/[^\d.]/g, ''));
const ev = parseFloat(((tds[ei].textContent) || '').replace(/[^\d.]/g, ''));
if (isFinite(tv)) { total += tv; rows++; }
if (isFinite(ev)) earned += ev;
}
if (rows > 0 && total > 0) {
return { total: Math.round(total * 100) / 100, earned: Math.round(earned * 100) / 100 };
}
}
} catch (e) { log('学分解析异常:', e.message); }
return null;
}
// v2.3.1:学分表格可能异步渲染,退避重试直到解析成功
let creditGen = 0;
function startCreditScanner() {
const gen = ++creditGen;
let n = 0;
const tryOnce = () => {
if (gen !== creditGen) return;
n++;
const cr = parseCredits();
if (cr) {
writeJson(CREDIT_KEY, cr);
setCredit();
log('📊 总进度统计:' + cr.earned + ' / ' + cr.total + ' 学分(' + (cr.earned / cr.total * 100).toFixed(1) + '%)');
return;
}
if (n <= 8) {
if (n === 1) log('学分表格未就绪,稍后重试…');
setTimeout(tryOnce, 500 * n);
} else {
const nT = document.querySelectorAll('table').length;
log('⚠️ 学分统计失败:重试' + (n - 1) + '次未取到数据(页面table数=' + nT + ')');
}
};
tryOnce();
}
function onMyCourse() {
// v2.3.0:进我的课程页就刷新学分总进度(暂停状态也统计)
startCreditScanner();
if (!loadState().running) return;
log('当前:我的课程页,寻找第一门未完成的课程');
setStatus('🟢 运行中|我的课程页');
let retries = 0, pages = 0;
const pick = () => {
if (!loadState().running) { setStatus('🔴 已暂停'); return; }
const btns = Array.from(document.querySelectorAll('a.operate-a'));
if (btns.length) {
recReset(); // 列表渲染成功 = 登录态健康,清异常恢复计数
retries = 0;
}
let target = null;
for (const b of btns) {
const t = (b.textContent || '').trim();
// 已完成课程按钮是「重新学习」,跳过;要学的是「开始学习 / 继续学习」
if (t === '开始学习' || t === '继续学习') { target = b; break; }
}
if (target) {
const title = target.closest('tr') ? target.closest('tr').innerText.replace(/\s+/g, ' ').slice(0, 60) : target.textContent.trim();
log('进入课程:', title);
saveBreak('进入课程:' + title);
setCur('课程:' + title);
setPg('');
target.click();
return;
}
// 当前页没有可学的 → 试试翻页
const next = findNextPage();
if (next && pages < 10) {
pages++;
log('本页无可学课程,翻到下一页(' + pages + ')');
setPg('已翻 ' + pages + ' 页继续找…');
next.click();
setTimeout(pick, 2000);
return;
}
if (++retries <= 20 && !btns.length) {
log('课程表格未渲染,1.5s 后重试(' + retries + '/20)');
setTimeout(pick, 1500);
return;
}
log('没有可学习的课程了,全部已完成 ✅');
setStatus('✅ 全部课程已完成');
};
pick();
}
// ═══ D. 课程目录页:找第一讲未学完的(支持翻页)═══
function onLectureList() {
if (!loadState().running) return;
try { sessionStorage.setItem('dongao_lecture_list', location.href); } catch (e) {}
log('当前:课程目录页,寻找第一讲未学完的');
setStatus('🟢 运行中|课程目录页');
setBar(null);
let retries = 0, pages = 0;
const pick = () => {
if (!loadState().running) { setStatus('🔴 已暂停'); return; }
const rows = Array.from(document.querySelectorAll('table.all tbody tr'));
let target = null, targetTitle = '', doneCount = 0;
for (const tr of rows) {
const done = tr.querySelector('span.learn-do');
const all = tr.querySelector('span.learn-all');
const studied = done ? done.textContent.trim() : '';
const total = all ? all.textContent.replace('/', '').trim() : '';
const btn = tr.querySelector('a.study-a');
if (!btn) continue;
if (studied && total && studied === total) { doneCount++; continue; }
target = btn;
const tl = tr.querySelector('.text-left a');
targetTitle = tl ? tl.textContent.trim() : '';
break;
}
if (target) {
// v2.0.3 防重入:刚学完但平台未确认完成的同一讲,不再重进(避免 99% 死循环)
const rd = readJson('dongao18_recentDone', null);
if (rd && (Date.now() - (rd.ts || 0)) < 5 * 60 * 1000) {
const hh = (target.getAttribute('href') || '') + ' ' + targetTitle;
const mm = hh.match(/lecture(ID|id)=(\d+)/);
if (mm && String(mm[2]) === String(rd.lec)) {
log('同一讲刚学完但平台未确认完成,视为已完成,返回课程列表换下一门(防死循环)');
setStatus('✅ 该讲已学完(防重入),返回课程列表');
saveBreak('防重入跳过讲次:' + targetTitle);
go(myCourseUrl());
return;
}
}
log('进入:', targetTitle || '(某讲)');
saveBreak('进入讲次:' + targetTitle);
setCur('讲次:' + targetTitle);
setPg('已学 ' + doneCount + ' 讲,正在进入下一讲');
target.click();
return;
}
const next = findNextPage();
if (next && pages < 10) {
pages++;
log('本页讲次已学完,翻到下一页(' + pages + ')');
next.click();
setTimeout(pick, 2000);
return;
}
if (++retries <= 20 && !rows.length) {
log('目录表格未就绪,1.5s 后重试(' + retries + '/20)');
setTimeout(pick, 1500);
return;
}
log('本课程所有讲已学完,返回我的课程');
setStatus('✅ 本课程已完成,返回课程列表');
go(myCourseUrl());
};
pick();
}
// ═══ E. 视频播放页:答题 + 放完检测 + 看门狗 + 会话保活 ═══
let quizGen = 0; // 答题循环代际(重入时作废旧循环)
let popupGen = 0; // 超时弹窗看门狗代际
let keepAliveTimer = null;
let videoLoopsStarted = false;
function onVideoPage() {
if (!loadState().running) return;
// 进入的是新讲次(非刚学完那讲)→ 清防重入标记
try {
const _m = location.href.match(/lecture(ID|id)=(\d+)/);
if (_m) {
const _rd = readJson('dongao18_recentDone', null);
if (_rd && String(_rd.lec) !== String(_m[2])) writeJson('dongao18_recentDone', null);
}
} catch (e) {}
log('当前:视频播放页,启动答题监听 + 放完检测 + 看门狗');
setStatus('🟢 运行中|视频播放页');
saveBreak('视频页');
startQuizWatcher();
startTimeoutPopupWatcher();
startKeepAlive();
if (!videoLoopsStarted) {
videoLoopsStarted = true;
setupEndDetector();
startVideoWatchdog();
}
}
// —— 答题(v2.0 加固:任何分支都续跑,.box-sure 缺失不炸环)——
function startQuizWatcher() {
const gen = ++quizGen;
detect();
function schedule() { setTimeout(detect, 5000); }
function detect() {
if (gen !== quizGen) return; // 已被新循环接管
if (!loadState().running) { schedule(); return; } // 暂停≠死掉,恢复后自动续跑
try {
if (document.querySelector('.lister_pop_box')) Checkanswer();
else schedule();
} catch (e) {
log('答题检测异常:', e.message);
schedule();
}
}
function Checkanswer() {
console.log('Checkanswer功能调用成功');
let answered = false;
try {
const ansEl = document.querySelector('.pop-right-ans');
const answers = ((ansEl && ansEl.getAttribute('value')) || '').split('');
const CheckList1 = document.querySelectorAll('.sub_radio_bg');
const CheckList2 = document.querySelectorAll('.sub_seclet_bg');
const list = CheckList1.length ? CheckList1 : (CheckList2.length ? CheckList2 : null);
if (list && answers.length) {
log(list === CheckList1 ? '检测到答题框,自动作答(单选)' : '检测到答题框,自动作答(多选)');
for (let i = 0; i < list.length; i++) {
for (let j = 0; j < answers.length; j++) {
if (list[i].getAttribute('value') === answers[j]) {
list[i].click();
console.log('点击' + list[i].getAttribute('value'));
}
}
}
answered = true;
}
} catch (e) {
log('未提取到答案:', e.message);
}
// 无论是否提取到答案,都点「确定」推进弹窗(500ms 让选项先落状态)
setTimeout(() => {
try {
const sure = document.querySelector('.box-sure');
if (sure) { sure.click(); log(answered ? '已作答并提交 ✔' : '未提取到答案,已直接点确定 ✔'); }
else log('未找到「确定」按钮,跳过');
} catch (e) { log('点确定异常:', e.message); }
schedule();
}, 500);
}
}
// —— 超时弹窗看门狗(v2.1.1:「听课页面已超时」→自动点「退出听课」)——
function startTimeoutPopupWatcher() {
const gen = ++popupGen;
detect();
function schedule() { setTimeout(detect, 3000); }
function detect() {
if (gen !== popupGen) return;
if (!loadState().running) { schedule(); return; }
try {
if (!findTimeoutPopup(document)) {
const frames = document.querySelectorAll('iframe');
for (const f of frames) {
try { const d = f.contentDocument; if (d && findTimeoutPopup(d)) break; } catch (e) {}
}
}
} catch (e) {
log('超时弹窗检测异常:', e.message);
}
schedule();
}
function findTimeoutPopup(root) {
const marker = [...root.querySelectorAll('div,span,p,h3,h4')].find(el => {
const t = (el.textContent || '').trim();
return t && t.length < 60 && /听课页面已超时|已超时[,,]?\s*请重新进入/.test(t);
});
if (!marker) return false;
// 实测:点「退出听课」会直接关闭网页。
// 正确做法:不点任何按钮,直接走恢复链回浙江门户入口重进。
log('⚠️ 检测到听课超时弹窗(不点退出,直接走恢复链重进)');
setTimeout(() => goRecover('听课超时'), 500);
return true;
}
}
// —— 视频查找/播放辅助 ——
function findVideo() {
const all = Array.from(document.querySelectorAll('video'));
if (!all.length) return null;
let best = null, bestScore = -1;
for (const v of all) {
let score = v.readyState; // 0~4
try { if (isFinite(v.duration) && v.duration > 0) score += 10; } catch (e) {}
try { if (!v.paused) score += 5; } catch (e) {}
if (score > bestScore) { bestScore = score; best = v; }
}
return best;
}
function nudgePlay(video) {
// 优先驱动 video.js 播放器实例,让 UI 状态同步(否则媒体在播、界面却停在"黑屏+播放按钮")
try {
const vj = document.querySelector('.video-js');
const player = vj && (vj.player || (window.videojs && window.videojs.getPlayer && window.videojs.getPlayer(vj.id)));
if (player && typeof player.play === 'function') {
player.play();
try { if (typeof player.muted === 'function') player.muted(true); } catch (e2) {}
}
} catch (e) {}
try { const p = video.play(); if (p && p.catch) p.catch(() => {}); } catch (e) {}
try {
const btn = document.querySelector('.vjs-big-play-button');
if (btn) btn.click();
} catch (e) {}
}
// v2.0.7:黑屏自愈——video 无数据(readyState<2 / 元数据未就绪)时,分步升级触发手段,
// 主动重新拉起 hls 拉流,替代“只 play() 无效 + 傻等 90s 死循环刷新”。
// 触发手段从弱到强:① 重新绑定 hls 源(player.src) ② video.load() 强刷 ③ 点大播放按钮。
let _bsTries = 0; // 本轮黑屏已尝试次数(有数据后归零)
// v2.0.9:真实黑屏状态快照,写进 localStorage 历史日志(不被外部探查掩盖)
function snapBlack(video, tag) {
const rec = {
ts: Date.now(), t: new Date().toTimeString().slice(0, 8), tag,
readyState: video ? video.readyState : null,
networkState: video ? video.networkState : null,
errorCode: video && video.error ? video.error.code : null,
errorMsg: video && video.error ? String(video.error.message).slice(0, 80) : '',
dur: video && isFinite(video.duration) ? Math.round(video.duration) : null,
cur: video ? Math.round(video.currentTime) : null,
paused: video ? video.paused : null,
muted: video ? video.muted : null,
preload: video ? video.preload : null,
src: (video && video.currentSrc || '').slice(0, 50),
hasEME: !!(window.navigator && navigator.requestMediaKeySystemAccess)
};
try {
let arr = []; try { arr = JSON.parse(localStorage.getItem('dop_blacklog') || '[]'); } catch (e) {}
arr.push(rec); if (arr.length > 30) arr = arr.slice(-30);
localStorage.setItem('dop_blacklog', JSON.stringify(arr));
} catch (e) {}
return rec;
}
function healBlackScreen(video) {
_bsTries++;
snapBlack(video, 'heal' + _bsTries);
const step = _bsTries;
if (step === 1) {
// 尝试1:快速重绑源+点播放(不刷新,尽量保住当前播放上下文)
log('黑屏自愈(1):重绑播放源…');
try {
const vj = document.querySelector('.video-js');
const player = vj && (vj.player || (window.videojs && window.videojs.getPlayer && window.videojs.getPlayer(vj.id)));
if (player) {
if (typeof player.muted === 'function') player.muted(true);
if (typeof player.play === 'function') player.play();
const src = (typeof player.currentSrc === 'function' ? player.currentSrc() : '') || (video && video.currentSrc) || '';
if (src && typeof player.src === 'function') { player.src(src); player.play(); }
}
} catch (e) {}
try { const btn = document.querySelector('.vjs-big-play-button'); if (btn) btn.click(); } catch (e3) {}
return;
}
// 尝试2+:重绑源无效(hls.js 拉流引擎已死,重设 src 不重建)→ 刷新页面重建播放器/拉流(实测有效)
log('黑屏自愈(' + step + '):重绑源无效,刷新页面重建…');
// 防死循环:同一讲次连续刷新 3 次仍黑屏 → 判定该视频源持续故障,暂停求助
let rc = { lec: '', n: 0 };
try { rc = JSON.parse(localStorage.getItem('dop_bs_reloads') || '{"lec":"","n":0}'); } catch (e) { rc = { lec: '', n: 0 }; }
const m = location.href.match(/lecture(ID|id)=(\d+)/);
const lec = m ? m[2] : '';
if (rc.lec !== lec) { rc = { lec: lec, n: 1 }; } else { rc.n++; }
try { localStorage.setItem('dop_bs_reloads', JSON.stringify(rc)); } catch (e) {}
if (rc.n >= 3) {
// 同一讲次连续刷新 3 次仍黑屏 = 视频源持续故障 → 跳过本讲次,去下一个(自动推进不卡死)
log('⚠️ 该讲次连续 ' + rc.n + ' 次刷新仍黑屏,视频源故障,跳过本讲次');
setStatus('🟠 视频源故障,跳过本讲次');
try { sessionStorage.removeItem('dop_fail_cnt'); } catch (e) {}
try { localStorage.removeItem('dop_bs_reloads'); } catch (e) {}
setTimeout(goBackToLecture, 500); // 回课程列表/我的课程,自动去下一个未学讲次
return;
}
setTimeout(function () { location.reload(); }, 800);
}
// v2.0.4:媒体在播但播放器 UI 仍停在暂停态(vjs-paused)时,同步回 playing,消除"黑屏假象"
let _syncLogged = false;
function syncPlayerUI(video) {
try {
const vj = document.querySelector('.video-js');
if (!vj) return;
const paused = vj.className.indexOf('vjs-paused') !== -1;
if (!paused) return;
// v2.0.7:仅当媒体确有好数据可播(readyState>=2)才同步 UI;
// readyState<2 = 真黑屏无数据,play() 对 MSE 无效,交由黑屏自愈 healBlackScreen 处理,不再误报“已同步”
if (!(video && video.readyState >= 2)) return;
const player = vj.player || (window.videojs && window.videojs.getPlayer && window.videojs.getPlayer(vj.id));
if (player && typeof player.play === 'function') player.play();
const btn = document.querySelector('.vjs-big-play-button');
if (btn) btn.click();
if (!_syncLogged) { _syncLogged = true; log('已同步播放器 UI(媒体在播但界面停在暂停,已恢复播放显示)'); }
} catch (e) {}
}
// —— 放完检测 + 进度可视化 ——
function setupEndDetector() {
let bound = false, finished = false;
const t0 = Date.now();
const bind = (video) => {
if (bound) return;
bound = true;
let durLogged = false; // v2.0.2:绑定瞬间元数据可能未就绪,等首次拿到时长再补一条真实时长
let nearEndSince = null; // v2.0.3:末尾兜底(ended 未触发时用)
try { sessionStorage.removeItem('dop_fail_cnt'); } catch (e) {}
nudgePlay(video);
syncPlayerUI(video);
const finish = () => {
if (finished) return;
finished = true;
if (!loadState().running) { setStatus('🔴 已暂停,不自动切课'); return; }
if (BLACKFIX) {
// v2.0.9 黑屏修复模式:学完不切课,停留当前视频专注诊断黑屏
log('🔧 黑屏修复模式:本节已看完,不切课,停留当前视频(等待手动重置)');
setStatus('🔧 修复模式:已看完,不切课');
return;
}
markDone();
log('本节播放结束,等待 3 秒让进度落库后返回目录');
setStatus('🟢 本节完成,返回目录…');
setTimeout(goBackToLecture, 3000);
};
video.addEventListener('ended', finish, { once: true });
try {
if (window.videojs) {
document.querySelectorAll('.video-js').forEach(pe => {
const player = pe.player;
if (player && player.on) player.on('ended', finish);
});
}
} catch (e) {}
const dur = (isFinite(video.duration) && video.duration > 0) ? Math.round(video.duration) + ' 秒' : '(元数据加载中)';
log('video 已绑定,时长', dur);
recReset(); // 播放器出流成功 = 会话健康
if (DEBUG) { log('[DEBUG] 3 秒后模拟学完'); setTimeout(finish, 3000); }
setInterval(() => {
if (!loadState().running || finished) return;
const v = findVideo();
if (!v) return;
if (v.paused) nudgePlay(v); // 媒体掉了就拉起来
syncPlayerUI(v); // 媒体在播但播放器 UI 停在暂停时同步(去黑屏假象)
let pct = 0, hasDur = false;
if (isFinite(v.duration) && v.duration > 0) {
hasDur = true;
pct = (v.currentTime / v.duration) * 100;
if (!durLogged) { durLogged = true; log('video 时长已就绪:' + Math.round(v.duration) + ' 秒'); }
} else {
const pp = document.querySelector('.vjs-play-progress');
if (pp) pct = parseFloat(pp.style.width) || 0;
}
setBar(pct);
setPg(hasDur ? ('当前进度 ' + pct.toFixed(0) + '%') : '等待视频元数据加载…');
// v2.0.3:学完判定以原生 ended 事件为准(真播完才触发)。旧版用 pct>=99 判定,会在 99% 就切课,
// 平台未标记学完 → 返回目录又重进同一讲 → 卡在 99% 死循环。这里只作兜底:
// 末尾(pct>=99.9 且 currentTime 距 duration≤5s)连续 15 秒不再前进才判学完。
if (hasDur) {
if (pct >= 99.9 && v.currentTime >= (v.duration - 5)) {
if (nearEndSince === null) nearEndSince = Date.now();
if (Date.now() - nearEndSince > 15000) {
log('接近末尾且播放停滞 15 秒,判为学完(兜底)');
finish();
return;
}
} else {
nearEndSince = null;
}
}
}, 3000);
};
const tryBind = () => {
if (bound) return;
const video = findVideo();
if (video) bind(video);
else if (Date.now() - t0 < 120000) setTimeout(tryBind, 3000); // 找不到就等;看门狗负责超时兜底
};
tryBind();
}
// —— 看门狗(v2.0 核心修复④:黑屏/出流失败不再“强绑死 video”等死)——
function startVideoWatchdog() {
const t0 = Date.now();
let lastTime = -1, stallSince = 0, pausedSince = 0, healTried = false; // v2.0.5 冻结自愈
setInterval(() => {
if (!loadState().running) return;
// 多开限制提示(服务端拒绝出流的直接证据)
const txt = (document.body && document.body.innerText) || '';
if (/不能同时学习|同时学习多个|多个视频/.test(txt)) {
log('⚠️ 检测到“同时学习多个视频”类限制提示(多开被服务端拒绝)');
saveBreak('多开限制提示');
goRecover('多开限制提示');
return;
}
const v = findVideo();
if (!v) {
if (Date.now() - t0 > 120000) failWatchdog('页面上始终没有 video 元素');
return;
}
if (v.error || v.networkState === 3 /* NETWORK_NO_SOURCE */) {
failWatchdog('video 加载失败(error=' + (v.error ? v.error.code : '-') + ')');
return;
}
const hasDur = isFinite(v.duration) && v.duration > 0;
if (!hasDur) {
// v2.0.10:黑屏(元数据未就绪/无数据)→ 15s 尝试1(重绑源) → 45s 尝试2(刷新页面重建,实测有效)
const el = Date.now() - t0;
if (el > 15000 && _bsTries === 0) healBlackScreen(v);
else if (el > 45000 && _bsTries <= 1) healBlackScreen(v);
if (el > 100000) failWatchdog('video 元数据 100 秒未就绪(黑屏,自愈无效)');
return;
}
// 已出流:查播放是否真的在走;有数据后重置黑屏自愈计数
_bsTries = 0;
if (v.paused) {
if (!pausedSince) pausedSince = Date.now();
else if (Date.now() - pausedSince > 120000) { pausedSince = 0; nudgePlay(v); }
} else {
pausedSince = 0;
syncPlayerUI(v); // v2.0.4:媒体在播但 UI 停在暂停时同步
if (v.currentTime === lastTime) {
// v2.0.5:currentTime 冻结(可能被后台/遮挡节流挂起)→ 先 nudgePlay 自愈,无效再走恢复
if (!stallSince) stallSince = Date.now();
const fz = Date.now() - stallSince;
if (fz > 40000 && !healTried) { healTried = true; nudgePlay(v); log('检测到播放冻结(约' + Math.round(fz / 1000) + 's未推进),已重新触发播放自愈'); }
if (fz > 120000) { stallSince = 0; healTried = false; failWatchdog('播放冻结 2 分钟(重播无效)'); }
} else { stallSince = 0; healTried = false; lastTime = v.currentTime; }
}
}, 15000);
}
function failWatchdog(reason) {
let cnt = 1;
try { cnt = (Number(sessionStorage.getItem('dop_fail_cnt')) || 0) + 1; sessionStorage.setItem('dop_fail_cnt', String(cnt)); } catch (e) {}
saveBreak('播放异常:' + reason);
if (cnt <= 1) {
log('播放异常:' + reason + ' → 刷新页面重试(1/2)');
setStatus('🟡 播放异常,刷新重试…');
setTimeout(() => location.reload(), 1500);
} else {
log('播放异常:' + reason + ' → 刷新无效,走登录恢复链重新进入');
goRecover('视频加载失败:' + reason);
}
}
// v2.0.3:记录刚真正学完的讲次,防止返回目录时平台未确认完成导致重进死循环
function markDone() {
try {
const m = location.href.match(/lecture(ID|id)=(\d+)/);
if (!m) return;
const c = location.href.match(/cw(ID|id)=(\d+)/) || location.href.match(/trainId=(\d+)/);
writeJson('dongao18_recentDone', { cw: c ? c[1] : '', lec: m[2], ts: Date.now() });
log('已记录学完讲次 ' + m[2] + '(防重入)');
} catch (e) {}
}
function goBackToLecture() {
if (!loadState().running) { setStatus('🔴 已暂停,不自动切课'); return; }
// 有讲次列表记录 → 回列表继续下一讲;没有(单视频课)→ 回我的课程
let back = null;
try { back = sessionStorage.getItem('dongao_lecture_list'); } catch (e) {}
if (back) {
log('返回课程目录(继续下一讲):', back);
go(back);
} else {
log('无列表记录(单视频课程),退回我的课程页');
go(myCourseUrl());
}
}
// ═══ 会话保活:防长时间挂视频 idle 掉线(保留,另加当前域轻触)═══
const KEEPALIVE_MS = 5 * 60 * 1000;
function keepAliveOnce() {
if (!loadState().running) return;
const hhmm = new Date().toTimeString().slice(0, 5);
const targets = [INDEX_URL + '?_ka=' + Date.now()];
try { targets.push(location.origin + '/?_ka=' + Date.now()); } catch (e) {}
targets.forEach((url, i) => {
try {
fetch(url, { mode: 'no-cors', credentials: 'include', cache: 'no-store' })
.then(() => { if (i === 0) log('会话保活 ✔ 已轻触学习页 ' + hhmm); })
.catch(() => {
try { const im = new Image(); im.src = url; } catch (e) {}
});
} catch (e) {
try { const im = new Image(); im.src = url; } catch (e2) {}
}
});
}
function startKeepAlive() {
if (keepAliveTimer) return;
log('启动会话保活:每 ' + (KEEPALIVE_MS / 60000) + ' 分钟轻触学习页,防 idle 掉线');
keepAliveTimer = setInterval(keepAliveOnce, KEEPALIVE_MS);
}
// ═══ 路由 ═══
let midPageTries = 0;
function main() {
// v2.2.0:URL 携带 dop_paused=1 时,保持暂停并清掉参数
try {
// v2.3.0:接收学分总进度并缓存到本域
const cm = (location.hash || '').match(/dop_credit=([\d.]+)\/([\d.]+)/);
if (cm) {
const t = parseFloat(cm[1]), e = parseFloat(cm[2]);
if (isFinite(t) && isFinite(e) && t > 0) writeJson(CREDIT_KEY, { total: t, earned: e });
}
const hm = (location.hash || '').match(/dop_paused=1|dop_run=1/);
if (hm) {
const st = loadState();
const wantRun = hm[0] === 'dop_run=1';
if (st.running !== wantRun) {
st.running = wantRun; saveState(st);
log(wantRun ? '▶ 跨页恢复运行(由上一页状态同步)' : '⏸ 跨页保持暂停(由上一页状态同步)');
}
const h = location.hash.replace(/[#&](dop_paused=1|dop_run=1|dop_credit=[\d.]+\/[\d.]+)/g, '').replace(/^#&?/, '');
history.replaceState(null, '', location.pathname + location.search + (h ? '#' + h : ''));
}
} catch (e) {}
const u = location.href;
const host = location.hostname;
const s = loadState();
// 登录续期中间页:等待自动跳转(带次数上限,防卡死)
if (u.indexOf('loginDeal') !== -1 || u.indexOf('/authority/') !== -1) {
if (++midPageTries > 40) { midPageTries = 0; goRecover('登录续期中间页卡住'); return; }
log('处于登录续期中间页,等待自动跳转…(' + midPageTries + '/40)');
setStatus('🟡 登录续期中…');
setTimeout(main, 1500);
return;
}
// v2.0.8:浙里办(浙江政务服务)登录页 = 真的需要重新登录的强信号。
// 在此页直接暂停并提示等待登录,绝不跳转(否则打断用户登录操作)。
// 登入成功后页面自动跳回 jxjy 门户(不同 origin,localStorage 隔离),
// 那边 loadState() 默认 running=true → 自动恢复继续,正好符合“登入后自动开始”。
if (host.indexOf('zjzwfw.gov.cn') !== -1) {
const sz = loadState();
if (sz.running) { sz.running = false; saveState(sz); }
setStatus('🔴 浙里办登录页:请完成登录');
setCur('登录后自动跳回并恢复自动学习');
addLog('检测到浙里办登录页,判定需要重新登录,已暂停等待登录');
refreshPanel();
return;
}
// 浙江门户:统一在此识别登录状态
if (host === 'jxjy.czt.zj.gov.cn') { onZjPortal(); return; }
// 学习域之外的东奥页面(guangdong.dongao.cn 等落地页)= 会话已死 → 恢复链
const isLearnHost = LEARN_HOSTS.indexOf(host) !== -1;
if (!isLearnHost && host.indexOf('dongao.cn') !== -1) {
if (!s.running) { setStatus('🔴 已暂停'); return; }
log('落在非学习页(' + host + '),判定会话中断');
setStatus('🟡 会话中断,正恢复…');
goRecover('落在' + host);
return;
}
if (u.indexOf('/videoShow/video/videoPlay') !== -1) onVideoPage();
else if (u.indexOf('/lecture/lectureList') !== -1) onLectureList();
else if (u.indexOf('/study/u/myCourse') !== -1) onMyCourse();
else if (u.indexOf('/study/index') !== -1) onIndex();
else {
// 学习域内的未知页(跳板/过渡页):等 4 秒看会不会自动跳,不会就回学习首页
log('学习域未知页面,等待自动跳转:', u);
setStatus('🟡 过渡页,等待跳转…');
setTimeout(() => {
if (!loadState().running) return;
if (location.href === u) {
log('4 秒未自动跳转,走门户链重建会话');
startPortalEntry();
}
}, 4000);
}
}
// ═══ 启动 ═══
try {
createPanel();
const s0 = loadState();
if (s0.running) {
setStatus('🟢 运行中');
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => { createPanel(); main(); });
} else {
main();
}
} else {
setStatus('🔴 已暂停(点“开始”继续)');
setCur('脚本已加载,但处于暂停状态');
addLog('已暂停。点击「▶ 开始」从当前页继续自动学习。');
}
} catch (e) {
console.error('[东奥2.0] 初始化失败:', e);
try {
const el = document.getElementById('dop-log');
if (el) el.textContent += '\n❌ 初始化失败:' + e.message + '\n请截图给开发者';
} catch (_) {}
}
})();