fix: resume video playback and advance linked exams with attempt controls
This commit is contained in:
+23
-5
@@ -64,11 +64,16 @@
|
||||
return Array.from(container.querySelectorAll('.modal-footer button,.pc_examtest_content_question_showanswer,button[data-quiz-submit]'))
|
||||
.filter(node => visible(node) && !node.disabled && node.getAttribute('aria-disabled') !== 'true' && expression.test(clean(node.textContent)));
|
||||
}
|
||||
function attemptPolicy(view) {
|
||||
if (!view?.container) return null;
|
||||
const scope = view.container.closest(OWNER) || view.container;
|
||||
return root.YianAttemptPolicy?.read?.(scope) || null;
|
||||
}
|
||||
async function run(options) {
|
||||
const doc = options.document || root.document, matcher = options.matcher || root.YianAnswerMatcher;
|
||||
const context = options.context || {};
|
||||
if (!doc || !matcher || typeof matcher.match !== 'function') return { status: 'waiting', verified: false, reason: '课中题匹配组件未就绪' };
|
||||
let cancelled = false, verifiedCount = 0;
|
||||
let cancelled = false, verifiedCount = 0, attemptBlocked = null;
|
||||
const timeout = Math.max(100, Math.min(15000, Number(options.timeoutMs) || 6000));
|
||||
const configured = Number(context.answerInterval);
|
||||
const interval = Math.max(0, Math.min(120, Number.isFinite(configured) ? configured : 4)) * 1000;
|
||||
@@ -77,6 +82,17 @@
|
||||
if (cancelled || options.signal?.aborted) return false;
|
||||
try { return typeof options.getActive !== 'function' || options.getActive() === true; } catch (_) { return false; }
|
||||
};
|
||||
const attemptAllowed = () => {
|
||||
if (context.skipSingleAttemptQuiz === false) return true;
|
||||
if (!root.YianAttemptPolicy || typeof root.YianAttemptPolicy.read !== 'function') {
|
||||
attemptBlocked = '答题次数核验组件未就绪,已停止自动操作,请人工处理';
|
||||
return false;
|
||||
}
|
||||
const policy = attemptPolicy(detect(doc));
|
||||
if (!policy?.blocked) return true;
|
||||
attemptBlocked = policy.reason || '网页提示课中题答题机会受限,已停止自动操作,请人工处理';
|
||||
return false;
|
||||
};
|
||||
const manual = event => {
|
||||
if (!event.isTrusted) return;
|
||||
const node = event.target?.nodeType === 1 ? event.target : event.target?.parentElement;
|
||||
@@ -101,16 +117,18 @@
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const click = node => { if (!active() || !visible(node) || node.disabled || node.getAttribute('aria-disabled') === 'true') return false; node.click(); return true; };
|
||||
const click = node => { if (!active() || !attemptAllowed() || !visible(node) || node.disabled || node.getAttribute('aria-disabled') === 'true') return false; node.click(); return true; };
|
||||
try {
|
||||
let view = detect(doc);
|
||||
if (!view || view.ambiguous || view.kind === 'portalAnswer') return result('waiting', '未发现唯一且可完整核验的课中题,等待人工检查');
|
||||
for (let questionIndex = 0; questionIndex < 100; questionIndex++) {
|
||||
if (!active()) return result('waiting', '课中答题已取消,等待重新启动');
|
||||
if (!attemptAllowed()) return result('waiting', attemptBlocked, { manualRequired: true, singleAttempt: true });
|
||||
const original = read(view, context);
|
||||
if (original.error) return result('waiting', original.error);
|
||||
const matched = await matcher.match(original.question, context, options.snapshot);
|
||||
if (!active()) return result('waiting', '课中答题已取消,等待重新启动');
|
||||
if (!attemptAllowed()) return result('waiting', attemptBlocked, { manualRequired: true, singleAttempt: true });
|
||||
if (!matched || matched.status !== 'matched') return result(['missing', 'conflict'].includes(matched?.status) ? matched.status : 'unsupported', matched?.reason || '统一题库没有唯一准确答案');
|
||||
if (!Array.isArray(matched.correctTexts) || !matched.correctTexts.length || !same(matched.correctTexts, original.question.options.filter(option => matched.correctLabels?.includes(option.label)).map(option => option.text))) return result('waiting', '正确答案全文不能对应当前选项,已停止');
|
||||
const current = () => {
|
||||
@@ -128,7 +146,7 @@
|
||||
}
|
||||
for (const step of plan) {
|
||||
const latest = current();
|
||||
if (!latest || !click(latest.nodes[step.i])) return result('waiting', '题目或任务状态已变化,停止课中答题');
|
||||
if (!latest || !click(latest.nodes[step.i])) return result('waiting', attemptBlocked || '题目或任务状态已变化,停止课中答题', attemptBlocked ? { manualRequired: true, singleAttempt: true } : null);
|
||||
const changed = await waitFor(() => { const next = current(); return next && next.nodes[step.i].classList.contains(CHECKED) === step.checked ? next : null; });
|
||||
if (!changed || changed.cancelled) return result('waiting', '网站未确认选中状态或任务已取消');
|
||||
if (!await pause(150)) return result('waiting', '课中答题已取消');
|
||||
@@ -136,7 +154,7 @@
|
||||
const evidence = current();
|
||||
if (!evidence || !same(evidence.question.options.filter((_, i) => evidence.nodes[i].classList.contains(CHECKED)).map(option => option.text), matched.correctTexts)) return result('waiting', '课中题实际选中集合与正确答案全文不一致');
|
||||
const submits = buttons(view.container, /^(查看答案|提交答案|确认答案)$/);
|
||||
if (submits.length !== 1 || !current() || !click(submits[0])) return result('waiting', '课中题提交入口无法唯一确认');
|
||||
if (submits.length !== 1 || !current() || !click(submits[0])) return result('waiting', attemptBlocked || '课中题提交入口无法唯一确认', attemptBlocked ? { manualRequired: true, singleAttempt: true } : null);
|
||||
// The real template replaces the question with an answer overlay immediately;
|
||||
// absence of the question alone does not imply that the grade request succeeded.
|
||||
const graded = await waitFor(() => {
|
||||
@@ -155,7 +173,7 @@
|
||||
if (graded.closed) return result('complete', '答案全文核验通过,正常提交后弹题已关闭');
|
||||
const advances = buttons(graded.container, /^(下一题|关闭)$/);
|
||||
const isLast = advances.length === 1 && clean(advances[0].textContent) === '关闭';
|
||||
if (advances.length !== 1 || !click(advances[0])) return result('waiting', '课中题已判定正确,但下一题或关闭按钮无法唯一确认');
|
||||
if (advances.length !== 1 || !click(advances[0])) return result('waiting', attemptBlocked || '课中题已判定正确,但下一题或关闭按钮无法唯一确认', attemptBlocked ? { manualRequired: true, singleAttempt: true } : null);
|
||||
const next = await waitFor(() => {
|
||||
const seen = detect(doc);
|
||||
// CDK detach/attach can briefly render neither overlay between questions.
|
||||
|
||||
Reference in New Issue
Block a user