feat: add manual confirmation toggle for incomplete exams
This commit is contained in:
+43
-8
@@ -54,7 +54,9 @@
|
||||
const complete = bank.expectedTotal > 0 && bank.expectedTotal === bank.total && boxes.length === bank.total;
|
||||
// The confirmed threshold is 80 raw points on the supplied 100-point paper.
|
||||
const scoreValid = complete && validPoints && Math.abs(totalScore - 100) < 0.000001;
|
||||
const fullyVerified = complete && matchedCount === bank.total && verifiedCount === bank.total;
|
||||
return { title: bank.title, total: bank.total, complete, totalScore, predictedScore, matchedCount, verifiedCount,
|
||||
fullyVerified, unverifiedCount: Math.max(0, bank.total - verifiedCount),
|
||||
missing: details.filter(item => item.status === 'missing').length,
|
||||
conflict: details.filter(item => item.status === 'conflict').length, scoreValid, details,
|
||||
signature: JSON.stringify([bank.title, bank.expectedTotal, bank.questions.map((q, index) => [q.type, q.question, q.options.map(o => [o.label, o.text]), details[index].points])]),
|
||||
@@ -97,6 +99,10 @@
|
||||
return /"(?:attemptid|examattemptid|answerrecordid|examrecordid|paperrecordid|recordid|data-attempt-id|data-exam-attempt-id|data-answer-record-id|data-exam-record-id)"/.test(keyFor(ctx));
|
||||
}
|
||||
function settings() { return options.getSettings ? options.getSettings() : {}; }
|
||||
function manualIncomplete() { return settings().manualConfirmIncomplete !== false; }
|
||||
function scopeValue(ctx, evidence, topic) {
|
||||
return JSON.stringify([keyFor(ctx), ctx.revision, evidence.signature, topic?.courses, manualIncomplete()]);
|
||||
}
|
||||
function resultVisible() {
|
||||
return Array.from(doc.querySelectorAll('.exam_submited_box,.exam-result,app-exam-result,[data-exam-result]'))
|
||||
.some(node => visible(node) && /提交成功|交卷成功|考试结束|得分|成绩/.test(node.textContent));
|
||||
@@ -152,20 +158,29 @@
|
||||
enabled = false;
|
||||
await observe();
|
||||
if (token !== generation || !sameContext(requested, context()) || !entry || ['submitting', 'confirming', 'unknown'].includes(stage) || destroyed || resultVisible()) return false;
|
||||
if (entry.manualRequired && stage !== 'awaitingAttempt' && manualIncomplete()) {
|
||||
stage = 'waiting'; error = '网站提示存在未作答题,请在原网页手动确认交卷';
|
||||
emit({ requiresManualSubmission: true, message: error }); return false;
|
||||
}
|
||||
const ctx = context(), topic = options.getTopic();
|
||||
if (!topic || topic.status !== 'matched') { error = topic?.reason || '请先确认考试主题与题库关联'; emit({ message: error }); return false; }
|
||||
const evidence = readPaper(doc, ctx, options.getSnapshot(), topic);
|
||||
if (!evidence.complete || !evidence.scoreValid || !ctx.ready || !ctx.exam || ctx.submitted) { emit({ message: evidence.reason || '等待整卷加载完成后启动考试任务' }); return false; }
|
||||
if (Array.from(doc.querySelectorAll('.dialog_box,[role="dialog"],.modal')).some(visible)) {
|
||||
emit({ message: '请先在网页取消或关闭当前交卷对话框,再继续任务' }); return false;
|
||||
}
|
||||
if (stage === 'awaitingAttempt') {
|
||||
const timer = timerEvidence(ctx);
|
||||
entry = { startedAt: timer.startedAt, initialRemaining: timer.remain, lastRemaining: timer.remain, lastObservedAt: timer.stamp, stage: 'idle' };
|
||||
}
|
||||
scope = JSON.stringify([currentKey, ctx.revision, evidence.signature, topic.courses]);
|
||||
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();
|
||||
if (token !== generation || destroyed || !enabled || !sameContext(ctx, context()) || !context().ready || options.getTopic()?.status !== 'matched' || scope !== scopeFor(context())) {
|
||||
if (token === generation) disable('启动过程中页面、题库或任务状态已变化,请重新启动'); return false;
|
||||
}
|
||||
log('当前考试已授权:全文核验后严格大于 80 分且达到最早交卷时间才交卷');
|
||||
log(manualIncomplete() ? '当前考试已授权:题目不全时由用户在网页确认;整卷核验后按得分和时长交卷'
|
||||
: '题目不全时手动确认已关闭:全部可匹配题核验后,严格大于 80 分且达到最早时长才交卷');
|
||||
return true;
|
||||
}
|
||||
function disable(reason) {
|
||||
@@ -177,7 +192,7 @@
|
||||
}
|
||||
function scopeFor(ctx) {
|
||||
const topic = options.getTopic(), evidence = readPaper(doc, ctx, options.getSnapshot(), topic);
|
||||
return JSON.stringify([keyFor(ctx), ctx.revision, evidence.signature, topic?.courses]);
|
||||
return scopeValue(ctx, evidence, topic);
|
||||
}
|
||||
function canClick(token, ctx, confirmation) {
|
||||
if (token !== generation || destroyed || !enabled || !sameContext(ctx, context())) return false;
|
||||
@@ -185,8 +200,9 @@
|
||||
const configured = Number(settings().minSubmitMinutes), minimum = Number.isFinite(configured) && configured > 0 ? configured * 60 : 360;
|
||||
const pendingTooLong = confirmation && now() - entry.submitAt > 12000;
|
||||
return current.mode === 'test' && current.root?.isConnected && current.ready && topic?.status === 'matched'
|
||||
&& scope === JSON.stringify([keyFor(current), current.revision, evidence.signature, topic.courses])
|
||||
&& evidence.scoreValid && evidence.matchedCount === evidence.verifiedCount && evidence.predictedScore > 80
|
||||
&& scope === scopeValue(current, evidence, topic)
|
||||
&& evidence.scoreValid && evidence.matchedCount === evidence.verifiedCount
|
||||
&& (!manualIncomplete() || evidence.fullyVerified && !entry.manualRequired) && evidence.predictedScore > 80
|
||||
&& options.getRunnerState() === 'complete' && (now() - entry.startedAt) / 1000 >= minimum
|
||||
&& remainingSeconds(doc) !== 0 && !pendingTooLong && (confirmation || current.exam && !current.submitted);
|
||||
}
|
||||
@@ -194,6 +210,9 @@
|
||||
const dialogs = Array.from(doc.querySelectorAll('.dialog_box,[role="dialog"],.modal')).filter(visible);
|
||||
const containers = dialogs.filter(node => !dialogs.some(other => other !== node && other.contains(node)));
|
||||
if (!containers.length) return { pending: true };
|
||||
if (manualIncomplete() && containers.some(node => /未答|未作答|没有作答|尚未作答|漏答|未完成.*题|题.*未完成/.test(clean(node.textContent)))) {
|
||||
return { manualRequired: true, reason: '网站提示存在未作答题,请在原网页手动确认交卷' };
|
||||
}
|
||||
const matches = containers.filter(node => /(?:确认|确定|是否).*交卷|交卷.*(?:确认|确定)|未答.*(?:提交|交卷)|(?:提交|交卷).*未答/.test(clean(node.textContent)));
|
||||
if (matches.length !== 1 || /验证|人脸|异常|失败|重新考试/.test(matches[0].textContent)) return { reason: '交卷对话框无法唯一确认,请人工检查' };
|
||||
const buttons = Array.from(matches[0].querySelectorAll('button,[role="button"],.btn,.submited_btn'))
|
||||
@@ -212,7 +231,7 @@
|
||||
if (resultVisible()) {
|
||||
stage = 'submitted'; entry.stage = stage; enabled = false; await persist();
|
||||
if (token !== generation || destroyed || !sameContext(ctx, context())) return last;
|
||||
return emit({ message: '网站已显示考试结果', submitted: true });
|
||||
return emit({ message: '网站已显示考试结果', submitted: true, requiresManualSubmission: false });
|
||||
}
|
||||
const topic = options.getTopic(), evidence = readPaper(doc, ctx, options.getSnapshot(), topic);
|
||||
const elapsedSeconds = Math.max(0, (now() - entry.startedAt) / 1000);
|
||||
@@ -220,11 +239,14 @@
|
||||
const minimum = Number.isFinite(configured) && configured > 0 ? configured * 60 : 360;
|
||||
const waitSeconds = Math.max(0, Math.ceil(minimum - elapsedSeconds));
|
||||
const remaining = remainingSeconds(doc);
|
||||
const runner = options.getRunnerState();
|
||||
entry.lastObservedAt = now(); entry.lastRemaining = remaining;
|
||||
emit(Object.assign({}, evidence, { elapsedSeconds: Math.floor(elapsedSeconds), waitSeconds, remainingSeconds: remaining,
|
||||
manualConfirmIncomplete: manualIncomplete(),
|
||||
requiresManualSubmission: manualIncomplete() && (Boolean(entry.manualRequired) || runner === 'complete' && !evidence.fullyVerified),
|
||||
predictedScore: Math.round(evidence.predictedScore * 100) / 100, submitted: false, message: error || evidence.reason }));
|
||||
if (!enabled) return last;
|
||||
if (scope !== JSON.stringify([currentKey, ctx.revision, evidence.signature, topic?.courses]) || topic?.status !== 'matched') {
|
||||
if (scope !== scopeValue(ctx, evidence, topic) || topic?.status !== 'matched') {
|
||||
disable('试卷、题库或主题关联已变更,请重新启动核验'); return last;
|
||||
}
|
||||
if (remaining === 0 || ctx.submitted && !['submitting', 'confirming'].includes(stage)) { disable('网站考试已经结束或提交'); return last; }
|
||||
@@ -233,7 +255,6 @@
|
||||
if (token !== generation || destroyed || !sameContext(ctx, context())) return last;
|
||||
return emit({ message: '提交结果未确认,已停止重复提交,请检查网站状态' });
|
||||
}
|
||||
const runner = options.getRunnerState();
|
||||
const allVerified = evidence.matchedCount === evidence.verifiedCount;
|
||||
if (!evidence.scoreValid || !allVerified || runner !== 'complete') {
|
||||
if (stage === 'confirming' || stage === 'submitting') {
|
||||
@@ -241,14 +262,28 @@
|
||||
if (token !== generation || destroyed || !sameContext(ctx, context())) return last;
|
||||
return emit({ message: '交卷过程中核验条件变化,等待人工检查' });
|
||||
}
|
||||
if (runner === 'complete') {
|
||||
enabled = false; stage = 'waiting'; error = evidence.reason || '题目未全部核验,请在网页检查未核验题并手动确认交卷';
|
||||
return emit({ requiresManualSubmission: true, message: error });
|
||||
}
|
||||
stage = 'answering'; return emit({ message: evidence.reason || '等待所有可匹配题处理并回读核验' });
|
||||
}
|
||||
if (manualIncomplete() && !evidence.fullyVerified) {
|
||||
enabled = false; stage = 'waiting';
|
||||
error = '题目未全部核验(' + evidence.verifiedCount + '/' + evidence.total + '),请在网页检查未核验题并手动确认交卷';
|
||||
log(error); return emit({ requiresManualSubmission: true, message: error });
|
||||
}
|
||||
if (!(evidence.predictedScore > 80)) { stage = 'waiting'; return emit({ message: '预计得分小于或等于 80 分,等待人工操作' }); }
|
||||
if (remaining !== null && remaining <= waitSeconds) { stage = 'waiting'; return emit({ message: '网站截止时间早于最早交卷时间,等待人工操作' }); }
|
||||
if (waitSeconds) { stage = 'waitingSubmit'; return emit({ message: '核验完成,等待最早交卷时间' }); }
|
||||
if (stage === 'confirming' || stage === 'submitting') {
|
||||
const target = confirmationButton();
|
||||
if (target.pending) return last;
|
||||
if (target.manualRequired) {
|
||||
generation++; enabled = false; stage = 'waiting'; entry.stage = 'manualRequired'; entry.manualRequired = true;
|
||||
error = target.reason; await persist(); log(error);
|
||||
return emit({ requiresManualSubmission: true, message: error });
|
||||
}
|
||||
if (target.reason) { disable(target.reason); return last; }
|
||||
if (!entry.confirmed) {
|
||||
entry.confirmed = true; entry.stage = stage = 'confirming'; await persist();
|
||||
|
||||
Reference in New Issue
Block a user