// ==UserScript==
// @name 中铁八大员etledu学习助手
// @namespace https://www.wlxy.live/
// @version 1.0
// @author 柠檬真酸
// @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png
// @description 中国中铁现八大人员继续教育(ztbdystudent.etledu.com)学员端:勾选课时自动学习,照片可采集/上传或头像兜底,授权(免费体验1课时 / Pro不限),自由选择效率模式、1:1常速模式。
// @antifeature payment 免费体验1个课时,升级Pro不限课时
// @antifeature membership 需云端授权
// @match *://ztbdystudent.etledu.com/*
// @match *://*student*.etledu.com/*
// @match *://*.etledu.com/*
// @noframes
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_openInTab
// @grant GM_info
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @connect huaweicloudobs.ahjxjy.cn
// @connect imageservice-cdn-01.etledu.com
// @connect *.etledu.com
// @connect oa32.ahzsksw.cn
// @connect *.ahzsksw.cn
// @connect card.wlxy.live
// @connect card.wlxy.top
// @run-at document-idle
// @license All Rights Reserved
// ==/UserScript==
(function () {
"use strict";
const HOST = location.hostname.replace(/^www\./, "");
if (!/etledu\.com$/i.test(HOST)) return;
const VER =
(typeof GM_info !== "undefined" && GM_info?.script?.version) || "1.2.0";
const SCRIPT_NAME = "\u4e2d\u94c1\u516b\u5927\u5458ETLEDU\u5b66\u4e60\u52a9\u624b";
const BOOT = "__ETL_ZTB_WATCH_BOOT__";
if (window[BOOT]) return;
window[BOOT] = true;
const UW = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
const API = location.origin;
const STORAGE = {
pace: `etl_pace_${HOST}`,
onlyIncomplete: `etl_only_incomplete_${HOST}`,
selected: `etl_selected_${HOST}`,
projectId: `etl_project_${HOST}`,
panelPos: `etl_panel_pos_${HOST}`,
collapsed: `etl_collapsed_${HOST}`,
faceImages: `etl_face_imgs_${HOST}`,
faceFromAvatar: `etl_face_from_avatar_${HOST}`,
cloudToken: `etl_cloud_token_${HOST}`,
cloudLease: `etl_cloud_lease_${HOST}`,
};
const DEFAULT_CLOUD_API_BASE = "https://oa32.ahzsksw.cn";
const PRO_BUY_URL_DEFAULT = "https://card.wlxy.live/details/594524C1";
const FREE_SECTION_LIMIT_DEFAULT = 1;
const PACE = { efficiency: "efficiency", realtime: "realtime" };
const PANEL_LOGO_URL =
"https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png";
const PANEL_NOTICE = "\u7981\u6b62\u4fee\u6539\u672c\u5730\u4ee3\u7801\u7ed5\u8fc7\u6388\u6743\u548c\u989d\u5ea6\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";
const LOG_LIMIT = 300;
const EFF_STEP = 300;
const EFF_INTERVAL_MS = 1500;
const RT_FALLBACK_SEC = 300;
const CAPTCHA_ID_BAIDU = "92c09f217bb725ef8194fcb58db266d8";
function loadFaceImages() {
try {
const raw = GM_getValue(STORAGE.faceImages, null);
const arr = typeof raw === "string" ? JSON.parse(raw) : raw;
return Array.isArray(arr) ? arr.filter((x) => typeof x === "string" && x.startsWith("data:image")) : [];
} catch (_) {
return [];
}
}
function saveFaceImages(arr) {
const list = Array.isArray(arr) ? arr.slice(0, 8) : [];
try {
GM_setValue(STORAGE.faceImages, list);
} catch (e) {
throw e;
}
S.faceImages = list;
if (!list.length) {
S.faceFromAvatar = false;
GM_setValue(STORAGE.faceFromAvatar, false);
}
}
function hasUserCapturedFace() {
const imgs = S.faceImages.length ? S.faceImages : loadFaceImages();
if (!imgs.length) return false;
if (S.faceFromAvatar || GM_getValue(STORAGE.faceFromAvatar, false) === true) return false;
return true;
}
function needsAvatarFaceConfirm() {
return !hasUserCapturedFace() && !!getAvatarUrl();
}
function getAvatarUrl() {
const img =
document.querySelector("#faceImg") ||
document.querySelector(".nbzj_user_img img") ||
UW.document?.querySelector?.("#faceImg") ||
UW.document?.querySelector?.(".nbzj_user_img img");
if (!img) return "";
const src = String(img.currentSrc || img.getAttribute("src") || img.src || "").trim();
if (!src || !/^https?:\/\//i.test(src)) return "";
if (/shilir\.png|default|placeholder|avatar_default/i.test(src)) return "";
return src;
}
function arrayBufferToDataUrl(buf, mime) {
const bytes = new Uint8Array(buf);
const chunk = 0x8000;
let binary = "";
for (let i = 0; i < bytes.length; i += chunk) {
binary += String.fromCharCode.apply(
null,
bytes.subarray(i, Math.min(i + chunk, bytes.length))
);
}
return `data:${mime || "image/jpeg"};base64,${btoa(binary)}`;
}
function fetchImageAsDataUrl(url) {
return new Promise((resolve, reject) => {
const pageFetch = UW.fetch ? UW.fetch.bind(UW) : null;
const viaFetch = () => {
if (!pageFetch) return Promise.reject(new Error("no fetch"));
return pageFetch(url, { mode: "cors", credentials: "omit", cache: "no-store" })
.then(async (r) => {
if (!r.ok) throw new Error("HTTP " + r.status);
const blob = await r.blob();
return new Promise((res, rej) => {
const fr = new FileReader();
fr.onload = () => res(String(fr.result || ""));
fr.onerror = () => rej(new Error("\u8bfb\u53d6\u5934\u50cf\u5931\u8d25"));
fr.readAsDataURL(blob);
});
});
};
const viaGm = () =>
new Promise((res, rej) => {
if (typeof GM_xmlhttpRequest !== "function") {
rej(new Error("\u65e0\u6cd5\u4e0b\u8f7d\u5934\u50cf"));
return;
}
GM_xmlhttpRequest({
method: "GET",
url,
responseType: "arraybuffer",
timeout: 30000,
onload(resp) {
if (resp.status < 200 || resp.status >= 300) {
rej(new Error("\u5934\u50cf\u4e0b\u8f7d HTTP " + resp.status));
return;
}
const mime =
(resp.responseHeaders || "").match(/content-type:\s*([^\s;]+)/i)?.[1] ||
"image/jpeg";
try {
res(arrayBufferToDataUrl(resp.response, mime));
} catch (e) {
rej(e);
}
},
onerror() {
rej(new Error("\u5934\u50cf\u4e0b\u8f7d\u7f51\u7edc\u9519\u8bef"));
},
ontimeout() {
rej(new Error("\u5934\u50cf\u4e0b\u8f7d\u8d85\u65f6"));
},
});
});
viaFetch()
.then(resolve)
.catch(() => {
viaGm().then(resolve).catch(reject);
});
});
}
async function ensureFaceImages() {
let imgs = S.faceImages.length ? S.faceImages.slice() : loadFaceImages();
if (imgs.length) {
S.faceImages = imgs;
return imgs;
}
const url = getAvatarUrl();
if (!url) return [];
try {
let dataUrl = await fetchImageAsDataUrl(url);
if (!dataUrl || !dataUrl.startsWith("data:image")) {
throw new Error("\u5934\u50cf\u683c\u5f0f\u65e0\u6548");
}
dataUrl = await shrinkDataUrl(dataUrl, 640, 0.82);
const need = Math.max(1, Number(S.studyConfig.BaiDuImageNum) || 1);
imgs = [];
for (let i = 0; i < need; i++) imgs.push(dataUrl);
S.faceFromAvatar = true;
GM_setValue(STORAGE.faceFromAvatar, true);
try {
await persistFaceImages(imgs);
} catch (_) {
S.faceImages = imgs;
}
return imgs;
} catch (e) {
log(`\u5934\u50cf\u515c\u5e95\u5931\u8d25\uff1a${e.message || e}`, "warn");
return [];
}
}
const S = {
enabled: false,
stopFlag: false,
running: false,
pace: String(GM_getValue(STORAGE.pace, "") || ""),
onlyIncomplete: true,
userGuid: "",
userName: "",
projects: [],
projectId: String(GM_getValue(STORAGE.projectId, "") || ""),
sections: [],
selectedIds: new Set(),
selectMode: "all",
studyConfig: {
VideoSource: 1,
RecordStudyTime: RT_FALLBACK_SEC,
BaiDuImageNum: 1,
FaceRecognitionMethod: 1,
},
faceImages: loadFaceImages(),
faceFromAvatar: GM_getValue(STORAGE.faceFromAvatar, false) === true,
faceStream: null,
logs: [],
task: "",
currentCourse: "",
currentChapter: "",
panelHint: "",
queueDone: 0,
queueTotal: 0,
cloudToken: String(GM_getValue(STORAGE.cloudToken, "") || "").trim(),
cloudLease: "",
cloudLeaseExp: 0,
cloudTier: "unknown",
cloudRevoked: false,
freeUsedSections: 0,
freeSectionLimit: FREE_SECTION_LIMIT_DEFAULT,
proBuyUrl: PRO_BUY_URL_DEFAULT,
proExpiresAt: 0,
proExpiresText: "",
remotePanelNotice: "",
panelNoticePath: "/api/etledu/panel-notice",
};
(function loadSelected() {
try {
const raw = GM_getValue(STORAGE.selected, null);
const sel = typeof raw === "string" ? JSON.parse(raw) : raw;
if (sel && typeof sel === "object" && !Array.isArray(sel)) {
S.selectMode =
sel.mode === "none" || sel.mode === "partial" ? sel.mode : "all";
S.selectedIds = new Set(Array.isArray(sel.ids) ? sel.ids.map(String) : []);
} else if (Array.isArray(sel)) {
S.selectMode = sel.length ? "partial" : "all";
S.selectedIds = new Set(sel.map(String));
}
} catch (_) {}
})();
function saveSelected() {
GM_setValue(STORAGE.selected, {
mode: S.selectMode,
ids: Array.from(S.selectedIds),
});
}
function $(id) {
return document.getElementById(id);
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, Math.max(0, ms)));
}
function esc(s) {
return String(s ?? "")
.replace(/&/g, "&")
.replace(//g, ">");
}
function trunc(s, n) {
const t = String(s || "");
return t.length > n ? t.slice(0, n) + "…" : t;
}
function fmtDur(sec) {
const n = Math.max(0, Math.floor(Number(sec) || 0));
const h = Math.floor(n / 3600);
const m = Math.floor((n % 3600) / 60);
const s = n % 60;
if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
return `${m}:${String(s).padStart(2, "0")}`;
}
function sectionKey(sec) {
return `${sec.courseId}_${sec.resourceId}_${sec.senctionId}`;
}
/* cloud-auth core (whitespace/syntax compact, names kept) */
function getCloudApiBase(){return DEFAULT_CLOUD_API_BASE.replace(/\/+$/,"")}function isCloudProTier(){return!S.cloudRevoked&&String(S.cloudTier||"").toLowerCase()==="pro"}function formatCloudTierText(){return S.cloudRevoked||String(S.cloudTier).toLowerCase()==="revoked"?"\u5DF2\u505C\u7528":isCloudProTier()?"\u4E13\u4E1A\u7248":String(S.cloudTier).toLowerCase()==="free"?"\u4F53\u9A8C\u7248":"\u672A\u6821\u9A8C"}function cloudRequest(path,method,body,headersExtra){const url=`${getCloudApiBase()}${path}`,headers=Object.assign({Accept:"application/json","Content-Type":"application/json;charset=UTF-8"},headersExtra||{});S.cloudToken&&(headers.Authorization=`Bearer ${S.cloudToken}`);const luid=String(S.userGuid||"").trim();luid&&(headers["x-learning-user-id"]=luid);const payload=body==null?null:JSON.stringify(body);return new Promise((resolve,reject)=>{if(typeof GM_xmlhttpRequest!="function"){reject(new Error("\u4E91\u7AEF\u8BF7\u6C42\u4E0D\u53EF\u7528"));return}GM_xmlhttpRequest({method:method||"POST",url,headers,data:payload,timeout:3e4,onload(resp){let data={};try{data=JSON.parse(String(resp.responseText||"").trim()||"{}")}catch(_){data={}}const status=Number(resp.status||0);if(status<200||status>=300){reject(new Error(String(data.detail||data.message||data.msg||`http_${status}`)));return}resolve(data)},onerror(){reject(new Error("\u4E91\u7AEF\u7F51\u7EDC\u9519\u8BEF"))},ontimeout(){reject(new Error("\u4E91\u7AEF\u8BF7\u6C42\u8D85\u65F6"))}})})}function formatProExpireText(){if(!isCloudProTier())return"\u2014";if(S.proExpiresText)return S.proExpiresText;const ts=Number(S.proExpiresAt||0);if(!ts)return"\u2014";try{const d=new Date(ts*1e3);if(Number.isNaN(d.getTime()))return"\u2014";const p=n=>String(n).padStart(2,"0");return`${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`}catch(_){return"\u2014"}}function renderPanelNotice(){const text=String(S.remotePanelNotice||PANEL_NOTICE||"").trim()||PANEL_NOTICE,span=$("etl-ann-text");span&&(span.textContent=text)}async function fetchPanelNotice(){const apply=text=>{const t=String(text||"").trim();t&&(S.remotePanelNotice=t),renderPanelNotice()};try{const text=await new Promise((resolve,reject)=>{if(typeof GM_xmlhttpRequest!="function"){reject(new Error("no_gm"));return}GM_xmlhttpRequest({method:"GET",url:`${getCloudApiBase()}${S.panelNoticePath||"/api/etledu/panel-notice"}`,headers:{Accept:"text/plain, */*"},timeout:15e3,anonymous:!0,onload(resp){resp.status>=200&&resp.status<300?resolve(String(resp.responseText||"").trim()):reject(new Error("notice_http_"+resp.status))},onerror(){reject(new Error("notice_net"))},ontimeout(){reject(new Error("notice_timeout"))}})});apply(text)}catch(e){renderPanelNotice();try{log(`\u516C\u544A\u62C9\u53D6\u5931\u8D25\uFF1A${e.message||e}`,"warn")}catch(_){}}}async function fetchClientConfig(){try{const data=await cloudRequest("/api/etledu/client-config","GET");if(data!=null&&data.panelNoticePath){const p=String(data.panelNoticePath).trim();p&&(S.panelNoticePath=p.startsWith("/")?p:`/${p}`)}if(data!=null&&data.panelNotice||data!=null&&data.panel_notice){const n=String(data.panelNotice||data.panel_notice||"").trim();n&&(S.remotePanelNotice=n)}if((data==null?void 0:data.freeSectionLimit)!=null&&(S.freeSectionLimit=Number(data.freeSectionLimit)||FREE_SECTION_LIMIT_DEFAULT),data!=null&&data.proBuyUrl){const buy=String(data.proBuyUrl).trim();buy&&(S.proBuyUrl=buy)}renderPanelNotice()}catch(_){}await fetchPanelNotice()}async function ensureCloudLease(force){var _a,_b;const now=Date.now()/1e3;if(!force&&S.cloudLease&&S.cloudLeaseExp-now>60)return S.cloudLease;const luid=String(S.userGuid||"").trim();if(!luid)throw new Error("\u672A\u83B7\u53D6\u5B66\u5458\u8EAB\u4EFD\uFF0C\u8BF7\u5148\u767B\u5F55");const data=await cloudRequest("/api/etledu/lease","POST",{learning_user_id:luid,name:S.userName||"",userName:S.userName||""});S.cloudLease=String(data.lease||""),S.cloudLeaseExp=Number(data.exp||0)||0,S.cloudTier=String(data.tier||(S.cloudToken?"pro":"free")).toLowerCase(),data.free_used_sections!=null&&(S.freeUsedSections=Number(data.free_used_sections)||0),data.free_section_limit!=null&&(S.freeSectionLimit=Number(data.free_section_limit)||FREE_SECTION_LIMIT_DEFAULT),data.proBuyUrl&&(S.proBuyUrl=String(data.proBuyUrl).trim()||S.proBuyUrl);const proExp=Number((_b=(_a=data.pro_expires_at)!=null?_a:data.proExpiresAt)!=null?_b:0);if(S.proExpiresAt=Number.isFinite(proExp)&&proExp>0?proExp:0,S.proExpiresText=String(data.pro_expires_at_text||data.proExpiresAtText||"").trim(),isCloudProTier()||(S.proExpiresAt=0,S.proExpiresText=""),!S.cloudLease)throw new Error("\u4E91\u7AEF lease \u4E3A\u7A7A");try{refreshCloudUi()}catch(_){}return S.cloudLease}async function consumeSectionQuota(sec){await ensureCloudLease(!1);const sid=sectionKey(sec),data=await cloudRequest("/api/etledu/section/consume","POST",{lease:S.cloudLease,section_id:sid,section_name:sec.title||""});return data.free_used_sections!=null&&(S.freeUsedSections=Number(data.free_used_sections)||0),data.free_section_limit!=null&&(S.freeSectionLimit=Number(data.free_section_limit)||S.freeSectionLimit),data.tier&&(S.cloudTier=String(data.tier).toLowerCase()),data}function refreshCloudUi(){const tierEl=$("etl-cloud-tier"),freeEl=$("etl-cloud-free"),expEl=$("etl-cloud-expire"),tokenEl=$("etl-cloud-token");tierEl&&(tierEl.textContent=formatCloudTierText(),tierEl.style.color=S.cloudRevoked?"#b91c1c":"#0f172a"),freeEl&&(freeEl.textContent=isCloudProTier()?"\u4E0D\u9650":`${Math.max(0,Number(S.freeUsedSections)||0)} / ${Math.max(0,Number(S.freeSectionLimit)||0)}`),expEl&&(expEl.textContent=formatProExpireText()),tokenEl&&document.activeElement!==tokenEl&&(tokenEl.value=S.cloudToken||""),renderPanelNotice()}
function log(msg, level) {
const row = {
t: new Date().toLocaleTimeString(),
msg: String(msg || ""),
level: level || "info",
};
S.logs.unshift(row);
if (S.logs.length > LOG_LIMIT) S.logs.length = LOG_LIMIT;
renderLog();
}
function formBody(obj) {
return Object.keys(obj)
.filter((k) => obj[k] !== undefined && obj[k] !== null)
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(obj[k])}`)
.join("&");
}
function xhr(method, url, body, headers) {
const full = url.startsWith("http") ? url : API + url;
const methodU = String(method || "GET").toUpperCase();
const hdrs = Object.assign(
{
"X-Requested-With": "XMLHttpRequest",
Accept: "*/*",
},
headers || {}
);
return new Promise((resolve, reject) => {
const $ = UW.jQuery || UW.$;
if ($ && typeof $.ajax === "function") {
const ajaxData =
body == null
? undefined
: typeof body === "string"
? body
: body;
const isJsonBody =
typeof ajaxData === "string" &&
/application\/json/i.test(String(hdrs["Content-Type"] || ""));
if (ajaxData != null && typeof ajaxData === "string" && !hdrs["Content-Type"]) {
hdrs["Content-Type"] =
"application/x-www-form-urlencoded; charset=UTF-8";
}
$.ajax({
url: full,
type: methodU,
data: ajaxData,
traditional: true,
processData: !isJsonBody && typeof ajaxData !== "string",
contentType: isJsonBody
? "application/json; charset=UTF-8"
: hdrs["Content-Type"] ||
"application/x-www-form-urlencoded; charset=UTF-8",
headers: hdrs,
xhrFields: { withCredentials: true },
cache: false,
timeout: 60000,
success(resp, _s, jq) {
const text =
typeof resp === "string"
? resp
: jq && jq.responseText != null
? jq.responseText
: JSON.stringify(resp);
resolve({
status: (jq && jq.status) || 200,
responseText: text,
data: resp,
});
},
error(jq, _t, err) {
if (jq && jq.responseText != null) {
resolve({
status: jq.status || 0,
responseText: String(jq.responseText || ""),
});
return;
}
reject(new Error(err || "\u7f51\u7edc\u9519\u8bef"));
},
});
return;
}
const pageFetch = UW.fetch ? UW.fetch.bind(UW) : fetch.bind(window);
let data;
if (body != null) {
data = typeof body === "string" ? body : formBody(body);
if (!hdrs["Content-Type"]) {
hdrs["Content-Type"] =
"application/x-www-form-urlencoded; charset=UTF-8";
}
}
pageFetch(full, {
method: methodU,
headers: hdrs,
body: methodU !== "GET" && methodU !== "HEAD" ? data : undefined,
credentials: "include",
cache: "no-store",
})
.then(async (r) => {
resolve({ status: r.status, responseText: await r.text() });
})
.catch((e) => reject(e));
});
}
async function apiForm(path, data) {
const res = await xhr("POST", path, data || {});
if (res.data != null && typeof res.data === "object") return res.data;
const text = String(res.responseText || "").trim();
if (!text) {
throw new Error(`\u63a5\u53e3\u7a7a\u54cd\u5e94\uff1a${path}\uff08HTTP ${res.status}\uff09`);
}
if (
!/^\s*[{\[]/.test(text) &&
(/\/account\/default|\/Account\/Login|name=["']?password/i.test(text) ||
(/= 400) return null;
if (res.data != null && typeof res.data === "object") return res.data;
const text = String(res.responseText || "").trim();
if (!text || !/^\s*[{\[]/.test(text)) return null;
return JSON.parse(text);
} catch (_) {
return null;
}
}
async function apiGetText(path) {
const res = await xhr("GET", path);
if (res.status >= 400) throw new Error(`HTTP ${res.status}`);
return String(res.responseText || "");
}
async function fetchUser() {
const j = await apiForm("/PersonalCenter/GetUserInfo", {});
if (!j || !j.Success) throw new Error("\u672a\u767b\u5f55\u6216\u4f1a\u8bdd\u5931\u6548\uff0c\u8bf7\u5148\u767b\u5f55");
S.userGuid = String(j.Guid || "");
return j;
}
async function fetchProjects() {
const j = await apiForm("/PersonalCenter/GetMyTrainPlanList", {
limit: 50,
page: 1,
LearnType: 0,
trainTargetId: 0,
trainTargetDetailId: 0,
LearnState: -1,
});
const list = Array.isArray(j?.data) ? j.data : [];
S.projects = list.map((p) => ({
id: String(p.Id),
name: String(p.TrainName || "\u57f9\u8bad\u8ba1\u5212"),
detail: String(p.TrainTargetDetailName || ""),
learnState: String(p.LearnState || ""),
learnedHours: Number(p.LearnedHours) || 0,
countLong: Number(p.CountLong) || 0,
userName: String(p.Name || ""),
provinceName: String(p.ProvinceName || ""),
raw: p,
}));
if (S.projects.length && !S.projectId) {
S.projectId = S.projects[0].id;
GM_setValue(STORAGE.projectId, S.projectId);
}
const cur = S.projects.find((p) => p.id === S.projectId);
if (cur?.userName) S.userName = cur.userName;
return S.projects;
}
async function fetchStudyConfig(projectId) {
const j = await apiForm("/PersonalCenter/GetCityStudyConfig", {
projectId,
});
const r = j?.Result || {};
S.studyConfig = {
VideoSource: Number(r.VideoSource) || 1,
RecordStudyTime: Math.max(30, Number(r.RecordStudyTime) || RT_FALLBACK_SEC),
StudyFaceOnOff: !!r.StudyFaceOnOff,
SDynamicCheckFaceOnOff: !!r.SDynamicCheckFaceOnOff,
SMSVerificationOnOff: !!r.SMSVerificationOnOff,
IsRebroadcast: !!r.IsRebroadcast,
BaiDuImageNum: Math.max(1, Number(r.BaiDuImageNum) || 1),
FaceRecognitionMethod: Number(r.FaceRecognitionMethod) || 1,
StudyCompareValue: Number(r.StudyCompareValue) || 0.6,
SDynamicCheckValue: Number(r.SDynamicCheckValue) || 12,
raw: r,
};
return S.studyConfig;
}
async function checkCanStudy(projectId) {
const j = await apiForm("/PersonalCenter/CheckIsCanStudy", { projectId });
if (j && j.Result === false) {
throw new Error(j.msg || j.Msg || "\u5f53\u524d\u65e0\u6cd5\u5b66\u4e60\uff0c\u8bf7\u8054\u7cfb\u5e73\u53f0\u7ba1\u7406\u5458");
}
return true;
}
function parseAttr(tag, name) {
const m = tag.match(new RegExp(`${name}="([^"]*)"`, "i"));
return m ? m[1] : "";
}
function parseSectionsFromHtml(html) {
const sections = [];
const re =
/
]*id="c-(\d+)_r-(\d+)_s-(\d+)"([^>]*)>/gi;
let m;
while ((m = re.exec(html))) {
const tag = m[0];
const courseId = m[1];
const resourceId = m[2];
const senctionId = m[3];
const attrs = m[4] || "";
const fullTag = tag;
const before = html.slice(Math.max(0, m.index - 8000), m.index);
const near = html.slice(Math.max(0, m.index - 600), m.index);
const nearTitle = near.match(
/bofang_list_name_title"[^>]*title="([^"]*)"/i
);
const liPos = html.lastIndexOf("kecheng_li", m.index);
let totalCount = 0;
if (liPos >= 0) {
const tm = html
.slice(liPos, liPos + 160)
.match(/data-totalCount="(\d+)"/i);
if (tm) totalCount = parseInt(tm[1], 10) || 0;
}
const after = html.slice(m.index, m.index + 450);
const statusM = after.match(
/bofang_list_name_wanchengdu[^>]*>([^<]+)/i
);
const isFinish = parseAttr(fullTag + attrs, "data-isFinish") === "1";
let chapter = "";
const chapRe = /layui-colla-title[^>]*>([^<]+)/gi;
let cm;
while ((cm = chapRe.exec(before))) {
const t = (cm[1] || "").trim();
if (t && !/\u5c55\u5f00|\u6536\u8d77/.test(t)) chapter = t;
}
sections.push({
courseId,
resourceId,
senctionId,
projectId: parseAttr(fullTag + attrs, "data-projectId") || S.projectId,
ccvu: parseAttr(fullTag + attrs, "data-ccvu"),
title: (nearTitle && nearTitle[1]) || `\u8bfe\u65f6 ${resourceId}`,
chapter,
totalCount,
isFinish,
status: statusM ? statusM[1].trim() : isFinish ? "\u5df2\u5b66\u5b8c" : "\u672a\u5b66\u4e60",
secondsLearned: 0,
});
}
return sections;
}
function parseSectionsFromDom() {
const nodes = document.querySelectorAll(
".bofang_list_biao .bofang_list_Detail[data-senctionid]"
);
if (!nodes.length) return [];
const out = [];
nodes.forEach((el) => {
if (!el.getAttribute("data-senctionid")) return;
const li = el.closest(".kecheng_li");
const titleEl =
el.closest(".bofang_list_name")?.querySelector(".bofang_list_name_title") ||
li?.querySelector(".bofang_list_name_title");
const chapEl = el.closest(".layui-colla-item")?.querySelector(
".layui-colla-title"
);
out.push({
courseId: el.getAttribute("data-courseid") || "",
resourceId: el.getAttribute("data-resourceid") || "",
senctionId: el.getAttribute("data-senctionid") || "",
projectId: el.getAttribute("data-projectId") || S.projectId,
ccvu: el.getAttribute("data-ccvu") || "",
title: titleEl?.getAttribute("title") || titleEl?.textContent?.trim() || "",
chapter: chapEl?.textContent?.trim() || "",
totalCount: parseInt(li?.getAttribute("data-totalCount") || "0", 10) || 0,
isFinish: el.getAttribute("data-isFinish") === "1",
status:
el.querySelector(".bofang_list_name_wanchengdu")?.textContent?.trim() ||
"",
secondsLearned:
parseFloat(li?.getAttribute("data-secondsLearned") || "0") || 0,
});
});
return out.filter((s) => s.senctionId);
}
async function fetchSections(projectId) {
const domSecs = parseSectionsFromDom();
if (
domSecs.length &&
String(domSecs[0].projectId || projectId) === String(projectId)
) {
S.sections = domSecs;
return S.sections;
}
const html = await apiGetText(
`/PersonalCenter/Study?projectId=${encodeURIComponent(projectId)}`
);
S.sections = parseSectionsFromHtml(html);
return S.sections;
}
function isSectionChecked(sec) {
const id = sectionKey(sec);
if (S.selectMode === "none") return false;
if (S.selectMode === "all") return true;
return S.selectedIds.has(id);
}
function getSelectedSections() {
if (S.selectMode === "none") return [];
if (S.selectMode === "all") return S.sections.slice();
return S.sections.filter((s) => S.selectedIds.has(sectionKey(s)));
}
async function getLastStudyTime(sec) {
const j = await apiForm("/PersonalCenter/GetLastStudyTime", {
projectId: sec.projectId,
courseId: sec.courseId,
resourceId: sec.resourceId,
senctionId: sec.senctionId,
t: new Date().toString(),
});
return Number(j?.LastTime) || 0;
}
async function saveStudyLog(sec, timeLong) {
const j = await apiForm("/PersonalCenter/SaveStudyLog", {
projectId: sec.projectId,
courseId: sec.courseId,
resourceId: sec.resourceId,
senctionId: sec.senctionId,
timeLong: Number(timeLong) || 0,
videoSource: S.studyConfig.VideoSource || 1,
});
return j;
}
async function getTextByText(text) {
const res = await xhr("POST", "/Config/GetTextByText", { text: String(text) });
if (typeof res.data === "string" && res.data.trim()) {
const s = res.data.trim();
if (s.startsWith('"') && s.endsWith('"')) {
try {
return JSON.parse(s);
} catch (_) {}
}
return s.replace(/^"|"$/g, "");
}
let t = String(res.responseText || "").trim();
if (t.startsWith('"') && t.endsWith('"')) {
try {
t = JSON.parse(t);
} catch (_) {
t = t.slice(1, -1);
}
}
if (!t) throw new Error("GetTextByText \u7a7a\u54cd\u5e94");
return t;
}
async function apiJsonPost(path, obj) {
const res = await xhr("POST", path, JSON.stringify(obj), {
"Content-Type": "application/json",
});
if (res.data != null && typeof res.data === "object") return res.data;
try {
return JSON.parse(String(res.responseText || "{}"));
} catch (_) {
throw new Error(`${path} \u8fd4\u56de\u975e JSON`);
}
}
function shrinkDataUrl(dataUrl, maxSide, quality) {
return new Promise((resolve) => {
try {
const img = new Image();
img.onload = () => {
const w0 = img.naturalWidth || img.width;
const h0 = img.naturalHeight || img.height;
const scale = Math.min(1, maxSide / Math.max(w0, h0));
const w = Math.max(1, Math.round(w0 * scale));
const h = Math.max(1, Math.round(h0 * scale));
const c = document.createElement("canvas");
c.width = w;
c.height = h;
c.getContext("2d").drawImage(img, 0, 0, w, h);
resolve(c.toDataURL("image/jpeg", quality));
};
img.onerror = () => resolve(dataUrl);
img.src = dataUrl;
} catch (_) {
resolve(dataUrl);
}
});
}
async function persistFaceImages(imgs) {
let list = (imgs || []).slice();
for (let q of [0.72, 0.55, 0.4]) {
try {
const shrunk = [];
for (const u of list) {
shrunk.push(await shrinkDataUrl(u, 640, q));
}
saveFaceImages(shrunk);
return shrunk;
} catch (_) {
}
}
if (list[0]) {
const one = [await shrinkDataUrl(list[0], 480, 0.35)];
saveFaceImages(one);
return one;
}
throw new Error("\u4eba\u8138\u56fe\u7247\u8fc7\u5927\uff0c\u65e0\u6cd5\u7f13\u5b58");
}
async function submitCachedFace(sec) {
const imgs = await ensureFaceImages();
S.faceImages = imgs;
if (!imgs.length) {
throw new Error("\u5c1a\u672a\u91c7\u96c6\u4eba\u8138\uff0c\u4e14\u65e0\u6cd5\u8bfb\u53d6\u4e2a\u4eba\u4e2d\u5fc3\u5934\u50cf\uff1a\u8bf7\u5230\u300c\u8bbe\u7f6e\u300d\u91c7\u96c6\u6216\u4e0a\u4f20");
}
const num = Math.max(1, Number(S.studyConfig.BaiDuImageNum) || imgs.length || 1);
const useImgs = imgs.slice(0, num);
while (useImgs.length < num) useImgs.push(imgs[0]);
try {
const ok = await doubleCheckWithGeetest(sec, useImgs, num);
if (ok) return true;
} catch (e) {
log(`\u6781\u9a8c\u901a\u9053\uff1a${e.message || e}`, "warn");
}
try {
const token = await getTextByText(
`${sec.resourceId},${num},${sec.senctionId}`
);
const liveBody = {
faceVideoWidth: 640,
faceVideoHeight: 480,
FirstFaceImgArrayBaiDu: useImgs[0] || "",
SecondFaceImgArrayBaiDu: useImgs[1] || "",
ThirdFaceImgArrayBaiDu: useImgs[2] || "",
FourthFaceImgArrayBaiDu: useImgs[3] || "",
FifthFaceImgArrayBaiDu: useImgs[4] || "",
SixthFaceImgArrayBaiDu: useImgs[5] || "",
SeventhFaceImgArrayBaiDu: useImgs[6] || "",
EighthFaceImgArrayBaiDu: useImgs[7] || "",
text: token,
};
for (const path of ["/BaiDuFace/FaceLivenessH5", "/BaiDuFace/FaceLiveness"]) {
const j = await apiFormSoft(path, liveBody);
if (j && (j.Result === true || j.result === true || j.Success === true)) {
return true;
}
}
} catch (e) {
log(`\u5907\u7528\u6d3b\u4f53\u901a\u9053\u8df3\u8fc7\uff1a${e.message || e}`, "warn");
}
return false;
}
function loadScriptInPage(src) {
return new Promise((resolve, reject) => {
const doc = UW.document || document;
if (doc.querySelector(`script[data-etl-gt="${src}"]`)) {
resolve();
return;
}
const s = doc.createElement("script");
s.src = src;
s.async = true;
s.setAttribute("data-etl-gt", src);
s.onload = () => resolve();
s.onerror = () => reject(new Error("\u811a\u672c\u52a0\u8f7d\u5931\u8d25 " + src));
(doc.head || doc.documentElement).appendChild(s);
});
}
async function ensureGeetest4() {
const pick = () => {
if (typeof UW.initGeetest4 === "function") return UW.initGeetest4.bind(UW);
if (typeof initGeetest4 === "function") return initGeetest4;
return null;
};
let fn = pick();
if (fn) return fn;
const candidates = [
"https://static.geetest.com/v4/gt4.js",
"https://static.geetest.com/v4/gt4.min.js",
"/Content/Plugin/GeeTest/gt4.js",
"/Content/js/gt4.js",
];
for (const src of candidates) {
try {
await loadScriptInPage(src);
await sleep(200);
fn = pick();
if (fn) return fn;
} catch (_) {}
}
throw new Error(
"\u672a\u52a0\u8f7d\u5230\u6781\u9a8c\u7ec4\u4ef6\u3002\u8bf7\u5148\u8fdb\u5165\u300c\u5f00\u59cb\u5b66\u4e60\u300d\u5b66\u4e60\u9875\u540e\u518d\u7528\u52a9\u624b\uff0c\u6216\u5237\u65b0\u540e\u91cd\u8bd5"
);
}
function clickEl(el) {
if (!el) return false;
try {
el.scrollIntoView?.({ block: "center", inline: "nearest" });
} catch (_) {}
const opts = { bubbles: true, cancelable: true, view: UW };
try {
el.dispatchEvent(new PointerEvent("pointerdown", opts));
} catch (_) {}
try {
el.dispatchEvent(new MouseEvent("mousedown", opts));
el.dispatchEvent(new MouseEvent("mouseup", opts));
el.dispatchEvent(new MouseEvent("click", opts));
} catch (_) {}
try {
el.click();
} catch (_) {
return false;
}
return true;
}
function isGeetestValidateOk(result) {
if (!result || typeof result !== "object") return false;
return !!(
result.lot_number &&
result.pass_token &&
(result.captcha_output || result.captcha_id)
);
}
function tryAutoClickGeetest(root) {
const doc = UW.document || document;
const scope = root || doc;
const selectors = [
".geetest_btn_click",
".geetest_click",
".geetest_radar_btn",
".geetest_radar_tip",
".geetest_btn",
".geetest_holder .geetest_btn",
".geetest_popover .geetest_btn_click",
".geetest_panel .geetest_btn_click",
".geetest_panel_box .geetest_btn",
"[class*='geetest'][class*='btn_click']",
"[class*='geetest_radar']",
"div.geetest_btn",
];
for (const sel of selectors) {
let nodes;
try {
nodes = scope.querySelectorAll(sel);
} catch (_) {
continue;
}
for (const el of nodes) {
if (!el) continue;
const st = UW.getComputedStyle ? UW.getComputedStyle(el) : null;
if (
st &&
(st.visibility === "hidden" || st.display === "none" || st.opacity === "0")
) {
continue;
}
if (clickEl(el)) return el;
}
}
const all = scope.querySelectorAll("div,button,a,span");
for (const el of all) {
const t = String(el.textContent || "").trim();
if (!t || t.length > 24) continue;
if (/\u70b9\u51fb\u6309\u94ae\u8fdb\u884c\u9a8c\u8bc1|\u70b9\u51fb\u6309\u94ae|\u70b9\u51fb\u5b8c\u6210|\u4e00\u952e\u901a\u8fc7|\u5f00\u59cb\u9a8c\u8bc1|\u70b9\u51fb\u9a8c\u8bc1/.test(t)) {
if (clickEl(el)) return el;
}
}
return null;
}
function startGeetestAutoClick(gt, mountEl, onValidateReady) {
let tries = 0;
let clickedOnce = false;
const maxTries = 60;
let stopped = false;
const stop = () => {
stopped = true;
};
const tick = () => {
if (stopped) return;
try {
const v = gt.getValidate && gt.getValidate();
if (isGeetestValidateOk(v)) {
if (typeof onValidateReady === "function") onValidateReady(v);
stop();
return;
}
} catch (_) {}
if (tries >= maxTries) {
log("\u6781\u9a8c\u81ea\u52a8\u70b9\u51fb\u8d85\u65f6\uff1b\u53ef\u6253\u5f00\u5b66\u4e60\u9875\u624b\u52a8\u8fc7\u4e00\u6b21\u6838\u9a8c\u540e\u518d\u8bd5", "warn");
stop();
return;
}
tries += 1;
try {
if (typeof gt.showCaptcha === "function") gt.showCaptcha();
} catch (_) {}
const hit =
tryAutoClickGeetest(mountEl) ||
tryAutoClickGeetest(UW.document?.body) ||
tryAutoClickGeetest(document.body);
if (hit && !clickedOnce) {
clickedOnce = true;
} else if (!hit && tries === 12) {
log("\u672a\u627e\u5230\u6781\u9a8c\u6309\u94ae\uff08\u540e\u53f0\u5bb9\u5668\uff09\uff0c\u5c06\u7ee7\u7eed\u91cd\u8bd5", "warn");
}
setTimeout(tick, clickedOnce ? 700 : 400);
};
setTimeout(tick, 400);
return stop;
}
function doubleCheckWithGeetest(sec, useImgs, num) {
return new Promise(async (resolve, reject) => {
let settled = false;
let stopAuto = null;
const done = (v) => {
if (settled) return;
settled = true;
try {
if (typeof stopAuto === "function") stopAuto();
} catch (_) {}
resolve(!!v);
};
const submitValidate = async (result) => {
if (settled) return;
try {
const payload = Object.assign({}, result || {});
payload.source = 3;
payload.projectId = String(sec.projectId || S.projectId || "");
payload.resourceId = String(sec.resourceId || "");
payload.FaceImgArrayBaiDus = useImgs;
payload.text = await getTextByText(
`${sec.resourceId},${num},${sec.senctionId}`
);
const j3 = await apiJsonPost("/GeeTest/DoubleCheck", payload);
const ok = !!(j3 && (j3.result === true || j3.Result === true));
if (ok) {
S.panelHint = "";
const box = $("etl-geetest-box");
if (box) box.style.display = "none";
updatePanel();
} else {
log(
`DoubleCheck \u672a\u901a\u8fc7\uff1a${j3?.msg || j3?.Msg || JSON.stringify(j3 || {}).slice(0, 120)}`,
"warn"
);
}
try {
if (S._etlGt) S._etlGt.destroy();
} catch (_) {}
done(ok);
} catch (e) {
log(`DoubleCheck \u5f02\u5e38\uff1a${e.message || e}`, "error");
done(false);
}
};
try {
const initGt = await ensureGeetest4();
let box = $("etl-geetest-box");
if (!box) {
box = document.createElement("div");
box.id = "etl-geetest-box";
box.style.cssText =
"position:fixed;left:-10000px;top:0;width:320px;height:240px;opacity:0;overflow:hidden;z-index:-1;pointer-events:auto;";
box.innerHTML = '
';
document.body.appendChild(box);
}
box.style.display = "block";
const mount = $("etl-geetest-mount");
if (mount) mount.innerHTML = "";
const timer = setTimeout(() => {
done(false);
log("\u6781\u9a8c\u7b49\u5f85\u8d85\u65f6\uff0c\u8bf7\u91cd\u8bd5\u6216\u5148\u6253\u5f00\u5b66\u4e60\u9875\u518d\u5f00\u59cb", "warn");
}, 180000);
const boot = (product) =>
new Promise((resBoot, rejBoot) => {
try {
initGt(
{
captchaId: CAPTCHA_ID_BAIDU,
product,
language: "zho",
hideSuccess: true,
hideError: true,
},
(gt) => resBoot(gt)
);
} catch (e) {
rejBoot(e);
}
});
let gt;
try {
gt = await boot("float");
} catch (_) {
gt = await boot("bind");
}
if (S._etlGt) {
try {
S._etlGt.destroy();
} catch (_) {}
}
S._etlGt = gt;
gt.appendTo("#etl-geetest-mount");
let submitting = false;
const handleOk = async (result) => {
if (settled || submitting) return;
submitting = true;
clearTimeout(timer);
try {
if (typeof stopAuto === "function") stopAuto();
} catch (_) {}
await submitValidate(
result || (gt.getValidate && gt.getValidate()) || {}
);
};
const onReady = () => {
try {
if (typeof gt.showCaptcha === "function") gt.showCaptcha();
} catch (_) {}
stopAuto = startGeetestAutoClick(gt, mount || box, (v) => {
handleOk(v);
});
};
try {
if (typeof gt.onReady === "function") gt.onReady(onReady);
else onReady();
} catch (_) {
onReady();
}
gt.onSuccess(() => {
handleOk(gt.getValidate && gt.getValidate());
});
gt.onFail(() => {
submitting = false;
log("\u6781\u9a8c\u672a\u901a\u8fc7\uff0c\u7ee7\u7eed\u81ea\u52a8\u70b9\u51fb…", "warn");
try {
if (typeof gt.showCaptcha === "function") gt.showCaptcha();
} catch (_) {}
});
gt.onError((err) => {
log(
`\u6781\u9a8c\u9519\u8bef\uff1a${(err && (err.msg || err.message)) || "unknown"}`,
"error"
);
});
gt.onClose(() => {
if (!settled) {
log("\u6781\u9a8c\u88ab\u5173\u95ed\uff0c\u91cd\u65b0\u5f39\u51fa\u5e76\u81ea\u52a8\u70b9\u51fb…", "warn");
try {
if (typeof gt.showCaptcha === "function") gt.showCaptcha();
} catch (_) {}
}
});
} catch (e) {
reject(e);
}
});
}
function interpretSaveError(j) {
const code = j?.Code;
const msg = String(j?.msg || j?.Msg || "").trim();
if (code === -3) return "\u9700\u8981\u6d3b\u4f53/\u4eba\u8138\u9a8c\u8bc1";
if (code === -2) return "\u9700\u8981\u6781\u9a8c\u9a8c\u8bc1";
if (code === 100 || code === -1)
return msg || "\u9700\u8981\u77ed\u4fe1\u9a8c\u8bc1";
if (msg) return `\u5b66\u65f6\u4e0a\u62a5\u5931\u8d25\uff1a${msg}\uff08Code=${code ?? "?"}\uff09`;
return `\u5b66\u65f6\u4e0a\u62a5\u5931\u8d25\uff08Success=false, Code=${code ?? "?"}\uff09`;
}
async function saveStudyLogWithFace(sec, timeLong) {
let j = await saveStudyLog(sec, timeLong);
if (j && j.Success !== false) return j;
const code = j?.Code;
if (code === -3 || code === -2) {
S.faceImages = await ensureFaceImages();
if (!S.faceImages.length) {
throw new Error(
`${interpretSaveError(j)}\uff1a\u8bf7\u5148\u5230\u300c\u8bbe\u7f6e\u300d\u91c7\u96c6/\u4e0a\u4f20\u4eba\u8138\uff0c\u6216\u786e\u8ba4\u4e2a\u4eba\u4e2d\u5fc3\u5df2\u6709\u5934\u50cf`
);
}
const ok = await submitCachedFace(sec);
if (!ok) {
throw new Error(
`${interpretSaveError(j)}\uff1a\u81ea\u52a8\u590d\u7528\u672a\u901a\u8fc7\uff0c\u8bf7\u91cd\u65b0\u91c7\u96c6\u6e05\u6670\u6b63\u8138\u540e\u91cd\u8bd5`
);
}
await sleep(500);
j = await saveStudyLog(sec, timeLong);
if (j && j.Success !== false) return j;
if (j?.Code === -3 || j?.Code === -2) {
await submitCachedFace(sec);
await sleep(500);
j = await saveStudyLog(sec, timeLong);
}
}
if (!j || j.Success === false) {
throw new Error(interpretSaveError(j || {}));
}
return j;
}
async function studySection(sec) {
const total = Math.max(1, Number(sec.totalCount) || 0);
let pos = 0;
try {
pos = await getLastStudyTime(sec);
} catch (_) {
pos = Number(sec.secondsLearned) || 0;
}
if (pos < 0) pos = 0;
if (pos >= total - 2 && sec.isFinish) {
log(`\u8df3\u8fc7\u5df2\u5b66\u5b8c\uff1a${sec.title}`, "info");
return { ok: true, skipped: true };
}
S.currentCourse = sec.chapter || sec.title;
S.currentChapter = sec.title;
S.task = `\u8fdb\u5ea6 ${fmtDur(pos)} / ${fmtDur(total)}`;
updatePanel();
log(`\u5f00\u59cb\uff1a${sec.title}\uff08${fmtDur(total)}\uff09`, "info");
const efficiency = S.pace === PACE.efficiency;
const intervalMs = efficiency
? EFF_INTERVAL_MS
: Math.max(5000, (S.studyConfig.RecordStudyTime || RT_FALLBACK_SEC) * 1000);
const step = efficiency
? EFF_STEP
: Math.max(30, S.studyConfig.RecordStudyTime || RT_FALLBACK_SEC);
let lastReport = -1;
const report = async (t, note) => {
if (S.stopFlag) throw new Error("\u5df2\u505c\u6b62");
const timeLong = Math.min(total, Math.max(0, t));
if (Math.abs(timeLong - lastReport) < 0.01 && timeLong < total - 1) return null;
const j = await saveStudyLogWithFace(sec, timeLong);
lastReport = timeLong;
sec.isFinish = !!j.IsFinish || timeLong >= total - 1;
S.task = `${note || "\u4e0a\u62a5"} ${fmtDur(timeLong)} / ${fmtDur(total)}${
j.IsFinish ? " · \u5df2\u5b8c\u6210" : ""
}`;
updatePanel();
return j;
};
await report(pos, "\u7eed\u5b66");
while (pos < total - 1) {
if (S.stopFlag) throw new Error("\u5df2\u505c\u6b62");
const next = Math.min(total, pos + step);
await sleep(intervalMs);
if (S.stopFlag) throw new Error("\u5df2\u505c\u6b62");
pos = next;
const j = await report(pos, "\u5b66\u4e60\u4e2d");
if (j?.IsFinish) break;
}
if (pos < total) {
await sleep(efficiency ? 800 : 1200);
pos = total;
await report(pos, "\u6536\u5c3e");
}
await sleep(600);
const fin = await report(total, "\u5b8c\u6210\u786e\u8ba4");
if (fin?.IsFinish || pos >= total - 1) {
sec.isFinish = true;
sec.status = "\u5df2\u5b66\u5b8c";
log(`\u5b8c\u6210\uff1a${sec.title}`, "ok");
return { ok: true };
}
log(`\u5df2\u4e0a\u62a5\u81f3\u7247\u5c3e\uff0c\u5e73\u53f0\u672a\u8fd4\u56de IsFinish\uff1a${sec.title}`, "warn");
return { ok: true, uncertain: true };
}
async function runLoop() {
if (S.running) return;
S.running = true;
S.stopFlag = false;
S.enabled = true;
updatePanel();
try {
log("\u6821\u9a8c\u767b\u5f55…", "info");
await fetchUser();
await fetchProjects();
try {
await ensureCloudLease(true);
log(isCloudProTier() ? "\u6388\u6743\uff1a\u4e13\u4e1a\u7248" : `\u6388\u6743\uff1a\u4f53\u9a8c\u7248 ${S.freeUsedSections}/${S.freeSectionLimit}`, "info");
} catch (ce) {
throw new Error(`\u4e91\u7aef\u6388\u6743\u5931\u8d25\uff1a${ce.message || ce}`);
}
if (!S.projectId) throw new Error("\u672a\u627e\u5230\u57f9\u8bad\u8ba1\u5212\uff0c\u8bf7\u5148\u5728\u300c\u6211\u7684\u57f9\u8bad\u300d\u62a5\u540d");
const proj = S.projects.find((p) => p.id === S.projectId) || S.projects[0];
S.projectId = proj.id;
GM_setValue(STORAGE.projectId, S.projectId);
log(`\u57f9\u8bad\uff1a${proj.name} · ${proj.detail || ""}`, "info");
await checkCanStudy(S.projectId);
await fetchStudyConfig(S.projectId);
if (
S.studyConfig.StudyFaceOnOff ||
S.studyConfig.SDynamicCheckFaceOnOff
) {
const faces = await ensureFaceImages();
if (!faces.length) {
log("\u672a\u627e\u5230\u4eba\u8138\u7f13\u5b58/\u5934\u50cf\uff1a\u8bf7\u5230\u8bbe\u7f6e\u91c7\u96c6\u6216\u4e0a\u4f20\uff0c\u6216\u5148\u5728\u6863\u6848\u4e0a\u4f20\u5934\u50cf", "warn");
}
}
log("\u52a0\u8f7d\u8bfe\u65f6\u5217\u8868…", "info");
await fetchSections(S.projectId);
renderLists();
let targets = getSelectedSections();
if (S.onlyIncomplete) {
targets = targets.filter((s) => !s.isFinish && !/\u5df2\u5b66\u5b8c/.test(s.status || ""));
}
S.queueTotal = targets.length;
S.queueDone = 0;
updatePanel();
if (!targets.length) {
log("\u52fe\u9009\u8bfe\u65f6\u5747\u5df2\u5b66\u5b8c\uff0c\u65e0\u9700\u7ee7\u7eed", "info");
return;
}
const modeLabel =
S.pace === PACE.efficiency ? "\u6548\u7387\u6a21\u5f0f" : "1:1 \u65f6\u957f\u6162\u5237";
log(`\u5f00\u59cb\u987a\u5e8f\u5b66\u4e60 · ${targets.length} \u4e2a\u8bfe\u65f6\uff08${modeLabel}\uff09`, "info");
for (const sec of targets) {
if (S.stopFlag) break;
try {
try {
await consumeSectionQuota(sec);
} catch (qe) {
const qm = String(qe.message || qe);
if (/free_.*quota|quota_exceeded/i.test(qm)) {
throw new Error("\u4f53\u9a8c\u989d\u5ea6\u5df2\u7528\u5b8c\uff0c\u8bf7\u5f00\u901a\u4e13\u4e1a\u7248");
}
throw new Error(`\u989d\u5ea6\u6821\u9a8c\u5931\u8d25\uff1a${qm}`);
}
await studySection(sec);
S.queueDone += 1;
updatePanel();
renderLists();
} catch (e) {
const msg = e?.message || String(e);
log(`\u4e2d\u65ad\uff1a${msg}`, "error");
S.panelHint = msg;
updatePanel();
break;
}
}
if (!S.stopFlag && S.queueDone >= S.queueTotal) {
log("\u5168\u90e8\u4efb\u52a1\u5b8c\u6210", "ok");
await fetchSections(S.projectId);
renderLists();
}
} catch (e) {
log(`\u9519\u8bef\uff1a${e?.message || e}`, "error");
S.panelHint = String(e?.message || e);
} finally {
S.running = false;
S.enabled = false;
if (!S.stopFlag) {
S.task = "";
S.currentCourse = "";
S.currentChapter = "";
}
updatePanel();
}
}
function stopRun() {
S.stopFlag = true;
S.enabled = false;
log("\u6b63\u5728\u505c\u6b62…", "warn");
updatePanel();
}
function studyModeLabel(v) {
if (v === PACE.efficiency) return "\u6548\u7387\u6a21\u5f0f";
if (v === PACE.realtime) return "1:1 \u65f6\u957f\u6162\u5237";
return "\u672a\u9009\u62e9";
}
function injectStyles() {
const old = $("etl-panel-style");
if (old) old.remove();
const st = document.createElement("style");
st.id = "etl-panel-style";
st.textContent = `
#etl-auto-panel,#etl-auto-panel *{box-sizing:border-box!important}
#etl-auto-panel{position:fixed!important;right:16px;top:64px;left:auto;z-index:2147483000!important;width:396px!important;max-height:min(88vh,700px)!important;display:flex!important;flex-direction:column!important;background:#f4f7fb!important;border:1px solid #d0dae8!important;border-radius:14px!important;box-shadow:0 14px 32px rgba(15,23,42,.16)!important;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif!important;font-size:12px!important;color:#0f172a!important;overflow:hidden!important;line-height:1.4!important}
#etl-auto-panel.etl-min #etl-panel-body,#etl-auto-panel.etl-min #etl-panel-actions,#etl-auto-panel.etl-min #etl-panel-footer{display:none!important}
#etl-auto-panel #etl-panel-header{padding:8px 11px!important;background:#f8fafc!important;border:0!important;border-bottom:1px solid #e2e8f0!important;display:flex!important;justify-content:space-between!important;align-items:center!important;cursor:move!important;user-select:none!important;flex-shrink:0!important}
#etl-auto-panel #etl-panel-brand{display:flex!important;align-items:center!important;gap:7px!important;min-width:0!important;flex:1!important}
#etl-auto-panel #etl-panel-logo{width:24px!important;height:24px!important;border-radius:7px!important;object-fit:cover!important;border:1px solid #cbd5e1!important;background:#fff!important}
#etl-auto-panel #etl-panel-title{font-size:12px!important;font-weight:900!important;color:#0f172a!important;line-height:1.2!important;white-space:nowrap!important;overflow:hidden!important;text-overflow:ellipsis!important}
#etl-auto-panel #etl-panel-sub{display:none!important}
#etl-auto-panel .etl-ver{font-size:10px!important;font-weight:800!important;color:#64748b!important;padding:1px 5px!important;border-radius:999px!important;background:#f1f5f9!important;border:1px solid #e2e8f0!important;margin-left:4px!important}
#etl-auto-panel .etl-ctl{border:none!important;background:#fff!important;color:#64748b!important;width:24px!important;height:24px!important;border-radius:999px!important;cursor:pointer!important;font-size:13px!important;font-weight:900!important}
#etl-auto-panel #etl-panel-body{padding:8px 9px 6px!important;overflow:auto!important;flex:1 1 auto!important;min-height:0!important;background:transparent!important;color:#0f172a!important}
#etl-auto-panel .etl-card{background:#fff!important;border:1px solid #d9e2ee!important;border-radius:10px!important;padding:8px 9px!important;margin-bottom:7px!important;box-shadow:none!important;outline:none!important}
#etl-auto-panel .etl-status-row{display:flex!important;justify-content:space-between!important;align-items:center!important;gap:6px!important}
#etl-auto-panel #etl-auto-status{padding:1px 6px!important;border-radius:999px!important;font-weight:900!important;font-size:10px!important;border:1px solid #cbd5e1!important;background:#fff!important;color:#334155!important}
#etl-auto-panel .etl-progress-bar{height:3px!important;border-radius:999px!important;background:#e2e8f0!important;overflow:hidden!important;margin-top:3px!important}
#etl-auto-panel .etl-progress-bar>span{display:block!important;height:100%!important;width:0!important;background:linear-gradient(90deg,#22d3ee,#2563eb)!important}
#etl-auto-panel .etl-tabbar{display:flex!important;gap:4px!important;margin-bottom:6px!important}
#etl-auto-panel .etl-tab{flex:1!important;border:1px solid #cbd5e1!important;background:#f8fafc!important;color:#334155!important;padding:4px 2px!important;border-radius:8px!important;cursor:pointer!important;font-weight:700!important;font-size:11px!important;box-shadow:none!important;outline:none!important}
#etl-auto-panel .etl-tab.etl-on{background:linear-gradient(135deg,#1d4ed8,#0ea5e9)!important;color:#fff!important;border-color:transparent!important}
#etl-auto-panel .etl-pane{display:none!important}
#etl-auto-panel .etl-pane.etl-on{display:block!important}
#etl-auto-panel #etl-course-list,#etl-auto-panel #etl-run-log{max-height:168px!important;overflow:auto!important;background:#f8fafc!important;border:1px solid #dbe4f0!important;border-radius:8px!important;padding:4px!important;color:#0f172a!important}
#etl-auto-panel .etl-item{display:flex!important;gap:7px!important;align-items:flex-start!important;padding:5px 6px!important;border-bottom:1px dashed #e2e8f0!important;background:transparent!important;color:#0f172a!important}
#etl-auto-panel .etl-item label{flex:1!important;cursor:pointer!important;line-height:1.35!important;color:#0f172a!important;font-size:12px!important;font-weight:600!important;opacity:1!important}
#etl-auto-panel .etl-meta{display:block!important;color:#64748b!important;font-size:11px!important;margin-top:2px!important;font-weight:500!important;opacity:1!important}
#etl-auto-panel .etl-log-row{padding:3px 4px!important;border-bottom:1px dashed #e2e8f0!important;line-height:1.35!important;color:#0f172a!important;font-size:11px!important}
#etl-auto-panel .etl-log-row .etl-log-time{color:#64748b!important}
#etl-auto-panel .etl-log-info{color:#0f172a!important}
#etl-auto-panel .etl-log-ok{color:#166534!important}
#etl-auto-panel .etl-log-warn{color:#b45309!important}
#etl-auto-panel .etl-log-error{color:#b91c1c!important}
#etl-auto-panel #etl-panel-actions{padding:6px 8px!important;flex-shrink:0!important;border-top:1px solid #e2e8f0!important;background:#f8fafc!important}
#etl-auto-panel .etl-btn-row{display:flex!important;gap:6px!important;margin:0!important}
#etl-auto-panel .etl-btn{flex:1!important;border:none!important;color:#fff!important;padding:7px 8px!important;border-radius:9px!important;cursor:pointer!important;font-weight:800!important;font-size:12px!important}
#etl-auto-panel .etl-btn-start{background:#16a34a!important}
#etl-auto-panel .etl-btn-stop{background:#ef4444!important}
#etl-auto-panel .etl-btn:disabled{background:#e2e8f0!important;color:#94a3b8!important;cursor:not-allowed!important}
#etl-auto-panel .etl-btn-ghost{border:1px solid #cbd5e1!important;background:#fff!important;color:#0f172a!important;padding:3px 7px!important;border-radius:7px!important;cursor:pointer!important;font-size:11px!important;font-weight:700!important}
#etl-auto-panel .etl-btn-ghost:disabled{opacity:.45!important;cursor:not-allowed!important}
#etl-auto-panel #etl-face-preview img{max-width:100%!important;max-height:88px!important;border-radius:8px!important;border:1px solid #dbe4f0!important}
#etl-geetest-box{position:fixed!important;left:-10000px!important;top:0!important;opacity:0!important;z-index:-1!important}
#etl-auto-panel .etl-select,#etl-auto-panel .etl-plan{width:100%!important;border:1px solid #cbd5e1!important;border-radius:7px!important;padding:5px 7px!important;font-size:11px!important;background:#fff!important;margin-top:3px!important;color:#0f172a!important}
#etl-auto-panel .etl-hint{font-size:11px!important;color:#334155!important;background:#fff!important;border:1px solid #dbe4f0!important;border-radius:7px!important;padding:4px 6px!important;margin-bottom:5px!important;text-align:center!important;font-weight:600!important;display:none}
#etl-auto-panel .etl-ann{position:relative!important;font-size:11px!important;color:#334155!important;line-height:1.45!important;background:linear-gradient(180deg,#f8fbff,#eef6ff)!important;border:1px solid #bfdbfe!important;border-radius:10px!important;padding:8px 10px 8px 12px!important;margin-top:6px!important}
#etl-auto-panel .etl-ann::before{content:""!important;position:absolute!important;left:0!important;top:0!important;bottom:0!important;width:3px!important;background:linear-gradient(180deg,#3b82f6,#0ea5e9)!important;border-radius:10px 0 0 10px!important}
#etl-auto-panel .etl-ann-label{display:inline-block!important;font-size:10px!important;font-weight:800!important;color:#1d4ed8!important;margin-bottom:3px!important}
#etl-auto-panel #etl-ann-text{display:block!important;color:#334155!important;word-break:break-word!important}
#etl-auto-panel #etl-panel-footer{padding:5px 10px!important;background:#eef2f7!important;border-top:1px solid #dbe4f0!important;flex-shrink:0!important}
#etl-auto-panel .etl-qq{display:flex!important;justify-content:space-between!important;align-items:center!important;margin:0!important;font-size:10px!important;color:#64748b!important}
#etl-auto-panel .etl-qq a{color:#1d4ed8!important;font-weight:800!important;text-decoration:none!important}
#etl-auto-panel .etl-row{display:flex!important;justify-content:space-between!important;gap:6px!important;font-size:11px!important;margin:2px 0!important;color:#0f172a!important}
#etl-auto-panel .etl-row span{color:#64748b!important}
#etl-auto-panel .etl-row b{font-weight:800!important;color:#0f172a!important;text-align:right!important;flex:1!important;min-width:0!important;word-break:break-all!important}
#etl-auto-panel .etl-inline{display:flex!important;align-items:center!important;gap:6px!important;flex-wrap:wrap!important}
#etl-auto-panel .etl-inline .etl-select{flex:1!important;min-width:140px!important;margin-top:0!important}
#etl-auto-panel #etl-list-tag{font-size:10px!important;color:#334155!important;background:#f1f5f9!important;border:1px solid #cbd5e1!important;border-radius:999px!important;padding:1px 6px!important}
#etl-auto-panel input[type="checkbox"]{accent-color:#2563eb!important}
#etl-auto-panel input[type="text"],#etl-auto-panel input[type="password"]{width:100%!important;border:1px solid #cbd5e1!important;border-radius:7px!important;padding:5px 7px!important;font-size:11px!important;color:#0f172a!important;background:#fff!important}
#etl-auto-panel label{color:#334155!important}
#etl-face-modal{position:fixed!important;inset:0!important;z-index:2147483646!important;display:none;align-items:center!important;justify-content:center!important;background:rgba(15,23,42,.48)!important;padding:16px!important}
#etl-face-modal.etl-show{display:flex!important}
#etl-face-modal .etl-modal-card{width:min(360px,100%)!important;background:#fff!important;border-radius:14px!important;border:1px solid #dbe4f0!important;box-shadow:0 18px 40px rgba(15,23,42,.28)!important;padding:14px 16px!important;color:#0f172a!important}
#etl-face-modal .etl-modal-title{font-size:14px!important;font-weight:900!important;margin:0 0 8px!important;color:#0f172a!important}
#etl-face-modal .etl-modal-body{font-size:12px!important;line-height:1.55!important;color:#334155!important;margin:0 0 12px!important}
#etl-face-modal .etl-modal-preview{display:flex!important;justify-content:center!important;margin:0 0 12px!important}
#etl-face-modal .etl-modal-preview img{max-width:96px!important;max-height:96px!important;border-radius:10px!important;border:1px solid #dbe4f0!important;object-fit:cover!important}
#etl-face-modal .etl-modal-actions{display:flex!important;flex-direction:column!important;gap:7px!important}
#etl-face-modal .etl-modal-btn{border:none!important;border-radius:9px!important;padding:9px 10px!important;font-size:12px!important;font-weight:800!important;cursor:pointer!important}
#etl-face-modal .etl-modal-btn-primary{background:linear-gradient(135deg,#1d4ed8,#0ea5e9)!important;color:#fff!important}
#etl-face-modal .etl-modal-btn-ok{background:#16a34a!important;color:#fff!important}
#etl-face-modal .etl-modal-btn-ghost{background:#f8fafc!important;color:#334155!important;border:1px solid #cbd5e1!important}
`;
document.head.appendChild(st);
}
function renderLog() {
const box = $("etl-run-log");
if (!box) return;
if (!S.logs.length) {
box.innerHTML = `
\u6682\u65e0\u65e5\u5fd7
`;
return;
}
box.innerHTML = S.logs
.slice(0, 80)
.map((r) => {
const cls =
r.level === "ok"
? "etl-log-ok"
: r.level === "warn"
? "etl-log-warn"
: r.level === "error"
? "etl-log-error"
: "etl-log-info";
return `
${esc(r.t)} ${esc(r.msg)}
`;
})
.join("");
}
function renderLists() {
const box = $("etl-course-list");
if (!box) return;
if (!S.sections.length) {
box.innerHTML = `
\u6682\u65e0\u8bfe\u65f6\uff0c\u8bf7\u9009\u62e9\u57f9\u8bad\u5e76\u5237\u65b0
`;
return;
}
box.innerHTML = S.sections
.map((sec) => {
const id = sectionKey(sec);
const checked = isSectionChecked(sec) ? "checked" : "";
const done = sec.isFinish || /\u5df2\u5b66\u5b8c/.test(sec.status || "");
return `
`;
})
.join("");
box.querySelectorAll("input[data-sid]").forEach((inp) => {
inp.addEventListener("change", () => {
const id = inp.getAttribute("data-sid");
const on = !!inp.checked;
if (S.selectMode === "all") {
S.selectMode = "partial";
S.selectedIds = new Set(S.sections.map(sectionKey));
}
if (S.selectMode === "none") {
S.selectMode = "partial";
S.selectedIds = new Set();
}
if (on) S.selectedIds.add(id);
else S.selectedIds.delete(id);
if (S.selectedIds.size === 0) S.selectMode = "none";
else if (S.selectedIds.size >= S.sections.length) {
S.selectMode = "all";
S.selectedIds = new Set();
} else S.selectMode = "partial";
saveSelected();
updatePanel();
});
});
const tag = $("etl-list-tag");
if (tag) {
const doneN = S.sections.filter(
(s) => s.isFinish || /\u5df2\u5b66\u5b8c/.test(s.status || "")
).length;
tag.textContent = `${doneN}/${S.sections.length} \u5df2\u5b66\u5b8c`;
}
}
function renderProjects() {
const sel = $("etl-project");
if (!sel) return;
sel.innerHTML = S.projects
.map(
(p) =>
`
`
)
.join("");
}
function updatePanel() {
const statusEl = $("etl-auto-status");
if (statusEl) {
if (S.running || S.enabled) {
statusEl.textContent = S.running ? "\u8fd0\u884c\u4e2d" : "\u5df2\u5f00\u542f";
statusEl.style.background = "#dcfce7";
statusEl.style.borderColor = "#86efac";
statusEl.style.color = "#166534";
} else {
statusEl.textContent = "\u5df2\u505c\u6b62";
statusEl.style.background = "#fff";
statusEl.style.borderColor = "#cbd5e1";
statusEl.style.color = "#334155";
}
}
const done = S.queueDone;
const total = S.queueTotal;
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
const setText = (id, text) => {
const el = $(id);
if (el) el.textContent = text;
};
setText("etl-queue-done", String(done));
setText("etl-queue-total", String(total || S.sections.length || 0));
setText("etl-queue-percent", `${pct}%`);
const bar = $("etl-queue-progress");
if (bar) bar.style.width = `${pct}%`;
setText(
"etl-current-chapter",
trunc(S.currentChapter || S.currentCourse || "\u65e0", 32)
);
setText("etl-current-course", trunc(S.currentCourse || "\u65e0", 28));
setText("etl-current-task", trunc(S.task || "\u5f85\u547d", 36));
setText("etl-user-line", S.userName || S.userGuid || "—");
setText("etl-mode-line", studyModeLabel(S.pace));
try { refreshCloudUi(); } catch (_) {}
const hint = $("etl-start-hint");
if (hint) {
if (S.panelHint) {
hint.style.display = "";
hint.textContent = S.panelHint;
} else {
hint.style.display = "none";
}
}
const startBtn = $("etl-start");
const stopBtn = $("etl-stop");
const modeSel = $("etl-study-mode");
if (startBtn) {
startBtn.disabled = !!S.running;
}
if (stopBtn) {
stopBtn.disabled = !S.running;
}
if (modeSel) {
modeSel.disabled = !!S.running;
if (modeSel.value !== S.pace && (S.pace === PACE.efficiency || S.pace === PACE.realtime)) {
modeSel.value = S.pace;
}
}
}
function hideFaceFallbackModal() {
const modal = $("etl-face-modal");
if (modal) modal.classList.remove("etl-show");
}
function showFaceFallbackModal() {
return new Promise((resolve) => {
const modal = $("etl-face-modal");
if (!modal) {
resolve("cancel");
return;
}
const preview = $("etl-face-modal-preview");
const avatar = getAvatarUrl();
if (preview) {
preview.innerHTML = avatar
? `

`
: "";
}
const done = (v) => {
hideFaceFallbackModal();
modal.querySelectorAll("button").forEach((b) => {
b.onclick = null;
});
resolve(v);
};
const go = $("etl-face-go-capture");
const use = $("etl-face-use-avatar");
const cancel = $("etl-face-modal-cancel");
if (go) go.onclick = () => done("capture");
if (use) use.onclick = () => done("avatar");
if (cancel) cancel.onclick = () => done("cancel");
modal.classList.add("etl-show");
});
}
function switchTab(name) {
document.querySelectorAll(".etl-tab").forEach((b) => {
b.classList.toggle("etl-on", b.getAttribute("data-tab") === name);
});
document.querySelectorAll(".etl-pane").forEach((p) => {
p.classList.toggle("etl-on", p.id === `etl-pane-${name}`);
});
}
function enableDrag(panel) {
const header = $("etl-panel-header");
if (!header) return;
let dragging = false;
let ox = 0;
let oy = 0;
header.addEventListener("mousedown", (e) => {
if (e.target?.closest(".etl-ctl")) return;
dragging = true;
const rect = panel.getBoundingClientRect();
ox = e.clientX - rect.left;
oy = e.clientY - rect.top;
panel.style.setProperty("right", "auto", "important");
panel.style.setProperty("left", `${rect.left}px`, "important");
panel.style.setProperty("top", `${rect.top}px`, "important");
e.preventDefault();
});
window.addEventListener("mousemove", (e) => {
if (!dragging) return;
const maxL = Math.max(0, window.innerWidth - panel.offsetWidth);
const maxT = Math.max(0, window.innerHeight - 48);
const left = Math.min(maxL, Math.max(0, e.clientX - ox));
const top = Math.min(maxT, Math.max(0, e.clientY - oy));
panel.style.setProperty("left", `${left}px`, "important");
panel.style.setProperty("top", `${top}px`, "important");
panel.style.setProperty("right", "auto", "important");
});
window.addEventListener("mouseup", () => {
if (!dragging) return;
dragging = false;
GM_setValue(STORAGE.panelPos, {
left: parseInt(panel.style.left, 10) || 0,
top: parseInt(panel.style.top, 10) || 0,
});
});
}
function buildPanel() {
injectStyles();
const oldPanel = $("etl-auto-panel");
if (oldPanel) oldPanel.remove();
const panel = document.createElement("div");
panel.id = "etl-auto-panel";
panel.innerHTML = `
\u5df2\u505c\u6b62
0/0
· 0%
\u5f53\u524d\u65e0
\u4efb\u52a1\u5f85\u547d
\u65e0
\u672a\u9009\u62e9
\u8bfe\u65f6
0/0
\u5b66\u5458—
\u6388\u6743—
\u4f53\u9a8c—
\u5230\u671f\u65f6\u95f4—
\u4eba\u8138\uff08\u4e00\u6b21\u590d\u7528\uff09
\u672a\u91c7\u96c6
\u53ef\u62cd\u7167/\u4e0a\u4f20\uff1b\u7f3a\u7701\u7528\u4e2a\u4eba\u4e2d\u5fc3\u5934\u50cf\u515c\u5e95\u3002
\u8fd0\u884c\u65e5\u5fd7
\u516c\u544a
${esc(PANEL_NOTICE)}
`;
document.body.appendChild(panel);
if (!$("etl-face-modal")) {
const modal = document.createElement("div");
modal.id = "etl-face-modal";
modal.innerHTML = `
\u4eba\u8138\u7167\u7247\u672a\u91c7\u96c6
\u5f53\u524d\u6ca1\u6709\u4e0a\u4f20\u6216\u62cd\u7167\u7684\u4e2a\u4eba\u4eba\u8138\u7167\u7247\u3002
\u82e5\u7ee7\u7eed\u5f00\u59cb\uff0c\u5c06\u76f4\u63a5\u8c03\u7528\u4e2a\u4eba\u4e2d\u5fc3\u5934\u50cf\u505a\u4eba\u8138\u6838\u9a8c\u515c\u5e95\uff0c\u53ef\u80fd\u5f71\u54cd\u901a\u8fc7\u7387\u3002
`;
document.body.appendChild(modal);
}
const pos = GM_getValue(STORAGE.panelPos, null);
if (pos && typeof pos === "object") {
panel.style.setProperty("left", `${pos.left || 0}px`, "important");
panel.style.setProperty("top", `${pos.top || 64}px`, "important");
panel.style.setProperty("right", "auto", "important");
}
if (GM_getValue(STORAGE.collapsed, false)) {
panel.classList.add("etl-min");
}
enableDrag(panel);
$("etl-min")?.addEventListener("click", () => {
const on = panel.classList.toggle("etl-min");
GM_setValue(STORAGE.collapsed, on);
});
document.querySelectorAll(".etl-tab").forEach((btn) => {
btn.addEventListener("click", () => switchTab(btn.getAttribute("data-tab")));
});
$("etl-sel-all")?.addEventListener("click", () => {
S.selectMode = "all";
S.selectedIds = new Set();
saveSelected();
renderLists();
});
$("etl-sel-none")?.addEventListener("click", () => {
S.selectMode = "none";
S.selectedIds = new Set();
saveSelected();
renderLists();
});
$("etl-log-clear")?.addEventListener("click", () => {
S.logs = [];
renderLog();
});
$("etl-qq-link")?.addEventListener("click", (e) => {
e.preventDefault();
if (typeof GM_openInTab === "function") {
GM_openInTab(QQ_GROUP_LINK, { active: true, insert: true, setParent: true });
} else {
window.open(QQ_GROUP_LINK, "_blank");
}
});
$("etl-study-mode")?.addEventListener("change", (e) => {
S.pace = e.target.value || "";
GM_setValue(STORAGE.pace, S.pace);
S.panelHint = "";
updatePanel();
});
function refreshFaceStatus() {
const n = (S.faceImages.length ? S.faceImages : loadFaceImages()).length;
S.faceImages = n ? (S.faceImages.length ? S.faceImages : loadFaceImages()) : [];
const avatar = getAvatarUrl();
const st = $("etl-face-status");
const prev = $("etl-face-preview");
if (st) {
if (n) {
st.textContent = S.faceFromAvatar ? `\u5934\u50cf\u515c\u5e95 ${n} \u5f20` : `\u5df2\u7f13\u5b58 ${n} \u5f20`;
st.style.color = "#166534";
} else if (avatar) {
st.textContent = "\u5c06\u7528\u5934\u50cf\u515c\u5e95";
st.style.color = "#b45309";
} else {
st.textContent = "\u672a\u91c7\u96c6";
st.style.color = "#64748b";
}
}
if (prev) {
if (n && S.faceImages[0]) {
prev.style.display = "";
prev.innerHTML = `

`;
} else if (avatar && !S.faceStream) {
prev.style.display = "";
prev.innerHTML = `

`;
} else if (!S.faceStream) {
prev.style.display = "none";
prev.innerHTML = "";
}
}
}
async function stopFaceCamera() {
try {
if (S.faceStream) {
S.faceStream.getTracks().forEach((t) => t.stop());
}
} catch (_) {}
S.faceStream = null;
const v = $("etl-face-video");
if (v) {
v.srcObject = null;
v.style.display = "none";
}
const shot = $("etl-face-shot");
if (shot) shot.disabled = true;
}
async function importFaceDataUrl(dataUrl, tip) {
if (!dataUrl || !String(dataUrl).startsWith("data:image")) {
throw new Error("\u4e0d\u662f\u6709\u6548\u7684\u56fe\u7247");
}
const need = Math.max(1, Number(S.studyConfig.BaiDuImageNum) || 1);
const imgs = [];
for (let i = 0; i < need; i++) imgs.push(dataUrl);
S.faceFromAvatar = false;
GM_setValue(STORAGE.faceFromAvatar, false);
await persistFaceImages(imgs);
await stopFaceCamera();
refreshFaceStatus();
log(tip || `\u4eba\u8138\u5df2\u4fdd\u5b58\uff08${S.faceImages.length} \u5f20\uff09\uff0c\u5b66\u4e60\u65f6\u81ea\u52a8\u590d\u7528`, "ok");
S.panelHint = "";
updatePanel();
}
function readFileAsDataUrl(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result || ""));
reader.onerror = () => reject(new Error("\u8bfb\u53d6\u6587\u4ef6\u5931\u8d25"));
reader.readAsDataURL(file);
});
}
$("etl-face-upload")?.addEventListener("click", () => {
$("etl-face-file")?.click();
});
$("etl-face-file")?.addEventListener("change", async (e) => {
const file = e.target?.files && e.target.files[0];
e.target.value = "";
if (!file) return;
try {
if (!/^image\//i.test(file.type) && !/\.(jpe?g|png|webp|bmp)$/i.test(file.name || "")) {
throw new Error("\u8bf7\u9009\u62e9\u56fe\u7247\u6587\u4ef6\uff08jpg/png/webp\uff09");
}
if (file.size > 8 * 1024 * 1024) {
throw new Error("\u56fe\u7247\u8fc7\u5927\uff08\u8bf7\u5c0f\u4e8e 8MB\uff09");
}
const raw = await readFileAsDataUrl(file);
const normalized = await shrinkDataUrl(raw, 640, 0.82);
await importFaceDataUrl(normalized, `\u5df2\u4e0a\u4f20\u4eba\u8138\u7167\u7247\uff1a${file.name || "image"}`);
} catch (err) {
log(`\u4e0a\u4f20\u5931\u8d25\uff1a${err.message || err}`, "error");
S.panelHint = String(err.message || err);
updatePanel();
}
});
$("etl-face-open")?.addEventListener("click", async () => {
try {
await stopFaceCamera();
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "user", width: { ideal: 640 }, height: { ideal: 480 } },
audio: false,
});
S.faceStream = stream;
const v = $("etl-face-video");
if (v) {
v.style.display = "";
v.srcObject = stream;
await v.play();
}
const shot = $("etl-face-shot");
if (shot) shot.disabled = false;
log("\u6444\u50cf\u5934\u5df2\u6253\u5f00\uff0c\u8bf7\u6b63\u5bf9\u955c\u5934\u540e\u70b9\u300c\u62cd\u7167\u4fdd\u5b58\u300d", "info");
switchTab("settings");
} catch (e) {
log(`\u6253\u5f00\u6444\u50cf\u5934\u5931\u8d25\uff1a${e.message || e}`, "error");
S.panelHint = "\u65e0\u6cd5\u6253\u5f00\u6444\u50cf\u5934\uff0c\u8bf7\u68c0\u67e5\u6d4f\u89c8\u5668\u6743\u9650";
updatePanel();
}
});
$("etl-face-shot")?.addEventListener("click", async () => {
try {
const v = $("etl-face-video");
const c = $("etl-face-canvas");
if (!v || !c || !S.faceStream) throw new Error("\u8bf7\u5148\u6253\u5f00\u6444\u50cf\u5934");
const w = v.videoWidth || 640;
const h = v.videoHeight || 480;
c.width = w;
c.height = h;
c.getContext("2d").drawImage(v, 0, 0, w, h);
const raw = c.toDataURL("image/jpeg", 0.85);
await importFaceDataUrl(raw, `\u4eba\u8138\u5df2\u91c7\u96c6\u5e76\u7f13\u5b58\uff08${Math.max(1, Number(S.studyConfig.BaiDuImageNum) || 1)} \u5f20\uff09`);
} catch (e) {
log(`\u62cd\u7167\u4fdd\u5b58\u5931\u8d25\uff1a${e.message || e}`, "error");
}
});
$("etl-face-clear")?.addEventListener("click", async () => {
await stopFaceCamera();
saveFaceImages([]);
S.faceFromAvatar = false;
GM_setValue(STORAGE.faceFromAvatar, false);
refreshFaceStatus();
log(
getAvatarUrl()
? "\u5df2\u6e05\u9664\u4eba\u8138\u7f13\u5b58\uff1b\u4e0b\u6b21\u6838\u9a8c\u5c06\u81ea\u52a8\u7528\u4e2a\u4eba\u4e2d\u5fc3\u5934\u50cf\u515c\u5e95"
: "\u5df2\u6e05\u9664\u4eba\u8138\u7f13\u5b58",
"info"
);
});
refreshFaceStatus();
$("etl-project")?.addEventListener("change", async (e) => {
S.projectId = e.target.value || "";
GM_setValue(STORAGE.projectId, S.projectId);
S.selectMode = "all";
S.selectedIds = new Set();
saveSelected();
try {
await fetchSections(S.projectId);
renderLists();
log(`\u5df2\u5207\u6362\u57f9\u8bad ${S.projectId}\uff0c\u5171 ${S.sections.length} \u4e2a\u8bfe\u65f6`, "info");
} catch (err) {
log(`\u52a0\u8f7d\u8bfe\u65f6\u5931\u8d25\uff1a${err.message || err}`, "error");
}
updatePanel();
});
$("etl-refresh")?.addEventListener("click", async () => {
try {
await fetchUser();
await fetchProjects();
renderProjects();
if (S.projectId) {
await fetchStudyConfig(S.projectId);
await fetchSections(S.projectId);
}
renderLists();
log("\u5df2\u5237\u65b0\u57f9\u8bad\u4e0e\u8bfe\u65f6", "ok");
updatePanel();
} catch (err) {
log(`\u5237\u65b0\u5931\u8d25\uff1a${err.message || err}`, "error");
}
});
$("etl-cloud-save")?.addEventListener("click", async () => {
const v = String($("etl-cloud-token")?.value || "").trim();
S.cloudToken = v;
GM_setValue(STORAGE.cloudToken, v);
try {
if (v) await cloudRequest("/api/license/verify", "GET");
await ensureCloudLease(true);
log(v ? "\u6388\u6743\u7801\u5df2\u4fdd\u5b58" : "\u5df2\u5207\u6362\u4e3a\u4f53\u9a8c\u7248", "ok");
S.panelHint = "";
} catch (e) {
S.cloudTier = "free";
log(`\u6388\u6743\u6821\u9a8c\u5931\u8d25\uff1a${e.message || e}`, "warn");
S.panelHint = String(e.message || e);
}
updatePanel();
});
$("etl-cloud-clear")?.addEventListener("click", () => {
S.cloudToken = "";
S.cloudLease = "";
S.cloudLeaseExp = 0;
S.cloudTier = "free";
GM_setValue(STORAGE.cloudToken, "");
if ($("etl-cloud-token")) $("etl-cloud-token").value = "";
log("\u5df2\u6e05\u9664\u6388\u6743\u7801", "info");
updatePanel();
ensureCloudLease(true).then(() => updatePanel()).catch(() => {});
});
$("etl-cloud-buy")?.addEventListener("click", () => {
const url = String(S.proBuyUrl || PRO_BUY_URL_DEFAULT).trim() || PRO_BUY_URL_DEFAULT;
try {
GM_openInTab(url, { active: true, insert: true, setParent: true });
} catch (_) {
window.open(url, "_blank");
}
});
$("etl-start")?.addEventListener("click", async () => {
if (!S.pace || (S.pace !== PACE.efficiency && S.pace !== PACE.realtime)) {
S.panelHint = "\u8bf7\u5148\u5728\u300c\u8bfe\u65f6\u300d\u4e2d\u9009\u62e9\u5b66\u4e60\u6a21\u5f0f";
switchTab("course");
updatePanel();
return;
}
if (
(S.studyConfig.SDynamicCheckFaceOnOff || S.studyConfig.StudyFaceOnOff) &&
!(S.faceImages.length || loadFaceImages().length || getAvatarUrl())
) {
S.panelHint = "\u5e73\u53f0\u9700\u8981\u4eba\u8138\u6838\u9a8c\uff1a\u8bf7\u91c7\u96c6/\u4e0a\u4f20\uff0c\u6216\u5148\u5728\u6863\u6848\u4e0a\u4f20\u5934\u50cf";
switchTab("settings");
updatePanel();
return;
}
if (needsAvatarFaceConfirm()) {
const choice = await showFaceFallbackModal();
if (choice === "capture") {
switchTab("settings");
S.panelHint = "\u8bf7\u4e0a\u4f20\u6216\u62cd\u7167\u91c7\u96c6\u4eba\u8138\u540e\u518d\u5f00\u59cb";
updatePanel();
return;
}
if (choice !== "avatar") return;
S.faceFromAvatar = true;
GM_setValue(STORAGE.faceFromAvatar, true);
}
S.panelHint = "";
runLoop();
});
$("etl-stop")?.addEventListener("click", stopRun);
if (S.pace === PACE.efficiency || S.pace === PACE.realtime) {
const modeSel = $("etl-study-mode");
if (modeSel) modeSel.value = S.pace;
}
updatePanel();
renderLog();
}
async function boot() {
buildPanel();
renderPanelNotice();
try {
await fetchUser();
await fetchProjects();
renderProjects();
fetchClientConfig().catch(() => {});
if (S.userGuid) {
ensureCloudLease(true)
.then(() => updatePanel())
.catch(() => {});
}
if (S.projectId) {
try {
await fetchStudyConfig(S.projectId);
await fetchSections(S.projectId);
} catch (e) {
log(`\u9884\u52a0\u8f7d\u8bfe\u65f6\uff1a${e.message || e}`, "warn");
}
}
renderLists();
updatePanel();
log("\u52a9\u624b\u5df2\u5c31\u7eea\uff0c\u9009\u62e9\u6a21\u5f0f\u540e\u70b9\u51fb\u5f00\u59cb", "info");
} catch (e) {
log(`\u8bf7\u5148\u767b\u5f55\u5b66\u5458\u7aef\uff1a${e.message || e}`, "warn");
S.panelHint = "\u8bf7\u5148\u767b\u5f55\u540e\u518d\u4f7f\u7528\u52a9\u624b";
updatePanel();
fetchClientConfig().catch(() => {});
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", boot);
} else {
boot();
}
})();