// ==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 ? `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 = `
${esc(SCRIPT_NAME)} v${esc(VER)}
\u5df2\u505c\u6b62 0/0 · 0%
\u5f53\u524d\u65e0
\u4efb\u52a1\u5f85\u547d
\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 = ` `; 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 = `face`; } else if (avatar && !S.faceStream) { prev.style.display = ""; prev.innerHTML = `avatar`; } 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(); } })();