// ==UserScript== // @name OK学习通网课作业助手 // @version 4.6.2 // @author ok // @license MIT // @description 超星学习通助手,支持视频/音频后台挂机、章节测验/考试AI自动答题、免费高分题库。后台上报不占用带宽,伪实时进度条显示。 // @match *://*.chaoxing.com/* // @run-at document-end // @grant unsafeWindow // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_info // @grant GM_getResourceText // @grant GM_openInTab // @icon http://pan-yz.chaoxing.com/favicon.ico // @homepage https://scriptcat.org/zh-CN/script-show-page/6564 // @require https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js // @require https://cdn.bootcdn.net/ajax/libs/limonte-sweetalert2/11.1.0/sweetalert2.all.min.js // @require https://cdn.bootcdn.net/ajax/libs/blueimp-md5/2.19.0/js/md5.min.js // @require https://so.ucuc.net/TyprMd5.js // @resource Table https://so.ucuc.net/table.json // @connect so.ucuc.net // @connect 101.96.197.41 // @connect scriptcat.org // @tag 学习通 // @tag 刷课工具 // @tag 免费搜题 // @tag AI答题 // @tag 可选付费 // @antifeature payment 部分高级搜题功能需要付费API密钥 // ==/UserScript== // 防止脚本在同一frame中重复执行 if (unsafeWindow._okHelperLoaded) return; unsafeWindow._okHelperLoaded = true; /*********************************配置系统******************************************************** */ var CFG_DEFAULTS = { showBox: 1, // 显示脚本浮窗 task: 0, // 只处理任务点任务 video: 1, // 处理视频 audio: 1, // 处理音频 rate: 1, // 视频/音频倍速 review: 0, // 复习模式 work: 1, // 测验自动处理 time: 5000, // 答题时间间隔(ms) sub: 0, // 测验自动提交 force: 0, // 测验强制提交 decrypt: 1, // 字体解密 examTurn: 0, // 考试自动跳转 examTurnTime: 1, // 考试自动跳转随机间隔 videoMode: 'simulate', // 'simulate'=模拟上报, 'normal'=正常播放 reportInterval: 50 // 上报间隔(秒) }; function cfg(key) { return GM_getValue('cfg_' + key, CFG_DEFAULTS[key]); } function setCfg(key, val) { GM_setValue('cfg_' + key, val); } function migrateOldConfig() { if (GM_getValue('cfg_migrated', false)) return; var oldGmMap = { videoMode: 'videoMode', videoRate: 'rate' }; for (var oldKey in oldGmMap) { var v = GM_getValue(oldKey, undefined); if (v !== undefined) setCfg(oldGmMap[oldKey], v); } var oldLsKeys = ['sub', 'force', 'examTurn', 'review', 'time']; oldLsKeys.forEach(function(k) { var v = localStorage.getItem('OKHelper.' + k); if (v !== null) { setCfg(k, k === 'time' ? parseInt(v) : v === 'true'); } }); GM_setValue('cfg_migrated', true); } /**************************************************************************************************/ var _w = unsafeWindow, _l = location, _d = _w.document, $ = _w.jQuery || top.jQuery, UE = _w.UE, md5 = window.md5 || $.md5; // 获取Typr对象 var TyprInstance = null; try { if (typeof unsafeWindow !== 'undefined' && unsafeWindow.Typr) { TyprInstance = unsafeWindow.Typr; } else if (typeof window !== 'undefined' && window.Typr) { TyprInstance = window.Typr; } else if (typeof Typr !== 'undefined') { TyprInstance = Typr; } else if (typeof unsafeWindow !== 'undefined' && unsafeWindow.TyprMd5) { TyprInstance = unsafeWindow.TyprMd5; } else if (typeof TyprMd5 !== 'undefined') { TyprInstance = TyprMd5; } } catch(e) {} // ========== 通用工具函数 ========== function getCookie(name) { return ('; ' + document.cookie).split('; ' + name + '=').pop().split(';')[0]; } function formatDuration(seconds) { if (!seconds || seconds < 0) return '00:00'; var total = Math.max(0, Math.floor(Number(seconds) || 0)); var h = Math.floor(total / 3600); var m = Math.floor((total % 3600) / 60); var s = total % 60; var pad = function(n) { return String(n).padStart(2, '0'); }; if (h > 0) { return pad(h) + ':' + pad(m) + ':' + pad(s); } return pad(m) + ':' + pad(s); } function sanitizeText(s) { if (!s) return null; return s.replace(/<(?!img).*?>/g, "").replace(/^【.*?】\s*/, '').replace(/\s*(\d+\.\d+分)$/, '').trim().replace(/ /g, ''); } function sanitizeQuestion(s) { if (!s) return null; return s.replace(/<(?!img).*?>/g, "").replace(/^【.*?】\s*/, '').replace(/\s*(\d+\.\d+分)$/, '').replace(/^\d+[.、]/, '').trim(); } function splitAnswer(answer) { if (!answer) return []; var parts = answer.split(/[#]+/).map(a => a.trim()).filter(a => a !== ''); if (parts.length === 0 && answer.trim() !== '') { return [answer.trim()]; } return parts; } function extractBetween(str, start, end) { let res = str.match(new RegExp(start + '(.*?)' + end)); return res ? res[1] : null; } function isTaskCompleted(task) { if (!task) return false; if (task.isPassed === true) return true; if (task.status === 'completed' || task.status === 'finished') return true; if (task.finished === true) return true; return false; } // ========== 日志系统 ========== var logQueue = []; var logTimer = null; function getTopLogContainer() { try { var topDoc = top.document; var container = topDoc.getElementById('ok-helper-log'); if (container) return $(container); container = document.getElementById('ok-helper-log'); if (container) return $(container); return null; } catch(e) { return null; } } function flushLogs() { if (logQueue.length === 0) return; var container = getTopLogContainer(); if (container && container.length) { var logsToShow = logQueue.slice(); logQueue = []; var frag = document.createDocumentFragment(); for (var i = 0; i < logsToShow.length; i++) { var tmp = document.createElement('div'); tmp.innerHTML = logsToShow[i]; if (tmp.firstChild) frag.appendChild(tmp.firstChild); } var rawEl = container[0] || container.get(0); if (rawEl) { rawEl.insertBefore(frag, rawEl.firstChild); } container.scrollTop(0); } if (logTimer) { clearTimeout(logTimer); logTimer = null; } } function scheduleFlushLogs() { if (logTimer) return; logTimer = setTimeout(flushLogs, 100); } function log(str, color) { var time = new Date().toLocaleTimeString(); var logHtml = '
' + answer + '
'); log(' 简答题已填写到UEditor iframe', 'green'); return true; } catch(e) {} } // 方法3: contenteditable区域 var $editorDiv = $question.find('[contenteditable="true"]'); if ($editorDiv.length) { $editorDiv.first().html(answer); $editorDiv.first().trigger('input').trigger('change'); log(' 简答题已填写到contenteditable', 'green'); return true; } // 方法4: 遍历所有UEditor实例(兜底) if (win.UE && win.UE.instants) { var editors = win.UE.instants; for (var key in editors) { var editor = editors[key]; if (!editor) continue; try { var editorContainer = editor.container; if (editorContainer && $question[0].contains(editorContainer)) { editor.setContent(answer); log(' 简答题已填写到UEditor(兜底)', 'green'); return true; } } catch(e) {} } } log(' 简答题未找到可填写的输入框', 'orange'); return false; } // PC端处理 var $ueTextarea = $question.find('textarea[name^="answerEditor"], .eidtDiv textarea, .divText textarea'); if ($ueTextarea.length) { var id = $ueTextarea.first().attr('id'); if (id && UE && UE.getEditor && UE.getEditor(id)) { try { UE.getEditor(id).setContent(answer); return true; } catch(e) {} } $ueTextarea.val(answer); $ueTextarea.trigger('input').trigger('change'); return true; } return false; } // ========== Web Worker 后台上报(修复版)========== var reportWorker = null; var activeVideoJob = null; // 完成上报多次未通过时的兜底:直接查 status 接口确认是否实际已 passed // 学生学习页面用 .ans-job-finished 类判断完成,源头来自 status 接口的 isPassed 字段 function verifyPassedFallback(ctx, worker) { // 修复:已取消的 worker 不再发起兜底查询 if (worker.aborted) { log(' Worker 已取消,跳过兜底查询', 'orange'); return; } var statusUrl = _l.protocol + '//' + _l.host + '/ananas/status/' + ctx.objectId + '?k=' + (getCookie('fid') || '') + '&_uid=' + ctx.uid + '&flag=normal&_dc=' + Date.now(); GM_xmlhttpRequest({ method: "GET", url: statusUrl, headers: { 'Host': _l.host, 'Referer': _l.protocol + '//' + _l.host + '/ananas/modules/video/index.html' }, onload: function(res) { if (worker.aborted) return; try { var info = JSON.parse(res.responseText); if (info.isPassed) { log(' 兜底查询确认任务已通过: ' + ctx.name, 'green'); if (!worker.isCompleted) { worker.isCompleted = true; if (worker.completeCallback) worker.completeCallback(); } } else { log(' 兜底查询确认仍未通过,可能受学时限制或防刷限制,继续下一任务', 'red'); if (worker.completeCallback) worker.completeCallback(); } } catch(e) { log(' 兜底查询解析失败,继续下一任务', 'orange'); if (worker.completeCallback) worker.completeCallback(); } }, onerror: function() { if (worker.aborted) return; log(' 兜底查询请求失败,继续下一任务', 'orange'); if (worker.completeCallback) worker.completeCallback(); } }); } // 创建Web Worker用于后台计时(包含实际上报逻辑) function createReportWorker(videoInfo, taskObj) { var name = videoInfo.name; var duration = videoInfo.duration; var playedTime = videoInfo.playedTime || 0; var dtoken = videoInfo.dtoken; var objectId = videoInfo.objectId; var jobId = taskObj.jobid; var rt = videoInfo.rt; var otherInfo = videoInfo.otherInfo; var reportUrl = chapterConfig.reportUrl; var classId = chapterConfig.clazzId; var uid = getCookie('_uid') || getCookie('UID'); var rate = cfg('rate'); var reportInterval = cfg('reportInterval'); // 上报必须从0开始(服务器要求完整序列) // 但对已观看部分(0→playedTime)快速跳过,缩短等待时间 var workerCode = ` var timer = null; var currentTime = 0; var duration = ${duration}; var rate = ${rate}; var playedTime = ${playedTime}; var reportInterval = ${reportInterval}; var lastReportTime = 0; var isRunning = true; var startTime = Date.now(); var reportCount = 0; // 上报函数 function sendReport(playTime, isComplete, isFirst) { self.postMessage({ type: 'doReport', playTime: playTime, isComplete: isComplete, isFirst: isFirst || false }); } // 首次上报(isdrag=3)从0开始 sendReport(0, false, true); // 计时循环 // 对已观看部分(0→playedTime)快速跳过,使用10x加速 // 超过playedTime后恢复正常速度 var fastRate = 10; // 已看部分的加速倍率 function updateLoop() { if (!isRunning) return; var now = Date.now(); var elapsed = (now - startTime) / 1000; // 已看部分用fastRate加速,未看部分用正常rate var effectiveRate = (currentTime < playedTime) ? fastRate : rate; var targetTime = Math.min(currentTime + (effectiveRate * 0.2), duration); // 每200ms推进 var isCatchingUp = false; if (targetTime > currentTime) { // 检测到时间跳跃(超过 1.5 倍上报间隔):步进推进 if (targetTime - currentTime > reportInterval * 1.5) { isCatchingUp = true; currentTime = Math.min(currentTime + reportInterval, targetTime); } else { currentTime = targetTime; } var timeSinceLastReport = currentTime - lastReportTime; var shouldReport = timeSinceLastReport >= reportInterval || currentTime >= duration; if (shouldReport) { var reportTime = currentTime >= duration ? duration : Math.ceil(currentTime); sendReport(reportTime, currentTime >= duration); lastReportTime = currentTime; } self.postMessage({ type: 'progress', currentTime: currentTime, duration: duration }); } if (currentTime >= duration) { isRunning = false; if (timer) clearInterval(timer); self.postMessage({ type: 'completed' }); } else { // 已看部分快速推进(200ms一次),追上后正常速度 // 补发心跳期间放慢到3秒 var nextDelay = isCatchingUp ? 3000 : 200; timer = setTimeout(updateLoop, nextDelay); } } updateLoop(); self.onmessage = function(e) { var data = e.data; switch(data.type) { case 'updateRate': rate = data.rate; break; case 'stop': isRunning = false; if (timer) clearTimeout(timer); self.postMessage({ type: 'stopped' }); break; } }; `; var blob = new Blob([workerCode], { type: 'application/javascript' }); var worker = new Worker(URL.createObjectURL(blob)); // 存储视频信息供上报使用 worker.videoContext = { name: name, duration: duration, dtoken: dtoken, objectId: objectId, jobId: jobId, rt: rt, otherInfo: otherInfo, reportUrl: reportUrl, classId: classId, uid: uid }; worker.onmessage = function(e) { var data = e.data; switch(data.type) { case 'progress': if (worker.aborted) break; // Worker已经从记忆位置开始,直接使用currentTime updateProgressBar(data.currentTime, data.duration); break; case 'doReport': // 检查 worker 是否已被取消(forceCleanupAll 设置了 aborted 标志) if (worker.aborted) { log(' Worker 已取消,忽略上报请求', 'orange'); break; } // 主线程执行实际上报 var ctx = worker.videoContext; var isdrag; if (data.isFirst) { isdrag = '3'; } else if (data.isComplete) { isdrag = '4'; } else { isdrag = '0'; } log(' 上报: ' + formatDuration(data.playTime) + '/' + formatDuration(ctx.duration) + (data.isFirst ? ' [开始]' : '') + (isdrag === '4' ? ' [完成]' : ''), 'blue'); // 完成上报重试函数 var doReportRequest = function(retryCount) { // 关键修复:每次重试前检查 worker 是否已被取消 // 防止旧任务的重试请求与新任务的上报并行,污染服务器状态 if (worker.aborted) { log(' Worker 已取消,停止重试上报', 'orange'); return; } retryCount = retryCount || 0; var maxRetries = 10; var enc = generateEncLocal(ctx.classId, ctx.uid, ctx.jobId, ctx.objectId, data.playTime, ctx.duration); var reportsUrl = ctx.reportUrl + '/' + ctx.dtoken + '?clazzId=' + ctx.classId + '&playingTime=' + data.playTime + '&duration=' + ctx.duration + '&clipTime=0_' + ctx.duration + '&objectId=' + ctx.objectId + '&otherInfo=' + ctx.otherInfo + '&jobid=' + ctx.jobId + '&userid=' + ctx.uid + '&isdrag=' + isdrag + '&view=pc' + '&enc=' + enc + '&rt=' + ctx.rt + '&dtype=Video' + '&_t=' + Date.now(); GM_xmlhttpRequest({ method: "GET", url: reportsUrl, headers: { 'Host': _l.host, 'Referer': _l.protocol + '//' + _l.host + '/ananas/modules/video/index.html', 'Content-Type': 'application/json', 'Sec-Fetch-Site': 'same-origin' }, onload: function(res) { // 回调中再次检查:请求发出后 worker 可能已被取消 if (worker.aborted) return; try { var result = JSON.parse(res.responseText); // 打印每次上报的服务器返回(截取前120字符) var respSnippet = (res.responseText || '').substring(0, 120); // log(' 上报返回[isdrag=' + isdrag + ']: ' + respSnippet, 'gray'); if (result.isPassed) { if (!worker.isCompleted) { worker.isCompleted = true; log(' 视频任务已完成: ' + ctx.name, 'green'); if (worker.completeCallback) worker.completeCallback(); } } else if (isdrag === '4' && retryCount < maxRetries) { var snippet = (res.responseText || '').substring(0, 160); log(' 完成上报未通过(' + (retryCount + 1) + '/' + maxRetries + ') 服务器返回: ' + snippet, 'orange'); setTimeout(function() { doReportRequest(retryCount + 1); }, 3000); } else if (isdrag === '4') { log(' 完成上报重试' + maxRetries + '次仍未通过,回退查询任务点状态', 'red'); // 兜底:上报响应没说通过,但服务器实际可能已记完成。直接查 status 接口确认 verifyPassedFallback(ctx, worker); } } catch(e) { log(' 上报响应解析失败: ' + e.message + ' 内容: ' + (res.responseText || '').substring(0, 120), 'orange'); if (isdrag === '4' && retryCount < maxRetries) { setTimeout(function() { doReportRequest(retryCount + 1); }, 3000); } } }, onerror: function() { if (worker.aborted) return; if (isdrag === '4' && retryCount < maxRetries) { log(' 上报失败,重试中(' + (retryCount + 1) + '/' + maxRetries + ')...', 'orange'); setTimeout(function() { doReportRequest(retryCount + 1); }, 3000); } else { log(' 上报失败,将继续尝试', 'orange'); } } }); }; doReportRequest(0); break; case 'completed': if (worker.aborted) break; // Worker计时已到,但如果完成上报还在重试中,等待重试完成 // isCompleted为true时completeCallback已被调用,这里作为兜底 log('视频播放计时完成,等待服务器确认...', 'green'); setTimeout(function() { if (worker.aborted) return; if (!worker.isCompleted && worker.completeCallback) { log(' 等待超时,强制继续下一任务', 'orange'); worker.completeCallback(); } }, 35000); break; case 'stopped': log(' Worker已停止', 'blue'); break; } }; return worker; } // ========== 视频处理相关变量 ========== var currentVideoInterval = null; var currentProgressBar = null; var currentVideoWorker = null; // 单独存储Worker引用 var currentVideoTaskId = null; // 当前视频任务ID var videoTaskCounter = 0; // 视频任务计数器 var taskQueue, chapterConfig, attachFrames, $subBtn, $saveBtn, $frame_c, $okBtn, quizFrameEl; var isProcessing = false; var isBoxHidden = false; var pendingMissionCount = 0; var completedMissionCount = 0; var isJumping = false; var hasTriggeredNoTaskJump = false; var isVideoTaskActive = false; var currentVideoName = null; var currentVideoPlayedTime = 0; // 记忆播放位置,进度条从此位置开始显示 // ========== 强制清理所有残留资源(每次新任务前调用)========== function forceCleanupAll() { // 只有本 frame 有活跃任务时才输出清理日志,避免多 frame 噪音 var hasActiveTask = isVideoTaskActive || currentVideoWorker || currentProgressBar || currentVideoInterval; if (hasActiveTask) { log(' 开始清理残留资源...', 'orange'); } // 停止Worker - 使用局部变量保存引用,确保 setTimeout 回调能正确终止 if (currentVideoWorker) { var workerToTerminate = currentVideoWorker; currentVideoWorker = null; // 先清空全局引用 reportWorker = null; // 同步清空兼容引用 try { workerToTerminate.aborted = true; // 标记为已取消,阻止旧请求重试 workerToTerminate.postMessage({ type: 'stop' }); setTimeout(function() { try { workerToTerminate.terminate(); } catch(e) {} }, 100); log(' 已终止视频上报Worker', 'green'); } catch(e) { log(' 终止Worker失败: ' + e.message, 'orange'); } } // 清除定时器 if (currentVideoInterval) { clearInterval(currentVideoInterval); currentVideoInterval = null; log(' 已清除视频定时器', 'green'); } // 清除reportWorker引用(兼容旧代码,Worker 可能已被上面清除) if (reportWorker) { try { reportWorker.aborted = true; reportWorker.terminate(); } catch(e) {} reportWorker = null; } // 立即移除进度条 removeProgressBar(); // 重置所有视频相关状态 isVideoTaskActive = false; currentVideoName = null; activeVideoJob = null; currentVideoTaskId = null; currentVideoPlayedTime = 0; if (hasActiveTask) { log(' 残留资源清理完成', 'green'); } } // 单独移除进度条函数 function removeProgressBar() { currentProgressBar = null; var el = getHeaderProgressEl(); if (el) { el.style.display = 'none'; el.textContent = ''; } // 清理旧版浮动进度条(兼容升级残留) try { $(_w.top.document).find('#video-progress-bar').remove(); } catch(e) {} try { $(document).find('#video-progress-bar').remove(); } catch(e) {} } // ========== 进度条管理(面板标题栏内嵌显示)========== function getHeaderProgressEl() { try { var el = top.document.querySelector('#header-progress'); if (el) return el; } catch(e) {} try { var el2 = document.querySelector('#header-progress'); if (el2) return el2; } catch(e) {} // 遍历父级frame查找 try { var w = window; while (w !== w.parent) { w = w.parent; var el3 = w.document.querySelector('#header-progress'); if (el3) return el3; } } catch(e) {} return null; } function createProgressBar(videoName, duration) { removeProgressBar(); currentVideoName = videoName; var el = getHeaderProgressEl(); if (el) { el.style.display = 'inline'; el.setAttribute('data-task-id', currentVideoTaskId || ''); el.setAttribute('data-duration', duration || 0); el.textContent = '▶ ' + videoName.substring(0, 12) + ' 0% 00:00'; } currentProgressBar = el; } function updateProgressBar(playTime, duration) { var el = currentProgressBar || getHeaderProgressEl(); if (!el) return; // 检查是否是当前任务 var taskId = el.getAttribute('data-task-id'); if (taskId && currentVideoTaskId && parseInt(taskId) !== currentVideoTaskId) { return; } var percent = Math.min(100, Math.round((playTime / duration) * 100)); var remaining = Math.max(0, duration - playTime); el.textContent = '▶ ' + percent + '% ' + formatDuration(playTime) + '/' + formatDuration(duration) + ' 余' + formatDuration(remaining); el.style.display = 'inline'; } // ========== 完成任务并继续 ========== function completeCurrentTask() { // 清理残留 forceCleanupAll(); if (taskQueue && taskQueue.length > 0) { var completedTask = taskQueue[0]; var taskName = completedTask.property ? (completedTask.property.name || completedTask.property.title || '未知') : '未知'; log(' 完成任务: ' + taskName, 'green'); taskQueue.splice(0, 1); completedMissionCount++; log(' 任务进度: ' + completedMissionCount + '/' + pendingMissionCount + ' | 剩余: ' + taskQueue.length, 'blue'); } if (attachFrames && attachFrames.length > 0) { attachFrames.splice(0, 1); } isProcessing = false; if (taskQueue && taskQueue.length > 0) { log(' 还有 ' + taskQueue.length + ' 个任务点待处理,继续...', 'green'); setTimeout(function() { dispatchNextTask(); }, 1000); } else { log(' 此页面所有任务处理完毕', 'green'); navigateForward(); } } function navigateForward() { if (isJumping) { log(' 跳转锁定中,5秒后重试...', 'orange'); setTimeout(function() { isJumping = false; navigateForward(); }, 5000); return; } isJumping = true; // 清除card去重标记(即将导航到新card/章节) try { top._okProcessedCard = ''; } catch(e) {} log(' 检查是否有下一章节...', 'blue'); var nextBtn = null; try { var topDoc = top.document; nextBtn = topDoc.querySelector('#prevNextFocusNext:not(.disabled)') || topDoc.querySelector('.orientationright:not(.disabled)') || topDoc.querySelector('.jb_btn.jb_btn_next:not(.disabled)') || topDoc.querySelector('#mainid > .prev_next.next:not(.disabled)') || topDoc.querySelector('.prev_next.next:not(.disabled)') || topDoc.querySelector('.nodeItem.r i') || topDoc.querySelector('[onclick*="PCount.next"]'); } catch(e) { log(' 无法访问顶层页面,尝试当前页面...', 'orange'); try { nextBtn = document.querySelector('#prevNextFocusNext:not(.disabled)') || document.querySelector('.orientationright:not(.disabled)') || document.querySelector('.prev_next.next:not(.disabled)'); } catch(e2) {} } if (nextBtn && !nextBtn.disabled) { log(' 找到下一节按钮,3秒后跳转', 'green'); setTimeout(() => { nextBtn.click(); log(' 已点击跳转按钮', 'green'); isJumping = false; }, 3000); return true; } else { log(' 未找到下一节按钮,尝试URL跳转...', 'orange'); try { var topDoc = top.document; var curNode = topDoc.querySelector('.posCatalog_active, .currents, .jb_item.active'); if (curNode) { var nextNode = curNode.nextElementSibling || curNode.parentElement.nextElementSibling; if (nextNode) { var link = nextNode.querySelector('a') || nextNode; if (link && link.click) { log(' 通过章节目录跳转下一节', 'green'); setTimeout(() => { link.click(); isJumping = false; }, 3000); return true; } } } } catch(e) {} log(' 课程已全部完成,无更多章节可跳转', 'green'); } isJumping = false; return false; } // ========== 获取视频状态 ========== function getVideoStatus(item, callback, iframeDom) { var objectId = item.property.objectid; var uid = getCookie('_uid') || getCookie('UID') || ''; var statusUrl = _l.protocol + '//' + _l.host + '/ananas/status/' + objectId + '?k=' + (getCookie('fid') || '') + '&_uid=' + uid + '&flag=normal&_dc=' + Date.now(); // 从任务对象获取已播放时间(headOffset单位是毫秒) var taskPlayedTime = 0; if (item.headOffset && item.headOffset > 0) { taskPlayedTime = Math.floor(item.headOffset / 1000); log(' 从任务数据获取播放位置: ' + formatDuration(taskPlayedTime), 'blue'); } else if (item.playTime && item.playTime > 0) { taskPlayedTime = Math.floor(item.playTime / 1000); log(' 从任务playTime获取播放位置: ' + formatDuration(taskPlayedTime), 'blue'); } // 尝试从iframe中的video元素获取当前播放位置 var iframePlayedTime = 0; if (iframeDom && iframeDom.length > 0) { try { var iframeEl = iframeDom[0]; var iframeDoc = iframeEl.contentDocument || iframeEl.contentWindow.document; var videoEl = iframeDoc.querySelector('video'); if (videoEl && videoEl.currentTime > 0) { iframePlayedTime = Math.floor(videoEl.currentTime); log(' 从页面视频元素获取到播放位置: ' + formatDuration(iframePlayedTime), 'blue'); } } catch(e) { // 跨域或无法访问iframe,忽略 } } GM_xmlhttpRequest({ method: "GET", url: statusUrl, headers: { 'Host': _l.host, 'Referer': _l.protocol + '//' + _l.host + '/ananas/modules/video/index.html' }, onload: function(res) { try { var videoInfo = JSON.parse(res.responseText); // 取所有来源中最大的播放位置(历史最远) var playedTime = Math.max(taskPlayedTime, iframePlayedTime); log(' 视频状态: 已播放 ' + formatDuration(playedTime) + '/' + formatDuration(videoInfo.duration) + (videoInfo.isPassed ? ' (已通过)' : ''), 'blue'); callback({ success: true, duration: videoInfo.duration, dtoken: videoInfo.dtoken, objectId: objectId, rt: item.property.rt || '0.9', otherInfo: item.otherInfo || '', jobid: item.jobid, playedTime: playedTime, isPassed: videoInfo.isPassed || false, name: item.property.name }); } catch(e) { callback({ success: false, error: e.message }); } }, onerror: function(err) { callback({ success: false, error: err.error }); } }); } // ========== 模拟视频上报(修复版)========== function simulateVideoReport(videoInfo, taskObj, iframeDom) { var name = videoInfo.name || taskObj.property.name; var duration = videoInfo.duration; var isAlreadyPassed = videoInfo.isPassed || false; if (isAlreadyPassed && !cfg('review')) { log(' 视频已完成: ' + name + ',跳过', 'green'); completeCurrentTask(); return; } // 注意:即使 playedTime >= duration,只要 isPassed 为 false,服务器仍未认定完成 // 此时需要重新完整上报(从0开始),让服务器记录完整观看序列 var playedTime = videoInfo.playedTime || 0; if (playedTime >= duration && duration > 0) { log(' 视频本地数据显示已播完: ' + name + ' (' + formatDuration(playedTime) + '/' + formatDuration(duration) + '),但服务器未认定完成,将重新完整上报', 'orange'); } log(' 模拟上报模式: ' + name + ',总时长: ' + formatDuration(duration), 'purple'); if (playedTime > 0 && playedTime < duration) { log(' 从0开始上报,已看部分(' + formatDuration(playedTime) + ')将快速跳过(10x),剩余 ' + formatDuration(duration - playedTime), 'blue'); } else { log(' 从头开始上报', 'blue'); } var taskId = ++videoTaskCounter; currentVideoTaskId = taskId; // 保存记忆播放位置,进度条以此为起点 currentVideoPlayedTime = playedTime; createProgressBar(name, duration); // 进度条从记忆播放位置开始显示(上报仍从0开始) updateProgressBar(playedTime, duration); isVideoTaskActive = true; currentVideoName = name; // 创建新的Worker(包含实际上报逻辑) currentVideoWorker = createReportWorker(videoInfo, taskObj); reportWorker = currentVideoWorker; // 兼容 var workerTaskId = taskId; // 设置完成回调 currentVideoWorker.completeCallback = function() { if (workerTaskId === currentVideoTaskId && isVideoTaskActive) { // 关键:触发 iframe 内 video 元素的 ended 事件,让播放器自动给任务点加 .ans-job-finished // 学生学习页面用此类判定完成;不触发的话即便服务器已记完成、页面仍显示未完成 try { if (iframeDom && iframeDom.length > 0) { var iframeEl = iframeDom[0]; var iframeDoc = iframeEl.contentDocument || iframeEl.contentWindow.document; if (iframeDoc) { var mediaEl = iframeDoc.querySelector('video') || iframeDoc.querySelector('audio'); if (mediaEl) { mediaEl.dispatchEvent(new Event('ended', { bubbles: true })); log(' 已触发 ended 事件,任务点UI将自动更新为已完成', 'green'); } else { log(' iframe 内未找到 video/audio 元素,任务点UI可能需刷新页面才会更新', 'orange'); } } } } catch(e) { log(' 触发 ended 事件失败: ' + e.message + ',任务点UI可能需刷新页面才会更新', 'orange'); } forceCleanupAll(); completeCurrentTask(); } }; } function normalVideoPlay(dom, obj) { var name = obj.property.name; var target = dom.length > 0 ? dom[0] : null; if (!target) { log(' 未找到视频iframe,3秒后重试', 'orange'); isProcessing = false; setTimeout(function() { normalVideoPlay(dom, obj); }, 3000); return; } log(' 正常播放模式处理视频:' + name, 'purple'); isVideoTaskActive = true; currentVideoName = name; var executed = false; var doc = target.contentDocument || target.contentWindow.document; if (currentVideoInterval) { clearInterval(currentVideoInterval); } // 60 秒超时兜底:iframe 加载失败时媒体元素永远找不到,避免永久轮询卡住后续任务 var _waitStartTime = Date.now(); var _MAX_WAIT_MS = 60000; currentVideoInterval = setInterval(function() { // 超时检测:60 秒内未找到媒体元素,说明 iframe 加载失败,跳过该任务 if (!executed && Date.now() - _waitStartTime > _MAX_WAIT_MS) { log('⏱️ 视频 [' + name + '] 等待超时,iframe 可能加载失败,跳过', 'orange'); clearInterval(currentVideoInterval); currentVideoInterval = null; isVideoTaskActive = false; completeCurrentTask(); return; } var media = doc.querySelector('video') || doc.querySelector('audio'); if (media && !executed) { executed = true; log(' ' + name + ' 开始播放', 'green'); media.pause(); media.muted = true; media.playbackRate = cfg('rate') > 1 ? Math.min(cfg('rate'), 16) : 1; media.play(); var resumePlay = function() { if (media.paused && !media.ended) { media.play(); } }; media.addEventListener('pause', resumePlay); var onVideoEnd = function() { log(' ' + name + ' 播放完成', 'green'); media.removeEventListener('pause', resumePlay); media.removeEventListener('ended', onVideoEnd); clearInterval(currentVideoInterval); currentVideoInterval = null; forceCleanupAll(); completeCurrentTask(); }; media.addEventListener('ended', onVideoEnd); if (media.ended) { onVideoEnd(); } } }, 1500); } function processVideo(dom, obj) { if (!cfg('video')) { log('ℹ️ 用户设置不处理视频任务', 'orange'); completeCurrentTask(); return; } var isPassed = obj.isPassed; var name = obj.property.name; if (!cfg('review') && isPassed === true) { log(' 视频:' + name + ' 已完成,跳过', 'green'); completeCurrentTask(); return; } if (cfg('videoMode') === 'simulate') { log(' 模拟上报模式处理视频: ' + name, 'purple'); getVideoStatus(obj, function(result) { if (result.success) { simulateVideoReport(result, obj, dom); } else { log(' 获取视频信息失败: ' + result.error + ',降级到正常播放', 'red'); normalVideoPlay(dom, obj); } }, dom); return; } normalVideoPlay(dom, obj); } function processAudio(dom, obj) { if (!cfg('audio')) { log('ℹ️ 用户设置不处理音频任务', 'orange'); completeCurrentTask(); return; } var isPassed = obj.isPassed; var name = obj.property.name; if (!cfg('review') && isPassed === true) { log(' 音频:' + name + ' 已完成,跳过', 'green'); completeCurrentTask(); return; } log(' 处理音频:' + name + ',等待5秒后完成', 'purple'); setTimeout(function() { completeCurrentTask(); }, 5000); } function processBook(dom, obj) { var jobId = obj.property.jobid; var name = obj.property.bookname; var jtoken = obj.jtoken; var knowledgeId = chapterConfig.knowledgeid; var courseId = chapterConfig.courseid; var clazzId = chapterConfig.clazzId; if (isTaskCompleted(obj)) { log(' 读书:' + name + ' 已完成,跳过', 'green'); completeCurrentTask(); return; } $.ajax({ url: _l.protocol + "//" + _l.host + '/ananas/job?jobid=' + jobId + '&knowledgeid=' + knowledgeId + '&courseid=' + courseId + '&clazzid=' + clazzId + '&jtoken=' + jtoken + '&_dc=' + Date.now(), method: 'GET', success: function(res) { log(' 读书:' + name + (res.msg || '完成'), 'green'); completeCurrentTask(); }, error: function() { log(' 读书:' + name + ' 处理失败,3秒后重试', 'red'); isProcessing = false; setTimeout(function() { processBook(dom, obj); }, 3000); } }); } function processDocument(dom, obj) { var jobId = obj.property.jobid; var name = obj.property.name; var jtoken = obj.jtoken; var knowledgeId = chapterConfig.knowledgeid; var courseId = chapterConfig.courseid; var clazzId = chapterConfig.clazzId; if (isTaskCompleted(obj)) { log(' 文档:' + name + ' 已完成,跳过', 'green'); completeCurrentTask(); return; } $.ajax({ url: _l.protocol + "//" + _l.host + '/ananas/job/document?jobid=' + jobId + '&knowledgeid=' + knowledgeId + '&courseid=' + courseId + '&clazzid=' + clazzId + '&jtoken=' + jtoken + '&_dc=' + Date.now(), method: 'GET', success: function(res) { log(' 文档:' + name + (res.msg || '完成'), 'green'); completeCurrentTask(); }, error: function() { log(' 文档:' + name + ' 处理失败,3秒后重试', 'red'); isProcessing = false; setTimeout(function() { processDocument(dom, obj); }, 3000); } }); } function processReading(dom, obj) { var jobId = obj.property.jobid; var name = obj.property.title; var jtoken = obj.jtoken; var knowledgeId = chapterConfig.knowledgeid; var courseId = chapterConfig.courseid; var clazzId = chapterConfig.clazzId; if (isTaskCompleted(obj)) { log(' 阅读:' + name + ' 已完成,跳过', 'green'); completeCurrentTask(); return; } $.ajax({ url: _l.protocol + '//' + _l.host + '/ananas/job/readv2?jobid=' + jobId + '&knowledgeid=' + knowledgeId + '&courseid=' + courseId + '&clazzid=' + clazzId + '&jtoken=' + jtoken + '&_dc=' + Date.now(), method: 'GET', success: function(res) { log(' 阅读:' + name + (res.msg || '完成'), 'green'); completeCurrentTask(); }, error: function() { log(' 阅读:' + name + ' 处理失败,3秒后重试', 'red'); isProcessing = false; setTimeout(function() { processReading(dom, obj); }, 3000); } }); } // ========== 旧版→新版自动跳转 ========== function handleExperienceNew() { var expLink = document.querySelector('li>a.experience:not([onclick])'); if (expLink) { expLink.click(); log(' 已自动点击"体验新版",跳转新版页面', 'green'); return; } var links = document.querySelectorAll('a.experience, .experience'); for (var i = 0; i < links.length; i++) { if (links[i].textContent.indexOf('新版') !== -1) { links[i].click(); log(' 已自动点击"体验新版",跳转新版页面', 'green'); return; } } var allLinks = document.querySelectorAll('a, button, .btn, [onclick]'); for (var i = 0; i < allLinks.length; i++) { var text = allLinks[i].textContent || allLinks[i].innerText || ''; if (text.indexOf('体验新版') !== -1) { allLinks[i].click(); log(' 已自动点击"新版"按钮,跳转新版页面', 'green'); return; } } if (_l.host === 'i.chaoxing.com' && _l.pathname.indexOf('/base') !== -1) { log(' 旧版个人页,直接跳转新版...', 'green'); _l.href = 'https://mooc1.chaoxing.com/visit/interaction'; return; } log('ℹ️ 未找到"体验新版"按钮,可能已是新版', 'blue'); } // ========== 任务调度(每次开始前强制清理残留)========== function dispatchNextTask() { if (isProcessing) { log(' 已有任务在处理中,跳过', 'orange'); return; } if (!taskQueue || taskQueue.length <= 0) { log(' 此页面任务处理完毕', 'green'); isProcessing = false; navigateForward(); return; } // 检查是否有apiKey(视频任务不需要,答题任务需要) var nextType = taskQueue[0].type || (taskQueue[0].property && taskQueue[0].property.module); if (nextType === 'workid' && !hasApiKey()) { log(' 未登录,无法处理答题任务。请先在面板中登录', 'red'); return; } // 关键:每次开始新任务前,强制清理所有残留资源 forceCleanupAll(); isProcessing = true; var _type = taskQueue[0].type; var _dom = attachFrames[0]; var _task = taskQueue[0]; if (_type == undefined) { _type = taskQueue[0].property.module; } var taskName = _task.property ? (_task.property.name || _task.property.title || '未知') : '未知'; log(' 开始处理任务: ' + taskName + ' (类型: ' + _type + ')', 'blue'); var handlers = { 'video': () => processVideo(_dom, _task), 'audio': () => processAudio(_dom, _task), 'workid': () => processQuiz(_dom, _task), 'document': () => processDocument(_dom, _task), 'read': () => processReading(_dom, _task), 'insertbook': () => processBook(_dom, _task) }; if (handlers[_type]) { handlers[_type](); } else if (['insertimage'].includes(_type)) { log('ℹ️ 发现无需处理任务,跳过', 'orange'); completeCurrentTask(); } else { log(' 暂不支持处理此类型:' + _type + ',跳过', 'red'); completeCurrentTask(); } } // ========== API答题 ========== function getAnswer(type, questionText, optionsData, _retried) { return new Promise((resolve, reject) => { var apiKey = getApiKey(); if (!apiKey || apiKey.trim() === '') { if (!_retried && isLoggedIn()) { log(' API密钥为空,自动刷新中...', 'orange'); apiFetchUserInfo().then(function() { getAnswer(type, questionText, optionsData, true).then(resolve).catch(reject); }).catch(function() { log(' 刷新失败,请手动重新登录', 'red'); reject('NO_API_KEY'); }); return; } log(' 请先注册或设置API密钥', 'red'); reject('NO_API_KEY'); return; } var typeMap = {0:'单选题',1:'多选题',2:'填空题',3:'判断题',4:'简答题'}; var typeText = typeMap[type] || '单选题'; log(' 请求答案 - 题型:' + typeText + ' 题目:' + questionText.substring(0, 40) + '...', 'blue'); var payload = { question: questionText, apiKey: apiKey, type: typeText }; if (optionsData && optionsData.length) { payload.options = JSON.stringify(optionsData); } GM_xmlhttpRequest({ method: 'POST', url: API_URL, headers: {'Content-Type': 'application/json'}, data: JSON.stringify(payload), timeout: 15000, onload: function(xhr) { if (xhr.status === 401) { clearAuth(); log(' 登录已过期,请重新登录', 'orange'); reject('TOKEN_EXPIRED'); return; } try { var res = JSON.parse(xhr.responseText); if (res.code == 200) { var d = res.data || {}; if (d.remainingCount !== undefined) { updateQuotaInfo({ remaining: d.remainingCount }); } var answer = d.answer; if (answer) { logAnswer(questionText, answer, type); resolve(answer); } else { log(' 未找到答案', 'orange'); resolve('暂无答案'); } } else if (res.code == 500 && res.msg && res.msg.indexOf('凭证') !== -1 && !_retried) { log(' 凭证无效,自动刷新中...', 'orange'); apiFetchUserInfo().then(function() { log(' 凭证已刷新,正在重试...', 'green'); getAnswer(type, questionText, optionsData, true).then(resolve).catch(reject); }).catch(function() { log(' 刷新凭证失败,请手动重新登录', 'red'); reject('凭证刷新失败'); }); return; } else if (res.code == 500) { log(' ' + (res.msg || 'API错误'), 'red'); reject(res.msg || 'API_ERROR'); } else { log(' 接口异常: ' + (res.msg || '未知错误'), 'red'); reject('接口异常'); } } catch(e) { log(' 解析响应失败', 'red'); reject('解析响应失败'); } }, onerror: function() { log(' 网络请求失败', 'red'); reject('网络请求失败'); }, ontimeout: function() { log(' 请求超时', 'red'); reject('请求超时'); } }); }); } // ========== 测验处理 ========== function processQuiz(dom, obj) { if (!cfg('work')) { log('ℹ️ 用户设置不自动处理测验', 'orange'); completeCurrentTask(); return; } if (!checkApiKeyBeforeAction('自动答题')) { completeCurrentTask(); return; } var isDo = true; if (cfg('task') && obj.jobid == undefined) { isDo = false; } if (isDo) { if (obj.jobid !== undefined) { var quizPageUrl = _l.protocol + '//' + _l.host + '/work/phone/work?workId=' + obj.jobid.replace('work-', '') + '&courseId=' + chapterConfig.courseid + '&clazzId=' + chapterConfig.clazzId + '&knowledgeId=' + chapterConfig.knowledgeid + '&jobId=' + obj.jobid + '&enc=' + obj.enc; log(' 准备处理测验', 'purple'); setTimeout(function() { iteratePhoneFrames(0, [dom], quizPageUrl, obj); }, 3000); } else { setTimeout(function() { iterateQuizFrames(0, [dom], obj); }, 3000); } } else { log('ℹ️ 用户设置只处理属于任务点的任务', 'orange'); completeCurrentTask(); } } // ========== 手机端测验处理 ========== var _phoneRetryCount = 0; var _PHONE_MAX_RETRY = 6; // 最多重试6次(共30秒) function iteratePhoneFrames(index, doms, quizPageUrl, taskObj) { if (index == doms.length) { log(' 此页面全部测验已处理完毕', 'green'); completeCurrentTask(); return; } var $outerIframe = $(doms[index]); var outerDoc = null; try { outerDoc = $outerIframe.contents()[0]; } catch(e) { log(' 无法访问iframe内容(可能跨域): ' + e.message, 'orange'); } if (!outerDoc) { _phoneRetryCount++; if (_phoneRetryCount > _PHONE_MAX_RETRY) { log(' 无法访问任务iframe内容,已重试' + _PHONE_MAX_RETRY + '次,跳过此任务', 'red'); _phoneRetryCount = 0; completeCurrentTask(); return; } log('⏳ 等待iframe加载... (第' + _phoneRetryCount + '次)', 'blue'); setTimeout(function() { iteratePhoneFrames(index, doms, quizPageUrl, taskObj); }, 5000); return; } // 先尝试在外层iframe内查找嵌套的iframe awaitElement(outerDoc, 'iframe').then(function(iframe) { if (!iframe) { // 找不到内层iframe,尝试直接使用外层iframe本身作为工作iframe(适配单层结构) log(' 未找到内层iframe,尝试直接使用外层iframe', 'blue'); var workStatus = ''; try { workStatus = $outerIframe.contents().find('.newTestCon .newTestTitle .testTit_status').text().trim(); } catch(e) {} if (!workStatus) { // 也尝试找 .CeYan(PC端测验容器) var hasCeYan = false; try { hasCeYan = $outerIframe.contents().find('.CeYan').length > 0; } catch(e) {} if (hasCeYan) { log(' 检测到PC端测验结构(.CeYan),切换到PC模式处理', 'purple'); setTimeout(function() { loadQuizFrame(index, doms, doms[index], taskObj); }, 2000); return; } _phoneRetryCount++; if (_phoneRetryCount > _PHONE_MAX_RETRY) { log(' 找不到测验状态和内层iframe,已重试' + _PHONE_MAX_RETRY + '次,跳过', 'red'); _phoneRetryCount = 0; attachFrames.splice(0, 1); isProcessing = false; setTimeout(advanceToNext, 2000); return; } log('⏳ 等待测验内容加载... (第' + _phoneRetryCount + '次)', 'blue'); setTimeout(function() { iteratePhoneFrames(index, doms, quizPageUrl, taskObj); }, 5000); return; } // 找到了状态,直接在外层iframe上处理 _phoneRetryCount = 0; handlePhoneWorkStatus($outerIframe, workStatus, index, doms, quizPageUrl, taskObj, outerDoc); return; } // 找到了内层iframe,正常流程 _phoneRetryCount = 0; var workIframe = $(iframe); var workStatus = ''; try { workStatus = workIframe.contents().find('.newTestCon .newTestTitle .testTit_status').text().trim(); } catch(e) { log(' 无法读取内层iframe状态: ' + e.message, 'orange'); } if (!workStatus) { attachFrames.splice(0, 1); isProcessing = false; setTimeout(advanceToNext, 2000); return; } handlePhoneWorkStatus(workIframe, workStatus, index, doms, quizPageUrl, taskObj, outerDoc); }); } function handlePhoneWorkStatus(workIframe, workStatus, index, doms, quizPageUrl, taskObj, outerDoc) { log(' 测验状态: ' + workStatus, 'blue'); if (workStatus.indexOf("待做") != -1 || workStatus.indexOf("待完成") != -1 || workStatus.indexOf("未达到及格线") != -1) { log(' 设置手机端测验URL并等待加载...', 'blue'); workIframe.attr('src', quizPageUrl); // 判断是否是外层iframe直接作为工作iframe(单层结构) var isOuterAsWork = (workIframe[0] === $(doms[index])[0]); if (isOuterAsWork) { // 单层结构:外层iframe的src被改为quizPageUrl,需要等它加载完 workIframe.on('load', function() { workIframe.off('load'); setTimeout(function() { try { solvePhoneQuiz(workIframe.contents(), taskObj); } catch(e) { log(' 手机端测验页面加载异常: ' + e.message, 'orange'); completeCurrentTask(); } }, 2000); }); // 超时兜底 setTimeout(function() { workIframe.off('load'); try { var $c = workIframe.contents(); if ($c && $c.find('.Wrappadding').length > 0) { solvePhoneQuiz($c, taskObj); } else { log(' 手机端页面加载超时,尝试处理...', 'orange'); solvePhoneQuiz($c, taskObj); } } catch(e) { log(' 手机端测验加载失败: ' + e.message, 'red'); completeCurrentTask(); } }, 15000); } else { // 双层结构:内层iframe的src被修改,等待它出现 awaitElement(outerDoc, 'iframe[src="' + quizPageUrl + '"]').then(function() { setTimeout(function() { solvePhoneQuiz(workIframe.contents(), taskObj); }, 3000); }); } } else if (workStatus.indexOf('待批阅') != -1) { taskQueue.splice(0, 1); attachFrames.splice(0, 1); log(' 测验待批阅,跳过', 'red'); completeCurrentTask(); } else if (workStatus.indexOf('已完成') != -1 || workStatus.indexOf('已交') != -1) { log(' 测验已完成,跳过', 'green'); completeCurrentTask(); } else { taskQueue.splice(0, 1); attachFrames.splice(0, 1); log(' 未知状态: ' + workStatus + ',跳过', 'red'); completeCurrentTask(); } } function solvePhoneQuiz($dom, taskObj) { // 答题前先解密iframe内的加密字体 deobfuscateFontInFrame($dom); var $cy = $dom.find('.Wrappadding form'); $subBtn = $cy.find('.zquestions .zsubmit .btn-ok-bottom'); $okBtn = $dom.find('#okBtn'); $saveBtn = $cy.find('.zquestions .zsubmit .btn-save'); var questionList = $cy.find('.zquestions .Py-mian1'); if (!questionList || questionList.length === 0) { log(' 未找到题目列表(.Py-mian1),尝试其他选择器...', 'orange'); // 尝试其他可能的选择器 questionList = $dom.find('.Py-mian1'); if (!questionList || questionList.length === 0) { questionList = $dom.find('.questionLi'); } if (!questionList || questionList.length === 0) { log(' 无法找到题目元素,跳过此测验', 'red'); completeCurrentTask(); return; } } log(' 找到 ' + questionList.length + ' 道题目,开始答题', 'purple'); iteratePhoneQuestions(0, questionList, taskObj); } function isQuestionAnswered($timu, _type) { if (_type == 0 || _type == 1) { var $opts = _type == 0 ? $timu.find('.answerList.singleChoice li') : $timu.find('.answerList.multiChoice li'); for (var i = 0; i < $opts.length; i++) { if ($($opts[i]).attr('aria-label')) return true; } } else if (_type == 2) { var $inputs = $timu.find('.blankList2 input'); if ($inputs.length && $inputs.first().val() && $inputs.first().val().trim() !== '') return true; // 检查编辑器填空 var $editorBlocks = $timu.find('[data-editorindex]'); if ($editorBlocks.length) { var hasContent = false; $editorBlocks.each(function() { var itemId = $(this).attr('data-itemid'); if (itemId && $('#answer' + itemId).val() && $('#answer' + itemId).val().trim() !== '') { hasContent = true; } }); if (hasContent) return true; } } else if (_type == 3) { var $pd = $timu.find('.answerList.panduan li'); for (var i = 0; i < $pd.length; i++) { if ($($pd[i]).attr('aria-label')) return true; } } else if (_type == 4) { var $ta = $timu.find('textarea[name^="answer"]'); if ($ta.length && $ta.first().val() && $ta.first().val().trim() !== '') return true; } return false; } function getQuestionType($timu, qTypeName) { var typeMap = { '单选题': 0, '多选题': 1, '填空题': 2, '判断题': 3, '简答题': 4 }; var _type = typeMap[qTypeName]; if (_type === undefined) { // 手机端选择器 if ($timu.find('.answerList.singleChoice li').length) _type = 0; else if ($timu.find('.answerList.multiChoice li').length) _type = 1; else if ($timu.find('.blankList2 input').length) _type = 2; else if ($timu.find('.answerList.panduan li').length) _type = 3; // PC端选择器(章节测验) else if ($timu.find('.Zy_ulTop li').length) { var hasCheckbox = $timu.find('.Zy_ulTop li input[type="checkbox"]').length > 0; _type = hasCheckbox ? 1 : 0; } else if ($timu.find('textarea').length) _type = 4; } return _type; } function selectPhoneJudge($timu, answer) { var trueList = ['正确', '是', '对', '√', 'T', 'true', 'ri', 'right']; var ansLower = (answer || '').trim().toLowerCase(); var isTrue = trueList.some(function(k) { return ansLower === k.toLowerCase() || ansLower.indexOf(k.toLowerCase()) !== -1; }); var $pd = $timu.find('.answerList.panduan li'); if (isTrue) { $pd.each(function(i, t) { if ($(t).attr('val-param') == 'true') { $(t).click(); log(' 手机端判断题选择: 正确 (接口返回: ' + answer + ')', 'green'); } }); } else { $pd.each(function(i, t) { if ($(t).attr('val-param') == 'false') { $(t).click(); log(' 手机端判断题选择: 错误 (接口返回: ' + answer + ')', 'green'); } }); } } function iteratePhoneQuestions(index, questionList, taskObj) { if (index == questionList.length) { submitQuizForm(); return; } var frameWin = questionList[index] ? (questionList[index]?.ownerDocument?.defaultView ?? _w) : unsafeWindow; var $timu = $(questionList[index]); var rawQuestion = $timu.find('.Py-m1-title').html(); var cleanedQ = sanitizeQuestion(rawQuestion).replace(/.*?\[.*?题\]\s*\n\s*/, '').trim(); var qTypeName = rawQuestion.match(/.*?\[(.*?)]|$/)[1]; var _type = getQuestionType($timu, qTypeName); if (isQuestionAnswered($timu, _type)) { log(' 第' + (index + 1) + '题已作答,跳过', 'green'); setTimeout(() => iteratePhoneQuestions(index + 1, questionList, taskObj), 30); return; } var $opts = []; var optsText = []; var optionsData = []; var pureQuestion = cleanedQ; if (_type == 0 || _type == 1) { $opts = _type == 0 ? $timu.find('.answerList.singleChoice li') : $timu.find('.answerList.multiChoice li'); $opts.each(function() { optsText.push(sanitizeText($(this).html()).replace(/^[A-Z]\s*\n\s*/, '').trim()); }); optionsData = optsText; } else if (_type == 2) { var blankCount = getBlankInputCount($timu); optionsData = [blankCount.toString()]; log(' 填空题传入空数量: ' + blankCount, 'blue'); } else if (_type == 3) { optionsData = ["对", "错"]; } else if (_type == 4) { optionsData = ["1"]; } getAnswer(_type, pureQuestion, optionsData).then((agrs) => { if (agrs == '暂无答案' || !agrs) { log(' 无法匹配正确答案,跳过此题', 'red'); setTimeout(() => iteratePhoneQuestions(index + 1, questionList, taskObj), cfg('time')); return; } if (false) { $timu.find('.Py-m1-title').html($timu.find('.Py-m1-title').html() + '' + agrs + '
'); } if (_type == 0) { var idx = optsText.findIndex(t => t == agrs); if (idx != -1) $timu.find('.answerList.singleChoice li').eq(idx).click(); else log(' 未找到匹配选项: ' + agrs, 'orange'); } else if (_type == 1) { var ansArr = splitAnswer(agrs); $opts.each((i, t) => { if (ansArr.includes(optsText[i])) { setTimeout(() => $(t).click(), 300); } }); } else if (_type == 2) { fillBlankAnswer($timu, agrs, true, frameWin); } else if (_type == 3) { selectPhoneJudge($timu, agrs); } else if (_type == 4) { var filled = fillShortAnswer($timu, agrs, true, frameWin); if (!filled) { log(' 第' + (index + 1) + '题答案未能填入,跳过', 'orange'); setTimeout(() => iteratePhoneQuestions(index + 1, questionList, taskObj), cfg('time')); return; } } log(' 第' + (index + 1) + '题自动答题成功', 'green'); setTimeout(() => iteratePhoneQuestions(index + 1, questionList, taskObj), cfg('time')); }).catch((err) => { if (err === 'NO_API_KEY' || err === 'TOKEN_EXPIRED' || err === '凭证刷新失败') { log(' 未登录,停止答题。请先登录后再使用', 'red'); isProcessing = false; return; } log(' 第' + (index + 1) + '题获取答案失败,跳过', 'orange'); setTimeout(() => iteratePhoneQuestions(index + 1, questionList, taskObj), cfg('time')); }); } // ========== PC端测验处理 ========== var _pcRetryCount = 0; var _PC_MAX_RETRY = 6; function iterateQuizFrames(index, doms, taskObj) { if (index == doms.length) { log(' 此页面全部测验已处理完毕', 'green'); completeCurrentTask(); return; } var $outerIframe = $(doms[index]); var outerDoc = null; try { outerDoc = $outerIframe.contents()[0]; } catch(e) { log(' PC端:无法访问iframe内容(可能跨域): ' + e.message, 'orange'); } if (!outerDoc) { _pcRetryCount++; if (_pcRetryCount > _PC_MAX_RETRY) { log(' PC端:无法访问任务iframe内容,已重试' + _PC_MAX_RETRY + '次,跳过', 'red'); _pcRetryCount = 0; completeCurrentTask(); return; } log('⏳ PC端:等待iframe加载... (第' + _pcRetryCount + '次)', 'blue'); setTimeout(function() { iterateQuizFrames(index, doms, taskObj); }, 5000); return; } awaitElement(outerDoc, 'iframe').then(function(iframe) { if (!iframe) { // 找不到内层iframe,尝试直接使用外层iframe(单层结构兼容) log(' PC端:未找到内层iframe,尝试直接使用外层iframe', 'blue'); var hasCeYan = false; try { hasCeYan = $outerIframe.contents().find('.CeYan').length > 0; } catch(e) {} if (hasCeYan) { log(' PC端:检测到测验结构(.CeYan),直接处理', 'purple'); _pcRetryCount = 0; setTimeout(function() { loadQuizFrame(index, doms, doms[index], taskObj); }, 2000); return; } _pcRetryCount++; if (_pcRetryCount > _PC_MAX_RETRY) { log(' PC端:找不到测验内容,已重试' + _PC_MAX_RETRY + '次,跳过', 'red'); _pcRetryCount = 0; attachFrames.splice(0, 1); isProcessing = false; setTimeout(advanceToNext, 2000); return; } log('⏳ PC端:等待内层iframe加载... (第' + _pcRetryCount + '次)', 'blue'); setTimeout(function() { iterateQuizFrames(index, doms, taskObj); }, 5000); return; } _pcRetryCount = 0; var workIframe = $(iframe); var workStatus = ''; try { workStatus = workIframe.contents().find(".newTestCon .newTestTitle .testTit_status").text().trim(); } catch(e) { log(' PC端:无法读取内层iframe状态: ' + e.message, 'orange'); } if (!workStatus) { attachFrames.splice(0, 1); isProcessing = false; setTimeout(advanceToNext, 2000); return; } if (workStatus.indexOf("待做") != -1 || workStatus.indexOf("待完成") != -1) { log(' 准备处理测验 (状态: ' + workStatus + ')', 'purple'); setTimeout(function() { loadQuizFrame(index, doms, iframe, taskObj); }, 5000); } else if (workStatus.indexOf('待批阅') != -1) { taskQueue.splice(0, 1); attachFrames.splice(0, 1); log(' 测验待批阅,跳过', 'red'); completeCurrentTask(); } else if (workStatus.indexOf('已完成') != -1 || workStatus.indexOf('已交') != -1) { log(' 测验已完成,跳过', 'green'); completeCurrentTask(); } else { taskQueue.splice(0, 1); attachFrames.splice(0, 1); log(' 未知状态: ' + workStatus + ',跳过', 'red'); completeCurrentTask(); } }); } function loadQuizFrame(index, doms, dom, taskObj) { quizFrameEl = dom; // 保存iframe元素引用,供submitQuizForm获取contentWindow $frame_c = $(dom).contents(); // 答题前先解密iframe内的加密字体 deobfuscateFontInFrame($frame_c); var $CyHtml = $frame_c.find('.CeYan'); var TiMuList = $CyHtml.find('.TiMu'); if (!TiMuList || TiMuList.length === 0) { log(' PC端:未找到题目列表(.TiMu),检查页面结构', 'orange'); log(' .CeYan元素数: ' + $CyHtml.length + ', iframe内容长度: ' + ($frame_c.find('body').html() || '').length, 'blue'); } else { log(' PC端:找到 ' + TiMuList.length + ' 道题目,开始答题', 'purple'); } $subBtn = $frame_c.find(".ZY_sub").find(".btnSubmit"); $saveBtn = $frame_c.find(".ZY_sub").find(".btnSave"); iterateQuestions(0, TiMuList); } function selectPCJudge($timu, answer) { var trueList = ['正确', '是', '对', '√', 'T', 'true', 'ri', 'right']; var ansLower = (answer || '').trim().toLowerCase(); var isTrue = trueList.some(function(k) { return ansLower === k.toLowerCase() || ansLower.indexOf(k.toLowerCase()) !== -1; }); var $pdContainer = $timu.find('.Zy_ulTop li'); if ($pdContainer.length === 0) { $pdContainer = $timu.find('.stem_answer .answer_p').parent(); } function selectJudgeOption(el) { if (!el) return; var dom = el.jquery ? el[0] : el; if (!dom) return; // 如果已经选中,不再操作(避免取消选择) if (dom.classList.contains('on') || dom.classList.contains('cur') || dom.classList.contains('selected') || dom.getAttribute('aria-checked') === 'true') { return; } var iframeWin = dom.ownerDocument.defaultView; var iframe$ = iframeWin && iframeWin.jQuery ? iframeWin.jQuery : $; // 直接操作DOM: 清除其他选项,选中当前 $(dom).siblings('li').removeClass('on'); $(dom).addClass('on'); try { iframe$(dom).siblings('li').removeClass('on'); } catch(e) {} try { iframe$(dom).addClass('on'); } catch(e) {} // 处理 radio input var inputs = dom.querySelectorAll('input[type="radio"], input[type="checkbox"]'); for (var i = 0; i < inputs.length; i++) { inputs[i].checked = true; try { inputs[i].dispatchEvent(new Event('change', {bubbles: true})); } catch(e) {} } // 更新隐藏答案字段 var $tm = $(dom).closest('.TiMu'); if ($tm.length) { var ansVal = ''; $tm.find('.Zy_ulTop li.on').each(function(i) { var letter = String.fromCharCode(65 + $(this).index()); ansVal += (i > 0 ? ',' : '') + letter; }); var $ansInput = $tm.find('.Zy_ulBottom input[name^="answer"]'); if ($ansInput.length) $ansInput.val(ansVal); var $ansTa = $tm.find('.Zy_ulBottom textarea[name^="answer"]'); if ($ansTa.length) $ansTa.val(ansVal); } // 补充点击尝试 try { iframe$(dom).click(); } catch(e) {} try { dom.click(); } catch(e) {} try { dom.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true, view: iframeWin})); } catch(e) {} } if (isTrue) { var clicked = false; $pdContainer.each(function(i, t) { var valAttr = $(t).find('span').attr('val-param') || $(t).attr('val-param'); if (valAttr == 'true') { selectJudgeOption(t); clicked = true; log(' PC端判断题选择: 正确 (接口返回: ' + answer + ')', 'green'); } }); if (!clicked && $pdContainer.length > 0) { selectJudgeOption($pdContainer[0]); log(' PC端判断题选择: 正确(默认第一个) (接口返回: ' + answer + ')', 'green'); } } else { var clicked = false; $pdContainer.each(function(i, t) { var valAttr = $(t).find('span').attr('val-param') || $(t).attr('val-param'); if (valAttr == 'false') { selectJudgeOption(t); clicked = true; log(' PC端判断题选择: 错误 (接口返回: ' + answer + ')', 'green'); } }); if (!clicked && $pdContainer.length > 1) { selectJudgeOption($pdContainer[1]); log(' PC端判断题选择: 错误(默认第二个) (接口返回: ' + answer + ')', 'green'); } else if (!clicked && $pdContainer.length === 1) { selectJudgeOption($pdContainer[0]); log(' PC端判断题选择: 错误(唯一选项) (接口返回: ' + answer + ')', 'green'); } } } // 关闭所有可能残留的确认弹窗(iframe内、顶层、父级) function dismissPopups(iframeWin) { // 可安全删除的弹窗(iframe内部/动态生成的) var removableSelectors = '#confirmSubWin, .layui-layer, .layui-layer-shade, .layui-layer-move, .dialog-box, .el-overlay, .ui-dialog, .ui-widget-overlay'; // 只隐藏不删除的弹窗(页面级复用组件) var hideOnlySelectors = '#workpop, .maskDiv, .mask, .popbg, #popWin, .popDiv, .AlertCon02, .modal'; // 关闭iframe内弹窗(可删除) if (iframeWin) { try { var iframeDoc = iframeWin.document; iframeDoc.querySelectorAll(removableSelectors).forEach(function(el) { el.style.display = 'none'; el.remove(); }); iframeDoc.querySelectorAll(hideOnlySelectors).forEach(function(el) { el.style.display = 'none'; }); } catch(e) {} } // 关闭$frame_c中的弹窗 if ($frame_c && $frame_c.length) { try { $frame_c.find(removableSelectors).hide().remove(); $frame_c.find(hideOnlySelectors).hide(); } catch(e) {} } // 关闭顶层、父级、当前页面的弹窗(只隐藏不删除,这些是页面级组件) var allDocs = [document]; try { allDocs.push(_w.top.document); } catch(e) {} try { allDocs.push(_w.parent.document); } catch(e) {} allDocs.forEach(function(doc) { try { $(doc).find(hideOnlySelectors).hide(); $(doc).find(removableSelectors).hide().remove(); } catch(e) {} }); log(' 已清理确认弹窗', 'gray'); } function submitQuizForm() { if (cfg('sub') || cfg('force')) { log(' 测验处理完成,准备自动提交', 'green'); // 获取iframe的window并覆盖alert/confirm,阻止提交确认弹窗残留 var iframeWin = null; try { if (quizFrameEl && quizFrameEl.contentWindow) { iframeWin = quizFrameEl.contentWindow; } else if ($frame_c && $frame_c.length) { iframeWin = $frame_c[0].defaultView || $frame_c[0].parentWindow; } } catch(e) {} // 覆盖iframe内的alert和confirm,阻止弹窗卡住页面 if (iframeWin) { try { iframeWin.alert = function() {}; iframeWin.confirm = function() { return true; }; } catch(e) {} } // 同时覆盖顶层和父级的confirm/alert以防万一 try { var _oldTopConfirm = _w.top.confirm; _w.top.confirm = function(msg) { if (msg && (msg.includes('确认提交') || msg.includes('未做完'))) return true; return _oldTopConfirm ? _oldTopConfirm(msg) : true; }; } catch(e) {} try { var _oldParentConfirm = _w.parent.confirm; _w.parent.confirm = function(msg) { if (msg && (msg.includes('确认提交') || msg.includes('未做完'))) return true; return _oldParentConfirm ? _oldParentConfirm(msg) : true; }; } catch(e) {} setTimeout(() => { // 直接调用iframe内原生提交函数 var usedNativeSubmit = false; if (iframeWin) { try { if (typeof iframeWin.btnBlueSubmit === 'function') { iframeWin.btnBlueSubmit(); usedNativeSubmit = true; log(' 调用btnBlueSubmit()提交', 'blue'); setTimeout(() => { try { if (typeof iframeWin.submitCheckTimes === 'function') { iframeWin.submitCheckTimes(); log(' 调用submitCheckTimes()确认提交成功', 'green'); } else { log(' 提交成功(无需submitCheckTimes)', 'green'); } } catch(e) { log(' 提交成功', 'green'); } // 延时关闭弹窗,确保提交请求已发出 setTimeout(() => { dismissPopups(iframeWin); completeCurrentTask(); }, 2000); }, 1500); return; // 已用原生函数提交,不走DOM点击流程 } } catch(e) {} } // 回退方式:DOM点击提交按钮 if ($subBtn && $subBtn.length) $subBtn.click(); setTimeout(() => { var confirmed = false; // 方式1: PC端确认弹窗在测验iframe内 (#confirmSubWin) if (!confirmed && $frame_c && $frame_c.length) { var $confirmBtn = $frame_c.find('#confirmSubWin > div > div > a.bluebtn'); if ($confirmBtn.length) { $confirmBtn.click(); confirmed = true; } } // 方式1.5: 尝试调用submitCheckTimes确认 if (!confirmed && iframeWin) { try { if (typeof iframeWin.submitCheckTimes === 'function') { iframeWin.submitCheckTimes(); confirmed = true; } } catch(e) {} } // 方式2: 顶层页面的确认弹窗 (#popok) if (!confirmed) { try { var topDoc = _w.top.document; var $popok = $(topDoc).find('#popok'); if ($popok.length && $popok.is(':visible')) { $popok.click(); confirmed = true; } } catch(e) {} } // 方式3: 父级页面的确认弹窗 if (!confirmed) { try { var parentDoc = _w.parent.document; var $popok = $(parentDoc).find('#popok'); if ($popok.length && $popok.is(':visible')) { $popok.click(); confirmed = true; } } catch(e) {} } // 方式4: 手机端确认按钮 if (!confirmed && $okBtn && $okBtn.length) { $okBtn.click(); confirmed = true; } // 强制关闭所有可能的确认弹窗 dismissPopups(iframeWin); if (confirmed) { log(' 提交成功', 'green'); } else { log(' 未找到提交确认按钮,弹窗已被拦截', 'orange'); } completeCurrentTask(); }, 3000); }, 5000); } else { log(' 测验处理完成,自动保存', 'green'); setTimeout(() => { if ($saveBtn && $saveBtn.length) $saveBtn.click(); log(' 保存成功', 'green'); completeCurrentTask(); }, 5000); } } function clickPCOption($opts, idx, isSingle) { var aEl = $opts[idx]; if (!aEl) { log(' clickPCOption: aEl为空, idx=' + idx, 'orange'); return; } var liEl = $(aEl).parent()[0]; if (!liEl) { log(' clickPCOption: liEl为空', 'orange'); return; } // 如果选项已经是选中状态,不再点击(避免取消选择) if (liEl.classList.contains('on') || liEl.classList.contains('cur') || liEl.classList.contains('selected') || liEl.getAttribute('aria-checked') === 'true') { var spanCheck = liEl.querySelector('span.num_option_dx') || liEl.querySelector('span.num_option'); if (!spanCheck || spanCheck.classList.contains('cur') || spanCheck.classList.contains('selected')) { return; } } var iframeDoc = aEl.ownerDocument; var iframeWin = iframeDoc.defaultView; var iframe$ = iframeWin && iframeWin.jQuery ? iframeWin.jQuery : $; // 策略: 直接操作DOM状态 + 多种点击方式并用 // 超星章节测验选中选项时,li 会加上 class "on" // 1. 单选题先清除同组其他选项的选中状态 if (isSingle !== false) { $(liEl).siblings('li').removeClass('on'); try { iframe$(liEl).siblings('li').removeClass('on'); } catch(e) {} } // 2. 给当前选项添加 "on" class $(liEl).addClass('on'); try { iframe$(liEl).addClass('on'); } catch(e) {} // 3. 处理 radio/checkbox input var inputs = liEl.querySelectorAll('input[type="radio"], input[type="checkbox"]'); for (var i = 0; i < inputs.length; i++) { inputs[i].checked = true; try { var changeEvt = new Event('change', {bubbles: true}); inputs[i].dispatchEvent(changeEvt); } catch(e) {} } // 4. 更新隐藏的答案字段(超星用 .Zy_ulBottom textarea 或 input[name^="answer"] 存答案) var $timu = $(liEl).closest('.TiMu'); if ($timu.length) { var answerVal = ''; var $allLi = $timu.find('.Zy_ulTop li.on'); $allLi.each(function(i) { var letter = String.fromCharCode(65 + $(this).index()); answerVal += (i > 0 ? ',' : '') + letter; }); var $ansInput = $timu.find('.Zy_ulBottom input[name^="answer"]'); if ($ansInput.length) { $ansInput.val(answerVal); try { iframe$($ansInput[0]).val(answerVal).trigger('change'); } catch(e) {} } var $ansTa = $timu.find('.Zy_ulBottom textarea[name^="answer"]'); if ($ansTa.length) { $ansTa.val(answerVal); try { iframe$($ansTa[0]).val(answerVal).trigger('change'); } catch(e) {} } } else { log(' clickPCOption: 未找到.TiMu父元素', 'orange'); } // 5. 仍然尝试各种点击方式作为补充 try { iframe$(liEl).click(); } catch(e) {} try { liEl.click(); } catch(e) {} try { iframe$(aEl).click(); } catch(e) {} try { aEl.click(); } catch(e) {} try { var evt = new MouseEvent('click', {bubbles: true, cancelable: true, view: iframeWin}); liEl.dispatchEvent(evt); } catch(e) {} } function isQuestionFilledPC($timu, _type) { // 检查隐藏答案字段是否已有值 var $ansInput = $timu.find('.Zy_ulBottom input[name^="answer"]'); if ($ansInput.length && $ansInput.val() && $ansInput.val().trim() !== '') return true; var $ansTa = $timu.find('.Zy_ulBottom textarea[name^="answer"]'); if ($ansTa.length && $ansTa.val() && $ansTa.val().trim() !== '') return true; if (_type == 0 || _type == 1 || _type == 3) { var $lis = $timu.find('.Zy_ulTop li'); for (var i = 0; i < $lis.length; i++) { var li = $lis[i]; // 检查 li 上的选中class if (li.classList.contains('on') || li.classList.contains('cur') || li.classList.contains('selected')) return true; // 检查 aria-checked 属性 if (li.getAttribute('aria-checked') === 'true') return true; // 检查 span.num_option_dx / span.num_option 上的选中class var spanEl = li.querySelector('span.num_option_dx') || li.querySelector('span.num_option') || li.querySelector('span[data]'); if (spanEl && (spanEl.classList.contains('cur') || spanEl.classList.contains('selected'))) return true; // 检查 .onChecked / .check_answer 子元素 if (li.querySelector('.onChecked') || li.querySelector('.check_answer') || li.querySelector('.check_answer_dx')) return true; } } else if (_type == 2) { var answered = false; $timu.find('.Zy_ulTk .XztiHover1 textarea, .stem_answer .Answer .divText .textDIV textarea, .subEditor textarea').each(function() { if ($(this).val() && $(this).val().trim() !== '') answered = true; }); return answered; } else if (_type == 4) { var $ta = $timu.find('textarea'); if ($ta.length && $ta.val() && $ta.val().trim() !== '') return true; return false; } return false; } function iterateQuestions(c, TiMuList) { if (c == TiMuList.length) { submitQuizForm(); return; } var $timu = $(TiMuList[c]); var rawQuestion = $timu.find('.Zy_TItle.clearfix > div').html(); var cleanedQ = sanitizeQuestion(rawQuestion); var qTypeName = rawQuestion ? rawQuestion.match(/【(.*?)】|$/)[1] : undefined; var _TimuType = getQuestionType($timu, qTypeName); // PC端已作答检测,避免重复点击导致取消选择 if (isQuestionFilledPC($timu, _TimuType)) { log(' PC第' + (c + 1) + '题已作答,跳过', 'green'); setTimeout(() => iterateQuestions(c + 1, TiMuList), 30); return; } var $opts = []; var optsText = []; var optionsData = []; var pureQuestion = cleanedQ; if (_TimuType == 0 || _TimuType == 1) { $opts = $timu.find('.Zy_ulTop li a'); $opts.each(function() { optsText.push(sanitizeText($(this).html()).replace(/^[A-Za-z]\s*[\n\r]*\s*/, '').trim()); }); optionsData = optsText; } else if (_TimuType == 2) { var blankCount = getBlankInputCount($timu); optionsData = [blankCount.toString()]; log(' PC端填空题传入空数量: ' + blankCount, 'blue'); } else if (_TimuType == 3) { optionsData = ["对", "错"]; } else if (_TimuType == 4) { optionsData = ["1"]; } getAnswer(_TimuType, pureQuestion, optionsData).then((agrs) => { if (agrs == '暂无答案' || !agrs) { log(' PC第' + (c + 1) + '题无法获取答案,跳过', 'orange'); setTimeout(() => iterateQuestions(c + 1, TiMuList), cfg('time')); return; } if (false) { $timu.find('.Zy_TItle.clearfix > div').html($timu.find('.Zy_TItle.clearfix > div').html() + '' + agrs + '
'); } var matched = false; if (_TimuType == 0) { var ansNorm = agrs.replace(/\s+/g, '').trim(); var idx = optsText.findIndex(t => t == agrs); if (idx == -1) idx = optsText.findIndex(t => t.replace(/\s+/g, '').trim() == ansNorm); if (idx == -1) idx = optsText.findIndex(t => t.indexOf(agrs) != -1 || agrs.indexOf(t) != -1); if (idx != -1) { clickPCOption($opts, idx, true); matched = true; } else { log(' PC第' + (c + 1) + '题未找到匹配选项,答案: [' + agrs + '],选项: [' + optsText.join(' | ') + ']', 'orange'); } } else if (_TimuType == 1) { var ansArr = splitAnswer(agrs); $opts.each((i, t) => { var optNorm = optsText[i].replace(/\s+/g, '').trim(); for (var ai = 0; ai < ansArr.length; ai++) { var aNorm = ansArr[ai].replace(/\s+/g, '').trim(); if (optsText[i] == ansArr[ai] || optNorm == aNorm || optsText[i].indexOf(ansArr[ai]) != -1 || ansArr[ai].indexOf(optsText[i]) != -1) { clickPCOption($opts, i, false); matched = true; break; } } }); if (!matched) { log(' PC第' + (c + 1) + '题多选未匹配,答案: [' + agrs + '],选项: [' + optsText.join(' | ') + ']', 'orange'); } } else if (_TimuType == 2) { fillBlankAnswer($timu, agrs, false, null); matched = true; } else if (_TimuType == 3) { selectPCJudge($timu, agrs); matched = true; } else if (_TimuType == 4) { fillShortAnswer($timu, agrs, false, null); matched = true; } if (matched) { log(' PC第' + (c + 1) + '题自动答题成功', 'green'); } setTimeout(() => iterateQuestions(c + 1, TiMuList), cfg('time')); }).catch((err) => { if (err === 'NO_API_KEY' || err === 'TOKEN_EXPIRED' || err === '凭证刷新失败') { log(' 未登录,停止答题。请先登录后再使用', 'red'); isProcessing = false; return; } log(' PC第' + (c + 1) + '题获取答案失败,跳过', 'orange'); setTimeout(() => iterateQuestions(c + 1, TiMuList), cfg('time')); }); } function advanceToNext() { isProcessing = false; dispatchNextTask(); } // ========== 重做功能 ========== var _redoMode = false; function redoCurrentWork() { log(' 开始重做:忽略已作答状态,重新自动答题...', 'purple'); _redoMode = true; var currentUrl = location.href; if (currentUrl.match(/\/mooc2\/work\/dowork/) || currentUrl.match(/\/work\/phone\/iterateHomeworkQuestions/)) { // 作业页面 - 直接重新答题(覆盖已有答案) processHomeworkPage(); } else if (currentUrl.match(/\/exam\/test\/reVersionTestStartNew/)) { // 考试页面 processExamPage(); } else { // 章节测验/学习页面 - 刷新重新处理 log(' 当前为章节学习页面,刷新页面重新处理...', 'blue'); _redoMode = false; location.reload(); } } // ========== 作业处理 ========== function processHomeworkPage() { log(' 开始处理作业', 'green'); if (!checkApiKeyBeforeAction('作业自动答题')) return; var $_homeworktable = $('.mark_table').find('form'); var questionList = $_homeworktable.find('.questionLi'); iterateHomeworkQuestions(0, questionList); } function iterateHomeworkQuestions(index, TiMuList) { if (index == TiMuList.length) { _redoMode = false; log(' 作业题目已全部完成', 'green'); return; } var qTypeName = $(TiMuList[index]).attr('typename'); var typeMap = { '单选题': 0, '多选题': 1, '填空题': 2, '判断题': 3, '简答题': 4 }; var _type = typeMap[qTypeName]; var _questionFull = $(TiMuList[index]).find('.mark_name').html(); var cleanedQ = sanitizeQuestion(_questionFull).replace(/^[(].*?[)]/, '').trim(); if (_type === undefined) { var optionEls = $(TiMuList[index]).find('.stem_answer').find('.answer_p'); if (optionEls && optionEls.length > 0) { _type = $(TiMuList[index]).find('input[type="checkbox"]').length ? 1 : 0; } else if ($(TiMuList[index]).find('.stem_answer').find('.divText textarea').length) { _type = 4; } } var checkAnswered = function() { if (_type == 0 || _type == 1) { var $opts = $(TiMuList[index]).find('.stem_answer .answer_p'); for (var i = 0; i < $opts.length; i++) { if ($($opts[i]).parent().find('span').attr('class') && $($opts[i]).parent().find('span').attr('class').indexOf('check_answer') != -1) { return true; } } } else if (_type == 2) { var $inputs = $(TiMuList[index]).find('.stem_answer .Answer .divText .textDIV textarea'); if ($inputs.length && $inputs.val() && $inputs.val().trim() !== '') return true; } else if (_type == 3) { var $opts = $(TiMuList[index]).find('.stem_answer .answer_p'); for (var i = 0; i < $opts.length; i++) { if ($($opts[i]).parent().find('span').attr('class') && $($opts[i]).parent().find('span').attr('class').indexOf('check_answer') != -1) { return true; } } } else if (_type == 4) { var $ta = $(TiMuList[index]).find('.stem_answer .eidtDiv textarea'); if ($ta.length && $ta.val() && $ta.val().trim() !== '') return true; } return false; }; if (!_redoMode && checkAnswered()) { log(' 第' + (index + 1) + '题已作答,跳过', 'green'); setTimeout(() => iterateHomeworkQuestions(index + 1, TiMuList), 30); return; } var $opts = []; var optsText = []; var optionsData = []; var pureQuestion = cleanedQ; if (_type == 0 || _type == 1) { $opts = $(TiMuList[index]).find('.stem_answer .answer_p'); $opts.each(function() { optsText.push(sanitizeText($(this).html())); }); optionsData = optsText; } else if (_type == 2) { var blankCount = getBlankInputCount($(TiMuList[index])); optionsData = [blankCount.toString()]; log(' 作业填空题传入空数量: ' + blankCount, 'blue'); } else if (_type == 3) { optionsData = ["对", "错"]; } else if (_type == 4) { optionsData = ["1"]; } getAnswer(_type, pureQuestion, optionsData).then((agrs) => { if (agrs == '暂无答案' || !agrs) { log(' 作业第' + (index + 1) + '题无法获取答案,跳过', 'orange'); setTimeout(() => iterateHomeworkQuestions(index + 1, TiMuList), cfg('time')); return; } if (false) { $(TiMuList[index]).find('.mark_name').html($(TiMuList[index]).find('.mark_name').html() + '' + agrs + '
'); } if (_type == 0) { var idx = optsText.findIndex(t => t == agrs); if (idx != -1) $($opts[idx]).parent().click(); } else if (_type == 1) { var ansArr = splitAnswer(agrs); $opts.each((i, t) => { if (ansArr.includes(optsText[i])) { setTimeout(() => $($opts[i]).parent().click(), 300); } }); } else if (_type == 2) { fillBlankAnswer($(TiMuList[index]), agrs, false, null); } else if (_type == 3) { selectPCJudge($(TiMuList[index]), agrs); } else if (_type == 4) { fillShortAnswer($(TiMuList[index]), agrs, false, null); } log(' 作业第' + (index + 1) + '题自动答题成功', 'green'); setTimeout(() => iterateHomeworkQuestions(index + 1, TiMuList), cfg('time')); }).catch((err) => { if (err === 'NO_API_KEY' || err === 'TOKEN_EXPIRED' || err === '凭证刷新失败') { log(' 未登录,停止答题。请先登录后再使用', 'red'); isProcessing = false; return; } log(' 作业第' + (index + 1) + '题获取答案失败,跳过', 'orange'); setTimeout(() => iterateHomeworkQuestions(index + 1, TiMuList), cfg('time')); }); } // ========== 考试处理 ========== function processExamPage() { if (!checkApiKeyBeforeAction('考试自动答题')) return; var $_examtable = $('.mark_table').find('.whiteDiv'); var _questionFull = sanitizeText($_examtable.find('h3.mark_name').html().trim()); var qTypeName = _questionFull.match(/[(](.*?),.*?分[)]|$/)[1]; var typeMap = { '单选题': 0, '多选题': 1, '填空题': 2, '判断题': 3, '简答题': 4 }; var _qType = typeMap[qTypeName]; var cleanedQ = sanitizeQuestion(_questionFull.replace(/[(].*?分[)]/, '').replace(/^\s*/, '')); var $_ansdom = $_examtable.find('#submitTest').find('.stem_answer'); if (_qType === undefined) { var $opts = $_ansdom.find('.clearfix.answerBg .fl.answer_p'); if ($opts.length) { _qType = $_ansdom.find('input[type="checkbox"]').length ? 1 : 0; } else if ($_ansdom.find('.Answer .divText .subEditor textarea').length) { _qType = 4; } } var checkAnswered = function() { if (_qType == 0 || _qType == 1) { var $opts = $_ansdom.find('.clearfix.answerBg .fl.answer_p'); for (var i = 0; i < $opts.length; i++) { if ($($opts[i]).parent().find('span').attr('class') && $($opts[i]).parent().find('span').attr('class').indexOf('check_answer') != -1) { return true; } } } else if (_qType == 2) { var $inputs = $_ansdom.find('.Answer .divText .subEditor textarea'); if ($inputs.length && $inputs.val() && $inputs.val().trim() !== '') return true; } else if (_qType == 4) { var $ta = $_ansdom.find('.subEditor textarea'); if ($ta.length && $ta.val() && $ta.val().trim() !== '') return true; } return false; }; if (!_redoMode && checkAnswered()) { log(' 此题已作答,跳过', 'green'); examNextQuestion(); return; } var $opts = []; var optsText = []; var optionsData = []; var pureQuestion = cleanedQ; if (_qType == 0 || _qType == 1) { $opts = $_ansdom.find('.clearfix.answerBg .fl.answer_p'); $opts.each(function() { optsText.push(sanitizeText($(this).html())); }); optionsData = optsText; } else if (_qType == 2) { var blankCount = getBlankInputCount($_ansdom); optionsData = [blankCount.toString()]; log(' 考试填空题传入空数量: ' + blankCount, 'blue'); } else if (_qType == 4) { optionsData = ["1"]; } else if (_qType == 3) { optionsData = ["对", "错"]; } getAnswer(_qType, pureQuestion, optionsData).then((agrs) => { if (agrs == '暂无答案' || !agrs) { log(' 考试此题无法获取答案,跳过', 'orange'); setTimeout(() => examNextQuestion(), cfg('time')); return; } if (false) { $_examtable.find('h3.mark_name').html($_examtable.find('h3.mark_name').html() + ' ' + agrs + ''); } if (_qType == 0) { var idx = optsText.findIndex(t => t == agrs); if (idx != -1) { var $target = $($opts[idx]).parent(); if (false) { $target.find('span').css('font-weight', 'bold'); } else { $target.click(); } } } else if (_qType == 1) { var ansArr = splitAnswer(agrs); $opts.each((i, t) => { if (ansArr.includes(optsText[i])) { if (false) { $($opts[i]).parent().find('span').css('font-weight', 'bold'); } else { setTimeout(() => $($opts[i]).parent().click(), 300); } } }); } else if (_qType == 2) { fillBlankAnswer($_ansdom, agrs, false, null); } else if (_qType == 3) { selectExamJudge($_ansdom, agrs, false); } else if (_qType == 4) { fillShortAnswer($_ansdom, agrs, false, null); } log(' 考试自动答题成功', 'green'); setTimeout(() => examNextQuestion(), cfg('time')); }).catch((err) => { if (err === 'NO_API_KEY' || err === 'TOKEN_EXPIRED' || err === '凭证刷新失败') { log(' 未登录,停止答题。请先登录后再使用', 'red'); isProcessing = false; return; } log(' 考试此题获取答案失败,跳过', 'orange'); setTimeout(() => examNextQuestion(), cfg('time')); }); } function selectExamJudge($container, answer) { var trueList = ['正确', '是', '对', '√', 'T', 'true', 'ri', 'right']; var ansLower = (answer || '').trim().toLowerCase(); var isTrue = trueList.some(function(k) { return ansLower === k.toLowerCase() || ansLower.indexOf(k.toLowerCase()) !== -1; }); var $pdContainer = $container.find('.clearfix.answerBg .fl.answer_p').parent(); if (isTrue) { if ($pdContainer.length > 0) { $($pdContainer[0]).click(); log(' 考试判断题选择: 正确 (接口返回: ' + answer + ')', 'green'); } } else { if ($pdContainer.length > 1) { $($pdContainer[1]).click(); log(' 考试判断题选择: 错误 (接口返回: ' + answer + ')', 'green'); } else if ($pdContainer.length === 1) { $($pdContainer[0]).click(); log(' 考试判断题选择: 错误(唯一选项) (接口返回: ' + answer + ')', 'green'); } } } function examNextQuestion() { if (cfg('examTurn')) { var $nextbtn = $('.mark_table .whiteDiv .nextDiv a.jb_btn'); var delay = cfg('examTurnTime') ? 2000 + (Math.floor(Math.random() * 5 + 1) * 1000) : 2000; setTimeout(() => $nextbtn.click(), delay); log('⏭️ ' + (delay/1000) + '秒后自动跳转下一题', 'blue'); } } // ========== 字体解密 ========== function b64ToBytes(base64) { var data = window.atob(base64); var buf = new Uint8Array(data.length); for (var i = 0; i < data.length; i++) buf[i] = data.charCodeAt(i); return buf; } function deobfuscateFont($context) { if (cfg('decrypt') !== 1) return; try { if (!TyprInstance || !TyprInstance.U || !TyprInstance.parse) { try { if (typeof unsafeWindow !== 'undefined' && unsafeWindow.Typr) { TyprInstance = unsafeWindow.Typr; } else if (typeof window !== 'undefined' && window.Typr) { TyprInstance = window.Typr; } else if (typeof Typr !== 'undefined') { TyprInstance = Typr; } else if (typeof unsafeWindow !== 'undefined' && unsafeWindow.TyprMd5) { TyprInstance = unsafeWindow.TyprMd5; } else if (typeof TyprMd5 !== 'undefined') { TyprInstance = TyprMd5; } } catch(e) {} if (!TyprInstance || !TyprInstance.U) { log('⏳ 等待字体解密库加载...', 'blue'); setTimeout(deobfuscateFont, 1000); return; } } // 确定搜索上下文:默认为当前页面,也可以传入iframe的$contents var $searchCtx = $context || $(document); var $tip = $searchCtx.find('style:contains(font-cxsecret)'); // 如果是当前页面调用且没有context,也尝试顶层选择器 if (!$tip.length && !$context) { $tip = $('style:contains(font-cxsecret)'); } if (!$tip.length) { return; } var styleText = $tip.text(); var fontBase64 = null; var match1 = styleText.match(/base64,([\w\W]+?)'/); if (match1 && match1[1]) fontBase64 = match1[1]; if (!fontBase64) { var match2 = styleText.match(/url\("data:application\/font-woff;charset=utf-8;base64,([\w\W]+?)"\)/); if (match2 && match2[1]) fontBase64 = match2[1]; } if (!fontBase64) { var match3 = styleText.match(/url\('data:application\/font-woff;charset=utf-8;base64,([\w\W]+?)'\)/); if (match3 && match3[1]) fontBase64 = match3[1]; } if (!fontBase64) return; var fontUint8Array = b64ToBytes(fontBase64); var parsedFont = TyprInstance.parse(fontUint8Array); if (!parsedFont || parsedFont.length === 0) return; var font = parsedFont[0]; var tableJson = GM_getResourceText('Table'); if (!tableJson) return; var table; try { table = JSON.parse(tableJson); } catch(e) { return; } // 分片异步解密:每批处理 500 个码点,避免长时间阻塞主线程导致页面卡顿 var CHUNK_SIZE = 500; var START_CODE = 19968; var END_CODE = 40870; var charMatch = {}; var totalMatchCount = 0; function processChunk(from) { var to = Math.min(from + CHUNK_SIZE, END_CODE); for (var i = from; i < to; i++) { var glyph = TyprInstance.U.codeToGlyph(font, i); if (!glyph) continue; var path = TyprInstance.U.glyphToPath(font, glyph); if (!path) continue; var pathStr = JSON.stringify(path); var hash = md5(pathStr); if (!hash) continue; var hashKey = hash.slice(24); var realChar = table[hashKey]; if (realChar !== undefined && realChar !== null && realChar !== 0) { charMatch[i] = realChar; totalMatchCount++; } } if (to < END_CODE) { // 还有剩余码点,让出主线程后继续处理下一批 setTimeout(function() { processChunk(to); }, 0); } else { // 所有码点处理完毕,执行 DOM 替换 applyDecryptedChars(); } } function applyDecryptedChars() { if (totalMatchCount === 0) return; var $elements = $searchCtx.find('.font-cxsecret'); if (!$context) { $elements = $elements.add($('.font-cxsecret')); } if ($elements.length === 0) return; $elements.each(function() { var $el = $(this); var html = $el.html(); if (!html) return; for (var code in charMatch) { var encryptedChar = String.fromCharCode(parseInt(code)); var realChar = String.fromCharCode(charMatch[code]); var escapedChar = encryptedChar.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); var regex = new RegExp(escapedChar, 'g'); html = html.replace(regex, realChar); } if ($el.html() !== html) { $el.html(html); $el.removeClass('font-cxsecret'); } }); log(' 字体解密完成!已还原 ' + totalMatchCount + ' 个字符' + ($context ? ' (iframe内)' : ''), 'green'); } // 启动分片处理 processChunk(START_CODE); } catch(e) { log(' 字体解密失败: ' + e.message, 'red'); } } // ========== iframe内字体解密(供答题前调用)========== function deobfuscateFontInFrame($iframeContents) { if (cfg('decrypt') !== 1) return; if (!$iframeContents || !$iframeContents.length) return; try { var $secretElements = $iframeContents.find('.font-cxsecret'); if ($secretElements.length === 0) return; log(' 检测到iframe内有 ' + $secretElements.length + ' 个加密字体元素,开始解密...', 'blue'); deobfuscateFont($iframeContents); } catch(e) { log(' iframe字体解密异常: ' + e.message, 'orange'); } } // ========== 认证面板渲染 ========== function renderAuthPanel(targetDoc) { var $auth = $('#authContent', targetDoc); if (!$auth.length) return; if (isLoggedIn()) { var info = getUserInfo(); var username = info ? info.username : '用户'; var remaining = info ? (info.remainingCount || 0) : 0; var freeCount = info ? (info.freeCount || 0) : 0; $auth.html( '