// ==UserScript== // @name 华莘学堂学习助手 // @namespace https://card.wlxy.live/details/CC9C84F2 // @version 1.0 // @description 华莘学堂(huashenxt.com)学习辅助:静默学习、按照勾选课程依次自动学习、课后作业;免费体验 3 章节,Pro 不限章节与作业(激活后约 30 天)。 // @antifeature payment 免费体验 3 章节,升级 Pro 不限章节与作业 // @antifeature membership 需云端授权 // @author 柠檬真酸 // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @match https://*.huashenxt.com/* // @match https://huashenxt.com/* // @noframes // @connect apicj.huashenxt.com // @connect oa42.ahzsksw.cn // @connect card.wlxy.live // @connect huaweicloudobs.ahjxjy.cn // @grant unsafeWindow // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @run-at document-start // @tag 华莘学堂 // @tag 成教 // @tag 学习辅助 // @license All Rights Reserved // ==/UserScript== (function () { 'use strict'; const VERSION = '1.0'; const CLOUD = 'https://oa42.ahzsksw.cn'; const PLATFORM = 'https://apicj.huashenxt.com'; const PREFIX = '/users/teachingprocController/'; const KEY = 'huashen_helper_v1'; const DEFAULT_PRO_BUY_URL = 'https://card.wlxy.live/details/CC9C84F2'; const OFFICIAL_TICK_SEC = 10; const OFFICIAL_GAP_MS = 10000; const REALTIME_READBACK_EVERY = 6; const EFF_GAP_MS = 2000; const EFF_MIN_STEP_SEC = 10; const EFF_STEP_SEC = 60; const EFF_MAX_STEP_SEC = 60; const EFF_PROBE_SEC = [60]; const EFF_READBACK_TRIES = 5; const EFF_READBACK_GAP_MS = 1200; const EFF_CREDIT_OK_SEC = 45; const EFF_MISS_LIMIT = 3; const EFF_AFTER_OK_GAP_MS = 12000; const END_DWELL_ROUNDS = 18; const END_DWELL_GAP_MS = 10000; const cleanText = value => String(value ?? '').replace(/<[^>]*>/g, '').replace(/ /gi, ' ').replace(/&/gi, '&').replace(/</gi, '<').replace(/>/gi, '>').replace(/(\d+);/g, (_, n) => String.fromCodePoint(Number(n))).replace(/\s+/g, ''); const _ck = (room, section) => `${room}:${section}`; const complete = item => Number(item.videoMinute) > 0 && Number(item.watchVideoMinute || 0) >= Number(item.videoMinute); const stem = q => q.question?.select?.text || q.question?.text || ''; const options = q => (q.question?.options || []).map(v => String(v.text ?? '')); const studyPace = value => value === 'efficiency' ? 'efficiency' : 'realtime'; const creditedSec = item => { const minutes = Math.max(Number(item.trueVideoMinute) || 0, Number(item.watchVideoMinute) || 0); return Number.isFinite(minutes) && minutes > 0 ? minutes * 60 : 0; }; function _aft(q, texts) { const opts = options(q).map(cleanText); if (!texts?.length || new Set(opts).size !== opts.length) return null; const indexes = texts.map(t => opts.indexOf(cleanText(t))); if (indexes.some(i => i < 0 || i > 25) || new Set(indexes).size !== indexes.length) return null; const letters = indexes.sort((a, b) => a - b).map(i => String.fromCharCode(65 + i)).join(''); return { equal: letters }; } function _ofa(q) { const ans = q.answer; if (!ans || typeof ans !== 'object' || Array.isArray(ans)) return null; const opts = options(q); if (opts.length) { const raw = ans.equal; if (typeof raw !== 'string') return null; const letters = raw.toUpperCase().replace(/[\s,\uff0c;\uff1b|]/g, ''); if (!/^[A-Z]+$/.test(letters) || new Set(letters).size !== letters.length) return null; if ([...letters].some(c => c.charCodeAt(0) - 65 >= opts.length)) return null; return { equal: letters }; } const tf = ans.equal; if (tf === 1 || tf === 2 || tf === '1' || tf === '2') return { equal: String(tf) }; if (typeof tf === 'boolean') return { equal: tf }; return null; } function _bi(q, answer) { const opts = options(q); if (!opts.length || typeof answer?.equal !== 'string') return null; const texts = [...answer.equal].map(c => opts[c.charCodeAt(0) - 65]); if (texts.some(t => !cleanText(t))) return null; return { stem: stem(q), options: opts, answer: { choices: texts } }; } function _nv(data, type, relation) { if (Number(data?.captureWait) > 0) return true; return (data?.behaviorList || []).some(b => Number(b.behaviorType) === type && String(b.relationno) === String(relation) && Number(b.captureWait) > 0); } function _vs(item, profile, session, domains = {}, accessToken = '') { domains = domains || {}; let url; try { url = new URL(item.url); } catch { throw new Error('\u8be5\u7ae0\u8282\u6ca1\u6709\u53ef\u64ad\u653e\u7684 HTTPS \u89c6\u9891'); } if (url.protocol !== 'https:' || url.username || url.password) throw new Error('\u8be5\u7ae0\u8282\u6ca1\u6709\u53ef\u64ad\u653e\u7684 HTTPS \u89c6\u9891'); const matches = domain => { if (typeof domain !== 'string' || !domain.trim()) return false; try { return new URL(domain.includes('://') ? domain : 'https://' + domain).hostname === url.hostname; } catch { return false; } }; const qiniu = url.hostname === 'vodlive.huashenxt.com' || matches(domains.privateDomainQn); if (qiniu && !url.searchParams.get('fileToken')) { const sameSession = profile?.userno != null && String(session?.userno) === String(profile.userno) && (!session?.accessToken || session.accessToken === accessToken); const token = profile?.fileToken || (sameSession && session?.fileToken); if (typeof token !== 'string' || !token.trim()) throw new Error('\u672a\u53d6\u5f97\u5f53\u524d\u767b\u5f55\u7684\u89c6\u9891\u51ed\u8bc1\uff0c\u8bf7\u5237\u65b0\u5e73\u53f0\u9875\u9762\u540e\u91cd\u8bd5\uff1b\u4ecd\u5931\u8d25\u65f6\u91cd\u65b0\u767b\u5f55'); url.searchParams.set('fileToken', token); } else if (!qiniu && matches(domains.privateDomainTx) && item.urlFileToken && !url.search) { url.search = String(item.urlFileToken).replace(/^\?/, ''); } return url.href; } function _mf(video) { const code = Number(video.error?.code || 0); const reason = { 1: '\u89c6\u9891\u52a0\u8f7d\u88ab\u4e2d\u6b62\uff0c\u8bf7\u91cd\u8bd5', 2: '\u89c6\u9891\u7f51\u7edc\u8bf7\u6c42\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u7f51\u7edc\u6216\u91cd\u65b0\u767b\u5f55\u540e\u91cd\u8bd5', 3: '\u6d4f\u89c8\u5668\u65e0\u6cd5\u89e3\u7801\u89c6\u9891\uff0c\u8bf7\u4f7f\u7528\u65b0\u7248 Chrome \u6216 Edge', 4: '\u89c6\u9891\u5730\u5740\u88ab\u62d2\u7edd\u6216\u683c\u5f0f\u4e0d\u53d7\u652f\u6301\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55\u540e\u91cd\u8bd5\uff0c\u5e76\u786e\u8ba4\u5e73\u53f0\u539f\u64ad\u653e\u5668\u53ef\u64ad\u653e', }[code] || '\u89c6\u9891\u52a0\u8f7d\u5931\u8d25\uff0c\u8bf7\u786e\u8ba4\u5e73\u53f0\u539f\u64ad\u653e\u5668\u53ef\u64ad\u653e'; return new Error(`${reason}\uff08\u64ad\u653e\u5668\u9519\u8bef\u7801 ${code}\uff09`); } function _asp(item) { const total = Number(item.videoMinute) * 60; if (!Number.isFinite(total) || total <= 0 || total > 86400) throw new Error('\u5e73\u53f0\u8fd4\u56de\u7684\u7ae0\u8282\u65f6\u957f\u5f02\u5e38\uff0c\u65e0\u6cd5\u5f00\u59cb\u5b66\u4e60'); const saved = creditedSec(item); return { total, resume: Math.min(Math.max(0, total - 0.25), Math.max(0, saved - 5)) }; } function _ev(gainedSec, attemptedSec = EFF_STEP_SEC, missStreak = 0) { const gained = Math.max(0, Number(gainedSec) || 0); const attempted = Math.max(0, Number(attemptedSec) || 0); const misses = Math.max(0, Number(missStreak) || 0); const need = attempted > 0 && attempted < EFF_CREDIT_OK_SEC ? Math.max(0.2, attempted * 0.75) : EFF_CREDIT_OK_SEC; if (gained >= need) return { pace: 'efficiency', stepSec: EFF_STEP_SEC, note: 'ok', missStreak: 0 }; const nextMiss = misses + 1; if (nextMiss >= EFF_MISS_LIMIT) { return { pace: 'realtime', stepSec: OFFICIAL_TICK_SEC, note: 'fallback', missStreak: nextMiss }; } return { pace: 'efficiency', stepSec: EFF_STEP_SEC, note: 'retry', missStreak: nextMiss }; } function _efp(credited, claimed) { return Math.max(0, Number(credited) || 0, Number(claimed) || 0); } function _plp(response) { if (!response || typeof response !== 'object') return response; const clone = { ...response }; if (clone.data && typeof clone.data === 'object' && !Array.isArray(clone.data)) { const data = { ...clone.data }; if (Array.isArray(data.behaviorList)) { const waiting = data.behaviorList.filter(b => Number(b.captureWait) > 0).length; data.behaviorList = `[${data.behaviorList.length} \u9879${waiting ? `\uff0c\u6838\u9a8c\u7b49\u5f85 ${waiting}` : ''}]`; } clone.data = data; } return clone; } function _ssp(pace, stepSec, position, total, credited, opts = {}) { const base = studyPace(pace) === 'efficiency' ? Math.max(0, Number(credited) || 0) : Math.max(Number(position) || 0, Number(credited) || 0); const remaining = Math.max(0, Number(total) - base); if (remaining <= 0) return { step: 0, target: Number(total), gapMs: OFFICIAL_GAP_MS, pace: studyPace(pace) }; if (studyPace(pace) === 'realtime') { const step = Math.min(OFFICIAL_TICK_SEC, remaining); const gapMs = step >= OFFICIAL_TICK_SEC ? OFFICIAL_GAP_MS : Math.max(400, Math.ceil(OFFICIAL_GAP_MS * (step / OFFICIAL_TICK_SEC))); return { step, target: base + step, gapMs, pace: 'realtime' }; } const step = Math.min(EFF_STEP_SEC, remaining); const gapMs = step >= 10 ? EFF_GAP_MS : Math.min(EFF_GAP_MS, Math.max(400, Math.ceil(step * 1000))); return { step, target: base + step, gapMs, pace: 'efficiency' }; } function _gbc(items) { const groups = new Map(); for (const item of items) { const key = String(item.classroomno ?? ''); if (!groups.has(key)) groups.set(key, []); groups.get(key).push(item); } return [...groups.values()].flat(); } function _dt(value, secrets = []) { let text = typeof value === 'string' ? value : JSON.stringify(value, (key, entry) => /token|authorization|cookie|password|secret|^pwd$|^lease$/i.test(key) ? '[\u5df2\u9690\u85cf]' : entry, 2); text = String(text ?? '\u65e0\u54cd\u5e94\u6b63\u6587'); for (const secret of secrets) if (typeof secret === 'string' && secret) { text = text.split(secret).join('[\u5df2\u9690\u85cf]').split(encodeURIComponent(secret)).join('[\u5df2\u9690\u85cf]'); } return text; } if (typeof module === 'object' && module.exports) { module.exports = { cleanText, _ck, complete, _aft, _ofa, _bi, _nv, _vs, _mf, studyPace, creditedSec, _asp, _ev, _efp, _ssp, _gbc, _dt, _plp, OFFICIAL_TICK_SEC, OFFICIAL_GAP_MS, REALTIME_READBACK_EVERY, EFF_GAP_MS, EFF_MIN_STEP_SEC, EFF_STEP_SEC, EFF_MAX_STEP_SEC, EFF_PROBE_SEC, EFF_READBACK_TRIES, EFF_READBACK_GAP_MS, EFF_CREDIT_OK_SEC, EFF_MISS_LIMIT, EFF_AFTER_OK_GAP_MS, END_DWELL_ROUNDS, END_DWELL_GAP_MS, }; return; } if (window.top !== window.self) return; const page = typeof unsafeWindow === 'undefined' ? window : unsafeWindow; if (page.__HUASHEN_HELPER_V1__) return; page.__HUASHEN_HELPER_V1__ = true; const store = { read(name, fallback) { try { return GM_getValue(`${KEY}_${name}`, fallback); } catch { return fallback; } }, write(name, value) { GM_setValue(`${KEY}_${name}`, value); }, }; const state = { headers: {}, profile: null, courses: [], chapters: [], selected: new Set(), token: store.read('token', ''), lease: null, config: null, running: false, stop: false, refreshing: false, logs: [], current: '\u7b49\u5f85\u8bfe\u7a0b\u9875\u9762\u52a0\u8f7d', pending: new Set(), video: null, autoLoaded: false, screen: 'courses', panel: null, boundIdentity: '', notice: '', owner: null, studyMode: 'api', pace: 'realtime', progressRequests: 0, activeChapters: new Set(), queueDone: 0, queueTotal: 0, }; const el = {}; const log = message => { state.logs.push(`${new Date().toLocaleTimeString()} ${message}`); state.logs = state.logs.slice(-100); if (el.logs) { el.logs.textContent = state.logs.join('\n'); el.logs.scrollTop = el.logs.scrollHeight; } }; function _sqp(done, total) { state.queueDone = done; state.queueTotal = total; if (el.queueText) el.queueText.textContent = `${done}/${total}`; if (el.queueBar) el.queueBar.style.width = total > 0 ? `${Math.min(100, (done / total) * 100)}%` : '0%'; } function status(message) { state.current = message; if (el.status) el.status.textContent = message; if (el.task) el.task.textContent = message; if (el.runStatus) { el.runStatus.textContent = state.running ? '\u8fd0\u884c\u4e2d' : '\u5df2\u505c\u6b62'; el.runStatus.classList.toggle('on', state.running); } } function _cs() { if (state.stop) throw new Error('\u4efb\u52a1\u5df2\u505c\u6b62'); if (state.running && state.boundIdentity && identity() !== state.boundIdentity) throw new Error('\u5e73\u53f0\u8d26\u53f7\u5df2\u5207\u6362\uff0c\u8bf7\u505c\u6b62\u540e\u5237\u65b0\u8bfe\u7a0b'); } async function wait(ms) { const end = Date.now() + ms; while (Date.now() < end) { _cs(); await new Promise(r => setTimeout(r, Math.min(250, end - Date.now()))); } _cs(); } function identity() { return `${state.headers.SolutionNo || ''}:${state.profile?.userno || ''}:${state.profile?.relationno || ''}`; } function _ah(headers) { let changed = false; for (const [key, value] of Object.entries(headers)) { const canonical = { accesstoken: 'AccessToken', solutionno: 'SolutionNo', applocalinfo: 'AppLocalInfo' }[key.toLowerCase()]; if (canonical && value && state.headers[canonical] !== String(value)) { changed = true; state.headers[canonical] = String(value); } } if (changed) { state.lease = null; state.profile = null; if (state.running) _sr('\u767b\u5f55\u72b6\u6001\u6539\u53d8\uff0c\u4efb\u52a1\u5df2\u505c\u6b62\uff0c\u8bf7\u5237\u65b0\u8bfe\u7a0b'); } } function observe(url, headers, response) { try { const u = new URL(String(url), location.href); if (u.origin !== PLATFORM || !u.pathname.startsWith('/users/')) return; _ah(headers); if (u.pathname.endsWith('/getUserInfo') && Number(response?.code) === 200) state.profile = response.data; if (state.headers.AccessToken && state.headers.SolutionNo && !state.autoLoaded && state.panel) { state.autoLoaded = true; setTimeout(() => refresh().catch(report), 500); } } catch { /* Ignore unrelated page traffic. */ } } function _io() { const proto = page.XMLHttpRequest.prototype; const open = proto.open, setHeader = proto.setRequestHeader, send = proto.send; const requests = new WeakMap(); proto.open = function (method, url, ...args) { requests.set(this, { url, headers: {} }); return open.call(this, method, url, ...args); }; proto.setRequestHeader = function (name, value) { const request = requests.get(this); if (request) request.headers[name] = value; return setHeader.call(this, name, value); }; proto.send = function (...args) { const request = requests.get(this); if (request) { observe(request.url, request.headers, null); this.addEventListener('load', () => { let data; try { data = this.responseType === 'json' ? this.response : JSON.parse(this.responseText); } catch { /* Binary response. */ } observe(request.url, request.headers, data); }, { once: true }); } return send.apply(this, args); }; const fetch = page.fetch; if (fetch) page.fetch = async function (input, init) { const url = typeof input === 'string' || input instanceof URL ? String(input) : input.url; const headers = Object.fromEntries(new page.Headers(init?.headers || input?.headers || {})); observe(url, headers, null); const response = await fetch.call(this, input, init); if (String(url).startsWith(PLATFORM)) response.clone().json().then(data => observe(url, headers, data)).catch(() => {}); return response; }; } _io(); const messages = { free_chapter_quota_exceeded: '\u514d\u8d39\u4f53\u9a8c 3 \u7ae0\u8282\u5df2\u7528\u5b8c\uff0c\u8bf7\u586b\u5199\u6709\u6548 Pro Token', token_bound_to_other_learning_user: '\u6b64 Pro Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u5e73\u53f0\u8d26\u53f7', invalid_or_revoked_token: 'Pro Token \u65e0\u6548\u6216\u5df2\u505c\u7528', pro_expired: 'Pro \u5df2\u5230\u671f', invalid_token: 'Pro Token \u65e0\u6548', invalid_lease: '\u6388\u6743\u4f1a\u8bdd\u5931\u6548', chapter_not_authorized: '\u6b64\u7ae0\u8282\u5c1a\u672a\u53d6\u5f97\u4f53\u9a8c\u6388\u6743', }; function report(error) { const message = messages[error.message] || error.message || '\u8bf7\u6c42\u5931\u8d25'; log(message); status(message); } function request(url, method = 'GET', body, headers = {}, onResult) { return new Promise((resolve, reject) => { let handle, done = false; const finish = (fn, value, result) => { if (done) return; done = true; state.pending.delete(handle); try { onResult?.(result); } catch { /* Diagnostics must not change request handling. */ } fn(value); }; handle = GM_xmlhttpRequest({ url, method, headers: { Accept: 'application/json', ...(body ? { 'Content-Type': 'application/json' } : {}), ...headers }, data: body ? JSON.stringify(body) : undefined, anonymous: true, timeout: 30000, onload: r => { let d; try { d = JSON.parse(r.responseText); } catch { finish(reject, new Error('\u63a5\u53e3\u672a\u8fd4\u56de JSON\uff0c\u8bf7\u68c0\u67e5\u767b\u5f55\u6216\u4e91\u7aef\u90e8\u7f72'), { status: r.status, event: '\u975e JSON \u54cd\u5e94', raw: r.responseText }); return; } const result = { status: r.status, event: '\u6536\u5230\u54cd\u5e94', response: d }; if (d == null || r.status < 200 || r.status >= 300 || d.ok === false || (d.code !== undefined && Number(d.code) !== 200)) { const e = new Error(String(d?.detail || d?.message || d?.msg || `HTTP ${r.status}`)); e.status = r.status; finish(reject, e, result); } else finish(resolve, d, result); }, onerror: r => finish(reject, new Error('\u7f51\u7edc\u8bf7\u6c42\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u8fde\u63a5\u548c\u811a\u672c\u8de8\u57df\u6743\u9650'), { status: r?.status, event: '\u7f51\u7edc\u5931\u8d25\uff0c\u65e0\u5e73\u53f0\u54cd\u5e94' }), ontimeout: r => finish(reject, new Error('\u63a5\u53e3\u8bf7\u6c42\u8d85\u65f6'), { status: r?.status, event: '\u8bf7\u6c42\u8d85\u65f6\uff0c\u65e0\u5e73\u53f0\u54cd\u5e94' }), onabort: () => finish(reject, new Error('\u4efb\u52a1\u5df2\u505c\u6b62'), { event: '\u8bf7\u6c42\u5df2\u53d6\u6d88\uff0c\u65e0\u5e73\u53f0\u54cd\u5e94' }), }); if (!done) state.pending.add(handle); }); } async function api(path, query, body) { if (!state.headers.AccessToken || !state.headers.SolutionNo) throw new Error('\u8bf7\u5148\u5728\u534e\u8398\u5b66\u5802\u767b\u5f55\u5e76\u6253\u5f00\u8bfe\u7a0b\u9875\u9762\uff0c\u518d\u70b9\u5237\u65b0'); const url = new URL('/users/' + path, PLATFORM); for (const [k, v] of Object.entries(query || {})) if (v != null) url.searchParams.set(k, String(v)); const d = await request(url.href, body ? 'POST' : 'GET', body, state.headers); return d.data; } function _ps(item) { const learned = Math.floor(Number(item.watchVideoMinute) || 0); const total = Math.ceil(Number(item.videoMinute) || 0); const name = item.course?.classroomName || item.classroomName || '\u8bfe\u7a0b'; return `${name} · \u5df2\u5b66 ${learned}/${total} \u5206\u949f`; } function _lc(item, prevMin, nowMin) { if (!(nowMin > prevMin)) return; log(`${item.section || item.chapter || '\u672c\u7ae0'} · \u5df2\u5b66 ${nowMin}/${item.videoMinute || 0} \u5206\u949f`); } async function cloud(path, body) { return request(CLOUD + '/api/huashen/' + path, body ? 'POST' : 'GET', body); } async function _el(force = false) { if (!state.profile?.userno) throw new Error('\u5c1a\u672a\u8bc6\u522b\u767b\u5f55\u8d26\u53f7\uff0c\u8bf7\u5237\u65b0\u8bfe\u7a0b'); const id = identity(); if (!force && state.lease?.identity === id && state.lease.exp * 1000 > Date.now() + 30000) return state.lease; const d = await request(CLOUD + '/api/huashen/lease', 'POST', { learning_user_id: String(state.profile.userno), solution_no: String(state.headers.SolutionNo), name: String(state.profile.studentInfo?.name || ''), user_name: String(state.profile.nickname || ''), }, state.token ? { Authorization: 'Bearer ' + state.token } : {}); if (!d.lease || !['free', 'pro'].includes(d.tier)) throw new Error('\u4e91\u7aef\u6388\u6743\u54cd\u5e94\u683c\u5f0f\u4e0d\u6b63\u786e'); if (id !== identity()) throw new Error('\u5e73\u53f0\u8d26\u53f7\u5df2\u5207\u6362\uff0c\u8bf7\u5237\u65b0\u8bfe\u7a0b'); state.lease = { ...d, identity: id }; _rt(); return state.lease; } async function _cc(path, item, extra = {}) { _cs(); const payload = () => ({ lease: state.lease.lease, classroom_no: String(item.classroomno), chapter_section: String(item.chapterSection), ...extra }); await _el(); _cs(); let d; try { d = await cloud(path, payload()); } catch (e) { if (e.message !== 'invalid_lease') throw e; await _el(true); _cs(); d = await cloud(path, payload()); } if (d.free_used_chapters !== undefined) state.lease.free_used_chapters = d.free_used_chapters; _rt(); return d; } async function refresh() { if (state.running || state.refreshing) return; state.refreshing = true; _sb(); try { status('\u6b63\u5728\u8bfb\u53d6\u8bfe\u7a0b\u4e0e\u7ae0\u8282…'); state.profile = await api('systemController/getUserInfo'); const roles = (state.profile.roles || []).filter(r => Number(r.roleno) === 5 && r.classno && r.studentno); if (!roles.length) throw new Error('\u5f53\u524d\u8d26\u53f7\u6ca1\u6709\u5b66\u751f\u73ed\u7ea7\u89d2\u8272'); const courses = [], seen = new Set(); for (const role of roles) { let offset = 0; while (true) { const data = await api('systemController/getStudentIndex', { classno: role.classno, studentCourseOffset: offset, studentCourseLimit: 50, openClassroomOffset: 0, openClassroomLimit: 0, trainClassroomOffset: 0, trainClassroomLimit: 0, }); const list = data?.studentCourseList || []; for (const c of list) if (!seen.has(String(c.classroomno))) { seen.add(String(c.classroomno)); courses.push({ ...c, role }); } offset += list.length; if (!list.length || offset >= Number(data.studentCourseCount ?? offset)) break; if (offset > 5000) throw new Error('\u8bfe\u7a0b\u5206\u9875\u5f02\u5e38\uff0c\u8bf7\u53cd\u9988'); } } state.courses = courses; const chapters = []; for (const c of courses) { const info = await api('teachingprocController/getClassroomInfo', { classroomno: c.classroomno }); c.info = info; for (const ch of info.courseware || []) { chapters.push({ ...ch, classroomno: c.classroomno, course: c, key: _ck(c.classroomno, ch.chapterSection), kind: 'video' }); } for (const exam of info.examList || []) if (Number(exam.exerciseType) === 2 && exam.chapterSection != null && !chapters.some(ch => ch.key === _ck(c.classroomno, exam.chapterSection))) { chapters.push({ ...exam, classroomno: c.classroomno, course: c, key: _ck(c.classroomno, exam.chapterSection), kind: 'homework' }); } } state.chapters = chapters; state.selected = new Set(chapters.filter(ch => ch.kind === 'homework' || !complete(ch)).map(ch => ch.key)); _rc(); status(`\u5df2\u52a0\u8f7d ${courses.length} \u95e8\u8bfe\u7a0b\u3001${chapters.length} \u4e2a\u7ae0\u8282`); log(`\u8bfe\u7a0b\u52a0\u8f7d\u5b8c\u6210\uff1a${chapters.length} \u4e2a\u7ae0\u8282`); try { state.config = await cloud('client-config'); el.notice.textContent = state.config.panel_notice || '\u514d\u8d39\u4f53\u9a8c 3 \u7ae0\u8282\uff1b\u6709\u6548 Pro \u4e0d\u9650\u7ae0\u8282\u4e0e\u4f5c\u4e1a\u3002'; const buyUrl = /^https:\/\//i.test(state.config.pro_buy_url || '') ? state.config.pro_buy_url : DEFAULT_PRO_BUY_URL; el.buy.href = buyUrl; el.buy.hidden = false; await _el(true); } catch (e) { report(e); } } finally { state.refreshing = false; _sb(); } } async function study(item, opts = {}) { if (item.kind !== 'video' || complete(item)) return; if (_nv(item, 3, item.coursewareno)) throw new Error('\u8bf7\u5148\u5728\u539f\u5e73\u53f0\u5b8c\u6210\u672c\u7ae0\u8282\u6838\u9a8c'); if (item.beginTime && Date.now() < Number(item.beginTime)) throw new Error('\u672c\u7ae0\u8282\u5c1a\u672a\u5f00\u653e'); if (item.endTime && Date.now() > Number(item.endTime)) throw new Error('\u672c\u7ae0\u8282\u5b66\u4e60\u65f6\u95f4\u5df2\u7ed3\u675f'); if (Number(item.course.info?.teachingMethod ?? item.course.teachingMethod) === 2) throw new Error('\u76f4\u64ad\u8bfe\u7a0b\u8bf7\u5728\u539f\u5e73\u53f0\u5b66\u4e60'); return _sba(item); } async function _rcp(item) { const info = await api('teachingprocController/getClassroomInfo', { classroomno: item.classroomno }); _cs(); const list = info.courseware || []; const latest = list.find(ch => item.coursewareno != null && String(ch.coursewareno) === String(item.coursewareno)) || list.find(ch => String(ch.chapterSection) === String(item.chapterSection)); if (latest) Object.assign(item, latest); return creditedSec(item); } async function _rcg(item, before, needSec = EFF_CREDIT_OK_SEC) { const need = Math.max(1, Number(needSec) || EFF_CREDIT_OK_SEC); let after = before; for (let i = 0; i < EFF_READBACK_TRIES; i++) { await wait(EFF_READBACK_GAP_MS); _cs(); after = await _rcp(item); if (after - before >= need) return { after, gained: after - before }; } return { after, gained: Math.max(0, after - before) }; } async function _rp(item, studentno, position, total) { const data = await api('teachingprocController/classroomMissionAdd', null, { classroomno: item.classroomno, missionType: 2, chapterSection: item.chapterSection, remark: '\u89c2\u770b\u89c6\u9891', postsOffset: 0, postsLimit: 20, currentWatchSecond: position, totalVideoSecond: total, studentno, }); _cs(); if (_nv(data, 3, item.coursewareno)) throw new Error('\u5e73\u53f0\u8981\u6c42\u5b66\u4e60\u6838\u9a8c\uff0c\u8bf7\u56de\u539f\u5e73\u53f0\u5b8c\u6210\u540e\u7eed\u5b66'); return data; } async function _sba(item) { const plan = _asp(item); const pace = 'realtime'; await _cc('chapter/consume', item); _cs(); const studentno = item.course.role.studentno; let signedIn = false; try { await api('teachingprocController/classroomMissionAdd', null, { classroomno: item.classroomno, studentno, missionType: 1, remark: '\u8bfe\u5802\u7b7e\u5230', }); signedIn = true; _cs(); let position = creditedSec(item) || plan.resume; let lastQuota = performance.now(), ticks = 0, stalled = 0; let lastWatchMin = Number(item.watchVideoMinute) || 0; let lastAliveLog = performance.now(); const ALIVE_LOG_MS = 5 * 60 * 1000; log(`\u5f00\u59cb\u5b66\u4e60\uff1a${item.course.classroomName} / ${item.section || item.chapter}`); status(_ps(item)); while (!complete(item)) { _cs(); const beforeWatch = Number(item.watchVideoMinute) || 0; const before = creditedSec(item); const tick = _ssp(pace, EFF_STEP_SEC, position, plan.total, before); if (tick.step <= 0) { await wait(OFFICIAL_GAP_MS); _cs(); await _rp(item, studentno, plan.total, plan.total); ticks += 1; } else { await wait(tick.gapMs); _cs(); position = tick.target; await _rp(item, studentno, position, plan.total); ticks += 1; } if (performance.now() - lastQuota >= 30000) { await _cc('chapter/consume', item); lastQuota = performance.now(); } if (ticks % REALTIME_READBACK_EVERY === 0 || tick.step <= 0) { await _rcp(item); const nowMin = Number(item.watchVideoMinute) || 0; position = Math.max(position, creditedSec(item)); _lc(item, beforeWatch, nowMin); if (nowMin > lastWatchMin) { lastWatchMin = nowMin; stalled = 0; lastAliveLog = performance.now(); } else { stalled += 1; if (stalled >= 3) throw new Error(`\u5b66\u4e60\u8fdb\u5ea6\u957f\u65f6\u95f4\u65e0\u66f4\u65b0\uff08\u4ecd\u4e3a ${nowMin}/${item.videoMinute} \u5206\u949f\uff09\uff0c\u8bf7\u5237\u65b0\u540e\u7eed\u5b66`); } _rc(); } if (performance.now() - lastAliveLog >= ALIVE_LOG_MS) { log(`\u5b66\u4e60\u4e2d · ${_ps(item)}`); lastAliveLog = performance.now(); } status(_ps(item)); } await _rcp(item); if (complete(item)) { log(`\u5df2\u5b8c\u6210\uff1a${item.section || item.chapter}\uff08${item.watchVideoMinute || 0}/${item.videoMinute} \u5206\u949f\uff09`); status(_ps(item)); _rc(); return; } _rc(); throw new Error(`\u672c\u7ae0\u5c1a\u672a\u5b66\u6ee1\uff08${item.watchVideoMinute || 0}/${item.videoMinute} \u5206\u949f\uff09\uff0c\u8bf7\u7a0d\u540e\u7eed\u5b66`); } finally { if (signedIn && !state.stop) { await api('teachingprocController/classroomMissionAdd', null, { classroomno: item.classroomno, studentno, missionType: 1, remark: '\u79bb\u5f00\u8bfe\u5802', }).catch(() => {}); } } } async function _sbp(item) { let session, domains; try { session = JSON.parse(page.sessionStorage.getItem('sessionUser') || '{}'); } catch { session = {}; } try { const config = JSON.parse(page.localStorage.getItem('configList') || '{}'); domains = JSON.parse(config.resDomain?.param || '{}'); } catch { domains = {}; } const source = _vs(item, state.profile, session, domains, state.headers.AccessToken); const plan = _asp(item); await _cc('chapter/consume', item); _cs(); const studentno = item.course.role.studentno; let signedIn = false; try { await api('teachingprocController/classroomMissionAdd', null, { classroomno: item.classroomno, studentno, missionType: 1, remark: '\u8bfe\u5802\u7b7e\u5230' }); signedIn = true; _cs(); const video = el.video; state.video = video; video.hidden = false; await new Promise((resolve, reject) => { let settled = false; const timer = setTimeout(() => done(new Error('\u89c6\u9891\u52a0\u8f7d\u8d85\u65f6\uff0c\u8bf7\u68c0\u67e5\u7f51\u7edc')), 30000); const poll = setInterval(() => { if (state.stop) done(new Error('\u4efb\u52a1\u5df2\u505c\u6b62')); }, 200); function done(err) { if (settled) return; settled = true; clearTimeout(timer); clearInterval(poll); video.removeEventListener('loadedmetadata', loaded); video.removeEventListener('error', failed); err ? reject(err) : resolve(); } const loaded = () => done(); const failed = () => done(_mf(video)); video.addEventListener('loadedmetadata', loaded, { once: true }); video.addEventListener('error', failed, { once: true }); try { video.src = source; video.load(); if (video.error) failed(); else if (video.readyState >= 1) done(); } catch { done(new Error('\u6d4f\u89c8\u5668\u65e0\u6cd5\u52a0\u8f7d\u89c6\u9891\uff0c\u8bf7\u5237\u65b0\u9875\u9762\u540e\u91cd\u8bd5')); } }); _cs(); let position = creditedSec(item) || plan.resume; const mediaTotal = Number(video.duration); const total = Number.isFinite(mediaTotal) && mediaTotal > 0 ? Math.max(plan.total, mediaTotal) : plan.total; const seekTo = Math.min(position, Math.max(0, (Number.isFinite(mediaTotal) ? mediaTotal : position) - 0.5)); if (seekTo > 0) video.currentTime = seekTo; video.defaultPlaybackRate = 1; video.playbackRate = 1; try { video.playbackRate = 1; } catch { /* ignore */ } try { await video.play(); } catch { status('\u6d4f\u89c8\u5668\u963b\u6b62\u81ea\u52a8\u64ad\u653e\uff0c\u8bf7\u70b9\u51fb\u9762\u677f\u89c6\u9891\u7684\u64ad\u653e\u6309\u94ae'); } let lastQuota = performance.now(), ticks = 0, stalled = 0; log(`\u5f00\u59cb\u8bfe\u65f6\uff08\u64ad\u653e\u5668 1x·10\u79d2\u62a5+10\uff09\uff1a${item.course.classroomName} / ${item.section || item.chapter} · \u52ff\u62d6\u52a8/\u500d\u901f`); while (!complete(item)) { _cs(); const before = creditedSec(item); const tick = _ssp('realtime', OFFICIAL_TICK_SEC, position, plan.total, before); if (tick.step <= 0) break; const gapEnd = Date.now() + tick.gapMs; while (Date.now() < gapEnd) { _cs(); if (video.error) throw _mf(video); if (video.playbackRate !== 1) video.playbackRate = 1; const cap = Math.min(tick.target, Number.isFinite(mediaTotal) ? mediaTotal : tick.target); if (Number.isFinite(video.currentTime) && video.currentTime > cap + 0.75) { video.currentTime = Math.max(0, cap - 0.1); } if (video.ended || video.paused) { const hold = Math.max(0, Math.min(cap, (Number.isFinite(mediaTotal) ? mediaTotal : cap) - 0.35)); if (Number.isFinite(hold)) video.currentTime = hold; try { await video.play(); } catch { /* wait gap */ } } await wait(Math.min(250, gapEnd - Date.now())); } _cs(); if (performance.now() - lastQuota >= 30000) { await _cc('chapter/consume', item); lastQuota = performance.now(); } position = tick.target; await _rp(item, studentno, Math.min(position, total), total); ticks += 1; if (ticks % REALTIME_READBACK_EVERY === 0 || position >= plan.total - 0.01) { const prevMin = Number(item.watchVideoMinute) || 0; const after = await _rcp(item); const gained = Math.max(0, after - before); const nowMin = Number(item.watchVideoMinute) || 0; position = Math.max(position, after); if (nowMin > prevMin) { log(`\u5e73\u53f0\u5165\u8d26 ${prevMin}→${nowMin} \u5206\u949f · \u7533\u62a5 ${Math.floor(position)}s · \u63a8\u9001 #${ticks}`); } if (complete(item) || position >= plan.total - 0.01) { stalled = 0; } else if (gained <= 0 && ticks >= REALTIME_READBACK_EVERY) { stalled += 1; if (stalled >= 3) throw new Error('\u5e73\u53f0\u957f\u65f6\u95f4\u672a\u786e\u8ba4\u8bfe\u65f6\u589e\u957f\uff0c\u8bf7\u6539\u7528\u9759\u9ed8\u540e\u53f0\u6216\u7a0d\u540e\u91cd\u8bd5'); } else if (gained > 0) { stalled = 0; } _rc(); } status(`${item.course.classroomName} · \u64ad\u653e\u56681x · \u7533\u62a5 ${Math.floor(position / 60)}/${Math.ceil(plan.total / 60)} \u5206\u949f · \u5e73\u53f0 ${item.watchVideoMinute || 0}/${item.videoMinute} \u5206\u949f`); } await _rcp(item); if (!complete(item)) throw new Error(`\u8fdb\u5ea6\u5df2\u63a8\u9001\uff0c\u4f46\u5e73\u53f0\u4ec5\u786e\u8ba4 ${item.watchVideoMinute || 0}/${item.videoMinute} \u5206\u949f\uff1b\u8bf7\u6539\u7528\u9759\u9ed8\u540e\u53f0\u6216\u7eed\u5b66`); log(`\u8bfe\u65f6\u5b8c\u6210\uff1a${item.section || item.chapter}\uff08\u64ad\u653e\u5668\uff0c\u5e73\u53f0\u5df2\u786e\u8ba4\uff09`); _rc(); } finally { el.video.pause(); el.video.removeAttribute('src'); el.video.load(); el.video.hidden = true; state.video = null; if (signedIn && !state.stop) { await api('teachingprocController/classroomMissionAdd', null, { classroomno: item.classroomno, studentno, missionType: 1, remark: '\u79bb\u5f00\u8bfe\u5802' }).catch(() => log('\u79bb\u5f00\u8bfe\u5802\u4e8b\u4ef6\u672a\u786e\u8ba4')); } } } async function homework(item, completedExams) { const c = item.course; const exams = (c.info.examList || []).filter(exam => Number(exam.exerciseType) === 2 && String(exam.chapterSection) === String(item.chapterSection)); for (const exam of exams) { _cs(); if (completedExams.has(String(exam.examno))) continue; if (exam.beginTime && Date.now() < Number(exam.beginTime)) { log(`\u4f5c\u4e1a\u672a\u5f00\u653e\uff1a${exam.examName}`); continue; } if (exam.endTime && Date.now() > Number(exam.endTime)) { log(`\u4f5c\u4e1a\u5df2\u622a\u6b62\uff1a${exam.examName}`); continue; } if (exam.remainderCount != null && Number(exam.remainderCount) <= 0) { log(`\u4f5c\u4e1a\u6b21\u6570\u5df2\u7528\u5b8c\uff1a${exam.examName}`); continue; } await _cc('chapter/consume', item); _cs(); status(`\u6b63\u5728\u5904\u7406\u4f5c\u4e1a\uff1a${exam.examName}`); const detail = await api('teachingprocController/getExamInfo', { examno: exam.examno }); if (Number(detail.exerciseType) !== 2) throw new Error('\u63a5\u53e3\u8fd4\u56de\u7684\u4e0d\u662f\u8bfe\u540e\u4f5c\u4e1a\uff0c\u5df2\u505c\u6b62'); if (_nv(detail, 2, exam.examno) || Number(detail.captureMax) > Number(detail.captureCount || 0)) throw new Error('\u4f5c\u4e1a\u8981\u6c42\u6838\u9a8c\uff0c\u8bf7\u5728\u539f\u5e73\u53f0\u5b8c\u6210\u540e\u518d\u7ee7\u7eed'); if (Number(detail.answerCount) > 0) { completedExams.add(String(exam.examno)); log(`\u5df2\u6709\u63d0\u4ea4\u8bb0\u5f55\uff0c\u8df3\u8fc7\u4f5c\u4e1a\uff1a${exam.examName}`); continue; } const questions = detail.content || []; if (!questions.length) throw new Error('\u4f5c\u4e1a\u9898\u76ee\u4e3a\u7a7a\uff0c\u5df2\u505c\u6b62'); const answers = questions.map(_ofa); const missing = questions.map((q, i) => ({ q, i })).filter(v => !answers[v.i] && options(v.q).length && stem(v.q)); if (missing.length) { const bank = await _cc('qbank/lookup', item, { questions: missing.map(({ q }) => ({ stem: stem(q), options: options(q) })) }); missing.forEach(({ q, i }, index) => { if (bank.items?.[index]?.found) answers[i] = _aft(q, bank.items[index].answer?.choices); }); } const unknown = answers.filter(a => !a).length; if (unknown) { log(`${exam.examName}\uff1a${unknown} \u9053\u9898\u7f3a\u5c11\u53ef\u9760\u7b54\u6848\uff0c\u8bf7\u5728\u539f\u5e73\u53f0\u5b8c\u6210\uff1b\u672a\u63d0\u4ea4`); continue; } _cs(); const payload = { examno: exam.examno, studentno: c.role.studentno }; const event = async (type, remark) => { _cs(); const data = await api('teachingprocController/addExamAnswer', null, { ...payload, examEvent: { examEventType: type, remark } }); if (_nv(data, 2, exam.examno)) throw new Error('\u5e73\u53f0\u8981\u6c42\u4f5c\u4e1a\u6838\u9a8c\uff0c\u8bf7\u56de\u539f\u5e73\u53f0\u5904\u7406'); return data; }; const started = Date.now(); await event('enter', 'web\u8fdb\u5165'); await event('get', '\u83b7\u53d6\u5185\u5bb9\u6210\u529f'); const minimum = Math.max(0, Number(detail.minSecond || 0)); if (minimum > 86400) throw new Error('\u4f5c\u4e1a\u6700\u77ed\u65f6\u95f4\u5f02\u5e38\uff0c\u8bf7\u5728\u5e73\u53f0\u786e\u8ba4'); while (Date.now() - started < minimum * 1000) { status(`\u4f5c\u4e1a\u8981\u6c42\u6700\u77ed\u4f5c\u7b54\u65f6\u95f4\uff0c\u5269\u4f59 ${Math.ceil(minimum - (Date.now() - started) / 1000)} \u79d2`); await wait(Math.min(10000, minimum * 1000 - (Date.now() - started))); await _cc('chapter/consume', item); await event('heartbeat', '\u5fc3\u8df3'); } await _cc('chapter/consume', item); _cs(); await api('teachingprocController/addExamAnswer', null, { ...payload, answer: questions.map((q, i) => ({ isFrom: 'web', examQuestionno: q.examQuestionno, answer: answers[i] })) }); await event('submit', '\u624b\u52a8\u63d0\u4ea4\u7b54\u6848\u6210\u529f'); let records = []; for (let attempt = 1; attempt <= 5; attempt++) { await wait(attempt === 1 ? 2000 : 2500); _cs(); const history = await api('teachingprocController/getExamAnswerList', { examno: exam.examno }); records = (history.examAnswerList || []).filter(v => String(v.studentno) === String(c.role.studentno)); if (records.length) break; log(`\u4f5c\u4e1a\u63d0\u4ea4\u786e\u8ba4\u4e2d\uff08${attempt}/5\uff09\uff1a${exam.examName}`); } if (!records.length) { throw new Error('\u63d0\u4ea4\u8bf7\u6c42\u5df2\u53d1\u9001\uff0c\u5c1a\u672a\u786e\u8ba4\u4f5c\u4e1a\u8bb0\u5f55\uff1b\u8bf7\u5230\u539f\u5e73\u53f0\u6838\u5b9e\uff0c\u907f\u514d\u91cd\u590d\u63d0\u4ea4'); } completedExams.add(String(exam.examno)); log(`\u4f5c\u4e1a\u5df2\u63d0\u4ea4\uff1a${exam.examName}\uff08${records.some(v => Number(v.isWaitMarking) === 1) ? '\u7b49\u5f85\u5e73\u53f0\u6279\u6539' : '\u5e73\u53f0\u5df2\u8bb0\u5f55'}\uff09`); const items = questions.map(q => _bi(q, _ofa(q))).filter(Boolean).map(entry => ({ ...entry, paper_name: String(exam.examName || ''), cour_no: String(item.classroomno), source: 'huashen-platform', })); if (items.length) { try { await _cc('qbank/upsert', item, { items }); } catch (e) { log('\u4f5c\u4e1a\u5df2\u63d0\u4ea4\uff0c\u9898\u5e93\u540c\u6b65\u5931\u8d25\uff1a' + (messages[e.message] || e.message)); } } await event('exit', '\u79bb\u5f00'); } } async function start(mode) { if (state.running || state.refreshing) return; if (navigator.locks?.request) { try { await navigator.locks.request(`${KEY}_queue`, { ifAvailable: true }, async lock => { if (!lock) { log('\u53e6\u4e00\u4e2a\u534e\u8398\u9875\u9762\u6b63\u5728\u5904\u7406\u961f\u5217\uff0c\u8bf7\u5148\u505c\u6b62\u8be5\u9875\u9762'); return; } await _rq(mode); }); } catch (e) { report(e); } return; } const lockKey = `${KEY}_queue_lock`; try { const prior = JSON.parse(localStorage.getItem(lockKey) || 'null'); if (prior && prior.until > Date.now()) { log('\u53e6\u4e00\u4e2a\u534e\u8398\u9875\u9762\u6b63\u5728\u5904\u7406\u961f\u5217\uff0c\u8bf7\u5148\u505c\u6b62\u8be5\u9875\u9762'); return; } const owner = crypto.randomUUID(); state.owner = owner; const renew = () => localStorage.setItem(lockKey, JSON.stringify({ owner, until: Date.now() + 15000 })); renew(); await new Promise(r => setTimeout(r, 150)); if (JSON.parse(localStorage.getItem(lockKey) || 'null')?.owner !== owner) throw new Error('\u53e6\u4e00\u9875\u9762\u5df2\u53d6\u5f97\u8fd0\u884c\u6743\uff0c\u8bf7\u7a0d\u540e\u518d\u8bd5'); const timer = setInterval(() => { try { if (JSON.parse(localStorage.getItem(lockKey) || 'null')?.owner !== owner) _sr('\u9875\u9762\u8fd0\u884c\u6743\u5df2\u6539\u53d8\uff0c\u4efb\u52a1\u505c\u6b62'); else renew(); } catch { _sr('\u65e0\u6cd5\u4fdd\u6301\u9875\u9762\u8fd0\u884c\u6743\uff0c\u4efb\u52a1\u505c\u6b62'); } }, 5000); try { await _rq(mode); } finally { clearInterval(timer); if (JSON.parse(localStorage.getItem(lockKey) || 'null')?.owner === owner) localStorage.removeItem(lockKey); } } catch (e) { report(e); } finally { state.owner = null; } } async function _mp(items, limit, worker) { let next = 0; const workers = Array.from({ length: Math.min(Math.max(1, limit), items.length) }, async () => { while (!state.stop) { const index = next++; if (index >= items.length) return; await worker(items[index], index); } }); await Promise.all(workers); } function _al() { const names = [...state.activeChapters].slice(0, 3); if (!names.length) return '\u7b49\u5f85\u4e0b\u4e00\u8282'; return names.join('\u3001') + (state.activeChapters.size > 3 ? ` \u7b49 ${state.activeChapters.size} \u8282` : ''); } async function _rq(mode) { if (state.running || state.refreshing) return; const queue = state.chapters.filter(ch => state.selected.has(ch.key)); if (!queue.length) { log('\u8bf7\u5148\u5237\u65b0\u8bfe\u7a0b\u5e76\u52fe\u9009\u7ae0\u8282'); return; } state.running = true; state.stop = false; state.boundIdentity = identity(); state.activeChapters.clear(); _sb(); status(state.current); const completedExams = new Set(); const ordered = _gbc(queue); const roomCount = new Set(ordered.map(ch => String(ch.classroomno))).size; let done = 0, failed = 0; _sqp(0, ordered.length); try { await _el(true); _cs(); const modeLabel = mode === 'pipeline' ? '\u8bfe\u65f6\uff0b\u4f5c\u4e1a' : mode === 'study' ? '\u4ec5\u8bfe\u65f6' : '\u4ec5\u4f5c\u4e1a'; log(`\u5f00\u59cb\u961f\u5217 ${ordered.length} \u8282 · ${roomCount} \u95e8\u8bfe · ${modeLabel}`); await _mp(ordered, 1, async item => { _cs(); const courseName = item.course.classroomName || item.course.disciplineName || '\u8bfe\u7a0b'; const label = `${courseName} / ${item.section || item.chapter || '\u8bfe\u540e\u4f5c\u4e1a'}`; state.activeChapters.add(label); if (el.courseNow) el.courseNow.textContent = courseName; status(item.kind === 'video' ? _ps(item) : `\u5904\u7406\u4e2d · ${label}`); try { if (mode !== 'homework') await study(item); _cs(); if (mode !== 'study') await homework(item, completedExams); done += 1; _sqp(done, ordered.length); log(`\u5b8c\u6210\uff08${done}/${ordered.length}\uff09\uff1a${label}`); } catch (e) { if (state.stop) throw e; failed += 1; done += 1; _sqp(done, ordered.length); report(e); log(`\u672a\u5b8c\u6210\uff0c\u7ee7\u7eed\u4e0b\u4e00\u8282\uff1a${label}`); } finally { state.activeChapters.delete(label); if (!state.stop) status(`\u961f\u5217 ${done}/${ordered.length}${failed ? ` · \u672a\u5b8c\u6210 ${failed}` : ''}`); _rc(); } }); if (state.stop) throw new Error('\u4efb\u52a1\u5df2\u505c\u6b62'); const summary = `\u961f\u5217\u7ed3\u675f\uff1a\u5b8c\u6210 ${done - failed}/${ordered.length}${failed ? `\uff0c\u672a\u5b8c\u6210 ${failed}` : ''}`; status(summary); log(summary); } catch (e) { report(e); } finally { state.running = false; state.boundIdentity = ''; state.activeChapters.clear(); state.video?.pause(); status(state.current); _sb(); } } function _sr(message = '\u6b63\u5728\u505c\u6b62…') { state.stop = true; state.video?.pause(); for (const pending of [...state.pending]) pending?.abort(); status(message); } function _rt() { const lease = state.lease; const tierText = lease?.tier === 'pro' ? 'PRO' : lease ? `\u514d\u8d39 ${lease.free_used_chapters || 0}/3` : '\u672a\u9a8c\u8bc1'; if (el.tier) el.tier.textContent = tierText; if (el.cloudTier) el.cloudTier.textContent = tierText; if (el.quota) { el.quota.textContent = lease?.tier === 'pro' ? `Pro \u4e0d\u9650\u7ae0\u8282 · \u5230\u671f ${new Date(lease.pro_expires_at * 1000).toLocaleString()}` : `\u514d\u8d39\u4f53\u9a8c\uff1a\u5df2\u4f7f\u7528 ${lease?.free_used_chapters || 0}/3 \u7ae0\u8282\uff0c\u540c\u4e00\u7ae0\u8282\u4e0d\u91cd\u590d\u6263\u989d\u3002`; } if (el.userName) el.userName.textContent = state.profile?.studentInfo?.name || state.profile?.nickname || '—'; if (el.selSummary) el.selSummary.textContent = `${state.selected.size}/${state.chapters.length}`; if (el.courseTag) el.courseTag.textContent = state.courses.length ? `${state.courses.length} \u95e8\u8bfe` : '\u672a\u52a0\u8f7d'; } function _sb() { const busy = state.running || state.refreshing; if (!el.refresh) return; for (const name of ['refresh', 'all', 'none', 'start', 'study', 'homework', 'verify', 'token']) { if (el[name]) el[name].disabled = busy; } if (el.stop) el.stop.disabled = !state.running; if (el.courses) for (const input of el.courses.querySelectorAll('input')) input.disabled = busy; status(state.current); } function _rc() { if (!el.courses) return; el.courses.replaceChildren(); for (const c of state.courses) { const box = document.createElement('div'); box.className = 'course'; const title = document.createElement('strong'); title.textContent = c.classroomName || c.disciplineName || '\u8bfe\u7a0b'; box.append(title); const children = state.chapters.filter(ch => ch.course === c); if (!children.length) { const msg = document.createElement('p'); msg.textContent = Number(c.teachingMethod) === 2 ? '\u76f4\u64ad\u8bfe\u7a0b\uff0c\u8bf7\u5728\u5e73\u53f0\u5b66\u4e60' : '\u6ca1\u6709\u53ef\u5904\u7406\u7684\u89c6\u9891\u7ae0\u8282\u6216\u8bfe\u540e\u4f5c\u4e1a'; box.append(msg); } for (const ch of children) { const row = document.createElement('label'); row.className = 'chapter'; const check = document.createElement('input'); check.type = 'checkbox'; check.checked = state.selected.has(ch.key); check.disabled = state.running; check.onchange = () => check.checked ? state.selected.add(ch.key) : state.selected.delete(ch.key); const text = document.createElement('span'); text.textContent = ch.section || ch.chapter || '\u8bfe\u540e\u4f5c\u4e1a'; const meta = document.createElement('small'); meta.textContent = ch.kind === 'homework' ? '\u4f5c\u4e1a\u7ae0\u8282' : `${Math.floor(ch.watchVideoMinute || 0)}/${ch.videoMinute} \u5206\u949f${complete(ch) ? ' · \u8bfe\u65f6\u5df2\u6ee1' : ''}`; row.append(check, text, meta); box.append(row); } el.courses.append(box); } _rt(); _sb(); } function mount() { if (!document.body) { setTimeout(mount, 50); return; } const host = document.createElement('div'); host.id = 'huashen-helper-panel'; const root = host.attachShadow({ mode: 'open' }); root.innerHTML = `
\u514d\u8d39\u4f53\u9a8c 3 \u7ae0\u8282\uff0c\u540c\u7ae0\u8bfe\u65f6\u548c\u4f5c\u4e1a\u5171\u7528\u989d\u5ea6\u3002
\u6309\u8bfe\u4e32\u884c\uff1a\u4e00\u95e8\u5237\u5b8c\u518d\u4e0b\u4e00\u95e8\u3002\u9875\u9762\u9700\u4fdd\u6301\u6253\u5f00\u3002