fix: resume video playback and advance linked exams with attempt controls

This commit is contained in:
薇星幻海
2026-10-18 02:12:06 +08:00
parent 2e044655bb
commit 1b497286dc
20 changed files with 1629 additions and 155 deletions
+28 -1
View File
@@ -100,8 +100,24 @@
}
function settings() { return options.getSettings ? options.getSettings() : {}; }
function manualIncomplete() { return settings().manualConfirmIncomplete !== false; }
function attemptPolicy() {
const policy = (options.getAttemptPolicy ? options.getAttemptPolicy() : root.YianAttemptPolicy?.read(doc.querySelector('app-examdetail,app-examtest,.pc_examtest'))) || {};
const active = settings().skipSingleAttemptQuiz !== false;
return { active, blocked: active && policy.blocked === true, count: policy.count == null ? null : policy.count,
reason: clean(policy.reason) || '仅有 1 次答题机会,已停止自动答题与交卷,请在原网页人工操作',
signature: active ? JSON.stringify([policy.signature || '', policy.blocked === true, policy.count == null ? null : policy.count]) : '' };
}
function blockSingleAttempt(policy) {
generation++; enabled = false;
if (['submitting', 'confirming', 'unknown'].includes(stage)) stage = 'unknown';
else stage = 'waiting';
error = policy.reason;
return emit({ skipSingleAttemptQuiz: policy.active, singleAttemptBlocked: true, attemptCount: policy.count,
requiresManualSubmission: true, message: error });
}
function scopeValue(ctx, evidence, topic) {
return JSON.stringify([keyFor(ctx), ctx.revision, evidence.signature, topic?.courses, manualIncomplete()]);
const policy = attemptPolicy();
return JSON.stringify([keyFor(ctx), ctx.revision, evidence.signature, topic?.courses, manualIncomplete(), policy.active, policy.signature]);
}
function resultVisible() {
return Array.from(doc.querySelectorAll('.exam_submited_box,.exam-result,app-exam-result,[data-exam-result]'))
@@ -156,8 +172,12 @@
async function enable() {
const token = ++generation, requested = context();
enabled = false;
const initialPolicy = attemptPolicy();
if (initialPolicy.blocked) { blockSingleAttempt(initialPolicy); return false; }
await observe();
if (token !== generation || !sameContext(requested, context()) || !entry || ['submitting', 'confirming', 'unknown'].includes(stage) || destroyed || resultVisible()) return false;
const currentPolicy = attemptPolicy();
if (currentPolicy.blocked) { blockSingleAttempt(currentPolicy); return false; }
if (entry.manualRequired && stage !== 'awaitingAttempt' && manualIncomplete()) {
stage = 'waiting'; error = '网站提示存在未作答题,请在原网页手动确认交卷';
emit({ requiresManualSubmission: true, message: error }); return false;
@@ -176,6 +196,8 @@
if (!manualIncomplete() && entry.manualRequired) { entry.manualRequired = false; entry.confirmed = false; delete entry.submitAt; }
scope = scopeValue(ctx, evidence, topic);
enabled = true; stage = 'answering'; error = ''; entry.stage = stage; entry.paperSignature = evidence.signature; await persist();
const finalPolicy = attemptPolicy();
if (token === generation && finalPolicy.blocked) { blockSingleAttempt(finalPolicy); return false; }
if (token !== generation || destroyed || !enabled || !sameContext(ctx, context()) || !context().ready || options.getTopic()?.status !== 'matched' || scope !== scopeFor(context())) {
if (token === generation) disable('启动过程中页面、题库或任务状态已变化,请重新启动'); return false;
}
@@ -196,6 +218,8 @@
}
function canClick(token, ctx, confirmation) {
if (token !== generation || destroyed || !enabled || !sameContext(ctx, context())) return false;
const policy = attemptPolicy();
if (policy.blocked) { blockSingleAttempt(policy); return false; }
const current = context(), topic = options.getTopic(), evidence = readPaper(doc, current, options.getSnapshot(), topic);
const configured = Number(settings().minSubmitMinutes), minimum = Number.isFinite(configured) && configured > 0 ? configured * 60 : 360;
const pendingTooLong = confirmation && now() - entry.submitAt > 12000;
@@ -234,6 +258,7 @@
return emit({ message: '网站已显示考试结果', submitted: true, requiresManualSubmission: false });
}
const topic = options.getTopic(), evidence = readPaper(doc, ctx, options.getSnapshot(), topic);
const policy = attemptPolicy();
const elapsedSeconds = Math.max(0, (now() - entry.startedAt) / 1000);
const configured = Number(settings().minSubmitMinutes);
const minimum = Number.isFinite(configured) && configured > 0 ? configured * 60 : 360;
@@ -243,8 +268,10 @@
entry.lastObservedAt = now(); entry.lastRemaining = remaining;
emit(Object.assign({}, evidence, { elapsedSeconds: Math.floor(elapsedSeconds), waitSeconds, remainingSeconds: remaining,
manualConfirmIncomplete: manualIncomplete(),
skipSingleAttemptQuiz: policy.active, singleAttemptBlocked: policy.blocked, attemptCount: policy.count,
requiresManualSubmission: manualIncomplete() && (Boolean(entry.manualRequired) || runner === 'complete' && !evidence.fullyVerified),
predictedScore: Math.round(evidence.predictedScore * 100) / 100, submitted: false, message: error || evidence.reason }));
if (policy.blocked) return blockSingleAttempt(policy);
if (!enabled) return last;
if (scope !== scopeValue(ctx, evidence, topic) || topic?.status !== 'matched') {
disable('试卷、题库或主题关联已变更,请重新启动核验'); return last;