Recover stalled platform progress and missing saves with bounded reloads

This commit is contained in:
2026-10-12 18:08:54 +08:00
parent 1f6706efbb
commit 27293ad195
8 changed files with 173 additions and 25 deletions
+23 -7
View File
@@ -1,7 +1,7 @@
// ==UserScript==
// @name 学员中心 · 2倍速学习助手
// @namespace local.icampus.study
// @version 1.4.2
// @version 1.4.3
// @description 支持面板收纳与拖拽、全过程静音、后台2倍速播放、自动测验及错题重试。
// @match https://icampus.yunyinsh.com/course/*
// @run-at document-start
@@ -346,6 +346,16 @@
if (active && active.courseId === meta.id && active.id === resourceId) {
lastNetworkAt = Date.now(); active.saved = true;
active.serverPosition = data.currentPositionSeconds;
const position=Number.isFinite(data.currentPositionSeconds) && data.currentPositionSeconds>=0 ? data.currentPositionSeconds : null;
const progress=Number.isFinite(data.courseProgressPercent) && data.courseProgressPercent>=0 && data.courseProgressPercent<=100 ? data.courseProgressPercent : null;
if(position!==null || progress!==null || data.resourceCompleted===true) {
if(!active.saveObserved || data.resourceCompleted===true ||
(position!==null && position>(active.savePosition ?? -1)+0.1) ||
(progress!==null && progress>(active.saveCoursePercent ?? -1)+0.001)) active.saveAdvancedAt=Date.now();
active.saveObserved=true;
if(position!==null) active.savePosition=Math.max(active.savePosition ?? position,position);
if(progress!==null) active.saveCoursePercent=Math.max(active.saveCoursePercent ?? progress,progress);
}
}
}
render();
@@ -624,7 +634,7 @@
}
function recoverMedia(id, resourceId, reason) {
const key = `${id}/${resourceId}`;
if (recoveryUsed.has(key)) return park(`${reason},自动恢复一次后仍失败,请检查网络并手动刷新。`);
if (recoveryUsed.has(key)) return park(`${reason},自动恢复一次后仍失败,请检查网络并手动刷新。`,{passive:true});
recoveryUsed.add(key);
persistIntent();
try {
@@ -632,7 +642,7 @@
mode:mode.value, autoQuiz:autoQuiz?.checked !== false, used:[...recoveryUsed]}));
} catch { return park(`${reason},无法保存恢复状态,请手动刷新后继续。`); }
// The website tears down its old session and fetches fresh media on reload.
// Only this explicit recovery reload resumes; ordinary refreshes stay paused.
// Persisted intent resumes after reload while keeping this resource's recovery quota.
running=false; ++runNumber;
const v=active?.video; active=null; mediaLoad=null;
if(v && !v.paused) v.pause();
@@ -684,7 +694,7 @@
active.endedAt ||= Date.now();
if (resource?.completed !== true) {
show('视频已结束,等待平台确认保存…');
if (Date.now() - active.endedAt > waitLimit(25000)) park('平台尚未确认视频完成,请返回列表核对后再继续。');
if (Date.now() - active.endedAt > waitLimit(25000)) recoverMedia(id,active.id,'视频已结束,但平台迟迟未确认完成');
return;
}
log('平台已确认本段视频完成。'); active = null;
@@ -719,7 +729,10 @@
const run = runNumber;
active = {video, id: resource.id, courseId: id, src: video.currentSrc,
lastPosition: video.currentTime, progressedAt: Date.now(), started: Date.now(), saved: false, serverPosition: null,
phase:'starting', attempts:0, playPending:false, nextPlayAt:0};
phase:'starting', attempts:0, playPending:false, nextPlayAt:0,
saveObserved:false, saveAdvancedAt:Date.now(),
savePosition:Number.isFinite(resource.currentPositionSeconds)?resource.currentPositionSeconds:null,
saveCoursePercent:Number.isFinite(detail.course.progressPercent)?detail.course.progressPercent:null};
mediaLoad=null;
lastNetworkAt = Date.now();
video.playbackRate = 2;
@@ -774,7 +787,10 @@
if (Date.now() - a.progressedAt > waitLimit(60000)) return recoverMedia(a.courseId,a.id,'视频长时间没有推进');
const saveTimeout=waitLimit(75000);
if (Date.now()>foregroundGraceUntil && Date.now()-lastNetworkAt>saveTimeout)
return park(`超过${saveTimeout/1000}秒未收到平台保存确认,已暂停,避免继续播放却未计入进度。`);
return recoverMedia(a.courseId,a.id,`超过${saveTimeout/1000}秒未收到平台保存确认`);
const progressTimeout=waitLimit(120000);
if (Date.now()>foregroundGraceUntil && a.saveObserved && resource?.completed!==true && Date.now()-a.saveAdvancedAt>progressTimeout)
return recoverMedia(a.courseId,a.id,`平台已保存的位置和学习进度超过${progressTimeout/1000}秒未推进`);
}
async function tick() {
applyFullMute();
@@ -918,7 +934,7 @@
panel = host.attachShadow({mode: 'open'});
panel.innerHTML = `<style>:host{display:block;font:14px/1.6 system-ui;color:#eaf0fa}[hidden]{display:none!important}section{box-sizing:border-box;width:322px;max-width:calc(100vw - 16px);background:#17243a;border:1px solid #6883a6;border-radius:12px;padding:15px;box-shadow:0 5px 24px #0005}strong{font-size:14px}p{margin:9px 0;white-space:pre-line}small{color:#b7c7df}button,select{font:inherit;border-radius:6px;border:1px solid #7186a5;padding:5px 9px;margin:3px 5px 3px 0}button{cursor:pointer;background:#dbeaff;color:#123}button:disabled{opacity:.5;cursor:default}select{width:100%;background:#fff;color:#123}[data-status]{min-height:45px}details{margin-top:8px}pre{white-space:pre-wrap;max-height:160px;overflow:auto;font-size:12px}[data-drag-handle]{display:flex;align-items:center;justify-content:space-between;gap:8px;cursor:grab;touch-action:none;user-select:none}[data-collapse]{flex-shrink:0;margin:0}.compact{display:flex;align-items:center;gap:10px;max-width:calc(100vw - 16px);box-sizing:border-box;margin:0;padding:10px 13px;border-radius:999px;background:#17243a;color:#eaf0fa;cursor:grab;touch-action:none;user-select:none}.compact [data-mini-state]{color:#b7c7df;font-size:12px}.compact:focus-visible,button:focus-visible{outline:2px solid #a3ccff;outline-offset:-3px}</style>
<button class="compact" data-expand hidden aria-expanded="false" aria-label="展开学习助手面板"><span>2×学习助手</span><span data-mini-state></span><span>展开</span></button>
<section data-main-panel aria-label="学习助手面板"><header data-drag-handle title="按住标题拖动面板"><strong>2倍速学习助手 · 1.4.2</strong><button data-collapse aria-label="收起学习助手面板">收起</button></header><p data-status></p><small data-progress></small>
<section data-main-panel aria-label="学习助手面板"><header data-drag-handle title="按住标题拖动面板"><strong>2倍速学习助手 · 1.4.3</strong><button data-collapse aria-label="收起学习助手面板">收起</button></header><p data-status></p><small data-progress></small>
<p data-overall></p><progress data-overall-bar aria-label="整体课程完成百分比" max="100" value="0" style="width:100%"></progress>
<p data-estimate style="font-size:12px;color:#b7c7df"></p>
<select aria-label="播放范围"><option value="all">按列表顺序继续课程</option><option value="current">仅当前课程</option></select>