// ==UserScript==
// @name 易班考试答题助手(新版)
// @namespace http://tampermonkey.net/
// @version 4.0.0
// @description 易班考试自动答题助手 - 支持ID与文本匹配,仅题库未命中时使用AI,结果验证后填写。
// @author You
// @license All Rights Reserved
// @match *://exam.yooc.me/group/*/exam/*
// @match *://exam.yooc.me/group/*/exams
// @match *://www.yooc.me/*
// @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @connect exambackend.yooc.me
// @connect 38.76.215.191
// @connect *
// ==/UserScript==
(function() {
'use strict';
const QUESTION_BANK_STORAGE_KEY = 'question_bank_v3';
const SCRIPT_VERSION = '4.0.0';
GM_deleteValue('ai_fallback_key');
var currentUrl = window.location.href;
if (currentUrl.includes('www.yooc.me') && !currentUrl.includes('/mobile/')) {
console.log('[考试助手] 检测到网页版,跳转到手机版');
window.location.href = 'https://www.yooc.me/mobile/dashboard';
return;
}
var _0xda = 'http://38.76.215.191/script-api/license';
var _0xhb = 1800000;
var _0xt = null;
function _0xfp() {
var cached = GM_getValue('license_fingerprint', '');
if (cached) return cached;
var parts = [];
try { parts.push(navigator.userAgent || ''); } catch(e) {}
try { parts.push(navigator.language || ''); } catch(e) {}
try { parts.push(navigator.platform || ''); } catch(e) {}
try { parts.push(String(navigator.hardwareConcurrency || '')); } catch(e) {}
try { parts.push(screen.width + 'x' + screen.height); } catch(e) {}
try { parts.push(String(screen.colorDepth || '')); } catch(e) {}
try { parts.push(Intl.DateTimeFormat().resolvedOptions().timeZone || ''); } catch(e) {}
try { parts.push(String(navigator.deviceMemory || '')); } catch(e) {}
try {
var canvas = document.createElement('canvas');
var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (gl) {
var ext = gl.getExtension('WEBGL_debug_renderer_info');
if (ext) {
parts.push(gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) || '');
parts.push(gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) || '');
}
}
} catch(e) {}
try {
var c = document.createElement('canvas');
c.width = 200; c.height = 50;
var ctx = c.getContext('2d');
ctx.textBaseline = 'top';
ctx.font = '14px Arial';
ctx.fillStyle = '#f60';
ctx.fillRect(0, 0, 200, 50);
ctx.fillStyle = '#069';
ctx.fillText('fingerprint_test_2024', 2, 15);
parts.push(c.toDataURL());
} catch(e) {}
var raw = parts.join('|||');
var hash = CryptoJS.SHA256(raw).toString(CryptoJS.enc.Hex);
GM_setValue('license_fingerprint', hash);
return hash;
}
function _0xm(mode, reason) {
var old = document.getElementById('license-overlay');
if (old) old.remove();
var overlay = document.createElement('div');
overlay.id = 'license-overlay';
var errorText = '';
if (mode === 'expired') errorText = '授权已过期,请更换授权码。';
else if (mode === 'error') {
var reasonMap = {
'invalid_key': '授权码无效',
'disabled': '授权码已被禁用',
'fingerprint_mismatch': '设备不匹配(已达换绑上限)',
'network': '网络请求失败,请检查网络后重试'
};
errorText = reasonMap[reason] || '验证失败:' + (reason || '未知错误');
}
var title = mode === 'expired' ? '授权已过期' : '考试助手 - 授权验证';
var subtitle = mode === 'expired' ? '你的授权码已过期,请更换新的授权码。' : '请输入授权码激活脚本,获取试用请加QQ群。';
overlay.innerHTML =
'
' +
'
' +
'
' +
'
' +
'
' + title + '
' +
'
' + subtitle + '
' +
'
' +
'
' +
'
' +
(errorText ? '
' + errorText + '
' : '') +
'
' +
'
' +
'
' +
'
' +
'
' +
'
';
document.body.appendChild(overlay);
var input = document.getElementById('license-input');
var btn = document.getElementById('license-submit');
var msg = document.getElementById('license-msg');
var closeBtn = document.getElementById('license-close');
if (closeBtn) closeBtn.addEventListener('click', function() { overlay.remove(); });
input.addEventListener('input', function() {
var v = input.value.replace(/[^A-Za-z0-9]/g, '').toUpperCase().substring(0, 16);
var formatted = '';
for (var i = 0; i < v.length; i++) {
if (i > 0 && i % 4 === 0) formatted += '-';
formatted += v[i];
}
input.value = formatted;
});
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') btn.click();
});
btn.addEventListener('click', function() {
var key = input.value.trim();
if (key.length !== 19) {
msg.textContent = '请输入完整的授权码';
msg.style.color = '#dc2626';
return;
}
btn.disabled = true;
btn.textContent = '验证中...';
msg.textContent = '';
var fp = _0xfp();
_0xv(key, fp, function(result) {
if (result.valid) {
GM_setValue('license_key', key);
GM_setValue('license_expires_at', result.expires_at);
GM_setValue('license_verified_at', Date.now());
GM_setValue('license_token', result.token || '');
overlay.remove();
_0xsh();
initWithLicense(result.remaining_hours);
} else {
btn.disabled = false;
btn.textContent = '激活';
msg.textContent = (function() {
var m = {
'invalid_key': '授权码无效',
'disabled': '授权码已被禁用',
'expired': '授权码已过期',
'fingerprint_mismatch': '设备不匹配(已达换绑上限)',
'network': '无法连接授权服务器,请检查 Caddy 的 /script-api 路由'
};
return m[result.reason] || '验证失败';
})();
msg.style.color = '#dc2626';
}
});
});
input.focus();
}
function _0xv(key, fingerprint, callback) {
GM_xmlhttpRequest({
method: 'POST',
url: _0xda + '/verify',
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({ key: key, fingerprint: fingerprint }),
onload: function(resp) {
try {
callback(JSON.parse(resp.responseText));
} catch(e) {
callback({ valid: false, reason: 'network' });
}
},
onerror: function() {
callback({ valid: false, reason: 'network' });
}
});
}
function _0xhc() {
var key = GM_getValue('license_key', '');
var fp = GM_getValue('license_fingerprint', '');
if (!key || !fp) return;
GM_xmlhttpRequest({
method: 'POST',
url: _0xda + '/heartbeat',
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({ key: key, fingerprint: fp }),
onload: function(resp) {
try {
var result = JSON.parse(resp.responseText);
if (result.valid) {
GM_setValue('license_expires_at', result.expires_at);
GM_setValue('license_verified_at', Date.now());
GM_setValue('license_token', result.token || '');
GM_setValue('license_checksum', result.checksum || '');
_0xups(true, result.remaining_hours);
} else {
_0xups(false, 0);
_0xm('error', result.reason);
}
} catch(e) {}
},
onerror: function() {
console.warn('[考试助手] 心跳请求失败');
}
});
}
function _0xsh() {
if (_0xt) clearInterval(_0xt);
_0xt = setInterval(_0xhc, _0xhb);
}
function _0xfmt(hours) {
if (hours >= 24) return Math.floor(hours / 24) + '天' + (hours % 24) + '小时';
return hours + '小时';
}
function _0xups(active, remainingHours) {
var status = document.querySelector('#hp-status');
if (!status) return;
if (active) {
status.innerHTML = '已激活
剩' + _0xfmt(remainingHours);
status.className = 'hp-status';
var btns = document.querySelectorAll('#helper-panel button');
btns.forEach(function(b) { b.style.pointerEvents = 'auto'; b.style.opacity = '1'; });
} else {
status.textContent = '授权已过期';
status.className = 'hp-status expired';
var btns2 = document.querySelectorAll('#helper-panel button');
btns2.forEach(function(b) { b.style.pointerEvents = 'none'; b.style.opacity = '0.4'; });
}
}
function _0xrun(callback) {
var key = GM_getValue('license_key', '');
if (!key) {
_0xm('enter');
return;
}
var expiresAt = GM_getValue('license_expires_at', '');
var localExpired = false;
if (expiresAt) {
var expireDate = new Date(expiresAt);
if (expireDate <= new Date()) {
localExpired = true;
}
}
var lastVerified = GM_getValue('license_verified_at', 0);
var elapsed = Date.now() - lastVerified;
if (elapsed < _0xhb && expiresAt && !localExpired) {
var remaining = Math.floor((new Date(expiresAt) - new Date()) / 3600000);
callback(remaining);
_0xsh();
return;
}
var remaining = expiresAt ? Math.floor((new Date(expiresAt) - new Date()) / 3600000) : 0;
if (remaining < 0) remaining = 0;
callback(remaining);
var fp = _0xfp();
var token = GM_getValue('license_token', '');
function onVerifySuccess(result) {
GM_setValue('license_expires_at', result.expires_at);
GM_setValue('license_verified_at', Date.now());
GM_setValue('license_token', result.token || '');
_0xups(true, result.remaining_hours);
_0xsh();
}
function onVerifyFail(reason) {
if (reason === 'network') {
if (expiresAt && elapsed < 2 * 3600 * 1000) {
_0xsh();
} else {
_0xups(false, 0);
_0xm('error', 'network');
}
} else {
_0xups(false, 0);
_0xm('error', reason);
}
}
if (token && !localExpired) {
GM_xmlhttpRequest({
method: 'POST',
url: _0xda + '/heartbeat',
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({ key: key, fingerprint: fp, token: token }),
onload: function(resp) {
try {
var result = JSON.parse(resp.responseText);
if (result.valid) {
onVerifySuccess(result);
} else {
_0xdv(key, fp, onVerifySuccess, onVerifyFail);
}
} catch(e) {
_0xdv(key, fp, onVerifySuccess, onVerifyFail);
}
},
onerror: function() {
onVerifyFail('network');
}
});
} else {
_0xdv(key, fp, onVerifySuccess, onVerifyFail);
}
}
function _0xdv(key, fp, onSuccess, onFail) {
_0xv(key, fp, function(result) {
if (result.valid) {
onSuccess(result);
} else {
onFail(result.reason);
}
});
}
function initWithLicense(remainingDays) {
console.log('[考试助手] 授权验证通过,剩余 ' + _0xfmt(remainingDays));
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
}
console.log("脚本开始初始化...");
const userId = getCookie('user_id');
const userToken = getCookie('user_token');
const yibanId = getCookie('yiban_id');
console.log("用户信息:", { userId, userToken, yibanId });
if (!userToken) {
console.warn("未能从Cookie中获取 user_token,部分功能(如API请求)可能受限。");
}
const RELATED_BANK_MATCH_KEY = 'enable_related_bank_matching';
const SERVER_BASE = 'http://38.76.215.191/script-api';
const EXAM_CATALOG_KEY = 'exam_catalog_v2';
const relatedDownloadSession = new Map();
const questionBankDownloads = new Map();
const QUESTION_RENDER_TIMEOUT_MS = 8000;
const QUESTION_RENDER_STABLE_MS = 160;
let autoAnswerRunning = false;
if (document.readyState === 'complete') {
createControlPanel(remainingDays);
if (window.location.href.match(/group\/\d+\/exams/)) {
injectExamListStats();
}
autoCollectOnReview();
} else {
window.addEventListener('load', function() {
createControlPanel(remainingDays);
if (window.location.href.match(/group\/\d+\/exams/)) {
injectExamListStats();
}
autoCollectOnReview();
});
}
function getCurrentGroupId() {
const match = window.location.href.match(/group\/(\d+)/);
return match ? String(match[1]) : '';
}
function normalizeExamNameForMatch(name) {
const holder = document.createElement('div');
holder.innerHTML = String(name || '');
let value = holder.textContent || holder.innerText || '';
try { value = value.normalize('NFKC'); } catch (e) {}
return value
.replace(/\u00a0/g, ' ')
.replace(/[“”"'‘’《》〈〉()()\[\]【】]/g, '')
.replace(/[::,,。!?!?、;;\-—_]/g, '')
.replace(/\s+/g, '')
.toLowerCase();
}
function buildExamNameCore(name) {
return normalizeExamNameForMatch(name)
.replace(/模拟卷|模拟题|模拟|练习卷|练习题|练习|正式答卷|正式考试|答卷|考试|试卷|测试题|测试|题库|题目/g, '')
.replace(/课程/g, '')
.trim();
}
function scoreExamNameSimilarity(targetName, candidateName) {
const target = buildExamNameCore(targetName);
const candidate = buildExamNameCore(candidateName);
if (!target || !candidate) return 0;
if (target === candidate) return 100;
return target.includes(candidate) || candidate.includes(target) ? 90 : 0;
}
function getStoredExamCatalog() {
try {
const catalog = JSON.parse(localStorage.getItem(EXAM_CATALOG_KEY) || '{}');
return catalog && typeof catalog === 'object' ? catalog : {};
} catch (e) {
return {};
}
}
function rememberExamName(examId, name, groupId) {
if (examId == null || !String(name || '').trim()) return;
const catalog = getStoredExamCatalog();
catalog[String(examId)] = {
examId: String(examId),
name: String(name).trim(),
groupId: String(groupId || getCurrentGroupId() || '')
};
localStorage.setItem(EXAM_CATALOG_KEY, JSON.stringify(catalog));
}
function fetchExamCatalog(callback) {
const groupId = getCurrentGroupId();
const catalog = getStoredExamCatalog();
let finished = false;
function finish() {
if (finished) return;
finished = true;
callback(catalog);
}
if (!groupId || !userToken) {
finish();
return;
}
const url = `https://exambackend.yooc.me/api/exam/list/get?userId=${encodeURIComponent(userId || '')}&token=${encodeURIComponent(userToken)}&yibanId=${encodeURIComponent(yibanId || '')}&groupId=${encodeURIComponent(groupId)}`;
try {
GM_xmlhttpRequest({
method: 'GET',
url: url,
timeout: 15000,
headers: { 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' },
onload: function(response) {
if (finished) return;
if (response.status >= 200 && response.status < 300) {
try {
const payload = JSON.parse(response.responseText);
(Array.isArray(payload.data) ? payload.data : []).forEach(function(item) {
if (item && item.examId != null && item.name) {
catalog[String(item.examId)] = { examId: String(item.examId), name: String(item.name).trim(), groupId: groupId };
}
});
localStorage.setItem(EXAM_CATALOG_KEY, JSON.stringify(catalog));
} catch (e) {
console.warn('[考试助手] 考试目录解析失败,继续使用本地目录。', e);
}
}
finish();
},
onerror: finish,
ontimeout: finish,
onabort: finish
});
} catch (error) {
console.warn('[考试助手] 考试目录请求失败,继续使用本地目录:', error.message);
finish();
}
}
function selectRelatedBankTargets(examId, catalog) {
const currentId = String(examId);
const current = catalog[currentId];
const groupId = current && current.groupId ? String(current.groupId) : getCurrentGroupId();
const candidates = Object.keys(catalog).map(function(id) { return catalog[id]; }).filter(function(item) {
return item && /^\d+$/.test(String(item.examId)) && String(item.examId) !== currentId &&
groupId && String(item.groupId || '') === groupId;
}).sort(function(first, second) {
const name = current && current.name ? current.name : '';
return scoreExamNameSimilarity(name, second.name) - scoreExamNameSimilarity(name, first.name) ||
String(first.examId).localeCompare(String(second.examId), undefined, { numeric: true });
});
return Array.from(new Set([currentId].concat(candidates.map(function(item) { return String(item.examId); })))).slice(0, 6);
}
async function downloadRelatedQuestionBanks(examId, callback) {
const sessionKey = String(examId);
let task = relatedDownloadSession.get(sessionKey);
if (!task) {
task = (async function() {
await downloadQuestionBank(sessionKey);
const catalog = await new Promise(function(resolve) { fetchExamCatalog(resolve); });
const targets = selectRelatedBankTargets(sessionKey, catalog);
console.log('[考试助手] 当前考试已优先尝试下载,继续下载 ' + (targets.length - 1) + ' 份相关题库。');
for (const target of targets.slice(1)) await downloadQuestionBank(target);
})();
relatedDownloadSession.set(sessionKey, task);
}
try {
await task;
} catch (error) {
console.error('[考试助手] 相关题库下载未完成,将使用已有本地题库,可再次点击重试:', error);
} finally {
if (relatedDownloadSession.get(sessionKey) === task) relatedDownloadSession.delete(sessionKey);
if (callback) callback();
}
}
function isLicenseValid() {
var expiresAt = GM_getValue('license_expires_at', '');
if (!expiresAt) return false;
return new Date(expiresAt).getTime() > Date.now();
}
function checkLicenseWithServer(key, cb) {
GM_xmlhttpRequest({
method: 'POST',
url: _0xda + '/check',
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({ key: key }),
onload: function(resp) {
try {
var result = JSON.parse(resp.responseText);
cb(result);
} catch(e) {
cb({ valid: true });
}
},
onerror: function() {
cb({ valid: true });
}
});
}
function withLicenseCheck(fn, btnEl) {
return function() {
if (!isLicenseValid()) {
_0xm('expired');
return;
}
var origText = btnEl ? btnEl.textContent : '';
if (btnEl) { btnEl.disabled = true; btnEl.textContent = '验证中...'; }
var key = GM_getValue('license_key', '');
checkLicenseWithServer(key, function(result) {
if (btnEl) { btnEl.disabled = false; btnEl.textContent = origText; }
if (!result.valid) {
GM_deleteValue('license_expires_at');
GM_deleteValue('license_token');
GM_deleteValue('license_verified_at');
_0xm('error', result.reason);
return;
}
fn();
});
};
}
function createControlPanel(remainingDays) {
GM_addStyle(`
#helper-panel {
position: fixed;
top: 50%;
left: 20px;
transform: translateY(-50%);
background-color: #fff;
border: 1px solid #e5e7eb;
border-radius: 14px;
padding: 16px;
box-shadow: 0 8px 28px rgba(15,23,42,0.12);
z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
box-sizing: border-box;
width: 216px;
max-width: calc(100vw - 40px);
max-height: calc(100vh - 32px);
overflow-y: auto;
overflow-wrap: anywhere;
color: #334155;
}
#helper-panel .hp-title {
margin: 0;
font-size: 15px;
font-weight: 600;
color: #1e293b;
text-align: center;
line-height: 1.4;
}
#helper-panel .hp-status {
margin: 6px 0 14px;
font-size: 11px;
color: #059669;
text-align: center;
line-height: 1.6;
}
#helper-panel .hp-status.expired { color: #dc2626; }
#helper-panel button {
display: block;
width: 100%;
box-sizing: border-box;
padding: 9px 10px;
margin: 0;
border: 1px solid #e2e8f0;
border-radius: 8px;
background-color: #f8fafc;
color: #334155;
font-size: 12px;
font-weight: 500;
line-height: 1.5;
cursor: pointer;
transition: background-color 0.2s, border-color 0.2s;
}
#helper-panel button:hover { background-color: #eff6ff; border-color: #bfdbfe; }
#helper-panel button:active { background-color: #dbeafe; }
#helper-panel button:disabled { opacity: 0.55; cursor: not-allowed; }
#helper-panel .hp-actions { display: grid; gap: 7px; }
#helper-panel button.hp-primary { background-color: #2563eb; border-color: #2563eb; color: #fff; font-weight: 600; }
#helper-panel button.hp-primary:hover { background-color: #1d4ed8; border-color: #1d4ed8; }
#helper-panel .hp-settings { margin-top: 14px; padding-top: 12px; border-top: 1px solid #eef2f6; }
#helper-panel .hp-mode-label { display: block; margin-bottom: 6px; font-size: 11px; color: #64748b; }
#helper-panel select { box-sizing: border-box; width: 100%; min-width: 0; padding: 7px 8px; border: 1px solid #dbe2ea; border-radius: 7px; background: #fff; color: #334155; font: inherit; font-size: 12px; }
#helper-panel .hp-related-label { display: flex; align-items: flex-start; gap: 7px; margin-top: 12px; font-size: 11px; line-height: 1.6; color: #475569; cursor: pointer; }
#helper-panel .hp-related-label input { flex: 0 0 auto; width: 14px; height: 14px; margin: 2px 0 0; accent-color: #2563eb; }
#helper-panel .hp-related-hint { margin: 5px 0 0 21px; font-size: 10px; line-height: 1.65; color: #94a3b8; }
#helper-panel button:focus-visible, #helper-panel select:focus-visible, #helper-panel input:focus-visible { outline: 2px solid #93c5fd; outline-offset: 2px; }
#hp-update-notify {
position: fixed;
top: calc(50% - 180px);
left: 20px;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
border-radius: 10px;
padding: 10px 14px;
box-shadow: 0 4px 12px rgba(99,102,241,0.3);
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
min-width: 140px;
box-sizing: border-box;
width: 216px;
max-width: calc(100vw - 40px);
color: #fff;
display: none;
}
#hp-update-notify .hp-un-title {
margin: 0 0 6px;
font-size: 12px;
font-weight: 600;
text-align: center;
line-height: 1.4;
}
#hp-update-notify .hp-un-versions {
font-size: 11px;
text-align: center;
line-height: 1.6;
opacity: 0.95;
margin-bottom: 8px;
}
#hp-update-notify .hp-un-btn {
display: block;
width: 100%;
padding: 6px 10px;
border: none;
border-radius: 6px;
background-color: #fff;
color: #6366f1;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
}
#hp-update-notify .hp-un-btn:hover { background-color: #f0f0f0; }
#hp-update-notify .hp-un-close {
position: absolute;
top: 6px;
right: 8px;
background: none;
border: none;
color: rgba(255,255,255,0.6);
font-size: 14px;
cursor: pointer;
padding: 0;
line-height: 1;
}
#hp-update-notify .hp-un-close:hover { color: #fff; }
`);
const panel = document.createElement('div');
panel.id = 'helper-panel';
panel.innerHTML = '考试助手
已激活
剩' + _0xfmt(remainingDays) + '
';
const collectButton = document.createElement('button');
collectButton.innerText = '更新题库';
collectButton.addEventListener('click', withLicenseCheck(runReviewMode, collectButton));
const examButton = document.createElement('button');
examButton.innerText = '自动答题';
examButton.addEventListener('click', withLicenseCheck(runExamMode, examButton));
examButton.className = 'hp-primary';
const modeLabel = document.createElement('label');
modeLabel.textContent = '匹配模式';
modeLabel.className = 'hp-mode-label';
modeLabel.htmlFor = 'answer-match-mode';
const modeSelect = document.createElement('select');
modeSelect.id = 'answer-match-mode';
modeSelect.innerHTML =
'' +
'' +
'';
modeSelect.value = getAnswerMatchMode();
modeSelect.addEventListener('change', function() {
GM_setValue('answer_match_mode', modeSelect.value);
console.log('[考试助手] 已切换题库匹配模式:' + modeSelect.value);
});
const relatedBankLabel = document.createElement('label');
relatedBankLabel.className = 'hp-related-label';
const relatedBankSwitch = document.createElement('input');
relatedBankSwitch.type = 'checkbox';
relatedBankSwitch.checked = GM_getValue(RELATED_BANK_MATCH_KEY, false);
relatedBankSwitch.addEventListener('change', function() {
GM_setValue(RELATED_BANK_MATCH_KEY, relatedBankSwitch.checked);
console.log('[考试助手] 相关题库下载已' + (relatedBankSwitch.checked ? '开启' : '关闭') + '。');
});
relatedBankLabel.appendChild(relatedBankSwitch);
relatedBankLabel.appendChild(document.createTextNode('自动下载相关题库'));
const relatedBankHint = document.createElement('p');
relatedBankHint.textContent = '自动查找同班级题库,无需填写 ID。仅控制下载,不影响本地匹配。';
relatedBankHint.title = '名称相近的考试优先下载;是否同题仍通过题干和选项严格确认。';
relatedBankHint.className = 'hp-related-hint';
const importButton = document.createElement('button');
importButton.innerText = '导入题库';
importButton.addEventListener('click', importQuestionBank);
const forceviewButton = document.createElement('button');
forceviewButton.innerText = '强制看题';
forceviewButton.addEventListener('click', forcevieExam);
const licenseButton = document.createElement('button');
licenseButton.innerText = '验证卡密';
licenseButton.addEventListener('click', function() { _0xm('enter'); });
const actions = document.createElement('div');
actions.className = 'hp-actions';
[examButton, collectButton, importButton, forceviewButton, licenseButton].forEach(function(button) { actions.appendChild(button); });
panel.appendChild(actions);
const settings = document.createElement('div');
settings.className = 'hp-settings';
settings.appendChild(modeLabel);
settings.appendChild(modeSelect);
settings.appendChild(relatedBankLabel);
settings.appendChild(relatedBankHint);
const aiStatus = document.createElement('p');
aiStatus.id = 'hp-ai-status';
aiStatus.className = 'hp-related-hint';
aiStatus.textContent = 'AI 由服务器统一提供,仅处理未命中题目,不写入题库。';
aiStatus.setAttribute('role', 'status');
settings.appendChild(aiStatus);
const aiLabel = document.createElement('label');
aiLabel.className = 'hp-related-label';
const aiSwitch = document.createElement('input');
aiSwitch.id = 'hp-ai-enabled';
aiSwitch.type = 'checkbox';
aiSwitch.checked = getAiConfig().enabled;
aiSwitch.addEventListener('change', function() {
GM_setValue('ai_fallback_enabled', aiSwitch.checked);
});
aiLabel.appendChild(aiSwitch);
aiLabel.appendChild(document.createTextNode('未命中时使用 AI'));
settings.appendChild(aiLabel);
panel.appendChild(settings);
document.body.appendChild(panel);
var updateNotify = document.createElement('div');
updateNotify.id = 'hp-update-notify';
updateNotify.innerHTML =
'' +
'有新版本可用
' +
'' +
'';
document.body.appendChild(updateNotify);
var closeNotify = document.getElementById('hp-un-close');
if (closeNotify) closeNotify.addEventListener('click', function() { updateNotify.style.display = 'none'; });
var updateBtn = document.getElementById('hp-un-btn');
if (updateBtn) updateBtn.addEventListener('click', function() {
window.open('http://38.76.215.191/YOOC-Exam-Helper-Online.user.js', '_blank');
});
checkForUpdate();
checkCertStatus();
console.log("操作面板已注入。");
}
function checkForUpdate() {
var currentVersion = SCRIPT_VERSION;
GM_xmlhttpRequest({
method: 'GET',
url: 'http://38.76.215.191/script/version.json?t=' + Date.now(),
onload: function(resp) {
try {
var data = JSON.parse(resp.responseText);
var latestVersion = data.version;
if (latestVersion && latestVersion !== currentVersion) {
var versionsEl = document.getElementById('hp-un-versions');
if (versionsEl) {
versionsEl.innerHTML = '当前版本 v' + currentVersion + '
最新版本 v' + latestVersion;
}
var notify = document.getElementById('hp-update-notify');
if (notify) notify.style.display = 'block';
console.log('[考试助手] 检测到新版本 v' + latestVersion + ',当前版本 v' + currentVersion);
}
} catch(e) {}
},
onerror: function() {}
});
}
function checkCertStatus() {
window.__certOk = false;
GM_xmlhttpRequest({
method: 'HEAD',
url: 'http://38.76.215.191/YOOC-Exam-Helper-Online.user.js',
timeout: 5000,
onload: function(resp) {
window.__certOk = true;
console.log('[考试助手] SSL 证书已信任');
},
onerror: function() {
window.__certOk = false;
console.log('[考试助手] SSL 证书未信任,点击更新将跳转到安装引导页');
},
ontimeout: function() {
window.__certOk = false;
console.log('[考试助手] SSL 连接超时,证书可能未信任');
}
});
}
function injectExamListStats() {
setTimeout(() => {
var root = document.getElementById('root');
if (!root) { console.log('[助手] 未找到 #root'); return; }
var reactKey = Object.keys(root).find(k => k.startsWith('__reactContainer') || k.startsWith('__reactFiber'));
if (!reactKey) { console.log('[助手] 未找到 React fiber key'); return; }
var exams = [];
function walk(fiber, depth) {
if (!fiber || depth > 80) return;
if (fiber.memoizedProps && fiber.memoizedProps.exam) {
var exam = fiber.memoizedProps.exam;
if (!exams.find(function(e) { return e.examId === exam.examId; })) {
exams.push(exam);
}
}
if (fiber.child) walk(fiber.child, depth + 1);
if (fiber.sibling) walk(fiber.sibling, depth + 1);
}
walk(root[reactKey], 0);
console.log('[助手] 找到考试数量:', exams.length);
exams.forEach(function(exam) {
rememberExamName(exam.examId, exam.name, getCurrentGroupId());
fetchQuestionCount(exam);
});
setTimeout(function() {
injectExamListStatsByDOM();
}, 1000);
}, 2000);
}
function injectExamListStatsByDOM() {
var articles = document.querySelectorAll('article');
articles.forEach(function(article) {
if (article.querySelector('.exam-helper-stats')) return;
var h3 = article.querySelector('h3');
if (!h3) return;
var name = h3.innerText.trim();
if (!name) return;
var examId = null;
var links = article.querySelectorAll('a[href*="/exam/"]');
for (var i = 0; i < links.length; i++) {
var m = links[i].href.match(/exam\/(\d+)/);
if (m) { examId = m[1]; break; }
}
if (!examId) {
var examIdMatch = article.innerHTML.match(/exam\/(\d+)/);
if (examIdMatch) examId = examIdMatch[1];
}
if (!examId) return;
rememberExamName(examId, name, getCurrentGroupId());
GM_xmlhttpRequest({
method: "GET",
url: `${SERVER_BASE}/question-bank/count?examId=${examId}`,
onload: function(response) {
var count = 0;
if (response.status >= 200 && response.status < 300) {
try { count = JSON.parse(response.responseText).count || 0; } catch(e) {}
}
var statsEl = document.createElement('span');
statsEl.className = 'exam-helper-stats';
statsEl.style.cssText = 'font-size:12px;color:#4338CA;margin-left:10px;font-weight:normal;';
statsEl.innerHTML = `(题库 ${count} 题)`;
h3.parentElement.appendChild(statsEl);
},
onerror: function() {}
});
});
}
function fetchQuestionCount(exam) {
GM_xmlhttpRequest({
method: "GET",
url: `${SERVER_BASE}/question-bank/count?examId=${exam.examId}`,
onload: function(response) {
let count = 0;
if (response.status >= 200 && response.status < 300) {
try {
const result = JSON.parse(response.responseText);
count = result.count || 0;
} catch(e) {}
}
injectStatsToCard(exam, count);
},
onerror: function() {
injectStatsToCard(exam, 0);
}
});
}
function injectStatsToCard(exam, serverCount) {
const articles = document.querySelectorAll('article');
for (const article of articles) {
if (article.querySelector('.exam-helper-stats')) continue;
const h3 = article.querySelector('h3');
if (h3 && h3.innerText.includes(exam.name)) {
const statsEl = document.createElement('span');
statsEl.className = 'exam-helper-stats';
statsEl.style.cssText = 'font-size:12px;color:#4338CA;margin-left:10px;font-weight:normal;';
statsEl.innerHTML = `(题库 ${serverCount} 题)`;
h3.parentElement.appendChild(statsEl);
break;
}
}
}
function extractAnswerSubjects(payload) {
if (!payload) return [];
if (Array.isArray(payload)) {
if (payload.every(function(item) { return item && item.subjectId != null; })) return payload;
return payload.reduce(function(all, item) {
return all.concat(extractAnswerSubjects(item));
}, []);
}
if (Array.isArray(payload.subjects)) return extractAnswerSubjects(payload.subjects);
if (payload.data != null) return extractAnswerSubjects(payload.data);
return payload.subjectId != null ? [payload] : [];
}
function autoCollectOnReview() {
var currentUrl = window.location.href;
var reviewMatch = currentUrl.match(/group\/(\d+)\/exam\/(\d+)\/review\/(\d+)/);
if (!reviewMatch) return;
var examId = reviewMatch[2];
var examuserId = reviewMatch[3];
if (!userToken || !examuserId) return;
console.log('[考试助手] 回顾页检测到,自动收集题库...');
GM_xmlhttpRequest({
method: "GET",
url: `https://exambackend.yooc.me/api/exam/answer/get?examuserId=${examuserId}&token=${userToken}&yibanId=${yibanId}`,
headers: { "Cache-Control": "no-cache", "Pragma": "no-cache" },
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
var data = JSON.parse(response.responseText);
handleApiResponseQuiet(data, examId);
}
}
});
}
function handleApiResponseQuiet(data, examId) {
var oldQuestionBank = readQuestionBank();
if (data && data.result === false) return;
if (!oldQuestionBank[examId]) oldQuestionBank[examId] = {};
var question_count = 0;
var subjects = extractAnswerSubjects(data);
if (subjects.length === 0) {
console.warn('[考试助手] 接口响应中没有可用题目 subject。', data);
return;
}
for (var i = 0; i < subjects.length; i++) {
var sub = subjects[i];
var subjectId = String(sub.subjectId);
try {
var record = makeStoredQuestionRecord(examId, subjectId, sub);
if (!oldQuestionBank[examId][subjectId]) question_count++;
oldQuestionBank[examId][subjectId] = record;
} catch (recordError) {
console.warn('[考试助手] 跳过无效题目:', subjectId, recordError.message);
}
}
localStorage.setItem(QUESTION_BANK_STORAGE_KEY, JSON.stringify(oldQuestionBank));
var totalCount = Object.keys(oldQuestionBank[examId]).length;
console.log('[考试助手] 自动收集完成,新增/更新 ' + question_count + ' 题。当前考试题库共 ' + totalCount + ' 题。');
uploadQuestionBank(examId);
var toast = document.createElement('div');
toast.textContent = '题库已自动更新 +' + question_count + ' 题';
toast.style.cssText = 'position:fixed;top:20px;right:20px;background:#059669;color:#fff;padding:10px 20px;border-radius:8px;z-index:99999;font-size:14px;box-shadow:0 4px 12px rgba(0,0,0,0.2);transition:opacity 0.5s;';
document.body.appendChild(toast);
setTimeout(function() { toast.style.opacity = '0'; }, 2000);
setTimeout(function() { toast.remove(); }, 2500);
}
function forcevieExam(){
const currentUrl = window.location.href;
const reviewMatch = currentUrl.match(/group\/(\d+)\/exam\/(\d+)\/review\/(\d+)/);
if (reviewMatch) {
alert("当前已在回顾页,可直接点击「更新题库」收集答案。");
return;
}
const resultMatch = currentUrl.match(/group\/(\d+)\/exam\/(\d+)/);
if (!resultMatch) {
alert("请先打开一场考试的结果页或回顾页,再点击「强制看题」。");
return;
}
const groupId = resultMatch[1];
const examId = resultMatch[2];
console.log("考试ID:", examId);
GM_xmlhttpRequest({
method: "GET",
url: `https://exambackend.yooc.me/api/exam/result/get?userId=${userId}&token=${userToken}&yibanId=${yibanId}&examId=${examId}`,
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
const responseData = JSON.parse(response.responseText);
if(responseData.result){
const examuserId = responseData.data.examuserId;
location.href = `https://exam.yooc.me/group/${groupId}/exam/${examId}/review/${examuserId}`
}
else{
alert("未能成功获取 examuserId,可能是还未参加过这场考试。");
}
}
else{
alert("接口请求失败: " + response.status);
console.error("接口请求失败:", response.status, response.statusText, response.responseText);
}
}
})
}
function importQuestionBank() {
importJson(function(error, data) {
if (error) {
console.error("导入题库失败:", error);
alert("导入失败: " + error.message);
return;
}
let mergedQuestionBank;
try {
mergedQuestionBank = mergeQuestionBanks(readQuestionBank(), data);
localStorage.setItem(QUESTION_BANK_STORAGE_KEY, JSON.stringify(mergedQuestionBank));
} catch (importError) {
console.error('导入题库失败:', importError);
alert('导入失败: ' + importError.message);
return;
}
const message = `题库导入成功!\n导入题库数: ${Object.keys(data).length}\n题库总数: ${Object.keys(mergedQuestionBank).length}`;
console.log(message.replace(/\n/g, ' '));
alert(message);
});
}
function getExamNameForUpload(examId) {
const catalog = getStoredExamCatalog();
const item = catalog[String(examId)];
return item && item.name ? String(item.name).trim() : '';
}
function uploadQuestionBank(examId) {
const questionBank = readQuestionBank();
const examData = questionBank[examId];
if (!examData || Object.keys(examData).length === 0) {
console.log("该考试题库为空,跳过上传。");
return;
}
GM_xmlhttpRequest({
method: "POST",
url: `${SERVER_BASE}/question-bank/upload`,
headers: { "Content-Type": "application/json" },
data: JSON.stringify({
examId: examId,
examName: getExamNameForUpload(examId),
questionBank: examData
}),
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
const result = JSON.parse(response.responseText);
console.log(`题库上传成功:新增 ${result.added || 0} 题,更新 ${result.updated || 0} 题,当前共 ${result.count || 0} 题。`);
} else {
console.error("题库上传失败:", response.status, response.responseText);
}
},
onerror: function(error) {
console.error("题库上传请求出错:", error);
}
});
}
function downloadQuestionBank(examId, callback) {
const requestKey = String(examId);
let task = questionBankDownloads.get(requestKey);
if (!task) {
task = new Promise(function(resolve) {
let finished = false;
function finish(success) {
if (finished) return;
finished = true;
resolve(success);
}
function fail(reason) {
console.warn('[考试助手] 考试 ' + requestKey + ' 题库下载失败(' + reason + '),继续使用本地题库,下次可重试。');
finish(false);
}
try {
GM_xmlhttpRequest({
method: 'GET',
url: `${SERVER_BASE}/question-bank/download?examId=${encodeURIComponent(requestKey)}`,
timeout: 15000,
onload: function(response) {
if (finished) return;
if (response.status < 200 || response.status >= 300) {
fail('HTTP ' + response.status);
return;
}
try {
const result = JSON.parse(response.responseText);
if (!result.success || !result.questionBank || Object.keys(result.questionBank).length === 0) {
fail('未找到可用题库');
return;
}
const questionBank = mergeQuestionBanks(readQuestionBank(), { [requestKey]: result.questionBank });
localStorage.setItem(QUESTION_BANK_STORAGE_KEY, JSON.stringify(questionBank));
console.log('[考试助手] 考试 ' + requestKey + ' 题库下载成功,服务器题目数: ' + Object.keys(result.questionBank).length);
finish(true);
} catch (error) {
fail('数据无效或保存失败: ' + error.message);
}
},
onerror: function() { fail('网络错误'); },
ontimeout: function() { fail('请求超时'); },
onabort: function() { fail('请求取消'); }
});
} catch (error) {
fail(error.message);
}
});
questionBankDownloads.set(requestKey, task);
task.then(function() {
if (questionBankDownloads.get(requestKey) === task) questionBankDownloads.delete(requestKey);
});
}
return task.then(function(success) {
if (callback) callback(success);
return success;
});
}
function importJson(callback) {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.style.display = 'none';
document.body.appendChild(input);
input.addEventListener('change', function(event) {
const file = event.target.files[0];
if (!file) {
callback(new Error('未选择文件'));
return;
}
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = JSON.parse(e.target.result);
callback(null, data);
} catch (error) {
callback(error);
}
};
reader.onerror = function() {
callback(new Error('文件读取失败'));
};
reader.readAsText(file);
});
input.click();
document.body.removeChild(input);
}
function runExamMode() {
try {
const currentUrl = window.location.href;
if (currentUrl.includes('/review/')) {
runReviewMode();
return;
}
const examIdMatch = currentUrl.match(/exam\/(\d+)/);
const examId = examIdMatch ? examIdMatch[1] : null;
console.log("考试ID:", examId);
if (!examId) {
alert("未能从URL中解析出 examId");
return;
}
console.log("正在从服务器下载题库...");
const downloadBanks = GM_getValue(RELATED_BANK_MATCH_KEY, false) ? downloadRelatedQuestionBanks : function(id, callback) {
downloadQuestionBank(id, callback);
};
downloadBanks(examId, function() {
console.log("题库下载尝试结束,使用已有题库开始自动答题...");
GM_xmlhttpRequest({
method: "GET",
url: `https://exambackend.yooc.me/api/exam/setting/get?examId=${examId}&userId=${userId}&token=${userToken}&yibanId=${yibanId}`,
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
const responseData = JSON.parse(response.responseText);
if(responseData.result){
const examuserId = responseData.data.examuserId;
if (Object.hasOwn(localStorage, `exam-paper-${examuserId}`)){
handleApiResponse2write(JSON.parse(localStorage.getItem(`exam-paper-${examuserId}`)).value.paper);
}
else{
GM_xmlhttpRequest({
method: "GET",
url: `https://exambackend.yooc.me/api/exam/paper/get?examuserId=${examuserId}&token=${userToken}&yibanId=${yibanId}`,
headers: { "Cache-Control": "no-cache", "Pragma": "no-cache" },
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
const responseData = JSON.parse(response.responseText);
if (responseData.result){
handleApiResponse2write(responseData.data);
} else {
alert("题目获取失败!");
}
} else {
console.error("接口请求失败:", response.status);
}
},
onerror: function(error) { console.error(error); }
});
}
} else {
alert("获取 examuserId 失败");
}
} else {
console.error("接口请求失败:", response.status);
}
},
onerror: function(error) { console.error(error); }
});
});
} catch (error) {
console.error("自动答题脚本出错:", error);
}
}
function runReviewMode() {
try {
const currentUrl = window.location.href;
const examIdMatch = currentUrl.match(/exam\/(\d+)/);
const examId = examIdMatch ? examIdMatch[1] : null;
console.log("考试ID:", examId);
if (!examId) {
console.error("未能从URL中解析出 examId,无法执行操作。");
alert("未能从URL中解析出 examId,无法执行操作。");
return;
}
const examuserIdMatch = currentUrl.match(/review\/(\d+)$/);
const examuserId = examuserIdMatch ? examuserIdMatch[1] : null;
console.log("考试用户ID:", examuserId);
const apiUrl = `https://exambackend.yooc.me/api/exam/answer/get?examuserId=${examuserId}&token=${userToken}&yibanId=${yibanId}`;
if (!apiUrl || !examId) {
console.error("未能构造有效的接口URL,请检查脚本。");
return;
}
console.log("正在请求接口:", apiUrl);
GM_xmlhttpRequest({
method: "GET",
url: apiUrl,
headers: {
"Cache-Control": "no-cache",
"Pragma": "no-cache"
},
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
const responseData = JSON.parse(response.responseText);
handleApiResponse(responseData, examId);
} else {
console.error("接口请求失败:", response.status, response.statusText, response.responseText);
}
},
onerror: function(error) {
console.error("接口请求发生错误:", error);
}
});
} catch (error) {
console.error("收集题库脚本出错:", error);
}
}
function normText(t){
var ta = document.createElement('textarea');
ta.innerHTML = String(t || '');
return ta.value.replace(/ /g,' ').replace(/\t/g,' ').replace(/\s+/g,' ').trim();
}
function normOptionText(t){
return normText(t)
.replace(/^[A-ZA-Z]\s*[..、::]\s*/, '')
.replace(/^[((]\s*[A-ZA-Z]\s*[))]\s*/i, '')
.trim();
}
function cleanOptions(options) {
if (!Array.isArray(options)) return [];
var result = [];
for (var idx = 0; idx < options.length; idx++) {
var opt = options[idx];
var text = typeof opt === 'string' ? opt : (Array.isArray(opt) ? opt[0] : String(opt));
var ta = document.createElement('textarea');
ta.innerHTML = text;
text = ta.value;
result.push(text.replace(/\t/g, ' ').replace(/\s+/g, ' ').trim());
}
return result;
}
function getAnswerMatchMode() {
var mode = GM_getValue('answer_match_mode', 'hybrid');
return ['id', 'text', 'hybrid'].indexOf(mode) >= 0 ? mode : 'hybrid';
}
function normalizeMatchText(text) {
var holder = document.createElement('div');
holder.innerHTML = String(text || '');
var value = holder.textContent || holder.innerText || '';
try { value = value.normalize('NFKC'); } catch (e) {}
return value
.replace(/\u00a0/g, ' ')
.replace(/\[x\]/gi, ' {{blank}} ')
.replace(/_{2,}|_{2,}/g, ' {{blank}} ')
.replace(/\s+/g, ' ')
.trim();
}
function normalizeTitleForMatch(text) {
return normalizeMatchText(text)
.replace(/^\s*[一二三四五六七八九十]+\s*[、..)]\s*/, '')
.replace(/^\s*\d+\s*[、..)]\s*/, '')
.replace(/\[\s*\d+(?:\.\d+)?\s*分\s*\]/g, '')
.replace(/\{\s*input\s*\}|\{\{\s*blank(?::\d+)?\s*\}\}/gi, ' {{blank}} ')
.replace(/\s+/g, ' ')
.trim();
}
function normalizeOptionForMatch(text) {
return normalizeMatchText(text)
.replace(/^[a-zA-Z]\s*[..、::]\s*/i, '')
.replace(/^[((]\s*[a-zA-Z]\s*[))]\s*/i, '')
.trim();
}
function getOptionSignature(optionTexts) {
return JSON.stringify((optionTexts || []).slice().sort());
}
function getBlankCandidates(value) {
return Array.isArray(value) ? value.slice() : [value];
}
function getBlankAnswerSignature(values) {
return JSON.stringify(values.map(function(value) {
return Array.from(new Set(getBlankCandidates(value))).sort();
}));
}
function getSimpleNumericValue(value) {
var text = value.trim().normalize('NFKC');
if (/^[+-]?\d+(?:\.\d+)?$/.test(text)) return Number(text);
var digits = { '零': 0, '〇': 0, '一': 1, '二': 2, '两': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9 };
if (Object.hasOwn(digits, text)) return digits[text];
var tens = text.match(/^([一二两三四五六七八九])?十([一二三四五六七八九])?$/);
return tens ? (tens[1] ? digits[tens[1]] : 1) * 10 + (tens[2] ? digits[tens[2]] : 0) : null;
}
function getBlankFillValues(values) {
var answers = [];
for (var value of values) {
var candidates = getBlankCandidates(value);
var numbers = candidates.map(getSimpleNumericValue).filter(function(number) { return number !== null; });
if (new Set(numbers).size > 1) return null;
answers.push(candidates[0]);
}
return answers;
}
function validateStoredQuestion(raw) {
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('题目必须是对象');
var fields = ['title', 'option', 'answer', 'type'];
if (Object.keys(raw).some(function(key) { return !fields.includes(key); }) ||
!fields.every(function(key) { return Object.hasOwn(raw, key); })) {
throw new Error('题目只接受 title、option、answer、type 四个字段,请先转换旧题库');
}
if (typeof raw.title !== 'string' || !raw.title.trim() || typeof raw.type !== 'string' ||
!Array.isArray(raw.option) || !raw.option.every(function(text) { return typeof text === 'string' && text.trim(); }) ||
!Array.isArray(raw.answer) || raw.answer.length === 0) {
throw new Error('题干、题型、选项或答案格式无效');
}
if (raw.option.length > 0) {
if (!raw.answer.every(function(index) { return Number.isInteger(index) && index >= 0 && index < raw.option.length; }) ||
new Set(raw.answer).size !== raw.answer.length) throw new Error('选择题答案必须是有效且不重复的零基数字索引');
} else if (!raw.answer.every(function(value) {
var candidates = getBlankCandidates(value);
return candidates.length > 0 && candidates.every(function(candidate) { return typeof candidate === 'string' && candidate.trim(); });
})) {
throw new Error('每个填空答案必须是非空字符串或非空字符串候选数组');
}
return { title: raw.title, option: raw.option.slice(), answer: raw.answer.map(function(value) { return Array.isArray(value) ? value.slice() : value; }), type: raw.type };
}
function normalizeQuestionBank(data) {
if (!data || typeof data !== 'object' || Array.isArray(data)) throw new Error('题库必须按考试 ID 分组');
var result = Object.create(null);
Object.keys(data).forEach(function(examId) {
var examBank = data[examId];
if (!examBank || typeof examBank !== 'object' || Array.isArray(examBank)) throw new Error('考试题库必须按题目 ID 分组');
result[examId] = Object.create(null);
Object.keys(examBank).forEach(function(subjectId) {
try {
result[examId][subjectId] = validateStoredQuestion(examBank[subjectId]);
} catch (error) {
throw new Error('考试 ' + examId + ',题目 ' + subjectId + ': ' + error.message);
}
});
});
return result;
}
function readQuestionBank() {
return normalizeQuestionBank(JSON.parse(localStorage.getItem(QUESTION_BANK_STORAGE_KEY) || '{}'));
}
function mergeQuestionBanks(current, incoming) {
var result = normalizeQuestionBank(current);
var additions = normalizeQuestionBank(incoming);
Object.keys(additions).forEach(function(examId) {
if (!Object.hasOwn(result, examId)) result[examId] = Object.create(null);
Object.keys(additions[examId]).forEach(function(subjectId) {
result[examId][subjectId] = additions[examId][subjectId];
});
});
return result;
}
function makeQuestionRecord(examId, subjectId, raw) {
var stored = validateStoredQuestion(raw);
var options = stored.option.map(normalizeOptionForMatch);
var titleKey = normalizeTitleForMatch(stored.title);
var choice = options.length > 0;
return {
examId: String(examId),
subjectId: String(subjectId),
titleKey: titleKey,
options: options,
optionTexts: choice ? stored.answer.map(function(index) { return options[index]; }) : [],
blankValues: choice ? [] : stored.answer.slice(),
optionSignature: getOptionSignature(options),
blankCount: choice ? 0 : stored.answer.length,
titleBlankCount: (titleKey.match(/\{\{blank\}\}/g) || []).length,
type: stored.type,
hasMedia: /<(?:img|svg|canvas|video|audio|math)\b/i.test(stored.title + stored.option.join(' ')),
kind: choice ? 'choice' : 'blank'
};
}
function decodeApiAnswer(value) {
if (Array.isArray(value)) return value;
if (value && typeof value === 'object') return value;
if (value == null || value === '') return [];
try {
return JSON.parse(decrypt(String(value), yibanId));
} catch (decryptError) {
try {
return JSON.parse(String(value));
} catch (parseError) {
throw new Error('题目答案解密或解析失败');
}
}
}
function makeStoredQuestionRecord(examId, subjectId, sub) {
var options = cleanOptions(sub && sub.option);
var answer = decodeApiAnswer(sub && sub.answer);
if (!Array.isArray(answer)) throw new Error('接口答案必须是数组');
if (options.length > 0) {
var optionKeys = options.map(normalizeOptionForMatch);
answer = answer.map(function(value) {
if (Number.isInteger(value)) return value;
if (typeof value !== 'string') throw new Error('无法识别选择题答案');
var text = value.trim();
if (/^\d+$/.test(text)) return Number(text);
if (/^[A-Z]$/i.test(text)) return text.toUpperCase().charCodeAt(0) - 65;
var textKey = normalizeOptionForMatch(text);
var matches = optionKeys.reduce(function(indices, option, index) {
if (option === textKey) indices.push(index);
return indices;
}, []);
if (matches.length === 1) return matches[0];
if (matches.length > 1) throw new Error('答案文本对应多个选项');
throw new Error('答案无法映射到选项: ' + text);
});
} else {
answer = answer.map(function(value) {
function convertCandidate(candidate) {
if (typeof candidate !== 'string' && typeof candidate !== 'number') throw new Error('无法识别填空答案');
return String(candidate);
}
return Array.isArray(value) ? value.map(convertCandidate) : convertCandidate(value);
});
}
return validateStoredQuestion({
title: String(sub && (Array.isArray(sub.title) ? sub.title[0] : sub.title) || ''),
option: options,
answer: answer,
type: sub && sub.type != null ? String(sub.type) : ''
});
}
function addIndexValue(index, key, value) {
if (!key) return;
if (!index.has(key)) index.set(key, []);
index.get(key).push(value);
}
function buildQuestionTextIndexes(questionBank) {
var composite = new Map();
var title = new Map();
Object.keys(questionBank || {}).forEach(function(examId) {
var examBank = questionBank[examId] || {};
Object.keys(examBank).forEach(function(subjectId) {
var record = makeQuestionRecord(examId, subjectId, examBank[subjectId]);
if (!record.titleKey) return;
var key = JSON.stringify([record.kind, record.titleKey, record.optionSignature, record.blankCount]);
addIndexValue(composite, key, record);
addIndexValue(title, record.kind + '|' + record.titleKey, record);
});
});
return { composite: composite, title: title };
}
function readCurrentQuestionSnapshot() {
var main = document.getElementsByTagName('main')[0];
var h3 = main ? main.getElementsByTagName('h3')[0] : null;
if (!h3) return null;
var container = h3.parentElement || h3;
var clone = h3.cloneNode(true);
var cloneInputs = clone.querySelectorAll('input, textarea');
for (var ci = 0; ci < cloneInputs.length; ci++) {
cloneInputs[ci].replaceWith(document.createTextNode(' {{blank}} '));
}
var optionList = container.querySelector('ul');
var optionItems = optionList ? Array.prototype.slice.call(optionList.children).filter(function(item) {
return item.tagName && item.tagName.toLowerCase() === 'li';
}) : [];
var inputs = Array.prototype.slice.call(container.querySelectorAll('input.exam-input, input[type=text], input:not([type]), textarea'));
var optionRawTexts = optionItems.map(function(item) {
var textNode = item.querySelector('.flex-auto') || item.lastElementChild || item;
return (textNode.textContent || item.textContent || '').trim();
});
var optionTexts = optionRawTexts.map(normalizeOptionForMatch);
var heading = main.querySelector('h2');
var headingText = heading ? heading.textContent || '' : '';
var kind = inputs.length > 0 && optionItems.length === 0 ? 'blank' : 'choice';
return {
container: container,
h3: h3,
optionItems: optionItems,
optionTexts: optionTexts,
optionRawTexts: optionRawTexts,
titleText: (clone.textContent || '').trim(),
inputs: inputs,
kind: kind,
multiple: /多项|多选/.test(headingText),
hasMedia: Boolean(h3.querySelector('img, svg, canvas, video, audio, math') ||
(optionList && optionList.querySelector('img, svg, canvas, video, audio, math'))),
titleKey: normalizeTitleForMatch(clone.textContent || ''),
optionSignature: getOptionSignature(optionTexts),
blankCount: inputs.length
};
}
function mapChoiceAnswers(snapshot, record) {
if (snapshot.kind !== 'choice' || record.kind !== 'choice' ||
record.optionSignature !== snapshot.optionSignature || record.optionTexts.length === 0 ||
(!snapshot.multiple && record.optionTexts.length !== 1)) return null;
var currentOptions = snapshot.optionTexts;
if (currentOptions.some(function(text) { return !text; }) || new Set(currentOptions).size !== currentOptions.length) return null;
var mapped = record.optionTexts.map(function(text) { return currentOptions.indexOf(text); });
if (mapped.some(function(index) { return index < 0; }) || new Set(mapped).size !== mapped.length) return null;
return mapped;
}
function canApplyQuestionAnswers(snapshot, record) {
if (!snapshot || record.kind !== snapshot.kind || record.hasMedia || snapshot.hasMedia) return false;
if (snapshot.type && record.type && snapshot.type !== record.type) return false;
if (record.kind === 'choice') return mapChoiceAnswers(snapshot, record) !== null;
return snapshot.blankCount > 0 && record.blankCount === snapshot.blankCount && getBlankFillValues(record.blankValues) !== null;
}
function isQuestionRecordCompatible(snapshot, record) {
return canApplyQuestionAnswers(snapshot, record) && Boolean(record.titleKey) &&
record.titleKey === snapshot.titleKey &&
(record.kind === 'choice' || record.titleBlankCount === snapshot.blankCount);
}
function findQuestionRecord(mode, examId, subjectId, snapshot, questionBank, indexes) {
if (!snapshot) return { status: 'invalid', record: null };
if (mode !== 'text' && questionBank[examId] && Object.hasOwn(questionBank[examId], subjectId)) {
var idRecord = makeQuestionRecord(examId, subjectId, questionBank[examId][subjectId]);
if (canApplyQuestionAnswers(snapshot, idRecord)) return { status: 'matched', source: 'id', record: idRecord };
console.warn('[考试助手] ID 命中但答案无法安全填写:', subjectId);
}
if (mode === 'id') return { status: idRecord ? 'invalid' : 'not_found', record: null };
var key = JSON.stringify([snapshot.kind, snapshot.titleKey, snapshot.optionSignature, snapshot.blankCount]);
var candidates = indexes.composite.get(key) || [];
if (candidates.length === 0) candidates = indexes.title.get(snapshot.kind + '|' + snapshot.titleKey) || [];
var compatible = candidates.filter(function(record) { return isQuestionRecordCompatible(snapshot, record); });
if (compatible.length === 0) return { status: idRecord || candidates.length > 0 ? 'invalid' : 'not_found', record: null };
var answers = new Set(compatible.map(function(record) {
return record.kind === 'choice' ? JSON.stringify(record.optionTexts.slice().sort()) : getBlankAnswerSignature(record.blankValues);
}));
if (answers.size > 1) {
console.warn('[考试助手] 同题候选答案冲突,保留当前答案,不使用随机兜底:', subjectId);
return { status: 'conflict', record: null };
}
var record = compatible.find(function(item) { return item.examId === String(examId); }) || compatible[0];
return { status: 'matched', source: record.examId === String(examId) ? 'text-current-exam' : 'text-cross-exam', record: record };
}
function findNavButton(name) {
var buttons = Array.prototype.slice.call(document.querySelectorAll('button'));
return buttons.find(function(button) { return (button.textContent || '').trim() === name; }) || null;
}
function getCurrentProgress() {
var items = Array.prototype.slice.call(document.querySelectorAll('li'));
var progress = items.find(function(item) { return /^\s*\d+\s*\/\s*\d+\s*$/.test(item.textContent || ''); });
var match = progress && (progress.textContent || '').match(/(\d+)\s*\/\s*(\d+)/);
if (!match) return null;
var current = Number(match[1]);
var total = Number(match[2]);
return current >= 1 && current <= total ? { current: current, total: total } : null;
}
function sameQuestionRender(first, second) {
if (!first || !second || first.h3 !== second.h3 || first.container !== second.container) return false;
if (first.kind !== second.kind || first.titleKey !== second.titleKey || first.titleText !== second.titleText || first.multiple !== second.multiple || first.hasMedia !== second.hasMedia ||
JSON.stringify(first.optionTexts) !== JSON.stringify(second.optionTexts) || JSON.stringify(first.optionRawTexts) !== JSON.stringify(second.optionRawTexts)) return false;
return ['optionItems', 'inputs'].every(function(key) {
return first[key].length === second[key].length && first[key].every(function(node, index) { return node === second[key][index]; });
});
}
function getAiConfig() {
return {
enabled: GM_getValue('ai_fallback_enabled', false) === true,
endpoint: SERVER_BASE + '/ai/answer'
};
}
function setAiAnswerStatus(message) {
var status = document.getElementById('hp-ai-status');
if (status) status.textContent = message;
}
function makeAiQuestionPayload(snapshot) {
if (!snapshot || snapshot.hasMedia || !snapshot.titleKey) throw new Error('AI 仅支持题干完整的纯文本题目');
if (snapshot.kind === 'choice') {
if (!snapshot.optionTexts.length || snapshot.optionTexts.some(function(text) { return !text; }) ||
new Set(snapshot.optionTexts).size !== snapshot.optionTexts.length) throw new Error('当前选项为空或重复,不能安全使用 AI 答案');
} else if (snapshot.kind !== 'blank' || !Number.isInteger(snapshot.blankCount) || snapshot.blankCount < 1) {
throw new Error('当前题型或空格数量无效,不能请求 AI');
}
return {
title: snapshot.titleText || snapshot.titleKey,
option: snapshot.kind === 'choice' ? (snapshot.optionRawTexts || snapshot.optionTexts).slice() : [],
type: snapshot.kind === 'blank' ? 'completion' : snapshot.multiple ? 'multiplechoice' : 'choice',
blankCount: snapshot.kind === 'blank' ? snapshot.blankCount : 0
};
}
function parseAiAnswerResponse(data, snapshot) {
if (!data || typeof data !== 'object' || Array.isArray(data) || data.success !== true ||
Object.keys(data).length !== 2 || !Object.hasOwn(data, 'answer')) throw new Error('服务器返回的 AI 答案结构无效');
var result = data;
if (result.answer === null) return null;
if (!Array.isArray(result.answer) || result.answer.length === 0) throw new Error('AI 返回的答案为空或结构无效');
if (snapshot.kind === 'choice') {
if ((!snapshot.multiple && result.answer.length !== 1) ||
!result.answer.every(function(index) { return Number.isInteger(index) && index >= 0 && index < snapshot.optionTexts.length; }) ||
new Set(result.answer).size !== result.answer.length) throw new Error('AI 选择题答案的数量或索引无效');
return {
kind: 'choice', type: snapshot.type || '', hasMedia: false,
optionSignature: snapshot.optionSignature,
optionTexts: result.answer.map(function(index) { return snapshot.optionTexts[index]; })
};
}
if (snapshot.kind !== 'blank' || result.answer.length !== snapshot.blankCount ||
!result.answer.every(function(value) { return typeof value === 'string' && value.trim(); })) throw new Error('AI 填空答案必须与空格数量一致,每空只能返回一个非空字符串');
return { kind: 'blank', type: snapshot.type || '', hasMedia: false, blankCount: result.answer.length, blankValues: result.answer.slice() };
}
function requestAiAnswer(snapshot, assertCurrent) {
return new Promise(function(resolve, reject) {
var config = getAiConfig();
if (!config.enabled) {
reject(new Error('AI 兜底尚未开启'));
return;
}
var key = String(GM_getValue('license_key', '') || '').trim();
var fingerprint = String(GM_getValue('license_fingerprint', '') || '').trim();
var token = String(GM_getValue('license_token', '') || '').trim();
if (!key || !fingerprint || !/^[a-f0-9]{64}$/.test(token)) {
reject(new Error('当前授权信息不完整,请重新验证卡密后重试'));
return;
}
var payload;
try { payload = makeAiQuestionPayload(snapshot); assertCurrent(); } catch (error) { reject(error); return; }
var settled = false;
var request = null;
var timer = null;
var watchdog = null;
function finish(error, answer, abort) {
if (settled) return;
settled = true;
clearTimeout(timer);
clearTimeout(watchdog);
window.removeEventListener('pagehide', pagehide);
if (abort && request && typeof request.abort === 'function') {
try { request.abort(); } catch (ignored) {}
}
if (error) reject(error); else resolve(answer);
}
function pagehide() { finish(new Error('页面已离开,AI 请求已取消'), null, true); }
function checkCurrent() {
if (settled) return;
try {
if (!getAiConfig().enabled) throw new Error('AI 已关闭,当前请求已取消');
assertCurrent();
watchdog = setTimeout(checkCurrent, 250);
} catch (error) { finish(error, null, true); }
}
timer = setTimeout(function() { finish(new Error('AI 请求超过 90 秒,已停止等待,请稍后重试'), null, true); }, 90000);
watchdog = setTimeout(checkCurrent, 250);
window.addEventListener('pagehide', pagehide);
try {
request = GM_xmlhttpRequest({
method: 'POST', url: config.endpoint, timeout: 90000, anonymous: true,
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
data: JSON.stringify({
key: key, fingerprint: fingerprint, question: payload
}),
onload: function(response) {
if (settled) return;
try {
assertCurrent();
if (!getAiConfig().enabled) throw new Error('AI 已关闭,忽略返回结果');
if (response.status !== 200) {
var messages = {
401: 'AI 授权校验失败,请重新验证卡密',
403: '当前卡密不可使用 AI,请联系管理员',
429: 'AI 调用过于频繁或额度已用完,请稍后重试',
503: '服务器尚未启用 AI 或配置不完整,请联系管理员',
504: '服务器请求 AI 超时,请稍后重试'
};
throw new Error(messages[response.status] || 'AI 服务器返回 HTTP ' + response.status + ',请稍后重试或联系管理员');
}
var data;
try { data = JSON.parse(response.responseText); } catch (error) { throw new Error('AI 接口未返回有效 JSON,请检查接口服务'); }
finish(null, parseAiAnswerResponse(data, snapshot), false);
} catch (error) { finish(error, null, false); }
},
onerror: function() { finish(new Error('AI 网络请求失败,请检查网络后重试'), null, false); },
ontimeout: function() { finish(new Error('AI 请求超时,请稍后重试'), null, true); },
onabort: function() { finish(new Error('AI 请求已取消'), null, false); }
});
} catch (error) { finish(new Error('AI 请求无法发起,请检查油猴权限与网络'), null, true); }
});
}
function waitForQuestionReady(expectedProgress, previousSnapshot, expectedTotal) {
return new Promise(function(resolve, reject) {
var started = Date.now();
var stableSince = null;
var lastSnapshot = null;
function check() {
try {
var progress = getCurrentProgress();
var snapshot = readCurrentQuestionSnapshot();
if (progress && expectedTotal && progress.total !== expectedTotal) throw new Error('页面总题数与试卷数据不一致');
var ready = progress && progress.current === expectedProgress && snapshot && snapshot.h3.isConnected !== false &&
(snapshot.kind === 'blank' ? snapshot.inputs.length > 0 : snapshot.optionItems.length > 0) &&
(!previousSnapshot || !sameQuestionRender(previousSnapshot, snapshot));
if (ready) {
if (!sameQuestionRender(lastSnapshot, snapshot)) stableSince = Date.now();
lastSnapshot = snapshot;
if (Date.now() - stableSince >= QUESTION_RENDER_STABLE_MS) {
resolve(snapshot);
return;
}
} else {
stableSince = null;
lastSnapshot = null;
}
if (Date.now() - started >= QUESTION_RENDER_TIMEOUT_MS) throw new Error('等待第 ' + expectedProgress + ' 题切换超时,停止以避免答案写到其他题目');
setTimeout(check, 40);
} catch (error) {
reject(error);
}
}
check();
});
}
async function handleApiResponse2write(data) {
if (autoAnswerRunning) {
console.warn('[考试助手] 自动答题正在进行,请勿重复启动。');
return;
}
autoAnswerRunning = true;
try {
const questionBank = readQuestionBank();
const examIdMatch = window.location.href.match(/exam\/(\d+)/);
const examId = examIdMatch ? examIdMatch[1] : null;
const matchMode = getAnswerMatchMode();
const textIndexes = buildQuestionTextIndexes(questionBank);
if (!Array.isArray(data)) throw new Error('试卷数据不是题目分组数组');
const questions = data.reduce(function(items, section) {
if (!section || !Array.isArray(section.subjects)) throw new Error('试卷题目分组无效');
return items.concat(section.subjects.map(function(subject) {
if (!subject || subject.subjectId == null) throw new Error('试卷题目缺少 ID');
return { subject: subject, examId: examId || String(section.sectionId) };
}));
}, []);
if (questions.length === 0) throw new Error('试卷没有可用题目');
let progress = getCurrentProgress();
if (!progress || progress.total !== questions.length) throw new Error('页面题号或总题数与试卷数据不一致');
let snapshot = await waitForQuestionReady(progress.current, null, questions.length);
async function moveToQuestion(currentPosition, targetPosition, name) {
const before = await waitForQuestionReady(currentPosition, null, questions.length);
const currentProgress = getCurrentProgress();
if (!currentProgress || currentProgress.current !== currentPosition) throw new Error('页面题号意外变化,停止自动答题');
const button = findNavButton(name);
if (!button || button.disabled || button.getAttribute('aria-disabled') === 'true') throw new Error('没有可用的' + name + '按钮');
button.click();
return waitForQuestionReady(targetPosition, before, questions.length);
}
while (progress.current > 1) {
snapshot = await moveToQuestion(progress.current, progress.current - 1, '上一题');
progress = getCurrentProgress();
}
function setInputValue(input, value) {
var proto = input instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
setter.call(input, value == null ? '' : String(value));
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
}
function applyBlankAnswer(currentSnapshot, record) {
var answers = getBlankFillValues(record.blankValues);
if (!answers || answers.length !== currentSnapshot.inputs.length) {
console.warn('[填空题] 答案含数值冲突或空格数量不一致,保留当前答案。');
return;
}
currentSnapshot.inputs.forEach(function(input, index) { setInputValue(input, answers[index]); });
console.log('[填空题] 已按空格顺序填写:', answers);
}
function applyChoiceAnswer(currentSnapshot, record, source) {
var mapped = mapChoiceAnswers(currentSnapshot, record);
if (!mapped || mapped.some(function(index) { return !currentSnapshot.optionItems[index]; })) return false;
var desired = new Set(mapped);
if (currentSnapshot.multiple) {
currentSnapshot.optionItems.forEach(function(item, index) {
if (item.classList.contains('_c') && !desired.has(index)) item.click();
});
currentSnapshot.optionItems.forEach(function(item, index) {
if (desired.has(index) && !item.classList.contains('_c')) item.click();
});
} else {
var target = mapped[0];
if (!currentSnapshot.optionItems[target].classList.contains('_c')) currentSnapshot.optionItems[target].click();
}
console.log('[选择题] 已按' + (source || '题库') + '匹配答案。');
return true;
}
for (let questionIndex = 0; questionIndex < questions.length; questionIndex++) {
const currentProgress = getCurrentProgress();
if (!currentProgress || currentProgress.current !== questionIndex + 1 || !sameQuestionRender(snapshot, readCurrentQuestionSnapshot())) {
throw new Error('当前题号或题目内容意外变化,停止自动答题');
}
const entry = questions[questionIndex];
const subjectId = String(entry.subject.subjectId);
snapshot.type = entry.subject.type != null ? String(entry.subject.type) : '';
const matched = findQuestionRecord(matchMode, entry.examId, subjectId, snapshot, questionBank, textIndexes);
if (matched.status === 'conflict' || matched.status === 'invalid' || snapshot.hasMedia) {
console.warn('[考试助手] 题目验证失败或答案冲突,保留当前答案:', matched.status);
} else if (matched.record && matched.record.kind === 'blank' && snapshot.kind === 'blank') {
applyBlankAnswer(snapshot, matched.record);
} else if (matched.record && matched.record.kind === 'choice' && snapshot.kind === 'choice') {
if (!applyChoiceAnswer(snapshot, matched.record, matched.source)) console.warn('[考试助手] 答案映射失败,保留当前答案。');
} else if (matched.status === 'not_found' && getAiConfig().enabled) {
const aiSnapshot = snapshot;
const pageUrl = window.location.href;
const inputValues = snapshot.inputs.map(function(input) { return input.value; });
const selectedOptions = snapshot.optionItems.map(function(item) { return item.classList.contains('_c'); });
function assertAiQuestionCurrent() {
const latestProgress = getCurrentProgress();
const latest = readCurrentQuestionSnapshot();
if (!autoAnswerRunning || window.location.href !== pageUrl || !latestProgress ||
latestProgress.current !== questionIndex + 1 || latestProgress.total !== questions.length ||
!latest || latest.h3.isConnected === false || !sameQuestionRender(aiSnapshot, latest) ||
latest.inputs.some(function(input, index) { return input.value !== inputValues[index]; }) ||
latest.optionItems.some(function(item, index) { return item.classList.contains('_c') !== selectedOptions[index]; })) {
throw new Error('等待 AI 时题目或手动答案发生变化,已停止以避免覆盖');
}
}
setAiAnswerStatus('正在请求 AI:第 ' + (questionIndex + 1) + ' 题,请勿切题或修改答案。');
try {
const aiRecord = await requestAiAnswer(aiSnapshot, assertAiQuestionCurrent);
assertAiQuestionCurrent();
if (!aiRecord) {
setAiAnswerStatus('AI 无法判断第 ' + (questionIndex + 1) + ' 题,已跳过。');
} else {
if (!canApplyQuestionAnswers(aiSnapshot, aiRecord)) throw new Error('AI 答案无法安全映射到当前题目');
if (aiRecord.kind === 'blank') applyBlankAnswer(aiSnapshot, aiRecord);
else if (!applyChoiceAnswer(aiSnapshot, aiRecord, 'AI')) throw new Error('AI 选项映射失败,保留当前答案');
setAiAnswerStatus('AI 已填写第 ' + (questionIndex + 1) + ' 题,请自行核对。');
}
} catch (error) {
setAiAnswerStatus(error.message);
throw error;
}
} else {
console.warn('[考试助手] 未匹配到答案且 AI 未启用,保留当前答案,不随机选择。');
}
if (questionIndex + 1 < questions.length) snapshot = await moveToQuestion(questionIndex + 1, questionIndex + 2, '下一题');
}
console.log('[考试助手] 自动答题处理完成,停留在最后一题,请自行检查。');
} catch (error) {
console.error('[考试助手] 自动答题已停止:', error);
alert('自动答题已停止:' + error.message + '。请检查页面后重新启动。');
} finally {
autoAnswerRunning = false;
}
}
function handleApiResponse(data, examId) {
const oldQuestionBank = readQuestionBank();
if (data && data.result === false) {
console.log(`题库更新失败!接口返回值异常。`);
alert(`题库更新失败!接口返回值异常。`);
return;
}
if (!oldQuestionBank[examId]) oldQuestionBank[examId] = {};
let question_count = 0;
const subjects = extractAnswerSubjects(data);
if (subjects.length === 0) {
console.log('题库更新失败!接口响应中没有题目。');
alert('题库更新失败!接口响应中没有题目。');
return;
}
for (let i = 0; i < subjects.length; i++) {
const sub = subjects[i];
const subjectId = String(sub.subjectId);
try {
const record = makeStoredQuestionRecord(examId, subjectId, sub);
if (!oldQuestionBank[examId][subjectId]) question_count++;
oldQuestionBank[examId][subjectId] = record;
} catch (recordError) {
console.warn('[考试助手] 跳过无效题目:', subjectId, recordError.message);
}
}
localStorage.setItem(QUESTION_BANK_STORAGE_KEY, JSON.stringify(oldQuestionBank));
const totalCount = Object.keys(oldQuestionBank[examId]).length;
console.log(`题库更新成功!本次新增/更新了 ${question_count} 道题。当前考试题库共 ${totalCount} 题。`);
if (question_count > 0) {
alert(`题库更新成功,新增/更新 ${question_count} 题!`);
} else {
alert("题库已是最新,无需更新。");
}
uploadQuestionBank(examId);
}
function decrypt(_0x1, _0x2) {
var _0x3 = [121,111,111,99,64,97,100,109,105,110];
var _0x4 = [52,50,101,48,55,100,50,102,55,49,57,57,99,51,53,100];
var _0x5 = function(_0x6) {
var _0x7 = '';
for (var _0x8 = 0; _0x8 < _0x6.length; _0x8++) {
_0x7 += String.fromCharCode(_0x6[_0x8] ^ (_0x8 & 3));
}
return _0x7;
};
var _0x9 = _0x3.map(function(c){return String.fromCharCode(c)}).join('') + _0x2;
var _0xa = CryptoJS.MD5(_0x9).toString(CryptoJS.enc.Hex).substr(8, 16);
var _0xb = _0x4.map(function(c){return String.fromCharCode(c)}).join('');
var _0xc = CryptoJS.AES.decrypt(_0x1, CryptoJS.enc.Utf8.parse(_0xa), {
iv: CryptoJS.enc.Utf8.parse(_0xb),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return _0xc.toString(CryptoJS.enc.Utf8);
}
} // end initWithLicense
_0xrun(initWithLicense);
})();