// ==UserScript== // @name 易班考试答题助手(新版) // @namespace http://tampermonkey.net/ // @version 4.0.1 // @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.1'; 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 + '

' + '
' + '
' + '
' + ' 加QQ群' + '
' + (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; var reviewAnswerObserver = null; 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: 16px; transform: translateY(-50%); background-color: #fff; border: 1px solid #e5e7eb; border-radius: 14px; padding: 8px; 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: 96px; max-width: calc(100vw - 32px); 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: 8px 12px; 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: 6px; } #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: 12px; padding-top: 10px; 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: 4px; margin-top: 10px; 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-switch-text { display: inline-block; flex: 0 0 44px; width: 44px; white-space: nowrap; text-align: left; } #helper-panel .hp-related-hint { margin: 5px 0 0 21px; font-size: 10px; line-height: 1.65; color: #94a3b8; } #helper-panel .hp-related-hint:empty { display: none; } #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: 16px; 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: 188px; max-width: calc(100vw - 32px); 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'; relatedBankLabel.title = '自动下载相关题库'; 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); const relatedBankText = document.createElement('span'); relatedBankText.className = 'hp-switch-text'; relatedBankText.textContent = '自动下载'; relatedBankLabel.appendChild(relatedBankText); const relatedBankHint = document.createElement('p'); 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.setAttribute('role', 'status'); settings.appendChild(aiStatus); const aiLabel = document.createElement('label'); aiLabel.className = 'hp-related-label'; aiLabel.title = '仅题库未命中时使用 AI'; 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); const aiText = document.createElement('span'); aiText.className = 'hp-switch-text'; aiText.textContent = '启用 AI'; aiLabel.appendChild(aiText); 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 (typeof latestVersion === 'string' && latestVersion.localeCompare(currentVersion, undefined, { numeric: true }) > 0) { 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; startReviewAnswerDisplay(); 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; renderHiddenReviewAnswers(); 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) { startReviewAnswerDisplay(); renderHiddenReviewAnswers(); 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 hasQuestionContentMedia(root, optionItems) { if (!root) return false; return Array.prototype.some.call(root.querySelectorAll('img, svg, canvas, video, audio, math'), function(media) { if (media.tagName.toLowerCase() !== 'svg') return true; var icon = media.closest('.icon'); return !icon || !optionItems || optionItems.indexOf(icon.parentElement) < 0 || Boolean(media.closest('.flex-auto')); }); } 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'; var optionInputs = optionItems.map(function(item) { return item.querySelector('input[type=checkbox], input[type=radio]'); }); var hasCheckbox = optionInputs.some(function(input) { return input && input.type === 'checkbox'; }); var hasRadio = optionInputs.some(function(input) { return input && input.type === 'radio'; }); return { container: container, h3: h3, optionItems: optionItems, optionTexts: optionTexts, optionRawTexts: optionRawTexts, titleText: (clone.textContent || '').trim(), inputs: inputs, kind: kind, multiple: hasCheckbox || (!hasRadio && /多项|多选/.test(headingText)), hasMedia: hasQuestionContentMedia(h3, []) || hasQuestionContentMedia(optionList, optionItems), 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 isChoiceItemSelected(item) { var input = item && item.querySelector ? item.querySelector('input[type=checkbox], input[type=radio]') : null; return input ? input.checked === true : Boolean(item && item.classList && item.classList.contains('_c')); } 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 调用过于频繁或额度已用完,请稍后重试', 502: 'AI 服务未返回可安全使用的答案,当前题未填写,请稍后重试', 503: '服务器尚未启用 AI 或配置不完整,请联系管理员', 504: '服务器请求 AI 超时,请稍后重试' }; if (response.status === 502) { var failure; try { failure = JSON.parse(response.responseText); } catch (error) { failure = null; } var failureMessages = { invalid_answer: 'AI 答案格式、数量或索引校验失败,当前题未填写,请重试当前题', provider_error: 'AI 上游服务异常或连接失败,当前题未填写,请稍后重试' }; if (failure && failure.success === false && typeof failure.code === 'string' && Object.hasOwn(failureMessages, failure.code)) { messages[502] = failureMessages[failure.code]; } } var httpError = new Error(messages[response.status] || 'AI 服务器返回 HTTP ' + response.status + ',请稍后重试或联系管理员'); httpError.httpStatus = response.status; throw httpError; } 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(); }); } function waitForAiQuestionInterval(assertCurrent) { return new Promise(function(resolve, reject) { var started = Date.now(); function check() { try { if (!getAiConfig().enabled) throw new Error('AI 已关闭,停止等待下一题'); assertCurrent(); var remaining = 1000 - (Date.now() - started); if (remaining <= 0) resolve(); else setTimeout(check, Math.min(100, remaining)); } 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); } const startQuestionIndex = progress.current - 1; console.log('[考试助手] 从当前第 ' + progress.current + ' 题开始,前面的题目不再处理。'); 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); var selected = currentSnapshot.optionItems.map(isChoiceItemSelected); if (currentSnapshot.multiple) { currentSnapshot.optionItems.forEach(function(item, index) { if (selected[index] && !desired.has(index)) item.click(); }); currentSnapshot.optionItems.forEach(function(item, index) { if (!selected[index] && desired.has(index)) item.click(); }); } else { var target = mapped[0]; if (!selected[target]) currentSnapshot.optionItems[target].click(); } console.log('[选择题] 已按' + (source || '题库') + '匹配答案。'); return true; } for (let questionIndex = startQuestionIndex; 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 (snapshot.hasMedia) { console.warn('[考试助手] 题干或选项包含图片或公式等非文本内容,保留当前答案,不调用 AI。'); setAiAnswerStatus('当前题目含图片或公式,已跳过 AI。'); } else if (matched.status === 'conflict' || matched.status === 'invalid') { 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(isChoiceItemSelected); 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 isChoiceItemSelected(item) !== 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) { error.resumeMessage = '当前第 ' + (questionIndex + 1) + ' 题未填写。处理后再次点击自动答题,将从页面当前题开始,不会返回第一题。'; setAiAnswerStatus(error.message + '。' + error.resumeMessage); throw error; } if (questionIndex + 1 < questions.length) { aiSnapshot.inputs.forEach(function(input, index) { inputValues[index] = input.value; }); aiSnapshot.optionItems.forEach(function(item, index) { selectedOptions[index] = isChoiceItemSelected(item); }); setAiAnswerStatus('第 ' + (questionIndex + 1) + ' 题 AI 处理完成,等待 1 秒后继续。'); await waitForAiQuestionInterval(assertAiQuestionCurrent); } } 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 + '。' + (error.resumeMessage || '请检查页面后重新启动。')); } 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 readReviewSubject(hidden, examId) { var root = document.getElementById('root'); if (!root) return null; var key = Object.keys(root).find(function(name) { return /^__react(Container|Fiber)/.test(name); }); var fiber = key && root[key]; var current = fiber && fiber.stateNode && fiber.stateNode.current || fiber; var pending = current ? [current] : []; var visited = new Set(); while (pending.length && visited.size < 20000) { var node = pending.pop(); if (!node || visited.has(node)) continue; visited.add(node); if (node.stateNode === hidden) { for (var parent = node; parent; parent = parent.return) { var props = parent.memoizedProps; if (props && props.review === true && String(props.examId) === examId && props.subject && props.subject.subjectId != null) return props.subject; } return null; } if (node.sibling) pending.push(node.sibling); if (node.child) pending.push(node.child); } return null; } function getReviewAnswerText(subject, examId, optionTexts) { var record; try { record = makeStoredQuestionRecord(examId, String(subject.subjectId), subject); } catch (error) { var bank = readQuestionBank(); var stored = bank[examId] && bank[examId][String(subject.subjectId)]; if (!stored) throw new Error('未获取到当前题的有效答案'); record = validateStoredQuestion(stored); } if (record.option.length) { var options = record.option.map(normalizeOptionForMatch); var displayed = optionTexts.map(normalizeOptionForMatch); if (getOptionSignature(options) !== getOptionSignature(displayed) || new Set(displayed).size !== displayed.length) throw new Error('当前选项无法唯一映射'); return '正确答案:' + record.answer.map(function(index) { var text = options[index]; var position = displayed.indexOf(text); if (position < 0) throw new Error('答案选项不存在'); return String.fromCharCode(65 + position) + '. ' + text; }).join(';'); } if (optionTexts.length) throw new Error('当前题型与答案不一致'); return '正确答案:' + record.answer.map(function(value, index) { return '第 ' + (index + 1) + ' 空:' + getBlankCandidates(value).map(function(candidate) { return normalizeMatchText(candidate); }).join(' / '); }).join(';'); } function renderHiddenReviewAnswers() { var match = window.location.href.match(/group\/\d+\/exam\/(\d+)\/review\/\d+/); var hiddenItems = match ? Array.from(document.querySelectorAll('main .hideanswer')) : []; var activeBoxes = new Set(); hiddenItems.forEach(function(hidden) { var container = hidden.parentElement; var box = container.querySelector('.exam-helper-review-answer'); try { var subject = readReviewSubject(hidden, match[1]); if (!subject) throw new Error('无法识别当前回顾题目'); var options = Array.from(container.querySelectorAll('ul li')).map(function(item) { var text = item.querySelector('.flex-auto') || item; return text.textContent || ''; }); var answerText = getReviewAnswerText(subject, match[1], options); if (!box) { box = document.createElement('div'); box.className = 'exam-helper-review-answer'; box.style.cssText = 'margin:12px 0;padding:10px 12px;border:1px solid #a7f3d0;border-radius:8px;background:#ecfdf5;color:#065f46;font-size:14px;line-height:1.7;white-space:pre-wrap;overflow-wrap:anywhere;'; container.appendChild(box); } if (box.textContent !== answerText) box.textContent = answerText; activeBoxes.add(box); if (!Object.hasOwn(hidden.dataset, 'helperAnswerDisplay')) hidden.dataset.helperAnswerDisplay = hidden.style.display; hidden.style.display = 'none'; } catch (error) { if (box) box.remove(); if (Object.hasOwn(hidden.dataset, 'helperAnswerDisplay')) { hidden.style.display = hidden.dataset.helperAnswerDisplay; delete hidden.dataset.helperAnswerDisplay; } } }); document.querySelectorAll('.exam-helper-review-answer').forEach(function(box) { if (!activeBoxes.has(box)) box.remove(); }); if (!match) document.querySelectorAll('[data-helper-answer-display]').forEach(function(hidden) { hidden.style.display = hidden.dataset.helperAnswerDisplay; delete hidden.dataset.helperAnswerDisplay; }); } function startReviewAnswerDisplay() { if (reviewAnswerObserver) return; var timer = null; reviewAnswerObserver = new MutationObserver(function() { clearTimeout(timer); timer = setTimeout(renderHiddenReviewAnswers, 80); }); reviewAnswerObserver.observe(document.getElementById('root') || document.body, { childList: true, subtree: true, characterData: true }); renderHiddenReviewAnswers(); } 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); })();