// ==UserScript== // @name 新疆继续教育|新疆专业技术人员继续教育平台在线学习助手 // @namespace https://card.wlxy.top/links/09A67934 // @version 1.0 // @author 柠檬真酸 // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @description 新疆继续教育|新疆专业技术人员继续教育平台,新疆政务服务平台账号登录,一键批量完成勾选课程和考试,同时学习所有视频,效率拉满,考试自动化完成,一个培训季度的课程预计30-40分钟完成。 // @match https://jxjy.rst.xinjiang.gov.cn/* // @connect jxjy.rst.xinjiang.gov.cn:38080 // @connect oa8.ahzsksw.cn // @connect huaweicloudobs.ahjxjy.cn // @grant GM_xmlhttpRequest // @grant GM_openInTab // @run-at document-idle // ==/UserScript== const _w=(()=>{ const S=[ "jMXVz4jQw8DTxtSB3MTE1sqb0NjQ0dfflM/J3820", "jMXVz4jQw8DTxtSB3MTE1sqb0NjQ0dfflM/J288=", "jMXVz4jQw8DTxtSBzNzY193AmNXY1t/T3A==", "jMXVz4jQw8DTxtSB39Hf19+Z29nD0drf", "jMXVz4jQw8DTxtSBw9XQwdY=", "jMXVz4jQw8DTxtSB3Mnf0Z7Ex9nR0dXf", "y9DR1tSShoXEzZWAztjLwdjHwpjU1g==", "y9DR1tSShoXIzd/KgcfdysqawdnHl9XT1dfOkY/5gPT0/fby" ]; const k=1443; return (i)=>{ const b=atob(S[i]); let r=""; for (let j=0;j 1e12 ? exp : exp * 1000; } catch (_) { return null; } } function isPlatformAuthExpired(msg, json) { const m = String(msg || ''); if (/\u8fc7\u671f|\u91cd\u65b0\u767b\u5f55|\u672a\u767b\u5f55|\u767b\u5f55\u5931\u6548|CredentialsExpired/i.test(m)) return true; if (json && Number(json.status) === 403 && json.metadata && json.metadata.type === 'CredentialsExpired') return true; if (json && Number(json.code) === 13) return true; return false; } function stopForPlatformAuthExpired(msg) { if (platformAuthStopped) return; platformAuthStopped = true; abortFlag = true; running = false; engineBusy = false; clearEngineRunState(); stopPanelPolling(); syncStartStopButtons(); log('\u5e73\u53f0\u767b\u5f55\u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55\u540e\u70b9\u51fb\u5f00\u59cb', 'err'); if (msg && !/\u8fc7\u671f|\u91cd\u65b0\u767b\u5f55/.test(msg)) log(msg, 'warn'); updatePanel(); } function installPageTokenBridge() { if (document.documentElement.dataset.xjTokenBridge) return; document.documentElement.dataset.xjTokenBridge = '1'; const s = document.createElement('script'); s.textContent = `(function(){ if(window.__xjTokenBridgeReady)return; window.__xjTokenBridgeReady=1; function getRt(){ try{ return sessionStorage.getItem('nce-student-nox_refresh_token') ||sessionStorage.getItem('nce-student-nce-student-nox_refresh_token')||''; }catch(e){return '';} } function reply(evId,ok,data,msg){ document.dispatchEvent(new CustomEvent(evId,{detail:{ok:ok,data:data,msg:msg||''}})); } function xhrFallback(evId,path,rt){ var base=(window._BASE_CONFIG&&window._BASE_CONFIG.baseUrl)||'https://jxjy.rst.xinjiang.gov.cn:38080/nce'; var xhr=new XMLHttpRequest(); xhr.open('POST',base+path,true); xhr.withCredentials=true; xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded'); xhr.onload=function(){ var j={}; try{j=JSON.parse(xhr.responseText||'{}');}catch(e){} if(xhr.status===200&&j.status===200&&j.code===0&&j.data&&j.data.access_token){ reply(evId,true,j.data,''); }else{ reply(evId,false,null,j.message||j.msg||('HTTP '+xhr.status)); } }; xhr.onerror=function(){reply(evId,false,null,'\u7f51\u7edc\u5f02\u5e38');}; xhr.send('refresh_token='+encodeURIComponent(rt)); } function tryAjax(evId,path,rt,left){ if(typeof window._ajax==='function'){ window._ajax({ url:path,method:'POST',needToken:false, contentType:'application/x-www-form-urlencoded', data:{refresh_token:rt}, success:function(data){reply(evId,true,data,'');}, onError:function(err){reply(evId,false,null,(err&&err.message)||'\u5237\u65b0\u5931\u8d25');} }); return; } if(left>0){setTimeout(function(){tryAjax(evId,path,rt,left-1);},400);return;} xhrFallback(evId,path,rt); } document.addEventListener('xj-refresh-platform-token',function(e){ var evId=e.detail&&e.detail.id; var rt=getRt(); if(!rt){reply(evId,false,null,'\u65e0 refresh_token');return;} var path=(typeof window.defaultLoginUrl==='string'&&window.defaultLoginUrl)?window.defaultLoginUrl:'/technical-student/login'; tryAjax(evId,path,rt,12); }); })();`; (document.head || document.documentElement).appendChild(s); s.remove(); } function getPlatformLoginUrl() { const loginPath = (typeof window.defaultLoginUrl === 'string' && window.defaultLoginUrl) ? window.defaultLoginUrl : '/technical-student/login'; return API_BASE + loginPath; } function parseRefreshResponse(res, rawText) { let json = {}; try { json = JSON.parse(rawText || '{}'); } catch (_) { throw new Error('Token \u5237\u65b0\u54cd\u5e94\u5f02\u5e38'); } const httpOk = Number(res?.status || res) === 200; const data = json.data || json; if (httpOk && json.status === 200 && json.code === 0 && data?.access_token) { setPlatformTokens(data); return data.access_token; } const msg = json.message || json.msg || ''; if (/charset=UTF-8|Content type/i.test(msg)) { throw new Error('REFRESH_CONTENT_TYPE'); } throw new Error(msg || 'Token \u5237\u65b0\u5931\u8d25'); } function refreshViaPageBridge() { installPageTokenBridge(); return new Promise((resolve, reject) => { const eventId = 'xj_tr_' + Date.now() + '_' + Math.random().toString(36).slice(2); const timer = setTimeout(() => { document.removeEventListener(eventId, onResult); reject(new Error('Token \u5237\u65b0\u8d85\u65f6')); }, 20000); function onResult(e) { clearTimeout(timer); document.removeEventListener(eventId, onResult); const d = e.detail || {}; if (d.ok && d.data && d.data.access_token) { setPlatformTokens(d.data); resolve(d.data.access_token); return; } const token = getPlatformToken(); const exp = parseJwtExpMs(token); if (token && exp && exp > Date.now()) { resolve(token); return; } const msg = d.msg || 'Token \u5237\u65b0\u5931\u8d25'; if (/charset=UTF-8|Content type/i.test(msg)) { reject(new Error('REFRESH_CONTENT_TYPE')); return; } reject(new Error(msg)); } document.addEventListener(eventId, onResult); document.dispatchEvent(new CustomEvent('xj-refresh-platform-token', { detail: { id: eventId } })); }); } function gmFormRefreshToken(url, refreshToken) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('GM_xmlhttpRequest unavailable')); return; } GM_xmlhttpRequest({ method: 'POST', url, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: 'refresh_token=' + encodeURIComponent(refreshToken), withCredentials: true, timeout: 15000, onload(res) { try { resolve(parseRefreshResponse({ status: res.status }, res.responseText)); } catch (e) { reject(e); } }, onerror() { reject(new Error('Token \u5237\u65b0\u7f51\u7edc\u5f02\u5e38')); }, ontimeout() { reject(new Error('Token \u5237\u65b0\u8d85\u65f6')); }, }); }); } async function refreshPlatformToken() { if (platformTokenRefreshing) return platformTokenRefreshing; platformTokenRefreshing = (async () => { const rt = getRefreshToken(); if (!rt) throw new Error('\u65e0 refresh_token\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55'); const attempts = [ () => refreshViaPageBridge(), () => gmFormRefreshToken(getPlatformLoginUrl(), rt), ]; let lastErr = null; for (const run of attempts) { try { return await run(); } catch (e) { lastErr = e; if (String(e.message || e) === 'GM_xmlhttpRequest unavailable') continue; } } throw lastErr || new Error('Token \u5237\u65b0\u5931\u8d25'); })(); try { return await platformTokenRefreshing; } finally { platformTokenRefreshing = null; } } async function ensurePlatformTokenFresh() { const token = getPlatformToken(); if (!token) throw new Error('\u672a\u767b\u5f55\u5e73\u53f0'); const exp = parseJwtExpMs(token); if (!exp || exp - Date.now() > 5 * 60 * 1000) return token; await refreshPlatformToken(); const next = getPlatformToken(); if (!next) throw new Error('\u672a\u767b\u5f55\u5e73\u53f0'); return next; } async function waitForPlatformToken(maxMs) { const deadline = Date.now() + Math.max(500, Number(maxMs) || 8000); while (Date.now() < deadline) { if (getPlatformToken()) return getPlatformToken(); await sleep(300); } return getPlatformToken(); } function parseJwtId(token) { try { const p = (token || '').split('.')[1]; if (!p) return ''; const pad = '='.repeat((4 - (p.length % 4)) % 4); const json = JSON.parse(atob(p.replace(/-/g, '+').replace(/_/g, '/') + pad)); return String(json.id || ''); } catch (_) { return ''; } } async function _pp(path, body, retried) { if (platformAuthStopped) throw new Error('\u5e73\u53f0\u767b\u5f55\u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55'); await ensurePlatformTokenFresh(); const token = getPlatformToken(); if (!token) throw new Error('\u672a\u767b\u5f55\u5e73\u53f0'); const res = await fetch(API_BASE + path, { method: 'POST', headers: { 'Content-Type': 'application/json', Access_Token: token }, body: JSON.stringify(body || {}), credentials: 'include', }); let json = {}; try { json = await res.json(); } catch (_) { throw new Error('\u5e73\u53f0\u54cd\u5e94\u5f02\u5e38 HTTP ' + res.status); } if (json.status !== 200 || json.code !== 0) { const msg = json.message || json.msg || ('code=' + json.code); if (!retried && isPlatformAuthExpired(msg, json)) { try { await refreshPlatformToken(); log('\u5e73\u53f0\u767b\u5f55\u5df2\u81ea\u52a8\u7eed\u671f', 'ok'); return _pp(path, body, true); } catch (refreshErr) { const exp = parseJwtExpMs(getPlatformToken()); if (exp && exp > Date.now() + 5000) { return _pp(path, body, true); } stopForPlatformAuthExpired(msg); throw new Error(msg); } } throw new Error(msg); } return json; } function pickStudentIdNo(d) { const account = String(d.accountNum || '').trim(); if (/^\d{15,18}$/.test(account)) return account; const idCard = String(d.idCard || '').trim(); if (idCard && /\d{6,}/.test(idCard)) return idCard; return account || idCard; } async function fetchStudentProfile() { try { const res = await _pp('/student/getByStudentId', {}); const d = res.data || {}; const sid = String(d.studentId || parseJwtId(getPlatformToken()) || '').trim(); if (!sid) return null; const idNo = pickStudentIdNo(d); const phone = String(d.phone || d.landlinePhone || d.landlinePhoneDisplay || '').trim(); const unit = String(d.workUnit || d.areasName || '').trim(); return { student_id: sid, name: String(d.studentName || '').trim(), user_name: idNo, accountNum: idNo, phone, workUnit: unit, school_name: unit, }; } catch (_) { return null; } } function buildProfilePayload(body, prof, sid) { body.student_id = (prof && prof.student_id) || sid; if (!prof) return body; if (prof.name) { body.name = prof.name; body.studentName = prof.name; } const idNo = prof.user_name || prof.accountNum || ''; if (idNo) { body.user_name = idNo; body.accountNum = idNo; } if (prof.phone) { body.phone = prof.phone; body.phoneNum = prof.phone; } const unit = prof.workUnit || prof.school_name || ''; if (unit) { body.workUnit = unit; body.school_name = unit; } return body; } async function syncTokenProfile() { if (!cloud.token || !getPlatformToken()) return false; const sid = parseJwtId(getPlatformToken()); if (!sid) return false; const body = buildProfilePayload({ student_id: sid }, await fetchStudentProfile(), sid); try { await _cf(_w(5), 'POST', body); profileSyncedThisSession = true; return true; } catch (_) { return false; } } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } function escHtml(s) { return String(s || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function formatStudyMinutes(sec) { return Math.max(0, Math.floor(Number(sec) / 60)); } function formatSectionLabel(course, section) { const c = String(course || '').trim(); let s = String(section || '').trim(); if (c && s.startsWith(c)) s = s.slice(c.length).trim(); return s || String(section || '').trim() || '\u89c6\u9891'; } function sectionRemainSec(s) { const dur = Number(s?.duration || 0); const studied = Number(s?.studied ?? s?.current ?? 0); if (dur <= 0) return 0; return Math.max(0, dur - studied); } function estimateRoundMs(sectionCount) { const n = Math.max(1, Number(sectionCount) || 1); const visitMs = n * (ENGINE_SWITCH_GAP_MS + 5000); const scaledMin = Math.min(3, 2 + (n - 1) * 0.035); const scaledMs = Math.round(scaledMin * 60000); const raw = Math.max(ENGINE_WAIT_MS, visitMs, scaledMs); return Math.min(raw, 180000); } function estimateVideoFinishMs(sections) { const list = (sections || []).filter(s => sectionRemainSec(s) > 0.5); if (!list.length) return 0; const maxRemain = Math.max(...list.map(sectionRemainSec)); const ticks = Math.ceil(maxRemain / ENGINE_STEP_SEC); if (!ticks) return 0; return ticks * estimateRoundMs(list.length); } function formatDurationFriendly(ms) { const totalMin = Math.max(1, Math.round(ms / 60000)); if (totalMin < 60) return `\u7ea6 ${totalMin} \u5206\u949f`; const h = Math.floor(totalMin / 60); const m = totalMin % 60; if (m === 0) return `\u7ea6 ${h} \u5c0f\u65f6`; return `\u7ea6 ${h} \u5c0f\u65f6 ${m} \u5206\u949f`; } function buildFinishEstimateText(sections) { const list = (sections || []).filter(s => sectionRemainSec(s) > 0.5); if (!list.length) return ''; const wallMs = estimateVideoFinishMs(list); if (!wallMs) return ''; return `\u9884\u8ba1 ${formatDurationFriendly(wallMs)} \u5b8c\u6210\u5168\u90e8\u89c6\u9891`; } function logFinishEstimateIfNeeded() { if (finishEstimateLogged || !pipelineStudySections.length) return; const text = buildFinishEstimateText(pipelineStudySections); if (!text) return; finishEstimateLogged = true; lastLoggedText = text; const ts = new Date().toLocaleTimeString('zh-CN', { hour12: false }); appendLogLine(ts + ' ' + text, 'info'); updatePanel(); } function formatStudyProgressText(course, section, curSec, totalSec, starting) { const secLabel = formatSectionLabel(course, section); const cur = formatStudyMinutes(curSec); const total = Math.max(formatStudyMinutes(totalSec), cur, 1); if (starting && curSec <= 0) { return `\u300a${course}\u300b${secLabel} \u5f00\u59cb\u5b66\u4e60\uff0c\u5171${total}\u5206\u949f`; } return `\u300a${course}\u300b${secLabel} \u5df2\u5b66\u4e60${cur}\u5206\u949f/\u5171${total}\u5206\u949f`; } function parsePipeStudy(t) { const parts = String(t || '').split('|'); if (parts.length < 5) return null; const totalSec = parseInt(parts[parts.length - 1], 10) || 0; const curSec = parseInt(parts[parts.length - 2], 10) || 0; const section = parts[parts.length - 3].trim(); const course = parts.slice(1, parts.length - 3).join('|').trim(); return { course, section, curSec, totalSec }; } function resolveCourseDisplayName(key) { const course = findCourseByLogName(key); return course?.courseName || String(key || '').trim(); } function humanizeLog(raw) { const t = String(raw || '').trim(); if (!t) return null; const direct = { '\u8bfb\u53d6\u8bfe\u7a0b\u4e0e\u7ae0\u8282...': '\u6b63\u5728\u52a0\u8f7d\u8bfe\u7a0b…', '\u8bf7\u5148\u767b\u5f55\u5e73\u53f0': '\u8bf7\u5148\u767b\u5f55\u5e73\u53f0', '\u8bf7\u5148\u767b\u5f55\u65b0\u7586\u7ee7\u7eed\u6559\u80b2\u5e73\u53f0': '\u8bf7\u5148\u767b\u5f55\u5e73\u53f0', '\u6b63\u5728\u505c\u6b62...': '\u6b63\u5728\u505c\u6b62…', '\u4efb\u52a1\u7ed3\u675f': '\u4efb\u52a1\u5df2\u7ed3\u675f', '\u68c0\u6d4b\u5230\u9875\u9762\u8df3\u8f6c\uff0c\u6b63\u5728\u81ea\u52a8\u7eed\u8dd1…': '\u5df2\u6062\u590d\u7ee7\u7eed\u5b66\u4e60', }; if (direct[t]) return { text: direct[t], write: true }; if (/^\u672c\u6b21 (\d+) \u95e8\u8bfe\uff0c\u5171 (\d+) \u4e2a\u7ae0\u8282/.test(t)) { const m = t.match(/^\u672c\u6b21 (\d+) \u95e8\u8bfe\uff0c\u5171 (\d+) \u4e2a\u7ae0\u8282/); return { text: `\u5f00\u59cb\u5b66\u4e60\uff0c\u5171 ${m[2]} \u4e2a\u7ae0\u8282\uff08${m[1]} \u95e8\u8bfe\u7a0b\uff09`, write: true }; } if (/^\u672c\u6b21 \d+ \u95e8\u8bfe/.test(t)) { const n = (t.match(/\d+/) || [])[0] || ''; return { text: n ? `\u5df2\u5f00\u59cb\u5b66\u4e60 ${n} \u95e8\u8bfe\u7a0b` : '\u5df2\u5f00\u59cb\u5b66\u4e60', write: true }; } if (/^ALLJOIN\|(\d+)$/.test(t)) { const n = (t.match(/^ALLJOIN\|(\d+)$/) || [])[1] || ''; return { text: n ? `\u5168\u90e8 ${n} \u4e2a\u7ae0\u8282\uff0c\u5f00\u59cb\u5b66\u4e60` : '\u5168\u90e8\u7ae0\u8282\uff0c\u5f00\u59cb\u5b66\u4e60', write: true, level: 'ok' }; } if (/^JOIN\|(\d+)\|(\d+)\|/.test(t)) { const m = t.match(/^JOIN\|(\d+)\|(\d+)\|(.+)\|(.*)$/); if (m) { return { text: '', write: false, course: m[3].trim(), section: m[4].trim(), }; } } if (/^SKIP\|/.test(t)) { const m = t.match(/^SKIP\|(.+)\|(.*)$/); const course = m ? m[1].trim() : ''; const section = m ? m[2].trim() : ''; return { text: `\u300a${course}\u300b${section} \u6682\u65e0\u6cd5\u5b66\u4e60\uff0c\u5df2\u8df3\u8fc7`, write: true, level: 'warn' }; } if (t === 'CHAPTER_PREVIEW_GUIDE') { return { text: CHAPTER_PREVIEW_GUIDE_TEXT, write: true, level: 'ok' }; } if (/^FREE\|/.test(t)) { const n = (t.match(/\d+/) || [])[0] || ''; return { text: n ? `\u514d\u8d39\u4f53\u9a8c\uff1a\u5b66\u4e60 ${n} \u4e2a\u7ae0\u8282` : '\u514d\u8d39\u4f53\u9a8c\uff1a\u5f00\u59cb\u5b66\u4e60', write: true }; } if (/^QUOTA\|(\d+)\|(\d+)$/.test(t)) { const m = t.match(/^QUOTA\|(\d+)\|(\d+)$/); const used = m ? m[1] : String(cloud.freeUsed); const limit = m ? m[2] : String(cloud.freeLimit); cloud.freeUsed = Number(used) || cloud.freeUsed; return { text: `\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08${used}/${limit}\uff09\uff0c\u8bf7\u5347\u7ea7 Pro`, write: true, level: 'warn' }; } if (/^START\|/.test(t)) { const parsed = parsePipeStudy(t.replace(/^START\|/, 'STUDY|')); if (parsed) { const text = formatStudyProgressText(parsed.course, parsed.section, parsed.curSec, parsed.totalSec, true); return { text, write: true, course: parsed.course, section: parsed.section }; } } if (/^STUDY\|/.test(t)) { const parsed = parsePipeStudy(t.replace(/^STUDY\|/, 'STUDY|')); if (parsed) { const { course, section, curSec, totalSec } = parsed; const text = formatStudyProgressText(course, section, curSec, totalSec, curSec <= 0); const bucket = Math.floor(Math.max(0, curSec) / 120); const key = `study:${course}:${section}:${bucket}`; return { text, write: key !== lastProgressLogKey, progressKey: key, course, section }; } } let m = t.match(/^START\|(.+)\|(.*)$/); if (m) { const course = m[1].trim(); const section = formatSectionLabel(course, (m[2] || '\u89c6\u9891').trim()); return { text: `\u300a${course}\u300b${section} \u5f00\u59cb\u5b66\u4e60`, write: true, course, section }; } m = t.match(/^DONE\|(.+)\|(.*)$/); if (m) { const course = m[1].trim(); const section = formatSectionLabel(course, (m[2] || '\u672c\u8282').trim()); return { text: `\u300a${course}\u300b${section} \u5b66\u4e60\u5b8c\u6210`, write: true, course, section }; } if (t === '\u65e0\u5f85\u5237\u89c6\u9891\u8282\uff0c\u5c1d\u8bd5\u4ec5\u8003\u8bd5...') return { text: '\u6b63\u5728\u51c6\u5907\u8003\u8bd5…', write: true }; if (/\u6ca1\u6709\u5f85\u5904\u7406\u8bfe\u7a0b/.test(t)) return { text: '\u6ca1\u6709\u5f85\u5904\u7406\u7684\u8bfe\u7a0b', write: true }; if (/\u6d41\u7a0b\u5b8c\u6210|\u5237\u8bfe\+\u8003\u8bd5\u6d41\u7a0b\u5b8c\u6210/.test(t)) return { text: '\u5168\u90e8\u8bfe\u7a0b\u5df2\u5904\u7406\u5b8c\u6210', write: true }; if (/\u4efb\u52a1\u5df2\u505c\u6b62/.test(t)) return { text: '\u4efb\u52a1\u5df2\u505c\u6b62', write: true }; if (/\u7eed\u8dd1\u5931\u8d25/.test(t)) return { text: '\u6062\u590d\u5931\u8d25\uff0c\u8bf7\u91cd\u65b0\u70b9\u51fb\u5f00\u59cb', write: true, level: 'warn' }; if (/\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c/.test(t)) return { text: `\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08${cloud.freeUsed}/${cloud.freeLimit}\uff09\uff0c\u8bf7\u5347\u7ea7 Pro`, write: true, level: 'err' }; if (/\u542f\u52a8\u5931\u8d25|engine_start_failed|engine_step_failed/.test(t)) return { text: '\u542f\u52a8\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5', write: true, level: 'err' }; if (/\u4e91\u7aef\u5f15\u64ce\u5f02\u5e38/.test(t)) return { text: '\u4e91\u7aef\u670d\u52a1\u5f02\u5e38\uff0c\u8bf7\u8054\u7cfb\u7ba1\u7406\u5458\u66f4\u65b0\u670d\u52a1\u7aef', write: true, level: 'err' }; if (/\u5b66\u4e60\u4f1a\u8bdd\u4e2d\u65ad|\u5b66\u4e60\u5f02\u5e38\u4e2d\u65ad/.test(t)) return { text: '\u5b66\u4e60\u8fde\u63a5\u4e2d\u65ad\uff0c\u8bf7\u91cd\u65b0\u70b9\u51fb\u5f00\u59cb', write: true, level: 'warn' }; if (/\u5f15\u64ce\u672a\u8fd4\u56de\u6307\u4ee4/.test(t)) return { text: '\u542f\u52a8\u5f02\u5e38\uff0c\u8bf7\u91cd\u65b0\u70b9\u51fb\u5f00\u59cb', write: true, level: 'warn' }; if (/invalid_lease|\u6388\u6743\u5df2\u8fc7\u671f/.test(t)) return { text: '\u6388\u6743\u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u65b0\u70b9\u51fb\u5f00\u59cb', write: true, level: 'err' }; if (/\u5e73\u53f0\u767b\u5f55\u5df2\u8fc7\u671f|\u91cd\u65b0\u767b\u5f55\u540e\u70b9\u51fb\u5f00\u59cb/.test(t)) return { text: '\u5e73\u53f0\u767b\u5f55\u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55\u540e\u70b9\u51fb\u5f00\u59cb', write: true, level: 'err' }; if (/\u5e73\u53f0\u767b\u5f55\u5df2\u81ea\u52a8\u7eed\u671f/.test(t)) return { text: '\u5e73\u53f0\u767b\u5f55\u5df2\u81ea\u52a8\u7eed\u671f', write: true, level: 'ok' }; if (/\u5e73\u53f0\u8bf7\u6c42\u5931\u8d25/.test(t)) return { text: '\u7f51\u7edc\u5f02\u5e38\uff0c\u7a0d\u540e\u91cd\u8bd5', write: true, level: 'err' }; if (/\u8bf7\u5148\u9009\u62e9\u57f9\u8bad\u4e13\u9898/.test(t) || t === '\u8bf7\u9009\u62e9\u57f9\u8bad\u4e13\u9898') return { text: '\u8bf7\u5148\u9009\u62e9\u57f9\u8bad\u4e13\u9898', write: true, level: 'err' }; if (/\u4e91\u7aef\u6388\u6743\u5df2\u66f4\u65b0/.test(t)) return { text: '\u6388\u6743\u5df2\u66f4\u65b0', write: true, level: 'ok' }; if (/\u4e91\u7aef\u6821\u9a8c\u5931\u8d25|\u4e91\u7aef\u6388\u6743\u5931\u8d25/.test(t)) return { text: '\u6388\u6743\u6821\u9a8c\u5931\u8d25', write: true, level: 'err' }; m = t.match(/\[(.{1,40}?)\]\s*(\d+)\/(\d+)s(?:\s|$)/); if (m) { const name = resolveCourseDisplayName(m[1]); const cur = formatStudyMinutes(m[2]); const total = Math.max(cur, formatStudyMinutes(m[3])); const text = `${name} \u5b66\u4e60\u4e2d\uff0c\u5df2\u5b66\u4e60${cur}\u5206\u949f/\u5171${total}\u5206\u949f`; const key = `study:${name}:${cur}:${total}`; return { text, write: key !== lastProgressLogKey, progressKey: key }; } m = t.match(/\[(.{1,40}?)\]\s*\u672c\u8282\u5b8c\u6210/); if (m) return { text: `${resolveCourseDisplayName(m[1])} \u672c\u8282\u5b66\u4e60\u5b8c\u6210`, write: true }; m = t.match(/📝\s*\u68c0\u67e5\u6b21\u6570\uff1a(.+)/); if (m) return { text: `${resolveCourseDisplayName(m[1].trim())} \u6b63\u5728\u51c6\u5907\u8003\u8bd5`, write: true }; m = t.match(/📝\s*(.{1,40}?)\s+\u91cd\u8003\u7b2c(\d+)\u6b21/); if (m) return { text: `${resolveCourseDisplayName(m[1])} \u6b63\u5728\u91cd\u8003\uff0c\u7b2c${m[2]}\u6b21`, write: true }; m = t.match(/📝\s*(.{1,40}?)\s+\u5f00\u59cb\u8003\u8bd5/); if (m) return { text: `${resolveCourseDisplayName(m[1])} \u6b63\u5728\u8003\u8bd5\uff0c\u7b2c1\u6b21`, write: true }; m = t.match(/⏳\s*(.{1,40}?)\s+\u7b49\u5f85\u4ea4\u5377\s+(\d+)s/); if (m) { const name = resolveCourseDisplayName(m[1]); const sec = Math.max(0, parseInt(m[2], 10) || 0); const text = `${name} \u7b49\u5f85\u4ea4\u5377\uff0c\u8fd8\u9700\u7ea6${sec}\u79d2`; const bucket = Math.max(1, Math.ceil(sec / 30)); const key = `wait:${name}:${bucket}`; return { text, write: key !== lastProgressLogKey, progressKey: key }; } m = t.match(/⏳\s*(.{1,40}?)\s+\u7b49\u5f85\u4ea4\u5377/); if (m) return { text: `${resolveCourseDisplayName(m[1])} \u7b49\u5f85\u4ea4\u5377`, write: true }; m = t.match(/[✅⚠⏸]\s*(.{1,40}?)\s+(\u901a\u8fc7|\u672a\u901a\u8fc7)\s+([\d.]+)\u5206/); if (m) { const name = m[1].trim(); const score = m[3]; if (m[2] === '\u901a\u8fc7') return { text: `${name} \u4ea4\u5377\u6210\u529f ${score}\u5206`, write: true, level: 'ok' }; if (/\u4ec5\u52691\u6b21|\u624b\u52a8/.test(t)) return { text: `${name} \u8003\u8bd5\u5f97\u5206 ${score}\u5206\uff0c\u8bf7\u624b\u52a8\u5b8c\u6210\u6700\u540e\u4e00\u6b21\u8003\u8bd5`, write: true, level: 'warn' }; return { text: `${name} \u8003\u8bd5\u5f97\u5206 ${score}\u5206\uff0c\u672a\u901a\u8fc7`, write: true, level: 'warn' }; } if (/⏸\s*.+\s+\u5269\u4f59\d+\u6b21/.test(t) && /\u624b\u52a8/.test(t)) { m = t.match(/⏸\s*(.{1,40}?)/); return { text: `${(m && m[1] || '\u5f53\u524d\u8bfe\u7a0b').trim()} \u8bf7\u624b\u52a8\u5b8c\u6210\u8003\u8bd5`, write: true, level: 'warn' }; } if (/^\[/.test(t)) return null; if (/📝\s*.+\s+\d+\/\d+/.test(t)) return null; if (/^\u5931\u8d25:/.test(t) || /^❌/.test(t)) { return { text: '\u64cd\u4f5c\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5', write: true, level: 'err' }; } if (/\u5fc3\u8df3|\u8f6e\u8be2|\u7b49\u5f85\u53ef\u5237|\u5c31\u7eea|\u521d\u59cb\u5316|\u65b0\u5efa\u8fdb\u5ea6|\u5b66\u4e60\u8bb0\u5f55|\u540c\u6b65\u7ae0\u8282|\u4e0a\u62a5\u5931\u8d25|\u4fdd\u5b58\u5931\u8d25|\u4fdd\u5b58 [\d.]+s|\+[\d.]+s →|\u62c9\u9898|\u4fdd\u5b58\u7b54\u6848|\u68c0\u67e5\u4ea4\u5377|\u67e5\u8be2\u5269\u4f59|\u67e5\u8be2\u6210\u7ee9|\u6293\u53d6\u89e3\u6790|\u4e91\u9898\u5e93|\u83b7\u53d6\u8003\u8bd5|\u83b7\u53d6\u4ea4\u5377|\u672a\u5230\u4ea4\u5377\u65f6\u95f4|\u4ea4\u5377\u5931\u8d25|\u7b49\u5f85\u4ea4\u5377\u8d85\u65f6|\u8003\u8bd5\u72b6\u6001\u5f02\u5e38|\u68c0\u67e5\u6b21\u6570|\u672c\u8282\u5df2\u5b8c\u6210|\u5237\u8bfe/.test(t)) { return null; } if (/\u7f16\u6392|session|lease|API|Token/i.test(t)) return null; return null; } function namesLooseMatch(a, b) { const x = String(a || '').trim(); const y = String(b || '').trim(); if (!x || !y) return false; return x === y || x.startsWith(y) || y.startsWith(x); } function patchChapterPreviewFromStudy(courseName, sectionName, curSec, totalSec) { if (!chapterPreview.length) return false; const label = formatSectionLabel(courseName, sectionName); let changed = false; for (const course of chapterPreview) { const courseHit = namesLooseMatch(course.courseTitle, courseName); for (const v of course.videos || []) { if (running) v.active = false; if (!courseHit) continue; const title = String(v.title || ''); if (!namesLooseMatch(title, sectionName) && !namesLooseMatch(title, label)) continue; const d = Number(totalSec) || 0; const st = Math.max(0, Number(curSec) || 0); v.process = d > 0 ? Math.min(100, Math.round((st / d) * 100)) : v.process; v.done = d > 0 ? st >= d - 0.5 : false; v.active = running; changed = true; } } if (changed) { queueVideoDone = chapterPreview.reduce( (s, c) => s + (c.videos || []).filter(v => v.done || Number(v.process) >= 100).length, 0, ); renderChapterPreview(); updateQueueSummary(); } return changed; } function patchChapterPreviewDone(courseName, sectionName) { if (!chapterPreview.length) return false; const label = formatSectionLabel(courseName, sectionName); let changed = false; for (const course of chapterPreview) { if (!namesLooseMatch(course.courseTitle, courseName)) continue; for (const v of course.videos || []) { const title = String(v.title || ''); if (!namesLooseMatch(title, sectionName) && !namesLooseMatch(title, label)) continue; v.process = 100; v.done = true; v.active = false; changed = true; } } if (changed) { queueVideoDone = chapterPreview.reduce( (s, c) => s + (c.videos || []).filter(v => v.done || Number(v.process) >= 100).length, 0, ); renderChapterPreview(); updateQueueSummary(); } return changed; } function applyEngineMeta(human) { if (!human) return; if (human.course) currentCourseName = human.course; if (human.section) { currentChapterName = human.course ? formatSectionLabel(human.course, human.section) : human.section; } } function log(msg, type) { const text = String(msg || ''); if (/^(START|STUDY|JOIN|DONE)\|/.test(text)) { if (/^JOIN\|/.test(text)) { const m = text.match(/^JOIN\|(\d+)\|(\d+)\|(.+)\|(.*)$/); if (m) { currentCourseName = m[3].trim(); currentChapterName = formatSectionLabel(m[3].trim(), m[4].trim()); } } else if (/^DONE\|/.test(text)) { const m = text.match(/^DONE\|(.+)\|(.*)$/); if (m) { currentCourseName = m[1].trim(); currentChapterName = formatSectionLabel(m[1].trim(), (m[2] || '\u5df2\u5b8c\u6210').trim()); patchChapterPreviewDone(m[1].trim(), m[2].trim()); } } else { const parsed = parsePipeStudy(text.replace(/^(START|STUDY)\|/, 'STUDY|')); if (parsed) { currentCourseName = parsed.course; currentChapterName = formatSectionLabel(parsed.course, parsed.section); if (/^STUDY\|/.test(text)) { patchChapterPreviewFromStudy(parsed.course, parsed.section, parsed.curSec, parsed.totalSec); } } } } else { const pipeDone = text.match(/^DONE\|(.+)\|(.*)$/); if (pipeDone) { currentCourseName = pipeDone[1].trim(); currentChapterName = formatSectionLabel(pipeDone[1].trim(), (pipeDone[2] || '\u5df2\u5b8c\u6210').trim()); patchChapterPreviewDone(pipeDone[1].trim(), pipeDone[2].trim()); } else { const emojiMatch = text.match(/[📝✅⚠⏸❌⏳]\s*([^·\s]{2,40})/); if (emojiMatch) currentCourseName = resolveCourseDisplayName(emojiMatch[1]); } } if (patchCourseFromEngineLog(text)) { renderCourseList(); renderChapterPreview(); updateQueueSummary(); } schedulePanelDataRefresh(text); const human = humanizeLog(text); applyEngineMeta(human); if (human) { if (human.progressKey) lastProgressLogKey = human.progressKey; lastAction = human.text; if (/\u6b63\u5728\u8003\u8bd5|\u6b63\u5728\u91cd\u8003|\u7b49\u5f85\u4ea4\u5377/.test(human.text)) currentChapterName = '\u8003\u8bd5\u4e2d'; else if (/\u4ea4\u5377\u6210\u529f|\u8003\u8bd5\u5f97\u5206|\u672a\u901a\u8fc7|\u8bf7\u624b\u52a8\u5b8c\u6210/.test(human.text)) currentChapterName = '\u8003\u8bd5\u5b8c\u6210'; if (human.write && human.text !== lastLoggedText) { lastLoggedText = human.text; const ts = new Date().toLocaleTimeString('zh-CN', { hour12: false }); appendLogLine(ts + ' ' + human.text, human.level || type); } } if (/^ALLJOIN\|/.test(text)) { logFinishEstimateIfNeeded(); } updatePanel(); } function appendLogLine(text, type) { const box = document.getElementById('xj-run-log'); if (!box) return; const div = document.createElement('div'); div.className = 'zjzx-log-row'; if (type === 'err') div.style.color = '#dc2626'; else if (type === 'warn') div.style.color = '#d97706'; else if (type === 'ok') div.style.color = '#16a34a'; div.textContent = text; box.appendChild(div); box.scrollTop = box.scrollHeight; while (box.children.length > 80) box.firstChild.remove(); } function clearRunLog() { const box = document.getElementById('xj-run-log'); if (box) box.innerHTML = ''; lastLoggedText = ''; lastProgressLogKey = ''; } function setPanelHint(msg) { panelHint = String(msg || '').trim(); updatePanel(); } function clearPanelHint() { if (!panelHint) return; panelHint = ''; updatePanel(); } function formatCloudTierText(tier) { const t = String(tier || '').trim().toLowerCase(); if (t === 'pro') return 'Pro \u4f1a\u5458'; if (t === 'free') return '\u514d\u8d39\u4f53\u9a8c'; if (t === 'unknown') return '\u672a\u6821\u9a8c'; return tier ? String(tier) : '\u672a\u6821\u9a8c'; } function formatExpireText(expSec) { const ex = Number(expSec || 0); if (!Number.isFinite(ex) || ex <= 0) return '—'; const d = new Date(ex * 1000); if (Number.isNaN(d.getTime())) return '—'; return d.toLocaleString('zh-CN', { hour12: false }); } function sanitizePanelNotice(text) { const t = String(text || '').trim(); if (!t || /2x|\u8f6e\u8be2|\u4e91\u7aef\u7f16\u6392|\u4e91\u9898\u5e93|\u903b\u8f91\u5728\u4e91\u7aef|\u4ea4\u9519|\u5165\u961f/i.test(t)) return PANEL_NOTICE_FALLBACK; return t; } function parseLeaseExp(lease) { try { const part = String(lease || '').split('.')[0]; if (!part) return 0; const pad = '='.repeat((4 - (part.length % 4)) % 4); const b64 = part.replace(/-/g, '+').replace(/_/g, '/'); const d = JSON.parse(atob(b64 + pad)); return Number(d.exp) || 0; } catch (_) { return 0; } } function leaseEffectiveExp() { const parsed = parseLeaseExp(cloud.lease); const cached = Number(cloud.leaseExp) || 0; if (parsed && cached) return Math.min(parsed, cached); return parsed || cached || 0; } function leaseSecondsLeft() { const exp = leaseEffectiveExp(); return exp > 0 ? exp - Date.now() / 1000 : 0; } async function _lf() { if (leaseSecondsLeft() > 120) return true; try { await _rl(true); return leaseSecondsLeft() > 30; } catch (_) { return false; } } async function _cf(path, method, body, retried) { if (path.indexOf('/lease') < 0) { const ok = await _lf(); if (!ok && path.indexOf('/study/engine/') >= 0) { throw new Error('invalid_lease'); } } const headers = { 'Content-Type': 'application/json' }; if (cloud.token) headers.Authorization = 'Bearer ' + cloud.token; const payload = { ...(body || {}) }; if (path.indexOf('/lease') < 0 && cloud.lease && payload.lease == null) { payload.lease = cloud.lease; } const res = await fetch(cloud.apiBase + path, { method: method || 'POST', headers, body: method === 'GET' ? undefined : JSON.stringify(payload), }); const json = await res.json().catch(() => ({})); if (!res.ok || json.ok === false) { const detail = String(json.detail || json.message || ''); if (!retried && res.status === 401 && /invalid_lease/i.test(detail)) { cloud.lease = ''; cloud.leaseExp = 0; try { await _rl(true); return _cf(path, method, body, true); } catch (_) {} } throw new Error(json.message || json.detail || ('HTTP ' + res.status)); } return json; } async function _rl(force) { if (!force && leaseSecondsLeft() > 120) return true; const sid = parseJwtId(getPlatformToken()); if (!sid) throw new Error('\u65e0\u6cd5\u89e3\u6790\u5b66\u5458ID\uff0c\u8bf7\u5148\u767b\u5f55'); const body = buildProfilePayload({ student_id: sid }, await fetchStudentProfile(), sid); const data = await _cf(_w(4), 'POST', body); cloud.lease = data.lease || ''; cloud.leaseExp = data.exp || 0; cloud.tier = data.tier || (cloud.token ? 'pro' : 'free'); if (data.pro_expires_at) cloud.proExpireAt = data.pro_expires_at; if (data.free_chapter_limit != null) cloud.freeLimit = data.free_chapter_limit; if (data.free_used_chapters != null) cloud.freeUsed = data.free_used_chapters; localStorage.setItem(CLOUD_LEASE_KEY, JSON.stringify({ lease: cloud.lease, exp: cloud.leaseExp, tier: cloud.tier, freeLimit: cloud.freeLimit, freeUsed: cloud.freeUsed, proExpireAt: cloud.proExpireAt, })); if (cloud.token) { profileSyncedThisSession = true; } updateCloudPanelUI(); return true; } async function _cr() { if (!getPlatformToken()) { log('\u8bf7\u5148\u767b\u5f55\u65b0\u7586\u7ee7\u7eed\u6559\u80b2\u5e73\u53f0', 'err'); return false; } if (cloud.lease && leaseSecondsLeft() > 120) { if (cloud.token && !profileSyncedThisSession) { await syncTokenProfile(); } return true; } try { await _rl(); return !!cloud.lease; } catch (e) { log('\u6388\u6743\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5', 'err'); return false; } } async function _lc() { try { const j = await _cf(_w(2), 'GET'); if (j.cloudApiBase) cloud.apiBase = j.cloudApiBase; if (j.freeChapterLimit != null) cloud.freeLimit = j.freeChapterLimit; if (j.proBuyUrl) cloud.proBuyUrl = j.proBuyUrl; const notice = await fetch(cloud.apiBase + (j.panelNoticePath || _w(3))).then(r => r.text()); if (notice) cloud.notice = sanitizePanelNotice(notice.trim()); } catch (_) {} } function updateCloudPanelUI() { const tierEl = document.getElementById('xj-cloud-tier'); const freeEl = document.getElementById('xj-cloud-free'); const freeLabelEl = document.getElementById('xj-cloud-free-label'); const tokenInput = document.getElementById('xj-cloud-token'); if (!tierEl) return; tierEl.textContent = formatCloudTierText(cloud.tier); tierEl.style.color = '#0f172a'; if (String(cloud.tier || '').toLowerCase() === 'pro') { if (freeLabelEl) freeLabelEl.textContent = 'Pro \u5230\u671f'; if (freeEl) freeEl.textContent = formatExpireText(cloud.proExpireAt || cloud.leaseExp); } else { if (freeLabelEl) freeLabelEl.textContent = '\u514d\u8d39\u4f53\u9a8c\u7ae0\u8282'; if (freeEl) freeEl.textContent = cloud.freeUsed + '/' + cloud.freeLimit; } if (tokenInput && tokenInput !== document.activeElement) { tokenInput.value = cloud.token || ''; } } function flattenSectionTree(data) { const out = []; for (const item of data || []) { const children = item?.sectionDirectoryInfos; if (Array.isArray(children) && children.length) { children.forEach(c => c && out.push(c)); } else if (item?.sectionDirectoryInfo) { out.push(item.sectionDirectoryInfo); } } return out; } function sectionDuration(s) { return Number(s.sectionDuration ?? s.allSumDuration ?? 0); } function sectionStudied(s) { return Number(s.lastStudyDuration ?? s.studyDuration ?? 0); } function isSectionDone(s) { if (!s) return true; const noStudy = Number(s.noStudyDuration); if (isFinite(noStudy) && noStudy <= 1) return true; if (isFinite(noStudy) && noStudy > 1) return false; if (s.viewStatus === 'C') return true; const d = sectionDuration(s); return d > 0 && sectionStudied(s) >= d - 0.5; } function isVideoSection(s) { if (!s) return false; if (s.videoId) return true; const t = String(s.sectionType || s.type || '').toUpperCase(); if (t.includes('EXAM') || t.includes('TEST')) return false; return sectionDuration(s) > 0; } async function initCourseSession(courseId, specialTopicId) { await _pp('/studentCourse/getByStudentId', { courseId, specialTopicId }).catch(() => {}); await _pp('/studentCourse/getPlayedInfoLast', { courseId, specialTopicId }).catch(() => {}); await _pp('/studentCourse/getServiceRoute', {}).catch(() => {}); } async function fetchTopics() { const res = await _pp('/studentSpecialTopic/querySpecialTopic', { pageNumber: 1, pageSize: 50, queryAll: false, queryParam: { sortEnum: 'A', specialTopicCategory: 'GXK' }, }); return res.data?.content || []; } async function fetchCourses(specialTopicId) { const all = []; let page = 1; while (true) { const res = await _pp('/studentSpecialTopic/queryCourseByStudentId', { pageNumber: page, pageSize: 50, queryAll: false, queryParam: { isNecessaryStudy: null, sortRule: 'A', studyState: null, examState: null, remainExamFrequency: null, specialTopicId, }, }); const content = res.data?.content || []; all.push(...content); const total = res.data?.totalElements ?? content.length; if (all.length >= total || content.length < 50) break; page++; } return all; } async function fetchSections(courseId, specialTopicId) { const res = await _pp('/studentCourse/queryStudentSection', { courseId, specialTopicId }); return flattenSectionTree(res.data); } function getSelectedCourseIds() { return [...document.querySelectorAll('#xj-course-list input[type=checkbox]:checked')] .map(el => el.dataset.courseId).filter(Boolean); } function readCourseSelectionState() { const boxes = document.querySelectorAll('#xj-course-list input[type=checkbox]'); if (!boxes.length) return null; return getSelectedCourseIds(); } function shouldCourseBeChecked(c, checkedIds) { if (isCourseFullyDone(c)) return false; if (checkedIds === null) return isCourseActionable(c); return checkedIds.map(String).includes(String(c.courseId)); } function resolveTargets(courses) { const state = readCourseSelectionState(); if (state === null) return courses.filter(c => isCourseActionable(c)); const list = state .map(id => courses.find(c => String(c.courseId) === String(id))) .filter(Boolean); return list.length ? list : courses.filter(c => isCourseActionable(c)); } function isExamPassed(c) { if (!c) return false; const state = String(c.examState ?? c.exam_state ?? '').trim().toUpperCase(); if (['1', 'Y', 'YES', 'PASS', 'PASSED', '\u5408\u683c'].includes(state)) return true; const qual = c.isQualified ?? c.is_qualified; if (qual === true || qual === 1 || String(qual).trim() === '1') return true; if (Number(c.examScore ?? c.exam_score ?? 0) >= 60) return true; const passFlag = String(c.passState ?? c.pass_state ?? '').trim().toUpperCase(); return passFlag === '1' || passFlag === 'Y'; } function isStudyDone(c) { const pct = Number(c.studySchedule ?? 0); return pct >= 100 || String(c.studyState || '') === '2'; } function isCourseFullyDone(c) { return isStudyDone(c) && isExamPassed(c); } function isCourseActionable(c) { return !isCourseFullyDone(c); } async function _bp(specialTopicId, courses) { const sections = []; const courseMeta = []; try { await ensurePlatformTokenFresh(); } catch (e) { if (!getPlatformToken()) throw e; } for (let ci = 0; ci < courses.length; ci++) { const c = courses[ci]; if (abortFlag) break; if (ci > 0 && ci % 3 === 0) { try { await ensurePlatformTokenFresh(); } catch (_) {} } const tag = String(c.courseName || '').slice(0, 12); try { await initCourseSession(c.courseId, specialTopicId); const secs = await fetchSections(c.courseId, specialTopicId); courseMeta.push({ courseId: c.courseId, courseName: c.courseName, specialTopicId, examType: c.examType || 'B', studySchedule: c.studySchedule, remainExamFrequency: c.remainExamFrequency, examState: c.examState, examScore: c.examScore, isQualified: c.isQualified, passScore: 60, }); let added = 0; for (const sec of secs) { if (!isVideoSection(sec) || isSectionDone(sec)) continue; sections.push({ courseId: c.courseId, courseName: c.courseName, specialTopicId, sectionId: sec.sectionId, sectionName: sec.sectionName, duration: sectionDuration(sec), studied: sectionStudied(sec), studentSectionId: sec.studentSectionId || '', done: false, }); added++; } if (added) { } else if (!secs.length) { log(`\u300a${c.courseName || tag}\u300b\u672a\u83b7\u53d6\u5230\u7ae0\u8282\uff0c\u8bf7\u5237\u65b0\u9875\u9762`, 'warn'); } } catch (e) { const msg = String(e.message || e); if (isPlatformAuthExpired(msg)) { try { await refreshPlatformToken(); log('\u5e73\u53f0\u767b\u5f55\u5df2\u81ea\u52a8\u7eed\u671f', 'ok'); ci--; continue; } catch (_) { stopForPlatformAuthExpired(msg); throw e; } } log(`\u300a${c.courseName || tag}\u300b\u7ae0\u8282\u52a0\u8f7d\u5931\u8d25\uff0c\u5c06\u8df3\u8fc7`, 'warn'); } } const limited = applyFreeSectionLimit(sections); if (limited.length < sections.length) { log(`\u514d\u8d39\u989d\u5ea6\uff1a\u672c\u6b21\u5b66\u4e60 ${limited.length}/${sections.length} \u4e2a\u7ae0\u8282`, 'warn'); } return { special_topic_id: specialTopicId, courses: courseMeta, sections: limited, auto_exam: true, }; } function applyFreeSectionLimit(sections) { const list = sections || []; if (String(cloud.tier || '').toLowerCase() === 'pro') return list; const remain = Math.max(0, Number(cloud.freeLimit || 3) - Number(cloud.freeUsed || 0)); if (!remain) return []; return list.slice(0, remain); } function findCourseByLogName(name) { const key = String(name || '').trim(); if (!key) return null; return panelCourses.find(c => { const title = String(c.courseName || ''); return title === key || title.startsWith(key) || key.startsWith(title.slice(0, Math.min(title.length, key.length))); }) || null; } function patchCourseFromEngineLog(msg) { const t = String(msg || ''); let changed = false; const scoreM = t.match(/[✅⚠⏸]\s*(.{1,30}?)\s+(\u901a\u8fc7|\u672a\u901a\u8fc7)\s+([\d.]+)\u5206/); if (scoreM) { const course = findCourseByLogName(scoreM[1].trim()); if (course) { const passed = scoreM[2] === '\u901a\u8fc7'; const score = Number(scoreM[3]); course.examScore = score; if (passed) { course.examState = '1'; course.isQualified = '1'; } changed = true; } } const remainM = t.match(/(?:\u5269\u4f59|\u5269)(\d+)\u6b21/); if (remainM) { const course = findCourseByLogName((scoreM && scoreM[1]) || (t.match(/[📝✅⚠⏸⏳❌]\s*(.{1,30})/) || [])[1]); if (course) { course.remainExamFrequency = Number(remainM[1]); changed = true; } } return changed; } function shouldRefreshPanelFromLog(msg) { const t = String(msg || ''); if (/^STUDY\|/.test(t)) return false; return /\u901a\u8fc7|\u672a\u901a\u8fc7|\u4ea4\u5377|\u8003\u8bd5|\u6210\u7ee9|\u7ae0\u8282|\u5b8c\u6210|DONE\|/.test(t); } function schedulePanelDataRefresh(msg, immediate) { if (!running && !immediate) return; if (/^STUDY\|/.test(String(msg || ''))) return; if (!immediate && msg && !shouldRefreshPanelFromLog(msg)) return; if (panelRefreshTimer) clearTimeout(panelRefreshTimer); panelRefreshTimer = setTimeout(() => { panelRefreshTimer = null; void refreshPanelProgress(); }, immediate ? 300 : 3000); } async function refreshPanelProgress() { const topicId = document.getElementById('xj-topic-select')?.value || panelTopicId; if (!topicId) return; if (panelRefreshBusy) { panelRefreshPending = true; return; } panelRefreshBusy = true; try { panelCourses = await fetchCourses(topicId); renderCourseList(); await refreshChapterPreview({ light: running }); } catch (_) {} finally { panelRefreshBusy = false; if (panelRefreshPending) { panelRefreshPending = false; void refreshPanelProgress(); } } } function startPanelPolling() { stopPanelPolling(); panelPollTimer = setInterval(() => { if (running) void refreshPanelProgress(); }, 45000); } function stopPanelPolling() { if (panelPollTimer) { clearInterval(panelPollTimer); panelPollTimer = null; } if (panelRefreshTimer) { clearTimeout(panelRefreshTimer); panelRefreshTimer = null; } } function readEngineRunState() { try { const raw = sessionStorage.getItem(ENGINE_RUN_KEY); if (!raw) return null; const o = JSON.parse(raw); if (!o || !o.sessionId) return null; if (o.ts && Date.now() - o.ts > ENGINE_RUN_TTL_MS) { clearEngineRunState(); return null; } return o; } catch (_) { return null; } } function writeEngineRunState() { if (!running || !engineSessionId) { clearEngineRunState(); return; } const topicId = document.getElementById('xj-topic-select')?.value || panelTopicId || ''; sessionStorage.setItem(ENGINE_RUN_KEY, JSON.stringify({ sessionId: engineSessionId, topicId, ts: Date.now(), })); } function clearEngineRunState() { sessionStorage.removeItem(ENGINE_RUN_KEY); } function bindEngineRunPersistence() { if (bindEngineRunPersistence._bound) return; bindEngineRunPersistence._bound = true; window.addEventListener('pagehide', writeEngineRunState); } function _nc(cmd) { if (!cmd || typeof cmd !== 'object') return null; const type = String(cmd.type || '').trim().toLowerCase(); if (type === 'platform_request') { return { type: 'platform_post', path: cmd.path || cmd.url || '', body: cmd.body || cmd.payload || {}, }; } if (type === 'sleep' || type === 'delay') { return { type: 'wait', ms: Number(cmd.ms || cmd.duration || 1000) }; } if (['platform_post', 'platform_batch', 'wait', 'done', 'failed'].includes(type)) { return cmd; } return null; } function _pa(data) { const raw = (data && data.data && typeof data.data === 'object') ? data.data : (data || {}); const sessionId = String(raw.session_id || raw.sessionId || '').trim(); const command = _nc(raw.command || raw.cmd || null); return { sessionId, command, log: String(raw.log || ''), ok: data?.ok !== false && raw?.ok !== false, message: String(raw.message || data?.message || ''), }; } async function _es(context) { lastPipelineContext = context; const data = await _cf(_w(0), 'POST', { kind: 'pipeline', context, config: { step: 240, wait_ms: 120000, switch_gap_ms: 2500, max_active_sections: 0, free_chapter_limit: cloud.freeLimit, }, }); const parsed = _pa(data); if (data.free_used_chapters != null) cloud.freeUsed = data.free_used_chapters; updateCloudPanelUI(); engineSessionId = parsed.sessionId || ''; writeEngineRunState(); if (parsed.log) log(parsed.log, 'info'); if (!parsed.command && !engineSessionId && parsed.message) { log(parsed.message, 'err'); } syncStartStopButtons(); updatePanel(); return parsed.command; } async function _ep(event, lastResult) { const data = await _cf(_w(1), 'POST', { session_id: engineSessionId, event: event || 'tick', last_result: lastResult || null, }); const parsed = _pa(data); if (data.free_used_chapters != null) cloud.freeUsed = data.free_used_chapters; updateCloudPanelUI(); updatePanel(); if (parsed.log) log(parsed.log, parsed.command?.type === 'failed' ? 'err' : 'info'); return parsed.command; } async function _pr(path, body, retries) { let lastErr = null; const max = Math.max(1, Number(retries) || 3); for (let i = 0; i < max; i++) { try { return await _pp(path, body); } catch (e) { lastErr = e; const msg = String(e.message || e); if (platformAuthStopped) break; if (isPlatformAuthExpired(msg)) { try { await refreshPlatformToken(); log('\u5e73\u53f0\u767b\u5f55\u5df2\u81ea\u52a8\u7eed\u671f', 'ok'); continue; } catch (_) { const exp = parseJwtExpMs(getPlatformToken()); if (exp && exp > Date.now() + 5000) continue; stopForPlatformAuthExpired(msg); break; } } if (i < max - 1) await sleep(1500 * (i + 1)); } } throw lastErr || new Error('\u5e73\u53f0\u8bf7\u6c42\u5931\u8d25'); } async function _xb(cmd) { const results = []; const steps = cmd.steps || []; for (let i = 0; i < steps.length; i++) { const step = steps[i]; results.push(await _pr(step.path, step.body, 3)); if (i < steps.length - 1) await sleep(200); } if (/\/studentCourse\/(addStudyDuration|addStudyRecord|queryStudentSection)/.test( steps.map(s => String(s.path || '')).join(' '), )) { schedulePanelDataRefresh('\u4fdd\u5b58', true); } return { results }; } async function _xc(cmd) { if (!cmd || abortFlag) return null; const type = cmd.type; if (type === 'platform_post') { const json = await _pr(cmd.path, cmd.body, 3); return json; } if (type === 'wait') { await sleep(cmd.ms || 1000); return { waited: cmd.ms }; } if (type === 'done') { log('\u5168\u90e8\u8bfe\u7a0b\u5df2\u5904\u7406\u5b8c\u6210', 'ok'); running = false; clearEngineRunState(); return null; } if (type === 'failed') { if (cmd.message === 'free_quota_exhausted') { log('\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08' + cloud.freeUsed + '/' + cloud.freeLimit + '\uff09\uff0c\u8bf7\u5347\u7ea7 Pro', 'err'); } else if (cmd.message !== 'init_failed') { log('\u5904\u7406\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5', 'err'); } if (cmd.message === 'free_quota_exhausted') { running = false; clearEngineRunState(); } return null; } return null; } async function _ss(event, lastResult) { try { return await _ep(event, lastResult); } catch (e) { const errMsg = String(e.message || e); if (/invalid_lease/i.test(errMsg)) { log('\u6388\u6743\u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u65b0\u70b9\u51fb\u5f00\u59cb', 'err'); return null; } if (/session_not_found|session_expired/i.test(errMsg)) { log('\u5b66\u4e60\u4f1a\u8bdd\u4e2d\u65ad\uff0c\u8bf7\u91cd\u65b0\u70b9\u51fb\u5f00\u59cb', 'warn'); clearEngineRunState(); engineSessionId = ''; return null; } if (/engine_start_failed|engine_step_failed/i.test(errMsg)) { log('\u4e91\u7aef\u5f15\u64ce\u5f02\u5e38\uff0c\u8bf7\u786e\u8ba4\u670d\u52a1\u7aef\u5df2\u66f4\u65b0\u5e76\u91cd\u542f', 'err'); return null; } throw e; } } async function _ve(firstCmd) { let cmd = _nc(firstCmd); let loopTick = 0; if (!cmd) { log('\u5f15\u64ce\u672a\u8fd4\u56de\u6307\u4ee4\uff0c\u8bf7\u91cd\u8bd5', 'warn'); return; } while (cmd && running && !abortFlag) { if (++loopTick % 4 === 0) { try { await ensurePlatformTokenFresh(); } catch (e) { const exp = parseJwtExpMs(getPlatformToken()); if (!exp || exp <= Date.now()) { stopForPlatformAuthExpired(String(e.message || e)); cmd = null; break; } } } writeEngineRunState(); if (cmd.type === 'platform_post') { try { const result = await _xc(cmd); if (/\/studentCourse\/(addStudyDuration|addStudyRecord|queryStudentSection)/.test(String(cmd.path || ''))) { schedulePanelDataRefresh('\u4fdd\u5b58', true); } cmd = await _ss('platform_result', result); } catch (e) { const errMsg = String(e.message || e); if (platformAuthStopped || isPlatformAuthExpired(errMsg)) { stopForPlatformAuthExpired(errMsg); cmd = null; break; } if (!/\u5e73\u53f0\u8bf7\u6c42\u5931\u8d25|\u7f51\u7edc\u5f02\u5e38/.test(lastLoggedText || '')) { log('\u7f51\u7edc\u5f02\u5e38\uff0c\u7a0d\u540e\u91cd\u8bd5', 'err'); } cmd = await _ss('platform_result', { status: 500, code: -1, message: errMsg }); } } else if (cmd.type === 'platform_batch') { try { const result = await _xb(cmd); cmd = await _ss('platform_batch_result', result); } catch (e) { const errMsg = String(e.message || e); if (platformAuthStopped || isPlatformAuthExpired(errMsg)) { stopForPlatformAuthExpired(errMsg); cmd = null; break; } if (!/\u5e73\u53f0\u8bf7\u6c42\u5931\u8d25|\u7f51\u7edc\u5f02\u5e38/.test(lastLoggedText || '')) { log('\u7f51\u7edc\u5f02\u5e38\uff0c\u7a0d\u540e\u91cd\u8bd5', 'err'); } cmd = await _ss('platform_batch_result', { results: [{ status: 500, code: -1, message: errMsg }], }); } } else if (cmd.type === 'wait') { await _xc(cmd); cmd = await _ss('tick', null); } else if (cmd.type === 'done' || cmd.type === 'failed') { await _xc(cmd); if (!running || abortFlag) break; cmd = await _ss('tick', null); } else { cmd = await _ss('tick', null); } if (!cmd && running && !abortFlag) { await sleep(1500); cmd = await _ss('tick', null); } } running = false; engineBusy = false; clearChapterTabHighlight(); clearEngineRunState(); stopPanelPolling(); syncStartStopButtons(); updatePanel(); const earlyStop = loopTick < 2 && !abortFlag && !platformAuthStopped; log( abortFlag ? '\u4efb\u52a1\u5df2\u505c\u6b62' : (earlyStop ? '\u5b66\u4e60\u5f02\u5e38\u4e2d\u65ad\uff0c\u8bf7\u91cd\u65b0\u70b9\u51fb\u5f00\u59cb' : '\u4efb\u52a1\u5df2\u7ed3\u675f'), abortFlag ? 'warn' : (earlyStop ? 'warn' : 'info'), ); void refreshPanelProgress(); } async function resumePipeline(saved) { if (running || engineBusy || !saved?.sessionId) return false; if (!(await _cr())) { clearEngineRunState(); return false; } engineBusy = true; running = true; abortFlag = false; engineSessionId = saved.sessionId; bindEngineRunPersistence(); startPanelPolling(); writeEngineRunState(); if (saved.topicId) { panelTopicId = saved.topicId; const sel = document.getElementById('xj-topic-select'); if (sel) sel.value = saved.topicId; loadCourseList(saved.topicId).catch(() => {}); } log('\u5df2\u6062\u590d\u7ee7\u7eed\u5b66\u4e60', 'info'); showChapterPreviewGuide(); syncStartStopButtons(); updatePanel(); try { const first = await _ep('tick', null); await _ve(first); return true; } catch (e) { clearEngineRunState(); running = false; engineBusy = false; stopPanelPolling(); syncStartStopButtons(); updatePanel(); log('\u6062\u590d\u5931\u8d25\uff0c\u8bf7\u91cd\u65b0\u70b9\u51fb\u5f00\u59cb', 'warn'); return false; } } async function tryResumePipeline() { const saved = readEngineRunState(); if (!saved) return false; return resumePipeline(saved); } async function _sp() { if (running || engineBusy) return; if (!(await _cr())) return; const topicId = document.getElementById('xj-topic-select')?.value || panelTopicId; if (!topicId) { log('\u8bf7\u9009\u62e9\u57f9\u8bad\u4e13\u9898', 'err'); return; } clearEngineRunState(); engineSessionId = ''; lastLoggedText = ''; lastProgressLogKey = ''; pipelineStudySections = []; finishEstimateLogged = false; platformAuthStopped = false; running = true; abortFlag = false; engineBusy = true; bindEngineRunPersistence(); startPanelPolling(); try { await ensurePlatformTokenFresh(); log('\u8bfb\u53d6\u8bfe\u7a0b\u4e0e\u7ae0\u8282...', 'info'); const courses = await fetchCourses(topicId); const targets = resolveTargets(courses); if (!targets.length) { log('\u6ca1\u6709\u5f85\u5904\u7406\u8bfe\u7a0b', 'warn'); return; } const ctx = await _bp(topicId, targets); pipelineStudySections = (ctx.sections || []).slice(); finishEstimateLogged = false; const secCount = ctx.sections.length; const isFree = String(cloud.tier || '').toLowerCase() !== 'pro'; if (!secCount) { log('\u6b63\u5728\u51c6\u5907\u8003\u8bd5…', 'warn'); } else if (isFree) { log(`FREE|${secCount}`, 'ok'); } else { log(`\u672c\u6b21 ${targets.length} \u95e8\u8bfe\uff0c\u5171 ${secCount} \u4e2a\u7ae0\u8282`, 'ok'); } if (ctx.sections[0]) { currentCourseName = ctx.sections[0].courseName || ''; currentChapterName = ctx.sections[0].sectionName || '\u51c6\u5907\u4e2d'; } if (secCount > 0) showChapterPreviewGuide(); const first = await _es(ctx); if (!first) return; await _ve(first); } catch (e) { const errMsg = String(e.message || e); if (/invalid_lease/i.test(errMsg)) { log('\u6388\u6743\u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u65b0\u70b9\u51fb\u5f00\u59cb', 'err'); } else if (!/\u7f51\u7edc\u5f02\u5e38|\u542f\u52a8\u5931\u8d25|\u514d\u8d39\u989d\u5ea6|invalid_lease|\u6388\u6743\u5df2\u8fc7\u671f/.test(lastLoggedText || '')) { log('\u542f\u52a8\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5', 'err'); } } finally { running = false; engineBusy = false; clearChapterTabHighlight(); stopPanelPolling(); syncStartStopButtons(); updatePanel(); } } let studyMode = 'efficiency'; let studyModeHintTimer = null; function getStudyMode() { const sel = document.getElementById('xj-study-mode'); return String((sel && sel.value) || studyMode || '').trim(); } function hasStudyModeSelected() { return true; } function readPanelCollapsed() { const v = localStorage.getItem(PANEL_COLLAPSED_KEY); return v == null ? true : v === '1'; } function writePanelCollapsed(collapsed) { localStorage.setItem(PANEL_COLLAPSED_KEY, collapsed ? '1' : '0'); } function readPanelPos() { try { return JSON.parse(localStorage.getItem(PANEL_POS_KEY) || '{}'); } catch (_) { return null; } } function writePanelPos(left, top) { localStorage.setItem(PANEL_POS_KEY, JSON.stringify({ left, top })); } function clampPanelInViewport(panel) { if (!panel) return; const rect = panel.getBoundingClientRect(); const maxLeft = Math.max(0, window.innerWidth - panel.offsetWidth); const maxTop = Math.max(0, window.innerHeight - panel.offsetHeight); const left = Math.max(0, Math.min(maxLeft, rect.left)); const top = Math.max(0, Math.min(maxTop, rect.top)); panel.style.right = 'auto'; panel.style.bottom = 'auto'; panel.style.left = left + 'px'; panel.style.top = top + 'px'; writePanelPos(left, top); } function applyPanelCollapsed(panel, collapsed) { const btnMin = panel.querySelector('#xj-btn-min'); const btnMax = panel.querySelector('#xj-btn-max'); panel.classList.toggle('zjzx-panel-min', !!collapsed); panel.classList.toggle('zjzx-panel-max', !collapsed); if (btnMin) btnMin.style.display = collapsed ? 'none' : ''; if (btnMax) btnMax.style.display = collapsed ? '' : 'none'; const footerExtra = panel.querySelector('.zjzx-footer-extra'); if (footerExtra) footerExtra.style.display = collapsed ? 'none' : ''; clampPanelInViewport(panel); } function enablePanelDrag(panel) { const header = panel.querySelector('#xj-panel-header'); if (!header) return; let dragging = false; let startX = 0, startY = 0, startLeft = 0, startTop = 0; header.addEventListener('mousedown', (e) => { if (e.target?.closest('#xj-panel-controls, .zjzx-panel-ctl')) return; dragging = true; startX = e.clientX; startY = e.clientY; const rect = panel.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; panel.style.right = 'auto'; panel.style.bottom = 'auto'; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!dragging) return; const left = Math.max(0, Math.min(window.innerWidth - panel.offsetWidth, startLeft + (e.clientX - startX))); const top = Math.max(0, Math.min(window.innerHeight - panel.offsetHeight, startTop + (e.clientY - startY))); panel.style.left = left + 'px'; panel.style.top = top + 'px'; }); document.addEventListener('mouseup', () => { if (!dragging) return; dragging = false; const rect = panel.getBoundingClientRect(); writePanelPos(rect.left, rect.top); }); } function injectPanelStyles() { const id = 'xj-panel-style-v1'; if (document.getElementById(id) || !document.head) return; const st = document.createElement('style'); st.id = id; st.textContent = ` #xj-auto-panel{position:fixed;right:20px;top:80px;z-index:999999;width:412px;display:flex;flex-direction:column;background:#f4f7fb;border:1px solid #dbe4f0;border-radius:16px;box-shadow:0 16px 40px rgba(15,23,42,.17);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;font-size:12px;color:#0f172a;overflow:hidden;transition:max-height .22s ease,box-shadow .22s ease;} #xj-auto-panel.zjzx-panel-hidden{display:none !important;} #xj-auto-panel.zjzx-panel-max{max-height:min(92vh,780px);} #xj-auto-panel.zjzx-panel-min{max-height:none;box-shadow:0 10px 28px rgba(15,23,42,.14);} #xj-auto-panel.zjzx-panel-min #xj-panel-header{border-bottom:none;} #xj-auto-panel.zjzx-panel-min #xj-panel-body,#xj-auto-panel.zjzx-panel-min #xj-panel-footer,#xj-auto-panel.zjzx-panel-min .zjzx-footer-extra{display:none !important;} #xj-panel-header{flex:0 0 auto;} #xj-panel-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;} .zjzx-footer-extra{flex:0 0 auto;} #xj-panel-footer{flex:0 0 auto;} #xj-panel-header{padding:8px 11px;background:linear-gradient(180deg,#f8ecd5,#f4e8cf);border-bottom:1px solid #e5dbc6;display:flex;justify-content:space-between;align-items:center;cursor:move;user-select:none;} #xj-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;} #xj-panel-logo{width:30px;height:30px;border-radius:9px;object-fit:cover;box-shadow:0 2px 9px rgba(15,23,42,.11);border:1px solid rgba(148,163,184,.45);background:#fff;flex:0 0 auto;} #xj-panel-title{font-size:13px;font-weight:900;color:#9a3412;line-height:1.26;display:flex;align-items:flex-start;gap:5px;flex-wrap:wrap;} .zjzx-panel-title-text{flex:1 1 12em;min-width:0;letter-spacing:-0.01em;} #xj-panel-sub{display:flex;flex-wrap:wrap;align-items:center;gap:3px 5px;margin-top:3px;line-height:1.32;} .zjzx-sub-chip{font-size:11px;color:#7c2d12;background:rgba(255,255,255,.62);padding:2px 7px;border-radius:999px;border:1px solid rgba(180,83,9,.18);font-weight:700;} .zjzx-sub-chip-em{color:#0f766e;background:rgba(236,253,245,.9);border-color:rgba(15,118,110,.28);} .zjzx-sub-dot{color:#d6d3d1;font-size:10px;font-weight:400;} .zjzx-panel-ctl .zjzx-ctl-ico{display:block;box-sizing:border-box;margin:0 auto;} .zjzx-ctl-min{width:10px;height:2px;background:#64748b;border-radius:1px;} .zjzx-ctl-plus{font-size:16px;line-height:1;font-weight:700;color:#64748b;} .zjzx-panel-version{font-size:11px;font-weight:900;color:#64748b;padding:2px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0;} #xj-panel-controls{display:flex;align-items:center;gap:5px;flex:0 0 auto;} .zjzx-panel-ctl{border:none;background:#fff;color:#64748b;width:28px;height:28px;border-radius:999px;cursor:pointer;box-shadow:0 1px 2px rgba(15,23,42,.1);font-size:15px;line-height:1;font-weight:900;padding:0;} .zjzx-panel-ctl:hover{filter:brightness(1.03);} .zjzx-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px;} .zjzx-card-status{padding:6px 8px;margin-bottom:6px;} .zjzx-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;line-height:1.28;} .zjzx-status-label{font-size:11px;color:#64748b;font-weight:700;} .zjzx-status-badge{padding:2px 7px;border-radius:999px;font-weight:800;font-size:11px;background:#fff;border:1px solid #cbd5e1;color:#334155;} .zjzx-status-main{margin-top:3px;} .zjzx-status-metrics{display:flex;align-items:center;gap:5px;font-size:12px;color:#475569;min-width:0;flex:1;} .zjzx-status-metrics em{font-style:normal;font-weight:900;color:#0f172a;} .zjzx-metric-div{color:#cbd5e1;} .zjzx-progress-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;font-size:11px;color:#334155;} .zjzx-progress-num{font-size:22px;font-weight:900;color:#0f172a;} .zjzx-progress-num small{font-size:13px;color:#64748b;} .zjzx-card-status .zjzx-progress-pct{font-size:14px;font-weight:900;color:#0369a1;flex:0 0 auto;} .zjzx-progress-pct{font-size:20px;font-weight:900;color:#0369a1;} .zjzx-card-status .zjzx-progress-bar{height:5px;margin-top:4px;margin-bottom:0;} .zjzx-progress-bar{height:9px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:5px;} .zjzx-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s ease;} .zjzx-list-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;} .zjzx-list-head-actions{display:flex;align-items:center;gap:5px;} .zjzx-log-clear-btn{border:1px solid #cbd5e1;background:#fff;color:#64748b;padding:1px 7px;border-radius:999px;font-size:10px;font-weight:700;cursor:pointer;line-height:1.4;} .zjzx-log-clear-btn:hover{background:#f8fafc;color:#0f172a;} .zjzx-plan-row{padding:0 2px 5px;} .zjzx-plan-row .zjzx-plan-select{padding:4px 8px;font-size:11px;line-height:1.25;border-radius:7px;} .zjzx-plan-select{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:6px 9px;font-size:12px;background:#fff;color:#0f172a;} .zjzx-list-title{font-size:12px;color:#64748b;font-weight:700;} .zjzx-list-tag{font-size:11px;color:#92400e;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:2px 7px;} .zjzx-settings-group{background:#f8fafc;border:1px solid #dbe4f0;border-radius:12px;padding:8px;margin-bottom:8px;} .zjzx-settings-title{font-size:12px;color:#64748b;font-weight:900;margin-bottom:7px;} .zjzx-hidden{display:none !important;} .zjzx-btn-row-nowrap{flex-wrap:nowrap;} .zjzx-btn-action{min-width:0 !important;flex:1 1 0 !important;} .zjzx-btn-ico{font-size:13px;line-height:1;display:inline-flex;align-items:center;margin-right:5px;} .zjzx-btn-label{white-space:nowrap;} .zjzx-toggle-on{background:linear-gradient(135deg,#1d4ed8,#0ea5e9) !important;border-color:transparent !important;color:#fff !important;} .zjzx-card-actions .zjzx-btn-row{margin-bottom:0;} .zjzx-study-mode-block{margin-bottom:5px;} .zjzx-study-mode-block .zjzx-study-mode-row{display:flex;align-items:center;gap:5px;margin-bottom:0;} .zjzx-study-mode-block .zjzx-meta-label{flex:0 0 auto;font-size:10px;white-space:nowrap;line-height:1.2;} .zjzx-study-mode-block .zjzx-plan-select{flex:1 1 auto;min-width:0;width:auto;padding:3px 22px 3px 7px;font-size:11px;line-height:1.25;border-radius:7px;} .zjzx-study-mode-block .zjzx-study-mode-hint{margin-top:0;} .zjzx-card-actions{padding:6px 8px;} .zjzx-empty-state{padding:12px 7px;text-align:center;color:#94a3b8;font-size:12px;font-weight:900;} #xj-course-list,#xj-chapter-preview,#xj-run-log{max-height:188px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;} #xj-settings-pane{padding:2px 0;} .zjzx-chapter-course{margin-bottom:7px;border:1px solid #dbe4f0;border-radius:9px;background:#fff;} .zjzx-chapter-title{padding:6px 9px;background:#eaf1ff;border-bottom:1px solid #dbe4f0;color:#1d4ed8;font-size:12px;font-weight:700;} .zjzx-chapter-item{padding:5px 9px;font-size:12px;display:flex;justify-content:space-between;gap:7px;} .zjzx-dot{font-weight:700;} .zjzx-footer-extra{padding:6px 9px;background:#f8fafc;border-top:1px solid #e2e8f0;opacity:.95;} .zjzx-ann{position:relative;font-size:12px;color:#334155;line-height:1.42;background:linear-gradient(180deg,#fffdf5,#fff7e6);border:1px solid #fcd34d;border-radius:11px;padding:8px 9px 8px 11px;margin-bottom:0;} .zjzx-ann-title{display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:900;color:#9a3412;margin-bottom:3px;} .zjzx-ann-text{display:block;word-break:break-word;} .zjzx-ann::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:linear-gradient(180deg,#f59e0b,#ef4444);border-top-left-radius:11px;border-bottom-left-radius:11px;} .zjzx-qq-row{display:flex;align-items:center;justify-content:space-between;gap:9px;margin-top:5px;} .zjzx-qq-text{font-size:12px;color:#475569;} .zjzx-qq-title{font-size:13px;font-weight:800;color:#0f172a;} .zjzx-qq-btn{border:none;background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:5px 12px;border-radius:999px;font-size:12px;font-weight:800;cursor:pointer;} .zjzx-exam-hint{font-size:11px;color:#64748b;margin-top:5px;line-height:1.42;} .zjzx-study-mode-hint{margin-top:5px;padding:7px 9px;border-radius:9px;border:1px solid #e2e8f0;font-size:11px;line-height:1.42;} .zjzx-study-mode-hint-title{display:block;font-weight:800;margin-bottom:3px;} .zjzx-study-mode-hint-body{margin:0;color:#475569;} .zjzx-study-mode-hint-efficiency{background:linear-gradient(135deg,#eff6ff,#f0f9ff);border-color:#bfdbfe;color:#1e3a8a;} .zjzx-study-mode-hint-efficiency .zjzx-study-mode-hint-body{color:#334155;} .zjzx-study-mode-hint-realtime{background:linear-gradient(135deg,#fffbeb,#fef3c7);border-color:#f59e0b;color:#78350f;} .zjzx-study-mode-hint-realtime .zjzx-study-mode-hint-body{color:#92400e;} .zjzx-start-hint{font-size:11px;color:#b45309;background:#fffbeb;border:1px solid #fcd34d;border-radius:7px;padding:5px 7px;margin-bottom:5px;line-height:1.32;text-align:center;font-weight:600;} .zjzx-start-hint-live{color:#1e3a8a;background:linear-gradient(135deg,#eff6ff,#ecfdf5);border:1px solid #60a5fa;font-size:12px;font-weight:800;padding:7px 9px;} .zjzx-tab-btn.zjzx-tab-highlight{border-color:#f59e0b !important;background:linear-gradient(135deg,#fff7ed,#fef3c7) !important;color:#b45309 !important;box-shadow:0 0 0 2px rgba(245,158,11,.28);animation:zjzxTabPulse 1.4s ease-in-out infinite;} @keyframes zjzxTabPulse{0%,100%{transform:scale(1);}50%{transform:scale(1.03);}} .zjzx-btn-pro{flex:0 0 auto;background:linear-gradient(135deg,#f59e0b,#ef4444);color:#fff;border:none;box-shadow:0 3px 10px rgba(239,68,68,.22);padding:4px 8px;font-size:12px;border-radius:10px;cursor:pointer;font-weight:800;} .zjzx-btn-pro:hover{filter:brightness(1.06);} #xj-pro-modal{position:fixed;inset:0;background:rgba(15,23,42,.45);z-index:1000001;display:none;align-items:center;justify-content:center;padding:20px;} .zjzx-pro-card{width:min(560px,92vw);max-height:88vh;overflow:auto;background:#fff;border-radius:18px;border:1px solid #dbe4f0;box-shadow:0 18px 46px rgba(15,23,42,.25);padding:16px;} .zjzx-pro-title{font-size:24px;font-weight:900;color:#0f172a;} .zjzx-pro-sub{font-size:13px;color:#64748b;margin-top:4px;line-height:1.45;} .zjzx-pro-sec{margin-top:12px;border:1px solid #dbe4f0;border-radius:12px;padding:12px;background:#f8fafc;} .zjzx-pro-sec h4{margin:0 0 8px;font-size:15px;color:#0f172a;} .zjzx-pro-sec p{margin:4px 0;font-size:13px;color:#334155;line-height:1.5;} .zjzx-pro-sec ul{margin:6px 0 0 18px;padding:0;} .zjzx-pro-sec li{margin:4px 0;font-size:13px;color:#334155;line-height:1.45;} .zjzx-pro-buy{display:flex;flex-direction:column;gap:10px;margin-top:8px;} .zjzx-pro-muted{color:#64748b;font-size:12px;line-height:1.5;} .zjzx-pro-buy-btn{display:inline-block;background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:10px 16px;border-radius:12px;font-size:14px;font-weight:800;border:none;cursor:pointer;align-self:flex-start;} .zjzx-pro-buy-btn:hover{filter:brightness(1.05);} .zjzx-pro-buy-btn:disabled{background:#94a3b8;cursor:not-allowed;box-shadow:none;opacity:.92;} .zjzx-pro-buy-btn:disabled:hover{filter:none;} .zjzx-pro-tip{margin-top:8px;background:#fef3c7;border:1px solid #fcd34d;border-radius:10px;padding:8px 10px;font-size:13px;color:#92400e;font-weight:700;line-height:1.45;} .zjzx-pro-tip-info{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8;font-weight:600;} .zjzx-pro-actions{margin-top:14px;display:flex;justify-content:flex-end;} .zjzx-pro-close{border:none;background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;padding:10px 18px;border-radius:12px;font-size:14px;font-weight:800;cursor:pointer;} .zjzx-tabbar{display:flex;gap:6px;margin-bottom:7px;} .zjzx-tab-btn{flex:1;border:1px solid #cbd5e1;background:#f8fafc;color:#475569;padding:4px 4px;border-radius:9px;cursor:pointer;font-weight:700;font-size:11px;} .zjzx-tab-btn.active{background:linear-gradient(135deg,#1d4ed8,#0ea5e9);color:#fff;border-color:transparent;} .zjzx-pane{display:none;}.zjzx-pane.active{display:block;} .zjzx-btn-row{display:flex;gap:7px;margin-bottom:6px;flex-wrap:wrap;} .zjzx-btn{flex:1;border:none;color:#fff;padding:7px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px;min-width:68px;} .zjzx-btn-start{background:#16a34a;}.zjzx-btn-stop{background:#ef4444;}.zjzx-btn-refresh{background:#64748b;} .zjzx-btn-start.zjzx-btn-off,.zjzx-btn-start:disabled{background:#cbd5e1 !important;color:#64748b !important;cursor:not-allowed;box-shadow:none;pointer-events:none;} .zjzx-btn-stop.zjzx-btn-off,.zjzx-btn-stop:disabled{background:#e2e8f0 !important;color:#94a3b8 !important;cursor:not-allowed;box-shadow:none;pointer-events:none;} .zjzx-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a;flex:0 0 auto;} .zjzx-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:12px;margin-bottom:5px;} .zjzx-meta-label{color:#64748b;}.zjzx-meta-value{font-weight:700;text-align:right;} .zjzx-card-current{padding:5px 8px;margin-bottom:6px;} .zjzx-card-current .zjzx-meta-row{font-size:11px;margin-bottom:2px;gap:5px;line-height:1.28;align-items:flex-start;} .zjzx-card-current .zjzx-meta-row:last-child{margin-bottom:0;} .zjzx-card-current .zjzx-meta-label{flex:0 0 auto;font-size:10px;white-space:nowrap;} .zjzx-card-current .zjzx-meta-value{flex:1 1 auto;min-width:0;font-size:11px;font-weight:600;text-align:right;word-break:break-all;line-height:1.28;} .zjzx-auth-badge{padding:2px 7px;border-radius:999px;border:1px solid #cbd5e1;font-size:11px;font-weight:900;} #xj-auto-status{padding:3px 9px;border-radius:999px;font-weight:900;font-size:12px;background:#fff;border:1px solid #cbd5e1;} .zjzx-card-status #xj-auto-status{padding:2px 7px;font-size:11px;} #xj-panel-footer{padding:7px 11px;background:#eef2f7;border-top:1px solid #dbe4f0;font-size:12px;} .zjzx-log-row{padding:4px 6px;border-bottom:1px dashed #d4deea;font-size:12px;line-height:1.42;}`; document.head.appendChild(st); } function clearChapterTabHighlight() { if (chapterTabHintTimer) { clearTimeout(chapterTabHintTimer); chapterTabHintTimer = null; } document.querySelector('.zjzx-tab-btn[data-tab="chapter"]')?.classList.remove('zjzx-tab-highlight'); } function highlightChapterTab(ms) { clearChapterTabHighlight(); const btn = document.querySelector('.zjzx-tab-btn[data-tab="chapter"]'); if (!btn) return; btn.classList.add('zjzx-tab-highlight'); chapterTabHintTimer = setTimeout(clearChapterTabHighlight, Math.max(8000, Number(ms) || 45000)); } function showChapterPreviewGuide() { log('CHAPTER_PREVIEW_GUIDE', 'ok'); highlightChapterTab(45000); void refreshChapterPreview(); updatePanel(); } function switchPanelTab(name) { document.querySelectorAll('.zjzx-tab-btn').forEach(el => { el.classList.toggle('active', el.dataset.tab === name); }); document.querySelectorAll('.zjzx-pane').forEach(el => { el.classList.toggle('active', el.dataset.pane === name); }); if (name === 'chapter') clearChapterTabHighlight(); } function syncProBuyButtonUi() { const btn = document.getElementById('xj-pro-buy-link'); if (!btn) return; btn.disabled = false; btn.textContent = '\u524d\u5f80\u8d2d\u4e70\u9875\u5f00\u901a Pro'; btn.title = '\u6253\u5f00\u8d2d\u4e70\u9875\u83b7\u53d6 Token'; } function openProBuyPage() { const url = cloud.proBuyUrl || PRO_BUY_URL; try { GM_openInTab(url, { active: true, insert: true, setParent: true }); } catch (_) { window.open(url, '_blank', 'noopener,noreferrer'); } } function openProModal() { syncProBuyButtonUi(); const vm = document.getElementById('xj-pro-modal'); if (vm) vm.style.display = 'flex'; } function closeProModal() { const vm = document.getElementById('xj-pro-modal'); if (vm) vm.style.display = 'none'; } function createProModal() { const old = document.getElementById('xj-pro-modal'); if (old) old.remove(); const modal = document.createElement('div'); modal.id = 'xj-pro-modal'; modal.innerHTML = `
\u5f00\u901a Pro
Pro \u7528\u6237\u53ef\u65e0\u9650\u5236\u5b66\u4e60\u7ae0\u8282\uff0c\u5e76\u81ea\u52a8\u5b8c\u6210\u8bfe\u7a0b\u8003\u8bd5

Pro \u6743\u76ca

  • \u89e3\u9664\u514d\u8d39\u4f53\u9a8c\u7ae0\u8282\u6b21\u6570\u9650\u5236\uff0c\u5df2\u9009\u8bfe\u7a0b\u53ef\u6301\u7eed\u5b66\u4e60
  • \u81ea\u52a8\u5b8c\u6210\u8bfe\u7a0b\u5b66\u4e60\u4e0e\u8003\u8bd5
  • Pro \u6709\u6548\u671f 30 \u5929\uff08\u81ea\u9996\u6b21\u4f7f\u7528\u6fc0\u6d3b\u8d77\u7b97\uff0c\u7ed1\u5b9a\u65b0\u7586\u7ee7\u7eed\u6559\u80b2\u8d26\u53f7\uff09
\u63d0\u793a\uff1aPro \u6743\u9650\u7ed1\u5b9a\u5f53\u524d\u767b\u5f55\u8d26\u53f7\uff0c\u8bf7\u52ff\u4e0e\u4ed6\u4eba\u5171\u7528 Token\u3002

\u5f00\u901a\u65b9\u5f0f

1) \u52a0\u5165 QQ \u7fa4 ${QQ_GROUP_NUMBER}\uff0c\u8054\u7cfb\u7ba1\u7406\u5458\u83b7\u53d6 Token

2) \u524d\u5f80\u8d2d\u4e70\u9875\u83b7\u53d6 Token\uff0c24 \u5c0f\u65f6\u81ea\u52a9\u53d1\u8d27

\u8d2d\u5f97 Token \u540e\uff1a\u5728\u8bbe\u7f6e\u9875 Token \u8f93\u5165\u6846\u7c98\u8d34\uff0c\u70b9\u51fb\u300c\u4fdd\u5b58\u5e76\u6821\u9a8c\u300d\u3002
\u91cd\u8981\uff1a\u8bf7\u52ff\u968f\u610f\u6cc4\u9732 Token\uff0c\u907f\u514d\u8d26\u53f7\u88ab\u591a\u4eba\u5171\u7528\u5bfc\u81f4\u5931\u6548\u3002
`; document.body.appendChild(modal); modal.addEventListener('click', (e) => { if (e.target === modal) closeProModal(); }); modal.querySelector('#xj-pro-close')?.addEventListener('click', closeProModal); modal.querySelector('#xj-pro-buy-link')?.addEventListener('click', openProBuyPage); syncProBuyButtonUi(); } function syncPanelNotice() { const ann = document.getElementById('xj-ann-text'); if (!ann) return; ann.textContent = cloud.notice || PANEL_NOTICE_FALLBACK; } function hideStudyModeHint() { const box = document.getElementById('xj-study-mode-hint'); if (box) box.style.display = 'none'; } function syncStudyModeHintContent() { hideStudyModeHint(); } function showStudyModeHintTemporary() { const box = document.getElementById('xj-study-mode-hint'); if (!box) return; syncStudyModeHintContent(); box.style.display = ''; if (studyModeHintTimer) clearTimeout(studyModeHintTimer); studyModeHintTimer = setTimeout(() => { studyModeHintTimer = null; hideStudyModeHint(); }, 5000); } function courseExamLabel(c) { const remain = Number(c.remainExamFrequency ?? c.remain_exam_frequency ?? 0); if (isExamPassed(c)) return ' · \u5df2\u901a\u8fc7'; if (isStudyDone(c)) return ' · \u5f85\u8003\u8bd5' + (remain > 0 ? '\uff08\u5269' + remain + '\u6b21\uff09' : ''); if (remain > 0) return ' · \u5269' + remain + '\u6b21\u8003\u8bd5'; return ''; } function renderCourseList() { const box = document.getElementById('xj-course-list'); if (!box) return; const checkedFromDom = getSelectedCourseIds(); const autoDefault = !courseListUserTouched && checkedFromDom.length === 0; if (!panelCourses.length) { box.innerHTML = "
\u5f53\u524d\u4e13\u9898\u6682\u65e0\u8bfe\u7a0b
\u53ef\u5207\u6362\u4e0a\u65b9\u57f9\u8bad\u4e13\u9898\u67e5\u770b
"; return; } box.innerHTML = panelCourses.map(c => { const pct = Math.min(100, Math.max(0, Number(c.studySchedule ?? 0))); const fullyDone = isCourseFullyDone(c); const examPending = isStudyDone(c) && !isExamPassed(c); const cid = String(c.courseId); const checked = autoDefault ? (isCourseActionable(c) ? 'checked' : '') : (checkedFromDom.includes(cid) ? 'checked' : ''); const border = examPending ? '#fdba74' : '#e2e8f0'; const bgColor = fullyDone ? '#f1f5f9' : examPending ? '#fffbeb' : '#f8fafc'; const titleColor = fullyDone ? '#64748b' : '#0f172a'; return ``; }).join(''); box.querySelectorAll("input[type='checkbox']").forEach(el => { el.addEventListener('change', () => { courseListUserTouched = true; if (getSelectedCourseIds().length) clearPanelHint(); void refreshChapterPreview(); }); }); if (autoDefault && getSelectedCourseIds().length) { void refreshChapterPreview(); } } async function refreshChapterPreview(opts) { const light = !!(opts && opts.light); const selected = getSelectedCourseIds(); const topicId = document.getElementById('xj-topic-select')?.value || panelTopicId; if (!selected.length || !topicId) { chapterPreview = []; queueVideoTotal = 0; queueVideoDone = 0; queueExamTotal = 0; queueExamDone = 0; renderChapterPreview(); updateQueueSummary(); return; } const list = []; let examTotal = 0; let examDone = 0; for (const cid of selected) { const course = panelCourses.find(c => String(c.courseId) === String(cid)); if (!course) continue; examTotal += 1; const passed = isExamPassed(course); if (passed) examDone += 1; try { if (!light) { await initCourseSession(cid, topicId); } const secs = await fetchSections(cid, topicId); const videos = secs.filter(isVideoSection).map(s => { const done = isSectionDone(s); const d = sectionDuration(s); const st = sectionStudied(s); const pctV = done ? 100 : (d > 0 ? Math.min(100, Math.round((st / d) * 100)) : 0); return { id: s.sectionId, title: s.sectionName || s.sectionId, process: pctV, done, active: running && namesLooseMatch(currentCourseName, course.courseName) && (namesLooseMatch(currentChapterName, s.sectionName || '') || namesLooseMatch(currentChapterName, formatSectionLabel(course.courseName, s.sectionName || ''))), }; }); list.push({ courseId: cid, courseTitle: course.courseName || '\u672a\u547d\u540d\u8bfe\u7a0b', videos }); } catch (_) {} } chapterPreview = list; queueVideoTotal = list.reduce((s, c) => s + (c.videos?.length || 0), 0); queueVideoDone = list.reduce((s, c) => s + (c.videos || []).filter(v => v.done || Number(v.process) >= 100).length, 0); queueExamTotal = examTotal; queueExamDone = examDone; renderChapterPreview(); updateQueueSummary(); } function renderChapterPreview() { const box = document.getElementById('xj-chapter-preview'); if (!box) return; if (!chapterPreview.length) { box.innerHTML = "
\u8bf7\u9009\u62e9\u8bfe\u7a0b
"; return; } box.innerHTML = chapterPreview.map(course => { const items = (course.videos || []).map(v => { const done = v.done || Number(v.process) >= 100; const color = v.active ? '#16a34a' : done ? '#64748b' : '#334155'; const mark = v.active ? '▶' : done ? '✓' : '•'; return `
${mark} ${escHtml(v.title)}${Math.round(Number(v.process) || 0)}%
`; }).join(''); return `
${escHtml(course.courseTitle)}
${items || "
\u65e0\u89c6\u9891
"}
`; }).join(''); } function updateQueueSummary() { const doneEl = document.getElementById('xj-queue-done'); const totalEl = document.getElementById('xj-queue-total'); const pctEl = document.getElementById('xj-queue-percent'); const barEl = document.getElementById('xj-queue-progress'); const examEl = document.getElementById('xj-queue-exam'); const textEl = document.getElementById('xj-queue-text'); const q = getSelectedCourseIds(); const total = Math.max(0, Number(queueVideoTotal || 0)); const done = Math.max(0, Math.min(total, Number(queueVideoDone || 0))); const examTotal = Math.max(0, Number(queueExamTotal || 0)); const examDone = Math.max(0, Math.min(examTotal, Number(queueExamDone || 0))); if (doneEl) doneEl.textContent = String(done); if (totalEl) totalEl.textContent = String(total); const pct = total > 0 ? Math.max(0, Math.min(100, Math.round((done / total) * 100))) : 0; if (pctEl) pctEl.textContent = pct + '%'; if (barEl) barEl.style.width = pct + '%'; if (examEl) examEl.textContent = examTotal > 0 ? examDone + ' / ' + examTotal : '—'; if (textEl) { if (!q.length) textEl.textContent = '\u672a\u9009\u62e9'; else if (total > 0) { const examPart = examTotal > 0 ? ' · \u8003\u8bd5 ' + examDone + '/' + examTotal : ''; textEl.textContent = '\u89c6\u9891\u5df2\u5b66\u4e60 ' + done + '/' + total + examPart + '\uff08\u5df2\u9009 ' + q.length + ' \u95e8\uff09'; } else textEl.textContent = '\u5df2\u9009 ' + q.length + ' \u95e8\uff0c\u6b63\u5728\u7edf\u8ba1…'; } } function updateCurrentMetaPanel() { const courseEl = document.getElementById('xj-current-course'); const chapterEl = document.getElementById('xj-current-chapter'); const taskEl = document.getElementById('xj-current-task'); if (courseEl) { courseEl.textContent = currentCourseName || '\u65e0'; courseEl.title = currentCourseName || '\u65e0'; } if (chapterEl) { chapterEl.textContent = currentChapterName || '\u65e0'; chapterEl.title = currentChapterName || '\u65e0'; } if (taskEl) { const taskText = lastAction || (running ? '\u5b66\u4e60\u4e2d…' : '\u70b9\u51fb\u5f00\u59cb'); taskEl.textContent = taskText; taskEl.title = taskText; } } function syncStartStopButtons() { const startBtn = document.getElementById('xj-start'); const stopBtn = document.getElementById('xj-stop'); const needMode = false; if (startBtn) { const startOff = running || needMode; startBtn.disabled = startOff; startBtn.classList.toggle('zjzx-btn-off', startOff); } if (stopBtn) { stopBtn.disabled = !running; stopBtn.classList.toggle('zjzx-btn-off', !running); } } function updatePanel() { syncProBuyButtonUi(); syncStartStopButtons(); const statusEl = document.getElementById('xj-auto-status'); if (!statusEl) return; if (panelRefreshing) statusEl.textContent = '\u5237\u65b0\u4e2d…'; else if (!running) statusEl.textContent = '\u5df2\u505c\u6b62'; else if (/\u6b63\u5728\u8003\u8bd5|\u6b63\u5728\u91cd\u8003|\u7b49\u5f85\u4ea4\u5377|\u8003\u8bd5\u4e2d/.test(lastAction)) statusEl.textContent = '\u8003\u8bd5\u4e2d'; else if (/\u5b66\u4e60\u4e2d/.test(lastAction)) statusEl.textContent = '\u5b66\u4e60\u4e2d'; else statusEl.textContent = '\u5b66\u4e60\u4e2d'; updateCloudPanelUI(); updateCurrentMetaPanel(); updateQueueSummary(); const studyModeSel = document.getElementById('xj-study-mode'); if (studyModeSel) studyModeSel.disabled = true; const hintEl = document.getElementById('xj-start-hint'); if (hintEl && running) { hintEl.textContent = CHAPTER_PREVIEW_RUNNING_HINT; hintEl.className = 'zjzx-start-hint zjzx-start-hint-live'; hintEl.style.display = 'block'; } else if (hintEl) { hintEl.className = 'zjzx-start-hint'; if (!running && panelHint) { hintEl.textContent = panelHint; hintEl.style.display = 'block'; } else { hintEl.textContent = ''; hintEl.style.display = 'none'; } } } async function loadCourseList(specialTopicId) { courseListUserTouched = false; panelRefreshing = true; updatePanel(); try { panelCourses = await fetchCourses(specialTopicId); renderCourseList(); await refreshChapterPreview(); } catch (e) { const raw = String(e.message || ''); let hint = '\u8bfe\u7a0b\u5217\u8868\u52a0\u8f7d\u5931\u8d25\uff0c\u8bf7\u5237\u65b0\u7f51\u9875\u540e\u91cd\u8bd5'; if (/\u672a\u767b\u5f55|\u672a\u68c0\u6d4b\u5230\u767b\u5f55/.test(raw)) hint = '\u672a\u68c0\u6d4b\u5230\u767b\u5f55\uff0c\u8bf7\u5148\u767b\u5f55\u5e73\u53f0\u5e76\u5237\u65b0\u7f51\u9875'; else if (/\u8fc7\u671f|\u91cd\u65b0\u767b\u5f55/.test(raw)) hint = '\u767b\u5f55\u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55\u5e76\u5237\u65b0\u7f51\u9875'; const box = document.getElementById('xj-course-list'); if (box) { box.innerHTML = "
" + escHtml(hint) + "
\u6309 F5 \u6216 Ctrl+R \u5237\u65b0\u9875\u9762
"; } setPanelHint(hint); log(hint, 'err'); } finally { panelRefreshing = false; updatePanel(); } } function formatTopicLabel(topic) { const year = String(topic?.year || '').trim(); const name = String(topic?.specialTopicName || '').trim(); if (!name) return year || '\u672a\u547d\u540d\u4e13\u9898'; if (!year || name.startsWith(year)) return name; return year + ' ' + name; } async function loadTopics(retry) { const sel = document.getElementById('xj-topic-select'); if (!sel) return; if (!retry) sel.innerHTML = ''; try { if (!getPlatformToken()) { await waitForPlatformToken(8000); } if (!getPlatformToken()) { throw new Error('\u672a\u68c0\u6d4b\u5230\u767b\u5f55\u72b6\u6001\uff0c\u8bf7\u5148\u767b\u5f55\u5e73\u53f0'); } const topics = await fetchTopics(); sel.innerHTML = topics.map(t => '' ).join('') || ''; sel.onchange = () => { panelTopicId = sel.value; loadCourseList(sel.value); }; panelTopicId = sel.value; await loadCourseList(sel.value); } catch (e) { sel.innerHTML = ''; const raw = String(e.message || '\u52a0\u8f7d\u5931\u8d25'); const hint = /\u672a\u767b\u5f55|\u8fc7\u671f|\u91cd\u65b0\u767b\u5f55/.test(raw) ? raw + '\uff0c\u8bf7\u5237\u65b0\u7f51\u9875\u540e\u91cd\u8bd5' : '\u4e13\u9898\u52a0\u8f7d\u5931\u8d25\uff0c\u8bf7\u5237\u65b0\u7f51\u9875\u540e\u91cd\u8bd5'; setPanelHint(hint); log(hint, 'err'); console.error('[\u65b0\u7586\u7ee7\u7eed\u6559\u80b2] \u52a0\u8f7d\u4e13\u9898\u5931\u8d25', e); if (!retry) { setTimeout(() => { void loadTopics(true); }, 3000); } } } function createPanel() { const old = document.getElementById('xj-auto-panel'); if (old) old.remove(); injectPanelStyles(); const panel = document.createElement('div'); panel.id = 'xj-auto-panel'; panel.className = 'zjzx-panel-max'; panel.innerHTML = `
\u65b0\u7586\u7ee7\u7eed\u6559\u80b2\u5728\u7ebf\u5b66\u4e60\u52a9\u624bv${SCRIPT_VERSION}
\u81ea\u52a8\u5b66\u4e60·\u81ea\u52a8\u8003\u8bd5·\u7701\u65f6\u7701\u5fc3
\u5b66\u4e60\u72b6\u6001\u5df2\u505c\u6b62
0/0 \u89c6\u9891\u5df2\u5b66\u4e60 · \u8003\u8bd5
0%
\u8bfe\u7a0b\u9009\u62e9\u8bfe\u7a0b\u5217\u8868
\u767b\u5f55\u540e\u5c06\u81ea\u52a8\u52a0\u8f7d\u8bfe\u7a0b
\u7ae0\u8282\u9884\u89c8\u5b9e\u65f6\u8fdb\u5ea6
\u8bf7\u9009\u62e9\u8bfe\u7a0b
\u5b66\u4e60\u65e5\u5fd7
\u6700\u8fd1\u8bb0\u5f55
\u6388\u6743\u4e0e\u9009\u9879\u8bbe\u7f6e
\u7528\u6237\u7c7b\u578b\u672a\u6821\u9a8c
\u514d\u8d39\u4f53\u9a8c\u7ae0\u82820/3
Token
\u5f53\u524d\u8bfe\u7a0b\u65e0
\u5f53\u524d\u7ae0\u8282\u65e0
\u5f53\u524d\u4efb\u52a1\u70b9\u51fb\u5f00\u59cb
`; document.body.appendChild(panel); createProModal(); const savedPos = readPanelPos(); if (savedPos && savedPos.left != null && savedPos.top != null) { panel.style.right = 'auto'; panel.style.left = savedPos.left + 'px'; panel.style.top = savedPos.top + 'px'; } applyPanelCollapsed(panel, readPanelCollapsed()); enablePanelDrag(panel); document.getElementById('xj-btn-min')?.addEventListener('click', (e) => { e.stopPropagation(); writePanelCollapsed(true); applyPanelCollapsed(panel, true); }); document.getElementById('xj-btn-max')?.addEventListener('click', (e) => { e.stopPropagation(); writePanelCollapsed(false); applyPanelCollapsed(panel, false); }); panel.querySelectorAll('.zjzx-tab-btn').forEach(btn => { btn.addEventListener('click', () => switchPanelTab(btn.dataset.tab)); }); document.getElementById('xj-clear-log')?.addEventListener('click', clearRunLog); document.getElementById('xj-topic-select')?.addEventListener('change', () => { panelTopicId = document.getElementById('xj-topic-select').value; loadCourseList(panelTopicId); }); document.getElementById('xj-start')?.addEventListener('click', async () => { if (running) return; clearPanelHint(); syncStartStopButtons(); await _sp(); syncStartStopButtons(); updatePanel(); }); document.getElementById('xj-stop')?.addEventListener('click', () => { if (!running) return; abortFlag = true; running = false; clearChapterTabHighlight(); clearEngineRunState(); stopPanelPolling(); log('\u6b63\u5728\u505c\u6b62…', 'warn'); syncStartStopButtons(); updatePanel(); }); document.getElementById('xj-join-qq')?.addEventListener('click', () => { try { GM_openInTab(QQ_GROUP_LINK, { active: true, insert: true, setParent: true }); } catch (_) { window.open(QQ_GROUP_LINK, '_blank'); } }); document.getElementById('xj-open-pro')?.addEventListener('click', openProModal); document.getElementById('xj-cloud-save')?.addEventListener('click', async () => { cloud.token = String(document.getElementById('xj-cloud-token')?.value || '').trim(); localStorage.setItem(CLOUD_TOKEN_KEY, cloud.token); cloud.tier = cloud.token ? 'unknown' : 'free'; cloud.lease = ''; cloud.leaseExp = 0; profileSyncedThisSession = false; try { await _rl(true); log('\u6388\u6743\u5df2\u66f4\u65b0', 'ok'); } catch (e) { log('\u6388\u6743\u6821\u9a8c\u5931\u8d25', 'err'); } updateCloudPanelUI(); }); studyMode = 'efficiency'; try { const cached = JSON.parse(localStorage.getItem(CLOUD_LEASE_KEY) || '{}'); if (cached.lease) { cloud.lease = cached.lease; const parsedExp = parseLeaseExp(cached.lease); cloud.leaseExp = parsedExp || cached.exp; cloud.tier = cached.tier || cloud.tier; cloud.freeLimit = cached.freeLimit || 3; cloud.freeUsed = cached.freeUsed || 0; cloud.proExpireAt = cached.proExpireAt || 0; } } catch (_) {} syncPanelNotice(); updateCloudPanelUI(); syncStartStopButtons(); updatePanel(); } async function init() { hookPlatformTokenCapture(); createPanel(); bindEngineRunPersistence(); await _lc(); syncPanelNotice(); await waitForPlatformToken(8000); if (getPlatformToken()) { await loadTopics(); if (readEngineRunState()) { tryResumePipeline().catch(() => clearEngineRunState()); } else { _cr().catch(() => {}); } } else { log('\u8bf7\u5148\u767b\u5f55\u5e73\u53f0', 'warn'); setPanelHint('\u8bf7\u5148\u767b\u5f55\u65b0\u7586\u7ee7\u7eed\u6559\u80b2\u5e73\u53f0\uff0c\u767b\u5f55\u540e\u5c06\u81ea\u52a8\u52a0\u8f7d\u8bfe\u7a0b'); } setInterval(updatePanel, 1000); } installPageTokenBridge(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();