// ==UserScript==
// @name 【效率版】2026秋季学期人教版义教新教材网络培训
// @namespace https://card.wlxy.live/details/C8B94027
// @version 2.0
// @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png
// @description 2026秋季学期人教版义教新教材网络培训,PEP人教教材秋季培训,支持一键刷回放课程,秒刷完至少间隔12小时通常隔日平台更新进度。
// @antifeature payment 免费体验2课程,升级pro不限制
// @match *://wp.pep.com.cn/*
// @match *://bjpep.gensee.com/*
// @match *://wp.pep.com.cn/web/index.php?/px/*
// @match *://bjpep.gensee.com/webcast/site/vod/*
// @connect oa12.ahzsksw.cn
// @connect wp.pep.com.cn
// @connect bjpep.gensee.com
// @connect albcdnvodproxy.gensee.com
// @connect wscache.gensee.com
// @grant GM_openInTab
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-start
// @tag 人教版新教材培训
// @tag 学习辅助
// @tag 自动刷课
// @tag 回放刷课
// @tag PEP教材
// @license MIT
// ==/UserScript==
(function () {
"use strict";
const SCRIPT_VERSION = (() => {
try {
if (typeof GM_info !== "undefined" && GM_info?.script?.version) {
return String(GM_info.script.version);
}
} catch (_) {}
return "2.1.0";
})();
const SCRIPT_TITLE = "\u4eba\u6559\u7248\u4e49\u6559\u65b0\u6559\u6750\u7f51\u7edc\u57f9\u8bad\u52a9\u624b";
const SCRIPT_ICON =
"https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png";
const CLOUD_API_BASE = "https://oa12.ahzsksw.cn";
const PRO_BUY_URL = "https://card.wlxy.live/details/C8B94027";
const CLOUD_TOKEN_KEY = "rjpx_cloud_token_v1";
const CLOUD_LEASE_KEY = "rjpx_cloud_lease_v1";
const FINISH_TIP =
"\u6240\u9009\u8bfe\u7a0b\u5df2\u5168\u90e8\u5b66\u5b8c\u3002\u5e73\u53f0\u8fdb\u5ea6\u901a\u5e38\u9694\u65e5\u66f4\u65b0\uff0c\u4e14\u81f3\u5c11\u5237\u5b8c 12 \u5c0f\u65f6\u540e\u518d\u67e5\u770b\uff0c\u65e0\u9700\u62c5\u5fc3\u672a\u751f\u6548\u3002";
const HOST = location.host;
const IS_PEP = /(^|\.)wp\.pep\.com\.cn$/i.test(HOST);
const IS_GENSEE = /(^|\.)bjpep\.gensee\.com$/i.test(HOST);
const IN_IFRAME = (() => {
try {
return window.self !== window.top;
} catch (_) {
return true;
}
})();
if (!IS_PEP && !IS_GENSEE) return;
if (IS_PEP && IN_IFRAME) return;
const stolenLicense = {
sessionid: "",
confid: "",
userid: "",
siteid: "",
otherTid: "",
alb: "",
lastType0Body: "",
hits: 0,
};
function parseSessionIdEarly(text) {
const raw = String(text || "").trim();
if (!raw) return "";
const first = raw.split(/\s+/)[0];
if (/^\d{5,}$/.test(first)) return first;
const m = raw.match(/\b(\d{6,})\b/);
return m ? m[1] : "";
}
function ingestLicenseUrl(url, bodyText) {
try {
const u = String(url || "");
if (!/albcmd\/license/i.test(u)) return;
stolenLicense.hits += 1;
const q = new URL(u, location.href);
const host = q.hostname || "";
if (host) stolenLicense.alb = host;
const typ = q.searchParams.get("type");
if (typ === "0") {
stolenLicense.lastType0Body = String(bodyText || "").slice(0, 200);
const sid = parseSessionIdEarly(bodyText);
if (sid) stolenLicense.sessionid = sid;
if (q.searchParams.get("confid")) stolenLicense.confid = q.searchParams.get("confid");
if (q.searchParams.get("userid")) stolenLicense.userid = q.searchParams.get("userid");
if (q.searchParams.get("siteid")) stolenLicense.siteid = q.searchParams.get("siteid");
}
if (typ === "1") {
const sid = q.searchParams.get("sessionid");
if (sid && /^\d{5,}$/.test(sid)) stolenLicense.sessionid = stolenLicense.sessionid || sid;
try {
const other = decodeURIComponent(q.searchParams.get("other") || "");
const tm = other.match(/tid=([a-f0-9]+)/i);
if (tm) stolenLicense.otherTid = tm[1];
} catch (_) {}
if (q.searchParams.get("confid")) stolenLicense.confid = stolenLicense.confid || q.searchParams.get("confid");
if (q.searchParams.get("userid")) stolenLicense.userid = stolenLicense.userid || q.searchParams.get("userid");
if (q.searchParams.get("siteid")) stolenLicense.siteid = stolenLicense.siteid || q.searchParams.get("siteid");
}
} catch (_) {}
}
if (IS_GENSEE) {
try {
const rawOpen = XMLHttpRequest.prototype.open;
const rawSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url) {
this.__rjpxUrl = String(url || "");
return rawOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function () {
try {
if (/albcmd\/license/i.test(this.__rjpxUrl || "")) {
this.addEventListener("load", () => {
ingestLicenseUrl(this.__rjpxUrl, this.responseText);
});
}
} catch (_) {}
return rawSend.apply(this, arguments);
};
} catch (_) {}
try {
const rawFetch = window.fetch;
if (typeof rawFetch === "function") {
window.fetch = function () {
const args = arguments;
const input = args[0];
const url = typeof input === "string" ? input : input && input.url ? input.url : "";
return rawFetch.apply(this, args).then((res) => {
try {
if (/albcmd\/license/i.test(String(url))) {
res
.clone()
.text()
.then((t) => ingestLicenseUrl(url, t))
.catch(() => {});
}
} catch (_) {}
return res;
});
};
}
} catch (_) {}
}
const K = {
enabled: "rjpx_auto_enabled_v1",
queue: "rjpx_course_queue_v1",
pos: "rjpx_panel_pos_v1",
collapsed: "rjpx_panel_collapsed_v1",
done: "rjpx_done_ids_v1",
current: "rjpx_current_lesson_v1",
logs: "rjpx_log_lines_v1",
localProg: "rjpx_local_progress_v1",
subject: "rjpx_selected_subject_v1",
job: "rjpx_api_job_v1",
openMode: "rjpx_open_mode_v1", // kept quietly; UI always iframe
cloudToken: CLOUD_TOKEN_KEY,
cloudLease: CLOUD_LEASE_KEY,
learningUserId: "rjpx_learning_user_id_v1",
progMigrated: "rjpx_prog_account_migrated_v1",
};
const PANEL_NOTICE =
"\u6e29\u99a8\u63d0\u793a\uff1a\u5f53\u524d\u57f9\u8bad\u672a\u5f00\u59cb\u65f6\u4ec5\u5904\u7406\u56de\u653e\u8bfe\u7a0b\uff1b\u5237\u5b8c\u540e\u5e73\u53f0\u8fdb\u5ea6\u901a\u5e38\u9694\u65e5\u66f4\u65b0\uff0c\u4e14\u81f3\u5c11\u6ee1 12 \u5c0f\u65f6\u540e\u518d\u67e5\u770b\u3002";
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";
function storeGet(key, fallback) {
try {
if (typeof GM_getValue === "function") {
const v = GM_getValue(key, undefined);
if (v !== undefined) return v;
}
} catch (_) {}
try {
const raw = localStorage.getItem(key);
if (raw == null) return fallback;
return JSON.parse(raw);
} catch (_) {
return fallback;
}
}
function storeSet(key, value) {
try {
if (typeof GM_setValue === "function") GM_setValue(key, value);
} catch (_) {}
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (_) {}
}
const state = {
enabled: !!storeGet(K.enabled, false),
companyId: "196",
subjects: [],
stages: [],
selectedSid: "",
lessons: [],
platformSections: [],
platformLessons: [],
logs: storeGet(K.logs, []) || [],
current: storeGet(K.current, null),
currentTask: "",
panelRefreshing: false,
bgBusy: false,
cloudToken: String(storeGet(K.cloudToken, "") || "").trim(),
cloudLease: "",
cloudLeaseExp: 0,
cloudTier: String(storeGet(K.cloudToken, "") || "").trim() ? "unknown" : "free",
cloudProExpireAt: 0,
tokenBoundBlocked: false,
freeVideoLimit: 2,
freeUsedVideos: 0,
proBuyUrl: PRO_BUY_URL,
finishTip: FINISH_TIP,
panelNotice: PANEL_NOTICE,
patchingPageButtons: false,
platformLessonsBySid: {},
platformSectionsBySid: {},
platformProgressInflight: {},
};
function loadQueue() {
const q = storeGet(K.queue, []);
return Array.isArray(q) ? q.map(String) : [];
}
function saveQueue(ids) {
storeSet(K.queue, (ids || []).map(String));
}
function loadDoneSet() {
migrateLegacyProgressIfNeeded();
const arr = storeGet(scopedProgKey(K.done), []);
return new Set(Array.isArray(arr) ? arr.map(String) : []);
}
function saveDoneSet(set) {
storeSet(scopedProgKey(K.done), [...set]);
}
function loadLocalProg() {
migrateLegacyProgressIfNeeded();
const o = storeGet(scopedProgKey(K.localProg), {});
return o && typeof o === "object" ? o : {};
}
function saveLocalProg(o) {
storeSet(scopedProgKey(K.localProg), o || {});
}
function clearLocalDone() {
saveDoneSet(new Set());
saveLocalProg({});
}
function progressAccountId() {
try {
if (typeof peekLiveLearningUserId === "function") {
const live = peekLiveLearningUserId();
if (live) return live;
}
} catch (_) {}
const saved = String(storeGet(K.learningUserId, "") || "").trim();
if (saved && saved !== "anon") return saved;
return "anon";
}
function scopedProgKey(base) {
return String(base) + "::" + progressAccountId();
}
function migrateLegacyProgressIfNeeded() {
const uid = progressAccountId();
if (!uid || uid === "anon") return;
const flag = K.progMigrated + "::" + uid;
if (storeGet(flag, false)) return;
const skProg = K.localProg + "::" + uid;
const skDone = K.done + "::" + uid;
if (!storeGet(skProg, null)) {
const legacyProg = storeGet(K.localProg, null);
if (legacyProg && typeof legacyProg === "object" && Object.keys(legacyProg).length) {
storeSet(skProg, legacyProg);
}
}
if (!storeGet(skDone, null)) {
const legacyDone = storeGet(K.done, null);
if (Array.isArray(legacyDone) && legacyDone.length) {
storeSet(skDone, legacyDone);
}
}
storeSet(flag, true);
}
function getCloudToken() {
return String(storeGet(K.cloudToken, "") || state.cloudToken || "").trim();
}
function setCloudToken(tok) {
const t = String(tok || "").trim();
state.cloudToken = t;
storeSet(K.cloudToken, t);
}
function getOpenMode() {
void storeGet(K.openMode, "iframe");
return "iframe";
}
function removePlayIframe() {
document.querySelectorAll("iframe[data-rjpx-play]").forEach((el) => {
try {
el.remove();
} catch (_) {}
});
}
function openPlayContext(entryUrl) {
removePlayIframe();
const iframe = document.createElement("iframe");
iframe.setAttribute("data-rjpx-play", "1");
iframe.setAttribute("title", "rjpx-play");
iframe.style.cssText =
"position:fixed;width:1px;height:1px;left:-100px;top:-100px;opacity:0;pointer-events:none;border:0;";
iframe.src = entryUrl;
document.documentElement.appendChild(iframe);
return "iframe";
}
function escHtml(s) {
return String(s || "")
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
}
const LOG_JARGON_RE =
/\b(license|sessionid|session|HTTP|http_|body|tid|albcdn|SDK|sdk|xhr|turbo|chunk|API|confid|userid|iframe|GM_|type\s*=|hits\s*=|otherTid|albcmd|gensee|lease|engine|Bearer|JSON)\b/i;
function isFriendlyLogLine(text) {
const line = String(text || "").trim();
if (!line) return false;
if (LOG_JARGON_RE.test(line)) return false;
return true;
}
function log(text) {
const line = String(text || "").trim();
if (!isFriendlyLogLine(line)) return;
console.log("[\u4eba\u6559\u770b\u8bfe] " + line);
state.logs.unshift(new Date().toLocaleTimeString() + " " + line);
if (state.logs.length > 200) state.logs.length = 200;
storeSet(K.logs, state.logs.slice(0, 80));
renderRunLog();
}
function renderRunLog() {
const box = document.getElementById("rjpx-run-log");
if (!box) return;
if (!state.logs.length) {
box.innerHTML = '
\u6682\u65e0\u65e5\u5fd7
';
return;
}
box.innerHTML = state.logs.map((t) => '' + escHtml(t) + "
").join("");
}
function clearRunLog() {
state.logs = [];
storeSet(K.logs, []);
renderRunLog();
}
function logCourse(action, title) {
const a = String(action || "").trim();
const t = String(title || "").trim();
if (!a) return;
log(t ? a + "\u300c" + t + "\u300d" : a);
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
function parseHms(s) {
const m = String(s || "")
.trim()
.match(/^(\d+):(\d{2}):(\d{2})$/);
if (!m) return 0;
return (+m[1] * 3600 + +m[2] * 60 + +m[3]) * 1000;
}
function formatHms(ms) {
const sec = Math.max(0, Math.floor((Number(ms) || 0) / 1000));
const h = Math.floor(sec / 3600);
const m = Math.floor((sec % 3600) / 60);
const s = sec % 60;
const pad = (n) => String(n).padStart(2, "0");
return pad(h) + ":" + pad(m) + ":" + pad(s);
}
function openUrl(url) {
try {
if (typeof GM_openInTab === "function") {
GM_openInTab(url, { active: true, insert: true, setParent: true });
return;
}
} catch (_) {}
window.open(url, "_blank");
}
function _gm(url, opts) {
opts = opts || {};
return new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest !== "function") {
reject(new Error("\u9700\u8981 Tampermonkey GM_xmlhttpRequest"));
return;
}
GM_xmlhttpRequest({
method: opts.method || "GET",
url,
headers: opts.headers || {},
data: opts.data || null,
timeout: opts.timeout || 60000,
anonymous: false,
onload: (res) => resolve(res),
onerror: (e) => reject(e || new Error("network error")),
ontimeout: () => reject(new Error("timeout")),
});
});
}
async function _gt(url, opts) {
const res = await _gm(url, opts || {});
return {
status: res.status || 0,
text: String(res.responseText || ""),
finalUrl: res.finalUrl || url,
headers: String(res.responseHeaders || ""),
};
}
function openProBuyPage() {
const url = String(state.proBuyUrl || PRO_BUY_URL).trim() || PRO_BUY_URL;
try {
if (typeof GM_openInTab === "function") {
GM_openInTab(url, { active: true, insert: true, setParent: true });
return;
}
} catch (_) {}
try {
window.open(url, "_blank");
} catch (_) {}
}
function readCookieValue(name) {
try {
const m = document.cookie.match(new RegExp("(?:^|;\\s*)" + name + "=([^;]*)"));
return m ? decodeURIComponent(m[1]) : "";
} catch (_) {
return "";
}
}
function peekLiveLearningUserId() {
const candidates = [
readCookieValue("peixun__px_uid"),
readCookieValue("peixun_uid"),
readCookieValue("peixun_userid"),
readCookieValue("PEPUSERID"),
];
for (const c of candidates) {
const s = String(c || "").trim();
if (s && /^\d{3,}$/.test(s)) return s;
}
try {
const uname = String(readCookieValue("peixun__px_uname") || "").trim();
const um = uname.match(/(\d{5,})\s*$/);
if (um && um[1]) return um[1];
} catch (_) {}
if (IS_PEP) {
try {
const html = document.documentElement ? document.documentElement.innerHTML : "";
const m =
html.match(/active_user:\s*['"](\d{5,})['"]/) ||
html.match(/partnerid=(\d{5,})/);
if (m && m[1]) return m[1];
} catch (_) {}
}
return "";
}
function invalidateCloudAuthCache(reason) {
state.cloudLease = "";
state.cloudLeaseExp = 0;
storeSet(K.cloudLease, null);
}
function onLearningUserChanged(prevId, nextId) {
invalidateCloudAuthCache();
storeSet(K.learningUserId, nextId);
state.tokenBoundBlocked = false;
state.cloudTier = getCloudToken() ? "unknown" : "free";
state.cloudProExpireAt = 0;
state.freeUsedVideos = 0;
try {
renderCourseList();
renderChapterPreview();
updatePanel();
} catch (_) {}
}
function resolveLearningUserId() {
if (IS_GENSEE) {
const saved = String(storeGet(K.learningUserId, "") || "").trim();
return saved && saved !== "anon" ? saved : "";
}
const live = peekLiveLearningUserId();
const saved = String(storeGet(K.learningUserId, "") || "").trim();
if (live) {
if (saved && saved !== "anon" && saved !== live) {
onLearningUserChanged(saved, live);
log("\u68c0\u6d4b\u5230\u4eba\u6559\u8d26\u53f7\u5207\u6362\uff0c\u5df2\u91cd\u65b0\u6821\u9a8c\u6388\u6743");
} else {
storeSet(K.learningUserId, live);
}
return live;
}
if (saved && saved !== "anon") return saved;
return "";
}
function formatProExpireText(expSec) {
const ex = Number(expSec || 0);
if (!Number.isFinite(ex) || ex <= 0) return "—";
const d = new Date(ex * 1000);
if (Number.isNaN(d.getTime())) return "—";
return d.toLocaleString("zh-CN", { hour12: false });
}
function resolveProExpireSec(data) {
const raw = data && (data.pro_expires_at ?? data.proExpiresAt ?? data.pro_expire_at ?? data.proExpireAt);
const n = Number(raw || 0);
if (Number.isFinite(n) && n > 1e12) return Math.floor(n / 1000);
return Number.isFinite(n) ? Math.floor(n) : 0;
}
function persistLeaseCacheExtra(extra) {
try {
const cur = storeGet(K.cloudLease, null) || {};
storeSet(K.cloudLease, Object.assign({}, typeof cur === "object" ? cur : {}, extra || {}));
} catch (_) {}
}
function applyLeasePayload(data) {
if (!data || typeof data !== "object") return;
const proExp = resolveProExpireSec(data);
if (data.lease) {
state.cloudLease = String(data.lease || "");
storeSet(K.cloudLease, {
lease: state.cloudLease,
exp: Number(data.lease_expires_at || data.exp || 0) || Math.floor(Date.now() / 1000) + 600,
tier: String(data.tier || ""),
freeVideoLimit: Number(data.free_video_limit ?? data.freeVideoLimit ?? 2),
freeUsedVideos: Number(data.free_used_videos ?? data.freeUsedVideos ?? 0),
proExpireAt: proExp || Number(data.proExpireAt || 0) || 0,
boundUid: String(data.bound_learning_user_id || data.userid || resolveLearningUserId() || ""),
proBuyUrl: String(data.proBuyUrl || state.proBuyUrl || PRO_BUY_URL),
finishTip: String(data.finishTip || state.finishTip || FINISH_TIP),
panelNotice: String(data.panelNotice || state.panelNotice || PANEL_NOTICE),
});
}
if (data.lease_expires_at != null || data.exp != null) {
state.cloudLeaseExp = Number(data.lease_expires_at ?? data.exp) || 0;
}
if (data.tier) state.cloudTier = String(data.tier);
const lim = data.free_video_limit ?? data.freeVideoLimit;
const used = data.free_used_videos ?? data.freeUsedVideos;
if (lim != null) state.freeVideoLimit = Number(lim) || 2;
if (used != null) state.freeUsedVideos = Number(used) || 0;
if (proExp > 0) state.cloudProExpireAt = proExp;
else if (data.proExpireAt != null) state.cloudProExpireAt = Number(data.proExpireAt) || 0;
if (data.proBuyUrl) state.proBuyUrl = String(data.proBuyUrl).trim() || PRO_BUY_URL;
if (data.finishTip) state.finishTip = String(data.finishTip);
if (data.panelNotice) state.panelNotice = String(data.panelNotice);
updateCloudPanelUI();
updatePanelNoticeUI();
}
function loadCachedLease() {
try {
const cached = storeGet(K.cloudLease, null);
if (!cached || typeof cached !== "object") return null;
const lease = String(cached.lease || "").trim();
const exp = Number(cached.exp || 0);
if (!lease || !exp) return null;
if (exp - Math.floor(Date.now() / 1000) <= 60) return null;
const liveUid = peekLiveLearningUserId();
const boundUid = String(cached.boundUid || "").trim();
if (liveUid && boundUid && liveUid !== boundUid) {
invalidateCloudAuthCache();
return null;
}
state.cloudLease = lease;
state.cloudLeaseExp = exp;
if (cached.tier) state.cloudTier = String(cached.tier);
if (cached.freeVideoLimit != null) state.freeVideoLimit = Number(cached.freeVideoLimit) || 2;
if (cached.freeUsedVideos != null) state.freeUsedVideos = Number(cached.freeUsedVideos) || 0;
if (cached.proExpireAt != null) state.cloudProExpireAt = Number(cached.proExpireAt) || 0;
if (cached.proBuyUrl) state.proBuyUrl = String(cached.proBuyUrl);
if (cached.finishTip) state.finishTip = String(cached.finishTip);
if (cached.panelNotice) state.panelNotice = String(cached.panelNotice);
return cached;
} catch (_) {
return null;
}
}
function parseCloudErrorDetail(data, status) {
let detail = data && (data.detail != null ? data.detail : data.message || data.code);
if (detail && typeof detail === "object") {
if (
detail.code === "free_quota_exceeded" ||
/free_quota/i.test(String(detail.code || "")) ||
/\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c/.test(String(detail.message || ""))
) {
if (detail.proBuyUrl) state.proBuyUrl = String(detail.proBuyUrl).trim() || PRO_BUY_URL;
if (detail.free_video_limit != null) state.freeVideoLimit = Number(detail.free_video_limit) || 2;
if (detail.free_used_videos != null) state.freeUsedVideos = Number(detail.free_used_videos) || 0;
const err = new Error("free_quota_exceeded");
err.code = "free_quota_exceeded";
err.detail = detail;
return err;
}
const code = String(detail.code || detail.message || "");
if (/token_bound/i.test(code)) {
const err = new Error("token_bound_other_user");
err.code = "token_bound_other_user";
err.detail = detail;
return err;
}
detail = detail.message || detail.code || JSON.stringify(detail);
}
const s = String(detail || "");
if (/token_bound/i.test(s)) {
const err = new Error("token_bound_other_user");
err.code = "token_bound_other_user";
return err;
}
return new Error(s || "\u8bf7\u6c42\u5931\u8d25");
}
async function _crq(path, method, payload) {
const base = CLOUD_API_BASE.replace(/\/$/, "");
const url = base + (path.startsWith("/") ? path : "/" + path);
const headers = { Accept: "application/json", "Content-Type": "application/json" };
const token = getCloudToken();
if (token && !state.tokenBoundBlocked) headers.Authorization = "Bearer " + token;
const luid = resolveLearningUserId();
if (luid) headers["x-learning-user-id"] = luid;
let bodyObj = payload;
if (bodyObj && typeof bodyObj === "object") {
bodyObj = Object.assign({}, bodyObj);
if (path !== "/api/rjpx/lease" && state.cloudLease && bodyObj.lease == null) {
bodyObj.lease = state.cloudLease;
}
if (path === "/api/rjpx/lease" && state.tokenBoundBlocked) {
bodyObj.token = "";
}
}
const res = await _gt(url, {
method: method || "POST",
headers: headers,
data: bodyObj == null ? null : JSON.stringify(bodyObj),
timeout: 60000,
});
let data = {};
const text = String(res.text || "").trim();
if (text) {
try {
data = JSON.parse(text);
} catch (_) {
data = { message: text.slice(0, 200) };
}
}
if (res.status < 200 || res.status >= 300) {
throw parseCloudErrorDetail(data, res.status);
}
return data;
}
async function _cl(force) {
const now = Math.floor(Date.now() / 1000);
const liveUid = resolveLearningUserId();
if (!force && state.cloudLease && state.cloudLeaseExp - now > 60) {
const cached = storeGet(K.cloudLease, null);
const boundUid = cached && typeof cached === "object" ? String(cached.boundUid || "").trim() : "";
if (!(liveUid && boundUid && liveUid !== boundUid)) return true;
invalidateCloudAuthCache();
}
if (!force) {
const cached = loadCachedLease();
if (cached) {
updateCloudPanelUI();
return true;
}
}
const luid = liveUid || resolveLearningUserId();
const postLease = async (tokenOverride) =>
_crq("/api/rjpx/lease", "POST", {
learning_user_id: luid || "",
userid: luid || "",
token: tokenOverride != null ? tokenOverride : state.tokenBoundBlocked ? "" : getCloudToken(),
company_id: String(state.companyId || detectCompanyId() || ""),
});
let data;
try {
data = await postLease();
state.tokenBoundBlocked = false;
} catch (e) {
if (String(e && e.code ? e.code : e.message || e).includes("token_bound")) {
state.tokenBoundBlocked = true;
invalidateCloudAuthCache();
state.cloudTier = "free";
state.cloudProExpireAt = 0;
log("\u5f53\u524d Pro Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u8d26\u53f7\uff0c\u672c\u8d26\u53f7\u6309\u514d\u8d39\u4f53\u9a8c");
showPanelDialog({
title: "Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u8d26\u53f7",
message: "\u8be5 Pro Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u5b66\u4e60\u8d26\u53f7\uff0c\u4e0d\u80fd\u5728\u672c\u8d26\u53f7\u4f7f\u7528\u3002\u672c\u8d26\u53f7\u5c06\u6309\u514d\u8d39\u4f53\u9a8c\u989d\u5ea6\u5b66\u4e60\u3002",
okText: "\u77e5\u9053\u4e86",
});
data = await postLease("");
} else {
throw e;
}
}
applyLeasePayload(data);
if (!state.cloudLeaseExp) state.cloudLeaseExp = now + 600;
return !!state.cloudLease;
}
async function _es(context) {
await _cl(false);
let data;
try {
data = await _crq("/api/rjpx/study/engine/start", "POST", {
lease: state.cloudLease,
kind: "vod",
context: context || {},
config: {},
});
} catch (e) {
if (String(e && e.code ? e.code : e.message || e).includes("token_bound")) {
state.tokenBoundBlocked = true;
invalidateCloudAuthCache();
await _cl(true);
throw new Error("token_bound_other_user");
}
throw e;
}
if (data.free_used_videos != null) state.freeUsedVideos = Number(data.free_used_videos) || 0;
if (data.free_video_limit != null) state.freeVideoLimit = Number(data.free_video_limit) || 2;
if (data.tier) state.cloudTier = String(data.tier);
if (data.proBuyUrl) state.proBuyUrl = String(data.proBuyUrl).trim() || PRO_BUY_URL;
persistLeaseCacheExtra({
freeUsedVideos: state.freeUsedVideos,
freeVideoLimit: state.freeVideoLimit,
tier: state.cloudTier,
});
updateCloudPanelUI();
return data;
}
async function _estep(sessionId, event, lastResult) {
await _cl(false);
return _crq("/api/rjpx/study/engine/step", "POST", {
lease: state.cloudLease,
session_id: String(sessionId || ""),
event: String(event || "tick"),
last_result: lastResult == null ? null : lastResult,
});
}
function handleFreeQuotaExceeded(detail) {
const lim = Number(
(detail && (detail.free_video_limit ?? detail.freeVideoLimit)) != null
? detail.free_video_limit ?? detail.freeVideoLimit
: state.freeVideoLimit || 2
);
const usedRaw = detail && (detail.free_used_videos ?? detail.freeUsedVideos);
const used = usedRaw != null ? Number(usedRaw) : Number(state.freeUsedVideos || 0);
state.cloudTier = "free";
state.freeVideoLimit = lim || 2;
state.freeUsedVideos = Math.max(used || 0, state.freeVideoLimit);
persistLeaseCacheExtra({
tier: "free",
freeUsedVideos: state.freeUsedVideos,
freeVideoLimit: state.freeVideoLimit,
});
log("\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff08" + state.freeUsedVideos + "/" + state.freeVideoLimit + "\uff09");
updateCloudPanelUI();
showFreeQuotaDialog();
}
async function _fm() {
try {
const cfg = await _crq("/api/rjpx/client-config", "GET", null);
if (cfg.proBuyUrl) state.proBuyUrl = String(cfg.proBuyUrl).trim() || PRO_BUY_URL;
if (cfg.finishTip) state.finishTip = String(cfg.finishTip);
if (cfg.panelNotice) state.panelNotice = String(cfg.panelNotice);
if (cfg.freeVideoLimit != null) state.freeVideoLimit = Number(cfg.freeVideoLimit) || 2;
updatePanelNoticeUI();
updateCloudPanelUI();
} catch (_) {}
try {
const res = await _gt(CLOUD_API_BASE.replace(/\/$/, "") + "/api/rjpx/panel-notice", {
method: "GET",
headers: { Accept: "text/plain" },
timeout: 15000,
});
if (res.status >= 200 && res.status < 300) {
const t = String(res.text || "").trim();
if (t) {
state.panelNotice = t;
updatePanelNoticeUI();
}
}
} catch (_) {}
}
function updatePanelNoticeUI() {
const el = document.getElementById("rjpx-ann-text");
if (el) el.textContent = state.panelNotice || PANEL_NOTICE;
}
function updateCloudPanelUI() {
const tierEl = document.getElementById("rjpx-cloud-tier");
const freeEl = document.getElementById("rjpx-cloud-free");
const freeLabel = document.getElementById("rjpx-cloud-free-label");
const tokenInput = document.getElementById("rjpx-cloud-token");
const t = String(state.cloudTier || "").toLowerCase();
if (tierEl) {
if (state.tokenBoundBlocked) tierEl.textContent = "\u514d\u8d39\u4f53\u9a8c\uff08Token \u5df2\u7ed1\u5176\u4ed6\u53f7\uff09";
else tierEl.textContent = t === "pro" ? "Pro \u4f1a\u5458" : t === "free" ? "\u514d\u8d39\u4f53\u9a8c" : "\u672a\u6821\u9a8c";
}
if (freeLabel && freeEl) {
if (t === "pro" && !state.tokenBoundBlocked) {
freeLabel.textContent = "Pro \u5230\u671f";
freeEl.textContent = formatProExpireText(state.cloudProExpireAt);
} else {
freeLabel.textContent = "\u514d\u8d39\u4f53\u9a8c\uff08" + (state.freeVideoLimit || 2) + " \u95e8\u8bfe\uff09";
freeEl.textContent = (state.freeUsedVideos || 0) + "/" + (state.freeVideoLimit || 2);
}
}
if (tokenInput && tokenInput !== document.activeElement) {
tokenInput.value = getCloudToken();
}
}
function detectCompanyId() {
const m = location.href.match(/\/(?:px|user|login|home)\/\w+\/(\d+)/i);
if (m) return m[1];
const a = document.querySelector('a[href*="/px/index/"]');
if (a) {
const m2 = String(a.href).match(/\/px\/index\/(\d+)/);
if (m2) return m2[1];
}
return "196";
}
function subjectKey(stageId, sid) {
return String(stageId || "").trim() + ":" + String(sid || "").trim();
}
function stageNameOf(stageId) {
const id = String(stageId || "").trim();
if (id === "3") return "\u5c0f\u5b66";
if (id === "4") return "\u521d\u4e2d";
if (id === "6") return "\u4e49\u6559";
return id ? "\u5b66\u6bb5" + id : "\u5b66\u6bb5";
}
function resolveStageId(preferred) {
const p = String(preferred || "").trim();
if (p && p !== "0") return p;
const page = getPageStageId();
if (page) return page;
if (state.stages && state.stages.length) return String(state.stages[0].id || "").trim();
return "";
}
function parseStagesFromHtml(html) {
const doc = typeof html === "string" ? new DOMParser().parseFromString(html, "text/html") : html;
const sel = doc && doc.getElementById ? doc.getElementById("stage") : null;
if (!sel) return [];
return [...sel.options]
.map((o) => ({ id: String(o.value || "").trim(), name: String(o.textContent || "").trim() }))
.filter((x) => x.id && x.id !== "0");
}
function stagesFromLimitMap(limitMap) {
if (!limitMap || typeof limitMap !== "object") return [];
return Object.keys(limitMap)
.filter((k) => /^\d+$/.test(k) && Array.isArray(limitMap[k]) && limitMap[k].length)
.map((id) => ({ id: String(id), name: stageNameOf(id) }))
.sort((a, b) => Number(a.id) - Number(b.id));
}
function parseSubjectsFromHtml(html) {
const doc = typeof html === "string" ? new DOMParser().parseFromString(html, "text/html") : html;
const sel = doc && doc.getElementById ? doc.getElementById("sid") : null;
if (!sel) return [];
return [...sel.options]
.map((o) => ({ id: String(o.value || "").trim(), name: String(o.textContent || "").trim() }))
.filter((x) => x.id);
}
function findSubjectByKey(keyOrSid) {
const raw = String(keyOrSid || "").trim();
if (!raw) return null;
const list = state.subjects || [];
return (
list.find((s) => s.key === raw) ||
list.find((s) => s.id === raw && String(s.stageId) === String(getPageStageId() || "")) ||
list.find((s) => s.id === raw) ||
null
);
}
function resolveSubjectRef(keyOrSid) {
const hit = findSubjectByKey(keyOrSid);
if (hit) return hit;
const raw = String(keyOrSid || "").trim();
if (!raw) return null;
if (raw.indexOf(":") >= 0) {
const parts = raw.split(":");
const stageId = parts[0];
const sid = parts.slice(1).join(":");
return {
key: raw,
id: sid,
stageId,
stageName: "",
name: sid,
label: sid,
};
}
const stageId = resolveStageId();
return {
key: subjectKey(stageId, raw),
id: raw,
stageId,
stageName: stageNameOf(stageId),
name: raw,
label: raw,
};
}
function lessonMatchesSelected(les, selectedKey) {
if (!selectedKey) return true;
const k = subjectKey(les.stageId, les.subjectId);
if (k === String(selectedKey)) return true;
if (String(les.subjectId) === String(selectedKey)) return true;
return false;
}
async function fetchChangeSubjectLimitMap() {
const url =
"https://wp.pep.com.cn/web/index.php?/user/changeSubjectLimit/" + state.companyId + "/";
const res = await _gm(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
Accept: "application/json, text/javascript, */*; q=0.01",
"X-Requested-With": "XMLHttpRequest",
},
data: "ajax=1",
timeout: 30000,
});
if (res.status < 200 || res.status >= 400) throw new Error("HTTP " + res.status);
let data = null;
try {
data = JSON.parse(String(res.responseText || ""));
} catch (_) {
throw new Error("\u5b66\u79d1\u5217\u8868\u89e3\u6790\u5931\u8d25");
}
if (data && Number(data.errno) === -1) throw new Error(String(data.errmsg || "\u767b\u5f55\u5931\u6548"));
return data && typeof data === "object" ? data : {};
}
function normalizeSectionText(el) {
return String(el ? el.textContent : "")
.replace(/\s+/g, " ")
.trim();
}
function resolveSectionForRow(tr) {
let cur = tr;
while (cur) {
const cell = [...cur.children].find((td) => td.classList && td.classList.contains("txt_pxkc_xk"));
if (cell) {
return {
section: normalizeSectionText(cell.querySelector("h4")),
hours: normalizeSectionText(cell.querySelector("h5")),
};
}
cur = cur.previousElementSibling;
}
const table = tr && tr.closest("table");
const cell = table && table.querySelector("td.txt_pxkc_xk");
if (cell) {
return {
section: normalizeSectionText(cell.querySelector("h4")),
hours: normalizeSectionText(cell.querySelector("h5")),
};
}
return { section: "", hours: "" };
}
function isCreditLesson(section, hours) {
const s = String(section || "");
const h = String(hours || "");
if (/\u4e0d\u8ba1\u5b66\u65f6|\u4e0d\u8ba1\u8bfe\u65f6/.test(h)) return false;
if (/\u9009\u5b66/.test(s)) return false;
return true;
}
function parseLessonsFromHtml(html, companyId, subjectId, subjectName, stageId, stageName) {
const doc = new DOMParser().parseFromString(html, "text/html");
const out = [];
const seen = new Set();
doc.querySelectorAll(".con_table_pxkc_gztb2020b table").forEach((table) => {
table.querySelectorAll("tr").forEach((tr) => {
const titleEl = tr.querySelector("td.txt_pxkc_pxxx h6") || tr.querySelector("h6");
const link = tr.querySelector('a[href*="/px/entryRoom/"]');
if (!titleEl || !link) return;
const { section, hours } = resolveSectionForRow(tr);
if (!isCreditLesson(section, hours)) return; // 不计学时/选学不进课单
const href = link.href || link.getAttribute("href") || "";
const m = href.match(/entryRoom\/(\d+)\/(\d+)\/(\d)/);
if (!m) return;
const lessonId = m[2];
if (seen.has(lessonId)) return;
seen.add(lessonId);
const spans = [...tr.querySelectorAll("td.txt_pxkc_pxxx p span")];
const teacherEl =
spans.filter((s) => !s.classList.contains("showtime_lesson")).pop() || spans[spans.length - 1];
const roomType = m[3];
const img = link.querySelector("img");
const imgSrc = img ? img.getAttribute("src") || "" : "";
let status = "\u53ef\u5b66";
if (/btn_wks/i.test(imgSrc)) status = "\u672a\u5f00\u59cb";
else if (/btn_gkhf/i.test(imgSrc)) status = "\u56de\u653e";
else if (/btn_jrpx/i.test(imgSrc)) status = "\u8fdb\u5165";
const isReplay = roomType === "2" || /btn_gkhf/i.test(imgSrc);
out.push({
id: String(lessonId),
companyId: String(m[1] || companyId),
subjectId: String(subjectId || ""),
subjectName: subjectName || "",
stageId: String(stageId || ""),
stageName: stageName || "",
roomType: String(roomType),
title: titleEl.textContent.trim() || "\u8bfe\u65f6 " + lessonId,
section: section,
hours: hours,
teacher: teacherEl ? teacherEl.textContent.trim() : "",
href: href.startsWith("http") ? href : "https://wp.pep.com.cn" + href,
status,
isReplay,
countsCredit: true,
});
});
});
return out;
}
function parseShowStuSections(html) {
const doc = new DOMParser().parseFromString(html, "text/html");
const sections = [];
doc.querySelectorAll('a[href*="/user/showStuOne/"]').forEach((a) => {
const href = a.href || "";
const m = href.match(/showStuOne\/(\d+)\/(\d+)\/(\d)/);
if (!m) return;
const ul = a.closest("ul");
const titleLi = ul ? ul.querySelector("li[title], li.ell") : null;
const title = (titleLi && (titleLi.getAttribute("title") || titleLi.textContent)) || "";
const timeText = (a.textContent || "").trim();
const pctMatch = (ul ? ul.textContent : "").match(/(\d+)%/);
sections.push({
stuOneId: m[2],
kind: m[3],
title: String(title).replace(/\s+/g, " ").trim(),
watchedText: timeText,
watchedMs: parseHms(timeText),
percent: pctMatch ? +pctMatch[1] : 0,
href,
});
});
const remainders = [];
doc.querySelectorAll(".con_xxsj_gztb2020b p").forEach((p) => {
const t = p.textContent.replace(/\s+/g, " ").trim();
if (t) remainders.push(t);
});
return { sections, remainders };
}
function parseShowStuOneLessons(html, stuOneId) {
const doc = new DOMParser().parseFromString(html, "text/html");
const lessons = [];
doc.querySelectorAll(".list_hftj_xstj_wp2022 ul.clearfix").forEach((ul) => {
const lis = [...ul.querySelectorAll("li")].map((li) => li.textContent.trim());
if (lis.length < 2) return;
const mm = lis[1].match(/([\d:]+)\s*\/\s*([\d:]+)/);
const watchedMs = mm ? parseHms(mm[1]) : 0;
const durMs = mm ? parseHms(mm[2]) : 0;
lessons.push({
stuOneId: String(stuOneId || ""),
title: lis[0],
watchedMs,
totalMs: durMs,
percent: durMs ? Math.min(100, Math.round((watchedMs / durMs) * 100)) : 0,
});
});
return lessons;
}
async function fetchHtml(pathOrUrl) {
let url = pathOrUrl.startsWith("http")
? pathOrUrl
: "https://wp.pep.com.cn/web/index.php?" + (pathOrUrl.startsWith("/") ? pathOrUrl : "/" + pathOrUrl);
url += (url.indexOf("?") >= 0 ? "&" : "?") + "_=" + Date.now();
const res = await _gt(url, {
headers: {
"Cache-Control": "no-cache",
Pragma: "no-cache",
},
});
if (res.status < 200 || res.status >= 400) throw new Error("HTTP " + res.status);
return res.text;
}
async function loadAllSubjectsCourses() {
state.companyId = detectCompanyId();
const indexHtml = await fetchHtml("/px/index/" + state.companyId);
let limitMap = null;
try {
limitMap = await fetchChangeSubjectLimitMap();
} catch (e) {
log("\u62c9\u53d6\u5b66\u6bb5\u5b66\u79d1\u6620\u5c04\u5931\u8d25\uff0c\u6539\u7528\u9875\u9762\u5b66\u79d1");
}
let stages = parseStagesFromHtml(document.documentElement.outerHTML);
if (!stages.length) stages = parseStagesFromHtml(indexHtml);
const mapStages = stagesFromLimitMap(limitMap);
if (mapStages.length) {
const byId = new Map(stages.map((s) => [s.id, s]));
mapStages.forEach((s) => {
if (!byId.has(s.id)) byId.set(s.id, s);
else if (!byId.get(s.id).name) byId.set(s.id, s);
});
stages = [...byId.values()].sort((a, b) => Number(a.id) - Number(b.id));
}
if (!stages.length) {
const m = String(location.href || "").match(/\/(?:px\/index|user\/showStu)\/\d+(?:\/\d+)?\/\d+\/(\d+)/);
const stageId = (m && m[1]) || getPageStageId();
if (stageId) stages = [{ id: String(stageId), name: stageNameOf(stageId) }];
}
state.stages = stages;
const pageStageId = resolveStageId(getPageStageId());
const pageSubjects = parseSubjectsFromHtml(document.documentElement.outerHTML);
const pageSubjectsIndex = parseSubjectsFromHtml(indexHtml);
const subjects = [];
const seenKey = new Set();
for (const st of stages) {
let rows = [];
const bag = limitMap && limitMap[st.id];
if (Array.isArray(bag) && bag.length) {
rows = bag
.map((ee) => ({
id: String((ee && (ee.sid != null ? ee.sid : ee.id)) || "").trim(),
name: String((ee && (ee.subjectname || ee.name)) || "").trim(),
}))
.filter((x) => x.id);
} else if (String(st.id) === String(pageStageId) || stages.length === 1) {
const src = pageSubjects.length ? pageSubjects : pageSubjectsIndex;
rows = src.map((x) => ({ id: x.id, name: x.name }));
}
if (!rows.length) {
log("\u5b66\u6bb5\u300c" + st.name + "\u300d\u6682\u65e0\u5b66\u79d1");
continue;
}
for (const row of rows) {
const key = subjectKey(st.id, row.id);
if (seenKey.has(key)) continue;
seenKey.add(key);
subjects.push({
id: row.id,
stageId: String(st.id),
stageName: st.name || stageNameOf(st.id),
name: row.name || row.id,
key,
label: stages.length > 1 ? (st.name || stageNameOf(st.id)) + " · " + (row.name || row.id) : row.name || row.id,
});
}
}
if (!subjects.length) {
const fallback = pageSubjects.length ? pageSubjects : pageSubjectsIndex;
const st = stages[0] || { id: pageStageId || "", name: stageNameOf(pageStageId) };
fallback.forEach((x) => {
const key = subjectKey(st.id, x.id);
subjects.push({
id: x.id,
stageId: String(st.id),
stageName: st.name,
name: x.name,
key,
label: x.name,
});
});
}
if (!subjects.length) {
const st = resolveStageId(pageStageId);
subjects.push({
id: "1",
stageId: st,
stageName: stageNameOf(st),
name: "\u5f53\u524d",
key: subjectKey(st, "1"),
label: "\u5f53\u524d",
});
}
state.subjects = subjects;
const savedSid = String(storeGet(K.subject, "") || "");
const pageKey = getPageSubjectKey();
const pick =
findSubjectByKey(savedSid) ||
findSubjectByKey(pageKey) ||
subjects[0];
state.selectedSid = pick ? pick.key : subjects[0].key;
const all = [];
for (const sub of subjects) {
const stageId = resolveStageId(sub.stageId);
if (!stageId) {
log("\u52a0\u8f7d\u300c" + sub.label + "\u300d\u5931\u8d25\uff1a\u5b66\u6bb5\u672a\u77e5");
continue;
}
try {
const html = await fetchHtml(
"/px/index/" + state.companyId + "/" + sub.id + "/" + stageId
);
const lessons = parseLessonsFromHtml(
html,
state.companyId,
sub.id,
sub.name,
stageId,
sub.stageName || stageNameOf(stageId)
);
all.push(...lessons);
log("\u5df2\u52a0\u8f7d\u300c" + sub.label + "\u300d" + lessons.length + " \u95e8\u8bfe");
} catch (e) {
log("\u52a0\u8f7d\u300c" + sub.label + "\u300d\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5");
}
await sleep(200);
}
state.lessons = all;
const allow = new Set(all.map((x) => String(x.id)));
saveQueue(loadQueue().filter((id) => allow.has(String(id))));
await _rp({});
pruneQueueLocked();
const markN = Object.keys(loadLocalProg()).length;
const doneN = loadDoneSet().size;
if (markN || doneN) {
log("\u5df2\u5b66\u6807\u8bb0\u5df2\u4fdd\u7559");
}
return all;
}
function platformLessonByTitle(title, sidHint) {
const t = String(title || "").trim();
if (!t) return null;
const findIn = (list) => {
if (!Array.isArray(list) || !list.length) return null;
let hit = list.find((x) => x.title === t);
if (hit) return hit;
return (
list.find((x) => x.title && (x.title.indexOf(t) >= 0 || t.indexOf(x.title) >= 0)) || null
);
};
const hint = String(sidHint || "").trim();
const bags = state.platformLessonsBySid || {};
const hintKeys = [];
if (hint) {
hintKeys.push(hint);
const ref = resolveSubjectRef(hint);
if (ref && ref.key && hintKeys.indexOf(ref.key) < 0) hintKeys.push(ref.key);
if (ref && ref.id && hintKeys.indexOf(ref.id) < 0) hintKeys.push(ref.id);
if (hint.indexOf(":") < 0) {
const staged = subjectKey(getPageStageId() || (ref && ref.stageId) || "", hint);
if (staged !== ":" && hintKeys.indexOf(staged) < 0) hintKeys.push(staged);
}
}
for (const k of hintKeys) {
if (bags[k]) {
const hit = findIn(bags[k]);
if (hit) return hit;
}
}
let hit = findIn(state.platformLessons || []);
if (hit) return hit;
for (const sid of Object.keys(bags)) {
if (hintKeys.indexOf(sid) >= 0) continue;
hit = findIn(bags[sid]);
if (hit) return hit;
}
return null;
}
function findLocalProgForLesson(lessonId, title) {
const local = loadLocalProg();
const id = String(lessonId || "");
if (id && local[id]) return { key: id, prog: local[id] };
const t = String(title || "").trim();
const matchIn = (bag) => {
if (!bag || typeof bag !== "object" || !t) return null;
for (const [key, prog] of Object.entries(bag)) {
if (!prog || !prog.title) continue;
if (prog.title === t || prog.title.indexOf(t) >= 0 || t.indexOf(prog.title) >= 0) {
return { key, prog };
}
}
return null;
};
let hit = matchIn(local);
if (!hit) {
hit = matchIn(storeGet(K.localProg, null));
}
if (hit) {
if (id && hit.key !== id) {
local[id] = Object.assign({}, hit.prog, { title: t || hit.prog.title });
saveLocalProg(local);
return { key: id, prog: local[id] };
}
return { key: hit.key, prog: hit.prog };
}
return null;
}
function isLessonBrushed(loc, lessonId) {
const done = loadDoneSet();
if (lessonId && done.has(String(lessonId))) return true;
try {
const legacyDone = storeGet(K.done, []);
if (lessonId && Array.isArray(legacyDone) && legacyDone.map(String).includes(String(lessonId))) {
return true;
}
} catch (_) {}
if (!loc) return false;
return !!(loc.done || isWatchDone(loc.submittedMs, loc.totalMs));
}
async function _rp(opts) {
opts = opts || {};
const keyOrSid = String(
opts.sid || state.selectedSid || (state.subjects[0] && state.subjects[0].key) || ""
).trim();
if (!keyOrSid) return;
const sub = resolveSubjectRef(keyOrSid);
const sid = String((sub && sub.id) || keyOrSid.split(":").pop() || "").trim();
const stageId = resolveStageId((sub && sub.stageId) || "");
if (!stageId) {
if (!quiet) log("\u540c\u6b65\u5e73\u53f0\u5df2\u5b66\u5931\u8d25\uff1a\u5b66\u6bb5\u672a\u77e5");
return;
}
const cacheKey = subjectKey(stageId, sid);
const quiet = !!opts.quiet;
const pageOnly = !!opts.pageOnly;
if (!sid) return;
if (state.platformProgressInflight[cacheKey]) {
return state.platformProgressInflight[cacheKey];
}
state.platformProgressInflight[cacheKey] = (async () => {
try {
const html = await fetchHtml(
"/user/showStu/" + state.companyId + "/0/" + sid + "/" + stageId
);
const parsed = parseShowStuSections(html);
const platLessons = [];
for (const sec of parsed.sections.slice(0, 20)) {
try {
const oneHtml = await fetchHtml(
"/user/showStuOne/" + state.companyId + "/" + sec.stuOneId + "/" + sec.kind
);
parseShowStuOneLessons(oneHtml, sec.stuOneId).forEach((L) => {
platLessons.push(
Object.assign({}, L, { section: sec.title, subjectId: sid, stageId: stageId })
);
});
await sleep(80);
} catch (e) {
if (!quiet) log("\u540c\u6b65\u300c" + sec.title + "\u300d\u5931\u8d25");
}
}
state.platformLessonsBySid[cacheKey] = platLessons;
state.platformSectionsBySid[cacheKey] = parsed.sections;
const isPanelSid =
cacheKey === String(state.selectedSid || "") ||
sid === String(state.selectedSid || "") ||
(sub && sub.key === String(state.selectedSid || ""));
if (isPanelSid) {
state.platformSections = parsed.sections;
state.platformLessons = platLessons;
}
const withWatch = platLessons.filter((x) => x.watchedMs > 0).length;
if (!quiet) {
log("\u5e73\u53f0\u5df2\u5b66\u5df2\u540c\u6b65\uff08" + withWatch + " \u95e8\u6709\u8fdb\u5ea6\uff09");
if (parsed.remainders.length) log("\u5e73\u53f0\u63d0\u793a\uff1a" + parsed.remainders.slice(0, 3).join("\uff1b"));
}
if (!pageOnly && isPanelSid) {
renderChapterPreview();
pruneQueueLocked();
renderCourseList();
updatePanel();
} else {
_sp();
}
} catch (e) {
if (!quiet) log("\u540c\u6b65\u5e73\u53f0\u5df2\u5b66\u5931\u8d25\uff0c\u8bf7\u5237\u65b0\u91cd\u8bd5");
} finally {
delete state.platformProgressInflight[cacheKey];
}
})();
return state.platformProgressInflight[cacheKey];
}
function getPageStageId() {
const el = document.getElementById("stage");
if (el && String(el.value || "").trim()) return String(el.value).trim();
const mIndex = String(location.href || "").match(/\/px\/index\/\d+\/\d+\/(\d+)/i);
if (mIndex) return mIndex[1];
const mStu = String(location.href || "").match(/\/user\/showStu\/\d+\/\d+\/\d+\/(\d+)/i);
if (mStu) return mStu[1];
return "";
}
function getPageSubjectId() {
const el = document.getElementById("sid");
return el ? String(el.value || "").trim() : "";
}
function getPageSubjectKey() {
const sid = getPageSubjectId();
const stage = getPageStageId();
if (sid && stage) return subjectKey(stage, sid);
return sid || "";
}
function randT() {
return ("0" + Date.now() + String(Math.floor(Math.random() * 1e6)).padStart(6, "0")).slice(0, 18);
}
function doubleEncodeURIComponent(s) {
return encodeURIComponent(encodeURIComponent(String(s || "")));
}
async function _al(url) {
const headers = {
Accept: "*/*",
Referer: "https://bjpep.gensee.com/",
};
if (IS_GENSEE) {
try {
const xhrRes = await new Promise((resolve, reject) => {
try {
const xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.onload = () => resolve({ status: xhr.status, text: String(xhr.responseText || "") });
xhr.onerror = () => reject(new Error("xhr network error"));
xhr.send();
} catch (e) {
reject(e);
}
});
return { status: xhrRes.status, text: xhrRes.text, via: "xhr" };
} catch (_) {}
try {
const res = await fetch(url, {
method: "GET",
mode: "cors",
credentials: "omit",
headers: headers,
});
return { status: res.status, text: await res.text(), via: "fetch" };
} catch (_) {}
}
const res = await _gt(url, {
headers: Object.assign({ Origin: "https://bjpep.gensee.com" }, headers),
});
return { status: res.status, text: res.text, via: "gm" };
}
function parseSessionId(text) {
const raw = String(text || "").trim();
if (!raw) return "";
const first = raw.split(/\s+/)[0];
if (/^\d{5,}$/.test(first)) return first;
const m = raw.match(/\b(\d{6,})\b/);
return m ? m[1] : "";
}
function normalizeAlbHost(alb) {
const a = String(alb || "").trim();
if (a && a.indexOf(".") > 0 && a.indexOf(" ") < 0) return a;
return "albcdnvodproxy.gensee.com";
}
async function requestLicenseSession(opts) {
const alb = normalizeAlbHost(opts.alb);
const siteId = opts.siteId;
const uid = opts.uid;
const confid = opts.confid;
const uname = String(opts.uname || "").trim();
const userEncList = [];
if (uname) {
userEncList.push(doubleEncodeURIComponent(uname));
userEncList.push(encodeURIComponent(uname));
}
userEncList.push(null);
let lastDetail = "";
for (const userEnc of userEncList) {
let url =
"https://" +
alb +
"/albcmd/license?siteid=" +
encodeURIComponent(siteId) +
"&userid=" +
encodeURIComponent(uid) +
"&confid=" +
encodeURIComponent(confid);
if (userEnc) url += "&username=" + userEnc;
url += "&onlylogin=0&type=0&t=" + randT();
const res = await _al(url);
const sid = parseSessionId(res.text);
if (sid) return { sessionid: sid, alb: alb, raw: res.text, via: res.via };
lastDetail =
"via=" +
res.via +
" HTTP " +
res.status +
" body=" +
String(res.text || "")
.replace(/\s+/g, " ")
.slice(0, 120);
}
throw new Error("license \u672a\u8fd4\u56de sessionid\uff08" + lastDetail + "\uff09");
}
function parsePlayPage(html, playUrl) {
const owner =
(html.match(/ownerid=["']([a-f0-9]+)["']/i) || [])[1] ||
(playUrl.match(/play-([a-f0-9]+)/i) || [])[1] ||
"";
const code = (html.match(/code=["']([a-f0-9]+)["']/i) || [])[1] || "";
const site = (html.match(/\bsite=["']([^"']+)["']/i) || [])[1] || "bjpep.gensee.com:443";
const uname =
(html.match(/\buname=["']([^"']*)["']/i) || [])[1] ||
(html.match(/name="nickName"[^>]*value="([^"]*)"/i) || [])[1] ||
(html.match(/value="([^"]*)"[^>]*name="nickName"/i) || [])[1] ||
"";
const uid =
(html.match(/var\s+quid\s*=\s*"(\d+)"/) || [])[1] ||
(html.match(/id="uid"[^>]*value="(\d+)"/) || [])[1] ||
(html.match(/value="(\d+)"[^>]*id="uid"/) || [])[1] ||
(playUrl.match(/[?&]uid=(\d+)/) || [])[1] ||
"";
return { ownerid: owner, code, site, uname, uid, playUrl };
}
function parseSdkVod(jsText) {
const tid = (jsText.match(/tid\s*:\s*"([a-f0-9]+)"/i) || [])[1] || "";
const siteId = (jsText.match(/siteId\s*:\s*"(\d+)"/i) || [])[1] || "183681";
const nameM =
jsText.match(/userId\s*:\s*"(\d+)"\s*,\s*userName\s*:\s*"([^"]*)"/i) ||
jsText.match(/userId:"(\d+)",\s*userName:"([^"]*)"/i);
const userId = (nameM && nameM[1]) || (jsText.match(/userId\s*:\s*"(\d+)"/i) || [])[1] || "";
const userName =
(nameM && nameM[2]) || (jsText.match(/userName\s*:\s*"([^"]*)"/i) || [])[1] || "";
const confId = (jsText.match(/confId\s*:\s*"([a-f0-9]+)"/i) || [])[1] || "";
const xmlUrl = (jsText.match(/xmlUrl\s*=\s*'([^']+)'/) || [])[1] || "";
let alb = "albcdnvodproxy.gensee.com";
const albLit = jsText.match(/isNotEmpty\("([^"]*albcdn[^"]*)"\)/i);
if (albLit && albLit[1]) alb = albLit[1];
else {
const assigns = [...jsText.matchAll(/alb\s*=\s*"([^"]*)"/g)].map((m) => m[1]).filter(Boolean);
if (assigns.length) alb = assigns[assigns.length - 1];
}
return { tid, siteId, userId, userName, confId, xmlUrl, alb };
}
async function resolveDurationMs(xmlUrl) {
if (!xmlUrl) return 0;
const jsonUrl = xmlUrl.replace(/record\d+\.xml(\?.*)?$/i, "hls/record.json");
try {
const res = await _gt(jsonUrl + (jsonUrl.includes("?") ? "&" : "?") + "t=" + randT());
const m = res.text.match(/"duration"\s*:\s*"([\d.]+)"/);
if (m) return Math.round(parseFloat(m[1]) * 1000);
} catch (_) {}
return 0;
}
function updateJob(patch) {
const cur = storeGet(K.job, null) || {};
const next = Object.assign({}, cur, patch, { updatedAt: Date.now() });
storeSet(K.job, next);
return next;
}
async function pageFetchText(url, opts) {
opts = opts || {};
try {
const res = await fetch(url, {
method: opts.method || "GET",
credentials: "include",
headers: opts.headers || {},
});
const text = await res.text();
return { status: res.status, text, finalUrl: res.url || url, headers: "" };
} catch (e) {
return _gt(url, opts);
}
}
async function openGenseeSessionFromHtml(playHtml, playUrl) {
const play = parsePlayPage(playHtml, playUrl);
if (!play.ownerid) throw new Error("\u64ad\u653e\u9875\u7f3a\u5c11 ownerid");
if (!play.code) throw new Error("\u64ad\u653e\u9875\u7f3a\u5c11 code\uff08\u9274\u6743\u7801\uff09\uff0c\u8bf7\u786e\u8ba4\u56de\u653e\u5df2\u5f00\u653e");
const sdkUrl =
"https://bjpep.gensee.com/sdk/site/sdk/gs/h5/vod?" +
new URLSearchParams({
widgetid: "_GS_WIDGET_1_" + Date.now(),
site: play.site || "bjpep.gensee.com:443",
ctx: "gensee",
ownerid: play.ownerid,
code: play.code,
uid: "",
uname: play.uname || "",
py: "1",
loop: "false",
lang: "zh_CN",
userdata: "",
hlsmode: "false",
gsver: "2",
}).toString();
let sdkRes;
if (IS_GENSEE) {
sdkRes = await pageFetchText(sdkUrl, {
headers: { Accept: "*/*", Referer: playUrl },
});
} else {
sdkRes = await _gt(sdkUrl, {
headers: {
Accept: "*/*",
Referer: playUrl,
"Accept-Language": "zh-CN,zh;q=0.9",
},
});
}
const sdk = parseSdkVod(sdkRes.text);
const uid = sdk.userId || play.uid;
const uname = sdk.userName || play.uname;
const siteId = sdk.siteId || "183681";
const confid = sdk.confId || play.ownerid;
if (!sdk.tid) {
const preview = String(sdkRes.text || "")
.replace(/\s+/g, " ")
.slice(0, 180);
throw new Error(
"SDK \u672a\u8fd4\u56de tid\uff08HTTP " + sdkRes.status + " len=" + (sdkRes.text || "").length + "\uff09" + (preview ? " · " + preview : "")
);
}
if (!uid) throw new Error("\u7f3a\u5c11 userid");
let durationMs = await resolveDurationMs(sdk.xmlUrl);
if (!durationMs && typeof platformLessonByTitle === "function") {
try {
const plat = platformLessonByTitle(play.uname);
if (plat && plat.totalMs) durationMs = plat.totalMs;
} catch (_) {}
}
if (!durationMs) durationMs = 60 * 60 * 1000;
const alb = normalizeAlbHost(sdk.alb);
const sess0 = await requestLicenseSession({
alb: alb,
siteId: siteId,
uid: uid,
confid: confid,
uname: uname,
});
return {
alb: sess0.alb,
siteId,
uid,
uname,
confid,
tid: sdk.tid,
sessionid: sess0.sessionid,
durationMs,
playUrl,
ownerid: play.ownerid,
};
}
async function openGenseeSession(lesson) {
const entry =
lesson.href ||
"https://wp.pep.com.cn/web/index.php?/px/entryRoom/" +
(lesson.companyId || state.companyId) +
"/" +
lesson.id +
"/2";
const entryRes = await _gt(entry, {
headers: { Accept: "text/html,application/xhtml+xml", "Upgrade-Insecure-Requests": "1" },
});
let playUrl = entryRes.finalUrl || "";
const loc = (entryRes.headers.match(/location:\s*(\S+)/i) || [])[1];
if (loc) playUrl = loc.trim();
if (!/gensee\.com/i.test(playUrl)) {
const m = entryRes.text.match(/https?:\/\/bjpep\.gensee\.com\/[^"'<\s]+/);
if (m) playUrl = m[0];
}
if (!/gensee\.com/i.test(playUrl)) throw new Error("entryRoom \u672a\u8df3\u8f6c\u5230\u5c55\u89c6\u56de\u653e");
const playRes = await _gt(playUrl, {
headers: {
Accept: "text/html,application/xhtml+xml",
Referer: "https://wp.pep.com.cn/",
},
});
return openGenseeSessionFromHtml(playRes.text, playUrl);
}
async function licenseHeartbeat(sess, opts) {
const t = Math.max(0, Math.floor(opts.t || 0));
const d = Math.max(0, Math.floor(opts.d || 0));
const v = Math.max(0, Math.floor(opts.v || sess.durationMs || 0));
const pos = Math.max(0, Math.floor(opts.pos != null ? opts.pos : Math.min(d, v)));
const other = "tid=" + sess.tid + ",t=" + t + ",d=" + d + ",v=" + v + ",sc=2,pos=" + pos;
let url =
"https://" +
normalizeAlbHost(sess.alb) +
"/albcmd/license?siteid=" +
encodeURIComponent(sess.siteId) +
"&userid=" +
encodeURIComponent(sess.uid) +
"&confid=" +
encodeURIComponent(sess.confid) +
"&onlylogin=0&type=1&sessionid=" +
encodeURIComponent(sess.sessionid) +
"&needlicense=1&other=" +
encodeURIComponent(other);
const uname = String(sess.uname || "").trim();
if (uname) url += "&username=" + encodeURIComponent(uname);
url += "&t=" + randT();
return _al(url);
}
const DONE_RATIO = 1; // 刷满片长 100%(d >= v)
function doneThresholdMs(totalMs) {
const v = Math.max(0, Math.floor(Number(totalMs) || 0));
return Math.min(v, Math.ceil(v * DONE_RATIO));
}
function isWatchDone(submittedMs, totalMs) {
const v = Math.max(0, Math.floor(Number(totalMs) || 0));
if (!v) return false;
return Math.floor(Number(submittedMs) || 0) >= doneThresholdMs(v);
}
function isSectionFull(sectionTitle, sidHint) {
const title = String(sectionTitle || "").trim();
if (!title) return false;
const bags = [];
const hint = String(sidHint || "").trim();
const bySid = state.platformSectionsBySid || {};
if (hint && bySid[hint]) bags.push(bySid[hint]);
if (state.platformSections) bags.push(state.platformSections);
Object.keys(bySid).forEach((sid) => {
if (hint && sid === hint) return;
bags.push(bySid[sid]);
});
for (const sections of bags) {
if (!Array.isArray(sections)) continue;
const sec = sections.find((s) => String(s.title || "").trim() === title);
if (sec && Number(sec.percent) >= 100) return true;
}
return false;
}
function isPlatformFull(plat, sidHint) {
if (!plat) return false;
const total = Number(plat.totalMs) || 0;
const watched = Number(plat.watchedMs) || 0;
if (total > 0) {
if (isWatchDone(watched, total)) return true;
if (watched > 0 && total - watched <= 2000) return true;
}
if (plat.section && isSectionFull(plat.section, sidHint || plat.subjectId)) return true;
return false;
}
function isCreditSectionName(name) {
const s = String(name || "");
if (/\u9009\u5b66|\u4e0d\u8ba1\u5b66\u65f6|\u4e0d\u8ba1\u8bfe\u65f6/.test(s)) return false;
return true;
}
function formatLessonLearnStatus(plat, loc, lessonId, sidHint) {
if (isPlatformFull(plat, sidHint)) {
return {
kind: "plat",
tag: "\u5e73\u53f0\u5df2\u5b66\u66f4\u65b0",
text: "\u5e73\u53f0\u5df2\u5b66\u66f4\u65b0 " + formatHms(plat.watchedMs) + "/" + formatHms(plat.totalMs),
};
}
if (isLessonBrushed(loc, lessonId)) {
return { kind: "pending", tag: "\u5df2\u5b66\u5f85\u5e73\u53f0\u66f4\u65b0", text: "\u5df2\u5b66\u5f85\u5e73\u53f0\u66f4\u65b0" };
}
return { kind: "todo", tag: "\u8bfe\u7a0b\u5f85\u5b66\u4e60", text: "\u8bfe\u7a0b\u5f85\u5b66\u4e60" };
}
function selectedSubjectSupportsReplay() {
const sid = String(state.selectedSid || "");
const list = (state.lessons || []).filter(
(x) => lessonMatchesSelected(x, sid) && x.countsCredit !== false
);
if (!list.length) return false;
return list.some((x) => x.isReplay && x.status !== "\u672a\u5f00\u59cb");
}
function updateCourseTip() {
const tip = document.getElementById("rjpx-course-tip");
if (!tip) return;
tip.hidden = selectedSubjectSupportsReplay();
}
function blockEntryRoomClick(e) {
e.preventDefault();
e.stopPropagation();
return false;
}
function restoreEntryRoomLink(link) {
if (!link || !link.classList.contains("rjpx-entry-locked")) return;
const raw = link.getAttribute("data-rjpx-orig-html");
if (raw != null) link.innerHTML = raw;
link.classList.remove("rjpx-entry-locked", "rjpx-entry-pending", "rjpx-entry-done");
link.removeAttribute("data-rjpx-orig-html");
link.removeAttribute("data-rjpx-lock");
link.removeAttribute("title");
link.removeAttribute("aria-disabled");
link.removeEventListener("click", blockEntryRoomClick, true);
}
function lockEntryRoomLink(link, mode) {
if (!link || (mode !== "pending" && mode !== "done")) return;
const label = mode === "done" ? "\u5df2\u5b66\u5b8c" : "\u5df2\u5b66\u5b8c\u5f85\u5e73\u53f0\u66f4\u65b0";
const prev = link.getAttribute("data-rjpx-lock");
if (prev === mode && link.classList.contains("rjpx-entry-locked")) {
const el = link.querySelector(".rjpx-entry-label");
if (el && el.textContent === label) return;
}
if (!link.getAttribute("data-rjpx-orig-html")) {
link.setAttribute("data-rjpx-orig-html", link.innerHTML);
}
link.classList.add("rjpx-entry-locked");
link.classList.toggle("rjpx-entry-pending", mode === "pending");
link.classList.toggle("rjpx-entry-done", mode === "done");
link.setAttribute("data-rjpx-lock", mode);
link.setAttribute("title", label);
link.setAttribute("aria-disabled", "true");
link.innerHTML = '' + label + "";
link.removeEventListener("click", blockEntryRoomClick, true);
link.addEventListener("click", blockEntryRoomClick, true);
}
function _pb() {
if (!IS_PEP || IN_IFRAME) return;
state.patchingPageButtons = true;
try {
const pageSid = getPageSubjectId();
const pageStage = getPageStageId();
const pageKey = getPageSubjectKey();
const links = document.querySelectorAll('a[href*="/px/entryRoom/"]');
links.forEach((link) => {
if (link.closest("#rjpx-auto-panel")) return;
const href = link.href || link.getAttribute("href") || "";
const m = href.match(/entryRoom\/(\d+)\/(\d+)\/(\d)/);
if (!m) return;
const lessonId = String(m[2]);
const roomType = String(m[3]);
const img = link.querySelector("img");
const imgSrc = img ? img.getAttribute("src") || "" : "";
const txt = String(link.textContent || "").replace(/\s+/g, "");
const isReplayBtn =
roomType === "2" ||
/btn_gkhf/i.test(imgSrc) ||
/\u89c2\u770b\u56de\u653e|\u5df2\u5b66\u5b8c/.test(txt) ||
link.classList.contains("rjpx-entry-locked");
if (!isReplayBtn) {
restoreEntryRoomLink(link);
return;
}
const tr = link.closest("tr") || link.closest("li") || link.parentElement;
const titleEl =
(tr && (tr.querySelector("td.txt_pxkc_pxxx h6") || tr.querySelector("h6"))) || null;
const les = (state.lessons || []).find((x) => String(x.id) === lessonId);
const title = (titleEl && titleEl.textContent.trim()) || (les && les.title) || "";
const sidHint =
(les && subjectKey(les.stageId, les.subjectId)) ||
pageKey ||
subjectKey(pageStage, pageSid) ||
pageSid;
const found = findLocalProgForLesson(lessonId, title);
const loc = found ? found.prog : null;
const plat = platformLessonByTitle(title, sidHint);
const st = formatLessonLearnStatus(plat, loc, lessonId, sidHint);
if (st.kind === "plat") lockEntryRoomLink(link, "done");
else if (st.kind === "pending") lockEntryRoomLink(link, "pending");
else restoreEntryRoomLink(link);
});
} finally {
state.patchingPageButtons = false;
}
}
let pageBtnObserver = null;
let pageBtnPatchTimer = 0;
let lastPageSidSynced = "";
let pageSidEnsureTimer = 0;
function _sp() {
if (pageBtnPatchTimer) clearTimeout(pageBtnPatchTimer);
pageBtnPatchTimer = setTimeout(() => {
pageBtnPatchTimer = 0;
_pb();
}, 80);
}
async function _eps(force) {
if (!IS_PEP || IN_IFRAME) return;
const key = getPageSubjectKey() || getPageSubjectId();
if (!key) {
_sp();
return;
}
const cached =
(state.platformLessonsBySid && state.platformLessonsBySid[key]) ||
(state.platformLessonsBySid &&
state.platformLessonsBySid[subjectKey(getPageStageId(), getPageSubjectId())]);
if (cached) _sp();
if (!force && cached && lastPageSidSynced === key) return;
try {
await _rp({ quiet: true, pageOnly: true, sid: key });
lastPageSidSynced = key;
} catch (_) {}
_sp();
}
function scheduleEnsurePageSubjectProgress(force) {
if (pageSidEnsureTimer) clearTimeout(pageSidEnsureTimer);
pageSidEnsureTimer = setTimeout(() => {
pageSidEnsureTimer = 0;
_eps(!!force);
}, 120);
}
function _bw() {
if (!IS_PEP || IN_IFRAME) return;
const onPageCourseChange = () => {
lastPageSidSynced = "";
scheduleEnsurePageSubjectProgress(true);
};
const sidEl = document.getElementById("sid");
if (sidEl && !sidEl.dataset.rjpxSidBound) {
sidEl.dataset.rjpxSidBound = "1";
sidEl.addEventListener("change", onPageCourseChange);
sidEl.addEventListener("input", onPageCourseChange);
}
const stageEl = document.getElementById("stage");
if (stageEl && !stageEl.dataset.rjpxStageBound) {
stageEl.dataset.rjpxStageBound = "1";
stageEl.addEventListener("change", onPageCourseChange);
stageEl.addEventListener("input", onPageCourseChange);
}
}
function ensureCoursePageButtonObserver() {
if (!IS_PEP || IN_IFRAME || pageBtnObserver || !document.body) return;
_bw();
pageBtnObserver = new MutationObserver(() => {
if (state.patchingPageButtons) return;
_bw();
const key = getPageSubjectKey() || getPageSubjectId();
if (key && key !== lastPageSidSynced) {
scheduleEnsurePageSubjectProgress(true);
} else {
_sp();
}
});
pageBtnObserver.observe(document.body, { childList: true, subtree: true });
}
function pruneQueueLocked() {
const local = loadLocalProg();
const done = loadDoneSet();
const next = loadQueue().filter((id) => {
const sid = String(id);
if (done.has(sid)) return false;
const loc = local[sid];
if (loc && (loc.done || isWatchDone(loc.submittedMs, loc.totalMs))) return false;
const les = state.lessons.find((x) => String(x.id) === sid);
if (!les || !les.isReplay || les.status === "\u672a\u5f00\u59cb") return false;
if (isPlatformFull(platformLessonByTitle(les.title))) return false;
return true;
});
saveQueue(next);
return next;
}
async function _ve(sess, lessonId, title, onProgress) {
const total = Math.max(0, Math.floor(Number(sess.durationMs) || 0));
const need = doneThresholdMs(total);
const local = loadLocalProg();
const prev = local[lessonId] || {};
let d = Math.min(Number(prev.submittedMs) || 0, total);
const started = Date.now();
const peixunUid = String(resolveLearningUserId() || "").trim();
const genseeUid = String(sess.uid || "").trim();
let startRes;
try {
startRes = await _es({
duration_ms: total,
submitted_ms: d,
userid: genseeUid,
learning_user_id: peixunUid,
peixun_uid: peixunUid,
confid: String(sess.confid || ""),
tid: String(sess.tid || ""),
sessionid: String(sess.sessionid || ""),
lesson_id: String(lessonId),
title: String(title || ""),
});
} catch (e) {
if (String(e && e.code ? e.code : e.message || e).includes("free_quota")) {
handleFreeQuotaExceeded(e.detail || null);
throw new Error("\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro");
}
if (String(e && e.code ? e.code : e.message || e).includes("token_bound")) {
throw new Error("\u5f53\u524d Pro Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u8d26\u53f7");
}
throw new Error("\u5b66\u4e60\u542f\u52a8\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5");
}
let sessionId = String(startRes.session_id || "");
let cmd = startRes.command || {};
const startLog = String(startRes.log || "").trim();
if (startLog && !/^\u5f00\u59cb\u5b66\u4e60/.test(startLog)) log(startLog);
logCourse("\u5f00\u59cb\u5b66\u4e60", title);
const maxSteps = 800;
for (let i = 0; i < maxSteps; i++) {
const job = storeGet(K.job, null);
if (job && job.status === "cancel") throw new Error("\u5df2\u505c\u6b62");
if (IS_PEP && !state.enabled) throw new Error("\u5df2\u505c\u6b62");
const type = String(cmd.type || "");
if (type === "done") {
local[lessonId] = {
title: title,
submittedMs: need,
totalMs: total,
needMs: need,
updatedAt: Date.now(),
done: true,
finishedAt: Date.now(),
};
saveLocalProg(local);
const done = loadDoneSet();
done.add(String(lessonId));
saveDoneSet(done);
if (typeof onProgress === "function") onProgress(need, total);
logCourse("\u672c\u8bfe\u5df2\u5b66\u5b8c", title);
return { total, need, elapsedSec: Math.round((Date.now() - started) / 1000) };
}
if (type === "failed") {
throw new Error(String(cmd.message || "\u5b66\u4e60\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5"));
}
if (type === "license_heartbeat") {
const waitMs = Math.max(0, Number(cmd.wait_ms) || 0);
if (waitMs) await sleep(waitMs);
const job2 = storeGet(K.job, null);
if (job2 && job2.status === "cancel") throw new Error("\u5df2\u505c\u6b62");
if (IS_PEP && !state.enabled) throw new Error("\u5df2\u505c\u6b62");
let res;
try {
res = await licenseHeartbeat(sess, {
t: Math.max(0, Math.floor(Number(cmd.t) || 0)),
d: Math.max(0, Math.floor(Number(cmd.d) || 0)),
v: Math.max(0, Math.floor(Number(cmd.v) || total)),
pos: Math.max(
0,
Math.floor(Number(cmd.pos != null ? cmd.pos : cmd.d) || 0)
),
});
} catch (_) {
res = { status: 0, text: "" };
}
const body = String(res.text || "").trim();
const ok = body === "0";
const nextD = Math.max(0, Math.floor(Number(cmd.d) || 0));
if (ok) {
d = Math.min(need, nextD);
local[lessonId] = {
title: title,
submittedMs: d,
totalMs: total,
needMs: need,
updatedAt: Date.now(),
done: isWatchDone(d, total),
};
saveLocalProg(local);
if (typeof onProgress === "function") onProgress(d, total);
}
const step = await _estep(sessionId, "heartbeat_result", {
body: body,
http_status: Number(res.status) || 0,
ok: ok,
});
sessionId = String(step.session_id || sessionId);
const stepLog = String(step.log || "").trim();
if (stepLog && !/^\u5f00\u59cb\u5b66\u4e60/.test(stepLog) && !/^\u672c\u8bfe\u5df2\u5b66\u5b8c/.test(stepLog) && !/^\u786e\u8ba4\u5b66\u5b8c/.test(stepLog)) {
log(stepLog);
}
cmd = step.command || {};
continue;
}
const step = await _estep(sessionId, "tick", null);
sessionId = String(step.session_id || sessionId);
const stepLog = String(step.log || "").trim();
if (stepLog && !/^\u5f00\u59cb\u5b66\u4e60/.test(stepLog) && !/^\u672c\u8bfe\u5df2\u5b66\u5b8c/.test(stepLog) && !/^\u786e\u8ba4\u5b66\u5b8c/.test(stepLog)) {
log(stepLog);
}
cmd = step.command || {};
}
throw new Error("\u5b66\u4e60\u8d85\u65f6\uff0c\u8bf7\u91cd\u8bd5");
}
async function submitLessonByApi(lesson) {
state.current = lesson;
storeSet(K.current, lesson);
state.currentTask = "\u5f00\u59cb\u5b66\u4e60\uff1a" + lesson.title;
updatePanel();
const entry =
lesson.href ||
"https://wp.pep.com.cn/web/index.php?/px/entryRoom/" +
(lesson.companyId || state.companyId) +
"/" +
lesson.id +
"/2";
const jobId = String(Date.now()) + "_" + lesson.id;
updateJob({
id: jobId,
lessonId: String(lesson.id),
title: lesson.title,
entryUrl: entry,
openMode: "iframe",
status: "queued",
submittedMs: 0,
totalMs: 0,
error: "",
msg: "\u51c6\u5907\u5b66\u4e60\u73af\u5883",
});
openPlayContext(entry);
const started = Date.now();
while (true) {
if (!state.enabled) {
updateJob({ status: "cancel", msg: "\u7528\u6237\u505c\u6b62" });
removePlayIframe();
throw new Error("\u5df2\u505c\u6b62");
}
await sleep(1000);
const job = storeGet(K.job, null);
if (!job || job.id !== jobId) continue;
if (job.submittedMs || job.totalMs) {
state.currentTask =
"\u5b66\u4e60\u4e2d " + lesson.title + " " + formatHms(job.submittedMs || 0) + "/" + formatHms(job.totalMs || 0);
updatePanel();
} else if (job.msg) {
state.currentTask = job.msg;
updatePanel();
}
if (job.status === "done") {
removePlayIframe();
state.current = null;
storeSet(K.current, null);
state.currentTask = "";
updatePanel();
return;
}
if (job.status === "error") {
removePlayIframe();
const err = String(job.error || "\u5b66\u4e60\u5931\u8d25");
if (/free_quota|\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c/.test(err)) {
handleFreeQuotaExceeded(null);
throw new Error("\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro");
}
throw new Error(/\u5df2\u505c\u6b62/.test(err) ? err : "\u5b66\u4e60\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5");
}
if (Date.now() - started > 3 * 60 * 60 * 1000) {
removePlayIframe();
throw new Error("\u5355\u8bfe\u8d85\u65f6\uff0c\u8bf7\u91cd\u8bd5");
}
}
}
async function bootGenseeApiWorker() {
if (!/\/webcast\/site\/vod\/play-/i.test(location.pathname)) return;
let job = null;
for (let i = 0; i < 40; i++) {
job = storeGet(K.job, null);
if (job && (job.status === "queued" || job.status === "running")) break;
await sleep(250);
}
if (!job || (job.status !== "queued" && job.status !== "running")) return;
const stopMedia = () => {
try {
document.querySelectorAll("video,audio").forEach((v) => {
v.muted = true;
v.pause();
v.removeAttribute("autoplay");
});
} catch (_) {}
const cont = document.getElementById("continueBtn");
if (cont) {
try {
cont.click();
} catch (_) {}
}
const idle = document.getElementById("idleModal");
if (idle) idle.style.display = "none";
const stopBg = document.querySelector(".stop-reporting-bg");
if (stopBg) stopBg.style.display = "none";
const volIgnore = document.querySelector(".autoplay-volume-ignore");
if (volIgnore) {
try {
volIgnore.click();
} catch (_) {}
}
};
updateJob({
status: "running",
msg: "\u51c6\u5907\u5b66\u4e60\u73af\u5883…",
});
let mediaTimer = null;
try {
for (let i = 0; i < 40; i++) {
if (document.documentElement && /ownerid=/i.test(document.documentElement.innerHTML)) break;
await sleep(250);
}
let sdkText = "";
const waitUntil = Date.now() + 25000;
while (Date.now() < waitUntil) {
const sdkScript = [...document.scripts].find((s) => /\/sdk\/site\/sdk\/gs\/h5\/vod/i.test(s.src || ""));
if (sdkScript && sdkScript.src && !sdkText) {
try {
const res = await pageFetchText(sdkScript.src, {
headers: { Accept: "*/*", Referer: location.href },
});
if (/tid\s*:\s*"[a-f0-9]+"/i.test(res.text || "")) sdkText = res.text || "";
} catch (_) {}
}
if (stolenLicense.sessionid && sdkText) break;
if (stolenLicense.sessionid && Date.now() > waitUntil - 5000) break;
await sleep(400);
}
const play = parsePlayPage(document.documentElement.innerHTML, location.href);
const sdk = sdkText ? parseSdkVod(sdkText) : null;
let tid = (sdk && sdk.tid) || stolenLicense.otherTid || "";
let uid = (sdk && sdk.userId) || play.uid || stolenLicense.userid || "";
let uname = (sdk && sdk.userName) || play.uname || "";
let siteId = (sdk && sdk.siteId) || stolenLicense.siteid || "183681";
let confid = (sdk && sdk.confId) || play.ownerid || stolenLicense.confid || "";
let alb = normalizeAlbHost((sdk && sdk.alb) || stolenLicense.alb);
let durationMs = sdk && sdk.xmlUrl ? await resolveDurationMs(sdk.xmlUrl) : 0;
if (!durationMs) durationMs = 60 * 60 * 1000;
if (!tid) {
for (let i = 0; i < 20 && !stolenLicense.otherTid; i++) await sleep(300);
tid = stolenLicense.otherTid || tid;
}
if (!tid) {
const built = await openGenseeSessionFromHtml(document.documentElement.innerHTML, location.href);
if (built && built.sessionid && built.tid) {
mediaTimer = setInterval(stopMedia, 2000);
stopMedia();
updateJob({
status: "running",
msg: "\u5b66\u4e60\u4e2d",
totalMs: built.durationMs,
submittedMs: 0,
});
const result = await _ve(
built,
job.lessonId,
job.title,
(d, total) => {
updateJob({
status: "running",
submittedMs: d,
totalMs: total,
msg: "\u5b66\u4e60\u4e2d " + formatHms(d) + "/" + formatHms(total),
});
}
);
updateJob({
status: "done",
submittedMs: result.total,
totalMs: result.total,
msg: "\u5b8c\u6210",
error: "",
});
if (mediaTimer) clearInterval(mediaTimer);
setTimeout(() => {
if (!IN_IFRAME) {
try {
window.close();
} catch (_) {}
}
}, 600);
return;
}
throw new Error("\u672a\u80fd\u51c6\u5907\u5b66\u4e60\u73af\u5883");
}
let sessionid = stolenLicense.sessionid;
if (!sessionid) {
updateJob({ status: "running", msg: "\u51c6\u5907\u5b66\u4e60\u73af\u5883…" });
if (!uid || !confid) {
throw new Error("\u672a\u80fd\u51c6\u5907\u5b66\u4e60\u73af\u5883");
}
const sess0 = await requestLicenseSession({
alb: alb,
siteId: siteId,
uid: uid,
confid: confid,
uname: uname,
});
sessionid = sess0.sessionid;
alb = sess0.alb || alb;
} else {
updateJob({
status: "running",
msg: "\u5b66\u4e60\u4e2d",
});
}
const sess = {
alb: alb,
siteId: siteId,
uid: uid,
uname: uname,
confid: confid,
tid: tid,
sessionid: sessionid,
durationMs: durationMs,
playUrl: location.href,
};
mediaTimer = setInterval(stopMedia, 2000);
stopMedia();
updateJob({
status: "running",
msg: "\u5b66\u4e60\u4e2d",
totalMs: sess.durationMs,
submittedMs: 0,
});
const result = await _ve(
sess,
job.lessonId,
job.title,
(d, total) => {
updateJob({
status: "running",
submittedMs: d,
totalMs: total,
msg: "\u5b66\u4e60\u4e2d " + formatHms(d) + "/" + formatHms(total),
});
}
);
updateJob({
status: "done",
submittedMs: result.total,
totalMs: result.total,
msg: "\u5b8c\u6210",
error: "",
});
if (mediaTimer) clearInterval(mediaTimer);
setTimeout(() => {
if (!IN_IFRAME) {
try {
window.close();
} catch (_) {}
}
}, 600);
} catch (e) {
if (mediaTimer) clearInterval(mediaTimer);
const raw = String(e && e.message ? e.message : e);
const friendly = /free_quota|\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c/.test(raw)
? "\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro"
: /\u5df2\u505c\u6b62/.test(raw)
? "\u5df2\u505c\u6b62"
: /\u8d85\u65f6/.test(raw)
? raw
: "\u5b66\u4e60\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5";
updateJob({
status: "error",
error: friendly,
msg: "\u5931\u8d25",
});
}
}
function pickNext() {
const q = loadQueue();
const done = loadDoneSet();
const local = loadLocalProg();
const map = new Map(state.lessons.map((x) => [String(x.id), x]));
for (const id of q) {
if (done.has(id)) continue;
const loc = local[id];
if (loc && (loc.done || isWatchDone(loc.submittedMs, loc.totalMs))) continue;
const les = map.get(String(id));
if (!les || !les.isReplay || les.status === "\u672a\u5f00\u59cb" || les.countsCredit === false) continue;
if (isPlatformFull(platformLessonByTitle(les.title))) continue;
return les;
}
return null;
}
async function bgLoop() {
if (state.bgBusy) return;
state.bgBusy = true;
try {
while (state.enabled) {
const next = pickNext();
if (!next) {
log("\u6240\u9009\u8bfe\u7a0b\u5df2\u5168\u90e8\u5b66\u5b8c");
state.enabled = false;
storeSet(K.enabled, false);
state.currentTask = "\u5df2\u5168\u90e8\u5b66\u5b8c";
updatePanel();
try {
showFinishTipDialog();
} catch (_) {}
break;
}
try {
await submitLessonByApi(next);
} catch (e) {
const msg = String(e && e.message ? e.message : e);
if (/free_quota|\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c/.test(msg)) {
log("\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c");
} else if (/\u5df2\u505c\u6b62/.test(msg)) {
log("\u5df2\u505c\u6b62");
} else {
log("\u5b66\u4e60\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5");
}
state.enabled = false;
storeSet(K.enabled, false);
state.currentTask = "\u51fa\u9519\u5df2\u6682\u505c";
break;
}
await sleep(1200);
renderCourseList();
renderChapterPreview();
updatePanel();
}
} finally {
state.bgBusy = false;
updatePanel();
}
}
async function startRun() {
if (state.enabled) return;
if (!loadQueue().length) {
log("\u8bf7\u5148\u52fe\u9009\u56de\u653e\u8bfe\u7a0b");
return;
}
try {
await _cl(true);
} catch (e) {
const msg = String(e && e.message ? e.message : e);
if (/free_quota|\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c/.test(msg)) {
handleFreeQuotaExceeded(e.detail || null);
return;
}
if (/token_bound/.test(msg)) {
log("\u5f53\u524d Pro Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u8d26\u53f7");
return;
}
log("\u4e91\u7aef\u6388\u6743\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5");
return;
}
if (
String(state.cloudTier || "").toLowerCase() !== "pro" &&
Number(state.freeUsedVideos || 0) >= Number(state.freeVideoLimit || 2)
) {
handleFreeQuotaExceeded({
free_video_limit: state.freeVideoLimit,
free_used_videos: Math.max(state.freeUsedVideos, state.freeVideoLimit),
});
return;
}
state.enabled = true;
storeSet(K.enabled, true);
log("\u5f00\u59cb\u961f\u5217\u5b66\u4e60");
updatePanel();
bgLoop();
}
function stopRun() {
state.enabled = false;
storeSet(K.enabled, false);
updateJob({ status: "cancel", msg: "\u7528\u6237\u505c\u6b62" });
removePlayIframe();
state.currentTask = "\u5df2\u505c\u6b62";
log("\u5df2\u505c\u6b62");
updatePanel();
}
function injectPanelStyles() {
if (document.getElementById("rjpx-panel-style")) return;
const st = document.createElement("style");
st.id = "rjpx-panel-style";
st.textContent =
"#rjpx-auto-panel{position:fixed;right:20px;top:70px;z-index:999999;width:412px;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}" +
"#rjpx-auto-panel.rjpx-min #rjpx-panel-body,#rjpx-auto-panel.rjpx-min #rjpx-panel-footer,#rjpx-auto-panel.rjpx-min .rjpx-footer-extra{display:none!important}" +
"#rjpx-panel-header{padding:6px 10px;background:linear-gradient(180deg,#f8ecd5,#f4e8cf);border-bottom:1px solid #e5dbc6;display:flex;justify-content:space-between;align-items:center;gap:8px;cursor:move;user-select:none}" +
"#rjpx-panel-brand{display:flex;align-items:center;gap:7px;min-width:0;flex:1}" +
"#rjpx-panel-logo{width:28px;height:28px;border-radius:7px;object-fit:cover;flex:0 0 auto;box-shadow:0 1px 3px rgba(15,23,42,.12);background:#fff}" +
"#rjpx-panel-title{font-size:12px;font-weight:900;color:#9a3412;line-height:1.2}" +
"#rjpx-panel-sub{margin-top:2px;display:flex;flex-wrap:wrap;gap:3px}" +
".rjpx-chip{font-size:10px;color:#7c2d12;background:rgba(255,255,255,.62);padding:1px 6px;border-radius:999px;border:1px solid rgba(180,83,9,.18);font-weight:700}" +
".rjpx-chip-em{color:#0f766e;background:rgba(236,253,245,.9);border-color:rgba(15,118,110,.28)}" +
".rjpx-ctl{border:none;background:#fff;color:#64748b;width:26px;height:26px;border-radius:999px;cursor:pointer;box-shadow:0 1px 2px rgba(15,23,42,.1);font-weight:900;font-size:16px;line-height:1;flex:0 0 auto}" +
"#rjpx-panel-body{padding:7px;max-height:min(78vh,720px);overflow:auto}" +
".rjpx-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px}" +
".rjpx-status-row{display:flex;justify-content:space-between;align-items:center;gap:8px}" +
".rjpx-progress-bar{height:6px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:6px}" +
".rjpx-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s}" +
".rjpx-tabbar{display:flex;gap:4px;margin-bottom:6px}" +
".rjpx-tab-btn{flex:1;border:1px solid #cbd5e1;background:#f8fafc;border-radius:8px;padding:5px 4px;cursor:pointer;font-size:11px;font-weight:700;color:#64748b}" +
".rjpx-tab-btn.active{background:#fff7ed;border-color:#fdba74;color:#9a3412}" +
".rjpx-pane{display:none}.rjpx-pane.active{display:block}" +
".rjpx-course-tip{margin:0 0 5px;padding:5px 7px;border-radius:8px;background:linear-gradient(180deg,#fffbeb,#fef3c7);border:1px solid #fcd34d;color:#92400e;font-size:11px;font-weight:700;line-height:1.35}" +
".rjpx-course-tip[hidden]{display:none!important}" +
".rjpx-course-toolbar{display:flex;gap:5px;margin-bottom:5px;align-items:center}" +
".rjpx-course-toolbar .rjpx-plan-select{flex:1;min-width:0;padding:3px 6px;font-size:11px;border-radius:7px}" +
".rjpx-btn-sm{flex:0 0 auto;border:1px solid #cbd5e1;background:#fff;color:#334155;border-radius:7px;padding:3px 8px;font-size:11px;font-weight:700;cursor:pointer;white-space:nowrap}" +
".rjpx-btn-sm:hover{border-color:#fdba74;color:#9a3412;background:#fff7ed}" +
"a.rjpx-entry-locked{pointer-events:none!important;cursor:not-allowed!important;text-decoration:none!important}" +
"a.rjpx-entry-locked .rjpx-entry-label{display:inline-flex;align-items:center;justify-content:center;min-width:88px;padding:6px 10px;border-radius:4px;font-size:12px;font-weight:700;line-height:1.2;color:#fff;box-sizing:border-box;white-space:nowrap}" +
"a.rjpx-entry-pending .rjpx-entry-label{background:#d97706}" +
"a.rjpx-entry-done .rjpx-entry-label{background:#64748b}" +
"#rjpx-course-list,#rjpx-chapter-preview,#rjpx-run-log{max-height:200px;overflow:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px}" +
".rjpx-item{display:flex;gap:7px;align-items:flex-start;padding:6px 7px;border-bottom:1px solid #eef2f7}" +
".rjpx-item-title{font-weight:700;line-height:1.35}.rjpx-item-meta{color:#64748b;font-size:11px;margin-top:2px}" +
".rjpx-tag{display:inline-block;padding:1px 6px;border-radius:999px;font-size:10px;font-weight:700;margin-left:4px;border:1px solid #e2e8f0}" +
".rjpx-tag-ok{background:#ecfdf5;color:#047857;border-color:#a7f3d0}.rjpx-tag-wait{background:#fff7ed;color:#c2410c;border-color:#fed7aa}" +
".rjpx-tag-done{background:#eff6ff;color:#1d4ed8;border-color:#bfdbfe}.rjpx-tag-local{background:#fef3c7;color:#92400e;border-color:#fcd34d}.rjpx-tag-plat{background:#ecfdf5;color:#047857;border-color:#a7f3d0}" +
".rjpx-meta-row{display:flex;align-items:center;gap:8px;margin:5px 0}.rjpx-meta-label{color:#64748b;font-weight:700;min-width:4.8em}" +
".rjpx-btn-row{display:flex;gap:6px}.rjpx-btn{flex:1;border:none;border-radius:10px;padding:8px 10px;font-weight:800;cursor:pointer}" +
".rjpx-btn-start{background:linear-gradient(135deg,#ea580c,#f59e0b);color:#fff}.rjpx-btn-stop{background:#fee2e2;color:#b91c1c}" +
".rjpx-btn-ghost{background:#fff;border:1px solid #cbd5e1;color:#334155}.rjpx-btn:disabled{opacity:.45;cursor:not-allowed}" +
".rjpx-plan-select{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:5px 8px;font-size:12px;background:#fff}" +
".rjpx-set-card{background:linear-gradient(180deg,#fffdf8,#fff);border:1px solid #e7d5b8;border-radius:12px;padding:10px;margin:0}" +
".rjpx-set-head{font-size:12px;font-weight:900;color:#9a3412;margin:0 0 8px;padding-bottom:6px;border-bottom:1px dashed #f0e0c8}" +
".rjpx-set-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:6px 0;padding:6px 8px;background:#f8fafc;border:1px solid #e8eef5;border-radius:9px}" +
".rjpx-set-label{color:#64748b;font-weight:700;font-size:11px;flex:0 0 auto}" +
".rjpx-set-val{font-weight:800;color:#0f172a;text-align:right;min-width:0}" +
".rjpx-set-token{display:flex;flex-direction:column;gap:6px;margin-top:8px}" +
".rjpx-set-actions{display:flex;gap:6px;flex-wrap:wrap}" +
".rjpx-set-tip{margin-top:8px;padding:7px 8px;border-radius:9px;background:#eff6ff;border:1px solid #bfdbfe;color:#1e40af;font-size:11px;line-height:1.45;font-weight:600}" +
".rjpx-ann{background:linear-gradient(180deg,#fffdf5,#fff7e6);border:1px solid #fcd34d;border-radius:11px;padding:8px 9px;color:#334155;line-height:1.42}" +
".rjpx-footer-extra{padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0}" +
"#rjpx-panel-footer{padding:6px 10px;background:#f1f5f9;border-top:1px solid #e2e8f0;color:#475569;font-size:11px}" +
".rjpx-log-line{padding:3px 4px;border-bottom:1px dashed #e2e8f0}.rjpx-empty{padding:14px 8px;text-align:center;color:#94a3b8;font-weight:800}" +
".rjpx-qq-row{display:flex;justify-content:space-between;align-items:center;margin-top:6px;gap:8px}" +
".rjpx-qq-btn{border:none;background:#ea580c;color:#fff;border-radius:8px;padding:6px 10px;font-weight:800;cursor:pointer}" +
".rjpx-btn-pro{border:none;background:linear-gradient(135deg,#0f766e,#14b8a6);color:#fff;border-radius:8px;padding:6px 10px;font-weight:800;cursor:pointer;white-space:nowrap}" +
".rjpx-token-input{width:100%;box-sizing:border-box;border:1px solid #cbd5e1;border-radius:8px;padding:6px 8px;font-size:12px}" +
".rjpx-hint{margin-top:4px;padding:6px 8px;border-radius:8px;background:#eff6ff;border:1px solid #bfdbfe;color:#1e40af;line-height:1.4}" +
".rjpx-chapter-course{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff}" +
".rjpx-chapter-title{padding:6px 9px;background:#fff7ed;border-bottom:1px solid #fed7aa;color:#9a3412;font-weight:700}" +
".rjpx-chapter-item{padding:5px 9px;display:flex;justify-content:space-between;gap:8px;font-size:12px;border-top:1px solid #f1f5f9}" +
"input.rjpx-num{width:56px;border:1px solid #cbd5e1;border-radius:8px;padding:4px 6px}" +
"#rjpx-dialog-mask{position:fixed;inset:0;z-index:1000001;background:rgba(15,23,42,.42);display:flex;align-items:center;justify-content:center;padding:20px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'PingFang SC','Microsoft YaHei',sans-serif}" +
"#rjpx-dialog-card{width:min(380px,92vw);background:#fff;border:1px solid #dbe4f0;border-radius:16px;box-shadow:0 20px 48px rgba(15,23,42,.22);overflow:hidden}" +
"#rjpx-dialog-head{padding:12px 14px;background:linear-gradient(180deg,#f8ecd5,#f4e8cf);border-bottom:1px solid #e5dbc6}" +
"#rjpx-dialog-title{font-size:15px;font-weight:900;color:#9a3412;margin:0}" +
"#rjpx-dialog-body{padding:14px 16px;color:#334155;font-size:13px;line-height:1.55;font-weight:600}" +
"#rjpx-dialog-link{display:block;margin-top:10px;padding:8px 10px;border-radius:9px;background:#f0fdfa;border:1px solid #99f6e4;color:#0f766e;font-size:12px;font-weight:700;line-height:1.4;word-break:break-all;text-decoration:none}" +
"#rjpx-dialog-link:hover{background:#ccfbf1}" +
"#rjpx-dialog-actions{padding:0 14px 14px;display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap}" +
"#rjpx-dialog-cancel{border:1px solid #cbd5e1;border-radius:10px;padding:8px 16px;font-weight:800;cursor:pointer;background:#fff;color:#475569;font-size:13px}" +
"#rjpx-dialog-ok{border:none;border-radius:10px;padding:8px 18px;font-weight:800;cursor:pointer;background:linear-gradient(135deg,#0f766e,#14b8a6);color:#fff;font-size:13px}";
document.documentElement.appendChild(st);
}
function closePanelDialog() {
const el = document.getElementById("rjpx-dialog-mask");
if (el) el.remove();
}
function showPanelDialog(opts) {
opts = opts || {};
injectPanelStyles();
closePanelDialog();
const link = String(opts.link || "").trim();
const hasCancel = !!opts.cancelText;
const mask = document.createElement("div");
mask.id = "rjpx-dialog-mask";
mask.innerHTML =
'' +
'
' +
escHtml(opts.title || "\u63d0\u793a") +
"
" +
'
' +
escHtml(opts.message || "") +
(link
? '
' +
escHtml(opts.linkText || link) +
""
: "") +
"
" +
'
' +
(hasCancel
? '"
: "") +
'
";
document.body.appendChild(mask);
const close = () => {
closePanelDialog();
if (typeof opts.onClose === "function") opts.onClose();
};
mask.addEventListener("click", (e) => {
if (e.target === mask) close();
});
const cancel = mask.querySelector("#rjpx-dialog-cancel");
if (cancel) cancel.addEventListener("click", close);
const linkEl = mask.querySelector("#rjpx-dialog-link");
if (linkEl) {
linkEl.addEventListener("click", (e) => {
e.preventDefault();
openUrl(link);
if (opts.closeOnLink !== false) close();
});
}
const ok = mask.querySelector("#rjpx-dialog-ok");
if (ok) {
ok.addEventListener("click", () => {
if (typeof opts.onOk === "function") opts.onOk();
close();
});
try {
ok.focus();
} catch (_) {}
}
}
function showFinishTipDialog() {
showPanelDialog({
title: "\u5168\u90e8\u5b66\u5b8c",
message: String(state.finishTip || FINISH_TIP),
okText: "\u77e5\u9053\u4e86",
});
}
function showFreeQuotaDialog() {
const url = String(state.proBuyUrl || PRO_BUY_URL).trim() || PRO_BUY_URL;
showPanelDialog({
title: "\u4f53\u9a8c\u5df2\u7528\u5b8c",
message: "\u514d\u8d39\u4f53\u9a8c\u5df2\u7528\u5b8c\uff0c\u7ee7\u7eed\u5904\u7406\u9700\u8981\u5347\u7ea7 Pro\u3002",
link: url,
cancelText: "\u5173\u95ed",
okText: "\u53bb\u5347\u7ea7 Pro",
onOk: () => openUrl(url),
});
}
function enableDrag(panel) {
const header = panel.querySelector("#rjpx-panel-header");
let dragging = false,
sx = 0,
sy = 0,
ol = 0,
ot = 0;
header.addEventListener("mousedown", (e) => {
if (e.target.closest(".rjpx-ctl")) return;
dragging = true;
const rect = panel.getBoundingClientRect();
sx = e.clientX;
sy = e.clientY;
ol = rect.left;
ot = rect.top;
e.preventDefault();
});
window.addEventListener("mousemove", (e) => {
if (!dragging) return;
panel.style.right = "auto";
panel.style.left = Math.max(0, ol + e.clientX - sx) + "px";
panel.style.top = Math.max(0, ot + e.clientY - sy) + "px";
});
window.addEventListener("mouseup", () => {
if (!dragging) return;
dragging = false;
storeSet(K.pos, {
left: parseInt(panel.style.left, 10) || 0,
top: parseInt(panel.style.top, 10) || 0,
});
});
}
function syncMinButton(panel) {
const btn = panel.querySelector("#rjpx-btn-min");
if (!btn) return;
const collapsed = panel.classList.contains("rjpx-min");
btn.textContent = collapsed ? "+" : "–";
btn.title = collapsed ? "\u5c55\u5f00" : "\u6536\u8d77";
}
function bindPanelEvents(panel) {
panel.querySelector("#rjpx-btn-min").addEventListener("click", () => {
const on = !panel.classList.contains("rjpx-min");
panel.classList.toggle("rjpx-min", on);
storeSet(K.collapsed, on);
syncMinButton(panel);
});
panel.querySelectorAll(".rjpx-tab-btn").forEach((btn) => {
btn.addEventListener("click", () => {
panel.querySelectorAll(".rjpx-tab-btn").forEach((b) => b.classList.toggle("active", b === btn));
panel.querySelectorAll(".rjpx-pane").forEach((p) => p.classList.toggle("active", p.dataset.pane === btn.dataset.tab));
});
});
panel.querySelector("#rjpx-select-replay").addEventListener("click", () => {
const sid = state.selectedSid;
const ids = state.lessons
.filter((x) => x.isReplay && x.status !== "\u672a\u5f00\u59cb" && x.countsCredit !== false)
.filter((x) => lessonMatchesSelected(x, sid))
.filter((x) => !isPlatformFull(platformLessonByTitle(x.title, subjectKey(x.stageId, x.subjectId)), subjectKey(x.stageId, x.subjectId)))
.filter((x) => {
const loc = loadLocalProg()[String(x.id)];
if (loadDoneSet().has(String(x.id))) return false;
if (loc && (loc.done || isWatchDone(loc.submittedMs, loc.totalMs))) return false;
return true;
})
.map((x) => x.id);
saveQueue(ids);
renderCourseList();
updatePanel();
log("\u5df2\u52fe\u9009\u53ef\u5237\u5fc5\u5b66\u56de\u653e " + ids.length + " \u8bfe\uff08\u5df2\u6392\u9664\u5e73\u53f0\u5df2\u5b66/\u5df2\u5b66\u5f85\u66f4\u65b0\uff09");
});
const clearLogBtn = panel.querySelector("#rjpx-clear-log");
if (clearLogBtn) {
clearLogBtn.addEventListener("click", () => {
clearRunLog();
log("\u65e5\u5fd7\u5df2\u6e05\u7406");
});
}
panel.querySelector("#rjpx-subject").addEventListener("change", async (e) => {
state.selectedSid = String(e.target.value || "");
storeSet(K.subject, state.selectedSid);
renderCourseList();
try {
await _rp({ quiet: true });
} catch (_) {}
renderChapterPreview();
_sp();
});
const tokenInput = panel.querySelector("#rjpx-cloud-token");
const tokenSave = panel.querySelector("#rjpx-token-save");
if (tokenSave && tokenInput) {
tokenSave.addEventListener("click", async () => {
setCloudToken(tokenInput.value);
state.tokenBoundBlocked = false;
invalidateCloudAuthCache();
try {
await _cl(true);
if (String(state.cloudTier || "").toLowerCase() === "pro") {
log("Pro \u6388\u6743\u5df2\u66f4\u65b0\uff0c\u5230\u671f\uff1a" + formatProExpireText(state.cloudProExpireAt));
} else if (state.tokenBoundBlocked) {
log("Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u8d26\u53f7\uff0c\u672c\u8d26\u53f7\u4e3a\u514d\u8d39\u4f53\u9a8c");
} else {
log("\u5f53\u524d\u4e3a\u514d\u8d39\u4f53\u9a8c " + state.freeUsedVideos + "/" + state.freeVideoLimit);
}
} catch (e) {
const msg = String(e && e.message ? e.message : e);
if (/token_bound/.test(msg)) log("Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u8d26\u53f7");
else log("\u6388\u6743\u6821\u9a8c\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u540e\u91cd\u8bd5");
}
updateCloudPanelUI();
});
}
const proBtn = panel.querySelector("#rjpx-open-pro");
if (proBtn) {
proBtn.addEventListener("click", () => openProBuyPage());
}
panel.querySelector("#rjpx-start").addEventListener("click", () => startRun());
panel.querySelector("#rjpx-stop").addEventListener("click", () => stopRun());
panel.querySelector("#rjpx-join-qq").addEventListener("click", () => openUrl(QQ_GROUP_LINK));
}
function renderSubjectSelect() {
const sel = document.getElementById("rjpx-subject");
if (!sel) return;
sel.innerHTML = state.subjects
.map((s) => {
const val = s.key || subjectKey(s.stageId, s.id) || s.id;
return (
'"
);
})
.join("");
}
function renderCourseList() {
const box = document.getElementById("rjpx-course-list");
updateCourseTip();
if (!box) return;
const q = new Set(loadQueue());
const sid = state.selectedSid;
const list = state.lessons.filter(
(x) => lessonMatchesSelected(x, sid) && x.countsCredit !== false
);
if (!list.length) {
box.innerHTML = '\u6682\u65e0\u8bfe\u65f6
';
_sp();
return;
}
box.innerHTML = list
.map((les) => {
const id = String(les.id);
const found = findLocalProgForLesson(id, les.title);
const loc = found ? found.prog : null;
const subKey = subjectKey(les.stageId, les.subjectId) || sid;
const plat = platformLessonByTitle(les.title, subKey);
const st = formatLessonLearnStatus(plat, loc, id, subKey);
const locked =
!les.isReplay || les.status === "\u672a\u5f00\u59cb" || st.kind === "plat" || st.kind === "pending";
let tagClass = "rjpx-tag-ok";
if (st.kind === "plat") tagClass = "rjpx-tag-plat";
else if (st.kind === "pending") tagClass = "rjpx-tag-done";
else if (st.kind === "todo") tagClass = "rjpx-tag-wait";
const tag = '' + st.tag + "";
const platTxt =
st.kind === "plat"
? formatHms(plat.watchedMs) + "/" + formatHms(plat.totalMs)
: st.kind === "pending"
? "\u5df2\u5b66\u5f85\u5e73\u53f0\u66f4\u65b0"
: "\u8bfe\u7a0b\u5f85\u5b66\u4e60";
return (
'"
);
})
.join("");
box.querySelectorAll("input[data-id]").forEach((inp) => {
inp.addEventListener("change", () => {
const other = loadQueue().filter((id) => {
const les = state.lessons.find((x) => x.id === id);
return les && !lessonMatchesSelected(les, sid);
});
const cur = [...box.querySelectorAll("input[data-id]:checked")].map((x) => x.getAttribute("data-id"));
saveQueue([...new Set(other.concat(cur))]);
updatePanel();
});
});
_sp();
}
function renderChapterPreview() {
const box = document.getElementById("rjpx-chapter-preview");
if (!box) return;
const local = loadLocalProg();
const creditSections = (state.platformSections || []).filter((sec) => isCreditSectionName(sec.title));
if (!creditSections.length && !Object.keys(local).length) {
box.innerHTML = '\u6682\u65e0\u5fc5\u5b66\u5e73\u53f0\u6570\u636e\uff08\u52a0\u8f7d\u8bfe\u5355\u65f6\u81ea\u52a8\u540c\u6b65\uff09
';
return;
}
const bySection = new Map();
(state.platformLessons || []).forEach((L) => {
if (!isCreditSectionName(L.section)) return;
const key = L.section || "\u672a\u5206\u7c7b";
if (!bySection.has(key)) bySection.set(key, []);
bySection.get(key).push(L);
});
let html = "";
creditSections.forEach((sec) => {
const items = bySection.get(sec.title) || [];
const body = items.length
? items
.map((L) => {
const found = findLocalProgForLesson("", L.title);
const loc = found ? found.prog : Object.values(local).find((x) => x && x.title === L.title);
const st = formatLessonLearnStatus(L, loc, "");
return (
'' +
escHtml(L.title) +
"" +
escHtml(st.text) +
"
"
);
})
.join("")
: '\u6c47\u603b' +
(Number(sec.percent) >= 100
? "\u5e73\u53f0\u5df2\u5b66\u66f4\u65b0"
: "\u8bfe\u7a0b\u5f85\u5b66\u4e60") +
"
";
html +=
'' +
escHtml(sec.title) +
"
" +
body +
"
";
});
box.innerHTML = html || '\u65e0\u5fc5\u5b66\u660e\u7ec6
';
}
function updatePanel() {
const q = loadQueue();
const done = loadDoneSet();
const local = loadLocalProg();
const doneCount = q.filter(
(id) => done.has(id) || (local[id] && (local[id].done || isWatchDone(local[id].submittedMs, local[id].totalMs)))
).length;
const total = q.length;
const pct = total ? Math.round((doneCount / total) * 100) : 0;
const set = (id, v) => {
const el = document.getElementById(id);
if (el) el.textContent = v;
};
set("rjpx-auto-status", state.enabled ? (state.bgBusy ? "\u5b66\u4e60\u4e2d" : "\u8fd0\u884c\u4e2d") : "\u5df2\u505c\u6b62");
set("rjpx-queue-done", String(doneCount));
set("rjpx-queue-total", String(total));
set("rjpx-queue-percent", pct + "%");
const bar = document.getElementById("rjpx-queue-progress");
if (bar) bar.style.width = pct + "%";
set(
"rjpx-current-task",
state.currentTask || (state.current && state.current.title) || (state.enabled ? "\u6392\u961f\u4e2d…" : "\u672a\u5f00\u59cb")
);
set("rjpx-queue-text", total ? "\u5df2\u9009 " + total + " · \u5f85\u5e73\u53f0\u66f4\u65b0 " + doneCount : "\u672a\u9009\u62e9");
const start = document.getElementById("rjpx-start");
const stop = document.getElementById("rjpx-stop");
if (start) start.disabled = !!state.enabled;
if (stop) stop.disabled = !state.enabled;
const logBox = document.getElementById("rjpx-run-log");
if (logBox) renderRunLog();
}
function createPanel() {
if (document.getElementById("rjpx-auto-panel")) return;
injectPanelStyles();
const panel = document.createElement("div");
panel.id = "rjpx-auto-panel";
panel.innerHTML =
'' +
'\u8fd0\u884c\u72b6\u6001\u5df2\u505c\u6b62
0/0 \u5df2\u5b66\u5f85\u5e73\u53f0\u66f4\u65b00%
' +
'
' +
'
\u57f9\u8bad\u672a\u5f00\u59cb · \u53ea\u5904\u7406\u56de\u653e\u8bfe\u7a0b
' +
'
\u5e73\u53f0\u5df2\u5b66\u66f4\u65b0\u9694\u65e5 / \u6ee1 12 \u5c0f\u65f6\u540e\u67e5\u770b
\u52a0\u8f7d\u8bfe\u5355\u540e\u81ea\u52a8\u540c\u6b65
' +
'
\u8fd0\u884c\u65e5\u5fd7
' +
'
\u4f1a\u5458\u4e0e\u6388\u6743
\u7528\u6237\u7c7b\u578b\u672a\u6821\u9a8c
\u514d\u8d39\u4f53\u9a8c0/2
\u5237\u5b8c\u63d0\u793a\uff1a\u5e73\u53f0\u8fdb\u5ea6\u901a\u5e38\u9694\u65e5\u66f4\u65b0\uff0c\u4e14\u81f3\u5c11\u5237\u5b8c 12 \u5c0f\u65f6\u540e\u624d\u80fd\u66f4\u65b0\u3002\u57f9\u8bad\u672a\u5f00\u59cb\u65f6\u53ea\u5904\u7406\u56de\u653e\u8bfe\u7a0b\u3002
' +
'
\u5f53\u524d\u4efb\u52a1\u672a\u5f00\u59cb
' +
'';
document.body.appendChild(panel);
const savedPos = storeGet(K.pos, null);
if (savedPos && savedPos.left != null) {
panel.style.right = "auto";
panel.style.left = savedPos.left + "px";
panel.style.top = (savedPos.top || 70) + "px";
}
if (storeGet(K.collapsed, false)) panel.classList.add("rjpx-min");
syncMinButton(panel);
enableDrag(panel);
bindPanelEvents(panel);
loadCachedLease();
updateCloudPanelUI();
updatePanel();
}
async function boot() {
if (IS_GENSEE) {
if (document.readyState === "loading") {
await new Promise((r) => document.addEventListener("DOMContentLoaded", r, { once: true }));
}
loadCachedLease();
state.cloudToken = getCloudToken();
await bootGenseeApiWorker();
return;
}
if (!IS_PEP) return;
await new Promise((resolve) => {
const tick = () => (document.body ? resolve() : setTimeout(tick, 100));
tick();
});
createPanel();
log("\u9762\u677f\u5df2\u5c31\u7eea");
_fm().catch(() => {});
state.panelRefreshing = true;
updatePanel();
try {
await loadAllSubjectsCourses();
renderSubjectSelect();
renderCourseList();
renderChapterPreview();
ensureCoursePageButtonObserver();
_bw();
await _eps(true);
} catch (e) {
log("\u521d\u59cb\u5316\u5931\u8d25\uff0c\u8bf7\u5237\u65b0\u91cd\u8bd5");
} finally {
state.panelRefreshing = false;
updatePanel();
}
if (storeGet(K.enabled, false)) {
state.enabled = true;
log("\u6062\u590d\u4e0a\u6b21\u672a\u5b8c\u6210\u961f\u5217");
try {
await _cl(false);
} catch (_) {}
bgLoop();
}
}
boot();
})();