// ==UserScript== // @name 荟智学园学习助手 // @namespace https://scriptcat.org/users/令狐不太冲 // @version 1.2.4 // @description 荟智学园增强:自动播放、1~16倍速、自动下一节;秒过本节/整课/我的班级全部课程;「我的考试」批量自动作答交卷 // @author 令狐不太冲 // @license MIT // @icon https://gips3.baidu.com/it/u=2085307030,2248755990&fm=3030&app=3030&size=re3,2&q=75&n=0&g=4n&f=JPEG&fmt=auto&maxorilen2heic=2000000?s=E0332572F0E506B149B7A5C7020070A2 // @match https://hz.baosteel.com/* // @match https://examhz.baosteel.com/* // @run-at document-idle // @grant none // ==/UserScript== (function () { 'use strict'; if (window.__HZ_HELPER_LOADED__) return; window.__HZ_HELPER_LOADED__ = true; /* ============================ 配置 ============================ */ const API_BASE = 'https://apihz.baosteel.com'; const CFG_KEY = 'hz_helper_cfg_v1'; const LOG_MAX = 200; const DEFAULT_CFG = { autoPlay: true, // 自动播放(静音绕过浏览器自动播放限制) autoNext: true, // 播完自动下一节 instant: false, // 秒过模式:进入某节即上报完成并切下一节 rate: 2, // 倍速 sync: true // 按播放位置周期性同步进度(让倍速真正生效) }; let CFG = Object.assign({}, DEFAULT_CFG); try { Object.assign(CFG, JSON.parse(localStorage.getItem(CFG_KEY) || '{}') || {}); } catch (e) { /* ignore */ } function saveCfg() { try { localStorage.setItem(CFG_KEY, JSON.stringify(CFG)); } catch (e) { /* ignore */ } } /* ============================ 状态 ============================ */ const ST = { courseId: null, trainId: null, courseName: '', resId: null, res: null, list: [], idx: -1, record: null, video: null, busy: false, bulkRunning: false, handledResId: '', retryCount: 0, lastNavAt: 0, rateTimer: null, syncTimer: null, keepTimer: null, statusTimer: null, panel: null, logEl: null, statusEl: null, logLines: [] }; /* ============================ 工具 ============================ */ const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); function nowStr() { const d = new Date(); const p = (n) => String(n).padStart(2, '0'); return p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds()); } function fmtTime(sec) { sec = Math.max(0, Math.round(Number(sec) || 0)); const h = Math.floor(sec / 3600); const m = Math.floor((sec % 3600) / 60); const s = sec % 60; const p = (n) => String(n).padStart(2, '0'); return (h > 0 ? h + ':' : '') + p(m) + ':' + p(s); } function hashQuery(key) { const h = location.hash || ''; const m = h.match(new RegExp('[?&]' + key + '=([^&]*)')); return m ? decodeURIComponent(m[1]) : null; } function isResourcePage() { return (location.hash || '').indexOf('/resourceInfo') > -1 && !!hashQuery('id'); } /* ============================ 日志 / 面板 ============================ */ const COLORS = { info: '#9aa4b2', ok: '#4ade80', warn: '#fbbf24', err: '#f87171', run: '#60a5fa' }; function log(msg, type) { type = type || 'info'; const text = '[' + nowStr() + '] ' + msg; try { ST.logLines.push(text); if (ST.logLines.length > 600) ST.logLines.splice(0, ST.logLines.length - 600); } catch (e) { /* ignore */ } const line = document.createElement('div'); line.style.cssText = 'color:' + (COLORS[type] || COLORS.info) + ';line-height:1.55;word-break:break-all;'; line.textContent = text; if (ST.logEl) { ST.logEl.appendChild(line); while (ST.logEl.children.length > LOG_MAX) ST.logEl.removeChild(ST.logEl.firstChild); ST.logEl.scrollTop = ST.logEl.scrollHeight; } console.log('%c[荟智助手] ' + msg, 'color:' + (COLORS[type] || '#888')); } async function copyText(text) { try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text); return true; } } catch (e) { /* 回退到 execCommand */ } try { const ta = document.createElement('textarea'); ta.value = text; ta.style.cssText = 'position:fixed;left:-9999px;top:0;opacity:0;'; document.body.appendChild(ta); ta.select(); const ok = document.execCommand('copy'); ta.remove(); return ok; } catch (e) { return false; } } function buildPanel() { if (ST.panel) return; const root = document.createElement('div'); root.id = 'hz-helper-panel'; root.style.cssText = [ 'position:fixed', 'right:16px', 'bottom:16px', 'width:302px', 'z-index:2147483000', 'background:#111827', 'border:1px solid #2b3547', 'border-radius:10px', 'box-shadow:0 10px 34px rgba(0,0,0,.45)', 'color:#d1d5db', 'font:13px/1.5 "Microsoft YaHei",system-ui,sans-serif', 'overflow:hidden', 'user-select:none' ].join(';'); const hd = document.createElement('div'); hd.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:9px 12px;cursor:move;background:linear-gradient(90deg,#1f2937,#111827);border-bottom:1px solid #2b3547;'; hd.innerHTML = '荟智学园助手 v1.2.4' + '' + '—' + ''; const bd = document.createElement('div'); bd.id = 'hz-body'; bd.style.cssText = 'padding:10px 12px 12px;'; const rowStyle = 'display:flex;align-items:center;justify-content:space-between;margin-bottom:7px;'; const chkStyle = 'width:16px;height:16px;accent-color:#3b82f6;cursor:pointer;'; function makeRow(labelText, checked, onChange, id) { const row = document.createElement('div'); row.style.cssText = rowStyle; const lab = document.createElement('label'); lab.textContent = labelText; lab.style.cssText = 'color:#cbd5e1;cursor:pointer;'; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.id = id; cb.checked = !!checked; cb.style.cssText = chkStyle; cb.addEventListener('change', () => onChange(cb.checked)); lab.addEventListener('click', () => { cb.checked = !cb.checked; onChange(cb.checked); }); row.appendChild(lab); row.appendChild(cb); bd.appendChild(row); return cb; } makeRow('自动播放(静音)', CFG.autoPlay, (v) => { CFG.autoPlay = v; saveCfg(); if (v) tryPlay(); }, 'hz-auto-play'); makeRow('播完自动下一节', CFG.autoNext, (v) => { CFG.autoNext = v; saveCfg(); }, 'hz-auto-next'); makeRow('秒过模式(进入即完成)', CFG.instant, (v) => { CFG.instant = v; saveCfg(); if (v) { ST.handledResId = ''; if (!ST.res) loadContext(); else scheduleInstant(); } }, 'hz-instant'); makeRow('按播放位置同步进度', CFG.sync, (v) => { CFG.sync = v; saveCfg(); }, 'hz-sync'); // 倍速 const rateRow = document.createElement('div'); rateRow.style.cssText = rowStyle; rateRow.innerHTML = ''; const sel = document.createElement('select'); sel.id = 'hz-rate'; sel.style.cssText = 'background:#1f2937;color:#e5e7eb;border:1px solid #374151;border-radius:5px;padding:2px 6px;font-size:12px;outline:none;cursor:pointer;'; [1, 1.25, 1.5, 2, 3, 4, 8, 16].forEach((r) => { const o = document.createElement('option'); o.value = String(r); o.textContent = '×' + r; if (Number(CFG.rate) === r) o.selected = true; sel.appendChild(o); }); sel.addEventListener('change', () => { CFG.rate = Number(sel.value) || 1; saveCfg(); applyRate(true); log('倍速设为 ×' + CFG.rate, 'run'); }); rateRow.appendChild(sel); bd.appendChild(rateRow); // 按钮 const btns = document.createElement('div'); btns.style.cssText = 'display:flex;gap:8px;margin:10px 0 8px;'; const btnStyle = 'flex:1;padding:7px 0;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;color:#fff;'; const bPass = document.createElement('button'); bPass.id = 'hz-pass'; bPass.textContent = '秒过本节'; bPass.style.cssText = btnStyle + 'background:#2563eb;'; bPass.addEventListener('click', () => runPassCurrent(true)); const bAll = document.createElement('button'); bAll.id = 'hz-pass-all'; bAll.textContent = '秒过整课'; bAll.style.cssText = btnStyle + 'background:#7c3aed;'; bAll.addEventListener('click', () => runPassCourse()); btns.appendChild(bPass); btns.appendChild(bAll); bd.appendChild(btns); // 全账号秒过 const row2 = document.createElement('div'); row2.style.cssText = 'display:flex;gap:8px;margin:0 0 8px;'; const bAllAcc = document.createElement('button'); bAllAcc.id = 'hz-pass-all-account'; bAllAcc.textContent = '秒过所有'; bAllAcc.title = '仅处理「我的班级」下所有未完成的课程,逐门秒过,无需打开学习页'; bAllAcc.style.cssText = btnStyle + 'flex:2;background:#dc2626;'; bAllAcc.addEventListener('click', () => runPassAll()); const bStopAll = document.createElement('button'); bStopAll.id = 'hz-pass-stop'; bStopAll.textContent = '停止'; bStopAll.style.cssText = btnStyle + 'flex:1;background:#4b5563;opacity:.5;'; bStopAll.disabled = true; bStopAll.addEventListener('click', () => { ALL.stop = true; log('已发出停止指令…', 'warn'); }); row2.appendChild(bAllAcc); row2.appendChild(bStopAll); bd.appendChild(row2); // 我的考试(批量自动作答) const row3 = document.createElement('div'); row3.style.cssText = 'display:flex;gap:8px;margin:0 0 8px;'; const bExam = document.createElement('button'); bExam.id = 'hz-my-exam'; bExam.textContent = '我的考试(自动作答)'; bExam.title = '读取「我的学习 → 我的考试」,勾选后逐场自动作答并交卷'; bExam.style.cssText = btnStyle + 'flex:1;background:#0d9488;'; bExam.addEventListener('click', () => openExamModal()); const bDiag = document.createElement('button'); bDiag.id = 'hz-diag'; bDiag.textContent = '诊断'; bDiag.title = '把当前课程的资源与平台记录打印到日志,排查「上报了但未判定完成」'; bDiag.style.cssText = btnStyle + 'flex:0 0 72px;background:#475569;'; bDiag.addEventListener('click', () => runDiagnose()); row3.appendChild(bExam); row3.appendChild(bDiag); bd.appendChild(row3); // 状态 const status = document.createElement('div'); status.id = 'hz-status'; status.style.cssText = 'background:#0b1220;border:1px solid #1f2937;border-radius:6px;padding:7px 9px;font-size:12px;color:#9ca3af;line-height:1.7;margin-bottom:8px;white-space:pre-line;user-select:text;-webkit-user-select:text;'; status.textContent = '等待进入具体学习页…'; bd.appendChild(status); // 日志工具条 const logBar = document.createElement('div'); logBar.style.cssText = 'display:flex;gap:6px;justify-content:flex-end;margin:0 0 5px;'; const tinyBtn = 'padding:3px 8px;border:none;border-radius:5px;font-size:11px;font-weight:600;cursor:pointer;color:#e5e7eb;background:#334155;'; const bCopy = document.createElement('button'); bCopy.id = 'hz-copy-log'; bCopy.type = 'button'; bCopy.textContent = '复制日志'; bCopy.title = '把日志区内容一键复制到剪贴板'; bCopy.style.cssText = tinyBtn; bCopy.addEventListener('click', async () => { const txt = (ST.logLines || []).join('\n') || '(暂无日志)'; const ok = await copyText(txt); bCopy.textContent = ok ? '已复制' : '复制失败'; setTimeout(() => { bCopy.textContent = '复制日志'; }, 1500); }); const bClear = document.createElement('button'); bClear.id = 'hz-clear-log'; bClear.type = 'button'; bClear.textContent = '清空'; bClear.title = '清空日志区'; bClear.style.cssText = tinyBtn + 'background:#1f2937;'; bClear.addEventListener('click', () => { ST.logLines = []; if (ST.logEl) ST.logEl.innerHTML = ''; bClear.textContent = '已清空'; setTimeout(() => { bClear.textContent = '清空'; }, 1200); }); logBar.appendChild(bCopy); logBar.appendChild(bClear); bd.appendChild(logBar); // 日志 const lg = document.createElement('div'); lg.id = 'hz-log'; lg.style.cssText = 'height:98px;overflow-y:auto;background:#0b1220;border:1px solid #1f2937;border-radius:6px;padding:6px 8px;font-size:11.5px;user-select:text;-webkit-user-select:text;cursor:text;'; bd.appendChild(lg); root.appendChild(hd); root.appendChild(bd); document.body.appendChild(root); ST.panel = root; ST.logEl = lg; ST.statusEl = status; // 折叠 let folded = false; hd.querySelector('#hz-fold').addEventListener('click', (ev) => { ev.stopPropagation(); folded = !folded; bd.style.display = folded ? 'none' : 'block'; hd.querySelector('#hz-fold').textContent = folded ? '+' : '—'; }); // 拖动 let dragging = false, sx = 0, sy = 0, ox = 0, oy = 0; hd.addEventListener('mousedown', (ev) => { if (ev.target.id === 'hz-fold') return; dragging = true; const rect = root.getBoundingClientRect(); sx = ev.clientX; sy = ev.clientY; ox = rect.left; oy = rect.top; ev.preventDefault(); }); document.addEventListener('mousemove', (ev) => { if (!dragging) return; const w = root.offsetWidth, h = root.offsetHeight; let left = ox + ev.clientX - sx; let top = oy + ev.clientY - sy; left = Math.max(4, Math.min(window.innerWidth - w - 4, left)); top = Math.max(4, Math.min(window.innerHeight - h - 4, top)); root.style.left = left + 'px'; root.style.top = top + 'px'; root.style.right = 'auto'; root.style.bottom = 'auto'; }); document.addEventListener('mouseup', () => { dragging = false; }); } function render() { if (!ST.statusEl) return; const r = ST.res; const v = ST.video; if (!r) { const tk = getToken(); ST.statusEl.textContent = isResourcePage() ? '正在读取资源信息…' + (tk ? '' : '\n登录态未获取到,正在重试') : '可直接点「秒过所有」批量处理账号下的课程'; return; } const total = Math.round(Number(r.duration) || (v && isFinite(v.duration) ? v.duration : 0) || 0); const pos = v ? Math.floor(v.currentTime || 0) : 0; const pct = total > 0 ? Math.min(100, Math.round((pos / total) * 100)) : 0; const typeMap = { '.mp4': '视频', '.pdf': 'PDF', html: 'H5图文' }; const recItem = ST.record && Array.isArray(ST.record.resUserRecords) ? ST.record.resUserRecords.find((x) => x.courseResourceId === r.id) : null; const lines = []; if (ST.courseName) lines.push('课程:' + ST.courseName); lines.push('本节:' + (ST.idx >= 0 ? (ST.idx + 1) + ' / ' + ST.list.length : '-') + ' ' + (r.name || '')); lines.push('类型:' + (typeMap[r.fileType] || r.resType || r.fileType || '未知') + ' 时长:' + (total ? fmtTime(total) : '未知')); if (v) lines.push('播放:' + (v.paused ? '已暂停' : '播放中') + ' 进度 ' + fmtTime(pos) + '(' + pct + '%)×' + (v.playbackRate || 1)); if (recItem) lines.push('平台已记录:' + fmtTime(recItem.resLearnDuration || 0) + (recItem.isComplete ? '(已完成)' : '(未完成)')); ST.statusEl.textContent = lines.join('\n'); } /* ============================ 接口 ============================ */ /* 该平台的登录态保存在 Cookie(js-cookie 写入),不是 localStorage */ const TOKEN_COOKIE = 'CMS-Public-Token'; let CAPTURED_TOKEN = ''; let TOKEN_SOURCE = ''; function readCookie(name) { try { const safe = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const m = document.cookie.match(new RegExp('(?:^|;\\s*)' + safe + '=([^;]*)')); return m ? decodeURIComponent(m[1]) : ''; } catch (e) { return ''; } } function readVueToken() { try { const el = document.getElementById('app'); const vm = el && el.__vue__; if (!vm) return ''; const store = vm.$store || (vm.$root && vm.$root.$store); const t = store && store.getters ? store.getters.token : ''; return typeof t === 'string' ? t : ''; } catch (e) { return ''; } } function getToken() { const cookie = String(readCookie(TOKEN_COOKIE) || '').trim(); if (cookie) { TOKEN_SOURCE = 'Cookie'; return cookie; } const vueToken = String(readVueToken() || '').trim(); if (vueToken) { TOKEN_SOURCE = '页面内存'; return vueToken; } if (CAPTURED_TOKEN) { TOKEN_SOURCE = '请求抓取'; return CAPTURED_TOKEN; } try { const keys = ['CMS-Public-Token', 'token', 'access_token', 'Access-Token']; for (let i = 0; i < keys.length; i++) { let v = localStorage.getItem(keys[i]); if (!v) continue; v = v.trim(); if (v.charAt(0) === '"') { try { v = JSON.parse(v); } catch (e) { /* ignore */ } } if (v) { TOKEN_SOURCE = 'localStorage'; return String(v); } } } catch (e) { /* ignore */ } TOKEN_SOURCE = ''; return ''; } /* 兜底:从页面自身的请求里抓 Authorization,防止取不到 Cookie */ function installTokenSniffer() { try { if (typeof window.fetch === 'function' && !window.fetch.__hzPatched) { const originFetch = window.fetch; const patched = function (input, init) { try { const url = typeof input === 'string' ? input : ((input && input.url) || ''); if (url && url.indexOf('apihz.baosteel.com') > -1 && init && init.headers) { const h = init.headers; let auth = ''; if (typeof h.get === 'function') auth = h.get('Authorization') || h.get('authorization') || ''; else auth = h.Authorization || h.authorization || ''; if (auth) CAPTURED_TOKEN = String(auth).replace(/^Bearer\s+/i, ''); } } catch (e) { /* ignore */ } return originFetch.apply(this, arguments); }; patched.__hzPatched = true; window.fetch = patched; } const xo = XMLHttpRequest.prototype.setRequestHeader; if (xo && !xo.__hzPatched) { const patchedSet = function (k, v) { try { if (String(k).toLowerCase() === 'authorization' && v) { CAPTURED_TOKEN = String(v).replace(/^Bearer\s+/i, ''); } } catch (e) { /* ignore */ } return xo.apply(this, arguments); }; patchedSet.__hzPatched = true; XMLHttpRequest.prototype.setRequestHeader = patchedSet; } } catch (e) { /* ignore */ } } const ENC_KEY = 'hz_helper_enc'; let ENCODING = 'json'; try { if (localStorage.getItem(ENC_KEY) === 'form') ENCODING = 'form'; } catch (e) { /* ignore */ } function setEncoding(enc) { if (ENCODING === enc) return; ENCODING = enc; try { localStorage.setItem(ENC_KEY, enc); } catch (e) { /* ignore */ } log('接口提交方式已切换为:' + (enc === 'form' ? '表单' : 'JSON'), 'run'); } function encodeBody(obj, enc) { if (enc === 'form') { const form = new URLSearchParams(); Object.keys(obj).forEach((k) => { const val = obj[k]; if (val === null || val === undefined) return; form.append(k, typeof val === 'object' ? JSON.stringify(val) : String(val)); }); return { contentType: 'application/x-www-form-urlencoded;charset=UTF-8', body: form.toString() }; } return { contentType: 'application/json;charset=UTF-8', body: JSON.stringify(obj) }; } async function api(path, opt) { opt = opt || {}; const token = getToken(); if (!token) throw new Error('未获取到登录凭证(Cookie / 页面内存 / 请求头均为空),请刷新页面后重试'); const enc = opt.encoding || ENCODING; const headers = { 'Accept-Language': 'zh-Hans', 'Authorization': 'Bearer ' + token }; let body; if (opt.body !== undefined) { const packed = encodeBody(opt.body, enc); headers['Content-Type'] = packed.contentType; body = packed.body; } const reqOpt = { method: opt.method || 'GET', headers, body, mode: 'cors', cache: 'no-store', credentials: 'omit' }; let resp = await fetch(API_BASE + path, reqOpt); // 后端绑定方式不匹配时,换另一种编码再试一次 if (!resp.ok && opt.body !== undefined && (resp.status === 400 || resp.status === 405 || resp.status === 415)) { const alt = enc === 'json' ? 'form' : 'json'; const packed2 = encodeBody(opt.body, alt); const retry = await fetch(API_BASE + path, Object.assign({}, reqOpt, { headers: Object.assign({}, headers, { 'Content-Type': packed2.contentType }), body: packed2.body })); if (retry.ok) { setEncoding(alt); resp = retry; } } const text = await resp.text(); let data = null; try { data = text ? JSON.parse(text) : null; } catch (e) { data = text; } if (!resp.ok) { let msg = 'HTTP ' + resp.status; if (data && data.error && data.error.message) msg += ' ' + data.error.message; else if (typeof data === 'string' && data) msg += ' ' + data.slice(0, 120); throw new Error(msg); } return data; } const apiResource = (id) => api('/api/cms/public/courses/resource/' + encodeURIComponent(id)); const apiCourse = (id) => api('/api/cms/public/courses/' + encodeURIComponent(id)); function apiRecord() { const params = new URLSearchParams(); if (ST.courseId) params.append('courseId', ST.courseId); if (ST.trainId) params.append('trainId', ST.trainId); return api('/api/cms/public/courses/user-record?' + params.toString()); } function clean(obj) { const out = {}; Object.keys(obj).forEach((k) => { if (obj[k] !== null && obj[k] !== undefined) out[k] = obj[k]; }); return out; } const apiUpdateRes = (payload, enc) => api('/api/cms/public/courses/user-record/update-res', { method: 'POST', body: clean(payload), encoding: enc }); const apiUpdateCourse = (payload) => api('/api/cms/public/courses/user-record/update-course', { method: 'POST', body: clean(payload) }); /* ============================ 资源列表 ============================ */ function flattenResources(list) { const out = []; const walk = (arr) => { (arr || []).forEach((d) => { if (!d) return; if (Array.isArray(d.resources) && d.resources.length) out.push.apply(out, d.resources); if (Array.isArray(d.directories) && d.directories.length) walk(d.directories); }); }; walk(list); return out; } /* ============================ 上下文加载 ============================ */ let loadSeq = 0; async function loadContext() { ST.res = null; ST.video = null; ST.idx = -1; if (!isResourcePage()) { render(); return; } const seq = ++loadSeq; const resId = hashQuery('id'); const courseId = hashQuery('courseId'); const trainId = hashQuery('trainId'); ST.resId = resId; ST.courseId = courseId || ST.courseId; ST.trainId = trainId || null; try { const jobs = [apiResource(resId)]; jobs.push(ST.courseId ? apiCourse(ST.courseId) : Promise.resolve(null)); const [res, course] = await Promise.all(jobs); if (seq !== loadSeq) return; ST.res = res; if (course) { ST.courseName = course.name || ''; ST.list = flattenResources(course.directoryResources || course.directories); } if (!ST.list.length) ST.list = [res]; ST.idx = ST.list.findIndex((x) => x.id === resId); try { ST.record = await apiRecord(); } catch (e) { ST.record = null; } if (seq !== loadSeq) return; ST.retryCount = 0; log('已进入:' + (res.name || '资源') + '(' + (ST.idx + 1) + '/' + ST.list.length + ')', 'ok'); render(); if (CFG.instant) { scheduleInstant(); } else { setupVideo(); } } catch (e) { if (seq !== loadSeq) return; log('加载资源失败:' + e.message, 'err'); render(); // 接口失败不影响播放器控制:倍速与自动播放照常生效 setupVideo(); if (ST.retryCount < 3) { ST.retryCount = (ST.retryCount || 0) + 1; setTimeout(loadContext, 2500); } else { log('资源信息连续读取失败,可刷新页面或重新登录后再试', 'err'); } } } /* ============================ 播放器控制 ============================ */ function findVideo() { return document.querySelector('#myPlayer_html5_api') || document.querySelector('.videoBox video') || document.querySelector('video'); } async function tryPlay() { const v = ST.video; if (!v) return; try { v.muted = true; await v.play(); } catch (e) { const btn = document.querySelector('.vjs-big-play-button') || document.querySelector('.videoBox .vjs-big-play-button') || document.querySelector('#myPlayer .prism-play-btn'); if (btn) { try { btn.click(); } catch (_) { /* ignore */ } await sleep(600); } try { await v.play(); } catch (_) { /* 仍需用户点击一次,浏览器策略限制 */ } } } function applyRate(force) { const v = ST.video; if (!v) return; const rate = Number(CFG.rate) || 1; if (rate === 1 && !force) return; if (Math.abs((v.playbackRate || 1) - rate) > 0.01) { try { v.playbackRate = rate; v.defaultPlaybackRate = rate; } catch (e) { /* ignore */ } } } function hookVideo(v) { if (!v) return; ST.video = v; if (!v.__hzHooked) { v.__hzHooked = true; v.addEventListener('ended', onVideoEnd); v.addEventListener('loadedmetadata', () => { applyRate(true); render(); }); v.addEventListener('play', () => { applyRate(); render(); }); } applyRate(true); if (CFG.autoPlay) tryPlay(); render(); if (!ST.rateTimer) ST.rateTimer = setInterval(() => applyRate(), 1500); if (!ST.syncTimer) ST.syncTimer = setInterval(syncProgress, 20000); if (!ST.keepTimer) { ST.keepTimer = setInterval(() => { if (!CFG.autoPlay || ST.busy || ST.bulkRunning) return; const vid = ST.video; if (!vid || !vid.__hzHooked) return; if (document.visibilityState === 'hidden') return; if (vid.paused && !vid.ended) tryPlay(); }, 4000); } } function setupVideo() { let tries = 0; const timer = setInterval(() => { const v = findVideo(); if (v) { clearInterval(timer); hookVideo(v); return; } if (++tries > 75) { clearInterval(timer); log('本节未检测到视频元素(可能是 PDF / H5 资源),可用「秒过本节」', 'warn'); } }, 400); } async function onVideoEnd() { if (ST.busy || ST.bulkRunning) return; const res = ST.res; if (!res) return; log('本节播放结束:' + (res.name || ''), 'ok'); try { const total = Math.round(Number(res.duration) || (ST.video && isFinite(ST.video.duration) ? ST.video.duration : 0) || 0); if (total > 0) { await apiUpdateRes({ trainId: ST.trainId || null, courseId: ST.courseId, courseResourceId: res.id, isVideo: true, resTotalDuration: total, resLearnDuration: total, lastPlayProgress: total, lastLearnProgress: 100 }); log('已按完整时长上报完成', 'ok'); } } catch (e) { log('完成上报失败:' + e.message, 'err'); } if (CFG.autoNext) { await sleep(800); goNext('播放结束'); } } function syncProgress() { if (!CFG.sync || ST.busy || ST.bulkRunning) return; const v = ST.video; const res = ST.res; if (!v || !res || v.readyState < 1 || v.paused) return; const total = Math.round(Number(res.duration) || (isFinite(v.duration) ? v.duration : 0) || 0); const pos = Math.floor(v.currentTime || 0); if (!total || pos <= 0) return; apiUpdateRes({ trainId: ST.trainId || null, courseId: ST.courseId, courseResourceId: res.id, isVideo: true, resTotalDuration: total, resLearnDuration: pos, lastPlayProgress: pos, lastLearnProgress: Math.min(100, Math.round((pos / total) * 100)) }).catch(() => { /* 静默失败,下次再同步 */ }); } /* ============================ 秒过 ============================ */ async function verifyDone(resId, total) { try { const rec = await apiRecord(); ST.record = rec; const item = (rec && Array.isArray(rec.resUserRecords) ? rec.resUserRecords : []) .find((x) => x.courseResourceId === resId); if (!item) return false; if (item.isComplete === true) return true; return total > 0 && Number(item.resLearnDuration || 0) >= total * 0.98; } catch (e) { return false; } } async function passOne(res) { const total = Math.round(Number(res.duration) || 0); if (!total) { if (!ST.courseId) { log('该节没有时长信息且缺少 courseId,无法秒过:' + (res.name || res.id), 'warn'); return false; } const r0 = await reportResource(ST.courseId, ST.trainId, res); log((r0.done ? '秒过成功(图文/无时长):' : '秒过未判定(图文/无时长):') + (res.name || ''), r0.done ? 'ok' : 'warn'); return r0.done; } const isVideo = String(res.fileType || '').toLowerCase() === '.mp4'; const payload = { trainId: ST.trainId || null, courseId: ST.courseId, courseResourceId: res.id, isVideo: isVideo, resTotalDuration: total, resLearnDuration: total, lastPlayProgress: total, lastLearnProgress: 100 }; const order = ENCODING === 'json' ? ['json', 'form'] : ['form', 'json']; let ok = false; let usedEnc = null; let lastErr = null; for (let i = 0; i < order.length && !ok; i++) { try { await apiUpdateRes(payload, order[i]); await sleep(400); if (await verifyDone(res.id, total)) { ok = true; usedEnc = order[i]; } } catch (e) { lastErr = e; } } if (ok) { if (usedEnc) setEncoding(usedEnc); log('秒过成功:' + (res.name || ''), 'ok'); } else if (lastErr) { log('秒过失败:' + (res.name || '') + ' ' + lastErr.message, 'err'); } else { log('已提交,但平台暂未判定完成:' + (res.name || '') + '(可在课程目录核对)', 'warn'); } return ok; } async function runPassCurrent(withNext) { if (!ST.res) { log('当前没有可处理的资源', 'warn'); return false; } if (ST.busy || ST.bulkRunning) return false; ST.busy = true; let ok = false; try { ok = await passOne(ST.res); if (ok && withNext && CFG.autoNext !== false) { await sleep(500); await goNext('秒过'); } render(); } catch (e) { log('秒过失败:' + e.message, 'err'); } finally { ST.busy = false; } return ok; } function scheduleInstant() { if (!ST.res || !CFG.instant) return; if (ST.handledResId === ST.res.id) return; ST.handledResId = ST.res.id; setTimeout(async () => { if (!CFG.instant || !ST.res) return; const ok = await runPassCurrent(false); if (ok && CFG.autoNext) { await sleep(450); goNext('秒过'); } }, 500); } async function runPassCourse() { if (ST.bulkRunning) { log('整课秒过正在进行中…', 'warn'); return; } if (!ST.list.length && ST.courseId) { try { const course = await apiCourse(ST.courseId); ST.courseName = course.name || ST.courseName; ST.list = flattenResources(course.directoryResources || course.directories); } catch (e) { log('获取课程资源列表失败:' + e.message, 'err'); return; } } if (!ST.list.length) { log('没有取到课程资源列表,请确认已进入课程内的学习页', 'err'); return; } ST.bulkRunning = true; ST.busy = true; let ok = 0, skip = 0, fail = 0; log('开始整课秒过,共 ' + ST.list.length + ' 节', 'run'); try { for (let i = 0; i < ST.list.length; i++) { const res = ST.list[i]; if (!res || !res.id) { continue; } const total = Math.round(Number(res.duration) || 0); if (!total) { if (!ST.courseId) { skip++; log('跳过(无时长且无 courseId):' + (res.name || res.id), 'warn'); continue; } const rr = await reportResource(ST.courseId, ST.trainId, res); if (rr.done) { ok++; log('完成(图文/无时长):' + (res.name || res.id), 'ok'); } else { skip++; log('未判定(图文/无时长):' + (res.name || res.id), 'warn'); } await sleep(300); continue; } try { const isVideo = String(res.fileType || '').toLowerCase() === '.mp4'; await apiUpdateRes({ trainId: ST.trainId || null, courseId: ST.courseId, courseResourceId: res.id, isVideo: isVideo, resTotalDuration: total, resLearnDuration: total, lastPlayProgress: total, lastLearnProgress: 100 }); ok++; log('进度上报 ' + (i + 1) + '/' + ST.list.length + ':' + (res.name || ''), 'ok'); } catch (e) { fail++; log('上报失败 ' + (i + 1) + ':' + e.message, 'err'); } await sleep(500 + Math.floor(Math.random() * 500)); } // 记录最后学习的资源 try { const lastRes = ST.list[ST.list.length - 1]; await apiUpdateCourse({ trainId: ST.trainId || null, courseId: ST.courseId, lastLearnResourceId: lastRes.id }); } catch (e) { /* 非关键 */ } log('整课秒过结束:成功 ' + ok + ' 节,跳过 ' + skip + ' 节,失败 ' + fail + ' 节', ok ? 'ok' : 'warn'); // 复核一遍 try { const rec = await apiRecord(); ST.record = rec; const items = (rec && rec.resUserRecords) || []; const doneCount = items.filter((x) => x.isComplete === true).length; log('平台当前记录:已完成 ' + doneCount + ' / ' + ST.list.length + ' 节', 'run'); } catch (e) { /* ignore */ } render(); if (ok > 0) { log('2 秒后自动刷新页面以同步状态', 'run'); setTimeout(() => location.reload(), 2000); } } finally { ST.bulkRunning = false; ST.busy = false; } } /* ============================ 切节 ============================ */ async function goNext(reason) { if (!ST.list.length || ST.idx < 0) { log('无法确定下一节,请手动切换', 'warn'); return; } const next = ST.list[ST.idx + 1]; if (!next) { log('已经是最后一节' + (reason ? '(' + reason + ')' : '') + ',已停止', 'warn'); if (CFG.instant) { CFG.instant = false; saveCfg(); const cb = document.getElementById('hz-instant'); if (cb) cb.checked = false; log('秒过模式已自动关闭', 'run'); } return; } const now = Date.now(); if (now - ST.lastNavAt < 1500) return; ST.lastNavAt = now; log('切换到下一节:' + (next.name || ''), 'run'); try { await apiUpdateCourse({ trainId: ST.trainId || null, courseId: ST.courseId, lastLearnResourceId: next.id }); } catch (e) { /* 非关键 */ } let hash = '#/resourceInfo?id=' + next.id; if (ST.courseId) hash += '&courseId=' + ST.courseId; if (ST.trainId) hash += '&trainId=' + ST.trainId; location.hash = hash; } /* ============================ 全账号秒过 ============================ */ const ALL = { running: false, stop: false, done: 0, total: 0 }; function qs(obj) { const p = new URLSearchParams(); Object.keys(obj).forEach((k) => { const v = obj[k]; if (v !== null && v !== undefined && v !== '') p.append(k, v); }); return p.toString(); } function setAllBtnState(running) { const b1 = document.getElementById('hz-pass-all-account'); const b2 = document.getElementById('hz-pass-stop'); if (b1) { b1.disabled = running; b1.style.opacity = running ? '.55' : '1'; b1.textContent = running ? '秒过进行中…' : '秒过所有'; } if (b2) { b2.disabled = !running; b2.style.opacity = running ? '1' : '.5'; } } async function listMyTrains() { const out = []; let skip = 0; for (let g = 0; g < 20; g++) { const data = await api('/api/cms/public/trains/my-trains?' + qs({ SkipCount: skip, MaxResultCount: 50 })); const items = (data && data.items) || []; out.push.apply(out, items); const total = (data && data.totalCount) || 0; if (!items.length || out.length >= total) break; skip += items.length; } return out; } async function getCourseRecord(courseId, trainId) { try { return await api('/api/cms/public/courses/user-record?' + qs({ courseId: courseId, trainId: trainId || null })); } catch (e) { return null; } } function flattenDir(list) { const out = []; const walk = (arr) => { (arr || []).forEach((d) => { if (!d) return; if (Array.isArray(d.resources) && d.resources.length) out.push.apply(out, d.resources); if (Array.isArray(d.directories) && d.directories.length) walk(d.directories); }); }; walk(list); return out; } /* 上报方式变体:按资源类型分组记忆,视频用一组、图文/PDF/无时长用另一组 */ const RES_V_KEY = 'hz_helper_res_v2'; let RES_V = { video: '', other: '' }; try { const raw = localStorage.getItem(RES_V_KEY); if (raw) { const o = JSON.parse(raw) || {}; RES_V.video = o.video || ''; RES_V.other = o.other || ''; } } catch (e) { /* ignore */ } function resCategory(r) { const ft = String(r.fileType || '').toLowerCase(); const total = Math.round(Number(r.duration) || 0); return ft === '.mp4' && total > 0 ? 'video' : 'other'; } function saveResVariant(cat, id) { if (RES_V[cat] === id) return; RES_V[cat] = id; try { localStorage.setItem(RES_V_KEY, JSON.stringify(RES_V)); } catch (e) { /* ignore */ } log('上报方式已记录:' + (cat === 'video' ? '视频' : '图文/PDF') + ' → ' + id, 'info'); } function buildVariants(cat, total) { if (cat === 'video') { const base = { isVideo: true, resTotalDuration: total, resLearnDuration: total, lastPlayProgress: total }; return [ { id: 'A', body: Object.assign({}, base, { lastLearnProgress: 100 }) }, { id: 'B', body: Object.assign({}, base, { lastLearnProgress: 100, learnDetail: JSON.stringify({ learnTime: total, progress: 100 }) }) }, { id: 'C', body: Object.assign({}, base) } ]; } // 图文 / PDF / 时长为 0 的节:平台自己的阅读器上报的学时很短,duration 常为 0 const t = total > 0 ? total : 0; const learned = t > 0 ? t : 60; return [ { id: 'P1', body: { isVideo: false, resTotalDuration: t, resLearnDuration: learned, lastPlayProgress: 0, lastLearnProgress: 100 } }, { id: 'P2', body: { isVideo: false, resTotalDuration: t, resLearnDuration: learned, lastPlayProgress: 0, lastLearnProgress: 100, learnDetail: JSON.stringify({ learnTime: learned, progress: 100 }) } }, { id: 'P3', body: { isVideo: false, resTotalDuration: t || 1, resLearnDuration: t || 1, lastPlayProgress: 0, lastLearnProgress: 100 } }, { id: 'P4', body: { isVideo: false, resTotalDuration: t, resLearnDuration: 0, lastPlayProgress: 0, lastLearnProgress: 100 } }, { id: 'P5', body: { isVideo: false, resLearnDuration: learned, lastPlayProgress: 0, lastLearnProgress: 100 } } ]; } async function isResDone(courseId, trainId, resId) { try { const rec = await getCourseRecord(courseId, trainId); const item = (rec && Array.isArray(rec.resUserRecords) ? rec.resUserRecords : []).find((x) => x.courseResourceId === resId); return !!(item && item.isComplete === true); } catch (e) { return false; } } async function waitResDone(courseId, trainId, resId, delays) { for (let i = 0; i < delays.length; i++) { if (delays[i]) await sleep(delays[i]); if (await isResDone(courseId, trainId, resId)) return true; } return false; } /* 像用户手动点开那一节一样,在隐藏 iframe 里把资源页加载一次: 平台的页面自己会走定时器 / H5 回传,把这一节学完并上报,这是最贴近真实操作的方式 */ async function openResPageUntilDone(courseId, trainId, resId) { let fr = null; try { fr = document.createElement('iframe'); fr.id = 'hz-res-frame'; fr.style.cssText = 'position:fixed;left:-10000px;top:0;width:900px;height:700px;opacity:0;pointer-events:none;border:0;z-index:-1;'; const url = location.origin + '/#/resourceInfo?id=' + encodeURIComponent(resId) + '&courseId=' + encodeURIComponent(courseId) + (trainId ? '&trainId=' + encodeURIComponent(trainId) : ''); log(' 打开该节页面(隐藏标签),让平台自己上报…', 'warn'); fr.src = url; document.body.appendChild(fr); const t0 = Date.now(); while (Date.now() - t0 < 16000) { await sleep(1500); if (await isResDone(courseId, trainId, resId)) return true; } return false; } catch (e) { log(' 打开该节页面失败:' + e.message, 'err'); return false; } finally { if (fr) { try { fr.remove(); } catch (e2) { /* ignore */ } } } } async function reportResource(courseId, trainId, r) { const cat = resCategory(r); const total = Math.round(Number(r.duration) || 0); const pref = RES_V[cat]; if (!courseId) return { done: false, variant: '' }; // 上一次是靠「打开页面」过的,这类资源直接走页面 if (cat !== 'video' && pref === 'page') { if (await openResPageUntilDone(courseId, trainId, r.id)) return { done: true, variant: 'page' }; } const variants = buildVariants(cat, total); variants.sort((a, b) => (a.id === pref ? -1 : (b.id === pref ? 1 : 0))); const delays = cat === 'video' ? [450, 1300] : [600, 1500, 2600]; for (let i = 0; i < variants.length; i++) { const v = variants[i]; try { await apiUpdateRes(Object.assign({ trainId: trainId || null, courseId: courseId, courseResourceId: r.id }, v.body)); } catch (e) { log(' 上报失败(' + v.id + '):' + e.message, 'err'); continue; } if (await waitResDone(courseId, trainId, r.id, delays)) { saveResVariant(cat, v.id); return { done: true, variant: v.id }; } } // 图文/PDF 兜底:直接上报不被判定时,改成把该节页面加载一次,让平台自己走完 if (cat !== 'video') { if (await openResPageUntilDone(courseId, trainId, r.id)) { saveResVariant(cat, 'page'); return { done: true, variant: 'page' }; } } return { done: false, variant: '' }; } async function waitCourseComplete(courseId, trainId, delays) { let last = null; for (let i = 0; i < delays.length; i++) { if (delays[i]) await sleep(delays[i]); try { last = await getCourseRecord(courseId, trainId); if (last && last.isComplete === true) return last; } catch (e) { /* ignore */ } } return last; } /* 诊断:把当前课程的资源、平台记录、班级记录打到日志里,用于排查「上报了但不判定完成」 */ async function runDiagnose() { log('=== 诊断开始 ===', 'run'); log('路由:' + location.hash, 'info'); log('courseId=' + (ST.courseId || '-') + ' trainId=' + (ST.trainId || '-') + ' resId=' + (ST.resId || '-'), 'info'); if (!ST.courseId) { log('当前不在课程页,先进入某门课程再点诊断', 'warn'); log('=== 诊断结束 ===', 'run'); return; } try { const course = await apiCourse(ST.courseId); const list = flattenResources(course.directoryResources || course.directories); log('课程:' + (course.name || '') + ' 资源 ' + list.length + ' 节 freeModel=' + course.freeModel, 'info'); list.slice(0, 15).forEach((r, i) => { log(' ' + (i + 1) + '. ' + (r.name || '') + ' id=' + r.id + ' fileType=' + r.fileType + ' resType=' + r.resType + ' duration=' + r.duration, 'info'); }); } catch (e) { log('读取课程失败:' + e.message, 'err'); } try { const rec = await apiRecord(); ST.record = rec; log('课程记录:isComplete=' + (rec && rec.isComplete) + ' learnPass=' + (rec && rec.learnPass) + ' id=' + (rec && rec.id), 'info'); const items = (rec && rec.resUserRecords) || []; items.slice(0, 15).forEach((x) => { log(' 节 ' + x.courseResourceId + ' isComplete=' + x.isComplete + ' learned=' + x.resLearnDuration + ' lastProgress=' + x.lastLearnProgress, 'info'); }); } catch (e) { log('读取课程记录失败:' + e.message, 'err'); } if (ST.trainId) { try { const d = await api('/api/cms/public/trains/detail?trainId=' + encodeURIComponent(ST.trainId)); const cs = (d && d.trainCourses) || []; const me = cs.find((c) => c && c.courseId === ST.courseId); log('班级记录:课程 ' + cs.length + ' 门 本课程 isComplete=' + (me && me.recordInfo ? me.recordInfo.isComplete : '无记录'), 'info'); } catch (e) { log('读取班级记录失败:' + e.message, 'err'); } } log('=== 诊断结束:点上方「复制日志」即可整段发给作者 ===', 'run'); } async function passCourse(courseId, courseName, trainId) { if (ALL.stop) return false; const rec = await getCourseRecord(courseId, trainId); if (rec && rec.isComplete === true) { log('已完成,跳过:' + courseName, 'info'); return true; } let detail; try { detail = await api('/api/cms/public/courses/' + encodeURIComponent(courseId)); } catch (e) { log('读取课程失败:' + courseName + ' ' + e.message, 'err'); return false; } const resources = flattenDir(detail.directoryResources || detail.directories) .filter((r) => r && r.id); if (!resources.length) { log('该课程没有可上报的资源,跳过:' + courseName, 'warn'); return false; } try { await apiUpdateCourse({ trainId: trainId || null, courseId: courseId, lastLearnResourceId: resources[0].id }); } catch (e) { /* 建立记录失败时仍继续尝试上报 */ } let ok = 0; const notDone = []; for (let i = 0; i < resources.length; i++) { if (ALL.stop) { log('已停止:' + courseName, 'warn'); return false; } const r = resources[i]; const pre = (rec && Array.isArray(rec.resUserRecords) ? rec.resUserRecords : []).find((x) => x.courseResourceId === r.id); if (pre && pre.isComplete === true) { ok++; log(' 已完成,跳过:' + (r.name || r.id), 'info'); continue; } const res = await reportResource(courseId, trainId, r); if (res.done) ok++; else notDone.push((r.name || r.id) + '(' + r.fileType + '/' + r.resType + ')'); if (i % 5 === 4 || i === resources.length - 1) { log(' ' + courseName + ' ' + (i + 1) + '/' + resources.length + '(已判定完成 ' + ok + ' 节)', 'info'); } await sleep(220 + Math.random() * 200); } const after = await waitCourseComplete(courseId, trainId, [0, 1200, 2600]); const done = !!(after && after.isComplete === true); if (!done && notDone.length) { log(' 未判定完成的节:' + notDone.slice(0, 3).join('、') + (notDone.length > 3 ? ' 等 ' + notDone.length + ' 节' : '') + '(可点「诊断」查看平台记录)', 'warn'); } log((done ? '课程完成:' : '已上报(平台暂未判定):') + courseName + '(' + ok + '/' + resources.length + ' 节)', done ? 'ok' : 'warn'); return done; } async function runPassAll() { if (ALL.running) { log('秒过所有正在进行中…', 'warn'); return; } ALL.running = true; ALL.stop = false; setAllBtnState(true); log('开始扫描「我的班级」…', 'run'); try { const tasks = []; let trains = []; try { trains = (await listMyTrains()).filter((t) => t && (t.trainState === 0 || t.trainState === 1)); log('我的班级(未结束):' + trains.length + ' 个', 'run'); } catch (e) { log('读取我的班级失败:' + e.message, 'err'); } for (let i = 0; i < trains.length; i++) { if (ALL.stop) break; const tr = trains[i]; let detail; try { detail = await api('/api/cms/public/trains/detail?trainId=' + encodeURIComponent(tr.id)); } catch (e) { log('读取班级失败:' + (tr.name || tr.id) + ' ' + e.message, 'err'); continue; } const list = (detail && detail.trainCourses) || []; log('班级「' + (tr.name || tr.id) + '」:' + list.length + ' 门课程', 'run'); list.forEach((c) => { if (c && c.courseId && !c.isExpire) { tasks.push({ courseId: c.courseId, name: (tr.name || '班级') + ' / ' + (c.courseName || c.courseId), trainId: tr.id }); } }); } // 同一门课程可能出现在多个班级里,按 班级+课程 去重 const uniq = []; const seen = {}; tasks.forEach((t) => { const key = String(t.trainId) + '|' + String(t.courseId); if (seen[key]) return; seen[key] = 1; uniq.push(t); }); ALL.total = uniq.length; if (!uniq.length) { log('没有找到可处理的课程', 'warn'); return; } log('合计 ' + uniq.length + ' 门课程,开始逐门秒过', 'run'); for (let i = 0; i < uniq.length; i++) { if (ALL.stop) { log('已手动停止', 'warn'); break; } ALL.done = i + 1; log('【' + (i + 1) + '/' + uniq.length + '】' + uniq[i].name, 'run'); await passCourse(uniq[i].courseId, uniq[i].name, uniq[i].trainId); await sleep(250); } // 班级完成度小结:课程都过了但班级仍显示未完成时,通常还缺考试/签到 if (trains.length && !ALL.stop) { log('—— 班级完成情况 ——', 'run'); for (let i = 0; i < trains.length; i++) { const tr = trains[i]; try { const d2 = await api('/api/cms/public/trains/detail?trainId=' + encodeURIComponent(tr.id)); const cs = (d2 && d2.trainCourses) || []; const doneN = cs.filter((c) => c && c.recordInfo && c.recordInfo.isComplete).length; log('班级「' + (tr.name || tr.id) + '」:课程完成 ' + doneN + '/' + cs.length, cs.length && doneN === cs.length ? 'ok' : 'warn'); } catch (e) { log('读取班级完成情况失败:' + (tr.name || tr.id), 'err'); } } log('若班级仍显示未完成,通常还需完成该班级的考试或签到;考试可在「我的考试」里批量处理', 'info'); } log(ALL.stop ? '秒过所有已停止' : '秒过所有执行完毕', ALL.stop ? 'warn' : 'ok'); } catch (e) { log('秒过所有失败:' + e.message, 'err'); } finally { ALL.running = false; setAllBtnState(false); } } /* ============================ 我的考试(批量自动作答) ============================ */ const EXLIST = { items: [], selected: {}, result: {}, running: false, stop: false, cancel: null, frame: null }; async function fetchMyExams() { const out = []; let skip = 0; for (let g = 0; g < 10; g++) { const data = await api('/api/exams/student/exams?' + qs({ IsSubmit: 0, SkipCount: skip, MaxResultCount: 50 })); const items = (data && data.items) || []; out.push.apply(out, items); const total = (data && data.totalCount) || 0; if (!items.length || out.length >= total) break; skip += items.length; } return out; } function examStatusText(e) { const s = Number(e.status); if (s === 1) return '未开始'; if (s === 2) return '进行中'; if (s === 3) return '已结束'; return '状态' + s; } function examAutomatable(e) { return Number(e.status) === 2 && !e.examUrl; } function examMeta(e) { const parts = []; parts.push('考核时间 ' + (e.startDate || '?') + ' 至 ' + (e.endDate || '?')); if (e.timeLong) parts.push('时长 ' + e.timeLong + ' 分钟'); if (Number(e.submitLimitTime) > 0) parts.push('开考 ' + e.submitLimitTime + ' 分钟后可交卷'); if (Number(e.submitTimes) > 0) parts.push('已交卷 ' + e.submitTimes + ' 次'); return parts.join(' '); } function buildExamUrl(e) { const token = getToken(); if (!token) throw new Error('未获取到登录凭证,请刷新页面后重试'); const returnUrl = location.origin + '/#/examcenter'; const inner = '/start?examId=' + (e.examinationId || '') + '&starttime=' + (e.startDate || '') + '&submitLimitTime=' + (e.submitLimitTime == null ? 0 : e.submitLimitTime) + '&examName=' + encodeURIComponent(e.name || '') + '&timeLong=' + (e.timeLong || 0) + '&returnUrl=' + encodeURIComponent(returnUrl); return 'https://examhz.baosteel.com/#/auth-redirect?redirect=' + encodeURIComponent(inner) + '&token=' + encodeURIComponent(token); } function updateExamCount() { const bRun = document.getElementById('hz-exam-run'); const bStop = document.getElementById('hz-exam-stop'); const bAll = document.getElementById('hz-exam-selectall'); const n = EXLIST.items.filter((e) => EXLIST.selected[e.examinationId] && examAutomatable(e)).length; if (bRun) { bRun.textContent = EXLIST.running ? '自动完成中…' : '开始自动完成(' + n + ')'; bRun.disabled = EXLIST.running || n === 0; bRun.style.opacity = EXLIST.running || n === 0 ? '.55' : '1'; } if (bStop) { bStop.disabled = !EXLIST.running; bStop.style.opacity = EXLIST.running ? '1' : '.5'; } if (bAll) bAll.disabled = EXLIST.running; } function renderExamList() { const list = document.getElementById('hz-exam-list'); if (!list) return; list.innerHTML = ''; if (!EXLIST.items.length) { const empty = document.createElement('div'); empty.textContent = '没有未提交的考试(或接口未返回数据)。'; empty.style.cssText = 'color:#94a3b8;padding:20px 4px;'; list.appendChild(empty); updateExamCount(); return; } EXLIST.items.forEach((e) => { const row = document.createElement('div'); row.style.cssText = 'display:flex;gap:10px;align-items:flex-start;padding:9px 4px;border-bottom:1px solid #1b2637;'; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.style.cssText = 'margin-top:4px;width:15px;height:15px;accent-color:#3b82f6;cursor:pointer;'; cb.checked = !!EXLIST.selected[e.examinationId]; cb.disabled = !examAutomatable(e) || EXLIST.running; cb.addEventListener('change', () => { if (cb.checked) EXLIST.selected[e.examinationId] = true; else delete EXLIST.selected[e.examinationId]; updateExamCount(); }); const col = document.createElement('div'); col.style.cssText = 'flex:1;min-width:0;cursor:pointer;'; col.addEventListener('click', () => { if (cb.disabled) return; cb.checked = !cb.checked; cb.dispatchEvent(new Event('change')); }); const title = document.createElement('div'); title.style.cssText = 'color:#e5e7eb;font-weight:600;'; title.textContent = e.name || e.examinationId; const meta = document.createElement('div'); meta.style.cssText = 'color:#94a3b8;font-size:12px;'; meta.textContent = examMeta(e); col.appendChild(title); col.appendChild(meta); const tag = document.createElement('span'); const auto = examAutomatable(e); tag.textContent = auto ? '可自动' : (e.examUrl ? '外部网页考试' : examStatusText(e)); tag.style.cssText = 'font-size:11px;padding:1px 7px;border-radius:10px;white-space:nowrap;margin-top:2px;' + (auto ? 'background:#164e63;color:#67e8f9;' : 'background:#334155;color:#cbd5e1;'); const mark = document.createElement('span'); const st = EXLIST.result[e.examinationId]; if (st) { mark.textContent = st; mark.style.cssText = 'font-size:11px;padding:1px 6px;border-radius:10px;white-space:nowrap;margin-top:2px;' + (st === '已交卷' ? 'background:#14532d;color:#86efac;' : (st === '进行中…' ? 'background:#1e3a8a;color:#93c5fd;' : 'background:#7f1d1d;color:#fca5a5;')); } row.appendChild(cb); row.appendChild(col); row.appendChild(tag); if (st) row.appendChild(mark); list.appendChild(row); }); updateExamCount(); } async function loadExams() { const tip = document.getElementById('hz-exam-tip'); if (tip) tip.textContent = '正在读取「我的考试」…'; try { EXLIST.items = await fetchMyExams(); EXLIST.selected = {}; renderExamList(); const auto = EXLIST.items.filter(examAutomatable).length; if (tip) tip.textContent = '共 ' + EXLIST.items.length + ' 场未提交考试,其中 ' + auto + ' 场可自动完成(外部网页考试需手动完成,未开始的需等开考)。'; log('我的考试(未提交):' + EXLIST.items.length + ' 场,可自动 ' + auto + ' 场', 'run'); } catch (e) { if (tip) tip.textContent = '读取考试列表失败:' + e.message; log('读取考试列表失败:' + e.message, 'err'); } } function openExamModal() { if (document.getElementById('hz-exam-overlay')) return; const ov = document.createElement('div'); ov.id = 'hz-exam-overlay'; ov.style.cssText = 'position:fixed;inset:0;z-index:2147483002;background:rgba(3,7,18,.62);display:flex;align-items:center;justify-content:center;'; const card = document.createElement('div'); card.style.cssText = 'width:min(780px,94vw);max-height:88vh;display:flex;flex-direction:column;background:#0f172a;border:1px solid #243043;border-radius:12px;box-shadow:0 18px 48px rgba(0,0,0,.55);color:#d1d5db;font:13px/1.6 "Microsoft YaHei",system-ui,sans-serif;overflow:hidden;'; const hd = document.createElement('div'); hd.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:11px 14px;border-bottom:1px solid #243043;background:linear-gradient(90deg,#132033,#0f172a);'; hd.innerHTML = '我的考试 · 自动作答'; const closeBtn = document.createElement('span'); closeBtn.textContent = '✕'; closeBtn.title = '关闭'; closeBtn.style.cssText = 'cursor:pointer;color:#94a3b8;font-size:15px;padding:0 4px;'; closeBtn.addEventListener('click', () => { if (EXLIST.running) { log('正在自动完成考试,请先点「停止」', 'warn'); return; } ov.remove(); }); hd.appendChild(closeBtn); const body = document.createElement('div'); body.style.cssText = 'padding:10px 14px 4px;overflow:auto;flex:1;'; const list = document.createElement('div'); list.id = 'hz-exam-list'; list.style.cssText = 'min-height:120px;'; const stage = document.createElement('div'); stage.id = 'hz-exam-stage'; stage.style.cssText = 'display:none;height:66vh;border:1px solid #243043;border-radius:8px;overflow:hidden;background:#fff;'; body.appendChild(list); body.appendChild(stage); const tip = document.createElement('div'); tip.id = 'hz-exam-tip'; tip.style.cssText = 'color:#94a3b8;font-size:12px;margin:8px 14px 10px;white-space:pre-line;'; const ft = document.createElement('div'); ft.style.cssText = 'display:flex;align-items:center;gap:8px;padding:10px 14px;border-top:1px solid #243043;background:#0b1220;'; const btnStyle = 'padding:7px 12px;border:none;border-radius:6px;color:#fff;font-size:12px;font-weight:600;cursor:pointer;'; const bAll = document.createElement('button'); bAll.id = 'hz-exam-selectall'; bAll.textContent = '全选可自动'; bAll.style.cssText = btnStyle + 'background:#475569;'; bAll.addEventListener('click', () => { EXLIST.items.forEach((e) => { if (examAutomatable(e)) EXLIST.selected[e.examinationId] = true; }); renderExamList(); }); const bRun = document.createElement('button'); bRun.id = 'hz-exam-run'; bRun.textContent = '开始自动完成(0)'; bRun.style.cssText = btnStyle + 'background:#0d9488;'; bRun.disabled = true; bRun.addEventListener('click', () => runSelectedExams()); const bStop = document.createElement('button'); bStop.id = 'hz-exam-stop'; bStop.textContent = '停止'; bStop.style.cssText = btnStyle + 'background:#b91c1c;opacity:.5;'; bStop.disabled = true; bStop.addEventListener('click', () => { EXLIST.stop = true; if (EXLIST.cancel) EXLIST.cancel(); log('已发出停止指令…', 'warn'); }); ft.appendChild(bAll); ft.appendChild(bRun); ft.appendChild(bStop); card.appendChild(hd); card.appendChild(body); card.appendChild(tip); card.appendChild(ft); ov.appendChild(card); document.body.appendChild(ov); loadExams(); } function runOneExam(e) { return new Promise((resolve) => { const stage = document.getElementById('hz-exam-stage'); if (!stage) { resolve({ ok: false, detail: '面板未打开' }); return; } let url; try { url = buildExamUrl(e); } catch (err) { resolve({ ok: false, detail: err.message }); return; } stage.innerHTML = ''; const frame = document.createElement('iframe'); frame.style.cssText = 'width:100%;height:100%;border:0;display:block;'; stage.appendChild(frame); EXLIST.frame = frame; let settled = false; let timer = null; const cleanup = () => { window.removeEventListener('message', onMsg); if (timer) clearTimeout(timer); EXLIST.cancel = null; try { frame.src = 'about:blank'; } catch (err) { /* ignore */ } setTimeout(() => { if (frame.parentNode) frame.parentNode.removeChild(frame); }, 300); if (EXLIST.frame === frame) EXLIST.frame = null; }; const finish = (ok, detail) => { if (settled) return; settled = true; cleanup(); resolve({ ok: ok, detail: detail || '' }); }; const onMsg = (ev) => { if (ev.origin !== 'https://examhz.baosteel.com') return; const d = ev.data; if (!d || d.source !== 'hz-exam-helper') return; if (d.type === 'done') finish(true, d.detail || '交卷成功'); else if (d.type === 'fail') finish(false, d.detail || '页面报告失败'); else if (d.type === 'progress') { const tip = document.getElementById('hz-exam-tip'); if (tip) tip.textContent = tip.textContent.split('\n')[0] + '\n' + (d.detail || ''); } }; window.addEventListener('message', onMsg); EXLIST.cancel = () => finish(false, '已手动停止'); timer = setTimeout(() => finish(false, '超时未完成(可能题目未加载或被交卷时间限制卡住)'), 180000); frame.src = url; }); } async function runSelectedExams() { if (EXLIST.running) return; const picked = EXLIST.items.filter((e) => EXLIST.selected[e.examinationId] && examAutomatable(e)); if (!picked.length) { log('请先勾选要自动完成的考试', 'warn'); return; } const list = document.getElementById('hz-exam-list'); const stage = document.getElementById('hz-exam-stage'); const tip = document.getElementById('hz-exam-tip'); EXLIST.running = true; EXLIST.stop = false; EXLIST.result = {}; if (list) list.style.display = 'none'; if (stage) stage.style.display = 'block'; renderExamList(); log('开始自动完成 ' + picked.length + ' 场考试', 'run'); let okCount = 0; try { for (let i = 0; i < picked.length; i++) { if (EXLIST.stop) break; const e = picked[i]; EXLIST.result[e.examinationId] = '进行中…'; if (tip) tip.textContent = '正在处理 ' + (i + 1) + '/' + picked.length + ':' + (e.name || e.examinationId) + '\n等待考试页加载并自动作答…'; log('【' + (i + 1) + '/' + picked.length + '】' + (e.name || e.examinationId), 'run'); const r = await runOneExam(e); EXLIST.result[e.examinationId] = r.ok ? '已交卷' : ('失败:' + r.detail); if (r.ok) okCount++; log((r.ok ? '已完成:' : '未完成:') + (e.name || e.examinationId) + (r.ok ? '' : ' ' + r.detail), r.ok ? 'ok' : 'warn'); await sleep(900 + Math.random() * 600); } } finally { EXLIST.running = false; if (stage) { stage.style.display = 'none'; stage.innerHTML = ''; } if (list) list.style.display = 'block'; renderExamList(); if (tip) tip.textContent = (EXLIST.stop ? '已停止。' : '批量完成结束。') + '成功 ' + okCount + ' / ' + picked.length + ' 场,正在刷新列表…'; setTimeout(loadExams, 1500); } } /* ============================ 考试站(examhz) ============================ */ const EXAM = { autoFill: true, autoSubmit: true, captured: '', filledOnce: false, submitted: false, started: false }; function examCfgLoad() { try { const c = JSON.parse(localStorage.getItem('hz_exam_cfg_v1') || '{}') || {}; if (typeof c.autoFill === 'boolean') EXAM.autoFill = c.autoFill; if (typeof c.autoSubmit === 'boolean') EXAM.autoSubmit = c.autoSubmit; } catch (e) { /* ignore */ } } function examCfgSave() { try { localStorage.setItem('hz_exam_cfg_v1', JSON.stringify({ autoFill: EXAM.autoFill, autoSubmit: EXAM.autoSubmit })); } catch (e) { /* ignore */ } } function installCanvasHook() { try { const proto = window.CanvasRenderingContext2D && CanvasRenderingContext2D.prototype; if (!proto || proto.__hzHooked) return; const origFill = proto.fillText; proto.fillText = function (text, x, y, mw) { try { if (this.canvas && this.canvas.id === 'mycanvas') EXAM.captured += String(text); } catch (e) { /* ignore */ } return arguments.length > 3 ? origFill.call(this, text, x, y, mw) : origFill.call(this, text, x, y); }; const origClear = proto.clearRect; proto.clearRect = function () { try { if (this.canvas && this.canvas.id === 'mycanvas') EXAM.captured = ''; } catch (e) { /* ignore */ } return origClear.apply(this, arguments); }; proto.__hzHooked = true; } catch (e) { /* ignore */ } } function setInputValue(el, val) { try { const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set; setter.call(el, val); } catch (e) { el.value = val; } el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } function clickStartIfAny() { if (EXAM.started) return; // 先勾选「已阅读考试须知」,否则「进入考试」按钮一直是禁用的 const accept = document.querySelector('.isaccept'); if (accept && !accept.classList.contains('van-checkbox--checked') && !accept.classList.contains('van-checkbox--disabled')) { accept.click(); return; } const nodes = Array.prototype.slice.call(document.querySelectorAll('button, .van-button, a')); const target = nodes.find((b) => /进入考试|开始考试|立即考试|继续考试/.test((b.textContent || '').replace(/\s+/g, '')) && !b.disabled); if (target) { EXAM.started = true; log('已自动点击「' + (target.textContent || '').trim() + '」', 'run'); target.click(); } } function fillAnswers() { let singleCount = 0; let multiCount = 0; document.querySelectorAll('.van-radio-group').forEach((g) => { const opts = g.querySelectorAll('.van-radio'); if (!opts.length) return; if (g.querySelector('.van-radio--checked')) return; opts[0].click(); singleCount++; }); document.querySelectorAll('.van-checkbox-group').forEach((g) => { const opts = g.querySelectorAll('.van-checkbox'); opts.forEach((o) => { if (!o.classList.contains('van-checkbox--checked')) o.click(); multiCount++; }); }); return { singleCount: singleCount, multiCount: multiCount }; } async function autoSubmit() { if (EXAM.submitted) return false; const openBtn = document.querySelector('.submitPage-btn'); if (!openBtn) { log('未找到「提交试卷」按钮', 'warn'); return false; } EXAM.submitted = true; EXAM.captured = ''; openBtn.click(); await sleep(1500); const code = String(EXAM.captured || '').trim(); if (!/^\d{4}$/.test(code)) { log('未捕获到验证码,请手动输入后交卷', 'warn'); EXAM.submitted = false; return false; } const input = document.querySelector('input.codetxt') || document.querySelector('.codetxt input'); if (!input) { log('未找到验证码输入框', 'warn'); EXAM.submitted = false; return false; } setInputValue(input, code); await sleep(450); const btn = document.querySelector('.code-opr .submit_btn'); if (!btn) { log('未找到确认交卷按钮', 'warn'); EXAM.submitted = false; return false; } if (btn.disabled) { log('交卷按钮仍为禁用(验证码未同步),请手动确认', 'warn'); EXAM.submitted = false; return false; } btn.click(); log('已自动交卷(验证码 ' + code + ')', 'ok'); return true; } /* 找到试卷页的 Vue 组件实例(用于在交卷按钮被时间限制锁住时调用其自身交卷方法) */ function findExamVM() { try { const el = document.getElementById('app'); const rootVm = el && el.__vue__; if (!rootVm) return null; let found = null; const walk = (vm, depth) => { if (!vm || found || depth > 14) return; if (typeof vm.handleData === 'function' && vm.exam) { found = vm; return; } (vm.$children || []).forEach((c) => walk(c, depth + 1)); }; walk(rootVm, 0); return found; } catch (e) { return null; } } function readExamId() { try { const q = location.hash.split('?')[1] || ''; return new URLSearchParams(q).get('examId') || ''; } catch (e) { return ''; } } /* 只在被批量考试任务嵌入 iframe 时向父页面回报 */ function reportToParent(type, detail) { if (window.top === window.self) return; try { window.parent.postMessage({ source: 'hz-exam-helper', type: type, detail: detail || '', examId: readExamId() }, '*'); } catch (e) { /* ignore */ } } async function autoSubmitAuto() { if (EXAM.submitted) return false; const openBtn = document.querySelector('.submitPage-btn'); if (openBtn && !openBtn.disabled) return await autoSubmit(); const vm = findExamVM(); if (!vm) { log('未找到试卷组件,无法交卷', 'warn'); return false; } EXAM.submitted = true; try { vm.handleData(1); log('交卷按钮受「开考 N 分钟后才可交卷」限制,已直接调用页面自身的交卷方法', 'warn'); return true; } catch (e) { EXAM.submitted = false; log('交卷失败:' + e.message, 'err'); return false; } } /* 监听页面上的交卷结果提示,用于向批量任务回报成功/失败 */ function watchSubmitResult() { if (window.top === window.self) return; let reported = false; const startedAt = Date.now(); setInterval(() => { if (reported) return; const txt = (document.body && document.body.innerText) || ''; if (txt.indexOf('交卷成功') > -1) { reported = true; reportToParent('done', '交卷成功'); return; } const bad = txt.match(/交卷失败[^\n]{0,30}|提交失败[^\n]{0,30}|未到交卷时间[^\n]{0,30}|禁止进入[^\n]{0,20}/); if (bad) { reported = true; reportToParent('fail', bad[0]); return; } // 25 秒还停在须知页 / 倒计时页,说明进不了试卷,直接回报失败,避免白等 if (Date.now() - startedAt > 25000) { const hasQ = document.querySelectorAll('.van-radio-group, .van-checkbox-group').length > 0 || document.querySelector('.test-item-div'); if (!hasQ) { if (txt.indexOf('距离开考时间') > -1) { reported = true; reportToParent('fail', '未到开考时间,需等开考后再进'); } else if (txt.indexOf('考试须知') > -1) { reported = true; reportToParent('fail', '卡在考试须知页,未能进入试卷'); } } } }, 700); } function buildExamPanel() { if (ST.panel) return; const root = document.createElement('div'); root.style.cssText = ['position:fixed', 'right:16px', 'bottom:16px', 'width:290px', 'z-index:2147483000', 'background:#111827', 'border:1px solid #2b3547', 'border-radius:10px', 'box-shadow:0 10px 34px rgba(0,0,0,.45)', 'color:#d1d5db', 'font:13px/1.5 "Microsoft YaHei",system-ui,sans-serif', 'overflow:hidden'].join(';'); const hd = document.createElement('div'); hd.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:9px 12px;background:linear-gradient(90deg,#1f2937,#111827);border-bottom:1px solid #2b3547;'; hd.innerHTML = '考试助手 v1.2.4'; const fold = document.createElement('span'); fold.textContent = '—'; fold.style.cssText = 'cursor:pointer;color:#9ca3af;font-size:15px;padding:0 3px;'; hd.appendChild(fold); const bd = document.createElement('div'); bd.style.cssText = 'padding:10px 12px 12px;'; const chkStyle = 'width:16px;height:16px;accent-color:#3b82f6;cursor:pointer;'; function row(label, checked, onChange) { const r = document.createElement('div'); r.style.cssText = 'display:flex;align-items:center;justify-content:space-between;margin-bottom:7px;'; const l = document.createElement('label'); l.textContent = label; l.style.cssText = 'color:#cbd5e1;cursor:pointer;'; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = !!checked; cb.style.cssText = chkStyle; cb.addEventListener('change', () => onChange(cb.checked)); l.addEventListener('click', () => { cb.checked = !cb.checked; onChange(cb.checked); }); r.appendChild(l); r.appendChild(cb); bd.appendChild(r); } row('自动作答(单选A / 多选全选)', EXAM.autoFill, (v) => { EXAM.autoFill = v; examCfgSave(); if (v) EXAM.filledOnce = false; }); row('自动交卷(自动识别验证码)', EXAM.autoSubmit, (v) => { EXAM.autoSubmit = v; examCfgSave(); }); const btns = document.createElement('div'); btns.style.cssText = 'display:flex;gap:8px;margin:10px 0 8px;'; const bStyle = 'flex:1;padding:7px 0;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;color:#fff;'; const bRefill = document.createElement('button'); bRefill.textContent = '重新作答'; bRefill.style.cssText = bStyle + 'background:#2563eb;'; bRefill.addEventListener('click', () => { const n = fillAnswers(); log('已重新作答:单选/判断 ' + n.singleCount + ' 题,多选选项 ' + n.multiCount + ' 个', 'ok'); }); const bSubmit = document.createElement('button'); bSubmit.textContent = '立即交卷'; bSubmit.style.cssText = bStyle + 'background:#dc2626;'; bSubmit.addEventListener('click', () => { EXAM.submitted = false; autoSubmitAuto(); }); btns.appendChild(bRefill); btns.appendChild(bSubmit); bd.appendChild(btns); const lg = document.createElement('div'); lg.style.cssText = 'height:110px;overflow-y:auto;background:#0b1220;border:1px solid #1f2937;border-radius:6px;padding:6px 8px;font-size:11.5px;user-select:text;-webkit-user-select:text;cursor:text;'; bd.appendChild(lg); root.appendChild(hd); root.appendChild(bd); document.body.appendChild(root); ST.panel = root; ST.logEl = lg; let folded = false; fold.addEventListener('click', () => { folded = !folded; bd.style.display = folded ? 'none' : 'block'; fold.textContent = folded ? '+' : '—'; }); } function startExamLoop() { setInterval(() => { clickStartIfAny(); if (!EXAM.autoFill || EXAM.filledOnce) return; const groups = document.querySelectorAll('.van-radio-group, .van-checkbox-group').length; const loading = document.querySelector('.q-loading'); if (groups > 0 && !loading) { EXAM.filledOnce = true; setTimeout(() => { const n = fillAnswers(); const total = n.singleCount + n.multiCount; log('已自动作答:单选/判断 ' + n.singleCount + ' 题,多选选项 ' + n.multiCount + ' 个', total ? 'ok' : 'warn'); if (!total) { EXAM.filledOnce = false; return; } reportToParent('progress', '已作答单选/判断 ' + n.singleCount + ' 题、多选 ' + n.multiCount + ' 个选项,准备交卷'); if (EXAM.autoSubmit) setTimeout(() => { autoSubmitAuto(); }, 1600); }, 900); } }, 1000); } function bootExam() { examCfgLoad(); installCanvasHook(); const inFrame = window.top !== window.self; if (inFrame) { // 由「我的考试」批量任务打开的考试窗口:不显示面板,直接作答、交卷并回报结果 EXAM.autoFill = true; EXAM.autoSubmit = true; startExamLoop(); watchSubmitResult(); log('批量考试模式:自动作答并交卷', 'run'); return; } buildExamPanel(); log('考试助手已启动(单选选A、多选全选、判断选正确)', 'run'); startExamLoop(); } /* ============================ 生命周期 ============================ */ function boot() { buildPanel(); installTokenSniffer(); const tk = getToken(); log('助手已启动 v1.2.4(' + (tk ? '登录态已获取,来源:' + TOKEN_SOURCE : '暂未获取登录态') + ')', 'run'); loadContext(); if (!ST.statusTimer) ST.statusTimer = setInterval(render, 1000); let lastHash = location.hash; setInterval(() => { if (location.hash !== lastHash) { lastHash = location.hash; ST.handledResId = ''; ST.record = null; ST.retryCount = 0; log('页面切换,重新加载…', 'info'); setTimeout(loadContext, 650); } }, 500); } // 考试站:只做自动作答,不加载课程相关逻辑 if (/examhz\.baosteel\.com/.test(location.hostname)) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(bootExam, 600)); } else { setTimeout(bootExam, 600); } return; } // 主站只在顶层窗口运行,避免同源 iframe 重复注入 if (window.top !== window.self) return; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(boot, 800)); } else { setTimeout(boot, 800); } })();