// ==UserScript==
// @name 华医网医学继续教育全自动学习助手
// @namespace https://card.wlxy.live/details/8028C15E
// @version 1.0
// @author 柠檬真酸
// @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png
// @description 华医网继续医学教育,收藏课程列表内所有课程学时和考试,安全稳定,支持扫码弹窗自行人脸识别和批量离线提交模式,支持正常继续教育和公需课
// @antifeature payment 免费体验2章节,全部需要升级Pro
// @match *://hdbl.91huayi.com/*
// @match *://cme*.91huayi.com/*
// @match *://*.91huayi.com/*
// @exclude *://apiwxmsg.91huayi.com/*
// @exclude *://hyuser*.91huayi.com/*
// @connect oa4.ahzsksw.cn
// @connect ocr.ahzsksw.cn
// @connect fa.ahsxks.com
// @connect dcwj.91huayi.com
// @connect hdbl.91huayi.com
// @connect *.91huayi.com
// @connect hls.videocc.net
// @connect player.polyv.net
// @connect cdn.jsdelivr.net
// @grant GM_xmlhttpRequest
// @run-at document-start
// @license All Rights Reserved
// ==/UserScript==
(function () {
"use strict";
function readUserScriptVersion() {
try {
const info = typeof GM_info !== "undefined" ? GM_info : null;
const script = info && info.script;
if (script && script.version) return String(script.version).trim();
const meta = String((info && (info.scriptMetaStr || info.script.metaStr)) || "");
const m = meta.match(/@version\s+(\S+)/);
if (m) return m[1];
} catch (_) {}
return "0.0.0";
}
const VERSION = readUserScriptVersion();
const ENABLED_KEY = "hy_cme_auto_enabled";
const MULTIPLIER_KEY = "hy_cme_time_multiplier";
const AUTO_EXAM_KEY = "hy_cme_auto_exam";
const AUTO_NEXT_KEY = "hy_cme_auto_next";
const API_MODE_KEY = "hy_cme_api_mode";
const QUEUE_KEY = "hy_cme_course_queue";
const QUEUE_TOUCHED_KEY = "hy_cme_queue_touched";
const STUDY_SESSION_QUEUE_KEY = "hy_cme_study_session_queue";
const PANEL_COLLAPSED_KEY = "hy_cme_panel_collapsed";
const PANEL_POS_KEY = "hy_cme_panel_pos";
const PANEL_NOTICE = "\u534e\u533b\u7f51\u7ee7\u7eed\u6559\u80b2 · \u81ea\u52a8\u5316\u6a21\u5f0f\u770b\u8bfe · \u5b66\u5b8c\u81ea\u52a8\u8003\u8bd5";
const PANEL_LOGO_URL = "https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png";
const QQ_GROUP_NUMBER = "903117129";
const QQ_GROUP_LINK =
"https://qun.qq.com/universal-share/share?ac=1&authKey=rxdL6YIJ0%2FxOEemjLqTGULvl5aAfJIVQcIvkvnwvmL%2FAmpFZnSafajYHgSXMUXvx&busi_data=eyJncm91cENvZGUiOiI5MDMxMTcxMjkiLCJ0b2tlbiI6IlB2dkFGSm5XRXBrSEhtQVFTUGQzdVNZakhNWDNMbW1kODA2enpoMi9obDh4SWp0YzBDODNFaGtwRU44Z0hyU0siLCJ1aW4iOiIxMjU0MzE1MTQifQ%3D%3D&data=9oyJixSPcigCQW-saV5eXlcMwV9C6J36XySx-rDHwVwNlofvRmd2ze5sLwFtHTbYbG4nAWIUrI0qftC6aTX9xg&svctype=4&tempid=h5_group_info";
const JOBS_KEY = "hy_cme_jobs";
const META_KEY = "hy_cme_api_meta";
const RUN_MODE_KEY = "hy_cme_run_mode";
const HANG_TABS_KEY = "hy_cme_hang_tabs";
const SERIAL_STATE_KEY = "hy_cme_serial_state";
const TAIL_STATE_KEY = "hy_cme_tail_state";
const LADDER_STATE_KEY = "hy_cme_ladder_state";
const SILENT_STATE_KEY = "hy_cme_silent_state";
const RUN_LOG_KEY = "hy_cme_run_log";
const HY_USER_PROFILE_KEY = "hy_cme_user_profile_v1";
const LAST_LEARNING_USER_ID_KEY = "hy_cme_last_learning_user_id_v1";
const LEADER_TAB_KEY = "hy_cme_leader_tab_v1";
const TAB_ID_SESSION_KEY = "hy_cme_tab_id";
const LEADER_HEARTBEAT_MS = 2000;
const LEADER_STALE_MS = 7000;
const DEFAULT_MULT = 3;
const API_GAP_MS = 5500;
const API_SEGMENTS = 3;
const COURSE_GAP_MS = 2000;
const FILL_GAP_MS = 5000;
const STEP_SEC = 300;
const ROUND_GAP_MS = 300000;
const VISIT_GAP_MS = 2500;
const TICK_MS = 3000;
const PROGRESS_MS = 300000;
const KEEPALIVE_MS = 1200000;
const PROCESS_SLOT_MS = PROGRESS_MS + 8000;
const TAIL_WAIT_KEEPALIVE_MS = 360000;
const STEP_INTERVAL_MIN = 300000;
const STEP_INTERVAL_MAX = 310000;
const PARALLEL_STAGGER_MIN = 2000;
const PARALLEL_STAGGER_MAX = 8000;
const P3PAR_COURSE_GAP_MS = 12000;
const P3PAR_START_STAGGER_MS = 30000;
const P3PAR_MAX_PER_TICK = 6;
const TAIL_HEARTBEAT_WAIT_MS = 330000;
const BIGSTEP_JUMP_SEC = 600;
const BIGSTEP_GAP_MS = 61000;
const RT_STEP_SEC = 300;
const RT_STEP_GAP_MS = 300000;
const OFFLINE_BATCH_BUFFER_MS = 30 * 60 * 1000;
const OFFLINE_WAIT_LOG_INTERVAL_MS = 30 * 60 * 1000;
const CODE3_RETRY_MS = 60000;
const CODE3_RETRY_MAX = 20;
const CODE3_BACKOFF_MS = 300000;
const LADDER_BATCH_SIZE = 3;
const HY_ENGINE_REQUIRED = true;
const CLOUD_TOKEN_KEY = "hy_cme_cloud_token_v1";
const CLOUD_LEASE_CACHE_KEY = "hy_cme_cloud_lease_cache_v1";
const CLOUD_PRO_EXPIRE_CACHE_KEY = "hy_cme_cloud_pro_expire_cache_v1";
const DEFAULT_CLOUD_API_BASE = "https://oa4.ahzsksw.cn";
const PRO_BUY_URL = "https://card.wlxy.live/details/8028C15E";
const HY_EXAM_CODE_PASS_KEY = "hy_cme_exam_code_pass";
const HY_EXAM_MAX_TRIES = 120;
const HY_EXAM_RETRY_GAP_MS = 3000;
const HY_EXAM_CAPTCHA_GAP_MS = 60000;
const HY_EXAM_CODE_OCR_MAX = 5;
const HY_EXAM_CODE_MANUAL_MAX = 2;
const EXAM_NAV_KEY = "hy_cme_exam_nav";
const HY_EXAM_ANSWER_CACHE_KEY = "hy_cme_exam_answers";
const HD_IFRAME_WATCHDOG_MS = 8000;
const HD_ACTIVE_KEY = "hy_cme_hd_active";
const HD_LAUNCH_KEY = "hy_cme_hd_launch";
const HD_META_COOKIE = "hy_cme_hd_meta";
const HD_IFRAME_ID = "hy-cme-hd-iframe";
const HD_POPUP_NAME = "hy-cme-hd-popup";
const HD_POPUP_W = 980;
const HD_POPUP_H = 680;
const HD_MSG_TYPE = "hy-cme-hd-event";
const HDBL_ORIGIN = "https://hdbl.91huayi.com";
const HD_MEDIA_TICK_MS = 60000;
const HD_VID_DUR_KEY = "hy_hd_vid_dur_cache";
const HD_PROGRESS_FRESH_PCT = 21;
const SHOW_HD_INTERACTIVE = false;
const RUN_MODE_INTRO = {
realtime:
"1:1 \u6302\u673a\uff1a\u6309\u771f\u5b9e\u8bfe\u957f\u6bcf 5 \u5206\u949f\u66f4\u65b0\u4e00\u6b21\u8fdb\u5ea6\uff0c\u9002\u5408\u6d4f\u89c8\u5668\u5728\u7ebf\u6302\u7740\u8dd1\uff0c\u5b66\u5b8c\u81ea\u52a8\u8003\u8bd5\u3002",
offline:
"\u79bb\u7ebf\u6279\u91cf\uff1a\u9996\u6b21\u8fd0\u884c\u5c06\u5168\u90e8\u89c6\u9891\u62c9\u6ee1\u8fdb\u5ea6\u5e76\u505c\u6b62\uff1b\u62c9\u6ee1\u540e\u4f1a\u81ea\u52a8\u68c0\u9a8c\u80fd\u5426\u63d0\u524d\u63d0\u4ea4\uff08\u6b64\u524d\u5df2\u770b\u90e8\u5206\u8bfe\u65f6\u53ef\u7acb\u5373\u5b8c\u6210\uff09\uff1b\u672a\u5230\u70b9\u53ef\u70b9\u300c\u63d0\u524d\u6536\u5c3e\u68c0\u9a8c\u300d\uff0c\u5230\u70b9\u540e\u518d\u70b9\u5f00\u59cb\u6279\u91cf\u8003\u8bd5\u3002",
};
function isHdInteractiveEnabled() {
return SHOW_HD_INTERACTIVE;
}
const TAIL_STEP_PHASES = [
"fill_s0",
"fill_step",
"fill_s1",
"fill_s3",
"tail_s0",
"tail_s1",
"tail_s2",
"tail_s3",
"finish_retry",
];
let engineRunning = false;
let bgRunning = false;
let gongxuFinishBusy = false;
let gongxuFinishAbort = 0;
let lastKeepAliveAt = 0;
let keepAliveIdx = 0;
let loginWarnAt = 0;
let cloudRuntimeWarnAt = 0;
let singleCourseTestRunning = false;
let hdIframeWatchdogTimer = null;
let hdPopupWindow = null;
const HY_CONSOLE_LOG = false;
function trace(msg) {
if (!HY_CONSOLE_LOG) return;
if (msg == null || msg === "") return;
console.log(`[\u534e\u533b\u7f51\u52a9\u624b] ${msg}`);
}
const cloudState = {
cloudToken: String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim(),
cloudTier: String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim() ? "unknown" : "free",
cloudLease: "",
cloudLeaseExp: 0,
cloudProExpireAt: 0,
cloudRevoked: false,
freeChapterLimit: 2,
freeUsedChapters: 0,
panelNoticePath: "/api/huayi/panel-notice",
remotePanelNotice: "",
proBuyUrl: PRO_BUY_URL,
captchaSelfApiEnable: true,
captchaSelfApiUrl: "",
captchaSelfApiViaProxy: false,
captchaSelfApiToken: "",
};
function getCloudApiBase() {
return DEFAULT_CLOUD_API_BASE;
}
function getLearningUserId() {
const m = document.cookie.match(/(?:^|;\s*)cme_user_id=([^;]+)/i);
if (m) return decodeURIComponent(m[1]).trim();
const uidVar = (document.documentElement.innerHTML.match(/var\s+uid\s*=\s*['"]([^'"]+)['"]/i) || [])[1];
return uidVar ? String(uidVar).trim() : "";
}
function getAccountBindingUserId() {
const m = document.cookie.match(/(?:^|;\s*)cme_user_id=([^;]+)/i);
return m ? decodeURIComponent(m[1]).trim() : "";
}
function normalizeUserIdForCompare(uid) {
return String(uid || "")
.trim()
.replace(/^0+/, "")
.toLowerCase();
}
function userIdsMatch(a, b) {
const x = normalizeUserIdForCompare(a);
const y = normalizeUserIdForCompare(b);
return !!(x && y && x === y);
}
function resolveStudyUserId(htmlUid) {
const bound = getAccountBindingUserId();
if (bound) return bound;
return String(htmlUid || getLearningUserId() || "").trim();
}
function currentSessionUserId() {
return getAccountBindingUserId() || getLearningUserId();
}
function readLastLearningUserId() {
try {
return String(localStorage.getItem(LAST_LEARNING_USER_ID_KEY) || "").trim();
} catch (_) {
return "";
}
}
function writeLastLearningUserId(uid) {
if (!uid) return;
try {
localStorage.setItem(LAST_LEARNING_USER_ID_KEY, uid);
} catch (_) {}
}
function traceProgressPostResult(ctx, data, label) {
const sessionUid = getLearningUserId();
const jobUid = String(ctx?.uid || "").trim();
const msg = String((data && (data.msg || data.message)) || "");
const code = data && data.code != null ? data.code : "?";
if (sessionUid && jobUid && sessionUid !== jobUid) {
trace(
`\u8fdb\u5ea6POST uid\u4e0d\u4e00\u81f4 [${label}] session=${sessionUid} job=${jobUid} code=${code} msg=${msg} cwrid=${ctx?.cwrid || ""}`
);
}
if (/\u7528\u6237|uid|ID|\u4e0d\u5339\u914d|mismatch/i.test(msg)) {
trace(
`\u8fdb\u5ea6POST\u5e73\u53f0\u8fd4\u56de [${label}] session=${sessionUid} job=${jobUid} code=${code} msg=${msg} cwrid=${ctx?.cwrid || ""}`
);
}
}
function resetLocalStateOnAccountSwitch(prevUid, nextUid) {
trace(`\u8d26\u53f7\u5207\u6362 ${prevUid} → ${nextUid}\uff0c\u6e05\u7406\u5f53\u524d\u8d26\u53f7\u9762\u677f\u72b6\u6001\uff08\u4fdd\u7559\u5404\u8d26\u53f7\u79bb\u7ebf\u4efb\u52a1\uff09`);
if (getEnabled()) {
stopAutoStudy(`\u8d26\u53f7\u5df2\u5207\u6362\uff08${prevUid} → ${nextUid}\uff09`);
}
purgeStaleJobsForSessionUid(nextUid);
writeCloudLeaseCache("", 0);
cloudState.cloudLease = "";
cloudState.cloudLeaseExp = 0;
panelState.chapterPreview = [];
panelState.lastCourseListKey = "";
const prof = readHyUserProfileCache();
if (prof.uid && prof.uid !== nextUid) {
try {
localStorage.removeItem(HY_USER_PROFILE_KEY);
} catch (_) {}
}
pushLog(`\u68c0\u6d4b\u5230\u8d26\u53f7\u5207\u6362\uff08${prevUid} → ${nextUid}\uff09\uff0c\u5df2\u52a0\u8f7d\u8be5\u8d26\u53f7\u4efb\u52a1`);
pushUserLog("\u8d26\u53f7\u5df2\u5207\u6362\uff0c\u8bf7\u70b9\u300c\u5237\u65b0\u300d\u540e\u91cd\u65b0\u52fe\u9009\u8bfe\u7a0b\uff1b\u82e5\u6b64\u524d\u5df2\u6279\u91cf\u62c9\u6ee1\uff0c\u5230\u70b9\u540e\u518d\u70b9\u5f00\u59cb\u6536\u5c3e");
setPanelHint("\u5df2\u5207\u6362\u8d26\u53f7\uff1b\u82e5\u6709\u79bb\u7ebf\u7b49\u5f85\u4efb\u52a1\uff0c\u9762\u677f\u4f1a\u663e\u793a\u8be5\u8d26\u53f7\u5230\u70b9\u65f6\u95f4");
syncRunModePanel();
renderRunLog();
updateJobsPanel();
}
function detectAccountSwitch() {
const current = getAccountBindingUserId();
if (!current) return false;
const prev = readLastLearningUserId();
if (prev && prev !== current) {
resetLocalStateOnAccountSwitch(prev, current);
writeLastLearningUserId(current);
return true;
}
writeLastLearningUserId(current);
purgeStaleJobsForSessionUid(current);
return false;
}
function purgeStaleJobsForSessionUid(sessionUid) {
const uid = String(sessionUid || getLearningUserId() || "").trim();
if (!uid) return false;
const jobs = loadJobs();
const keys = Object.keys(jobs);
if (!keys.length) return false;
let removed = 0;
keys.forEach((k) => {
const j = jobs[k];
if (j && j.uid && !userIdsMatch(j.uid, uid)) {
trace(`\u79fb\u9664\u65e7\u8d26\u53f7\u4efb\u52a1 job.uid=${j.uid} session=${uid} key=${k}`);
delete jobs[k];
removed++;
}
});
if (!removed) return false;
saveJobs(jobs);
pushLog(`\u5df2\u6e05\u7406 ${removed} \u6761\u4e0e\u5f53\u524d\u8d26\u53f7\u4e0d\u7b26\u7684\u672c\u5730\u4efb\u52a1`);
updateJobsPanel();
return true;
}
function pruneJobsOutsideSelection(selectedCids) {
const allow = new Set((selectedCids || []).map(normalizeQueueCid).filter(Boolean));
if (!allow.size) return 0;
const jobs = loadJobs();
let removed = 0;
Object.keys(jobs).forEach((k) => {
const j = jobs[k];
if (!j || j.phase === "done") return;
const cid = normalizeQueueCid(j.cid);
if (cid && !allow.has(cid)) {
trace(`\u79fb\u9664\u672a\u52fe\u9009\u8bfe\u7a0b\u4efb\u52a1 cid=${cid} key=${k}`);
delete jobs[k];
removed++;
}
});
if (removed) saveJobs(jobs);
return removed;
}
function migrateOfflineJobsForRealtime(selectedCids) {
if (!isRealtimeMode()) return 0;
const allow = new Set((selectedCids || []).map(normalizeQueueCid).filter(Boolean));
const jobs = loadJobs();
let n = 0;
Object.keys(jobs).forEach((key) => {
const j = jobs[key];
if (!j || j.phase === "done") return;
if (allow.size && j.cid && !allow.has(normalizeQueueCid(j.cid))) return;
const offlinePhase = ["silent_clock", "silent_filled", "silent_filling", "silent_finish_retry"].includes(
j.phase
);
const prefilled =
offlinePhase ||
!!(j.progressFilled || j.status2Sent) ||
Number(j.lastPostedSec || 0) >= Number(j.duration || 0) - 90;
if (!prefilled) return;
const ctx = jobToCtx(j);
const sec = Math.max(Number(j.lastPostedSec) || 0, Number(j.studiedSec) || 0, Number(j.duration) || 0);
if (sec >= 60) syncLocal(ctx, sec);
jobs[key] = Object.assign({}, j, {
phase: j.phase === "silent_finish_retry" ? "rt_finish_retry" : "pending",
nextStepDue: 0,
engineSessionId: "",
progressFilled: !!(j.progressFilled || sec >= Number(j.duration || 0) - 90),
status2Sent: !!(j.status2Sent || j.progressFilled),
lastPostedSec: Math.max(Number(j.lastPostedSec) || 0, sec),
studiedSec: Math.max(Number(j.studiedSec) || 0, sec),
rtStartedAt: 0,
});
n++;
});
if (n) saveJobs(jobs);
return n;
}
function gongxuLikelyPrefilled(job, ctx) {
if (!isGongxuCtx(ctx, job)) return false;
const j = job || {};
const dur = Math.max(60, Number(j.duration) || 3600);
if (j.progressFilled || j.status2Sent) return true;
if (Number(j.lastPostedSec) >= dur - 90) return true;
if (Number(j.studiedSec) >= dur - 90) return true;
const st = loadSilentState();
if (st.filledAt || st.sessionStart) return true;
const cid = normalizeQueueCid(j.cid || (ctx && ctx.cid));
if (cid) {
const siblings = Object.values(loadJobs()).filter(
(x) => x && normalizeQueueCid(x.cid) === cid && x !== j
);
if (siblings.some((x) => x.progressFilled || Number(x.lastPostedSec) >= Number(x.duration) - 90)) {
return true;
}
if (siblings.some((x) => ["silent_clock", "silent_filled"].includes(x.phase))) return true;
}
return false;
}
function prepareJobsForStudyStart(selectedCids) {
purgeStaleJobsForSessionUid();
const removed = pruneJobsOutsideSelection(selectedCids);
if (removed > 0) {
pushUserLog(`\u5df2\u6e05\u7406 ${removed} \u6761\u672a\u52fe\u9009\u8bfe\u7a0b\u4efb\u52a1\uff0c\u672c\u6b21\u4ec5\u5b66\u4e60\u5f53\u524d\u52fe\u9009`);
}
const migrated = migrateOfflineJobsForRealtime(selectedCids);
if (migrated > 0) {
pushLog(`\u5df2\u8fc1\u79fb ${migrated} \u6761\u79bb\u7ebf\u62c9\u6ee1\u4efb\u52a1\u5230 1:1 \u6536\u5c3e\u961f\u5217`);
}
const woke = wakeJobsForStudyStart(selectedCids);
if (woke > 0) {
pushLog(`\u5df2\u6062\u590d ${woke} \u6761\u6682\u505c\u4e2d\u7684\u4efb\u52a1\uff0c\u7ee7\u7eed\u5b66\u4e60`);
}
return removed;
}
function readHyUserProfileCache() {
try {
return JSON.parse(localStorage.getItem(HY_USER_PROFILE_KEY) || "{}") || {};
} catch (_) {
return {};
}
}
function writeHyUserProfileCache(profile) {
if (!profile || !profile.uid) return;
try {
localStorage.setItem(HY_USER_PROFILE_KEY, JSON.stringify(profile));
} catch (_) {}
}
function parseHyUserProfileFromDom() {
return { uid: getLearningUserId() || "", name: "", specialty: "", unit: "", school_name: "" };
}
async function fetchHyUserProfileIfNeeded() {
return getHyUserProfile();
}
function getHyUserProfile() {
const uid = getLearningUserId() || "";
return { uid, name: "", specialty: "", unit: "", school_name: "" };
}
function buildHyLeasePayload() {
return { learning_user_id: getLearningUserId() || "" };
}
function formatHyBindPreview() {
const p = getHyUserProfile();
if (!p.uid) return "";
return [p.uid, p.name || "-", p.specialty || "-", p.unit || "-"].join("|");
}
function isHyLoggedIn() {
if (getLearningUserId()) return true;
return /login_out\.aspx|tar_name/i.test(document.body?.innerHTML || "");
}
function gmRequestWithStatus(url, method, headers, data) {
return new Promise((resolve, reject) => {
const req = {
method: method || "GET",
url,
headers: headers || {},
timeout: 90000,
withCredentials: false,
responseType: "text",
onload: (res) => resolve({ status: res.status, text: res.responseText || "" }),
onerror: () => reject(new Error("\u7f51\u7edc\u9519\u8bef " + url)),
ontimeout: () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6 " + url)),
};
if (data != null) req.data = data;
if (typeof GM_xmlhttpRequest === "function") {
GM_xmlhttpRequest(req);
return;
}
const xhr = new XMLHttpRequest();
xhr.open(req.method, url, true);
xhr.timeout = req.timeout;
Object.keys(req.headers).forEach((k) => xhr.setRequestHeader(k, req.headers[k]));
xhr.onload = () => resolve({ status: xhr.status, text: xhr.responseText || "" });
xhr.onerror = () => reject(new Error("\u7f51\u7edc\u9519\u8bef " + url));
xhr.ontimeout = () => reject(new Error("\u8bf7\u6c42\u8d85\u65f6 " + url));
xhr.send(data);
});
}
async function _hr(path, method, payload) {
const base = getCloudApiBase();
const url = `${base}${path.startsWith("/") ? path : `/${path}`}`;
const headers = { Accept: "application/json", "Content-Type": "application/json" };
const luid = getLearningUserId();
if (luid) headers["x-learning-user-id"] = luid;
if (cloudState.cloudToken) headers.Authorization = `Bearer ${cloudState.cloudToken}`;
let bodyObj = payload;
if (bodyObj && typeof bodyObj === "object") {
bodyObj = Object.assign({}, bodyObj);
if (path !== "/api/huayi/lease" && cloudState.cloudLease && bodyObj.lease == null) {
bodyObj.lease = cloudState.cloudLease;
}
}
const data = bodyObj == null ? null : JSON.stringify(bodyObj);
const res = await gmRequestWithStatus(url, method || "POST", headers, data);
const text = String(res.text || "").trim();
let parsed = {};
if (text) {
try {
parsed = JSON.parse(text);
} catch (_) {
parsed = {};
}
}
if (res.status < 200 || res.status >= 300) {
throw new Error(String(parsed.detail || parsed.message || parsed.code || `http_${res.status}`));
}
return parsed;
}
function resolveLeaseExpireSec(data) {
const raw = data?.exp ?? data?.expire_at ?? data?.expires_at ?? data?.lease_exp ?? 0;
const n = Number(raw);
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
}
function resolveProExpireSec(data) {
const raw = data?.pro_expires_at ?? data?.proExpiresAt ?? 0;
const n = Number(raw);
if (Number.isFinite(n) && n > 0) return Math.floor(n);
const cached = readCloudProExpireCache();
const tk = String(cloudState.cloudToken || "").trim();
if (cached && tk && cached.token === tk && cached.exp > 0) return cached.exp;
return 0;
}
function writeCloudProExpireCache(token, exp) {
try {
const tk = String(token || "").trim();
if (!tk || !exp) {
localStorage.removeItem(CLOUD_PRO_EXPIRE_CACHE_KEY);
return;
}
localStorage.setItem(
CLOUD_PRO_EXPIRE_CACHE_KEY,
JSON.stringify({ token: tk, exp: Math.floor(exp) })
);
} catch (_) {}
}
function readCloudProExpireCache() {
try {
const raw = localStorage.getItem(CLOUD_PRO_EXPIRE_CACHE_KEY);
if (!raw) return null;
const p = JSON.parse(raw);
if (!p?.token || !p?.exp) return null;
return { token: String(p.token), exp: Number(p.exp) };
} catch (_) {
return null;
}
}
function readCloudLeaseCache() {
try {
const raw = localStorage.getItem(CLOUD_LEASE_CACHE_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw);
const lease = String(parsed.lease || "").trim();
const exp = Number(parsed.exp || 0);
if (!lease || !Number.isFinite(exp) || exp <= 0) return null;
return {
lease,
exp,
tier: parsed.tier || "",
freeChapterLimit: Number(parsed.freeChapterLimit ?? parsed.free_chapter_limit ?? 2),
freeUsedChapters: Number(parsed.freeUsedChapters ?? parsed.free_used_chapters ?? 0),
proExpireAt: Number(parsed.proExpireAt || 0),
};
} catch (_) {
return null;
}
}
function writeCloudLeaseCache(lease, exp, extra) {
if (!lease || !exp) {
localStorage.removeItem(CLOUD_LEASE_CACHE_KEY);
return;
}
localStorage.setItem(
CLOUD_LEASE_CACHE_KEY,
JSON.stringify(Object.assign({ lease, exp }, extra || {}))
);
}
function syncHyCloudQuotaFromResponse(data) {
if (!data || typeof data !== "object") return;
if (data.tier) cloudState.cloudTier = String(data.tier);
const lim = data.free_chapter_limit ?? data.freeChapterLimit ?? data.free_video_limit ?? data.limit;
const used = data.free_used_chapters ?? data.freeUsedChapters ?? data.free_used_videos ?? data.used;
if (lim != null) cloudState.freeChapterLimit = Number(lim) || 2;
if (used != null) cloudState.freeUsedChapters = Number(used) || 0;
}
function formatCloudTierText(tier) {
const t = String(tier || "").toLowerCase();
if (t === "pro") return "Pro";
if (t === "revoked") return "\u5df2\u7981\u7528";
if (t === "free") return "\u514d\u8d39\u4f53\u9a8c";
if (t === "unknown") return "\u6821\u9a8c\u5931\u8d25";
return tier || "\u672a\u6821\u9a8c";
}
function isCloudProTier() {
return String(cloudState.cloudTier || "").toLowerCase() === "pro";
}
function canUseOfflineMode() {
return isCloudProTier();
}
function ensureRunModeForTier() {
if (canUseOfflineMode()) {
setRunMode("offline");
return;
}
if (getRunMode() === "offline") setRunMode("realtime");
}
function jobBelongsToSession(job, sessionUid) {
const uid = String(sessionUid || getLearningUserId() || "").trim();
if (!uid || !job) return false;
if (!job.uid) return true;
return userIdsMatch(job.uid, uid);
}
function getSelectedCidSet() {
return new Set(
pruneQueueToCourses()
.map(normalizeQueueCid)
.filter(Boolean)
);
}
const OFFLINE_BATCH_PHASES = new Set([
"silent_clock",
"silent_filled",
"silent_filling",
"silent_finish_retry",
"done",
]);
function courseNeedsOfflineAppend(cid, sessionUid) {
const k = normalizeQueueCid(cid);
const uid = String(sessionUid || getLearningUserId() || "").trim();
if (!k || !uid) return false;
const courseJobs = Object.values(loadJobs()).filter(function (j) {
return j && jobBelongsToSession(j, uid) && normalizeQueueCid(j.cid) === k;
});
if (!courseJobs.length) return true;
if (courseJobs.some(function (j) {
return j.phase === "pending";
})) {
return true;
}
return !courseJobs.every(function (j) {
return OFFLINE_BATCH_PHASES.has(j.phase);
});
}
function countSelectedCoursesNeedingOfflineAppend(sessionUid) {
const uid = String(sessionUid || getLearningUserId() || "").trim();
return pruneQueueToCourses().filter(function (cid) {
return courseNeedsOfflineAppend(cid, uid);
}).length;
}
function countPendingOfflineFillJobs(sessionUid) {
const uid = String(sessionUid || getLearningUserId() || "").trim();
const selectedSet = getSelectedCidSet();
if (!uid || !selectedSet.size) return 0;
return Object.values(loadJobs()).filter(function (j) {
if (!j || !jobBelongsToSession(j, uid)) return false;
if (j.interactive) return false;
if (j.phase !== "pending") return false;
if (j.cid && !selectedSet.has(normalizeQueueCid(j.cid))) return false;
return true;
}).length;
}
function hasProOfflineAppendFillWork() {
if (!getOfflineBatchSnapshot()) return false;
return countPendingOfflineFillJobs() > 0 || countSelectedCoursesNeedingOfflineAppend() > 0;
}
function hasOfflineBatchFillWork(sessionUid) {
if (!isOfflineMode()) return false;
const uid = sessionUid || getLearningUserId();
if (!uid) return false;
const hasJob = Object.values(loadJobs()).some(function (j) {
return (
j &&
jobBelongsToSession(j, uid) &&
!j.interactive &&
(j.phase === "pending" || j.phase === "face_pending" || j.phase === "silent_filling")
);
});
return hasJob || hasProOfflineAppendFillWork();
}
async function tryResumeFacePendingOnStart() {
const uid = getLearningUserId();
if (!uid) return false;
const hit = Object.values(loadJobs()).find(function (j) {
return j && jobBelongsToSession(j, uid) && j.phase === "face_pending" && !j.interactive;
});
if (!hit) return false;
const ctx = jobToCtx(hit);
const label = hit.title || hit.cwrid.slice(0, 8);
pushUserLog(`${label} · \u5f85\u770b\u8bfe\u524d\u4eba\u8138\uff0c\u8bf7\u7528\u638c\u4e0a\u534e\u533b APP \u626b\u7801`);
injectHyPanelStyles();
const ok = await ensureHyVideoFaceVerified(ctx, false);
if (ok) {
saveJob(ctx, { phase: "pending", engineSessionId: "" });
pushUserLog(`${label} · \u770b\u8bfe\u524d\u4eba\u8138\u5df2\u901a\u8fc7\uff0c\u7ee7\u7eed\u62c9\u6ee1`);
return true;
}
return false;
}
function isProOfflineWaitingNotDue() {
if (!canUseOfflineMode() || !isOfflineMode()) return false;
if (hasProOfflineAppendFillWork()) return false;
if (hasOfflinePartialFinishReady()) return false;
const snap = getOfflineBatchSnapshot();
return !!(snap && snap.filledReady && !snap.ready);
}
function isProOfflineFinishReady() {
if (!canUseOfflineMode() || !isOfflineMode()) return false;
const snap = getOfflineBatchSnapshot();
return !!(snap && snap.filledReady && snap.ready);
}
function silentStateKeyForUid(uid) {
const u = String(uid || getLearningUserId() || "").trim();
return u ? `${SILENT_STATE_KEY}_${u}` : SILENT_STATE_KEY;
}
function migrateLegacySilentStateForUid(uid) {
const u = String(uid || getLearningUserId() || "").trim();
if (!u) return;
const perKey = silentStateKeyForUid(u);
if (localStorage.getItem(perKey)) return;
try {
const legacy = JSON.parse(localStorage.getItem(SILENT_STATE_KEY) || "null");
if (!legacy || typeof legacy !== "object") return;
const jobs = loadJobs();
const hasMine = Object.values(jobs).some((j) => jobBelongsToSession(j, u));
if (!hasMine) return;
localStorage.setItem(perKey, JSON.stringify(legacy));
} catch (_) {}
}
function formatLeaseExpireText(ts) {
const n = Number(ts || 0);
if (!n) return "—";
const d = new Date(n * 1000);
return Number.isNaN(d.getTime()) ? "—" : d.toLocaleString();
}
function applyCloudLeaseFromCache(cached) {
if (!cached || !cached.lease) return false;
cloudState.cloudLease = cached.lease;
cloudState.cloudLeaseExp = cached.exp;
cloudState.cloudProExpireAt = Number(cached.proExpireAt || 0);
if (cached.tier) cloudState.cloudTier = cached.tier;
if (cached.freeChapterLimit > 0) cloudState.freeChapterLimit = cached.freeChapterLimit;
if (cached.freeUsedChapters >= 0) cloudState.freeUsedChapters = cached.freeUsedChapters;
updatePanelCloudStatus();
return true;
}
function isCloudNetworkError(msg) {
return /\u8d85\u65f6|\u7f51\u7edc\u9519\u8bef|timeout|network|fetch failed|failed to fetch/i.test(String(msg || ""));
}
async function _cl(forceRefresh) {
const now = Math.floor(Date.now() / 1000);
if (!forceRefresh && cloudState.cloudLease && cloudState.cloudLeaseExp - now > 60) return true;
const cached = readCloudLeaseCache();
if (!forceRefresh && cached && cached.exp - now > 60) {
applyCloudLeaseFromCache(cached);
return true;
}
const luid = getLearningUserId();
if (!luid) throw new Error("\u672a\u767b\u5f55\uff0c\u65e0\u6cd5\u83b7\u53d6\u4e91\u7aef\u6388\u6743");
let data;
try {
data = await _hr("/api/huayi/lease", "POST", buildHyLeasePayload());
cloudState.cloudRevoked = false;
} catch (e) {
const em = String(e?.message || e);
if (/(revoked|invalid token|expired)/i.test(em)) {
cloudState.cloudRevoked = true;
cloudState.cloudTier = "revoked";
cloudState.cloudLease = "";
cloudState.cloudLeaseExp = 0;
writeCloudLeaseCache("", 0);
updatePanelCloudStatus();
throw e;
}
if (cached && cached.lease && cached.exp - now > -3600) {
applyCloudLeaseFromCache(cached);
pushLog(`\u6388\u6743\u670d\u6682\u4e0d\u53ef\u7528\uff0c\u7ee7\u7eed\uff08${em.slice(0, 48)}\uff09`);
return true;
}
throw e;
}
cloudState.cloudLease = String(data.lease || "");
cloudState.cloudLeaseExp = resolveLeaseExpireSec(data);
cloudState.cloudTier = String(data.tier || "").trim() || (cloudState.cloudToken ? "pro" : "free");
cloudState.cloudProExpireAt = resolveProExpireSec(data);
if (cloudState.cloudProExpireAt && cloudState.cloudToken) {
writeCloudProExpireCache(cloudState.cloudToken, cloudState.cloudProExpireAt);
}
syncHyCloudQuotaFromResponse(data);
writeCloudLeaseCache(cloudState.cloudLease, cloudState.cloudLeaseExp, {
tier: cloudState.cloudTier,
freeChapterLimit: cloudState.freeChapterLimit,
freeUsedChapters: cloudState.freeUsedChapters,
proExpireAt: cloudState.cloudProExpireAt,
});
updatePanelCloudStatus();
return !!cloudState.cloudLease;
}
async function _vt() {
if (!cloudState.cloudToken) return true;
const res = await gmRequestWithStatus(
`${getCloudApiBase()}${"/api/license/verify"}`,
"GET",
{
Accept: "application/json",
Authorization: `Bearer ${cloudState.cloudToken}`,
...(getLearningUserId() ? { "x-learning-user-id": getLearningUserId() } : {}),
},
null
);
const text = String(res.text || "").trim();
let data = {};
if (text) {
try {
data = JSON.parse(text);
} catch (_) {
data = {};
}
}
if (res.status < 200 || res.status >= 300 || !data.ok) {
throw new Error(String(data.message || data.detail || "Token \u6821\u9a8c\u5931\u8d25"));
}
if (data.tier) cloudState.cloudTier = String(data.tier);
return true;
}
async function _crt() {
if (!isHyLoggedIn()) return false;
try {
await _cl(false);
return !!cloudState.cloudLease;
} catch (e) {
const now = Date.now();
if (now - cloudRuntimeWarnAt > 120000) {
cloudRuntimeWarnAt = now;
pushLog("\u4e91\u7aef\u6682\u65f6\u4e0d\u53ef\u7528\uff0c\u7a0d\u540e\u81ea\u52a8\u91cd\u8bd5\uff1a" + String(e?.message || e).slice(0, 72));
}
return false;
}
}
async function _cr(options) {
const forceLease = !!(options && options.forceLease);
if (detectAccountSwitch()) {
notifyUser("\u8d26\u53f7\u5df2\u5207\u6362\uff0c\u8bf7\u5237\u65b0\u6536\u85cf\u540e\u91cd\u65b0\u52fe\u9009\u8bfe\u7a0b");
updatePanel();
return false;
}
if (!isHyLoggedIn()) {
notifyAuthError("\u8bf7\u5148\u767b\u5f55\u534e\u533b\u7f51");
return false;
}
try {
if (cloudState.cloudToken) {
try {
await _vt();
} catch (ve) {
const vm = String(ve?.message || ve);
if (!isCloudNetworkError(vm)) throw ve;
const cached = readCloudLeaseCache();
const now = Math.floor(Date.now() / 1000);
if (cached && cached.exp - now > 60) {
pushLog("Token \u6821\u9a8c\u8d85\u65f6\uff0c\u4f7f\u7528\u672c\u5730\u6388\u6743\u7ee7\u7eed");
} else {
throw ve;
}
}
}
await _cl(forceLease);
} catch (e) {
notifyAuthError(e?.message || e);
updatePanelCloudStatus();
return false;
}
const tier = String(cloudState.cloudTier || "").toLowerCase();
if (tier === "pro") {
clearPanelHint();
return true;
}
if (tier === "free") {
if (Number(cloudState.freeUsedChapters) >= Number(cloudState.freeChapterLimit)) {
const quotaMsg = `\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08${cloudState.freeUsedChapters}/${cloudState.freeChapterLimit} \u8282\uff09\uff0c\u8bf7\u5347\u7ea7 Pro`;
notifyAuthError(quotaMsg);
openHyProModal();
return false;
}
clearPanelHint();
return true;
}
if (cloudState.cloudRevoked) {
notifyAuthError("Token \u5df2\u7981\u7528\uff0c\u8bf7\u8054\u7cfb\u7ba1\u7406\u5458");
return false;
}
notifyAuthError("\u4e91\u7aef\u6388\u6743\u72b6\u6001\u5f02\u5e38\uff0c\u8bf7\u5728\u300c\u8bbe\u7f6e\u300d\u4fdd\u5b58\u5e76\u6821\u9a8c Token");
return false;
}
async function consumeChapterCloudQuota(ctx) {
const key = jobKey(ctx);
const jobs = loadJobs();
const job = jobs[key];
if (!job || job.quotaConsumed) return true;
if (String(cloudState.cloudTier || "").toLowerCase() === "pro") {
job.quotaConsumed = true;
jobs[key] = job;
saveJobs(jobs);
return true;
}
try {
await _cl(false);
const data = await _hr("/api/huayi/chapter/consume", "POST", {
cwrid: ctx.cwrid || "",
cid: ctx.cid || "",
});
syncHyCloudQuotaFromResponse(data);
job.quotaConsumed = true;
jobs[key] = job;
saveJobs(jobs);
updatePanelCloudStatus();
return true;
} catch (e) {
const msg = String(e?.message || e);
if (/free_quota_exhausted|\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c/i.test(msg)) {
pushLog(
`\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08${cloudState.freeUsedChapters}/${cloudState.freeChapterLimit} \u8282\uff09\uff0c\u8bf7\u5347\u7ea7 Pro`
);
stopAutoStudy("\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff0c\u7a0b\u5e8f\u5df2\u505c\u6b62");
return false;
}
pushLog("\u4e91\u7aef\u914d\u989d\u5931\u8d25\uff1a" + msg);
return false;
}
}
async function _cf() {
try {
const res = await gmRequestWithStatus(
`${getCloudApiBase()}${"/api/huayi/client-config"}`,
"GET",
{ Accept: "application/json" },
null
);
if (res.status < 200 || res.status >= 300 || !res.text) return;
const j = JSON.parse(res.text);
if (j?.panelNoticePath) cloudState.panelNoticePath = String(j.panelNoticePath);
if (j?.freeChapterLimit != null) cloudState.freeChapterLimit = Number(j.freeChapterLimit) || 2;
else if (j?.freeVideoLimit != null) cloudState.freeChapterLimit = Number(j.freeVideoLimit) || 2;
if (j?.proBuyUrl) cloudState.proBuyUrl = String(j.proBuyUrl).trim() || PRO_BUY_URL;
const cap = j.captcha && typeof j.captcha === "object" ? j.captcha : {};
cloudState.captchaSelfApiEnable = cap.selfEnabled !== false;
cloudState.captchaSelfApiUrl = String(cap.selfUrl || "").trim();
cloudState.captchaSelfApiViaProxy = cap.selfViaProxy === true;
cloudState.captchaSelfApiToken = String(cap.selfToken || "").trim();
} catch (_) {}
try {
const noticeRes = await gmRequestWithStatus(
`${getCloudApiBase()}${cloudState.panelNoticePath || "/api/huayi/panel-notice"}`,
"GET",
{},
null
);
if (noticeRes.status >= 200 && noticeRes.status < 300) {
cloudState.remotePanelNotice = String(noticeRes.text || "").trim() || PANEL_NOTICE;
const ann = document.getElementById("hy-cme-ann-text");
if (ann) ann.textContent = cloudState.remotePanelNotice;
}
} catch (_) {}
updatePanelCloudStatus();
}
function buildHyCaptchaPredictUrl() {
if (cloudState.captchaSelfApiViaProxy) {
try {
return `${new URL(getCloudApiBase()).origin}${"/api/huayi/captcha/self-predict"}`;
} catch (_) {
return `${getCloudApiBase()}${"/api/huayi/captcha/self-predict"}`;
}
}
const configured = String(cloudState.captchaSelfApiUrl || "").trim();
const root = configured || getCloudApiBase();
try {
const cloudOrigin = new URL(getCloudApiBase()).origin;
const target = new URL(root.startsWith("http") ? root : `https://${root}`);
if (target.origin === cloudOrigin) {
return `${cloudOrigin}${"/api/huayi/captcha/self-predict"}`;
}
} catch (_) {}
return buildHyOcrPredictUrl(root);
}
function isLiveStudyStatusText(text) {
return /· \u603b\d+\u5206\u949f · \u5df2\u5b66|\u540e\u66f4\u65b0\u8fdb\u5ea6|\u5373\u5c06\u4e0a\u62a5|\u540e\u91cd\u8bd5/.test(String(text || ""));
}
function clearLiveStudyStatusBar() {
const el = document.getElementById("hy-cme-log-status");
if (!el || el.style.display === "none") return;
if (isLiveStudyStatusText(el.textContent || "")) updateLogStatusBar("");
}
function getEnabled() {
return localStorage.getItem(ENABLED_KEY) === "1";
}
function setEnabled(v, reason) {
const prev = getEnabled();
const next = !!v;
localStorage.setItem(ENABLED_KEY, next ? "1" : "0");
trace(`setEnabled(${next}) prev=${prev}${reason ? ` reason=${reason}` : ""}`);
if (next) {
skipExamsSession = false;
examFaceCancel = false;
examFaceSkipAll = false;
videoFaceCancel = false;
startPreviewAutoRefresh();
} else {
stopPreviewAutoRefresh();
panelState.lastChapterStudyLogKey = "";
clearLiveStudyStatusBar();
}
syncStartStopButtons();
}
const JOB_IDLE_RETRY_PHASES = new Set([
"rt_finish_retry",
"silent_finish_retry",
"finish_wait",
]);
const PAUSE_TO_PENDING_PHASES = new Set(["rt_stepping", "silent_filling"]);
function pauseJobsOnStudyStop() {
gongxuFinishAbort++;
bgRunning = false;
engineRunning = false;
const jobs = loadJobs();
let n = 0;
Object.entries(jobs).forEach(([key, j]) => {
if (!j || j.phase === "done") return;
let patch = null;
if (PAUSE_TO_PENDING_PHASES.has(j.phase)) {
patch = {
phase: "pending",
nextStepDue: 0,
engineSessionId: "",
processExamsDone: false,
processExamsAt: 0,
rtStartedAt: 0,
};
} else if (j.phase === "rt_finish_retry" || j.phase === "silent_finish_retry") {
patch = { recordRetryDue: 0, finishDue: 0, nextStepDue: 0, engineSessionId: "" };
} else if (JOB_IDLE_RETRY_PHASES.has(j.phase)) {
patch = { recordRetryDue: 0, finishDue: 0, nextStepDue: 0, engineSessionId: "" };
} else if (j.phase === "pending" && (j.nextStepDue > Date.now() || j.engineSessionId)) {
patch = { nextStepDue: 0, engineSessionId: "" };
} else if (j.phase === "exam_pending" && j.examRetryDue > Date.now()) {
patch = { examRetryDue: 0 };
}
if (patch) {
jobs[key] = Object.assign({}, j, patch);
n++;
}
});
if (n) {
saveJobs(jobs);
trace(`pauseJobsOnStudyStop: normalized ${n} job(s)`);
}
return n;
}
function wakeJobsForStudyStart(selectedCids) {
const selected = new Set((selectedCids || []).map(normalizeQueueCid).filter(Boolean));
const jobs = loadJobs();
let n = 0;
Object.entries(jobs).forEach(([key, j]) => {
if (!j || j.phase === "done") return;
if (selected.size && j.cid && !selected.has(normalizeQueueCid(j.cid))) return;
let patch = null;
if (j.phase === "rt_stepping" || j.phase === "silent_filling") {
patch = { phase: "pending", nextStepDue: 0, engineSessionId: "" };
} else if (j.phase === "pending" && (j.nextStepDue > Date.now() || j.engineSessionId)) {
patch = { nextStepDue: 0, engineSessionId: "" };
} else if (
j.phase === "rt_finish_retry" ||
j.phase === "silent_finish_retry" ||
JOB_IDLE_RETRY_PHASES.has(j.phase)
) {
if (j.recordRetryDue > Date.now() || j.engineSessionId || j.nextStepDue > Date.now()) {
patch = { recordRetryDue: 0, finishDue: 0, nextStepDue: 0, engineSessionId: "" };
}
} else if (j.phase === "exam_pending" && j.examRetryDue > Date.now()) {
patch = { examRetryDue: 0 };
}
if (patch) {
jobs[key] = Object.assign({}, j, patch);
n++;
}
});
if (n) {
saveJobs(jobs);
trace(`wakeJobsForStudyStart: woke ${n} job(s)`);
}
return n;
}
function captureQueueSnapshot() {
const seen = new Set();
const out = [];
const add = (list) => {
(list || []).forEach((cid) => {
const k = normalizeQueueCid(cid);
if (k && !seen.has(k)) {
seen.add(k);
out.push(k);
}
});
};
add(getStudySessionQueue());
add(panelState.selectionLock);
add(getEffectiveQueue());
add(getQueue());
if (!out.length) add(readCourseListSelectionFromDom());
return out;
}
function stopAutoStudy(reason) {
const why = String(reason || "\u7a0b\u5e8f\u5df2\u81ea\u52a8\u505c\u6b62").trim();
const wasEnabled = getEnabled();
const preservedQueue = captureQueueSnapshot();
trace(`stopAutoStudy: ${why} enabled=${wasEnabled} queue=${preservedQueue.length}`);
if (wasEnabled) {
const paused = pauseJobsOnStudyStop();
if (paused > 0) trace(`stopAutoStudy: paused ${paused} in-flight job(s)`);
setEnabled(false, why);
}
panelState.selectionLock = null;
panelState.studyStartedAt = 0;
setStudySessionQueue([]);
if (preservedQueue.length) commitQueueSelection(preservedQueue);
pushLog(why);
if (reason === "\u5df2\u6682\u505c") pushUserLog("\u5df2\u505c\u6b62\u5b66\u4e60");
else pushUserLog(why);
pushUserLog(formatSelectedProgressLine());
syncStartStopButtons();
updatePanel();
}
function findPanelChapterByLsKey(lsKey) {
if (!lsKey) return null;
for (const c of panelState.courses) {
const ch = (c.chapters || []).find((x) => x.lsKey === lsKey);
if (ch) return ch;
}
for (const c of panelState.chapterPreview) {
const ch = (c.chapters || []).find((x) => x.lsKey === lsKey);
if (ch) return ch;
}
return null;
}
function reconcileFinishedJobsFromPanel() {
const jobs = loadJobs();
let changed = false;
Object.entries(jobs).forEach(([key, job]) => {
if (!job || job.phase === "done") return;
const ch = findPanelChapterByLsKey(job.lsKey);
if (!ch) return;
const videoOk = !!ch.done;
const examOk = !!ch.examDone || !!ch.interactive || !!job.interactive;
if (videoOk && examOk) {
jobs[key] = Object.assign({}, job, { phase: "done", examDone: true });
changed = true;
return;
}
if (job.phase === "exam_pending" && examOk) {
jobs[key] = Object.assign({}, job, { phase: "done", examDone: true });
changed = true;
}
});
if (changed) saveJobs(jobs);
return changed;
}
function isGongxuCourse(course) {
return !!(course && course.gongxu);
}
function isGongxuCourseByCid(cid) {
return isGongxuCourse(findPanelCourseByCid(cid));
}
function isGongxuCtx(ctx, job) {
const cid = (job && job.cid) || (ctx && ctx.cid);
if (cid && isGongxuCourseByCid(cid)) return true;
const title = [(job && job.title) || "", (ctx && ctx.title) || ""].join(" ");
if (/\u516c\u9700\u8bfe|\u516c\u9700\u79d1\u76ee/.test(title)) return true;
if (cid) {
const course = findPanelCourseByCid(cid);
if (course && /\u516c\u9700\u8bfe|\u516c\u9700\u79d1\u76ee/.test(course.title || "")) return true;
}
return false;
}
const GONGXU_FINISH_RETRY_MS = 15000;
const GONGXU_STOP_START_HINT =
"\u516c\u9700\u8bfe\uff1a\u67d0\u8282\u5361\u4f4f\u6216\u51fa\u73b0\u300c5\u5206\u949f\u540e\u66f4\u65b0\u300d\u65f6\uff0c\u8bf7\u70b9\u300c\u505c\u6b62\u300d\u518d\u70b9\u300c\u5f00\u59cb\u300d\uff0c\u53ef\u9010\u8282\u63a8\u8fdb";
function selectionHasGongxuCourse() {
const selected = pruneQueueToCourses();
return selected.some((cid) => isGongxuCourseByCid(cid));
}
function refreshGongxuStudyHint() {
if (selectionHasGongxuCourse()) setPanelHint(GONGXU_STOP_START_HINT);
}
function pushGongxuStopStartUserTip(chapterTitle) {
try {
const key = "hy_gongxu_stopstart_user_tip";
const last = Number(sessionStorage.getItem(key) || 0);
if (Date.now() - last < 30000) return;
sessionStorage.setItem(key, String(Date.now()));
} catch (_) {}
const prefix = chapterTitle ? `\u300c${chapterTitle}\u300d` : "";
pushUserLog(`${prefix}\u516c\u9700\u8bfe\uff1a\u8bf7\u70b9\u300c\u505c\u6b62\u300d→ \u518d\u70b9\u300c\u5f00\u59cb\u300d\u7ee7\u7eed\uff08\u53ef\u53cd\u590d\u64cd\u4f5c\u9010\u8282\u8fc7\u5173\uff09`);
}
function parkGongxuChapterForManualRetry(ctx, tag) {
saveJob(ctx, {
phase: "pending",
nextStepDue: 0,
engineSessionId: "",
rtStartedAt: 0,
});
pushGongxuStopStartUserTip(tag);
refreshGongxuStudyHint();
}
function scheduleBgRunSoon(ms) {
const delay = Math.max(200, Number(ms) || 300);
setTimeout(() => {
if (getEnabled() && getApiMode() && isLeaderTab()) runBgScheduler();
}, delay);
}
function courseHasChapterLevelExam(doc, chapters) {
if (Array.isArray(chapters) && chapters.length) {
if (
chapters.some(
(ch) =>
!ch.interactive &&
(ch.examStatus === "exam" ||
ch.examDone ||
/\u5f85\u8003\u8bd5|\u5f85\u8003|\u8003\u8bd5\u901a\u8fc7|\u5df2\u901a\u8fc7|\u91cd\u65b0\u8003\u8bd5/.test(String(ch.studyState || "")))
)
) {
return true;
}
}
if (!doc || !doc.querySelectorAll) return false;
const nodes = doc.querySelectorAll(".course[data-href]");
for (let i = 0; i < nodes.length; i++) {
const el = nodes[i];
if (isElectiveChapter(el)) continue;
const statusText = (el.querySelector(".cw-status")?.textContent || "").replace(/\s+/g, " ");
const btnText = Array.from(el.querySelectorAll(".cw-status button, .cw-status a, .cw-status span"))
.map((n) => n.textContent || "")
.join(" ");
const blob = statusText + btnText;
if (/\u5f85\u8003\u8bd5|\u5f85\u8003|\u53bb\u8003\u8bd5|\u8003\u8bd5\u901a\u8fc7|\u5df2\u901a\u8fc7|\u91cd\u65b0\u8003\u8bd5/.test(blob)) return true;
const href = el.getAttribute("data-href") || "";
if (/exam\.aspx|\/exam\//i.test(href)) return true;
}
return false;
}
function detectGongxuCourseMeta(doc, courseTitle, html, chapters) {
const titleBlob = [
courseTitle || "",
doc.querySelector(".pace_text")?.textContent || "",
doc.querySelector(".information")?.textContent || "",
]
.join(" ")
.replace(/\s+/g, " ");
if (courseHasChapterLevelExam(doc, chapters)) return false;
if (/\u516c\u9700\u8bfe|\u516c\u9700\u79d1\u76ee/.test(titleBlob)) return true;
if (/\u7edf\u4e00\u8bfe\u7a0b\u8003\u6838/.test(titleBlob + String(html || ""))) return true;
return false;
}
function isSelectedCoursesAllFinished() {
const selected = pruneQueueToCourses();
if (!selected.length) return false;
return selected.every((cid) => {
const course = panelState.courses.find((c) => c.cid === cid);
if (!course || !isCourseChaptersReady(course)) return false;
const total = Number(course.chapterTotal) || 0;
if (total <= 0) return false;
const studyDone = Number(course.studyDone) || 0;
if (isGongxuCourse(course)) return studyDone >= total;
return studyDone >= total && (Number(course.examDone) || 0) >= total;
});
}
function getIncompleteJobs() {
return Object.values(loadJobs()).filter((j) => j && j.phase !== "done");
}
function hasSelectedPanelWorkRemaining() {
const selected = pruneQueueToCourses();
if (!selected.length) return false;
return selected.some((cid) => {
const c = findPanelCourseByCid(cid);
if (!c) return true;
return isCourseUnfinished(c);
});
}
function maybeAutoStopWhenFinished() {
if (!getEnabled()) return false;
if (hasPendingExamWork()) return false;
reconcileFinishedJobsFromPanel();
const selected = pruneQueueToCourses();
const jobs = loadJobs();
const selectedJobs = Object.values(jobs).filter(
(j) => j && selected.some((cid) => normalizeQueueCid(j.cid) === normalizeQueueCid(cid))
);
const incompleteSelected = selectedJobs.filter((j) => j.phase !== "done");
const coursesDone = isSelectedCoursesAllFinished();
if (
hasSelectedPanelWorkRemaining() &&
incompleteSelected.length === 0 &&
selectedJobs.length === 0
) {
trace("maybeAutoStop: \u9762\u677f\u4ecd\u6709\u672a\u5b8c\u6210\u7ae0\u8282\u4f46\u672c\u5730\u4efb\u52a1\u4e3a\u7a7a\uff0c\u8df3\u8fc7\u81ea\u52a8\u505c\u6b62");
return false;
}
if (
panelState.studyStartedAt &&
Date.now() - panelState.studyStartedAt < 120000 &&
incompleteSelected.length === 0 &&
selectedJobs.length === 0
) {
trace("maybeAutoStop: \u5f00\u59cb\u5b66\u4e60\u540e 2 \u5206\u949f\u5185\u4e14\u65e0\u4efb\u52a1\uff0c\u8df3\u8fc7\u81ea\u52a8\u505c\u6b62");
return false;
}
if (selected.length > 0 && incompleteSelected.length === 0 && selectedJobs.length > 0) {
notifyGongxuCourseExamIfNeeded();
stopAutoStudy("\u6240\u9009\u8bfe\u7a0b\u5df2\u5168\u90e8\u5b8c\u6210");
return true;
}
if (coursesDone && incompleteSelected.length === 0 && selectedJobs.length > 0) {
notifyGongxuCourseExamIfNeeded();
stopAutoStudy("\u6240\u9009\u8bfe\u7a0b\u5df2\u5168\u90e8\u5b8c\u6210");
return true;
}
if (coursesDone && incompleteSelected.length > 0) {
const onlyRetry = incompleteSelected.every(
(j) =>
JOB_IDLE_RETRY_PHASES.has(j.phase) ||
(j.phase === "exam_pending" && (j.examFailCount || 0) >= 3)
);
if (onlyRetry) {
incompleteSelected.forEach((j) => {
saveJob(jobToCtx(j), { phase: "done", examDone: true });
});
stopAutoStudy("\u8bfe\u7a0b\u8fdb\u5ea6\u5df2\u5168\u90e8\u5b8c\u6210\uff0c\u7a0b\u5e8f\u81ea\u52a8\u505c\u6b62");
return true;
}
}
return false;
}
function openHyQqGroup() {
try {
GM_openInTab(QQ_GROUP_LINK, { active: true, insert: true, setParent: true });
} catch (_) {
window.open(QQ_GROUP_LINK, "_blank", "noopener,noreferrer");
}
}
function getApiMode() {
return true;
}
function getAutoExam() {
return true;
}
function migrateRunModeKey() {
const v = localStorage.getItem(RUN_MODE_KEY);
if (v === "silent") localStorage.setItem(RUN_MODE_KEY, "offline");
}
function ensurePanelDefaults() {
localStorage.setItem(API_MODE_KEY, "1");
localStorage.setItem(AUTO_EXAM_KEY, "1");
migrateRunModeKey();
}
function isParallelMode() {
return getRunMode() !== "heartbeat";
}
function getRunMode() {
migrateRunModeKey();
return localStorage.getItem(RUN_MODE_KEY) === "offline" ? "offline" : "realtime";
}
function formatRunModeLabel(mode) {
if (mode === "offline") return "\u79bb\u7ebf\u6279\u91cf";
return "1:1\u6302\u673a";
}
function setRunMode(mode) {
localStorage.setItem(RUN_MODE_KEY, mode === "offline" ? "offline" : "realtime");
}
function isLadderMode() {
return getRunMode() === "ladder";
}
function isP3ParallelMode() {
return getRunMode() === "p3par";
}
function isParallelStepMode() {
return getRunMode() === "pstep";
}
function isTailMode() {
return getRunMode() === "tail";
}
function isSerialMode() {
return getRunMode() === "serial";
}
function isRealtimeMode() {
return getRunMode() === "realtime";
}
function isOfflineMode() {
return getRunMode() === "offline";
}
function isSilentMode() {
return isOfflineMode();
}
function loadSilentState() {
const uid = getLearningUserId();
if (uid) migrateLegacySilentStateForUid(uid);
try {
return JSON.parse(localStorage.getItem(silentStateKeyForUid(uid)) || "{}") || {};
} catch (_) {
return {};
}
}
function saveSilentState(s) {
const uid = getLearningUserId();
if (!uid) return;
localStorage.setItem(silentStateKeyForUid(uid), JSON.stringify(s));
}
function patchSilentState(patch) {
saveSilentState(Object.assign({}, loadSilentState(), patch || {}));
}
function resetSilentState() {
const uid = getLearningUserId();
if (uid) localStorage.removeItem(silentStateKeyForUid(uid));
}
function getHangTabs() {
return localStorage.getItem(HANG_TABS_KEY) !== "0";
}
function getMultiplier() {
const v = parseFloat(localStorage.getItem(MULTIPLIER_KEY) || String(DEFAULT_MULT));
if (!Number.isFinite(v)) return DEFAULT_MULT;
return Math.min(10, Math.max(1, v));
}
function setMultiplier(v) {
localStorage.setItem(MULTIPLIER_KEY, String(v));
}
function getAutoNext() {
return localStorage.getItem(AUTO_NEXT_KEY) !== "0";
}
function loadRunLog() {
try {
const arr = JSON.parse(localStorage.getItem(RUN_LOG_KEY) || "[]");
return Array.isArray(arr) ? arr.slice(-320) : [];
} catch (_) {
return [];
}
}
function saveRunLog() {
try {
localStorage.setItem(RUN_LOG_KEY, JSON.stringify(runLogLines));
} catch (_) {}
}
let runLogLines = loadRunLog();
const panelState = {
courses: [],
chapterPreview: [],
refreshing: false,
lastCourseListKey: "",
loginRequired: false,
collectLoginLost: false,
previewRefreshTimer: null,
selectionLock: null,
queueSelectSuppressUntil: 0,
chapterPreviewDebounce: null,
lastChapterStudyLogKey: "",
studyStartedAt: 0,
};
function renderHyCheckbox(opts = {}) {
const { id = "", checked = false, dataCid = "" } = opts;
const idAttr = id ? ` id="${escapeHtml(id)}"` : "";
const dataAttr = dataCid ? ` data-cid="${escapeHtml(dataCid)}"` : "";
return ``;
}
function getCourseListRenderKey() {
const selected = getEffectiveQueue().slice().sort().join(",");
const courses = panelState.courses
.map(
(c) =>
`${c.cid}:${c.chaptersLoaded}:${c.chapterTotal || 0}:${c.studyDone}:${c.examDone || 0}:${c.progressError || ""}`
)
.join("|");
const refreshing = panelState.refreshing ? "1" : "0";
const loginFlag = panelState.loginRequired ? "1" : "0";
const curJob = getActiveJob(loadJobs());
return `${refreshing}|${loginFlag}|${courses}|${selected}|${curJob?.cid || ""}`;
}
function escapeHtml(s) {
return String(s || "")
.replace(/&/g, "&")
.replace(//g, ">");
}
function pushLog(text) {
trace(text);
}
function pushUserLog(text) {
const line = `[${new Date().toLocaleTimeString()}] ${text}`;
runLogLines.push(line);
if (runLogLines.length > 320) runLogLines = runLogLines.slice(-320);
saveRunLog();
renderRunLog();
if (getEnabled() || !isLiveStudyStatusText(text)) {
updateLogStatusBar(text);
}
if (window.parent !== window) {
postHdIframeEvent("log", { text });
}
}
function formatCloudErrorMessage(msg) {
const m = String(msg || "").trim();
if (!m) return "\u4e91\u7aef\u6388\u6743\u5931\u8d25";
if (/invalid token/i.test(m)) return "Token \u65e0\u6548\uff0c\u8bf7\u68c0\u67e5\u662f\u5426\u7c98\u8d34\u5b8c\u6574";
if (/expired/i.test(m)) return "Token \u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u65b0\u83b7\u53d6";
if (/revoked|disabled/i.test(m)) return "Token \u5df2\u7981\u7528\uff0c\u8bf7\u8054\u7cfb\u7ba1\u7406\u5458";
if (/token_bound_to_other/i.test(m)) return "Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u534e\u533b\u7f51\u8d26\u53f7\uff0c\u8bf7\u6362 Token \u6216\u767b\u5f55\u5bf9\u5e94\u8d26\u53f7";
if (/free_quota_exhausted|\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c/i.test(m)) {
return `\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08${cloudState.freeUsedChapters}/${cloudState.freeChapterLimit} \u8282\uff09\uff0c\u8bf7\u5347\u7ea7 Pro`;
}
return m;
}
function notifyUser(msg) {
const text = String(msg || "").trim();
if (!text) return;
pushUserLog(text);
}
function notifyAuthError(msg) {
const text = formatCloudErrorMessage(msg);
notifyUser(text);
setPanelHint(text.slice(0, 72));
}
function updateLogStatusBar(text) {
const el = document.getElementById("hy-cme-log-status");
if (!el) return;
const msg = String(text || "").trim();
if (!msg) {
el.textContent = "";
el.style.display = "none";
return;
}
el.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
el.style.display = "block";
}
function resolveWallClockStudiedSec(job) {
const j = job || {};
const dur = Number(j.duration) || 3600;
const entry = Number(j.rtEntrySec != null ? j.rtEntrySec : j.startSec != null ? j.startSec : 1);
let posted = j.lastPostedSec;
if (posted == null) posted = j.studiedSec;
if (posted == null && j.uid && j.cwrid) {
posted = readChapterStudiedSec(j.uid, j.cwrid, j.coaid, dur);
}
posted = Number(posted) || entry;
if (!j.rtStartedAt) return Math.min(dur, Math.max(entry, posted));
const elapsed = Math.max(0, Date.now() - Number(j.rtStartedAt));
const wallSteps = Math.floor(elapsed / RT_STEP_GAP_MS);
const wallSec = Math.min(dur, entry + wallSteps * RT_STEP_SEC);
return Math.min(dur, Math.max(posted, wallSec));
}
function ensureRtWallClock(ctx) {
const job = getJob(ctx);
if (!job || job.rtStartedAt) return;
const entry =
job.rtEntrySec != null ? job.rtEntrySec : job.startSec != null ? job.startSec : 1;
saveJob(ctx, { rtStartedAt: Date.now(), rtEntrySec: entry });
}
function formatChapterStudyStatusLine(job, nextDueMs) {
const j = job || {};
const course = j.cid ? findPanelCourseByCid(j.cid) : null;
const courseName = (course && course.title) || "\u8bfe\u7a0b";
const chapter = resolveChapterDisplayTitle(j);
const dur = Number(j.duration) || 3600;
const studied = resolveWallClockStudiedSec(j);
const totalMin = Math.max(1, Math.round(dur / 60));
const studiedText = formatStudiedDuration(studied);
let nextPart = "";
const waitMs =
nextDueMs > 0
? nextDueMs
: j.nextStepDue && j.nextStepDue > Date.now()
? j.nextStepDue - Date.now()
: 0;
if (waitMs >= RT_STEP_GAP_MS * 0.85) {
nextPart = ` · ${formatEta(waitMs)}\u540e\u66f4\u65b0\u8fdb\u5ea6`;
} else if (waitMs >= 30000) {
nextPart = ` · ${formatEta(waitMs)}\u540e\u91cd\u8bd5`;
} else if (waitMs > 0) {
nextPart = " · \u5373\u5c06\u4e0a\u62a5";
}
return `\u300c${courseName}\u300d${chapter} · \u603b${totalMin}\u5206\u949f · \u5df2\u5b66${studiedText}${nextPart}`;
}
function getSelectedProgressTotals() {
const selected = pruneQueueToCourses();
let videoDone = 0;
let videoTotal = 0;
let examDone = 0;
let examTotal = 0;
selected.forEach((cid) => {
const c = panelState.courses.find((x) => x.cid === cid);
if (!c || !isCourseChaptersReady(c)) return;
const t = Number(c.chapterTotal) || 0;
videoTotal += t;
videoDone += Number(c.studyDone) || 0;
if (isGongxuCourse(c)) {
examTotal += 1;
if ((Number(c.studyDone) || 0) >= t && t > 0) examDone += 1;
} else {
examTotal += t;
examDone += Number(c.examDone) || 0;
}
});
return { videoDone, videoTotal, examDone, examTotal, selectedCount: selected.length };
}
function formatSelectedProgressLine() {
const p = getSelectedProgressTotals();
if (!p.selectedCount) return "\u8bf7\u52fe\u9009\u8bfe\u7a0b";
if (!p.videoTotal) return `\u5df2\u9009 ${p.selectedCount} \u95e8 · \u7ae0\u8282\u52a0\u8f7d\u4e2d…`;
const selected = pruneQueueToCourses();
const allGongxu =
selected.length > 0 &&
selected.every((cid) => {
const c = findPanelCourseByCid(cid);
return c && isGongxuCourse(c);
});
if (allGongxu) {
return `\u89c6\u9891 ${p.videoDone}/${p.videoTotal} · \u8bfe\u7ea7\u8003\u6838 ${p.examDone}/${p.examTotal}`;
}
return `\u89c6\u9891 ${p.videoDone}/${p.videoTotal} · \u8003\u8bd5 ${p.examDone}/${p.examTotal}`;
}
function renderRunLog() {
const box = document.getElementById("hy-cme-run-log");
if (!box) return;
box.innerHTML = runLogLines
.map((line) => `
${escapeHtml(line)}
`)
.join("");
box.scrollTop = box.scrollHeight;
}
function clearRunLog() {
runLogLines = [];
try {
localStorage.removeItem(RUN_LOG_KEY);
} catch (_) {}
renderRunLog();
updateLogStatusBar("");
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function hyExamRetryWait(ms, tag) {
const step = 500;
let left = Math.max(0, ms | 0);
while (left > 0) {
if (!getEnabled()) return false;
await sleep(Math.min(step, left));
left -= step;
}
return true;
}
function isHyExamCaptchaHtml(text) {
return (
/exam_code\.aspx|\u68c0\u6d4b\u5230\u8003\u8bd5\u5f02\u5e38|txtCheckCode|imgCheckCode/i.test(String(text || "")) ||
/\u9a8c\u8bc1\u7801|ValidateCode|validatecode|checkcode|chk_?code|yzm|imgcode/i.test(String(text || ""))
);
}
function isHyExamCodePage(text, url) {
return (
/exam_code\.aspx/i.test(String(url || "")) ||
/\u68c0\u6d4b\u5230\u8003\u8bd5\u5f02\u5e38|name=["']txtCheckCode["']|id=["']imgCheckCode["']/i.test(String(text || ""))
);
}
function parseHyExamCodePage(html, pageUrl) {
if (!html || !isHyExamCodePage(html, pageUrl)) return null;
const cwidM =
String(pageUrl || "").match(/[?&]cwid=([^&]+)/i) ||
String(html).match(/exam_code\.aspx\?cwid=([^&'"]+)/i);
const cwid = cwidM ? decodeURIComponent(cwidM[1]) : "";
const page =
pageUrl ||
(cwid ? `${location.origin}/pages/exam_code.aspx?cwid=${encodeURIComponent(cwid)}` : "");
return {
cwid,
pageUrl: page,
hidden: parseAspNetHidden(html),
imageUrl: `${location.origin}/secure/CheckCode.aspx?id=${Math.random()}`,
};
}
let hyTesseractWorker = null;
function buildHyOcrPredictUrl(root) {
const u = String(root || "").trim().replace(/\/+$/, "");
if (!u) return "";
if (/\/predict$/i.test(u)) return u;
return `${u}/predict`;
}
function cleanHyOcrDigits(text) {
const digits = String(text || "").replace(/\D/g, "");
if (digits.length >= 5) return digits.slice(0, 5);
if (digits.length === 4) return digits;
if (digits.length === 6) return digits.slice(0, 5);
return "";
}
function parseHyOcrPredictResponse(text) {
const raw = String(text || "").trim();
if (!raw) return "";
try {
const obj = JSON.parse(raw);
const candidates = [
obj && obj.text,
obj && obj.raw,
obj && obj.result,
obj && obj.data && obj.data.text,
obj && obj.data && obj.data.result,
];
for (const c of candidates) {
const code = cleanHyOcrDigits(c);
if (code) return code;
}
if (obj && obj.ok === false && obj.raw) {
const code = cleanHyOcrDigits(obj.raw);
if (code) return code;
}
} catch (_) {}
return cleanHyOcrDigits(raw);
}
async function blobToBase64(blob) {
const buf = await blob.arrayBuffer();
const bytes = new Uint8Array(buf);
let binary = "";
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
return btoa(binary);
}
async function ocrHyCheckCodeViaDdddocr(blob) {
if (!cloudState.captchaSelfApiEnable) return "";
await _cl(false);
const url = buildHyCaptchaPredictUrl();
if (!url) return "";
const base64 = await blobToBase64(blob);
const isProxy = /\/api\/huayi\/captcha\/self-predict/i.test(url);
const headers = { "Content-Type": "application/json" };
if (!isProxy && cloudState.captchaSelfApiToken) {
headers["X-Token"] = cloudState.captchaSelfApiToken;
}
const payload = isProxy
? JSON.stringify({ lease: cloudState.cloudLease, image_base64: base64 })
: JSON.stringify({ image_base64: base64 });
const res = await gmRequestWithStatus(url, "POST", headers, payload);
const text = String(res.text || "");
if (res.status === 401) {
throw new Error(
isProxy
? "OCR 401\uff1a\u6388\u6743\u670d\u672a\u914d\u7f6e HY_CME_OCR_SELF_TOKEN \u6216 lease \u65e0\u6548"
: "OCR 401\uff1a\u76f4\u8fde OCR \u9700 X-Token"
);
}
if (res.status < 200 || res.status >= 300) {
let extra = "";
try {
const obj = JSON.parse(text);
if (obj.path) extra = ` path=${obj.path}`;
if (obj.reason === "ocr_http_404") {
extra += " → OCR 404\uff1a/predict_huayi \u672a\u6ce8\u518c\u6216\u7aef\u53e3\u4e0d\u5bf9";
}
} catch (_) {}
throw new Error(`HTTP ${res.status}${extra}${text ? ` ${text.slice(0, 160)}` : ""}`);
}
const parsed = parseHyOcrPredictResponse(text);
const code = typeof parsed === "string" ? parsed : "";
if (!code) {
let detail = "";
try {
const obj = JSON.parse(text);
const parts = [];
if (obj && obj.reason) parts.push(String(obj.reason));
if (obj && obj.hint) parts.push(String(obj.hint));
if (obj && obj.raw) parts.push(`raw=${obj.raw}`);
if (obj && obj.path) parts.push(`path=${obj.path}`);
detail = parts.length ? ` (${parts.join("; ")})` : "";
} catch (_) {}
throw new Error(`\u8bc6\u522b\u7ed3\u679c\u4e3a\u7a7a${detail || "\uff08\u534e\u533b\u7f515\u4f4d\u6570\u5b57\uff0c\u786e\u8ba4 predict_huayi \u5df2\u90e8\u7f72\uff09"}`);
}
return code;
}
function resolveHyTesseractGlobal() {
const g = window.Tesseract;
if (!g) return null;
if (typeof g.createWorker === "function") return g;
if (g.default && typeof g.default.createWorker === "function") return g.default;
return null;
}
async function loadHyTesseract() {
let T = resolveHyTesseractGlobal();
if (T) return T;
await new Promise((resolve, reject) => {
const s = document.createElement("script");
s.src = "https://cdn.jsdelivr.net/npm/tesseract.js@4.1.4/dist/tesseract.min.js";
s.onload = resolve;
s.onerror = () => reject(new Error("Tesseract \u52a0\u8f7d\u5931\u8d25"));
document.head.appendChild(s);
});
for (let i = 0; i < 30; i++) {
T = resolveHyTesseractGlobal();
if (T) return T;
await sleep(100);
}
throw new Error("Tesseract \u52a0\u8f7d\u5931\u8d25");
}
async function getHyTesseractWorker() {
if (hyTesseractWorker) return hyTesseractWorker;
const Tesseract = await loadHyTesseract();
if (!Tesseract || typeof Tesseract.createWorker !== "function") {
throw new Error("Tesseract.createWorker \u4e0d\u53ef\u7528");
}
const worker = await Tesseract.createWorker("eng", 1, { logger: () => {} });
await worker.setParameters({
tessedit_char_whitelist: "0123456789",
tessedit_pageseg_mode: "7",
});
hyTesseractWorker = worker;
return worker;
}
async function preprocessHyCheckCodeBlob(blob) {
const bmp = await createImageBitmap(blob);
const scale = 3;
const canvas = document.createElement("canvas");
canvas.width = Math.max(1, bmp.width * scale);
canvas.height = Math.max(1, bmp.height * scale);
const ctx = canvas.getContext("2d");
ctx.imageSmoothingEnabled = false;
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(bmp, 0, 0, canvas.width, canvas.height);
const img = ctx.getImageData(0, 0, canvas.width, canvas.height);
for (let i = 0; i < img.data.length; i += 4) {
const r = img.data[i];
const g = img.data[i + 1];
const b = img.data[i + 2];
const isDigit = b > 70 && b > r + 15 && r + g + b < 650;
const v = isDigit ? 0 : 255;
img.data[i] = v;
img.data[i + 1] = v;
img.data[i + 2] = v;
img.data[i + 3] = 255;
}
ctx.putImageData(img, 0, 0);
return new Promise((resolve, reject) => {
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error("\u9a8c\u8bc1\u7801\u56fe\u5904\u7406\u5931\u8d25"))), "image/png");
});
}
async function ocrHyCheckCodeViaTesseract(blob) {
const processed = await preprocessHyCheckCodeBlob(blob);
const worker = await getHyTesseractWorker();
const { data } = await worker.recognize(processed);
return cleanHyOcrDigits(data.text || "");
}
async function ocrHyCheckCodeImage(blob, tag) {
if (cloudState.captchaSelfApiEnable && (cloudState.captchaSelfApiUrl || getCloudApiBase())) {
try {
const code = await ocrHyCheckCodeViaDdddocr(blob);
if (code) {
if (tag) pushLog(`[${tag}] OCR\uff1a${code}`);
return code;
}
} catch (e) {
if (tag) {
pushLog(`[${tag}] OCR \u5931\u8d25\uff1a${e && e.message ? e.message : e}`);
}
}
}
try {
const code = await ocrHyCheckCodeViaTesseract(blob);
if (code) {
if (tag) pushLog(`[${tag}] Tesseract\uff1a${code}`);
return code;
}
if (tag) pushLog(`[${tag}] Tesseract \u4e5f\u672a\u8bc6\u522b\u51fa\u6570\u5b57`);
} catch (e) {
if (tag) pushLog(`[${tag}] Tesseract \u5931\u8d25\uff1a${e && e.message ? e.message : e}`);
}
return "";
}
async function fetchHyExamCodePage(cwid, referer) {
const pageUrl = `${location.origin}/pages/exam_code.aspx?cwid=${encodeURIComponent(cwid)}`;
const r = await fetch(pageUrl, {
credentials: "include",
headers: {
Accept: "text/html,application/xhtml+xml",
Referer: referer || pageUrl,
},
__hyEngineMark: true,
});
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const html = await r.text();
const page = parseHyExamCodePage(html, r.url || pageUrl);
if (!page) throw new Error("\u9a8c\u8bc1\u7801\u9875\u89e3\u6790\u5931\u8d25");
return { html, page };
}
async function fetchHyCheckCodeBlob(pageUrl) {
const imageUrl = `${location.origin}/secure/CheckCode.aspx?id=${Math.random()}`;
const r = await fetch(imageUrl, {
credentials: "include",
headers: { Accept: "image/*", Referer: pageUrl },
__hyEngineMark: true,
});
if (!r.ok) throw new Error(`\u9a8c\u8bc1\u7801\u56fe HTTP ${r.status}`);
return r.blob();
}
async function submitHyExamCode(cwid, code, hidden, referer) {
const pageUrl = `${location.origin}/pages/exam_code.aspx?cwid=${encodeURIComponent(cwid)}`;
const parts = [
"__EVENTTARGET=",
"__EVENTARGUMENT=",
`__VIEWSTATE=${encodeURIComponent(hidden.__VIEWSTATE || "")}`,
`__VIEWSTATEGENERATOR=${encodeURIComponent(hidden.__VIEWSTATEGENERATOR || "")}`,
`__EVENTVALIDATION=${encodeURIComponent(hidden.__EVENTVALIDATION || "")}`,
`txtCheckCode=${encodeURIComponent(code)}`,
`btnYes=${encodeURIComponent("\u63d0\u4ea4")}`,
];
const r = await fetch(pageUrl, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "text/html,application/xhtml+xml",
Referer: referer || pageUrl,
},
body: parts.join("&"),
redirect: "follow",
__hyEngineMark: true,
});
const text = await r.text();
const finalUrl = r.url || pageUrl;
if (/exam\.aspx/i.test(finalUrl) && !/exam_code/i.test(finalUrl)) {
return { ok: true, url: finalUrl, text };
}
if (isHyExamCodePage(text, finalUrl)) {
return { ok: false, wrongCode: true, html: text, hidden: parseAspNetHidden(text), url: finalUrl };
}
return { ok: false, msg: "\u9a8c\u8bc1\u7801\u63d0\u4ea4\u672a\u901a\u8fc7", html: text, url: finalUrl };
}
function removeHyExamCodeModal() {
const el = document.getElementById("hy-cme-exam-code-modal");
if (!el) return;
const img = el.querySelector("#hy-cme-cap-img");
if (img && img._objUrl) URL.revokeObjectURL(img._objUrl);
el.remove();
}
async function promptHyExamCodeManual(tag, blob, pageUrl) {
let imageBlob = blob || null;
if (!imageBlob && pageUrl) {
try {
imageBlob = await fetchHyCheckCodeBlob(pageUrl);
} catch (_) {}
}
return new Promise((resolve) => {
injectHyPanelStyles();
removeHyExamCodeModal();
const wrap = document.createElement("div");
wrap.id = "hy-cme-exam-code-modal";
const title = escapeHtml(tag || "\u8003\u8bd5\u9a8c\u8bc1\u7801");
wrap.innerHTML = `
${title}
\u8bf7\u8f93\u5165\u56fe\u4e2d\u9a8c\u8bc1\u7801\uff08\u7eaf\u6570\u5b57\uff09
`;
document.body.appendChild(wrap);
const img = wrap.querySelector("#hy-cme-cap-img");
const input = wrap.querySelector("#hy-cme-cap-input");
const setImageBlob = (b) => {
if (!img || !b) return;
if (img._objUrl) URL.revokeObjectURL(img._objUrl);
img._objUrl = URL.createObjectURL(b);
img.src = img._objUrl;
};
if (imageBlob) setImageBlob(imageBlob);
const finish = (code) => {
removeHyExamCodeModal();
resolve(String(code || "").replace(/\D/g, ""));
};
wrap.querySelector("#hy-cme-cap-ok")?.addEventListener("click", () => finish(input.value));
wrap.querySelector("#hy-cme-cap-cancel")?.addEventListener("click", () => finish(""));
wrap.querySelector("#hy-cme-cap-refresh")?.addEventListener("click", async () => {
if (!pageUrl) return;
try {
const b = await fetchHyCheckCodeBlob(pageUrl);
imageBlob = b;
setImageBlob(b);
input.value = "";
input.focus();
} catch (e) {
pushLog(`[${tag}] \u6362\u9a8c\u8bc1\u7801\u56fe\u5931\u8d25\uff1a${e && e.message ? e.message : e}`);
}
});
wrap.addEventListener("click", (e) => {
if (e.target === wrap) finish("");
});
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") finish(input.value);
if (e.key === "Escape") finish("");
});
setTimeout(() => input.focus(), 50);
});
}
async function solveHyExamCodeChallenge(cwid, tag, seedHtml, referer) {
const maxTry = HY_EXAM_CODE_OCR_MAX + HY_EXAM_CODE_MANUAL_MAX;
let html = seedHtml || "";
let pageReferer = referer || `${location.origin}/pages/exam.aspx?cwid=${encodeURIComponent(cwid)}`;
const ocrMax =
cloudState.captchaSelfApiEnable && (cloudState.captchaSelfApiUrl || getCloudApiBase())
? HY_EXAM_CODE_OCR_MAX
: 0;
pushLog(
`[${tag}] \u8003\u8bd5\u9a8c\u8bc1\u7801\uff1a${ocrMax ? `OCR ${buildHyCaptchaPredictUrl()}` : "\u624b\u52a8\u8f93\u5165"}…`
);
for (let i = 0; i < maxTry; i++) {
if (!getEnabled()) return { ok: false, msg: "\u5df2\u6682\u505c", paused: true };
let page;
if (html && isHyExamCodePage(html)) {
page = parseHyExamCodePage(html, `${location.origin}/pages/exam_code.aspx?cwid=${encodeURIComponent(cwid)}`);
html = "";
} else {
try {
const fetched = await fetchHyExamCodePage(cwid, pageReferer);
page = fetched.page;
pageReferer = page.pageUrl;
} catch (e) {
return { ok: false, msg: e && e.message ? e.message : "\u62c9\u53d6\u9a8c\u8bc1\u7801\u9875\u5931\u8d25" };
}
}
if (!page) return { ok: false, msg: "\u9a8c\u8bc1\u7801\u9875\u89e3\u6790\u5931\u8d25" };
let code = "";
let captchaBlob = null;
try {
captchaBlob = await fetchHyCheckCodeBlob(page.pageUrl);
if (i < ocrMax) {
code = await ocrHyCheckCodeImage(captchaBlob, tag);
if (code) pushLog(`[${tag}] \u91c7\u7528\u9a8c\u8bc1\u7801 ${code}\uff08${i + 1}/${ocrMax}\uff09`);
}
} catch (e) {
pushLog(`[${tag}] \u9a8c\u8bc1\u7801\u56fe\u62c9\u53d6\u5931\u8d25\uff1a${e && e.message ? e.message : e}`);
}
if (!code || code.length < 4) {
if (i >= ocrMax) {
code = await promptHyExamCodeManual(tag, captchaBlob, page.pageUrl);
if (!code) return { ok: false, msg: "\u672a\u8f93\u5165\u9a8c\u8bc1\u7801" };
pushLog(`[${tag}] \u4f7f\u7528\u624b\u52a8\u8f93\u5165\u9a8c\u8bc1\u7801`);
} else {
await sleep(1200);
continue;
}
}
const sub = await submitHyExamCode(cwid, code, page.hidden, page.pageUrl);
if (sub.ok) {
markHyExamCodePassed(cwid);
pushLog(`[${tag}] \u9a8c\u8bc1\u7801\u5df2\u901a\u8fc7\uff0c\u7ee7\u7eed\u8003\u8bd5`);
return { ok: true, url: sub.url };
}
if (sub.wrongCode) {
pushLog(`[${tag}] \u9a8c\u8bc1\u7801\u9519\u8bef\uff0c\u6362\u56fe\u91cd\u8bd5\uff08${i + 1}/${maxTry}\uff09`);
html = sub.html || "";
if (!(await hyExamRetryWait(2000, tag))) {
return { ok: false, msg: "\u5df2\u6682\u505c", paused: true };
}
continue;
}
return { ok: false, msg: sub.msg || "\u9a8c\u8bc1\u7801\u63d0\u4ea4\u5931\u8d25" };
}
return { ok: false, msg: "\u9a8c\u8bc1\u7801\u591a\u6b21\u5931\u8d25" };
}
async function tryPassHyExamCaptcha(cwid, tag, html, referer) {
if (!cwid) return { ok: false, msg: "\u7f3a\u5c11 cwid" };
if (isHyExamCodeRecentlyPassed(cwid)) {
try {
const url = `${location.origin}/pages/exam.aspx?cwid=${encodeURIComponent(cwid)}`;
const r = await fetch(url, {
credentials: "include",
headers: { Accept: "text/html", Referer: referer || url },
__hyEngineMark: true,
});
const text = await r.text();
if (!isHyExamCodePage(text, r.url || url) && parseHyExamQuestions(text).length) {
pushLog(`[${tag}] \u9a8c\u8bc1\u7801\u6709\u6548\u671f\u5185\uff0c\u8df3\u8fc7\u91cd\u590d\u9a8c\u8bc1`);
return { ok: true, skipped: true };
}
} catch (_) {}
}
return solveHyExamCodeChallenge(cwid, tag, html, referer);
}
function waitUntil(fn, timeoutMs = 30000, stepMs = 300) {
return new Promise((resolve, reject) => {
const start = Date.now();
const tick = () => {
try {
const v = fn();
if (v) return resolve(v);
} catch (_) {}
if (Date.now() - start > timeoutMs) return reject(new Error("wait timeout"));
setTimeout(tick, stepMs);
};
tick();
});
}
function formatEta(ms) {
if (ms <= 0) return "\u5df2\u5230\u70b9";
if (ms < 60000) return `${Math.max(1, Math.ceil(ms / 1000))}\u79d2`;
const m = Math.ceil(ms / 60000);
return m < 60 ? `${m}\u5206\u949f` : `${Math.floor(m / 60)}h${m % 60}m`;
}
function formatStudiedDuration(studiedSec) {
const s = Math.max(0, Math.round(Number(studiedSec) || 0));
if (s <= 0) return "0\u5206\u949f";
if (s < 60) return `${s}\u79d2`;
return `${Math.round(s / 60)}\u5206\u949f`;
}
function saveEnginePostedSec(ctx, sec) {
const n = Math.max(0, Math.round(Number(sec) || 0));
if (!ctx || n <= 0) return;
const prev = getJob(ctx);
const prevSec = prev && prev.lastPostedSec != null ? Number(prev.lastPostedSec) : 0;
const nextSec = Math.max(prevSec, n);
if (prev && prev.lastPostedSec === nextSec) return;
saveJob(ctx, {
lastPostedSec: nextSec,
rtEntrySec: prev && prev.rtEntrySec != null ? prev.rtEntrySec : n,
});
if (ctx.lsKey && n >= 60) {
try {
const cur = parseInt(localStorage.getItem(ctx.lsKey) || "0", 10) || 0;
if (n > cur) localStorage.setItem(ctx.lsKey, String(n));
} catch (_) {}
}
}
function chapterStudyWaitMs(job, nextDueMs) {
const j = job || {};
return nextDueMs > 0
? nextDueMs
: j.nextStepDue && j.nextStepDue > Date.now()
? j.nextStepDue - Date.now()
: 0;
}
function shouldShowChapterStudyUserLog(job, nextDueMs) {
const waitMs = chapterStudyWaitMs(job, nextDueMs);
if (waitMs >= 30000 && waitMs < RT_STEP_GAP_MS * 0.85) return false;
return true;
}
function pushUserChapterStudyLog(job, nextDueMs) {
if (!getEnabled()) return;
const j = job || {};
if (!shouldShowChapterStudyUserLog(j, nextDueMs)) return;
const waitKey =
nextDueMs > 0
? Math.round(nextDueMs / 1000)
: j.nextStepDue && j.nextStepDue > Date.now()
? Math.round((j.nextStepDue - Date.now()) / 1000)
: 0;
const studiedKey = Math.floor(resolveWallClockStudiedSec(j) / 60);
const logKey = `${j.lsKey || j.cwrid || ""}:${studiedKey}:${waitKey}`;
if (panelState.lastChapterStudyLogKey === logKey) return;
panelState.lastChapterStudyLogKey = logKey;
pushUserLog(formatChapterStudyStatusLine(j, nextDueMs || 0));
}
function formatBeijingDateTime(ts) {
const n = Number(ts || 0);
if (!n) return "—";
try {
return new Date(n).toLocaleString("zh-CN", {
timeZone: "Asia/Shanghai",
hour12: false,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
} catch (_) {
return new Date(n).toLocaleString();
}
}
function formatDurationCn(sec) {
const s = Math.max(0, Math.round(sec || 0));
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
if (h > 0) return `${h}\u5c0f\u65f6${m}\u5206\u949f`;
return `${m}\u5206\u949f`;
}
function offlineBatchBufferMs(st) {
return Number(st && st.batchBufferMs) || OFFLINE_BATCH_BUFFER_MS;
}
function offlineBatchDueMs(st, courseTotalMs, sessionStart) {
if (st && st.batchCompleteDue) return Number(st.batchCompleteDue);
const start = sessionStart || (st && st.sessionStart) || Date.now();
const courseMs = courseTotalMs != null ? courseTotalMs : Number(st && st.totalMs) || 0;
return start + courseMs + offlineBatchBufferMs(st);
}
function sortOfflineClockJobs(entries) {
return entries.slice().sort(function (a, b) {
const ja = a[1];
const jb = b[1];
const ta = ja.filledAt || ja.createdAt || 0;
const tb = jb.filledAt || jb.createdAt || 0;
if (ta !== tb) return ta - tb;
return String(a[0]).localeCompare(String(b[0]));
});
}
function assignPerCourseOfflineDue(jobs, sessionUid) {
const st = loadSilentState();
const sessionStart = Number(st.sessionStart || 0);
if (!sessionStart) return;
const uid = sessionUid || getLearningUserId();
const clocked = sortOfflineClockJobs(
Object.entries(jobs).filter(function (entry) {
const j = entry[1];
return j && jobBelongsToSession(j, uid) && j.phase === "silent_clock";
})
);
let cumulativeMs = 0;
let changed = false;
clocked.forEach(function (entry) {
const key = entry[0];
const job = entry[1];
cumulativeMs += (job.duration || 3600) * 1000;
const due = sessionStart + cumulativeMs;
if (job.completeDue !== due) {
job.completeDue = due;
jobs[key] = job;
changed = true;
}
});
if (changed) saveJobs(jobs);
}
function isOfflineJobFinishDue(job, now, st) {
if (!job || job.phase !== "silent_clock") return false;
const ts = now != null ? now : Date.now();
const batchDue = offlineBatchDueMs(st);
if (batchDue && ts >= batchDue) return true;
const perDue = Number(job.completeDue || 0);
return perDue > 0 && ts >= perDue;
}
function listOfflineFinishReadyJobs(jobs, sessionUid, now) {
const st = loadSilentState();
const uid = sessionUid || getLearningUserId();
const ts = now != null ? now : Date.now();
return sortOfflineClockJobs(
Object.entries(jobs).filter(function (entry) {
const j = entry[1];
return j && jobBelongsToSession(j, uid) && isOfflineJobFinishDue(j, ts, st);
})
);
}
function countOfflinePartialFinishReady(jobs, sessionUid, now) {
const st = loadSilentState();
const ts = now != null ? now : Date.now();
const batchDue = offlineBatchDueMs(st);
if (batchDue && ts >= batchDue) return 0;
return listOfflineFinishReadyJobs(jobs, sessionUid, ts).length;
}
function hasOfflinePartialFinishReady() {
if (!isOfflineMode()) return false;
return countOfflinePartialFinishReady(loadJobs(), getLearningUserId(), Date.now()) > 0;
}
function maybeStopAfterPartialOfflineFinish(sessionUid) {
if (!getEnabled() || !isOfflineMode()) return;
const now = Date.now();
const st = loadSilentState();
const batchDue = offlineBatchDueMs(st);
if (!batchDue || now >= batchDue) return;
const jobs = loadJobs();
const stillWaiting = Object.values(jobs).some(function (j) {
return j && jobBelongsToSession(j, sessionUid) && j.phase === "silent_clock";
});
if (!stillWaiting) return;
if (listOfflineFinishReadyJobs(jobs, sessionUid, now).length) return;
stopAutoStudy("\u5df2\u5230\u70b9\u8bfe\u7a0b\u5df2\u6536\u5c3e\uff0c\u5176\u4f59\u8bf7\u7b49\u5f85\u540e\u518d\u70b9\u5f00\u59cb");
pushUserLog("\u90e8\u5206\u8bfe\u7a0b\u5df2\u5230\u70b9\u5e76\u5b8c\u6210\u6536\u5c3e\uff0c\u5176\u4f59\u8bfe\u7a0b\u8bf7\u7b49\u5f85\u7d2f\u8ba1\u8bfe\u957f\u65f6\u95f4\u540e\u518d\u70b9\u300c\u5f00\u59cb\u300d");
syncRunModePanel();
updatePanel();
}
function getOfflineBatchSnapshot() {
if (!isOfflineMode()) return null;
const sessionUid = getLearningUserId();
if (!sessionUid) return null;
const selected = pruneQueueToCourses();
if (!selected.length) return null;
const selectedSet = new Set(selected.map((s) => normalizeQueueCid(s)));
const allClocked = Object.values(loadJobs()).filter(
(j) =>
j &&
jobBelongsToSession(j, sessionUid) &&
["silent_clock", "silent_finish_retry"].includes(j.phase)
);
if (!allClocked.length) return null;
const clocked = allClocked.filter((j) => j.cid && selectedSet.has(normalizeQueueCid(j.cid)));
if (!clocked.length) return null;
const st = loadSilentState();
const filledAt = Number(st.filledAt || 0);
const sessionStart = Number(st.sessionStart || 0);
if (!filledAt || !sessionStart) return null;
const now = Date.now();
const batchTotalMs = allClocked.reduce((s, j) => s + (j.duration || 3600) * 1000, 0);
const matchTotalMs = clocked.reduce((s, j) => s + (j.duration || 3600) * 1000, 0);
const due = offlineBatchDueMs(st, batchTotalMs, sessionStart);
const totalSec = Math.round(matchTotalMs / 1000);
const partialReadyCount = clocked.filter(function (j) {
const perDue = Number(j.completeDue || 0);
return perDue > 0 && now >= perDue && now < due;
}).length;
return {
due,
totalSec,
totalMs: matchTotalMs,
totalCourses: clocked.length,
batchCourses: allClocked.length,
sessionStart,
filledAt,
waitingCount: clocked.length,
ready: now >= due,
partialReady: partialReadyCount > 0,
partialReadyCount,
beijingDue: st.beijingDue || formatBeijingDateTime(due),
filledReady: true,
sessionUid,
};
}
function notifyOfflineBatchFilled(due, remain, earlyStats) {
const st = loadSilentState();
const logKey = String(st.filledAt || st.sessionStart || due);
if (st.filledUserLogKey === logKey) return;
patchSilentState({ filledUserLogKey: logKey, lastWaitUserLogAt: Date.now() });
pushUserLog(`\u6279\u91cf\u62c9\u6ee1\u5b8c\u6210 · ${formatSelectedProgressLine()}`);
const early = earlyStats || {};
if (early.finished > 0) {
pushUserLog(
`\u81ea\u52a8\u68c0\u9a8c\uff1a${early.finished} \u8282\u5df2\u6ee1\u8db3\u5e73\u53f0\u65f6\u957f\u5e76\u63d0\u524d\u63d0\u4ea4${early.examQueued ? "\uff0c\u5373\u5c06\u81ea\u52a8\u8003\u8bd5" : ""}`
);
}
if (early.waiting > 0 || !early.finished) {
pushUserLog(
`\u7ea6 ${formatEta(remain)} \u540e\u8bf7\u518d\u70b9\u300c\u5f00\u59cb\u300d\u6536\u5c3e\u4e0e\u8003\u8bd5\uff08\u5317\u4eac\u65f6\u95f4 ${formatBeijingDateTime(due)}\uff09\uff1b\u4e5f\u53ef\u70b9\u300c\u63d0\u524d\u6536\u5c3e\u68c0\u9a8c\u300d`
);
}
if (early.waiting > 0) {
pushUserLog(`\u53ef\u5173\u95ed\u672c\u7f51\u9875\uff1b\u5230\u65f6\u95f4\u540e\u91cd\u65b0\u6253\u5f00\u534e\u533b\u7f51\u518d\u70b9\u300c\u5f00\u59cb\u300d\uff0c\u6216\u968f\u65f6\u70b9\u300c\u63d0\u524d\u6536\u5c3e\u68c0\u9a8c\u300d`);
} else if (!early.finished) {
pushUserLog(`\u53ef\u5173\u95ed\u672c\u7f51\u9875\uff0c\u7a0b\u5e8f\u5df2\u505c\u6b62\uff1b\u5230\u65f6\u95f4\u540e\u91cd\u65b0\u6253\u5f00\u534e\u533b\u7f51\u518d\u70b9\u300c\u5f00\u59cb\u300d\u5373\u53ef`);
}
}
async function probePlayRecordFinish(ctx, job, label, sourceTag) {
const duration = (job && job.duration) || ctx.duration || 3600;
const fullCtx = await ensureCtxApiMeta(
Object.assign({}, ctx, { cwid: (job && job.cwid) || ctx.cwid })
);
const tag = sourceTag || "";
try {
if (HY_ENGINE_REQUIRED) {
if (!(await warmPlaySession(fullCtx, true))) {
return { ok: false, msg: "warm_fail" };
}
}
await sleep(500);
const rec = HY_ENGINE_REQUIRED
? await postPlayRecordViaEngine(fullCtx, duration)
: await postPlayRecord(fullCtx, duration);
const code = rec && rec.code;
pushLog(
`[${label}] ${tag}\u64ad\u5b8c\u68c0\u9a8c code=${code != null ? code : "?"}${rec && rec.msg ? " " + rec.msg : ""}`
);
if (code === -2) {
warnLoginIfNeeded(false);
return { ok: false, login: true };
}
if (code === 0) return { ok: true, rec };
return { ok: false, code, msg: (rec && rec.msg) || "" };
} catch (e) {
pushLog(`[${label}] ${tag}\u64ad\u5b8c\u68c0\u9a8c\u5f02\u5e38: ${e && e.message ? e.message : e}`);
return { ok: false, msg: e && e.message ? e.message : String(e) };
}
}
async function runOfflineEarlyFinishScan(options) {
const opts = options || {};
const sessionUid = getLearningUserId();
if (!sessionUid || !isOfflineMode()) {
return { scanned: 0, finished: 0, ready: 0, waiting: 0, examQueued: 0 };
}
await keepSessionAlive();
const now = Date.now();
let jobs = loadJobs();
const entries = sortOfflineClockJobs(
Object.entries(jobs).filter(function (entry) {
const j = entry[1];
if (!j || !jobBelongsToSession(j, sessionUid)) return false;
if (j.phase === "silent_clock") return true;
return j.phase === "silent_finish_retry" && Number(j.recordRetryDue || 0) <= now;
})
);
if (!entries.length) {
return { scanned: 0, finished: 0, ready: 0, waiting: 0, examQueued: 0 };
}
let finished = 0;
let ready = 0;
let examQueued = 0;
for (let i = 0; i < entries.length; i++) {
if (!opts.force && !getEnabled()) break;
const job = entries[i][1];
const ctx = jobToCtx(job);
const label = job.title || job.cwrid.slice(0, 8);
const tag = opts.quiet ? "[\u79bb\u7ebf·\u68c0\u9a8c] " : "[\u79bb\u7ebf·\u63d0\u524d] ";
const probe = await probePlayRecordFinish(ctx, job, label, tag);
if (probe.login) break;
if (probe.ok) {
ready++;
if (opts.autoFinish !== false) {
pushUserLog(`${label} · \u5e73\u53f0\u65f6\u957f\u5df2\u6ee1\u8db3\uff0c\u63d0\u524d\u63d0\u4ea4\u6536\u5c3e…`);
await finalizeVideoChapterDone(ctx, job, label, job.duration || ctx.duration, "[\u79bb\u7ebf·\u63d0\u524d]");
finished++;
const live = getJob(ctx);
if (live && live.phase === "exam_pending") examQueued++;
}
}
if (i < entries.length - 1) await sleep(800);
}
jobs = loadJobs();
const waiting = Object.values(jobs).filter(function (j) {
return j && jobBelongsToSession(j, sessionUid) && j.phase === "silent_clock";
}).length;
if (finished > 0) {
recalcOfflineBatchDue(jobs, sessionUid);
panelState.lastCourseListKey = "";
renderCourseList();
updatePanel();
}
return { scanned: entries.length, finished, ready, waiting, examQueued };
}
async function onOfflineEarlyFinishClick() {
if (bgRunning || engineRunning) {
pushUserLog("\u540e\u53f0\u4efb\u52a1\u8fd0\u884c\u4e2d\uff0c\u8bf7\u7a0d\u5019\u518d\u8bd5");
return;
}
if (!isOfflineMode() || !getOfflineBatchSnapshot()) {
pushUserLog("\u5f53\u524d\u4e0d\u5728\u79bb\u7ebf\u6279\u91cf\u7b49\u5f85\u9636\u6bb5");
return;
}
bgRunning = true;
updatePanel();
try {
pushUserLog("\u6b63\u5728\u68c0\u9a8c\u5404\u8282\u662f\u5426\u53ef\u63d0\u524d\u63d0\u4ea4\u64ad\u5b8c\u8bb0\u5f55…");
const res = await runOfflineEarlyFinishScan({ autoFinish: true, force: true, quiet: false });
if (res.finished > 0) {
pushUserLog(
`\u63d0\u524d\u6536\u5c3e ${res.finished} \u8282${res.examQueued ? "\uff0c\u6b63\u5728\u81ea\u52a8\u8003\u8bd5…" : ""}${res.waiting > 0 ? ` · \u5c1a\u6709 ${res.waiting} \u8282\u9700\u7b49\u5f85` : ""}`
);
bgRunning = false;
updateJobsPanel();
updatePanelCloudStatus();
if (!getEnabled()) {
setEnabled(true);
claimLeaderTab(true);
startLeaderHeartbeat();
startGlobalTicker();
}
await tryDrainPendingExamsAfterEarlyFinish();
syncRunModePanel();
maybeAutoStopWhenFinished();
} else if (res.scanned > 0) {
pushUserLog(`\u5df2\u68c0\u9a8c ${res.scanned} \u8282\uff0c\u5e73\u53f0\u6709\u6548\u65f6\u957f\u5c1a\u672a\u6ee1\u8db3\uff0c\u8bf7\u7a0d\u540e\u518d\u8bd5\u6216\u7b49\u5230\u70b9`);
} else {
pushUserLog("\u6ca1\u6709\u7b49\u5f85\u4e2d\u7684\u79bb\u7ebf\u4efb\u52a1\u53ef\u68c0\u9a8c");
}
} catch (e) {
pushUserLog("\u63d0\u524d\u6536\u5c3e\u68c0\u9a8c\u5931\u8d25\uff1a" + (e && e.message ? e.message : e));
} finally {
bgRunning = false;
updateJobsPanel();
updatePanel();
}
}
function tickOfflineBatchWaitUserLog() {
if (!isOfflineMode()) return;
const snap = getOfflineBatchSnapshot();
if (!snap || !snap.filledReady) return;
const now = Date.now();
const remain = Math.max(0, snap.due - now);
const dueText = snap.beijingDue || formatBeijingDateTime(snap.due);
const st = loadSilentState();
const coursePart = snap.totalCourses ? `${snap.totalCourses} \u8bfe` : "\u6279\u91cf\u4efb\u52a1";
if (snap.ready) {
const key = `ready:${snap.due}`;
if (st.readyUserLogKey === key) return;
patchSilentState({ readyUserLogKey: key });
pushUserLog(`\u5df2\u5230\u7b49\u5f85\u65f6\u95f4 · \u8bf7\u70b9\u300c\u5f00\u59cb\u300d\u68c0\u67e5\u8bfe\u7a0b\u5b8c\u6210\u5e76\u81ea\u52a8\u8003\u8bd5\uff08\u5317\u4eac\u65f6\u95f4 ${dueText}\uff09`);
return;
}
if (snap.partialReady) {
const key = `partial:${snap.partialReadyCount}:${Math.floor(now / 600000)}`;
if (st.partialReadyUserLogKey === key) return;
patchSilentState({ partialReadyUserLogKey: key, lastWaitUserLogAt: now });
pushUserLog(
`\u6709 ${snap.partialReadyCount} \u95e8\u8bfe\u5df2\u5230\u53ef\u6536\u5c3e\u65f6\u95f4 · \u53ef\u70b9\u300c\u5f00\u59cb\u300d\u5148\u5b8c\u6210\u5df2\u5230\u70b9\u8bfe\u7a0b\uff08\u5168\u90e8\u5230\u70b9\uff1a\u5317\u4eac\u65f6\u95f4 ${dueText}\uff09`
);
return;
}
const lastAt = Number(st.lastWaitUserLogAt || st.filledAt || 0);
if (now - lastAt < OFFLINE_WAIT_LOG_INTERVAL_MS) return;
const slot = Math.floor(remain / OFFLINE_WAIT_LOG_INTERVAL_MS);
const key = `wait:${snap.due}:${slot}`;
if (st.waitUserLogKey === key) return;
patchSilentState({ waitUserLogKey: key, lastWaitUserLogAt: now });
pushUserLog(
`\u79bb\u7ebf\u7b49\u5f85\u4e2d · ${coursePart}\u8fdb\u5ea6\u5df2\u63d0\u4ea4 · \u7ea6 ${formatEta(remain)} \u540e\u518d\u70b9\u300c\u5f00\u59cb\u300d\u68c0\u67e5\u6536\u5c3e\u4e0e\u8003\u8bd5\uff08\u5317\u4eac\u65f6\u95f4 ${dueText}\uff09· \u7a0b\u5e8f\u5df2\u505c\u6b62\uff0c\u53ef\u5173\u95ed\u7f51\u9875`
);
}
function clearOfflinePanelHint() {
const el = document.getElementById("hy-cme-start-hint");
if (el && /^\u79bb\u7ebf\u6279\u91cf\uff1a/.test(String(el.textContent || ""))) {
clearPanelHint();
}
}
function isLoginRelatedPage() {
const host = location.hostname.toLowerCase();
const path = (location.pathname + location.search).toLowerCase();
if (/^apiwxmsg\.|^hyuser\d*\./.test(host)) return true;
if (/\/secure\/login|login_sso|login_sq|wxuserlogin/i.test(path)) return true;
return false;
}
let hdRunnerBusy = false;
function isHdblHost() {
return /^hdbl\.91huayi\.com$/i.test(location.hostname);
}
function isCmeHost() {
const host = location.hostname.toLowerCase();
if (/^apiwxmsg\.|^hyuser\d*\./.test(host)) return false;
return /^cme[\w-]*\.91huayi\.com$/i.test(host);
}
function shouldRunScript() {
if (!location.host.includes("91huayi.com")) return false;
if (isHdblHost()) return isHdInteractiveEnabled();
if (!isCmeHost()) return false;
if (isLoginRelatedPage()) return false;
return true;
}
function pageType() {
const p = location.pathname.toLowerCase();
if (isHdblHost()) return "hdbl";
if (p.includes("exam_result_hd.aspx")) return "hd_result";
if (p.includes("/course_ware/course_ware_polyv.aspx")) return "play";
if (p.includes("/course_ware/course_ware_hd.aspx")) return "ware_hd";
if (p.includes("/course_ware/course_ware.aspx")) return "ware";
if (p.includes("/pages/course.aspx")) return "course";
if (p.includes("/personalcenter/course_collect.aspx")) return "collect";
if (p.includes("/pages/exam_code.aspx")) return "exam_code";
if (p.includes("/pages/face.aspx")) return "face";
if (p.includes("/pages/exam.aspx")) return "exam";
if (p.includes("/pages/exam_result.aspx")) return "exam_result";
return "other";
}
function shouldShowPanel() {
if (pageType() === "play") return false;
return isLeaderTab();
}
let tabIsLeader = false;
let leaderHeartbeatTimer = null;
let leaderRoleTimer = null;
let examFaceCancel = false;
let examFaceSkipAll = false;
let videoFaceCancel = false;
let hyFaceModalKind = "exam";
const hyVideoFaceVerified = new Set();
const hyVideoFacePending = new Set();
const hyVideoFacePrompting = new Set();
const hyVideoFaceVerifyPromises = new Map();
let skipExamsSession = false;
function getTabId() {
try {
let id = sessionStorage.getItem(TAB_ID_SESSION_KEY);
if (!id) {
id =
"tab_" +
Date.now().toString(36) +
"_" +
Math.random().toString(36).slice(2, 10);
sessionStorage.setItem(TAB_ID_SESSION_KEY, id);
}
return id;
} catch (_) {
return "tab_fb_" + Math.random().toString(36).slice(2, 10);
}
}
function loadLeaderRecord() {
try {
return JSON.parse(localStorage.getItem(LEADER_TAB_KEY) || "null");
} catch (_) {
return null;
}
}
function saveLeaderRecord(rec) {
try {
localStorage.setItem(LEADER_TAB_KEY, JSON.stringify(rec || {}));
} catch (_) {}
}
function clearLeaderRecord() {
try {
localStorage.removeItem(LEADER_TAB_KEY);
} catch (_) {}
}
function isLeaderStale(rec) {
if (!rec || !rec.tabId) return true;
return Date.now() - (Number(rec.ts) || 0) > LEADER_STALE_MS;
}
function hasActiveLeader() {
const rec = loadLeaderRecord();
return !!(rec && rec.tabId && !isLeaderStale(rec));
}
function isOurLeaderRecord(rec) {
return !!(rec && rec.tabId && rec.tabId === getTabId());
}
function formatLeaderPageHint(rec) {
if (!rec) return "\u53e6\u4e00\u6807\u7b7e\u9875";
if (rec.title) return `\u300c${String(rec.title).slice(0, 24)}\u300d`;
if (rec.path && /collect/i.test(rec.path)) return "\u9879\u76ee\u6536\u85cf\u9875";
if (rec.path && /course/i.test(rec.path)) return "\u8bfe\u7a0b\u9875";
return "\u53e6\u4e00\u6807\u7b7e\u9875";
}
function pulseLeaderHeartbeat() {
const rec = loadLeaderRecord();
if (!isOurLeaderRecord(rec)) return;
saveLeaderRecord({
tabId: getTabId(),
ts: Date.now(),
path: location.pathname,
title: String(document.title || "").slice(0, 48),
host: location.hostname,
});
}
function claimLeaderTab(force) {
const rec = loadLeaderRecord();
if (!force && hasActiveLeader() && !isOurLeaderRecord(rec)) {
tabIsLeader = false;
return false;
}
saveLeaderRecord({
tabId: getTabId(),
ts: Date.now(),
path: location.pathname,
title: String(document.title || "").slice(0, 48),
host: location.hostname,
});
tabIsLeader = true;
return true;
}
function releaseLeaderTab() {
const rec = loadLeaderRecord();
if (isOurLeaderRecord(rec)) clearLeaderRecord();
tabIsLeader = false;
stopLeaderHeartbeat();
}
function refreshLeaderRole() {
const rec = loadLeaderRecord();
if (!rec || !rec.tabId || isLeaderStale(rec)) {
tabIsLeader = false;
return false;
}
tabIsLeader = isOurLeaderRecord(rec);
return tabIsLeader;
}
function isLeaderTab() {
if (!tabIsLeader) refreshLeaderRole();
return tabIsLeader;
}
function stopLeaderHeartbeat() {
if (leaderHeartbeatTimer) {
clearInterval(leaderHeartbeatTimer);
leaderHeartbeatTimer = null;
}
}
function startLeaderHeartbeat() {
stopLeaderHeartbeat();
if (!tabIsLeader) return;
pulseLeaderHeartbeat();
leaderHeartbeatTimer = setInterval(() => {
if (!tabIsLeader) {
stopLeaderHeartbeat();
return;
}
pulseLeaderHeartbeat();
}, LEADER_HEARTBEAT_MS);
}
function stopGlobalTicker() {
if (window.__hyJobTicker) {
clearInterval(window.__hyJobTicker);
window.__hyJobTicker = null;
}
}
function stopPanelUpdateTimer() {
if (window.__hyPanelUpdateTimer) {
clearInterval(window.__hyPanelUpdateTimer);
window.__hyPanelUpdateTimer = null;
}
}
function removePanelDom() {
const panel = document.getElementById("hy-cme-auto-panel");
if (panel) panel.remove();
stopPreviewAutoRefresh();
stopPanelUpdateTimer();
}
function showFollowerBanner() {
if (isLeaderTab()) return;
const rec = loadLeaderRecord();
if (!getEnabled() && !hasActiveLeader()) return;
let el = document.getElementById("hy-cme-follower-banner");
if (!el) {
el = document.createElement("div");
el.id = "hy-cme-follower-banner";
el.style.cssText =
"position:fixed;right:16px;top:72px;z-index:2147483646;max-width:320px;padding:10px 12px;border-radius:10px;background:#eff6ff;border:1px solid #93c5fd;color:#1e3a8a;font:700 12px/1.45 -apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;box-shadow:0 8px 24px rgba(30,64,175,.12);";
document.body.appendChild(el);
}
const hint = formatLeaderPageHint(rec);
const running = getEnabled() ? "\u8fd0\u884c\u4e2d" : "\u5df2\u6253\u5f00\u9762\u677f";
el.textContent = `\u52a9\u624b\u5728 ${hint} ${running}\uff0c\u8bf7\u5207\u56de\u8be5\u6807\u7b7e\u67e5\u770b\u9762\u677f\u4e0e\u65e5\u5fd7`;
el.style.display = "block";
}
function removeFollowerBanner() {
const el = document.getElementById("hy-cme-follower-banner");
if (el) el.remove();
}
function syncFollowerBanner() {
if (isLeaderTab()) removeFollowerBanner();
else showFollowerBanner();
}
function onGainedLeadership() {
removeFollowerBanner();
startLeaderHeartbeat();
if (!document.getElementById("hy-cme-auto-panel")) buildPanel();
else {
renderRunLog();
updatePanel();
syncRunModePanel();
}
if (getEnabled()) {
startPreviewAutoRefresh();
startGlobalTicker();
}
}
function onLostLeadership() {
stopLeaderHeartbeat();
stopGlobalTicker();
removePanelDom();
syncFollowerBanner();
}
function prepareLeaderOnBoot() {
refreshLeaderRole();
if (!hasActiveLeader()) {
claimLeaderTab(false);
} else if (getEnabled() && isLeaderStale(loadLeaderRecord())) {
claimLeaderTab(true);
}
if (tabIsLeader) startLeaderHeartbeat();
}
function installLeaderTabSystem() {
const onLeaderStorage = () => {
const was = tabIsLeader;
refreshLeaderRole();
if (!was && tabIsLeader) onGainedLeadership();
else if (was && !tabIsLeader) onLostLeadership();
else syncFollowerBanner();
if (tabIsLeader && getEnabled() && !leaderHeartbeatTimer) startLeaderHeartbeat();
if (tabIsLeader && getEnabled() && !window.__hyJobTicker) startGlobalTicker();
};
window.addEventListener("storage", (e) => {
if (e.key === LEADER_TAB_KEY) onLeaderStorage();
});
window.addEventListener("beforeunload", () => releaseLeaderTab());
window.addEventListener("pagehide", () => releaseLeaderTab());
if (leaderRoleTimer) clearInterval(leaderRoleTimer);
leaderRoleTimer = setInterval(() => {
const was = tabIsLeader;
const rec = loadLeaderRecord();
if (getEnabled() && (!rec || isLeaderStale(rec))) {
if (claimLeaderTab(true)) {
if (!was) onGainedLeadership();
else startLeaderHeartbeat();
return;
}
}
refreshLeaderRole();
if (was && !tabIsLeader) onLostLeadership();
else if (!was && tabIsLeader) onGainedLeadership();
else syncFollowerBanner();
}, LEADER_HEARTBEAT_MS);
}
function shouldBlockNativeProgress() {
return getEnabled() && getApiMode();
}
const PROGRESS_URL_RE = /addCourseWareProcess\.ashx|addCourseWarePlayRecord\.ashx/i;
function fakeBlockedXhrResponse(xhr) {
try {
Object.defineProperty(xhr, "readyState", { configurable: true, get: () => 4 });
Object.defineProperty(xhr, "status", { configurable: true, get: () => 200 });
Object.defineProperty(xhr, "responseText", { configurable: true, get: () => '{"code":0}' });
if (typeof xhr.onreadystatechange === "function") xhr.onreadystatechange();
xhr.dispatchEvent(new Event("load"));
} catch (_) {}
}
function installNetworkHooks() {
if (window.__hyCmeNetHooked) return;
window.__hyCmeNetHooked = true;
const blockNative = (xhr) => {
if (!shouldBlockNativeProgress()) return false;
if (xhr && xhr.__hyEngineMark) return false;
const url = (xhr && xhr.__hyUrl) || "";
return PROGRESS_URL_RE.test(String(url));
};
try {
const origFetch = window.fetch;
if (typeof origFetch === "function") {
window.fetch = function (input, init) {
const url = typeof input === "string" ? input : input && input.url;
if (shouldBlockNativeProgress() && PROGRESS_URL_RE.test(String(url || ""))) {
if (!(init && init.__hyEngineMark)) {
return Promise.resolve(
new Response('{"code":0}', { status: 200, headers: { "Content-Type": "application/json" } })
);
}
}
const p = origFetch.apply(this, arguments);
if (!/course_ware_polyv\.aspx/i.test(String(url || ""))) return p;
return p.then(function (response) {
try {
if (!getEnabled()) return response;
const engineMark = !!(init && init.__hyEngineMark);
if (response.status >= 300 && response.status < 400) {
const loc = response.headers.get("Location") || response.headers.get("location") || "";
const next = resolveCmeAbsoluteUrl(loc, String(url || ""));
if (isHyVideoFaceGateUrl(next)) {
const id = extractHyFaceCwrid(next) || extractHyFaceCwrid(url);
if (id) {
markHyVideoFacePending(id);
if (!engineMark) {
scheduleHyVideoFacePrompt(
findJobCtxByCwid(id) || findPanelChapterCtxByCwid(id) || { cwrid: id, cwid: id }
);
}
}
}
return response;
}
return response;
} catch (_) {}
return response;
});
};
}
} catch (_) {}
const proto = XMLHttpRequest.prototype;
const origOpen = proto.open;
const origSend = proto.send;
const patchedOpen = function (...args) {
this.__hyUrl = args[1] != null ? String(args[1]) : "";
return origOpen.apply(this, args);
};
const patchedSend = function (body) {
if (blockNative(this)) {
setTimeout(() => fakeBlockedXhrResponse(this), 0);
return;
}
return origSend.call(this, body);
};
let xhrHooked = false;
try {
proto.open = patchedOpen;
proto.send = patchedSend;
xhrHooked = true;
} catch (_) {}
if (!xhrHooked) {
try {
Object.defineProperty(proto, "open", {
configurable: true,
writable: true,
value: patchedOpen,
});
Object.defineProperty(proto, "send", {
configurable: true,
writable: true,
value: patchedSend,
});
xhrHooked = true;
} catch (_) {}
}
if (!xhrHooked) {
try {
const NativeXHR = window.XMLHttpRequest;
if (NativeXHR && !NativeXHR.__hyWrapped) {
function WrappedXHR() {
const xhr = new NativeXHR();
const bindOpen = Function.prototype.call.bind(origOpen, xhr);
const bindSend = Function.prototype.call.bind(origSend, xhr);
xhr.open = function (...args) {
xhr.__hyUrl = args[1] != null ? String(args[1]) : "";
return bindOpen(...args);
};
xhr.send = function (body) {
if (blockNative(xhr)) {
setTimeout(() => fakeBlockedXhrResponse(xhr), 0);
return;
}
return bindSend(body);
};
return xhr;
}
WrappedXHR.prototype = NativeXHR.prototype;
WrappedXHR.__hyWrapped = true;
window.XMLHttpRequest = WrappedXHR;
}
} catch (_) {}
}
try {
window.blockAbnormalPlugin = function () {};
} catch (_) {}
}
if (shouldRunScript()) installNetworkHooks();
function setHdMetaCookie(meta) {
try {
const val = encodeURIComponent(JSON.stringify(meta || {}));
document.cookie = `${HD_META_COOKIE}=${val}; path=/; max-age=3600; SameSite=Lax`;
if (location.hostname.includes("91huayi.com")) {
document.cookie = `${HD_META_COOKIE}=${val}; path=/; domain=.91huayi.com; max-age=3600; SameSite=Lax`;
}
} catch (_) {}
}
function readHdMetaCookie() {
try {
const m = document.cookie.match(new RegExp(`(?:^|;\\s*)${HD_META_COOKIE}=([^;]*)`));
if (!m) return null;
return JSON.parse(decodeURIComponent(m[1]));
} catch (_) {
return null;
}
}
function clearHdMetaCookie() {
document.cookie = `${HD_META_COOKIE}=; path=/; max-age=0`;
if (location.hostname.includes("91huayi.com")) {
document.cookie = `${HD_META_COOKIE}=; path=/; domain=.91huayi.com; max-age=0`;
}
}
function saveHdLaunchSnapshot(params, launchUrl) {
if (!params || !params.courseWareId || !params.userId) return;
try {
localStorage.setItem(
HD_LAUNCH_KEY,
JSON.stringify({ params, launchUrl: launchUrl || location.href, capturedAt: Date.now() })
);
} catch (_) {}
}
function loadHdLaunchSnapshot() {
try {
return JSON.parse(localStorage.getItem(HD_LAUNCH_KEY) || "null");
} catch (_) {
return null;
}
}
function clearHdLaunchSnapshot() {
localStorage.removeItem(HD_LAUNCH_KEY);
}
function captureHdLaunchParamsEarly() {
try {
const href = location.href.split("#")[0];
const params = parseHdLaunchParams(href);
if (params.courseWareId && params.userId && params.hash) {
saveHdLaunchSnapshot(params, href);
}
} catch (_) {}
}
function resolveHdLaunchBundle() {
captureHdLaunchParamsEarly();
const meta = readHdMetaCookie();
const snap = loadHdLaunchSnapshot();
const liveHref = location.href.split("#")[0];
const live = parseHdLaunchParams(liveHref);
if (
snap &&
meta &&
snap.params &&
snap.params.businessCustomParams &&
meta.cwid &&
snap.params.businessCustomParams !== meta.cwid
) {
clearHdLaunchSnapshot();
}
if (live.hash && live.courseWareId && live.userId) {
return { params: live, launchReferer: liveHref };
}
if (snap && snap.params && snap.params.hash && snap.params.courseWareId && snap.params.userId) {
return { params: snap.params, launchReferer: snap.launchUrl || liveHref };
}
if (snap && snap.launchUrl) {
const fromSnap = parseHdLaunchParams(snap.launchUrl);
if (fromSnap.courseWareId && fromSnap.userId && fromSnap.hash) {
return { params: fromSnap, launchReferer: snap.launchUrl };
}
}
return { params: live, launchReferer: liveHref };
}
function resolveHdLaunchParams() {
return resolveHdLaunchBundle().params;
}
function isHdLaunchReady() {
const { params } = resolveHdLaunchBundle();
return !!(params.courseWareId && params.userId && params.hash);
}
function isHdDetachedRunner() {
if (window.parent !== window) return true;
try {
return !!(window.opener && !window.opener.closed);
} catch (_) {
return false;
}
}
function closeHdPopup() {
if (hdPopupWindow && !hdPopupWindow.closed) {
try {
hdPopupWindow.close();
} catch (_) {}
}
hdPopupWindow = null;
}
function clearHdHostSurface() {
removeHdIframe();
closeHdPopup();
}
function removeHdIframe() {
const frame = document.getElementById(HD_IFRAME_ID);
if (frame) frame.remove();
}
function launchHdInSilentFrame(url) {
let frame = document.getElementById(HD_IFRAME_ID);
if (!frame) {
frame = document.createElement("iframe");
frame.id = HD_IFRAME_ID;
frame.name = HD_IFRAME_ID;
frame.style.cssText =
"position:fixed;left:-9999px;top:-9999px;width:1px;height:1px;border:0;opacity:0;pointer-events:none";
frame.setAttribute("aria-hidden", "true");
document.documentElement.appendChild(frame);
}
frame.src = url;
}
function resetHdRunningJob(cwid) {
const jobs = loadJobs();
const key = Object.keys(jobs).find(
(k) =>
jobs[k] &&
jobs[k].interactive &&
jobs[k].phase === "hd_running" &&
(jobs[k].cwid === cwid || !cwid)
);
if (!key) return;
saveJob(jobToCtx(jobs[key]), { phase: "pending" });
updateJobsPanel();
}
function clearHdIframeWatchdog() {
if (hdIframeWatchdogTimer) {
clearTimeout(hdIframeWatchdogTimer);
hdIframeWatchdogTimer = null;
}
}
function launchHdPopupWindow(url, job) {
clearHdIframeWatchdog();
clearHdHostSurface();
const title = job.title || job.cwrid || job.cwid || "";
const w = HD_POPUP_W;
const h = HD_POPUP_H;
const features = [
`width=${w}`,
`height=${h}`,
`left=${Math.max(0, Math.round((screen.availWidth - w) / 2))}`,
`top=${Math.max(0, Math.round((screen.availHeight - h) / 2))}`,
"menubar=no",
"toolbar=no",
"status=no",
"resizable=yes",
"scrollbars=yes",
].join(",");
hdPopupWindow = window.open(url, HD_POPUP_NAME, features);
if (!hdPopupWindow) {
pushLog(`[${title}] [\u4e92\u52a8] \u5f39\u7a97\u88ab\u6d4f\u89c8\u5668\u62e6\u622a\uff0c\u6539\u4e3a\u5f53\u524d\u9875\u6253\u5f00…`);
location.href = url;
return false;
}
try {
hdPopupWindow.focus();
} catch (_) {}
pushLog(`[${title}] [\u4e92\u52a8] \u5df2\u6253\u5f00\u8ff7\u4f60\u7a97\u53e3\uff08\u4e3b\u9875\u9762\u4fdd\u7559\uff0c\u5b8c\u6210\u540e\u81ea\u52a8\u5173\u95ed\uff09…`);
return true;
}
function armHdHostWatchdog(job, launchUrl, mode) {
clearHdIframeWatchdog();
const started = Date.now();
const cwid = job.cwid;
const title = job.title || "";
const hostMode = mode || "popup";
hdIframeWatchdogTimer = setTimeout(() => {
hdIframeWatchdogTimer = null;
const jobs = loadJobs();
const j = Object.values(jobs).find(
(x) => x && x.interactive && x.phase === "hd_running" && x.cwid === cwid
);
if (!j) return;
const progressAt = j.hdProgressAt || 0;
const launchedAt = j.hdLaunchedAt || started;
if (progressAt >= launchedAt) return;
if (hostMode === "iframe") {
pushLog(
`[${title}] [\u4e92\u52a8] iframe ${HD_IFRAME_WATCHDOG_MS / 1000}s \u65e0\u54cd\u5e94\uff0c\u6539\u5f00\u8ff7\u4f60\u7a97\u53e3…`
);
launchHdPopupWindow(launchUrl, job);
armHdHostWatchdog(job, launchUrl, "popup");
return;
}
pushLog(
`[${title}] [\u4e92\u52a8] \u8ff7\u4f60\u7a97\u53e3 ${HD_IFRAME_WATCHDOG_MS / 1000}s \u65e0\u54cd\u5e94\uff08\u8bf7\u786e\u8ba4 Tampermonkey \u5df2\u6ce8\u5165 hdbl \u9875\u9762\uff09`
);
if (hdPopupWindow && !hdPopupWindow.closed) {
try {
hdPopupWindow.focus();
} catch (_) {}
}
}, HD_IFRAME_WATCHDOG_MS);
}
function installHdIframeBridge() {
if (window.__hyHdIframeBridge) return;
window.__hyHdIframeBridge = true;
window.addEventListener("message", (e) => {
const d = e.data;
if (!d || d.type !== HD_MSG_TYPE) return;
clearHdIframeWatchdog();
if (d.event === "ping") {
const where = [d.host, d.path].filter(Boolean).join("");
pushLog(`[\u4e92\u52a8] iframe \u811a\u672c\u5df2\u6ce8\u5165 · ${where || d.stage || "ok"}`);
const jobs = loadJobs();
const key = Object.keys(jobs).find(
(k) => jobs[k] && jobs[k].interactive && jobs[k].phase === "hd_running"
);
if (key) {
saveJob(jobToCtx(jobs[key]), { hdProgressAt: Date.now(), hdProgressText: "iframe \u5df2\u8fde\u63a5" });
updateJobsPanel();
}
} else if (d.event === "log" && d.text) {
pushLog(d.text);
if (d.cwid || d.title) {
const jobs = loadJobs();
const key = Object.keys(jobs).find(
(k) =>
jobs[k] &&
jobs[k].interactive &&
jobs[k].phase === "hd_running" &&
(d.cwid ? jobs[k].cwid === d.cwid : d.title && jobs[k].title === d.title)
);
if (key) {
saveJob(jobToCtx(jobs[key]), { hdLastLog: d.text, hdProgressAt: Date.now() });
updateJobsPanel();
updatePanel();
}
}
} else if (d.event === "progress") {
const jobs = loadJobs();
const key = Object.keys(jobs).find((k) => jobs[k] && jobs[k].interactive && jobs[k].phase === "hd_running");
if (key) {
saveJob(jobToCtx(jobs[key]), {
hdStep: d.step,
hdStepTotal: d.total,
hdProgressText: d.detail || "",
hdLastLog: d.detail || "",
hdProgressAt: Date.now(),
});
updateJobsPanel();
updatePanel();
}
} else if (d.event === "done") {
clearHdIframeWatchdog();
if (d.cwid) completeInteractiveJobByCwid(d.cwid);
clearHdHostSurface();
clearHdMetaCookie();
pushLog("[\u4e92\u52a8] \u540e\u53f0\u4e92\u52a8\u8bfe\u5b8c\u6210");
runBgScheduler();
} else if (d.event === "fail") {
clearHdIframeWatchdog();
clearHdHostSurface();
clearHdMetaCookie();
resetHdRunningJob(d.cwid);
pushLog("[\u4e92\u52a8] \u540e\u53f0\u5931\u8d25: " + (d.message || "\u672a\u77e5\u9519\u8bef"));
runBgScheduler();
}
});
}
function postHdIframeEvent(event, payload) {
if (!isHdDetachedRunner()) return;
const meta = readHdMetaCookie();
const msg = Object.assign({ type: HD_MSG_TYPE, event }, payload || {}, {
cwid: (payload && payload.cwid) || (meta && meta.cwid) || "",
title: (payload && payload.title) || (meta && meta.title) || "",
});
try {
if (window.parent !== window) window.parent.postMessage(msg, "*");
} catch (_) {}
try {
if (window.opener && !window.opener.closed) window.opener.postMessage(msg, "*");
} catch (_) {}
}
function tryCloseHdRunnerWindow() {
try {
if (window.opener && !window.opener.closed) window.close();
} catch (_) {}
}
function postHdProgress(sess, detail, step, total) {
postHdIframeEvent("progress", {
detail,
step,
total,
cwid: sess.businessCustomParams,
title: sess.label,
});
}
function readPageVar(name) {
const html = document.documentElement.innerHTML;
const m = html.match(new RegExp("var\\s+" + name + "\\s*=\\s*['\"]([^'\"]+)['\"]", "i"));
return m ? m[1] : "";
}
function readGroupId() {
const m = document.documentElement.innerHTML.match(/group_id:\s*['"]([^'"]+)['"]/i);
return m ? m[1] : "";
}
function readProvinceId() {
const m = document.documentElement.innerHTML.match(/province_id:\s*['"]([^'"]+)['"]/i);
return m ? m[1] : "";
}
function readWareMeta(uid, cwrid, coaid) {
const row = document.querySelector(
`.cw-progress-row[data-cwrid="${cwrid}"][data-coaid="${coaid}"]`
);
let maxtime = 0;
let totaltime = 0;
if (row) {
maxtime = parseInt(row.getAttribute("data-maxtime") || "0", 10) || 0;
totaltime = parseInt(row.getAttribute("data-totaltime") || "0", 10) || 0;
}
const lsKey = uid + cwrid + coaid;
const local = parseInt(localStorage.getItem(lsKey) || "0", 10) || 0;
let base = Math.max(maxtime, local);
if (totaltime > 0 && base >= totaltime - 15) base = 0;
return { maxtime: base, totaltime, lsKey };
}
function resolveStartSec(stored, duration) {
const d = Math.max(60, duration || 3600);
let s = Math.max(0, Math.floor(stored || 0));
if (s >= d - 15) return 1;
if (s <= 0) return 1;
return Math.min(s, d - 1);
}
function resolveRtEntrySec(stored, duration) {
const d = Math.max(60, duration || 3600);
let s = Math.max(0, Math.floor(stored || 0));
if (s <= 0) return 1;
if (s >= d - 2) {
const blocks = Math.floor((d - 1) / RT_STEP_SEC);
return Math.max(1, Math.min(blocks * RT_STEP_SEC, d - 1));
}
const floored = Math.floor(s / RT_STEP_SEC) * RT_STEP_SEC;
return Math.max(1, Math.min(floored || 1, d - 1));
}
function readChapterStudiedSec(uid, cwrid, coaid, duration) {
const meta = readWareMeta(uid, cwrid, coaid);
const ls = parseInt(localStorage.getItem(meta.lsKey || uid + cwrid + coaid) || "0", 10) || 0;
return Math.max(0, meta.maxtime, ls);
}
function resolveChapterRtStart(ch) {
const studied = readChapterStudiedSec(ch.uid, ch.cwrid, ch.coaid, ch.duration);
const duration = Math.max(60, ch.duration || 3600);
return {
studiedSec: studied,
startSec: resolveRtEntrySec(studied, duration),
};
}
function formatPlayProgressPct(sec, total) {
const t = Math.max(1, total || 1);
return Math.min(100, Math.max(0, Math.round((Math.max(0, sec || 0) / t) * 100)));
}
function formatProgressLabelFromSec(sec, duration) {
const pct = formatPlayProgressPct(sec, duration);
if (pct >= 100) return "\u89c6\u9891\u5df2\u5b66\u5b8c";
return `\u64ad\u653e\u81f3\uff1a${pct}%`;
}
function readCourseId() {
const q = new URLSearchParams(location.search).get("cid");
if (q) return q;
const m = document.documentElement.innerHTML.match(/courseId:\s*["']([^"']+)["']/i);
return m ? m[1] : "";
}
function getCachedApiMeta(cid) {
try {
const all = JSON.parse(localStorage.getItem(META_KEY) || "{}");
return all[cid] || all._default || null;
} catch (_) {
return null;
}
}
function setCachedApiMeta(cid, meta) {
try {
const all = JSON.parse(localStorage.getItem(META_KEY) || "{}");
all[cid] = meta;
all._default = meta;
localStorage.setItem(META_KEY, JSON.stringify(all));
} catch (_) {}
}
async function ensureCourseApiMeta(cid, sampleCwrid) {
const cached = getCachedApiMeta(cid);
if (cached && cached.groupId && cached.provinceId) return cached;
pushLog("\u62c9\u53d6 group_id…");
const url =
"/course_ware/course_ware_polyv.aspx?cwid=" +
encodeURIComponent(sampleCwrid) +
"&ff=0&ft=0&t=0";
const page = await fetchCmeHtmlSafe(url);
if (page.surveyRedirect || isCmeSurveyRedirectUrl(page.url || "")) {
pushLog("\u62c9\u53d6 group_id\uff1a\u5e73\u53f0\u8df3\u8f6c\u95ee\u5377\u9875\uff0c\u5df2\u505c\u6b62\u81ea\u52a8\u5b66\u4e60");
if (getEnabled()) {
handleSurveyGateManual({ title: "\u8bfe\u4ef6", cwrid: sampleCwrid, cwid: sampleCwrid }, page.url || "");
}
return cached || { groupId: "", provinceId: "" };
}
const html = page.text || "";
const meta = {
groupId: (html.match(/group_id:\s*['"]([^'"]+)['"]/i) || [])[1] || "",
provinceId: (html.match(/province_id:\s*['"]([^'"]+)['"]/i) || [])[1] || "",
};
if (meta.groupId) setCachedApiMeta(cid, meta);
return meta;
}
function readVarFromHtml(html, name) {
const m = html.match(new RegExp("var\\s+" + name + "\\s*=\\s*['\"]([^'\"]+)['\"]", "i"));
return m ? m[1] : "";
}
function getChapterMetaEl(el) {
if (!el) return null;
return el.querySelector(".play_process") || el.querySelector(".cw-progress-row");
}
function chapterHasInteractiveTag(el) {
if (!el) return false;
return Array.from(el.querySelectorAll(".cw-tag-list span, h3 label")).some((node) =>
/\u4e92\u52a8/.test((node.textContent || "").replace(/\s+/g, ""))
);
}
function isInteractiveChapter(el) {
return isHdInteractiveEnabled() && chapterHasInteractiveTag(el);
}
function purgeInteractiveJobs() {
if (isHdInteractiveEnabled()) return;
const jobs = loadJobs();
const keys = Object.keys(jobs).filter((k) => jobs[k] && jobs[k].interactive);
if (!keys.length) return;
keys.forEach((k) => delete jobs[k]);
saveJobs(jobs);
pushLog("[\u4e92\u52a8] \u4e92\u52a8\u8bfe\u529f\u80fd\u5df2\u5173\u95ed\uff0c\u5df2\u6e05\u7406\u961f\u5217\u4e2d\u7684\u4e92\u52a8\u8bfe\u4efb\u52a1");
}
function findChapterElementByCwid(cwid) {
if (!cwid) return null;
const target = String(cwid).toUpperCase();
for (const el of document.querySelectorAll(".course[data-href]")) {
const href = el.getAttribute("data-href") || "";
const m = href.match(/cwid=([^&]+)/i);
if (m && String(m[1]).toUpperCase() === target) return el;
}
return null;
}
function readChapterBriefFromElement(el) {
if (!el) return null;
const meta = readChapterMeta(el);
if (!meta) return null;
const href = el.getAttribute("data-href") || "";
const cwidM = href.match(/cwid=([^&]+)/i);
const titleEl =
el.querySelector(".cw-title-link strong") ||
el.querySelector("h3 strong") ||
el.querySelector("h3 a strong") ||
el.querySelector("h3 a");
const title = titleEl ? titleEl.textContent.replace(/\s+/g, " ").trim().slice(0, 24) : "";
return {
cwrid: meta.cwrid,
coaid: meta.coaid,
cwid: cwidM ? cwidM[1] : "",
interactive: meta.interactive,
title,
};
}
function findAnyChapterOnPageByCwid(cwid) {
const el = findChapterElementByCwid(cwid);
if (!el) return null;
const brief = readChapterBriefFromElement(el);
if (!brief || !brief.cwid) return null;
const uid = window.uid || readPageVar("uid");
const cid = readCourseId();
return Object.assign({ uid, cid, el }, brief);
}
function ensureInteractiveRunReady() {
if (!getEnabled()) {
setEnabled(true);
pushLog("\u5df2\u81ea\u52a8\u5f00\u542f\uff08\u4e92\u52a8\u8bfe\u6d4b\u8bd5\uff09");
}
ensurePanelDefaults();
}
function rememberInteractiveChapterLaunch(ch) {
if (!ch || !ch.cwid) return;
const ctx = {
uid: ch.uid,
cwrid: ch.cwrid,
coaid: ch.coaid,
cid: ch.cid,
lsKey: ch.uid && ch.cwrid && ch.coaid ? ch.uid + ch.cwrid + ch.coaid : "",
title: ch.title,
};
setHdMetaCookie({
title: ch.title || ch.cwid.slice(0, 8),
cmeOrigin: location.origin,
jobKey: ctx.lsKey ? jobKey(ctx) : "",
cwid: ch.cwid,
});
saveHdActive({
jobKey: ctx.lsKey ? jobKey(ctx) : "",
cwid: ch.cwid,
title: ch.title || ch.cwid.slice(0, 8),
cmeOrigin: location.origin,
launchedAt: Date.now(),
manual: true,
});
}
function installInteractiveChapterClickHook() {
if (!isHdInteractiveEnabled()) return;
if (window.__hyInteractiveClickHook) return;
window.__hyInteractiveClickHook = true;
document.addEventListener(
"click",
(e) => {
if (pageType() !== "course") return;
const courseEl = e.target.closest(".course[data-href]");
if (!courseEl || !isInteractiveChapter(courseEl)) return;
const ch = readChapterBriefFromElement(courseEl);
if (!ch || !ch.cwid) return;
const uid = window.uid || readPageVar("uid");
const cid = readCourseId();
rememberInteractiveChapterLaunch(Object.assign({ uid, cid }, ch));
ensureInteractiveRunReady();
pushLog(`[\u4e92\u52a8] \u6253\u5f00\u300c${ch.title || ch.cwid.slice(0, 8)}\u300d· \u8df3\u8f6c\u540e\u81ea\u52a8\u8dd1 API…`);
},
true
);
}
async function runWareHdPage() {
if (!isHdInteractiveEnabled()) return;
const cwid = new URLSearchParams(location.search).get("cwid");
if (!cwid) {
pushLog("[\u4e92\u52a8] \u5165\u53e3\u9875\u7f3a\u5c11 cwid");
return;
}
ensureInteractiveRunReady();
if (isHdLaunchReady()) {
pushLog("[\u4e92\u52a8] \u5df2\u643a\u5e26\u542f\u52a8\u53c2\u6570\uff0c\u5f00\u59cb API…");
scheduleHdInteractiveBoot();
return;
}
let meta = readHdMetaCookie();
if (!meta || String(meta.cwid || "").toUpperCase() !== String(cwid).toUpperCase()) {
const onPage = findAnyChapterOnPageByCwid(cwid);
rememberInteractiveChapterLaunch(
onPage || { cwid, title: cwid.slice(0, 8), uid: "", cid: "", cwrid: "", coaid: "" }
);
}
pushLog(`[\u4e92\u52a8] \u89e3\u6790\u5165\u53e3 cwid=${cwid.slice(0, 8)}…`);
const probe = await probeHdChapterEntry(cwid);
if (probe.status === "done") {
pushLog("[\u4e92\u52a8] \u8be5\u4e92\u52a8\u8bfe\u5df2\u5b8c\u6210");
location.replace(
`${location.origin}/pages/exam_result_hd.aspx?businessCustomParams=${encodeURIComponent(cwid)}`
);
return;
}
const launchUrl =
probe.status === "hdbl" && probe.url
? probe.url
: probe.status === "notice" && probe.url
? probe.url.startsWith("http")
? probe.url
: new URL(probe.url, location.origin).href
: buildHdCmeEntryUrl(cwid);
if (probe.status === "hdbl" && probe.url) {
const p = parseHdLaunchParams(probe.url);
if (p.courseWareId && p.userId && p.hash) {
saveHdLaunchSnapshot(p, probe.url);
}
}
pushLog("[\u4e92\u52a8] \u8df3\u8f6c\u4e92\u52a8\u8bfe\u9875\u9762…");
location.replace(launchUrl);
}
function scheduleHdInteractiveBoot() {
if (!isHdInteractiveEnabled()) return;
if (window.__hyHdBootTimer) return;
let tries = 0;
const tick = () => {
window.__hyHdBootTimer = null;
captureHdLaunchParamsEarly();
if (isHdLaunchReady()) {
if (!getEnabled()) setEnabled(true);
ensurePanelDefaults();
if (!hdRunnerBusy) {
const delay = tries === 0 ? 1200 : 400;
setTimeout(() => runInteractiveHdFlow(), delay);
}
return;
}
tries++;
if (tries < 24) {
window.__hyHdBootTimer = setTimeout(tick, 500);
return;
}
pushLog("[\u4e92\u52a8] \u672a\u68c0\u6d4b\u5230\u542f\u52a8\u53c2\u6570\uff08hash/courseWareId\uff09\uff0c\u8bf7\u4ece\u8bfe\u7a0b\u9875\u91cd\u65b0\u70b9\u51fb\u8fdb\u5165");
};
tick();
}
function readChapterMeta(el) {
const ps = getChapterMetaEl(el);
if (!ps) return null;
const cwrid = ps.getAttribute("data-cwrid") || "";
const coaid = ps.getAttribute("data-coaid") || "";
const maxtime = parseInt(ps.getAttribute("data-maxtime") || "0", 10) || 0;
const totaltime = parseInt(ps.getAttribute("data-totaltime") || "0", 10) || 0;
const interactive = isInteractiveChapter(el);
if (!cwrid || !coaid) return null;
if (totaltime <= 0 && !interactive) return null;
return { cwrid, coaid, maxtime, totaltime, interactive, metaEl: ps };
}
function getChapterStatusParts(el) {
const metaEl = getChapterMetaEl(el);
const studyState = (metaEl?.getAttribute("data-studystate") || "").replace(/\s+/g, " ").trim();
let progressText = (el.querySelector(".cw-progress-text")?.textContent || "").replace(/\s+/g, " ").trim();
if (!progressText && metaEl?.classList?.contains("play_process")) {
progressText = (metaEl.textContent || "").replace(/\s+/g, " ").trim();
}
const pctM = progressText.match(/^(\d+)%$/);
if (pctM && pctM[1] !== "100") progressText = `\u64ad\u653e\u81f3\uff1a${pctM[1]}%`;
const btn = el.querySelector(".cw-status button");
const btnLabel = btn ? (btn.textContent || "").replace(/\s+/g, " ").trim() : "";
let label = btnLabel || studyState;
if (!label) {
const statusBox = el.querySelector(".cw-status");
if (statusBox) label = (statusBox.textContent || "").replace(/\s+/g, " ").trim();
}
if (!label) {
const h3 = el.querySelector("h3");
if (h3) {
for (const sp of h3.querySelectorAll("span[style]")) {
if (sp.classList.contains("play_process")) continue;
const t = (sp.textContent || "").replace(/\s+/g, " ").trim();
if (t) {
label = t;
break;
}
}
}
}
return { label, progressText, studyStateAttr: metaEl?.getAttribute("data-studystate") || "" };
}
function inferHyChapterStatuses(labelText, progressText, studyStateAttr, interactive) {
const label = String(labelText || "").replace(/\s+/g, " ").trim();
const progress = String(progressText || "").replace(/\s+/g, " ").trim();
const studyStateNum = parseInt(studyStateAttr || "", 10);
const studyDone = /\u5df2\u5b8c\u6210|\u5b66\u4e60\u5b8c\u6bd5|\u5df2\u5b66\u5b8c/.test(label) || studyStateNum >= 2;
if (interactive) {
const studying = /\u5b66\u4e60\u4e2d/.test(label);
const waitStudy = /\u5f85\u5b66\u4e60|\u672a\u5b66\u4e60/.test(label);
const partialPct = /\u64ad\u653e\u81f3\uff1a\d+%/.test(progress);
const blank = !label;
const untouched =
!studyDone &&
!studying &&
(blank || waitStudy || partialPct || /\u64ad\u653e\u81f3\uff1a/.test(progress) || /\u5f85\u5b66\u4e60/.test(label));
return {
videoDone: studyDone,
examDone: studyDone,
untouched,
examStatus: "none",
pendingExam: false,
progress,
};
}
const examPassedLabel = /\u8003\u8bd5\u901a\u8fc7|\u5df2\u901a\u8fc7|\u5df2\u7ecf\u901a\u8fc7|\u5408\u683c/.test(label);
const pendingExam = /\u5f85\u8003\u8bd5|\u5f85\u8003/.test(label) && !examPassedLabel;
const chapterComplete =
/\u5df2\u5b8c\u6210/.test(label) && !pendingExam && !/\u5f85\u5b66\u4e60|\u672a\u5b66\u4e60|\u5b66\u4e60\u4e2d/.test(label);
const examDone = examPassedLabel || chapterComplete;
const videoDone = pendingExam || examDone || studyDone;
const partialPct = /\u64ad\u653e\u81f3\uff1a\d+%/.test(progress);
const waitStudy = /\u5f85\u5b66\u4e60|\u672a\u5b66\u4e60/.test(label);
const studying = /\u5b66\u4e60\u4e2d/.test(label);
const blank = !label;
const untouched =
!videoDone &&
!studying &&
(blank || waitStudy || partialPct || /\u64ad\u653e\u81f3\uff1a/.test(progress) || /\u5f85\u5b66\u4e60/.test(label));
const examStatus = examDone ? "done" : pendingExam ? "exam" : "none";
return { videoDone, examDone, untouched, examStatus, pendingExam, progress };
}
function isElectiveChapter(el) {
if (!el) return true;
const tagText = Array.from(el.querySelectorAll(".cw-tag-list span, h3 label"))
.map((node) => node.textContent || "")
.join(" ");
return /\u9009\u4fee/.test(tagText);
}
function formatChapterVideoTag(ch) {
if (ch.interactive) {
if (ch.done) return "\u4e92\u52a8\u5df2\u5b8c\u6210";
if (ch.studyState === "\u5b66\u4e60\u4e2d") return "\u4e92\u52a8\u5b66\u4e60\u4e2d";
return "\u89c6\u9891\u5b66\u4e60\u4e2d";
}
const live = applyLiveProgressToChapter(ch);
if (live.progressLabel && /\u64ad\u653e\u81f3\uff1a\d+%/.test(live.progressLabel)) return live.progressLabel;
if (ch.done) return "\u89c6\u9891\u5df2\u5b66\u5b8c";
if (ch.studyState === "\u5b66\u4e60\u4e2d") return "\u89c6\u9891\u5b66\u4e60\u4e2d";
if (ch.progressLabel && /\u64ad\u653e\u81f3\uff1a\d+%/.test(ch.progressLabel)) return ch.progressLabel;
if (ch.untouched) return "\u89c6\u9891\u672a\u5b66";
return "\u89c6\u9891\u5b66\u4e60\u4e2d";
}
function isChapterNeedStudy(ch) {
return !ch.done;
}
function isChapterNeedExam(ch) {
return !!(
ch &&
!ch.interactive &&
ch.done &&
!ch.examDone &&
(ch.pendingExam || ch.examStatus === "exam")
);
}
function isChapterRegistrable(ch) {
return isChapterNeedStudy(ch) || isChapterNeedExam(ch);
}
function isChapterAutoStudy(ch) {
return isChapterNeedStudy(ch) && !ch.interactive;
}
function formatChapterExamTag(status, ch, course) {
if (course && isGongxuCourse(course)) return ch && ch.done ? "\u95ee\u7b54\u9898\u5df2\u5b8c" : "\u89c6\u9891\u95ee\u7b54";
if (ch && ch.interactive) return ch.done ? "\u65e0\u8003\u8bd5·\u5df2\u5b8c\u6210" : "\u65e0\u8003\u8bd5";
if (status === "done") return "\u5df2\u901a\u8fc7";
if (status === "exam") return "\u5f85\u8003\u8bd5";
return "\u672a\u8003\u8bd5";
}
function formatChapterPreviewLine(ch, course) {
const live = applyLiveProgressToChapter(ch);
const dot = live.examDone ? "✓" : live.done ? "◐" : live.untouched ? "○" : "◐";
const videoTag = formatChapterVideoTag(live);
const examTag = formatChapterExamTag(live.examStatus, live, course);
const suffix = live.interactive
? escapeHtml(videoTag)
: `${escapeHtml(videoTag)} · ${escapeHtml(examTag)}`;
return `${dot} ${escapeHtml(live.title)}${suffix}
`;
}
function parseCourseChapterElements(root, cid, uid) {
if (!uid || !cid) return [];
const list = [];
root.querySelectorAll(".course[data-href]").forEach((el, idx) => {
if (isElectiveChapter(el)) return;
if (!isHdInteractiveEnabled() && chapterHasInteractiveTag(el)) return;
const meta = readChapterMeta(el);
if (!meta) return;
const { cwrid, coaid, maxtime, totaltime, interactive } = meta;
const titleEl =
el.querySelector(".cw-title-link strong") ||
el.querySelector("h3 strong") ||
el.querySelector("h3 a strong") ||
el.querySelector("h3 a");
const title = titleEl ? titleEl.textContent.replace(/\s+/g, " ").trim() : `\u7b2c${idx + 1}\u8282`;
const { label, progressText, studyStateAttr } = getChapterStatusParts(el);
const st = inferHyChapterStatuses(label, progressText, studyStateAttr, interactive);
const href = el.getAttribute("data-href") || "";
const cwidM = href.match(/cwid=([^&]+)/i);
const hrefCwid = cwidM ? cwidM[1] : "";
list.push({
uid,
cid,
cwrid,
coaid,
cwid: interactive ? hrefCwid || cwrid : cwrid,
wareCwid: !interactive && hrefCwid && hrefCwid !== cwrid ? hrefCwid : "",
interactive: !!interactive,
duration: totaltime,
startSec: interactive ? 1 : resolveRtEntrySec(maxtime, totaltime),
studiedSec: interactive ? 0 : Math.max(maxtime, parseInt(localStorage.getItem(uid + cwrid + coaid) || "0", 10) || 0),
lsKey: uid + cwrid + coaid,
title,
done: st.videoDone,
examDone: st.examDone,
examStatus: st.examStatus,
untouched: st.untouched,
studyState: label,
progressLabel: st.progress || progressText,
});
});
return list;
}
function parseCourseChaptersFromHtml(html, cid) {
const uid = resolveStudyUserId(readVarFromHtml(html, "uid"));
const courseId =
cid ||
(html.match(/courseId:\s*["']([^"']+)["']/i) || [])[1] ||
(html.match(/cid=([^&'"]+)/i) || [])[1] ||
"";
if (!uid || !courseId) {
return { uid, cid: courseId, chapters: [], courseTitle: "" };
}
const doc = new DOMParser().parseFromString(html, "text/html");
const chapters = parseCourseChapterElements(doc, courseId, uid);
const courseTitle = (
doc.querySelector(".course_lord .info h3 strong")?.textContent ||
doc.querySelector(".course_lord strong.f14blue")?.textContent ||
doc.querySelector(".course_title h1")?.textContent ||
doc.querySelector("h1")?.textContent ||
""
)
.replace(/\s+/g, " ")
.trim();
return { uid, cid: courseId, chapters, courseTitle, gongxu: detectGongxuCourseMeta(doc, courseTitle, html, chapters) };
}
function parseCourseChapterJobs() {
const uid = resolveStudyUserId(window.uid || readPageVar("uid"));
const cid = readCourseId();
if (!uid || !cid) return [];
return parseCourseChapterElements(document, cid, uid);
}
function jobToCtx(job) {
return {
uid: job.uid,
cwrid: job.cwrid,
coaid: job.coaid,
cid: job.cid,
cwid: job.cwid || job.cwrid,
wareCwid: job.wareCwid,
groupId: job.groupId,
provinceId: job.provinceId,
duration: job.duration,
baseSec: job.startSec || 1,
lsKey: job.lsKey,
title: job.title,
};
}
function buildPlayContext() {
const uid = window.uid || readPageVar("uid");
const cwrid = window.cwrid || readPageVar("cwrid");
const coaid = window.coaid || readPageVar("coaid");
const cid = window.cid || readPageVar("cid");
const meta = readWareMeta(uid, cwrid, coaid);
let duration = meta.totaltime;
try {
const pd = Math.floor(window.player.j2s_getDuration());
if (pd > 0) duration = pd;
} catch (_) {}
duration = Math.max(60, duration || 3600);
return {
uid,
cwrid,
coaid,
cid,
groupId: readGroupId(),
provinceId: readProvinceId(),
duration,
baseSec: resolveStartSec(meta.maxtime, duration),
lsKey: meta.lsKey,
title: (document.title || cwrid || "").slice(0, 24),
};
}
function jobKey(ctx) {
return `${ctx.uid}:${ctx.cwrid}:${ctx.coaid}`;
}
function loadJobs() {
try {
return JSON.parse(localStorage.getItem(JOBS_KEY) || "{}");
} catch (_) {
return {};
}
}
function saveJobs(jobs) {
localStorage.setItem(JOBS_KEY, JSON.stringify(jobs));
}
function getJob(ctx) {
return loadJobs()[jobKey(ctx)] || null;
}
function saveJob(ctx, patch) {
const jobs = loadJobs();
const key = jobKey(ctx);
jobs[key] = Object.assign({}, jobs[key], patch, {
uid: ctx.uid,
cwrid: ctx.cwrid,
coaid: ctx.coaid,
cid: ctx.cid,
groupId: ctx.groupId,
provinceId: ctx.provinceId,
lsKey: ctx.lsKey,
title: ctx.title || (jobs[key] && jobs[key].title) || key.slice(-8),
});
saveJobs(jobs);
}
function apiFetch(url, bodyObj) {
return fetch(url, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
body: new URLSearchParams(bodyObj).toString(),
__hyEngineMark: true,
})
.then((r) => r.text())
.then(parseApiResponse);
}
function isApiLoginLost(text) {
if (!text) return false;
return (
text.indexOf("%2fsecure%2flogin_sso.aspx") > -1 ||
text.indexOf("/secure/login_sso.aspx") > -1 ||
(text.length < 600 && /checkLogin/i.test(text))
);
}
function isLoggedInPageHtml(text) {
if (!text) return false;
return /var\s+uid\s*=|course_ware_polyv|id=["']player["']|window\.player|polyvPlayer|class=["']course["'][^>]*data-href|\u8bfe\u7a0b\u5b66\u4e60\u4e0e\u8003\u8bd5|ul\.sub_ul|course_collect\.aspx/i.test(
text
);
}
function isPageLoginRedirect(text) {
if (!text) return false;
if (isLoggedInPageHtml(text)) return false;
return (
/login_sso\.aspx/i.test(text) ||
/%2fsecure%2flogin_sso/i.test(text) ||
(text.length < 800 && /checkLogin/i.test(text))
);
}
function warnLoginIfNeeded(quiet) {
if (quiet || Date.now() - loginWarnAt <= 600000) return;
loginWarnAt = Date.now();
pushLog("⚠ \u767b\u5f55\u53ef\u80fd\u5df2\u5931\u6548\uff0c\u8bf7\u5728\u672c\u9875 F5 \u5237\u65b0\u91cd\u65b0\u767b\u5f55\uff0c\u6536\u5c3e\u4f1a\u81ea\u52a8\u91cd\u8bd5");
}
function parseApiResponse(text) {
if (!text) return { code: -1, msg: "empty" };
if (isApiLoginLost(text)) return { code: -2, msg: "\u767b\u5f55\u5df2\u5931\u6548\uff0c\u8bf7\u5237\u65b0\u9875\u9762" };
const m = text.match(/\{"code"[\s\S]*\}/);
if (m) {
try {
return JSON.parse(m[0]);
} catch (_) {}
}
try {
return JSON.parse(text);
} catch (_) {
return { code: -1, msg: String(text).slice(0, 120) };
}
}
function apiGet(url, params) {
const qs = new URLSearchParams(params).toString();
return fetch(url + (url.includes("?") ? "&" : "?") + qs, {
credentials: "include",
__hyEngineMark: true,
})
.then((r) => r.text())
.then(parseApiResponse);
}
function normalizeProcessExamQuestions(raw) {
if (Array.isArray(raw)) return raw;
if (raw && Array.isArray(raw.listBody)) return raw.listBody;
if (raw && Array.isArray(raw.body)) return raw.body;
if (raw && Array.isArray(raw.list)) return raw.list;
return [];
}
function pickProcessExamUserAnswer(choices) {
if (!Array.isArray(choices) || !choices.length) return null;
let correctIdx = -1;
choices.forEach((c, i) => {
if (c && Number(c.right_answer) === 1) correctIdx = i;
});
if (correctIdx < 0) {
choices.forEach((c, i) => {
if (c && Number(c.right_answer) === i) correctIdx = i;
});
}
if (correctIdx < 0) correctIdx = 0;
return { letter: String.fromCharCode(65 + correctIdx), isRight: 1 };
}
async function resolveWareCwid(ctx, job) {
const j = job || (ctx ? getJob(ctx) : null);
const cached = (j && j.wareCwid) || (ctx && ctx.wareCwid) || "";
if (cached) return cached;
try {
const ai = await apiGet("/ashx/getAiQuestionList.ashx", { relationId: ctx.cwrid });
const wid = ai && ai.listBody && ai.listBody[0] && ai.listBody[0].course_ware_id;
if (wid) {
if (j) saveJob(ctx, { wareCwid: wid });
return wid;
}
} catch (_) {}
try {
const url =
"/course_ware/course_ware_polyv.aspx?cwid=" +
encodeURIComponent(ctx.cwrid) +
"&ff=0&ft=0&t=0";
const page = await fetchCmeHtmlSafe(url, { __hyEngineMark: true });
if (page.surveyRedirect || isCmeSurveyRedirectUrl(page.url || "")) {
handleSurveyGateManual(ctx, page.url || "");
return "";
}
const html = page.text || "";
const m = html.match(/var\s+cwid\s*=\s*['"]([^'"]+)['"]/i);
if (m && m[1]) {
if (j) saveJob(ctx, { wareCwid: m[1] });
return m[1];
}
} catch (_) {}
return "";
}
async function postProcessViaEngine(ctx, status, sec, label) {
const out = await _xc(
{
type: "platform_post",
path: "/ashx/addCourseWareProcess.ashx?r=" + Math.random(),
payload: {
group_id: ctx.groupId,
course_organ_assign_id: ctx.coaid,
relation_id: ctx.cwrid,
user_id: ctx.uid,
province_id: ctx.provinceId,
player_current_time: Math.floor(sec),
course_id: ctx.cid,
status,
},
event: "process_result",
label: label || `status=${status}@${sec}s`,
},
ctx
);
const data = out && out.lastResult && out.lastResult.data;
if (data) return data;
return parseApiResponse((out && out.lastResult && out.lastResult.text) || "");
}
async function postPlayRecordViaEngine(ctx, sec) {
const out = await _xc(
{
type: "platform_get",
path: "/ashx/addCourseWarePlayRecord.ashx",
params: {
relation_id: ctx.cwrid,
user_id: ctx.uid,
group_id: ctx.groupId,
player_current_time: Math.floor(sec),
},
event: "play_record_result",
label: `play_record@${sec}s`,
},
ctx
);
const data = out && out.lastResult && out.lastResult.data;
if (data) return data;
return parseApiResponse((out && out.lastResult && out.lastResult.text) || "");
}
async function ensureCtxApiMeta(ctx) {
if (!ctx) return ctx;
if (ctx.groupId && ctx.provinceId) return ctx;
const sample = ctx.cwrid || ctx.cwid || "";
const meta = (ctx.cid && sample ? await ensureCourseApiMeta(ctx.cid, sample) : null) || getCachedApiMeta(ctx.cid);
return Object.assign({}, ctx, {
groupId: ctx.groupId || (meta && meta.groupId) || readGroupId(),
provinceId: ctx.provinceId || (meta && meta.provinceId) || readProvinceId(),
});
}
async function submitChapterProcessExams(ctx, job, label, options) {
const quiet = !!(options && options.quiet);
const tag = label || (ctx && ctx.title) || (ctx && ctx.cwrid ? ctx.cwrid.slice(0, 8) : "\u7ae0\u8282");
const wareCwid = await resolveWareCwid(ctx, job);
if (!wareCwid) {
pushLog(`[${tag}] \u516c\u9700\u8bfe\uff1a\u672a\u83b7\u53d6\u8bfe\u4ef6ID\uff0c\u8df3\u8fc7\u89c6\u9891\u5185\u95ee\u7b54\u9898`);
return { ok: false, count: 0, total: 0 };
}
let questions = [];
try {
const raw = await apiGet("/ashx/get_course_ware_process.ashx", {
cwid: wareCwid,
video_type: "polyv",
});
questions = normalizeProcessExamQuestions(raw);
} catch (e) {
pushLog(`[${tag}] \u516c\u9700\u8bfe\uff1a\u62c9\u53d6\u89c6\u9891\u95ee\u7b54\u9898\u5931\u8d25`);
return { ok: false, count: 0, total: 0 };
}
if (!questions.length) return { ok: true, count: 0, total: 0 };
let okCount = 0;
for (const q of questions) {
const examid = q.examid || q.questionId || q.question_id;
if (!examid) continue;
const pick = pickProcessExamUserAnswer(q.choices);
if (!pick) continue;
try {
const res = await apiFetch("/ashx/addCourseWareProcessExamLog.ashx", {
userId: ctx.uid,
relationId: ctx.cwrid,
questionId: examid,
isRight: pick.isRight,
userAnswer: pick.letter,
videoSource: 2,
});
if (res && res.code === 0) okCount++;
await sleep(200);
} catch (_) {}
}
if (okCount > 0) {
pushLog(`[${tag}] \u516c\u9700\u8bfe\uff1a\u5df2\u63d0\u4ea4\u89c6\u9891\u5185\u95ee\u7b54\u9898 ${okCount}/${questions.length}`);
if (!quiet) pushUserLog(`${tag} · \u89c6\u9891\u95ee\u7b54\u9898 ${okCount}/${questions.length}`);
} else if (questions.length) {
pushLog(`[${tag}] \u516c\u9700\u8bfe\uff1a\u89c6\u9891\u5185\u95ee\u7b54\u9898 0/${questions.length}\uff08\u672a\u63d0\u4ea4\u6210\u529f\uff09`);
}
const ok = questions.length ? okCount >= questions.length : true;
return { ok, count: okCount, total: questions.length };
}
function resolveGongxuChapterProgress(job, ctx) {
const j = job || {};
const metaDuration = Math.max(60, Number(j.duration || ctx?.duration) || 3600);
const fromLs =
j.uid && j.cwrid ? readChapterStudiedSec(j.uid, j.cwrid, j.coaid, metaDuration) : 0;
const posted = Math.max(0, Number(j.lastPostedSec) || Number(j.studiedSec) || 0);
const wareMeta =
j.uid && j.cwrid ? readWareMeta(j.uid, j.cwrid, j.coaid) : { maxtime: 0, totaltime: 0 };
const domTotal = wareMeta.totaltime > 0 ? wareMeta.totaltime : metaDuration;
const apiFilled = !!(j.progressFilled || j.status2Sent);
let finishSec = metaDuration;
const best = Math.max(fromLs, posted, wareMeta.maxtime || 0);
if (best > 0) {
if (best >= metaDuration - 90) finishSec = Math.max(metaDuration, best);
else if (domTotal > 0 && best >= domTotal - 90) finishSec = Math.max(best, domTotal);
else if (best >= metaDuration * 0.88) finishSec = Math.max(best, metaDuration);
}
const wall =
resolveWallClockStudiedSec(j) || Number(j.studiedSec) || Number(j.lastPostedSec) || 0;
const studied = Math.min(finishSec, Math.max(fromLs, posted, wall));
const nearFull =
apiFilled ||
posted >= metaDuration - 90 ||
posted >= finishSec - 90 ||
(fromLs > 0 && fromLs >= finishSec - 90) ||
(fromLs > 0 && fromLs >= metaDuration - 90) ||
studied >= finishSec - 90 ||
(fromLs > 0 && fromLs >= metaDuration * 0.88) ||
formatPlayProgressPct(fromLs || posted || studied, finishSec) >= 97;
return { studied, duration: finishSec, metaDuration, fromLs, posted, nearFull, apiFilled };
}
function chapterStudiedSec(job, ctx) {
if (isGongxuCtx(ctx, job)) {
const p = resolveGongxuChapterProgress(job, ctx);
return { studied: p.studied, duration: p.duration };
}
const j = job || {};
const duration = Math.max(60, Number(j.duration || ctx?.duration) || 3600);
const fromLs =
j.uid && j.cwrid ? readChapterStudiedSec(j.uid, j.cwrid, j.coaid, duration) : 0;
const studied = Math.min(
duration,
Math.max(
0,
fromLs,
resolveWallClockStudiedSec(j) || Number(j.studiedSec) || Number(j.lastPostedSec) || 0
)
);
return { studied, duration };
}
function isGongxuChapterNearFull(job, ctx) {
return resolveGongxuChapterProgress(job, ctx).nearFull;
}
function shouldGongxuFastFinish(job, ctx) {
if (!isGongxuCtx(ctx, job)) return false;
if (resolveGongxuChapterProgress(job, ctx).nearFull) return true;
return gongxuLikelyPrefilled(job, ctx);
}
function courseHadOfflineFill(cid) {
const k = normalizeQueueCid(cid);
if (!k) return false;
const st = loadSilentState();
if (st.filledAt || st.sessionStart) return true;
return Object.values(loadJobs()).some(
(j) =>
j &&
normalizeQueueCid(j.cid) === k &&
(j.progressFilled || ["silent_clock", "silent_filled", "silent_filling"].includes(j.phase))
);
}
function gongxuFinishWasAborted(token) {
return token !== gongxuFinishAbort || !getEnabled();
}
async function confirmGongxuChapterExamResult(ctx, job, label) {
const examId = resolveExamCwid(ctx, job);
if (!examId) return { ok: false, msg: "\u7f3a\u5c11 cwid" };
const tag = label || (job && job.title) || ctx.title || examId.slice(0, 8);
const wareCwid = (job && (job.wareCwid || job.cwid)) || ctx.wareCwid || ctx.cwid || examId;
const resultUrl = `${location.origin}/pages/exam_result.aspx?cwid=${encodeURIComponent(examId)}`;
const referer = `${location.origin}/course_ware/course_ware_polyv.aspx?ff=0&ft=0&cwid=${encodeURIComponent(wareCwid)}&t=0`;
for (let attempt = 0; attempt < 3; attempt++) {
if (!getEnabled()) return { ok: false, msg: "\u5df2\u6682\u505c" };
try {
const r = await fetch(resultUrl, {
credentials: "include",
headers: {
Accept: "text/html,application/xhtml+xml",
Referer: referer,
},
__hyEngineMark: true,
});
const text = await r.text();
const url = r.url || resultUrl;
if (isPageLoginRedirect(text)) return { ok: false, msg: "\u767b\u5f55\u5931\u6548" };
const fb = parseHyExamResultPage(text, url);
if (fb && fb.failed) return { ok: false, msg: "exam_result \u663e\u793a\u672a\u901a\u8fc7" };
if (isHyExamPassHtml(text, url)) {
pushLog(`[${tag}] \u516c\u9700\u8bfe\uff1aexam_result \u5df2\u786e\u8ba4\uff08\u672c\u8bfe\u7a0b\u5b8c\u6210\uff09`);
markPanelChapterExamDone(ctx);
return { ok: true };
}
if (/\u8bf7\u5148\u5b66\u4e60|\u65f6\u957f\u4e0d\u8db3|\u4e0d\u80fd\u7b54\u5377|\u672a\u5b66\u5b8c/.test(text)) {
return { ok: false, msg: "\u5b66\u65f6\u672a\u6ee1\uff0c\u65e0\u6cd5\u786e\u8ba4\u5b8c\u6210" };
}
} catch (e) {
if (attempt === 2) {
return { ok: false, msg: e && e.message ? e.message : "exam_result \u8bf7\u6c42\u5931\u8d25" };
}
}
if (attempt < 2) await sleep(700);
}
return { ok: false, msg: "exam_result \u672a\u786e\u8ba4\u5b8c\u6210" };
}
async function gongxuChapterFinish(ctx, job, label, prog, options) {
const opts = options || {};
const p = prog || resolveGongxuChapterProgress(job, ctx);
const studied = p.studied;
const duration = p.duration;
const fromLs = p.fromLs;
const tag = label || job?.title || "\u7ae0\u8282";
const finishToken = gongxuFinishAbort;
if (gongxuFinishBusy) return { ok: false, reason: "busy" };
gongxuFinishBusy = true;
try {
let fullCtx = await ensureCtxApiMeta(
Object.assign({}, ctx, {
cwid: (job && (job.wareCwid || job.cwid)) || ctx.cwid || ctx.cwrid,
wareCwid: (job && job.wareCwid) || ctx.wareCwid || "",
duration,
})
);
if (gongxuFinishWasAborted(finishToken)) return { ok: false, reason: "aborted" };
const live = Object.assign({}, job, loadJobs()[jobKey(fullCtx)] || {});
let examRes = { ok: true, count: 0, total: 0 };
const skipExams =
!!opts.skipExams ||
(!!live.processExamsDone && live.processExamsAt && Date.now() - live.processExamsAt < 600000);
if (!skipExams) {
examRes = await submitChapterProcessExams(fullCtx, job, tag);
if (gongxuFinishWasAborted(finishToken)) return { ok: false, reason: "aborted" };
if (examRes.total > 0) {
saveJob(fullCtx, {
processExamsDone: !!examRes.ok,
processExamsAt: Date.now(),
});
}
if (examRes.total > 0 && !examRes.ok) {
return { ok: false, reason: "exams", examRes };
}
if (examRes.total > 0) await sleep(800);
}
if (!skipExams) pushUserLog(`${tag} · \u6b63\u5728\u63d0\u4ea4\u6536\u5c3e…`);
await warmPlaySession(fullCtx, true);
if (gongxuFinishWasAborted(finishToken)) return { ok: false, reason: "aborted" };
const entry = Math.max(1, duration - 1);
const needEnter = !live.progressFilled;
if (needEnter) {
const r0 = await postProcessViaEngine(fullCtx, 0, entry, `gongxu_enter@${entry}s`);
pushLog(
`[${tag}] \u516c\u9700\u8fdb\u5165 status=0 code=${r0 && r0.code != null ? r0.code : "?"}${r0 && r0.msg ? " " + r0.msg : ""}`
);
if (r0 && r0.code === -2) return { ok: false, reason: "login" };
await sleep(500);
}
if (gongxuFinishWasAborted(finishToken)) return { ok: false, reason: "aborted" };
const r2 = await postProcessViaEngine(fullCtx, 2, duration, `gongxu_status2@${duration}s`);
pushLog(
`[${tag}] \u516c\u9700\u64ad\u5b8c status=2 code=${r2 && r2.code != null ? r2.code : "?"}${r2 && r2.msg ? " " + r2.msg : ""}`
);
if (r2 && r2.code === -2) return { ok: false, reason: "login" };
await sleep(400);
await postProcessViaEngine(fullCtx, 3, duration, `gongxu_status3@${duration}s`);
await sleep(300);
if (gongxuFinishWasAborted(finishToken)) return { ok: false, reason: "aborted" };
const recordSecs = [];
[duration, fromLs, Math.max(1, duration - 1)].forEach((sec) => {
const n = Math.floor(Number(sec) || 0);
if (n > 0 && !recordSecs.includes(n)) recordSecs.push(n);
});
let rec = null;
for (const sec of recordSecs) {
try {
rec = await postPlayRecordViaEngine(fullCtx, sec);
} catch (e) {
pushLog(`[${tag}] \u516c\u9700\u64ad\u5b8c\u8bb0\u5f55\u5f02\u5e38@${sec}s: ${e && e.message ? e.message : e}`);
continue;
}
pushLog(
`[${tag}] \u516c\u9700\u64ad\u5b8c\u8bb0\u5f55@${sec}s code=${rec && rec.code != null ? rec.code : "?"}${rec && rec.msg ? " " + rec.msg : ""}`
);
if (rec && rec.code === 0) break;
}
if (gongxuFinishWasAborted(finishToken)) return { ok: false, reason: "aborted" };
if (rec && rec.code === 0) {
saveJob(fullCtx, {
progressFilled: true,
status2Sent: true,
lastPostedSec: duration,
engineSessionId: "",
gongxuUnlockFail: 0,
});
const fin = await finalizeVideoChapterDone(fullCtx, job, tag, studied, "[\u516c\u9700]");
if (fin && fin.examResultPending) {
return { ok: false, reason: "exam_result", examConfirm: fin };
}
return { ok: true };
}
if (!gongxuFinishWasAborted(finishToken)) {
saveJob(fullCtx, {
progressFilled: true,
status2Sent: true,
lastPostedSec: duration,
engineSessionId: "",
gongxuUnlockFail: (Number(live.gongxuUnlockFail) || 0) + 1,
});
}
return {
ok: false,
reason: rec && rec.code === 1 ? "play_record_1" : "play_record",
examRes,
rec,
};
} finally {
gongxuFinishBusy = false;
}
}
function scheduleGongxuUnlockRetry(ctx, tag, examRes, reason) {
if (!getEnabled()) return;
parkGongxuChapterForManualRetry(ctx, tag);
if (reason === "exams" && examRes && examRes.total > 0) {
pushUserLog(
`\u300c${tag}\u300d\u89c6\u9891\u5185\u95ee\u7b54\u9898 ${examRes.count}/${examRes.total}\uff0c\u8bf7\u624b\u52a8\u6253\u5f00\u672c\u8282\u7b54\u5b8c\u5f39\u9898\u540e\u518d\u300c\u505c\u6b62→\u5f00\u59cb\u300d`
);
} else if (reason === "play_record_1") {
pushLog(`[${tag}] \u516c\u9700\u8bfe\u64ad\u5b8c\u6821\u9a8c\u672a\u901a\u8fc7\uff0c\u7b49\u5f85\u624b\u52a8\u300c\u505c\u6b62→\u5f00\u59cb\u300d`);
} else if (reason === "exam_result") {
pushUserLog(`\u300c${tag}\u300d\u9700\u786e\u8ba4 exam_result \u5b8c\u6210\u9875\uff0c\u811a\u672c\u5c06\u81ea\u52a8\u91cd\u8bd5\uff1b\u4ecd\u663e\u793a\u5f85\u8003\u8bd5\u8bf7\u624b\u52a8\u6253\u5f00\u7ed3\u679c\u9875`);
}
}
async function tryUnlockGongxuChapter(ctx, job, label) {
if (!isGongxuCtx(ctx, job)) return false;
const tag = label || job?.title || "\u7ae0\u8282";
let prog = resolveGongxuChapterProgress(job, ctx);
if (!prog.nearFull && gongxuLikelyPrefilled(job, ctx)) {
const dur = Math.max(60, Number(job.duration || ctx.duration) || 3600);
prog = {
studied: dur,
duration: dur,
metaDuration: dur,
fromLs: prog.fromLs,
posted: dur,
nearFull: true,
apiFilled: true,
prefilled: true,
};
pushLog(`[${tag}] \u516c\u9700\u8bfe\uff1a\u68c0\u6d4b\u5230\u79bb\u7ebf\u62c9\u6ee1\uff0c\u76f4\u63a5\u6536\u5c3e\uff08\u8df3\u8fc7 1:1 \u91cd\u5b66\uff09`);
}
if (!prog.nearFull) return false;
const fullCtx = Object.assign({}, ctx, { duration: prog.duration });
const live = Object.assign({}, job, loadJobs()[jobKey(fullCtx)] || {});
const skipExams = !!(
live.processExamsDone &&
live.processExamsAt &&
Date.now() - live.processExamsAt < 600000
);
if (!skipExams) {
pushLog(
`[${tag}] \u516c\u9700\u8bfe\uff1a\u8bfe\u65f6\u5df2\u6ee1\uff08${Math.round(prog.studied / 60)}/${Math.round(prog.duration / 60)}\u5206\uff09\uff0c\u63d0\u4ea4\u95ee\u7b54\u9898\u5e76\u6536\u5c3e…`
);
} else {
pushLog(
`[${tag}] \u516c\u9700\u8bfe\uff1a\u95ee\u7b54\u9898\u5df2\u63d0\u4ea4\uff0c\u91cd\u8bd5\u6536\u5c3e\uff08${Math.round(prog.studied / 60)}/${Math.round(prog.duration / 60)}\u5206\uff09…`
);
}
const res = await gongxuChapterFinish(fullCtx, live, tag, prog, { skipExams });
if (res.ok) {
scheduleBgRunSoon(500);
return true;
}
if (res.reason === "aborted") return false;
if (res.reason === "busy") {
scheduleBgRunSoon(800);
return false;
}
if (res.reason === "login") {
warnLoginIfNeeded(false);
scheduleGongxuUnlockRetry(fullCtx, tag, res.examRes, res.reason);
return false;
}
scheduleGongxuUnlockRetry(fullCtx, tag, res.examRes, res.reason);
return false;
}
function isCmeSurveyRedirectUrl(url) {
const u = String(url || "");
return (
/dcwj\.91huayi\.com/i.test(u) ||
/sojumpparm=/i.test(u) ||
/\/vm\/[A-Za-z0-9]+\.aspx/i.test(u)
);
}
function openSurveyUrlForManual(url) {
const u = String(url || "").trim();
if (!u || !isCmeSurveyRedirectUrl(u)) return false;
try {
if (typeof GM_openInTab === "function") {
GM_openInTab(u, { active: true, insert: true, setParent: true });
return true;
}
} catch (_) {}
try {
window.open(u, "_blank", "noopener,noreferrer");
return true;
} catch (_) {
return false;
}
}
/** 课前问卷:停止自动学习,打开问卷页让用户手动填完后再点「开始」 */
function handleSurveyGateManual(ctx, surveyUrl) {
const label = String((ctx && (ctx.title || ctx.cwrid || ctx.cwid)) || "课件").trim();
const url = String(surveyUrl || "").trim();
const opened = openSurveyUrlForManual(url);
const tip = opened
? `${label} · 需先完成课前问卷,已打开问卷页;完成后回本页点「开始」继续`
: `${label} · 需先完成课前问卷:请手动打开该章节完成问卷后,回本页点「开始」继续`;
pushUserLog(tip);
if (getEnabled()) stopAutoStudy("需先完成课前问卷,完成后点「开始」继续");
return false;
}
function resolveCmeAbsoluteUrl(url, base) {
if (!url) return "";
if (/^https?:\/\//i.test(url)) return url.replace(/&/g, "&");
try {
return new URL(url, base || location.origin).href.replace(/&/g, "&");
} catch (_) {
return url;
}
}
function isPolyvPlayRequestUrl(url) {
return /course_ware_polyv\.aspx/i.test(String(url || ""));
}
function buildPolyvPlayUrl(cwid) {
return (
"/course_ware/course_ware_polyv.aspx?cwid=" +
encodeURIComponent(cwid) +
"&ff=0&ft=0&t=0"
);
}
function extractDcwjSurveyUrl(text) {
const s = String(text || "");
let m = s.match(/https?:\/\/dcwj\.91huayi\.com\/vm\/[A-Za-z0-9]+\.aspx\?sojumpparm=[^\s"'<>]*/i);
if (m) return m[0].replace(/&/g, "&").replace(/["']+$/, "");
m = s.match(/https?:\/\/dcwj\.91huayi\.com\/vm\/[A-Za-z0-9]+\.aspx[^\s"'<>]*/i);
if (m) return m[0].replace(/&/g, "&").replace(/["']+$/, "");
m = s.match(/\/vm\/[A-Za-z0-9]+\.aspx\?sojumpparm=[^\s"'<>]*/i);
if (m) return ("https://dcwj.91huayi.com" + m[0]).replace(/&/g, "&");
m = s.match(/(?:^|[^\w/])([A-Za-z0-9]{5,}\.aspx\?sojumpparm=[^\s"'<>]*)/i);
if (m) return ("https://dcwj.91huayi.com/vm/" + m[1]).replace(/&/g, "&");
return "";
}
function discoverSurveyFromPerformance(cwid) {
try {
const entries = performance.getEntriesByType("resource") || [];
const id = String(cwid || "").toLowerCase();
for (let i = entries.length - 1; i >= 0; i--) {
const n = String((entries[i] && entries[i].name) || "");
if (!/dcwj\.91huayi\.com|sojumpparm=/i.test(n)) continue;
if (id && n.toLowerCase().indexOf(id) < 0 && !/sojumpparm=/i.test(n)) continue;
const hit = extractDcwjSurveyUrl(n);
if (hit) return hit;
if (isCmeSurveyRedirectUrl(n)) return n;
}
} catch (_) {}
return "";
}
function xhrGetFollow(url, options) {
const opts = options || {};
return new Promise(function (resolve) {
const absUrl = resolveCmeAbsoluteUrl(url, location.origin);
const xhr = new XMLHttpRequest();
xhr.__hyEngineMark = !!opts.__hyEngineMark;
xhr.__hyUrl = absUrl;
xhr.open("GET", absUrl, true);
xhr.withCredentials = true;
try {
const referer = String(opts.referer || document.referrer || location.href || "").trim();
if (referer) xhr.setRequestHeader("Referer", referer);
} catch (_) {}
xhr.onload = function () {
resolve({
ok: xhr.status >= 200 && xhr.status < 300,
status: xhr.status,
text: String(xhr.responseText || ""),
url: absUrl,
responseURL: String(xhr.responseURL || absUrl),
});
};
xhr.onerror = function () {
resolve({ ok: false, status: 0, text: "", url: absUrl, responseURL: absUrl, error: true });
};
xhr.ontimeout = function () {
resolve({ ok: false, status: 0, text: "", url: absUrl, responseURL: absUrl, timeout: true });
};
try {
xhr.send();
} catch (e) {
resolve({ ok: false, status: 0, text: "", url: absUrl, responseURL: absUrl, error: e });
}
});
}
function polyvProbeToPage(xhrRes) {
const finalUrl = String(xhrRes.responseURL || xhrRes.url || "");
const text = String(xhrRes.text || "");
if (
isCmeSurveyRedirectUrl(finalUrl) ||
/sojumpparm|sojumpParm|问卷星|processjq\.ashx/i.test(finalUrl + text)
) {
return {
ok: false,
text,
url: extractDcwjSurveyUrl(finalUrl) || finalUrl,
responseURL: finalUrl,
status: xhrRes.status,
surveyRedirect: true,
videoFaceGate: false,
polyvProbe: true,
};
}
const gate = isHyVideoFaceGateUrl(finalUrl);
const gateCwrid = extractHyFaceCwrid(finalUrl);
if (gate && gateCwrid) markHyVideoFacePending(gateCwrid);
else if (gateCwrid) clearHyVideoFacePending(gateCwrid);
return {
ok: !!xhrRes.ok && !gate,
text,
url: finalUrl,
responseURL: finalUrl,
status: xhrRes.status,
surveyRedirect: false,
videoFaceGate: gate,
polyvProbe: true,
};
}
/** 禁止 XHR 跟随到 dcwj(会 CORS 且探测失败);用 redirect:manual + Performance/GM 识别问卷 */
async function fetchPolyvPlayPage(url, options) {
const opts = options || {};
const absUrl = resolveCmeAbsoluteUrl(url, location.origin);
let cwid = "";
try {
cwid = new URL(absUrl).searchParams.get("cwid") || "";
} catch (_) {}
function packSurvey(next) {
const u = extractDcwjSurveyUrl(next) || String(next || "").trim();
return {
ok: false,
text: "",
url: u,
responseURL: u,
status: 302,
surveyRedirect: true,
videoFaceGate: false,
polyvProbe: true,
};
}
function packFace(next) {
const u = String(next || "").trim();
const gateCwrid = extractHyFaceCwrid(u);
if (gateCwrid) markHyVideoFacePending(gateCwrid);
return {
ok: false,
text: "",
url: u,
responseURL: u,
status: 302,
surveyRedirect: false,
videoFaceGate: true,
polyvProbe: true,
};
}
// 1) GM:可读跨域 302 Location / finalUrl,避免页面 XHR CORS
try {
if (typeof GM_xmlhttpRequest === "function") {
const gm = await new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET",
url: absUrl,
anonymous: false,
timeout: 20000,
responseType: "text",
onload: (res) => resolve(res),
onerror: () => resolve(null),
ontimeout: () => resolve(null),
});
});
if (gm) {
const finalUrl = String(gm.finalUrl || "");
const headers = String(gm.responseHeaders || "");
const locM = headers.match(/^(?:location):\s*(.+)$/im);
const loc = locM ? String(locM[1] || "").trim() : "";
const body = String(gm.responseText || "");
const candidates = [loc, finalUrl, extractDcwjSurveyUrl(body), extractDcwjSurveyUrl(finalUrl + " " + loc)];
for (let i = 0; i < candidates.length; i++) {
const c = String(candidates[i] || "").trim();
if (!c) continue;
if (isCmeSurveyRedirectUrl(c) || extractDcwjSurveyUrl(c)) {
trace(`polyv-probe GM survey → ${(extractDcwjSurveyUrl(c) || c).slice(0, 120)}`);
return packSurvey(c);
}
if (isHyVideoFaceGateUrl(c)) return packFace(c);
}
if (gm.status >= 200 && gm.status < 300 && finalUrl && /91huayi\.com/i.test(finalUrl) && !isCmeSurveyRedirectUrl(finalUrl)) {
if (isHyVideoFaceGateUrl(finalUrl)) return packFace(finalUrl);
const surveyInBody = extractDcwjSurveyUrl(body);
if (surveyInBody || /dcwj\.91huayi\.com|sojumpparm|课前问卷|问卷调查/i.test(body)) {
return packSurvey(surveyInBody || discoverSurveyFromPerformance(cwid) || "");
}
return {
ok: true,
text: body,
url: finalUrl,
responseURL: finalUrl,
status: gm.status,
surveyRedirect: false,
videoFaceGate: false,
polyvProbe: true,
};
}
}
}
} catch (e) {
trace("polyv-probe GM: " + (e && e.message ? e.message : e));
}
// 2) 同域 fetch 不跟随重定向(绝不上 dcwj)
try {
const fetchInit = { credentials: "include", redirect: "manual" };
if (opts.__hyEngineMark) fetchInit.__hyEngineMark = true;
const r = await fetch(absUrl, fetchInit);
if (!r || r.type === "opaqueredirect" || r.status === 0) {
await sleep(80);
const discovered = discoverSurveyFromPerformance(cwid);
trace(`polyv-probe opaque → ${(discovered || "(survey-gate)").slice(0, 120)}`);
return packSurvey(discovered);
}
if (r.status >= 300 && r.status < 400) {
let loc = "";
try {
loc = (r.headers && (r.headers.get("Location") || r.headers.get("location"))) || "";
} catch (_) {}
const next = resolveCmeAbsoluteUrl(loc, absUrl);
if (isCmeSurveyRedirectUrl(next) || extractDcwjSurveyUrl(next)) return packSurvey(next);
if (isHyVideoFaceGateUrl(next)) return packFace(next);
await sleep(80);
const discovered = discoverSurveyFromPerformance(cwid);
if (discovered) return packSurvey(discovered);
try {
if (next && new URL(next).origin !== location.origin) return packSurvey(next);
} catch (_) {
return packSurvey(discovered);
}
}
const text = await r.text().catch(() => "");
const surveyInBody = extractDcwjSurveyUrl(text) || extractDcwjSurveyUrl(String(r.url || ""));
if (surveyInBody || isCmeSurveyRedirectUrl(r.url || "") || /dcwj\.91huayi\.com|sojumpparm|课前问卷|问卷调查/i.test(text)) {
return packSurvey(surveyInBody || discoverSurveyFromPerformance(cwid) || r.url || "");
}
if (isHyVideoFaceGateUrl(r.url || absUrl)) return packFace(r.url || absUrl);
return {
ok: r.status >= 200 && r.status < 300,
text,
url: absUrl,
responseURL: absUrl,
status: r.status,
surveyRedirect: false,
videoFaceGate: false,
polyvProbe: true,
};
} catch (e) {
trace("polyv-probe fetch: " + (e && e.message ? e.message : e));
}
const discovered = discoverSurveyFromPerformance(cwid);
if (discovered) return packSurvey(discovered);
return {
ok: false,
text: "",
url: absUrl,
responseURL: absUrl,
status: 0,
surveyRedirect: false,
videoFaceGate: false,
polyvProbe: true,
};
}
async function fetchCmeHtmlSafe(url, options) {
const opts = options || {};
const absUrl = resolveCmeAbsoluteUrl(url, location.origin);
if (isPolyvPlayRequestUrl(absUrl)) {
return fetchPolyvPlayPage(absUrl, opts);
}
const fetchInit = { credentials: "include", redirect: "manual" };
if (opts.__hyEngineMark) fetchInit.__hyEngineMark = true;
let hopUrl = absUrl;
let videoFaceRedirect = false;
for (let i = 0; i < 8; i++) {
try {
const r = await fetch(hopUrl, fetchInit);
const loc = (r.headers && (r.headers.get("Location") || r.headers.get("location"))) || "";
if (r.type === "opaqueredirect") {
return {
ok: false,
text: "",
url: hopUrl,
surveyRedirect: true,
opaqueRedirect: true,
};
}
if (r.status >= 300 && r.status < 400 && loc) {
const next = resolveCmeAbsoluteUrl(loc, hopUrl);
if (isHyVideoFaceGateUrl(next)) {
videoFaceRedirect = true;
const gateCwrid = extractHyFaceCwrid(next);
if (gateCwrid) markHyVideoFacePending(gateCwrid);
return {
ok: false,
text: "",
url: next,
surveyRedirect: false,
videoFaceRedirect: true,
videoFaceGate: true,
status: r.status,
};
}
if (isCmeSurveyRedirectUrl(next)) {
return { ok: false, text: "", url: next, surveyRedirect: true, videoFaceRedirect, status: r.status };
}
let nextOrigin = "";
try {
nextOrigin = new URL(next).origin;
} catch (_) {
return { ok: false, text: "", url: next, surveyRedirect: false, videoFaceRedirect, status: r.status };
}
if (nextOrigin !== location.origin) {
return {
ok: false,
text: "",
url: next,
surveyRedirect: false,
crossOrigin: true,
videoFaceRedirect,
status: r.status,
};
}
hopUrl = next;
continue;
}
const text = await r.text().catch(() => "");
if (r.status >= 300 && r.status < 400) {
return {
ok: false,
text,
url: hopUrl,
status: r.status,
surveyRedirect: false,
};
}
return {
ok: r.status >= 200 && r.status < 300,
text,
url: hopUrl,
status: r.status,
surveyRedirect: false,
};
} catch (e) {
return { ok: false, text: "", url: hopUrl, surveyRedirect: false, error: e, videoFaceRedirect };
}
}
return { ok: false, text: "", url: absUrl, surveyRedirect: false, videoFaceRedirect };
}
async function warmPlaySession(ctx, quiet) {
const cwid = ctx.cwid || ctx.cwrid;
if (!cwid) return false;
const label = ctx.title || ctx.cwrid.slice(0, 8);
if (!quiet) pushLog(`[${label}] \u540e\u53f0\u9884\u70ed\u64ad\u653e\u9875\uff08\u5237\u65b0\u767b\u5f55\u6001\uff09…`);
const url = buildPolyvPlayUrl(cwid);
let page;
try {
page = await fetchPolyvPlayPage(url, { __hyEngineMark: true });
} catch (_) {
page = { ok: false, text: "" };
}
if (page.surveyRedirect || isCmeSurveyRedirectUrl(page.url || "")) {
return handleSurveyGateManual(ctx, page.url || "");
}
let text = page.text || "";
let finalUrl = page.url || url;
if (isHyVideoFaceRequiredPage(text, finalUrl, page)) {
const verified = await ensureHyVideoFaceVerified(ctx, quiet);
if (!verified) return false;
try {
page = await fetchCmeHtmlSafe(url, { __hyEngineMark: true });
} catch (_) {
page = { ok: false, text: "" };
}
if (page.surveyRedirect || isCmeSurveyRedirectUrl(page.url || "")) {
return handleSurveyGateManual(ctx, page.url || "");
}
text = page.text || "";
finalUrl = page.url || url;
if (isHyVideoFaceRequiredPage(text, finalUrl, page)) {
pushLog(`[${label}] \u64ad\u653e\u9875\u4ecd\u8981\u6c42\u4eba\u8138\uff0c\u8bf7\u626b\u7801\u5b8c\u6210\u8ba4\u8bc1`);
return false;
}
}
if (isPageLoginRedirect(text)) {
warnLoginIfNeeded(quiet);
return false;
}
// HTML 内嵌问卷链(未走 302 时)
{
const embedded = extractDcwjSurveyUrl(text);
if (embedded) return handleSurveyGateManual(ctx, embedded);
}
if (!page.ok) {
if (isHyVideoFaceRequiredPage(text, finalUrl, page)) {
pushLog(`[${label}] \u64ad\u653e\u9875\u8981\u6c42\u770b\u8bfe\u524d\u4eba\u8138\uff0c\u5df2\u62e6\u622a\u8fdb\u5ea6\u63d0\u4ea4`);
return false;
}
const discovered = discoverSurveyFromPerformance(cwid) || extractDcwjSurveyUrl(text);
if (page.surveyRedirect || isCmeSurveyRedirectUrl(page.url || "") || discovered) {
return handleSurveyGateManual(ctx, page.url || discovered || "");
}
// 关键:预热失败时「继续 API」会跳过课前问卷;离线/批量一律停学让用户手动完成问卷
if (/course_ware_polyv\.aspx/i.test(url)) {
return handleSurveyGateManual(ctx, page.url || discovered || "");
}
if (!quiet) pushLog(`[${label}] \u64ad\u653e\u9875\u9884\u70ed\u5931\u8d25`);
return false;
}
if (!quiet) await sleep(800);
return true;
}
async function ensureHyVideoFaceVerified(ctx, quiet) {
const preKey = ctxVideoFaceKey(ctx);
if (!preKey) return true;
if (isHyVideoFaceVerified(preKey)) return true;
if (hyVideoFaceVerifyPromises.has(preKey)) return hyVideoFaceVerifyPromises.get(preKey);
const task = (async function () {
const enriched = await resolveCoaidForFace(ctx);
const fc = resolveHyFaceCtx(Object.assign({}, enriched, { ff: "1" }));
if (!fc.cwrid) return true;
const key = String(fc.cwrid).trim().toLowerCase();
if (isHyVideoFaceVerified(key)) return true;
if (!fc.coaid) {
const label = ctx.title || fc.cwrid.slice(0, 8);
pushUserLog(`${label} · \u770b\u8bfe\u524d\u4eba\u8138\uff1a\u7f3a\u5c11\u673a\u6784\u53c2\u6570\uff0c\u8bf7\u6253\u5f00\u8bfe\u7a0b\u7ae0\u8282\u9875\u540e\u70b9\u300c\u5f00\u59cb\u300d\u91cd\u8bd5`);
pushLog(`[${label}] \u770b\u8bfe\u524d\u4eba\u8138\uff1acoaid \u4e3a\u7a7a\uff0c\u65e0\u6cd5\u62c9\u53d6\u4e8c\u7ef4\u7801`);
return false;
}
injectHyPanelStyles();
const label = ctx.title || fc.cwrid.slice(0, 8);
if (!quiet) pushUserLog(`${label} · \u770b\u8bfe\u524d\u9700\u4eba\u8138\u8bc6\u522b\uff0c\u8bf7\u7528\u638c\u4e0a\u534e\u533b APP \u626b\u7801`);
showHyExamFaceModalLoading(label);
const face = await waitHyFaceVerify(Object.assign({}, enriched, { ff: "1" }), label, "video");
if (face.ok) {
markHyVideoFaceVerified(fc.cwrid);
return true;
}
pushLog(`[${label}] \u770b\u8bfe\u524d\u4eba\u8138: ${face.msg || "\u672a\u901a\u8fc7"}`);
return false;
})();
hyVideoFaceVerifyPromises.set(preKey, task);
try {
return await task;
} finally {
hyVideoFaceVerifyPromises.delete(preKey);
}
}
function postProcess(ctx, status, sec) {
if (HY_ENGINE_REQUIRED) {
return Promise.reject(new Error("HY_ENGINE_ONLY"));
}
const sessionUid = currentSessionUserId();
if (sessionUid && ctx.uid && !userIdsMatch(sessionUid, ctx.uid)) {
trace(
`postProcess \u62e6\u622a\uff1asession=${sessionUid} job.uid=${ctx.uid} status=${status} cwrid=${ctx.cwrid}`
);
purgeStaleJobsForSessionUid(sessionUid);
if (getEnabled()) stopAutoStudy("\u7528\u6237ID\u4e0e\u5f53\u524d\u767b\u5f55\u4e0d\u4e00\u81f4\uff0c\u5df2\u505c\u6b62");
return Promise.resolve({ code: -3, msg: "\u7528\u6237id\u4e0d\u5339\u914d(\u672c\u5730\u4efb\u52a1\u4e0e\u5f53\u524d\u767b\u5f55\u4e0d\u4e00\u81f4)" });
}
return apiFetch("/ashx/addCourseWareProcess.ashx?r=" + Math.random(), {
group_id: ctx.groupId,
course_organ_assign_id: ctx.coaid,
relation_id: ctx.cwrid,
user_id: ctx.uid,
province_id: ctx.provinceId,
player_current_time: Math.floor(sec),
course_id: ctx.cid,
status,
}).then((data) => {
traceProgressPostResult(ctx, data, `status=${status}@${sec}`);
return data;
});
}
function postPlayRecord(ctx, sec) {
if (HY_ENGINE_REQUIRED) {
return Promise.reject(new Error("HY_ENGINE_ONLY"));
}
return apiGet("/ashx/addCourseWarePlayRecord.ashx", {
relation_id: ctx.cwrid,
user_id: ctx.uid,
group_id: ctx.groupId,
player_current_time: Math.floor(sec),
});
}
function syncLocal(ctx, sec) {
try {
const studied = Math.max(0, Math.floor(sec || 0));
localStorage.setItem(ctx.lsKey, String(studied));
const row = document.querySelector(
`.cw-progress-row[data-cwrid="${ctx.cwrid}"][data-coaid="${ctx.coaid}"]`
);
const total =
(row && parseInt(row.getAttribute("data-totaltime") || "0", 10)) ||
ctx.duration ||
3600;
if (row) row.setAttribute("data-maxtime", String(studied));
updateDomChapterProgress(ctx.cwrid, ctx.coaid, studied, total);
syncPanelChapterProgress(ctx, studied, total);
} catch (_) {}
}
function updateDomChapterProgress(cwrid, coaid, sec, total) {
const row = document.querySelector(
`.cw-progress-row[data-cwrid="${cwrid}"][data-coaid="${coaid}"]`
);
if (!row) return;
const pct = formatPlayProgressPct(sec, total);
const courseEl = row.closest(".course[data-href]");
if (!courseEl) return;
const pt =
courseEl.querySelector(".play_process .cw-progress-text") ||
courseEl.querySelector(".cw-progress-text") ||
(row.classList.contains("play_process") ? row : null);
if (pt) {
pt.textContent = pct >= 100 ? "100%" : `${pct}%`;
}
}
function syncPanelChapterProgress(ctx, sec, total) {
const label =
formatPlayProgressPct(sec, total) >= 100 ? "\u64ad\u653e\u81f3\uff1a100%" : formatProgressLabelFromSec(sec, total);
const patch = (ch) => {
if (!ch || ch.lsKey !== ctx.lsKey) return;
ch.progressLabel = label.replace("\u89c6\u9891\u5df2\u5b66\u5b8c", "\u64ad\u653e\u81f3\uff1a100%");
ch.studiedSec = sec;
if (formatPlayProgressPct(sec, total) < 100) {
ch.done = false;
ch.studyState = "\u5b66\u4e60\u4e2d";
}
};
panelState.courses.forEach((c) => (c.chapters || []).forEach(patch));
panelState.chapterPreview.forEach((c) => (c.chapters || []).forEach(patch));
if (document.getElementById("hy-cme-chapter-preview")) renderChapterPreview();
}
function engineCtxFromJob(job) {
if (!job) return {};
return {
uid: job.uid,
cwrid: job.cwrid,
coaid: job.coaid,
cid: job.cid,
cwid: job.cwid || job.cwrid,
wareCwid: job.wareCwid || "",
groupId: job.groupId,
provinceId: job.provinceId,
duration: job.duration || 3600,
startSec: job.startSec,
studiedSec: job.studiedSec,
title: job.title,
lsKey: job.lsKey,
};
}
async function _es(kind, context, cfg) {
await _cl(false);
const data = await _hr("/api/huayi/study/engine/start", "POST", {
kind: String(kind || ""),
context: context || {},
config: cfg || {},
});
syncHyCloudQuotaFromResponse(data);
return data;
}
async function _ep(sessionId, event, lastResult, contextPatch) {
await _cl(false);
const data = await _hr("/api/huayi/study/engine/step", "POST", {
session_id: String(sessionId || ""),
event: String(event || "tick"),
last_result: lastResult == null ? null : lastResult,
context_patch: contextPatch == null ? null : contextPatch,
});
syncHyCloudQuotaFromResponse(data);
return data;
}
async function executeHyEngineCaptcha(cmd) {
const examId = cmd.exam_id || "";
const html = cmd.page_html || "";
const tag = examId.slice(0, 8);
const cap = await tryPassHyExamCaptcha(examId, tag, html, cmd.referer || "");
return { code: cap.ok ? cap.code || "ok" : "", ok: !!cap.ok, msg: cap.msg || "" };
}
function assertHyAllowedEnginePath(pathOrUrl) {
const raw = String(pathOrUrl || "").trim();
if (!raw) throw new Error("engine_path_empty");
if (raw.startsWith("/") && !raw.startsWith("//")) return raw;
let host = "";
try {
host = new URL(raw, location.href).hostname || "";
} catch (_) {
throw new Error("engine_path_invalid");
}
if (!/(^|\.)91huayi\.com$/i.test(host)) {
throw new Error("engine_host_blocked");
}
return raw;
}
async function _xc(cmd, ctx) {
const c = cmd || {};
const t = String(c.type || "");
if (t === "wait") {
return { pause: true, ms: Math.max(500, Number(c.ms || 1000)) };
}
if (t === "done") {
return {
terminal: true,
ok: c.success !== false,
skipped: !!c.skipped,
score: c.score,
studied_sec: c.studied_sec,
early_finish: !!c.early_finish,
filled: !!c.filled,
};
}
if (t === "failed") {
return { terminal: true, ok: false, msg: c.message || "failed" };
}
if (t === "local_sync") {
const sec = Math.max(0, Number(c.sec || 0));
if (ctx) {
ensureRtWallClock(ctx);
syncLocal(ctx, sec);
saveEnginePostedSec(ctx, sec);
}
return { event: "local_sync_done", lastResult: { ok: true, sec } };
}
if (t === "document_fetch") {
const path = assertHyAllowedEnginePath(c.path || c.url || "");
const page = await fetchCmeHtmlSafe(path, { __hyEngineMark: true });
const text = page.text || "";
const finalUrl = page.url || path;
if (page.surveyRedirect || isCmeSurveyRedirectUrl(finalUrl) || extractDcwjSurveyUrl(text + " " + finalUrl)) {
const surveyUrl = extractDcwjSurveyUrl(finalUrl) || extractDcwjSurveyUrl(text) || finalUrl;
handleSurveyGateManual(ctx, surveyUrl);
return { terminal: true, ok: false, msg: "need_survey", needSurvey: true };
}
if (isHyVideoFaceRequiredPage(text, finalUrl, page)) {
return {
pause: true,
needVideoFace: true,
ms: 2000,
event: c.event || "warm_result",
lastResult: { text, status: 403, url: finalUrl, need_video_face: true },
};
}
if (isPageLoginRedirect(text)) warnLoginIfNeeded(true);
return {
event: c.event || "submit_result",
lastResult: {
text,
status: isPageLoginRedirect(text) ? 401 : page.ok ? 200 : 404,
url: finalUrl,
},
};
}
if (t === "platform_post") {
const postPath = assertHyAllowedEnginePath(c.path || "");
if (ctx && PROGRESS_URL_RE.test(String(postPath || ""))) {
const faceOk = await ensureProgressFaceAllowed(ctx);
if (!faceOk) {
return {
pause: true,
needVideoFace: true,
ms: 2000,
event: c.event || "process_result",
lastResult: { text: "", status: 403, need_video_face: true, url: postPath },
};
}
}
if (typeof c.body === "string") {
const res = await fetch(postPath, {
method: "POST",
credentials: "include",
headers: Object.assign({ "Content-Type": "application/x-www-form-urlencoded" }, c.headers || {}),
body: c.body,
__hyEngineMark: true,
});
const text = await res.text();
const data = parseApiResponse(text);
return { event: c.event || "process_result", lastResult: { text, data, status: res.status, url: postPath } };
}
const data = await apiFetch(postPath, c.payload || {});
if (ctx && c.payload && c.payload.player_current_time != null) {
traceProgressPostResult(ctx, data, c.label || "platform_post");
const sessionUid = currentSessionUserId();
const jobUid = String((c.payload && c.payload.user_id) || ctx.uid || "").trim();
if (sessionUid && jobUid && !userIdsMatch(sessionUid, jobUid)) {
trace(`engine platform_post uid\u4e0d\u4e00\u81f4 session=${sessionUid} job=${jobUid}`);
purgeStaleJobsForSessionUid(sessionUid);
if (getEnabled()) stopAutoStudy("\u7528\u6237ID\u4e0e\u5f53\u524d\u767b\u5f55\u4e0d\u4e00\u81f4\uff0c\u5df2\u505c\u6b62");
return {
event: c.event || "process_result",
lastResult: {
text: JSON.stringify({ code: -3, msg: "\u7528\u6237id\u4e0d\u5339\u914d" }),
data: { code: -3, msg: "\u7528\u6237id\u4e0d\u5339\u914d" },
status: 200,
url: postPath,
},
};
}
const code = data && data.code;
if (code === 0 || code === 3) saveEnginePostedSec(ctx, c.payload.player_current_time);
}
return {
event: c.event || "process_result",
lastResult: { text: JSON.stringify(data), data, status: 200, url: postPath },
};
}
if (t === "platform_get") {
const getPath = assertHyAllowedEnginePath(c.path || "");
const data = await apiGet(getPath, c.params || {});
return {
event: c.event || "play_record_result",
lastResult: { text: JSON.stringify(data), data, status: 200, url: getPath },
};
}
if (t === "captcha") {
const cap = await executeHyEngineCaptcha(c);
return { event: c.event || "captcha_result", lastResult: cap };
}
return { event: "tick", lastResult: null };
}
async function _ve(startRes, options) {
const opts = options || {};
const ctx = opts.ctx;
let sessionId = String((startRes && startRes.session_id) || "");
let cmd = startRes && startRes.command;
const maxSteps = Number(opts.maxSteps) || 64;
trace(startRes && startRes.log);
for (let i = 0; i < maxSteps; i++) {
if (!cmd) {
const next = await _ep(sessionId, "tick", null, null);
sessionId = String(next.session_id || sessionId);
trace(next.log);
cmd = next.command;
}
if (!cmd) break;
if (cmd.type === "wait") {
return { pause: true, sessionId, ms: Math.max(500, Number(cmd.ms || 1000)), ok: true };
}
const out = await _xc(cmd, ctx);
if (out.pause) return Object.assign({ sessionId }, out);
if (out.terminal) return Object.assign({ sessionId }, out);
const next = await _ep(sessionId, out.event, out.lastResult, null);
sessionId = String(next.session_id || sessionId);
trace(next.log);
cmd = next.command;
}
return { terminal: true, ok: false, msg: "\u4e91\u7aef\u5f15\u64ce\u6b65\u6570\u8d85\u9650", sessionId };
}
async function _vf(job, kind, cfg, options) {
const ctx = engineCtxFromJob(job);
const opts = Object.assign({ ctx, maxSteps: 64 }, options || {});
let sessionId = job.engineSessionId || "";
let startRes;
if (sessionId) {
startRes = { session_id: sessionId, command: null };
} else {
startRes = await _es(kind, ctx, cfg || {});
sessionId = String(startRes.session_id || "");
saveJob(jobToCtx(job), { engineSessionId: sessionId });
}
return _ve(startRes, opts);
}
function applyHyEnginePause(ctx, loopOut) {
if (!loopOut || !loopOut.pause) return;
ensureRtWallClock(ctx);
const ms = loopOut.ms || RT_STEP_GAP_MS;
saveJob(ctx, {
engineSessionId: loopOut.sessionId || "",
nextStepDue: Date.now() + ms,
});
if (ms >= RT_STEP_GAP_MS * 0.85) {
const job = getJob(ctx) || ctx;
pushUserChapterStudyLog(job, ms);
if (isGongxuCtx(ctx, job)) pushGongxuStopStartUserTip(job.title || labelFromJob(job));
}
}
function handleHyEngineFailure(ctx, job, label, loopOut, tag) {
const now = Date.now();
const msg = String(loopOut?.msg || "\u5931\u8d25");
if (/free_quota_exhausted|\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c/i.test(msg)) {
syncHyCloudQuotaFromResponse({ tier: "free", free_used_chapters: cloudState.freeChapterLimit });
updatePanelCloudStatus();
pushUserLog(`\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08${cloudState.freeUsedChapters}/${cloudState.freeChapterLimit} \u8282\uff09\uff0c\u8bf7\u5347\u7ea7 Pro`);
stopAutoStudy("\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff0c\u7a0b\u5e8f\u5df2\u505c\u6b62");
return;
}
const isLoginLost = /login_lost/i.test(msg);
if (isLoginLost) {
warnLoginIfNeeded(false);
pushLog(`[${label}] ${tag} \u767b\u5f55\u6001\u5f02\u5e38\uff0c\u5df2\u6e05\u9664\u4f1a\u8bdd · \u8bf7 F5 \u5237\u65b0\u540e\u81ea\u52a8\u91cd\u8bd5`);
pushUserLog(`${label} · \u767b\u5f55\u6001\u5f02\u5e38\uff0c\u8bf7\u5237\u65b0\u9875\u9762`);
} else {
pushLog(`[${label}] ${tag} \u4e91\u7aef\u5f15\u64ce\uff1a${msg}`);
}
saveJob(ctx, {
nextStepDue: now + (isLoginLost ? 120000 : 60000),
engineSessionId: "",
});
}
async function handleHyEngineVideoDone(ctx, job, label, loopOut) {
const studiedSec = loopOut.studied_sec != null ? loopOut.studied_sec : job.duration;
saveJob(ctx, { engineSessionId: "", nextStepDue: 0, progressFilled: true, status2Sent: true, lastPostedSec: studiedSec });
const fin = await finalizeVideoChapterDone(ctx, job, label, studiedSec, "");
if (fin && fin.examResultPending && isGongxuCtx(ctx, job)) {
await tryUnlockGongxuChapter(ctx, job, label);
return false;
}
return true;
}
function applyLiveProgressToChapter(ch) {
const jobs = loadJobs();
const j = jobs[ch.lsKey];
if (!j || j.phase === "done") return ch;
const sec =
resolveWallClockStudiedSec(j) ||
(j.lastPostedSec != null
? j.lastPostedSec
: j.rtEntrySec != null
? j.rtEntrySec
: j.startSec);
if (sec == null || sec <= 0) return ch;
const dur = j.duration || ch.duration || 3600;
const pct = formatPlayProgressPct(sec, dur);
return Object.assign({}, ch, {
progressLabel: pct >= 100 ? "\u64ad\u653e\u81f3\uff1a100%" : `\u64ad\u653e\u81f3\uff1a${pct}%`,
studiedSec: sec,
done: pct >= 100 ? ch.done : false,
});
}
function startPreviewAutoRefresh() {
stopPreviewAutoRefresh();
panelState.previewRefreshTimer = setInterval(() => {
if (!getEnabled()) return;
if (pageType() === "course") {
const chapters = parseCourseChapterJobs();
chapters.forEach((ch) => {
const live = applyLiveProgressToChapter(ch);
syncPanelChapterProgress(
{ lsKey: ch.lsKey, cwrid: ch.cwrid, coaid: ch.coaid, duration: ch.duration },
live.studiedSec != null ? live.studiedSec : readChapterStudiedSec(ch.uid, ch.cwrid, ch.coaid, ch.duration),
ch.duration
);
});
}
renderChapterPreview();
}, 5000);
}
function stopPreviewAutoRefresh() {
if (panelState.previewRefreshTimer) {
clearInterval(panelState.previewRefreshTimer);
panelState.previewRefreshTimer = null;
}
}
async function postProcessOnce(ctx, status, sec) {
try {
return await postProcess(ctx, status, sec);
} catch (e) {
return { code: -1, msg: e && e.message ? e.message : String(e) };
}
}
function isRtIgnorableCode3(res) {
return !!(res && res.code === 3);
}
function handleRealtimeApiMiss(job, ctx, res, label, tag, stepGapMs) {
const now = Date.now();
const gap = stepGapMs || RT_STEP_GAP_MS;
const code = res && res.code != null ? res.code : "?";
const msg = res && res.msg ? " " + res.msg : "";
if (res && res.code === -2) {
warnLoginIfNeeded(false);
pushLog(`[${label}] ${tag} \u767b\u5f55\u5931\u6548\uff0c\u8bf7 F5 \u5237\u65b0\u540e\u81ea\u52a8\u91cd\u8bd5${msg}`);
saveJob(ctx, { nextStepDue: now + 60000 });
return false;
}
if (isRtIgnorableCode3(res)) {
pushLog(
`[${label}] ${tag} code=3 \u9650\u6d41\uff08\u5df2\u5ffd\u7565\uff09\uff0c${Math.round(gap / 60000)}\u5206\u949f\u540e\u6309\u65e2\u5b9a\u95f4\u9694\u7ee7\u7eed`
);
saveJob(ctx, { nextStepDue: now + gap });
return false;
}
pushLog(`[${label}] ${tag} code=${code}${msg}\uff0c${Math.round(gap / 60000)}\u5206\u949f\u540e\u91cd\u8bd5`);
saveJob(ctx, { nextStepDue: now + gap });
return false;
}
function markRtEntered(job, ctx, entrySec, duration, now, code3Ignored) {
saveJob(ctx, {
rtEntered: true,
rtStartedAt: now,
rtEntrySec: entrySec,
lastPostedSec: entrySec,
nextStepDue: now + RT_STEP_GAP_MS,
rtStepCount: 1,
});
const suffix = code3Ignored ? " code=3 \u9650\u6d41\uff08\u5df2\u5ffd\u7565\uff09" : "";
pushLog(
`[${labelFromJob(job)}] [1:1] \u8fdb\u5165 status=0@${entrySec}s${suffix}\uff0c${formatRtStudyLine(entrySec, duration)}\uff0c${RT_STEP_SEC / 60}\u5206\u949f\u540e status=1`
);
}
function labelFromJob(job) {
return job.title || (job.cwrid || "").slice(0, 8);
}
async function postProcessCode3Retry(ctx, status, sec) {
return postProcessOnce(ctx, status, sec);
}
async function instantFillToFull(ctx, job) {
const label = ctx.title || ctx.cwrid.slice(0, 8);
const start = ctx.baseSec || (job && job.startSec) || 1;
const duration = ctx.duration;
const warmed = await warmPlaySession(ctx, true);
if (!warmed) {
pushLog(`[${label}] \u9884\u70ed\u5931\u8d25\uff0c\u8bf7\u5148 F5 \u767b\u5f55`);
return false;
}
await sleep(800);
const r0 = await postProcessCode3Retry(ctx, 0, start);
if (!r0 || r0.code !== 0) {
pushLog(
`[${label}] \u8fdb\u5165 status=0 code=${r0 && r0.code != null ? r0.code : "?"}${r0 && r0.msg ? " " + r0.msg : ""}`
);
if (!r0 || r0.code === 3) return false;
}
await sleep(500);
const r2 = await postProcessCode3Retry(ctx, 2, duration);
pushLog(
`[${label}] \u6ee1\u8fdb\u5ea6 status=2@${duration}s code=${r2 && r2.code != null ? r2.code : "?"}${r2 && r2.msg ? " " + r2.msg : ""}`
);
if (r2 && r2.code === 0) {
syncLocal(ctx, duration);
return true;
}
return false;
}
function formatOfflineFillUserLine(job, index, total) {
const course = job.cid ? findPanelCourseByCid(job.cid) : null;
const coursePrefix = course?.title ? `\u300c${course.title}\u300d` : "";
const chapter = resolveChapterDisplayTitle(job);
return `\u6279\u91cf\u62c9\u6ee1\u4e2d ${index}/${total} · ${coursePrefix}${chapter}`;
}
async function fillChapterSegmentsToFull(ctx, job, fillProgress) {
const label = ctx.title || ctx.cwrid.slice(0, 8);
if (fillProgress) {
pushUserLog(formatOfflineFillUserLine(job, fillProgress.index, fillProgress.total));
}
if (!HY_ENGINE_REQUIRED) {
return { ok: false };
}
if (!(await requireHyVideoFaceIfNeeded(ctx, label))) {
return { ok: false, needFace: true };
}
if (!(await warmPlaySession(ctx, false))) {
if (!getEnabled()) {
return { ok: false, needSurvey: true };
}
const cwid = ctx.cwrid || ctx.cwid || "";
const discovered = discoverSurveyFromPerformance(cwid);
if (discovered) {
handleSurveyGateManual(ctx, discovered);
return { ok: false, needSurvey: true };
}
if (isOfflineMode()) {
handleSurveyGateManual(ctx, "");
return { ok: false, needSurvey: true };
}
const needFace = await probeHyVideoFaceNeed(ctx).catch(function () {
return false;
});
pushLog(
`[${label}] \u79bb\u7ebf\u62c9\u6ee1\uff1a\u64ad\u653e\u9875\u9884\u70ed\u5931\u8d25${needFace ? "\uff08\u9700\u770b\u8bfe\u524d\u4eba\u8138\uff09" : ""}`
);
return { ok: false, needFace: !!needFace };
}
try {
let liveJob = Object.assign({}, job, loadJobs()[jobKey(ctx)] || {});
let sessionId = String(liveJob.engineSessionId || "");
const maxRounds = 48;
for (let round = 0; round < maxRounds && getEnabled(); round++) {
const loop = await _vf(
liveJob,
"video_offline_fill",
{ mode: "offline" },
{ ctx, maxSteps: 32 }
);
sessionId = String(loop.sessionId || sessionId || "");
if (loop.needVideoFace) {
saveJob(ctx, { engineSessionId: sessionId });
injectHyPanelStyles();
const verified = await ensureHyVideoFaceVerified(ctx, false);
if (!verified) {
return { ok: false, needFace: true };
}
liveJob = Object.assign({}, liveJob, loadJobs()[jobKey(ctx)] || {}, { engineSessionId: "" });
continue;
}
if (loop.pause) {
if (loop.needVideoFace) {
saveJob(ctx, { engineSessionId: sessionId });
injectHyPanelStyles();
const verified = await ensureHyVideoFaceVerified(ctx, false);
if (!verified) {
return { ok: false, needFace: true };
}
liveJob = Object.assign({}, liveJob, loadJobs()[jobKey(ctx)] || {}, { engineSessionId: "" });
continue;
}
saveJob(ctx, { engineSessionId: sessionId, phase: "silent_filling" });
const waitMs = Math.max(500, Number(loop.ms || 1000));
await sleep(waitMs);
liveJob = Object.assign({}, liveJob, loadJobs()[jobKey(ctx)] || {}, {
engineSessionId: sessionId,
});
continue;
}
if (loop.terminal && loop.ok) {
if (isGongxuCtx(ctx, liveJob)) {
await submitChapterProcessExams(ctx, liveJob, label);
}
saveJob(ctx, { engineSessionId: "" });
return { ok: true };
}
if (loop.terminal && !loop.ok) {
if (loop.needSurvey || /need_survey/i.test(String(loop.msg || ""))) {
saveJob(ctx, { engineSessionId: "" });
return { ok: false, needSurvey: true };
}
handleHyEngineFailure(ctx, liveJob, label, loop, "[\u79bb\u7ebf]");
saveJob(ctx, { engineSessionId: "" });
return { ok: false };
}
if (!loop.ok) {
pushLog(`[${label}] [\u79bb\u7ebf] \u4e91\u7aef\u62c9\u6ee1\u5931\u8d25\uff1a${loop.msg || "\u672a\u77e5"}`);
saveJob(ctx, { engineSessionId: "" });
return { ok: false };
}
liveJob = Object.assign({}, liveJob, loadJobs()[jobKey(ctx)] || {}, {
engineSessionId: sessionId,
});
}
pushLog(`[${label}] [\u79bb\u7ebf] \u4e91\u7aef\u62c9\u6ee1\u6b65\u6570\u8d85\u9650`);
return { ok: false };
} catch (e) {
pushLog(`[${label}] [\u79bb\u7ebf] \u4e91\u7aef\u5f15\u64ce\uff1a${e && e.message ? e.message : e}`);
return { ok: false };
}
}
function getNextRtStep(last, duration) {
const step = RT_STEP_SEC;
const next = last + step;
const tail = duration % step;
if (last >= duration - 2) return { type: "done" };
if (tail > 0 && next >= duration - tail) {
return {
type: "finish",
sec: duration,
status: 2,
gapMs: Math.max(30000, (duration - last) * 1000),
final: true,
};
}
if (next >= duration - 2) {
return {
type: "finish",
sec: duration,
status: 2,
gapMs: Math.max(30000, (duration - last) * 1000),
final: true,
};
}
return { type: "progress", sec: next, status: 1, gapMs: RT_STEP_GAP_MS };
}
function formatRtStudyLine(studiedSec, totalSec) {
const studied = Math.max(0, Math.round((studiedSec || 0) / 60));
const total = Math.max(1, Math.round((totalSec || 0) / 60));
return `\u5df2\u5b66 ${studied}\u5206\u949f/\u603b${total}\u5206\u949f`;
}
function countRtPlanSteps(duration, start) {
let last = start;
let n = 1;
for (;;) {
const s = getNextRtStep(last, duration);
if (s.type === "done") break;
n++;
if (s.final) break;
last = s.sec;
}
return n;
}
function planRealtimeCourse(job) {
const duration = job.duration || 3600;
const studied =
job.studiedSec != null
? job.studiedSec
: readChapterStudiedSec(job.uid, job.cwrid, job.coaid, duration);
const entrySec =
job.startSec != null ? job.startSec : resolveRtEntrySec(studied, duration);
return {
stepCount: countRtPlanSteps(duration, entrySec),
stepGapMs: RT_STEP_GAP_MS,
courseMin: Math.round(duration / 60),
entrySec,
studiedSec: studied,
};
}
function getRealtimeFinishAt(job) {
if (!job || !job.rtStartedAt) return 0;
return job.rtStartedAt + (job.duration || 3600) * 1000;
}
function recalcOfflineBatchDue(jobs, sessionUid) {
const st = loadSilentState();
const sessionStart = Number(st.sessionStart || 0);
if (!sessionStart) return 0;
const clocked = Object.values(jobs).filter(function (j) {
return j && jobBelongsToSession(j, sessionUid) && j.phase === "silent_clock";
});
const totalMs = clocked.reduce(function (s, j) {
return s + (j.duration || 3600) * 1000;
}, 0);
if (!totalMs) return 0;
const batchCompleteDue = sessionStart + totalMs + offlineBatchBufferMs(st);
patchSilentState({
totalMs: totalMs,
totalSec: Math.round(totalMs / 1000),
totalCourses: clocked.length,
batchCompleteDue: batchCompleteDue,
beijingDue: formatBeijingDateTime(batchCompleteDue),
filledAt: st.filledAt || Date.now(),
sessionUid: sessionUid,
});
assignPerCourseOfflineDue(jobs, sessionUid);
return totalMs;
}
function assignSilentBatchClock(jobs) {
const now = Date.now();
const sessionUid = getLearningUserId();
const st = loadSilentState();
const sessionStart = Number(st.sessionStart || now);
const filled = Object.entries(jobs).filter(function (entry) {
const j = entry[1];
return j && j.phase === "silent_filled" && jobBelongsToSession(j, sessionUid);
});
if (!filled.length && !Object.values(jobs).some(function (j) {
return j && jobBelongsToSession(j, sessionUid) && j.phase === "silent_clock";
})) {
return 0;
}
filled.forEach(function (entry) {
const key = entry[0];
const job = entry[1];
job.phase = "silent_clock";
job.progressFilled = true;
job.status2Sent = true;
job.lastPostedSec = job.duration;
jobs[key] = job;
try {
syncLocal(jobToCtx(job), job.duration);
} catch (_) {}
});
if (!st.sessionStart) {
patchSilentState({ sessionStart: sessionStart });
}
saveJobs(jobs);
assignPerCourseOfflineDue(jobs, sessionUid);
return recalcOfflineBatchDue(jobs, sessionUid);
}
function readPanelCollapsed() {
return localStorage.getItem(PANEL_COLLAPSED_KEY) === "1";
}
function writePanelCollapsed(collapsed) {
localStorage.setItem(PANEL_COLLAPSED_KEY, collapsed ? "1" : "0");
}
function readPanelPos() {
try {
return JSON.parse(localStorage.getItem(PANEL_POS_KEY) || "{}");
} catch (_) {
return null;
}
}
function writePanelPos(left, top) {
localStorage.setItem(PANEL_POS_KEY, JSON.stringify({ left, top }));
}
function clampPanelInViewport(panel) {
if (!panel) return;
const rect = panel.getBoundingClientRect();
const maxLeft = Math.max(0, window.innerWidth - panel.offsetWidth);
const maxTop = Math.max(0, window.innerHeight - panel.offsetHeight);
const left = Math.max(0, Math.min(maxLeft, rect.left));
const top = Math.max(0, Math.min(maxTop, rect.top));
panel.style.right = "auto";
panel.style.bottom = "auto";
panel.style.left = `${left}px`;
panel.style.top = `${top}px`;
writePanelPos(left, top);
}
function applyPanelCollapsed(panel, collapsed) {
const btnMin = panel.querySelector("#hy-cme-btn-min");
const btnMax = panel.querySelector("#hy-cme-btn-max");
panel.classList.toggle("cme-panel-min", !!collapsed);
panel.classList.toggle("cme-panel-max", !collapsed);
if (btnMin) btnMin.style.display = collapsed ? "none" : "";
if (btnMax) btnMax.style.display = collapsed ? "" : "none";
const footerExtra = panel.querySelector(".cme-footer-extra");
if (footerExtra) footerExtra.style.display = collapsed ? "none" : "";
clampPanelInViewport(panel);
}
function enablePanelDrag(panel) {
const header = panel.querySelector("#hy-cme-panel-header");
if (!header) return;
let dragging = false;
let startX = 0;
let startY = 0;
let startLeft = 0;
let startTop = 0;
header.addEventListener("mousedown", (e) => {
if (e.target?.closest("#hy-cme-panel-controls, .cme-panel-ctl")) return;
dragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = panel.getBoundingClientRect();
startLeft = rect.left;
startTop = rect.top;
panel.style.right = "auto";
panel.style.bottom = "auto";
e.preventDefault();
});
document.addEventListener("mousemove", (e) => {
if (!dragging) return;
const left = Math.max(0, Math.min(window.innerWidth - panel.offsetWidth, startLeft + (e.clientX - startX)));
const top = Math.max(0, Math.min(window.innerHeight - panel.offsetHeight, startTop + (e.clientY - startY)));
panel.style.left = `${left}px`;
panel.style.top = `${top}px`;
});
document.addEventListener("mouseup", () => {
if (!dragging) return;
dragging = false;
const rect = panel.getBoundingClientRect();
writePanelPos(rect.left, rect.top);
});
}
function switchPanelTab(name) {
document.querySelectorAll("#hy-cme-auto-panel .cme-tab-btn").forEach((el) => {
el.classList.toggle("active", el.dataset.tab === name);
});
document.querySelectorAll("#hy-cme-auto-panel .cme-pane").forEach((el) => {
el.classList.toggle("active", el.dataset.pane === name);
});
}
async function clearJobQueue() {
const jobs = loadJobs();
const keys = Object.keys(jobs);
if (!keys.length) {
pushLog("\u961f\u5217\u5df2\u662f\u7a7a\u7684");
return;
}
const active = keys.filter((k) => jobs[k] && jobs[k].phase !== "done").length;
const message = active
? `\u5f53\u524d\u6709 ${active} \u8282\u8fdb\u884c\u4e2d\u3001\u5171 ${keys.length} \u6761\u4efb\u52a1\u3002\n\u6e05\u7a7a\u540e\u4e0d\u5f71\u54cd\u7f51\u7ad9\u4e0a\u7684\u5b66\u4e60\u8bb0\u5f55\uff0c\u662f\u5426\u7ee7\u7eed\uff1f`
: `\u5171 ${keys.length} \u6761\u4efb\u52a1\u5c06\u88ab\u5220\u9664\uff0c\u662f\u5426\u7ee7\u7eed\uff1f`;
const ok = await askHyPanelConfirm({
title: "\u6e05\u7a7a\u961f\u5217",
message,
okText: "\u6e05\u7a7a\u961f\u5217",
cancelText: "\u53d6\u6d88",
});
if (!ok) return;
saveJobs({});
resetSilentState();
panelState.lastCourseListKey = "";
updateJobsPanel();
updatePanelCloudStatus();
renderCourseList();
void refreshChapterPreview();
pushLog(`\u5df2\u6e05\u7a7a\u961f\u5217\uff08${keys.length} \u6761\u4efb\u52a1\uff09`);
}
function setPanelHint(text) {
const el = document.getElementById("hy-cme-start-hint");
if (!el) return;
if (text) {
el.textContent = String(text);
el.style.display = "block";
} else {
el.textContent = "";
el.style.display = "none";
}
}
function clearPanelHint() {
setPanelHint("");
}
function syncStartStopButtons() {
const running = getEnabled();
const startBtn = document.getElementById("hy-cme-start");
const stopBtn = document.getElementById("hy-cme-stop");
const modeSel = document.getElementById("hy-cme-run-mode");
const clearBtn = document.getElementById("hy-cme-clear-queue");
if (startBtn) {
startBtn.classList.toggle("cme-btn-off", running);
startBtn.disabled = running;
}
if (stopBtn) {
stopBtn.classList.toggle("cme-btn-off", !running);
stopBtn.disabled = !running;
}
if (modeSel) {
if (running) modeSel.disabled = true;
else syncRunModePanel();
}
if (clearBtn) clearBtn.disabled = running;
if (!running) clearLiveStudyStatusBar();
}
let modeHintCollapseTimer = null;
function scheduleModeHintCollapse() {
const hint = document.getElementById("hy-cme-mode-desc");
if (!hint) return;
hint.classList.remove("cme-study-mode-hint-collapsed");
if (modeHintCollapseTimer) clearTimeout(modeHintCollapseTimer);
modeHintCollapseTimer = setTimeout(() => {
hint.classList.add("cme-study-mode-hint-collapsed");
}, 5000);
}
function syncRunModePanel() {
const sel = document.getElementById("hy-cme-run-mode");
const hint = document.getElementById("hy-cme-mode-desc");
const metric = document.getElementById("hy-cme-mode-metric");
ensureRunModeForTier();
const mode = getRunMode();
const pro = canUseOfflineMode();
if (sel) {
const offlineOpt = sel.querySelector('option[value="offline"]');
const realtimeOpt = sel.querySelector('option[value="realtime"]');
if (offlineOpt) {
offlineOpt.disabled = !pro;
offlineOpt.textContent = pro ? "\u79bb\u7ebf\u5f02\u6b65\u6279\u91cf\u6a21\u5f0f" : "\u79bb\u7ebf\u5f02\u6b65\u6279\u91cf\u6a21\u5f0f\uff08Pro \u53ef\u7528\uff09";
}
if (realtimeOpt) {
realtimeOpt.disabled = pro;
realtimeOpt.textContent = pro ? "1:1 \u6302\u673a\uff08Pro \u8bf7\u7528\u79bb\u7ebf\u6279\u91cf\uff09" : "1:1 \u6302\u673a";
}
if (pro && sel.value !== "offline") sel.value = "offline";
else if (sel.value !== mode) sel.value = mode;
if (pro || isProOfflineWaitingNotDue()) sel.disabled = true;
else sel.disabled = !!getEnabled();
}
if (hint) {
const rt = mode === "realtime";
hint.classList.toggle("cme-study-mode-hint-realtime", rt);
hint.classList.toggle("cme-study-mode-hint-offline", !rt);
const title = hint.querySelector(".cme-study-mode-hint-title");
const body = hint.querySelector(".cme-study-mode-hint-body");
if (title) title.textContent = rt ? "1:1\u6302\u673a\u6a21\u5f0f" : "\u79bb\u7ebf\u5f02\u6b65\u6279\u91cf\u6a21\u5f0f";
if (body) body.textContent = RUN_MODE_INTRO[mode] || "";
}
if (metric) metric.textContent = formatRunModeLabel(mode);
const subMetric = document.getElementById("hy-cme-mode-metric-sub");
if (subMetric) subMetric.textContent = formatRunModeLabel(mode);
updateOfflineBatchPanel();
scheduleModeHintCollapse();
}
function updateOfflineBatchPanel() {
const box = document.getElementById("hy-cme-offline-batch");
if (!box) return;
if (!isOfflineMode()) {
box.style.display = "none";
box.textContent = "";
clearOfflinePanelHint();
return;
}
const snap = getOfflineBatchSnapshot();
if (!snap || !snap.filledReady) {
box.style.display = "none";
box.textContent = "";
clearOfflinePanelHint();
return;
}
box.style.display = "";
const remain = snap.due - Date.now();
const totalText = formatDurationCn(snap.totalSec);
const dueText = snap.beijingDue || formatBeijingDateTime(snap.due);
const earlyBtn =
'';
if (snap.ready) {
box.innerHTML = `\u7d2f\u8ba1\u8bfe\u957f ${totalText}\uff08${snap.totalCourses || snap.waitingCount} \u8bfe\uff09+ \u7f13\u51b230\u5206\u949f · \u5df2\u5230\u65f6\u95f4\uff08\u5317\u4eac\u65f6\u95f4 ${dueText}\uff09\uff0c\u8bf7\u70b9\u300c\u5f00\u59cb\u300d\u68c0\u67e5\u6536\u5c3e\u4e0e\u8003\u8bd5 ${earlyBtn}`;
if (!getEnabled()) {
setPanelHint("\u5df2\u5230\u65f6\u95f4\uff0c\u8bf7\u70b9\u300c\u5f00\u59cb\u300d\u68c0\u67e5\u8bfe\u7a0b\u5b8c\u6210\u5e76\u81ea\u52a8\u8003\u8bd5");
}
} else if (snap.partialReady) {
box.innerHTML = `\u7d2f\u8ba1\u8bfe\u957f ${totalText}\uff08${snap.totalCourses || snap.waitingCount} \u8bfe\uff09+ \u7f13\u51b230\u5206\u949f · \u5df2\u6709 ${snap.partialReadyCount} \u95e8\u53ef\u5230\u70b9\u6536\u5c3e · \u5168\u90e8\u5230\u70b9\u5317\u4eac\u65f6\u95f4 ${dueText}\uff08\u7ea6 ${formatEta(remain)}\uff09 ${earlyBtn}`;
if (!getEnabled()) {
setPanelHint(`\u6709 ${snap.partialReadyCount} \u95e8\u8bfe\u5df2\u5230\u53ef\u6536\u5c3e\u65f6\u95f4\uff0c\u53ef\u70b9\u300c\u5f00\u59cb\u300d\u5148\u5b8c\u6210\u5df2\u5230\u70b9\u8bfe\u7a0b`);
}
} else {
box.innerHTML = `\u7d2f\u8ba1\u8bfe\u957f ${totalText}\uff08${snap.totalCourses || snap.waitingCount} \u8bfe\uff09+ \u7f13\u51b230\u5206\u949f · \u8bf7\u4e8e\u5317\u4eac\u65f6\u95f4 ${dueText} \u540e\u518d\u70b9\u300c\u5f00\u59cb\u300d\u6536\u5c3e\uff08\u7ea6 ${formatEta(remain)}\uff09 · \u82e5\u6b64\u524d\u5df2\u770b\u8fc7\u90e8\u5206\u8bfe\u65f6\u53ef ${earlyBtn}`;
if (!getEnabled()) {
const appendN = countSelectedCoursesNeedingOfflineAppend();
if (appendN > 0) {
setPanelHint(`\u7b49\u5f85\u4e2d\uff1b\u53e6\u6709 ${appendN} \u95e8\u65b0\u8bfe\u672a\u62c9\u6ee1\uff0c\u53ef\u70b9\u300c\u5f00\u59cb\u300d\u8865\u6279\u91cf\uff08\u5230\u70b9\u6309\u7d2f\u8ba1\u8bfe\u957f\u91cd\u7b97\uff09`);
} else {
setPanelHint(`\u7ea6 ${formatEta(remain)} \u540e\u53ef\u7ee7\u7eed\uff1b\u53ef\u70b9\u300c\u63d0\u524d\u6536\u5c3e\u68c0\u9a8c\u300d\u6216\u5230\u70b9\u518d\u5f00\u59cb`);
}
}
}
const earlyFinishBtn = box.querySelector("#hy-cme-offline-early-finish");
if (earlyFinishBtn && !earlyFinishBtn.dataset.bound) {
earlyFinishBtn.dataset.bound = "1";
earlyFinishBtn.addEventListener("click", () => void onOfflineEarlyFinishClick());
}
}
function ensureHyConfirmModal() {
let modal = document.getElementById("hy-cme-confirm-modal");
if (modal) return modal;
modal = document.createElement("div");
modal.id = "hy-cme-confirm-modal";
modal.innerHTML = `
`;
document.body.appendChild(modal);
modal.addEventListener("click", (e) => {
if (e.target === modal && modal.__hyConfirmReject) modal.__hyConfirmReject(false);
});
return modal;
}
function askHyPanelConfirm(options) {
const opts = options || {};
const modal = ensureHyConfirmModal();
return new Promise((resolve) => {
const finish = (ok) => {
modal.style.display = "none";
modal.__hyConfirmReject = null;
resolve(!!ok);
};
modal.__hyConfirmReject = finish;
const titleEl = modal.querySelector(".cme-confirm-title");
const bodyEl = modal.querySelector(".cme-confirm-body");
const cancelBtn = modal.querySelector(".cme-confirm-cancel");
const okBtn = modal.querySelector(".cme-confirm-ok");
if (titleEl) titleEl.textContent = opts.title || "\u786e\u8ba4";
if (bodyEl) {
bodyEl.textContent = opts.message || "";
bodyEl.style.whiteSpace = (opts.message || "").includes("\n") ? "pre-line" : "";
}
if (cancelBtn) {
cancelBtn.textContent = opts.cancelText || "\u53d6\u6d88";
cancelBtn.onclick = () => finish(false);
}
if (okBtn) {
okBtn.textContent = opts.okText || "\u786e\u5b9a";
okBtn.onclick = () => finish(true);
}
modal.style.display = "flex";
});
}
function clearStalePanelSelection() {
setQueue([]);
setStudySessionQueue([]);
localStorage.removeItem(QUEUE_TOUCHED_KEY);
panelState.chapterPreview = [];
panelState.selectionLock = null;
}
function getStudySessionQueue() {
try {
return JSON.parse(localStorage.getItem(STUDY_SESSION_QUEUE_KEY) || "[]")
.map(normalizeQueueCid)
.filter(Boolean);
} catch (_) {
return [];
}
}
function setStudySessionQueue(list) {
try {
const next = (list || []).map(normalizeQueueCid).filter(Boolean);
if (!next.length) localStorage.removeItem(STUDY_SESSION_QUEUE_KEY);
else localStorage.setItem(STUDY_SESSION_QUEUE_KEY, JSON.stringify(next));
} catch (_) {}
}
function getEffectiveQueue() {
if (getEnabled()) {
const session = getStudySessionQueue();
if (session.length) return session;
if (panelState.selectionLock && panelState.selectionLock.length) {
return panelState.selectionLock.map(normalizeQueueCid).filter(Boolean);
}
const q = getQueue();
if (q.length) return q;
}
return getQueue();
}
function ensureStudyQueueSelection() {
if (!getEnabled()) return;
const expected = getStudySessionQueue();
if (!expected.length) return;
const current = getQueue();
const a = expected.slice().sort().join(",");
const b = current.slice().sort().join(",");
if (a === b) return;
commitQueueSelection(expected);
}
function pruneQueueToCourses() {
const raw = getEffectiveQueue();
if (!panelState.courses.length) return raw;
const index = new Map();
panelState.courses.forEach((c) => {
const k = normalizeQueueCid(c.cid);
if (k) index.set(k, c.cid);
});
const q = [];
raw.forEach((cid) => {
const k = normalizeQueueCid(cid);
const canon = index.get(k);
if (canon) q.push(canon);
});
if (q.length === 0 && raw.length > 0) {
return raw;
}
const stored = getQueue();
const changed =
q.length !== stored.length ||
q.some((cid, i) => normalizeQueueCid(cid) !== normalizeQueueCid(stored[i]));
if (changed) setQueue(q);
if (getEnabled() && getStudySessionQueue().length) {
const sessionRaw = getStudySessionQueue();
const sessionNext = [];
sessionRaw.forEach((cid) => {
const k = normalizeQueueCid(cid);
const canon = index.get(k);
if (canon) sessionNext.push(normalizeQueueCid(canon));
});
if (sessionNext.length === 0 && sessionRaw.length > 0) {
return q.length ? q : raw;
}
const sessionChanged =
sessionNext.length !== sessionRaw.length ||
sessionNext.some((cid, i) => cid !== sessionRaw[i]);
if (sessionChanged) setStudySessionQueue(sessionNext);
}
return q.length ? q : raw;
}
function findPanelCourseByCid(cid) {
const k = normalizeQueueCid(cid);
if (!k) return null;
return panelState.courses.find((c) => normalizeQueueCid(c.cid) === k) || null;
}
function findPanelChapterByJob(job) {
if (!job) return null;
const lsKey = job.lsKey;
const cwrid = job.cwrid;
const cid = job.cid;
for (const course of panelState.courses) {
if (cid && normalizeQueueCid(course.cid) !== normalizeQueueCid(cid)) continue;
if (!course.chapters || !course.chapters.length) continue;
const ch =
(lsKey && course.chapters.find((c) => c.lsKey === lsKey)) ||
(cwrid && course.chapters.find((c) => c.cwrid === cwrid)) ||
(job.cwid && course.chapters.find((c) => c.cwid === job.cwid));
if (ch) return ch;
}
for (const preview of panelState.chapterPreview || []) {
if (cid && preview.cid !== cid) continue;
if (!preview.chapters) continue;
const ch =
(lsKey && preview.chapters.find((c) => c.lsKey === lsKey)) ||
(cwrid && preview.chapters.find((c) => c.cwrid === cwrid));
if (ch) return ch;
}
return null;
}
function resolveChapterDisplayTitle(job) {
const ch = findPanelChapterByJob(job);
return (ch && ch.title) || (job && job.title) || "\u7ae0\u8282";
}
function syncJobTitlesFromPanelCourses() {
const jobs = loadJobs();
let changed = false;
Object.keys(jobs).forEach((k) => {
const job = jobs[k];
if (!job || job.phase === "done") return;
const ch = findPanelChapterByJob(job);
if (ch && ch.title && ch.title !== job.title) {
job.title = ch.title;
changed = true;
}
});
if (changed) saveJobs(jobs);
return changed;
}
function readCourseListSelectionFromDom() {
const el = document.getElementById("hy-cme-course-list");
if (!el) return [];
const selected = [];
el.querySelectorAll("input[type='checkbox'][data-cid]").forEach((box) => {
if (box.checked && box.dataset.cid) selected.push(normalizeQueueCid(box.dataset.cid));
});
return selected.filter(Boolean);
}
function commitQueueSelection(list) {
const next = (list || []).map(normalizeQueueCid).filter(Boolean);
if (getEnabled() && !next.length) return;
localStorage.setItem(QUEUE_TOUCHED_KEY, "1");
setQueue(next);
panelState.queueSelectSuppressUntil = Date.now() + 2000;
panelState.lastCourseListKey = getCourseListRenderKey();
}
function scheduleChapterPreviewRefresh() {
if (panelState.chapterPreviewDebounce) clearTimeout(panelState.chapterPreviewDebounce);
panelState.chapterPreviewDebounce = setTimeout(() => {
panelState.chapterPreviewDebounce = null;
void refreshChapterPreview();
}, 500);
}
function restoreLockedQueueSelection() {
const list =
panelState.selectionLock && panelState.selectionLock.length
? panelState.selectionLock
: getStudySessionQueue();
if (list && list.length) commitQueueSelection(list);
}
function detectCollectPageIssue(html) {
if (!html || html.length < 300) return { login: true, error: "\u672a\u767b\u5f55" };
if (isPageLoginRedirect(html)) return { login: true, error: "\u672a\u767b\u5f55" };
if (/course_collect\.aspx|ul\.sub_ul|\u9879\u76ee\u6536\u85cf/i.test(html)) return null;
if (/\u8bf7\u5148\u767b\u5f55|\u8bf7\u767b\u5f55|password|login_sso/i.test(html) && !/\u9000\u51fa\u767b\u5f55|login_out/i.test(html)) {
return { login: true, error: "\u672a\u767b\u5f55" };
}
return null;
}
function isQueueTouched() {
return localStorage.getItem(QUEUE_TOUCHED_KEY) === "1";
}
function initDefaultQueueIfNeeded() {
if (panelState.loginRequired || !panelState.courses.length) return;
pruneQueueToCourses();
if (isQueueTouched()) return;
const q = getQueue();
if (!q.length) {
setQueue(panelState.courses.map((c) => c.cid));
}
}
function syncQueueFromCourseList() {
if (getEnabled()) return;
const selected = readCourseListSelectionFromDom();
commitQueueSelection(selected);
scheduleChapterPreviewRefresh();
updateOfflineBatchPanel();
renderSelectAllBar();
}
function bindCourseListEvents() {
const panel = document.getElementById("hy-cme-auto-panel");
if (!panel || panel.__hyCourseBound) return;
panel.__hyCourseBound = true;
panel.addEventListener("change", (e) => {
const t = e.target;
if (t.id === "hy-cme-select-all") {
document.querySelectorAll("#hy-cme-course-list input[data-cid]").forEach((cb) => {
cb.checked = t.checked;
});
syncQueueFromCourseList();
return;
}
if (t.matches("#hy-cme-course-list input[data-cid]")) {
syncQueueFromCourseList();
}
});
}
function renderSelectAllBar() {
const bar = document.getElementById("hy-cme-select-all-bar");
if (!bar) return;
if (panelState.refreshing || !panelState.courses.length) {
bar.style.display = "none";
bar.dataset.key = "";
return;
}
bar.style.display = "block";
const selectedSet = new Set(pruneQueueToCourses().map(normalizeQueueCid));
const total = panelState.courses.length;
const allChecked = total > 0 && selectedSet.size === total;
const indeterminate = selectedSet.size > 0 && selectedSet.size < total;
const key = `${selectedSet.size}/${total}/${allChecked}/${indeterminate}`;
if (bar.dataset.key === key) return;
bar.dataset.key = key;
bar.innerHTML = `
`;
const box = bar.querySelector("#hy-cme-select-all");
if (box) box.indeterminate = indeterminate;
}
function getModeHintText() {
if (isOfflineMode()) {
return "\u79bb\u7ebf\u6279\u91cf\uff1a\u9996\u6b21\u5206\u6bb5\u62c9\u6ee1\u5168\u90e8\u8fdb\u5ea6\uff0c\u7d2f\u8ba1\u8bfe\u957f\u5199\u5165\u672c\u5730\uff0c\u5230\u70b9\u540e\u518d\u8fd0\u884c\u6536\u5c3e\u4e0e\u8003\u8bd5";
}
return "1:1\u6302\u673a\uff1a\u8bfe\u957f=\u6302\u949f\uff08\u8fdb\u5165 status=0\uff0c\u6bcf5\u5206\u949f status=1+\u5b66\u5b8c\u68c0\u6d4b\uff0c\u672b\u6bb5 status=2\uff09";
}
function formatJobQueueLine(j, now, mode) {
const name = j.title || j.cwrid.slice(0, 8);
const filling = hasPendingFill();
const state = mode === "serial" ? loadSerialState() : null;
const apiWait = isTailMode() ? msUntilNextProcessApi(now) : 0;
const slot =
apiWait > 0 ? ` API${formatEta(apiWait)}` : mode === "tail" && TAIL_STEP_PHASES.includes(j.phase) ? " API\u8fdb\u884c\u4e2d" : "";
if (mode === "ladder" && j.phase === "pending") return `${name} \u5f85\u9636\u68af\uff08\u6392\u961f\uff09`;
if (mode === "ladder" && j.phase === "ladder_jump") {
const jd = j.ladderJumpDue || 0;
const tag = jd <= now ? "\u5230\u70b9\u8df3" : `${formatEta(jd - now)}\u540e\u8df3`;
return `${name} \u9636\u68af#${j.ladderSlot || "?"} \u5f85status=3@${j.ladderJumpSec || "?"}s ${tag}`;
}
if (mode === "ladder" && j.phase === "ladder_finish") {
const fd = j.ladderFinishDue || 0;
const tag = fd <= now ? "\u5230\u70b9\u6536\u5c3e" : `${formatEta(fd - now)}\u540estatus=2+\u64ad\u5b8c`;
return `${name} \u9636\u68af#${j.ladderSlot || "?"} \u5df2\u8df3@${j.lastPostedSec || "?"}s ${tag}`;
}
if (mode === "p3par" && j.phase === "pending") {
const nd = j.nextStepDue || 0;
const tag = nd <= now ? "\u5373\u5c06\u6b65\u8fdb" : `${formatEta(nd - now)}\u540e\u6b65\u8fdb`;
return `${name} \u5f85\u5e76\u884c ${tag}`;
}
if (mode === "pstep" && j.phase === "pending") return `${name} \u5f85\u6b65\u8fdb\uff08\u8f6e\u6d41\uff09`;
if ((mode === "p3par" || mode === "pstep") && j.phase === "stepping") {
const last = j.lastPostedSec || j.startSec || 0;
const dur = j.duration || 1;
const pct = Math.min(99, Math.round((last / dur) * 100));
const nd = j.nextStepDue || 0;
const tag = nd <= now ? "\u5230\u70b9+300s" : `${formatEta(nd - now)}\u540e+300s`;
return `${name} \u6b65\u8fdb@${last}/${dur}s ${pct}% \u7b2c${j.stepCount || 0}\u6b65 ${tag}`;
}
if (mode === "tail" && j.phase === "pending") return `${name} \u5f85\u5feb\u586b\u81f3\u672b5\u5206${slot}`;
if (mode === "tail" && TAIL_STEP_PHASES.includes(j.phase)) {
const tailSec = getTailSec(j);
const map = {
fill_s0: "\u5feb\u586b\u8fdb\u5165",
fill_step: `\u5feb\u586b+300s→@${tailSec}s \u7b2c${j.fillStepCount || 0}\u6b65`,
fill_s1: `\u5feb\u586b+300s→@${tailSec}s`,
fill_s3: "\u5feb\u586b\u9000\u51fa",
tail_s0: "\u672b5\u5206\u8fdb\u5165",
tail_s1: `\u672b5\u5206@${j.duration}s`,
tail_s2: "\u5b8c\u6bd5",
tail_s3: "\u9000\u51fa",
};
return `${name} ${map[j.phase] || j.phase}${slot}`;
}
if (mode === "tail" && j.phase === "waiting") {
const last = j.lastPostedSec || j.startSec || 0;
const dur = j.duration || 1;
const pct = Math.min(99, Math.round((last / dur) * 100));
const td = j.tailDue || 0;
const tag = td <= now ? "\u5f85\u672b5\u5206" : `${formatEta(td - now)}\u540e\u672b5\u5206`;
return `${name} \u6302\u949f@${last}/${dur}s ${pct}% ${tag}${slot}`;
}
if (mode === "tail" && j.phase === "finish_wait") {
const rd = j.recordRetryDue || 0;
const tag = rd <= now ? "\u5f851\u6b21\u5fc3\u8df3+\u64ad\u5b8c" : `${formatEta(rd - now)}\u540e\u6536\u5c3e\u91cd\u8bd5`;
return `${name} \u6302\u949f\u7b49\u64ad\u5b8c\u8bb0\u5f55 ${tag}${slot}`;
}
if (mode === "tail" && j.phase === "ready") return `${name} \u5f85\u63d0\u4ea4\u64ad\u5b8c\u8bb0\u5f55${slot}`;
if (mode === "tail" && (j.phase === "serial" || j.phase === "stepping")) {
return `${name} \u5f85\u5feb\u586b\uff08\u65e7\u8f6e\u8be2\u72b6\u6001\uff0c\u81ea\u52a8\u8fc1\u79fb\u4e2d\uff09${slot}`;
}
if (mode === "serial" && j.phase === "pending") return `${name} \u6392\u961f\u4e2d\uff08\u5f85\u8f6e\u8be2\uff09`;
if (mode === "serial" && (j.phase === "serial" || j.phase === "stepping")) {
const last = j.lastPostedSec || j.startSec || 0;
const dur = j.duration || 1;
const pct = Math.min(100, Math.round((last / dur) * 100));
const roundWait =
state && now < (state.roundDue || 0)
? ` \u4e0b\u8f6e${formatEta((state.roundDue || 0) - now)}`
: state && (state.roundIdx || 0) > 0
? ` \u672c\u8f6e\u8fdb\u884c\u4e2d`
: "";
return `${name} \u8f6e\u8be2@${last}/${dur}s ${pct}% \u7b2c${j.stepCount || 0}\u6b65${roundWait}`;
}
if (mode === "serial" && j.phase === "ready") return `${name} \u5df2\u6ee1\uff0c\u5f85\u63d0\u4ea4\u64ad\u5b8c\u8bb0\u5f55`;
if (mode === "realtime" && j.phase === "pending") return `${name} [1:1] \u6392\u961f\u4e2d`;
if (mode === "realtime" && j.phase === "rt_stepping") {
const dur = j.duration || 1;
const nd = j.nextStepDue || 0;
if (!j.rtEntered) {
return `${name} [1:1] \u7b49\u5f85\u8fdb\u5165 ${nd <= now ? "\u5373\u5c06\u4e0a\u62a5" : formatEta(nd - now) + "\u540e\u4e0a\u62a5"}`;
}
const studiedSec = j.lastPostedSec != null ? j.lastPostedSec : j.rtEntrySec != null ? j.rtEntrySec : 0;
const prog = formatRtStudyLine(studiedSec, dur);
const finishAt = getRealtimeFinishAt(j);
const tag =
finishAt && finishAt <= now
? "\u5230\u70b9\u64ad\u5b8c"
: finishAt
? `${formatEta(finishAt - now)}\u540e\u64ad\u5b8c`
: nd <= now
? "\u5230\u70b9\u4e0a\u62a5"
: `${formatEta(nd - now)}\u540e\u4e0a\u62a5`;
return `${name} [1:1] ${prog} ${tag}`;
}
if (mode === "realtime" && j.phase === "rt_finish_retry") {
const rd = j.recordRetryDue || 0;
return `${name} [1:1] \u64ad\u5b8c\u91cd\u8bd5 ${rd <= now ? "\u5230\u70b9" : formatEta(rd - now)}`;
}
if (j.interactive && j.phase === "pending") return `${name} [\u4e92\u52a8] \u6392\u961f\u4e2d\uff08API\uff09`;
if (j.interactive && j.phase === "hd_running") {
const prog = j.hdProgressText || j.hdLastLog || "";
const step =
j.hdStep && j.hdStepTotal ? ` \u7b2c${j.hdStep}/${j.hdStepTotal}\u9875` : j.hdStep ? ` \u7b2c${j.hdStep}\u9875` : "";
const tail = prog ? ` ${prog}` : " API \u6267\u884c\u4e2d…";
return `${name} [\u4e92\u52a8]${step}${tail}`;
}
if (mode === "offline" && j.phase === "pending") return `${name} [\u79bb\u7ebf] \u5f85\u6279\u91cf\u62c9\u6ee1`;
if (mode === "offline" && j.phase === "face_pending") return `${name} [\u79bb\u7ebf] \u5f85\u770b\u8bfe\u524d\u4eba\u8138\u626b\u7801`;
if (mode === "offline" && j.phase === "silent_filling") return `${name} [\u79bb\u7ebf] \u5206\u6bb5\u62c9\u6ee1\u4e2d…`;
if (mode === "offline" && j.phase === "silent_clock") {
const due = j.completeDue || loadSilentState().batchCompleteDue || 0;
const tag = due <= now ? "\u5230\u70b9\u5f85\u6536\u5c3e" : `${formatEta(due - now)}\u540e\u6536\u5c3e`;
return `${name} [\u79bb\u7ebf] \u5df2\u62c9\u6ee1 ${tag}`;
}
if (mode === "offline" && j.phase === "silent_finish_retry") {
const rd = j.recordRetryDue || 0;
return `${name} [\u79bb\u7ebf] \u64ad\u5b8c\u91cd\u8bd5 ${rd <= now ? "\u5230\u70b9" : formatEta(rd - now)}`;
}
if (mode === "heartbeat" && j.phase === "pending") return `${name} \u5f85\u5feb\u586b`;
if (j.phase === "anchor") {
const tag = filling ? "\u951a\u70b9\u4fdd\u6d3b" : "\u6392\u961f";
return `${name} ${tag} ${formatEta(j.finishDue - now)}\u540e\u6536\u5c3e`;
}
if (j.phase === "active") {
const sec = j.lastPostedSec || j.startSec || 1;
const hb = j.heartbeatOk
? `\u6709\u6548\u5fc3\u8df3${j.heartbeatOk}\u6b21`
: `\u9996\u5fc3\u8df3${formatEta((j.nextHeartbeatDue || now) - now)}`;
return `${name} ★active@${sec}s ${hb} ${formatEta(j.finishDue - now)}\u540e\u6536\u5c3e`;
}
if (j.phase === "waiting" && mode !== "tail") {
const qd = getQueueDue(j);
const tag = qd <= now ? "\u5373\u5c06\u8f6e\u5230" : formatEta(qd - now) + "\u540e\u8f6e\u5230";
return `${name} \u6392\u961f(${tag}\uff0c\u7ea6${Math.round((j.duration || 0) / 60)}\u5206\u949f\u5fc3\u8df3)`;
}
return `${name} ${j.phase}`;
}
function getActiveJob(jobs) {
const active = Object.values(jobs).filter((j) => j && j.phase !== "done");
const runningPhases = [
"hd_running",
"rt_stepping",
"rt_finish_retry",
"silent_filling",
"silent_clock",
"silent_finish_retry",
"stepping",
"active",
"ladder_jump",
"ladder_finish",
"anchor",
];
return (
active.find((j) => runningPhases.includes(j.phase)) ||
active.sort((a, b) => (getQueueDue(a) || a.createdAt || 0) - (getQueueDue(b) || b.createdAt || 0))[0] ||
null
);
}
function renderCourseList() {
const el = document.getElementById("hy-cme-course-list");
if (!el) return;
if (Date.now() < (panelState.queueSelectSuppressUntil || 0)) {
renderSelectAllBar();
return;
}
if (panelState.refreshing) {
el.innerHTML = '\u6b63\u5728\u52a0\u8f7d\u6536\u85cf\u8bfe\u7a0b…
';
return;
}
const selectedSet = new Set(pruneQueueToCourses().map(normalizeQueueCid));
const jobs = loadJobs();
const curJob = getActiveJob(jobs);
if (!panelState.courses.length) {
const active = Object.values(jobs).filter((j) => j && j.phase !== "done");
if (panelState.loginRequired) {
el.innerHTML =
'\u672a\u767b\u5f55
\u8bf7\u5148\u767b\u5f55\u534e\u533b\u7f51\uff0c\u767b\u5f55\u540e\u70b9\u300c\u5237\u65b0\u6536\u85cf\u300d
';
return;
}
if (!active.length) {
el.innerHTML =
'\u6536\u85cf\u5939\u6682\u65e0\u8bfe\u7a0b
\u8bf7\u5148\u5728\u7f51\u7ad9\u6536\u85cf\u8981\u5b66\u4e60\u7684\u8bfe\u7a0b
';
return;
}
el.innerHTML = active
.sort((a, b) => (getQueueDue(a) || a.createdAt || 0) - (getQueueDue(b) || b.createdAt || 0))
.map((j) => {
const isCur = curJob && curJob.lsKey === j.lsKey;
const title = resolveChapterDisplayTitle(j);
return `${escapeHtml(title)}\u5b66\u4e60\u4e2d
`;
})
.join("");
return;
}
const allChecked =
panelState.courses.length > 0 && selectedSet.size === panelState.courses.length;
el.innerHTML = panelState.courses
.map((c) => {
const total = Number(c.chapterTotal) || 0;
const studyDone = Number(c.studyDone) || 0;
const examDone = Number(c.examDone) || 0;
const hasProgress = c.chaptersLoaded && total > 0 && !c.progressError;
const studyPct = hasProgress ? Math.round((studyDone / total) * 100) : 0;
const examPct = hasProgress ? Math.round((examDone / total) * 100) : 0;
const finished = hasProgress && !isCourseUnfinished(c);
const checked = selectedSet.has(normalizeQueueCid(c.cid));
const isActive = curJob && curJob.cid === c.cid;
const border = isActive ? "#16a34a" : "#e2e8f0";
const bgColor = isActive ? "#f0fdf4" : finished ? "#f1f5f9" : "#f8fafc";
const titleColor = finished ? "#64748b" : "#0f172a";
const progressLine = getCourseProgressLine(c);
const studyPctDisplay = studyPct;
const examPctDisplay = examPct;
return `
`;
})
.join("");
renderSelectAllBar();
panelState.lastCourseListKey = getCourseListRenderKey();
}
function renderChapterPreview() {
const el = document.getElementById("hy-cme-chapter-preview");
if (!el) return;
const list = panelState.chapterPreview;
if (!list.length) {
if (pageType() === "course") {
const chapters = parseCourseChapterJobs();
if (!chapters.length) {
el.innerHTML = '\u8bf7\u52fe\u9009\u8bfe\u7a0b\u6216\u6253\u5f00\u7ae0\u8282\u9875
';
return;
}
const jobs = loadJobs();
const title = (document.querySelector("h1,h2,.course_title")?.textContent || document.title || "\u5f53\u524d\u8bfe\u7a0b")
.replace(/\s+/g, " ")
.trim()
.slice(0, 40);
const cid = readCourseId();
const courseOnPage = cid ? findPanelCourseByCid(cid) : null;
const items = chapters
.map((ch) => formatChapterPreviewLine(applyLiveProgressToChapter(ch), courseOnPage))
.join("");
el.innerHTML = `${escapeHtml(title)}
${items}
`;
return;
}
el.innerHTML = '\u8bf7\u52fe\u9009\u8bfe\u7a0b\u67e5\u770b\u7ae0\u8282
';
return;
}
el.innerHTML = list
.map((course) => {
const items = (course.chapters || [])
.map((ch) => formatChapterPreviewLine(applyLiveProgressToChapter(ch), course))
.join("");
const emptyHint = course.progressError
? course.progressError
: course.chaptersLoaded
? "\u672a\u8bc6\u522b\u5230\u5fc5\u4fee\u7ae0\u8282"
: "\u7ae0\u8282\u672a\u52a0\u8f7d\uff0c\u8bf7 F5 \u540e\u70b9\u300c\u5237\u65b0\u6536\u85cf\u300d";
return `${escapeHtml(course.title)}
${items || `
${escapeHtml(emptyHint)}
`}
`;
})
.join("");
}
function updatePanel() {
const statusEl = document.getElementById("hy-cme-auto-status");
if (!statusEl) return;
ensureStudyQueueSelection();
const jobs = loadJobs();
const all = Object.values(jobs).filter(Boolean);
const active = all.filter((j) => j.phase !== "done");
const progress = getSelectedProgressTotals();
const now = Date.now();
syncStartStopButtons();
const videoEl = document.getElementById("hy-cme-queue-video");
const examEl = document.getElementById("hy-cme-queue-exam");
const pctEl = document.getElementById("hy-cme-queue-percent");
const barEl = document.getElementById("hy-cme-queue-progress");
if (videoEl) {
videoEl.textContent = progress.videoTotal ? `${progress.videoDone}/${progress.videoTotal}` : "—";
}
if (examEl) {
examEl.textContent = progress.examTotal ? `${progress.examDone}/${progress.examTotal}` : "—";
}
let pct = 0;
if (progress.videoTotal) {
pct = Math.round((progress.videoDone / progress.videoTotal) * 100);
} else if (progress.examTotal) {
pct = Math.round((progress.examDone / progress.examTotal) * 100);
}
if (getEnabled()) {
if (panelState.refreshing) statusEl.textContent = "\u5237\u65b0\u4e2d…";
else if (active.length || progress.videoDone < progress.videoTotal || progress.examDone < progress.examTotal) {
statusEl.textContent = "\u8fd0\u884c\u4e2d";
} else statusEl.textContent = "\u8fd0\u884c\u4e2d · \u5f85\u767b\u8bb0";
statusEl.style.color = "#047857";
statusEl.style.borderColor = "#6ee7b7";
} else {
statusEl.textContent = "\u5df2\u505c\u6b62";
statusEl.style.color = "#64748b";
statusEl.style.borderColor = "#cbd5e1";
}
if (pctEl) pctEl.textContent = `${pct}%`;
if (barEl) barEl.style.width = `${pct}%`;
const footerEl = document.getElementById("hy-cme-queue-text");
if (footerEl) {
if (panelState.courses.length) {
footerEl.textContent = progress.selectedCount
? formatSelectedProgressLine()
: `\u6536\u85cf ${panelState.courses.length} \u95e8 · \u8bf7\u52fe\u9009\u8bfe\u7a0b`;
} else if (!all.length) footerEl.textContent = "\u672a\u767b\u8bb0\u7ae0\u8282";
else if (!active.length) footerEl.textContent = formatSelectedProgressLine();
else footerEl.textContent = formatSelectedProgressLine();
}
const listKey = getCourseListRenderKey();
const suppressListRender = Date.now() < (panelState.queueSelectSuppressUntil || 0);
if (!suppressListRender && listKey !== panelState.lastCourseListKey) {
panelState.lastCourseListKey = listKey;
renderCourseList();
} else {
renderSelectAllBar();
}
renderChapterPreview();
syncRunModePanel();
refreshGongxuStudyHint();
const selectedQueue = pruneQueueToCourses();
if (
getEnabled() &&
selectedQueue.length > 0 &&
all.length > 0 &&
!active.length &&
!hasPendingExamWork()
) {
maybeAutoStopWhenFinished();
}
tickOfflineBatchWaitUserLog();
}
function updatePanelCloudStatus() {
const tierEl = document.getElementById("hy-cme-cloud-tier");
const freeEl = document.getElementById("hy-cme-cloud-free");
const freeLabelEl = document.getElementById("hy-cme-cloud-free-label");
const tokenInput = document.getElementById("hy-cme-cloud-token");
if (tierEl) {
tierEl.textContent = formatCloudTierText(cloudState.cloudTier);
tierEl.style.color =
cloudState.cloudRevoked || String(cloudState.cloudTier || "").toLowerCase() === "revoked"
? "#dc2626"
: "#0f172a";
}
if (cloudState.cloudRevoked) {
if (freeLabelEl) freeLabelEl.textContent = "\u6388\u6743\u72b6\u6001";
if (freeEl) freeEl.textContent = "Token \u5df2\u7981\u7528";
} else if (String(cloudState.cloudTier || "").toLowerCase() === "pro") {
if (freeLabelEl) freeLabelEl.textContent = "Pro \u5230\u671f";
if (freeEl) freeEl.textContent = formatLeaseExpireText(cloudState.cloudProExpireAt || cloudState.cloudLeaseExp);
} else {
if (freeLabelEl) freeLabelEl.textContent = `\u514d\u8d39\u4f53\u9a8c\uff08${cloudState.freeChapterLimit} \u8282\uff09`;
if (freeEl) freeEl.textContent = `${cloudState.freeUsedChapters}/${cloudState.freeChapterLimit}`;
}
if (tokenInput && tokenInput !== document.activeElement) {
tokenInput.value = cloudState.cloudToken || "";
}
updatePanel();
}
function buildSegmentTargets(start, end) {
const span = Math.max(0, end - start);
if (span <= 0) return [end];
const targets = [];
for (let i = 1; i <= API_SEGMENTS; i++) {
targets.push(Math.floor(start + (span * i) / API_SEGMENTS));
}
targets[targets.length - 1] = end;
return targets.filter((t, i, arr) => i === 0 || t > arr[i - 1]);
}
function segmentGapMs() {
return Math.max(3000, Math.round(API_GAP_MS / getMultiplier()));
}
function unlockExamButton() {
try {
const btn = document.getElementById("jrks");
if (!btn) return;
btn.removeAttribute("disabled");
btn.classList.remove("inputstyle2_2");
btn.classList.add("inputstyle2");
btn.textContent = "\u8fdb\u5165\u8003\u8bd5";
} catch (_) {}
}
function countSteps(duration, start) {
const s = start || 1;
return Math.max(1, Math.ceil((duration - s) / STEP_SEC));
}
function loadSerialState() {
try {
return JSON.parse(localStorage.getItem(SERIAL_STATE_KEY) || "{}") || {};
} catch (_) {
return {};
}
}
function saveSerialState(s) {
localStorage.setItem(SERIAL_STATE_KEY, JSON.stringify(s));
}
function resetSerialState() {
localStorage.removeItem(SERIAL_STATE_KEY);
}
function getTailState() {
try {
return JSON.parse(localStorage.getItem(TAIL_STATE_KEY) || "{}") || {};
} catch (_) {
return {};
}
}
function saveTailState(s) {
localStorage.setItem(TAIL_STATE_KEY, JSON.stringify(s));
}
function resetTailState() {
localStorage.removeItem(TAIL_STATE_KEY);
}
function randomStepIntervalMs() {
return (
STEP_INTERVAL_MIN +
Math.floor(Math.random() * (STEP_INTERVAL_MAX - STEP_INTERVAL_MIN + 1))
);
}
function randomStaggerMs() {
return (
PARALLEL_STAGGER_MIN +
Math.floor(Math.random() * (PARALLEL_STAGGER_MAX - PARALLEL_STAGGER_MIN + 1))
);
}
async function runOneProcessPstep(ctx, status, sec, label, tag) {
let res = null;
try {
res = await postProcess(ctx, status, sec);
} catch (e) {
pushLog(`[${label}] ${tag} status=${status}@${sec}s \u5f02\u5e38: ${e && e.message ? e.message : e}`);
return { ok: false, code: -1 };
}
const code = res && res.code;
if (code === 3) {
const hint =
status === 2
? "\uff08\u6ee1\u8fdb\u5ea6\u4e0d\u5e94\u9891\u7e41\uff0c\u8bf7\u68c0\u67e5\uff09"
: "\uff08\u6b65\u8fdb+300s \u9650\u6d41\uff0c\u7ea65\u5206\u949f1\u6b21\uff1b\u624b\u6d4b status=2 \u6ee1\u8fdb\u5ea6\u51e0\u79d2\u6362\u8bfe\u4ecd code=0\uff09";
pushLog(`[${label}] ${tag} status=${status}@${sec}s code=3 ${hint}`);
return { ok: false, code: 3 };
}
if (code !== 0) {
pushLog(
`[${label}] ${tag} status=${status}@${sec}s code=${code != null ? code : "?"}${res && res.msg ? " " + res.msg : ""}`
);
return { ok: false, code };
}
return { ok: true, code: 0, res };
}
function jobStepIntervalReady(job, now) {
if (!job || !job.lastPstepAt) return true;
return now - job.lastPstepAt >= STEP_INTERVAL_MIN;
}
function countPstepDueKeys(jobs, now) {
return Object.keys(jobs).filter((k) => isPstepJobDue(jobs[k], now)).length;
}
function isPstepJobDue(j, now) {
if (!j || j.phase === "done") return false;
if (j.phase === "pending" && (!j.nextStepDue || j.nextStepDue <= now)) return true;
if (j.phase === "stepping" && (!j.nextStepDue || j.nextStepDue <= now)) return true;
return false;
}
function sortPstepDueKeys(jobs, now) {
return Object.keys(jobs)
.filter((k) => isPstepJobDue(jobs[k], now))
.sort((a, b) => {
const ja = jobs[a];
const jb = jobs[b];
const sa = ja.stepCount || 0;
const sb = jb.stepCount || 0;
if (sa !== sb) return sa - sb;
return (ja.nextStepDue || ja.createdAt || 0) - (jb.nextStepDue || jb.createdAt || 0);
});
}
function preparePstepJob(jobs, key) {
const job = jobs[key];
if (!job || job.phase !== "pending") return job;
job.phase = "stepping";
job.lastPostedSec = job.startSec || 1;
job.stepCount = 0;
jobs[key] = job;
saveJobs(jobs);
return job;
}
function estimateP3ParallelWallMin(jobs) {
const list = Object.values(jobs).filter((j) => j && j.phase !== "done");
if (!list.length) return 0;
const maxSteps = Math.max(
...list.map((j) => countFillSteps(j.duration || 3600, j.startSec || 1))
);
return maxSteps * Math.round(STEP_INTERVAL_MIN / 60000);
}
async function runParallelStepVisit(jobKey, job) {
const ctx = jobToCtx(job);
const label = job.title || job.cwrid.slice(0, 8);
const start = job.startSec || 1;
const duration = job.duration;
const last = job.lastPostedSec != null ? job.lastPostedSec : start;
const next = Math.min(duration, last + STEP_SEC);
const isFinal = next >= duration - 2;
const stepsTotal = countFillSteps(duration, start);
const now = Date.now();
if (!isFinal && !jobStepIntervalReady(job, now)) {
saveJob(ctx, { nextStepDue: job.lastPstepAt + randomStepIntervalMs() });
return;
}
const warmed = await warmPlaySession(ctx, true);
if (!warmed) {
saveJob(ctx, { nextStepDue: Date.now() + 60000 });
pushLog(`[${label}] \u9884\u70ed\u64ad\u653e\u9875\u5931\u8d25\uff0c60s\u540e\u91cd\u8bd5\uff08\u8bf7\u5148 F5 \u786e\u8ba4\u5df2\u767b\u5f55\uff09`);
return;
}
await sleep(1500);
if (!isFinal) {
const r = await runOneProcessPstep(ctx, 3, next, label, "\u6b65\u8fdb");
if (!r.ok) {
saveJob(ctx, {
lastPstepAt: Date.now(),
nextStepDue: Date.now() + randomStepIntervalMs(),
});
if (isP3ParallelMode() && r.code === 3) {
pushLog(`[${label}] \u82e5\u591a\u8bfe\u8fde\u7eedcode=3 → \u9650\u6d41\u53ef\u80fd\u662f\u5168\u8d26\u53f7\uff0c\u6539\u300c\u8f6e\u6d41+300s\u300d`);
}
return;
}
job.stepCount = (job.stepCount || 0) + 1;
syncLocal(ctx, next);
const stepAt = Date.now();
saveJob(ctx, {
phase: "stepping",
stepCount: job.stepCount,
lastPostedSec: next,
lastPstepAt: stepAt,
nextStepDue: stepAt + randomStepIntervalMs(),
});
pushLog(
`[${label}] \u6b65\u8fdb status=3@${next}s code=0 · \u7b2c${job.stepCount}/${stepsTotal}\u6b65 · \u4e0b\u6b65\u7ea6${Math.round(STEP_INTERVAL_MIN / 60000)}\u5206\u949f\u540e`
);
return;
}
const r2 = await runOneProcessPstep(ctx, 2, duration, label, "\u5b8c\u6bd5");
if (!r2.ok) {
saveJob(ctx, {
lastPstepAt: Date.now(),
nextStepDue: Date.now() + randomStepIntervalMs(),
});
return;
}
saveJob(ctx, { lastPstepAt: Date.now() });
pushLog(`[${label}] \u5b8c\u6bd5 status=2@${duration}s code=0 → \u64ad\u5b8c\u8bb0\u5f55…`);
let rec = null;
try {
rec = await postPlayRecord(ctx, duration);
pushLog(
`[${label}] \u64ad\u5b8c\u8bb0\u5f55 @${duration}s code=${rec && rec.code}${rec && rec.msg ? " " + rec.msg : ""}`
);
} catch (e) {
pushLog(`[${label}] \u64ad\u5b8c\u8bb0\u5f55\u5f02\u5e38: ${e && e.message ? e.message : e}`);
}
if (rec && rec.code === 1) {
saveJob(ctx, {
phase: "stepping",
lastPostedSec: Math.max(start, duration - STEP_SEC * 2),
nextStepDue: Date.now() + randomStepIntervalMs(),
});
pushLog(`[${label}] \u65f6\u957f\u4e0d\u8db3\uff0c\u9000\u56de\u7ee7\u7eed\u6b65\u8fdb`);
return;
}
if (rec && rec.code === -2) {
return;
}
try {
await postProcess(ctx, 3, duration);
} catch (_) {}
syncLocal(ctx, duration);
unlockExamButton();
saveJob(ctx, { phase: "done", lastPostedSec: duration });
pushLog(`[${label}] ✓ \u5b8c\u6210 status=2+\u64ad\u5b8c\u8bb0\u5f55`);
}
function getLadderState() {
try {
return JSON.parse(localStorage.getItem(LADDER_STATE_KEY) || "{}");
} catch (_) {
return {};
}
}
function markLadderProcess() {
localStorage.setItem(LADDER_STATE_KEY, JSON.stringify({ lastAt: Date.now() }));
}
function msUntilLadderProcessGap(now) {
const last = getLadderState().lastAt || 0;
if (!last) return 0;
return Math.max(0, last + STEP_INTERVAL_MIN - now);
}
function applyLadderSchedule(job, slotInBatch, base) {
const slot = slotInBatch + 1;
const dur = job.duration || 3600;
job.phase = "ladder_jump";
job.ladderSlot = slot;
job.ladderJumpSec = Math.min(slot * STEP_SEC, Math.max(1, dur - 2));
job.ladderJumpDue = base + slotInBatch * 2 * STEP_INTERVAL_MIN;
job.ladderFinishDue = job.ladderJumpDue + STEP_INTERVAL_MIN;
job.ladderJumpDone = false;
}
function assignLadderBatch(jobs) {
const inLadder = Object.values(jobs).filter(
(j) => j && (j.phase === "ladder_jump" || j.phase === "ladder_finish")
).length;
const slots = LADDER_BATCH_SIZE - inLadder;
if (slots <= 0) return 0;
const pending = Object.entries(jobs)
.filter(([, j]) => j && j.phase === "pending")
.sort((a, b) => (a[1].createdAt || 0) - (b[1].createdAt || 0))
.slice(0, slots);
if (!pending.length) return 0;
const base = Date.now();
pending.forEach(([k, job], i) => {
applyLadderSchedule(job, i, base);
jobs[k] = job;
});
saveJobs(jobs);
pushLog(
`\u9636\u68af\u4e32\u884c\u672c\u6279 ${pending.length} \u8bfe\uff08process\u7ea65\u5206\u949f1\u6b21\uff0c\u8df3/\u6ee1\u4e0d\u649e\u8f66\uff09\uff1a${pending
.map(([, j], i) => {
const slot = i + 1;
const jumpAt = i === 0 ? "\u7acb\u5373" : `${i * 10}\u5206`;
const finAt = `${(i * 2 + 1) * 5}\u5206`;
return `\u8bfe${slot} ${jumpAt} status=3@${slot * 300}s → ${finAt} status=2\u6ee1+\u64ad\u5b8c\u8bb0\u5f55`;
})
.join("\uff1b")}`
);
return pending.length;
}
function collectLadderDueActions(jobs, now) {
const actions = [];
Object.entries(jobs).forEach(([key, j]) => {
if (!j) return;
if (j.phase === "ladder_jump" && !j.ladderJumpDone && j.ladderJumpDue && j.ladderJumpDue <= now) {
actions.push({ key, type: "jump", due: j.ladderJumpDue, slot: j.ladderSlot || 0 });
}
if (j.phase === "ladder_finish" && j.ladderFinishDue && j.ladderFinishDue <= now) {
actions.push({ key, type: "finish", due: j.ladderFinishDue, slot: j.ladderSlot || 0 });
}
});
actions.sort((a, b) => a.due - b.due || a.slot - b.slot);
return actions;
}
async function runLadderJump(jobKey, job) {
const ctx = jobToCtx(job);
const label = job.title || job.cwrid.slice(0, 8);
const sec = job.ladderJumpSec;
const slot = job.ladderSlot || "?";
if (!(await warmPlaySession(ctx, true))) {
saveJob(ctx, { ladderJumpDue: Date.now() + 60000 });
pushLog(`[${label}] \u9636\u68af\u8df3#${slot}\uff1a\u9884\u70ed\u5931\u8d25\uff0c60s\u540e\u91cd\u8bd5`);
return;
}
await sleep(1500);
const gap = msUntilLadderProcessGap(Date.now());
if (gap > 0) {
saveJob(ctx, { ladderJumpDue: Date.now() + gap });
pushLog(`[${label}] \u9636\u68af\u8df3#${slot}\uff1a\u8ddd\u4e0a\u6b21process ${formatEta(gap)}\uff0c\u5df2\u63a8\u8fdf`);
return;
}
const r = await runOneProcessPstep(ctx, 3, sec, label, `\u9636\u68af\u8df3#${slot}`);
if (!r.ok) {
const wait = Math.max(randomStepIntervalMs(), msUntilLadderProcessGap(Date.now()));
saveJob(ctx, { ladderJumpDue: Date.now() + wait });
return;
}
markLadderProcess();
syncLocal(ctx, sec);
saveJob(ctx, { phase: "ladder_finish", lastPostedSec: sec, ladderJumpDone: true });
pushLog(
`[${label}] ★\u9636\u68af\u8df3#${slot} status=3@${sec}s code=0 → ${formatEta(
Math.max(0, (job.ladderFinishDue || Date.now()) - Date.now())
)}\u540e status=2@${job.duration}s+\u64ad\u5b8c\u8bb0\u5f55`
);
}
async function runLadderFinish(jobKey, job) {
const ctx = jobToCtx(job);
const label = job.title || job.cwrid.slice(0, 8);
const duration = job.duration;
const slot = job.ladderSlot || "?";
if (!(await warmPlaySession(ctx, true))) {
saveJob(ctx, { ladderFinishDue: Date.now() + 60000 });
pushLog(`[${label}] \u9636\u68af\u5b8c\u6bd5#${slot}\uff1a\u9884\u70ed\u5931\u8d25\uff0c60s\u540e\u91cd\u8bd5`);
return;
}
await sleep(1500);
const gap = msUntilLadderProcessGap(Date.now());
if (gap > 0) {
saveJob(ctx, { ladderFinishDue: Date.now() + gap });
pushLog(`[${label}] \u9636\u68af\u5b8c\u6bd5#${slot}\uff1a\u8ddd\u4e0a\u6b21process ${formatEta(gap)}\uff0c\u5df2\u63a8\u8fdf`);
return;
}
const r2 = await runOneProcessPstep(ctx, 2, duration, label, `\u9636\u68af\u5b8c\u6bd5#${slot}`);
if (!r2.ok) {
const wait = Math.max(randomStepIntervalMs(), msUntilLadderProcessGap(Date.now()));
saveJob(ctx, { ladderFinishDue: Date.now() + wait });
return;
}
markLadderProcess();
pushLog(`[${label}] ★\u9636\u68af\u5b8c\u6bd5#${slot} status=2@${duration}s code=0 → \u64ad\u5b8c\u8bb0\u5f55…`);
let rec = null;
try {
rec = await postPlayRecord(ctx, duration);
} catch (e) {
pushLog(`[${label}] ★\u64ad\u5b8c\u8bb0\u5f55\u5f02\u5e38: ${e && e.message ? e.message : e}`);
}
pushLog(
`[${label}] ★\u64ad\u5b8c\u8bb0\u5f55#${slot} @${duration}s code=${rec && rec.code != null ? rec.code : "?"}${rec && rec.msg ? " " + rec.msg : ""}`
);
if (rec && rec.code === 1) {
pushLog(
`[${label}] \u8bd5\u9a8c\u8bf4\u660e\uff1a\u4ec5\u8df3@${job.lastPostedSec || "?"}s \u64ad\u5b8c\u5e38 code=1\uff1b\u957f\u8bfe\u9700\u66f4\u591a\u6b65\u8fdb\u6216\u6302\u949f`
);
}
if (rec && rec.code === -2) return;
syncLocal(ctx, duration);
saveJob(ctx, { phase: "done", lastPostedSec: duration });
const jobs = loadJobs();
const batchActive = Object.values(jobs).some(
(j) => j && (j.phase === "ladder_jump" || j.phase === "ladder_finish")
);
if (!batchActive && Object.values(jobs).some((j) => j && j.phase === "pending")) {
assignLadderBatch(jobs);
}
}
async function runLadderScheduler() {
await keepSessionAlive();
if (!getEnabled() || !getApiMode() || bgRunning || engineRunning) return;
bgRunning = true;
try {
let jobs = loadJobs();
const now = Date.now();
let actions = collectLadderDueActions(jobs, now);
if (!actions.length) {
const inLadder = Object.values(jobs).some(
(j) => j && (j.phase === "ladder_jump" || j.phase === "ladder_finish")
);
if (!inLadder) assignLadderBatch(jobs);
return;
}
if (msUntilLadderProcessGap(now) > 0) return;
const act = actions[0];
const job = jobs[act.key];
if (!job) return;
if (act.type === "jump") await runLadderJump(act.key, job);
else await runLadderFinish(act.key, job);
} catch (e) {
pushLog("\u9636\u68af\u4e32\u884c: " + (e && e.message ? e.message : e));
} finally {
bgRunning = false;
updateJobsPanel();
}
}
async function runP3ParallelScheduler() {
await keepSessionAlive();
if (!getEnabled() || !getApiMode() || bgRunning || engineRunning) return;
bgRunning = true;
try {
const now = Date.now();
let jobs = loadJobs();
let dueKeys = sortPstepDueKeys(jobs, now);
if (!dueKeys.length) return;
const batch = dueKeys.slice(0, P3PAR_MAX_PER_TICK);
for (let i = 0; i < batch.length; i++) {
jobs = loadJobs();
let job = jobs[batch[i]];
if (!job || !isPstepJobDue(job, Date.now())) continue;
job = preparePstepJob(jobs, batch[i]) || job;
await runParallelStepVisit(batch[i], job);
if (i < batch.length - 1) await sleep(P3PAR_COURSE_GAP_MS);
}
} catch (e) {
pushLog("status=3\u771f\u5e76\u884c: " + (e && e.message ? e.message : e));
} finally {
bgRunning = false;
updateJobsPanel();
}
}
async function runParallelStepScheduler() {
await keepSessionAlive();
if (!getEnabled() || !getApiMode() || bgRunning || engineRunning) return;
bgRunning = true;
try {
const now = Date.now();
let jobs = loadJobs();
const dueKeys = sortPstepDueKeys(jobs, now);
if (!dueKeys.length) return;
const key = dueKeys[0];
let job = jobs[key];
if (!job) return;
job = preparePstepJob(jobs, key) || job;
await runParallelStepVisit(key, job);
} catch (e) {
pushLog("\u5e76\u884c\u6b65\u8fdb: " + (e && e.message ? e.message : e));
} finally {
bgRunning = false;
updateJobsPanel();
}
}
function getTailSec(job) {
const start = job.startSec || 1;
const dur = job.duration || 3600;
return Math.max(start + 1, dur - STEP_SEC);
}
function msUntilNextProcessApi(now) {
const last = getTailState().lastApiAt || 0;
return Math.max(0, last - now);
}
function countFillSteps(tailSec, start) {
const s = start || 1;
return Math.max(1, Math.ceil((tailSec - s) / STEP_SEC));
}
function lockProcessApi(ms) {
saveTailState({ ...getTailState(), lastApiAt: Date.now() + (ms || PROCESS_SLOT_MS) });
}
function processApiReady(now) {
return msUntilNextProcessApi(now) <= 0;
}
async function runOneProcess(ctx, status, sec, label, tag) {
let res = null;
try {
res = await postProcess(ctx, status, sec);
} catch (e) {
pushLog(`[${label}] ${tag} status=${status}@${sec}s \u5f02\u5e38: ${e && e.message ? e.message : e}`);
}
const code = res && res.code;
if (code === 3) {
lockProcessApi();
pushLog(
`[${label}] ${tag} status=${status}@${sec}s code=3 \u5931\u8d25\uff085\u5206\u949f1\u6b21\u4e14\u6bcf\u6b21\u6700\u591a+300s\uff09\uff0c${Math.round(PROCESS_SLOT_MS / 60000)}\u5206\u949f\u540e\u91cd\u8bd5`
);
return { ok: false, code: 3 };
}
if (code !== 0) {
lockProcessApi();
pushLog(
`[${label}] ${tag} status=${status}@${sec}s code=${code != null ? code : "?"}${res && res.msg ? " " + res.msg : ""}`
);
return { ok: false, code };
}
lockProcessApi();
return { ok: true, code: 0 };
}
function normalizeTailPhase(job) {
if (job.phase === "fill_s1") job.phase = "fill_step";
}
async function runTailStep(jobKey, job) {
normalizeTailPhase(job);
const ctx = jobToCtx(job);
const label = job.title || job.cwrid.slice(0, 8);
const start = job.startSec || 1;
const tailSec = getTailSec(job);
const duration = job.duration;
if (job.phase === "fill_s0" || job.phase === "tail_s0") {
await warmPlaySession(ctx, true);
}
if (job.phase === "fill_s0") {
const r = await runOneProcess(ctx, 0, start, label, "\u5feb\u586b\u8fdb\u5165");
if (!r.ok) return;
job.sessionStartAt = Date.now();
job.lastPostedSec = start;
job.fillStepCount = 0;
job.phase = "fill_step";
const steps = countFillSteps(tailSec, start);
pushLog(
`[${label}] status=0 @${start}s → \u6bcf5\u5206\u949f+300s\u81f3@${tailSec}s\uff0c\u7ea6${steps}\u6b65\uff08\u4e0d\u53ef\u4e00\u6b21\u8df3\u6ee1\uff09`
);
} else if (job.phase === "fill_step") {
const last = job.lastPostedSec != null ? job.lastPostedSec : start;
if (last >= tailSec - 1) {
job.phase = "fill_s3";
} else {
const next = Math.min(tailSec, last + STEP_SEC);
const steps = countFillSteps(tailSec, start);
const r = await runOneProcess(ctx, 1, next, label, `\u5feb\u586b+300s`);
if (!r.ok) return;
syncLocal(ctx, next);
job.lastPostedSec = next;
job.fillStepCount = (job.fillStepCount || 0) + 1;
pushLog(
`[${label}] \u5feb\u586b \u7b2c${job.fillStepCount}/${steps}\u6b65 @${next}/${tailSec}s code=0`
);
if (next >= tailSec - 1) {
job.phase = "fill_s3";
pushLog(`[${label}] \u5feb\u586b@${tailSec}s \u8fbe\u6807 → \u4e0b\u69fd status=3 \u9000\u51fa`);
}
}
} else if (job.phase === "fill_s3") {
const r = await runOneProcess(ctx, 3, tailSec, label, "\u5feb\u586b\u9000\u51fa");
if (!r.ok) return;
const base = job.sessionStartAt || Date.now();
job.phase = "waiting";
job.filledAt = Date.now();
job.tailDue = base + duration * 1000;
pushLog(
`[${label}] \u5feb\u586b\u5b8c\u6210@${tailSec}s → \u6302\u949f${Math.round(duration / 60)}\u5206\u949f\u540e\u672b5\u5206\u771f\u5b9e\u5fc3\u8df3`
);
} else if (job.phase === "tail_s0") {
const r = await runOneProcess(ctx, 0, tailSec, label, "\u672b5\u5206\u8fdb\u5165");
if (!r.ok) return;
job.tailEnterAt = Date.now();
job.phase = "tail_s1";
pushLog(`[${label}] status=0 @${tailSec}s → \u4e0b\u69fd\u672b5\u5206\u5fc3\u8df3@${duration}s`);
} else if (job.phase === "tail_s1") {
const r = await runOneProcess(ctx, 1, duration, label, `\u672b5\u5206@${duration}s`);
if (!r.ok) return;
syncLocal(ctx, duration);
job.lastPostedSec = duration;
job.phase = "tail_s2";
} else if (job.phase === "tail_s2") {
const r = await runOneProcess(ctx, 2, duration, label, "\u64ad\u653e\u5b8c\u6bd5");
if (!r.ok) return;
job.status2Sent = true;
job.status2At = Date.now();
job.phase = "tail_s3";
} else if (job.phase === "tail_s3") {
const r = await runOneProcess(ctx, 3, duration, label, "\u9000\u51fa");
if (!r.ok) return;
job.phase = "ready";
pushLog(`[${label}] \u672b5\u5206\u5b8c\u6210 → \u6392\u961f\u63d0\u4ea4\u64ad\u5b8c\u8bb0\u5f55`);
}
const jobs = loadJobs();
jobs[jobKey] = job;
saveJobs(jobs);
}
function migrateJobsForTailMode(jobs) {
let changed = false;
Object.keys(jobs).forEach((k) => {
const j = jobs[k];
if (!j || j.phase === "done") return;
if (j.phase === "serial" || j.phase === "stepping") {
const tailSec = getTailSec(j);
const last = j.lastPostedSec != null ? j.lastPostedSec : j.startSec || 1;
if (last >= tailSec - 1) {
j.phase = "waiting";
j.tailDue = j.tailDue || Date.now() + (j.duration || 3600) * 1000;
} else if (last > (j.startSec || 1)) {
j.phase = "fill_step";
} else {
j.phase = "pending";
}
changed = true;
}
});
if (changed) saveJobs(jobs);
return changed;
}
function getRecordRetryDue(job) {
const duration = job.duration || 3600;
const base = job.sessionStartAt || job.filledAt || job.status2At || Date.now();
return base + duration * 1000;
}
async function runFinishRetry(jobKey, job) {
const ctx = jobToCtx(job);
const label = job.title || job.cwrid.slice(0, 8);
const duration = job.duration;
await warmPlaySession(ctx, true);
const r = await runOneProcess(ctx, 1, duration, label, "\u6536\u5c3e\u5fc3\u8df3(1\u6b21)");
if (!r.ok) return;
await sleep(800);
let rec = null;
try {
rec = await postPlayRecord(ctx, duration);
pushLog(
`[${label}] \u64ad\u5b8c\u8bb0\u5f55 @${duration}s code=${rec && rec.code}${rec && rec.msg ? " " + rec.msg : ""}`
);
} catch (e) {
pushLog(`[${label}] \u64ad\u5b8c\u8bb0\u5f55\u5f02\u5e38: ${e && e.message ? e.message : e}`);
}
if (rec && rec.code === 1) {
job.recordRetryDue = Date.now() + duration * 1000;
job.phase = "finish_wait";
pushLog(
`[${label}] \u4ecd\u65f6\u957f\u4e0d\u8db3\uff0c\u518d\u6302\u949f${Math.round(duration / 60)}\u5206\u949f\u540e\u91cd\u8bd51\u6b21\u5fc3\u8df3+\u64ad\u5b8c\u8bb0\u5f55`
);
} else if (rec && rec.code === 0) {
try {
await postProcess(ctx, 3, duration);
} catch (_) {}
syncLocal(ctx, duration);
job.phase = "done";
pushLog(`[${label}] ✓ \u6536\u5c3e\u6210\u529f\uff08\u6302\u949f\u540e\u5355\u5fc3\u8df3+\u64ad\u5b8c\u8bb0\u5f55\uff09`);
}
const jobs = loadJobs();
jobs[jobKey] = job;
saveJobs(jobs);
updateJobsPanel();
}
async function runTailScheduler() {
await keepSessionAlive();
if (!getEnabled() || !getApiMode() || bgRunning || engineRunning) return;
bgRunning = true;
try {
let jobs = loadJobs();
if (migrateJobsForTailMode(jobs)) {
pushLog("\u5df2\u5c06\u65e7\u300c\u4e32\u884c\u8f6e\u8be2\u300d\u4efb\u52a1\u8f6c\u4e3a\u672b\u6bb5\u6302\u949f\u961f\u5217");
jobs = loadJobs();
}
const now = Date.now();
const finishWaitKey = Object.keys(jobs)
.filter((k) => {
const j = jobs[k];
return j && j.phase === "finish_wait" && j.recordRetryDue && j.recordRetryDue <= now;
})
.sort((a, b) => (jobs[a].recordRetryDue || 0) - (jobs[b].recordRetryDue || 0))[0];
if (finishWaitKey) {
if (!processApiReady(now)) return;
jobs[finishWaitKey].phase = "finish_retry";
saveJobs(jobs);
await runFinishRetry(finishWaitKey, jobs[finishWaitKey]);
return;
}
const readyKey = Object.keys(jobs).find((k) => jobs[k] && jobs[k].phase === "ready");
if (readyKey) {
if (!processApiReady(now)) return;
await runFinishPhase(jobToCtx(jobs[readyKey]));
return;
}
if (!processApiReady(now)) return;
const midKey = Object.keys(jobs).find((k) => {
const p = jobs[k] && jobs[k].phase;
return p && TAIL_STEP_PHASES.includes(p);
});
if (midKey) {
await runTailStep(midKey, jobs[midKey]);
return;
}
const waitingKey = Object.keys(jobs)
.filter((k) => {
const j = jobs[k];
return j && j.phase === "waiting" && j.tailDue && j.tailDue <= now;
})
.sort((a, b) => (jobs[a].tailDue || 0) - (jobs[b].tailDue || 0))[0];
if (waitingKey) {
jobs[waitingKey].phase = "tail_s0";
saveJobs(jobs);
await runTailStep(waitingKey, jobs[waitingKey]);
return;
}
const pendingKey = Object.keys(jobs)
.filter((k) => jobs[k] && jobs[k].phase === "pending")
.sort((a, b) => (jobs[a].createdAt || 0) - (jobs[b].createdAt || 0))[0];
if (pendingKey) {
jobs[pendingKey].phase = "fill_s0";
saveJobs(jobs);
await runTailStep(pendingKey, jobs[pendingKey]);
}
} catch (e) {
pushLog("\u672b\u6bb5\u6302\u949f: " + (e && e.message ? e.message : e));
} finally {
bgRunning = false;
updateJobsPanel();
}
}
function jobNeedsSerialStep(job) {
if (!job || !job.duration) return false;
const last = job.lastPostedSec != null ? job.lastPostedSec : job.startSec || 1;
return last < job.duration - 2;
}
function getSerialQueue(jobs) {
return Object.keys(jobs)
.filter((k) => {
const j = jobs[k];
if (!j || j.phase === "done" || j.phase === "ready") return false;
if (j.phase === "pending" || j.phase === "serial" || j.phase === "stepping") {
return jobNeedsSerialStep(j);
}
return false;
})
.sort((a, b) => (jobs[a].createdAt || 0) - (jobs[b].createdAt || 0));
}
async function runSerialVisit(jobKey, job) {
const ctx = jobToCtx(job);
if (!(await consumeChapterCloudQuota(ctx))) return;
const label = job.title || job.cwrid.slice(0, 8);
const duration = job.duration;
const start = job.startSec || 1;
const last = job.lastPostedSec != null ? job.lastPostedSec : start;
const sec = Math.min(duration, last + STEP_SEC);
await warmPlaySession(ctx, true);
let r0 = null;
let r1 = null;
try {
r0 = await postProcess(ctx, 0, last);
await sleep(400);
r1 = await postProcess(ctx, 1, sec);
if (r1 && r1.code === 1) {
await postProcess(ctx, 0, sec);
await sleep(400);
r1 = await postProcess(ctx, 1, sec);
}
} catch (e) {
pushLog(`[${label}] \u8bbf\u95ee\u5f02\u5e38: ${e && e.message ? e.message : e}`);
}
const code = r1 && r1.code;
job.phase = "serial";
job.stepCount = (job.stepCount || 0) + 1;
job.lastPostedSec = sec;
syncLocal(ctx, sec);
try {
await postProcess(ctx, 3, sec);
} catch (_) {}
const note =
code === 3 ? "\uff08\u9891\u7e41\uff0c\u5df2\u63a8\u8fdb\uff09" : code !== 0 && r1 && r1.msg ? " " + r1.msg : "";
pushLog(
`[${label}] \u8f6e\u8be2 \u7b2c${job.stepCount}\u6b65 \u8fdb\u5165@${last}s → +300s@${sec}s code=${code != null ? code : "?"} → \u5df2\u9000\u51fa${note}`
);
if (sec >= duration - 2) {
job.lastPostedSec = duration;
syncLocal(ctx, duration);
job.phase = "ready";
try {
await postProcess(ctx, 2, duration);
} catch (_) {}
pushLog(`[${label}] \u8fdb\u5ea6\u5df2\u6ee1\uff0c\u6392\u961f\u63d0\u4ea4\u64ad\u5b8c\u8bb0\u5f55`);
}
const jobs = loadJobs();
jobs[jobKey] = job;
saveJobs(jobs);
}
async function runSerialRoundScheduler() {
await keepSessionAlive();
if (!getEnabled() || !getApiMode() || bgRunning || engineRunning) return;
bgRunning = true;
try {
const jobs = loadJobs();
const now = Date.now();
const state = loadSerialState();
let roundDue = state.roundDue || 0;
let roundIdx = state.roundIdx || 0;
if (roundDue > now + ROUND_GAP_MS + 60000) {
pushLog("\u68c0\u6d4b\u5230\u5168\u5c40\u7b49\u5f85\u5f02\u5e38\uff08\u65e7\u7248\u5355\u8bfe\u5931\u8d25\u8bef\u4f24\uff09\uff0c\u5df2\u6062\u590d5\u5206\u949f\u4e00\u8f6e");
roundDue = 0;
saveSerialState({ roundDue: 0, roundIdx });
}
const readyKey = Object.keys(jobs).find((k) => jobs[k] && jobs[k].phase === "ready");
if (readyKey) {
await runFinishPhase(jobToCtx(jobs[readyKey]));
return;
}
const queue = getSerialQueue(jobs);
if (!queue.length) {
if (roundIdx > 0) saveSerialState({ roundDue: 0, roundIdx: 0 });
return;
}
if (now < roundDue) return;
if (roundIdx >= queue.length) {
saveSerialState({ roundDue: now + ROUND_GAP_MS, roundIdx: 0 });
pushLog(
`✓ \u4e00\u8f6e\u5b8c\u6210\uff08${queue.length}\u8bfe\u5404+300s\uff09\uff0c${Math.round(ROUND_GAP_MS / 60000)}\u5206\u949f\u540e\u4e0b\u4e00\u8f6e`
);
return;
}
const key = queue[roundIdx];
const job = jobs[key];
if (!job || !jobNeedsSerialStep(job)) {
saveSerialState({ roundDue, roundIdx: roundIdx + 1 });
return;
}
if (roundIdx === 0 && job.stepCount > 0) {
pushLog(`--- \u65b0\u4e00\u8f6e\u5f00\u59cb\uff08${queue.length}\u8bfe\uff09---`);
}
await runSerialVisit(key, job);
saveSerialState({ roundDue, roundIdx: roundIdx + 1 });
await sleep(VISIT_GAP_MS);
} catch (e) {
pushLog("\u4e32\u884c\u8f6e\u8be2: " + (e && e.message ? e.message : e));
} finally {
bgRunning = false;
updateJobsPanel();
}
}
function hasPendingFill() {
return Object.values(loadJobs()).some((j) => j && j.phase === "pending");
}
function hangPlayTab(job) {
const playId = job.wareCwid || job.cwid;
if (!getHangTabs() || !playId) return;
const url =
location.origin +
"/course_ware/course_ware_polyv.aspx?cwid=" +
encodeURIComponent(playId) +
"&ff=0&ft=0&t=0";
try {
window.open(url, "hy_cme_" + job.cwrid.slice(0, 8));
pushLog(`[${job.title}] \u5df2\u5f00\u64ad\u653e\u9875\u6807\u7b7e\uff08prtas \u5e76\u884c\u6302\u8bfe\uff09`);
} catch (_) {}
}
async function runPlayHangMode(ctx, job) {
pushLog(`[${job.title || ctx.cwrid.slice(0, 8)}] \u6302\u9875\u6a21\u5f0f\uff1a\u4fdd\u6301\u6253\u5f00\uff0c\u6302\u949f\u8ba1\u65f6\u4e2d`);
window.blockAbnormalPlugin = function () {};
try {
await waitUntil(() => window.player, 20000);
try {
window.player.j2s_pauseVideo();
} catch (_) {}
} catch (_) {}
}
function getQueueDue(job) {
if (job.phase === "ladder_jump" && job.ladderJumpDue) return job.ladderJumpDue;
if (job.phase === "ladder_finish" && job.ladderFinishDue) return job.ladderFinishDue;
if (job.phase === "silent_clock") return job.completeDue || loadSilentState().batchCompleteDue || 0;
if (job.phase === "silent_finish_retry" && job.recordRetryDue) return job.recordRetryDue;
if (job.phase === "rt_stepping" && job.nextStepDue) return job.nextStepDue;
if (job.phase === "rt_finish_retry" && job.recordRetryDue) return job.recordRetryDue;
return job.queueDue || job.tailDue || job.finishDue || job.nextStepDue || 0;
}
function getRemainSec(job) {
const start = job.startSec || 1;
return Math.max(60, (job.duration || 3600) - start + 1);
}
function beginActivePhase(job) {
const now = Date.now();
const start = job.startSec || 1;
const remain = getRemainSec(job);
job.activeSince = now;
job.filledAt = now;
job.finishDue = now + remain * 1000;
job.lastPostedSec = start;
job.heartbeatCount = 0;
job.heartbeatOk = 0;
job.nextHeartbeatDue = now + PROGRESS_MS;
return job;
}
function assignAnchorIfNeeded() {
const jobs = loadJobs();
const entries = Object.entries(jobs).filter(([, j]) => j && j.phase !== "done");
const pending = entries.filter(([, j]) => j.phase === "pending");
if (!pending.length) return false;
if (entries.some(([, j]) => j.phase === "anchor" || j.isAnchor)) return false;
const [key, job] = pending.sort((a, b) => a[1].duration - b[1].duration)[0];
const now = Date.now();
const remain = getRemainSec(job);
job.phase = "anchor";
job.isAnchor = true;
job.activeSince = now;
job.filledAt = now;
job.queueDue = now;
job.finishDue = now + remain * 1000;
job.nextHeartbeatDue = now + PROGRESS_MS;
job.lastPostedSec = job.startSec || 1;
job.heartbeatCount = 0;
job.heartbeatOk = 0;
jobs[key] = job;
saveJobs(jobs);
pushLog(
`[${job.title}] \u6700\u77ed\u8bfe→\u4fdd\u6d3b\u951a\u70b9\uff0c\u7ea6 ${Math.round(remain / 60)} \u5206\u949f\u771f\u5b9e\u5fc3\u8df3\u540e\u6536\u5c3e`
);
return true;
}
function getRealPlaySec(job) {
const base = job.activeSince || job.filledAt || Date.now();
const start = job.lastPostedSec || job.startSec || 1;
const elapsed = Math.floor((Date.now() - base) / 1000);
return Math.min(job.duration, start + elapsed);
}
function getHeartbeatSec(job) {
const wall = getRealPlaySec(job);
const last = job.lastPostedSec || job.startSec || 1;
return Math.min(job.duration, Math.max(last + 1, Math.min(wall, last + 305)));
}
async function postHeartbeat(ctx, sec) {
const r0 = await postProcess(ctx, 0, sec);
await sleep(800);
let r1 = await postProcess(ctx, 1, sec);
if (r1 && r1.code === 1) {
await warmPlaySession(ctx, true);
await postProcess(ctx, 0, sec);
await sleep(800);
r1 = await postProcess(ctx, 1, sec);
}
return { r0, r1 };
}
async function releaseActiveSession() {
const jobs = loadJobs();
const activeKey = Object.keys(jobs).find((k) => jobs[k] && jobs[k].phase === "active");
if (!activeKey) return;
const job = jobs[activeKey];
const sec = getRealPlaySec(job);
try {
await postProcess(jobToCtx(job), 3, sec);
} catch (_) {}
job.phase = job.isAnchor ? "anchor" : "waiting";
jobs[activeKey] = job;
saveJobs(jobs);
}
async function ensureActiveSession(reEnter) {
const jobs = loadJobs();
const now = Date.now();
if (Object.values(jobs).some((j) => j && j.phase === "active")) return;
const filling = hasPendingFill();
let candidates;
if (filling) {
candidates = Object.entries(jobs).filter(([, j]) => j && j.phase === "anchor");
} else {
candidates = Object.entries(jobs).filter(
([, j]) => j && (j.phase === "anchor" || j.phase === "waiting")
);
}
if (!candidates.length) return;
candidates.sort((a, b) => getQueueDue(a[1]) - getQueueDue(b[1]));
const [key, job] = candidates[0];
const label = job.title || job.cwrid.slice(0, 8);
if (!reEnter && job.phase === "waiting") {
beginActivePhase(job);
pushLog(
`[${label}] \u8f6e\u5230\u672c\u8bfe\uff0c\u5f00\u59cb\u7ea6 ${Math.round(getRemainSec(job) / 60)} \u5206\u949f\u771f\u5b9e\u5fc3\u8df3\uff085 \u5206\u949f/\u6b21\uff09\u540e\u6536\u5c3e`
);
}
job.phase = "active";
if (!job.nextHeartbeatDue) job.nextHeartbeatDue = now + PROGRESS_MS;
jobs[key] = job;
saveJobs(jobs);
const ctx = jobToCtx(job);
const sec = job.lastPostedSec || job.startSec || 1;
try {
if (!reEnter) await warmPlaySession(ctx);
const r0 = await postProcess(ctx, 0, sec);
pushLog(
`[${label}] ${reEnter ? "\u6062\u590d" : "\u8fdb\u5165"}\u4fdd\u6d3b status=0 @${sec}s code=${r0 && r0.code}\uff08${formatEta(job.finishDue - now)}\u540e\u6536\u5c3e\uff09`
);
} catch (e) {
pushLog(`[${label}] \u8fdb\u5165\u4f1a\u8bdd\u5f02\u5e38: ${e && e.message ? e.message : e}`);
}
}
async function tickRealHeartbeat(job) {
const now = Date.now();
if (job.phase !== "active" || now >= job.finishDue) return false;
const nextDue = job.nextHeartbeatDue || now + PROGRESS_MS;
if (now < nextDue) return false;
const sec = getHeartbeatSec(job);
const ctx = jobToCtx(job);
const label = job.title || job.cwrid.slice(0, 8);
try {
const { r0, r1 } = await postHeartbeat(ctx, sec);
job.lastHeartbeatAt = now;
job.heartbeatCount = (job.heartbeatCount || 0) + 1;
if (r1 && r1.code === 0) {
job.lastPostedSec = sec;
job.heartbeatOk = (job.heartbeatOk || 0) + 1;
syncLocal(ctx, sec);
}
job.nextHeartbeatDue = now + PROGRESS_MS;
const jobs = loadJobs();
jobs[`${job.uid}:${job.cwrid}:${job.coaid}`] = job;
saveJobs(jobs);
const ok = r1 && r1.code === 0;
pushLog(
`[${label}] ★\u5fc3\u8df3 \u7b2c${job.heartbeatCount}\u6b21 status=0→1 @${sec}s code=${r1 && r1.code}${r1 && r1.msg && r1.code !== 0 ? " " + r1.msg : ""}${ok ? "" : "\uff08\u4e0b\u8f6e\u5c06\u91cd\u8bd5\u8fdb\u5165\uff09"}`
);
return true;
} catch (e) {
pushLog(`[${label}] \u5fc3\u8df3\u5f02\u5e38: ${e && e.message ? e.message : e}`);
return false;
}
}
async function runFillPhase(ctx) {
if (isParallelMode()) {
pushLog("\u4e32\u884c\u8f6e\u8be2\u6a21\u5f0f\uff1a\u8bf7\u5728\u7ae0\u8282\u9875\u767b\u8bb0\uff0c\u540e\u53f0\u8f6e\u6d41\u8fdb\u5165\u5404\u8bfe+300s\u540e\u9000\u51fa");
return;
}
if (!(await consumeChapterCloudQuota(ctx))) return;
const duration = ctx.duration;
const start = ctx.baseSec;
const label = ctx.title || ctx.cwrid.slice(0, 8);
const targets = buildSegmentTargets(start, duration);
const segText = targets.map((t) => `${t}s`).join(" → ");
await warmPlaySession(ctx);
pushLog(`[${label}] \u5feb\u586b \u603b\u957f${duration}s \u8d77\u70b9${start}s \u500d\u7387${getMultiplier()}x`);
pushLog(`\u5206${targets.length}\u6bb5: ${segText}`);
let t0 = Date.now();
let processOk = false;
try {
const r0 = await postProcess(ctx, 0, start);
pushLog(`status=0 @${start}s code=${r0 && r0.code}`);
if (r0 && r0.code === 0) {
t0 = Date.now();
processOk = true;
}
} catch (e) {
pushLog(`status=0 \u5f02\u5e38: ${e && e.message ? e.message : e}`);
}
for (let i = 0; i < targets.length; i++) {
if (!getEnabled()) return;
if (i > 0) {
const waitMs = segmentGapMs();
pushLog(`\u6bb5\u95f4\u7b49\u5f85 ${(waitMs / 1000).toFixed(1)}s…`);
await sleep(waitMs);
}
const target = targets[i];
const isLast = i === targets.length - 1;
const status = isLast ? 2 : 1;
syncLocal(ctx, target);
try {
const res = await postProcess(ctx, status, target);
pushLog(
`API \u7b2c${i + 1}/${targets.length}\u6bb5 ${target}/${duration}s status=${status} code=${res && res.code}${res && res.msg && res.code !== 0 ? " " + res.msg : ""}`
);
if (res && res.code === 0) processOk = true;
} catch (e) {
pushLog(`API \u7b2c${i + 1}\u6bb5 \u5f02\u5e38: ${e && e.message ? e.message : e}`);
}
}
try {
await postProcess(ctx, 3, duration);
} catch (_) {}
pushLog(`[${label}] status=3 \u5df2\u9000\u51fa → \u4e0b\u4e00\u8bfe`);
const now = Date.now();
saveJob(ctx, {
phase: "waiting",
filledAt: now,
finishDue: now + getRemainSec({ startSec: start, duration }) * 1000,
duration,
lastPostedSec: duration,
activeSince: null,
});
pushLog(`[${label}] \u5df2\u6392\u961f\uff0c\u8f6e\u5230\u540e\u9700\u7ea6 ${Math.round(duration / 60)} \u5206\u949f\u771f\u5b9e\u5fc3\u8df3`);
updateJobsPanel();
}
async function runFinishPhase(ctx) {
const job = getJob(ctx);
const duration = (job && job.duration) || ctx.duration;
const label = ctx.title || ctx.cwrid.slice(0, 8);
const fullCtx = Object.assign({}, ctx, { cwid: (job && job.cwid) || ctx.cwid });
if (HY_ENGINE_REQUIRED && (isRealtimeMode() || isSilentMode() || isOfflineMode())) {
if (!(await warmPlaySession(fullCtx, true))) {
pushLog(`[${label}] \u6536\u5c3e\uff1a\u64ad\u653e\u9875\u9884\u70ed/\u4eba\u8138\u672a\u901a\u8fc7\uff0c\u7a0d\u540e\u91cd\u8bd5`);
return;
}
try {
const loop = await _vf(
job || ctx,
"video_finish",
{
skip_enter: !!(job && job.progressFilled),
progress_filled: !!(job && job.progressFilled),
},
{ ctx: fullCtx, maxSteps: 24 }
);
if (loop.needVideoFace) {
saveJob(fullCtx, { engineSessionId: loop.sessionId || "" });
const verified = await ensureHyVideoFaceVerified(fullCtx, false);
if (!verified) return;
saveJob(fullCtx, { engineSessionId: "" });
return runFinishPhase(ctx);
}
if (loop.pause) {
applyHyEnginePause(fullCtx, loop);
return;
}
if (loop.ok) {
saveJob(fullCtx, { engineSessionId: "", progressFilled: true, status2Sent: true, lastPostedSec: duration });
await finalizeVideoChapterDone(fullCtx, job, label, duration, isSilentMode() ? "[\u79bb\u7ebf]" : "[1:1]");
return;
}
if (/code=1|finish_code_1/i.test(String(loop.msg || ""))) {
const gongxu = isGongxuCtx(fullCtx, job);
const retryDue = Date.now() + (gongxu ? GONGXU_FINISH_RETRY_MS : duration * 1000);
saveJob(fullCtx, {
phase: isSilentMode() ? "silent_finish_retry" : "rt_finish_retry",
recordRetryDue: retryDue,
finishDue: retryDue,
status2Sent: true,
progressFilled: true,
lastPostedSec: duration,
engineSessionId: loop.sessionId || "",
});
if (gongxu) scheduleBgRunSoon(GONGXU_FINISH_RETRY_MS + 200);
return;
}
pushLog(`[${label}] \u4e91\u7aef\u6536\u5c3e\u5931\u8d25\uff1a${loop.msg || "\u672a\u77e5"}`);
saveJob(fullCtx, { engineSessionId: "" });
} catch (e) {
pushLog(`[${label}] \u4e91\u7aef\u6536\u5c3e\uff1a${e && e.message ? e.message : e}`);
}
return;
}
pushLog(`[${label}] \u63d0\u4ea4\u64ad\u5b8c\u8bb0\u5f55 @${duration}s`);
await warmPlaySession(fullCtx);
const skipEnter =
(isTailMode() && job && job.status2Sent) ||
((isRealtimeMode() || isSilentMode()) && job && job.progressFilled);
if (!skipEnter) {
try {
await postProcess(fullCtx, 0, Math.max(1, duration - 1));
if (isTailMode()) lockProcessApi();
await sleep(1500);
} catch (_) {}
} else {
pushLog(`[${label}] \u5df2\u6709 status=2\uff0c\u8df3\u8fc7 status=0\uff0c\u76f4\u63a5\u64ad\u5b8c\u8bb0\u5f55`);
}
let rec = null;
try {
rec = await postPlayRecord(fullCtx, duration);
pushLog(`\u64ad\u5b8c\u8bb0\u5f55 @${duration}s code=${rec && rec.code}${rec && rec.msg ? " " + rec.msg : ""}`);
} catch (e) {
pushLog(`\u64ad\u5b8c\u8bb0\u5f55\u5f02\u5e38: ${e && e.message ? e.message : e}`);
}
if (rec && rec.code === 1) {
if (isRealtimeMode()) {
const retryDue = Date.now() + duration * 1000;
pushLog(
`[${label}] [1:1] \u64ad\u5b8c code=1\uff0c${formatEta(retryDue - Date.now())}\u540e\u518d\u8bd5\u64ad\u5b8c\u8bb0\u5f55`
);
saveJob(fullCtx, {
phase: "rt_finish_retry",
recordRetryDue: retryDue,
finishDue: retryDue,
status2Sent: true,
progressFilled: true,
lastPostedSec: duration,
});
} else if (isSilentMode()) {
const retryDue = Date.now() + duration * 1000;
pushLog(
`[${label}] [\u9759\u9ed8] \u64ad\u5b8c code=1\uff0c${formatEta(retryDue - Date.now())}\u540e\u518d\u8bd5\u64ad\u5b8c\u8bb0\u5f55`
);
saveJob(fullCtx, {
phase: "silent_finish_retry",
recordRetryDue: retryDue,
completeDue: retryDue,
status2Sent: true,
progressFilled: true,
lastPostedSec: duration,
});
} else if (isTailMode()) {
const retryDue = getRecordRetryDue(job || { duration, sessionStartAt: Date.now() });
pushLog(
`[${label}] \u65f6\u957f\u4e0d\u8db3\uff08status=2\u5df2\u8fc7\uff09\uff0c\u6302\u949f${formatEta(retryDue - Date.now())}\u540e\u300c1\u6b21\u5fc3\u8df3+\u64ad\u5b8c\u8bb0\u5f55\u300d\u518d\u8bd5\uff08\u9694\u65e5\u4ea6\u53ef\uff09`
);
saveJob(fullCtx, {
phase: "finish_wait",
lastPostedSec: duration,
status2Sent: true,
status2At: (job && job.status2At) || Date.now(),
sessionStartAt: (job && job.sessionStartAt) || (job && job.filledAt) || Date.now(),
recordRetryDue: retryDue,
});
} else if (isSerialMode()) {
const cur = (job && job.lastPostedSec) || duration;
const backSec = Math.max((job && job.startSec) || 1, cur - STEP_SEC);
pushLog(
`[${label}] \u89c2\u770b\u65f6\u957f\u4e0d\u8db3\uff0c\u9000\u56de@${backSec}s \u7ee7\u7eed\u8f6e\u8be2\uff08\u4ec5\u672c\u8bfe\uff0c\u5176\u4ed6\u8bfe\u4ecd5\u5206\u949f\u4e00\u8f6e\uff09`
);
saveJob(fullCtx, {
phase: "serial",
lastPostedSec: backSec,
});
} else {
const remainMs = Math.max(ROUND_GAP_MS, getRemainSec(job || { startSec: 1, duration }) * 1000);
pushLog(`[${label}] \u6709\u6548\u65f6\u957f\u4e0d\u8db3\uff0c${Math.ceil(remainMs / 60000)} \u5206\u949f\u540e\u91cd\u8bd5\u64ad\u5b8c\u8bb0\u5f55`);
saveJob(fullCtx, {
phase: "active",
activeSince: (job && job.activeSince) || Date.now(),
finishDue: Date.now() + remainMs,
nextHeartbeatDue: Date.now(),
});
await ensureActiveSession();
}
updateJobsPanel();
return;
}
if (rec && rec.code === -2) {
updateJobsPanel();
return;
}
try {
await postProcess(fullCtx, 3, duration);
} catch (_) {}
pushLog(`[${label}] status=3 \u5df2\u9000\u51fa → \u4e0b\u4e00\u8bfe`);
await finalizeVideoChapterDone(fullCtx, job, label, duration, "");
}
function getKeepAliveIntervalMs(jobs) {
if (
(isTailMode() && jobs.some((j) => j && j.phase === "waiting")) ||
(isP3ParallelMode() && jobs.some((j) => j && (j.phase === "stepping" || j.phase === "pending"))) ||
(isParallelStepMode() && jobs.some((j) => j && (j.phase === "stepping" || j.phase === "pending")))
) {
return TAIL_WAIT_KEEPALIVE_MS;
}
return KEEPALIVE_MS;
}
async function pingKeepAliveUrl(url) {
let page;
try {
page = await fetchCmeHtmlSafe(url, { __hyEngineMark: true });
} catch (_) {
return { ok: false, login: false };
}
if (page.surveyRedirect) return { ok: true, login: false, survey: true };
const text = page.text || "";
if (isPageLoginRedirect(text)) return { ok: false, login: true };
return { ok: !!page.ok, login: false };
}
async function pingPlayState(job) {
if (!job.cwrid || !job.uid) return { ok: false, skip: true };
try {
const res = await apiGet("/ashx/getCourseWarePlayState.ashx", {
relationId: job.cwrid,
userId: job.uid,
});
if (res && res.code === -2) return { ok: false, login: true };
return { ok: true, login: false };
} catch (_) {
return { ok: false, skip: true };
}
}
async function keepSessionAlive() {
if (!getEnabled() || !getApiMode()) return;
const jobs = Object.values(loadJobs()).filter(
(j) =>
j &&
(j.phase === "waiting" ||
j.phase === "finish_wait" ||
j.phase === "ready" ||
TAIL_STEP_PHASES.includes(j.phase) ||
j.phase === "serial" ||
j.phase === "stepping" ||
j.phase === "pending" ||
j.phase === "anchor" ||
j.phase === "active" ||
j.phase === "rt_stepping" ||
j.phase === "rt_finish_retry" ||
j.phase === "silent_clock" ||
j.phase === "silent_filling" ||
j.phase === "silent_finish_retry")
);
if (!jobs.length) return;
const interval = getKeepAliveIntervalMs(jobs);
if (Date.now() - lastKeepAliveAt < interval) return;
lastKeepAliveAt = Date.now();
const waitingJobs = jobs.filter((j) => j.phase === "waiting");
const pool = waitingJobs.length && isTailMode() ? waitingJobs : jobs;
const job = pool[keepAliveIdx % pool.length];
keepAliveIdx++;
const urls = [];
if (pageType() === "course" && /course\.aspx/i.test(location.pathname)) {
urls.push(location.pathname + location.search);
}
if (job.cid) {
const courseUrl = "/pages/course.aspx?cid=" + encodeURIComponent(job.cid);
if (!urls.includes(courseUrl)) urls.push(courseUrl);
}
if (job.cwid || job.wareCwid) {
urls.push(
"/course_ware/course_ware_polyv.aspx?cwid=" +
encodeURIComponent(job.wareCwid || job.cwid) +
"&ff=0&ft=0&t=0"
);
}
let ok = false;
let loginLost = false;
for (const url of urls) {
try {
const r = await pingKeepAliveUrl(url);
if (r.login) {
loginLost = true;
break;
}
if (r.ok) {
ok = true;
trace(`\u9875\u9762\u4fdd\u6d3b OK ${url.slice(0, 48)} (${job.title || job.cwrid.slice(0, 8)})`);
}
} catch (_) {}
}
if (!loginLost && !ok) {
const st = await pingPlayState(job);
if (st.login) loginLost = true;
else if (st.ok) ok = true;
}
if (loginLost) {
warnLoginIfNeeded(false);
return;
}
if (waitingJobs.length && isTailMode()) {
const nearest = waitingJobs
.filter((j) => j.tailDue)
.sort((a, b) => a.tailDue - b.tailDue)[0];
if (nearest) {
pushLog(
`\u6302\u949f\u4fdd\u6d3b OK\uff08${Math.round(interval / 60000)}\u5206\u949f/\u6b21\uff09\uff0c\u300c${nearest.title || nearest.cwrid.slice(0, 8)}\u300d${formatEta(Math.max(0, nearest.tailDue - Date.now()))}\u540e\u672b5\u5206\uff0c\u5171${waitingJobs.length}\u8bfe\u6302\u949f\u4e2d`
);
}
} else if (ok) {
trace(`\u4f1a\u8bdd\u4fdd\u6d3b OK (${job.title || job.cwrid.slice(0, 8)})`);
}
}
function recountCourseProgress(course) {
if (!course || !course.chapters) return;
course.chapterTotal = course.chapters.length;
course.studyDone = course.chapters.filter((ch) => ch.done).length;
course.examDone = course.chapters.filter((ch) => ch.examDone).length;
course.untouchedCount = course.chapters.filter((ch) => ch.untouched && !ch.done).length;
}
function markPanelChapterStudyDone(ctx) {
if (!ctx || !ctx.cid) return;
const course = panelState.courses.find((c) => c.cid === ctx.cid);
if (!course || !course.chapters || !course.chapters.length) return;
const key = ctx.lsKey || ctx.uid + ctx.cwrid + ctx.coaid;
const ch = course.chapters.find((c) => c.lsKey === key || (ctx.cwrid && c.cwrid === ctx.cwrid));
if (!ch) return;
const wasDone = !!ch.done;
const job = getJob({ uid: ctx.uid, cwrid: ctx.cwrid, coaid: ctx.coaid, cid: ctx.cid, lsKey: key });
const isInteractive = ch.interactive || (job && job.interactive);
ch.done = true;
ch.untouched = false;
if (isInteractive) {
ch.examDone = true;
ch.examStatus = "none";
ch.studyState = "\u5df2\u5b8c\u6210";
ch.progressLabel = "";
} else if (course && isGongxuCourse(course)) {
ch.examDone = true;
ch.examStatus = "none";
ch.studyState = "\u5df2\u5b8c\u6210";
ch.progressLabel = "";
} else {
ch.examDone = false;
ch.examStatus = "exam";
ch.studyState = "\u5f85\u8003\u8bd5";
ch.progressLabel = "";
}
recountCourseProgress(course);
if (!wasDone) {
const courseTitle = course.title || "";
const chapterTitle = ch.title || "\u7ae0\u8282";
pushUserLog(`${courseTitle ? `\u300c${courseTitle}\u300d` : ""}${chapterTitle} · \u89c6\u9891\u5df2\u5b66\u5b8c`);
pushUserLog(getCourseProgressLine(course));
}
const preview = panelState.chapterPreview.find((p) => p.cid === ctx.cid);
if (preview && preview.chapters) {
const pch = preview.chapters.find((c) => c.lsKey === key || (ctx.cwrid && c.cwrid === ctx.cwrid));
if (pch) {
pch.done = true;
pch.untouched = false;
if (isInteractive) {
pch.examDone = true;
pch.examStatus = "none";
pch.studyState = "\u5df2\u5b8c\u6210";
pch.progressLabel = "";
} else if (course && isGongxuCourse(course)) {
pch.examDone = true;
pch.examStatus = "none";
pch.studyState = "\u5df2\u5b8c\u6210";
pch.progressLabel = "";
} else {
pch.examDone = false;
pch.examStatus = "exam";
pch.studyState = "\u5f85\u8003\u8bd5";
pch.progressLabel = "";
}
}
}
}
function findPanelChapterCtxByCwid(cwid) {
if (!cwid) return null;
const target = String(cwid).toUpperCase();
for (const course of panelState.courses) {
if (!course.chapters || !course.chapters.length) continue;
const ch = course.chapters.find((c) => String(c.cwid || "").toUpperCase() === target);
if (ch) {
return {
uid: ch.uid,
cwrid: ch.cwrid,
coaid: ch.coaid,
cid: ch.cid,
cwid: ch.cwid,
lsKey: ch.lsKey,
title: ch.title,
};
}
}
return null;
}
function markPanelChapterExamDone(ctx) {
if (!ctx) return;
let course = ctx.cid ? panelState.courses.find((c) => c.cid === ctx.cid) : null;
const key = ctx.lsKey || (ctx.uid && ctx.cwrid && ctx.coaid ? ctx.uid + ctx.cwrid + ctx.coaid : "");
let ch = null;
if (course && course.chapters && course.chapters.length) {
ch =
course.chapters.find((c) => c.lsKey === key || (ctx.cwrid && c.cwrid === ctx.cwrid)) ||
(ctx.cwid ? course.chapters.find((c) => c.cwid === ctx.cwid) : null);
}
if (!ch && ctx.cwid) {
const hit = findPanelChapterCtxByCwid(ctx.cwid);
if (hit) {
ctx = hit;
course = panelState.courses.find((c) => c.cid === hit.cid);
ch = course?.chapters?.find((c) => c.cwid === hit.cwid);
}
}
if (!course || !ch) return;
if (ch.examDone) return;
ch.examDone = true;
ch.examStatus = "done";
ch.studyState = "\u8003\u8bd5\u901a\u8fc7";
ch.progressLabel = "";
recountCourseProgress(course);
pushUserLog(`${course.title ? `\u300c${course.title}\u300d` : ""}${ch.title || "\u7ae0\u8282"} · \u8003\u8bd5\u901a\u8fc7`);
pushUserLog(getCourseProgressLine(course));
const preview = panelState.chapterPreview.find((p) => p.cid === ctx.cid);
if (preview && preview.chapters) {
const pch = preview.chapters.find((c) => c.lsKey === key || (ctx.cwrid && c.cwrid === ctx.cwrid));
if (pch) {
pch.examDone = true;
pch.examStatus = "done";
pch.studyState = "\u8003\u8bd5\u901a\u8fc7";
pch.progressLabel = "";
}
}
}
function shouldQueueExamAfterVideo(ctx, job) {
if (isGongxuCtx(ctx, job)) return false;
if (!getAutoExam()) return false;
if (skipExamsSession) return false;
const j = job || (ctx ? getJob(ctx) : null);
if (j && j.interactive) return false;
if (j && j.examDone) return false;
const ch = findPanelChapterByLsKey(
(ctx && ctx.lsKey) || (j && j.lsKey) || (ctx && ctx.uid && ctx.cwrid && ctx.coaid ? ctx.uid + ctx.cwrid + ctx.coaid : "")
);
if (ch && ch.interactive) return false;
if (ch && ch.examDone) return false;
return !!resolveExamCwid(ctx, j);
}
function hasPendingExamWork() {
if (!getAutoExam()) return false;
const pendingJobs = Object.values(loadJobs()).some((j) => j && j.phase === "exam_pending");
if (pendingJobs) return true;
const selected = pruneQueueToCourses();
for (const cid of selected) {
const course = panelState.courses.find((c) => c.cid === cid);
if (!course || !course.chapters) continue;
if (isGongxuCourse(course)) continue;
if (course.chapters.some((ch) => !ch.interactive && ch.done && !ch.examDone)) return true;
}
return false;
}
async function finalizeVideoChapterDone(ctx, job, label, studiedSec, sourceTag) {
const duration = (job && job.duration) || ctx.duration;
const fullCtx = Object.assign({}, ctx, { cwid: (job && job.cwid) || ctx.cwid });
try {
await postProcess(fullCtx, 3, duration);
} catch (_) {}
syncLocal(fullCtx, duration);
unlockExamButton();
const tag = sourceTag ? `${sourceTag} ` : "";
const prog = formatRtStudyLine(studiedSec, duration);
if (isGongxuCtx(fullCtx, job)) {
const gj = getJob(fullCtx);
if (!gj || !gj.processExamsDone) {
try {
await submitChapterProcessExams(fullCtx, job, label, { quiet: true });
} catch (_) {}
}
const confirm = await confirmGongxuChapterExamResult(fullCtx, job, label);
if (!confirm.ok) {
const tag = sourceTag ? `${sourceTag} ` : "";
pushLog(
`[${label}] ${tag}\u516c\u9700\u8bfe exam_result \u672a\u786e\u8ba4${confirm.msg ? "\uff1a" + confirm.msg : ""}\uff0c\u7a0d\u540e\u91cd\u8bd5…`
);
saveJob(fullCtx, {
gongxuExamResultRetryDue: Date.now() + GONGXU_FINISH_RETRY_MS,
gongxuUnlockFail: (Number(gj && gj.gongxuUnlockFail) || 0) + 1,
});
scheduleBgRunSoon(GONGXU_FINISH_RETRY_MS + 200);
return { queuedExam: false, examResultPending: true, msg: confirm.msg };
}
saveJob(fullCtx, { gongxuExamResultDone: true, gongxuUnlockFail: 0 });
}
if (shouldQueueExamAfterVideo(fullCtx, job)) {
saveJob(fullCtx, {
phase: "exam_pending",
lastPostedSec: duration,
progressFilled: !!(job && job.progressFilled),
status2Sent: !!(job && job.status2Sent),
examRetryDue: 0,
});
pushLog(`[${label}] ${tag}✓ \u89c6\u9891\u5b66\u5b8c ${prog}\uff0c\u6392\u961f\u8003\u8bd5…`);
refreshPanelAfterChapterDone(fullCtx);
updateJobsPanel();
if (!isParallelMode()) await ensureActiveSession();
if (getEnabled() && getApiMode()) setTimeout(() => runBgScheduler(), 500);
return { queuedExam: true };
}
saveJob(fullCtx, {
phase: "done",
lastPostedSec: duration,
progressFilled: !!(job && job.progressFilled),
status2Sent: !!(job && job.status2Sent),
});
if (isGongxuCtx(fullCtx, job)) {
pushLog(`[${label}] ${tag}✓ \u6302\u949f\u6536\u5c3e\u5b8c\u6210 ${prog}\uff0cexam_result \u5df2\u786e\u8ba4`);
pushUserLog(`${label} · \u672c\u8bfe\u7a0b\u5b8c\u6210\uff08exam_result \u5df2\u6838\u9a8c\uff09`);
} else {
pushLog(`[${label}] ${tag}✓ \u89c6\u9891\u5b66\u5b8c ${prog}`);
}
refreshPanelAfterChapterDone(fullCtx);
updateJobsPanel();
if (!isParallelMode()) await ensureActiveSession();
if (isGongxuCtx(fullCtx, job)) {
scheduleBgRunSoon(400);
} else {
maybeAutoStopWhenFinished();
}
return { queuedExam: false };
}
function refreshPanelAfterChapterDone(ctx) {
markPanelChapterStudyDone(ctx);
panelState.lastCourseListKey = "";
renderCourseList();
renderChapterPreview();
updatePanel();
}
async function completeChapterFromProbe(ctx, job, label, studiedSec) {
pushLog(
`[${label}] [1:1] ✓ \u5b66\u5b8c\u68c0\u6d4b\u901a\u8fc7\uff0c\u63d0\u524d\u5b8c\u6210 ${formatRtStudyLine(studiedSec, job.duration || ctx.duration)}`
);
await finalizeVideoChapterDone(ctx, job, label, studiedSec, "[1:1]");
return true;
}
async function probeRealtimeFinish(ctx, job, label, studiedSec) {
const duration = job.duration || ctx.duration;
const probe = await probePlayRecordFinish(ctx, job, label, "[1:1] ");
if (probe.login) return false;
if (probe.ok) {
pushLog(
`[${label}] [1:1] \u5b66\u5b8c\u68c0\u6d4b ${formatRtStudyLine(studiedSec, duration)} code=0`
);
return completeChapterFromProbe(ctx, job, label, studiedSec);
}
return false;
}
async function tryProbeAfterApiMiss(ctx, job, label, studiedSec, res) {
if (!res || res.code !== 3) return false;
return probeRealtimeFinish(ctx, job, label, studiedSec);
}
async function tickRealtimeStep(job) {
const ctx = jobToCtx(job);
const label = job.title || job.cwrid.slice(0, 8);
const now = Date.now();
const nextDue = job.nextStepDue || 0;
if (now < nextDue) return false;
if (!HY_ENGINE_REQUIRED) return false;
if (!(await requireHyVideoFaceIfNeeded(ctx, label))) {
saveJob(ctx, { nextStepDue: now + 60000, phase: "face_pending" });
return false;
}
if (!(await warmPlaySession(ctx, true))) {
saveJob(ctx, { nextStepDue: now + 120000 });
return false;
}
if (isGongxuCtx(ctx, job)) {
if (shouldGongxuFastFinish(job, ctx) || gongxuLikelyPrefilled(job, ctx)) {
if (await tryUnlockGongxuChapter(ctx, job, label)) return true;
return true;
}
}
try {
const loop = await _vf(
Object.assign({}, job, loadJobs()[jobKey(ctx)]),
"video_realtime",
{ mode: "realtime" },
{ ctx, maxSteps: 24 }
);
if (loop.needVideoFace) {
saveJob(ctx, { engineSessionId: loop.sessionId || "" });
const verified = await ensureHyVideoFaceVerified(ctx, false);
if (!verified) {
saveJob(ctx, { nextStepDue: now + 60000 });
return false;
}
saveJob(ctx, { engineSessionId: "" });
return true;
}
if (loop.pause) {
if (loop.needVideoFace) {
saveJob(ctx, { engineSessionId: loop.sessionId || "" });
injectHyPanelStyles();
const verified = await ensureHyVideoFaceVerified(ctx, false);
if (!verified) {
saveJob(ctx, { nextStepDue: now + 60000, phase: "face_pending" });
return false;
}
saveJob(ctx, { engineSessionId: "" });
return true;
}
applyHyEnginePause(ctx, loop);
return true;
}
if (loop.terminal && loop.ok) {
return handleHyEngineVideoDone(ctx, job, label, loop);
}
if (loop.terminal && !loop.ok) {
handleHyEngineFailure(ctx, job, label, loop, "[1:1]");
if (isGongxuCtx(ctx, job) && (await tryUnlockGongxuChapter(ctx, job, label))) return true;
return false;
}
return true;
} catch (e) {
pushLog(`[${label}] [1:1] \u4e91\u7aef\u5f15\u64ce\uff1a${e && e.message ? e.message : e}`);
saveJob(ctx, { nextStepDue: now + 60000 });
return false;
}
}
function genUuid() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
function saveHdActive(payload) {
localStorage.setItem(HD_ACTIVE_KEY, JSON.stringify(payload || {}));
}
function loadHdActive() {
try {
return JSON.parse(localStorage.getItem(HD_ACTIVE_KEY) || "null");
} catch (_) {
return null;
}
}
function clearHdActive() {
localStorage.removeItem(HD_ACTIVE_KEY);
clearHdLaunchSnapshot();
clearHdMetaCookie();
}
function clearHdSessionArtifacts() {
clearHdActive();
removeHdIframe();
}
function hdApiOk(res) {
if (!res) return false;
const st = res._httpStatus;
if (st === 401 || st === 403) return false;
if (res.code === 0 || res.code === "0" || res.success === true) return true;
if (res.code != null && res.code !== 0 && res.code !== "0" && res.success !== true) {
if (typeof res.code === "number" && res.code !== 200) return false;
}
if (st >= 200 && st < 300) {
if (res.token || res.courseWareId || res.userId || res.vid || res.catalogId != null) return true;
if (res.code == null && res.success == null && !res._raw) return true;
}
return false;
}
function extractHdToken(res, body) {
const b = body || hdApiBody(res);
if (b && b.token) return b.token;
if (res && res.token) return res.token;
return "";
}
function hdApiBody(res) {
if (!res) return null;
if (res.body !== undefined && res.body !== null) return res.body;
if (res.data !== undefined && res.data !== null) return res.data;
return res;
}
function extractHdblUrlFromText(text) {
if (!text) return "";
const patterns = [
/href=["'](https?:\/\/hdbl\.91huayi\.com[^"']+)["']/i,
/href=["'](\/\/hdbl\.91huayi\.com[^"']+)["']/i,
/(https?:\/\/hdbl\.91huayi\.com\/\?[^"'<>\s]+)/i,
];
for (const re of patterns) {
const m = text.match(re);
if (m) return m[1].replace(/&/g, "&").replace(/^\/\//, "https://");
}
return "";
}
function classifyHdProbeTarget(url, status, text) {
const u = String(url || "");
const html = String(text || "");
if (/hdbl\.91huayi\.com/i.test(u)) return { status: "hdbl", url: u.replace(/&/g, "&") };
if (/exam_result_hd\.aspx/i.test(u)) return { status: "done" };
if (/exam_notice_hd\.aspx/i.test(u)) return { status: "notice", url: u };
const fromHtml = extractHdblUrlFromText(html);
if (fromHtml) return { status: "hdbl", url: fromHtml };
if (/exam_result_hd\.aspx/i.test(html)) return { status: "done" };
if (/exam_notice_hd\.aspx/i.test(html)) return { status: "notice", url: u };
return { status: "unknown", url: u, httpStatus: status };
}
function xhrGetFollow(url) {
return new Promise((resolve) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.withCredentials = true;
xhr.onload = () =>
resolve({
url: xhr.responseURL || url,
status: xhr.status,
text: xhr.responseText || "",
});
xhr.onerror = () => resolve({ url, status: 0, text: "" });
xhr.send();
});
}
async function probeHdChapterEntry(cwid) {
const bases = [
`${location.origin}/course_ware/course_ware_hd.aspx?cwid=${encodeURIComponent(cwid)}&t=0`,
`${location.origin}/course_ware/course_ware.aspx?cwid=${encodeURIComponent(cwid)}&t=0`,
];
for (const startUrl of bases) {
const xhrRes = await xhrGetFollow(startUrl);
const hit = classifyHdProbeTarget(xhrRes.url, xhrRes.status, xhrRes.text);
if (hit.status === "hdbl" || hit.status === "done" || hit.status === "notice") return hit;
let url = startUrl;
for (let i = 0; i < 8; i++) {
try {
const r = await fetch(url, { credentials: "include", redirect: "manual" });
const loc = (r.headers && (r.headers.get("Location") || r.headers.get("location"))) || "";
if (r.type === "opaqueredirect" || (r.status >= 300 && r.status < 400 && loc)) {
url = loc.startsWith("http") ? loc : new URL(loc, location.origin).href;
const hop = classifyHdProbeTarget(url, r.status, "");
if (hop.status !== "unknown") return hop;
continue;
}
const html = await r.text().catch(() => "");
const bodyHit = classifyHdProbeTarget(url, r.status, html);
if (bodyHit.status !== "unknown") return bodyHit;
break;
} catch (_) {
break;
}
}
try {
const r = await fetch(startUrl, { credentials: "include", redirect: "follow" });
const hit = classifyHdProbeTarget(r.url || startUrl, r.status, await r.text().catch(() => ""));
if (hit.status !== "unknown") return hit;
} catch (_) {}
}
return { status: "unknown" };
}
function buildHdCmeEntryUrl(cwid) {
return `${location.origin}/course_ware/course_ware_hd.aspx?cwid=${encodeURIComponent(cwid)}&t=0`;
}
async function resolveHdLaunchUrl(cwid) {
const probe = await probeHdChapterEntry(cwid);
if (probe.status === "hdbl") return probe.url;
return null;
}
function findChapterOnPageByCwid(cwid) {
if (!cwid) return null;
return parseCourseChapterJobs().find((ch) => ch.cwid === cwid) || findAnyChapterOnPageByCwid(cwid);
}
function syncInteractiveJobsFromChapters(chapters) {
if (!isHdInteractiveEnabled()) return 0;
if (!chapters || !chapters.length) return 0;
let n = 0;
for (const ch of chapters) {
if (!ch.interactive || !ch.done || !ch.cwid) continue;
const jobs = loadJobs();
const key = Object.keys(jobs).find(
(k) => jobs[k] && jobs[k].interactive && jobs[k].cwid === ch.cwid && jobs[k].phase !== "done"
);
if (!key) continue;
saveJob(jobToCtx(jobs[key]), { phase: "done" });
pushLog(`[${ch.title || ch.cwrid}] [\u4e92\u52a8] \u9875\u9762\u5df2\u5b8c\u6210\uff0c\u5df2\u540c\u6b65\u961f\u5217`);
n++;
}
if (n) updateJobsPanel();
return n;
}
function parseHdLaunchParams(urlStr) {
const u = new URL(urlStr);
const p = u.searchParams;
const num = (k, d = 0) => {
const v = parseFloat(p.get(k));
return isNaN(v) ? d : v;
};
return {
aiAnalysis: num("aiAnalysis", 1),
appId: p.get("appId") || "1",
backUrl: decodeURIComponent(p.get("backUrl") || ""),
businessCourseId: p.get("businessCourseId") || "",
businessCustomParams: p.get("businessCustomParams") || "",
checkDuration: num("checkDuration", 1),
checkDurationNum: num("checkDurationNum", 1),
checkStudyPath: num("checkStudyPath", 1),
clientType: num("clientType", 0),
courseWareId: p.get("courseWareId") || "",
hash: p.get("hash") || "",
noticeUrl: decodeURIComponent(p.get("noticeUrl") || ""),
platformId: p.get("platformId") || "",
userId: p.get("userId") || "",
watchCompletelyFirst: num("watchCompletelyFirst", 1),
timer: num("timer", Math.floor(Date.now() / 1000)),
noDragFirst: num("noDragFirst", 1),
noSpeedFirst: num("noSpeedFirst", 1),
speedMaxNum: num("speedMaxNum", 1),
isShowReturnBtn: num("isShowReturnBtn", 0),
isShowWenjuan: num("isShowWenjuan", 1),
};
}
async function hdFetchJson(path, opts = {}) {
const method = (opts.method || "GET").toUpperCase();
let url = path.startsWith("http") ? path : HDBL_ORIGIN + path;
if (opts.query) {
const u = new URL(url);
Object.entries(opts.query).forEach(([k, v]) => {
if (v != null && v !== "") u.searchParams.set(k, String(v));
});
url = u.href;
}
const headers = {
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json;charset=UTF-8",
Origin: HDBL_ORIGIN,
};
if (opts.referer) headers.Referer = opts.referer;
if (opts.token) headers.token = opts.token;
const init = { method, credentials: "include", headers, __hyEngineMark: true };
if (method !== "GET" && opts.body != null) init.body = JSON.stringify(opts.body);
const r = await fetch(url, init);
const txt = await r.text();
let json;
try {
json = JSON.parse(txt);
} catch (_) {
json = { code: r.ok ? 0 : -1, _raw: txt.slice(0, 200) };
}
json._httpStatus = r.status;
return json;
}
function hdCatalogPayload(sess) {
return {
userId: sess.userId,
courseWareId: sess.courseWareId,
businessCourseId: sess.businessCourseId,
platformId: sess.platformId,
catalogId: sess.catalogId ?? null,
pageType: sess.pageType || "start",
token: sess.token || "",
infoSource: null,
branchQuestionCatalogId: sess.branchQuestionCatalogId ?? null,
branchQuestionOptionId: sess.branchQuestionOptionId ?? null,
branchCatalogId: sess.branchCatalogId ?? null,
mainCatalogId: sess.mainCatalogId ?? null,
currentCatalogType: sess.currentCatalogType ?? null,
};
}
function hdInsertCatalogPayload(sess) {
return Object.assign({}, hdCatalogPayload(sess), { pageType: "start" });
}
function hdPlayRecordPayload(sess) {
return {
userId: sess.userId,
courseWareId: sess.courseWareId,
businessCourseId: sess.businessCourseId,
platformId: sess.platformId,
infoSource: 0,
};
}
function hdDurationPayload(sess) {
return {
userId: sess.userId,
courseWareId: sess.courseWareId,
businessCourseId: sess.businessCourseId,
platformId: sess.platformId,
catalogId: sess.catalogId ?? null,
pageType: sess.pageType || "start",
token: sess.token || "",
infoSource: 0,
};
}
function hdBasePayload(sess) {
return hdDurationPayload(sess);
}
function extractHdMedias(data) {
if (!data || typeof data !== "object") return [];
const out = [];
const seen = new Set();
const add = (m) => {
if (!m || typeof m !== "object") return;
const vid = m.vid || m.videoId || m.mediaId || m.polyvVid || m.videoVid || m.vId;
if (!vid || seen.has(vid)) return;
seen.add(String(vid));
out.push({
vid: String(vid),
fileSize: m.fileSize || m.videoSize || "",
duration: Math.max(
0,
Math.floor(Number(m.duration || m.videoDuration || m.playDuration || m.totalTime || 0) || 0)
),
});
};
if (Array.isArray(data.catalogInfoList)) {
data.catalogInfoList.forEach((cat) => {
const vos = cat && cat.catalogViewInfoVO;
if (!Array.isArray(vos)) return;
vos.forEach((vo) => {
if (!vo || !vo.vid) return;
const ct = String(vo.contentType || vo.vidType || "").toLowerCase();
if (ct === "videoupload" || ct === "video" || vo.vidType === "video") add(vo);
});
});
}
["videoList", "mediaList", "hdVideoList", "videos", "hdMediaList", "playList", "mediaInfoList"].forEach(
(k) => {
const arr = data[k];
if (Array.isArray(arr)) arr.forEach(add);
}
);
add(data);
if (data.video && typeof data.video === "object") add(data.video);
const walk = (node, depth) => {
if (!node || depth > 10) return;
if (typeof node === "string") {
if (/^[0-9a-f]{10,}_[0-9]+$/i.test(node)) add({ vid: node, duration: 0 });
return;
}
if (Array.isArray(node)) {
node.forEach((x) => walk(x, depth + 1));
return;
}
if (typeof node === "object") {
if (node.vid || node.videoId || node.mediaId) add(node);
Object.values(node).forEach((x) => walk(x, depth + 1));
}
};
walk(data, 0);
return out;
}
function extractDurationForVid(data, vid) {
if (!data || !vid) return 0;
const target = String(vid).toLowerCase();
let found = 0;
const walk = (node, depth) => {
if (found > 0 || !node || depth > 14) return;
if (Array.isArray(node)) {
node.forEach((x) => walk(x, depth + 1));
return;
}
if (typeof node !== "object") return;
const nodeVid = node.vid || node.videoId || node.mediaId;
if (nodeVid && String(nodeVid).toLowerCase() === target) {
const d = Number(
node.duration || node.videoDuration || node.playDuration || node.totalTime || node.mediaDuration || 0
);
if (d > 0) found = Math.ceil(d);
}
Object.values(node).forEach((x) => walk(x, depth + 1));
};
walk(data, 0);
return found;
}
function estimateHdVideoDurationFromFileSize(fileSize) {
const bytes = Number(String(fileSize || "").replace(/[^\d.]/g, ""));
if (!bytes || bytes <= 0) return 0;
const rate = bytes < 8000000 ? 180000 : bytes < 30000000 ? 220000 : 280000;
const pad = bytes < 8000000 ? 25 : bytes < 30000000 ? 20 : 15;
return Math.max(60, Math.min(7200, Math.ceil(bytes / rate) + pad));
}
function loadHdVidDurCache() {
try {
return JSON.parse(localStorage.getItem(HD_VID_DUR_KEY) || "{}");
} catch (_) {
return {};
}
}
function getHdVidDurFromCache(vid) {
const sec = Number(loadHdVidDurCache()[String(vid || "").toLowerCase()] || 0);
return sec > 0 ? Math.ceil(sec) : 0;
}
function saveHdVidDurCache(vid, sec) {
const n = Math.ceil(Number(sec) || 0);
if (!vid || n <= 0) return;
const cache = loadHdVidDurCache();
cache[String(vid).toLowerCase()] = n;
const keys = Object.keys(cache);
if (keys.length > 400) {
keys.slice(0, keys.length - 400).forEach((k) => delete cache[k]);
}
localStorage.setItem(HD_VID_DUR_KEY, JSON.stringify(cache));
}
function normalizePolyvDurationSec(val) {
const n = Number(val);
if (!n || n <= 0) return 0;
if (n > 86400) return Math.floor(n / 1000);
return Math.max(1, Math.floor(n));
}
function normalizePlayerDurationSec(val) {
const n = Number(val);
if (!n || !isFinite(n) || n <= 0) return 0;
return Math.max(1, Math.floor(n));
}
function hdParseClockTextToSec(text) {
const s = String(text || "").trim();
const hm = s.match(/^(\d{1,2}):(\d{2})$/);
if (hm) return parseInt(hm[1], 10) * 60 + parseInt(hm[2], 10);
const hms = s.match(/^(\d{1,2}):(\d{2}):(\d{2})$/);
if (hms) return parseInt(hms[1], 10) * 3600 + parseInt(hms[2], 10) * 60 + parseInt(hms[3], 10);
return 0;
}
function hdReadPolyvDurationFromDom(doc) {
if (!doc) return 0;
const selectors = [
".pv-duration",
".pv-time-duration",
".plv-duration",
".plv-time-duration",
".pv-time .pv-duration",
".pv-controls-right .pv-duration",
"[class*='time-duration']",
"[class*='pv-duration']",
];
for (const q of selectors) {
try {
const el = doc.querySelector(q);
if (!el) continue;
const d = hdParseClockTextToSec((el.textContent || "").trim());
if (d >= 5 && d <= 7200) return d;
} catch (_) {}
}
const collectClockSecs = (root) => {
const secs = [];
if (!root) return secs;
try {
root.querySelectorAll("span, div, em, i, b, p, label, time").forEach((el) => {
if (el.children.length > 2) return;
const d = hdParseClockTextToSec((el.textContent || "").trim());
if (d >= 5 && d <= 7200) secs.push(d);
});
} catch (_) {}
return secs;
};
try {
const roots = [
doc.querySelector(".pv-video-player"),
doc.querySelector(".plv-player"),
doc.querySelector(".polyv-player"),
doc.querySelector("#video"),
doc.querySelector("video") && doc.querySelector("video").closest("[class*='player'], [class*='video']"),
].filter(Boolean);
for (const root of roots) {
const secs = collectClockSecs(root);
if (secs.length) return Math.max.apply(null, secs);
}
} catch (_) {}
try {
const video = doc.querySelector("video");
if (video) {
let node = video.parentElement;
for (let depth = 0; depth < 10 && node; depth++) {
const secs = collectClockSecs(node);
if (secs.length) return Math.max.apply(null, secs);
node = node.parentElement;
}
}
} catch (_) {}
return 0;
}
function extractPolyvDurationFromPayload(data) {
if (!data) return 0;
const walk = (node, depth) => {
if (!node || depth > 10) return 0;
if (typeof node !== "object") return 0;
for (const k of ["duration", "videoDuration", "totalDuration", "playDuration", "pd"]) {
if (node[k] != null) {
const v = normalizePolyvDurationSec(node[k]);
if (v > 0) return v;
}
}
const nested = [node.body, node.data, node.video, node.meta, node.result];
for (const x of nested) {
const d = walk(x, depth + 1);
if (d > 0) return d;
}
const list = Array.isArray(node) ? node : Object.values(node);
for (const x of list) {
const d = walk(x, depth + 1);
if (d > 0) return d;
}
return 0;
};
return walk(data, 0);
}
function parsePolyvPdxDuration(buf) {
if (!buf || !buf.byteLength) return 0;
let text = "";
try {
text = new TextDecoder("utf-8").decode(buf);
} catch (_) {}
if (text) {
const m1 = text.match(
/\b(?:duration|videoDuration|totalDuration|playDuration|pd)\b[=:"\s]+(\d+(?:\.\d+)?)/i
);
if (m1) return normalizePolyvDurationSec(Number(m1[1]));
const m2 = text.match(/\bduration\s*=\s*["'](\d+(?:\.\d+)?)["']/i);
if (m2) return normalizePolyvDurationSec(Number(m2[1]));
try {
const d = extractPolyvDurationFromPayload(JSON.parse(text));
if (d > 0) return d;
} catch (_) {}
}
return 0;
}
function buildPolyvPlayinfoUrls(vid, param, token) {
const userid = param.userid || param.userId || param.uid || "";
const ts = param.ts != null ? String(param.ts) : "";
const sign = param.sign || "";
const hash = param.hash || param.h || "";
const out = [];
const push = (base, extra) => {
const u = new URL(base);
u.searchParams.set("vid", vid);
if (userid) u.searchParams.set("userid", userid);
if (ts) u.searchParams.set("ts", ts);
if (sign) u.searchParams.set("sign", sign);
if (hash) u.searchParams.set("hash", hash);
if (token) u.searchParams.set("token", token);
if (extra) {
Object.keys(extra).forEach((k) => u.searchParams.set(k, extra[k]));
}
out.push(u.toString());
};
if (userid || token) {
push("https://hls.videocc.net/player/playinfo", token ? { device: "desktop" } : null);
}
if (userid && ts && sign) {
push("https://player.polyv.net/secure/" + encodeURIComponent(vid) + ".json");
}
return out;
}
async function hdFetchExternalJson(url) {
const r = await fetch(url, { credentials: "omit", mode: "cors" });
const text = await r.text();
try {
return JSON.parse(text);
} catch (_) {
const m = text.match(
/\b(?:duration|videoDuration|totalDuration|playDuration|pd)\b['"]?\s*[:=]\s*(\d+(?:\.\d+)?)/i
);
return m ? { duration: Number(m[1]) } : null;
}
}
async function hdFetchPolyvPdxDuration(vid, userid, token) {
if (!vid || !userid || !token) return 0;
const baseVid = String(vid).replace(/_9$/i, "_2");
for (const bucket of "0123456789abcdef") {
const url =
"https://hls.videocc.net/" +
encodeURIComponent(userid) +
"/" +
bucket +
"/" +
encodeURIComponent(baseVid) +
".pdx?device=desktop&token=" +
encodeURIComponent(token);
try {
const r = await fetch(url, { credentials: "omit", mode: "cors" });
if (!r.ok) continue;
const d = parsePolyvPdxDuration(await r.arrayBuffer());
if (d > 0) return d;
} catch (_) {}
}
return 0;
}
function unwrapPolyvApiBody(res) {
let data = hdApiBody(res);
if (typeof data === "string") {
const s = data.trim();
if (s.startsWith("{") || s.startsWith("[")) {
try {
data = JSON.parse(s);
} catch (_) {}
}
}
if (data && typeof data === "object" && data.body != null) {
const inner = data.body;
if (typeof inner === "object" && (inner.userid || inner.userId || inner.sign || inner.ts)) {
return inner;
}
if (typeof inner === "string" && inner.length > 8 && !/^\{/.test(inner)) {
return { token: inner };
}
}
return data && typeof data === "object" ? data : {};
}
function extractPlaySafeToken(res) {
const data = unwrapPolyvApiBody(res);
if (typeof data === "string" && data.length > 8) return data.trim();
if (!data || typeof data !== "object") return "";
return String(
data.token || data.playSafeToken || data.playsafeToken || data.body || data.data || ""
).trim();
}
function hdJsonpRequest(url, paramName, timeoutMs) {
return new Promise((resolve) => {
const cb = "_hyPv" + Date.now() + Math.floor(Math.random() * 1e5);
let settled = false;
const finish = (val) => {
if (settled) return;
settled = true;
clearTimeout(timer);
try {
delete window[cb];
} catch (_) {}
if (script.parentNode) script.remove();
resolve(val);
};
const timer = setTimeout(() => finish(null), timeoutMs || 9000);
window[cb] = (data) => finish(data);
const sep = url.includes("?") ? "&" : "?";
const script = document.createElement("script");
script.src = url + sep + encodeURIComponent(paramName || "callback") + "=" + encodeURIComponent(cb);
script.onerror = () => finish(null);
document.head.appendChild(script);
});
}
async function hdTryPolyvPlayinfoUrls(urls) {
const cbNames = ["callback", "jsonpCallback", "cb"];
for (const url of urls) {
try {
const data = await hdFetchExternalJson(url);
const d = extractPolyvDurationFromPayload(data);
if (d > 0) return d;
} catch (_) {}
for (const p of cbNames) {
const data = await hdJsonpRequest(url, p);
const d = extractPolyvDurationFromPayload(data);
if (d > 0) return d;
}
}
return 0;
}
function hdReadPolyvDurationFromWindow(win) {
if (!win) return 0;
let fromDom = 0;
let doc = null;
try {
doc = win.document;
fromDom = hdReadPolyvDurationFromDom(doc);
} catch (_) {}
const apiSecs = [];
try {
const players = [
win.player,
win.polyvPlayerobject,
win.polyvVideoPlayer,
win.polyvPlayer,
];
for (const pl of players) {
if (pl && typeof pl.j2s_getDuration === "function") {
const d = normalizePlayerDurationSec(pl.j2s_getDuration());
if (d > 0) apiSecs.push(d);
}
}
} catch (_) {}
try {
if (doc) {
for (const v of doc.querySelectorAll("video")) {
if (v.duration && isFinite(v.duration) && v.duration > 1) {
const d = normalizePlayerDurationSec(v.duration);
if (d > 0) apiSecs.push(d);
}
}
}
} catch (_) {}
const fromApi = apiSecs.length ? Math.min.apply(null, apiSecs) : 0;
if (fromDom > 0) {
if (fromApi > 0 && fromApi !== fromDom && Math.abs(fromApi - fromDom) <= 2) {
return fromDom;
}
return fromDom;
}
return fromApi;
}
async function hdProbePolyvDurationFromDom(maxMs) {
const start = Date.now();
while (Date.now() - start < (maxMs || 12000)) {
const d = hdReadPolyvDurationFromWindow(window);
if (d > 0) return d;
await sleep(500);
}
return 0;
}
async function hdSyncPageToCurrentCatalog(sess) {
if (!isHdblHost() || !sess || !sess.catalogId) return;
const target = buildHdProblemReferer(sess);
const cur = location.href.split("#")[0];
const tgt = target.split("#")[0];
if (cur === tgt) return;
try {
history.replaceState(null, "", target);
window.dispatchEvent(new PopStateEvent("popstate"));
} catch (_) {
return;
}
await sleep(2800);
}
function hdPauseVisiblePlayers() {
try {
const pause = (pl) => {
if (pl && typeof pl.j2s_pauseVideo === "function") pl.j2s_pauseVideo();
};
pause(window.player);
pause(window.polyvPlayerobject);
pause(window.polyvVideoPlayer);
pause(window.polyvPlayer);
document.querySelectorAll("video").forEach((v) => {
try {
v.pause();
} catch (_) {}
});
} catch (_) {}
}
function hdEnsureVideoApiReferer(sess) {
if (!sess) return;
sess.launchReferer = buildHdProblemReferer(sess);
}
async function hdRetreatToHomePage(sess, reason) {
if (!isHdblHost()) return;
const target = `${HDBL_ORIGIN}/home`;
const cur = location.href.split("#")[0];
hdPauseVisiblePlayers();
if (!cur.endsWith("/home")) {
try {
history.replaceState(null, "", target);
window.dispatchEvent(new PopStateEvent("popstate"));
} catch (_) {}
}
hdEnsureVideoApiReferer(sess);
if (reason) pushLog(`[${sess.label}] [\u4e92\u52a8] ${reason}`);
await sleep(350);
}
async function hdSyncVideoPageForApi(sess) {
hdEnsureVideoApiReferer(sess);
if (!isHdblHost() || !sess || !sess.catalogId) return;
const target = sess.launchReferer;
const cur = location.href.split("#")[0];
const tgt = String(target || "").split("#")[0];
if (cur === tgt) return;
try {
history.replaceState(null, "", target);
window.dispatchEvent(new PopStateEvent("popstate"));
} catch (_) {}
await sleep(300);
}
async function hdFinalizeVideoPlayback(sess, vid, basePayload, finalTime) {
await hdSyncVideoPageForApi(sess);
await hdSendMediaProgress(
sess,
Object.assign({}, basePayload, { eventType: 17, currentPlayTime: finalTime })
);
await hdSendMediaProgress(
sess,
Object.assign({}, basePayload, { eventType: 19, currentPlayTime: finalTime })
);
await hdPost(
"/api/hdCourseWare/insertUserHdVideoPlayRecord",
{
businessCourseId: sess.businessCourseId,
courseWareId: String(sess.courseWareId).toUpperCase(),
platformId: sess.platformId,
userId: sess.userId,
vid,
type: 2,
infoSource: 0,
ban_seek: true,
},
sess
);
pushLog(
`[${sess.label}] [\u4e92\u52a8] \u5df2\u4e0a\u62a5\u5b8c\u64ad\u8bb0\u5f55 type=2 · ${Math.round(finalTime)}\u79d2`
);
}
async function hdProbePolyvDurationViaIframe(sess, maxMs) {
if (!isHdblHost() || !sess || !sess.catalogId) return 0;
const url = buildHdProblemReferer(sess);
return new Promise((resolve) => {
const iframe = document.createElement("iframe");
iframe.setAttribute(
"style",
"position:fixed;left:0;bottom:0;width:320px;height:180px;opacity:0.02;pointer-events:none;z-index:2147483646;border:0"
);
let settled = false;
const finish = (v) => {
if (settled) return;
settled = true;
clearInterval(timer);
iframe.remove();
resolve(v || 0);
};
document.body.appendChild(iframe);
iframe.src = url;
const start = Date.now();
const timer = setInterval(() => {
if (Date.now() - start > (maxMs || 18000)) return finish(0);
try {
const d = hdReadPolyvDurationFromWindow(iframe.contentWindow);
if (d > 0) return finish(d);
} catch (_) {}
}, 600);
});
}
function resolveHdVideoDuration(sess, media, catalogData) {
if (media && media.duration > 0) {
return { duration: media.duration, source: "\u76ee\u5f55\u5b57\u6bb5" };
}
const fromData = extractDurationForVid(catalogData, media && media.vid);
if (fromData > 0) return { duration: fromData, source: "\u76ee\u5f55\u6570\u636e" };
const fromSize = estimateHdVideoDurationFromFileSize(media && media.fileSize);
if (fromSize > 0) {
return {
duration: fromSize,
source: media && media.fileSize ? `\u6587\u4ef6\u5927\u5c0f ${media.fileSize} \u5b57\u8282\u4f30\u7b97` : "\u6587\u4ef6\u5927\u5c0f\u4f30\u7b97",
};
}
const std = Number(sess && sess.hdStandardDuration) || 0;
if (std > 0) {
return { duration: Math.max(120, Math.ceil(std / 5)), source: "\u8bfe\u4ef6\u6807\u51c6\u65f6\u957f\u5206\u644a" };
}
const chk = Number(sess && sess.checkDurationNum) || 0;
if (sess && sess.checkDuration && chk > 0) {
return {
duration: Math.max(300, Math.ceil((chk * 3600) / 5)),
source: "checkDurationNum \u5206\u644a",
};
}
return { duration: 0, source: "" };
}
function syncHdCoursewareMeta(sess, body) {
if (!body || typeof body !== "object") return;
const info = body.coursewareInfo;
if (info && info.hdStandardDuration != null) {
const std = Number(info.hdStandardDuration);
if (std > 0) sess.hdStandardDuration = std;
}
}
function extractHdDirectoryList(data) {
if (!data || typeof data !== "object") return [];
const list = data.directoryList;
if (!Array.isArray(list)) return [];
return list
.slice()
.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
.map((x) => ({
catalogId: String(x.catalogId || ""),
pageType: x.pageType || "view",
title: x.title || x.catalogTitle || "",
disabled: !!x.disabled,
}))
.filter((x) => x.catalogId);
}
function extractHdCatalogList(data) {
const out = [];
const seen = new Set();
const walk = (node, depth) => {
if (!node || depth > 12) return;
if (Array.isArray(node)) {
node.forEach((x) => walk(x, depth + 1));
return;
}
if (typeof node !== "object") return;
const cid = node.catalogId || node.pageCatalogId || node.id;
if (cid && /^[0-9A-F-]{36}$/i.test(String(cid)) && !seen.has(String(cid).toUpperCase())) {
seen.add(String(cid).toUpperCase());
out.push({
catalogId: String(cid),
pageType: node.pageType || "view",
title: node.title || node.catalogTitle || node.name || "",
});
}
Object.values(node).forEach((x) => walk(x, depth + 1));
};
walk(data, 0);
return out;
}
function extractPreNextEntry(body, option) {
if (!Array.isArray(body)) return null;
const opt = String(option || "").toLowerCase();
return body.find((x) => String((x && x.option) || "").toLowerCase() === opt) || null;
}
function extractNextCatalogId(body, currentId) {
if (!body || typeof body !== "object") return "";
const cur = String(currentId || "").toUpperCase();
const pick = (id) => {
if (!id) return "";
const s = String(id);
return s.toUpperCase() !== cur ? s : "";
};
const nextEntry = extractPreNextEntry(body, "next");
if (nextEntry) return pick(nextEntry.catalogId);
if (Array.isArray(body)) return "";
const direct = [
body.nextCatalogId,
body.nextPageCatalogId,
body.nextId,
body.next && (body.next.catalogId || body.next.id),
body.nextCatalog && (body.nextCatalog.catalogId || body.nextCatalog.id),
body.afterCatalog && body.afterCatalog.catalogId,
];
for (const f of direct) {
const n = pick(f);
if (n) return n;
}
if (String(body.option || "").toLowerCase() === "next") {
const n = pick(body.catalogId);
if (n) return n;
}
const list = extractHdCatalogList(body);
if (list.length >= 2) {
const idx = list.findIndex((x) => String(x.catalogId).toUpperCase() === cur);
if (idx >= 0 && idx + 1 < list.length) return list[idx + 1].catalogId;
}
return pick(body.catalogId);
}
function extractNextPageType(body) {
const nextEntry = extractPreNextEntry(body, "next");
if (nextEntry && nextEntry.pageType) return String(nextEntry.pageType);
if (!body || Array.isArray(body)) return "";
return (
body.pageType ||
(body.nextCatalog && body.nextCatalog.pageType) ||
(body.catalog && body.catalog.pageType) ||
""
);
}
function syncBranchFieldsFromData(sess, data) {
if (!data || typeof data !== "object") return;
[
"branchQuestionCatalogId",
"branchQuestionOptionId",
"branchCatalogId",
"mainCatalogId",
"currentCatalogType",
].forEach((k) => {
if (data[k] !== undefined) sess[k] = data[k];
});
}
async function hdFetchCatalogNav(sess, withOption) {
const query = {
userId: sess.userId,
courseWareId: sess.courseWareId,
businessCourseId: sess.businessCourseId,
platformId: sess.platformId,
pageType: sess.pageType || "view",
token: sess.token,
};
if (sess.catalogId) query.catalogId = sess.catalogId;
if (withOption) query.option = "next";
const res = await hdGet("/api/hdCatalog/getCatalogPreAndNextV2", query, sess);
if (res._httpStatus === 401 || res._httpStatus === 403) return { nextId: "", pageType: "" };
const body = hdApiBody(res) || res;
const nextId = extractNextCatalogId(body, sess.catalogId);
const pageType = extractNextPageType(body);
const nextEntry = extractPreNextEntry(body, "next");
if (nextEntry) syncBranchFieldsFromData(sess, nextEntry);
return { nextId, pageType, _navBody: body };
}
async function hdIsCourseFinished(sess) {
try {
const res = await hdGet(
"/api/hdCourseWare/getEndPageInfo",
{
userId: sess.userId,
courseWareId: sess.courseWareId,
businessCourseId: sess.businessCourseId,
platformId: sess.platformId,
pageType: "start",
token: sess.token,
},
sess
);
if (!hdApiOk(res) && res._httpStatus && res._httpStatus >= 400) return false;
const body = hdApiBody(res) || res;
if (body.isFinish === 1 || body.isFinish === true) return true;
if (body.canFinish === 1 || body.canFinish === true) return true;
if (body.finishFlag === 1 || body.finishFlag === true) return true;
if (body.hasRecord === 1 || body.hasRecord === true) return true;
return false;
} catch (_) {
return false;
}
}
function hdPickNextFromDirectory(sess, data) {
if (!data || typeof data !== "object") return null;
const cur = String(sess.catalogId || "").toUpperCase();
const pick = (id, pageType, source, title) => {
if (!id || String(id).toUpperCase() === cur) return null;
return {
catalogId: String(id),
pageType: pageType || "view",
source: source || "\u76ee\u5f55",
title: title || "",
};
};
if (data.pageSkipParams && data.pageSkipParams.catalogId) {
const x = pick(
data.pageSkipParams.catalogId,
data.pageSkipParams.pageType,
"pageSkipParams",
data.pageSkipParams.title
);
if (x) return x;
}
const dirs = extractHdDirectoryList(data);
if (!dirs.length) return null;
const idx = dirs.findIndex((x) => String(x.catalogId).toUpperCase() === cur);
for (let i = idx + 1; i < dirs.length; i++) {
const x = dirs[i];
if (x.disabled) {
const r = pick(x.catalogId, x.pageType, "\u76ee\u5f55\u4e0b\u4e00\u672a\u5b66\u5b8c", x.title);
if (r) return r;
}
}
if (sess.mainCatalogId) {
const mainIdx = dirs.findIndex(
(x) => String(x.catalogId).toUpperCase() === String(sess.mainCatalogId).toUpperCase()
);
if (mainIdx >= 0 && mainIdx + 1 < dirs.length) {
const x = dirs[mainIdx + 1];
const r = pick(x.catalogId, x.pageType, "\u4e3b\u76ee\u5f55\u4e0b\u4e00\u9879", x.title);
if (r) return r;
}
}
const any = dirs.find((x) => x.disabled && String(x.catalogId).toUpperCase() !== cur);
if (any) {
const r = pick(any.catalogId, any.pageType, "\u76ee\u5f55\u672a\u5b66\u5b8c\u9879", any.title);
if (r) return r;
}
return null;
}
function hdIsDirectoryComplete(data) {
const dirs = extractHdDirectoryList(data);
if (!dirs.length) return false;
return dirs.every((x) => !x.disabled);
}
async function hdIsCoursewareProgressComplete(sess) {
const pct = await hdFetchProgressPct(sess);
return pct >= 99;
}
function extractFirstCatalogId(data) {
if (!data || typeof data !== "object") return "";
if (data.pageSkipParams && data.pageSkipParams.catalogId) {
return String(data.pageSkipParams.catalogId);
}
const nextEntry = extractPreNextEntry(data, "next");
if (nextEntry && nextEntry.catalogId) return String(nextEntry.catalogId);
const direct =
data.firstCatalogId ||
data.startCatalogId ||
data.defaultCatalogId ||
data.nextCatalogId ||
data.currentCatalogId;
if (direct) return String(direct);
const list = extractHdCatalogList(data);
if (list.length) return list[0].catalogId;
if (data.catalogId) return String(data.catalogId);
const next = data.nextCatalog;
if (next && next.catalogId) return String(next.catalogId);
return "";
}
function buildHdProblemReferer(sess, extra) {
const u = new URL(`${HDBL_ORIGIN}/problem/view`);
u.searchParams.set("userId", sess.userId);
u.searchParams.set("courseWareId", sess.courseWareId);
u.searchParams.set("businessCourseId", sess.businessCourseId);
u.searchParams.set("platformId", sess.platformId);
if (sess.catalogId) u.searchParams.set("catalogId", sess.catalogId);
u.searchParams.set("pageType", sess.pageType || "view");
if (extra && extra.option) u.searchParams.set("option", extra.option);
return u.href;
}
function buildHdEndReferer(sess) {
const u = new URL(`${HDBL_ORIGIN}/problem/end`);
u.searchParams.set("option", "next");
u.searchParams.set("catalogId", sess.catalogId || "");
u.searchParams.set("pageType", sess.pageType === "end" ? "" : sess.pageType || "");
if (sess.branchQuestionCatalogId) {
u.searchParams.set("branchQuestionCatalogId", sess.branchQuestionCatalogId);
}
if (sess.branchQuestionOptionId) {
u.searchParams.set("branchQuestionOptionId", sess.branchQuestionOptionId);
}
if (sess.branchCatalogId) u.searchParams.set("branchCatalogId", sess.branchCatalogId);
if (sess.mainCatalogId) u.searchParams.set("mainCatalogId", sess.mainCatalogId);
if (sess.currentCatalogType) {
u.searchParams.set("currentCatalogType", sess.currentCatalogType);
}
return u.href;
}
function hdNavIndicatesEnd(navBody) {
if (!Array.isArray(navBody)) return false;
const hasNext = navBody.some(
(x) => String((x && x.option) || "").toLowerCase() === "next" && x && x.catalogId
);
if (hasNext) return false;
return navBody.some((x) => String((x && x.option) || "").toLowerCase() === "pre");
}
async function hdFetchEndPageInfo(sess) {
const res = await hdGet(
"/api/hdCourseWare/getEndPageInfo",
{
userId: sess.userId,
courseWareId: sess.courseWareId,
businessCourseId: sess.businessCourseId,
platformId: sess.platformId,
pageType: "start",
token: sess.token,
},
sess
);
return hdApiBody(res) || {};
}
async function hdRunEndPageFlow(sess) {
pushLog(`[${sess.label}] [\u4e92\u52a8] \u5df2\u5230\u6700\u540e\u4e00\u9875\uff0c\u8fdb\u5165\u7ed3\u675f\u6d41\u7a0b…`);
const endRefererCtx = Object.assign({}, sess, {
branchQuestionCatalogId: sess.branchQuestionCatalogId,
branchQuestionOptionId: sess.branchQuestionOptionId,
branchCatalogId: sess.branchCatalogId,
mainCatalogId: sess.mainCatalogId,
currentCatalogType: sess.currentCatalogType || "question",
});
sess.launchReferer = buildHdEndReferer(endRefererCtx);
try {
const endInfo = await hdFetchEndPageInfo(sess);
if (endInfo && endInfo.score != null) {
pushLog(`[${sess.label}] [\u4e92\u52a8] \u7ed3\u675f\u9875 · \u5f97\u5206 ${endInfo.score}`);
}
} catch (_) {}
sess.pageType = "end";
sess.catalogId = null;
await hdPost(
"/api/hdCourseWare/insertHdDurationLog",
Object.assign({}, hdDurationPayload(sess), { catalogId: null, pageType: "end" }),
sess
);
const finishRes = await hdPost(
"/api/hdCourseWare/finishCourseWare",
Object.assign({}, hdDurationPayload(sess), { catalogId: null, pageType: "start" }),
sess
);
const finishBody = hdApiBody(finishRes) || finishRes;
const msg =
(finishBody && finishBody.resultMsg) ||
(finishBody && finishBody.message) ||
"\u5b66\u4e60\u5b8c\u6bd5";
pushLog(`[${sess.label}] [\u4e92\u52a8] finishCourseWare · ${msg}`);
sess._endFlowDone = true;
return true;
}
async function hdFetchCoursewareProgress(sess) {
const res = await hdGet(
"/api/hdCatalog/getHdCoursewareProgress",
{
userId: sess.userId,
courseWareId: sess.courseWareId,
businessCourseId: sess.businessCourseId,
platformId: sess.platformId,
pageType: sess.pageType || "start",
token: sess.token,
},
sess
);
return hdApiBody(res) || res;
}
async function hdFetchProgressPct(sess) {
try {
const body = await hdFetchCoursewareProgress(sess);
const pct = Number(body);
return isFinite(pct) && pct > 0 ? pct : 0;
} catch (_) {
return 0;
}
}
async function hdResolveFirstCatalog(sess, data, logLabel) {
let first = extractFirstCatalogId(data);
if (!first) {
const progress = await hdFetchCoursewareProgress(sess);
first = extractFirstCatalogId(progress);
}
if (!first) {
const res = await hdGet(
"/api/hdCatalog/getCatalogPreAndNextV2",
{
userId: sess.userId,
courseWareId: sess.courseWareId,
businessCourseId: sess.businessCourseId,
platformId: sess.platformId,
pageType: "view",
token: sess.token,
option: "next",
},
sess
);
const body = hdApiBody(res) || res;
first = extractNextCatalogId(body, "") || extractFirstCatalogId(body);
}
if (!first) return false;
sess.catalogId = first;
sess.pageType = "view";
sess.pageSessionId = genUuid();
sess.launchReferer = buildHdProblemReferer(sess);
pushLog(
`[${sess.label}] [\u4e92\u52a8] ${logLabel || "\u8fdb\u5165\u9996\u76ee\u5f55"} · catalog=${String(first).slice(0, 8)}…`
);
return true;
}
async function hdResolveResumeCatalog(sess, data) {
const progressPct = await hdFetchProgressPct(sess);
if (!progressPct || progressPct <= HD_PROGRESS_FRESH_PCT) {
return hdResolveFirstCatalog(sess, data, "\u8fdb\u5165\u9996\u76ee\u5f55");
}
const dirs = extractHdDirectoryList(data);
const next = dirs.find((x) => x.disabled);
if (next) {
sess.catalogId = next.catalogId;
sess.pageType = next.pageType || "view";
sess.pageSessionId = genUuid();
sess.launchReferer = buildHdProblemReferer(sess);
const label = next.title || next.catalogId.slice(0, 8) + "…";
pushLog(
`[${sess.label}] [\u4e92\u52a8] \u65ad\u70b9\u7eed\u5b66 · \u4ece\u300c${label}\u300d\u7ee7\u7eed\uff08\u8fdb\u5ea6 ${Math.round(progressPct)}%\uff09`
);
return true;
}
if (await hdIsCourseFinished(sess)) return "finished";
return hdResolveFirstCatalog(sess, data, "\u672a\u8bc6\u522b\u65ad\u70b9\uff0c\u4ece\u9996\u76ee\u5f55");
}
function extractHdQuestionId(data) {
if (!data || typeof data !== "object") return "";
let found = "";
const walk = (node, depth) => {
if (found || !node || depth > 14) return;
if (Array.isArray(node)) {
node.forEach((x) => walk(x, depth + 1));
return;
}
if (typeof node !== "object") return;
if (node.questionId || node.questionID) {
found = String(node.questionId || node.questionID);
return;
}
if (node.question && typeof node.question === "object") {
const qid = node.question.id || node.question.questionId || node.question.questionID;
if (qid) {
found = String(qid);
return;
}
}
Object.values(node).forEach((x) => walk(x, depth + 1));
};
walk(data, 0);
return found;
}
function pickChoiceLetter(options) {
if (!options || !options.length) return "A";
const norm = options.map((o, i) => ({
label: String(o.optionLabel || o.label || o.optionKey || String.fromCharCode(65 + i)).toUpperCase(),
raw: o,
}));
const correct = norm.find(
(o) => o.raw.isCorrect === 1 || o.raw.isCorrect === true || o.raw.correct === 1
);
if (correct) return correct.label.replace(/[^A-Z]/g, "").slice(0, 1) || "A";
return norm[0].label.replace(/[^A-Z]/g, "").slice(0, 1) || "A";
}
async function hdPost(path, body, sess) {
return hdFetchJson(path, {
method: "POST",
body,
token: sess && sess.token,
referer: sess && sess.launchReferer,
});
}
async function hdGet(path, query, sess) {
return hdFetchJson(path, {
query,
token: sess && sess.token,
referer: sess && sess.launchReferer,
});
}
async function initHdSessionFromLocation() {
const meta = readHdMetaCookie();
const active = loadHdActive();
const { params, launchReferer } = resolveHdLaunchBundle();
if (!params.courseWareId || !params.userId) {
throw new Error("\u7f3a\u5c11\u4e92\u52a8\u8bfe\u542f\u52a8\u53c2\u6570\uff08URL \u5df2\u88ab SPA \u6e05\u7a7a\uff0c\u4e14\u65e0\u7f13\u5b58\uff09");
}
if (!params.hash) {
throw new Error("\u7f3a\u5c11 hash \u7b7e\u540d\uff08\u8bf7\u56de\u8bfe\u7a0b\u9875\u91cd\u65b0\u542f\u52a8\u4e92\u52a8\u8bfe\uff09");
}
const sess = Object.assign(
{
catalogId: null,
pageType: "start",
token: "",
infoSource: 0,
pendingNextCatalogId: null,
branchQuestionCatalogId: null,
branchQuestionOptionId: null,
branchCatalogId: null,
mainCatalogId: null,
currentCatalogType: null,
coursewareSessionId: genUuid(),
pageSessionId: genUuid(),
launchReferer: launchReferer || location.href,
label:
(meta && meta.title) ||
(active && active.title) ||
(params.businessCustomParams || "").slice(0, 8),
jobKey: (meta && meta.jobKey) || (active && active.jobKey),
cmeOrigin:
(meta && meta.cmeOrigin) ||
(active && active.cmeOrigin) ||
(params.backUrl ? String(params.backUrl).replace(/\/pages\/.*$/i, "") : ""),
},
params
);
await sleep(1500);
const signRes = await hdPost("/api/hdCourseWare/hdCheckSkipSign", params, sess);
if (!hdApiOk(signRes)) {
const hint = signRes.message || signRes.msg || signRes._raw || "\u7b7e\u540d\u65e0\u6548";
throw new Error(
`hdCheckSkipSign \u5931\u8d25(${signRes._httpStatus || "?"}): ${hint}\uff08\u8bf7\u56de\u8bfe\u7a0b\u9875\u91cd\u65b0\u8fdb\u5165\uff09`
);
}
const signToken = extractHdToken(signRes);
if (signToken) sess.token = signToken;
pushLog(`[${sess.label}] [\u4e92\u52a8] \u7b7e\u540d\u6821\u9a8c\u901a\u8fc7`);
const cfgRes = await hdGet(
"/api/hdCourseWare/getHdCoursewareConfig",
{
userId: sess.userId,
courseWareId: sess.courseWareId,
platformId: sess.platformId,
businessCourseId: sess.businessCourseId,
},
sess
);
if (cfgRes && cfgRes._httpStatus === 401) {
throw new Error("getHdCoursewareConfig 401\uff08Referer/hash \u65e0\u6548\uff0c\u8bf7\u56de\u8bfe\u7a0b\u9875\u91cd\u8fdb\uff09");
}
sess.launchReferer = `${HDBL_ORIGIN}/home`;
const initialCatalog = await refreshHdCatalog(sess);
sess._initialCatalog = initialCatalog;
await hdPost("/api/hdCourseWare/insertHdDurationLog", hdDurationPayload(sess), sess);
await hdPost("/api/hdCourseWare/insertCourseWarePlayRecord", hdPlayRecordPayload(sess), sess);
pushLog(
`[${sess.label}] [\u4e92\u52a8] \u4f1a\u8bdd\u5c31\u7eea · courseWare=${String(sess.courseWareId).slice(0, 8)}… · token=${sess.token ? "ok" : "\u5f85\u53d6"}`
);
return sess;
}
async function refreshHdCatalog(sess) {
const res = await hdPost("/api/hdCourseWare/getCourseWareAndCatalogInfo", hdCatalogPayload(sess), sess);
if (!hdApiOk(res)) {
const hint = res.message || res.msg || res._raw || "";
throw new Error(`getCourseWareAndCatalogInfo \u5931\u8d25(${res._httpStatus || "?"}): ${hint}`);
}
const body = hdApiBody(res);
const tok = extractHdToken(res, body);
if (tok) sess.token = tok;
if (body && body.catalogId) sess.catalogId = body.catalogId;
if (body && body.pageType) sess.pageType = body.pageType;
syncBranchFieldsFromData(sess, body);
syncHdCoursewareMeta(sess, body);
const pending = extractNextCatalogId(body, sess.catalogId);
if (pending) sess.pendingNextCatalogId = pending;
return body;
}
async function hdInsertCatalogRecord(sess) {
await hdPost("/api/hdCourseWare/insertCatalogRecord", hdInsertCatalogPayload(sess), sess);
}
async function hdPreparePolyvVideo(sess, vid) {
try {
await hdGet("/api/polyv/getParam", { vid }, sess);
} catch (_) {}
try {
await hdGet("/api/polyv/getPlaySafeToken", { vid }, sess);
} catch (_) {}
}
async function hdFetchPolyvDuration(sess, vid, catalogData) {
const fromData = extractDurationForVid(catalogData, vid);
if (fromData > 0) return { duration: fromData, source: "\u76ee\u5f55\u6570\u636e" };
let param = {};
try {
const paramRes = await hdGet("/api/polyv/getParam", { vid }, sess);
param = unwrapPolyvApiBody(paramRes);
} catch (_) {}
let token = "";
try {
const tokenRes = await hdGet("/api/polyv/getPlaySafeToken", { vid }, sess);
token = extractPlaySafeToken(tokenRes);
} catch (_) {}
const userid =
param.userid ||
param.userId ||
param.uid ||
(String(vid).match(/^([0-9a-f]{10})/i) || [])[1] ||
"";
const playinfoUrls = buildPolyvPlayinfoUrls(vid, param, token);
let d = await hdTryPolyvPlayinfoUrls(playinfoUrls);
if (d > 0) return { duration: d, source: "PolyV playinfo" };
if (token && userid) {
try {
d = await hdFetchPolyvPdxDuration(vid, userid, token);
if (d > 0) return { duration: d, source: "PolyV PDX" };
} catch (_) {}
}
if (isHdblHost()) {
d = hdReadPolyvDurationFromWindow(window);
if (d > 0) return { duration: d, source: "\u64ad\u653e\u5668\u754c\u9762" };
d = await hdProbePolyvDurationFromDom(8000);
if (d > 0) return { duration: d, source: "\u64ad\u653e\u5668\u754c\u9762" };
d = await hdProbePolyvDurationViaIframe(sess, 16000);
if (d > 0) return { duration: d, source: "\u64ad\u653e\u5668\u63a2\u6d4b" };
}
return { duration: 0, source: "" };
}
async function hdCheckMediaComplete(sess, vid) {
try {
const res = await hdGet(
"/api/hdCatalog/getHdMediaIsPlayComplete",
{
userId: sess.userId,
businessCourseId: sess.businessCourseId,
platformId: sess.platformId,
courseWareId: String(sess.courseWareId).toUpperCase(),
vid,
},
sess
);
const body = hdApiBody(res);
return body === true || body === 1 || body === "1";
} catch (_) {
return false;
}
}
async function hdCheckAllVideoComplete(sess, catalogId) {
try {
const res = await hdGet(
"/api/hdCatalog/getHdVideoIsAllPlayComplete",
{
userId: sess.userId,
courseWareId: sess.courseWareId,
businessCourseId: sess.businessCourseId,
platformId: sess.platformId,
catalogId: catalogId || sess.catalogId,
pageType: sess.pageType || "start",
token: sess.token,
},
sess
);
const body = hdApiBody(res);
return body === true || body === 1 || body === "1";
} catch (_) {
return false;
}
}
async function hdWaitMediaComplete(sess, vid, maxMs = 20000) {
const start = Date.now();
while (Date.now() - start < maxMs) {
if (await hdCheckMediaComplete(sess, vid)) return true;
await sleep(1500);
}
return false;
}
async function hdWaitAllVideoComplete(sess, catalogId) {
const start = Date.now();
while (Date.now() - start < 25000) {
if (await hdCheckAllVideoComplete(sess, catalogId)) return true;
await sleep(1500);
}
return false;
}
async function hdSendMediaProgress(sess, payload) {
await hdPost(
"/api/hdLog/mediaProgress",
Object.assign({}, payload, {
userId: sess.userId,
courseWareId: sess.courseWareId,
businessCourseId: sess.businessCourseId,
pageCatalogId: payload.pageCatalogId || sess.catalogId,
coursewareSessionId: sess.coursewareSessionId,
pageSessionId: sess.pageSessionId,
infoSource: 0,
recordTime: new Date().toISOString().slice(0, 19).replace("T", " "),
}),
sess
);
}
async function runHdVideoWatch(sess, media, catalogData) {
const vid = media.vid;
if (await hdCheckMediaComplete(sess, vid)) {
pushLog(
`[${sess.label}] [\u4e92\u52a8] \u89c6\u9891\u5df2\u770b\u5b8c\uff0c\u8df3\u8fc7 · vid=${String(vid).slice(0, 12)}…`
);
return;
}
if (await hdCheckAllVideoComplete(sess, sess.catalogId)) {
pushLog(
`[${sess.label}] [\u4e92\u52a8] \u672c\u9875\u89c6\u9891\u5df2\u5168\u90e8\u770b\u5b8c\uff0c\u8df3\u8fc7 · catalog=${String(sess.catalogId || "").slice(0, 8)}…`
);
return;
}
let duration = 0;
let durationSource = "";
duration = getHdVidDurFromCache(vid);
if (duration > 0) durationSource = "\u672c\u5730\u7f13\u5b58(vid)";
if (duration <= 0) {
const fromCatalog = resolveHdVideoDuration(sess, media, catalogData);
if (fromCatalog.duration > 0 && !/\u4f30\u7b97|\u5206\u644a/.test(fromCatalog.source || "")) {
duration = fromCatalog.duration;
durationSource = fromCatalog.source;
}
}
if (duration <= 0) {
const polyv = await hdFetchPolyvDuration(sess, vid, catalogData);
if (polyv.duration > 0) {
duration = polyv.duration;
durationSource = polyv.source;
saveHdVidDurCache(vid, duration);
}
}
if (duration <= 0 && isHdblHost()) {
await hdSyncPageToCurrentCatalog(sess);
const liveDur = await hdProbePolyvDurationFromDom(8000);
if (liveDur > 0) {
duration = liveDur;
durationSource = "\u64ad\u653e\u5668\u754c\u9762";
saveHdVidDurCache(vid, liveDur);
}
}
if (duration <= 0) {
const fromSize = estimateHdVideoDurationFromFileSize(media && media.fileSize);
if (fromSize > 0) {
duration = fromSize;
durationSource =
media && media.fileSize ? `\u6587\u4ef6\u5927\u5c0f ${media.fileSize} \u5b57\u8282\u4f30\u7b97` : "\u6587\u4ef6\u5927\u5c0f\u4f30\u7b97";
pushLog(
`[${sess.label}] [\u4e92\u52a8] \u672a\u80fd\u8bfb\u53d6 PolyV/\u64ad\u653e\u5668\u771f\u5b9e\u65f6\u957f\uff0c\u6682\u7528\u6587\u4ef6\u5927\u5c0f\u4f30\u7b97\uff08\u53ef\u80fd\u4e0d\u51c6\uff09`
);
}
}
if (duration <= 0) {
duration = Math.max(300, Math.ceil((Number(sess.hdStandardDuration) || 1800) / 5));
durationSource = "\u8bfe\u4ef6\u6807\u51c6\u65f6\u957f\u5206\u644a(\u515c\u5e95)";
pushLog(
`[${sess.label}] [\u4e92\u52a8] \u89c6\u9891\u65f6\u957f\u672a\u8fd4\u56de\uff0c\u6309\u8bfe\u4ef6\u6807\u51c6\u65f6\u957f\u9884\u4f30 · \u7ea6 ${Math.ceil(duration / 60)} \u5206\u949f`
);
}
if (isHdblHost()) {
await hdRetreatToHomePage(
sess,
durationSource === "\u64ad\u653e\u5668\u754c\u9762"
? "\u5df2\u8bfb\u53d6\u64ad\u653e\u5668\u65f6\u957f\uff0c\u8fd4\u56de\u9996\u9875\u540e\u53f0\u7b49\u5f85\uff08\u8bf7\u52ff\u70b9\u64ad\u653e\uff09"
: "\u8fd4\u56de\u9996\u9875\uff0c\u540e\u53f0\u4e0a\u62a5\u89c6\u9891\u8fdb\u5ea6"
);
}
hdEnsureVideoApiReferer(sess);
const initialTarget = duration;
let reportCap = initialTarget;
let target = initialTarget;
let extendCount = 0;
const maxExtend = 2;
pushLog(
`[${sess.label}] [\u4e92\u52a8] \u89c6\u9891\u5f00\u59cb · \u7ea6 ${Math.ceil(target / 60)} \u5206\u949f (${Math.round(target)} \u79d2${durationSource ? " · " + durationSource : ""})`
);
postHdProgress(sess, `\u89c6\u9891\u64ad\u653e\u4e2d 0% · \u7ea6${Math.ceil(target / 60)}\u5206\u949f`, sess.hdStep, sess.hdStepTotal);
const mediaSessionId = genUuid();
const basePayload = {
pageCatalogId: sess.catalogId,
mediaSessionId,
mediaType: 6,
mediaId: vid,
infoSource: 0,
};
await hdPost(
"/api/hdCourseWare/insertUserHdVideoPlayRecord",
{
businessCourseId: sess.businessCourseId,
courseWareId: String(sess.courseWareId).toUpperCase(),
platformId: sess.platformId,
userId: sess.userId,
vid,
type: 1,
infoSource: 0,
ban_seek: true,
},
sess
);
await hdSendMediaProgress(sess, Object.assign({}, basePayload, { eventType: 16, currentPlayTime: 0 }));
let played = 0;
let lastLogPct = -1;
let forceFinish = false;
while (true) {
while (played < target) {
if (!getEnabled() && !isHdblHost()) throw new Error("\u5df2\u6682\u505c");
const remainSec = Math.max(1, target - played);
const waitMs = Math.min(HD_MEDIA_TICK_MS, Math.max(1000, remainSec * 1000));
await sleep(waitMs);
played = Math.min(reportCap, played + waitMs / 1000);
await hdSendMediaProgress(
sess,
Object.assign({}, basePayload, { eventType: 22, currentPlayTime: played })
);
const pct = Math.min(100, Math.round((played / target) * 100));
if (pct >= lastLogPct + 15 || played >= target) {
const detail = `\u89c6\u9891 ${pct}% (${Math.round(played)}/${Math.round(target)}\u79d2)`;
pushLog(`[${sess.label}] [\u4e92\u52a8] ${detail}`);
postHdProgress(sess, detail, sess.hdStep, sess.hdStepTotal);
lastLogPct = pct;
}
}
await hdFinalizeVideoPlayback(sess, vid, basePayload, reportCap);
if (
(await hdWaitMediaComplete(sess, vid, 8000)) ||
(await hdCheckAllVideoComplete(sess, sess.catalogId))
) {
break;
}
if (/\u4f30\u7b97|\u5206\u644a/.test(durationSource) && extendCount < maxExtend) {
const polyv = await hdFetchPolyvDuration(sess, vid, catalogData);
if (polyv.duration > target) {
extendCount++;
target = polyv.duration;
reportCap = polyv.duration;
durationSource = polyv.source;
saveHdVidDurCache(vid, target);
pushLog(
`[${sess.label}] [\u4e92\u52a8] \u83b7\u53d6\u771f\u5b9e\u65f6\u957f ${Math.round(target)} \u79d2\uff0c\u7ee7\u7eed\u7b49\u5f85 (${extendCount}/${maxExtend})…`
);
continue;
}
}
if (extendCount >= maxExtend) {
forceFinish = true;
pushLog(
`[${sess.label}] [\u4e92\u52a8] \u670d\u52a1\u7aef\u672a\u786e\u8ba4\u5b8c\u64ad\uff0c\u6309\u5df2\u4e0a\u62a5\u65f6\u957f\u7ed3\u675f\uff08${Math.round(played)}/${Math.round(initialTarget)}\u79d2\uff09`
);
break;
}
extendCount++;
pushLog(
`[${sess.label}] [\u4e92\u52a8] \u5b8c\u64ad\u6821\u9a8c\u672a\u901a\u8fc7\uff0c\u4fdd\u6301 ${Math.round(reportCap)} \u79d2\u8fdb\u5ea6\u5fc3\u8df3 (${extendCount}/${maxExtend})…`
);
for (let i = 0; i < 2; i++) {
await sleep(15000);
await hdSendMediaProgress(
sess,
Object.assign({}, basePayload, { eventType: 22, currentPlayTime: reportCap })
);
if (
(await hdCheckMediaComplete(sess, vid)) ||
(await hdCheckAllVideoComplete(sess, sess.catalogId))
) {
break;
}
}
forceFinish = true;
break;
}
await hdWaitMediaComplete(sess, vid, forceFinish ? 4000 : 20000);
if (!forceFinish) await hdWaitAllVideoComplete(sess, sess.catalogId);
if (isHdblHost()) {
await hdRetreatToHomePage(sess, "\u89c6\u9891\u5df2\u5b66\u5b8c\uff0c\u8fd4\u56de\u9996\u9875");
}
pushLog(`[${sess.label}] [\u4e92\u52a8] \u89c6\u9891\u9875\u5b8c\u6210`);
}
async function answerHdQuestion(sess, questionId) {
if (!questionId) return;
const qRes = await hdGet("/api/hdQuestion/getQuestionInfoIncludeOptionList", { questionId }, sess);
const qBody = hdApiBody(qRes) || {};
const options = qBody.optionList || qBody.options || qBody.answerList || [];
const letter = pickChoiceLetter(Array.isArray(options) ? options : []);
pushLog(`[${sess.label}] [\u4e92\u52a8] \u7b54\u9898 ${letter}`);
await hdPost(
"/api/hdAnswerRecord/insertAnswerRecord",
{
userId: sess.userId,
courseWareId: sess.courseWareId,
businessCourseId: sess.businessCourseId,
platformId: sess.platformId,
questionId,
userChoiceAnswer: letter,
source: "pc",
questionType: 0,
shortAnswer: "",
},
sess
);
}
async function hdGoNextCatalog(sess) {
if (!sess.catalogId && String(sess.pageType || "").toLowerCase() === "start") {
return null;
}
if (sess.pendingNextCatalogId) {
const pending = String(sess.pendingNextCatalogId);
sess.pendingNextCatalogId = null;
if (pending.toUpperCase() !== String(sess.catalogId || "").toUpperCase()) {
sess.catalogId = pending;
sess.pageType = "view";
sess.pageSessionId = genUuid();
sess.launchReferer = buildHdProblemReferer(sess, { option: "next" });
pushLog(`[${sess.label}] [\u4e92\u52a8] \u4e0b\u4e00\u9875(\u76ee\u5f55\u7f13\u5b58) · catalog=${pending.slice(0, 8)}…`);
return { catalogId: pending };
}
}
let nextId = "";
let nextPageType = "";
let navSource = "";
let nav1 = null;
let nav2 = null;
try {
const beforeId = sess.catalogId;
const fresh = await refreshHdCatalog(sess);
if (
sess.catalogId &&
String(sess.catalogId).toUpperCase() !== String(beforeId || "").toUpperCase()
) {
nextId = String(sess.catalogId);
nextPageType = sess.pageType || "view";
navSource = "\u76ee\u5f55\u5237\u65b0\u8df3\u8f6c";
}
syncBranchFieldsFromData(sess, fresh);
if (!nextId) {
const dirPick = hdPickNextFromDirectory(
Object.assign({}, sess, { catalogId: beforeId }),
fresh
);
if (dirPick) {
nextId = dirPick.catalogId;
nextPageType = dirPick.pageType;
navSource = dirPick.source;
}
}
if (!nextId) {
const fromFresh = extractNextCatalogId(fresh, beforeId);
if (fromFresh) {
nextId = fromFresh;
navSource = "\u76ee\u5f55\u6570\u636e";
}
}
} catch (_) {}
if (!nextId) {
const nav1 = await hdFetchCatalogNav(sess, true);
nextId = nav1.nextId;
nextPageType = nav1.pageType;
if (nextId) navSource = "\u7ffb\u9875API(next)";
if (!nextId) {
nav2 = await hdFetchCatalogNav(sess, false);
nextId = nav2.nextId;
if (!nextPageType) nextPageType = nav2.pageType;
if (nextId) navSource = "\u7ffb\u9875API";
}
}
if (!nextId) {
const progress = await hdFetchCoursewareProgress(sess);
nextId = extractNextCatalogId(progress, sess.catalogId);
if (nextId) navSource = "\u8fdb\u5ea6API";
if (!nextId) {
const list = extractHdCatalogList(progress);
const cur = String(sess.catalogId || "").toUpperCase();
const idx = list.findIndex((x) => String(x.catalogId).toUpperCase() === cur);
if (idx >= 0 && idx + 1 < list.length) {
nextId = list[idx + 1].catalogId;
nextPageType = list[idx + 1].pageType || nextPageType;
navSource = "\u8fdb\u5ea6\u5217\u8868";
}
}
if (!nextId) {
const dirPick = hdPickNextFromDirectory(sess, progress);
if (dirPick) {
nextId = dirPick.catalogId;
nextPageType = dirPick.pageType;
navSource = dirPick.source;
}
}
}
if (!nextId) {
let navHint = "";
let navSnap = null;
try {
navSnap = nav1 && nav1._navBody || nav2 && nav2._navBody;
if (navSnap) {
navHint = ` · nav=${JSON.stringify(navSnap).slice(0, 180)}`;
}
} catch (_) {}
if (hdNavIndicatesEnd(navSnap)) {
pushLog(
`[${sess.label}] [\u4e92\u52a8] \u7ffb\u9875 API \u65e0\u4e0b\u4e00\u9875\uff08\u4ec5 pre\uff09\uff0c\u5224\u5b9a\u4e3a\u6700\u540e\u4e00\u9875${navHint}`
);
return { end: true };
}
pushLog(
`[${sess.label}] [\u4e92\u52a8] \u7ffb\u9875 API \u672a\u8fd4\u56de\u4e0b\u4e00 catalog\uff08\u5f53\u524d ${String(sess.catalogId || "").slice(0, 8)}…${navHint}\uff09`
);
return null;
}
sess.catalogId = String(nextId);
sess.pageType = nextPageType || "view";
sess.pageSessionId = genUuid();
sess.launchReferer = buildHdProblemReferer(sess, { option: "next" });
pushLog(
`[${sess.label}] [\u4e92\u52a8] \u4e0b\u4e00\u9875 · catalog=${String(sess.catalogId).slice(0, 8)}…${navSource ? " · " + navSource : ""}`
);
return { catalogId: sess.catalogId };
}
async function processHdCatalogPage(sess, data) {
const pageLabel = String(sess.pageType || "view").toLowerCase();
const catShort = (sess.catalogId || "").slice(0, 8);
const stepInfo = sess.hdStep ? `\u7b2c ${sess.hdStep} \u9875` : "\u5f53\u524d\u9875";
const medias = extractHdMedias(data);
const qid = extractHdQuestionId(data);
const pageKind = medias.length
? "\u89c6\u9891"
: qid || pageLabel === "question"
? "\u7b54\u9898"
: pageLabel;
pushLog(
`[${sess.label}] [\u4e92\u52a8] ${stepInfo} · ${pageKind} · catalog=${catShort || "?"}`
);
postHdProgress(sess, `${stepInfo} · ${pageKind}`, sess.hdStep, sess.hdStepTotal);
await hdInsertCatalogRecord(sess);
if (medias.length && (await hdCheckAllVideoComplete(sess, sess.catalogId))) {
pushLog(
`[${sess.label}] [\u4e92\u52a8] \u672c\u9875\u89c6\u9891\u5df2\u5168\u90e8\u770b\u5b8c\uff0c\u8df3\u8fc7\u7b49\u5f85 · catalog=${catShort || "?"}`
);
} else {
for (const m of medias) {
if (await hdCheckMediaComplete(sess, m.vid)) {
pushLog(
`[${sess.label}] [\u4e92\u52a8] \u89c6\u9891\u5df2\u770b\u5b8c\uff0c\u8df3\u8fc7 · vid=${String(m.vid).slice(0, 12)}…`
);
continue;
}
await runHdVideoWatch(sess, m, data);
}
}
if (qid || pageLabel === "question") {
await answerHdQuestion(sess, qid);
}
await hdPost("/api/hdCourseWare/insertHdDurationLog", hdDurationPayload(sess), sess);
try {
const fresh = await refreshHdCatalog(sess);
const dirPick = hdPickNextFromDirectory(sess, fresh);
if (dirPick) sess.pendingNextCatalogId = dirPick.catalogId;
else {
const pending = extractNextCatalogId(fresh, sess.catalogId);
if (pending) sess.pendingNextCatalogId = pending;
}
} catch (_) {}
}
async function runInteractiveHdFlow() {
if (!isHdInteractiveEnabled()) return;
if (hdRunnerBusy) return;
hdRunnerBusy = true;
let sess;
if (isHdblHost() && !getEnabled()) setEnabled(true);
try {
pushLog("[\u4e92\u52a8] \u5f00\u59cb API \u6d41\u7a0b");
sess = await initHdSessionFromLocation();
let data = sess._initialCatalog;
delete sess._initialCatalog;
if (!data) data = await refreshHdCatalog(sess);
sess.hdStepTotal = 0;
let guard = 0;
let endFlowDone = false;
while (getEnabled() && guard < 60) {
guard++;
sess.hdStep = guard;
if (String(sess.pageType || "").toLowerCase() === "start" && !sess.catalogId) {
pushLog(`[${sess.label}] [\u4e92\u52a8] \u6b22\u8fce\u9875\uff0c\u8df3\u8fc7\u7b49\u5f85\uff0c\u5b9a\u4f4d\u5b66\u4e60\u4f4d\u7f6e…`);
const resolved = await hdResolveResumeCatalog(sess, data);
if (resolved === "finished") {
await hdRunEndPageFlow(sess);
endFlowDone = true;
pushLog(`[${sess.label}] [\u4e92\u52a8] \u68c0\u6d4b\u5230\u5df2\u5168\u90e8\u5b66\u5b8c\uff0c\u76f4\u63a5\u5b8c\u6210`);
break;
}
if (!resolved) {
throw new Error("\u65e0\u6cd5\u89e3\u6790\u9996\u76ee\u5f55\uff08\u6b22\u8fce\u9875\u540e\u65e0 catalogId\uff09");
}
data = await refreshHdCatalog(sess);
continue;
}
await processHdCatalogPage(sess, data);
const next = await hdGoNextCatalog(sess);
if (next && next.end) {
await hdRunEndPageFlow(sess);
endFlowDone = true;
pushLog(`[${sess.label}] [\u4e92\u52a8] \u8bfe\u7a0b\u5df2\u5168\u90e8\u5b8c\u6210\uff08\u5171 ${guard} \u9875\uff09`);
break;
}
if (!next) {
let finished = await hdIsCourseFinished(sess);
if (!finished) finished = await hdIsCoursewareProgressComplete(sess);
if (!finished) {
try {
const fresh = await refreshHdCatalog(sess);
finished = hdIsDirectoryComplete(fresh);
} catch (_) {}
}
if (!finished) {
try {
const endInfo = await hdFetchEndPageInfo(sess);
if (endInfo && (endInfo.hasRecord === 1 || endInfo.hasRecord === true)) {
finished = true;
}
} catch (_) {}
}
if (finished) {
await hdRunEndPageFlow(sess);
endFlowDone = true;
pushLog(`[${sess.label}] [\u4e92\u52a8] \u8bfe\u7a0b\u5df2\u5168\u90e8\u5b8c\u6210\uff08\u5171 ${guard} \u9875\uff09`);
break;
}
throw new Error(
`\u65e0\u6cd5\u7ffb\u5230\u4e0b\u4e00\u9875\uff08\u5f53\u524d catalog=${String(sess.catalogId || "").slice(0, 8)}…\uff0c\u5df2\u5b8c\u6210 ${guard} \u9875\uff0c\u8bfe\u7a0b\u5c1a\u672a\u7ed3\u675f\uff09`
);
}
data = await refreshHdCatalog(sess);
}
if (!endFlowDone) {
await hdPost("/api/hdCourseWare/finishCourseWare", hdDurationPayload(sess), sess);
}
pushLog(`[${sess.label}] [\u4e92\u52a8] \u5b8c\u6210\uff0c\u8fd4\u56de CME…`);
const cwidParam = sess.businessCustomParams || "";
const metaDone = readHdMetaCookie();
const back =
sess.backUrl ||
(sess.cmeOrigin
? `${sess.cmeOrigin}/pages/exam_result_hd.aspx?businessCustomParams=${encodeURIComponent(cwidParam)}`
: "");
const cwidDone = cwidParam || (metaDone && metaDone.cwid) || "";
if (isHdDetachedRunner()) {
clearHdSessionArtifacts();
postHdIframeEvent("done", { cwid: cwidDone });
setTimeout(tryCloseHdRunnerWindow, 800);
return;
}
clearHdSessionArtifacts();
if (!back) throw new Error("\u65e0\u6cd5\u786e\u5b9a\u8fd4\u56de CME \u5730\u5740");
location.replace(back.startsWith("http") ? back : new URL(back, HDBL_ORIGIN).href);
} catch (e) {
const errMsg = e && e.message ? e.message : String(e);
pushLog("[\u4e92\u52a8] \u5931\u8d25: " + errMsg);
const meta = readHdMetaCookie();
if (isHdDetachedRunner()) {
postHdIframeEvent("fail", {
cwid: meta && meta.cwid,
message: errMsg,
});
setTimeout(tryCloseHdRunnerWindow, 1200);
}
clearHdSessionArtifacts();
} finally {
hdRunnerBusy = false;
}
}
async function launchInteractiveJob(job) {
if (!isHdInteractiveEnabled()) return false;
const cwid = job.cwid;
if (!cwid) {
pushLog(`[${job.title || job.cwrid}] [\u4e92\u52a8] \u7f3a\u5c11 cwid`);
return false;
}
const onPage = findChapterOnPageByCwid(cwid);
if (onPage && onPage.done) {
completeInteractiveJobByCwid(cwid);
pushLog(`[${job.title || job.cwrid}] [\u4e92\u52a8] \u9875\u9762\u5df2\u5b8c\u6210\uff0c\u8df3\u8fc7`);
return true;
}
const probe = await probeHdChapterEntry(cwid);
if (probe.status === "done") {
completeInteractiveJobByCwid(cwid);
pushLog(`[${job.title || job.cwrid}] [\u4e92\u52a8] \u5165\u53e3\u6307\u5411\u7ed3\u679c\u9875\uff0c\u89c6\u4e3a\u5df2\u5b8c\u6210`);
return true;
}
const ctx = jobToCtx(job);
setHdMetaCookie({
title: job.title,
cmeOrigin: location.origin,
jobKey: jobKey(ctx),
cwid,
});
saveHdActive({
jobKey: jobKey(ctx),
cwid,
title: job.title,
cmeOrigin: location.origin,
launchedAt: Date.now(),
launchUrl: probe.status === "hdbl" ? probe.url : "",
});
const entryUrl = buildHdCmeEntryUrl(cwid);
const launchUrl =
probe.status === "hdbl" && probe.url
? probe.url
: probe.status === "notice" && probe.url
? probe.url.startsWith("http")
? probe.url
: new URL(probe.url, location.origin).href
: entryUrl;
saveJob(ctx, {
phase: "hd_running",
hdLaunchUrl: launchUrl,
hdStep: 0,
hdProgressText: "\u542f\u52a8\u4e2d…",
hdLaunchedAt: Date.now(),
});
const probeHint =
probe.status === "hdbl"
? "\u5df2\u89e3\u6790 hdbl+hash"
: probe.status === "notice"
? "\u7ecf\u987b\u77e5\u9875"
: probe.status === "done"
? "\u5df2\u5b8c\u6210"
: `\u8d70 CME \u91cd\u5b9a\u5411\u94fe → ${(probe.url || "").replace(/^https?:\/\/[^/]+/, "").slice(0, 48)}`;
pushLog(`[${job.title}] [\u4e92\u52a8] \u767b\u8bb0\u5b8c\u6210 · cwid=${cwid.slice(0, 8)}… · ${probeHint}`);
await sleep(300);
const useSilentIframe = probe.status === "hdbl" && probe.url;
if (useSilentIframe) {
pushLog(`[${job.title}] [\u4e92\u52a8] \u5c1d\u8bd5\u540e\u53f0 iframe\uff08${HD_IFRAME_WATCHDOG_MS / 1000}s \u65e0\u54cd\u5e94\u5219\u6539\u8ff7\u4f60\u7a97\u53e3\uff09…`);
launchHdInSilentFrame(launchUrl);
armHdHostWatchdog(job, launchUrl, "iframe");
} else {
launchHdPopupWindow(launchUrl, job);
armHdHostWatchdog(job, launchUrl, "popup");
}
return true;
}
function completeInteractiveJobByCwid(cwid) {
if (!cwid) return false;
const jobs = loadJobs();
const key = Object.keys(jobs).find((k) => jobs[k] && jobs[k].interactive && jobs[k].cwid === cwid);
if (!key) return false;
const job = jobs[key];
saveJob(jobToCtx(job), { phase: "done" });
refreshPanelAfterChapterDone(jobToCtx(job));
return true;
}
async function runRealtimeScheduler() {
await keepSessionAlive();
if (!getEnabled() || !getApiMode() || bgRunning || engineRunning) return;
bgRunning = true;
try {
const jobs = loadJobs();
const now = Date.now();
const hdRunning = Object.values(jobs).some((j) => j && j.phase === "hd_running");
if (hdRunning) return;
const retryKey = Object.keys(jobs).find(
(k) => jobs[k] && jobs[k].phase === "rt_finish_retry" && jobs[k].recordRetryDue <= now
);
if (retryKey) {
const retryJob = jobs[retryKey];
const retryCtx = jobToCtx(retryJob);
const retryLabel = retryJob.title || retryJob.cwrid.slice(0, 8);
if (isGongxuCtx(retryCtx, retryJob)) {
await tryUnlockGongxuChapter(retryCtx, retryJob, retryLabel);
} else {
await runFinishPhase(retryCtx);
}
return;
}
const steppingKey = Object.keys(jobs).find((k) => jobs[k] && jobs[k].phase === "rt_stepping");
if (steppingKey) {
await tickRealtimeStep(jobs[steppingKey]);
return;
}
const pending = Object.entries(jobs)
.filter(([, j]) => j && j.phase === "pending" && !j.interactive)
.sort((a, b) => (a[1].createdAt || 0) - (b[1].createdAt || 0));
if (!pending.length) {
const busy = Object.values(jobs).some(
(j) =>
j &&
(j.phase === "rt_stepping" ||
j.phase === "rt_finish_retry" ||
j.phase === "silent_filling" ||
j.phase === "silent_clock" ||
j.phase === "silent_finish_retry" ||
j.phase === "exam_pending")
);
if (!busy) maybeAutoStopWhenFinished();
return;
}
const [, job] = pending[0];
const ctx = jobToCtx(job);
const label = job.title || job.cwrid.slice(0, 8);
if (!HY_ENGINE_REQUIRED && !(await consumeChapterCloudQuota(ctx))) return;
saveJob(ctx, {
phase: "rt_stepping",
nextStepDue: Date.now(),
engineSessionId: "",
});
const liveJob = Object.assign({}, job, loadJobs()[jobKey(ctx)]);
pushUserChapterStudyLog(liveJob, 0);
if (shouldGongxuFastFinish(liveJob, ctx) || gongxuLikelyPrefilled(liveJob, ctx)) {
if (await tryUnlockGongxuChapter(ctx, liveJob, label)) return;
return;
}
ensureRtWallClock(ctx);
await tickRealtimeStep(Object.assign({}, liveJob, loadJobs()[jobKey(ctx)]));
} catch (e) {
pushLog("[1:1] \u8c03\u5ea6: " + (e && e.message ? e.message : e));
} finally {
bgRunning = false;
updateJobsPanel();
updatePanelCloudStatus();
}
}
async function runSilentScheduler() {
await keepSessionAlive();
if (!getEnabled() || !getApiMode() || bgRunning || engineRunning) return;
bgRunning = true;
try {
const sessionUid = getLearningUserId();
let jobs = loadJobs();
const now = Date.now();
const st = loadSilentState();
const jobForSession = (j) => jobBelongsToSession(j, sessionUid);
const retryKey = Object.keys(jobs).find(
(k) =>
jobs[k] &&
jobForSession(jobs[k]) &&
jobs[k].phase === "silent_finish_retry" &&
jobs[k].recordRetryDue <= now
);
if (retryKey) {
await runFinishPhase(jobToCtx(jobs[retryKey]));
return;
}
const batchDue = offlineBatchDueMs(st);
const ready = listOfflineFinishReadyJobs(jobs, sessionUid, now);
if (ready.length) {
const [, job] = ready[0];
const partial = batchDue && now < batchDue;
pushLog(
`[${job.title}] [\u79bb\u7ebf\u6279\u91cf] ${partial ? "\u90e8\u5206\u5230\u70b9" : "\u5230\u70b9"}\u63d0\u4ea4\u6536\u5c3e\uff08\u5269${ready.length}\u8bfe\uff09…`
);
if (partial) {
pushUserLog(`\u6b63\u5728\u5b8c\u6210\u5df2\u5230\u70b9\u8bfe\u7a0b · \u5269 ${ready.length} \u95e8\u5f85\u6536\u5c3e`);
}
await runFinishPhase(jobToCtx(job));
maybeStopAfterPartialOfflineFinish(sessionUid);
return;
}
if (Object.values(jobs).some((j) => j && jobForSession(j) && j.phase === "silent_filling")) return;
const pending = Object.entries(jobs)
.filter(
([, j]) =>
j &&
jobForSession(j) &&
(j.phase === "pending" || j.phase === "face_pending") &&
!j.interactive
)
.sort((a, b) => (a[1].createdAt || 0) - (b[1].createdAt || 0));
const filledCount = Object.values(jobs).filter(
(j) => j && jobForSession(j) && j.phase === "silent_filled"
).length;
const hasWaitingClock = Object.values(jobs).some(
(j) => j && jobForSession(j) && j.phase === "silent_clock"
);
if (hasWaitingClock && !pending.length) return;
const batchInProgress = filledCount > 0 || !!st.batchFillActive || hasWaitingClock;
if (!pending.length) {
if (filledCount) {
const totalMs = assignSilentBatchClock(jobs);
if (totalMs > 0) {
patchSilentState({ batchFillActive: false });
const earlyStats = await runOfflineEarlyFinishScan({
autoFinish: true,
force: true,
quiet: true,
});
const st2 = loadSilentState();
const due = offlineBatchDueMs(st2, totalMs, st2.sessionStart || now);
notifyOfflineBatchFilled(due, Math.max(0, due - Date.now()), earlyStats);
syncRunModePanel();
await completeOfflineBatchAfterFill(earlyStats);
return;
}
}
return;
}
if (!batchInProgress) {
const totalSec = pending.reduce((s, [, j]) => s + (j.duration || 3600), 0);
resetSilentState();
saveSilentState({ sessionStart: Date.now(), batchFillActive: true });
pushLog(
`[\u79bb\u7ebf\u6279\u91cf] \u5f00\u59cb\u5206\u6bb5\u62c9\u6ee1 ${pending.length} \u8bfe\uff0c\u7d2f\u8ba1\u8bfe\u957f\u7ea6 ${formatDurationCn(totalSec)}`
);
pushUserLog(
`\u6b63\u5728\u6279\u91cf\u62c9\u6ee1\u8fdb\u5ea6\uff0c\u5171 ${pending.length} \u8282\uff08\u7d2f\u8ba1\u7ea6 ${formatDurationCn(totalSec)}\uff09\uff0c\u8bf7\u7a0d\u5019…`
);
} else if (hasWaitingClock && pending.length) {
patchSilentState({ batchFillActive: true });
const appendSec = pending.reduce((s, [, j]) => s + (j.duration || 3600), 0);
pushLog(`[\u79bb\u7ebf\u6279\u91cf] \u7b49\u5f85\u4e2d\u8865\u62c9\u65b0\u8bfe ${pending.length} \u8282\uff08\u7ea6 ${formatDurationCn(appendSec)}\uff09\uff0c\u5230\u70b9\u5c06\u6309\u7d2f\u8ba1\u8bfe\u957f\u91cd\u7b97`);
pushUserLog(`\u68c0\u6d4b\u5230\u65b0\u52fe\u9009\u8bfe\u7a0b\uff0c\u6b63\u5728\u8865\u6279\u91cf\u62c9\u6ee1 ${pending.length} \u8282\u65b0\u8bfe…`);
}
let fillIdx = filledCount;
const batchTotal = filledCount + pending.length;
for (const [, job] of pending) {
if (!getEnabled()) break;
fillIdx++;
const ctx = jobToCtx(job);
const label = job.title || job.cwrid.slice(0, 8);
if (!HY_ENGINE_REQUIRED && !(await consumeChapterCloudQuota(ctx))) break;
saveJob(ctx, { phase: "silent_filling" });
const fillRes = await fillChapterSegmentsToFull(ctx, job, {
index: fillIdx,
total: batchTotal,
});
if (fillRes && fillRes.ok) {
saveJob(ctx, {
phase: "silent_filled",
progressFilled: true,
status2Sent: true,
lastPostedSec: job.duration,
filledAt: Date.now(),
engineSessionId: "",
});
try {
syncLocal(ctx, job.duration);
} catch (_) {}
} else if (fillRes && fillRes.needFace) {
saveJob(ctx, { phase: "face_pending", engineSessionId: "" });
injectHyPanelStyles();
const verified = await ensureHyVideoFaceVerified(ctx, false);
if (verified) {
saveJob(ctx, { phase: "pending", engineSessionId: "" });
fillRes = await fillChapterSegmentsToFull(ctx, job, {
index: fillIdx,
total: batchTotal,
});
if (fillRes && fillRes.ok) {
saveJob(ctx, {
phase: "silent_filled",
progressFilled: true,
status2Sent: true,
lastPostedSec: job.duration,
filledAt: Date.now(),
engineSessionId: "",
});
try {
syncLocal(ctx, job.duration);
} catch (_) {}
await sleep(1500);
continue;
}
}
if (!verified || (fillRes && fillRes.needFace)) {
stopAutoStudy("\u8bf7\u626b\u7801\u5b8c\u6210\u770b\u8bfe\u524d\u4eba\u8138\u9a8c\u8bc1\uff0c\u5b8c\u6210\u540e\u70b9\u300c\u5f00\u59cb\u300d\u7ee7\u7eed\u62c9\u6ee1");
break;
}
saveJob(ctx, { phase: "pending", engineSessionId: "" });
pushUserLog(`\u6279\u91cf\u62c9\u6ee1\u5931\u8d25 · ${label}`);
break;
} else if (fillRes && fillRes.needSurvey) {
saveJob(ctx, { phase: "pending", engineSessionId: "" });
break;
} else {
const discovered = discoverSurveyFromPerformance(job.cwrid || job.cwid || "");
if (discovered) {
handleSurveyGateManual(ctx, discovered);
saveJob(ctx, { phase: "pending", engineSessionId: "" });
break;
}
saveJob(ctx, { phase: "pending", engineSessionId: "" });
pushUserLog(`\u6279\u91cf\u62c9\u6ee1\u5931\u8d25 · ${label}`);
if (isOfflineMode()) {
handleSurveyGateManual(ctx, "");
break;
}
break;
}
await sleep(1500);
}
jobs = loadJobs();
const stillPending = Object.values(jobs).some(
(j) => j && jobForSession(j) && (j.phase === "pending" || j.phase === "face_pending")
);
const stillFilling = Object.values(jobs).some(
(j) => j && jobForSession(j) && j.phase === "silent_filling"
);
if (!stillPending && !stillFilling) {
const totalMs = assignSilentBatchClock(jobs);
if (totalMs > 0) {
patchSilentState({ batchFillActive: false });
const earlyStats = await runOfflineEarlyFinishScan({
autoFinish: true,
force: true,
quiet: true,
});
const st2 = loadSilentState();
const due = offlineBatchDueMs(st2, totalMs, st2.sessionStart || now);
const remain = Math.max(0, due - Date.now());
notifyOfflineBatchFilled(due, remain, earlyStats);
syncRunModePanel();
await completeOfflineBatchAfterFill(earlyStats);
return;
}
}
} catch (e) {
pushLog("[\u79bb\u7ebf\u6279\u91cf] \u8c03\u5ea6: " + (e && e.message ? e.message : e));
} finally {
bgRunning = false;
updateJobsPanel();
updatePanelCloudStatus();
}
}
async function runExamPendingScheduler() {
if (!getEnabled() || !getApiMode() || bgRunning || engineRunning) return false;
const jobs = loadJobs();
const now = Date.now();
const pending = Object.entries(jobs)
.filter(
([, j]) =>
j && j.phase === "exam_pending" && (!j.examRetryDue || j.examRetryDue <= now)
)
.sort((a, b) => (a[1].createdAt || 0) - (b[1].createdAt || 0));
if (!pending.length) {
return false;
}
bgRunning = true;
try {
const [, job] = pending[0];
const ctx = jobToCtx(job);
const label = job.title || (job.cwrid || "").slice(0, 8) || "\u8003\u8bd5";
if (!HY_ENGINE_REQUIRED && !(await consumeChapterCloudQuota(ctx))) return;
pushUserLog(`${label} · \u5f00\u59cb\u8003\u8bd5…`);
pushLog(`[${label}] \u89c6\u9891\u5df2\u5b66\u5b8c\uff0cAPI \u7b54\u9898\u4e2d…`);
const res = await afterVideoMaybeExam(ctx, label);
if (res.ok) {
saveJob(ctx, { phase: "done", examDone: true, examFailCount: 0, examRetryDue: 0 });
try {
sessionStorage.removeItem(EXAM_NAV_KEY);
} catch (_) {}
maybeAutoStopWhenFinished();
} else if (res.skipAll) {
skipAllPendingExams(`[${label}] \u5df2\u9009\u62e9\u6682\u4e0d\u8003\u8bd5\uff0c\u672c\u6b21\u8fd0\u884c\u8df3\u8fc7\u6240\u6709\u8003\u8bd5`);
} else if (res.needNavigate && pageType() !== "exam") {
navigateToExamPage(ctx, label);
} else if (res.paused) {
saveJob(ctx, { phase: "exam_pending", examRetryDue: 0 });
} else if (/\u7a0d\u540e\u91cd\u8bd5\u672c\u8282\u8003\u8bd5/.test(res.msg || "")) {
saveJob(ctx, { phase: "exam_pending", examRetryDue: Date.now() + 15000 });
} else {
const fails = (job.examFailCount || 0) + 1;
saveJob(ctx, {
phase: "exam_pending",
examFailCount: fails,
examRetryDue: now + Math.min(30000, 4000 * fails),
});
if (fails >= 3 && /\u672a\u89e3\u6790\u5230\u8bd5\u9898|\u672a\u83b7\u53d6\u8bd5\u5377|ID \u65e0\u6548/.test(res.msg || "")) {
pushLog(`[${label}] \u8fde\u7eed\u62c9\u5377\u5931\u8d25 ${fails} \u6b21\uff0c\u8bf7\u624b\u52a8\u6253\u5f00\u8be5\u8282\u300c\u8fdb\u5165\u8003\u8bd5\u300d\u786e\u8ba4\u9875\u9762\u6b63\u5e38`);
}
}
} catch (e) {
pushLog("\u8003\u8bd5\u8c03\u5ea6: " + (e && e.message ? e.message : e));
} finally {
bgRunning = false;
updateJobsPanel();
updatePanelCloudStatus();
}
return true;
}
function countSessionExamPending() {
const uid = getLearningUserId();
const now = Date.now();
return Object.values(loadJobs()).filter(function (j) {
return (
j &&
jobBelongsToSession(j, uid) &&
j.phase === "exam_pending" &&
(!j.examRetryDue || j.examRetryDue <= now)
);
}).length;
}
function buildOfflineBatchStopMsg(earlyStats) {
const early = earlyStats || {};
if (countSessionExamPending() > 0) {
return "\u90e8\u5206\u8003\u8bd5\u672a\u5b8c\u6210\uff0c\u8bf7\u70b9\u300c\u5f00\u59cb\u300d\u7ee7\u7eed";
}
if (early.finished > 0 && early.waiting === 0) {
return early.examQueued ? "\u63d0\u524d\u6536\u5c3e\u4e0e\u8003\u8bd5\u5df2\u5b8c\u6210" : "\u79bb\u7ebf\u6279\u91cf\u63d0\u524d\u6536\u5c3e\u5b8c\u6210";
}
return "\u79bb\u7ebf\u6279\u91cf\u62c9\u6ee1\u5b8c\u6210\uff0c\u8bf7\u7b49\u5f85\u5230\u70b9\u540e\u518d\u70b9\u5f00\u59cb";
}
async function tryDrainPendingExamsAfterEarlyFinish() {
if (!getEnabled() || !getAutoExam() || !getApiMode()) {
return { done: 0, remain: countSessionExamPending() };
}
if (!(await _crt())) {
return { done: 0, remain: countSessionExamPending() };
}
let done = 0;
for (let i = 0; i < 64; i++) {
if (!getEnabled()) break;
const remain = countSessionExamPending();
if (!remain) break;
if (done === 0) pushUserLog(`\u5f00\u59cb\u81ea\u52a8\u8003\u8bd5 · \u5171 ${remain} \u8282\u5f85\u8003…`);
while (bgRunning || engineRunning) await sleep(300);
const ran = await runExamPendingScheduler();
if (!ran) break;
done++;
await sleep(800);
}
const left = countSessionExamPending();
if (done > 0 && left === 0) {
pushUserLog("\u5f85\u8003\u8bd5\u7ae0\u8282\u5df2\u5168\u90e8\u5b8c\u6210");
} else if (done > 0 && left > 0) {
pushUserLog(`\u5df2\u8003 ${done} \u8282 · \u5c1a\u6709 ${left} \u8282\u5f85\u8003\u8bd5\uff0c\u8bf7\u70b9\u300c\u5f00\u59cb\u300d\u7ee7\u7eed`);
}
return { done, remain: left };
}
async function completeOfflineBatchAfterFill(earlyStats) {
bgRunning = false;
updateJobsPanel();
updatePanelCloudStatus();
if (countSessionExamPending() > 0 && getEnabled()) {
await tryDrainPendingExamsAfterEarlyFinish();
}
stopAutoStudy(buildOfflineBatchStopMsg(earlyStats));
updatePanel();
}
async function runBgScheduler() {
if (!isLeaderTab()) return;
if (!getEnabled() || !getApiMode() || bgRunning || engineRunning) return;
if (detectAccountSwitch()) {
updatePanel();
return;
}
if (!(await _crt())) {
updatePanel();
return;
}
if (hasOfflineBatchFillWork(getLearningUserId())) {
await runSilentScheduler();
maybeAutoStopWhenFinished();
return;
}
if (await runExamPendingScheduler()) return;
const jobs = loadJobs();
if (
isHdInteractiveEnabled() &&
Object.values(jobs).some((j) => j && j.phase === "hd_running")
) {
return;
}
if (isHdInteractiveEnabled()) {
const hdPending = Object.entries(jobs)
.filter(([, j]) => j && j.phase === "pending" && j.interactive)
.sort((a, b) => (a[1].createdAt || 0) - (b[1].createdAt || 0));
if (hdPending.length) {
bgRunning = true;
try {
await launchInteractiveJob(hdPending[0][1]);
} catch (e) {
pushLog("[\u4e92\u52a8] \u8c03\u5ea6: " + (e && e.message ? e.message : e));
} finally {
bgRunning = false;
updateJobsPanel();
updatePanelCloudStatus();
}
return;
}
}
const mode = getRunMode();
if (mode === "realtime") {
await runRealtimeScheduler();
maybeAutoStopWhenFinished();
return;
}
if (mode === "offline") return runSilentScheduler();
if (mode === "ladder") return runLadderScheduler();
if (mode === "p3par") return runP3ParallelScheduler();
if (mode === "pstep") return runParallelStepScheduler();
if (mode === "tail") return runTailScheduler();
if (mode === "serial") return runSerialRoundScheduler();
return runHeartbeatScheduler();
}
async function runHeartbeatScheduler() {
await keepSessionAlive();
if (!getEnabled() || !getApiMode() || bgRunning || engineRunning) return;
bgRunning = true;
try {
for (let round = 0; round < 20; round++) {
assignAnchorIfNeeded();
let jobs = loadJobs();
const now = Date.now();
const keys = Object.keys(jobs);
const readyFinish = keys
.filter((k) => {
const j = jobs[k];
return j && j.phase === "active" && j.activeSince && j.finishDue <= now;
})
.sort((a, b) => jobs[a].finishDue - jobs[b].finishDue);
if (readyFinish.length) {
await runFinishPhase(jobToCtx(jobs[readyFinish[0]]));
continue;
}
await ensureActiveSession();
jobs = loadJobs();
const jobKeys = Object.keys(jobs);
const activeKey = jobKeys.find((k) => jobs[k] && jobs[k].phase === "active");
if (activeKey && jobs[activeKey].finishDue > now) {
if (await tickRealHeartbeat(jobs[activeKey])) {
await sleep(COURSE_GAP_MS);
continue;
}
}
const pending = jobKeys
.filter((k) => jobs[k] && jobs[k].phase === "pending" && !jobs[k].interactive)
.sort((a, b) => (jobs[a].createdAt || 0) - (jobs[b].createdAt || 0));
if (pending.length) {
const job = jobs[pending[0]];
pushLog(`[${job.title}] \u5feb\u586b (\u5269 ${pending.length} \u8bfe\uff0c\u951a\u70b9\u4fdd\u6d3b\u4e2d)`);
await releaseActiveSession();
await runFillPhase(jobToCtx(job));
await ensureActiveSession(true);
await sleep(COURSE_GAP_MS);
continue;
}
if (activeKey || jobKeys.some((k) => jobs[k] && jobs[k].phase === "anchor")) break;
break;
}
} catch (e) {
pushLog("\u5fc3\u8df3\u8c03\u5ea6: " + (e && e.message ? e.message : e));
} finally {
bgRunning = false;
updateJobsPanel();
}
}
function registerJob(ch, meta) {
const rt = resolveChapterRtStart(ch);
const ctx = {
uid: ch.uid,
cwrid: ch.cwrid,
coaid: ch.coaid,
cid: ch.cid,
groupId: meta.groupId,
provinceId: meta.provinceId,
duration: ch.duration,
baseSec: rt.startSec,
lsKey: ch.lsKey,
title: ch.title,
};
const key = jobKey(ctx);
const jobs = loadJobs();
if (jobs[key] && jobs[key].phase !== "done") {
return false;
}
const assumePrefilled =
isGongxuCourseByCid(ch.cid) &&
courseHadOfflineFill(ch.cid) &&
!ch.done &&
!ch.interactive;
const dur = Math.max(60, ch.duration || 3600);
const fullSec = assumePrefilled ? dur : rt.studiedSec;
jobs[key] = {
uid: ch.uid,
cwrid: ch.cwrid,
coaid: ch.coaid,
cid: ch.cid,
cwid: ch.cwrid,
wareCwid: ch.wareCwid || "",
groupId: meta.groupId,
provinceId: meta.provinceId,
duration: dur,
startSec: assumePrefilled ? resolveRtEntrySec(dur, dur) : rt.startSec,
studiedSec: fullSec,
lastPostedSec: assumePrefilled ? dur : rt.studiedSec,
progressFilled: !!assumePrefilled,
status2Sent: !!assumePrefilled,
lsKey: ch.lsKey,
title: ch.title,
interactive: !!ch.interactive,
phase: "pending",
createdAt: Date.now(),
};
if (assumePrefilled) {
try {
syncLocal(ctx, dur);
} catch (_) {}
}
const sessionUid = getLearningUserId();
if (sessionUid && ch.uid && sessionUid !== String(ch.uid)) {
trace(`registerJob uid\u4e0d\u4e00\u81f4 session=${sessionUid} chapter.uid=${ch.uid} key=${key}`);
}
saveJobs(jobs);
return true;
}
function registerExamJob(ch) {
const ctx = {
uid: ch.uid,
cwrid: ch.cwrid,
coaid: ch.coaid,
cid: ch.cid,
lsKey: ch.lsKey,
title: ch.title,
};
const key = jobKey(ctx);
const jobs = loadJobs();
if (jobs[key] && jobs[key].phase === "exam_pending") return false;
jobs[key] = {
uid: ch.uid,
cwrid: ch.cwrid,
coaid: ch.coaid,
cid: ch.cid,
cwid: ch.cwrid,
lsKey: ch.lsKey,
title: ch.title,
interactive: false,
examOnly: true,
phase: "exam_pending",
createdAt: Date.now(),
};
saveJobs(jobs);
return true;
}
function registerInteractiveJob(ch) {
const ctx = {
uid: ch.uid,
cwrid: ch.cwrid,
coaid: ch.coaid,
cid: ch.cid,
lsKey: ch.lsKey,
title: ch.title,
};
const key = jobKey(ctx);
const jobs = loadJobs();
if (jobs[key] && jobs[key].phase !== "done") return false;
jobs[key] = {
uid: ch.uid,
cwrid: ch.cwrid,
coaid: ch.coaid,
cid: ch.cid,
cwid: ch.cwid || ch.cwrid || "",
lsKey: ch.lsKey,
title: ch.title,
interactive: true,
duration: ch.duration || 0,
phase: "pending",
createdAt: Date.now(),
};
saveJobs(jobs);
return true;
}
async function registerPickedChapters(picked, courseTitle) {
if (!picked.length) return { n: 0, skip: 0, registeredKeys: [] };
const visible = isHdInteractiveEnabled() ? picked : picked.filter((ch) => !ch.interactive);
const normal = visible.filter((ch) => !ch.interactive && isChapterAutoStudy(ch));
const examOnly = visible.filter((ch) => !ch.interactive && isChapterNeedExam(ch));
const interactive = isHdInteractiveEnabled()
? visible.filter((ch) => ch.interactive && isChapterNeedStudy(ch))
: [];
let n = 0;
let skip = 0;
const registeredKeys = [];
for (const ch of examOnly) {
if (registerExamJob(ch)) {
n++;
registeredKeys.push(
jobKey({ uid: ch.uid, cwrid: ch.cwrid, coaid: ch.coaid, cid: ch.cid, lsKey: ch.lsKey })
);
pushLog(` · \u5f85\u8003\u8bd5\u300c${ch.title}\u300d→ \u8003\u8bd5\u961f\u5217`);
} else skip++;
}
if (normal.length) {
const meta = await ensureCourseApiMeta(normal[0].cid, normal[0].cwrid);
if (!meta.groupId) {
pushLog(`\u300c${courseTitle}\u300d\u7f3a\u5c11 group_id`);
} else {
for (const ch of normal) {
if (registerJob(ch, meta)) {
n++;
registeredKeys.push(
jobKey({
uid: ch.uid,
cwrid: ch.cwrid,
coaid: ch.coaid,
cid: ch.cid,
groupId: meta.groupId,
provinceId: meta.provinceId,
lsKey: ch.lsKey,
})
);
} else skip++;
}
}
}
for (const ch of interactive) {
if (registerInteractiveJob(ch)) {
n++;
registeredKeys.push(jobKey({ uid: ch.uid, cwrid: ch.cwrid, coaid: ch.coaid, cid: ch.cid, lsKey: ch.lsKey }));
pushLog(` · \u4e92\u52a8\u8bfe\u300c${ch.title}\u300d→ \u961f\u5217\uff08${ch.studyState || "\u5f85\u5b66"}\uff09`);
} else skip++;
}
if (interactive.length) {
pushLog(`\u300c${courseTitle}\u300d\u542b ${interactive.length} \u8282\u4e92\u52a8\u8bfe\uff1aAPI \u6309\u9875\u63a8\u8fdb\uff0c\u89c6\u9891\u6309\u771f\u5b9e\u65f6\u957f\u7b49\u5f85`);
}
if (examOnly.length && !normal.length) {
pushLog(`\u300c${courseTitle}\u300d${examOnly.length} \u8282\u5f85\u8003\u8bd5\uff08\u89c6\u9891\u5df2\u5b66\u5b8c\uff0c\u4ec5 API \u7b54\u9898\uff09`);
}
return { n, skip, registeredKeys, picked: [...normal, ...interactive, ...examOnly], meta: null };
}
function finalizeChapterRegistration(result, beforeActive, courseTitle) {
const { n = 0, skip = 0, registeredKeys = [], picked = [] } = result || {};
if (!registeredKeys.length && !n) return;
if (isP3ParallelMode() && registeredKeys.length) {
const jobs = loadJobs();
const base = Date.now();
registeredKeys.forEach((k, i) => {
if (jobs[k] && jobs[k].phase === "pending") {
jobs[k].nextStepDue = base + i * P3PAR_START_STAGGER_MS;
}
});
saveJobs(jobs);
const wall = estimateP3ParallelWallMin(loadJobs());
pushLog(
registeredKeys.length > 1
? `status=3\u771f\u5e76\u884c\uff1a${registeredKeys.length}\u8bfe\u5404\u72ec\u7acb5\u5206\u949f\u6b65\u8fdb\uff0c\u8bfe\u95f4\u9519\u5f0030s\uff1b\u82e5\u9650\u6d41\u6309\u5355\u8bfe\u5219\u7ea6${wall}\u5206\u5168\u90e8\u8dd1\u5b8c`
: `status=3\u771f\u5e76\u884c\uff1a\u4ec5status=3\u6b65\u8fdb+300s\uff0c\u7ea6${wall}\u5206/\u8bfe`
);
} else if (isLadderMode() && registeredKeys.length) {
assignLadderBatch(loadJobs());
} else if (isParallelStepMode() && registeredKeys.length > 1) {
const jobs = loadJobs();
registeredKeys.sort((a, b) => (jobs[a]?.createdAt || 0) - (jobs[b]?.createdAt || 0));
const base = Date.now();
registeredKeys.forEach((k, i) => {
if (jobs[k] && jobs[k].phase === "pending") {
jobs[k].nextStepDue = base + i * randomStepIntervalMs();
}
});
saveJobs(jobs);
}
const afterActive = Object.values(loadJobs()).filter((j) => j && j.phase !== "done").length;
pushLog(
`\u300c${courseTitle}\u300d\u65b0\u767b\u8bb0 ${n} \u8282${skip ? `\uff08${skip} \u8282\u5df2\u5728\u961f\u5217\u8df3\u8fc7\uff09` : ""}\u3002\u5168\u7ad9\u961f\u5217\u5171 ${afterActive} \u8282`
);
if (beforeActive > 0 && n > 0) {
pushLog(`\u5df2\u6709 ${beforeActive} \u8282\u5728\u8ba1\u65f6/\u6392\u961f\uff0c\u65b0\u8282\u63a5\u5728\u540e\u9762\uff0c\u52ff\u70b9\u6e05\u7a7a`);
}
if (isLadderMode()) {
pushLog("\u9636\u68af\u4e32\u884c\u8bd5\u9a8c\uff1a\u591a\u8bfe\u6309\u9636\u68af\u69fd\u6392\u961f");
} else if (isParallelStepMode()) {
pushLog("\u5e76\u884c+300s\uff1astatus=3 \u6b65\u8fdb+300s\uff1b\u672b\u6b65 status=2+\u64ad\u5b8c\u8bb0\u5f55");
} else if (isTailMode()) {
pushLog("\u672b\u6bb5\u6302\u949f\uff1a\u5feb\u586b→\u6302\u949f→\u672b5\u5206");
} else if (isSerialMode()) {
pushLog("\u4e32\u884c\u8f6e\u8be2\uff1a\u9010\u8bfe+300s\u9000\u51fa");
} else if (isRealtimeMode()) {
pushLog(`[1:1\u6302\u673a] \u4e32\u884c\uff1a\u8bfe\u957f=\u6302\u949f\uff08${afterActive} \u8282\u6392\u961f\uff09`);
} else if (isOfflineMode()) {
const totalSec = picked.reduce((s, ch) => s + (ch.duration || 3600), 0);
pushLog(
`[\u79bb\u7ebf\u6279\u91cf] \u5c06\u5206\u6bb5\u62c9\u6ee1\uff0c\u7d2f\u8ba1\u7ea6 ${formatDurationCn(totalSec)} + \u7f13\u51b230\u5206\u949f \u540e\u7edf\u4e00\u6536\u5c3e\u4e0e\u8003\u8bd5`
);
} else {
assignAnchorIfNeeded();
}
}
async function registerChaptersFromCourses(mode, cids) {
if (!getEnabled()) {
setEnabled(true);
pushLog("\u5df2\u81ea\u52a8\u5f00\u542f");
}
if (!getApiMode()) {
localStorage.setItem(API_MODE_KEY, "1");
pushLog("\u5df2\u81ea\u52a8\u5f00\u542f");
}
if (!cids || !cids.length) {
pushLog("\u8bf7\u5148\u52fe\u9009\u81f3\u5c11\u4e00\u95e8\u8bfe\u7a0b");
return 0;
}
const beforeActive = Object.values(loadJobs()).filter((j) => j && j.phase !== "done").length;
let totalN = 0;
for (const cid of cids) {
let course = findPanelCourseByCid(cid);
if (!course) {
course = blankPanelCourse({ cid, title: cid.slice(0, 8), url: coursePageUrl(cid) });
panelState.courses.push(course);
}
await enrichPanelCourse(course, true);
const chapters = course.chapters || [];
if (!chapters.length) {
pushLog(`\u300c${course.title}\u300d${course.progressError || "\u672a\u8bc6\u522b\u5230\u7ae0\u8282"}`);
continue;
}
let picked = chapters;
if (mode === "untouched") picked = chapters.filter(isChapterRegistrable);
else if (mode === "all") picked = chapters.filter(isChapterRegistrable);
if (!picked.length) {
const examLeft = chapters.filter(isChapterNeedExam).length;
if (examLeft) {
pushLog(`\u300c${course.title}\u300d${examLeft} \u8282\u5f85\u8003\u8bd5\u4f46\u672a\u80fd\u767b\u8bb0\uff08\u8bf7\u70b9\u5237\u65b0\u540e\u91cd\u8bd5\uff09`);
} else {
pushLog(`\u300c${course.title}\u300d\u6ca1\u6709\u53ef\u767b\u8bb0\u7ae0\u8282`);
}
continue;
}
const result = await registerPickedChapters(picked, course.title || cid.slice(0, 8));
totalN += result.n || 0;
finalizeChapterRegistration(result, beforeActive, course.title || cid.slice(0, 8));
await sleep(200);
}
restoreLockedQueueSelection();
renderCourseList();
await refreshChapterPreview();
if (totalN > 0) {
startGlobalTicker();
runBgScheduler();
}
updatePanel();
return totalN;
}
async function registerCourseChapters(mode) {
if (!getEnabled()) {
setEnabled(true);
pushLog("\u5df2\u81ea\u52a8\u5f00\u542f");
}
if (!getApiMode()) {
localStorage.setItem(API_MODE_KEY, "1");
pushLog("\u5df2\u81ea\u52a8\u5f00\u542f");
}
const chapters = parseCourseChapterJobs();
if (!chapters.length) {
const selected = getQueue();
if (selected.length) {
return registerChaptersFromCourses(mode, selected);
}
pushLog("\u8bf7\u5728\u7ae0\u8282\u5217\u8868\u9875 /pages/course.aspx\uff0c\u6216\u52fe\u9009\u6536\u85cf\u8bfe\u7a0b");
return;
}
let picked = chapters;
if (mode === "untouched") picked = chapters.filter(isChapterRegistrable);
else if (mode === "all") picked = chapters.filter(isChapterRegistrable);
if (!picked.length) {
const examLeft = chapters.filter(isChapterNeedExam).length;
if (examLeft) pushLog(`${examLeft} \u8282\u5f85\u8003\u8bd5\u4f46\u672a\u80fd\u767b\u8bb0\uff08\u8bf7 F5 \u5237\u65b0\u8bfe\u7a0b\u9875\uff09`);
else pushLog("\u6ca1\u6709\u53ef\u767b\u8bb0\u7684\u7ae0\u8282");
return;
}
const beforeActive = Object.values(loadJobs()).filter((j) => j && j.phase !== "done").length;
const courseTitle =
(document.querySelector(".course_title h1") || document.querySelector("h1"))?.textContent
?.replace(/\s+/g, " ")
.trim()
.slice(0, 20) || chapters[0].cid.slice(0, 8);
const result = await registerPickedChapters(picked, courseTitle);
finalizeChapterRegistration(result, beforeActive, courseTitle);
renderCourseList();
await refreshChapterPreview();
startGlobalTicker();
runBgScheduler();
}
function startGlobalTicker() {
if (!isLeaderTab()) return;
stopGlobalTicker();
window.__hyJobTicker = setInterval(() => {
if (getEnabled() && getApiMode() && isLeaderTab()) runBgScheduler();
}, TICK_MS);
runBgScheduler();
}
async function runPlayPage() {
if (!getEnabled() || !getApiMode()) return;
if (engineRunning || bgRunning) {
pushLog("\u5f15\u64ce\u8fd0\u884c\u4e2d");
return;
}
pushLog("\u64ad\u653e\u9875\uff1a\u521d\u59cb\u5316 API \u5f15\u64ce…");
window.blockAbnormalPlugin = function () {};
try {
await waitUntil(() => window.player, 15000);
try {
window.player.j2s_pauseVideo();
} catch (_) {}
} catch (_) {
pushLog("\u672a\u68c0\u6d4b\u5230\u64ad\u653e\u5668\uff0c\u4f7f\u7528\u9875\u9762\u65f6\u957f");
}
const ctx = buildPlayContext();
if (!ctx.cwrid || !ctx.uid) {
pushLog("\u7f3a\u5c11 cwrid/uid");
return;
}
const job = getJob(ctx);
if (job && job.phase === "done") {
pushLog("\u672c\u8bfe\u5df2\u5b8c\u6210");
return;
}
engineRunning = true;
try {
if (job && isParallelMode() && (job.phase === "serial" || job.phase === "stepping" || job.phase === "pending" || job.phase === "ready" || TAIL_STEP_PHASES.includes(job.phase) || (job.phase === "waiting" && isTailMode()))) {
pushLog("\u540e\u53f0\u6a21\u5f0f\u7531\u7ae0\u8282\u9875\u8c03\u5ea6\uff0c\u65e0\u9700\u5728\u64ad\u653e\u9875\u624b\u52a8\u89e6\u53d1");
return;
}
if (job && (job.phase === "waiting" || job.phase === "anchor" || job.phase === "active")) {
if (Date.now() < job.finishDue) {
pushLog(
`\u5df2\u5728\u961f\u5217\uff0c${formatEta(job.finishDue - Date.now())} \u540e\u81ea\u52a8\u6536\u5c3e\uff08\u540e\u53f0\u5355\u8bfe\u4fdd\u6d3b\u5fc3\u8df3\u4e2d\uff09`
);
return;
}
await runFinishPhase(ctx);
return;
}
if (job && job.isAnchor) {
pushLog("\u672c\u8bfe\u4e3a\u4fdd\u6d3b\u951a\u70b9\uff0c\u7531\u540e\u53f0\u8c03\u5ea6");
return;
}
await runFillPhase(ctx);
} finally {
engineRunning = false;
}
}
async function resolveTestContext() {
if (pageType() === "play") {
const ctx = buildPlayContext();
if (ctx.cwrid && ctx.uid) return ctx;
}
const jobs = Object.values(loadJobs())
.filter((j) => j && j.phase !== "done")
.sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0));
if (jobs.length) return jobToCtx(jobs[0]);
const chapters = parseCourseChapterJobs();
const ch = chapters.find((c) => !c.done) || chapters[0];
if (!ch) return null;
const meta = await ensureCourseApiMeta(ch.cid, ch.cwrid);
return {
uid: ch.uid,
cwrid: ch.cwrid,
coaid: ch.coaid,
cid: ch.cid,
cwid: ch.cwid,
groupId: meta.groupId,
provinceId: meta.provinceId,
duration: ch.duration,
baseSec: ch.startSec,
lsKey: ch.lsKey,
title: ch.title,
};
}
async function runStatus2ThenPlayRecordTest() {
const ctx = await resolveTestContext();
if (!ctx || !ctx.cwrid || !ctx.uid) {
pushLog("\u6d4b\u8bd5\u5931\u8d25\uff1a\u8bf7\u5728\u64ad\u653e\u9875\uff0c\u6216\u7ae0\u8282\u9875\u767b\u8bb0/\u6709\u672a\u5b66\u7ae0\u8282");
return;
}
const label = ctx.title || ctx.cwrid.slice(0, 8);
const duration = ctx.duration;
pushLog(`[\u6d4b\u8bd5] ${label} status=2 @${duration}s → \u7acb\u5373\u64ad\u5b8c\u8bb0\u5f55`);
await warmPlaySession(ctx, true);
let s2 = null;
try {
s2 = await postProcess(ctx, 2, duration);
pushLog(
`[\u6d4b\u8bd5] status=2 code=${s2 && s2.code}${s2 && s2.msg ? " " + s2.msg : ""}`
);
} catch (e) {
pushLog(`[\u6d4b\u8bd5] status=2 \u5f02\u5e38: ${e && e.message ? e.message : e}`);
}
await sleep(800);
let rec = null;
try {
rec = await postPlayRecord(ctx, duration);
pushLog(
`[\u6d4b\u8bd5] \u64ad\u5b8c\u8bb0\u5f55 code=${rec && rec.code}${rec && rec.msg ? " " + rec.msg : ""}`
);
} catch (e) {
pushLog(`[\u6d4b\u8bd5] \u64ad\u5b8c\u8bb0\u5f55\u5f02\u5e38: ${e && e.message ? e.message : e}`);
}
if (rec && rec.code === 0) {
syncLocal(ctx, duration);
try {
await postProcess(ctx, 3, duration);
pushLog(`[\u6d4b\u8bd5] status=3 \u5df2\u9000\u51fa`);
} catch (_) {}
pushLog(`[\u6d4b\u8bd5] ✓ \u7ec4\u5408\u6d4b\u8bd5\u901a\u8fc7\uff08status=2 + \u64ad\u5b8c\u8bb0\u5f55 ok\uff09`);
} else if (rec && rec.code === 1) {
pushLog(`[\u6d4b\u8bd5] ✗ \u64ad\u5b8c\u8bb0\u5f55\u65f6\u957f\u4e0d\u8db3 → \u9700\u6302\u949f\u540e\u518d 1 \u6b21 status=1 + \u64ad\u5b8c\u8bb0\u5f55\uff08\u9694\u65e5\u4e5f\u884c\uff09`);
}
}
async function runStatus2OnlyTest() {
const ctx = await resolveTestContext();
if (!ctx || !ctx.cwrid || !ctx.uid) {
pushLog("\u6d4b\u8bd5\u5931\u8d25\uff1a\u8bf7\u5728\u64ad\u653e\u9875\uff0c\u6216\u7ae0\u8282\u9875\u767b\u8bb0/\u6709\u672a\u5b66\u7ae0\u8282");
return;
}
const label = ctx.title || ctx.cwrid.slice(0, 8);
const duration = ctx.duration;
pushLog(`[\u6d4b\u8bd5] ${label} \u4ec5 POST status=2 @${duration}s\uff08\u4e0d\u53d1 0/1/3\uff09`);
await warmPlaySession(ctx, true);
try {
const res = await postProcess(ctx, 2, duration);
pushLog(
`[\u6d4b\u8bd5] status=2 code=${res && res.code}${res && res.msg ? " " + res.msg : ""}`
);
} catch (e) {
pushLog(`[\u6d4b\u8bd5] status=2 \u5f02\u5e38: ${e && e.message ? e.message : e}`);
}
pushLog("[\u6d4b\u8bd5] \u7ae0\u8282\u300c\u5b8c\u6210\u300d\u9760\u64ad\u5b8c\u8bb0\u5f55\uff0c\u4e0d\u662f status=2\uff1b\u53ef\u70b9\u300c\u6d4b\u64ad\u5b8c\u8bb0\u5f55\u300d");
}
async function runTailDoubleHeartbeatTest() {
if (singleCourseTestRunning) {
pushLog("[\u6d4b\u8bd5] \u672b5\u5206\u53cc\u5fc3\u8df3\u8fdb\u884c\u4e2d\uff0c\u8bf7\u52ff\u91cd\u590d\u70b9\u51fb");
return;
}
const ctx = await resolveTestContext();
if (!ctx || !ctx.cwrid || !ctx.uid) {
pushLog("\u6d4b\u8bd5\u5931\u8d25\uff1a\u8bf7\u5728\u64ad\u653e\u9875\uff0c\u6216\u7ae0\u8282\u9875\u767b\u8bb0/\u6709\u672a\u5b66\u7ae0\u8282\uff08\u53ea\u6d4b\u961f\u5217\u7b2c\u4e00\u8bfe\uff09");
return;
}
const label = ctx.title || ctx.cwrid.slice(0, 8);
const duration = Math.max(60, ctx.duration || 3600);
const tailSec = Math.max(1, duration - STEP_SEC);
singleCourseTestRunning = true;
pushLog(
`[\u6d4b\u8bd5·\u5355\u8bfe] ${label} ① status=1@${tailSec}s(\u603b-${STEP_SEC}s) → \u7b495\u520630\u79d2 → ② status=1@${duration}s → \u64ad\u5b8c\u8bb0\u5f55`
);
try {
if (!(await warmPlaySession(ctx, true))) {
pushLog("[\u6d4b\u8bd5] \u9884\u70ed\u64ad\u653e\u9875\u5931\u8d25\uff0c\u8bf7\u5148 F5 \u767b\u5f55");
return;
}
await sleep(1500);
let r1 = null;
try {
r1 = await postProcess(ctx, 1, tailSec);
} catch (e) {
pushLog(`[\u6d4b\u8bd5] ① status=1@${tailSec}s \u5f02\u5e38: ${e && e.message ? e.message : e}`);
return;
}
pushLog(
`[\u6d4b\u8bd5] ① status=1@${tailSec}s code=${r1 && r1.code != null ? r1.code : "?"}${r1 && r1.msg ? " " + r1.msg : ""}`
);
if (!r1 || r1.code === 3) {
pushLog("[\u6d4b\u8bd5] ① code=3 \u9650\u6d41\uff0c\u7ea65\u5206\u949f\u540e\u518d\u8bd5");
return;
}
if (r1.code !== 0) return;
syncLocal(ctx, tailSec);
pushLog("[\u6d4b\u8bd5] \u6302\u949f 5\u520630\u79d2…\uff08\u7ae0\u8282\u9875\u4fdd\u6301\u5f00\u7740\uff09");
const waitUntil = Date.now() + TAIL_HEARTBEAT_WAIT_MS;
while (Date.now() < waitUntil) {
await sleep(60000);
const left = waitUntil - Date.now();
if (left > 0) pushLog(`[\u6d4b\u8bd5] \u8fd8\u5269 ${Math.ceil(left / 60000)} \u5206\u949f…`);
}
if (!(await warmPlaySession(ctx, true))) {
pushLog("[\u6d4b\u8bd5] ② \u524d\u9884\u70ed\u5931\u8d25\uff0c\u8bf7 F5 \u540e\u624b\u52a8\u70b9\u300c\u6d4b\u64ad\u5b8c\u8bb0\u5f55\u300d");
return;
}
await sleep(800);
let r2 = null;
try {
r2 = await postProcess(ctx, 1, duration);
} catch (e) {
pushLog(`[\u6d4b\u8bd5] ② status=1@${duration}s \u5f02\u5e38: ${e && e.message ? e.message : e}`);
return;
}
pushLog(
`[\u6d4b\u8bd5] ② status=1@${duration}s code=${r2 && r2.code != null ? r2.code : "?"}${r2 && r2.msg ? " " + r2.msg : ""}`
);
if (!r2 || r2.code !== 0) return;
syncLocal(ctx, duration);
await sleep(800);
let rec = null;
try {
rec = await postPlayRecord(ctx, duration);
} catch (e) {
pushLog(`[\u6d4b\u8bd5] \u64ad\u5b8c\u8bb0\u5f55\u5f02\u5e38: ${e && e.message ? e.message : e}`);
return;
}
pushLog(
`[\u6d4b\u8bd5] ★\u64ad\u5b8c\u8bb0\u5f55 @${duration}s code=${rec && rec.code != null ? rec.code : "?"}${rec && rec.msg ? " " + rec.msg : ""}`
);
if (rec && rec.code === 0) {
pushLog("[\u6d4b\u8bd5] ✓ \u672b5\u5206\u53cc\u5fc3\u8df3 + \u64ad\u5b8c\u8bb0\u5f55\u6210\u529f");
} else if (rec && rec.code === 1) {
pushLog("[\u6d4b\u8bd5] ✗ \u4ecd\u65f6\u957f\u4e0d\u8db3 → \u524d\u9762\u6709\u6548\u5fc3\u8df3\u6b21\u6570/\u6302\u949f\u53ef\u80fd\u8fd8\u4e0d\u591f");
}
} finally {
singleCourseTestRunning = false;
}
}
async function sleepWithMinuteLogs(totalMs, tag) {
const waitUntil = Date.now() + totalMs;
pushLog(
`[\u6d4b\u8bd5] ${tag} \u6302\u949f ${Math.round(totalMs / 60000)}\u5206${Math.round((totalMs % 60000) / 1000)}\u79d2…`
);
while (Date.now() < waitUntil) {
await sleep(60000);
const left = waitUntil - Date.now();
if (left > 0) pushLog(`[\u6d4b\u8bd5] ${tag} \u8fd8\u5269 ${Math.ceil(left / 60000)} \u5206\u949f…`);
}
}
async function postProcessRetryOnCode3(ctx, status, sec, label, stepTag) {
for (let attempt = 1; attempt <= CODE3_RETRY_MAX; attempt++) {
let res = null;
try {
res = await postProcess(ctx, status, sec);
} catch (e) {
pushLog(
`[\u6d4b\u8bd5] ${stepTag} status=${status}@${sec}s \u5f02\u5e38: ${e && e.message ? e.message : e}\uff0c1\u5206\u949f\u540e\u91cd\u8bd5`
);
await sleep(CODE3_RETRY_MS);
continue;
}
const code = res && res.code;
const msg = res && res.msg ? " " + res.msg : "";
if (code === 3) {
pushLog(
`[\u6d4b\u8bd5] ${stepTag} status=${status}@${sec}s code=3${msg}\uff0c1\u5206\u949f\u540e\u91cd\u8bd5\uff08${attempt}/${CODE3_RETRY_MAX}\uff09`
);
await sleep(CODE3_RETRY_MS);
continue;
}
pushLog(
`[\u6d4b\u8bd5] ${stepTag} status=${status}@${sec}s code=${code != null ? code : "?"}${msg}`
);
if (code !== 0) return { ok: false, code, res };
return { ok: true, code: 0, res };
}
pushLog(`[\u6d4b\u8bd5] ${stepTag} code=3 \u91cd\u8bd5\u5df2\u8fbe\u4e0a\u9650\uff0c\u8bf7\u7a0d\u540e\u518d\u8bd5`);
return { ok: false, code: 3 };
}
function buildTenMinLadderTargets(duration) {
const targets = [];
for (let sec = BIGSTEP_JUMP_SEC; sec < duration; sec += BIGSTEP_JUMP_SEC) {
targets.push(sec);
}
return targets;
}
function formatStepTag(index) {
const circled = "①②③④⑤⑥⑦⑧⑨⑩";
if (index >= 1 && index <= circled.length) return circled[index - 1];
return `#${index}`;
}
async function runJump10Min20MinTest() {
if (singleCourseTestRunning) {
pushLog("[\u6d4b\u8bd5] \u5355\u8bfe\u6d4b\u8bd5\u8fdb\u884c\u4e2d\uff0c\u8bf7\u52ff\u91cd\u590d\u70b9\u51fb");
return;
}
const ctx = await resolveTestContext();
if (!ctx || !ctx.cwrid || !ctx.uid) {
pushLog("\u6d4b\u8bd5\u5931\u8d25\uff1a\u8bf7\u5728\u64ad\u653e\u9875\uff0c\u6216\u7ae0\u8282\u9875\u767b\u8bb0/\u6709\u672a\u5b66\u7ae0\u8282\uff08\u53ea\u6d4b\u7b2c\u4e00\u8bfe\uff09");
return;
}
const label = ctx.title || ctx.cwrid.slice(0, 8);
const duration = Math.max(60, ctx.duration || 3600);
const targets = buildTenMinLadderTargets(duration);
if (!targets.length) {
pushLog(`[\u6d4b\u8bd5] \u89c6\u9891\u4ec5${duration}s\uff0c\u4e0d\u8db310\u5206\u949f\uff0c\u8bf7\u6362\u66f4\u957f\u8bfe\u65f6\u518d\u8bd5`);
return;
}
const total = targets.length;
const wallMin = Math.max(0, Math.round(((total - 1) * BIGSTEP_GAP_MS) / 60000));
const wallSec = Math.max(0, Math.round(((total - 1) * BIGSTEP_GAP_MS) / 1000));
const lastMin = Math.round(targets[targets.length - 1] / 60);
singleCourseTestRunning = true;
pushLog(
`[\u6d4b\u8bd5·2\u500d\u901f\u9636\u68af] ${label} ${duration}s\uff1a10\u5206→20\u5206→30\u5206…\u6b65\u95f41\u52061\u79d2\uff0c\u5171${total}\u6b21\u81f3${lastMin}\u5206 → status=2+\u64ad\u5b8c`
);
pushLog(
`[\u6d4b\u8bd5] \u9884\u8ba1\u6302\u949f\u7ea6 ${wallMin > 0 ? wallMin + "\u5206" : wallSec + "\u79d2"}\uff08\u4e0d\u542b code=3\uff09\uff1b\u5931\u8d25 code=3 \u7b491\u5206\u949f\u91cd\u8bd5`
);
try {
if (!(await warmPlaySession(ctx, true))) {
pushLog("[\u6d4b\u8bd5] \u9884\u70ed\u5931\u8d25\uff0c\u8bf7\u5148 F5 \u767b\u5f55");
return;
}
await sleep(1500);
for (let i = 0; i < total; i++) {
const sec = targets[i];
const min = Math.round(sec / 60);
const tag = `${formatStepTag(i + 1)}/${total} ${min}\u5206@${sec}s`;
if (i > 0) {
const prevMin = Math.round(targets[i - 1] / 60);
await sleepWithMinuteLogs(BIGSTEP_GAP_MS, `${prevMin}\u5206→${min}\u5206`);
if (!(await warmPlaySession(ctx, true))) {
pushLog(`[\u6d4b\u8bd5] ${tag} \u524d\u9884\u70ed\u5931\u8d25`);
return;
}
await sleep(800);
}
const r = await postProcessRetryOnCode3(ctx, 1, sec, label, tag);
if (!r.ok) return;
syncLocal(ctx, sec);
}
await sleep(800);
const r2 = await postProcessRetryOnCode3(ctx, 2, duration, label, "status=2");
if (r2.ok) syncLocal(ctx, duration);
await sleep(800);
let rec = null;
try {
rec = await postPlayRecord(ctx, duration);
} catch (e) {
pushLog(`[\u6d4b\u8bd5] \u64ad\u5b8c\u8bb0\u5f55\u5f02\u5e38: ${e && e.message ? e.message : e}`);
return;
}
pushLog(
`[\u6d4b\u8bd5] ★\u64ad\u5b8c\u8bb0\u5f55 @${duration}s code=${rec && rec.code != null ? rec.code : "?"}${rec && rec.msg ? " " + rec.msg : ""}`
);
if (rec && rec.code === 0) {
pushLog("[\u6d4b\u8bd5] ✓ \u64ad\u5b8c\u8bb0\u5f55\u6210\u529f\uff0c\u8bf7\u5728\u7ae0\u8282\u9875\u786e\u8ba4\u662f\u5426\u663e\u793a\u5df2\u5b8c\u6210");
} else if (rec && rec.code === 1) {
pushLog("[\u6d4b\u8bd5] \u4ecd\u65f6\u957f\u4e0d\u8db3 → \u53ef\u80fd\u8fd8\u9700\u66f4\u591a\u9636\u68af\u6b65\u6216\u6302\u949f\u540e\u518d\u8865 1 \u6b21\u5fc3\u8df3+\u64ad\u5b8c");
}
} finally {
singleCourseTestRunning = false;
}
}
async function runPlayRecordOnlyTest() {
const ctx = await resolveTestContext();
if (!ctx || !ctx.cwrid || !ctx.uid) {
pushLog("\u6d4b\u8bd5\u5931\u8d25\uff1a\u8bf7\u5728\u64ad\u653e\u9875\uff0c\u6216\u7ae0\u8282\u9875\u767b\u8bb0/\u6709\u672a\u5b66\u7ae0\u8282");
return;
}
const label = ctx.title || ctx.cwrid.slice(0, 8);
const duration = ctx.duration;
pushLog(`[\u6d4b\u8bd5] ${label} \u4ec5 GET addCourseWarePlayRecord @${duration}s`);
await warmPlaySession(ctx, true);
try {
const rec = await postPlayRecord(ctx, duration);
pushLog(
`[\u6d4b\u8bd5] \u64ad\u5b8c\u8bb0\u5f55 code=${rec && rec.code}${rec && rec.msg ? " " + rec.msg : ""}`
);
if (rec && rec.code === 0) syncLocal(ctx, duration);
} catch (e) {
pushLog(`[\u6d4b\u8bd5] \u64ad\u5b8c\u8bb0\u5f55\u5f02\u5e38: ${e && e.message ? e.message : e}`);
}
}
function tryEnterExam() {
const btn = document.getElementById("jrks");
if (!btn || btn.getAttribute("disabled") === "disabled") return;
pushLog("\u81ea\u52a8\u8fdb\u5165\u8003\u8bd5");
btn.click();
}
function resolveExamCwid(ctx, job) {
const j = job || (ctx ? getJob(ctx) : null);
return (
(ctx && ctx.cwrid) ||
(j && j.cwrid) ||
(j && j.cwid) ||
(ctx && ctx.cwid) ||
new URLSearchParams(location.search).get("cwid") ||
""
);
}
function diagnoseExamPageHtml(html) {
const text = String(html || "");
if (isHyExamFacePage(text)) return "\u9700\u8981\u4eba\u8138\u8bc6\u522b\uff08\u638c\u4e0a\u534e\u533b APP \u626b\u7801\uff09";
if (isPageLoginRedirect(text) || (/checkLogin|login_sso/i.test(text) && text.length < 4000)) {
return "\u767b\u5f55\u5931\u6548\uff0c\u8bf7 F5 \u5237\u65b0";
}
if (/\u5b66\u4e60\u65f6\u957f|\u8bf7\u5148\u5b66\u4e60|\u4e0d\u80fd\u7b54\u5377|\u672a\u5b66\u5b8c|\u65f6\u957f\u4e0d\u8db3/.test(text)) return "\u5b66\u65f6\u672a\u6ee1\uff0c\u4e0d\u80fd\u7b54\u5377";
if (/\u53c2\u6570\u9519\u8bef|\u4e0d\u5b58\u5728|\u65e0\u6548\u8bfe\u4ef6|\u627e\u4e0d\u5230\u8bfe\u4ef6|\u672a\u627e\u5230/.test(text)) return "\u8003\u8bd5 ID \u65e0\u6548";
if (/class=["']test["']|\.test\s*>\s*table/i.test(text) && !/q_name|tablestyle/.test(text)) {
return "\u8bd5\u5377\u7ed3\u6784\u5df2\u8bc6\u522b\u4f46\u9009\u9879\u672a\u52a0\u8f7d\uff08\u8bf7\u624b\u52a8\u5237\u65b0\u8003\u8bd5\u9875\uff09";
}
return "";
}
function parseHyExamQuestionsV1(doc) {
const questions = [];
const seen = new Set();
function pushQuestion(qEl, scope) {
const qid = qEl.getAttribute("data-qid") || "";
if (!qid || seen.has(qid)) return;
const root = scope || qEl.closest("table, .tablestyle, tr, li, .exam_item, form") || qEl.parentElement;
const options = [];
const radioSel = `input[type='radio'][name='radio_${qid}'], input.qo_name[type='radio']`;
(root || doc).querySelectorAll(radioSel).forEach((input) => {
const id = input.value || "";
if (!id) return;
options.push({ id, name: input.getAttribute("name") || `radio_${qid}`, label: (input.closest("label")?.textContent || "").replace(/\s+/g, " ").trim() });
});
if (!options.length) {
doc.querySelectorAll(radioSel).forEach((input) => {
const id = input.value || "";
if (!id || options.some((o) => o.id === id)) return;
options.push({ id, name: input.getAttribute("name") || `radio_${qid}`, label: (input.closest("label")?.textContent || "").replace(/\s+/g, " ").trim() });
});
}
if (qid && options.length) {
seen.add(qid);
questions.push({
qid,
format: "v1",
radioName: `radio_${qid}`,
title: (qEl.textContent || "").replace(/\s+/g, " ").trim(),
options,
});
}
}
doc.querySelectorAll(".tablestyle").forEach((table) => {
const qEl = table.querySelector(".q_name");
if (qEl) pushQuestion(qEl, table);
});
if (!questions.length) {
doc.querySelectorAll(".q_name[data-qid], label.q_name[data-qid]").forEach((qEl) => pushQuestion(qEl));
}
return questions;
}
function parseHyExamQuestionsV2(doc) {
const questions = [];
const seen = new Set();
doc.querySelectorAll(".test > table, .test table, div.test table").forEach((table, idx) => {
const titleEl = table.querySelector("thead th, thead td, thead");
let title = titleEl ? (titleEl.textContent || "").replace(/\s+/g, " ").trim() : "";
title = title.replace(/^\d+[\u3001.\s]+/, "").trim();
const options = [];
table.querySelectorAll("tbody input[type='radio']").forEach((input) => {
const name = input.getAttribute("name") || "";
const id = input.value || "";
if (!name || !id) return;
options.push({
id,
name,
label: (input.closest("label")?.textContent || input.parentElement?.textContent || "").replace(/\s+/g, " ").trim(),
});
});
if (!options.length) return;
const radioName = options[0].name;
if (seen.has(radioName)) return;
seen.add(radioName);
questions.push({
qid: radioName,
format: "v2",
radioName,
title: title || `\u7b2c${idx + 1}\u9898`,
options,
});
});
return questions;
}
function parseHyExamQuestions(html) {
const doc = new DOMParser().parseFromString(html, "text/html");
const v1 = parseHyExamQuestionsV1(doc);
if (v1.length) return v1;
const v2 = parseHyExamQuestionsV2(doc);
if (v2.length) return v2;
return [];
}
function isHyExamPassHtml(html, url) {
const text = String(html || "");
const u = String(url || "");
if (/exam_result/i.test(u) && /\u8003\u8bd5\u672a\u901a\u8fc7|tips_fail\.png|\u91cd\u65b0\u8003\u8bd5/i.test(text)) return false;
if (/exam_result/i.test(u) && /\u672c\u8bfe\u7a0b\u5b8c\u6210|\u8bfe\u7a0b\u5df2\u5b8c\u6210/.test(text)) return true;
if (/\u8003\u8bd5\u901a\u8fc7|\u606d\u559c.*\u901a\u8fc7|\u5408\u683c/.test(text) && !/\u8003\u8bd5\u672a\u901a\u8fc7/.test(text)) return true;
if (/tips_success\.png/i.test(text) && /exam_result/i.test(u)) return true;
if (/exam_result\.aspx/i.test(u) && !/\u8003\u8bd5\u672a\u901a\u8fc7|\u4e0d\u53ca\u683c|\u672a\u80fd\u901a\u8fc7|\u91cd\u65b0\u7b54\u9898|\u672a\u901a\u8fc7|tips_fail/.test(text)) {
return /\u8003\u8bd5\u901a\u8fc7|tips_success/.test(text);
}
return false;
}
function isHyExamAlreadyPassedHtml(html) {
return /\u5df2\u5b8c\u6210|\u5df2\u901a\u8fc7|\u4e0d\u8981\u91cd\u590d|\u5df2\u7ecf\u901a\u8fc7|\u8003\u8bd5\u901a\u8fc7/.test(String(html || ""));
}
function findHyOptionByLetter(q, letter) {
if (!q || !letter) return null;
const L = String(letter).toUpperCase().slice(0, 1);
const byLabel = (q.options || []).find((o) => new RegExp(`^${L}[\u3001.\\s]`).test(o.label || ""));
if (byLabel) return byLabel;
const idx = L.charCodeAt(0) - 65;
if (idx >= 0 && idx < (q.options || []).length) return q.options[idx];
return null;
}
function hyExamOptionLetter(q, opt) {
if (!opt) return "";
const m = String(opt.label || "").match(/^([A-Ha-h])[\u3001.\s]/);
if (m) return m[1].toUpperCase();
const idx = (q.options || []).indexOf(opt);
return idx >= 0 ? String.fromCharCode(65 + idx) : "";
}
function ensureHyExamTriedLetters(state, qid) {
if (!state.triedLetters) state.triedLetters = {};
if (!state.triedLetters[qid]) state.triedLetters[qid] = new Set();
return state.triedLetters[qid];
}
function syncHyExamTriedLetters(state, q) {
const triedLet = ensureHyExamTriedLetters(state, q.qid);
const triedIds = ensureHyExamTried(state, q.qid);
q.options.forEach((o) => {
if (triedIds.has(o.id)) {
const L = hyExamOptionLetter(q, o);
if (L) triedLet.add(L);
}
});
return triedLet;
}
function formatHyExamPicksLog(questions, picks) {
return picks
.map((p) => {
const q = questions.find((x) => x.qid === p.qid);
const o = q && q.options.find((x) => x.id === p.optionId);
return q && o ? hyExamOptionLetter(q, o) || "?" : "?";
})
.join("");
}
function markHyExamCodePassed(cwid) {
if (!cwid) return;
try {
sessionStorage.setItem(
HY_EXAM_CODE_PASS_KEY,
JSON.stringify({ cwid: String(cwid).toUpperCase(), ts: Date.now() })
);
} catch (_) {}
}
function isHyExamCodeRecentlyPassed(cwid) {
if (!cwid) return false;
try {
const raw = JSON.parse(sessionStorage.getItem(HY_EXAM_CODE_PASS_KEY) || "null");
const target = String(cwid).toUpperCase();
return !!(raw && raw.cwid === target && Date.now() - (raw.ts || 0) < 15 * 60 * 1000);
} catch (_) {
return false;
}
}
function parseHyExamResultPage(html, url) {
const text = String(html || "");
const u = String(url || "");
if (!/exam_result/i.test(u) && !/state_cour_ul|state_cour_lis/.test(text)) return null;
const failed = /\u8003\u8bd5\u672a\u901a\u8fc7|tips_fail\.png/i.test(text);
const passed = !failed && isHyExamPassHtml(text, u);
const scoreM = text.match(/test_score:\s*["'](\d+)/i);
const score = scoreM ? scoreM[1] : "";
const doc = new DOMParser().parseFromString(text, "text/html");
const wrong = [];
const correct = [];
doc.querySelectorAll("ul.state_cour_ul > li.state_cour_lis, .state_cour_ul .state_cour_lis").forEach((li, idx) => {
const inner = li.innerHTML || "";
const isWrong = /error_icon\.png/i.test(inner);
const isRight = !isWrong && /bar_img\.png/i.test(inner);
const texts = li.querySelectorAll(".state_lis_text");
const titleRaw = (texts[0]?.getAttribute("title") || texts[0]?.textContent || "")
.replace(/\s+/g, " ")
.trim();
const ansRaw = (texts[1]?.getAttribute("title") || texts[1]?.textContent || "")
.replace(/\s+/g, " ")
.trim();
const numM = titleRaw.match(/^(\d+)[\u3001.]/);
const qIndex = numM ? parseInt(numM[1], 10) - 1 : idx;
const letterM = ansRaw.match(/\u3010\u60a8\u7684\u7b54\u6848\uff1a\s*([A-Ha-h])\u3001?/) || ansRaw.match(/([A-Ha-h])[\u3001.]/);
const optionLetter = letterM ? letterM[1].toUpperCase() : "";
const title = titleRaw.replace(/^\d+[\u3001.]\s*/, "").trim();
const entry = { qIndex, title, optionLetter, userAnswer: ansRaw };
if (isWrong) wrong.push(entry);
else if (isRight && optionLetter) correct.push(entry);
});
if (!wrong.length && !correct.length && !passed && !failed) return null;
return { passed, failed, score, wrong, correct };
}
function matchHyQuestionForResult(questions, item) {
if (!questions || !questions.length || !item) return null;
if (item.qIndex >= 0 && item.qIndex < questions.length) {
const q = questions[item.qIndex];
if (!item.title || !q.title) return q;
const a = item.title.slice(0, 18);
const b = q.title.replace(/^\d+[\u3001.]\s*/, "").slice(0, 18);
if (!a || !b || a === b || a.includes(b) || b.includes(a)) return q;
}
if (item.title) {
const t = item.title.slice(0, 18);
return (
questions.find((q) => {
const qt = q.title.replace(/^\d+[\u3001.]\s*/, "").slice(0, 18);
return qt === t || qt.includes(t) || t.includes(qt);
}) || (item.qIndex >= 0 && item.qIndex < questions.length ? questions[item.qIndex] : null)
);
}
return item.qIndex >= 0 && item.qIndex < questions.length ? questions[item.qIndex] : null;
}
function loadHyExamLetterCache(examId) {
if (!examId) return { lock: {}, tried: {} };
try {
const all = JSON.parse(localStorage.getItem(HY_EXAM_ANSWER_CACHE_KEY) || "{}");
const hit = all[examId] || {};
return { lock: hit.lock || {}, tried: hit.tried || {} };
} catch (_) {
return { lock: {}, tried: {} };
}
}
function hyExamCacheQid(item, questions) {
const q = matchHyQuestionForResult(questions, item);
return q ? q.qid : String(item.qIndex);
}
function isHyExamOptionId(val) {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(String(val || ""));
}
function resolveHyExamCachedOptionId(q, val) {
if (!val || !q) return "";
if (isHyExamOptionId(val)) return val;
const opt = findHyOptionByLetter(q, val);
return opt ? opt.id : "";
}
function saveHyExamLetterCache(examId, feedback, questions) {
if (!examId || !feedback) return;
try {
const all = JSON.parse(localStorage.getItem(HY_EXAM_ANSWER_CACHE_KEY) || "{}");
const cache = all[examId] || { lock: {}, tried: {} };
(feedback.correct || []).forEach((item) => {
const q = questions ? matchHyQuestionForResult(questions, item) : null;
const k = q ? q.qid : String(item.qIndex);
const opt = q && item.optionLetter ? findHyOptionByLetter(q, item.optionLetter) : null;
if (opt) cache.lock[k] = opt.id;
});
(feedback.wrong || []).forEach((item) => {
const q = questions ? matchHyQuestionForResult(questions, item) : null;
const k = q ? q.qid : String(item.qIndex);
delete cache.lock[k];
if (!cache.tried[k]) cache.tried[k] = [];
const opt = q && item.optionLetter ? findHyOptionByLetter(q, item.optionLetter) : null;
const oid = opt ? opt.id : item.optionLetter ? String(item.optionLetter).toUpperCase() : "";
if (oid && !cache.tried[k].includes(oid)) cache.tried[k].push(oid);
});
all[examId] = cache;
localStorage.setItem(HY_EXAM_ANSWER_CACHE_KEY, JSON.stringify(all));
} catch (_) {}
}
function applyHyExamLetterCache(state, questions, examId) {
const cache = loadHyExamLetterCache(examId);
let n = 0;
questions.forEach((q, i) => {
const lockedRaw = cache.lock[q.qid] || cache.lock[String(i)];
const lockedId = resolveHyExamCachedOptionId(q, lockedRaw);
if (lockedId) {
state.byQid[q.qid] = lockedId;
n++;
}
const triedRaw = cache.tried[q.qid] || cache.tried[String(i)] || [];
const triedLet = ensureHyExamTriedLetters(state, q.qid);
const triedIds = ensureHyExamTried(state, q.qid);
triedRaw.forEach((raw) => {
if (isHyExamOptionId(raw)) {
triedIds.add(raw);
const o = q.options.find((x) => x.id === raw);
const L = o && hyExamOptionLetter(q, o);
if (L) triedLet.add(L);
} else {
const opt = findHyOptionByLetter(q, raw);
if (opt) {
triedIds.add(opt.id);
triedLet.add(String(raw).toUpperCase());
}
}
});
});
return n;
}
function applyHyExamResultFeedback(state, questions, feedback) {
let locked = 0;
let adjusted = 0;
(feedback.correct || []).forEach((item) => {
const q = matchHyQuestionForResult(questions, item);
if (!q || !item.optionLetter) return;
const opt = findHyOptionByLetter(q, item.optionLetter);
if (!opt) return;
state.byQid[q.qid] = opt.id;
ensureHyExamTriedLetters(state, q.qid).add(item.optionLetter.toUpperCase());
locked++;
});
(feedback.wrong || []).forEach((item) => {
const q = matchHyQuestionForResult(questions, item);
if (!q) return;
if (item.optionLetter) {
const L = item.optionLetter.toUpperCase();
ensureHyExamTriedLetters(state, q.qid).add(L);
const opt = findHyOptionByLetter(q, L);
if (opt) ensureHyExamTried(state, q.qid).add(opt.id);
}
delete state.byQid[q.qid];
adjusted++;
});
return { locked, adjusted };
}
function parseHyExamWrongQids(html, questions) {
const resultFb = parseHyExamResultPage(html, html.includes("exam_result") ? "exam_result.aspx" : "");
if (resultFb && resultFb.wrong.length) {
return resultFb.wrong
.map((item) => {
const q = matchHyQuestionForResult(questions, item);
return q ? q.qid : "";
})
.filter(Boolean);
}
const out = new Set();
const text = String(html || "");
const doc = new DOMParser().parseFromString(text, "text/html");
doc.querySelectorAll(".q_name[data-qid], label.q_name[data-qid]").forEach((el) => {
const row = el.closest("table, .tablestyle, tr, li");
const ctx = (row && row.textContent) || el.textContent || "";
if (/\u9519\u8bef|\u4e0d\u6b63\u786e|\u7b54\u9519|×|✗|\u672a\u901a\u8fc7/.test(ctx)) {
const qid = el.getAttribute("data-qid");
if (qid) out.add(qid);
}
});
doc.querySelectorAll(".test > table, .test table").forEach((table) => {
const rowText = (table.textContent || "").replace(/\s+/g, " ");
if (!/\u9519\u8bef|\u4e0d\u6b63\u786e|\u7b54\u9519|×|✗|\u672a\u901a\u8fc7|\u4f60\u7684\u7b54\u6848/.test(rowText)) return;
const radio = table.querySelector("tbody input[type='radio']");
const name = radio && radio.getAttribute("name");
if (name) out.add(name);
});
const re = /data-qid=["']([0-9a-f-]{36})["'][^>]*>[\s\S]{0,240}?(?:\u9519\u8bef|\u4e0d\u6b63\u786e|\u7b54\u9519)/gi;
let m;
while ((m = re.exec(text))) out.add(m[1]);
if (!out.size && questions && questions.length) {
questions.forEach((q, i) => {
const block = text.split(q.title || "").slice(1, 2)[0] || "";
if (/\u9519\u8bef|\u4e0d\u6b63\u786e|\u7b54\u9519|×|✗/.test(block)) out.add(q.qid || `q${i}`);
});
}
return Array.from(out);
}
function createHyExamAnswerState() {
return { byQid: {}, tried: {}, triedLetters: {}, rotateIdx: 0, rotateSeq: 0 };
}
function ensureHyExamTried(state, qid) {
if (!state.tried[qid]) state.tried[qid] = new Set();
return state.tried[qid];
}
function fillHyExamPicks(questions, state) {
if (state.rotateSeq == null) state.rotateSeq = 0;
return questions.map((q) => {
if (state.byQid[q.qid]) {
return { qid: q.qid, optionId: state.byQid[q.qid] };
}
const triedIds = ensureHyExamTried(state, q.qid);
syncHyExamTriedLetters(state, q);
let chosen = q.options.find((o) => !triedIds.has(o.id));
if (!chosen) {
const idx = state.rotateSeq % Math.max(1, q.options.length);
state.rotateSeq++;
chosen = q.options[idx] || q.options[0];
}
return { qid: q.qid, optionId: chosen.id };
});
}
function buildHyExamPostBody(hidden, picks, questions) {
const parts = [];
if (hidden.__VIEWSTATE != null) parts.push(`__VIEWSTATE=${encodeURIComponent(hidden.__VIEWSTATE || "")}`);
if (hidden.__VIEWSTATEGENERATOR != null) {
parts.push(`__VIEWSTATEGENERATOR=${encodeURIComponent(hidden.__VIEWSTATEGENERATOR || "")}`);
}
if (hidden.__EVENTVALIDATION) {
parts.push(`__EVENTVALIDATION=${encodeURIComponent(hidden.__EVENTVALIDATION)}`);
}
const skipHidden = new Set(["__VIEWSTATE", "__VIEWSTATEGENERATOR", "__EVENTVALIDATION", "hd_result"]);
Object.keys(hidden || {}).forEach((k) => {
if (skipHidden.has(k) || hidden[k] == null || hidden[k] === "") return;
parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(hidden[k])}`);
});
const pickMap = new Map((picks || []).map((p) => [p.qid, p.optionId]));
const ordered = (questions || [])
.map((q) => ({ qid: q.qid, optionId: pickMap.get(q.qid) }))
.filter((p) => p.qid && p.optionId);
const hdParts = [];
const qMap = new Map((questions || []).map((q) => [q.qid, q]));
ordered.forEach((p) => {
const q = qMap.get(p.qid);
if (q && q.format === "v2" && q.radioName) {
parts.push(`${encodeURIComponent(q.radioName)}=${encodeURIComponent(p.optionId)}`);
} else {
parts.push(`radio_${p.qid}=${encodeURIComponent(p.optionId)}`);
hdParts.push(`${p.qid}|${p.optionId}`);
}
});
if (hdParts.length) {
parts.push(`hd_result=${encodeURIComponent(hdParts.join(","))}`);
}
parts.push("btn_submit.x=48", "btn_submit.y=17");
return parts.join("&");
}
async function resolveHyExamPostResponse(cwid, post, referer) {
let text = post.text || "";
let url = post.url || "";
if (parseHyExamResultPage(text, url) || /state_cour_ul/i.test(text)) {
return { text, url };
}
if (!/exam_result/i.test(url)) {
const resultUrl = `${location.origin}/pages/exam_result.aspx?cwid=${encodeURIComponent(cwid)}`;
try {
const r = await fetch(resultUrl, {
credentials: "include",
headers: {
Accept: "text/html,application/xhtml+xml",
Referer: referer || `${location.origin}/pages/exam.aspx?cwid=${encodeURIComponent(cwid)}`,
},
__hyEngineMark: true,
});
if (r.ok) {
const resultText = await r.text();
const resultUrlFinal = r.url || resultUrl;
if (
parseHyExamResultPage(resultText, resultUrlFinal) ||
/state_cour_ul/i.test(resultText) ||
isHyExamPassHtml(resultText, resultUrlFinal)
) {
return { text: resultText, url: resultUrlFinal };
}
}
} catch (_) {}
}
return { text, url };
}
function readLiveExamPageBundle(examId) {
if (pageType() !== "exam") return null;
const cur = new URLSearchParams(location.search).get("cwid");
if (!cur || String(cur).toUpperCase() !== String(examId).toUpperCase()) return null;
const html = document.documentElement ? document.documentElement.outerHTML : "";
const questions = parseHyExamQuestions(html);
if (!questions.length) return null;
return {
ok: true,
url: location.href,
html,
hidden: parseAspNetHidden(html),
questions,
examId: cur,
live: true,
};
}
function isHyExamFacePage(html, url) {
const text = String(html || "");
const u = String(url || "");
if (/\/pages\/face\.aspx/i.test(u)) return true;
if (/getFaceQrcode\.ashx|getFaceAuth\.ashx|\u5237\u8138\u8ba4\u8bc1|\u638c\u4e0a\u534e\u533b.*\u626b\u4e00\u626b|\u626b\u4e00\u626b.*\u638c\u4e0a\u534e\u533b/.test(text)) return true;
return false;
}
function extractHyFaceCwrid(url) {
try {
const u = new URL(String(url || ""), location.origin);
return u.searchParams.get("cwrid") || u.searchParams.get("cwid") || "";
} catch (_) {
const m = String(url || "").match(/(?:cwrid|cwid)=([^&]+)/i);
return m ? decodeURIComponent(m[1]) : "";
}
}
function ctxVideoFaceKey(ctx) {
return String((ctx && ctx.cwrid) || (ctx && ctx.cwid) || "").trim().toLowerCase();
}
function ctxNeedsHyVideoFaceBlock(ctx) {
return false;
}
async function ensureProgressFaceAllowed(ctx) {
const key = ctxVideoFaceKey(ctx);
if (!key || isHyVideoFaceVerified(key)) return true;
if (isHyVideoFacePending(key)) {
const still = await probeHyVideoFaceNeed(ctx).catch(function () {
return false;
});
if (!still) {
clearHyVideoFacePending(key);
return true;
}
const ok = await ensureHyVideoFaceVerified(ctx, false);
if (!ok && getEnabled()) {
stopAutoStudy("\u8bf7\u5148\u5b8c\u6210\u770b\u8bfe\u524d\u4eba\u8138\u9a8c\u8bc1\uff0c\u5b8c\u6210\u540e\u70b9\u300c\u5f00\u59cb\u300d\u7ee7\u7eed");
}
return ok;
}
const need = await probeHyVideoFaceNeed(ctx).catch(function () {
return false;
});
if (!need) return true;
const ok = await ensureHyVideoFaceVerified(ctx, false);
if (!ok && getEnabled()) {
stopAutoStudy("\u8bf7\u5148\u5b8c\u6210\u770b\u8bfe\u524d\u4eba\u8138\u9a8c\u8bc1\uff0c\u5b8c\u6210\u540e\u70b9\u300c\u5f00\u59cb\u300d\u7ee7\u7eed");
}
return ok;
}
function markHyVideoFacePending(cwrid) {
const id = String(cwrid || "").trim().toLowerCase();
if (id) hyVideoFacePending.add(id);
}
function clearHyVideoFacePending(cwrid) {
hyVideoFacePending.delete(String(cwrid || "").trim().toLowerCase());
}
function isHyVideoFacePending(cwrid) {
return hyVideoFacePending.has(String(cwrid || "").trim().toLowerCase());
}
function isHyVideoFaceGateUrl(url) {
const u = String(url || "");
return /face\.aspx/i.test(u) && /(?:\?|&)ff=1(?:&|$|#)/i.test(u);
}
function isHyPlayPageReady(html, url) {
const text = String(html || "");
const u = String(url || "");
return (
/course_ware_polyv\.aspx/i.test(u) &&
/polyvPlayer|player\.polyv\.net|s2j_onPlayOver|pv-video-player|id=[\"']player[\"']/.test(text)
);
}
function isHyVideoFaceRequiredPage(html, url, pageMeta) {
const text = String(html || "");
const u = String((pageMeta && pageMeta.responseURL) || (pageMeta && pageMeta.url) || url || "");
const meta = pageMeta || {};
const cwrid =
extractHyFaceCwrid(u) ||
(meta.cwrid ? String(meta.cwrid) : "") ||
extractHyFaceCwrid(meta.url || "") ||
extractHyFaceCwrid(url || "");
if (cwrid && isHyVideoFaceVerified(cwrid)) {
clearHyVideoFacePending(cwrid);
return false;
}
if (isHyPlayPageReady(text, u)) {
if (cwrid) clearHyVideoFacePending(cwrid);
return false;
}
if (meta.videoFaceGate && isHyVideoFaceGateUrl(u)) {
if (cwrid) markHyVideoFacePending(cwrid);
return true;
}
if (isHyVideoFaceGateUrl(u)) {
if (cwrid) markHyVideoFacePending(cwrid);
return true;
}
if (cwrid) clearHyVideoFacePending(cwrid);
return false;
}
function scheduleHyVideoFacePrompt(ctx) {
const key = ctxVideoFaceKey(ctx);
if (!key || !getEnabled() || isHyVideoFaceVerified(key)) return;
if (hyVideoFacePrompting.has(key) || hyVideoFaceVerifyPromises.has(key)) return;
hyVideoFacePrompting.add(key);
setTimeout(function () {
void (async function () {
try {
if (!getEnabled() || isHyVideoFaceVerified(key)) return;
const need = await probeHyVideoFaceNeed(ctx).catch(function () {
return false;
});
if (!need) return;
injectHyPanelStyles();
await ensureHyVideoFaceVerified(ctx, false);
} catch (_) {
} finally {
hyVideoFacePrompting.delete(key);
}
})();
}, 80);
}
async function resolveCoaidForFace(ctx) {
let enriched = enrichFaceCtxFromDom(ctx);
if (enriched.coaid) return enriched;
const key = enriched.cwrid || enriched.cwid || "";
const jobHit = findJobCtxByCwid(key);
if (jobHit && jobHit.coaid) {
return Object.assign({}, enriched, {
coaid: jobHit.coaid,
cid: enriched.cid || jobHit.cid,
uid: enriched.uid || jobHit.uid,
});
}
const panelHit = findPanelChapterCtxByCwid(key);
if (panelHit && panelHit.coaid) {
return Object.assign({}, enriched, {
coaid: panelHit.coaid,
cid: enriched.cid || panelHit.cid,
uid: enriched.uid || panelHit.uid,
});
}
const cid = enriched.cid || (jobHit && jobHit.cid) || (panelHit && panelHit.cid);
if (cid) {
let course = panelState.courses.find((c) => c.cid === cid);
if (course && !isCourseChaptersReady(course)) {
try {
await enrichPanelCourse(course, true);
} catch (_) {}
course = panelState.courses.find((c) => c.cid === cid);
}
const ch =
(course &&
course.chapters &&
course.chapters.find(
(c) =>
String(c.cwrid || "").toLowerCase() === String(key).toLowerCase() ||
String(c.cwid || "").toLowerCase() === String(key).toLowerCase()
)) ||
null;
if (ch && ch.coaid) {
return Object.assign({}, enriched, {
coaid: ch.coaid,
cid: ch.cid || cid,
uid: enriched.uid || ch.uid,
});
}
const coaidFromPage = await fetchCoaidFromCoursePage(cid, key);
if (coaidFromPage) {
return Object.assign({}, enriched, { coaid: coaidFromPage, cid });
}
}
return enriched;
}
async function fetchCoaidFromCoursePage(cid, cwrid) {
if (!cid || !cwrid) return "";
try {
const html = await hyFetchHtml(coursePageUrl(cid));
const id = String(cwrid).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const m1 = html.match(
new RegExp('data-cwrid=["\']' + id + '["\'][^>]*data-coaid=["\']([^"\']+)["\']', "i")
);
if (m1) return m1[1];
const m2 = html.match(
new RegExp('data-coaid=["\']([^"\']+)["\'][^>]*data-cwrid=["\']' + id + '["\']', "i")
);
if (m2) return m2[1];
const m3 = html.match(
new RegExp('data-cwrid=["\']' + id + '["\'][^>]*data-coaid=["\']([^"\']+)["\']', "i")
);
return m3 ? m3[1] : "";
} catch (_) {
return "";
}
}
async function probeHyVideoFaceNeed(ctx) {
const cwid = ctx.cwid || ctx.cwrid;
if (!cwid) return false;
const enriched = await resolveCoaidForFace(ctx);
const fc = resolveHyFaceCtx(Object.assign({}, enriched, { ff: "1" }));
if (!fc.cwrid || isHyVideoFaceVerified(fc.cwrid)) return false;
const playUrl = buildPolyvPlayUrl(cwid);
try {
const page = await fetchPolyvPlayPage(playUrl, { __hyEngineMark: true });
const need = isHyVideoFaceRequiredPage(page.text || "", page.url || playUrl, page);
if (!need) clearHyVideoFacePending(fc.cwrid);
return need;
} catch (_) {
clearHyVideoFacePending(fc.cwrid);
return false;
}
}
function markHyVideoFaceVerified(cwrid) {
const id = String(cwrid || "").trim().toLowerCase();
if (id) {
hyVideoFaceVerified.add(id);
clearHyVideoFacePending(id);
}
}
function clearHyVideoFaceVerified(cwrid) {
hyVideoFaceVerified.delete(String(cwrid || "").trim().toLowerCase());
}
function isHyVideoFaceVerified(cwrid) {
return hyVideoFaceVerified.has(String(cwrid || "").trim().toLowerCase());
}
function enrichFaceCtxFromDom(ctx) {
const cwrid = (ctx && ctx.cwrid) || (ctx && ctx.cwid) || "";
if (!cwrid) return ctx || {};
let coaid = (ctx && ctx.coaid) || "";
if (!coaid) {
const row = document.querySelector(`.cw-progress-row[data-cwrid="${cwrid}"]`);
if (row) coaid = row.getAttribute("data-coaid") || "";
}
if (!coaid) {
const row2 = document.querySelector(`.cw-progress-row[data-cwrid="${cwrid.toUpperCase()}"]`);
if (row2) coaid = row2.getAttribute("data-coaid") || "";
}
if (!coaid) {
const jobHit = findJobCtxByCwid(cwrid);
if (jobHit && jobHit.coaid) coaid = jobHit.coaid;
}
if (!coaid) {
const panelHit = findPanelChapterCtxByCwid(cwrid);
if (panelHit && panelHit.coaid) coaid = panelHit.coaid;
}
return Object.assign({}, ctx, { coaid, cwrid: cwrid || ctx.cwrid });
}
async function requireHyVideoFaceIfNeeded(ctx, label) {
const enriched = await resolveCoaidForFace(ctx);
const fc = resolveHyFaceCtx(Object.assign({}, enriched, { ff: "1" }));
if (!fc.cwrid || isHyVideoFaceVerified(fc.cwrid)) return true;
const needsFace = await probeHyVideoFaceNeed(enriched);
if (!needsFace) return true;
const faceLabel = label || ctx.title || fc.cwrid.slice(0, 8);
pushUserLog(`${faceLabel} · polyv \u8df3\u8f6c face.aspx?ff=1\uff0c\u9700\u5148\u770b\u8bfe\u524d\u4eba\u8138`);
injectHyPanelStyles();
return ensureHyVideoFaceVerified(enriched, false);
}
function resolveHyFaceCtx(ctx, refererCwrid) {
const cwrid =
(ctx && ctx.cwrid) ||
refererCwrid ||
(ctx && ctx.cwid) ||
new URLSearchParams(location.search).get("cwrid") ||
"";
const coaid = (ctx && ctx.coaid) || "";
const ff =
(ctx && ctx.ff != null ? String(ctx.ff) : "") ||
new URLSearchParams(location.search).get("ff") ||
"0";
return { cwrid: String(cwrid || ""), coaid: String(coaid || ""), ff };
}
function normalizeHyFaceImageUrl(raw) {
const src = String(raw || "").trim();
if (!src) return "";
if (/^data:image\//i.test(src)) return src;
if (/^https?:\/\//i.test(src)) return src;
if (src.startsWith("//")) return location.protocol + src;
if (/^(iVBOR|\/9j\/|R0lGOD)/.test(src)) {
const mime = src.startsWith("iVBOR") ? "image/png" : src.startsWith("/9j/") ? "image/jpeg" : "image/gif";
return `data:${mime};base64,${src}`;
}
return `${location.origin}${src.startsWith("/") ? "" : "/"}${src}`;
}
function parseHyFaceQrcodeResponse(text) {
const raw = String(text || "").trim();
if (!raw || raw === "0") return { ok: false, msg: "\u4e8c\u7ef4\u7801\u83b7\u53d6\u5931\u8d25" };
const sep = "|hy|";
const pos = raw.indexOf(sep);
if (pos < 0) return { ok: false, msg: "\u4e8c\u7ef4\u7801\u683c\u5f0f\u5f02\u5e38" };
const qrCodeId = raw.slice(0, pos).trim();
const qrPart = raw.slice(pos + sep.length).trim();
if (!qrCodeId || !qrPart) return { ok: false, msg: "\u4e8c\u7ef4\u7801\u683c\u5f0f\u5f02\u5e38" };
return {
ok: true,
qrCodeId,
qrImageUrl: normalizeHyFaceImageUrl(qrPart),
};
}
async function fetchHyFaceQrcode(cwrid, coaid, ff) {
const params = new URLSearchParams({
erWeiMaClass: "02",
erWeiMaSystem: "02",
ff: String(ff != null ? ff : "0"),
cwrid: String(cwrid || ""),
coaid: String(coaid || ""),
r: String(Math.random()),
});
try {
const r = await fetch(`${location.origin}/ashx/getFaceQrcode.ashx?${params}`, {
credentials: "include",
headers: { Accept: "text/plain,*/*" },
});
const text = String(await r.text()).trim();
const parsed = parseHyFaceQrcodeResponse(text);
if (!parsed.ok) return parsed;
return parsed;
} catch (e) {
return { ok: false, msg: (e && e.message) || "\u83b7\u53d6\u4e8c\u7ef4\u7801\u5931\u8d25" };
}
}
async function fetchHyFaceAuth(qrCodeId, cwrid, coaid, ff) {
const body = new URLSearchParams({
qrcode_id: String(qrCodeId || ""),
unit_no: "02",
cwrid: String(cwrid || ""),
cid: "",
coaid: String(coaid || ""),
ff: String(ff != null ? ff : "0"),
r: String(Math.random()),
});
const r = await fetch(`${location.origin}/ashx/getFaceAuth.ashx`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json,*/*" },
body: body.toString(),
});
const text = await r.text();
try {
return JSON.parse(text);
} catch (_) {
return { code: -1, message: text || "\u8ba4\u8bc1\u72b6\u6001\u89e3\u6790\u5931\u8d25" };
}
}
function ensureHyExamFaceModal() {
if (document.getElementById("hy-cme-face-modal")) return;
const modal = document.createElement("div");
modal.id = "hy-cme-face-modal";
modal.innerHTML = `
\u8003\u8bd5\u524d\u4eba\u8138\u8bc6\u522b
\u8bf7\u4f7f\u7528\u638c\u4e0a\u534e\u533b APP \u626b\u7801\u5b8c\u6210\u8ba4\u8bc1
\u8bf7\u4f7f\u7528\u638c\u4e0a\u534e\u533b APP\u300c\u626b\u4e00\u626b\u300d\u626b\u63cf\u4e0a\u65b9\u4e8c\u7ef4\u7801\uff0c\u5b8c\u6210\u8ba4\u8bc1\u540e\u53ef\u7ee7\u7eed\u8003\u8bd5\u3002
\u300c\u53d6\u6d88\u300d\u7a0d\u540e\u91cd\u8bd5\u672c\u8282\u8003\u8bd5\uff1b\u300c\u6682\u4e0d\u8003\u8bd5\u300d\u672c\u6b21\u8fd0\u884c\u8df3\u8fc7\u6240\u6709\u8003\u8bd5\u3001\u4ec5\u5b66\u89c6\u9891\uff08\u91cd\u65b0\u70b9\u5f00\u59cb\u540e\u6062\u590d\u8003\u8bd5\uff09\u3002
\u7b49\u5f85\u626b\u7801…
`;
document.body.appendChild(modal);
modal.querySelector("#hy-cme-face-cancel")?.addEventListener("click", () => {
if (hyFaceModalKind === "video") videoFaceCancel = true;
else examFaceCancel = true;
updateHyExamFaceModalStatus("\u5df2\u53d6\u6d88\uff0c\u7a0d\u540e\u91cd\u8bd5");
});
modal.querySelector("#hy-cme-face-skip")?.addEventListener("click", () => {
examFaceSkipAll = true;
examFaceCancel = true;
pushUserLog("\u5df2\u9009\u62e9\u6682\u4e0d\u8003\u8bd5\uff0c\u672c\u6b21\u8fd0\u884c\u4ec5\u5b66\u4e60\u89c6\u9891\uff08\u91cd\u65b0\u70b9\u5f00\u59cb\u53ef\u6062\u590d\u8003\u8bd5\uff09");
updateHyExamFaceModalStatus("\u5df2\u9009\u62e9\u6682\u4e0d\u8003\u8bd5\uff0c\u6b63\u5728\u5207\u6362\u5230\u89c6\u9891\u5b66\u4e60…");
});
}
function resetHyFaceModalFlags(kind) {
if (kind === "video") {
videoFaceCancel = false;
} else {
examFaceCancel = false;
examFaceSkipAll = false;
}
}
function configureHyFaceModal(kind) {
injectHyPanelStyles();
ensureHyExamFaceModal();
const isVideo = kind === "video";
hyFaceModalKind = isVideo ? "video" : "exam";
resetHyFaceModalFlags(hyFaceModalKind);
const title = document.getElementById("hy-cme-face-title");
const tipMain = document.getElementById("hy-cme-face-tip-main");
const tipActions = document.getElementById("hy-cme-face-tip-actions");
const skipBtn = document.getElementById("hy-cme-face-skip");
const cancelBtn = document.getElementById("hy-cme-face-cancel");
if (title) title.textContent = isVideo ? "\u770b\u8bfe\u524d\u4eba\u8138\u8bc6\u522b" : "\u8003\u8bd5\u524d\u4eba\u8138\u8bc6\u522b";
if (tipMain) {
tipMain.innerHTML = isVideo
? "\u8bf7\u4f7f\u7528\u638c\u4e0a\u534e\u533b APP\u300c\u626b\u4e00\u626b\u300d\u626b\u63cf\u4e0a\u65b9\u4e8c\u7ef4\u7801\uff0c\u5b8c\u6210\u8ba4\u8bc1\u540e\u53ef\u7ee7\u7eed\u89c2\u770b\u8bfe\u4ef6\u3002"
: "\u8bf7\u4f7f\u7528\u638c\u4e0a\u534e\u533b APP\u300c\u626b\u4e00\u626b\u300d\u626b\u63cf\u4e0a\u65b9\u4e8c\u7ef4\u7801\uff0c\u5b8c\u6210\u8ba4\u8bc1\u540e\u53ef\u7ee7\u7eed\u8003\u8bd5\u3002";
}
if (tipActions) {
tipActions.style.display = isVideo ? "none" : "";
if (!isVideo) {
tipActions.textContent =
"\u300c\u53d6\u6d88\u300d\u7a0d\u540e\u91cd\u8bd5\u672c\u8282\u8003\u8bd5\uff1b\u300c\u6682\u4e0d\u8003\u8bd5\u300d\u672c\u6b21\u8fd0\u884c\u8df3\u8fc7\u6240\u6709\u8003\u8bd5\u3001\u4ec5\u5b66\u89c6\u9891\uff08\u91cd\u65b0\u70b9\u5f00\u59cb\u540e\u6062\u590d\u8003\u8bd5\uff09\u3002";
}
}
if (skipBtn) skipBtn.style.display = isVideo ? "none" : "";
if (cancelBtn) cancelBtn.textContent = "\u53d6\u6d88";
}
function showHyExamFaceModalLoading(label) {
configureHyFaceModal("video");
const modal = document.getElementById("hy-cme-face-modal");
const qr = document.getElementById("hy-cme-face-qr");
const labelEl = document.getElementById("hy-cme-face-label");
const status = document.getElementById("hy-cme-face-status");
if (!modal) return;
if (labelEl && label) labelEl.textContent = `${label} · \u8bf7\u4f7f\u7528\u638c\u4e0a\u534e\u533b APP \u626b\u7801\u5b8c\u6210\u8ba4\u8bc1`;
if (qr) qr.removeAttribute("src");
if (status) status.textContent = "\u6b63\u5728\u83b7\u53d6\u4e8c\u7ef4\u7801…";
modal.style.display = "flex";
}
function showHyExamFaceModal(opts) {
const kind = opts?.kind === "video" ? "video" : "exam";
configureHyFaceModal(kind);
const modal = document.getElementById("hy-cme-face-modal");
const qr = document.getElementById("hy-cme-face-qr");
const label = document.getElementById("hy-cme-face-label");
const status = document.getElementById("hy-cme-face-status");
if (!modal || !qr) return;
if (label && opts?.label) {
label.textContent = `${opts.label} · \u8bf7\u4f7f\u7528\u638c\u4e0a\u534e\u533b APP \u626b\u7801\u5b8c\u6210\u8ba4\u8bc1`;
}
qr.src = String(opts?.qrImageUrl || "");
if (status) status.textContent = "\u7b49\u5f85\u626b\u7801…\uff08\u4e8c\u7ef4\u7801\u7ea6 90 \u79d2\u5185\u6709\u6548\uff09";
modal.style.display = "flex";
}
function updateHyExamFaceModalStatus(text) {
const el = document.getElementById("hy-cme-face-status");
if (el) el.textContent = String(text || "");
}
function updateHyExamFaceModalCountdown(sec, kind) {
const mode = kind === "video" ? "video" : hyFaceModalKind;
if (mode === "video" ? videoFaceCancel : examFaceCancel) return;
updateHyExamFaceModalStatus(`\u7b49\u5f85\u626b\u7801…\u5269\u4f59 ${Math.max(0, sec)} \u79d2`);
}
function closeHyExamFaceModal() {
const modal = document.getElementById("hy-cme-face-modal");
if (modal) modal.style.display = "none";
examFaceCancel = false;
examFaceSkipAll = false;
videoFaceCancel = false;
}
function hyFacePollAbortResult(kind) {
const mode = kind === "video" ? "video" : "exam";
if (mode === "video") {
if (!videoFaceCancel) return null;
return { ok: false, cancelled: true, skipAll: false };
}
if (!examFaceCancel && !examFaceSkipAll) return null;
return { ok: false, cancelled: true, skipAll: !!examFaceSkipAll };
}
async function pollHyFaceUntilVerified(qrCodeId, faceCtx, timeoutMs, kind) {
const fc = resolveHyFaceCtx(faceCtx);
const deadline = Date.now() + (timeoutMs || 90000);
while (Date.now() < deadline) {
const abort = hyFacePollAbortResult(kind);
if (abort) return abort;
try {
const data = await fetchHyFaceAuth(qrCodeId, fc.cwrid, fc.coaid, fc.ff);
if (data && Number(data.code) === 1) return { ok: true };
if (data && Number(data.code) === 5) return { ok: false, expired: true };
if (data && Number(data.code) !== 0) {
return { ok: false, msg: (data && data.message) || "\u4eba\u8138\u8bc6\u522b\u5931\u8d25" };
}
} catch (e) {
trace("getFaceAuth\uff1a" + ((e && e.message) || e));
}
updateHyExamFaceModalCountdown(Math.ceil((deadline - Date.now()) / 1000), kind);
await sleep(2000);
}
return { ok: false, timeout: true };
}
async function waitHyFaceVerify(faceCtx, label, kind) {
const mode = kind === "video" ? "video" : "exam";
configureHyFaceModal(mode);
const enriched = mode === "video" ? await resolveCoaidForFace(faceCtx) : enrichFaceCtxFromDom(faceCtx);
const fc = resolveHyFaceCtx(enriched);
if (!fc.cwrid) return { ok: false, msg: "\u7f3a\u5c11 cwrid\uff0c\u65e0\u6cd5\u4eba\u8138\u8bc6\u522b" };
const apiFf = mode === "video" ? "1" : fc.ff;
const apiCtx = Object.assign({}, enriched, { ff: apiFf });
const maxCycles = 24;
for (let cycle = 0; cycle < maxCycles; cycle++) {
const abort = hyFacePollAbortResult(mode);
if (abort) {
closeHyExamFaceModal();
if (mode === "video") return { ok: false, msg: "\u7a0d\u540e\u91cd\u8bd5\u672c\u8282" };
if (abort.skipAll) return { ok: false, skipAll: true, msg: "\u5df2\u8df3\u8fc7\u8003\u8bd5\uff0c\u4ec5\u5b66\u4e60\u89c6\u9891" };
return { ok: false, msg: "\u7a0d\u540e\u91cd\u8bd5\u672c\u8282\u8003\u8bd5" };
}
const ewm = await fetchHyFaceQrcode(fc.cwrid, fc.coaid, apiFf);
if (!ewm.ok) {
closeHyExamFaceModal();
pushUserLog(
`${label || fc.cwrid.slice(0, 8)} · \u4eba\u8138\u4e8c\u7ef4\u7801\u83b7\u53d6\u5931\u8d25${ewm.msg ? "\uff1a" + ewm.msg : ""}${!fc.coaid ? "\uff08\u7f3a\u5c11 coaid\uff09" : ""}`
);
return { ok: false, msg: ewm.msg || "\u83b7\u53d6\u4eba\u8138\u8bc6\u522b\u4e8c\u7ef4\u7801\u5931\u8d25" };
}
showHyExamFaceModal({ label, qrImageUrl: ewm.qrImageUrl, kind: mode });
if (cycle === 0) {
pushLog(
`[${label || fc.cwrid.slice(0, 8)}] ${mode === "video" ? "\u770b\u8bfe\u524d\u9700\u4eba\u8138\u8bc6\u522b\uff0c\u8bf7\u7528\u638c\u4e0a\u534e\u533b APP \u626b\u7801" : "\u9700\u8981\u4eba\u8138\u8bc6\u522b\uff0c\u8bf7\u7528\u638c\u4e0a\u534e\u533b APP \u626b\u7801\u9a8c\u8bc1"}`
);
}
const verified = await pollHyFaceUntilVerified(ewm.qrCodeId, apiCtx, 90000, mode);
if (verified.ok) {
closeHyExamFaceModal();
pushLog(
`[${label || fc.cwrid.slice(0, 8)}] ${mode === "video" ? "\u770b\u8bfe\u524d\u4eba\u8138\u5df2\u901a\u8fc7\uff0c\u7ee7\u7eed\u5b66\u4e60" : "\u4eba\u8138\u8bc6\u522b\u5df2\u901a\u8fc7\uff0c\u7ee7\u7eed\u8003\u8bd5"}`
);
return { ok: true, qrCodeId: ewm.qrCodeId };
}
if (verified.cancelled) {
closeHyExamFaceModal();
if (mode === "video") return { ok: false, msg: "\u7a0d\u540e\u91cd\u8bd5\u672c\u8282" };
if (verified.skipAll) return { ok: false, skipAll: true, msg: "\u5df2\u8df3\u8fc7\u8003\u8bd5\uff0c\u4ec5\u5b66\u4e60\u89c6\u9891" };
return { ok: false, msg: "\u7a0d\u540e\u91cd\u8bd5\u672c\u8282\u8003\u8bd5" };
}
if (verified.msg) {
closeHyExamFaceModal();
return { ok: false, msg: verified.msg };
}
updateHyExamFaceModalStatus("\u4e8c\u7ef4\u7801\u5df2\u8fc7\u671f\uff0c\u6b63\u5728\u5237\u65b0…");
await sleep(600);
}
closeHyExamFaceModal();
return { ok: false, msg: "\u4eba\u8138\u8bc6\u522b\u8d85\u65f6\uff0c\u8bf7\u91cd\u8bd5" };
}
async function waitHyExamFaceVerify(faceCtx, label) {
return waitHyFaceVerify(faceCtx, label, "exam");
}
function skipAllPendingExams(reason) {
skipExamsSession = true;
closeHyExamFaceModal();
const jobs = loadJobs();
let n = 0;
Object.keys(jobs).forEach((key) => {
const j = jobs[key];
if (j && j.phase === "exam_pending") {
jobs[key] = Object.assign({}, j, { phase: "done", examDone: false, examSkipped: true });
n++;
}
});
if (n) saveJobs(jobs);
if (reason) pushLog(reason);
if (n > 0) {
pushUserLog(`\u5df2\u8df3\u8fc7 ${n} \u8282\u5f85\u8003\u8bd5\uff0c\u672c\u6b21\u8fd0\u884c\u4ec5\u5b66\u4e60\u89c6\u9891\uff08\u91cd\u65b0\u70b9\u5f00\u59cb\u53ef\u6062\u590d\u8003\u8bd5\uff09`);
} else {
pushUserLog("\u5df2\u9009\u62e9\u6682\u4e0d\u8003\u8bd5\uff0c\u672c\u6b21\u8fd0\u884c\u4ec5\u5b66\u4e60\u89c6\u9891\uff08\u91cd\u65b0\u70b9\u5f00\u59cb\u53ef\u6062\u590d\u8003\u8bd5\uff09");
}
updateJobsPanel();
maybeAutoStopWhenFinished();
}
function packHyExamPage(html, url, examId, live) {
if (isHyExamFacePage(html, url)) {
return { ok: false, needFace: true, html, url, examId, msg: "\u9700\u8981\u4eba\u8138\u8bc6\u522b" };
}
if (isHyExamCodePage(html, url)) {
return { ok: false, needCaptcha: true, html, url, examId, msg: "\u9700\u8981\u9a8c\u8bc1\u7801" };
}
if (isHyExamAlreadyPassedHtml(html) && !parseHyExamQuestions(html).length) {
return { ok: false, alreadyPassed: true, url, html, examId };
}
const hidden = parseAspNetHidden(html);
const questions = parseHyExamQuestions(html);
if (!questions.length) {
const hint = diagnoseExamPageHtml(html);
return { ok: false, msg: hint || "\u672a\u89e3\u6790\u5230\u8bd5\u9898", url, html, examId };
}
return { ok: true, url, html, hidden, questions, examId, live: !!live };
}
async function fetchHyExamPageOnce(examId, refererCwrid) {
const live = readLiveExamPageBundle(examId);
if (live) return live;
const url = `${location.origin}/pages/exam.aspx?cwid=${encodeURIComponent(examId)}`;
const referer = refererCwrid
? `${location.origin}/course_ware/course_ware_polyv.aspx?cwid=${encodeURIComponent(refererCwrid)}`
: `${location.origin}/pages/course.aspx`;
const r = await fetch(url, {
credentials: "include",
headers: {
Accept: "text/html,application/xhtml+xml",
Referer: referer,
},
});
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const html = await r.text();
const finalUrl = r.url || url;
if (isHyExamFacePage(html, finalUrl)) {
return { ok: false, needFace: true, html, url: finalUrl, examId, msg: "\u9700\u8981\u4eba\u8138\u8bc6\u522b" };
}
if (isHyExamCodePage(html, finalUrl)) {
return { ok: false, needCaptcha: true, html, url: finalUrl, examId, msg: "\u9700\u8981\u9a8c\u8bc1\u7801" };
}
return packHyExamPage(html, finalUrl, examId, false);
}
async function fetchHyExamPage(examIds, refererCwrid) {
const ids = [...new Set((Array.isArray(examIds) ? examIds : [examIds]).filter(Boolean))];
let last = null;
for (const id of ids) {
try {
last = await fetchHyExamPageOnce(id, refererCwrid || id);
if (last.ok || last.alreadyPassed) return last;
} catch (e) {
last = { ok: false, msg: e && e.message ? e.message : "\u62c9\u53d6\u8bd5\u5377\u5931\u8d25", examId: id };
}
}
return last || { ok: false, msg: "\u672a\u89e3\u6790\u5230\u8bd5\u9898" };
}
async function postHyExamPage(cwid, hidden, picks, referer, questions) {
const url = `${location.origin}/pages/exam.aspx?cwid=${encodeURIComponent(cwid)}`;
const body = buildHyExamPostBody(hidden, picks, questions);
const r = await fetch(url, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "text/html,application/xhtml+xml",
},
body,
redirect: "follow",
referrer: referer || url,
__hyEngineMark: true,
});
const raw = { text: await r.text(), url: r.url || url, status: r.status };
return resolveHyExamPostResponse(cwid, raw, referer || url);
}
function shouldNavigateToExamPage(res) {
return !!(res && !res.ok && /\u672a\u89e3\u6790\u5230\u8bd5\u9898|\u672a\u83b7\u53d6\u8bd5\u5377|\u767b\u5f55\u5931\u6548|\u9009\u9879\u672a\u52a0\u8f7d/.test(String(res.msg || "")));
}
function navigateToExamPage(ctx, label) {
const examId = resolveExamCwid(ctx);
if (!examId) return false;
try {
sessionStorage.setItem(
EXAM_NAV_KEY,
JSON.stringify({ key: jobKey(ctx), examId, label: label || ctx.title || "", ts: Date.now() })
);
} catch (_) {}
pushLog(`[${label || ctx.title || examId.slice(0, 8)}] \u8df3\u8f6c\u8003\u8bd5\u9875\uff08DOM \u62c9\u5377\uff09…`);
location.href = `${location.origin}/pages/exam.aspx?cwid=${encodeURIComponent(examId)}`;
return true;
}
async function verifyHyExamPlatformPassed(examId, refererCwrid) {
const ids = [...new Set([examId].filter(Boolean))];
for (const id of ids) {
const resultUrl = `${location.origin}/pages/exam_result.aspx?cwid=${encodeURIComponent(id)}`;
try {
const r = await fetch(resultUrl, {
credentials: "include",
headers: {
Accept: "text/html,application/xhtml+xml",
Referer: `${location.origin}/pages/exam.aspx?cwid=${encodeURIComponent(id)}`,
},
__hyEngineMark: true,
});
const text = await r.text();
const url = r.url || resultUrl;
const fb = parseHyExamResultPage(text, url);
if (fb && fb.failed) {
return { passed: false, msg: fb.score ? `\u672a\u901a\u8fc7\uff08${fb.score}\u5206\uff09` : "\u5e73\u53f0\u663e\u793a\u672a\u901a\u8fc7" };
}
if (isHyExamPassHtml(text, url)) {
return { passed: true, score: (fb && fb.score) || "" };
}
} catch (_) {}
try {
const page = await fetchHyExamPageOnce(id, refererCwrid || id);
if (page.alreadyPassed) return { passed: true, skipped: true };
if (page.ok && page.questions && page.questions.length) {
return { passed: false, msg: "\u5e73\u53f0\u4ecd\u53ef\u7b54\u5377\uff0c\u8003\u8bd5\u672a\u901a\u8fc7" };
}
if (isHyExamAlreadyPassedHtml(page.html || "")) return { passed: true, skipped: true };
} catch (_) {}
}
return { passed: false, msg: "\u5e73\u53f0\u672a\u786e\u8ba4\u8003\u8bd5\u901a\u8fc7" };
}
async function runHyClientExam(cwid, label, altIds, refererCwrid, faceCtx) {
if (!cwid && (!altIds || !altIds.length)) return { ok: false, msg: "\u7f3a\u5c11\u8003\u8bd5 cwid" };
if (skipExamsSession) return { ok: false, skipAll: true, msg: "\u5df2\u8df3\u8fc7\u8003\u8bd5\uff0c\u4ec5\u5b66\u4e60\u89c6\u9891" };
const tag = label || String(cwid || altIds[0] || "").slice(0, 8);
const state = createHyExamAnswerState();
const examIds = [...new Set([cwid, ...(altIds || [])].filter(Boolean))];
pushLog(`[${tag}] \u5f00\u59cb\u8003\u8bd5\uff08API \u66b4\u529b\u63d0\u4ea4\uff09…`);
for (let t = 0; t < HY_EXAM_MAX_TRIES; t++) {
if (!getEnabled()) {
pushLog(`[${tag}] \u5df2\u6682\u505c\uff0c\u505c\u6b62\u8003\u8bd5`);
return { ok: false, msg: "\u5df2\u6682\u505c", paused: true };
}
let fresh;
try {
fresh = await fetchHyExamPage(examIds, refererCwrid || cwid || examIds[0]);
} catch (e) {
return { ok: false, msg: e && e.message ? e.message : "\u62c9\u53d6\u8bd5\u5377\u5931\u8d25" };
}
if (!fresh.ok) {
if (fresh.alreadyPassed) {
return { ok: true, skipped: true, msg: "\u5df2\u901a\u8fc7" };
}
let needFace = !!(fresh.needFace || isHyExamFacePage(fresh.html, fresh.url));
if (!needFace && t === 0 && /\u672a\u89e3\u6790\u5230\u8bd5\u9898|\u672a\u83b7\u53d6\u8bd5\u5377/.test(fresh.msg || "")) {
const fc = resolveHyFaceCtx(faceCtx, refererCwrid);
if (fc.cwrid) {
const probe = await fetchHyFaceQrcode(fc.cwrid, fc.coaid, fc.ff);
needFace = probe.ok;
}
}
if (needFace) {
if (t > 0) return { ok: false, msg: "\u4eba\u8138\u8bc6\u522b\u540e\u4ecd\u672a\u83b7\u53d6\u8bd5\u5377\uff0c\u8bf7\u91cd\u8bd5" };
pushUserLog(`${tag} · \u8003\u8bd5\u524d\u9700\u4eba\u8138\u8bc6\u522b\uff0c\u8bf7\u7528\u638c\u4e0a\u534e\u533b APP \u626b\u7801`);
const face = await waitHyExamFaceVerify(faceCtx || { cwrid: refererCwrid || cwid }, label);
if (!face.ok) {
if (face.skipAll) return { ok: false, skipAll: true, msg: face.msg || "\u5df2\u8df3\u8fc7\u8003\u8bd5\uff0c\u4ec5\u5b66\u4e60\u89c6\u9891" };
return { ok: false, msg: face.msg || "\u7a0d\u540e\u91cd\u8bd5\u672c\u8282\u8003\u8bd5" };
}
await sleep(500);
continue;
}
if (fresh.needCaptcha || isHyExamCodePage(fresh.html, fresh.url) || isHyExamCaptchaHtml(fresh.html)) {
const cap = await tryPassHyExamCaptcha(
fresh.examId || cwid || examIds[0],
tag,
fresh.html,
refererCwrid || fresh.url
);
if (!cap.ok) return cap;
continue;
}
return { ok: false, msg: fresh.msg || "\u672a\u83b7\u53d6\u8bd5\u5377", needNavigate: shouldNavigateToExamPage(fresh) };
}
applyHyExamLetterCache(state, fresh.questions, fresh.examId || cwid);
if (t === 0) {
pushLog(
`[${tag}] \u5df2\u89e3\u6790 ${fresh.questions.length} \u9898\uff08${fresh.live ? "\u5f53\u524d\u8003\u8bd5\u9875" : "API \u62c9\u5377"}·${fresh.questions[0].format || "v1"}${Object.keys(state.byQid).length ? `\uff0c\u5df2\u8bb0\u5fc6 ${Object.keys(state.byQid).length} \u9898` : ""}\uff09`
);
}
const picks = fillHyExamPicks(fresh.questions, state);
const pickLetters = formatHyExamPicksLog(fresh.questions, picks);
pushLog(`[${tag}] \u672c\u6b21\u9009\u9898 ${pickLetters} (${t + 1}/${HY_EXAM_MAX_TRIES})`);
let post;
try {
post = await postHyExamPage(
fresh.examId || cwid || examIds[0],
fresh.hidden,
picks,
fresh.url,
fresh.questions
);
} catch (e) {
return { ok: false, msg: e && e.message ? e.message : "\u4ea4\u5377\u5931\u8d25" };
}
const resultFbEarly = parseHyExamResultPage(post.text, post.url);
if (
!resultFbEarly &&
!isHyExamPassHtml(post.text, post.url) &&
/exam\.aspx/i.test(post.url || "") &&
parseHyExamQuestions(post.text).length
) {
pushLog(`[${tag}] \u4ea4\u5377\u672a\u8df3\u8f6c\u7ed3\u679c\u9875\uff08hd_result \u53ef\u80fd\u65e0\u6548\uff09`);
} else if (t === 0 && !resultFbEarly && !isHyExamPassHtml(post.text, post.url)) {
const pageName = (post.url || "").split("/").pop() || "?";
pushLog(`[${tag}] \u4ea4\u5377\u54cd\u5e94 ${pageName}\uff08${(post.text || "").length} \u5b57\u8282\uff09`);
}
if (isHyExamPassHtml(post.text, post.url)) {
const verify = await verifyHyExamPlatformPassed(fresh.examId || cwid, refererCwrid || cwid);
if (!verify.passed) {
pushLog(`[${tag}] \u4ea4\u5377\u54cd\u5e94\u50cf\u901a\u8fc7\u4f46\u5e73\u53f0\u672a\u786e\u8ba4\uff1a${verify.msg || "\u672a\u901a\u8fc7"}`);
if (/\u5e73\u53f0\u4ecd\u53ef\u7b54\u5377/.test(verify.msg || "")) {
const resultFb = parseHyExamResultPage(post.text, post.url);
if (resultFb && (resultFb.correct.length || resultFb.wrong.length)) {
saveHyExamLetterCache(fresh.examId || cwid, resultFb, fresh.questions);
applyHyExamResultFeedback(state, fresh.questions, resultFb);
}
pushLog(`[${tag}] \u7b49\u5f85 ${HY_EXAM_RETRY_GAP_MS / 1000} \u79d2\u540e\u91cd\u8bd5…`);
if (!(await hyExamRetryWait(HY_EXAM_RETRY_GAP_MS, tag))) {
return { ok: false, msg: "\u5df2\u6682\u505c", paused: true };
}
continue;
}
return { ok: false, msg: verify.msg || "\u5e73\u53f0\u672a\u786e\u8ba4\u8003\u8bd5\u901a\u8fc7" };
}
picks.forEach((p) => {
state.byQid[p.qid] = p.optionId;
});
const passFb = parseHyExamResultPage(post.text, post.url);
if (passFb) saveHyExamLetterCache(fresh.examId || cwid, passFb, fresh.questions);
const scoreM =
verify.score ||
(passFb && passFb.score) ||
(post.text.match(/test_score:\s*["'](\d+)/i) || [])[1] ||
(post.text.match(/(\d+)\s*\u5206/) || [])[1];
return { ok: true, skipped: !!verify.skipped, score: scoreM || "", msg: "\u8003\u8bd5\u901a\u8fc7" };
}
if (/\u5b66\u4e60\u65f6\u957f|\u8bf7\u5148\u5b66\u4e60|\u4e0d\u80fd\u7b54\u5377|\u672a\u5b66\u5b8c/.test(post.text)) {
return { ok: false, msg: "\u5b66\u65f6\u672a\u6ee1\uff0c\u4e0d\u80fd\u7b54\u5377" };
}
if (isHyExamCodePage(post.text, post.url) || isHyExamCaptchaHtml(post.text)) {
const cap = await tryPassHyExamCaptcha(
fresh.examId || cwid || examIds[0],
tag,
post.text,
fresh.url
);
if (!cap.ok) return cap;
continue;
}
const resultFb = parseHyExamResultPage(post.text, post.url);
if (resultFb && (resultFb.correct.length || resultFb.wrong.length)) {
saveHyExamLetterCache(fresh.examId || cwid, resultFb, fresh.questions);
const applied = applyHyExamResultFeedback(state, fresh.questions, resultFb);
const scoreTxt = resultFb.score ? ` ${resultFb.score}\u5206` : "";
const triedSummary = fresh.questions
.map((q) => {
const letters = [...(state.triedLetters[q.qid] || [])].join("") || "-";
return `${hyExamOptionLetter(q, q.options.find((o) => o.id === state.byQid[q.qid])) || "?"}[\u8bd5${letters}]`;
})
.join(" ");
pushLog(
`[${tag}] \u7ed3\u679c\u9875${scoreTxt}\uff1a\u4fdd\u7559 ${applied.locked} \u9898\u6b63\u786e\uff0c\u8c03\u6574 ${applied.adjusted} \u9898 (${t + 1}/${HY_EXAM_MAX_TRIES})`
);
pushLog(`[${tag}] \u5df2\u8bd5\u9009\u9879 ${triedSummary}`);
pushLog(`[${tag}] \u7b49\u5f85 ${HY_EXAM_RETRY_GAP_MS / 1000} \u79d2\u540e\u91cd\u8bd5…`);
if (!(await hyExamRetryWait(HY_EXAM_RETRY_GAP_MS, tag))) {
pushLog(`[${tag}] \u5df2\u6682\u505c\uff0c\u505c\u6b62\u8003\u8bd5`);
return { ok: false, msg: "\u5df2\u6682\u505c", paused: true };
}
continue;
}
const wrongQids = parseHyExamWrongQids(post.text, fresh.questions);
if (wrongQids.length) {
wrongQids.forEach((qid) => {
const pick = picks.find((p) => p.qid === qid);
if (pick) ensureHyExamTried(state, qid).add(pick.optionId);
delete state.byQid[qid];
});
pushLog(`[${tag}] \u8003\u8bd5\u7ea0\u9519\uff1a\u8c03\u6574 ${wrongQids.length} \u9898 (${t + 1}/${HY_EXAM_MAX_TRIES})`);
} else if (!/exam_result/i.test(post.url || "")) {
const q = fresh.questions[state.rotateIdx % fresh.questions.length];
state.rotateIdx++;
const pick = picks.find((p) => p.qid === q.qid);
if (pick) ensureHyExamTried(state, q.qid).add(pick.optionId);
delete state.byQid[q.qid];
pushLog(
`[${tag}] \u65e0\u7ed3\u679c\u53cd\u9988\uff0c\u8bd5\u9519\u7b2c ${((state.rotateIdx - 1) % fresh.questions.length) + 1} \u9898 (${t + 1}/${HY_EXAM_MAX_TRIES})`
);
} else {
pushLog(`[${tag}] \u7ed3\u679c\u9875\u672a\u8bc6\u522b\u5bf9\u9519\uff0c\u8bf7\u624b\u52a8\u67e5\u770b`);
return { ok: false, msg: "\u7ed3\u679c\u9875\u89e3\u6790\u5931\u8d25" };
}
if (!getEnabled()) {
pushLog(`[${tag}] \u5df2\u6682\u505c\uff0c\u505c\u6b62\u8003\u8bd5`);
return { ok: false, msg: "\u5df2\u6682\u505c", paused: true };
}
pushLog(`[${tag}] \u7b49\u5f85 ${HY_EXAM_RETRY_GAP_MS / 1000} \u79d2\u540e\u91cd\u8bd5…`);
if (!(await hyExamRetryWait(HY_EXAM_RETRY_GAP_MS, tag))) {
pushLog(`[${tag}] \u5df2\u6682\u505c\uff0c\u505c\u6b62\u8003\u8bd5`);
return { ok: false, msg: "\u5df2\u6682\u505c", paused: true };
}
}
return { ok: false, msg: "\u66b4\u529b\u7b54\u9898\u5931\u8d25" };
}
function findJobCtxByCwid(cwid) {
if (!cwid) return null;
const target = String(cwid).toUpperCase();
const jobs = loadJobs();
const hit = Object.values(jobs).find(
(j) => j && (String(j.cwid || "").toUpperCase() === target || String(j.cwrid || "").toUpperCase() === target)
);
return hit ? jobToCtx(hit) : null;
}
async function afterVideoMaybeExam(ctx, label) {
if (skipExamsSession) {
return { ok: false, skipAll: true, msg: "\u5df2\u8df3\u8fc7\u8003\u8bd5\uff0c\u4ec5\u5b66\u4e60\u89c6\u9891" };
}
const examId = resolveExamCwid(ctx);
const altIds = [ctx && ctx.wareCwid, ctx && ctx.cwid].filter(
(id, i, arr) => id && id !== examId && arr.indexOf(id) === i
);
if (!examId) return { ok: false, msg: "\u7f3a\u5c11\u8003\u8bd5 cwid" };
const res = await runHyClientExam(examId, label || ctx.title, altIds, ctx && ctx.cwrid, ctx);
if (res.skipAll) {
skipAllPendingExams(`[${label || ctx.title || examId.slice(0, 8)}] \u5df2\u9009\u62e9\u6682\u4e0d\u8003\u8bd5\uff0c\u672c\u6b21\u8fd0\u884c\u8df3\u8fc7\u6240\u6709\u8003\u8bd5`);
return res;
}
if (res.ok) {
markPanelChapterExamDone(ctx);
panelState.lastCourseListKey = "";
renderCourseList();
renderChapterPreview();
updatePanel();
maybeAutoStopWhenFinished();
} else if (res.needNavigate && pageType() !== "exam") {
navigateToExamPage(ctx, label || ctx.title);
}
return res;
}
async function handleExamResultPage() {
const cwid = new URLSearchParams(location.search).get("cwid");
if (!cwid) return;
const html = document.documentElement ? document.documentElement.outerHTML : "";
const fb = parseHyExamResultPage(html, location.href);
if (fb && (fb.correct.length || fb.wrong.length)) {
saveHyExamLetterCache(cwid, fb, parseHyExamQuestions(html));
}
if (fb && fb.failed) {
return;
}
if (!isHyExamPassHtml(html, location.href)) return;
let ctx = findJobCtxByCwid(cwid) || findPanelChapterCtxByCwid(cwid);
if (ctx) {
markPanelChapterExamDone(ctx);
panelState.lastCourseListKey = "";
renderCourseList();
renderChapterPreview();
updatePanel();
pushLog(`[${ctx.title || cwid.slice(0, 8)}] \u8003\u8bd5\u7ed3\u679c\u9875\uff1a\u5df2\u901a\u8fc7`);
return;
}
await enrichPanelCoursesProgress(getQueue(), true);
ctx = findPanelChapterCtxByCwid(cwid);
if (ctx) {
markPanelChapterExamDone(ctx);
panelState.lastCourseListKey = "";
renderCourseList();
renderChapterPreview();
updatePanel();
pushLog(`[${ctx.title || cwid.slice(0, 8)}] \u8003\u8bd5\u7ed3\u679c\u9875\uff1a\u5df2\u901a\u8fc7\uff08\u5df2\u540c\u6b65\u6536\u85cf\u8fdb\u5ea6\uff09`);
} else {
pushLog(`\u8003\u8bd5\u7ed3\u679c\u9875\uff1a\u5df2\u901a\u8fc7\uff08${cwid.slice(0, 8)}\uff0c\u8bf7\u70b9\u9762\u677f\u300c\u5237\u65b0\u300d\u540c\u6b65\u8fdb\u5ea6\uff09`);
}
}
function updateJobsPanel() {
updatePanel();
}
function refreshPanelActions() {
updatePanel();
}
function parseCourseChapters() {
return Array.from(document.querySelectorAll(".course[data-href]"))
.filter((el) => !isElectiveChapter(el))
.filter((el) => isHdInteractiveEnabled() || !chapterHasInteractiveTag(el))
.map((el) => {
const href = el.getAttribute("data-href") || "";
const { label, progressText, studyStateAttr } = getChapterStatusParts(el);
const interactive = isInteractiveChapter(el);
const st = inferHyChapterStatuses(label, progressText, studyStateAttr, interactive);
return {
href,
done: st.videoDone,
examDone: st.examDone,
untouched: st.untouched,
el,
};
});
}
function coursePageUrl(cid) {
return `${location.origin}/pages/course.aspx?cid=${encodeURIComponent(cid)}`;
}
function normalizeQueueCid(item) {
if (!item) return "";
const s = String(item).trim();
const m = s.match(/cid=([^&]+)/i);
const cid = m ? m[1] : s;
return cid ? cid.toLowerCase() : "";
}
async function hyFetchHtml(url, postBody) {
const opts = { credentials: "include", headers: { Accept: "text/html,application/xhtml+xml" } };
if (postBody) {
opts.method = "POST";
opts.headers["Content-Type"] = "application/x-www-form-urlencoded";
opts.body = postBody;
}
const r = await fetch(url, opts);
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.text();
}
function parseAspNetHidden(html) {
const pick = (name) => {
const m1 = html.match(new RegExp(`name="${name}"[^>]*value="([^"]*)"`, "i"));
if (m1) return m1[1];
const m2 = html.match(new RegExp(`id="${name}"[^>]*value="([^"]*)"`, "i"));
return m2 ? m2[1] : "";
};
const hidden = {
__VIEWSTATE: pick("__VIEWSTATE"),
__VIEWSTATEGENERATOR: pick("__VIEWSTATEGENERATOR"),
__EVENTVALIDATION: pick("__EVENTVALIDATION"),
};
const re = /]+type=["']hidden["'][^>]*>/gi;
let m;
while ((m = re.exec(html))) {
const tag = m[0];
const nm = tag.match(/name=["']([^"']+)["']/i);
const val = tag.match(/value=["']([^"']*)["']/i);
if (!nm || !nm[1]) continue;
const name = nm[1];
if (name === "hd_result") continue;
if (/^(radio_|btn_)/i.test(name)) continue;
if (hidden[name] == null) hidden[name] = val ? val[1] : "";
}
return hidden;
}
function isFallbackCourseTitle(title, cid) {
const t = String(title || "").trim();
if (!t) return true;
if (t.toLowerCase() === String(cid || "").slice(0, 8).toLowerCase()) return true;
return /^[0-9a-f]{8}$/i.test(t);
}
function parseCollectCourseItem(li) {
const boxA = li.querySelector(".sub_box a[href*='course.aspx?cid=']");
const centA = li.querySelector(".sub_cent a[href*='course.aspx?cid=']");
const a = boxA || centA || li.querySelector("a[href*='course.aspx?cid=']");
if (!a) return null;
const href = a.getAttribute("href") || "";
const m = href.match(/cid=([^&'"]+)/i);
if (!m) return null;
const cid = normalizeQueueCid(m[1]);
const title = (li.querySelector(".sub_tit")?.textContent || "").replace(/\s+/g, " ").trim();
const credits = (li.querySelector(".sub_text")?.textContent || "").replace(/\s+/g, " ").trim();
return {
cid,
title: title || "",
credits,
url: href.startsWith("http") ? href : new URL(href, location.origin).href,
};
}
function parseCollectCoursesFromHtml(html) {
const doc = new DOMParser().parseFromString(html, "text/html");
const seen = new Set();
const list = [];
doc.querySelectorAll("ul.sub_ul > li").forEach((li) => {
const item = parseCollectCourseItem(li);
if (!item || seen.has(item.cid)) return;
seen.add(item.cid);
list.push(item);
});
return list;
}
function parseCollectMaxPage(html) {
const pages = new Set([1]);
const normalized = String(html || "")
.replace(/'/g, "'")
.replace(/"/g, '"')
.replace(/&/g, "&");
const postRe = /__doPostBack\s*\(\s*['"]AspNetPager['"]\s*,\s*['"](\d+)['"]\s*\)/gi;
let m;
while ((m = postRe.exec(normalized))) {
const n = parseInt(m[1], 10);
if (n > 0) pages.add(n);
}
const pagerBlock = normalized.match(/id=["']AspNetPager["'][\s\S]*?<\/div>\s*<\/div>/i);
if (pagerBlock) {
const numRe = />(\d+) 0 && n < 50) pages.add(n);
}
}
return Math.max(...pages);
}
async function fetchCollectPageHtml(pageNum, form) {
const url = `${location.origin}/PersonalCenter/course_collect.aspx`;
if (pageNum <= 1 && !form) {
const html = await hyFetchHtml(url);
return { html, hidden: parseAspNetHidden(html) };
}
const hidden = form || {};
const body = new URLSearchParams({
__EVENTTARGET: "AspNetPager",
__EVENTARGUMENT: String(pageNum),
__VIEWSTATE: hidden.__VIEWSTATE || "",
__VIEWSTATEGENERATOR: hidden.__VIEWSTATEGENERATOR || "",
__EVENTVALIDATION: hidden.__EVENTVALIDATION || "",
hd_coa_id: "",
});
const html = await hyFetchHtml(url, body.toString());
return { html, hidden: parseAspNetHidden(html) };
}
async function fetchAllCollectCourses() {
const all = [];
const seen = new Set();
let hidden = null;
let maxPage = 1;
const first = await fetchCollectPageHtml(1, null);
const issue = detectCollectPageIssue(first.html);
if (issue?.login) {
panelState.collectLoginLost = true;
return [];
}
panelState.collectLoginLost = false;
hidden = first.hidden;
maxPage = Math.max(1, parseCollectMaxPage(first.html));
parseCollectCoursesFromHtml(first.html).forEach((c) => {
if (!seen.has(c.cid)) {
seen.add(c.cid);
all.push(c);
}
});
for (let page = 2; page <= maxPage; page++) {
const res = await fetchCollectPageHtml(page, hidden);
hidden = res.hidden;
maxPage = Math.max(maxPage, parseCollectMaxPage(res.html));
parseCollectCoursesFromHtml(res.html).forEach((c) => {
if (!seen.has(c.cid)) {
seen.add(c.cid);
all.push(c);
}
});
if (page < maxPage) await sleep(320);
}
panelState.collectPages = maxPage;
return all;
}
function blankPanelCourse(raw) {
const cid = normalizeQueueCid(raw.cid);
const title = raw.title && !isFallbackCourseTitle(raw.title, cid) ? raw.title : "";
return {
cid,
title: title || "\u52a0\u8f7d\u4e2d…",
credits: raw.credits || "",
url: raw.url || coursePageUrl(raw.cid),
chapterTotal: 0,
studyDone: 0,
examDone: 0,
gongxu: false,
untouchedCount: 0,
chaptersLoaded: false,
chapters: [],
progressError: "",
titleLoaded: !!title,
};
}
function isCourseChaptersReady(course) {
return !!(course && course.chaptersLoaded && (course.chapterTotal || 0) > 0 && !course.progressError);
}
function getCourseProgressLine(course) {
if (!course) return "—";
if (course.progressError) return course.progressError;
if (!course.chaptersLoaded) return "\u7ae0\u8282\u52a0\u8f7d\u4e2d…";
const total = Number(course.chapterTotal) || 0;
if (total <= 0) return "\u7ae0\u8282\u672a\u52a0\u8f7d";
const studyDone = Number(course.studyDone) || 0;
const examDone = Number(course.examDone) || 0;
if (course.gongxu) {
if (studyDone < total) return `\u89c6\u9891 ${studyDone}/${total} · \u8bfe\u7ea7\u8003\u6838\u5f85\u5b66\u5b8c`;
return `\u89c6\u9891 ${studyDone}/${total} · \u8bfe\u7ea7\u8003\u6838\u5f85\u7533\u8bf7\u8bc1\u4e66`;
}
return `\u89c6\u9891 ${studyDone}/${total} · \u8003\u8bd5 ${examDone}/${total}`;
}
function detectChapterLoadIssue(html, parsed) {
if (parsed.uid && parsed.chapters.length > 0) return null;
if (parsed.uid && !parsed.chapters.length) {
if (/class=["']course["'][^>]*data-href|\u8bfe\u7a0b\u5b66\u4e60\u4e0e\u8003\u8bd5|exam_tit/i.test(html)) {
return { error: "\u672a\u8bc6\u522b\u5230\u5fc5\u4fee\u7ae0\u8282\uff08\u53ef\u80fd\u5747\u4e3a\u9009\u4fee\u6216\u9875\u9762\u7ed3\u6784\u53d8\u5316\uff09" };
}
return { error: "\u7ae0\u8282\u672a\u52a0\u8f7d\uff0c\u8bf7 F5 \u540e\u5237\u65b0\u6536\u85cf" };
}
if (isPageLoginRedirect(html)) {
return { error: "\u767b\u5f55\u5931\u6548\uff0c\u8bf7 F5 \u5237\u65b0", login: true };
}
return { error: "\u7ae0\u8282\u9875\u89e3\u6790\u5931\u8d25\uff0c\u8bf7 F5 \u5237\u65b0" };
}
function applyCourseChapterParse(course, parsed, html) {
const issue = detectChapterLoadIssue(html, parsed);
if (issue) {
course.chapters = [];
course.chapterTotal = 0;
course.studyDone = 0;
course.examDone = 0;
course.untouchedCount = 0;
course.chaptersLoaded = false;
course.progressError = issue.error;
if (issue.login) warnLoginIfNeeded(false);
return false;
}
course.chapters = parsed.chapters;
course.chapterTotal = parsed.chapters.length;
course.gongxu = !!parsed.gongxu;
course.studyDone = parsed.chapters.filter((ch) => ch.done).length;
course.examDone = parsed.chapters.filter((ch) => ch.examDone).length;
course.untouchedCount = parsed.chapters.filter((ch) => ch.untouched && !ch.done).length;
if (parsed.courseTitle) {
course.title = parsed.courseTitle;
course.titleLoaded = true;
} else if (isFallbackCourseTitle(course.title, course.cid)) {
course.title = "\u672a\u547d\u540d\u8bfe\u7a0b";
}
course.chaptersLoaded = true;
course.progressError = "";
return true;
}
async function enrichPanelCourse(course, force) {
if (!course) return course;
if (!force && isCourseChaptersReady(course)) return course;
try {
const html = await hyFetchHtml(course.url || coursePageUrl(course.cid));
const parsed = parseCourseChaptersFromHtml(html, course.cid);
if (!applyCourseChapterParse(course, parsed, html)) {
pushLog(`\u7ae0\u8282\u52a0\u8f7d\u5931\u8d25\uff1a${course.title || course.cid}\uff08${course.progressError}\uff09`);
} else if (course.chapters && course.chapters.length) {
syncInteractiveJobsFromChapters(course.chapters);
syncJobTitlesFromPanelCourses();
}
} catch (err) {
course.chapters = [];
course.chapterTotal = 0;
course.studyDone = 0;
course.examDone = 0;
course.untouchedCount = 0;
course.chaptersLoaded = false;
course.progressError = "\u7ae0\u8282\u52a0\u8f7d\u5931\u8d25\uff0c\u8bf7 F5 \u5237\u65b0";
pushLog(`\u7ae0\u8282\u52a0\u8f7d\u5931\u8d25\uff1a${course.title || course.cid}`);
}
return course;
}
async function enrichPanelCoursesProgress(cids, force) {
if (panelState.loginRequired) return;
const ids = (cids && cids.length ? cids : panelState.courses.map((c) => c.cid)).filter((cid) =>
panelState.courses.some((c) => c.cid === cid)
);
const targets = ids.map((cid) => panelState.courses.find((c) => c.cid === cid)).filter(Boolean);
for (const course of targets) {
await enrichPanelCourse(course, !!force);
await sleep(120);
}
}
async function fetchPanelCourses() {
panelState.refreshing = true;
updatePanel();
try {
let raw = [];
if (pageType() === "collect") {
raw = parseCollectCourses();
}
const remote = await fetchAllCollectCourses();
const map = new Map();
raw.forEach((c) => map.set(c.cid, c));
remote.forEach((c) => {
const prev = map.get(c.cid);
const title =
c.title && !isFallbackCourseTitle(c.title, c.cid)
? c.title
: prev?.title && !isFallbackCourseTitle(prev.title, c.cid)
? prev.title
: c.title || prev?.title || "";
map.set(c.cid, { ...(prev || {}), ...c, title });
});
panelState.courses = Array.from(map.values()).map(blankPanelCourse);
panelState.courses.sort((a, b) => a.title.localeCompare(b.title, "zh-CN"));
if (panelState.collectLoginLost) {
panelState.courses = [];
panelState.loginRequired = true;
clearStalePanelSelection();
pushLog("\u672a\u767b\u5f55\uff0c\u5df2\u6e05\u7a7a\u672c\u5730\u8bfe\u7a0b\u52fe\u9009\u7f13\u5b58");
return panelState.courses;
}
panelState.loginRequired = false;
initDefaultQueueIfNeeded();
return panelState.courses;
} catch (err) {
panelState.courses = [];
pushLog("\u52a0\u8f7d\u6536\u85cf\u5931\u8d25\uff1a" + (err.message || err));
return [];
} finally {
panelState.refreshing = false;
updatePanel();
}
}
async function refreshPanelCourses() {
detectAccountSwitch();
panelState.refreshing = true;
updatePanel();
try {
await fetchPanelCourses();
localStorage.removeItem(QUEUE_TOUCHED_KEY);
setQueue(panelState.courses.map((c) => c.cid));
pushLog(`\u5df2\u5237\u65b0\u6536\u85cf ${panelState.courses.length} \u95e8\uff08${panelState.collectPages || 1} \u9875\uff09\uff0c\u9ed8\u8ba4\u5168\u9009`);
renderCourseList();
await enrichPanelCoursesProgress(getQueue(), true);
await refreshChapterPreview();
} catch (err) {
pushLog("\u5237\u65b0\u8bfe\u7a0b\u5931\u8d25\uff1a" + (err.message || err));
} finally {
panelState.refreshing = false;
updatePanel();
}
}
async function refreshChapterPreview() {
if (panelState.loginRequired) {
panelState.chapterPreview = [];
renderChapterPreview();
return;
}
const selected = pruneQueueToCourses();
if (!selected.length) {
panelState.chapterPreview = [];
renderChapterPreview();
return;
}
const list = [];
for (const cid of selected) {
const course = panelState.courses.find((c) => c.cid === cid);
if (!course) continue;
if (!isCourseChaptersReady(course)) await enrichPanelCourse(course);
list.push({
cid,
title: course.title || cid,
chapters: course.chapters || [],
progressError: course.progressError || "",
chaptersLoaded: !!course.chaptersLoaded,
});
}
panelState.chapterPreview = list;
renderChapterPreview();
}
async function initPanelData() {
detectAccountSwitch();
cloudState.cloudToken = String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim();
void _cf();
try {
await fetchPanelCourses();
if (getEnabled()) ensureStudyQueueSelection();
if (panelState.courses.length) {
const pages = panelState.collectPages || 1;
pushLog(`\u5df2\u52a0\u8f7d\u6536\u85cf ${panelState.courses.length} \u95e8\uff08${pages} \u9875\uff09`);
}
renderCourseList();
void enrichPanelCoursesProgress(getQueue()).then(() => {
renderCourseList();
refreshChapterPreview();
});
} catch (_) {}
}
function isCourseUnfinished(course) {
if (!isCourseChaptersReady(course)) return true;
const total = course.chapterTotal || 0;
if (course.gongxu) return (course.studyDone || 0) < total;
return (course.studyDone || 0) < total || (course.examDone || 0) < total;
}
function notifyGongxuCourseExamIfNeeded() {
const selected = pruneQueueToCourses();
selected.forEach((cid) => {
const course = findPanelCourseByCid(cid);
if (!course || !isGongxuCourse(course) || !isCourseChaptersReady(course)) return;
const total = Number(course.chapterTotal) || 0;
if (total <= 0 || (Number(course.studyDone) || 0) < total) return;
const key = `hy_gongxu_tip_${cid}`;
try {
if (sessionStorage.getItem(key)) return;
sessionStorage.setItem(key, "1");
} catch (_) {}
pushUserLog(
`\u300c${course.title || "\u516c\u9700\u8bfe"}\u300d\u5168\u90e8\u89c6\u9891\u5df2\u5b66\u5b8c\uff0c\u8bf7\u5230\u8bfe\u7a0b\u9875\u70b9\u51fb\u300c\u7533\u8bf7\u8bc1\u4e66\u300d\u5b8c\u6210\u8bfe\u7ea7\u8003\u6838\uff08≥60\u5206\uff09`
);
});
}
function parseCollectCourses() {
const seen = new Set();
const list = [];
document.querySelectorAll("ul.sub_ul > li").forEach((li) => {
const item = parseCollectCourseItem(li);
if (!item || seen.has(item.cid)) return;
seen.add(item.cid);
list.push(item);
});
return list;
}
function getQueue() {
try {
return JSON.parse(localStorage.getItem(QUEUE_KEY) || "[]")
.map(normalizeQueueCid)
.filter(Boolean);
} catch (_) {
return [];
}
}
function setQueue(list) {
localStorage.setItem(QUEUE_KEY, JSON.stringify((list || []).map(normalizeQueueCid).filter(Boolean)));
}
async function runCoursePage() {
updateJobsPanel();
if (!getEnabled() || !getAutoNext() || getApiMode()) return;
await sleep(1500);
const chapters = parseCourseChapters();
const next = chapters.find((c) => c.untouched || !c.done);
if (!next) {
pushLog("\u672c\u8bfe\u7a0b\u5168\u90e8\u8bfe\u4ef6\u5df2\u5b66\u4e60");
advanceQueue();
return;
}
const title = (next.el.querySelector("h3") || next.el).textContent.replace(/\s+/g, " ").trim();
pushLog("\u6253\u5f00\u4e0b\u4e00\u8bfe\u4ef6\uff1a" + title.slice(0, 40));
const url = next.href.startsWith("http") ? next.href : new URL(next.href, location.origin).href;
window.open(url, "new_courseWare");
}
function advanceQueue() {
const queue = getQueue();
if (queue.length <= 1) {
setQueue([]);
pushLog("\u961f\u5217\u5df2\u5168\u90e8\u5b8c\u6210");
return;
}
const rest = queue.slice(1);
setQueue(rest);
if (getApiMode()) {
pushLog("\u6a21\u5f0f\uff1a\u4e0d\u4e0b\u94bb\u6253\u5f00\u8bfe\u7a0b\u9875");
return;
}
location.href = coursePageUrl(rest[0]);
}
async function runCollectPage() {
if (!panelState.courses.length) {
try {
await fetchPanelCourses();
} catch (_) {}
}
renderCourseList();
}
async function runWareRedirect() {
if (!getEnabled()) return;
const pathLower = location.pathname.toLowerCase();
if (pathLower.includes("course_ware_hd.aspx")) return;
const cwid = new URLSearchParams(location.search).get("cwid");
if (!cwid) return;
const jobs = loadJobs();
const interactiveJob = Object.values(jobs).some(
(j) => j && j.interactive && j.cwid === cwid && j.phase !== "done"
);
if (interactiveJob) return;
await sleep(800);
if (pageType() !== "ware") return;
const target = location.pathname.replace("course_ware.aspx", "course_ware_polyv.aspx") + location.search;
if (!location.pathname.includes("polyv")) {
location.replace(target + (target.includes("?") ? "&" : "?") + "ff=0&ft=0&t=0");
}
}
function syncPanelDebugLayout() {
const panel = document.getElementById("hy-cme-auto-panel");
if (!panel) return;
panel.classList.toggle("hy-api-debug", getApiMode());
}
function injectHyPanelStyles() {
const id = "hy-cme-panel-style-v9";
document.getElementById("hy-cme-panel-style-v4")?.remove();
document.getElementById("hy-cme-panel-style-v5")?.remove();
document.getElementById("hy-cme-panel-style-v6")?.remove();
document.getElementById("hy-cme-panel-style-v7")?.remove();
document.getElementById("hy-cme-panel-style-v8")?.remove();
if (document.getElementById(id) || !document.head) return;
const st = document.createElement("style");
st.id = id;
st.textContent = `
#hy-cme-auto-panel{position:fixed;right:20px;top:80px;z-index:2147483647;width:412px;max-width:calc(100vw - 24px);display:flex;flex-direction:column;background:#f4f7fb;border:1px solid #dbe4f0;border-radius:16px;box-shadow:0 16px 40px rgba(15,23,42,.17);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;font-size:12px;color:#0f172a;overflow:hidden;transition:max-height .22s ease,box-shadow .22s ease;box-sizing:border-box;}
#hy-cme-auto-panel *,#hy-cme-auto-panel *::before,#hy-cme-auto-panel *::after{box-sizing:border-box;}
#hy-cme-auto-panel.cme-panel-max{max-height:min(92vh,780px);}
#hy-cme-auto-panel.cme-panel-min{max-height:none;box-shadow:0 10px 28px rgba(15,23,42,.14);}
#hy-cme-auto-panel.cme-panel-min #hy-cme-panel-header{border-bottom:none;}
#hy-cme-auto-panel.cme-panel-min #hy-cme-panel-body,#hy-cme-auto-panel.cme-panel-min #hy-cme-panel-actions,#hy-cme-auto-panel.cme-panel-min #hy-cme-panel-footer,#hy-cme-auto-panel.cme-panel-min .cme-footer-extra{display:none !important;}
#hy-cme-panel-header{flex:0 0 auto;padding:8px 11px;background:linear-gradient(180deg,#f8ecd5,#f4e8cf);border-bottom:1px solid #e5dbc6;display:flex;justify-content:space-between;align-items:center;cursor:move;user-select:none;}
#hy-cme-panel-body{flex:1 1 auto;min-height:0;overflow:hidden;padding:8px 8px 4px;display:flex;flex-direction:column;gap:6px;}
#hy-cme-panel-actions{flex:0 0 auto;margin:0 8px 6px;}
.cme-footer-extra{flex:0 0 auto;}
#hy-cme-panel-footer{flex:0 0 auto;padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;}
#hy-cme-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;}
#hy-cme-panel-logo{width:30px;height:30px;border-radius:9px;object-fit:cover;box-shadow:0 2px 9px rgba(15,23,42,.11);border:1px solid rgba(148,163,184,.45);background:#fff;flex:0 0 auto;}
#hy-cme-panel-title{font-size:13px;font-weight:900;color:#9a3412;line-height:1.26;display:flex;align-items:flex-start;gap:5px;flex-wrap:wrap;}
.cme-panel-title-text{flex:1 1 12em;min-width:0;letter-spacing:-0.01em;}
#hy-cme-panel-sub{display:flex;flex-wrap:wrap;align-items:center;gap:3px 5px;margin-top:3px;line-height:1.32;}
.cme-sub-chip{font-size:11px;color:#7c2d12;background:rgba(255,255,255,.62);padding:2px 7px;border-radius:999px;border:1px solid rgba(180,83,9,.18);font-weight:700;}
.cme-sub-chip-em{color:#0f766e;background:rgba(236,253,245,.9);border-color:rgba(15,118,110,.28);}
.cme-sub-dot{color:#d6d3d1;font-size:10px;font-weight:400;}
.cme-panel-version{font-size:11px;font-weight:900;color:#64748b;padding:2px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0;}
#hy-cme-panel-controls{display:flex;align-items:center;gap:5px;flex:0 0 auto;}
.cme-panel-ctl{border:none;background:#fff;color:#64748b;width:28px;height:28px;border-radius:999px;cursor:pointer;box-shadow:0 1px 2px rgba(15,23,42,.1);font-size:15px;line-height:1;font-weight:900;padding:0;}
.cme-ctl-min{width:10px;height:2px;background:#64748b;border-radius:1px;display:block;margin:0 auto;}
.cme-ctl-plus{font-size:16px;line-height:1;font-weight:700;color:#64748b;}
.cme-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:0;}
.cme-card-status{padding:6px 8px;flex:0 0 auto;}
.cme-card-tabs{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow:hidden;}
.cme-card-tabs .cme-tabbar{flex:0 0 auto;}
.cme-card-tabs .cme-pane.active{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow:hidden;}
.cme-card-tabs .cme-list-head{flex:0 0 auto;}
.cme-card-tabs #hy-cme-select-all-bar{flex:0 0 auto;}
.cme-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;line-height:1.28;}
.cme-status-label{font-size:11px;color:#64748b;font-weight:700;}
#hy-cme-auto-status{padding:2px 7px;border-radius:999px;font-weight:800;font-size:11px;background:#fff;border:1px solid #cbd5e1;}
.cme-status-main{margin-top:3px;}
.cme-status-metrics{display:flex;align-items:center;gap:5px;font-size:12px;color:#475569;min-width:0;flex:1;}
.cme-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;}
.cme-metric-div{color:#cbd5e1;}
.cme-card-status .cme-progress-pct{font-size:14px;font-weight:900;color:#0369a1;flex:0 0 auto;}
.cme-progress-bar{height:5px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:4px;}
.cme-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s ease;}
.cme-tabbar{display:flex;gap:6px;margin-bottom:7px;}
.cme-tab-btn{flex:1;border:1px solid #cbd5e1;background:#f8fafc;color:#475569;padding:4px 4px;border-radius:9px;cursor:pointer;font-weight:700;font-size:11px;}
.cme-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;}
#hy-cme-auto-panel .cme-pane{display:none;}
#hy-cme-auto-panel .cme-card-tabs .cme-pane.active{display:flex;}
.cme-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;}
.cme-list-title{font-size:12px;color:#64748b;font-weight:700;}
.cme-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 7px;}
.cme-list-head-actions{display:flex;align-items:center;gap:5px;}
.cme-log-clear-btn{border:1px solid #cbd5e1;background:#fff;color:#64748b;padding:1px 7px;border-radius:999px;font-size:10px;font-weight:700;cursor:pointer;line-height:1.4;}
.cme-log-clear-btn:hover{background:#f8fafc;color:#0f172a;}
#hy-cme-select-all-bar{margin-bottom:6px;}
.hy-course-select-all{display:flex;align-items:center;gap:7px;padding:2px 4px 6px;font-size:12px;font-weight:700;color:#475569;cursor:pointer;user-select:none;}
.hy-course-selected-count{margin-left:auto;font-size:11px;color:#64748b;font-weight:600;}
.hy-cme-cb-wrap{position:relative;display:inline-flex;flex:0 0 16px;width:16px;height:16px;margin-top:2px;}
.hy-cme-cb-wrap .hy-cme-course-cb{position:absolute;inset:0;width:16px;height:16px;margin:0;opacity:0;cursor:pointer;z-index:2;}
.hy-cme-cb-box{width:16px;height:16px;border:2px solid #94a3b8;border-radius:4px;background:#fff;display:flex;align-items:center;justify-content:center;pointer-events:none;box-sizing:border-box;}
.hy-cme-cb-wrap .hy-cme-course-cb:checked+.hy-cme-cb-box{background:#2563eb;border-color:#2563eb;}
.hy-cme-cb-wrap .hy-cme-course-cb:checked+.hy-cme-cb-box::after{content:"✓";color:#fff;font-size:11px;font-weight:900;line-height:1;}
.hy-cme-cb-wrap .hy-cme-course-cb:indeterminate+.hy-cme-cb-box{background:#2563eb;border-color:#2563eb;}
.hy-cme-cb-wrap .hy-cme-course-cb:indeterminate+.hy-cme-cb-box::after{content:"−";color:#fff;font-size:13px;font-weight:900;line-height:1;}
#hy-cme-auto-panel .cme-settings-check input[type=checkbox]{all:revert!important;appearance:checkbox!important;-webkit-appearance:checkbox!important;width:14px!important;height:14px!important;min-width:14px!important;opacity:1!important;visibility:visible!important;display:inline-block!important;position:static!important;margin:0!important;cursor:pointer!important;accent-color:#2563eb;}
#hy-cme-course-list,#hy-cme-chapter-preview,#hy-cme-run-log{flex:1 1 auto;min-height:80px;max-height:none;overflow-y:auto;overflow-x:hidden;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;}
#hy-cme-auto-panel.hy-api-debug #hy-cme-run-log{max-height:min(280px,36vh);}
#hy-cme-course-list label{max-width:100%;}
#hy-cme-course-list label span{display:block;max-width:100%;overflow-wrap:anywhere;word-break:break-word;}
.cme-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:900;}
.cme-chapter-course{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;}
.cme-chapter-title{padding:6px 9px;background:#eaf1ff;border-bottom:1px solid #dbe4f0;color:#1d4ed8;font-size:12px;font-weight:700;}
.cme-chapter-item{padding:5px 9px;font-size:12px;display:flex;justify-content:space-between;gap:7px;}
.cme-dot{font-weight:700;}
.cme-log-row{padding:4px 6px;border-bottom:1px dashed #d4deea;font-size:12px;line-height:1.42;}
.cme-settings-group{background:#f8fafc;border:1px solid #dbe4f0;border-radius:12px;padding:8px;margin-bottom:8px;}
.cme-settings-title{font-size:12px;color:#64748b;font-weight:900;margin-bottom:7px;}
.cme-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:12px;margin-bottom:5px;align-items:center;}
.cme-meta-label{color:#64748b;}.cme-meta-value{font-weight:700;text-align:right;}
.cme-card-current{padding:5px 8px;margin-bottom:6px;}
.cme-card-current .cme-meta-row{font-size:11px;margin-bottom:2px;}
.cme-card-current .cme-meta-value{word-break:break-all;line-height:1.28;}
.cme-card-actions{padding:6px 8px;margin-bottom:0;width:100%;}
#hy-cme-auto-panel .cme-btn-row{display:flex !important;flex-direction:row !important;align-items:stretch;gap:7px;margin-bottom:0;width:100%;}
.cme-btn-row-nowrap{flex-wrap:nowrap !important;}
#hy-cme-auto-panel button.cme-btn::before,#hy-cme-auto-panel button.cme-btn::after{content:none !important;display:none !important;}
#hy-cme-auto-panel .cme-btn{position:static !important;display:inline-flex !important;align-items:center;justify-content:center;border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;min-width:0 !important;flex:1 1 0 !important;width:auto !important;height:auto !important;float:none !important;}
#hy-cme-auto-panel .cme-btn-ico{margin-right:4px;font-size:12px;line-height:1;}
.cme-log-status{display:block;font-size:11px;line-height:1.42;color:#334155;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:6px 8px;margin-bottom:5px;word-break:break-word;}
.cme-study-mode-hint-collapsed .cme-study-mode-hint-body{display:none;}
.cme-study-mode-hint-collapsed{padding:5px 9px;}
.cme-study-mode-hint-title{cursor:pointer;}
.cme-btn-start{background:#16a34a;}.cme-btn-stop{background:#ef4444;}.cme-btn-clear{background:#d97706;}
.cme-btn-start.cme-btn-off,.cme-btn-start:disabled{background:#cbd5e1 !important;color:#64748b !important;cursor:not-allowed;pointer-events:none;}
.cme-btn-stop.cme-btn-off,.cme-btn-stop:disabled{background:#e2e8f0 !important;color:#94a3b8 !important;cursor:not-allowed;pointer-events:none;}
.cme-btn-action{min-width:0 !important;flex:1 1 0 !important;}
.cme-btn-ico{font-size:13px;margin-right:5px;}
.cme-plan-select,.cme-settings-select{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:6px 9px;font-size:12px;background:#fff;color:#0f172a;}
.cme-settings-check{display:flex;align-items:center;gap:6px;margin:5px 0;font-size:12px;}
.cme-exam-hint{font-size:11px;color:#64748b;margin-top:4px;line-height:1.42;}
.cme-footer-extra{padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;opacity:.95;}
.cme-ann-title{display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:900;color:#9a3412;margin-bottom:3px;}
.cme-ann-text{display:block;word-break:break-word;}
.cme-qq-row{display:flex;align-items:center;justify-content:space-between;gap:9px;margin-top:5px;}
.cme-qq-text{font-size:12px;color:#475569;}
.cme-qq-title{font-size:13px;font-weight:800;color:#0f172a;}
.cme-qq-btn{border:none;background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:5px 12px;border-radius:999px;font-size:12px;font-weight:800;cursor:pointer;}
.cme-start-hint{font-size:11px;color:#b45309;background:#fffbeb;border:1px solid #fcd34d;border-radius:7px;padding:5px 7px;margin-bottom:5px;line-height:1.32;text-align:center;font-weight:600;}
.cme-ann{position:relative;font-size:12px;color:#334155;line-height:1.42;background:linear-gradient(180deg,#fffdf5,#fff7e6);border:1px solid #fcd34d;border-radius:11px;padding:8px 9px 8px 14px;}
.cme-ann::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:linear-gradient(180deg,#f59e0b,#ef4444);border-top-left-radius:11px;border-bottom-left-radius:11px;}
.cme-clock-row{font-size:11px;color:#475569;margin-top:4px;display:flex;justify-content:space-between;gap:6px;}
.cme-clock-row span:last-child{font-weight:700;color:#0369a1;text-align:right;}
.cme-mode-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:8px;padding-top:8px;border-top:1px dashed #e2e8f0;}
.cme-mode-label{font-size:11px;color:#64748b;font-weight:700;white-space:nowrap;}
.cme-mode-select{flex:1;min-width:0;border:1px solid #cbd5e1;border-radius:8px;padding:4px 6px;font-size:11px;background:#fff;color:#0f172a;}
.cme-plan-select{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:6px 9px;font-size:12px;background:#fff;color:#0f172a;}
.cme-study-mode-block{margin-bottom:4px;width:100%;}
.cme-study-mode-block .cme-study-mode-row{display:flex;align-items:center;gap:5px;margin-bottom:0;width:100%;}
.cme-study-mode-hint{margin-top:5px;padding:7px 9px;border-radius:9px;border:1px solid #e2e8f0;font-size:11px;line-height:1.42;overflow-wrap:anywhere;word-break:break-word;width:100%;}
.cme-study-mode-hint-title{display:block;font-weight:800;margin-bottom:3px;}
.cme-study-mode-hint-body{margin:0;color:#475569;}
.cme-study-mode-hint-realtime{background:linear-gradient(135deg,#fffbeb,#fef3c7);border-color:#f59e0b;color:#78350f;}
.cme-study-mode-hint-realtime .cme-study-mode-hint-body{color:#92400e;}
.cme-study-mode-hint-offline{background:linear-gradient(135deg,#eff6ff,#f0f9ff);border-color:#bfdbfe;color:#1e3a8a;}
.cme-study-mode-hint-offline .cme-study-mode-hint-body{color:#334155;}
.cme-offline-batch{margin-top:5px;padding:7px 9px;border-radius:8px;background:#eff6ff;border:1px solid #bfdbfe;font-size:11px;line-height:1.5;color:#1e40af;}
.cme-offline-early-btn{margin-top:6px;padding:4px 10px;border-radius:6px;border:1px solid #93c5fd;background:#fff;color:#1d4ed8;font-size:11px;font-weight:600;cursor:pointer;display:inline-block;}
.cme-offline-early-btn:hover{background:#dbeafe;}
#hy-cme-exam-code-modal{position:fixed;inset:0;z-index:2147483647;background:rgba(15,23,42,.52);display:flex;align-items:center;justify-content:center;padding:16px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;}
#hy-cme-exam-code-dialog{background:#fff;border-radius:16px;box-shadow:0 24px 60px rgba(15,23,42,.28);padding:18px 20px 16px;width:min(360px,92vw);border:1px solid #dbe4f0;}
.hy-cap-title{font-size:14px;font-weight:800;color:#0f172a;line-height:1.35;margin-bottom:4px;}
.hy-cap-sub{font-size:12px;color:#64748b;margin-bottom:12px;}
.hy-cap-imgbox{display:flex;align-items:center;gap:10px;margin-bottom:12px;padding:10px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;}
#hy-cme-cap-img{display:block;height:40px;min-width:100px;max-width:200px;object-fit:contain;background:#fff;border:1px solid #dbe4f0;border-radius:6px;padding:2px 6px;flex:1;}
.hy-cap-refresh{flex:0 0 auto;border:1px solid #cbd5e1;background:#fff;color:#334155;border-radius:8px;padding:6px 10px;font-size:12px;font-weight:700;cursor:pointer;}
.hy-cap-refresh:hover{background:#f1f5f9;}
#hy-cme-cap-input{width:100%;box-sizing:border-box;border:1px solid #cbd5e1;border-radius:10px;padding:10px 12px;font-size:18px;letter-spacing:.12em;text-align:center;font-weight:700;margin-bottom:12px;}
#hy-cme-cap-input:focus{outline:none;border-color:#2563eb;box-shadow:0 0 0 3px rgba(37,99,235,.15);}
.hy-cap-actions{display:flex;gap:8px;justify-content:flex-end;}
.hy-cap-btn{border:none;border-radius:10px;padding:8px 16px;font-size:13px;font-weight:800;cursor:pointer;}
.hy-cap-btn-ghost{background:#f1f5f9;color:#475569;}
.hy-cap-btn-primary{background:#188aae;color:#fff;}
#hy-cme-settings-pane{padding:2px 0;overflow-y:auto;flex:1 1 auto;min-height:0;}
.cme-cloud-token-block{margin-top:4px;padding-top:6px;border-top:1px dashed #e2e8f0;}
.cme-cloud-token-label{display:block;font-size:12px;color:#64748b;margin-bottom:5px;}
.cme-cloud-token-input{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:6px 8px;font-size:12px;background:#fff;color:#0f172a;box-sizing:border-box;}
.cme-cloud-token-actions{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px;align-items:center;}
#hy-cme-cloud-save{flex:1 1 120px !important;min-width:120px !important;white-space:nowrap !important;padding:6px 10px !important;font-size:12px !important;color:#0f172a !important;background:#fff !important;border:1px solid #cbd5e1 !important;}
#hy-cme-open-pro{flex:1 1 88px !important;min-width:88px !important;}
.cme-btn-pro{flex:0 0 auto;background:linear-gradient(135deg,#f59e0b,#ef4444);color:#fff;border:none;box-shadow:0 3px 10px rgba(239,68,68,.22);padding:6px 10px;font-size:12px;border-radius:10px;cursor:pointer;font-weight:800;}
.cme-btn-pro:hover{filter:brightness(1.06);}
.cme-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a;flex:0 0 auto;}
#hy-cme-pro-modal{position:fixed;inset:0;background:rgba(15,23,42,.45);z-index:1000001;display:none;align-items:center;justify-content:center;padding:20px;}
#hy-cme-confirm-modal{position:fixed;inset:0;background:rgba(15,23,42,.45);z-index:1000002;display:none;align-items:center;justify-content:center;padding:20px;}
.cme-confirm-card{width:min(92vw,340px);background:#fff;border:1px solid #dbe4f0;border-radius:14px;padding:14px 16px;box-shadow:0 16px 40px rgba(15,23,42,.18);}
.cme-confirm-title{font-size:14px;font-weight:900;color:#0f172a;margin-bottom:8px;}
.cme-confirm-body{font-size:12px;line-height:1.55;color:#475569;margin-bottom:14px;}
.cme-confirm-actions{display:flex;justify-content:flex-end;gap:8px;}
.cme-confirm-actions .cme-btn{min-width:72px;padding:6px 12px;font-size:12px;}
.cme-pro-card{width:min(560px,92vw);max-height:88vh;overflow:auto;background:#fff;border-radius:18px;border:1px solid #dbe4f0;box-shadow:0 18px 46px rgba(15,23,42,.25);padding:16px;}
.cme-pro-title{font-size:24px;font-weight:900;color:#0f172a;}
.cme-pro-sub{font-size:13px;color:#64748b;margin-top:4px;line-height:1.45;}
.cme-pro-sec{margin-top:12px;border:1px solid #dbe4f0;border-radius:12px;padding:12px;background:#f8fafc;}
.cme-pro-sec h4{margin:0 0 8px;font-size:15px;color:#0f172a;}
.cme-pro-sec p{margin:4px 0;font-size:13px;color:#334155;line-height:1.5;}
.cme-pro-sec ul{margin:6px 0 0 18px;padding:0;}
.cme-pro-sec li{margin:4px 0;font-size:13px;color:#334155;line-height:1.45;}
.cme-pro-buy{display:flex;flex-direction:column;gap:10px;margin-top:8px;}
.cme-pro-muted{color:#64748b;font-size:12px;line-height:1.5;}
.cme-pro-buy-btn{display:inline-block;background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:10px 16px;border-radius:12px;font-size:14px;font-weight:800;border:none;cursor:pointer;align-self:flex-start;}
.cme-pro-tip{margin-top:8px;background:#fef3c7;border:1px solid #fcd34d;border-radius:10px;padding:8px 10px;font-size:13px;color:#92400e;font-weight:700;line-height:1.45;}
.cme-pro-tip-info{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8;font-weight:600;}
.cme-pro-actions{margin-top:14px;display:flex;justify-content:flex-end;}
.cme-pro-close{border:none;background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:10px 18px;border-radius:12px;font-size:14px;font-weight:800;cursor:pointer;}
#hy-cme-face-modal{position:fixed;inset:0;background:rgba(15,23,42,.52);z-index:1000003;display:none;align-items:center;justify-content:center;padding:20px;}
.cme-face-card{width:min(420px,92vw);background:#fff;border-radius:18px;border:1px solid #dbe4f0;box-shadow:0 18px 46px rgba(15,23,42,.28);padding:18px 16px 14px;text-align:center;}
.cme-face-title{font-size:20px;font-weight:900;color:#0f172a;}
.cme-face-sub{font-size:13px;color:#64748b;margin-top:6px;line-height:1.45;}
.cme-face-qr-wrap{width:220px;height:220px;margin:14px auto 10px;border:1px solid #dbdbdb;border-radius:8px;overflow:hidden;background:#fff;}
.cme-face-qr-wrap img{display:block;width:220px;height:220px;object-fit:contain;}
.cme-face-tip{font-size:12px;color:#475569;line-height:1.55;text-align:left;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;padding:8px 10px;}
.cme-face-tip strong{color:#1277af;}
.cme-face-tip-actions{margin-top:8px;font-size:11px;color:#64748b;background:#fffbeb;border-color:#fde68a;}
.cme-face-status{margin-top:10px;font-size:13px;font-weight:700;color:#0369a1;}
.cme-face-actions{margin-top:12px;display:flex;justify-content:center;gap:10px;flex-wrap:wrap;}
.cme-face-cancel{border:1px solid #cbd5e1;background:#fff;color:#64748b;padding:8px 18px;border-radius:10px;font-size:13px;font-weight:700;cursor:pointer;}
.cme-face-cancel:hover{background:#f8fafc;color:#0f172a;}
.cme-face-skip{border:none;background:linear-gradient(135deg,#f59e0b,#ef4444);color:#fff;padding:8px 18px;border-radius:10px;font-size:13px;font-weight:800;cursor:pointer;}
.cme-face-skip:hover{filter:brightness(1.05);}
`;
document.head.appendChild(st);
}
function openHyProBuyPage() {
const url = String(cloudState.proBuyUrl || PRO_BUY_URL).trim() || PRO_BUY_URL;
try {
GM_openInTab(url, { active: true, insert: true, setParent: true });
} catch (_) {
window.open(url, "_blank", "noopener,noreferrer");
}
}
function openHyProModal() {
const vm = document.getElementById("hy-cme-pro-modal");
if (vm) vm.style.display = "flex";
}
function closeHyProModal() {
const vm = document.getElementById("hy-cme-pro-modal");
if (vm) vm.style.display = "none";
}
function createHyProModal() {
const old = document.getElementById("hy-cme-pro-modal");
if (old) old.remove();
const modal = document.createElement("div");
modal.id = "hy-cme-pro-modal";
modal.innerHTML = `
\u5f00\u901a Pro
6\u670822\u65e520\u70b9\u5f00\u653e\u8d2d\u4e70
Pro \u6743\u76ca
- \u89e3\u9664\u514d\u8d39\u4f53\u9a8c\u7ae0\u8282\u6b21\u6570\u9650\u5236
- \u89e3\u9501\u79bb\u7ebf\u5f02\u6b65\u6279\u91cf\u6a21\u5f0f
- \u652f\u6301\u5b66\u5b8c\u540e\u81ea\u52a8\u8003\u8bd5\u4ea4\u5377
- Pro \u6709\u6548\u671f 30 \u5929\uff08\u6fc0\u6d3b\u540e\u8d77\u7b97\uff0c\u7ed1\u5b9a\u534e\u533b\u7f51\u8d26\u53f7\uff09
\u516c\u6d4b\u622a\u6b626\u670822\u65e520\u70b9\uff0c\u516c\u6d4b\u671f\u95f4 QQ \u7fa4\uff08${QQ_GROUP_NUMBER}\uff09\u514d\u8d39\u83b7\u53d6 Token\u3002
\u5f00\u901a\u65b9\u5f0f
\u516c\u6d4b\u671f\u95f4\uff1a\u52a0\u5165 QQ \u7fa4 ${QQ_GROUP_NUMBER}\uff0c\u8054\u7cfb\u7ba1\u7406\u5458\u514d\u8d39\u83b7\u53d6 Token
\u6b63\u5f0f\u8d2d\u4e70\uff1a6\u670822\u65e520\u70b9\u8d77\u524d\u5f80\u8d2d\u4e70\u9875\u81ea\u52a9\u83b7\u53d6 Token
\u8d2d\u5f97 Token \u540e\uff1a\u5728\u300c\u8bbe\u7f6e\u300d\u9875\u7c98\u8d34\uff0c\u70b9\u51fb\u300c\u4fdd\u5b58\u5e76\u6821\u9a8c\u300d\u3002
\u91cd\u8981\uff1a\u8bf7\u52ff\u968f\u610f\u6cc4\u9732 Token\uff0c\u907f\u514d\u8d26\u53f7\u88ab\u591a\u4eba\u5171\u7528\u5bfc\u81f4\u5931\u6548\u3002
`;
document.body.appendChild(modal);
modal.addEventListener("click", (e) => {
if (e.target === modal) closeHyProModal();
});
modal.querySelector("#hy-cme-pro-close")?.addEventListener("click", closeHyProModal);
const buyBtn = modal.querySelector("#hy-cme-pro-buy-link");
if (buyBtn) {
const buyOpenAt = new Date("2026-06-22T20:00:00+08:00").getTime();
if (Date.now() >= buyOpenAt) {
buyBtn.disabled = false;
buyBtn.style.opacity = "";
buyBtn.style.cursor = "";
buyBtn.textContent = "\u524d\u5f80\u8d2d\u4e70\u9875\u5f00\u901a Pro";
buyBtn.addEventListener("click", openHyProBuyPage);
}
}
}
function buildPanel() {
if (!shouldShowPanel()) {
syncFollowerBanner();
return;
}
removeFollowerBanner();
if (document.getElementById("hy-cme-auto-panel")) return;
injectHyPanelStyles();
const panel = document.createElement("div");
panel.id = "hy-cme-auto-panel";
panel.className = "cme-panel-max";
panel.innerHTML = `
\u8fd0\u884c\u72b6\u6001
\u5df2\u505c\u6b62
\u89c6\u9891 0/0
·
\u8003\u8bd5 —
0%
\u8bfe\u7a0b\u9009\u62e9
\u9879\u76ee\u6536\u85cf
\u6b63\u5728\u52a0\u8f7d\u6536\u85cf\u8bfe\u7a0b…
\u7ae0\u8282\u9884\u89c8\u5f53\u524d\u9875
\u8bf7\u5728\u7ae0\u8282\u5217\u8868\u9875\u67e5\u770b
\u8fd0\u884c\u65e5\u5fd7
\u8fdb\u5ea6
\u6388\u6743\u4e0e\u9009\u9879\u8bbe\u7f6e
\u7528\u6237\u7c7b\u578b\u672a\u6821\u9a8c
\u514d\u8d39\u4f53\u9a8c\uff082 \u8282\uff090/2
1:1\u6302\u673a\u6a21\u5f0f
`;
document.body.appendChild(panel);
createHyProModal();
const savedPos = readPanelPos();
if (savedPos && savedPos.left != null && savedPos.top != null) {
panel.style.right = "auto";
panel.style.left = `${savedPos.left}px`;
panel.style.top = `${savedPos.top}px`;
} else {
panel.style.top = "80px";
panel.style.right = "20px";
}
applyPanelCollapsed(panel, readPanelCollapsed());
enablePanelDrag(panel);
syncPanelDebugLayout();
panel.querySelector("#hy-cme-btn-min")?.addEventListener("click", (e) => {
e.stopPropagation();
writePanelCollapsed(true);
applyPanelCollapsed(panel, true);
});
panel.querySelector("#hy-cme-btn-max")?.addEventListener("click", (e) => {
e.stopPropagation();
writePanelCollapsed(false);
applyPanelCollapsed(panel, false);
});
panel.querySelectorAll(".cme-tab-btn").forEach((btn) => {
btn.addEventListener("click", () => switchPanelTab(btn.dataset.tab));
});
panel.querySelector("#hy-cme-clear-log")?.addEventListener("click", () => clearRunLog());
panel.querySelector("#hy-cme-refresh-courses")?.addEventListener("click", () => refreshPanelCourses());
bindCourseListEvents();
panel.querySelector("#hy-cme-open-pro")?.addEventListener("click", openHyProModal);
panel.querySelector("#hy-cme-join-qq")?.addEventListener("click", openHyQqGroup);
const modeHintTitle = panel.querySelector("#hy-cme-mode-desc .cme-study-mode-hint-title");
modeHintTitle?.addEventListener("click", () => {
scheduleModeHintCollapse();
});
const modeSel = panel.querySelector("#hy-cme-run-mode");
modeSel?.addEventListener("change", async () => {
const next = modeSel.value === "offline" ? "offline" : "realtime";
if (canUseOfflineMode()) {
modeSel.value = "offline";
pushUserLog("Pro \u7528\u6237\u8bf7\u4f7f\u7528\u79bb\u7ebf\u6279\u91cf\uff1a\u62c9\u6ee1\u540e\u7b49\u5f85\u5230\u70b9\uff0c\u518d\u70b9\u5f00\u59cb\u6279\u91cf\u6536\u5c3e\u4e0e\u8003\u8bd5");
syncRunModePanel();
return;
}
if (next === "offline" && !canUseOfflineMode()) {
modeSel.value = getRunMode() === "offline" ? "realtime" : getRunMode();
pushUserLog("\u79bb\u7ebf\u6279\u91cf\u6a21\u5f0f\u4ec5 Pro \u53ef\u7528\uff0c\u514d\u8d39\u6863\u8bf7\u4f7f\u7528 1:1 \u6302\u673a");
openHyProModal();
syncRunModePanel();
return;
}
const prev = getRunMode();
const active = Object.values(loadJobs()).some((j) => j && j.phase !== "done");
if (active && next !== prev) {
const ok = await askHyPanelConfirm({
title: "\u5207\u6362\u5b66\u4e60\u6a21\u5f0f",
message: "\u5f53\u524d\u961f\u5217\u4e2d\u6709\u8fdb\u884c\u4e2d\u7684\u4efb\u52a1\uff0c\u5207\u6362\u6a21\u5f0f\u5efa\u8bae\u5148\u6e05\u7a7a\u961f\u5217\u3002\u662f\u5426\u7ee7\u7eed\u5207\u6362\uff1f",
okText: "\u7ee7\u7eed\u5207\u6362",
cancelText: "\u53d6\u6d88",
});
if (!ok) {
modeSel.value = prev;
return;
}
}
setRunMode(next);
syncRunModePanel();
pushLog(`\u5df2\u5207\u6362\u4e3a\uff1a${formatRunModeLabel(next)}`);
});
panel.querySelector("#hy-cme-cloud-save")?.addEventListener("click", async () => {
const input = document.getElementById("hy-cme-cloud-token");
const btn = document.getElementById("hy-cme-cloud-save");
cloudState.cloudToken = String((input && input.value) || "").trim();
cloudState.cloudRevoked = false;
localStorage.setItem(CLOUD_TOKEN_KEY, cloudState.cloudToken);
cloudState.cloudLease = "";
cloudState.cloudLeaseExp = 0;
if (btn) btn.disabled = true;
try {
if (!cloudState.cloudToken) {
cloudState.cloudTier = "free";
cloudState.cloudRevoked = false;
notifyUser("\u5df2\u6e05\u9664 Pro Token\uff0c\u4f7f\u7528\u514d\u8d39\u4f53\u9a8c");
clearPanelHint();
} else {
await _vt();
await _cl(true);
notifyUser(`Token \u6821\u9a8c\u901a\u8fc7 · ${formatCloudTierText(cloudState.cloudTier)}`);
clearPanelHint();
}
updatePanelCloudStatus();
} catch (e) {
cloudState.cloudTier = "unknown";
cloudState.cloudRevoked = /revoked|invalid token|expired/i.test(String(e?.message || e));
cloudState.cloudLease = "";
cloudState.cloudLeaseExp = 0;
writeCloudLeaseCache("", 0);
notifyAuthError(e?.message || e);
updatePanelCloudStatus();
} finally {
if (btn) btn.disabled = false;
}
});
panel.querySelector("#hy-cme-start")?.addEventListener("click", async () => {
if (getEnabled()) return;
syncQueueFromCourseList();
let selected = pruneQueueToCourses().slice();
if (!selected.length) {
const fallback = getQueue().slice();
if (fallback.length) {
commitQueueSelection(fallback);
selected = pruneQueueToCourses().slice();
}
}
if (!selected.length) {
pushUserLog("\u8bf7\u5148\u52fe\u9009\u81f3\u5c11\u4e00\u95e8\u8bfe\u7a0b");
updatePanel();
return;
}
panelState.selectionLock = selected.slice();
setStudySessionQueue(selected);
commitQueueSelection(selected);
try {
if (isOfflineMode() && !canUseOfflineMode()) {
setRunMode("realtime");
syncRunModePanel();
panelState.selectionLock = null;
setStudySessionQueue([]);
pushUserLog("\u79bb\u7ebf\u6279\u91cf\u6a21\u5f0f\u4ec5 Pro \u53ef\u7528\uff0c\u514d\u8d39\u6863\u8bf7\u4f7f\u7528 1:1 \u6302\u673a");
openHyProModal();
updatePanel();
return;
}
if (!(await _cr({ forceLease: true }))) {
panelState.selectionLock = null;
setStudySessionQueue([]);
updatePanel();
return;
}
if (isOfflineMode() && getOfflineBatchSnapshot()?.filledReady) {
pushUserLog("\u6b63\u5728\u68c0\u9a8c\u662f\u5426\u53ef\u63d0\u524d\u63d0\u4ea4\u64ad\u5b8c\u8bb0\u5f55…");
const earlyRes = await runOfflineEarlyFinishScan({
autoFinish: true,
force: true,
quiet: false,
});
if (earlyRes.finished > 0) {
pushUserLog(
`\u63d0\u524d\u6536\u5c3e ${earlyRes.finished} \u8282${earlyRes.examQueued ? "\uff0c\u7ee7\u7eed\u542f\u52a8\u81ea\u52a8\u8003\u8bd5" : ""}`
);
syncRunModePanel();
}
}
if (isProOfflineWaitingNotDue()) {
const snap = getOfflineBatchSnapshot();
const dueText = (snap && snap.beijingDue) || "\u5230\u70b9\u65f6\u95f4";
panelState.selectionLock = null;
setStudySessionQueue([]);
pushUserLog(
`\u79bb\u7ebf\u6279\u91cf\u7b49\u5f85\u4e2d\uff0c\u8bf7\u4e8e\u5317\u4eac\u65f6\u95f4 ${dueText} \u540e\u518d\u70b9\u300c\u5f00\u59cb\u300d\uff1b\u4e5f\u53ef\u70b9\u300c\u63d0\u524d\u6536\u5c3e\u68c0\u9a8c\u300d`
);
setPanelHint(`\u7ea6 ${formatEta((snap && snap.due ? snap.due : Date.now()) - Date.now())} \u540e\u53ef\u7ee7\u7eed`);
updatePanel();
return;
}
if (isProOfflineFinishReady()) {
pushUserLog("\u5df2\u5230\u7b49\u5f85\u65f6\u95f4\uff0c\u5c06\u68c0\u67e5\u8bfe\u7a0b\u5b8c\u6210\u5e76\u6279\u91cf\u6536\u5c3e\u3001\u81ea\u52a8\u8003\u8bd5");
} else if (hasOfflinePartialFinishReady()) {
const n = countOfflinePartialFinishReady(loadJobs(), getLearningUserId(), Date.now());
pushUserLog(`\u6709 ${n} \u95e8\u8bfe\u5df2\u5230\u53ef\u6536\u5c3e\u65f6\u95f4\uff0c\u5c06\u5148\u5b8c\u6210\u5df2\u5230\u70b9\u8bfe\u7a0b`);
} else if (hasProOfflineAppendFillWork()) {
pushUserLog("\u68c0\u6d4b\u5230\u65b0\u52fe\u9009\u8bfe\u7a0b\uff0c\u5c06\u8865\u6279\u91cf\u62c9\u6ee1\uff1b\u5168\u90e8\u62c9\u6ee1\u540e\u5230\u70b9\u6309\u7d2f\u8ba1\u8bfe\u957f\u81ea\u52a8\u91cd\u7b97");
} else if (canUseOfflineMode()) {
pushUserLog("Pro \u79bb\u7ebf\u6279\u91cf\uff1a\u5c06\u5148\u62c9\u6ee1\u8fdb\u5ea6\uff0c\u5b8c\u6210\u540e\u8bf7\u7b49\u5f85\u5230\u70b9\u518d\u70b9\u5f00\u59cb\u6536\u5c3e");
}
restoreLockedQueueSelection();
prepareJobsForStudyStart(selected);
claimLeaderTab(true);
startLeaderHeartbeat();
panelState.studyStartedAt = Date.now();
setEnabled(true);
ensurePanelDefaults();
clearPanelHint();
startPreviewAutoRefresh();
startGlobalTicker();
await registerChaptersFromCourses("untouched", panelState.selectionLock.slice());
restoreLockedQueueSelection();
pushUserLog(`\u5f00\u59cb\u5b66\u4e60 · ${formatSelectedProgressLine()}`);
refreshGongxuStudyHint();
if (selectionHasGongxuCourse()) {
pushUserLog("\u516c\u9700\u8bfe\uff1a\u82e5\u5361\u5728\u67d0\u8282\uff0c\u8bf7\u300c\u505c\u6b62\u300d\u540e\u9a6c\u4e0a\u518d\u70b9\u300c\u5f00\u59cb\u300d\uff08\u53ef\u53cd\u590d\u64cd\u4f5c\u9010\u8282\u8fc7\u5173\uff09");
}
updatePanel();
if (getApiMode() && isLeaderTab()) {
await tryResumeFacePendingOnStart().catch(function () {});
setTimeout(() => runBgScheduler(), 300);
}
} catch (e) {
const em = String(e?.message || e);
panelState.studyStartedAt = 0;
if (getEnabled()) {
stopAutoStudy(`\u5f00\u59cb\u5b66\u4e60\u5931\u8d25\uff1a${em.slice(0, 80)}`);
} else {
const q = captureQueueSnapshot();
panelState.selectionLock = null;
setStudySessionQueue([]);
if (q.length) commitQueueSelection(q);
pushUserLog("\u5f00\u59cb\u5b66\u4e60\u5931\u8d25\uff1a" + em.slice(0, 80));
}
pushLog("\u5f00\u59cb\u5b66\u4e60\u5931\u8d25\uff1a" + em);
updatePanel();
}
});
panel.querySelector("#hy-cme-stop")?.addEventListener("click", () => {
if (!getEnabled()) return;
stopAutoStudy("\u5df2\u6682\u505c");
});
panel.querySelector("#hy-cme-clear-queue")?.addEventListener("click", () => void clearJobQueue());
updatePanel();
syncRunModePanel();
try {
const logVerKey = "hy_cme_user_log_v";
if (localStorage.getItem(logVerKey) !== VERSION) {
clearRunLog();
localStorage.setItem(logVerKey, VERSION);
}
} catch (_) {}
renderRunLog();
startGlobalTicker();
stopPanelUpdateTimer();
window.__hyPanelUpdateTimer = setInterval(() => {
if (isLeaderTab()) updatePanel();
}, 1000);
initPanelData();
if (getEnabled()) startPreviewAutoRefresh();
}
async function bootPage(manual) {
const type = pageType();
if (manual) pushLog("\u624b\u52a8\uff1a" + type);
if (type === "hdbl") {
if (!isHdInteractiveEnabled()) return;
captureHdLaunchParamsEarly();
scheduleHdInteractiveBoot();
return;
}
if (type === "hd_result") {
if (!isHdInteractiveEnabled()) return;
const cwid =
new URLSearchParams(location.search).get("businessCustomParams") ||
new URLSearchParams(location.search).get("cwid") ||
"";
if (isHdDetachedRunner()) {
postHdIframeEvent("done", { cwid });
setTimeout(tryCloseHdRunnerWindow, 800);
return;
}
if (cwid && completeInteractiveJobByCwid(cwid)) {
pushLog(`[\u4e92\u52a8] ${cwid.slice(0, 8)} \u5df2\u5b8c\u6210\uff0c\u7ee7\u7eed\u961f\u5217`);
}
clearHdMetaCookie();
if (getEnabled() && getApiMode()) setTimeout(() => runBgScheduler(), 1500);
return;
}
if (type === "exam_code") {
if (getEnabled() && getAutoExam()) {
const cwid = new URLSearchParams(location.search).get("cwid");
if (cwid) {
const runCode = async () => {
const tag = cwid.slice(0, 8);
const html = document.documentElement ? document.documentElement.outerHTML : "";
const cap = await solveHyExamCodeChallenge(cwid, tag, html, location.href);
if (cap.ok) {
location.href = `${location.origin}/pages/exam.aspx?cwid=${encodeURIComponent(cwid)}`;
} else if (!cap.paused) {
pushLog(`[${tag}] \u9a8c\u8bc1\u7801\u672a\u901a\u8fc7: ${cap.msg || "\u672a\u77e5"}`);
}
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => setTimeout(runCode, 600));
} else {
setTimeout(runCode, 600);
}
}
}
return;
}
if (type === "face") {
injectHyPanelStyles();
const cwrid = new URLSearchParams(location.search).get("cwrid");
const ff = new URLSearchParams(location.search).get("ff") || "";
const isVideoFace = ff === "1";
if (cwrid && getEnabled()) {
const runFace = async () => {
const ctx =
findJobCtxByCwid(cwrid) ||
findPanelChapterCtxByCwid(cwrid) ||
{ cwrid, cwid: cwrid, ff: isVideoFace ? "1" : ff, title: cwrid.slice(0, 8) };
if (isVideoFace) {
const face = await waitHyFaceVerify(Object.assign({}, ctx, { ff: "1" }), ctx.title || cwrid.slice(0, 8), "video");
if (face.ok) {
markHyVideoFaceVerified(cwrid);
if (getApiMode()) setTimeout(() => runBgScheduler(), 1500);
} else if (face.msg && !/\u7a0d\u540e\u91cd\u8bd5/.test(face.msg)) {
pushLog(`[${ctx.title || cwrid.slice(0, 8)}] \u770b\u8bfe\u524d\u4eba\u8138: ${face.msg}`);
}
} else if (getAutoExam() && !skipExamsSession) {
const face = await waitHyExamFaceVerify(ctx, ctx.title || cwrid.slice(0, 8));
if (face.ok) {
await afterVideoMaybeExam(ctx, ctx.title || cwrid.slice(0, 8));
if (getApiMode()) setTimeout(() => runBgScheduler(), 1500);
} else if (face.skipAll) {
skipAllPendingExams(`[${ctx.title || cwrid.slice(0, 8)}] \u5df2\u9009\u62e9\u6682\u4e0d\u8003\u8bd5\uff0c\u672c\u6b21\u8fd0\u884c\u8df3\u8fc7\u6240\u6709\u8003\u8bd5`);
} else if (face.msg && !/\u7a0d\u540e\u91cd\u8bd5/.test(face.msg)) {
pushLog(`[${ctx.title || cwrid.slice(0, 8)}] \u4eba\u8138\u8bc6\u522b: ${face.msg}`);
}
}
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => setTimeout(runFace, 600));
} else {
setTimeout(runFace, 600);
}
}
return;
}
if (type === "exam") {
if (getEnabled() && getAutoExam()) {
const cwid = new URLSearchParams(location.search).get("cwid");
if (cwid) {
const runExam = async () => {
const ctx =
findJobCtxByCwid(cwid) || findPanelChapterCtxByCwid(cwid) || { cwid, cwrid: cwid, title: cwid.slice(0, 8) };
await afterVideoMaybeExam(ctx, ctx.title || cwid.slice(0, 8));
if (getApiMode()) setTimeout(() => runBgScheduler(), 1500);
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => setTimeout(runExam, 600));
} else {
setTimeout(runExam, 600);
}
}
}
return;
}
if (type === "exam_result") {
await handleExamResultPage();
if (getEnabled() && getApiMode()) setTimeout(() => runBgScheduler(), 1500);
return;
}
if (type === "ware_hd") return runWareHdPage();
if (type === "play" && manual) await runPlayPage();
if (type === "ware") return runWareRedirect();
if (type === "course") {
updateJobsPanel();
if (isHdInteractiveEnabled()) {
installInteractiveChapterClickHook();
syncInteractiveJobsFromChapters(parseCourseChapterJobs());
}
if (getEnabled() && getApiMode()) runBgScheduler();
return;
}
if (type === "collect") return runCollectPage();
}
function init() {
if (isHdInteractiveEnabled()) {
captureHdLaunchParamsEarly();
}
purgeInteractiveJobs();
if (isCmeHost() && isHdInteractiveEnabled()) installHdIframeBridge();
if (isHdDetachedRunner()) {
postHdIframeEvent("ping", {
stage: "init",
host: location.hostname,
path: location.pathname,
});
}
ensurePanelDefaults();
installLeaderTabSystem();
const boot = () => {
prepareLeaderOnBoot();
buildPanel();
syncFollowerBanner();
bootPage(false);
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", boot);
} else {
boot();
}
}
if (shouldRunScript()) {
try {
if (isHdInteractiveEnabled()) captureHdLaunchParamsEarly();
} catch (_) {}
init();
}
})();